uce/src/lib/markdown.cpp

1379 lines
35 KiB
C++

DValue markdown_to_ast(String src, DValue options);
String markdown_to_html(String src, DValue options);
bool markdown_has_key(DValue& tree, String key)
{
return(tree.type == 'M' && tree._map.count(key) > 0);
}
DValue markdown_get_value(DValue& tree, String key)
{
if(markdown_has_key(tree, key))
return(tree._map[key]);
return(DValue());
}
String markdown_get_string(DValue& tree, String key, String default_value = "")
{
if(markdown_has_key(tree, key))
return(tree._map[key].to_string());
return(default_value);
}
bool markdown_truthy(DValue value, bool default_value = false)
{
switch(value.type)
{
case('B'):
return(value._bool);
case('F'):
return(value._float != 0);
case('S'):
{
String raw = to_lower(trim(value._String));
if(raw == "true" || raw == "1" || raw == "yes" || raw == "on" || raw == "(true)")
return(true);
if(raw == "false" || raw == "0" || raw == "no" || raw == "off" || raw == "(false)")
return(false);
return(default_value);
}
case('M'):
return(!value._map.empty());
default:
return(default_value);
}
}
bool markdown_get_bool(DValue& tree, String key, bool default_value = false)
{
if(markdown_has_key(tree, key))
return(markdown_truthy(tree._map[key], default_value));
return(default_value);
}
bool markdown_gfm_enabled(DValue& options)
{
return(markdown_get_bool(options, "gfm", true));
}
bool markdown_allow_html(DValue& options)
{
return(markdown_get_bool(options, "allow_html", false));
}
String markdown_get_component_target(DValue& options, String hook)
{
if(!markdown_has_key(options, "components"))
return("");
DValue components = options._map["components"];
if(!markdown_has_key(components, hook))
return("");
return(trim(components._map[hook].to_string()));
}
DValue markdown_make_node(String type)
{
DValue node;
node["type"] = type;
return(node);
}
void markdown_push(DValue& parent, DValue child)
{
parent.push(child);
}
bool markdown_is_blank(String line)
{
return(trim(line) == "");
}
s64 markdown_count_indent(String line)
{
s64 result = 0;
while(result < (s64)line.length() && line[result] == ' ')
result += 1;
return(result);
}
bool markdown_is_digits(String s)
{
if(s == "")
return(false);
for(auto c : s)
{
if(!isdigit((unsigned char)c))
return(false);
}
return(true);
}
bool markdown_is_identifier(String s)
{
if(s == "")
return(false);
for(auto c : s)
{
if(!(isalnum((unsigned char)c) || c == '_' || c == '-'))
return(false);
}
return(true);
}
String markdown_strip_newlines(String s)
{
s = replace(s, "\r\n", "\n");
s = replace(s, "\r", "\n");
return(s);
}
String markdown_trim_quotes(String s)
{
s = trim(s);
if(s.length() >= 2)
{
char first = s.front();
char last = s.back();
if((first == '"' && last == '"') || (first == '\'' && last == '\''))
return(s.substr(1, s.length() - 2));
}
return(s);
}
String markdown_read_quoted(String raw, s64& i)
{
String result = "";
if(i >= (s64)raw.length())
return(result);
char quote = raw[i];
i += 1;
while(i < (s64)raw.length())
{
char c = raw[i];
if(c == '\\' && i + 1 < (s64)raw.length())
{
result.append(1, raw[i + 1]);
i += 2;
continue;
}
if(c == quote)
{
i += 1;
return(result);
}
result.append(1, c);
i += 1;
}
return(result);
}
DValue markdown_parse_attrs(String raw, String& argument_out)
{
DValue attrs;
argument_out = "";
s64 i = 0;
while(i < (s64)raw.length())
{
while(i < (s64)raw.length() && isspace((unsigned char)raw[i]))
i += 1;
if(i >= (s64)raw.length())
break;
String token = "";
if(raw[i] == '"' || raw[i] == '\'')
{
token = markdown_read_quoted(raw, i);
if(argument_out != "")
argument_out += " ";
argument_out += token;
continue;
}
s64 token_start = i;
while(i < (s64)raw.length() && !isspace((unsigned char)raw[i]))
{
if(raw[i] == '=')
break;
i += 1;
}
token = raw.substr(token_start, i - token_start);
if(i < (s64)raw.length() && raw[i] == '=' && markdown_is_identifier(token))
{
i += 1;
String value = "";
if(i < (s64)raw.length() && (raw[i] == '"' || raw[i] == '\''))
value = markdown_read_quoted(raw, i);
else
{
s64 value_start = i;
while(i < (s64)raw.length() && !isspace((unsigned char)raw[i]))
i += 1;
value = raw.substr(value_start, i - value_start);
}
attrs[token] = markdown_trim_quotes(value);
}
else
{
if(argument_out != "")
argument_out += " ";
argument_out += token;
}
}
return(attrs);
}
StringList markdown_sorted_keys(DValue tree)
{
StringList keys;
if(tree.type != 'M')
return(keys);
for(auto& it : tree._map)
keys.push_back(it.first);
std::sort(keys.begin(), keys.end(), [] (String a, String b) {
bool a_numeric = markdown_is_digits(a);
bool b_numeric = markdown_is_digits(b);
if(a_numeric && b_numeric)
return(int_val(a) < int_val(b));
if(a_numeric != b_numeric)
return(a_numeric);
return(a < b);
});
return(keys);
}
StringList markdown_split_pipe_row(String raw)
{
StringList result;
String cell = "";
bool escaped = false;
for(auto c : raw)
{
if(escaped)
{
cell.append(1, c);
escaped = false;
continue;
}
if(c == '\\')
{
escaped = true;
continue;
}
if(c == '|')
{
result.push_back(trim(cell));
cell = "";
}
else
{
cell.append(1, c);
}
}
result.push_back(trim(cell));
if(result.size() > 0 && result.front() == "")
result.erase(result.begin());
if(result.size() > 0 && result.back() == "")
result.pop_back();
return(result);
}
bool markdown_parse_table_separator(String raw, StringList& alignments)
{
alignments.clear();
StringList cells = markdown_split_pipe_row(raw);
if(cells.size() == 0)
return(false);
for(auto cell : cells)
{
cell = trim(cell);
if(cell == "")
return(false);
String align = "";
bool left = cell.front() == ':';
bool right = cell.back() == ':';
String test = cell;
if(left)
test = test.substr(1);
if(right && test.length() > 0)
test = test.substr(0, test.length() - 1);
test = trim(test);
if(test.length() < 3)
return(false);
for(auto c : test)
{
if(c != '-')
return(false);
}
if(left && right)
align = "center";
else if(left)
align = "left";
else if(right)
align = "right";
else
align = "";
alignments.push_back(align);
}
return(true);
}
bool markdown_parse_fence(String raw, char& fence_char, s64& fence_length, String& info)
{
String trimmed = trim(raw);
if(trimmed.length() < 3)
return(false);
char c = trimmed[0];
if(c != '`' && c != '~')
return(false);
s64 i = 0;
while(i < (s64)trimmed.length() && trimmed[i] == c)
i += 1;
if(i < 3)
return(false);
fence_char = c;
fence_length = i;
info = trim(trimmed.substr(i));
return(true);
}
bool markdown_is_hr(String raw)
{
String trimmed = trim(raw);
if(trimmed.length() < 3)
return(false);
char marker = trimmed[0];
if(marker != '-' && marker != '*' && marker != '_')
return(false);
s64 mark_count = 0;
for(auto c : trimmed)
{
if(c == marker)
mark_count += 1;
else if(!isspace((unsigned char)c))
return(false);
}
return(mark_count >= 3);
}
bool markdown_parse_list_marker(String raw, bool& ordered, s64& marker_length, s64& start_number, String& content)
{
ordered = false;
marker_length = 0;
start_number = 1;
content = "";
if(raw.length() < 2)
return(false);
char first = raw[0];
if((first == '-' || first == '*' || first == '+') && raw.length() >= 2 && raw[1] == ' ')
{
ordered = false;
marker_length = 2;
content = raw.substr(2);
return(true);
}
s64 i = 0;
while(i < (s64)raw.length() && isdigit((unsigned char)raw[i]))
i += 1;
if(i == 0 || i + 1 >= (s64)raw.length())
return(false);
if((raw[i] == '.' || raw[i] == ')') && raw[i + 1] == ' ')
{
ordered = true;
start_number = int_val(raw.substr(0, i));
marker_length = i + 2;
content = raw.substr(marker_length);
return(true);
}
return(false);
}
bool markdown_parse_task_prefix(String& content, bool& checked)
{
if(content.length() < 4 || content[0] != '[' || content[2] != ']' || content[3] != ' ')
return(false);
char marker = to_lower(String().append(1, content[1]))[0];
if(marker != 'x' && marker != ' ')
return(false);
checked = (marker == 'x');
content = content.substr(4);
return(true);
}
bool markdown_parse_atx_heading(String raw, s64& level, String& content)
{
String trimmed = trim(raw);
if(trimmed == "" || trimmed[0] != '#')
return(false);
level = 0;
while(level < (s64)trimmed.length() && trimmed[level] == '#')
level += 1;
if(level == 0 || level > 6)
return(false);
if(level >= (s64)trimmed.length() || !isspace((unsigned char)trimmed[level]))
return(false);
content = trim(trimmed.substr(level));
while(content.length() > 0 && content.back() == '#')
{
String stripped = trim(content.substr(0, content.length() - 1));
if(stripped != "")
content = stripped;
else
break;
}
return(true);
}
bool markdown_parse_setext_heading(String current, String next, s64& level, String& content)
{
current = trim(current);
next = trim(next);
if(current == "" || next.length() < 3)
return(false);
bool all_equals = true;
bool all_dashes = true;
for(auto c : next)
{
if(c != '=' && !isspace((unsigned char)c))
all_equals = false;
if(c != '-' && !isspace((unsigned char)c))
all_dashes = false;
}
if(!all_equals && !all_dashes)
return(false);
level = all_equals ? 1 : 2;
content = current;
return(true);
}
bool markdown_parse_directive_open(String raw, String& name, String& argument, DValue& attrs)
{
String trimmed = trim(raw);
if(trimmed.rfind(":::", 0) != 0 || trimmed == ":::")
return(false);
String payload = trim(trimmed.substr(3));
StringList parts = split_space(payload);
if(parts.size() == 0)
return(false);
name = parts[0];
if(!markdown_is_identifier(name))
return(false);
String remainder = trim(payload.substr(name.length()));
attrs = markdown_parse_attrs(remainder, argument);
return(true);
}
bool markdown_parse_autolink(String raw, s64 start, s64& end, String& href, String& text)
{
String rest = raw.substr(start);
if(rest.rfind("https://", 0) != 0 && rest.rfind("http://", 0) != 0)
return(false);
end = start;
while(end < (s64)raw.length())
{
char c = raw[end];
if(isspace((unsigned char)c) || c == '<' || c == '>')
break;
end += 1;
}
if(end <= start)
return(false);
href = raw.substr(start, end - start);
while(href.length() > 0)
{
char last = href.back();
if(last == '.' || last == ',' || last == '!' || last == '?' || last == ';' || last == ':')
{
href.pop_back();
end -= 1;
}
else
{
break;
}
}
if(href == "")
return(false);
text = href;
return(true);
}
bool markdown_parse_link_target(String raw, String& url, String& title)
{
raw = trim(raw);
if(raw == "")
return(false);
if(raw.find(" ") == String::npos)
{
url = markdown_trim_quotes(raw);
title = "";
return(true);
}
String work = raw;
url = markdown_trim_quotes(trim(nibble(work, " ")));
title = markdown_trim_quotes(trim(work));
return(url != "");
}
DValue markdown_parse_inline(String raw, DValue options);
void markdown_flush_text(String& buffer, DValue& children)
{
if(buffer == "")
return;
DValue node = markdown_make_node("text");
node["text"] = buffer;
markdown_push(children, node);
buffer = "";
}
DValue markdown_parse_inline(String raw, DValue options)
{
DValue children;
String buffer = "";
bool gfm = markdown_gfm_enabled(options);
bool allow_html = markdown_allow_html(options);
for(s64 i = 0; i < (s64)raw.length(); i++)
{
char c = raw[i];
char c1 = (i + 1 < (s64)raw.length()) ? raw[i + 1] : '\0';
if(c == '\\' && i + 1 < (s64)raw.length())
{
buffer.append(1, raw[i + 1]);
i += 1;
continue;
}
if(c == '`')
{
s64 tick_count = 1;
while(i + tick_count < (s64)raw.length() && raw[i + tick_count] == '`')
tick_count += 1;
String ticks(tick_count, '`');
auto close_pos = raw.find(ticks, i + tick_count);
if(close_pos != String::npos)
{
markdown_flush_text(buffer, children);
DValue node = markdown_make_node("code");
node["text"] = raw.substr(i + tick_count, close_pos - (i + tick_count));
markdown_push(children, node);
i = close_pos + tick_count - 1;
continue;
}
}
if(c == '!' && c1 == '[')
{
auto label_end = raw.find("]", i + 2);
if(label_end != String::npos && label_end + 1 < (s64)raw.length() && raw[label_end + 1] == '(')
{
auto target_end = raw.find(")", label_end + 2);
if(target_end != String::npos)
{
String url = "";
String title = "";
if(markdown_parse_link_target(raw.substr(label_end + 2, target_end - (label_end + 2)), url, title))
{
markdown_flush_text(buffer, children);
DValue node = markdown_make_node("image");
node["alt"] = raw.substr(i + 2, label_end - (i + 2));
node["src"] = url;
node["title"] = title;
markdown_push(children, node);
i = target_end;
continue;
}
}
}
}
if(c == '[')
{
auto label_end = raw.find("]", i + 1);
if(label_end != String::npos && label_end + 1 < (s64)raw.length() && raw[label_end + 1] == '(')
{
auto target_end = raw.find(")", label_end + 2);
if(target_end != String::npos)
{
String url = "";
String title = "";
if(markdown_parse_link_target(raw.substr(label_end + 2, target_end - (label_end + 2)), url, title))
{
markdown_flush_text(buffer, children);
DValue node = markdown_make_node("link");
node["href"] = url;
node["title"] = title;
node["children"] = markdown_parse_inline(raw.substr(i + 1, label_end - (i + 1)), options);
markdown_push(children, node);
i = target_end;
continue;
}
}
}
}
if(gfm && c == '~' && c1 == '~')
{
auto close_pos = raw.find("~~", i + 2);
if(close_pos != String::npos)
{
markdown_flush_text(buffer, children);
DValue node = markdown_make_node("strike");
node["children"] = markdown_parse_inline(raw.substr(i + 2, close_pos - (i + 2)), options);
markdown_push(children, node);
i = close_pos + 1;
continue;
}
}
if((c == '*' && c1 == '*') || (c == '_' && c1 == '_'))
{
String delim = raw.substr(i, 2);
auto close_pos = raw.find(delim, i + 2);
if(close_pos != String::npos)
{
markdown_flush_text(buffer, children);
DValue node = markdown_make_node("strong");
node["children"] = markdown_parse_inline(raw.substr(i + 2, close_pos - (i + 2)), options);
markdown_push(children, node);
i = close_pos + 1;
continue;
}
}
if(c == '*' || c == '_')
{
String delim = raw.substr(i, 1);
auto close_pos = raw.find(delim, i + 1);
if(close_pos != String::npos)
{
markdown_flush_text(buffer, children);
DValue node = markdown_make_node("em");
node["children"] = markdown_parse_inline(raw.substr(i + 1, close_pos - (i + 1)), options);
markdown_push(children, node);
i = close_pos;
continue;
}
}
if(gfm)
{
s64 end = 0;
String href = "";
String text = "";
if(markdown_parse_autolink(raw, i, end, href, text))
{
markdown_flush_text(buffer, children);
DValue node = markdown_make_node("link");
node["href"] = href;
node["title"] = "";
DValue link_children;
DValue text_node = markdown_make_node("text");
text_node["text"] = text;
markdown_push(link_children, text_node);
node["children"] = link_children;
markdown_push(children, node);
i = end - 1;
continue;
}
}
if(allow_html && c == '<')
{
auto close_pos = raw.find(">", i + 1);
if(close_pos != String::npos)
{
String html = raw.substr(i, close_pos - i + 1);
if(html.length() >= 3)
{
markdown_flush_text(buffer, children);
DValue node = markdown_make_node("raw_html");
node["html"] = html;
markdown_push(children, node);
i = close_pos;
continue;
}
}
}
buffer.append(1, c);
}
markdown_flush_text(buffer, children);
return(children);
}
DValue markdown_parse_blocks(StringList lines, DValue options);
DValue markdown_parse_paragraph(StringList& lines, s64& idx, s64 base_indent, DValue& options)
{
StringList paragraph_lines;
while(idx < (s64)lines.size())
{
if(markdown_is_blank(lines[idx]))
break;
if(markdown_count_indent(lines[idx]) < base_indent)
break;
if(paragraph_lines.size() > 0)
{
String current = lines[idx].substr(base_indent);
s64 heading_level = 0;
String heading_content = "";
char fence_char = '\0';
s64 fence_length = 0;
String fence_info = "";
bool ordered = false;
s64 marker_length = 0;
s64 start_number = 1;
String list_content = "";
String directive_name = "";
String directive_argument = "";
DValue directive_attrs;
if(markdown_parse_atx_heading(current, heading_level, heading_content) ||
markdown_parse_fence(current, fence_char, fence_length, fence_info) ||
markdown_is_hr(current) ||
trim(current).rfind(">", 0) == 0 ||
markdown_parse_list_marker(current, ordered, marker_length, start_number, list_content) ||
markdown_parse_directive_open(current, directive_name, directive_argument, directive_attrs))
break;
if(markdown_gfm_enabled(options) && idx + 1 < (s64)lines.size() && current.find("|") != String::npos &&
markdown_count_indent(lines[idx + 1]) >= base_indent)
{
StringList alignments;
if(markdown_parse_table_separator(lines[idx + 1].substr(base_indent), alignments))
break;
}
}
paragraph_lines.push_back(lines[idx].substr(base_indent));
idx += 1;
}
DValue node = markdown_make_node("paragraph");
String text = join(paragraph_lines, "\n");
node["text"] = text;
node["children"] = markdown_parse_inline(text, options);
return(node);
}
DValue markdown_parse_list(StringList& lines, s64& idx, s64 base_indent, DValue& options)
{
bool ordered = false;
s64 marker_length = 0;
s64 start_number = 1;
String content = "";
String raw = lines[idx].substr(base_indent);
markdown_parse_list_marker(raw, ordered, marker_length, start_number, content);
DValue node = markdown_make_node("list");
node["ordered"].set_bool(ordered);
node["start"] = (f64)start_number;
while(idx < (s64)lines.size())
{
if(markdown_is_blank(lines[idx]))
break;
if(markdown_count_indent(lines[idx]) < base_indent)
break;
String line_raw = lines[idx].substr(base_indent);
bool item_ordered = false;
s64 item_marker_length = 0;
s64 item_start_number = 1;
String item_content = "";
if(!markdown_parse_list_marker(line_raw, item_ordered, item_marker_length, item_start_number, item_content) || item_ordered != ordered)
break;
DValue item = markdown_make_node("list_item");
if(markdown_gfm_enabled(options))
{
bool checked = false;
String task_content = item_content;
if(markdown_parse_task_prefix(task_content, checked))
{
item["task"].set_bool(true);
item["checked"].set_bool(checked);
item_content = task_content;
}
}
StringList child_lines;
child_lines.push_back(item_content);
idx += 1;
while(idx < (s64)lines.size())
{
if(markdown_is_blank(lines[idx]))
{
child_lines.push_back("");
idx += 1;
continue;
}
s64 indent = markdown_count_indent(lines[idx]);
if(indent < base_indent + 2)
break;
String next_raw = lines[idx].substr(base_indent);
bool next_item_ordered = false;
s64 next_marker_length = 0;
s64 next_start_number = 1;
String next_content = "";
if(indent == base_indent &&
markdown_parse_list_marker(next_raw, next_item_ordered, next_marker_length, next_start_number, next_content) &&
next_item_ordered == ordered)
break;
s64 consume_indent = std::min<s64>((s64)lines[idx].length(), base_indent + 2);
child_lines.push_back(lines[idx].substr(consume_indent));
idx += 1;
}
DValue child_doc = markdown_parse_blocks(child_lines, options);
item["children"] = markdown_get_value(child_doc, "children");
markdown_push(node["children"], item);
}
return(node);
}
DValue markdown_parse_blockquote(StringList& lines, s64& idx, s64 base_indent, DValue& options)
{
StringList quote_lines;
while(idx < (s64)lines.size())
{
if(markdown_is_blank(lines[idx]))
{
quote_lines.push_back("");
idx += 1;
continue;
}
if(markdown_count_indent(lines[idx]) < base_indent)
break;
String raw = lines[idx].substr(base_indent);
String trimmed = trim(raw);
if(trimmed.rfind(">", 0) != 0)
break;
s64 marker_pos = raw.find(">");
String stripped = raw.substr(marker_pos + 1);
if(stripped.rfind(" ", 0) == 0)
stripped = stripped.substr(1);
quote_lines.push_back(stripped);
idx += 1;
}
DValue node = markdown_make_node("blockquote");
DValue child_doc = markdown_parse_blocks(quote_lines, options);
node["children"] = markdown_get_value(child_doc, "children");
return(node);
}
DValue markdown_parse_code_block(StringList& lines, s64& idx, s64 base_indent)
{
char fence_char = '\0';
s64 fence_length = 0;
String info = "";
String raw = lines[idx].substr(base_indent);
markdown_parse_fence(raw, fence_char, fence_length, info);
String lang = trim(nibble(info, " "));
String meta = trim(info);
idx += 1;
StringList body;
while(idx < (s64)lines.size())
{
String current = lines[idx];
String current_raw = current;
if(markdown_count_indent(current) >= base_indent)
current_raw = current.substr(base_indent);
String trimmed = trim(current_raw);
s64 close_length = 0;
while(close_length < (s64)trimmed.length() && trimmed[close_length] == fence_char)
close_length += 1;
if(close_length >= fence_length)
{
idx += 1;
break;
}
body.push_back(current_raw);
idx += 1;
}
DValue node = markdown_make_node("code_block");
node["text"] = join(body, "\n");
node["lang"] = lang;
node["meta"] = meta;
return(node);
}
DValue markdown_parse_table(StringList& lines, s64& idx, s64 base_indent, DValue& options)
{
DValue node = markdown_make_node("table");
String header_line = lines[idx].substr(base_indent);
String separator_line = lines[idx + 1].substr(base_indent);
StringList alignments;
markdown_parse_table_separator(separator_line, alignments);
StringList header_cells = markdown_split_pipe_row(header_line);
for(auto cell_text : header_cells)
{
DValue cell = markdown_make_node("table_cell");
cell["text"] = cell_text;
cell["children"] = markdown_parse_inline(cell_text, options);
markdown_push(node["header"], cell);
}
for(auto align : alignments)
{
DValue cell_align;
cell_align = align;
markdown_push(node["align"], cell_align);
}
idx += 2;
while(idx < (s64)lines.size())
{
if(markdown_is_blank(lines[idx]))
break;
if(markdown_count_indent(lines[idx]) < base_indent)
break;
String raw = lines[idx].substr(base_indent);
if(raw.find("|") == String::npos)
break;
StringList cells = markdown_split_pipe_row(raw);
if(cells.size() == 0)
break;
DValue row;
for(auto cell_text : cells)
{
DValue cell = markdown_make_node("table_cell");
cell["text"] = cell_text;
cell["children"] = markdown_parse_inline(cell_text, options);
markdown_push(row, cell);
}
markdown_push(node["rows"], row);
idx += 1;
}
return(node);
}
DValue markdown_parse_directive(StringList& lines, s64& idx, s64 base_indent, DValue& options)
{
String name = "";
String argument = "";
DValue attrs;
markdown_parse_directive_open(lines[idx].substr(base_indent), name, argument, attrs);
StringList body_lines;
s64 depth = 1;
idx += 1;
while(idx < (s64)lines.size())
{
String current = lines[idx];
String raw = current;
if(markdown_count_indent(current) >= base_indent)
raw = current.substr(base_indent);
String trimmed = trim(raw);
if(trimmed.rfind(":::", 0) == 0)
{
if(trimmed == ":::")
{
depth -= 1;
if(depth == 0)
{
idx += 1;
break;
}
body_lines.push_back(raw);
idx += 1;
continue;
}
String nested_name = "";
String nested_argument = "";
DValue nested_attrs;
if(markdown_parse_directive_open(raw, nested_name, nested_argument, nested_attrs))
depth += 1;
}
body_lines.push_back(raw);
idx += 1;
}
DValue node = markdown_make_node("directive");
node["name"] = name;
node["argument"] = argument;
node["attrs"] = attrs;
node["body_source"] = join(body_lines, "\n");
DValue child_doc = markdown_parse_blocks(body_lines, options);
node["children"] = markdown_get_value(child_doc, "children");
return(node);
}
DValue markdown_parse_raw_html_block(StringList& lines, s64& idx, s64 base_indent)
{
StringList html_lines;
while(idx < (s64)lines.size())
{
if(markdown_is_blank(lines[idx]))
break;
if(markdown_count_indent(lines[idx]) < base_indent)
break;
String raw = lines[idx].substr(base_indent);
if(trim(raw).rfind("<", 0) != 0)
break;
html_lines.push_back(raw);
idx += 1;
}
DValue node = markdown_make_node("raw_html");
node["html"] = join(html_lines, "\n");
return(node);
}
DValue markdown_parse_blocks(StringList lines, DValue options)
{
DValue document = markdown_make_node("document");
document["source"] = join(lines, "\n");
s64 idx = 0;
while(idx < (s64)lines.size())
{
if(markdown_is_blank(lines[idx]))
{
idx += 1;
continue;
}
s64 base_indent = markdown_count_indent(lines[idx]);
String raw = lines[idx].substr(base_indent);
String trimmed = trim(raw);
s64 atx_level = 0;
String heading_content = "";
if(markdown_parse_atx_heading(raw, atx_level, heading_content))
{
DValue node = markdown_make_node("heading");
node["level"] = (f64)atx_level;
node["text"] = heading_content;
node["children"] = markdown_parse_inline(heading_content, options);
markdown_push(document["children"], node);
idx += 1;
continue;
}
if(idx + 1 < (s64)lines.size() && markdown_count_indent(lines[idx + 1]) >= base_indent)
{
s64 setext_level = 0;
String setext_content = "";
if(markdown_parse_setext_heading(raw, lines[idx + 1].substr(base_indent), setext_level, setext_content))
{
DValue node = markdown_make_node("heading");
node["level"] = (f64)setext_level;
node["text"] = setext_content;
node["children"] = markdown_parse_inline(setext_content, options);
markdown_push(document["children"], node);
idx += 2;
continue;
}
}
char fence_char = '\0';
s64 fence_length = 0;
String fence_info = "";
if(markdown_parse_fence(raw, fence_char, fence_length, fence_info))
{
DValue node = markdown_parse_code_block(lines, idx, base_indent);
markdown_push(document["children"], node);
continue;
}
if(markdown_is_hr(raw))
{
DValue node = markdown_make_node("hr");
markdown_push(document["children"], node);
idx += 1;
continue;
}
String directive_name = "";
String directive_argument = "";
DValue directive_attrs;
if(markdown_parse_directive_open(raw, directive_name, directive_argument, directive_attrs))
{
DValue node = markdown_parse_directive(lines, idx, base_indent, options);
markdown_push(document["children"], node);
continue;
}
if(trimmed.rfind(">", 0) == 0)
{
DValue node = markdown_parse_blockquote(lines, idx, base_indent, options);
markdown_push(document["children"], node);
continue;
}
bool ordered = false;
s64 marker_length = 0;
s64 start_number = 1;
String list_content = "";
if(markdown_parse_list_marker(raw, ordered, marker_length, start_number, list_content))
{
DValue node = markdown_parse_list(lines, idx, base_indent, options);
markdown_push(document["children"], node);
continue;
}
if(markdown_gfm_enabled(options) && idx + 1 < (s64)lines.size() && raw.find("|") != String::npos &&
markdown_count_indent(lines[idx + 1]) >= base_indent)
{
StringList alignments;
if(markdown_parse_table_separator(lines[idx + 1].substr(base_indent), alignments))
{
DValue node = markdown_parse_table(lines, idx, base_indent, options);
markdown_push(document["children"], node);
continue;
}
}
if(markdown_allow_html(options) && trimmed.rfind("<", 0) == 0)
{
DValue node = markdown_parse_raw_html_block(lines, idx, base_indent);
markdown_push(document["children"], node);
continue;
}
DValue node = markdown_parse_paragraph(lines, idx, base_indent, options);
markdown_push(document["children"], node);
}
return(document);
}
String markdown_render_children(DValue children, DValue& options);
String markdown_render_node(DValue node, DValue& options);
String markdown_render_cell_children(DValue cell, DValue& options)
{
return(markdown_render_children(markdown_get_value(cell, "children"), options));
}
String markdown_render_with_component_hook(DValue node, DValue& options, String hook, String children_html, String default_html)
{
String type = markdown_get_string(node, "type");
String target = "";
String exact_hook = "";
if(type == "directive")
{
exact_hook = ":::" + markdown_get_string(node, "name");
target = markdown_get_component_target(options, exact_hook);
}
if(target == "")
target = markdown_get_component_target(options, hook);
if(target == "" || !context)
return(default_html);
if(target != "" && exact_hook != "")
hook = exact_hook;
DValue props;
props["hook"] = hook;
props["target"] = target;
props["default_html"] = default_html;
props["children_html"] = children_html;
props["node"] = node;
props["type"] = type;
props["name"] = markdown_get_string(node, "name");
props["argument"] = markdown_get_string(node, "argument");
props["text"] = markdown_get_string(node, "text");
props["lang"] = markdown_get_string(node, "lang");
props["href"] = markdown_get_string(node, "href");
props["src"] = markdown_get_string(node, "src");
props["title"] = markdown_get_string(node, "title");
props["options"] = options;
return(component(target, props, *context));
}
String markdown_render_children(DValue children, DValue& options)
{
String result = "";
for(auto key : markdown_sorted_keys(children))
result += markdown_render_node(children._map[key], options);
return(result);
}
String markdown_render_node(DValue node, DValue& options)
{
String type = markdown_get_string(node, "type");
String hook = "node." + type;
String children_html = markdown_render_children(markdown_get_value(node, "children"), options);
String html = "";
if(type == "document")
{
html = children_html;
}
else if(type == "paragraph")
{
html = "<p>" + children_html + "</p>";
}
else if(type == "heading")
{
s64 level = int_val(markdown_get_string(node, "level", "1"));
if(level < 1) level = 1;
if(level > 6) level = 6;
html = String("<h") + level + ">" + children_html + String("</h") + level + ">";
}
else if(type == "text")
{
html = html_escape(markdown_get_string(node, "text"));
}
else if(type == "code")
{
html = "<code>" + html_escape(markdown_get_string(node, "text")) + "</code>";
}
else if(type == "strong")
{
html = "<strong>" + children_html + "</strong>";
}
else if(type == "em")
{
html = "<em>" + children_html + "</em>";
}
else if(type == "strike")
{
html = "<del>" + children_html + "</del>";
}
else if(type == "link")
{
String attrs = " href=\"" + html_escape(markdown_get_string(node, "href")) + "\"";
String title = markdown_get_string(node, "title");
if(title != "")
attrs += " title=\"" + html_escape(title) + "\"";
html = "<a" + attrs + ">" + children_html + "</a>";
}
else if(type == "image")
{
String attrs = " src=\"" + html_escape(markdown_get_string(node, "src")) + "\"";
attrs += " alt=\"" + html_escape(markdown_get_string(node, "alt")) + "\"";
String title = markdown_get_string(node, "title");
if(title != "")
attrs += " title=\"" + html_escape(title) + "\"";
html = "<img" + attrs + "/>";
}
else if(type == "raw_html")
{
html = markdown_get_string(node, "html");
}
else if(type == "hr")
{
html = "<hr/>";
}
else if(type == "blockquote")
{
html = "<blockquote>" + children_html + "</blockquote>";
}
else if(type == "list")
{
bool ordered = markdown_get_bool(node, "ordered", false);
String tag = ordered ? "ol" : "ul";
String attrs = "";
if(ordered)
{
s64 start = int_val(markdown_get_string(node, "start", "1"));
if(start > 1)
attrs = String(" start=\"") + start + "\"";
}
html = "<" + tag + attrs + ">" + children_html + "</" + tag + ">";
}
else if(type == "list_item")
{
String attrs = "";
if(markdown_get_bool(node, "task", false))
attrs = " class=\"task-list-item\"";
html = "<li" + attrs + ">";
if(markdown_get_bool(node, "task", false))
{
html += "<input type=\"checkbox\" disabled";
if(markdown_get_bool(node, "checked", false))
html += " checked";
html += "/> ";
}
html += children_html + "</li>";
}
else if(type == "code_block")
{
String lang = markdown_get_string(node, "lang");
String class_attr = "";
if(lang != "")
class_attr = " class=\"language-" + html_escape(lang) + "\"";
html = "<pre><code" + class_attr + ">" + html_escape(markdown_get_string(node, "text")) + "</code></pre>";
}
else if(type == "table")
{
String header_html = "";
StringList align_keys = markdown_sorted_keys(markdown_get_value(node, "align"));
StringList header_keys = markdown_sorted_keys(markdown_get_value(node, "header"));
for(s64 i = 0; i < (s64)header_keys.size(); i++)
{
String align = (i < (s64)align_keys.size()) ? node["align"]._map[align_keys[i]].to_string() : "";
String align_attr = align != "" ? " align=\"" + html_escape(align) + "\"" : "";
header_html += "<th" + align_attr + ">" + markdown_render_cell_children(node["header"]._map[header_keys[i]], options) + "</th>";
}
String body_html = "";
for(auto row_key : markdown_sorted_keys(markdown_get_value(node, "rows")))
{
DValue row = node["rows"]._map[row_key];
String row_html = "";
StringList row_keys = markdown_sorted_keys(row);
for(s64 i = 0; i < (s64)row_keys.size(); i++)
{
String align = (i < (s64)align_keys.size()) ? node["align"]._map[align_keys[i]].to_string() : "";
String align_attr = align != "" ? " align=\"" + html_escape(align) + "\"" : "";
row_html += "<td" + align_attr + ">" + markdown_render_cell_children(row._map[row_keys[i]], options) + "</td>";
}
body_html += "<tr>" + row_html + "</tr>";
}
html = "<table><thead><tr>" + header_html + "</tr></thead><tbody>" + body_html + "</tbody></table>";
}
else if(type == "directive")
{
String name = markdown_get_string(node, "name");
String title = markdown_get_string(node["attrs"], "title", markdown_get_string(node, "argument"));
String title_html = "";
if(title != "")
title_html = "<div class=\"md-directive-title\">" + markdown_render_children(markdown_parse_inline(title, options), options) + "</div>";
html = "<div class=\"md-directive md-directive-" + safe_name(name) + "\">" + title_html + children_html + "</div>";
}
else
{
html = children_html;
}
return(markdown_render_with_component_hook(node, options, hook, children_html, html));
}
DValue markdown_to_ast(String src)
{
DValue options;
return(markdown_to_ast(src, options));
}
DValue markdown_to_ast(String src, DValue options)
{
src = markdown_strip_newlines(src);
return(markdown_parse_blocks(split(src, "\n"), options));
}
String markdown_to_html(String src)
{
DValue options;
return(markdown_to_html(src, options));
}
String markdown_to_html(String src, DValue options)
{
DValue ast = markdown_to_ast(src, options);
return(markdown_render_node(ast, options));
}