no-themed-vars-in-root.mjs 1.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. import stylelint from 'stylelint'
  2. const ruleName = 'overleaf/no-themed-vars-in-root'
  3. const messages = stylelint.utils.ruleMessages(ruleName, {
  4. rejected: (variable, prop) =>
  5. `Unexpected themed variable "${variable}" in the value of "${prop}" inside a ":root" block. ` +
  6. 'The "data-theme" attribute is set on <body>, not <html>, so a themed variable referenced at ' +
  7. '":root" resolves against <html> (always the dark default) and never follows light mode. Declare ' +
  8. 'this custom property on a descendant selector (e.g. the component container) so the themed ' +
  9. 'variable resolves within the themed subtree.',
  10. })
  11. // Matches var(--foo-themed), capturing the variable name.
  12. const THEMED_VAR_RE = /var\(\s*(--[\w-]*-themed)\s*[,)]/g
  13. /** @type {import('stylelint').Rule} */
  14. const rule = primary => {
  15. return (root, result) => {
  16. const validOptions = stylelint.utils.validateOptions(result, ruleName, {
  17. actual: primary,
  18. possible: [true, false],
  19. })
  20. if (!validOptions || !primary) {
  21. return
  22. }
  23. root.walkRules(node => {
  24. const targetsRoot = node.selector
  25. .split(',')
  26. .map(selector => selector.trim())
  27. .some(selector => selector === ':root')
  28. if (!targetsRoot) {
  29. return
  30. }
  31. node.walkDecls(decl => {
  32. for (const [, variable] of decl.value.matchAll(THEMED_VAR_RE)) {
  33. stylelint.utils.report({
  34. result,
  35. ruleName,
  36. node: decl,
  37. word: variable,
  38. message: messages.rejected(variable, decl.prop),
  39. })
  40. }
  41. })
  42. })
  43. }
  44. }
  45. rule.ruleName = ruleName
  46. rule.messages = messages
  47. export default stylelint.createPlugin(ruleName, rule)