aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/css-tree/lib/utils
diff options
context:
space:
mode:
Diffstat (limited to 'vanilla/node_modules/css-tree/lib/utils')
-rw-r--r--vanilla/node_modules/css-tree/lib/utils/List.js469
-rw-r--r--vanilla/node_modules/css-tree/lib/utils/clone.js21
-rw-r--r--vanilla/node_modules/css-tree/lib/utils/create-custom-error.js14
-rw-r--r--vanilla/node_modules/css-tree/lib/utils/ident.js101
-rw-r--r--vanilla/node_modules/css-tree/lib/utils/index.js6
-rw-r--r--vanilla/node_modules/css-tree/lib/utils/names.js106
-rw-r--r--vanilla/node_modules/css-tree/lib/utils/string.js99
-rw-r--r--vanilla/node_modules/css-tree/lib/utils/url.js108
8 files changed, 924 insertions, 0 deletions
diff --git a/vanilla/node_modules/css-tree/lib/utils/List.js b/vanilla/node_modules/css-tree/lib/utils/List.js
new file mode 100644
index 0000000..8953264
--- /dev/null
+++ b/vanilla/node_modules/css-tree/lib/utils/List.js
@@ -0,0 +1,469 @@
+//
+// list
+// ┌──────┐
+// ┌──────────────┼─head │
+// │ │ tail─┼──────────────┐
+// │ └──────┘ │
+// ▼ ▼
+// item item item item
+// ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
+// null ◀──┼─prev │◀───┼─prev │◀───┼─prev │◀───┼─prev │
+// │ next─┼───▶│ next─┼───▶│ next─┼───▶│ next─┼──▶ null
+// ├──────┤ ├──────┤ ├──────┤ ├──────┤
+// │ data │ │ data │ │ data │ │ data │
+// └──────┘ └──────┘ └──────┘ └──────┘
+//
+
+let releasedCursors = null;
+
+export class List {
+ static createItem(data) {
+ return {
+ prev: null,
+ next: null,
+ data
+ };
+ }
+
+ constructor() {
+ this.head = null;
+ this.tail = null;
+ this.cursor = null;
+ }
+ createItem(data) {
+ return List.createItem(data);
+ }
+
+ // cursor helpers
+ allocateCursor(prev, next) {
+ let cursor;
+
+ if (releasedCursors !== null) {
+ cursor = releasedCursors;
+ releasedCursors = releasedCursors.cursor;
+ cursor.prev = prev;
+ cursor.next = next;
+ cursor.cursor = this.cursor;
+ } else {
+ cursor = {
+ prev,
+ next,
+ cursor: this.cursor
+ };
+ }
+
+ this.cursor = cursor;
+
+ return cursor;
+ }
+ releaseCursor() {
+ const { cursor } = this;
+
+ this.cursor = cursor.cursor;
+ cursor.prev = null;
+ cursor.next = null;
+ cursor.cursor = releasedCursors;
+ releasedCursors = cursor;
+ }
+ updateCursors(prevOld, prevNew, nextOld, nextNew) {
+ let { cursor } = this;
+
+ while (cursor !== null) {
+ if (cursor.prev === prevOld) {
+ cursor.prev = prevNew;
+ }
+
+ if (cursor.next === nextOld) {
+ cursor.next = nextNew;
+ }
+
+ cursor = cursor.cursor;
+ }
+ }
+ *[Symbol.iterator]() {
+ for (let cursor = this.head; cursor !== null; cursor = cursor.next) {
+ yield cursor.data;
+ }
+ }
+
+ // getters
+ get size() {
+ let size = 0;
+
+ for (let cursor = this.head; cursor !== null; cursor = cursor.next) {
+ size++;
+ }
+
+ return size;
+ }
+ get isEmpty() {
+ return this.head === null;
+ }
+ get first() {
+ return this.head && this.head.data;
+ }
+ get last() {
+ return this.tail && this.tail.data;
+ }
+
+ // convertors
+ fromArray(array) {
+ let cursor = null;
+ this.head = null;
+
+ for (let data of array) {
+ const item = List.createItem(data);
+
+ if (cursor !== null) {
+ cursor.next = item;
+ } else {
+ this.head = item;
+ }
+
+ item.prev = cursor;
+ cursor = item;
+ }
+
+ this.tail = cursor;
+ return this;
+ }
+ toArray() {
+ return [...this];
+ }
+ toJSON() {
+ return [...this];
+ }
+
+ // array-like methods
+ forEach(fn, thisArg = this) {
+ // push cursor
+ const cursor = this.allocateCursor(null, this.head);
+
+ while (cursor.next !== null) {
+ const item = cursor.next;
+ cursor.next = item.next;
+ fn.call(thisArg, item.data, item, this);
+ }
+
+ // pop cursor
+ this.releaseCursor();
+ }
+ forEachRight(fn, thisArg = this) {
+ // push cursor
+ const cursor = this.allocateCursor(this.tail, null);
+
+ while (cursor.prev !== null) {
+ const item = cursor.prev;
+ cursor.prev = item.prev;
+ fn.call(thisArg, item.data, item, this);
+ }
+
+ // pop cursor
+ this.releaseCursor();
+ }
+ reduce(fn, initialValue, thisArg = this) {
+ // push cursor
+ let cursor = this.allocateCursor(null, this.head);
+ let acc = initialValue;
+ let item;
+
+ while (cursor.next !== null) {
+ item = cursor.next;
+ cursor.next = item.next;
+
+ acc = fn.call(thisArg, acc, item.data, item, this);
+ }
+
+ // pop cursor
+ this.releaseCursor();
+
+ return acc;
+ }
+ reduceRight(fn, initialValue, thisArg = this) {
+ // push cursor
+ let cursor = this.allocateCursor(this.tail, null);
+ let acc = initialValue;
+ let item;
+
+ while (cursor.prev !== null) {
+ item = cursor.prev;
+ cursor.prev = item.prev;
+
+ acc = fn.call(thisArg, acc, item.data, item, this);
+ }
+
+ // pop cursor
+ this.releaseCursor();
+
+ return acc;
+ }
+ some(fn, thisArg = this) {
+ for (let cursor = this.head; cursor !== null; cursor = cursor.next) {
+ if (fn.call(thisArg, cursor.data, cursor, this)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ map(fn, thisArg = this) {
+ const result = new List();
+
+ for (let cursor = this.head; cursor !== null; cursor = cursor.next) {
+ result.appendData(fn.call(thisArg, cursor.data, cursor, this));
+ }
+
+ return result;
+ }
+ filter(fn, thisArg = this) {
+ const result = new List();
+
+ for (let cursor = this.head; cursor !== null; cursor = cursor.next) {
+ if (fn.call(thisArg, cursor.data, cursor, this)) {
+ result.appendData(cursor.data);
+ }
+ }
+
+ return result;
+ }
+
+ nextUntil(start, fn, thisArg = this) {
+ if (start === null) {
+ return;
+ }
+
+ // push cursor
+ const cursor = this.allocateCursor(null, start);
+
+ while (cursor.next !== null) {
+ const item = cursor.next;
+ cursor.next = item.next;
+ if (fn.call(thisArg, item.data, item, this)) {
+ break;
+ }
+ }
+
+ // pop cursor
+ this.releaseCursor();
+ }
+ prevUntil(start, fn, thisArg = this) {
+ if (start === null) {
+ return;
+ }
+
+ // push cursor
+ const cursor = this.allocateCursor(start, null);
+
+ while (cursor.prev !== null) {
+ const item = cursor.prev;
+ cursor.prev = item.prev;
+ if (fn.call(thisArg, item.data, item, this)) {
+ break;
+ }
+ }
+
+ // pop cursor
+ this.releaseCursor();
+ }
+
+ // mutation
+ clear() {
+ this.head = null;
+ this.tail = null;
+ }
+ copy() {
+ const result = new List();
+
+ for (let data of this) {
+ result.appendData(data);
+ }
+
+ return result;
+ }
+ prepend(item) {
+ // head
+ // ^
+ // item
+ this.updateCursors(null, item, this.head, item);
+
+ // insert to the beginning of the list
+ if (this.head !== null) {
+ // new item <- first item
+ this.head.prev = item;
+ // new item -> first item
+ item.next = this.head;
+ } else {
+ // if list has no head, then it also has no tail
+ // in this case tail points to the new item
+ this.tail = item;
+ }
+
+ // head always points to new item
+ this.head = item;
+ return this;
+ }
+ prependData(data) {
+ return this.prepend(List.createItem(data));
+ }
+ append(item) {
+ return this.insert(item);
+ }
+ appendData(data) {
+ return this.insert(List.createItem(data));
+ }
+ insert(item, before = null) {
+ if (before !== null) {
+ // prev before
+ // ^
+ // item
+ this.updateCursors(before.prev, item, before, item);
+
+ if (before.prev === null) {
+ // insert to the beginning of list
+ if (this.head !== before) {
+ throw new Error('before doesn\'t belong to list');
+ }
+ // since head points to before therefore list doesn't empty
+ // no need to check tail
+ this.head = item;
+ before.prev = item;
+ item.next = before;
+ this.updateCursors(null, item);
+ } else {
+ // insert between two items
+ before.prev.next = item;
+ item.prev = before.prev;
+ before.prev = item;
+ item.next = before;
+ }
+ } else {
+ // tail
+ // ^
+ // item
+ this.updateCursors(this.tail, item, null, item);
+
+ // insert to the ending of the list
+ if (this.tail !== null) {
+ // last item -> new item
+ this.tail.next = item;
+ // last item <- new item
+ item.prev = this.tail;
+ } else {
+ // if list has no tail, then it also has no head
+ // in this case head points to new item
+ this.head = item;
+ }
+
+ // tail always points to new item
+ this.tail = item;
+ }
+
+ return this;
+ }
+ insertData(data, before) {
+ return this.insert(List.createItem(data), before);
+ }
+ remove(item) {
+ // item
+ // ^
+ // prev next
+ this.updateCursors(item, item.prev, item, item.next);
+
+ if (item.prev !== null) {
+ item.prev.next = item.next;
+ } else {
+ if (this.head !== item) {
+ throw new Error('item doesn\'t belong to list');
+ }
+
+ this.head = item.next;
+ }
+
+ if (item.next !== null) {
+ item.next.prev = item.prev;
+ } else {
+ if (this.tail !== item) {
+ throw new Error('item doesn\'t belong to list');
+ }
+
+ this.tail = item.prev;
+ }
+
+ item.prev = null;
+ item.next = null;
+
+ return item;
+ }
+ push(data) {
+ this.insert(List.createItem(data));
+ }
+ pop() {
+ return this.tail !== null ? this.remove(this.tail) : null;
+ }
+ unshift(data) {
+ this.prepend(List.createItem(data));
+ }
+ shift() {
+ return this.head !== null ? this.remove(this.head) : null;
+ }
+ prependList(list) {
+ return this.insertList(list, this.head);
+ }
+ appendList(list) {
+ return this.insertList(list);
+ }
+ insertList(list, before) {
+ // ignore empty lists
+ if (list.head === null) {
+ return this;
+ }
+
+ if (before !== undefined && before !== null) {
+ this.updateCursors(before.prev, list.tail, before, list.head);
+
+ // insert in the middle of dist list
+ if (before.prev !== null) {
+ // before.prev <-> list.head
+ before.prev.next = list.head;
+ list.head.prev = before.prev;
+ } else {
+ this.head = list.head;
+ }
+
+ before.prev = list.tail;
+ list.tail.next = before;
+ } else {
+ this.updateCursors(this.tail, list.tail, null, list.head);
+
+ // insert to end of the list
+ if (this.tail !== null) {
+ // if destination list has a tail, then it also has a head,
+ // but head doesn't change
+ // dest tail -> source head
+ this.tail.next = list.head;
+ // dest tail <- source head
+ list.head.prev = this.tail;
+ } else {
+ // if list has no a tail, then it also has no a head
+ // in this case points head to new item
+ this.head = list.head;
+ }
+
+ // tail always start point to new item
+ this.tail = list.tail;
+ }
+
+ list.head = null;
+ list.tail = null;
+ return this;
+ }
+ replace(oldItem, newItemOrList) {
+ if ('head' in newItemOrList) {
+ this.insertList(newItemOrList, oldItem);
+ } else {
+ this.insert(newItemOrList, oldItem);
+ }
+
+ this.remove(oldItem);
+ }
+}
diff --git a/vanilla/node_modules/css-tree/lib/utils/clone.js b/vanilla/node_modules/css-tree/lib/utils/clone.js
new file mode 100644
index 0000000..84819c0
--- /dev/null
+++ b/vanilla/node_modules/css-tree/lib/utils/clone.js
@@ -0,0 +1,21 @@
+import { List } from './List.js';
+
+export function clone(node) {
+ const result = {};
+
+ for (const key of Object.keys(node)) {
+ let value = node[key];
+
+ if (value) {
+ if (Array.isArray(value) || value instanceof List) {
+ value = value.map(clone);
+ } else if (value.constructor === Object) {
+ value = clone(value);
+ }
+ }
+
+ result[key] = value;
+ }
+
+ return result;
+}
diff --git a/vanilla/node_modules/css-tree/lib/utils/create-custom-error.js b/vanilla/node_modules/css-tree/lib/utils/create-custom-error.js
new file mode 100644
index 0000000..dba122f
--- /dev/null
+++ b/vanilla/node_modules/css-tree/lib/utils/create-custom-error.js
@@ -0,0 +1,14 @@
+export function createCustomError(name, message) {
+ // use Object.create(), because some VMs prevent setting line/column otherwise
+ // (iOS Safari 10 even throws an exception)
+ const error = Object.create(SyntaxError.prototype);
+ const errorStack = new Error();
+
+ return Object.assign(error, {
+ name,
+ message,
+ get stack() {
+ return (errorStack.stack || '').replace(/^(.+\n){1,3}/, `${name}: ${message}\n`);
+ }
+ });
+};
diff --git a/vanilla/node_modules/css-tree/lib/utils/ident.js b/vanilla/node_modules/css-tree/lib/utils/ident.js
new file mode 100644
index 0000000..9cbe0f8
--- /dev/null
+++ b/vanilla/node_modules/css-tree/lib/utils/ident.js
@@ -0,0 +1,101 @@
+import {
+ isName,
+ isValidEscape,
+ consumeEscaped,
+ decodeEscaped
+} from '../tokenizer/index.js';
+
+const REVERSE_SOLIDUS = 0x005c; // U+005C REVERSE SOLIDUS (\)
+
+export function decode(str) {
+ const end = str.length - 1;
+ let decoded = '';
+
+ for (let i = 0; i < str.length; 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
+ 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;
+}
+
+// https://drafts.csswg.org/cssom/#serialize-an-identifier
+// § 2.1. Common Serializing Idioms
+export function encode(str) {
+ let encoded = '';
+
+ // If the character is the first character and is a "-" (U+002D),
+ // and there is no second character, then the escaped character.
+ // Note: That's means a single dash string "-" return as escaped dash,
+ // so move the condition out of the main loop
+ if (str.length === 1 && str.charCodeAt(0) === 0x002D) {
+ return '\\-';
+ }
+
+ // To serialize an identifier means to create a string represented
+ // by the concatenation of, for each character of the identifier:
+ for (let i = 0; i < str.length; i++) {
+ const code = str.charCodeAt(i);
+
+ // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER (U+FFFD).
+ if (code === 0x0000) {
+ encoded += '\uFFFD';
+ continue;
+ }
+
+ if (
+ // If the character is in the range [\1-\1f] (U+0001 to U+001F) or is U+007F ...
+ // Note: Do not compare with 0x0001 since 0x0000 is precessed before
+ code <= 0x001F || code === 0x007F ||
+ // [or] ... is in the range [0-9] (U+0030 to U+0039),
+ (code >= 0x0030 && code <= 0x0039 && (
+ // If the character is the first character ...
+ i === 0 ||
+ // If the character is the second character ... and the first character is a "-" (U+002D)
+ i === 1 && str.charCodeAt(0) === 0x002D
+ ))
+ ) {
+ // ... then the character escaped as code point.
+ encoded += '\\' + code.toString(16) + ' ';
+ continue;
+ }
+
+ // If the character is not handled by one of the above rules and is greater
+ // than or equal to U+0080, is "-" (U+002D) or "_" (U+005F), or is in one
+ // of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to U+005A),
+ // or \[a-z] (U+0061 to U+007A), then the character itself.
+ if (isName(code)) {
+ encoded += str.charAt(i);
+ } else {
+ // Otherwise, the escaped character.
+ encoded += '\\' + str.charAt(i);
+ }
+ }
+
+ return encoded;
+}
diff --git a/vanilla/node_modules/css-tree/lib/utils/index.js b/vanilla/node_modules/css-tree/lib/utils/index.js
new file mode 100644
index 0000000..07bf0f9
--- /dev/null
+++ b/vanilla/node_modules/css-tree/lib/utils/index.js
@@ -0,0 +1,6 @@
+export * from './clone.js';
+export * as ident from './ident.js';
+export * from './List.js';
+export * from './names.js';
+export * as string from './string.js';
+export * as url from './url.js';
diff --git a/vanilla/node_modules/css-tree/lib/utils/names.js b/vanilla/node_modules/css-tree/lib/utils/names.js
new file mode 100644
index 0000000..b4f74b9
--- /dev/null
+++ b/vanilla/node_modules/css-tree/lib/utils/names.js
@@ -0,0 +1,106 @@
+const keywords = new Map();
+const properties = new Map();
+const HYPHENMINUS = 45; // '-'.charCodeAt()
+
+export const keyword = getKeywordDescriptor;
+export const property = getPropertyDescriptor;
+export const vendorPrefix = getVendorPrefix;
+export function isCustomProperty(str, offset) {
+ offset = offset || 0;
+
+ return str.length - offset >= 2 &&
+ str.charCodeAt(offset) === HYPHENMINUS &&
+ str.charCodeAt(offset + 1) === HYPHENMINUS;
+}
+
+function getVendorPrefix(str, offset) {
+ offset = offset || 0;
+
+ // verdor prefix should be at least 3 chars length
+ if (str.length - offset >= 3) {
+ // vendor prefix starts with hyper minus following non-hyper minus
+ if (str.charCodeAt(offset) === HYPHENMINUS &&
+ str.charCodeAt(offset + 1) !== HYPHENMINUS) {
+ // vendor prefix should contain a hyper minus at the ending
+ const secondDashIndex = str.indexOf('-', offset + 2);
+
+ if (secondDashIndex !== -1) {
+ return str.substring(offset, secondDashIndex + 1);
+ }
+ }
+ }
+
+ return '';
+}
+
+function getKeywordDescriptor(keyword) {
+ if (keywords.has(keyword)) {
+ return keywords.get(keyword);
+ }
+
+ const name = keyword.toLowerCase();
+ let descriptor = keywords.get(name);
+
+ if (descriptor === undefined) {
+ const custom = isCustomProperty(name, 0);
+ const vendor = !custom ? getVendorPrefix(name, 0) : '';
+ descriptor = Object.freeze({
+ basename: name.substr(vendor.length),
+ name,
+ prefix: vendor,
+ vendor,
+ custom
+ });
+ }
+
+ keywords.set(keyword, descriptor);
+
+ return descriptor;
+}
+
+function getPropertyDescriptor(property) {
+ if (properties.has(property)) {
+ return properties.get(property);
+ }
+
+ let name = property;
+ let hack = property[0];
+
+ if (hack === '/') {
+ hack = property[1] === '/' ? '//' : '/';
+ } else if (hack !== '_' &&
+ hack !== '*' &&
+ hack !== '$' &&
+ hack !== '#' &&
+ hack !== '+' &&
+ hack !== '&') {
+ hack = '';
+ }
+
+ const custom = isCustomProperty(name, hack.length);
+
+ // re-use result when possible (the same as for lower case)
+ if (!custom) {
+ name = name.toLowerCase();
+ if (properties.has(name)) {
+ const descriptor = properties.get(name);
+ properties.set(property, descriptor);
+ return descriptor;
+ }
+ }
+
+ const vendor = !custom ? getVendorPrefix(name, hack.length) : '';
+ const prefix = name.substr(0, hack.length + vendor.length);
+ const descriptor = Object.freeze({
+ basename: name.substr(prefix.length),
+ name: name.substr(hack.length),
+ hack,
+ vendor,
+ prefix,
+ custom
+ });
+
+ properties.set(property, descriptor);
+
+ return descriptor;
+}
diff --git a/vanilla/node_modules/css-tree/lib/utils/string.js b/vanilla/node_modules/css-tree/lib/utils/string.js
new file mode 100644
index 0000000..928a85b
--- /dev/null
+++ b/vanilla/node_modules/css-tree/lib/utils/string.js
@@ -0,0 +1,99 @@
+import {
+ isHexDigit,
+ isWhiteSpace,
+ isValidEscape,
+ consumeEscaped,
+ decodeEscaped
+} from '../tokenizer/index.js';
+
+const REVERSE_SOLIDUS = 0x005c; // U+005C REVERSE SOLIDUS (\)
+const QUOTATION_MARK = 0x0022; // "
+const APOSTROPHE = 0x0027; // '
+
+export function decode(str) {
+ const len = str.length;
+ const firstChar = str.charCodeAt(0);
+ const start = firstChar === QUOTATION_MARK || firstChar === APOSTROPHE ? 1 : 0;
+ const end = start === 1 && len > 1 && str.charCodeAt(len - 1) === firstChar ? len - 2 : len - 1;
+ let decoded = '';
+
+ 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;
+}
+
+// https://drafts.csswg.org/cssom/#serialize-a-string
+// § 2.1. Common Serializing Idioms
+export function encode(str, apostrophe) {
+ const quote = apostrophe ? '\'' : '"';
+ const quoteCode = apostrophe ? APOSTROPHE : QUOTATION_MARK;
+ let encoded = '';
+ let wsBeforeHexIsNeeded = false;
+
+ for (let i = 0; i < str.length; i++) {
+ const code = str.charCodeAt(i);
+
+ // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER (U+FFFD).
+ if (code === 0x0000) {
+ encoded += '\uFFFD';
+ continue;
+ }
+
+ // If the character is in the range [\1-\1f] (U+0001 to U+001F) or is U+007F,
+ // the character escaped as code point.
+ // Note: Do not compare with 0x0001 since 0x0000 is precessed before
+ if (code <= 0x001f || code === 0x007F) {
+ encoded += '\\' + code.toString(16);
+ wsBeforeHexIsNeeded = true;
+ continue;
+ }
+
+ // If the character is '"' (U+0022) or "\" (U+005C), the escaped character.
+ if (code === quoteCode || code === REVERSE_SOLIDUS) {
+ encoded += '\\' + str.charAt(i);
+ wsBeforeHexIsNeeded = false;
+ } else {
+ if (wsBeforeHexIsNeeded && (isHexDigit(code) || isWhiteSpace(code))) {
+ encoded += ' ';
+ }
+
+ // Otherwise, the character itself.
+ encoded += str.charAt(i);
+ wsBeforeHexIsNeeded = false;
+ }
+ }
+
+ return quote + encoded + quote;
+}
diff --git a/vanilla/node_modules/css-tree/lib/utils/url.js b/vanilla/node_modules/css-tree/lib/utils/url.js
new file mode 100644
index 0000000..cce5709
--- /dev/null
+++ b/vanilla/node_modules/css-tree/lib/utils/url.js
@@ -0,0 +1,108 @@
+import {
+ isHexDigit,
+ isWhiteSpace,
+ isValidEscape,
+ consumeEscaped,
+ decodeEscaped
+} from '../tokenizer/index.js';
+
+const SPACE = 0x0020; // U+0020 SPACE
+const REVERSE_SOLIDUS = 0x005c; // U+005C REVERSE SOLIDUS (\)
+const QUOTATION_MARK = 0x0022; // "
+const APOSTROPHE = 0x0027; // '
+const LEFTPARENTHESIS = 0x0028; // U+0028 LEFT PARENTHESIS (()
+const RIGHTPARENTHESIS = 0x0029; // U+0029 RIGHT PARENTHESIS ())
+
+export 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 left parenthesis 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;
+}
+
+export function encode(str) {
+ let encoded = '';
+ let wsBeforeHexIsNeeded = false;
+
+ for (let i = 0; i < str.length; i++) {
+ const code = str.charCodeAt(i);
+
+ // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER (U+FFFD).
+ if (code === 0x0000) {
+ encoded += '\uFFFD';
+ continue;
+ }
+
+ // If the character is in the range [\1-\1f] (U+0001 to U+001F) or is U+007F,
+ // the character escaped as code point.
+ // Note: Do not compare with 0x0001 since 0x0000 is precessed before
+ if (code <= 0x001f || code === 0x007F) {
+ encoded += '\\' + code.toString(16);
+ wsBeforeHexIsNeeded = true;
+ continue;
+ }
+
+ if (code === SPACE ||
+ 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 + ')';
+}