Skip to content

Commit 942c24d

Browse files
committed
Add support for :scope, :read-only, and :read-write
1 parent 0d684d9 commit 942c24d

File tree

7 files changed

+258
-16
lines changed

7 files changed

+258
-16
lines changed

lib/any.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ function rule(query, tree, state) {
4646
schema: state.space === 'svg' ? svg : html,
4747
language: undefined,
4848
direction: 'ltr',
49+
editableOrEditingHost: false,
4950
iterator: match,
5051
one: state.one,
5152
shallow: state.shallow

lib/enter-state.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ module.exports = config
1919

2020
function config(state, node) {
2121
var schema = state.schema
22+
var space = schema.space
2223
var language = state.language
2324
var currentDirection = state.direction
24-
var space = schema.space
25+
var editableOrEditingHost = state.editableOrEditingHost
2526
var props = node.properties
2627
var dirInferred
2728
var type
@@ -39,13 +40,25 @@ function config(state, node) {
3940
found = true
4041
}
4142

43+
// Turn off editing mode in non-HTML spaces.
44+
if (space !== 'html' && state.editableOrEditingHost) {
45+
state.editableOrEditingHost = false
46+
found = true
47+
}
48+
4249
if (space === 'html') {
50+
if (props.contentEditable === 'true') {
51+
state.editableOrEditingHost = true
52+
found = true
53+
}
54+
4355
if (is(node, 'svg')) {
4456
state.schema = svg
4557
space = 'svg'
4658
found = true
4759
}
4860

61+
// See: https://html.spec.whatwg.org/#the-directionality
4962
// Explicit `[dir=rtl]`
5063
if (dir === rtl) {
5164
dirInferred = dir
@@ -85,6 +98,7 @@ function config(state, node) {
8598
state.schema = schema
8699
state.language = language
87100
state.direction = currentDirection
101+
state.editableOrEditingHost = editableOrEditingHost
88102
}
89103

90104
function inferDirectionality(child) {

lib/pseudo.js

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
var commaSeparated = require('comma-separated-tokens').parse
44
var filter = require('bcp-47-match').extendedFilter
5+
var zwitch = require('zwitch')
6+
var not = require('not')
7+
var is = require('hast-util-is-element')
8+
var has = require('hast-util-has-property')
9+
var whitespace = require('hast-util-whitespace')
510

611
module.exports = match
712

@@ -20,11 +25,6 @@ match.needsIndex = [
2025
'only-of-type'
2126
]
2227

23-
var zwitch = require('zwitch')
24-
var not = require('not')
25-
var is = require('hast-util-is-element')
26-
var has = require('hast-util-has-property')
27-
var whitespace = require('hast-util-whitespace')
2828
var anything = require('./any')
2929

3030
var handle = zwitch('name')
@@ -68,8 +68,11 @@ handlers['nth-last-of-type'] = nthLastOfType
6868
handlers['only-child'] = onlyChild
6969
handlers['only-of-type'] = onlyOfType
7070
handlers.optional = not(required)
71+
handlers['read-only'] = not(readWrite)
72+
handlers['read-write'] = readWrite
7173
handlers.required = required
7274
handlers.root = root
75+
handlers.scope = scope
7376

7477
function match(query, node, index, parent, state) {
7578
var pseudos = query.pseudos
@@ -122,15 +125,28 @@ function required(query, node) {
122125
return is(node, requirable) && has(node, 'required')
123126
}
124127

128+
function readWrite(query, node, index, parent, state) {
129+
var inputOrArea = is(node, ['input', 'textarea'])
130+
131+
return (
132+
(inputOrArea && !has(node, 'readOnly') && !has(node, 'disabled')) ||
133+
(!inputOrArea && state.editableOrEditingHost)
134+
)
135+
}
136+
125137
function root(query, node, index, parent, state) {
126138
var space = state.schema.space
127139
return (
140+
(!parent || parent.type === 'root') &&
128141
space in roots &&
129-
node.tagName === roots[space] &&
130-
(!parent || parent.type === 'root')
142+
is(node, roots[space])
131143
)
132144
}
133145

146+
function scope(query, node, index, parent) {
147+
return (!parent || parent.type === 'root') && is(node)
148+
}
149+
134150
function empty(query, node) {
135151
return !someChildren(node, check)
136152

readme.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -166,19 +166,22 @@ Yields:
166166
* [x] `[attr$=value]` (attribute ends with)
167167
* [x] `[attr*=value]` (attribute contains)
168168
* [x] `:any()` (pseudo-class, use `:matches` instead)
169+
* [x] `:dir()` (pseudo-class)
170+
* [x] `:lang()` (pseudo-class)
169171
* [x] `:matches()` (pseudo-class)
170172
* [x] `:not()` (pseudo-class)
171173
* [x] `:any-link` (pseudo-class)
172-
* [x] `:empty` (pseudo-class)
173174
* [x] `:blank` (pseudo-class)
174175
* [x] `:checked` (pseudo-class)
175176
* [x] `:disabled` (pseudo-class)
177+
* [x] `:empty` (pseudo-class)
176178
* [x] `:enabled` (pseudo-class)
177179
* [x] `:optional` (pseudo-class)
180+
* [x] `:read-only` (pseudo-class)
181+
* [x] `:read-write` (pseudo-class)
178182
* [x] `:required` (pseudo-class)
179183
* [x] `:root` (pseudo-class)
180-
* [x] `:dir()` (pseudo-class)
181-
* [x] `:lang()` (pseudo-class)
184+
* [x] `:scope` (pseudo-class):
182185
* [x] `article p` (combinator: descendant selector)
183186
* [x] `article > p` (combinator: child selector)
184187
* [x] `h1 + p` (combinator: adjacent sibling selector)
@@ -187,12 +190,12 @@ Yields:
187190
* [x] \* `:first-of-type` (pseudo-class)
188191
* [x] \* `:last-child` (pseudo-class)
189192
* [x] \* `:last-of-type` (pseudo-class)
193+
* [x] \* `:only-child` (pseudo-class)
194+
* [x] \* `:only-of-type` (pseudo-class)
190195
* [x] \* `:nth-child()` (pseudo-class)
191196
* [x] \* `:nth-last-child()` (pseudo-class)
192197
* [x] \* `:nth-last-of-type()` (pseudo-class)
193198
* [x] \* `:nth-of-type()` (pseudo-class)
194-
* [x] \* `:only-child` (pseudo-class)
195-
* [x] \* `:only-of-type` (pseudo-class)
196199

197200
## Unsupported
198201

@@ -220,9 +223,6 @@ Yields:
220223
* [ ]`:paused` (pseudo-class)
221224
* [ ]`:placeholder-shown` (pseudo-class)
222225
* [ ]`:playing` (pseudo-class)
223-
* [ ] § `:read-only` (pseudo-class)
224-
* [ ] § `:read-write` (pseudo-class)
225-
* [ ] § `:scope` (pseudo-class)
226226
* [ ]`:user-error` (pseudo-class)
227227
* [ ]`:user-invalid` (pseudo-class)
228228
* [ ] § `:valid` (pseudo-class)

test/matches.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,67 @@ test('select.matches()', function(t) {
11301130
sst.end()
11311131
})
11321132

1133+
st.test(':scope', function(sst) {
1134+
sst.ok(matches(':scope', h('html')), 'always true for elements')
1135+
sst.ok(matches(':scope', h('p')), 'always true for elements')
1136+
sst.notOk(matches(':scope', u('text'), '!'), 'always true for elements')
1137+
sst.end()
1138+
})
1139+
1140+
st.test(':read-write', function(sst) {
1141+
sst.ok(matches(':read-write', h('input')), 'true on input')
1142+
sst.ok(matches(':read-write', h('input')), 'true on textarea')
1143+
sst.notOk(
1144+
matches(':read-write', h('input', {readOnly: true})),
1145+
'false on input w/ readonly'
1146+
)
1147+
sst.notOk(
1148+
matches(':read-write', h('textarea', {readOnly: true})),
1149+
'false on textarea w/ readonly'
1150+
)
1151+
sst.notOk(
1152+
matches(':read-write', h('input', {disabled: true})),
1153+
'false on input w/ disabled'
1154+
)
1155+
sst.notOk(
1156+
matches(':read-write', h('textarea', {disabled: true})),
1157+
'false on textarea w/ disabled'
1158+
)
1159+
sst.ok(
1160+
matches(':read-write', h('div', {contentEditable: 'true'})),
1161+
'true on element w/ contenteditable'
1162+
)
1163+
1164+
sst.end()
1165+
})
1166+
1167+
st.test(':read-only', function(sst) {
1168+
sst.notOk(matches(':read-only', h('input')), 'false on input')
1169+
sst.notOk(matches(':read-only', h('input')), 'false on textarea')
1170+
sst.ok(
1171+
matches(':read-only', h('input', {readOnly: true})),
1172+
'true on input w/ readonly'
1173+
)
1174+
sst.ok(
1175+
matches(':read-only', h('textarea', {readOnly: true})),
1176+
'true on textarea w/ readonly'
1177+
)
1178+
sst.ok(
1179+
matches(':read-only', h('input', {disabled: true})),
1180+
'true on input w/ disabled'
1181+
)
1182+
sst.ok(
1183+
matches(':read-only', h('textarea', {disabled: true})),
1184+
'true on textarea w/ disabled'
1185+
)
1186+
sst.notOk(
1187+
matches(':read-only', h('div', {contentEditable: 'true'})),
1188+
'false on element w/ contenteditable'
1189+
)
1190+
1191+
sst.end()
1192+
})
1193+
11331194
st.end()
11341195
})
11351196

test/select-all.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,62 @@ test('select.selectAll()', function(t) {
770770
sst.end()
771771
})
772772

773+
st.test(':read-write', function(sst) {
774+
sst.deepEqual(
775+
selectAll(
776+
'p:read-write',
777+
u('root', [h('div', {contentEditable: 'true'}, [h('p', 'A')])])
778+
),
779+
[h('p', 'A')],
780+
'should return elements inside `[contentEditable=true]`'
781+
)
782+
783+
sst.deepEqual(
784+
selectAll(
785+
'a:read-write',
786+
u('root', [
787+
h('div', {contentEditable: 'true'}, [
788+
s('svg', {viewBox: [0, 0, 50, 50]}, [
789+
s('a', {download: true}, '!')
790+
])
791+
])
792+
])
793+
),
794+
[],
795+
'should not return elements inside SVG embedded in `[contentEditable=true]`'
796+
)
797+
798+
sst.end()
799+
})
800+
801+
st.test(':read-only', function(sst) {
802+
sst.deepEqual(
803+
selectAll(
804+
'p:read-only',
805+
u('root', [h('div', {contentEditable: 'true'}, [h('p', 'A')])])
806+
),
807+
[],
808+
'should not return elements inside `[contentEditable=true]`'
809+
)
810+
811+
sst.deepEqual(
812+
selectAll(
813+
'a:read-only',
814+
u('root', [
815+
h('div', {contentEditable: 'true'}, [
816+
s('svg', {viewBox: [0, 0, 50, 50]}, [
817+
s('a', {download: true}, '!')
818+
])
819+
])
820+
])
821+
),
822+
[s('a', {download: true}, '!')],
823+
'should return elements inside SVG embedded in `[contentEditable=true]`'
824+
)
825+
826+
sst.end()
827+
})
828+
773829
st.test(':root', function(sst) {
774830
sst.deepEqual(
775831
selectAll(
@@ -852,6 +908,25 @@ test('select.selectAll()', function(t) {
852908
sst.end()
853909
})
854910

911+
st.test(':scope', function(sst) {
912+
sst.deepEqual(
913+
selectAll(
914+
':scope',
915+
u('root', [h('strong', h('b', 'a')), h('em', h('i', 'b'))])
916+
),
917+
[h('strong', h('b', 'a')), h('em', h('i', 'b'))],
918+
'should select the elements directly in `root`, if a `root` is given'
919+
)
920+
921+
sst.deepEqual(
922+
selectAll(':scope', h('em', h('i', 'b'))),
923+
[h('em', h('i', 'b'))],
924+
'should select the root element if one is given'
925+
)
926+
927+
sst.end()
928+
})
929+
855930
st.end()
856931
})
857932

0 commit comments

Comments
 (0)