Kaku/kaku/inline.go
2026-08-28 01:47:25 +02:00

131 lines
3.4 KiB
Go

package kaku
import "strings"
// We use rune for non-latin characters instead of bytes
type delimiter struct {
rune rune
wrap func(children []Node) Node
}
var delimiters = []delimiter{
{'*', func(children []Node) Node { return &Bold{Children: children} }},
{'_', func(children []Node) Node { return &Em{Children: children} }},
{'~', func(children []Node) Node { return &Strike{Children: children} }},
}
// Scans text once, left to right, turning it into a list of Text/Bold/Em/Strike/Code/Link/Image nodes.
func parseInline(text string) []Node {
runes := []rune(text)
var nodes []Node // list of nodes to return
var plain strings.Builder // current text that will be added to node
// Packages up whatever plain text has been accumulated so far into a Text node and adds it to nodes, then clears the buffer so the next run of plain characters starts fresh.
flush := func() {
if plain.Len() > 0 {
nodes = append(nodes, &Text{Value: plain.String()})
plain.Reset()
}
}
for i := 0; i < len(runes); {
switch runes[i] {
case '`':
if end := findNextRune(runes, i+1, '`'); end != -1 {
flush()
nodes = append(nodes, &Code{Value: string(runes[i+1 : end])})
i = end + 1
continue
}
case '(':
if end := findMatchingParenthesis(runes, i); end != -1 {
inner := string(runes[i+1 : end])
if node, ok := buildInlineTag(inner); ok {
flush()
nodes = append(nodes, node)
i = end + 1
continue
}
}
default:
// Checks if the rune is part of delimeters, if yes look ahead for matching tag and if it exists creates a node
if d, ok := checkDelimeter(runes[i]); ok {
if end := findNextRune(runes, i+1, d.rune); end != -1 {
flush()
nodes = append(nodes, d.wrap(parseInline(string(runes[i+1:end]))))
i = end + 1
continue
}
}
}
// Else write text
plain.WriteRune(runes[i])
i++
}
flush()
return nodes
}
// Checks if a rune is one of the known wrapping symbols (*, _, ~) and returns which node type it builds.
func checkDelimeter(r rune) (delimiter, bool) {
for _, delimeter := range delimiters {
if delimeter.rune == r {
return delimeter, true
}
}
return delimiter{}, false
}
// Find the position of the next occurrence of a given rune, or -1 if none.
func findNextRune(runes []rune, from int, delim rune) int {
for i := from; i < len(runes); i++ {
if runes[i] == delim {
return i
}
}
return -1
}
// Finds the ) that correctly closes a given (, even if there are nested parentheses in between. Counts the depth of ()
func findMatchingParenthesis(runes []rune, openAt int) int {
depth := 1
for i := openAt + 1; i < len(runes); i++ {
switch runes[i] {
case '(':
depth++
case ')':
depth--
if depth == 0 {
return i
}
}
}
return -1
}
// Reads a (link: ...) or (image: ...) starting at a (, and builds the corresponding node.
func buildInlineTag(inner string) (Node, bool) {
switch {
case strings.HasPrefix(inner, "link:"):
f := parseKeyedTag(inner, []string{"link", "text", "label", "title"})
return &Link{
URL: f["link"],
Text: parseInline(f["text"]),
Label: f["label"],
Title: f["title"],
}, true
case strings.HasPrefix(inner, "image:"):
f := parseKeyedTag(inner, []string{"image", "alt", "figcaption"})
return &Image{
URL: f["image"],
Alt: f["alt"],
Caption: parseInline(f["figcaption"]),
}, true
}
return nil, false
}