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
|
package gemtext
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"fmt"
|
||||||
"fmt"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ObjectType int64
|
type ObjectType int64
|
||||||
|
@ -20,11 +20,11 @@ const (
|
||||||
|
|
||||||
// type GemtextObject represents a gemtext object.
|
// type GemtextObject represents a gemtext object.
|
||||||
type GemtextObject struct {
|
type GemtextObject struct {
|
||||||
Type ObjectType
|
Type ObjectType
|
||||||
Text string // Contains the text of the element. For a link this is the label.
|
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
|
Literal string // Contains the line as it exists in the file
|
||||||
Path string // Populated if object is a link
|
Path string // Populated if object is a link
|
||||||
Level int // Populated if object is a heading
|
Level int // Populated if object is a heading
|
||||||
}
|
}
|
||||||
|
|
||||||
// type GemtextPage represents a gemtext page. It is equivalent to a slice of GemtextObjects.
|
// 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, "=>") {
|
if !strings.HasPrefix(l, "=>") {
|
||||||
return GemtextObject{}, fmt.Errorf("Not a gemtext link!")
|
return GemtextObject{}, fmt.Errorf("Not a gemtext link!")
|
||||||
} else {
|
} else {
|
||||||
f := strings.Fields(l[2:])
|
f := strings.Fields(l[2:])
|
||||||
link := GemtextObject{Type: LINK, Literal: l}
|
link := GemtextObject{Type: LINK, Literal: l}
|
||||||
link.Path = f[0]
|
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
|
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.
|
// 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) {
|
func ParseHeading(l string) (GemtextObject, error) {
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(l, "####"): // Fake headings
|
case strings.HasPrefix(l, "####"): // Fake headings
|
||||||
return GemtextObject{Type: TEXT, Text:l, Literal: l}, nil
|
return GemtextObject{Type: TEXT, Text: l, Literal: l}, nil
|
||||||
case strings.HasPrefix(l, "###"):
|
case strings.HasPrefix(l, "###"):
|
||||||
return GemtextObject{Type: HEADING, Level: 3, Text: strings.TrimSpace(l[3:]), Literal: l},nil
|
return GemtextObject{Type: HEADING, Level: 3, Text: strings.TrimSpace(l[3:]), Literal: l}, nil
|
||||||
case strings.HasPrefix(l, "##"):
|
case strings.HasPrefix(l, "##"):
|
||||||
return GemtextObject{Type: HEADING, Level: 2, Text: strings.TrimSpace(l[2:]), Literal: l},nil
|
return GemtextObject{Type: HEADING, Level: 2, Text: strings.TrimSpace(l[2:]), Literal: l}, nil
|
||||||
case strings.HasPrefix(l, "#"):
|
case strings.HasPrefix(l, "#"):
|
||||||
return GemtextObject{Type: HEADING, Level: 1, Text: strings.TrimSpace(l[1:]), Literal: l},nil
|
return GemtextObject{Type: HEADING, Level: 1, Text: strings.TrimSpace(l[1:]), Literal: l}, nil
|
||||||
}
|
}
|
||||||
return GemtextObject{Type: TEXT, Text:l, Literal: l}, nil
|
return GemtextObject{Type: TEXT, Text: l, Literal: l}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseLine parses a gemtext line, and returns a GemtextObject representing it.
|
// 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.
|
// "isPreformatted" takes a true if the line is in a preformatted block, else otherwise.
|
||||||
func ParseLine(line string, isPreformatted bool) (GemtextObject, error) {
|
func ParseLine(line string, isPreformatted bool) (GemtextObject, error) {
|
||||||
if !isPreformatted {
|
if !isPreformatted {
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(line, "=>"):
|
case strings.HasPrefix(line, "=>"):
|
||||||
return ParseLink(line)
|
return ParseLink(line)
|
||||||
case strings.HasPrefix(line, "```"):
|
case strings.HasPrefix(line, "```"):
|
||||||
return GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: line, Text: line[3:]}, nil
|
return GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: line, Text: line[3:]}, nil
|
||||||
case strings.HasPrefix(line, "#"):
|
case strings.HasPrefix(line, "#"):
|
||||||
return ParseHeading(line)
|
return ParseHeading(line)
|
||||||
case strings.HasPrefix(line, ">"):
|
case strings.HasPrefix(line, ">"):
|
||||||
return GemtextObject{Type: QUOTE, Text: strings.TrimSpace(line[1:]), Literal: line}, nil
|
return GemtextObject{Type: QUOTE, Text: strings.TrimSpace(line[1:]), Literal: line}, nil
|
||||||
case strings.HasPrefix(line, "* "): // Bullets require a space in the spec
|
case strings.HasPrefix(line, "* "): // Bullets require a space in the spec
|
||||||
return GemtextObject{Type: LIST, Text: line[2:], Literal: line}, nil
|
return GemtextObject{Type: LIST, Text: line[2:], Literal: line}, nil
|
||||||
}
|
}
|
||||||
return GemtextObject{Type: TEXT, Text: line, Literal: line}, nil
|
return GemtextObject{Type: TEXT, Text: line, Literal: line}, nil
|
||||||
} else {
|
} else {
|
||||||
if strings.HasPrefix(line, "```") { // toggles are the only special line type in preformatted mode
|
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_TOGGLE, Literal: line, Text: line[3:]}, nil
|
||||||
}
|
}
|
||||||
return GemtextObject{Type: PREFORMATTED_TEXT, Text: line, Literal: line}, 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.
|
// ParsePage takes a string containing the contents of a gemtext page and returns a GemtextPage.
|
||||||
func ParsePage(p string) (GemtextPage, error) {
|
func ParsePage(p string) (GemtextPage, error) {
|
||||||
preformatted := false
|
preformatted := false
|
||||||
lines := strings.Split(p, "\n")
|
lines := strings.Split(p, "\n")
|
||||||
var page GemtextPage
|
var page GemtextPage
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
l, err := ParseLine(line, preformatted)
|
l, err := ParseLine(line, preformatted)
|
||||||
page = append(page, l)
|
page = append(page, l)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return GemtextPage{}, err
|
return GemtextPage{}, err
|
||||||
}
|
}
|
||||||
if l.Type == PREFORMATTED_TOGGLE {
|
if l.Type == PREFORMATTED_TOGGLE {
|
||||||
preformatted = !preformatted
|
preformatted = !preformatted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return page, nil
|
return page, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,25 @@ type LineCase struct {
|
||||||
Want GemtextObject
|
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.
|
// These tests suck.
|
||||||
func TestParseLine(t *testing.T) {
|
func TestParseLine(t *testing.T) {
|
||||||
cases := []LineCase{
|
cases := []LineCase{
|
||||||
|
@ -194,23 +213,9 @@ func TestParseLine(t *testing.T) {
|
||||||
// We test each line in ParseLine()
|
// We test each line in ParseLine()
|
||||||
// so all we have to test for ParsePage() is blank lines and preformatting.
|
// so all we have to test for ParsePage() is blank lines and preformatting.
|
||||||
func TestParsePage(t *testing.T) {
|
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
|
input := testdocument
|
||||||
want := GemtextPage{
|
want := testdocumenttree
|
||||||
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:"```"},
|
|
||||||
}
|
|
||||||
output, _ := ParsePage(input)
|
output, _ := ParsePage(input)
|
||||||
for i, _ := range output {
|
for i, _ := range output {
|
||||||
if output[i] != want[i] {
|
if output[i] != want[i] {
|
||||||
|
|
63
render.go
63
render.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"html"
|
||||||
)
|
)
|
||||||
|
|
||||||
// addPrefix adds the prefix "pref" to a path if it is absolute
|
// 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
|
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)
|
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