| 🛟 Updated. 28d5985 k33g 18h ago | 1 | package syntax |
| 2 | |
| 3 | import "strings" |
| 4 | |
| 5 | // highlightHTML colours an HTML document, one slice of spans per line. |
| 6 | // |
| 7 | // The contents of <script> and <style> are left as plain text rather than |
| 8 | // coloured as JavaScript and CSS. Doing that means following the element to |
| 9 | // its closing tag across lines and mapping another scanner's columns back out, |
| 10 | // and there is no CSS scanner here to reach for anyway. |
| 11 | func highlightHTML(src string) [][]Span { |
| 12 | return ScanLines(src, scanHTMLLine) |
| 13 | } |
| 14 | |
| 15 | // htmlCarry is what a line can leave open. Only a comment spans lines in what |
| 16 | // this scanner recognises. |
| 17 | type htmlCarry uint8 |
| 18 | |
| 19 | const ( |
| 20 | htmlGround htmlCarry = iota |
| 21 | htmlInComment |
| 22 | ) |
| 23 | |
| 24 | // scanHTMLLine returns the spans of one line, and what is left open. |
| 25 | func scanHTMLLine(line []rune, carry htmlCarry) ([]Span, htmlCarry) { |
| 26 | s := &LineScanner{line: line} |
| 27 | |
| 28 | if carry == htmlInComment && !FinishBlockComment(s, "-->", ClassComment) { |
| 29 | return s.spans, htmlInComment |
| 30 | } |
| 31 | |
| 32 | for !s.AtEnd() { |
| 33 | if openComment := stepHTML(s); openComment { |
| 34 | return s.spans, htmlInComment |
| 35 | } |
| 36 | } |
| 37 | return s.spans, htmlGround |
| 38 | } |
| 39 | |
| 40 | // stepHTML recognises whatever starts at the current position, and reports |
| 41 | // whether it left a comment open at the end of the line. |
| 42 | func stepHTML(s *LineScanner) bool { |
| 43 | switch { |
| 44 | case s.HasPrefix(0, "<!--"): |
| 45 | return !OpenBlockComment(s, "<!--", "-->", ClassComment) |
| 46 | case s.HasPrefix(0, "<!"): |
| 47 | takeDoctype(s) |
| 48 | case s.line[s.pos] == '<': |
| 49 | takeTag(s) |
| 50 | case s.line[s.pos] == '&': |
| 51 | takeEntity(s) |
| 52 | default: |
| 53 | s.pos++ |
| 54 | } |
| 55 | return false |
| 56 | } |
| 57 | |
| 58 | // takeDoctype colours <!DOCTYPE html> and the other declarations. |
| 59 | func takeDoctype(s *LineScanner) { |
| 60 | start := s.pos |
| 61 | for !s.AtEnd() && s.line[s.pos] != '>' { |
| 62 | s.pos++ |
| 63 | } |
| 64 | if !s.AtEnd() { |
| 65 | s.pos++ // the closing bracket |
| 66 | } |
| 67 | s.Emit(start, s.pos, ClassKeyword) |
| 68 | } |
| 69 | |
| 70 | // takeEntity colours & and its numeric forms. |
| 71 | // |
| 72 | // A bare ampersand is left alone: it is legal text, and colouring the rest of |
| 73 | // a paragraph after one would be worse than colouring nothing. |
| 74 | func takeEntity(s *LineScanner) { |
| 75 | for at := s.pos + 1; at < len(s.line) && at <= s.pos+entityLimit; at++ { |
| 76 | if s.line[at] == ';' { |
| 77 | s.Take(at-s.pos+1, ClassConstant) |
| 78 | return |
| 79 | } |
| 80 | if !IsWordRune(s.line[at]) && s.line[at] != '#' { |
| 81 | break |
| 82 | } |
| 83 | } |
| 84 | s.pos++ |
| 85 | } |
| 86 | |
| 87 | // entityLimit is how far past an ampersand a semicolon may be and still make |
| 88 | // an entity. The longest named entity in HTML is well under this. |
| 89 | const entityLimit = 32 |
| 90 | |
| 91 | // takeTag colours an element: its brackets and name as a tag, then the |
| 92 | // attributes inside it, up to the closing bracket. |
| 93 | func takeTag(s *LineScanner) { |
| 94 | start := s.pos |
| 95 | s.pos++ // the opening bracket |
| 96 | if !s.AtEnd() && s.line[s.pos] == '/' { |
| 97 | s.pos++ // a closing tag |
| 98 | } |
| 99 | |
| 100 | for !s.AtEnd() && (IsWordRune(s.line[s.pos]) || s.line[s.pos] == '-' || s.line[s.pos] == ':') { |
| 101 | s.pos++ |
| 102 | } |
| 103 | s.Emit(start, s.pos, ClassTag) |
| 104 | |
| 105 | takeAttributes(s) |
| 106 | } |
| 107 | |
| 108 | // takeAttributes colours what is between an element's name and its closing |
| 109 | // bracket. |
| 110 | func takeAttributes(s *LineScanner) { |
| 111 | for !s.AtEnd() { |
| 112 | switch { |
| 113 | case s.line[s.pos] == '>': |
| 114 | s.Take(1, ClassTag) |
| 115 | return |
| 116 | case s.HasPrefix(0, "/>"): |
| 117 | s.Take(2, ClassTag) |
| 118 | return |
| 119 | case s.line[s.pos] == '"' || s.line[s.pos] == '\'': |
| 120 | TakeQuoted(s, s.line[s.pos], ClassString) |
| 121 | case s.line[s.pos] == '=': |
| 122 | s.Take(1, ClassOperator) |
| 123 | case IsWordRune(s.line[s.pos]) || s.line[s.pos] == '-' || s.line[s.pos] == ':' || s.line[s.pos] == '@': |
| 124 | s.TakeWhile(ClassAttribute, isAttributeRune) |
| 125 | default: |
| 126 | s.pos++ |
| 127 | } |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | // attributeExtras are the characters an attribute's name may hold besides |
| 132 | // letters, digits and underscores. |
| 133 | // |
| 134 | // Hyphens, colons, at-signs and dots are in because data-*, xlink:href and the |
| 135 | // framework spellings such as @click and v-bind.prop are all attribute names |
| 136 | // in the wild. |
| 137 | const attributeExtras = "-:@." |
| 138 | |
| 139 | // isAttributeRune reports whether a rune can appear in an attribute's name. |
| 140 | func isAttributeRune(r rune) bool { |
| 141 | return IsWordRune(r) || strings.ContainsRune(attributeExtras, r) |
| 142 | } |