This commit is contained in:
2026-07-20 11:17:57 +02:00
parent ea4b28ceba
commit f9f5349e02
4 changed files with 470 additions and 52 deletions
+6
View File
@@ -138,6 +138,8 @@ HELP_TEXT :: `Usage:
If there is no file provided, TypoML reads from stdin. If there is no file provided, TypoML reads from stdin.
Options: Options:
--html Output HTML5 (default)
--html4, --html-4 Output HTML4
--title <text> Set the HTML document title (default: "TypoML") --title <text> Set the HTML document title (default: "TypoML")
--plain Don't wrap the output in <html>, <head>, <body> and <style> tags --plain Don't wrap the output in <html>, <head>, <body> and <style> tags
--no-js Don't include the JavaScript variable proxy (<script>) --no-js Don't include the JavaScript variable proxy (<script>)
@@ -166,6 +168,10 @@ parse_args :: proc(args: []string) -> (files: [dynamic]string, flags: renderer.D
} }
case "--print-tree": case "--print-tree":
flags.print_tree = true flags.print_tree = true
case "--html":
flags.mode = .HTML5
case "--html4", "--html-4":
flags.mode = .HTML4
case: case:
if strings.has_prefix(arg, "--") { if strings.has_prefix(arg, "--") {
fmt.eprintf("typoml: unknown option '%s'\n", arg) fmt.eprintf("typoml: unknown option '%s'\n", arg)
@@ -5,47 +5,47 @@ import "../token"
import "core:fmt" import "core:fmt"
import "core:strings" import "core:strings"
render_aligned_cell :: proc(tok: ^token.Token, tag: string) { html4_render_aligned_cell :: proc(tok: ^token.Token, tag: string) {
align := get_alignment_attribute(tok) align := get_alignment_attribute(tok)
if align != "" { if align != "" {
fmt.printf("<%s align=\"%s\">", tag, align) fmt.printf("<%s align=\"%s\">", tag, align)
for child in tok.children[0].children { for child in tok.children[0].children {
print_html(child) print_html4(child)
} }
} else { } else {
fmt.printf("<%s>", tag) fmt.printf("<%s>", tag)
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
} }
fmt.printf("</%s>", tag) fmt.printf("</%s>", tag)
} }
render_simple_wrapper :: proc(tok: ^token.Token, tag: string) { html4_render_simple_wrapper :: proc(tok: ^token.Token, tag: string) {
fmt.printf("<%s>", tag) fmt.printf("<%s>", tag)
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.printf("</%s>", tag) fmt.printf("</%s>", tag)
} }
render_div_wrapper :: proc(tok: ^token.Token, align: string) { html4_render_div_wrapper :: proc(tok: ^token.Token, align: string) {
fmt.printf("<div align=\"%s\">", align) fmt.printf("<div align=\"%s\">", align)
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.print("</div>") fmt.print("</div>")
} }
render_container :: proc(tok: ^token.Token, tag: string) { html4_render_container :: proc(tok: ^token.Token, tag: string) {
fmt.printf("<%s>\n", tag) fmt.printf("<%s>\n", tag)
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.printf("</%s>\n", tag) fmt.printf("</%s>\n", tag)
} }
render_media_object :: proc(tok: ^token.Token, mime_type: string) { html4_render_media_object :: proc(tok: ^token.Token, mime_type: string) {
url, ok := tok.value.(string) url, ok := tok.value.(string)
if !ok { if !ok {
return return
@@ -66,7 +66,7 @@ render_media_object :: proc(tok: ^token.Token, mime_type: string) {
fmt.print("</object>\n") fmt.print("</object>\n")
} }
TEMPLATE_PROXY_SCRIPT :: ` HTML4_TEMPLATE_PROXY_SCRIPT :: `
<script> <script>
(function () { (function () {
var defs = {}; var defs = {};
@@ -95,10 +95,10 @@ TEMPLATE_PROXY_SCRIPT :: `
})(); })();
</script>` </script>`
print_document :: proc(root: ^token.Token, flags: DocumentFlags) { print_html4_document :: proc(root: ^token.Token, flags: DocumentFlags) {
if flags.plain { if flags.plain {
for child in root.children { for child in root.children {
print_html(child) print_html4(child)
} }
return return
} }
@@ -119,23 +119,16 @@ print_document :: proc(root: ^token.Token, flags: DocumentFlags) {
style, style,
) )
for child in root.children { for child in root.children {
print_html(child) print_html4(child)
} }
if !flags.no_js { if !flags.no_js {
fmt.print("\n" + TEMPLATE_PROXY_SCRIPT + "\n") fmt.print("\n" + HTML4_TEMPLATE_PROXY_SCRIPT + "\n")
} }
fmt.print("</body></html>\n") fmt.print("</body></html>\n")
} }
DocumentFlags :: struct {
plain: bool,
no_js: bool,
title: string,
print_tree: bool,
help: bool,
}
print_html :: proc(tok: ^token.Token) { print_html4 :: proc(tok: ^token.Token) {
switch tok.type { switch tok.type {
case token.TokenType.Root: case token.TokenType.Root:
has_footnotes := false has_footnotes := false
@@ -144,7 +137,7 @@ print_html :: proc(tok: ^token.Token) {
fmt.print("<hr class=\"footnotes-sep\">\n") fmt.print("<hr class=\"footnotes-sep\">\n")
has_footnotes = true has_footnotes = true
} }
print_html(child) print_html4(child)
} }
case token.TokenType.Header: case token.TokenType.Header:
@@ -161,7 +154,7 @@ print_html :: proc(tok: ^token.Token) {
fmt.printf("<h%d id=\"%s\">", level, escape_html(slug)) fmt.printf("<h%d id=\"%s\">", level, escape_html(slug))
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.printf("</h%d>\n", level) fmt.printf("</h%d>\n", level)
@@ -170,73 +163,73 @@ print_html :: proc(tok: ^token.Token) {
if !only_var_defs { if !only_var_defs {
fmt.print("<p>") fmt.print("<p>")
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.print("</p>\n") fmt.print("</p>\n")
} else { } else {
for child in tok.children { for child in tok.children {
if child.type != .BreakLine { if child.type != .BreakLine {
print_html(child) print_html4(child)
} }
} }
} }
case token.TokenType.Blockquote: case token.TokenType.Blockquote:
render_container(tok, "blockquote") html4_render_container(tok, "blockquote")
case token.TokenType.UnorderedList: case token.TokenType.UnorderedList:
render_container(tok, "ul") html4_render_container(tok, "ul")
case token.TokenType.OrderedList: case token.TokenType.OrderedList:
render_container(tok, "ol") html4_render_container(tok, "ol")
case token.TokenType.ListItem: case token.TokenType.ListItem:
fmt.print("<li>") fmt.print("<li>")
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.print("</li>\n") fmt.print("</li>\n")
case token.TokenType.Bold: case token.TokenType.Bold:
render_simple_wrapper(tok, "strong") html4_render_simple_wrapper(tok, "strong")
case token.TokenType.Italics: case token.TokenType.Italics:
render_simple_wrapper(tok, "em") html4_render_simple_wrapper(tok, "em")
case token.TokenType.Strikethrough: case token.TokenType.Strikethrough:
render_simple_wrapper(tok, "strike") html4_render_simple_wrapper(tok, "strike")
case token.TokenType.Table: case token.TokenType.Table:
render_container(tok, "table") html4_render_container(tok, "table")
case token.TokenType.TableHeaderRow: case token.TokenType.TableHeaderRow:
fmt.print("<tr>") fmt.print("<tr>")
for child in tok.children { for child in tok.children {
render_aligned_cell(child, "th") html4_render_aligned_cell(child, "th")
} }
fmt.print("</tr>\n") fmt.print("</tr>\n")
case token.TokenType.TableRow: case token.TokenType.TableRow:
fmt.print("<tr>") fmt.print("<tr>")
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.print("</tr>\n") fmt.print("</tr>\n")
case token.TokenType.TableCell: case token.TokenType.TableCell:
render_aligned_cell(tok, "td") html4_render_aligned_cell(tok, "td")
case token.TokenType.TableSeparator: case token.TokenType.TableSeparator:
fmt.print("<tr class=\"table-sep\"><td colspan=\"100\"></td></tr>\n") fmt.print("<tr class=\"table-sep\"><td colspan=\"100\"></td></tr>\n")
case token.TokenType.Left: case token.TokenType.Left:
render_div_wrapper(tok, "left") html4_render_div_wrapper(tok, "left")
case token.TokenType.Right: case token.TokenType.Right:
render_div_wrapper(tok, "right") html4_render_div_wrapper(tok, "right")
case token.TokenType.Middle: case token.TokenType.Middle:
render_div_wrapper(tok, "center") html4_render_div_wrapper(tok, "center")
case token.TokenType.FootnoteRef: case token.TokenType.FootnoteRef:
if id, ok := tok.value.(string); ok { if id, ok := tok.value.(string); ok {
@@ -256,7 +249,7 @@ print_html :: proc(tok: ^token.Token) {
escape_html(id), escape_html(id),
) )
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.print("</div>\n") fmt.print("</div>\n")
} }
@@ -272,7 +265,7 @@ print_html :: proc(tok: ^token.Token) {
fmt.print("<pre><code>") fmt.print("<pre><code>")
} }
for i := 0; i < len(tok.children); i += 1 { for i := 0; i < len(tok.children); i += 1 {
print_html(tok.children[i]) print_html4(tok.children[i])
if i + 1 < len(tok.children) { if i + 1 < len(tok.children) {
fmt.print("\n") fmt.print("\n")
} }
@@ -290,21 +283,21 @@ print_html :: proc(tok: ^token.Token) {
case token.TokenType.Underline: case token.TokenType.Underline:
fmt.print("<u>") fmt.print("<u>")
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.print("</u>") fmt.print("</u>")
case token.TokenType.Superscript: case token.TokenType.Superscript:
fmt.print("<sup>") fmt.print("<sup>")
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.print("</sup>") fmt.print("</sup>")
case token.TokenType.Subscript: case token.TokenType.Subscript:
fmt.print("<sub>") fmt.print("<sub>")
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.print("</sub>") fmt.print("</sub>")
@@ -313,7 +306,7 @@ print_html :: proc(tok: ^token.Token) {
slug := slugifyifyifyify(target) slug := slugifyifyifyify(target)
fmt.printf("<a href=\"#%s\">", escape_html(slug)) fmt.printf("<a href=\"#%s\">", escape_html(slug))
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.print("</a>") fmt.print("</a>")
} }
@@ -322,7 +315,7 @@ print_html :: proc(tok: ^token.Token) {
if url, ok := tok.value.(string); ok { if url, ok := tok.value.(string); ok {
fmt.printf("<a href=\"%s\">", escape_html(url)) fmt.printf("<a href=\"%s\">", escape_html(url))
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.print("</a>") fmt.print("</a>")
} }
@@ -339,12 +332,12 @@ print_html :: proc(tok: ^token.Token) {
case token.TokenType.Audio: case token.TokenType.Audio:
if url, ok := tok.value.(string); ok { if url, ok := tok.value.(string); ok {
render_media_object(tok, mime.detect_mime_type(url)) html4_render_media_object(tok, mime.detect_mime_type(url))
} }
case token.TokenType.Video: case token.TokenType.Video:
if url, ok := tok.value.(string); ok { if url, ok := tok.value.(string); ok {
render_media_object(tok, mime.detect_mime_type(url)) html4_render_media_object(tok, mime.detect_mime_type(url))
} }
case token.TokenType.Iframe: case token.TokenType.Iframe:
@@ -366,7 +359,7 @@ print_html :: proc(tok: ^token.Token) {
if name, ok := tok.value.(string); ok { if name, ok := tok.value.(string); ok {
fmt.printf("<span data-tlm-var=\"%s\">", escape_html(name)) fmt.printf("<span data-tlm-var=\"%s\">", escape_html(name))
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
fmt.print("</span>") fmt.print("</span>")
} }
@@ -393,7 +386,7 @@ print_html :: proc(tok: ^token.Token) {
fmt.print(escape_html(v)) fmt.print(escape_html(v))
} }
for child in tok.children { for child in tok.children {
print_html(child) print_html4(child)
} }
} }
} }
+392
View File
@@ -0,0 +1,392 @@
package renderer
import "../mime"
import "../token"
import "core:fmt"
import "core:strings"
html5_render_aligned_cell :: proc(tok: ^token.Token, tag: string) {
align := get_alignment_attribute(tok)
if align != "" {
fmt.printf("<%s style=\"text-align: %s\">", tag, align)
for child in tok.children[0].children {
print_html5(child)
}
} else {
fmt.printf("<%s>", tag)
for child in tok.children {
print_html5(child)
}
}
fmt.printf("</%s>", tag)
}
html5_render_simple_wrapper :: proc(tok: ^token.Token, tag: string) {
fmt.printf("<%s>", tag)
for child in tok.children {
print_html5(child)
}
fmt.printf("</%s>", tag)
}
html5_render_div_wrapper :: proc(tok: ^token.Token, align: string) {
fmt.printf("<div style=\"text-align: %s\">", align)
for child in tok.children {
print_html5(child)
}
fmt.print("</div>")
}
html5_render_container :: proc(tok: ^token.Token, tag: string) {
fmt.printf("<%s>\n", tag)
for child in tok.children {
print_html5(child)
}
fmt.printf("</%s>\n", tag)
}
html5_render_media_object :: proc(tok: ^token.Token, mime_type: string) {
url, ok := tok.value.(string)
if !ok {
return
}
b := strings.builder_make(context.temp_allocator)
for child in tok.children {
get_text_content(child, &b)
}
alt_text := strings.to_string(b)
if alt_text == "" {
alt_text = url
}
fmt.printf("<object data=\"%s\" type=\"%s\">\n", escape_html(url), escape_html(mime_type))
fmt.printf("<param name=\"src\" value=\"%s\">\n", escape_html(url))
fmt.printf("<param name=\"autoplay\" value=\"false\">\n")
fmt.printf("<param name=\"controller\" value=\"true\">\n")
fmt.printf("<a href=\"%s\">%s</a>\n", escape_html(url), escape_html(alt_text))
fmt.print("</object>\n")
}
HTML5_TEMPLATE_PROXY_SCRIPT :: `
<script>
(function () {
var defs = {};
var spans = document.querySelectorAll("[data-tlm-def]");
for (var i = 0; i < spans.length; i++) {
var el = spans[i];
defs[el.dataset.tlmDef] = el.dataset.tlmValue;
}
var tlm = new Proxy({}, {
get: function (_, name) {
return defs[name];
},
set: function (_, name, value) {
defs[name] = value;
var refs = document.querySelectorAll("[data-tlm-var=\"" + name + "\"]");
for (var i = 0; i < refs.length; i++) {
refs[i].textContent = value;
}
return true;
},
ownKeys: function () {
return Object.keys(defs);
}
});
window.tlm = tlm;
})();
</script>`
print_html5_document :: proc(root: ^token.Token, flags: DocumentFlags) {
if flags.plain {
for child in root.children {
print_html5(child)
}
return
}
doctype := "<!DOCTYPE html>"
meta := "<meta charset=\"utf-8\">"
style := "<style>" + DEFAULT_STYLE + "</style>"
title := flags.title
if title == "" {
title = "TypoML"
}
fmt.printf(
"%s\n<html><head><title>%s</title>%s%s</head>\n<body>\n",
doctype,
escape_html(title),
meta,
style,
)
for child in root.children {
print_html5(child)
}
if !flags.no_js {
fmt.print("\n" + HTML5_TEMPLATE_PROXY_SCRIPT + "\n")
}
fmt.print("</body></html>\n")
}
print_html5 :: proc(tok: ^token.Token) {
switch tok.type {
case token.TokenType.Root:
has_footnotes := false
for child in tok.children {
if child.type == .FootnoteDef && !has_footnotes {
fmt.print("<hr class=\"footnotes-sep\">\n")
has_footnotes = true
}
print_html5(child)
}
case token.TokenType.Header:
level := 1
if v, ok := tok.value.(int); ok {
level = v
}
b := strings.builder_make(context.temp_allocator)
for child in tok.children {
get_text_content(child, &b)
}
slug := slugifyifyifyify(strings.to_string(b))
fmt.printf("<h%d id=\"%s\">", level, escape_html(slug))
for child in tok.children {
print_html5(child)
}
fmt.printf("</h%d>\n", level)
case token.TokenType.Paragraph:
only_var_defs := check_only_var_defs(tok)
if !only_var_defs {
fmt.print("<p>")
for child in tok.children {
print_html5(child)
}
fmt.print("</p>\n")
} else {
for child in tok.children {
if child.type != .BreakLine {
print_html5(child)
}
}
}
case token.TokenType.Blockquote:
html5_render_container(tok, "blockquote")
case token.TokenType.UnorderedList:
html5_render_container(tok, "ul")
case token.TokenType.OrderedList:
html5_render_container(tok, "ol")
case token.TokenType.ListItem:
fmt.print("<li>")
for child in tok.children {
print_html5(child)
}
fmt.print("</li>\n")
case token.TokenType.Bold:
html5_render_simple_wrapper(tok, "strong")
case token.TokenType.Italics:
html5_render_simple_wrapper(tok, "em")
case token.TokenType.Strikethrough:
html5_render_simple_wrapper(tok, "strike")
case token.TokenType.Table:
html5_render_container(tok, "table")
case token.TokenType.TableHeaderRow:
fmt.print("<tr>")
for child in tok.children {
html5_render_aligned_cell(child, "th")
}
fmt.print("</tr>\n")
case token.TokenType.TableRow:
fmt.print("<tr>")
for child in tok.children {
print_html5(child)
}
fmt.print("</tr>\n")
case token.TokenType.TableCell:
html5_render_aligned_cell(tok, "td")
case token.TokenType.TableSeparator:
fmt.print("<tr class=\"table-sep\"><td colspan=\"100\"></td></tr>\n")
case token.TokenType.Left:
html5_render_div_wrapper(tok, "left")
case token.TokenType.Right:
html5_render_div_wrapper(tok, "right")
case token.TokenType.Middle:
html5_render_div_wrapper(tok, "center")
case token.TokenType.FootnoteRef:
if id, ok := tok.value.(string); ok {
fmt.printf(
"<a href=\"#fn-%s\" id=\"ref-%s\"><sup>%s</sup></a>",
escape_html(id),
escape_html(id),
escape_html(id),
)
}
case token.TokenType.FootnoteDef:
if id, ok := tok.value.(string); ok {
fmt.printf(
"<div class=\"footnote\" id=\"fn-%s\"><a href=\"#ref-%s\">^</a> ",
escape_html(id),
escape_html(id),
)
for child in tok.children {
print_html5(child)
}
fmt.print("</div>\n")
}
case token.TokenType.CodeBlock:
lang := ""
if v, ok := tok.value.(string); ok && v != "" {
lang = v
}
if lang != "" {
fmt.printf("<pre><code class=\"lang-%s\">", lang)
} else {
fmt.print("<pre><code>")
}
for i := 0; i < len(tok.children); i += 1 {
print_html5(tok.children[i])
if i + 1 < len(tok.children) {
fmt.print("\n")
}
}
fmt.print("\n</code></pre>\n")
case token.TokenType.InlineCode:
if code, ok := tok.value.(string); ok {
fmt.printf("<code>%s</code>", escape_html(code))
}
case token.TokenType.HorizontalRule:
fmt.print("<hr>\n")
case token.TokenType.Underline:
fmt.print("<u>")
for child in tok.children {
print_html5(child)
}
fmt.print("</u>")
case token.TokenType.Superscript:
fmt.print("<sup>")
for child in tok.children {
print_html5(child)
}
fmt.print("</sup>")
case token.TokenType.Subscript:
fmt.print("<sub>")
for child in tok.children {
print_html5(child)
}
fmt.print("</sub>")
case token.TokenType.HeaderLink:
if target, ok := tok.value.(string); ok {
slug := slugifyifyifyify(target)
fmt.printf("<a href=\"#%s\">", escape_html(slug))
for child in tok.children {
print_html5(child)
}
fmt.print("</a>")
}
case token.TokenType.Link:
if url, ok := tok.value.(string); ok {
fmt.printf("<a href=\"%s\">", escape_html(url))
for child in tok.children {
print_html5(child)
}
fmt.print("</a>")
}
case token.TokenType.Image:
if url, ok := tok.value.(string); ok {
b := strings.builder_make(context.temp_allocator)
for child in tok.children {
get_text_content(child, &b)
}
alt_text := strings.to_string(b)
fmt.printf("<img src=\"%s\" alt=\"%s\">", escape_html(url), escape_html(alt_text))
}
case token.TokenType.Audio:
if url, ok := tok.value.(string); ok {
fmt.printf("<audio controls src=\"%s\"></audio>", escape_html(url))
}
case token.TokenType.Video:
if url, ok := tok.value.(string); ok {
fmt.printf("<video controls src=\"%s\"></video>", escape_html(url))
}
case token.TokenType.Iframe:
if url, ok := tok.value.(string); ok {
fmt.printf("<iframe src=\"%s\"></iframe>", escape_html(url))
}
case token.TokenType.Script:
if url, ok := tok.value.(string); ok {
fmt.printf("<script src=\"%s\"></script>", escape_html(url))
}
case token.TokenType.ScriptBlock:
if content, ok := tok.value.(string); ok {
fmt.printf("<script>%s</script>", content)
}
case token.TokenType.Variable:
if name, ok := tok.value.(string); ok {
fmt.printf("<span data-tlm-var=\"%s\">", escape_html(name))
for child in tok.children {
print_html5(child)
}
fmt.print("</span>")
}
case token.TokenType.VariableDef:
if name, ok := tok.value.(string); ok {
b := strings.builder_make(context.temp_allocator)
for child in tok.children {
get_text_content(child, &b)
}
val := strings.to_string(b)
fmt.printf(
"<template data-tlm-def=\"%s\" data-tlm-value=\"%s\"></template>",
escape_html(name),
escape_html(val),
)
}
case token.TokenType.BreakLine:
fmt.print("<br>\n")
case token.TokenType.Text:
if v, ok := tok.value.(string); ok {
fmt.print(escape_html(v))
}
for child in tok.children {
print_html5(child)
}
}
}
+27
View File
@@ -0,0 +1,27 @@
package renderer
import "../token"
import "core:fmt"
OutputMode :: enum {
HTML5,
HTML4,
}
DocumentFlags :: struct {
plain: bool,
no_js: bool,
title: string,
print_tree: bool,
help: bool,
mode: OutputMode,
}
print_document :: proc(root: ^token.Token, flags: DocumentFlags) {
switch flags.mode {
case .HTML5:
print_html5_document(root, flags)
case .HTML4:
print_html4_document(root, flags)
}
}