diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-13 21:34:48 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-13 21:34:48 -0800 |
| commit | 76cb9c2a39d477a64824a985ade40507e3bbade1 (patch) | |
| tree | 41e997aa9c6f538d3a136af61dae9424db2005a9 /vanilla/node_modules/css-tree/lib/utils | |
| parent | 819a39a21ac992b1393244a4c283bbb125208c69 (diff) | |
| download | neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.gz neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.bz2 neko-76cb9c2a39d477a64824a985ade40507e3bbade1.zip | |
feat(vanilla): add testing infrastructure and tests (NK-wjnczv)
Diffstat (limited to 'vanilla/node_modules/css-tree/lib/utils')
| -rw-r--r-- | vanilla/node_modules/css-tree/lib/utils/List.js | 469 | ||||
| -rw-r--r-- | vanilla/node_modules/css-tree/lib/utils/clone.js | 21 | ||||
| -rw-r--r-- | vanilla/node_modules/css-tree/lib/utils/create-custom-error.js | 14 | ||||
| -rw-r--r-- | vanilla/node_modules/css-tree/lib/utils/ident.js | 101 | ||||
| -rw-r--r-- | vanilla/node_modules/css-tree/lib/utils/index.js | 6 | ||||
| -rw-r--r-- | vanilla/node_modules/css-tree/lib/utils/names.js | 106 | ||||
| -rw-r--r-- | vanilla/node_modules/css-tree/lib/utils/string.js | 99 | ||||
| -rw-r--r-- | vanilla/node_modules/css-tree/lib/utils/url.js | 108 |
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 + ')'; +} |
