#include "website.h"
#include <ctime>
#define TAG_P 0
#define TAG_H1 1
#define TAG_H2 2
#define TAG_H3 3
#define TAG_H4 4
#define TAG_H5 5
#define TAG_H6 6
#define TAG_LI 7
#define TAG_TR 8
#define TAG_TD 9
static const char *closing_tag_strings[] = {
"</p>\n",
"</h1>\n",
"</h2>\n",
"</h3>\n",
"</h4>\n",
"</h5>\n",
"</h6>\n",
"</li>\n",
"</tr>\n",
"</td>\n"
};
static const char *months[] = {
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
Space produce_markdown_html(Filesystem& fs, String& html, const char *input, int in_sz, Markdown_Params& md_params)
{
Space title = {0};
html.reserve(html.len + in_sz);
int tag_levels[16] = {0};
int tag_cursor = 0;
int tag_of_prev_line = 0;
int header_level = 0;
int quote_level = 0;
int new_quote_level = 0;
int code_level = 0;
int list_level = 0;
int new_list_level = 0;
int consq_backticks = 0;
int consq_tildes = 0;
int consq_asterisks = 0;
int level_asterisks = 0;
int consq_underscores = 0;
int level_underscores = 0;
int leading_spaces = 0;
int nl_count = 0;
int prev_line_len = 0;
int prev_nl_pos = 0;
int code_type = 0;
int table_mode = 0;
int new_table_mode = 0;
int table_col_idx = 0;
int table_row_idx = 0;
bool uses_canvas_js = false;
bool ignore_underscores = false;
bool should_not_open_tag = false;
bool started_line = false;
bool is_strikethrough = false;
bool was_esc = false;
char prev = 0;
char chbuf[8];
for (int i = 0; i < in_sz; i++) {
char c = input[i];
bool should_process = true;
bool is_list_asterisk = false;
chbuf[0] = c;
chbuf[1] = 0;
if (!started_line && code_type != 1) {
should_process = false;
if (c == ' ') {
leading_spaces++;
}
else if (c == '\t') {
leading_spaces += 4;
}
else if (code_type == 0) {
if (c == '>') {
new_quote_level++;
}
else if (c == '#') {
header_level++;
if (header_level > 6)
started_line = true;
}
else if (c == '|') {
should_process = false;
bool is_sep = false;
int j = i;
for (; j < in_sz; j++) {
if (input[j] == '\n')
break;
if (input[j] != '|' && input[j] != ' ' && input[j] != '-' && input[j] != '+' && input[j] != ':')
break;
if (input[j] == '-')
is_sep = true;
}
if (is_sep || j >= in_sz) {
i = j;
c = '\n';
table_row_idx = 0;
should_not_open_tag = true;
}
new_table_mode = table_mode ? table_mode : 1;
new_table_mode = is_sep ? 2 : new_table_mode;
started_line = true;
}
else if (c == '-' || c == '+' || c == '*') {
if (i < in_sz-1 && input[i+1] == ' ') {
new_list_level = (leading_spaces / 4) + 1;
is_list_asterisk = c == '*';
}
else if (i < in_sz-2 && input[i+1] == c && input[i+2] == c) {
html.add("<hr>");
should_process = false;
while (i < in_sz && input[i] != '\n')
i++;
c = '\n';
}
started_line = true;
}
else if (c != '\n') {
started_line = true;
should_process = true;
}
}
else if (c != '\n') {
started_line = true;
should_process = true;
}
if (started_line) {
if (tag_cursor > 0) {
int prev_cursor = tag_cursor;
int tag = tag_levels[tag_cursor];
if (tag == TAG_P && tag_of_prev_line == TAG_P && !header_level && !new_list_level && !new_table_mode && prev_line_len > 1) {
html.add(" ");
should_not_open_tag = true;
}
else {
const char *tag_str = closing_tag_strings[tag];
tag_cursor--;
if (tag_cursor < 0) {
tag_cursor = 0;
tag_levels[0] = 0;
}
bool is_block = code_level || quote_level;
html.add(tag_str, strlen(tag_str) - is_block);
}
}
if (new_list_level > list_level) {
for (int j = 0; j < new_list_level - list_level; j++) {
html.add("<ul>");
}
}
else if (new_list_level < list_level) {
for (int j = 0; j < list_level - new_list_level; j++) {
html.add("</ul>");
}
}
list_level = new_list_level;
if (new_quote_level != quote_level) {
if (!quote_level) {
html.add("<div class=\"quote block\">");
}
else if (!new_quote_level) {
html.add("</div>");
}
quote_level = new_quote_level;
}
int prev_code_type = code_type;
if (quote_level == 0 && list_level == 0 && leading_spaces >= 4) {
code_type = 2;
}
else if (code_type == 2) {
code_type = 0;
}
if (!prev_code_type && code_type) {
html.add("<code>");
}
else if (prev_code_type && !code_type) {
html.add("</code>");
}
if (new_table_mode != table_mode) {
if (table_mode == 0) {
html.add("<table>");
html.add(new_table_mode == 1 ? "<thead>" : "<tbody>");
}
else {
if (table_mode == 1) {
html.add("</thead>");
if (new_table_mode)
html.add("<tbody>");
}
else {
if (new_table_mode == 1)
html.add("</tbody><thead>");
else if (!new_table_mode)
html.add("</tbody>");
}
if (!new_table_mode)
html.add("</table>");
}
table_row_idx = 0;
table_mode = new_table_mode;
}
if (!should_not_open_tag) {
if (tag_cursor < 15)
tag_cursor++;
if (header_level > 0 && header_level <= 6) {
char tag[8];
tag[0] = '<';
tag[1] = 'h';
tag[2] = '0' + header_level;
tag[3] = '>';
tag[4] = 0;
html.add(tag);
if (!title.offset)
title.offset = i;
tag_levels[tag_cursor] = header_level;
}
else {
if (table_mode) {
tag_levels[tag_cursor] = TAG_TR;
html.add("<tr class=\"tbl-row-0\">");
html.data()[html.len - 3] = '0' + (table_row_idx % 2);
html.add("<td>");
}
else if (list_level > 0) {
tag_levels[tag_cursor] = TAG_LI;
html.add("<li>");
}
else {
tag_levels[tag_cursor] = TAG_P;
html.add("<p>");
}
}
}
if (header_level > 6)
html.add("#######");
header_level = 0;
}
}
if (code_type == 0) {
int prev_em = level_asterisks + level_underscores;
if (c == '*' && !was_esc && !is_list_asterisk) {
consq_asterisks++;
should_process = false;
}
else if (consq_asterisks) {
level_asterisks = level_asterisks ? 0 : consq_asterisks;
consq_asterisks = 0;
}
if (c == '_' && !was_esc) {
if ((prev >= '0' && prev <= '9') || (prev >= 'A' && prev <= 'Z') || (prev >= 'a' && prev <= 'z'))
ignore_underscores = !level_underscores;
if (!ignore_underscores) {
consq_underscores++;
should_process = false;
}
}
else {
if (consq_underscores) {
level_underscores = level_underscores ? 0 : consq_underscores;
consq_underscores = 0;
}
ignore_underscores = false;
}
int diff_em = (level_asterisks + level_underscores) - prev_em;
if (diff_em < -2) diff_em = -2;
if (diff_em > 2) diff_em = 2;
const char *em_tags[] = { "</strong>", "</em>", "", "<em>", "<strong>" };
html.add(em_tags[diff_em + 2]);
if (c == '~' && !was_esc) {
consq_tildes++;
should_process = false;
}
else {
if (consq_tildes == 2) {
is_strikethrough = !is_strikethrough;
html.add(is_strikethrough ? "<s>" : "</s>");
}
consq_tildes = 0;
}
if (c == '|' && table_mode) {
if (table_col_idx)
html.add("</td><td>");
table_col_idx++;
should_process = false;
}
}
int prev_backticks = consq_backticks;
if (c == '`' && !was_esc) {
// this avoids the case where a code block is formed using leading spaces, since code_level would equal 0
consq_backticks++;
should_process = false;
}
else {
if (code_type == 1 && consq_backticks == code_level) {
code_level = 0;
html.add(consq_backticks >= 3 ? "</div>" : "</code>");
code_type = 0;
}
else if (code_type == 0 && consq_backticks) {
code_level = consq_backticks;
html.add(consq_backticks >= 3 ? "<div class=\"code block\">" : "<code>");
code_type = 1;
}
consq_backticks = 0;
}
if (code_type != 0) {
should_process = false;
if (c != '`' && !(prev_backticks > 0 && c == '\n')) {
for (int j = 0; j < consq_backticks; j++)
html.add("`");
if (NEEDS_ESCAPE(c))
write_escaped_byte(c, chbuf);
html.add(chbuf);
}
}
else if (!was_esc) {
if (c == '!' || c == '?' || c == '$') {
if (i < in_sz-1 && input[i+1] == '[')
should_process = false;
}
else if (c == '[') {
should_process = false;
if (prev == '!')
html.add("<img src=\"");
else if (prev == '?')
html.add("<span ");
else if (prev == '$')
html.add("<canvas class=\"needs-canvas-js\" data-js=\"");
else if (md_params.disable_anchors)
html.add("<span href=\"");
else
html.add("<a href=\"");
int j = i+1;
int alt = j;
bool alt_contains_colon = false;
for (; j < in_sz && input[j] != ']'; j++) {
if (input[j] == ':')
alt_contains_colon = true;
}
int alt_end = j;
for (; j < in_sz && input[j] != ']'; j++);
int link = alt;
int link_end = alt_end;
bool link_contains_colon = alt_contains_colon;
if (j < in_sz-1 && input[j+1] == '(') {
link_contains_colon = false;
link = j + 2;
for (; j < in_sz && input[j] != ')'; j++) {
if (input[j] == ':')
link_contains_colon = true;
}
link_end = j;
for (; j < in_sz && input[j] != ')'; j++);
}
if (prev != '?') {
if (md_params.path && !link_contains_colon && input[link] != '/') {
if (md_params.path[0] != '/') html.add("/");
html.add(md_params.path);
html.add("/");
}
if (link_end > link)
html.add_and_escape(&input[link], link_end - link);
}
if (link <= alt) {
html.add("\">");
if (prev != '!' && prev != '?' && prev != '$')
html.add_and_escape(&input[link], link_end - link);
}
else if (prev == '$') {
if (alt_end > alt) {
html.add("\" data-params=\"");
html.add_and_escape(&input[alt], alt_end - alt);
}
html.add("\">");
}
else if (prev == '?') {
if (input[alt] == '=' && alt_end - alt > 1) {
html.add("class=\"");
alt++;
}
else if (alt_contains_colon) {
html.add("style=\"");
}
html.add_and_escape(&input[alt], alt_end - alt);
html.add("\">");
html.add_and_escape(&input[link], link_end - link);
}
else {
if (prev == '!')
html.add("\" alt=\"");
else
html.add("\">");
if (alt_end > alt) {
html.add_and_escape(&input[alt], alt_end - alt);
}
if (prev == '!') {
if (input[alt] == '=' && alt_end - alt > 1) {
html.add("\" class=\"");
alt++;
html.add_and_escape(&input[alt], alt_end - alt);
}
else if (alt_contains_colon) {
html.add("\" style=\"");
html.add_and_escape(&input[alt], alt_end - alt);
}
html.add("\">");
}
}
if (prev == '$') {
html.add("</canvas>");
uses_canvas_js = true;
}
else if (prev == '?') {
html.add("</span>");
}
else if (prev != '!') {
html.add(md_params.disable_anchors ? "</span>" : "</a>");
}
i = j;
}
}
if (should_process) {
bool should_add_c = true;
if (!was_esc) {
should_add_c = c != '\\' && c != '\n';
}
if (should_add_c) {
if (NEEDS_ESCAPE(c))
write_escaped_byte(c, chbuf);
html.add(chbuf);
}
}
if (c == '\n') {
if (header_level > 0) {
while (tag_cursor > 0) {
int tag = tag_levels[tag_cursor];
const char *tag_str = closing_tag_strings[tag];
tag_cursor--;
bool is_block = code_level || quote_level;
html.add(tag_str, strlen(tag_str) - is_block);
if (tag >= 1 && tag <= 6)
break;
}
}
if (header_level == 1 && !title.size) {
title.size = i - title.offset;
}
else if (header_level == 2 && md_params.created_time) {
struct tm *date = localtime(&md_params.created_time);
int day_kind = 0;
int day_mod10 = date->tm_mday % 10;
if (day_mod10 < 4 && (date->tm_mday < 4 || date->tm_mday > 20))
day_kind = day_mod10;
const char *kinds[] = {"th", "st", "nd", "rd"};
char datebuf[128];
String date_str(datebuf, 128);
date_str.reformat(
"<em>{d}{s} {s}, {d}</em>",
date->tm_mday, kinds[day_kind], months[date->tm_mon], 1900 + date->tm_year
);
html.add(datebuf);
md_params.created_time = 0;
}
if (table_mode) {
while (tag_cursor > 0) {
int tag = tag_levels[tag_cursor];
const char *tag_str = closing_tag_strings[tag];
tag_cursor--;
if (tag == TAG_TR) {
html.add("</td></tr>");
break;
}
bool is_block = code_level || quote_level;
html.add(tag_str, strlen(tag_str) - is_block);
}
if (!should_not_open_tag)
table_row_idx++;
}
header_level = 0;
tag_of_prev_line = tag_levels[tag_cursor];
started_line = false;
should_not_open_tag = false;
leading_spaces = 0;
new_quote_level = 0;
new_list_level = 0;
new_table_mode = 0;
table_col_idx = 0;
prev_line_len = i - prev_nl_pos - 1;
prev_nl_pos = i;
nl_count++;
if (md_params.line_limit > 0 && nl_count >= md_params.line_limit)
break;
}
if (!code_type)
was_esc = c == '\\' && !was_esc;
else
was_esc = false;
prev = c;
}
if (code_type)
html.add("</code>\n");
if (is_strikethrough)
html.add("</s>\n");
int em_level = level_asterisks + level_underscores;
if (em_level == 1 || em_level == 3)
html.add("</em>\n");
if (em_level == 2 || em_level == 3)
html.add("</strong>\n");
while (tag_cursor > 0) {
const char *tag = closing_tag_strings[tag_levels[tag_cursor]];
tag_cursor--;
html.add(tag);
}
if (table_mode) {
html.add(table_mode == 1 ? "</thead>" : "</tbody>");
html.add("</table>");
}
if (uses_canvas_js) {
html.add("<script>");
fs.add_file_to_html(&html, "client/md-load-canvases.js");
html.add("</script>");
}
return title;
}
void serve_markdown_tester(Filesystem& fs, Request& request, Response& response)
{
bool allow_html = false;
if (request.accept > 0) {
char *p = request.str.data() + request.accept;
while (*p && *p != '\r' && *p != '\n') {
if (p[0] == 'h' && p[1] == 't' && p[2] == 'm' && p[3] == 'l') {
allow_html = true;
break;
}
p++;
}
}
if (!allow_html) {
const char *md_text = request.str.data() + request.header_size;
int md_len = request.str.len - request.header_size;
response.mime = "text/plain";
Markdown_Params md_params = {0};
produce_markdown_html(fs, response.html, md_text, md_len, md_params);
//log_info("{S}", response.html.data(), response.html.len);
return;
}
String *html = &response.html;
html->add(
"<!DOCTYPE html><html class=\"full\"><head>" HTML_METAS "<title>Markdown Editor</title><style>\n"
);
fs.add_file_to_html(html, "client/banner.css");
fs.add_file_to_html(html, "client/article.css");
html->add(
"article h1 { font-size: 200%; }\n"
"article h2 { font-size: 150%; }\n"
"article h3 { font-size: 120%; }\n"
);
fs.add_file_to_html(html, "client/md-editor.css");
html->add(
"canvas { image-rendering: crisp-edges; }\n"
);
html->add("</style><script>");
fs.add_file_to_html(html, "client/code-editor.js");
fs.add_file_to_html(html, "client/md-editor.js");
html->add(
"</script></head>"
"<body class=\"full\" onload=\"load_code_editors(); setup_mdedit_editor();\""
"onmousemove=\"global_mouse_handler(event)\" onmouseup=\"stop_dragging()\">"
"<div class=\"full flex-column\">"
);
add_banner(fs, html, NAV_IDX_NULL);
html->add(
"<div id=\"mdedit-main\">"
"<div id=\"mdedit-left\">"
"<p>Editor</p>"
"<div id=\"mdedit-editor\" class=\"mdedit-container code-editor\" data-listener=\"mdedit_listener\"></div>"
"</div>"
"<div id=\"mdedit-separator\" onmousedown=\"start_dragging()\"></div>"
"<div id=\"mdedit-right\">"
"<p>Preview</p>"
"<div id=\"mdedit-preview-container\" class=\"mdedit-container\">"
"<article id=\"mdedit-preview\"></article>"
"</div>"
"</div>"
"</div>"
);
html->add("</div></body></html>");
}