- commit
- 0e93909
- parent
- 880b572
- author
- Eric Bower
- date
- 2022-08-05 20:48:37 +0000 UTC
feat(lists): nested lists Nested lists adds more complexity to our parser but the time complexity is still `O(n)` where `n = lines in the list`.
4 files changed,
+164,
-65
+21,
-5
1@@ -1,22 +1,38 @@
2 {{define "list"}}
3+{{$indent := 0}}
4+{{$mod := 0}}
5 <ul style="list-style-type: {{.ListType}};">
6 {{range .Items}}
7+ {{if lt $indent .Indent}}
8+ <ul style="list-style-type: {{$.ListType}};">
9+ {{else if gt $indent .Indent}}
10+
11+ {{$mod = minus $indent .Indent}}
12+ {{range $y := intRange 1 $mod}}
13+ </li></ul>
14+ {{end}}
15+
16+ {{else}}
17+ </li>
18+ {{end}}
19+ {{$indent = .Indent}}
20+
21 {{if .IsText}}
22 {{if .Value}}
23- <li>{{.Value}}</li>
24+ <li>{{.Value}}
25 {{end}}
26 {{end}}
27
28 {{if .IsURL}}
29- <li><a href="{{.URL}}">{{.Value}}</a></li>
30+ <li><a href="{{.URL}}">{{.Value}}</a>
31 {{end}}
32
33 {{if .IsImg}}
34- <li><img src="{{.URL}}" alt="{{.Value}}" /></li>
35+ <li><img src="{{.URL}}" alt="{{.Value}}" />
36 {{end}}
37
38 {{if .IsBlock}}
39- <li><blockquote>{{.Value}}</blockquote></li>
40+ <li><blockquote>{{.Value}}</blockquote>
41 {{end}}
42
43 {{if .IsHeaderOne}}
44@@ -28,7 +44,7 @@
45 {{end}}
46
47 {{if .IsPre}}
48- <li><pre>{{.Value}}</pre></li>
49+ <li><pre>{{.Value}}</pre>
50 {{end}}
51 {{end}}
52 </ul>
+57,
-15
1@@ -12,7 +12,7 @@
2 <h2 class="text-xl">Speculative specification</h2>
3 <dl>
4 <dt>Version</dt>
5- <dd>2022.05.02.dev</dd>
6+ <dd>2022.08.05.dev</dd>
7
8 <dt>Status</dt>
9 <dd>Draft</dd>
10@@ -41,16 +41,15 @@
11
12 <p>
13 The source code for our parser can be found
14- <a href="https://github.com/neurosnap/lists.sh/blob/main/pkg/parser.go">here</a>.
15- </p>
16-
17- <p>
18- The source code for an example list demonstrating all the features can be found
19- <a href="https://github.com/neurosnap/lists-official-blog/blob/main/spec-example.txt">here</a>.
20+ <a href="https://git.sr.ht/~erock/pico/tree/main/item/lists/parser.go">here</a>.
21 </p>
22 </section>
23
24 <section id="parameters">
25+ <h2 class="text-xl">
26+ <a href="#parameters" rel="nofollow noopener">#</a>
27+ Parameters
28+ </h2>
29 <p>
30 As a subtype of the top-level media type "text", "text/plain" inherits the "charset"
31 parameter defined in <a href="https://datatracker.ietf.org/doc/html/rfc2046#section-4.1">RFC 2046</a>.
32@@ -59,6 +58,10 @@
33 </section>
34
35 <section id="line-orientation">
36+ <h2 class="text-xl">
37+ <a href="#line-orientation" rel="nofollow noopener">#</a>
38+ Line orientation
39+ </h2>
40 <p>
41 As mentioned, the text format is line-oriented. Each line of a document has a single
42 "line type". It is possible to unambiguously determine a line's type purely by
43@@ -69,7 +72,10 @@
44 </section>
45
46 <section id="file-extensions">
47- <h2 class="text-xl">File extension</h2>
48+ <h2 class="text-xl">
49+ <a href="#file-extensions" rel="nofollow noopener">#</a>
50+ File extension
51+ </h2>
52 <p>
53 {{.Site.Domain}} only supports the <code>.txt</code> file extension and will
54 ignore all other file extensions.
55@@ -77,7 +83,10 @@
56 </section>
57
58 <section id="list-item">
59- <h2 class="text-xl">List item</h2>
60+ <h2 class="text-xl">
61+ <a href="#list-item" rel="nofollow noopener">#</a>
62+ List item
63+ </h2>
64 <p>
65 List items are separated by newline characters <code>\n</code>.
66 Each list item is on its own line. A list item does not require any special formatting.
67@@ -90,7 +99,10 @@
68 </section>
69
70 <section id="hyperlinks">
71- <h2 class="text-xl">Hyperlinks</h2>
72+ <h2 class="text-xl">
73+ <a href="#hyperlinks" rel="nofollow noopener">#</a>
74+ Hyperlinks
75+ </h2>
76 <p>
77 Hyperlinks are denoted by the prefix <code>=></code>. The following text should then be
78 the hyperlink.
79@@ -100,8 +112,26 @@
80 <pre>=> https://{{.Site.Domain}} microblog for lists</pre>
81 </section>
82
83+ <section id="nested-lists">
84+ <h2 class="text-xl">
85+ <a href="#nested-lists" rel="nofollow noopener">#</a>
86+ Nested lists
87+ </h2>
88+ <p>
89+ Users can create nested lists. Tabbing a list will nest it under the list item
90+ directly above it. Both tab character `\t` or whitespace as tabs are permitted.
91+ </p>
92+ <pre>first item
93+ second item
94+ third item
95+last item</pre>
96+ </section>
97+
98 <section id="images">
99- <h2 class="text-xl">Images</h2>
100+ <h2 class="text-xl">
101+ <a href="#hyperlinks" rel="nofollow noopener">#</a>
102+ Images
103+ </h2>
104 <p>
105 List items can be represented as images by prefixing the line with <code>=<</code>.
106 </p>
107@@ -111,7 +141,10 @@
108 </section>
109
110 <section id="headers">
111- <h2 class="text-xl">Headers</h2>
112+ <h2 class="text-xl">
113+ <a href="#headers" rel="nofollow noopener">#</a>
114+ Headers
115+ </h2>
116 <p>
117 List items can be represented as headers. We support two headers currently. Headers
118 will end the previous list and then create a new one after it. This allows a single
119@@ -122,7 +155,10 @@
120 </section>
121
122 <section id="blockquotes">
123- <h2 class="text-xl">Blockquotes</h2>
124+ <h2 class="text-xl">
125+ <a href="#headers" rel="nofollow noopener">#</a>
126+ Blockquotes
127+ </h2>
128 <p>
129 List items can be represented as blockquotes.
130 </p>
131@@ -130,7 +166,10 @@
132 </section>
133
134 <section id="preformatted">
135- <h2 class="text-xl">Preformatted</h2>
136+ <h2 class="text-xl">
137+ <a href="#preformatted" rel="nofollow noopener">#</a>
138+ Preformatted
139+ </h2>
140 <p>
141 List items can be represented as preformatted text where newline characters are not
142 considered part of new list items. They can be represented by prefixing the line with
143@@ -154,7 +193,10 @@ echo "This will not render properly"```</pre>
144 </section>
145
146 <section id="variables">
147- <h2 class="text-xl">Variables</h2>
148+ <h2 class="text-xl">
149+ <a href="#variables" rel="nofollow noopener">#</a>
150+ Variables
151+ </h2>
152 <p>
153 Variables allow us to store metadata within our system. Variables are list items with
154 key value pairs denoted by <code>=:</code> followed by the key, a whitespace character,
+66,
-44
1@@ -3,15 +3,18 @@ package lists
2 import (
3 "fmt"
4 "html/template"
5+ "regexp"
6 "strings"
7 "time"
8
9 "github.com/araddon/dateparse"
10 )
11
12+var reIndent = regexp.MustCompile(`^[[:blank:]]+`)
13+
14 type ParsedText struct {
15- Items []*ListItem
16- MetaData *MetaData
17+ Items []*ListItem
18+ *MetaData
19 }
20
21 type ListItem struct {
22@@ -25,6 +28,7 @@ type ListItem struct {
23 IsHeaderTwo bool
24 IsImg bool
25 IsPre bool
26+ Indent int
27 }
28
29 type MetaData struct {
30@@ -107,6 +111,63 @@ func KeyAsValue(token *SplitToken) string {
31 return token.Value
32 }
33
34+func parseItem(meta *MetaData, li *ListItem, prevItem *ListItem, pre bool, mod int) (bool, bool, int) {
35+ skip := false
36+
37+ if strings.HasPrefix(li.Value, preToken) {
38+ pre = !pre
39+ if pre {
40+ nextValue := strings.Replace(li.Value, preToken, "", 1)
41+ li.IsPre = true
42+ li.Value = nextValue
43+ } else {
44+ skip = true
45+ }
46+ } else if pre {
47+ nextValue := strings.Replace(li.Value, preToken, "", 1)
48+ prevItem.Value = fmt.Sprintf("%s\n%s", prevItem.Value, nextValue)
49+ skip = true
50+ } else if strings.HasPrefix(li.Value, urlToken) {
51+ li.IsURL = true
52+ split := TextToSplitToken(strings.Replace(li.Value, urlToken, "", 1))
53+ li.URL = template.URL(split.Key)
54+ li.Value = KeyAsValue(split)
55+ } else if strings.HasPrefix(li.Value, blockToken) {
56+ li.IsBlock = true
57+ li.Value = strings.Replace(li.Value, blockToken, "", 1)
58+ } else if strings.HasPrefix(li.Value, imgToken) {
59+ li.IsImg = true
60+ split := TextToSplitToken(strings.Replace(li.Value, imgToken, "", 1))
61+ li.URL = template.URL(split.Key)
62+ li.Value = KeyAsValue(split)
63+ } else if strings.HasPrefix(li.Value, varToken) {
64+ split := TextToSplitToken(strings.Replace(li.Value, varToken, "", 1))
65+ TokenToMetaField(meta, split)
66+ } else if strings.HasPrefix(li.Value, headerTwoToken) {
67+ li.IsHeaderTwo = true
68+ li.Value = strings.Replace(li.Value, headerTwoToken, "", 1)
69+ } else if strings.HasPrefix(li.Value, headerOneToken) {
70+ li.IsHeaderOne = true
71+ li.Value = strings.Replace(li.Value, headerOneToken, "", 1)
72+ } else if reIndent.MatchString(li.Value) {
73+ trim := reIndent.ReplaceAllString(li.Value, "")
74+ old := len(li.Value)
75+ li.Value = trim
76+
77+ pre, skip, _ = parseItem(meta, li, prevItem, pre, mod)
78+ if prevItem.Indent == 0 {
79+ mod = old - len(trim)
80+ li.Indent = 1
81+ } else {
82+ li.Indent = (old - len(trim)) / mod
83+ }
84+ } else {
85+ li.IsText = true
86+ }
87+
88+ return pre, skip, mod
89+}
90+
91 func ParseText(text string) *ParsedText {
92 textItems := SplitByNewline(text)
93 items := []*ListItem{}
94@@ -116,58 +177,19 @@ func ParseText(text string) *ParsedText {
95 }
96 pre := false
97 skip := false
98+ mod := 0
99 var prevItem *ListItem
100
101 for _, t := range textItems {
102- skip = false
103-
104 if len(items) > 0 {
105 prevItem = items[len(items)-1]
106 }
107
108 li := ListItem{
109- Value: strings.Trim(t, " "),
110+ Value: t,
111 }
112
113- if strings.HasPrefix(li.Value, preToken) {
114- pre = !pre
115- if pre {
116- nextValue := strings.Replace(li.Value, preToken, "", 1)
117- li.IsPre = true
118- li.Value = nextValue
119- } else {
120- skip = true
121- }
122- } else if pre {
123- nextValue := strings.Replace(li.Value, preToken, "", 1)
124- prevItem.Value = fmt.Sprintf("%s\n%s", prevItem.Value, nextValue)
125- skip = true
126- } else if strings.HasPrefix(li.Value, urlToken) {
127- li.IsURL = true
128- split := TextToSplitToken(strings.Replace(li.Value, urlToken, "", 1))
129- li.URL = template.URL(split.Key)
130- li.Value = KeyAsValue(split)
131- } else if strings.HasPrefix(li.Value, blockToken) {
132- li.IsBlock = true
133- li.Value = strings.Replace(li.Value, blockToken, "", 1)
134- } else if strings.HasPrefix(li.Value, imgToken) {
135- li.IsImg = true
136- split := TextToSplitToken(strings.Replace(li.Value, imgToken, "", 1))
137- li.URL = template.URL(split.Key)
138- li.Value = KeyAsValue(split)
139- } else if strings.HasPrefix(li.Value, varToken) {
140- split := TextToSplitToken(strings.Replace(li.Value, varToken, "", 1))
141- TokenToMetaField(&meta, split)
142- continue
143- } else if strings.HasPrefix(li.Value, headerTwoToken) {
144- li.IsHeaderTwo = true
145- li.Value = strings.Replace(li.Value, headerTwoToken, "", 1)
146- } else if strings.HasPrefix(li.Value, headerOneToken) {
147- li.IsHeaderOne = true
148- li.Value = strings.Replace(li.Value, headerOneToken, "", 1)
149- } else {
150- li.IsText = true
151- }
152+ pre, skip, mod = parseItem(&meta, &li, prevItem, pre, mod)
153
154 if li.IsText && li.Value == "" {
155 skip = true
1@@ -61,6 +61,24 @@ func ServeFile(file string, contentType string) http.HandlerFunc {
2 }
3 }
4
5+func minus(a, b int) int {
6+ return a - b
7+}
8+
9+func intRange(start, end int) []int {
10+ n := end - start + 1
11+ result := make([]int, n)
12+ for i := 0; i < n; i++ {
13+ result[i] = start + i
14+ }
15+ return result
16+}
17+
18+var funcMap = template.FuncMap{
19+ "minus": minus,
20+ "intRange": intRange,
21+}
22+
23 func RenderTemplate(cfg *ConfigSite, templates []string) (*template.Template, error) {
24 files := make([]string, len(templates))
25 copy(files, templates)
26@@ -71,7 +89,8 @@ func RenderTemplate(cfg *ConfigSite, templates []string) (*template.Template, er
27 cfg.StaticPath("html/base.layout.tmpl"),
28 )
29
30- ts, err := template.ParseFiles(files...)
31+ ts, err := template.New("base").Funcs(funcMap).ParseFiles(files...)
32+ // ts, err := template.ParseFiles(files...)
33 if err != nil {
34 return nil, err
35 }