diff --git a/parser.go b/parser.go index 615634c..d29475e 100644 --- a/parser.go +++ b/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 } diff --git a/parser_test.go b/parser_test.go index 1712ea5..dd0d573 100644 --- a/parser_test.go +++ b/parser_test.go @@ -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] { diff --git a/render.go b/render.go index 13c5752..4845e2c 100644 --- a/render.go +++ b/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 += "" + listmode = false + } + } + if quotemode { + if item.Type != QUOTE { + s += "" + quotemode = false + }} + switch { + case item.Type == TEXT: + s += fmt.Sprintf("

%s

", escape(item.Text)) + case item.Type == LINK: + s += fmt.Sprintf("%s
", escape(item.Path), escape(item.Text)) + case item.Type == PREFORMATTED_TOGGLE: + if premode { + s += "" + premode = false + } else { + s += "
"
+					premode = true
+				}
+			case item.Type == PREFORMATTED_TEXT:
+				s += escape(item.Text) + "\n"
+			case item.Type == HEADING:
+				s += fmt.Sprintf("%s", item.Level, escape(item.Text), item.Level)
+			case item.Type == LIST:
+				if !listmode {
+					s += ""
+	}
+	if quotemode {
+		s += ""
+	}
+	if premode {
+		s += "
" + } + return s, nil } \ No newline at end of file diff --git a/render_test.go b/render_test.go index 1bf3f24..13a35cc 100644 --- a/render_test.go +++ b/render_test.go @@ -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 := "

Document

This is paragraph text & It exists

example.com
this should be preformatted.\n=> https://example.com Not a link\n
one line of pre\n
Something\n- someone\n
" + if out != want { + t.Errorf("Got %#v want %#v", out, want) + } + } \ No newline at end of file