Skip to content

Commit

Permalink
fix(prefixIds): dynamic functions should produce same prefix for give…
Browse files Browse the repository at this point in the history
…n id (#1814)

Reuse the same prefix if an ID is encountered multiple times.
  • Loading branch information
SethFalco authored Oct 25, 2023
1 parent d4f0de0 commit bf45a02
Showing 1 changed file with 91 additions and 36 deletions.
127 changes: 91 additions & 36 deletions plugins/prefixIds.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
'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');

Expand Down Expand Up @@ -41,29 +46,73 @@ const unquote = (string) => {
};

/**
* prefix an ID
* @type {(prefix: string, name: string) => 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 = (prefix, value) => {
if (value.startsWith(prefix)) {
return value;
const prefixId = (prefixGenerator, body) => {
const prefix = prefixGenerator(body);
if (body.startsWith(prefix)) {
return body;
}
return prefix + value;
return prefix + body;
};

/**
* prefix an #ID
* @type {(prefix: string, name: string) => string | null}
* 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|null} The given string with a prefix inserted, or null if the string did not start with "#".
*/
const prefixReference = (prefix, value) => {
if (value.startsWith('#')) {
return '#' + prefixId(prefix, value.slice(1));
const prefixReference = (prefixGenerator, reference) => {
if (reference.startsWith('#')) {
return '#' + prefixId(prefixGenerator, reference.slice(1));
}
return null;
};

/** @type {(value: any) => any} */
const toAny = (value) => value;
/**
* 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<string, string>} 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
Expand All @@ -73,25 +122,25 @@ const toAny = (value) => value;
* @type {import('./plugins-types').Plugin<'prefixIds'>}
*/
exports.fn = (_root, params, info) => {
const { delim = '__', prefixIds = true, prefixClassNames = true } = params;
const {
delim = '__',
prefix,
prefixIds = true,
prefixClassNames = true,
} = params;

/** @type {Map<string, string>} */
const prefixMap = new Map();

return {
element: {
enter: (node) => {
/**
* prefix, from file name or option
* @type {string}
* @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 "#".
*/
let prefix = 'prefix' + delim;
if (typeof params.prefix === 'function') {
prefix = params.prefix(node, info) + delim;
} else if (typeof params.prefix === 'string') {
prefix = params.prefix + delim;
} else if (params.prefix === false) {
prefix = '';
} else if (info.path != null && info.path.length > 0) {
prefix = escapeIdentifierName(getBasename(info.path)) + delim;
}
const prefixGenerator = (id) =>
generatePrefix(id, node, info, prefix, delim, prefixMap);

// prefix id/class selectors and url() references in styles
if (node.name === 'style') {
Expand Down Expand Up @@ -127,18 +176,21 @@ exports.fn = (_root, params, info) => {
(prefixIds && node.type === 'IdSelector') ||
(prefixClassNames && node.type === 'ClassSelector')
) {
node.name = prefixId(prefix, node.name);
node.name = prefixId(prefixGenerator, node.name);
return;
}
// url(...) references
// csstree v2 changed this type
if (node.type === 'Url' && toAny(node.value).length > 0) {
// @ts-ignore
if (node.type === 'Url' && node.value.length > 0) {
const prefixed = prefixReference(
prefix,
unquote(toAny(node.value))
prefixGenerator,
// @ts-ignore
unquote(node.value)
);
if (prefixed != null) {
toAny(node).value = prefixed;
// @ts-ignore
node.value = prefixed;
}
}
});
Expand All @@ -159,7 +211,7 @@ exports.fn = (_root, params, info) => {
node.attributes.id != null &&
node.attributes.id.length !== 0
) {
node.attributes.id = prefixId(prefix, node.attributes.id);
node.attributes.id = prefixId(prefixGenerator, node.attributes.id);
}

// prefix a class attribute value
Expand All @@ -170,7 +222,7 @@ exports.fn = (_root, params, info) => {
) {
node.attributes.class = node.attributes.class
.split(/\s+/)
.map((name) => prefixId(prefix, name))
.map((name) => prefixId(prefixGenerator, name))
.join(' ');
}

Expand All @@ -181,7 +233,10 @@ exports.fn = (_root, params, info) => {
node.attributes[name] != null &&
node.attributes[name].length !== 0
) {
const prefixed = prefixReference(prefix, node.attributes[name]);
const prefixed = prefixReference(
prefixGenerator,
node.attributes[name]
);
if (prefixed != null) {
node.attributes[name] = prefixed;
}
Expand All @@ -197,7 +252,7 @@ exports.fn = (_root, params, info) => {
node.attributes[name] = node.attributes[name].replace(
/url\((.*?)\)/gi,
(match, url) => {
const prefixed = prefixReference(prefix, url);
const prefixed = prefixReference(prefixGenerator, url);
if (prefixed == null) {
return match;
}
Expand All @@ -216,7 +271,7 @@ exports.fn = (_root, params, info) => {
const parts = node.attributes[name].split(/\s*;\s+/).map((val) => {
if (val.endsWith('.end') || val.endsWith('.start')) {
const [id, postfix] = val.split('.');
return `${prefixId(prefix, id)}.${postfix}`;
return `${prefixId(prefixGenerator, id)}.${postfix}`;
}
return val;
});
Expand Down

0 comments on commit bf45a02

Please sign in to comment.