Compare commits

...

4 Commits

Author SHA1 Message Date
Nico 872459479c use GemtextObject in renderer 2020-11-12 10:27:02 +00:00
Nico 4d53e2fa87 more ParseLine cleanup 2020-11-12 10:26:46 +00:00
Nico 3149020ec5 clean up parser, change GeminiObject to GemtextObject 2020-11-12 10:23:00 +00:00
Nico 3155f9bc47 start writing some basic tests 2020-11-12 10:22:43 +00:00
3 changed files with 147 additions and 42 deletions

View File

@ -1,3 +1,4 @@
// Package gemtext provides a variety of tools for processing gemtext files.
package gemtext
import (
@ -18,8 +19,9 @@ const (
LISTMARKER // Marks the start/end of a list. Not a real gemtext object but useful for conversion.
QUOTE
)
// type GeminiObject represents a gemtext object.
type GeminiObject struct {
// 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.
Literal string // Contains the line as it exists in the file
@ -27,6 +29,9 @@ type GeminiObject struct {
Level int // Populated if object is a heading
}
// type GemtextPage represents a gemtext page.
type GemtextPage []GemtextObject
// addPrefix adds the prefix "pref" to a path if it is absolute
// this is to allow protocol-independent absolute links
@ -39,13 +44,13 @@ func addPrefix(pref string, link string) string {
}
}
// ParseLink takes a gemini link as a string and returns a GeminiObject and an error.
func ParseLink(l string) (GeminiObject, error) {
// ParseLink takes a gemini link as a string and returns a GemtextObject and an error.
func ParseLink(l string) (GemtextObject, error) {
f := strings.Fields(l)
if f[0] != "=>" {
return GeminiObject{}, fmt.Errorf("Not a gemtext link!")
return GemtextObject{}, fmt.Errorf("Not a gemtext link!")
} else {
link := GeminiObject{Type: LINK, Literal: l}
link := GemtextObject{Type: LINK, Literal: l}
link.Path = f[1]
link.Text = strings.Join(f[2:]," ")
return link, nil
@ -53,39 +58,39 @@ func ParseLink(l string) (GeminiObject, error) {
}
// ParseHeading parses a gemtext heading, returns a HEADING GeminiObject if it is between levels 1-3 and TEXT otherwise.
func ParseHeading(l string) (GeminiObject, error) {
if strings.HasPrefix(l, "### ") {
return GeminiObject{Type: HEADING, Level: 3, Text: strings.TrimSpace(l[3:]), Literal: l},nil
} else if strings.HasPrefix(l, "## ") {
return GeminiObject{Type: HEADING, Level: 2, Text: strings.TrimSpace(l[2:]), Literal: l},nil
} else if strings.HasPrefix(l, "# ") {
return GeminiObject{Type: HEADING, Level: 1, Text: strings.TrimSpace(l[1:]), Literal: l},nil
} else {
return GeminiObject{Type: TEXT, Text:l, Literal: l}, nil
}
// 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, "### "):
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 GeminiObject 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.
func ParseLine(line string, isPreformatted bool) (GeminiObject, error) {
func ParseLine(line string, isPreformatted bool) (GemtextObject, error) {
l := strings.TrimSpace(line)
if strings.HasPrefix(l, "=>") {
return ParseLink(line)
} else if strings.HasPrefix(l, "```") {
return GeminiObject{Type: PREFORMATTED_TOGGLE, Literal: l}, nil
} else if strings.HasPrefix(l, "#") {
return ParseHeading(line)
} else if strings.HasPrefix(l, ">") {
return GeminiObject{Type: QUOTE, Text: strings.TrimSpace(l[1:]), Literal: l}, nil
} else if strings.HasPrefix(l, "*") {
return GeminiObject{Type: LIST, Text: strings.TrimSpace(l[1:]), Literal: l}, nil
} else { // Everything else is just text
if isPreformatted {
return GeminiObject{Type: PREFORMATTED_TEXT, Text: l, Literal: l}, nil
} else {
return GeminiObject{Type: TEXT, Text: l, Literal: l}, nil
}
}
switch {
case strings.HasPrefix(l, "=>"):
return ParseLink(line)
case strings.HasPrefix(l, "```"):
return GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: l}, nil
case strings.HasPrefix(l, "#"):
return ParseHeading(line)
case strings.HasPrefix(l, ">"):
return GemtextObject{Type: QUOTE, Text: strings.TrimSpace(l[1:]), Literal: l}, nil
case strings.HasPrefix(l, "*"):
return GemtextObject{Type: LIST, Text: strings.TrimSpace(l[1:]), Literal: l}, nil
case isPreformatted:
return GemtextObject{Type: PREFORMATTED_TEXT, Text: l, Literal: l}, nil
}
return GemtextObject{Type: TEXT, Text: l, Literal: l}, nil
}
// ParsePage takes a string containing the contents of a gemtext page and returns a GemtextPage.
func ParseFile() {}

104
parser_test.go Normal file
View File

@ -0,0 +1,104 @@
package gemtext
import "testing"
type LineCase struct {
Str string
Preformatted bool
Want GemtextObject
}
// These tests suck. They're not really tests.
func TestParseLine(t *testing.T) {
cases := []LineCase{
LineCase{Str: "```", Preformatted: false, Want: GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: "```"}},
LineCase{
Str: "This is a test of a normal text line.",
Preformatted: false,
Want: GemtextObject{
Type: TEXT,
Literal: "This is a test of a normal text line.",
Text: "This is a test of a normal text line."},
},
LineCase{
Str: "This is a test of a normal text line.",
Preformatted: true,
Want: GemtextObject{
Type: PREFORMATTED_TEXT,
Literal: "This is a test of a normal text line.",
Text: "This is a test of a normal text line."},
},
LineCase{
Str: "# heading 1",
Preformatted: false,
Want: GemtextObject{
Type: HEADING,
Literal: "# heading 1",
Text: "heading 1",
Level: 1},
},
LineCase{
Str: "## heading 2",
Preformatted: false,
Want: GemtextObject{
Type: HEADING,
Literal: "## heading 2",
Text: "heading 2",
Level: 2},
},
LineCase{
Str: "### heading 3",
Preformatted: false,
Want: GemtextObject{
Type: HEADING,
Literal: "### heading 3",
Text: "heading 3",
Level: 3},
},
LineCase{
Str: "#### heading 4",
Preformatted: false,
Want: GemtextObject{
Type: TEXT,
Literal: "#### heading 4",
Text: "#### heading 4"},
},
LineCase{
Str: "*list item",
Preformatted: false,
Want: GemtextObject{
Type: LIST,
Literal: "*list item",
Text: "list item"},
},
LineCase{
Str: "* list item",
Preformatted: false,
Want: GemtextObject{
Type: LIST,
Literal: "* list item",
Text: "list item"},
},
LineCase{
Str: ">quote",
Preformatted: false,
Want: GemtextObject{
Type: QUOTE,
Literal: ">quote",
Text: "quote"},
},
LineCase{
Str: "> quote",
Preformatted: false,
Want: GemtextObject{
Type: QUOTE,
Literal: "> quote",
Text: "quote"},
},
}
for _, c := range cases {
got, _ := ParseLine(c.Str, c.Preformatted)
if got != c.Want {
t.Errorf("case %#v, got %#v", c, got)
}
}
}

View File

@ -4,12 +4,8 @@ import (
"fmt"
)
// TODO tests for renderer and parser
// TODO finish gemtext renderer
// TODO html renderer
// RenderLink takes a GeminiObject and returns a gemini link as a string or an error if the object is not a link.
func RenderLink(obj GeminiObject) (string, error) {
// RenderLink takes a GemtextObject and returns a gemini link as a string or an error if the object is not a link.
func RenderLink(obj GemtextObject) (string, error) {
if obj.Type != LINK {
return "", fmt.Errorf("Not a link!")
} else {