Compare commits
5 Commits
872459479c
...
e14436a8e9
Author | SHA1 | Date |
---|---|---|
Nico | e14436a8e9 | |
Nico | b6e1c2ac1e | |
Nico | 4f40ff17b6 | |
Nico | 561165ad84 | |
Nico | c462020d16 |
78
parser.go
78
parser.go
|
@ -4,7 +4,6 @@ package gemtext
|
|||
import (
|
||||
"strings"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type ObjectType int64
|
||||
|
@ -16,7 +15,6 @@ const (
|
|||
PREFORMATTED_TEXT
|
||||
HEADING
|
||||
LIST
|
||||
LISTMARKER // Marks the start/end of a list. Not a real gemtext object but useful for conversion.
|
||||
QUOTE
|
||||
)
|
||||
|
||||
|
@ -29,30 +27,18 @@ type GemtextObject struct {
|
|||
Level int // Populated if object is a heading
|
||||
}
|
||||
|
||||
// type GemtextPage represents a gemtext page.
|
||||
// type GemtextPage represents a gemtext page. It is equivalent to a slice of GemtextObjects.
|
||||
type GemtextPage []GemtextObject
|
||||
|
||||
|
||||
// addPrefix adds the prefix "pref" to a path if it is absolute
|
||||
// this is to allow protocol-independent absolute links
|
||||
// in input files.
|
||||
func addPrefix(pref string, link string) string {
|
||||
if filepath.IsAbs(link) {
|
||||
return filepath.Join(pref, link)
|
||||
} else {
|
||||
return link
|
||||
}
|
||||
}
|
||||
|
||||
// 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] != "=>" {
|
||||
if !strings.HasPrefix(l, "=>") {
|
||||
return GemtextObject{}, fmt.Errorf("Not a gemtext link!")
|
||||
} else {
|
||||
f := strings.Fields(l[2:])
|
||||
link := GemtextObject{Type: LINK, Literal: l}
|
||||
link.Path = f[1]
|
||||
link.Text = strings.Join(f[2:]," ")
|
||||
link.Path = f[0]
|
||||
link.Text = strings.TrimSpace(l[3+len(f[0]):]) // text that remains after removing the URL and link marker
|
||||
return link, nil
|
||||
}
|
||||
|
||||
|
@ -61,11 +47,13 @@ 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, "### "):
|
||||
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, "## "):
|
||||
case strings.HasPrefix(l, "##"):
|
||||
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: TEXT, Text:l, Literal: l}, nil
|
||||
|
@ -74,23 +62,43 @@ func ParseHeading(l string) (GemtextObject, error) {
|
|||
// 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) {
|
||||
l := strings.TrimSpace(line)
|
||||
if !isPreformatted {
|
||||
switch {
|
||||
case strings.HasPrefix(l, "=>"):
|
||||
case strings.HasPrefix(line, "=>"):
|
||||
return ParseLink(line)
|
||||
case strings.HasPrefix(l, "```"):
|
||||
return GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: l}, nil
|
||||
case strings.HasPrefix(l, "#"):
|
||||
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(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
|
||||
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: l, Literal: l}, 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 ParseFile() {}
|
||||
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
|
||||
}
|
||||
|
|
124
parser_test.go
124
parser_test.go
|
@ -1,16 +1,20 @@
|
|||
package gemtext
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type LineCase struct {
|
||||
Str string
|
||||
Preformatted bool
|
||||
Want GemtextObject
|
||||
}
|
||||
// These tests suck. They're not really tests.
|
||||
|
||||
// These tests suck.
|
||||
func TestParseLine(t *testing.T) {
|
||||
cases := []LineCase{
|
||||
LineCase{Str: "```", Preformatted: false, Want: GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: "```"}},
|
||||
LineCase{Str: "```alt", Preformatted: false, Want: GemtextObject{Type: PREFORMATTED_TOGGLE, Literal: "```alt", Text:"alt"}},
|
||||
LineCase{
|
||||
Str: "This is a test of a normal text line.",
|
||||
Preformatted: false,
|
||||
|
@ -54,6 +58,24 @@ func TestParseLine(t *testing.T) {
|
|||
Text: "heading 3",
|
||||
Level: 3},
|
||||
},
|
||||
LineCase{
|
||||
Str: "###heading 3",
|
||||
Preformatted: false,
|
||||
Want: GemtextObject{
|
||||
Type: HEADING,
|
||||
Literal: "###heading 3",
|
||||
Text: "heading 3",
|
||||
Level: 3},
|
||||
},
|
||||
LineCase{
|
||||
Str: "### heading 3",
|
||||
Preformatted: false,
|
||||
Want: GemtextObject{
|
||||
Type: HEADING,
|
||||
Literal: "### heading 3",
|
||||
Text: "heading 3",
|
||||
Level: 3},
|
||||
},
|
||||
LineCase{
|
||||
Str: "#### heading 4",
|
||||
Preformatted: false,
|
||||
|
@ -66,9 +88,9 @@ func TestParseLine(t *testing.T) {
|
|||
Str: "*list item",
|
||||
Preformatted: false,
|
||||
Want: GemtextObject{
|
||||
Type: LIST,
|
||||
Type: TEXT,
|
||||
Literal: "*list item",
|
||||
Text: "list item"},
|
||||
Text: "*list item"},
|
||||
},
|
||||
LineCase{
|
||||
Str: "* list item",
|
||||
|
@ -94,6 +116,72 @@ func TestParseLine(t *testing.T) {
|
|||
Literal: "> quote",
|
||||
Text: "quote"},
|
||||
},
|
||||
LineCase{
|
||||
Str: "> quote",
|
||||
Preformatted: false,
|
||||
Want: GemtextObject{
|
||||
Type: QUOTE,
|
||||
Literal: "> quote",
|
||||
Text: "quote"},
|
||||
},
|
||||
LineCase{
|
||||
Str: "> quote",
|
||||
Preformatted: true,
|
||||
Want: GemtextObject{
|
||||
Type: PREFORMATTED_TEXT,
|
||||
Literal: "> quote",
|
||||
Text: "> quote"},
|
||||
},
|
||||
LineCase{
|
||||
Str: "=> https://example.com link",
|
||||
Preformatted: true,
|
||||
Want: GemtextObject{
|
||||
Type: PREFORMATTED_TEXT,
|
||||
Literal: "=> https://example.com link",
|
||||
Text: "=> https://example.com link"},
|
||||
},
|
||||
LineCase{
|
||||
Str: "=> https://example.com link",
|
||||
Preformatted: false,
|
||||
Want: GemtextObject{
|
||||
Type: LINK,
|
||||
Literal: "=> https://example.com link",
|
||||
Text: "link",
|
||||
Path: "https://example.com"},
|
||||
},
|
||||
LineCase{
|
||||
Str: "\t",
|
||||
Preformatted: true,
|
||||
Want: GemtextObject{
|
||||
Type: PREFORMATTED_TEXT,
|
||||
Literal: "\t",
|
||||
Text: "\t"},
|
||||
},
|
||||
LineCase{
|
||||
Str: "=>https://example.com link",
|
||||
Preformatted: false,
|
||||
Want: GemtextObject{
|
||||
Type: LINK,
|
||||
Literal: "=>https://example.com link",
|
||||
Text: "link",
|
||||
Path: "https://example.com"},
|
||||
},
|
||||
LineCase{
|
||||
Str: "\n",
|
||||
Preformatted: false,
|
||||
Want: GemtextObject{
|
||||
Type: TEXT,
|
||||
Literal: "\n",
|
||||
Text: "\n"},
|
||||
},
|
||||
LineCase{
|
||||
Str: "",
|
||||
Preformatted: false,
|
||||
Want: GemtextObject{
|
||||
Type: TEXT,
|
||||
Literal: "",
|
||||
Text: ""},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, _ := ParseLine(c.Str, c.Preformatted)
|
||||
|
@ -102,3 +190,31 @@ 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:"```"},
|
||||
}
|
||||
output, _ := ParsePage(input)
|
||||
for i, _ := range output {
|
||||
if output[i] != want[i] {
|
||||
t.Errorf("Wanted: %#v, got: %#v at index %d", want[i], output[i], i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
11
render.go
11
render.go
|
@ -2,8 +2,19 @@ package gemtext
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// addPrefix adds the prefix "pref" to a path if it is absolute
|
||||
// this is to allow protocol-independent absolute links.
|
||||
func addPrefix(pref string, link string) string {
|
||||
if filepath.IsAbs(link) {
|
||||
return filepath.Join(pref, link)
|
||||
} else {
|
||||
return link
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
Reference in New Issue