├── .eslintrc.json ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── dependabot-automerge.yml │ ├── gh-pages.yml │ ├── nodejs-test.yml │ └── publish.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── decode.js ├── escape.js ├── maps ├── entities.json ├── legacy.json └── xml.json ├── package-lock.json ├── package.json ├── readme.md ├── scripts ├── .eslintrc.json ├── benchmark.ts ├── trie │ ├── README.md │ ├── decode-trie.spec.ts │ ├── decode-trie.ts │ ├── encode-trie.spec.ts │ ├── encode-trie.ts │ └── trie.ts ├── write-decode-map.ts └── write-encode-map.ts ├── src ├── decode-codepoint.ts ├── decode.spec.ts ├── decode.ts ├── encode.spec.ts ├── encode.ts ├── escape.spec.ts ├── escape.ts ├── generated │ ├── .eslintrc.json │ ├── decode-data-html.ts │ ├── decode-data-xml.ts │ └── encode-html.ts ├── index.spec.ts └── index.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "prettier", 5 | "plugin:n/recommended", 6 | "plugin:unicorn/recommended" 7 | ], 8 | "env": { 9 | "node": true, 10 | "es6": true 11 | }, 12 | "rules": { 13 | "eqeqeq": [2, "smart"], 14 | "no-caller": 2, 15 | "dot-notation": 2, 16 | "no-var": 2, 17 | "prefer-const": 2, 18 | "prefer-arrow-callback": [2, { "allowNamedFunctions": true }], 19 | "arrow-body-style": [2, "as-needed"], 20 | "object-shorthand": 2, 21 | "prefer-template": 2, 22 | "one-var": [2, "never"], 23 | "prefer-destructuring": [2, { "object": true }], 24 | "capitalized-comments": 2, 25 | "multiline-comment-style": [2, "starred-block"], 26 | "spaced-comment": 2, 27 | "yoda": [2, "never"], 28 | "curly": [2, "multi-line"], 29 | "no-else-return": 2, 30 | 31 | "n/no-unpublished-import": 0, 32 | 33 | "unicorn/no-null": 0, 34 | "unicorn/prefer-code-point": 0, 35 | "unicorn/prefer-string-slice": 0, 36 | "unicorn/prefer-add-event-listener": 0, 37 | "unicorn/prefer-at": 0, 38 | "unicorn/prefer-string-replace-all": 0 39 | }, 40 | "overrides": [ 41 | { 42 | "files": "*.ts", 43 | "extends": [ 44 | "plugin:@typescript-eslint/eslint-recommended", 45 | "plugin:@typescript-eslint/recommended", 46 | "prettier" 47 | ], 48 | "parserOptions": { 49 | "sourceType": "module", 50 | "project": "./tsconfig.json" 51 | }, 52 | "rules": { 53 | "curly": [2, "multi-line"], 54 | 55 | "@typescript-eslint/prefer-for-of": 0, 56 | "@typescript-eslint/member-ordering": 0, 57 | "@typescript-eslint/explicit-function-return-type": 0, 58 | "@typescript-eslint/no-unused-vars": 0, 59 | "@typescript-eslint/no-use-before-define": [ 60 | 2, 61 | { "functions": false } 62 | ], 63 | "@typescript-eslint/consistent-type-definitions": [ 64 | 2, 65 | "interface" 66 | ], 67 | "@typescript-eslint/prefer-function-type": 2, 68 | "@typescript-eslint/no-unnecessary-type-arguments": 2, 69 | "@typescript-eslint/prefer-string-starts-ends-with": 2, 70 | "@typescript-eslint/prefer-readonly": 2, 71 | "@typescript-eslint/prefer-includes": 2, 72 | "@typescript-eslint/no-unnecessary-condition": 2, 73 | "@typescript-eslint/switch-exhaustiveness-check": 2, 74 | "@typescript-eslint/prefer-nullish-coalescing": 2, 75 | "@typescript-eslint/consistent-type-imports": [ 76 | 2, 77 | { "fixStyle": "inline-type-imports" } 78 | ], 79 | "@typescript-eslint/consistent-type-exports": 2, 80 | 81 | "n/no-missing-import": 0, 82 | "n/no-unsupported-features/es-syntax": 0 83 | } 84 | }, 85 | { 86 | "files": "*.spec.ts", 87 | "rules": { 88 | "n/no-unsupported-features/node-builtins": 0 89 | } 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [fb55] 2 | tidelift: "npm/entities" 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | versioning-strategy: increase 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | analyze: 17 | name: Analyze 18 | runs-on: ubuntu-latest 19 | permissions: 20 | actions: read 21 | contents: read 22 | security-events: write 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 27 | 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e 30 | with: 31 | languages: "javascript" 32 | 33 | - name: Perform CodeQL Analysis 34 | uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e 35 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-automerge.yml: -------------------------------------------------------------------------------- 1 | # Based on https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request 2 | name: Dependabot auto-merge 3 | on: pull_request_target 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | dependabot: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.actor == 'dependabot[bot]' }} 13 | steps: 14 | - name: Dependabot metadata 15 | id: metadata 16 | uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b 17 | with: 18 | github-token: "${{ secrets.GITHUB_TOKEN }}" 19 | - name: Enable auto-merge for Dependabot PRs 20 | # Automatically merge semver-patch and semver-minor PRs 21 | if: 22 | "${{ steps.metadata.outputs.update-type == 23 | 'version-update:semver-minor' || 24 | steps.metadata.outputs.update-type == 25 | 'version-update:semver-patch' }}" 26 | run: gh pr merge --auto --squash "$PR_URL" 27 | env: 28 | PR_URL: ${{github.event.pull_request.html_url}} 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | env: 8 | FORCE_COLOR: 2 9 | NODE: 16 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | pages: 16 | permissions: 17 | contents: write # for peaceiris/actions-gh-pages to push pages branch 18 | name: Deploy to GitHub Pages 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 22 | - uses: actions/setup-node@d86ebcd40b3cb50b156bfa44dd277faf38282d12 23 | with: 24 | node-version: "${{ env.NODE }}" 25 | cache: "npm" 26 | - run: npm ci 27 | - name: Build docs 28 | run: npm run build:docs 29 | - name: Deploy 30 | uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: docs 34 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "dependabot/**" 7 | pull_request: 8 | 9 | env: 10 | CI: true 11 | FORCE_COLOR: 2 12 | NODE_COV: lts/* # The Node.js version to run coveralls on 13 | 14 | permissions: 15 | contents: read # to fetch code (actions/checkout) 16 | 17 | jobs: 18 | lint: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 22 | - uses: actions/setup-node@v4.0.1 23 | with: 24 | node-version: lts/* 25 | cache: npm 26 | - run: npm ci 27 | - run: npm run lint 28 | 29 | test: 30 | permissions: 31 | contents: read # to fetch code (actions/checkout) 32 | checks: write # to create new checks (coverallsapp/github-action) 33 | 34 | name: Node ${{ matrix.node }} 35 | runs-on: ubuntu-latest 36 | 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | node: 41 | - 18 42 | - 20 43 | - 22 44 | - lts/* 45 | 46 | steps: 47 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 48 | - name: Use Node.js ${{ matrix.node }} 49 | uses: actions/setup-node@v4.0.1 50 | with: 51 | node-version: ${{ matrix.node }} 52 | cache: npm 53 | - run: npm ci 54 | - run: npm run build --if-present 55 | 56 | - name: Run tests 57 | run: npm run test:vi 58 | if: matrix.node != env.NODE_COV 59 | 60 | - name: Run tests with coverage 61 | run: npm run test:vi -- --coverage 62 | if: matrix.node == env.NODE_COV 63 | 64 | - name: Run Coveralls 65 | uses: coverallsapp/github-action@v2.3.6 66 | if: matrix.node == env.NODE_COV 67 | continue-on-error: true 68 | with: 69 | github-token: "${{ secrets.GITHUB_TOKEN }}" 70 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | 10 | permissions: 11 | contents: write 12 | id-token: write 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: lts/* 20 | registry-url: https://registry.npmjs.org 21 | cache: npm 22 | 23 | - name: Install dependencies 24 | run: npm ci 25 | 26 | - name: Update package version to latest tag 27 | run: | 28 | # Set up git user 29 | git config user.name "github-actions[bot]" 30 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 31 | npm version --allow-same-version from-git 32 | git push --follow-tags origin HEAD:main 33 | 34 | - name: Create jsr.json based on package.json 35 | run: | 36 | node -e ' 37 | const p = require("./package.json"); 38 | const jsrJson = { 39 | name: `@cheerio/${p.name}`, 40 | version: p.version, 41 | ...p.tshy, 42 | }; 43 | require("fs").writeFileSync("./jsr.json", JSON.stringify(jsrJson, null, 2)); 44 | ' 45 | 46 | - name: Publish package to JSR 47 | run: npx jsr publish 48 | 49 | - name: Publish package to NPM 50 | run: npm publish --provenance --access public 51 | env: 52 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | .tshy/ 5 | docs/ 6 | jsr.json 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | maps/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Felix Böhm 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, 11 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /decode.js: -------------------------------------------------------------------------------- 1 | // Make exports work in Node < 12 2 | // eslint-disable-next-line no-undef, unicorn/prefer-module 3 | module.exports = require("./dist/commonjs/decode.js"); 4 | -------------------------------------------------------------------------------- /escape.js: -------------------------------------------------------------------------------- 1 | // Make exports work in Node < 12 2 | // eslint-disable-next-line no-undef, unicorn/prefer-module 3 | module.exports = require("./dist/commonjs/escape.js"); 4 | -------------------------------------------------------------------------------- /maps/entities.json: -------------------------------------------------------------------------------- 1 | {"Aacute":"Á","aacute":"á","Abreve":"Ă","abreve":"ă","ac":"∾","acd":"∿","acE":"∾̳","Acirc":"Â","acirc":"â","acute":"´","Acy":"А","acy":"а","AElig":"Æ","aelig":"æ","af":"⁡","Afr":"𝔄","afr":"𝔞","Agrave":"À","agrave":"à","alefsym":"ℵ","aleph":"ℵ","Alpha":"Α","alpha":"α","Amacr":"Ā","amacr":"ā","amalg":"⨿","amp":"&","AMP":"&","andand":"⩕","And":"⩓","and":"∧","andd":"⩜","andslope":"⩘","andv":"⩚","ang":"∠","ange":"⦤","angle":"∠","angmsdaa":"⦨","angmsdab":"⦩","angmsdac":"⦪","angmsdad":"⦫","angmsdae":"⦬","angmsdaf":"⦭","angmsdag":"⦮","angmsdah":"⦯","angmsd":"∡","angrt":"∟","angrtvb":"⊾","angrtvbd":"⦝","angsph":"∢","angst":"Å","angzarr":"⍼","Aogon":"Ą","aogon":"ą","Aopf":"𝔸","aopf":"𝕒","apacir":"⩯","ap":"≈","apE":"⩰","ape":"≊","apid":"≋","apos":"'","ApplyFunction":"⁡","approx":"≈","approxeq":"≊","Aring":"Å","aring":"å","Ascr":"𝒜","ascr":"𝒶","Assign":"≔","ast":"*","asymp":"≈","asympeq":"≍","Atilde":"Ã","atilde":"ã","Auml":"Ä","auml":"ä","awconint":"∳","awint":"⨑","backcong":"≌","backepsilon":"϶","backprime":"‵","backsim":"∽","backsimeq":"⋍","Backslash":"∖","Barv":"⫧","barvee":"⊽","barwed":"⌅","Barwed":"⌆","barwedge":"⌅","bbrk":"⎵","bbrktbrk":"⎶","bcong":"≌","Bcy":"Б","bcy":"б","bdquo":"„","becaus":"∵","because":"∵","Because":"∵","bemptyv":"⦰","bepsi":"϶","bernou":"ℬ","Bernoullis":"ℬ","Beta":"Β","beta":"β","beth":"ℶ","between":"≬","Bfr":"𝔅","bfr":"𝔟","bigcap":"⋂","bigcirc":"◯","bigcup":"⋃","bigodot":"⨀","bigoplus":"⨁","bigotimes":"⨂","bigsqcup":"⨆","bigstar":"★","bigtriangledown":"▽","bigtriangleup":"△","biguplus":"⨄","bigvee":"⋁","bigwedge":"⋀","bkarow":"⤍","blacklozenge":"⧫","blacksquare":"▪","blacktriangle":"▴","blacktriangledown":"▾","blacktriangleleft":"◂","blacktriangleright":"▸","blank":"␣","blk12":"▒","blk14":"░","blk34":"▓","block":"█","bne":"=⃥","bnequiv":"≡⃥","bNot":"⫭","bnot":"⌐","Bopf":"𝔹","bopf":"𝕓","bot":"⊥","bottom":"⊥","bowtie":"⋈","boxbox":"⧉","boxdl":"┐","boxdL":"╕","boxDl":"╖","boxDL":"╗","boxdr":"┌","boxdR":"╒","boxDr":"╓","boxDR":"╔","boxh":"─","boxH":"═","boxhd":"┬","boxHd":"╤","boxhD":"╥","boxHD":"╦","boxhu":"┴","boxHu":"╧","boxhU":"╨","boxHU":"╩","boxminus":"⊟","boxplus":"⊞","boxtimes":"⊠","boxul":"┘","boxuL":"╛","boxUl":"╜","boxUL":"╝","boxur":"└","boxuR":"╘","boxUr":"╙","boxUR":"╚","boxv":"│","boxV":"║","boxvh":"┼","boxvH":"╪","boxVh":"╫","boxVH":"╬","boxvl":"┤","boxvL":"╡","boxVl":"╢","boxVL":"╣","boxvr":"├","boxvR":"╞","boxVr":"╟","boxVR":"╠","bprime":"‵","breve":"˘","Breve":"˘","brvbar":"¦","bscr":"𝒷","Bscr":"ℬ","bsemi":"⁏","bsim":"∽","bsime":"⋍","bsolb":"⧅","bsol":"\\","bsolhsub":"⟈","bull":"•","bullet":"•","bump":"≎","bumpE":"⪮","bumpe":"≏","Bumpeq":"≎","bumpeq":"≏","Cacute":"Ć","cacute":"ć","capand":"⩄","capbrcup":"⩉","capcap":"⩋","cap":"∩","Cap":"⋒","capcup":"⩇","capdot":"⩀","CapitalDifferentialD":"ⅅ","caps":"∩︀","caret":"⁁","caron":"ˇ","Cayleys":"ℭ","ccaps":"⩍","Ccaron":"Č","ccaron":"č","Ccedil":"Ç","ccedil":"ç","Ccirc":"Ĉ","ccirc":"ĉ","Cconint":"∰","ccups":"⩌","ccupssm":"⩐","Cdot":"Ċ","cdot":"ċ","cedil":"¸","Cedilla":"¸","cemptyv":"⦲","cent":"¢","centerdot":"·","CenterDot":"·","cfr":"𝔠","Cfr":"ℭ","CHcy":"Ч","chcy":"ч","check":"✓","checkmark":"✓","Chi":"Χ","chi":"χ","circ":"ˆ","circeq":"≗","circlearrowleft":"↺","circlearrowright":"↻","circledast":"⊛","circledcirc":"⊚","circleddash":"⊝","CircleDot":"⊙","circledR":"®","circledS":"Ⓢ","CircleMinus":"⊖","CirclePlus":"⊕","CircleTimes":"⊗","cir":"○","cirE":"⧃","cire":"≗","cirfnint":"⨐","cirmid":"⫯","cirscir":"⧂","ClockwiseContourIntegral":"∲","CloseCurlyDoubleQuote":"”","CloseCurlyQuote":"’","clubs":"♣","clubsuit":"♣","colon":":","Colon":"∷","Colone":"⩴","colone":"≔","coloneq":"≔","comma":",","commat":"@","comp":"∁","compfn":"∘","complement":"∁","complexes":"ℂ","cong":"≅","congdot":"⩭","Congruent":"≡","conint":"∮","Conint":"∯","ContourIntegral":"∮","copf":"𝕔","Copf":"ℂ","coprod":"∐","Coproduct":"∐","copy":"©","COPY":"©","copysr":"℗","CounterClockwiseContourIntegral":"∳","crarr":"↵","cross":"✗","Cross":"⨯","Cscr":"𝒞","cscr":"𝒸","csub":"⫏","csube":"⫑","csup":"⫐","csupe":"⫒","ctdot":"⋯","cudarrl":"⤸","cudarrr":"⤵","cuepr":"⋞","cuesc":"⋟","cularr":"↶","cularrp":"⤽","cupbrcap":"⩈","cupcap":"⩆","CupCap":"≍","cup":"∪","Cup":"⋓","cupcup":"⩊","cupdot":"⊍","cupor":"⩅","cups":"∪︀","curarr":"↷","curarrm":"⤼","curlyeqprec":"⋞","curlyeqsucc":"⋟","curlyvee":"⋎","curlywedge":"⋏","curren":"¤","curvearrowleft":"↶","curvearrowright":"↷","cuvee":"⋎","cuwed":"⋏","cwconint":"∲","cwint":"∱","cylcty":"⌭","dagger":"†","Dagger":"‡","daleth":"ℸ","darr":"↓","Darr":"↡","dArr":"⇓","dash":"‐","Dashv":"⫤","dashv":"⊣","dbkarow":"⤏","dblac":"˝","Dcaron":"Ď","dcaron":"ď","Dcy":"Д","dcy":"д","ddagger":"‡","ddarr":"⇊","DD":"ⅅ","dd":"ⅆ","DDotrahd":"⤑","ddotseq":"⩷","deg":"°","Del":"∇","Delta":"Δ","delta":"δ","demptyv":"⦱","dfisht":"⥿","Dfr":"𝔇","dfr":"𝔡","dHar":"⥥","dharl":"⇃","dharr":"⇂","DiacriticalAcute":"´","DiacriticalDot":"˙","DiacriticalDoubleAcute":"˝","DiacriticalGrave":"`","DiacriticalTilde":"˜","diam":"⋄","diamond":"⋄","Diamond":"⋄","diamondsuit":"♦","diams":"♦","die":"¨","DifferentialD":"ⅆ","digamma":"ϝ","disin":"⋲","div":"÷","divide":"÷","divideontimes":"⋇","divonx":"⋇","DJcy":"Ђ","djcy":"ђ","dlcorn":"⌞","dlcrop":"⌍","dollar":"$","Dopf":"𝔻","dopf":"𝕕","Dot":"¨","dot":"˙","DotDot":"⃜","doteq":"≐","doteqdot":"≑","DotEqual":"≐","dotminus":"∸","dotplus":"∔","dotsquare":"⊡","doublebarwedge":"⌆","DoubleContourIntegral":"∯","DoubleDot":"¨","DoubleDownArrow":"⇓","DoubleLeftArrow":"⇐","DoubleLeftRightArrow":"⇔","DoubleLeftTee":"⫤","DoubleLongLeftArrow":"⟸","DoubleLongLeftRightArrow":"⟺","DoubleLongRightArrow":"⟹","DoubleRightArrow":"⇒","DoubleRightTee":"⊨","DoubleUpArrow":"⇑","DoubleUpDownArrow":"⇕","DoubleVerticalBar":"∥","DownArrowBar":"⤓","downarrow":"↓","DownArrow":"↓","Downarrow":"⇓","DownArrowUpArrow":"⇵","DownBreve":"̑","downdownarrows":"⇊","downharpoonleft":"⇃","downharpoonright":"⇂","DownLeftRightVector":"⥐","DownLeftTeeVector":"⥞","DownLeftVectorBar":"⥖","DownLeftVector":"↽","DownRightTeeVector":"⥟","DownRightVectorBar":"⥗","DownRightVector":"⇁","DownTeeArrow":"↧","DownTee":"⊤","drbkarow":"⤐","drcorn":"⌟","drcrop":"⌌","Dscr":"𝒟","dscr":"𝒹","DScy":"Ѕ","dscy":"ѕ","dsol":"⧶","Dstrok":"Đ","dstrok":"đ","dtdot":"⋱","dtri":"▿","dtrif":"▾","duarr":"⇵","duhar":"⥯","dwangle":"⦦","DZcy":"Џ","dzcy":"џ","dzigrarr":"⟿","Eacute":"É","eacute":"é","easter":"⩮","Ecaron":"Ě","ecaron":"ě","Ecirc":"Ê","ecirc":"ê","ecir":"≖","ecolon":"≕","Ecy":"Э","ecy":"э","eDDot":"⩷","Edot":"Ė","edot":"ė","eDot":"≑","ee":"ⅇ","efDot":"≒","Efr":"𝔈","efr":"𝔢","eg":"⪚","Egrave":"È","egrave":"è","egs":"⪖","egsdot":"⪘","el":"⪙","Element":"∈","elinters":"⏧","ell":"ℓ","els":"⪕","elsdot":"⪗","Emacr":"Ē","emacr":"ē","empty":"∅","emptyset":"∅","EmptySmallSquare":"◻","emptyv":"∅","EmptyVerySmallSquare":"▫","emsp13":" ","emsp14":" ","emsp":" ","ENG":"Ŋ","eng":"ŋ","ensp":" ","Eogon":"Ę","eogon":"ę","Eopf":"𝔼","eopf":"𝕖","epar":"⋕","eparsl":"⧣","eplus":"⩱","epsi":"ε","Epsilon":"Ε","epsilon":"ε","epsiv":"ϵ","eqcirc":"≖","eqcolon":"≕","eqsim":"≂","eqslantgtr":"⪖","eqslantless":"⪕","Equal":"⩵","equals":"=","EqualTilde":"≂","equest":"≟","Equilibrium":"⇌","equiv":"≡","equivDD":"⩸","eqvparsl":"⧥","erarr":"⥱","erDot":"≓","escr":"ℯ","Escr":"ℰ","esdot":"≐","Esim":"⩳","esim":"≂","Eta":"Η","eta":"η","ETH":"Ð","eth":"ð","Euml":"Ë","euml":"ë","euro":"€","excl":"!","exist":"∃","Exists":"∃","expectation":"ℰ","exponentiale":"ⅇ","ExponentialE":"ⅇ","fallingdotseq":"≒","Fcy":"Ф","fcy":"ф","female":"♀","ffilig":"ffi","fflig":"ff","ffllig":"ffl","Ffr":"𝔉","ffr":"𝔣","filig":"fi","FilledSmallSquare":"◼","FilledVerySmallSquare":"▪","fjlig":"fj","flat":"♭","fllig":"fl","fltns":"▱","fnof":"ƒ","Fopf":"𝔽","fopf":"𝕗","forall":"∀","ForAll":"∀","fork":"⋔","forkv":"⫙","Fouriertrf":"ℱ","fpartint":"⨍","frac12":"½","frac13":"⅓","frac14":"¼","frac15":"⅕","frac16":"⅙","frac18":"⅛","frac23":"⅔","frac25":"⅖","frac34":"¾","frac35":"⅗","frac38":"⅜","frac45":"⅘","frac56":"⅚","frac58":"⅝","frac78":"⅞","frasl":"⁄","frown":"⌢","fscr":"𝒻","Fscr":"ℱ","gacute":"ǵ","Gamma":"Γ","gamma":"γ","Gammad":"Ϝ","gammad":"ϝ","gap":"⪆","Gbreve":"Ğ","gbreve":"ğ","Gcedil":"Ģ","Gcirc":"Ĝ","gcirc":"ĝ","Gcy":"Г","gcy":"г","Gdot":"Ġ","gdot":"ġ","ge":"≥","gE":"≧","gEl":"⪌","gel":"⋛","geq":"≥","geqq":"≧","geqslant":"⩾","gescc":"⪩","ges":"⩾","gesdot":"⪀","gesdoto":"⪂","gesdotol":"⪄","gesl":"⋛︀","gesles":"⪔","Gfr":"𝔊","gfr":"𝔤","gg":"≫","Gg":"⋙","ggg":"⋙","gimel":"ℷ","GJcy":"Ѓ","gjcy":"ѓ","gla":"⪥","gl":"≷","glE":"⪒","glj":"⪤","gnap":"⪊","gnapprox":"⪊","gne":"⪈","gnE":"≩","gneq":"⪈","gneqq":"≩","gnsim":"⋧","Gopf":"𝔾","gopf":"𝕘","grave":"`","GreaterEqual":"≥","GreaterEqualLess":"⋛","GreaterFullEqual":"≧","GreaterGreater":"⪢","GreaterLess":"≷","GreaterSlantEqual":"⩾","GreaterTilde":"≳","Gscr":"𝒢","gscr":"ℊ","gsim":"≳","gsime":"⪎","gsiml":"⪐","gtcc":"⪧","gtcir":"⩺","gt":">","GT":">","Gt":"≫","gtdot":"⋗","gtlPar":"⦕","gtquest":"⩼","gtrapprox":"⪆","gtrarr":"⥸","gtrdot":"⋗","gtreqless":"⋛","gtreqqless":"⪌","gtrless":"≷","gtrsim":"≳","gvertneqq":"≩︀","gvnE":"≩︀","Hacek":"ˇ","hairsp":" ","half":"½","hamilt":"ℋ","HARDcy":"Ъ","hardcy":"ъ","harrcir":"⥈","harr":"↔","hArr":"⇔","harrw":"↭","Hat":"^","hbar":"ℏ","Hcirc":"Ĥ","hcirc":"ĥ","hearts":"♥","heartsuit":"♥","hellip":"…","hercon":"⊹","hfr":"𝔥","Hfr":"ℌ","HilbertSpace":"ℋ","hksearow":"⤥","hkswarow":"⤦","hoarr":"⇿","homtht":"∻","hookleftarrow":"↩","hookrightarrow":"↪","hopf":"𝕙","Hopf":"ℍ","horbar":"―","HorizontalLine":"─","hscr":"𝒽","Hscr":"ℋ","hslash":"ℏ","Hstrok":"Ħ","hstrok":"ħ","HumpDownHump":"≎","HumpEqual":"≏","hybull":"⁃","hyphen":"‐","Iacute":"Í","iacute":"í","ic":"⁣","Icirc":"Î","icirc":"î","Icy":"И","icy":"и","Idot":"İ","IEcy":"Е","iecy":"е","iexcl":"¡","iff":"⇔","ifr":"𝔦","Ifr":"ℑ","Igrave":"Ì","igrave":"ì","ii":"ⅈ","iiiint":"⨌","iiint":"∭","iinfin":"⧜","iiota":"℩","IJlig":"IJ","ijlig":"ij","Imacr":"Ī","imacr":"ī","image":"ℑ","ImaginaryI":"ⅈ","imagline":"ℐ","imagpart":"ℑ","imath":"ı","Im":"ℑ","imof":"⊷","imped":"Ƶ","Implies":"⇒","incare":"℅","in":"∈","infin":"∞","infintie":"⧝","inodot":"ı","intcal":"⊺","int":"∫","Int":"∬","integers":"ℤ","Integral":"∫","intercal":"⊺","Intersection":"⋂","intlarhk":"⨗","intprod":"⨼","InvisibleComma":"⁣","InvisibleTimes":"⁢","IOcy":"Ё","iocy":"ё","Iogon":"Į","iogon":"į","Iopf":"𝕀","iopf":"𝕚","Iota":"Ι","iota":"ι","iprod":"⨼","iquest":"¿","iscr":"𝒾","Iscr":"ℐ","isin":"∈","isindot":"⋵","isinE":"⋹","isins":"⋴","isinsv":"⋳","isinv":"∈","it":"⁢","Itilde":"Ĩ","itilde":"ĩ","Iukcy":"І","iukcy":"і","Iuml":"Ï","iuml":"ï","Jcirc":"Ĵ","jcirc":"ĵ","Jcy":"Й","jcy":"й","Jfr":"𝔍","jfr":"𝔧","jmath":"ȷ","Jopf":"𝕁","jopf":"𝕛","Jscr":"𝒥","jscr":"𝒿","Jsercy":"Ј","jsercy":"ј","Jukcy":"Є","jukcy":"є","Kappa":"Κ","kappa":"κ","kappav":"ϰ","Kcedil":"Ķ","kcedil":"ķ","Kcy":"К","kcy":"к","Kfr":"𝔎","kfr":"𝔨","kgreen":"ĸ","KHcy":"Х","khcy":"х","KJcy":"Ќ","kjcy":"ќ","Kopf":"𝕂","kopf":"𝕜","Kscr":"𝒦","kscr":"𝓀","lAarr":"⇚","Lacute":"Ĺ","lacute":"ĺ","laemptyv":"⦴","lagran":"ℒ","Lambda":"Λ","lambda":"λ","lang":"⟨","Lang":"⟪","langd":"⦑","langle":"⟨","lap":"⪅","Laplacetrf":"ℒ","laquo":"«","larrb":"⇤","larrbfs":"⤟","larr":"←","Larr":"↞","lArr":"⇐","larrfs":"⤝","larrhk":"↩","larrlp":"↫","larrpl":"⤹","larrsim":"⥳","larrtl":"↢","latail":"⤙","lAtail":"⤛","lat":"⪫","late":"⪭","lates":"⪭︀","lbarr":"⤌","lBarr":"⤎","lbbrk":"❲","lbrace":"{","lbrack":"[","lbrke":"⦋","lbrksld":"⦏","lbrkslu":"⦍","Lcaron":"Ľ","lcaron":"ľ","Lcedil":"Ļ","lcedil":"ļ","lceil":"⌈","lcub":"{","Lcy":"Л","lcy":"л","ldca":"⤶","ldquo":"“","ldquor":"„","ldrdhar":"⥧","ldrushar":"⥋","ldsh":"↲","le":"≤","lE":"≦","LeftAngleBracket":"⟨","LeftArrowBar":"⇤","leftarrow":"←","LeftArrow":"←","Leftarrow":"⇐","LeftArrowRightArrow":"⇆","leftarrowtail":"↢","LeftCeiling":"⌈","LeftDoubleBracket":"⟦","LeftDownTeeVector":"⥡","LeftDownVectorBar":"⥙","LeftDownVector":"⇃","LeftFloor":"⌊","leftharpoondown":"↽","leftharpoonup":"↼","leftleftarrows":"⇇","leftrightarrow":"↔","LeftRightArrow":"↔","Leftrightarrow":"⇔","leftrightarrows":"⇆","leftrightharpoons":"⇋","leftrightsquigarrow":"↭","LeftRightVector":"⥎","LeftTeeArrow":"↤","LeftTee":"⊣","LeftTeeVector":"⥚","leftthreetimes":"⋋","LeftTriangleBar":"⧏","LeftTriangle":"⊲","LeftTriangleEqual":"⊴","LeftUpDownVector":"⥑","LeftUpTeeVector":"⥠","LeftUpVectorBar":"⥘","LeftUpVector":"↿","LeftVectorBar":"⥒","LeftVector":"↼","lEg":"⪋","leg":"⋚","leq":"≤","leqq":"≦","leqslant":"⩽","lescc":"⪨","les":"⩽","lesdot":"⩿","lesdoto":"⪁","lesdotor":"⪃","lesg":"⋚︀","lesges":"⪓","lessapprox":"⪅","lessdot":"⋖","lesseqgtr":"⋚","lesseqqgtr":"⪋","LessEqualGreater":"⋚","LessFullEqual":"≦","LessGreater":"≶","lessgtr":"≶","LessLess":"⪡","lesssim":"≲","LessSlantEqual":"⩽","LessTilde":"≲","lfisht":"⥼","lfloor":"⌊","Lfr":"𝔏","lfr":"𝔩","lg":"≶","lgE":"⪑","lHar":"⥢","lhard":"↽","lharu":"↼","lharul":"⥪","lhblk":"▄","LJcy":"Љ","ljcy":"љ","llarr":"⇇","ll":"≪","Ll":"⋘","llcorner":"⌞","Lleftarrow":"⇚","llhard":"⥫","lltri":"◺","Lmidot":"Ŀ","lmidot":"ŀ","lmoustache":"⎰","lmoust":"⎰","lnap":"⪉","lnapprox":"⪉","lne":"⪇","lnE":"≨","lneq":"⪇","lneqq":"≨","lnsim":"⋦","loang":"⟬","loarr":"⇽","lobrk":"⟦","longleftarrow":"⟵","LongLeftArrow":"⟵","Longleftarrow":"⟸","longleftrightarrow":"⟷","LongLeftRightArrow":"⟷","Longleftrightarrow":"⟺","longmapsto":"⟼","longrightarrow":"⟶","LongRightArrow":"⟶","Longrightarrow":"⟹","looparrowleft":"↫","looparrowright":"↬","lopar":"⦅","Lopf":"𝕃","lopf":"𝕝","loplus":"⨭","lotimes":"⨴","lowast":"∗","lowbar":"_","LowerLeftArrow":"↙","LowerRightArrow":"↘","loz":"◊","lozenge":"◊","lozf":"⧫","lpar":"(","lparlt":"⦓","lrarr":"⇆","lrcorner":"⌟","lrhar":"⇋","lrhard":"⥭","lrm":"‎","lrtri":"⊿","lsaquo":"‹","lscr":"𝓁","Lscr":"ℒ","lsh":"↰","Lsh":"↰","lsim":"≲","lsime":"⪍","lsimg":"⪏","lsqb":"[","lsquo":"‘","lsquor":"‚","Lstrok":"Ł","lstrok":"ł","ltcc":"⪦","ltcir":"⩹","lt":"<","LT":"<","Lt":"≪","ltdot":"⋖","lthree":"⋋","ltimes":"⋉","ltlarr":"⥶","ltquest":"⩻","ltri":"◃","ltrie":"⊴","ltrif":"◂","ltrPar":"⦖","lurdshar":"⥊","luruhar":"⥦","lvertneqq":"≨︀","lvnE":"≨︀","macr":"¯","male":"♂","malt":"✠","maltese":"✠","Map":"⤅","map":"↦","mapsto":"↦","mapstodown":"↧","mapstoleft":"↤","mapstoup":"↥","marker":"▮","mcomma":"⨩","Mcy":"М","mcy":"м","mdash":"—","mDDot":"∺","measuredangle":"∡","MediumSpace":" ","Mellintrf":"ℳ","Mfr":"𝔐","mfr":"𝔪","mho":"℧","micro":"µ","midast":"*","midcir":"⫰","mid":"∣","middot":"·","minusb":"⊟","minus":"−","minusd":"∸","minusdu":"⨪","MinusPlus":"∓","mlcp":"⫛","mldr":"…","mnplus":"∓","models":"⊧","Mopf":"𝕄","mopf":"𝕞","mp":"∓","mscr":"𝓂","Mscr":"ℳ","mstpos":"∾","Mu":"Μ","mu":"μ","multimap":"⊸","mumap":"⊸","nabla":"∇","Nacute":"Ń","nacute":"ń","nang":"∠⃒","nap":"≉","napE":"⩰̸","napid":"≋̸","napos":"ʼn","napprox":"≉","natural":"♮","naturals":"ℕ","natur":"♮","nbsp":" ","nbump":"≎̸","nbumpe":"≏̸","ncap":"⩃","Ncaron":"Ň","ncaron":"ň","Ncedil":"Ņ","ncedil":"ņ","ncong":"≇","ncongdot":"⩭̸","ncup":"⩂","Ncy":"Н","ncy":"н","ndash":"–","nearhk":"⤤","nearr":"↗","neArr":"⇗","nearrow":"↗","ne":"≠","nedot":"≐̸","NegativeMediumSpace":"​","NegativeThickSpace":"​","NegativeThinSpace":"​","NegativeVeryThinSpace":"​","nequiv":"≢","nesear":"⤨","nesim":"≂̸","NestedGreaterGreater":"≫","NestedLessLess":"≪","NewLine":"\n","nexist":"∄","nexists":"∄","Nfr":"𝔑","nfr":"𝔫","ngE":"≧̸","nge":"≱","ngeq":"≱","ngeqq":"≧̸","ngeqslant":"⩾̸","nges":"⩾̸","nGg":"⋙̸","ngsim":"≵","nGt":"≫⃒","ngt":"≯","ngtr":"≯","nGtv":"≫̸","nharr":"↮","nhArr":"⇎","nhpar":"⫲","ni":"∋","nis":"⋼","nisd":"⋺","niv":"∋","NJcy":"Њ","njcy":"њ","nlarr":"↚","nlArr":"⇍","nldr":"‥","nlE":"≦̸","nle":"≰","nleftarrow":"↚","nLeftarrow":"⇍","nleftrightarrow":"↮","nLeftrightarrow":"⇎","nleq":"≰","nleqq":"≦̸","nleqslant":"⩽̸","nles":"⩽̸","nless":"≮","nLl":"⋘̸","nlsim":"≴","nLt":"≪⃒","nlt":"≮","nltri":"⋪","nltrie":"⋬","nLtv":"≪̸","nmid":"∤","NoBreak":"⁠","NonBreakingSpace":" ","nopf":"𝕟","Nopf":"ℕ","Not":"⫬","not":"¬","NotCongruent":"≢","NotCupCap":"≭","NotDoubleVerticalBar":"∦","NotElement":"∉","NotEqual":"≠","NotEqualTilde":"≂̸","NotExists":"∄","NotGreater":"≯","NotGreaterEqual":"≱","NotGreaterFullEqual":"≧̸","NotGreaterGreater":"≫̸","NotGreaterLess":"≹","NotGreaterSlantEqual":"⩾̸","NotGreaterTilde":"≵","NotHumpDownHump":"≎̸","NotHumpEqual":"≏̸","notin":"∉","notindot":"⋵̸","notinE":"⋹̸","notinva":"∉","notinvb":"⋷","notinvc":"⋶","NotLeftTriangleBar":"⧏̸","NotLeftTriangle":"⋪","NotLeftTriangleEqual":"⋬","NotLess":"≮","NotLessEqual":"≰","NotLessGreater":"≸","NotLessLess":"≪̸","NotLessSlantEqual":"⩽̸","NotLessTilde":"≴","NotNestedGreaterGreater":"⪢̸","NotNestedLessLess":"⪡̸","notni":"∌","notniva":"∌","notnivb":"⋾","notnivc":"⋽","NotPrecedes":"⊀","NotPrecedesEqual":"⪯̸","NotPrecedesSlantEqual":"⋠","NotReverseElement":"∌","NotRightTriangleBar":"⧐̸","NotRightTriangle":"⋫","NotRightTriangleEqual":"⋭","NotSquareSubset":"⊏̸","NotSquareSubsetEqual":"⋢","NotSquareSuperset":"⊐̸","NotSquareSupersetEqual":"⋣","NotSubset":"⊂⃒","NotSubsetEqual":"⊈","NotSucceeds":"⊁","NotSucceedsEqual":"⪰̸","NotSucceedsSlantEqual":"⋡","NotSucceedsTilde":"≿̸","NotSuperset":"⊃⃒","NotSupersetEqual":"⊉","NotTilde":"≁","NotTildeEqual":"≄","NotTildeFullEqual":"≇","NotTildeTilde":"≉","NotVerticalBar":"∤","nparallel":"∦","npar":"∦","nparsl":"⫽⃥","npart":"∂̸","npolint":"⨔","npr":"⊀","nprcue":"⋠","nprec":"⊀","npreceq":"⪯̸","npre":"⪯̸","nrarrc":"⤳̸","nrarr":"↛","nrArr":"⇏","nrarrw":"↝̸","nrightarrow":"↛","nRightarrow":"⇏","nrtri":"⋫","nrtrie":"⋭","nsc":"⊁","nsccue":"⋡","nsce":"⪰̸","Nscr":"𝒩","nscr":"𝓃","nshortmid":"∤","nshortparallel":"∦","nsim":"≁","nsime":"≄","nsimeq":"≄","nsmid":"∤","nspar":"∦","nsqsube":"⋢","nsqsupe":"⋣","nsub":"⊄","nsubE":"⫅̸","nsube":"⊈","nsubset":"⊂⃒","nsubseteq":"⊈","nsubseteqq":"⫅̸","nsucc":"⊁","nsucceq":"⪰̸","nsup":"⊅","nsupE":"⫆̸","nsupe":"⊉","nsupset":"⊃⃒","nsupseteq":"⊉","nsupseteqq":"⫆̸","ntgl":"≹","Ntilde":"Ñ","ntilde":"ñ","ntlg":"≸","ntriangleleft":"⋪","ntrianglelefteq":"⋬","ntriangleright":"⋫","ntrianglerighteq":"⋭","Nu":"Ν","nu":"ν","num":"#","numero":"№","numsp":" ","nvap":"≍⃒","nvdash":"⊬","nvDash":"⊭","nVdash":"⊮","nVDash":"⊯","nvge":"≥⃒","nvgt":">⃒","nvHarr":"⤄","nvinfin":"⧞","nvlArr":"⤂","nvle":"≤⃒","nvlt":"<⃒","nvltrie":"⊴⃒","nvrArr":"⤃","nvrtrie":"⊵⃒","nvsim":"∼⃒","nwarhk":"⤣","nwarr":"↖","nwArr":"⇖","nwarrow":"↖","nwnear":"⤧","Oacute":"Ó","oacute":"ó","oast":"⊛","Ocirc":"Ô","ocirc":"ô","ocir":"⊚","Ocy":"О","ocy":"о","odash":"⊝","Odblac":"Ő","odblac":"ő","odiv":"⨸","odot":"⊙","odsold":"⦼","OElig":"Œ","oelig":"œ","ofcir":"⦿","Ofr":"𝔒","ofr":"𝔬","ogon":"˛","Ograve":"Ò","ograve":"ò","ogt":"⧁","ohbar":"⦵","ohm":"Ω","oint":"∮","olarr":"↺","olcir":"⦾","olcross":"⦻","oline":"‾","olt":"⧀","Omacr":"Ō","omacr":"ō","Omega":"Ω","omega":"ω","Omicron":"Ο","omicron":"ο","omid":"⦶","ominus":"⊖","Oopf":"𝕆","oopf":"𝕠","opar":"⦷","OpenCurlyDoubleQuote":"“","OpenCurlyQuote":"‘","operp":"⦹","oplus":"⊕","orarr":"↻","Or":"⩔","or":"∨","ord":"⩝","order":"ℴ","orderof":"ℴ","ordf":"ª","ordm":"º","origof":"⊶","oror":"⩖","orslope":"⩗","orv":"⩛","oS":"Ⓢ","Oscr":"𝒪","oscr":"ℴ","Oslash":"Ø","oslash":"ø","osol":"⊘","Otilde":"Õ","otilde":"õ","otimesas":"⨶","Otimes":"⨷","otimes":"⊗","Ouml":"Ö","ouml":"ö","ovbar":"⌽","OverBar":"‾","OverBrace":"⏞","OverBracket":"⎴","OverParenthesis":"⏜","para":"¶","parallel":"∥","par":"∥","parsim":"⫳","parsl":"⫽","part":"∂","PartialD":"∂","Pcy":"П","pcy":"п","percnt":"%","period":".","permil":"‰","perp":"⊥","pertenk":"‱","Pfr":"𝔓","pfr":"𝔭","Phi":"Φ","phi":"φ","phiv":"ϕ","phmmat":"ℳ","phone":"☎","Pi":"Π","pi":"π","pitchfork":"⋔","piv":"ϖ","planck":"ℏ","planckh":"ℎ","plankv":"ℏ","plusacir":"⨣","plusb":"⊞","pluscir":"⨢","plus":"+","plusdo":"∔","plusdu":"⨥","pluse":"⩲","PlusMinus":"±","plusmn":"±","plussim":"⨦","plustwo":"⨧","pm":"±","Poincareplane":"ℌ","pointint":"⨕","popf":"𝕡","Popf":"ℙ","pound":"£","prap":"⪷","Pr":"⪻","pr":"≺","prcue":"≼","precapprox":"⪷","prec":"≺","preccurlyeq":"≼","Precedes":"≺","PrecedesEqual":"⪯","PrecedesSlantEqual":"≼","PrecedesTilde":"≾","preceq":"⪯","precnapprox":"⪹","precneqq":"⪵","precnsim":"⋨","pre":"⪯","prE":"⪳","precsim":"≾","prime":"′","Prime":"″","primes":"ℙ","prnap":"⪹","prnE":"⪵","prnsim":"⋨","prod":"∏","Product":"∏","profalar":"⌮","profline":"⌒","profsurf":"⌓","prop":"∝","Proportional":"∝","Proportion":"∷","propto":"∝","prsim":"≾","prurel":"⊰","Pscr":"𝒫","pscr":"𝓅","Psi":"Ψ","psi":"ψ","puncsp":" ","Qfr":"𝔔","qfr":"𝔮","qint":"⨌","qopf":"𝕢","Qopf":"ℚ","qprime":"⁗","Qscr":"𝒬","qscr":"𝓆","quaternions":"ℍ","quatint":"⨖","quest":"?","questeq":"≟","quot":"\"","QUOT":"\"","rAarr":"⇛","race":"∽̱","Racute":"Ŕ","racute":"ŕ","radic":"√","raemptyv":"⦳","rang":"⟩","Rang":"⟫","rangd":"⦒","range":"⦥","rangle":"⟩","raquo":"»","rarrap":"⥵","rarrb":"⇥","rarrbfs":"⤠","rarrc":"⤳","rarr":"→","Rarr":"↠","rArr":"⇒","rarrfs":"⤞","rarrhk":"↪","rarrlp":"↬","rarrpl":"⥅","rarrsim":"⥴","Rarrtl":"⤖","rarrtl":"↣","rarrw":"↝","ratail":"⤚","rAtail":"⤜","ratio":"∶","rationals":"ℚ","rbarr":"⤍","rBarr":"⤏","RBarr":"⤐","rbbrk":"❳","rbrace":"}","rbrack":"]","rbrke":"⦌","rbrksld":"⦎","rbrkslu":"⦐","Rcaron":"Ř","rcaron":"ř","Rcedil":"Ŗ","rcedil":"ŗ","rceil":"⌉","rcub":"}","Rcy":"Р","rcy":"р","rdca":"⤷","rdldhar":"⥩","rdquo":"”","rdquor":"”","rdsh":"↳","real":"ℜ","realine":"ℛ","realpart":"ℜ","reals":"ℝ","Re":"ℜ","rect":"▭","reg":"®","REG":"®","ReverseElement":"∋","ReverseEquilibrium":"⇋","ReverseUpEquilibrium":"⥯","rfisht":"⥽","rfloor":"⌋","rfr":"𝔯","Rfr":"ℜ","rHar":"⥤","rhard":"⇁","rharu":"⇀","rharul":"⥬","Rho":"Ρ","rho":"ρ","rhov":"ϱ","RightAngleBracket":"⟩","RightArrowBar":"⇥","rightarrow":"→","RightArrow":"→","Rightarrow":"⇒","RightArrowLeftArrow":"⇄","rightarrowtail":"↣","RightCeiling":"⌉","RightDoubleBracket":"⟧","RightDownTeeVector":"⥝","RightDownVectorBar":"⥕","RightDownVector":"⇂","RightFloor":"⌋","rightharpoondown":"⇁","rightharpoonup":"⇀","rightleftarrows":"⇄","rightleftharpoons":"⇌","rightrightarrows":"⇉","rightsquigarrow":"↝","RightTeeArrow":"↦","RightTee":"⊢","RightTeeVector":"⥛","rightthreetimes":"⋌","RightTriangleBar":"⧐","RightTriangle":"⊳","RightTriangleEqual":"⊵","RightUpDownVector":"⥏","RightUpTeeVector":"⥜","RightUpVectorBar":"⥔","RightUpVector":"↾","RightVectorBar":"⥓","RightVector":"⇀","ring":"˚","risingdotseq":"≓","rlarr":"⇄","rlhar":"⇌","rlm":"‏","rmoustache":"⎱","rmoust":"⎱","rnmid":"⫮","roang":"⟭","roarr":"⇾","robrk":"⟧","ropar":"⦆","ropf":"𝕣","Ropf":"ℝ","roplus":"⨮","rotimes":"⨵","RoundImplies":"⥰","rpar":")","rpargt":"⦔","rppolint":"⨒","rrarr":"⇉","Rrightarrow":"⇛","rsaquo":"›","rscr":"𝓇","Rscr":"ℛ","rsh":"↱","Rsh":"↱","rsqb":"]","rsquo":"’","rsquor":"’","rthree":"⋌","rtimes":"⋊","rtri":"▹","rtrie":"⊵","rtrif":"▸","rtriltri":"⧎","RuleDelayed":"⧴","ruluhar":"⥨","rx":"℞","Sacute":"Ś","sacute":"ś","sbquo":"‚","scap":"⪸","Scaron":"Š","scaron":"š","Sc":"⪼","sc":"≻","sccue":"≽","sce":"⪰","scE":"⪴","Scedil":"Ş","scedil":"ş","Scirc":"Ŝ","scirc":"ŝ","scnap":"⪺","scnE":"⪶","scnsim":"⋩","scpolint":"⨓","scsim":"≿","Scy":"С","scy":"с","sdotb":"⊡","sdot":"⋅","sdote":"⩦","searhk":"⤥","searr":"↘","seArr":"⇘","searrow":"↘","sect":"§","semi":";","seswar":"⤩","setminus":"∖","setmn":"∖","sext":"✶","Sfr":"𝔖","sfr":"𝔰","sfrown":"⌢","sharp":"♯","SHCHcy":"Щ","shchcy":"щ","SHcy":"Ш","shcy":"ш","ShortDownArrow":"↓","ShortLeftArrow":"←","shortmid":"∣","shortparallel":"∥","ShortRightArrow":"→","ShortUpArrow":"↑","shy":"­","Sigma":"Σ","sigma":"σ","sigmaf":"ς","sigmav":"ς","sim":"∼","simdot":"⩪","sime":"≃","simeq":"≃","simg":"⪞","simgE":"⪠","siml":"⪝","simlE":"⪟","simne":"≆","simplus":"⨤","simrarr":"⥲","slarr":"←","SmallCircle":"∘","smallsetminus":"∖","smashp":"⨳","smeparsl":"⧤","smid":"∣","smile":"⌣","smt":"⪪","smte":"⪬","smtes":"⪬︀","SOFTcy":"Ь","softcy":"ь","solbar":"⌿","solb":"⧄","sol":"/","Sopf":"𝕊","sopf":"𝕤","spades":"♠","spadesuit":"♠","spar":"∥","sqcap":"⊓","sqcaps":"⊓︀","sqcup":"⊔","sqcups":"⊔︀","Sqrt":"√","sqsub":"⊏","sqsube":"⊑","sqsubset":"⊏","sqsubseteq":"⊑","sqsup":"⊐","sqsupe":"⊒","sqsupset":"⊐","sqsupseteq":"⊒","square":"□","Square":"□","SquareIntersection":"⊓","SquareSubset":"⊏","SquareSubsetEqual":"⊑","SquareSuperset":"⊐","SquareSupersetEqual":"⊒","SquareUnion":"⊔","squarf":"▪","squ":"□","squf":"▪","srarr":"→","Sscr":"𝒮","sscr":"𝓈","ssetmn":"∖","ssmile":"⌣","sstarf":"⋆","Star":"⋆","star":"☆","starf":"★","straightepsilon":"ϵ","straightphi":"ϕ","strns":"¯","sub":"⊂","Sub":"⋐","subdot":"⪽","subE":"⫅","sube":"⊆","subedot":"⫃","submult":"⫁","subnE":"⫋","subne":"⊊","subplus":"⪿","subrarr":"⥹","subset":"⊂","Subset":"⋐","subseteq":"⊆","subseteqq":"⫅","SubsetEqual":"⊆","subsetneq":"⊊","subsetneqq":"⫋","subsim":"⫇","subsub":"⫕","subsup":"⫓","succapprox":"⪸","succ":"≻","succcurlyeq":"≽","Succeeds":"≻","SucceedsEqual":"⪰","SucceedsSlantEqual":"≽","SucceedsTilde":"≿","succeq":"⪰","succnapprox":"⪺","succneqq":"⪶","succnsim":"⋩","succsim":"≿","SuchThat":"∋","sum":"∑","Sum":"∑","sung":"♪","sup1":"¹","sup2":"²","sup3":"³","sup":"⊃","Sup":"⋑","supdot":"⪾","supdsub":"⫘","supE":"⫆","supe":"⊇","supedot":"⫄","Superset":"⊃","SupersetEqual":"⊇","suphsol":"⟉","suphsub":"⫗","suplarr":"⥻","supmult":"⫂","supnE":"⫌","supne":"⊋","supplus":"⫀","supset":"⊃","Supset":"⋑","supseteq":"⊇","supseteqq":"⫆","supsetneq":"⊋","supsetneqq":"⫌","supsim":"⫈","supsub":"⫔","supsup":"⫖","swarhk":"⤦","swarr":"↙","swArr":"⇙","swarrow":"↙","swnwar":"⤪","szlig":"ß","Tab":"\t","target":"⌖","Tau":"Τ","tau":"τ","tbrk":"⎴","Tcaron":"Ť","tcaron":"ť","Tcedil":"Ţ","tcedil":"ţ","Tcy":"Т","tcy":"т","tdot":"⃛","telrec":"⌕","Tfr":"𝔗","tfr":"𝔱","there4":"∴","therefore":"∴","Therefore":"∴","Theta":"Θ","theta":"θ","thetasym":"ϑ","thetav":"ϑ","thickapprox":"≈","thicksim":"∼","ThickSpace":"  ","ThinSpace":" ","thinsp":" ","thkap":"≈","thksim":"∼","THORN":"Þ","thorn":"þ","tilde":"˜","Tilde":"∼","TildeEqual":"≃","TildeFullEqual":"≅","TildeTilde":"≈","timesbar":"⨱","timesb":"⊠","times":"×","timesd":"⨰","tint":"∭","toea":"⤨","topbot":"⌶","topcir":"⫱","top":"⊤","Topf":"𝕋","topf":"𝕥","topfork":"⫚","tosa":"⤩","tprime":"‴","trade":"™","TRADE":"™","triangle":"▵","triangledown":"▿","triangleleft":"◃","trianglelefteq":"⊴","triangleq":"≜","triangleright":"▹","trianglerighteq":"⊵","tridot":"◬","trie":"≜","triminus":"⨺","TripleDot":"⃛","triplus":"⨹","trisb":"⧍","tritime":"⨻","trpezium":"⏢","Tscr":"𝒯","tscr":"𝓉","TScy":"Ц","tscy":"ц","TSHcy":"Ћ","tshcy":"ћ","Tstrok":"Ŧ","tstrok":"ŧ","twixt":"≬","twoheadleftarrow":"↞","twoheadrightarrow":"↠","Uacute":"Ú","uacute":"ú","uarr":"↑","Uarr":"↟","uArr":"⇑","Uarrocir":"⥉","Ubrcy":"Ў","ubrcy":"ў","Ubreve":"Ŭ","ubreve":"ŭ","Ucirc":"Û","ucirc":"û","Ucy":"У","ucy":"у","udarr":"⇅","Udblac":"Ű","udblac":"ű","udhar":"⥮","ufisht":"⥾","Ufr":"𝔘","ufr":"𝔲","Ugrave":"Ù","ugrave":"ù","uHar":"⥣","uharl":"↿","uharr":"↾","uhblk":"▀","ulcorn":"⌜","ulcorner":"⌜","ulcrop":"⌏","ultri":"◸","Umacr":"Ū","umacr":"ū","uml":"¨","UnderBar":"_","UnderBrace":"⏟","UnderBracket":"⎵","UnderParenthesis":"⏝","Union":"⋃","UnionPlus":"⊎","Uogon":"Ų","uogon":"ų","Uopf":"𝕌","uopf":"𝕦","UpArrowBar":"⤒","uparrow":"↑","UpArrow":"↑","Uparrow":"⇑","UpArrowDownArrow":"⇅","updownarrow":"↕","UpDownArrow":"↕","Updownarrow":"⇕","UpEquilibrium":"⥮","upharpoonleft":"↿","upharpoonright":"↾","uplus":"⊎","UpperLeftArrow":"↖","UpperRightArrow":"↗","upsi":"υ","Upsi":"ϒ","upsih":"ϒ","Upsilon":"Υ","upsilon":"υ","UpTeeArrow":"↥","UpTee":"⊥","upuparrows":"⇈","urcorn":"⌝","urcorner":"⌝","urcrop":"⌎","Uring":"Ů","uring":"ů","urtri":"◹","Uscr":"𝒰","uscr":"𝓊","utdot":"⋰","Utilde":"Ũ","utilde":"ũ","utri":"▵","utrif":"▴","uuarr":"⇈","Uuml":"Ü","uuml":"ü","uwangle":"⦧","vangrt":"⦜","varepsilon":"ϵ","varkappa":"ϰ","varnothing":"∅","varphi":"ϕ","varpi":"ϖ","varpropto":"∝","varr":"↕","vArr":"⇕","varrho":"ϱ","varsigma":"ς","varsubsetneq":"⊊︀","varsubsetneqq":"⫋︀","varsupsetneq":"⊋︀","varsupsetneqq":"⫌︀","vartheta":"ϑ","vartriangleleft":"⊲","vartriangleright":"⊳","vBar":"⫨","Vbar":"⫫","vBarv":"⫩","Vcy":"В","vcy":"в","vdash":"⊢","vDash":"⊨","Vdash":"⊩","VDash":"⊫","Vdashl":"⫦","veebar":"⊻","vee":"∨","Vee":"⋁","veeeq":"≚","vellip":"⋮","verbar":"|","Verbar":"‖","vert":"|","Vert":"‖","VerticalBar":"∣","VerticalLine":"|","VerticalSeparator":"❘","VerticalTilde":"≀","VeryThinSpace":" ","Vfr":"𝔙","vfr":"𝔳","vltri":"⊲","vnsub":"⊂⃒","vnsup":"⊃⃒","Vopf":"𝕍","vopf":"𝕧","vprop":"∝","vrtri":"⊳","Vscr":"𝒱","vscr":"𝓋","vsubnE":"⫋︀","vsubne":"⊊︀","vsupnE":"⫌︀","vsupne":"⊋︀","Vvdash":"⊪","vzigzag":"⦚","Wcirc":"Ŵ","wcirc":"ŵ","wedbar":"⩟","wedge":"∧","Wedge":"⋀","wedgeq":"≙","weierp":"℘","Wfr":"𝔚","wfr":"𝔴","Wopf":"𝕎","wopf":"𝕨","wp":"℘","wr":"≀","wreath":"≀","Wscr":"𝒲","wscr":"𝓌","xcap":"⋂","xcirc":"◯","xcup":"⋃","xdtri":"▽","Xfr":"𝔛","xfr":"𝔵","xharr":"⟷","xhArr":"⟺","Xi":"Ξ","xi":"ξ","xlarr":"⟵","xlArr":"⟸","xmap":"⟼","xnis":"⋻","xodot":"⨀","Xopf":"𝕏","xopf":"𝕩","xoplus":"⨁","xotime":"⨂","xrarr":"⟶","xrArr":"⟹","Xscr":"𝒳","xscr":"𝓍","xsqcup":"⨆","xuplus":"⨄","xutri":"△","xvee":"⋁","xwedge":"⋀","Yacute":"Ý","yacute":"ý","YAcy":"Я","yacy":"я","Ycirc":"Ŷ","ycirc":"ŷ","Ycy":"Ы","ycy":"ы","yen":"¥","Yfr":"𝔜","yfr":"𝔶","YIcy":"Ї","yicy":"ї","Yopf":"𝕐","yopf":"𝕪","Yscr":"𝒴","yscr":"𝓎","YUcy":"Ю","yucy":"ю","yuml":"ÿ","Yuml":"Ÿ","Zacute":"Ź","zacute":"ź","Zcaron":"Ž","zcaron":"ž","Zcy":"З","zcy":"з","Zdot":"Ż","zdot":"ż","zeetrf":"ℨ","ZeroWidthSpace":"​","Zeta":"Ζ","zeta":"ζ","zfr":"𝔷","Zfr":"ℨ","ZHcy":"Ж","zhcy":"ж","zigrarr":"⇝","zopf":"𝕫","Zopf":"ℤ","Zscr":"𝒵","zscr":"𝓏","zwj":"‍","zwnj":"‌"} 2 | -------------------------------------------------------------------------------- /maps/legacy.json: -------------------------------------------------------------------------------- 1 | {"Aacute":"Á","aacute":"á","Acirc":"Â","acirc":"â","acute":"´","AElig":"Æ","aelig":"æ","Agrave":"À","agrave":"à","amp":"&","AMP":"&","Aring":"Å","aring":"å","Atilde":"Ã","atilde":"ã","Auml":"Ä","auml":"ä","brvbar":"¦","Ccedil":"Ç","ccedil":"ç","cedil":"¸","cent":"¢","copy":"©","COPY":"©","curren":"¤","deg":"°","divide":"÷","Eacute":"É","eacute":"é","Ecirc":"Ê","ecirc":"ê","Egrave":"È","egrave":"è","ETH":"Ð","eth":"ð","Euml":"Ë","euml":"ë","frac12":"½","frac14":"¼","frac34":"¾","gt":">","GT":">","Iacute":"Í","iacute":"í","Icirc":"Î","icirc":"î","iexcl":"¡","Igrave":"Ì","igrave":"ì","iquest":"¿","Iuml":"Ï","iuml":"ï","laquo":"«","lt":"<","LT":"<","macr":"¯","micro":"µ","middot":"·","nbsp":" ","not":"¬","Ntilde":"Ñ","ntilde":"ñ","Oacute":"Ó","oacute":"ó","Ocirc":"Ô","ocirc":"ô","Ograve":"Ò","ograve":"ò","ordf":"ª","ordm":"º","Oslash":"Ø","oslash":"ø","Otilde":"Õ","otilde":"õ","Ouml":"Ö","ouml":"ö","para":"¶","plusmn":"±","pound":"£","quot":"\"","QUOT":"\"","raquo":"»","reg":"®","REG":"®","sect":"§","shy":"­","sup1":"¹","sup2":"²","sup3":"³","szlig":"ß","THORN":"Þ","thorn":"þ","times":"×","Uacute":"Ú","uacute":"ú","Ucirc":"Û","ucirc":"û","Ugrave":"Ù","ugrave":"ù","uml":"¨","Uuml":"Ü","uuml":"ü","Yacute":"Ý","yacute":"ý","yen":"¥","yuml":"ÿ"} 2 | -------------------------------------------------------------------------------- /maps/xml.json: -------------------------------------------------------------------------------- 1 | {"amp":"&","apos":"'","gt":">","lt":"<","quot":"\""} 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "entities", 3 | "version": "6.0.0", 4 | "description": "Encode & decode XML and HTML entities with ease & speed", 5 | "keywords": [ 6 | "html entities", 7 | "entity decoder", 8 | "entity encoding", 9 | "html decoding", 10 | "html encoding", 11 | "xml decoding", 12 | "xml encoding" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/fb55/entities.git" 17 | }, 18 | "funding": "https://github.com/fb55/entities?sponsor=1", 19 | "license": "BSD-2-Clause", 20 | "author": "Felix Boehm ", 21 | "sideEffects": false, 22 | "type": "module", 23 | "exports": { 24 | ".": { 25 | "import": { 26 | "types": "./dist/esm/index.d.ts", 27 | "default": "./dist/esm/index.js" 28 | }, 29 | "require": { 30 | "types": "./dist/commonjs/index.d.ts", 31 | "default": "./dist/commonjs/index.js" 32 | } 33 | }, 34 | "./decode": { 35 | "import": { 36 | "types": "./dist/esm/decode.d.ts", 37 | "default": "./dist/esm/decode.js" 38 | }, 39 | "require": { 40 | "types": "./dist/commonjs/decode.d.ts", 41 | "default": "./dist/commonjs/decode.js" 42 | } 43 | }, 44 | "./escape": { 45 | "import": { 46 | "types": "./dist/esm/escape.d.ts", 47 | "default": "./dist/esm/escape.js" 48 | }, 49 | "require": { 50 | "types": "./dist/commonjs/escape.d.ts", 51 | "default": "./dist/commonjs/escape.js" 52 | } 53 | } 54 | }, 55 | "main": "./dist/commonjs/index.js", 56 | "module": "./dist/esm/index.js", 57 | "types": "./dist/commonjs/index.d.ts", 58 | "files": [ 59 | "decode.js", 60 | "escape.js", 61 | "dist", 62 | "src" 63 | ], 64 | "scripts": { 65 | "build:docs": "typedoc --hideGenerator src/index.ts", 66 | "build:encode-trie": "node --import=tsx scripts/write-encode-map.ts", 67 | "build:trie": "node --import=tsx scripts/write-decode-map.ts", 68 | "format": "npm run format:es && npm run format:prettier", 69 | "format:es": "npm run lint:es -- --fix", 70 | "format:prettier": "npm run prettier -- --write", 71 | "lint": "npm run lint:es && npm run lint:ts && npm run lint:prettier", 72 | "lint:es": "eslint . --ignore-path .gitignore", 73 | "lint:prettier": "npm run prettier -- --check", 74 | "lint:ts": "tsc --noEmit", 75 | "prepublishOnly": "tshy", 76 | "prettier": "prettier '**/*.{ts,md,json,yml}'", 77 | "test": "npm run test:vi && npm run lint", 78 | "test:vi": "vitest run" 79 | }, 80 | "prettier": { 81 | "proseWrap": "always", 82 | "tabWidth": 4 83 | }, 84 | "devDependencies": { 85 | "@types/node": "^22.15.29", 86 | "@typescript-eslint/eslint-plugin": "^8.33.1", 87 | "@typescript-eslint/parser": "^8.33.1", 88 | "@vitest/coverage-v8": "^2.1.8", 89 | "eslint": "^8.57.1", 90 | "eslint-config-prettier": "^10.1.5", 91 | "eslint-plugin-n": "^17.19.0", 92 | "eslint-plugin-unicorn": "^56.0.1", 93 | "prettier": "^3.5.3", 94 | "tshy": "^3.0.2", 95 | "tsx": "^4.19.4", 96 | "typedoc": "^0.28.5", 97 | "typescript": "^5.8.3", 98 | "vitest": "^2.0.2" 99 | }, 100 | "engines": { 101 | "node": ">=0.12" 102 | }, 103 | "tshy": { 104 | "exclude": [ 105 | "**/*.spec.ts", 106 | "**/__fixtures__/*", 107 | "**/__tests__/*", 108 | "**/__snapshots__/*" 109 | ], 110 | "exports": { 111 | ".": "./src/index.ts", 112 | "./decode": "./src/decode.ts", 113 | "./escape": "./src/escape.ts" 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # entities [![NPM version](https://img.shields.io/npm/v/entities.svg)](https://npmjs.org/package/entities) [![Downloads](https://img.shields.io/npm/dm/entities.svg)](https://npmjs.org/package/entities) [![Node.js CI](https://github.com/fb55/entities/actions/workflows/nodejs-test.yml/badge.svg)](https://github.com/fb55/entities/actions/workflows/nodejs-test.yml) 2 | 3 | Encode & decode HTML & XML entities with ease & speed. 4 | 5 | ## Features 6 | 7 | - 😇 Tried and true: `entities` is used by many popular libraries; eg. 8 | [`htmlparser2`](https://github.com/fb55/htmlparser2), the official 9 | [AWS SDK](https://github.com/aws/aws-sdk-js-v3) and 10 | [`commonmark`](https://github.com/commonmark/commonmark.js) use it to process 11 | HTML entities. 12 | - ⚡️ Fast: `entities` is the fastest library for decoding HTML entities (as of 13 | April 2022); see [performance](#performance). 14 | - 🎛 Configurable: Get an output tailored for your needs. You are fine with 15 | UTF8? That'll save you some bytes. Prefer to only have ASCII characters? We 16 | can do that as well! 17 | 18 | ## How to… 19 | 20 | ### …install `entities` 21 | 22 | npm install entities 23 | 24 | ### …use `entities` 25 | 26 | ```javascript 27 | const entities = require("entities"); 28 | 29 | // Encoding 30 | entities.escapeUTF8("& ü"); // "&#38; ü" 31 | entities.encodeXML("& ü"); // "&#38; ü" 32 | entities.encodeHTML("& ü"); // "&#38; ü" 33 | 34 | // Decoding 35 | entities.decodeXML("asdf & ÿ ü '"); // "asdf & ÿ ü '" 36 | entities.decodeHTML("asdf & ÿ ü '"); // "asdf & ÿ ü '" 37 | ``` 38 | 39 | ## Performance 40 | 41 | This is how `entities` compares to other libraries on a very basic benchmark 42 | (see `scripts/benchmark.ts`, for 10,000,000 iterations; **lower is better**): 43 | 44 | | Library | Version | `decode` perf | `encode` perf | `escape` perf | 45 | | -------------- | ------- | ------------- | ------------- | ------------- | 46 | | entities | `3.0.1` | 1.418s | 6.786s | 2.196s | 47 | | html-entities | `2.3.2` | 2.530s | 6.829s | 2.415s | 48 | | he | `1.2.0` | 5.800s | 24.237s | 3.624s | 49 | | parse-entities | `3.0.0` | 9.660s | N/A | N/A | 50 | 51 | --- 52 | 53 | ## FAQ 54 | 55 | > What methods should I actually use to encode my documents? 56 | 57 | If your target supports UTF-8, the `escapeUTF8` method is going to be your best 58 | choice. Otherwise, use either `encodeHTML` or `encodeXML` based on whether 59 | you're dealing with an HTML or an XML document. 60 | 61 | You can have a look at the options for the `encode` and `decode` methods to see 62 | everything you can configure. 63 | 64 | > When should I use strict decoding? 65 | 66 | When strict decoding, entities not terminated with a semicolon will be ignored. 67 | This is helpful for decoding entities in legacy environments. 68 | 69 | > Why should I use `entities` instead of alternative modules? 70 | 71 | As of April 2022, `entities` is a bit faster than other modules. Still, this is 72 | not a very differentiated space and other modules can catch up. 73 | 74 | **More importantly**, you might already have `entities` in your dependency graph 75 | (as a dependency of eg. `cheerio`, or `htmlparser2`), and including it directly 76 | might not even increase your bundle size. The same is true for other entity 77 | libraries, so have a look through your `node_modules` directory! 78 | 79 | > Does `entities` support tree shaking? 80 | 81 | Yes! `entities` ships as both a CommonJS and a ES module. Note that for best 82 | results, you should not use the `encode` and `decode` functions, as they wrap 83 | around a number of other functions, all of which will remain in the bundle. 84 | Instead, use the functions that you need directly. 85 | 86 | --- 87 | 88 | ## Acknowledgements 89 | 90 | This library wouldn't be possible without the work of these individuals. Thanks 91 | to 92 | 93 | - [@mathiasbynens](https://github.com/mathiasbynens) for his explanations about 94 | character encodings, and his library `he`, which was one of the inspirations 95 | for `entities` 96 | - [@inikulin](https://github.com/inikulin) for his work on optimized tries for 97 | decoding HTML entities for the `parse5` project 98 | - [@mdevils](https://github.com/mdevils) for taking on the challenge of 99 | producing a quick entity library with his `html-entities` library. `entities` 100 | would be quite a bit slower if there wasn't any competition. Right now 101 | `entities` is on top, but we'll see how long that lasts! 102 | 103 | --- 104 | 105 | License: BSD-2-Clause 106 | 107 | ## Security contact information 108 | 109 | To report a security vulnerability, please use the 110 | [Tidelift security contact](https://tidelift.com/security). Tidelift will 111 | coordinate the fix and disclosure. 112 | 113 | ## `entities` for enterprise 114 | 115 | Available as part of the Tidelift Subscription 116 | 117 | The maintainers of `entities` and thousands of other packages are working with 118 | Tidelift to deliver commercial support and maintenance for the open source 119 | dependencies you use to build your applications. Save time, reduce risk, and 120 | improve code health, while paying the maintainers of the exact dependencies you 121 | use. 122 | [Learn more.](https://tidelift.com/subscription/pkg/npm-entities?utm_source=npm-entities&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) 123 | -------------------------------------------------------------------------------- /scripts/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "n/no-unsupported-features/es-builtins": 0, 4 | "n/no-unsupported-features/node-builtins": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /scripts/benchmark.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable n/no-missing-import */ 2 | import * as entities from "../src/index.js"; 3 | // @ts-expect-error Not a dependency; only added for benchmarking. 4 | import * as he from "he"; 5 | // @ts-expect-error Not a dependency; only added for benchmarking. 6 | import { parseEntities } from "parse-entities"; 7 | // @ts-expect-error Not a dependency; only added for benchmarking. 8 | import * as htmlEntities from "html-entities"; 9 | 10 | const RUNS = 1e7; 11 | 12 | const htmlEntitiesHtml5EncodeOptions: htmlEntities.EncodeOptions = { 13 | level: "html5", 14 | mode: "nonAsciiPrintable", 15 | }; 16 | 17 | const heEscapeOptions = { useNamedReferences: true }; 18 | 19 | const encoders: [string, (stringToEncode: string) => string][] = [ 20 | ["entities", (stringToEncode) => entities.encodeHTML(stringToEncode)], 21 | ["he", (stringToEncode) => he.encode(stringToEncode, heEscapeOptions)], 22 | [ 23 | "html-entities", 24 | (stringToEncode) => 25 | htmlEntities.encode(stringToEncode, htmlEntitiesHtml5EncodeOptions), 26 | ], 27 | ]; 28 | 29 | const htmlEntitiesHtml5DecodeOptions: htmlEntities.DecodeOptions = { 30 | level: "html5", 31 | scope: "body", 32 | }; 33 | 34 | const decoders: [string, (stringToDecode: string) => string][] = [ 35 | ["entities", (stringToDecode) => entities.decodeHTML(stringToDecode)], 36 | ["he", (stringToDecode) => he.decode(stringToDecode)], 37 | ["parse-entities", (stringToDecode) => parseEntities(stringToDecode)], 38 | [ 39 | "html-entities", 40 | (stringToDecode) => 41 | htmlEntities.decode(stringToDecode, htmlEntitiesHtml5DecodeOptions), 42 | ], 43 | ]; 44 | 45 | const htmlEntitiesXmlEncodeOptions: htmlEntities.EncodeOptions = { 46 | level: "xml", 47 | mode: "specialChars", 48 | }; 49 | 50 | const escapers: [string, (escapee: string) => string][] = [ 51 | ["entities", (escapee) => entities.escapeUTF8(escapee)], 52 | ["he", (escapee) => he.escape(escapee)], 53 | // Html-entities cannot escape, so we use its simplest mode. 54 | [ 55 | "html-entities", 56 | (escapee) => htmlEntities.encode(escapee, htmlEntitiesXmlEncodeOptions), 57 | ], 58 | ]; 59 | 60 | const textToDecode = `This is a simple text über &#x${"?" 61 | .charCodeAt(0) 62 | .toString(16)}; something.`; 63 | 64 | const textToEncode = `über & unter's sprießende ❤️👊😉`; 65 | 66 | console.log( 67 | "Escaping results", 68 | escapers.map(([name, escape]) => [name, escape(textToEncode)]), 69 | ); 70 | 71 | console.log( 72 | "Encoding results", 73 | encoders.map(([name, encode]) => [name, encode(textToEncode)]), 74 | ); 75 | 76 | console.log( 77 | "Decoding results", 78 | decoders.map(([name, decode]) => [name, decode(textToDecode)]), 79 | ); 80 | 81 | for (const [name, escape] of escapers) { 82 | console.time(`Escaping ${name}`); 83 | for (let index = 0; index < RUNS; index++) { 84 | escape(textToEncode); 85 | } 86 | console.timeEnd(`Escaping ${name}`); 87 | } 88 | 89 | for (const [name, encode] of encoders) { 90 | console.time(`Encoding ${name}`); 91 | for (let index = 0; index < RUNS; index++) { 92 | encode(textToEncode); 93 | } 94 | console.timeEnd(`Encoding ${name}`); 95 | } 96 | 97 | for (const [name, decode] of decoders) { 98 | console.time(`Decoding ${name}`); 99 | for (let index = 0; index < RUNS; index++) { 100 | decode(textToDecode); 101 | } 102 | console.timeEnd(`Decoding ${name}`); 103 | } 104 | -------------------------------------------------------------------------------- /scripts/trie/README.md: -------------------------------------------------------------------------------- 1 | # Named entity array-mapped trie generator 2 | 3 | In `v3.0.0`, `entities` adopted a version of the radix tree from 4 | [`parse5`](https://github.com/inikulin/parse5). The below is adapted from 5 | @inikulin's explanation of this structure. 6 | 7 | Prior to `parse5@3.0.0`, the library used simple pre-generated 8 | [trie data structure](https://en.wikipedia.org/wiki/Trie) for 9 | [named character references](https://html.spec.whatwg.org/multipage/syntax.html#named-character-references) 10 | in the tokenizer. This approach suffered from huge constant memory consumption: 11 | the in-memory size of the structure was ~8.5Mb. This new approach reduces the 12 | size of the character reference data to ~250Kb, at equivalent performance. 13 | 14 | ## Radix tree 15 | 16 | All entities are encoded as a trie, which contains _nodes_. Nodes contain data 17 | and branches. 18 | 19 | E.g. for the words `test`, `tester` and `testing`, we'll receive the following 20 | trie: 21 | 22 | Legend: `[a, ...]` - node, `*` - data. 23 | 24 | ``` 25 | [t] 26 | | 27 | [e] 28 | | 29 | [s] 30 | | 31 | [t] 32 | | 33 | [e, i, *] 34 | / | 35 | [r] [n] 36 | | | 37 | [*] [g] 38 | | 39 | [*] 40 | ``` 41 | 42 | ## Mapping the trie to an array 43 | 44 | If we had to allocate an object for each node, the trie would consume a lot of 45 | memory (the aforementioned ~8.5Mb). Therefore, we map our trie to an array, so 46 | we'll end up with just a single object. Since we don't have indices and code 47 | points which are more than `MAX_UINT16` (which is `0xFFFF`), we can use a 48 | `Uint16Array` for this. 49 | 50 | The only exception here are 51 | [surrogate pairs](https://en.wikipedia.org/wiki/UTF-16#U.2B10000_to_U.2B10FFFF), 52 | which appear in named character reference results. They can be split across two 53 | `uint16` code points. The advantage of typed arrays is that they consume less 54 | memory and are extremely fast to traverse. 55 | 56 | ### Node layout 57 | 58 | A node may contain one or two bytes of data and/or branch data. The layout of a 59 | node is as follows: 60 | 61 | ``` 62 | 2 bit | 7 bit | 7 bit 63 | \ \ \ 64 | \ \ \ 65 | \ \ \ 66 | \ \ jump table offset 67 | \ number of branches 68 | value length 69 | ``` 70 | 71 | The _value length_ is the number of bytes used for the value. If the length is 72 | 0, we don't have a value. If the length is 1, the node does not have any 73 | branches and the value will be stored inside the lower 14 bit of the node 74 | itself. Otherwise, the value will be stored in the next one or two bytes of the 75 | array. 76 | 77 | If it has any branch data (indicated by the _number of branches_ or the _jump 78 | table offset_ being set), the node will be followed by the branch data. 79 | 80 | ### Branch data 81 | 82 | Branches can be represented in three different ways: 83 | 84 | 1. If we only have a single branch, and this branch wasn't encoded earlier in 85 | the tree, we set the number of branches to 0 and the jump table offset to 86 | the branch value. The node will be followed by the serialized branch. 87 | 2. If the branch values are close to one another, we use a jump table. This is 88 | indicated by the jump table offset not being 0. The jump table is an array 89 | of destination indices. 90 | 3. If the branch values are far apart, we use a dictionary. Branch data is 91 | represented by two arrays, following one after another. The first array 92 | contains sorted transition code points, the second one the corresponding 93 | next edge/node indices. The traversing algorithm will use binary search to 94 | find the key, and will then use the corresponding value as the jump target. 95 | 96 | The original `parse5` implementation used a radix tree as the basis for the 97 | encoded structure. It used a dictionary (see (3) above), as well as a variation 98 | of (1) for edges of the radix tree. The implementation in `entities` allowed us 99 | to use a trie when starting to decode, and gave us some space savings in the 100 | output. 101 | -------------------------------------------------------------------------------- /scripts/trie/decode-trie.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { encodeTrie } from "./encode-trie.js"; 3 | import { decodeNode } from "./decode-trie.js"; 4 | import { getTrie } from "./trie.js"; 5 | import xmlMap from "../../maps/xml.json" with { type: "json" }; 6 | import entityMap from "../../maps/entities.json" with { type: "json" }; 7 | import legacyMap from "../../maps/legacy.json" with { type: "json" }; 8 | 9 | function decode(decodeMap: number[]) { 10 | const map = {}; 11 | decodeNode(decodeMap, map, "", 0); 12 | 13 | return map; 14 | } 15 | 16 | function mergeMaps( 17 | map: Record, 18 | legacy: Record, 19 | ): Record { 20 | const merged = { 21 | ...legacy, 22 | ...Object.fromEntries( 23 | Object.entries(map).map(([key, value]) => [`${key};`, value]), 24 | ), 25 | }; 26 | 27 | return merged; 28 | } 29 | 30 | describe("decode_trie", () => { 31 | it("should decode an empty node", () => 32 | expect(decode([0b0000_0000_0000_0000])).toStrictEqual({})); 33 | 34 | it("should decode an empty encode", () => 35 | expect(decode(encodeTrie({}))).toStrictEqual({})); 36 | 37 | it("should decode a node with a value", () => 38 | expect(decode(encodeTrie({ value: "a" }))).toStrictEqual({ "": "a" })); 39 | 40 | it("should decode a node with a multi-byte value", () => 41 | expect(decode(encodeTrie({ value: "ab" }))).toStrictEqual({ 42 | "": "ab", 43 | })); 44 | 45 | it("should decode a branch of size 1", () => 46 | expect( 47 | decode( 48 | encodeTrie({ 49 | next: new Map([["b".charCodeAt(0), { value: "a" }]]), 50 | }), 51 | ), 52 | ).toStrictEqual({ b: "a" })); 53 | 54 | it("should decode a dictionary of size 2", () => 55 | expect( 56 | decode( 57 | encodeTrie({ 58 | next: new Map([ 59 | ["A".charCodeAt(0), { value: "a" }], 60 | ["b".charCodeAt(0), { value: "B" }], 61 | ]), 62 | }), 63 | ), 64 | ).toStrictEqual({ A: "a", b: "B" })); 65 | 66 | it("should decode a jump table of size 2", () => 67 | expect( 68 | decode( 69 | encodeTrie({ 70 | next: new Map([ 71 | ["a".charCodeAt(0), { value: "a" }], 72 | ["b".charCodeAt(0), { value: "B" }], 73 | ]), 74 | }), 75 | ), 76 | ).toStrictEqual({ a: "a", b: "B" })); 77 | 78 | it("should decode the XML map", () => 79 | expect(decode(encodeTrie(getTrie(xmlMap, {})))).toStrictEqual( 80 | mergeMaps(xmlMap, {}), 81 | )); 82 | 83 | it("should decode the HTML map", () => 84 | expect(decode(encodeTrie(getTrie(entityMap, legacyMap)))).toStrictEqual( 85 | mergeMaps(entityMap, legacyMap), 86 | )); 87 | }); 88 | -------------------------------------------------------------------------------- /scripts/trie/decode-trie.ts: -------------------------------------------------------------------------------- 1 | enum BinTrieFlags { 2 | VALUE_LENGTH = 0b1100_0000_0000_0000, 3 | BRANCH_LENGTH = 0b0011_1111_1000_0000, 4 | JUMP_TABLE = 0b0000_0000_0111_1111, 5 | } 6 | 7 | export function decodeNode( 8 | decodeMap: number[], 9 | resultMap: Record, 10 | prefix: string, 11 | startIndex: number, 12 | ): void { 13 | const current = decodeMap[startIndex]; 14 | const valueLength = (current & BinTrieFlags.VALUE_LENGTH) >> 14; 15 | 16 | if (valueLength > 0) { 17 | resultMap[prefix] = 18 | valueLength === 1 19 | ? String.fromCharCode( 20 | decodeMap[startIndex] & ~BinTrieFlags.VALUE_LENGTH, 21 | ) 22 | : valueLength === 2 23 | ? String.fromCharCode(decodeMap[startIndex + 1]) 24 | : String.fromCharCode( 25 | decodeMap[startIndex + 1], 26 | decodeMap[startIndex + 2], 27 | ); 28 | } 29 | 30 | const branchLength = (current & BinTrieFlags.BRANCH_LENGTH) >> 7; 31 | const jumpOffset = current & BinTrieFlags.JUMP_TABLE; 32 | 33 | if (valueLength === 1 || (branchLength === 0 && jumpOffset === 0)) { 34 | return; 35 | } 36 | 37 | const branchIndex = startIndex + Math.max(valueLength, 1); 38 | 39 | if (branchLength === 0) { 40 | return decodeNode( 41 | decodeMap, 42 | resultMap, 43 | prefix + String.fromCharCode(jumpOffset), 44 | branchIndex, 45 | ); 46 | } 47 | 48 | if (jumpOffset === 0) { 49 | for (let index = 0; index < branchLength; index++) { 50 | decodeNode( 51 | decodeMap, 52 | resultMap, 53 | prefix + String.fromCharCode(decodeMap[branchIndex + index]), 54 | decodeMap[branchIndex + branchLength + index], 55 | ); 56 | } 57 | } else { 58 | for (let index = 0; index < branchLength; index++) { 59 | const value = decodeMap[branchIndex + index] - 1; 60 | if (value !== -1) { 61 | const code = jumpOffset + index; 62 | 63 | decodeNode( 64 | decodeMap, 65 | resultMap, 66 | prefix + String.fromCharCode(code), 67 | value, 68 | ); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /scripts/trie/encode-trie.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { encodeTrie } from "./encode-trie.js"; 3 | import type { TrieNode } from "./trie.js"; 4 | 5 | describe("encode_trie", () => { 6 | it("should encode an empty node", () => { 7 | expect(encodeTrie({})).toStrictEqual([0b0000_0000_0000_0000]); 8 | }); 9 | 10 | it("should encode a node with a value", () => { 11 | expect(encodeTrie({ value: "a" })).toStrictEqual([ 12 | 0b0100_0000_0000_0000 | "a".charCodeAt(0), 13 | ]); 14 | }); 15 | 16 | it("should encode a node with a multi-byte value", () => { 17 | expect(encodeTrie({ value: "ab" })).toStrictEqual([ 18 | 0b1100_0000_0000_0000, 19 | "a".charCodeAt(0), 20 | "b".charCodeAt(0), 21 | ]); 22 | }); 23 | 24 | it("should encode a branch of size 1", () => { 25 | expect( 26 | encodeTrie({ 27 | next: new Map([["b".charCodeAt(0), { value: "a" }]]), 28 | }), 29 | ).toStrictEqual([ 30 | "b".charCodeAt(0), 31 | 0b0100_0000_0000_0000 | "a".charCodeAt(0), 32 | ]); 33 | }); 34 | 35 | it("should encode a branch of size 1 with a value that's already encoded", () => { 36 | const nodeA: TrieNode = { value: "a" }; 37 | const nodeC = { next: new Map([["c".charCodeAt(0), nodeA]]) }; 38 | const trie = { 39 | next: new Map([ 40 | ["A".charCodeAt(0), nodeA], 41 | ["b".charCodeAt(0), nodeC], 42 | ]), 43 | }; 44 | expect(encodeTrie(trie)).toStrictEqual([ 45 | 0b0000_0001_0000_0000, 46 | "A".charCodeAt(0), 47 | "b".charCodeAt(0), 48 | 0b101, 49 | 0b110, 50 | 0b0100_0000_0000_0000 | "a".charCodeAt(0), 51 | 0b0000_0000_1000_0000 | "c".charCodeAt(0), 52 | 0b110, // Index plus one 53 | ]); 54 | }); 55 | 56 | it("should encode a disjoint recursive branch", () => { 57 | const recursiveTrie = { next: new Map() }; 58 | recursiveTrie.next.set("a".charCodeAt(0), { value: "a" }); 59 | recursiveTrie.next.set("0".charCodeAt(0), recursiveTrie); 60 | expect(encodeTrie(recursiveTrie)).toStrictEqual([ 61 | 0b0000_0001_0000_0000, 62 | "0".charCodeAt(0), 63 | "a".charCodeAt(0), 64 | 0, 65 | 5, 66 | 0b0100_0000_0000_0000 | "a".charCodeAt(0), 67 | ]); 68 | }); 69 | 70 | it("should encode a recursive branch to a jump map", () => { 71 | const jumpRecursiveTrie = { next: new Map() }; 72 | for (const value of [48, 49, 52, 54, 56, 57]) { 73 | jumpRecursiveTrie.next.set(value, jumpRecursiveTrie); 74 | } 75 | expect(encodeTrie(jumpRecursiveTrie)).toStrictEqual([ 76 | 0b0000_0101_0011_0000, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 77 | ]); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /scripts/trie/encode-trie.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "node:assert"; 2 | import type { TrieNode } from "./trie.js"; 3 | 4 | /** 5 | * Determines the binary length of an integer. 6 | */ 7 | function binaryLength(integer: number): number { 8 | return Math.ceil(Math.log2(integer)); 9 | } 10 | 11 | /** 12 | * Encodes the trie in binary form. 13 | * 14 | * We have three different types of nodes: 15 | * - Values are UNICODE values that an entity resolves to 16 | * - Branches can be: 17 | * 1. If size is 1, then a matching character followed by the destination 18 | * 2. Two successive tables: characters and destination pointers. 19 | * Characters have to be binary-searched to get the index of the destination pointer. 20 | * 3. A jump table: For each character, the destination pointer is stored in a jump table. 21 | * - Records have a value greater than 128 (the max ASCII value). Their format is 8 bits main data, 8 bits supplemental data: 22 | * ( 23 | * 1 bit has has value flag 24 | * 7 bit branch length if this is a branch — needs to be here to ensure value is >128 with a branch 25 | * 1 bit data is multi-byte 26 | * 7 bit branch jump table offset (if branch is a jump table) 27 | * ) 28 | * 29 | */ 30 | export function encodeTrie(trie: TrieNode, maxJumpTableOverhead = 2): number[] { 31 | const encodeCache = new Map(); 32 | const enc: number[] = []; 33 | 34 | function encodeNode(node: TrieNode): number { 35 | // Cache nodes, as we can have loops 36 | const cached = encodeCache.get(node); 37 | if (cached != null) return cached; 38 | 39 | const startIndex = enc.length; 40 | 41 | encodeCache.set(node, startIndex); 42 | 43 | const nodeIndex = enc.push(0) - 1; 44 | 45 | if (node.value != null) { 46 | let valueLength = 0; 47 | 48 | /* 49 | * If we don't have a branch and the value is short, we can 50 | * store the value in the node. 51 | */ 52 | if ( 53 | node.next !== undefined || 54 | node.value.length > 1 || 55 | binaryLength(node.value.charCodeAt(0)) > 14 56 | ) { 57 | valueLength = node.value.length; 58 | } 59 | 60 | // Add 1 to the value length, to signal that we have a value. 61 | valueLength += 1; 62 | 63 | assert.ok( 64 | binaryLength(valueLength) <= 2, 65 | "Too many bits for value length", 66 | ); 67 | 68 | enc[nodeIndex] |= valueLength << 14; 69 | 70 | if (valueLength === 1) { 71 | enc[nodeIndex] |= node.value.charCodeAt(0); 72 | } else { 73 | for (let index = 0; index < node.value.length; index++) { 74 | enc.push(node.value.charCodeAt(index)); 75 | } 76 | } 77 | } 78 | 79 | if (node.next) addBranches(node.next, nodeIndex); 80 | 81 | assert.strictEqual(nodeIndex, startIndex, "Has expected location"); 82 | 83 | return startIndex; 84 | } 85 | 86 | function addBranches(next: Map, nodeIndex: number) { 87 | const branches = [...next.entries()]; 88 | 89 | // Sort branches ASC by key 90 | branches.sort(([a], [b]) => a - b); 91 | 92 | assert.ok( 93 | binaryLength(branches.length) <= 6, 94 | "Too many bits for branches", 95 | ); 96 | 97 | // If we only have a single branch, we can write the next value directly 98 | if (branches.length === 1 && !encodeCache.has(branches[0][1])) { 99 | const [char, next] = branches[0]; 100 | 101 | assert.ok(binaryLength(char) <= 7, "Too many bits for single char"); 102 | 103 | enc[nodeIndex] |= char; 104 | encodeNode(next); 105 | return; 106 | } 107 | 108 | const branchIndex = enc.length; 109 | 110 | // If we have consecutive branches, we can write the next value as a jump table 111 | 112 | /* 113 | * First, we determine how much space adding the jump table adds. 114 | * 115 | * If it is more than 2x the number of branches (which is equivalent 116 | * to the size of the dictionary), skip it. 117 | */ 118 | 119 | const jumpOffset = branches[0][0]; 120 | const jumpEndValue = branches[branches.length - 1][0]; 121 | 122 | const jumpTableLength = jumpEndValue - jumpOffset + 1; 123 | 124 | const jumpTableOverhead = jumpTableLength / branches.length; 125 | 126 | if (jumpTableOverhead <= maxJumpTableOverhead) { 127 | assert.ok( 128 | binaryLength(jumpOffset) <= 16, 129 | `Offset ${jumpOffset} too large at ${binaryLength(jumpOffset)}`, 130 | ); 131 | 132 | // Write the length of the adjusted table, plus jump offset 133 | enc[nodeIndex] |= (jumpTableLength << 7) | jumpOffset; 134 | 135 | assert.ok( 136 | binaryLength(jumpTableLength) <= 7, 137 | `Too many bits (${binaryLength(jumpTableLength)}) for branches`, 138 | ); 139 | 140 | // Reserve space for the jump table 141 | for (let index = 0; index < jumpTableLength; index++) enc.push(0); 142 | 143 | // Write the jump table 144 | for (const [char, next] of branches) { 145 | const index = char - jumpOffset; 146 | // Write all values + 1, so 0 will result in a -1 when decoding 147 | enc[branchIndex + index] = encodeNode(next) + 1; 148 | } 149 | 150 | return; 151 | } 152 | 153 | enc[nodeIndex] |= branches.length << 7; 154 | 155 | enc.push( 156 | ...branches.map(([char]) => char), 157 | // Reserve space for destinations, using a value that is out of bounds 158 | ...branches.map((_) => Number.MAX_SAFE_INTEGER), 159 | ); 160 | 161 | assert.strictEqual( 162 | enc.length, 163 | branchIndex + branches.length * 2, 164 | "Did not reserve enough space", 165 | ); 166 | 167 | // Encode the branches 168 | for (const [index, [value, next]] of branches.entries()) { 169 | assert.ok(value < 128, "Branch value too large"); 170 | 171 | const currentIndex = branchIndex + branches.length + index; 172 | assert.strictEqual( 173 | enc[currentIndex - branches.length], 174 | value, 175 | "Should have the value as the first element", 176 | ); 177 | assert.strictEqual( 178 | enc[currentIndex], 179 | Number.MAX_SAFE_INTEGER, 180 | "Should have the placeholder as the second element", 181 | ); 182 | const offset = encodeNode(next); 183 | 184 | assert.ok(binaryLength(offset) <= 16, "Too many bits for offset"); 185 | enc[currentIndex] = offset; 186 | } 187 | } 188 | 189 | encodeNode(trie); 190 | 191 | // Make sure that every value fits in a UInt16 192 | assert.ok( 193 | enc.every( 194 | (value) => 195 | typeof value === "number" && 196 | value >= 0 && 197 | binaryLength(value) <= 16, 198 | ), 199 | "Too many bits", 200 | ); 201 | 202 | return enc; 203 | } 204 | -------------------------------------------------------------------------------- /scripts/trie/trie.ts: -------------------------------------------------------------------------------- 1 | export interface TrieNode { 2 | value?: string; 3 | next?: Map | undefined; 4 | } 5 | 6 | export function getTrie( 7 | map: Record, 8 | legacy: Record, 9 | ): TrieNode { 10 | const trie = new Map(); 11 | const root = { next: trie }; 12 | 13 | for (const key of Object.keys(map)) { 14 | // Resolve the key 15 | let lastMap = trie; 16 | let next!: TrieNode; 17 | for (let index = 0; index < key.length; index++) { 18 | const char = key.charCodeAt(index); 19 | next = lastMap.get(char) ?? {}; 20 | lastMap.set(char, next); 21 | lastMap = next.next ??= new Map(); 22 | } 23 | 24 | if (key in legacy) next.value = map[key]; 25 | 26 | lastMap.set(";".charCodeAt(0), { value: map[key] }); 27 | } 28 | 29 | function isEqual(node1: TrieNode, node2: TrieNode): boolean { 30 | if (node1 === node2) return true; 31 | 32 | if (node1.value !== node2.value) { 33 | return false; 34 | } 35 | 36 | // Check if the next nodes are equal. That means both are undefined. 37 | if (node1.next === node2.next) return true; 38 | if ( 39 | node1.next == null || 40 | node2.next == null || 41 | node1.next.size !== node2.next.size 42 | ) { 43 | return false; 44 | } 45 | 46 | for (const [char, node] of node1.next) { 47 | const value = node2.next.get(char); 48 | if (value == null || !isEqual(node, value)) { 49 | return false; 50 | } 51 | } 52 | 53 | return true; 54 | } 55 | 56 | function mergeDuplicates(node: TrieNode) { 57 | const nodes = [node]; 58 | 59 | for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++) { 60 | const { next } = nodes[nodeIndex]; 61 | 62 | if (!next) continue; 63 | 64 | for (const [char, node] of next) { 65 | const index = nodes.findIndex((n) => isEqual(n, node)); 66 | 67 | if (index === -1) { 68 | nodes.push(node); 69 | } else { 70 | next.set(char, nodes[index]); 71 | } 72 | } 73 | } 74 | } 75 | 76 | mergeDuplicates(root); 77 | 78 | return root; 79 | } 80 | -------------------------------------------------------------------------------- /scripts/write-decode-map.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import entityMap from "../maps/entities.json" with { type: "json" }; 3 | import legacyMap from "../maps/legacy.json" with { type: "json" }; 4 | import xmlMap from "../maps/xml.json" with { type: "json" }; 5 | 6 | import { getTrie } from "./trie/trie.js"; 7 | import { encodeTrie } from "./trie/encode-trie.js"; 8 | 9 | function convertMapToBinaryTrie( 10 | name: "xml" | "html", 11 | map: Record, 12 | legacy: Record, 13 | ) { 14 | const encoded = encodeTrie(getTrie(map, legacy)); 15 | const stringified = JSON.stringify(String.fromCharCode(...encoded)) 16 | .replace( 17 | /[^\u0020-\u007E]/g, 18 | (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`, 19 | ) 20 | .replace(/\\u0{4}/g, String.raw`\0`) 21 | .replace(/\\u00([\da-f]{2})/g, String.raw`\x$1`); 22 | 23 | // Write the encoded trie to disk 24 | fs.writeFileSync( 25 | new URL(`../src/generated/decode-data-${name}.ts`, import.meta.url), 26 | `// Generated using scripts/write-decode-map.ts 27 | 28 | export const ${name}DecodeTree: Uint16Array = /* #__PURE__ */ new Uint16Array( 29 | // prettier-ignore 30 | /* #__PURE__ */ ${stringified} 31 | .split("") 32 | .map((c) => c.charCodeAt(0)), 33 | ); 34 | `, 35 | ); 36 | } 37 | 38 | convertMapToBinaryTrie("xml", xmlMap, {}); 39 | convertMapToBinaryTrie("html", entityMap, legacyMap); 40 | 41 | console.log("Done!"); 42 | -------------------------------------------------------------------------------- /scripts/write-encode-map.ts: -------------------------------------------------------------------------------- 1 | import htmlMap from "../maps/entities.json" with { type: "json" }; 2 | import { writeFileSync } from "node:fs"; 3 | 4 | interface TrieNode { 5 | /** The value, if the node has a value. */ 6 | value?: string | undefined; 7 | /** A map with the next nodes, if there are any. */ 8 | next?: Map | undefined; 9 | } 10 | 11 | const htmlTrie = getTrie(htmlMap); 12 | const serialized = serializeTrie(htmlTrie); 13 | 14 | writeFileSync( 15 | new URL("../src/generated/encode-html.ts", import.meta.url), 16 | `// Generated using scripts/write-encode-map.ts 17 | 18 | type EncodeTrieNode = 19 | | string 20 | | { v?: string; n: number | Map; o?: string }; 21 | 22 | function restoreDiff>( 23 | array: T 24 | ): T { 25 | for (let index = 1; index < array.length; index++) { 26 | array[index][0] += array[index - 1][0] + 1; 27 | } 28 | return array; 29 | } 30 | 31 | // prettier-ignore 32 | export const htmlTrie: Map = ${ 33 | // Fix the type of the first map to refer to trie nodes. 34 | serialized.replace("", "") 35 | }; 36 | `, 37 | ); 38 | 39 | console.log("Done!"); 40 | 41 | function getTrie(map: Record): Map { 42 | const trie = new Map(); 43 | 44 | for (const entity of Object.keys(map)) { 45 | const decoded = map[entity]; 46 | // Resolve the key 47 | let lastMap = trie; 48 | for (let index = 0; index < decoded.length - 1; index++) { 49 | const char = decoded.charCodeAt(index); 50 | const next = lastMap.get(char) ?? {}; 51 | lastMap.set(char, next); 52 | lastMap = next.next ??= new Map(); 53 | } 54 | const value = lastMap.get(decoded.charCodeAt(decoded.length - 1)) ?? {}; 55 | value.value ??= entity; 56 | lastMap.set(decoded.charCodeAt(decoded.length - 1), value); 57 | } 58 | 59 | return trie; 60 | } 61 | 62 | function wrapValue(value: string | undefined): string { 63 | if (value == null) throw new Error("unexpected null"); 64 | 65 | return `"&${value};"`; 66 | } 67 | 68 | function serializeTrie(trie: Map): string { 69 | const entries: [number, TrieNode][] = [...trie.entries()].sort( 70 | (a, b) => a[0] - b[0], 71 | ); 72 | 73 | return `/* #__PURE__ */ new Map(/* #__PURE__ */restoreDiff([${entries 74 | .map(([key, value], index, array) => { 75 | if (index !== 0) { 76 | key -= array[index - 1][0] + 1; 77 | } 78 | if (!value.next) { 79 | if (value.value == null) throw new Error("unexpected null"); 80 | 81 | return `[${key},${wrapValue(value.value)}]`; 82 | } 83 | 84 | const entries: string[] = []; 85 | 86 | if (value.value != null) { 87 | entries.push(`v:${wrapValue(value.value)}`); 88 | } 89 | 90 | /* 91 | * We encode branches as either a number with an `o` (other) value, 92 | * or as a map. 93 | * 94 | * We use a map if there are more than one character in the key. 95 | */ 96 | if (value.next.size > 1) { 97 | entries.push(`n:${serializeTrie(value.next)}`); 98 | } else { 99 | const [condition, other] = [...value.next][0]; 100 | 101 | entries.push(`n:${condition},o:${wrapValue(other.value)}`); 102 | } 103 | 104 | return `[${key},{${entries.join(",")}}]`; 105 | }) 106 | .join(",")}]))`; 107 | } 108 | -------------------------------------------------------------------------------- /src/decode-codepoint.ts: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/mathiasbynens/he/blob/36afe179392226cf1b6ccdb16ebbb7a5a844d93a/src/he.js#L106-L134 2 | 3 | const decodeMap = new Map([ 4 | [0, 65_533], 5 | // C1 Unicode control character reference replacements 6 | [128, 8364], 7 | [130, 8218], 8 | [131, 402], 9 | [132, 8222], 10 | [133, 8230], 11 | [134, 8224], 12 | [135, 8225], 13 | [136, 710], 14 | [137, 8240], 15 | [138, 352], 16 | [139, 8249], 17 | [140, 338], 18 | [142, 381], 19 | [145, 8216], 20 | [146, 8217], 21 | [147, 8220], 22 | [148, 8221], 23 | [149, 8226], 24 | [150, 8211], 25 | [151, 8212], 26 | [152, 732], 27 | [153, 8482], 28 | [154, 353], 29 | [155, 8250], 30 | [156, 339], 31 | [158, 382], 32 | [159, 376], 33 | ]); 34 | 35 | /** 36 | * Polyfill for `String.fromCodePoint`. It is used to create a string from a Unicode code point. 37 | */ 38 | export const fromCodePoint: (...codePoints: number[]) => string = 39 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, n/no-unsupported-features/es-builtins 40 | String.fromCodePoint ?? 41 | function (codePoint: number): string { 42 | let output = ""; 43 | 44 | if (codePoint > 0xff_ff) { 45 | codePoint -= 0x1_00_00; 46 | output += String.fromCharCode( 47 | ((codePoint >>> 10) & 0x3_ff) | 0xd8_00, 48 | ); 49 | codePoint = 0xdc_00 | (codePoint & 0x3_ff); 50 | } 51 | 52 | output += String.fromCharCode(codePoint); 53 | return output; 54 | }; 55 | 56 | /** 57 | * Replace the given code point with a replacement character if it is a 58 | * surrogate or is outside the valid range. Otherwise return the code 59 | * point unchanged. 60 | */ 61 | export function replaceCodePoint(codePoint: number): number { 62 | if ( 63 | (codePoint >= 0xd8_00 && codePoint <= 0xdf_ff) || 64 | codePoint > 0x10_ff_ff 65 | ) { 66 | return 0xff_fd; 67 | } 68 | 69 | return decodeMap.get(codePoint) ?? codePoint; 70 | } 71 | 72 | /** 73 | * Replace the code point if relevant, then convert it to a string. 74 | * 75 | * @deprecated Use `fromCodePoint(replaceCodePoint(codePoint))` instead. 76 | * @param codePoint The code point to decode. 77 | * @returns The decoded code point. 78 | */ 79 | export function decodeCodePoint(codePoint: number): string { 80 | return fromCodePoint(replaceCodePoint(codePoint)); 81 | } 82 | -------------------------------------------------------------------------------- /src/decode.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vitest } from "vitest"; 2 | import * as entities from "./decode.js"; 3 | 4 | describe("Decode test", () => { 5 | const testcases = [ 6 | { input: "&amp;", output: "&" }, 7 | { input: "&#38;", output: "&" }, 8 | { input: "&#x26;", output: "&" }, 9 | { input: "&#X26;", output: "&" }, 10 | { input: "&#38;", output: "&" }, 11 | { input: "&#38;", output: "&" }, 12 | { input: "&#38;", output: "&" }, 13 | { input: ":", output: ":" }, 14 | { input: ":", output: ":" }, 15 | { input: ":", output: ":" }, 16 | { input: ":", output: ":" }, 17 | { input: "&#", output: "&#" }, 18 | { input: "&>", output: "&>" }, 19 | { input: "id=770&#anchor", output: "id=770&#anchor" }, 20 | ]; 21 | 22 | for (const { input, output } of testcases) { 23 | it(`should XML decode ${input}`, () => 24 | expect(entities.decodeXML(input)).toBe(output)); 25 | it(`should HTML decode ${input}`, () => 26 | expect(entities.decodeHTML(input)).toBe(output)); 27 | } 28 | 29 | it("should HTML decode partial legacy entity", () => { 30 | expect(entities.decodeHTMLStrict("×bar")).toBe("×bar"); 31 | expect(entities.decodeHTML("×bar")).toBe("×bar"); 32 | }); 33 | 34 | it("should HTML decode legacy entities according to spec", () => 35 | expect(entities.decodeHTML("?&image_uri=1&ℑ=2&image=3")).toBe( 36 | "?&image_uri=1&ℑ=2&image=3", 37 | )); 38 | 39 | it("should back out of legacy entities", () => 40 | expect(entities.decodeHTML("&a")).toBe("&a")); 41 | 42 | it("should not parse numeric entities in strict mode", () => 43 | expect(entities.decodeHTMLStrict("7")).toBe("7")); 44 | 45 | it("should parse   followed by < (#852)", () => 46 | expect(entities.decodeHTML(" <")).toBe("\u00A0<")); 47 | 48 | it("should decode trailing legacy entities", () => { 49 | expect(entities.decodeHTML("⨱×bar")).toBe("⨱×bar"); 50 | }); 51 | 52 | it("should decode multi-byte entities", () => { 53 | expect(entities.decodeHTML("≧̸")).toBe("≧̸"); 54 | }); 55 | 56 | it("should not decode legacy entities followed by text in attribute mode", () => { 57 | expect( 58 | entities.decodeHTML("¬", entities.DecodingMode.Attribute), 59 | ).toBe("¬"); 60 | 61 | expect( 62 | entities.decodeHTML("¬i", entities.DecodingMode.Attribute), 63 | ).toBe("¬i"); 64 | 65 | expect( 66 | entities.decodeHTML("¬=", entities.DecodingMode.Attribute), 67 | ).toBe("¬="); 68 | 69 | expect(entities.decodeHTMLAttribute("¬p")).toBe("¬p"); 70 | expect(entities.decodeHTMLAttribute("¬P")).toBe("¬P"); 71 | expect(entities.decodeHTMLAttribute("¬3")).toBe("¬3"); 72 | }); 73 | }); 74 | 75 | describe("EntityDecoder", () => { 76 | it("should decode decimal entities", () => { 77 | const callback = vitest.fn(); 78 | const decoder = new entities.EntityDecoder( 79 | entities.htmlDecodeTree, 80 | callback, 81 | ); 82 | 83 | expect(decoder.write("", 1)).toBe(-1); 84 | expect(decoder.write("8;", 0)).toBe(5); 85 | 86 | expect(callback).toHaveBeenCalledTimes(1); 87 | expect(callback).toHaveBeenCalledWith(":".charCodeAt(0), 5); 88 | }); 89 | 90 | it("should decode hex entities", () => { 91 | const callback = vitest.fn(); 92 | const decoder = new entities.EntityDecoder( 93 | entities.htmlDecodeTree, 94 | callback, 95 | ); 96 | 97 | expect(decoder.write(":", 1)).toBe(6); 98 | 99 | expect(callback).toHaveBeenCalledTimes(1); 100 | expect(callback).toHaveBeenCalledWith(":".charCodeAt(0), 6); 101 | }); 102 | 103 | it("should decode named entities", () => { 104 | const callback = vitest.fn(); 105 | const decoder = new entities.EntityDecoder( 106 | entities.htmlDecodeTree, 107 | callback, 108 | ); 109 | 110 | expect(decoder.write("&", 1)).toBe(5); 111 | 112 | expect(callback).toHaveBeenCalledTimes(1); 113 | expect(callback).toHaveBeenCalledWith("&".charCodeAt(0), 5); 114 | }); 115 | 116 | it("should decode legacy entities", () => { 117 | const callback = vitest.fn(); 118 | const decoder = new entities.EntityDecoder( 119 | entities.htmlDecodeTree, 120 | callback, 121 | ); 122 | decoder.startEntity(entities.DecodingMode.Legacy); 123 | 124 | expect(decoder.write("&", 1)).toBe(-1); 125 | 126 | expect(callback).toHaveBeenCalledTimes(0); 127 | 128 | expect(decoder.end()).toBe(4); 129 | 130 | expect(callback).toHaveBeenCalledTimes(1); 131 | expect(callback).toHaveBeenCalledWith("&".charCodeAt(0), 4); 132 | }); 133 | 134 | it("should decode named entity written character by character", () => { 135 | const callback = vitest.fn(); 136 | const decoder = new entities.EntityDecoder( 137 | entities.htmlDecodeTree, 138 | callback, 139 | ); 140 | 141 | for (const c of "amp") { 142 | expect(decoder.write(c, 0)).toBe(-1); 143 | } 144 | expect(decoder.write(";", 0)).toBe(5); 145 | 146 | expect(callback).toHaveBeenCalledTimes(1); 147 | expect(callback).toHaveBeenCalledWith("&".charCodeAt(0), 5); 148 | }); 149 | 150 | it("should decode numeric entity written character by character", () => { 151 | const callback = vitest.fn(); 152 | const decoder = new entities.EntityDecoder( 153 | entities.htmlDecodeTree, 154 | callback, 155 | ); 156 | 157 | for (const c of "#x3a") { 158 | expect(decoder.write(c, 0)).toBe(-1); 159 | } 160 | expect(decoder.write(";", 0)).toBe(6); 161 | 162 | expect(callback).toHaveBeenCalledTimes(1); 163 | expect(callback).toHaveBeenCalledWith(":".charCodeAt(0), 6); 164 | }); 165 | 166 | it("should decode hex entities across several chunks", () => { 167 | const callback = vitest.fn(); 168 | const decoder = new entities.EntityDecoder( 169 | entities.htmlDecodeTree, 170 | callback, 171 | ); 172 | 173 | for (const chunk of ["#x", "cf", "ff", "d"]) { 174 | expect(decoder.write(chunk, 0)).toBe(-1); 175 | } 176 | 177 | expect(decoder.write(";", 0)).toBe(9); 178 | expect(callback).toHaveBeenCalledTimes(1); 179 | expect(callback).toHaveBeenCalledWith(0xc_ff_fd, 9); 180 | }); 181 | 182 | it("should not fail if nothing is written", () => { 183 | const callback = vitest.fn(); 184 | const decoder = new entities.EntityDecoder( 185 | entities.htmlDecodeTree, 186 | callback, 187 | ); 188 | 189 | expect(decoder.end()).toBe(0); 190 | expect(callback).toHaveBeenCalledTimes(0); 191 | }); 192 | 193 | describe("errors", () => { 194 | it("should produce an error for a named entity without a semicolon", () => { 195 | const errorHandlers = { 196 | missingSemicolonAfterCharacterReference: vitest.fn(), 197 | absenceOfDigitsInNumericCharacterReference: vitest.fn(), 198 | validateNumericCharacterReference: vitest.fn(), 199 | }; 200 | const callback = vitest.fn(); 201 | const decoder = new entities.EntityDecoder( 202 | entities.htmlDecodeTree, 203 | callback, 204 | errorHandlers, 205 | ); 206 | 207 | decoder.startEntity(entities.DecodingMode.Legacy); 208 | expect(decoder.write("&", 1)).toBe(5); 209 | expect(callback).toHaveBeenCalledTimes(1); 210 | expect(callback).toHaveBeenCalledWith("&".charCodeAt(0), 5); 211 | expect( 212 | errorHandlers.missingSemicolonAfterCharacterReference, 213 | ).toHaveBeenCalledTimes(0); 214 | 215 | decoder.startEntity(entities.DecodingMode.Legacy); 216 | expect(decoder.write("&", 1)).toBe(-1); 217 | expect(decoder.end()).toBe(4); 218 | 219 | expect(callback).toHaveBeenCalledTimes(2); 220 | expect(callback).toHaveBeenLastCalledWith("&".charCodeAt(0), 4); 221 | expect( 222 | errorHandlers.missingSemicolonAfterCharacterReference, 223 | ).toHaveBeenCalledTimes(1); 224 | }); 225 | 226 | it("should produce an error for a numeric entity without a semicolon", () => { 227 | const errorHandlers = { 228 | missingSemicolonAfterCharacterReference: vitest.fn(), 229 | absenceOfDigitsInNumericCharacterReference: vitest.fn(), 230 | validateNumericCharacterReference: vitest.fn(), 231 | }; 232 | const callback = vitest.fn(); 233 | const decoder = new entities.EntityDecoder( 234 | entities.htmlDecodeTree, 235 | callback, 236 | errorHandlers, 237 | ); 238 | 239 | decoder.startEntity(entities.DecodingMode.Legacy); 240 | expect(decoder.write(":", 1)).toBe(-1); 241 | expect(decoder.end()).toBe(5); 242 | 243 | expect(callback).toHaveBeenCalledTimes(1); 244 | expect(callback).toHaveBeenCalledWith(0x3a, 5); 245 | expect( 246 | errorHandlers.missingSemicolonAfterCharacterReference, 247 | ).toHaveBeenCalledTimes(1); 248 | expect( 249 | errorHandlers.absenceOfDigitsInNumericCharacterReference, 250 | ).toHaveBeenCalledTimes(0); 251 | expect( 252 | errorHandlers.validateNumericCharacterReference, 253 | ).toHaveBeenCalledTimes(1); 254 | expect( 255 | errorHandlers.validateNumericCharacterReference, 256 | ).toHaveBeenCalledWith(0x3a); 257 | }); 258 | 259 | it("should produce an error for numeric entities without digits", () => { 260 | const errorHandlers = { 261 | missingSemicolonAfterCharacterReference: vitest.fn(), 262 | absenceOfDigitsInNumericCharacterReference: vitest.fn(), 263 | validateNumericCharacterReference: vitest.fn(), 264 | }; 265 | const callback = vitest.fn(); 266 | const decoder = new entities.EntityDecoder( 267 | entities.htmlDecodeTree, 268 | callback, 269 | errorHandlers, 270 | ); 271 | 272 | decoder.startEntity(entities.DecodingMode.Legacy); 273 | expect(decoder.write("&#", 1)).toBe(-1); 274 | expect(decoder.end()).toBe(0); 275 | 276 | expect(callback).toHaveBeenCalledTimes(0); 277 | expect( 278 | errorHandlers.missingSemicolonAfterCharacterReference, 279 | ).toHaveBeenCalledTimes(0); 280 | expect( 281 | errorHandlers.absenceOfDigitsInNumericCharacterReference, 282 | ).toHaveBeenCalledTimes(1); 283 | expect( 284 | errorHandlers.absenceOfDigitsInNumericCharacterReference, 285 | ).toHaveBeenCalledWith(2); 286 | expect( 287 | errorHandlers.validateNumericCharacterReference, 288 | ).toHaveBeenCalledTimes(0); 289 | }); 290 | 291 | it("should produce an error for hex entities without digits", () => { 292 | const errorHandlers = { 293 | missingSemicolonAfterCharacterReference: vitest.fn(), 294 | absenceOfDigitsInNumericCharacterReference: vitest.fn(), 295 | validateNumericCharacterReference: vitest.fn(), 296 | }; 297 | const callback = vitest.fn(); 298 | const decoder = new entities.EntityDecoder( 299 | entities.htmlDecodeTree, 300 | callback, 301 | errorHandlers, 302 | ); 303 | 304 | decoder.startEntity(entities.DecodingMode.Legacy); 305 | expect(decoder.write("&#x", 1)).toBe(-1); 306 | expect(decoder.end()).toBe(0); 307 | 308 | expect(callback).toHaveBeenCalledTimes(0); 309 | expect( 310 | errorHandlers.missingSemicolonAfterCharacterReference, 311 | ).toHaveBeenCalledTimes(0); 312 | expect( 313 | errorHandlers.absenceOfDigitsInNumericCharacterReference, 314 | ).toHaveBeenCalledTimes(1); 315 | expect( 316 | errorHandlers.validateNumericCharacterReference, 317 | ).toHaveBeenCalledTimes(0); 318 | }); 319 | }); 320 | }); 321 | -------------------------------------------------------------------------------- /src/decode.ts: -------------------------------------------------------------------------------- 1 | import { htmlDecodeTree } from "./generated/decode-data-html.js"; 2 | import { xmlDecodeTree } from "./generated/decode-data-xml.js"; 3 | import { replaceCodePoint, fromCodePoint } from "./decode-codepoint.js"; 4 | 5 | const enum CharCodes { 6 | NUM = 35, // "#" 7 | SEMI = 59, // ";" 8 | EQUALS = 61, // "=" 9 | ZERO = 48, // "0" 10 | NINE = 57, // "9" 11 | LOWER_A = 97, // "a" 12 | LOWER_F = 102, // "f" 13 | LOWER_X = 120, // "x" 14 | LOWER_Z = 122, // "z" 15 | UPPER_A = 65, // "A" 16 | UPPER_F = 70, // "F" 17 | UPPER_Z = 90, // "Z" 18 | } 19 | 20 | /** Bit that needs to be set to convert an upper case ASCII character to lower case */ 21 | const TO_LOWER_BIT = 0b10_0000; 22 | 23 | export enum BinTrieFlags { 24 | VALUE_LENGTH = 0b1100_0000_0000_0000, 25 | BRANCH_LENGTH = 0b0011_1111_1000_0000, 26 | JUMP_TABLE = 0b0000_0000_0111_1111, 27 | } 28 | 29 | function isNumber(code: number): boolean { 30 | return code >= CharCodes.ZERO && code <= CharCodes.NINE; 31 | } 32 | 33 | function isHexadecimalCharacter(code: number): boolean { 34 | return ( 35 | (code >= CharCodes.UPPER_A && code <= CharCodes.UPPER_F) || 36 | (code >= CharCodes.LOWER_A && code <= CharCodes.LOWER_F) 37 | ); 38 | } 39 | 40 | function isAsciiAlphaNumeric(code: number): boolean { 41 | return ( 42 | (code >= CharCodes.UPPER_A && code <= CharCodes.UPPER_Z) || 43 | (code >= CharCodes.LOWER_A && code <= CharCodes.LOWER_Z) || 44 | isNumber(code) 45 | ); 46 | } 47 | 48 | /** 49 | * Checks if the given character is a valid end character for an entity in an attribute. 50 | * 51 | * Attribute values that aren't terminated properly aren't parsed, and shouldn't lead to a parser error. 52 | * See the example in https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state 53 | */ 54 | function isEntityInAttributeInvalidEnd(code: number): boolean { 55 | return code === CharCodes.EQUALS || isAsciiAlphaNumeric(code); 56 | } 57 | 58 | const enum EntityDecoderState { 59 | EntityStart, 60 | NumericStart, 61 | NumericDecimal, 62 | NumericHex, 63 | NamedEntity, 64 | } 65 | 66 | export enum DecodingMode { 67 | /** Entities in text nodes that can end with any character. */ 68 | Legacy = 0, 69 | /** Only allow entities terminated with a semicolon. */ 70 | Strict = 1, 71 | /** Entities in attributes have limitations on ending characters. */ 72 | Attribute = 2, 73 | } 74 | 75 | /** 76 | * Producers for character reference errors as defined in the HTML spec. 77 | */ 78 | export interface EntityErrorProducer { 79 | missingSemicolonAfterCharacterReference(): void; 80 | absenceOfDigitsInNumericCharacterReference( 81 | consumedCharacters: number, 82 | ): void; 83 | validateNumericCharacterReference(code: number): void; 84 | } 85 | 86 | /** 87 | * Token decoder with support of writing partial entities. 88 | */ 89 | export class EntityDecoder { 90 | constructor( 91 | /** The tree used to decode entities. */ 92 | private readonly decodeTree: Uint16Array, 93 | /** 94 | * The function that is called when a codepoint is decoded. 95 | * 96 | * For multi-byte named entities, this will be called multiple times, 97 | * with the second codepoint, and the same `consumed` value. 98 | * 99 | * @param codepoint The decoded codepoint. 100 | * @param consumed The number of bytes consumed by the decoder. 101 | */ 102 | private readonly emitCodePoint: (cp: number, consumed: number) => void, 103 | /** An object that is used to produce errors. */ 104 | private readonly errors?: EntityErrorProducer | undefined, 105 | ) {} 106 | 107 | /** The current state of the decoder. */ 108 | private state = EntityDecoderState.EntityStart; 109 | /** Characters that were consumed while parsing an entity. */ 110 | private consumed = 1; 111 | /** 112 | * The result of the entity. 113 | * 114 | * Either the result index of a numeric entity, or the codepoint of a 115 | * numeric entity. 116 | */ 117 | private result = 0; 118 | 119 | /** The current index in the decode tree. */ 120 | private treeIndex = 0; 121 | /** The number of characters that were consumed in excess. */ 122 | private excess = 1; 123 | /** The mode in which the decoder is operating. */ 124 | private decodeMode = DecodingMode.Strict; 125 | 126 | /** Resets the instance to make it reusable. */ 127 | startEntity(decodeMode: DecodingMode): void { 128 | this.decodeMode = decodeMode; 129 | this.state = EntityDecoderState.EntityStart; 130 | this.result = 0; 131 | this.treeIndex = 0; 132 | this.excess = 1; 133 | this.consumed = 1; 134 | } 135 | 136 | /** 137 | * Write an entity to the decoder. This can be called multiple times with partial entities. 138 | * If the entity is incomplete, the decoder will return -1. 139 | * 140 | * Mirrors the implementation of `getDecoder`, but with the ability to stop decoding if the 141 | * entity is incomplete, and resume when the next string is written. 142 | * 143 | * @param input The string containing the entity (or a continuation of the entity). 144 | * @param offset The offset at which the entity begins. Should be 0 if this is not the first call. 145 | * @returns The number of characters that were consumed, or -1 if the entity is incomplete. 146 | */ 147 | write(input: string, offset: number): number { 148 | switch (this.state) { 149 | case EntityDecoderState.EntityStart: { 150 | if (input.charCodeAt(offset) === CharCodes.NUM) { 151 | this.state = EntityDecoderState.NumericStart; 152 | this.consumed += 1; 153 | return this.stateNumericStart(input, offset + 1); 154 | } 155 | this.state = EntityDecoderState.NamedEntity; 156 | return this.stateNamedEntity(input, offset); 157 | } 158 | 159 | case EntityDecoderState.NumericStart: { 160 | return this.stateNumericStart(input, offset); 161 | } 162 | 163 | case EntityDecoderState.NumericDecimal: { 164 | return this.stateNumericDecimal(input, offset); 165 | } 166 | 167 | case EntityDecoderState.NumericHex: { 168 | return this.stateNumericHex(input, offset); 169 | } 170 | 171 | case EntityDecoderState.NamedEntity: { 172 | return this.stateNamedEntity(input, offset); 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * Switches between the numeric decimal and hexadecimal states. 179 | * 180 | * Equivalent to the `Numeric character reference state` in the HTML spec. 181 | * 182 | * @param input The string containing the entity (or a continuation of the entity). 183 | * @param offset The current offset. 184 | * @returns The number of characters that were consumed, or -1 if the entity is incomplete. 185 | */ 186 | private stateNumericStart(input: string, offset: number): number { 187 | if (offset >= input.length) { 188 | return -1; 189 | } 190 | 191 | if ((input.charCodeAt(offset) | TO_LOWER_BIT) === CharCodes.LOWER_X) { 192 | this.state = EntityDecoderState.NumericHex; 193 | this.consumed += 1; 194 | return this.stateNumericHex(input, offset + 1); 195 | } 196 | 197 | this.state = EntityDecoderState.NumericDecimal; 198 | return this.stateNumericDecimal(input, offset); 199 | } 200 | 201 | private addToNumericResult( 202 | input: string, 203 | start: number, 204 | end: number, 205 | base: number, 206 | ): void { 207 | if (start !== end) { 208 | const digitCount = end - start; 209 | this.result = 210 | this.result * Math.pow(base, digitCount) + 211 | Number.parseInt(input.substr(start, digitCount), base); 212 | this.consumed += digitCount; 213 | } 214 | } 215 | 216 | /** 217 | * Parses a hexadecimal numeric entity. 218 | * 219 | * Equivalent to the `Hexademical character reference state` in the HTML spec. 220 | * 221 | * @param input The string containing the entity (or a continuation of the entity). 222 | * @param offset The current offset. 223 | * @returns The number of characters that were consumed, or -1 if the entity is incomplete. 224 | */ 225 | private stateNumericHex(input: string, offset: number): number { 226 | const startIndex = offset; 227 | 228 | while (offset < input.length) { 229 | const char = input.charCodeAt(offset); 230 | if (isNumber(char) || isHexadecimalCharacter(char)) { 231 | offset += 1; 232 | } else { 233 | this.addToNumericResult(input, startIndex, offset, 16); 234 | return this.emitNumericEntity(char, 3); 235 | } 236 | } 237 | 238 | this.addToNumericResult(input, startIndex, offset, 16); 239 | 240 | return -1; 241 | } 242 | 243 | /** 244 | * Parses a decimal numeric entity. 245 | * 246 | * Equivalent to the `Decimal character reference state` in the HTML spec. 247 | * 248 | * @param input The string containing the entity (or a continuation of the entity). 249 | * @param offset The current offset. 250 | * @returns The number of characters that were consumed, or -1 if the entity is incomplete. 251 | */ 252 | private stateNumericDecimal(input: string, offset: number): number { 253 | const startIndex = offset; 254 | 255 | while (offset < input.length) { 256 | const char = input.charCodeAt(offset); 257 | if (isNumber(char)) { 258 | offset += 1; 259 | } else { 260 | this.addToNumericResult(input, startIndex, offset, 10); 261 | return this.emitNumericEntity(char, 2); 262 | } 263 | } 264 | 265 | this.addToNumericResult(input, startIndex, offset, 10); 266 | 267 | return -1; 268 | } 269 | 270 | /** 271 | * Validate and emit a numeric entity. 272 | * 273 | * Implements the logic from the `Hexademical character reference start 274 | * state` and `Numeric character reference end state` in the HTML spec. 275 | * 276 | * @param lastCp The last code point of the entity. Used to see if the 277 | * entity was terminated with a semicolon. 278 | * @param expectedLength The minimum number of characters that should be 279 | * consumed. Used to validate that at least one digit 280 | * was consumed. 281 | * @returns The number of characters that were consumed. 282 | */ 283 | private emitNumericEntity(lastCp: number, expectedLength: number): number { 284 | // Ensure we consumed at least one digit. 285 | if (this.consumed <= expectedLength) { 286 | this.errors?.absenceOfDigitsInNumericCharacterReference( 287 | this.consumed, 288 | ); 289 | return 0; 290 | } 291 | 292 | // Figure out if this is a legit end of the entity 293 | if (lastCp === CharCodes.SEMI) { 294 | this.consumed += 1; 295 | } else if (this.decodeMode === DecodingMode.Strict) { 296 | return 0; 297 | } 298 | 299 | this.emitCodePoint(replaceCodePoint(this.result), this.consumed); 300 | 301 | if (this.errors) { 302 | if (lastCp !== CharCodes.SEMI) { 303 | this.errors.missingSemicolonAfterCharacterReference(); 304 | } 305 | 306 | this.errors.validateNumericCharacterReference(this.result); 307 | } 308 | 309 | return this.consumed; 310 | } 311 | 312 | /** 313 | * Parses a named entity. 314 | * 315 | * Equivalent to the `Named character reference state` in the HTML spec. 316 | * 317 | * @param input The string containing the entity (or a continuation of the entity). 318 | * @param offset The current offset. 319 | * @returns The number of characters that were consumed, or -1 if the entity is incomplete. 320 | */ 321 | private stateNamedEntity(input: string, offset: number): number { 322 | const { decodeTree } = this; 323 | let current = decodeTree[this.treeIndex]; 324 | // The mask is the number of bytes of the value, including the current byte. 325 | let valueLength = (current & BinTrieFlags.VALUE_LENGTH) >> 14; 326 | 327 | for (; offset < input.length; offset++, this.excess++) { 328 | const char = input.charCodeAt(offset); 329 | 330 | this.treeIndex = determineBranch( 331 | decodeTree, 332 | current, 333 | this.treeIndex + Math.max(1, valueLength), 334 | char, 335 | ); 336 | 337 | if (this.treeIndex < 0) { 338 | return this.result === 0 || 339 | // If we are parsing an attribute 340 | (this.decodeMode === DecodingMode.Attribute && 341 | // We shouldn't have consumed any characters after the entity, 342 | (valueLength === 0 || 343 | // And there should be no invalid characters. 344 | isEntityInAttributeInvalidEnd(char))) 345 | ? 0 346 | : this.emitNotTerminatedNamedEntity(); 347 | } 348 | 349 | current = decodeTree[this.treeIndex]; 350 | valueLength = (current & BinTrieFlags.VALUE_LENGTH) >> 14; 351 | 352 | // If the branch is a value, store it and continue 353 | if (valueLength !== 0) { 354 | // If the entity is terminated by a semicolon, we are done. 355 | if (char === CharCodes.SEMI) { 356 | return this.emitNamedEntityData( 357 | this.treeIndex, 358 | valueLength, 359 | this.consumed + this.excess, 360 | ); 361 | } 362 | 363 | // If we encounter a non-terminated (legacy) entity while parsing strictly, then ignore it. 364 | if (this.decodeMode !== DecodingMode.Strict) { 365 | this.result = this.treeIndex; 366 | this.consumed += this.excess; 367 | this.excess = 0; 368 | } 369 | } 370 | } 371 | 372 | return -1; 373 | } 374 | 375 | /** 376 | * Emit a named entity that was not terminated with a semicolon. 377 | * 378 | * @returns The number of characters consumed. 379 | */ 380 | private emitNotTerminatedNamedEntity(): number { 381 | const { result, decodeTree } = this; 382 | 383 | const valueLength = 384 | (decodeTree[result] & BinTrieFlags.VALUE_LENGTH) >> 14; 385 | 386 | this.emitNamedEntityData(result, valueLength, this.consumed); 387 | this.errors?.missingSemicolonAfterCharacterReference(); 388 | 389 | return this.consumed; 390 | } 391 | 392 | /** 393 | * Emit a named entity. 394 | * 395 | * @param result The index of the entity in the decode tree. 396 | * @param valueLength The number of bytes in the entity. 397 | * @param consumed The number of characters consumed. 398 | * 399 | * @returns The number of characters consumed. 400 | */ 401 | private emitNamedEntityData( 402 | result: number, 403 | valueLength: number, 404 | consumed: number, 405 | ): number { 406 | const { decodeTree } = this; 407 | 408 | this.emitCodePoint( 409 | valueLength === 1 410 | ? decodeTree[result] & ~BinTrieFlags.VALUE_LENGTH 411 | : decodeTree[result + 1], 412 | consumed, 413 | ); 414 | if (valueLength === 3) { 415 | // For multi-byte values, we need to emit the second byte. 416 | this.emitCodePoint(decodeTree[result + 2], consumed); 417 | } 418 | 419 | return consumed; 420 | } 421 | 422 | /** 423 | * Signal to the parser that the end of the input was reached. 424 | * 425 | * Remaining data will be emitted and relevant errors will be produced. 426 | * 427 | * @returns The number of characters consumed. 428 | */ 429 | end(): number { 430 | switch (this.state) { 431 | case EntityDecoderState.NamedEntity: { 432 | // Emit a named entity if we have one. 433 | return this.result !== 0 && 434 | (this.decodeMode !== DecodingMode.Attribute || 435 | this.result === this.treeIndex) 436 | ? this.emitNotTerminatedNamedEntity() 437 | : 0; 438 | } 439 | // Otherwise, emit a numeric entity if we have one. 440 | case EntityDecoderState.NumericDecimal: { 441 | return this.emitNumericEntity(0, 2); 442 | } 443 | case EntityDecoderState.NumericHex: { 444 | return this.emitNumericEntity(0, 3); 445 | } 446 | case EntityDecoderState.NumericStart: { 447 | this.errors?.absenceOfDigitsInNumericCharacterReference( 448 | this.consumed, 449 | ); 450 | return 0; 451 | } 452 | case EntityDecoderState.EntityStart: { 453 | // Return 0 if we have no entity. 454 | return 0; 455 | } 456 | } 457 | } 458 | } 459 | 460 | /** 461 | * Creates a function that decodes entities in a string. 462 | * 463 | * @param decodeTree The decode tree. 464 | * @returns A function that decodes entities in a string. 465 | */ 466 | function getDecoder(decodeTree: Uint16Array) { 467 | let returnValue = ""; 468 | const decoder = new EntityDecoder( 469 | decodeTree, 470 | (data) => (returnValue += fromCodePoint(data)), 471 | ); 472 | 473 | return function decodeWithTrie( 474 | input: string, 475 | decodeMode: DecodingMode, 476 | ): string { 477 | let lastIndex = 0; 478 | let offset = 0; 479 | 480 | while ((offset = input.indexOf("&", offset)) >= 0) { 481 | returnValue += input.slice(lastIndex, offset); 482 | 483 | decoder.startEntity(decodeMode); 484 | 485 | const length = decoder.write( 486 | input, 487 | // Skip the "&" 488 | offset + 1, 489 | ); 490 | 491 | if (length < 0) { 492 | lastIndex = offset + decoder.end(); 493 | break; 494 | } 495 | 496 | lastIndex = offset + length; 497 | // If `length` is 0, skip the current `&` and continue. 498 | offset = length === 0 ? lastIndex + 1 : lastIndex; 499 | } 500 | 501 | const result = returnValue + input.slice(lastIndex); 502 | 503 | // Make sure we don't keep a reference to the final string. 504 | returnValue = ""; 505 | 506 | return result; 507 | }; 508 | } 509 | 510 | /** 511 | * Determines the branch of the current node that is taken given the current 512 | * character. This function is used to traverse the trie. 513 | * 514 | * @param decodeTree The trie. 515 | * @param current The current node. 516 | * @param nodeIdx The index right after the current node and its value. 517 | * @param char The current character. 518 | * @returns The index of the next node, or -1 if no branch is taken. 519 | */ 520 | export function determineBranch( 521 | decodeTree: Uint16Array, 522 | current: number, 523 | nodeIndex: number, 524 | char: number, 525 | ): number { 526 | const branchCount = (current & BinTrieFlags.BRANCH_LENGTH) >> 7; 527 | const jumpOffset = current & BinTrieFlags.JUMP_TABLE; 528 | 529 | // Case 1: Single branch encoded in jump offset 530 | if (branchCount === 0) { 531 | return jumpOffset !== 0 && char === jumpOffset ? nodeIndex : -1; 532 | } 533 | 534 | // Case 2: Multiple branches encoded in jump table 535 | if (jumpOffset) { 536 | const value = char - jumpOffset; 537 | 538 | return value < 0 || value >= branchCount 539 | ? -1 540 | : decodeTree[nodeIndex + value] - 1; 541 | } 542 | 543 | // Case 3: Multiple branches encoded in dictionary 544 | 545 | // Binary search for the character. 546 | let lo = nodeIndex; 547 | let hi = lo + branchCount - 1; 548 | 549 | while (lo <= hi) { 550 | const mid = (lo + hi) >>> 1; 551 | const midValue = decodeTree[mid]; 552 | 553 | if (midValue < char) { 554 | lo = mid + 1; 555 | } else if (midValue > char) { 556 | hi = mid - 1; 557 | } else { 558 | return decodeTree[mid + branchCount]; 559 | } 560 | } 561 | 562 | return -1; 563 | } 564 | 565 | const htmlDecoder = /* #__PURE__ */ getDecoder(htmlDecodeTree); 566 | const xmlDecoder = /* #__PURE__ */ getDecoder(xmlDecodeTree); 567 | 568 | /** 569 | * Decodes an HTML string. 570 | * 571 | * @param htmlString The string to decode. 572 | * @param mode The decoding mode. 573 | * @returns The decoded string. 574 | */ 575 | export function decodeHTML( 576 | htmlString: string, 577 | mode: DecodingMode = DecodingMode.Legacy, 578 | ): string { 579 | return htmlDecoder(htmlString, mode); 580 | } 581 | 582 | /** 583 | * Decodes an HTML string in an attribute. 584 | * 585 | * @param htmlAttribute The string to decode. 586 | * @returns The decoded string. 587 | */ 588 | export function decodeHTMLAttribute(htmlAttribute: string): string { 589 | return htmlDecoder(htmlAttribute, DecodingMode.Attribute); 590 | } 591 | 592 | /** 593 | * Decodes an HTML string, requiring all entities to be terminated by a semicolon. 594 | * 595 | * @param htmlString The string to decode. 596 | * @returns The decoded string. 597 | */ 598 | export function decodeHTMLStrict(htmlString: string): string { 599 | return htmlDecoder(htmlString, DecodingMode.Strict); 600 | } 601 | 602 | /** 603 | * Decodes an XML string, requiring all entities to be terminated by a semicolon. 604 | * 605 | * @param xmlString The string to decode. 606 | * @returns The decoded string. 607 | */ 608 | export function decodeXML(xmlString: string): string { 609 | return xmlDecoder(xmlString, DecodingMode.Strict); 610 | } 611 | 612 | // Re-export for use by eg. htmlparser2 613 | export { htmlDecodeTree } from "./generated/decode-data-html.js"; 614 | export { xmlDecodeTree } from "./generated/decode-data-xml.js"; 615 | 616 | export { 617 | decodeCodePoint, 618 | replaceCodePoint, 619 | fromCodePoint, 620 | } from "./decode-codepoint.js"; 621 | -------------------------------------------------------------------------------- /src/encode.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import * as entities from "./index.js"; 3 | 4 | describe("Encode->decode test", () => { 5 | const testcases = [ 6 | { 7 | input: "asdf & ÿ ü '", 8 | xml: "asdf & ÿ ü '", 9 | html: "asdf & ÿ ü '", 10 | }, 11 | { 12 | input: "&", 13 | xml: "&#38;", 14 | html: "&#38;", 15 | }, 16 | ]; 17 | 18 | for (const { input, xml, html } of testcases) { 19 | const encodedXML = entities.encodeXML(input); 20 | it(`should XML encode ${input}`, () => expect(encodedXML).toBe(xml)); 21 | it(`should default to XML encode ${input}`, () => 22 | expect(entities.encode(input)).toBe(xml)); 23 | it(`should XML decode ${encodedXML}`, () => 24 | expect(entities.decodeXML(encodedXML)).toBe(input)); 25 | it(`should default to XML encode ${encodedXML}`, () => 26 | expect(entities.decode(encodedXML)).toBe(input)); 27 | it(`should default strict to XML encode ${encodedXML}`, () => 28 | expect(entities.decodeStrict(encodedXML)).toBe(input)); 29 | 30 | const encodedHTML5 = entities.encodeHTML5(input); 31 | it(`should HTML5 encode ${input}`, () => 32 | expect(encodedHTML5).toBe(html)); 33 | it(`should HTML5 decode ${encodedHTML5}`, () => 34 | expect(entities.decodeHTML(encodedHTML5)).toBe(input)); 35 | it("should encode emojis", () => 36 | expect(entities.encodeHTML5("😄🍾🥳💥😇")).toBe( 37 | "😄🍾🥳💥😇", 38 | )); 39 | } 40 | 41 | it("should encode data URIs (issue #16)", () => { 42 | const data = 43 | "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAALAAABAAEAAAIBRAA7"; 44 | expect(entities.decode(entities.encode(data))).toBe(data); 45 | }); 46 | 47 | it("should HTML encode all ASCII characters", () => { 48 | for (let index = 0; index < 128; index++) { 49 | const char = String.fromCharCode(index); 50 | const encoded = entities.encodeHTML(char); 51 | const decoded = entities.decodeHTML(encoded); 52 | expect(decoded).toBe(char); 53 | } 54 | }); 55 | 56 | it("should encode trailing parts of entities", () => 57 | expect(entities.encodeHTML("\uD835")).toBe("�")); 58 | 59 | it("should encode surrogate pair with first surrogate equivalent of entity, without corresponding entity", () => 60 | expect(entities.encodeHTML("\u{1D4A4}")).toBe("𝒤")); 61 | }); 62 | 63 | describe("encodeNonAsciiHTML", () => { 64 | it("should encode all non-ASCII characters", () => 65 | expect(entities.encodeNonAsciiHTML(" #123! übermaßen")).toBe( 66 | "<test> #123! übermaßen", 67 | )); 68 | 69 | it("should encode emojis", () => 70 | expect(entities.encodeNonAsciiHTML("😄🍾🥳💥😇")).toBe( 71 | "😄🍾🥳💥😇", 72 | )); 73 | 74 | it("should encode chars above surrogates", () => 75 | expect(entities.encodeNonAsciiHTML("♒️♓️♈️♉️♊️♋️♌️♍️♎️♏️♐️♑️")).toBe( 76 | "♒️♓️♈️♉️♊️♋️♌️♍️♎️♏️♐️♑️", 77 | )); 78 | }); 79 | -------------------------------------------------------------------------------- /src/encode.ts: -------------------------------------------------------------------------------- 1 | import { htmlTrie } from "./generated/encode-html.js"; 2 | import { xmlReplacer, getCodePoint } from "./escape.js"; 3 | 4 | const htmlReplacer = /[\t\n\f!-,./:-@[-`{-}\u0080-\uFFFF]/g; 5 | 6 | /** 7 | * Encodes all characters in the input using HTML entities. This includes 8 | * characters that are valid ASCII characters in HTML documents, such as `#`. 9 | * 10 | * To get a more compact output, consider using the `encodeNonAsciiHTML` 11 | * function, which will only encode characters that are not valid in HTML 12 | * documents, as well as non-ASCII characters. 13 | * 14 | * If a character has no equivalent entity, a numeric hexadecimal reference 15 | * (eg. `ü`) will be used. 16 | */ 17 | export function encodeHTML(input: string): string { 18 | return encodeHTMLTrieRe(htmlReplacer, input); 19 | } 20 | /** 21 | * Encodes all non-ASCII characters, as well as characters not valid in HTML 22 | * documents using HTML entities. This function will not encode characters that 23 | * are valid in HTML documents, such as `#`. 24 | * 25 | * If a character has no equivalent entity, a numeric hexadecimal reference 26 | * (eg. `ü`) will be used. 27 | */ 28 | export function encodeNonAsciiHTML(input: string): string { 29 | return encodeHTMLTrieRe(xmlReplacer, input); 30 | } 31 | 32 | function encodeHTMLTrieRe(regExp: RegExp, input: string): string { 33 | let returnValue = ""; 34 | let lastIndex = 0; 35 | let match; 36 | 37 | while ((match = regExp.exec(input)) !== null) { 38 | const { index } = match; 39 | returnValue += input.substring(lastIndex, index); 40 | const char = input.charCodeAt(index); 41 | let next = htmlTrie.get(char); 42 | 43 | if (typeof next === "object") { 44 | // We are in a branch. Try to match the next char. 45 | if (index + 1 < input.length) { 46 | const nextChar = input.charCodeAt(index + 1); 47 | const value = 48 | typeof next.n === "number" 49 | ? next.n === nextChar 50 | ? next.o 51 | : undefined 52 | : next.n.get(nextChar); 53 | 54 | if (value !== undefined) { 55 | returnValue += value; 56 | lastIndex = regExp.lastIndex += 1; 57 | continue; 58 | } 59 | } 60 | 61 | next = next.v; 62 | } 63 | 64 | // We might have a tree node without a value; skip and use a numeric entity. 65 | if (next === undefined) { 66 | const cp = getCodePoint(input, index); 67 | returnValue += `&#x${cp.toString(16)};`; 68 | // Increase by 1 if we have a surrogate pair 69 | lastIndex = regExp.lastIndex += Number(cp !== char); 70 | } else { 71 | returnValue += next; 72 | lastIndex = index + 1; 73 | } 74 | } 75 | 76 | return returnValue + input.substr(lastIndex); 77 | } 78 | -------------------------------------------------------------------------------- /src/escape.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import * as entities from "./index.js"; 3 | 4 | describe("escape HTML", () => { 5 | it("should escape HTML attribute values", () => 6 | expect(entities.escapeAttribute(' & value \u00A0!')).toBe( 7 | " & value  !", 8 | )); 9 | 10 | it("should escape HTML text", () => 11 | expect(entities.escapeText(' & value \u00A0!')).toBe( 12 | '<a " text > & value  !', 13 | )); 14 | }); 15 | -------------------------------------------------------------------------------- /src/escape.ts: -------------------------------------------------------------------------------- 1 | export const xmlReplacer: RegExp = /["$&'<>\u0080-\uFFFF]/g; 2 | 3 | const xmlCodeMap = new Map([ 4 | [34, """], 5 | [38, "&"], 6 | [39, "'"], 7 | [60, "<"], 8 | [62, ">"], 9 | ]); 10 | 11 | // For compatibility with node < 4, we wrap `codePointAt` 12 | export const getCodePoint: (c: string, index: number) => number = 13 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 14 | String.prototype.codePointAt == null 15 | ? (c: string, index: number): number => 16 | (c.charCodeAt(index) & 0xfc_00) === 0xd8_00 17 | ? (c.charCodeAt(index) - 0xd8_00) * 0x4_00 + 18 | c.charCodeAt(index + 1) - 19 | 0xdc_00 + 20 | 0x1_00_00 21 | : c.charCodeAt(index) 22 | : // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae 23 | (input: string, index: number): number => input.codePointAt(index)!; 24 | 25 | /** 26 | * Encodes all non-ASCII characters, as well as characters not valid in XML 27 | * documents using XML entities. 28 | * 29 | * If a character has no equivalent entity, a 30 | * numeric hexadecimal reference (eg. `ü`) will be used. 31 | */ 32 | export function encodeXML(input: string): string { 33 | let returnValue = ""; 34 | let lastIndex = 0; 35 | let match; 36 | 37 | while ((match = xmlReplacer.exec(input)) !== null) { 38 | const { index } = match; 39 | const char = input.charCodeAt(index); 40 | const next = xmlCodeMap.get(char); 41 | 42 | if (next === undefined) { 43 | returnValue += `${input.substring(lastIndex, index)}&#x${getCodePoint( 44 | input, 45 | index, 46 | ).toString(16)};`; 47 | // Increase by 1 if we have a surrogate pair 48 | lastIndex = xmlReplacer.lastIndex += Number( 49 | (char & 0xfc_00) === 0xd8_00, 50 | ); 51 | } else { 52 | returnValue += input.substring(lastIndex, index) + next; 53 | lastIndex = index + 1; 54 | } 55 | } 56 | 57 | return returnValue + input.substr(lastIndex); 58 | } 59 | 60 | /** 61 | * Encodes all non-ASCII characters, as well as characters not valid in XML 62 | * documents using numeric hexadecimal reference (eg. `ü`). 63 | * 64 | * Have a look at `escapeUTF8` if you want a more concise output at the expense 65 | * of reduced transportability. 66 | * 67 | * @param data String to escape. 68 | */ 69 | export const escape: typeof encodeXML = encodeXML; 70 | 71 | /** 72 | * Creates a function that escapes all characters matched by the given regular 73 | * expression using the given map of characters to escape to their entities. 74 | * 75 | * @param regex Regular expression to match characters to escape. 76 | * @param map Map of characters to escape to their entities. 77 | * 78 | * @returns Function that escapes all characters matched by the given regular 79 | * expression using the given map of characters to escape to their entities. 80 | */ 81 | function getEscaper( 82 | regex: RegExp, 83 | map: Map, 84 | ): (data: string) => string { 85 | return function escape(data: string): string { 86 | let match; 87 | let lastIndex = 0; 88 | let result = ""; 89 | 90 | while ((match = regex.exec(data))) { 91 | if (lastIndex !== match.index) { 92 | result += data.substring(lastIndex, match.index); 93 | } 94 | 95 | // We know that this character will be in the map. 96 | result += map.get(match[0].charCodeAt(0))!; 97 | 98 | // Every match will be of length 1 99 | lastIndex = match.index + 1; 100 | } 101 | 102 | return result + data.substring(lastIndex); 103 | }; 104 | } 105 | 106 | /** 107 | * Encodes all characters not valid in XML documents using XML entities. 108 | * 109 | * Note that the output will be character-set dependent. 110 | * 111 | * @param data String to escape. 112 | */ 113 | export const escapeUTF8: (data: string) => string = /* #__PURE__ */ getEscaper( 114 | /["&'<>]/g, 115 | xmlCodeMap, 116 | ); 117 | 118 | /** 119 | * Encodes all characters that have to be escaped in HTML attributes, 120 | * following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}. 121 | * 122 | * @param data String to escape. 123 | */ 124 | export const escapeAttribute: (data: string) => string = 125 | /* #__PURE__ */ getEscaper( 126 | /["&\u00A0]/g, 127 | new Map([ 128 | [34, """], 129 | [38, "&"], 130 | [160, " "], 131 | ]), 132 | ); 133 | 134 | /** 135 | * Encodes all characters that have to be escaped in HTML text, 136 | * following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}. 137 | * 138 | * @param data String to escape. 139 | */ 140 | export const escapeText: (data: string) => string = /* #__PURE__ */ getEscaper( 141 | /[&<>\u00A0]/g, 142 | new Map([ 143 | [38, "&"], 144 | [60, "<"], 145 | [62, ">"], 146 | [160, " "], 147 | ]), 148 | ); 149 | -------------------------------------------------------------------------------- /src/generated/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "multiline-comment-style": 0, 4 | "capitalized-comments": 0, 5 | "unicorn/escape-case": 0, 6 | "unicorn/no-hex-escape": 0, 7 | "unicorn/numeric-separators-style": 0, 8 | "unicorn/prefer-spread": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/generated/decode-data-html.ts: -------------------------------------------------------------------------------- 1 | // Generated using scripts/write-decode-map.ts 2 | 3 | export const htmlDecodeTree: Uint16Array = /* #__PURE__ */ new Uint16Array( 4 | // prettier-ignore 5 | /* #__PURE__ */ "\u1d41<\xd5\u0131\u028a\u049d\u057b\u05d0\u0675\u06de\u07a2\u07d6\u080f\u0a4a\u0a91\u0da1\u0e6d\u0f09\u0f26\u10ca\u1228\u12e1\u1415\u149d\u14c3\u14df\u1525\0\0\0\0\0\0\u156b\u16cd\u198d\u1c12\u1ddd\u1f7e\u2060\u21b0\u228d\u23c0\u23fb\u2442\u2824\u2912\u2d08\u2e48\u2fce\u3016\u32ba\u3639\u37ac\u38fe\u3a28\u3a71\u3ae0\u3b2e\u0800EMabcfglmnoprstu\\bfms\x7f\x84\x8b\x90\x95\x98\xa6\xb3\xb9\xc8\xcflig\u803b\xc6\u40c6P\u803b&\u4026cute\u803b\xc1\u40c1reve;\u4102\u0100iyx}rc\u803b\xc2\u40c2;\u4410r;\uc000\ud835\udd04rave\u803b\xc0\u40c0pha;\u4391acr;\u4100d;\u6a53\u0100gp\x9d\xa1on;\u4104f;\uc000\ud835\udd38plyFunction;\u6061ing\u803b\xc5\u40c5\u0100cs\xbe\xc3r;\uc000\ud835\udc9cign;\u6254ilde\u803b\xc3\u40c3ml\u803b\xc4\u40c4\u0400aceforsu\xe5\xfb\xfe\u0117\u011c\u0122\u0127\u012a\u0100cr\xea\xf2kslash;\u6216\u0176\xf6\xf8;\u6ae7ed;\u6306y;\u4411\u0180crt\u0105\u010b\u0114ause;\u6235noullis;\u612ca;\u4392r;\uc000\ud835\udd05pf;\uc000\ud835\udd39eve;\u42d8c\xf2\u0113mpeq;\u624e\u0700HOacdefhilorsu\u014d\u0151\u0156\u0180\u019e\u01a2\u01b5\u01b7\u01ba\u01dc\u0215\u0273\u0278\u027ecy;\u4427PY\u803b\xa9\u40a9\u0180cpy\u015d\u0162\u017aute;\u4106\u0100;i\u0167\u0168\u62d2talDifferentialD;\u6145leys;\u612d\u0200aeio\u0189\u018e\u0194\u0198ron;\u410cdil\u803b\xc7\u40c7rc;\u4108nint;\u6230ot;\u410a\u0100dn\u01a7\u01adilla;\u40b8terDot;\u40b7\xf2\u017fi;\u43a7rcle\u0200DMPT\u01c7\u01cb\u01d1\u01d6ot;\u6299inus;\u6296lus;\u6295imes;\u6297o\u0100cs\u01e2\u01f8kwiseContourIntegral;\u6232eCurly\u0100DQ\u0203\u020foubleQuote;\u601duote;\u6019\u0200lnpu\u021e\u0228\u0247\u0255on\u0100;e\u0225\u0226\u6237;\u6a74\u0180git\u022f\u0236\u023aruent;\u6261nt;\u622fourIntegral;\u622e\u0100fr\u024c\u024e;\u6102oduct;\u6210nterClockwiseContourIntegral;\u6233oss;\u6a2fcr;\uc000\ud835\udc9ep\u0100;C\u0284\u0285\u62d3ap;\u624d\u0580DJSZacefios\u02a0\u02ac\u02b0\u02b4\u02b8\u02cb\u02d7\u02e1\u02e6\u0333\u048d\u0100;o\u0179\u02a5trahd;\u6911cy;\u4402cy;\u4405cy;\u440f\u0180grs\u02bf\u02c4\u02c7ger;\u6021r;\u61a1hv;\u6ae4\u0100ay\u02d0\u02d5ron;\u410e;\u4414l\u0100;t\u02dd\u02de\u6207a;\u4394r;\uc000\ud835\udd07\u0100af\u02eb\u0327\u0100cm\u02f0\u0322ritical\u0200ADGT\u0300\u0306\u0316\u031ccute;\u40b4o\u0174\u030b\u030d;\u42d9bleAcute;\u42ddrave;\u4060ilde;\u42dcond;\u62c4ferentialD;\u6146\u0470\u033d\0\0\0\u0342\u0354\0\u0405f;\uc000\ud835\udd3b\u0180;DE\u0348\u0349\u034d\u40a8ot;\u60dcqual;\u6250ble\u0300CDLRUV\u0363\u0372\u0382\u03cf\u03e2\u03f8ontourIntegra\xec\u0239o\u0274\u0379\0\0\u037b\xbb\u0349nArrow;\u61d3\u0100eo\u0387\u03a4ft\u0180ART\u0390\u0396\u03a1rrow;\u61d0ightArrow;\u61d4e\xe5\u02cang\u0100LR\u03ab\u03c4eft\u0100AR\u03b3\u03b9rrow;\u67f8ightArrow;\u67faightArrow;\u67f9ight\u0100AT\u03d8\u03derrow;\u61d2ee;\u62a8p\u0241\u03e9\0\0\u03efrrow;\u61d1ownArrow;\u61d5erticalBar;\u6225n\u0300ABLRTa\u0412\u042a\u0430\u045e\u047f\u037crrow\u0180;BU\u041d\u041e\u0422\u6193ar;\u6913pArrow;\u61f5reve;\u4311eft\u02d2\u043a\0\u0446\0\u0450ightVector;\u6950eeVector;\u695eector\u0100;B\u0459\u045a\u61bdar;\u6956ight\u01d4\u0467\0\u0471eeVector;\u695fector\u0100;B\u047a\u047b\u61c1ar;\u6957ee\u0100;A\u0486\u0487\u62a4rrow;\u61a7\u0100ct\u0492\u0497r;\uc000\ud835\udc9frok;\u4110\u0800NTacdfglmopqstux\u04bd\u04c0\u04c4\u04cb\u04de\u04e2\u04e7\u04ee\u04f5\u0521\u052f\u0536\u0552\u055d\u0560\u0565G;\u414aH\u803b\xd0\u40d0cute\u803b\xc9\u40c9\u0180aiy\u04d2\u04d7\u04dcron;\u411arc\u803b\xca\u40ca;\u442dot;\u4116r;\uc000\ud835\udd08rave\u803b\xc8\u40c8ement;\u6208\u0100ap\u04fa\u04fecr;\u4112ty\u0253\u0506\0\0\u0512mallSquare;\u65fberySmallSquare;\u65ab\u0100gp\u0526\u052aon;\u4118f;\uc000\ud835\udd3csilon;\u4395u\u0100ai\u053c\u0549l\u0100;T\u0542\u0543\u6a75ilde;\u6242librium;\u61cc\u0100ci\u0557\u055ar;\u6130m;\u6a73a;\u4397ml\u803b\xcb\u40cb\u0100ip\u056a\u056fsts;\u6203onentialE;\u6147\u0280cfios\u0585\u0588\u058d\u05b2\u05ccy;\u4424r;\uc000\ud835\udd09lled\u0253\u0597\0\0\u05a3mallSquare;\u65fcerySmallSquare;\u65aa\u0370\u05ba\0\u05bf\0\0\u05c4f;\uc000\ud835\udd3dAll;\u6200riertrf;\u6131c\xf2\u05cb\u0600JTabcdfgorst\u05e8\u05ec\u05ef\u05fa\u0600\u0612\u0616\u061b\u061d\u0623\u066c\u0672cy;\u4403\u803b>\u403emma\u0100;d\u05f7\u05f8\u4393;\u43dcreve;\u411e\u0180eiy\u0607\u060c\u0610dil;\u4122rc;\u411c;\u4413ot;\u4120r;\uc000\ud835\udd0a;\u62d9pf;\uc000\ud835\udd3eeater\u0300EFGLST\u0635\u0644\u064e\u0656\u065b\u0666qual\u0100;L\u063e\u063f\u6265ess;\u62dbullEqual;\u6267reater;\u6aa2ess;\u6277lantEqual;\u6a7eilde;\u6273cr;\uc000\ud835\udca2;\u626b\u0400Aacfiosu\u0685\u068b\u0696\u069b\u069e\u06aa\u06be\u06caRDcy;\u442a\u0100ct\u0690\u0694ek;\u42c7;\u405eirc;\u4124r;\u610clbertSpace;\u610b\u01f0\u06af\0\u06b2f;\u610dizontalLine;\u6500\u0100ct\u06c3\u06c5\xf2\u06a9rok;\u4126mp\u0144\u06d0\u06d8ownHum\xf0\u012fqual;\u624f\u0700EJOacdfgmnostu\u06fa\u06fe\u0703\u0707\u070e\u071a\u071e\u0721\u0728\u0744\u0778\u078b\u078f\u0795cy;\u4415lig;\u4132cy;\u4401cute\u803b\xcd\u40cd\u0100iy\u0713\u0718rc\u803b\xce\u40ce;\u4418ot;\u4130r;\u6111rave\u803b\xcc\u40cc\u0180;ap\u0720\u072f\u073f\u0100cg\u0734\u0737r;\u412ainaryI;\u6148lie\xf3\u03dd\u01f4\u0749\0\u0762\u0100;e\u074d\u074e\u622c\u0100gr\u0753\u0758ral;\u622bsection;\u62c2isible\u0100CT\u076c\u0772omma;\u6063imes;\u6062\u0180gpt\u077f\u0783\u0788on;\u412ef;\uc000\ud835\udd40a;\u4399cr;\u6110ilde;\u4128\u01eb\u079a\0\u079ecy;\u4406l\u803b\xcf\u40cf\u0280cfosu\u07ac\u07b7\u07bc\u07c2\u07d0\u0100iy\u07b1\u07b5rc;\u4134;\u4419r;\uc000\ud835\udd0dpf;\uc000\ud835\udd41\u01e3\u07c7\0\u07ccr;\uc000\ud835\udca5rcy;\u4408kcy;\u4404\u0380HJacfos\u07e4\u07e8\u07ec\u07f1\u07fd\u0802\u0808cy;\u4425cy;\u440cppa;\u439a\u0100ey\u07f6\u07fbdil;\u4136;\u441ar;\uc000\ud835\udd0epf;\uc000\ud835\udd42cr;\uc000\ud835\udca6\u0580JTaceflmost\u0825\u0829\u082c\u0850\u0863\u09b3\u09b8\u09c7\u09cd\u0a37\u0a47cy;\u4409\u803b<\u403c\u0280cmnpr\u0837\u083c\u0841\u0844\u084dute;\u4139bda;\u439bg;\u67ealacetrf;\u6112r;\u619e\u0180aey\u0857\u085c\u0861ron;\u413ddil;\u413b;\u441b\u0100fs\u0868\u0970t\u0500ACDFRTUVar\u087e\u08a9\u08b1\u08e0\u08e6\u08fc\u092f\u095b\u0390\u096a\u0100nr\u0883\u088fgleBracket;\u67e8row\u0180;BR\u0899\u089a\u089e\u6190ar;\u61e4ightArrow;\u61c6eiling;\u6308o\u01f5\u08b7\0\u08c3bleBracket;\u67e6n\u01d4\u08c8\0\u08d2eeVector;\u6961ector\u0100;B\u08db\u08dc\u61c3ar;\u6959loor;\u630aight\u0100AV\u08ef\u08f5rrow;\u6194ector;\u694e\u0100er\u0901\u0917e\u0180;AV\u0909\u090a\u0910\u62a3rrow;\u61a4ector;\u695aiangle\u0180;BE\u0924\u0925\u0929\u62b2ar;\u69cfqual;\u62b4p\u0180DTV\u0937\u0942\u094cownVector;\u6951eeVector;\u6960ector\u0100;B\u0956\u0957\u61bfar;\u6958ector\u0100;B\u0965\u0966\u61bcar;\u6952ight\xe1\u039cs\u0300EFGLST\u097e\u098b\u0995\u099d\u09a2\u09adqualGreater;\u62daullEqual;\u6266reater;\u6276ess;\u6aa1lantEqual;\u6a7dilde;\u6272r;\uc000\ud835\udd0f\u0100;e\u09bd\u09be\u62d8ftarrow;\u61daidot;\u413f\u0180npw\u09d4\u0a16\u0a1bg\u0200LRlr\u09de\u09f7\u0a02\u0a10eft\u0100AR\u09e6\u09ecrrow;\u67f5ightArrow;\u67f7ightArrow;\u67f6eft\u0100ar\u03b3\u0a0aight\xe1\u03bfight\xe1\u03caf;\uc000\ud835\udd43er\u0100LR\u0a22\u0a2ceftArrow;\u6199ightArrow;\u6198\u0180cht\u0a3e\u0a40\u0a42\xf2\u084c;\u61b0rok;\u4141;\u626a\u0400acefiosu\u0a5a\u0a5d\u0a60\u0a77\u0a7c\u0a85\u0a8b\u0a8ep;\u6905y;\u441c\u0100dl\u0a65\u0a6fiumSpace;\u605flintrf;\u6133r;\uc000\ud835\udd10nusPlus;\u6213pf;\uc000\ud835\udd44c\xf2\u0a76;\u439c\u0480Jacefostu\u0aa3\u0aa7\u0aad\u0ac0\u0b14\u0b19\u0d91\u0d97\u0d9ecy;\u440acute;\u4143\u0180aey\u0ab4\u0ab9\u0aberon;\u4147dil;\u4145;\u441d\u0180gsw\u0ac7\u0af0\u0b0eative\u0180MTV\u0ad3\u0adf\u0ae8ediumSpace;\u600bhi\u0100cn\u0ae6\u0ad8\xeb\u0ad9eryThi\xee\u0ad9ted\u0100GL\u0af8\u0b06reaterGreate\xf2\u0673essLes\xf3\u0a48Line;\u400ar;\uc000\ud835\udd11\u0200Bnpt\u0b22\u0b28\u0b37\u0b3areak;\u6060BreakingSpace;\u40a0f;\u6115\u0680;CDEGHLNPRSTV\u0b55\u0b56\u0b6a\u0b7c\u0ba1\u0beb\u0c04\u0c5e\u0c84\u0ca6\u0cd8\u0d61\u0d85\u6aec\u0100ou\u0b5b\u0b64ngruent;\u6262pCap;\u626doubleVerticalBar;\u6226\u0180lqx\u0b83\u0b8a\u0b9bement;\u6209ual\u0100;T\u0b92\u0b93\u6260ilde;\uc000\u2242\u0338ists;\u6204reater\u0380;EFGLST\u0bb6\u0bb7\u0bbd\u0bc9\u0bd3\u0bd8\u0be5\u626fqual;\u6271ullEqual;\uc000\u2267\u0338reater;\uc000\u226b\u0338ess;\u6279lantEqual;\uc000\u2a7e\u0338ilde;\u6275ump\u0144\u0bf2\u0bfdownHump;\uc000\u224e\u0338qual;\uc000\u224f\u0338e\u0100fs\u0c0a\u0c27tTriangle\u0180;BE\u0c1a\u0c1b\u0c21\u62eaar;\uc000\u29cf\u0338qual;\u62ecs\u0300;EGLST\u0c35\u0c36\u0c3c\u0c44\u0c4b\u0c58\u626equal;\u6270reater;\u6278ess;\uc000\u226a\u0338lantEqual;\uc000\u2a7d\u0338ilde;\u6274ested\u0100GL\u0c68\u0c79reaterGreater;\uc000\u2aa2\u0338essLess;\uc000\u2aa1\u0338recedes\u0180;ES\u0c92\u0c93\u0c9b\u6280qual;\uc000\u2aaf\u0338lantEqual;\u62e0\u0100ei\u0cab\u0cb9verseElement;\u620cghtTriangle\u0180;BE\u0ccb\u0ccc\u0cd2\u62ebar;\uc000\u29d0\u0338qual;\u62ed\u0100qu\u0cdd\u0d0cuareSu\u0100bp\u0ce8\u0cf9set\u0100;E\u0cf0\u0cf3\uc000\u228f\u0338qual;\u62e2erset\u0100;E\u0d03\u0d06\uc000\u2290\u0338qual;\u62e3\u0180bcp\u0d13\u0d24\u0d4eset\u0100;E\u0d1b\u0d1e\uc000\u2282\u20d2qual;\u6288ceeds\u0200;EST\u0d32\u0d33\u0d3b\u0d46\u6281qual;\uc000\u2ab0\u0338lantEqual;\u62e1ilde;\uc000\u227f\u0338erset\u0100;E\u0d58\u0d5b\uc000\u2283\u20d2qual;\u6289ilde\u0200;EFT\u0d6e\u0d6f\u0d75\u0d7f\u6241qual;\u6244ullEqual;\u6247ilde;\u6249erticalBar;\u6224cr;\uc000\ud835\udca9ilde\u803b\xd1\u40d1;\u439d\u0700Eacdfgmoprstuv\u0dbd\u0dc2\u0dc9\u0dd5\u0ddb\u0de0\u0de7\u0dfc\u0e02\u0e20\u0e22\u0e32\u0e3f\u0e44lig;\u4152cute\u803b\xd3\u40d3\u0100iy\u0dce\u0dd3rc\u803b\xd4\u40d4;\u441eblac;\u4150r;\uc000\ud835\udd12rave\u803b\xd2\u40d2\u0180aei\u0dee\u0df2\u0df6cr;\u414cga;\u43a9cron;\u439fpf;\uc000\ud835\udd46enCurly\u0100DQ\u0e0e\u0e1aoubleQuote;\u601cuote;\u6018;\u6a54\u0100cl\u0e27\u0e2cr;\uc000\ud835\udcaaash\u803b\xd8\u40d8i\u016c\u0e37\u0e3cde\u803b\xd5\u40d5es;\u6a37ml\u803b\xd6\u40d6er\u0100BP\u0e4b\u0e60\u0100ar\u0e50\u0e53r;\u603eac\u0100ek\u0e5a\u0e5c;\u63deet;\u63b4arenthesis;\u63dc\u0480acfhilors\u0e7f\u0e87\u0e8a\u0e8f\u0e92\u0e94\u0e9d\u0eb0\u0efcrtialD;\u6202y;\u441fr;\uc000\ud835\udd13i;\u43a6;\u43a0usMinus;\u40b1\u0100ip\u0ea2\u0eadncareplan\xe5\u069df;\u6119\u0200;eio\u0eb9\u0eba\u0ee0\u0ee4\u6abbcedes\u0200;EST\u0ec8\u0ec9\u0ecf\u0eda\u627aqual;\u6aaflantEqual;\u627cilde;\u627eme;\u6033\u0100dp\u0ee9\u0eeeuct;\u620fortion\u0100;a\u0225\u0ef9l;\u621d\u0100ci\u0f01\u0f06r;\uc000\ud835\udcab;\u43a8\u0200Ufos\u0f11\u0f16\u0f1b\u0f1fOT\u803b\"\u4022r;\uc000\ud835\udd14pf;\u611acr;\uc000\ud835\udcac\u0600BEacefhiorsu\u0f3e\u0f43\u0f47\u0f60\u0f73\u0fa7\u0faa\u0fad\u1096\u10a9\u10b4\u10bearr;\u6910G\u803b\xae\u40ae\u0180cnr\u0f4e\u0f53\u0f56ute;\u4154g;\u67ebr\u0100;t\u0f5c\u0f5d\u61a0l;\u6916\u0180aey\u0f67\u0f6c\u0f71ron;\u4158dil;\u4156;\u4420\u0100;v\u0f78\u0f79\u611cerse\u0100EU\u0f82\u0f99\u0100lq\u0f87\u0f8eement;\u620builibrium;\u61cbpEquilibrium;\u696fr\xbb\u0f79o;\u43a1ght\u0400ACDFTUVa\u0fc1\u0feb\u0ff3\u1022\u1028\u105b\u1087\u03d8\u0100nr\u0fc6\u0fd2gleBracket;\u67e9row\u0180;BL\u0fdc\u0fdd\u0fe1\u6192ar;\u61e5eftArrow;\u61c4eiling;\u6309o\u01f5\u0ff9\0\u1005bleBracket;\u67e7n\u01d4\u100a\0\u1014eeVector;\u695dector\u0100;B\u101d\u101e\u61c2ar;\u6955loor;\u630b\u0100er\u102d\u1043e\u0180;AV\u1035\u1036\u103c\u62a2rrow;\u61a6ector;\u695biangle\u0180;BE\u1050\u1051\u1055\u62b3ar;\u69d0qual;\u62b5p\u0180DTV\u1063\u106e\u1078ownVector;\u694feeVector;\u695cector\u0100;B\u1082\u1083\u61bear;\u6954ector\u0100;B\u1091\u1092\u61c0ar;\u6953\u0100pu\u109b\u109ef;\u611dndImplies;\u6970ightarrow;\u61db\u0100ch\u10b9\u10bcr;\u611b;\u61b1leDelayed;\u69f4\u0680HOacfhimoqstu\u10e4\u10f1\u10f7\u10fd\u1119\u111e\u1151\u1156\u1161\u1167\u11b5\u11bb\u11bf\u0100Cc\u10e9\u10eeHcy;\u4429y;\u4428FTcy;\u442ccute;\u415a\u0280;aeiy\u1108\u1109\u110e\u1113\u1117\u6abcron;\u4160dil;\u415erc;\u415c;\u4421r;\uc000\ud835\udd16ort\u0200DLRU\u112a\u1134\u113e\u1149ownArrow\xbb\u041eeftArrow\xbb\u089aightArrow\xbb\u0fddpArrow;\u6191gma;\u43a3allCircle;\u6218pf;\uc000\ud835\udd4a\u0272\u116d\0\0\u1170t;\u621aare\u0200;ISU\u117b\u117c\u1189\u11af\u65a1ntersection;\u6293u\u0100bp\u118f\u119eset\u0100;E\u1197\u1198\u628fqual;\u6291erset\u0100;E\u11a8\u11a9\u6290qual;\u6292nion;\u6294cr;\uc000\ud835\udcaear;\u62c6\u0200bcmp\u11c8\u11db\u1209\u120b\u0100;s\u11cd\u11ce\u62d0et\u0100;E\u11cd\u11d5qual;\u6286\u0100ch\u11e0\u1205eeds\u0200;EST\u11ed\u11ee\u11f4\u11ff\u627bqual;\u6ab0lantEqual;\u627dilde;\u627fTh\xe1\u0f8c;\u6211\u0180;es\u1212\u1213\u1223\u62d1rset\u0100;E\u121c\u121d\u6283qual;\u6287et\xbb\u1213\u0580HRSacfhiors\u123e\u1244\u1249\u1255\u125e\u1271\u1276\u129f\u12c2\u12c8\u12d1ORN\u803b\xde\u40deADE;\u6122\u0100Hc\u124e\u1252cy;\u440by;\u4426\u0100bu\u125a\u125c;\u4009;\u43a4\u0180aey\u1265\u126a\u126fron;\u4164dil;\u4162;\u4422r;\uc000\ud835\udd17\u0100ei\u127b\u1289\u01f2\u1280\0\u1287efore;\u6234a;\u4398\u0100cn\u128e\u1298kSpace;\uc000\u205f\u200aSpace;\u6009lde\u0200;EFT\u12ab\u12ac\u12b2\u12bc\u623cqual;\u6243ullEqual;\u6245ilde;\u6248pf;\uc000\ud835\udd4bipleDot;\u60db\u0100ct\u12d6\u12dbr;\uc000\ud835\udcafrok;\u4166\u0ae1\u12f7\u130e\u131a\u1326\0\u132c\u1331\0\0\0\0\0\u1338\u133d\u1377\u1385\0\u13ff\u1404\u140a\u1410\u0100cr\u12fb\u1301ute\u803b\xda\u40dar\u0100;o\u1307\u1308\u619fcir;\u6949r\u01e3\u1313\0\u1316y;\u440eve;\u416c\u0100iy\u131e\u1323rc\u803b\xdb\u40db;\u4423blac;\u4170r;\uc000\ud835\udd18rave\u803b\xd9\u40d9acr;\u416a\u0100di\u1341\u1369er\u0100BP\u1348\u135d\u0100ar\u134d\u1350r;\u405fac\u0100ek\u1357\u1359;\u63dfet;\u63b5arenthesis;\u63ddon\u0100;P\u1370\u1371\u62c3lus;\u628e\u0100gp\u137b\u137fon;\u4172f;\uc000\ud835\udd4c\u0400ADETadps\u1395\u13ae\u13b8\u13c4\u03e8\u13d2\u13d7\u13f3rrow\u0180;BD\u1150\u13a0\u13a4ar;\u6912ownArrow;\u61c5ownArrow;\u6195quilibrium;\u696eee\u0100;A\u13cb\u13cc\u62a5rrow;\u61a5own\xe1\u03f3er\u0100LR\u13de\u13e8eftArrow;\u6196ightArrow;\u6197i\u0100;l\u13f9\u13fa\u43d2on;\u43a5ing;\u416ecr;\uc000\ud835\udcb0ilde;\u4168ml\u803b\xdc\u40dc\u0480Dbcdefosv\u1427\u142c\u1430\u1433\u143e\u1485\u148a\u1490\u1496ash;\u62abar;\u6aeby;\u4412ash\u0100;l\u143b\u143c\u62a9;\u6ae6\u0100er\u1443\u1445;\u62c1\u0180bty\u144c\u1450\u147aar;\u6016\u0100;i\u144f\u1455cal\u0200BLST\u1461\u1465\u146a\u1474ar;\u6223ine;\u407ceparator;\u6758ilde;\u6240ThinSpace;\u600ar;\uc000\ud835\udd19pf;\uc000\ud835\udd4dcr;\uc000\ud835\udcb1dash;\u62aa\u0280cefos\u14a7\u14ac\u14b1\u14b6\u14bcirc;\u4174dge;\u62c0r;\uc000\ud835\udd1apf;\uc000\ud835\udd4ecr;\uc000\ud835\udcb2\u0200fios\u14cb\u14d0\u14d2\u14d8r;\uc000\ud835\udd1b;\u439epf;\uc000\ud835\udd4fcr;\uc000\ud835\udcb3\u0480AIUacfosu\u14f1\u14f5\u14f9\u14fd\u1504\u150f\u1514\u151a\u1520cy;\u442fcy;\u4407cy;\u442ecute\u803b\xdd\u40dd\u0100iy\u1509\u150drc;\u4176;\u442br;\uc000\ud835\udd1cpf;\uc000\ud835\udd50cr;\uc000\ud835\udcb4ml;\u4178\u0400Hacdefos\u1535\u1539\u153f\u154b\u154f\u155d\u1560\u1564cy;\u4416cute;\u4179\u0100ay\u1544\u1549ron;\u417d;\u4417ot;\u417b\u01f2\u1554\0\u155boWidt\xe8\u0ad9a;\u4396r;\u6128pf;\u6124cr;\uc000\ud835\udcb5\u0be1\u1583\u158a\u1590\0\u15b0\u15b6\u15bf\0\0\0\0\u15c6\u15db\u15eb\u165f\u166d\0\u1695\u169b\u16b2\u16b9\0\u16becute\u803b\xe1\u40e1reve;\u4103\u0300;Ediuy\u159c\u159d\u15a1\u15a3\u15a8\u15ad\u623e;\uc000\u223e\u0333;\u623frc\u803b\xe2\u40e2te\u80bb\xb4\u0306;\u4430lig\u803b\xe6\u40e6\u0100;r\xb2\u15ba;\uc000\ud835\udd1erave\u803b\xe0\u40e0\u0100ep\u15ca\u15d6\u0100fp\u15cf\u15d4sym;\u6135\xe8\u15d3ha;\u43b1\u0100ap\u15dfc\u0100cl\u15e4\u15e7r;\u4101g;\u6a3f\u0264\u15f0\0\0\u160a\u0280;adsv\u15fa\u15fb\u15ff\u1601\u1607\u6227nd;\u6a55;\u6a5clope;\u6a58;\u6a5a\u0380;elmrsz\u1618\u1619\u161b\u161e\u163f\u164f\u1659\u6220;\u69a4e\xbb\u1619sd\u0100;a\u1625\u1626\u6221\u0461\u1630\u1632\u1634\u1636\u1638\u163a\u163c\u163e;\u69a8;\u69a9;\u69aa;\u69ab;\u69ac;\u69ad;\u69ae;\u69aft\u0100;v\u1645\u1646\u621fb\u0100;d\u164c\u164d\u62be;\u699d\u0100pt\u1654\u1657h;\u6222\xbb\xb9arr;\u637c\u0100gp\u1663\u1667on;\u4105f;\uc000\ud835\udd52\u0380;Eaeiop\u12c1\u167b\u167d\u1682\u1684\u1687\u168a;\u6a70cir;\u6a6f;\u624ad;\u624bs;\u4027rox\u0100;e\u12c1\u1692\xf1\u1683ing\u803b\xe5\u40e5\u0180cty\u16a1\u16a6\u16a8r;\uc000\ud835\udcb6;\u402amp\u0100;e\u12c1\u16af\xf1\u0288ilde\u803b\xe3\u40e3ml\u803b\xe4\u40e4\u0100ci\u16c2\u16c8onin\xf4\u0272nt;\u6a11\u0800Nabcdefiklnoprsu\u16ed\u16f1\u1730\u173c\u1743\u1748\u1778\u177d\u17e0\u17e6\u1839\u1850\u170d\u193d\u1948\u1970ot;\u6aed\u0100cr\u16f6\u171ek\u0200ceps\u1700\u1705\u170d\u1713ong;\u624cpsilon;\u43f6rime;\u6035im\u0100;e\u171a\u171b\u623dq;\u62cd\u0176\u1722\u1726ee;\u62bded\u0100;g\u172c\u172d\u6305e\xbb\u172drk\u0100;t\u135c\u1737brk;\u63b6\u0100oy\u1701\u1741;\u4431quo;\u601e\u0280cmprt\u1753\u175b\u1761\u1764\u1768aus\u0100;e\u010a\u0109ptyv;\u69b0s\xe9\u170cno\xf5\u0113\u0180ahw\u176f\u1771\u1773;\u43b2;\u6136een;\u626cr;\uc000\ud835\udd1fg\u0380costuvw\u178d\u179d\u17b3\u17c1\u17d5\u17db\u17de\u0180aiu\u1794\u1796\u179a\xf0\u0760rc;\u65efp\xbb\u1371\u0180dpt\u17a4\u17a8\u17adot;\u6a00lus;\u6a01imes;\u6a02\u0271\u17b9\0\0\u17becup;\u6a06ar;\u6605riangle\u0100du\u17cd\u17d2own;\u65bdp;\u65b3plus;\u6a04e\xe5\u1444\xe5\u14adarow;\u690d\u0180ako\u17ed\u1826\u1835\u0100cn\u17f2\u1823k\u0180lst\u17fa\u05ab\u1802ozenge;\u69ebriangle\u0200;dlr\u1812\u1813\u1818\u181d\u65b4own;\u65beeft;\u65c2ight;\u65b8k;\u6423\u01b1\u182b\0\u1833\u01b2\u182f\0\u1831;\u6592;\u65914;\u6593ck;\u6588\u0100eo\u183e\u184d\u0100;q\u1843\u1846\uc000=\u20e5uiv;\uc000\u2261\u20e5t;\u6310\u0200ptwx\u1859\u185e\u1867\u186cf;\uc000\ud835\udd53\u0100;t\u13cb\u1863om\xbb\u13cctie;\u62c8\u0600DHUVbdhmptuv\u1885\u1896\u18aa\u18bb\u18d7\u18db\u18ec\u18ff\u1905\u190a\u1910\u1921\u0200LRlr\u188e\u1890\u1892\u1894;\u6557;\u6554;\u6556;\u6553\u0280;DUdu\u18a1\u18a2\u18a4\u18a6\u18a8\u6550;\u6566;\u6569;\u6564;\u6567\u0200LRlr\u18b3\u18b5\u18b7\u18b9;\u655d;\u655a;\u655c;\u6559\u0380;HLRhlr\u18ca\u18cb\u18cd\u18cf\u18d1\u18d3\u18d5\u6551;\u656c;\u6563;\u6560;\u656b;\u6562;\u655fox;\u69c9\u0200LRlr\u18e4\u18e6\u18e8\u18ea;\u6555;\u6552;\u6510;\u650c\u0280;DUdu\u06bd\u18f7\u18f9\u18fb\u18fd;\u6565;\u6568;\u652c;\u6534inus;\u629flus;\u629eimes;\u62a0\u0200LRlr\u1919\u191b\u191d\u191f;\u655b;\u6558;\u6518;\u6514\u0380;HLRhlr\u1930\u1931\u1933\u1935\u1937\u1939\u193b\u6502;\u656a;\u6561;\u655e;\u653c;\u6524;\u651c\u0100ev\u0123\u1942bar\u803b\xa6\u40a6\u0200ceio\u1951\u1956\u195a\u1960r;\uc000\ud835\udcb7mi;\u604fm\u0100;e\u171a\u171cl\u0180;bh\u1968\u1969\u196b\u405c;\u69c5sub;\u67c8\u016c\u1974\u197el\u0100;e\u1979\u197a\u6022t\xbb\u197ap\u0180;Ee\u012f\u1985\u1987;\u6aae\u0100;q\u06dc\u06db\u0ce1\u19a7\0\u19e8\u1a11\u1a15\u1a32\0\u1a37\u1a50\0\0\u1ab4\0\0\u1ac1\0\0\u1b21\u1b2e\u1b4d\u1b52\0\u1bfd\0\u1c0c\u0180cpr\u19ad\u19b2\u19ddute;\u4107\u0300;abcds\u19bf\u19c0\u19c4\u19ca\u19d5\u19d9\u6229nd;\u6a44rcup;\u6a49\u0100au\u19cf\u19d2p;\u6a4bp;\u6a47ot;\u6a40;\uc000\u2229\ufe00\u0100eo\u19e2\u19e5t;\u6041\xee\u0693\u0200aeiu\u19f0\u19fb\u1a01\u1a05\u01f0\u19f5\0\u19f8s;\u6a4don;\u410ddil\u803b\xe7\u40e7rc;\u4109ps\u0100;s\u1a0c\u1a0d\u6a4cm;\u6a50ot;\u410b\u0180dmn\u1a1b\u1a20\u1a26il\u80bb\xb8\u01adptyv;\u69b2t\u8100\xa2;e\u1a2d\u1a2e\u40a2r\xe4\u01b2r;\uc000\ud835\udd20\u0180cei\u1a3d\u1a40\u1a4dy;\u4447ck\u0100;m\u1a47\u1a48\u6713ark\xbb\u1a48;\u43c7r\u0380;Ecefms\u1a5f\u1a60\u1a62\u1a6b\u1aa4\u1aaa\u1aae\u65cb;\u69c3\u0180;el\u1a69\u1a6a\u1a6d\u42c6q;\u6257e\u0261\u1a74\0\0\u1a88rrow\u0100lr\u1a7c\u1a81eft;\u61baight;\u61bb\u0280RSacd\u1a92\u1a94\u1a96\u1a9a\u1a9f\xbb\u0f47;\u64c8st;\u629birc;\u629aash;\u629dnint;\u6a10id;\u6aefcir;\u69c2ubs\u0100;u\u1abb\u1abc\u6663it\xbb\u1abc\u02ec\u1ac7\u1ad4\u1afa\0\u1b0aon\u0100;e\u1acd\u1ace\u403a\u0100;q\xc7\xc6\u026d\u1ad9\0\0\u1ae2a\u0100;t\u1ade\u1adf\u402c;\u4040\u0180;fl\u1ae8\u1ae9\u1aeb\u6201\xee\u1160e\u0100mx\u1af1\u1af6ent\xbb\u1ae9e\xf3\u024d\u01e7\u1afe\0\u1b07\u0100;d\u12bb\u1b02ot;\u6a6dn\xf4\u0246\u0180fry\u1b10\u1b14\u1b17;\uc000\ud835\udd54o\xe4\u0254\u8100\xa9;s\u0155\u1b1dr;\u6117\u0100ao\u1b25\u1b29rr;\u61b5ss;\u6717\u0100cu\u1b32\u1b37r;\uc000\ud835\udcb8\u0100bp\u1b3c\u1b44\u0100;e\u1b41\u1b42\u6acf;\u6ad1\u0100;e\u1b49\u1b4a\u6ad0;\u6ad2dot;\u62ef\u0380delprvw\u1b60\u1b6c\u1b77\u1b82\u1bac\u1bd4\u1bf9arr\u0100lr\u1b68\u1b6a;\u6938;\u6935\u0270\u1b72\0\0\u1b75r;\u62dec;\u62dfarr\u0100;p\u1b7f\u1b80\u61b6;\u693d\u0300;bcdos\u1b8f\u1b90\u1b96\u1ba1\u1ba5\u1ba8\u622arcap;\u6a48\u0100au\u1b9b\u1b9ep;\u6a46p;\u6a4aot;\u628dr;\u6a45;\uc000\u222a\ufe00\u0200alrv\u1bb5\u1bbf\u1bde\u1be3rr\u0100;m\u1bbc\u1bbd\u61b7;\u693cy\u0180evw\u1bc7\u1bd4\u1bd8q\u0270\u1bce\0\0\u1bd2re\xe3\u1b73u\xe3\u1b75ee;\u62ceedge;\u62cfen\u803b\xa4\u40a4earrow\u0100lr\u1bee\u1bf3eft\xbb\u1b80ight\xbb\u1bbde\xe4\u1bdd\u0100ci\u1c01\u1c07onin\xf4\u01f7nt;\u6231lcty;\u632d\u0980AHabcdefhijlorstuwz\u1c38\u1c3b\u1c3f\u1c5d\u1c69\u1c75\u1c8a\u1c9e\u1cac\u1cb7\u1cfb\u1cff\u1d0d\u1d7b\u1d91\u1dab\u1dbb\u1dc6\u1dcdr\xf2\u0381ar;\u6965\u0200glrs\u1c48\u1c4d\u1c52\u1c54ger;\u6020eth;\u6138\xf2\u1133h\u0100;v\u1c5a\u1c5b\u6010\xbb\u090a\u016b\u1c61\u1c67arow;\u690fa\xe3\u0315\u0100ay\u1c6e\u1c73ron;\u410f;\u4434\u0180;ao\u0332\u1c7c\u1c84\u0100gr\u02bf\u1c81r;\u61catseq;\u6a77\u0180glm\u1c91\u1c94\u1c98\u803b\xb0\u40b0ta;\u43b4ptyv;\u69b1\u0100ir\u1ca3\u1ca8sht;\u697f;\uc000\ud835\udd21ar\u0100lr\u1cb3\u1cb5\xbb\u08dc\xbb\u101e\u0280aegsv\u1cc2\u0378\u1cd6\u1cdc\u1ce0m\u0180;os\u0326\u1cca\u1cd4nd\u0100;s\u0326\u1cd1uit;\u6666amma;\u43ddin;\u62f2\u0180;io\u1ce7\u1ce8\u1cf8\u40f7de\u8100\xf7;o\u1ce7\u1cf0ntimes;\u62c7n\xf8\u1cf7cy;\u4452c\u026f\u1d06\0\0\u1d0arn;\u631eop;\u630d\u0280lptuw\u1d18\u1d1d\u1d22\u1d49\u1d55lar;\u4024f;\uc000\ud835\udd55\u0280;emps\u030b\u1d2d\u1d37\u1d3d\u1d42q\u0100;d\u0352\u1d33ot;\u6251inus;\u6238lus;\u6214quare;\u62a1blebarwedg\xe5\xfan\u0180adh\u112e\u1d5d\u1d67ownarrow\xf3\u1c83arpoon\u0100lr\u1d72\u1d76ef\xf4\u1cb4igh\xf4\u1cb6\u0162\u1d7f\u1d85karo\xf7\u0f42\u026f\u1d8a\0\0\u1d8ern;\u631fop;\u630c\u0180cot\u1d98\u1da3\u1da6\u0100ry\u1d9d\u1da1;\uc000\ud835\udcb9;\u4455l;\u69f6rok;\u4111\u0100dr\u1db0\u1db4ot;\u62f1i\u0100;f\u1dba\u1816\u65bf\u0100ah\u1dc0\u1dc3r\xf2\u0429a\xf2\u0fa6angle;\u69a6\u0100ci\u1dd2\u1dd5y;\u445fgrarr;\u67ff\u0900Dacdefglmnopqrstux\u1e01\u1e09\u1e19\u1e38\u0578\u1e3c\u1e49\u1e61\u1e7e\u1ea5\u1eaf\u1ebd\u1ee1\u1f2a\u1f37\u1f44\u1f4e\u1f5a\u0100Do\u1e06\u1d34o\xf4\u1c89\u0100cs\u1e0e\u1e14ute\u803b\xe9\u40e9ter;\u6a6e\u0200aioy\u1e22\u1e27\u1e31\u1e36ron;\u411br\u0100;c\u1e2d\u1e2e\u6256\u803b\xea\u40ealon;\u6255;\u444dot;\u4117\u0100Dr\u1e41\u1e45ot;\u6252;\uc000\ud835\udd22\u0180;rs\u1e50\u1e51\u1e57\u6a9aave\u803b\xe8\u40e8\u0100;d\u1e5c\u1e5d\u6a96ot;\u6a98\u0200;ils\u1e6a\u1e6b\u1e72\u1e74\u6a99nters;\u63e7;\u6113\u0100;d\u1e79\u1e7a\u6a95ot;\u6a97\u0180aps\u1e85\u1e89\u1e97cr;\u4113ty\u0180;sv\u1e92\u1e93\u1e95\u6205et\xbb\u1e93p\u01001;\u1e9d\u1ea4\u0133\u1ea1\u1ea3;\u6004;\u6005\u6003\u0100gs\u1eaa\u1eac;\u414bp;\u6002\u0100gp\u1eb4\u1eb8on;\u4119f;\uc000\ud835\udd56\u0180als\u1ec4\u1ece\u1ed2r\u0100;s\u1eca\u1ecb\u62d5l;\u69e3us;\u6a71i\u0180;lv\u1eda\u1edb\u1edf\u43b5on\xbb\u1edb;\u43f5\u0200csuv\u1eea\u1ef3\u1f0b\u1f23\u0100io\u1eef\u1e31rc\xbb\u1e2e\u0269\u1ef9\0\0\u1efb\xed\u0548ant\u0100gl\u1f02\u1f06tr\xbb\u1e5dess\xbb\u1e7a\u0180aei\u1f12\u1f16\u1f1als;\u403dst;\u625fv\u0100;D\u0235\u1f20D;\u6a78parsl;\u69e5\u0100Da\u1f2f\u1f33ot;\u6253rr;\u6971\u0180cdi\u1f3e\u1f41\u1ef8r;\u612fo\xf4\u0352\u0100ah\u1f49\u1f4b;\u43b7\u803b\xf0\u40f0\u0100mr\u1f53\u1f57l\u803b\xeb\u40ebo;\u60ac\u0180cip\u1f61\u1f64\u1f67l;\u4021s\xf4\u056e\u0100eo\u1f6c\u1f74ctatio\xee\u0559nential\xe5\u0579\u09e1\u1f92\0\u1f9e\0\u1fa1\u1fa7\0\0\u1fc6\u1fcc\0\u1fd3\0\u1fe6\u1fea\u2000\0\u2008\u205allingdotse\xf1\u1e44y;\u4444male;\u6640\u0180ilr\u1fad\u1fb3\u1fc1lig;\u8000\ufb03\u0269\u1fb9\0\0\u1fbdg;\u8000\ufb00ig;\u8000\ufb04;\uc000\ud835\udd23lig;\u8000\ufb01lig;\uc000fj\u0180alt\u1fd9\u1fdc\u1fe1t;\u666dig;\u8000\ufb02ns;\u65b1of;\u4192\u01f0\u1fee\0\u1ff3f;\uc000\ud835\udd57\u0100ak\u05bf\u1ff7\u0100;v\u1ffc\u1ffd\u62d4;\u6ad9artint;\u6a0d\u0100ao\u200c\u2055\u0100cs\u2011\u2052\u03b1\u201a\u2030\u2038\u2045\u2048\0\u2050\u03b2\u2022\u2025\u2027\u202a\u202c\0\u202e\u803b\xbd\u40bd;\u6153\u803b\xbc\u40bc;\u6155;\u6159;\u615b\u01b3\u2034\0\u2036;\u6154;\u6156\u02b4\u203e\u2041\0\0\u2043\u803b\xbe\u40be;\u6157;\u615c5;\u6158\u01b6\u204c\0\u204e;\u615a;\u615d8;\u615el;\u6044wn;\u6322cr;\uc000\ud835\udcbb\u0880Eabcdefgijlnorstv\u2082\u2089\u209f\u20a5\u20b0\u20b4\u20f0\u20f5\u20fa\u20ff\u2103\u2112\u2138\u0317\u213e\u2152\u219e\u0100;l\u064d\u2087;\u6a8c\u0180cmp\u2090\u2095\u209dute;\u41f5ma\u0100;d\u209c\u1cda\u43b3;\u6a86reve;\u411f\u0100iy\u20aa\u20aerc;\u411d;\u4433ot;\u4121\u0200;lqs\u063e\u0642\u20bd\u20c9\u0180;qs\u063e\u064c\u20c4lan\xf4\u0665\u0200;cdl\u0665\u20d2\u20d5\u20e5c;\u6aa9ot\u0100;o\u20dc\u20dd\u6a80\u0100;l\u20e2\u20e3\u6a82;\u6a84\u0100;e\u20ea\u20ed\uc000\u22db\ufe00s;\u6a94r;\uc000\ud835\udd24\u0100;g\u0673\u061bmel;\u6137cy;\u4453\u0200;Eaj\u065a\u210c\u210e\u2110;\u6a92;\u6aa5;\u6aa4\u0200Eaes\u211b\u211d\u2129\u2134;\u6269p\u0100;p\u2123\u2124\u6a8arox\xbb\u2124\u0100;q\u212e\u212f\u6a88\u0100;q\u212e\u211bim;\u62e7pf;\uc000\ud835\udd58\u0100ci\u2143\u2146r;\u610am\u0180;el\u066b\u214e\u2150;\u6a8e;\u6a90\u8300>;cdlqr\u05ee\u2160\u216a\u216e\u2173\u2179\u0100ci\u2165\u2167;\u6aa7r;\u6a7aot;\u62d7Par;\u6995uest;\u6a7c\u0280adels\u2184\u216a\u2190\u0656\u219b\u01f0\u2189\0\u218epro\xf8\u209er;\u6978q\u0100lq\u063f\u2196les\xf3\u2088i\xed\u066b\u0100en\u21a3\u21adrtneqq;\uc000\u2269\ufe00\xc5\u21aa\u0500Aabcefkosy\u21c4\u21c7\u21f1\u21f5\u21fa\u2218\u221d\u222f\u2268\u227dr\xf2\u03a0\u0200ilmr\u21d0\u21d4\u21d7\u21dbrs\xf0\u1484f\xbb\u2024il\xf4\u06a9\u0100dr\u21e0\u21e4cy;\u444a\u0180;cw\u08f4\u21eb\u21efir;\u6948;\u61adar;\u610firc;\u4125\u0180alr\u2201\u220e\u2213rts\u0100;u\u2209\u220a\u6665it\xbb\u220alip;\u6026con;\u62b9r;\uc000\ud835\udd25s\u0100ew\u2223\u2229arow;\u6925arow;\u6926\u0280amopr\u223a\u223e\u2243\u225e\u2263rr;\u61fftht;\u623bk\u0100lr\u2249\u2253eftarrow;\u61a9ightarrow;\u61aaf;\uc000\ud835\udd59bar;\u6015\u0180clt\u226f\u2274\u2278r;\uc000\ud835\udcbdas\xe8\u21f4rok;\u4127\u0100bp\u2282\u2287ull;\u6043hen\xbb\u1c5b\u0ae1\u22a3\0\u22aa\0\u22b8\u22c5\u22ce\0\u22d5\u22f3\0\0\u22f8\u2322\u2367\u2362\u237f\0\u2386\u23aa\u23b4cute\u803b\xed\u40ed\u0180;iy\u0771\u22b0\u22b5rc\u803b\xee\u40ee;\u4438\u0100cx\u22bc\u22bfy;\u4435cl\u803b\xa1\u40a1\u0100fr\u039f\u22c9;\uc000\ud835\udd26rave\u803b\xec\u40ec\u0200;ino\u073e\u22dd\u22e9\u22ee\u0100in\u22e2\u22e6nt;\u6a0ct;\u622dfin;\u69dcta;\u6129lig;\u4133\u0180aop\u22fe\u231a\u231d\u0180cgt\u2305\u2308\u2317r;\u412b\u0180elp\u071f\u230f\u2313in\xe5\u078ear\xf4\u0720h;\u4131f;\u62b7ed;\u41b5\u0280;cfot\u04f4\u232c\u2331\u233d\u2341are;\u6105in\u0100;t\u2338\u2339\u621eie;\u69dddo\xf4\u2319\u0280;celp\u0757\u234c\u2350\u235b\u2361al;\u62ba\u0100gr\u2355\u2359er\xf3\u1563\xe3\u234darhk;\u6a17rod;\u6a3c\u0200cgpt\u236f\u2372\u2376\u237by;\u4451on;\u412ff;\uc000\ud835\udd5aa;\u43b9uest\u803b\xbf\u40bf\u0100ci\u238a\u238fr;\uc000\ud835\udcben\u0280;Edsv\u04f4\u239b\u239d\u23a1\u04f3;\u62f9ot;\u62f5\u0100;v\u23a6\u23a7\u62f4;\u62f3\u0100;i\u0777\u23aelde;\u4129\u01eb\u23b8\0\u23bccy;\u4456l\u803b\xef\u40ef\u0300cfmosu\u23cc\u23d7\u23dc\u23e1\u23e7\u23f5\u0100iy\u23d1\u23d5rc;\u4135;\u4439r;\uc000\ud835\udd27ath;\u4237pf;\uc000\ud835\udd5b\u01e3\u23ec\0\u23f1r;\uc000\ud835\udcbfrcy;\u4458kcy;\u4454\u0400acfghjos\u240b\u2416\u2422\u2427\u242d\u2431\u2435\u243bppa\u0100;v\u2413\u2414\u43ba;\u43f0\u0100ey\u241b\u2420dil;\u4137;\u443ar;\uc000\ud835\udd28reen;\u4138cy;\u4445cy;\u445cpf;\uc000\ud835\udd5ccr;\uc000\ud835\udcc0\u0b80ABEHabcdefghjlmnoprstuv\u2470\u2481\u2486\u248d\u2491\u250e\u253d\u255a\u2580\u264e\u265e\u2665\u2679\u267d\u269a\u26b2\u26d8\u275d\u2768\u278b\u27c0\u2801\u2812\u0180art\u2477\u247a\u247cr\xf2\u09c6\xf2\u0395ail;\u691barr;\u690e\u0100;g\u0994\u248b;\u6a8bar;\u6962\u0963\u24a5\0\u24aa\0\u24b1\0\0\0\0\0\u24b5\u24ba\0\u24c6\u24c8\u24cd\0\u24f9ute;\u413amptyv;\u69b4ra\xee\u084cbda;\u43bbg\u0180;dl\u088e\u24c1\u24c3;\u6991\xe5\u088e;\u6a85uo\u803b\xab\u40abr\u0400;bfhlpst\u0899\u24de\u24e6\u24e9\u24eb\u24ee\u24f1\u24f5\u0100;f\u089d\u24e3s;\u691fs;\u691d\xeb\u2252p;\u61abl;\u6939im;\u6973l;\u61a2\u0180;ae\u24ff\u2500\u2504\u6aabil;\u6919\u0100;s\u2509\u250a\u6aad;\uc000\u2aad\ufe00\u0180abr\u2515\u2519\u251drr;\u690crk;\u6772\u0100ak\u2522\u252cc\u0100ek\u2528\u252a;\u407b;\u405b\u0100es\u2531\u2533;\u698bl\u0100du\u2539\u253b;\u698f;\u698d\u0200aeuy\u2546\u254b\u2556\u2558ron;\u413e\u0100di\u2550\u2554il;\u413c\xec\u08b0\xe2\u2529;\u443b\u0200cqrs\u2563\u2566\u256d\u257da;\u6936uo\u0100;r\u0e19\u1746\u0100du\u2572\u2577har;\u6967shar;\u694bh;\u61b2\u0280;fgqs\u258b\u258c\u0989\u25f3\u25ff\u6264t\u0280ahlrt\u2598\u25a4\u25b7\u25c2\u25e8rrow\u0100;t\u0899\u25a1a\xe9\u24f6arpoon\u0100du\u25af\u25b4own\xbb\u045ap\xbb\u0966eftarrows;\u61c7ight\u0180ahs\u25cd\u25d6\u25derrow\u0100;s\u08f4\u08a7arpoon\xf3\u0f98quigarro\xf7\u21f0hreetimes;\u62cb\u0180;qs\u258b\u0993\u25falan\xf4\u09ac\u0280;cdgs\u09ac\u260a\u260d\u261d\u2628c;\u6aa8ot\u0100;o\u2614\u2615\u6a7f\u0100;r\u261a\u261b\u6a81;\u6a83\u0100;e\u2622\u2625\uc000\u22da\ufe00s;\u6a93\u0280adegs\u2633\u2639\u263d\u2649\u264bppro\xf8\u24c6ot;\u62d6q\u0100gq\u2643\u2645\xf4\u0989gt\xf2\u248c\xf4\u099bi\xed\u09b2\u0180ilr\u2655\u08e1\u265asht;\u697c;\uc000\ud835\udd29\u0100;E\u099c\u2663;\u6a91\u0161\u2669\u2676r\u0100du\u25b2\u266e\u0100;l\u0965\u2673;\u696alk;\u6584cy;\u4459\u0280;acht\u0a48\u2688\u268b\u2691\u2696r\xf2\u25c1orne\xf2\u1d08ard;\u696bri;\u65fa\u0100io\u269f\u26a4dot;\u4140ust\u0100;a\u26ac\u26ad\u63b0che\xbb\u26ad\u0200Eaes\u26bb\u26bd\u26c9\u26d4;\u6268p\u0100;p\u26c3\u26c4\u6a89rox\xbb\u26c4\u0100;q\u26ce\u26cf\u6a87\u0100;q\u26ce\u26bbim;\u62e6\u0400abnoptwz\u26e9\u26f4\u26f7\u271a\u272f\u2741\u2747\u2750\u0100nr\u26ee\u26f1g;\u67ecr;\u61fdr\xeb\u08c1g\u0180lmr\u26ff\u270d\u2714eft\u0100ar\u09e6\u2707ight\xe1\u09f2apsto;\u67fcight\xe1\u09fdparrow\u0100lr\u2725\u2729ef\xf4\u24edight;\u61ac\u0180afl\u2736\u2739\u273dr;\u6985;\uc000\ud835\udd5dus;\u6a2dimes;\u6a34\u0161\u274b\u274fst;\u6217\xe1\u134e\u0180;ef\u2757\u2758\u1800\u65cange\xbb\u2758ar\u0100;l\u2764\u2765\u4028t;\u6993\u0280achmt\u2773\u2776\u277c\u2785\u2787r\xf2\u08a8orne\xf2\u1d8car\u0100;d\u0f98\u2783;\u696d;\u600eri;\u62bf\u0300achiqt\u2798\u279d\u0a40\u27a2\u27ae\u27bbquo;\u6039r;\uc000\ud835\udcc1m\u0180;eg\u09b2\u27aa\u27ac;\u6a8d;\u6a8f\u0100bu\u252a\u27b3o\u0100;r\u0e1f\u27b9;\u601arok;\u4142\u8400<;cdhilqr\u082b\u27d2\u2639\u27dc\u27e0\u27e5\u27ea\u27f0\u0100ci\u27d7\u27d9;\u6aa6r;\u6a79re\xe5\u25f2mes;\u62c9arr;\u6976uest;\u6a7b\u0100Pi\u27f5\u27f9ar;\u6996\u0180;ef\u2800\u092d\u181b\u65c3r\u0100du\u2807\u280dshar;\u694ahar;\u6966\u0100en\u2817\u2821rtneqq;\uc000\u2268\ufe00\xc5\u281e\u0700Dacdefhilnopsu\u2840\u2845\u2882\u288e\u2893\u28a0\u28a5\u28a8\u28da\u28e2\u28e4\u0a83\u28f3\u2902Dot;\u623a\u0200clpr\u284e\u2852\u2863\u287dr\u803b\xaf\u40af\u0100et\u2857\u2859;\u6642\u0100;e\u285e\u285f\u6720se\xbb\u285f\u0100;s\u103b\u2868to\u0200;dlu\u103b\u2873\u2877\u287bow\xee\u048cef\xf4\u090f\xf0\u13d1ker;\u65ae\u0100oy\u2887\u288cmma;\u6a29;\u443cash;\u6014asuredangle\xbb\u1626r;\uc000\ud835\udd2ao;\u6127\u0180cdn\u28af\u28b4\u28c9ro\u803b\xb5\u40b5\u0200;acd\u1464\u28bd\u28c0\u28c4s\xf4\u16a7ir;\u6af0ot\u80bb\xb7\u01b5us\u0180;bd\u28d2\u1903\u28d3\u6212\u0100;u\u1d3c\u28d8;\u6a2a\u0163\u28de\u28e1p;\u6adb\xf2\u2212\xf0\u0a81\u0100dp\u28e9\u28eeels;\u62a7f;\uc000\ud835\udd5e\u0100ct\u28f8\u28fdr;\uc000\ud835\udcc2pos\xbb\u159d\u0180;lm\u2909\u290a\u290d\u43bctimap;\u62b8\u0c00GLRVabcdefghijlmoprstuvw\u2942\u2953\u297e\u2989\u2998\u29da\u29e9\u2a15\u2a1a\u2a58\u2a5d\u2a83\u2a95\u2aa4\u2aa8\u2b04\u2b07\u2b44\u2b7f\u2bae\u2c34\u2c67\u2c7c\u2ce9\u0100gt\u2947\u294b;\uc000\u22d9\u0338\u0100;v\u2950\u0bcf\uc000\u226b\u20d2\u0180elt\u295a\u2972\u2976ft\u0100ar\u2961\u2967rrow;\u61cdightarrow;\u61ce;\uc000\u22d8\u0338\u0100;v\u297b\u0c47\uc000\u226a\u20d2ightarrow;\u61cf\u0100Dd\u298e\u2993ash;\u62afash;\u62ae\u0280bcnpt\u29a3\u29a7\u29ac\u29b1\u29ccla\xbb\u02deute;\u4144g;\uc000\u2220\u20d2\u0280;Eiop\u0d84\u29bc\u29c0\u29c5\u29c8;\uc000\u2a70\u0338d;\uc000\u224b\u0338s;\u4149ro\xf8\u0d84ur\u0100;a\u29d3\u29d4\u666el\u0100;s\u29d3\u0b38\u01f3\u29df\0\u29e3p\u80bb\xa0\u0b37mp\u0100;e\u0bf9\u0c00\u0280aeouy\u29f4\u29fe\u2a03\u2a10\u2a13\u01f0\u29f9\0\u29fb;\u6a43on;\u4148dil;\u4146ng\u0100;d\u0d7e\u2a0aot;\uc000\u2a6d\u0338p;\u6a42;\u443dash;\u6013\u0380;Aadqsx\u0b92\u2a29\u2a2d\u2a3b\u2a41\u2a45\u2a50rr;\u61d7r\u0100hr\u2a33\u2a36k;\u6924\u0100;o\u13f2\u13f0ot;\uc000\u2250\u0338ui\xf6\u0b63\u0100ei\u2a4a\u2a4ear;\u6928\xed\u0b98ist\u0100;s\u0ba0\u0b9fr;\uc000\ud835\udd2b\u0200Eest\u0bc5\u2a66\u2a79\u2a7c\u0180;qs\u0bbc\u2a6d\u0be1\u0180;qs\u0bbc\u0bc5\u2a74lan\xf4\u0be2i\xed\u0bea\u0100;r\u0bb6\u2a81\xbb\u0bb7\u0180Aap\u2a8a\u2a8d\u2a91r\xf2\u2971rr;\u61aear;\u6af2\u0180;sv\u0f8d\u2a9c\u0f8c\u0100;d\u2aa1\u2aa2\u62fc;\u62facy;\u445a\u0380AEadest\u2ab7\u2aba\u2abe\u2ac2\u2ac5\u2af6\u2af9r\xf2\u2966;\uc000\u2266\u0338rr;\u619ar;\u6025\u0200;fqs\u0c3b\u2ace\u2ae3\u2aeft\u0100ar\u2ad4\u2ad9rro\xf7\u2ac1ightarro\xf7\u2a90\u0180;qs\u0c3b\u2aba\u2aealan\xf4\u0c55\u0100;s\u0c55\u2af4\xbb\u0c36i\xed\u0c5d\u0100;r\u0c35\u2afei\u0100;e\u0c1a\u0c25i\xe4\u0d90\u0100pt\u2b0c\u2b11f;\uc000\ud835\udd5f\u8180\xac;in\u2b19\u2b1a\u2b36\u40acn\u0200;Edv\u0b89\u2b24\u2b28\u2b2e;\uc000\u22f9\u0338ot;\uc000\u22f5\u0338\u01e1\u0b89\u2b33\u2b35;\u62f7;\u62f6i\u0100;v\u0cb8\u2b3c\u01e1\u0cb8\u2b41\u2b43;\u62fe;\u62fd\u0180aor\u2b4b\u2b63\u2b69r\u0200;ast\u0b7b\u2b55\u2b5a\u2b5flle\xec\u0b7bl;\uc000\u2afd\u20e5;\uc000\u2202\u0338lint;\u6a14\u0180;ce\u0c92\u2b70\u2b73u\xe5\u0ca5\u0100;c\u0c98\u2b78\u0100;e\u0c92\u2b7d\xf1\u0c98\u0200Aait\u2b88\u2b8b\u2b9d\u2ba7r\xf2\u2988rr\u0180;cw\u2b94\u2b95\u2b99\u619b;\uc000\u2933\u0338;\uc000\u219d\u0338ghtarrow\xbb\u2b95ri\u0100;e\u0ccb\u0cd6\u0380chimpqu\u2bbd\u2bcd\u2bd9\u2b04\u0b78\u2be4\u2bef\u0200;cer\u0d32\u2bc6\u0d37\u2bc9u\xe5\u0d45;\uc000\ud835\udcc3ort\u026d\u2b05\0\0\u2bd6ar\xe1\u2b56m\u0100;e\u0d6e\u2bdf\u0100;q\u0d74\u0d73su\u0100bp\u2beb\u2bed\xe5\u0cf8\xe5\u0d0b\u0180bcp\u2bf6\u2c11\u2c19\u0200;Ees\u2bff\u2c00\u0d22\u2c04\u6284;\uc000\u2ac5\u0338et\u0100;e\u0d1b\u2c0bq\u0100;q\u0d23\u2c00c\u0100;e\u0d32\u2c17\xf1\u0d38\u0200;Ees\u2c22\u2c23\u0d5f\u2c27\u6285;\uc000\u2ac6\u0338et\u0100;e\u0d58\u2c2eq\u0100;q\u0d60\u2c23\u0200gilr\u2c3d\u2c3f\u2c45\u2c47\xec\u0bd7lde\u803b\xf1\u40f1\xe7\u0c43iangle\u0100lr\u2c52\u2c5ceft\u0100;e\u0c1a\u2c5a\xf1\u0c26ight\u0100;e\u0ccb\u2c65\xf1\u0cd7\u0100;m\u2c6c\u2c6d\u43bd\u0180;es\u2c74\u2c75\u2c79\u4023ro;\u6116p;\u6007\u0480DHadgilrs\u2c8f\u2c94\u2c99\u2c9e\u2ca3\u2cb0\u2cb6\u2cd3\u2ce3ash;\u62adarr;\u6904p;\uc000\u224d\u20d2ash;\u62ac\u0100et\u2ca8\u2cac;\uc000\u2265\u20d2;\uc000>\u20d2nfin;\u69de\u0180Aet\u2cbd\u2cc1\u2cc5rr;\u6902;\uc000\u2264\u20d2\u0100;r\u2cca\u2ccd\uc000<\u20d2ie;\uc000\u22b4\u20d2\u0100At\u2cd8\u2cdcrr;\u6903rie;\uc000\u22b5\u20d2im;\uc000\u223c\u20d2\u0180Aan\u2cf0\u2cf4\u2d02rr;\u61d6r\u0100hr\u2cfa\u2cfdk;\u6923\u0100;o\u13e7\u13e5ear;\u6927\u1253\u1a95\0\0\0\0\0\0\0\0\0\0\0\0\0\u2d2d\0\u2d38\u2d48\u2d60\u2d65\u2d72\u2d84\u1b07\0\0\u2d8d\u2dab\0\u2dc8\u2dce\0\u2ddc\u2e19\u2e2b\u2e3e\u2e43\u0100cs\u2d31\u1a97ute\u803b\xf3\u40f3\u0100iy\u2d3c\u2d45r\u0100;c\u1a9e\u2d42\u803b\xf4\u40f4;\u443e\u0280abios\u1aa0\u2d52\u2d57\u01c8\u2d5alac;\u4151v;\u6a38old;\u69bclig;\u4153\u0100cr\u2d69\u2d6dir;\u69bf;\uc000\ud835\udd2c\u036f\u2d79\0\0\u2d7c\0\u2d82n;\u42dbave\u803b\xf2\u40f2;\u69c1\u0100bm\u2d88\u0df4ar;\u69b5\u0200acit\u2d95\u2d98\u2da5\u2da8r\xf2\u1a80\u0100ir\u2d9d\u2da0r;\u69beoss;\u69bbn\xe5\u0e52;\u69c0\u0180aei\u2db1\u2db5\u2db9cr;\u414dga;\u43c9\u0180cdn\u2dc0\u2dc5\u01cdron;\u43bf;\u69b6pf;\uc000\ud835\udd60\u0180ael\u2dd4\u2dd7\u01d2r;\u69b7rp;\u69b9\u0380;adiosv\u2dea\u2deb\u2dee\u2e08\u2e0d\u2e10\u2e16\u6228r\xf2\u1a86\u0200;efm\u2df7\u2df8\u2e02\u2e05\u6a5dr\u0100;o\u2dfe\u2dff\u6134f\xbb\u2dff\u803b\xaa\u40aa\u803b\xba\u40bagof;\u62b6r;\u6a56lope;\u6a57;\u6a5b\u0180clo\u2e1f\u2e21\u2e27\xf2\u2e01ash\u803b\xf8\u40f8l;\u6298i\u016c\u2e2f\u2e34de\u803b\xf5\u40f5es\u0100;a\u01db\u2e3as;\u6a36ml\u803b\xf6\u40f6bar;\u633d\u0ae1\u2e5e\0\u2e7d\0\u2e80\u2e9d\0\u2ea2\u2eb9\0\0\u2ecb\u0e9c\0\u2f13\0\0\u2f2b\u2fbc\0\u2fc8r\u0200;ast\u0403\u2e67\u2e72\u0e85\u8100\xb6;l\u2e6d\u2e6e\u40b6le\xec\u0403\u0269\u2e78\0\0\u2e7bm;\u6af3;\u6afdy;\u443fr\u0280cimpt\u2e8b\u2e8f\u2e93\u1865\u2e97nt;\u4025od;\u402eil;\u6030enk;\u6031r;\uc000\ud835\udd2d\u0180imo\u2ea8\u2eb0\u2eb4\u0100;v\u2ead\u2eae\u43c6;\u43d5ma\xf4\u0a76ne;\u660e\u0180;tv\u2ebf\u2ec0\u2ec8\u43c0chfork\xbb\u1ffd;\u43d6\u0100au\u2ecf\u2edfn\u0100ck\u2ed5\u2eddk\u0100;h\u21f4\u2edb;\u610e\xf6\u21f4s\u0480;abcdemst\u2ef3\u2ef4\u1908\u2ef9\u2efd\u2f04\u2f06\u2f0a\u2f0e\u402bcir;\u6a23ir;\u6a22\u0100ou\u1d40\u2f02;\u6a25;\u6a72n\u80bb\xb1\u0e9dim;\u6a26wo;\u6a27\u0180ipu\u2f19\u2f20\u2f25ntint;\u6a15f;\uc000\ud835\udd61nd\u803b\xa3\u40a3\u0500;Eaceinosu\u0ec8\u2f3f\u2f41\u2f44\u2f47\u2f81\u2f89\u2f92\u2f7e\u2fb6;\u6ab3p;\u6ab7u\xe5\u0ed9\u0100;c\u0ece\u2f4c\u0300;acens\u0ec8\u2f59\u2f5f\u2f66\u2f68\u2f7eppro\xf8\u2f43urlye\xf1\u0ed9\xf1\u0ece\u0180aes\u2f6f\u2f76\u2f7approx;\u6ab9qq;\u6ab5im;\u62e8i\xed\u0edfme\u0100;s\u2f88\u0eae\u6032\u0180Eas\u2f78\u2f90\u2f7a\xf0\u2f75\u0180dfp\u0eec\u2f99\u2faf\u0180als\u2fa0\u2fa5\u2faalar;\u632eine;\u6312urf;\u6313\u0100;t\u0efb\u2fb4\xef\u0efbrel;\u62b0\u0100ci\u2fc0\u2fc5r;\uc000\ud835\udcc5;\u43c8ncsp;\u6008\u0300fiopsu\u2fda\u22e2\u2fdf\u2fe5\u2feb\u2ff1r;\uc000\ud835\udd2epf;\uc000\ud835\udd62rime;\u6057cr;\uc000\ud835\udcc6\u0180aeo\u2ff8\u3009\u3013t\u0100ei\u2ffe\u3005rnion\xf3\u06b0nt;\u6a16st\u0100;e\u3010\u3011\u403f\xf1\u1f19\xf4\u0f14\u0a80ABHabcdefhilmnoprstux\u3040\u3051\u3055\u3059\u30e0\u310e\u312b\u3147\u3162\u3172\u318e\u3206\u3215\u3224\u3229\u3258\u326e\u3272\u3290\u32b0\u32b7\u0180art\u3047\u304a\u304cr\xf2\u10b3\xf2\u03ddail;\u691car\xf2\u1c65ar;\u6964\u0380cdenqrt\u3068\u3075\u3078\u307f\u308f\u3094\u30cc\u0100eu\u306d\u3071;\uc000\u223d\u0331te;\u4155i\xe3\u116emptyv;\u69b3g\u0200;del\u0fd1\u3089\u308b\u308d;\u6992;\u69a5\xe5\u0fd1uo\u803b\xbb\u40bbr\u0580;abcfhlpstw\u0fdc\u30ac\u30af\u30b7\u30b9\u30bc\u30be\u30c0\u30c3\u30c7\u30cap;\u6975\u0100;f\u0fe0\u30b4s;\u6920;\u6933s;\u691e\xeb\u225d\xf0\u272el;\u6945im;\u6974l;\u61a3;\u619d\u0100ai\u30d1\u30d5il;\u691ao\u0100;n\u30db\u30dc\u6236al\xf3\u0f1e\u0180abr\u30e7\u30ea\u30eer\xf2\u17e5rk;\u6773\u0100ak\u30f3\u30fdc\u0100ek\u30f9\u30fb;\u407d;\u405d\u0100es\u3102\u3104;\u698cl\u0100du\u310a\u310c;\u698e;\u6990\u0200aeuy\u3117\u311c\u3127\u3129ron;\u4159\u0100di\u3121\u3125il;\u4157\xec\u0ff2\xe2\u30fa;\u4440\u0200clqs\u3134\u3137\u313d\u3144a;\u6937dhar;\u6969uo\u0100;r\u020e\u020dh;\u61b3\u0180acg\u314e\u315f\u0f44l\u0200;ips\u0f78\u3158\u315b\u109cn\xe5\u10bbar\xf4\u0fa9t;\u65ad\u0180ilr\u3169\u1023\u316esht;\u697d;\uc000\ud835\udd2f\u0100ao\u3177\u3186r\u0100du\u317d\u317f\xbb\u047b\u0100;l\u1091\u3184;\u696c\u0100;v\u318b\u318c\u43c1;\u43f1\u0180gns\u3195\u31f9\u31fcht\u0300ahlrst\u31a4\u31b0\u31c2\u31d8\u31e4\u31eerrow\u0100;t\u0fdc\u31ada\xe9\u30c8arpoon\u0100du\u31bb\u31bfow\xee\u317ep\xbb\u1092eft\u0100ah\u31ca\u31d0rrow\xf3\u0feaarpoon\xf3\u0551ightarrows;\u61c9quigarro\xf7\u30cbhreetimes;\u62ccg;\u42daingdotse\xf1\u1f32\u0180ahm\u320d\u3210\u3213r\xf2\u0feaa\xf2\u0551;\u600foust\u0100;a\u321e\u321f\u63b1che\xbb\u321fmid;\u6aee\u0200abpt\u3232\u323d\u3240\u3252\u0100nr\u3237\u323ag;\u67edr;\u61fer\xeb\u1003\u0180afl\u3247\u324a\u324er;\u6986;\uc000\ud835\udd63us;\u6a2eimes;\u6a35\u0100ap\u325d\u3267r\u0100;g\u3263\u3264\u4029t;\u6994olint;\u6a12ar\xf2\u31e3\u0200achq\u327b\u3280\u10bc\u3285quo;\u603ar;\uc000\ud835\udcc7\u0100bu\u30fb\u328ao\u0100;r\u0214\u0213\u0180hir\u3297\u329b\u32a0re\xe5\u31f8mes;\u62cai\u0200;efl\u32aa\u1059\u1821\u32ab\u65b9tri;\u69celuhar;\u6968;\u611e\u0d61\u32d5\u32db\u32df\u332c\u3338\u3371\0\u337a\u33a4\0\0\u33ec\u33f0\0\u3428\u3448\u345a\u34ad\u34b1\u34ca\u34f1\0\u3616\0\0\u3633cute;\u415bqu\xef\u27ba\u0500;Eaceinpsy\u11ed\u32f3\u32f5\u32ff\u3302\u330b\u330f\u331f\u3326\u3329;\u6ab4\u01f0\u32fa\0\u32fc;\u6ab8on;\u4161u\xe5\u11fe\u0100;d\u11f3\u3307il;\u415frc;\u415d\u0180Eas\u3316\u3318\u331b;\u6ab6p;\u6abaim;\u62e9olint;\u6a13i\xed\u1204;\u4441ot\u0180;be\u3334\u1d47\u3335\u62c5;\u6a66\u0380Aacmstx\u3346\u334a\u3357\u335b\u335e\u3363\u336drr;\u61d8r\u0100hr\u3350\u3352\xeb\u2228\u0100;o\u0a36\u0a34t\u803b\xa7\u40a7i;\u403bwar;\u6929m\u0100in\u3369\xf0nu\xf3\xf1t;\u6736r\u0100;o\u3376\u2055\uc000\ud835\udd30\u0200acoy\u3382\u3386\u3391\u33a0rp;\u666f\u0100hy\u338b\u338fcy;\u4449;\u4448rt\u026d\u3399\0\0\u339ci\xe4\u1464ara\xec\u2e6f\u803b\xad\u40ad\u0100gm\u33a8\u33b4ma\u0180;fv\u33b1\u33b2\u33b2\u43c3;\u43c2\u0400;deglnpr\u12ab\u33c5\u33c9\u33ce\u33d6\u33de\u33e1\u33e6ot;\u6a6a\u0100;q\u12b1\u12b0\u0100;E\u33d3\u33d4\u6a9e;\u6aa0\u0100;E\u33db\u33dc\u6a9d;\u6a9fe;\u6246lus;\u6a24arr;\u6972ar\xf2\u113d\u0200aeit\u33f8\u3408\u340f\u3417\u0100ls\u33fd\u3404lsetm\xe9\u336ahp;\u6a33parsl;\u69e4\u0100dl\u1463\u3414e;\u6323\u0100;e\u341c\u341d\u6aaa\u0100;s\u3422\u3423\u6aac;\uc000\u2aac\ufe00\u0180flp\u342e\u3433\u3442tcy;\u444c\u0100;b\u3438\u3439\u402f\u0100;a\u343e\u343f\u69c4r;\u633ff;\uc000\ud835\udd64a\u0100dr\u344d\u0402es\u0100;u\u3454\u3455\u6660it\xbb\u3455\u0180csu\u3460\u3479\u349f\u0100au\u3465\u346fp\u0100;s\u1188\u346b;\uc000\u2293\ufe00p\u0100;s\u11b4\u3475;\uc000\u2294\ufe00u\u0100bp\u347f\u348f\u0180;es\u1197\u119c\u3486et\u0100;e\u1197\u348d\xf1\u119d\u0180;es\u11a8\u11ad\u3496et\u0100;e\u11a8\u349d\xf1\u11ae\u0180;af\u117b\u34a6\u05b0r\u0165\u34ab\u05b1\xbb\u117car\xf2\u1148\u0200cemt\u34b9\u34be\u34c2\u34c5r;\uc000\ud835\udcc8tm\xee\xf1i\xec\u3415ar\xe6\u11be\u0100ar\u34ce\u34d5r\u0100;f\u34d4\u17bf\u6606\u0100an\u34da\u34edight\u0100ep\u34e3\u34eapsilo\xee\u1ee0h\xe9\u2eafs\xbb\u2852\u0280bcmnp\u34fb\u355e\u1209\u358b\u358e\u0480;Edemnprs\u350e\u350f\u3511\u3515\u351e\u3523\u352c\u3531\u3536\u6282;\u6ac5ot;\u6abd\u0100;d\u11da\u351aot;\u6ac3ult;\u6ac1\u0100Ee\u3528\u352a;\u6acb;\u628alus;\u6abfarr;\u6979\u0180eiu\u353d\u3552\u3555t\u0180;en\u350e\u3545\u354bq\u0100;q\u11da\u350feq\u0100;q\u352b\u3528m;\u6ac7\u0100bp\u355a\u355c;\u6ad5;\u6ad3c\u0300;acens\u11ed\u356c\u3572\u3579\u357b\u3326ppro\xf8\u32faurlye\xf1\u11fe\xf1\u11f3\u0180aes\u3582\u3588\u331bppro\xf8\u331aq\xf1\u3317g;\u666a\u0680123;Edehlmnps\u35a9\u35ac\u35af\u121c\u35b2\u35b4\u35c0\u35c9\u35d5\u35da\u35df\u35e8\u35ed\u803b\xb9\u40b9\u803b\xb2\u40b2\u803b\xb3\u40b3;\u6ac6\u0100os\u35b9\u35bct;\u6abeub;\u6ad8\u0100;d\u1222\u35c5ot;\u6ac4s\u0100ou\u35cf\u35d2l;\u67c9b;\u6ad7arr;\u697bult;\u6ac2\u0100Ee\u35e4\u35e6;\u6acc;\u628blus;\u6ac0\u0180eiu\u35f4\u3609\u360ct\u0180;en\u121c\u35fc\u3602q\u0100;q\u1222\u35b2eq\u0100;q\u35e7\u35e4m;\u6ac8\u0100bp\u3611\u3613;\u6ad4;\u6ad6\u0180Aan\u361c\u3620\u362drr;\u61d9r\u0100hr\u3626\u3628\xeb\u222e\u0100;o\u0a2b\u0a29war;\u692alig\u803b\xdf\u40df\u0be1\u3651\u365d\u3660\u12ce\u3673\u3679\0\u367e\u36c2\0\0\0\0\0\u36db\u3703\0\u3709\u376c\0\0\0\u3787\u0272\u3656\0\0\u365bget;\u6316;\u43c4r\xeb\u0e5f\u0180aey\u3666\u366b\u3670ron;\u4165dil;\u4163;\u4442lrec;\u6315r;\uc000\ud835\udd31\u0200eiko\u3686\u369d\u36b5\u36bc\u01f2\u368b\0\u3691e\u01004f\u1284\u1281a\u0180;sv\u3698\u3699\u369b\u43b8ym;\u43d1\u0100cn\u36a2\u36b2k\u0100as\u36a8\u36aeppro\xf8\u12c1im\xbb\u12acs\xf0\u129e\u0100as\u36ba\u36ae\xf0\u12c1rn\u803b\xfe\u40fe\u01ec\u031f\u36c6\u22e7es\u8180\xd7;bd\u36cf\u36d0\u36d8\u40d7\u0100;a\u190f\u36d5r;\u6a31;\u6a30\u0180eps\u36e1\u36e3\u3700\xe1\u2a4d\u0200;bcf\u0486\u36ec\u36f0\u36f4ot;\u6336ir;\u6af1\u0100;o\u36f9\u36fc\uc000\ud835\udd65rk;\u6ada\xe1\u3362rime;\u6034\u0180aip\u370f\u3712\u3764d\xe5\u1248\u0380adempst\u3721\u374d\u3740\u3751\u3757\u375c\u375fngle\u0280;dlqr\u3730\u3731\u3736\u3740\u3742\u65b5own\xbb\u1dbbeft\u0100;e\u2800\u373e\xf1\u092e;\u625cight\u0100;e\u32aa\u374b\xf1\u105aot;\u65ecinus;\u6a3alus;\u6a39b;\u69cdime;\u6a3bezium;\u63e2\u0180cht\u3772\u377d\u3781\u0100ry\u3777\u377b;\uc000\ud835\udcc9;\u4446cy;\u445brok;\u4167\u0100io\u378b\u378ex\xf4\u1777head\u0100lr\u3797\u37a0eftarro\xf7\u084fightarrow\xbb\u0f5d\u0900AHabcdfghlmoprstuw\u37d0\u37d3\u37d7\u37e4\u37f0\u37fc\u380e\u381c\u3823\u3834\u3851\u385d\u386b\u38a9\u38cc\u38d2\u38ea\u38f6r\xf2\u03edar;\u6963\u0100cr\u37dc\u37e2ute\u803b\xfa\u40fa\xf2\u1150r\u01e3\u37ea\0\u37edy;\u445eve;\u416d\u0100iy\u37f5\u37farc\u803b\xfb\u40fb;\u4443\u0180abh\u3803\u3806\u380br\xf2\u13adlac;\u4171a\xf2\u13c3\u0100ir\u3813\u3818sht;\u697e;\uc000\ud835\udd32rave\u803b\xf9\u40f9\u0161\u3827\u3831r\u0100lr\u382c\u382e\xbb\u0957\xbb\u1083lk;\u6580\u0100ct\u3839\u384d\u026f\u383f\0\0\u384arn\u0100;e\u3845\u3846\u631cr\xbb\u3846op;\u630fri;\u65f8\u0100al\u3856\u385acr;\u416b\u80bb\xa8\u0349\u0100gp\u3862\u3866on;\u4173f;\uc000\ud835\udd66\u0300adhlsu\u114b\u3878\u387d\u1372\u3891\u38a0own\xe1\u13b3arpoon\u0100lr\u3888\u388cef\xf4\u382digh\xf4\u382fi\u0180;hl\u3899\u389a\u389c\u43c5\xbb\u13faon\xbb\u389aparrows;\u61c8\u0180cit\u38b0\u38c4\u38c8\u026f\u38b6\0\0\u38c1rn\u0100;e\u38bc\u38bd\u631dr\xbb\u38bdop;\u630eng;\u416fri;\u65f9cr;\uc000\ud835\udcca\u0180dir\u38d9\u38dd\u38e2ot;\u62f0lde;\u4169i\u0100;f\u3730\u38e8\xbb\u1813\u0100am\u38ef\u38f2r\xf2\u38a8l\u803b\xfc\u40fcangle;\u69a7\u0780ABDacdeflnoprsz\u391c\u391f\u3929\u392d\u39b5\u39b8\u39bd\u39df\u39e4\u39e8\u39f3\u39f9\u39fd\u3a01\u3a20r\xf2\u03f7ar\u0100;v\u3926\u3927\u6ae8;\u6ae9as\xe8\u03e1\u0100nr\u3932\u3937grt;\u699c\u0380eknprst\u34e3\u3946\u394b\u3952\u395d\u3964\u3996app\xe1\u2415othin\xe7\u1e96\u0180hir\u34eb\u2ec8\u3959op\xf4\u2fb5\u0100;h\u13b7\u3962\xef\u318d\u0100iu\u3969\u396dgm\xe1\u33b3\u0100bp\u3972\u3984setneq\u0100;q\u397d\u3980\uc000\u228a\ufe00;\uc000\u2acb\ufe00setneq\u0100;q\u398f\u3992\uc000\u228b\ufe00;\uc000\u2acc\ufe00\u0100hr\u399b\u399fet\xe1\u369ciangle\u0100lr\u39aa\u39afeft\xbb\u0925ight\xbb\u1051y;\u4432ash\xbb\u1036\u0180elr\u39c4\u39d2\u39d7\u0180;be\u2dea\u39cb\u39cfar;\u62bbq;\u625alip;\u62ee\u0100bt\u39dc\u1468a\xf2\u1469r;\uc000\ud835\udd33tr\xe9\u39aesu\u0100bp\u39ef\u39f1\xbb\u0d1c\xbb\u0d59pf;\uc000\ud835\udd67ro\xf0\u0efbtr\xe9\u39b4\u0100cu\u3a06\u3a0br;\uc000\ud835\udccb\u0100bp\u3a10\u3a18n\u0100Ee\u3980\u3a16\xbb\u397en\u0100Ee\u3992\u3a1e\xbb\u3990igzag;\u699a\u0380cefoprs\u3a36\u3a3b\u3a56\u3a5b\u3a54\u3a61\u3a6airc;\u4175\u0100di\u3a40\u3a51\u0100bg\u3a45\u3a49ar;\u6a5fe\u0100;q\u15fa\u3a4f;\u6259erp;\u6118r;\uc000\ud835\udd34pf;\uc000\ud835\udd68\u0100;e\u1479\u3a66at\xe8\u1479cr;\uc000\ud835\udccc\u0ae3\u178e\u3a87\0\u3a8b\0\u3a90\u3a9b\0\0\u3a9d\u3aa8\u3aab\u3aaf\0\0\u3ac3\u3ace\0\u3ad8\u17dc\u17dftr\xe9\u17d1r;\uc000\ud835\udd35\u0100Aa\u3a94\u3a97r\xf2\u03c3r\xf2\u09f6;\u43be\u0100Aa\u3aa1\u3aa4r\xf2\u03b8r\xf2\u09eba\xf0\u2713is;\u62fb\u0180dpt\u17a4\u3ab5\u3abe\u0100fl\u3aba\u17a9;\uc000\ud835\udd69im\xe5\u17b2\u0100Aa\u3ac7\u3acar\xf2\u03cer\xf2\u0a01\u0100cq\u3ad2\u17b8r;\uc000\ud835\udccd\u0100pt\u17d6\u3adcr\xe9\u17d4\u0400acefiosu\u3af0\u3afd\u3b08\u3b0c\u3b11\u3b15\u3b1b\u3b21c\u0100uy\u3af6\u3afbte\u803b\xfd\u40fd;\u444f\u0100iy\u3b02\u3b06rc;\u4177;\u444bn\u803b\xa5\u40a5r;\uc000\ud835\udd36cy;\u4457pf;\uc000\ud835\udd6acr;\uc000\ud835\udcce\u0100cm\u3b26\u3b29y;\u444el\u803b\xff\u40ff\u0500acdefhiosw\u3b42\u3b48\u3b54\u3b58\u3b64\u3b69\u3b6d\u3b74\u3b7a\u3b80cute;\u417a\u0100ay\u3b4d\u3b52ron;\u417e;\u4437ot;\u417c\u0100et\u3b5d\u3b61tr\xe6\u155fa;\u43b6r;\uc000\ud835\udd37cy;\u4436grarr;\u61ddpf;\uc000\ud835\udd6bcr;\uc000\ud835\udccf\u0100jn\u3b85\u3b87;\u600dj;\u600c" 6 | .split("") 7 | .map((c) => c.charCodeAt(0)), 8 | ); 9 | -------------------------------------------------------------------------------- /src/generated/decode-data-xml.ts: -------------------------------------------------------------------------------- 1 | // Generated using scripts/write-decode-map.ts 2 | 3 | export const xmlDecodeTree: Uint16Array = /* #__PURE__ */ new Uint16Array( 4 | // prettier-ignore 5 | /* #__PURE__ */ "\u0200aglq\t\x15\x18\x1b\u026d\x0f\0\0\x12p;\u4026os;\u4027t;\u403et;\u403cuot;\u4022" 6 | .split("") 7 | .map((c) => c.charCodeAt(0)), 8 | ); 9 | -------------------------------------------------------------------------------- /src/generated/encode-html.ts: -------------------------------------------------------------------------------- 1 | // Generated using scripts/write-encode-map.ts 2 | 3 | type EncodeTrieNode = 4 | | string 5 | | { v?: string; n: number | Map; o?: string }; 6 | 7 | function restoreDiff>( 8 | array: T, 9 | ): T { 10 | for (let index = 1; index < array.length; index++) { 11 | array[index][0] += array[index - 1][0] + 1; 12 | } 13 | return array; 14 | } 15 | 16 | // prettier-ignore 17 | export const htmlTrie: Map = /* #__PURE__ */ new Map(/* #__PURE__ */restoreDiff([[9," "],[0," "],[22,"!"],[0,"""],[0,"#"],[0,"$"],[0,"%"],[0,"&"],[0,"'"],[0,"("],[0,")"],[0,"*"],[0,"+"],[0,","],[1,"."],[0,"/"],[10,":"],[0,";"],[0,{v:"<",n:8402,o:"<⃒"}],[0,{v:"=",n:8421,o:"=⃥"}],[0,{v:">",n:8402,o:">⃒"}],[0,"?"],[0,"@"],[26,"["],[0,"\"],[0,"]"],[0,"^"],[0,"_"],[0,"`"],[5,{n:106,o:"fj"}],[20,"{"],[0,"|"],[0,"}"],[34," "],[0,"¡"],[0,"¢"],[0,"£"],[0,"¤"],[0,"¥"],[0,"¦"],[0,"§"],[0,"¨"],[0,"©"],[0,"ª"],[0,"«"],[0,"¬"],[0,"­"],[0,"®"],[0,"¯"],[0,"°"],[0,"±"],[0,"²"],[0,"³"],[0,"´"],[0,"µ"],[0,"¶"],[0,"·"],[0,"¸"],[0,"¹"],[0,"º"],[0,"»"],[0,"¼"],[0,"½"],[0,"¾"],[0,"¿"],[0,"À"],[0,"Á"],[0,"Â"],[0,"Ã"],[0,"Ä"],[0,"Å"],[0,"Æ"],[0,"Ç"],[0,"È"],[0,"É"],[0,"Ê"],[0,"Ë"],[0,"Ì"],[0,"Í"],[0,"Î"],[0,"Ï"],[0,"Ð"],[0,"Ñ"],[0,"Ò"],[0,"Ó"],[0,"Ô"],[0,"Õ"],[0,"Ö"],[0,"×"],[0,"Ø"],[0,"Ù"],[0,"Ú"],[0,"Û"],[0,"Ü"],[0,"Ý"],[0,"Þ"],[0,"ß"],[0,"à"],[0,"á"],[0,"â"],[0,"ã"],[0,"ä"],[0,"å"],[0,"æ"],[0,"ç"],[0,"è"],[0,"é"],[0,"ê"],[0,"ë"],[0,"ì"],[0,"í"],[0,"î"],[0,"ï"],[0,"ð"],[0,"ñ"],[0,"ò"],[0,"ó"],[0,"ô"],[0,"õ"],[0,"ö"],[0,"÷"],[0,"ø"],[0,"ù"],[0,"ú"],[0,"û"],[0,"ü"],[0,"ý"],[0,"þ"],[0,"ÿ"],[0,"Ā"],[0,"ā"],[0,"Ă"],[0,"ă"],[0,"Ą"],[0,"ą"],[0,"Ć"],[0,"ć"],[0,"Ĉ"],[0,"ĉ"],[0,"Ċ"],[0,"ċ"],[0,"Č"],[0,"č"],[0,"Ď"],[0,"ď"],[0,"Đ"],[0,"đ"],[0,"Ē"],[0,"ē"],[2,"Ė"],[0,"ė"],[0,"Ę"],[0,"ę"],[0,"Ě"],[0,"ě"],[0,"Ĝ"],[0,"ĝ"],[0,"Ğ"],[0,"ğ"],[0,"Ġ"],[0,"ġ"],[0,"Ģ"],[1,"Ĥ"],[0,"ĥ"],[0,"Ħ"],[0,"ħ"],[0,"Ĩ"],[0,"ĩ"],[0,"Ī"],[0,"ī"],[2,"Į"],[0,"į"],[0,"İ"],[0,"ı"],[0,"IJ"],[0,"ij"],[0,"Ĵ"],[0,"ĵ"],[0,"Ķ"],[0,"ķ"],[0,"ĸ"],[0,"Ĺ"],[0,"ĺ"],[0,"Ļ"],[0,"ļ"],[0,"Ľ"],[0,"ľ"],[0,"Ŀ"],[0,"ŀ"],[0,"Ł"],[0,"ł"],[0,"Ń"],[0,"ń"],[0,"Ņ"],[0,"ņ"],[0,"Ň"],[0,"ň"],[0,"ʼn"],[0,"Ŋ"],[0,"ŋ"],[0,"Ō"],[0,"ō"],[2,"Ő"],[0,"ő"],[0,"Œ"],[0,"œ"],[0,"Ŕ"],[0,"ŕ"],[0,"Ŗ"],[0,"ŗ"],[0,"Ř"],[0,"ř"],[0,"Ś"],[0,"ś"],[0,"Ŝ"],[0,"ŝ"],[0,"Ş"],[0,"ş"],[0,"Š"],[0,"š"],[0,"Ţ"],[0,"ţ"],[0,"Ť"],[0,"ť"],[0,"Ŧ"],[0,"ŧ"],[0,"Ũ"],[0,"ũ"],[0,"Ū"],[0,"ū"],[0,"Ŭ"],[0,"ŭ"],[0,"Ů"],[0,"ů"],[0,"Ű"],[0,"ű"],[0,"Ų"],[0,"ų"],[0,"Ŵ"],[0,"ŵ"],[0,"Ŷ"],[0,"ŷ"],[0,"Ÿ"],[0,"Ź"],[0,"ź"],[0,"Ż"],[0,"ż"],[0,"Ž"],[0,"ž"],[19,"ƒ"],[34,"Ƶ"],[63,"ǵ"],[65,"ȷ"],[142,"ˆ"],[0,"ˇ"],[16,"˘"],[0,"˙"],[0,"˚"],[0,"˛"],[0,"˜"],[0,"˝"],[51,"̑"],[127,"Α"],[0,"Β"],[0,"Γ"],[0,"Δ"],[0,"Ε"],[0,"Ζ"],[0,"Η"],[0,"Θ"],[0,"Ι"],[0,"Κ"],[0,"Λ"],[0,"Μ"],[0,"Ν"],[0,"Ξ"],[0,"Ο"],[0,"Π"],[0,"Ρ"],[1,"Σ"],[0,"Τ"],[0,"Υ"],[0,"Φ"],[0,"Χ"],[0,"Ψ"],[0,"Ω"],[7,"α"],[0,"β"],[0,"γ"],[0,"δ"],[0,"ε"],[0,"ζ"],[0,"η"],[0,"θ"],[0,"ι"],[0,"κ"],[0,"λ"],[0,"μ"],[0,"ν"],[0,"ξ"],[0,"ο"],[0,"π"],[0,"ρ"],[0,"ς"],[0,"σ"],[0,"τ"],[0,"υ"],[0,"φ"],[0,"χ"],[0,"ψ"],[0,"ω"],[7,"ϑ"],[0,"ϒ"],[2,"ϕ"],[0,"ϖ"],[5,"Ϝ"],[0,"ϝ"],[18,"ϰ"],[0,"ϱ"],[3,"ϵ"],[0,"϶"],[10,"Ё"],[0,"Ђ"],[0,"Ѓ"],[0,"Є"],[0,"Ѕ"],[0,"І"],[0,"Ї"],[0,"Ј"],[0,"Љ"],[0,"Њ"],[0,"Ћ"],[0,"Ќ"],[1,"Ў"],[0,"Џ"],[0,"А"],[0,"Б"],[0,"В"],[0,"Г"],[0,"Д"],[0,"Е"],[0,"Ж"],[0,"З"],[0,"И"],[0,"Й"],[0,"К"],[0,"Л"],[0,"М"],[0,"Н"],[0,"О"],[0,"П"],[0,"Р"],[0,"С"],[0,"Т"],[0,"У"],[0,"Ф"],[0,"Х"],[0,"Ц"],[0,"Ч"],[0,"Ш"],[0,"Щ"],[0,"Ъ"],[0,"Ы"],[0,"Ь"],[0,"Э"],[0,"Ю"],[0,"Я"],[0,"а"],[0,"б"],[0,"в"],[0,"г"],[0,"д"],[0,"е"],[0,"ж"],[0,"з"],[0,"и"],[0,"й"],[0,"к"],[0,"л"],[0,"м"],[0,"н"],[0,"о"],[0,"п"],[0,"р"],[0,"с"],[0,"т"],[0,"у"],[0,"ф"],[0,"х"],[0,"ц"],[0,"ч"],[0,"ш"],[0,"щ"],[0,"ъ"],[0,"ы"],[0,"ь"],[0,"э"],[0,"ю"],[0,"я"],[1,"ё"],[0,"ђ"],[0,"ѓ"],[0,"є"],[0,"ѕ"],[0,"і"],[0,"ї"],[0,"ј"],[0,"љ"],[0,"њ"],[0,"ћ"],[0,"ќ"],[1,"ў"],[0,"џ"],[7074," "],[0," "],[0," "],[0," "],[1," "],[0," "],[0," "],[0," "],[0,"​"],[0,"‌"],[0,"‍"],[0,"‎"],[0,"‏"],[0,"‐"],[2,"–"],[0,"—"],[0,"―"],[0,"‖"],[1,"‘"],[0,"’"],[0,"‚"],[1,"“"],[0,"”"],[0,"„"],[1,"†"],[0,"‡"],[0,"•"],[2,"‥"],[0,"…"],[9,"‰"],[0,"‱"],[0,"′"],[0,"″"],[0,"‴"],[0,"‵"],[3,"‹"],[0,"›"],[3,"‾"],[2,"⁁"],[1,"⁃"],[0,"⁄"],[10,"⁏"],[7,"⁗"],[7,{v:" ",n:8202,o:"  "}],[0,"⁠"],[0,"⁡"],[0,"⁢"],[0,"⁣"],[72,"€"],[46,"⃛"],[0,"⃜"],[37,"ℂ"],[2,"℅"],[4,"ℊ"],[0,"ℋ"],[0,"ℌ"],[0,"ℍ"],[0,"ℎ"],[0,"ℏ"],[0,"ℐ"],[0,"ℑ"],[0,"ℒ"],[0,"ℓ"],[1,"ℕ"],[0,"№"],[0,"℗"],[0,"℘"],[0,"ℙ"],[0,"ℚ"],[0,"ℛ"],[0,"ℜ"],[0,"ℝ"],[0,"℞"],[3,"™"],[1,"ℤ"],[2,"℧"],[0,"ℨ"],[0,"℩"],[2,"ℬ"],[0,"ℭ"],[1,"ℯ"],[0,"ℰ"],[0,"ℱ"],[1,"ℳ"],[0,"ℴ"],[0,"ℵ"],[0,"ℶ"],[0,"ℷ"],[0,"ℸ"],[12,"ⅅ"],[0,"ⅆ"],[0,"ⅇ"],[0,"ⅈ"],[10,"⅓"],[0,"⅔"],[0,"⅕"],[0,"⅖"],[0,"⅗"],[0,"⅘"],[0,"⅙"],[0,"⅚"],[0,"⅛"],[0,"⅜"],[0,"⅝"],[0,"⅞"],[49,"←"],[0,"↑"],[0,"→"],[0,"↓"],[0,"↔"],[0,"↕"],[0,"↖"],[0,"↗"],[0,"↘"],[0,"↙"],[0,"↚"],[0,"↛"],[1,{v:"↝",n:824,o:"↝̸"}],[0,"↞"],[0,"↟"],[0,"↠"],[0,"↡"],[0,"↢"],[0,"↣"],[0,"↤"],[0,"↥"],[0,"↦"],[0,"↧"],[1,"↩"],[0,"↪"],[0,"↫"],[0,"↬"],[0,"↭"],[0,"↮"],[1,"↰"],[0,"↱"],[0,"↲"],[0,"↳"],[1,"↵"],[0,"↶"],[0,"↷"],[2,"↺"],[0,"↻"],[0,"↼"],[0,"↽"],[0,"↾"],[0,"↿"],[0,"⇀"],[0,"⇁"],[0,"⇂"],[0,"⇃"],[0,"⇄"],[0,"⇅"],[0,"⇆"],[0,"⇇"],[0,"⇈"],[0,"⇉"],[0,"⇊"],[0,"⇋"],[0,"⇌"],[0,"⇍"],[0,"⇎"],[0,"⇏"],[0,"⇐"],[0,"⇑"],[0,"⇒"],[0,"⇓"],[0,"⇔"],[0,"⇕"],[0,"⇖"],[0,"⇗"],[0,"⇘"],[0,"⇙"],[0,"⇚"],[0,"⇛"],[1,"⇝"],[6,"⇤"],[0,"⇥"],[15,"⇵"],[7,"⇽"],[0,"⇾"],[0,"⇿"],[0,"∀"],[0,"∁"],[0,{v:"∂",n:824,o:"∂̸"}],[0,"∃"],[0,"∄"],[0,"∅"],[1,"∇"],[0,"∈"],[0,"∉"],[1,"∋"],[0,"∌"],[2,"∏"],[0,"∐"],[0,"∑"],[0,"−"],[0,"∓"],[0,"∔"],[1,"∖"],[0,"∗"],[0,"∘"],[1,"√"],[2,"∝"],[0,"∞"],[0,"∟"],[0,{v:"∠",n:8402,o:"∠⃒"}],[0,"∡"],[0,"∢"],[0,"∣"],[0,"∤"],[0,"∥"],[0,"∦"],[0,"∧"],[0,"∨"],[0,{v:"∩",n:65024,o:"∩︀"}],[0,{v:"∪",n:65024,o:"∪︀"}],[0,"∫"],[0,"∬"],[0,"∭"],[0,"∮"],[0,"∯"],[0,"∰"],[0,"∱"],[0,"∲"],[0,"∳"],[0,"∴"],[0,"∵"],[0,"∶"],[0,"∷"],[0,"∸"],[1,"∺"],[0,"∻"],[0,{v:"∼",n:8402,o:"∼⃒"}],[0,{v:"∽",n:817,o:"∽̱"}],[0,{v:"∾",n:819,o:"∾̳"}],[0,"∿"],[0,"≀"],[0,"≁"],[0,{v:"≂",n:824,o:"≂̸"}],[0,"≃"],[0,"≄"],[0,"≅"],[0,"≆"],[0,"≇"],[0,"≈"],[0,"≉"],[0,"≊"],[0,{v:"≋",n:824,o:"≋̸"}],[0,"≌"],[0,{v:"≍",n:8402,o:"≍⃒"}],[0,{v:"≎",n:824,o:"≎̸"}],[0,{v:"≏",n:824,o:"≏̸"}],[0,{v:"≐",n:824,o:"≐̸"}],[0,"≑"],[0,"≒"],[0,"≓"],[0,"≔"],[0,"≕"],[0,"≖"],[0,"≗"],[1,"≙"],[0,"≚"],[1,"≜"],[2,"≟"],[0,"≠"],[0,{v:"≡",n:8421,o:"≡⃥"}],[0,"≢"],[1,{v:"≤",n:8402,o:"≤⃒"}],[0,{v:"≥",n:8402,o:"≥⃒"}],[0,{v:"≦",n:824,o:"≦̸"}],[0,{v:"≧",n:824,o:"≧̸"}],[0,{v:"≨",n:65024,o:"≨︀"}],[0,{v:"≩",n:65024,o:"≩︀"}],[0,{v:"≪",n:/* #__PURE__ */ new Map(/* #__PURE__ */restoreDiff([[824,"≪̸"],[7577,"≪⃒"]]))}],[0,{v:"≫",n:/* #__PURE__ */ new Map(/* #__PURE__ */restoreDiff([[824,"≫̸"],[7577,"≫⃒"]]))}],[0,"≬"],[0,"≭"],[0,"≮"],[0,"≯"],[0,"≰"],[0,"≱"],[0,"≲"],[0,"≳"],[0,"≴"],[0,"≵"],[0,"≶"],[0,"≷"],[0,"≸"],[0,"≹"],[0,"≺"],[0,"≻"],[0,"≼"],[0,"≽"],[0,"≾"],[0,{v:"≿",n:824,o:"≿̸"}],[0,"⊀"],[0,"⊁"],[0,{v:"⊂",n:8402,o:"⊂⃒"}],[0,{v:"⊃",n:8402,o:"⊃⃒"}],[0,"⊄"],[0,"⊅"],[0,"⊆"],[0,"⊇"],[0,"⊈"],[0,"⊉"],[0,{v:"⊊",n:65024,o:"⊊︀"}],[0,{v:"⊋",n:65024,o:"⊋︀"}],[1,"⊍"],[0,"⊎"],[0,{v:"⊏",n:824,o:"⊏̸"}],[0,{v:"⊐",n:824,o:"⊐̸"}],[0,"⊑"],[0,"⊒"],[0,{v:"⊓",n:65024,o:"⊓︀"}],[0,{v:"⊔",n:65024,o:"⊔︀"}],[0,"⊕"],[0,"⊖"],[0,"⊗"],[0,"⊘"],[0,"⊙"],[0,"⊚"],[0,"⊛"],[1,"⊝"],[0,"⊞"],[0,"⊟"],[0,"⊠"],[0,"⊡"],[0,"⊢"],[0,"⊣"],[0,"⊤"],[0,"⊥"],[1,"⊧"],[0,"⊨"],[0,"⊩"],[0,"⊪"],[0,"⊫"],[0,"⊬"],[0,"⊭"],[0,"⊮"],[0,"⊯"],[0,"⊰"],[1,"⊲"],[0,"⊳"],[0,{v:"⊴",n:8402,o:"⊴⃒"}],[0,{v:"⊵",n:8402,o:"⊵⃒"}],[0,"⊶"],[0,"⊷"],[0,"⊸"],[0,"⊹"],[0,"⊺"],[0,"⊻"],[1,"⊽"],[0,"⊾"],[0,"⊿"],[0,"⋀"],[0,"⋁"],[0,"⋂"],[0,"⋃"],[0,"⋄"],[0,"⋅"],[0,"⋆"],[0,"⋇"],[0,"⋈"],[0,"⋉"],[0,"⋊"],[0,"⋋"],[0,"⋌"],[0,"⋍"],[0,"⋎"],[0,"⋏"],[0,"⋐"],[0,"⋑"],[0,"⋒"],[0,"⋓"],[0,"⋔"],[0,"⋕"],[0,"⋖"],[0,"⋗"],[0,{v:"⋘",n:824,o:"⋘̸"}],[0,{v:"⋙",n:824,o:"⋙̸"}],[0,{v:"⋚",n:65024,o:"⋚︀"}],[0,{v:"⋛",n:65024,o:"⋛︀"}],[2,"⋞"],[0,"⋟"],[0,"⋠"],[0,"⋡"],[0,"⋢"],[0,"⋣"],[2,"⋦"],[0,"⋧"],[0,"⋨"],[0,"⋩"],[0,"⋪"],[0,"⋫"],[0,"⋬"],[0,"⋭"],[0,"⋮"],[0,"⋯"],[0,"⋰"],[0,"⋱"],[0,"⋲"],[0,"⋳"],[0,"⋴"],[0,{v:"⋵",n:824,o:"⋵̸"}],[0,"⋶"],[0,"⋷"],[1,{v:"⋹",n:824,o:"⋹̸"}],[0,"⋺"],[0,"⋻"],[0,"⋼"],[0,"⋽"],[0,"⋾"],[6,"⌅"],[0,"⌆"],[1,"⌈"],[0,"⌉"],[0,"⌊"],[0,"⌋"],[0,"⌌"],[0,"⌍"],[0,"⌎"],[0,"⌏"],[0,"⌐"],[1,"⌒"],[0,"⌓"],[1,"⌕"],[0,"⌖"],[5,"⌜"],[0,"⌝"],[0,"⌞"],[0,"⌟"],[2,"⌢"],[0,"⌣"],[9,"⌭"],[0,"⌮"],[7,"⌶"],[6,"⌽"],[1,"⌿"],[60,"⍼"],[51,"⎰"],[0,"⎱"],[2,"⎴"],[0,"⎵"],[0,"⎶"],[37,"⏜"],[0,"⏝"],[0,"⏞"],[0,"⏟"],[2,"⏢"],[4,"⏧"],[59,"␣"],[164,"Ⓢ"],[55,"─"],[1,"│"],[9,"┌"],[3,"┐"],[3,"└"],[3,"┘"],[3,"├"],[7,"┤"],[7,"┬"],[7,"┴"],[7,"┼"],[19,"═"],[0,"║"],[0,"╒"],[0,"╓"],[0,"╔"],[0,"╕"],[0,"╖"],[0,"╗"],[0,"╘"],[0,"╙"],[0,"╚"],[0,"╛"],[0,"╜"],[0,"╝"],[0,"╞"],[0,"╟"],[0,"╠"],[0,"╡"],[0,"╢"],[0,"╣"],[0,"╤"],[0,"╥"],[0,"╦"],[0,"╧"],[0,"╨"],[0,"╩"],[0,"╪"],[0,"╫"],[0,"╬"],[19,"▀"],[3,"▄"],[3,"█"],[8,"░"],[0,"▒"],[0,"▓"],[13,"□"],[8,"▪"],[0,"▫"],[1,"▭"],[0,"▮"],[2,"▱"],[1,"△"],[0,"▴"],[0,"▵"],[2,"▸"],[0,"▹"],[3,"▽"],[0,"▾"],[0,"▿"],[2,"◂"],[0,"◃"],[6,"◊"],[0,"○"],[32,"◬"],[2,"◯"],[8,"◸"],[0,"◹"],[0,"◺"],[0,"◻"],[0,"◼"],[8,"★"],[0,"☆"],[7,"☎"],[49,"♀"],[1,"♂"],[29,"♠"],[2,"♣"],[1,"♥"],[0,"♦"],[3,"♪"],[2,"♭"],[0,"♮"],[0,"♯"],[163,"✓"],[3,"✗"],[8,"✠"],[21,"✶"],[33,"❘"],[25,"❲"],[0,"❳"],[84,"⟈"],[0,"⟉"],[28,"⟦"],[0,"⟧"],[0,"⟨"],[0,"⟩"],[0,"⟪"],[0,"⟫"],[0,"⟬"],[0,"⟭"],[7,"⟵"],[0,"⟶"],[0,"⟷"],[0,"⟸"],[0,"⟹"],[0,"⟺"],[1,"⟼"],[2,"⟿"],[258,"⤂"],[0,"⤃"],[0,"⤄"],[0,"⤅"],[6,"⤌"],[0,"⤍"],[0,"⤎"],[0,"⤏"],[0,"⤐"],[0,"⤑"],[0,"⤒"],[0,"⤓"],[2,"⤖"],[2,"⤙"],[0,"⤚"],[0,"⤛"],[0,"⤜"],[0,"⤝"],[0,"⤞"],[0,"⤟"],[0,"⤠"],[2,"⤣"],[0,"⤤"],[0,"⤥"],[0,"⤦"],[0,"⤧"],[0,"⤨"],[0,"⤩"],[0,"⤪"],[8,{v:"⤳",n:824,o:"⤳̸"}],[1,"⤵"],[0,"⤶"],[0,"⤷"],[0,"⤸"],[0,"⤹"],[2,"⤼"],[0,"⤽"],[7,"⥅"],[2,"⥈"],[0,"⥉"],[0,"⥊"],[0,"⥋"],[2,"⥎"],[0,"⥏"],[0,"⥐"],[0,"⥑"],[0,"⥒"],[0,"⥓"],[0,"⥔"],[0,"⥕"],[0,"⥖"],[0,"⥗"],[0,"⥘"],[0,"⥙"],[0,"⥚"],[0,"⥛"],[0,"⥜"],[0,"⥝"],[0,"⥞"],[0,"⥟"],[0,"⥠"],[0,"⥡"],[0,"⥢"],[0,"⥣"],[0,"⥤"],[0,"⥥"],[0,"⥦"],[0,"⥧"],[0,"⥨"],[0,"⥩"],[0,"⥪"],[0,"⥫"],[0,"⥬"],[0,"⥭"],[0,"⥮"],[0,"⥯"],[0,"⥰"],[0,"⥱"],[0,"⥲"],[0,"⥳"],[0,"⥴"],[0,"⥵"],[0,"⥶"],[1,"⥸"],[0,"⥹"],[1,"⥻"],[0,"⥼"],[0,"⥽"],[0,"⥾"],[0,"⥿"],[5,"⦅"],[0,"⦆"],[4,"⦋"],[0,"⦌"],[0,"⦍"],[0,"⦎"],[0,"⦏"],[0,"⦐"],[0,"⦑"],[0,"⦒"],[0,"⦓"],[0,"⦔"],[0,"⦕"],[0,"⦖"],[3,"⦚"],[1,"⦜"],[0,"⦝"],[6,"⦤"],[0,"⦥"],[0,"⦦"],[0,"⦧"],[0,"⦨"],[0,"⦩"],[0,"⦪"],[0,"⦫"],[0,"⦬"],[0,"⦭"],[0,"⦮"],[0,"⦯"],[0,"⦰"],[0,"⦱"],[0,"⦲"],[0,"⦳"],[0,"⦴"],[0,"⦵"],[0,"⦶"],[0,"⦷"],[1,"⦹"],[1,"⦻"],[0,"⦼"],[1,"⦾"],[0,"⦿"],[0,"⧀"],[0,"⧁"],[0,"⧂"],[0,"⧃"],[0,"⧄"],[0,"⧅"],[3,"⧉"],[3,"⧍"],[0,"⧎"],[0,{v:"⧏",n:824,o:"⧏̸"}],[0,{v:"⧐",n:824,o:"⧐̸"}],[11,"⧜"],[0,"⧝"],[0,"⧞"],[4,"⧣"],[0,"⧤"],[0,"⧥"],[5,"⧫"],[8,"⧴"],[1,"⧶"],[9,"⨀"],[0,"⨁"],[0,"⨂"],[1,"⨄"],[1,"⨆"],[5,"⨌"],[0,"⨍"],[2,"⨐"],[0,"⨑"],[0,"⨒"],[0,"⨓"],[0,"⨔"],[0,"⨕"],[0,"⨖"],[0,"⨗"],[10,"⨢"],[0,"⨣"],[0,"⨤"],[0,"⨥"],[0,"⨦"],[0,"⨧"],[1,"⨩"],[0,"⨪"],[2,"⨭"],[0,"⨮"],[0,"⨯"],[0,"⨰"],[0,"⨱"],[1,"⨳"],[0,"⨴"],[0,"⨵"],[0,"⨶"],[0,"⨷"],[0,"⨸"],[0,"⨹"],[0,"⨺"],[0,"⨻"],[0,"⨼"],[2,"⨿"],[0,"⩀"],[1,"⩂"],[0,"⩃"],[0,"⩄"],[0,"⩅"],[0,"⩆"],[0,"⩇"],[0,"⩈"],[0,"⩉"],[0,"⩊"],[0,"⩋"],[0,"⩌"],[0,"⩍"],[2,"⩐"],[2,"⩓"],[0,"⩔"],[0,"⩕"],[0,"⩖"],[0,"⩗"],[0,"⩘"],[1,"⩚"],[0,"⩛"],[0,"⩜"],[0,"⩝"],[1,"⩟"],[6,"⩦"],[3,"⩪"],[2,{v:"⩭",n:824,o:"⩭̸"}],[0,"⩮"],[0,"⩯"],[0,{v:"⩰",n:824,o:"⩰̸"}],[0,"⩱"],[0,"⩲"],[0,"⩳"],[0,"⩴"],[0,"⩵"],[1,"⩷"],[0,"⩸"],[0,"⩹"],[0,"⩺"],[0,"⩻"],[0,"⩼"],[0,{v:"⩽",n:824,o:"⩽̸"}],[0,{v:"⩾",n:824,o:"⩾̸"}],[0,"⩿"],[0,"⪀"],[0,"⪁"],[0,"⪂"],[0,"⪃"],[0,"⪄"],[0,"⪅"],[0,"⪆"],[0,"⪇"],[0,"⪈"],[0,"⪉"],[0,"⪊"],[0,"⪋"],[0,"⪌"],[0,"⪍"],[0,"⪎"],[0,"⪏"],[0,"⪐"],[0,"⪑"],[0,"⪒"],[0,"⪓"],[0,"⪔"],[0,"⪕"],[0,"⪖"],[0,"⪗"],[0,"⪘"],[0,"⪙"],[0,"⪚"],[2,"⪝"],[0,"⪞"],[0,"⪟"],[0,"⪠"],[0,{v:"⪡",n:824,o:"⪡̸"}],[0,{v:"⪢",n:824,o:"⪢̸"}],[1,"⪤"],[0,"⪥"],[0,"⪦"],[0,"⪧"],[0,"⪨"],[0,"⪩"],[0,"⪪"],[0,"⪫"],[0,{v:"⪬",n:65024,o:"⪬︀"}],[0,{v:"⪭",n:65024,o:"⪭︀"}],[0,"⪮"],[0,{v:"⪯",n:824,o:"⪯̸"}],[0,{v:"⪰",n:824,o:"⪰̸"}],[2,"⪳"],[0,"⪴"],[0,"⪵"],[0,"⪶"],[0,"⪷"],[0,"⪸"],[0,"⪹"],[0,"⪺"],[0,"⪻"],[0,"⪼"],[0,"⪽"],[0,"⪾"],[0,"⪿"],[0,"⫀"],[0,"⫁"],[0,"⫂"],[0,"⫃"],[0,"⫄"],[0,{v:"⫅",n:824,o:"⫅̸"}],[0,{v:"⫆",n:824,o:"⫆̸"}],[0,"⫇"],[0,"⫈"],[2,{v:"⫋",n:65024,o:"⫋︀"}],[0,{v:"⫌",n:65024,o:"⫌︀"}],[2,"⫏"],[0,"⫐"],[0,"⫑"],[0,"⫒"],[0,"⫓"],[0,"⫔"],[0,"⫕"],[0,"⫖"],[0,"⫗"],[0,"⫘"],[0,"⫙"],[0,"⫚"],[0,"⫛"],[8,"⫤"],[1,"⫦"],[0,"⫧"],[0,"⫨"],[0,"⫩"],[1,"⫫"],[0,"⫬"],[0,"⫭"],[0,"⫮"],[0,"⫯"],[0,"⫰"],[0,"⫱"],[0,"⫲"],[0,"⫳"],[9,{v:"⫽",n:8421,o:"⫽⃥"}],[44343,{n:/* #__PURE__ */ new Map(/* #__PURE__ */restoreDiff([[56476,"𝒜"],[1,"𝒞"],[0,"𝒟"],[2,"𝒢"],[2,"𝒥"],[0,"𝒦"],[2,"𝒩"],[0,"𝒪"],[0,"𝒫"],[0,"𝒬"],[1,"𝒮"],[0,"𝒯"],[0,"𝒰"],[0,"𝒱"],[0,"𝒲"],[0,"𝒳"],[0,"𝒴"],[0,"𝒵"],[0,"𝒶"],[0,"𝒷"],[0,"𝒸"],[0,"𝒹"],[1,"𝒻"],[1,"𝒽"],[0,"𝒾"],[0,"𝒿"],[0,"𝓀"],[0,"𝓁"],[0,"𝓂"],[0,"𝓃"],[1,"𝓅"],[0,"𝓆"],[0,"𝓇"],[0,"𝓈"],[0,"𝓉"],[0,"𝓊"],[0,"𝓋"],[0,"𝓌"],[0,"𝓍"],[0,"𝓎"],[0,"𝓏"],[52,"𝔄"],[0,"𝔅"],[1,"𝔇"],[0,"𝔈"],[0,"𝔉"],[0,"𝔊"],[2,"𝔍"],[0,"𝔎"],[0,"𝔏"],[0,"𝔐"],[0,"𝔑"],[0,"𝔒"],[0,"𝔓"],[0,"𝔔"],[1,"𝔖"],[0,"𝔗"],[0,"𝔘"],[0,"𝔙"],[0,"𝔚"],[0,"𝔛"],[0,"𝔜"],[1,"𝔞"],[0,"𝔟"],[0,"𝔠"],[0,"𝔡"],[0,"𝔢"],[0,"𝔣"],[0,"𝔤"],[0,"𝔥"],[0,"𝔦"],[0,"𝔧"],[0,"𝔨"],[0,"𝔩"],[0,"𝔪"],[0,"𝔫"],[0,"𝔬"],[0,"𝔭"],[0,"𝔮"],[0,"𝔯"],[0,"𝔰"],[0,"𝔱"],[0,"𝔲"],[0,"𝔳"],[0,"𝔴"],[0,"𝔵"],[0,"𝔶"],[0,"𝔷"],[0,"𝔸"],[0,"𝔹"],[1,"𝔻"],[0,"𝔼"],[0,"𝔽"],[0,"𝔾"],[1,"𝕀"],[0,"𝕁"],[0,"𝕂"],[0,"𝕃"],[0,"𝕄"],[1,"𝕆"],[3,"𝕊"],[0,"𝕋"],[0,"𝕌"],[0,"𝕍"],[0,"𝕎"],[0,"𝕏"],[0,"𝕐"],[1,"𝕒"],[0,"𝕓"],[0,"𝕔"],[0,"𝕕"],[0,"𝕖"],[0,"𝕗"],[0,"𝕘"],[0,"𝕙"],[0,"𝕚"],[0,"𝕛"],[0,"𝕜"],[0,"𝕝"],[0,"𝕞"],[0,"𝕟"],[0,"𝕠"],[0,"𝕡"],[0,"𝕢"],[0,"𝕣"],[0,"𝕤"],[0,"𝕥"],[0,"𝕦"],[0,"𝕧"],[0,"𝕨"],[0,"𝕩"],[0,"𝕪"],[0,"𝕫"]]))}],[8906,"ff"],[0,"fi"],[0,"fl"],[0,"ffi"],[0,"ffl"]])); 18 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { describe, it, expect } from "vitest"; 3 | import * as entities from "./index.js"; 4 | import legacy from "../maps/legacy.json" with { type: "json" }; 5 | 6 | const levels = ["xml", "entities"]; 7 | 8 | describe("Documents", () => { 9 | const levelDocuments = levels 10 | .map((name) => new URL(`../maps/${name}.json`, import.meta.url)) 11 | .map((url) => JSON.parse(readFileSync(url, "utf8"))) 12 | .map((document, index) => [index, document]); 13 | 14 | for (const [level, document] of levelDocuments) { 15 | describe("Decode", () => { 16 | it(levels[level], () => { 17 | for (const entity of Object.keys(document)) { 18 | for (let l = level; l < levels.length; l++) { 19 | expect(entities.decode(`&${entity};`, l)).toBe( 20 | document[entity], 21 | ); 22 | expect( 23 | entities.decode(`&${entity};`, { level: l }), 24 | ).toBe(document[entity]); 25 | } 26 | } 27 | }); 28 | }); 29 | 30 | describe("Decode strict", () => { 31 | it(levels[level], () => { 32 | for (const entity of Object.keys(document)) { 33 | for (let l = level; l < levels.length; l++) { 34 | expect(entities.decodeStrict(`&${entity};`, l)).toBe( 35 | document[entity], 36 | ); 37 | expect( 38 | entities.decode(`&${entity};`, { 39 | level: l, 40 | mode: entities.DecodingMode.Strict, 41 | }), 42 | ).toBe(document[entity]); 43 | } 44 | } 45 | }); 46 | }); 47 | 48 | describe("Encode", () => { 49 | it(levels[level], () => { 50 | for (const entity of Object.keys(document)) { 51 | for (let l = level; l < levels.length; l++) { 52 | const encoded = entities.encode(document[entity], l); 53 | const decoded = entities.decode(encoded, l); 54 | expect(decoded).toBe(document[entity]); 55 | } 56 | } 57 | }); 58 | 59 | it("should only encode non-ASCII values if asked", () => 60 | expect( 61 | entities.encode("Great #'s of 🎁", { 62 | level, 63 | mode: entities.EncodingMode.ASCII, 64 | }), 65 | ).toBe("Great #'s of 🎁")); 66 | }); 67 | } 68 | 69 | describe("Legacy", () => { 70 | const legacyMap: Record = legacy; 71 | it("should decode", () => { 72 | for (const entity of Object.keys(legacyMap)) { 73 | expect(entities.decodeHTML(`&${entity}`)).toBe( 74 | legacyMap[entity], 75 | ); 76 | expect( 77 | entities.decodeStrict(`&${entity}`, { 78 | level: entities.EntityLevel.HTML, 79 | mode: entities.DecodingMode.Legacy, 80 | }), 81 | ).toBe(legacyMap[entity]); 82 | } 83 | }); 84 | }); 85 | }); 86 | 87 | const astral = [ 88 | ["1d306", "\uD834\uDF06"], 89 | ["1d11e", "\uD834\uDD1E"], 90 | ]; 91 | 92 | const astralSpecial = [ 93 | ["80", "\u20AC"], 94 | ["110000", "\uFFFD"], 95 | ]; 96 | 97 | describe("Astral entities", () => { 98 | for (const [c, value] of astral) { 99 | it(`should decode ${value}`, () => 100 | expect(entities.decode(`&#x${c};`)).toBe(value)); 101 | 102 | it(`should encode ${value}`, () => 103 | expect(entities.encode(value)).toBe(`&#x${c};`)); 104 | 105 | it(`should escape ${value}`, () => 106 | expect(entities.escape(value)).toBe(`&#x${c};`)); 107 | } 108 | 109 | for (const [c, value] of astralSpecial) { 110 | it(`should decode special \\u${c}`, () => 111 | expect(entities.decode(`&#x${c};`)).toBe(value)); 112 | } 113 | }); 114 | 115 | describe("Escape", () => { 116 | it("should always decode ASCII chars", () => { 117 | for (let index = 0; index < 0x7f; index++) { 118 | const c = String.fromCharCode(index); 119 | expect(entities.decodeXML(entities.escape(c))).toBe(c); 120 | } 121 | }); 122 | 123 | it("should keep UTF8 characters", () => 124 | expect(entities.escapeUTF8('ß < "ü"')).toBe(`ß < "ü"`)); 125 | }); 126 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { decodeXML, decodeHTML, DecodingMode } from "./decode.js"; 2 | import { encodeHTML, encodeNonAsciiHTML } from "./encode.js"; 3 | import { 4 | encodeXML, 5 | escapeUTF8, 6 | escapeAttribute, 7 | escapeText, 8 | } from "./escape.js"; 9 | 10 | /** The level of entities to support. */ 11 | export enum EntityLevel { 12 | /** Support only XML entities. */ 13 | XML = 0, 14 | /** Support HTML entities, which are a superset of XML entities. */ 15 | HTML = 1, 16 | } 17 | 18 | export enum EncodingMode { 19 | /** 20 | * The output is UTF-8 encoded. Only characters that need escaping within 21 | * XML will be escaped. 22 | */ 23 | UTF8, 24 | /** 25 | * The output consists only of ASCII characters. Characters that need 26 | * escaping within HTML, and characters that aren't ASCII characters will 27 | * be escaped. 28 | */ 29 | ASCII, 30 | /** 31 | * Encode all characters that have an equivalent entity, as well as all 32 | * characters that are not ASCII characters. 33 | */ 34 | Extensive, 35 | /** 36 | * Encode all characters that have to be escaped in HTML attributes, 37 | * following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}. 38 | */ 39 | Attribute, 40 | /** 41 | * Encode all characters that have to be escaped in HTML text, 42 | * following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}. 43 | */ 44 | Text, 45 | } 46 | 47 | export interface DecodingOptions { 48 | /** 49 | * The level of entities to support. 50 | * @default {@link EntityLevel.XML} 51 | */ 52 | level?: EntityLevel; 53 | /** 54 | * Decoding mode. If `Legacy`, will support legacy entities not terminated 55 | * with a semicolon (`;`). 56 | * 57 | * Always `Strict` for XML. For HTML, set this to `true` if you are parsing 58 | * an attribute value. 59 | * 60 | * The deprecated `decodeStrict` function defaults this to `Strict`. 61 | * 62 | * @default {@link DecodingMode.Legacy} 63 | */ 64 | mode?: DecodingMode | undefined; 65 | } 66 | 67 | /** 68 | * Decodes a string with entities. 69 | * 70 | * @param input String to decode. 71 | * @param options Decoding options. 72 | */ 73 | export function decode( 74 | input: string, 75 | options: DecodingOptions | EntityLevel = EntityLevel.XML, 76 | ): string { 77 | const level = typeof options === "number" ? options : options.level; 78 | 79 | if (level === EntityLevel.HTML) { 80 | const mode = typeof options === "object" ? options.mode : undefined; 81 | return decodeHTML(input, mode); 82 | } 83 | 84 | return decodeXML(input); 85 | } 86 | 87 | /** 88 | * Decodes a string with entities. Does not allow missing trailing semicolons for entities. 89 | * 90 | * @param input String to decode. 91 | * @param options Decoding options. 92 | * @deprecated Use `decode` with the `mode` set to `Strict`. 93 | */ 94 | export function decodeStrict( 95 | input: string, 96 | options: DecodingOptions | EntityLevel = EntityLevel.XML, 97 | ): string { 98 | const normalizedOptions = 99 | typeof options === "number" ? { level: options } : options; 100 | normalizedOptions.mode ??= DecodingMode.Strict; 101 | 102 | return decode(input, normalizedOptions); 103 | } 104 | 105 | /** 106 | * Options for `encode`. 107 | */ 108 | export interface EncodingOptions { 109 | /** 110 | * The level of entities to support. 111 | * @default {@link EntityLevel.XML} 112 | */ 113 | level?: EntityLevel; 114 | /** 115 | * Output format. 116 | * @default {@link EncodingMode.Extensive} 117 | */ 118 | mode?: EncodingMode; 119 | } 120 | 121 | /** 122 | * Encodes a string with entities. 123 | * 124 | * @param input String to encode. 125 | * @param options Encoding options. 126 | */ 127 | export function encode( 128 | input: string, 129 | options: EncodingOptions | EntityLevel = EntityLevel.XML, 130 | ): string { 131 | const { mode = EncodingMode.Extensive, level = EntityLevel.XML } = 132 | typeof options === "number" ? { level: options } : options; 133 | 134 | switch (mode) { 135 | case EncodingMode.UTF8: { 136 | return escapeUTF8(input); 137 | } 138 | case EncodingMode.Attribute: { 139 | return escapeAttribute(input); 140 | } 141 | case EncodingMode.Text: { 142 | return escapeText(input); 143 | } 144 | case EncodingMode.ASCII: { 145 | return level === EntityLevel.HTML 146 | ? encodeNonAsciiHTML(input) 147 | : encodeXML(input); 148 | } 149 | // eslint-disable-next-line unicorn/no-useless-switch-case 150 | case EncodingMode.Extensive: 151 | default: { 152 | return level === EntityLevel.HTML 153 | ? encodeHTML(input) 154 | : encodeXML(input); 155 | } 156 | } 157 | } 158 | 159 | export { 160 | encodeXML, 161 | escape, 162 | escapeUTF8, 163 | escapeAttribute, 164 | escapeText, 165 | } from "./escape.js"; 166 | 167 | export { 168 | encodeHTML, 169 | encodeNonAsciiHTML, 170 | // Legacy aliases (deprecated) 171 | encodeHTML as encodeHTML4, 172 | encodeHTML as encodeHTML5, 173 | } from "./encode.js"; 174 | 175 | export { 176 | EntityDecoder, 177 | DecodingMode, 178 | decodeXML, 179 | decodeHTML, 180 | decodeHTMLStrict, 181 | decodeHTMLAttribute, 182 | // Legacy aliases (deprecated) 183 | decodeHTML as decodeHTML4, 184 | decodeHTML as decodeHTML5, 185 | decodeHTMLStrict as decodeHTML4Strict, 186 | decodeHTMLStrict as decodeHTML5Strict, 187 | decodeXML as decodeXMLStrict, 188 | } from "./decode.js"; 189 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2019", 5 | "module": "nodenext", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | 10 | /* Strict Type-Checking Options */ 11 | "strict": true, 12 | 13 | /* Additional Checks */ 14 | "exactOptionalPropertyTypes": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "isolatedDeclarations": true, 17 | "isolatedModules": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noImplicitOverride": true, 20 | "noImplicitReturns": true, 21 | "noPropertyAccessFromIndexSignature": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | 25 | /* Module Resolution Options */ 26 | "esModuleInterop": true, 27 | "resolveJsonModule": true 28 | } 29 | } 30 | --------------------------------------------------------------------------------