Moon Healing Reformation!!!

(I know it would be refactoration, but that sounds much worse)
This commit is contained in:
2026-07-16 09:40:32 +02:00
parent 033039858b
commit 69e5c62382
15 changed files with 1775 additions and 1661 deletions
+82 -2
View File
@@ -1,6 +1,5 @@
! TypoML ! TypoML
!! Installation !! Installation
TypoML requires the Odin compiler. Build from source: TypoML requires the Odin compiler. Build from source:
@@ -19,6 +18,8 @@ TypoML reads from stdin and outputs HTML to stdout:
cat input.tlm | ./TypoML > output.html cat input.tlm | ./TypoML > output.html
``` ```
---
!! Syntax !! Syntax
!!! Text Formatting !!! Text Formatting
@@ -31,6 +32,8 @@ cat input.tlm | ./TypoML > output.html
`_` - H_2_O_4_U - Subscript `_` - H_2_O_4_U - Subscript
`^` - Δ = b^2^ - 4ac - Superscript `^` - Δ = b^2^ - 4ac - Superscript
---
!!! Headings !!! Headings
\! Heading 1 \! Heading 1
@@ -38,6 +41,14 @@ cat input.tlm | ./TypoML > output.html
\!!! Heading 3 \!!! Heading 3
(And so on) (And so on)
Result:
! Heading 1
!! Heading 2
!!! Heading 3
---
!!! Links !!! Links
\#[Link text](url) - Standard link \#[Link text](url) - Standard link
@@ -45,6 +56,15 @@ cat input.tlm | ./TypoML > output.html
\#[url] - Auto-link (no alt text) \#[url] - Auto-link (no alt text)
[Link text](\#heading) - Header link (anchor) (can also be done like an auto-link, then the text is auto-generated from the heading) [Link text](\#heading) - Header link (anchor) (can also be done like an auto-link, then the text is auto-generated from the heading)
Result:
#[Link text](https://example.com) - Standard link
#(https://example.com)[Link text] - Can go either way
#[https://example.com] - Auto-link (no alt text)
[Link text](#headings) - Header link (anchor)
---
!!! Alignment !!! Alignment
Placed at the end of start of a line Placed at the end of start of a line
@@ -53,6 +73,14 @@ Placed at the end of start of a line
\<< - Left aligned \<< - Left aligned
\^^ - Center aligned \^^ - Center aligned
Result:
This text is right aligned >>
This text is left aligned <<
This text is center aligned ^^
---
!!! Embedded Content !!! Embedded Content
\$[url] - Auto-detect type \$[url] - Auto-detect type
@@ -68,6 +96,13 @@ Types:
- `iframe`, `frame`, `f` - `iframe`, `frame`, `f`
- `script`, `s`, `javascript`, `js` - `script`, `s`, `javascript`, `js`
Result:
$[https://upload.wikimedia.org/wikipedia/commons/b/b2/Voynich_manuscript_recipe_example_107r_crop.jpg|img]
$[staircase.mp3]
---
!!! Lists !!! Lists
- Item 1 - Item 1
@@ -80,6 +115,8 @@ Unordered lists use - or . as markers.
Ordered lists use numbers followed by periods. Ordered lists use numbers followed by periods.
---
!!! Code Blocks !!! Code Blocks
```language ```language
@@ -88,6 +125,8 @@ code here
Inline code uses backticks: `code` Inline code uses backticks: `code`
---
!!! Tables !!! Tables
| Header 1 | Header 2 | Header 3 | | Header 1 | Header 2 | Header 3 |
@@ -97,11 +136,20 @@ Inline code uses backticks: `code`
Do not need a header to work, just the first separator row (`|---|---|---|`) Do not need a header to work, just the first separator row (`|---|---|---|`)
Can be aligned using the alignment markers Can be aligned using the alignment markers
---
!!! Blockquotes !!! Blockquotes
\> This is a blockquote \> This is a blockquote
\> Multiple lines are supported \> Multiple lines are supported
Result:
> This is a blockquote
> Multiple lines are supported
---
!!! Footnotes !!! Footnotes
[\&footnote] - Footnote reference [\&footnote] - Footnote reference
@@ -109,23 +157,55 @@ Can be aligned using the alignment markers
Can be numbers or names Can be numbers or names
Result:
Here is a footnote reference [&demo]
[&&demo] And here is its definition
---
!!! Variables !!! Variables
\$(name=value) - Define a variable \$(name=value) - Define a variable
\$(name) - Use a variable \$(name) - Use a variable
Result:
$(greeting=Hello, world!)
The value of `greeting` is: $(greeting)
Variables can be accessed in JavaScript using `tlm.<name>` (without the `<>`)
---
!!! Script Blocks !!! Script Blocks
\$[script] \$[script]
JavaScript code here JavaScript code here
\$[/script] \$[/script]
Result:
$[script]
console.log("Hello from TypoML!");
$[/script]
(Open your browser's console to see the output)
---
!!! Escaping !!! Escaping
Standard escaping using a backslash (`\`) Standard escaping using a backslash (`\`)
!!! Horizontal Rules !!! Horizontal Rules
\--\-, \__\_, or \==\= \---, \___, or \===
Three or more, have to be on their own line Three or more, have to be on their own line
Result:
---
-468
View File
@@ -1,468 +0,0 @@
package main
import "core:fmt"
import "core:strings"
import "core:unicode"
escape_html :: proc(s: string, allocator := context.temp_allocator) -> string {
escaped := decode_escapes(s, allocator)
b := strings.builder_make(allocator)
for ch in escaped {
switch ch {
case '&':
strings.write_string(&b, "&amp;")
case '<':
strings.write_string(&b, "&lt;")
case '>':
strings.write_string(&b, "&gt;")
case '"':
strings.write_string(&b, "&quot;")
case:
strings.write_rune(&b, ch)
}
}
return strings.to_string(b)
}
get_text_content :: proc(token: ^Token, b: ^strings.Builder) {
if token.type == .Text {
if v, ok := token.value.(string); ok {
strings.write_string(b, v)
}
}
for child in token.children {
get_text_content(child, b)
}
}
check_only_var_defs :: proc(token: ^Token) -> bool {
for child in token.children {
if child.type == .VariableDef || child.type == .BreakLine {
continue
}
if child.type == .Text {
if child.value != nil {
if v, ok := child.value.(string); ok && v != "" {
return false
}
}
has_non_var_def := false
for gc in child.children {
if gc.type != .VariableDef {
has_non_var_def = true
break
}
}
if has_non_var_def {
return false
}
continue
}
return false
}
return true
}
slugifyifyifyify :: proc(text: string) -> string {
b := strings.builder_make(context.temp_allocator)
for ch in text {
lower_ch := unicode.to_lower(ch)
if unicode.is_letter(lower_ch) || unicode.is_digit(lower_ch) {
strings.write_rune(&b, lower_ch)
} else if lower_ch == ' ' || lower_ch == '_' || lower_ch == '-' {
strings.write_rune(&b, '-')
}
}
res := strings.to_string(b)
for {
new_res, replaced := strings.replace_all(res, "--", "-", context.temp_allocator)
if !replaced {
break
}
res = new_res
}
res = strings.trim(res, "-")
return res
}
print_html :: proc(token: ^Token) {
switch token.type {
case TokenType.Root:
has_footnotes := false
for child in token.children {
if child.type == .FootnoteDef && !has_footnotes {
fmt.print("<hr class=\"footnotes-sep\">\n")
has_footnotes = true
}
print_html(child)
}
case TokenType.Header:
level := 1
if v, ok := token.value.(int); ok {
level = v
}
b := strings.builder_make(context.temp_allocator)
for child in token.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 token.children {
print_html(child)
}
fmt.printf("</h%d>\n", level)
case TokenType.Paragraph:
only_var_defs := check_only_var_defs(token)
if !only_var_defs {
fmt.print("<p>")
for child in token.children {
print_html(child)
}
fmt.print("</p>\n")
} else {
for child in token.children {
if child.type != .BreakLine {
print_html(child)
}
}
}
case TokenType.Blockquote:
fmt.print("<blockquote>\n")
for child in token.children {
print_html(child)
}
fmt.print("</blockquote>\n")
case TokenType.UnorderedList:
fmt.print("<ul>\n")
for child in token.children {
print_html(child)
}
fmt.print("</ul>\n")
case TokenType.OrderedList:
fmt.print("<ol>\n")
for child in token.children {
print_html(child)
}
fmt.print("</ol>\n")
case TokenType.ListItem:
fmt.print("<li>")
for child in token.children {
print_html(child)
}
fmt.print("</li>\n")
case TokenType.Bold:
fmt.print("<strong>")
for child in token.children {
print_html(child)
}
fmt.print("</strong>")
case TokenType.Italics:
fmt.print("<em>")
for child in token.children {
print_html(child)
}
fmt.print("</em>")
case TokenType.Strikethrough:
fmt.print("<strike>")
for child in token.children {
print_html(child)
}
fmt.print("</strike>")
case TokenType.Table:
fmt.print("<table>\n")
for child in token.children {
print_html(child)
}
fmt.print("</table>\n")
case TokenType.TableHeaderRow:
fmt.print("<tr>")
for child in token.children {
align := ""
if len(child.children) > 0 {
first_child := child.children[0]
if first_child.type == .Left {
align = "left"
} else if first_child.type == .Right {
align = "right"
} else if first_child.type == .Middle {
align = "center"
}
}
if align != "" {
fmt.printf("<th align=\"%s\">", align)
for grandchild in child.children[0].children {
print_html(grandchild)
}
} else {
fmt.print("<th>")
for grandchild in child.children {
print_html(grandchild)
}
}
fmt.print("</th>")
}
fmt.print("</tr>\n")
case TokenType.TableRow:
fmt.print("<tr>")
for child in token.children {
print_html(child)
}
fmt.print("</tr>\n")
case TokenType.TableCell:
align := ""
if len(token.children) > 0 {
first_child := token.children[0]
if first_child.type == .Left {
align = "left"
} else if first_child.type == .Right {
align = "right"
} else if first_child.type == .Middle {
align = "center"
}
}
if align != "" {
fmt.printf("<td align=\"%s\">", align)
for child in token.children[0].children {
print_html(child)
}
} else {
fmt.print("<td>")
for child in token.children {
print_html(child)
}
}
fmt.print("</td>")
case TokenType.TableSeparator:
fmt.print("<tr class=\"table-sep\"><td colspan=\"100\"></td></tr>\n")
case TokenType.Left:
fmt.print("<div align=\"left\">")
for child in token.children {
print_html(child)
}
fmt.print("</div>")
case TokenType.Right:
fmt.print("<div align=\"right\">")
for child in token.children {
print_html(child)
}
fmt.print("</div>")
case TokenType.Middle:
fmt.print("<div align=\"center\">")
for child in token.children {
print_html(child)
}
fmt.print("</div>")
case TokenType.FootnoteRef:
if id, ok := token.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 TokenType.FootnoteDef:
if id, ok := token.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 token.children {
print_html(child)
}
fmt.print("</div>\n")
}
case TokenType.CodeBlock:
lang := ""
if v, ok := token.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(token.children); i += 1 {
print_html(token.children[i])
if i + 1 < len(token.children) {
fmt.print("\n")
}
}
fmt.print("\n</code></pre>\n")
case TokenType.InlineCode:
if code, ok := token.value.(string); ok {
fmt.printf("<code>%s</code>", escape_html(code))
}
case TokenType.HorizontalRule:
fmt.print("<hr>\n")
case TokenType.Underline:
fmt.print("<u>")
for child in token.children {
print_html(child)
}
fmt.print("</u>")
case TokenType.Superscript:
fmt.print("<sup>")
for child in token.children {
print_html(child)
}
fmt.print("</sup>")
case TokenType.Subscript:
fmt.print("<sub>")
for child in token.children {
print_html(child)
}
fmt.print("</sub>")
case TokenType.HeaderLink:
if target, ok := token.value.(string); ok {
slug := slugifyifyifyify(target)
fmt.printf("<a href=\"#%s\">", escape_html(slug))
for child in token.children {
print_html(child)
}
fmt.print("</a>")
}
case TokenType.Link:
if url, ok := token.value.(string); ok {
fmt.printf("<a href=\"%s\">", escape_html(url))
for child in token.children {
print_html(child)
}
fmt.print("</a>")
}
case TokenType.Image:
if url, ok := token.value.(string); ok {
b := strings.builder_make(context.temp_allocator)
for child in token.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 TokenType.Audio:
if url, ok := token.value.(string); ok {
mime := detect_mime_type(url)
b := strings.builder_make(context.temp_allocator)
for child in token.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))
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")
}
case TokenType.Video:
if url, ok := token.value.(string); ok {
mime := detect_mime_type(url)
b := strings.builder_make(context.temp_allocator)
for child in token.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))
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")
}
case TokenType.Iframe:
if url, ok := token.value.(string); ok {
fmt.printf("<iframe src=\"%s\"></iframe>", escape_html(url))
}
case TokenType.Script:
if url, ok := token.value.(string); ok {
fmt.printf("<script src=\"%s\"></script>", escape_html(url))
}
case TokenType.ScriptBlock:
if content, ok := token.value.(string); ok {
fmt.printf("<script>%s</script>", content)
}
case TokenType.Variable:
if name, ok := token.value.(string); ok {
fmt.printf("<span data-tlm-var=\"%s\">", escape_html(name))
for child in token.children {
print_html(child)
}
fmt.print("</span>")
}
case TokenType.VariableDef:
if name, ok := token.value.(string); ok {
b := strings.builder_make(context.temp_allocator)
for child in token.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 TokenType.BreakLine:
fmt.print("<br>\n")
case TokenType.Text:
if v, ok := token.value.(string); ok {
fmt.print(escape_html(v))
}
for child in token.children {
print_html(child)
}
}
}
+47 -44
View File
@@ -1,5 +1,8 @@
package main package main
import "./parser"
import "./renderer"
import "./token"
import "core:fmt" import "core:fmt"
import "core:os" import "core:os"
import "core:strings" import "core:strings"
@@ -10,91 +13,91 @@ read_stdin :: proc() -> []string {
return strings.split_lines(content) return strings.split_lines(content)
} }
print_token :: proc(token: ^Token, depth: int = 0) { print_token :: proc(tok: ^token.Token, depth: int = 0) {
indent := strings.repeat(" ", depth, context.temp_allocator) indent := strings.repeat(" ", depth, context.temp_allocator)
type_str := "" type_str := ""
switch token.type { switch tok.type {
case TokenType.Root: case token.TokenType.Root:
type_str = "Root" type_str = "Root"
case TokenType.Text: case token.TokenType.Text:
type_str = "Text" type_str = "Text"
case TokenType.Italics: case token.TokenType.Italics:
type_str = "Italics" type_str = "Italics"
case TokenType.Bold: case token.TokenType.Bold:
type_str = "Bold" type_str = "Bold"
case TokenType.BreakLine: case token.TokenType.BreakLine:
type_str = "BreakLine" type_str = "BreakLine"
case TokenType.Header: case token.TokenType.Header:
type_str = "Header" type_str = "Header"
case TokenType.Paragraph: case token.TokenType.Paragraph:
type_str = "Paragraph" type_str = "Paragraph"
case TokenType.Blockquote: case token.TokenType.Blockquote:
type_str = "Blockquote" type_str = "Blockquote"
case TokenType.UnorderedList: case token.TokenType.UnorderedList:
type_str = "UnorderedList" type_str = "UnorderedList"
case TokenType.OrderedList: case token.TokenType.OrderedList:
type_str = "OrderedList" type_str = "OrderedList"
case TokenType.ListItem: case token.TokenType.ListItem:
type_str = "ListItem" type_str = "ListItem"
case TokenType.Strikethrough: case token.TokenType.Strikethrough:
type_str = "Strikethrough" type_str = "Strikethrough"
case TokenType.Table: case token.TokenType.Table:
type_str = "Table" type_str = "Table"
case TokenType.TableRow: case token.TokenType.TableRow:
type_str = "TableRow" type_str = "TableRow"
case TokenType.TableHeaderRow: case token.TokenType.TableHeaderRow:
type_str = "TableHeaderRow" type_str = "TableHeaderRow"
case TokenType.TableCell: case token.TokenType.TableCell:
type_str = "TableCell" type_str = "TableCell"
case TokenType.TableSeparator: case token.TokenType.TableSeparator:
type_str = "TableSeparator" type_str = "TableSeparator"
case TokenType.Left: case token.TokenType.Left:
type_str = "Left" type_str = "Left"
case TokenType.Right: case token.TokenType.Right:
type_str = "Right" type_str = "Right"
case TokenType.Middle: case token.TokenType.Middle:
type_str = "Middle" type_str = "Middle"
case TokenType.FootnoteDef: case token.TokenType.FootnoteDef:
type_str = "FootnoteDef" type_str = "FootnoteDef"
case TokenType.FootnoteRef: case token.TokenType.FootnoteRef:
type_str = "FootnoteRef" type_str = "FootnoteRef"
case TokenType.CodeBlock: case token.TokenType.CodeBlock:
type_str = "CodeBlock" type_str = "CodeBlock"
case TokenType.InlineCode: case token.TokenType.InlineCode:
type_str = "InlineCode" type_str = "InlineCode"
case TokenType.HorizontalRule: case token.TokenType.HorizontalRule:
type_str = "HorizontalRule" type_str = "HorizontalRule"
case TokenType.Underline: case token.TokenType.Underline:
type_str = "Underline" type_str = "Underline"
case TokenType.Superscript: case token.TokenType.Superscript:
type_str = "Superscript" type_str = "Superscript"
case TokenType.Subscript: case token.TokenType.Subscript:
type_str = "Subscript" type_str = "Subscript"
case TokenType.HeaderLink: case token.TokenType.HeaderLink:
type_str = "HeaderLink" type_str = "HeaderLink"
case TokenType.Link: case token.TokenType.Link:
type_str = "Link" type_str = "Link"
case TokenType.Image: case token.TokenType.Image:
type_str = "Image" type_str = "Image"
case TokenType.Audio: case token.TokenType.Audio:
type_str = "Audio" type_str = "Audio"
case TokenType.Video: case token.TokenType.Video:
type_str = "Video" type_str = "Video"
case TokenType.Iframe: case token.TokenType.Iframe:
type_str = "Iframe" type_str = "Iframe"
case TokenType.Script: case token.TokenType.Script:
type_str = "Script" type_str = "Script"
case TokenType.ScriptBlock: case token.TokenType.ScriptBlock:
type_str = "ScriptBlock" type_str = "ScriptBlock"
case TokenType.Variable: case token.TokenType.Variable:
type_str = "Variable" type_str = "Variable"
case TokenType.VariableDef: case token.TokenType.VariableDef:
type_str = "VariableDef" type_str = "VariableDef"
} }
value_str := "" value_str := ""
switch v in token.value { switch v in tok.value {
case string: case string:
value_str = v value_str = v
case int: case int:
@@ -108,13 +111,13 @@ print_token :: proc(token: ^Token, depth: int = 0) {
} }
fmt.println() fmt.println()
for child in token.children { for child in tok.children {
print_token(child, depth + 1) print_token(child, depth + 1)
} }
} }
main :: proc() { main :: proc() {
lines := read_stdin() lines := read_stdin()
parsed := parse(lines) parsed := parser.parse(lines)
print_html(&parsed) renderer.print_html(&parsed)
} }
+1 -1
View File
@@ -1,4 +1,4 @@
package main package mime
import "core:strings" import "core:strings"
-1146
View File
File diff suppressed because it is too large Load Diff
+204
View File
@@ -0,0 +1,204 @@
package parser
import "../mime"
import "../token"
import "core:strings"
find_script_block :: proc(
str: string,
) -> (
start_idx, end_idx: int,
is_block: bool,
block_type: string,
content: string,
) {
for i := 0; i < len(str); i += 1 {
if str[i] == '$' && i + 1 < len(str) && str[i + 1] == '[' {
if end, inner, ok := match_single(str[i + 1:], '[', ']'); ok {
inner = strings.trim_space(inner)
if strings.has_prefix(inner, "/") {
continue
}
if inner == "script" || inner == "js" || inner == "javascript" {
b := strings.builder_make(context.temp_allocator)
strings.write_string(&b, "$[/")
strings.write_string(&b, inner)
strings.write_string(&b, "]")
close_tag := strings.to_string(b)
close_idx := strings.index(str[i + 1 + end:], close_tag)
if close_idx != -1 {
close_idx += i + 1 + end
content := str[i + 1 + end:close_idx]
return i, close_idx + len(
close_tag,
), true, inner, strings.trim_space(content)
}
}
}
}
}
return -1, -1, false, "", ""
}
find_embed :: proc(
str: string,
) -> (
start_idx, end_idx: int,
is_embed: bool,
url: string,
alt: string,
embed_type: string,
) {
for i := 0; i < len(str); i += 1 {
if str[i] == '$' && i + 1 < len(str) {
if str[i + 1] == '[' {
if end, alt_text, url_text, ok := match_pair(str[i + 1:], '[', ']', '(', ')'); ok {
clean_url := url_text
type_hint := ""
if pipe_idx := strings.index_byte(url_text, '|'); pipe_idx != -1 {
clean_url = url_text[:pipe_idx]
type_hint = url_text[pipe_idx + 1:]
}
if mime.is_url(clean_url) {
e_type := ""
if type_hint != "" {
e_type = mime.parse_type_hint(type_hint)
}
if e_type == "" {
e_type = mime.detect_embed_type(clean_url)
}
return i, i + 1 + end, true, clean_url, alt_text, e_type
}
}
if end, inner, ok := match_single(str[i + 1:], '[', ']'); ok {
url_text := inner
type_hint := ""
if pipe_idx := strings.index_byte(inner, '|'); pipe_idx != -1 {
url_text = inner[:pipe_idx]
type_hint = inner[pipe_idx + 1:]
}
if mime.is_url(url_text) {
e_type := ""
if type_hint != "" {
e_type = mime.parse_type_hint(type_hint)
}
if e_type == "" {
e_type = mime.detect_embed_type(url_text)
}
return i, i + 1 + end, true, url_text, url_text, e_type
}
}
} else if str[i + 1] == '(' {
if end, url_text, alt_text, ok := match_pair(str[i + 1:], '(', ')', '[', ']'); ok {
clean_url := url_text
type_hint := ""
if pipe_idx := strings.index_byte(url_text, '|'); pipe_idx != -1 {
clean_url = url_text[:pipe_idx]
type_hint = url_text[pipe_idx + 1:]
}
if mime.is_url(clean_url) {
e_type := ""
if type_hint != "" {
e_type = mime.parse_type_hint(type_hint)
}
if e_type == "" {
e_type = mime.detect_embed_type(clean_url)
}
return i, i + 1 + end, true, clean_url, alt_text, e_type
}
}
if end, inner, ok := match_single(str[i + 1:], '(', ')'); ok {
url_text := inner
type_hint := ""
if pipe_idx := strings.index_byte(inner, '|'); pipe_idx != -1 {
url_text = inner[:pipe_idx]
type_hint = inner[pipe_idx + 1:]
}
if mime.is_url(url_text) {
e_type := ""
if type_hint != "" {
e_type = mime.parse_type_hint(type_hint)
}
if e_type == "" {
e_type = mime.detect_embed_type(url_text)
}
return i, i + 1 + end, true, url_text, url_text, e_type
}
}
}
}
}
return -1, -1, false, "", "", ""
}
parse_embeds :: proc(tok: ^token.Token) {
if tok.type == .CodeBlock || tok.type == .InlineCode {
return
}
if tok.children != nil {
for i := 0; i < len(tok.children); i += 1 {
parse_embeds(tok.children[i])
}
}
if tok.type == .Text {
str, ok := tok.value.(string)
if !ok {
return
}
s_start, s_end, is_script, s_type, s_content := find_script_block(str)
if is_script {
tok.value = nil
if s_start > 0 {
push_token(tok, .Text, str[:s_start])
}
script_token := push_token(tok, .ScriptBlock, s_content)
if s_end < len(str) {
rem := push_token(tok, .Text, str[s_end:])
parse_embeds(rem)
}
return
}
start_idx, end_idx, is_embed, url, alt, e_type := find_embed(str)
if !is_embed {
return
}
tok.value = nil
if start_idx > 0 {
push_token(tok, .Text, str[:start_idx])
}
embed_token_type: token.TokenType
if e_type == "image" {
embed_token_type = .Image
} else if e_type == "audio" {
embed_token_type = .Audio
} else if e_type == "video" {
embed_token_type = .Video
} else if e_type == "script" {
embed_token_type = .Script
} else {
embed_token_type = .Iframe
}
embed_token := push_token(tok, embed_token_type, url)
push_token(embed_token, .Text, alt)
if end_idx < len(str) {
rem := push_token(tok, .Text, str[end_idx:])
parse_embeds(rem)
}
}
}
+124
View File
@@ -0,0 +1,124 @@
package parser
import "../token"
import "core:strings"
ESCAPE_PLACEHOLDER_BASE :: 0xE000
is_escapable_char :: proc(b: byte) -> bool {
switch b {
case '\\',
'*',
'_',
'~',
'^',
'`',
'[',
']',
'(',
')',
'$',
'&',
'!',
'>',
'<',
'|',
'-',
'=',
'#',
'.':
return true
}
return false
}
is_repeatable_token_char :: proc(b: byte) -> bool {
switch b {
case '*', '_', '~', '^', '<', '>', '-', '=', '&', '!', '.':
return true
}
return false
}
escape_placeholder_rune :: proc(b: byte) -> rune {
return rune(ESCAPE_PLACEHOLDER_BASE + int(b))
}
is_escape_placeholder :: proc(r: rune) -> bool {
return r >= ESCAPE_PLACEHOLDER_BASE && r < ESCAPE_PLACEHOLDER_BASE + 256
}
escape_placeholder_to_byte :: proc(r: rune) -> byte {
return byte(int(r) - ESCAPE_PLACEHOLDER_BASE)
}
unescape_backslashes :: proc(s: string, allocator := context.temp_allocator) -> string {
if !strings.contains(s, "\\") {
return s
}
b := strings.builder_make(allocator)
i := 0
for i < len(s) {
if s[i] == '`' {
close := strings.index_byte(s[i + 1:], '`')
if close != -1 {
end := i + 1 + close
strings.write_string(&b, s[i:end + 1])
i = end + 1
continue
}
}
if s[i] == '\\' && i + 1 < len(s) && is_escapable_char(s[i + 1]) {
esc := s[i + 1]
strings.write_rune(&b, escape_placeholder_rune(esc))
i += 2
if is_repeatable_token_char(esc) {
for i < len(s) && s[i] == esc {
strings.write_rune(&b, escape_placeholder_rune(esc))
i += 1
}
}
} else {
strings.write_byte(&b, s[i])
i += 1
}
}
return strings.to_string(b)
}
decode_escapes :: proc(s: string, allocator := context.temp_allocator) -> string {
has_placeholder := false
for r in s {
if is_escape_placeholder(r) {
has_placeholder = true
break
}
}
if !has_placeholder {
return s
}
b := strings.builder_make(allocator)
for r in s {
if is_escape_placeholder(r) {
strings.write_byte(&b, escape_placeholder_to_byte(r))
} else {
strings.write_rune(&b, r)
}
}
return strings.to_string(b)
}
decode_text_escapes :: proc(tok: ^token.Token) {
if tok.type == .Text {
if str, ok := tok.value.(string); ok {
tok.value = decode_escapes(str)
}
}
if tok.children != nil {
for child in tok.children {
decode_text_escapes(child)
}
}
}
+470
View File
@@ -0,0 +1,470 @@
package parser
import "../mime"
import "../token"
import "core:strings"
StyleToggle :: struct {
amount: int,
type: token.TokenType,
}
parse_inline_style :: proc(tok: ^token.Token, symbol: byte, toggles: []StyleToggle) {
if tok.children != nil {
for i := 0; i < len(tok.children); i += 1 {
parse_inline_style(tok.children[i], symbol, toggles)
}
}
if tok.type == .Text {
str, ok := tok.value.(string)
if !ok {
return
}
symbol_found := false
for i := 0; i < len(str); i += 1 {
if str[i] == symbol {
symbol_found = true
break
}
}
if !symbol_found {
return
}
tok.value = nil
active_styles: [256]bool
current_node := tok
i := 0
start := 0
for i < len(str) {
if str[i] == symbol {
if i > start {
push_token(current_node, token.TokenType.Text, str[start:i])
}
count := 0
symbol_start := i
for i < len(str) && str[i] == symbol {
count += 1
i += 1
}
preceded_by_space := symbol_start > 0 && is_space_byte(str[symbol_start - 1])
followed_by_space := i < len(str) && is_space_byte(str[i])
if preceded_by_space && followed_by_space {
push_token(current_node, token.TokenType.Text, str[symbol_start:i])
start = i
continue
}
for count > 0 {
matched := false
for toggle in toggles {
if count >= toggle.amount {
active_styles[int(toggle.type)] = !active_styles[int(toggle.type)]
count -= toggle.amount
matched = true
break
}
}
if !matched {
push_token(
current_node,
token.TokenType.Text,
str[symbol_start:symbol_start + count],
)
count = 0
break
}
}
current_node = tok
for toggle in toggles {
if active_styles[int(toggle.type)] {
last :=
current_node.children[len(current_node.children) - 1] if len(current_node.children) > 0 else nil
current_node =
last if last != nil && last.type == toggle.type else push_token(current_node, toggle.type, nil)
}
}
start = i
} else {
i += 1
}
}
if i > start {
push_token(current_node, token.TokenType.Text, str[start:i])
}
}
}
parse_inline_code :: proc(tok: ^token.Token) {
if tok.type == .CodeBlock || tok.type == .InlineCode {
return
}
if tok.children != nil {
for i := 0; i < len(tok.children); i += 1 {
parse_inline_code(tok.children[i])
}
}
if tok.type == .Text {
str, ok := tok.value.(string)
if !ok {
return
}
start_idx := strings.index_byte(str, '`')
if start_idx == -1 {
return
}
end_idx := strings.index_byte(str[start_idx + 1:], '`')
if end_idx == -1 {
return
}
end_idx += start_idx + 1
code := str[start_idx + 1:end_idx]
tok.value = nil
if start_idx > 0 {
push_token(tok, .Text, str[:start_idx])
}
push_token(tok, .InlineCode, code)
if end_idx + 1 < len(str) {
rem := push_token(tok, .Text, str[end_idx + 1:])
parse_inline_code(rem)
}
}
}
parse_inline_footnotes :: proc(tok: ^token.Token) {
if tok.type == .CodeBlock || tok.type == .InlineCode {
return
}
if tok.children != nil {
for i := 0; i < len(tok.children); i += 1 {
parse_inline_footnotes(tok.children[i])
}
}
if tok.type == .Text {
str, ok := tok.value.(string)
if !ok {
return
}
start_idx := strings.index(str, "[&")
if start_idx == -1 {
return
}
end_idx := strings.index_byte(str[start_idx:], ']')
if end_idx == -1 {
return
}
end_idx += start_idx
id := str[start_idx + 2:end_idx]
tok.value = nil
if start_idx > 0 {
push_token(tok, .Text, str[:start_idx])
}
push_token(tok, .FootnoteRef, id)
if end_idx + 1 < len(str) {
rem := push_token(tok, .Text, str[end_idx + 1:])
parse_inline_footnotes(rem)
}
}
}
match_pair :: proc(s: string, open1, close1, open2, close2: byte) -> (int, string, string, bool) {
if len(s) == 0 || s[0] != open1 {
return 0, "", "", false
}
depth := 1
close1_idx := -1
for i := 1; i < len(s); i += 1 {
if s[i] == open1 {
depth += 1
} else if s[i] == close1 {
depth -= 1
if depth == 0 {
close1_idx = i
break
}
}
}
if close1_idx == -1 {
return 0, "", "", false
}
if close1_idx + 1 < len(s) && s[close1_idx + 1] == open2 {
depth = 1
close2_idx := -1
for i := close1_idx + 2; i < len(s); i += 1 {
if s[i] == open2 {
depth += 1
} else if s[i] == close2 {
depth -= 1
if depth == 0 {
close2_idx = i
break
}
}
}
if close2_idx != -1 {
return close2_idx + 1, s[1:close1_idx], s[close1_idx + 2:close2_idx], true
}
}
return 0, "", "", false
}
match_single :: proc(s: string, open, close: byte) -> (int, string, bool) {
if len(s) == 0 || s[0] != open {
return 0, "", false
}
close_idx := strings.index_byte(s, close)
if close_idx == -1 {
return 0, "", false
}
return close_idx + 1, s[1:close_idx], true
}
find_link :: proc(
str: string,
) -> (
start_idx, end_idx: int,
is_link: bool,
url: string,
alt: string,
is_header: bool,
) {
for i := 0; i < len(str); i += 1 {
if str[i] == '#' && i + 1 < len(str) {
if str[i + 1] == '[' {
if end, alt_text, url_text, ok := match_pair(str[i + 1:], '[', ']', '(', ')'); ok {
return i, i + 1 + end, true, url_text, alt_text, false
}
if end, inner, ok := match_single(str[i + 1:], '[', ']'); ok {
return i, i + 1 + end, true, inner, inner, false
}
} else if str[i + 1] == '(' {
if end, url_text, alt_text, ok := match_pair(str[i + 1:], '(', ')', '[', ']'); ok {
return i, i + 1 + end, true, url_text, alt_text, false
}
if end, inner, ok := match_single(str[i + 1:], '(', ')'); ok {
return i, i + 1 + end, true, inner, inner, false
}
}
}
if str[i] == '[' {
if end, alt_text, url_text, ok := match_pair(str[i:], '[', ']', '(', ')'); ok {
if strings.has_prefix(url_text, "#") {
return i, i + end, true, url_text[1:], alt_text, true
}
}
if end, inner, ok := match_single(str[i:], '[', ']'); ok {
if strings.has_prefix(inner, "#") {
return i, i + end, true, inner[1:], inner[1:], true
}
}
} else if str[i] == '(' {
if end, url_text, alt_text, ok := match_pair(str[i:], '(', ')', '[', ']'); ok {
if strings.has_prefix(url_text, "#") {
return i, i + end, true, url_text[1:], alt_text, true
}
}
if end, inner, ok := match_single(str[i:], '(', ')'); ok {
if strings.has_prefix(inner, "#") {
return i, i + end, true, inner[1:], inner[1:], true
}
}
}
}
return -1, -1, false, "", "", false
}
parse_links :: proc(tok: ^token.Token) {
if tok.type == .CodeBlock || tok.type == .InlineCode {
return
}
if tok.children != nil {
for i := 0; i < len(tok.children); i += 1 {
parse_links(tok.children[i])
}
}
if tok.type == .Text {
str, ok := tok.value.(string)
if !ok {
return
}
start_idx, end_idx, is_link, url, alt, is_header := find_link(str)
if !is_link {
return
}
tok.value = nil
if start_idx > 0 {
push_token(tok, .Text, str[:start_idx])
}
link_token := push_token(tok, .HeaderLink if is_header else .Link, url)
push_token(link_token, .Text, alt)
if end_idx < len(str) {
rem := push_token(tok, .Text, str[end_idx:])
parse_links(rem)
}
}
}
find_variable :: proc(
str: string,
) -> (
start_idx, end_idx: int,
is_var: bool,
name: string,
val: string,
is_def: bool,
) {
for i := 0; i < len(str); i += 1 {
if str[i] == '$' && i + 1 < len(str) && str[i + 1] == '(' {
if end, inner, ok := match_single(str[i + 1:], '(', ')'); ok {
eq := strings.index_byte(inner, '=')
if eq != -1 {
return i,
i + 1 + end,
true,
strings.trim_space(inner[:eq]),
strings.trim_space(inner[eq + 1:]),
true
} else if mime.is_url(inner) {
continue
} else {
return i, i + 1 + end, true, strings.trim_space(inner), "", false
}
}
}
}
return -1, -1, false, "", "", false
}
parse_variables :: proc(tok: ^token.Token) {
if tok.type == .CodeBlock || tok.type == .InlineCode {
return
}
if tok.children != nil {
for i := 0; i < len(tok.children); i += 1 {
parse_variables(tok.children[i])
}
}
if tok.type == .Text {
str, ok := tok.value.(string)
if !ok {
return
}
start_idx, end_idx, is_var, name, val, is_def := find_variable(str)
if !is_var {
return
}
tok.value = nil
if start_idx > 0 {
push_token(tok, .Text, str[:start_idx])
}
if is_def {
def_token := push_token(tok, .VariableDef, name)
push_token(def_token, .Text, val)
} else {
push_token(tok, .Variable, name)
}
if end_idx < len(str) {
rem := push_token(tok, .Text, str[end_idx:])
parse_variables(rem)
}
}
}
resolve_variables :: proc(tok: ^token.Token, vars: ^map[string]string) {
if tok.type == .VariableDef {
if name, ok := tok.value.(string); ok {
if len(tok.children) > 0 {
if val, vok := tok.children[0].value.(string); vok {
vars[name] = val
}
}
}
return
}
if tok.type == .Variable {
if name, ok := tok.value.(string); ok {
if val, found := vars[name]; found {
if tok.children == nil {
push_token(tok, .Text, val)
}
}
}
return
}
if tok.type == .Link || tok.type == .HeaderLink {
if url, ok := tok.value.(string); ok {
for i := 0; i < len(url); i += 1 {
if url[i] == '$' && i + 1 < len(url) && url[i + 1] == '(' {
if end, inner, ok := match_single(url[i + 1:], '(', ')'); ok {
var_name := strings.trim_space(inner)
if val, found := vars[var_name]; found {
b := strings.builder_make(context.temp_allocator)
strings.write_string(&b, url[:i])
strings.write_string(&b, val)
strings.write_string(&b, url[i + 1 + end:])
tok.value = strings.to_string(b)
break
}
}
}
}
}
}
if tok.children != nil {
for child in tok.children {
resolve_variables(child, vars)
}
}
}
pass_through_children :: proc(tok: ^token.Token) {
if tok.type == .CodeBlock || tok.type == .InlineCode {
return
}
parse_inline_style(tok, '*', []StyleToggle{{2, .Bold}, {1, .Italics}})
parse_inline_style(tok, '~', []StyleToggle{{2, .Strikethrough}})
parse_inline_style(tok, '-', []StyleToggle{{2, .Strikethrough}})
parse_inline_style(tok, '^', []StyleToggle{{1, .Superscript}})
parse_inline_style(tok, '_', []StyleToggle{{2, .Underline}, {1, .Subscript}})
}
+211
View File
@@ -0,0 +1,211 @@
package parser
import "../token"
import "core:strconv"
import "core:strings"
parse :: proc(lines: []string, allocator := context.allocator) -> token.Token {
root := token.Token{token.TokenType.Root, nil, nil, nil}
current_token := &root
in_code_block := false
in_script_block := false
script_block_content: [dynamic]string
script_block_type := ""
for untrimmed_line, line_index in lines {
line := strings.trim_space(untrimmed_line)
if in_code_block {
if strings.has_prefix(line, "```") {
in_code_block = false
current_token = &root
} else {
push_token(current_token, token.TokenType.Text, untrimmed_line)
}
continue
}
if in_script_block {
if strings.has_prefix(line, "$[/") {
full_content := strings.join(script_block_content[:], "\n")
script_token := push_token(&root, token.TokenType.ScriptBlock, full_content)
push_token(script_token, token.TokenType.Text, script_block_type)
in_script_block = false
script_block_content = nil
script_block_type = ""
current_token = &root
} else {
append(&script_block_content, untrimmed_line)
}
continue
}
line = unescape_backslashes(line)
if strings.has_prefix(line, "```") {
lang := strings.trim_space(line[3:])
current_token = push_token(&root, token.TokenType.CodeBlock, lang)
in_code_block = true
continue
}
if strings.has_prefix(line, "$[") && strings.has_suffix(line, "]") {
inner := strings.trim_space(line[2:len(line) - 1])
if inner == "script" || inner == "js" || inner == "javascript" {
in_script_block = true
script_block_type = inner
script_block_content = nil
current_token = &root
continue
}
}
if len(line) >= 3 {
is_hr := true
ch := line[0]
if ch == '-' || ch == '_' || ch == '=' {
for i := 1; i < len(line); i += 1 {
if line[i] != ch {
is_hr = false
break
}
}
} else {
is_hr = false
}
if is_hr {
push_token(&root, token.TokenType.HorizontalRule, nil)
current_token = &root
continue
}
}
if len(line) == 0 {
current_token = &root
continue
}
header_level, text := parse_header(line)
if header_level > 0 {
current_token = push_token(&root, token.TokenType.Header, header_level)
push_text(current_token, text)
current_token = &root
continue
}
if id, ftext, ok := parse_footnote_def(line); ok {
current_token = push_token(&root, token.TokenType.FootnoteDef, id)
push_text(current_token, ftext)
current_token = &root
continue
}
if strings.has_prefix(line, "> ") {
if current_token.type != token.TokenType.Blockquote {
current_token = push_token(&root, token.TokenType.Blockquote, nil)
push_token(current_token, token.TokenType.Paragraph, nil)
}
last_p := current_token.children[len(current_token.children) - 1]
push_text_line(last_p, line[2:])
continue
}
if strings.has_prefix(line, ". ") || strings.has_prefix(line, "- ") {
if current_token.type != token.TokenType.UnorderedList {
current_token = push_token(&root, token.TokenType.UnorderedList, nil)
}
li := push_token(current_token, token.TokenType.ListItem, nil)
push_text(li, line[2:])
continue
}
if len(line) >= 3 {
item := 1
idx := 0
for idx < len(line) && line[idx] >= '0' && line[idx] <= '9' {
idx += 1
}
if idx > 0 && idx + 1 < len(line) && line[idx] == '.' && line[idx + 1] == ' ' {
item, _ = strconv.parse_int(line[:idx])
remaining_text := line[idx + 2:]
if current_token.type != token.TokenType.OrderedList {
current_token = push_token(&root, token.TokenType.OrderedList, item)
}
li := push_token(current_token, token.TokenType.ListItem, nil)
push_text(li, remaining_text)
continue
}
}
if len(line) >= 3 && line[0] == '|' && line[len(line) - 1] == '|' {
if current_token.type != token.TokenType.Table {
current_token = push_token(&root, token.TokenType.Table, nil)
}
if is_table_separator(line) {
row_count := len(current_token.children)
if row_count == 1 {
current_token.children[0].type = token.TokenType.TableHeaderRow
} else if row_count > 1 {
push_token(current_token, token.TokenType.TableSeparator, nil)
}
} else {
row := push_token(current_token, token.TokenType.TableRow, nil)
cells := parse_table_cells(line)
for cell_text in cells {
cell := push_token(row, token.TokenType.TableCell, nil)
push_text(cell, cell_text)
}
}
continue
}
if current_token.type == .Blockquote {
last_p := current_token.children[len(current_token.children) - 1]
push_text_line(last_p, line)
} else if current_token.type == .UnorderedList || current_token.type == .OrderedList {
last_li := current_token.children[len(current_token.children) - 1]
push_text_line(last_li, line)
} else {
if current_token.type != .Paragraph {
current_token = push_token(&root, token.TokenType.Paragraph, nil)
}
push_text_line(current_token, line)
}
}
for tok in root.children {
parse_inline_code(tok)
parse_inline_footnotes(tok)
parse_links(tok)
parse_variables(tok)
parse_embeds(tok)
pass_through_children(tok)
decode_text_escapes(tok)
}
vars: map[string]string
for child in root.children {
resolve_variables(child, &vars)
}
footnotes: [dynamic]^token.Token
new_root_children: [dynamic]^token.Token
for child in root.children {
if child.type == .FootnoteDef {
append(&footnotes, child)
} else {
append(&new_root_children, child)
}
}
for child in footnotes {
append(&new_root_children, child)
}
root.children = new_root_children
return root
}
+34
View File
@@ -0,0 +1,34 @@
package parser
import "../token"
import "core:strings"
is_table_separator :: proc(line: string) -> bool {
inner := line[1:len(line) - 1]
cells := strings.split(inner, "|", context.temp_allocator)
if len(cells) == 0 {
return false
}
for cell in cells {
trimmed := strings.trim_space(cell)
if len(trimmed) == 0 {
return false
}
for ch in trimmed {
if ch != '-' {
return false
}
}
}
return true
}
parse_table_cells :: proc(line: string) -> []string {
inner := line[1:len(line) - 1]
cells := strings.split(inner, "|", context.temp_allocator)
result := make([]string, len(cells), context.temp_allocator)
for cell, i in cells {
result[i] = strings.trim_space(cell)
}
return result
}
+106
View File
@@ -0,0 +1,106 @@
package parser
import "../token"
import "core:strings"
push_token :: proc(
parent: ^token.Token,
type: token.TokenType,
value: token.TokenValue = nil,
) -> ^token.Token {
tok := new(token.Token)
tok^ = token.Token{type, value, nil, parent}
append(&parent.children, tok)
return tok
}
is_space_byte :: proc(b: byte) -> bool {
return b == ' ' || b == '\t'
}
parse_header :: proc(line: string) -> (int, string) {
if len(line) == 0 {
return 0, line
}
lvl := 0
for lvl < len(line) && line[lvl] == '!' {
lvl += 1
}
if lvl == 0 || lvl >= len(line) || line[lvl] != ' ' {
return 0, line
}
return lvl, line[lvl + 1:]
}
parse_footnote_def :: proc(line: string) -> (string, string, bool) {
if strings.has_prefix(line, "[&&") {
idx := strings.index_byte(line, ']')
if idx != -1 {
id := line[3:idx]
text := strings.trim_space(line[idx + 1:])
return id, text, true
}
}
return "", "", false
}
push_text :: proc(parent: ^token.Token, text: string) {
align_type, has_align := get_alignment_token(text)
cleaned := strip_alignment_markers(text)
target := parent
if has_align {
target = push_token(parent, align_type, nil)
}
push_token(target, token.TokenType.Text, cleaned)
}
push_text_line :: proc(target: ^token.Token, text: string) {
if len(target.children) > 0 {
br := new(token.Token)
br^ = token.Token{token.TokenType.BreakLine, nil, nil, target}
append(&target.children, br)
}
push_text(target, text)
}
get_alignment_token :: proc(text: string) -> (token.TokenType, bool) {
trimmed := strings.trim_space(text)
if len(trimmed) == 0 {
return .Text, false
}
if strings.has_suffix(trimmed, ">>") || strings.has_prefix(trimmed, ">>") {
return .Right, true
}
if strings.has_suffix(trimmed, "<<") || strings.has_prefix(trimmed, "<<") {
return .Left, true
}
if strings.has_suffix(trimmed, "^^") || strings.has_prefix(trimmed, "^^") {
return .Middle, true
}
return .Text, false
}
strip_alignment_markers :: proc(text: string) -> string {
trimmed := strings.trim_space(text)
if strings.has_suffix(trimmed, ">>") {
return strings.trim_space(trimmed[:len(trimmed) - 2])
}
if strings.has_prefix(trimmed, ">>") {
return strings.trim_space(trimmed[2:])
}
if strings.has_suffix(trimmed, "<<") {
return strings.trim_space(trimmed[:len(trimmed) - 2])
}
if strings.has_prefix(trimmed, "<<") {
return strings.trim_space(trimmed[2:])
}
if strings.has_suffix(trimmed, "^^") {
return strings.trim_space(trimmed[:len(trimmed) - 2])
}
if strings.has_prefix(trimmed, "^^") {
return strings.trim_space(trimmed[2:])
}
return trimmed
}
+25
View File
@@ -0,0 +1,25 @@
package renderer
import "../parser"
import "core:strings"
escape_html :: proc(s: string, allocator := context.temp_allocator) -> string {
escaped := parser.decode_escapes(s, allocator)
b := strings.builder_make(allocator)
for ch in escaped {
switch ch {
case '&':
strings.write_string(&b, "&amp;")
case '<':
strings.write_string(&b, "&lt;")
case '>':
strings.write_string(&b, "&gt;")
case '"':
strings.write_string(&b, "&quot;")
case:
strings.write_rune(&b, ch)
}
}
return strings.to_string(b)
}
+330
View File
@@ -0,0 +1,330 @@
package renderer
import "../mime"
import "../token"
import "core:fmt"
import "core:strings"
render_aligned_cell :: proc(tok: ^token.Token, tag: string) {
align := get_alignment_attribute(tok)
if align != "" {
fmt.printf("<%s align=\"%s\">", tag, align)
for child in tok.children[0].children {
print_html(child)
}
} else {
fmt.printf("<%s>", tag)
for child in tok.children {
print_html(child)
}
}
fmt.printf("</%s>", tag)
}
render_simple_wrapper :: proc(tok: ^token.Token, tag: string) {
fmt.printf("<%s>", tag)
for child in tok.children {
print_html(child)
}
fmt.printf("</%s>", tag)
}
render_div_wrapper :: proc(tok: ^token.Token, align: string) {
fmt.printf("<div align=\"%s\">", align)
for child in tok.children {
print_html(child)
}
fmt.print("</div>")
}
render_container :: proc(tok: ^token.Token, tag: string) {
fmt.printf("<%s>\n", tag)
for child in tok.children {
print_html(child)
}
fmt.printf("</%s>\n", tag)
}
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")
}
print_html :: 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_html(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_html(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_html(child)
}
fmt.print("</p>\n")
} else {
for child in tok.children {
if child.type != .BreakLine {
print_html(child)
}
}
}
case token.TokenType.Blockquote:
render_container(tok, "blockquote")
case token.TokenType.UnorderedList:
render_container(tok, "ul")
case token.TokenType.OrderedList:
render_container(tok, "ol")
case token.TokenType.ListItem:
fmt.print("<li>")
for child in tok.children {
print_html(child)
}
fmt.print("</li>\n")
case token.TokenType.Bold:
render_simple_wrapper(tok, "strong")
case token.TokenType.Italics:
render_simple_wrapper(tok, "em")
case token.TokenType.Strikethrough:
render_simple_wrapper(tok, "strike")
case token.TokenType.Table:
render_container(tok, "table")
case token.TokenType.TableHeaderRow:
fmt.print("<tr>")
for child in tok.children {
render_aligned_cell(child, "th")
}
fmt.print("</tr>\n")
case token.TokenType.TableRow:
fmt.print("<tr>")
for child in tok.children {
print_html(child)
}
fmt.print("</tr>\n")
case token.TokenType.TableCell:
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:
render_div_wrapper(tok, "left")
case token.TokenType.Right:
render_div_wrapper(tok, "right")
case token.TokenType.Middle:
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_html(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_html(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_html(child)
}
fmt.print("</u>")
case token.TokenType.Superscript:
fmt.print("<sup>")
for child in tok.children {
print_html(child)
}
fmt.print("</sup>")
case token.TokenType.Subscript:
fmt.print("<sub>")
for child in tok.children {
print_html(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_html(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_html(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 {
render_media_object(tok, mime.detect_mime_type(url))
}
case token.TokenType.Video:
if url, ok := tok.value.(string); ok {
render_media_object(tok, mime.detect_mime_type(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_html(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_html(child)
}
}
}
+87
View File
@@ -0,0 +1,87 @@
package renderer
import "../token"
import "core:fmt"
import "core:strings"
import "core:unicode"
get_text_content :: proc(token: ^token.Token, b: ^strings.Builder) {
if token.type == .Text {
if v, ok := token.value.(string); ok {
strings.write_string(b, v)
}
}
for child in token.children {
get_text_content(child, b)
}
}
check_only_var_defs :: proc(token: ^token.Token) -> bool {
for child in token.children {
if child.type == .VariableDef || child.type == .BreakLine {
continue
}
if child.type == .Text {
if child.value != nil {
if v, ok := child.value.(string); ok && v != "" {
return false
}
}
has_non_var_def := false
for gc in child.children {
if gc.type != .VariableDef {
has_non_var_def = true
break
}
}
if has_non_var_def {
return false
}
continue
}
return false
}
return true
}
slugifyifyifyify :: proc(text: string) -> string {
b := strings.builder_make(context.temp_allocator)
for ch in text {
lower_ch := unicode.to_lower(ch)
if unicode.is_letter(lower_ch) || unicode.is_digit(lower_ch) {
strings.write_rune(&b, lower_ch)
} else if lower_ch == ' ' || lower_ch == '_' || lower_ch == '-' {
strings.write_rune(&b, '-')
}
}
res := strings.to_string(b)
for {
new_res, replaced := strings.replace_all(res, "--", "-", context.temp_allocator)
if !replaced {
break
}
res = new_res
}
res = strings.trim(res, "-")
return res
}
get_alignment_attribute :: proc(tok: ^token.Token) -> string {
if len(tok.children) > 0 {
first_child := tok.children[0]
if first_child.type == .Left {
return "left"
} else if first_child.type == .Right {
return "right"
} else if first_child.type == .Middle {
return "center"
}
}
return ""
}
+54
View File
@@ -0,0 +1,54 @@
package token
TokenType :: enum {
Text,
Italics,
Bold,
BreakLine,
Header,
Paragraph,
Root,
Blockquote,
UnorderedList,
OrderedList,
ListItem,
Strikethrough,
Table,
TableRow,
TableHeaderRow,
TableCell,
TableSeparator,
Left,
Right,
Middle,
FootnoteDef,
FootnoteRef,
CodeBlock,
InlineCode,
HorizontalRule,
Underline,
Superscript,
Subscript,
HeaderLink,
Link,
Image,
Audio,
Video,
Iframe,
Script,
ScriptBlock,
Variable,
VariableDef,
}
TokenValue :: union {
string,
int,
}
Token :: struct {
type: TokenType,
value: TokenValue,
children: [dynamic]^Token,
parent: ^Token,
}