parser updates, HTML support

This commit is contained in:
Nico 2020-11-14 19:29:55 +00:00
parent d24c5846c4
commit bd6fa12191
4 changed files with 148 additions and 70 deletions

107
parser.go
View File

@ -2,8 +2,8 @@
package gemtext
import (
"strings"
"fmt"
"fmt"
"strings"
)
type ObjectType int64
@ -20,11 +20,11 @@ const (
// type GemtextObject represents a gemtext object.
type GemtextObject struct {
Type ObjectType
Text string // Contains the text of the element. For a link this is the label.
Type ObjectType
Text string // Contains the text of the element. For a link this is the label.
Literal string // Contains the line as it exists in the file
Path string // Populated if object is a link
Level int // Populated if object is a heading
Path string // Populated if object is a link
Level int // Populated if object is a heading
}
// type GemtextPage represents a gemtext page. It is equivalent to a slice of GemtextObjects.
@ -35,10 +35,10 @@ func ParseLink(l string) (GemtextObject, error) {
if !strings.HasPrefix(l, "=>") {
return GemtextObject{}, fmt.Errorf("Not a gemtext link!")
} else {
f := strings.Fields(l[2:])
f := strings.Fields(l[2:])
link := GemtextObject{Type: LINK, Literal: l}
link.Path = f[0]
link.Text = strings.TrimSpace(l[3+len(f[0]):]) // text that remains after removing the URL and link marker
link.Text = strings.TrimSpace(l[3+len(f[0]):]) // text that remains after removing the URL and link marker
return link, nil
}
@ -46,59 +46,60 @@ func ParseLink(l string) (GemtextObject, error) {
// ParseHeading parses a gemtext heading, returns a HEADING GemtextObject if it is between levels 1-3 and TEXT otherwise.
func ParseHeading(l string) (GemtextObject, error) {
switch {
case strings.HasPrefix(l, "####"): // Fake headings
return GemtextObject{Type: TEXT, Text:l, Literal: l}, nil
case strings.HasPrefix(l, "###"):
return GemtextObject{Type: HEADING, Level: 3, Text: strings.TrimSpace(l[3:]), Literal: l},nil
case strings.HasPrefix(l, "##"):
return GemtextObject{Type: HEADING, Level: 2, Text: strings.TrimSpace(l[2:]), Literal: l},nil
case strings.HasPrefix(l, "#"):
return GemtextObject{Type: HEADING, Level: 1, Text: strings.TrimSpace(l[1:]), Literal: l},nil
}
return GemtextObject{Type: TEXT, Text:l, Literal: l}, nil
switch {
case strings.HasPrefix(l, "####"): // Fake headings
return GemtextObject{Type: TEXT, Text: l, Literal: l}, nil
case strings.HasPrefix(l, "###"):
return GemtextObject{Type: HEADING, Level: 3, Text: strings.TrimSpace(l[3:]), Literal: l}, nil
case strings.HasPrefix(l, "##"):
return GemtextObject{Type: HEADING, Level: 2, Text: strings.TrimSpace(l[2:]), Literal: l}, nil
case strings.HasPrefix(l, "#"):
return GemtextObject{Type: HEADING, Level: 1, Text: strings.TrimSpace(l[1:]), Literal: l}, nil
}
return GemtextObject{Type: TEXT, Text: l, Literal: l}, nil
}
// ParseLine parses a gemtext line, and returns a GemtextObject representing it.
// "isPreformatted" takes a true if the line is in a preformatted block, else otherwise.
func ParseLine(line string, isPreformatted bool) (GemtextObject, error) {
if !isPreformatted {
switch {
case strings.HasPrefix(line, "=>"):
return ParseLink(line)
case strings.HasPrefix(line, "```"):
return GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: line, Text: line[3:]}, nil
case strings.HasPrefix(line, "#"):
return ParseHeading(line)
case strings.HasPrefix(line, ">"):
return GemtextObject{Type: QUOTE, Text: strings.TrimSpace(line[1:]), Literal: line}, nil
case strings.HasPrefix(line, "* "): // Bullets require a space in the spec
return GemtextObject{Type: LIST, Text: line[2:], Literal: line}, nil
}
return GemtextObject{Type: TEXT, Text: line, Literal: line}, nil
} else {
if strings.HasPrefix(line, "```") { // toggles are the only special line type in preformatted mode
return GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: line, Text: line[3:]}, nil
}
return GemtextObject{Type: PREFORMATTED_TEXT, Text: line, Literal: line}, nil
}
if !isPreformatted {
switch {
case strings.HasPrefix(line, "=>"):
return ParseLink(line)
case strings.HasPrefix(line, "```"):
return GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: line, Text: line[3:]}, nil
case strings.HasPrefix(line, "#"):
return ParseHeading(line)
case strings.HasPrefix(line, ">"):
return GemtextObject{Type: QUOTE, Text: strings.TrimSpace(line[1:]), Literal: line}, nil
case strings.HasPrefix(line, "* "): // Bullets require a space in the spec
return GemtextObject{Type: LIST, Text: line[2:], Literal: line}, nil
}
return GemtextObject{Type: TEXT, Text: line, Literal: line}, nil
} else {
if strings.HasPrefix(line, "```") { // toggles are the only special line type in preformatted mode
return GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: line, Text: line[3:]}, nil
}
return GemtextObject{Type: PREFORMATTED_TEXT, Text: line, Literal: line}, nil
}
}
// ParsePage takes a string containing the contents of a gemtext page and returns a GemtextPage.
func ParsePage(p string) (GemtextPage, error) {
preformatted := false
lines := strings.Split(p, "\n")
var page GemtextPage
for _, line := range lines {
l, err := ParseLine(line, preformatted)
page = append(page, l)
if err != nil {
return GemtextPage{}, err
}
if l.Type == PREFORMATTED_TOGGLE {
preformatted = !preformatted
}
}
return page, nil
preformatted := false
lines := strings.Split(p, "\n")
var page GemtextPage
for _, line := range lines {
l, err := ParseLine(line, preformatted)
page = append(page, l)
if err != nil {
return GemtextPage{}, err
}
if l.Type == PREFORMATTED_TOGGLE {
preformatted = !preformatted
}
}
return page, nil
}

