diff --git a/.eslintrc.js b/.eslintrc.js
index 70db9953..3ce84430 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -368,5 +368,29 @@ module.exports = {
"@typescript-eslint/no-unused-expressions": "off",
},
},
+ {
+ // xml-tools was integrated with these rules relaxed for the migrated code.
+ // TODO: clean up violations and tighten incrementally.
+ files: ["projects/xml-tools/**"],
+ rules: {
+ "eslint-comments/require-description": "off",
+ // chevrotain-generated parser sources use generator syntax without yield
+ // and switch/case lexical declarations.
+ "require-yield": "off",
+ "no-case-declarations": "off",
+ "no-useless-escape": "off",
+ },
+ },
+ {
+ // Additional TypeScript rules for xml-tools packages (api.d.ts type surfaces).
+ // Legacy code migrated as-is; relaxed to match historical behaviour.
+ // TODO: clean up violations and tighten incrementally.
+ files: ["projects/xml-tools/**/*.ts"],
+ rules: {
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-redundant-type-constituents": "off",
+ },
+ },
],
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 67254903..38a2fa44 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2156,6 +2156,180 @@ importers:
specifier: ^4.2.3
version: 4.9.5
+ projects/xml-tools/packages/ast:
+ dependencies:
+ '@xml-tools/common':
+ specifier: workspace:*
+ version: link:../common
+ '@xml-tools/parser':
+ specifier: workspace:*
+ version: link:../parser
+ chevrotain:
+ specifier: 7.1.1
+ version: 7.1.1
+ lodash:
+ specifier: 4.17.23
+ version: 4.17.23
+ devDependencies:
+ klaw-sync:
+ specifier: 6.0.0
+ version: 6.0.0
+
+ projects/xml-tools/packages/ast-position:
+ dependencies:
+ '@xml-tools/ast':
+ specifier: workspace:*
+ version: link:../ast
+ devDependencies:
+ '@xml-tools/parser':
+ specifier: workspace:*
+ version: link:../parser
+
+ projects/xml-tools/packages/common:
+ dependencies:
+ chevrotain:
+ specifier: 7.1.1
+ version: 7.1.1
+ lodash:
+ specifier: 4.17.23
+ version: 4.17.23
+ devDependencies:
+ '@xml-tools/parser':
+ specifier: workspace:*
+ version: link:../parser
+
+ projects/xml-tools/packages/constraints:
+ dependencies:
+ '@xml-tools/validation':
+ specifier: workspace:*
+ version: link:../validation
+ lodash:
+ specifier: 4.17.23
+ version: 4.17.23
+ devDependencies:
+ '@xml-tools/ast':
+ specifier: workspace:*
+ version: link:../ast
+
+ projects/xml-tools/packages/content-assist:
+ dependencies:
+ '@xml-tools/ast':
+ specifier: workspace:*
+ version: link:../ast
+ '@xml-tools/common':
+ specifier: workspace:*
+ version: link:../common
+ '@xml-tools/parser':
+ specifier: workspace:*
+ version: link:../parser
+ chevrotain:
+ specifier: 7.1.1
+ version: 7.1.1
+ lodash:
+ specifier: 4.17.23
+ version: 4.17.23
+
+ projects/xml-tools/packages/language-server:
+ dependencies:
+ '@xml-tools/ast':
+ specifier: workspace:*
+ version: link:../ast
+ '@xml-tools/constraints':
+ specifier: workspace:*
+ version: link:../constraints
+ '@xml-tools/parser':
+ specifier: workspace:*
+ version: link:../parser
+ lodash:
+ specifier: 4.17.23
+ version: 4.17.23
+ vscode-languageserver:
+ specifier: 7.0.0
+ version: 7.0.0
+ vscode-languageserver-textdocument:
+ specifier: 1.0.1
+ version: 1.0.1
+ devDependencies:
+ chevrotain:
+ specifier: ^7.0.1
+ version: 7.1.1
+ vscode-languageserver-types:
+ specifier: 3.16.0
+ version: 3.16.0
+
+ projects/xml-tools/packages/parser:
+ dependencies:
+ chevrotain:
+ specifier: 7.1.1
+ version: 7.1.1
+ devDependencies:
+ klaw-sync:
+ specifier: 6.0.0
+ version: 6.0.0
+
+ projects/xml-tools/packages/simple-schema:
+ dependencies:
+ '@xml-tools/ast':
+ specifier: workspace:*
+ version: link:../ast
+ '@xml-tools/common':
+ specifier: workspace:*
+ version: link:../common
+ '@xml-tools/content-assist':
+ specifier: workspace:*
+ version: link:../content-assist
+ lodash:
+ specifier: 4.17.23
+ version: 4.17.23
+ devDependencies:
+ '@xml-tools/parser':
+ specifier: workspace:*
+ version: link:../parser
+ '@xml-tools/validation':
+ specifier: workspace:*
+ version: link:../validation
+
+ projects/xml-tools/packages/validation:
+ dependencies:
+ '@xml-tools/ast':
+ specifier: workspace:*
+ version: link:../ast
+ lodash:
+ specifier: 4.17.23
+ version: 4.17.23
+ devDependencies:
+ '@xml-tools/parser':
+ specifier: workspace:*
+ version: link:../parser
+
+ projects/xml-tools/packages/xml-toolkit:
+ dependencies:
+ '@xml-tools/language-server':
+ specifier: workspace:*
+ version: link:../language-server
+ vscode-languageclient:
+ specifier: 6.1.3
+ version: 6.1.3
+ devDependencies:
+ '@types/vscode':
+ specifier: 1.56.0
+ version: 1.56.0
+ '@vscode/test-electron':
+ specifier: ^2.4.0
+ version: 2.5.2
+ deep-equal-in-any-order:
+ specifier: 1.0.28
+ version: 1.0.28
+ lodash:
+ specifier: 4.17.23
+ version: 4.17.23
+ proxyquire:
+ specifier: 2.1.3
+ version: 2.1.3
+ vsce:
+ specifier: 1.84.0
+ version: 1.84.0
+
projects/yeoman-ui:
devDependencies:
null-loader:
@@ -6049,6 +6223,9 @@ packages:
'@types/vscode@1.52.0':
resolution: {integrity: sha512-Kt3bvWzAvvF/WH9YEcrCICDp0Z7aHhJGhLJ1BxeyNP6yRjonWqWnAIh35/pXAjswAnWOABrYlF7SwXR9+1nnLA==}
+ '@types/vscode@1.56.0':
+ resolution: {integrity: sha512-Q5VmQxOx+L1Y6lIJiGcJzwcyV3pQo/eiW8P+7sNLhFI16tJCwtua2DLjHRcpjbCLNVYpQM73kzfFo1Z0HyP9eQ==}
+
'@types/vscode@1.75.0':
resolution: {integrity: sha512-SAr0PoOhJS6FUq5LjNr8C/StBKALZwDVm3+U4pjF/3iYkt3GioJOPV/oB1Sf1l7lROe4TgrMyL5N1yaEgTWycw==}
@@ -6255,6 +6432,10 @@ packages:
'@vscode-logging/wrapper@2.0.9':
resolution: {integrity: sha512-kTx73Ev1O85oS51s6CnIep/F02uwTGrpuUZMi5/uMaxcU8AdzYWpHVeD1T1BuvInaQhHdADzVnF2gpehKYUbog==}
+ '@vscode/test-electron@2.5.2':
+ resolution: {integrity: sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==}
+ engines: {node: '>=16'}
+
'@vscode/vsce-sign-alpine-arm64@2.0.5':
resolution: {integrity: sha512-XVmnF40APwRPXSLYA28Ye+qWxB25KhSVpF2eZVtVOs6g7fkpOxsVnpRU1Bz2xG4ySI79IRuapDJoAQFkoOgfdQ==}
cpu: [arm64]
@@ -7182,6 +7363,9 @@ packages:
azure-devops-node-api@12.5.0:
resolution: {integrity: sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==}
+ azure-devops-node-api@7.2.0:
+ resolution: {integrity: sha512-pMfGJ6gAQ7LRKTHgiRF+8iaUUeGAI0c8puLaqHLc7B8AR7W6GJLozK9RFeUHFjEGybC9/EB3r67WPd7e46zQ8w==}
+
b4a@1.8.1:
resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}
peerDependencies:
@@ -7724,6 +7908,9 @@ packages:
resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==}
engines: {node: '>=20.18.1'}
+ chevrotain@7.1.1:
+ resolution: {integrity: sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==}
+
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -8632,6 +8819,9 @@ packages:
resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==}
engines: {node: '>=6'}
+ deep-equal-in-any-order@1.0.28:
+ resolution: {integrity: sha512-qq3jffpGmAG9kGpZGKusjRwoGxmFgIqNW076HQmV9rNdrFsgTcpuCyp6dBhzdVCWgQDkgRmvZLYAilV4u2BsfQ==}
+
deep-equal-in-any-order@1.1.20:
resolution: {integrity: sha512-GTpQxcQx28KvV6ChrHb4AcL5z+eFEymtoSUaWe+FEakEpZ/66jTvz52xLUym/sK4Pn1sbYLsCV5rEURDeiFl3w==}
@@ -8962,6 +9152,9 @@ packages:
resolution: {integrity: sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==}
engines: {node: '>=10'}
+ emoji-regex@10.6.0:
+ resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
+
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -10449,6 +10642,9 @@ packages:
engines: {node: '>=12.0.0'}
hasBin: true
+ immediate@3.0.6:
+ resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
+
immutable@4.3.8:
resolution: {integrity: sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==}
@@ -10838,6 +11034,10 @@ packages:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
+ is-unicode-supported@1.3.0:
+ resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
+ engines: {node: '>=12'}
+
is-unicode-supported@2.1.0:
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
engines: {node: '>=18'}
@@ -11495,6 +11695,9 @@ packages:
resolution: {integrity: sha512-4Dj8Rf+fQ+/Pn7C5qeEX02op1WfOss3PKTE9Nsop3Dx+6UPxlm1dr/og7o2cRa5hNN07CACr4NFzRLtj/rjWog==}
engines: {'0': node >=0.6.0}
+ jszip@3.10.2:
+ resolution: {integrity: sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==}
+
just-diff-apply@5.5.0:
resolution: {integrity: sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==}
@@ -11566,6 +11769,9 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
+ klaw-sync@6.0.0:
+ resolution: {integrity: sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==}
+
kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
@@ -11624,6 +11830,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
+ lie@3.3.0:
+ resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
+
lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@@ -11786,6 +11995,9 @@ packages:
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+ lodash@4.17.23:
+ resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
+
lodash@4.18.1:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
@@ -11801,6 +12013,10 @@ packages:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
+ log-symbols@6.0.0:
+ resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
+ engines: {node: '>=18'}
+
log-symbols@7.0.1:
resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==}
engines: {node: '>=18'}
@@ -12773,6 +12989,10 @@ packages:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
+ ora@8.2.0:
+ resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==}
+ engines: {node: '>=18'}
+
ora@9.4.1:
resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==}
engines: {node: '>=20'}
@@ -12785,6 +13005,9 @@ packages:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
+ os@0.1.1:
+ resolution: {integrity: sha512-jg06S2xr5De63mLjZVJDf3/k37tpjppr2LR7MUOsxv8XuUCVpCnvbCksXCBcB5gQqQf/K0+87WGTRlAj5q7r1A==}
+
osenv@0.1.5:
resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==}
deprecated: This package is no longer supported.
@@ -12949,6 +13172,9 @@ packages:
resolution: {integrity: sha512-NWrGqneZImDdcMU/7vMcAOo1bIi5h/pmpJqe7/jdsy85BA/s5MSaU/KlpxwW/IVPmIwBcq2uKPrBWWhEWhtxag==}
engines: {node: '>=8'}
+ pako@1.0.11:
+ resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
+
param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
@@ -13779,6 +14005,9 @@ packages:
resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==}
engines: {node: '>=0.10.0'}
+ regexp-to-ast@0.5.0:
+ resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==}
+
regexp.prototype.flags@1.5.4:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
@@ -14451,6 +14680,9 @@ packages:
resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
+ sort-any@1.2.9:
+ resolution: {integrity: sha512-6LEFNVLeWrbCpFxhS+KdZypqY4XrtOA1dY1vQSacR91SJ/sXKUrR1wK7AUgLOXlLVXnaws3NcZ8lZPMdfkhlHw==}
+
sort-any@2.0.0:
resolution: {integrity: sha512-T9JoiDewQEmWcnmPn/s9h/PH9t3d/LSWi0RgVmXSuDYeZXTZOZ1/wrK2PHaptuR1VXe3clLLt0pD6sgVOwjNEA==}
@@ -14619,6 +14851,10 @@ packages:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
+ stdin-discarder@0.2.2:
+ resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==}
+ engines: {node: '>=18'}
+
stdin-discarder@0.3.2:
resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==}
engines: {node: '>=18'}
@@ -14675,6 +14911,10 @@ packages:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
+ string-width@7.2.0:
+ resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
+ engines: {node: '>=18'}
+
string-width@8.2.1:
resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==}
engines: {node: '>=20'}
@@ -14999,6 +15239,10 @@ packages:
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
hasBin: true
+ tmp@0.0.29:
+ resolution: {integrity: sha512-89PTqMWGDva+GqClOqBV9s3SMh7MA3Mq0pJUdAoHuF65YoE7O0LermaZkVfT5/Ngfo18H4eYiyG7zKOtnEbxsw==}
+ engines: {node: '>=0.4.0'}
+
tmp@0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
@@ -15262,6 +15506,10 @@ packages:
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
+ tunnel@0.0.4:
+ resolution: {integrity: sha512-o9QYRJN5WgS8oCtqvwzzcfnzaTnDPr7HpUsQdSXscTyzXbjvl4wSHPTUKOKzEaDeQvOuyRtt3ui+ujM7x7TReQ==}
+ engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
+
tunnel@0.0.6:
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
@@ -15350,6 +15598,9 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'}
+ typed-rest-client@1.2.0:
+ resolution: {integrity: sha512-FrUshzZ1yxH8YwGR29PWWnfksLEILbWJydU7zfIRkyH7kAEzB62uMAl2WY6EyolWpLpVHeJGgQm45/MaruaHpw==}
+
typed-rest-client@1.8.6:
resolution: {integrity: sha512-xcQpTEAJw2DP7GqVNECh4dD+riS+C1qndXLfBCJ3xk0kqprtGN491P5KlmrDbKdtuW8NEcP/5ChxiJI3S9WYTA==}
@@ -15388,6 +15639,9 @@ packages:
underscore@1.13.1:
resolution: {integrity: sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==}
+ underscore@1.8.3:
+ resolution: {integrity: sha512-5WsVTFcH1ut/kkhAaHf4PVgI8c7++GiVcpCGxPouI6ZVjsqPnSDf8h/8HtVqc0t4fzRXwnMK70EcZeAs3PIddg==}
+
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
@@ -15807,10 +16061,37 @@ packages:
deprecated: vsce has been renamed to @vscode/vsce. Install using @vscode/vsce instead.
hasBin: true
+ vsce@1.84.0:
+ resolution: {integrity: sha512-mRJDTMC/1GO7byz6dleY28CZo97cM1p0nU3EEMT5bGMpi5Yga3dKdgTvmwgSQF5BYWozNSNSqN1zih5SYzMZjA==}
+ engines: {node: '>= 10'}
+ deprecated: vsce has been renamed to @vscode/vsce. Install using @vscode/vsce instead.
+ hasBin: true
+
vscode-jsonrpc@4.0.0:
resolution: {integrity: sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg==}
engines: {node: '>=8.0.0 || >=10.0.0'}
+ vscode-jsonrpc@6.0.0:
+ resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==}
+ engines: {node: '>=8.0.0 || >=10.0.0'}
+
+ vscode-jsonrpc@9.0.2:
+ resolution: {integrity: sha512-SbQSV9yRemARxeXw6LU5sS6Zq0e9/DgCCX5yelH263ZQWukbTk8EF8fjTrr1dziasf4GwlJbvTwFnTrnQFWZXQ==}
+ engines: {node: '>=14.0.0'}
+
+ vscode-languageclient@6.1.3:
+ resolution: {integrity: sha512-YciJxk08iU5LmWu7j5dUt9/1OLjokKET6rME3cI4BRpiF6HZlusm2ZwPt0MYJ0lV5y43sZsQHhyon2xBg4ZJVA==}
+ engines: {vscode: ^1.41.0}
+
+ vscode-languageserver-protocol@3.16.0:
+ resolution: {integrity: sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==}
+
+ vscode-languageserver-protocol@3.18.3:
+ resolution: {integrity: sha512-DF49+WeV5py4zO5hhobp60jjsDSK0lAqA0OuKBLBvp423HPWQcCbhZz3JgyfIewsEz2f8U+X75xNIFHdiXZm2w==}
+
+ vscode-languageserver-textdocument@1.0.1:
+ resolution: {integrity: sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA==}
+
vscode-languageserver-textdocument@1.0.4:
resolution: {integrity: sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ==}
@@ -15820,6 +16101,13 @@ packages:
vscode-languageserver-types@3.17.5:
resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
+ vscode-languageserver-types@3.18.3:
+ resolution: {integrity: sha512-XIlzJ7Qp/jzSI1ds7/FwPAWrPeTZA7pAtlW4hdJ1J6xXWJL6dR9QYnDhJOdLzdKhUQ5Mm6mvUMw+3DcOQQasPw==}
+
+ vscode-languageserver@7.0.0:
+ resolution: {integrity: sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==}
+ hasBin: true
+
vscode-test@1.5.2:
resolution: {integrity: sha512-x9PVfKxF6EInH9iSFGQi0V8H5zIW1fC7RAer6yNQR6sy3WyOwlWkuT3I+wf75xW/cO53hxMi1aj/EvqQfDFOAg==}
engines: {node: '>=8.9.3'}
@@ -21229,6 +21517,8 @@ snapshots:
'@types/vscode@1.52.0': {}
+ '@types/vscode@1.56.0': {}
+
'@types/vscode@1.75.0': {}
'@types/webpack-env@1.18.8': {}
@@ -21548,6 +21838,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@vscode/test-electron@2.5.2':
+ dependencies:
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ jszip: 3.10.2
+ ora: 8.2.0
+ semver: 7.6.3
+ transitivePeerDependencies:
+ - supports-color
+
'@vscode/vsce-sign-alpine-arm64@2.0.5':
optional: true
@@ -23123,6 +23423,13 @@ snapshots:
tunnel: 0.0.6
typed-rest-client: 1.8.6
+ azure-devops-node-api@7.2.0:
+ dependencies:
+ os: 0.1.1
+ tunnel: 0.0.4
+ typed-rest-client: 1.2.0
+ underscore: 1.8.3
+
b4a@1.8.1: {}
babel-code-frame@6.26.0:
@@ -23897,6 +24204,10 @@ snapshots:
undici: 7.25.0
whatwg-mimetype: 4.0.0
+ chevrotain@7.1.1:
+ dependencies:
+ regexp-to-ast: 0.5.0
+
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@@ -24671,6 +24982,11 @@ snapshots:
dependencies:
type-detect: 4.1.0
+ deep-equal-in-any-order@1.0.28:
+ dependencies:
+ lodash.mapvalues: 4.6.0
+ sort-any: 1.2.9
+
deep-equal-in-any-order@1.1.20:
dependencies:
lodash.mapvalues: 4.6.0
@@ -24987,6 +25303,8 @@ snapshots:
emittery@0.8.1: {}
+ emoji-regex@10.6.0: {}
+
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
@@ -26979,6 +27297,8 @@ snapshots:
dependencies:
queue: 6.0.2
+ immediate@3.0.6: {}
+
immutable@4.3.8: {}
immutable@5.1.9: {}
@@ -27346,6 +27666,8 @@ snapshots:
is-unicode-supported@0.1.0: {}
+ is-unicode-supported@1.3.0: {}
+
is-unicode-supported@2.1.0: {}
is-utf8@0.2.1: {}
@@ -28565,6 +28887,13 @@ snapshots:
json-schema: 0.2.3
verror: 1.10.0
+ jszip@3.10.2:
+ dependencies:
+ lie: 3.3.0
+ pako: 1.0.11
+ readable-stream: 2.3.8
+ setimmediate: 1.0.5
+
just-diff-apply@5.5.0: {}
just-diff@5.2.0: {}
@@ -28669,6 +28998,10 @@ snapshots:
kind-of@6.0.3: {}
+ klaw-sync@6.0.0:
+ dependencies:
+ graceful-fs: 4.2.11
+
kleur@3.0.3: {}
klona@2.0.6: {}
@@ -28721,6 +29054,10 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
+ lie@3.3.0:
+ dependencies:
+ immediate: 3.0.6
+
lilconfig@2.1.0: {}
lines-and-columns@1.2.4: {}
@@ -28892,6 +29229,8 @@ snapshots:
lodash@4.17.21: {}
+ lodash@4.17.23: {}
+
lodash@4.18.1: {}
log-driver@1.2.7: {}
@@ -28905,6 +29244,11 @@ snapshots:
chalk: 4.1.2
is-unicode-supported: 0.1.0
+ log-symbols@6.0.0:
+ dependencies:
+ chalk: 5.6.2
+ is-unicode-supported: 1.3.0
+
log-symbols@7.0.1:
dependencies:
is-unicode-supported: 2.1.0
@@ -30214,6 +30558,18 @@ snapshots:
strip-ansi: 6.0.0
wcwidth: 1.0.1
+ ora@8.2.0:
+ dependencies:
+ chalk: 5.6.2
+ cli-cursor: 5.0.0
+ cli-spinners: 2.9.2
+ is-interactive: 2.0.0
+ is-unicode-supported: 2.1.0
+ log-symbols: 6.0.0
+ stdin-discarder: 0.2.2
+ string-width: 7.2.0
+ strip-ansi: 7.1.0
+
ora@9.4.1:
dependencies:
chalk: 5.6.2
@@ -30229,6 +30585,8 @@ snapshots:
os-tmpdir@1.0.2: {}
+ os@0.1.1: {}
+
osenv@0.1.5:
dependencies:
os-homedir: 1.0.2
@@ -30448,6 +30806,8 @@ snapshots:
transitivePeerDependencies:
- debug
+ pako@1.0.11: {}
+
param-case@3.0.4:
dependencies:
dot-case: 3.0.4
@@ -31279,6 +31639,8 @@ snapshots:
extend-shallow: 3.0.2
safe-regex: 1.1.0
+ regexp-to-ast@0.5.0: {}
+
regexp.prototype.flags@1.5.4:
dependencies:
call-bind: 1.0.9
@@ -32117,6 +32479,10 @@ snapshots:
ip-address: 10.2.0
smart-buffer: 4.2.0
+ sort-any@1.2.9:
+ dependencies:
+ lodash: 4.18.1
+
sort-any@2.0.0:
dependencies:
lodash: 4.18.1
@@ -32307,6 +32673,8 @@ snapshots:
statuses@2.0.2: {}
+ stdin-discarder@0.2.2: {}
+
stdin-discarder@0.3.2: {}
stop-iteration-iterator@1.1.0:
@@ -32380,7 +32748,13 @@ snapshots:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
- strip-ansi: 7.2.0
+ strip-ansi: 7.1.0
+
+ string-width@7.2.0:
+ dependencies:
+ emoji-regex: 10.6.0
+ get-east-asian-width: 1.6.0
+ strip-ansi: 7.1.0
string-width@8.2.1:
dependencies:
@@ -32745,6 +33119,10 @@ snapshots:
dependencies:
tldts-core: 6.1.86
+ tmp@0.0.29:
+ dependencies:
+ os-tmpdir: 1.0.2
+
tmp@0.0.33:
dependencies:
os-tmpdir: 1.0.2
@@ -33016,6 +33394,8 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
+ tunnel@0.0.4: {}
+
tunnel@0.0.6: {}
tweetnacl@0.14.5: {}
@@ -33096,6 +33476,11 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
+ typed-rest-client@1.2.0:
+ dependencies:
+ tunnel: 0.0.4
+ underscore: 1.8.3
+
typed-rest-client@1.8.6:
dependencies:
qs: 6.13.0
@@ -33128,6 +33513,8 @@ snapshots:
underscore@1.13.1: {}
+ underscore@1.8.3: {}
+
undici-types@5.26.5: {}
undici-types@6.21.0: {}
@@ -33517,14 +33904,64 @@ snapshots:
yauzl: 2.10.0
yazl: 2.5.1
+ vsce@1.84.0:
+ dependencies:
+ azure-devops-node-api: 7.2.0
+ chalk: 2.4.2
+ cheerio: 1.2.0
+ commander: 6.2.1
+ denodeify: 1.2.1
+ glob: 7.2.0
+ leven: 3.1.0
+ lodash: 4.18.1
+ markdown-it: 10.0.0
+ mime: 1.6.0
+ minimatch: 3.1.2
+ osenv: 0.1.5
+ parse-semver: 1.1.1
+ read: 1.0.7
+ semver: 5.7.1
+ tmp: 0.0.29
+ typed-rest-client: 1.2.0
+ url-join: 1.1.0
+ yauzl: 2.10.0
+ yazl: 2.5.1
+
vscode-jsonrpc@4.0.0: {}
+ vscode-jsonrpc@6.0.0: {}
+
+ vscode-jsonrpc@9.0.2: {}
+
+ vscode-languageclient@6.1.3:
+ dependencies:
+ semver: 6.3.1
+ vscode-languageserver-protocol: 3.18.3
+
+ vscode-languageserver-protocol@3.16.0:
+ dependencies:
+ vscode-jsonrpc: 6.0.0
+ vscode-languageserver-types: 3.16.0
+
+ vscode-languageserver-protocol@3.18.3:
+ dependencies:
+ vscode-jsonrpc: 9.0.2
+ vscode-languageserver-types: 3.18.3
+
+ vscode-languageserver-textdocument@1.0.1: {}
+
vscode-languageserver-textdocument@1.0.4: {}
vscode-languageserver-types@3.16.0: {}
vscode-languageserver-types@3.17.5: {}
+ vscode-languageserver-types@3.18.3: {}
+
+ vscode-languageserver@7.0.0:
+ dependencies:
+ vscode-languageserver-protocol: 3.16.0
+
vscode-test@1.5.2:
dependencies:
http-proxy-agent: 4.0.1
@@ -34314,7 +34751,7 @@ snapshots:
dependencies:
ansi-styles: 6.2.1
string-width: 5.1.2
- strip-ansi: 7.2.0
+ strip-ansi: 7.1.0
wrappy@1.0.2: {}
diff --git a/projects/xml-tools/README.md b/projects/xml-tools/README.md
new file mode 100644
index 00000000..21abf87e
--- /dev/null
+++ b/projects/xml-tools/README.md
@@ -0,0 +1,51 @@
+[](https://github.com/prettier/prettier)
+
+# XML-Tools
+
+XML-Tools is an npm [mono-repo][mono-repo] that contains tools & libraries for [Extensible Markup Language][xml] (XML).
+These tools & libraries are mainly focused on capabilities for implementing Editor Services flows in IDEs, for example:
+Content Assist and Diagnostics.
+
+It currently contains the following packages:
+
+- [![npm-xml-tools-parser][npm-xml-tools-parser-image]][npm-xml-tools-parser-url] [@xml-tools/parser](./packages/parser) A Fault Tolerant XML Parser which outputs a [Concrete Syntax Tree][cst].
+- [![npm-xml-tools-ast][npm-xml-tools-ast-image]][npm-xml-tools-ast-url] [@xml-tools/ast](./packages/ast) XML [Abstract Syntax Tree][ast].
+- [![npm-xml-tools-ast-position][npm-xml-tools-ast-position-image]][npm-xml-tools-ast-position-url] [@xml-tools/ast-position](./packages/ast-position) XML Abstract Syntax Tree position utilities.
+- [![npm-xml-tools-content-assist][npm-xml-tools-content-assist-image]][npm-xml-tools-content-assist-url] [@xml-tools/content-assist](./packages/content-assist) Extensible Content Assist APIs for XML.
+- [![npm-xml-tools-validation][npm-xml-tools-validation-image]][npm-xml-tools-validation-url] [@xml-tools/validation](./packages/validation) Extensible Validation APIs for XML.
+- [![npm-xml-tools-constraints][npm-xml-tools-constraints-image]][npm-xml-tools-constraints-url] [@xml-tools/constraints](./packages/constraints) Validations for XML constraints.
+- [![npm-xml-tools-simple-schema][npm-xml-tools-simple-schema-image]][npm-xml-tools-simple-schema-url] [@xml-tools/simple-schema](./packages/simple-schema) Simple XML Schema represented as a JavaScript object literal.
+- [![npm-xml-tools-language-server][npm-xml-tools-language-server-image]][npm-xml-tools-language-server-url] [@xml-tools/language-server](./packages/language-server) XML Language Server.
+- [![npm-xml-tools-common][npm-xml-tools-common-image]][npm-xml-tools-common-url] [@xml-tools/common](./packages/common) Shared Utilities for xml-tools packages.
+
+[npm-xml-tools-parser-image]: https://img.shields.io/npm/v/@xml-tools/parser.svg
+[npm-xml-tools-parser-url]: https://www.npmjs.com/package/@xml-tools/parser
+[npm-xml-tools-ast-image]: https://img.shields.io/npm/v/@xml-tools/ast.svg
+[npm-xml-tools-ast-url]: https://www.npmjs.com/package/@xml-tools/ast
+[npm-xml-tools-ast-position-image]: https://img.shields.io/npm/v/@xml-tools/ast-position.svg
+[npm-xml-tools-ast-position-url]: https://www.npmjs.com/package/@xml-tools/ast-position
+[npm-xml-tools-content-assist-image]: https://img.shields.io/npm/v/@xml-tools/content-assist.svg
+[npm-xml-tools-content-assist-url]: https://www.npmjs.com/package/@xml-tools/content-assist
+[npm-xml-tools-validation-image]: https://img.shields.io/npm/v/@xml-tools/validation.svg
+[npm-xml-tools-validation-url]: https://www.npmjs.com/package/@xml-tools/validation
+[npm-xml-tools-constraints-url]: https://www.npmjs.com/package/@xml-tools/constraints
+[npm-xml-tools-constraints-image]: https://img.shields.io/npm/v/@xml-tools/constraints.svg
+[npm-xml-tools-simple-schema-image]: https://img.shields.io/npm/v/@xml-tools/simple-schema.svg
+[npm-xml-tools-simple-schema-url]: https://www.npmjs.com/package/@xml-tools/simple-schema
+[npm-xml-tools-language-server-image]: https://img.shields.io/npm/v/@xml-tools/language-server.svg
+[npm-xml-tools-language-server-url]: https://www.npmjs.com/package/@xml-tools/language-server
+[npm-xml-tools-common-image]: https://img.shields.io/npm/v/@xml-tools/common.svg
+[npm-xml-tools-common-url]: https://www.npmjs.com/package/@xml-tools/common
+
+## Support
+
+Please report issues [here](https://github.com/SAP/app-studio-toolkit/issues/new/choose) and label them with `project:xml-tools`.
+
+## Contributing
+
+See [CONTRIBUTING.md](https://github.com/SAP/app-studio-toolkit/blob/main/CONTRIBUTING.md).
+
+[mono-repo]: https://github.com/babel/babel/blob/master/doc/design/monorepo.md
+[xml]: https://github.com/babel/babel/blob/master/doc/design/monorepo.md
+[ast]: https://en.wikipedia.org/wiki/Abstract_syntax_tree
+[cst]: https://en.wikipedia.org/wiki/Parse_tree
diff --git a/projects/xml-tools/packages/ast-position/CHANGELOG.old.md b/projects/xml-tools/packages/ast-position/CHANGELOG.old.md
new file mode 100644
index 00000000..b8400d2f
--- /dev/null
+++ b/projects/xml-tools/packages/ast-position/CHANGELOG.old.md
@@ -0,0 +1,59 @@
+> **Historic changelog.** This records changes made while the project lived in the
+> standalone [SAP/xml-tools](https://github.com/SAP/xml-tools) repository, before it was
+> migrated into [SAP/app-studio-toolkit](https://github.com/SAP/app-studio-toolkit).
+> Newer entries are generated by Changesets in `CHANGELOG.md`.
+
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+## [2.0.8](https://github.com/sap/xml-tools/compare/@xml-tools/ast-position@2.0.7...@xml-tools/ast-position@2.0.8) (2026-02-11)
+
+**Note:** Version bump only for package @xml-tools/ast-position
+
+## [2.0.7](https://github.com/sap/xml-tools/compare/@xml-tools/ast-position@2.0.6...@xml-tools/ast-position@2.0.7) (2021-06-03)
+
+**Note:** Version bump only for package @xml-tools/ast-position
+
+## [2.0.6](https://github.com/sap/xml-tools/compare/@xml-tools/ast-position@2.0.5...@xml-tools/ast-position@2.0.6) (2021-02-09)
+
+**Note:** Version bump only for package @xml-tools/ast-position
+
+## [2.0.5](https://github.com/sap/xml-tools/compare/@xml-tools/ast-position@2.0.4...@xml-tools/ast-position@2.0.5) (2021-01-21)
+
+**Note:** Version bump only for package @xml-tools/ast-position
+
+## [2.0.4](https://github.com/sap/xml-tools/compare/@xml-tools/ast-position@2.0.3...@xml-tools/ast-position@2.0.4) (2021-01-20)
+
+### Bug Fixes
+
+- missing lib folder in npm packages ([ffed3c2](https://github.com/sap/xml-tools/commit/ffed3c2c54c70aea8b9ded0d53786382bc190cc5))
+
+## [2.0.3](https://github.com/sap/xml-tools/compare/@xml-tools/ast-position@2.0.2...@xml-tools/ast-position@2.0.3) (2021-01-20)
+
+**Note:** Version bump only for package @xml-tools/ast-position
+
+## [2.0.2](https://github.com/sap/xml-tools/compare/@xml-tools/ast-position@2.0.1...@xml-tools/ast-position@2.0.2) (2020-06-29)
+
+**Note:** Version bump only for package @xml-tools/ast-position
+
+## [2.0.1](https://github.com/sap/xml-tools/compare/@xml-tools/ast-position@2.0.0...@xml-tools/ast-position@2.0.1) (2020-06-18)
+
+**Note:** Version bump only for package @xml-tools/ast-position
+
+# [2.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/ast-position@1.1.0...@xml-tools/ast-position@2.0.0) (2020-05-21)
+
+### Features
+
+- **ast-position:** rename `getAstNodeInPosition` -> astPositionAtOffset ([585253f](https://github.com/sap/xml-tools/commit/585253f))
+
+### BREAKING CHANGES
+
+- **ast-position:** rename `getAstNodeInPosition` -> astPositionAtOffset
+
+# 1.1.0 (2020-05-18)
+
+### Features
+
+- **ast-position:** add AST position visitor ([#175](https://github.com/sap/xml-tools/issues/175)) ([9eb1253](https://github.com/sap/xml-tools/commit/9eb1253))
diff --git a/projects/xml-tools/packages/ast-position/README.md b/projects/xml-tools/packages/ast-position/README.md
new file mode 100644
index 00000000..62afa846
--- /dev/null
+++ b/projects/xml-tools/packages/ast-position/README.md
@@ -0,0 +1,32 @@
+[](https://www.npmjs.com/package/@xml-tools/ast-position)
+
+# @xml-tools/ast-position
+
+Utilities related to textual positions in the XML-Tools **A**bstract **S**yntax **T**ree.
+
+## Installation
+
+With npm:
+
+- `npm install @xml-tools/ast-position`
+
+With Yarn
+
+- `yarn add @xml-tools/ast-position`
+
+## Usage
+
+Please see the [TypeScript Definitions](./api.d.ts) for full API details.
+
+Simply import/require the @xml-tools/ast-position package and use the utilities
+as declared in the TypeScript signatures above.
+
+## Support
+
+Please report issues [here](https://github.com/SAP/app-studio-toolkit/issues/new/choose) and label them with `project:xml-tools`.
+
+## Contributing
+
+See [CONTRIBUTING.md](https://github.com/SAP/app-studio-toolkit/blob/main/CONTRIBUTING.md).
+
+[ast]: https://en.wikipedia.org/wiki/Abstract_syntax_tree
diff --git a/projects/xml-tools/packages/ast-position/api.d.ts b/projects/xml-tools/packages/ast-position/api.d.ts
new file mode 100644
index 00000000..dae97dba
--- /dev/null
+++ b/projects/xml-tools/packages/ast-position/api.d.ts
@@ -0,0 +1,36 @@
+import { XMLAttribute, XMLElement, XMLDocument } from "@xml-tools/ast";
+
+/**
+ * Not all possible kinds of positions are currently supported.
+ * More may be added in the future.
+ */
+type AstPosition =
+ | XMLElementOpenName
+ | XMLElementCloseName
+ | XMLAttributeKey
+ | XMLAttributeValue;
+
+declare function astPositionAtOffset(
+ ast: XMLDocument,
+ offset: number
+): AstPosition | undefined;
+
+interface XMLElementOpenName {
+ kind: "XMLElementOpenName";
+ astNode: XMLElement;
+}
+
+interface XMLElementCloseName {
+ kind: "XMLElementCloseName";
+ astNode: XMLElement;
+}
+
+interface XMLAttributeKey {
+ kind: "XMLAttributeKey";
+ astNode: XMLAttribute;
+}
+
+interface XMLAttributeValue {
+ kind: "XMLAttributeValue";
+ astNode: XMLAttribute;
+}
diff --git a/projects/xml-tools/packages/ast-position/lib/api.js b/projects/xml-tools/packages/ast-position/lib/api.js
new file mode 100644
index 00000000..2e1c3be5
--- /dev/null
+++ b/projects/xml-tools/packages/ast-position/lib/api.js
@@ -0,0 +1,17 @@
+const { AstPositionVisitor } = require("./ast-position");
+const { accept } = require("@xml-tools/ast");
+
+/**
+ * @param {XMLDocument} xmlDoc
+ * @param {number} offset
+ * @returns {AstPosition | undefined}
+ */
+function astPositionAtOffset(xmlDoc, offset) {
+ const positionVisitor = new AstPositionVisitor(offset);
+ accept(xmlDoc, positionVisitor);
+ return positionVisitor.position;
+}
+
+module.exports = {
+ astPositionAtOffset,
+};
diff --git a/projects/xml-tools/packages/ast-position/lib/ast-position.js b/projects/xml-tools/packages/ast-position/lib/ast-position.js
new file mode 100644
index 00000000..49818ee3
--- /dev/null
+++ b/projects/xml-tools/packages/ast-position/lib/ast-position.js
@@ -0,0 +1,39 @@
+class AstPositionVisitor {
+ constructor(offset) {
+ this.offset = offset;
+ this.position = undefined;
+ }
+
+ visitXMLElement(node) {
+ const openName = node.syntax.openName;
+ const closeName = node.syntax.closeName;
+ if (this.isOffsetInRange(openName)) {
+ this.position = { kind: "XMLElementOpenName", astNode: node };
+ } else if (this.isOffsetInRange(closeName)) {
+ this.position = { kind: "XMLElementCloseName", astNode: node };
+ }
+ }
+
+ visitXMLAttribute(node) {
+ const key = node.syntax.key;
+ const value = node.syntax.value;
+
+ if (this.isOffsetInRange(key)) {
+ this.position = { kind: "XMLAttributeKey", astNode: node };
+ } else if (this.isOffsetInRange(value)) {
+ this.position = { kind: "XMLAttributeValue", astNode: node };
+ }
+ }
+
+ isOffsetInRange(position) {
+ return (
+ position !== undefined &&
+ position.startOffset <= this.offset &&
+ position.endOffset >= this.offset
+ );
+ }
+}
+
+module.exports = {
+ AstPositionVisitor: AstPositionVisitor,
+};
diff --git a/projects/xml-tools/packages/ast-position/nyc.config.js b/projects/xml-tools/packages/ast-position/nyc.config.js
new file mode 100644
index 00000000..3e3f8962
--- /dev/null
+++ b/projects/xml-tools/packages/ast-position/nyc.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ // xml-tools sources live in lib/ (not src/), so this local config overrides
+ // the repo-root nyc.config.js include of **/src/** (which would report 0%).
+ include: ["lib/**/*.js"],
+ reporter: ["text", "lcov"],
+ all: true,
+ branches: 100,
+ lines: 100,
+ functions: 100,
+ statements: 100,
+};
diff --git a/projects/xml-tools/packages/ast-position/package.json b/projects/xml-tools/packages/ast-position/package.json
new file mode 100644
index 00000000..6cce1150
--- /dev/null
+++ b/projects/xml-tools/packages/ast-position/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "@xml-tools/ast-position",
+ "version": "2.0.8",
+ "description": "XML Ast Position APIs",
+ "keywords": [
+ "xml",
+ "position"
+ ],
+ "main": "lib/api.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/SAP/app-studio-toolkit.git",
+ "directory": "projects/xml-tools/packages/ast-position"
+ },
+ "license": "Apache-2.0",
+ "typings": "./api.d.ts",
+ "files": [
+ "lib",
+ "api.d.ts",
+ "CHANGELOG.md",
+ "CHANGELOG.old.md"
+ ],
+ "dependencies": {
+ "@xml-tools/ast": "workspace:*"
+ },
+ "devDependencies": {
+ "@xml-tools/parser": "workspace:*"
+ },
+ "scripts": {
+ "ci": "npm-run-all type-check coverage:*",
+ "test": "mocha \"./test/**/*spec.js\"",
+ "coverage:run": "nyc mocha \"./test/**/*spec.js\"",
+ "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100",
+ "type-check": "tsc"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "bugs": {
+ "url": "https://github.com/SAP/app-studio-toolkit/issues"
+ }
+}
diff --git a/projects/xml-tools/packages/ast-position/test/ast-position-spec.js b/projects/xml-tools/packages/ast-position/test/ast-position-spec.js
new file mode 100644
index 00000000..b04e9bb6
--- /dev/null
+++ b/projects/xml-tools/packages/ast-position/test/ast-position-spec.js
@@ -0,0 +1,143 @@
+const { expect } = require("chai");
+const { buildAst } = require("@xml-tools/ast");
+const { parse } = require("@xml-tools/parser");
+// Testing the npm package `main` exports by requiring the folder where the `package.json` resides.
+const { astPositionAtOffset } = require("../");
+
+describe("AST Position visitor", () => {
+ it("will get xml attribute key", () => {
+ const xmlSnippet = `
+
+
+ `;
+ const astNodeInPosition = getXMLPositionFromSnippet(xmlSnippet);
+
+ expect(astNodeInPosition).to.exist;
+ expect(astNodeInPosition.kind).to.equal("XMLAttributeKey");
+ expect(astNodeInPosition.astNode.key).to.equal("showSeparators");
+ });
+
+ it("will get xml attribute value", () => {
+ const xmlSnippet = `
+
+
+ `;
+ const astNodeInPosition = getXMLPositionFromSnippet(xmlSnippet);
+
+ expect(astNodeInPosition).to.exist;
+ expect(astNodeInPosition.kind).to.equal("XMLAttributeValue");
+ expect(astNodeInPosition.astNode.value).to.equal("All");
+ });
+
+ it("will get xml element open name", () => {
+ const xmlSnippet = `
+
+
+ `;
+ const astNodeInPosition = getXMLPositionFromSnippet(xmlSnippet);
+
+ expect(astNodeInPosition).to.exist;
+ expect(astNodeInPosition.kind).to.equal("XMLElementOpenName");
+ expect(astNodeInPosition.astNode.name).to.equal("List");
+ });
+
+ it("will get xml element close name", () => {
+ const xmlSnippet = `
+
+
+ `;
+ const astNodeInPosition = getXMLPositionFromSnippet(xmlSnippet);
+
+ expect(astNodeInPosition).to.exist;
+ expect(astNodeInPosition.kind).to.equal("XMLElementCloseName");
+ expect(astNodeInPosition.astNode.name).to.equal("customData");
+ });
+
+ it("Out of element - before open tag", () => {
+ const xmlSnippet = `
+ ⇶
+
+ `;
+
+ const astNodeInPosition = getXMLPositionFromSnippet(xmlSnippet);
+ expect(astNodeInPosition).to.be.undefined;
+ });
+
+ it("Out of element - after close tag", () => {
+ const xmlSnippet = `
+
+
+ ⇶`;
+
+ const astNodeInPosition = getXMLPositionFromSnippet(xmlSnippet);
+ expect(astNodeInPosition).to.be.undefined;
+ });
+
+ it("Out of element - between elements", () => {
+ const xmlSnippet = `
+
+ ⇶
+ `;
+
+ const astNodeInPosition = getXMLPositionFromSnippet(xmlSnippet);
+ expect(astNodeInPosition).to.be.undefined;
+ });
+
+ it("Out of attribute - before attribute", () => {
+ const xmlSnippet = `
+
+
+ `;
+
+ const astNodeInPosition = getXMLPositionFromSnippet(xmlSnippet);
+ expect(astNodeInPosition).to.be.undefined;
+ });
+
+ it("Out of attribute - between attributes", () => {
+ const xmlSnippet = `
+
+
+ `;
+
+ const astNodeInPosition = getXMLPositionFromSnippet(xmlSnippet);
+ expect(astNodeInPosition).to.be.undefined;
+ });
+
+ it("Out of attribute - between key to vale", () => {
+ const xmlSnippet = `
+
+
+ `;
+
+ const astNodeInPosition = getXMLPositionFromSnippet(xmlSnippet);
+ expect(astNodeInPosition).to.be.undefined;
+ });
+
+ function getXMLPositionFromSnippet(xmlSnippet) {
+ const xmlText = xmlSnippet.replace("⇶", "");
+ const offset = xmlSnippet.indexOf("⇶");
+ const { cst, tokenVector } = parse(xmlText);
+ const xmlDoc = buildAst(cst, tokenVector);
+ return astPositionAtOffset(xmlDoc, offset);
+ }
+});
diff --git a/projects/xml-tools/packages/ast-position/tsconfig.json b/projects/xml-tools/packages/ast-position/tsconfig.json
new file mode 100644
index 00000000..a4ed07be
--- /dev/null
+++ b/projects/xml-tools/packages/ast-position/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": ["./api.d.ts"]
+}
diff --git a/projects/xml-tools/packages/ast/CHANGELOG.old.md b/projects/xml-tools/packages/ast/CHANGELOG.old.md
new file mode 100644
index 00000000..9208cfb6
--- /dev/null
+++ b/projects/xml-tools/packages/ast/CHANGELOG.old.md
@@ -0,0 +1,170 @@
+> **Historic changelog.** This records changes made while the project lived in the
+> standalone [SAP/xml-tools](https://github.com/SAP/xml-tools) repository, before it was
+> migrated into [SAP/app-studio-toolkit](https://github.com/SAP/app-studio-toolkit).
+> Newer entries are generated by Changesets in `CHANGELOG.md`.
+
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [5.1.0](https://github.com/sap/xml-tools/compare/@xml-tools/ast@5.0.5...@xml-tools/ast@5.1.0) (2026-02-11)
+
+### Features
+
+- update lodash dependency to version 4.17.23 ([#461](https://github.com/sap/xml-tools/issues/461)) ([23a319d](https://github.com/sap/xml-tools/commit/23a319d6ed42a4a3885ec5748eb127147a8960bc))
+
+## [5.0.5](https://github.com/sap/xml-tools/compare/@xml-tools/ast@5.0.4...@xml-tools/ast@5.0.5) (2021-06-03)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+## [5.0.4](https://github.com/sap/xml-tools/compare/@xml-tools/ast@5.0.3...@xml-tools/ast@5.0.4) (2021-02-09)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+## [5.0.3](https://github.com/sap/xml-tools/compare/@xml-tools/ast@5.0.2...@xml-tools/ast@5.0.3) (2021-01-21)
+
+### Bug Fixes
+
+- missing lib folder in ast package ([c61d703](https://github.com/sap/xml-tools/commit/c61d70320bdc13800402082aeaac42a1fd92cc68))
+
+## [5.0.2](https://github.com/sap/xml-tools/compare/@xml-tools/ast@5.0.1...@xml-tools/ast@5.0.2) (2021-01-20)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+## [5.0.1](https://github.com/sap/xml-tools/compare/@xml-tools/ast@5.0.0...@xml-tools/ast@5.0.1) (2021-01-20)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+# [5.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/ast@4.2.1...@xml-tools/ast@5.0.0) (2020-06-29)
+
+### Features
+
+- **ast:** attribute inside a prolog are now of XMLPrologAttribute type ([#208](https://github.com/sap/xml-tools/issues/208)) ([a303db5](https://github.com/sap/xml-tools/commit/a303db5))
+
+### BREAKING CHANGES
+
+- **ast:** Vistors that need to handle attributes inside prolog must now implement the `visitPrologAttribute` method.
+
+## [4.2.1](https://github.com/sap/xml-tools/compare/@xml-tools/ast@4.2.0...@xml-tools/ast@4.2.1) (2020-06-18)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+# [4.2.0](https://github.com/sap/xml-tools/compare/@xml-tools/ast@4.1.0...@xml-tools/ast@4.2.0) (2020-05-17)
+
+### Features
+
+- **common:** add utility functions for xmlns attributes ([#173](https://github.com/sap/xml-tools/issues/173)) ([20d6c09](https://github.com/sap/xml-tools/commit/20d6c09))
+
+# [4.1.0](https://github.com/sap/xml-tools/compare/@xml-tools/ast@4.0.3...@xml-tools/ast@4.1.0) (2020-05-05)
+
+### Features
+
+- **ast:** added `.syntax.isSelfClosing` property on an XMLElement node ([#164](https://github.com/sap/xml-tools/issues/164)) ([bfc448b](https://github.com/sap/xml-tools/commit/bfc448b))
+
+## [4.0.3](https://github.com/sap/xml-tools/compare/@xml-tools/ast@4.0.2...@xml-tools/ast@4.0.3) (2020-04-02)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+## [4.0.2](https://github.com/sap/xml-tools/compare/@xml-tools/ast@4.0.1...@xml-tools/ast@4.0.2) (2020-02-19)
+
+### Bug Fixes
+
+- add npmignore file to each package ([5bbf209](https://github.com/sap/xml-tools/commit/5bbf209))
+
+## [4.0.1](https://github.com/sap/xml-tools/compare/@xml-tools/ast@4.0.0...@xml-tools/ast@4.0.1) (2020-02-11)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+# [4.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/ast@2.0.1...@xml-tools/ast@4.0.0) (2020-02-10)
+
+### Features
+
+- **ast:** element Namespaces are now represented as Records in a Map ([f9f6fdc](https://github.com/sap/xml-tools/commit/f9f6fdc))
+
+### BREAKING CHANGES
+
+- **ast:** The `namespaces` property of an XMLElement is now a Map/Dictionary not an Array.
+
+# [3.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/ast@2.0.1...@xml-tools/ast@3.0.0) (2020-02-07)
+
+### Features
+
+- **ast:** element Namespaces are now represented as Records in a Map ([f9f6fdc](https://github.com/sap/xml-tools/commit/f9f6fdc))
+
+### BREAKING CHANGES
+
+- **ast:** The `namespaces` property of an XMLElement is now a Map/Dictionary not an Array.
+
+## [2.0.1](https://github.com/sap/xml-tools/compare/@xml-tools/ast@2.0.0...@xml-tools/ast@2.0.1) (2020-01-28)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+# [2.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/ast@1.0.0...@xml-tools/ast@2.0.0) (2020-01-20)
+
+### Features
+
+- **parser:** support Basic DocType Declarations ([5b4db21](https://github.com/sap/xml-tools/commit/5b4db21))
+
+### BREAKING CHANGES
+
+- **parser:** Implementing XmlCstVisitor now requires implementing two additional methods:
+ (docTypeDecl and externalID)
+
+# [1.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/ast@0.5.0...@xml-tools/ast@1.0.0) (2019-12-08)
+
+### Features
+
+- **ast:** attributesRange and guessedAttributeRange support ([8ce840d](https://github.com/sap/xml-tools/commit/8ce840d)), closes [#51](https://github.com/sap/xml-tools/issues/51)
+
+### BREAKING CHANGES
+
+- **ast:** buildAst now requires a tokenVector argument
+
+# [0.5.0](https://github.com/sap/xml-tools/compare/@xml-tools/ast@0.4.6...@xml-tools/ast@0.5.0) (2019-12-05)
+
+### Features
+
+- better Handling of Open/Close Body ranges ([0751a7e](https://github.com/sap/xml-tools/commit/0751a7e))
+
+## [0.4.6](https://github.com/sap/xml-tools/compare/@xml-tools/ast@0.4.5...@xml-tools/ast@0.4.6) (2019-12-03)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+## [0.4.5](https://github.com/sap/xml-tools/compare/@xml-tools/ast@0.4.4...@xml-tools/ast@0.4.5) (2019-11-20)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+## [0.4.4](https://github.com/sap/xml-tools/compare/@xml-tools/ast@0.4.3...@xml-tools/ast@0.4.4) (2019-11-12)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+## [0.4.3](https://github.com/sap/xml-tools/compare/@xml-tools/ast@0.4.2...@xml-tools/ast@0.4.3) (2019-11-11)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+## [0.4.2](https://github.com/sap/xml-tools/compare/@xml-tools/ast@0.4.1...@xml-tools/ast@0.4.2) (2019-11-11)
+
+**Note:** Version bump only for package @xml-tools/ast
+
+## [0.4.1](https://github.com/sap/xml-tools/compare/@xml-tools/ast@0.4.0...@xml-tools/ast@0.4.1) (2019-11-06)
+
+### Bug Fixes
+
+- new property added to ast interface ([#35](https://github.com/sap/xml-tools/issues/35)) ([d7fd672](https://github.com/sap/xml-tools/commit/d7fd672))
+
+# [0.4.0](https://github.com/sap/xml-tools/compare/@xml-tools/ast@0.3.0...@xml-tools/ast@0.4.0) (2019-11-06)
+
+### Features
+
+- open tag body range added ([#34](https://github.com/sap/xml-tools/issues/34)) ([5414111](https://github.com/sap/xml-tools/commit/5414111))
+
+# 0.3.0 (2019-10-28)
+
+### Features
+
+- move to lerna independent mode ([6c347a8](https://github.com/sap/xml-tools/commit/6c347a8)), closes [#18](https://github.com/sap/xml-tools/issues/18)
+
+# 0.2.0 (2019-10-28)
+
+# 0.1.0 (2019-10-28)
diff --git a/projects/xml-tools/packages/ast/README.md b/projects/xml-tools/packages/ast/README.md
new file mode 100644
index 00000000..bb5f3940
--- /dev/null
+++ b/projects/xml-tools/packages/ast/README.md
@@ -0,0 +1,70 @@
+[](https://www.npmjs.com/package/@xml-tools/ast)
+
+# @xml-tools/ast
+
+Utilities for building and traversing an XML **A**bstract **S**yntax **T**ree ([AST][ast]).
+
+There are two things which distinguish this AST from most others ASTs:
+
+1. This AST can represent a **partially valid** XML, in practice this means virtually all properties on
+ the AST are optional and may have undefined values.
+2. This AST contains **additional syntactic information** to enable additional linting & formatting flows, for example:
+ - the original position and value of an attribute's value (including the quotes).
+ - The original positions and values of an XMLElement's open/close names.
+
+The input for constructing the AST is a CST which is created by the [@xml-tools/parser](../parser) package.
+
+The AST structure is used as part of the input for the [@xml-tools/content-assist](../content-assist) APIs.
+
+## Installation
+
+With npm:
+
+- `npm install @xml-tools/ast`
+
+With Yarn
+
+- `yarn add @xml-tools/ast`
+
+## Usage
+
+Please see the [TypeScript Definitions](./api.d.ts) for full API details.
+
+A simple usage example:
+
+```javascript
+const { parse } = require("@xml-tools/parser");
+const { buildAst, accept } = require("@xml-tools/ast");
+
+const xmlText = `
+ Bill
+ Tim
+
+`;
+
+const { cst, tokenVector } = parse(xmlText);
+const xmlDocAst = buildAst(cst, tokenVector);
+console.log(xmlDocAst.rootElement.name); // -> note
+
+// A Visitor allows us to invoke actions on the XML ASTNodes without worrying about
+// The XML AST structure / traversal method.
+const printVisitor = {
+ // Will be invoked once for each Element node in the AST.
+ visitXMLElement: function (node) {
+ console.log(node.name);
+ },
+
+ // An XML AST Visitor may have other methods as well, see the api.d.ts file/
+};
+
+// Invoking the Visitor
+accept(xmlDocAst, printVisitor); // -> note, Bill, Tim
+```
+
+## Support
+
+Please report issues [here](https://github.com/SAP/app-studio-toolkit/issues/new/choose) and label them with `project:xml-tools`.
+
+## Contributing
+
+See [CONTRIBUTING.md](https://github.com/SAP/app-studio-toolkit/blob/main/CONTRIBUTING.md).
diff --git a/projects/xml-tools/packages/ast/api.d.ts b/projects/xml-tools/packages/ast/api.d.ts
new file mode 100644
index 00000000..2fc95e55
--- /dev/null
+++ b/projects/xml-tools/packages/ast/api.d.ts
@@ -0,0 +1,216 @@
+import { IToken } from "chevrotain";
+import { DocumentCstNode } from "@xml-tools/parser";
+
+/**
+ * Builds an XML Ast from an XML Cst.
+ * Can process even partial CSTs...
+ */
+export function buildAst(
+ docCst: DocumentCstNode,
+ tokenVector: IToken[]
+): XMLDocument;
+
+/**
+ * An Abstract Syntax Tree structure
+ * Which contains partial position information relevant
+ * for validation and content assist scenarios.
+ *
+ * Note that:
+ *
+ * - This data structure is immutable.
+ * - The AST is is a "pure data structure"
+ * there are no methods directly on the nodes, instead utilities are provided
+ * as external functions.
+ * - The AST does not contain the full syntactic information, which means
+ * implementing certain editor services scenarios may require first augmenting
+ * this data structure (e.g formatting).
+ * - Some properties are optional (marked with '?' syntax)
+ * - Some properties are mandatory but may be `InvalidSyntax` (null) because
+ * this structure also represents partially valid XML.
+ */
+declare interface XMLDocument {
+ readonly type: "XMLDocument";
+ readonly prolog?: XMLProlog;
+ readonly rootElement: XMLElement | InvalidSyntax;
+ readonly position: SourcePosition;
+}
+
+declare interface XMLProlog {
+ readonly type: "XMLProlog";
+ readonly parent: XMLDocument;
+
+ readonly attributes: XMLPrologAttribute[];
+
+ readonly position: SourcePosition;
+}
+
+// A Prefix cannot include a colon ":" so the below string will never conflict
+// with a valid namespace prefix.
+declare type DefaultNS = "::DEFAULT";
+declare const DEFAULT_NS: DefaultNS;
+declare type Prefix = string | DefaultNS;
+declare type Uri = string;
+
+declare interface XMLElement {
+ readonly type: "XMLElement";
+ readonly parent: XMLElement | XMLDocument;
+
+ readonly namespaces: Record;
+
+ // namespace prefix used by this XML Element.
+ // - Note that this is an optional syntax.
+ readonly ns?: string;
+ // Actual name part (without the namespace prefix) used by this XML Element.
+ readonly name: string | InvalidSyntax;
+
+ readonly attributes: XMLAttribute[];
+ readonly subElements: XMLElement[];
+ readonly textContents: XMLTextContent[];
+
+ readonly syntax: {
+ // Is the XML Element self closing or with a distinct closing tag.
+ // - e.g: `` vs ``
+ // This value depends on the existence of the self closing "/>" token
+ // versus the regular closing ">" token of the opening tag.
+ // If none of these tokens exist than this property would not be defined.
+ //
+ // - Note that the a missing `closeName` token is not sufficient to conclude
+ // the element is self closing, e.g when dealing with invalid syntax:
+ // - `>`
+ readonly isSelfClosing?: boolean;
+
+ // Will only exist if there is a valid "Start Name" for the XML Element
+ // - Note this would always exist in a valid XML.
+ readonly openName?: XMLToken;
+
+ // Will only exist if there is a valid "Closing Name" for the XML Element
+ // - Note that this is an **optional** syntax in a valid XML.
+ readonly closeName?: XMLToken;
+
+ // Describes the range of the opening element name + attributes area:
+ // Examples:
+ // Some content
+ // <===== openBody range =====>
+ //
+ // <======= openBody range ===========>
+ //
+ // Will not exist if there is no open '<' or closing '>' | "/>" (self closing) tokens.
+ readonly openBody?: XMLToken;
+
+ // Describes the range of the **closing** element name area:
+ // Example:
+ // Some content
+ // < ------ >
+ //
+ // Will not exist if any of the closing "brackets" are missing
+ // - e.g in a self closing element.
+ readonly closeBody?: XMLToken;
+
+ // Describes the range of the attributes section:
+ // This starts one character **after** the opening name token
+ // and ends one character before the open body closing '>' or self closing '/>
+ // Examples:
+ // Some content
+ // <===== attributesRange =====>
+ //
+ // <===== attributesRange =====>
+ readonly attributesRange?: SourceRange;
+
+ // Same as attributesRange except this property will be used for partially valid
+ // XMLElements when the AstBuilder cannot be certain what is the exact attributes range.
+ // - Only one of the attributeRanges properties may exist at the same time.
+ // - It is possible that in some cases it won't be possible to even guess the attributes range.
+ // In that scenario neither of the properties will exist.
+ readonly guessedAttributesRange?: SourceRange;
+ };
+ readonly position: SourcePosition;
+}
+
+declare interface XMLTextContent {
+ readonly type: "XMLTextContent";
+ readonly parent: XMLElement;
+
+ readonly text: string | InvalidSyntax;
+
+ readonly position: SourcePosition;
+}
+
+declare interface XMLAttributeBase {
+ readonly type: string;
+
+ readonly key: string | InvalidSyntax;
+ // Semantic Value: Would not include the quotes!
+ readonly value: string | InvalidSyntax;
+
+ readonly syntax: {
+ readonly key?: XMLToken;
+ // Original Literal value, would include the quotes
+ readonly value?: XMLToken;
+ };
+ readonly position: SourcePosition;
+}
+
+declare interface XMLAttribute extends XMLAttributeBase {
+ readonly type: "XMLAttribute";
+ readonly parent: XMLElement;
+}
+
+/**
+ * XMLPrologAttribute is virtually identical to a regular `XMLAttribute`.
+ * However it was moved to a separate interface to allow consistent handling of the `parent`
+ * property in `XMLAttribute`, as 99.9% of the time the edge case of a parent being an XMLProlog
+ * is not relevant.
+ */
+declare interface XMLPrologAttribute extends XMLAttributeBase {
+ readonly type: "XMLPrologAttribute";
+ readonly parent: XMLProlog;
+}
+
+declare interface SourceRange {
+ readonly startOffset: number;
+ readonly endOffset: number;
+}
+
+declare interface SourcePosition {
+ readonly startOffset: number;
+ readonly endOffset: number;
+ readonly startLine: number;
+ readonly endLine: number;
+ readonly startColumn: number;
+ readonly endColumn: number;
+}
+
+declare interface XMLToken extends SourcePosition {
+ readonly image: string;
+}
+
+declare type XMLAstNode =
+ | XMLDocument
+ | XMLProlog
+ | XMLPrologAttribute
+ | XMLElement
+ | XMLAttribute
+ | XMLTextContent;
+
+/**
+ * Will Traverse as XML AST and activate the relevant visitor method on each node.
+ * There is no guarantee on the order in which the nodes will be visited.
+ * For greater control of the traversal, one should evaluate implementing a recursive tree walker.
+ */
+declare function accept(node: XMLAstNode, visitor: XMLAstVisitor): void;
+
+/**
+ * Very Simple Visitor API.
+ * Currently No support for IN/OUT params on the visit methods
+ * Which
+ */
+declare interface XMLAstVisitor {
+ visitXMLDocument?(node: XMLDocument): void;
+ visitXMLProlog?(node: XMLProlog): void;
+ visitXMLPrologAttribute?(node: XMLPrologAttribute): void;
+ visitXMLElement?(node: XMLElement): void;
+ visitXMLAttribute?(node: XMLAttribute): void;
+ visitXMLTextContent?(node: XMLTextContent): void;
+}
+
+export type InvalidSyntax = null;
diff --git a/projects/xml-tools/packages/ast/lib/api.js b/projects/xml-tools/packages/ast/lib/api.js
new file mode 100644
index 00000000..3ef2b0bc
--- /dev/null
+++ b/projects/xml-tools/packages/ast/lib/api.js
@@ -0,0 +1,9 @@
+const { buildAst } = require("./build-ast");
+const { accept } = require("./visit-ast");
+const { DEFAULT_NS } = require("./constants");
+
+module.exports = {
+ buildAst: buildAst,
+ accept: accept,
+ DEFAULT_NS: DEFAULT_NS,
+};
diff --git a/projects/xml-tools/packages/ast/lib/build-ast.js b/projects/xml-tools/packages/ast/lib/build-ast.js
new file mode 100644
index 00000000..6346ae0b
--- /dev/null
+++ b/projects/xml-tools/packages/ast/lib/build-ast.js
@@ -0,0 +1,457 @@
+const { BaseXmlCstVisitor } = require("@xml-tools/parser");
+const {
+ last,
+ forEach,
+ reduce,
+ map,
+ pick,
+ sortBy,
+ isEmpty,
+ isArray,
+ assign,
+} = require("lodash");
+const {
+ findNextTextualToken,
+ isXMLNamespaceKey,
+ getXMLNamespaceKeyPrefix,
+} = require("@xml-tools/common");
+
+const { getAstChildrenReflective } = require("./utils");
+const { DEFAULT_NS } = require("./constants");
+
+/**
+ * @param {DocumentCstNode} docCst
+ * @param {IToken[]} tokenVector
+ * @returns {XMLDocument}
+ */
+function buildAst(docCst, tokenVector) {
+ AstBuilder.setState({ tokenVector });
+ const xmlDocAst = AstBuilder.visit(docCst);
+
+ if (xmlDocAst.rootElement !== invalidSyntax) {
+ updateNamespaces(xmlDocAst.rootElement);
+ }
+ return xmlDocAst;
+}
+
+/* eslint-disable no-unused-vars -- consistent signatures in visitor methods even if they are empty placeholders */
+class CstToAstVisitor extends BaseXmlCstVisitor {
+ constructor() {
+ super();
+ }
+
+ setState({ tokenVector }) {
+ this.tokenVector = tokenVector;
+ }
+
+ visit(cstNode, params = {}) {
+ return super.visit(cstNode, { location: cstNode.location, ...params });
+ }
+
+ /**
+ * @param ctx {DocumentCtx}
+ * @param opts {Object}
+ * @param opts.location {SourcePosition}
+ *
+ * @returns {XMLDocument}
+ */
+ document(ctx, { location }) {
+ const astNode = {
+ type: "XMLDocument",
+ rootElement: invalidSyntax,
+ position: location,
+ };
+
+ if (ctx.prolog !== undefined) {
+ astNode.prolog = this.visit(ctx.prolog[0]);
+ }
+
+ if (
+ ctx.element !== undefined &&
+ isEmpty(ctx.element[0].children) === false
+ ) {
+ astNode.rootElement = this.visit(ctx.element[0]);
+ }
+
+ setChildrenParent(astNode);
+
+ return astNode;
+ }
+
+ /**
+ * @param ctx {PrologCtx}
+ * @param opts {Object}
+ * @param opts.location {SourcePosition}
+ */
+ prolog(ctx, { location }) {
+ const astNode = {
+ type: "XMLProlog",
+ attributes: [],
+ position: location,
+ };
+
+ if (ctx.attribute !== undefined) {
+ astNode.attributes = map(ctx.attribute, (_) =>
+ this.visit(_, { isPrologParent: true })
+ );
+ }
+
+ setChildrenParent(astNode);
+
+ return astNode;
+ }
+
+ /**
+ * @param {docTypeDeclCtx} ctx
+ */
+ /* istanbul ignore next - place holder*/
+ docTypeDecl(ctx, astNode) {}
+
+ /**
+ * @param {ExternalIDCtx} ctx
+ */
+ /* istanbul ignore next - place holder*/
+ externalID(ctx, astNode) {}
+
+ /**
+ * @param ctx {ContentCtx}
+ * @param opts {Object}
+ * @param opts.location {SourcePosition}
+ *
+ * @return {{elements, textContents}}
+ */
+ content(ctx, { location }) {
+ let elements = [];
+ let textContents = [];
+
+ if (ctx.element !== undefined) {
+ elements = map(ctx.element, this.visit.bind(this));
+ }
+
+ if (ctx.chardata !== undefined) {
+ textContents = map(ctx.chardata, this.visit.bind(this));
+ }
+
+ return { elements, textContents };
+ }
+
+ /**
+ * @param ctx {ElementCtx}
+ * @param opts {Object}
+ * @param opts.location {SourcePosition}
+ */
+ element(ctx, { location }) {
+ const astNode = {
+ type: "XMLElement",
+ // Avoid Accidental Keys in this map
+ namespaces: Object.create(null),
+ name: invalidSyntax,
+ attributes: [],
+ subElements: [],
+ textContents: [],
+ position: location,
+ syntax: {},
+ };
+
+ if (ctx.attribute !== undefined) {
+ astNode.attributes = map(ctx.attribute, this.visit.bind(this));
+ }
+
+ if (ctx.content !== undefined) {
+ const { elements, textContents } = this.visit(ctx.content[0]);
+ astNode.subElements = elements;
+ astNode.textContents = textContents;
+ }
+
+ handleElementOpenCloseNameRanges(astNode, ctx);
+ handleElementOpenCloseBodyRanges(astNode, ctx);
+ handleElementAttributeRanges(astNode, ctx, this.tokenVector);
+
+ setChildrenParent(astNode);
+
+ return astNode;
+ }
+
+ /**
+ * @param ctx {ReferenceCtx}
+ * @param opts {Object}
+ * @param opts.location {SourcePosition}
+ */
+ /* istanbul ignore next - place holder*/
+ reference(ctx, { location }) {
+ // Irrelevant for the AST at this time
+ }
+
+ /**
+ * @param ctx {AttributeCtx}
+ * @param opts {Object}
+ * @param opts.location {SourcePosition}
+ * @param opts.isPrologParent {boolean}
+ */
+ attribute(ctx, { location, isPrologParent }) {
+ const astNode = {
+ type: isPrologParent ? "XMLPrologAttribute" : "XMLAttribute",
+ position: location,
+ key: invalidSyntax,
+ value: invalidSyntax,
+ syntax: {},
+ };
+
+ /* istanbul ignore else - Defensive Coding, not actually possible else branch */
+ if (ctx.Name !== undefined && ctx.Name[0].isInsertedInRecovery !== true) {
+ const keyToken = ctx.Name[0];
+ astNode.key = keyToken.image;
+ astNode.syntax.key = toXMLToken(keyToken);
+ }
+
+ if (
+ ctx.STRING !== undefined &&
+ ctx.STRING[0].isInsertedInRecovery !== true
+ ) {
+ const valueToken = ctx.STRING[0];
+ astNode.value = stripQuotes(valueToken.image);
+ astNode.syntax.value = toXMLToken(valueToken);
+ }
+
+ setChildrenParent(astNode);
+
+ return astNode;
+ }
+
+ /**
+ * @param ctx {ChardataCtx}
+ * @param opts {Object}
+ * @param opts.location {SourcePosition}
+ */
+ chardata(ctx, { location }) {
+ const astNode = {
+ type: "XMLTextContent",
+ position: location,
+ text: invalidSyntax,
+ };
+
+ let allTokens = [];
+ if (ctx.SEA_WS !== undefined) {
+ allTokens = allTokens.concat(ctx.SEA_WS);
+ }
+ if (ctx.TEXT !== undefined) {
+ allTokens = allTokens.concat(ctx.TEXT);
+ }
+ const sortedTokens = sortBy(allTokens, ["startOffset"]);
+ const fullText = map(sortedTokens, "image").join("");
+ astNode.text = fullText;
+
+ return astNode;
+ }
+
+ /**
+ * @param ctx {MiscCtx}
+ * @param opts {Object}
+ * @param opts.location {SourcePosition}
+ */
+ /* istanbul ignore next - place holder*/
+ misc(ctx, { location }) {
+ // Irrelevant for the AST at this time
+ }
+}
+/* eslint-enable no-unused-vars -- see matching disable comment */
+
+const AstBuilder = new CstToAstVisitor();
+
+function setChildrenParent(astParent) {
+ const astChildren = getAstChildrenReflective(astParent);
+ forEach(astChildren, (child) => (child.parent = astParent));
+}
+
+/**
+ * @param {XMLElement} element
+ * @param {Record} prevNamespaces
+ */
+function updateNamespaces(element, prevNamespaces = []) {
+ const currElemNamespaces = reduce(
+ element.attributes,
+ (result, attrib) => {
+ /* istanbul ignore else - Defensive Coding, not actually possible branch */
+ if (attrib.key !== invalidSyntax) {
+ if (
+ isXMLNamespaceKey({ key: attrib.key, includeEmptyPrefix: false }) ===
+ true
+ ) {
+ const prefix = getXMLNamespaceKeyPrefix(attrib.key);
+ // TODO: Support un-defining namespaces (including the default one)
+ if (attrib.value) {
+ const uri = attrib.value;
+ if (prefix !== "") {
+ result[prefix] = uri;
+ } else {
+ // default namespace
+ result[DEFAULT_NS] = uri;
+ }
+ }
+ }
+ }
+
+ return result;
+ },
+ {}
+ );
+
+ const emptyMap = Object.create(null);
+ // "newer" (closer scope) namespaces definitions will overwrite "older" ones.
+ element.namespaces = assign(emptyMap, prevNamespaces, currElemNamespaces);
+
+ forEach(element.subElements, (subElem) =>
+ updateNamespaces(subElem, element.namespaces)
+ );
+}
+
+/**
+ * @param {chevrotain.IToken} token
+ */
+function toXMLToken(token) {
+ return pick(token, [
+ "image",
+ "startOffset",
+ "endOffset",
+ "startLine",
+ "endLine",
+ "startColumn",
+ "endColumn",
+ ]);
+}
+
+function startOfXMLToken(token) {
+ return pick(token, ["startOffset", "startLine", "startColumn"]);
+}
+
+function endOfXMLToken(token) {
+ return pick(token, ["endOffset", "endLine", "endColumn"]);
+}
+
+function exists(tokArr) {
+ return (
+ isArray(tokArr) &&
+ tokArr.length === 1 &&
+ tokArr[0].isInsertedInRecovery !== true
+ );
+}
+
+function stripQuotes(quotedText) {
+ return quotedText.substring(1, quotedText.length - 1);
+}
+
+/**
+ * @param {string} text
+ */
+function nsToParts(text) {
+ const matchResult = /^([^:]+):([^:]+)$/.exec(text);
+ if (matchResult === null) {
+ return null;
+ }
+ const ns = matchResult[1];
+ const name = matchResult[2];
+ return { ns, name };
+}
+
+/**
+ * @type {InvalidSyntax}
+ */
+const invalidSyntax = null;
+
+/**
+ * @param {XMLElement} astNode
+ * @param {ElementCtx} ctx
+ */
+function handleElementOpenCloseNameRanges(astNode, ctx) {
+ if (ctx.Name !== undefined && ctx.Name[0].isInsertedInRecovery !== true) {
+ const openNameToken = ctx.Name[0];
+ astNode.syntax.openName = toXMLToken(openNameToken);
+ const nsParts = nsToParts(openNameToken.image);
+ if (nsParts !== null) {
+ astNode.ns = nsParts.ns;
+ astNode.name = nsParts.name;
+ } else {
+ astNode.name = openNameToken.image;
+ }
+ }
+
+ if (
+ ctx.END_NAME !== undefined &&
+ ctx.END_NAME[0].isInsertedInRecovery !== true
+ ) {
+ astNode.syntax.closeName = toXMLToken(ctx.END_NAME[0]);
+ }
+}
+
+/**
+ * @param {XMLElement} astNode
+ * @param {ElementCtx} ctx
+ */
+function handleElementOpenCloseBodyRanges(astNode, ctx) {
+ /* istanbul ignore else - Defensive Coding */
+ if (exists(ctx.OPEN)) {
+ let openBodyCloseTok = undefined;
+ /* istanbul ignore else - Defensive Coding */
+ if (exists(ctx.START_CLOSE)) {
+ openBodyCloseTok = ctx.START_CLOSE[0];
+ astNode.syntax.isSelfClosing = false;
+ } else if (exists(ctx.SLASH_CLOSE)) {
+ openBodyCloseTok = ctx.SLASH_CLOSE[0];
+ astNode.syntax.isSelfClosing = true;
+ }
+
+ if (openBodyCloseTok !== undefined) {
+ astNode.syntax.openBody = {
+ ...startOfXMLToken(ctx.OPEN[0]),
+ ...endOfXMLToken(openBodyCloseTok),
+ };
+ }
+
+ if (exists(ctx.SLASH_OPEN) && exists(ctx.END)) {
+ astNode.syntax.closeBody = {
+ ...startOfXMLToken(ctx.SLASH_OPEN[0]),
+ ...endOfXMLToken(ctx.END[0]),
+ };
+ }
+ }
+}
+
+/**
+ * @param {XMLElement} astNode
+ * @param {ElementCtx} ctx
+ * @param {IToken[]} tokenVector
+ */
+function handleElementAttributeRanges(astNode, ctx, tokenVector) {
+ if (exists(ctx.Name)) {
+ const startOffset = ctx.Name[0].endOffset + 2;
+ // Valid `attributesRange` exists
+ if (exists(ctx.START_CLOSE) || exists(ctx.SLASH_CLOSE)) {
+ const endOffset =
+ (exists(ctx.START_CLOSE)
+ ? ctx.START_CLOSE[0].startOffset
+ : ctx.SLASH_CLOSE[0].startOffset) - 1;
+ astNode.syntax.attributesRange = { startOffset, endOffset };
+ }
+ // Have to scan-ahead and guess where the attributes range ends
+ else {
+ const hasAttributes = isArray(ctx.attribute);
+ const lastKnownAttribRangeTokenEnd = hasAttributes
+ ? last(ctx.attribute).location.endOffset
+ : ctx.Name[0].endOffset;
+ const nextTextualToken = findNextTextualToken(
+ tokenVector,
+ lastKnownAttribRangeTokenEnd
+ );
+ if (nextTextualToken !== null) {
+ astNode.syntax.guessedAttributesRange = {
+ startOffset,
+ endOffset: nextTextualToken.endOffset - 1,
+ };
+ }
+ }
+ }
+}
+
+module.exports = {
+ buildAst: buildAst,
+};
diff --git a/projects/xml-tools/packages/ast/lib/constants.js b/projects/xml-tools/packages/ast/lib/constants.js
new file mode 100644
index 00000000..7fb0bae3
--- /dev/null
+++ b/projects/xml-tools/packages/ast/lib/constants.js
@@ -0,0 +1,3 @@
+module.exports = {
+ DEFAULT_NS: "::DEFAULT",
+};
diff --git a/projects/xml-tools/packages/ast/lib/utils.js b/projects/xml-tools/packages/ast/lib/utils.js
new file mode 100644
index 00000000..27c8ee20
--- /dev/null
+++ b/projects/xml-tools/packages/ast/lib/utils.js
@@ -0,0 +1,25 @@
+const { reduce, has, isArray } = require("lodash");
+
+function getAstChildrenReflective(astParent) {
+ const astChildren = reduce(
+ astParent,
+ (result, prop, name) => {
+ if (name === "parent") {
+ // parent property is never a child...
+ } else if (has(prop, "type")) {
+ result.push(prop);
+ } else if (isArray(prop) && prop.length > 0 && has(prop[0], "type")) {
+ result = result.concat(prop);
+ }
+
+ return result;
+ },
+ []
+ );
+
+ return astChildren;
+}
+
+module.exports = {
+ getAstChildrenReflective: getAstChildrenReflective,
+};
diff --git a/projects/xml-tools/packages/ast/lib/visit-ast.js b/projects/xml-tools/packages/ast/lib/visit-ast.js
new file mode 100644
index 00000000..2ec6b0f7
--- /dev/null
+++ b/projects/xml-tools/packages/ast/lib/visit-ast.js
@@ -0,0 +1,61 @@
+const { forEach, isFunction } = require("lodash");
+const { getAstChildrenReflective } = require("./utils");
+
+/**
+ * @param {XMLAstNode} node
+ * @param {XMLAstVisitor} visitor
+ *
+ * @returns {void}
+ */
+function accept(node, visitor) {
+ switch (node.type) {
+ case "XMLDocument": {
+ if (isFunction(visitor.visitXMLDocument)) {
+ visitor.visitXMLDocument(node);
+ }
+ break;
+ }
+ case "XMLProlog": {
+ if (isFunction(visitor.visitXMLProlog)) {
+ visitor.visitXMLProlog(node);
+ }
+ break;
+ }
+ case "XMLPrologAttribute": {
+ if (isFunction(visitor.visitXMLPrologAttribute)) {
+ visitor.visitXMLPrologAttribute(node);
+ }
+ break;
+ }
+ case "XMLElement": {
+ if (isFunction(visitor.visitXMLElement)) {
+ visitor.visitXMLElement(node);
+ }
+ break;
+ }
+ case "XMLAttribute": {
+ if (isFunction(visitor.visitXMLAttribute)) {
+ visitor.visitXMLAttribute(node);
+ }
+ break;
+ }
+ case "XMLTextContent": {
+ if (isFunction(visitor.visitXMLTextContent)) {
+ visitor.visitXMLTextContent(node);
+ }
+ break;
+ }
+ /* istanbul ignore next defensive programming */
+ default:
+ throw Error("None Exhaustive Match");
+ }
+
+ const astChildren = getAstChildrenReflective(node);
+ forEach(astChildren, (childNode) => {
+ accept(childNode, visitor);
+ });
+}
+
+module.exports = {
+ accept: accept,
+};
diff --git a/projects/xml-tools/packages/ast/nyc.config.js b/projects/xml-tools/packages/ast/nyc.config.js
new file mode 100644
index 00000000..3e3f8962
--- /dev/null
+++ b/projects/xml-tools/packages/ast/nyc.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ // xml-tools sources live in lib/ (not src/), so this local config overrides
+ // the repo-root nyc.config.js include of **/src/** (which would report 0%).
+ include: ["lib/**/*.js"],
+ reporter: ["text", "lcov"],
+ all: true,
+ branches: 100,
+ lines: 100,
+ functions: 100,
+ statements: 100,
+};
diff --git a/projects/xml-tools/packages/ast/package.json b/projects/xml-tools/packages/ast/package.json
new file mode 100644
index 00000000..60a4d0fc
--- /dev/null
+++ b/projects/xml-tools/packages/ast/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@xml-tools/ast",
+ "version": "5.1.0",
+ "description": "XML Ast and Utilities",
+ "keywords": [
+ "xml",
+ "ast"
+ ],
+ "main": "lib/api.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/SAP/app-studio-toolkit.git",
+ "directory": "projects/xml-tools/packages/ast"
+ },
+ "license": "Apache-2.0",
+ "typings": "./api.d.ts",
+ "files": [
+ "lib",
+ "api.d.ts",
+ "CHANGELOG.md",
+ "CHANGELOG.old.md"
+ ],
+ "dependencies": {
+ "@xml-tools/common": "workspace:*",
+ "@xml-tools/parser": "workspace:*",
+ "chevrotain": "7.1.1",
+ "lodash": "4.17.23"
+ },
+ "devDependencies": {
+ "klaw-sync": "6.0.0"
+ },
+ "scripts": {
+ "ci": "npm-run-all clean type-check coverage:*",
+ "clean": "rimraf ./coverage ./nyc_output",
+ "test": "mocha \"./test/**/*spec.js\"",
+ "coverage:run": "nyc mocha \"./test/**/*spec.js\"",
+ "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100",
+ "snapshots:update": "node ./scripts/update-snapshots.js",
+ "type-check": "tsc"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "bugs": {
+ "url": "https://github.com/SAP/app-studio-toolkit/issues"
+ }
+}
diff --git a/projects/xml-tools/packages/ast/scripts/update-snapshots.js b/projects/xml-tools/packages/ast/scripts/update-snapshots.js
new file mode 100644
index 00000000..81349ce9
--- /dev/null
+++ b/projects/xml-tools/packages/ast/scripts/update-snapshots.js
@@ -0,0 +1,32 @@
+"use strict";
+const { parse } = require("@xml-tools/parser");
+const { modifyAstForAssertions } = require("../test/utils");
+const klawSync = require("klaw-sync");
+const { resolve } = require("path");
+const { readFileSync, writeFileSync } = require("fs");
+const { format } = require("prettier");
+
+const testsDir = resolve(__dirname, "..", "test");
+const { buildAst } = require("../");
+
+const sampleFiles = klawSync(testsDir, { nodir: true });
+const xmlSampleFiles = sampleFiles.filter((fileDesc) => {
+ if (fileDesc.path.includes("node_modules")) {
+ return false;
+ }
+ return fileDesc.path.endsWith("input.xml");
+});
+
+xmlSampleFiles.forEach((fileDesc) => {
+ const xmlInput = readFileSync(fileDesc.path, "utf8");
+ const simpleNewLinesInput = xmlInput.replace(/\r\n/g, "\n");
+ console.log(`Reading <${fileDesc.path}>`);
+ const { cst, tokenVector } = parse(simpleNewLinesInput);
+ const ast = buildAst(cst, tokenVector);
+ modifyAstForAssertions(ast);
+ const snapshotOutput = `module.exports = { ast : ${JSON.stringify(ast)}}`;
+ const formattedSnapshotOutput = format(snapshotOutput, { parser: "babel" });
+ const outputFilePath = fileDesc.path.replace(/input.xml$/, "output.js");
+ console.log(`writing <${outputFilePath}>`);
+ writeFileSync(outputFilePath, formattedSnapshotOutput);
+});
diff --git a/projects/xml-tools/packages/ast/test/sample-test.js b/projects/xml-tools/packages/ast/test/sample-test.js
new file mode 100644
index 00000000..96a45d43
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/sample-test.js
@@ -0,0 +1,40 @@
+const { expect } = require("chai");
+const { readFileSync } = require("fs");
+const { resolve, basename } = require("path");
+const { parse } = require("@xml-tools/parser");
+const { partialRight } = require("lodash");
+
+const {
+ modifyAstForAssertions,
+ assertParentPropsAreValid,
+} = require("./utils");
+const { buildAst } = require("../");
+
+function executeSampleTest(dirPath, assertNoErrors) {
+ it("Can build an AST for a valid XML", () => {
+ const inputPath = resolve(dirPath, "input.xml");
+ const inputText = readFileSync(inputPath).toString("utf8");
+ const simpleNewLinesInput = inputText.replace(/\r\n/g, "\n");
+ const { cst, tokenVector, lexErrors, parseErrors } =
+ parse(simpleNewLinesInput);
+ if (assertNoErrors === true) {
+ expect(lexErrors).to.be.empty;
+ expect(parseErrors).to.be.empty;
+ }
+ const ast = buildAst(cst, tokenVector);
+ assertParentPropsAreValid(ast);
+ modifyAstForAssertions(ast);
+ const expectedOutput = require(resolve(dirPath, "output.js")).ast;
+ expect(ast).to.deep.eql(expectedOutput);
+ });
+}
+
+function testNameFromDir(dirPath) {
+ return basename(dirPath);
+}
+
+module.exports = {
+ executeValidSampleTest: partialRight(executeSampleTest, true),
+ executeInValidSampleTest: partialRight(executeSampleTest, false),
+ testNameFromDir: testNameFromDir,
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-last/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-last/input.xml
new file mode 100644
index 00000000..9b28ba6a
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-last/input.xml
@@ -0,0 +1 @@
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-last/output.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-last/output.js
new file mode 100644
index 00000000..80abddc4
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-last/output.js
@@ -0,0 +1,51 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 6, endOffset: 16 },
+ key: "attr1",
+ value: "666",
+ syntax: {
+ key: { image: "attr1", startOffset: 6, endOffset: 10 },
+ value: { image: '"666"', startOffset: 12, endOffset: 16 },
+ },
+ },
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 18, endOffset: 28 },
+ key: "attr2",
+ value: "333",
+ syntax: {
+ key: { image: "attr2", startOffset: 18, endOffset: 22 },
+ value: { image: '"333"', startOffset: 24, endOffset: 28 },
+ },
+ },
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 30, endOffset: 35 },
+ key: "attr3",
+ value: null,
+ syntax: { key: { image: "attr3", startOffset: 30, endOffset: 34 } },
+ },
+ ],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 0, endOffset: 44 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 40, endOffset: 43 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 37 },
+ closeBody: { startOffset: 38, endOffset: 44 },
+ attributesRange: { startOffset: 6, endOffset: 36 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 45 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-last/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-last/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-last/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-middle/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-middle/input.xml
new file mode 100644
index 00000000..e8b46b55
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-middle/input.xml
@@ -0,0 +1 @@
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-middle/output.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-middle/output.js
new file mode 100644
index 00000000..20f6d585
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-middle/output.js
@@ -0,0 +1,51 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 6, endOffset: 16 },
+ key: "attr1",
+ value: "666",
+ syntax: {
+ key: { image: "attr1", startOffset: 6, endOffset: 10 },
+ value: { image: '"666"', startOffset: 12, endOffset: 16 },
+ },
+ },
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 18, endOffset: 23 },
+ key: "attr2",
+ value: null,
+ syntax: { key: { image: "attr2", startOffset: 18, endOffset: 22 } },
+ },
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 25, endOffset: 35 },
+ key: "attr3",
+ value: "111",
+ syntax: {
+ key: { image: "attr3", startOffset: 25, endOffset: 29 },
+ value: { image: '"111"', startOffset: 31, endOffset: 35 },
+ },
+ },
+ ],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 0, endOffset: 44 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 40, endOffset: 43 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 37 },
+ closeBody: { startOffset: 38, endOffset: 44 },
+ attributesRange: { startOffset: 6, endOffset: 36 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 45 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-middle/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-middle/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-middle/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-last/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-last/input.xml
new file mode 100644
index 00000000..48e91654
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-last/input.xml
@@ -0,0 +1 @@
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-last/output.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-last/output.js
new file mode 100644
index 00000000..f17d2ec2
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-last/output.js
@@ -0,0 +1,51 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 6, endOffset: 16 },
+ key: "attr1",
+ value: "666",
+ syntax: {
+ key: { image: "attr1", startOffset: 6, endOffset: 10 },
+ value: { image: '"666"', startOffset: 12, endOffset: 16 },
+ },
+ },
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 18, endOffset: 28 },
+ key: "attr2",
+ value: "333",
+ syntax: {
+ key: { image: "attr2", startOffset: 18, endOffset: 22 },
+ value: { image: '"333"', startOffset: 24, endOffset: 28 },
+ },
+ },
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 30, endOffset: 34 },
+ key: "attr3",
+ value: null,
+ syntax: { key: { image: "attr3", startOffset: 30, endOffset: 34 } },
+ },
+ ],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 0, endOffset: 43 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 39, endOffset: 42 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 36 },
+ closeBody: { startOffset: 37, endOffset: 43 },
+ attributesRange: { startOffset: 6, endOffset: 35 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 44 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-last/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-last/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-last/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-middle/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-middle/input.xml
new file mode 100644
index 00000000..a1a9f34d
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-middle/input.xml
@@ -0,0 +1 @@
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-middle/output.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-middle/output.js
new file mode 100644
index 00000000..0ea3997c
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-middle/output.js
@@ -0,0 +1,51 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 6, endOffset: 16 },
+ key: "attr1",
+ value: "666",
+ syntax: {
+ key: { image: "attr1", startOffset: 6, endOffset: 10 },
+ value: { image: '"666"', startOffset: 12, endOffset: 16 },
+ },
+ },
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 18, endOffset: 22 },
+ key: "attr2",
+ value: null,
+ syntax: { key: { image: "attr2", startOffset: 18, endOffset: 22 } },
+ },
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 24, endOffset: 34 },
+ key: "attr3",
+ value: "111",
+ syntax: {
+ key: { image: "attr3", startOffset: 24, endOffset: 28 },
+ value: { image: '"111"', startOffset: 30, endOffset: 34 },
+ },
+ },
+ ],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 0, endOffset: 43 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 39, endOffset: 42 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 36 },
+ closeBody: { startOffset: 37, endOffset: 43 },
+ attributesRange: { startOffset: 6, endOffset: 35 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 44 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-middle/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-middle/sample.spec.js
new file mode 100644
index 00000000..8d7c77f8
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-middle/sample.spec.js
@@ -0,0 +1,9 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+
+// TODO: should we disable single token deletion in this case?
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-not-closed-nested/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-not-closed-nested/input.xml
new file mode 100644
index 00000000..d8b56c02
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-not-closed-nested/input.xml
@@ -0,0 +1,5 @@
+
+ hello world
+
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-not-closed-nested/output.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-not-closed-nested/output.js
new file mode 100644
index 00000000..2c4b8874
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-not-closed-nested/output.js
@@ -0,0 +1,130 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "top",
+ attributes: [],
+ subElements: [
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "nested",
+ attributes: [
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 18, endOffset: 28 },
+ key: "attr1",
+ value: "666",
+ syntax: {
+ key: { image: "attr1", startOffset: 18, endOffset: 22 },
+ value: { image: '"666"', startOffset: 24, endOffset: 28 },
+ },
+ },
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 30, endOffset: 35 },
+ key: "attr2",
+ value: null,
+ syntax: {
+ key: { image: "attr2", startOffset: 30, endOffset: 34 },
+ },
+ },
+ ],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 10, endOffset: 35 },
+ syntax: {
+ openName: { image: "nested", startOffset: 11, endOffset: 16 },
+ guessedAttributesRange: { startOffset: 18, endOffset: 40 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "nest2",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 48, endOffset: 58 },
+ text: "hello world",
+ },
+ ],
+ position: { startOffset: 41, endOffset: 66 },
+ syntax: {
+ openName: { image: "nest2", startOffset: 42, endOffset: 46 },
+ closeName: { image: "nest2", startOffset: 61, endOffset: 65 },
+ isSelfClosing: false,
+ openBody: { startOffset: 41, endOffset: 47 },
+ closeBody: { startOffset: 59, endOffset: 66 },
+ attributesRange: { startOffset: 48, endOffset: 46 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "nested3",
+ attributes: [
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 81, endOffset: 89 },
+ key: "attr5",
+ value: "1",
+ syntax: {
+ key: { image: "attr5", startOffset: 81, endOffset: 85 },
+ value: { image: '"1"', startOffset: 87, endOffset: 89 },
+ },
+ },
+ ],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 91, endOffset: 91 },
+ text: " ",
+ },
+ ],
+ position: { startOffset: 72, endOffset: 101 },
+ syntax: {
+ openName: { image: "nested3", startOffset: 73, endOffset: 79 },
+ closeName: { image: "nested3", startOffset: 94, endOffset: 100 },
+ isSelfClosing: false,
+ openBody: { startOffset: 72, endOffset: 90 },
+ closeBody: { startOffset: 92, endOffset: 101 },
+ attributesRange: { startOffset: 81, endOffset: 89 },
+ },
+ },
+ ],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 5, endOffset: 9 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 67, endOffset: 71 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 102, endOffset: 102 },
+ text: "\n",
+ },
+ ],
+ position: { startOffset: 0, endOffset: 108 },
+ syntax: {
+ openName: { image: "top", startOffset: 1, endOffset: 3 },
+ closeName: { image: "top", startOffset: 105, endOffset: 107 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 4 },
+ closeBody: { startOffset: 103, endOffset: 108 },
+ attributesRange: { startOffset: 5, endOffset: 3 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 109 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-not-closed-nested/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-not-closed-nested/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-not-closed-nested/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-not-closed/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-not-closed/input.xml
new file mode 100644
index 00000000..caf98d53
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/attributes-no-value-not-closed/input.xml
@@ -0,0 +1 @@
+ {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/bad-namespace/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/bad-namespace/input.xml
new file mode 100644
index 00000000..96a3921e
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/bad-namespace/input.xml
@@ -0,0 +1,6 @@
+
+
+ Some Chair
+ 50
+ 67
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/bad-namespace/output.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/bad-namespace/output.js
new file mode 100644
index 00000000..9804b264
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/bad-namespace/output.js
@@ -0,0 +1,126 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "table",
+ attributes: [
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 10, endOffset: 17 },
+ key: "xmlns:f",
+ value: null,
+ syntax: { key: { image: "xmlns:f", startOffset: 10, endOffset: 16 } },
+ },
+ ],
+ subElements: [
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "name",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 32, endOffset: 41 },
+ text: "Some Chair",
+ },
+ ],
+ position: { startOffset: 24, endOffset: 50 },
+ syntax: {
+ openName: { image: "f:name", startOffset: 25, endOffset: 30 },
+ closeName: { image: "f:name", startOffset: 44, endOffset: 49 },
+ isSelfClosing: false,
+ openBody: { startOffset: 24, endOffset: 31 },
+ closeBody: { startOffset: 42, endOffset: 50 },
+ attributesRange: { startOffset: 32, endOffset: 30 },
+ },
+ ns: "f",
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "width",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 65, endOffset: 66 },
+ text: "50",
+ },
+ ],
+ position: { startOffset: 56, endOffset: 76 },
+ syntax: {
+ openName: { image: "f:width", startOffset: 57, endOffset: 63 },
+ closeName: { image: "f:width", startOffset: 69, endOffset: 75 },
+ isSelfClosing: false,
+ openBody: { startOffset: 56, endOffset: 64 },
+ closeBody: { startOffset: 67, endOffset: 76 },
+ attributesRange: { startOffset: 65, endOffset: 63 },
+ },
+ ns: "f",
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "length",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 92, endOffset: 93 },
+ text: "67",
+ },
+ ],
+ position: { startOffset: 82, endOffset: 104 },
+ syntax: {
+ openName: { image: "f:length", startOffset: 83, endOffset: 90 },
+ closeName: { image: "f:length", startOffset: 96, endOffset: 103 },
+ isSelfClosing: false,
+ openBody: { startOffset: 82, endOffset: 91 },
+ closeBody: { startOffset: 94, endOffset: 104 },
+ attributesRange: { startOffset: 92, endOffset: 90 },
+ },
+ ns: "f",
+ },
+ ],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 19, endOffset: 23 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 51, endOffset: 55 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 77, endOffset: 81 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 105, endOffset: 105 },
+ text: "\n",
+ },
+ ],
+ position: { startOffset: 1, endOffset: 115 },
+ syntax: {
+ openName: { image: "f:table", startOffset: 2, endOffset: 8 },
+ closeName: { image: "f:table", startOffset: 108, endOffset: 114 },
+ isSelfClosing: false,
+ openBody: { startOffset: 1, endOffset: 18 },
+ closeBody: { startOffset: 106, endOffset: 115 },
+ attributesRange: { startOffset: 10, endOffset: 17 },
+ },
+ ns: "f",
+ },
+ position: { startOffset: 0, endOffset: 116 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/bad-namespace/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/bad-namespace/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/bad-namespace/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-last/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-last/input.xml
new file mode 100644
index 00000000..92dceccd
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-last/input.xml
@@ -0,0 +1,5 @@
+
+ bobi
+ john
+ <
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-last/output.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-last/output.js
new file mode 100644
index 00000000..fef24c50
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-last/output.js
@@ -0,0 +1,96 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [],
+ subElements: [
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "to",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 15, endOffset: 18 },
+ text: "bobi",
+ },
+ ],
+ position: { startOffset: 11, endOffset: 23 },
+ syntax: {
+ openName: { image: "to", startOffset: 12, endOffset: 13 },
+ closeName: { image: "to", startOffset: 21, endOffset: 22 },
+ isSelfClosing: false,
+ openBody: { startOffset: 11, endOffset: 14 },
+ closeBody: { startOffset: 19, endOffset: 23 },
+ attributesRange: { startOffset: 15, endOffset: 13 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "from",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 35, endOffset: 38 },
+ text: "john",
+ },
+ ],
+ position: { startOffset: 29, endOffset: 45 },
+ syntax: {
+ openName: { image: "from", startOffset: 30, endOffset: 33 },
+ closeName: { image: "from", startOffset: 41, endOffset: 44 },
+ isSelfClosing: false,
+ openBody: { startOffset: 29, endOffset: 34 },
+ closeBody: { startOffset: 39, endOffset: 45 },
+ attributesRange: { startOffset: 35, endOffset: 33 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: null,
+ attributes: [],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 51, endOffset: 51 },
+ syntax: {},
+ },
+ ],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 6, endOffset: 10 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 24, endOffset: 28 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 46, endOffset: 50 },
+ text: "\n ",
+ },
+ ],
+ position: { startOffset: 0, endOffset: 59 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 55, endOffset: 58 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 5 },
+ closeBody: { startOffset: 53, endOffset: 59 },
+ attributesRange: { startOffset: 6, endOffset: 4 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 60 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-last/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-last/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-last/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle-comment/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle-comment/input.xml
new file mode 100644
index 00000000..925ade84
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle-comment/input.xml
@@ -0,0 +1,6 @@
+
+ bobi
+ <
+
+ john
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle-comment/output.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle-comment/output.js
new file mode 100644
index 00000000..6c8431e3
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle-comment/output.js
@@ -0,0 +1,96 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [],
+ subElements: [
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "to",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 15, endOffset: 18 },
+ text: "bobi",
+ },
+ ],
+ position: { startOffset: 11, endOffset: 23 },
+ syntax: {
+ openName: { image: "to", startOffset: 12, endOffset: 13 },
+ closeName: { image: "to", startOffset: 21, endOffset: 22 },
+ isSelfClosing: false,
+ openBody: { startOffset: 11, endOffset: 14 },
+ closeBody: { startOffset: 19, endOffset: 23 },
+ attributesRange: { startOffset: 15, endOffset: 13 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: null,
+ attributes: [],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 29, endOffset: 29 },
+ syntax: {},
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "from",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 70, endOffset: 73 },
+ text: "john",
+ },
+ ],
+ position: { startOffset: 64, endOffset: 80 },
+ syntax: {
+ openName: { image: "from", startOffset: 65, endOffset: 68 },
+ closeName: { image: "from", startOffset: 76, endOffset: 79 },
+ isSelfClosing: false,
+ openBody: { startOffset: 64, endOffset: 69 },
+ closeBody: { startOffset: 74, endOffset: 80 },
+ attributesRange: { startOffset: 70, endOffset: 68 },
+ },
+ },
+ ],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 6, endOffset: 10 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 24, endOffset: 28 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 81, endOffset: 81 },
+ text: "\n",
+ },
+ ],
+ position: { startOffset: 0, endOffset: 88 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 84, endOffset: 87 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 5 },
+ closeBody: { startOffset: 82, endOffset: 88 },
+ attributesRange: { startOffset: 6, endOffset: 4 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 89 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle-comment/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle-comment/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle-comment/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle/input.xml
new file mode 100644
index 00000000..9d4efe38
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle/input.xml
@@ -0,0 +1,5 @@
+
+ bobi
+ <
+ john
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle/output.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle/output.js
new file mode 100644
index 00000000..e43d6870
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle/output.js
@@ -0,0 +1,96 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [],
+ subElements: [
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "to",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 15, endOffset: 18 },
+ text: "bobi",
+ },
+ ],
+ position: { startOffset: 11, endOffset: 23 },
+ syntax: {
+ openName: { image: "to", startOffset: 12, endOffset: 13 },
+ closeName: { image: "to", startOffset: 21, endOffset: 22 },
+ isSelfClosing: false,
+ openBody: { startOffset: 11, endOffset: 14 },
+ closeBody: { startOffset: 19, endOffset: 23 },
+ attributesRange: { startOffset: 15, endOffset: 13 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: null,
+ attributes: [],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 29, endOffset: 29 },
+ syntax: {},
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "from",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 41, endOffset: 44 },
+ text: "john",
+ },
+ ],
+ position: { startOffset: 35, endOffset: 51 },
+ syntax: {
+ openName: { image: "from", startOffset: 36, endOffset: 39 },
+ closeName: { image: "from", startOffset: 47, endOffset: 50 },
+ isSelfClosing: false,
+ openBody: { startOffset: 35, endOffset: 40 },
+ closeBody: { startOffset: 45, endOffset: 51 },
+ attributesRange: { startOffset: 41, endOffset: 39 },
+ },
+ },
+ ],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 6, endOffset: 10 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 24, endOffset: 28 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 52, endOffset: 52 },
+ text: "\n",
+ },
+ ],
+ position: { startOffset: 0, endOffset: 59 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 55, endOffset: 58 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 5 },
+ closeBody: { startOffset: 53, endOffset: 59 },
+ attributesRange: { startOffset: 6, endOffset: 4 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 60 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-empty-middle/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-last/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-last/input.xml
new file mode 100644
index 00000000..b0036ccc
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-last/input.xml
@@ -0,0 +1,5 @@
+
+ bobi
+ john
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-last/output.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-last/output.js
new file mode 100644
index 00000000..dd465c41
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-last/output.js
@@ -0,0 +1,99 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [],
+ subElements: [
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "to",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 15, endOffset: 18 },
+ text: "bobi",
+ },
+ ],
+ position: { startOffset: 11, endOffset: 23 },
+ syntax: {
+ openName: { image: "to", startOffset: 12, endOffset: 13 },
+ closeName: { image: "to", startOffset: 21, endOffset: 22 },
+ isSelfClosing: false,
+ openBody: { startOffset: 11, endOffset: 14 },
+ closeBody: { startOffset: 19, endOffset: 23 },
+ attributesRange: { startOffset: 15, endOffset: 13 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "from",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 35, endOffset: 38 },
+ text: "john",
+ },
+ ],
+ position: { startOffset: 29, endOffset: 45 },
+ syntax: {
+ openName: { image: "from", startOffset: 30, endOffset: 33 },
+ closeName: { image: "from", startOffset: 41, endOffset: 44 },
+ isSelfClosing: false,
+ openBody: { startOffset: 29, endOffset: 34 },
+ closeBody: { startOffset: 39, endOffset: 45 },
+ attributesRange: { startOffset: 35, endOffset: 33 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "ad",
+ attributes: [],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 51, endOffset: 53 },
+ syntax: {
+ openName: { image: "ad", startOffset: 52, endOffset: 53 },
+ guessedAttributesRange: { startOffset: 55, endOffset: 55 },
+ },
+ },
+ ],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 6, endOffset: 10 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 24, endOffset: 28 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 46, endOffset: 50 },
+ text: "\n ",
+ },
+ ],
+ position: { startOffset: 0, endOffset: 61 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 57, endOffset: 60 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 5 },
+ closeBody: { startOffset: 55, endOffset: 61 },
+ attributesRange: { startOffset: 6, endOffset: 4 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 62 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-last/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-last/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-last/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-middle/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-middle/input.xml
new file mode 100644
index 00000000..1955ad5e
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-middle/input.xml
@@ -0,0 +1,6 @@
+
+ bobi
+ john
+ ny
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-middle/output.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-middle/output.js
new file mode 100644
index 00000000..38522076
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-middle/output.js
@@ -0,0 +1,127 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [],
+ subElements: [
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "to",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 15, endOffset: 18 },
+ text: "bobi",
+ },
+ ],
+ position: { startOffset: 11, endOffset: 23 },
+ syntax: {
+ openName: { image: "to", startOffset: 12, endOffset: 13 },
+ closeName: { image: "to", startOffset: 21, endOffset: 22 },
+ isSelfClosing: false,
+ openBody: { startOffset: 11, endOffset: 14 },
+ closeBody: { startOffset: 19, endOffset: 23 },
+ attributesRange: { startOffset: 15, endOffset: 13 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "ad",
+ attributes: [],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 29, endOffset: 31 },
+ syntax: {
+ openName: { image: "ad", startOffset: 30, endOffset: 31 },
+ guessedAttributesRange: { startOffset: 33, endOffset: 36 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "from",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 43, endOffset: 46 },
+ text: "john",
+ },
+ ],
+ position: { startOffset: 37, endOffset: 53 },
+ syntax: {
+ openName: { image: "from", startOffset: 38, endOffset: 41 },
+ closeName: { image: "from", startOffset: 49, endOffset: 52 },
+ isSelfClosing: false,
+ openBody: { startOffset: 37, endOffset: 42 },
+ closeBody: { startOffset: 47, endOffset: 53 },
+ attributesRange: { startOffset: 43, endOffset: 41 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "city",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 65, endOffset: 66 },
+ text: "ny",
+ },
+ ],
+ position: { startOffset: 59, endOffset: 73 },
+ syntax: {
+ openName: { image: "city", startOffset: 60, endOffset: 63 },
+ closeName: { image: "city", startOffset: 69, endOffset: 72 },
+ isSelfClosing: false,
+ openBody: { startOffset: 59, endOffset: 64 },
+ closeBody: { startOffset: 67, endOffset: 73 },
+ attributesRange: { startOffset: 65, endOffset: 63 },
+ },
+ },
+ ],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 6, endOffset: 10 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 24, endOffset: 28 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 54, endOffset: 58 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 74, endOffset: 74 },
+ text: "\n",
+ },
+ ],
+ position: { startOffset: 0, endOffset: 81 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 77, endOffset: 80 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 5 },
+ closeBody: { startOffset: 75, endOffset: 81 },
+ attributesRange: { startOffset: 6, endOffset: 4 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 82 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-middle/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-middle/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/element-name-middle/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/missing-top-level-element/input.xml b/projects/xml-tools/packages/ast/test/snapshots/invalid/missing-top-level-element/input.xml
new file mode 100644
index 00000000..fb7c4aba
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/missing-top-level-element/input.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/missing-top-level-element/output.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/missing-top-level-element/output.js
new file mode 100644
index 00000000..4a972ec2
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/missing-top-level-element/output.js
@@ -0,0 +1,33 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: null,
+ position: { startOffset: 0, endOffset: 39 },
+ prolog: {
+ type: "XMLProlog",
+ attributes: [
+ {
+ type: "XMLPrologAttribute",
+ position: { startOffset: 6, endOffset: 18 },
+ key: "version",
+ value: "1.0",
+ syntax: {
+ key: { image: "version", startOffset: 6, endOffset: 12 },
+ value: { image: '"1.0"', startOffset: 14, endOffset: 18 },
+ },
+ },
+ {
+ type: "XMLPrologAttribute",
+ position: { startOffset: 20, endOffset: 35 },
+ key: "encoding",
+ value: "UTF-8",
+ syntax: {
+ key: { image: "encoding", startOffset: 20, endOffset: 27 },
+ value: { image: '"UTF-8"', startOffset: 29, endOffset: 35 },
+ },
+ },
+ ],
+ position: { startOffset: 0, endOffset: 37 },
+ },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/invalid/missing-top-level-element/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/invalid/missing-top-level-element/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/invalid/missing-top-level-element/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/attributes/input.xml b/projects/xml-tools/packages/ast/test/snapshots/valid/attributes/input.xml
new file mode 100644
index 00000000..8a9a5902
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/attributes/input.xml
@@ -0,0 +1 @@
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/attributes/output.js b/projects/xml-tools/packages/ast/test/snapshots/valid/attributes/output.js
new file mode 100644
index 00000000..f538a5cc
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/attributes/output.js
@@ -0,0 +1,44 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 6, endOffset: 16 },
+ key: "attr1",
+ value: "666",
+ syntax: {
+ key: { image: "attr1", startOffset: 6, endOffset: 10 },
+ value: { image: '"666"', startOffset: 12, endOffset: 16 },
+ },
+ },
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 18, endOffset: 28 },
+ key: "attr2",
+ value: "333",
+ syntax: {
+ key: { image: "attr2", startOffset: 18, endOffset: 22 },
+ value: { image: '"333"', startOffset: 24, endOffset: 28 },
+ },
+ },
+ ],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 0, endOffset: 36 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 32, endOffset: 35 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 29 },
+ closeBody: { startOffset: 30, endOffset: 36 },
+ attributesRange: { startOffset: 6, endOffset: 28 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 37 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/attributes/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/valid/attributes/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/attributes/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/complex-contents/input.xml b/projects/xml-tools/packages/ast/test/snapshots/valid/complex-contents/input.xml
new file mode 100644
index 00000000..8aca3184
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/complex-contents/input.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/complex-contents/output.js b/projects/xml-tools/packages/ast/test/snapshots/valid/complex-contents/output.js
new file mode 100644
index 00000000..937979f7
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/complex-contents/output.js
@@ -0,0 +1,28 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 9, endOffset: 21 },
+ syntax: {
+ openName: { image: "note", startOffset: 10, endOffset: 13 },
+ closeName: { image: "note", startOffset: 17, endOffset: 20 },
+ isSelfClosing: false,
+ openBody: { startOffset: 9, endOffset: 14 },
+ closeBody: { startOffset: 15, endOffset: 21 },
+ attributesRange: { startOffset: 15, endOffset: 13 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 22 },
+ prolog: {
+ type: "XMLProlog",
+ attributes: [],
+ position: { startOffset: 0, endOffset: 7 },
+ },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/complex-contents/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/valid/complex-contents/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/complex-contents/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/elements-short-form/input.xml b/projects/xml-tools/packages/ast/test/snapshots/valid/elements-short-form/input.xml
new file mode 100644
index 00000000..f9d7191f
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/elements-short-form/input.xml
@@ -0,0 +1,4 @@
+
+ Bill
+
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/elements-short-form/output.js b/projects/xml-tools/packages/ast/test/snapshots/valid/elements-short-form/output.js
new file mode 100644
index 00000000..307fb63c
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/elements-short-form/output.js
@@ -0,0 +1,89 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [],
+ subElements: [
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "to",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 15, endOffset: 18 },
+ text: "Bill",
+ },
+ ],
+ position: { startOffset: 11, endOffset: 23 },
+ syntax: {
+ openName: { image: "to", startOffset: 12, endOffset: 13 },
+ closeName: { image: "to", startOffset: 21, endOffset: 22 },
+ isSelfClosing: false,
+ openBody: { startOffset: 11, endOffset: 14 },
+ closeBody: { startOffset: 19, endOffset: 23 },
+ attributesRange: { startOffset: 15, endOffset: 13 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "from",
+ attributes: [
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 35, endOffset: 44 },
+ key: "name",
+ value: "tim",
+ syntax: {
+ key: { image: "name", startOffset: 35, endOffset: 38 },
+ value: { image: '"tim"', startOffset: 40, endOffset: 44 },
+ },
+ },
+ ],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 29, endOffset: 46 },
+ syntax: {
+ openName: { image: "from", startOffset: 30, endOffset: 33 },
+ isSelfClosing: true,
+ openBody: { startOffset: 29, endOffset: 46 },
+ attributesRange: { startOffset: 35, endOffset: 44 },
+ },
+ },
+ ],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 6, endOffset: 10 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 24, endOffset: 28 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 47, endOffset: 47 },
+ text: "\n",
+ },
+ ],
+ position: { startOffset: 0, endOffset: 54 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 50, endOffset: 53 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 5 },
+ closeBody: { startOffset: 48, endOffset: 54 },
+ attributesRange: { startOffset: 6, endOffset: 4 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 55 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/elements-short-form/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/valid/elements-short-form/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/elements-short-form/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/elements/input.xml b/projects/xml-tools/packages/ast/test/snapshots/valid/elements/input.xml
new file mode 100644
index 00000000..77dfac07
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/elements/input.xml
@@ -0,0 +1,4 @@
+
+ Bill
+ Tim
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/elements/output.js b/projects/xml-tools/packages/ast/test/snapshots/valid/elements/output.js
new file mode 100644
index 00000000..d1789754
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/elements/output.js
@@ -0,0 +1,86 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [],
+ subElements: [
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "to",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 15, endOffset: 18 },
+ text: "Bill",
+ },
+ ],
+ position: { startOffset: 11, endOffset: 23 },
+ syntax: {
+ openName: { image: "to", startOffset: 12, endOffset: 13 },
+ closeName: { image: "to", startOffset: 21, endOffset: 22 },
+ isSelfClosing: false,
+ openBody: { startOffset: 11, endOffset: 14 },
+ closeBody: { startOffset: 19, endOffset: 23 },
+ attributesRange: { startOffset: 15, endOffset: 13 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "from",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 35, endOffset: 37 },
+ text: "Tim",
+ },
+ ],
+ position: { startOffset: 29, endOffset: 44 },
+ syntax: {
+ openName: { image: "from", startOffset: 30, endOffset: 33 },
+ closeName: { image: "from", startOffset: 40, endOffset: 43 },
+ isSelfClosing: false,
+ openBody: { startOffset: 29, endOffset: 34 },
+ closeBody: { startOffset: 38, endOffset: 44 },
+ attributesRange: { startOffset: 35, endOffset: 33 },
+ },
+ },
+ ],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 6, endOffset: 10 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 24, endOffset: 28 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 45, endOffset: 45 },
+ text: "\n",
+ },
+ ],
+ position: { startOffset: 0, endOffset: 52 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 48, endOffset: 51 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 5 },
+ closeBody: { startOffset: 46, endOffset: 52 },
+ attributesRange: { startOffset: 6, endOffset: 4 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 53 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/elements/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/valid/elements/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/elements/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/namespaces/input.xml b/projects/xml-tools/packages/ast/test/snapshots/valid/namespaces/input.xml
new file mode 100644
index 00000000..c54a334c
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/namespaces/input.xml
@@ -0,0 +1,7 @@
+
+
+ Some Chair
+ 50
+ 67
+ Awsome Chair
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/namespaces/output.js b/projects/xml-tools/packages/ast/test/snapshots/valid/namespaces/output.js
new file mode 100644
index 00000000..20c33f89
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/namespaces/output.js
@@ -0,0 +1,198 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {
+ f: "https://blah.com/furniture",
+ "::DEFAULT": "https://blah.com/default",
+ },
+ name: "table",
+ attributes: [
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 10, endOffset: 45 },
+ key: "xmlns:f",
+ value: "https://blah.com/furniture",
+ syntax: {
+ key: { image: "xmlns:f", startOffset: 10, endOffset: 16 },
+ value: {
+ image: '"https://blah.com/furniture"',
+ startOffset: 18,
+ endOffset: 45,
+ },
+ },
+ },
+ {
+ type: "XMLAttribute",
+ position: { startOffset: 47, endOffset: 78 },
+ key: "xmlns",
+ value: "https://blah.com/default",
+ syntax: {
+ key: { image: "xmlns", startOffset: 47, endOffset: 51 },
+ value: {
+ image: '"https://blah.com/default"',
+ startOffset: 53,
+ endOffset: 78,
+ },
+ },
+ },
+ ],
+ subElements: [
+ {
+ type: "XMLElement",
+ namespaces: {
+ f: "https://blah.com/furniture",
+ "::DEFAULT": "https://blah.com/default",
+ },
+ name: "name",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 93, endOffset: 102 },
+ text: "Some Chair",
+ },
+ ],
+ position: { startOffset: 85, endOffset: 111 },
+ syntax: {
+ openName: { image: "f:name", startOffset: 86, endOffset: 91 },
+ closeName: { image: "f:name", startOffset: 105, endOffset: 110 },
+ isSelfClosing: false,
+ openBody: { startOffset: 85, endOffset: 92 },
+ closeBody: { startOffset: 103, endOffset: 111 },
+ attributesRange: { startOffset: 93, endOffset: 91 },
+ },
+ ns: "f",
+ },
+ {
+ type: "XMLElement",
+ namespaces: {
+ f: "https://blah.com/furniture",
+ "::DEFAULT": "https://blah.com/default",
+ },
+ name: "width",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 126, endOffset: 127 },
+ text: "50",
+ },
+ ],
+ position: { startOffset: 117, endOffset: 137 },
+ syntax: {
+ openName: { image: "f:width", startOffset: 118, endOffset: 124 },
+ closeName: { image: "f:width", startOffset: 130, endOffset: 136 },
+ isSelfClosing: false,
+ openBody: { startOffset: 117, endOffset: 125 },
+ closeBody: { startOffset: 128, endOffset: 137 },
+ attributesRange: { startOffset: 126, endOffset: 124 },
+ },
+ ns: "f",
+ },
+ {
+ type: "XMLElement",
+ namespaces: {
+ f: "https://blah.com/furniture",
+ "::DEFAULT": "https://blah.com/default",
+ },
+ name: "length",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 153, endOffset: 154 },
+ text: "67",
+ },
+ ],
+ position: { startOffset: 143, endOffset: 165 },
+ syntax: {
+ openName: { image: "f:length", startOffset: 144, endOffset: 151 },
+ closeName: { image: "f:length", startOffset: 157, endOffset: 164 },
+ isSelfClosing: false,
+ openBody: { startOffset: 143, endOffset: 152 },
+ closeBody: { startOffset: 155, endOffset: 165 },
+ attributesRange: { startOffset: 153, endOffset: 151 },
+ },
+ ns: "f",
+ },
+ {
+ type: "XMLElement",
+ namespaces: {
+ f: "https://blah.com/furniture",
+ "::DEFAULT": "https://blah.com/default",
+ },
+ name: "description",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 184, endOffset: 195 },
+ text: "Awsome Chair",
+ },
+ ],
+ position: { startOffset: 171, endOffset: 209 },
+ syntax: {
+ openName: {
+ image: "description",
+ startOffset: 172,
+ endOffset: 182,
+ },
+ closeName: {
+ image: "description",
+ startOffset: 198,
+ endOffset: 208,
+ },
+ isSelfClosing: false,
+ openBody: { startOffset: 171, endOffset: 183 },
+ closeBody: { startOffset: 196, endOffset: 209 },
+ attributesRange: { startOffset: 184, endOffset: 182 },
+ },
+ },
+ ],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 80, endOffset: 84 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 112, endOffset: 116 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 138, endOffset: 142 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 166, endOffset: 170 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 210, endOffset: 210 },
+ text: "\n",
+ },
+ ],
+ position: { startOffset: 1, endOffset: 220 },
+ syntax: {
+ openName: { image: "f:table", startOffset: 2, endOffset: 8 },
+ closeName: { image: "f:table", startOffset: 213, endOffset: 219 },
+ isSelfClosing: false,
+ openBody: { startOffset: 1, endOffset: 79 },
+ closeBody: { startOffset: 211, endOffset: 220 },
+ attributesRange: { startOffset: 10, endOffset: 78 },
+ },
+ ns: "f",
+ },
+ position: { startOffset: 0, endOffset: 221 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/namespaces/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/valid/namespaces/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/namespaces/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/prolog-no-attriibs/input.xml b/projects/xml-tools/packages/ast/test/snapshots/valid/prolog-no-attriibs/input.xml
new file mode 100644
index 00000000..6d98cea1
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/prolog-no-attriibs/input.xml
@@ -0,0 +1,7 @@
+
+
+ Bill
+ hello
+ Tim
+ world
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/prolog-no-attriibs/output.js b/projects/xml-tools/packages/ast/test/snapshots/valid/prolog-no-attriibs/output.js
new file mode 100644
index 00000000..9984db53
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/prolog-no-attriibs/output.js
@@ -0,0 +1,101 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [],
+ subElements: [
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "to",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 39, endOffset: 42 },
+ text: "Bill",
+ },
+ ],
+ position: { startOffset: 35, endOffset: 47 },
+ syntax: {
+ openName: { image: "to", startOffset: 36, endOffset: 37 },
+ closeName: { image: "to", startOffset: 45, endOffset: 46 },
+ isSelfClosing: false,
+ openBody: { startOffset: 35, endOffset: 38 },
+ closeBody: { startOffset: 43, endOffset: 47 },
+ attributesRange: { startOffset: 39, endOffset: 37 },
+ },
+ },
+ {
+ type: "XMLElement",
+ namespaces: {},
+ name: "from",
+ attributes: [],
+ subElements: [],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 69, endOffset: 71 },
+ text: "Tim",
+ },
+ ],
+ position: { startOffset: 63, endOffset: 78 },
+ syntax: {
+ openName: { image: "from", startOffset: 64, endOffset: 67 },
+ closeName: { image: "from", startOffset: 74, endOffset: 77 },
+ isSelfClosing: false,
+ openBody: { startOffset: 63, endOffset: 68 },
+ closeBody: { startOffset: 72, endOffset: 78 },
+ attributesRange: { startOffset: 69, endOffset: 67 },
+ },
+ },
+ ],
+ textContents: [
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 6, endOffset: 10 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 30, endOffset: 34 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 48, endOffset: 52 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 53, endOffset: 62 },
+ text: "hello\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 79, endOffset: 83 },
+ text: "\n ",
+ },
+ {
+ type: "XMLTextContent",
+ position: { startOffset: 84, endOffset: 89 },
+ text: "world\n",
+ },
+ ],
+ position: { startOffset: 0, endOffset: 96 },
+ syntax: {
+ openName: { image: "note", startOffset: 1, endOffset: 4 },
+ closeName: { image: "note", startOffset: 92, endOffset: 95 },
+ isSelfClosing: false,
+ openBody: { startOffset: 0, endOffset: 5 },
+ closeBody: { startOffset: 90, endOffset: 96 },
+ attributesRange: { startOffset: 6, endOffset: 4 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 97 },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/prolog-no-attriibs/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/valid/prolog-no-attriibs/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/prolog-no-attriibs/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/prolog/input.xml b/projects/xml-tools/packages/ast/test/snapshots/valid/prolog/input.xml
new file mode 100644
index 00000000..30f696f1
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/prolog/input.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/prolog/output.js b/projects/xml-tools/packages/ast/test/snapshots/valid/prolog/output.js
new file mode 100644
index 00000000..7fe19cf7
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/prolog/output.js
@@ -0,0 +1,49 @@
+module.exports = {
+ ast: {
+ type: "XMLDocument",
+ rootElement: {
+ type: "XMLElement",
+ namespaces: {},
+ name: "note",
+ attributes: [],
+ subElements: [],
+ textContents: [],
+ position: { startOffset: 39, endOffset: 51 },
+ syntax: {
+ openName: { image: "note", startOffset: 40, endOffset: 43 },
+ closeName: { image: "note", startOffset: 47, endOffset: 50 },
+ isSelfClosing: false,
+ openBody: { startOffset: 39, endOffset: 44 },
+ closeBody: { startOffset: 45, endOffset: 51 },
+ attributesRange: { startOffset: 45, endOffset: 43 },
+ },
+ },
+ position: { startOffset: 0, endOffset: 52 },
+ prolog: {
+ type: "XMLProlog",
+ attributes: [
+ {
+ type: "XMLPrologAttribute",
+ position: { startOffset: 6, endOffset: 18 },
+ key: "version",
+ value: "1.0",
+ syntax: {
+ key: { image: "version", startOffset: 6, endOffset: 12 },
+ value: { image: '"1.0"', startOffset: 14, endOffset: 18 },
+ },
+ },
+ {
+ type: "XMLPrologAttribute",
+ position: { startOffset: 20, endOffset: 35 },
+ key: "encoding",
+ value: "UTF-8",
+ syntax: {
+ key: { image: "encoding", startOffset: 20, endOffset: 27 },
+ value: { image: '"UTF-8"', startOffset: 29, endOffset: 35 },
+ },
+ },
+ ],
+ position: { startOffset: 0, endOffset: 37 },
+ },
+ },
+};
diff --git a/projects/xml-tools/packages/ast/test/snapshots/valid/prolog/sample.spec.js b/projects/xml-tools/packages/ast/test/snapshots/valid/prolog/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/snapshots/valid/prolog/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/ast/test/utils.js b/projects/xml-tools/packages/ast/test/utils.js
new file mode 100644
index 00000000..914b9dae
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/utils.js
@@ -0,0 +1,112 @@
+const { forEach } = require("lodash");
+const { expect } = require("chai");
+
+const { accept } = require("../");
+
+function modifyAstForAssertions(astNode) {
+ // Avoid comparing cyclic structures
+ accept(astNode, parentRemoverVisitor);
+ // Reduce verbosity of assertions
+ accept(astNode, positionReducerVisitor);
+}
+
+/**
+ * @type {XMLAstVisitor}
+ */
+const parentRemoverVisitor = {
+ /* eslint-disable-next-line no-unused-vars -- consistent signatures in visitor methods even if they are empty placeholders */
+ visitXMLDocument: (node) => {
+ // top level XML Doc does not have a parent...
+ },
+ visitXMLProlog: (node) => {
+ delete node.parent;
+ },
+ visitXMLPrologAttribute: (node) => {
+ delete node.parent;
+ },
+ visitXMLElement: (node) => {
+ delete node.parent;
+ },
+ visitXMLAttribute: (node) => {
+ delete node.parent;
+ },
+ visitXMLTextContent: (node) => {
+ delete node.parent;
+ },
+};
+
+/**
+ * @type {XMLAstVisitor}
+ */
+const positionReducerVisitor = {
+ visitXMLDocument: (node) => {
+ reduceNodePoseInfo(node);
+ },
+ visitXMLProlog: (node) => {
+ reduceNodePoseInfo(node);
+ },
+ visitXMLPrologAttribute: (node) => {
+ reduceNodePoseInfo(node);
+ },
+ visitXMLElement: (node) => {
+ reduceNodePoseInfo(node);
+ },
+ visitXMLAttribute: (node) => {
+ reduceNodePoseInfo(node);
+ },
+ visitXMLTextContent: (node) => {
+ reduceNodePoseInfo(node);
+ },
+};
+
+function reduceNodePoseInfo(node) {
+ reducePositionInfo(node.position);
+
+ forEach(node.syntax, (tok) => {
+ reducePositionInfo(tok);
+ });
+}
+
+function reducePositionInfo(pos) {
+ delete pos.startColumn;
+ delete pos.endColumn;
+ delete pos.startLine;
+ delete pos.endLine;
+}
+
+function assertParentPropsAreValid(astNode) {
+ accept(astNode, parentPropsValidatorVisitor);
+}
+
+/**
+ * @type {XMLAstVisitor}
+ */
+const parentPropsValidatorVisitor = {
+ /* eslint-disable-next-line no-unused-vars -- consistent signatures in visitor methods even if they are empty placeholders */
+ visitXMLDocument: (node) => {
+ // top level XML Doc does not have a parent...
+ },
+ visitXMLProlog: (node) => {
+ expect(node.parent.prolog).to.eql(node);
+ },
+ visitXMLElement: (node) => {
+ const parent = node.parent;
+ if (parent.type === "XMLDocument") {
+ expect(parent.rootElement).to.eql(node);
+ } else {
+ expect(parent.subElements).to.include(node);
+ }
+ },
+ visitXMLAttribute: (node) => {
+ expect(node.parent.attributes).to.include(node);
+ },
+ visitXMLTextContent: (node) => {
+ expect(node.parent.textContents).to.include(node);
+ },
+};
+
+module.exports = {
+ modifyAstForAssertions: modifyAstForAssertions,
+ positionReducerVisitor: positionReducerVisitor,
+ assertParentPropsAreValid: assertParentPropsAreValid,
+};
diff --git a/projects/xml-tools/packages/ast/test/visitor/visitor-spec.js b/projects/xml-tools/packages/ast/test/visitor/visitor-spec.js
new file mode 100644
index 00000000..7430a0a3
--- /dev/null
+++ b/projects/xml-tools/packages/ast/test/visitor/visitor-spec.js
@@ -0,0 +1,137 @@
+const { expect } = require("chai");
+const { parse } = require("@xml-tools/parser");
+const { buildAst, accept } = require("../../");
+
+describe("The XML AST Visitor", () => {
+ it("can traverse AST Elements", () => {
+ const inputText = `
+ Bill
+ Tim
+
+ `;
+
+ let visitedCounter = 0;
+ const visitor = {
+ visitXMLElement: function (node) {
+ expect(["to", "from", "note"]).to.include(node.name);
+ visitedCounter++;
+ },
+ };
+
+ const astNode = getAst(inputText);
+ accept(astNode, visitor);
+ expect(visitedCounter).to.eql(3);
+ });
+
+ it("can traverse AST TextContents", () => {
+ const inputText = `
+ Bill
+ Tim
+
+ `;
+
+ let visitedCounter = 0;
+ const visitor = {
+ visitXMLTextContent: function (node) {
+ // ignore whitespace TextNodes
+ if (/^\s+$/.test(node.text) === false) {
+ expect(["Bill", "Tim", "note"]).to.include(node.text);
+ visitedCounter++;
+ }
+ },
+ };
+
+ const astNode = getAst(inputText);
+ accept(astNode, visitor);
+ expect(visitedCounter).to.eql(2);
+ });
+
+ it("can traverse AST Attributes", () => {
+ const inputText = `
+
+ Bill
+ Tim
+
+ `;
+
+ let visitedCounter = 0;
+ const visitor = {
+ visitXMLAttribute: function (node) {
+ expect(["foo", "bar"]).to.include(node.key);
+ visitedCounter++;
+ },
+ };
+
+ const astNode = getAst(inputText);
+ accept(astNode, visitor);
+ expect(visitedCounter).to.eql(2);
+ });
+
+ it("can traverse AST Prolog Attributes", () => {
+ const inputText = `
+
+ Bill
+ Tim
+
+ `;
+
+ let visitedCounter = 0;
+ const visitor = {
+ visitXMLPrologAttribute: function (node) {
+ expect(["version", "encoding"]).to.include(node.key);
+ visitedCounter++;
+ },
+ };
+
+ const astNode = getAst(inputText);
+ accept(astNode, visitor);
+ expect(visitedCounter).to.eql(2);
+ });
+
+ it("can traverse AST Prolog", () => {
+ const inputText = `
+
+
+ `;
+
+ let visitedCounter = 0;
+ const visitor = {
+ visitXMLProlog: function (node) {
+ expect(node.attributes).to.have.lengthOf(2);
+ expect(node.attributes[0].key).to.eql("version");
+ expect(node.attributes[1].key).to.eql("encoding");
+ visitedCounter++;
+ },
+ };
+
+ const astNode = getAst(inputText);
+ accept(astNode, visitor);
+ expect(visitedCounter).to.eql(1);
+ });
+
+ it("can traverse XML Document AST", () => {
+ const inputText = `
+ foo
+ bar
+
+ `;
+
+ let visitedCounter = 0;
+ const visitor = {
+ visitXMLDocument: function (node) {
+ expect(node.rootElement.name).to.eql("note");
+ visitedCounter++;
+ },
+ };
+
+ const astNode = getAst(inputText);
+ accept(astNode, visitor);
+ expect(visitedCounter).to.eql(1);
+ });
+});
+
+function getAst(text) {
+ const { cst, tokenVector } = parse(text);
+ const ast = buildAst(cst, tokenVector);
+ return ast;
+}
diff --git a/projects/xml-tools/packages/ast/tsconfig.json b/projects/xml-tools/packages/ast/tsconfig.json
new file mode 100644
index 00000000..a4ed07be
--- /dev/null
+++ b/projects/xml-tools/packages/ast/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": ["./api.d.ts"]
+}
diff --git a/projects/xml-tools/packages/common/CHANGELOG.old.md b/projects/xml-tools/packages/common/CHANGELOG.old.md
new file mode 100644
index 00000000..a778e644
--- /dev/null
+++ b/projects/xml-tools/packages/common/CHANGELOG.old.md
@@ -0,0 +1,73 @@
+> **Historic changelog.** This records changes made while the project lived in the
+> standalone [SAP/xml-tools](https://github.com/SAP/xml-tools) repository, before it was
+> migrated into [SAP/app-studio-toolkit](https://github.com/SAP/app-studio-toolkit).
+> Newer entries are generated by Changesets in `CHANGELOG.md`.
+
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [0.2.0](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.1.6...@xml-tools/common@0.2.0) (2026-02-11)
+
+### Features
+
+- update lodash dependency to version 4.17.23 ([#461](https://github.com/sap/xml-tools/issues/461)) ([23a319d](https://github.com/sap/xml-tools/commit/23a319d6ed42a4a3885ec5748eb127147a8960bc))
+
+## [0.1.6](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.1.5...@xml-tools/common@0.1.6) (2021-06-03)
+
+**Note:** Version bump only for package @xml-tools/common
+
+## [0.1.5](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.1.4...@xml-tools/common@0.1.5) (2021-02-09)
+
+**Note:** Version bump only for package @xml-tools/common
+
+## [0.1.4](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.1.3...@xml-tools/common@0.1.4) (2021-01-20)
+
+### Bug Fixes
+
+- missing lib folder in npm packages ([ffed3c2](https://github.com/sap/xml-tools/commit/ffed3c2c54c70aea8b9ded0d53786382bc190cc5))
+
+## [0.1.3](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.1.2...@xml-tools/common@0.1.3) (2021-01-20)
+
+**Note:** Version bump only for package @xml-tools/common
+
+## [0.1.2](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.1.1...@xml-tools/common@0.1.2) (2020-06-29)
+
+**Note:** Version bump only for package @xml-tools/common
+
+## [0.1.1](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.1.0...@xml-tools/common@0.1.1) (2020-06-18)
+
+**Note:** Version bump only for package @xml-tools/common
+
+# [0.1.0](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.0.6...@xml-tools/common@0.1.0) (2020-05-17)
+
+### Features
+
+- **common:** add utility functions for xmlns attributes ([#173](https://github.com/sap/xml-tools/issues/173)) ([20d6c09](https://github.com/sap/xml-tools/commit/20d6c09))
+
+## [0.0.6](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.0.5...@xml-tools/common@0.0.6) (2020-04-02)
+
+**Note:** Version bump only for package @xml-tools/common
+
+## [0.0.5](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.0.4...@xml-tools/common@0.0.5) (2020-02-19)
+
+### Bug Fixes
+
+- add npmignore file to each package ([5bbf209](https://github.com/sap/xml-tools/commit/5bbf209))
+
+## [0.0.4](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.0.3...@xml-tools/common@0.0.4) (2020-02-11)
+
+**Note:** Version bump only for package @xml-tools/common
+
+## [0.0.3](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.0.2...@xml-tools/common@0.0.3) (2020-01-28)
+
+**Note:** Version bump only for package @xml-tools/common
+
+## [0.0.2](https://github.com/sap/xml-tools/compare/@xml-tools/common@0.0.1...@xml-tools/common@0.0.2) (2020-01-20)
+
+**Note:** Version bump only for package @xml-tools/common
+
+## 0.0.1 (2019-12-05)
+
+**Note:** Version bump only for package @xml-tools/common
diff --git a/projects/xml-tools/packages/common/README.md b/projects/xml-tools/packages/common/README.md
new file mode 100644
index 00000000..c4472614
--- /dev/null
+++ b/projects/xml-tools/packages/common/README.md
@@ -0,0 +1,35 @@
+[](https://www.npmjs.com/package/@xml-tools/common)
+
+# @xml-tools/common
+
+Shared Utilities for XML-Tools
+
+## Installation
+
+With npm:
+
+- `npm install @xml-tools/Common`
+
+With Yarn
+
+- `yarn add @xml-tools/Common`
+
+## Usage
+
+Please see the [TypeScript Definitions](./api.d.ts) for full API details.
+
+Simply import/require the @xml-tools/common package and use the utilities
+as defined in the APIs above.
+
+```javascript
+const xmlToolsCommon = require("@xml-tools/common");
+xmlToolsCommon.findNextTextualToken(/*... */);
+```
+
+## Support
+
+Please report issues [here](https://github.com/SAP/app-studio-toolkit/issues/new/choose) and label them with `project:xml-tools`.
+
+## Contributing
+
+See [CONTRIBUTING.md](https://github.com/SAP/app-studio-toolkit/blob/main/CONTRIBUTING.md).
diff --git a/projects/xml-tools/packages/common/api.d.ts b/projects/xml-tools/packages/common/api.d.ts
new file mode 100644
index 00000000..882fec45
--- /dev/null
+++ b/projects/xml-tools/packages/common/api.d.ts
@@ -0,0 +1,33 @@
+import { IToken } from "chevrotain";
+
+/**
+ * Finds the next none "SEA_WS" Token after a specific offset.
+ *
+ * @param tokenVector - The TokenVector returned from @xml-tools/parser parse method.
+ * @param prevTokEndOffset - This must be the endOffset of an existing token.
+ */
+export function findNextTextualToken(
+ tokenVector: IToken[],
+ prevTokEndOffset: number
+): IToken | null;
+
+/**
+ * Check if an xml attribute key is an xmlns key.
+ * Attribute keys which are xmlns keys: "xmlns", "xmlns:core".
+ * Attribute keys which are not xmlns keys: "myattr", "xmlns:with:extra:colon".
+ * "xmlns:" is considered an xmlns key if opts.includeEmptyPrefix is true.
+ *
+ * @param opts.key - the attribute key
+ * @param opts.includeEmptyPrefix - should true be returned when there is no prefix (key === "xmlns:")
+ */
+export function isXMLNamespaceKey(opts: {
+ key: string;
+ includeEmptyPrefix: boolean;
+}): boolean;
+
+/**
+ * Get the attribute name, without its "xmlns:" prefix, from an xmlns attribute key.
+ * If the attribute key is not an xmlns key, undefined is returned.
+ * @param key - the attribute key
+ */
+export function getXMLNamespaceKeyPrefix(key: string): string | undefined;
diff --git a/projects/xml-tools/packages/common/lib/api.js b/projects/xml-tools/packages/common/lib/api.js
new file mode 100644
index 00000000..d35bce6b
--- /dev/null
+++ b/projects/xml-tools/packages/common/lib/api.js
@@ -0,0 +1,11 @@
+const { findNextTextualToken } = require("./find-next-textual-token");
+const {
+ isXMLNamespaceKey,
+ getXMLNamespaceKeyPrefix,
+} = require("./xml-ns-key.js");
+
+module.exports = {
+ findNextTextualToken: findNextTextualToken,
+ isXMLNamespaceKey: isXMLNamespaceKey,
+ getXMLNamespaceKeyPrefix: getXMLNamespaceKeyPrefix,
+};
diff --git a/projects/xml-tools/packages/common/lib/find-next-textual-token.js b/projects/xml-tools/packages/common/lib/find-next-textual-token.js
new file mode 100644
index 00000000..8752c759
--- /dev/null
+++ b/projects/xml-tools/packages/common/lib/find-next-textual-token.js
@@ -0,0 +1,33 @@
+const { findIndex } = require("lodash");
+
+function findNextTextualToken(tokenVector, prevTokenEndOffset) {
+ // The TokenVector is sorted, so we could use a BinarySearch to optimize performance
+ const prevTokenIdx = findIndex(
+ tokenVector,
+ (tok) => tok.endOffset === prevTokenEndOffset
+ );
+ let nextTokenIdx = prevTokenIdx;
+ let found = false;
+ while (found === false) {
+ nextTokenIdx++;
+ const nextPossibleToken = tokenVector[nextTokenIdx];
+ // No Next textualToken
+ if (nextPossibleToken === undefined) {
+ return null;
+ }
+ /* istanbul ignore next
+ * I don't think this scenario can be created, however defensive coding never killed anyone...
+ * Basically SEA_WS can only only appear in "OUTSIDE" mode, and we need a CLOSE/SLASH_CLOSE to get back to outside
+ * mode, however if we had those this function would never have been called...
+ */
+ if (nextPossibleToken.tokenType.name === "SEA_WS") {
+ // skip pure WS tokens as they do not contain any actual text
+ } else {
+ return nextPossibleToken;
+ }
+ }
+}
+
+module.exports = {
+ findNextTextualToken: findNextTextualToken,
+};
diff --git a/projects/xml-tools/packages/common/lib/xml-ns-key.js b/projects/xml-tools/packages/common/lib/xml-ns-key.js
new file mode 100644
index 00000000..cc3f960d
--- /dev/null
+++ b/projects/xml-tools/packages/common/lib/xml-ns-key.js
@@ -0,0 +1,54 @@
+// The xml parser takes care of validating the attribute name.
+// If the user started the attribute name with "xmlns:" we can assume that
+// they meant for it to be an xml namespace attribute.
+// xmlns attributes explicitly can't contain ":" after the "xmlns:" part.
+const namespaceRegex = /^xmlns(?:(?[^:]*))?$/;
+
+/**
+ * See comment in api.d.ts.
+ *
+ * @param {string} key
+ * @param {boolean} includeEmptyPrefix
+ * @returns {boolean}
+ */
+function isXMLNamespaceKey({ key, includeEmptyPrefix }) {
+ if (typeof key !== "string") {
+ return false;
+ }
+ const matchArr = key.match(namespaceRegex);
+
+ // No match - this is not an xmlns key
+ if (matchArr === null) {
+ return false;
+ }
+
+ return !!(
+ includeEmptyPrefix === true ||
+ // "xmlns" case
+ !matchArr.groups.prefixWithColon ||
+ // "xmlns:" case
+ matchArr.groups.prefix
+ );
+}
+
+/**
+ * See comment in api.d.ts.
+ *
+ * @param {string} key
+ * @returns {string|undefined}
+ */
+function getXMLNamespaceKeyPrefix(key) {
+ if (typeof key !== "string") {
+ return undefined;
+ }
+ const matchArr = key.match(namespaceRegex);
+ if (matchArr === null) {
+ return undefined;
+ }
+ return (matchArr.groups && matchArr.groups.prefix) || "";
+}
+
+module.exports = {
+ isXMLNamespaceKey: isXMLNamespaceKey,
+ getXMLNamespaceKeyPrefix: getXMLNamespaceKeyPrefix,
+};
diff --git a/projects/xml-tools/packages/common/nyc.config.js b/projects/xml-tools/packages/common/nyc.config.js
new file mode 100644
index 00000000..3e3f8962
--- /dev/null
+++ b/projects/xml-tools/packages/common/nyc.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ // xml-tools sources live in lib/ (not src/), so this local config overrides
+ // the repo-root nyc.config.js include of **/src/** (which would report 0%).
+ include: ["lib/**/*.js"],
+ reporter: ["text", "lcov"],
+ all: true,
+ branches: 100,
+ lines: 100,
+ functions: 100,
+ statements: 100,
+};
diff --git a/projects/xml-tools/packages/common/package.json b/projects/xml-tools/packages/common/package.json
new file mode 100644
index 00000000..c3d164fa
--- /dev/null
+++ b/projects/xml-tools/packages/common/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@xml-tools/common",
+ "version": "0.2.0",
+ "description": "XML-Tools Common Utilities",
+ "keywords": [
+ "xml",
+ "validation"
+ ],
+ "main": "lib/api.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/SAP/app-studio-toolkit.git",
+ "directory": "projects/xml-tools/packages/common"
+ },
+ "license": "Apache-2.0",
+ "typings": "./api.d.ts",
+ "files": [
+ "lib",
+ "api.d.ts",
+ "CHANGELOG.md",
+ "CHANGELOG.old.md"
+ ],
+ "dependencies": {
+ "chevrotain": "7.1.1",
+ "lodash": "4.17.23"
+ },
+ "devDependencies": {
+ "@xml-tools/parser": "workspace:*"
+ },
+ "scripts": {
+ "ci": "npm-run-all clean type-check coverage:*",
+ "clean": "rimraf ./coverage ./nyc_output",
+ "test": "mocha \"./test/**/*spec.js\"",
+ "coverage:run": "nyc mocha \"./test/**/*spec.js\"",
+ "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100",
+ "type-check": "tsc"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "bugs": {
+ "url": "https://github.com/SAP/app-studio-toolkit/issues"
+ }
+}
diff --git a/projects/xml-tools/packages/common/test/find-next-textual-token-spec.js b/projects/xml-tools/packages/common/test/find-next-textual-token-spec.js
new file mode 100644
index 00000000..40a415af
--- /dev/null
+++ b/projects/xml-tools/packages/common/test/find-next-textual-token-spec.js
@@ -0,0 +1,31 @@
+const { expect } = require("chai");
+const { parse } = require("@xml-tools/parser");
+const { findNextTextualToken } = require("../");
+
+describe("The XML-Tools Common Utils", () => {
+ context("findNextTextualToken", () => {
+ it("can find the next textual token in a valid XML", () => {
+ const xmlText = `
+ Bill
+ Tim
+ `;
+
+ const { tokenVector } = parse(xmlText);
+ // "28" is the endOffset of `Bill`
+ const nextTextualToken = findNextTextualToken(tokenVector, 27);
+ expect(nextTextualToken.image).to.eql("<");
+ expect(nextTextualToken.startOffset).to.eql(37);
+ });
+
+ it("will return null when there is no following textual token", () => {
+ const xmlText = `
+ Bill
+ `;
+
+ const { tokenVector } = parse(xmlText);
+ // "28" is the endOffset of `Bill`
+ const nextTextualToken = findNextTextualToken(tokenVector, 27);
+ expect(nextTextualToken).to.be.null;
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/common/test/xml-ns-key-spec.js b/projects/xml-tools/packages/common/test/xml-ns-key-spec.js
new file mode 100644
index 00000000..a7fefcef
--- /dev/null
+++ b/projects/xml-tools/packages/common/test/xml-ns-key-spec.js
@@ -0,0 +1,106 @@
+const { expect } = require("chai");
+const { isXMLNamespaceKey, getXMLNamespaceKeyPrefix } = require("../");
+
+describe("The XML-Tools Common Utils", () => {
+ context("isXMLNamespaceKey", () => {
+ it("will return true for attribute name starting with xmlns:", () => {
+ expect(isXMLNamespaceKey({ key: "xmlns:a", includeEmptyPrefix: true })).to
+ .be.true;
+ expect(isXMLNamespaceKey({ key: "xmlns:a", includeEmptyPrefix: false }))
+ .to.be.true;
+ });
+
+ it("will return true for attribute name starting with xmlns: that contains dots", () => {
+ expect(
+ isXMLNamespaceKey({ key: "xmlns:a.b.c", includeEmptyPrefix: true })
+ ).to.be.true;
+ expect(
+ isXMLNamespaceKey({ key: "xmlns:a.b.c", includeEmptyPrefix: false })
+ ).to.be.true;
+ });
+
+ it("will return true for the default namespace attribute", () => {
+ expect(isXMLNamespaceKey({ key: "xmlns", includeEmptyPrefix: true })).to
+ .be.true;
+ expect(isXMLNamespaceKey({ key: "xmlns", includeEmptyPrefix: false })).to
+ .be.true;
+ });
+
+ it("will return true for xmlns attribute without a name when includeEmptyPrefix is true", () => {
+ expect(isXMLNamespaceKey({ key: "xmlns:", includeEmptyPrefix: true })).to
+ .be.true;
+ });
+
+ it("will return false for xmlns attribute without a name when includeEmptyPrefix is false", () => {
+ expect(isXMLNamespaceKey({ key: "xmlns:", includeEmptyPrefix: false })).to
+ .be.false;
+ });
+
+ it("will return false for the non-xmlns attribute", () => {
+ expect(isXMLNamespaceKey({ key: "abc", includeEmptyPrefix: true })).to.be
+ .false;
+ expect(isXMLNamespaceKey({ key: "abc", includeEmptyPrefix: false })).to.be
+ .false;
+ });
+
+ it("will return false for non-xmlns attribute that starts with xmlns", () => {
+ expect(isXMLNamespaceKey({ key: "xmlnst", includeEmptyPrefix: true })).to
+ .be.false;
+ expect(isXMLNamespaceKey({ key: "xmlnst", includeEmptyPrefix: false })).to
+ .be.false;
+ });
+
+ it("will return false for attribute name starting with xmlns: that contains additional colons", () => {
+ expect(
+ isXMLNamespaceKey({ key: "xmlns:a.b:c", includeEmptyPrefix: true })
+ ).to.be.false;
+ expect(
+ isXMLNamespaceKey({ key: "xmlns:a.b:c", includeEmptyPrefix: false })
+ ).to.be.false;
+ });
+
+ it("will return false for undefined", () => {
+ expect(isXMLNamespaceKey({ key: undefined, includeEmptyPrefix: true })).to
+ .be.false;
+ expect(isXMLNamespaceKey({ key: undefined, includeEmptyPrefix: false }))
+ .to.be.false;
+ });
+
+ it("will return false for null", () => {
+ expect(isXMLNamespaceKey({ key: null, includeEmptyPrefix: true })).to.be
+ .false;
+ expect(isXMLNamespaceKey({ key: null, includeEmptyPrefix: false })).to.be
+ .false;
+ });
+ });
+
+ context("getNamespaceKeyPrefix", () => {
+ it("will return the name without xmlns prefix for xmlns attribute with name", () => {
+ expect(getXMLNamespaceKeyPrefix("xmlns:abc")).to.eql("abc");
+ });
+
+ it("will return the name without xmlns prefix for xmlns attribute with name that contains dots", () => {
+ expect(getXMLNamespaceKeyPrefix("xmlns:abc.efg")).to.eql("abc.efg");
+ });
+
+ it("will return empty string when prefix is empty", () => {
+ expect(getXMLNamespaceKeyPrefix("xmlns:")).to.be.empty;
+ });
+
+ it("will return undefined when key does not start with xmlns", () => {
+ expect(getXMLNamespaceKeyPrefix("abc")).to.be.undefined;
+ });
+
+ it("will return undefined when key starts with xmlns but is not an xmlns key", () => {
+ expect(getXMLNamespaceKeyPrefix("xmlns*")).to.be.undefined;
+ });
+
+ it("will return undefined for undefined", () => {
+ expect(getXMLNamespaceKeyPrefix(undefined)).to.be.undefined;
+ });
+
+ it("will return undefined for null", () => {
+ expect(getXMLNamespaceKeyPrefix(null)).to.be.undefined;
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/common/tsconfig.json b/projects/xml-tools/packages/common/tsconfig.json
new file mode 100644
index 00000000..a4ed07be
--- /dev/null
+++ b/projects/xml-tools/packages/common/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": ["./api.d.ts"]
+}
diff --git a/projects/xml-tools/packages/constraints/CHANGELOG.old.md b/projects/xml-tools/packages/constraints/CHANGELOG.old.md
new file mode 100644
index 00000000..50c6f84e
--- /dev/null
+++ b/projects/xml-tools/packages/constraints/CHANGELOG.old.md
@@ -0,0 +1,25 @@
+> **Historic changelog.** This records changes made while the project lived in the
+> standalone [SAP/xml-tools](https://github.com/SAP/xml-tools) repository, before it was
+> migrated into [SAP/app-studio-toolkit](https://github.com/SAP/app-studio-toolkit).
+> Newer entries are generated by Changesets in `CHANGELOG.md`.
+
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [1.2.0](https://github.com/sap/xml-tools/compare/@xml-tools/constraints@1.1.1...@xml-tools/constraints@1.2.0) (2026-02-11)
+
+### Features
+
+- update lodash dependency to version 4.17.23 ([#461](https://github.com/sap/xml-tools/issues/461)) ([23a319d](https://github.com/sap/xml-tools/commit/23a319d6ed42a4a3885ec5748eb127147a8960bc))
+
+## [1.1.1](https://github.com/sap/xml-tools/compare/@xml-tools/constraints@1.1.0...@xml-tools/constraints@1.1.1) (2021-06-03)
+
+**Note:** Version bump only for package @xml-tools/constraints
+
+# 1.1.0 (2021-02-09)
+
+### Features
+
+- well formed constraints validations ([#293](https://github.com/sap/xml-tools/issues/293)) ([8a9d535](https://github.com/sap/xml-tools/commit/8a9d535e95a52b9dfc1068d2e203b09bd08e1066)), closes [#13](https://github.com/sap/xml-tools/issues/13) [#14](https://github.com/sap/xml-tools/issues/14)
diff --git a/projects/xml-tools/packages/constraints/README.md b/projects/xml-tools/packages/constraints/README.md
new file mode 100644
index 00000000..96600f93
--- /dev/null
+++ b/projects/xml-tools/packages/constraints/README.md
@@ -0,0 +1,51 @@
+[](https://www.npmjs.com/package/@xml-tools/constraints)
+
+# @xml-tools/constraints
+
+Validations for XML constraints.
+
+The following constraints are currently implemented:
+
+- Uniqueness of attribute keys inside the same element.
+- Element opening tag name must be identical to the element closing tag name.
+
+## Installation
+
+With npm:
+
+- `npm install @xml-tools/constraints`
+
+With Yarn
+
+- `yarn add @xml-tools/constraints`
+
+## Usage
+
+Please see the [TypeScript Definitions](./api.d.ts) for full API details.
+
+A simple usage example:
+
+```javascript
+const { parse } = require("@xml-tools/parser");
+const { buildAst } = require("@xml-tools/ast");
+const { checkConstraints } = require("@xml-tools/constraints");
+
+const xmlText = `
+
+ Bill
+ Tim
+ `;
+
+const { cst, tokenVector } = parse(xmlText);
+const document = buildAst(cst, tokenVector);
+const validationIssues = checkConstraints(document);
+console.log(validationIssues[0].msg); // --> 'opening tag: "note" must match closing tag: "note-typo"
+```
+
+## Support
+
+Please report issues [here](https://github.com/SAP/app-studio-toolkit/issues/new/choose) and label them with `project:xml-tools`.
+
+## Contributing
+
+See [CONTRIBUTING.md](https://github.com/SAP/app-studio-toolkit/blob/main/CONTRIBUTING.md).
diff --git a/projects/xml-tools/packages/constraints/api.d.ts b/projects/xml-tools/packages/constraints/api.d.ts
new file mode 100644
index 00000000..622104fc
--- /dev/null
+++ b/projects/xml-tools/packages/constraints/api.d.ts
@@ -0,0 +1,8 @@
+import { ValidationIssue } from "@xml-tools/validation";
+
+/**
+ * detects XML constraints issues (e.g non well-formed XML)
+ * - https://www.w3.org/TR/xml/#sec-starttags
+ * - see: "Well-formedness constraint: Unique Att Spec".
+ */
+export function checkConstraints(ast: XMLDocument): ValidationIssue[];
diff --git a/projects/xml-tools/packages/constraints/lib/api.js b/projects/xml-tools/packages/constraints/lib/api.js
new file mode 100644
index 00000000..f39210de
--- /dev/null
+++ b/projects/xml-tools/packages/constraints/lib/api.js
@@ -0,0 +1,26 @@
+const { validate } = require("@xml-tools/validation");
+const {
+ validateUniqueAttributeKeys,
+} = require("./constraints/unique-attribute-keys");
+const {
+ validateTagClosingNameMatch,
+} = require("./constraints/tag-closing-name-match");
+
+/**
+ * @param {XMLDocument} ast
+ * @returns {ValidationIssue[]}
+ */
+function checkConstraints(ast) {
+ const constraintIssues = validate({
+ doc: ast,
+ validators: {
+ element: [validateTagClosingNameMatch, validateUniqueAttributeKeys],
+ },
+ });
+
+ return constraintIssues;
+}
+
+module.exports = {
+ checkConstraints,
+};
diff --git a/projects/xml-tools/packages/constraints/lib/constraints/tag-closing-name-match.js b/projects/xml-tools/packages/constraints/lib/constraints/tag-closing-name-match.js
new file mode 100644
index 00000000..7d02744a
--- /dev/null
+++ b/projects/xml-tools/packages/constraints/lib/constraints/tag-closing-name-match.js
@@ -0,0 +1,44 @@
+/**
+ * @param {XMLElement} elem
+ * @returns {ValidationIssue[]}
+ */
+function validateTagClosingNameMatch(elem) {
+ const openTagToken = elem.syntax.openName;
+ const closeTagToken = elem.syntax.closeName;
+
+ // The element tag must have **both** the opening and closing tokens
+ // to be able to validate.
+ if (!openTagToken || !closeTagToken) {
+ return [];
+ }
+
+ // alles gut
+ if (openTagToken.image === closeTagToken.image) {
+ return [];
+ } else {
+ return [
+ {
+ msg: `tags mismatch: "${openTagToken.image}" must match closing tag: "${closeTagToken.image}"`,
+ node: elem,
+ severity: "error",
+ position: {
+ startOffset: openTagToken.startOffset,
+ endOffset: openTagToken.endOffset,
+ },
+ },
+ {
+ msg: `tags mismatch: "${closeTagToken.image}" must match opening tag: "${openTagToken.image}"`,
+ node: elem,
+ severity: "error",
+ position: {
+ startOffset: closeTagToken.startOffset,
+ endOffset: closeTagToken.endOffset,
+ },
+ },
+ ];
+ }
+}
+
+module.exports = {
+ validateTagClosingNameMatch,
+};
diff --git a/projects/xml-tools/packages/constraints/lib/constraints/unique-attribute-keys.js b/projects/xml-tools/packages/constraints/lib/constraints/unique-attribute-keys.js
new file mode 100644
index 00000000..6dccf039
--- /dev/null
+++ b/projects/xml-tools/packages/constraints/lib/constraints/unique-attribute-keys.js
@@ -0,0 +1,37 @@
+const { groupBy, pickBy, reduce, map, filter } = require("lodash");
+
+/**
+ * @param {XMLElement} elem
+ * @returns {ValidationIssue[]}
+ */
+function validateUniqueAttributeKeys(elem) {
+ const attributesWithKeys = filter(elem.attributes, (_) => _.key !== null);
+ const attribByKey = groupBy(attributesWithKeys, "key");
+ const nonUniqueAttribsGroups = pickBy(attribByKey, (_) => _.length > 1);
+ const nonUniqueAttribs = reduce(
+ nonUniqueAttribsGroups,
+ (result, attribsGroup) => result.concat(attribsGroup),
+ []
+ );
+
+ const validationIssues = map(nonUniqueAttribs, (_) => {
+ // the `key` is guaranteed to exist because we have filtered above
+ // for attributes with valid keys
+ const keyToken = _.syntax.key;
+ return {
+ msg: `duplicate attribute: "${_.key}"`,
+ node: _,
+ severity: "error",
+ position: {
+ startOffset: keyToken.startOffset,
+ endOffset: keyToken.endOffset,
+ },
+ };
+ });
+
+ return validationIssues;
+}
+
+module.exports = {
+ validateUniqueAttributeKeys,
+};
diff --git a/projects/xml-tools/packages/constraints/nyc.config.js b/projects/xml-tools/packages/constraints/nyc.config.js
new file mode 100644
index 00000000..3e3f8962
--- /dev/null
+++ b/projects/xml-tools/packages/constraints/nyc.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ // xml-tools sources live in lib/ (not src/), so this local config overrides
+ // the repo-root nyc.config.js include of **/src/** (which would report 0%).
+ include: ["lib/**/*.js"],
+ reporter: ["text", "lcov"],
+ all: true,
+ branches: 100,
+ lines: 100,
+ functions: 100,
+ statements: 100,
+};
diff --git a/projects/xml-tools/packages/constraints/package.json b/projects/xml-tools/packages/constraints/package.json
new file mode 100644
index 00000000..48ec659b
--- /dev/null
+++ b/projects/xml-tools/packages/constraints/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@xml-tools/constraints",
+ "version": "1.2.0",
+ "description": "XML constraints validations",
+ "keywords": [
+ "xml",
+ "validation"
+ ],
+ "main": "lib/api.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/SAP/app-studio-toolkit.git",
+ "directory": "projects/xml-tools/packages/constraints"
+ },
+ "license": "Apache-2.0",
+ "typings": "./api.d.ts",
+ "files": [
+ "lib",
+ "api.d.ts",
+ "CHANGELOG.md",
+ "CHANGELOG.old.md"
+ ],
+ "dependencies": {
+ "@xml-tools/validation": "workspace:*",
+ "lodash": "4.17.23"
+ },
+ "devDependencies": {
+ "@xml-tools/ast": "workspace:*"
+ },
+ "scripts": {
+ "ci": "npm-run-all clean type-check coverage:*",
+ "clean": "rimraf ./coverage ./nyc_output",
+ "test": "mocha \"./test/**/*spec.js\"",
+ "coverage:run": "nyc mocha \"./test/**/*spec.js\"",
+ "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100",
+ "type-check": "tsc"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "bugs": {
+ "url": "https://github.com/SAP/app-studio-toolkit/issues"
+ }
+}
diff --git a/projects/xml-tools/packages/constraints/test/api-spec.js b/projects/xml-tools/packages/constraints/test/api-spec.js
new file mode 100644
index 00000000..8f7404b9
--- /dev/null
+++ b/projects/xml-tools/packages/constraints/test/api-spec.js
@@ -0,0 +1,40 @@
+const { expect } = require("chai");
+const { parse } = require("@xml-tools/parser");
+const { buildAst } = require("@xml-tools/ast");
+const { checkConstraints } = require("../");
+
+describe("the constraints sub-package apis", () => {
+ it("can detect validation issues using `checkConstraints` api", () => {
+ const xmlText = `
+
+ Bill
+ Tim
+ `;
+
+ const { cst, tokenVector } = parse(xmlText);
+ const document = buildAst(cst, tokenVector);
+ const validationIssues = checkConstraints(document);
+ const element = document.rootElement;
+ expect(validationIssues).to.have.lengthOf(2);
+ expect(validationIssues).to.deep.include.members([
+ {
+ msg: 'tags mismatch: "note" must match closing tag: "note-typo"',
+ severity: "error",
+ node: element,
+ position: {
+ startOffset: element.syntax.openName.startOffset,
+ endOffset: element.syntax.openName.endOffset,
+ },
+ },
+ {
+ msg: 'tags mismatch: "note-typo" must match opening tag: "note"',
+ severity: "error",
+ node: element,
+ position: {
+ startOffset: element.syntax.closeName.startOffset,
+ endOffset: element.syntax.closeName.endOffset,
+ },
+ },
+ ]);
+ });
+});
diff --git a/projects/xml-tools/packages/constraints/test/constraints/tag-closing-name-match-spec.js b/projects/xml-tools/packages/constraints/test/constraints/tag-closing-name-match-spec.js
new file mode 100644
index 00000000..829eb242
--- /dev/null
+++ b/projects/xml-tools/packages/constraints/test/constraints/tag-closing-name-match-spec.js
@@ -0,0 +1,109 @@
+const { expect } = require("chai");
+const { parse } = require("@xml-tools/parser");
+const { buildAst } = require("@xml-tools/ast");
+const {
+ validateTagClosingNameMatch,
+} = require("../../lib/constraints/tag-closing-name-match");
+
+describe("tag closing name constraint", () => {
+ context("no issues", () => {
+ it("will not detect any issues for valid XML", () => {
+ const xmlText = `
+
+
+ Tim
+ `;
+
+ const { cst, tokenVector } = parse(xmlText);
+ const document = buildAst(cst, tokenVector);
+ const element = document.rootElement;
+ const result = validateTagClosingNameMatch(element);
+
+ expect(result).to.be.empty;
+ });
+ });
+
+ context("unable to validate", () => {
+ it("will not detect any issues when the opening tag name is missing", () => {
+ const xmlText = `
+ <>
+ Bill
+ Tim
+ `;
+
+ const { cst, tokenVector } = parse(xmlText);
+ const document = buildAst(cst, tokenVector);
+ const element = document.rootElement;
+ const result = validateTagClosingNameMatch(element);
+
+ expect(result).to.be.empty;
+ });
+
+ it("will not detect any issues when the closing tag name is missing", () => {
+ const xmlText = `
+
+ Bill
+ Tim
+ >`;
+
+ const { cst, tokenVector } = parse(xmlText);
+ const document = buildAst(cst, tokenVector);
+ const element = document.rootElement;
+ const result = validateTagClosingNameMatch(element);
+
+ expect(result).to.be.empty;
+ });
+
+ it("will not detect any issues when both tag names are missing", () => {
+ const xmlText = `
+ <>
+ Bill
+ Tim
+ >`;
+
+ const { cst, tokenVector } = parse(xmlText);
+ const document = buildAst(cst, tokenVector);
+ const element = document.rootElement;
+ const result = validateTagClosingNameMatch(element);
+
+ expect(result).to.be.empty;
+ });
+ });
+
+ context("positive constraint checks", () => {
+ it("detects a mis-aligned closing tag", () => {
+ const xmlText = `
+
+ Bill
+ Tim
+ `;
+
+ const { cst, tokenVector } = parse(xmlText);
+ const document = buildAst(cst, tokenVector);
+ const element = document.rootElement;
+ const result = validateTagClosingNameMatch(element);
+
+ expect(result).to.have.lengthOf(2);
+ expect(result).to.deep.include.members([
+ {
+ msg: 'tags mismatch: "note" must match closing tag: "note-typo"',
+ severity: "error",
+ node: element,
+ position: {
+ startOffset: element.syntax.openName.startOffset,
+ endOffset: element.syntax.openName.endOffset,
+ },
+ },
+ {
+ msg: 'tags mismatch: "note-typo" must match opening tag: "note"',
+ severity: "error",
+ node: element,
+ position: {
+ startOffset: element.syntax.closeName.startOffset,
+ endOffset: element.syntax.closeName.endOffset,
+ },
+ },
+ ]);
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/constraints/test/constraints/unique-attribute-keys-spec.js b/projects/xml-tools/packages/constraints/test/constraints/unique-attribute-keys-spec.js
new file mode 100644
index 00000000..963a5e68
--- /dev/null
+++ b/projects/xml-tools/packages/constraints/test/constraints/unique-attribute-keys-spec.js
@@ -0,0 +1,66 @@
+const { expect } = require("chai");
+const { parse } = require("@xml-tools/parser");
+const { buildAst } = require("@xml-tools/ast");
+const {
+ validateUniqueAttributeKeys,
+} = require("../../lib/constraints/unique-attribute-keys");
+
+describe("unique attribute keys constraint", () => {
+ context("no issues", () => {
+ it("will not detect any issues for a valid XML with attributes", () => {
+ const xmlText = ``;
+ const { cst, tokenVector } = parse(xmlText);
+ const document = buildAst(cst, tokenVector);
+ const element = document.rootElement;
+ const result = validateUniqueAttributeKeys(element);
+
+ expect(result).to.be.empty;
+ });
+
+ it("will not detect any issues for a valid XML without attributes", () => {
+ const xmlText = ``;
+ const { cst, tokenVector } = parse(xmlText);
+ const document = buildAst(cst, tokenVector);
+ const element = document.rootElement;
+ const result = validateUniqueAttributeKeys(element);
+
+ expect(result).to.be.empty;
+ });
+ });
+
+ context("positive constraint checks", () => {
+ it("detects a duplicate attribute keys", () => {
+ const xmlText = ``;
+
+ const { cst, tokenVector } = parse(xmlText);
+ const document = buildAst(cst, tokenVector);
+ const element = document.rootElement;
+ const result = validateUniqueAttributeKeys(element);
+
+ const donaldNameAttrib = element.attributes[0];
+ const bambaNameAttrib = element.attributes[3];
+
+ expect(result).to.have.lengthOf(2);
+ expect(result).to.deep.include.members([
+ {
+ msg: 'duplicate attribute: "name"',
+ severity: "error",
+ node: donaldNameAttrib,
+ position: {
+ startOffset: donaldNameAttrib.syntax.key.startOffset,
+ endOffset: donaldNameAttrib.syntax.key.endOffset,
+ },
+ },
+ {
+ msg: 'duplicate attribute: "name"',
+ severity: "error",
+ node: bambaNameAttrib,
+ position: {
+ startOffset: bambaNameAttrib.syntax.key.startOffset,
+ endOffset: bambaNameAttrib.syntax.key.endOffset,
+ },
+ },
+ ]);
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/constraints/tsconfig.json b/projects/xml-tools/packages/constraints/tsconfig.json
new file mode 100644
index 00000000..a4ed07be
--- /dev/null
+++ b/projects/xml-tools/packages/constraints/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": ["./api.d.ts"]
+}
diff --git a/projects/xml-tools/packages/content-assist/CHANGELOG.old.md b/projects/xml-tools/packages/content-assist/CHANGELOG.old.md
new file mode 100644
index 00000000..058cfbc9
--- /dev/null
+++ b/projects/xml-tools/packages/content-assist/CHANGELOG.old.md
@@ -0,0 +1,168 @@
+> **Historic changelog.** This records changes made while the project lived in the
+> standalone [SAP/xml-tools](https://github.com/SAP/xml-tools) repository, before it was
+> migrated into [SAP/app-studio-toolkit](https://github.com/SAP/app-studio-toolkit).
+> Newer entries are generated by Changesets in `CHANGELOG.md`.
+
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [3.2.0](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.1.11...@xml-tools/content-assist@3.2.0) (2026-02-11)
+
+### Features
+
+- update lodash dependency to version 4.17.23 ([#461](https://github.com/sap/xml-tools/issues/461)) ([23a319d](https://github.com/sap/xml-tools/commit/23a319d6ed42a4a3885ec5748eb127147a8960bc))
+
+## [3.1.11](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.1.10...@xml-tools/content-assist@3.1.11) (2021-06-03)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [3.1.10](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.1.9...@xml-tools/content-assist@3.1.10) (2021-02-09)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [3.1.9](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.1.8...@xml-tools/content-assist@3.1.9) (2021-01-20)
+
+### Bug Fixes
+
+- missing lib folder in npm packages ([ffed3c2](https://github.com/sap/xml-tools/commit/ffed3c2c54c70aea8b9ded0d53786382bc190cc5))
+
+## [3.1.8](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.1.7...@xml-tools/content-assist@3.1.8) (2021-01-20)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [3.1.7](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.1.6...@xml-tools/content-assist@3.1.7) (2020-06-29)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [3.1.6](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.1.5...@xml-tools/content-assist@3.1.6) (2020-06-18)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [3.1.5](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.1.4...@xml-tools/content-assist@3.1.5) (2020-05-17)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [3.1.4](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.1.3...@xml-tools/content-assist@3.1.4) (2020-04-02)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [3.1.3](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.1.2...@xml-tools/content-assist@3.1.3) (2020-02-19)
+
+### Bug Fixes
+
+- add npmignore file to each package ([5bbf209](https://github.com/sap/xml-tools/commit/5bbf209))
+
+## [3.1.2](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.1.1...@xml-tools/content-assist@3.1.2) (2020-02-11)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [3.1.1](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.1.0...@xml-tools/content-assist@3.1.1) (2020-02-10)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+# [3.1.0](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.0.2...@xml-tools/content-assist@3.1.0) (2020-02-06)
+
+### Bug Fixes
+
+- **content-assist:** avoid breaking changes with new `context` arg ([c7bbdc2](https://github.com/sap/xml-tools/commit/c7bbdc2))
+
+### Features
+
+- **content-assist:** support optional `context` arg in `getSuggestions` ([6e5a228](https://github.com/sap/xml-tools/commit/6e5a228))
+
+## [3.0.2](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.0.1...@xml-tools/content-assist@3.0.2) (2020-02-06)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [3.0.1](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@3.0.0...@xml-tools/content-assist@3.0.1) (2020-01-28)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+# [3.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@2.0.0...@xml-tools/content-assist@3.0.0) (2020-01-20)
+
+### Features
+
+- **parser:** support Basic DocType Declarations ([5b4db21](https://github.com/sap/xml-tools/commit/5b4db21))
+
+### BREAKING CHANGES
+
+- **parser:** Implementing XmlCstVisitor now requires implementing two additional methods:
+ (docTypeDecl and externalID)
+
+# [2.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@1.0.0...@xml-tools/content-assist@2.0.0) (2019-12-18)
+
+### Bug Fixes
+
+- **content-assist:** crash with invalid xml ([#57](https://github.com/sap/xml-tools/issues/57)) ([774c623](https://github.com/sap/xml-tools/commit/774c623))
+
+### Features
+
+- **content-assist:** decouple content assist from parsing ([#58](https://github.com/sap/xml-tools/issues/58)) ([3688da8](https://github.com/sap/xml-tools/commit/3688da8))
+
+### BREAKING CHANGES
+
+- **content-assist:** getSuggestions now uses ast, cst and tokenVector instead of text
+
+# [1.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@0.4.2...@xml-tools/content-assist@1.0.0) (2019-12-08)
+
+### Features
+
+- **ast:** attributesRange and guessedAttributeRange support ([8ce840d](https://github.com/sap/xml-tools/commit/8ce840d)), closes [#51](https://github.com/sap/xml-tools/issues/51)
+
+### BREAKING CHANGES
+
+- **ast:** buildAst now requires a tokenVector argument
+
+## [0.4.2](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@0.4.1...@xml-tools/content-assist@0.4.2) (2019-12-05)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [0.4.1](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@0.4.0...@xml-tools/content-assist@0.4.1) (2019-12-03)
+
+### Bug Fixes
+
+- simple schema with xml namespaces ([#52](https://github.com/sap/xml-tools/issues/52)) ([bb0163e](https://github.com/sap/xml-tools/commit/bb0163e))
+
+# [0.4.0](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@0.3.6...@xml-tools/content-assist@0.4.0) (2019-11-20)
+
+### Features
+
+- **content assist:** improved attributes keys content assist ([3d3b7c2](https://github.com/sap/xml-tools/commit/3d3b7c2)), closes [#42](https://github.com/sap/xml-tools/issues/42)
+
+## [0.3.6](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@0.3.5...@xml-tools/content-assist@0.3.6) (2019-11-12)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [0.3.5](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@0.3.4...@xml-tools/content-assist@0.3.5) (2019-11-11)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [0.3.4](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@0.3.3...@xml-tools/content-assist@0.3.4) (2019-11-11)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [0.3.3](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@0.3.2...@xml-tools/content-assist@0.3.3) (2019-11-06)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [0.3.2](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@0.3.1...@xml-tools/content-assist@0.3.2) (2019-11-06)
+
+**Note:** Version bump only for package @xml-tools/content-assist
+
+## [0.3.1](https://github.com/sap/xml-tools/compare/@xml-tools/content-assist@0.3.0...@xml-tools/content-assist@0.3.1) (2019-10-29)
+
+### Bug Fixes
+
+- content Assist - ElementContents bug fixes ([1eba20a](https://github.com/sap/xml-tools/commit/1eba20a)), closes [#15](https://github.com/sap/xml-tools/issues/15) [#16](https://github.com/sap/xml-tools/issues/16)
+
+# 0.3.0 (2019-10-28)
+
+### Features
+
+- move to lerna independent mode ([6c347a8](https://github.com/sap/xml-tools/commit/6c347a8)), closes [#18](https://github.com/sap/xml-tools/issues/18)
+
+# 0.2.0 (2019-10-28)
+
+# 0.1.0 (2019-10-28)
diff --git a/projects/xml-tools/packages/content-assist/README.md b/projects/xml-tools/packages/content-assist/README.md
new file mode 100644
index 00000000..cda04d7c
--- /dev/null
+++ b/projects/xml-tools/packages/content-assist/README.md
@@ -0,0 +1,94 @@
+[](https://www.npmjs.com/package/@xml-tools/content-assist)
+
+# @xml-tools/content-assist
+
+APIs which assist in implementing content assist (auto-complete) logic for XML.
+This package handles the identification of the specific content assist **scenario and context**.
+Given this scenario & context information a user of this package can then implement
+their custom content assist logic.
+
+For example:
+
+Given the following XML and a content assist request at the `⇶` sign:
+
+```xml
+
+
+
+
+
+```
+
+A **Attribute Value** content assist scenario would be identified.
+With a **prefix** `New` and references to the ASTNodes
+of the relevant attribute(city) and the containing element (address).
+
+- Note that an ASTNode possesses a `parent` property, so the full structure is accessible.
+
+## Installation
+
+With npm:
+
+- `npm install @xml-tools/content-assist`
+
+With Yarn
+
+- `yarn add @xml-tools/content-assist`
+
+## Usage
+
+Please see the [TypeScript Definitions](./api.d.ts) for full API details.
+
+A simple usage example:
+
+```javascript
+const { parse } = require("@xml-tools/parser");
+const { buildAst } = require("@xml-tools/ast");
+const { getSuggestions } = require("@xml-tools/content-assist");
+
+const xmlText = `
+ Bill
+ Bill
+
+`;
+
+// A-lot of contextual information is needed to compute the content assist context.
+const { cst, tokenVector } = parse(xmlText);
+const xmlDocAst = buildAst(cst, tokenVector);
+
+const suggestions = getSuggestions({
+ ast: xmlDocAst,
+ cst: cst,
+ tokVector: tokenVector,
+ offset: 66, // Right after the ' {
+ const suggestions = [];
+ if (element.parent.name === "note" && "address".startsWith(prefix)) {
+ suggestions.push({
+ text: "address".slice(prefix.length),
+ label: "address",
+ });
+ }
+ return suggestions;
+ },
+ ],
+ },
+});
+
+console.log(suggestions[0].text); // -> dress
+```
+
+## Support
+
+Please report issues [here](https://github.com/SAP/app-studio-toolkit/issues/new/choose) and label them with `project:xml-tools`.
+
+## Contributing
+
+See [CONTRIBUTING.md](https://github.com/SAP/app-studio-toolkit/blob/main/CONTRIBUTING.md).
+
+[ast]: https://en.wikipedia.org/wiki/Abstract_syntax_tree
diff --git a/projects/xml-tools/packages/content-assist/api.d.ts b/projects/xml-tools/packages/content-assist/api.d.ts
new file mode 100644
index 00000000..d8e4d6f3
--- /dev/null
+++ b/projects/xml-tools/packages/content-assist/api.d.ts
@@ -0,0 +1,194 @@
+import { IToken } from "chevrotain";
+import { DocumentCstNode } from "@xml-tools/parser";
+import {
+ XMLAttribute,
+ XMLElement,
+ XMLTextContent,
+ XMLDocument,
+} from "@xml-tools/ast";
+
+declare function getSuggestions(options: {
+ cst: DocumentCstNode;
+ ast: XMLDocument;
+ offset: number;
+ tokenVector: IToken[];
+ providers: SuggestionProviders;
+}): OUT[];
+
+/**
+ * The `context` argument can be used to inject state/data
+ * to the be used in the implementation of the suggestionsProviders.
+ * For example a Symbol Table of known Attributes / Tags.
+ */
+declare function getSuggestions(options: {
+ cst: DocumentCstNode;
+ ast: XMLDocument;
+ offset: number;
+ tokenVector: IToken[];
+ providers: SuggestionProviders;
+ context: CONTEXT;
+}): OUT[];
+
+declare type SuggestionProviders = {
+ elementContent?: ElementContentCompletion[];
+ elementName?: ElementNameCompletion[];
+ attributeName?: AttributeNameCompletion[];
+ attributeValue?: AttributeValueCompletion[];
+};
+
+declare type ProviderOptions =
+ | ElementContentCompletionOptions
+ | ElementNameCompletionOptions
+ | AttributeNameCompletionOptions
+ | AttributeValueCompletionOptions;
+
+declare type SuggestionProvider<
+ IN extends ProviderOptions,
+ OUT,
+ CONTEXT = undefined
+> = (options: IN) => OUT[];
+
+declare type ElementContentCompletionOptions = {
+ /**
+ * Element ASTNode for which the name content assist was requested.
+ * This ASTNode may be used to:
+ * - Inspect if there is any other content in the Element.
+ * - Inspect the available namespaces aliases in this scope.
+ * - ...
+ */
+ element: XMLElement;
+
+ /**
+ * The pre-existing part of the name at the content assist request offset.
+ * This would normally be used to filter out suggestions that do not match the prefix.
+ */
+ prefix: string | undefined;
+
+ /**
+ * TextContent ASTNode in which content assist was requested
+ * Note that this property may be undefined if no prefix was provided.
+ */
+ textContent: XMLTextContent | undefined;
+
+ context: CONTEXT;
+};
+/**
+ * Suggestions provider for element's contents.
+ * This is triggered when content assist is requested:
+ * 1. **inside** an element's contents.
+ * 2. And **outside** any of the element's sub-parts
+ *
+ * For example: ('⇶' marks the content assist position):
+ * -
+ * bobi
+ * ⇶
+ *
+ *
+ *
+ * - ⇶
+ */
+declare type ElementContentCompletion<
+ OUT,
+ CONTEXT = undefined
+> = SuggestionProvider, OUT, CONTEXT>;
+
+declare type ElementNameCompletionOptions = {
+ /**
+ * Element ASTNode for which the name content assist was requested
+ */
+ element: XMLElement;
+ /**
+ * The pre-existing part of the name at the content assist request offset.
+ * This would normally be used to filter out suggestions that do not match the prefix.
+ */
+ prefix: string | undefined;
+
+ context: CONTEXT;
+};
+/**
+ * Suggestions provider for element names.
+ * This is triggered when content assist is requested:
+ * 1. **inside** an element's contents.
+ * 2. Following the '<' character.
+ * 3. optionally also following a name prefix.
+ *
+ * For example: ('⇶' marks the content assist position):
+ * - = SuggestionProvider, OUT, CONTEXT>;
+
+declare type AttributeNameCompletionOptions = {
+ /**
+ * Element ASTNode in which content assist was requested
+ * This ASTNode may be used to:
+ * - Inspect pre-existing attribute names.
+ * - Identify namespaces aliases available in this "scope"
+ * - ...
+ */
+ element: XMLElement;
+ /**
+ * Attribute ASTNode in which content assist was requested
+ * Note that this property may be undefined if no prefix was provided.
+ */
+ attribute: XMLAttribute | undefined;
+ /**
+ * The pre-existing part of the name at the content assist request offset.
+ * This would normally be used to filter out suggestions that do not match the prefix.
+ */
+ prefix: string | undefined;
+
+ context: CONTEXT;
+};
+
+/**
+ * Suggestions provider for attribute names.
+ * This is triggered when content assist is requested **inside** an element opening name block
+ * in a position where an attribute is valid e.g ('⇶' marks the content assist position):
+ * -
+ * -
+ */
+declare type AttributeNameCompletion<
+ OUT,
+ CONTEXT = undefined
+> = SuggestionProvider, OUT, CONTEXT>;
+
+declare type AttributeValueCompletionOptions = {
+ /**
+ * Element ASTNode in which content assist was requested
+ */
+ element: XMLElement;
+ /**
+ * Attribute ASTNode in which content assist was requested
+ * Note that **unlike** `AttributeNameCompletion` this property **always** exists.
+ * This ASTNode may be used to:
+ * - Inspect the attribute key for which value suggestions are requested.
+ * - Inspect the **whole** pre-existing value of the attribute.
+ */
+ attribute: XMLAttribute;
+ /**
+ * The pre-existing part of the value at the content assist request location.
+ * This would normally be used to filter out suggestions that do not match the prefix.
+ *
+ * Note that the prefix does not include any quotes.
+ */
+ prefix: string | undefined;
+
+ context: CONTEXT;
+};
+
+/**
+ * Suggestions provider for attribute values.
+ * This is triggered when content assist is requested **inside** the quotes
+ * d.g: ('⇶' marks the content assist position):
+ * -
+ * -
+ * - ...
+ */
+declare type AttributeValueCompletion<
+ OUT,
+ CONTEXT = undefined
+> = SuggestionProvider, OUT, CONTEXT>;
diff --git a/projects/xml-tools/packages/content-assist/lib/api.js b/projects/xml-tools/packages/content-assist/lib/api.js
new file mode 100644
index 00000000..690d7176
--- /dev/null
+++ b/projects/xml-tools/packages/content-assist/lib/api.js
@@ -0,0 +1,40 @@
+const { defaultsDeep, flatMap } = require("lodash");
+
+const { computeCompletionSyntacticContext } = require("./content-assist");
+
+function getSuggestions(options) {
+ const actualOptions = defaultsDeep(options, {
+ providers: {
+ elementContent: [],
+ elementName: [],
+ attributeName: [],
+ attributeValue: [],
+ },
+ context: undefined,
+ });
+
+ let { providerType, providerArgs } = computeCompletionSyntacticContext({
+ cst: actualOptions.cst,
+ tokenVector: actualOptions.tokenVector,
+ ast: actualOptions.ast,
+ offset: actualOptions.offset,
+ });
+
+ // Inject Additional semantic context for the content assist providers.
+ providerArgs.context = actualOptions.context;
+
+ if (providerType === null) {
+ return [];
+ } else {
+ const selectedProviders = actualOptions.providers[providerType];
+
+ const suggestions = flatMap(selectedProviders, (suggestionProvider) =>
+ suggestionProvider(providerArgs)
+ );
+ return suggestions;
+ }
+}
+
+module.exports = {
+ getSuggestions: getSuggestions,
+};
diff --git a/projects/xml-tools/packages/content-assist/lib/content-assist.js b/projects/xml-tools/packages/content-assist/lib/content-assist.js
new file mode 100644
index 00000000..6a55a9a6
--- /dev/null
+++ b/projects/xml-tools/packages/content-assist/lib/content-assist.js
@@ -0,0 +1,399 @@
+const {
+ forEach,
+ isArray,
+ find,
+ findIndex,
+ flatMap,
+ identity,
+ last,
+ isEmpty,
+} = require("lodash");
+const { BaseXmlCstVisitor } = require("@xml-tools/parser");
+const { findNextTextualToken } = require("@xml-tools/common");
+
+/**
+ *
+ * @return {{providerType:string, providerArgs: {prefix?:string, element:XMLElement, attribute?:XMLAttribute}}}
+ */
+function computeCompletionSyntacticContext({
+ cst,
+ ast: docAst,
+ offset,
+ tokenVector,
+}) {
+ const contextVisitor = new SuggestionContextVisitor(
+ docAst,
+ offset,
+ tokenVector
+ );
+ contextVisitor.visit(cst);
+ return contextVisitor.result;
+}
+
+/* eslint-disable no-unused-vars -- consistent signatures in visitor methods even if they are empty placeholders */
+class SuggestionContextVisitor extends BaseXmlCstVisitor {
+ constructor(docAst, offset, tokenVector) {
+ super();
+ this.docAst = docAst;
+ this.targetOffset = offset;
+ this.tokenVector = tokenVector;
+ this.result = { providerType: null, providerArgs: {} };
+ this.found = false;
+ }
+
+ /**
+ * @param {DocumentCtx} ctx
+ */
+ document(ctx) {
+ this.visit(ctx.element, this.docAst.rootElement);
+ }
+
+ /**
+ * @param {PrologCtx} ctx
+ */
+ /* istanbul ignore next - place holder*/
+ prolog(ctx, astNode) {}
+
+ /**
+ * @param {docTypeDeclCtx} ctx
+ */
+ /* istanbul ignore next - place holder*/
+ docTypeDecl(ctx, astNode) {}
+
+ /**
+ * @param {ExternalIDCtx} ctx
+ */
+ /* istanbul ignore next - place holder*/
+ externalID(ctx, astNode) {}
+
+ /**
+ * @param {ContentCtx} ctx
+ * @param {XMLElement} astNode
+ *
+ */
+ content(ctx, astNode) {
+ forEach(ctx.element, (elem, idx) => {
+ this.visit(elem, astNode.subElements[idx]);
+ });
+ }
+
+ /**
+ * @param {ElementCtx} ctx
+ * @param {XMLElement} astNode
+ */
+ element(ctx, astNode) {
+ // Order of handlers can affect the result!
+ // They are ordered by priority, the more specific handlers are listed first.
+ handleElementNameWithoutPrefixScenario(ctx, astNode, this);
+
+ if (this.found === false) {
+ handleElementNameWithPrefixScenario(ctx, astNode, this);
+ }
+
+ // Traverse Deeper
+ if (this.found === false) {
+ forEach(ctx.attribute, (attrib, idx) =>
+ this.visit(attrib, astNode.attributes[idx])
+ );
+ this.visit(ctx.content, astNode);
+ }
+
+ if (this.found === false) {
+ handleNewAttributeKeyScenario(ctx, astNode, this.tokenVector, this);
+ }
+
+ if (this.found === false) {
+ handleElementContentScenario(ctx, astNode, this);
+ }
+ }
+
+ /**
+ * @param {ReferenceCtx} ctx
+ */
+ /* istanbul ignore next - place holder*/
+ reference(ctx, astNode) {}
+
+ /**
+ * @param {AttributeCtx} ctx
+ * @param {XMLAttribute} astNode
+ */
+ attribute(ctx, astNode) {
+ // potential Attribute Value scenarios
+ /* istanbul ignore else - Very difficult to reproduce specific partial CSTs */
+ if (exists(ctx.STRING)) {
+ const valueTok = ctx.STRING[0];
+ if (
+ // The content assist point must be inside the string quotes
+ valueTok.startOffset < this.targetOffset &&
+ valueTok.endOffset >= this.targetOffset
+ ) {
+ const prefixEnd = this.targetOffset - valueTok.startOffset;
+ const prefix = valueTok.image.substring(1, prefixEnd);
+ this.result.providerType = "attributeValue";
+ this.result.providerArgs = {
+ element: astNode.parent,
+ attribute: astNode,
+ prefix: prefix !== "" ? prefix : undefined,
+ };
+ this.found = true;
+ }
+ }
+
+ /* istanbul ignore else - Very difficult to reproduce specific partial CSTs */
+ if (exists(ctx.Name)) {
+ const keyTok = ctx.Name[0];
+
+ if (
+ keyTok.startOffset <= this.targetOffset &&
+ keyTok.endOffset + 1 >= this.targetOffset
+ ) {
+ const prefixEnd = this.targetOffset - keyTok.startOffset;
+ const prefix = keyTok.image.substring(0, prefixEnd);
+ this.result.providerType = "attributeName";
+ this.result.providerArgs = {
+ element: astNode.parent,
+ attribute: astNode,
+ prefix: prefix !== "" ? prefix : undefined,
+ };
+ this.found = true;
+ }
+ }
+ }
+
+ /**
+ * @param {ChardataCtx} ctx
+ */
+ /* istanbul ignore next - place holder*/
+ chardata(ctx, astNode) {}
+
+ /**
+ * @param {MiscCtx} ctx
+ */
+ /* istanbul ignore next - place holder*/
+ misc(ctx, astNode) {}
+}
+/* eslint-enable no-unused-vars -- see matching pair above */
+
+function handleElementNameWithoutPrefixScenario(ctx, astNode, visitor) {
+ /* istanbul ignore else - Very difficult to reproduce specific partial CSTs */
+ if (exists(ctx.OPEN)) {
+ const openTok = ctx.OPEN[0];
+ if (openTok.endOffset + 1 === visitor.targetOffset) {
+ visitor.result.providerType = "elementName";
+ visitor.result.providerArgs = { element: astNode, prefix: undefined };
+ visitor.found = true;
+ }
+ }
+}
+
+function handleElementNameWithPrefixScenario(ctx, astNode, visitor) {
+ /* istanbul ignore else - Very difficult to reproduce specific partial CSTs */
+ if (exists(ctx.Name)) {
+ const nameTok = ctx.Name[0];
+ if (
+ nameTok.startOffset < visitor.targetOffset &&
+ nameTok.endOffset + 1 >= visitor.targetOffset
+ ) {
+ visitor.result.providerType = "elementName";
+ const prefixLength = visitor.targetOffset - nameTok.startOffset;
+ const prefix = nameTok.image.substring(0, prefixLength);
+ visitor.result.providerArgs = { element: astNode, prefix: prefix };
+ visitor.found = true;
+ }
+ }
+}
+
+function handleNewAttributeKeyScenario(ctx, astNode, tokenVector, visitor) {
+ // Potential AttributeKey scenario in a completely new attribute
+ // Note the guard condition (in caller) to avoid this branch if one of the attributes scenarios was already detected.
+ // This means the order of checking the scenarios is meaningful!
+ // Example of the problem:
+ // - `` should be matched as attributeName **with prefix** (In attribute handler code)
+ // - `` should be matched as a **new** attributeName **without prefix** (in the code below).
+ // But the logic below cannot distinguish these, so it must only be executed if the attribute handler failed.
+ const attributesRange = { from: undefined, to: undefined };
+ let hasTerminatedAttribRange = false;
+ let hasAttributeCloseToken = false;
+ /* istanbul ignore else - Very difficult to reproduce specific partial CSTs */
+ if (exists(ctx.Name)) {
+ attributesRange.from = ctx.Name[0].endOffset + 1;
+ // Figure where does the attributes area end
+ if (exists(ctx.START_CLOSE)) {
+ attributesRange.to = ctx.START_CLOSE[0].startOffset;
+ hasTerminatedAttribRange = true;
+ hasAttributeCloseToken = true;
+ } else if (exists(ctx.SLASH_CLOSE)) {
+ attributesRange.to = ctx.SLASH_CLOSE[0].startOffset;
+ hasTerminatedAttribRange = true;
+ hasAttributeCloseToken = true;
+ // If we do not have a proper Attribute Section closing mark we use the last attribute instead
+ } else if (isEmpty(ctx.attribute) === false) {
+ attributesRange.to = last(ctx.attribute).location.endOffset;
+ hasTerminatedAttribRange = true;
+ }
+ }
+
+ if (
+ hasTerminatedAttribRange &&
+ visitor.targetOffset <= attributesRange.to &&
+ attributesRange.from <= visitor.targetOffset
+ ) {
+ const isNotInExistingAttribute =
+ find(ctx.attribute, (attribCst) => {
+ const attribLoc = attribCst.location;
+ return (
+ visitor.targetOffset >= attribLoc.startOffset &&
+ visitor.targetOffset <= attribLoc.endOffset
+ );
+ }) === undefined;
+
+ // inside attribute area but not contained in any existing attribute
+ /* istanbul ignore else - else branch is handled inside the attributes themselves and can never occur here */
+ if (isNotInExistingAttribute) {
+ visitor.result.providerType = "attributeName";
+ visitor.result.providerArgs = {
+ element: astNode,
+ attribute: undefined,
+ prefix: undefined,
+ };
+ visitor.found = true;
+ }
+ } else if (
+ /**
+ * Heuristic when we some attributes but no proper attribute Range
+ * and we request the assist after the last attribute.
+ *
+ *
+ */
+ hasTerminatedAttribRange &&
+ visitor.targetOffset > attributesRange.to &&
+ hasAttributeCloseToken === false
+ ) {
+ handleNewAttributeKeyForPartialElement(
+ // dummy pesudo token a we are searching the following token using the endOffset
+ { endOffset: attributesRange.to },
+ tokenVector,
+ visitor,
+ astNode
+ );
+ /**
+ * Heuristic when we have no attribute range, e.g:
+ *
+ *
+ */
+ } else if (hasTerminatedAttribRange === false) {
+ handleNewAttributeKeyForPartialElement(
+ ctx.Name ? ctx.Name[0] : ctx.OPEN[0],
+ tokenVector,
+ visitor,
+ astNode
+ );
+ }
+}
+
+function handleNewAttributeKeyForPartialElement(
+ possibleAttribKeyRangeStartTok,
+ tokenVector,
+ visitor,
+ astNode
+) {
+ const nextAfterElemNameTok = findNextTextualToken(
+ tokenVector,
+ possibleAttribKeyRangeStartTok.endOffset
+ );
+ if (
+ visitor.targetOffset > possibleAttribKeyRangeStartTok.endOffset &&
+ (nextAfterElemNameTok === null || // `null`` means there are no more textual tokens in the input
+ visitor.targetOffset <= nextAfterElemNameTok.startOffset)
+ ) {
+ visitor.result.providerType = "attributeName";
+ visitor.result.providerArgs = {
+ element: astNode,
+ attribute: undefined,
+ prefix: undefined,
+ };
+ visitor.found = true;
+ }
+}
+
+function handleElementContentScenario(ctx, astNode, visitor) {
+ const contentRange = { from: undefined, to: undefined };
+ let hasContentRange = false;
+ /* istanbul ignore else - Very difficult to reproduce specific partial CSTs */
+ if (exists(ctx.START_CLOSE)) {
+ contentRange.from = ctx.START_CLOSE[0].endOffset + 1;
+ // visitor logic only works when the attribute section is properly terminated
+ /* istanbul ignore else - Very difficult to reproduce specific partial CSTs */
+ if (exists(ctx.SLASH_OPEN)) {
+ contentRange.to = ctx.SLASH_OPEN[0].startOffset;
+ hasContentRange = true;
+ } else if (exists(ctx.SLASH_CLOSE)) {
+ contentRange.to = ctx.SLASH_CLOSE[0].startOffset;
+ hasContentRange = true;
+ }
+ }
+
+ if (
+ hasContentRange &&
+ visitor.targetOffset <= contentRange.to &&
+ contentRange.from <= visitor.targetOffset
+ ) {
+ const allContentChildren = flatMap(ctx.content[0].children, identity);
+ const innerContentPart = find(allContentChildren, (subContent) => {
+ // Handling either CSTNodes or Tokens
+ const subContentLoc = subContent.location
+ ? subContent.location
+ : subContent;
+
+ // Our offset ranges are Inclusive to Exclusive
+ // abc⇶ --> Not inside the CharData Location, but uses `abc` as prefix
+ // ⇶abc --> Inside the CharData Location, but does not have any prefix
+ const targetOffsetForPrefix = visitor.targetOffset - 1;
+
+ return (
+ targetOffsetForPrefix >= subContentLoc.startOffset &&
+ targetOffsetForPrefix <= subContentLoc.endOffset
+ );
+ });
+
+ // ElementContent without prefix
+ if (innerContentPart === undefined) {
+ visitor.result.providerType = "elementContent";
+ visitor.result.providerArgs = {
+ element: astNode,
+ textContent: undefined,
+ prefix: undefined,
+ };
+ visitor.found = true;
+ } else if (innerContentPart.name === "chardata") {
+ const textNodeIdx = findIndex(
+ ctx.content[0].children.chardata,
+ innerContentPart
+ );
+ const textContentsAstNode = astNode.textContents[textNodeIdx];
+ const prefixEnd =
+ visitor.targetOffset - textContentsAstNode.position.startOffset;
+ visitor.result.providerType = "elementContent";
+ visitor.result.providerArgs = {
+ element: astNode,
+ textContent: textContentsAstNode,
+ prefix: textContentsAstNode.text.substring(0, prefixEnd),
+ };
+ visitor.found = true;
+ }
+ }
+}
+
+function exists(tokArr) {
+ return (
+ isArray(tokArr) &&
+ tokArr.length === 1 &&
+ tokArr[0].isInsertedInRecovery !== true
+ );
+}
+
+module.exports = {
+ computeCompletionSyntacticContext: computeCompletionSyntacticContext,
+};
diff --git a/projects/xml-tools/packages/content-assist/nyc.config.js b/projects/xml-tools/packages/content-assist/nyc.config.js
new file mode 100644
index 00000000..3e3f8962
--- /dev/null
+++ b/projects/xml-tools/packages/content-assist/nyc.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ // xml-tools sources live in lib/ (not src/), so this local config overrides
+ // the repo-root nyc.config.js include of **/src/** (which would report 0%).
+ include: ["lib/**/*.js"],
+ reporter: ["text", "lcov"],
+ all: true,
+ branches: 100,
+ lines: 100,
+ functions: 100,
+ statements: 100,
+};
diff --git a/projects/xml-tools/packages/content-assist/package.json b/projects/xml-tools/packages/content-assist/package.json
new file mode 100644
index 00000000..63896403
--- /dev/null
+++ b/projects/xml-tools/packages/content-assist/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@xml-tools/content-assist",
+ "version": "3.2.0",
+ "description": "XML Content Assist APIs",
+ "keywords": [
+ "xml",
+ "auto-complete",
+ "content-assist"
+ ],
+ "main": "lib/api.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/SAP/app-studio-toolkit.git",
+ "directory": "projects/xml-tools/packages/content-assist"
+ },
+ "license": "Apache-2.0",
+ "typings": "./api.d.ts",
+ "files": [
+ "lib",
+ "api.d.ts",
+ "CHANGELOG.md",
+ "CHANGELOG.old.md"
+ ],
+ "dependencies": {
+ "@xml-tools/ast": "workspace:*",
+ "@xml-tools/common": "workspace:*",
+ "@xml-tools/parser": "workspace:*",
+ "chevrotain": "7.1.1",
+ "lodash": "4.17.23"
+ },
+ "scripts": {
+ "ci": "npm-run-all clean type-check coverage:*",
+ "clean": "rimraf ./coverage ./nyc_output",
+ "test": "mocha \"./test/**/*spec.js\"",
+ "coverage:run": "nyc mocha \"./test/**/*spec.js\"",
+ "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100",
+ "type-check": "tsc"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "bugs": {
+ "url": "https://github.com/SAP/app-studio-toolkit/issues"
+ }
+}
diff --git a/projects/xml-tools/packages/content-assist/test/scenarios-spec.js b/projects/xml-tools/packages/content-assist/test/scenarios-spec.js
new file mode 100644
index 00000000..54bea315
--- /dev/null
+++ b/projects/xml-tools/packages/content-assist/test/scenarios-spec.js
@@ -0,0 +1,555 @@
+const { parse } = require("@xml-tools/parser");
+const { buildAst } = require("@xml-tools/ast");
+const { expect } = require("chai");
+const { getSuggestions } = require("../");
+
+describe("The XML Content Assist Capabilities", () => {
+ describe("Can provide suggestion for", () => {
+ context("element Content", () => {
+ it("in middle of text content", () => {
+ const sample = `hello ⇶world`;
+
+ let providerCalled = false;
+ getSampleSuggestions(
+ sample,
+ {
+ elementContent: [
+ ({ element, prefix, textContent, context }) => {
+ expect(prefix).to.eql("hello ");
+ expect(textContent.text).to.eql("hello world");
+ expect(element.name).to.eql("person");
+ expect(context).to.eql(999);
+ providerCalled = true;
+ },
+ ],
+ },
+ 999
+ );
+ expect(providerCalled).to.be.true;
+ });
+
+ it("With other elements", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\t⇶\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ elementContent: [
+ ({ element, prefix, textContent, context }) => {
+ expect(prefix).to.eql("\n\t");
+ expect(textContent.text).to.eql("\n\t\n\t");
+ expect(element.name).to.eql("person");
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("In empty element", () => {
+ const sample = `` + `⇶` + ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ elementContent: [
+ ({ element, prefix, textContent, context }) => {
+ expect(prefix).to.be.undefined;
+ expect(textContent).to.be.undefined;
+ expect(element.name).to.eql("person");
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("Before invalid element", () => {
+ const sample = `` + `⇶` + `<\n`;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ elementContent: [
+ ({ element, prefix, textContent, context }) => {
+ expect(prefix).to.be.undefined;
+ expect(textContent).to.be.undefined;
+ expect(element.name).to.eql("person");
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("before comment", () => {
+ const sample =
+ `\n` +
+ `\t⇶\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ elementContent: [
+ ({ element, prefix, textContent, context }) => {
+ expect(prefix).to.eql("\n\t");
+ expect(textContent.text).to.eql("\n\t");
+ expect(element.name).to.eql("person");
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("inside comment - will not trigger", () => {
+ const sample =
+ `\n` +
+ `\t\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ elementContent: [
+ // eslint-disable-next-line no-unused-vars -- match the type of `ElementContentCompletion` for consistency
+ ({ element }) => {
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.false;
+ });
+ });
+
+ context("element Name", () => {
+ it("With Prefix and Context", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(
+ sample,
+ {
+ elementName: [
+ ({ element, prefix, context }) => {
+ expect(prefix).to.eql("ag");
+ expect(element.name).to.eql("age");
+ expect(context).to.eql(666);
+ providerCalled = true;
+ },
+ ],
+ },
+ 666
+ );
+ expect(providerCalled).to.be.true;
+ });
+
+ it("With Prefix - end", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ elementName: [
+ ({ element, prefix, context }) => {
+ expect(prefix).to.eql("age");
+ expect(element.name).to.eql("age");
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("without Prefix", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\t<⇶\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ elementName: [
+ ({ element, prefix, context }) => {
+ expect(prefix).to.be.undefined;
+ expect(element.name).to.be.null;
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+ });
+
+ context("attribute Value", () => {
+ it("Without prefix and empty value and context", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(
+ sample,
+ {
+ attributeValue: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("person");
+ expect(attribute.key).to.eql("gender");
+ expect(prefix).to.be.undefined;
+ expect(context).to.eql(123);
+ providerCalled = true;
+ },
+ ],
+ },
+ 123
+ );
+ expect(providerCalled).to.be.true;
+ });
+
+ it("Without prefix but with value", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ attributeValue: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("lastname");
+ expect(attribute.key).to.eql("language");
+ expect(attribute.value).to.eql("english");
+ expect(prefix).to.be.undefined;
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("With prefix identical to value", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ attributeValue: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("lastname");
+ expect(attribute.key).to.eql("language");
+ expect(attribute.value).to.eql("eng");
+ expect(prefix).to.eql("eng");
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("With prefix different from value", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ attributeValue: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("lastname");
+ expect(attribute.key).to.eql("language");
+ expect(attribute.value).to.eql("english");
+ expect(prefix).to.eql("eng");
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+ });
+
+ context("attribute name", () => {
+ it("With prefix identical to existing key and context", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(
+ sample,
+ {
+ attributeName: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("person");
+ expect(attribute.key).to.eql("gen");
+ expect(prefix).to.eql("gen");
+ expect(context).to.eql(333);
+ providerCalled = true;
+ },
+ ],
+ },
+ 333
+ );
+ expect(providerCalled).to.be.true;
+ });
+
+ it("With prefix which is a subset of an existing key", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ attributeName: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("person");
+ expect(attribute.key).to.eql("gender");
+ expect(prefix).to.eql("gen");
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ context("with empty prefix", () => {
+ it("With empty prefix which is a subset of an existing key", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ attributeName: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("person");
+ expect(attribute.key).to.eql("gen");
+ expect(prefix).to.be.undefined;
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("without key", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ attributeName: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("person");
+ expect(attribute).to.be.undefined;
+ expect(prefix).to.be.undefined;
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("without key - short ElementForm", () => {
+ const sample = ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ attributeName: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("person");
+ expect(attribute).to.be.undefined;
+ expect(prefix).to.be.undefined;
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("without key - end", () => {
+ const sample =
+ `\n` +
+ `\tAnna\n` +
+ `\tSmith\n` +
+ ``;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ attributeName: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("person");
+ expect(attribute).to.be.undefined;
+ expect(prefix).to.be.undefined;
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("without attributes section close", () => {
+ const sample = `
+ `;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ attributeName: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("person");
+ expect(attribute).to.be.undefined;
+ expect(prefix).to.be.undefined;
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("without attributes section close with prior attributes", () => {
+ const sample = `
+ `;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ attributeName: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("person");
+ expect(attribute).to.be.undefined;
+ expect(prefix).to.be.undefined;
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("without attributes section close with following attributes", () => {
+ const sample = `
+ `;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ attributeName: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("person");
+ expect(attribute).to.be.undefined;
+ expect(prefix).to.be.undefined;
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("without attributes section close with following and prior attributes", () => {
+ const sample = `
+ `;
+
+ let providerCalled = false;
+ getSampleSuggestions(sample, {
+ attributeName: [
+ ({ element, attribute, prefix, context }) => {
+ expect(element.name).to.eql("person");
+ expect(attribute).to.be.undefined;
+ expect(prefix).to.be.undefined;
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+
+ it("without attributes section close - no following tokens", () => {
+ const sample = `
+ {
+ expect(element.name).to.eql("person");
+ expect(attribute).to.be.undefined;
+ expect(prefix).to.be.undefined;
+ expect(context).to.be.undefined;
+ providerCalled = true;
+ },
+ ],
+ });
+ expect(providerCalled).to.be.true;
+ });
+ });
+ });
+ });
+});
+
+/**
+ * Get Content Assist Suggestions from a sample which marks the offset
+ * with unicode triple arrow: " ⇶ "
+ */
+function getSampleSuggestions(sample, providers, context) {
+ const realSample = sample.replace("⇶", "");
+ const offset = sample.indexOf("⇶");
+ const { cst, tokenVector } = parse(realSample);
+ const ast = buildAst(cst, tokenVector);
+
+ return getSuggestions({
+ cst,
+ ast,
+ tokenVector,
+ offset: offset,
+ providers: providers,
+ context: context,
+ });
+}
diff --git a/projects/xml-tools/packages/content-assist/tsconfig.json b/projects/xml-tools/packages/content-assist/tsconfig.json
new file mode 100644
index 00000000..a4ed07be
--- /dev/null
+++ b/projects/xml-tools/packages/content-assist/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": ["./api.d.ts"]
+}
diff --git a/projects/xml-tools/packages/language-server/.gitignore b/projects/xml-tools/packages/language-server/.gitignore
new file mode 100644
index 00000000..16d3c4db
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/.gitignore
@@ -0,0 +1 @@
+.cache
diff --git a/projects/xml-tools/packages/language-server/CHANGELOG.old.md b/projects/xml-tools/packages/language-server/CHANGELOG.old.md
new file mode 100644
index 00000000..af42e22d
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/CHANGELOG.old.md
@@ -0,0 +1,86 @@
+> **Historic changelog.** This records changes made while the project lived in the
+> standalone [SAP/xml-tools](https://github.com/SAP/xml-tools) repository, before it was
+> migrated into [SAP/app-studio-toolkit](https://github.com/SAP/app-studio-toolkit).
+> Newer entries are generated by Changesets in `CHANGELOG.md`.
+
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [1.2.0](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.1.1...@xml-tools/language-server@1.2.0) (2026-02-11)
+
+### Features
+
+- update lodash dependency to version 4.17.23 ([#461](https://github.com/SAP/xml-tools/issues/461)) ([23a319d](https://github.com/SAP/xml-tools/commit/23a319d6ed42a4a3885ec5748eb127147a8960bc))
+
+## [1.1.1](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.1.0...@xml-tools/language-server@1.1.1) (2021-06-03)
+
+**Note:** Version bump only for package @xml-tools/language-server
+
+# [1.1.0](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.0.12...@xml-tools/language-server@1.1.0) (2021-02-09)
+
+### Bug Fixes
+
+- use extension name as diagnostic source ([#297](https://github.com/SAP/xml-tools/issues/297)) ([f6d7dc4](https://github.com/SAP/xml-tools/commit/f6d7dc42a05c4e1d7c9ab3f1fbe35ecb1eb820fa))
+
+### Features
+
+- well formed constraints validations ([#293](https://github.com/SAP/xml-tools/issues/293)) ([8a9d535](https://github.com/SAP/xml-tools/commit/8a9d535e95a52b9dfc1068d2e203b09bd08e1066)), closes [#13](https://github.com/SAP/xml-tools/issues/13) [#14](https://github.com/SAP/xml-tools/issues/14)
+
+## [1.0.12](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.0.11...@xml-tools/language-server@1.0.12) (2021-01-20)
+
+### Bug Fixes
+
+- missing lib folder in npm packages ([ffed3c2](https://github.com/SAP/xml-tools/commit/ffed3c2c54c70aea8b9ded0d53786382bc190cc5))
+
+## [1.0.11](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.0.10...@xml-tools/language-server@1.0.11) (2021-01-20)
+
+**Note:** Version bump only for package @xml-tools/language-server
+
+## [1.0.10](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.0.9...@xml-tools/language-server@1.0.10) (2020-06-29)
+
+**Note:** Version bump only for package @xml-tools/language-server
+
+## [1.0.9](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.0.8...@xml-tools/language-server@1.0.9) (2020-06-18)
+
+**Note:** Version bump only for package @xml-tools/language-server
+
+## [1.0.8](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.0.7...@xml-tools/language-server@1.0.8) (2020-05-21)
+
+**Note:** Version bump only for package @xml-tools/language-server
+
+## [1.0.7](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.0.6...@xml-tools/language-server@1.0.7) (2020-05-17)
+
+**Note:** Version bump only for package @xml-tools/language-server
+
+## [1.0.6](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.0.5...@xml-tools/language-server@1.0.6) (2020-04-02)
+
+**Note:** Version bump only for package @xml-tools/language-server
+
+## [1.0.5](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.0.4...@xml-tools/language-server@1.0.5) (2020-03-08)
+
+**Note:** Version bump only for package @xml-tools/language-server
+
+## [1.0.4](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.0.2...@xml-tools/language-server@1.0.4) (2020-03-08)
+
+### Bug Fixes
+
+- fix README.md ([472301d](https://github.com/SAP/xml-tools/commit/472301d))
+
+## [1.0.3](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.0.2...@xml-tools/language-server@1.0.3) (2020-03-08)
+
+### Bug Fixes
+
+- fix README.md ([472301d](https://github.com/SAP/xml-tools/commit/472301d))
+
+## [1.0.2](https://github.com/SAP/xml-tools/compare/@xml-tools/language-server@1.0.1...@xml-tools/language-server@1.0.2) (2020-02-19)
+
+### Bug Fixes
+
+- add npmignore file to each package ([5bbf209](https://github.com/SAP/xml-tools/commit/5bbf209))
+- missing import ([2a51ab9](https://github.com/SAP/xml-tools/commit/2a51ab9))
+
+## 1.0.1 (2020-02-19)
+
+**Note:** Version bump only for package @xml-tools/language-server
diff --git a/projects/xml-tools/packages/language-server/README.md b/projects/xml-tools/packages/language-server/README.md
new file mode 100644
index 00000000..44da6f28
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/README.md
@@ -0,0 +1,41 @@
+[](https://www.npmjs.com/package/@xml-tools/language-server)
+
+# @xml-tools/language-server
+
+XML Language Server
+
+Current Features:
+
+- Syntax Diagnostics.
+
+## Installation
+
+With npm:
+
+- `npm install @xml-tools/language-server`
+
+With Yarn
+
+- `yarn add @xml-tools/language-server`
+
+## Usage
+
+Please see the [TypeScript Definitions](./api.d.ts) for full API details.
+
+A simple usage example:
+
+```javascript
+const { SERVER_PATH } = require("@xml-tools/language-server");
+
+// SERVER_PATH is the only API currently and it is meant to expose the "main" module's absolute
+// path which would then be executed in a different process
+console.log(SERVER_PATH); // --> .../node_module/@xml-tools/language-server/lib/server.js
+```
+
+## Support
+
+Please report issues [here](https://github.com/SAP/app-studio-toolkit/issues/new/choose) and label them with `project:xml-tools`.
+
+## Contributing
+
+See [CONTRIBUTING.md](https://github.com/SAP/app-studio-toolkit/blob/main/CONTRIBUTING.md).
diff --git a/projects/xml-tools/packages/language-server/api.d.ts b/projects/xml-tools/packages/language-server/api.d.ts
new file mode 100644
index 00000000..99cda8dc
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/api.d.ts
@@ -0,0 +1,13 @@
+/**
+ * Absolute path to the server's "main" module
+ * This is useful when launching the server in a separate process (e.g via spawn).
+ */
+export const SERVER_PATH: string;
+
+export type ServerInitializationOptions = {
+ /**
+ * optional name of the IDE plugin/extension using this language server.
+ * This name would be used in the `source` property of dignostic issues.
+ */
+ consumer?: string;
+};
diff --git a/projects/xml-tools/packages/language-server/lib/api.js b/projects/xml-tools/packages/language-server/lib/api.js
new file mode 100644
index 00000000..a639a43a
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/lib/api.js
@@ -0,0 +1,19 @@
+const { existsSync } = require("fs");
+const { resolve } = require("path");
+
+// for use in productive flows (webpack-bundled, shipped inside the .vsix)
+const bundledPath = resolve(__dirname, "..", "dist", "server.js");
+// for dev flows (unbundled sources)
+const sourcesPath = resolve(__dirname, "server.js");
+
+// Prefer the bundled server if it was produced (production / packaged .vsix),
+// otherwise fall back to the unbundled sources (development).
+// The previous heuristic keyed off the existence of `node_modules`, which is
+// unreliable under pnpm (it always creates a symlinked node_modules), so we
+// detect the artifact directly instead.
+/* istanbul ignore next - no tests (yet?) on bundled artifacts */
+const serverPath = existsSync(bundledPath) ? bundledPath : sourcesPath;
+
+module.exports = {
+ SERVER_PATH: serverPath,
+};
diff --git a/projects/xml-tools/packages/language-server/lib/constants.js b/projects/xml-tools/packages/language-server/lib/constants.js
new file mode 100644
index 00000000..dde36454
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/lib/constants.js
@@ -0,0 +1,5 @@
+const DEFAULT_CONSUMER_NAME = "XML LS";
+
+module.exports = {
+ DEFAULT_CONSUMER_NAME,
+};
diff --git a/projects/xml-tools/packages/language-server/lib/language-services.js b/projects/xml-tools/packages/language-server/lib/language-services.js
new file mode 100644
index 00000000..3d016fa3
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/lib/language-services.js
@@ -0,0 +1,109 @@
+const { DiagnosticSeverity, Range } = require("vscode-languageserver");
+const { map, forEach } = require("lodash");
+const { parse } = require("@xml-tools/parser");
+const { buildAst } = require("@xml-tools/ast");
+const { checkConstraints } = require("@xml-tools/constraints");
+
+/**
+ * @param {TextDocument} document
+ * @param {import("chevrotain").ILexingError} error
+ * @returns {import("vscode-languageserver-types").Diagnostic}
+ */
+function lexingErrorToDiagnostic(document, error) {
+ return {
+ message: error.message,
+ range: Range.create(
+ document.positionAt(error.offset),
+ document.positionAt(error.offset + error.length)
+ ),
+ severity: DiagnosticSeverity.Error,
+ };
+}
+
+/**
+ * @param {TextDocument} document
+ * @param {import("chevrotain").ILexingError} error
+ * @returns {import("vscode-languageserver-types").Diagnostic}
+ */
+function parsingErrorToDiagnostic(document, error) {
+ return {
+ message: error.message,
+ range: {
+ start: document.positionAt(error.token.startOffset),
+ end: document.positionAt(
+ error.token.endOffset ? error.token.endOffset : 0
+ ),
+ },
+ severity: DiagnosticSeverity.Error,
+ };
+}
+
+/**
+ * @param {TextDocument} document
+ * @param {import("@xml-tools/validation").ValidationIssue} issue
+ * @returns {import("vscode-languageserver-types").Diagnostic}
+ */
+function constraintIssueToDiagnostic(document, issue) {
+ return {
+ message: issue.msg,
+ range: {
+ start: document.positionAt(issue.position.startOffset),
+ // Chevrotain Token positions are non-inclusive for endOffsets
+ end: document.positionAt(issue.position.endOffset + 1),
+ },
+ severity: toDiagnosticSeverity(issue.severity),
+ };
+}
+
+/**
+ * @param {"info" | "warning" | "error"} issueSeverity
+ * @returns {import("vscode-languageserver-types").DiagnosticSeverity}
+ */
+function toDiagnosticSeverity(issueSeverity) {
+ switch (issueSeverity) {
+ case "error":
+ return DiagnosticSeverity.Error;
+ case "warning":
+ return DiagnosticSeverity.Warning;
+ case "info":
+ return DiagnosticSeverity.Information;
+ /* istanbul ignore next -- runtime check */
+ default:
+ throw Error("Non Exhaustive Match");
+ }
+}
+
+/**
+ * @param {TextDocument} document
+ * @param {object} opts
+ * @param {string} opts.consumer
+ *
+ * @returns {import("vscode-languageserver-types").Diagnostic[]}
+ */
+async function validateDocument(document, opts) {
+ let diagnostics = [];
+ if (document.languageId === "xml") {
+ const { cst, tokenVector, lexErrors, parseErrors } = parse(
+ document.getText()
+ );
+ const ast = buildAst(cst, tokenVector);
+ const constraintsIssues = checkConstraints(ast);
+
+ diagnostics = [
+ ...map(lexErrors, (_) => lexingErrorToDiagnostic(document, _)),
+ ...map(parseErrors, (_) => parsingErrorToDiagnostic(document, _)),
+ ...map(constraintsIssues, (_) =>
+ constraintIssueToDiagnostic(document, _)
+ ),
+ ];
+ }
+
+ forEach(diagnostics, (_) => (_.source = opts.consumer));
+
+ return diagnostics;
+}
+
+module.exports = {
+ validateDocument: validateDocument,
+ toDiagnosticSeverity: toDiagnosticSeverity,
+};
diff --git a/projects/xml-tools/packages/language-server/lib/server.js b/projects/xml-tools/packages/language-server/lib/server.js
new file mode 100644
index 00000000..0fabfa30
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/lib/server.js
@@ -0,0 +1,37 @@
+const {
+ createConnection,
+ ProposedFeatures,
+ TextDocuments,
+ TextDocumentSyncKind,
+} = require("vscode-languageserver");
+const { TextDocument } = require("vscode-languageserver-textdocument");
+const { has } = require("lodash");
+const { validateDocument } = require("./language-services");
+const { DEFAULT_CONSUMER_NAME } = require("./constants");
+
+const connection = createConnection(ProposedFeatures.all);
+const documents = new TextDocuments(TextDocument);
+
+let consumer;
+
+connection.onInitialize((params) => {
+ if (has(params, "initializationOptions.consumer")) {
+ consumer = params.initializationOptions.consumer;
+ } else {
+ consumer = DEFAULT_CONSUMER_NAME;
+ }
+
+ return {
+ capabilities: {
+ textDocumentSync: TextDocumentSyncKind.Full,
+ },
+ };
+});
+
+documents.onDidChangeContent(async (event) => {
+ const diagnostics = await validateDocument(event.document, { consumer });
+ connection.sendDiagnostics({ uri: event.document.uri, diagnostics });
+});
+
+documents.listen(connection);
+connection.listen();
diff --git a/projects/xml-tools/packages/language-server/nyc.config.js b/projects/xml-tools/packages/language-server/nyc.config.js
new file mode 100644
index 00000000..3e3f8962
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/nyc.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ // xml-tools sources live in lib/ (not src/), so this local config overrides
+ // the repo-root nyc.config.js include of **/src/** (which would report 0%).
+ include: ["lib/**/*.js"],
+ reporter: ["text", "lcov"],
+ all: true,
+ branches: 100,
+ lines: 100,
+ functions: 100,
+ statements: 100,
+};
diff --git a/projects/xml-tools/packages/language-server/package.json b/projects/xml-tools/packages/language-server/package.json
new file mode 100644
index 00000000..f40fae60
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@xml-tools/language-server",
+ "version": "1.2.0",
+ "description": "XML language server",
+ "keywords": [
+ "LSP",
+ "language-server",
+ "XML"
+ ],
+ "main": "lib/api.js",
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/SAP/app-studio-toolkit.git",
+ "directory": "projects/xml-tools/packages/language-server"
+ },
+ "typings": "./api.d.ts",
+ "files": [
+ "dist",
+ "lib",
+ "api.d.ts",
+ "CHANGELOG.md",
+ "CHANGELOG.old.md"
+ ],
+ "dependencies": {
+ "@xml-tools/ast": "workspace:*",
+ "@xml-tools/constraints": "workspace:*",
+ "@xml-tools/parser": "workspace:*",
+ "lodash": "4.17.23",
+ "vscode-languageserver": "7.0.0",
+ "vscode-languageserver-textdocument": "1.0.1"
+ },
+ "devDependencies": {
+ "chevrotain": "^7.0.1",
+ "vscode-languageserver-types": "3.16.0"
+ },
+ "scripts": {
+ "ci": "npm-run-all clean type-check coverage bundle",
+ "clean": "rimraf ./dist ./coverage ./nyc_output",
+ "test": "mocha \"./test/**/*spec.js\"",
+ "coverage": "nyc mocha \"./test/**/*spec.js\"",
+ "type-check": "tsc",
+ "bundle": "webpack --mode production"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "bugs": {
+ "url": "https://github.com/SAP/app-studio-toolkit/issues"
+ }
+}
diff --git a/projects/xml-tools/packages/language-server/test/langauge-services.spec.js b/projects/xml-tools/packages/language-server/test/langauge-services.spec.js
new file mode 100644
index 00000000..32d00e70
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/test/langauge-services.spec.js
@@ -0,0 +1,153 @@
+const { resolve } = require("path");
+const { expect } = require("chai");
+const { TextDocument, DiagnosticSeverity } = require("vscode-languageserver");
+const { DEFAULT_CONSUMER_NAME } = require("../lib/constants");
+const { toDiagnosticSeverity } = require("../lib/language-services");
+
+const { validateDocument } = require("../lib/language-services");
+const { SERVER_PATH } = require("../lib/api");
+
+describe("the XML Language Services", () => {
+ it("will get the correct path to server module", async () => {
+ const serverPath = resolve(__dirname, "../", "lib", "server.js");
+ expect(SERVER_PATH).to.deep.equal(serverPath);
+ });
+
+ it("will detect a parsing error in an xml document", async () => {
+ const doc = createTextDocument("xml", ">");
+ const pos = { line: 0, character: 0 };
+ const expectedDiagnostics = [
+ {
+ message: "Expecting token of type --> OPEN <-- but found --> '>' <--",
+ range: {
+ start: pos,
+ end: pos,
+ },
+ severity: DiagnosticSeverity.Error,
+ source: "XML LS",
+ },
+ ];
+ const diagnostics = await validateDocument(doc, {
+ consumer: DEFAULT_CONSUMER_NAME,
+ });
+ expect(diagnostics).to.deep.equal(expectedDiagnostics);
+ });
+
+ it("will detect a lexing error in an xml document", async () => {
+ const doc = createTextDocument("xml", "!>");
+ const start = { line: 0, character: 5 };
+ const pos = { line: 0, character: 6 };
+
+ const expectedDiagnostics = [
+ {
+ message: `unexpected character: ->!<- at offset: 5, skipped 1 characters.`,
+ range: {
+ start: start,
+ end: pos,
+ },
+ severity: DiagnosticSeverity.Error,
+ source: "XML LS",
+ },
+ {
+ message: `Expecting token of type --> Name <-- but found --> '>' <--`,
+ range: {
+ start: pos,
+ end: pos,
+ },
+ severity: DiagnosticSeverity.Error,
+ source: "XML LS",
+ },
+ ];
+ const diagnostics = await validateDocument(doc, {
+ consumer: DEFAULT_CONSUMER_NAME,
+ });
+ expect(diagnostics).to.deep.equal(expectedDiagnostics);
+ });
+
+ it("will detect a well-formedness issue in an xml document", async () => {
+ const xmlSnippet = "";
+ const doc = createTextDocument("xml", xmlSnippet);
+
+ const diagnostics = await validateDocument(doc, {
+ consumer: DEFAULT_CONSUMER_NAME,
+ });
+ expect(diagnostics).to.have.lengthOf(2);
+ expect(diagnostics).to.deep.include.members([
+ {
+ message: 'tags mismatch: "a" must match closing tag: "bc"',
+ range: {
+ start: {
+ line: 0,
+ character: xmlSnippet.indexOf("a"),
+ },
+ end: {
+ line: 0,
+ character: xmlSnippet.indexOf("a") + 1,
+ },
+ },
+ severity: 1,
+ source: "XML LS",
+ },
+ {
+ message: 'tags mismatch: "bc" must match opening tag: "a"',
+ range: {
+ start: {
+ line: 0,
+ character: xmlSnippet.indexOf("bc"),
+ },
+ end: {
+ line: 0,
+ character: xmlSnippet.indexOf("bc") + 2,
+ },
+ },
+ severity: 1,
+ source: "XML LS",
+ },
+ ]);
+ });
+
+ it("will not detect any error in a none XML document", async () => {
+ const doc = createTextDocument("txt", ">");
+ const diagnostics = await validateDocument(doc, {
+ consumer: DEFAULT_CONSUMER_NAME,
+ });
+ expect(diagnostics).to.deep.equal([]);
+ });
+
+ context("severity transformations", () => {
+ it("will transform `error` severity correctly", () => {
+ expect(toDiagnosticSeverity("error")).to.equal(DiagnosticSeverity.Error);
+ });
+
+ it("will transform `warning` severity correctly", () => {
+ expect(toDiagnosticSeverity("warning")).to.equal(
+ DiagnosticSeverity.Warning
+ );
+ });
+
+ it("will transform `info` severity correctly", () => {
+ expect(toDiagnosticSeverity("info")).to.equal(
+ DiagnosticSeverity.Information
+ );
+ });
+ });
+
+ context("custom diagnostic `source` support", () => {
+ it("will use the `consumer` option in `validateDocument` as the diagnostics `source` prop", async () => {
+ const doc = createTextDocument("xml", ">");
+ const diagnostics = await validateDocument(doc, {
+ consumer: "my xml eclipse plugin",
+ });
+ expect(diagnostics).to.have.lengthOf(1);
+ expect(diagnostics[0].source).to.equal("my xml eclipse plugin");
+ });
+ });
+});
+
+/**
+ * @param {string} languageId
+ * @param {string} content
+ */
+function createTextDocument(languageId, content) {
+ return TextDocument.create("uri", languageId, 0, content);
+}
diff --git a/projects/xml-tools/packages/language-server/tsconfig.json b/projects/xml-tools/packages/language-server/tsconfig.json
new file mode 100644
index 00000000..a4ed07be
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": ["./api.d.ts"]
+}
diff --git a/projects/xml-tools/packages/language-server/webpack.config.js b/projects/xml-tools/packages/language-server/webpack.config.js
new file mode 100644
index 00000000..5fba1e9b
--- /dev/null
+++ b/projects/xml-tools/packages/language-server/webpack.config.js
@@ -0,0 +1,14 @@
+const path = require("path");
+const baseConfig = require("../../../../webpack.config.vscode.base");
+
+const config = {
+ ...baseConfig,
+ entry: "./lib/server.js",
+ output: {
+ path: path.resolve(__dirname, "dist"),
+ filename: "server.js",
+ libraryTarget: "commonjs2",
+ devtoolModuleFilenameTemplate: "../[resource-path]",
+ },
+};
+module.exports = config;
diff --git a/projects/xml-tools/packages/parser/CHANGELOG.old.md b/projects/xml-tools/packages/parser/CHANGELOG.old.md
new file mode 100644
index 00000000..9463ca8b
--- /dev/null
+++ b/projects/xml-tools/packages/parser/CHANGELOG.old.md
@@ -0,0 +1,114 @@
+> **Historic changelog.** This records changes made while the project lived in the
+> standalone [SAP/xml-tools](https://github.com/SAP/xml-tools) repository, before it was
+> migrated into [SAP/app-studio-toolkit](https://github.com/SAP/app-studio-toolkit).
+> Newer entries are generated by Changesets in `CHANGELOG.md`.
+
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+## [1.0.11](https://github.com/sap/xml-tools/compare/@xml-tools/parser@1.0.10...@xml-tools/parser@1.0.11) (2021-06-03)
+
+**Note:** Version bump only for package @xml-tools/parser
+
+## [1.0.10](https://github.com/sap/xml-tools/compare/@xml-tools/parser@1.0.9...@xml-tools/parser@1.0.10) (2021-02-09)
+
+**Note:** Version bump only for package @xml-tools/parser
+
+## [1.0.9](https://github.com/sap/xml-tools/compare/@xml-tools/parser@1.0.8...@xml-tools/parser@1.0.9) (2021-01-20)
+
+### Bug Fixes
+
+- missing lib folder in npm packages ([ffed3c2](https://github.com/sap/xml-tools/commit/ffed3c2c54c70aea8b9ded0d53786382bc190cc5))
+
+## [1.0.8](https://github.com/sap/xml-tools/compare/@xml-tools/parser@1.0.7...@xml-tools/parser@1.0.8) (2021-01-20)
+
+**Note:** Version bump only for package @xml-tools/parser
+
+## [1.0.7](https://github.com/sap/xml-tools/compare/@xml-tools/parser@1.0.6...@xml-tools/parser@1.0.7) (2020-06-29)
+
+**Note:** Version bump only for package @xml-tools/parser
+
+## [1.0.6](https://github.com/sap/xml-tools/compare/@xml-tools/parser@1.0.5...@xml-tools/parser@1.0.6) (2020-06-18)
+
+**Note:** Version bump only for package @xml-tools/parser
+
+## [1.0.5](https://github.com/sap/xml-tools/compare/@xml-tools/parser@1.0.4...@xml-tools/parser@1.0.5) (2020-05-17)
+
+**Note:** Version bump only for package @xml-tools/parser
+
+## [1.0.4](https://github.com/sap/xml-tools/compare/@xml-tools/parser@1.0.3...@xml-tools/parser@1.0.4) (2020-04-02)
+
+**Note:** Version bump only for package @xml-tools/parser
+
+## [1.0.3](https://github.com/sap/xml-tools/compare/@xml-tools/parser@1.0.2...@xml-tools/parser@1.0.3) (2020-02-19)
+
+### Bug Fixes
+
+- add npmignore file to each package ([5bbf209](https://github.com/sap/xml-tools/commit/5bbf209))
+
+## [1.0.2](https://github.com/sap/xml-tools/compare/@xml-tools/parser@1.0.1...@xml-tools/parser@1.0.2) (2020-02-11)
+
+### Bug Fixes
+
+- **parser:** names may start with an underscore '\_' ([#103](https://github.com/sap/xml-tools/issues/103)) ([63dfd8f](https://github.com/sap/xml-tools/commit/63dfd8f))
+
+## [1.0.1](https://github.com/sap/xml-tools/compare/@xml-tools/parser@1.0.0...@xml-tools/parser@1.0.1) (2020-01-28)
+
+### Bug Fixes
+
+- **parser:** allow CData nodes to be multi-line ([#89](https://github.com/sap/xml-tools/issues/89)) ([2b2fbfb](https://github.com/sap/xml-tools/commit/2b2fbfb))
+
+# [1.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/parser@0.4.0...@xml-tools/parser@1.0.0) (2020-01-20)
+
+### Features
+
+- **parser:** support Basic DocType Declarations ([5b4db21](https://github.com/sap/xml-tools/commit/5b4db21))
+
+### BREAKING CHANGES
+
+- **parser:** Implementing XmlCstVisitor now requires implementing two additional methods:
+ (docTypeDecl and externalID)
+
+# [0.4.0](https://github.com/sap/xml-tools/compare/@xml-tools/parser@0.3.4...@xml-tools/parser@0.4.0) (2019-11-20)
+
+### Bug Fixes
+
+- **parser:** parsing for multi-line comments ([#46](https://github.com/sap/xml-tools/issues/46)) ([4970e9a](https://github.com/sap/xml-tools/commit/4970e9a))
+
+### Features
+
+- **parser:** expose The Token Vector on the parse result ([31d50f3](https://github.com/sap/xml-tools/commit/31d50f3))
+
+## [0.3.4](https://github.com/sap/xml-tools/compare/@xml-tools/parser@0.3.3...@xml-tools/parser@0.3.4) (2019-11-12)
+
+### Bug Fixes
+
+- **parser:** fix Lexing of XML processing instructions ([2c503b4](https://github.com/sap/xml-tools/commit/2c503b4))
+
+## [0.3.3](https://github.com/sap/xml-tools/compare/@xml-tools/parser@0.3.2...@xml-tools/parser@0.3.3) (2019-11-11)
+
+### Bug Fixes
+
+- more CST Visitor API fixes ([89d1cef](https://github.com/sap/xml-tools/commit/89d1cef))
+
+## [0.3.2](https://github.com/sap/xml-tools/compare/@xml-tools/parser@0.3.1...@xml-tools/parser@0.3.2) (2019-11-11)
+
+### Bug Fixes
+
+- cST Visitor method's Params Type fix ([a0a0e2d](https://github.com/sap/xml-tools/commit/a0a0e2d))
+
+## [0.3.1](https://github.com/sap/xml-tools/compare/@xml-tools/parser@0.3.0...@xml-tools/parser@0.3.1) (2019-11-06)
+
+**Note:** Version bump only for package @xml-tools/parser
+
+# 0.3.0 (2019-10-28)
+
+### Features
+
+- move to lerna independent mode ([6c347a8](https://github.com/sap/xml-tools/commit/6c347a8)), closes [#18](https://github.com/sap/xml-tools/issues/18)
+
+# 0.2.0 (2019-10-28)
+
+# 0.1.0 (2019-10-28)
diff --git a/projects/xml-tools/packages/parser/README.md b/projects/xml-tools/packages/parser/README.md
new file mode 100644
index 00000000..7fe1ffef
--- /dev/null
+++ b/projects/xml-tools/packages/parser/README.md
@@ -0,0 +1,54 @@
+[](https://www.npmjs.com/package/@xml-tools/parser)
+
+# @xml-tools/parser
+
+A Fault Tolerant XML Parser which produces a [Concrete Syntax Tree][cst].
+
+This means that the Parser will **not** stop on the first error and instead attempt to perform automatic error recovery.
+This also means that the CST outputted by the Parser may only have **partial** results.
+For example, In a valid XML an attribute must always have a value, however in the CST produced
+by this parser an attribute's value may be missing as the XML Text input is not necessarily valid.
+
+The CST produced by this parser is often used as the input for other packages in the xml-tools scope, e.g:
+
+- [@xml-tools/ast](../ast) As the input for building an XML AST.
+- [@xml-tools/content-assist](../content-assist) As part of the input for the content assist APIs.
+
+## Installation
+
+With npm:
+
+- `npm install @xml-tools/parser`
+
+With Yarn
+
+- `yarn add @xml-tools/parser`
+
+## Usage
+
+Please see the [TypeScript Definitions](./api.d.ts) for full API details.
+
+A simple usage example:
+
+```javascript
+const { parse } = require("@xml-tools/parser");
+
+const xmlText = `
+ Bill
+ Tim
+
+`;
+
+const { cst, lexErrors, parseErrors } = parse(xmlText);
+console.log(cst.children["element"][0].children["Name"][0].image); // -> note
+```
+
+## Support
+
+Please report issues [here](https://github.com/SAP/app-studio-toolkit/issues/new/choose) and label them with `project:xml-tools`.
+
+## Contributing
+
+See [CONTRIBUTING.md](https://github.com/SAP/app-studio-toolkit/blob/main/CONTRIBUTING.md).
+
+[cst]: https://en.wikipedia.org/wiki/Parse_tree
diff --git a/projects/xml-tools/packages/parser/api.d.ts b/projects/xml-tools/packages/parser/api.d.ts
new file mode 100644
index 00000000..d85b17dd
--- /dev/null
+++ b/projects/xml-tools/packages/parser/api.d.ts
@@ -0,0 +1,187 @@
+import {
+ CstNode,
+ ICstVisitor,
+ ILexingError,
+ IRecognitionException,
+ IToken,
+} from "chevrotain";
+
+export function parse(
+ text: string,
+ startProduction?: string
+): {
+ cst: CstNode;
+ tokenVector: IToken[];
+ // TODO: decouple from Chevrotain APIs?
+ lexErrors: ILexingError[];
+ parseErrors: IRecognitionException[];
+};
+
+export const BaseXmlCstVisitor: XmlCstVisitorConstructor;
+export const BaseXmlCstVisitorWithDefaults: XmlCstVisitorWithDefaultsConstructor<
+ any,
+ any
+>;
+
+export abstract class XmlCstVisitor implements ICstVisitor {
+ // No need to implement these two methods
+ // Generic Visit method implemented by the Chevrotain Library
+ visit(cstNode: CstNode | CstNode[], param?: IN): OUT;
+ validateVisitor(): void;
+
+ document(ctx: DocumentCtx, param?: IN): OUT;
+ prolog(ctx: PrologCtx, param?: IN): OUT;
+ docTypeDecl(ctx: DocTypeDeclCtx, param?: IN): OUT;
+ externalID(ctx: ExternalIDCtx, param?: IN): OUT;
+ content(ctx: ContentCtx, param?: IN): OUT;
+ element(ctx: ElementCtx, param?: IN): OUT;
+ reference(ctx: ReferenceCtx, param?: IN): OUT;
+ attribute(ctx: AttributeCtx, param?: IN): OUT;
+ chardata(ctx: ChardataCtx, param?: IN): OUT;
+ misc(ctx: MiscCtx, param?: IN): OUT;
+}
+
+interface XmlCstVisitorConstructor {
+ new (): XmlCstVisitor;
+}
+
+export abstract class XmlCstVisitorWithDefaults
+ implements ICstVisitor
+{
+ // No need to implement these two methods
+ // Generic Visit method implemented by the Chevrotain Library
+ visit(cstNode: CstNode | CstNode[], param?: IN): OUT;
+ validateVisitor(): void;
+
+ document(ctx: DocumentCtx, param?: IN): OUT;
+ prolog(ctx: PrologCtx, param?: IN): OUT;
+ docTypeDecl(ctx: DocTypeDeclCtx, param?: IN): OUT;
+ externalID(ctx: ExternalIDCtx, param?: IN): OUT;
+ content(ctx: ContentCtx, param?: IN): OUT;
+ element(ctx: ElementCtx, param?: IN): OUT;
+ reference(ctx: ReferenceCtx, param?: IN): OUT;
+ attribute(ctx: AttributeCtx, param?: IN): OUT;
+ chardata(ctx: ChardataCtx, param?: IN): OUT;
+ misc(ctx: MiscCtx, param?: IN): OUT;
+}
+
+interface XmlCstVisitorWithDefaultsConstructor {
+ new (): XmlCstVisitorWithDefaults;
+}
+
+export interface DocumentCstNode extends CstNode {
+ name: "document";
+ children: DocumentCtx;
+}
+export type DocumentCtx = {
+ prolog: PrologCstNode[];
+ docTypeDecl: DocTypeDeclNode[];
+ misc: MiscCstNode[];
+ element: ElementCstNode[];
+};
+
+export interface PrologCstNode extends CstNode {
+ name: "prolog";
+ children: PrologCtx;
+}
+
+export type PrologCtx = {
+ XMLDeclOpen: IToken[];
+ attribute: AttributeCstNode[];
+ SPECIAL_CLOSE: IToken[];
+};
+
+export interface DocTypeDeclNode extends CstNode {
+ name: "docTypeDecl";
+ children: DocTypeDeclCtx;
+}
+
+export type DocTypeDeclCtx = {
+ DocType: IToken[];
+ Name: IToken[];
+ externalID: ExternalIDNode[];
+};
+
+export interface ExternalIDNode extends CstNode {
+ name: "ExternalIDNode";
+ children: ExternalIDCtx;
+}
+
+export type ExternalIDCtx = {
+ System: IToken[];
+ Public: IToken[];
+ PubIDLiteral: IToken[];
+ SystemLiteral: IToken[];
+};
+
+export interface ContentCstNode extends CstNode {
+ name: "content";
+ children: ContentCtx;
+}
+
+export type ContentCtx = {
+ chardata: ChardataCstNode[];
+ element: ElementCstNode[];
+ reference: ReferenceCstNode[];
+ CData: IToken[];
+ Comment: IToken[];
+};
+
+export interface ElementCstNode extends CstNode {
+ name: "element";
+ children: ElementCtx;
+}
+
+export type ElementCtx = {
+ OPEN: IToken[];
+ Name: IToken[];
+ attribute: AttributeCstNode[];
+ START_CLOSE: IToken[];
+ content: ContentCstNode[];
+ SLASH_OPEN: IToken[];
+ END_NAME: IToken[];
+ END: IToken[];
+ SLASH_CLOSE: IToken[];
+};
+
+export interface ReferenceCstNode extends CstNode {
+ name: "reference";
+ children: ReferenceCtx;
+}
+
+export type ReferenceCtx = {
+ EntityRef: IToken[];
+ CharRef: IToken[];
+};
+
+export interface AttributeCstNode extends CstNode {
+ name: "attribute";
+ children: AttributeCtx;
+}
+
+export type AttributeCtx = {
+ Name: IToken[];
+ EQUALS: IToken[];
+ STRING: IToken[];
+};
+
+export interface ChardataCstNode extends CstNode {
+ name: "chardata";
+ children: ChardataCtx;
+}
+
+export type ChardataCtx = {
+ TEXT: IToken[];
+ SEA_WS: IToken[];
+};
+
+export interface MiscCstNode extends CstNode {
+ name: "misc";
+ children: MiscCtx;
+}
+
+export type MiscCtx = {
+ Comment: IToken[];
+ PROCESSING_INSTRUCTION: IToken[];
+ SEA_WS: IToken[];
+};
diff --git a/projects/xml-tools/packages/parser/lib/api.js b/projects/xml-tools/packages/parser/lib/api.js
new file mode 100644
index 00000000..8e6d8ea3
--- /dev/null
+++ b/projects/xml-tools/packages/parser/lib/api.js
@@ -0,0 +1,21 @@
+const { xmlLexer } = require("./lexer");
+const { xmlParser } = require("./parser");
+
+module.exports = {
+ parse: function parse(text) {
+ const lexResult = xmlLexer.tokenize(text);
+ // setting a new input will RESET the parser instance's state.
+ xmlParser.input = lexResult.tokens;
+ // any top level rule may be used as an entry point
+ const cst = xmlParser.document();
+
+ return {
+ cst: cst,
+ tokenVector: lexResult.tokens,
+ lexErrors: lexResult.errors,
+ parseErrors: xmlParser.errors,
+ };
+ },
+
+ BaseXmlCstVisitor: xmlParser.getBaseCstVisitorConstructor(),
+};
diff --git a/projects/xml-tools/packages/parser/lib/lexer.js b/projects/xml-tools/packages/parser/lib/lexer.js
new file mode 100644
index 00000000..e7e0686e
--- /dev/null
+++ b/projects/xml-tools/packages/parser/lib/lexer.js
@@ -0,0 +1,206 @@
+const { createToken: createTokenOrg, Lexer } = require("chevrotain");
+
+// A little mini DSL for easier lexer definition.
+const fragments = {};
+const f = fragments;
+
+function FRAGMENT(name, def) {
+ fragments[name] = typeof def === "string" ? def : def.source;
+}
+
+function makePattern(strings, ...args) {
+ let combined = "";
+ for (let i = 0; i < strings.length; i++) {
+ combined += strings[i];
+ if (i < args.length) {
+ let pattern = args[i];
+ // By wrapping in a RegExp (none) capturing group
+ // We enabled the safe usage of qualifiers and assertions.
+ combined += `(?:${pattern})`;
+ }
+ }
+ return new RegExp(combined);
+}
+
+const tokensArray = [];
+const tokensDictionary = {};
+
+function createToken(options) {
+ const newTokenType = createTokenOrg(options);
+ tokensArray.push(newTokenType);
+ tokensDictionary[options.name] = newTokenType;
+ return newTokenType;
+}
+
+FRAGMENT(
+ "NameStartChar",
+ "(:|[a-zA-Z]|_|\\u2070-\\u218F|\\u2C00-\\u2FEF|\\u3001-\\uD7FF|\\uF900-\\uFDCF|\\uFDF0-\\uFFFD)"
+);
+
+FRAGMENT(
+ "NameChar",
+ makePattern`${f.NameStartChar}|-|\\.|\\d|\\u00B7||[\\u0300-\\u036F]|[\\u203F-\\u2040]`
+);
+FRAGMENT("Name", makePattern`${f.NameStartChar}(${f.NameChar})*`);
+
+const Comment = createToken({
+ name: "Comment",
+ pattern: //,
+ // A Comment may span multiple lines.
+ line_breaks: true,
+});
+
+const CData = createToken({
+ name: "CData",
+ pattern: //,
+ line_breaks: true,
+});
+
+const DocType = createToken({
+ name: "DocType",
+ pattern: //,
+ group: Lexer.SKIPPED,
+});
+
+const EntityRef = createToken({
+ name: "EntityRef",
+ pattern: makePattern`&${f.Name};`,
+});
+
+const CharRef = createToken({
+ name: "CharRef",
+ pattern: /\d+;|[a-fA-F0-9]/,
+});
+
+const SEA_WS = createToken({
+ name: "SEA_WS",
+ pattern: /( |\t|\n|\r\n)+/,
+});
+
+const XMLDeclOpen = createToken({
+ name: "XMLDeclOpen",
+ pattern: /<\?xml[ \t\r\n]/,
+ push_mode: "INSIDE",
+});
+
+const SLASH_OPEN = createToken({
+ name: "SLASH_OPEN",
+ pattern: /<\//,
+ push_mode: "INSIDE",
+});
+
+const INVALID_SLASH_OPEN = createToken({
+ name: "INVALID_SLASH_OPEN",
+ pattern: /<\//,
+ categories: [SLASH_OPEN],
+});
+
+const PROCESSING_INSTRUCTION = createToken({
+ name: "PROCESSING_INSTRUCTION",
+ pattern: makePattern`<\\?${f.Name}.*\\?>`,
+});
+
+const OPEN = createToken({ name: "OPEN", pattern: /, push_mode: "INSIDE" });
+// Meant to avoid skipping '<' token in a partial sequence of elements.
+// Example of the problem this solves:
+// <
+// john
+// - The second '<' will be skipped because in the mode "INSIDE" '<' is not recognized.
+// - This means the AST will include only a single element instead of two
+const INVALID_OPEN_INSIDE = createToken({
+ name: "INVALID_OPEN_INSIDE",
+ pattern: /,
+ categories: [OPEN],
+});
+
+const TEXT = createToken({ name: "TEXT", pattern: /[^<&]+/ });
+
+const CLOSE = createToken({ name: "CLOSE", pattern: />/, pop_mode: true });
+
+const SPECIAL_CLOSE = createToken({
+ name: "SPECIAL_CLOSE",
+ pattern: /\?>/,
+ pop_mode: true,
+});
+
+const SLASH_CLOSE = createToken({
+ name: "SLASH_CLOSE",
+ pattern: /\/>/,
+ pop_mode: true,
+});
+
+const SLASH = createToken({ name: "SLASH", pattern: /\// });
+
+const STRING = createToken({
+ name: "STRING",
+ pattern: /"[^<"]*"|'[^<']*'/,
+});
+
+const EQUALS = createToken({ name: "EQUALS", pattern: /=/ });
+
+const Name = createToken({ name: "Name", pattern: makePattern`${f.Name}` });
+
+const S = createToken({
+ name: "S",
+ pattern: /[ \t\r\n]/,
+ group: Lexer.SKIPPED,
+});
+
+const xmlLexerDefinition = {
+ defaultMode: "OUTSIDE",
+
+ modes: {
+ OUTSIDE: [
+ Comment,
+ CData,
+ DocType,
+ IgnoredDTD,
+ EntityRef,
+ CharRef,
+ SEA_WS,
+ XMLDeclOpen,
+ SLASH_OPEN,
+ PROCESSING_INSTRUCTION,
+ OPEN,
+ TEXT,
+ ],
+ INSIDE: [
+ // Tokens from `OUTSIDE` to improve error recovery behavior
+ Comment,
+ INVALID_SLASH_OPEN,
+ INVALID_OPEN_INSIDE,
+ // "Real" `INSIDE` tokens
+ CLOSE,
+ SPECIAL_CLOSE,
+ SLASH_CLOSE,
+ SLASH,
+ EQUALS,
+ STRING,
+ Name,
+ S,
+ ],
+ },
+};
+
+const xmlLexer = new Lexer(xmlLexerDefinition, {
+ // Reducing the amount of position tracking can provide a small performance boost (<10%)
+ // Likely best to keep the full info for better error position reporting and
+ // to expose "fuller" ITokens from the Lexer.
+ positionTracking: "full",
+ ensureOptimizations: false,
+
+ // TODO: inspect definitions for XML line terminators
+ lineTerminatorCharacters: ["\n"],
+ lineTerminatorsPattern: /\n|\r\n/g,
+});
+
+module.exports = {
+ xmlLexer,
+ tokensDictionary,
+};
diff --git a/projects/xml-tools/packages/parser/lib/parser.js b/projects/xml-tools/packages/parser/lib/parser.js
new file mode 100644
index 00000000..313757c5
--- /dev/null
+++ b/projects/xml-tools/packages/parser/lib/parser.js
@@ -0,0 +1,211 @@
+const { CstParser, tokenMatcher } = require("chevrotain");
+const { tokensDictionary: t } = require("./lexer");
+
+class Parser extends CstParser {
+ constructor() {
+ super(t, {
+ maxLookahead: 1,
+ recoveryEnabled: true,
+ nodeLocationTracking: "full",
+ });
+
+ this.deletionRecoveryEnabled = true;
+
+ const $ = this;
+
+ $.RULE("document", () => {
+ $.OPTION(() => {
+ $.SUBRULE($.prolog);
+ });
+
+ $.MANY(() => {
+ $.SUBRULE($.misc);
+ });
+
+ $.OPTION2(() => {
+ $.SUBRULE($.docTypeDecl);
+ });
+
+ $.MANY2(() => {
+ $.SUBRULE2($.misc);
+ });
+
+ $.SUBRULE($.element);
+
+ $.MANY3(() => {
+ $.SUBRULE3($.misc);
+ });
+ });
+
+ $.RULE("prolog", () => {
+ $.CONSUME(t.XMLDeclOpen);
+ $.MANY(() => {
+ $.SUBRULE($.attribute);
+ });
+ $.CONSUME(t.SPECIAL_CLOSE);
+ });
+
+ // https://www.w3.org/TR/xml/#NT-doctypedecl
+ $.RULE("docTypeDecl", () => {
+ $.CONSUME(t.DocType);
+ $.CONSUME(t.Name);
+
+ $.OPTION(() => {
+ $.SUBRULE($.externalID);
+ });
+
+ // The internal subSet part is intentionally not implemented because we do not at this
+ // time wish to implement a full DTD Parser as part of this project...
+ // https://www.w3.org/TR/xml/#NT-intSubset
+
+ $.CONSUME(t.CLOSE);
+ });
+
+ $.RULE("externalID", () => {
+ // Using gates to assert the value of the "Name" Identifiers.
+ // We could use Categories to model un-reserved keywords, however I am not sure
+ // The added complexity is needed at this time...
+ $.OR([
+ {
+ GATE: () => $.LA(1).image === "SYSTEM",
+ ALT: () => {
+ $.CONSUME2(t.Name, { LABEL: "System" });
+ $.CONSUME(t.STRING, { LABEL: "SystemLiteral" });
+ },
+ },
+ {
+ GATE: () => $.LA(1).image === "PUBLIC",
+ ALT: () => {
+ $.CONSUME3(t.Name, { LABEL: "Public" });
+ $.CONSUME2(t.STRING, { LABEL: "PubIDLiteral" });
+ $.CONSUME3(t.STRING, { LABEL: "SystemLiteral" });
+ },
+ },
+ ]);
+ });
+
+ $.RULE("content", () => {
+ $.MANY(() => {
+ $.OR([
+ { ALT: () => $.SUBRULE($.element) },
+ { ALT: () => $.SUBRULE($.chardata) },
+ { ALT: () => $.SUBRULE($.reference) },
+ { ALT: () => $.CONSUME(t.CData) },
+ { ALT: () => $.CONSUME(t.PROCESSING_INSTRUCTION) },
+ { ALT: () => $.CONSUME(t.Comment) },
+ ]);
+ });
+ });
+
+ $.RULE("element", () => {
+ $.CONSUME(t.OPEN);
+ try {
+ this.deletionRecoveryEnabled = false;
+ // disabling single token deletion here
+ // because `<
+ // `
+ // will be parsed as: ``
+ // and the next element will be lost
+ $.CONSUME(t.Name);
+ } finally {
+ this.deletionRecoveryEnabled = true;
+ }
+ $.MANY(() => {
+ $.SUBRULE($.attribute);
+ });
+
+ $.OR([
+ {
+ ALT: () => {
+ $.CONSUME(t.CLOSE, { LABEL: "START_CLOSE" });
+ $.SUBRULE($.content);
+ $.CONSUME(t.SLASH_OPEN);
+ $.CONSUME2(t.Name, { LABEL: "END_NAME" });
+ $.CONSUME2(t.CLOSE, { LABEL: "END" });
+ },
+ },
+ {
+ ALT: () => {
+ $.CONSUME(t.SLASH_CLOSE);
+ },
+ },
+ ]);
+ });
+
+ $.RULE("reference", () => {
+ $.OR([
+ { ALT: () => $.CONSUME(t.EntityRef) },
+ { ALT: () => $.CONSUME(t.CharRef) },
+ ]);
+ });
+
+ $.RULE("attribute", () => {
+ $.CONSUME(t.Name);
+ try {
+ this.deletionRecoveryEnabled = false;
+ // disabling single token deletion here
+ // because `attrib1 attrib2="666`
+ // will be parsed as: `attrib1="666`
+ $.CONSUME(t.EQUALS);
+ // disabling single token deletion here
+ // to avoid new elementName being
+ $.CONSUME(t.STRING);
+ } finally {
+ this.deletionRecoveryEnabled = true;
+ }
+ });
+
+ $.RULE("chardata", () => {
+ $.OR([
+ { ALT: () => $.CONSUME(t.TEXT) },
+ { ALT: () => $.CONSUME(t.SEA_WS) },
+ ]);
+ });
+
+ $.RULE("misc", () => {
+ $.OR([
+ { ALT: () => $.CONSUME(t.Comment) },
+ { ALT: () => $.CONSUME(t.PROCESSING_INSTRUCTION) },
+ { ALT: () => $.CONSUME(t.SEA_WS) },
+ ]);
+ });
+
+ this.performSelfAnalysis();
+ }
+
+ canRecoverWithSingleTokenDeletion(expectedTokType) {
+ if (this.deletionRecoveryEnabled === false) {
+ return false;
+ }
+ return super.canRecoverWithSingleTokenDeletion(expectedTokType);
+ }
+
+ // TODO: provide this fix upstream to chevrotain
+ // https://github.com/SAP/chevrotain/issues/1055
+ /* istanbul ignore next - should be tested as part of Chevrotain */
+ findReSyncTokenType() {
+ const allPossibleReSyncTokTypes = this.flattenFollowSet();
+ // this loop will always terminate as EOF is always in the follow stack and also always (virtually) in the input
+ let nextToken = this.LA(1);
+ let k = 2;
+ /* eslint-disable-next-line no-constant-condition -- see above comment */
+ while (true) {
+ const foundMatch = allPossibleReSyncTokTypes.find((resyncTokType) => {
+ const canMatch = tokenMatcher(nextToken, resyncTokType);
+ return canMatch;
+ });
+ if (foundMatch !== undefined) {
+ return foundMatch;
+ }
+ nextToken = this.LA(k);
+ k++;
+ }
+ }
+}
+
+// Re-use the same parser instance
+const xmlParser = new Parser();
+
+module.exports = {
+ xmlParser,
+};
diff --git a/projects/xml-tools/packages/parser/nyc.config.js b/projects/xml-tools/packages/parser/nyc.config.js
new file mode 100644
index 00000000..3e3f8962
--- /dev/null
+++ b/projects/xml-tools/packages/parser/nyc.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ // xml-tools sources live in lib/ (not src/), so this local config overrides
+ // the repo-root nyc.config.js include of **/src/** (which would report 0%).
+ include: ["lib/**/*.js"],
+ reporter: ["text", "lcov"],
+ all: true,
+ branches: 100,
+ lines: 100,
+ functions: 100,
+ statements: 100,
+};
diff --git a/projects/xml-tools/packages/parser/package.json b/projects/xml-tools/packages/parser/package.json
new file mode 100644
index 00000000..97f8802f
--- /dev/null
+++ b/projects/xml-tools/packages/parser/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@xml-tools/parser",
+ "version": "1.0.11",
+ "description": "XML Parser Implemented in JavaScript",
+ "keywords": [
+ "xml",
+ "parser"
+ ],
+ "main": "lib/api.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/SAP/app-studio-toolkit.git",
+ "directory": "projects/xml-tools/packages/parser"
+ },
+ "license": "Apache-2.0",
+ "typings": "./api.d.ts",
+ "files": [
+ "lib",
+ "api.d.ts",
+ "CHANGELOG.md",
+ "CHANGELOG.old.md"
+ ],
+ "dependencies": {
+ "chevrotain": "7.1.1"
+ },
+ "devDependencies": {
+ "klaw-sync": "6.0.0"
+ },
+ "scripts": {
+ "ci": "npm-run-all clean type-check coverage:*",
+ "clean": "rimraf ./coverage ./nyc_output",
+ "test": "mocha \"./test/**/*spec.js\"",
+ "coverage:run": "nyc mocha \"./test/**/*spec.js\"",
+ "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100",
+ "snapshots:update": "node ./scripts/update-snapshots.js",
+ "type-check": "tsc"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "bugs": {
+ "url": "https://github.com/SAP/app-studio-toolkit/issues"
+ }
+}
diff --git a/projects/xml-tools/packages/parser/scripts/update-snapshots.js b/projects/xml-tools/packages/parser/scripts/update-snapshots.js
new file mode 100644
index 00000000..4ca4313a
--- /dev/null
+++ b/projects/xml-tools/packages/parser/scripts/update-snapshots.js
@@ -0,0 +1,31 @@
+"use strict";
+const klawSync = require("klaw-sync");
+const { resolve } = require("path");
+const { readFileSync, writeFileSync } = require("fs");
+const { format } = require("prettier");
+
+const { transformCstForAssertions } = require("../test/utils");
+const { parse } = require("../");
+
+const testsDir = resolve(__dirname, "..", "test");
+
+const sampleFiles = klawSync(testsDir, { nodir: true });
+const xmlSampleFiles = sampleFiles.filter((fileDesc) => {
+ if (fileDesc.path.includes("node_modules")) {
+ return false;
+ }
+ return fileDesc.path.endsWith("input.xml");
+});
+
+xmlSampleFiles.forEach((fileDesc) => {
+ const xmlInput = readFileSync(fileDesc.path, "utf8");
+ const simpleNewLinesInput = xmlInput.replace(/\r\n/g, "\n");
+ console.log(`Reading <${fileDesc.path}>`);
+ const { cst } = parse(simpleNewLinesInput);
+ transformCstForAssertions(cst);
+ const snapshotOutput = `module.exports = { cst : ${JSON.stringify(cst)}}`;
+ const formattedSnapshotOutput = format(snapshotOutput, { parser: "babel" });
+ const outputFilePath = fileDesc.path.replace(/input.xml$/, "output.js");
+ console.log(`writing <${outputFilePath}>`);
+ writeFileSync(outputFilePath, formattedSnapshotOutput);
+});
diff --git a/projects/xml-tools/packages/parser/test/parser-smoke-spec.js b/projects/xml-tools/packages/parser/test/parser-smoke-spec.js
new file mode 100644
index 00000000..4e28b236
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/parser-smoke-spec.js
@@ -0,0 +1,132 @@
+const { expect } = require("chai");
+
+const { parse } = require("../lib/api");
+
+describe("The XML Parser", () => {
+ it("can parse a simple XML without errors", () => {
+ const inputText =
+ '\n' +
+ "\n" +
+ ' \n' +
+ " Gambardella, Matthew\n" +
+ " XML Developer's Guide\n" +
+ " Computer\n" +
+ " 44.95\n" +
+ " 2000-10-01\n" +
+ " An in-depth look at creating applications \n" +
+ " with XML.\n" +
+ " \n" +
+ ' \n' +
+ " Ralls, Kim\n" +
+ " Midnight Rain\n" +
+ " Fantasy\n" +
+ " 5.95\n" +
+ " 2000-12-16\n" +
+ " A former architect battles corporate zombies, \n" +
+ " an evil sorceress, and her own childhood to become queen \n" +
+ " of the world.\n" +
+ " \n" +
+ ' \n' +
+ " Corets, Eva\n" +
+ " Maeve Ascendant\n" +
+ " Fantasy\n" +
+ " 5.95\n" +
+ " 2000-11-17\n" +
+ " After the collapse of a nanotechnology \n" +
+ " society in England, the young survivors lay the \n" +
+ " foundation for a new society.\n" +
+ " \n" +
+ ' \n' +
+ " Corets, Eva\n" +
+ " Oberon's Legacy\n" +
+ " Fantasy\n" +
+ " 5.95\n" +
+ " 2001-03-10\n" +
+ " In post-apocalypse England, the mysterious \n" +
+ " agent known only as Oberon helps to create a new life \n" +
+ " for the inhabitants of London. Sequel to Maeve \n" +
+ " Ascendant.\n" +
+ " \n" +
+ ' \n' +
+ " Corets, Eva\n" +
+ " The Sundered Grail\n" +
+ " Fantasy\n" +
+ " 5.95\n" +
+ " 2001-09-10\n" +
+ " The two daughters of Maeve, half-sisters, \n" +
+ " battle one another for control of England. Sequel to \n" +
+ " Oberon's Legacy.\n" +
+ " \n" +
+ ' \n' +
+ " Randall, Cynthia\n" +
+ " Lover Birds\n" +
+ " Romance\n" +
+ " 4.95\n" +
+ " 2000-09-02\n" +
+ " When Carla meets Paul at an ornithology \n" +
+ " conference, tempers fly as feathers get ruffled.\n" +
+ " \n" +
+ ' \n' +
+ " Thurman, Paula\n" +
+ " Splish Splash\n" +
+ " Romance\n" +
+ " 4.95\n" +
+ " 2000-11-02\n" +
+ " A deep sea diver finds true love twenty \n" +
+ " thousand leagues beneath the sea.\n" +
+ " \n" +
+ ' \n' +
+ " Knorr, Stefan\n" +
+ " Creepy Crawlies\n" +
+ " Horror\n" +
+ " 4.95\n" +
+ " 2000-12-06\n" +
+ " An anthology of horror stories about roaches,\n" +
+ " centipedes, scorpions and other insects.\n" +
+ " \n" +
+ ' \n' +
+ " Kress, Peter\n" +
+ " Paradox Lost\n" +
+ " Science Fiction\n" +
+ " 6.95\n" +
+ " 2000-11-02\n" +
+ " After an inadvertant trip through a Heisenberg\n" +
+ " Uncertainty Device, James Salway discovers the problems \n" +
+ " of being quantum.\n" +
+ " \n" +
+ ' \n' +
+ " O'Brien, Tim\n" +
+ " Microsoft .NET: The Programming Bible\n" +
+ " Computer\n" +
+ " 36.95\n" +
+ " 2000-12-09\n" +
+ " Microsoft's .NET initiative is explored in \n" +
+ " detail in this deep programmer's reference.\n" +
+ " \n" +
+ ' \n' +
+ " O'Brien, Tim\n" +
+ " MSXML3: A Comprehensive Guide\n" +
+ " Computer\n" +
+ " 36.95\n" +
+ " 2000-12-01\n" +
+ " The Microsoft MSXML3 parser is covered in \n" +
+ " detail, with attention to XML DOM interfaces, XSLT processing, \n" +
+ " SAX and more.\n" +
+ " \n" +
+ ' \n' +
+ " Galos, Mike\n" +
+ " Visual Studio 7: A Comprehensive Guide\n" +
+ " Computer\n" +
+ " 49.95\n" +
+ " 2001-04-16\n" +
+ " Microsoft Visual Studio 7 is explored in depth,\n" +
+ " looking at how Visual Basic, Visual C++, C#, and ASP+ are \n" +
+ " integrated into a comprehensive development \n" +
+ " environment.\n" +
+ " \n" +
+ "";
+
+ const lexAndParseResult = parse(inputText);
+ expect(lexAndParseResult.parseErrors).to.be.empty;
+ });
+});
diff --git a/projects/xml-tools/packages/parser/test/sample-test.js b/projects/xml-tools/packages/parser/test/sample-test.js
new file mode 100644
index 00000000..0d98e827
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/sample-test.js
@@ -0,0 +1,33 @@
+const { expect } = require("chai");
+const { readFileSync } = require("fs");
+const { resolve, basename } = require("path");
+const { partialRight } = require("lodash");
+
+const { transformCstForAssertions } = require("./utils");
+const { parse } = require("../");
+
+function executeSampleTest(dirPath, assertNoErrors) {
+ it("Can Parse an XML text", () => {
+ const inputPath = resolve(dirPath, "input.xml");
+ const inputText = readFileSync(inputPath).toString("utf8");
+ const simpleNewLinesInput = inputText.replace(/\r\n/g, "\n");
+ const { cst, lexErrors, parseErrors } = parse(simpleNewLinesInput);
+ if (assertNoErrors === true) {
+ expect(lexErrors).to.be.empty;
+ expect(parseErrors).to.be.empty;
+ }
+ transformCstForAssertions(cst);
+ const expectedOutput = require(resolve(dirPath, "output.js")).cst;
+ expect(cst).to.deep.eql(expectedOutput);
+ });
+}
+
+function testNameFromDir(dirPath) {
+ return basename(dirPath);
+}
+
+module.exports = {
+ executeValidSampleTest: partialRight(executeSampleTest, true),
+ executeInValidSampleTest: partialRight(executeSampleTest, false),
+ testNameFromDir: testNameFromDir,
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/invalid/attributes/input.xml b/projects/xml-tools/packages/parser/test/snapshots/invalid/attributes/input.xml
new file mode 100644
index 00000000..c1d7025c
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/invalid/attributes/input.xml
@@ -0,0 +1 @@
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/invalid/attributes/output.js b/projects/xml-tools/packages/parser/test/snapshots/invalid/attributes/output.js
new file mode 100644
index 00000000..af1f3251
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/invalid/attributes/output.js
@@ -0,0 +1,129 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 0, endOffset: 0, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 1,
+ endOffset: 4,
+ tokenType: "Name",
+ },
+ ],
+ attribute: [
+ {
+ name: "attribute",
+ children: {
+ Name: [
+ {
+ image: "attr1",
+ startOffset: 6,
+ endOffset: 10,
+ tokenType: "Name",
+ },
+ ],
+ },
+ location: { startOffset: 6, endOffset: 10 },
+ recoveredNode: true,
+ },
+ {
+ name: "attribute",
+ children: {
+ Name: [
+ {
+ image: "attr2",
+ startOffset: 12,
+ endOffset: 16,
+ tokenType: "Name",
+ },
+ ],
+ EQUALS: [
+ {
+ image: "=",
+ startOffset: 17,
+ endOffset: 17,
+ tokenType: "EQUALS",
+ },
+ ],
+ STRING: [
+ {
+ image: '"333"',
+ startOffset: 18,
+ endOffset: 22,
+ tokenType: "STRING",
+ },
+ ],
+ },
+ location: { startOffset: 12, endOffset: 22 },
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 23,
+ endOffset: 23,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {},
+ location: { startOffset: null, endOffset: null },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 24,
+ endOffset: 25,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 26,
+ endOffset: 29,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 30,
+ endOffset: 30,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 30 },
+ },
+ ],
+ misc: [
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 31,
+ endOffset: 31,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 31, endOffset: 31 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 31 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/invalid/attributes/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/invalid/attributes/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/invalid/attributes/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/invalid/element-closing-name/input.xml b/projects/xml-tools/packages/parser/test/snapshots/invalid/element-closing-name/input.xml
new file mode 100644
index 00000000..c2024272
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/invalid/element-closing-name/input.xml
@@ -0,0 +1 @@
+ note note>
diff --git a/projects/xml-tools/packages/parser/test/snapshots/invalid/element-closing-name/output.js b/projects/xml-tools/packages/parser/test/snapshots/invalid/element-closing-name/output.js
new file mode 100644
index 00000000..9c9493ca
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/invalid/element-closing-name/output.js
@@ -0,0 +1,77 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 0, endOffset: 0, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 1,
+ endOffset: 4,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ { image: ">", startOffset: 5, endOffset: 5, tokenType: "CLOSE" },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {},
+ location: { startOffset: null, endOffset: null },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 6,
+ endOffset: 7,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 9,
+ endOffset: 12,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 18,
+ endOffset: 18,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 18 },
+ },
+ ],
+ misc: [
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 19,
+ endOffset: 19,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 19, endOffset: 19 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 19 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/invalid/element-closing-name/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/invalid/element-closing-name/sample.spec.js
new file mode 100644
index 00000000..7e6e9a0b
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/invalid/element-closing-name/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeInValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeInValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/attributes/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/attributes/input.xml
new file mode 100644
index 00000000..c11455bd
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/attributes/input.xml
@@ -0,0 +1 @@
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/attributes/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/attributes/output.js
new file mode 100644
index 00000000..68f930f4
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/attributes/output.js
@@ -0,0 +1,144 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 0, endOffset: 0, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 1,
+ endOffset: 4,
+ tokenType: "Name",
+ },
+ ],
+ attribute: [
+ {
+ name: "attribute",
+ children: {
+ Name: [
+ {
+ image: "attr1",
+ startOffset: 6,
+ endOffset: 10,
+ tokenType: "Name",
+ },
+ ],
+ EQUALS: [
+ {
+ image: "=",
+ startOffset: 11,
+ endOffset: 11,
+ tokenType: "EQUALS",
+ },
+ ],
+ STRING: [
+ {
+ image: '"666"',
+ startOffset: 12,
+ endOffset: 16,
+ tokenType: "STRING",
+ },
+ ],
+ },
+ location: { startOffset: 6, endOffset: 16 },
+ },
+ {
+ name: "attribute",
+ children: {
+ Name: [
+ {
+ image: "_attr2",
+ startOffset: 18,
+ endOffset: 23,
+ tokenType: "Name",
+ },
+ ],
+ EQUALS: [
+ {
+ image: "=",
+ startOffset: 24,
+ endOffset: 24,
+ tokenType: "EQUALS",
+ },
+ ],
+ STRING: [
+ {
+ image: '"333"',
+ startOffset: 25,
+ endOffset: 29,
+ tokenType: "STRING",
+ },
+ ],
+ },
+ location: { startOffset: 18, endOffset: 29 },
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 30,
+ endOffset: 30,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {},
+ location: { startOffset: null, endOffset: null },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 31,
+ endOffset: 32,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 33,
+ endOffset: 36,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 37,
+ endOffset: 37,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 37 },
+ },
+ ],
+ misc: [
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 38,
+ endOffset: 38,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 38, endOffset: 38 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 38 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/attributes/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/attributes/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/attributes/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/cdata/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/cdata/input.xml
new file mode 100644
index 00000000..d359a2f0
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/cdata/input.xml
@@ -0,0 +1,3 @@
+
+ Hello, world!]]>
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/cdata/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/cdata/output.js
new file mode 100644
index 00000000..ed00044d
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/cdata/output.js
@@ -0,0 +1,116 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 0, endOffset: 0, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 1,
+ endOffset: 4,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ { image: ">", startOffset: 5, endOffset: 5, tokenType: "CLOSE" },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 6,
+ endOffset: 10,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 6, endOffset: 10 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 57,
+ endOffset: 57,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 57, endOffset: 57 },
+ },
+ ],
+ CData: [
+ {
+ image: "Hello, world!]]>",
+ startOffset: 11,
+ endOffset: 56,
+ tokenType: "CData",
+ },
+ ],
+ },
+ location: { startOffset: 6, endOffset: 57 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 58,
+ endOffset: 59,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 60,
+ endOffset: 63,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 64,
+ endOffset: 64,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 64 },
+ },
+ ],
+ misc: [
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 65,
+ endOffset: 65,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 65, endOffset: 65 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 65 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/cdata/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/cdata/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/cdata/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/comments/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/comments/input.xml
new file mode 100644
index 00000000..f6d16dee
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/comments/input.xml
@@ -0,0 +1,6 @@
+
+
+
+ Bill
+
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/comments/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/comments/output.js
new file mode 100644
index 00000000..9e1d655f
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/comments/output.js
@@ -0,0 +1,271 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ misc: [
+ {
+ name: "misc",
+ children: {
+ Comment: [
+ {
+ image: "",
+ startOffset: 0,
+ endOffset: 21,
+ tokenType: "Comment",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 21 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 22,
+ endOffset: 22,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 22, endOffset: 22 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 82,
+ endOffset: 82,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 82, endOffset: 82 },
+ },
+ {
+ name: "misc",
+ children: {
+ Comment: [
+ {
+ image: "",
+ startOffset: 83,
+ endOffset: 104,
+ tokenType: "Comment",
+ },
+ ],
+ },
+ location: { startOffset: 83, endOffset: 104 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 105,
+ endOffset: 105,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 105, endOffset: 105 },
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 23, endOffset: 23, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 24,
+ endOffset: 27,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 28,
+ endOffset: 28,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 29,
+ endOffset: 33,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 29, endOffset: 33 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 56,
+ endOffset: 60,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 56, endOffset: 60 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 74,
+ endOffset: 74,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 74, endOffset: 74 },
+ },
+ ],
+ Comment: [
+ {
+ image: "",
+ startOffset: 34,
+ endOffset: 55,
+ tokenType: "Comment",
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ {
+ image: "<",
+ startOffset: 61,
+ endOffset: 61,
+ tokenType: "OPEN",
+ },
+ ],
+ Name: [
+ {
+ image: "to",
+ startOffset: 62,
+ endOffset: 63,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 64,
+ endOffset: 64,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ TEXT: [
+ {
+ image: "Bill",
+ startOffset: 65,
+ endOffset: 68,
+ tokenType: "TEXT",
+ },
+ ],
+ },
+ location: { startOffset: 65, endOffset: 68 },
+ },
+ ],
+ },
+ location: { startOffset: 65, endOffset: 68 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 69,
+ endOffset: 70,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "to",
+ startOffset: 71,
+ endOffset: 72,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 73,
+ endOffset: 73,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 61, endOffset: 73 },
+ },
+ ],
+ },
+ location: { startOffset: 29, endOffset: 74 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 75,
+ endOffset: 76,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 77,
+ endOffset: 80,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 81,
+ endOffset: 81,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 23, endOffset: 81 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 105 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/comments/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/comments/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/comments/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/complex-contents/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/complex-contents/input.xml
new file mode 100644
index 00000000..8aca3184
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/complex-contents/input.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/complex-contents/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/complex-contents/output.js
new file mode 100644
index 00000000..97c8c1fd
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/complex-contents/output.js
@@ -0,0 +1,120 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ prolog: [
+ {
+ name: "prolog",
+ children: {
+ XMLDeclOpen: [
+ {
+ image: "",
+ startOffset: 6,
+ endOffset: 7,
+ tokenType: "SPECIAL_CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 7 },
+ },
+ ],
+ misc: [
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 8,
+ endOffset: 8,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 8, endOffset: 8 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 22,
+ endOffset: 22,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 22, endOffset: 22 },
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 9, endOffset: 9, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 10,
+ endOffset: 13,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 14,
+ endOffset: 14,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {},
+ location: { startOffset: null, endOffset: null },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 15,
+ endOffset: 16,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 17,
+ endOffset: 20,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 21,
+ endOffset: 21,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 9, endOffset: 21 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 22 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/complex-contents/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/complex-contents/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/complex-contents/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-public/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-public/input.xml
new file mode 100644
index 00000000..6f089fb0
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-public/input.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-public/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-public/output.js
new file mode 100644
index 00000000..62b0b1f7
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-public/output.js
@@ -0,0 +1,160 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ docTypeDecl: [
+ {
+ name: "docTypeDecl",
+ children: {
+ DocType: [
+ {
+ image: "",
+ startOffset: 62,
+ endOffset: 62,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 62 },
+ },
+ ],
+ misc: [
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 63,
+ endOffset: 63,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 63, endOffset: 63 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 77,
+ endOffset: 77,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 77, endOffset: 77 },
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 64, endOffset: 64, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 65,
+ endOffset: 68,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 69,
+ endOffset: 69,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {},
+ location: { startOffset: null, endOffset: null },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 70,
+ endOffset: 71,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 72,
+ endOffset: 75,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 76,
+ endOffset: 76,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 64, endOffset: 76 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 77 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-public/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-public/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-public/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-system/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-system/input.xml
new file mode 100644
index 00000000..d9eba62b
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-system/input.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-system/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-system/output.js
new file mode 100644
index 00000000..175c063b
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-system/output.js
@@ -0,0 +1,152 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ docTypeDecl: [
+ {
+ name: "docTypeDecl",
+ children: {
+ DocType: [
+ {
+ image: "",
+ startOffset: 37,
+ endOffset: 37,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 37 },
+ },
+ ],
+ misc: [
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 38,
+ endOffset: 38,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 38, endOffset: 38 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 52,
+ endOffset: 52,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 52, endOffset: 52 },
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 39, endOffset: 39, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 40,
+ endOffset: 43,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 44,
+ endOffset: 44,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {},
+ location: { startOffset: null, endOffset: null },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 45,
+ endOffset: 46,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 47,
+ endOffset: 50,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 51,
+ endOffset: 51,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 39, endOffset: 51 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 52 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-system/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-system/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/doc-type-system/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/elements-short-form/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/elements-short-form/input.xml
new file mode 100644
index 00000000..f9d7191f
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/elements-short-form/input.xml
@@ -0,0 +1,4 @@
+
+ Bill
+
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/elements-short-form/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/elements-short-form/output.js
new file mode 100644
index 00000000..ce3dee65
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/elements-short-form/output.js
@@ -0,0 +1,264 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 0, endOffset: 0, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 1,
+ endOffset: 4,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ { image: ">", startOffset: 5, endOffset: 5, tokenType: "CLOSE" },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 6,
+ endOffset: 10,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 6, endOffset: 10 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 24,
+ endOffset: 28,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 24, endOffset: 28 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 47,
+ endOffset: 47,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 47, endOffset: 47 },
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ {
+ image: "<",
+ startOffset: 11,
+ endOffset: 11,
+ tokenType: "OPEN",
+ },
+ ],
+ Name: [
+ {
+ image: "to",
+ startOffset: 12,
+ endOffset: 13,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 14,
+ endOffset: 14,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ TEXT: [
+ {
+ image: "Bill",
+ startOffset: 15,
+ endOffset: 18,
+ tokenType: "TEXT",
+ },
+ ],
+ },
+ location: { startOffset: 15, endOffset: 18 },
+ },
+ ],
+ },
+ location: { startOffset: 15, endOffset: 18 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 19,
+ endOffset: 20,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "to",
+ startOffset: 21,
+ endOffset: 22,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 23,
+ endOffset: 23,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 11, endOffset: 23 },
+ },
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ {
+ image: "<",
+ startOffset: 29,
+ endOffset: 29,
+ tokenType: "OPEN",
+ },
+ ],
+ Name: [
+ {
+ image: "from",
+ startOffset: 30,
+ endOffset: 33,
+ tokenType: "Name",
+ },
+ ],
+ attribute: [
+ {
+ name: "attribute",
+ children: {
+ Name: [
+ {
+ image: "name",
+ startOffset: 35,
+ endOffset: 38,
+ tokenType: "Name",
+ },
+ ],
+ EQUALS: [
+ {
+ image: "=",
+ startOffset: 39,
+ endOffset: 39,
+ tokenType: "EQUALS",
+ },
+ ],
+ STRING: [
+ {
+ image: '"tim"',
+ startOffset: 40,
+ endOffset: 44,
+ tokenType: "STRING",
+ },
+ ],
+ },
+ location: { startOffset: 35, endOffset: 44 },
+ },
+ ],
+ SLASH_CLOSE: [
+ {
+ image: "/>",
+ startOffset: 45,
+ endOffset: 46,
+ tokenType: "SLASH_CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 29, endOffset: 46 },
+ },
+ ],
+ },
+ location: { startOffset: 6, endOffset: 47 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 48,
+ endOffset: 49,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 50,
+ endOffset: 53,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 54,
+ endOffset: 54,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 54 },
+ },
+ ],
+ misc: [
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 55,
+ endOffset: 55,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 55, endOffset: 55 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 55 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/elements-short-form/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/elements-short-form/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/elements-short-form/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/elements/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/elements/input.xml
new file mode 100644
index 00000000..77dfac07
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/elements/input.xml
@@ -0,0 +1,4 @@
+
+ Bill
+ Tim
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/elements/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/elements/output.js
new file mode 100644
index 00000000..12e6bbf5
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/elements/output.js
@@ -0,0 +1,280 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 0, endOffset: 0, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 1,
+ endOffset: 4,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ { image: ">", startOffset: 5, endOffset: 5, tokenType: "CLOSE" },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 6,
+ endOffset: 10,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 6, endOffset: 10 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 24,
+ endOffset: 28,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 24, endOffset: 28 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 45,
+ endOffset: 45,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 45, endOffset: 45 },
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ {
+ image: "<",
+ startOffset: 11,
+ endOffset: 11,
+ tokenType: "OPEN",
+ },
+ ],
+ Name: [
+ {
+ image: "to",
+ startOffset: 12,
+ endOffset: 13,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 14,
+ endOffset: 14,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ TEXT: [
+ {
+ image: "Bill",
+ startOffset: 15,
+ endOffset: 18,
+ tokenType: "TEXT",
+ },
+ ],
+ },
+ location: { startOffset: 15, endOffset: 18 },
+ },
+ ],
+ },
+ location: { startOffset: 15, endOffset: 18 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 19,
+ endOffset: 20,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "to",
+ startOffset: 21,
+ endOffset: 22,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 23,
+ endOffset: 23,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 11, endOffset: 23 },
+ },
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ {
+ image: "<",
+ startOffset: 29,
+ endOffset: 29,
+ tokenType: "OPEN",
+ },
+ ],
+ Name: [
+ {
+ image: "from",
+ startOffset: 30,
+ endOffset: 33,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 34,
+ endOffset: 34,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ TEXT: [
+ {
+ image: "Tim",
+ startOffset: 35,
+ endOffset: 37,
+ tokenType: "TEXT",
+ },
+ ],
+ },
+ location: { startOffset: 35, endOffset: 37 },
+ },
+ ],
+ },
+ location: { startOffset: 35, endOffset: 37 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 38,
+ endOffset: 39,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "from",
+ startOffset: 40,
+ endOffset: 43,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 44,
+ endOffset: 44,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 29, endOffset: 44 },
+ },
+ ],
+ },
+ location: { startOffset: 6, endOffset: 45 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 46,
+ endOffset: 47,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 48,
+ endOffset: 51,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 52,
+ endOffset: 52,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 52 },
+ },
+ ],
+ misc: [
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 53,
+ endOffset: 53,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 53, endOffset: 53 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 53 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/elements/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/elements/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/elements/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/entity-char-ref/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/entity-char-ref/input.xml
new file mode 100644
index 00000000..816e0202
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/entity-char-ref/input.xml
@@ -0,0 +1,5 @@
+
+ &to;
+ Ӓ
+ Tim
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/entity-char-ref/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/entity-char-ref/output.js
new file mode 100644
index 00000000..de9ebca3
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/entity-char-ref/output.js
@@ -0,0 +1,246 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 0, endOffset: 0, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 1,
+ endOffset: 4,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ { image: ">", startOffset: 5, endOffset: 5, tokenType: "CLOSE" },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 6,
+ endOffset: 10,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 6, endOffset: 10 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 15,
+ endOffset: 19,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 15, endOffset: 19 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 27,
+ endOffset: 31,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 27, endOffset: 31 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 48,
+ endOffset: 48,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 48, endOffset: 48 },
+ },
+ ],
+ reference: [
+ {
+ name: "reference",
+ children: {
+ EntityRef: [
+ {
+ image: "&to;",
+ startOffset: 11,
+ endOffset: 14,
+ tokenType: "EntityRef",
+ },
+ ],
+ },
+ location: { startOffset: 11, endOffset: 14 },
+ },
+ {
+ name: "reference",
+ children: {
+ CharRef: [
+ {
+ image: "Ӓ",
+ startOffset: 20,
+ endOffset: 26,
+ tokenType: "CharRef",
+ },
+ ],
+ },
+ location: { startOffset: 20, endOffset: 26 },
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ {
+ image: "<",
+ startOffset: 32,
+ endOffset: 32,
+ tokenType: "OPEN",
+ },
+ ],
+ Name: [
+ {
+ image: "from",
+ startOffset: 33,
+ endOffset: 36,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 37,
+ endOffset: 37,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ TEXT: [
+ {
+ image: "Tim",
+ startOffset: 38,
+ endOffset: 40,
+ tokenType: "TEXT",
+ },
+ ],
+ },
+ location: { startOffset: 38, endOffset: 40 },
+ },
+ ],
+ },
+ location: { startOffset: 38, endOffset: 40 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 41,
+ endOffset: 42,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "from",
+ startOffset: 43,
+ endOffset: 46,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 47,
+ endOffset: 47,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 32, endOffset: 47 },
+ },
+ ],
+ },
+ location: { startOffset: 6, endOffset: 48 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 49,
+ endOffset: 50,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 51,
+ endOffset: 54,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 55,
+ endOffset: 55,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 55 },
+ },
+ ],
+ misc: [
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 56,
+ endOffset: 56,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 56, endOffset: 56 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 56 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/entity-char-ref/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/entity-char-ref/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/entity-char-ref/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/multi-line-comments/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/multi-line-comments/input.xml
new file mode 100644
index 00000000..e47b7cec
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/multi-line-comments/input.xml
@@ -0,0 +1,13 @@
+
+
+
+ Bill
+
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/multi-line-comments/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/multi-line-comments/output.js
new file mode 100644
index 00000000..f96208bc
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/multi-line-comments/output.js
@@ -0,0 +1,271 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ misc: [
+ {
+ name: "misc",
+ children: {
+ Comment: [
+ {
+ image: "",
+ startOffset: 0,
+ endOffset: 31,
+ tokenType: "Comment",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 31 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 32,
+ endOffset: 32,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 32, endOffset: 32 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 102,
+ endOffset: 102,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 102, endOffset: 102 },
+ },
+ {
+ name: "misc",
+ children: {
+ Comment: [
+ {
+ image: "",
+ startOffset: 103,
+ endOffset: 126,
+ tokenType: "Comment",
+ },
+ ],
+ },
+ location: { startOffset: 103, endOffset: 126 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 127,
+ endOffset: 127,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 127, endOffset: 127 },
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 33, endOffset: 33, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 34,
+ endOffset: 37,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 38,
+ endOffset: 38,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 39,
+ endOffset: 43,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 39, endOffset: 43 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 76,
+ endOffset: 80,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 76, endOffset: 80 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 94,
+ endOffset: 94,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 94, endOffset: 94 },
+ },
+ ],
+ Comment: [
+ {
+ image: "",
+ startOffset: 44,
+ endOffset: 75,
+ tokenType: "Comment",
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ {
+ image: "<",
+ startOffset: 81,
+ endOffset: 81,
+ tokenType: "OPEN",
+ },
+ ],
+ Name: [
+ {
+ image: "to",
+ startOffset: 82,
+ endOffset: 83,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 84,
+ endOffset: 84,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ TEXT: [
+ {
+ image: "Bill",
+ startOffset: 85,
+ endOffset: 88,
+ tokenType: "TEXT",
+ },
+ ],
+ },
+ location: { startOffset: 85, endOffset: 88 },
+ },
+ ],
+ },
+ location: { startOffset: 85, endOffset: 88 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 89,
+ endOffset: 90,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "to",
+ startOffset: 91,
+ endOffset: 92,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 93,
+ endOffset: 93,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 81, endOffset: 93 },
+ },
+ ],
+ },
+ location: { startOffset: 39, endOffset: 94 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 95,
+ endOffset: 96,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 97,
+ endOffset: 100,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 101,
+ endOffset: 101,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 33, endOffset: 101 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 127 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/multi-line-comments/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/multi-line-comments/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/multi-line-comments/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/namespaces/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/namespaces/input.xml
new file mode 100644
index 00000000..4a72de40
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/namespaces/input.xml
@@ -0,0 +1,6 @@
+
+
+ Some Chair
+ 50
+ 67
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/namespaces/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/namespaces/output.js
new file mode 100644
index 00000000..20f356d6
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/namespaces/output.js
@@ -0,0 +1,426 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ misc: [
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 0,
+ endOffset: 0,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 0 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 144,
+ endOffset: 144,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 144, endOffset: 144 },
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 1, endOffset: 1, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "f:table",
+ startOffset: 2,
+ endOffset: 8,
+ tokenType: "Name",
+ },
+ ],
+ attribute: [
+ {
+ name: "attribute",
+ children: {
+ Name: [
+ {
+ image: "xmlns:f",
+ startOffset: 10,
+ endOffset: 16,
+ tokenType: "Name",
+ },
+ ],
+ EQUALS: [
+ {
+ image: "=",
+ startOffset: 17,
+ endOffset: 17,
+ tokenType: "EQUALS",
+ },
+ ],
+ STRING: [
+ {
+ image: '"https://blah.com/furniture"',
+ startOffset: 18,
+ endOffset: 45,
+ tokenType: "STRING",
+ },
+ ],
+ },
+ location: { startOffset: 10, endOffset: 45 },
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 46,
+ endOffset: 46,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 47,
+ endOffset: 51,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 47, endOffset: 51 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 79,
+ endOffset: 83,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 79, endOffset: 83 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 105,
+ endOffset: 109,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 105, endOffset: 109 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 133,
+ endOffset: 133,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 133, endOffset: 133 },
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ {
+ image: "<",
+ startOffset: 52,
+ endOffset: 52,
+ tokenType: "OPEN",
+ },
+ ],
+ Name: [
+ {
+ image: "f:name",
+ startOffset: 53,
+ endOffset: 58,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 59,
+ endOffset: 59,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ TEXT: [
+ {
+ image: "Some Chair",
+ startOffset: 60,
+ endOffset: 69,
+ tokenType: "TEXT",
+ },
+ ],
+ },
+ location: { startOffset: 60, endOffset: 69 },
+ },
+ ],
+ },
+ location: { startOffset: 60, endOffset: 69 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 70,
+ endOffset: 71,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "f:name",
+ startOffset: 72,
+ endOffset: 77,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 78,
+ endOffset: 78,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 52, endOffset: 78 },
+ },
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ {
+ image: "<",
+ startOffset: 84,
+ endOffset: 84,
+ tokenType: "OPEN",
+ },
+ ],
+ Name: [
+ {
+ image: "f:width",
+ startOffset: 85,
+ endOffset: 91,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 92,
+ endOffset: 92,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ TEXT: [
+ {
+ image: "50",
+ startOffset: 93,
+ endOffset: 94,
+ tokenType: "TEXT",
+ },
+ ],
+ },
+ location: { startOffset: 93, endOffset: 94 },
+ },
+ ],
+ },
+ location: { startOffset: 93, endOffset: 94 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 95,
+ endOffset: 96,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "f:width",
+ startOffset: 97,
+ endOffset: 103,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 104,
+ endOffset: 104,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 84, endOffset: 104 },
+ },
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ {
+ image: "<",
+ startOffset: 110,
+ endOffset: 110,
+ tokenType: "OPEN",
+ },
+ ],
+ Name: [
+ {
+ image: "f:length",
+ startOffset: 111,
+ endOffset: 118,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 119,
+ endOffset: 119,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ TEXT: [
+ {
+ image: "67",
+ startOffset: 120,
+ endOffset: 121,
+ tokenType: "TEXT",
+ },
+ ],
+ },
+ location: {
+ startOffset: 120,
+ endOffset: 121,
+ },
+ },
+ ],
+ },
+ location: { startOffset: 120, endOffset: 121 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 122,
+ endOffset: 123,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "f:length",
+ startOffset: 124,
+ endOffset: 131,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 132,
+ endOffset: 132,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 110, endOffset: 132 },
+ },
+ ],
+ },
+ location: { startOffset: 47, endOffset: 133 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 134,
+ endOffset: 135,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "f:table",
+ startOffset: 136,
+ endOffset: 142,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 143,
+ endOffset: 143,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 1, endOffset: 143 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 144 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/namespaces/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/namespaces/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/namespaces/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/processing-instruction/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/processing-instruction/input.xml
new file mode 100644
index 00000000..bbf92201
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/processing-instruction/input.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/processing-instruction/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/processing-instruction/output.js
new file mode 100644
index 00000000..1f83bd52
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/processing-instruction/output.js
@@ -0,0 +1,151 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ misc: [
+ {
+ name: "misc",
+ children: {
+ PROCESSING_INSTRUCTION: [
+ {
+ image:
+ '',
+ startOffset: 0,
+ endOffset: 67,
+ tokenType: "PROCESSING_INSTRUCTION",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 67 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 68,
+ endOffset: 68,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 68, endOffset: 68 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 156,
+ endOffset: 156,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 156, endOffset: 156 },
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 69, endOffset: 69, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 70,
+ endOffset: 73,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 74,
+ endOffset: 74,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {
+ chardata: [
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n ",
+ startOffset: 75,
+ endOffset: 79,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 75, endOffset: 79 },
+ },
+ {
+ name: "chardata",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 148,
+ endOffset: 148,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 148, endOffset: 148 },
+ },
+ ],
+ PROCESSING_INSTRUCTION: [
+ {
+ image:
+ '',
+ startOffset: 80,
+ endOffset: 147,
+ tokenType: "PROCESSING_INSTRUCTION",
+ },
+ ],
+ },
+ location: { startOffset: 75, endOffset: 148 },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 149,
+ endOffset: 150,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 151,
+ endOffset: 154,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 155,
+ endOffset: 155,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 69, endOffset: 155 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 156 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/processing-instruction/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/processing-instruction/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/processing-instruction/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/prolog/input.xml b/projects/xml-tools/packages/parser/test/snapshots/valid/prolog/input.xml
new file mode 100644
index 00000000..30f696f1
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/prolog/input.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/prolog/output.js b/projects/xml-tools/packages/parser/test/snapshots/valid/prolog/output.js
new file mode 100644
index 00000000..2bcd3c66
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/prolog/output.js
@@ -0,0 +1,182 @@
+module.exports = {
+ cst: {
+ name: "document",
+ children: {
+ prolog: [
+ {
+ name: "prolog",
+ children: {
+ XMLDeclOpen: [
+ {
+ image: "",
+ startOffset: 36,
+ endOffset: 37,
+ tokenType: "SPECIAL_CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 37 },
+ },
+ ],
+ misc: [
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 38,
+ endOffset: 38,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 38, endOffset: 38 },
+ },
+ {
+ name: "misc",
+ children: {
+ SEA_WS: [
+ {
+ image: "\n",
+ startOffset: 52,
+ endOffset: 52,
+ tokenType: "SEA_WS",
+ },
+ ],
+ },
+ location: { startOffset: 52, endOffset: 52 },
+ },
+ ],
+ element: [
+ {
+ name: "element",
+ children: {
+ OPEN: [
+ { image: "<", startOffset: 39, endOffset: 39, tokenType: "OPEN" },
+ ],
+ Name: [
+ {
+ image: "note",
+ startOffset: 40,
+ endOffset: 43,
+ tokenType: "Name",
+ },
+ ],
+ START_CLOSE: [
+ {
+ image: ">",
+ startOffset: 44,
+ endOffset: 44,
+ tokenType: "CLOSE",
+ },
+ ],
+ content: [
+ {
+ name: "content",
+ children: {},
+ location: { startOffset: null, endOffset: null },
+ },
+ ],
+ SLASH_OPEN: [
+ {
+ image: "",
+ startOffset: 45,
+ endOffset: 46,
+ tokenType: "SLASH_OPEN",
+ },
+ ],
+ END_NAME: [
+ {
+ image: "note",
+ startOffset: 47,
+ endOffset: 50,
+ tokenType: "Name",
+ },
+ ],
+ END: [
+ {
+ image: ">",
+ startOffset: 51,
+ endOffset: 51,
+ tokenType: "CLOSE",
+ },
+ ],
+ },
+ location: { startOffset: 39, endOffset: 51 },
+ },
+ ],
+ },
+ location: { startOffset: 0, endOffset: 52 },
+ },
+};
diff --git a/projects/xml-tools/packages/parser/test/snapshots/valid/prolog/sample.spec.js b/projects/xml-tools/packages/parser/test/snapshots/valid/prolog/sample.spec.js
new file mode 100644
index 00000000..afb88de9
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/snapshots/valid/prolog/sample.spec.js
@@ -0,0 +1,7 @@
+const {
+ executeValidSampleTest,
+ testNameFromDir,
+} = require("../../../sample-test");
+describe(`${testNameFromDir(__dirname)}`, () => {
+ executeValidSampleTest(__dirname);
+});
diff --git a/projects/xml-tools/packages/parser/test/utils.js b/projects/xml-tools/packages/parser/test/utils.js
new file mode 100644
index 00000000..a6fafcb3
--- /dev/null
+++ b/projects/xml-tools/packages/parser/test/utils.js
@@ -0,0 +1,47 @@
+const { has, forEach, flatMap, identity } = require("lodash");
+
+function transformCstForAssertions(cstNode) {
+ if (has(cstNode, "children")) {
+ reduceLocationInfo(cstNode.location);
+ const allChildren = flatMap(cstNode.children, identity);
+ forEach(allChildren, transformCstForAssertions);
+ } else if (has(cstNode, "image")) {
+ reduceTokenInfo(cstNode);
+ } else {
+ throw Error("None Exhaustive Match");
+ }
+}
+
+function reduceLocationInfo(loc) {
+ if (isNaN(loc.startOffset)) {
+ loc.startOffset = null;
+ }
+
+ if (isNaN(loc.endOffset)) {
+ loc.endOffset = null;
+ }
+ delete loc.startLine;
+ delete loc.endLine;
+ delete loc.startColumn;
+ delete loc.endColumn;
+}
+
+function reduceTokenInfo(tok) {
+ if (isNaN(tok.startOffset)) {
+ tok.startOffset = null;
+ }
+
+ if (isNaN(tok.endOffset)) {
+ tok.endOffset = null;
+ }
+ delete tok.startLine;
+ delete tok.endLine;
+ delete tok.startColumn;
+ delete tok.endColumn;
+ delete tok.tokenTypeIdx;
+ tok.tokenType = tok.tokenType.name;
+}
+
+module.exports = {
+ transformCstForAssertions: transformCstForAssertions,
+};
diff --git a/projects/xml-tools/packages/parser/tsconfig.json b/projects/xml-tools/packages/parser/tsconfig.json
new file mode 100644
index 00000000..a4ed07be
--- /dev/null
+++ b/projects/xml-tools/packages/parser/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": ["./api.d.ts"]
+}
diff --git a/projects/xml-tools/packages/simple-schema/CHANGELOG.old.md b/projects/xml-tools/packages/simple-schema/CHANGELOG.old.md
new file mode 100644
index 00000000..01eb120a
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/CHANGELOG.old.md
@@ -0,0 +1,160 @@
+> **Historic changelog.** This records changes made while the project lived in the
+> standalone [SAP/xml-tools](https://github.com/SAP/xml-tools) repository, before it was
+> migrated into [SAP/app-studio-toolkit](https://github.com/SAP/app-studio-toolkit).
+> Newer entries are generated by Changesets in `CHANGELOG.md`.
+
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [3.1.0](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@3.0.5...@xml-tools/simple-schema@3.1.0) (2026-02-11)
+
+### Features
+
+- update lodash dependency to version 4.17.23 ([#461](https://github.com/sap/xml-tools/issues/461)) ([23a319d](https://github.com/sap/xml-tools/commit/23a319d6ed42a4a3885ec5748eb127147a8960bc))
+
+## [3.0.5](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@3.0.4...@xml-tools/simple-schema@3.0.5) (2021-06-03)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [3.0.4](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@3.0.3...@xml-tools/simple-schema@3.0.4) (2021-02-09)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [3.0.3](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@3.0.2...@xml-tools/simple-schema@3.0.3) (2021-01-21)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [3.0.2](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@3.0.1...@xml-tools/simple-schema@3.0.2) (2021-01-20)
+
+### Bug Fixes
+
+- missing lib folder in npm packages ([ffed3c2](https://github.com/sap/xml-tools/commit/ffed3c2c54c70aea8b9ded0d53786382bc190cc5))
+
+## [3.0.1](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@3.0.0...@xml-tools/simple-schema@3.0.1) (2021-01-20)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+# [3.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.1.1...@xml-tools/simple-schema@3.0.0) (2020-06-29)
+
+### Features
+
+- **ast:** attribute inside a prolog are now of XMLPrologAttribute type ([#208](https://github.com/sap/xml-tools/issues/208)) ([a303db5](https://github.com/sap/xml-tools/commit/a303db5))
+
+### BREAKING CHANGES
+
+- **ast:** Vistors that need to handle attributes inside prolog must now implement the `visitPrologAttribute` method.
+
+## [2.1.1](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.1.0...@xml-tools/simple-schema@2.1.1) (2020-06-18)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+# [2.1.0](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.0.11...@xml-tools/simple-schema@2.1.0) (2020-05-17)
+
+### Features
+
+- **common:** add utility functions for xmlns attributes ([#173](https://github.com/sap/xml-tools/issues/173)) ([20d6c09](https://github.com/sap/xml-tools/commit/20d6c09))
+
+## [2.0.11](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.0.10...@xml-tools/simple-schema@2.0.11) (2020-05-05)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [2.0.10](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.0.9...@xml-tools/simple-schema@2.0.10) (2020-04-02)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [2.0.9](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.0.8...@xml-tools/simple-schema@2.0.9) (2020-02-19)
+
+### Bug Fixes
+
+- add npmignore file to each package ([5bbf209](https://github.com/sap/xml-tools/commit/5bbf209))
+
+## [2.0.8](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.0.7...@xml-tools/simple-schema@2.0.8) (2020-02-11)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [2.0.7](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.0.6...@xml-tools/simple-schema@2.0.7) (2020-02-11)
+
+### Bug Fixes
+
+- remove extraneous prefix completion items ([#98](https://github.com/sap/xml-tools/issues/98)) ([9b52e70](https://github.com/sap/xml-tools/commit/9b52e70))
+
+## [2.0.6](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.0.4...@xml-tools/simple-schema@2.0.6) (2020-02-10)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [2.0.5](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.0.4...@xml-tools/simple-schema@2.0.5) (2020-02-07)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [2.0.4](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.0.3...@xml-tools/simple-schema@2.0.4) (2020-02-06)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [2.0.3](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.0.2...@xml-tools/simple-schema@2.0.3) (2020-02-06)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [2.0.2](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.0.1...@xml-tools/simple-schema@2.0.2) (2020-01-28)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [2.0.1](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@2.0.0...@xml-tools/simple-schema@2.0.1) (2020-01-20)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+# [2.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@1.0.0...@xml-tools/simple-schema@2.0.0) (2019-12-18)
+
+### Features
+
+- **content-assist:** decouple content assist from parsing ([#58](https://github.com/sap/xml-tools/issues/58)) ([3688da8](https://github.com/sap/xml-tools/commit/3688da8))
+
+### BREAKING CHANGES
+
+- **content-assist:** getSuggestions now uses ast, cst and tokenVector instead of text
+
+# [1.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@0.0.7...@xml-tools/simple-schema@1.0.0) (2019-12-08)
+
+### Features
+
+- **ast:** attributesRange and guessedAttributeRange support ([8ce840d](https://github.com/sap/xml-tools/commit/8ce840d)), closes [#51](https://github.com/sap/xml-tools/issues/51)
+
+### BREAKING CHANGES
+
+- **ast:** buildAst now requires a tokenVector argument
+
+## [0.0.7](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@0.0.6...@xml-tools/simple-schema@0.0.7) (2019-12-05)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [0.0.6](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@0.0.5...@xml-tools/simple-schema@0.0.6) (2019-12-05)
+
+### Bug Fixes
+
+- **simple-schema:** duplicate element error ([f434af8](https://github.com/sap/xml-tools/commit/f434af8))
+
+## [0.0.5](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@0.0.4...@xml-tools/simple-schema@0.0.5) (2019-12-03)
+
+### Bug Fixes
+
+- api type definitions ([46d6c2c](https://github.com/sap/xml-tools/commit/46d6c2c))
+- simple schema with xml namespaces ([#52](https://github.com/sap/xml-tools/issues/52)) ([bb0163e](https://github.com/sap/xml-tools/commit/bb0163e))
+
+## [0.0.4](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@0.0.3...@xml-tools/simple-schema@0.0.4) (2019-11-20)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## [0.0.3](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@0.0.2...@xml-tools/simple-schema@0.0.3) (2019-11-12)
+
+### Bug Fixes
+
+- **simple-schema:** fix Simple-Schema API for enumValue ([0e4fdd8](https://github.com/sap/xml-tools/commit/0e4fdd8))
+
+## [0.0.2](https://github.com/sap/xml-tools/compare/@xml-tools/simple-schema@0.0.1...@xml-tools/simple-schema@0.0.2) (2019-11-11)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
+
+## 0.0.1 (2019-11-11)
+
+**Note:** Version bump only for package @xml-tools/simple-schema
diff --git a/projects/xml-tools/packages/simple-schema/README.md b/projects/xml-tools/packages/simple-schema/README.md
new file mode 100644
index 00000000..bc9a0522
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/README.md
@@ -0,0 +1,119 @@
+[](https://www.npmjs.com/package/@xml-tools/simple-schema)
+
+# @xml-tools/simple-schema
+
+This package includes three elements:
+
+- Data structure definition for a basic XML schema represented as a JavaScript object literal.
+- Validation logic to inspect a given XML conforms to a Schema.
+- Content Assist logic that provides suggestions using the Schema information.
+
+## Installation
+
+With npm:
+
+- `npm install @xml-tools/simple-schema`
+
+With Yarn
+
+- `yarn add @xml-tools/simple-schema`
+
+## Usage
+
+Please see the [TypeScript Definitions](./api.d.ts) for full API details.
+
+- Defining a Schema:
+ ```javascript
+ const schema = {
+ name: "people",
+ required: true,
+ cardinality: "single",
+ attributes: {},
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ eyeColor: {
+ key: "eyeColor",
+ required: false,
+ value: ["grey", "blue", "green", "red"],
+ },
+ },
+ elements: {
+ name: {
+ cardinality: "single",
+ required: true,
+ name: "name",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+ ```
+- A Simple XML Text
+ ```javascript
+ // A Sample XML Text
+ const xmlText = `
+
+ Daenerys Targaryen
+
+ `;
+ ```
+- Performing Validations
+
+ ```javascript
+ const { parse } = require("@xml-tools/parser");
+ const { buildAst } = require("@xml-tools/ast");
+ const { validate } = require("@xml-tools/validation");
+ const { getSchemaValidators } = require("@xml-tools/simple-schema");
+
+ const { cst, tokenVector } = parse(xmlText);
+ const xmlDoc = buildAst(cst, tokenVector);
+ const schemaValidators = getSchemaValidators(schema);
+ const issues = validate({
+ doc: xmlDoc,
+ validators: {
+ attribute: [schemaValidators.attribute],
+ element: [schemaValidators.element],
+ },
+ });
+
+ console.log(issues[0].msg); // Expecting one of but found
+ console.log(issues[0].position); // { startOffset: 46, endOffset: 53 }
+ ```
+
+- Using Content Assist APIs
+
+ ```javascript
+ const { getSuggestions } = require("@xml-tools/content-assist");
+ const { getSchemaSuggestionsProviders } = require("@xml-tools/simple-schema");
+
+ const schemaSuggestionsProviders = getSchemaSuggestionsProviders(schema);
+
+ const suggestions = getSuggestions({
+ text: xmlText,
+ offset: 47, // within the eyeColor quoted value
+ providers: {
+ attributeValue: [
+ schemaSuggestionsProviders.schemaAttributeValueCompletion,
+ ],
+ attributeName: [schemaSuggestionsProviders.schemaAttributeNameCompletion],
+ elementName: [schemaSuggestionsProviders.schemaElementNameCompletion],
+ },
+ });
+
+ console.log(suggestions[0]); // { text: 'grey', label: 'grey' }
+ console.log(suggestions[1]); // { text: 'blue', label: 'blue' }
+ ```
+
+## Support
+
+Please report issues [here](https://github.com/SAP/app-studio-toolkit/issues/new/choose) and label them with `project:xml-tools`.
+
+## Contributing
+
+See [CONTRIBUTING.md](https://github.com/SAP/app-studio-toolkit/blob/main/CONTRIBUTING.md).
diff --git a/projects/xml-tools/packages/simple-schema/api.d.ts b/projects/xml-tools/packages/simple-schema/api.d.ts
new file mode 100644
index 00000000..e48796b8
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/api.d.ts
@@ -0,0 +1,73 @@
+import { XMLAstNode, XMLAttribute, XMLElement } from "@xml-tools/ast";
+import {
+ ValidationIssue,
+ AttributeValidator,
+ ElementValidator,
+} from "@xml-tools/validation";
+import {
+ AttributeNameCompletion,
+ AttributeValueCompletion,
+ ElementContentCompletion,
+ ElementNameCompletion,
+} from "@xml-tools/content-assist";
+
+// Represents the root element in an XML document
+declare type SimpleSchema = XSSElement & {
+ // There may only be a single top level element in XML
+ cardinality: "single";
+ // The top level element in mandatory in XML
+ require: true;
+};
+
+declare type XSSElement = {
+ required: boolean;
+ cardinality: "many" | "single";
+ name: string;
+ namespace?: string;
+
+ attributesType?: "open" | "closed";
+ attributes: Record;
+
+ elementsType?: "open" | "closed";
+ elements: Record;
+ // TODO: textValue definition (for simple pure text only?)
+};
+
+declare type XSSAttribute = {
+ required: boolean;
+ key: string;
+ value?: XSSValue;
+};
+
+// TODO: could be expended to support more primitive types and also complex types such as `union type`
+declare type XSSValue = RegExp | XSSValueEnum;
+
+declare type XSSValueEnum = string[];
+
+declare function getSchemaValidators(schema: SimpleSchema): {
+ attribute: AttributeValidator;
+ element: ElementValidator;
+};
+
+interface CompletionSuggestion {
+ text: string;
+ label?: string;
+ docs?: string;
+ commitCharacter?: string;
+ isNamespace?: boolean;
+ /**
+ * A measure of how certain we are about this suggestion's relevance.
+ * This value could be used to:
+ * - Filter out less relevant suggestions when there are too many possible suggestions.
+ * - Sort the suggestions by relevance.
+ */
+ confidence?: number;
+}
+
+declare function getSchemaSuggestionsProviders(schema: SimpleSchema): {
+ // TBD in the future...
+ // schemaElementContentCompletion: ElementContentCompletion;
+ schemaElementNameCompletion: ElementNameCompletion;
+ schemaAttributeNameCompletion: AttributeNameCompletion;
+ schemaAttributeValueCompletion: AttributeValueCompletion;
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/api.js b/projects/xml-tools/packages/simple-schema/lib/api.js
new file mode 100644
index 00000000..200fc047
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/api.js
@@ -0,0 +1,7 @@
+const { getSchemaValidators } = require("./get-validators");
+const { getSchemaSuggestionsProviders } = require("./get-content-assist");
+
+module.exports = {
+ getSchemaValidators: getSchemaValidators,
+ getSchemaSuggestionsProviders: getSchemaSuggestionsProviders,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/content-assist/attribute-name.js b/projects/xml-tools/packages/simple-schema/lib/content-assist/attribute-name.js
new file mode 100644
index 00000000..c48ea666
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/content-assist/attribute-name.js
@@ -0,0 +1,30 @@
+const { difference, map } = require("lodash");
+
+/**
+ * @param {XMLElement} elementNode
+ * @param {XSSElement} xssElement
+ *
+ * @returns {CompletionSuggestion[]}
+ */
+function attributeNameCompletion(elementNode, xssElement) {
+ const possibleSuggestions = map(xssElement.attributes, (_) => _.key);
+ const existingAttribNames = map(elementNode.attributes, (_) => _.key);
+ const possibleNewSuggestions = difference(
+ possibleSuggestions,
+ existingAttribNames
+ );
+
+ const suggestions = map(possibleNewSuggestions, (_) => {
+ return {
+ text: _,
+ label: _,
+ commitCharacter: "=",
+ };
+ });
+
+ return suggestions;
+}
+
+module.exports = {
+ attributeNameCompletion: attributeNameCompletion,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/content-assist/attribute-value.js b/projects/xml-tools/packages/simple-schema/lib/content-assist/attribute-value.js
new file mode 100644
index 00000000..6ca4c3a7
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/content-assist/attribute-value.js
@@ -0,0 +1,40 @@
+const { has, isRegExp, forEach, isArray } = require("lodash");
+
+/**
+ * @param {XMLAttribute} attributeNode
+ * @param {XSSAttribute} xssAttribute
+ * @param {string} prefix
+ *
+ * @returns {CompletionSuggestion[]}
+ */
+function attributeValueCompletion(attributeNode, xssAttribute, prefix = "") {
+ // An XSS Attribute definition may not specify any constraints on a value
+ if (has(xssAttribute, "value") === false) {
+ return [];
+ }
+
+ const suggestions = [];
+ const valueDef = xssAttribute.value;
+ /* istanbul ignore else - defensive programming */
+ if (isRegExp(valueDef)) {
+ // No suggestions for regExp value definitions...
+ } else if (isArray(valueDef)) {
+ forEach(valueDef, (enumVal) => {
+ if (enumVal.startsWith(prefix)) {
+ suggestions.push({
+ text: enumVal.substring(prefix.length),
+ label: enumVal,
+ });
+ }
+ });
+ } else {
+ /* istanbul ignore next defensive programming */
+ throw Error("None Exhaustive Match");
+ }
+
+ return suggestions;
+}
+
+module.exports = {
+ attributeValueCompletion: attributeValueCompletion,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/content-assist/element-name.js b/projects/xml-tools/packages/simple-schema/lib/content-assist/element-name.js
new file mode 100644
index 00000000..0f2da9fb
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/content-assist/element-name.js
@@ -0,0 +1,96 @@
+const { difference, map, filter, has, pickBy } = require("lodash");
+const { DEFAULT_NS } = require("@xml-tools/ast");
+
+// https://www.w3.org/TR/2009/REC-xml-names-20091208/#NT-PrefixedName
+const NAMESPACE_PATTERN = /^(?:([^:]*):)?([^:]*)$/;
+/**
+ *
+ * Note that the Element (XML/XSS) are of the parent node of the element
+ * in which content assist was requested.
+ *
+ * @param {XMLElement} elementNode
+ * @param {XSSElement} xssElement
+ *
+ * @returns {CompletionSuggestion[]}
+ */
+function elementNameCompletion(elementNode, xssElement, prefix = "") {
+ const match = prefix.match(NAMESPACE_PATTERN);
+ if (match === null) {
+ return [];
+ }
+ // If there is no prefix, use the default namespace prefix
+ const namespacePrefix = match[1] ? match[1] : DEFAULT_NS;
+ const elementNamespaceUri = elementNode.namespaces[namespacePrefix];
+ const possibleElements = filter(
+ xssElement.elements,
+ (_) =>
+ has(_, "namespace") === false ||
+ (_.namespace && _.namespace === elementNamespaceUri)
+ );
+ const possibleSuggestionsWithoutExistingSingular = applicableElements(
+ xssElement.elements,
+ elementNode.subElements,
+ possibleElements
+ );
+
+ const suggestions = map(possibleSuggestionsWithoutExistingSingular, (_) => {
+ return {
+ text: _,
+ label: _,
+ };
+ });
+
+ if (namespacePrefix === undefined || namespacePrefix === DEFAULT_NS) {
+ // Can't really suggest anything for the `implicit` default namespace...
+ const namespacesWithoutDefault = pickBy(
+ elementNode.namespaces,
+ (uri, prefix) => prefix !== DEFAULT_NS
+ );
+ const applicableNamespaces = pickBy(namespacesWithoutDefault, (uri) => {
+ const possibleElements = filter(
+ xssElement.elements,
+ (element) =>
+ has(element, "namespace") === true && element.namespace === uri
+ );
+ const possibleSuggestionsWithoutExistingSingular = applicableElements(
+ xssElement.elements,
+ elementNode.subElements,
+ possibleElements
+ );
+ const namespaceHasApplicableElements =
+ possibleSuggestionsWithoutExistingSingular.length > 0;
+ return namespaceHasApplicableElements;
+ });
+ const namespaceSuggestions = map(applicableNamespaces, (uri, prefix) => ({
+ text: prefix,
+ label: prefix,
+ commitCharacter: ":",
+ isNamespace: true,
+ }));
+ return [...namespaceSuggestions, ...suggestions];
+ }
+ return suggestions;
+}
+
+function applicableElements(xssElements, subElements, possibleElements) {
+ const allPossibleSuggestions = map(
+ possibleElements,
+ (element) => element.name
+ );
+ const notSingularElem = filter(
+ xssElements,
+ (element) => element.cardinality === "many"
+ );
+ const notSingularElemNames = map(notSingularElem, (element) => element.name);
+ const existingElemNames = map(subElements, (element) => element.name);
+ const existingSingular = difference(existingElemNames, notSingularElemNames);
+ const possibleSuggestionsWithoutExistingSingular = difference(
+ allPossibleSuggestions,
+ existingSingular
+ );
+ return possibleSuggestionsWithoutExistingSingular;
+}
+
+module.exports = {
+ elementNameCompletion: elementNameCompletion,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/get-content-assist.js b/projects/xml-tools/packages/simple-schema/lib/get-content-assist.js
new file mode 100644
index 00000000..e4ef8eb3
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/get-content-assist.js
@@ -0,0 +1,62 @@
+const { attributeNameCompletion } = require("./content-assist/attribute-name");
+const {
+ attributeValueCompletion,
+} = require("./content-assist/attribute-value");
+const { elementNameCompletion } = require("./content-assist/element-name");
+const { findElementXssDef, findAttributeXssDef } = require("./path");
+
+function getSchemaSuggestionsProviders(schema) {
+ const attributeNameProvider = buildAttributeNameProvider(schema);
+ const attributeValueProvider = buildAttributeValueProvider(schema);
+ const elementNameProvider = buildElementNameProvider(schema);
+
+ return {
+ schemaElementNameCompletion: elementNameProvider,
+ schemaAttributeNameCompletion: attributeNameProvider,
+ schemaAttributeValueCompletion: attributeValueProvider,
+ };
+}
+
+/**
+ * @param {SimpleSchema} schema
+ */
+function buildAttributeNameProvider(schema) {
+ return ({ element, prefix }) => {
+ const xssElementDef = findElementXssDef(element, schema);
+ if (xssElementDef !== undefined) {
+ return attributeNameCompletion(element, xssElementDef, prefix);
+ } else {
+ return [];
+ }
+ };
+}
+
+/**
+ * @param {SimpleSchema} schema
+ */
+function buildElementNameProvider(schema) {
+ return ({ element, prefix }) => {
+ // Note we are finding the definition for the element's parent
+ // Because the information on possible sibling elements exists there...
+ const xssElementDef = findElementXssDef(element.parent, schema);
+ if (xssElementDef !== undefined) {
+ return elementNameCompletion(element.parent, xssElementDef, prefix);
+ } else {
+ return [];
+ }
+ };
+}
+
+/**
+ * @param {SimpleSchema} schema
+ */
+function buildAttributeValueProvider(schema) {
+ return ({ attribute, prefix }) => {
+ const attributeXssDef = findAttributeXssDef(attribute, schema);
+ return attributeValueCompletion(attribute, attributeXssDef, prefix);
+ };
+}
+
+module.exports = {
+ getSchemaSuggestionsProviders: getSchemaSuggestionsProviders,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/get-validators.js b/projects/xml-tools/packages/simple-schema/lib/get-validators.js
new file mode 100644
index 00000000..3cedb410
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/get-validators.js
@@ -0,0 +1,99 @@
+const { validateAttributeValue } = require("./validators/attribute-value");
+
+const {
+ validateDuplicateSubElements,
+} = require("./validators/duplicate-sub-elements");
+
+const {
+ validateRequiredAttributes,
+} = require("./validators/required-attributes");
+
+const {
+ validateRequiredSubElements,
+} = require("./validators/required-sub-elements");
+
+const {
+ validateUnknownAttributes,
+} = require("./validators/unknown-attributes");
+
+const {
+ validateUnknownSubElements,
+} = require("./validators/unknown-sub-elements");
+
+const { findAttributeXssDef, findElementXssDef } = require("./path");
+
+function getSchemaValidators(schema) {
+ const attributeValidator = buildAttributeValidator(schema);
+ const elementValidator = buildElementValidator(schema);
+
+ return {
+ attribute: attributeValidator,
+ element: elementValidator,
+ };
+}
+
+/**
+ * @param {SimpleSchema} schema
+ */
+function buildAttributeValidator(schema) {
+ return (attributeNode) => {
+ let issues = [];
+ const xssAttributeDef = findAttributeXssDef(attributeNode, schema);
+
+ if (xssAttributeDef !== undefined) {
+ const attributeValueIssues = validateAttributeValue(
+ attributeNode,
+ xssAttributeDef
+ );
+ issues = issues.concat(attributeValueIssues);
+ }
+
+ return issues;
+ };
+}
+
+/**
+ * @param {SimpleSchema} schema
+ */
+function buildElementValidator(schema) {
+ return (elementNode) => {
+ let issues = [];
+ const xssElementDef = findElementXssDef(elementNode, schema);
+
+ if (xssElementDef !== undefined) {
+ const duplicateElementsIssues = validateDuplicateSubElements(
+ elementNode,
+ xssElementDef
+ );
+ const requiredAttributesIssues = validateRequiredAttributes(
+ elementNode,
+ xssElementDef
+ );
+ const requiredSubElementsIssues = validateRequiredSubElements(
+ elementNode,
+ xssElementDef
+ );
+ const unknownAttributesIssues = validateUnknownAttributes(
+ elementNode,
+ xssElementDef
+ );
+ const unknownSubElementsIssues = validateUnknownSubElements(
+ elementNode,
+ xssElementDef
+ );
+ issues = issues.concat(
+ duplicateElementsIssues,
+ requiredAttributesIssues,
+ requiredSubElementsIssues,
+ unknownAttributesIssues,
+ unknownSubElementsIssues
+ );
+ }
+
+ return issues;
+ };
+}
+
+module.exports = {
+ getSchemaValidators: getSchemaValidators,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/path.js b/projects/xml-tools/packages/simple-schema/lib/path.js
new file mode 100644
index 00000000..619c1aa6
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/path.js
@@ -0,0 +1,69 @@
+const { drop, map, forEach, first } = require("lodash");
+
+/**
+ * @param {XMLAttribute} attribNode
+ * @param {SimpleSchema} schema
+ */
+function findAttributeXssDef(attribNode, schema) {
+ const xssElement = findElementXssDef(attribNode.parent, schema);
+
+ let xssAttribute = undefined;
+ if (xssElement !== undefined) {
+ const attributeName = attribNode.key;
+ xssAttribute = xssElement.attributes[attributeName];
+ }
+
+ return xssAttribute;
+}
+
+/**
+ * @param {XMLElement} node
+ * @param {SimpleSchema} schema
+ */
+function findElementXssDef(node, schema) {
+ const ancestors = getAstNodeAncestors(node);
+ const elementsPath = map(ancestors, "name");
+
+ const rootElement = first(elementsPath);
+ // Root Element mis-match, The Schema cannot provide any attribute validations for this XML AST.
+ if (rootElement !== schema.name) {
+ return undefined;
+ }
+ let xssElement = schema;
+ forEach(drop(elementsPath), (elemName) => {
+ // traverse subElements
+ xssElement = xssElement.elements[elemName];
+ if (xssElement === undefined) {
+ return false;
+ }
+ });
+
+ return xssElement;
+}
+
+/**
+ * @param {XMLAstNode} node
+ *
+ * @returns {XMLAstNode[]} - The Ancestors do not include the XMLDocument
+ */
+function getAstNodeAncestors(node) {
+ const ancestors = [];
+ ancestors.push(node);
+ let currAncestor = node.parent;
+ while (
+ currAncestor !== undefined &&
+ // The Simple Schema only starts at the root Element (not the Root Document).
+ currAncestor.type !== "XMLDocument"
+ ) {
+ ancestors.push(currAncestor);
+ currAncestor = currAncestor.parent;
+ }
+ ancestors.reverse();
+
+ return ancestors;
+}
+
+module.exports = {
+ findAttributeXssDef: findAttributeXssDef,
+ findElementXssDef: findElementXssDef,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/validators/attribute-value.js b/projects/xml-tools/packages/simple-schema/lib/validators/attribute-value.js
new file mode 100644
index 00000000..0cde64c1
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/validators/attribute-value.js
@@ -0,0 +1,57 @@
+const { isRegExp, isArray, includes, has } = require("lodash");
+const { tokenToOffsetPosition } = require("./utils");
+
+/**
+ * @param {XMLAttribute} attributeNode
+ * @param {XSSAttribute }xssAttribute
+ *
+ * @returns {ValidationIssue[]}
+ */
+function validateAttributeValue(attributeNode, xssAttribute) {
+ const issues = [];
+
+ const valueDef = xssAttribute.value;
+
+ // An XSS Attribute definition may not specify any constraints on a value
+ if (has(xssAttribute, "value") === false) {
+ return issues;
+ }
+
+ const actualValue = attributeNode.value;
+ if (actualValue === null) {
+ // we cannot validate a partial attribute AST without an actual value...
+ return issues;
+ }
+
+ // This is always safe because at this point we know the attribute has a value
+ const errPosition = tokenToOffsetPosition(attributeNode.syntax.value);
+ /* istanbul ignore else defensive programming */
+ if (isRegExp(valueDef)) {
+ if (valueDef.test(actualValue) === false) {
+ issues.push({
+ msg: `Expecting Value matching <${valueDef.toString()}> but found <${actualValue}>`,
+ node: attributeNode,
+ severity: "error",
+ position: errPosition,
+ });
+ }
+ } else if (isArray(valueDef)) {
+ if (includes(valueDef, actualValue) === false) {
+ issues.push({
+ msg: `Expecting one of <${valueDef.toString()}> but found <${actualValue}>`,
+ node: attributeNode,
+ severity: "error",
+ position: errPosition,
+ });
+ }
+ } else {
+ /* istanbul ignore next defensive programming */
+ throw Error("None Exhaustive Match");
+ }
+
+ return issues;
+}
+
+module.exports = {
+ validateAttributeValue: validateAttributeValue,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/validators/duplicate-sub-elements.js b/projects/xml-tools/packages/simple-schema/lib/validators/duplicate-sub-elements.js
new file mode 100644
index 00000000..fc9d0284
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/validators/duplicate-sub-elements.js
@@ -0,0 +1,42 @@
+const { map, forEach, includes, filter, groupBy } = require("lodash");
+const { tokenToOffsetPosition } = require("./utils");
+
+/**
+ * @param {XMLElement} elem
+ * @param {XSSElement} schema
+ *
+ * @returns {ValidationIssue[]}
+ */
+function validateDuplicateSubElements(elem, schema) {
+ const allowedDupElem = filter(
+ schema.elements,
+ (_) => _.cardinality === "many"
+ );
+ const allowedDupElemNames = map(allowedDupElem, (_) => _.name);
+
+ const actualSubElemByName = groupBy(elem.subElements, (_) => _.name);
+ const issues = [];
+ forEach(actualSubElemByName, (dupElements, dupElementsName) => {
+ const allowedDup = includes(allowedDupElemNames, dupElementsName);
+ const hasConfiguration = schema.elements[dupElementsName] !== undefined;
+ const hasDuplicates = dupElements.length > 1;
+
+ if (allowedDup === false && hasDuplicates && hasConfiguration) {
+ forEach(dupElements, (dupElem) => {
+ issues.push({
+ msg: `Duplicate Sub-Element: <${dupElem.name}> only a single occurrence of this Sub-Element is allowed here.`,
+ node: dupElem,
+ severity: "error",
+ // safe assumption that we have an `openName` (see above condition)
+ position: tokenToOffsetPosition(dupElem.syntax.openName),
+ });
+ });
+ }
+ });
+
+ return issues;
+}
+
+module.exports = {
+ validateDuplicateSubElements: validateDuplicateSubElements,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/validators/required-attributes.js b/projects/xml-tools/packages/simple-schema/lib/validators/required-attributes.js
new file mode 100644
index 00000000..073cfe5d
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/validators/required-attributes.js
@@ -0,0 +1,39 @@
+const { map, filter, difference } = require("lodash");
+const { tokenToOffsetPosition } = require("./utils");
+
+/**
+ * @param {XMLElement} elem
+ * @param {XSSElement} schema
+ *
+ * @returns {ValidationIssue[]}
+ */
+function validateRequiredAttributes(elem, schema) {
+ const requiredAttribsDef = filter(
+ schema.attributes,
+ (_) => _.required === true
+ );
+ const requiredAttribNames = map(requiredAttribsDef, (_) => _.key);
+
+ const actualAttribNames = map(elem.attributes, (_) => _.key);
+ const missingAttributesNames = difference(
+ requiredAttribNames,
+ actualAttribNames
+ );
+
+ // This elementName must always exist, otherwise we could not locate the relevant schema definition
+ // so this validation could have never executed...
+ const errPosition = tokenToOffsetPosition(elem.syntax.openName);
+ const issues = map(missingAttributesNames, (_) => {
+ return {
+ msg: `Missing Required Attribute: <${_}>`,
+ node: elem,
+ severity: "error",
+ position: errPosition,
+ };
+ });
+ return issues;
+}
+
+module.exports = {
+ validateRequiredAttributes: validateRequiredAttributes,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/validators/required-sub-elements.js b/projects/xml-tools/packages/simple-schema/lib/validators/required-sub-elements.js
new file mode 100644
index 00000000..48dd3cec
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/validators/required-sub-elements.js
@@ -0,0 +1,39 @@
+const { map, filter, difference } = require("lodash");
+const { tokenToOffsetPosition } = require("./utils");
+
+/**
+ * @param {XMLElement} elem
+ * @param {XSSElement} schema
+ *
+ * @returns {ValidationIssue[]}
+ */
+function validateRequiredSubElements(elem, schema) {
+ const requiredSubElemsDef = filter(
+ schema.elements,
+ (_) => _.required === true
+ );
+ const requiredElemNames = map(requiredSubElemsDef, (_) => _.name);
+
+ const actualSubElemNameNames = map(elem.subElements, (_) => _.name);
+ const missingSubElemNames = difference(
+ requiredElemNames,
+ actualSubElemNameNames
+ );
+
+ // This elementName must always exist, otherwise we could not locate the relevant schema definition
+ // so this validation could have never executed...
+ const errPosition = tokenToOffsetPosition(elem.syntax.openName);
+ const issues = map(missingSubElemNames, (_) => {
+ return {
+ msg: `Missing Required Sub-Element: <${_}>`,
+ node: elem,
+ severity: "error",
+ position: errPosition,
+ };
+ });
+ return issues;
+}
+
+module.exports = {
+ validateRequiredSubElements: validateRequiredSubElements,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/validators/unknown-attributes.js b/projects/xml-tools/packages/simple-schema/lib/validators/unknown-attributes.js
new file mode 100644
index 00000000..3c7930bd
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/validators/unknown-attributes.js
@@ -0,0 +1,46 @@
+const { map, includes, forEach } = require("lodash");
+const { isXMLNamespaceKey } = require("@xml-tools/common");
+const { tokenToOffsetPosition } = require("./utils");
+
+/**
+ * @param {XMLElement} elem
+ * @param {XSSElement} schema
+ *
+ * @returns {ValidationIssue[]}
+ */
+function validateUnknownAttributes(elem, schema) {
+ // This validation is only relevant if the Schema disallows unknown attributes.
+ if (schema.attributesType !== "closed") {
+ return [];
+ }
+ const allowedAttribNames = map(schema.attributes, (_) => _.key);
+
+ const issues = [];
+ forEach(elem.attributes, (attrib) => {
+ /* istanbul ignore else - Defensive programming, but cannot
+ * reproduce this branch with current error recovery heuristics
+ */
+ if (attrib.key !== null) {
+ if (
+ includes(allowedAttribNames, attrib.key) === false &&
+ isXMLNamespaceKey({ key: attrib.key, includeEmptyPrefix: true }) ===
+ false
+ ) {
+ issues.push({
+ msg: `Unknown Attribute: <${
+ attrib.key
+ }> only [${allowedAttribNames.toString()}] attributes are allowed`,
+ node: attrib,
+ severity: "error",
+ // safe assumption that we have a `key` token (see above condition)
+ position: tokenToOffsetPosition(attrib.syntax.key),
+ });
+ }
+ }
+ });
+ return issues;
+}
+
+module.exports = {
+ validateUnknownAttributes: validateUnknownAttributes,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/validators/unknown-sub-elements.js b/projects/xml-tools/packages/simple-schema/lib/validators/unknown-sub-elements.js
new file mode 100644
index 00000000..54a5094f
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/validators/unknown-sub-elements.js
@@ -0,0 +1,40 @@
+const { map, forEach, includes } = require("lodash");
+const { tokenToOffsetPosition } = require("./utils");
+
+/**
+ * @param {XMLElement} elem
+ * @param {XSSElement} schema
+ *
+ * @returns {ValidationIssue[]}
+ */
+function validateUnknownSubElements(elem, schema) {
+ // This validation is only relevant if the Schema disallows unknown elements.
+ if (schema.elementsType !== "closed") {
+ return [];
+ }
+
+ const allowedElemNames = map(schema.elements, (_) => _.name);
+
+ const issues = [];
+ forEach(elem.subElements, (subElem) => {
+ if (subElem.name !== null) {
+ if (includes(allowedElemNames, subElem.name) === false) {
+ issues.push({
+ msg: `Unknown Sub-Element: <${
+ subElem.name
+ }> only [${allowedElemNames.toString()}] Sub-Elements are allowed`,
+ node: subElem,
+ severity: "error",
+ // safe assumption that we have an `openName` (see above condition)
+ position: tokenToOffsetPosition(subElem.syntax.openName),
+ });
+ }
+ }
+ });
+
+ return issues;
+}
+
+module.exports = {
+ validateUnknownSubElements: validateUnknownSubElements,
+};
diff --git a/projects/xml-tools/packages/simple-schema/lib/validators/utils.js b/projects/xml-tools/packages/simple-schema/lib/validators/utils.js
new file mode 100644
index 00000000..95560d1e
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/lib/validators/utils.js
@@ -0,0 +1,9 @@
+const { pick } = require("lodash");
+
+function tokenToOffsetPosition(token) {
+ return pick(token, ["startOffset", "endOffset"]);
+}
+
+module.exports = {
+ tokenToOffsetPosition: tokenToOffsetPosition,
+};
diff --git a/projects/xml-tools/packages/simple-schema/nyc.config.js b/projects/xml-tools/packages/simple-schema/nyc.config.js
new file mode 100644
index 00000000..3e3f8962
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/nyc.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ // xml-tools sources live in lib/ (not src/), so this local config overrides
+ // the repo-root nyc.config.js include of **/src/** (which would report 0%).
+ include: ["lib/**/*.js"],
+ reporter: ["text", "lcov"],
+ all: true,
+ branches: 100,
+ lines: 100,
+ functions: 100,
+ statements: 100,
+};
diff --git a/projects/xml-tools/packages/simple-schema/package.json b/projects/xml-tools/packages/simple-schema/package.json
new file mode 100644
index 00000000..a9e811be
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@xml-tools/simple-schema",
+ "version": "3.1.0",
+ "description": "XML Simple Schema",
+ "keywords": [
+ "xml",
+ "validation"
+ ],
+ "main": "lib/api.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/SAP/app-studio-toolkit.git",
+ "directory": "projects/xml-tools/packages/simple-schema"
+ },
+ "license": "Apache-2.0",
+ "typings": "./api.d.ts",
+ "files": [
+ "lib",
+ "api.d.ts",
+ "CHANGELOG.md",
+ "CHANGELOG.old.md"
+ ],
+ "dependencies": {
+ "@xml-tools/ast": "workspace:*",
+ "@xml-tools/common": "workspace:*",
+ "@xml-tools/content-assist": "workspace:*",
+ "lodash": "4.17.23"
+ },
+ "devDependencies": {
+ "@xml-tools/parser": "workspace:*",
+ "@xml-tools/validation": "workspace:*"
+ },
+ "scripts": {
+ "ci": "npm-run-all clean type-check coverage:*",
+ "clean": "rimraf ./coverage ./nyc_output",
+ "test": "mocha \"./test/**/*spec.js\"",
+ "coverage:run": "nyc mocha \"./test/**/*spec.js\"",
+ "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100",
+ "type-check": "tsc"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "bugs": {
+ "url": "https://github.com/SAP/app-studio-toolkit/issues"
+ }
+}
diff --git a/projects/xml-tools/packages/simple-schema/test/content-assist/atrribute-name-spec.js b/projects/xml-tools/packages/simple-schema/test/content-assist/atrribute-name-spec.js
new file mode 100644
index 00000000..eaae3b49
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/test/content-assist/atrribute-name-spec.js
@@ -0,0 +1,189 @@
+const { expect } = require("chai");
+const { suggestionsBySchema } = require("./utils");
+
+describe("The XML Simple Schema", () => {
+ describe("Content Assist Functionality", () => {
+ context("In Attribute Names Scenario", () => {
+ it("provides suggestion: without prefix", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ name: {
+ required: false,
+ key: "name",
+ },
+ age: {
+ required: false,
+ key: "age",
+ },
+ address: {
+ required: false,
+ key: "address",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([
+ {
+ label: "name",
+ text: "name",
+ commitCharacter: "=",
+ },
+ {
+ label: "age",
+ text: "age",
+ commitCharacter: "=",
+ },
+ {
+ label: "address",
+ text: "address",
+ commitCharacter: "=",
+ },
+ ]);
+ expect(suggestions).to.have.lengthOf(3);
+ });
+
+ it("provides suggestion: with prefix", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ name: {
+ required: false,
+ key: "name",
+ },
+ age: {
+ required: false,
+ key: "age",
+ },
+ address: {
+ required: false,
+ key: "address",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([
+ {
+ label: "name",
+ text: "name",
+ commitCharacter: "=",
+ },
+ {
+ label: "age",
+ text: "age",
+ commitCharacter: "=",
+ },
+ {
+ label: "address",
+ text: "address",
+ commitCharacter: "=",
+ },
+ ]);
+ expect(suggestions).to.have.lengthOf(3);
+ });
+ it("does not crash wehen xss element does not exist", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {},
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([]);
+ expect(suggestions).to.have.lengthOf(0);
+ });
+
+ it("filters suggestions by existing attributes", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ name: {
+ required: false,
+ key: "name",
+ },
+ age: {
+ required: false,
+ key: "age",
+ },
+ address: {
+ required: false,
+ key: "address",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([
+ {
+ label: "name",
+ text: "name",
+ commitCharacter: "=",
+ },
+ {
+ label: "address",
+ text: "address",
+ commitCharacter: "=",
+ },
+ ]);
+ expect(suggestions).to.have.lengthOf(2);
+ });
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/simple-schema/test/content-assist/atrribute-value-spec.js b/projects/xml-tools/packages/simple-schema/test/content-assist/atrribute-value-spec.js
new file mode 100644
index 00000000..e4a61c21
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/test/content-assist/atrribute-value-spec.js
@@ -0,0 +1,181 @@
+const { expect } = require("chai");
+const { suggestionsBySchema } = require("./utils");
+
+describe("The XML Simple Schema", () => {
+ describe("Content Assist Functionality", () => {
+ context("In Attribute Values Scenario", () => {
+ it("provides suggestion for enum without prefix", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ age: {
+ required: false,
+ key: "age",
+ value: ["66", "67", "666"],
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.have.lengthOf(3);
+ expect(suggestions).to.deep.include.members([
+ {
+ label: "66",
+ text: "66",
+ },
+ {
+ label: "67",
+ text: "67",
+ },
+ {
+ label: "666",
+ text: "666",
+ },
+ ]);
+ });
+
+ it("Filters suggestions for enum using prefix", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ age: {
+ required: false,
+ key: "age",
+ value: ["66", "67", "666"],
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.have.lengthOf(3);
+ expect(suggestions).to.deep.include.members([
+ {
+ label: "66",
+ text: "6",
+ },
+ {
+ label: "67",
+ text: "7",
+ },
+ {
+ label: "666",
+ text: "66",
+ },
+ ]);
+ });
+
+ it("Does not provide suggestions for regExp value Definitions", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ age: {
+ required: false,
+ key: "age",
+ value: /^\d\d$/,
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.have.lengthOf(0);
+ });
+
+ it("Does not provide suggestions if there is no XSS value definition", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ age: {
+ required: false,
+ key: "age",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.have.lengthOf(0);
+ });
+
+ it("Does not crash if there is no XSS value definition", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {},
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.have.lengthOf(0);
+ });
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/simple-schema/test/content-assist/element-name-spec.js b/projects/xml-tools/packages/simple-schema/test/content-assist/element-name-spec.js
new file mode 100644
index 00000000..3477017e
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/test/content-assist/element-name-spec.js
@@ -0,0 +1,667 @@
+const { expect } = require("chai");
+const { suggestionsBySchema } = require("./utils");
+
+describe("The XML Simple Schema", () => {
+ describe("Content Assist Functionality", () => {
+ context("In Element Names Scenario", () => {
+ it("provides suggestion: without prefix", () => {
+ const xmlText = `
+
+ <⇶
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ cardinality: "single",
+ required: false,
+ name: "name",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ cardinality: "single",
+ required: false,
+ name: "age",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ cardinality: "many",
+ required: false,
+ name: "address",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([
+ {
+ label: "name",
+ text: "name",
+ },
+ {
+ label: "age",
+ text: "age",
+ },
+ {
+ label: "address",
+ text: "address",
+ },
+ ]);
+ expect(suggestions).to.have.lengthOf(3);
+ });
+
+ it("provides suggestion: with prefix", () => {
+ const xmlText = `
+
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ cardinality: "single",
+ required: false,
+ name: "name",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ cardinality: "single",
+ required: false,
+ name: "age",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ cardinality: "many",
+ required: false,
+ name: "address",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([
+ {
+ label: "name",
+ text: "name",
+ },
+ {
+ label: "age",
+ text: "age",
+ },
+ {
+ label: "address",
+ text: "address",
+ },
+ ]);
+ expect(suggestions).to.have.lengthOf(3);
+ });
+
+ it("filters pre-existing sub-elements with `single` cardinality", () => {
+ const xmlText = `
+
+ 66
+ <⇶
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ cardinality: "single",
+ required: false,
+ name: "name",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ cardinality: "single",
+ required: false,
+ name: "age",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ cardinality: "many",
+ required: false,
+ name: "address",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([
+ {
+ label: "name",
+ text: "name",
+ },
+ {
+ label: "address",
+ text: "address",
+ },
+ ]);
+ expect(suggestions).to.have.lengthOf(2);
+ });
+
+ it("allows multiple sub-elements with `multiple` cardinality", () => {
+ const xmlText = `
+
+ 66
+
+ <⇶
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ cardinality: "single",
+ required: false,
+ name: "name",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ cardinality: "single",
+ required: false,
+ name: "age",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ cardinality: "many",
+ required: false,
+ name: "address",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([
+ {
+ label: "person",
+ text: "person",
+ },
+ ]);
+ expect(suggestions).to.have.lengthOf(1);
+ });
+ });
+
+ it("does not crash if there is no xss element", () => {
+ const xmlText = `
+
+ <⇶
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {},
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([]);
+ expect(suggestions).to.have.lengthOf(0);
+ });
+
+ it("nested elements with namespaces", () => {
+ const xmlText = `
+
+ <⇶
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ namespace: "http://namespace.com/1",
+ attributes: {},
+ elements: {
+ person: {
+ name: "person",
+ namespace: "http://namespace.com/2",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ cardinality: "single",
+ required: false,
+ name: "name",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ friends: {
+ cardinality: "single",
+ required: false,
+ name: "friends",
+ namespace: "http://namespace.com/1",
+ attributes: {},
+ elements: {
+ person: {
+ name: "person",
+ namespace: "http://namespace.com/2",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ name: {
+ cardinality: "single",
+ required: false,
+ name: "name",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ },
+ age: {
+ cardinality: "single",
+ required: false,
+ name: "age",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ cardinality: "many",
+ required: false,
+ name: "address",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([
+ {
+ text: "abc",
+ label: "abc",
+ isNamespace: true,
+ commitCharacter: ":",
+ },
+ {
+ label: "name",
+ text: "name",
+ },
+ {
+ label: "age",
+ text: "age",
+ },
+ {
+ label: "address",
+ text: "address",
+ },
+ ]);
+ expect(suggestions).to.have.lengthOf(4);
+ });
+
+ it("with namespace prefix", () => {
+ const xmlText = `
+
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ namespace: "http://namespace.com/1",
+ attributes: {},
+ elements: {
+ person: {
+ name: "person",
+ namespace: "http://namespace.com/2",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ cardinality: "single",
+ required: false,
+ name: "name",
+ namespace: "http://namespace.com/1",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ cardinality: "single",
+ required: false,
+ name: "age",
+ namespace: "http://namespace.com/1",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ cardinality: "many",
+ required: false,
+ name: "address",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([
+ {
+ label: "name",
+ text: "name",
+ },
+ {
+ label: "age",
+ text: "age",
+ },
+ ]);
+ expect(suggestions).to.have.lengthOf(2);
+ });
+
+ it("should not crash with invalid prefix", () => {
+ const xmlText = `
+
+ <:::⇶
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ namespace: "http://namespace.com/1",
+ attributes: {},
+ elements: {
+ person: {
+ name: "person",
+ namespace: "http://namespace.com/2",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ cardinality: "single",
+ required: false,
+ name: "name",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ cardinality: "single",
+ required: false,
+ name: "age",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ cardinality: "many",
+ required: false,
+ name: "address",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([]);
+ expect(suggestions).to.have.lengthOf(0);
+ });
+
+ it("should not provide extraneous namespace prefixes", () => {
+ const xmlText = `
+
+ <⇶
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ namespace: "http://namespace.com/1",
+ attributes: {},
+ elements: {
+ person: {
+ name: "person",
+ namespace: "http://namespace.com/2",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ cardinality: "single",
+ required: false,
+ name: "name",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ cardinality: "single",
+ required: false,
+ name: "age",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ cardinality: "many",
+ required: false,
+ name: "address",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([
+ {
+ label: "name",
+ text: "name",
+ },
+ {
+ label: "age",
+ text: "age",
+ },
+ {
+ label: "address",
+ text: "address",
+ },
+ ]);
+ expect(suggestions).to.have.lengthOf(3);
+ expect(suggestions).to.not.deep.include.members([
+ {
+ label: "abc",
+ text: "abc",
+ },
+ ]);
+ });
+
+ it("should not provide extraneous namespace prefixes for singular items", () => {
+ const xmlText = `
+
+
+ <⇶
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ namespace: "http://namespace.com/1",
+ attributes: {},
+ elements: {
+ person: {
+ name: "person",
+ namespace: "http://namespace.com/2",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ cardinality: "single",
+ required: false,
+ name: "name",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ cardinality: "single",
+ required: false,
+ name: "age",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ cardinality: "many",
+ required: false,
+ name: "address",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ friends: {
+ cardinality: "single",
+ required: false,
+ name: "friends",
+ namespace: "http://namespace.com/1",
+ attributes: {},
+ elements: {
+ person: {
+ name: "person",
+ namespace: "http://namespace.com/2",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ name: {
+ cardinality: "single",
+ required: false,
+ name: "name",
+ namespace: "http://namespace.com/2",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const suggestions = suggestionsBySchema(xmlText, schema);
+ expect(suggestions).to.deep.include.members([
+ {
+ label: "name",
+ text: "name",
+ },
+ {
+ label: "age",
+ text: "age",
+ },
+ {
+ label: "address",
+ text: "address",
+ },
+ ]);
+ expect(suggestions).to.have.lengthOf(3);
+ expect(suggestions).to.not.deep.include.members([
+ {
+ label: "abc",
+ text: "abc",
+ },
+ ]);
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/simple-schema/test/content-assist/utils.js b/projects/xml-tools/packages/simple-schema/test/content-assist/utils.js
new file mode 100644
index 00000000..0ebe17a9
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/test/content-assist/utils.js
@@ -0,0 +1,36 @@
+const { parse } = require("@xml-tools/parser");
+const { buildAst } = require("@xml-tools/ast");
+const { getSuggestions } = require("@xml-tools/content-assist");
+const { getSchemaSuggestionsProviders } = require("../../");
+
+/**
+ *
+ * @param {string} xmlText
+ * @param {SimpleSchema} schema
+ * @returns {ValidationIssue[] | *}
+ */
+function suggestionsBySchema(xmlText, schema) {
+ const schemaSuggestionsProviders = getSchemaSuggestionsProviders(schema);
+ const realXmlText = xmlText.replace("⇶", "");
+ const offset = xmlText.indexOf("⇶");
+ const { cst, tokenVector } = parse(realXmlText);
+ const ast = buildAst(cst, tokenVector);
+ const suggestions = getSuggestions({
+ cst,
+ tokenVector,
+ ast,
+ offset: offset,
+ providers: {
+ attributeValue: [
+ schemaSuggestionsProviders.schemaAttributeValueCompletion,
+ ],
+ attributeName: [schemaSuggestionsProviders.schemaAttributeNameCompletion],
+ elementName: [schemaSuggestionsProviders.schemaElementNameCompletion],
+ },
+ });
+ return suggestions;
+}
+
+module.exports = {
+ suggestionsBySchema: suggestionsBySchema,
+};
diff --git a/projects/xml-tools/packages/simple-schema/test/validators/attribute-value-spec.js b/projects/xml-tools/packages/simple-schema/test/validators/attribute-value-spec.js
new file mode 100644
index 00000000..91790eac
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/test/validators/attribute-value-spec.js
@@ -0,0 +1,250 @@
+const { expect } = require("chai");
+const { validateBySchema } = require("./utils");
+
+describe("The XML Simple Schema", () => {
+ describe("Validations Functionality", () => {
+ context("attributes values", () => {
+ context("enum values", () => {
+ it("positive", () => {
+ const xmlText = `
+ Bill
+ Tim
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "note",
+ attributes: {
+ private: {
+ required: false,
+ key: "private",
+ value: ["true", "false"],
+ },
+ },
+ elements: {},
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(1);
+ const issue = issues[0];
+ expect(issue.msg).to.eql(
+ "Expecting one of but found "
+ );
+ expect(issue.severity).to.eql("error");
+ expect(issue.node.key).to.eql("private");
+ expect(issue.position).to.eql({ startOffset: 14, endOffset: 23 });
+ });
+
+ it("negative", () => {
+ const xmlText = `
+
+ Bill
+ Tim
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "note",
+ attributes: {
+ private: {
+ required: false,
+ key: "private",
+ value: ["true", "false"],
+ },
+ },
+ elements: {},
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+ });
+
+ context("regexp value", () => {
+ it("positive", () => {
+ const xmlText = `
+ Bill
+ Tim
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "note",
+ attributeType: "open",
+ attributes: {
+ private: {
+ required: false,
+ key: "private",
+ value: /^(true|false)$/,
+ },
+ },
+ elements: {},
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(1);
+ const issue = issues[0];
+ expect(issue.msg).to.eql(
+ "Expecting Value matching ^(true|false)$/> but found "
+ );
+ expect(issue.severity).to.eql("error");
+ expect(issue.node.key).to.eql("private");
+ });
+
+ it("negative", () => {
+ const xmlText = `
+ Bill
+ Tim
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "note",
+ attributes: {
+ private: {
+ required: false,
+ key: "private",
+ value: /^(true|false)$/,
+ },
+ },
+ elements: {},
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+ });
+
+ context("won't validate edge cases", () => {
+ it("if there is no value in the XML Text", () => {
+ const xmlText = `
+ Bill
+ Tim
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "note",
+ attributes: {
+ private: {
+ required: false,
+ key: "private",
+ value: ["true", "false"],
+ },
+ },
+ elements: {},
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+
+ it("if there is no value definition in the XSS", () => {
+ const xmlText = `
+ Bill
+ Tim
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "note",
+ attributes: {
+ private: {
+ required: false,
+ key: "private",
+ },
+ },
+ elements: {},
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+
+ it("if there is no matching schema node", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ // Typo - can't find people.person path in the schema
+ OOPS: {
+ name: "OOPS",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ age: {
+ required: false,
+ key: "age",
+ value: "66",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+
+ it("if there is full schema mismatch", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ // "humans" !== "person" this schema is completely irrelevant for this scenario.
+ name: "humans",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ age: {
+ required: false,
+ key: "age",
+ value: "66",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+
+ it("on an xml prolog", () => {
+ // This test is only to ensure code coverage is special prolog branch.
+ // There is no way to actually test be behaivor directly as we don't actually do anything with
+ // a prolong (yet) in the XSS, and there is no way to define prolog XSS constraints.
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "person",
+ attributes: {},
+
+ elements: {},
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+ });
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/simple-schema/test/validators/element-duplicate-sub-elements-spec.js b/projects/xml-tools/packages/simple-schema/test/validators/element-duplicate-sub-elements-spec.js
new file mode 100644
index 00000000..a6ea77f4
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/test/validators/element-duplicate-sub-elements-spec.js
@@ -0,0 +1,189 @@
+const { expect } = require("chai");
+const { map, countBy, identity } = require("lodash");
+const { validateBySchema } = require("./utils");
+
+describe("The XML Simple Schema", () => {
+ describe("Validations Functionality", () => {
+ context("element duplicate sub elements", () => {
+ it("Detects a single duplicate sub element", () => {
+ const xmlText = `
+
+ Duck
+ Duck
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ lastName: {
+ lastName: "name",
+ cardinality: "single",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(2);
+ const issue1 = issues[0];
+ expect(issue1.msg).to.eql(
+ "Duplicate Sub-Element: only a single occurrence of this Sub-Element is allowed here."
+ );
+ expect(issue1.severity).to.eql("error");
+ expect(issue1.node.name).to.eql("lastName");
+ expect(issue1.position).to.eql({ startOffset: 55, endOffset: 62 });
+
+ const issue2 = issues[1];
+ expect(issue2.msg).to.eql(
+ "Duplicate Sub-Element: only a single occurrence of this Sub-Element is allowed here."
+ );
+ expect(issue2.severity).to.eql("error");
+ expect(issue2.node.name).to.eql("lastName");
+ expect(issue2.position).to.eql({ startOffset: 100, endOffset: 107 });
+ });
+
+ it("Detects multiple duplicate sub elements", () => {
+ const xmlText = `
+
+ Duck
+ Duck
+ Duck
+
+ NY
+ NY
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ lastName: {
+ name: "lastName",
+ cardinality: "single",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ name: "address",
+ cardinality: "single",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(5);
+ const messages = map(issues, "msg");
+ const countByMessages = countBy(messages, identity);
+ expect(countByMessages).to.eql({
+ "Duplicate Sub-Element: only a single occurrence of this Sub-Element is allowed here.": 3,
+ "Duplicate Sub-Element: only a single occurrence of this Sub-Element is allowed here.": 2,
+ });
+ });
+
+ it("Won't complain when no sub elements breach their cardinality constraints", () => {
+ const xmlText = `
+
+ Donald
+ 85
+ NY
+ IL
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ name: "name",
+ cardinality: "single",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ name: "age",
+ cardinality: "single",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ name: "address",
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+
+ it("Won't complain when no sub elements are defined", () => {
+ const xmlText = `
+
+ Donald
+ 85
+ NY
+ IL
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/simple-schema/test/validators/element-required-attributes-spec.js b/projects/xml-tools/packages/simple-schema/test/validators/element-required-attributes-spec.js
new file mode 100644
index 00000000..28845c97
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/test/validators/element-required-attributes-spec.js
@@ -0,0 +1,126 @@
+const { expect } = require("chai");
+const { map } = require("lodash");
+const { validateBySchema } = require("./utils");
+
+describe("The XML Simple Schema", () => {
+ describe("Validations Functionality", () => {
+ context("element required attribute", () => {
+ it("Detects a missing attribute", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ age: {
+ required: true,
+ key: "age",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(1);
+ const issue = issues[0];
+ expect(issue.msg).to.eql("Missing Required Attribute: ");
+ expect(issue.severity).to.eql("error");
+ expect(issue.node.name).to.eql("person");
+ expect(issue.position).to.eql({ startOffset: 27, endOffset: 32 });
+ });
+
+ it("Detects multiple missing attributes", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ age: {
+ required: true,
+ key: "age",
+ },
+ firstName: {
+ required: true,
+ key: "firstName",
+ },
+ lastName: {
+ required: true,
+ key: "lastName",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(3);
+ const messages = map(issues, "msg");
+ expect(messages).to.have.members([
+ "Missing Required Attribute: ",
+ "Missing Required Attribute: ",
+ "Missing Required Attribute: ",
+ ]);
+ });
+
+ it("Won't complain when all required attributes are present", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ age: {
+ required: true,
+ key: "age",
+ },
+ firstName: {
+ required: true,
+ key: "firstName",
+ },
+ lastName: {
+ required: true,
+ key: "lastName",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/simple-schema/test/validators/element-required-elements-spec.js b/projects/xml-tools/packages/simple-schema/test/validators/element-required-elements-spec.js
new file mode 100644
index 00000000..6204b9c1
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/test/validators/element-required-elements-spec.js
@@ -0,0 +1,140 @@
+const { expect } = require("chai");
+const { map } = require("lodash");
+const { validateBySchema } = require("./utils");
+
+describe("The XML Simple Schema", () => {
+ describe("Validations Functionality", () => {
+ context("element required sub elements", () => {
+ it("Detects a missing sub element", () => {
+ const xmlText = `
+
+ Duck
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ name: "name",
+ required: true,
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(1);
+ const issue = issues[0];
+ expect(issue.msg).to.eql("Missing Required Sub-Element: ");
+ expect(issue.severity).to.eql("error");
+ expect(issue.node.name).to.eql("person");
+ expect(issue.position).to.eql({ startOffset: 27, endOffset: 32 });
+ });
+
+ it("Detects multiple missing sub elements", () => {
+ const xmlText = `
+
+ Duck
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ name: "name",
+ required: true,
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ name: "age",
+ required: true,
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(2);
+ const messages = map(issues, "msg");
+ expect(messages).to.have.members([
+ "Missing Required Sub-Element: ",
+ "Missing Required Sub-Element: ",
+ ]);
+ });
+
+ it("Won't complain when all required sub-elements are present", () => {
+ const xmlText = `
+
+ Donald
+ 85
+ Duck
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elements: {
+ name: {
+ name: "name",
+ required: true,
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ name: "age",
+ required: true,
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/simple-schema/test/validators/element-unknown-attributes-spec.js b/projects/xml-tools/packages/simple-schema/test/validators/element-unknown-attributes-spec.js
new file mode 100644
index 00000000..582d91b2
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/test/validators/element-unknown-attributes-spec.js
@@ -0,0 +1,195 @@
+const { expect } = require("chai");
+const { map } = require("lodash");
+const { validateBySchema } = require("./utils");
+
+describe("The XML Simple Schema", () => {
+ describe("Validations Functionality", () => {
+ context("element unknown attribute", () => {
+ it("Detects a single unknown attribute", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ attributesType: "closed",
+ cardinality: "many",
+ attributes: {
+ age: {
+ key: "age",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(1);
+ const issue = issues[0];
+ expect(issue.msg).to.eql(
+ "Unknown Attribute: only [age] attributes are allowed"
+ );
+ expect(issue.severity).to.eql("error");
+ expect(issue.node.key).to.eql("name");
+ expect(issue.position).to.eql({ startOffset: 34, endOffset: 37 });
+ });
+
+ it("Detects multiple missing attributes", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ attributesType: "closed",
+ cardinality: "many",
+ attributes: {
+ age: {
+ key: "age",
+ },
+ firstName: {
+ key: "firstName",
+ },
+ lastName: {
+ key: "lastName",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(3);
+ const messages = map(issues, "msg");
+ expect(messages).to.have.members([
+ "Unknown Attribute: only [age,firstName,lastName] attributes are allowed",
+ "Unknown Attribute: only [age,firstName,lastName] attributes are allowed",
+ "Unknown Attribute: only [age,firstName,lastName] attributes are allowed",
+ ]);
+ });
+
+ it("Won't complain when all attributes are known", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributesType: "closed",
+ attributes: {
+ age: {
+ key: "age",
+ },
+ firstName: {
+ key: "firstName",
+ },
+ lastName: {
+ key: "lastName",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+
+ it("Default namespace attribute is always known", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ name: "people",
+ cardinality: "single",
+ attributesType: "closed",
+ attributes: {
+ description: {
+ key: "description",
+ required: false,
+ },
+ },
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ age: {
+ required: true,
+ key: "age",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(0);
+ });
+ it("Namespace attribute with prefix is always known", () => {
+ const xmlText = `
+
+ `;
+
+ const schema = {
+ required: true,
+ name: "people",
+ cardinality: "single",
+ attributesType: "closed",
+ attributes: {
+ description: {
+ key: "description",
+ required: false,
+ },
+ },
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {
+ age: {
+ required: true,
+ key: "age",
+ },
+ },
+ elements: {},
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(0);
+ });
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/simple-schema/test/validators/element-unknown-sub-elements-spec.js b/projects/xml-tools/packages/simple-schema/test/validators/element-unknown-sub-elements-spec.js
new file mode 100644
index 00000000..dabb397e
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/test/validators/element-unknown-sub-elements-spec.js
@@ -0,0 +1,193 @@
+const { expect } = require("chai");
+const { map } = require("lodash");
+const { validateBySchema } = require("./utils");
+
+describe("The XML Simple Schema", () => {
+ describe("Validations Functionality", () => {
+ context("element unknown sub elements", () => {
+ it("Detects a single unknown sub element", () => {
+ const xmlText = `
+
+ Duck
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ elementsType: "closed",
+ attributes: {},
+ elements: {
+ name: {
+ name: "name",
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(1);
+ const issue = issues[0];
+ expect(issue.msg).to.eql(
+ "Unknown Sub-Element: only [name] Sub-Elements are allowed"
+ );
+ expect(issue.severity).to.eql("error");
+ expect(issue.node.name).to.eql("lastName");
+ expect(issue.position).to.eql({ startOffset: 55, endOffset: 62 });
+ });
+
+ it("Detects multiple unknown sub elements", () => {
+ const xmlText = `
+
+ Duck
+ NY
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elementsType: "closed",
+ elements: {
+ name: {
+ name: "name",
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ name: "age",
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.have.lengthOf(2);
+ const messages = map(issues, "msg");
+ expect(messages).to.have.members([
+ "Unknown Sub-Element: only [name,age] Sub-Elements are allowed",
+ "Unknown Sub-Element: only [name,age] Sub-Elements are allowed",
+ ]);
+ });
+
+ it("Won't complain when all sub-elements are known", () => {
+ const xmlText = `
+
+ Donald
+ 85
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elementsType: "closed",
+ elements: {
+ name: {
+ name: "name",
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ name: "age",
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ name: "address",
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+
+ it("Won't complain for partial sub elements", () => {
+ const xmlText = `
+
+ <
+
+ `;
+
+ const schema = {
+ required: true,
+ cardinality: "single",
+ name: "people",
+ attributes: {},
+
+ elements: {
+ person: {
+ name: "person",
+ required: false,
+ cardinality: "many",
+ attributes: {},
+ elementsType: "closed",
+ elements: {
+ name: {
+ name: "name",
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ age: {
+ name: "age",
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ address: {
+ name: "address",
+ cardinality: "many",
+ attributes: {},
+ elements: {},
+ },
+ },
+ },
+ },
+ };
+ const issues = validateBySchema(xmlText, schema);
+ expect(issues).to.be.empty;
+ });
+ });
+ });
+});
diff --git a/projects/xml-tools/packages/simple-schema/test/validators/utils.js b/projects/xml-tools/packages/simple-schema/test/validators/utils.js
new file mode 100644
index 00000000..d680a6ce
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/test/validators/utils.js
@@ -0,0 +1,28 @@
+const { parse } = require("@xml-tools/parser");
+const { buildAst } = require("@xml-tools/ast");
+const { validate } = require("@xml-tools/validation");
+const { getSchemaValidators } = require("../../");
+
+/**
+ *
+ * @param {string} xmlText
+ * @param {SimpleSchema} schema
+ * @returns {ValidationIssue[] | *}
+ */
+function validateBySchema(xmlText, schema) {
+ const { cst, tokenVector } = parse(xmlText);
+ const xmlDoc = buildAst(cst, tokenVector);
+ const schemaValidators = getSchemaValidators(schema);
+ const issues = validate({
+ doc: xmlDoc,
+ validators: {
+ attribute: [schemaValidators.attribute],
+ element: [schemaValidators.element],
+ },
+ });
+ return issues;
+}
+
+module.exports = {
+ validateBySchema: validateBySchema,
+};
diff --git a/projects/xml-tools/packages/simple-schema/tsconfig.json b/projects/xml-tools/packages/simple-schema/tsconfig.json
new file mode 100644
index 00000000..a4ed07be
--- /dev/null
+++ b/projects/xml-tools/packages/simple-schema/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": ["./api.d.ts"]
+}
diff --git a/projects/xml-tools/packages/validation/CHANGELOG.old.md b/projects/xml-tools/packages/validation/CHANGELOG.old.md
new file mode 100644
index 00000000..0ab80e99
--- /dev/null
+++ b/projects/xml-tools/packages/validation/CHANGELOG.old.md
@@ -0,0 +1,135 @@
+> **Historic changelog.** This records changes made while the project lived in the
+> standalone [SAP/xml-tools](https://github.com/SAP/xml-tools) repository, before it was
+> migrated into [SAP/app-studio-toolkit](https://github.com/SAP/app-studio-toolkit).
+> Newer entries are generated by Changesets in `CHANGELOG.md`.
+
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [1.1.0](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.16...@xml-tools/validation@1.1.0) (2026-02-11)
+
+### Features
+
+- update lodash dependency to version 4.17.23 ([#461](https://github.com/sap/xml-tools/issues/461)) ([23a319d](https://github.com/sap/xml-tools/commit/23a319d6ed42a4a3885ec5748eb127147a8960bc))
+
+## [1.0.16](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.15...@xml-tools/validation@1.0.16) (2021-06-03)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.15](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.14...@xml-tools/validation@1.0.15) (2021-02-09)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.14](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.13...@xml-tools/validation@1.0.14) (2021-01-21)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.13](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.12...@xml-tools/validation@1.0.13) (2021-01-20)
+
+### Bug Fixes
+
+- missing lib folder in npm packages ([ffed3c2](https://github.com/sap/xml-tools/commit/ffed3c2c54c70aea8b9ded0d53786382bc190cc5))
+
+## [1.0.12](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.11...@xml-tools/validation@1.0.12) (2021-01-20)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.11](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.10...@xml-tools/validation@1.0.11) (2020-06-29)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.10](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.9...@xml-tools/validation@1.0.10) (2020-06-18)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.9](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.8...@xml-tools/validation@1.0.9) (2020-05-17)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.8](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.7...@xml-tools/validation@1.0.8) (2020-05-05)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.7](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.6...@xml-tools/validation@1.0.7) (2020-04-02)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.6](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.5...@xml-tools/validation@1.0.6) (2020-02-19)
+
+### Bug Fixes
+
+- add npmignore file to each package ([5bbf209](https://github.com/sap/xml-tools/commit/5bbf209))
+
+## [1.0.5](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.4...@xml-tools/validation@1.0.5) (2020-02-11)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.4](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.2...@xml-tools/validation@1.0.4) (2020-02-10)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.3](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.2...@xml-tools/validation@1.0.3) (2020-02-07)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.2](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.1...@xml-tools/validation@1.0.2) (2020-01-28)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [1.0.1](https://github.com/sap/xml-tools/compare/@xml-tools/validation@1.0.0...@xml-tools/validation@1.0.1) (2020-01-20)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+# [1.0.0](https://github.com/sap/xml-tools/compare/@xml-tools/validation@0.3.8...@xml-tools/validation@1.0.0) (2019-12-08)
+
+### Features
+
+- **ast:** attributesRange and guessedAttributeRange support ([8ce840d](https://github.com/sap/xml-tools/commit/8ce840d)), closes [#51](https://github.com/sap/xml-tools/issues/51)
+
+### BREAKING CHANGES
+
+- **ast:** buildAst now requires a tokenVector argument
+
+## [0.3.8](https://github.com/sap/xml-tools/compare/@xml-tools/validation@0.3.7...@xml-tools/validation@0.3.8) (2019-12-05)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [0.3.7](https://github.com/sap/xml-tools/compare/@xml-tools/validation@0.3.6...@xml-tools/validation@0.3.7) (2019-12-03)
+
+### Bug Fixes
+
+- api type definitions ([46d6c2c](https://github.com/sap/xml-tools/commit/46d6c2c))
+
+## [0.3.6](https://github.com/sap/xml-tools/compare/@xml-tools/validation@0.3.5...@xml-tools/validation@0.3.6) (2019-11-20)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [0.3.5](https://github.com/sap/xml-tools/compare/@xml-tools/validation@0.3.4...@xml-tools/validation@0.3.5) (2019-11-12)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [0.3.4](https://github.com/sap/xml-tools/compare/@xml-tools/validation@0.3.3...@xml-tools/validation@0.3.4) (2019-11-11)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [0.3.3](https://github.com/sap/xml-tools/compare/@xml-tools/validation@0.3.2...@xml-tools/validation@0.3.3) (2019-11-11)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [0.3.2](https://github.com/sap/xml-tools/compare/@xml-tools/validation@0.3.1...@xml-tools/validation@0.3.2) (2019-11-06)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+## [0.3.1](https://github.com/sap/xml-tools/compare/@xml-tools/validation@0.3.0...@xml-tools/validation@0.3.1) (2019-11-06)
+
+**Note:** Version bump only for package @xml-tools/validation
+
+# 0.3.0 (2019-10-28)
+
+### Features
+
+- move to lerna independent mode ([6c347a8](https://github.com/sap/xml-tools/commit/6c347a8)), closes [#18](https://github.com/sap/xml-tools/issues/18)
+
+# 0.2.0 (2019-10-28)
diff --git a/projects/xml-tools/packages/validation/README.md b/projects/xml-tools/packages/validation/README.md
new file mode 100644
index 00000000..2ac2de50
--- /dev/null
+++ b/projects/xml-tools/packages/validation/README.md
@@ -0,0 +1,70 @@
+[](https://www.npmjs.com/package/@xml-tools/validation)
+
+# @xml-tools/validation
+
+APIs which assist in implementing Validations / Diagnostics logic for XML.
+
+## Installation
+
+With npm:
+
+- `npm install @xml-tools/validation`
+
+With Yarn
+
+- `yarn add @xml-tools/validation`
+
+## Usage
+
+Please see the [TypeScript Definitions](./api.d.ts) for full API details.
+
+A simple usage example:
+
+```javascript
+const { parse } = require("xml-tools/parser");
+const { buildAst } = require("xml-tools/ast");
+const { validate } = require("xml-tools/validation");
+
+const xmlText = `
+ Bill
+
+`;
+
+const { cst, tokenVector } = parse(xmlText);
+const xmlDocAst = buildAst(cst, tokenVector);
+const issues = validate({
+ doc: xmlDocAst,
+ validators: {
+ element: [
+ (node) => {
+ if (node.name === "note") {
+ const hasFrom = node.subElements.find(
+ (subNode) => subNode.name === "from"
+ );
+ if (hasFrom === undefined) {
+ return [
+ {
+ msg: "A Note Element **must** have a `from` subElement",
+ node: node,
+ },
+ ];
+ }
+ }
+ return [];
+ },
+ ],
+ },
+});
+
+console.log(issues[0].msg); // -> "A Note Element **must** have a `from` subElement"
+// Issue position can be extracted from the relevant ASTNode.
+console.log(issues[0].node.position.endLine); // -> 3
+```
+
+## Support
+
+Please report issues [here](https://github.com/SAP/app-studio-toolkit/issues/new/choose) and label them with `project:xml-tools`.
+
+## Contributing
+
+See [CONTRIBUTING.md](https://github.com/SAP/app-studio-toolkit/blob/main/CONTRIBUTING.md).
diff --git a/projects/xml-tools/packages/validation/api.d.ts b/projects/xml-tools/packages/validation/api.d.ts
new file mode 100644
index 00000000..2575f631
--- /dev/null
+++ b/projects/xml-tools/packages/validation/api.d.ts
@@ -0,0 +1,36 @@
+import {
+ XMLAstNode,
+ XMLAttribute,
+ XMLElement,
+ XMLDocument,
+} from "@xml-tools/ast";
+
+declare type AttributeValidator = (node: XMLAttribute) => ValidationIssue[];
+declare type ElementValidator = (node: XMLElement) => ValidationIssue[];
+declare function validate(options: {
+ doc: XMLDocument;
+ validators: {
+ attribute?: AttributeValidator[];
+ element?: ElementValidator[];
+ };
+}): ValidationIssue[];
+
+interface ValidationIssue {
+ msg: string;
+ /**
+ * ASTNode where the issue occurred.
+ */
+ node: XMLAstNode;
+ /**
+ * The Severity is optional as that may need to be determined outside
+ * the validation logic, For example in an IDE's inspections configuration.
+ */
+ severity?: "info" | "warning" | "error";
+
+ /**
+ * Optional property to indicate a more specific error location.
+ * For example: For Missing Attribute error in an element we may want to
+ * only mark the Element **Name** area instead of the whole element.
+ */
+ position?: { startOffset: number; endOffset: number };
+}
diff --git a/projects/xml-tools/packages/validation/lib/api.js b/projects/xml-tools/packages/validation/lib/api.js
new file mode 100644
index 00000000..d09bfa2e
--- /dev/null
+++ b/projects/xml-tools/packages/validation/lib/api.js
@@ -0,0 +1,5 @@
+const { validate } = require("./validate");
+
+module.exports = {
+ validate: validate,
+};
diff --git a/projects/xml-tools/packages/validation/lib/validate.js b/projects/xml-tools/packages/validation/lib/validate.js
new file mode 100644
index 00000000..5887ae45
--- /dev/null
+++ b/projects/xml-tools/packages/validation/lib/validate.js
@@ -0,0 +1,37 @@
+const { accept } = require("@xml-tools/ast");
+const { defaultsDeep, flatMap } = require("lodash");
+
+function validate(options) {
+ const actualOptions = defaultsDeep(options, {
+ validators: {
+ attribute: [],
+ element: [],
+ },
+ });
+
+ let issues = [];
+
+ const validateVisitor = {
+ visitXMLElement: function (node) {
+ const newIssues = flatMap(actualOptions.validators.element, (validator) =>
+ validator(node)
+ );
+ issues = issues.concat(newIssues);
+ },
+ visitXMLAttribute: function (node) {
+ const newIssues = flatMap(
+ actualOptions.validators.attribute,
+ (validator) => validator(node)
+ );
+ issues = issues.concat(newIssues);
+ },
+ };
+
+ accept(actualOptions.doc, validateVisitor);
+
+ return issues;
+}
+
+module.exports = {
+ validate: validate,
+};
diff --git a/projects/xml-tools/packages/validation/nyc.config.js b/projects/xml-tools/packages/validation/nyc.config.js
new file mode 100644
index 00000000..3e3f8962
--- /dev/null
+++ b/projects/xml-tools/packages/validation/nyc.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ // xml-tools sources live in lib/ (not src/), so this local config overrides
+ // the repo-root nyc.config.js include of **/src/** (which would report 0%).
+ include: ["lib/**/*.js"],
+ reporter: ["text", "lcov"],
+ all: true,
+ branches: 100,
+ lines: 100,
+ functions: 100,
+ statements: 100,
+};
diff --git a/projects/xml-tools/packages/validation/package.json b/projects/xml-tools/packages/validation/package.json
new file mode 100644
index 00000000..29579502
--- /dev/null
+++ b/projects/xml-tools/packages/validation/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "@xml-tools/validation",
+ "version": "1.1.0",
+ "description": "XML validation APIs",
+ "keywords": [
+ "xml",
+ "validation"
+ ],
+ "main": "lib/api.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/SAP/app-studio-toolkit.git",
+ "directory": "projects/xml-tools/packages/validation"
+ },
+ "license": "Apache-2.0",
+ "typings": "./api.d.ts",
+ "files": [
+ "lib",
+ "api.d.ts",
+ "CHANGELOG.md",
+ "CHANGELOG.old.md"
+ ],
+ "dependencies": {
+ "@xml-tools/ast": "workspace:*",
+ "lodash": "4.17.23"
+ },
+ "devDependencies": {
+ "@xml-tools/parser": "workspace:*"
+ },
+ "scripts": {
+ "ci": "npm-run-all clean type-check coverage:*",
+ "clean": "rimraf ./coverage ./nyc_output",
+ "test": "mocha \"./test/**/*spec.js\"",
+ "coverage:run": "nyc mocha \"./test/**/*spec.js\"",
+ "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100",
+ "type-check": "tsc"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "bugs": {
+ "url": "https://github.com/SAP/app-studio-toolkit/issues"
+ }
+}
diff --git a/projects/xml-tools/packages/validation/test/validate-apis-spec.js b/projects/xml-tools/packages/validation/test/validate-apis-spec.js
new file mode 100644
index 00000000..39e716d2
--- /dev/null
+++ b/projects/xml-tools/packages/validation/test/validate-apis-spec.js
@@ -0,0 +1,62 @@
+const { expect } = require("chai");
+const { map } = require("lodash");
+const { parse } = require("@xml-tools/parser");
+const { buildAst } = require("@xml-tools/ast");
+const { validate } = require("../");
+
+describe("The XML Validations APIs", () => {
+ it("can validate attributes", () => {
+ const xmlText = `
+ Bill
+ Tim
+ `;
+
+ const ast = xmlTextToAst(xmlText);
+
+ const issues = validate({
+ doc: ast,
+ validators: {
+ element: [
+ (node) => {
+ return [{ msg: node.name }];
+ },
+ ],
+ },
+ });
+
+ const msgNodeNames = map(issues, (_) => _.msg);
+ expect(msgNodeNames).to.have.lengthOf(3);
+ expect(msgNodeNames).to.have.members(["note", "to", "from"]);
+ });
+
+ it("can validate elements", () => {
+ const xmlText = `
+ Bill
+ Gates
+ Tim
+ `;
+
+ const ast = xmlTextToAst(xmlText);
+
+ const issues = validate({
+ doc: ast,
+ validators: {
+ attribute: [
+ (node) => {
+ return [{ msg: node.key }];
+ },
+ ],
+ },
+ });
+
+ const msgNodeNames = map(issues, (_) => _.msg);
+ expect(msgNodeNames).to.have.lengthOf(2);
+ expect(msgNodeNames).to.have.members(["clearance", "hidden"]);
+ });
+});
+
+function xmlTextToAst(text) {
+ const { cst, tokenVector } = parse(text);
+ const ast = buildAst(cst, tokenVector);
+ return ast;
+}
diff --git a/projects/xml-tools/packages/validation/tsconfig.json b/projects/xml-tools/packages/validation/tsconfig.json
new file mode 100644
index 00000000..a4ed07be
--- /dev/null
+++ b/projects/xml-tools/packages/validation/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": ["./api.d.ts"]
+}
diff --git a/projects/xml-tools/packages/xml-toolkit/.gitignore b/projects/xml-tools/packages/xml-toolkit/.gitignore
new file mode 100644
index 00000000..905fe8fb
--- /dev/null
+++ b/projects/xml-tools/packages/xml-toolkit/.gitignore
@@ -0,0 +1,10 @@
+# These files are copied/generated from the monorepo root during VSIX packaging.
+/LICENSE
+/LICENSES
+/NOTICE
+
+# Build output
+/dist
+*.vsix
+
+.cache
diff --git a/projects/xml-tools/packages/xml-toolkit/.vscodeignore b/projects/xml-tools/packages/xml-toolkit/.vscodeignore
new file mode 100644
index 00000000..26b68004
--- /dev/null
+++ b/projects/xml-tools/packages/xml-toolkit/.vscodeignore
@@ -0,0 +1,27 @@
+
+# We are bundling our extension so no need to include the dependencies separately.
+node_modules
+
+# Except the language-server package, which runs as a separate process and is
+# loaded by path at runtime. Include only what that needs: its package.json (for
+# resolution), its lib/ (api.js computes the server path), and its bundled dist/.
+!**/@xml-tools/language-server/package.json
+!**/@xml-tools/language-server/lib/**
+!**/@xml-tools/language-server/dist/**
+
+# Exclude the language-server's own dev/meta files that the broad rules above pull back in.
+**/@xml-tools/language-server/.gitignore
+**/@xml-tools/language-server/*.md
+**/@xml-tools/language-server/nyc.config.js
+**/@xml-tools/language-server/tsconfig.json
+**/@xml-tools/language-server/webpack.config.js
+**/@xml-tools/language-server/api.d.ts
+
+# The VSCode Extension's dev artifacts
+coverage/**
+.nyc_output/**
+test/**
+scripts
+
+# Loaded via gitcdn.xyz in the README, avoid packaging to reduce .vsix size
+resources/readme/**
\ No newline at end of file
diff --git a/projects/xml-tools/packages/xml-toolkit/CHANGELOG.old.md b/projects/xml-tools/packages/xml-toolkit/CHANGELOG.old.md
new file mode 100644
index 00000000..647cac7d
--- /dev/null
+++ b/projects/xml-tools/packages/xml-toolkit/CHANGELOG.old.md
@@ -0,0 +1,101 @@
+> **Historic changelog.** This records changes made while the project lived in the
+> standalone [SAP/xml-tools](https://github.com/SAP/xml-tools) repository, before it was
+> migrated into [SAP/app-studio-toolkit](https://github.com/SAP/app-studio-toolkit).
+> Newer entries are generated by Changesets in `CHANGELOG.md`.
+
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [1.3.0](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/compare/xml-toolkit@1.2.1...xml-toolkit@1.3.0) (2026-02-11)
+
+### Features
+
+- update lodash dependency to version 4.17.23 ([#461](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/issues/461)) ([23a319d](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/commit/23a319d6ed42a4a3885ec5748eb127147a8960bc))
+
+## [1.2.1](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/compare/xml-toolkit@1.2.0...xml-toolkit@1.2.1) (2025-07-15)
+
+### Bug Fixes
+
+- updating xml-toolkit README.md file ([#458](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/issues/458)) ([2283302](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/commit/2283302fae161106299179e5aa8822a9bc7010bc))
+
+# [1.2.0](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/compare/xml-toolkit@1.1.1...xml-toolkit@1.2.0) (2025-07-14)
+
+### Features
+
+- new xml-toolkit logo ([#454](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/issues/454)) ([4a1888f](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/commit/4a1888fb5128a8373d6e16de5db7e97ba6577311))
+
+## [1.1.1](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/compare/xml-toolkit@1.1.0...xml-toolkit@1.1.1) (2021-06-03)
+
+**Note:** Version bump only for package xml-toolkit
+
+# [1.1.0](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/compare/xml-toolkit@1.0.5...xml-toolkit@1.1.0) (2021-02-09)
+
+### Bug Fixes
+
+- use extension name as diagnostic source ([#297](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/issues/297)) ([f6d7dc4](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/commit/f6d7dc42a05c4e1d7c9ab3f1fbe35ecb1eb820fa))
+
+### Features
+
+- well formed constraints validations ([#293](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/issues/293)) ([8a9d535](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/commit/8a9d535e95a52b9dfc1068d2e203b09bd08e1066)), closes [#13](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/issues/13) [#14](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/issues/14)
+
+## [1.0.5](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/compare/xml-toolkit@1.0.4...xml-toolkit@1.0.5) (2021-01-20)
+
+**Note:** Version bump only for package xml-toolkit
+
+## [1.0.4](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/compare/xml-toolkit@1.0.3...xml-toolkit@1.0.4) (2021-01-20)
+
+**Note:** Version bump only for package xml-toolkit
+
+## [1.0.3](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/compare/xml-toolkit@1.0.2...xml-toolkit@1.0.3) (2020-06-29)
+
+**Note:** Version bump only for package xml-toolkit
+
+## [1.0.2](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/compare/xml-toolkit@1.0.1...xml-toolkit@1.0.2) (2020-06-18)
+
+**Note:** Version bump only for package xml-toolkit
+
+## [1.0.1](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/compare/xml-toolkit@1.0.0...xml-toolkit@1.0.1) (2020-05-26)
+
+**Note:** Version bump only for package xml-toolkit
+
+# [1.0.0](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/compare/xml-toolkit@0.1.7...xml-toolkit@1.0.0) (2020-05-25)
+
+### chore
+
+- **xml-toolkit:** first major version ([9e1f5a9](https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit/commit/9e1f5a9))
+
+### BREAKING CHANGES
+
+- **xml-toolkit:** First Major Version
+
+## 0.1.7 (2020-05-21)
+
+**Note:** Version bump only for package xml-toolkit
+
+## [0.1.6](https://github.com/SAP/xml-tools/compare/xml-for-vscode@0.1.5...xml-for-vscode@0.1.6) (2020-05-17)
+
+### Bug Fixes
+
+- **xml-for-vscode:** incorrect vscode engine version ([88adfa3](https://github.com/SAP/xml-tools/commit/88adfa3))
+
+## [0.1.5](https://github.com/SAP/xml-tools/compare/xml-for-vscode@0.1.4...xml-for-vscode@0.1.5) (2020-05-17)
+
+**Note:** Version bump only for package xml-for-vscode
+
+## [0.1.4](https://github.com/SAP/xml-tools/compare/xml-for-vscode@0.1.3...xml-for-vscode@0.1.4) (2020-05-05)
+
+**Note:** Version bump only for package xml-for-vscode
+
+## [0.1.3](https://github.com/SAP/xml-tools/compare/xml-for-vscode@0.1.2...xml-for-vscode@0.1.3) (2020-04-02)
+
+**Note:** Version bump only for package xml-for-vscode
+
+## [0.1.2](https://github.com/SAP/xml-tools/compare/xml-for-vscode@0.1.1...xml-for-vscode@0.1.2) (2020-03-08)
+
+**Note:** Version bump only for package xml-for-vscode
+
+## 0.1.1 (2020-03-08)
+
+**Note:** Version bump only for package xml-for-vscode
diff --git a/projects/xml-tools/packages/xml-toolkit/README.md b/projects/xml-tools/packages/xml-toolkit/README.md
new file mode 100644
index 00000000..bbb972c7
--- /dev/null
+++ b/projects/xml-tools/packages/xml-toolkit/README.md
@@ -0,0 +1,48 @@
+# XML Toolkit
+
+The XML toolkit is a VSCode extension that provides XML language editor support.
+
+## Features
+
+### XML syntax validations
+
+The syntax validations use a fault-tolerant parser. This enables
+the detection of **multiple** syntax errors.
+
+### XML Well-formed validations
+
+- Unique Attribute Names.
+- Tag open/close names are aligned.
+
+#### Preview
+
+
+
+## Installation
+
+### From the VS Code Marketplace
+
+In the [XML Toolkit](https://marketplace.visualstudio.com/items?itemName=SAPOSS.xml-toolkit)
+page, click **Install**.
+
+### From Github releases
+
+1. Go to [GitHub Releases](https://github.com/SAP/app-studio-toolkit/releases).
+2. Search for the `.vsix` archive under `xml-toolkit\@x.y.z` releases.
+ - Replace `x.y.z` with the desired version number.
+3. Follow the instructions for installing an extension from a `.vsix`
+ file in the [VSCode's guide](https://code.visualstudio.com/docs/editor/extension-gallery#_install-from-a-vsix).
+
+## Usage
+
+The extension is activated when an `.xml` file is opened.
+Both lexing and parsing errors will be shown as squiggly red underlines
+and in the Problems view.
+
+## Support
+
+Please report issues [here](https://github.com/SAP/app-studio-toolkit/issues/new/choose) and label them with `project:xml-tools`.
+
+## Contributing
+
+See [CONTRIBUTING.md](https://github.com/SAP/app-studio-toolkit/blob/main/CONTRIBUTING.md).
diff --git a/projects/xml-tools/packages/xml-toolkit/lib/extension.js b/projects/xml-tools/packages/xml-toolkit/lib/extension.js
new file mode 100644
index 00000000..b75797f1
--- /dev/null
+++ b/projects/xml-tools/packages/xml-toolkit/lib/extension.js
@@ -0,0 +1,64 @@
+const { readFile: readFileCallback } = require("fs");
+const { resolve } = require("path");
+const { promisify } = require("util");
+const { workspace } = require("vscode");
+const { LanguageClient, TransportKind } = require("vscode-languageclient");
+const { SERVER_PATH } = require("@xml-tools/language-server");
+
+const readFile = promisify(readFileCallback);
+
+let client;
+
+async function activate(context) {
+ const debugOptions = { execArgv: ["--nolazy", "--inspect=6009"] };
+
+ const serverOptions = {
+ run: { module: SERVER_PATH, transport: TransportKind.ipc },
+ debug: {
+ module: SERVER_PATH,
+ transport: TransportKind.ipc,
+ options: debugOptions,
+ },
+ };
+
+ const meta = JSON.parse(
+ await readFile(resolve(context.extensionPath, "package.json"), "utf8")
+ );
+
+ /**
+ * @type {import("@xml-tools/language-server).ServerInitializationOptions}
+ */
+ const initializationOptions = {
+ consumer: meta.displayName,
+ };
+
+ let clientOptions = {
+ documentSelector: [{ scheme: "file", language: "xml" }],
+ synchronize: {
+ fileEvents: workspace.createFileSystemWatcher("**/*.xml"),
+ },
+ initializationOptions: initializationOptions,
+ };
+
+ client = new LanguageClient(
+ "XMLforVSCode",
+ "XML For VSCode",
+ serverOptions,
+ clientOptions
+ );
+
+ client.start();
+}
+
+function deactivate() {
+ /* istanbul ignore if - scenario that cannot be reached in productive code */
+ if (!client) {
+ return undefined;
+ }
+ return client.stop();
+}
+
+module.exports = {
+ activate: activate,
+ deactivate: deactivate,
+};
diff --git a/projects/xml-tools/packages/xml-toolkit/nyc.config.js b/projects/xml-tools/packages/xml-toolkit/nyc.config.js
new file mode 100644
index 00000000..3e3f8962
--- /dev/null
+++ b/projects/xml-tools/packages/xml-toolkit/nyc.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ // xml-tools sources live in lib/ (not src/), so this local config overrides
+ // the repo-root nyc.config.js include of **/src/** (which would report 0%).
+ include: ["lib/**/*.js"],
+ reporter: ["text", "lcov"],
+ all: true,
+ branches: 100,
+ lines: 100,
+ functions: 100,
+ statements: 100,
+};
diff --git a/projects/xml-tools/packages/xml-toolkit/package.json b/projects/xml-tools/packages/xml-toolkit/package.json
new file mode 100644
index 00000000..3d8d6e53
--- /dev/null
+++ b/projects/xml-tools/packages/xml-toolkit/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "xml-toolkit",
+ "version": "1.3.0",
+ "private": true,
+ "displayName": "XML Toolkit",
+ "description": "Language Support for XML",
+ "publisher": "SAPOSS",
+ "icon": "resources/xml-toolkit.png",
+ "keywords": [
+ "xml"
+ ],
+ "categories": [
+ "Programming Languages"
+ ],
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/SAP/app-studio-toolkit.git",
+ "directory": "projects/xml-tools/packages/xml-toolkit"
+ },
+ "engines": {
+ "vscode": "^1.56.0"
+ },
+ "activationEvents": [
+ "onLanguage:xml"
+ ],
+ "main": "./lib/extension",
+ "scripts": {
+ "ci": "npm-run-all clean bundle package",
+ "clean": "rimraf ./dist ./coverage ./nyc_output *.vsix NOTICE LICENSE",
+ "test": "node test/runTest.js",
+ "coverage:run": "nyc node test/runTest.js",
+ "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100",
+ "bundle": "webpack --mode production",
+ "package": "node ./scripts/package-vsix"
+ },
+ "dependencies": {
+ "@xml-tools/language-server": "workspace:*",
+ "vscode-languageclient": "6.1.3"
+ },
+ "//@types/vscode": "Pinned to the minimum VS Code API version supported here (kept from the standalone xml-tools repo). Intentionally not raised to the monorepo root version, which would allow newer APIs and break support for older VS Code.",
+ "devDependencies": {
+ "@types/vscode": "1.56.0",
+ "deep-equal-in-any-order": "1.0.28",
+ "lodash": "4.17.23",
+ "proxyquire": "2.1.3",
+ "vsce": "1.84.0",
+ "@vscode/test-electron": "^2.4.0"
+ },
+ "bugs": {
+ "url": "https://github.com/SAP/app-studio-toolkit/issues"
+ }
+}
diff --git a/projects/xml-tools/packages/xml-toolkit/resources/readme/preview-syntax-validation.png b/projects/xml-tools/packages/xml-toolkit/resources/readme/preview-syntax-validation.png
new file mode 100644
index 00000000..cf2a261f
Binary files /dev/null and b/projects/xml-tools/packages/xml-toolkit/resources/readme/preview-syntax-validation.png differ
diff --git a/projects/xml-tools/packages/xml-toolkit/resources/xml-toolkit.png b/projects/xml-tools/packages/xml-toolkit/resources/xml-toolkit.png
new file mode 100644
index 00000000..88252978
Binary files /dev/null and b/projects/xml-tools/packages/xml-toolkit/resources/xml-toolkit.png differ
diff --git a/projects/xml-tools/packages/xml-toolkit/scripts/package-vsix.js b/projects/xml-tools/packages/xml-toolkit/scripts/package-vsix.js
new file mode 100644
index 00000000..d8a9d1aa
--- /dev/null
+++ b/projects/xml-tools/packages/xml-toolkit/scripts/package-vsix.js
@@ -0,0 +1,94 @@
+/* istanbul ignore file */
+/**
+ * Workaround to: https://github.com/microsoft/vscode-vsce/issues/300
+ *
+ * The extension is webpack-bundled, but it loads the language server in a
+ * separate process from `@xml-tools/language-server`'s bundled `dist/server.js`
+ * (webpack marks that package as `external`). So the VSIX must include the
+ * language-server package's `dist/` alongside the extension bundle.
+ *
+ * vsce derives the files to package from the package manager's dependency
+ * listing, which does not work reliably here (pnpm's symlinked node_modules).
+ * We therefore hot-patch vsce's dependency resolution to include exactly the
+ * extension root plus the resolved language-server package directory.
+ *
+ * Migrated from the standalone SAP/xml-tools repo, which relied on yarn's
+ * `nohoist`. Under pnpm we resolve the sibling package via `require.resolve`
+ * instead, which follows pnpm's symlinks correctly.
+ */
+const proxyquire = require("proxyquire");
+const { resolve } = require("path");
+const {
+ readFileSync,
+ writeFileSync,
+ copyFileSync,
+ rmSync,
+ cpSync,
+} = require("fs");
+
+const rootExtDir = resolve(__dirname, "..");
+
+// Resolve the language-server package via the extension's OWN node_modules
+// symlink (pnpm creates node_modules/@xml-tools/language-server -> the sibling
+// package). We deliberately use this symlink path rather than its realpath:
+// vsce rejects file paths that escape the extension root ("../language-server/..."),
+// so the package must appear as a CHILD of the extension dir.
+const langServerDir = resolve(
+ rootExtDir,
+ "node_modules",
+ "@xml-tools",
+ "language-server"
+);
+
+// **Hot-Patching** vsce using proxyquire so it packages exactly these dirs.
+const getDepsStub = {
+ getDependencies: async () => [rootExtDir, langServerDir],
+};
+const { packageCommand } = proxyquire("vsce/out/package", {
+ "./npm": getDepsStub,
+});
+
+const pkgJsonPath = resolve(rootExtDir, "package.json");
+// Read & save the original literal representation of the pkg.json
+// to avoid dealing with re-formatting (prettier) later on.
+const pkgJsonOrgStr = readFileSync(pkgJsonPath, "utf8");
+const pkgJson = JSON.parse(pkgJsonOrgStr);
+
+// Ensure License / copyright files are part of the packaged .vsix.
+// Done BEFORE mutating package.json so a copy failure leaves pkg.json pristine.
+// Sourced from the monorepo root (five levels up: scripts -> xml-toolkit ->
+// packages -> xml-tools -> projects -> repo root).
+const rootMonoRepoDir = resolve(__dirname, "..", "..", "..", "..", "..");
+copyFileSync(
+ resolve(rootMonoRepoDir, "LICENSE"),
+ resolve(rootExtDir, "LICENSE")
+);
+
+const licensesDirExtPath = resolve(rootExtDir, "LICENSES");
+rmSync(licensesDirExtPath, { recursive: true, force: true });
+cpSync(resolve(rootMonoRepoDir, "LICENSES"), licensesDirExtPath, {
+ recursive: true,
+});
+
+// During development flows the `main` points to the compiled sources for fast
+// dev feedback loops; for the packaged .vsix it must point to the bundle.
+pkgJson.main = "./dist/extension";
+writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
+
+packageCommand({
+ cwd: rootExtDir,
+ packagePath: undefined,
+ baseContentUrl: undefined,
+ baseImagesUrl: undefined,
+ useYarn: false,
+ ignoreFile: undefined,
+ expandGitHubIssueLinks: undefined,
+})
+ .catch((e) => {
+ console.error(e.message);
+ process.exitCode = 666;
+ })
+ .finally(() => {
+ // revert changes to the pkg.json, ensure clean git working directory
+ writeFileSync(pkgJsonPath, pkgJsonOrgStr);
+ });
diff --git a/projects/xml-tools/packages/xml-toolkit/test/runTest.js b/projects/xml-tools/packages/xml-toolkit/test/runTest.js
new file mode 100644
index 00000000..a5e047c9
--- /dev/null
+++ b/projects/xml-tools/packages/xml-toolkit/test/runTest.js
@@ -0,0 +1,42 @@
+const { resolve } = require("path");
+const { runTests } = require("@vscode/test-electron");
+
+async function main() {
+ try {
+ // NODE_OPTIONS set by nyc for coverage instrumentation are not supported
+ // in Electron/VS Code packaged apps and cause the test runner to fail.
+ // Clear them before launching VS Code to avoid this issue.
+ delete process.env.NODE_OPTIONS;
+
+ // The path to the extension test environment
+ // Passed to --extensionTestsEnv
+ const extensionTestsEnv = {
+ path: resolve(__dirname, "..", "..", "test", "suite", "textFixure"),
+ };
+
+ // The folder containing the Extension Manifest package.json
+ // Passed to `--extensionDevelopmentPath`
+ const extensionDevelopmentPath = resolve(__dirname, "..");
+
+ // The path to the extension test script
+ // Passed to --extensionTestsPath
+ const extensionTestsPath = resolve(__dirname, "suite", "index");
+
+ // Download VS Code, unzip it and run the integration test
+ await runTests({
+ extensionTestsEnv,
+ extensionDevelopmentPath,
+ extensionTestsPath,
+ // Required for headless CI environments (Jenkins, etc.) where no
+ // display server is available. Without these flags Electron refuses
+ // to start and the test run fails immediately.
+ launchArgs: ["--headless", "--disable-gpu", "--no-sandbox"],
+ });
+ } catch (err) {
+ // Log the underlying error so CI logs actually show what went wrong.
+ console.error("Failed to run tests:", err);
+ process.exit(1);
+ }
+}
+
+main();
diff --git a/projects/xml-tools/packages/xml-toolkit/test/suite/extension.test.js b/projects/xml-tools/packages/xml-toolkit/test/suite/extension.test.js
new file mode 100644
index 00000000..fd9617fb
--- /dev/null
+++ b/projects/xml-tools/packages/xml-toolkit/test/suite/extension.test.js
@@ -0,0 +1,129 @@
+const { resolve } = require("path");
+const vscode = require("vscode");
+const fs = require("fs").promises;
+const { deactivate } = require("../../lib/extension");
+const deepEqualInAnyOrder = require("deep-equal-in-any-order");
+const chai = require("chai");
+const { expect } = chai;
+chai.use(deepEqualInAnyOrder);
+
+const docPath = resolve(
+ __dirname,
+ "..",
+ "..",
+ "test",
+ "testFixure",
+ "test.xml"
+);
+const docUri = vscode.Uri.file(docPath);
+
+describe("XML for VSCode extension", () => {
+ before(async function () {
+ await vscode.workspace.openTextDocument(docUri);
+ await vscode.window.showTextDocument(docUri);
+ // We need to explicitly wait for extension to load
+ await sleep(1000);
+ });
+
+ after(async function () {
+ await setContent("");
+ const extensionStatus = await deactivate();
+ expect(extensionStatus).to.equal(undefined);
+ });
+
+ it("Will show lexing error in problems view", async function () {
+ const content = `
+
+ Tove
+ Jani
+ Reminder
+ Don't forget me this weekend!
+ `;
+ await setContent(content);
+ await testDiagnostics(docUri, [
+ {
+ severity: vscode.DiagnosticSeverity.Error,
+ message:
+ "unexpected character: ->!<- at offset: 12, skipped 1 characters.",
+ range: toRange(1, 11, 1, 12),
+ source: "XML Toolkit",
+ },
+ ]);
+ });
+
+ it("Will show parsing errors in problems view", async function () {
+ const content = `
+
+ Tove
+ Jani
+ Reminder
+ Don't forget me this weekend!
+ STRING <-- but found --> '>' <--",
+ range: toRange(2, 14, 2, 14),
+ source: "XML Toolkit",
+ },
+ {
+ severity: vscode.DiagnosticSeverity.Error,
+ message: "Expecting token of type --> CLOSE <-- but found --> '' <--",
+ range: toRange(6, null, 0, 0),
+ source: "XML Toolkit",
+ },
+ ]);
+ });
+
+ it("Will show multiple errors in problems view", async function () {
+ const content = `
+
+ Tove
+ Jani
+ Reminder
+ Don't forget me this weekend!
+ `;
+ await setContent(content);
+ await testDiagnostics(docUri, [
+ {
+ severity: vscode.DiagnosticSeverity.Error,
+ message: "Expecting token of type --> STRING <-- but found --> '>' <--",
+ range: toRange(2, 14, 2, 14),
+ source: "XML Toolkit",
+ },
+ {
+ severity: vscode.DiagnosticSeverity.Error,
+ message: "Expecting token of type --> STRING <-- but found --> '>' <--",
+ range: toRange(3, 16, 3, 16),
+ source: "XML Toolkit",
+ },
+ {
+ severity: vscode.DiagnosticSeverity.Error,
+ message: "Expecting token of type --> STRING <-- but found --> '>' <--",
+ range: toRange(4, 19, 4, 19),
+ source: "XML Toolkit",
+ },
+ ]);
+ });
+
+ function toRange(sLine, sChar, eLine, eChar) {
+ const start = new vscode.Position(sLine, sChar);
+ const end = new vscode.Position(eLine, eChar);
+ return new vscode.Range(start, end);
+ }
+
+ async function setContent(content) {
+ await fs.writeFile(docPath, content);
+ await sleep(1000);
+ }
+
+ async function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
+
+ async function testDiagnostics(docUri, expectedDiagnostics) {
+ const actualDiagnostics = vscode.languages.getDiagnostics(docUri);
+ expect(expectedDiagnostics).to.deep.equalInAnyOrder(actualDiagnostics);
+ }
+});
diff --git a/projects/xml-tools/packages/xml-toolkit/test/suite/index.js b/projects/xml-tools/packages/xml-toolkit/test/suite/index.js
new file mode 100644
index 00000000..1b4b20d0
--- /dev/null
+++ b/projects/xml-tools/packages/xml-toolkit/test/suite/index.js
@@ -0,0 +1,43 @@
+const { resolve } = require("path");
+const Mocha = require("mocha");
+const glob = require("glob");
+
+function run() {
+ // Create the mocha test
+ const mocha = new Mocha({
+ ui: "bdd",
+ timeout: 20000,
+ color: true,
+ });
+
+ const testsRoot = resolve(__dirname);
+
+ return new Promise((c, e) => {
+ glob("**.test.js", { cwd: testsRoot }, (err, files) => {
+ if (err) {
+ return e(err);
+ }
+
+ // Add files to the test suite
+ files.forEach((f) => mocha.addFile(resolve(testsRoot, f)));
+
+ try {
+ // Run the mocha test
+ mocha.run((failures) => {
+ if (failures > 0) {
+ e(new Error(`${failures} tests failed.`));
+ } else {
+ c();
+ }
+ });
+ } catch (err) {
+ console.error(err);
+ e(err);
+ }
+ });
+ });
+}
+
+module.exports = {
+ run: run,
+};
diff --git a/projects/xml-tools/packages/xml-toolkit/test/testFixure/test.xml b/projects/xml-tools/packages/xml-toolkit/test/testFixure/test.xml
new file mode 100644
index 00000000..e69de29b
diff --git a/projects/xml-tools/packages/xml-toolkit/webpack.config.js b/projects/xml-tools/packages/xml-toolkit/webpack.config.js
new file mode 100644
index 00000000..6f272ea7
--- /dev/null
+++ b/projects/xml-tools/packages/xml-toolkit/webpack.config.js
@@ -0,0 +1,23 @@
+const path = require("path");
+const baseConfig = require("../../../../webpack.config.vscode.base");
+
+const config = {
+ ...baseConfig,
+ entry: "./lib/extension.js",
+ output: {
+ // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
+ path: path.resolve(__dirname, "dist"),
+ filename: "extension.js",
+ libraryTarget: "commonjs2",
+ devtoolModuleFilenameTemplate: "../[resource-path]",
+ },
+ // 📖 -> https://webpack.js.org/configuration/externals/
+ externals: {
+ // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed.
+ vscode: "commonjs vscode",
+ // the language-server must be bundled separately as it is executed in a separate process (by path).
+ "@xml-tools/language-server": "commonjs @xml-tools/language-server",
+ },
+};
+
+module.exports = config;