parser updates, HTML support
This commit is contained in:
parent
d24c5846c4
commit
bd6fa12191
107
parser.go
107
parser.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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] {
|
||||
|
|
63
render.go
63
render.go
|
@ -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
|
||||
}
|
|
@ -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 & 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=> 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)
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue