From 874ea39ec0f278a48b26a172d877a094d17248eb Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:33:38 -0600 Subject: [PATCH] feat: add XML grammar --- README.md | 4 +- docs/index.html | 17 +- src/grammars/xml.js | 381 ++++++++++++++++++++++++++ test/fixtures/grammar-exhaustive.html | 12 + test/grammar-dependencies.spec.js | 2 + 5 files changed, 413 insertions(+), 3 deletions(-) create mode 100644 src/grammars/xml.js diff --git a/README.md b/README.md index cca0f63..0dc0e54 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ your markup stays clean and editable. - About 2 KiB compressed - No runtime dependencies -- 37 languages, loaded on demand +- 41 languages, loaded on demand - 10 bundled themes - Clean DOM with no token markup - Programmatic, automatic, and web component APIs @@ -191,7 +191,7 @@ MicroLighter includes these grammars: `git-diff`, `go`, `graphql`, `heex`, `html`, `ini`, `java`, `javascript`, `json`, `kotlin`, `lua`, `markdown`, `nginx`, `objective-c`, `perl`, `php`, `powershell`, `python`, `r`, `ruby`, `rust`, `scss`, `sql`, `svelte`, `swift`, `toml`, `tsx`, -`typescript`, `vue`, and `yaml`. +`typescript`, `vue`, `xml`, and `yaml`. Grammars are ES modules and load on demand. diff --git a/docs/index.html b/docs/index.html index 2aeab99..b8194cf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -121,7 +121,7 @@
<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE catalog [
+ <!ENTITY publisher "MicroLighter Press">
+]>
+<!-- Namespaced inventory feed -->
+<catalog xmlns:book="urn:example:books">
+ <book:item id="bk-42" available="true">
+ <title>Syntax & Semantics by &publisher;</title>
+ <summary><![CDATA[Use <tokens> safely.]]></summary>
+ <book:cover href="cover.jpg" />
+ </book:item>
+</catalog>
name: CI
diff --git a/src/grammars/xml.js b/src/grammars/xml.js
new file mode 100644
index 0000000..795a314
--- /dev/null
+++ b/src/grammars/xml.js
@@ -0,0 +1,381 @@
+// Reference: Microsoft VS Code (MIT) - https://github.com/microsoft/vscode/blob/main/extensions/xml/syntaxes/xml.tmLanguage.json
+export default {
+ "scopeName": "text.xml",
+ "patterns": [
+ {
+ "begin": "(<\\?)\\s*([-_a-zA-Z0-9]+)",
+ "captures": {
+ "1": {
+ "name": "punctuation.definition.tag.xml"
+ },
+ "2": {
+ "name": "entity.name.tag.xml"
+ }
+ },
+ "end": "(\\?>)",
+ "name": "meta.tag.preprocessor.xml",
+ "patterns": [
+ {
+ "match": " ([a-zA-Z-]+)",
+ "name": "entity.other.attribute-name.xml"
+ },
+ {
+ "include": "#doublequotedString"
+ },
+ {
+ "include": "#singlequotedString"
+ }
+ ]
+ },
+ {
+ "begin": "()",
+ "name": "meta.tag.sgml.doctype.xml",
+ "patterns": [
+ {
+ "include": "#internalSubset"
+ }
+ ]
+ },
+ {
+ "include": "#comments"
+ },
+ {
+ "begin": "(<)((?:([-_a-zA-Z0-9]+)(:))?([-_a-zA-Z0-9:]+))(?=(\\s[^>]*)?>\\2>)",
+ "beginCaptures": {
+ "1": {
+ "name": "punctuation.definition.tag.xml"
+ },
+ "2": {
+ "name": "entity.name.tag.xml"
+ },
+ "3": {
+ "name": "entity.name.tag.namespace.xml"
+ },
+ "4": {
+ "name": "punctuation.separator.namespace.xml"
+ },
+ "5": {
+ "name": "entity.name.tag.localname.xml"
+ }
+ },
+ "end": "(>)()((?:([-_a-zA-Z0-9]+)(:))?([-_a-zA-Z0-9:]+))(>)",
+ "endCaptures": {
+ "1": {
+ "name": "punctuation.definition.tag.xml"
+ },
+ "2": {
+ "name": "punctuation.definition.tag.xml"
+ },
+ "3": {
+ "name": "entity.name.tag.xml"
+ },
+ "4": {
+ "name": "entity.name.tag.namespace.xml"
+ },
+ "5": {
+ "name": "punctuation.separator.namespace.xml"
+ },
+ "6": {
+ "name": "entity.name.tag.localname.xml"
+ },
+ "7": {
+ "name": "punctuation.definition.tag.xml"
+ }
+ },
+ "name": "meta.tag.no-content.xml",
+ "patterns": [
+ {
+ "include": "#tagStuff"
+ }
+ ]
+ },
+ {
+ "begin": "(?)(?:([-\\w\\.]+)((:)))?([-\\w\\.:]+)",
+ "captures": {
+ "1": {
+ "name": "punctuation.definition.tag.xml"
+ },
+ "2": {
+ "name": "entity.name.tag.namespace.xml"
+ },
+ "3": {
+ "name": "entity.name.tag.xml"
+ },
+ "4": {
+ "name": "punctuation.separator.namespace.xml"
+ },
+ "5": {
+ "name": "entity.name.tag.localname.xml"
+ }
+ },
+ "end": "(/?>)",
+ "name": "meta.tag.xml",
+ "patterns": [
+ {
+ "include": "#tagStuff"
+ }
+ ]
+ },
+ {
+ "include": "#entity"
+ },
+ {
+ "include": "#bare-ampersand"
+ },
+ {
+ "begin": "<%@",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.section.embedded.begin.xml"
+ }
+ },
+ "end": "%>",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.section.embedded.end.xml"
+ }
+ },
+ "name": "source.java-props.embedded.xml",
+ "patterns": [
+ {
+ "match": "page|include|taglib",
+ "name": "keyword.other.page-props.xml"
+ }
+ ]
+ },
+ {
+ "begin": "<%[!=]?(?!--)",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.section.embedded.begin.xml"
+ }
+ },
+ "end": "(?!--)%>",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.section.embedded.end.xml"
+ }
+ },
+ "name": "source.java.embedded.xml",
+ "patterns": [
+ {
+ "include": "source.java"
+ }
+ ]
+ },
+ {
+ "begin": "",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.end.xml"
+ }
+ },
+ "name": "string.unquoted.cdata.xml"
+ }
+ ],
+ "repository": {
+ "EntityDecl": {
+ "begin": "()",
+ "patterns": [
+ {
+ "include": "#doublequotedString"
+ },
+ {
+ "include": "#singlequotedString"
+ }
+ ]
+ },
+ "bare-ampersand": {
+ "match": "&",
+ "name": "invalid.illegal.bad-ampersand.xml"
+ },
+ "doublequotedString": {
+ "begin": "\"",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.begin.xml"
+ }
+ },
+ "end": "\"",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.end.xml"
+ }
+ },
+ "name": "string.quoted.double.xml",
+ "patterns": [
+ {
+ "include": "#entity"
+ },
+ {
+ "include": "#bare-ampersand"
+ }
+ ]
+ },
+ "entity": {
+ "captures": {
+ "1": {
+ "name": "punctuation.definition.constant.xml"
+ },
+ "3": {
+ "name": "punctuation.definition.constant.xml"
+ }
+ },
+ "match": "(&)([:a-zA-Z_][:a-zA-Z0-9_.-]*|#[0-9]+|#x[0-9a-fA-F]+)(;)",
+ "name": "constant.character.entity.xml"
+ },
+ "internalSubset": {
+ "begin": "(\\[)",
+ "captures": {
+ "1": {
+ "name": "punctuation.definition.constant.xml"
+ }
+ },
+ "end": "(\\])",
+ "name": "meta.internalsubset.xml",
+ "patterns": [
+ {
+ "include": "#EntityDecl"
+ },
+ {
+ "include": "#parameterEntity"
+ },
+ {
+ "include": "#comments"
+ }
+ ]
+ },
+ "parameterEntity": {
+ "captures": {
+ "1": {
+ "name": "punctuation.definition.constant.xml"
+ },
+ "3": {
+ "name": "punctuation.definition.constant.xml"
+ }
+ },
+ "match": "(%)([:a-zA-Z_][:a-zA-Z0-9_.-]*)(;)",
+ "name": "constant.character.parameter-entity.xml"
+ },
+ "singlequotedString": {
+ "begin": "'",
+ "beginCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.begin.xml"
+ }
+ },
+ "end": "'",
+ "endCaptures": {
+ "0": {
+ "name": "punctuation.definition.string.end.xml"
+ }
+ },
+ "name": "string.quoted.single.xml",
+ "patterns": [
+ {
+ "include": "#entity"
+ },
+ {
+ "include": "#bare-ampersand"
+ }
+ ]
+ },
+ "tagStuff": {
+ "patterns": [
+ {
+ "captures": {
+ "1": {
+ "name": "entity.other.attribute-name.namespace.xml"
+ },
+ "2": {
+ "name": "entity.other.attribute-name.xml"
+ },
+ "3": {
+ "name": "punctuation.separator.namespace.xml"
+ },
+ "4": {
+ "name": "entity.other.attribute-name.localname.xml"
+ }
+ },
+ "match": "(?:^|\\s+)(?:([-\\w.]+)((:)))?([-\\w.:]+)\\s*="
+ },
+ {
+ "include": "#doublequotedString"
+ },
+ {
+ "include": "#singlequotedString"
+ }
+ ]
+ },
+ "comments": {
+ "patterns": [
+ {
+ "begin": "<%--",
+ "captures": {
+ "0": {
+ "name": "punctuation.definition.comment.xml"
+ },
+ "end": "--%>",
+ "name": "comment.block.xml"
+ }
+ },
+ {
+ "begin": "",
+ "name": "comment.block.xml",
+ "patterns": [
+ {
+ "begin": "--(?!>)",
+ "captures": {
+ "0": {
+ "name": "invalid.illegal.bad-comments-or-CDATA.xml"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+};
diff --git a/test/fixtures/grammar-exhaustive.html b/test/fixtures/grammar-exhaustive.html
index 06d1254..6382e1e 100644
--- a/test/fixtures/grammar-exhaustive.html
+++ b/test/fixtures/grammar-exhaustive.html
@@ -208,6 +208,18 @@
const count = ref(0)
</script>
<button @click="count++">Count: {{ count }}</button>
+ <?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE catalog [
+ <!ENTITY publisher "MicroLighter Press">
+]>
+<!-- Namespaced inventory feed -->
+<catalog xmlns:book="urn:example:books">
+ <book:item id="bk-42" available="true">
+ <title>Syntax & Semantics by &publisher;</title>
+ <summary><![CDATA[Use <tokens> safely.]]></summary>
+ <book:cover href="cover.jpg" />
+ </book:item>
+</catalog>
name: CI
on: [push]
jobs:
diff --git a/test/grammar-dependencies.spec.js b/test/grammar-dependencies.spec.js
index e7dcf03..7d72158 100644
--- a/test/grammar-dependencies.spec.js
+++ b/test/grammar-dependencies.spec.js
@@ -16,6 +16,7 @@ import tsx from "../src/grammars/tsx.js";
import typescript from "../src/grammars/typescript.js";
import svelte from "../src/grammars/svelte.js";
import vue from "../src/grammars/vue.js";
+import xml from "../src/grammars/xml.js";
test("finds external grammar languages in nested rules", () => {
const grammar = {
@@ -64,6 +65,7 @@ test("reports dependencies used by the shipped grammars", () => {
assert.deepEqual([...getExternalLanguages(cpp)], ["c"]);
assert.deepEqual([...getExternalLanguages(tsx)], ["js", "ts"]);
assert.deepEqual([...getExternalLanguages(objectiveC)], ["c"]);
+ assert.deepEqual([...getExternalLanguages(xml)], ["java"]);
assert.deepEqual(heex.dependencies, ["html", "elixir"]);
assert.deepEqual(svelte.dependencies, ["html", "css", "scss", "javascript", "typescript"]);
assert.deepEqual(vue.dependencies, ["html", "css", "scss", "javascript", "typescript"]);