turbo-editors/turbo-corepublic Fork 0
main
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.

🛟 Updated. 28d5985 · on main · k33g · 4h ago
xml.go · 189 lines · 5.2 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
package syntax

// highlightXML colours an XML document, one slice of spans per line.
//
// XML gets its own scanner rather than borrowing HTML's, for one reason that
// matters and two that follow from it. The one that matters is CDATA: the whole
// point of `<![CDATA[ … ]]>` is that its contents are *not* markup, and HTML's
// scanner would colour the tags inside one as tags — which is exactly backwards
// in the files most likely to contain any. The other two are the `<?xml ?>`
// declaration and namespaced names, neither of which HTML has.
func highlightXML(src string) [][]Span {
	return ScanLines(src, scanXMLLine)
}

// xmlCarry is what a line can leave open. Two things can: a comment and a CDATA
// section, and they close on different delimiters.
type xmlCarry uint8

const (
	xmlGround xmlCarry = iota
	xmlInComment
	xmlInCDATA
)

// closer returns the text that ends whatever is open.
func (c xmlCarry) closer() string {
	if c == xmlInCDATA {
		return "]]>"
	}
	return "-->"
}

// class returns the class the open construct is drawn in. A CDATA section is a
// string because that is what it is: text that happens to sit inside markup.
func (c xmlCarry) class() Class {
	if c == xmlInCDATA {
		return ClassString
	}
	return ClassComment
}

// scanXMLLine returns the spans of one line, and what it leaves open.
func scanXMLLine(line []rune, carry xmlCarry) ([]Span, xmlCarry) {
	s := &LineScanner{line: line}

	if carry != xmlGround && !FinishBlockComment(s, carry.closer(), carry.class()) {
		return s.spans, carry
	}

	for !s.AtEnd() {
		if open := stepXML(s); open != xmlGround {
			return s.spans, open
		}
	}
	return s.spans, xmlGround
}

// stepXML colours whatever starts at the current position, and reports anything
// it left open at the end of the line.
func stepXML(s *LineScanner) xmlCarry {
	switch {
	case s.HasPrefix(0, "<!--"):
		if !OpenBlockComment(s, "<!--", "-->", ClassComment) {
			return xmlInComment
		}
	case s.HasPrefix(0, "<![CDATA["):
		if !OpenBlockComment(s, "<![CDATA[", "]]>", ClassString) {
			return xmlInCDATA
		}
	case s.HasPrefix(0, "<?"):
		takeXMLProcessingInstruction(s)
	case s.HasPrefix(0, "<!"):
		takeXMLDeclaration(s)
	case s.Peek(0) == '<':
		takeXMLTag(s)
	case s.Peek(0) == '&':
		takeXMLEntity(s)
	default:
		s.Advance(1)
	}
	return xmlGround
}

// takeXMLProcessingInstruction colours `<?xml version="1.0"?>` and the
// stylesheet instructions that follow it in real documents.
//
// The target and the `?>` are keyword; what is between them is coloured as
// attributes, because that is what it looks like and what a reader is scanning
// for.
func takeXMLProcessingInstruction(s *LineScanner) {
	start := s.Pos()
	s.Advance(2)
	for !s.AtEnd() && isXMLNameRune(s.Peek(0)) {
		s.Advance(1)
	}
	s.Emit(start, s.Pos(), ClassKeyword)

	takeXMLAttributes(s, "?>")

	if s.HasPrefix(0, "?>") {
		s.Take(2, ClassKeyword)
	}
}

// takeXMLDeclaration colours `<!DOCTYPE …>` and the other `<!` forms.
func takeXMLDeclaration(s *LineScanner) {
	start := s.Pos()
	for !s.AtEnd() && s.Peek(0) != '>' {
		s.Advance(1)
	}
	s.Advance(1) // the closing angle bracket
	s.Emit(start, s.Pos(), ClassKeyword)
}

// takeXMLTag colours an element's opening or closing tag, with its attributes.
//
// The name may be namespaced — `<xsl:template>` — and the colon is part of it:
// splitting the prefix from the local name would be two colours for one name.
func takeXMLTag(s *LineScanner) {
	start := s.Pos()
	s.Advance(1)
	if s.Peek(0) == '/' {
		s.Advance(1)
	}
	for !s.AtEnd() && isXMLNameRune(s.Peek(0)) {
		s.Advance(1)
	}
	s.Emit(start, s.Pos(), ClassTag)

	takeXMLAttributes(s, "/>")

	switch {
	case s.HasPrefix(0, "/>"):
		s.Take(2, ClassTag)
	case s.Peek(0) == '>':
		s.Take(1, ClassTag)
	}
}

// takeXMLAttributes colours the name="value" pairs up to a tag's closing
// delimiter, which differs between an element and a processing instruction.
func takeXMLAttributes(s *LineScanner, closing string) {
	for !s.AtEnd() {
		switch r := s.Peek(0); {
		case r == ' ' || r == '\t':
			s.SkipSpaces()
		case s.HasPrefix(0, closing) || r == '>':
			return
		case r == '=':
			s.Take(1, ClassOperator)
		case r == '"' || r == '\'':
			TakeQuoted(s, r, ClassString)
		case isXMLNameRune(r):
			s.TakeWhile(ClassAttribute, isXMLNameRune)
		default:
			s.Advance(1)
		}
	}
}

// takeXMLEntity colours `&amp;` and `&#169;`.
//
// A bare `&` with no semicolon soon after is left alone: it is legal text in
// plenty of documents, and colouring the rest of the line after it would be a
// bigger mistake than missing an entity.
func takeXMLEntity(s *LineScanner) {
	for at := 1; at <= maxEntityLength; at++ {
		switch s.Peek(at) {
		case ';':
			s.Take(at+1, ClassConstant)
			return
		case 0, ' ', '<', '&':
			s.Advance(1)
			return
		}
	}
	s.Advance(1)
}

// maxEntityLength is how far past an `&` a semicolon may sit for the two to be
// read as an entity. The longest named entity in common use is well inside it.
const maxEntityLength = 32

// isXMLNameRune reports whether a rune can appear in an element or attribute
// name. The colon is included because a namespace prefix is part of the name,
// and the dot and dash because XML allows them.
func isXMLNameRune(r rune) bool {
	return IsWordRune(r) || r == ':' || r == '-' || r == '.'
}