Skip to content

Commit

Permalink
Separate prefixed pseudos in nested rules to avoid CSSOM injection fa…
Browse files Browse the repository at this point in the history
…ilures (thysultan#315)

* chore: add failing test for prefixer + rulesheet re: placeholder pseudo

* serialize pseudos in nested rules recursively

* read-write, placeholder prefix refactor

also adds ability to introspect an elements siblings from the element object

* get the correct order of prefixed values

* nit redundant checks

* fixed mixed indentation in test files

---------

Co-authored-by: Mateusz Burzyński <[email protected]>
Co-authored-by: Sultan <[email protected]>
  • Loading branch information
3 people authored Jun 27, 2023
1 parent 1bf92ea commit 0b6d567
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 40 deletions.
24 changes: 14 additions & 10 deletions src/Middleware.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {MS, MOZ, WEBKIT, RULESET, KEYFRAMES, DECLARATION} from './Enum.js'
import {match, charat, substr, strlen, sizeof, replace, combine} from './Utility.js'
import {copy, tokenize} from './Tokenizer.js'
import {match, charat, substr, strlen, sizeof, replace, combine, filter, assign} from './Utility.js'
import {copy, lift, tokenize} from './Tokenizer.js'
import {serialize} from './Serializer.js'
import {prefix} from './Prefixer.js'

Expand Down Expand Up @@ -49,18 +49,22 @@ export function prefixer (element, index, children, callback) {
return serialize([copy(element, {value: replace(element.value, '@', '@' + WEBKIT)})], callback)
case RULESET:
if (element.length)
return combine(element.props, function (value) {
switch (match(value, /(::plac\w+|:read-\w+)/)) {
return combine(children = element.props, function (value) {
switch (match(value, callback = /(::plac\w+|:read-\w+)/)) {
// :read-(only|write)
case ':read-only': case ':read-write':
return serialize([copy(element, {props: [replace(value, /:(read-\w+)/, ':' + MOZ + '$1')]})], callback)
lift(copy(element, {props: [replace(value, /:(read-\w+)/, ':' + MOZ + '$1')]}))
lift(copy(element, {props: [value]}))
assign(element, {props: filter(children, callback)})
break
// :placeholder
case '::placeholder':
return serialize([
copy(element, {props: [replace(value, /:(plac\w+)/, ':' + WEBKIT + 'input-$1')]}),
copy(element, {props: [replace(value, /:(plac\w+)/, ':' + MOZ + '$1')]}),
copy(element, {props: [replace(value, /:(plac\w+)/, MS + 'input-$1')]})
], callback)
lift(copy(element, {props: [replace(value, /:(plac\w+)/, ':' + WEBKIT + 'input-$1')]}))
lift(copy(element, {props: [replace(value, /:(plac\w+)/, ':' + MOZ + '$1')]}))
lift(copy(element, {props: [replace(value, /:(plac\w+)/, MS + 'input-$1')]}))
lift(copy(element, {props: [value]}))
assign(element, {props: filter(children, callback)})
break
}

return ''
Expand Down
23 changes: 13 additions & 10 deletions src/Parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function parse (value, root, parent, rule, rules, rulesets, pseudo, point
case 47:
switch (peek()) {
case 42: case 47:
append(comment(commenter(next(), caret()), root, parent), declarations)
append(comment(commenter(next(), caret()), root, parent, declarations), declarations)
break
default:
characters += '/'
Expand All @@ -81,13 +81,13 @@ export function parse (value, root, parent, rule, rules, rulesets, pseudo, point
// ;
case 59 + offset: if (ampersand == -1) characters = replace(characters, /\f/g, '')
if (property > 0 && (strlen(characters) - length))
append(property > 32 ? declaration(characters + ';', rule, parent, length - 1) : declaration(replace(characters, ' ', '') + ';', rule, parent, length - 2), declarations)
append(property > 32 ? declaration(characters + ';', rule, parent, length - 1, declarations) : declaration(replace(characters, ' ', '') + ';', rule, parent, length - 2, declarations), declarations)
break
// @ ;
case 59: characters += ';'
// { rule/at-rule
default:
append(reference = ruleset(characters, root, parent, index, offset, rules, points, type, props = [], children = [], length), rulesets)
append(reference = ruleset(characters, root, parent, index, offset, rules, points, type, props = [], children = [], length, rulesets), rulesets)

if (character === 123)
if (offset === 0)
Expand All @@ -96,7 +96,7 @@ export function parse (value, root, parent, rule, rules, rulesets, pseudo, point
switch (atrule === 99 && charat(characters, 3) === 110 ? 100 : atrule) {
// d l m s
case 100: case 108: case 109: case 115:
parse(value, reference, reference, rule && append(ruleset(value, reference, reference, 0, 0, rules, points, type, rules, props = [], length), children), rules, children, length, points, rule ? props : children)
parse(value, reference, reference, rule && append(ruleset(value, reference, reference, 0, 0, rules, points, type, rules, props = [], length, children), children), rules, children, length, points, rule ? props : children)
break
default:
parse(characters, reference, reference, reference, [''], children, 0, points, children)
Expand Down Expand Up @@ -154,9 +154,10 @@ export function parse (value, root, parent, rule, rules, rulesets, pseudo, point
* @param {string[]} props
* @param {string[]} children
* @param {number} length
* @param {object[]} siblings
* @return {object}
*/
export function ruleset (value, root, parent, index, offset, rules, points, type, props, children, length) {
export function ruleset (value, root, parent, index, offset, rules, points, type, props, children, length, siblings) {
var post = offset - 1
var rule = offset === 0 ? rules : ['']
var size = sizeof(rule)
Expand All @@ -166,26 +167,28 @@ export function ruleset (value, root, parent, index, offset, rules, points, type
if (z = trim(j > 0 ? rule[x] + ' ' + y : replace(y, /&\f/g, rule[x])))
props[k++] = z

return node(value, root, parent, offset === 0 ? RULESET : type, props, children, length)
return node(value, root, parent, offset === 0 ? RULESET : type, props, children, length, siblings)
}

/**
* @param {number} value
* @param {object} root
* @param {object?} parent
* @param {object[]} siblings
* @return {object}
*/
export function comment (value, root, parent) {
return node(value, root, parent, COMMENT, from(char()), substr(value, 2, -2), 0)
export function comment (value, root, parent, siblings) {
return node(value, root, parent, COMMENT, from(char()), substr(value, 2, -2), 0, siblings)
}

/**
* @param {string} value
* @param {object} root
* @param {object?} parent
* @param {number} length
* @param {object[]} siblings
* @return {object}
*/
export function declaration (value, root, parent, length) {
return node(value, root, parent, DECLARATION, substr(value, 0, length), substr(value, length + 1, -1), length)
export function declaration (value, root, parent, length, siblings) {
return node(value, root, parent, DECLARATION, substr(value, 0, length), substr(value, length + 1, -1), length, siblings)
}
7 changes: 3 additions & 4 deletions src/Serializer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {IMPORT, LAYER, COMMENT, RULESET, DECLARATION, KEYFRAMES} from './Enum.js'
import {strlen, sizeof} from './Utility.js'
import {strlen} from './Utility.js'

/**
* @param {object[]} children
Expand All @@ -8,9 +8,8 @@ import {strlen, sizeof} from './Utility.js'
*/
export function serialize (children, callback) {
var output = ''
var length = sizeof(children)

for (var i = 0; i < length; i++)
for (var i = 0; i < children.length; i++)
output += callback(children[i], i, children, callback) || ''

return output
Expand All @@ -29,7 +28,7 @@ export function stringify (element, index, children, callback) {
case IMPORT: case DECLARATION: return element.return = element.return || element.value
case COMMENT: return ''
case KEYFRAMES: return element.return = element.value + '{' + serialize(element.children, callback) + '}'
case RULESET: element.value = element.props.join(',')
case RULESET: if (!strlen(element.value = element.props.join(','))) return ''
}

return strlen(children = serialize(element.children, callback)) ? element.return = element.value + '{' + children + '}' : ''
Expand Down
17 changes: 14 additions & 3 deletions src/Tokenizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ export var characters = ''
* @param {string} type
* @param {string[] | string} props
* @param {object[] | string} children
* @param {object[]} siblings
* @param {number} length
*/
export function node (value, root, parent, type, props, children, length) {
return {value: value, root: root, parent: parent, type: type, props: props, children: children, line: line, column: column, length: length, return: ''}
export function node (value, root, parent, type, props, children, length, siblings) {
return {value: value, root: root, parent: parent, type: type, props: props, children: children, line: line, column: column, length: length, return: '', siblings: siblings}
}

/**
Expand All @@ -26,7 +27,17 @@ export function node (value, root, parent, type, props, children, length) {
* @return {object}
*/
export function copy (root, props) {
return assign(node('', null, null, '', null, null, 0), root, {length: -root.length}, props)
return assign(node('', null, null, '', null, null, 0, root.siblings), root, {length: -root.length}, props)
}

/**
* @param {object} root
*/
export function lift (root) {
while (root.root)
root = copy(root.root, {children: [root]})

append(root, root.siblings)
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/Utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,12 @@ export function append (value, array) {
export function combine (array, callback) {
return array.map(callback).join('')
}

/**
* @param {string[]} array
* @param {RegExp} pattern
* @return {string[]}
*/
export function filter (array, pattern) {
return array.filter(function (value) { return !match(value, pattern) })
}
48 changes: 38 additions & 10 deletions test/Middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,41 @@ import {compile, serialize, stringify, middleware, rulesheet, prefixer, namespac
const stack = []

describe('Middleware', () => {
test('rulesheet', () => {
serialize(compile(`@import url('something.com/file.css');.user{ h1 {width:0;} @media{width:1;}@keyframes name{from{width:0;}to{width:1;}}}@keyframes empty{}`), middleware([prefixer, stringify, rulesheet(value => stack.push(value))]))
expect(stack).to.deep.equal([
test('rulesheet', () => {
serialize(compile(`
@import url('something.com/file.css');
.user{ h1 {width:0;}
@media{width:1;}
@keyframes name{from{width:0;}to{width:1;}}}
@keyframes empty{}
@supports (display: grid) {
@media (min-width: 500px){
.a::placeholder{color: red;}
}
}
@media (min-width: 500px) {
a:read-only{color:red;}
}
`), middleware([prefixer, stringify, rulesheet(value => stack.push(value))]))
expect(stack).to.deep.equal([
`@import url('something.com/file.css');`,
`.user h1{width:0;}`,
`@media{.user{width:1;}}`,
`@-webkit-keyframes name{from{width:0;}to{width:1;}}`,
`@keyframes name{from{width:0;}to{width:1;}}`,
'@-webkit-keyframes empty{}',
'@keyframes empty{}',
'@supports (display: grid){@media (min-width: 500px){.a::-webkit-input-placeholder{color:red;}}}',
'@supports (display: grid){@media (min-width: 500px){.a::-moz-placeholder{color:red;}}}',
'@supports (display: grid){@media (min-width: 500px){.a:-ms-input-placeholder{color:red;}}}',
'@supports (display: grid){@media (min-width: 500px){.a::placeholder{color:red;}}}',
'@media (min-width: 500px){a:-moz-read-only{color:red;}}',
'@media (min-width: 500px){a:read-only{color:red;}}',
])
})

test('namespace', () => {
expect(serialize(compile(`.user{width:0; :global(p,a){width:1;} h1 {width:1; h2:last-child {width:2} h2 h3 {width:3}}}`), middleware([namespace, stringify]))).to.equal([
expect(serialize(compile(`.user{width:0; :global(p,a){width:1;} h1 {width:1; h2:last-child {width:2} h2 h3 {width:3}}}`), middleware([namespace, stringify]))).to.equal([
`.user{width:0;}`, `p,a{width:1;}`, `h1.user.user{width:1;}`, `h1.user h2:last-child.user{width:2;}`, `h1.user h2 h3.user{width:3;}`
].join(''))

Expand Down Expand Up @@ -59,34 +79,42 @@ describe('Middleware', () => {
].join(''))

expect(serialize(compile(`textarea::placeholder{font-size:14px;@media{font-size:16px;}}`), middleware([prefixer, stringify]))).to.equal([
`textarea::-webkit-input-placeholder{font-size:14px;}textarea::-moz-placeholder{font-size:14px;}textarea:-ms-input-placeholder{font-size:14px;}textarea::placeholder{font-size:14px;}`,
`@media{textarea::-webkit-input-placeholder{font-size:16px;}textarea::-moz-placeholder{font-size:16px;}textarea:-ms-input-placeholder{font-size:16px;}textarea::placeholder{font-size:16px;}}`
`textarea::-webkit-input-placeholder{font-size:14px;}`,
`textarea::-moz-placeholder{font-size:14px;}`,
`textarea:-ms-input-placeholder{font-size:14px;}`,
`textarea::placeholder{font-size:14px;}`,
`@media{textarea::-webkit-input-placeholder{font-size:16px;}}`,
`@media{textarea::-moz-placeholder{font-size:16px;}}`,
`@media{textarea:-ms-input-placeholder{font-size:16px;}}`,
`@media{textarea::placeholder{font-size:16px;}}`,
].join(''))

expect(serialize(compile(`div:read-write{background-color:red;span{background-color:green;}}`), middleware([prefixer, stringify]))).to.equal([
`div:-moz-read-write{background-color:red;}`,
`div:read-write{background-color:red;}`,
`div:-moz-read-write span{background-color:green;}`,
`div:read-write span{background-color:green;}`
`div:read-write span{background-color:green;}`,
].join(''))

expect(serialize(compile(`div:read-write span{background-color:hotpink;}`), middleware([prefixer, stringify]))).to.equal([
`div:-moz-read-write span{background-color:hotpink;}`,
`div:read-write span{background-color:hotpink;}`
`div:read-write span{background-color:hotpink;}`,
].join(''))

expect(serialize(compile(`.read-write:read-write,.read-only:read-only,.placeholder::placeholder{background-color:hotpink;}`), middleware([prefixer, stringify]))).to.equal([
`.read-write:-moz-read-write{background-color:hotpink;}`,
`.read-write:read-write{background-color:hotpink;}`,
`.read-only:-moz-read-only{background-color:hotpink;}`,
`.read-only:read-only{background-color:hotpink;}`,
`.placeholder::-webkit-input-placeholder{background-color:hotpink;}`,
`.placeholder::-moz-placeholder{background-color:hotpink;}`,
`.placeholder:-ms-input-placeholder{background-color:hotpink;}`,
`.read-write:read-write,.read-only:read-only,.placeholder::placeholder{background-color:hotpink;}`,
`.placeholder::placeholder{background-color:hotpink;}`,
].join(''))

expect(serialize(compile(`:read-write{background-color:hotpink;}`), middleware([prefixer, stringify]))).to.equal([
`:-moz-read-write{background-color:hotpink;}`,
`:read-write{background-color:hotpink;}`
`:read-write{background-color:hotpink;}`,
].join(''))
})
})
6 changes: 3 additions & 3 deletions test/Tokenizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {tokenize} from "../index.js"

describe('Tokenizer', () => {
test('tokenize', () => {
expect(tokenize(`h1 h2 (h1 h2) 1 / 3 * 2 + 1 [1 2] "1 2" a`)).to.deep.equal([
'h1', ' ', 'h2', ' ', '(h1 h2)', ' ', '1', ' ', '/', ' ', '3', ' ', '*', ' ', '2', ' ', '+', ' ', '1', ' ', '[1 2]', ' ', '"1 2"', ' ', 'a'
])
expect(tokenize(`h1 h2 (h1 h2) 1 / 3 * 2 + 1 [1 2] "1 2" a`)).to.deep.equal([
'h1', ' ', 'h2', ' ', '(h1 h2)', ' ', '1', ' ', '/', ' ', '3', ' ', '*', ' ', '2', ' ', '+', ' ', '1', ' ', '[1 2]', ' ', '"1 2"', ' ', 'a'
])

expect(tokenize(`:global([data-popper-placement^='top'])`)).to.deep.equal([
`:`, `global`, `([data-popper-placement^='top'])`
Expand Down

0 comments on commit 0b6d567

Please sign in to comment.