prefixIds.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. 'use strict';
  2. const csstree = require('css-tree');
  3. const { referencesProps } = require('./_collections.js');
  4. /**
  5. * @typedef {import('../lib/types').XastElement} XastElement
  6. * @typedef {import('../lib/types').PluginInfo} PluginInfo
  7. */
  8. exports.type = 'visitor';
  9. exports.name = 'prefixIds';
  10. exports.active = false;
  11. exports.description = 'prefix IDs';
  12. /**
  13. * extract basename from path
  14. * @type {(path: string) => string}
  15. */
  16. const getBasename = (path) => {
  17. // extract everything after latest slash or backslash
  18. const matched = path.match(/[/\\]?([^/\\]+)$/);
  19. if (matched) {
  20. return matched[1];
  21. }
  22. return '';
  23. };
  24. /**
  25. * escapes a string for being used as ID
  26. * @type {(string: string) => string}
  27. */
  28. const escapeIdentifierName = (str) => {
  29. return str.replace(/[. ]/g, '_');
  30. };
  31. /**
  32. * @type {(string: string) => string}
  33. */
  34. const unquote = (string) => {
  35. if (
  36. (string.startsWith('"') && string.endsWith('"')) ||
  37. (string.startsWith("'") && string.endsWith("'"))
  38. ) {
  39. return string.slice(1, -1);
  40. }
  41. return string;
  42. };
  43. /**
  44. * prefix an ID
  45. * @type {(prefix: string, name: string) => string}
  46. */
  47. const prefixId = (prefix, value) => {
  48. if (value.startsWith(prefix)) {
  49. return value;
  50. }
  51. return prefix + value;
  52. };
  53. /**
  54. * prefix an #ID
  55. * @type {(prefix: string, name: string) => string | null}
  56. */
  57. const prefixReference = (prefix, value) => {
  58. if (value.startsWith('#')) {
  59. return '#' + prefixId(prefix, value.slice(1));
  60. }
  61. return null;
  62. };
  63. /**
  64. * Prefixes identifiers
  65. *
  66. * @author strarsis <strarsis@gmail.com>
  67. *
  68. * @type {import('../lib/types').Plugin<{
  69. * prefix?: boolean | string | ((node: XastElement, info: PluginInfo) => string),
  70. * delim?: string,
  71. * prefixIds?: boolean,
  72. * prefixClassNames?: boolean,
  73. * }>}
  74. */
  75. exports.fn = (_root, params, info) => {
  76. const { delim = '__', prefixIds = true, prefixClassNames = true } = params;
  77. return {
  78. element: {
  79. enter: (node) => {
  80. /**
  81. * prefix, from file name or option
  82. * @type {string}
  83. */
  84. let prefix = 'prefix' + delim;
  85. if (typeof params.prefix === 'function') {
  86. prefix = params.prefix(node, info) + delim;
  87. } else if (typeof params.prefix === 'string') {
  88. prefix = params.prefix + delim;
  89. } else if (params.prefix === false) {
  90. prefix = '';
  91. } else if (info.path != null && info.path.length > 0) {
  92. prefix = escapeIdentifierName(getBasename(info.path)) + delim;
  93. }
  94. // prefix id/class selectors and url() references in styles
  95. if (node.name === 'style') {
  96. // skip empty <style/> elements
  97. if (node.children.length === 0) {
  98. return;
  99. }
  100. // parse styles
  101. let cssText = '';
  102. if (
  103. node.children[0].type === 'text' ||
  104. node.children[0].type === 'cdata'
  105. ) {
  106. cssText = node.children[0].value;
  107. }
  108. /**
  109. * @type {null | csstree.CssNode}
  110. */
  111. let cssAst = null;
  112. try {
  113. cssAst = csstree.parse(cssText, {
  114. parseValue: true,
  115. parseCustomProperty: false,
  116. });
  117. } catch {
  118. return;
  119. }
  120. csstree.walk(cssAst, (node) => {
  121. // #ID, .class selectors
  122. if (
  123. (prefixIds && node.type === 'IdSelector') ||
  124. (prefixClassNames && node.type === 'ClassSelector')
  125. ) {
  126. node.name = prefixId(prefix, node.name);
  127. return;
  128. }
  129. // url(...) references
  130. if (
  131. node.type === 'Url' &&
  132. node.value.value &&
  133. node.value.value.length > 0
  134. ) {
  135. const prefixed = prefixReference(
  136. prefix,
  137. unquote(node.value.value)
  138. );
  139. if (prefixed != null) {
  140. node.value.value = prefixed;
  141. }
  142. }
  143. });
  144. // update styles
  145. if (
  146. node.children[0].type === 'text' ||
  147. node.children[0].type === 'cdata'
  148. ) {
  149. node.children[0].value = csstree.generate(cssAst);
  150. }
  151. return;
  152. }
  153. // prefix an ID attribute value
  154. if (
  155. prefixIds &&
  156. node.attributes.id != null &&
  157. node.attributes.id.length !== 0
  158. ) {
  159. node.attributes.id = prefixId(prefix, node.attributes.id);
  160. }
  161. // prefix a class attribute value
  162. if (
  163. prefixClassNames &&
  164. node.attributes.class != null &&
  165. node.attributes.class.length !== 0
  166. ) {
  167. node.attributes.class = node.attributes.class
  168. .split(/\s+/)
  169. .map((name) => prefixId(prefix, name))
  170. .join(' ');
  171. }
  172. // prefix a href attribute value
  173. // xlink:href is deprecated, must be still supported
  174. for (const name of ['href', 'xlink:href']) {
  175. if (
  176. node.attributes[name] != null &&
  177. node.attributes[name].length !== 0
  178. ) {
  179. const prefixed = prefixReference(prefix, node.attributes[name]);
  180. if (prefixed != null) {
  181. node.attributes[name] = prefixed;
  182. }
  183. }
  184. }
  185. // prefix an URL attribute value
  186. for (const name of referencesProps) {
  187. if (
  188. node.attributes[name] != null &&
  189. node.attributes[name].length !== 0
  190. ) {
  191. node.attributes[name] = node.attributes[name].replace(
  192. /url\((.*?)\)/gi,
  193. (match, url) => {
  194. const prefixed = prefixReference(prefix, url);
  195. if (prefixed == null) {
  196. return match;
  197. }
  198. return `url(${prefixed})`;
  199. }
  200. );
  201. }
  202. }
  203. // prefix begin/end attribute value
  204. for (const name of ['begin', 'end']) {
  205. if (
  206. node.attributes[name] != null &&
  207. node.attributes[name].length !== 0
  208. ) {
  209. const parts = node.attributes[name].split(/\s*;\s+/).map((val) => {
  210. if (val.endsWith('.end') || val.endsWith('.start')) {
  211. const [id, postfix] = val.split('.');
  212. return `${prefixId(prefix, id)}.${postfix}`;
  213. }
  214. return val;
  215. });
  216. node.attributes[name] = parts.join('; ');
  217. }
  218. }
  219. },
  220. },
  221. };
  222. };