essence-os/apps/markdown_viewer.cpp

500 lines
16 KiB
C++

// This file is part of the Essence operating system.
// It is released under the terms of the MIT license -- see LICENSE.md.
// Written by: nakst.
#define ES_INSTANCE_TYPE Instance
#include <essence.h>
#include <shared/strings.cpp>
#include <shared/array.cpp>
#include <ports/md4c/md4c.c>
// TODO Inline code background?
// TODO When resizing the window, maintain the scroll position.
// TODO Table of contents/navigation pane.
// TODO Searching.
// TODO Images.
// TODO Proper link support.
// TODO Embedding markdown viewers into other applications.
// #define DEBUG_PARSER_OUTPUT
struct Span {
bool em, strong, monospaced, link;
uintptr_t offset;
};
struct Instance : EsInstance {
EsElement *root;
EsElement *active;
Array<Span> spans;
Array<char> text;
int32_t paddingBefore;
bool inBlockQuote;
int inListDepth;
int tableColumnCount;
int debugNestDepth;
};
#define MAKE_TEXT_STYLE(_textColor, _textSize, _fontFamily, _fontWeight, _isItalic) \
{ \
.metrics = { \
.mask = ES_THEME_METRICS_TEXT_COLOR | ES_THEME_METRICS_TEXT_ALIGN | ES_THEME_METRICS_TEXT_SIZE \
| ES_THEME_METRICS_FONT_FAMILY | ES_THEME_METRICS_FONT_WEIGHT \
| ES_THEME_METRICS_IS_ITALIC, \
.textColor = _textColor, \
.textAlign = ES_TEXT_H_LEFT | ES_TEXT_V_TOP | ES_TEXT_WRAP, \
.textSize = (int) (_textSize * 0.64 + 0.5), \
.fontFamily = _fontFamily, \
.fontWeight = _fontWeight, \
.isItalic = _isItalic, \
}, \
}
#define PARAGRAPH_PADDING_BEFORE (0)
#define PARAGRAPH_PADDING_AFTER (16)
#define H1_PADDING_BEFORE (16)
#define H1_PADDING_AFTER (16)
#define H2_PADDING_BEFORE (24)
#define H2_PADDING_AFTER (16)
#define H3_PADDING_BEFORE (24)
#define H3_PADDING_AFTER (16)
#define HEADING_UNDERLINE_GAP (7)
#define HR_PADDING_BEFORE (24)
#define HR_PADDING_AFTER (24)
#define QUOTE_PADDING_BEFORE (16)
#define QUOTE_PADDING_AFTER (0)
#define TABLE_PADDING_BEFORE (16)
#define TABLE_PADDING_AFTER (16)
#define COLOR_BACKGROUND (0xFFFDFDFD)
#define COLOR_GRAY1 (0xFFE1E4E8)
#define COLOR_GRAY2 (0xFFEBEDEF)
#define COLOR_GRAY3 (0xFFF6F8FA)
#define COLOR_TEXT1 (0xFF24292E)
#define COLOR_TEXT2 (0xFF5A636D)
#define COLOR_TEXT_LINK (0xFF0366D6)
EsStyle styleBackground = {
.appearance = {
.enabled = true,
.backgroundColor = COLOR_BACKGROUND,
},
};
EsStyle styleRoot = {
.metrics = {
.mask = ES_THEME_METRICS_INSETS | ES_THEME_METRICS_PREFERRED_WIDTH,
.insets = ES_RECT_4(32, 32, 16, 32),
.preferredWidth = 800,
},
};
EsStyle styleQuote = {
.metrics = {
.mask = ES_THEME_METRICS_INSETS,
.insets = ES_RECT_4(20, 16, 0, 0),
},
.appearance = {
.enabled = true,
.borderColor = COLOR_GRAY1,
.borderSize = ES_RECT_4(4, 0, 0, 0),
},
};
EsStyle styleList = {
.metrics = {
.mask = ES_THEME_METRICS_GAP_MAJOR,
.gapMajor = 5,
},
};
EsStyle styleHeadingUnderline = {
.metrics = {
.mask = ES_THEME_METRICS_PREFERRED_HEIGHT,
.preferredHeight = 1,
},
.appearance = {
.enabled = true,
.backgroundColor = COLOR_GRAY2,
},
};
EsStyle styleCodeBlock = {
.metrics = {
.mask = ES_THEME_METRICS_INSETS | ES_THEME_METRICS_CLIP_ENABLED,
.insets = ES_RECT_4(16, 16, 10, 10),
.clipEnabled = true,
},
.appearance = {
.enabled = true,
.backgroundColor = COLOR_GRAY3,
},
};
EsStyle styleHorizontalRule = {
.metrics = {
.mask = ES_THEME_METRICS_PREFERRED_HEIGHT,
.preferredHeight = 4,
},
.appearance = {
.enabled = true,
.backgroundColor = COLOR_GRAY1,
},
};
EsStyle styleTable = {
};
EsStyle styleTD = {
.inherit = ES_STYLE_TEXT_PARAGRAPH,
.metrics = {
.mask = ES_THEME_METRICS_INSETS | ES_THEME_METRICS_TEXT_SIZE | ES_THEME_METRICS_TEXT_COLOR,
.insets = ES_RECT_4(8, 8, 8, 0),
.textColor = COLOR_TEXT1,
.textSize = (int) (16 * 0.64 + 0.5),
},
};
EsStyle styleTH = {
.inherit = ES_STYLE_TEXT_PARAGRAPH,
.metrics = {
.mask = ES_THEME_METRICS_INSETS | ES_THEME_METRICS_TEXT_SIZE | ES_THEME_METRICS_TEXT_COLOR | ES_THEME_METRICS_FONT_WEIGHT,
.insets = ES_RECT_4(8, 8, 0, 4),
.textColor = COLOR_TEXT1,
.textSize = (int) (16 * 0.64 + 0.5),
.fontWeight = 5,
},
.appearance = {
.enabled = true,
.borderColor = COLOR_GRAY1,
.borderSize = ES_RECT_4(0, 0, 0, 1),
},
};
EsStyle styleHeading1 = MAKE_TEXT_STYLE(COLOR_TEXT1, 32, ES_FONT_SANS, 6, false);
EsStyle styleHeading2 = MAKE_TEXT_STYLE(COLOR_TEXT1, 24, ES_FONT_SANS, 6, false);
EsStyle styleHeading3 = MAKE_TEXT_STYLE(COLOR_TEXT1, 20, ES_FONT_SANS, 6, false);
EsStyle styleParagraph = MAKE_TEXT_STYLE(COLOR_TEXT1, 16, ES_FONT_SANS, 4, false);
EsStyle styleQuoteParagraph = MAKE_TEXT_STYLE(COLOR_TEXT2, 16, ES_FONT_SANS, 4, false);
EsStyle styleCode = MAKE_TEXT_STYLE(COLOR_TEXT1, 16, ES_FONT_MONOSPACED, 4, false);
const char *blockTypes[] = {
"MD_BLOCK_DOC",
"MD_BLOCK_QUOTE",
"MD_BLOCK_UL",
"MD_BLOCK_OL",
"MD_BLOCK_LI",
"MD_BLOCK_HR",
"MD_BLOCK_H",
"MD_BLOCK_CODE",
"MD_BLOCK_HTML",
"MD_BLOCK_P",
"MD_BLOCK_TABLE",
"MD_BLOCK_THEAD",
"MD_BLOCK_TBODY",
"MD_BLOCK_TR",
"MD_BLOCK_TH",
"MD_BLOCK_TD",
};
const char *spanTypes[] = {
"MD_SPAN_EM",
"MD_SPAN_STRONG",
"MD_SPAN_A",
"MD_SPAN_IMG",
"MD_SPAN_CODE",
"MD_SPAN_DEL",
"MD_SPAN_LATEXMATH",
"MD_SPAN_LATEXMATH_DISPLAY",
"MD_SPAN_WIKILINK",
"MD_SPAN_U",
};
const char *textTypes[] = {
"MD_TEXT_NORMAL",
"MD_TEXT_NULLCHAR",
"MD_TEXT_BR",
"MD_TEXT_SOFTBR",
"MD_TEXT_ENTITY",
"MD_TEXT_CODE",
"MD_TEXT_HTML",
"MD_TEXT_LATEXMATH",
};
void AddPadding(Instance *instance, int32_t before, int32_t after) {
if (instance->inListDepth) return;
if (before < instance->paddingBefore) before = instance->paddingBefore;
EsSpacerCreate(instance->active, ES_FLAGS_DEFAULT, nullptr, 0, before);
instance->paddingBefore = after;
}
void CreateStyledTextDisplay(Instance *instance, EsStyle *style, uint64_t flags = ES_CELL_H_FILL) {
EsTextDisplay *display = EsTextDisplayCreate(instance->active, flags, style);
EsTextRun *runs = (EsTextRun *) EsHeapAllocate(sizeof(EsTextRun) * (instance->spans.Length() + 1), false);
for (uintptr_t i = 0; i < instance->spans.Length(); i++) {
runs[i].offset = instance->spans[i].offset;
EsElementGetTextStyle(display, &runs[i].style);
if (instance->spans[i].link) { runs[i].style.decorations |= ES_TEXT_DECORATION_UNDERLINE; runs[i].style.color = COLOR_TEXT_LINK; }
if (instance->spans[i].em) runs[i].style.font.italic = true;
if (instance->spans[i].strong) runs[i].style.font.weight = 7;
if (instance->spans[i].monospaced) runs[i].style.font.family = ES_FONT_MONOSPACED;
runs[i].style.decorationsColor = runs[i].style.color;
}
runs[instance->spans.Length()].offset = instance->text.Length();
EsTextDisplaySetStyledContents(display, instance->text.array, runs, instance->spans.Length());
EsHeapFree(runs);
}
#ifdef DEBUG_PARSER_OUTPUT
void ParserOutputPrintIndentation(Instance *instance) {
for (int i = 0; i < instance->debugNestDepth; i++) {
EsPrint(" ");
}
}
#endif
int ParserEnterBlock(MD_BLOCKTYPE type, void *detail, void *_instance) {
Instance *instance = (Instance *) _instance;
#ifdef DEBUG_PARSER_OUTPUT
ParserOutputPrintIndentation(instance);
EsPrint(">> Enter block %z\n", blockTypes[type]);
instance->debugNestDepth++;
#endif
(void) detail;
if (instance->inListDepth && instance->text.Length()) {
CreateStyledTextDisplay(instance, &styleParagraph);
}
instance->text.SetLength(0);
instance->spans.SetLength(0);
Span span = {};
instance->spans.Add(span);
if (type == MD_BLOCK_UL) {
AddPadding(instance, PARAGRAPH_PADDING_BEFORE, PARAGRAPH_PADDING_AFTER);
instance->active = EsListDisplayCreate(instance->active, ES_CELL_H_FILL | ES_LIST_DISPLAY_BULLETED, ES_STYLE_LIST_DISPLAY_DEFAULT);
} else if (type == MD_BLOCK_OL) {
AddPadding(instance, PARAGRAPH_PADDING_BEFORE, PARAGRAPH_PADDING_AFTER);
EsListDisplay *display = EsListDisplayCreate(instance->active, ES_CELL_H_FILL | ES_LIST_DISPLAY_NUMBERED, ES_STYLE_LIST_DISPLAY_DEFAULT);
instance->active = display;
EsListDisplaySetCounterStart(display, ((MD_BLOCK_OL_DETAIL *) detail)->start - 1);
} else if (type == MD_BLOCK_QUOTE) {
AddPadding(instance, QUOTE_PADDING_BEFORE, QUOTE_PADDING_AFTER);
instance->active = EsPanelCreate(instance->active, ES_CELL_H_FILL, &styleQuote);
instance->inBlockQuote = true;
} else if (type == MD_BLOCK_LI) {
instance->active = EsPanelCreate(instance->active, ES_CELL_H_FILL, &styleList);
instance->inListDepth++;
} else if (type == MD_BLOCK_TABLE) {
AddPadding(instance, TABLE_PADDING_BEFORE, TABLE_PADDING_AFTER);
EsPanel *table = EsPanelCreate(instance->active, ES_PANEL_TABLE | ES_PANEL_HORIZONTAL | ES_CELL_H_SHRINK, &styleTable);
instance->active = table;
instance->tableColumnCount = 0;
}
return 0;
}
int ParserLeaveBlock(MD_BLOCKTYPE type, void *detail, void *_instance) {
Instance *instance = (Instance *) _instance;
#ifdef DEBUG_PARSER_OUTPUT
instance->debugNestDepth--;
ParserOutputPrintIndentation(instance);
EsPrint(">> Leave block %z\n", blockTypes[type]);
#endif
if (type == MD_BLOCK_P) {
if (instance->text.Length()) {
if (type == MD_BLOCK_P) {
AddPadding(instance, PARAGRAPH_PADDING_BEFORE, PARAGRAPH_PADDING_AFTER);
}
CreateStyledTextDisplay(instance, instance->inBlockQuote ? &styleQuoteParagraph : &styleParagraph);
}
} else if (type == MD_BLOCK_LI) {
if (instance->text.Length()) CreateStyledTextDisplay(instance, &styleParagraph);
instance->text.SetLength(0);
instance->spans.SetLength(0);
instance->active = EsElementGetLayoutParent(instance->active);
instance->inListDepth--;
} else if (type == MD_BLOCK_TD || type == MD_BLOCK_TH) {
if (type == MD_BLOCK_TH) instance->tableColumnCount++;
CreateStyledTextDisplay(instance, type == MD_BLOCK_TH ? &styleTH : &styleTD, ES_CELL_H_EXPAND | ES_CELL_V_EXPAND | ES_CELL_H_SHRINK | ES_CELL_V_SHRINK);
instance->text.SetLength(0);
instance->spans.SetLength(0);
} else if (type == MD_BLOCK_H) {
unsigned level = ((MD_BLOCK_H_DETAIL *) detail)->level;
if (level == 1) AddPadding(instance, H1_PADDING_BEFORE, H1_PADDING_AFTER);
else if (level == 2) AddPadding(instance, H2_PADDING_BEFORE, H2_PADDING_AFTER);
else AddPadding(instance, H3_PADDING_BEFORE, H3_PADDING_AFTER);
CreateStyledTextDisplay(instance, level == 1 ? &styleHeading1 : level == 2 ? &styleHeading2 : &styleHeading3);
if (level <= 2) {
EsSpacerCreate(instance->active, ES_FLAGS_DEFAULT, nullptr, 0, HEADING_UNDERLINE_GAP);
EsSpacerCreate(instance->active, ES_CELL_H_FILL, &styleHeadingUnderline, 0, 0);
}
} else if (type == MD_BLOCK_CODE) {
MD_BLOCK_CODE_DETAIL *code = (MD_BLOCK_CODE_DETAIL *) detail;
AddPadding(instance, PARAGRAPH_PADDING_BEFORE, PARAGRAPH_PADDING_AFTER);
EsElement *wrapper = EsPanelCreate(instance->active, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL | ES_PANEL_H_SCROLL_AUTO, &styleCodeBlock);
EsTextDisplay *display = EsTextDisplayCreate(wrapper, ES_TEXT_DISPLAY_PREFORMATTED, &styleCode, instance->text.array, instance->text.Length());
if (0 == EsStringCompare(code->lang.text, code->lang.size, EsLiteral("ini"))) {
EsTextDisplaySetupSyntaxHighlighting(display, ES_SYNTAX_HIGHLIGHTING_LANGUAGE_INI);
} else if (0 == EsStringCompare(code->lang.text, code->lang.size, EsLiteral("c"))
|| 0 == EsStringCompare(code->lang.text, code->lang.size, EsLiteral("cpp"))
|| 0 == EsStringCompare(code->lang.text, code->lang.size, EsLiteral("c++"))) {
EsTextDisplaySetupSyntaxHighlighting(display, ES_SYNTAX_HIGHLIGHTING_LANGUAGE_C);
}
} else if (type == MD_BLOCK_UL || type == MD_BLOCK_OL || type == MD_BLOCK_QUOTE) {
instance->active = EsElementGetLayoutParent(instance->active);
instance->inBlockQuote = false;
} else if (type == MD_BLOCK_HR) {
AddPadding(instance, HR_PADDING_BEFORE, HR_PADDING_AFTER);
EsSpacerCreate(instance->active, ES_CELL_H_FILL, &styleHorizontalRule, 0, 0);
} else if (type == MD_BLOCK_TABLE) {
EsPanel *panel = (EsPanel *) instance->active;
EsPanelSetBands(panel, instance->tableColumnCount);
EsPanelTableSetChildCells(panel);
EsPanelBand column = {};
column.preferredSize = column.minimumSize = column.maximumSize = ES_PANEL_BAND_SIZE_DEFAULT;
column.pull = 1; // Shrink all columns with equal weight.
EsPanelSetBandsAll(panel, &column);
instance->active = EsElementGetLayoutParent(instance->active);
} else {
// EsPrint("Unhandled block of type %z.\n", blockTypes[type]);
}
return 0;
}
int ParserEnterSpan(MD_SPANTYPE type, void *detail, void *_instance) {
Instance *instance = (Instance *) _instance;
#ifdef DEBUG_PARSER_OUTPUT
ParserOutputPrintIndentation(instance);
EsPrint(">> Enter span %z\n", spanTypes[type]);
instance->debugNestDepth++;
#endif
(void) detail;
if (type == MD_SPAN_EM || type == MD_SPAN_STRONG || type == MD_SPAN_CODE || type == MD_SPAN_A) {
Span span = instance->spans.Last();
if (type == MD_SPAN_EM) span.em = true;
if (type == MD_SPAN_STRONG) span.strong = true;
if (type == MD_SPAN_CODE) span.monospaced = true;
if (type == MD_SPAN_A) span.link = true;
span.offset = instance->text.Length();
instance->spans.Add(span);
}
return 0;
}
int ParserLeaveSpan(MD_SPANTYPE type, void *detail, void *_instance) {
Instance *instance = (Instance *) _instance;
#ifdef DEBUG_PARSER_OUTPUT
instance->debugNestDepth--;
ParserOutputPrintIndentation(instance);
EsPrint(">> Leave span %z\n", spanTypes[type]);
#endif
(void) detail;
if (type == MD_SPAN_EM || type == MD_SPAN_STRONG || type == MD_SPAN_CODE || type == MD_SPAN_A) {
Span span = instance->spans.Last();
if (type == MD_SPAN_EM) span.em = false;
if (type == MD_SPAN_STRONG) span.strong = false;
if (type == MD_SPAN_CODE) span.monospaced = false;
if (type == MD_SPAN_A) span.link = false;
span.offset = instance->text.Length();
instance->spans.Add(span);
}
return 0;
}
int ParserText(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *_instance) {
(void) type;
Instance *instance = (Instance *) _instance;
#ifdef DEBUG_PARSER_OUTPUT
ParserOutputPrintIndentation(instance);
EsPrint(">> Text %z, %x: %s\n", textTypes[type], text, size, text);
#endif
char *buffer = instance->text.InsertMany(instance->text.Length(), size);
EsMemoryCopy(buffer, text, size);
return 0;
}
void ProcessApplicationMessage(EsMessage *message) {
if (message->type == ES_MSG_INSTANCE_CREATE) {
Instance *instance = EsInstanceCreate(message, INTERFACE_STRING(MarkdownViewerTitle));
EsInstanceSetClassViewer(instance, nullptr);
EsWindow *window = instance->window;
EsPanel *wrapper = EsPanelCreate(instance->window, ES_CELL_FILL, ES_STYLE_PANEL_WINDOW_DIVIDER);
EsPanel *background = EsPanelCreate(wrapper, ES_CELL_FILL | ES_PANEL_V_SCROLL_AUTO, &styleBackground);
instance->root = EsPanelCreate(background, ES_CELL_H_SHRINK, &styleRoot);
EsWindowSetIcon(window, ES_ICON_TEXT_MARKDOWN);
} else if (message->type == ES_MSG_INSTANCE_OPEN) {
Instance *instance = message->instanceOpen.instance;
if (message->instanceOpen.update) {
EsElementStartTransition(instance->root, ES_TRANSITION_ZOOM_IN);
}
EsElementDestroyContents(instance->root);
size_t fileSize;
char *file = (char *) EsFileStoreReadAll(message->instanceOpen.file, &fileSize);
if (!file || !EsUTF8IsValid(file, fileSize)) {
EsInstanceOpenComplete(message, false);
return;
}
MD_PARSER parser = {};
parser.flags = MD_DIALECT_GITHUB | MD_FLAG_NOHTML;
parser.enter_block = ParserEnterBlock;
parser.leave_block = ParserLeaveBlock;
parser.enter_span = ParserEnterSpan;
parser.leave_span = ParserLeaveSpan;
parser.text = ParserText;
instance->active = instance->root;
int result = md_parse(file, fileSize, &parser, instance);
if (result) EsElementDestroyContents(instance->root); // An error occurred.
EsInstanceOpenComplete(message, result == 0);
EsHeapFree(file);
EsElementRelayout(instance->root);
instance->spans.Free();
instance->text.Free();
}
}
void _start() {
_init();
while (true) {
ProcessApplicationMessage(EsMessageReceive());
}
}