var REVERSE_SOLIDUS = 0x5c; // \ var QUOTATION_MARK = 0x22; // " var APOSTROPHE = 0x27; // ' var TAB = 0x09; // tab var WHITESPACE = 0x20; // space var AMPERSAND = 0x26; var LESSTHANSIGN = 0x3C; var GREATERTHANSIGN = 0x3E; function isHex(code) { return (code >= 48 && code <= 57) || // 0 .. 9 (code >= 65 && code <= 70) || // A .. F (code >= 97 && code <= 102); // a .. f } function decodeString(str) { var decoded = ''; var len = str.length; var firstChar = str.charCodeAt(0); var start = firstChar === QUOTATION_MARK || firstChar === APOSTROPHE ? 1 : 0; var end = start === 1 && len > 1 && str.charCodeAt(len - 1) === firstChar ? len - 2 : len - 1; for (var i = start; i <= end; i++) { var 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); // ignore escaped newline if (code !== 0x0A && code !== 0x0C && code !== 0x0D) { // TODO: should treat a "CR/LF" pair (U+000D/U+000A) as a single white space character // https://drafts.csswg.org/css-syntax/#consume-escaped-code-point for (var j = 0; j < 6 && i + j <= end;) { code = str.charCodeAt(i + j); if (isHex(code)) { j++; } else { break; } } if (j > 0) { code = str.charCodeAt(i + j); // include space into sequence // TODO: add newline support if (code === WHITESPACE || code === TAB) { j++; } code = parseInt(str.substr(i, j), 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; } // FIXME: code above 0xFFFF will be converted incorrectly, // better to use String.fromCharPoint() but it lack of support by engines decoded += String.fromCharCode(code); i += j - 1; } else { decoded += str.charAt(i); } } } else { decoded += str.charAt(i); } } return decoded; } function encodeString(str, apostrophe) { var quote = apostrophe ? '\'' : '"'; var quoteCode = apostrophe ? APOSTROPHE : QUOTATION_MARK; var encoded = quote; var wsBeforeHexIsNeeded = false; for (var i = 0; i < str.length; i++) { var code = str.charCodeAt(i); if (code <= 0x1F || code === AMPERSAND || code === LESSTHANSIGN || code === GREATERTHANSIGN) { encoded += '\\' + code.toString(16); wsBeforeHexIsNeeded = true; } else if (code === REVERSE_SOLIDUS || code === quoteCode) { encoded += '\\' + str.charAt(i); wsBeforeHexIsNeeded = false; } else { if (wsBeforeHexIsNeeded && isHex(code)) { encoded += ' '; } encoded += str.charAt(i); wsBeforeHexIsNeeded = false; } } encoded += quote; return encoded; } module.exports = { decode: decodeString, encode: encodeString };