- commit
- f33ac35
- parent
- 568a6ac
- author
- Eric Bower
- date
- 2026-01-10 13:30:43 -0500 EST
chore(prose): list parser tests
2 files changed,
+404,
-1
1@@ -12,6 +12,15 @@ import (
2 "github.com/araddon/dateparse"
3 )
4
5+/*
6+=> https://www.youtube.com/watch?v=HxaD_trXwRE Lexical Scanning in Go - Rob Pike
7+func run() {
8+ for state := startState; state != nil {
9+ state = stateFn(lexer);
10+ }
11+}
12+*/
13+
14 var reIndent = regexp.MustCompile(`^[[:blank:]]+`)
15
16 type ListParsedText struct {
17@@ -99,7 +108,7 @@ func PublishAtDate(date string) (*time.Time, error) {
18
19 func TokenToMetaField(meta *ListMetaData, token *SplitToken) error {
20 switch token.Key {
21- case "publish_at":
22+ case "date":
23 publishAt, err := PublishAtDate(token.Value)
24 if err == nil {
25 meta.PublishAt = publishAt
1@@ -0,0 +1,394 @@
2+package shared
3+
4+import (
5+ "reflect"
6+ "testing"
7+)
8+
9+func TestListParseText(t *testing.T) {
10+ t.Run("TestEmptyList", func(t *testing.T) {
11+ text := ""
12+ parsed := ListParseText(text)
13+ if parsed == nil {
14+ t.Fatal("parsed should not be nil")
15+ }
16+ if len(parsed.Items) != 0 {
17+ t.Fatalf("expected 0 items, got %d", len(parsed.Items))
18+ }
19+ if parsed.ListMetaData == nil {
20+ t.Fatal("ListMetaData should not be nil")
21+ }
22+ })
23+
24+ t.Run("TestSimpleListItems", func(t *testing.T) {
25+ text := "First item\nSecond item"
26+ parsed := ListParseText(text)
27+ if parsed == nil {
28+ t.Fatal("parsed should not be nil")
29+ }
30+ if len(parsed.Items) != 2 {
31+ t.Fatalf("expected 2 items, got %d", len(parsed.Items))
32+ }
33+ if parsed.Items[0].Value != "First item" {
34+ t.Errorf("expected 'First item', got '%s'", parsed.Items[0].Value)
35+ }
36+ if !parsed.Items[0].IsText {
37+ t.Error("expected IsText to be true")
38+ }
39+ if parsed.Items[1].Value != "Second item" {
40+ t.Errorf("expected 'Second item', got '%s'", parsed.Items[1].Value)
41+ }
42+ if !parsed.Items[1].IsText {
43+ t.Error("expected IsText to be true")
44+ }
45+ })
46+
47+ t.Run("TestListWithEmptyLines", func(t *testing.T) {
48+ text := "First item\n\nSecond item"
49+ parsed := ListParseText(text)
50+ if parsed == nil {
51+ t.Fatal("parsed should not be nil")
52+ }
53+ if len(parsed.Items) != 2 {
54+ t.Fatalf("expected 2 items, got %d", len(parsed.Items))
55+ }
56+ if parsed.Items[0].Value != "First item" {
57+ t.Errorf("expected 'First item', got '%s'", parsed.Items[0].Value)
58+ }
59+ if parsed.Items[1].Value != "Second item" {
60+ t.Errorf("expected 'Second item', got '%s'", parsed.Items[1].Value)
61+ }
62+ })
63+
64+ t.Run("TestHyperlinks", func(t *testing.T) {
65+ text := "=> https://pico.sh\n=> https://prose.sh blog platform"
66+ parsed := ListParseText(text)
67+ if parsed == nil {
68+ t.Fatal("parsed should not be nil")
69+ }
70+ if len(parsed.Items) != 2 {
71+ t.Fatalf("expected 2 items, got %d", len(parsed.Items))
72+ }
73+
74+ if !parsed.Items[0].IsURL {
75+ t.Error("expected IsURL to be true")
76+ }
77+ if string(parsed.Items[0].URL) != "https://pico.sh" {
78+ t.Errorf("expected URL 'https://pico.sh', got '%s'", parsed.Items[0].URL)
79+ }
80+ if parsed.Items[0].Value != "https://pico.sh" {
81+ t.Errorf("expected Value 'https://pico.sh', got '%s'", parsed.Items[0].Value)
82+ }
83+
84+ if !parsed.Items[1].IsURL {
85+ t.Error("expected IsURL to be true")
86+ }
87+ if string(parsed.Items[1].URL) != "https://prose.sh" {
88+ t.Errorf("expected URL 'https://prose.sh', got '%s'", parsed.Items[1].URL)
89+ }
90+ if parsed.Items[1].Value != "blog platform" {
91+ t.Errorf("expected Value 'blog platform', got '%s'", parsed.Items[1].Value)
92+ }
93+ })
94+
95+ t.Run("TestImages", func(t *testing.T) {
96+ text := "=< https://i.imgur.com/iXMNUN5.jpg\n=< https://i.imgur.com/iXMNUN5.jpg I use arch, btw"
97+ parsed := ListParseText(text)
98+ if parsed == nil {
99+ t.Fatal("parsed should not be nil")
100+ }
101+ if len(parsed.Items) != 2 {
102+ t.Fatalf("expected 2 items, got %d", len(parsed.Items))
103+ }
104+
105+ if !parsed.Items[0].IsImg {
106+ t.Error("expected IsImg to be true")
107+ }
108+ if string(parsed.Items[0].URL) != "https://i.imgur.com/iXMNUN5.jpg" {
109+ t.Errorf("expected URL 'https://i.imgur.com/iXMNUN5.jpg', got '%s'", parsed.Items[0].URL)
110+ }
111+ if parsed.Items[0].Value != "https://i.imgur.com/iXMNUN5.jpg" {
112+ t.Errorf("expected Value 'https://i.imgur.com/iXMNUN5.jpg', got '%s'", parsed.Items[0].Value)
113+ }
114+
115+ if !parsed.Items[1].IsImg {
116+ t.Error("expected IsImg to be true")
117+ }
118+ if string(parsed.Items[1].URL) != "https://i.imgur.com/iXMNUN5.jpg" {
119+ t.Errorf("expected URL 'https://i.imgur.com/iXMNUN5.jpg', got '%s'", parsed.Items[1].URL)
120+ }
121+ if parsed.Items[1].Value != "I use arch, btw" {
122+ t.Errorf("expected Value 'I use arch, btw', got '%s'", parsed.Items[1].Value)
123+ }
124+ })
125+
126+ t.Run("TestHeaders", func(t *testing.T) {
127+ text := "# Header One\n## Header Two"
128+ parsed := ListParseText(text)
129+ if parsed == nil {
130+ t.Fatal("parsed should not be nil")
131+ }
132+ if len(parsed.Items) != 2 {
133+ t.Fatalf("expected 2 items, got %d", len(parsed.Items))
134+ }
135+
136+ if !parsed.Items[0].IsHeaderOne {
137+ t.Error("expected IsHeaderOne to be true")
138+ }
139+ if parsed.Items[0].Value != " Header One" {
140+ t.Errorf("expected ' Header One', got '%s'", parsed.Items[0].Value)
141+ }
142+
143+ if !parsed.Items[1].IsHeaderTwo {
144+ t.Error("expected IsHeaderTwo to be true")
145+ }
146+ if parsed.Items[1].Value != " Header Two" {
147+ t.Errorf("expected ' Header Two', got '%s'", parsed.Items[1].Value)
148+ }
149+ })
150+
151+ t.Run("TestBlockquotes", func(t *testing.T) {
152+ text := "> This is a blockquote."
153+ parsed := ListParseText(text)
154+ if parsed == nil {
155+ t.Fatal("parsed should not be nil")
156+ }
157+ if len(parsed.Items) != 1 {
158+ t.Fatalf("expected 1 item, got %d", len(parsed.Items))
159+ }
160+
161+ if !parsed.Items[0].IsBlock {
162+ t.Error("expected IsBlock to be true")
163+ }
164+ if parsed.Items[0].Value != " This is a blockquote." {
165+ t.Errorf("expected ' This is a blockquote.', got '%s'", parsed.Items[0].Value)
166+ }
167+ })
168+
169+ t.Run("TestPreformattedText", func(t *testing.T) {
170+ text := "```\nsimple preformatted\nline 2\n```"
171+ parsed := ListParseText(text)
172+ if parsed == nil {
173+ t.Fatal("parsed should not be nil")
174+ }
175+ if len(parsed.Items) != 1 {
176+ t.Fatalf("expected 1 item, got %d", len(parsed.Items))
177+ }
178+
179+ if !parsed.Items[0].IsPre {
180+ t.Error("expected IsPre to be true")
181+ }
182+ expected := "\nsimple preformatted\nline 2"
183+ if expected != parsed.Items[0].Value {
184+ t.Errorf("expected '%s', got '%s'", expected, parsed.Items[0].Value)
185+ }
186+ })
187+
188+ t.Run("TestUnclosedPreformattedText", func(t *testing.T) {
189+ text := "```\nsimple unclosed"
190+ parsed := ListParseText(text)
191+ if parsed == nil {
192+ t.Fatal("parsed should not be nil")
193+ }
194+ if len(parsed.Items) != 1 {
195+ t.Fatalf("expected 1 item, got %d", len(parsed.Items))
196+ }
197+ if !parsed.Items[0].IsPre {
198+ t.Error("expected IsPre to be true")
199+ }
200+ expected := "\nsimple unclosed"
201+ if expected != parsed.Items[0].Value {
202+ t.Errorf("expected '%s', got '%s'", expected, parsed.Items[0].Value)
203+ }
204+ })
205+
206+ t.Run("TestVariableParsing", func(t *testing.T) {
207+ text := "=: title Hello World\n=: tags one, two, three\nAn actual list item"
208+ parsed := ListParseText(text)
209+ if parsed == nil {
210+ t.Fatal("parsed should not be nil")
211+ }
212+ if len(parsed.Items) != 3 {
213+ t.Fatalf("expected 3 items, got %d", len(parsed.Items))
214+ }
215+ if parsed.Items[0].Value != "=: title Hello World" {
216+ t.Errorf("expected '=: title Hello World', got '%s'", parsed.Items[0].Value)
217+ }
218+ if parsed.Items[1].Value != "=: tags one, two, three" {
219+ t.Errorf("expected '=: tags one, two, three', got '%s'", parsed.Items[1].Value)
220+ }
221+ if parsed.Items[2].Value != "An actual list item" {
222+ t.Errorf("expected 'An actual list item', got '%s'", parsed.Items[2].Value)
223+ }
224+
225+ if parsed.Title != "Hello World" {
226+ t.Errorf("expected Title 'Hello World', got '%s'", parsed.Title)
227+ }
228+ expectedTags := []string{"one", "two", "three"}
229+ if !reflect.DeepEqual(expectedTags, parsed.Tags) {
230+ t.Errorf("expected Tags '%v', got '%v'", expectedTags, parsed.Tags)
231+ }
232+ })
233+
234+ t.Run("TestNestedListWithSpaces", func(t *testing.T) {
235+ text := "first item\n second item\n third item\nlast item"
236+ parsed := ListParseText(text)
237+ if parsed == nil {
238+ t.Fatal("parsed should not be nil")
239+ }
240+ if len(parsed.Items) != 4 {
241+ t.Fatalf("expected 4 items, got %d", len(parsed.Items))
242+ }
243+
244+ if parsed.Items[0].Indent != 0 {
245+ t.Errorf("expected Indent 0, got %d", parsed.Items[0].Indent)
246+ }
247+ if parsed.Items[1].Indent != 1 {
248+ t.Errorf("expected Indent 1, got %d", parsed.Items[1].Indent)
249+ }
250+ if parsed.Items[2].Indent != 2 {
251+ t.Errorf("expected Indent 2, got %d", parsed.Items[2].Indent)
252+ }
253+ if parsed.Items[3].Indent != 0 {
254+ t.Errorf("expected Indent 0, got %d", parsed.Items[3].Indent)
255+ }
256+ })
257+
258+ t.Run("TestNestedListWithTabs", func(t *testing.T) {
259+ text := "first item\n\tsecond item\n\t\tthird item\nlast item"
260+ parsed := ListParseText(text)
261+ if parsed == nil {
262+ t.Fatal("parsed should not be nil")
263+ }
264+ if len(parsed.Items) != 4 {
265+ t.Fatalf("expected 4 items, got %d", len(parsed.Items))
266+ }
267+
268+ if parsed.Items[0].Indent != 0 {
269+ t.Errorf("expected Indent 0, got %d", parsed.Items[0].Indent)
270+ }
271+ if parsed.Items[1].Indent != 1 {
272+ t.Errorf("expected Indent 1, got %d", parsed.Items[1].Indent)
273+ }
274+ if parsed.Items[2].Indent != 2 {
275+ t.Errorf("expected Indent 2, got %d", parsed.Items[2].Indent)
276+ }
277+ if parsed.Items[3].Indent != 0 {
278+ t.Errorf("expected Indent 0, got %d", parsed.Items[3].Indent)
279+ }
280+ })
281+
282+ t.Run("TestDeeplyNestedList", func(t *testing.T) {
283+ text := "1\n 2\n 3\n 4\n 5"
284+ parsed := ListParseText(text)
285+ if parsed == nil {
286+ t.Fatal("parsed should not be nil")
287+ }
288+ if len(parsed.Items) != 5 {
289+ t.Fatalf("expected 5 items, got %d", len(parsed.Items))
290+ }
291+
292+ if parsed.Items[0].Indent != 0 {
293+ t.Errorf("expected Indent 0, got %d", parsed.Items[0].Indent)
294+ }
295+ if parsed.Items[1].Indent != 1 {
296+ t.Errorf("expected Indent 1, got %d", parsed.Items[1].Indent)
297+ }
298+ if parsed.Items[2].Indent != 2 {
299+ t.Errorf("expected Indent 2, got %d", parsed.Items[2].Indent)
300+ }
301+ if parsed.Items[3].Indent != 3 {
302+ t.Errorf("expected Indent 3, got %d", parsed.Items[3].Indent)
303+ }
304+ if parsed.Items[4].Indent != 4 {
305+ t.Errorf("expected Indent 4, got %d", parsed.Items[4].Indent)
306+ }
307+ })
308+
309+ t.Run("TestMixedSpaceAndTabIndentation", func(t *testing.T) {
310+ // spec advises against this, but we test the behavior
311+ text := "first\n\tsecond\n third"
312+ parsed := ListParseText(text)
313+
314+ if len(parsed.Items) != 3 {
315+ t.Fatalf("expected 3 items, got %d", len(parsed.Items))
316+ }
317+ if parsed.Items[0].Indent != 0 {
318+ t.Errorf("expected Indent 0, got %d", parsed.Items[0].Indent)
319+ }
320+ if parsed.Items[1].Indent != 1 {
321+ t.Errorf("expected Indent 1, got %d", parsed.Items[1].Indent)
322+ }
323+ if parsed.Items[2].Indent != 2 {
324+ t.Errorf("expected Indent 2, got %d", parsed.Items[2].Indent)
325+ }
326+ })
327+
328+ t.Run("TestComprehensiveList", func(t *testing.T) {
329+ text := `=: title My Awesome List
330+# Big Header
331+Here is a normal item.
332+=> https://pico.sh
333+ * nested item 1
334+> A wise man once said...
335+`
336+ parsed := ListParseText(text)
337+ if parsed == nil {
338+ t.Fatal("parsed should not be nil")
339+ }
340+ if parsed.Title != "My Awesome List" {
341+ t.Errorf("expected Title 'My Awesome List', got '%s'", parsed.Title)
342+ }
343+
344+ if len(parsed.Items) != 6 {
345+ t.Fatalf("expected 6 items, got %d", len(parsed.Items))
346+ }
347+
348+ if parsed.Items[0].Value != "=: title My Awesome List" {
349+ t.Errorf("expected '=: title My Awesome List', got '%s'", parsed.Items[0].Value)
350+ }
351+ if !parsed.Items[1].IsHeaderOne {
352+ t.Error("expected IsHeaderOne to be true")
353+ }
354+ if parsed.Items[1].Value != " Big Header" {
355+ t.Errorf("expected ' Big Header', got '%s'", parsed.Items[1].Value)
356+ }
357+ if !parsed.Items[2].IsText {
358+ t.Error("expected IsText to be true")
359+ }
360+ if parsed.Items[2].Value != "Here is a normal item." {
361+ t.Errorf("expected 'Here is a normal item.', got '%s'", parsed.Items[2].Value)
362+ }
363+ if !parsed.Items[3].IsURL {
364+ t.Error("expected IsURL to be true")
365+ }
366+ if parsed.Items[4].Value != "* nested item 1" {
367+ t.Errorf("expected '* nested item 1', got '%s'", parsed.Items[4].Value)
368+ }
369+ if !parsed.Items[5].IsBlock {
370+ t.Error("expected IsBlock to be true")
371+ }
372+ })
373+
374+ t.Run("TestMalformedLines", func(t *testing.T) {
375+ text := "= > malformed url\n= < malformed image"
376+ parsed := ListParseText(text)
377+ if len(parsed.Items) != 2 {
378+ t.Fatalf("expected 2 items, got %d", len(parsed.Items))
379+ }
380+
381+ if !parsed.Items[0].IsText {
382+ t.Error("expected IsText to be true")
383+ }
384+ if parsed.Items[0].Value != "= > malformed url" {
385+ t.Errorf("expected '= > malformed url', got '%s'", parsed.Items[0].Value)
386+ }
387+
388+ if !parsed.Items[1].IsText {
389+ t.Error("expected IsText to be true")
390+ }
391+ if parsed.Items[1].Value != "= < malformed image" {
392+ t.Errorf("expected '= < malformed image', got '%s'", parsed.Items[1].Value)
393+ }
394+ })
395+}