repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

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
M lists/html/list.partial.tmpl
+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>
M lists/html/spec.page.tmpl
+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,
M lists/parser.go
+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
M shared/api.go
+20, -1
 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 	}