131 lines
3.4 KiB
Go
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
|
|
}
|