CtrlCore: RTF parser now imports monospace fonts as Font::MONOSPACE and imports anchored images too, ide: Markdown export tool

This commit is contained in:
Mirek Fidler 2026-01-26 11:37:18 +01:00
parent 85c3e1af6a
commit 6770526838
7 changed files with 224 additions and 10 deletions

View file

@ -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;

View file

@ -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)

View file

@ -116,4 +116,6 @@ void QTFEdit(String& text);
void IdeHelpButton(Button& help, const String& link);
void ExportMarkdown(const char *qtf);
#endif

View file

@ -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

View file

@ -14,5 +14,7 @@ file
TreeDes.cpp,
Xml.cpp,
Json.cpp,
md.cpp;
md.cpp,
export_md.cpp,
Designers.lay;

View file

@ -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)

View file

@ -0,0 +1,188 @@
#include "Designers.h"
#define LAYOUTFILE <ide/Designers/Designers.lay>
#include <CtrlCore/lay.h>
struct ExportMD : WithExportMDLayout<TopWindow> {
String md;
Vector<RichObject> 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 << "<sup>";
endtag = "</sup>" + endtag;
}
if(part.format.sscript == 2) {
md << "<sub>";
endtag = "</sub>" + endtag;
}
if(part.format.IsUnderline()) {
md << "<ins>";
endtag = "</ins>" + 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>& ctrl) {
Button& b = ctrl.Create<Button>();
b.SetImage(CtrlImg::copy());
b.SetLabel("Copy IMAGE:" + AsString(ii));
b << [=] {
if(ii >= 0 && ii < img.GetCount()) {
RichObjectPaintInfo pi;
pi.ink = SBlack();
WriteClipboardImage(img[ii].ToImage(img[ii].GetPixelSize(), pi));
}
};
});
for(int i = 0; i < img.GetCount(); i++)
list.Add();
Run();
}
else
PromptOK("Markdown copied to clipboard.");
}
void ExportMarkdown(const char *qtf)
{
ExportMD md;
md.Do(qtf);
}