'use strict'; /** * @typedef {import('../lib/types.js').PluginInfo} PluginInfo * @typedef {import('../lib/types').XastElement} XastElement */ const csstree = require('css-tree'); const { referencesProps } = require('./_collections.js'); exports.name = 'prefixIds'; exports.description = 'prefix IDs'; /** * extract basename from path * @type {(path: string) => string} */ const getBasename = (path) => { // extract everything after latest slash or backslash const matched = path.match(/[/\\]?([^/\\]+)$/); if (matched) { return matched[1]; } return ''; }; /** * escapes a string for being used as ID * @type {(string: string) => string} */ const escapeIdentifierName = (str) => { return str.replace(/[. ]/g, '_'); }; /** * @type {(string: string) => string} */ const unquote = (string) => { if ( (string.startsWith('"') && string.endsWith('"')) || (string.startsWith("'") && string.endsWith("'")) ) { return string.slice(1, -1); } return string; }; /** * Prefix the given string, unless it already starts with the generated prefix. * * @param {(id: string) => string} prefixGenerator Function to generate a prefix. * @param {string} body An arbitrary string. * @returns {string} The given string with a prefix prepended to it. */ const prefixId = (prefixGenerator, body) => { const prefix = prefixGenerator(body); if (body.startsWith(prefix)) { return body; } return prefix + body; }; /** * Insert the prefix in a reference string. A reference string is already * prefixed with #, so the prefix is inserted after the first character. * * @param {(id: string) => string} prefixGenerator Function to generate a prefix. * @param {string} reference An arbitrary string, should start with "#". * @returns {?string} The given string with a prefix inserted, or null if the string did not start with "#". */ const prefixReference = (prefixGenerator, reference) => { if (reference.startsWith('#')) { return '#' + prefixId(prefixGenerator, reference.slice(1)); } return null; }; /** * Generates a prefix for the given string. * * @param {string} body An arbitrary string. * @param {XastElement} node XML node that the identifier belongs to. * @param {PluginInfo} info * @param {((node: XastElement, info: PluginInfo) => string)|string|boolean|undefined} prefixGenerator Some way of obtaining a prefix. * @param {string} delim Content to insert between the prefix and original value. * @param {Map} history Map of previously generated prefixes to IDs. * @returns {string} A generated prefix. */ const generatePrefix = (body, node, info, prefixGenerator, delim, history) => { if (typeof prefixGenerator === 'function') { let prefix = history.get(body); if (prefix != null) { return prefix; } prefix = prefixGenerator(node, info) + delim; history.set(body, prefix); return prefix; } if (typeof prefixGenerator === 'string') { return prefixGenerator + delim; } if (prefixGenerator === false) { return ''; } if (info.path != null && info.path.length > 0) { return escapeIdentifierName(getBasename(info.path)) + delim; } return 'prefix' + delim; }; /** * Prefixes identifiers * * @author strarsis * * @type {import('./plugins-types').Plugin<'prefixIds'>} */ exports.fn = (_root, params, info) => { const { delim = '__', prefix, prefixIds = true, prefixClassNames = true, } = params; /** @type {Map} */ const prefixMap = new Map(); return { element: { enter: (node) => { /** * @param {string} id A node identifier or class. * @returns {string} Given string with a prefix inserted, or null if the string did not start with "#". */ const prefixGenerator = (id) => generatePrefix(id, node, info, prefix, delim, prefixMap); // prefix id/class selectors and url() references in styles if (node.name === 'style') { // skip empty