From 3d911fc51bdf207352d3da4fe981328f46e6d9ea Mon Sep 17 00:00:00 2001 From: handlerug Date: Wed, 20 Jan 2021 22:05:09 +0700 Subject: [PATCH] feat(markup): nested lists --- markup/lexer.go | 69 +++++-------------- markup/list.go | 171 +++++++++++++++++++++++++++++++++++++++++++++++ markup/parser.go | 2 + 3 files changed, 190 insertions(+), 52 deletions(-) create mode 100644 markup/list.go 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
  • %s
  • \n", ParagraphToHtml(state.name, line[2:])) - 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
    -
    -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("
      1. ") + 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: