@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
252 lines (214 loc) • 6.65 kB
JavaScript
/**
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact schukai GmbH.
*
* SPDX-License-Identifier: AGPL-3.0
*/
export { parseBracketedKeyValueHash, createBracketedKeyValueHash };
/**
* Parses a string containing bracketed key-value pairs and returns an object representing the parsed result.
*
* - The string starts with a hash symbol #.
* - After the hash symbol, there are one or more selector strings, separated by a semicolon ;.
* - Each selector string has the format selectorName(key1=value1,key2=value2,...).
* - The selector name is a string of one or more alphanumeric characters.
* - The key-value pairs are separated by commas , and are of the form key=value.
* - The key is a string of one or more alphanumeric characters.
* - The value can be an empty string or a string of one or more characters.
* - If the value contains commas, it must be enclosed in double quotes ".
* - The entire key-value pair must be URL-encoded.
* - The closing parenthesis ) for each selector must be present, even if there are no key-value pairs.
*
* @example
*
* ```javascript
* // Example 1:
* const hashString = '#selector1(key1=value1,key2=value2);selector2(key3=value3)';
* const result = parseBracketedKeyValueHash(hashString);
* // result => { selector1: { key1: "value1", key2: "value2" }, selector2: { key3: "value3" } }
* ```
*
* @example
*
* ```javascript
* // Example 2:
* const hashString = '#selector1(key1=value1,key2=value2);selector2(';
* const result = parseBracketedKeyValueHash(hashString);
* // result => {}
* ```
*
* @since 3.37.0
* @param {string} hashString - The string to parse, containing bracketed key-value pairs.
* @return {Object} - An object representing the parsed result, with keys representing the selectors and values representing the key-value pairs associated with each selector.
* - Returns an empty object if there was an error during parsing. */
function parseBracketedKeyValueHash(hashString) {
const selectors = {};
//const selectorStack = [];
//const keyValueStack = [];
const trimmedHashString = hashString.trim();
const cleanedHashString =
trimmedHashString.charAt(0) === "#"
? trimmedHashString.slice(1)
: trimmedHashString;
//const selectors = (keyValueStack.length > 0) ? result[selectorStack[selectorStack.length - 1]] : result;
let currentSelector = "";
function addToResult(key, value) {
if (currentSelector && key) {
if (!selectors[currentSelector]) {
selectors[currentSelector] = {};
}
selectors[currentSelector][key] = value;
}
}
let currentKey = "";
let currentValue = "";
let inKey = true;
let inValue = false;
let inQuotedValue = false;
let inSelector = true;
let escaped = false;
let quotedValueStartChar = "";
for (let i = 0; i < cleanedHashString.length; i++) {
const c = cleanedHashString[i];
const nextChar = cleanedHashString?.[i + 1];
if (c === "\\" && !escaped) {
escaped = true;
continue;
}
if (escaped) {
if (inSelector) {
currentSelector += c;
} else if (inKey) {
currentKey += c;
} else if (inValue) {
currentValue += c;
}
escaped = false;
continue;
}
if (inQuotedValue && quotedValueStartChar !== c) {
if (inSelector) {
currentSelector += c;
} else if (inKey) {
currentKey += c;
} else if (inValue) {
currentValue += c;
}
continue;
}
if (c === ";" && inSelector) {
inSelector = true;
currentSelector = "";
continue;
}
if (inSelector === true && c !== "(") {
currentSelector += c;
continue;
}
if (c === "(" && inSelector) {
inSelector = false;
inKey = true;
currentKey = "";
continue;
}
if (inKey === true && c !== "=") {
currentKey += c;
continue;
}
if (c === "=" && inKey) {
inKey = false;
inValue = true;
if (nextChar === '"' || nextChar === "'") {
inQuotedValue = true;
quotedValueStartChar = nextChar;
i++;
continue;
}
currentValue = "";
continue;
}
if (inValue === true) {
if (inQuotedValue) {
if (c === quotedValueStartChar) {
inQuotedValue = false;
continue;
}
currentValue += c;
continue;
}
if (c === ",") {
inValue = false;
inKey = true;
const decodedCurrentValue = decodeURIComponent(currentValue);
addToResult(currentKey, decodedCurrentValue);
currentKey = "";
currentValue = "";
continue;
}
if (c === ")") {
inValue = false;
//inKey = true;
inSelector = true;
const decodedCurrentValue = decodeURIComponent(currentValue);
addToResult(currentKey, decodedCurrentValue);
currentKey = "";
currentValue = "";
currentSelector = "";
continue;
}
currentValue += c;
continue;
}
}
if (inSelector) {
return selectors;
}
return {};
}
/**
* Creates a hash selector string from an object.
*
* @param {Object} object - The object containing selectors and key-value pairs.
* @param {boolean} addHashPrefix - Whether to add the hash prefix # to the beginning of the string.
* @return {string} The hash selector string.
* @since 3.37.0
*/
function createBracketedKeyValueHash(object, addHashPrefix = true) {
if (!object) {
return addHashPrefix ? "#" : "";
}
let hashString = "";
function encodeKeyValue(key, value) {
return encodeURIComponent(key) + "=" + encodeURIComponent(value);
}
for (const selector in object) {
if (object.hasOwnProperty(selector)) {
const keyValuePairs = object[selector];
let selectorString = selector;
let keyValueString = "";
for (const key in keyValuePairs) {
if (keyValuePairs.hasOwnProperty(key)) {
const value = keyValuePairs[key];
keyValueString += keyValueString.length === 0 ? "" : ",";
keyValueString += encodeKeyValue(key, value);
}
}
if (keyValueString.length > 0) {
selectorString += "(" + keyValueString + ")";
hashString += hashString.length === 0 ? "" : ";";
hashString += selectorString;
}
}
}
if (hashString.length > 0 && addHashPrefix) {
hashString = "#" + hashString;
}
return hashString;
}