├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .release-it.json ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO.md ├── babel.config.js ├── docs └── original_license.txt ├── examples └── head.xsl ├── interactive-tests ├── js │ ├── simplelog.js │ ├── xpath_script.js │ └── xslt_script.js ├── xpath.html └── xslt.html ├── jest.config.ts ├── package.json ├── rollup.config.js ├── src ├── constants.ts ├── dom │ ├── functions.ts │ ├── index.ts │ ├── util.ts │ ├── xbrowser-node.ts │ ├── xdocument.ts │ ├── xml-functions.ts │ ├── xml-output-options.ts │ ├── xml-parser.ts │ ├── xmltoken.ts │ └── xnode.ts ├── index.ts ├── xpath │ ├── common-function.ts │ ├── expr-context.ts │ ├── expressions │ │ ├── README.md │ │ ├── binary-expr.ts │ │ ├── expression.ts │ │ ├── filter-expr.ts │ │ ├── function-call-expr.ts │ │ ├── index.ts │ │ ├── literal-expr.ts │ │ ├── location-expr.ts │ │ ├── number-expr.ts │ │ ├── path-expr.ts │ │ ├── predicate-expr.ts │ │ ├── step-expr.ts │ │ ├── token-expr.ts │ │ ├── unary-minus-expr.ts │ │ ├── union-expr.ts │ │ └── variable-expr.ts │ ├── functions │ │ ├── index.ts │ │ ├── internal-functions.ts │ │ ├── non-standard.ts │ │ ├── standard-20.ts │ │ ├── standard.ts │ │ └── xslt-specific.ts │ ├── grammar-rule-candidate.ts │ ├── index.ts │ ├── match-resolver.ts │ ├── node-tests │ │ ├── index.ts │ │ ├── node-test-any.ts │ │ ├── node-test-comment.ts │ │ ├── node-test-element-or-attribute.ts │ │ ├── node-test-name.ts │ │ ├── node-test-nc.ts │ │ ├── node-test-pi.ts │ │ ├── node-test-text.ts │ │ └── node-test.ts │ ├── tokens.ts │ ├── values │ │ ├── boolean-value.ts │ │ ├── index.ts │ │ ├── node-set-value.ts │ │ ├── node-value.ts │ │ ├── number-value.ts │ │ └── string-value.ts │ ├── xpath-grammar-rules.ts │ ├── xpath-token-rule.ts │ └── xpath.ts ├── xpathdebug.ts └── xslt │ ├── index.ts │ ├── xslt-decimal-format-settings.ts │ ├── xslt-options.ts │ ├── xslt-parameter.ts │ └── xslt.ts ├── tests ├── dom.test.tsx ├── interactive-tests-examples.test.tsx ├── lmht │ ├── html-to-lmht.test.tsx │ └── lmht.test.tsx ├── local-name.test.tsx ├── namespaces.test.tsx ├── root-element.test.tsx ├── simple.test.tsx ├── template-precedence.test.tsx ├── variables-as-parameters.test.tsx ├── xml │ ├── escape.test.tsx │ ├── html.test.tsx │ ├── xml-to-html.test.tsx │ ├── xml-to-json.test.tsx │ ├── xml.test.tsx │ └── xmltoken.test.tsx ├── xpath │ ├── functions.test.tsx │ └── xpath.test.tsx └── xslt │ ├── apply-template.test.ts │ ├── choose.test.tsx │ ├── copy-of.test.tsx │ ├── for-each.test.tsx │ ├── import.test.tsx │ ├── include.test.tsx │ ├── key.test.tsx │ └── xslt.test.tsx ├── tsconfig.debug.json ├── tsconfig.json ├── tsconfig.rollup.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist/**/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | "jest/globals": true 7 | }, 8 | extends: ['eslint:recommended'], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | ecmaVersion: 2018, 12 | sourceType: 'module' 13 | }, 14 | plugins: [ 15 | 'jest', 16 | 'jsx', 17 | '@typescript-eslint' 18 | ], 19 | root: true, 20 | rules: { 21 | 'accessor-pairs': 'error', 22 | 'array-bracket-newline': 'off', 23 | 'array-bracket-spacing': ['error', 'never'], 24 | 'array-callback-return': 'error', 25 | 'array-element-newline': 'off', 26 | 'arrow-body-style': 'error', 27 | 'arrow-spacing': 'off', 28 | 'block-scoped-var': 'error', 29 | 'block-spacing': ['error', 'always'], 30 | 'brace-style': ['error', '1tbs'], 31 | 'callback-return': 'error', 32 | camelcase: 'off', 33 | 'capitalized-comments': 'off', 34 | 'class-methods-use-this': 'off', 35 | 'comma-dangle': 'error', 36 | 'comma-spacing': 'off', 37 | 'comma-style': ['error', 'last'], 38 | complexity: 'off', 39 | 'computed-property-spacing': ['error', 'never'], 40 | 'consistent-return': 'off', 41 | 'consistent-this': 'off', 42 | curly: 'off', 43 | 'default-case': 'off', 44 | 'dot-notation': 'error', 45 | 'eol-last': 'error', 46 | eqeqeq: 'off', 47 | 'func-call-spacing': 'error', 48 | 'func-name-matching': 'error', 49 | 'func-names': 'off', 50 | 'func-style': 'off', 51 | 'function-paren-newline': 'off', 52 | 'generator-star-spacing': 'error', 53 | 'global-require': 'error', 54 | 'guard-for-in': 'error', 55 | 'handle-callback-err': 'error', 56 | 'id-blacklist': 'error', 57 | 'id-length': 'off', 58 | 'id-match': 'error', 59 | 'implicit-arrow-linebreak': ['error', 'beside'], 60 | indent: 'off', 61 | 'indent-legacy': 'off', 62 | 'init-declarations': 'off', 63 | 'jsx-quotes': 'error', 64 | 'key-spacing': 'error', 65 | 'keyword-spacing': 'off', 66 | 'line-comment-position': 'off', 67 | 'lines-around-directive': 'error', 68 | 'max-classes-per-file': 'off', 69 | 'max-depth': 'off', 70 | 'max-len': 'off', 71 | 'max-lines': 'off', 72 | 'max-lines-per-function': 'off', 73 | 'max-nested-callbacks': 'error', 74 | 'max-params': 'off', 75 | 'max-statements': 'off', 76 | 'max-statements-per-line': 'error', 77 | 'multiline-comment-style': 'off', 78 | 'multiline-ternary': 'off', 79 | 'new-cap': 'error', 80 | 'new-parens': 'off', 81 | 'newline-after-var': 'off', 82 | 'newline-before-return': 'off', 83 | 'newline-per-chained-call': 'off', 84 | 'no-alert': 'error', 85 | 'no-array-constructor': 'error', 86 | 'no-async-promise-executor': 'error', 87 | 'no-buffer-constructor': 'error', 88 | 'no-caller': 'error', 89 | 'no-case-declarations': 'off', 90 | 'no-catch-shadow': 'error', 91 | 'no-confusing-arrow': 'error', 92 | 'no-console': 'warn', 93 | 'no-continue': 'off', 94 | 'no-div-regex': 'error', 95 | 'no-duplicate-imports': 'error', 96 | 'no-else-return': 'off', 97 | 'no-empty-function': 'off', 98 | 'no-eq-null': 'error', 99 | 'no-eval': 'error', 100 | 'no-extend-native': 'error', 101 | 'no-extra-bind': 'error', 102 | 'no-extra-label': 'error', 103 | 'no-extra-parens': 'off', 104 | 'no-floating-decimal': 'error', 105 | 'no-implicit-globals': 'error', 106 | 'no-implied-eval': 'error', 107 | 'no-inline-comments': 'off', 108 | 'no-invalid-this': 'off', 109 | 'no-iterator': 'error', 110 | 'no-label-var': 'error', 111 | 'no-lone-blocks': 'error', 112 | 'no-lonely-if': 'off', 113 | 'no-loop-func': 'error', 114 | 'no-magic-numbers': 'off', 115 | 'no-misleading-character-class': 'error', 116 | 'no-mixed-requires': 'error', 117 | 'no-multi-assign': 'error', 118 | 'no-multi-spaces': 'error', 119 | 'no-multi-str': 'error', 120 | 'no-multiple-empty-lines': 'error', 121 | 'no-native-reassign': 'error', 122 | 'no-negated-condition': 'off', 123 | 'no-negated-in-lhs': 'error', 124 | 'no-nested-ternary': 'error', 125 | 'no-new': 'error', 126 | 'no-new-func': 'error', 127 | 'no-new-object': 'error', 128 | 'no-new-require': 'error', 129 | 'no-new-wrappers': 'error', 130 | 'no-octal-escape': 'error', 131 | 'no-param-reassign': 'off', 132 | 'no-path-concat': 'error', 133 | 'no-plusplus': 'off', 134 | 'no-process-env': 'error', 135 | 'no-process-exit': 'error', 136 | 'no-proto': 'error', 137 | 'no-prototype-builtins': 'error', 138 | 'no-restricted-globals': 'error', 139 | 'no-restricted-imports': 'error', 140 | 'no-restricted-modules': 'error', 141 | 'no-restricted-properties': 'error', 142 | 'no-restricted-syntax': 'error', 143 | 'no-return-assign': 'off', 144 | 'no-return-await': 'error', 145 | 'no-script-url': 'error', 146 | 'no-self-compare': 'error', 147 | 'no-sequences': 'error', 148 | 'no-shadow': 'off', 149 | 'no-shadow-restricted-names': 'error', 150 | 'no-spaced-func': 'error', 151 | 'no-sync': 'error', 152 | 'no-tabs': 'error', 153 | 'no-template-curly-in-string': 'error', 154 | 'no-ternary': 'off', 155 | 'no-throw-literal': 'off', 156 | 'no-trailing-spaces': 'error', 157 | 'no-undef-init': 'error', 158 | 'no-undefined': 'off', 159 | 'no-underscore-dangle': 'off', 160 | 'no-unmodified-loop-condition': 'error', 161 | 'no-unneeded-ternary': 'error', 162 | 'no-unused-expressions': 'off', 163 | 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 164 | 'no-use-before-define': 'off', 165 | 'no-useless-call': 'off', 166 | 'no-useless-computed-key': 'error', 167 | 'no-useless-concat': 'warn', 168 | 'no-useless-constructor': 'error', 169 | 'no-useless-rename': 'error', 170 | 'no-useless-return': 'error', 171 | 'no-var': 'error', 172 | 'no-void': 'error', 173 | 'no-warning-comments': 'off', 174 | 'no-whitespace-before-property': 'error', 175 | 'no-with': 'error', 176 | 'nonblock-statement-body-position': 'error', 177 | 'object-shorthand': 'error', 178 | 'one-var': 'off', 179 | 'one-var-declaration-per-line': ['error', 'initializations'], 180 | 'operator-assignment': ['error', 'always'], 181 | 'padded-blocks': 'off', 182 | 'padding-line-between-statements': 'error', 183 | 'prefer-arrow-callback': 'error', 184 | 'prefer-const': 'off', 185 | 'prefer-destructuring': 'off', 186 | 'prefer-numeric-literals': 'error', 187 | 'prefer-object-spread': 'error', 188 | 'prefer-promise-reject-errors': 'error', 189 | 'prefer-reflect': 'off', 190 | 'prefer-rest-params': 'error', 191 | 'prefer-spread': 'error', 192 | 'prefer-template': 'off', 193 | 'quote-props': 'off', 194 | quotes: 'off', 195 | radix: 'off', 196 | 'require-atomic-updates': 'error', 197 | 'require-await': 'warn', 198 | 'require-jsdoc': 'off', 199 | 'require-unicode-regexp': 'off', 200 | 'rest-spread-spacing': 'error', 201 | semi: 'off', 202 | 'semi-spacing': [ 203 | 'error', 204 | { 205 | after: true, 206 | before: false 207 | } 208 | ], 209 | 'semi-style': ['error', 'last'], 210 | 'sort-imports': 'off', 211 | 'sort-keys': 'off', 212 | 'sort-vars': 'off', 213 | 'space-before-blocks': 'error', 214 | 'space-before-function-paren': 'off', 215 | 'space-in-parens': 'off', 216 | 'space-infix-ops': 'off', 217 | 'space-unary-ops': [ 218 | 'error', 219 | { 220 | nonwords: false, 221 | words: false 222 | } 223 | ], 224 | 'spaced-comment': 'off', 225 | strict: 'error', 226 | 'switch-colon-spacing': 'error', 227 | 'symbol-description': 'error', 228 | 'template-curly-spacing': ['error', 'never'], 229 | 'template-tag-spacing': 'error', 230 | 'unicode-bom': ['error', 'never'], 231 | 'valid-jsdoc': 'off', 232 | 'vars-on-top': 'error', 233 | 'wrap-iife': 'error', 234 | 'wrap-regex': 'off', 235 | 'yield-star-spacing': 'error', 236 | yoda: 'off' 237 | } 238 | }; 239 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Commit and PR - Main 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | unit-tests: 9 | permissions: 10 | checks: write 11 | pull-requests: write 12 | contents: write 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v2-beta 18 | with: 19 | node-version: '20' 20 | - uses: ArtiomTr/jest-coverage-report-action@v2 21 | id: coverage 22 | with: 23 | github-token: ${{ secrets.GITHUB_TOKEN }} 24 | test-script: yarn test 25 | package-manager: yarn 26 | output: report-markdown 27 | - uses: marocchino/sticky-pull-request-comment@v2 28 | with: 29 | message: ${{ steps.coverage.outputs.report }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .idea/ 3 | 4 | coverage/ 5 | demo/ 6 | dist/ 7 | node_modules/ 8 | 9 | package-lock.json 10 | .history/ 11 | 12 | # Strange bug with debugging a unit test causes a stop in `async_hooks`. 13 | test-without-jest.ts -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | localtests/ 2 | node_modules/ 3 | .git/ 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 4 7 | } 8 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "Version v${version}" 4 | }, 5 | "github": { 6 | "release": true 7 | }, 8 | "npm": { 9 | "publishPath": "./dist" 10 | }, 11 | "hooks": { 12 | "before:init": ["yarn build"], 13 | "after:bump": "yarn copyfiles -V ./package.json ./dist && yarn copyfiles -V ./README.md ./dist" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Unit tests", 9 | "type": "node", 10 | "request": "launch", 11 | "runtimeArgs": [ 12 | "--inspect-brk", 13 | "${workspaceRoot}/node_modules/jest/bin/jest.js", 14 | // "include.test", 15 | "--runInBand", 16 | "--testTimeout=100000000" 17 | ], 18 | "smartStep": true, 19 | "skipFiles": [ 20 | "/**", 21 | "node_modules/**" 22 | ], 23 | "console": "integratedTerminal", 24 | "internalConsoleOptions": "neverOpen" 25 | }, 26 | { 27 | "name": "Launch TS file", 28 | "type": "node", 29 | "request": "launch", 30 | "runtimeArgs": [ 31 | "-r", 32 | "ts-node/register" 33 | ], 34 | "args": [ 35 | "${file}" 36 | ], 37 | "env": { 38 | "TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.debug.json" 39 | }, 40 | "console": "integratedTerminal", 41 | "internalConsoleOptions": "neverOpen" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | This document is no longer maintained in favor of [GitHub's release system](https://github.com/DesignLiquido/xslt-processor/releases), and just kept here for historical reasons. 2 | 3 | 2018-03-20 Johannes Wilm 4 | 5 | * Version 0.9.0 6 | * Changed code to ES2015+ style 7 | * Fixed a bug where xml parsing choked on < and > inside of quotation marks. 8 | * Renamed to xslt-processor 9 | 10 | 2007-11-16 Google Inc. 11 | 12 | * Version 0.8 13 | * Released as http://ajaxslt.googlecode.com/svn/tags/release-0-8/ 14 | 15 | * Fixed a bug in parsing XPaths that end with "//qname" (Issue 17) 16 | 17 | * Added feature to optionally allow case-insensitive node name 18 | comparisons; this is useful when using XPaths on HTML, where 19 | node names are not consistent across browsers. 20 | 21 | * Improved performance by relying on getElementsByTagName where 22 | possible 23 | 24 | * Workaround IE bug where "javascript:" href attribute is URL 25 | encoded. (Issue 19) 26 | 27 | 2006-12-28 Google Inc. 28 | 29 | * Version 0.7 30 | * Released as http://ajaxslt.googlecode.com/svn/tags/release-0-7/ 31 | 32 | * Fixed a bug that semicolons are dropped by the XML parser when a 33 | text nodes also contains an entity. 34 | 35 | * Fixed a bug that xsl:variable definitions with a node set value 36 | encountered at the root level of the input document would throw an 37 | exception. 38 | 39 | * Fixed a bug that XPath expression @* always evaluated to the 40 | empty node set. 41 | 42 | * Fixed a bug that xsl:copy would copy only attribute and element 43 | nodes. 44 | 45 | * Fixed a bug that if xsl:apply-templates matches multiple 46 | templates, the output is sorted according to the order of the 47 | matching templates, and not according to the sort order defined 48 | for the selected node set to which templates are applied. 49 | 50 | * Added unittests for all fixed bugs. 51 | 52 | * Added wrapper function xmlOwnerDocument() to uniformly access 53 | the document on both document nodes and other nodes and use it 54 | throughout the xslt processor. 55 | 56 | 2006-12-14 Google Inc. 57 | 58 | * Version 0.6 59 | * Released as http://ajaxslt.googlecode.com/svn/tags/release-0-6/ 60 | * Fixes infinite loops in evaluation of XPath axes "ancestor", 61 | "ancestor-or-self", "preceding-sibling", "following-sibling". 62 | * Fixes evaluation of XPath axes "preceding", "following". 63 | * Added unittests for both. 64 | * Fixed xmlEscape*() functions to escape *all* markup characters 65 | not just the first. 66 | * Fixed xsl:copy-of to also copy CDATA and COMMENT nodes. 67 | 68 | 2006-09-10 Google Inc. 69 | 70 | * Version 0.5 71 | * Released on http://code.google.com/hosting/ 72 | * General changes: 73 | - remove all uses of for-in iteration 74 | - rename misc.js to util.js 75 | - log window is now in simplelog.js 76 | * XPath changes: 77 | - fixed id() function 78 | - fixed UnionExpr::evaluate() 79 | - added support for Unicode chracters 80 | * XSLT changes: 81 | - fixed xsl:sort in xsl:for-each (again) 82 | * DOM changes: 83 | - added a few methods 84 | * XML parser changes: 85 | - parses CDATA sections 86 | - parses comments 87 | - parses XML declaration 88 | - parses Unicode XML markup 89 | * Test changes: 90 | - added several jsunit tests 91 | 92 | 2005-10-19 Google Inc. 93 | 94 | * Version 0.4 95 | * XPath changes: 96 | - Optimize parsing of very common and simple expressions. 97 | - Fix use of XPath operator names -- div, mod, and, or -- 98 | as node names in abbreviated step expressions. 99 | - Fix root node -- it is now set to ownerDocument. 100 | * XSLT changes: 101 | - Fix xsl:sort in xsl:for-each. 102 | * DOM changes: 103 | - Add replaceChild(), insertBefore(), removeChild(). 104 | These are still not needed in XSLT processing, but 105 | in another client of the DOM implementation. 106 | - DOM nodes are recycled instead of garbage collected, 107 | in order to improve performance in some browsers. 108 | * Test changes: 109 | - Add many more test cases to the XPath tests. 110 | - Add a note mentioning jsunit in the README. 111 | - Add a DOM unittest file. 112 | 113 | 2005-08-27 Google Inc. 114 | 115 | * Version 0.3 (not released on sourceforge) 116 | * XPath changes: 117 | - Fix implementation of the * node test. 118 | - Fix implementation of the substring() function. 119 | - Fix non-abbreviated axis names. 120 | - Fix filter expressions. 121 | * XSLT changes: 122 | - Fix xsl:sort. 123 | * DOM changes: 124 | - Avoid using String.split() that breaks in IE. 125 | - Coerce nodeType to number and nodeName and nodeValue to string. 126 | - Fix SGML entity replacement of single quotes in attribute values. 127 | * Test changes: 128 | - Fix end tags of script elements in test HTML files. 129 | - Add jsunit tests for xpath.js. 130 | 131 | 132 | 2005-06-29 Google Inc. 133 | 134 | * Version 0.2 135 | * Add more missing code 136 | - XML parser and simple DOM implementation in dom.js 137 | - miscellaneous functions in misc.js. 138 | * Add simple test pages that serve as examples. 139 | - test-xpath.html tests and demonstrates the XPath parser. 140 | - test-xslt.html tests and demonstrates the XSLT processor. 141 | - output methods for debugging of XPath expressions added 142 | in xpathdebug.js. 143 | * Some additions and corrections in README and TODO 144 | - renamed XSL-T to XSLT, because that's more common. 145 | - miscellaneous updates. 146 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XSLT-processor 2 | 3 | _A JavaScript XSLT processor without native library dependencies._ 4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

17 | 18 | ## How to 19 | 20 | Install xslt-processor using [npm](https://docs.npmjs.com/about-npm) or [yarn](https://yarnpkg.com): 21 | 22 | ```sh 23 | npm install xslt-processor 24 | ``` 25 | 26 | ```sh 27 | yarn add xslt-processor 28 | ``` 29 | 30 | Within your ES2015+ code, import the `Xslt` class, the `XmlParser` class and use this way: 31 | 32 | ```js 33 | import { Xslt, XmlParser } from 'xslt-processor' 34 | 35 | // xmlString: string of xml file contents 36 | // xsltString: string of xslt file contents 37 | // outXmlString: output xml string. 38 | const xslt = new Xslt(); 39 | const xmlParser = new XmlParser(); 40 | // Either 41 | const outXmlString = await xslt.xsltProcess( 42 | xmlParser.xmlParse(xmlString), 43 | xmlParser.xmlParse(xsltString) 44 | ); 45 | // Or 46 | xslt.xsltProcess( 47 | xmlParser.xmlParse(xmlString), 48 | xmlParser.xmlParse(xsltString) 49 | ).then(output => { 50 | // `output` is equivalent to `outXmlString` (a string with XML). 51 | }); 52 | ``` 53 | 54 | To access the XPath parser, you can use the instance present at `Xslt` class: 55 | 56 | ```js 57 | const xslt = new Xslt(); 58 | const xPath = xslt.xPath; 59 | ``` 60 | 61 | Or ou can import it like this: 62 | 63 | ```js 64 | import { XPath } from 'xslt-processor' 65 | 66 | const xPath = new XPath(); 67 | ``` 68 | 69 | If you write pre-2015 JS code, make adjustments as needed. 70 | 71 | ### `Xslt` class options 72 | 73 | You can pass an `options` object to `Xslt` class: 74 | 75 | ```js 76 | const options = { 77 | escape: false, 78 | selfClosingTags: true, 79 | parameters: [{ name: 'myparam', value: '123' }], 80 | outputMethod: 'xml' 81 | }; 82 | const xslt = new Xslt(options); 83 | ``` 84 | 85 | - `cData` (`boolean`, default `true`): resolves CDATA elements in the output. Content under CDATA is resolved as text. This overrides `escape` for CDATA content. 86 | - `escape` (`boolean`, default `true`): replaces symbols like `<`, `>`, `&` and `"` by the corresponding [HTML/XML entities](https://www.tutorialspoint.com/xml/xml_character_entities.htm). Can be overridden by `disable-output-escaping`, that also does the opposite, unescaping `>` and `<` by `<` and `>`, respectively. 87 | - `selfClosingTags` (`boolean`, default `true`): Self-closes tags that don't have inner elements, if `true`. For instance, `` becomes ``. 88 | - `outputMethod` (`string`, default `xml`): Specifies the default output method. if `` is declared in your XSLT file, this will be overridden. 89 | - `parameters` (`array`, default `[]`): external parameters that you want to use. 90 | - `name`: the parameter name; 91 | - `namespaceUri` (optional): the namespace; 92 | - `value`: the value. 93 | 94 | ### Direct use in browsers 95 | 96 | You can simply add a tag like this: 97 | 98 | ```html 99 | 100 | ``` 101 | 102 | All the exports will live under `globalThis.XsltProcessor` and `window.XsltProcessor`. [See a usage example here](https://github.com/DesignLiquido/xslt-processor/blob/main/interactive-tests/xslt.html). 103 | 104 | ### Breaking Changes 105 | 106 | #### Version 2 107 | 108 | Until version 2.3.1, use like the example below: 109 | 110 | ```js 111 | import { Xslt, XmlParser } from 'xslt-processor' 112 | 113 | // xmlString: string of xml file contents 114 | // xsltString: string of xslt file contents 115 | // outXmlString: output xml string. 116 | const xslt = new Xslt(); 117 | const xmlParser = new XmlParser(); 118 | const outXmlString = xslt.xsltProcess( // Not async. 119 | xmlParser.xmlParse(xmlString), 120 | xmlParser.xmlParse(xsltString) 121 | ); 122 | ``` 123 | 124 | Version 3 received `` which relies on Fetch API, which is asynchronous. Version 2 doesn't support ``. 125 | 126 | If using Node.js older than version v17.5.0, please use version 3.2.3, that uses `node-fetch` package. Versions 3.3.0 onward require at least Node.js version v17.5.0, since they use native `fetch()` function. 127 | 128 | #### Version 1 129 | 130 | Until version 1.2.8, use like the example below: 131 | 132 | ```js 133 | import { Xslt, xmlParse } from 'xslt-processor' 134 | 135 | // xmlString: string of xml file contents 136 | // xsltString: string of xslt file contents 137 | // outXmlString: output xml string. 138 | const xslt = new Xslt(); 139 | const outXmlString = xslt.xsltProcess( 140 | xmlParse(xmlString), 141 | xmlParse(xsltString) 142 | ); 143 | ``` 144 | 145 | #### Version 0 146 | 147 | Until version 0.11.7, use like the example below: 148 | 149 | ```js 150 | import { xsltProcess, xmlParse } from 'xslt-processor' 151 | 152 | // xmlString: string of xml file contents 153 | // xsltString: string of xslt file contents 154 | // outXmlString: output xml string. 155 | const outXmlString = xsltProcess( 156 | xmlParse(xmlString), 157 | xmlParse(xsltString) 158 | ); 159 | ``` 160 | 161 | and to access the XPath parser: 162 | 163 | ```js 164 | import { xpathParse } from 'xslt-processor' 165 | ``` 166 | 167 | These functions are part of `Xslt` and `XPath` classes, respectively, at version 1.x onward. 168 | 169 | ## Introduction 170 | 171 | XSLT-processor contains an implementation of XSLT in JavaScript. Because XSLT uses XPath, it also contains an implementation of XPath that can be used 172 | independently of XSLT. This implementation has the advantage that it makes XSLT uniformly available whenever the browser's native `XSLTProcessor()` 173 | is not available such as in Node.js or in web workers. 174 | 175 | XSLT-processor builds on Google's [AJAXSLT](https://github.com/4031651/ajaxslt) which was written before `XSLTProcessor()` became available in browsers, but the 176 | code base has been updated to comply with ES2015+ and to make it work outside of browsers. 177 | 178 | This implementation of XSLT operates at the DOM level on its input documents. 179 | It internally uses a DOM implementation to create the output document, but usually 180 | returns the output document as text stream. The DOM to construct the output document can 181 | be supplied by the application, or else an internal minimal DOM implementation is used. This 182 | DOM comes with a minimal XML parser that can be used to generate a suitable DOM 183 | representation of the input documents if they are present as text. 184 | 185 | ## Tests and usage examples 186 | 187 | New tests are written in Jest an can be run by calling: `npm test`. 188 | 189 | The files `xslt.html` and `xpath.html` in the directory `interactive-tests` are interactive tests. They can be run directly from the file system; no HTTP server is needed. 190 | Both interactive tests and automatic tests demonstrate the use of the library functions. There is not much more documentation so far. 191 | 192 | ## Conformance 193 | 194 | A few features that are required by the XSLT and XPath standards were left out (but patches to add them are welcome). 195 | See our [TODO](TODO.md) for a list of missing features that we are aware of (please add more items by means of PRs). 196 | 197 | So far, we have implemented XQuery functions for versions 1.0 and 2.0, but this is not complete yet. 198 | 199 | Issues are also marked in the source code using throw-statements. 200 | 201 | The DOM implementation is minimal so as to support the XSLT processing, and not intended to be complete. 202 | 203 | The implementation is all agnostic about namespaces. It just expects XSLT elements to have tags that carry the `xsl:` prefix, but we disregard all namespace declaration for them. 204 | 205 | [There are a few nonstandard XPath functions](https://github.com/search?q=repo%3ADesignLiquido%2Fxslt-processor%20ext-&type=code). 206 | 207 | ### HTML Conformance 208 | 209 | HTML per se is not strict XML. Because of that, starting on version 2.0.0, this library handles HTML differently than XML: 210 | 211 | - For a document to be treated as HTML, it needs to have a `` tag defined with one of the following valid formats: 212 | - `` (for HTML5); 213 | - `` (for HTML4); 214 | - `` (for XHTML 1.1). 215 | - Tags like `
`, `` and `` don't need to be closed. The output for these tags doesn't close them (adding a `/` before the tag closes, or a corresponding close tag); 216 | - This rule doesn't apply for XHTML, which is strict XML. 217 | 218 | ## References 219 | 220 | - XPath Specification: http://www.w3.org/TR/1999/REC-xpath-19991116 221 | - XSLT Specification: http://www.w3.org/TR/1999/REC-xslt-19991116 222 | - W3C DOM Level 3 Core Specification: http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/ 223 | - ECMAScript Language Specification: http://www.ecma-international.org/publications/standards/Ecma-262.htm 224 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | XSLT-processor TODO 2 | ===== 3 | 4 | * Rethink match algorithm, as described in https://github.com/DesignLiquido/xslt-processor/pull/62#issuecomment-1636684453. There's a good number of issues open about this problem: 5 | * https://github.com/DesignLiquido/xslt-processor/issues/108 6 | * https://github.com/DesignLiquido/xslt-processor/issues/109 7 | * https://github.com/DesignLiquido/xslt-processor/issues/110 8 | * XSLT validation, besides the version number; 9 | * XSL:number 10 | * `attribute-set`, `decimal-format`, etc. (check `src/xslt.ts`) 11 | * `/html/body//ul/li|html/body//ol/li` has `/html/body//ul/li` evaluated by this XPath implementation as "absolute", and `/html/body//ol/li` as "relative". Both should be evaluated as "absolute". One idea is to rewrite the XPath logic entirely, since it is nearly impossible to debug it. 12 | * Implement `` with correct template precedence. 13 | 14 | Help is much appreciated. It seems to currently work for most of our purposes, but fixes and additions are always welcome! 15 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-react', 5 | { 6 | pragma: 'dom', 7 | throwIfNamespace: false 8 | } 9 | ], 10 | '@babel/preset-env', 11 | '@babel/preset-typescript' 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /docs/original_license.txt: -------------------------------------------------------------------------------- 1 | The following BSD-3 clause license covers the original code. The terms of the 2 | license need to be respected. The current code is available under the terms of 3 | the LGPL v3 license (see LICENSE). 4 | 5 | 6 | Copyright (c) 2005,2006 Google Inc. 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are 11 | met: 12 | 13 | * Redistributions of source code must retain the above copyright 14 | notice, this list of conditions and the following disclaimer. 15 | 16 | * Redistributions in binary form must reproduce the above copyright 17 | notice, this list of conditions and the following disclaimer in the 18 | documentation and/or other materials provided with the 19 | distribution. 20 | 21 | * Neither the name of Google Inc. nor the names of its contributors 22 | may be used to endorse or promote products derived from this 23 | software without specific prior written permission. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 27 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 28 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 29 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 30 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 31 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 32 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 33 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 34 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | -------------------------------------------------------------------------------- /examples/head.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | </head> 9 | <body> 10 | <div id="container"> 11 | <div id="header"> 12 | <div id="menu"> 13 | <ul> 14 | <li><a href="#" class="active">Home</a></li> 15 | <li><a href="#">about</a></li> 16 | </ul> 17 | </div> 18 | </div> 19 | </div> 20 | </body> 21 | </html> 22 | </xsl:template> 23 | </xsl:stylesheet> 24 | -------------------------------------------------------------------------------- /interactive-tests/js/simplelog.js: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Design Liquido 2 | // Copyright 2018 Johannes Wilm 3 | // Copyright 2005-2006 Google 4 | // 5 | // Author: Steffen Meschkat <mesch@google.com> 6 | // 7 | // A very simple logging facility, used in test/xpath.html. 8 | 9 | export class Log { 10 | 11 | constructor() { 12 | this.lines = []; 13 | } 14 | 15 | static write(s) { 16 | this.lines.push(window.xmlEscapeText(s)); 17 | this.show(); 18 | } 19 | 20 | // Writes the given XML with every tag on a new line. 21 | static writeXML(xml) { 22 | const s0 = xml.replace(/</g, '\n<'); 23 | const s1 = window.xmlEscapeText(s0); 24 | const s2 = s1.replace(/\s*\n(\s|\n)*/g, '<br/>'); 25 | this.lines.push(s2); 26 | this.show(); 27 | } 28 | 29 | // Writes without any escaping 30 | static writeRaw(s) { 31 | this.lines.push(s); 32 | this.show(); 33 | } 34 | 35 | static clear() { 36 | const l = this.div(); 37 | l.innerHTML = ''; 38 | this.lines = []; 39 | } 40 | 41 | static show() { 42 | const l = this.div(); 43 | l.innerHTML += `${this.lines.join('<br/>')}<br/>`; 44 | this.lines = []; 45 | l.scrollTop = l.scrollHeight; 46 | } 47 | 48 | static div() { 49 | let l = document.getElementById('log'); 50 | if (!l) { 51 | l = document.createElement('div'); 52 | l.id = 'log'; 53 | l.style.position = 'absolute'; 54 | l.style.right = '5px'; 55 | l.style.top = '5px'; 56 | l.style.width = '250px'; 57 | l.style.height = '150px'; 58 | l.style.overflow = 'auto'; 59 | l.style.backgroundColor = '#f0f0f0'; 60 | l.style.border = '1px solid gray'; 61 | l.style.fontSize = '10px'; 62 | l.style.padding = '5px'; 63 | document.body.appendChild(l); 64 | } 65 | return l; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /interactive-tests/js/xpath_script.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Johannes Wilm 2 | // Copyright 2005 Google Inc. 3 | // All Rights Reserved 4 | // 5 | // Tests for the XPath parser. To run the test, open the file from the 6 | // file system. No server support is required. 7 | // 8 | // 9 | // Author: Steffen Meschkat <mesch@google.com> 10 | import { 11 | Log 12 | } from "./simplelog.js" 13 | import { 14 | xpathParse 15 | } from "../src/xpath.js" 16 | import { 17 | expr 18 | } from "../tests_src/xpath_unittest.js" 19 | import { 20 | parseTree 21 | } from "../../src/xpathdebug.js" 22 | 23 | window.logging = true; 24 | window.xpathdebug = true; 25 | 26 | window.load_expr = () => { 27 | const s = document.getElementById('s'); 28 | for (let i = 0; i < expr.length; ++i) { 29 | const o = new Option(expr[i].replace(/>/, '>').replace(/</, '<')); 30 | s.options[s.options.length] = o; 31 | } 32 | s.selectedIndex = 0; 33 | } 34 | 35 | let log = new Log() 36 | 37 | window.xpath_test = form => { 38 | log.clear(); 39 | try { 40 | const i = form.cases.selectedIndex; 41 | const options = form.cases.options; 42 | 43 | const text = options[i].value; 44 | log.writeRaw(`<tt><b>${text}</b></tt>`); 45 | 46 | const expr = xpathParse(text, message => log.write(message)); 47 | log.writeRaw(`<tt><b>${text}</b></tt>`); 48 | log.writeRaw(`<pre>${parseTree(expr, '')}</pre>`); 49 | 50 | options[i].selected = false; 51 | if (i < options.length - 1) { 52 | options[i + 1].selected = true; 53 | } else { 54 | options[0].selected = true; 55 | } 56 | 57 | } catch (e) { 58 | log.write(`EXCEPTION ${e}`); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /interactive-tests/js/xslt_script.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Johannes Wilm 2 | // Copyright 2005 Google Inc. 3 | // All Rights Reserved 4 | // 5 | // Tests for the XSLT processor. To run the test, open the file from 6 | // the file system. No server support is required. 7 | // 8 | // 9 | // Author: Steffen Meschkat <mesch@google.com> 10 | import { 11 | XmlParser 12 | } from "../../src/dom" 13 | import { 14 | xsltProcess 15 | } from "../../src/xslt" 16 | 17 | window.logging = true; 18 | window.xsltdebug = true; 19 | 20 | window.el = function(id) { 21 | return document.getElementById(id); 22 | } 23 | 24 | window.test_xslt = function() { 25 | const xmlParser = new XmlParser(); 26 | const xml = xmlParser.xmlParse(el('xml').value); 27 | const xslt = xmlParser.xmlParse(el('xslt').value); 28 | const html = xsltProcess(xml, xslt); 29 | el('html').value = html; 30 | el('htmldisplay').innerHTML = html; 31 | } 32 | 33 | window.cleanxml = function() { 34 | cleanvalue('xml'); 35 | cleanvalue('xslt'); 36 | } 37 | 38 | window.cleanvalue = function(id) { 39 | const x = el(id); 40 | x.value = x.value.replace(/^\s*/, '').replace(/\n\s*/g, '\n'); 41 | } 42 | -------------------------------------------------------------------------------- /interactive-tests/xpath.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <head> 3 | <script type="application/javascript" src="js/xslt-processor.js"></script> 4 | <script> 5 | // Copyright 2023-2024 Design Liquido 6 | // Copyright 2018 Johannes Wilm 7 | // Copyright 2005 Google Inc. 8 | // All Rights Reserved 9 | // 10 | // Tests for the XPath parser. To run the test, open the file from the 11 | // file system. No server support is required. 12 | // 13 | // 14 | // Author: Steffen Meschkat <mesch@google.com> 15 | window.logging = true; 16 | window.xpathdebug = true; 17 | 18 | class Log { 19 | constructor() { 20 | this.lines = []; 21 | } 22 | 23 | static write(s) { 24 | this.lines.push(globalThis.XsltProcessor.xmlEscapeText(s)); 25 | this.show(); 26 | } 27 | 28 | // Writes the given XML with every tag on a new line. 29 | static writeXML(xml) { 30 | const s0 = xml.replace(/</g, '\n<'); 31 | const s1 = global.XsltProcessor.xmlEscapeText(s0); 32 | const s2 = s1.replace(/\s*\n(\s|\n)*/g, '<br/>'); 33 | this.lines.push(s2); 34 | this.show(); 35 | } 36 | 37 | // Writes without any escaping 38 | static writeRaw(s) { 39 | this.lines.push(s); 40 | this.show(); 41 | } 42 | 43 | static clear() { 44 | const l = this.div(); 45 | l.innerHTML = ''; 46 | this.lines = []; 47 | } 48 | 49 | static show() { 50 | const l = this.div(); 51 | l.innerHTML += `${this.lines.join('<br/>')}<br/>`; 52 | this.lines = []; 53 | l.scrollTop = l.scrollHeight; 54 | } 55 | 56 | static div() { 57 | let l = document.getElementById('log'); 58 | if (!l) { 59 | l = document.createElement('div'); 60 | l.id = 'log'; 61 | l.style.position = 'absolute'; 62 | l.style.right = '5px'; 63 | l.style.top = '5px'; 64 | l.style.width = '250px'; 65 | l.style.height = '150px'; 66 | l.style.overflow = 'auto'; 67 | l.style.backgroundColor = '#f0f0f0'; 68 | l.style.border = '1px solid gray'; 69 | l.style.fontSize = '10px'; 70 | l.style.padding = '5px'; 71 | document.body.appendChild(l); 72 | } 73 | return l; 74 | } 75 | } 76 | 77 | window.load_expr = () => { 78 | const s = document.getElementById('s'); 79 | const expr = ['<xsl:stylesheet>']; 80 | for (let i = 0; i < expr.length; ++i) { 81 | const o = new Option(expr[i].replace(/>/, '>').replace(/</, '<')); 82 | s.options[s.options.length] = o; 83 | } 84 | s.selectedIndex = 0; 85 | }; 86 | 87 | let log = new Log(); 88 | 89 | window.xpath_test = (form) => { 90 | log.clear(); 91 | try { 92 | const i = form.cases.selectedIndex; 93 | const options = form.cases.options; 94 | 95 | const text = options[i].value; 96 | log.writeRaw(`<tt><b>${text}</b></tt>`); 97 | 98 | const expr = xpathParse(text, (message) => log.write(message)); 99 | log.writeRaw(`<tt><b>${text}</b></tt>`); 100 | log.writeRaw(`<pre>${parseTree(expr, '')}</pre>`); 101 | 102 | options[i].selected = false; 103 | if (i < options.length - 1) { 104 | options[i + 1].selected = true; 105 | } else { 106 | options[0].selected = true; 107 | } 108 | } catch (e) { 109 | log.write(`EXCEPTION ${e}`); 110 | } 111 | }; 112 | </script> 113 | </head> 114 | <body onload="window.load_expr()"> 115 | <form onsubmit="window.xpath_test(this);return false" action="javascript:void(0)"> 116 | <select id="s" multiple="1" size="30" name="cases"></select> 117 | <input type="submit" value="parse" align="top" /> 118 | </form> 119 | </body> 120 | </html> 121 | -------------------------------------------------------------------------------- /interactive-tests/xslt.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 2 | <html> 3 | <head> 4 | <title>Simple XSLT test 5 | 6 | 36 | 37 | 38 |
39 | 40 | 41 | 50 | 65 | 66 | 67 | 70 | 71 | 72 | 76 | 79 | 80 |
42 | 49 | 51 | 64 |
68 | 69 |
73 | 75 | 77 |
78 |
81 |
82 | 83 | 84 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | export default async (): Promise => ({ 3 | verbose: true, 4 | modulePathIgnorePatterns: [ 5 | '/dist/' 6 | ], 7 | testEnvironment: 'node', 8 | coverageReporters: ['json-summary', 'lcov', 'text', 'text-summary'], 9 | displayName: { 10 | name: 'xslt-transform', 11 | color: 'greenBright' 12 | }, 13 | detectOpenHandles: true, 14 | preset: 'ts-jest', 15 | transform: { 16 | '^.+\\.(ts|tsx)?$': 'ts-jest', 17 | '^.+\\.(js|jsx)$': 'babel-jest' 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xslt-processor", 3 | "version": "3.3.1", 4 | "description": "A JavaScript XSLT Processor", 5 | "main": "index.js", 6 | "module": "index.js", 7 | "directories": { 8 | "doc": "docs", 9 | "test": "tests" 10 | }, 11 | "scripts": { 12 | "test": "jest", 13 | "pre-build-setup": "rimraf ./demo && rimraf ./dist", 14 | "build": "yarn pre-build-setup && yarn cjs-build && yarn rollup-build && yarn copy-files-from-to", 15 | "cjs-build": "tsc", 16 | "rollup-build": "rollup src/index.ts -c -f umd -o dist/umd/xslt-processor.js", 17 | "lint": "eslint src/**/*" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/DesignLiquido/xslt-processor.git" 22 | }, 23 | "keywords": [ 24 | "xslt", 25 | "xpath", 26 | "xml" 27 | ], 28 | "author": "Johannes Wilm", 29 | "contributors": [ 30 | { 31 | "name": "Leonel Sanches da Silva", 32 | "url": "https://www.linkedin.com/in/leonelsanchesdasilva/" 33 | } 34 | ], 35 | "license": "LGPL-3.0", 36 | "bugs": { 37 | "url": "https://github.com/DesignLiquido/xslt-processor/issues" 38 | }, 39 | "homepage": "https://github.com/DesignLiquido/xslt-processor#readme", 40 | "devDependencies": { 41 | "@babel/cli": "^7.22.5", 42 | "@babel/core": "^7.22.5", 43 | "@babel/eslint-parser": "^7.22.5", 44 | "@babel/preset-env": "^7.22.5", 45 | "@babel/preset-react": "^7.22.5", 46 | "@babel/preset-typescript": "^7.22.5", 47 | "@rollup/plugin-terser": "^0.4.3", 48 | "@rollup/plugin-typescript": "^11.1.1", 49 | "@types/he": "^1.2.0", 50 | "@types/jest": "^29.5.12", 51 | "@types/node": "^22.9.0", 52 | "@types/node-fetch": "^2.6.11", 53 | "@typescript-eslint/eslint-plugin": "^8.4.0", 54 | "@typescript-eslint/parser": "^8.4.0", 55 | "babel-jest": "^29.7.0", 56 | "copy-files-from-to": "^3.9.0", 57 | "copyfiles": "^2.4.1", 58 | "eslint": "^9.12.0", 59 | "eslint-plugin-jest": "^28.8.3", 60 | "eslint-plugin-jsx": "^0.1.0", 61 | "jest": "^29.7.0", 62 | "npm-check-updates": "^16.10.13", 63 | "release-it": "^17.6.0", 64 | "rimraf": "^5.0.1", 65 | "rollup": "^1.1.2", 66 | "rollup-plugin-commonjs": "^9.2.0", 67 | "rollup-plugin-node-resolve": "^4.0.0", 68 | "rollup-plugin-terser": "^4.0.3", 69 | "ts-jest": "^29.2.5", 70 | "ts-node": "^10.9.2", 71 | "typescript": "^5.5.4" 72 | }, 73 | "dependencies": { 74 | "he": "^1.2.0" 75 | }, 76 | "copyFiles": [ 77 | { 78 | "from": "LICENSE", 79 | "to": "dist/LICENSE" 80 | }, 81 | { 82 | "from": "README.md", 83 | "to": "dist/README.md" 84 | }, 85 | { 86 | "from": "interactive-tests/xslt.html", 87 | "to": "demo/xslt.html" 88 | }, 89 | { 90 | "from": "dist/umd/xslt-processor.js", 91 | "to": "demo/js/xslt-processor.js" 92 | } 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import terser from '@rollup/plugin-terser'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | 6 | export default { 7 | plugins: [ 8 | typescript({ 9 | exclude: [ 10 | "**/__tests__", 11 | "**/*.test.ts", 12 | "jest.config.ts" 13 | ], 14 | tsconfig: './tsconfig.rollup.json' 15 | }), 16 | commonjs(), 17 | resolve(), 18 | terser() 19 | ], 20 | output: { 21 | format: 'umd', 22 | name: 'XsltProcessor', 23 | sourcemap: true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | // Based on 2 | export const DOM_ELEMENT_NODE = 1; 3 | export const DOM_ATTRIBUTE_NODE = 2; 4 | export const DOM_TEXT_NODE = 3; 5 | export const DOM_CDATA_SECTION_NODE = 4; 6 | export const DOM_ENTITY_REFERENCE_NODE = 5; 7 | export const DOM_ENTITY_NODE = 6; 8 | export const DOM_PROCESSING_INSTRUCTION_NODE = 7; 9 | export const DOM_COMMENT_NODE = 8; 10 | export const DOM_DOCUMENT_NODE = 9; 11 | export const DOM_DOCUMENT_TYPE_NODE = 10; 12 | export const DOM_DOCUMENT_FRAGMENT_NODE = 11; 13 | export const DOM_NOTATION_NODE = 12; 14 | -------------------------------------------------------------------------------- /src/dom/functions.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Design Liquido 2 | // Copyright 2018 Johannes Wilm 3 | // Copyright 2005 Google Inc. 4 | // All Rights Reserved 5 | 6 | import { XDocument } from "./xdocument"; 7 | import { XNode } from './xnode'; 8 | 9 | // Wrapper around DOM methods so we can condense their invocations. 10 | export function domGetAttributeValue(node: XNode, name: string) { 11 | return node.getAttributeValue(name); 12 | } 13 | 14 | export function domSetAttribute(node: XNode, name: string, value: any) { 15 | return node.setAttribute(name, value); 16 | } 17 | 18 | export function domSetTransformedAttribute(node: XNode, name: string, value: any) { 19 | return node.setTransformedAttribute(name, value); 20 | } 21 | 22 | export function domAppendChild(node: XNode, child: any) { 23 | return node.appendChild(child); 24 | } 25 | 26 | export function domAppendTransformedChild(node: XNode, child: any) { 27 | return node.appendTransformedChild(child); 28 | } 29 | 30 | export function domCreateTextNode(node: XDocument, text: string) { 31 | return node.createTextNode(text); 32 | } 33 | 34 | export function domCreateTransformedTextNode(node: XDocument, text: string) { 35 | return node.createTransformedTextNode(text); 36 | } 37 | 38 | export function domCreateElement(doc: XDocument, name: string) { 39 | return doc.createElement(name); 40 | } 41 | 42 | export function domCreateCDATASection(doc: XDocument, data: any) { 43 | return doc.createCDATASection(data); 44 | } 45 | 46 | export function domCreateComment(doc: any, text: any) { 47 | return doc.createComment(text); 48 | } 49 | 50 | export function domCreateDocumentFragment(doc: XDocument): XNode { 51 | return doc.createDocumentFragment(); 52 | } 53 | 54 | export function domCreateDTDSection(doc: XDocument, data: any) { 55 | return doc.createDTDSection(data); 56 | } 57 | 58 | //XDocument.prototype = new XNode(DOM_DOCUMENT_NODE, '#document'); 59 | -------------------------------------------------------------------------------- /src/dom/index.ts: -------------------------------------------------------------------------------- 1 | export * from './functions'; 2 | export * from './xdocument'; 3 | export * from './xml-functions'; 4 | export * from './xml-output-options'; 5 | export * from './xml-parser'; 6 | export * from './xbrowser-node'; 7 | export * from './xnode'; 8 | -------------------------------------------------------------------------------- /src/dom/util.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Design Liquido 2 | // Copyright 2018 Johannes Wilm 3 | // Copyright 2005 Google 4 | // 5 | // Original author: Steffen Meschkat 6 | // 7 | // Miscellaneous utility and placeholder functions. 8 | // Dummy implmentation for the logging functions. Replace by something 9 | // useful when you want to debug. 10 | 11 | // Applies the given function to each element of the array, preserving 12 | // this, and passing the index. 13 | export function mapExec(array: any[], func: Function) { 14 | for (let i = 0; i < array.length; ++i) { 15 | func.call(this, array[i], i); 16 | } 17 | } 18 | 19 | // Returns an array that contains the return value of the given 20 | // function applied to every element of the input array. 21 | export function mapExpr(array: any[], func: Function) { 22 | const ret = []; 23 | for (let i = 0; i < array.length; ++i) { 24 | ret.push(func(array[i])); 25 | } 26 | return ret; 27 | } 28 | 29 | /** 30 | * Reverses the given array in place. 31 | * @param array The array to be reversed. 32 | */ 33 | export function reverseInPlace(array: any[]) { 34 | for (let i = 0; i < array.length / 2; ++i) { 35 | const h = array[i]; 36 | const ii = array.length - i - 1; 37 | array[i] = array[ii]; 38 | array[ii] = h; 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/dom/xbrowser-node.ts: -------------------------------------------------------------------------------- 1 | import { XNode } from "./xnode"; 2 | 3 | /** 4 | * Special XNode class, that retains properties from browsers like 5 | * IE, Opera, Safari, etc. 6 | */ 7 | export class XBrowserNode extends XNode { 8 | innerText?: string; 9 | textContent?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/dom/xdocument.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DOM_ATTRIBUTE_NODE, 3 | DOM_CDATA_SECTION_NODE, 4 | DOM_COMMENT_NODE, 5 | DOM_DOCUMENT_FRAGMENT_NODE, 6 | DOM_DOCUMENT_NODE, 7 | DOM_DOCUMENT_TYPE_NODE, 8 | DOM_ELEMENT_NODE, 9 | DOM_TEXT_NODE 10 | } from '../constants'; 11 | import { XNode } from './xnode'; 12 | 13 | export class XDocument extends XNode { 14 | documentElement: any; 15 | 16 | constructor() { 17 | // NOTE(mesch): According to the DOM Spec, ownerDocument of a 18 | // document node is null. 19 | super(DOM_DOCUMENT_NODE, '#document', null, null); 20 | this.documentElement = null; 21 | } 22 | 23 | // TODO: Do we still need this? 24 | /* clear() { 25 | XNode.recycle(this.documentElement); 26 | this.documentElement = null; 27 | } */ 28 | 29 | appendChild(node: any) { 30 | super.appendChild(node); 31 | this.documentElement = this.childNodes[0]; 32 | } 33 | 34 | createElement(name: string): XNode { 35 | return XNode.create(DOM_ELEMENT_NODE, name, null, this); 36 | } 37 | 38 | createElementNS(namespace: any, name: any) { 39 | return XNode.create(DOM_ELEMENT_NODE, name, null, this, namespace); 40 | } 41 | 42 | createDocumentFragment(): XNode { 43 | return XNode.create(DOM_DOCUMENT_FRAGMENT_NODE, '#document-fragment', null, this); 44 | } 45 | 46 | createTextNode(value: any) { 47 | return XNode.create(DOM_TEXT_NODE, '#text', value, this); 48 | } 49 | 50 | createTransformedTextNode(value: any) { 51 | const node = XNode.create(DOM_TEXT_NODE, '#text', value, this); 52 | node.transformedNodeValue = value; 53 | return node; 54 | } 55 | 56 | createAttribute(name: any) { 57 | return XNode.create(DOM_ATTRIBUTE_NODE, name, null, this); 58 | } 59 | 60 | createAttributeNS(namespace: any, name: any) { 61 | return XNode.create(DOM_ATTRIBUTE_NODE, name, null, this, namespace); 62 | } 63 | 64 | createComment(data: any) { 65 | return XNode.create(DOM_COMMENT_NODE, '#comment', data, this); 66 | } 67 | 68 | createCDATASection(data: any) { 69 | return XNode.create(DOM_CDATA_SECTION_NODE, '#cdata-section', data, this); 70 | } 71 | 72 | createDTDSection(data: any) { 73 | return XNode.create(DOM_DOCUMENT_TYPE_NODE, '#dtd-section', data, this); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/dom/xml-output-options.ts: -------------------------------------------------------------------------------- 1 | export type XmlOutputOptions = { 2 | cData: boolean; 3 | escape: boolean; 4 | selfClosingTags: boolean; 5 | outputMethod: 'xml' | 'html' | 'text' | 'name' 6 | } 7 | -------------------------------------------------------------------------------- /src/dom/xmltoken.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Design Liquido 2 | // Copyright 2018 Johannes Wilm 3 | // Copyright 2006 Google Inc. 4 | // All Rights Reserved 5 | // 6 | // Defines regular expression patterns to extract XML tokens from string. 7 | // See , 8 | // and 9 | // for the specifications. 10 | // 11 | // Original author: Junji Takagi 12 | 13 | // Common tokens in XML 1.0 and XML 1.1. 14 | 15 | const XML_S = '[ \t\r\n]+'; 16 | const XML_EQ = `(${XML_S})?=(${XML_S})?`; 17 | export const XML_CHAR_REF = '&#[0-9]+;|&#x[0-9a-fA-F]+;'; 18 | 19 | // XML 1.0 tokens. 20 | 21 | export const XML10_VERSION_INFO = `${XML_S}version${XML_EQ}("1\\.0"|'1\\.0')`; 22 | const XML10_BASE_CHAR = 23 | '\u0041-\u005a\u0061-\u007a\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff' + 24 | '\u0100-\u0131\u0134-\u013e\u0141-\u0148\u014a-\u017e\u0180-\u01c3' + 25 | '\u01cd-\u01f0\u01f4-\u01f5\u01fa-\u0217\u0250-\u02a8\u02bb-\u02c1\u0386' + 26 | '\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03ce\u03d0-\u03d6\u03da\u03dc' + 27 | '\u03de\u03e0\u03e2-\u03f3\u0401-\u040c\u040e-\u044f\u0451-\u045c' + 28 | '\u045e-\u0481\u0490-\u04c4\u04c7-\u04c8\u04cb-\u04cc\u04d0-\u04eb' + 29 | '\u04ee-\u04f5\u04f8-\u04f9\u0531-\u0556\u0559\u0561-\u0586\u05d0-\u05ea' + 30 | '\u05f0-\u05f2\u0621-\u063a\u0641-\u064a\u0671-\u06b7\u06ba-\u06be' + 31 | '\u06c0-\u06ce\u06d0-\u06d3\u06d5\u06e5-\u06e6\u0905-\u0939\u093d' + 32 | '\u0958-\u0961\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2' + 33 | '\u09b6-\u09b9\u09dc-\u09dd\u09df-\u09e1\u09f0-\u09f1\u0a05-\u0a0a' + 34 | '\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36' + 35 | '\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8b\u0a8d' + 36 | '\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9' + 37 | '\u0abd\u0ae0\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30' + 38 | '\u0b32-\u0b33\u0b36-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b85-\u0b8a' + 39 | '\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4' + 40 | '\u0ba8-\u0baa\u0bae-\u0bb5\u0bb7-\u0bb9\u0c05-\u0c0c\u0c0e-\u0c10' + 41 | '\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c60-\u0c61\u0c85-\u0c8c' + 42 | '\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cde\u0ce0-\u0ce1' + 43 | '\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d60-\u0d61' + 44 | '\u0e01-\u0e2e\u0e30\u0e32-\u0e33\u0e40-\u0e45\u0e81-\u0e82\u0e84' + 45 | '\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5' + 46 | '\u0ea7\u0eaa-\u0eab\u0ead-\u0eae\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4' + 47 | '\u0f40-\u0f47\u0f49-\u0f69\u10a0-\u10c5\u10d0-\u10f6\u1100\u1102-\u1103' + 48 | '\u1105-\u1107\u1109\u110b-\u110c\u110e-\u1112\u113c\u113e\u1140\u114c' + 49 | '\u114e\u1150\u1154-\u1155\u1159\u115f-\u1161\u1163\u1165\u1167\u1169' + 50 | '\u116d-\u116e\u1172-\u1173\u1175\u119e\u11a8\u11ab\u11ae-\u11af' + 51 | '\u11b7-\u11b8\u11ba\u11bc-\u11c2\u11eb\u11f0\u11f9\u1e00-\u1e9b' + 52 | '\u1ea0-\u1ef9\u1f00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d' + 53 | '\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc' + 54 | '\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec' + 55 | '\u1ff2-\u1ff4\u1ff6-\u1ffc\u2126\u212a-\u212b\u212e\u2180-\u2182' + 56 | '\u3041-\u3094\u30a1-\u30fa\u3105-\u312c\uac00-\ud7a3'; 57 | const XML10_IDEOGRAPHIC = '\u4e00-\u9fa5\u3007\u3021-\u3029'; 58 | const XML10_COMBINING_CHAR = 59 | '\u0300-\u0345\u0360-\u0361\u0483-\u0486\u0591-\u05a1\u05a3-\u05b9' + 60 | '\u05bb-\u05bd\u05bf\u05c1-\u05c2\u05c4\u064b-\u0652\u0670\u06d6-\u06dc' + 61 | '\u06dd-\u06df\u06e0-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u0901-\u0903\u093c' + 62 | '\u093e-\u094c\u094d\u0951-\u0954\u0962-\u0963\u0981-\u0983\u09bc\u09be' + 63 | '\u09bf\u09c0-\u09c4\u09c7-\u09c8\u09cb-\u09cd\u09d7\u09e2-\u09e3\u0a02' + 64 | '\u0a3c\u0a3e\u0a3f\u0a40-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a70-\u0a71' + 65 | '\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0b01-\u0b03' + 66 | '\u0b3c\u0b3e-\u0b43\u0b47-\u0b48\u0b4b-\u0b4d\u0b56-\u0b57\u0b82-\u0b83' + 67 | '\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0c01-\u0c03\u0c3e-\u0c44' + 68 | '\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c82-\u0c83\u0cbe-\u0cc4' + 69 | '\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5-\u0cd6\u0d02-\u0d03\u0d3e-\u0d43' + 70 | '\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1' + 71 | '\u0eb4-\u0eb9\u0ebb-\u0ebc\u0ec8-\u0ecd\u0f18-\u0f19\u0f35\u0f37\u0f39' + 72 | '\u0f3e\u0f3f\u0f71-\u0f84\u0f86-\u0f8b\u0f90-\u0f95\u0f97\u0f99-\u0fad' + 73 | '\u0fb1-\u0fb7\u0fb9\u20d0-\u20dc\u20e1\u302a-\u302f\u3099\u309a'; 74 | const XML10_DIGIT = 75 | '\u0030-\u0039\u0660-\u0669\u06f0-\u06f9\u0966-\u096f\u09e6-\u09ef' + 76 | '\u0a66-\u0a6f\u0ae6-\u0aef\u0b66-\u0b6f\u0be7-\u0bef\u0c66-\u0c6f' + 77 | '\u0ce6-\u0cef\u0d66-\u0d6f\u0e50-\u0e59\u0ed0-\u0ed9\u0f20-\u0f29'; 78 | const XML10_EXTENDER = '\u00b7\u02d0\u02d1\u0387\u0640\u0e46\u0ec6\u3005\u3031-\u3035' + '\u309d-\u309e\u30fc-\u30fe'; 79 | const XML10_LETTER = XML10_BASE_CHAR + XML10_IDEOGRAPHIC; 80 | const XML10_NAME_CHAR = `${XML10_LETTER + XML10_DIGIT}\\._:${XML10_COMBINING_CHAR}${XML10_EXTENDER}-`; 81 | export const XML10_NAME = `[${XML10_LETTER}_:][${XML10_NAME_CHAR}]*`; 82 | 83 | export const XML10_ENTITY_REF = `&${XML10_NAME};`; 84 | const XML10_REFERENCE = `${XML10_ENTITY_REF}|${XML_CHAR_REF}`; 85 | export const XML10_ATT_VALUE = `"(([^<&"]|${XML10_REFERENCE})*)"|'(([^<&']|${XML10_REFERENCE})*)'`; 86 | export const XML10_ATTRIBUTE = `(${XML10_NAME})${XML_EQ}(${XML10_ATT_VALUE})`; 87 | 88 | // XML 1.1 tokens. 89 | // TODO(jtakagi): NameStartChar also includes \u10000-\ueffff. 90 | // ECMAScript Language Specifiction defines UnicodeEscapeSequence as 91 | // "\u HexDigit HexDigit HexDigit HexDigit" and we may need to use 92 | // surrogate pairs, but any browser doesn't support surrogate paris in 93 | // character classes of regular expression, so avoid including them for now. 94 | 95 | export const XML11_VERSION_INFO = `${XML_S}version${XML_EQ}("1\\.1"|'1\\.1')`; 96 | const XML11_NAME_START_CHAR = 97 | ':A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff\u0370-\u037d' + 98 | '\u037f-\u1fff\u200c-\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff' + 99 | '\uf900-\ufdcf\ufdf0-\ufffd'; 100 | const XML11_NAME_CHAR = XML11_NAME_START_CHAR + '\\.0-9\u00b7\u0300-\u036f\u203f-\u2040-'; 101 | export const XML11_NAME = `[${XML11_NAME_START_CHAR}][${XML11_NAME_CHAR}]*`; 102 | 103 | export const XML11_ENTITY_REF = `&${XML11_NAME};`; 104 | const XML11_REFERENCE = `${XML11_ENTITY_REF}|${XML_CHAR_REF}`; 105 | export const XML11_ATT_VALUE = `"(([^<&"]|${XML11_REFERENCE})*)"|'(([^<&']|${XML11_REFERENCE})*)'`; 106 | export const XML11_ATTRIBUTE = `(${XML11_NAME})${XML_EQ}(${XML11_ATT_VALUE})`; 107 | 108 | // XML Namespace tokens. 109 | // Used in XML parser and XPath parser. 110 | 111 | const XML_NC_NAME_CHAR = `${XML10_LETTER + XML10_DIGIT}\\._${XML10_COMBINING_CHAR}${XML10_EXTENDER}-`; 112 | export const XML_NC_NAME = `[${XML10_LETTER}_][${XML_NC_NAME_CHAR}]*`; 113 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { XPath } from './xpath'; 2 | export { Xslt, XsltOptions } from './xslt'; 3 | export { XmlParser, xmlEscapeText } from './dom'; 4 | export { ExprContext } from './xpath'; 5 | -------------------------------------------------------------------------------- /src/xpath/common-function.ts: -------------------------------------------------------------------------------- 1 | // Shallow-copies an array to the end of another array 2 | // Basically Array.concat, but works with other non-array collections 3 | export function copyArray(dst: any[], src: any[]) { 4 | if (!src) return; 5 | const dstLength = dst.length; 6 | for (let i = src.length - 1; i >= 0; --i) { 7 | dst[i + dstLength] = src[i]; 8 | } 9 | } 10 | 11 | /** 12 | * This is an optimization for copying attribute lists in IE. IE includes many 13 | * extraneous properties in its DOM attribute lists, which take require 14 | * significant extra processing when evaluating attribute steps. With this 15 | * function, we ignore any such attributes that has an empty string value. 16 | */ 17 | export function copyArrayIgnoringAttributesWithoutValue(dst: any[], src: any[]) { 18 | if (!src) return; 19 | for (let i = src.length - 1; i >= 0; --i) { 20 | // this test will pass so long as the attribute has a non-empty string 21 | // value, even if that value is "false", "0", "undefined", etc. 22 | if (src[i].nodeValue) { 23 | dst.push(src[i]); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/xpath/expr-context.ts: -------------------------------------------------------------------------------- 1 | import { DOM_DOCUMENT_NODE } from '../constants'; 2 | import { BooleanValue } from './values/boolean-value'; 3 | import { NodeSetValue } from './values/node-set-value'; 4 | import { NumberValue } from './values/number-value'; 5 | import { StringValue } from './values/string-value'; 6 | import { TOK_NUMBER } from './tokens'; 7 | import { XNode } from '../dom'; 8 | import { XsltDecimalFormatSettings } from '../xslt/xslt-decimal-format-settings'; 9 | import { NodeValue } from './values'; 10 | 11 | /** 12 | * XPath expression evaluation context. An XPath context consists of a 13 | * DOM node, a list of DOM nodes that contains this node, a number 14 | * that represents the position of the single node in the list, and a 15 | * current set of variable bindings. (See XPath spec.) 16 | * 17 | * setVariable(name, expr) -- binds given XPath expression to the 18 | * name. 19 | * 20 | * getVariable(name) -- what the name says. 21 | * 22 | * setNode(position) -- sets the context to the node at the given 23 | * position. Needed to implement scoping rules for variables in 24 | * XPath. (A variable is visible to all subsequent siblings, not 25 | * only to its children.) 26 | * 27 | * set/isCaseInsensitive -- specifies whether node name tests should 28 | * be case sensitive. If you're executing xpaths against a regular 29 | * HTML DOM, you probably don't want case-sensitivity, because 30 | * browsers tend to disagree about whether elements & attributes 31 | * should be upper/lower case. If you're running xpaths in an 32 | * XSLT instance, you probably DO want case sensitivity, as per the 33 | * XSL spec. 34 | * 35 | * set/isReturnOnFirstMatch -- whether XPath evaluation should quit as soon 36 | * as a result is found. This is an optimization that might make sense if you 37 | * only care about the first result. 38 | * 39 | * set/isIgnoreNonElementNodesForNTA -- whether to ignore non-element nodes 40 | * when evaluating the "node()" any node test. While technically this is 41 | * contrary to the XPath spec, practically it can enhance performance 42 | * significantly, and makes sense if you a) use "node()" when you mean "*", 43 | * and b) use "//" when you mean "/descendant::* /". 44 | */ 45 | export class ExprContext { 46 | position: number; 47 | nodeList: XNode[]; 48 | outputPosition: number; 49 | outputNodeList: XNode[]; 50 | outputDepth: number; 51 | xsltVersion: '1.0' | '2.0' | '3.0'; 52 | 53 | variables: { [name: string]: NodeValue }; 54 | keys: { [name: string]: { [key: string]: NodeValue } }; 55 | knownNamespaces: { [alias: string]: string }; 56 | 57 | caseInsensitive: any; 58 | ignoreAttributesWithoutValue: any; 59 | returnOnFirstMatch: any; 60 | ignoreNonElementNodesForNTA: any; 61 | 62 | parent: ExprContext; 63 | root: XNode; 64 | decimalFormatSettings: XsltDecimalFormatSettings; 65 | 66 | inApplyTemplates: boolean; 67 | baseTemplateMatched: boolean; 68 | 69 | /** 70 | * Constructor -- gets the node, its position, the node set it 71 | * belongs to, and a parent context as arguments. The parent context 72 | * is used to implement scoping rules for variables: if a variable 73 | * is not found in the current context, it is looked for in the 74 | * parent context, recursively. Except for node, all arguments have 75 | * default values: default position is 0, default node set is the 76 | * set that contains only the node, and the default parent is null. 77 | * 78 | * Notice that position starts at 0 at the outside interface; 79 | * inside XPath expressions this shows up as position()=1. 80 | * @param nodeList TODO 81 | * @param outputNodeList TODO 82 | * @param opt_position TODO 83 | * @param opt_outputPosition TODO 84 | * @param opt_parent TODO 85 | * @param opt_caseInsensitive TODO 86 | * @param opt_ignoreAttributesWithoutValue TODO 87 | * @param opt_returnOnFirstMatch TODO 88 | * @param opt_ignoreNonElementNodesForNTA TODO 89 | */ 90 | constructor( 91 | nodeList: XNode[], 92 | outputNodeList: XNode[], 93 | xsltVersion: '1.0' | '2.0' | '3.0' = '1.0', 94 | opt_position?: number, 95 | opt_outputPosition?: number, 96 | opt_outputDepth?: number, 97 | opt_decimalFormatSettings?: XsltDecimalFormatSettings, 98 | opt_variables?: { [name: string]: any }, 99 | opt_knownNamespaces?: { [alias: string]: string }, 100 | opt_parent?: ExprContext, 101 | opt_caseInsensitive?: any, 102 | opt_ignoreAttributesWithoutValue?: any, 103 | opt_returnOnFirstMatch?: any, 104 | opt_ignoreNonElementNodesForNTA?: any 105 | ) { 106 | this.nodeList = nodeList; 107 | this.outputNodeList = outputNodeList; 108 | this.xsltVersion = xsltVersion; 109 | 110 | this.position = opt_position || 0; 111 | this.outputPosition = opt_outputPosition || 0; 112 | 113 | this.variables = opt_variables || {}; 114 | this.keys = opt_parent?.keys || {}; 115 | this.knownNamespaces = opt_knownNamespaces || {}; 116 | 117 | this.parent = opt_parent || null; 118 | this.caseInsensitive = opt_caseInsensitive || false; 119 | this.ignoreAttributesWithoutValue = opt_ignoreAttributesWithoutValue || false; 120 | this.returnOnFirstMatch = opt_returnOnFirstMatch || false; 121 | this.ignoreNonElementNodesForNTA = opt_ignoreNonElementNodesForNTA || false; 122 | this.inApplyTemplates = false; 123 | this.baseTemplateMatched = false; 124 | this.outputDepth = opt_outputDepth || 0; 125 | 126 | this.decimalFormatSettings = opt_decimalFormatSettings || { 127 | decimalSeparator: '.', 128 | groupingSeparator: ',', 129 | infinity: 'Infinity', 130 | minusSign: '-', 131 | naN: 'NaN', 132 | percent: '%', 133 | perMille: '‰', 134 | zeroDigit: '0', 135 | digit: '#', 136 | patternSeparator: ';' 137 | }; 138 | 139 | if (opt_parent) { 140 | this.root = opt_parent.root; 141 | } else if (this.nodeList[this.position].nodeType == DOM_DOCUMENT_NODE) { 142 | // NOTE(mesch): DOM Spec stipulates that the ownerDocument of a 143 | // document is null. Our root, however is the document that we are 144 | // processing, so the initial context is created from its document 145 | // node, which case we must handle here explicitly. 146 | this.root = this.nodeList[this.position]; 147 | } else { 148 | this.root = this.nodeList[this.position].ownerDocument; 149 | } 150 | } 151 | 152 | /** 153 | * clone() -- creates a new context with the current context as 154 | * parent. If passed as argument to clone(), the new context has a 155 | * different node, position, or node set. What is not passed is 156 | * inherited from the cloned context. 157 | * @param opt_nodeList TODO 158 | * @param opt_outputNodeList TODO 159 | * @param opt_position TODO 160 | * @param opt_outputPosition TODO 161 | * @returns TODO 162 | */ 163 | clone(opt_nodeList?: XNode[], opt_outputNodeList?: XNode[], opt_position?: number, opt_outputPosition?: number) { 164 | return new ExprContext( 165 | opt_nodeList || this.nodeList, 166 | opt_outputNodeList || this.outputNodeList, 167 | this.xsltVersion, 168 | typeof opt_position !== 'undefined' ? opt_position : this.position, 169 | typeof opt_outputPosition !== 'undefined' ? opt_outputPosition : this.outputPosition, 170 | this.outputDepth, 171 | this.decimalFormatSettings, 172 | this.variables, 173 | this.knownNamespaces, 174 | this, 175 | this.caseInsensitive, 176 | this.ignoreAttributesWithoutValue, 177 | this.returnOnFirstMatch, 178 | this.ignoreNonElementNodesForNTA 179 | ); 180 | } 181 | 182 | cloneByOutput(opt_outputNodeList?: XNode[], opt_outputPosition?: number, opt_outputDepth?: number) { 183 | return new ExprContext( 184 | this.nodeList, 185 | opt_outputNodeList || this.outputNodeList, 186 | this.xsltVersion, 187 | this.position, 188 | typeof opt_outputPosition !== 'undefined' ? opt_outputPosition : this.outputPosition, 189 | typeof opt_outputDepth !== 'undefined' ? opt_outputDepth : this.outputDepth, 190 | this.decimalFormatSettings, 191 | this.variables, 192 | this.knownNamespaces, 193 | this, 194 | this.caseInsensitive, 195 | this.ignoreAttributesWithoutValue, 196 | this.returnOnFirstMatch, 197 | this.ignoreNonElementNodesForNTA 198 | ); 199 | } 200 | 201 | setVariable(name?: string, value?: NodeValue | string) { 202 | if ( 203 | value instanceof StringValue || 204 | value instanceof BooleanValue || 205 | value instanceof NumberValue || 206 | value instanceof NodeSetValue 207 | ) { 208 | this.variables[name] = value; 209 | return; 210 | } 211 | 212 | if ('true' === value) { 213 | this.variables[name] = new BooleanValue(true); 214 | } else if ('false' === value) { 215 | this.variables[name] = new BooleanValue(false); 216 | } else if (TOK_NUMBER.re.test(String(value))) { 217 | this.variables[name] = new NumberValue(value); 218 | } else { 219 | // DGF What if it's null? 220 | this.variables[name] = new StringValue(value); 221 | } 222 | } 223 | 224 | getVariable(name: string): NodeValue { 225 | if (typeof this.variables[name] != 'undefined') { 226 | return this.variables[name]; 227 | } 228 | 229 | if (this.parent) { 230 | return this.parent.getVariable(name); 231 | } 232 | 233 | return null; 234 | } 235 | 236 | setNode(position: number) { 237 | this.position = position; 238 | } 239 | 240 | contextSize() { 241 | return this.nodeList.length; 242 | } 243 | 244 | isCaseInsensitive() { 245 | return this.caseInsensitive; 246 | } 247 | 248 | setCaseInsensitive(caseInsensitive) { 249 | return (this.caseInsensitive = caseInsensitive); 250 | } 251 | 252 | isIgnoreAttributesWithoutValue() { 253 | return this.ignoreAttributesWithoutValue; 254 | } 255 | 256 | setIgnoreAttributesWithoutValue(ignore) { 257 | return (this.ignoreAttributesWithoutValue = ignore); 258 | } 259 | 260 | isReturnOnFirstMatch() { 261 | return this.returnOnFirstMatch; 262 | } 263 | 264 | setReturnOnFirstMatch(returnOnFirstMatch) { 265 | return (this.returnOnFirstMatch = returnOnFirstMatch); 266 | } 267 | 268 | isIgnoreNonElementNodesForNTA() { 269 | return this.ignoreNonElementNodesForNTA; 270 | } 271 | 272 | setIgnoreNonElementNodesForNTA(ignoreNonElementNodesForNTA) { 273 | return (this.ignoreNonElementNodesForNTA = ignoreNonElementNodesForNTA); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/xpath/expressions/README.md: -------------------------------------------------------------------------------- 1 | # XPath expressions. 2 | 3 | They are used as nodes in the parse tree and possess an evaluate() method to compute an XPath value given an XPath context. Expressions are returned from the parser. The set of 4 | expression classes closely mirrors the set of non terminal symbols in the grammar. Every non trivial nonterminal symbol has a corresponding expression class. 5 | 6 | The common expression interface consists of the following methods: 7 | 8 | - `evaluate(context)` - evaluates the expression, returns a value. 9 | - `toString(expr)` - returns the XPath text representation of the expression. 10 | - `parseTree(expr, indent)` - returns a parse tree representation of the expression. 11 | -------------------------------------------------------------------------------- /src/xpath/expressions/binary-expr.ts: -------------------------------------------------------------------------------- 1 | import { xmlValue } from "../../dom"; 2 | import { ExprContext } from "../expr-context"; 3 | import { BooleanValue } from "../values/boolean-value"; 4 | import { NumberValue } from "../values/number-value"; 5 | import { Expression } from "./expression"; 6 | 7 | export class BinaryExpr extends Expression { 8 | expr1: any; 9 | expr2: any; 10 | op: any; 11 | 12 | constructor(expr1: any, op: any, expr2: any) { 13 | super(); 14 | this.expr1 = expr1; 15 | this.expr2 = expr2; 16 | this.op = op; 17 | } 18 | 19 | evaluate(ctx: any) { 20 | let ret; 21 | switch (this.op.value) { 22 | case 'or': 23 | ret = new BooleanValue( 24 | this.expr1.evaluate(ctx).booleanValue() || this.expr2.evaluate(ctx).booleanValue() 25 | ); 26 | break; 27 | 28 | case 'and': 29 | ret = new BooleanValue( 30 | this.expr1.evaluate(ctx).booleanValue() && this.expr2.evaluate(ctx).booleanValue() 31 | ); 32 | break; 33 | 34 | case '+': 35 | ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() + this.expr2.evaluate(ctx).numberValue()); 36 | break; 37 | 38 | case '-': 39 | ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() - this.expr2.evaluate(ctx).numberValue()); 40 | break; 41 | 42 | case '*': 43 | ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() * this.expr2.evaluate(ctx).numberValue()); 44 | break; 45 | 46 | case 'mod': 47 | ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() % this.expr2.evaluate(ctx).numberValue()); 48 | break; 49 | 50 | case 'div': 51 | ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() / this.expr2.evaluate(ctx).numberValue()); 52 | break; 53 | 54 | case '=': 55 | ret = this.compare(ctx, (x1, x2) => x1 == x2); 56 | break; 57 | 58 | case '!=': 59 | ret = this.compare(ctx, (x1, x2) => x1 != x2); 60 | break; 61 | 62 | case '<': 63 | ret = this.compare(ctx, (x1, x2) => x1 < x2); 64 | break; 65 | 66 | case '<=': 67 | ret = this.compare(ctx, (x1, x2) => x1 <= x2); 68 | break; 69 | 70 | case '>': 71 | ret = this.compare(ctx, (x1, x2) => x1 > x2); 72 | break; 73 | 74 | case '>=': 75 | ret = this.compare(ctx, (x1, x2) => x1 >= x2); 76 | break; 77 | 78 | default: 79 | throw `BinaryExpr.evaluate: ${this.op.value}`; 80 | } 81 | return ret; 82 | } 83 | 84 | compare(ctx: ExprContext, cmp: any) { 85 | const v1 = this.expr1.evaluate(ctx); 86 | const v2 = this.expr2.evaluate(ctx); 87 | 88 | let ret; 89 | if (v1.type == 'node-set' && v2.type == 'node-set') { 90 | const n1 = v1.nodeSetValue(); 91 | const n2 = v2.nodeSetValue(); 92 | ret = false; 93 | for (let i1 = 0; i1 < n1.length; ++i1) { 94 | for (let i2 = 0; i2 < n2.length; ++i2) { 95 | if (cmp(xmlValue(n1[i1]), xmlValue(n2[i2]))) { 96 | ret = true; 97 | // Break outer loop. Labels confuse the jscompiler and we 98 | // don't use them. 99 | i2 = n2.length; 100 | i1 = n1.length; 101 | } 102 | } 103 | } 104 | } else if (v1.type == 'node-set' || v2.type == 'node-set') { 105 | if (v1.type == 'number') { 106 | let s = v1.numberValue(); 107 | let n = v2.nodeSetValue(); 108 | 109 | ret = false; 110 | for (let i = 0; i < n.length; ++i) { 111 | let nn = parseInt(xmlValue(n[i])) - 0; 112 | if (cmp(s, nn)) { 113 | ret = true; 114 | break; 115 | } 116 | } 117 | } else if (v2.type == 'number') { 118 | let n = v1.nodeSetValue(); 119 | let s = v2.numberValue(); 120 | 121 | ret = false; 122 | for (let i = 0; i < n.length; ++i) { 123 | let nn = parseInt(xmlValue(n[i])) - 0; 124 | if (cmp(nn, s)) { 125 | ret = true; 126 | break; 127 | } 128 | } 129 | } else if (v1.type == 'string') { 130 | let s = v1.stringValue(); 131 | let n = v2.nodeSetValue(); 132 | 133 | ret = false; 134 | for (let i = 0; i < n.length; ++i) { 135 | let nn = xmlValue(n[i]); 136 | if (cmp(s, nn)) { 137 | ret = true; 138 | break; 139 | } 140 | } 141 | } else if (v2.type == 'string') { 142 | let n = v1.nodeSetValue(); 143 | let s = v2.stringValue(); 144 | 145 | ret = false; 146 | for (let i = 0; i < n.length; ++i) { 147 | let nn = xmlValue(n[i]); 148 | if (cmp(nn, s)) { 149 | ret = true; 150 | break; 151 | } 152 | } 153 | } else { 154 | ret = cmp(v1.booleanValue(), v2.booleanValue()); 155 | } 156 | } else if (v1.type == 'boolean' || v2.type == 'boolean') { 157 | ret = cmp(v1.booleanValue(), v2.booleanValue()); 158 | } else if (v1.type == 'number' || v2.type == 'number') { 159 | ret = cmp(v1.numberValue(), v2.numberValue()); 160 | } else { 161 | ret = cmp(v1.stringValue(), v2.stringValue()); 162 | } 163 | 164 | return new BooleanValue(ret); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/xpath/expressions/expression.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { ExprContext } from ".."; 3 | 4 | export abstract class Expression { 5 | abstract evaluate(ctx: ExprContext); 6 | } 7 | -------------------------------------------------------------------------------- /src/xpath/expressions/filter-expr.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from ".."; 2 | import { NodeSetValue } from "../values/node-set-value"; 3 | import { Expression } from "./expression"; 4 | 5 | export class FilterExpr extends Expression { 6 | expr: any; 7 | predicate: any; 8 | 9 | constructor(expr: any, predicate: any) { 10 | super(); 11 | this.expr = expr; 12 | this.predicate = predicate; 13 | } 14 | 15 | evaluate(context: ExprContext) { 16 | // the filter expression should be evaluated in its entirety with no 17 | // optimization, as we can't backtrack to it after having moved on to 18 | // evaluating the relative location path. See the testReturnOnFirstMatch 19 | // unit test. 20 | const flag = context.returnOnFirstMatch; 21 | context.setReturnOnFirstMatch(false); 22 | let nodes = this.expr.evaluate(context).nodeSetValue(); 23 | context.setReturnOnFirstMatch(flag); 24 | 25 | for (let i = 0; i < this.predicate.length; ++i) { 26 | const nodes0 = nodes; 27 | nodes = []; 28 | for (let j = 0; j < nodes0.length; ++j) { 29 | const n = nodes0[j]; 30 | if (this.predicate[i].evaluate(context.clone(nodes0, undefined, j)).booleanValue()) { 31 | nodes.push(n); 32 | } 33 | } 34 | } 35 | 36 | return new NodeSetValue(nodes); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/xpath/expressions/function-call-expr.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from '../expr-context'; 2 | import { 3 | count, 4 | generateId, 5 | id, 6 | last, 7 | localName, 8 | _name, 9 | namespaceUri, 10 | position, 11 | xmlToJson, 12 | _string, 13 | concat, 14 | startsWith, 15 | endsWith, 16 | contains, 17 | substringBefore, 18 | substringAfter, 19 | substring, 20 | stringLength, 21 | normalizeSpace, 22 | translate, 23 | matches, 24 | boolean, 25 | not, 26 | _true, 27 | _false, 28 | lang, 29 | number, 30 | sum, 31 | floor, 32 | ceiling, 33 | round, 34 | current, 35 | formatNumber, 36 | key 37 | } from '../functions'; 38 | import { extCardinal, extIf, extJoin } from '../functions/non-standard'; 39 | import { lowerCase, _replace, upperCase } from '../functions/standard-20'; 40 | import { BooleanValue } from '../values/boolean-value'; 41 | import { Expression } from './expression'; 42 | 43 | export class FunctionCallExpr extends Expression { 44 | name: any; 45 | args: any[]; 46 | 47 | xPathFunctions: { [key: string]: Function } = { 48 | boolean, 49 | ceiling, 50 | concat, 51 | contains, 52 | count, 53 | current, 54 | 'ends-with': endsWith, 55 | false: _false, 56 | 'format-number': formatNumber, 57 | floor, 58 | 'generate-id': generateId, 59 | id, 60 | key, 61 | lang, 62 | last, 63 | 'local-name': localName, 64 | 'lower-case': lowerCase, 65 | 'replace': _replace, 66 | matches, 67 | name: _name, 68 | 'namespace-uri': namespaceUri, 69 | 'normalize-space': normalizeSpace, 70 | not, 71 | number, 72 | position, 73 | round, 74 | 'starts-with': startsWith, 75 | string: _string, 76 | 'xml-to-json': xmlToJson, 77 | substring, 78 | 'substring-before': substringBefore, 79 | 'substring-after': substringAfter, 80 | sum, 81 | 'string-length': stringLength, 82 | translate, 83 | true: _true, 84 | 'upper-case': upperCase, 85 | 86 | // TODO(mesch): The following functions are custom. There is a 87 | // standard that defines how to add functions, which should be 88 | // applied here. 89 | 90 | 'ext-join': extJoin, 91 | 92 | // ext-if() evaluates and returns its second argument, if the 93 | // boolean value of its first argument is true, otherwise it 94 | // evaluates and returns its third argument. 95 | 96 | 'ext-if': extIf, 97 | 98 | // ext-cardinal() evaluates its single argument as a number, and 99 | // returns the current node that many times. It can be used in the 100 | // select attribute to iterate over an integer range. 101 | 102 | 'ext-cardinal': extCardinal 103 | }; 104 | 105 | constructor(name: any) { 106 | super(); 107 | this.name = name; 108 | this.args = []; 109 | } 110 | 111 | appendArg(arg: any) { 112 | this.args.push(arg); 113 | } 114 | 115 | evaluate(context: ExprContext) { 116 | const functionName = `${this.name.value}`; 117 | const resolvedFunction = this.xPathFunctions[functionName]; 118 | if (resolvedFunction) { 119 | return resolvedFunction.call(this, context); 120 | } 121 | 122 | return new BooleanValue(false); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/xpath/expressions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './binary-expr'; 2 | export * from './filter-expr'; 3 | export * from './function-call-expr'; 4 | export * from './literal-expr'; 5 | export * from './location-expr'; 6 | export * from './number-expr'; 7 | export * from './path-expr'; 8 | export * from './predicate-expr'; 9 | export * from './step-expr'; 10 | export * from './token-expr'; 11 | export * from './unary-minus-expr'; 12 | export * from './union-expr'; 13 | export * from './variable-expr'; 14 | -------------------------------------------------------------------------------- /src/xpath/expressions/literal-expr.ts: -------------------------------------------------------------------------------- 1 | import { StringValue } from "../values/string-value"; 2 | import { Expression } from "./expression"; 3 | 4 | export class LiteralExpr extends Expression { 5 | value: any; 6 | 7 | constructor(value: any) { 8 | super(); 9 | this.value = value; 10 | } 11 | 12 | evaluate() { 13 | return new StringValue(this.value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/xpath/expressions/location-expr.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from "../expr-context"; 2 | import { NodeSetValue } from "../values/node-set-value"; 3 | import { NodeTestAny } from "../node-tests/node-test-any"; 4 | import { xPathAxis } from "../tokens"; 5 | import { Expression } from "./expression"; 6 | import { XPath } from "../xpath"; 7 | import { XNode } from "../../dom"; 8 | import { StepExpr } from "./step-expr"; 9 | 10 | export class LocationExpr extends Expression { 11 | absolute: boolean; 12 | steps: StepExpr[]; 13 | xPath: XPath; 14 | 15 | constructor(xPath: XPath) { 16 | super(); 17 | this.absolute = false; 18 | this.steps = []; 19 | this.xPath = xPath; 20 | } 21 | 22 | appendStep(s: StepExpr) { 23 | const combinedStep = this._combineSteps(this.steps[this.steps.length - 1], s); 24 | if (combinedStep) { 25 | this.steps[this.steps.length - 1] = combinedStep; 26 | } else { 27 | this.steps.push(s); 28 | } 29 | } 30 | 31 | prependStep(s: StepExpr) { 32 | const combinedStep = this._combineSteps(s, this.steps[0]); 33 | if (combinedStep) { 34 | this.steps[0] = combinedStep; 35 | } else { 36 | this.steps.unshift(s); 37 | } 38 | } 39 | 40 | // DGF try to combine two steps into one step (perf enhancement) 41 | private _combineSteps(prevStep: any, nextStep: any) { 42 | if (!prevStep) return null; 43 | if (!nextStep) return null; 44 | const hasPredicates = prevStep.predicates && prevStep.predicates.length > 0; 45 | if (prevStep.nodeTest instanceof NodeTestAny && !hasPredicates) { 46 | // maybe suitable to be combined 47 | if (prevStep.axis == xPathAxis.DESCENDANT_OR_SELF) { 48 | if (nextStep.axis == xPathAxis.CHILD) { 49 | // HBC - commenting out, because this is not a valid reduction 50 | //nextStep.axis = xpathAxis.DESCENDANT; 51 | //return nextStep; 52 | } else if (nextStep.axis == xPathAxis.SELF) { 53 | nextStep.axis = xPathAxis.DESCENDANT_OR_SELF; 54 | return nextStep; 55 | } 56 | } else if (prevStep.axis == xPathAxis.DESCENDANT) { 57 | if (nextStep.axis == xPathAxis.SELF) { 58 | nextStep.axis = xPathAxis.DESCENDANT; 59 | return nextStep; 60 | } 61 | } 62 | } 63 | return null; 64 | } 65 | 66 | evaluate(context: ExprContext) { 67 | let start: XNode; 68 | if (this.absolute) { 69 | start = context.root; 70 | } else { 71 | start = context.nodeList[context.position]; 72 | // TODO: `` with relative path, starting on root node, 73 | // conflicts with ``, for some reason considered as relative. 74 | /* if (start.nodeName === '#document' && this.steps[0].axis === 'self-and-siblings') { 75 | start = start.childNodes[0]; 76 | } */ 77 | } 78 | 79 | const nodes = []; 80 | this.xPath.xPathStep(nodes, this.steps, 0, start, context); 81 | return new NodeSetValue(nodes); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/xpath/expressions/number-expr.ts: -------------------------------------------------------------------------------- 1 | import { NumberValue } from "../values/number-value"; 2 | import { Expression } from "./expression"; 3 | 4 | export class NumberExpr extends Expression { 5 | value: any; 6 | 7 | constructor(value: any) { 8 | super(); 9 | this.value = value; 10 | } 11 | 12 | evaluate() { 13 | return new NumberValue(this.value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/xpath/expressions/path-expr.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from ".."; 2 | import { NodeSetValue } from "../values/node-set-value"; 3 | import { Expression } from "./expression"; 4 | 5 | export class PathExpr extends Expression { 6 | filter: any; 7 | rel: any; 8 | 9 | constructor(filter: any, rel: any) { 10 | super(); 11 | this.filter = filter; 12 | this.rel = rel; 13 | } 14 | 15 | evaluate(ctx: ExprContext) { 16 | const nodes = this.filter.evaluate(ctx).nodeSetValue(); 17 | let nodes1 = []; 18 | if (ctx.returnOnFirstMatch) { 19 | for (let i = 0; i < nodes.length; ++i) { 20 | nodes1 = this.rel.evaluate(ctx.clone(nodes, undefined, i)).nodeSetValue(); 21 | if (nodes1.length > 0) { 22 | break; 23 | } 24 | } 25 | return new NodeSetValue(nodes1); 26 | } 27 | 28 | for (let i = 0; i < nodes.length; ++i) { 29 | const nodes0 = this.rel.evaluate(ctx.clone(nodes, undefined, i)).nodeSetValue(); 30 | for (let ii = 0; ii < nodes0.length; ++ii) { 31 | nodes1.push(nodes0[ii]); 32 | } 33 | } 34 | return new NodeSetValue(nodes1); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/xpath/expressions/predicate-expr.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from ".."; 2 | import { BooleanValue } from "../values/boolean-value"; 3 | import { Expression } from "./expression"; 4 | 5 | export class PredicateExpr extends Expression { 6 | expression: Expression; 7 | 8 | constructor(expression: Expression) { 9 | super(); 10 | this.expression = expression; 11 | } 12 | 13 | evaluate(context: ExprContext) { 14 | const value = this.expression.evaluate(context); 15 | if (value.type == 'number') { 16 | // NOTE(mesch): Internally, position is represented starting with 17 | // 0, however in XPath position starts with 1. See functions 18 | // position() and last(). 19 | return new BooleanValue(context.position == value.numberValue() - 1); 20 | } 21 | 22 | return new BooleanValue(value.booleanValue()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/xpath/expressions/step-expr.ts: -------------------------------------------------------------------------------- 1 | import { DOM_ATTRIBUTE_NODE } from '../../constants'; 2 | import { XNode } from '../../dom'; 3 | import { ExprContext } from '../expr-context'; 4 | import { NodeSetValue } from '../values/node-set-value'; 5 | import { NodeTestAny } from '../node-tests/node-test-any'; 6 | import { xPathAxis } from '../tokens'; 7 | import { Expression } from './expression'; 8 | import { XPath } from '../xpath'; 9 | import { BinaryExpr } from './binary-expr'; 10 | import { FunctionCallExpr } from './function-call-expr'; 11 | import { NumberExpr } from './number-expr'; 12 | import { UnaryMinusExpr } from './unary-minus-expr'; 13 | import { copyArray, copyArrayIgnoringAttributesWithoutValue } from '../common-function'; 14 | import { PredicateExpr } from './predicate-expr'; 15 | 16 | export class StepExpr extends Expression { 17 | axis: any; 18 | nodeTest: any; 19 | predicate: any; 20 | hasPositionalPredicate: any; 21 | xPath: XPath; 22 | 23 | constructor(axis: any, nodeTest: any, xPath: XPath, opt_predicate?: any) { 24 | super(); 25 | this.axis = axis; 26 | this.nodeTest = nodeTest; 27 | this.predicate = opt_predicate || []; 28 | this.hasPositionalPredicate = false; 29 | this.xPath = xPath; 30 | 31 | for (let i = 0; i < this.predicate.length; ++i) { 32 | if (this.predicateExprHasPositionalSelector(this.predicate[i].expr)) { 33 | this.hasPositionalPredicate = true; 34 | break; 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * Determines whether a predicate expression contains a "positional selector". 41 | * A positional selector filters nodes from the nodeList input based on their 42 | * position within that list. When such selectors are encountered, the 43 | * evaluation of the predicate cannot be depth-first, because the positional 44 | * selector may be based on the result of evaluating predicates that precede 45 | * it. 46 | */ 47 | private predicateExprHasPositionalSelector(expr: Expression, isRecursiveCall?: any) { 48 | if (!expr) { 49 | return false; 50 | } 51 | if (!isRecursiveCall && this.exprReturnsNumberValue(expr)) { 52 | // this is a "proximity position"-based predicate 53 | return true; 54 | } 55 | if (expr instanceof FunctionCallExpr) { 56 | const value = (expr as any).name.value; 57 | return value == 'last' || value == 'position'; 58 | } 59 | if (expr instanceof BinaryExpr) { 60 | return ( 61 | this.predicateExprHasPositionalSelector(expr.expr1, true) || 62 | this.predicateExprHasPositionalSelector(expr.expr2, true) 63 | ); 64 | } 65 | return false; 66 | } 67 | 68 | private exprReturnsNumberValue(expr) { 69 | if (expr instanceof FunctionCallExpr) { 70 | let isMember = { 71 | last: true, 72 | position: true, 73 | count: true, 74 | 'string-length': true, 75 | number: true, 76 | sum: true, 77 | floor: true, 78 | ceiling: true, 79 | round: true 80 | }; 81 | return isMember[(expr as any).name.value]; 82 | } 83 | 84 | if (expr instanceof UnaryMinusExpr) { 85 | return true; 86 | } 87 | 88 | if (expr instanceof BinaryExpr) { 89 | let isMember = { 90 | '+': true, 91 | '-': true, 92 | '*': true, 93 | mod: true, 94 | div: true 95 | }; 96 | return isMember[expr.op.value]; 97 | } 98 | 99 | if (expr instanceof NumberExpr) { 100 | return true; 101 | } 102 | 103 | return false; 104 | } 105 | 106 | appendPredicate(predicateExpression: PredicateExpr) { 107 | this.predicate.push(predicateExpression); 108 | if (!this.hasPositionalPredicate) { 109 | this.hasPositionalPredicate = this.predicateExprHasPositionalSelector(predicateExpression.expression); 110 | } 111 | } 112 | 113 | evaluate(context: ExprContext) { 114 | const node = context.nodeList[context.position]; 115 | let nodeList = []; 116 | let skipNodeTest = false; 117 | 118 | if (this.nodeTest instanceof NodeTestAny) { 119 | skipNodeTest = true; 120 | } 121 | 122 | switch (this.axis) { 123 | case xPathAxis.ANCESTOR_OR_SELF: 124 | nodeList.push(node); 125 | for (let n = node.parentNode; n; n = n.parentNode) { 126 | if (n.nodeType !== DOM_ATTRIBUTE_NODE) { 127 | nodeList.push(n); 128 | } 129 | } 130 | break; 131 | 132 | case xPathAxis.ANCESTOR: 133 | for (let n = node.parentNode; n; n = n.parentNode) { 134 | if (n.nodeType !== DOM_ATTRIBUTE_NODE) { 135 | nodeList.push(n); 136 | } 137 | } 138 | break; 139 | 140 | case xPathAxis.ATTRIBUTE: 141 | const attributes = node.childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); 142 | if (this.nodeTest.name !== undefined) { 143 | // single-attribute step 144 | if (attributes) { 145 | if (attributes instanceof Array) { 146 | // probably evaluating on document created by xmlParse() 147 | copyArray(nodeList, attributes); 148 | } else { 149 | // TODO: I think this `else` does't make any sense now. 150 | // Before unifying attributes with child nodes, `node.attributes` was always an array. 151 | if (this.nodeTest.name == 'style') { 152 | const value = node.getAttributeValue('style'); 153 | if (value && typeof value != 'string') { 154 | // this is the case where indexing into the attributes array 155 | // doesn't give us the attribute node in IE - we create our own 156 | // node instead 157 | nodeList.push(XNode.create(DOM_ATTRIBUTE_NODE, 'style', value.cssText, document)); 158 | } else { 159 | nodeList.push(attributes[this.nodeTest.name]); 160 | } 161 | } else { 162 | nodeList.push(attributes[this.nodeTest.name]); 163 | } 164 | } 165 | } 166 | } else { 167 | // all-attributes step 168 | if (context.ignoreAttributesWithoutValue) { 169 | copyArrayIgnoringAttributesWithoutValue(nodeList, attributes); 170 | } else { 171 | copyArray(nodeList, attributes); 172 | } 173 | } 174 | 175 | break; 176 | 177 | case xPathAxis.CHILD: 178 | copyArray(nodeList, node.childNodes.filter(n => n.nodeType !== DOM_ATTRIBUTE_NODE)); 179 | break; 180 | 181 | case xPathAxis.DESCENDANT_OR_SELF: { 182 | if (this.nodeTest.evaluate(context).booleanValue()) { 183 | nodeList.push(node); 184 | } 185 | 186 | let tagName = this.xPath.xPathExtractTagNameFromNodeTest( 187 | this.nodeTest, 188 | context.ignoreNonElementNodesForNTA 189 | ); 190 | 191 | this.xPath.xPathCollectDescendants(nodeList, node, tagName); 192 | if (tagName) skipNodeTest = true; 193 | 194 | break; 195 | } 196 | 197 | case xPathAxis.DESCENDANT: { 198 | let tagName = this.xPath.xPathExtractTagNameFromNodeTest( 199 | this.nodeTest, 200 | context.ignoreNonElementNodesForNTA 201 | ); 202 | this.xPath.xPathCollectDescendants(nodeList, node, tagName); 203 | if (tagName) skipNodeTest = true; 204 | 205 | break; 206 | } 207 | 208 | case xPathAxis.FOLLOWING: 209 | for (let n = node; n; n = n.parentNode) { 210 | for (let nn = n.nextSibling; nn; nn = nn.nextSibling) { 211 | if (nn.nodeType !== DOM_ATTRIBUTE_NODE) { 212 | nodeList.push(nn); 213 | } 214 | 215 | this.xPath.xPathCollectDescendants(nodeList, nn); 216 | } 217 | } 218 | 219 | break; 220 | 221 | case xPathAxis.FOLLOWING_SIBLING: 222 | if (node.nodeType === DOM_ATTRIBUTE_NODE) { 223 | break; 224 | } 225 | 226 | for (let n = node.nextSibling; n; n = n.nextSibling) { 227 | if (n.nodeType !== DOM_ATTRIBUTE_NODE) { 228 | nodeList.push(n); 229 | } 230 | } 231 | 232 | break; 233 | 234 | case xPathAxis.NAMESPACE: 235 | throw new Error('not implemented: axis namespace'); 236 | 237 | case xPathAxis.PARENT: 238 | if (node.parentNode) { 239 | nodeList.push(node.parentNode); 240 | } 241 | 242 | break; 243 | 244 | case xPathAxis.PRECEDING: 245 | for (let n = node; n; n = n.parentNode) { 246 | for (let nn = n.previousSibling; nn; nn = nn.previousSibling) { 247 | if (nn.nodeType !== DOM_ATTRIBUTE_NODE) { 248 | nodeList.push(nn); 249 | } 250 | 251 | this.xPath.xPathCollectDescendantsReverse(nodeList, nn); 252 | } 253 | } 254 | 255 | break; 256 | 257 | case xPathAxis.PRECEDING_SIBLING: 258 | for (let n = node.previousSibling; n; n = n.previousSibling) { 259 | if (n.nodeType !== DOM_ATTRIBUTE_NODE) { 260 | nodeList.push(n); 261 | } 262 | } 263 | 264 | break; 265 | 266 | case xPathAxis.SELF: 267 | nodeList.push(node); 268 | break; 269 | 270 | case xPathAxis.SELF_AND_SIBLINGS: 271 | for (const node of context.nodeList) { 272 | if (node.nodeType !== DOM_ATTRIBUTE_NODE) { 273 | nodeList.push(node); 274 | } 275 | } 276 | 277 | break; 278 | 279 | default: 280 | throw new Error(`ERROR -- NO SUCH AXIS: ${this.axis}`); 281 | } 282 | 283 | if (!skipNodeTest) { 284 | // process node test 285 | let nodeList0 = nodeList; 286 | nodeList = []; 287 | for (let i = 0; i < nodeList0.length; ++i) { 288 | if (this.nodeTest.evaluate(context.clone(nodeList0, undefined, i)).booleanValue()) { 289 | nodeList.push(nodeList0[i]); 290 | } 291 | } 292 | } 293 | 294 | // process predicates 295 | if (!context.returnOnFirstMatch) { 296 | for (let i = 0; i < this.predicate.length; ++i) { 297 | let nodeList0 = nodeList; 298 | nodeList = []; 299 | for (let ii = 0; ii < nodeList0.length; ++ii) { 300 | let n = nodeList0[ii]; 301 | if (this.predicate[i].evaluate(context.clone(nodeList0, undefined, ii)).booleanValue()) { 302 | nodeList.push(n); 303 | } 304 | } 305 | } 306 | } 307 | 308 | return new NodeSetValue(nodeList); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/xpath/expressions/token-expr.ts: -------------------------------------------------------------------------------- 1 | import { StringValue } from "../values/string-value"; 2 | import { Expression } from "./expression"; 3 | 4 | export class TokenExpr extends Expression { 5 | value: any; 6 | 7 | constructor(m: any) { 8 | super(); 9 | this.value = m; 10 | } 11 | 12 | evaluate() { 13 | return new StringValue(this.value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/xpath/expressions/unary-minus-expr.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from ".."; 2 | import { NumberValue } from "../values/number-value"; 3 | import { Expression } from "./expression"; 4 | 5 | export class UnaryMinusExpr extends Expression { 6 | expr: any; 7 | 8 | constructor(expr: any) { 9 | super(); 10 | this.expr = expr; 11 | } 12 | 13 | evaluate(ctx: ExprContext) { 14 | return new NumberValue(-this.expr.evaluate(ctx).numberValue()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/xpath/expressions/union-expr.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from ".."; 2 | import { NodeSetValue } from "../values/node-set-value"; 3 | import { Expression } from "./expression"; 4 | 5 | export class UnionExpr extends Expression { 6 | expr1: Expression; 7 | expr2: Expression; 8 | 9 | constructor(expr1: Expression, expr2: Expression) { 10 | super(); 11 | this.expr1 = expr1; 12 | this.expr2 = expr2; 13 | } 14 | 15 | evaluate(context: ExprContext) { 16 | const nodes1 = this.expr1.evaluate(context).nodeSetValue(); 17 | const nodes2 = this.expr2.evaluate(context).nodeSetValue(); 18 | const I1 = nodes1.length; 19 | 20 | for (const n of nodes2) { 21 | let inBoth = false; 22 | for (let i1 = 0; i1 < I1; ++i1) { 23 | if (nodes1[i1] == n) { 24 | inBoth = true; 25 | i1 = I1; // break inner loop 26 | } 27 | } 28 | if (!inBoth) { 29 | nodes1.push(n); 30 | } 31 | } 32 | 33 | return new NodeSetValue(nodes1); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/xpath/expressions/variable-expr.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from ".."; 2 | import { Expression } from "./expression"; 3 | 4 | export class VariableExpr extends Expression { 5 | name: string; 6 | 7 | constructor(name: string) { 8 | super(); 9 | this.name = name; 10 | } 11 | 12 | evaluate(context: ExprContext) { 13 | return context.getVariable(this.name); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/xpath/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './non-standard'; 2 | export * from './standard'; 3 | export * from './standard-20'; 4 | export * from './xslt-specific'; 5 | -------------------------------------------------------------------------------- /src/xpath/functions/internal-functions.ts: -------------------------------------------------------------------------------- 1 | // Throws an exception if false. 2 | export function assert(b: any) { 3 | if (!b) { 4 | throw new Error('Assertion failed'); 5 | } 6 | } 7 | 8 | /** 9 | * Escape the special regular expression characters when the regular expression 10 | * is specified as a string. 11 | * 12 | * Based on: http://simonwillison.net/2006/Jan/20/escape/ 13 | */ 14 | const regExpSpecials = ['/', '.', '*', '+', '?', '|', '^', '$', '(', ')', '[', ']', '{', '}', '\\']; 15 | 16 | const sRE = new RegExp(`(\\${regExpSpecials.join('|\\')})`, 'g'); 17 | 18 | export function regExpEscape(text: string) { 19 | return text.replace(sRE, '\\$1'); 20 | } 21 | -------------------------------------------------------------------------------- /src/xpath/functions/non-standard.ts: -------------------------------------------------------------------------------- 1 | import { xmlValue } from "../../dom"; 2 | import { ExprContext } from "../expr-context"; 3 | import { NodeSetValue, StringValue } from "../values"; 4 | import { assert } from "./internal-functions"; 5 | 6 | export function extCardinal(context: ExprContext) { 7 | assert(this.args.length >= 1); 8 | const c = this.args[0].evaluate(context).numberValue(); 9 | const ret = []; 10 | for (let i = 0; i < c; ++i) { 11 | ret.push(context.nodeList[context.position]); 12 | } 13 | return new NodeSetValue(ret); 14 | } 15 | 16 | /** 17 | * evaluates and returns its second argument, if the 18 | * boolean value of its first argument is true, otherwise it 19 | * evaluates and returns its third argument. 20 | * @param context The Expression Context 21 | * @returns A `BooleanValue`. 22 | */ 23 | export function extIf(context: ExprContext) { 24 | assert(this.args.length === 3); 25 | if (this.args[0].evaluate(context).booleanValue()) { 26 | return this.args[1].evaluate(context); 27 | } 28 | 29 | return this.args[2].evaluate(context); 30 | } 31 | 32 | export function extJoin(context: ExprContext) { 33 | assert(this.args.length === 2); 34 | const nodes = this.args[0].evaluate(context).nodeSetValue(); 35 | const delim = this.args[1].evaluate(context).stringValue(); 36 | let ret = ''; 37 | for (let i = 0; i < nodes.length; ++i) { 38 | if (ret) { 39 | ret += delim; 40 | } 41 | ret += xmlValue(nodes[i]); 42 | } 43 | return new StringValue(ret); 44 | } 45 | -------------------------------------------------------------------------------- /src/xpath/functions/standard-20.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from "../expr-context"; 2 | import { StringValue } from "../values"; 3 | import { assert } from "./internal-functions"; 4 | 5 | export function upperCase(context: ExprContext) { 6 | assert(['2.0', '3.0'].includes(context.xsltVersion)); 7 | const str: string = this.args[0].evaluate(context).stringValue(); 8 | return new StringValue(str.toUpperCase()); 9 | } 10 | 11 | export function lowerCase(context: ExprContext) { 12 | assert(['2.0', '3.0'].includes(context.xsltVersion)); 13 | const str: string = this.args[0].evaluate(context).stringValue(); 14 | return new StringValue(str.toLowerCase()); 15 | } 16 | 17 | export function _replace(context: ExprContext) { 18 | assert(['2.0', '3.0'].includes(context.xsltVersion)); 19 | const str: string = this.args[0].evaluate(context).stringValue(); 20 | const s1 = this.args[1].evaluate(context).stringValue(); 21 | const s2 = this.args[2].evaluate(context).stringValue(); 22 | 23 | const searchPattern = new RegExp(s1, 'g'); 24 | return new StringValue(str.replace(searchPattern, s2)); 25 | } 26 | -------------------------------------------------------------------------------- /src/xpath/functions/xslt-specific.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from "../expr-context"; 2 | import { Expression } from "../expressions/expression"; 3 | import { NodeValue, StringValue } from "../values"; 4 | import { assert } from "./internal-functions"; 5 | 6 | export function key(context: ExprContext): NodeValue { 7 | assert(this.args.length === 2); 8 | const keyNameStringValue: StringValue = (this.args[0] as Expression).evaluate(context); 9 | const keyValueStringValue: StringValue = (this.args[1] as Expression).evaluate(context); 10 | const keyName = keyNameStringValue.stringValue(); 11 | const keyValue = keyValueStringValue.stringValue(); 12 | const nodeSet = context.keys[keyName][keyValue]; 13 | return nodeSet; 14 | } 15 | -------------------------------------------------------------------------------- /src/xpath/grammar-rule-candidate.ts: -------------------------------------------------------------------------------- 1 | import { XPathTokenRule } from "./xpath-token-rule"; 2 | 3 | export type GrammarRuleCandidate = { 4 | tag: XPathTokenRule, 5 | rule?: any, 6 | match: any, 7 | prec?: number, 8 | expr?: any 9 | }; 10 | -------------------------------------------------------------------------------- /src/xpath/index.ts: -------------------------------------------------------------------------------- 1 | export * from './expr-context'; 2 | export * from './xpath'; 3 | export * from './xpath-token-rule'; 4 | -------------------------------------------------------------------------------- /src/xpath/match-resolver.ts: -------------------------------------------------------------------------------- 1 | import { XNode } from "../dom"; 2 | import { ExprContext } from "./expr-context"; 3 | import { LocationExpr, UnionExpr } from "./expressions"; 4 | import { Expression } from "./expressions/expression"; 5 | 6 | /** 7 | * Class that resolves XPath expressions, returning nodes. 8 | */ 9 | export class MatchResolver { 10 | 11 | /** 12 | * This class entry point. 13 | * @param expression The expression to be resolved. 14 | * @param context The Expression Context 15 | * @returns An array of nodes. 16 | */ 17 | expressionMatch(expression: Expression, context: ExprContext): XNode[] { 18 | if (expression instanceof LocationExpr) { 19 | return this.locationExpressionMatch(expression, context); 20 | } 21 | 22 | if (expression instanceof UnionExpr) { 23 | return this.unionExpressionMatch(expression, context); 24 | } 25 | 26 | // TODO: Other expressions 27 | return []; 28 | } 29 | 30 | /** 31 | * Resolves a `LocationExpr`. 32 | * @param expression The Location Expression. 33 | * @param context The Expression Context. 34 | * @returns Either the results of a relative resolution, or the results of an 35 | * absolute resolution. 36 | */ 37 | private locationExpressionMatch(expression: LocationExpr, context: ExprContext) { 38 | if (expression === undefined || expression.steps === undefined || expression.steps.length <= 0) { 39 | throw new Error('Error resolving XSLT match: Location Expression should have steps.'); 40 | } 41 | 42 | if (expression.absolute) { 43 | // If expression is absolute and the axis of first step is self, 44 | // the match starts by the #document node (for instance, ``). 45 | // Otherwise (axis === 'child'), the match starts on the first 46 | // child of #document node. 47 | const firstStep = expression.steps[0]; 48 | if (firstStep.axis === 'self') { 49 | return this.absoluteXsltMatchByDocumentNode(expression, context); 50 | } 51 | 52 | return this.absoluteXsltMatch(expression, context); 53 | } 54 | 55 | return this.relativeXsltMatch(expression, context); 56 | } 57 | 58 | /** 59 | * Resolves a `UnionExpr`. 60 | * @param expression The Union Expression. 61 | * @param context The Expression Context. 62 | * @returns The concatenated result of evaluating the both sides of the expression. 63 | */ 64 | private unionExpressionMatch(expression: UnionExpr, context: ExprContext) { 65 | let expr1Nodes = this.expressionMatch(expression.expr1, context); 66 | return expr1Nodes.concat(this.expressionMatch(expression.expr2, context)); 67 | } 68 | 69 | /** 70 | * Finds all the nodes through absolute XPath search, starting on 71 | * the #document parent node. 72 | * @param expression The Expression. 73 | * @param context The Expression Context. 74 | * @returns The list of found nodes. 75 | */ 76 | private absoluteXsltMatchByDocumentNode(expression: LocationExpr, context: ExprContext): XNode[] { 77 | const clonedContext = context.clone([context.root], undefined, 0, undefined); 78 | const matchedNodes = expression.evaluate(clonedContext).nodeSetValue(); 79 | const finalList = []; 80 | 81 | for (let element of matchedNodes) { 82 | if (element.id === context.nodeList[context.position].id) { 83 | finalList.push(element); 84 | continue; 85 | } 86 | } 87 | 88 | return finalList; 89 | } 90 | 91 | /** 92 | * Finds all the nodes through absolute xPath search, starting with the 93 | * first child of the #document node. 94 | * @param expression The Expression. 95 | * @param context The Expression Context. 96 | * @returns The list of found nodes. 97 | */ 98 | private absoluteXsltMatch(expression: LocationExpr, context: ExprContext): XNode[] { 99 | const firstChildOfRoot = context.root.childNodes.find(c => c.nodeName !== '#dtd-section'); 100 | const clonedContext = context.clone([firstChildOfRoot], undefined, 0, undefined); 101 | const matchedNodes = expression.evaluate(clonedContext).nodeSetValue(); 102 | const finalList = []; 103 | 104 | // If the context is pointing to #document node, it's child node is 105 | // considered. 106 | let nodeList: XNode[]; 107 | if (context.nodeList.length === 1 && context.nodeList[0].nodeName === '#document') { 108 | nodeList = [context.nodeList[0].childNodes.find(c => c.nodeName !== '#dtd-section')]; 109 | } else { 110 | nodeList = context.nodeList; 111 | } 112 | 113 | for (let element of matchedNodes) { 114 | if (element.id === nodeList[context.position].id) { 115 | finalList.push(element); 116 | continue; 117 | } 118 | } 119 | 120 | return finalList; 121 | } 122 | 123 | /** 124 | * Tries to find relative nodes from the actual context position. 125 | * If found nodes are already in the context, or if they are children of 126 | * nodes in the context, they are returned. 127 | * @param expression The expression used. 128 | * @param context The Expression Context. 129 | * @returns The list of found nodes. 130 | */ 131 | private relativeXsltMatch(expression: LocationExpr, context: ExprContext): XNode[] { 132 | // For some reason, XPath understands a default as 'child axis'. 133 | // There's no "self + siblings" axis, so what is expected at this point 134 | // is to have in the expression context the parent that should 135 | // have the nodes we are interested in. 136 | 137 | const clonedContext = context.clone(); 138 | let nodes = expression.evaluate(clonedContext).nodeSetValue(); 139 | if (nodes.length === 1 && nodes[0].nodeName === '#document') { 140 | // As we don't work with the #document node directly, this part 141 | // returns its first sibling. 142 | // By the way, it should be *always* one sibling here. 143 | return [nodes[0].childNodes[0]]; 144 | } 145 | 146 | return nodes; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/xpath/node-tests/index.ts: -------------------------------------------------------------------------------- 1 | export { NodeTest } from './node-test'; 2 | export { NodeTestAny } from './node-test-any'; 3 | export { NodeTestComment } from './node-test-comment'; 4 | export { NodeTestElementOrAttribute } from './node-test-element-or-attribute'; 5 | export { NodeTestName } from './node-test-name'; 6 | export { NodeTestNC } from './node-test-nc'; 7 | export { NodeTestPI } from './node-test-pi'; 8 | export { NodeTestText } from './node-test-text'; 9 | -------------------------------------------------------------------------------- /src/xpath/node-tests/node-test-any.ts: -------------------------------------------------------------------------------- 1 | import { BooleanValue } from "../values/boolean-value"; 2 | import { NodeTest } from "./node-test"; 3 | 4 | export class NodeTestAny implements NodeTest { 5 | value: any; 6 | 7 | constructor() { 8 | this.value = new BooleanValue(true); 9 | } 10 | 11 | evaluate() { 12 | return this.value; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/xpath/node-tests/node-test-comment.ts: -------------------------------------------------------------------------------- 1 | import { DOM_COMMENT_NODE } from "../../constants"; 2 | import { ExprContext } from "../expr-context"; 3 | import { NodeTest } from "./node-test"; 4 | import { BooleanValue } from "../values/boolean-value"; 5 | 6 | export class NodeTestComment implements NodeTest { 7 | evaluate(ctx: ExprContext) { 8 | return new BooleanValue(ctx.nodeList[ctx.position].nodeType == DOM_COMMENT_NODE); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/xpath/node-tests/node-test-element-or-attribute.ts: -------------------------------------------------------------------------------- 1 | import { DOM_ATTRIBUTE_NODE, DOM_ELEMENT_NODE } from "../../constants"; 2 | import { ExprContext } from "../expr-context"; 3 | import { BooleanValue } from "../values/boolean-value"; 4 | import { NodeTest } from "./node-test"; 5 | 6 | export class NodeTestElementOrAttribute implements NodeTest { 7 | evaluate(context: ExprContext) { 8 | const node = context.nodeList[context.position]; 9 | return new BooleanValue(node.nodeType == DOM_ELEMENT_NODE || node.nodeType == DOM_ATTRIBUTE_NODE); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/xpath/node-tests/node-test-name.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from "../expr-context"; 2 | import { NodeValue } from "../values"; 3 | import { BooleanValue } from "../values/boolean-value"; 4 | import { NodeTest } from "./node-test"; 5 | 6 | export class NodeTestName implements NodeTest { 7 | name: string; 8 | namespacePrefix: string; 9 | re: RegExp; 10 | 11 | constructor(name: string) { 12 | this.name = name; 13 | if (name.indexOf(':') > 0) { 14 | const nameAndNamespacePrefix = name.split(':'); 15 | this.namespacePrefix = nameAndNamespacePrefix[0]; 16 | this.name = nameAndNamespacePrefix[1]; 17 | } 18 | 19 | this.re = new RegExp(`^${name}$`, 'i'); 20 | } 21 | 22 | evaluate(context: ExprContext): NodeValue { 23 | const node = context.nodeList[context.position]; 24 | if (this.namespacePrefix !== undefined) { 25 | const namespaceValue = context.knownNamespaces[this.namespacePrefix]; 26 | if (namespaceValue !== node.namespaceUri) { 27 | return new BooleanValue(false); 28 | } 29 | 30 | if (context.caseInsensitive) { 31 | if (node.localName.length !== this.name.length) return new BooleanValue(false); 32 | return new BooleanValue(this.re.test(node.localName)); 33 | } 34 | 35 | return new BooleanValue(node.localName === this.name); 36 | } 37 | 38 | if (context.caseInsensitive) { 39 | if (node.nodeName.length !== this.name.length) return new BooleanValue(false); 40 | return new BooleanValue(this.re.test(node.nodeName)); 41 | } 42 | 43 | return new BooleanValue(node.nodeName === this.name); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/xpath/node-tests/node-test-nc.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from "../expr-context"; 2 | import { NodeTest } from "./node-test"; 3 | import { BooleanValue } from "../values/boolean-value"; 4 | 5 | export class NodeTestNC implements NodeTest { 6 | regex: RegExp; 7 | 8 | nsprefix: any; 9 | 10 | constructor(nsprefix: string) { 11 | this.regex = new RegExp(`^${nsprefix}:`); 12 | this.nsprefix = nsprefix; 13 | } 14 | 15 | evaluate(ctx: ExprContext) { 16 | const n = ctx.nodeList[ctx.position]; 17 | return new BooleanValue(n.nodeName.match(this.regex)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/xpath/node-tests/node-test-pi.ts: -------------------------------------------------------------------------------- 1 | import { DOM_PROCESSING_INSTRUCTION_NODE } from "../../constants"; 2 | import { ExprContext } from "../expr-context"; 3 | import { BooleanValue } from "../values/boolean-value"; 4 | import { NodeTest } from "./node-test"; 5 | 6 | export class NodeTestPI implements NodeTest { 7 | target: any; 8 | 9 | constructor(target: any) { 10 | this.target = target; 11 | } 12 | 13 | evaluate(context: ExprContext) { 14 | const node = context.nodeList[context.position]; 15 | return new BooleanValue( 16 | node.nodeType == DOM_PROCESSING_INSTRUCTION_NODE && (!this.target || node.nodeName == this.target) 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/xpath/node-tests/node-test-text.ts: -------------------------------------------------------------------------------- 1 | import { DOM_TEXT_NODE } from "../../constants"; 2 | import { ExprContext } from "../expr-context"; 3 | import { BooleanValue } from "../values/boolean-value"; 4 | import { NodeTest } from "./node-test"; 5 | 6 | export class NodeTestText implements NodeTest { 7 | evaluate(ctx: ExprContext) { 8 | return new BooleanValue(ctx.nodeList[ctx.position].nodeType == DOM_TEXT_NODE); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/xpath/node-tests/node-test.ts: -------------------------------------------------------------------------------- 1 | import { ExprContext } from "../expr-context"; 2 | import { NodeValue } from "../values"; 3 | 4 | export interface NodeTest { 5 | evaluate(_ctx: ExprContext): NodeValue; 6 | } 7 | -------------------------------------------------------------------------------- /src/xpath/tokens.ts: -------------------------------------------------------------------------------- 1 | // The tokens of the language. The label property is just used for 2 | // generating debug output. The prec property is the precedence used 3 | // for shift/reduce resolution. Default precedence is 0 as a lookahead 4 | // token and 2 on the stack. TODO(mesch): this is certainly not 5 | // necessary and too complicated. Simplify this! 6 | 7 | import { XML_NC_NAME } from "../dom/xmltoken"; 8 | import { XPathTokenRule } from "./xpath-token-rule"; 9 | 10 | // NOTE: tabular formatting is the big exception, but here it should 11 | // be OK. 12 | 13 | // The axes of XPath expressions. 14 | 15 | export const xPathAxis = { 16 | ANCESTOR_OR_SELF: 'ancestor-or-self', 17 | ANCESTOR: 'ancestor', 18 | ATTRIBUTE: 'attribute', 19 | CHILD: 'child', 20 | DESCENDANT_OR_SELF: 'descendant-or-self', 21 | DESCENDANT: 'descendant', 22 | FOLLOWING_SIBLING: 'following-sibling', 23 | FOLLOWING: 'following', 24 | NAMESPACE: 'namespace', 25 | PARENT: 'parent', 26 | PRECEDING_SIBLING: 'preceding-sibling', 27 | PRECEDING: 'preceding', 28 | SELF: 'self', 29 | SELF_AND_SIBLINGS: 'self-and-siblings' // Doesn't exist officially. 30 | // It is here for a special case of ``. 31 | }; 32 | 33 | const xpathAxesRe = 34 | [ 35 | xPathAxis.ANCESTOR_OR_SELF, 36 | xPathAxis.ANCESTOR, 37 | xPathAxis.ATTRIBUTE, 38 | xPathAxis.CHILD, 39 | xPathAxis.DESCENDANT_OR_SELF, 40 | xPathAxis.DESCENDANT, 41 | xPathAxis.FOLLOWING_SIBLING, 42 | xPathAxis.FOLLOWING, 43 | xPathAxis.NAMESPACE, 44 | xPathAxis.PARENT, 45 | xPathAxis.PRECEDING_SIBLING, 46 | xPathAxis.PRECEDING, 47 | xPathAxis.SELF 48 | ].join('(?=::)|') + '(?=::)'; //(viat) bodgy fix because namespace-uri() was getting detected as the namespace axis. maybe less bodgy fix later. 49 | 50 | 51 | export const TOK_PIPE: XPathTokenRule = { 52 | label: '|', 53 | prec: 17, 54 | re: new RegExp('^\\|'), 55 | key: undefined 56 | }; 57 | 58 | export const TOK_DSLASH: XPathTokenRule = { 59 | label: '//', 60 | prec: 19, 61 | re: new RegExp('^//'), 62 | key: undefined 63 | }; 64 | 65 | export const TOK_SLASH: XPathTokenRule = { 66 | label: '/', 67 | prec: 30, 68 | re: new RegExp('^/'), 69 | key: undefined 70 | }; 71 | 72 | export const TOK_AXIS: XPathTokenRule = { 73 | label: '::', 74 | prec: 20, 75 | re: new RegExp('^::'), 76 | key: undefined 77 | }; 78 | 79 | export const TOK_COLON: XPathTokenRule = { 80 | label: ':', 81 | prec: 1000, 82 | re: new RegExp('^:'), 83 | key: undefined 84 | }; 85 | 86 | export const TOK_AXISNAME: XPathTokenRule = { 87 | label: '[axis]', 88 | re: new RegExp(`^(${xpathAxesRe})`), 89 | key: undefined 90 | }; 91 | 92 | export const TOK_PARENO: XPathTokenRule = { 93 | label: '(', 94 | prec: 34, 95 | re: new RegExp('^\\('), 96 | key: undefined 97 | }; 98 | 99 | export const TOK_PARENC: XPathTokenRule = { 100 | label: ')', 101 | re: new RegExp('^\\)'), 102 | key: undefined 103 | }; 104 | export const TOK_DDOT: XPathTokenRule = { 105 | label: '..', 106 | prec: 34, 107 | re: new RegExp('^\\.\\.'), 108 | key: undefined 109 | }; 110 | 111 | export const TOK_DOT: XPathTokenRule = { 112 | label: '.', 113 | prec: 34, 114 | re: new RegExp('^\\.'), 115 | key: undefined 116 | }; 117 | 118 | export const TOK_AT: XPathTokenRule = { 119 | label: '@', 120 | prec: 34, 121 | re: new RegExp('^@'), 122 | key: undefined 123 | }; 124 | 125 | export const TOK_COMMA: XPathTokenRule = { 126 | label: ',', 127 | re: new RegExp('^,'), 128 | key: undefined 129 | }; 130 | 131 | export const TOK_OR: XPathTokenRule = { 132 | label: 'or', 133 | prec: 10, 134 | re: new RegExp('^or\\b'), 135 | key: undefined 136 | }; 137 | 138 | export const TOK_AND: XPathTokenRule = { 139 | label: 'and', 140 | prec: 11, 141 | re: new RegExp('^and\\b'), 142 | key: undefined 143 | }; 144 | 145 | export const TOK_EQ: XPathTokenRule = { 146 | label: '=', 147 | prec: 12, 148 | re: new RegExp('^='), 149 | key: undefined 150 | }; 151 | 152 | export const TOK_NEQ: XPathTokenRule = { 153 | label: '!=', 154 | prec: 12, 155 | re: new RegExp('^!='), 156 | key: undefined 157 | }; 158 | 159 | export const TOK_GE: XPathTokenRule = { 160 | label: '>=', 161 | prec: 13, 162 | re: new RegExp('^>='), 163 | key: undefined 164 | }; 165 | 166 | export const TOK_GT: XPathTokenRule = { 167 | label: '>', 168 | prec: 13, 169 | re: new RegExp('^>'), 170 | key: undefined 171 | }; 172 | 173 | export const TOK_LE: XPathTokenRule = { 174 | label: '<=', 175 | prec: 13, 176 | re: new RegExp('^<='), 177 | key: undefined 178 | }; 179 | 180 | export const TOK_LT: XPathTokenRule = { 181 | label: '<', 182 | prec: 13, 183 | re: new RegExp('^<'), 184 | key: undefined 185 | }; 186 | 187 | export const TOK_PLUS: XPathTokenRule = { 188 | label: '+', 189 | prec: 14, 190 | re: new RegExp('^\\+'), 191 | left: true, 192 | key: undefined 193 | }; 194 | 195 | export const TOK_MINUS: XPathTokenRule = { 196 | label: '-', 197 | prec: 14, 198 | re: new RegExp('^\\-'), 199 | left: true, 200 | key: undefined 201 | }; 202 | 203 | export const TOK_DIV: XPathTokenRule = { 204 | label: 'div', 205 | prec: 15, 206 | re: new RegExp('^div\\b'), 207 | left: true, 208 | key: undefined 209 | }; 210 | 211 | export const TOK_MOD: XPathTokenRule = { 212 | label: 'mod', 213 | prec: 15, 214 | re: new RegExp('^mod\\b'), 215 | left: true, 216 | key: undefined 217 | }; 218 | 219 | export const TOK_BRACKO: XPathTokenRule = { 220 | label: '[', 221 | prec: 32, 222 | re: new RegExp('^\\['), 223 | key: undefined 224 | }; 225 | 226 | export const TOK_BRACKC: XPathTokenRule = { 227 | label: ']', 228 | re: new RegExp('^\\]'), 229 | key: undefined 230 | }; 231 | 232 | export const TOK_DOLLAR: XPathTokenRule = { 233 | label: '$', 234 | re: new RegExp('^\\$'), 235 | key: undefined 236 | }; 237 | 238 | export const TOK_NCNAME: XPathTokenRule = { 239 | label: '[ncname]', 240 | re: new RegExp(`^${XML_NC_NAME}`), 241 | key: undefined 242 | }; 243 | 244 | export const TOK_ASTERISK: XPathTokenRule = { 245 | label: '*', 246 | prec: 15, 247 | re: new RegExp('^\\*'), 248 | left: true, 249 | key: undefined 250 | }; 251 | 252 | export const TOK_LITERALQ: XPathTokenRule = { 253 | label: '[litq]', 254 | prec: 20, 255 | re: new RegExp("^'[^\\']*'"), 256 | key: undefined 257 | }; 258 | 259 | export const TOK_LITERALQQ: XPathTokenRule = { 260 | label: '[litqq]', 261 | prec: 20, 262 | re: new RegExp('^"[^\\"]*"'), 263 | key: undefined 264 | }; 265 | 266 | export const TOK_NUMBER: XPathTokenRule = { 267 | label: '[number]', 268 | prec: 35, 269 | re: new RegExp('^\\d+(\\.\\d*)?'), 270 | key: undefined 271 | }; 272 | 273 | export const TOK_QNAME: XPathTokenRule = { 274 | label: '[qname]', 275 | re: new RegExp(`^(${XML_NC_NAME}:)?${XML_NC_NAME}`), 276 | key: undefined 277 | }; 278 | 279 | export const TOK_NODEO: XPathTokenRule = { 280 | label: '[nodeTest-start]', 281 | re: new RegExp('^(processing-instruction|comment|text|node)\\('), 282 | key: undefined 283 | }; 284 | 285 | // The table of the tokens of our grammar, used by the lexer: first 286 | // column the tag, second column a regexp to recognize it in the 287 | // input, third column the precedence of the token, fourth column a 288 | // factory function for the semantic value of the token. 289 | // 290 | // NOTE: order of this list is important, because the first match 291 | // counts. Cf. DDOT and DOT, and AXIS and COLON. 292 | 293 | export const xPathTokenRules: XPathTokenRule[] = [ 294 | TOK_DSLASH, 295 | TOK_SLASH, 296 | TOK_DDOT, 297 | TOK_DOT, 298 | TOK_AXIS, 299 | TOK_COLON, 300 | TOK_AXISNAME, 301 | TOK_NODEO, 302 | TOK_PARENO, 303 | TOK_PARENC, 304 | TOK_BRACKO, 305 | TOK_BRACKC, 306 | TOK_AT, 307 | TOK_COMMA, 308 | TOK_OR, 309 | TOK_AND, 310 | TOK_NEQ, 311 | TOK_EQ, 312 | TOK_GE, 313 | TOK_GT, 314 | TOK_LE, 315 | TOK_LT, 316 | TOK_PLUS, 317 | TOK_MINUS, 318 | TOK_ASTERISK, 319 | TOK_PIPE, 320 | TOK_MOD, 321 | TOK_DIV, 322 | TOK_LITERALQ, 323 | TOK_LITERALQQ, 324 | TOK_NUMBER, 325 | TOK_QNAME, 326 | TOK_NCNAME, 327 | TOK_DOLLAR 328 | ]; 329 | 330 | // Quantifiers that are used in the productions of the grammar. 331 | export const Q_ZERO_OR_ONE = { 332 | label: '?' 333 | }; 334 | export const Q_ZERO_OR_MULTIPLE = { 335 | label: '*' 336 | }; 337 | export const Q_ONE_OR_MULTIPLE = { 338 | label: '+' 339 | }; 340 | 341 | // Tag for left associativity (right assoc is implied by undefined). 342 | export const ASSOC_LEFT = true; 343 | -------------------------------------------------------------------------------- /src/xpath/values/boolean-value.ts: -------------------------------------------------------------------------------- 1 | import { XNode } from "../../dom"; 2 | import { NodeValue } from "./node-value"; 3 | 4 | export class BooleanValue implements NodeValue { 5 | value: any; 6 | type: string; 7 | 8 | constructor(value: any) { 9 | this.value = value; 10 | this.type = 'boolean'; 11 | } 12 | 13 | stringValue(): string { 14 | return `${this.value}`; 15 | } 16 | 17 | booleanValue() { 18 | return this.value; 19 | } 20 | 21 | numberValue() { 22 | return this.value ? 1 : 0; 23 | } 24 | 25 | nodeSetValue(): XNode[] { 26 | throw this; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/xpath/values/index.ts: -------------------------------------------------------------------------------- 1 | // XPath expression values. They are what XPath expressions evaluate 2 | // to. Strangely, the different value types are not specified in the 3 | // XPath syntax, but only in the semantics, so they don't show up as 4 | // nonterminals in the grammar. Yet, some expressions are required to 5 | // evaluate to particular types, and not every type can be coerced 6 | // into every other type. Although the types of XPath values are 7 | // similar to the types present in JavaScript, the type coercion rules 8 | // are a bit peculiar, so we explicitly model XPath types instead of 9 | // mapping them onto JavaScript types. (See XPath spec.) 10 | // 11 | // The four types are: 12 | // 13 | // - `StringValue` 14 | // - `NumberValue` 15 | // - `BooleanValue` 16 | // - `NodeSetValue` 17 | // 18 | // The common interface of the value classes consists of methods that 19 | // implement the XPath type coercion rules: 20 | // 21 | // - `stringValue()` -- returns the value as a JavaScript String; 22 | // - `numberValue()` -- returns the value as a JavaScript Number; 23 | // - `booleanValue()` -- returns the value as a JavaScript Boolean; 24 | // - `nodeSetValue()` -- returns the value as a JavaScript Array of DOM 25 | // Node objects. 26 | 27 | export * from './boolean-value'; 28 | export * from './node-set-value'; 29 | export * from './node-value'; 30 | export * from './number-value'; 31 | export * from './string-value'; 32 | -------------------------------------------------------------------------------- /src/xpath/values/node-set-value.ts: -------------------------------------------------------------------------------- 1 | import { XNode, xmlValue } from "../../dom"; 2 | import { NodeValue } from "./node-value"; 3 | 4 | export class NodeSetValue implements NodeValue { 5 | value: XNode[]; 6 | type: string; 7 | 8 | constructor(value: XNode[]) { 9 | this.value = value; 10 | this.type = 'node-set'; 11 | } 12 | 13 | stringValue(): string { 14 | if (this.value.length === 0) { 15 | return ''; 16 | } 17 | 18 | return xmlValue(this.value[0]); 19 | } 20 | 21 | booleanValue() { 22 | return this.value.length > 0; 23 | } 24 | 25 | numberValue() { 26 | return parseInt(this.stringValue()) - 0; 27 | } 28 | 29 | nodeSetValue(): XNode[] { 30 | return this.value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/xpath/values/node-value.ts: -------------------------------------------------------------------------------- 1 | import { XNode } from "../../dom"; 2 | 3 | export interface NodeValue { 4 | stringValue(): string; 5 | booleanValue(): boolean; 6 | numberValue(): number; 7 | nodeSetValue(): XNode[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/xpath/values/number-value.ts: -------------------------------------------------------------------------------- 1 | import { XNode } from "../../dom"; 2 | import { NodeValue } from "./node-value"; 3 | 4 | export class NumberValue implements NodeValue { 5 | value: any; 6 | type: string; 7 | 8 | constructor(value: any) { 9 | this.value = value; 10 | this.type = 'number'; 11 | } 12 | 13 | stringValue(): string { 14 | return `${this.value}`; 15 | } 16 | 17 | booleanValue() { 18 | return !!this.value; 19 | } 20 | 21 | numberValue() { 22 | return this.value - 0; 23 | } 24 | 25 | nodeSetValue(): XNode[] { 26 | throw this; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/xpath/values/string-value.ts: -------------------------------------------------------------------------------- 1 | import { XNode } from "../../dom"; 2 | import { NodeValue } from "./node-value"; 3 | 4 | export class StringValue implements NodeValue { 5 | value: any; 6 | type: string; 7 | 8 | constructor(value: any) { 9 | this.value = value; 10 | this.type = 'string'; 11 | } 12 | 13 | stringValue(): string { 14 | return String(this.value); 15 | } 16 | 17 | booleanValue() { 18 | return this.value.length > 0; 19 | } 20 | 21 | numberValue() { 22 | return this.value - 0; 23 | } 24 | 25 | nodeSetValue(): XNode[] { 26 | throw this; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/xpath/xpath-grammar-rules.ts: -------------------------------------------------------------------------------- 1 | // All the nonterminals of the grammar. The nonterminal objects are 2 | // identified by object identity; the labels are used in the debug 3 | 4 | // output only. 5 | export const XPathLocationPath = { 6 | label: 'LocationPath', 7 | key: undefined 8 | }; 9 | 10 | export const XPathRelativeLocationPath = { 11 | label: 'RelativeLocationPath', 12 | key: undefined 13 | }; 14 | 15 | export const XPathAbsoluteLocationPath = { 16 | label: 'AbsoluteLocationPath', 17 | key: undefined 18 | }; 19 | 20 | export const XPathStep = { 21 | label: 'Step', 22 | key: undefined 23 | }; 24 | 25 | export const XPathNodeTest = { 26 | label: 'NodeTest', 27 | key: undefined 28 | }; 29 | 30 | export const XPathPredicate = { 31 | label: 'Predicate', 32 | key: undefined 33 | }; 34 | 35 | export const XPathLiteral = { 36 | label: 'Literal', 37 | key: undefined 38 | }; 39 | 40 | export const XPathExpr = { 41 | label: 'Expr', 42 | key: undefined 43 | }; 44 | 45 | export const XPathPrimaryExpr = { 46 | label: 'PrimaryExpr', 47 | key: undefined 48 | }; 49 | 50 | export const XPathVariableReference = { 51 | label: 'Variablereference', 52 | key: undefined 53 | }; 54 | 55 | export const XPathNumber = { 56 | label: 'Number', 57 | key: undefined 58 | }; 59 | 60 | export const XPathFunctionCall = { 61 | label: 'FunctionCall', 62 | key: undefined 63 | }; 64 | 65 | export const XPathArgumentRemainder = { 66 | label: 'ArgumentRemainder', 67 | key: undefined 68 | }; 69 | 70 | export const XPathPathExpr = { 71 | label: 'PathExpr', 72 | key: undefined 73 | }; 74 | 75 | export const XPathUnionExpr = { 76 | label: 'UnionExpr', 77 | key: undefined 78 | }; 79 | 80 | export const XPathFilterExpr = { 81 | label: 'FilterExpr', 82 | key: undefined 83 | }; 84 | 85 | export const XPathDigits = { 86 | label: 'Digits', 87 | key: undefined 88 | }; 89 | -------------------------------------------------------------------------------- /src/xpath/xpath-token-rule.ts: -------------------------------------------------------------------------------- 1 | export type XPathTokenRule = { 2 | label: string, 3 | prec?: number, 4 | re: RegExp, 5 | key?: any, 6 | left?: boolean 7 | } 8 | -------------------------------------------------------------------------------- /src/xpathdebug.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Design Liquido 2 | // Copyright 2018 Johannes Wilm 3 | // Copyright 2005 Google Inc. 4 | // All Rights Reserved 5 | // 6 | // Debug stuff for the XPath parser. Also used by XSLT. 7 | import { XNode } from './dom'; 8 | import { 9 | ExprContext 10 | } from './xpath'; 11 | import { 12 | BinaryExpr, 13 | FilterExpr, 14 | FunctionCallExpr, 15 | LiteralExpr, 16 | LocationExpr, 17 | NumberExpr, 18 | PathExpr, 19 | PredicateExpr, 20 | StepExpr, 21 | TokenExpr, 22 | UnaryMinusExpr, 23 | UnionExpr, 24 | VariableExpr 25 | } from './xpath/expressions'; 26 | import { NodeTestAny, NodeTestElementOrAttribute, NodeTestText, NodeTestComment, NodeTestPI, NodeTestName, NodeTestNC } from './xpath/node-tests'; 27 | import { StringValue, NumberValue, BooleanValue, NodeSetValue } from './xpath/values'; 28 | 29 | export let parseTree = function (expr, indent) { 30 | let ret; 31 | switch (expr.constructor) { 32 | case TokenExpr: 33 | ret = `${indent}[token] ${expr.value}\n`; 34 | break; 35 | case LocationExpr: 36 | ret = `${indent}[location] ${expr.absolute ? 'absolute' : 'relative'}\n`; 37 | for (let i = 0; i < expr.steps.length; ++i) { 38 | ret += parseTree(expr.steps[i], `${indent} `); 39 | } 40 | break; 41 | case StepExpr: 42 | ret = `${indent}[step]\n${indent} [axis] ${expr.axis}\n${parseTree(expr.nodeTest, `${indent} `)}`; 43 | for (let i = 0; i < expr.predicate.length; ++i) { 44 | ret += parseTree(expr.predicate[i], `${indent} `); 45 | } 46 | break; 47 | case NodeTestAny: 48 | case NodeTestElementOrAttribute: 49 | case NodeTestText: 50 | case NodeTestComment: 51 | case NodeTestPI: 52 | case NodeTestName: 53 | case NodeTestNC: 54 | ret = `${indent}[nodeTest] ${toString(expr)}\n`; 55 | break; 56 | case PredicateExpr: 57 | ret = `${indent}[predicate]\n${parseTree(expr.expr, `${indent} `)}`; 58 | break; 59 | case FunctionCallExpr: 60 | ret = `${indent}[function call] ${expr.name.value}\n`; 61 | for (let i = 0; i < expr.args.length; ++i) { 62 | ret += parseTree(expr.args[i], `${indent} `); 63 | } 64 | break; 65 | case UnionExpr: 66 | ret = `${indent}[union]\n${parseTree(expr.expr1, indent + ' ')}${parseTree(expr.expr2, `${indent} `)}`; 67 | break; 68 | case PathExpr: 69 | ret = `${indent}[path]\n${indent}- filter:\n${parseTree( 70 | expr.filter, 71 | `${indent} ` 72 | )}${indent}- location path:\n${parseTree(expr.rel, `${indent} `)}`; 73 | break; 74 | case FilterExpr: 75 | ret = `${indent}[filter]\n${indent}- expr:\n${parseTree(expr.expr, `${indent} `)}`; 76 | `${indent}- predicates:\n`; 77 | for (let i = 0; i < expr.predicate.length; ++i) { 78 | ret += parseTree(expr.predicate[i], `${indent} `); 79 | } 80 | break; 81 | case UnaryMinusExpr: 82 | ret = `${indent}[unary] -\n${parseTree(expr.expr, `${indent} `)}`; 83 | break; 84 | case BinaryExpr: 85 | ret = `${indent}[binary] ${expr.op.value}\n${parseTree(expr.expr1, `${indent} `)}${parseTree( 86 | expr.expr2, 87 | `${indent} ` 88 | )}`; 89 | break; 90 | case LiteralExpr: 91 | ret = `${indent}[literal] ${toString(expr)}\n`; 92 | break; 93 | case NumberExpr: 94 | ret = `${indent}[number] ${toString(expr)}\n`; 95 | break; 96 | case VariableExpr: 97 | ret = `${indent}[variable] ${toString(expr)}\n`; 98 | break; 99 | case StringValue: 100 | case NumberValue: 101 | case BooleanValue: 102 | case NodeSetValue: 103 | ret = `${expr.type}: ${expr.value}`; 104 | break; 105 | 106 | default: 107 | break; 108 | } 109 | return ret; 110 | }; 111 | 112 | export let toString = function (expr) { 113 | let ret; 114 | switch (expr.constructor) { 115 | case FunctionCallExpr: 116 | ret = `${expr.name.value}(`; 117 | for (let i = 0; i < expr.args.length; ++i) { 118 | if (i > 0) { 119 | ret += ', '; 120 | } 121 | ret += toString(expr.args[i]); 122 | } 123 | ret += ')'; 124 | break; 125 | case UnionExpr: 126 | ret = `${toString(expr.expr1)} | ${toString(expr.expr2)}`; 127 | break; 128 | case PathExpr: 129 | ret = `{path: {${toString(expr.filter)}} {${toString(expr.rel)}}}`; 130 | break; 131 | case FilterExpr: 132 | ret = toString(expr.expr); 133 | for (let i = 0; i < expr.predicate.length; ++i) { 134 | ret += toString(expr.predicate[i]); 135 | } 136 | break; 137 | case UnaryMinusExpr: 138 | ret = `-${toString(expr.expr)}`; 139 | break; 140 | case BinaryExpr: 141 | ret = `${toString(expr.expr1)} ${expr.op.value} ${toString(expr.expr2)}`; 142 | break; 143 | case LiteralExpr: 144 | ret = `"${expr.value}"`; 145 | break; 146 | case NumberExpr: 147 | ret = `${expr.value}`; 148 | break; 149 | case VariableExpr: 150 | ret = `$${expr.name}`; 151 | break; 152 | case XNode: 153 | ret = expr.nodeName; 154 | break; 155 | case ExprContext: 156 | ret = `[${expr.position}/${expr.nodeList.length}] ${expr.node.nodeName}`; 157 | break; 158 | case TokenExpr: 159 | ret = expr.value; 160 | break; 161 | case LocationExpr: 162 | ret = ''; 163 | if (expr.absolute) { 164 | ret += '/'; 165 | } 166 | for (let i = 0; i < expr.steps.length; ++i) { 167 | if (i > 0) { 168 | ret += '/'; 169 | } 170 | ret += toString(expr.steps[i]); 171 | } 172 | break; 173 | case StepExpr: 174 | ret = `${expr.axis}::${toString(expr.nodeTest)}`; 175 | for (let i = 0; i < expr.predicate.length; ++i) { 176 | ret += toString(expr.predicate[i]); 177 | } 178 | break; 179 | case NodeTestAny: 180 | ret = 'node()'; 181 | break; 182 | case NodeTestElementOrAttribute: 183 | ret = '*'; 184 | break; 185 | case NodeTestText: 186 | ret = 'text()'; 187 | break; 188 | case NodeTestComment: 189 | ret = 'comment()'; 190 | break; 191 | case NodeTestPI: 192 | ret = 'processing-instruction()'; 193 | break; 194 | case NodeTestNC: 195 | ret = `${expr.nsprefix}:*`; 196 | break; 197 | case NodeTestName: 198 | ret = expr.name; 199 | break; 200 | case PredicateExpr: 201 | ret = `[${toString(expr.expr)}]`; 202 | break; 203 | default: 204 | break; 205 | } 206 | return ret; 207 | }; 208 | -------------------------------------------------------------------------------- /src/xslt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './xslt-options'; 2 | export * from './xslt-parameter'; 3 | export * from './xslt'; 4 | -------------------------------------------------------------------------------- /src/xslt/xslt-decimal-format-settings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * According to https://www.w3schools.com/xml/ref_xsl_el_decimal-format.asp: 3 | * 4 | * @property {string} name: Optional. Specifies a name for this format. 5 | * @property {string} decimalSeparator: Optional. Specifies the decimal point character. Default is ".". 6 | * @property {string} groupingSeparator: Optional. Specifies the thousands separator character. Default is ",". 7 | * @property {string} infinity: Optional. Specifies the string used to represent infinity. Default is "Infinity". 8 | * @property {string} minusSign: Optional. Specifies the character to represent negative numbers. Default is "-". 9 | * @property {string} naN: Optional. Specifies the string used when the value is not a number". Default is "NaN". 10 | * @property {string} percent: Optional. Specifies the percentage sign character. Default is "%". 11 | * @property {string} perMille: Optional. Specifies the per thousand sign character. Default is "‰". 12 | * @property {string} zeroDigit: Optional. Specifies the digit zero character. Default is "0". 13 | * @property {string} digit: Optional. Specifies the character used to indicate a place where a digit is required. Default is #. 14 | * @property {string} patternSeparator: Optional. Specifies the character used to separate positive and negative subpatterns in a format pattern. Default is ";". 15 | */ 16 | export type XsltDecimalFormatSettings = { 17 | name?: string, 18 | decimalSeparator: string, 19 | groupingSeparator: string, 20 | infinity: string, 21 | minusSign: string, 22 | naN: string, 23 | percent: string, 24 | perMille: string, 25 | zeroDigit: string, 26 | digit: string, 27 | patternSeparator: string 28 | } 29 | -------------------------------------------------------------------------------- /src/xslt/xslt-options.ts: -------------------------------------------------------------------------------- 1 | import { XsltParameter } from "./xslt-parameter" 2 | 3 | export type XsltOptions = { 4 | cData: boolean, 5 | escape: boolean, 6 | selfClosingTags: boolean, 7 | parameters?: XsltParameter[] 8 | } 9 | -------------------------------------------------------------------------------- /src/xslt/xslt-parameter.ts: -------------------------------------------------------------------------------- 1 | export type XsltParameter = { 2 | name: string; 3 | namespaceUri?: string; 4 | value: any; 5 | } 6 | -------------------------------------------------------------------------------- /tests/dom.test.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Design Liquido 2 | // Copyright 2018 Johannes Wilm 3 | // Copyright 2005 Google Inc. 4 | // All Rights Reserved 5 | // 6 | // 7 | // Author: Steffen Meschkat 8 | // Junji Takagi 9 | // Johannes Wilm 10 | import he from 'he'; 11 | import assert from 'assert'; 12 | 13 | import { XmlParser, xmlText } from '../src/dom'; 14 | 15 | import { DOM_ATTRIBUTE_NODE } from '../src/constants'; 16 | 17 | describe('dom parsing', () => { 18 | let xmlParser: XmlParser; 19 | 20 | beforeAll(() => { 21 | xmlParser = new XmlParser(); 22 | }); 23 | 24 | it('can parse xml', () => { 25 | const xml = " " + 26 | " " + 27 | " new york " + 28 | " " + 29 | " " + 30 | ""; 31 | 32 | const dom1 = xmlParser.xmlParse(`${xml}`); 33 | const dom2 = xmlParser.xmlParse(`${xml}`); 34 | doTestXmlParse(dom1, dom2); 35 | 36 | const dom1Attributes = dom1.firstChild.childNodes[1].childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); 37 | const dom2Attributes = dom2.firstChild.childNodes[1].childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); 38 | 39 | assert.equal( 40 | dom1Attributes.length, 41 | dom2Attributes.length, 42 | 'location.attributes.length' 43 | ); 44 | 45 | const tag = 'q'; 46 | const byTag = dom1.getElementsByTagName(tag); 47 | assert.equal(1, byTag.length); 48 | assert.equal(tag, (byTag[0] as any).nodeName); 49 | 50 | const id = 'q'; 51 | const byId = dom1.getElementById(id); 52 | assert.notEqual(byId, null); 53 | assert.equal(id, byId.getAttributeValue('id')); 54 | }); 55 | 56 | it('can parse weird xml', () => { 57 | const xml = [ 58 | '<_>', 59 | '<_.:->', 60 | '<:>!"#$%&\'()*+,-./:;<=>?[\\]^_`{|}~', 61 | '', 62 | '<:-_. _=".-" :="-."/>', 63 | '' 64 | ].join(''); 65 | 66 | const dom1 = xmlParser.xmlParse(`${xml}`); 67 | const dom2 = xmlParser.xmlParse(`${xml}`); 68 | doTestXmlParse(dom1, dom2); 69 | 70 | const dom1Attributes = dom1.firstChild.childNodes[1].childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); 71 | const dom2Attributes = dom2.firstChild.childNodes[1].childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); 72 | 73 | assert.equal( 74 | dom1Attributes.length, 75 | dom2Attributes.length, 76 | 'location.attributes.length' 77 | ); 78 | assert.equal(dom1Attributes.length, 2, 'location.attributes.length'); 79 | }); 80 | 81 | it('can parse Japanese xml', () => { 82 | const xml = [ 83 | '<\u30da\u30fc\u30b8>', 84 | '<\u30ea\u30af\u30a8\u30b9\u30c8>', 85 | '<\u30af\u30a8\u30ea>\u6771\u4eac', 86 | '', 87 | '<\u4f4d\u7f6e \u7def\u5ea6="\u4e09\u5341\u4e94" ', 88 | "\u7d4c\u5ea6='\u767e\u56db\u5341'/>", 89 | '' 90 | ].join(''); 91 | 92 | const dom1 = xmlParser.xmlParse(`${xml}`); 93 | const dom2 = xmlParser.xmlParse(`${xml}`); 94 | doTestXmlParse(dom1, dom2); 95 | 96 | const dom1Attributes = dom1.firstChild.childNodes[1].childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); 97 | const dom2Attributes = dom2.firstChild.childNodes[1].childNodes.filter(n => n.nodeType === DOM_ATTRIBUTE_NODE); 98 | 99 | assert.equal( 100 | dom1Attributes.length, 101 | dom2Attributes.length, 102 | 'location.attributes.length' 103 | ); 104 | assert.equal(dom1Attributes.length, 2, 'location.attributes.length'); 105 | }); 106 | 107 | it('can resolve entities', () => { 108 | assert.equal('";"', he.decode('";"')); 109 | }); 110 | }); 111 | 112 | const doTestXmlParse = (dom1: any, dom2: any) => { 113 | assert.equal(xmlText(dom1), xmlText(dom2), 'xmlText'); 114 | 115 | assert.equal(dom1.nodeName, dom2.nodeName, '#document'); 116 | 117 | assert.equal(dom1.documentElement, dom1.firstChild, 'documentElement'); 118 | assert.equal(dom2.documentElement, dom2.firstChild, 'documentElement'); 119 | 120 | assert.equal(dom1.parentNode, null, 'parentNode'); 121 | assert.equal(dom2.parentNode, null, 'parentNode'); 122 | 123 | assert.equal(dom1.documentElement.parentNode, dom1, 'parentNode'); 124 | assert.equal(dom2.documentElement.parentNode, dom2, 'parentNode'); 125 | 126 | assert.equal(dom1.documentElement.nodeName, dom2.documentElement.nodeName, 'page'); 127 | assert.equal(dom1.childNodes.length, dom2.childNodes.length, 'dom.childNodes.length'); 128 | assert.equal(dom1.childNodes.length, dom2.childNodes.length, 'dom.childNodes.length'); 129 | assert.equal(dom1.firstChild.childNodes.length, dom2.firstChild.childNodes.length, 'dom.childNodes.length'); 130 | assert.equal(dom1.firstChild.childNodes.length, dom2.firstChild.childNodes.length, 'dom.childNodes.length'); 131 | }; 132 | -------------------------------------------------------------------------------- /tests/interactive-tests-examples.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import assert from 'assert'; 3 | 4 | import { Xslt } from '../src/xslt'; 5 | import { XmlParser } from '../src/dom'; 6 | 7 | describe('Interactive Tests Examples', () => { 8 | // TODO: Per https://github.com/DesignLiquido/xslt-processor/issues/116, while debugging 9 | // this test, we've found that `` with a `select` attribute + 10 | // `` does not work well for relative XPath (it works for absolute XPath). 11 | // The problem happens because the implementation calls the traditional ``, 12 | // which it tries to re-select nodes that are already selected in the expression context by 13 | // ``. Instead, it should "select the appropriate template", as mentioned 14 | // at https://www.w3.org/TR/xslt-10/#section-Applying-Template-Rules and 15 | // https://learn.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/ms256184(v=vs.100), 16 | // and process each child by the selected template. 17 | it.skip('Former xslt.html', async () => { 18 | const xmlString = ( 19 | ` 20 | 21 | Hello World. 22 | 23 | ` 24 | ); 25 | 26 | const xsltString = 27 | ` 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 |
38 |
`; 39 | 40 | const xsltClass = new Xslt(); 41 | const xmlParser = new XmlParser(); 42 | const xml = xmlParser.xmlParse(xmlString); 43 | const xslt = xmlParser.xmlParse(xsltString); 44 | 45 | const outXmlString = await xsltClass.xsltProcess(xml, xslt); 46 | 47 | assert.ok(outXmlString); 48 | }); 49 | }); -------------------------------------------------------------------------------- /tests/local-name.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import assert from 'assert'; 3 | 4 | import { Xslt } from '../src/xslt'; 5 | import { XmlParser } from '../src/dom'; 6 | 7 | describe('local-name', () => { 8 | it('local-name() without namespace test', async () => { 9 | const xmlString = ( 10 | ` 11 | 12 | 13 | 14 | 15 | ` 16 | ); 17 | 18 | const xsltString = 19 | ` 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | `; 35 | 36 | const expectedOutString = `test1test2test3test4`; 37 | 38 | const xsltClass = new Xslt(); 39 | const xmlParser = new XmlParser(); 40 | const xml = xmlParser.xmlParse(xmlString); 41 | const xslt = xmlParser.xmlParse(xsltString); 42 | 43 | const outXmlString = await xsltClass.xsltProcess(xml, xslt); 44 | 45 | assert.equal(outXmlString, expectedOutString); 46 | }); 47 | 48 | it('local-name() with namespace test', async () => { 49 | const xmlString = ( 50 | ` 51 | 52 | 53 | 54 | 55 | ` 56 | ); 57 | 58 | const xsltString = 59 | ` 60 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | `; 79 | 80 | const expectedOutString = `test1test2test3test4`; 81 | 82 | const xsltClass = new Xslt(); 83 | const xmlParser = new XmlParser(); 84 | const xml = xmlParser.xmlParse(xmlString); 85 | const xslt = xmlParser.xmlParse(xsltString); 86 | 87 | const outXmlString = await xsltClass.xsltProcess(xml, xslt); 88 | 89 | assert.equal(outXmlString, expectedOutString); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/namespaces.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable no-undef */ 3 | import assert from 'assert'; 4 | 5 | import { Xslt } from '../src/xslt'; 6 | import { XmlParser } from '../src/dom'; 7 | 8 | // TODO: 9 | // "xsl" prefix for non-XSL namespace 10 | // namespaces in input XML 11 | // using namespace prefixes in xpath 12 | 13 | describe('namespaces', () => { 14 | it('non-"xsl" prefix in stylesheet test', async () => { 15 | const xmlString = ( 16 | ` 17 | 18 | 19 | 20 | 21 | ` 22 | ); 23 | 24 | const xsltString = 25 | ` 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 |
36 |
37 |
`; 38 | 39 | const expectedOutString = `
test1test2test3test4
`; 40 | 41 | const xsltClass = new Xslt(); 42 | const xmlParser = new XmlParser(); 43 | const xml = xmlParser.xmlParse(xmlString); 44 | const xslt = xmlParser.xmlParse(xsltString); 45 | const outXmlString = await xsltClass.xsltProcess( 46 | xml, 47 | xslt 48 | ); 49 | 50 | assert.equal(outXmlString, expectedOutString); 51 | }); 52 | 53 | // TODO: Fix test to be relevant again. 54 | it.skip('namespace-uri() test', async () => { 55 | const xmlString = ( 56 | ` 57 | 58 | 59 | 60 | 61 | ` 62 | ); 63 | 64 | const xsltString = 65 | ` 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 | 75 |
76 |
77 |
`; 78 | 79 | const expectedOutString = ( 80 | `
81 | http://example.com 82 | http://example.test/2 83 | http://example.test/3 84 | 85 |
` 86 | ); 87 | 88 | const xsltClass = new Xslt(); 89 | const xmlParser = new XmlParser(); 90 | const xml = xmlParser.xmlParse(xmlString); 91 | const xslt = xmlParser.xmlParse(xsltString); 92 | const outXmlString = await xsltClass.xsltProcess( 93 | xml, 94 | xslt 95 | ); 96 | 97 | assert.equal(outXmlString, expectedOutString); 98 | }); 99 | 100 | it('namespace per node', async () => { 101 | const xmlString = ` 102 | 103 | 104 | 105 | 106 | 107 | `; 108 | 109 | const xsltString = ` 110 | 113 | 114 | 115 | 116 | `; 117 | 118 | const expectedOutString = `` + 119 | `` + 120 | `` + 121 | ``; 122 | 123 | const xsltClass = new Xslt(); 124 | // Uncomment to see how XPath resolves. 125 | // xsltClass.xPath.xPathLog = console.log; 126 | const xmlParser = new XmlParser(); 127 | const xml = xmlParser.xmlParse(xmlString); 128 | const xslt = xmlParser.xmlParse(xsltString); 129 | const outXmlString = await xsltClass.xsltProcess( 130 | xml, 131 | xslt 132 | ); 133 | 134 | assert.equal(outXmlString, expectedOutString); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /tests/root-element.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import assert from 'assert'; 3 | 4 | import { Xslt } from '../src/xslt'; 5 | import { XmlParser } from '../src/dom'; 6 | 7 | describe('root-element', () => { 8 | it('select root element test', async () => { 9 | const xmlString = ( 10 | ` 11 | 12 | 13 | 14 | 15 | ` 16 | ); 17 | 18 | const xsltString = 19 | ` 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 |
31 |
`; 32 | 33 | const expectedOutString = `
test1test2test3test4
`; 34 | 35 | const xsltClass = new Xslt(); 36 | const xmlParser = new XmlParser(); 37 | const xml = xmlParser.xmlParse(xmlString); 38 | const xslt = xmlParser.xmlParse(xsltString); 39 | const outXmlString = await xsltClass.xsltProcess(xml, xslt); 40 | 41 | assert.equal(outXmlString, expectedOutString); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/simple.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import assert from 'assert'; 3 | 4 | import { Xslt } from '../src/xslt'; 5 | import { XmlParser } from '../src/dom'; 6 | 7 | describe('simple', () => { 8 | it('simple test', async () => { 9 | const xmlString = ( 10 | ` 11 | 12 | 13 | 14 | 15 | ` 16 | ); 17 | 18 | const xsltString = 19 | ` 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 |
31 |
`; 32 | 33 | const expectedOutString = ( 34 | `
test1test2test3test4
` 35 | ); 36 | 37 | const xsltClass = new Xslt(); 38 | const xmlParser = new XmlParser(); 39 | const xml = xmlParser.xmlParse(xmlString); 40 | const xslt = xmlParser.xmlParse(xsltString); 41 | const outXmlString = await xsltClass.xsltProcess(xml, xslt); 42 | 43 | assert.equal(outXmlString, expectedOutString); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/template-precedence.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import assert from 'assert'; 3 | 4 | import { Xslt } from '../src/xslt'; 5 | import { XmlParser } from '../src/dom'; 6 | 7 | describe('template-precedence', () => { 8 | it('XSLT template precedence test', async () => { 9 | const xmlString = ` 10 | 11 | 12 | 13 | 14 | `; 15 | 16 | const xsltString = ` 17 | 18 | 19 | 20 | 21 | 22 | another name 23 | 24 | 25 |
26 | 27 |
28 |
29 |
`; 30 | 31 | const expectedOutString = `
another nametest2test3test4
`; 32 | 33 | const xsltClass = new Xslt(); 34 | const xmlParser = new XmlParser(); 35 | const xml = xmlParser.xmlParse(xmlString); 36 | const xslt = xmlParser.xmlParse(xsltString); 37 | 38 | const outXmlString = await xsltClass.xsltProcess(xml, xslt); 39 | 40 | assert.equal(outXmlString, expectedOutString); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/variables-as-parameters.test.tsx: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import { Xslt } from '../src/xslt'; 4 | import { XmlParser } from '../src/dom'; 5 | 6 | describe('variables-as-parameters', () => { 7 | it('variables-as-parameters 1', async () => { 8 | const xmlString = ` 9 | 10 | `; 11 | 12 | const xsltString = ` 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | `; 26 | 27 | const expectedOutString = `hugo`; 28 | 29 | const xsltClass = new Xslt({ parameters: [ 30 | { name: 'test', value: 'hugo' } 31 | ] }); 32 | const xmlParser = new XmlParser(); 33 | const xml = xmlParser.xmlParse(xmlString); 34 | const xslt = xmlParser.xmlParse(xsltString); 35 | const outXmlString = await xsltClass.xsltProcess( 36 | xml, 37 | xslt, 38 | ); 39 | 40 | assert.equal(outXmlString, expectedOutString); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/xml/escape.test.tsx: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import { XmlParser, xmlText } from '../../src/dom'; 4 | 5 | describe('escape', () => { 6 | let xmlParser: XmlParser; 7 | 8 | beforeAll(() => { 9 | xmlParser = new XmlParser(); 10 | }); 11 | 12 | it('accepts already escaped ampersand', () => { 13 | const xmlString = 'Fish&pie'; 14 | 15 | const outXmlString = xmlText(xmlParser.xmlParse(xmlString)); 16 | 17 | assert.equal(outXmlString, xmlString); 18 | }); 19 | 20 | it('escapes non-escaped ampersand', () => { 21 | const xmlString = 'Fish&pie'; 22 | 23 | const outXmlString = xmlText(xmlParser.xmlParse(xmlString)); 24 | 25 | assert.equal(outXmlString, 'Fish&pie'); 26 | }); 27 | 28 | it('accepts non-escaped ">" between elements', () => { 29 | const xmlString = 'Fish>pie'; 30 | 31 | const outXmlString = xmlText(xmlParser.xmlParse(xmlString)); 32 | 33 | assert.equal(outXmlString, 'Fish>pie'); 34 | }); 35 | 36 | it('accepts non-escaped "\'" between elements', () => { 37 | const xmlString = "Fish'pie"; 38 | 39 | const outXmlString = xmlText(xmlParser.xmlParse(xmlString)); 40 | 41 | assert.equal(outXmlString, "Fish'pie"); 42 | }); 43 | 44 | it("accepts non-escaped '\"' between elements", () => { 45 | const xmlString = 'Fish"pie'; 46 | 47 | const outXmlString = xmlText(xmlParser.xmlParse(xmlString)); 48 | 49 | assert.equal(outXmlString, 'Fish"pie'); 50 | }); 51 | 52 | it('accepts non-escaped ">" in attributes', () => { 53 | const xmlString = 'Fish'; 54 | 55 | const outXmlString = xmlText(xmlParser.xmlParse(xmlString)); 56 | 57 | assert.equal(outXmlString, 'Fish'); 58 | }); 59 | 60 | it('accepts non-escaped "\'" in attributes', () => { 61 | const xmlString = 'Fish'; 62 | 63 | const outXmlString = xmlText(xmlParser.xmlParse(xmlString)); 64 | 65 | assert.equal(outXmlString, 'Fish'); 66 | }); 67 | 68 | it("accepts non-escaped '\"' in attributes", () => { 69 | const xmlString = "Fish"; 70 | const outXmlString = xmlText(xmlParser.xmlParse(xmlString)); 71 | 72 | assert.equal(outXmlString, 'Fish'); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/xml/html.test.tsx: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { XmlParser, xmlText } from "../../src/dom"; 3 | 4 | describe('HTML', () => { 5 | it('Trivial', () => { 6 | const htmlString = '' + 7 | ` 8 | 9 | 10 | About - Simple Blog Template 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 55 | 56 | 57 | 58 |
59 | 60 |
61 | 62 |
63 | 64 | 65 |

About

66 | 67 |
68 | 69 | 70 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ducimus, vero, obcaecati, aut, error quam sapiente nemo saepe quibusdam sit excepturi nam quia corporis eligendi eos magni recusandae laborum minus inventore?

71 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus.

72 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eos, doloribus, dolorem iusto blanditiis unde eius illum consequuntur neque dicta incidunt ullam ea hic porro optio ratione repellat perspiciatis. Enim, iure!

73 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error, nostrum, aliquid, animi, ut quas placeat totam sunt tempora commodi nihil ullam alias modi dicta saepe minima ab quo voluptatem obcaecati?

74 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Harum, dolor quis. Sunt, ut, explicabo, aliquam tenetur ratione tempore quidem voluptates cupiditate voluptas illo saepe quaerat numquam recusandae? Qui, necessitatibus, est!

75 | 76 |
77 | 78 |
79 |
80 | 81 | 82 |
83 | 84 | 85 | 86 |
87 |
88 |
89 |
90 |

Copyright © Your Website 2014

91 |
92 | 93 |
94 | 95 |
96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | `; 107 | 108 | const xmlParser = new XmlParser(); 109 | const parsedHtml = xmlParser.xmlParse(htmlString); 110 | const outHtmlString = xmlText(parsedHtml, { 111 | cData: true, 112 | selfClosingTags: false, 113 | escape: true, 114 | outputMethod: 'html' 115 | }); 116 | 117 | // Uncomment to see the result. 118 | // console.log(outHtmlString); 119 | assert.ok(outHtmlString); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /tests/xml/xml-to-html.test.tsx: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { XmlParser } from "../../src/dom"; 3 | import { Xslt } from '../../src/xslt'; 4 | 5 | describe('XML to HTML', () => { 6 | it('Issue 74', async () => { 7 | const xmlString = ` 8 | `; 9 | 10 | const xsltString = ` 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
ABC
AABBCC
27 |
28 |
29 |
30 |
should be below table rite??!
31 |
32 |
33 |
`; 34 | 35 | const expectedOutHtml = `
ABC
AABBCC
should be below table rite??!
`; 36 | 37 | const xsltClass = new Xslt(); 38 | const xmlParser = new XmlParser(); 39 | const xml = xmlParser.xmlParse(xmlString); 40 | const xslt = xmlParser.xmlParse(xsltString); 41 | const outXmlString = await xsltClass.xsltProcess( 42 | xml, 43 | xslt 44 | ); 45 | 46 | // console.log(outXmlString); 47 | assert.equal(outXmlString, expectedOutHtml); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/xml/xml-to-json.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | import assert from 'assert'; 3 | 4 | import { Xslt } from '../../src/xslt'; 5 | import { XmlParser } from '../../src/dom'; 6 | 7 | describe('xml-to-json', () => { 8 | it('xml-to-json() without namespace test', async () => { 9 | const xmlString = ` 10 | test 11 | 123 12 | \{hugo\} 13 | 14 | 15 | `; 16 | 17 | const xsltString = ` 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | `; 31 | 32 | const expectedOutString = `"test""123""{hugo}"""`; 33 | 34 | const xsltClass = new Xslt(); 35 | const xmlParser = new XmlParser(); 36 | const xml = xmlParser.xmlParse(xmlString); 37 | const xslt = xmlParser.xmlParse(xsltString); 38 | const outXmlString = await xsltClass.xsltProcess( 39 | xml, 40 | xslt 41 | ); 42 | 43 | assert.equal(outXmlString, expectedOutString); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/xml/xml.test.tsx: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import { XmlParser, xmlText } from '../../src/dom'; 4 | 5 | describe('General XML', () => { 6 | it('Self-closing tags disabled', () => { 7 | const xmlString = ``; 8 | 9 | const xmlParser = new XmlParser(); 10 | const outXmlString = xmlText(xmlParser.xmlParse(xmlString), { 11 | cData: true, 12 | selfClosingTags: false, 13 | escape: true, 14 | outputMethod: 'xml' 15 | }); 16 | assert.equal(outXmlString, ''); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/xslt/apply-template.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import { Xslt } from '../../src/xslt'; 4 | import { XmlParser } from '../../src/dom'; 5 | 6 | describe('xsl:apply-template', () => { 7 | /** 8 | * Returning: '

test1

helloreplaced text

' 9 | * Expected is: '

test1

This is replaced text hello

' 10 | */ 11 | it.skip('XSLT apply-template inside text test (https://github.com/DesignLiquido/xslt-processor/issues/108)', async () => { 12 | const xmlString = ` 13 | This is text hello 14 | `; 15 | 16 | const xsltString = ` 17 | 18 | 19 | replaced 20 | 21 | 22 |
23 |

24 |

25 |
26 |
27 |
`; 28 | 29 | const expectedOutString = `

test1

This is replaced text hello

`; 30 | 31 | const xsltClass = new Xslt(); 32 | const xmlParser = new XmlParser(); 33 | const xml = xmlParser.xmlParse(xmlString); 34 | const xslt = xmlParser.xmlParse(xsltString); 35 | 36 | const outXmlString = await xsltClass.xsltProcess(xml, xslt); 37 | 38 | assert.equal(outXmlString, expectedOutString); 39 | // assert.ok(outXmlString); 40 | }); 41 | 42 | it.skip('XSLT template with text on both sides', async () => { 43 | const xmlString = ` 44 | This text lost 45 | `; 46 | 47 | const xsltString = ` 48 | 49 | 50 | XY 51 | 52 | `; 53 | 54 | const expectedOutString = `Xtest1Y`; 55 | 56 | const xsltClass = new Xslt(); 57 | const xmlParser = new XmlParser(); 58 | const xml = xmlParser.xmlParse(xmlString); 59 | const xslt = xmlParser.xmlParse(xsltString); 60 | 61 | const outXmlString = await xsltClass.xsltProcess(xml, xslt); 62 | 63 | assert.equal(outXmlString, expectedOutString); 64 | }); 65 | 66 | it.skip('https://github.com/DesignLiquido/xslt-processor/issues/110', async () => { 67 | const xmlString = ` 68 | 69 |
70 | My Article 71 | 72 | Mr. Foo 73 | Mr. Bar 74 | 75 | This is my article text. 76 |
`; 77 | 78 | const xsltString = ` 79 | 80 | 81 | 82 | 83 | 84 | Article - 85 | Authors: 86 | 87 | 88 | 89 | - 90 | 91 | 92 | `; 93 | 94 | const expectedOutString = `Article - My Article\nAuthors:\n- Mr. Foo\n- Mr. Bar`; 95 | 96 | const xsltClass = new Xslt(); 97 | const xmlParser = new XmlParser(); 98 | const xml = xmlParser.xmlParse(xmlString); 99 | const xslt = xmlParser.xmlParse(xsltString); 100 | 101 | const outXmlString = await xsltClass.xsltProcess(xml, xslt); 102 | 103 | assert.equal(outXmlString, expectedOutString); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /tests/xslt/copy-of.test.tsx: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import { XmlParser } from '../../src/dom'; 4 | import { Xslt } from '../../src/xslt'; 5 | 6 | describe('xsl:copy-of', () => { 7 | it('Trivial', async () => { 8 | const xmlSource = ` 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | `; 19 | 20 | const xsltSource = ` 21 | 22 | 23 | 24 | 25 |

26 | - 27 |

28 |
29 |
`; 30 | 31 | const xsltClass = new Xslt(); 32 | const xmlParser = new XmlParser(); 33 | const xml = xmlParser.xmlParse(xmlSource); 34 | const xslt = xmlParser.xmlParse(xsltSource); 35 | const html = await xsltClass.xsltProcess(xml, xslt); 36 | assert.equal(html, '

Hello-World

'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/xslt/for-each.test.tsx: -------------------------------------------------------------------------------- 1 | import { XmlParser } from '../../src/dom'; 2 | import { Xslt } from '../../src/xslt'; 3 | 4 | import assert from 'assert'; 5 | 6 | describe('xsl:for-each', () => { 7 | const xmlString = ( 8 | ` 9 | A 10 | B 11 | C 12 | ` 13 | ); 14 | 15 | it('handles for-each sort', async () => { 16 | const xsltForEachSort = ( 17 | ` 18 | 19 | 20 | 21 | 22 | 23 | 24 | ` 25 | ); 26 | 27 | const xsltClass = new Xslt(); 28 | const xmlParser = new XmlParser(); 29 | const xml = xmlParser.xmlParse(xmlString); 30 | const xslt = xmlParser.xmlParse(xsltForEachSort); 31 | const html = await xsltClass.xsltProcess(xml, xslt); 32 | assert.equal(html, 'CAB'); 33 | }); 34 | 35 | it('handles for-each sort ascending', async () => { 36 | const xsltForEachSortAscending = ( 37 | ` 38 | 39 | 40 | 41 | 42 | 43 | 44 | ` 45 | ); 46 | 47 | const xsltClass = new Xslt(); 48 | const xmlParser = new XmlParser(); 49 | const xml = xmlParser.xmlParse(xmlString); 50 | const xslt = xmlParser.xmlParse(xsltForEachSortAscending); 51 | const html = await xsltClass.xsltProcess(xml, xslt); 52 | assert.equal(html, 'ABC'); 53 | }); 54 | 55 | it('handles for-each sort descending', async () => { 56 | const xsltForEachSortDescending = ( 57 | ` 58 | 59 | 60 | 61 | 62 | 63 | 64 | ` 65 | ); 66 | 67 | const xsltClass = new Xslt(); 68 | const xmlParser = new XmlParser(); 69 | const xml = xmlParser.xmlParse(xmlString); 70 | const xslt = xmlParser.xmlParse(xsltForEachSortDescending); 71 | const html = await xsltClass.xsltProcess(xml, xslt); 72 | assert.equal(html, 'CBA'); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/xslt/import.test.tsx: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import { XmlParser } from "../../src/dom"; 4 | import { Xslt } from "../../src/xslt"; 5 | 6 | describe('xsl:import', () => { 7 | it('Trivial', async () => { 8 | const xmlSource = ``; 9 | 10 | const xsltSource = ` 11 | 12 | 13 | `; 14 | 15 | const xsltClass = new Xslt(); 16 | const xmlParser = new XmlParser(); 17 | const xml = xmlParser.xmlParse(xmlSource); 18 | const xslt = xmlParser.xmlParse(xsltSource); 19 | const resultingXml = await xsltClass.xsltProcess(xml, xslt); 20 | assert.equal(resultingXml, '</head><body><div id="container"><div id="header"><div id="menu"><ul><li><a href="#" class="active">Home</a></li><li><a href="#">about</a></li></ul></div></div></div></body></html>'); 21 | }); 22 | 23 | it('Not the first child of `<xsl:stylesheet>` or `<xsl:transform>`', async () => { 24 | const xmlSource = `<html></html>`; 25 | 26 | const xsltSource = `<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format"> 27 | <xsl:template match="/"> 28 | Anything 29 | </xsl:template> 30 | <xsl:import href="https://raw.githubusercontent.com/DesignLiquido/xslt-processor/main/examples/head.xsl"/> 31 | </xsl:stylesheet>`; 32 | 33 | const xsltClass = new Xslt(); 34 | const xmlParser = new XmlParser(); 35 | const xml = xmlParser.xmlParse(xmlSource); 36 | const xslt = xmlParser.xmlParse(xsltSource); 37 | assert.rejects(async () => await xsltClass.xsltProcess(xml, xslt)); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/xslt/include.test.tsx: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import { XmlParser } from "../../src/dom"; 4 | import { Xslt } from "../../src/xslt"; 5 | 6 | describe('xsl:include', () => { 7 | it('Trivial', async () => { 8 | const xmlSource = `<html></html>`; 9 | 10 | const xsltSource = `<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format"> 11 | <xsl:output method="html" indent="yes"/> 12 | <xsl:include href="https://raw.githubusercontent.com/DesignLiquido/xslt-processor/main/examples/head.xsl"/> 13 | </xsl:stylesheet>`; 14 | 15 | const xsltClass = new Xslt(); 16 | const xmlParser = new XmlParser(); 17 | const xml = xmlParser.xmlParse(xmlSource); 18 | const xslt = xmlParser.xmlParse(xsltSource); 19 | const resultingXml = await xsltClass.xsltProcess(xml, xslt); 20 | assert.equal(resultingXml, '<html><head><link rel="stylesheet" type="text/css" href="style.css"><title/></head><body><div id="container"><div id="header"><div id="menu"><ul><li><a href="#" class="active">Home</a></li><li><a href="#">about</a></li></ul></div></div></div></body></html>'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/xslt/key.test.tsx: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import { XmlParser } from "../../src/dom"; 4 | import { Xslt } from "../../src/xslt"; 5 | 6 | describe('xsl:key', () => { 7 | it('Trivial', async () => { 8 | const xmlSource = `<persons> 9 | <person name="Tarzan" id="050676"/> 10 | <person name="Donald" id="070754"/> 11 | <person name="Dolly" id="231256"/> 12 | </persons> `; 13 | 14 | const xsltSource = `<?xml version="1.0" encoding="UTF-8"?> 15 | <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> 16 | 17 | <xsl:key name="preg" match="person" use="@id"/> 18 | 19 | <xsl:template match="/"> 20 | <html> 21 | <body> 22 | <xsl:for-each select="key('preg','050676')"> 23 | <p>Name: <xsl:value-of select="@name"/></p> 24 | </xsl:for-each> 25 | </body> 26 | </html> 27 | </xsl:template> 28 | 29 | </xsl:stylesheet> `; 30 | 31 | const xsltClass = new Xslt(); 32 | const xmlParser = new XmlParser(); 33 | const xml = xmlParser.xmlParse(xmlSource); 34 | const xslt = xmlParser.xmlParse(xsltSource); 35 | const resultingXml = await xsltClass.xsltProcess(xml, xslt); 36 | assert.equal(resultingXml, '<html><body><p>Name: Tarzan</p></body></html>'); 37 | }); 38 | }); -------------------------------------------------------------------------------- /tsconfig.debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM"], 4 | "outDir": "dist", 5 | "module": "CommonJS", 6 | "target": "ES2023", 7 | "rootDir": "src", 8 | "allowJs": true, 9 | "sourceMap": true, 10 | "declaration": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "preserveSymlinks": true 16 | }, 17 | "exclude": [ 18 | "babel.config.js", 19 | "jest.config.ts", 20 | "rollup.config.js", 21 | "coverage/**/*", 22 | "demo/**/*", 23 | "dist/**/*", 24 | "interactive-tests/js/**/*", 25 | "node_modules/**/*", 26 | "tests/**/*" 27 | ] 28 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM"], 4 | "outDir": "dist", 5 | "module": "CommonJS", 6 | "target": "ES5", 7 | "rootDir": "src", 8 | "allowJs": true, 9 | "sourceMap": true, 10 | "declaration": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "preserveSymlinks": true 16 | }, 17 | "exclude": [ 18 | "babel.config.js", 19 | "jest.config.ts", 20 | "rollup.config.js", 21 | "coverage/**/*", 22 | "demo/**/*", 23 | "dist/**/*", 24 | "interactive-tests/js/**/*", 25 | "node_modules/**/*", 26 | "tests/**/*" 27 | ] 28 | } -------------------------------------------------------------------------------- /tsconfig.rollup.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "outDir": "dist", 5 | "target": "ES5", 6 | "rootDir": "src", 7 | "allowJs": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "experimentalDecorators": true, 13 | "emitDecoratorMetadata": true 14 | }, 15 | "exclude": [ 16 | "babel.config.js", 17 | "jest.config.ts", 18 | "rollup.config.js", 19 | "coverage/**/*", 20 | "demo/**/*", 21 | "dist/**/*", 22 | "test_src/**/*", 23 | "tests/**/*" 24 | ] 25 | } --------------------------------------------------------------------------------