turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

xml.go · 189 lines · 5.2 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 14h ago1package syntax
2
3// highlightXML colours an XML document, one slice of spans per line.
4//
5// XML gets its own scanner rather than borrowing HTML's, for one reason that
6// matters and two that follow from it. The one that matters is CDATA: the whole
7// point of `<![CDATA[ … ]]>` is that its contents are *not* markup, and HTML's
8// scanner would colour the tags inside one as tags — which is exactly backwards
9// in the files most likely to contain any. The other two are the `<?xml ?>`
10// declaration and namespaced names, neither of which HTML has.
11func highlightXML(src string) [][]Span {
12 return ScanLines(src, scanXMLLine)
13}
14
15// xmlCarry is what a line can leave open. Two things can: a comment and a CDATA
16// section, and they close on different delimiters.
17type xmlCarry uint8
18
19const (
20 xmlGround xmlCarry = iota
21 xmlInComment
22 xmlInCDATA
23)
24
25// closer returns the text that ends whatever is open.
26func (c xmlCarry) closer() string {
27 if c == xmlInCDATA {
28 return "]]>"
29 }
30 return "-->"
31}
32
33// class returns the class the open construct is drawn in. A CDATA section is a
34// string because that is what it is: text that happens to sit inside markup.
35func (c xmlCarry) class() Class {
36 if c == xmlInCDATA {
37 return ClassString
38 }
39 return ClassComment
40}
41
42// scanXMLLine returns the spans of one line, and what it leaves open.
43func scanXMLLine(line []rune, carry xmlCarry) ([]Span, xmlCarry) {
44 s := &LineScanner{line: line}
45
46 if carry != xmlGround && !FinishBlockComment(s, carry.closer(), carry.class()) {
47 return s.spans, carry
48 }
49
50 for !s.AtEnd() {
51 if open := stepXML(s); open != xmlGround {
52 return s.spans, open
53 }
54 }
55 return s.spans, xmlGround
56}
57
58// stepXML colours whatever starts at the current position, and reports anything
59// it left open at the end of the line.
60func stepXML(s *LineScanner) xmlCarry {
61 switch {
62 case s.HasPrefix(0, "<!--"):
63 if !OpenBlockComment(s, "<!--", "-->", ClassComment) {
64 return xmlInComment
65 }
66 case s.HasPrefix(0, "<![CDATA["):
67 if !OpenBlockComment(s, "<![CDATA[", "]]>", ClassString) {
68 return xmlInCDATA
69 }
70 case s.HasPrefix(0, "<?"):
71 takeXMLProcessingInstruction(s)
72 case s.HasPrefix(0, "<!"):
73 takeXMLDeclaration(s)
74 case s.Peek(0) == '<':
75 takeXMLTag(s)
76 case s.Peek(0) == '&':
77 takeXMLEntity(s)
78 default:
79 s.Advance(1)
80 }
81 return xmlGround
82}
83
84// takeXMLProcessingInstruction colours `<?xml version="1.0"?>` and the
85// stylesheet instructions that follow it in real documents.
86//
87// The target and the `?>` are keyword; what is between them is coloured as
88// attributes, because that is what it looks like and what a reader is scanning
89// for.
90func takeXMLProcessingInstruction(s *LineScanner) {
91 start := s.Pos()
92 s.Advance(2)
93 for !s.AtEnd() && isXMLNameRune(s.Peek(0)) {
94 s.Advance(1)
95 }
96 s.Emit(start, s.Pos(), ClassKeyword)
97
98 takeXMLAttributes(s, "?>")
99
100 if s.HasPrefix(0, "?>") {
101 s.Take(2, ClassKeyword)
102 }
103}
104
105// takeXMLDeclaration colours `<!DOCTYPE …>` and the other `<!` forms.
106func takeXMLDeclaration(s *LineScanner) {
107 start := s.Pos()
108 for !s.AtEnd() && s.Peek(0) != '>' {
109 s.Advance(1)
110 }
111 s.Advance(1) // the closing angle bracket
112 s.Emit(start, s.Pos(), ClassKeyword)
113}
114
115// takeXMLTag colours an element's opening or closing tag, with its attributes.
116//
117// The name may be namespaced — `<xsl:template>` — and the colon is part of it:
118// splitting the prefix from the local name would be two colours for one name.
119func takeXMLTag(s *LineScanner) {
120 start := s.Pos()
121 s.Advance(1)
122 if s.Peek(0) == '/' {
123 s.Advance(1)
124 }
125 for !s.AtEnd() && isXMLNameRune(s.Peek(0)) {
126 s.Advance(1)
127 }
128 s.Emit(start, s.Pos(), ClassTag)
129
130 takeXMLAttributes(s, "/>")
131
132 switch {
133 case s.HasPrefix(0, "/>"):
134 s.Take(2, ClassTag)
135 case s.Peek(0) == '>':
136 s.Take(1, ClassTag)
137 }
138}
139
140// takeXMLAttributes colours the name="value" pairs up to a tag's closing
141// delimiter, which differs between an element and a processing instruction.
142func takeXMLAttributes(s *LineScanner, closing string) {
143 for !s.AtEnd() {
144 switch r := s.Peek(0); {
145 case r == ' ' || r == '\t':
146 s.SkipSpaces()
147 case s.HasPrefix(0, closing) || r == '>':
148 return
149 case r == '=':
150 s.Take(1, ClassOperator)
151 case r == '"' || r == '\'':
152 TakeQuoted(s, r, ClassString)
153 case isXMLNameRune(r):
154 s.TakeWhile(ClassAttribute, isXMLNameRune)
155 default:
156 s.Advance(1)
157 }
158 }
159}
160
161// takeXMLEntity colours `&amp;` and `&#169;`.
162//
163// A bare `&` with no semicolon soon after is left alone: it is legal text in
164// plenty of documents, and colouring the rest of the line after it would be a
165// bigger mistake than missing an entity.
166func takeXMLEntity(s *LineScanner) {
167 for at := 1; at <= maxEntityLength; at++ {
168 switch s.Peek(at) {
169 case ';':
170 s.Take(at+1, ClassConstant)
171 return
172 case 0, ' ', '<', '&':
173 s.Advance(1)
174 return
175 }
176 }
177 s.Advance(1)
178}
179
180// maxEntityLength is how far past an `&` a semicolon may sit for the two to be
181// read as an entity. The longest named entity in common use is well inside it.
182const maxEntityLength = 32
183
184// isXMLNameRune reports whether a rune can appear in an element or attribute
185// name. The colon is included because a namespace prefix is part of the name,
186// and the dot and dash because XML allows them.
187func isXMLNameRune(r rune) bool {
188 return IsWordRune(r) || r == ':' || r == '-' || r == '.'
189}