package syntax
import (
"strings"
"testing"
)
// xmlClassOf returns the class covering the first occurrence of a piece of text
// in an XML document.
func xmlClassOf(t *testing.T, src, want string) Class {
t.Helper()
index := strings.Index(src, want)
if index < 0 {
t.Fatalf("%q does not appear in the source", want)
}
line := strings.Count(src[:index], "\n")
col := index - (strings.LastIndex(src[:index], "\n") + 1)
class, ok := classAt(Highlight(LanguageXML, src), line, col)
if !ok {
t.Fatalf("no span covers %q at line %d column %d", want, line, col)
}
return class
}
func TestXMLColoursItsParts(t *testing.T) {
const src = `
Tove
&
`
tests := map[string]Class{
"": ClassComment,
"\n\n"
spans := Highlight(LanguageXML, src)
for col := 0; col < len(name); col++ {
if class, ok := classAt(spans, 0, col); !ok || class != ClassTag {
t.Fatalf("column %d of %q is %v (covered: %v)", col, name, class, ok)
}
}
}
func TestTheEqualsBetweenAnAttributeAndItsValueIsAnOperator(t *testing.T) {
const src = ``
if got := xmlClassOf(t, src, "="); got != ClassOperator {
t.Errorf("the = is %v, want operator", got)
}
}
func TestASelfClosingTagIsClosed(t *testing.T) {
const src = `
`
spans := Highlight(LanguageXML, src)
if class, ok := classAt(spans, 0, 3); !ok || class != ClassTag {
t.Errorf("the /> is %v (covered: %v), want tag", class, ok)
}
}
func TestCDATAContentsAreNotColouredAsMarkup(t *testing.T) {
// The reason XML has a scanner of its own. Everything between the
// delimiters is text, and a `` inside one is not a tag.
const src = ` & markup ]]>`
spans := Highlight(LanguageXML, src)
start := strings.Index(src, "") + len("]]>")
for col := start; col < end; col++ {
class, ok := classAt(spans, 0, col)
if !ok || class != ClassString {
t.Fatalf("column %d of the CDATA section is %v (covered: %v), want string", col-start, class, ok)
}
}
// And the document carries on as markup afterwards.
if class, ok := classAt(spans, 0, end); !ok || class != ClassTag {
t.Errorf("the tag after the CDATA is %v (covered: %v), want tag", class, ok)
}
}
func TestCDATACarriesAcrossLines(t *testing.T) {
const src = " markup\n]]>\n"
spans := Highlight(LanguageXML, src)
if class, ok := classAt(spans, 1, 2); !ok || class != ClassString {
t.Errorf("the second line of the CDATA is %v (covered: %v), want string", class, ok)
}
if class, ok := classAt(spans, 2, 3); !ok || class != ClassTag {
t.Errorf("the tag after the CDATA is %v (covered: %v), want tag", class, ok)
}
}
func TestACommentCarriesAcrossLines(t *testing.T) {
const src = "\n"
spans := Highlight(LanguageXML, src)
if class, ok := classAt(spans, 1, 0); !ok || class != ClassComment {
t.Errorf("the second line of the comment is %v (covered: %v), want comment", class, ok)
}
if class, ok := classAt(spans, 2, 3); !ok || class != ClassTag {
t.Errorf("the tag after the comment is %v (covered: %v), want tag", class, ok)
}
}
func TestACommentAndACDATAAreClosedByDifferentDelimiters(t *testing.T) {
// A single carried flag would close whichever came first on either, so a
// `-->` inside a CDATA section would end it.
const src = " b\n]]>\n"
spans := Highlight(LanguageXML, src)
if class, ok := classAt(spans, 1, 2); !ok || class != ClassString {
t.Errorf("a --> inside CDATA ended it: line 1 is %v (covered: %v)", class, ok)
}
}
func TestABareAmpersandIsLeftAloneInXML(t *testing.T) {
// It is legal text in plenty of documents, and swallowing the rest of the
// line looking for a semicolon would be worse than missing an entity.
const src = `Tom & Jerry`
spans := Highlight(LanguageXML, src)
if class, ok := classAt(spans, 0, strings.Index(src, "&")); ok && class == ClassConstant {
t.Error("a bare & was coloured as an entity")
}
if class, ok := classAt(spans, 0, strings.Index(src, "")); !ok || class != ClassTag {
t.Errorf("the closing tag is %v (covered: %v); the & swallowed it", class, ok)
}
}
func TestANumericEntityIsAConstant(t *testing.T) {
if got := xmlClassOf(t, "©", "©"); got != ClassConstant {
t.Errorf("a numeric entity is %v, want constant", got)
}
}
func TestXMLIsRecognisedByItsManyExtensions(t *testing.T) {
for _, name := range []string{
"pom.xml", "icon.svg", "schema.xsd", "sheet.xsl", "sheet.xslt",
"Info.plist", "app.csproj",
} {
t.Run(name, func(t *testing.T) {
if got := LanguageOf(name, ""); got != LanguageXML {
t.Errorf("LanguageOf(%q) = %q, want xml", name, got)
}
})
}
}
func TestHTMLIsStillHTML(t *testing.T) {
// Adding XML must not take the .html extension with it.
for _, name := range []string{"index.html", "page.htm"} {
if got := LanguageOf(name, ""); got != LanguageHTML {
t.Errorf("LanguageOf(%q) = %q, want html", name, got)
}
}
}
func TestXMLReturnsOneEntryPerLine(t *testing.T) {
tests := map[string]int{
"": 1,
"": 1,
"\n": 2,
"\n \n\n": 4,
}
for src, want := range tests {
if got := len(Highlight(LanguageXML, src)); got != want {
t.Errorf("Highlight(%q) returned %d lines, want %d", src, got, want)
}
}
}
func TestBrokenXMLIsStillColoured(t *testing.T) {
for _, src := range []string{"\n\n&\n"
for line, onLine := range Highlight(LanguageXML, src) {
previousEnd := 0
for _, span := range onLine {
if span.Start < previousEnd {
t.Errorf("line %d: span %+v starts before the previous one ended at %d", line, span, previousEnd)
}
previousEnd = span.End
}
}
}