Commit f33ac35

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