rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1// Package codeview lays out and colours the source a design-system
2// gallery shows in its HTML and Template tabs. It knows nothing about
3// the gallery: it takes HTML or template source and gives back the same
4// text, broken into lines or marked up for colour, so that the bytes a
5// reader copies are the bytes the source holds.
6package codeview
7
8import "strings"
9
10// piece is one run of markup: a tag, a comment, or the text between.
11type piece struct {
12 text string
13 kind pieceKind
14 name string // lower-case element name, for a tag
15 end bool // a closing tag
16 void bool // opens no element: a void element or a self-closed tag
17}
18
19type pieceKind int
20
21const (
22 textPiece pieceKind = iota
23 tagPiece
24 commentPiece
25)
26
27// voidElements open nothing, so they never deepen the indent.
28var voidElements = map[string]bool{
29 "area": true, "base": true, "br": true, "col": true, "embed": true, "hr": true, "img": true,
30 "input": true, "link": true, "meta": true, "source": true, "track": true, "wbr": true,
31}
32
33// tagEnd returns the index just past the tag that starts at s[i], or -1
34// when s[i] does not open one. A '>' inside a quoted attribute value
35// does not close the tag: data-tip="a > b" is one tag and not two.
36func tagEnd(s string, i int) int {
37 j := i + 1
38 if j < len(s) && s[j] == '/' {
39 j++
40 }
41 if j >= len(s) || !isLetter(s[j]) {
42 return -1
43 }
44 var quote byte
45 for ; j < len(s); j++ {
46 c := s[j]
47 switch {
48 case quote != 0:
49 if c == quote {
50 quote = 0
51 }
52 case c == '"' || c == '\'':
53 quote = c
54 case c == '>':
55 return j + 1
56 }
57 }
58 return -1
59}
60
61// pieces splits markup into tags, comments and text. A '<' that opens
62// no tag ("a < b") stays text.
63func pieces(s string) []piece {
64 var out []piece
65 text := 0
66 flush := func(at int) {
67 if at > text {
68 out = append(out, piece{text: s[text:at]})
69 }
70 }
71 for i := 0; i < len(s); {
72 if s[i] != '<' {
73 i++
74 continue
75 }
76 if strings.HasPrefix(s[i:], "<!--") {
77 end := strings.Index(s[i+4:], "-->")
78 if end < 0 {
79 break
80 }
81 flush(i)
82 j := i + 4 + end + 3
83 out = append(out, piece{text: s[i:j], kind: commentPiece})
84 i, text = j, j
85 continue
86 }
87 j := tagEnd(s, i)
88 if j < 0 {
89 i++
90 continue
91 }
92 flush(i)
93 tag := s[i:j]
94 closing := strings.HasPrefix(tag, "</")
95 n := 1
96 if closing {
97 n = 2
98 }
99 k := n
100 for k < len(tag) && !isSpace(tag[k]) && tag[k] != '>' && tag[k] != '/' {
101 k++
102 }
103 name := strings.ToLower(tag[n:k])
104 out = append(out, piece{
105 text: tag, kind: tagPiece, name: name, end: closing,
106 void: !closing && (voidElements[name] || strings.HasSuffix(tag, "/>")),
107 })
108 i, text = j, j
109 }
110 flush(len(s))
111 return out
112}
113
114func isLetter(c byte) bool { return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' }
115
116func isSpace(c byte) bool { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' }
117
118// attrsOf reads a start tag's attributes: names lower-cased, values
119// unquoted, a bare attribute present with an empty value, and spaces
120// around an = allowed, as HTML allows them.
121func attrsOf(tag string) map[string]string {
122 out := map[string]string{}
123 i := 1
124 for i < len(tag) && !isSpace(tag[i]) && tag[i] != '>' && tag[i] != '/' {
125 i++
126 }
127 for i < len(tag) {
128 for i < len(tag) && (isSpace(tag[i]) || tag[i] == '/') {
129 i++
130 }
131 if i >= len(tag) || tag[i] == '>' {
132 break
133 }
134 start := i
135 for i < len(tag) && !isSpace(tag[i]) && tag[i] != '=' && tag[i] != '>' && tag[i] != '/' {
136 i++
137 }
138 name := strings.ToLower(tag[start:i])
139 value := ""
140 // HTML allows whitespace either side of the =: style = "…" is a
141 // style. Without an = after the space, the name was a bare
142 // attribute and the space starts the next one.
143 j := i
144 for j < len(tag) && isSpace(tag[j]) {
145 j++
146 }
147 if j < len(tag) && tag[j] == '=' {
148 i = j + 1
149 for i < len(tag) && isSpace(tag[i]) {
150 i++
151 }
152 if i < len(tag) && (tag[i] == '"' || tag[i] == '\'') {
153 q := tag[i]
154 end := strings.IndexByte(tag[i+1:], q)
155 if end < 0 {
156 end = len(tag) - i - 1
157 }
158 value = tag[i+1 : i+1+end]
159 i += end + 2
160 } else {
161 start := i
162 for i < len(tag) && !isSpace(tag[i]) && tag[i] != '>' {
163 i++
164 }
165 value = tag[start:i]
166 }
167 }
168 if name != "" {
169 out[name] = value
170 }
171 }
172 return out
173}
174