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 @@ +[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](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 @@ +[![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/ast-position.svg)](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 @@ +[![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/ast.svg)](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 @@ +[![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/common.svg)](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 @@ +[![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/constraints.svg)](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 @@ +[![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/content-assist.svg)](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 @@ +[![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/language-server.svg)](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 @@ +[![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/parser.svg)](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+;|&#x[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: /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: //, 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: 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 @@ + 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: 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: 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: 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: 73, + endOffset: 73, + tokenType: "CLOSE", + }, + ], + }, + location: { startOffset: 61, endOffset: 73 }, + }, + ], + }, + location: { startOffset: 29, endOffset: 74 }, + }, + ], + SLASH_OPEN: [ + { + 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: 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: 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: 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: 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: 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: 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: 44, + endOffset: 44, + tokenType: "CLOSE", + }, + ], + }, + location: { startOffset: 29, endOffset: 44 }, + }, + ], + }, + location: { startOffset: 6, endOffset: 45 }, + }, + ], + SLASH_OPEN: [ + { + 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: 47, + endOffset: 47, + tokenType: "CLOSE", + }, + ], + }, + location: { startOffset: 32, endOffset: 47 }, + }, + ], + }, + location: { startOffset: 6, endOffset: 48 }, + }, + ], + SLASH_OPEN: [ + { + 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: 93, + endOffset: 93, + tokenType: "CLOSE", + }, + ], + }, + location: { startOffset: 81, endOffset: 93 }, + }, + ], + }, + location: { startOffset: 39, endOffset: 94 }, + }, + ], + SLASH_OPEN: [ + { + 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: 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: 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: 132, + endOffset: 132, + tokenType: "CLOSE", + }, + ], + }, + location: { startOffset: 110, endOffset: 132 }, + }, + ], + }, + location: { startOffset: 47, endOffset: 133 }, + }, + ], + SLASH_OPEN: [ + { + 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: 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: 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 @@ +[![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/simple-schema.svg)](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 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 @@ +[![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/validation.svg)](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 = ` + + 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 + +![](./resources/readme/preview-syntax-validation.png) + +## 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;