Skip to content

Commit

Permalink
Decode/encode value for Url node
Browse files Browse the repository at this point in the history
  • Loading branch information
lahmatiy committed Jan 20, 2020
1 parent d6a6b6e commit 3726659
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 131 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
- Added `mode` option for `generate()` to specify a mode of token separation: `spec` or `safe` (by default)
- Renamed `HexColor` node type into `Hash`
- Removed `element()` specific parsing rules
- Changed `String` to store decoded string value (and auto encode a value on serialize)
- Changed `String` node type to store decoded string value (and auto encode a value on serialize)
- Changed `Url` node type to store decoded url value (and auto encode a value on serialize)

## 1.0.0-alpha.39 (December 5, 2019)

Expand Down
2 changes: 1 addition & 1 deletion docs/ast.md
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ Used for [the Unicode-Range microsyntax](https://drafts.csswg.org/css-syntax/#ur
```js
{
type: "Url",
value: <String> | <Raw>
value: String
}
```

Expand Down
8 changes: 1 addition & 7 deletions docs/traversal.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,7 @@ const ast = csstree.parse(`
const urls = [];
csstree.walk(ast, function(node) {
if (this.declaration !== null && node.type === 'Url') {
const value = node.value;

if (value.type === 'Raw') {
urls.push(value.value);
} else {
urls.push(value.value.substr(1, value.value.length - 2));
}
urls.push(node.value);
}
});

Expand Down
45 changes: 16 additions & 29 deletions lib/syntax/node/Url.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const { isWhiteSpace } = require('../../tokenizer');
const url = require('../../utils/url');
const string = require('../../utils/string');
const {
Function: FunctionToken,
String: StringToken,
Url,
RightParenthesis
} = require('../../tokenizer/types');
Expand All @@ -9,32 +11,15 @@ const {
module.exports = {
name: 'Url',
structure: {
value: ['String', 'Raw']
value: String
},
parse: function() {
const start = this.tokenStart;
let value;

switch (this.tokenType) {
case Url:
let rawStart = start + 4;
let rawEnd = this.tokenEnd - 1;

while (rawStart < rawEnd && isWhiteSpace(this.charCodeAt(rawStart))) {
rawStart++;
}

while (rawStart < rawEnd && isWhiteSpace(this.charCodeAt(rawEnd - 1))) {
rawEnd--;
}

value = {
type: 'Raw',
loc: this.getLocation(rawStart, rawEnd),
value: this.substring(rawStart, rawEnd)
};

this.eat(Url);
value = url.decode(this.consume(Url));
break;

case FunctionToken:
Expand All @@ -44,9 +29,11 @@ module.exports = {

this.eat(FunctionToken);
this.skipSC();
value = this.String();
value = string.decode(this.consume(StringToken));
this.skipSC();
this.eat(RightParenthesis);
if (!this.eof) {
this.eat(RightParenthesis);
}
break;

default:
Expand All @@ -60,12 +47,12 @@ module.exports = {
};
},
generate: function(node) {
if (node.value.type === 'Raw') {
this.token(Url, 'url(' + node.value.value + ')');
} else {
this.token(FunctionToken, 'url(');
this.node(node.value);
this.token(RightParenthesis, ')');
}
// if (node.value.type === 'Raw') {
this.token(Url, url.encode(node.value));
// } else {
// this.token(FunctionToken, 'url(');
// this.token(StringToken, string.encode(node.value));
// this.token(RightParenthesis, ')');
// }
}
};
114 changes: 114 additions & 0 deletions lib/utils/url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
const { isHexDigit, isNewline, isWhiteSpace, isValidEscape } = require('../tokenizer/char-code-definitions');
const { consumeEscaped } = require('../tokenizer/utils');

const REVERSE_SOLIDUS = 0x005c; // \
const QUOTATION_MARK = 0x0022; // "
const APOSTROPHE = 0x0027; // '
const LEFTPARENTHESIS = 0x0028; // U+0028 LEFT PARENTHESIS (()
const RIGHTPARENTHESIS = 0x0029; // U+0029 RIGHT PARENTHESIS ())

function decodeEscaped(escaped) {
if (escaped.length === 1 && !isHexDigit(escaped.charCodeAt(0))) {
return escaped[0];
}

let code = parseInt(escaped, 16);

if (
(code === 0) || // If this number is zero,
(code >= 0xD800 && code <= 0xDFFF) || // or is for a surrogate,
(code > 0x10FFFF) // or is greater than the maximum allowed code point
) {
// ... return U+FFFD REPLACEMENT CHARACTER
code = 0xFFFD;
}

return String.fromCodePoint(code);
}

function decode(str) {
const len = str.length;
let start = 4; // length of "url("
let end = str.charCodeAt(len - 1) === RIGHTPARENTHESIS ? len - 2 : len - 1;
let decoded = '';

while (start < end && isWhiteSpace(str.charCodeAt(start))) {
start++;
}

while (start < end && isWhiteSpace(str.charCodeAt(end))) {
end--;
}

for (let i = start; i <= end; i++) {
let code = str.charCodeAt(i);

if (code === REVERSE_SOLIDUS) {
// special case at the ending
if (i === end) {
// if the next input code point is EOF, do nothing
// otherwise include last quote as escaped
if (i !== len - 1) {
decoded = str.substr(i + 1);
}
break;
}

code = str.charCodeAt(++i);

// consume escaped
if (isValidEscape(REVERSE_SOLIDUS, code)) {
const escapeStart = i - 1;
const escapeEnd = consumeEscaped(str, escapeStart);

i = escapeEnd - 1;
decoded += decodeEscaped(str.substring(escapeStart + 1, escapeEnd));
} else {
// \r\n
if (code === 0x000d && str.charCodeAt(i + 1) === 0x000a) {
i++;
}
}
} else {
decoded += str[i];
}
}

return decoded;
}

function encode(str) {
let encoded = '';
let wsBeforeHexIsNeeded = false;

for (let i = 0; i < str.length; i++) {
let code = str.charCodeAt(i);

if (isNewline(code)) {
encoded += '\\' + code.toString(16);
wsBeforeHexIsNeeded = true;
} else if (isWhiteSpace(code) ||
code === REVERSE_SOLIDUS ||
code === QUOTATION_MARK ||
code === APOSTROPHE ||
code === LEFTPARENTHESIS ||
code === RIGHTPARENTHESIS) {
encoded += '\\' + str.charAt(i);
wsBeforeHexIsNeeded = false;
} else {
if (wsBeforeHexIsNeeded && isHexDigit(code)) {
encoded += ' ';
}

encoded += str.charAt(i);
wsBeforeHexIsNeeded = false;
}
}

return 'url(' + encoded + ')';
}

module.exports = {
decode,
encode
};
5 changes: 1 addition & 4 deletions test/fixture/parse/atrule/atrule/font-face.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@
"children": [
{
"type": "Url",
"value": {
"type": "Raw",
"value": "test.woff"
}
"value": "test.woff"
}
]
}
Expand Down
5 changes: 1 addition & 4 deletions test/fixture/parse/atrule/atrule/import.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@
"children": [
{
"type": "Url",
"value": {
"type": "Raw",
"value": "test"
}
"value": "test"
}
]
},
Expand Down
12 changes: 3 additions & 9 deletions test/fixture/parse/atrule/no-block.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,7 @@
"children": [
{
"type": "Url",
"value": {
"type": "Raw",
"value": "http://example.com"
}
"value": "http://example.com"
}
]
},
Expand Down Expand Up @@ -169,7 +166,7 @@
},
"no at-rule ending on eof is not an error": {
"source": "@atrule url('..')",
"generate": "@atrule url(\"..\");",
"generate": "@atrule url(..);",
"ast": {
"type": "Atrule",
"name": "atrule",
Expand All @@ -178,10 +175,7 @@
"children": [
{
"type": "Url",
"value": {
"type": "String",
"value": ".."
}
"value": ".."
}
]
},
Expand Down
52 changes: 13 additions & 39 deletions test/fixture/parse/value/Url.json
Original file line number Diff line number Diff line change
@@ -1,98 +1,72 @@
{
"uri.0": {
"source": "url(\"http://test.com\")",
"generate": "url(http://test.com)",
"ast": {
"type": "Url",
"value": {
"type": "String",
"value": "http://test.com"
}
"value": "http://test.com"
}
},
"uri.1": {
"source": "url(http://test.com)",
"ast": {
"type": "Url",
"value": {
"type": "Raw",
"value": "http://test.com"
}
"value": "http://test.com"
}
},
"uri.c.1": {
"source": "url(/*test*/http://test.com/*test*/)",
"ast": {
"type": "Url",
"value": {
"type": "Raw",
"value": "/*test*/http://test.com/*test*/"
}
"value": "/*test*/http://test.com/*test*/"
}
},
"uri.s.0": {
"source": "url( 'http://test.com' )",
"generate": "url(\"http://test.com\")",
"generate": "url(http://test.com)",
"ast": {
"type": "Url",
"value": {
"type": "String",
"value": "http://test.com"
}
"value": "http://test.com"
}
},
"uri.s.1": {
"source": "url( http://test.com )",
"generate": "url(http://test.com)",
"ast": {
"type": "Url",
"value": {
"type": "Raw",
"value": "http://test.com"
}
"value": "http://test.com"
}
},
"uri with parentheses": {
"source": "url( p\\(1\\).png )",
"generate": "url(p\\(1\\).png)",
"ast": {
"type": "Url",
"value": {
"type": "Raw",
"value": "p\\(1\\).png"
}
"value": "p(1).png"
}
},
"uri escaping": {
"source": "url( \\31 \\ \\(2\\)\\.png )",
"generate": "url(\\31 \\ \\(2\\)\\.png)",
"generate": "url(1\\ \\(2\\).png)",
"ast": {
"type": "Url",
"value": {
"type": "Raw",
"value": "\\31 \\ \\(2\\)\\.png"
}
"value": "1 (2).png"
}
},
"uri with special symbols": {
"source": "url( p{1};.png )",
"generate": "url(p{1};.png)",
"ast": {
"type": "Url",
"value": {
"type": "Raw",
"value": "p{1};.png"
}
"value": "p{1};.png"
}
},
"uri with parentheses in string": {
"source": "url( 'p (1).png' )",
"generate": "url(\"p (1).png\")",
"generate": "url(p\\ \\(1\\).png)",
"ast": {
"type": "Url",
"value": {
"type": "String",
"value": "p (1).png"
}
"value": "p (1).png"
}
},
"error": [
Expand Down

0 comments on commit 3726659

Please sign in to comment.