Skip to content

Commit 0d684d9

Browse files
committed
Add support for :dir() selector
1 parent 4c5b2a3 commit 0d684d9

File tree

8 files changed

+291
-7
lines changed

8 files changed

+291
-7
lines changed

lib/any.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ function rule(query, tree, state) {
4545
var opts = {
4646
schema: state.space === 'svg' ? svg : html,
4747
language: undefined,
48+
direction: 'ltr',
4849
iterator: match,
4950
one: state.one,
5051
shallow: state.shallow

lib/enter-state.js

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,113 @@
11
'use strict'
22

3+
/* eslint-disable complexity */
4+
35
var svg = require('property-information/svg')
6+
var direction = require('direction')
7+
var visit = require('unist-util-visit')
8+
var toString = require('hast-util-to-string')
9+
var is = require('hast-util-is-element')
10+
11+
var ltr = 'ltr'
12+
var rtl = 'rtl'
13+
var auto = 'auto'
14+
var valueTypes = ['text', 'search', 'tel', 'url', 'email']
15+
var validDirections = [ltr, rtl, auto]
16+
var ignoreElements = ['bdi', 'script', 'style', 'textare']
417

518
module.exports = config
619

720
function config(state, node) {
821
var schema = state.schema
922
var language = state.language
23+
var currentDirection = state.direction
24+
var space = schema.space
25+
var props = node.properties
26+
var dirInferred
27+
var type
1028
var found
1129
var lang
30+
var dir
1231

1332
if (node.type === 'element') {
14-
if (schema.space === 'html' && node.tagName === 'svg') {
15-
state.schema = svg
16-
found = true
17-
}
18-
19-
lang = node.properties.xmlLang || node.properties.lang
33+
lang = props.xmlLang || props.lang
34+
type = props.type || 'text'
35+
dir = dirProperty(node)
2036

2137
if (lang !== undefined && lang !== null) {
2238
state.language = lang
2339
found = true
2440
}
41+
42+
if (space === 'html') {
43+
if (is(node, 'svg')) {
44+
state.schema = svg
45+
space = 'svg'
46+
found = true
47+
}
48+
49+
// Explicit `[dir=rtl]`
50+
if (dir === rtl) {
51+
dirInferred = dir
52+
} else if (
53+
// Explicit `[dir=ltr]`
54+
dir === ltr ||
55+
// HTML with an invalid or no `[dir]`
56+
(dir !== auto && is(node, 'html')) ||
57+
// `input[type=tel]` with an invalid or no `[dir]`
58+
(dir !== auto && is(node, 'input') && props.type === 'tel')
59+
) {
60+
dirInferred = ltr
61+
// `[dir=auto]` or `bdi` with an invalid or no `[dir]`
62+
} else if (dir === auto || is(node, 'bdi')) {
63+
if (is(node, 'textarea')) {
64+
// Check contents of textarea
65+
dirInferred = dirBidi(toString(node))
66+
} else if (is(node, 'input') && valueTypes.indexOf(type) !== -1) {
67+
// Check value of input
68+
dirInferred = props.value ? dirBidi(props.value) : ltr
69+
} else {
70+
// Check text nodes in `node`
71+
visit(node, inferDirectionality)
72+
}
73+
}
74+
75+
if (dirInferred) {
76+
state.direction = dirInferred
77+
found = true
78+
}
79+
}
2580
}
2681

2782
return found ? reset : noop
2883

2984
function reset() {
3085
state.schema = schema
3186
state.language = language
87+
state.direction = currentDirection
3288
}
89+
90+
function inferDirectionality(child) {
91+
if (child.type === 'text') {
92+
dirInferred = dirBidi(child.value)
93+
return dirInferred ? visit.EXIT : null
94+
}
95+
96+
if (child !== node && (is(child, ignoreElements) || dirProperty(child))) {
97+
return visit.SKIP
98+
}
99+
}
100+
}
101+
102+
function dirBidi(value) {
103+
var val = direction(value)
104+
return val === 'neutral' ? null : val
105+
}
106+
107+
function dirProperty(node) {
108+
var val = node.properties.dir
109+
val = typeof val === 'string' ? val.toLowerCase() : null
110+
return validDirections.indexOf(val) === -1 ? null : val
33111
}
34112

35113
function noop() {}

lib/pseudo.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ handlers.any = matches
5050
handlers['any-link'] = anyLink
5151
handlers.blank = blank
5252
handlers.checked = checked
53+
handlers.dir = dir
5354
handlers.disabled = disabled
5455
handlers.empty = empty
5556
handlers.enabled = not(disabled)
@@ -109,6 +110,10 @@ function checked(query, node) {
109110
return false
110111
}
111112

113+
function dir(query, node, index, parent, state) {
114+
return state.direction === query.value
115+
}
116+
112117
function disabled(query, node) {
113118
return is(node, disableable) && has(node, 'disabled')
114119
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@
2323
"bcp-47-match": "^1.0.0",
2424
"comma-separated-tokens": "^1.0.2",
2525
"css-selector-parser": "^1.3.0",
26+
"direction": "^1.0.2",
2627
"hast-util-has-property": "^1.0.0",
2728
"hast-util-is-element": "^1.0.0",
29+
"hast-util-to-string": "^1.0.1",
2830
"hast-util-whitespace": "^1.0.0",
2931
"not": "^0.1.0",
3032
"nth-check": "^1.0.1",
3133
"property-information": "^4.0.0",
3234
"space-separated-tokens": "^1.1.0",
35+
"unist-util-visit": "^1.3.1",
3336
"zwitch": "^1.0.0"
3437
},
3538
"files": [

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ Yields:
177177
* [x] `:optional` (pseudo-class)
178178
* [x] `:required` (pseudo-class)
179179
* [x] `:root` (pseudo-class)
180+
* [x] `:dir()` (pseudo-class)
180181
* [x] `:lang()` (pseudo-class)
181182
* [x] `article p` (combinator: descendant selector)
182183
* [x] `article > p` (combinator: child selector)
@@ -202,7 +203,6 @@ Yields:
202203
* [ ]`:current` (pseudo-class)
203204
* [ ]`:default` (pseudo-class)
204205
* [ ]`:defined` (pseudo-class)
205-
* [ ] § `:dir()` (pseudo-class)
206206
* [ ]`:fullscreen` (pseudo-class)
207207
* [ ]`:focus` (pseudo-class)
208208
* [ ]`:future` (pseudo-class)

test/matches.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,158 @@ test('select.matches()', function(t) {
960960
sst.end()
961961
})
962962

963+
st.test(':dir()', function(sst) {
964+
var ltr = 'a'
965+
var rtl = 'أ'
966+
var neutral = '!'
967+
968+
sst.ok(
969+
matches(':dir(ltr)', h('html', {dir: 'ltr'})),
970+
'matching `ltr` if the element has a matching explicit `dir` attribute'
971+
)
972+
973+
sst.ok(
974+
matches(':dir(rtl)', h('html', {dir: 'rtl'})),
975+
'matching `rtl` if the element has a matching explicit `dir` attribute'
976+
)
977+
978+
sst.ok(
979+
matches(':dir(ltr)', h('html')),
980+
'matching `ltr` if the element is `html` with no `dir` attribute'
981+
)
982+
983+
sst.ok(
984+
matches(':dir(ltr)', h('html', {dir: 'foo'})),
985+
'matching `ltr` if the element is `html` with an invalid `dir` attribute'
986+
)
987+
988+
sst.ok(
989+
matches(':dir(ltr)', h('input', {type: 'tel'})),
990+
'matching `ltr` if the element is `input[type=tel]` with no `dir` attribute'
991+
)
992+
993+
sst.ok(
994+
matches(':dir(ltr)', h('input', {type: 'tel', dir: 'foo'})),
995+
'matching `ltr` if the element is `input[type=tel]` with an invalid `dir` attribute'
996+
)
997+
998+
sst.ok(
999+
matches(':dir(ltr)', h('textarea', {dir: 'auto'}, ltr)),
1000+
'matching `ltr` if `[dir=auto]` on a textarea and it’s content is BIDI LTR'
1001+
)
1002+
1003+
sst.ok(
1004+
matches(':dir(rtl)', h('textarea', {dir: 'auto'}, rtl)),
1005+
'matching `rtl` if `[dir=auto]` on a textarea and it’s content is BIDI RTL'
1006+
)
1007+
1008+
sst.ok(
1009+
matches(':dir(ltr)', h('textarea', {dir: 'auto'}, neutral)),
1010+
'matching `ltr` if `[dir=auto]` on a textarea and it’s content is BIDI neutral'
1011+
)
1012+
1013+
sst.ok(
1014+
matches(':dir(ltr)', h('input', {dir: 'auto', value: ltr})),
1015+
'matching `ltr` if `[dir=auto]` on a text input and it’s value is BIDI LTR'
1016+
)
1017+
1018+
sst.ok(
1019+
matches(
1020+
':dir(rtl)',
1021+
h('input', {type: 'search', dir: 'auto', value: rtl})
1022+
),
1023+
'matching `rtl` if `[dir=auto]` on a search input and it’s value is BIDI RTL'
1024+
)
1025+
1026+
sst.ok(
1027+
matches(
1028+
':dir(ltr)',
1029+
h('input', {type: 'url', dir: 'auto', value: neutral})
1030+
),
1031+
'matching `ltr` if `[dir=auto]` on a URL input and it’s value is BIDI neutral'
1032+
)
1033+
1034+
sst.ok(
1035+
matches(':dir(ltr)', h('input', {type: 'email', dir: 'auto'})),
1036+
'matching `ltr` if `[dir=auto]` on an email input without value'
1037+
)
1038+
1039+
sst.ok(
1040+
matches(':dir(ltr)', h('p', {dir: 'auto'}, ltr)),
1041+
'matching `ltr` if `[dir=auto]` and the element has BIDI LTR text'
1042+
)
1043+
1044+
sst.ok(
1045+
matches(':dir(rtl)', h('p', {dir: 'auto'}, rtl)),
1046+
'matching `rtl` if `[dir=auto]` and the element has BIDI RTL text'
1047+
)
1048+
1049+
sst.ok(
1050+
matches(':dir(ltr)', h('p', {dir: 'auto'}, neutral)),
1051+
'matching `ltr` if `[dir=auto]` and the element has BIDI neutral text'
1052+
)
1053+
1054+
sst.ok(
1055+
matches(':dir(ltr)', h('p', {dir: 'auto'}, [neutral, ltr, rtl])),
1056+
'matching `ltr` if `[dir=auto]` and the element has BIDI neutral text followed by LTR text'
1057+
)
1058+
1059+
sst.ok(
1060+
matches(':dir(rtl)', h('p', {dir: 'auto'}, [neutral, rtl, ltr])),
1061+
'matching `rtl` if `[dir=auto]` and the element has BIDI neutral text followed by RTL text'
1062+
)
1063+
1064+
sst.ok(
1065+
matches(
1066+
':dir(ltr)',
1067+
h('p', {dir: 'auto'}, [neutral, h('script', rtl), ltr])
1068+
),
1069+
'matching `ltr` if `[dir=auto]`, ignoring BIDI text in scripts, followed by LTR text'
1070+
)
1071+
1072+
sst.ok(
1073+
matches(
1074+
':dir(rtl)',
1075+
h('p', {dir: 'auto'}, [neutral, h('style', ltr), rtl])
1076+
),
1077+
'matching `rtl` if `[dir=auto]`, ignoring BIDI text in styles, followed by RTL text'
1078+
)
1079+
1080+
sst.ok(
1081+
matches(
1082+
':dir(ltr)',
1083+
h('p', {dir: 'auto'}, [neutral, h('span', {dir: 'rtl'}, rtl), ltr])
1084+
),
1085+
'matching `ltr` if `[dir=auto]`, ignoring elements with directions, followed by LTR text'
1086+
)
1087+
1088+
sst.ok(
1089+
matches(
1090+
':dir(rtl)',
1091+
h('p', {dir: 'auto'}, [neutral, h('span', {dir: 'ltr'}, ltr), rtl])
1092+
),
1093+
'matching `rtl` if `[dir=auto]`, ignoring elements with directions, followed by RTL text'
1094+
)
1095+
1096+
sst.ok(
1097+
matches(
1098+
':dir(ltr)',
1099+
h('bdi', [neutral, h('span', {dir: 'rtl'}, rtl), ltr])
1100+
),
1101+
'matching `ltr` on `bdi` elements, ignoring elements with directions, followed by LTR text'
1102+
)
1103+
1104+
sst.ok(
1105+
matches(
1106+
':dir(rtl)',
1107+
h('bdi', [neutral, h('span', {dir: 'ltr'}, ltr), rtl])
1108+
),
1109+
'matching `rtl` on `bdi` elements, ignoring elements with directions, followed by RTL text'
1110+
)
1111+
1112+
sst.end()
1113+
})
1114+
9631115
st.test(':root', function(sst) {
9641116
sst.ok(matches(':root', h('html')), 'true if `<html>` in HTML space')
9651117

test/select-all.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,28 @@ test('select.selectAll()', function(t) {
748748
sst.end()
749749
})
750750

751+
st.test(':dir()', function(sst) {
752+
var ltr = 'a'
753+
var rtl = 'أ'
754+
755+
sst.deepEqual(
756+
selectAll(
757+
'q:dir(rtl)',
758+
u('root', [
759+
h('div', {dir: 'rtl'}, h('p', {dir: ''}, h('q#a', ltr))),
760+
h('p', {dir: 'ltr'}, h('q#b', {dir: 'ltr'}, rtl)),
761+
h('p', {dir: 'ltr'}, h('q#c', {dir: ''}, rtl)),
762+
h('p', {dir: 'ltr'}, h('q#d', {dir: 'foo'}, rtl)),
763+
h('p', {dir: 'ltr'}, h('q#e', {dir: 'rtl'}, rtl))
764+
])
765+
),
766+
[h('q#a', ltr), h('q#e', {dir: 'rtl'}, rtl)],
767+
'should return the correct matching element'
768+
)
769+
770+
sst.end()
771+
})
772+
751773
st.test(':root', function(sst) {
752774
sst.deepEqual(
753775
selectAll(

test/select.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,29 @@ test('select.select()', function(t) {
691691
sst.end()
692692
})
693693

694+
st.test(':dir()', function(sst) {
695+
var ltr = 'a'
696+
var rtl = 'أ'
697+
var neutral = '!'
698+
699+
sst.deepEqual(
700+
select(
701+
'q:dir(rtl)',
702+
u('root', [
703+
h('div', {dir: 'rtl'}, h('p', {dir: ''}, h('q', ltr))),
704+
h('p', {dir: 'ltr'}, h('q', {dir: 'ltr'}, rtl)),
705+
h('p', {dir: 'ltr'}, h('q', {dir: ''}, neutral)),
706+
h('p', {dir: 'ltr'}, h('q', {dir: 'foo'}, ltr)),
707+
h('p', {dir: 'ltr'}, h('q', {dir: 'rtl'}, rtl))
708+
])
709+
),
710+
h('q', ltr),
711+
'should return the correct matching element'
712+
)
713+
714+
sst.end()
715+
})
716+
694717
st.test(':root', function(sst) {
695718
sst.deepEqual(
696719
select(

0 commit comments

Comments
 (0)