From 677052683803f547a148593e59dbccb8bd31f45f Mon Sep 17 00:00:00 2001 From: Mirek Fidler Date: Mon, 26 Jan 2026 11:37:18 +0100 Subject: [PATCH] CtrlCore: RTF parser now imports monospace fonts as Font::MONOSPACE and imports anchored images too, ide: Markdown export tool --- uppsrc/CtrlCore/ParseRTF.cpp | 26 ++-- uppsrc/ide/Browser/TopicWin.cpp | 4 + uppsrc/ide/Common/Common.h | 2 + uppsrc/ide/Designers/Designers.lay | 6 + uppsrc/ide/Designers/Designers.upp | 4 +- uppsrc/ide/Designers/Qtf.cpp | 4 + uppsrc/ide/Designers/export_md.cpp | 188 +++++++++++++++++++++++++++++ 7 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 uppsrc/ide/Designers/Designers.lay create mode 100644 uppsrc/ide/Designers/export_md.cpp diff --git a/uppsrc/CtrlCore/ParseRTF.cpp b/uppsrc/CtrlCore/ParseRTF.cpp index dc3ddf85c..60c599ffa 100644 --- a/uppsrc/CtrlCore/ParseRTF.cpp +++ b/uppsrc/CtrlCore/ParseRTF.cpp @@ -200,7 +200,8 @@ RTFParser::Cell::Cell() RTFParser::RTFParser(const char *rtf) : rtf(rtf) { -#ifdef _DEBUG0 +#ifdef _DEBUG + _DBG_ SaveFile(ConfigFile("rtfparser.rtf"), rtf); LOG(rtf); #endif @@ -543,6 +544,13 @@ bool RTFParser::PassEndGroup(int level) { if(Token() == T_EOF) return true; + if(PassQ("pict")) { // insert anchored images too + Flush(false, 1); + ReadPict(); + Flush(false, 1); + para.part.Clear(); + output.Cat(para); + } if(token != T_END_GROUP) return false; is_full = false; @@ -671,6 +679,7 @@ void RTFParser::ReadFaceTable() Face n; n.face = Font::ARIAL; n.charset = default_charset; + String facename; while(!PassEndGroup()) { if(PassCmd("f")) fx = command_arg; @@ -716,13 +725,9 @@ void RTFParser::ReadFaceTable() case 255: n.charset = CHARSET_WIN1252; break; // OEM } } -/* else if(PassText()) { - String s = FromUnicode(text, charset); - if(!s.IsEmpty() && *s.Last() == ';') - s.Trim(s.GetLength() - 1); - if(!s.IsEmpty()) - f = Font::FindFaceNameIndex(s); - } + else if(PassText()) + facename = text.ToString(); + /* else if(PassGroup()) { int level = Level(); if(PassCmd("falt") && PassText() && f < 0) @@ -733,7 +738,10 @@ void RTFParser::ReadFaceTable() Skip(); } if(fx >= 0 && fx < MAX_FONTS) { -// if(f < 0) // Cxl 2005-11-29 + facename.TrimEnd(";"); + int fi = Font::FindFaceNameIndex(facename); + if(fi >= 0 && (Font::GetFaceInfo(fi) & Font::FIXEDPITCH)) + n.face = Font::MONOSPACE; if(default_font == fx) { plain_format.Face(n.face); plain_charset = n.charset; diff --git a/uppsrc/ide/Browser/TopicWin.cpp b/uppsrc/ide/Browser/TopicWin.cpp index 7aeb32d32..415206ccd 100644 --- a/uppsrc/ide/Browser/TopicWin.cpp +++ b/uppsrc/ide/Browser/TopicWin.cpp @@ -280,6 +280,10 @@ void TopicEditor::EditMenu(Bar& bar) .Check(allfonts); bar.Separator(); bar.Add("Table", THISBACK(TableMenu)); + bar.Separator(); + bar.Add("Export as GitHub Markdown", [=] { + ExportMarkdown(editor.GetQTF()); + }); } void TopicEditor::FormatMenu(Bar& bar) diff --git a/uppsrc/ide/Common/Common.h b/uppsrc/ide/Common/Common.h index 9aa2254d0..051f47613 100644 --- a/uppsrc/ide/Common/Common.h +++ b/uppsrc/ide/Common/Common.h @@ -116,4 +116,6 @@ void QTFEdit(String& text); void IdeHelpButton(Button& help, const String& link); +void ExportMarkdown(const char *qtf); + #endif diff --git a/uppsrc/ide/Designers/Designers.lay b/uppsrc/ide/Designers/Designers.lay new file mode 100644 index 000000000..bca2c8359 --- /dev/null +++ b/uppsrc/ide/Designers/Designers.lay @@ -0,0 +1,6 @@ +LAYOUT(ExportMDLayout, 400, 488) + ITEM(Upp::Label, dv___0, SetLabel(t_("\001[g Markdown text copied! Since Markdown doesn't natively embed images, please use the table to manually copy images for their respective placeholders (IMAGE:N).")).SetVAlign(Upp::ALIGN_TOP).LeftPosZ(160, 236).TopPosZ(4, 88)) + ITEM(Upp::ArrayCtrl, list, LeftPosZ(4, 150).TopPosZ(4, 480)) + ITEM(Upp::Button, exit, SetLabel(t_("Close")).LeftPosZ(332, 64).TopPosZ(460, 24)) +END_LAYOUT + diff --git a/uppsrc/ide/Designers/Designers.upp b/uppsrc/ide/Designers/Designers.upp index e07ddaf1d..e7b9bce09 100644 --- a/uppsrc/ide/Designers/Designers.upp +++ b/uppsrc/ide/Designers/Designers.upp @@ -14,5 +14,7 @@ file TreeDes.cpp, Xml.cpp, Json.cpp, - md.cpp; + md.cpp, + export_md.cpp, + Designers.lay; diff --git a/uppsrc/ide/Designers/Qtf.cpp b/uppsrc/ide/Designers/Qtf.cpp index 3cf88faf2..186e1b56b 100644 --- a/uppsrc/ide/Designers/Qtf.cpp +++ b/uppsrc/ide/Designers/Qtf.cpp @@ -49,6 +49,10 @@ void IdeQtfDes::Save() void IdeQtfDes::EditMenu(Bar& menu) { EditTools(menu); + menu.Separator(); + menu.Add("Export as GitHub Markdown", [=] { + ExportMarkdown(GetQTF()); + }); } void IdeQtfDes::Serialize(Stream& s) diff --git a/uppsrc/ide/Designers/export_md.cpp b/uppsrc/ide/Designers/export_md.cpp new file mode 100644 index 000000000..4e91d794f --- /dev/null +++ b/uppsrc/ide/Designers/export_md.cpp @@ -0,0 +1,188 @@ +#include "Designers.h" + +#define LAYOUTFILE +#include + +struct ExportMD : WithExportMDLayout { + String md; + Vector img; + + void Export(const RichPara& p); + void Export(const RichTable& table, const RichStyles& styles); + void Export(const RichText& txt); + + static bool IsPreformatted(const RichPara& p); + + void Do(const char *qtf); + + ExportMD(); +}; + +ExportMD::ExportMD() +{ + CtrlLayoutExit(*this, "Export as GitHub Markdown"); +} + +void ExportMD::Export(const RichPara& p) +{ + if(p.format.ruler) + md << "---"; + if(p.format.bullet) + md << "- "; + for(int i = 0; i < p.part.GetCount(); i++) { + const RichPara::Part& part = p.part[i]; + int q; + if(part.object) { + md << "IMAGE:" << img.GetCount(); + img << part.object; + } + else { + const wchar *s = part.text; + + while(*s == ' ') { + md << ' '; + s++; + } + + String endtag; + if(part.format.sscript == 1) { + md << ""; + endtag = "" + endtag; + } + if(part.format.sscript == 2) { + md << ""; + endtag = "" + endtag; + } + if(part.format.IsUnderline()) { + md << ""; + endtag = "" + endtag; + } + if(part.format.IsBold()) { + if(!md.TrimEnd("**")) + md << "**"; + endtag = "**" + endtag; + } + if(part.format.IsItalic()) { + md << "_"; + endtag = "_" + endtag; + } + if(part.format.IsStrikeout()) { + md << "~~"; + endtag = "~~" + endtag; + } + + while(*s) { + auto NeedsEscape = [](int c) { return strchr("|[]{}^*_<>~-`'\"", c); }; + if(*s == '\\' && NeedsEscape(s[1])) + md << "\\\\" << (char)*++s; + else + if(NeedsEscape(*s)) + md << "\\" << (char)*s; + else + md << ToUtf8(*s); + s++; + } + + if(endtag.GetCount()) { + String t; + while(md.TrimEnd(" ")) + t << ' '; + md << endtag << t; + } + } + } +} + +void ExportMD::Export(const RichTable& table, const RichStyles& styles) +{ + for(int i = 0; i < table.GetRows(); i++) { + for(int j = 0; j < table.GetColumns(); j++) { + md << "|"; + const RichTxt& txt = table.Get(i, j); + for(int i = 0; i < txt.GetPartCount(); i++) + if(txt.IsPara(i)) + Export(txt.Get(i, styles)); + } + md << "|\n"; + if(i == 0) { + for(int j = 0; j < table.GetColumns(); j++) + md << "|-"; + md << "|\n"; + } + } + md << "\n\n"; +} + +bool ExportMD::IsPreformatted(const RichPara& p) +{ + bool b = false; + for(int i = 0; i < p.part.GetCount(); i++) { + const RichPara::Part& part = p.part[i]; + if(part.object || !(part.format.GetFaceInfo() & Font::FIXEDPITCH)) + return false; + b = b || part.text.GetCount(); + } + return b; +} + +void ExportMD::Export(const RichText& txt) +{ + int i = 0; + while(i < txt.GetPartCount()) + if(txt.IsPara(i)) { + const RichPara& p = txt.Get(i++); + if(IsPreformatted(p)) { + md << "```\n"; + md << ToUtf8(p.GetText()) << '\n'; + while(i < txt.GetPartCount() && txt.IsPara(i)) { + const RichPara& p = txt.Get(i); + if(!IsPreformatted(p)) + break; + i++; + md << ToUtf8(p.GetText()) << '\n'; + } + md << "```\n"; + } + else { + Export(p); + md << "\n\n"; + } + } + else + if(txt.IsTable(i)) + Export(txt.GetTable(i++), txt.GetStyles()); + else + i++; +} + +void ExportMD::Do(const char *qtf) +{ + Export(ParseQTF(qtf)); + WriteClipboardText(md); + if(img.GetCount()) { + list.SetLineCy(DPI(28)); + list.AddColumn("Image").Ctrls([&](int ii, One& ctrl) { + Button& b = ctrl.Create