View File

@ -10,6 +10,25 @@ type LineCase struct {
Want GemtextObject
}
var testdocument string = "# Document\nThis is paragraph text & It exists\n=> https://example.com example.com\n* List Item 1\n* List Item 2\n```preformatted stuff\nthis should be preformatted.\n=> https://example.com Not a link\n```\n* A single list item\n```\none line of pre\n```\n```\n```\n>Something\n>- someone"
var testdocumenttree GemtextPage = GemtextPage{
GemtextObject{Type: HEADING, Literal: "# Document", Level: 1, Text: "Document"}, GemtextObject{Type: TEXT, Literal: "This is paragraph text & It exists", Text: "This is paragraph text & It exists"},
GemtextObject{Type: LINK, Literal: "=> https://example.com example.com", Text: "example.com", Path: "https://example.com"},
GemtextObject{Type: LIST, Literal: "* List Item 1", Text: "List Item 1"},
GemtextObject{Type: LIST, Literal: "* List Item 2", Text: "List Item 2"},
GemtextObject{Type: PREFORMATTED_TOGGLE, Literal:"```preformatted stuff", Text:"preformatted stuff"},
GemtextObject{Type: PREFORMATTED_TEXT, Literal: "this should be preformatted.", Text:"this should be preformatted."},
GemtextObject{Type: PREFORMATTED_TEXT, Literal: "=> https://example.com Not a link", Text: "=> https://example.com Not a link"},
GemtextObject{Type: PREFORMATTED_TOGGLE, Literal:"```"},
GemtextObject{Type: LIST, Literal: "* A single list item", Text: "A single list item"},
GemtextObject{Type: PREFORMATTED_TOGGLE, Literal:"```"},
GemtextObject{Type: PREFORMATTED_TEXT, Literal:"one line of pre", Text:"one line of pre"},
GemtextObject{Type: PREFORMATTED_TOGGLE, Literal:"```"},
GemtextObject{Type: PREFORMATTED_TOGGLE, Literal:"```"},
GemtextObject{Type: PREFORMATTED_TOGGLE, Literal:"```"},
GemtextObject{Type: QUOTE, Text: "Something", Literal:">Something"},
GemtextObject{Type: QUOTE, Text: "- someone", Literal:">- someone"},
}
// These tests suck.
func TestParseLine(t *testing.T) {
cases := []LineCase{
@ -194,23 +213,9 @@ func TestParseLine(t *testing.T) {
// We test each line in ParseLine()
// so all we have to test for ParsePage() is blank lines and preformatting.
func TestParsePage(t *testing.T) {
input := "# Document\n\n* List Item 1\n* List Item 2\n```preformatted stuff\nthis should be preformatted.\n=> https://example.com Not a link\n```\n* A single list item\n```\none line of pre\n```\n```\n```" // TODO make this readable
want := GemtextPage{
GemtextObject{Type: HEADING, Literal: "# Document", Level: 1, Text: "Document"},
GemtextObject{Type: TEXT, Literal: "", Text: ""},
GemtextObject{Type: LIST, Literal: "* List Item 1", Text: "List Item 1"},
GemtextObject{Type: LIST, Literal: "* List Item 2", Text: "List Item 2"},
GemtextObject{Type: PREFORMATTED_TOGGLE, Literal:"```preformatted stuff", Text:"preformatted stuff"},
GemtextObject{Type: PREFORMATTED_TEXT, Literal: "this should be preformatted.", Text:"this should be preformatted."},
GemtextObject{Type: PREFORMATTED_TEXT, Literal: "=> https://example.com Not a link", Text: "=> https://example.com Not a link"},
GemtextObject{Type: PREFORMATTED_TOGGLE, Literal:"```"},
GemtextObject{Type: LIST, Literal: "* A single list item", Text: "A single list item"},
GemtextObject{Type: PREFORMATTED_TOGGLE, Literal:"```"},
GemtextObject{Type: PREFORMATTED_TEXT, Literal:"one line of pre", Text:"one line of pre"},
GemtextObject{Type: PREFORMATTED_TOGGLE, Literal:"```"},
GemtextObject{Type: PREFORMATTED_TOGGLE, Literal:"```"},
GemtextObject{Type: PREFORMATTED_TOGGLE, Literal:"```"},
}
input := testdocument
want := testdocumenttree
output, _ := ParsePage(input)
for i, _ := range output {
if output[i] != want[i] {

View File

@ -4,6 +4,7 @@ import (
"fmt"
"path/filepath"
"strings"
"html"
)
// addPrefix adds the prefix "pref" to a path if it is absolute
@ -67,4 +68,66 @@ func RenderGemtext(p GemtextPage) (string, error) {
}
}
return str, nil
}
// RenderHTML renders a GemtextPage p to a string containing html.
func RenderHTML(p GemtextPage) (string, error) {
escape := html.EscapeString // for brevity
listmode := false
quotemode := false
premode := false
var s string
for _, item := range p {
if listmode {
if item.Type != LIST {
s += "</ul>"
listmode = false
}
}
if quotemode {
if item.Type != QUOTE {
s += "</blockquote>"
quotemode = false
}}
switch {
case item.Type == TEXT:
s += fmt.Sprintf("<p>%s</p>", escape(item.Text))
case item.Type == LINK:
s += fmt.Sprintf("<a href=\"%s\">%s</a><br />", escape(item.Path), escape(item.Text))
case item.Type == PREFORMATTED_TOGGLE:
if premode {
s += "</pre>"
premode = false
} else {
s += "<pre>"
premode = true
}
case item.Type == PREFORMATTED_TEXT:
s += escape(item.Text) + "\n"
case item.Type == HEADING:
s += fmt.Sprintf("<h%d>%s</h%d>", item.Level, escape(item.Text), item.Level)
case item.Type == LIST:
if !listmode {
s += "<ul>"
listmode = true
}
s += fmt.Sprintf("<li>%s</li>", escape(item.Text))
case item.Type == QUOTE:
if !quotemode {
s += "<blockquote>"
quotemode = true
}
s += escape(item.Text) + "\n"
}
}
if listmode {
s += "</ul>"
}
if quotemode {
s += "</blockquote>"
}
if premode {
s += "</pre>"
}
return s, nil
}

View File

@ -45,4 +45,13 @@ func TestRenderHeading(t *testing.T) {
t.Errorf("Case %#v, Got %#v", c, got)
}
}
}
func TestRenderHTML(t *testing.T) {
out, _ := RenderHTML(testdocumenttree)
want := "<h1>Document</h1><p>This is paragraph text &amp; It exists</p><a href=\"https://example.com\">example.com</a><br /><ul><li>List Item 1</li><li>List Item 2</li></ul><pre>this should be preformatted.\n=&gt; https://example.com Not a link\n</pre><ul><li>A single list item</li></ul><pre>one line of pre\n</pre><pre></pre><blockquote>Something\n- someone\n</blockquote>"
if out != want {
t.Errorf("Got %#v want %#v", out, want)
}
}