mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2024-12-13 05:50:27 +00:00
feat(markup): nested lists
This commit is contained in:
parent
53981c28ca
commit
3d911fc51b
@ -31,6 +31,7 @@ type GemLexerState struct {
|
|||||||
// Temporaries
|
// Temporaries
|
||||||
img *Img
|
img *Img
|
||||||
table *Table
|
table *Table
|
||||||
|
list *List
|
||||||
}
|
}
|
||||||
|
|
||||||
type Line struct {
|
type Line struct {
|
||||||
@ -64,12 +65,6 @@ func lineToAST(line string, state *GemLexerState, ast *[]Line) {
|
|||||||
// Process empty lines depending on the current state
|
// Process empty lines depending on the current state
|
||||||
if "" == strings.TrimSpace(line) {
|
if "" == strings.TrimSpace(line) {
|
||||||
switch state.where {
|
switch state.where {
|
||||||
case "list":
|
|
||||||
state.where = ""
|
|
||||||
addLine(state.buf + "</ul>")
|
|
||||||
case "number":
|
|
||||||
state.where = ""
|
|
||||||
addLine(state.buf + "</ol>")
|
|
||||||
case "pre":
|
case "pre":
|
||||||
state.buf += "\n"
|
state.buf += "\n"
|
||||||
case "launchpad":
|
case "launchpad":
|
||||||
@ -95,12 +90,10 @@ func lineToAST(line string, state *GemLexerState, ast *[]Line) {
|
|||||||
goto imgState
|
goto imgState
|
||||||
case "table":
|
case "table":
|
||||||
goto tableState
|
goto tableState
|
||||||
case "pre":
|
|
||||||
goto preformattedState
|
|
||||||
case "list":
|
case "list":
|
||||||
goto listState
|
goto listState
|
||||||
case "number":
|
case "pre":
|
||||||
goto numberState
|
goto preformattedState
|
||||||
case "launchpad":
|
case "launchpad":
|
||||||
goto launchpadState
|
goto launchpadState
|
||||||
default: // "p" or ""
|
default: // "p" or ""
|
||||||
@ -121,6 +114,14 @@ tableState:
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
||||||
|
listState:
|
||||||
|
if done := state.list.Parse(line); done {
|
||||||
|
state.list.Finalize()
|
||||||
|
state.where = ""
|
||||||
|
goto normalState
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
preformattedState:
|
preformattedState:
|
||||||
switch {
|
switch {
|
||||||
case startsWith("```"):
|
case startsWith("```"):
|
||||||
@ -133,38 +134,6 @@ preformattedState:
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
||||||
listState:
|
|
||||||
switch {
|
|
||||||
case startsWith("* "):
|
|
||||||
state.buf += fmt.Sprintf("\t<li>%s</li>\n", ParagraphToHtml(state.name, line[2:]))
|
|
||||||
case startsWith("```"):
|
|
||||||
state.where = "pre"
|
|
||||||
addLine(state.buf + "</ul>")
|
|
||||||
state.id++
|
|
||||||
state.buf = fmt.Sprintf("<pre id='%d' alt='%s' class='codeblock'><code>", state.id, strings.TrimPrefix(line, "```"))
|
|
||||||
default:
|
|
||||||
state.where = ""
|
|
||||||
addLine(state.buf + "</ul>")
|
|
||||||
goto normalState
|
|
||||||
}
|
|
||||||
return
|
|
||||||
|
|
||||||
numberState:
|
|
||||||
switch {
|
|
||||||
case startsWith("*. "):
|
|
||||||
state.buf += fmt.Sprintf("\t<li>%s</li>\n", ParagraphToHtml(state.name, line[3:]))
|
|
||||||
case startsWith("```"):
|
|
||||||
state.where = "pre"
|
|
||||||
addLine(state.buf + "</ol>")
|
|
||||||
state.id++
|
|
||||||
state.buf = fmt.Sprintf("<pre id='%d' alt='%s' class='codeblock'><code>", state.id, strings.TrimPrefix(line, "```"))
|
|
||||||
default:
|
|
||||||
state.where = ""
|
|
||||||
addLine(state.buf + "</ol>")
|
|
||||||
goto normalState
|
|
||||||
}
|
|
||||||
return
|
|
||||||
|
|
||||||
launchpadState:
|
launchpadState:
|
||||||
switch {
|
switch {
|
||||||
case startsWith("=>"):
|
case startsWith("=>"):
|
||||||
@ -190,16 +159,6 @@ normalState:
|
|||||||
addParagraphIfNeeded()
|
addParagraphIfNeeded()
|
||||||
state.where = "pre"
|
state.where = "pre"
|
||||||
state.buf = fmt.Sprintf("<pre id='%d' alt='%s' class='codeblock'><code>", state.id, strings.TrimPrefix(line, "```"))
|
state.buf = fmt.Sprintf("<pre id='%d' alt='%s' class='codeblock'><code>", state.id, strings.TrimPrefix(line, "```"))
|
||||||
case startsWith("* "):
|
|
||||||
addParagraphIfNeeded()
|
|
||||||
state.where = "list"
|
|
||||||
state.buf = fmt.Sprintf("<ul id='%d'>\n", state.id)
|
|
||||||
goto listState
|
|
||||||
case startsWith("*. "):
|
|
||||||
addParagraphIfNeeded()
|
|
||||||
state.where = "number"
|
|
||||||
state.buf = fmt.Sprintf("<ol id='%d'>\n", state.id)
|
|
||||||
goto numberState
|
|
||||||
|
|
||||||
case startsWith("###### "):
|
case startsWith("###### "):
|
||||||
addParagraphIfNeeded()
|
addParagraphIfNeeded()
|
||||||
@ -241,6 +200,12 @@ normalState:
|
|||||||
case MatchesHorizontalLine(line):
|
case MatchesHorizontalLine(line):
|
||||||
addParagraphIfNeeded()
|
addParagraphIfNeeded()
|
||||||
*ast = append(*ast, Line{id: -1, contents: "<hr/>"})
|
*ast = append(*ast, Line{id: -1, contents: "<hr/>"})
|
||||||
|
case MatchesList(line):
|
||||||
|
addParagraphIfNeeded()
|
||||||
|
list, _ := NewList(line, state.name)
|
||||||
|
state.where = "list"
|
||||||
|
state.list = list
|
||||||
|
addLine(state.list)
|
||||||
case MatchesImg(line):
|
case MatchesImg(line):
|
||||||
addParagraphIfNeeded()
|
addParagraphIfNeeded()
|
||||||
img, shouldGoBackToNormal := ImgFromFirstLine(line, state.name)
|
img, shouldGoBackToNormal := ImgFromFirstLine(line, state.name)
|
||||||
|
171
markup/list.go
Normal file
171
markup/list.go
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
package markup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseListItem(line string) (level int, offset int, ordered bool, err error) {
|
||||||
|
for line[level] == '*' {
|
||||||
|
level++
|
||||||
|
}
|
||||||
|
|
||||||
|
if line[level] == '.' {
|
||||||
|
ordered = true
|
||||||
|
offset = level + 2
|
||||||
|
} else {
|
||||||
|
ordered = false
|
||||||
|
offset = level + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if line[offset-1] != ' ' || len(line) < offset+2 || level < 1 || level > 6 {
|
||||||
|
err = errors.New("ill-formatted list item")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchesList(line string) bool {
|
||||||
|
level, _, _, err := parseListItem(line)
|
||||||
|
return err == nil && level == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
type listItem struct {
|
||||||
|
content string
|
||||||
|
parent *listItem
|
||||||
|
children []*listItem
|
||||||
|
depth int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListItem(parent *listItem) *listItem {
|
||||||
|
depth := 0
|
||||||
|
if parent != nil {
|
||||||
|
depth = parent.depth + 1
|
||||||
|
}
|
||||||
|
return &listItem{
|
||||||
|
parent: parent,
|
||||||
|
children: make([]*listItem, 0),
|
||||||
|
depth: depth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item *listItem) renderAsHtmlTo(b *strings.Builder, hyphaName string, ordered bool) {
|
||||||
|
if len(item.content) > 0 {
|
||||||
|
b.WriteString("<li>")
|
||||||
|
b.WriteString(ParagraphToHtml(hyphaName, item.content))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(item.children) > 0 {
|
||||||
|
if ordered {
|
||||||
|
b.WriteString("<ol>")
|
||||||
|
} else {
|
||||||
|
b.WriteString("<ul>")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range item.children {
|
||||||
|
child.renderAsHtmlTo(b, hyphaName, ordered)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ordered {
|
||||||
|
b.WriteString("</ol>")
|
||||||
|
} else {
|
||||||
|
b.WriteString("</ul>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(item.content) > 0 {
|
||||||
|
b.WriteString("</li>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A structure representing ordered and unordered lists in the AST.
|
||||||
|
type List struct {
|
||||||
|
curr *listItem
|
||||||
|
hyphaName string
|
||||||
|
ordered bool
|
||||||
|
finalized bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewList(line, hyphaName string) (*List, bool) {
|
||||||
|
list := &List{
|
||||||
|
hyphaName: hyphaName,
|
||||||
|
curr: newListItem(nil),
|
||||||
|
}
|
||||||
|
return list, list.Parse(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *List) pushItem() {
|
||||||
|
item := newListItem(list.curr)
|
||||||
|
list.curr.children = append(list.curr.children, item)
|
||||||
|
list.curr = item
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *List) popItem() {
|
||||||
|
if list.curr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list.curr = list.curr.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *List) balance(level int) {
|
||||||
|
for level > list.curr.depth {
|
||||||
|
list.pushItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
for level < list.curr.depth {
|
||||||
|
list.popItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *List) Parse(line string) (done bool) {
|
||||||
|
level, offset, ordered, err := parseListItem(line)
|
||||||
|
if err != nil {
|
||||||
|
list.Finalize()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// update ordered flag if the current node is the root one
|
||||||
|
// (i.e. no parsing has been done yet)
|
||||||
|
if list.curr.parent == nil {
|
||||||
|
list.ordered = ordered
|
||||||
|
}
|
||||||
|
|
||||||
|
// if list type has suddenly changed (ill-formatted list), quit
|
||||||
|
if ordered != list.ordered {
|
||||||
|
list.Finalize()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
list.balance(level)
|
||||||
|
|
||||||
|
// if the current node already has content, create a new one
|
||||||
|
// to prevent overwriting existing content (effectively creating
|
||||||
|
// a new sibling node)
|
||||||
|
if len(list.curr.content) > 0 {
|
||||||
|
list.popItem()
|
||||||
|
list.pushItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
list.curr.content = line[offset:]
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *List) Finalize() {
|
||||||
|
if !list.finalized {
|
||||||
|
// close all opened nodes, effectively going up to the root node
|
||||||
|
list.balance(0)
|
||||||
|
list.finalized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *List) RenderAsHtml() (html string) {
|
||||||
|
// for a good measure
|
||||||
|
list.Finalize()
|
||||||
|
|
||||||
|
b := &strings.Builder{}
|
||||||
|
|
||||||
|
// fire up recursive render process
|
||||||
|
list.curr.renderAsHtmlTo(b, list.hyphaName, list.ordered)
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
@ -15,6 +15,8 @@ func Parse(ast []Line, from, to int, recursionLevel int) (html string) {
|
|||||||
html += v.ToHtml()
|
html += v.ToHtml()
|
||||||
case Table:
|
case Table:
|
||||||
html += v.asHtml()
|
html += v.asHtml()
|
||||||
|
case *List:
|
||||||
|
html += v.RenderAsHtml()
|
||||||
case string:
|
case string:
|
||||||
html += v
|
html += v
|
||||||
default:
|
default:
|
||||||
|
Loading…
Reference in New Issue
Block a user