This commit is contained in:
2026-07-14 22:24:48 +02:00
parent f3650e59b1
commit 10afe9f22a
4 changed files with 351 additions and 14 deletions
+29 -1
View File
@@ -5,7 +5,35 @@ style="<style>$(cat styles.css)</style>"
doctype='<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
meta='<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
start="$doctype<html><head><title>TypoML Preview</title>$meta$style</head><body>"
end="</body></html>"
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>'
end="$script</body></html>"
output="$start$input$end"
echo "$output" > /tmp/preview.html
nohup xdg-open /tmp/preview.html > /dev/null 2>&1 &
+65
View File
@@ -14,6 +14,34 @@ get_text_content :: proc(token: ^Token, b: ^strings.Builder) {
}
}
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 {
lower := strings.to_lower(text, context.temp_allocator)
res, _ := strings.replace_all(lower, " ", "-", context.temp_allocator)
@@ -51,11 +79,20 @@ print_html :: proc(token: ^Token) {
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")
@@ -316,6 +353,34 @@ print_html :: proc(token: ^Token) {
fmt.printf("<script src=\"%s\"></script>", 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\">", 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>",
name,
val,
)
}
case TokenType.BreakLine:
fmt.print("<br>\n")
+6
View File
@@ -85,6 +85,12 @@ print_token :: proc(token: ^Token, depth: int = 0) {
type_str = "Iframe"
case TokenType.Script:
type_str = "Script"
case TokenType.ScriptBlock:
type_str = "ScriptBlock"
case TokenType.Variable:
type_str = "Variable"
case TokenType.VariableDef:
type_str = "VariableDef"
}
value_str := ""
+247 -9
View File
@@ -1,5 +1,6 @@
package main
import "core:fmt"
import "core:strconv"
import "core:strings"
@@ -39,6 +40,9 @@ TokenType :: enum {
Video,
Iframe,
Script,
ScriptBlock,
Variable,
VariableDef,
}
TokenValue :: union {
@@ -362,15 +366,39 @@ match_pair :: proc(s: string, open1, close1, open2, close2: byte) -> (int, strin
if len(s) == 0 || s[0] != open1 {
return 0, "", "", false
}
close1_idx := strings.index_byte(s, close1)
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 {
close2_idx := strings.index_byte(s[close1_idx + 2:], close2)
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 {
close2_idx += close1_idx + 2
return close2_idx + 1, s[1:close1_idx], s[close1_idx + 2:close2_idx], true
}
}
@@ -481,6 +509,42 @@ parse_links :: proc(token: ^Token) {
}
}
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,
) -> (
@@ -593,6 +657,23 @@ parse_embeds :: proc(token: ^Token) {
return
}
s_start, s_end, is_script, s_type, s_content := find_script_block(str)
if is_script {
token.value = nil
if s_start > 0 {
push_token(token, .Text, str[:s_start])
}
script_token := push_token(token, .ScriptBlock, s_content)
if s_end < len(str) {
rem := push_token(token, .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
@@ -627,6 +708,126 @@ parse_embeds :: proc(token: ^Token) {
}
}
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 is_url(inner) {
continue
} else {
return i, i + 1 + end, true, strings.trim_space(inner), "", false
}
}
}
}
return -1, -1, false, "", "", false
}
parse_variables :: proc(token: ^Token) {
if token.type == .CodeBlock || token.type == .InlineCode {
return
}
if token.children != nil {
for i := 0; i < len(token.children); i += 1 {
parse_variables(token.children[i])
}
}
if token.type == .Text {
str, ok := token.value.(string)
if !ok {
return
}
start_idx, end_idx, is_var, name, val, is_def := find_variable(str)
if !is_var {
return
}
token.value = nil
if start_idx > 0 {
push_token(token, .Text, str[:start_idx])
}
if is_def {
def_token := push_token(token, .VariableDef, name)
push_token(def_token, .Text, val)
} else {
push_token(token, .Variable, name)
}
if end_idx < len(str) {
rem := push_token(token, .Text, str[end_idx:])
parse_variables(rem)
}
}
}
resolve_variables :: proc(token: ^Token, vars: ^map[string]string) {
if token.type == .VariableDef {
if name, ok := token.value.(string); ok {
if len(token.children) > 0 {
if val, vok := token.children[0].value.(string); vok {
vars[name] = val
}
}
}
return
}
if token.type == .Variable {
if name, ok := token.value.(string); ok {
if val, found := vars[name]; found {
if token.children == nil {
push_token(token, .Text, val)
}
}
}
return
}
if token.type == .Link || token.type == .HeaderLink {
if url, ok := token.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:])
token.value = strings.to_string(b)
break
}
}
}
}
}
}
if token.children != nil {
for child in token.children {
resolve_variables(child, vars)
}
}
}
add_breaklines :: proc(token: ^Token) {
if token.type == .CodeBlock {
return
@@ -668,6 +869,9 @@ parse :: proc(lines: []string, allocator := context.allocator) -> Token {
// FIRST PASS
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)
@@ -682,6 +886,21 @@ parse :: proc(lines: []string, allocator := context.allocator) -> Token {
continue
}
if in_script_block {
if strings.has_prefix(line, "$[/") {
full_content := strings.join(script_block_content[:], "\n")
script_token := push_token(&root, TokenType.ScriptBlock, full_content)
push_token(script_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
}
if strings.has_prefix(line, "```") {
lang := strings.trim_space(line[3:])
current_token = push_token(&root, TokenType.CodeBlock, lang)
@@ -689,6 +908,17 @@ parse :: proc(lines: []string, allocator := context.allocator) -> Token {
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]
@@ -811,17 +1041,25 @@ parse :: proc(lines: []string, allocator := context.allocator) -> Token {
// SECOND PASS
for token in root.children {
add_breaklines(token)
parse_inline_code(token)
parse_links(token)
parse_variables(token)
parse_embeds(token)
parse_inline_footnotes(token)
pass_through_children(token)
}
// THIRD PASS
for token in root.children {
parse_inline_code(token)
parse_embeds(token)
parse_inline_footnotes(token)
parse_links(token)
pass_through_children(token)
add_breaklines(token)
}
// VARIABLE RESOLUTION PASS
vars: map[string]string
for child in root.children {
resolve_variables(child, &vars)
}
// FOURTH PASS