mirror of
https://github.com/ultimatepp/ultimatepp.git
synced 2026-05-15 22:02:49 -06:00
What's changed: - Added line spacing 1.15 for paragraph and list elements - Added line spacing 1.5 for headers - Change code size from 1 to 2 (previously code was very small)
419 lines
10 KiB
C++
419 lines
10 KiB
C++
#include "Markdown.h"
|
|
|
|
#define LLOG(x) // DLOG("MarkdownConverter: " << x)
|
|
|
|
namespace Upp {
|
|
|
|
class sMarkdownContext : NoCopy {
|
|
struct Block
|
|
{
|
|
MD_BLOCKTYPE type;
|
|
String text;
|
|
Value data;
|
|
int level;
|
|
Block *parent;
|
|
Array<Block> children;
|
|
Block(MD_BLOCKTYPE t, Block *p);
|
|
};
|
|
|
|
enum NestedBlockPosition {
|
|
BLOCK_IN_OLIST = 1,
|
|
BLOCK_IN_ULIST = 2,
|
|
BLOCK_IN_QUOTE = 4,
|
|
BLOCK_IN_THEAD = 8,
|
|
BLOCK_IN_TBODY = 16
|
|
};
|
|
|
|
Array<Block> document;
|
|
Block* current_block;
|
|
int current_level;
|
|
|
|
String Compose(const Array<Block>& doc, int data = 0, bool notext = false, dword flags = 0) const;
|
|
|
|
public:
|
|
void BeginBlock(MD_BLOCKTYPE type, void *detail);
|
|
void EndBlock(MD_BLOCKTYPE type, void *detail);
|
|
|
|
String ToQtf() { return Compose(document); }
|
|
|
|
sMarkdownContext& operator<<(const String& s) { ASSERT(current_block); current_block->text << s; return *this; }
|
|
sMarkdownContext& operator<<(const char *s) { ASSERT(current_block); current_block->text << s; return *this; }
|
|
|
|
sMarkdownContext()
|
|
: current_block(nullptr)
|
|
, current_level(0)
|
|
, no_text(false)
|
|
{}
|
|
|
|
VectorMap<String, String> images;
|
|
bool no_text; // Image alt text is propagated as normal text. This is to suppress it.
|
|
};
|
|
|
|
sMarkdownContext::Block::Block(MD_BLOCKTYPE t, Block *p)
|
|
: type(t)
|
|
, level(0)
|
|
, parent(p)
|
|
{
|
|
}
|
|
|
|
void sMarkdownContext::BeginBlock(MD_BLOCKTYPE type, void *detail)
|
|
{
|
|
if(!current_block) {
|
|
current_block = &document.Create<Block>(type, current_block);
|
|
}
|
|
else {
|
|
current_block = ¤t_block->children.Create<Block>(type, current_block);
|
|
}
|
|
|
|
switch(type) {
|
|
case MD_BLOCK_UL:
|
|
current_block->data = reinterpret_cast<MD_BLOCK_UL_DETAIL*>(detail)->mark;
|
|
current_block->level = ++current_level;
|
|
break;
|
|
case MD_BLOCK_OL:
|
|
current_block->data = '1';
|
|
current_block->level = ++current_level;
|
|
break;
|
|
case MD_BLOCK_H:
|
|
current_block->data = (int) reinterpret_cast<MD_BLOCK_H_DETAIL*>(detail)->level;
|
|
current_block->level = current_level;
|
|
break;
|
|
default:
|
|
current_block->level = current_level;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void sMarkdownContext::EndBlock(MD_BLOCKTYPE type, void *detail)
|
|
{
|
|
if(findarg(type, MD_BLOCK_UL, MD_BLOCK_OL) >= 0)
|
|
--current_level;
|
|
|
|
if(current_block)
|
|
current_block = current_block->parent;
|
|
}
|
|
|
|
String sMarkdownContext::Compose(const Array<Block>& doc, int data, bool notext, dword flags) const
|
|
{
|
|
// TODO:
|
|
// 1) Refactor this method.
|
|
// 2) Make certain block styles and page properties (e.g. margins, indentation, etc.) configurable.
|
|
|
|
String txt;
|
|
|
|
for(int i = 0; i < doc.GetCount(); i++) {
|
|
const Block& b = doc[i];
|
|
switch(b.type) {
|
|
case MD_BLOCK_DOC:
|
|
{
|
|
txt << "[G2 "
|
|
<< Compose(b.children, data, false, flags)
|
|
<< "&]";
|
|
break;
|
|
}
|
|
case MD_BLOCK_HR:
|
|
{
|
|
txt << "[H1;L0;h(220.220.220) &]";
|
|
break;
|
|
}
|
|
case MD_BLOCK_H:
|
|
{
|
|
txt << "[*;a20;b20;pht300;"
|
|
<< clamp(6 - b.data.To<int>(), 1, 6)
|
|
<< " "
|
|
<< b.text
|
|
<< "&]";
|
|
break;
|
|
}
|
|
case MD_BLOCK_UL:
|
|
{
|
|
txt << Compose(b.children, b.data, false, flags|BLOCK_IN_ULIST);
|
|
break;
|
|
}
|
|
case MD_BLOCK_OL:
|
|
{
|
|
txt << "[N!;wt300; "
|
|
<< Compose(b.children, b.data, false, flags|BLOCK_IN_OLIST)
|
|
<< "&]";
|
|
break;
|
|
}
|
|
case MD_BLOCK_LI:
|
|
{
|
|
bool q = b.text.IsEmpty();
|
|
if(!q) {
|
|
txt << "[b20;l"
|
|
<< b.level * ((flags & BLOCK_IN_QUOTE) ? 100 : 200)
|
|
<< ";i200;"
|
|
<< decode(data, '*', "OO ", '-', "O1 ", '+', "O2 ", "N1 ")
|
|
<< b.text
|
|
<< "&]";
|
|
}
|
|
txt << Compose(b.children, data, q, flags);
|
|
break;
|
|
}
|
|
case MD_BLOCK_P:
|
|
{
|
|
if(!b.level) {
|
|
txt << "[b20;wt300 " << b.text << " &]";
|
|
}
|
|
else
|
|
if(notext && !i) {
|
|
txt << "[b20;wt300;l"
|
|
<< b.level * ((flags & BLOCK_IN_QUOTE) ? 20 : 200)
|
|
<< ";i200;"
|
|
<< decode(data, '*', "OO ", '-', "O1 ", '+', "O2 ", "N1 ")
|
|
<< b.text
|
|
<< "&]";
|
|
}
|
|
else {
|
|
txt << "[b20;wt300;l"
|
|
<< b.level * ((flags & BLOCK_IN_QUOTE) ? 40 : 400)
|
|
<< ";O_ "
|
|
<< b.text
|
|
<< "&]";
|
|
}
|
|
break;
|
|
}
|
|
case MD_BLOCK_CODE:
|
|
case MD_BLOCK_HTML: // Treat HTML as code block....
|
|
{
|
|
txt << "{{10000;A20;B20;<"
|
|
<< b.level * ((flags & BLOCK_IN_QUOTE) ? 10 : 400)
|
|
<< ";@(250.250.250);F(230.230.230) [i10;C;2;@5;< "
|
|
<< b.text
|
|
<< " ]}}&";
|
|
break;
|
|
}
|
|
case MD_BLOCK_QUOTE:
|
|
{
|
|
txt << "{{1:500;G4;g20;F0;f0;<"
|
|
<< b.level * ((flags & BLOCK_IN_QUOTE) ? 10 : 400)
|
|
<< ((flags & BLOCK_IN_QUOTE) ? "" : ";A20;B20")
|
|
<< " :: [i10;<; "
|
|
<< Nvl(b.text, "")
|
|
<< Compose(b.children, data, false, flags|BLOCK_IN_QUOTE)
|
|
<< " ]}}&";
|
|
break;
|
|
}
|
|
case MD_BLOCK_TABLE:
|
|
{
|
|
txt << "{{"
|
|
<< Compose(b.children, data, false, flags)
|
|
<< "}}&";
|
|
break;
|
|
}
|
|
case MD_BLOCK_THEAD:
|
|
{
|
|
txt << Compose(b.children, data, false, flags|BLOCK_IN_THEAD);
|
|
break;
|
|
}
|
|
case MD_BLOCK_TBODY:
|
|
{
|
|
txt << Compose(b.children, data, false, flags|BLOCK_IN_TBODY);
|
|
break;
|
|
}
|
|
case MD_BLOCK_TR:
|
|
{
|
|
int n = b.children.GetCount();
|
|
if(flags & BLOCK_IN_THEAD) {
|
|
for(int j = 0; j < n; j++) {
|
|
txt << '1';
|
|
if(j < n - 1)
|
|
txt << ':';
|
|
else {
|
|
txt << "A20;B20;";
|
|
txt << "@(220.225.230);G(220.220.220);<"
|
|
<< b.level * ((flags & BLOCK_IN_QUOTE) ? 10 : 200)
|
|
<< " ";
|
|
}
|
|
}
|
|
}
|
|
for(int j = 0; j < n; j++) {
|
|
const Block& bb = b.children[j];
|
|
if(j == 0 && bb.type == MD_BLOCK_TD)
|
|
txt << "::@2 ";
|
|
txt << bb.text;
|
|
if(j < n - 1)
|
|
txt << "||";
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
//txt << b.text;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return txt;
|
|
}
|
|
|
|
static String sDeQtfMd(const char *s)
|
|
{
|
|
// Here we duplicate DeQtf() function, because we don't want to bring in the RichText package.
|
|
|
|
StringBuffer r;
|
|
for(; *s; s++) {
|
|
if(*s == '\n')
|
|
r.Cat('&');
|
|
else {
|
|
if((byte) *s > ' ' && !IsDigit(*s) && !IsAlpha(*s) && (byte) *s < 128)
|
|
r.Cat('`');
|
|
r.Cat(*s);
|
|
}
|
|
}
|
|
return String(r); // Make compilers happy...
|
|
}
|
|
|
|
static int sParseMdObjects(MD_PARSER& parser, const String& txt, void *ctx)
|
|
{
|
|
// NOP (ATM...)
|
|
parser.enter_block = [](MD_BLOCKTYPE type, void *detail, void *udata) -> int { return 0; };
|
|
parser.leave_block = [](MD_BLOCKTYPE type, void *detail, void *udata) -> int { return 0; };
|
|
parser.leave_span = [](MD_SPANTYPE type, void *detail, void *udata) -> int { return 0; };
|
|
parser.text = [](MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void* udata) -> int { return 0; };
|
|
|
|
parser.enter_span = [](MD_SPANTYPE type, void *detail, void *udata) -> int
|
|
{
|
|
auto& ctx = *reinterpret_cast<sMarkdownContext*>(udata);
|
|
|
|
if(type == MD_SPAN_IMG) // Handle images
|
|
{
|
|
auto *q = reinterpret_cast<MD_SPAN_IMG_DETAIL*>(detail);
|
|
String url(q->src.text, q->src.size);
|
|
ctx.images.Add(url);
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
int rc = md_parse((const MD_CHAR*)~txt, (MD_SIZE) txt.GetLength(), &parser, ctx);
|
|
return rc;
|
|
}
|
|
|
|
static int sParseMdDocument(MD_PARSER& parser, const String& txt, void *ctx)
|
|
{
|
|
parser.enter_block = [](MD_BLOCKTYPE type, void *detail, void *udata) -> int
|
|
{
|
|
reinterpret_cast<sMarkdownContext*>(udata)->BeginBlock(type, detail);
|
|
return 0;
|
|
};
|
|
|
|
parser.leave_block = [](MD_BLOCKTYPE type, void *detail, void *udata) -> int
|
|
{
|
|
reinterpret_cast<sMarkdownContext*>(udata)->EndBlock(type, detail);
|
|
return 0;
|
|
};
|
|
|
|
parser.enter_span = [](MD_SPANTYPE type, void *detail, void *udata) -> int
|
|
{
|
|
auto& ctx = *reinterpret_cast<sMarkdownContext*>(udata);
|
|
|
|
switch(type) {
|
|
case MD_SPAN_A:
|
|
{
|
|
auto *q = reinterpret_cast<MD_SPAN_A_DETAIL*>(detail);
|
|
ctx << "[^" << String(q->href.text, q->href.size) << "^ ";
|
|
break;
|
|
}
|
|
case MD_SPAN_IMG:
|
|
{
|
|
auto *q = reinterpret_cast<MD_SPAN_IMG_DETAIL*>(detail);
|
|
String url(q->src.text, q->src.size);
|
|
int i = ctx.images.Find(url);
|
|
if(i >= 0 && !IsNull(ctx.images[i])) {
|
|
ctx.no_text = true;
|
|
ctx << ctx.images[i];
|
|
}
|
|
else {
|
|
ctx.no_text = false;
|
|
ctx << "[^" << url << "^ ";
|
|
}
|
|
break;
|
|
}
|
|
case MD_SPAN_WIKILINK:
|
|
{
|
|
auto *q = reinterpret_cast<MD_SPAN_WIKILINK_DETAIL*>(detail);
|
|
ctx << "[^" << String(q->target.text, q->target.size) << "^ ";
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
ctx << decode(type,
|
|
MD_SPAN_U, "[_ ",
|
|
MD_SPAN_EM, "[/ ",
|
|
MD_SPAN_DEL, "[- ",
|
|
MD_SPAN_CODE, "[C;@5;$(245.245.245) ",
|
|
MD_SPAN_STRONG, "[* ", "");
|
|
break;
|
|
}}
|
|
return 0;
|
|
};
|
|
|
|
parser.leave_span = [](MD_SPANTYPE type, void *detail, void *udata) -> int
|
|
{
|
|
auto& ctx = *reinterpret_cast<sMarkdownContext*>(udata);
|
|
|
|
if(type == MD_SPAN_IMG) {
|
|
if(!ctx.no_text) {
|
|
ctx << " ]";
|
|
}
|
|
ctx.no_text = false;
|
|
}
|
|
else
|
|
if(findarg(type,
|
|
MD_SPAN_A,
|
|
MD_SPAN_U,
|
|
MD_SPAN_EM,
|
|
MD_SPAN_DEL,
|
|
MD_SPAN_CODE,
|
|
MD_SPAN_STRONG,
|
|
MD_SPAN_WIKILINK) >= 0)
|
|
ctx << "]";
|
|
|
|
return 0;
|
|
};
|
|
|
|
parser.text = [](MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void* udata) -> int
|
|
{
|
|
auto& ctx = *reinterpret_cast<sMarkdownContext*>(udata);
|
|
if(!ctx.no_text)
|
|
ctx << decode(type,
|
|
MD_TEXT_NULLCHAR, "?",
|
|
MD_TEXT_BR, "&",
|
|
MD_TEXT_SOFTBR, " ", // TODO: See if there is a way to properly imitate this in qtf...
|
|
(const char*) ~sDeQtfMd(String((const char*) text, size)));
|
|
return 0;
|
|
};
|
|
|
|
int rc = md_parse((const MD_CHAR*)~txt, (MD_SIZE) txt.GetLength(), &parser, ctx);
|
|
return rc;
|
|
}
|
|
|
|
String MarkdownConverter::ToQtf(const String& mdtext)
|
|
{
|
|
MD_PARSER parser;
|
|
parser.abi_version = 0;
|
|
parser.flags = flags;
|
|
parser.syntax = nullptr;
|
|
|
|
#ifdef _DEBUG
|
|
parser.debug_log = [](const char *msg, void *udata) -> void
|
|
{
|
|
LLOG(msg);
|
|
};
|
|
#else
|
|
parser.debug_log = nullptr;
|
|
#endif
|
|
|
|
sMarkdownContext ctx;
|
|
int rc = sParseMdObjects(parser, mdtext, &ctx); // Pass 1 (collect the image information)
|
|
if(rc == 0) {
|
|
ctx.no_text = false;
|
|
if(ctx.images.GetCount())
|
|
WhenImages(ctx.images);
|
|
rc = sParseMdDocument(parser, mdtext, &ctx); // Pass 2 (parse the document)
|
|
}
|
|
return rc ? String::GetVoid() : ctx.ToQtf();
|
|
}
|
|
|
|
}
|