├── .gitattributes ├── packages ├── language-server │ ├── .gitignore │ ├── lib │ │ ├── constants.js │ │ ├── api.js │ │ ├── server.js │ │ └── language-services.js │ ├── .npmignore │ ├── nyc.config.js │ ├── webpack.config.js │ ├── api.d.ts │ ├── README.md │ ├── package.json │ └── CHANGELOG.md ├── xml-toolkit │ ├── test │ │ ├── testFixure │ │ │ └── test.xml │ │ ├── runTest.js │ │ └── suite │ │ │ └── index.js │ ├── .gitignore │ ├── resources │ │ ├── xml-toolkit.png │ │ └── readme │ │ │ └── preview-syntax-validation.png │ ├── .vscodeignore │ ├── webpack.config.js │ ├── README.md │ ├── package.json │ └── lib │ │ └── extension.js ├── ast │ ├── lib │ │ ├── constants.js │ │ ├── api.js │ │ ├── utils.js │ │ └── visit-ast.js │ ├── test │ │ ├── snapshots │ │ │ ├── valid │ │ │ │ ├── complex-contents │ │ │ │ │ ├── input.xml │ │ │ │ │ ├── sample.spec.js │ │ │ │ │ └── output.js │ │ │ │ ├── attributes │ │ │ │ │ ├── input.xml │ │ │ │ │ ├── sample.spec.js │ │ │ │ │ └── output.js │ │ │ │ ├── prolog │ │ │ │ │ ├── input.xml │ │ │ │ │ ├── sample.spec.js │ │ │ │ │ └── output.js │ │ │ │ ├── elements │ │ │ │ │ ├── input.xml │ │ │ │ │ ├── sample.spec.js │ │ │ │ │ └── output.js │ │ │ │ ├── elements-short-form │ │ │ │ │ ├── input.xml │ │ │ │ │ ├── sample.spec.js │ │ │ │ │ └── output.js │ │ │ │ ├── prolog-no-attriibs │ │ │ │ │ ├── input.xml │ │ │ │ │ ├── sample.spec.js │ │ │ │ │ └── output.js │ │ │ │ └── namespaces │ │ │ │ │ ├── sample.spec.js │ │ │ │ │ └── input.xml │ │ │ └── invalid │ │ │ │ ├── attributes-no-value-not-closed │ │ │ │ ├── input.xml │ │ │ │ ├── sample.spec.js │ │ │ │ └── output.js │ │ │ │ ├── attributes-no-value-last │ │ │ │ ├── input.xml │ │ │ │ ├── sample.spec.js │ │ │ │ └── output.js │ │ │ │ ├── attributes-no-value-middle │ │ │ │ ├── input.xml │ │ │ │ ├── sample.spec.js │ │ │ │ └── output.js │ │ │ │ ├── missing-top-level-element │ │ │ │ ├── input.xml │ │ │ │ ├── sample.spec.js │ │ │ │ └── output.js │ │ │ │ ├── attributes-no-value-no-eql-last │ │ │ │ ├── input.xml │ │ │ │ ├── sample.spec.js │ │ │ │ └── output.js │ │ │ │ ├── attributes-no-value-no-eql-middle │ │ │ │ ├── input.xml │ │ │ │ ├── sample.spec.js │ │ │ │ └── output.js │ │ │ │ ├── element-name-last │ │ │ │ ├── input.xml │ │ │ │ ├── sample.spec.js │ │ │ │ └── output.js │ │ │ │ ├── element-name-empty-last │ │ │ │ ├── input.xml │ │ │ │ ├── sample.spec.js │ │ │ │ └── output.js │ │ │ │ ├── element-name-empty-middle │ │ │ │ ├── input.xml │ │ │ │ ├── sample.spec.js │ │ │ │ └── output.js │ │ │ │ ├── element-name-middle │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ │ │ ├── element-name-empty-middle-comment │ │ │ │ ├── input.xml │ │ │ │ ├── sample.spec.js │ │ │ │ └── output.js │ │ │ │ ├── bad-namespace │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ │ │ └── attributes-no-value-not-closed-nested │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ ├── sample-test.js │ │ ├── utils.js │ │ └── visitor │ │ │ └── visitor-spec.js │ ├── .npmignore │ ├── CONTRIBUTING.md │ ├── package.json │ ├── scripts │ │ └── update-snapshots.js │ └── README.md ├── parser │ ├── test │ │ ├── snapshots │ │ │ ├── invalid │ │ │ │ ├── element-closing-name │ │ │ │ │ ├── input.xml │ │ │ │ │ ├── sample.spec.js │ │ │ │ │ └── output.js │ │ │ │ └── attributes │ │ │ │ │ ├── input.xml │ │ │ │ │ └── sample.spec.js │ │ │ └── valid │ │ │ │ ├── complex-contents │ │ │ │ ├── input.xml │ │ │ │ ├── sample.spec.js │ │ │ │ └── output.js │ │ │ │ ├── attributes │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ │ │ ├── prolog │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ │ │ ├── doc-type-system │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ │ │ ├── elements │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ │ │ ├── cdata │ │ │ │ ├── input.xml │ │ │ │ ├── sample.spec.js │ │ │ │ └── output.js │ │ │ │ ├── elements-short-form │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ │ │ ├── entity-char-ref │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ │ │ ├── doc-type-public │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ │ │ ├── comments │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ │ │ ├── namespaces │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ │ │ ├── processing-instruction │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ │ │ └── multi-line-comments │ │ │ │ ├── input.xml │ │ │ │ └── sample.spec.js │ │ ├── sample-test.js │ │ └── utils.js │ ├── .npmignore │ ├── CONTRIBUTING.md │ ├── lib │ │ └── api.js │ ├── package.json │ ├── scripts │ │ └── update-snapshots.js │ └── README.md ├── validation │ ├── lib │ │ ├── api.js │ │ └── validate.js │ ├── .npmignore │ ├── CONTRIBUTING.md │ ├── api.d.ts │ ├── package.json │ ├── test │ │ └── validate-apis-spec.js │ └── README.md ├── common │ ├── .npmignore │ ├── CONTRIBUTING.md │ ├── lib │ │ ├── api.js │ │ ├── find-next-textual-token.js │ │ └── xml-ns-key.js │ ├── README.md │ ├── test │ │ └── find-next-textual-token-spec.js │ ├── package.json │ ├── api.d.ts │ └── CHANGELOG.md ├── constraints │ ├── .npmignore │ ├── CONTRIBUTING.md │ ├── api.d.ts │ ├── lib │ │ ├── api.js │ │ └── constraints │ │ │ ├── unique-attribute-keys.js │ │ │ └── tag-closing-name-match.js │ ├── CHANGELOG.md │ ├── package.json │ ├── test │ │ ├── api-spec.js │ │ └── constraints │ │ │ ├── unique-attribute-keys-spec.js │ │ │ └── tag-closing-name-match-spec.js │ └── README.md ├── content-assist │ ├── .npmignore │ ├── CONTRIBUTING.md │ ├── lib │ │ └── api.js │ ├── package.json │ └── README.md ├── simple-schema │ ├── .npmignore │ ├── CONTRIBUTING.md │ ├── lib │ │ ├── validators │ │ │ ├── utils.js │ │ │ ├── required-attributes.js │ │ │ ├── required-sub-elements.js │ │ │ ├── unknown-sub-elements.js │ │ │ ├── duplicate-sub-elements.js │ │ │ ├── unknown-attributes.js │ │ │ └── attribute-value.js │ │ ├── api.js │ │ ├── content-assist │ │ │ ├── attribute-name.js │ │ │ ├── attribute-value.js │ │ │ └── element-name.js │ │ ├── path.js │ │ ├── get-content-assist.js │ │ └── get-validators.js │ ├── test │ │ ├── validators │ │ │ └── utils.js │ │ └── content-assist │ │ │ └── utils.js │ ├── package.json │ ├── api.d.ts │ └── README.md └── ast-position │ ├── CONTRIBUTING.md │ ├── lib │ ├── api.js │ └── ast-position.js │ ├── api.d.ts │ ├── README.md │ ├── package.json │ └── CHANGELOG.md ├── .github └── dependabot.yml ├── .reuse └── dep5 ├── lerna.json ├── .eslintrc.js ├── webpack.config.base.js ├── scripts ├── merge-coverage.js └── trigger-release.js ├── .gitignore ├── package.json └── .circleci └── config.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /packages/language-server/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | -------------------------------------------------------------------------------- /packages/xml-toolkit/test/testFixure/test.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ast/lib/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | DEFAULT_NS: "::DEFAULT", 3 | }; 4 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/complex-contents/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/invalid/element-closing-name/input.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/attributes/input.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/invalid/attributes/input.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/complex-contents/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/attributes/input.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-not-closed/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/prolog/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-last/input.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-middle/input.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/missing-top-level-element/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/elements/input.xml: -------------------------------------------------------------------------------- 1 | 2 | Bill 3 | Tim 4 | 5 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/doc-type-system/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-last/input.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-middle/input.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/elements/input.xml: -------------------------------------------------------------------------------- 1 | 2 | Bill 3 | Tim 4 | 5 | -------------------------------------------------------------------------------- /packages/xml-toolkit/.gitignore: -------------------------------------------------------------------------------- 1 | # These files will be copied from the root package during bundling. 2 | /LICENSE 3 | 4 | .cache -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/cdata/input.xml: -------------------------------------------------------------------------------- 1 | 2 | Hello, world!]]> 3 | 4 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/elements-short-form/input.xml: -------------------------------------------------------------------------------- 1 | 2 | Bill 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/elements-short-form/input.xml: -------------------------------------------------------------------------------- 1 | 2 | Bill 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/entity-char-ref/input.xml: -------------------------------------------------------------------------------- 1 | 2 | &to; 3 | Ӓ 4 | Tim 5 | 6 | -------------------------------------------------------------------------------- /packages/validation/lib/api.js: -------------------------------------------------------------------------------- 1 | const { validate } = require("./validate"); 2 | 3 | module.exports = { 4 | validate: validate, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/xml-toolkit/resources/xml-toolkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/xml-tools/HEAD/packages/xml-toolkit/resources/xml-toolkit.png -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-last/input.xml: -------------------------------------------------------------------------------- 1 | 2 | bobi 3 | john 4 | 6 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/doc-type-public/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-empty-last/input.xml: -------------------------------------------------------------------------------- 1 | 2 | bobi 3 | john 4 | < 5 | 6 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-empty-middle/input.xml: -------------------------------------------------------------------------------- 1 | 2 | bobi 3 | < 4 | john 5 | 6 | -------------------------------------------------------------------------------- /packages/language-server/lib/constants.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_CONSUMER_NAME = "XML LS"; 2 | 3 | module.exports = { 4 | DEFAULT_CONSUMER_NAME, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/ast/.npmignore: -------------------------------------------------------------------------------- 1 | # NYC 2 | .nyc_output 3 | nyc.config.js 4 | coverage 5 | 6 | # tests 7 | test 8 | 9 | # contributing 10 | CONTRIBUTING.md -------------------------------------------------------------------------------- /packages/common/.npmignore: -------------------------------------------------------------------------------- 1 | # NYC 2 | .nyc_output 3 | nyc.config.js 4 | coverage 5 | 6 | # tests 7 | test 8 | 9 | # contributing 10 | CONTRIBUTING.md -------------------------------------------------------------------------------- /packages/parser/.npmignore: -------------------------------------------------------------------------------- 1 | # NYC 2 | .nyc_output 3 | nyc.config.js 4 | coverage 5 | 6 | # tests 7 | test 8 | 9 | # contributing 10 | CONTRIBUTING.md -------------------------------------------------------------------------------- /packages/constraints/.npmignore: -------------------------------------------------------------------------------- 1 | # NYC 2 | .nyc_output 3 | nyc.config.js 4 | coverage 5 | 6 | # tests 7 | test 8 | 9 | # contributing 10 | CONTRIBUTING.md -------------------------------------------------------------------------------- /packages/validation/.npmignore: -------------------------------------------------------------------------------- 1 | # NYC 2 | .nyc_output 3 | nyc.config.js 4 | coverage 5 | 6 | # tests 7 | test 8 | 9 | # contributing 10 | CONTRIBUTING.md -------------------------------------------------------------------------------- /packages/content-assist/.npmignore: -------------------------------------------------------------------------------- 1 | # NYC 2 | .nyc_output 3 | nyc.config.js 4 | coverage 5 | 6 | # tests 7 | test 8 | 9 | # contributing 10 | CONTRIBUTING.md -------------------------------------------------------------------------------- /packages/language-server/.npmignore: -------------------------------------------------------------------------------- 1 | # NYC 2 | .nyc_output 3 | nyc.config.js 4 | coverage 5 | 6 | # tests 7 | test 8 | 9 | # contributing 10 | CONTRIBUTING.md -------------------------------------------------------------------------------- /packages/simple-schema/.npmignore: -------------------------------------------------------------------------------- 1 | # NYC 2 | .nyc_output 3 | nyc.config.js 4 | coverage 5 | 6 | # tests 7 | test 8 | 9 | # contributing 10 | CONTRIBUTING.md -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-middle/input.xml: -------------------------------------------------------------------------------- 1 | 2 | bobi 3 | john 5 | ny 6 | 7 | -------------------------------------------------------------------------------- /packages/xml-toolkit/resources/readme/preview-syntax-validation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SAP/xml-tools/HEAD/packages/xml-toolkit/resources/readme/preview-syntax-validation.png -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/prolog-no-attriibs/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bill 4 | hello 5 | Tim 6 | world 7 | 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/comments/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bill 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/ast/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This package does not have any unique development flows. 4 | Please see the top level [Contribution Guide](../../CONTRIBUTING.md). 5 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-empty-middle-comment/input.xml: -------------------------------------------------------------------------------- 1 | 2 | bobi 3 | < 4 | 5 | john 6 | 7 | -------------------------------------------------------------------------------- /packages/common/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This package does not have any unique development flows. 4 | Please see the top level [Contribution Guide](../../CONTRIBUTING.md). 5 | -------------------------------------------------------------------------------- /packages/parser/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This package does not have any unique development flows. 4 | Please see the top level [Contribution Guide](../../CONTRIBUTING.md). 5 | -------------------------------------------------------------------------------- /packages/ast-position/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This package does not have any unique development flows. 4 | Please see the top level [Contribution Guide](../../CONTRIBUTING.md). 5 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/bad-namespace/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Some Chair 4 | 50 5 | 67 6 | 7 | -------------------------------------------------------------------------------- /packages/constraints/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This package does not have any unique development flows. 4 | Please see the top level [Contribution Guide](../../CONTRIBUTING.md). 5 | -------------------------------------------------------------------------------- /packages/simple-schema/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This package does not have any unique development flows. 4 | Please see the top level [Contribution Guide](../../CONTRIBUTING.md). 5 | -------------------------------------------------------------------------------- /packages/validation/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This package does not have any unique development flows. 4 | Please see the top level [Contribution Guide](../../CONTRIBUTING.md). 5 | -------------------------------------------------------------------------------- /packages/content-assist/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This package does not have any unique development flows. 4 | Please see the top level [Contribution Guide](../../CONTRIBUTING.md). 5 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-not-closed-nested/input.xml: -------------------------------------------------------------------------------- 1 | 2 | hello world 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/namespaces/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Some Chair 4 | 50 5 | 67 6 | 7 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/processing-instruction/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/multi-line-comments/input.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | Bill 9 | 10 | 14 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/attributes/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/elements/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/namespaces/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/prolog/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/cdata/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/prolog/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/attributes/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/comments/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/elements/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/namespaces/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/bad-namespace/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/complex-contents/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/elements-short-form/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/prolog-no-attriibs/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/invalid/attributes/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/complex-contents/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/doc-type-public/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/doc-type-system/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/entity-char-ref/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-last/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-middle/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/elements-short-form/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/multi-line-comments/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/processing-instruction/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-last/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-empty-last/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/invalid/element-closing-name/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/lib/api.js: -------------------------------------------------------------------------------- 1 | const { buildAst } = require("./build-ast"); 2 | const { accept } = require("./visit-ast"); 3 | const { DEFAULT_NS } = require("./constants"); 4 | 5 | module.exports = { 6 | buildAst: buildAst, 7 | accept: accept, 8 | DEFAULT_NS: DEFAULT_NS, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-middle/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-empty-middle/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/missing-top-level-element/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-last/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-not-closed/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-empty-middle-comment/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/validators/utils.js: -------------------------------------------------------------------------------- 1 | const { pick } = require("lodash"); 2 | 3 | function tokenToOffsetPosition(token) { 4 | return pick(token, ["startOffset", "endOffset"]); 5 | } 6 | 7 | module.exports = { 8 | tokenToOffsetPosition: tokenToOffsetPosition, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-not-closed-nested/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | describe(`${testNameFromDir(__dirname)}`, () => { 6 | executeInValidSampleTest(__dirname); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/namespaces/input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Some Chair 4 | 50 5 | 67 6 | Awsome Chair 7 | 8 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/api.js: -------------------------------------------------------------------------------- 1 | const { getSchemaValidators } = require("./get-validators"); 2 | const { getSchemaSuggestionsProviders } = require("./get-content-assist"); 3 | 4 | module.exports = { 5 | getSchemaValidators: getSchemaValidators, 6 | getSchemaSuggestionsProviders: getSchemaSuggestionsProviders, 7 | }; 8 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: xml-tools 3 | Upstream-Contact: Shachar Soel 4 | Source: https://github.com/SAP/xml-tools 5 | 6 | Files: * 7 | Copyright: 2019-2024 SAP SE or an SAP affiliate company and XML Tools contributors 8 | License: Apache-2.0 9 | -------------------------------------------------------------------------------- /packages/constraints/api.d.ts: -------------------------------------------------------------------------------- 1 | import { ValidationIssue } from "@xml-tools/validation"; 2 | 3 | /** 4 | * detects XML constraints issues (e.g non well-formed XML) 5 | * - https://www.w3.org/TR/xml/#sec-starttags 6 | * - see: "Well-formedness constraint: Unique Att Spec". 7 | */ 8 | export function checkConstraints(ast: XMLDocument): ValidationIssue[]; 9 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "useWorkspaces": true, 4 | "npmClient": "yarn", 5 | "version": "independent", 6 | "command": { 7 | "publish": { 8 | "conventionalCommits": true 9 | }, 10 | "version": { 11 | "allowBranch": "master", 12 | "message": "chore(release): release" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-middle/sample.spec.js: -------------------------------------------------------------------------------- 1 | const { 2 | executeInValidSampleTest, 3 | testNameFromDir, 4 | } = require("../../../sample-test"); 5 | 6 | // TODO: should we disable single token deletion in this case? 7 | describe(`${testNameFromDir(__dirname)}`, () => { 8 | executeInValidSampleTest(__dirname); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/common/lib/api.js: -------------------------------------------------------------------------------- 1 | const { findNextTextualToken } = require("./find-next-textual-token"); 2 | const { 3 | isXMLNamespaceKey, 4 | getXMLNamespaceKeyPrefix, 5 | } = require("./xml-ns-key.js"); 6 | 7 | module.exports = { 8 | findNextTextualToken: findNextTextualToken, 9 | isXMLNamespaceKey: isXMLNamespaceKey, 10 | getXMLNamespaceKeyPrefix: getXMLNamespaceKeyPrefix, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/language-server/nyc.config.js: -------------------------------------------------------------------------------- 1 | // TODO: extract to top level nyc config. 2 | module.exports = { 3 | include: ["lib/**/*.js"], 4 | // Not yet sure how to implement an Integration test for the LSP Server 5 | exclude: ["lib/server.js"], 6 | reporter: ["text", "lcov"], 7 | "check-coverage": true, 8 | all: true, 9 | branches: 100, 10 | lines: 100, 11 | functions: 100, 12 | statements: 100, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/language-server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const baseConfig = require("../../webpack.config.base"); 3 | 4 | const config = { 5 | ...baseConfig, 6 | entry: "./lib/server.js", 7 | output: { 8 | path: path.resolve(__dirname, "dist"), 9 | filename: "server.js", 10 | libraryTarget: "commonjs2", 11 | devtoolModuleFilenameTemplate: "../[resource-path]", 12 | }, 13 | }; 14 | module.exports = config; 15 | -------------------------------------------------------------------------------- /packages/language-server/api.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Absolute path to the server's "main" module 3 | * This is useful when launching the server in a separate process (e.g via spawn). 4 | */ 5 | export const SERVER_PATH: string; 6 | 7 | export type ServerInitializationOptions = { 8 | /** 9 | * optional name of the IDE plugin/extension using this language server. 10 | * This name would be used in the `source` property of dignostic issues. 11 | */ 12 | consumer?: string; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/ast-position/lib/api.js: -------------------------------------------------------------------------------- 1 | const { AstPositionVisitor } = require("./ast-position"); 2 | const { accept } = require("@xml-tools/ast"); 3 | 4 | /** 5 | * @param {XMLDocument} xmlDoc 6 | * @param {number} offset 7 | * @returns {AstPosition | undefined} 8 | */ 9 | function astPositionAtOffset(xmlDoc, offset) { 10 | const positionVisitor = new AstPositionVisitor(offset); 11 | accept(xmlDoc, positionVisitor); 12 | return positionVisitor.position; 13 | } 14 | 15 | module.exports = { 16 | astPositionAtOffset, 17 | }; 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Common settings for JS Files. 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:eslint-comments/recommended", 6 | // Disables all formatting related rules as formatting is handled by prettier, not eslint. 7 | "prettier", 8 | ], 9 | parserOptions: { 10 | // Targeting modern nodejs versions 11 | // Consult with: https://kangax.github.io/compat-table/es2016plus/ 12 | ecmaVersion: 2018, 13 | }, 14 | env: { 15 | commonjs: true, 16 | mocha: true, 17 | node: true, 18 | es6: true, 19 | }, 20 | rules: { 21 | "eslint-comments/require-description": ["error", { ignore: [] }], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/xml-toolkit/.vscodeignore: -------------------------------------------------------------------------------- 1 | 2 | # We are bundling our extension so no need to include the dependencies separately. 3 | node_modules 4 | 5 | # Except a **subset** language-server package which is executed as a separate process 6 | # Note only one star (*) is expended in these dirs. 7 | !**/@xml-tools/language-server/* 8 | !**/@xml-tools/language-server/dist/* 9 | 10 | # For debugging of bundled language-server in production (source maps) 11 | !**/@xml-tools/language-server/lib/** 12 | 13 | # The VSCode Extension's dev artifacts 14 | coverage/** 15 | .nyc_output/** 16 | test/** 17 | scripts 18 | 19 | # Loaded via gitcdn.xyz in the README, avoid packaging to reduce .vsix size 20 | resources/readme/** -------------------------------------------------------------------------------- /packages/constraints/lib/api.js: -------------------------------------------------------------------------------- 1 | const { validate } = require("@xml-tools/validation"); 2 | const { 3 | validateUniqueAttributeKeys, 4 | } = require("./constraints/unique-attribute-keys"); 5 | const { 6 | validateTagClosingNameMatch, 7 | } = require("./constraints/tag-closing-name-match"); 8 | 9 | /** 10 | * @param {XMLDocument} ast 11 | * @returns {ValidationIssue[]} 12 | */ 13 | function checkConstraints(ast) { 14 | const constraintIssues = validate({ 15 | doc: ast, 16 | validators: { 17 | element: [validateTagClosingNameMatch, validateUniqueAttributeKeys], 18 | }, 19 | }); 20 | 21 | return constraintIssues; 22 | } 23 | 24 | module.exports = { 25 | checkConstraints, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/ast/lib/utils.js: -------------------------------------------------------------------------------- 1 | const { reduce, has, isArray } = require("lodash"); 2 | 3 | function getAstChildrenReflective(astParent) { 4 | const astChildren = reduce( 5 | astParent, 6 | (result, prop, name) => { 7 | if (name === "parent") { 8 | // parent property is never a child... 9 | } else if (has(prop, "type")) { 10 | result.push(prop); 11 | } else if (isArray(prop) && prop.length > 0 && has(prop[0], "type")) { 12 | result = result.concat(prop); 13 | } 14 | 15 | return result; 16 | }, 17 | [] 18 | ); 19 | 20 | return astChildren; 21 | } 22 | 23 | module.exports = { 24 | getAstChildrenReflective: getAstChildrenReflective, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/parser/lib/api.js: -------------------------------------------------------------------------------- 1 | const { xmlLexer } = require("./lexer"); 2 | const { xmlParser } = require("./parser"); 3 | 4 | module.exports = { 5 | parse: function parse(text) { 6 | const lexResult = xmlLexer.tokenize(text); 7 | // setting a new input will RESET the parser instance's state. 8 | xmlParser.input = lexResult.tokens; 9 | // any top level rule may be used as an entry point 10 | const cst = xmlParser.document(); 11 | 12 | return { 13 | cst: cst, 14 | tokenVector: lexResult.tokens, 15 | lexErrors: lexResult.errors, 16 | parseErrors: xmlParser.errors, 17 | }; 18 | }, 19 | 20 | BaseXmlCstVisitor: xmlParser.getBaseCstVisitorConstructor(), 21 | }; 22 | -------------------------------------------------------------------------------- /packages/constraints/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [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) 7 | 8 | **Note:** Version bump only for package @xml-tools/constraints 9 | 10 | # 1.1.0 (2021-02-09) 11 | 12 | ### Features 13 | 14 | - 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) 15 | -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const config = { 4 | optimization: { 5 | // The Default minimization options cause JavaScript runtime errors. 6 | // Also we don't actually need to minimize as much (not targeted for browser). 7 | // Rather we mostly need to reduce the number of fileSystem access requests 8 | // by reducing the number of files. 9 | minimize: false, 10 | }, 11 | target: "node", 12 | devtool: "source-map", 13 | resolve: { 14 | // Solution for resolution inside mono-repo 15 | modules: [ 16 | path.resolve(__dirname, "node_modules"), 17 | path.resolve(__dirname, "../node_modules"), 18 | "node_modules", 19 | ], 20 | extensions: [".js"], 21 | }, 22 | }; 23 | 24 | module.exports = config; 25 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/content-assist/attribute-name.js: -------------------------------------------------------------------------------- 1 | const { difference, map } = require("lodash"); 2 | 3 | /** 4 | * @param {XMLElement} elementNode 5 | * @param {XSSElement} xssElement 6 | * 7 | * @returns {CompletionSuggestion[]} 8 | */ 9 | function attributeNameCompletion(elementNode, xssElement) { 10 | const possibleSuggestions = map(xssElement.attributes, (_) => _.key); 11 | const existingAttribNames = map(elementNode.attributes, (_) => _.key); 12 | const possibleNewSuggestions = difference( 13 | possibleSuggestions, 14 | existingAttribNames 15 | ); 16 | 17 | const suggestions = map(possibleNewSuggestions, (_) => { 18 | return { 19 | text: _, 20 | label: _, 21 | commitCharacter: "=", 22 | }; 23 | }); 24 | 25 | return suggestions; 26 | } 27 | 28 | module.exports = { 29 | attributeNameCompletion: attributeNameCompletion, 30 | }; 31 | -------------------------------------------------------------------------------- /packages/common/README.md: -------------------------------------------------------------------------------- 1 | [![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/common.svg)](https://www.npmjs.com/package/@xml-tools/common) 2 | 3 | # @xml-tools/common 4 | 5 | Shared Utilities for XML-Tools 6 | 7 | ## Installation 8 | 9 | With npm: 10 | 11 | - `npm install @xml-tools/Common` 12 | 13 | With Yarn 14 | 15 | - `yarn add @xml-tools/Common` 16 | 17 | ## Usage 18 | 19 | Please see the [TypeScript Definitions](./api.d.ts) for full API details. 20 | 21 | Simply import/require the @xml-tools/common package and use the utilities 22 | as defined in the APIs above. 23 | 24 | ```javascript 25 | const xmlToolsCommon = require("@xml-tools/common"); 26 | xmlToolsCommon.findNextTextualToken(/*... */); 27 | ``` 28 | 29 | ## Support 30 | 31 | Please open [issues](https://github.com/SAP/xml-tols/issues) on github. 32 | 33 | ## Contributing 34 | 35 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 36 | -------------------------------------------------------------------------------- /packages/simple-schema/test/validators/utils.js: -------------------------------------------------------------------------------- 1 | const { parse } = require("@xml-tools/parser"); 2 | const { buildAst } = require("@xml-tools/ast"); 3 | const { validate } = require("@xml-tools/validation"); 4 | const { getSchemaValidators } = require("../../"); 5 | 6 | /** 7 | * 8 | * @param {string} xmlText 9 | * @param {SimpleSchema} schema 10 | * @returns {ValidationIssue[] | *} 11 | */ 12 | function validateBySchema(xmlText, schema) { 13 | const { cst, tokenVector } = parse(xmlText); 14 | const xmlDoc = buildAst(cst, tokenVector); 15 | const schemaValidators = getSchemaValidators(schema); 16 | const issues = validate({ 17 | doc: xmlDoc, 18 | validators: { 19 | attribute: [schemaValidators.attribute], 20 | element: [schemaValidators.element], 21 | }, 22 | }); 23 | return issues; 24 | } 25 | 26 | module.exports = { 27 | validateBySchema: validateBySchema, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/ast-position/api.d.ts: -------------------------------------------------------------------------------- 1 | import { XMLAttribute, XMLElement, XMLDocument } from "@xml-tools/ast"; 2 | 3 | /** 4 | * Not all possible kinds of positions are currently supported. 5 | * More may be added in the future. 6 | */ 7 | type AstPosition = 8 | | XMLElementOpenName 9 | | XMLElementCloseName 10 | | XMLAttributeKey 11 | | XMLAttributeValue; 12 | 13 | declare function astPositionAtOffset( 14 | ast: XMLDocument, 15 | offset: number 16 | ): AstPosition | undefined; 17 | 18 | interface XMLElementOpenName { 19 | kind: "XMLElementOpenName"; 20 | astNode: XMLElement; 21 | } 22 | 23 | interface XMLElementCloseName { 24 | kind: "XMLElementCloseName"; 25 | astNode: XMLElement; 26 | } 27 | 28 | interface XMLAttributeKey { 29 | kind: "XMLAttributeKey"; 30 | astNode: XMLAttribute; 31 | } 32 | 33 | interface XMLAttributeValue { 34 | kind: "XMLAttributeValue"; 35 | astNode: XMLAttribute; 36 | } 37 | -------------------------------------------------------------------------------- /packages/ast-position/README.md: -------------------------------------------------------------------------------- 1 | [![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/ast-position.svg)](https://www.npmjs.com/package/@xml-tools/ast-position) 2 | 3 | # @xml-tools/ast-position 4 | 5 | Utilities related to textual positions in the XML-Tools **A**bstract **S**yntax **T**ree. 6 | 7 | ## Installation 8 | 9 | With npm: 10 | 11 | - `npm install @xml-tools/ast-position` 12 | 13 | With Yarn 14 | 15 | - `yarn add @xml-tools/ast-position` 16 | 17 | ## Usage 18 | 19 | Please see the [TypeScript Definitions](./api.d.ts) for full API details. 20 | 21 | Simply import/require the @xml-tools/ast-position package and use the utilities 22 | as declared in the TypeScript signatures above. 23 | 24 | ## Support 25 | 26 | Please open [issues](https://github.com/SAP/xml-tols/issues) on github. 27 | 28 | ## Contributing 29 | 30 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 31 | 32 | [ast]: https://en.wikipedia.org/wiki/Abstract_syntax_tree 33 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/complex-contents/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [], 9 | subElements: [], 10 | textContents: [], 11 | position: { startOffset: 9, endOffset: 21 }, 12 | syntax: { 13 | openName: { image: "note", startOffset: 10, endOffset: 13 }, 14 | closeName: { image: "note", startOffset: 17, endOffset: 20 }, 15 | isSelfClosing: false, 16 | openBody: { startOffset: 9, endOffset: 14 }, 17 | closeBody: { startOffset: 15, endOffset: 21 }, 18 | attributesRange: { startOffset: 15, endOffset: 13 }, 19 | }, 20 | }, 21 | position: { startOffset: 0, endOffset: 22 }, 22 | prolog: { 23 | type: "XMLProlog", 24 | attributes: [], 25 | position: { startOffset: 0, endOffset: 7 }, 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/xml-toolkit/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const baseConfig = require("../../webpack.config.base"); 3 | 4 | const config = { 5 | ...baseConfig, 6 | entry: "./lib/extension.js", 7 | output: { 8 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 9 | path: path.resolve(__dirname, "dist"), 10 | filename: "extension.js", 11 | libraryTarget: "commonjs2", 12 | devtoolModuleFilenameTemplate: "../[resource-path]", 13 | }, 14 | // 📖 -> https://webpack.js.org/configuration/externals/ 15 | externals: { 16 | // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed. 17 | vscode: "commonjs vscode", 18 | // the language-server must be bundled separately as it is executed in a separate process (by path). 19 | "@xml-tools/language-server": "commonjs @xml-tools/language-server", 20 | }, 21 | }; 22 | 23 | module.exports = config; 24 | -------------------------------------------------------------------------------- /packages/validation/lib/validate.js: -------------------------------------------------------------------------------- 1 | const { accept } = require("@xml-tools/ast"); 2 | const { defaultsDeep, flatMap } = require("lodash"); 3 | 4 | function validate(options) { 5 | const actualOptions = defaultsDeep(options, { 6 | validators: { 7 | attribute: [], 8 | element: [], 9 | }, 10 | }); 11 | 12 | let issues = []; 13 | 14 | const validateVisitor = { 15 | visitXMLElement: function (node) { 16 | const newIssues = flatMap(actualOptions.validators.element, (validator) => 17 | validator(node) 18 | ); 19 | issues = issues.concat(newIssues); 20 | }, 21 | visitXMLAttribute: function (node) { 22 | const newIssues = flatMap( 23 | actualOptions.validators.attribute, 24 | (validator) => validator(node) 25 | ); 26 | issues = issues.concat(newIssues); 27 | }, 28 | }; 29 | 30 | accept(actualOptions.doc, validateVisitor); 31 | 32 | return issues; 33 | } 34 | 35 | module.exports = { 36 | validate: validate, 37 | }; 38 | -------------------------------------------------------------------------------- /packages/language-server/lib/api.js: -------------------------------------------------------------------------------- 1 | const { existsSync } = require("fs"); 2 | const { resolve } = require("path"); 3 | 4 | // for use in productive flows 5 | const bundledPath = resolve(__dirname, "..", "dist", "server.js"); 6 | // for dev flows 7 | const sourcesPath = resolve(__dirname, "server.js"); 8 | 9 | // We assume that if the `node_modules` directory exists then we are running 10 | // in development mode, We rely on the fact that the bundled .vsix file of the 11 | // VSCode extension excludes the this package's `node_modules` folder (among others). 12 | // Note this logic would not work with yarn 2.0 / npm 8 / pnpm as `node_modules` 13 | // is no longer created by those package managers. 14 | const isDevelopmentRun = existsSync(resolve(__dirname, "..", "node_modules")); 15 | 16 | let serverPath; 17 | /* istanbul ignore else - no tests (yet?) on bundled artifacts */ 18 | if (isDevelopmentRun) { 19 | serverPath = sourcesPath; 20 | } else { 21 | serverPath = bundledPath; 22 | } 23 | 24 | module.exports = { 25 | SERVER_PATH: serverPath, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/xml-toolkit/test/runTest.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | const { runTests } = require("vscode-test"); 3 | 4 | async function main() { 5 | try { 6 | // The path to the extension test environment 7 | // Passed to --extensionTestsEnv 8 | const extensionTestsEnv = { 9 | path: resolve(__dirname, "..", "..", "test", "suite", "textFixure"), 10 | }; 11 | 12 | // The folder containing the Extension Manifest package.json 13 | // Passed to `--extensionDevelopmentPath` 14 | const extensionDevelopmentPath = resolve(__dirname, ".."); 15 | 16 | // The path to the extension test script 17 | // Passed to --extensionTestsPath 18 | const extensionTestsPath = resolve(__dirname, "suite", "index"); 19 | 20 | // Download VS Code, unzip it and run the integration test 21 | await runTests({ 22 | extensionTestsEnv, 23 | extensionDevelopmentPath, 24 | extensionTestsPath, 25 | }); 26 | } catch (err) { 27 | console.error("Failed to run tests"); 28 | process.exit(1); 29 | } 30 | } 31 | 32 | main(); 33 | -------------------------------------------------------------------------------- /packages/xml-toolkit/test/suite/index.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | const Mocha = require("mocha"); 3 | const glob = require("glob"); 4 | 5 | function run() { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: "bdd", 9 | timeout: 20000, 10 | }); 11 | mocha.useColors(true); 12 | 13 | const testsRoot = resolve(__dirname); 14 | 15 | return new Promise((c, e) => { 16 | glob("**.test.js", { cwd: testsRoot }, (err, files) => { 17 | if (err) { 18 | return e(err); 19 | } 20 | 21 | // Add files to the test suite 22 | files.forEach((f) => mocha.addFile(resolve(testsRoot, f))); 23 | 24 | try { 25 | // Run the mocha test 26 | mocha.run((failures) => { 27 | if (failures > 0) { 28 | e(new Error(`${failures} tests failed.`)); 29 | } else { 30 | c(); 31 | } 32 | }); 33 | } catch (err) { 34 | console.error(err); 35 | e(err); 36 | } 37 | }); 38 | }); 39 | } 40 | 41 | module.exports = { 42 | run: run, 43 | }; 44 | -------------------------------------------------------------------------------- /packages/language-server/README.md: -------------------------------------------------------------------------------- 1 | [![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/language-server.svg)](https://www.npmjs.com/package/@xml-tools/language-server) 2 | 3 | # @xml-tools/language-server 4 | 5 | XML Language Server 6 | 7 | Current Features: 8 | 9 | - Syntax Diagnostics. 10 | 11 | ## Installation 12 | 13 | With npm: 14 | 15 | - `npm install @xml-tools/language-server` 16 | 17 | With Yarn 18 | 19 | - `yarn add @xml-tools/language-server` 20 | 21 | ## Usage 22 | 23 | Please see the [TypeScript Definitions](./api.d.ts) for full API details. 24 | 25 | A simple usage example: 26 | 27 | ```javascript 28 | const { SERVER_PATH } = require("@xml-tools/language-server"); 29 | 30 | // SERVER_PATH is the only API currently and it is meant to expose the "main" module's absolute 31 | // path which would then be executed in a different process 32 | console.log(SERVER_PATH); // --> .../node_module/@xml-tools/language-server/lib/server.js 33 | ``` 34 | 35 | ## Support 36 | 37 | Please open [issues](https://github.com/SAP/xml-tols/issues) on github. 38 | 39 | ## Contributing 40 | 41 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 42 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/missing-top-level-element/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: null, 5 | position: { startOffset: 0, endOffset: 39 }, 6 | prolog: { 7 | type: "XMLProlog", 8 | attributes: [ 9 | { 10 | type: "XMLPrologAttribute", 11 | position: { startOffset: 6, endOffset: 18 }, 12 | key: "version", 13 | value: "1.0", 14 | syntax: { 15 | key: { image: "version", startOffset: 6, endOffset: 12 }, 16 | value: { image: '"1.0"', startOffset: 14, endOffset: 18 }, 17 | }, 18 | }, 19 | { 20 | type: "XMLPrologAttribute", 21 | position: { startOffset: 20, endOffset: 35 }, 22 | key: "encoding", 23 | value: "UTF-8", 24 | syntax: { 25 | key: { image: "encoding", startOffset: 20, endOffset: 27 }, 26 | value: { image: '"UTF-8"', startOffset: 29, endOffset: 35 }, 27 | }, 28 | }, 29 | ], 30 | position: { startOffset: 0, endOffset: 37 }, 31 | }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /packages/ast-position/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xml-tools/ast-position", 3 | "version": "2.0.7", 4 | "description": "XML Ast Position APIs", 5 | "keywords": [ 6 | "xml", 7 | "position" 8 | ], 9 | "main": "lib/api.js", 10 | "repository": "https://github.com/sap/xml-tools/", 11 | "license": "Apache-2.0", 12 | "typings": "./api.d.ts", 13 | "files": [ 14 | "lib", 15 | ".reuse", 16 | "LICENSES", 17 | "api.d.ts" 18 | ], 19 | "dependencies": { 20 | "@xml-tools/ast": "^5.0.5" 21 | }, 22 | "devDependencies": { 23 | "@xml-tools/parser": "^1.0.11" 24 | }, 25 | "scripts": { 26 | "ci": "npm-run-all type-check coverage:*", 27 | "test": "mocha \"./test/**/*spec.js\"", 28 | "coverage:run": "nyc mocha \"./test/**/*spec.js\"", 29 | "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100", 30 | "type-check": "tsc api.d.ts" 31 | }, 32 | "publishConfig": { 33 | "access": "public" 34 | }, 35 | "nyc": { 36 | "include": [ 37 | "lib/**/*.js" 38 | ], 39 | "reporter": [ 40 | "text", 41 | "lcov" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-not-closed/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [ 9 | { 10 | type: "XMLAttribute", 11 | position: { startOffset: 6, endOffset: 16 }, 12 | key: "attr1", 13 | value: "666", 14 | syntax: { 15 | key: { image: "attr1", startOffset: 6, endOffset: 10 }, 16 | value: { image: '"666"', startOffset: 12, endOffset: 16 }, 17 | }, 18 | }, 19 | { 20 | type: "XMLAttribute", 21 | position: { startOffset: 18, endOffset: 23 }, 22 | key: "attr2", 23 | value: null, 24 | syntax: { key: { image: "attr2", startOffset: 18, endOffset: 22 } }, 25 | }, 26 | ], 27 | subElements: [], 28 | textContents: [], 29 | position: { startOffset: 0, endOffset: 23 }, 30 | syntax: { openName: { image: "note", startOffset: 1, endOffset: 4 } }, 31 | }, 32 | position: { startOffset: 0, endOffset: 23 }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /packages/validation/api.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | XMLAstNode, 3 | XMLAttribute, 4 | XMLElement, 5 | XMLDocument, 6 | } from "@xml-tools/ast"; 7 | 8 | declare type AttributeValidator = (node: XMLAttribute) => ValidationIssue[]; 9 | declare type ElementValidator = (node: XMLElement) => ValidationIssue[]; 10 | declare function validate(options: { 11 | doc: XMLDocument; 12 | validators: { 13 | attribute?: AttributeValidator[]; 14 | element?: ElementValidator[]; 15 | }; 16 | }): ValidationIssue[]; 17 | 18 | interface ValidationIssue { 19 | msg: string; 20 | /** 21 | * ASTNode where the issue occurred. 22 | */ 23 | node: XMLAstNode; 24 | /** 25 | * The Severity is optional as that may need to be determined outside 26 | * the validation logic, For example in an IDE's inspections configuration. 27 | */ 28 | severity?: "info" | "warning" | "error"; 29 | 30 | /** 31 | * Optional property to indicate a more specific error location. 32 | * For example: For Missing Attribute error in an element we may want to 33 | * only mark the Element **Name** area instead of the whole element. 34 | */ 35 | position?: { startOffset: number; endOffset: number }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/common/test/find-next-textual-token-spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { parse } = require("@xml-tools/parser"); 3 | const { findNextTextualToken } = require("../"); 4 | 5 | describe("The XML-Tools Common Utils", () => { 6 | context("findNextTextualToken", () => { 7 | it("can find the next textual token in a valid XML", () => { 8 | const xmlText = ` 9 | Bill 10 | Tim 11 | `; 12 | 13 | const { tokenVector } = parse(xmlText); 14 | // "28" is the endOffset of `Bill` 15 | const nextTextualToken = findNextTextualToken(tokenVector, 27); 16 | expect(nextTextualToken.image).to.eql("<"); 17 | expect(nextTextualToken.startOffset).to.eql(37); 18 | }); 19 | 20 | it("will return null when there is no following textual token", () => { 21 | const xmlText = ` 22 | Bill 23 | `; 24 | 25 | const { tokenVector } = parse(xmlText); 26 | // "28" is the endOffset of `Bill` 27 | const nextTextualToken = findNextTextualToken(tokenVector, 27); 28 | expect(nextTextualToken).to.be.null; 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xml-tools/common", 3 | "version": "0.1.6", 4 | "description": "XML-Tools Common Utilities", 5 | "keywords": [ 6 | "xml", 7 | "validation" 8 | ], 9 | "main": "lib/api.js", 10 | "repository": "https://github.com/sap/xml-tools/", 11 | "license": "Apache-2.0", 12 | "typings": "./api.d.ts", 13 | "files": [ 14 | "lib", 15 | ".reuse", 16 | "LICENSES", 17 | "api.d.ts" 18 | ], 19 | "dependencies": { 20 | "lodash": "4.17.21" 21 | }, 22 | "devDependencies": { 23 | "@xml-tools/parser": "^1.0.11" 24 | }, 25 | "scripts": { 26 | "ci": "npm-run-all clean type-check coverage:*", 27 | "clean": "rimraf ./coverage ./nyc_output", 28 | "test": "mocha \"./test/**/*spec.js\"", 29 | "coverage:run": "nyc mocha \"./test/**/*spec.js\"", 30 | "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100", 31 | "type-check": "tsc api.d.ts" 32 | }, 33 | "publishConfig": { 34 | "access": "public" 35 | }, 36 | "nyc": { 37 | "include": [ 38 | "lib/**/*.js" 39 | ], 40 | "reporter": [ 41 | "text", 42 | "lcov" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/language-server/lib/server.js: -------------------------------------------------------------------------------- 1 | const { 2 | createConnection, 3 | ProposedFeatures, 4 | TextDocuments, 5 | TextDocumentSyncKind, 6 | } = require("vscode-languageserver"); 7 | const { TextDocument } = require("vscode-languageserver-textdocument"); 8 | const { has } = require("lodash"); 9 | const { validateDocument } = require("./language-services"); 10 | const { DEFAULT_CONSUMER_NAME } = require("./constants"); 11 | 12 | const connection = createConnection(ProposedFeatures.all); 13 | const documents = new TextDocuments(TextDocument); 14 | 15 | let consumer; 16 | 17 | connection.onInitialize((params) => { 18 | if (has(params, "initializationOptions.consumer")) { 19 | consumer = params.initializationOptions.consumer; 20 | } else { 21 | consumer = DEFAULT_CONSUMER_NAME; 22 | } 23 | 24 | return { 25 | capabilities: { 26 | textDocumentSync: TextDocumentSyncKind.Full, 27 | }, 28 | }; 29 | }); 30 | 31 | documents.onDidChangeContent(async (event) => { 32 | const diagnostics = await validateDocument(event.document, { consumer }); 33 | connection.sendDiagnostics({ uri: event.document.uri, diagnostics }); 34 | }); 35 | 36 | documents.listen(connection); 37 | connection.listen(); 38 | -------------------------------------------------------------------------------- /packages/ast-position/lib/ast-position.js: -------------------------------------------------------------------------------- 1 | class AstPositionVisitor { 2 | constructor(offset) { 3 | this.offset = offset; 4 | this.position = undefined; 5 | } 6 | 7 | visitXMLElement(node) { 8 | const openName = node.syntax.openName; 9 | const closeName = node.syntax.closeName; 10 | if (this.isOffsetInRange(openName)) { 11 | this.position = { kind: "XMLElementOpenName", astNode: node }; 12 | } else if (this.isOffsetInRange(closeName)) { 13 | this.position = { kind: "XMLElementCloseName", astNode: node }; 14 | } 15 | } 16 | 17 | visitXMLAttribute(node) { 18 | const key = node.syntax.key; 19 | const value = node.syntax.value; 20 | 21 | if (this.isOffsetInRange(key)) { 22 | this.position = { kind: "XMLAttributeKey", astNode: node }; 23 | } else if (this.isOffsetInRange(value)) { 24 | this.position = { kind: "XMLAttributeValue", astNode: node }; 25 | } 26 | } 27 | 28 | isOffsetInRange(position) { 29 | return ( 30 | position !== undefined && 31 | position.startOffset <= this.offset && 32 | position.endOffset >= this.offset 33 | ); 34 | } 35 | } 36 | 37 | module.exports = { 38 | AstPositionVisitor: AstPositionVisitor, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/content-assist/lib/api.js: -------------------------------------------------------------------------------- 1 | const { defaultsDeep, flatMap } = require("lodash"); 2 | 3 | const { computeCompletionSyntacticContext } = require("./content-assist"); 4 | 5 | function getSuggestions(options) { 6 | const actualOptions = defaultsDeep(options, { 7 | providers: { 8 | elementContent: [], 9 | elementName: [], 10 | attributeName: [], 11 | attributeValue: [], 12 | }, 13 | context: undefined, 14 | }); 15 | 16 | let { providerType, providerArgs } = computeCompletionSyntacticContext({ 17 | cst: actualOptions.cst, 18 | tokenVector: actualOptions.tokenVector, 19 | ast: actualOptions.ast, 20 | offset: actualOptions.offset, 21 | }); 22 | 23 | // Inject Additional semantic context for the content assist providers. 24 | providerArgs.context = actualOptions.context; 25 | 26 | if (providerType === null) { 27 | return []; 28 | } else { 29 | const selectedProviders = actualOptions.providers[providerType]; 30 | 31 | const suggestions = flatMap(selectedProviders, (suggestionProvider) => 32 | suggestionProvider(providerArgs) 33 | ); 34 | return suggestions; 35 | } 36 | } 37 | 38 | module.exports = { 39 | getSuggestions: getSuggestions, 40 | }; 41 | -------------------------------------------------------------------------------- /packages/constraints/lib/constraints/unique-attribute-keys.js: -------------------------------------------------------------------------------- 1 | const { groupBy, pickBy, reduce, map, filter } = require("lodash"); 2 | 3 | /** 4 | * @param {XMLElement} elem 5 | * @returns {ValidationIssue[]} 6 | */ 7 | function validateUniqueAttributeKeys(elem) { 8 | const attributesWithKeys = filter(elem.attributes, (_) => _.key !== null); 9 | const attribByKey = groupBy(attributesWithKeys, "key"); 10 | const nonUniqueAttribsGroups = pickBy(attribByKey, (_) => _.length > 1); 11 | const nonUniqueAttribs = reduce( 12 | nonUniqueAttribsGroups, 13 | (result, attribsGroup) => result.concat(attribsGroup), 14 | [] 15 | ); 16 | 17 | const validationIssues = map(nonUniqueAttribs, (_) => { 18 | // the `key` is guaranteed to exist because we have filtered above 19 | // for attributes with valid keys 20 | const keyToken = _.syntax.key; 21 | return { 22 | msg: `duplicate attribute: "${_.key}"`, 23 | node: _, 24 | severity: "error", 25 | position: { 26 | startOffset: keyToken.startOffset, 27 | endOffset: keyToken.endOffset, 28 | }, 29 | }; 30 | }); 31 | 32 | return validationIssues; 33 | } 34 | 35 | module.exports = { 36 | validateUniqueAttributeKeys, 37 | }; 38 | -------------------------------------------------------------------------------- /packages/validation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xml-tools/validation", 3 | "version": "1.0.16", 4 | "description": "XML validation APIs", 5 | "keywords": [ 6 | "xml", 7 | "validation" 8 | ], 9 | "main": "lib/api.js", 10 | "repository": "https://github.com/sap/xml-tools/", 11 | "license": "Apache-2.0", 12 | "typings": "./api.d.ts", 13 | "files": [ 14 | "lib", 15 | ".reuse", 16 | "LICENSES", 17 | "api.d.ts" 18 | ], 19 | "dependencies": { 20 | "@xml-tools/ast": "^5.0.5", 21 | "lodash": "4.17.21" 22 | }, 23 | "devDependencies": { 24 | "@xml-tools/parser": "^1.0.11" 25 | }, 26 | "scripts": { 27 | "ci": "npm-run-all clean type-check coverage:*", 28 | "clean": "rimraf ./coverage ./nyc_output", 29 | "test": "mocha \"./test/**/*spec.js\"", 30 | "coverage:run": "nyc mocha \"./test/**/*spec.js\"", 31 | "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100", 32 | "type-check": "tsc api.d.ts" 33 | }, 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "include": [ 39 | "lib/**/*.js" 40 | ], 41 | "reporter": [ 42 | "text", 43 | "lcov" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/content-assist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xml-tools/content-assist", 3 | "version": "3.1.11", 4 | "description": "XML Content Assist APIs", 5 | "keywords": [ 6 | "xml", 7 | "auto-complete", 8 | "content-assist" 9 | ], 10 | "main": "lib/api.js", 11 | "repository": "https://github.com/sap/xml-tools/", 12 | "license": "Apache-2.0", 13 | "typings": "./api.d.ts", 14 | "files": [ 15 | "lib", 16 | ".reuse", 17 | "LICENSES", 18 | "api.d.ts" 19 | ], 20 | "dependencies": { 21 | "@xml-tools/common": "^0.1.6", 22 | "@xml-tools/parser": "^1.0.11", 23 | "lodash": "4.17.21" 24 | }, 25 | "scripts": { 26 | "ci": "npm-run-all clean type-check coverage:*", 27 | "clean": "rimraf ./coverage ./nyc_output", 28 | "test": "mocha \"./test/**/*spec.js\"", 29 | "coverage:run": "nyc mocha \"./test/**/*spec.js\"", 30 | "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100", 31 | "type-check": "tsc api.d.ts" 32 | }, 33 | "publishConfig": { 34 | "access": "public" 35 | }, 36 | "nyc": { 37 | "include": [ 38 | "lib/**/*.js" 39 | ], 40 | "reporter": [ 41 | "text", 42 | "lcov" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/constraints/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xml-tools/constraints", 3 | "version": "1.1.1", 4 | "description": "XML constraints validations", 5 | "keywords": [ 6 | "xml", 7 | "validation" 8 | ], 9 | "main": "lib/api.js", 10 | "repository": "https://github.com/sap/xml-tools/", 11 | "license": "Apache-2.0", 12 | "typings": "./api.d.ts", 13 | "files": [ 14 | "lib", 15 | ".reuse", 16 | "LICENSES", 17 | "api.d.ts" 18 | ], 19 | "dependencies": { 20 | "@xml-tools/validation": "^1.0.16", 21 | "lodash": "4.17.21" 22 | }, 23 | "devDependencies": { 24 | "@xml-tools/ast": "^5.0.5" 25 | }, 26 | "scripts": { 27 | "ci": "npm-run-all clean type-check coverage:*", 28 | "clean": "rimraf ./coverage ./nyc_output", 29 | "test": "mocha \"./test/**/*spec.js\"", 30 | "coverage:run": "nyc mocha \"./test/**/*spec.js\"", 31 | "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100", 32 | "type-check": "tsc api.d.ts" 33 | }, 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "include": [ 39 | "lib/**/*.js" 40 | ], 41 | "reporter": [ 42 | "text", 43 | "lcov" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xml-tools/parser", 3 | "version": "1.0.11", 4 | "description": "XML Parser Implemented in JavaScript", 5 | "keywords": [ 6 | "xml", 7 | "parser" 8 | ], 9 | "main": "lib/api.js", 10 | "repository": "https://github.com/sap/xml-tools/", 11 | "license": "Apache-2.0", 12 | "typings": "./api.d.ts", 13 | "files": [ 14 | "lib", 15 | ".reuse", 16 | "LICENSES", 17 | "api.d.ts" 18 | ], 19 | "dependencies": { 20 | "chevrotain": "7.1.1" 21 | }, 22 | "devDependencies": { 23 | "klaw-sync": "6.0.0" 24 | }, 25 | "scripts": { 26 | "ci": "npm-run-all clean type-check coverage:*", 27 | "clean": "rimraf ./coverage ./nyc_output", 28 | "test": "mocha \"./test/**/*spec.js\"", 29 | "coverage:run": "nyc mocha \"./test/**/*spec.js\"", 30 | "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100", 31 | "snapshots:update": "node ./scripts/update-snapshots.js", 32 | "type-check": "tsc api.d.ts" 33 | }, 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "include": [ 39 | "lib/**/*.js" 40 | ], 41 | "reporter": [ 42 | "text", 43 | "lcov" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/simple-schema/test/content-assist/utils.js: -------------------------------------------------------------------------------- 1 | const { parse } = require("@xml-tools/parser"); 2 | const { buildAst } = require("@xml-tools/ast"); 3 | const { getSuggestions } = require("@xml-tools/content-assist"); 4 | const { getSchemaSuggestionsProviders } = require("../../"); 5 | 6 | /** 7 | * 8 | * @param {string} xmlText 9 | * @param {SimpleSchema} schema 10 | * @returns {ValidationIssue[] | *} 11 | */ 12 | function suggestionsBySchema(xmlText, schema) { 13 | const schemaSuggestionsProviders = getSchemaSuggestionsProviders(schema); 14 | const realXmlText = xmlText.replace("⇶", ""); 15 | const offset = xmlText.indexOf("⇶"); 16 | const { cst, tokenVector } = parse(realXmlText); 17 | const ast = buildAst(cst, tokenVector); 18 | const suggestions = getSuggestions({ 19 | cst, 20 | tokenVector, 21 | ast, 22 | offset: offset, 23 | providers: { 24 | attributeValue: [ 25 | schemaSuggestionsProviders.schemaAttributeValueCompletion, 26 | ], 27 | attributeName: [schemaSuggestionsProviders.schemaAttributeNameCompletion], 28 | elementName: [schemaSuggestionsProviders.schemaElementNameCompletion], 29 | }, 30 | }); 31 | return suggestions; 32 | } 33 | 34 | module.exports = { 35 | suggestionsBySchema: suggestionsBySchema, 36 | }; 37 | -------------------------------------------------------------------------------- /packages/parser/test/sample-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { readFileSync } = require("fs"); 3 | const { resolve, basename } = require("path"); 4 | const { partialRight } = require("lodash"); 5 | 6 | const { transformCstForAssertions } = require("./utils"); 7 | const { parse } = require("../"); 8 | 9 | function executeSampleTest(dirPath, assertNoErrors) { 10 | it("Can Parse an XML text", () => { 11 | const inputPath = resolve(dirPath, "input.xml"); 12 | const inputText = readFileSync(inputPath).toString("utf8"); 13 | const simpleNewLinesInput = inputText.replace(/\r\n/g, "\n"); 14 | const { cst, lexErrors, parseErrors } = parse(simpleNewLinesInput); 15 | if (assertNoErrors === true) { 16 | expect(lexErrors).to.be.empty; 17 | expect(parseErrors).to.be.empty; 18 | } 19 | transformCstForAssertions(cst); 20 | const expectedOutput = require(resolve(dirPath, "output.js")).cst; 21 | expect(cst).to.deep.eql(expectedOutput); 22 | }); 23 | } 24 | 25 | function testNameFromDir(dirPath) { 26 | return basename(dirPath); 27 | } 28 | 29 | module.exports = { 30 | executeValidSampleTest: partialRight(executeSampleTest, true), 31 | executeInValidSampleTest: partialRight(executeSampleTest, false), 32 | testNameFromDir: testNameFromDir, 33 | }; 34 | -------------------------------------------------------------------------------- /packages/parser/test/utils.js: -------------------------------------------------------------------------------- 1 | const { has, forEach, flatMap, identity } = require("lodash"); 2 | 3 | function transformCstForAssertions(cstNode) { 4 | if (has(cstNode, "children")) { 5 | reduceLocationInfo(cstNode.location); 6 | const allChildren = flatMap(cstNode.children, identity); 7 | forEach(allChildren, transformCstForAssertions); 8 | } else if (has(cstNode, "image")) { 9 | reduceTokenInfo(cstNode); 10 | } else { 11 | throw Error("None Exhaustive Match"); 12 | } 13 | } 14 | 15 | function reduceLocationInfo(loc) { 16 | if (isNaN(loc.startOffset)) { 17 | loc.startOffset = null; 18 | } 19 | 20 | if (isNaN(loc.endOffset)) { 21 | loc.endOffset = null; 22 | } 23 | delete loc.startLine; 24 | delete loc.endLine; 25 | delete loc.startColumn; 26 | delete loc.endColumn; 27 | } 28 | 29 | function reduceTokenInfo(tok) { 30 | if (isNaN(tok.startOffset)) { 31 | tok.startOffset = null; 32 | } 33 | 34 | if (isNaN(tok.endOffset)) { 35 | tok.endOffset = null; 36 | } 37 | delete tok.startLine; 38 | delete tok.endLine; 39 | delete tok.startColumn; 40 | delete tok.endColumn; 41 | delete tok.tokenTypeIdx; 42 | tok.tokenType = tok.tokenType.name; 43 | } 44 | 45 | module.exports = { 46 | transformCstForAssertions: transformCstForAssertions, 47 | }; 48 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/content-assist/attribute-value.js: -------------------------------------------------------------------------------- 1 | const { has, isRegExp, forEach, isArray } = require("lodash"); 2 | 3 | /** 4 | * @param {XMLAttribute} attributeNode 5 | * @param {XSSAttribute} xssAttribute 6 | * @param {string} prefix 7 | * 8 | * @returns {CompletionSuggestion[]} 9 | */ 10 | function attributeValueCompletion(attributeNode, xssAttribute, prefix = "") { 11 | // An XSS Attribute definition may not specify any constraints on a value 12 | if (has(xssAttribute, "value") === false) { 13 | return []; 14 | } 15 | 16 | const suggestions = []; 17 | const valueDef = xssAttribute.value; 18 | /* istanbul ignore else - defensive programming */ 19 | if (isRegExp(valueDef)) { 20 | // No suggestions for regExp value definitions... 21 | } else if (isArray(valueDef)) { 22 | forEach(valueDef, (enumVal) => { 23 | if (enumVal.startsWith(prefix)) { 24 | suggestions.push({ 25 | text: enumVal.substring(prefix.length), 26 | label: enumVal, 27 | }); 28 | } 29 | }); 30 | } else { 31 | /* istanbul ignore next defensive programming */ 32 | throw Error("None Exhaustive Match"); 33 | } 34 | 35 | return suggestions; 36 | } 37 | 38 | module.exports = { 39 | attributeValueCompletion: attributeValueCompletion, 40 | }; 41 | -------------------------------------------------------------------------------- /packages/common/lib/find-next-textual-token.js: -------------------------------------------------------------------------------- 1 | const { findIndex } = require("lodash"); 2 | 3 | function findNextTextualToken(tokenVector, prevTokenEndOffset) { 4 | // The TokenVector is sorted, so we could use a BinarySearch to optimize performance 5 | const prevTokenIdx = findIndex( 6 | tokenVector, 7 | (tok) => tok.endOffset === prevTokenEndOffset 8 | ); 9 | let nextTokenIdx = prevTokenIdx; 10 | let found = false; 11 | while (found === false) { 12 | nextTokenIdx++; 13 | const nextPossibleToken = tokenVector[nextTokenIdx]; 14 | // No Next textualToken 15 | if (nextPossibleToken === undefined) { 16 | return null; 17 | } 18 | /* istanbul ignore next 19 | * I don't think this scenario can be created, however defensive coding never killed anyone... 20 | * Basically SEA_WS can only only appear in "OUTSIDE" mode, and we need a CLOSE/SLASH_CLOSE to get back to outside 21 | * mode, however if we had those this function would never have been called... 22 | */ 23 | if (nextPossibleToken.tokenType.name === "SEA_WS") { 24 | // skip pure WS tokens as they do not contain any actual text 25 | } else { 26 | return nextPossibleToken; 27 | } 28 | } 29 | } 30 | 31 | module.exports = { 32 | findNextTextualToken: findNextTextualToken, 33 | }; 34 | -------------------------------------------------------------------------------- /packages/ast/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xml-tools/ast", 3 | "version": "5.0.5", 4 | "description": "XML Ast and Utilities", 5 | "keywords": [ 6 | "xml", 7 | "ast" 8 | ], 9 | "main": "lib/api.js", 10 | "repository": "https://github.com/sap/xml-tools/", 11 | "license": "Apache-2.0", 12 | "typings": "./api.d.ts", 13 | "files": [ 14 | "lib", 15 | ".reuse", 16 | "LICENSES", 17 | "api.d.ts" 18 | ], 19 | "dependencies": { 20 | "@xml-tools/common": "^0.1.6", 21 | "@xml-tools/parser": "^1.0.11", 22 | "lodash": "4.17.21" 23 | }, 24 | "devDependencies": { 25 | "klaw-sync": "6.0.0" 26 | }, 27 | "scripts": { 28 | "ci": "npm-run-all clean type-check coverage:*", 29 | "clean": "rimraf ./coverage ./nyc_output", 30 | "test": "mocha \"./test/**/*spec.js\"", 31 | "coverage:run": "nyc mocha \"./test/**/*spec.js\"", 32 | "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100", 33 | "snapshots:update": "node ./scripts/update-snapshots.js", 34 | "type-check": "tsc api.d.ts" 35 | }, 36 | "publishConfig": { 37 | "access": "public" 38 | }, 39 | "nyc": { 40 | "include": [ 41 | "lib/**/*.js" 42 | ], 43 | "reporter": [ 44 | "text", 45 | "lcov" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/common/api.d.ts: -------------------------------------------------------------------------------- 1 | import { IToken } from "chevrotain"; 2 | 3 | /** 4 | * Finds the next none "SEA_WS" Token after a specific offset. 5 | * 6 | * @param tokenVector - The TokenVector returned from @xml-tools/parser parse method. 7 | * @param prevTokEndOffset - This must be the endOffset of an existing token. 8 | */ 9 | export function findNextTextualToken( 10 | tokenVector: IToken[], 11 | prevTokEndOffset: number 12 | ): IToken | null; 13 | 14 | /** 15 | * Check if an xml attribute key is an xmlns key. 16 | * Attribute keys which are xmlns keys: "xmlns", "xmlns:core". 17 | * Attribute keys which are not xmlns keys: "myattr", "xmlns:with:extra:colon". 18 | * "xmlns:" is considered an xmlns key if opts.includeEmptyPrefix is true. 19 | * 20 | * @param opts.key - the attribute key 21 | * @param opts.includeEmptyPrefix - should true be returned when there is no prefix (key === "xmlns:") 22 | */ 23 | export function isXMLNamespaceKey(opts: { 24 | key: string; 25 | includeEmptyPrefix: boolean; 26 | }): boolean; 27 | 28 | /** 29 | * Get the attribute name, without its "xmlns:" prefix, from an xmlns attribute key. 30 | * If the attribute key is not an xmlns key, undefined is returned. 31 | * @param key - the attribute key 32 | */ 33 | export function getXMLNamespaceKeyPrefix(key: string): string | undefined; 34 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/validators/required-attributes.js: -------------------------------------------------------------------------------- 1 | const { map, filter, difference } = require("lodash"); 2 | const { tokenToOffsetPosition } = require("./utils"); 3 | 4 | /** 5 | * @param {XMLElement} elem 6 | * @param {XSSElement} schema 7 | * 8 | * @returns {ValidationIssue[]} 9 | */ 10 | function validateRequiredAttributes(elem, schema) { 11 | const requiredAttribsDef = filter( 12 | schema.attributes, 13 | (_) => _.required === true 14 | ); 15 | const requiredAttribNames = map(requiredAttribsDef, (_) => _.key); 16 | 17 | const actualAttribNames = map(elem.attributes, (_) => _.key); 18 | const missingAttributesNames = difference( 19 | requiredAttribNames, 20 | actualAttribNames 21 | ); 22 | 23 | // This elementName must always exist, otherwise we could not locate the relevant schema definition 24 | // so this validation could have never executed... 25 | const errPosition = tokenToOffsetPosition(elem.syntax.openName); 26 | const issues = map(missingAttributesNames, (_) => { 27 | return { 28 | msg: `Missing Required Attribute: <${_}>`, 29 | node: elem, 30 | severity: "error", 31 | position: errPosition, 32 | }; 33 | }); 34 | return issues; 35 | } 36 | 37 | module.exports = { 38 | validateRequiredAttributes: validateRequiredAttributes, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/simple-schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xml-tools/simple-schema", 3 | "version": "3.0.5", 4 | "description": "XML Simple Schema", 5 | "keywords": [ 6 | "xml", 7 | "validation" 8 | ], 9 | "main": "lib/api.js", 10 | "repository": "https://github.com/sap/xml-tools/", 11 | "license": "Apache-2.0", 12 | "typings": "./api.d.ts", 13 | "files": [ 14 | "lib", 15 | ".reuse", 16 | "LICENSES", 17 | "api.d.ts" 18 | ], 19 | "dependencies": { 20 | "@xml-tools/ast": "^5.0.5", 21 | "@xml-tools/content-assist": "^3.1.11", 22 | "lodash": "4.17.21" 23 | }, 24 | "devDependencies": { 25 | "@xml-tools/parser": "^1.0.11", 26 | "@xml-tools/validation": "^1.0.16" 27 | }, 28 | "scripts": { 29 | "ci": "npm-run-all clean type-check coverage:*", 30 | "clean": "rimraf ./coverage ./nyc_output", 31 | "test": "mocha \"./test/**/*spec.js\"", 32 | "coverage:run": "nyc mocha \"./test/**/*spec.js\"", 33 | "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100", 34 | "type-check": "tsc api.d.ts" 35 | }, 36 | "publishConfig": { 37 | "access": "public" 38 | }, 39 | "nyc": { 40 | "include": [ 41 | "lib/**/*.js" 42 | ], 43 | "reporter": [ 44 | "text", 45 | "lcov" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/validators/required-sub-elements.js: -------------------------------------------------------------------------------- 1 | const { map, filter, difference } = require("lodash"); 2 | const { tokenToOffsetPosition } = require("./utils"); 3 | 4 | /** 5 | * @param {XMLElement} elem 6 | * @param {XSSElement} schema 7 | * 8 | * @returns {ValidationIssue[]} 9 | */ 10 | function validateRequiredSubElements(elem, schema) { 11 | const requiredSubElemsDef = filter( 12 | schema.elements, 13 | (_) => _.required === true 14 | ); 15 | const requiredElemNames = map(requiredSubElemsDef, (_) => _.name); 16 | 17 | const actualSubElemNameNames = map(elem.subElements, (_) => _.name); 18 | const missingSubElemNames = difference( 19 | requiredElemNames, 20 | actualSubElemNameNames 21 | ); 22 | 23 | // This elementName must always exist, otherwise we could not locate the relevant schema definition 24 | // so this validation could have never executed... 25 | const errPosition = tokenToOffsetPosition(elem.syntax.openName); 26 | const issues = map(missingSubElemNames, (_) => { 27 | return { 28 | msg: `Missing Required Sub-Element: <${_}>`, 29 | node: elem, 30 | severity: "error", 31 | position: errPosition, 32 | }; 33 | }); 34 | return issues; 35 | } 36 | 37 | module.exports = { 38 | validateRequiredSubElements: validateRequiredSubElements, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/validators/unknown-sub-elements.js: -------------------------------------------------------------------------------- 1 | const { map, forEach, includes } = require("lodash"); 2 | const { tokenToOffsetPosition } = require("./utils"); 3 | 4 | /** 5 | * @param {XMLElement} elem 6 | * @param {XSSElement} schema 7 | * 8 | * @returns {ValidationIssue[]} 9 | */ 10 | function validateUnknownSubElements(elem, schema) { 11 | // This validation is only relevant if the Schema disallows unknown elements. 12 | if (schema.elementsType !== "closed") { 13 | return []; 14 | } 15 | 16 | const allowedElemNames = map(schema.elements, (_) => _.name); 17 | 18 | const issues = []; 19 | forEach(elem.subElements, (subElem) => { 20 | if (subElem.name !== null) { 21 | if (includes(allowedElemNames, subElem.name) === false) { 22 | issues.push({ 23 | msg: `Unknown Sub-Element: <${ 24 | subElem.name 25 | }> only [${allowedElemNames.toString()}] Sub-Elements are allowed`, 26 | node: subElem, 27 | severity: "error", 28 | // safe assumption that we have an `openName` (see above condition) 29 | position: tokenToOffsetPosition(subElem.syntax.openName), 30 | }); 31 | } 32 | } 33 | }); 34 | 35 | return issues; 36 | } 37 | 38 | module.exports = { 39 | validateUnknownSubElements: validateUnknownSubElements, 40 | }; 41 | -------------------------------------------------------------------------------- /packages/parser/scripts/update-snapshots.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const klawSync = require("klaw-sync"); 3 | const { resolve } = require("path"); 4 | const { readFileSync, writeFileSync } = require("fs"); 5 | const { format } = require("prettier"); 6 | 7 | const { transformCstForAssertions } = require("../test/utils"); 8 | const { parse } = require("../"); 9 | 10 | const testsDir = resolve(__dirname, "..", "test"); 11 | 12 | const sampleFiles = klawSync(testsDir, { nodir: true }); 13 | const xmlSampleFiles = sampleFiles.filter((fileDesc) => { 14 | if (fileDesc.path.includes("node_modules")) { 15 | return false; 16 | } 17 | return fileDesc.path.endsWith("input.xml"); 18 | }); 19 | 20 | xmlSampleFiles.forEach((fileDesc) => { 21 | const xmlInput = readFileSync(fileDesc.path, "utf8"); 22 | const simpleNewLinesInput = xmlInput.replace(/\r\n/g, "\n"); 23 | console.log(`Reading <${fileDesc.path}>`); 24 | const { cst } = parse(simpleNewLinesInput); 25 | transformCstForAssertions(cst); 26 | const snapshotOutput = `module.exports = { cst : ${JSON.stringify(cst)}}`; 27 | const formattedSnapshotOutput = format(snapshotOutput, { parser: "babel" }); 28 | const outputFilePath = fileDesc.path.replace(/input.xml$/, "output.js"); 29 | console.log(`writing <${outputFilePath}>`); 30 | writeFileSync(outputFilePath, formattedSnapshotOutput); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/constraints/lib/constraints/tag-closing-name-match.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {XMLElement} elem 3 | * @returns {ValidationIssue[]} 4 | */ 5 | function validateTagClosingNameMatch(elem) { 6 | const openTagToken = elem.syntax.openName; 7 | const closeTagToken = elem.syntax.closeName; 8 | 9 | // The element tag must have **both** the opening and closing tokens 10 | // to be able to validate. 11 | if (!openTagToken || !closeTagToken) { 12 | return []; 13 | } 14 | 15 | // alles gut 16 | if (openTagToken.image === closeTagToken.image) { 17 | return []; 18 | } else { 19 | return [ 20 | { 21 | msg: `tags mismatch: "${openTagToken.image}" must match closing tag: "${closeTagToken.image}"`, 22 | node: elem, 23 | severity: "error", 24 | position: { 25 | startOffset: openTagToken.startOffset, 26 | endOffset: openTagToken.endOffset, 27 | }, 28 | }, 29 | { 30 | msg: `tags mismatch: "${closeTagToken.image}" must match opening tag: "${openTagToken.image}"`, 31 | node: elem, 32 | severity: "error", 33 | position: { 34 | startOffset: closeTagToken.startOffset, 35 | endOffset: closeTagToken.endOffset, 36 | }, 37 | }, 38 | ]; 39 | } 40 | } 41 | 42 | module.exports = { 43 | validateTagClosingNameMatch, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/language-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xml-tools/language-server", 3 | "version": "1.1.1", 4 | "description": "XML language server", 5 | "keywords": [ 6 | "LSP", 7 | "language-server", 8 | "XML" 9 | ], 10 | "main": "lib/api.js", 11 | "license": "Apache-2.0", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/SAP/xml-tools/" 15 | }, 16 | "typings": "./api.d.ts", 17 | "files": [ 18 | "dist", 19 | "lib", 20 | ".reuse", 21 | "LICENSES", 22 | "api.d.ts" 23 | ], 24 | "engines": { 25 | "node": ">=10.0.0" 26 | }, 27 | "dependencies": { 28 | "@xml-tools/constraints": "^1.1.1", 29 | "@xml-tools/parser": "^1.0.11", 30 | "lodash": "4.17.21", 31 | "vscode-languageserver": "7.0.0", 32 | "vscode-languageserver-textdocument": "1.0.1" 33 | }, 34 | "devDependencies": { 35 | "chevrotain": "^7.0.1", 36 | "vscode-languageserver-types": "3.16.0" 37 | }, 38 | "scripts": { 39 | "ci": "npm-run-all clean type-check coverage bundle", 40 | "clean": "rimraf ./dist ./coverage ./nyc_output", 41 | "test": "mocha \"./test/**/*spec.js\"", 42 | "coverage": "nyc mocha \"./test/**/*spec.js\"", 43 | "type-check": "tsc api.d.ts", 44 | "bundle": "webpack --mode production" 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/ast/scripts/update-snapshots.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { parse } = require("@xml-tools/parser"); 3 | const { modifyAstForAssertions } = require("../test/utils"); 4 | const klawSync = require("klaw-sync"); 5 | const { resolve } = require("path"); 6 | const { readFileSync, writeFileSync } = require("fs"); 7 | const { format } = require("prettier"); 8 | 9 | const testsDir = resolve(__dirname, "..", "test"); 10 | const { buildAst } = require("../"); 11 | 12 | const sampleFiles = klawSync(testsDir, { nodir: true }); 13 | const xmlSampleFiles = sampleFiles.filter((fileDesc) => { 14 | if (fileDesc.path.includes("node_modules")) { 15 | return false; 16 | } 17 | return fileDesc.path.endsWith("input.xml"); 18 | }); 19 | 20 | xmlSampleFiles.forEach((fileDesc) => { 21 | const xmlInput = readFileSync(fileDesc.path, "utf8"); 22 | const simpleNewLinesInput = xmlInput.replace(/\r\n/g, "\n"); 23 | console.log(`Reading <${fileDesc.path}>`); 24 | const { cst, tokenVector } = parse(simpleNewLinesInput); 25 | const ast = buildAst(cst, tokenVector); 26 | modifyAstForAssertions(ast); 27 | const snapshotOutput = `module.exports = { ast : ${JSON.stringify(ast)}}`; 28 | const formattedSnapshotOutput = format(snapshotOutput, { parser: "babel" }); 29 | const outputFilePath = fileDesc.path.replace(/input.xml$/, "output.js"); 30 | console.log(`writing <${outputFilePath}>`); 31 | writeFileSync(outputFilePath, formattedSnapshotOutput); 32 | }); 33 | -------------------------------------------------------------------------------- /scripts/merge-coverage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * based on https://github.com/istanbuljs/istanbuljs/blob/1fe490e51909607137ded25b1688581c9fd926cd/monorepo-merge-reports.js 3 | */ 4 | const { dirname, basename, join, resolve } = require("path"); 5 | const { spawnSync } = require("child_process"); 6 | 7 | const rimraf = require("rimraf"); 8 | const makeDir = require("make-dir"); 9 | const glob = require("glob"); 10 | 11 | process.chdir(resolve(__dirname, "..")); 12 | rimraf.sync(".nyc_output"); 13 | makeDir.sync(".nyc_output"); 14 | 15 | // Merge coverage data from each package so we can generate a complete reports 16 | glob.sync("packages/*/.nyc_output").forEach((nycOutput) => { 17 | const cwd = dirname(nycOutput); 18 | const { status, stderr } = spawnSync( 19 | resolve("node_modules", ".bin", "nyc"), 20 | [ 21 | "merge", 22 | ".nyc_output", 23 | join(__dirname, "..", ".nyc_output", basename(cwd) + ".json"), 24 | ], 25 | { 26 | encoding: "utf8", 27 | shell: true, 28 | cwd, 29 | } 30 | ); 31 | 32 | if (status !== 0) { 33 | console.error(stderr); 34 | process.exit(status); 35 | } 36 | }); 37 | 38 | const { status, stderr } = spawnSync( 39 | resolve("node_modules", ".bin", "nyc"), 40 | ["report", "--reporter=lcov"], 41 | { 42 | encoding: "utf8", 43 | shell: true, 44 | cwd: resolve(__dirname, ".."), 45 | } 46 | ); 47 | 48 | if (status !== 0) { 49 | console.error(stderr); 50 | process.exit(status); 51 | } 52 | -------------------------------------------------------------------------------- /packages/constraints/test/api-spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { parse } = require("@xml-tools/parser"); 3 | const { buildAst } = require("@xml-tools/ast"); 4 | const { checkConstraints } = require("../"); 5 | 6 | describe("the constraints sub-package apis", () => { 7 | it("can detect validation issues using `checkConstraints` api", () => { 8 | const xmlText = ` 9 | 10 | Bill 11 | Tim 12 | `; 13 | 14 | const { cst, tokenVector } = parse(xmlText); 15 | const document = buildAst(cst, tokenVector); 16 | const validationIssues = checkConstraints(document); 17 | const element = document.rootElement; 18 | expect(validationIssues).to.have.lengthOf(2); 19 | expect(validationIssues).to.deep.include.members([ 20 | { 21 | msg: 'tags mismatch: "note" must match closing tag: "note-typo"', 22 | severity: "error", 23 | node: element, 24 | position: { 25 | startOffset: element.syntax.openName.startOffset, 26 | endOffset: element.syntax.openName.endOffset, 27 | }, 28 | }, 29 | { 30 | msg: 'tags mismatch: "note-typo" must match opening tag: "note"', 31 | severity: "error", 32 | node: element, 33 | position: { 34 | startOffset: element.syntax.closeName.startOffset, 35 | endOffset: element.syntax.closeName.endOffset, 36 | }, 37 | }, 38 | ]); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/constraints/README.md: -------------------------------------------------------------------------------- 1 | [![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/constraints.svg)](https://www.npmjs.com/package/@xml-tools/constraints) 2 | 3 | # @xml-tools/constraints 4 | 5 | Validations for XML constraints. 6 | 7 | The following constraints are currently implemented: 8 | 9 | - Uniqueness of attribute keys inside the same element. 10 | - Element opening tag name must be identical to the element closing tag name. 11 | 12 | ## Installation 13 | 14 | With npm: 15 | 16 | - `npm install @xml-tools/constraints` 17 | 18 | With Yarn 19 | 20 | - `yarn add @xml-tools/constraints` 21 | 22 | ## Usage 23 | 24 | Please see the [TypeScript Definitions](./api.d.ts) for full API details. 25 | 26 | A simple usage example: 27 | 28 | ```javascript 29 | const { parse } = require("@xml-tools/parser"); 30 | const { buildAst } = require("@xml-tools/ast"); 31 | const { checkConstraints } = require("@xml-tools/constraints"); 32 | 33 | const xmlText = ` 34 | 35 | Bill 36 | Tim 37 | `; 38 | 39 | const { cst, tokenVector } = parse(xmlText); 40 | const document = buildAst(cst, tokenVector); 41 | const validationIssues = checkConstraints(document); 42 | console.log(validationIssues[0].msg); // --> 'opening tag: "note" must match closing tag: "note-typo" 43 | ``` 44 | 45 | ## Support 46 | 47 | Please open [issues](https://github.com/SAP/xml-tols/issues) on github. 48 | 49 | ## Contributing 50 | 51 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 52 | -------------------------------------------------------------------------------- /packages/ast/test/sample-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { readFileSync } = require("fs"); 3 | const { resolve, basename } = require("path"); 4 | const { parse } = require("@xml-tools/parser"); 5 | const { partialRight } = require("lodash"); 6 | 7 | const { 8 | modifyAstForAssertions, 9 | assertParentPropsAreValid, 10 | } = require("./utils"); 11 | const { buildAst } = require("../"); 12 | 13 | function executeSampleTest(dirPath, assertNoErrors) { 14 | it("Can build an AST for a valid XML", () => { 15 | const inputPath = resolve(dirPath, "input.xml"); 16 | const inputText = readFileSync(inputPath).toString("utf8"); 17 | const simpleNewLinesInput = inputText.replace(/\r\n/g, "\n"); 18 | const { cst, tokenVector, lexErrors, parseErrors } = parse( 19 | simpleNewLinesInput 20 | ); 21 | if (assertNoErrors === true) { 22 | expect(lexErrors).to.be.empty; 23 | expect(parseErrors).to.be.empty; 24 | } 25 | const ast = buildAst(cst, tokenVector); 26 | assertParentPropsAreValid(ast); 27 | modifyAstForAssertions(ast); 28 | const expectedOutput = require(resolve(dirPath, "output.js")).ast; 29 | expect(ast).to.deep.eql(expectedOutput); 30 | }); 31 | } 32 | 33 | function testNameFromDir(dirPath) { 34 | return basename(dirPath); 35 | } 36 | 37 | module.exports = { 38 | executeValidSampleTest: partialRight(executeSampleTest, true), 39 | executeInValidSampleTest: partialRight(executeSampleTest, false), 40 | testNameFromDir: testNameFromDir, 41 | }; 42 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/attributes/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [ 9 | { 10 | type: "XMLAttribute", 11 | position: { startOffset: 6, endOffset: 16 }, 12 | key: "attr1", 13 | value: "666", 14 | syntax: { 15 | key: { image: "attr1", startOffset: 6, endOffset: 10 }, 16 | value: { image: '"666"', startOffset: 12, endOffset: 16 }, 17 | }, 18 | }, 19 | { 20 | type: "XMLAttribute", 21 | position: { startOffset: 18, endOffset: 28 }, 22 | key: "attr2", 23 | value: "333", 24 | syntax: { 25 | key: { image: "attr2", startOffset: 18, endOffset: 22 }, 26 | value: { image: '"333"', startOffset: 24, endOffset: 28 }, 27 | }, 28 | }, 29 | ], 30 | subElements: [], 31 | textContents: [], 32 | position: { startOffset: 0, endOffset: 36 }, 33 | syntax: { 34 | openName: { image: "note", startOffset: 1, endOffset: 4 }, 35 | closeName: { image: "note", startOffset: 32, endOffset: 35 }, 36 | isSelfClosing: false, 37 | openBody: { startOffset: 0, endOffset: 29 }, 38 | closeBody: { startOffset: 30, endOffset: 36 }, 39 | attributesRange: { startOffset: 6, endOffset: 28 }, 40 | }, 41 | }, 42 | position: { startOffset: 0, endOffset: 37 }, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/xml-toolkit/README.md: -------------------------------------------------------------------------------- 1 | # XML Toolkit 2 | 3 | The XML toolkit is a VSCode extension that provides XML language editor support. 4 | 5 | ## Features 6 | 7 | ### XML syntax validations 8 | 9 | The syntax validations use a fault-tolerant parser. This enables 10 | the detection of **multiple** syntax errors. 11 | 12 | ### XML Well-formed validations 13 | 14 | - Unique Attribute Names. 15 | - Tag open/close names are aligned. 16 | 17 | #### Preview 18 | 19 | ![](https://raw.githubusercontent.com/SAP/xml-tools/master/packages/xml-toolkit/resources/readme/preview-syntax-validation.png) 20 | 21 | ## Installation 22 | 23 | ### From the VS Code Marketplace 24 | 25 | In the [XML Toolkit](https://marketplace.visualstudio.com/items?itemName=SAPOSS.xml-toolkit) 26 | page, click **Install**. 27 | 28 | ### From Github releases 29 | 30 | 1. Go to [GitHub Releases](https://github.com/sap/xml-tools/releases). 31 | 2. Search for the `.vsix` archive under `xml-toolkit\@x.y.z` releases. 32 | - Replace `x.y.z` with the desired version number. 33 | 3. Follow the instructions for installing an extension from a `.vsix` 34 | file in the [VSCode's guide](https://code.visualstudio.com/docs/editor/extension-gallery#_install-from-a-vsix). 35 | 36 | ## Usage 37 | 38 | The extension is activated when an `.xml` file is opened. 39 | Both lexing and parsing errors will be shown as squiggly red underlines 40 | and in the Problems view. 41 | 42 | ## Support 43 | 44 | You can open [issues](https://github.com/SAP/xml-tools/issues) on GitHub. 45 | 46 | ## Contributing 47 | 48 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 49 | -------------------------------------------------------------------------------- /packages/common/lib/xml-ns-key.js: -------------------------------------------------------------------------------- 1 | // The xml parser takes care of validating the attribute name. 2 | // If the user started the attribute name with "xmlns:" we can assume that 3 | // they meant for it to be an xml namespace attribute. 4 | // xmlns attributes explicitly can't contain ":" after the "xmlns:" part. 5 | const namespaceRegex = /^xmlns(?:(?[^:]*))?$/; 6 | 7 | /** 8 | * See comment in api.d.ts. 9 | * 10 | * @param {string} key 11 | * @param {boolean} includeEmptyPrefix 12 | * @returns {boolean} 13 | */ 14 | function isXMLNamespaceKey({ key, includeEmptyPrefix }) { 15 | if (typeof key !== "string") { 16 | return false; 17 | } 18 | const matchArr = key.match(namespaceRegex); 19 | 20 | // No match - this is not an xmlns key 21 | if (matchArr === null) { 22 | return false; 23 | } 24 | 25 | return !!( 26 | includeEmptyPrefix === true || 27 | // "xmlns" case 28 | !matchArr.groups.prefixWithColon || 29 | // "xmlns:" case 30 | matchArr.groups.prefix 31 | ); 32 | } 33 | 34 | /** 35 | * See comment in api.d.ts. 36 | * 37 | * @param {string} key 38 | * @returns {string|undefined} 39 | */ 40 | function getXMLNamespaceKeyPrefix(key) { 41 | if (typeof key !== "string") { 42 | return undefined; 43 | } 44 | const matchArr = key.match(namespaceRegex); 45 | if (matchArr === null) { 46 | return undefined; 47 | } 48 | return (matchArr.groups && matchArr.groups.prefix) || ""; 49 | } 50 | 51 | module.exports = { 52 | isXMLNamespaceKey: isXMLNamespaceKey, 53 | getXMLNamespaceKeyPrefix: getXMLNamespaceKeyPrefix, 54 | }; 55 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/validators/duplicate-sub-elements.js: -------------------------------------------------------------------------------- 1 | const { map, forEach, includes, filter, groupBy } = require("lodash"); 2 | const { tokenToOffsetPosition } = require("./utils"); 3 | 4 | /** 5 | * @param {XMLElement} elem 6 | * @param {XSSElement} schema 7 | * 8 | * @returns {ValidationIssue[]} 9 | */ 10 | function validateDuplicateSubElements(elem, schema) { 11 | const allowedDupElem = filter( 12 | schema.elements, 13 | (_) => _.cardinality === "many" 14 | ); 15 | const allowedDupElemNames = map(allowedDupElem, (_) => _.name); 16 | 17 | const actualSubElemByName = groupBy(elem.subElements, (_) => _.name); 18 | const issues = []; 19 | forEach(actualSubElemByName, (dupElements, dupElementsName) => { 20 | const allowedDup = includes(allowedDupElemNames, dupElementsName); 21 | const hasConfiguration = schema.elements[dupElementsName] !== undefined; 22 | const hasDuplicates = dupElements.length > 1; 23 | 24 | if (allowedDup === false && hasDuplicates && hasConfiguration) { 25 | forEach(dupElements, (dupElem) => { 26 | issues.push({ 27 | msg: `Duplicate Sub-Element: <${dupElem.name}> only a single occurrence of this Sub-Element is allowed here.`, 28 | node: dupElem, 29 | severity: "error", 30 | // safe assumption that we have an `openName` (see above condition) 31 | position: tokenToOffsetPosition(dupElem.syntax.openName), 32 | }); 33 | }); 34 | } 35 | }); 36 | 37 | return issues; 38 | } 39 | 40 | module.exports = { 41 | validateDuplicateSubElements: validateDuplicateSubElements, 42 | }; 43 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/validators/unknown-attributes.js: -------------------------------------------------------------------------------- 1 | const { map, includes, forEach } = require("lodash"); 2 | const { isXMLNamespaceKey } = require("@xml-tools/common"); 3 | const { tokenToOffsetPosition } = require("./utils"); 4 | 5 | /** 6 | * @param {XMLElement} elem 7 | * @param {XSSElement} schema 8 | * 9 | * @returns {ValidationIssue[]} 10 | */ 11 | function validateUnknownAttributes(elem, schema) { 12 | // This validation is only relevant if the Schema disallows unknown attributes. 13 | if (schema.attributesType !== "closed") { 14 | return []; 15 | } 16 | const allowedAttribNames = map(schema.attributes, (_) => _.key); 17 | 18 | const issues = []; 19 | forEach(elem.attributes, (attrib) => { 20 | /* istanbul ignore else - Defensive programming, but cannot 21 | * reproduce this branch with current error recovery heuristics 22 | */ 23 | if (attrib.key !== null) { 24 | if ( 25 | includes(allowedAttribNames, attrib.key) === false && 26 | isXMLNamespaceKey({ key: attrib.key, includeEmptyPrefix: true }) === 27 | false 28 | ) { 29 | issues.push({ 30 | msg: `Unknown Attribute: <${ 31 | attrib.key 32 | }> only [${allowedAttribNames.toString()}] attributes are allowed`, 33 | node: attrib, 34 | severity: "error", 35 | // safe assumption that we have a `key` token (see above condition) 36 | position: tokenToOffsetPosition(attrib.syntax.key), 37 | }); 38 | } 39 | } 40 | }); 41 | return issues; 42 | } 43 | 44 | module.exports = { 45 | validateUnknownAttributes: validateUnknownAttributes, 46 | }; 47 | -------------------------------------------------------------------------------- /packages/ast/lib/visit-ast.js: -------------------------------------------------------------------------------- 1 | const { forEach, isFunction } = require("lodash"); 2 | const { getAstChildrenReflective } = require("./utils"); 3 | 4 | /** 5 | * @param {XMLAstNode} node 6 | * @param {XMLAstVisitor} visitor 7 | * 8 | * @returns {void} 9 | */ 10 | function accept(node, visitor) { 11 | switch (node.type) { 12 | case "XMLDocument": { 13 | if (isFunction(visitor.visitXMLDocument)) { 14 | visitor.visitXMLDocument(node); 15 | } 16 | break; 17 | } 18 | case "XMLProlog": { 19 | if (isFunction(visitor.visitXMLProlog)) { 20 | visitor.visitXMLProlog(node); 21 | } 22 | break; 23 | } 24 | case "XMLPrologAttribute": { 25 | if (isFunction(visitor.visitXMLPrologAttribute)) { 26 | visitor.visitXMLPrologAttribute(node); 27 | } 28 | break; 29 | } 30 | case "XMLElement": { 31 | if (isFunction(visitor.visitXMLElement)) { 32 | visitor.visitXMLElement(node); 33 | } 34 | break; 35 | } 36 | case "XMLAttribute": { 37 | if (isFunction(visitor.visitXMLAttribute)) { 38 | visitor.visitXMLAttribute(node); 39 | } 40 | break; 41 | } 42 | case "XMLTextContent": { 43 | if (isFunction(visitor.visitXMLTextContent)) { 44 | visitor.visitXMLTextContent(node); 45 | } 46 | break; 47 | } 48 | /* istanbul ignore next defensive programming */ 49 | default: 50 | throw Error("None Exhaustive Match"); 51 | } 52 | 53 | const astChildren = getAstChildrenReflective(node); 54 | forEach(astChildren, (childNode) => { 55 | accept(childNode, visitor); 56 | }); 57 | } 58 | 59 | module.exports = { 60 | accept: accept, 61 | }; 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # sub-packages files for https://reuse.software (avoid duplication) 2 | .reuse 3 | LICENSES 4 | LICENSE 5 | 6 | # root files for https://reuse.software 7 | !/.reuse 8 | !/LICENSES 9 | !/LICENSE 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | 70 | # next.js build output 71 | .next 72 | 73 | # IDE 74 | .idea 75 | .vscode 76 | 77 | # vscode-test 78 | .vscode-test 79 | 80 | # VSCode extension build result 81 | *.vsix 82 | 83 | # bundling results 84 | dist 85 | 86 | # NYC 87 | .nyc_output 88 | coverage 89 | 90 | #Output 91 | report 92 | *.map 93 | 94 | # we are using yarn instead of npm 95 | package-lock.json 96 | 97 | -------------------------------------------------------------------------------- /packages/xml-toolkit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xml-toolkit", 3 | "version": "1.2.1", 4 | "private": true, 5 | "displayName": "XML Toolkit", 6 | "description": "Language Support for XML", 7 | "publisher": "SAPOSS", 8 | "icon": "resources/xml-toolkit.png", 9 | "keywords": [ 10 | "xml" 11 | ], 12 | "categories": [ 13 | "Programming Languages" 14 | ], 15 | "license": "Apache-2.0", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/SAP/xml-tools/tree/master/packages/xml-toolkit" 19 | }, 20 | "engines": { 21 | "vscode": "^1.56.0" 22 | }, 23 | "activationEvents": [ 24 | "onLanguage:xml" 25 | ], 26 | "main": "./lib/extension", 27 | "scripts": { 28 | "ci": "npm-run-all clean coverage:* bundle package", 29 | "clean": "rimraf ./dist ./coverage ./nyc_output *.vsix NOTICE LICENSE", 30 | "test": "node test/runTest.js", 31 | "coverage:run": "nyc node test/runTest.js", 32 | "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100", 33 | "vsix:prepare": "sh prepare-vsix-package.sh", 34 | "bundle": "webpack --mode production", 35 | "package": "node ./scripts/package-vsix" 36 | }, 37 | "dependencies": { 38 | "@xml-tools/language-server": "^1.1.1", 39 | "vscode-languageclient": "6.1.3" 40 | }, 41 | "devDependencies": { 42 | "@types/vscode": "1.56.0", 43 | "deep-equal-in-any-order": "1.0.28", 44 | "lodash": "4.17.21", 45 | "proxyquire": "2.1.3", 46 | "vsce": "1.84.0", 47 | "vscode-test": "1.5.2" 48 | }, 49 | "nyc": { 50 | "include": [ 51 | "lib/**/*.js" 52 | ], 53 | "reporter": [ 54 | "text", 55 | "lcov" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/prolog/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [], 9 | subElements: [], 10 | textContents: [], 11 | position: { startOffset: 39, endOffset: 51 }, 12 | syntax: { 13 | openName: { image: "note", startOffset: 40, endOffset: 43 }, 14 | closeName: { image: "note", startOffset: 47, endOffset: 50 }, 15 | isSelfClosing: false, 16 | openBody: { startOffset: 39, endOffset: 44 }, 17 | closeBody: { startOffset: 45, endOffset: 51 }, 18 | attributesRange: { startOffset: 45, endOffset: 43 }, 19 | }, 20 | }, 21 | position: { startOffset: 0, endOffset: 52 }, 22 | prolog: { 23 | type: "XMLProlog", 24 | attributes: [ 25 | { 26 | type: "XMLPrologAttribute", 27 | position: { startOffset: 6, endOffset: 18 }, 28 | key: "version", 29 | value: "1.0", 30 | syntax: { 31 | key: { image: "version", startOffset: 6, endOffset: 12 }, 32 | value: { image: '"1.0"', startOffset: 14, endOffset: 18 }, 33 | }, 34 | }, 35 | { 36 | type: "XMLPrologAttribute", 37 | position: { startOffset: 20, endOffset: 35 }, 38 | key: "encoding", 39 | value: "UTF-8", 40 | syntax: { 41 | key: { image: "encoding", startOffset: 20, endOffset: 27 }, 42 | value: { image: '"UTF-8"', startOffset: 29, endOffset: 35 }, 43 | }, 44 | }, 45 | ], 46 | position: { startOffset: 0, endOffset: 37 }, 47 | }, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /packages/validation/test/validate-apis-spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { map } = require("lodash"); 3 | const { parse } = require("@xml-tools/parser"); 4 | const { buildAst } = require("@xml-tools/ast"); 5 | const { validate } = require("../"); 6 | 7 | describe("The XML Validations APIs", () => { 8 | it("can validate attributes", () => { 9 | const xmlText = ` 10 | Bill 11 | Tim 12 | `; 13 | 14 | const ast = xmlTextToAst(xmlText); 15 | 16 | const issues = validate({ 17 | doc: ast, 18 | validators: { 19 | element: [ 20 | (node) => { 21 | return [{ msg: node.name }]; 22 | }, 23 | ], 24 | }, 25 | }); 26 | 27 | const msgNodeNames = map(issues, (_) => _.msg); 28 | expect(msgNodeNames).to.have.lengthOf(3); 29 | expect(msgNodeNames).to.have.members(["note", "to", "from"]); 30 | }); 31 | 32 | it("can validate elements", () => { 33 | const xmlText = ` 34 | 35 | Gates 36 | Tim 37 | `; 38 | 39 | const ast = xmlTextToAst(xmlText); 40 | 41 | const issues = validate({ 42 | doc: ast, 43 | validators: { 44 | attribute: [ 45 | (node) => { 46 | return [{ msg: node.key }]; 47 | }, 48 | ], 49 | }, 50 | }); 51 | 52 | const msgNodeNames = map(issues, (_) => _.msg); 53 | expect(msgNodeNames).to.have.lengthOf(2); 54 | expect(msgNodeNames).to.have.members(["clearance", "hidden"]); 55 | }); 56 | }); 57 | 58 | function xmlTextToAst(text) { 59 | const { cst, tokenVector } = parse(text); 60 | const ast = buildAst(cst, tokenVector); 61 | return ast; 62 | } 63 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-last/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [ 9 | { 10 | type: "XMLAttribute", 11 | position: { startOffset: 6, endOffset: 16 }, 12 | key: "attr1", 13 | value: "666", 14 | syntax: { 15 | key: { image: "attr1", startOffset: 6, endOffset: 10 }, 16 | value: { image: '"666"', startOffset: 12, endOffset: 16 }, 17 | }, 18 | }, 19 | { 20 | type: "XMLAttribute", 21 | position: { startOffset: 18, endOffset: 28 }, 22 | key: "attr2", 23 | value: "333", 24 | syntax: { 25 | key: { image: "attr2", startOffset: 18, endOffset: 22 }, 26 | value: { image: '"333"', startOffset: 24, endOffset: 28 }, 27 | }, 28 | }, 29 | { 30 | type: "XMLAttribute", 31 | position: { startOffset: 30, endOffset: 35 }, 32 | key: "attr3", 33 | value: null, 34 | syntax: { key: { image: "attr3", startOffset: 30, endOffset: 34 } }, 35 | }, 36 | ], 37 | subElements: [], 38 | textContents: [], 39 | position: { startOffset: 0, endOffset: 44 }, 40 | syntax: { 41 | openName: { image: "note", startOffset: 1, endOffset: 4 }, 42 | closeName: { image: "note", startOffset: 40, endOffset: 43 }, 43 | isSelfClosing: false, 44 | openBody: { startOffset: 0, endOffset: 37 }, 45 | closeBody: { startOffset: 38, endOffset: 44 }, 46 | attributesRange: { startOffset: 6, endOffset: 36 }, 47 | }, 48 | }, 49 | position: { startOffset: 0, endOffset: 45 }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-middle/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [ 9 | { 10 | type: "XMLAttribute", 11 | position: { startOffset: 6, endOffset: 16 }, 12 | key: "attr1", 13 | value: "666", 14 | syntax: { 15 | key: { image: "attr1", startOffset: 6, endOffset: 10 }, 16 | value: { image: '"666"', startOffset: 12, endOffset: 16 }, 17 | }, 18 | }, 19 | { 20 | type: "XMLAttribute", 21 | position: { startOffset: 18, endOffset: 23 }, 22 | key: "attr2", 23 | value: null, 24 | syntax: { key: { image: "attr2", startOffset: 18, endOffset: 22 } }, 25 | }, 26 | { 27 | type: "XMLAttribute", 28 | position: { startOffset: 25, endOffset: 35 }, 29 | key: "attr3", 30 | value: "111", 31 | syntax: { 32 | key: { image: "attr3", startOffset: 25, endOffset: 29 }, 33 | value: { image: '"111"', startOffset: 31, endOffset: 35 }, 34 | }, 35 | }, 36 | ], 37 | subElements: [], 38 | textContents: [], 39 | position: { startOffset: 0, endOffset: 44 }, 40 | syntax: { 41 | openName: { image: "note", startOffset: 1, endOffset: 4 }, 42 | closeName: { image: "note", startOffset: 40, endOffset: 43 }, 43 | isSelfClosing: false, 44 | openBody: { startOffset: 0, endOffset: 37 }, 45 | closeBody: { startOffset: 38, endOffset: 44 }, 46 | attributesRange: { startOffset: 6, endOffset: 36 }, 47 | }, 48 | }, 49 | position: { startOffset: 0, endOffset: 45 }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-last/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [ 9 | { 10 | type: "XMLAttribute", 11 | position: { startOffset: 6, endOffset: 16 }, 12 | key: "attr1", 13 | value: "666", 14 | syntax: { 15 | key: { image: "attr1", startOffset: 6, endOffset: 10 }, 16 | value: { image: '"666"', startOffset: 12, endOffset: 16 }, 17 | }, 18 | }, 19 | { 20 | type: "XMLAttribute", 21 | position: { startOffset: 18, endOffset: 28 }, 22 | key: "attr2", 23 | value: "333", 24 | syntax: { 25 | key: { image: "attr2", startOffset: 18, endOffset: 22 }, 26 | value: { image: '"333"', startOffset: 24, endOffset: 28 }, 27 | }, 28 | }, 29 | { 30 | type: "XMLAttribute", 31 | position: { startOffset: 30, endOffset: 34 }, 32 | key: "attr3", 33 | value: null, 34 | syntax: { key: { image: "attr3", startOffset: 30, endOffset: 34 } }, 35 | }, 36 | ], 37 | subElements: [], 38 | textContents: [], 39 | position: { startOffset: 0, endOffset: 43 }, 40 | syntax: { 41 | openName: { image: "note", startOffset: 1, endOffset: 4 }, 42 | closeName: { image: "note", startOffset: 39, endOffset: 42 }, 43 | isSelfClosing: false, 44 | openBody: { startOffset: 0, endOffset: 36 }, 45 | closeBody: { startOffset: 37, endOffset: 43 }, 46 | attributesRange: { startOffset: 6, endOffset: 35 }, 47 | }, 48 | }, 49 | position: { startOffset: 0, endOffset: 44 }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/attributes-no-value-no-eql-middle/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [ 9 | { 10 | type: "XMLAttribute", 11 | position: { startOffset: 6, endOffset: 16 }, 12 | key: "attr1", 13 | value: "666", 14 | syntax: { 15 | key: { image: "attr1", startOffset: 6, endOffset: 10 }, 16 | value: { image: '"666"', startOffset: 12, endOffset: 16 }, 17 | }, 18 | }, 19 | { 20 | type: "XMLAttribute", 21 | position: { startOffset: 18, endOffset: 22 }, 22 | key: "attr2", 23 | value: null, 24 | syntax: { key: { image: "attr2", startOffset: 18, endOffset: 22 } }, 25 | }, 26 | { 27 | type: "XMLAttribute", 28 | position: { startOffset: 24, endOffset: 34 }, 29 | key: "attr3", 30 | value: "111", 31 | syntax: { 32 | key: { image: "attr3", startOffset: 24, endOffset: 28 }, 33 | value: { image: '"111"', startOffset: 30, endOffset: 34 }, 34 | }, 35 | }, 36 | ], 37 | subElements: [], 38 | textContents: [], 39 | position: { startOffset: 0, endOffset: 43 }, 40 | syntax: { 41 | openName: { image: "note", startOffset: 1, endOffset: 4 }, 42 | closeName: { image: "note", startOffset: 39, endOffset: 42 }, 43 | isSelfClosing: false, 44 | openBody: { startOffset: 0, endOffset: 36 }, 45 | closeBody: { startOffset: 37, endOffset: 43 }, 46 | attributesRange: { startOffset: 6, endOffset: 35 }, 47 | }, 48 | }, 49 | position: { startOffset: 0, endOffset: 44 }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /packages/parser/README.md: -------------------------------------------------------------------------------- 1 | [![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/parser.svg)](https://www.npmjs.com/package/@xml-tools/parser) 2 | 3 | # @xml-tools/parser 4 | 5 | A Fault Tolerant XML Parser which produces a [Concrete Syntax Tree][cst]. 6 | 7 | This means that the Parser will **not** stop on the first error and instead attempt to perform automatic error recovery. 8 | This also means that the CST outputted by the Parser may only have **partial** results. 9 | For example, In a valid XML an attribute must always have a value, however in the CST produced 10 | by this parser an attribute's value may be missing as the XML Text input is not necessarily valid. 11 | 12 | The CST produced by this parser is often used as the input for other packages in the xml-tools scope, e.g: 13 | 14 | - [@xml-tools/ast](../ast) As the input for building an XML AST. 15 | - [@xml-tools/content-assist](../content-assist) As part of the input for the content assist APIs. 16 | 17 | ## Installation 18 | 19 | With npm: 20 | 21 | - `npm install @xml-tools/parser` 22 | 23 | With Yarn 24 | 25 | - `yarn add @xml-tools/parser` 26 | 27 | ## Usage 28 | 29 | Please see the [TypeScript Definitions](./api.d.ts) for full API details. 30 | 31 | A simple usage example: 32 | 33 | ```javascript 34 | const { parse } = require("@xml-tools/parser"); 35 | 36 | const xmlText = ` 37 | Bill 38 | Tim 39 | 40 | `; 41 | 42 | const { cst, lexErrors, parseErrors } = parse(xmlText); 43 | console.log(cst.children["element"][0].children["Name"][0].image); // -> note 44 | ``` 45 | 46 | ## Support 47 | 48 | Please open [issues](https://github.com/SAP/xml-tols/issues) on github. 49 | 50 | ## Contributing 51 | 52 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 53 | 54 | [cst]: https://en.wikipedia.org/wiki/Parse_tree 55 | -------------------------------------------------------------------------------- /packages/xml-toolkit/lib/extension.js: -------------------------------------------------------------------------------- 1 | const { readFile: readFileCallback } = require("fs"); 2 | const { resolve } = require("path"); 3 | const { promisify } = require("util"); 4 | const { workspace } = require("vscode"); 5 | const { LanguageClient, TransportKind } = require("vscode-languageclient"); 6 | const { SERVER_PATH } = require("@xml-tools/language-server"); 7 | 8 | const readFile = promisify(readFileCallback); 9 | 10 | let client; 11 | 12 | async function activate(context) { 13 | const debugOptions = { execArgv: ["--nolazy", "--inspect=6009"] }; 14 | 15 | const serverOptions = { 16 | run: { module: SERVER_PATH, transport: TransportKind.ipc }, 17 | debug: { 18 | module: SERVER_PATH, 19 | transport: TransportKind.ipc, 20 | options: debugOptions, 21 | }, 22 | }; 23 | 24 | const meta = JSON.parse( 25 | await readFile(resolve(context.extensionPath, "package.json"), "utf8") 26 | ); 27 | 28 | /** 29 | * @type {import("@xml-tools/language-server).ServerInitializationOptions} 30 | */ 31 | const initializationOptions = { 32 | consumer: meta.displayName, 33 | }; 34 | 35 | let clientOptions = { 36 | documentSelector: [{ scheme: "file", language: "xml" }], 37 | synchronize: { 38 | fileEvents: workspace.createFileSystemWatcher("**/*.xml"), 39 | }, 40 | initializationOptions: initializationOptions, 41 | }; 42 | 43 | client = new LanguageClient( 44 | "XMLforVSCode", 45 | "XML For VSCode", 46 | serverOptions, 47 | clientOptions 48 | ); 49 | 50 | client.start(); 51 | } 52 | 53 | function deactivate() { 54 | /* istanbul ignore if - scenario that cannot be reached in productive code */ 55 | if (!client) { 56 | return undefined; 57 | } 58 | return client.stop(); 59 | } 60 | 61 | module.exports = { 62 | activate: activate, 63 | deactivate: deactivate, 64 | }; 65 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/validators/attribute-value.js: -------------------------------------------------------------------------------- 1 | const { isRegExp, isArray, includes, has } = require("lodash"); 2 | const { tokenToOffsetPosition } = require("./utils"); 3 | 4 | /** 5 | * @param {XMLAttribute} attributeNode 6 | * @param {XSSAttribute }xssAttribute 7 | * 8 | * @returns {ValidationIssue[]} 9 | */ 10 | function validateAttributeValue(attributeNode, xssAttribute) { 11 | const issues = []; 12 | 13 | const valueDef = xssAttribute.value; 14 | 15 | // An XSS Attribute definition may not specify any constraints on a value 16 | if (has(xssAttribute, "value") === false) { 17 | return issues; 18 | } 19 | 20 | const actualValue = attributeNode.value; 21 | if (actualValue === null) { 22 | // we cannot validate a partial attribute AST without an actual value... 23 | return issues; 24 | } 25 | 26 | // This is always safe because at this point we know the attribute has a value 27 | const errPosition = tokenToOffsetPosition(attributeNode.syntax.value); 28 | /* istanbul ignore else defensive programming */ 29 | if (isRegExp(valueDef)) { 30 | if (valueDef.test(actualValue) === false) { 31 | issues.push({ 32 | msg: `Expecting Value matching <${valueDef.toString()}> but found <${actualValue}>`, 33 | node: attributeNode, 34 | severity: "error", 35 | position: errPosition, 36 | }); 37 | } 38 | } else if (isArray(valueDef)) { 39 | if (includes(valueDef, actualValue) === false) { 40 | issues.push({ 41 | msg: `Expecting one of <${valueDef.toString()}> but found <${actualValue}>`, 42 | node: attributeNode, 43 | severity: "error", 44 | position: errPosition, 45 | }); 46 | } 47 | } else { 48 | /* istanbul ignore next defensive programming */ 49 | throw Error("None Exhaustive Match"); 50 | } 51 | 52 | return issues; 53 | } 54 | 55 | module.exports = { 56 | validateAttributeValue: validateAttributeValue, 57 | }; 58 | -------------------------------------------------------------------------------- /packages/validation/README.md: -------------------------------------------------------------------------------- 1 | [![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/validation.svg)](https://www.npmjs.com/package/@xml-tools/validation) 2 | 3 | # @xml-tools/validation 4 | 5 | APIs which assist in implementing Validations / Diagnostics logic for XML. 6 | 7 | ## Installation 8 | 9 | With npm: 10 | 11 | - `npm install @xml-tools/validation` 12 | 13 | With Yarn 14 | 15 | - `yarn add @xml-tools/validation` 16 | 17 | ## Usage 18 | 19 | Please see the [TypeScript Definitions](./api.d.ts) for full API details. 20 | 21 | A simple usage example: 22 | 23 | ```javascript 24 | const { parse } = require("xml-tools/parser"); 25 | const { buildAst } = require("xml-tools/ast"); 26 | const { validate } = require("xml-tools/validation"); 27 | 28 | const xmlText = ` 29 | Bill 30 | 31 | `; 32 | 33 | const { cst, tokenVector } = parse(xmlText); 34 | const xmlDocAst = buildAst(cst, tokenVector); 35 | const issues = validate({ 36 | doc: xmlDocAst, 37 | validators: { 38 | element: [ 39 | (node) => { 40 | if (node.name === "note") { 41 | const hasFrom = node.subElements.find( 42 | (subNode) => subNode.name === "from" 43 | ); 44 | if (hasFrom === undefined) { 45 | return [ 46 | { 47 | msg: "A Note Element **must** have a `from` subElement", 48 | node: node, 49 | }, 50 | ]; 51 | } 52 | } 53 | return []; 54 | }, 55 | ], 56 | }, 57 | }); 58 | 59 | console.log(issues[0].msg); // -> "A Note Element **must** have a `from` subElement" 60 | // Issue position can be extracted from the relevant ASTNode. 61 | console.log(issues[0].node.position.endLine); // -> 3 62 | ``` 63 | 64 | ## Support 65 | 66 | Please open [issues](https://github.com/SAP/xml-tols/issues) on github. 67 | 68 | ## Contributing 69 | 70 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 71 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/path.js: -------------------------------------------------------------------------------- 1 | const { drop, map, forEach, first } = require("lodash"); 2 | 3 | /** 4 | * @param {XMLAttribute} attribNode 5 | * @param {SimpleSchema} schema 6 | */ 7 | function findAttributeXssDef(attribNode, schema) { 8 | const xssElement = findElementXssDef(attribNode.parent, schema); 9 | 10 | let xssAttribute = undefined; 11 | if (xssElement !== undefined) { 12 | const attributeName = attribNode.key; 13 | xssAttribute = xssElement.attributes[attributeName]; 14 | } 15 | 16 | return xssAttribute; 17 | } 18 | 19 | /** 20 | * @param {XMLElement} node 21 | * @param {SimpleSchema} schema 22 | */ 23 | function findElementXssDef(node, schema) { 24 | const ancestors = getAstNodeAncestors(node); 25 | const elementsPath = map(ancestors, "name"); 26 | 27 | const rootElement = first(elementsPath); 28 | // Root Element mis-match, The Schema cannot provide any attribute validations for this XML AST. 29 | if (rootElement !== schema.name) { 30 | return undefined; 31 | } 32 | let xssElement = schema; 33 | forEach(drop(elementsPath), (elemName) => { 34 | // traverse subElements 35 | xssElement = xssElement.elements[elemName]; 36 | if (xssElement === undefined) { 37 | return false; 38 | } 39 | }); 40 | 41 | return xssElement; 42 | } 43 | 44 | /** 45 | * @param {XMLAstNode} node 46 | * 47 | * @returns {XMLAstNode[]} - The Ancestors do not include the XMLDocument 48 | */ 49 | function getAstNodeAncestors(node) { 50 | const ancestors = []; 51 | ancestors.push(node); 52 | let currAncestor = node.parent; 53 | while ( 54 | currAncestor !== undefined && 55 | // The Simple Schema only starts at the root Element (not the Root Document). 56 | currAncestor.type !== "XMLDocument" 57 | ) { 58 | ancestors.push(currAncestor); 59 | currAncestor = currAncestor.parent; 60 | } 61 | ancestors.reverse(); 62 | 63 | return ancestors; 64 | } 65 | 66 | module.exports = { 67 | findAttributeXssDef: findAttributeXssDef, 68 | findElementXssDef: findElementXssDef, 69 | }; 70 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/get-content-assist.js: -------------------------------------------------------------------------------- 1 | const { attributeNameCompletion } = require("./content-assist/attribute-name"); 2 | const { 3 | attributeValueCompletion, 4 | } = require("./content-assist/attribute-value"); 5 | const { elementNameCompletion } = require("./content-assist/element-name"); 6 | const { findElementXssDef, findAttributeXssDef } = require("./path"); 7 | 8 | function getSchemaSuggestionsProviders(schema) { 9 | const attributeNameProvider = buildAttributeNameProvider(schema); 10 | const attributeValueProvider = buildAttributeValueProvider(schema); 11 | const elementNameProvider = buildElementNameProvider(schema); 12 | 13 | return { 14 | schemaElementNameCompletion: elementNameProvider, 15 | schemaAttributeNameCompletion: attributeNameProvider, 16 | schemaAttributeValueCompletion: attributeValueProvider, 17 | }; 18 | } 19 | 20 | /** 21 | * @param {SimpleSchema} schema 22 | */ 23 | function buildAttributeNameProvider(schema) { 24 | return ({ element, prefix }) => { 25 | const xssElementDef = findElementXssDef(element, schema); 26 | if (xssElementDef !== undefined) { 27 | return attributeNameCompletion(element, xssElementDef, prefix); 28 | } else { 29 | return []; 30 | } 31 | }; 32 | } 33 | 34 | /** 35 | * @param {SimpleSchema} schema 36 | */ 37 | function buildElementNameProvider(schema) { 38 | return ({ element, prefix }) => { 39 | // Note we are finding the definition for the element's parent 40 | // Because the information on possible sibling elements exists there... 41 | const xssElementDef = findElementXssDef(element.parent, schema); 42 | if (xssElementDef !== undefined) { 43 | return elementNameCompletion(element.parent, xssElementDef, prefix); 44 | } else { 45 | return []; 46 | } 47 | }; 48 | } 49 | 50 | /** 51 | * @param {SimpleSchema} schema 52 | */ 53 | function buildAttributeValueProvider(schema) { 54 | return ({ attribute, prefix }) => { 55 | const attributeXssDef = findAttributeXssDef(attribute, schema); 56 | return attributeValueCompletion(attribute, attributeXssDef, prefix); 57 | }; 58 | } 59 | 60 | module.exports = { 61 | getSchemaSuggestionsProviders: getSchemaSuggestionsProviders, 62 | }; 63 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/invalid/element-closing-name/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cst: { 3 | name: "document", 4 | children: { 5 | element: [ 6 | { 7 | name: "element", 8 | children: { 9 | OPEN: [ 10 | { image: "<", startOffset: 0, endOffset: 0, tokenType: "OPEN" }, 11 | ], 12 | Name: [ 13 | { 14 | image: "note", 15 | startOffset: 1, 16 | endOffset: 4, 17 | tokenType: "Name", 18 | }, 19 | ], 20 | START_CLOSE: [ 21 | { image: ">", startOffset: 5, endOffset: 5, tokenType: "CLOSE" }, 22 | ], 23 | content: [ 24 | { 25 | name: "content", 26 | children: {}, 27 | location: { startOffset: null, endOffset: null }, 28 | }, 29 | ], 30 | SLASH_OPEN: [ 31 | { 32 | image: "", 49 | startOffset: 18, 50 | endOffset: 18, 51 | tokenType: "CLOSE", 52 | }, 53 | ], 54 | }, 55 | location: { startOffset: 0, endOffset: 18 }, 56 | }, 57 | ], 58 | misc: [ 59 | { 60 | name: "misc", 61 | children: { 62 | SEA_WS: [ 63 | { 64 | image: "\n", 65 | startOffset: 19, 66 | endOffset: 19, 67 | tokenType: "SEA_WS", 68 | }, 69 | ], 70 | }, 71 | location: { startOffset: 19, endOffset: 19 }, 72 | }, 73 | ], 74 | }, 75 | location: { startOffset: 0, endOffset: 19 }, 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /scripts/trigger-release.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | const { includes } = require("lodash"); 3 | const simpleGit = require("simple-git/promise"); 4 | 5 | const rootDir = resolve(__dirname, ".."); 6 | const git = simpleGit(rootDir); 7 | 8 | /** 9 | * Github Releases are triggered when a new version (tag) of the VSCode extension 10 | * is pushed. **however** there seems to be a bug in either CircleCI or github.com 11 | * Which causes circleCI to **not** trigger tag builds when many tags are pushed at once. 12 | * Therefore we are manually deleting and re-pushing the VSCode extension tag 13 | */ 14 | async function triggerGHReleasesPublish() { 15 | const log = await git.log(); 16 | const latestCommit = log.latest; 17 | const vscodeExtResult = /xml-toolkit@\d+(?:\.\d+)*/.exec(latestCommit.body); 18 | if (vscodeExtResult !== null) { 19 | const vscodeExtTag = vscodeExtResult[0]; 20 | console.log(`deleting remote tag ${vscodeExtTag}`); 21 | await git.push(["--delete", "origin", vscodeExtTag]); 22 | console.log(`pushing tag ${vscodeExtTag}`); 23 | await git.push("origin", vscodeExtTag); 24 | } 25 | } 26 | 27 | /** 28 | * `lerna version` pushes a tag per each new package version. 29 | * However we need a single unique tag to trigger the `lerna publish --from-git` on circle-CI 30 | * This is why we delete and push the `RELEASE` as part of our release process. 31 | */ 32 | async function triggerNPMPublish() { 33 | const allTags = (await git.tags()).all; 34 | if (includes(allTags, "RELEASE")) { 35 | console.log("deleting old local Tag"); 36 | await git.tag(["-d", "RELEASE"]); 37 | } 38 | console.log("creating new local Tag"); 39 | await git.tag(["RELEASE"]); 40 | 41 | try { 42 | console.log("trying to delete old remote Tag"); 43 | await git.push(["--delete", "origin", "RELEASE"]); 44 | } catch (e) { 45 | console.log(e.message); 46 | } 47 | 48 | console.log("pushing new remote Tag"); 49 | await git.push("origin", "RELEASE"); 50 | } 51 | 52 | async function main() { 53 | await triggerNPMPublish(); 54 | // push tags slowly and one at a time to ensure CircleCi recognizes them. 55 | await new Promise((r) => setTimeout(r, 1000)); 56 | await triggerGHReleasesPublish(); 57 | } 58 | 59 | main(); 60 | -------------------------------------------------------------------------------- /packages/ast-position/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [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) 7 | 8 | **Note:** Version bump only for package @xml-tools/ast-position 9 | 10 | ## [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) 11 | 12 | **Note:** Version bump only for package @xml-tools/ast-position 13 | 14 | ## [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) 15 | 16 | **Note:** Version bump only for package @xml-tools/ast-position 17 | 18 | ## [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) 19 | 20 | ### Bug Fixes 21 | 22 | - missing lib folder in npm packages ([ffed3c2](https://github.com/sap/xml-tools/commit/ffed3c2c54c70aea8b9ded0d53786382bc190cc5)) 23 | 24 | ## [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) 25 | 26 | **Note:** Version bump only for package @xml-tools/ast-position 27 | 28 | ## [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) 29 | 30 | **Note:** Version bump only for package @xml-tools/ast-position 31 | 32 | ## [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) 33 | 34 | **Note:** Version bump only for package @xml-tools/ast-position 35 | 36 | # [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) 37 | 38 | ### Features 39 | 40 | - **ast-position:** rename `getAstNodeInPosition` -> astPositionAtOffset ([585253f](https://github.com/sap/xml-tools/commit/585253f)) 41 | 42 | ### BREAKING CHANGES 43 | 44 | - **ast-position:** rename `getAstNodeInPosition` -> astPositionAtOffset 45 | 46 | # 1.1.0 (2020-05-18) 47 | 48 | ### Features 49 | 50 | - **ast-position:** add AST position visitor ([#175](https://github.com/sap/xml-tools/issues/175)) ([9eb1253](https://github.com/sap/xml-tools/commit/9eb1253)) 51 | -------------------------------------------------------------------------------- /packages/ast/README.md: -------------------------------------------------------------------------------- 1 | [![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/ast.svg)](https://www.npmjs.com/package/@xml-tools/ast) 2 | 3 | # @xml-tools/ast 4 | 5 | Utilities for building and traversing an XML **A**bstract **S**yntax **T**ree ([AST][ast]). 6 | 7 | There are two things which distinguish this AST from most others ASTs: 8 | 9 | 1. This AST can represent a **partially valid** XML, in practice this means virtually all properties on 10 | the AST are optional and may have undefined values. 11 | 2. This AST contains **additional syntactic information** to enable additional linting & formatting flows, for example: 12 | - the original position and value of an attribute's value (including the quotes). 13 | - The original positions and values of an XMLElement's open/close names. 14 | 15 | The input for constructing the AST is a CST which is created by the [@xml-tools/parser](../parser) package. 16 | 17 | The AST structure is used as part of the input for the [@xml-tools/content-assist](../content-assist) APIs. 18 | 19 | ## Installation 20 | 21 | With npm: 22 | 23 | - `npm install @xml-tools/ast` 24 | 25 | With Yarn 26 | 27 | - `yarn add @xml-tools/ast` 28 | 29 | ## Usage 30 | 31 | Please see the [TypeScript Definitions](./api.d.ts) for full API details. 32 | 33 | A simple usage example: 34 | 35 | ```javascript 36 | const { parse } = require("@xml-tools/parser"); 37 | const { buildAst, accept } = require("@xml-tools/ast"); 38 | 39 | const xmlText = ` 40 | Bill 41 | Tim 42 | 43 | `; 44 | 45 | const { cst, tokenVector } = parse(xmlText); 46 | const xmlDocAst = buildAst(cst, tokenVector); 47 | console.log(xmlDocAst.rootElement.name); // -> note 48 | 49 | // A Visitor allows us to invoke actions on the XML ASTNodes without worrying about 50 | // The XML AST structure / traversal method. 51 | const printVisitor = { 52 | // Will be invoked once for each Element node in the AST. 53 | visitXMLElement: function (node) { 54 | console.log(node.name); 55 | }, 56 | 57 | // An XML AST Visitor may have other methods as well, see the api.d.ts file/ 58 | }; 59 | 60 | // Invoking the Visitor 61 | accept(xmlDocAst, printVisitor); // -> note, Bill, Tim 62 | ``` 63 | 64 | ## Support 65 | 66 | Please open [issues](https://github.com/SAP/xml-tols/issues) on github. 67 | 68 | ## Contributing 69 | 70 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 71 | -------------------------------------------------------------------------------- /packages/simple-schema/api.d.ts: -------------------------------------------------------------------------------- 1 | import { XMLAstNode, XMLAttribute, XMLElement } from "@xml-tools/ast"; 2 | import { 3 | ValidationIssue, 4 | AttributeValidator, 5 | ElementValidator, 6 | } from "@xml-tools/validation"; 7 | import { 8 | AttributeNameCompletion, 9 | AttributeValueCompletion, 10 | ElementContentCompletion, 11 | ElementNameCompletion, 12 | } from "@xml-tools/content-assist"; 13 | 14 | // Represents the root element in an XML document 15 | declare type SimpleSchema = XSSElement & { 16 | // There may only be a single top level element in XML 17 | cardinality: "single"; 18 | // The top level element in mandatory in XML 19 | require: true; 20 | }; 21 | 22 | declare type XSSElement = { 23 | required: boolean; 24 | cardinality: "many" | "single"; 25 | name: string; 26 | namespace?: string; 27 | 28 | attributesType?: "open" | "closed"; 29 | attributes: Record; 30 | 31 | elementsType?: "open" | "closed"; 32 | elements: Record; 33 | // TODO: textValue definition (for simple pure text only?) 34 | }; 35 | 36 | declare type XSSAttribute = { 37 | required: boolean; 38 | key: string; 39 | value?: XSSValue; 40 | }; 41 | 42 | // TODO: could be expended to support more primitive types and also complex types such as `union type` 43 | declare type XSSValue = RegExp | XSSValueEnum; 44 | 45 | declare type XSSValueEnum = string[]; 46 | 47 | declare function getSchemaValidators( 48 | schema: SimpleSchema 49 | ): { 50 | attribute: AttributeValidator; 51 | element: ElementValidator; 52 | }; 53 | 54 | interface CompletionSuggestion { 55 | text: string; 56 | label?: string; 57 | docs?: string; 58 | commitCharacter?: string; 59 | isNamespace?: boolean; 60 | /** 61 | * A measure of how certain we are about this suggestion's relevance. 62 | * This value could be used to: 63 | * - Filter out less relevant suggestions when there are too many possible suggestions. 64 | * - Sort the suggestions by relevance. 65 | */ 66 | confidence?: number; 67 | } 68 | 69 | declare function getSchemaSuggestionsProviders( 70 | schema: SimpleSchema 71 | ): { 72 | // TBD in the future... 73 | // schemaElementContentCompletion: ElementContentCompletion; 74 | schemaElementNameCompletion: ElementNameCompletion; 75 | schemaAttributeNameCompletion: AttributeNameCompletion; 76 | schemaAttributeValueCompletion: AttributeValueCompletion; 77 | }; 78 | -------------------------------------------------------------------------------- /packages/constraints/test/constraints/unique-attribute-keys-spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { parse } = require("@xml-tools/parser"); 3 | const { buildAst } = require("@xml-tools/ast"); 4 | const { 5 | validateUniqueAttributeKeys, 6 | } = require("../../lib/constraints/unique-attribute-keys"); 7 | 8 | describe("unique attribute keys constraint", () => { 9 | context("no issues", () => { 10 | it("will not detect any issues for a valid XML with attributes", () => { 11 | const xmlText = ``; 12 | const { cst, tokenVector } = parse(xmlText); 13 | const document = buildAst(cst, tokenVector); 14 | const element = document.rootElement; 15 | const result = validateUniqueAttributeKeys(element); 16 | 17 | expect(result).to.be.empty; 18 | }); 19 | 20 | it("will not detect any issues for a valid XML without attributes", () => { 21 | const xmlText = ``; 22 | const { cst, tokenVector } = parse(xmlText); 23 | const document = buildAst(cst, tokenVector); 24 | const element = document.rootElement; 25 | const result = validateUniqueAttributeKeys(element); 26 | 27 | expect(result).to.be.empty; 28 | }); 29 | }); 30 | 31 | context("positive constraint checks", () => { 32 | it("detects a duplicate attribute keys", () => { 33 | const xmlText = ``; 34 | 35 | const { cst, tokenVector } = parse(xmlText); 36 | const document = buildAst(cst, tokenVector); 37 | const element = document.rootElement; 38 | const result = validateUniqueAttributeKeys(element); 39 | 40 | const donaldNameAttrib = element.attributes[0]; 41 | const bambaNameAttrib = element.attributes[3]; 42 | 43 | expect(result).to.have.lengthOf(2); 44 | expect(result).to.deep.include.members([ 45 | { 46 | msg: 'duplicate attribute: "name"', 47 | severity: "error", 48 | node: donaldNameAttrib, 49 | position: { 50 | startOffset: donaldNameAttrib.syntax.key.startOffset, 51 | endOffset: donaldNameAttrib.syntax.key.endOffset, 52 | }, 53 | }, 54 | { 55 | msg: 'duplicate attribute: "name"', 56 | severity: "error", 57 | node: bambaNameAttrib, 58 | position: { 59 | startOffset: bambaNameAttrib.syntax.key.startOffset, 60 | endOffset: bambaNameAttrib.syntax.key.endOffset, 61 | }, 62 | }, 63 | ]); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /packages/common/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [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) 7 | 8 | **Note:** Version bump only for package @xml-tools/common 9 | 10 | ## [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) 11 | 12 | **Note:** Version bump only for package @xml-tools/common 13 | 14 | ## [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) 15 | 16 | ### Bug Fixes 17 | 18 | - missing lib folder in npm packages ([ffed3c2](https://github.com/sap/xml-tools/commit/ffed3c2c54c70aea8b9ded0d53786382bc190cc5)) 19 | 20 | ## [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) 21 | 22 | **Note:** Version bump only for package @xml-tools/common 23 | 24 | ## [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) 25 | 26 | **Note:** Version bump only for package @xml-tools/common 27 | 28 | ## [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) 29 | 30 | **Note:** Version bump only for package @xml-tools/common 31 | 32 | # [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) 33 | 34 | ### Features 35 | 36 | - **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)) 37 | 38 | ## [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) 39 | 40 | **Note:** Version bump only for package @xml-tools/common 41 | 42 | ## [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) 43 | 44 | ### Bug Fixes 45 | 46 | - add npmignore file to each package ([5bbf209](https://github.com/sap/xml-tools/commit/5bbf209)) 47 | 48 | ## [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) 49 | 50 | **Note:** Version bump only for package @xml-tools/common 51 | 52 | ## [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) 53 | 54 | **Note:** Version bump only for package @xml-tools/common 55 | 56 | ## [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) 57 | 58 | **Note:** Version bump only for package @xml-tools/common 59 | 60 | ## 0.0.1 (2019-12-05) 61 | 62 | **Note:** Version bump only for package @xml-tools/common 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": { 5 | "packages": [ 6 | "packages/*" 7 | ], 8 | "nohoist": [ 9 | "xml-toolkit/@xml-tools/language-server", 10 | "xml-toolkit/@xml-tools/language-server/**" 11 | ] 12 | }, 13 | "scripts": { 14 | "release:version": "lerna version && yarn run release:trigger", 15 | "release:trigger": "node ./scripts/trigger-release", 16 | "release:publish": "lerna publish from-git --yes", 17 | "ci": "npm-run-all format:validate lint:validate ci:subpackages coverage:merge legal:*", 18 | "format:fix": "prettier --write --ignore-path .gitignore \"**/*.@(ts|js|json|md)\"", 19 | "format:validate": "prettier --check --ignore-path .gitignore \"**/*.@(ts|js|json|md)\"", 20 | "lint:fix": "eslint . --ext=js --fix --max-warnings=0 --ignore-path=.gitignore", 21 | "lint:validate": "eslint . --ext=js --max-warnings=0 --ignore-path=.gitignore", 22 | "ci:subpackages": "lerna run ci", 23 | "test": "lerna run test", 24 | "coverage": "lerna run coverage", 25 | "coverage:merge": "node ./scripts/merge-coverage", 26 | "clean": "lerna run clean", 27 | "legal:delete": "lerna exec \"shx rm -rf .reuse LICENSES\" || true", 28 | "legal:copy": "lerna exec \"shx cp -r ../../.reuse .reuse && shx cp -r ../../LICENSES LICENSES\"" 29 | }, 30 | "devDependencies": { 31 | "@types/chai": "4.2.14", 32 | "@types/mocha": "8.2.0", 33 | "@types/node": "14.14.19", 34 | "eslint": "7.27.0", 35 | "eslint-config-prettier": "7.2.0", 36 | "eslint-plugin-eslint-comments": "3.2.0", 37 | "coveralls": "3.1.0", 38 | "make-dir": "3.1.0", 39 | "glob": "7.1.6", 40 | "chai": "4.3.4", 41 | "husky": "4.3.8", 42 | "lerna": "4.0.0", 43 | "lint-staged": "11.0.0", 44 | "mocha": "7.2.0", 45 | "npm-run-all": "4.1.5", 46 | "nyc": "15.1.0", 47 | "prettier": "2.2.1", 48 | "typescript": "4.1.3", 49 | "cz-conventional-changelog": "3.3.0", 50 | "@commitlint/cli": "11.0.0", 51 | "@commitlint/config-conventional": "11.0.0", 52 | "rimraf": "3.0.2", 53 | "simple-git": "2.31.0", 54 | "fs-extra": "9.1.0", 55 | "webpack": "^5.61.0", 56 | "webpack-cli": "^4.9.1", 57 | "shx": "0.3.3" 58 | }, 59 | "prettier": { 60 | "plugins": [], 61 | "pluginSearchDirs": [] 62 | }, 63 | "husky": { 64 | "hooks": { 65 | "pre-commit": "lint-staged", 66 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 67 | } 68 | }, 69 | "lint-staged": { 70 | "*.{ts,js,md,json}": [ 71 | "prettier --write" 72 | ], 73 | "*.{js}": [ 74 | "eslint --fix --max-warnings=0 --ignore-pattern=!.*" 75 | ] 76 | }, 77 | "config": { 78 | "commitizen": { 79 | "path": "./node_modules/cz-conventional-changelog" 80 | } 81 | }, 82 | "commitlint": { 83 | "extends": [ 84 | "@commitlint/config-conventional" 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/content-assist/README.md: -------------------------------------------------------------------------------- 1 | [![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/content-assist.svg)](https://www.npmjs.com/package/@xml-tools/content-assist) 2 | 3 | # @xml-tools/content-assist 4 | 5 | APIs which assist in implementing content assist (auto-complete) logic for XML. 6 | This package handles the identification of the specific content assist **scenario and context**. 7 | Given this scenario & context information a user of this package can then implement 8 | their custom content assist logic. 9 | 10 | For example: 11 | 12 | Given the following XML and a content assist request at the `⇶` sign: 13 | 14 | ```xml 15 | 16 |
17 | 18 | 19 | 20 | ``` 21 | 22 | A **Attribute Value** content assist scenario would be identified. 23 | With a **prefix** `New` and references to the ASTNodes 24 | of the relevant attribute(city) and the containing element (address). 25 | 26 | - Note that an ASTNode possesses a `parent` property, so the full structure is accessible. 27 | 28 | ## Installation 29 | 30 | With npm: 31 | 32 | - `npm install @xml-tools/content-assist` 33 | 34 | With Yarn 35 | 36 | - `yarn add @xml-tools/content-assist` 37 | 38 | ## Usage 39 | 40 | Please see the [TypeScript Definitions](./api.d.ts) for full API details. 41 | 42 | A simple usage example: 43 | 44 | ```javascript 45 | const { parse } = require("@xml-tools/parser"); 46 | const { buildAst } = require("@xml-tools/ast"); 47 | const { getSuggestions } = require("@xml-tools/content-assist"); 48 | 49 | const xmlText = ` 50 | Bill 51 | Bill 53 | 54 | `; 55 | 56 | // A-lot of contextual information is needed to compute the content assist context. 57 | const { cst, tokenVector } = parse(xmlText); 58 | const xmlDocAst = buildAst(cst, tokenVector); 59 | 60 | const suggestions = getSuggestions({ 61 | ast: xmlDocAst, 62 | cst: cst, 63 | tokVector: tokenVector, 64 | offset: 66, // Right after the ' { 70 | const suggestions = []; 71 | if (element.parent.name === "note" && "address".startsWith(prefix)) { 72 | suggestions.push({ 73 | text: "address".slice(prefix.length), 74 | label: "address", 75 | }); 76 | } 77 | return suggestions; 78 | }, 79 | ], 80 | }, 81 | }); 82 | 83 | console.log(suggestions[0].text); // -> dress 84 | ``` 85 | 86 | ## Support 87 | 88 | Please open [issues](https://github.com/SAP/xml-tols/issues) on github. 89 | 90 | ## Contributing 91 | 92 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 93 | 94 | [ast]: https://en.wikipedia.org/wiki/Abstract_syntax_tree 95 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/get-validators.js: -------------------------------------------------------------------------------- 1 | const { validateAttributeValue } = require("./validators/attribute-value"); 2 | 3 | const { 4 | validateDuplicateSubElements, 5 | } = require("./validators/duplicate-sub-elements"); 6 | 7 | const { 8 | validateRequiredAttributes, 9 | } = require("./validators/required-attributes"); 10 | 11 | const { 12 | validateRequiredSubElements, 13 | } = require("./validators/required-sub-elements"); 14 | 15 | const { 16 | validateUnknownAttributes, 17 | } = require("./validators/unknown-attributes"); 18 | 19 | const { 20 | validateUnknownSubElements, 21 | } = require("./validators/unknown-sub-elements"); 22 | 23 | const { findAttributeXssDef, findElementXssDef } = require("./path"); 24 | 25 | function getSchemaValidators(schema) { 26 | const attributeValidator = buildAttributeValidator(schema); 27 | const elementValidator = buildElementValidator(schema); 28 | 29 | return { 30 | attribute: attributeValidator, 31 | element: elementValidator, 32 | }; 33 | } 34 | 35 | /** 36 | * @param {SimpleSchema} schema 37 | */ 38 | function buildAttributeValidator(schema) { 39 | return (attributeNode) => { 40 | let issues = []; 41 | const xssAttributeDef = findAttributeXssDef(attributeNode, schema); 42 | 43 | if (xssAttributeDef !== undefined) { 44 | const attributeValueIssues = validateAttributeValue( 45 | attributeNode, 46 | xssAttributeDef 47 | ); 48 | issues = issues.concat(attributeValueIssues); 49 | } 50 | 51 | return issues; 52 | }; 53 | } 54 | 55 | /** 56 | * @param {SimpleSchema} schema 57 | */ 58 | function buildElementValidator(schema) { 59 | return (elementNode) => { 60 | let issues = []; 61 | const xssElementDef = findElementXssDef(elementNode, schema); 62 | 63 | if (xssElementDef !== undefined) { 64 | const duplicateElementsIssues = validateDuplicateSubElements( 65 | elementNode, 66 | xssElementDef 67 | ); 68 | const requiredAttributesIssues = validateRequiredAttributes( 69 | elementNode, 70 | xssElementDef 71 | ); 72 | const requiredSubElementsIssues = validateRequiredSubElements( 73 | elementNode, 74 | xssElementDef 75 | ); 76 | const unknownAttributesIssues = validateUnknownAttributes( 77 | elementNode, 78 | xssElementDef 79 | ); 80 | const unknownSubElementsIssues = validateUnknownSubElements( 81 | elementNode, 82 | xssElementDef 83 | ); 84 | issues = issues.concat( 85 | duplicateElementsIssues, 86 | requiredAttributesIssues, 87 | requiredSubElementsIssues, 88 | unknownAttributesIssues, 89 | unknownSubElementsIssues 90 | ); 91 | } 92 | 93 | return issues; 94 | }; 95 | } 96 | 97 | module.exports = { 98 | getSchemaValidators: getSchemaValidators, 99 | }; 100 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/elements/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [], 9 | subElements: [ 10 | { 11 | type: "XMLElement", 12 | namespaces: {}, 13 | name: "to", 14 | attributes: [], 15 | subElements: [], 16 | textContents: [ 17 | { 18 | type: "XMLTextContent", 19 | position: { startOffset: 15, endOffset: 18 }, 20 | text: "Bill", 21 | }, 22 | ], 23 | position: { startOffset: 11, endOffset: 23 }, 24 | syntax: { 25 | openName: { image: "to", startOffset: 12, endOffset: 13 }, 26 | closeName: { image: "to", startOffset: 21, endOffset: 22 }, 27 | isSelfClosing: false, 28 | openBody: { startOffset: 11, endOffset: 14 }, 29 | closeBody: { startOffset: 19, endOffset: 23 }, 30 | attributesRange: { startOffset: 15, endOffset: 13 }, 31 | }, 32 | }, 33 | { 34 | type: "XMLElement", 35 | namespaces: {}, 36 | name: "from", 37 | attributes: [], 38 | subElements: [], 39 | textContents: [ 40 | { 41 | type: "XMLTextContent", 42 | position: { startOffset: 35, endOffset: 37 }, 43 | text: "Tim", 44 | }, 45 | ], 46 | position: { startOffset: 29, endOffset: 44 }, 47 | syntax: { 48 | openName: { image: "from", startOffset: 30, endOffset: 33 }, 49 | closeName: { image: "from", startOffset: 40, endOffset: 43 }, 50 | isSelfClosing: false, 51 | openBody: { startOffset: 29, endOffset: 34 }, 52 | closeBody: { startOffset: 38, endOffset: 44 }, 53 | attributesRange: { startOffset: 35, endOffset: 33 }, 54 | }, 55 | }, 56 | ], 57 | textContents: [ 58 | { 59 | type: "XMLTextContent", 60 | position: { startOffset: 6, endOffset: 10 }, 61 | text: "\n ", 62 | }, 63 | { 64 | type: "XMLTextContent", 65 | position: { startOffset: 24, endOffset: 28 }, 66 | text: "\n ", 67 | }, 68 | { 69 | type: "XMLTextContent", 70 | position: { startOffset: 45, endOffset: 45 }, 71 | text: "\n", 72 | }, 73 | ], 74 | position: { startOffset: 0, endOffset: 52 }, 75 | syntax: { 76 | openName: { image: "note", startOffset: 1, endOffset: 4 }, 77 | closeName: { image: "note", startOffset: 48, endOffset: 51 }, 78 | isSelfClosing: false, 79 | openBody: { startOffset: 0, endOffset: 5 }, 80 | closeBody: { startOffset: 46, endOffset: 52 }, 81 | attributesRange: { startOffset: 6, endOffset: 4 }, 82 | }, 83 | }, 84 | position: { startOffset: 0, endOffset: 53 }, 85 | }, 86 | }; 87 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/elements-short-form/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [], 9 | subElements: [ 10 | { 11 | type: "XMLElement", 12 | namespaces: {}, 13 | name: "to", 14 | attributes: [], 15 | subElements: [], 16 | textContents: [ 17 | { 18 | type: "XMLTextContent", 19 | position: { startOffset: 15, endOffset: 18 }, 20 | text: "Bill", 21 | }, 22 | ], 23 | position: { startOffset: 11, endOffset: 23 }, 24 | syntax: { 25 | openName: { image: "to", startOffset: 12, endOffset: 13 }, 26 | closeName: { image: "to", startOffset: 21, endOffset: 22 }, 27 | isSelfClosing: false, 28 | openBody: { startOffset: 11, endOffset: 14 }, 29 | closeBody: { startOffset: 19, endOffset: 23 }, 30 | attributesRange: { startOffset: 15, endOffset: 13 }, 31 | }, 32 | }, 33 | { 34 | type: "XMLElement", 35 | namespaces: {}, 36 | name: "from", 37 | attributes: [ 38 | { 39 | type: "XMLAttribute", 40 | position: { startOffset: 35, endOffset: 44 }, 41 | key: "name", 42 | value: "tim", 43 | syntax: { 44 | key: { image: "name", startOffset: 35, endOffset: 38 }, 45 | value: { image: '"tim"', startOffset: 40, endOffset: 44 }, 46 | }, 47 | }, 48 | ], 49 | subElements: [], 50 | textContents: [], 51 | position: { startOffset: 29, endOffset: 46 }, 52 | syntax: { 53 | openName: { image: "from", startOffset: 30, endOffset: 33 }, 54 | isSelfClosing: true, 55 | openBody: { startOffset: 29, endOffset: 46 }, 56 | attributesRange: { startOffset: 35, endOffset: 44 }, 57 | }, 58 | }, 59 | ], 60 | textContents: [ 61 | { 62 | type: "XMLTextContent", 63 | position: { startOffset: 6, endOffset: 10 }, 64 | text: "\n ", 65 | }, 66 | { 67 | type: "XMLTextContent", 68 | position: { startOffset: 24, endOffset: 28 }, 69 | text: "\n ", 70 | }, 71 | { 72 | type: "XMLTextContent", 73 | position: { startOffset: 47, endOffset: 47 }, 74 | text: "\n", 75 | }, 76 | ], 77 | position: { startOffset: 0, endOffset: 54 }, 78 | syntax: { 79 | openName: { image: "note", startOffset: 1, endOffset: 4 }, 80 | closeName: { image: "note", startOffset: 50, endOffset: 53 }, 81 | isSelfClosing: false, 82 | openBody: { startOffset: 0, endOffset: 5 }, 83 | closeBody: { startOffset: 48, endOffset: 54 }, 84 | attributesRange: { startOffset: 6, endOffset: 4 }, 85 | }, 86 | }, 87 | position: { startOffset: 0, endOffset: 55 }, 88 | }, 89 | }; 90 | -------------------------------------------------------------------------------- /packages/ast/test/utils.js: -------------------------------------------------------------------------------- 1 | const { forEach } = require("lodash"); 2 | const { expect } = require("chai"); 3 | 4 | const { accept } = require("../"); 5 | 6 | function modifyAstForAssertions(astNode) { 7 | // Avoid comparing cyclic structures 8 | accept(astNode, parentRemoverVisitor); 9 | // Reduce verbosity of assertions 10 | accept(astNode, positionReducerVisitor); 11 | } 12 | 13 | /** 14 | * @type {XMLAstVisitor} 15 | */ 16 | const parentRemoverVisitor = { 17 | /* eslint-disable-next-line no-unused-vars -- consistent signatures in visitor methods even if they are empty placeholders */ 18 | visitXMLDocument: (node) => { 19 | // top level XML Doc does not have a parent... 20 | }, 21 | visitXMLProlog: (node) => { 22 | delete node.parent; 23 | }, 24 | visitXMLPrologAttribute: (node) => { 25 | delete node.parent; 26 | }, 27 | visitXMLElement: (node) => { 28 | delete node.parent; 29 | }, 30 | visitXMLAttribute: (node) => { 31 | delete node.parent; 32 | }, 33 | visitXMLTextContent: (node) => { 34 | delete node.parent; 35 | }, 36 | }; 37 | 38 | /** 39 | * @type {XMLAstVisitor} 40 | */ 41 | const positionReducerVisitor = { 42 | visitXMLDocument: (node) => { 43 | reduceNodePoseInfo(node); 44 | }, 45 | visitXMLProlog: (node) => { 46 | reduceNodePoseInfo(node); 47 | }, 48 | visitXMLPrologAttribute: (node) => { 49 | reduceNodePoseInfo(node); 50 | }, 51 | visitXMLElement: (node) => { 52 | reduceNodePoseInfo(node); 53 | }, 54 | visitXMLAttribute: (node) => { 55 | reduceNodePoseInfo(node); 56 | }, 57 | visitXMLTextContent: (node) => { 58 | reduceNodePoseInfo(node); 59 | }, 60 | }; 61 | 62 | function reduceNodePoseInfo(node) { 63 | reducePositionInfo(node.position); 64 | 65 | forEach(node.syntax, (tok) => { 66 | reducePositionInfo(tok); 67 | }); 68 | } 69 | 70 | function reducePositionInfo(pos) { 71 | delete pos.startColumn; 72 | delete pos.endColumn; 73 | delete pos.startLine; 74 | delete pos.endLine; 75 | } 76 | 77 | function assertParentPropsAreValid(astNode) { 78 | accept(astNode, parentPropsValidatorVisitor); 79 | } 80 | 81 | /** 82 | * @type {XMLAstVisitor} 83 | */ 84 | const parentPropsValidatorVisitor = { 85 | /* eslint-disable-next-line no-unused-vars -- consistent signatures in visitor methods even if they are empty placeholders */ 86 | visitXMLDocument: (node) => { 87 | // top level XML Doc does not have a parent... 88 | }, 89 | visitXMLProlog: (node) => { 90 | expect(node.parent.prolog).to.eql(node); 91 | }, 92 | visitXMLElement: (node) => { 93 | const parent = node.parent; 94 | if (parent.type === "XMLDocument") { 95 | expect(parent.rootElement).to.eql(node); 96 | } else { 97 | expect(parent.subElements).to.include(node); 98 | } 99 | }, 100 | visitXMLAttribute: (node) => { 101 | expect(node.parent.attributes).to.include(node); 102 | }, 103 | visitXMLTextContent: (node) => { 104 | expect(node.parent.textContents).to.include(node); 105 | }, 106 | }; 107 | 108 | module.exports = { 109 | modifyAstForAssertions: modifyAstForAssertions, 110 | positionReducerVisitor: positionReducerVisitor, 111 | assertParentPropsAreValid: assertParentPropsAreValid, 112 | }; 113 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-empty-last/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [], 9 | subElements: [ 10 | { 11 | type: "XMLElement", 12 | namespaces: {}, 13 | name: "to", 14 | attributes: [], 15 | subElements: [], 16 | textContents: [ 17 | { 18 | type: "XMLTextContent", 19 | position: { startOffset: 15, endOffset: 18 }, 20 | text: "bobi", 21 | }, 22 | ], 23 | position: { startOffset: 11, endOffset: 23 }, 24 | syntax: { 25 | openName: { image: "to", startOffset: 12, endOffset: 13 }, 26 | closeName: { image: "to", startOffset: 21, endOffset: 22 }, 27 | isSelfClosing: false, 28 | openBody: { startOffset: 11, endOffset: 14 }, 29 | closeBody: { startOffset: 19, endOffset: 23 }, 30 | attributesRange: { startOffset: 15, endOffset: 13 }, 31 | }, 32 | }, 33 | { 34 | type: "XMLElement", 35 | namespaces: {}, 36 | name: "from", 37 | attributes: [], 38 | subElements: [], 39 | textContents: [ 40 | { 41 | type: "XMLTextContent", 42 | position: { startOffset: 35, endOffset: 38 }, 43 | text: "john", 44 | }, 45 | ], 46 | position: { startOffset: 29, endOffset: 45 }, 47 | syntax: { 48 | openName: { image: "from", startOffset: 30, endOffset: 33 }, 49 | closeName: { image: "from", startOffset: 41, endOffset: 44 }, 50 | isSelfClosing: false, 51 | openBody: { startOffset: 29, endOffset: 34 }, 52 | closeBody: { startOffset: 39, endOffset: 45 }, 53 | attributesRange: { startOffset: 35, endOffset: 33 }, 54 | }, 55 | }, 56 | { 57 | type: "XMLElement", 58 | namespaces: {}, 59 | name: null, 60 | attributes: [], 61 | subElements: [], 62 | textContents: [], 63 | position: { startOffset: 51, endOffset: 51 }, 64 | syntax: {}, 65 | }, 66 | ], 67 | textContents: [ 68 | { 69 | type: "XMLTextContent", 70 | position: { startOffset: 6, endOffset: 10 }, 71 | text: "\n ", 72 | }, 73 | { 74 | type: "XMLTextContent", 75 | position: { startOffset: 24, endOffset: 28 }, 76 | text: "\n ", 77 | }, 78 | { 79 | type: "XMLTextContent", 80 | position: { startOffset: 46, endOffset: 50 }, 81 | text: "\n ", 82 | }, 83 | ], 84 | position: { startOffset: 0, endOffset: 59 }, 85 | syntax: { 86 | openName: { image: "note", startOffset: 1, endOffset: 4 }, 87 | closeName: { image: "note", startOffset: 55, endOffset: 58 }, 88 | isSelfClosing: false, 89 | openBody: { startOffset: 0, endOffset: 5 }, 90 | closeBody: { startOffset: 53, endOffset: 59 }, 91 | attributesRange: { startOffset: 6, endOffset: 4 }, 92 | }, 93 | }, 94 | position: { startOffset: 0, endOffset: 60 }, 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-empty-middle/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [], 9 | subElements: [ 10 | { 11 | type: "XMLElement", 12 | namespaces: {}, 13 | name: "to", 14 | attributes: [], 15 | subElements: [], 16 | textContents: [ 17 | { 18 | type: "XMLTextContent", 19 | position: { startOffset: 15, endOffset: 18 }, 20 | text: "bobi", 21 | }, 22 | ], 23 | position: { startOffset: 11, endOffset: 23 }, 24 | syntax: { 25 | openName: { image: "to", startOffset: 12, endOffset: 13 }, 26 | closeName: { image: "to", startOffset: 21, endOffset: 22 }, 27 | isSelfClosing: false, 28 | openBody: { startOffset: 11, endOffset: 14 }, 29 | closeBody: { startOffset: 19, endOffset: 23 }, 30 | attributesRange: { startOffset: 15, endOffset: 13 }, 31 | }, 32 | }, 33 | { 34 | type: "XMLElement", 35 | namespaces: {}, 36 | name: null, 37 | attributes: [], 38 | subElements: [], 39 | textContents: [], 40 | position: { startOffset: 29, endOffset: 29 }, 41 | syntax: {}, 42 | }, 43 | { 44 | type: "XMLElement", 45 | namespaces: {}, 46 | name: "from", 47 | attributes: [], 48 | subElements: [], 49 | textContents: [ 50 | { 51 | type: "XMLTextContent", 52 | position: { startOffset: 41, endOffset: 44 }, 53 | text: "john", 54 | }, 55 | ], 56 | position: { startOffset: 35, endOffset: 51 }, 57 | syntax: { 58 | openName: { image: "from", startOffset: 36, endOffset: 39 }, 59 | closeName: { image: "from", startOffset: 47, endOffset: 50 }, 60 | isSelfClosing: false, 61 | openBody: { startOffset: 35, endOffset: 40 }, 62 | closeBody: { startOffset: 45, endOffset: 51 }, 63 | attributesRange: { startOffset: 41, endOffset: 39 }, 64 | }, 65 | }, 66 | ], 67 | textContents: [ 68 | { 69 | type: "XMLTextContent", 70 | position: { startOffset: 6, endOffset: 10 }, 71 | text: "\n ", 72 | }, 73 | { 74 | type: "XMLTextContent", 75 | position: { startOffset: 24, endOffset: 28 }, 76 | text: "\n ", 77 | }, 78 | { 79 | type: "XMLTextContent", 80 | position: { startOffset: 52, endOffset: 52 }, 81 | text: "\n", 82 | }, 83 | ], 84 | position: { startOffset: 0, endOffset: 59 }, 85 | syntax: { 86 | openName: { image: "note", startOffset: 1, endOffset: 4 }, 87 | closeName: { image: "note", startOffset: 55, endOffset: 58 }, 88 | isSelfClosing: false, 89 | openBody: { startOffset: 0, endOffset: 5 }, 90 | closeBody: { startOffset: 53, endOffset: 59 }, 91 | attributesRange: { startOffset: 6, endOffset: 4 }, 92 | }, 93 | }, 94 | position: { startOffset: 0, endOffset: 60 }, 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-empty-middle-comment/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [], 9 | subElements: [ 10 | { 11 | type: "XMLElement", 12 | namespaces: {}, 13 | name: "to", 14 | attributes: [], 15 | subElements: [], 16 | textContents: [ 17 | { 18 | type: "XMLTextContent", 19 | position: { startOffset: 15, endOffset: 18 }, 20 | text: "bobi", 21 | }, 22 | ], 23 | position: { startOffset: 11, endOffset: 23 }, 24 | syntax: { 25 | openName: { image: "to", startOffset: 12, endOffset: 13 }, 26 | closeName: { image: "to", startOffset: 21, endOffset: 22 }, 27 | isSelfClosing: false, 28 | openBody: { startOffset: 11, endOffset: 14 }, 29 | closeBody: { startOffset: 19, endOffset: 23 }, 30 | attributesRange: { startOffset: 15, endOffset: 13 }, 31 | }, 32 | }, 33 | { 34 | type: "XMLElement", 35 | namespaces: {}, 36 | name: null, 37 | attributes: [], 38 | subElements: [], 39 | textContents: [], 40 | position: { startOffset: 29, endOffset: 29 }, 41 | syntax: {}, 42 | }, 43 | { 44 | type: "XMLElement", 45 | namespaces: {}, 46 | name: "from", 47 | attributes: [], 48 | subElements: [], 49 | textContents: [ 50 | { 51 | type: "XMLTextContent", 52 | position: { startOffset: 70, endOffset: 73 }, 53 | text: "john", 54 | }, 55 | ], 56 | position: { startOffset: 64, endOffset: 80 }, 57 | syntax: { 58 | openName: { image: "from", startOffset: 65, endOffset: 68 }, 59 | closeName: { image: "from", startOffset: 76, endOffset: 79 }, 60 | isSelfClosing: false, 61 | openBody: { startOffset: 64, endOffset: 69 }, 62 | closeBody: { startOffset: 74, endOffset: 80 }, 63 | attributesRange: { startOffset: 70, endOffset: 68 }, 64 | }, 65 | }, 66 | ], 67 | textContents: [ 68 | { 69 | type: "XMLTextContent", 70 | position: { startOffset: 6, endOffset: 10 }, 71 | text: "\n ", 72 | }, 73 | { 74 | type: "XMLTextContent", 75 | position: { startOffset: 24, endOffset: 28 }, 76 | text: "\n ", 77 | }, 78 | { 79 | type: "XMLTextContent", 80 | position: { startOffset: 81, endOffset: 81 }, 81 | text: "\n", 82 | }, 83 | ], 84 | position: { startOffset: 0, endOffset: 88 }, 85 | syntax: { 86 | openName: { image: "note", startOffset: 1, endOffset: 4 }, 87 | closeName: { image: "note", startOffset: 84, endOffset: 87 }, 88 | isSelfClosing: false, 89 | openBody: { startOffset: 0, endOffset: 5 }, 90 | closeBody: { startOffset: 82, endOffset: 88 }, 91 | attributesRange: { startOffset: 6, endOffset: 4 }, 92 | }, 93 | }, 94 | position: { startOffset: 0, endOffset: 89 }, 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build-node18: 4 | docker: 5 | - image: cimg/node:18.20-browsers 6 | working_directory: ~/repo 7 | steps: 8 | - run: sudo apt-get install libxss1 9 | - checkout 10 | - run: yarn 11 | - run: yarn run ci 12 | - run: 13 | command: cat ./coverage/lcov.info | ./node_modules/.bin/coveralls 14 | environment: 15 | COVERALLS_SERVICE_NAME: circle-ci 16 | COVERALLS_GIT_BRANCH: ${CIRCLE_BRANCH} 17 | 18 | build-node20: 19 | docker: 20 | - image: cimg/node:20.11-browsers 21 | working_directory: ~/repo 22 | steps: 23 | - run: sudo apt-get install libxss1 24 | - checkout 25 | - run: yarn 26 | - run: yarn run ci 27 | 28 | compliance: 29 | docker: 30 | - image: cimg/python:3.10 31 | working_directory: ~/workdir 32 | steps: 33 | - checkout 34 | - run: 35 | name: upgrade pip 36 | command: python -m pip install --upgrade pip 37 | - run: 38 | name: compliance check 39 | command: | 40 | pip install --user reuse 41 | ~/.local/bin/reuse lint 42 | 43 | deploy-npm: 44 | docker: 45 | - image: cimg/node:18.20-browsers 46 | working_directory: ~/repo 47 | steps: 48 | - run: sudo apt-get install libxss1 49 | - checkout 50 | - run: yarn --pure-lockfile 51 | - run: yarn run ci 52 | - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 53 | # To help debug when lerna detects changes to the working tree and fails the publish 54 | - run: git status 55 | - run: yarn run release:publish --no-verify-access 56 | 57 | prepare-vsix: 58 | docker: 59 | - image: cimg/node:18.20-browsers 60 | working_directory: ~/repo 61 | steps: 62 | - run: sudo apt-get install libxss1 63 | - checkout 64 | - run: yarn 65 | - run: yarn run ci 66 | - persist_to_workspace: 67 | root: "./packages/xml-toolkit" 68 | paths: 69 | # https://golang.org/pkg/path/filepath/#Match 70 | - xml-toolkit-* 71 | 72 | deploy-gh-releases: 73 | docker: 74 | - image: cimg/go:1.21 75 | steps: 76 | - attach_workspace: 77 | at: ./dist 78 | - run: 79 | name: "Publish Release on GitHub" 80 | command: | 81 | export GO111MODULE=on 82 | go install github.com/tcnksm/ghr@latest 83 | VERSION=${CIRCLE_TAG} 84 | ~/go/bin/ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${VERSION} ./dist/ 85 | 86 | workflows: 87 | version: 2 88 | voter: 89 | jobs: 90 | - build-node18 91 | - build-node20 92 | - compliance 93 | release: 94 | jobs: 95 | - deploy-npm: 96 | filters: 97 | tags: 98 | only: /^RELEASE/ 99 | branches: 100 | ignore: /.*/ 101 | - prepare-vsix: 102 | filters: 103 | tags: 104 | only: /^xml-toolkit@[0-9]+(.[0-9]+)*/ 105 | branches: 106 | ignore: /.*/ 107 | - deploy-gh-releases: 108 | requires: 109 | - prepare-vsix 110 | filters: 111 | tags: 112 | only: /^xml-toolkit@[0-9]+(.[0-9]+)*/ 113 | branches: 114 | ignore: /.*/ 115 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/complex-contents/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cst: { 3 | name: "document", 4 | children: { 5 | prolog: [ 6 | { 7 | name: "prolog", 8 | children: { 9 | XMLDeclOpen: [ 10 | { 11 | image: "", 20 | startOffset: 6, 21 | endOffset: 7, 22 | tokenType: "SPECIAL_CLOSE", 23 | }, 24 | ], 25 | }, 26 | location: { startOffset: 0, endOffset: 7 }, 27 | }, 28 | ], 29 | misc: [ 30 | { 31 | name: "misc", 32 | children: { 33 | SEA_WS: [ 34 | { 35 | image: "\n", 36 | startOffset: 8, 37 | endOffset: 8, 38 | tokenType: "SEA_WS", 39 | }, 40 | ], 41 | }, 42 | location: { startOffset: 8, endOffset: 8 }, 43 | }, 44 | { 45 | name: "misc", 46 | children: { 47 | SEA_WS: [ 48 | { 49 | image: "\n", 50 | startOffset: 22, 51 | endOffset: 22, 52 | tokenType: "SEA_WS", 53 | }, 54 | ], 55 | }, 56 | location: { startOffset: 22, endOffset: 22 }, 57 | }, 58 | ], 59 | element: [ 60 | { 61 | name: "element", 62 | children: { 63 | OPEN: [ 64 | { image: "<", startOffset: 9, endOffset: 9, tokenType: "OPEN" }, 65 | ], 66 | Name: [ 67 | { 68 | image: "note", 69 | startOffset: 10, 70 | endOffset: 13, 71 | tokenType: "Name", 72 | }, 73 | ], 74 | START_CLOSE: [ 75 | { 76 | image: ">", 77 | startOffset: 14, 78 | endOffset: 14, 79 | tokenType: "CLOSE", 80 | }, 81 | ], 82 | content: [ 83 | { 84 | name: "content", 85 | children: {}, 86 | location: { startOffset: null, endOffset: null }, 87 | }, 88 | ], 89 | SLASH_OPEN: [ 90 | { 91 | image: "", 108 | startOffset: 21, 109 | endOffset: 21, 110 | tokenType: "CLOSE", 111 | }, 112 | ], 113 | }, 114 | location: { startOffset: 9, endOffset: 21 }, 115 | }, 116 | ], 117 | }, 118 | location: { startOffset: 0, endOffset: 22 }, 119 | }, 120 | }; 121 | -------------------------------------------------------------------------------- /packages/simple-schema/lib/content-assist/element-name.js: -------------------------------------------------------------------------------- 1 | const { difference, map, filter, has, pickBy } = require("lodash"); 2 | const { DEFAULT_NS } = require("@xml-tools/ast"); 3 | 4 | // https://www.w3.org/TR/2009/REC-xml-names-20091208/#NT-PrefixedName 5 | const NAMESPACE_PATTERN = /^(?:([^:]*):)?([^:]*)$/; 6 | /** 7 | * 8 | * Note that the Element (XML/XSS) are of the parent node of the element 9 | * in which content assist was requested. 10 | * 11 | * @param {XMLElement} elementNode 12 | * @param {XSSElement} xssElement 13 | * 14 | * @returns {CompletionSuggestion[]} 15 | */ 16 | function elementNameCompletion(elementNode, xssElement, prefix = "") { 17 | const match = prefix.match(NAMESPACE_PATTERN); 18 | if (match === null) { 19 | return []; 20 | } 21 | // If there is no prefix, use the default namespace prefix 22 | const namespacePrefix = match[1] ? match[1] : DEFAULT_NS; 23 | const elementNamespaceUri = elementNode.namespaces[namespacePrefix]; 24 | const possibleElements = filter( 25 | xssElement.elements, 26 | (_) => 27 | has(_, "namespace") === false || 28 | (_.namespace && _.namespace === elementNamespaceUri) 29 | ); 30 | const possibleSuggestionsWithoutExistingSingular = applicableElements( 31 | xssElement.elements, 32 | elementNode.subElements, 33 | possibleElements 34 | ); 35 | 36 | const suggestions = map(possibleSuggestionsWithoutExistingSingular, (_) => { 37 | return { 38 | text: _, 39 | label: _, 40 | }; 41 | }); 42 | 43 | if (namespacePrefix === undefined || namespacePrefix === DEFAULT_NS) { 44 | // Can't really suggest anything for the `implicit` default namespace... 45 | const namespacesWithoutDefault = pickBy( 46 | elementNode.namespaces, 47 | (uri, prefix) => prefix !== DEFAULT_NS 48 | ); 49 | const applicableNamespaces = pickBy(namespacesWithoutDefault, (uri) => { 50 | const possibleElements = filter( 51 | xssElement.elements, 52 | (element) => 53 | has(element, "namespace") === true && element.namespace === uri 54 | ); 55 | const possibleSuggestionsWithoutExistingSingular = applicableElements( 56 | xssElement.elements, 57 | elementNode.subElements, 58 | possibleElements 59 | ); 60 | const namespaceHasApplicableElements = 61 | possibleSuggestionsWithoutExistingSingular.length > 0; 62 | return namespaceHasApplicableElements; 63 | }); 64 | const namespaceSuggestions = map(applicableNamespaces, (uri, prefix) => ({ 65 | text: prefix, 66 | label: prefix, 67 | commitCharacter: ":", 68 | isNamespace: true, 69 | })); 70 | return [...namespaceSuggestions, ...suggestions]; 71 | } 72 | return suggestions; 73 | } 74 | 75 | function applicableElements(xssElements, subElements, possibleElements) { 76 | const allPossibleSuggestions = map( 77 | possibleElements, 78 | (element) => element.name 79 | ); 80 | const notSingularElem = filter( 81 | xssElements, 82 | (element) => element.cardinality === "many" 83 | ); 84 | const notSingularElemNames = map(notSingularElem, (element) => element.name); 85 | const existingElemNames = map(subElements, (element) => element.name); 86 | const existingSingular = difference(existingElemNames, notSingularElemNames); 87 | const possibleSuggestionsWithoutExistingSingular = difference( 88 | allPossibleSuggestions, 89 | existingSingular 90 | ); 91 | return possibleSuggestionsWithoutExistingSingular; 92 | } 93 | 94 | module.exports = { 95 | elementNameCompletion: elementNameCompletion, 96 | }; 97 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/invalid/element-name-last/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [], 9 | subElements: [ 10 | { 11 | type: "XMLElement", 12 | namespaces: {}, 13 | name: "to", 14 | attributes: [], 15 | subElements: [], 16 | textContents: [ 17 | { 18 | type: "XMLTextContent", 19 | position: { startOffset: 15, endOffset: 18 }, 20 | text: "bobi", 21 | }, 22 | ], 23 | position: { startOffset: 11, endOffset: 23 }, 24 | syntax: { 25 | openName: { image: "to", startOffset: 12, endOffset: 13 }, 26 | closeName: { image: "to", startOffset: 21, endOffset: 22 }, 27 | isSelfClosing: false, 28 | openBody: { startOffset: 11, endOffset: 14 }, 29 | closeBody: { startOffset: 19, endOffset: 23 }, 30 | attributesRange: { startOffset: 15, endOffset: 13 }, 31 | }, 32 | }, 33 | { 34 | type: "XMLElement", 35 | namespaces: {}, 36 | name: "from", 37 | attributes: [], 38 | subElements: [], 39 | textContents: [ 40 | { 41 | type: "XMLTextContent", 42 | position: { startOffset: 35, endOffset: 38 }, 43 | text: "john", 44 | }, 45 | ], 46 | position: { startOffset: 29, endOffset: 45 }, 47 | syntax: { 48 | openName: { image: "from", startOffset: 30, endOffset: 33 }, 49 | closeName: { image: "from", startOffset: 41, endOffset: 44 }, 50 | isSelfClosing: false, 51 | openBody: { startOffset: 29, endOffset: 34 }, 52 | closeBody: { startOffset: 39, endOffset: 45 }, 53 | attributesRange: { startOffset: 35, endOffset: 33 }, 54 | }, 55 | }, 56 | { 57 | type: "XMLElement", 58 | namespaces: {}, 59 | name: "ad", 60 | attributes: [], 61 | subElements: [], 62 | textContents: [], 63 | position: { startOffset: 51, endOffset: 53 }, 64 | syntax: { 65 | openName: { image: "ad", startOffset: 52, endOffset: 53 }, 66 | guessedAttributesRange: { startOffset: 55, endOffset: 55 }, 67 | }, 68 | }, 69 | ], 70 | textContents: [ 71 | { 72 | type: "XMLTextContent", 73 | position: { startOffset: 6, endOffset: 10 }, 74 | text: "\n ", 75 | }, 76 | { 77 | type: "XMLTextContent", 78 | position: { startOffset: 24, endOffset: 28 }, 79 | text: "\n ", 80 | }, 81 | { 82 | type: "XMLTextContent", 83 | position: { startOffset: 46, endOffset: 50 }, 84 | text: "\n ", 85 | }, 86 | ], 87 | position: { startOffset: 0, endOffset: 61 }, 88 | syntax: { 89 | openName: { image: "note", startOffset: 1, endOffset: 4 }, 90 | closeName: { image: "note", startOffset: 57, endOffset: 60 }, 91 | isSelfClosing: false, 92 | openBody: { startOffset: 0, endOffset: 5 }, 93 | closeBody: { startOffset: 55, endOffset: 61 }, 94 | attributesRange: { startOffset: 6, endOffset: 4 }, 95 | }, 96 | }, 97 | position: { startOffset: 0, endOffset: 62 }, 98 | }, 99 | }; 100 | -------------------------------------------------------------------------------- /packages/ast/test/snapshots/valid/prolog-no-attriibs/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ast: { 3 | type: "XMLDocument", 4 | rootElement: { 5 | type: "XMLElement", 6 | namespaces: {}, 7 | name: "note", 8 | attributes: [], 9 | subElements: [ 10 | { 11 | type: "XMLElement", 12 | namespaces: {}, 13 | name: "to", 14 | attributes: [], 15 | subElements: [], 16 | textContents: [ 17 | { 18 | type: "XMLTextContent", 19 | position: { startOffset: 39, endOffset: 42 }, 20 | text: "Bill", 21 | }, 22 | ], 23 | position: { startOffset: 35, endOffset: 47 }, 24 | syntax: { 25 | openName: { image: "to", startOffset: 36, endOffset: 37 }, 26 | closeName: { image: "to", startOffset: 45, endOffset: 46 }, 27 | isSelfClosing: false, 28 | openBody: { startOffset: 35, endOffset: 38 }, 29 | closeBody: { startOffset: 43, endOffset: 47 }, 30 | attributesRange: { startOffset: 39, endOffset: 37 }, 31 | }, 32 | }, 33 | { 34 | type: "XMLElement", 35 | namespaces: {}, 36 | name: "from", 37 | attributes: [], 38 | subElements: [], 39 | textContents: [ 40 | { 41 | type: "XMLTextContent", 42 | position: { startOffset: 69, endOffset: 71 }, 43 | text: "Tim", 44 | }, 45 | ], 46 | position: { startOffset: 63, endOffset: 78 }, 47 | syntax: { 48 | openName: { image: "from", startOffset: 64, endOffset: 67 }, 49 | closeName: { image: "from", startOffset: 74, endOffset: 77 }, 50 | isSelfClosing: false, 51 | openBody: { startOffset: 63, endOffset: 68 }, 52 | closeBody: { startOffset: 72, endOffset: 78 }, 53 | attributesRange: { startOffset: 69, endOffset: 67 }, 54 | }, 55 | }, 56 | ], 57 | textContents: [ 58 | { 59 | type: "XMLTextContent", 60 | position: { startOffset: 6, endOffset: 10 }, 61 | text: "\n ", 62 | }, 63 | { 64 | type: "XMLTextContent", 65 | position: { startOffset: 30, endOffset: 34 }, 66 | text: "\n ", 67 | }, 68 | { 69 | type: "XMLTextContent", 70 | position: { startOffset: 48, endOffset: 52 }, 71 | text: "\n ", 72 | }, 73 | { 74 | type: "XMLTextContent", 75 | position: { startOffset: 53, endOffset: 62 }, 76 | text: "hello\n ", 77 | }, 78 | { 79 | type: "XMLTextContent", 80 | position: { startOffset: 79, endOffset: 83 }, 81 | text: "\n ", 82 | }, 83 | { 84 | type: "XMLTextContent", 85 | position: { startOffset: 84, endOffset: 89 }, 86 | text: "world\n", 87 | }, 88 | ], 89 | position: { startOffset: 0, endOffset: 96 }, 90 | syntax: { 91 | openName: { image: "note", startOffset: 1, endOffset: 4 }, 92 | closeName: { image: "note", startOffset: 92, endOffset: 95 }, 93 | isSelfClosing: false, 94 | openBody: { startOffset: 0, endOffset: 5 }, 95 | closeBody: { startOffset: 90, endOffset: 96 }, 96 | attributesRange: { startOffset: 6, endOffset: 4 }, 97 | }, 98 | }, 99 | position: { startOffset: 0, endOffset: 97 }, 100 | }, 101 | }; 102 | -------------------------------------------------------------------------------- /packages/language-server/lib/language-services.js: -------------------------------------------------------------------------------- 1 | const { DiagnosticSeverity, Range } = require("vscode-languageserver"); 2 | const { map, forEach } = require("lodash"); 3 | const { parse } = require("@xml-tools/parser"); 4 | const { buildAst } = require("@xml-tools/ast"); 5 | const { checkConstraints } = require("@xml-tools/constraints"); 6 | 7 | /** 8 | * @param {TextDocument} document 9 | * @param {import("chevrotain").ILexingError} error 10 | * @returns {import("vscode-languageserver-types").Diagnostic} 11 | */ 12 | function lexingErrorToDiagnostic(document, error) { 13 | return { 14 | message: error.message, 15 | range: Range.create( 16 | document.positionAt(error.offset), 17 | document.positionAt(error.offset + error.length) 18 | ), 19 | severity: DiagnosticSeverity.Error, 20 | }; 21 | } 22 | 23 | /** 24 | * @param {TextDocument} document 25 | * @param {import("chevrotain").ILexingError} error 26 | * @returns {import("vscode-languageserver-types").Diagnostic} 27 | */ 28 | function parsingErrorToDiagnostic(document, error) { 29 | return { 30 | message: error.message, 31 | range: { 32 | start: document.positionAt(error.token.startOffset), 33 | end: document.positionAt( 34 | error.token.endOffset ? error.token.endOffset : 0 35 | ), 36 | }, 37 | severity: DiagnosticSeverity.Error, 38 | }; 39 | } 40 | 41 | /** 42 | * @param {TextDocument} document 43 | * @param {import("@xml-tools/validation").ValidationIssue} issue 44 | * @returns {import("vscode-languageserver-types").Diagnostic} 45 | */ 46 | function constraintIssueToDiagnostic(document, issue) { 47 | return { 48 | message: issue.msg, 49 | range: { 50 | start: document.positionAt(issue.position.startOffset), 51 | // Chevrotain Token positions are non-inclusive for endOffsets 52 | end: document.positionAt(issue.position.endOffset + 1), 53 | }, 54 | severity: toDiagnosticSeverity(issue.severity), 55 | }; 56 | } 57 | 58 | /** 59 | * @param {"info" | "warning" | "error"} issueSeverity 60 | * @returns {import("vscode-languageserver-types").DiagnosticSeverity} 61 | */ 62 | function toDiagnosticSeverity(issueSeverity) { 63 | switch (issueSeverity) { 64 | case "error": 65 | return DiagnosticSeverity.Error; 66 | case "warning": 67 | return DiagnosticSeverity.Warning; 68 | case "info": 69 | return DiagnosticSeverity.Information; 70 | /* istanbul ignore next -- runtime check */ 71 | default: 72 | throw Error("Non Exhaustive Match"); 73 | } 74 | } 75 | 76 | /** 77 | * @param {TextDocument} document 78 | * @param {object} opts 79 | * @param {string} opts.consumer 80 | * 81 | * @returns {import("vscode-languageserver-types").Diagnostic[]} 82 | */ 83 | async function validateDocument(document, opts) { 84 | let diagnostics = []; 85 | if (document.languageId === "xml") { 86 | const { cst, tokenVector, lexErrors, parseErrors } = parse( 87 | document.getText() 88 | ); 89 | const ast = buildAst(cst, tokenVector); 90 | const constraintsIssues = checkConstraints(ast); 91 | 92 | diagnostics = [ 93 | ...map(lexErrors, (_) => lexingErrorToDiagnostic(document, _)), 94 | ...map(parseErrors, (_) => parsingErrorToDiagnostic(document, _)), 95 | ...map(constraintsIssues, (_) => 96 | constraintIssueToDiagnostic(document, _) 97 | ), 98 | ]; 99 | } 100 | 101 | forEach(diagnostics, (_) => (_.source = opts.consumer)); 102 | 103 | return diagnostics; 104 | } 105 | 106 | module.exports = { 107 | validateDocument: validateDocument, 108 | toDiagnosticSeverity: toDiagnosticSeverity, 109 | }; 110 | -------------------------------------------------------------------------------- /packages/constraints/test/constraints/tag-closing-name-match-spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { parse } = require("@xml-tools/parser"); 3 | const { buildAst } = require("@xml-tools/ast"); 4 | const { 5 | validateTagClosingNameMatch, 6 | } = require("../../lib/constraints/tag-closing-name-match"); 7 | 8 | describe("tag closing name constraint", () => { 9 | context("no issues", () => { 10 | it("will not detect any issues for valid XML", () => { 11 | const xmlText = ` 12 | 13 | 14 | Tim 15 | `; 16 | 17 | const { cst, tokenVector } = parse(xmlText); 18 | const document = buildAst(cst, tokenVector); 19 | const element = document.rootElement; 20 | const result = validateTagClosingNameMatch(element); 21 | 22 | expect(result).to.be.empty; 23 | }); 24 | }); 25 | 26 | context("unable to validate", () => { 27 | it("will not detect any issues when the opening tag name is missing", () => { 28 | const xmlText = ` 29 | <> 30 | Bill 31 | Tim 32 | `; 33 | 34 | const { cst, tokenVector } = parse(xmlText); 35 | const document = buildAst(cst, tokenVector); 36 | const element = document.rootElement; 37 | const result = validateTagClosingNameMatch(element); 38 | 39 | expect(result).to.be.empty; 40 | }); 41 | 42 | it("will not detect any issues when the closing tag name is missing", () => { 43 | const xmlText = ` 44 | 45 | Bill 46 | Tim 47 | `; 48 | 49 | const { cst, tokenVector } = parse(xmlText); 50 | const document = buildAst(cst, tokenVector); 51 | const element = document.rootElement; 52 | const result = validateTagClosingNameMatch(element); 53 | 54 | expect(result).to.be.empty; 55 | }); 56 | 57 | it("will not detect any issues when both tag names are missing", () => { 58 | const xmlText = ` 59 | <> 60 | Bill 61 | Tim 62 | `; 63 | 64 | const { cst, tokenVector } = parse(xmlText); 65 | const document = buildAst(cst, tokenVector); 66 | const element = document.rootElement; 67 | const result = validateTagClosingNameMatch(element); 68 | 69 | expect(result).to.be.empty; 70 | }); 71 | }); 72 | 73 | context("positive constraint checks", () => { 74 | it("detects a mis-aligned closing tag", () => { 75 | const xmlText = ` 76 | 77 | Bill 78 | Tim 79 | `; 80 | 81 | const { cst, tokenVector } = parse(xmlText); 82 | const document = buildAst(cst, tokenVector); 83 | const element = document.rootElement; 84 | const result = validateTagClosingNameMatch(element); 85 | 86 | expect(result).to.have.lengthOf(2); 87 | expect(result).to.deep.include.members([ 88 | { 89 | msg: 'tags mismatch: "note" must match closing tag: "note-typo"', 90 | severity: "error", 91 | node: element, 92 | position: { 93 | startOffset: element.syntax.openName.startOffset, 94 | endOffset: element.syntax.openName.endOffset, 95 | }, 96 | }, 97 | { 98 | msg: 'tags mismatch: "note-typo" must match opening tag: "note"', 99 | severity: "error", 100 | node: element, 101 | position: { 102 | startOffset: element.syntax.closeName.startOffset, 103 | endOffset: element.syntax.closeName.endOffset, 104 | }, 105 | }, 106 | ]); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /packages/parser/test/snapshots/valid/cdata/output.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cst: { 3 | name: "document", 4 | children: { 5 | element: [ 6 | { 7 | name: "element", 8 | children: { 9 | OPEN: [ 10 | { image: "<", startOffset: 0, endOffset: 0, tokenType: "OPEN" }, 11 | ], 12 | Name: [ 13 | { 14 | image: "note", 15 | startOffset: 1, 16 | endOffset: 4, 17 | tokenType: "Name", 18 | }, 19 | ], 20 | START_CLOSE: [ 21 | { image: ">", startOffset: 5, endOffset: 5, tokenType: "CLOSE" }, 22 | ], 23 | content: [ 24 | { 25 | name: "content", 26 | children: { 27 | chardata: [ 28 | { 29 | name: "chardata", 30 | children: { 31 | SEA_WS: [ 32 | { 33 | image: "\n ", 34 | startOffset: 6, 35 | endOffset: 10, 36 | tokenType: "SEA_WS", 37 | }, 38 | ], 39 | }, 40 | location: { startOffset: 6, endOffset: 10 }, 41 | }, 42 | { 43 | name: "chardata", 44 | children: { 45 | SEA_WS: [ 46 | { 47 | image: "\n", 48 | startOffset: 57, 49 | endOffset: 57, 50 | tokenType: "SEA_WS", 51 | }, 52 | ], 53 | }, 54 | location: { startOffset: 57, endOffset: 57 }, 55 | }, 56 | ], 57 | CData: [ 58 | { 59 | image: "Hello, world!]]>", 60 | startOffset: 11, 61 | endOffset: 56, 62 | tokenType: "CData", 63 | }, 64 | ], 65 | }, 66 | location: { startOffset: 6, endOffset: 57 }, 67 | }, 68 | ], 69 | SLASH_OPEN: [ 70 | { 71 | image: "", 88 | startOffset: 64, 89 | endOffset: 64, 90 | tokenType: "CLOSE", 91 | }, 92 | ], 93 | }, 94 | location: { startOffset: 0, endOffset: 64 }, 95 | }, 96 | ], 97 | misc: [ 98 | { 99 | name: "misc", 100 | children: { 101 | SEA_WS: [ 102 | { 103 | image: "\n", 104 | startOffset: 65, 105 | endOffset: 65, 106 | tokenType: "SEA_WS", 107 | }, 108 | ], 109 | }, 110 | location: { startOffset: 65, endOffset: 65 }, 111 | }, 112 | ], 113 | }, 114 | location: { startOffset: 0, endOffset: 65 }, 115 | }, 116 | }; 117 | -------------------------------------------------------------------------------- /packages/language-server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [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) 7 | 8 | **Note:** Version bump only for package @xml-tools/language-server 9 | 10 | # [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) 11 | 12 | ### Bug Fixes 13 | 14 | - use extension name as diagnostic source ([#297](https://github.com/SAP/xml-tools/issues/297)) ([f6d7dc4](https://github.com/SAP/xml-tools/commit/f6d7dc42a05c4e1d7c9ab3f1fbe35ecb1eb820fa)) 15 | 16 | ### Features 17 | 18 | - 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) 19 | 20 | ## [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) 21 | 22 | ### Bug Fixes 23 | 24 | - missing lib folder in npm packages ([ffed3c2](https://github.com/SAP/xml-tools/commit/ffed3c2c54c70aea8b9ded0d53786382bc190cc5)) 25 | 26 | ## [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) 27 | 28 | **Note:** Version bump only for package @xml-tools/language-server 29 | 30 | ## [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) 31 | 32 | **Note:** Version bump only for package @xml-tools/language-server 33 | 34 | ## [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) 35 | 36 | **Note:** Version bump only for package @xml-tools/language-server 37 | 38 | ## [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) 39 | 40 | **Note:** Version bump only for package @xml-tools/language-server 41 | 42 | ## [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) 43 | 44 | **Note:** Version bump only for package @xml-tools/language-server 45 | 46 | ## [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) 47 | 48 | **Note:** Version bump only for package @xml-tools/language-server 49 | 50 | ## [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) 51 | 52 | **Note:** Version bump only for package @xml-tools/language-server 53 | 54 | ## [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) 55 | 56 | ### Bug Fixes 57 | 58 | - fix README.md ([472301d](https://github.com/SAP/xml-tools/commit/472301d)) 59 | 60 | ## [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) 61 | 62 | ### Bug Fixes 63 | 64 | - fix README.md ([472301d](https://github.com/SAP/xml-tools/commit/472301d)) 65 | 66 | ## [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) 67 | 68 | ### Bug Fixes 69 | 70 | - add npmignore file to each package ([5bbf209](https://github.com/SAP/xml-tools/commit/5bbf209)) 71 | - missing import ([2a51ab9](https://github.com/SAP/xml-tools/commit/2a51ab9)) 72 | 73 | ## 1.0.1 (2020-02-19) 74 | 75 | **Note:** Version bump only for package @xml-tools/language-server 76 | -------------------------------------------------------------------------------- /packages/simple-schema/README.md: -------------------------------------------------------------------------------- 1 | [![npm (scoped)](https://img.shields.io/npm/v/@xml-tools/simple-schema.svg)](https://www.npmjs.com/package/@xml-tools/simple-schema) 2 | 3 | # @xml-tools/simple-schema 4 | 5 | This package includes three elements: 6 | 7 | - Data structure definition for a basic XML schema represented as a JavaScript object literal. 8 | - Validation logic to inspect a given XML conforms to a Schema. 9 | - Content Assist logic that provides suggestions using the Schema information. 10 | 11 | ## Installation 12 | 13 | With npm: 14 | 15 | - `npm install @xml-tools/simple-schema` 16 | 17 | With Yarn 18 | 19 | - `yarn add @xml-tools/simple-schema` 20 | 21 | ## Usage 22 | 23 | Please see the [TypeScript Definitions](./api.d.ts) for full API details. 24 | 25 | - Defining a Schema: 26 | ```javascript 27 | const schema = { 28 | name: "people", 29 | required: true, 30 | cardinality: "single", 31 | attributes: {}, 32 | elements: { 33 | person: { 34 | name: "person", 35 | required: false, 36 | cardinality: "many", 37 | attributes: { 38 | eyeColor: { 39 | key: "eyeColor", 40 | required: false, 41 | value: ["grey", "blue", "green", "red"], 42 | }, 43 | }, 44 | elements: { 45 | name: { 46 | cardinality: "single", 47 | required: true, 48 | name: "name", 49 | attributes: {}, 50 | elements: {}, 51 | }, 52 | }, 53 | }, 54 | }, 55 | }; 56 | ``` 57 | - A Simple XML Text 58 | ```javascript 59 | // A Sample XML Text 60 | const xmlText = ` 61 | 62 | Daenerys Targaryen 63 | 64 | `; 65 | ``` 66 | - Performing Validations 67 | 68 | ```javascript 69 | const { parse } = require("@xml-tools/parser"); 70 | const { buildAst } = require("@xml-tools/ast"); 71 | const { validate } = require("@xml-tools/validation"); 72 | const { getSchemaValidators } = require("@xml-tools/simple-schema"); 73 | 74 | const { cst, tokenVector } = parse(xmlText); 75 | const xmlDoc = buildAst(cst, tokenVector); 76 | const schemaValidators = getSchemaValidators(schema); 77 | const issues = validate({ 78 | doc: xmlDoc, 79 | validators: { 80 | attribute: [schemaValidators.attribute], 81 | element: [schemaValidators.element], 82 | }, 83 | }); 84 | 85 | console.log(issues[0].msg); // Expecting one of but found 86 | console.log(issues[0].position); // { startOffset: 46, endOffset: 53 } 87 | ``` 88 | 89 | - Using Content Assist APIs 90 | 91 | ```javascript 92 | const { getSuggestions } = require("@xml-tools/content-assist"); 93 | const { getSchemaSuggestionsProviders } = require("@xml-tools/simple-schema"); 94 | 95 | const schemaSuggestionsProviders = getSchemaSuggestionsProviders(schema); 96 | 97 | const suggestions = getSuggestions({ 98 | text: xmlText, 99 | offset: 47, // within the eyeColor quoted value 100 | providers: { 101 | attributeValue: [ 102 | schemaSuggestionsProviders.schemaAttributeValueCompletion, 103 | ], 104 | attributeName: [schemaSuggestionsProviders.schemaAttributeNameCompletion], 105 | elementName: [schemaSuggestionsProviders.schemaElementNameCompletion], 106 | }, 107 | }); 108 | 109 | console.log(suggestions[0]); // { text: 'grey', label: 'grey' } 110 | console.log(suggestions[1]); // { text: 'blue', label: 'blue' } 111 | ``` 112 | 113 | ## Support 114 | 115 | Please open [issues](https://github.com/SAP/xml-tols/issues) on github. 116 | 117 | ## Contributing 118 | 119 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 120 | -------------------------------------------------------------------------------- /packages/ast/test/visitor/visitor-spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { parse } = require("@xml-tools/parser"); 3 | const { buildAst, accept } = require("../../"); 4 | 5 | describe("The XML AST Visitor", () => { 6 | it("can traverse AST Elements", () => { 7 | const inputText = ` 8 | Bill 9 | Tim 10 | 11 | `; 12 | 13 | let visitedCounter = 0; 14 | const visitor = { 15 | visitXMLElement: function (node) { 16 | expect(["to", "from", "note"]).to.include(node.name); 17 | visitedCounter++; 18 | }, 19 | }; 20 | 21 | const astNode = getAst(inputText); 22 | accept(astNode, visitor); 23 | expect(visitedCounter).to.eql(3); 24 | }); 25 | 26 | it("can traverse AST TextContents", () => { 27 | const inputText = ` 28 | Bill 29 | Tim 30 | 31 | `; 32 | 33 | let visitedCounter = 0; 34 | const visitor = { 35 | visitXMLTextContent: function (node) { 36 | // ignore whitespace TextNodes 37 | if (/^\s+$/.test(node.text) === false) { 38 | expect(["Bill", "Tim", "note"]).to.include(node.text); 39 | visitedCounter++; 40 | } 41 | }, 42 | }; 43 | 44 | const astNode = getAst(inputText); 45 | accept(astNode, visitor); 46 | expect(visitedCounter).to.eql(2); 47 | }); 48 | 49 | it("can traverse AST Attributes", () => { 50 | const inputText = ` 51 | 52 | Bill 53 | Tim 54 | 55 | `; 56 | 57 | let visitedCounter = 0; 58 | const visitor = { 59 | visitXMLAttribute: function (node) { 60 | expect(["foo", "bar"]).to.include(node.key); 61 | visitedCounter++; 62 | }, 63 | }; 64 | 65 | const astNode = getAst(inputText); 66 | accept(astNode, visitor); 67 | expect(visitedCounter).to.eql(2); 68 | }); 69 | 70 | it("can traverse AST Prolog Attributes", () => { 71 | const inputText = ` 72 | 73 | Bill 74 | Tim 75 | 76 | `; 77 | 78 | let visitedCounter = 0; 79 | const visitor = { 80 | visitXMLPrologAttribute: function (node) { 81 | expect(["version", "encoding"]).to.include(node.key); 82 | visitedCounter++; 83 | }, 84 | }; 85 | 86 | const astNode = getAst(inputText); 87 | accept(astNode, visitor); 88 | expect(visitedCounter).to.eql(2); 89 | }); 90 | 91 | it("can traverse AST Prolog", () => { 92 | const inputText = ` 93 | 94 | 95 | `; 96 | 97 | let visitedCounter = 0; 98 | const visitor = { 99 | visitXMLProlog: function (node) { 100 | expect(node.attributes).to.have.lengthOf(2); 101 | expect(node.attributes[0].key).to.eql("version"); 102 | expect(node.attributes[1].key).to.eql("encoding"); 103 | visitedCounter++; 104 | }, 105 | }; 106 | 107 | const astNode = getAst(inputText); 108 | accept(astNode, visitor); 109 | expect(visitedCounter).to.eql(1); 110 | }); 111 | 112 | it("can traverse XML Document AST", () => { 113 | const inputText = ` 114 | foo 115 | bar 116 | 117 | `; 118 | 119 | let visitedCounter = 0; 120 | const visitor = { 121 | visitXMLDocument: function (node) { 122 | expect(node.rootElement.name).to.eql("note"); 123 | visitedCounter++; 124 | }, 125 | }; 126 | 127 | const astNode = getAst(inputText); 128 | accept(astNode, visitor); 129 | expect(visitedCounter).to.eql(1); 130 | }); 131 | }); 132 | 133 | function getAst(text) { 134 | const { cst, tokenVector } = parse(text); 135 | const ast = buildAst(cst, tokenVector); 136 | return ast; 137 | } 138 | --------------------------------------------------------------------------------