├── .prettierignore ├── .npmrc ├── .gitignore ├── test ├── index.js ├── svg.js ├── core.js ├── select.js ├── select-all.js └── matches.js ├── index.js ├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── tsconfig.json ├── lib ├── id.js ├── name.js ├── parse.js ├── class-name.js ├── test.js ├── attribute.js ├── enter-state.js ├── index.js ├── walk.js └── pseudo.js ├── license ├── package.json └── readme.md /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | *.log 3 | *.map 4 | *.tsbuildinfo 5 | .DS_Store 6 | coverage/ 7 | node_modules/ 8 | yarn.lock 9 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import './core.js' 2 | import './matches.js' 3 | import './select.js' 4 | import './select-all.js' 5 | import './svg.js' 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./lib/index.js').Space} Space 3 | */ 4 | 5 | export {matches, select, selectAll} from './lib/index.js' 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | name: bb 2 | on: 3 | issues: 4 | types: [closed, edited, labeled, opened, reopened, unlabeled] 5 | pull_request_target: 6 | types: [closed, edited, labeled, opened, reopened, unlabeled] 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: unifiedjs/beep-boop-beta@main 12 | with: 13 | repo-token: ${{secrets.GITHUB_TOKEN}} 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declarationMap": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "strict": true, 12 | "target": "es2022" 13 | }, 14 | "exclude": ["coverage/", "node_modules/"], 15 | "include": ["**/*.js"] 16 | } 17 | -------------------------------------------------------------------------------- /lib/id.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {AstId} from 'css-selector-parser' 3 | * @import {Element} from 'hast' 4 | */ 5 | 6 | /** 7 | * Check whether an element has an ID. 8 | * 9 | * @param {AstId} query 10 | * AST rule (with `ids`). 11 | * @param {Element} element 12 | * Element. 13 | * @returns {boolean} 14 | * Whether `element` matches `query`. 15 | */ 16 | export function id(query, element) { 17 | return element.properties.id === query.name 18 | } 19 | -------------------------------------------------------------------------------- /lib/name.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {AstTagName} from 'css-selector-parser' 3 | * @import {Element} from 'hast' 4 | */ 5 | 6 | /** 7 | * Check whether an element has a tag name. 8 | * 9 | * @param {AstTagName} query 10 | * AST rule (with `tag`). 11 | * @param {Element} element 12 | * Element. 13 | * @returns {boolean} 14 | * Whether `element` matches `query`. 15 | */ 16 | export function name(query, element) { 17 | return query.name === element.tagName 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | name: ${{matrix.node}} 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v4 7 | - uses: actions/setup-node@v4 8 | with: 9 | node-version: ${{matrix.node}} 10 | - run: npm install 11 | - run: npm test 12 | - uses: codecov/codecov-action@v5 13 | strategy: 14 | matrix: 15 | node: 16 | - lts/hydrogen 17 | - node 18 | name: main 19 | on: 20 | - pull_request 21 | - push 22 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {AstSelector} from 'css-selector-parser' 3 | */ 4 | 5 | import {createParser} from 'css-selector-parser' 6 | 7 | const cssSelectorParse = createParser({syntax: 'selectors-4'}) 8 | 9 | /** 10 | * @param {string} selector 11 | * Selector to parse. 12 | * @returns {AstSelector} 13 | * Parsed selector. 14 | */ 15 | export function parse(selector) { 16 | if (typeof selector !== 'string') { 17 | throw new TypeError('Expected `string` as selector, not `' + selector + '`') 18 | } 19 | 20 | return cssSelectorParse(selector) 21 | } 22 | -------------------------------------------------------------------------------- /lib/class-name.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {AstClassName} from 'css-selector-parser' 3 | * @import {Element} from 'hast' 4 | */ 5 | 6 | /** @type {Array} */ 7 | const emptyClassNames = [] 8 | 9 | /** 10 | * Check whether an element has all class names. 11 | * 12 | * @param {AstClassName} query 13 | * AST rule (with `classNames`). 14 | * @param {Element} element 15 | * Element. 16 | * @returns {boolean} 17 | * Whether `element` matches `query`. 18 | */ 19 | export function className(query, element) { 20 | // Assume array. 21 | const value = /** @type {Readonly>} */ ( 22 | element.properties.className || emptyClassNames 23 | ) 24 | 25 | return value.includes(query.name) 26 | } 27 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {AstRule} from 'css-selector-parser' 3 | * @import {Element, Parents} from 'hast' 4 | * @import {State} from './index.js' 5 | */ 6 | 7 | import {attribute} from './attribute.js' 8 | import {className} from './class-name.js' 9 | import {id} from './id.js' 10 | import {name} from './name.js' 11 | import {pseudo} from './pseudo.js' 12 | 13 | /** 14 | * Test a rule. 15 | * 16 | * @param {AstRule} query 17 | * AST rule (with `pseudoClasses`). 18 | * @param {Element} element 19 | * Element. 20 | * @param {number | undefined} index 21 | * Index of `element` in `parent`. 22 | * @param {Parents | undefined} parent 23 | * Parent of `element`. 24 | * @param {State} state 25 | * State. 26 | * @returns {boolean} 27 | * Whether `element` matches `query`. 28 | */ 29 | export function test(query, element, index, parent, state) { 30 | for (const item of query.items) { 31 | // eslint-disable-next-line unicorn/prefer-switch 32 | if (item.type === 'Attribute') { 33 | if (!attribute(item, element, state.schema)) return false 34 | } else if (item.type === 'Id') { 35 | if (!id(item, element)) return false 36 | } else if (item.type === 'ClassName') { 37 | if (!className(item, element)) return false 38 | } else if (item.type === 'PseudoClass') { 39 | if (!pseudo(item, element, index, parent, state)) return false 40 | } else if (item.type === 'PseudoElement') { 41 | throw new Error('Invalid selector: `::' + item.name + '`') 42 | } else if (item.type === 'TagName') { 43 | if (!name(item, element)) return false 44 | } else { 45 | // Otherwise `item.type` is `WildcardTag`, which matches. 46 | } 47 | } 48 | 49 | return true 50 | } 51 | -------------------------------------------------------------------------------- /test/svg.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import {h, s} from 'hastscript' 4 | import {select, selectAll} from 'hast-util-select' 5 | import {u} from 'unist-builder' 6 | 7 | test('svg', async function (t) { 8 | await t.test('should match svg (#1)', async function () { 9 | assert.deepEqual( 10 | select( 11 | '[writing-mode]', 12 | u('root', [ 13 | s('svg', [s('text', {writingMode: 'lr-tb'}, '!')]), 14 | s('p', [ 15 | h( 16 | 'text', 17 | {writingMode: 'lr-tb'}, 18 | 'this is a camelcased HTML attribute' 19 | ) 20 | ]) 21 | ]) 22 | ), 23 | s('text', {writingMode: 'lr-tb'}, '!') 24 | ) 25 | }) 26 | 27 | await t.test('should match svg (#2)', async function () { 28 | assert.deepEqual( 29 | selectAll( 30 | '[writing-mode]', 31 | u('root', [ 32 | s('svg', [s('text', {writingMode: 'lr-tb'}, '!')]), 33 | s('p', [ 34 | h( 35 | 'text', 36 | {writingMode: 'lr-tb'}, 37 | 'this is a camelcased HTML attribute' 38 | ) 39 | ]) 40 | ]) 41 | ), 42 | [s('text', {writingMode: 'lr-tb'}, '!')] 43 | ) 44 | }) 45 | 46 | await t.test('should match svg (#3)', async function () { 47 | assert.deepEqual( 48 | select('[writing-mode]', s('text', {writingMode: 'lr-tb'}, '!')), 49 | undefined 50 | ) 51 | }) 52 | 53 | await t.test('should match svg (#4)', async function () { 54 | assert.deepEqual( 55 | select('[writing-mode]', s('text', {writingMode: 'lr-tb'}, '!'), 'svg'), 56 | s('text', {writingMode: 'lr-tb'}, '!') 57 | ) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /lib/attribute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {AstAttribute} from 'css-selector-parser' 3 | * @import {Element, Properties} from 'hast' 4 | * @import {Info, Schema} from 'property-information' 5 | */ 6 | 7 | import {stringify as commas} from 'comma-separated-tokens' 8 | import {ok as assert} from 'devlop' 9 | import {find} from 'property-information' 10 | import * as spaces from 'space-separated-tokens' 11 | 12 | /** 13 | * @param {AstAttribute} query 14 | * Query. 15 | * @param {Element} element 16 | * Element. 17 | * @param {Schema} schema 18 | * Schema of element. 19 | * @returns {boolean} 20 | * Whether `element` matches `query`. 21 | */ 22 | export function attribute(query, element, schema) { 23 | const info = find(schema, query.name) 24 | const propertyValue = element.properties[info.property] 25 | let value = normalizeValue(propertyValue, info) 26 | 27 | // Exists. 28 | if (!query.value) { 29 | return value !== undefined 30 | } 31 | 32 | assert(query.value.type === 'String', 'expected plain string') 33 | let key = query.value.value 34 | 35 | // Case-sensitivity. 36 | if (query.caseSensitivityModifier === 'i') { 37 | key = key.toLowerCase() 38 | 39 | if (value) { 40 | value = value.toLowerCase() 41 | } 42 | } 43 | 44 | if (value !== undefined) { 45 | switch (query.operator) { 46 | // Exact. 47 | case '=': { 48 | return key === value 49 | } 50 | 51 | // Ends. 52 | case '$=': { 53 | return key === value.slice(-key.length) 54 | } 55 | 56 | // Contains. 57 | case '*=': { 58 | return value.includes(key) 59 | } 60 | 61 | // Begins. 62 | case '^=': { 63 | return key === value.slice(0, key.length) 64 | } 65 | 66 | // Exact or prefix. 67 | case '|=': { 68 | return ( 69 | key === value || 70 | (key === value.slice(0, key.length) && 71 | value.charAt(key.length) === '-') 72 | ) 73 | } 74 | 75 | // Space-separated list. 76 | case '~=': { 77 | return ( 78 | // For all other values (including comma-separated lists), return whether this 79 | // is an exact match. 80 | key === value || 81 | // If this is a space-separated list, and the query is contained in it, return 82 | // true. 83 | spaces.parse(value).includes(key) 84 | ) 85 | } 86 | // Other values are not yet supported by CSS. 87 | // No default 88 | } 89 | } 90 | 91 | return false 92 | } 93 | 94 | /** 95 | * 96 | * @param {Properties[keyof Properties]} value 97 | * @param {Info} info 98 | * @returns {string | undefined} 99 | */ 100 | function normalizeValue(value, info) { 101 | if (value === null || value === undefined) { 102 | // Empty. 103 | } else if (typeof value === 'boolean') { 104 | if (value) { 105 | return info.attribute 106 | } 107 | } else if (Array.isArray(value)) { 108 | if (value.length > 0) { 109 | return (info.commaSeparated ? commas : spaces.stringify)(value) 110 | } 111 | } else { 112 | return String(value) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Titus Wormer (https://wooorm.com)", 3 | "bugs": "https://github.com/syntax-tree/hast-util-select/issues", 4 | "contributors": [ 5 | "Titus Wormer (https://wooorm.com)" 6 | ], 7 | "dependencies": { 8 | "@types/hast": "^3.0.0", 9 | "@types/unist": "^3.0.0", 10 | "bcp-47-match": "^2.0.0", 11 | "comma-separated-tokens": "^2.0.0", 12 | "css-selector-parser": "^3.0.0", 13 | "devlop": "^1.0.0", 14 | "direction": "^2.0.0", 15 | "hast-util-has-property": "^3.0.0", 16 | "hast-util-to-string": "^3.0.0", 17 | "hast-util-whitespace": "^3.0.0", 18 | "nth-check": "^2.0.0", 19 | "property-information": "^7.0.0", 20 | "space-separated-tokens": "^2.0.0", 21 | "unist-util-visit": "^5.0.0", 22 | "zwitch": "^2.0.0" 23 | }, 24 | "description": "hast utility for `querySelector`, `querySelectorAll`, and `matches`", 25 | "devDependencies": { 26 | "@types/node": "^22.0.0", 27 | "c8": "^10.0.0", 28 | "hastscript": "^9.0.0", 29 | "prettier": "^3.0.0", 30 | "remark-cli": "^12.0.0", 31 | "remark-preset-wooorm": "^11.0.0", 32 | "type-coverage": "^2.0.0", 33 | "typescript": "^5.0.0", 34 | "unist-builder": "^4.0.0", 35 | "xo": "^0.60.0" 36 | }, 37 | "exports": "./index.js", 38 | "files": [ 39 | "lib/", 40 | "index.d.ts.map", 41 | "index.d.ts", 42 | "index.js" 43 | ], 44 | "funding": { 45 | "type": "opencollective", 46 | "url": "https://opencollective.com/unified" 47 | }, 48 | "keywords": [ 49 | "css", 50 | "hast-util", 51 | "hast", 52 | "html", 53 | "match", 54 | "matches", 55 | "query", 56 | "selectall", 57 | "selector", 58 | "select", 59 | "unist", 60 | "utility", 61 | "util" 62 | ], 63 | "license": "MIT", 64 | "name": "hast-util-select", 65 | "prettier": { 66 | "bracketSpacing": false, 67 | "semi": false, 68 | "singleQuote": true, 69 | "tabWidth": 2, 70 | "trailingComma": "none", 71 | "useTabs": false 72 | }, 73 | "remarkConfig": { 74 | "plugins": [ 75 | "remark-preset-wooorm" 76 | ] 77 | }, 78 | "repository": "syntax-tree/hast-util-select", 79 | "scripts": { 80 | "build": "tsc --build --clean && tsc --build && type-coverage", 81 | "format": "remark --frail --output --quiet -- . && prettier --log-level warn --write -- . && xo --fix", 82 | "prepack": "npm run build && npm run format", 83 | "test-api": "node --conditions development test/index.js", 84 | "test-coverage": "c8 --100 --reporter lcov npm run test-api", 85 | "test": "npm run build && npm run format && npm run test-coverage" 86 | }, 87 | "sideEffects": false, 88 | "typeCoverage": { 89 | "atLeast": 100, 90 | "detail": true, 91 | "ignoreCatch": true, 92 | "strict": true 93 | }, 94 | "type": "module", 95 | "version": "6.0.4", 96 | "xo": { 97 | "overrides": [ 98 | { 99 | "files": [ 100 | "test/**/*.js" 101 | ], 102 | "rules": { 103 | "import/no-unassigned-import": "off", 104 | "max-nested-callbacks": "off", 105 | "no-await-in-loop": "off" 106 | } 107 | } 108 | ], 109 | "prettier": true, 110 | "rules": { 111 | "logical-assignment-operators": "off", 112 | "max-params": "off", 113 | "unicorn/prefer-at": "off" 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/core.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import {h} from 'hastscript' 4 | import {selectAll} from 'hast-util-select' 5 | import {u} from 'unist-builder' 6 | 7 | test('hast-util-select', async function (t) { 8 | await t.test('should expose the public api', async function () { 9 | assert.deepEqual(Object.keys(await import('hast-util-select')).sort(), [ 10 | 'matches', 11 | 'select', 12 | 'selectAll' 13 | ]) 14 | }) 15 | 16 | await t.test('should select stuff (#1)', async function () { 17 | assert.deepEqual( 18 | selectAll( 19 | 'dl > dt.foo:nth-of-type(odd)', 20 | u('root', [ 21 | h('dl', [ 22 | '\n ', 23 | h('dt.foo', 'Alpha'), 24 | '\n ', 25 | h('dd', 'Bravo'), 26 | '\n ', 27 | h('dt', 'Charlie'), 28 | '\n ', 29 | h('dd', 'Delta'), 30 | '\n ', 31 | h('dt', 'Echo'), 32 | '\n ', 33 | h('dd', 'Foxtrot'), 34 | '\n' 35 | ]) 36 | ]) 37 | ), 38 | [h('dt.foo', 'Alpha')] 39 | ) 40 | }) 41 | 42 | await t.test('should select stuff (#2)', async function () { 43 | assert.deepEqual( 44 | selectAll( 45 | '.foo ~ dd:nth-of-type(even)', 46 | u('root', [ 47 | h('dl', [ 48 | '\n ', 49 | h('dt', 'Alpha'), 50 | '\n ', 51 | h('dd', 'Bravo'), 52 | '\n ', 53 | h('dt.foo', 'Charlie'), 54 | '\n ', 55 | h('dd', 'Delta'), 56 | '\n ', 57 | h('dt', 'Echo'), 58 | '\n ', 59 | h('dd', 'Foxtrot'), 60 | '\n ', 61 | h('dt', 'Golf'), 62 | '\n ', 63 | h('dd', 'Hotel'), 64 | '\n' 65 | ]) 66 | ]) 67 | ), 68 | [h('dd', 'Delta'), h('dd', 'Hotel')] 69 | ) 70 | }) 71 | 72 | await t.test('should select stuff (#3)', async function () { 73 | assert.deepEqual( 74 | selectAll( 75 | '.foo + dd:nth-of-type(even)', 76 | u('root', [ 77 | h('dl', [ 78 | '\n ', 79 | h('dt', 'Alpha'), 80 | '\n ', 81 | h('dd', 'Bravo'), 82 | '\n ', 83 | h('dt.foo', 'Charlie'), 84 | '\n ', 85 | h('dd', 'Delta'), 86 | '\n ', 87 | h('dt', 'Echo'), 88 | '\n ', 89 | h('dd', 'Foxtrot'), 90 | '\n ', 91 | h('dt', 'Golf'), 92 | '\n ', 93 | h('dd', 'Hotel'), 94 | '\n' 95 | ]) 96 | ]) 97 | ), 98 | [h('dd', 'Delta')] 99 | ) 100 | }) 101 | 102 | await t.test('should select stuff (#4)', async function () { 103 | assert.deepEqual( 104 | selectAll( 105 | '.foo, :nth-of-type(even), [title]', 106 | u('root', [ 107 | h('dl', [ 108 | h('dt', {title: 'bar'}, 'Alpha'), 109 | h('dd', 'Bravo'), 110 | h('dt.foo', 'Charlie'), 111 | h('dd', 'Delta'), 112 | h('dt', 'Echo'), 113 | h('dd.foo', {title: 'baz'}, 'Foxtrot'), 114 | h('dt', 'Golf'), 115 | h('dd', 'Hotel') 116 | ]) 117 | ]) 118 | ), 119 | [ 120 | h('dt', {title: 'bar'}, 'Alpha'), 121 | h('dt.foo', 'Charlie'), 122 | h('dd', 'Delta'), 123 | h('dd.foo', {title: 'baz'}, 'Foxtrot'), 124 | h('dt', 'Golf'), 125 | h('dd', 'Hotel') 126 | ] 127 | ) 128 | }) 129 | 130 | await t.test('should select stuff (#5)', async function () { 131 | assert.deepEqual( 132 | selectAll( 133 | 'a:not([class])', 134 | u('root', [h('a#w.a'), h('a#x'), h('a#y.b'), h('a#z')]) 135 | ), 136 | [h('a#x'), h('a#z')], 137 | 'should support `:not` with multiple matches (GH-6)' 138 | ) 139 | }) 140 | }) 141 | -------------------------------------------------------------------------------- /lib/enter-state.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Visitor} from 'unist-util-visit' 3 | * @import {ElementContent, Nodes} from 'hast' 4 | * @import {Direction, State} from './index.js' 5 | */ 6 | 7 | import {direction} from 'direction' 8 | import {toString} from 'hast-util-to-string' 9 | import {svg} from 'property-information' 10 | import {EXIT, SKIP, visit} from 'unist-util-visit' 11 | 12 | /** 13 | * Enter a node. 14 | * 15 | * The caller is responsible for calling the return value `exit`. 16 | * 17 | * @param {State} state 18 | * Current state. 19 | * 20 | * Will be mutated: `exit` undos the changes. 21 | * @param {Nodes} node 22 | * Node to enter. 23 | * @returns {() => undefined} 24 | * Call to exit. 25 | */ 26 | // eslint-disable-next-line complexity 27 | export function enterState(state, node) { 28 | const schema = state.schema 29 | const language = state.language 30 | const currentDirection = state.direction 31 | const editableOrEditingHost = state.editableOrEditingHost 32 | /** @type {Direction | undefined} */ 33 | let directionInferred 34 | 35 | if (node.type === 'element') { 36 | const lang = node.properties.xmlLang || node.properties.lang 37 | const type = node.properties.type || 'text' 38 | const direction = directionProperty(node) 39 | 40 | if (lang !== null && lang !== undefined) { 41 | state.language = String(lang) 42 | } 43 | 44 | if (schema && schema.space === 'html') { 45 | if (node.properties.contentEditable === 'true') { 46 | state.editableOrEditingHost = true 47 | } 48 | 49 | if (node.tagName === 'svg') { 50 | state.schema = svg 51 | } 52 | 53 | // See: . 54 | // Explicit `[dir=rtl]`. 55 | if (direction === 'rtl') { 56 | directionInferred = direction 57 | } else if ( 58 | // Explicit `[dir=ltr]`. 59 | direction === 'ltr' || 60 | // HTML with an invalid or no `[dir]`. 61 | (direction !== 'auto' && node.tagName === 'html') || 62 | // `input[type=tel]` with an invalid or no `[dir]`. 63 | (direction !== 'auto' && node.tagName === 'input' && type === 'tel') 64 | ) { 65 | directionInferred = 'ltr' 66 | // `[dir=auto]` or `bdi` with an invalid or no `[dir]`. 67 | } else if (direction === 'auto' || node.tagName === 'bdi') { 68 | if (node.tagName === 'textarea') { 69 | // Check contents of `