Skip to content

Commit 4c5b2a3

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

File tree

10 files changed

+205
-14
lines changed

10 files changed

+205
-14
lines changed

lib/any.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ var svg = require('property-information/svg')
88
var needsIndex = require('./pseudo').needsIndex
99
var test = require('./test')
1010
var nest = require('./nest')
11+
var enter = require('./enter-state')
1112

1213
var type = zwitch('type')
1314
var handlers = type.handlers
@@ -43,6 +44,7 @@ function rule(query, tree, state) {
4344
var collect = collector(state.one)
4445
var opts = {
4546
schema: state.space === 'svg' ? svg : html,
47+
language: undefined,
4648
iterator: match,
4749
one: state.one,
4850
shallow: state.shallow
@@ -57,6 +59,8 @@ function rule(query, tree, state) {
5759
return collect.result
5860

5961
function match(query, node, index, parent, state) {
62+
var exit = enter(state, node)
63+
6064
if (test(query, node, index, parent, state)) {
6165
if (query.rule) {
6266
nest(query.rule, node, index, parent, configure(query.rule, state))
@@ -65,6 +69,8 @@ function rule(query, tree, state) {
6569
state.found = true
6670
}
6771
}
72+
73+
exit()
6874
}
6975

7076
function configure(query, state) {

lib/enter-state.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict'
2+
3+
var svg = require('property-information/svg')
4+
5+
module.exports = config
6+
7+
function config(state, node) {
8+
var schema = state.schema
9+
var language = state.language
10+
var found
11+
var lang
12+
13+
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
20+
21+
if (lang !== undefined && lang !== null) {
22+
state.language = lang
23+
found = true
24+
}
25+
}
26+
27+
return found ? reset : noop
28+
29+
function reset() {
30+
state.schema = schema
31+
state.language = language
32+
}
33+
}
34+
35+
function noop() {}

lib/nest.js

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict'
22

33
var zwitch = require('zwitch')
4-
var svg = require('property-information/svg')
4+
var enter = require('./enter-state')
55

66
module.exports = zwitch('nestingOperator')
77

@@ -98,15 +98,6 @@ function walkIterator(query, parent, state) {
9898
var nodes = parent.children
9999
var typeIndex = state.index ? createTypeIndex() : null
100100
var delayed = []
101-
var parentSchema = state.schema
102-
103-
if (
104-
parentSchema.space === 'html' &&
105-
parent.type === 'element' &&
106-
parent.tagName === 'svg'
107-
) {
108-
state.schema = svg
109-
}
110101

111102
return {
112103
prefillTypeIndex: rangeDefaults(prefillTypeIndex),
@@ -118,8 +109,6 @@ function walkIterator(query, parent, state) {
118109
var length = delayed.length
119110
var index = -1
120111

121-
state.schema = parentSchema
122-
123112
while (++index < length) {
124113
delayed[index]()
125114

@@ -175,7 +164,9 @@ function walkIterator(query, parent, state) {
175164
}
176165

177166
function pushNode() {
167+
var exit = enter(state, child)
178168
state.iterator(query, child, start, parent, state)
169+
exit()
179170
}
180171
}
181172

lib/pseudo.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
'use strict'
22

3+
var commaSeparated = require('comma-separated-tokens').parse
4+
var filter = require('bcp-47-match').extendedFilter
5+
36
module.exports = match
47

58
match.selectorPseudoSupport = ['any', 'matches', 'not']
@@ -52,6 +55,7 @@ handlers.empty = empty
5255
handlers.enabled = not(disabled)
5356
handlers['first-child'] = firstChild
5457
handlers['first-of-type'] = firstOfType
58+
handlers.lang = lang
5559
handlers['last-child'] = lastChild
5660
handlers['last-of-type'] = lastOfType
5761
handlers.matches = matches
@@ -145,6 +149,14 @@ function firstChild(query, node, index, parent, state) {
145149
return state.elementIndex === 0
146150
}
147151

152+
function lang(query, node, index, parent, state) {
153+
return (
154+
state.language !== '' &&
155+
state.language !== undefined &&
156+
filter(state.language, commaSeparated(query.value)).length !== 0
157+
)
158+
}
159+
148160
function lastChild(query, node, index, parent, state) {
149161
assertDeep(state, query)
150162
return state.elementIndex === state.elementCount - 1

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"Titus Wormer <[email protected]> (http://wooorm.com)"
2121
],
2222
"dependencies": {
23+
"bcp-47-match": "^1.0.0",
2324
"comma-separated-tokens": "^1.0.2",
2425
"css-selector-parser": "^1.3.0",
2526
"hast-util-has-property": "^1.0.0",

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] `:lang()` (pseudo-class)
180181
* [x] `article p` (combinator: descendant selector)
181182
* [x] `article > p` (combinator: child selector)
182183
* [x] `h1 + p` (combinator: adjacent sibling selector)
@@ -210,7 +211,6 @@ Yields:
210211
* [ ]`:indeterminate` (pseudo-class)
211212
* [ ] § `:in-range` (pseudo-class)
212213
* [ ] § `:invalid` (pseudo-class)
213-
* [ ] § `:lang()` (pseudo-class)
214214
* [ ]`:link` (pseudo-class)
215215
* [ ]`:local-link` (pseudo-class)
216216
* [ ]`nth-column()` (pseudo-class)

test/matches.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,66 @@ test('select.matches()', function(t) {
900900
sst.end()
901901
})
902902

903+
st.test(':lang()', function(sst) {
904+
sst.ok(
905+
matches(':lang(de, en)', h('html', {xmlLang: 'en'})),
906+
'true if the element has an `xml:lang` attribute'
907+
)
908+
909+
sst.ok(
910+
matches(':lang(de, en)', h('html', {lang: 'de'})),
911+
'true if the element has a `lang` attribute'
912+
)
913+
914+
sst.notOk(
915+
matches(':lang(de, en)', h('html', {xmlLang: 'jp'})),
916+
'false if the element has an different language set'
917+
)
918+
919+
sst.notOk(
920+
matches(':lang(de, en)', h('html', {xmlLang: 'jp', lang: 'de'})),
921+
'should prefer `xmlLang` over `lang` (#1)'
922+
)
923+
924+
sst.ok(
925+
matches(':lang(de, en)', h('html', {xmlLang: 'de', lang: 'jp'})),
926+
'should prefer `xmlLang` over `lang` (#2)'
927+
)
928+
929+
sst.notOk(
930+
matches(':lang(de, en)', h('html', {xmlLang: 'jp'})),
931+
'false if the element has an different language set'
932+
)
933+
934+
sst.ok(
935+
matches(':lang("*")', h('html', {lang: 'en'})),
936+
'should support wildcards'
937+
)
938+
939+
sst.notOk(
940+
matches(':lang(en)', h('html', {lang: ''})),
941+
'false if [lang] is an empty string (means unknown language)'
942+
)
943+
944+
sst.notOk(
945+
matches(':lang(*)', h('html', {lang: ''})),
946+
'false with wildcard if [lang] is an empty string (means unknown language)'
947+
)
948+
949+
sst.ok(
950+
matches(':lang("de-*-DE")', h('html', {lang: 'de-Latn-DE'})),
951+
'should support non-primary wildcard subtags (#1)'
952+
)
953+
954+
// Not supported by `css-selector-parser` yet :(
955+
// sst.ok(
956+
// matches(':lang("fr-BE", "de-*-DE")', h('html', {lang: 'de-Latn-DE'})),
957+
// 'should support non-primary wildcard subtags (#2)'
958+
// )
959+
960+
sst.end()
961+
})
962+
903963
st.test(':root', function(sst) {
904964
sst.ok(matches(':root', h('html')), 'true if `<html>` in HTML space')
905965

test/select-all.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,45 @@ test('select.selectAll()', function(t) {
709709
sst.end()
710710
})
711711

712+
st.test(':lang()', function(sst) {
713+
sst.deepEqual(
714+
selectAll(
715+
'q:lang(en)',
716+
u('root', [
717+
h('div', {lang: 'en'}, h('p', {lang: ''}, h('q', '0'))),
718+
h('p', {lang: 'fr'}, h('q', {lang: 'fr'}, 'A')),
719+
h('p', {lang: 'fr'}, h('q', {lang: 'en'}, 'B')),
720+
h('p', {lang: 'fr'}, h('q', {lang: 'en-GB'}, 'C')),
721+
h('p', {lang: 'fr'}, h('q', {lang: ''}, 'D')),
722+
h('p', {lang: 'fr'}, h('q', 'E')),
723+
h('p', {lang: 'en'}, h('q', {lang: 'fr'}, 'F')),
724+
h('p', {lang: 'en'}, h('q', {lang: 'en'}, 'G')),
725+
h('p', {lang: 'en'}, h('q', {lang: 'en-GB'}, 'H')),
726+
h('p', {lang: 'en'}, h('q', {lang: ''}, 'I')),
727+
h('p', {lang: 'en'}, h('q', 'J')),
728+
h('p', {lang: 'en-GB'}, h('q', {lang: 'fr'}, 'K')),
729+
h('p', {lang: 'en-GB'}, h('q', {lang: 'en'}, 'L')),
730+
h('p', {lang: 'en-GB'}, h('q', {lang: 'en-GB'}, 'M')),
731+
h('p', {lang: 'en-GB'}, h('q', {lang: ''}, 'N')),
732+
h('p', {lang: 'en-GB'}, h('q', 'O'))
733+
])
734+
),
735+
[
736+
h('q', {lang: 'en'}, 'B'),
737+
h('q', {lang: 'en-GB'}, 'C'),
738+
h('q', {lang: 'en'}, 'G'),
739+
h('q', {lang: 'en-GB'}, 'H'),
740+
h('q', 'J'),
741+
h('q', {lang: 'en'}, 'L'),
742+
h('q', {lang: 'en-GB'}, 'M'),
743+
h('q', 'O')
744+
],
745+
'should return the correct matching elements'
746+
)
747+
748+
sst.end()
749+
})
750+
712751
st.test(':root', function(sst) {
713752
sst.deepEqual(
714753
selectAll(

test/select.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,25 @@ test('select.select()', function(t) {
672672
sst.end()
673673
})
674674

675+
st.test(':lang()', function(sst) {
676+
sst.deepEqual(
677+
select(
678+
'q:lang(en)',
679+
u('root', [
680+
h('div', {lang: 'en'}, h('p', {lang: ''}, h('q', '0'))),
681+
h('p', {lang: 'fr'}, h('q', {lang: 'fr'}, 'A')),
682+
h('p', {lang: 'fr'}, h('q', {lang: ''}, 'B')),
683+
h('p', {lang: 'fr'}, h('q', {lang: 'en-GB'}, 'C')),
684+
h('p', {lang: 'fr'}, h('q', {lang: 'en'}, 'D'))
685+
])
686+
),
687+
h('q', {lang: 'en-GB'}, 'C'),
688+
'should return the correct matching element'
689+
)
690+
691+
sst.end()
692+
})
693+
675694
st.test(':root', function(sst) {
676695
sst.deepEqual(
677696
select(

test/svg.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,45 @@
33
var test = require('tape')
44
var u = require('unist-builder')
55
var s = require('hastscript/svg')
6+
var h = require('hastscript')
67
var select = require('..').select
8+
var selectAll = require('..').selectAll
79

810
test('svg', function(t) {
911
t.deepEqual(
1012
select(
1113
'[writing-mode]',
12-
u('root', [s('svg', [s('text', {writingMode: 'lr-tb'}, '!')])])
14+
u('root', [
15+
s('svg', [s('text', {writingMode: 'lr-tb'}, '!')]),
16+
s('p', [
17+
h(
18+
'text',
19+
{writingMode: 'lr-tb'},
20+
'this is a camelcased HTML attribute'
21+
)
22+
])
23+
])
1324
),
1425
s('text', {writingMode: 'lr-tb'}, '!')
1526
)
1627

28+
t.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+
1745
t.deepEqual(
1846
select('[writing-mode]', s('text', {writingMode: 'lr-tb'}, '!')),
1947
null

0 commit comments

Comments
 (0)