diff --git a/markup/lexer.go b/markup/lexer.go index e2efcfa..4a73007 100644 --- a/markup/lexer.go +++ b/markup/lexer.go @@ -31,6 +31,7 @@ type GemLexerState struct { // Temporaries img *Img table *Table + list *List } type Line struct { @@ -64,12 +65,6 @@ func lineToAST(line string, state *GemLexerState, ast *[]Line) { // Process empty lines depending on the current state if "" == strings.TrimSpace(line) { switch state.where { - case "list": - state.where = "" - addLine(state.buf + "") - case "number": - state.where = "" - addLine(state.buf + "") case "pre": state.buf += "\n" case "launchpad": @@ -95,12 +90,10 @@ func lineToAST(line string, state *GemLexerState, ast *[]Line) { goto imgState case "table": goto tableState - case "pre": - goto preformattedState case "list": goto listState - case "number": - goto numberState + case "pre": + goto preformattedState case "launchpad": goto launchpadState default: // "p" or "" @@ -121,6 +114,14 @@ tableState: } return +listState: + if done := state.list.Parse(line); done { + state.list.Finalize() + state.where = "" + goto normalState + } + return + preformattedState: switch { case startsWith("```"): @@ -133,38 +134,6 @@ preformattedState: } return -listState: - switch { - case startsWith("* "): - state.buf += fmt.Sprintf("\t
", state.id, strings.TrimPrefix(line, "```"))
- default:
- state.where = ""
- addLine(state.buf + "")
- goto normalState
- }
- return
-
-numberState:
- switch {
- case startsWith("*. "):
- state.buf += fmt.Sprintf("\t%s \n", ParagraphToHtml(state.name, line[3:]))
- case startsWith("```"):
- state.where = "pre"
- addLine(state.buf + "")
- state.id++
- state.buf = fmt.Sprintf("", state.id, strings.TrimPrefix(line, "```"))
- default:
- state.where = ""
- addLine(state.buf + "")
- goto normalState
- }
- return
-
launchpadState:
switch {
case startsWith("=>"):
@@ -190,16 +159,6 @@ normalState:
addParagraphIfNeeded()
state.where = "pre"
state.buf = fmt.Sprintf("", state.id, strings.TrimPrefix(line, "```"))
- case startsWith("* "):
- addParagraphIfNeeded()
- state.where = "list"
- state.buf = fmt.Sprintf("\n", state.id)
- goto listState
- case startsWith("*. "):
- addParagraphIfNeeded()
- state.where = "number"
- state.buf = fmt.Sprintf("\n", state.id)
- goto numberState
case startsWith("###### "):
addParagraphIfNeeded()
@@ -241,6 +200,12 @@ normalState:
case MatchesHorizontalLine(line):
addParagraphIfNeeded()
*ast = append(*ast, Line{id: -1, contents: "
"})
+ case MatchesList(line):
+ addParagraphIfNeeded()
+ list, _ := NewList(line, state.name)
+ state.where = "list"
+ state.list = list
+ addLine(state.list)
case MatchesImg(line):
addParagraphIfNeeded()
img, shouldGoBackToNormal := ImgFromFirstLine(line, state.name)
diff --git a/markup/list.go b/markup/list.go
new file mode 100644
index 0000000..81b1d1c
--- /dev/null
+++ b/markup/list.go
@@ -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("- ")
+ b.WriteString(ParagraphToHtml(hyphaName, item.content))
+ }
+
+ if len(item.children) > 0 {
+ if ordered {
+ b.WriteString("
")
+ } else {
+ b.WriteString("")
+ }
+
+ for _, child := range item.children {
+ child.renderAsHtmlTo(b, hyphaName, ordered)
+ }
+
+ if ordered {
+ b.WriteString("
")
+ } else {
+ b.WriteString("
")
+ }
+ }
+
+ if len(item.content) > 0 {
+ b.WriteString("")
+ }
+}
+
+// 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()
+}
diff --git a/markup/parser.go b/markup/parser.go
index 665f8ba..db715f1 100644
--- a/markup/parser.go
+++ b/markup/parser.go
@@ -15,6 +15,8 @@ func Parse(ast []Line, from, to int, recursionLevel int) (html string) {
html += v.ToHtml()
case Table:
html += v.asHtml()
+ case *List:
+ html += v.RenderAsHtml()
case string:
html += v
default: