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 `` 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 `` // 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, "", ClassComment) { return xmlInComment } case s.HasPrefix(0, "", ClassString) { return xmlInCDATA } case s.HasPrefix(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 `` and the other `' { 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 — `` — 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 `&` and `©`. // // 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 == '.' }