mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
feat: Added footnotes and syntax highlighting.
This commit is contained in:
+3
-1
@@ -173,7 +173,9 @@ load_page :: proc(
|
|||||||
if strings.has_suffix(file_path, ".html") {
|
if strings.has_suffix(file_path, ".html") {
|
||||||
page.body_html = strings.clone(body)
|
page.body_html = strings.clone(body)
|
||||||
} else {
|
} else {
|
||||||
page.body_html = cm.markdown_to_html_from_string(body, {.Unsafe})
|
clean_body, defs := strip_definitions(body)
|
||||||
|
html := cm.markdown_to_html_from_string(clean_body, {.Unsafe})
|
||||||
|
page.body_html = inject_sidenotes(html, defs)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch page_type {
|
switch page_type {
|
||||||
|
|||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import cm "vendor:commonmark"
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
// strip_definitions scans markdown text for footnote definitions ([^id]: text),
|
||||||
|
// removes them, and returns the cleaned text plus a map of id→definition.
|
||||||
|
// Handles multi-line definitions with indented continuation lines.
|
||||||
|
strip_definitions :: proc(body: string) -> (clean_body: string, defs: map[string]string) {
|
||||||
|
defs = make(map[string]string)
|
||||||
|
|
||||||
|
lines := strings.split(body, "\n")
|
||||||
|
output_lines: [dynamic]string
|
||||||
|
defer delete(output_lines)
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for i < len(lines) {
|
||||||
|
line := lines[i]
|
||||||
|
|
||||||
|
id, def_text, is_def := parse_def_line(line)
|
||||||
|
if !is_def {
|
||||||
|
append(&output_lines, line)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect definition text (initial line + multi-line continuations)
|
||||||
|
def_parts: [dynamic]string
|
||||||
|
if def_text != "" {
|
||||||
|
append(&def_parts, def_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
for i < len(lines) {
|
||||||
|
next := lines[i]
|
||||||
|
// Stop at blank lines
|
||||||
|
if len(next) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Stop at new footnote definitions
|
||||||
|
_, _, is_new_def := parse_def_line(next)
|
||||||
|
if is_new_def {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Include as continuation (trim indented lines)
|
||||||
|
if is_indented(next) {
|
||||||
|
append(&def_parts, strings.trim_left(next, " \t"))
|
||||||
|
} else {
|
||||||
|
append(&def_parts, next)
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
defs[id] = strings.join(def_parts[:], "\n")
|
||||||
|
delete(def_parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_body = strings.join(output_lines[:], "\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse_def_line checks if a line is a footnote definition: [^id]: text
|
||||||
|
parse_def_line :: proc(line: string) -> (id: string, text: string, ok: bool) {
|
||||||
|
if len(line) < 5 || line[0] != '[' || line[1] != '^' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
close := strings.index(line[2:], "]")
|
||||||
|
if close < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
close += 2
|
||||||
|
|
||||||
|
if close + 1 >= len(line) || line[close + 1] != ':' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id = line[2:close]
|
||||||
|
text = strings.trim_left(line[close + 2:], " \t")
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
is_indented :: proc(line: string) -> bool {
|
||||||
|
if len(line) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return line[0] == ' ' || line[0] == '\t'
|
||||||
|
}
|
||||||
|
|
||||||
|
// inject_sidenotes finds [^id] references in rendered HTML and replaces them
|
||||||
|
// with sidenote markup. Each definition is rendered through cmark separately.
|
||||||
|
inject_sidenotes :: proc(html: string, defs: map[string]string) -> string {
|
||||||
|
if len(defs) == 0 {
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
parts: [dynamic]string
|
||||||
|
defer delete(parts)
|
||||||
|
|
||||||
|
remaining := html
|
||||||
|
|
||||||
|
for {
|
||||||
|
pos := strings.index(remaining, "[^")
|
||||||
|
if pos < 0 {
|
||||||
|
append(&parts, remaining)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append text before [^
|
||||||
|
append(&parts, remaining[:pos])
|
||||||
|
|
||||||
|
close := strings.index(remaining[pos + 2:], "]")
|
||||||
|
if close < 0 {
|
||||||
|
append(&parts, remaining[pos:])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
id := remaining[pos + 2 : pos + 2 + close]
|
||||||
|
ref_end := pos + 2 + close + 1
|
||||||
|
|
||||||
|
def_text, found := defs[id]
|
||||||
|
if !found {
|
||||||
|
// No definition found, leave as literal text
|
||||||
|
append(&parts, remaining[pos:ref_end])
|
||||||
|
remaining = remaining[ref_end:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render definition through cmark for markdown support
|
||||||
|
def_html := cm.markdown_to_html_from_string(def_text, {.Unsafe})
|
||||||
|
def_html = strip_p_tags(def_html)
|
||||||
|
|
||||||
|
sidenote := fmt.aprintf(
|
||||||
|
`<label for="fn-%s" class="margin-toggle sidenote-number"></label><input type="checkbox" id="fn-%s" class="margin-toggle"><span class="sidenote">%s</span>`,
|
||||||
|
id,
|
||||||
|
id,
|
||||||
|
def_html,
|
||||||
|
)
|
||||||
|
append(&parts, sidenote)
|
||||||
|
|
||||||
|
remaining = remaining[ref_end:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.join(parts[:], "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip_p_tags removes surrounding <p></p> if the HTML is a single paragraph.
|
||||||
|
strip_p_tags :: proc(html: string) -> string {
|
||||||
|
s := html
|
||||||
|
if len(s) > 0 && s[len(s) - 1] == '\n' {
|
||||||
|
s = s[:len(s) - 1]
|
||||||
|
}
|
||||||
|
if strings.has_prefix(s, "<p>") && strings.has_suffix(s, "</p>") {
|
||||||
|
return s[3:len(s) - 4]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -148,6 +148,7 @@ render_chrome :: proc(page_title: string, body: string) -> string {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>%s</title>
|
<title>%s</title>
|
||||||
<link rel="stylesheet" href="/css/main.css">
|
<link rel="stylesheet" href="/css/main.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
||||||
<script src="/js/main.js" defer></script>
|
<script src="/js/main.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -157,6 +158,8 @@ render_chrome :: proc(page_title: string, body: string) -> string {
|
|||||||
<p class="pt-5 prose">Proudly built with <a href="https://odin-lang.org/">Odin</a></p>
|
<p class="pt-5 prose">Proudly built with <a href="https://odin-lang.org/">Odin</a></p>
|
||||||
<p><small>©</small> 2026</p>
|
<p><small>©</small> 2026</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
<script>hljs.highlightAll();</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`,
|
`,
|
||||||
|
|||||||
Reference in New Issue
Block a user