You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
539 lines
15 KiB
539 lines
15 KiB
"use strict"; |
|
|
|
const selectorParser = require("postcss-selector-parser"); |
|
const valueParser = require("postcss-value-parser"); |
|
const { extractICSS } = require("icss-utils"); |
|
|
|
const isSpacing = (node) => node.type === "combinator" && node.value === " "; |
|
|
|
function normalizeNodeArray(nodes) { |
|
const array = []; |
|
|
|
nodes.forEach((x) => { |
|
if (Array.isArray(x)) { |
|
normalizeNodeArray(x).forEach((item) => { |
|
array.push(item); |
|
}); |
|
} else if (x) { |
|
array.push(x); |
|
} |
|
}); |
|
|
|
if (array.length > 0 && isSpacing(array[array.length - 1])) { |
|
array.pop(); |
|
} |
|
return array; |
|
} |
|
|
|
function localizeNode(rule, mode, localAliasMap) { |
|
const transform = (node, context) => { |
|
if (context.ignoreNextSpacing && !isSpacing(node)) { |
|
throw new Error("Missing whitespace after " + context.ignoreNextSpacing); |
|
} |
|
|
|
if (context.enforceNoSpacing && isSpacing(node)) { |
|
throw new Error("Missing whitespace before " + context.enforceNoSpacing); |
|
} |
|
|
|
let newNodes; |
|
|
|
switch (node.type) { |
|
case "root": { |
|
let resultingGlobal; |
|
|
|
context.hasPureGlobals = false; |
|
|
|
newNodes = node.nodes.map((n) => { |
|
const nContext = { |
|
global: context.global, |
|
lastWasSpacing: true, |
|
hasLocals: false, |
|
explicit: false, |
|
}; |
|
|
|
n = transform(n, nContext); |
|
|
|
if (typeof resultingGlobal === "undefined") { |
|
resultingGlobal = nContext.global; |
|
} else if (resultingGlobal !== nContext.global) { |
|
throw new Error( |
|
'Inconsistent rule global/local result in rule "' + |
|
node + |
|
'" (multiple selectors must result in the same mode for the rule)' |
|
); |
|
} |
|
|
|
if (!nContext.hasLocals) { |
|
context.hasPureGlobals = true; |
|
} |
|
|
|
return n; |
|
}); |
|
|
|
context.global = resultingGlobal; |
|
|
|
node.nodes = normalizeNodeArray(newNodes); |
|
break; |
|
} |
|
case "selector": { |
|
newNodes = node.map((childNode) => transform(childNode, context)); |
|
|
|
node = node.clone(); |
|
node.nodes = normalizeNodeArray(newNodes); |
|
break; |
|
} |
|
case "combinator": { |
|
if (isSpacing(node)) { |
|
if (context.ignoreNextSpacing) { |
|
context.ignoreNextSpacing = false; |
|
context.lastWasSpacing = false; |
|
context.enforceNoSpacing = false; |
|
return null; |
|
} |
|
context.lastWasSpacing = true; |
|
return node; |
|
} |
|
break; |
|
} |
|
case "pseudo": { |
|
let childContext; |
|
const isNested = !!node.length; |
|
const isScoped = node.value === ":local" || node.value === ":global"; |
|
const isImportExport = |
|
node.value === ":import" || node.value === ":export"; |
|
|
|
if (isImportExport) { |
|
context.hasLocals = true; |
|
// :local(.foo) |
|
} else if (isNested) { |
|
if (isScoped) { |
|
if (node.nodes.length === 0) { |
|
throw new Error(`${node.value}() can't be empty`); |
|
} |
|
|
|
if (context.inside) { |
|
throw new Error( |
|
`A ${node.value} is not allowed inside of a ${context.inside}(...)` |
|
); |
|
} |
|
|
|
childContext = { |
|
global: node.value === ":global", |
|
inside: node.value, |
|
hasLocals: false, |
|
explicit: true, |
|
}; |
|
|
|
newNodes = node |
|
.map((childNode) => transform(childNode, childContext)) |
|
.reduce((acc, next) => acc.concat(next.nodes), []); |
|
|
|
if (newNodes.length) { |
|
const { before, after } = node.spaces; |
|
|
|
const first = newNodes[0]; |
|
const last = newNodes[newNodes.length - 1]; |
|
|
|
first.spaces = { before, after: first.spaces.after }; |
|
last.spaces = { before: last.spaces.before, after }; |
|
} |
|
|
|
node = newNodes; |
|
|
|
break; |
|
} else { |
|
childContext = { |
|
global: context.global, |
|
inside: context.inside, |
|
lastWasSpacing: true, |
|
hasLocals: false, |
|
explicit: context.explicit, |
|
}; |
|
newNodes = node.map((childNode) => |
|
transform(childNode, childContext) |
|
); |
|
|
|
node = node.clone(); |
|
node.nodes = normalizeNodeArray(newNodes); |
|
|
|
if (childContext.hasLocals) { |
|
context.hasLocals = true; |
|
} |
|
} |
|
break; |
|
|
|
//:local .foo .bar |
|
} else if (isScoped) { |
|
if (context.inside) { |
|
throw new Error( |
|
`A ${node.value} is not allowed inside of a ${context.inside}(...)` |
|
); |
|
} |
|
|
|
const addBackSpacing = !!node.spaces.before; |
|
|
|
context.ignoreNextSpacing = context.lastWasSpacing |
|
? node.value |
|
: false; |
|
|
|
context.enforceNoSpacing = context.lastWasSpacing |
|
? false |
|
: node.value; |
|
|
|
context.global = node.value === ":global"; |
|
context.explicit = true; |
|
|
|
// because this node has spacing that is lost when we remove it |
|
// we make up for it by adding an extra combinator in since adding |
|
// spacing on the parent selector doesn't work |
|
return addBackSpacing |
|
? selectorParser.combinator({ value: " " }) |
|
: null; |
|
} |
|
break; |
|
} |
|
case "id": |
|
case "class": { |
|
if (!node.value) { |
|
throw new Error("Invalid class or id selector syntax"); |
|
} |
|
|
|
if (context.global) { |
|
break; |
|
} |
|
|
|
const isImportedValue = localAliasMap.has(node.value); |
|
const isImportedWithExplicitScope = isImportedValue && context.explicit; |
|
|
|
if (!isImportedValue || isImportedWithExplicitScope) { |
|
const innerNode = node.clone(); |
|
innerNode.spaces = { before: "", after: "" }; |
|
|
|
node = selectorParser.pseudo({ |
|
value: ":local", |
|
nodes: [innerNode], |
|
spaces: node.spaces, |
|
}); |
|
|
|
context.hasLocals = true; |
|
} |
|
|
|
break; |
|
} |
|
} |
|
|
|
context.lastWasSpacing = false; |
|
context.ignoreNextSpacing = false; |
|
context.enforceNoSpacing = false; |
|
|
|
return node; |
|
}; |
|
|
|
const rootContext = { |
|
global: mode === "global", |
|
hasPureGlobals: false, |
|
}; |
|
|
|
rootContext.selector = selectorParser((root) => { |
|
transform(root, rootContext); |
|
}).processSync(rule, { updateSelector: false, lossless: true }); |
|
|
|
return rootContext; |
|
} |
|
|
|
function localizeDeclNode(node, context) { |
|
switch (node.type) { |
|
case "word": |
|
if (context.localizeNextItem) { |
|
if (!context.localAliasMap.has(node.value)) { |
|
node.value = ":local(" + node.value + ")"; |
|
context.localizeNextItem = false; |
|
} |
|
} |
|
break; |
|
|
|
case "function": |
|
if ( |
|
context.options && |
|
context.options.rewriteUrl && |
|
node.value.toLowerCase() === "url" |
|
) { |
|
node.nodes.map((nestedNode) => { |
|
if (nestedNode.type !== "string" && nestedNode.type !== "word") { |
|
return; |
|
} |
|
|
|
let newUrl = context.options.rewriteUrl( |
|
context.global, |
|
nestedNode.value |
|
); |
|
|
|
switch (nestedNode.type) { |
|
case "string": |
|
if (nestedNode.quote === "'") { |
|
newUrl = newUrl.replace(/(\\)/g, "\\$1").replace(/'/g, "\\'"); |
|
} |
|
|
|
if (nestedNode.quote === '"') { |
|
newUrl = newUrl.replace(/(\\)/g, "\\$1").replace(/"/g, '\\"'); |
|
} |
|
|
|
break; |
|
case "word": |
|
newUrl = newUrl.replace(/("|'|\)|\\)/g, "\\$1"); |
|
break; |
|
} |
|
|
|
nestedNode.value = newUrl; |
|
}); |
|
} |
|
break; |
|
} |
|
return node; |
|
} |
|
|
|
function isWordAFunctionArgument(wordNode, functionNode) { |
|
return functionNode |
|
? functionNode.nodes.some( |
|
(functionNodeChild) => |
|
functionNodeChild.sourceIndex === wordNode.sourceIndex |
|
) |
|
: false; |
|
} |
|
|
|
function localizeDeclarationValues(localize, declaration, context) { |
|
const valueNodes = valueParser(declaration.value); |
|
|
|
valueNodes.walk((node, index, nodes) => { |
|
const subContext = { |
|
options: context.options, |
|
global: context.global, |
|
localizeNextItem: localize && !context.global, |
|
localAliasMap: context.localAliasMap, |
|
}; |
|
nodes[index] = localizeDeclNode(node, subContext); |
|
}); |
|
|
|
declaration.value = valueNodes.toString(); |
|
} |
|
|
|
function localizeDeclaration(declaration, context) { |
|
const isAnimation = /animation$/i.test(declaration.prop); |
|
|
|
if (isAnimation) { |
|
const validIdent = /^-?[_a-z][_a-z0-9-]*$/i; |
|
|
|
/* |
|
The spec defines some keywords that you can use to describe properties such as the timing |
|
function. These are still valid animation names, so as long as there is a property that accepts |
|
a keyword, it is given priority. Only when all the properties that can take a keyword are |
|
exhausted can the animation name be set to the keyword. I.e. |
|
|
|
animation: infinite infinite; |
|
|
|
The animation will repeat an infinite number of times from the first argument, and will have an |
|
animation name of infinite from the second. |
|
*/ |
|
const animationKeywords = { |
|
$alternate: 1, |
|
"$alternate-reverse": 1, |
|
$backwards: 1, |
|
$both: 1, |
|
$ease: 1, |
|
"$ease-in": 1, |
|
"$ease-in-out": 1, |
|
"$ease-out": 1, |
|
$forwards: 1, |
|
$infinite: 1, |
|
$linear: 1, |
|
$none: Infinity, // No matter how many times you write none, it will never be an animation name |
|
$normal: 1, |
|
$paused: 1, |
|
$reverse: 1, |
|
$running: 1, |
|
"$step-end": 1, |
|
"$step-start": 1, |
|
$initial: Infinity, |
|
$inherit: Infinity, |
|
$unset: Infinity, |
|
}; |
|
|
|
const didParseAnimationName = false; |
|
let parsedAnimationKeywords = {}; |
|
let stepsFunctionNode = null; |
|
const valueNodes = valueParser(declaration.value).walk((node) => { |
|
/* If div-token appeared (represents as comma ','), a possibility of an animation-keywords should be reflesh. */ |
|
if (node.type === "div") { |
|
parsedAnimationKeywords = {}; |
|
} |
|
if (node.type === "function" && node.value.toLowerCase() === "steps") { |
|
stepsFunctionNode = node; |
|
} |
|
const value = |
|
node.type === "word" && |
|
!isWordAFunctionArgument(node, stepsFunctionNode) |
|
? node.value.toLowerCase() |
|
: null; |
|
|
|
let shouldParseAnimationName = false; |
|
|
|
if (!didParseAnimationName && value && validIdent.test(value)) { |
|
if ("$" + value in animationKeywords) { |
|
parsedAnimationKeywords["$" + value] = |
|
"$" + value in parsedAnimationKeywords |
|
? parsedAnimationKeywords["$" + value] + 1 |
|
: 0; |
|
|
|
shouldParseAnimationName = |
|
parsedAnimationKeywords["$" + value] >= |
|
animationKeywords["$" + value]; |
|
} else { |
|
shouldParseAnimationName = true; |
|
} |
|
} |
|
|
|
const subContext = { |
|
options: context.options, |
|
global: context.global, |
|
localizeNextItem: shouldParseAnimationName && !context.global, |
|
localAliasMap: context.localAliasMap, |
|
}; |
|
return localizeDeclNode(node, subContext); |
|
}); |
|
|
|
declaration.value = valueNodes.toString(); |
|
|
|
return; |
|
} |
|
|
|
const isAnimationName = /animation(-name)?$/i.test(declaration.prop); |
|
|
|
if (isAnimationName) { |
|
return localizeDeclarationValues(true, declaration, context); |
|
} |
|
|
|
const hasUrl = /url\(/i.test(declaration.value); |
|
|
|
if (hasUrl) { |
|
return localizeDeclarationValues(false, declaration, context); |
|
} |
|
} |
|
|
|
module.exports = (options = {}) => { |
|
if ( |
|
options && |
|
options.mode && |
|
options.mode !== "global" && |
|
options.mode !== "local" && |
|
options.mode !== "pure" |
|
) { |
|
throw new Error( |
|
'options.mode must be either "global", "local" or "pure" (default "local")' |
|
); |
|
} |
|
|
|
const pureMode = options && options.mode === "pure"; |
|
const globalMode = options && options.mode === "global"; |
|
|
|
return { |
|
postcssPlugin: "postcss-modules-local-by-default", |
|
prepare() { |
|
const localAliasMap = new Map(); |
|
|
|
return { |
|
Once(root) { |
|
const { icssImports } = extractICSS(root, false); |
|
|
|
Object.keys(icssImports).forEach((key) => { |
|
Object.keys(icssImports[key]).forEach((prop) => { |
|
localAliasMap.set(prop, icssImports[key][prop]); |
|
}); |
|
}); |
|
|
|
root.walkAtRules((atRule) => { |
|
if (/keyframes$/i.test(atRule.name)) { |
|
const globalMatch = /^\s*:global\s*\((.+)\)\s*$/.exec( |
|
atRule.params |
|
); |
|
const localMatch = /^\s*:local\s*\((.+)\)\s*$/.exec( |
|
atRule.params |
|
); |
|
|
|
let globalKeyframes = globalMode; |
|
|
|
if (globalMatch) { |
|
if (pureMode) { |
|
throw atRule.error( |
|
"@keyframes :global(...) is not allowed in pure mode" |
|
); |
|
} |
|
atRule.params = globalMatch[1]; |
|
globalKeyframes = true; |
|
} else if (localMatch) { |
|
atRule.params = localMatch[0]; |
|
globalKeyframes = false; |
|
} else if (!globalMode) { |
|
if (atRule.params && !localAliasMap.has(atRule.params)) { |
|
atRule.params = ":local(" + atRule.params + ")"; |
|
} |
|
} |
|
|
|
atRule.walkDecls((declaration) => { |
|
localizeDeclaration(declaration, { |
|
localAliasMap, |
|
options: options, |
|
global: globalKeyframes, |
|
}); |
|
}); |
|
} else if (atRule.nodes) { |
|
atRule.nodes.forEach((declaration) => { |
|
if (declaration.type === "decl") { |
|
localizeDeclaration(declaration, { |
|
localAliasMap, |
|
options: options, |
|
global: globalMode, |
|
}); |
|
} |
|
}); |
|
} |
|
}); |
|
|
|
root.walkRules((rule) => { |
|
if ( |
|
rule.parent && |
|
rule.parent.type === "atrule" && |
|
/keyframes$/i.test(rule.parent.name) |
|
) { |
|
// ignore keyframe rules |
|
return; |
|
} |
|
|
|
const context = localizeNode(rule, options.mode, localAliasMap); |
|
|
|
context.options = options; |
|
context.localAliasMap = localAliasMap; |
|
|
|
if (pureMode && context.hasPureGlobals) { |
|
throw rule.error( |
|
'Selector "' + |
|
rule.selector + |
|
'" is not pure ' + |
|
"(pure selectors must contain at least one local class or id)" |
|
); |
|
} |
|
|
|
rule.selector = context.selector; |
|
|
|
// Less-syntax mixins parse as rules with no nodes |
|
if (rule.nodes) { |
|
rule.nodes.forEach((declaration) => |
|
localizeDeclaration(declaration, context) |
|
); |
|
} |
|
}); |
|
}, |
|
}; |
|
}, |
|
}; |
|
}; |
|
module.exports.postcss = true;
|
|
|