diff options
-rw-r--r-- | gemini/gemtext/parse_line.go | 43 | ||||
-rw-r--r-- | gemini/gemtext/parse_line_test.go | 51 | ||||
-rw-r--r-- | gemini/gemtext/parse_test.go | 39 | ||||
-rw-r--r-- | gemini/gemtext/types.go | 26 |
4 files changed, 142 insertions, 17 deletions
diff --git a/gemini/gemtext/parse_line.go b/gemini/gemtext/parse_line.go index 39187a8..e31e5b6 100644 --- a/gemini/gemtext/parse_line.go +++ b/gemini/gemtext/parse_line.go @@ -10,10 +10,16 @@ func ParseLine(line []byte) Line { switch line[0] { case '=': - if len(line) == 1 || line[1] != '>' { + if len(line) == 1 { break } - return parseLinkLine(line) + if line[1] == '>' { + return parseLinkLine(line) + } + if line[1] == ':' { + return parsePromptLine(line) + } + break case '`': if len(line) < 3 || line[1] != '`' || line[2] != '`' { break @@ -73,6 +79,39 @@ func parseLinkLine(raw []byte) LinkLine { return line } +func parsePromptLine(raw []byte) PromptLine { + line := PromptLine{raw: raw} + + // move past =:[<whitespace>] + raw = bytes.TrimLeft(raw[2:], " \t") + + // find the next space or tab + spIdx := bytes.IndexByte(raw, ' ') + tbIdx := bytes.IndexByte(raw, '\t') + idx := spIdx + if idx == -1 { + idx = tbIdx + } + if tbIdx >= 0 && tbIdx < idx { + idx = tbIdx + } + + if idx < 0 { + line.url = bytes.TrimRight(raw, "\r\n") + return line + } + + line.url = raw[:idx] + raw = raw[idx+1:] + + label := bytes.TrimRight(bytes.TrimLeft(raw, " \t"), "\r\n") + if len(label) > 0 { + line.label = label + } + + return line +} + func parsePreformatToggleLine(raw []byte) PreformatToggleLine { line := PreformatToggleLine{raw: raw} diff --git a/gemini/gemtext/parse_line_test.go b/gemini/gemtext/parse_line_test.go index a07fa3b..82b0c28 100644 --- a/gemini/gemtext/parse_line_test.go +++ b/gemini/gemtext/parse_line_test.go @@ -57,6 +57,57 @@ func TestParseLinkLine(t *testing.T) { } } +func TestParsePromptLine(t *testing.T) { + tests := []struct { + input string + url string + label string + }{ + { + input: "=: gemini.ctrl-c.club/~tjp/ home page\r\n", + url: "gemini.ctrl-c.club/~tjp/", + label: "home page", + }, + { + input: "=: gemi.dev/\n", + url: "gemi.dev/", + }, + { + input: "=: /gemlog/foobar 2023-01-13 - Foo Bar\n", + url: "/gemlog/foobar", + label: "2023-01-13 - Foo Bar", + }, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + line := gemtext.ParseLine([]byte(test.input)) + if line == nil { + t.Fatal("ParseLine() returned nil line") + } + if string(line.Raw()) != string(test.input) { + t.Error("Raw() does not match input") + } + + if line.Type() != gemtext.LineTypePrompt{ + t.Errorf("expected LineTypePrompt, got %d", line.Type()) + } + link, ok := line.(gemtext.PromptLine) + if !ok { + t.Fatalf("expected a PromptLine, got %T", line) + } + + if link.URL() != test.url { + t.Errorf("expected url %q, got %q", test.url, link.URL()) + } + + if link.Label() != test.label { + t.Errorf("expected label %q, got %q", test.label, link.Label()) + } + }) + } +} + func TestParsePreformatToggleLine(t *testing.T) { tests := []struct { input string diff --git a/gemini/gemtext/parse_test.go b/gemini/gemtext/parse_test.go index d2860ff..6b35431 100644 --- a/gemini/gemtext/parse_test.go +++ b/gemini/gemtext/parse_test.go @@ -24,6 +24,8 @@ This is some non-blank regular text. => gemini://google.com/ as if +=: spartan://foo.bar/baz this should be a spartan prompt + > this is a quote > -tjp @@ -37,7 +39,7 @@ This is some non-blank regular text. doc, err := gemtext.Parse(bytes.NewBuffer(docBytes)) require.Nil(t, err) - require.Equal(t, 18, len(doc)) + require.Equal(t, 20, len(doc)) assert.Equal(t, gemtext.LineTypeHeading1, doc[0].Type()) assert.Equal(t, "# top-level header line\n", string(doc[0].Raw())) @@ -74,26 +76,33 @@ This is some non-blank regular text. assertEmptyLine(t, doc[11]) - assert.Equal(t, gemtext.LineTypeQuote, doc[12].Type()) - assert.Equal(t, "> this is a quote\n", string(doc[12].Raw())) - assert.Equal(t, " this is a quote", doc[12].(gemtext.QuoteLine).Body()) + assert.Equal(t, gemtext.LineTypePrompt, doc[12].Type()) + assert.Equal(t, "=: spartan://foo.bar/baz this should be a spartan prompt\n", string(doc[12].Raw())) + assert.Equal(t, "spartan://foo.bar/baz", doc[12].(gemtext.PromptLine).URL()) + assert.Equal(t, "this should be a spartan prompt", doc[12].(gemtext.PromptLine).Label()) - assert.Equal(t, gemtext.LineTypeQuote, doc[13].Type()) - assert.Equal(t, "> -tjp\n", string(doc[13].Raw())) - assert.Equal(t, " -tjp", doc[13].(gemtext.QuoteLine).Body()) + assertEmptyLine(t, doc[13]) - assertEmptyLine(t, doc[14]) + assert.Equal(t, gemtext.LineTypeQuote, doc[14].Type()) + assert.Equal(t, "> this is a quote\n", string(doc[14].Raw())) + assert.Equal(t, " this is a quote", doc[14].(gemtext.QuoteLine).Body()) - assert.Equal(t, gemtext.LineTypePreformatToggle, doc[15].Type()) - assert.Equal(t, "```pre-formatted code\n", string(doc[15].Raw())) - assert.Equal(t, "pre-formatted code", doc[15].(gemtext.PreformatToggleLine).AltText()) + assert.Equal(t, gemtext.LineTypeQuote, doc[15].Type()) + assert.Equal(t, "> -tjp\n", string(doc[15].Raw())) + assert.Equal(t, " -tjp", doc[15].(gemtext.QuoteLine).Body()) - assert.Equal(t, gemtext.LineTypePreformattedText, doc[16].Type()) - assert.Equal(t, "doc := gemtext.Parse(req.Body)\n", string(doc[16].Raw())) + assertEmptyLine(t, doc[16]) assert.Equal(t, gemtext.LineTypePreformatToggle, doc[17].Type()) - assert.Equal(t, "```ignored closing alt-text\n", string(doc[17].Raw())) - assert.Equal(t, "", doc[17].(gemtext.PreformatToggleLine).AltText()) + assert.Equal(t, "```pre-formatted code\n", string(doc[17].Raw())) + assert.Equal(t, "pre-formatted code", doc[17].(gemtext.PreformatToggleLine).AltText()) + + assert.Equal(t, gemtext.LineTypePreformattedText, doc[18].Type()) + assert.Equal(t, "doc := gemtext.Parse(req.Body)\n", string(doc[18].Raw())) + + assert.Equal(t, gemtext.LineTypePreformatToggle, doc[19].Type()) + assert.Equal(t, "```ignored closing alt-text\n", string(doc[19].Raw())) + assert.Equal(t, "", doc[19].(gemtext.PreformatToggleLine).AltText()) // ensure we can rebuild the original doc from all the line.Raw()s buf := &bytes.Buffer{} diff --git a/gemini/gemtext/types.go b/gemini/gemtext/types.go index 440fed4..3965b11 100644 --- a/gemini/gemtext/types.go +++ b/gemini/gemtext/types.go @@ -16,6 +16,13 @@ const ( // The line is a LinkLine. LineTypeLink + // LineTypePrompt is a spartan =: prompt line. + // + // =:[<ws>]<url>[<ws><label>][\r]\n + // + // The line is a PromptLine. + LineTypePrompt + // LineTypePreformatToggle switches the document between pre-formatted text or not. // // ```[<alt-text>][\r]\n @@ -111,6 +118,25 @@ func (ll LinkLine) URL() string { return string(ll.url) } // Label returns the label portion of the line. func (ll LinkLine) Label() string { return string(ll.label) } +// PromptLine is a Spartan =: prompt line. +type PromptLine struct { + raw []byte + url []byte + label []byte +} + +func (pl PromptLine) Type() LineType { return LineTypePrompt } +func (pl PromptLine) Raw() []byte { return pl.raw } +func (pl PromptLine) String() string { return string(pl.raw) } + +// URL returns the original url portion of the line. +// +// It is not guaranteed to be a valid URL. +func (pl PromptLine) URL() string { return string(pl.url) } + +// Label retrns the label portion of the line. +func (pl PromptLine) Label() string { return string(pl.label) } + // PreformatToggleLine is a preformatted text toggle line. type PreformatToggleLine struct { raw []byte |