| 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. |
| 6 | package codeview |
| 7 | |
| 8 | import "strings" |
| 9 | |
| 10 | // piece is one run of markup: a tag, a comment, or the text between. |
| 11 | type 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 | |
| 19 | type pieceKind int |
| 20 | |
| 21 | const ( |
| 22 | textPiece pieceKind = iota |
| 23 | tagPiece |
| 24 | commentPiece |
| 25 | ) |
| 26 | |
| 27 | // voidElements open nothing, so they never deepen the indent. |
| 28 | var 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. |
| 36 | func 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. |
| 63 | func 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 | |
| 114 | func isLetter(c byte) bool { return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' } |
| 115 | |
| 116 | func 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. |
| 121 | func 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 | |