UNPKG

@stylable/core

Version:

CSS for Components

660 lines 27.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getPartNames = exports.getPart = exports.getParts = exports.setPart = exports.createPartSymbol = exports.isStructureMode = exports.hooks = exports.experimentalMsg = exports.diagnostics = void 0; const plugable_record_1 = require("../helpers/plugable-record"); const feature_1 = require("./feature"); const STSymbol = __importStar(require("./st-symbol")); const STCustomSelector = __importStar(require("./st-custom-selector")); const STCustomState = __importStar(require("./st-custom-state")); const CSSClass = __importStar(require("./css-class")); const deprecation_1 = require("../helpers/deprecation"); const postcss_1 = __importDefault(require("postcss")); const css_value_parser_1 = require("@tokey/css-value-parser"); const selector_1 = require("../helpers/selector"); const css_selector_parser_1 = require("@tokey/css-selector-parser"); const diagnostics_1 = require("../diagnostics"); const stylable_utils_1 = require("../stylable-utils"); const css_value_seeker_1 = require("../helpers/css-value-seeker"); exports.diagnostics = { GLOBAL_MAPPING_LIMITATION: (0, diagnostics_1.createDiagnosticReporter)('21000', 'error', () => `Currently class mapping is limited to single global selector: :global(<selector>)`), UNSUPPORTED_TOP_DEF: (0, diagnostics_1.createDiagnosticReporter)('21001', 'error', () => 'top level @st must start with a class'), MISSING_EXTEND: (0, diagnostics_1.createDiagnosticReporter)('21002', 'error', () => `missing required class reference to extend a class (e.g. ":is(.class-name)"`), OVERRIDE_IMPORTED_CLASS: (0, diagnostics_1.createDiagnosticReporter)('21003', 'error', () => `cannot override imported class definition`), STATE_OUT_OF_CONTEXT: (0, diagnostics_1.createDiagnosticReporter)('21004', 'error', () => 'pseudo-state definition must be directly nested in a `@st .class{}` definition'), // 31005 - unused MISSING_MAPPED_SELECTOR: (0, diagnostics_1.createDiagnosticReporter)('21006', 'error', () => `missing mapped selector after "=>"`), MULTI_MAPPED_SELECTOR: (0, diagnostics_1.createDiagnosticReporter)('21007', 'error', () => 'mapped selector accepts only a single selector.\nuse `:is()` or `:where()` to map multiple selectors)'), ELEMENT_OUT_OF_CONTEXT: (0, diagnostics_1.createDiagnosticReporter)('21008', 'error', () => 'pseudo-element definition must be directly nested in a `@st .class{}` definition'), MISSING_MAPPING: (0, diagnostics_1.createDiagnosticReporter)('21009', 'error', () => 'expected selector mapping (e.g. "=> <selector>")'), REDECLARE: (0, diagnostics_1.createDiagnosticReporter)('21010', 'error', (type, src) => `redeclare ${type} definition: "${src}"`), INVALID_ST_DEF: (0, diagnostics_1.createDiagnosticReporter)('21011', 'error', (params) => `invalid @st "${params}" definition`), MAPPING_UNSUPPORTED_NESTING: (0, diagnostics_1.createDiagnosticReporter)('21012', 'error', () => 'mapped selector can only contain `&` as an initial selector'), UNEXPECTED_EXTRA_VALUE: (0, diagnostics_1.createDiagnosticReporter)('21013', 'error', (extraValue) => `found unexpected extra value definition: "${extraValue}"`), CLASS_OUT_OF_CONTEXT: (0, diagnostics_1.createDiagnosticReporter)('21014', 'error', () => 'class definition must be top level'), }; exports.experimentalMsg = '[experimental feature] stylable structure (@st): API might change!'; const dataKey = plugable_record_1.plugableRecord.key('st-structure'); // HOOKS exports.hooks = (0, feature_1.createFeature)({ analyzeInit(context) { const { meta } = context; if (meta.type !== 'stylable') { return; } const stAtRule = meta.sourceAst.nodes.find((node) => isStAtRule(node)); if (stAtRule) { (0, deprecation_1.warnOnce)(exports.experimentalMsg); const metaAnalysis = plugable_record_1.plugableRecord.getUnsafe(context.meta.data, dataKey); metaAnalysis.isStructureMode = true; } else { // set implicit root for legacy mode (root with flat structure) meta.root = 'root'; const rootSymbol = CSSClass.addClass(context, 'root'); rootSymbol[`-st-root`] = true; } }, metaInit({ meta }) { plugable_record_1.plugableRecord.set(meta.data, dataKey, { // default to legacy flat mode isStructureMode: false, analyzedDefs: new WeakMap(), analyzedDefToPartSymbol: new Map(), declaredClasses: new Set(), }); }, analyzeAtRule({ context, atRule, analyzeRule }) { if (!isStAtRule(atRule) || context.meta.type !== 'stylable') { return; } const { analyzedDefToPartSymbol, declaredClasses } = plugable_record_1.plugableRecord.getUnsafe(context.meta.data, dataKey); const analyzed = analyzeStAtRule(atRule, context); if (!analyzed) { // not valid } else if (analyzed.type === 'topLevelClass') { declaredClasses.add(analyzed.name); CSSClass.addClass(context, analyzed.name, atRule); CSSClass.disableDirectivesForClass(context, analyzed.name); // extend class if (analyzed.extendedClass) { const extendedSymbol = CSSClass.get(context.meta, analyzed.extendedClass) || CSSClass.addClass(context, analyzed.extendedClass); CSSClass.extendTypedRule(context, atRule, '.' + analyzed.name, '-st-extends', (0, stylable_utils_1.getAlias)(extendedSymbol) || extendedSymbol); } // class mapping if (analyzed.mappedSelector) { // ToDo: support non global mapping const globalNode = findGlobalPseudo(analyzed); if (globalNode && globalNode.nodes?.length === 1) { const mappedSelectorAst = globalNode.nodes[0]; // analyze mapped selector analyzeRule(postcss_1.default.rule({ selector: (0, css_selector_parser_1.stringifySelectorAst)(mappedSelectorAst), source: atRule.source, }), { isScoped: false, originalNode: atRule, }); // register global mapping to class CSSClass.extendTypedRule(context, atRule, analyzed.name, '-st-global', mappedSelectorAst.nodes); } } } else if (analyzed.type === 'part') { const parentSymbol = getPartParentSymbol(context, analyzed, analyzedDefToPartSymbol); if (!parentSymbol) { // unreachable: assuming analyzing @st definitions dfs - class/part must be defined context.diagnostics.report(exports.diagnostics.ELEMENT_OUT_OF_CONTEXT(), { node: atRule, }); return; } const partName = analyzed.name; // check re-declare if (getPart(parentSymbol, partName)) { const srcWord = '::' + partName; context.diagnostics.report(exports.diagnostics.REDECLARE('pseudo-element', srcWord), { node: atRule, word: srcWord, }); return; } // analyze mapped selector analyzeRule(postcss_1.default.rule({ selector: (0, css_selector_parser_1.stringifySelectorAst)(analyzed.mappedSelector), source: atRule.source, }), { isScoped: true, originalNode: atRule, }); // register part mapping to parent definition const partSymbol = setPart(parentSymbol, getSymbolId(parentSymbol), partName, [ analyzed.mappedSelector, ]); analyzedDefToPartSymbol.set(analyzed, partSymbol); } else if (analyzed.type === 'state') { const parentSymbol = analyzed.parentAnalyze.type === 'topLevelClass' ? CSSClass.get(context.meta, analyzed.parentAnalyze.name) : analyzedDefToPartSymbol.get(analyzed.parentAnalyze); if (!parentSymbol) { // unreachable: assuming analyzing @st definitions dfs - class/part must be defined context.diagnostics.report(exports.diagnostics.STATE_OUT_OF_CONTEXT(), { node: atRule, }); return; } const mappedStates = (parentSymbol['-st-states'] || (parentSymbol['-st-states'] = {})); const stateName = analyzed.name; if (mappedStates[stateName]) { // first state definition wins const srcWord = ':' + stateName; context.diagnostics.report(exports.diagnostics.REDECLARE('pseudo-state', srcWord), { node: atRule, word: srcWord, }); return; } mappedStates[stateName] = analyzed.stateDef; } }, analyzeDone({ meta }) { const { isStructureMode } = plugable_record_1.plugableRecord.getUnsafe(meta.data, dataKey); if (meta.type === 'stylable' && !isStructureMode) { // legacy flat mode: // classes and custom-selectors are registered as .root pseudo-elements const customSelectors = STCustomSelector.getCustomSelectors(meta); const classes = CSSClass.getAll(meta); const rootClass = classes['root']; const rootId = getSymbolId(rootClass); // custom-selector definition precedence over class definition for (const [partName, mapTo] of Object.entries(customSelectors)) { setPart(rootClass, rootId, partName, mapTo); } for (const [className, classSymbol] of Object.entries(classes)) { if (className === 'root' || customSelectors[className]) { continue; } setPart(rootClass, rootId, className, classSymbol); } } }, }); // API function isStAtRule(node) { return node?.type === 'atrule' && node.name === 'st'; } function getPartParentSymbol(context, { parentAnalyze }, analyzedDefToPartSymbol) { return parentAnalyze.type === 'topLevelClass' ? CSSClass.get(context.meta, parentAnalyze.name) : analyzedDefToPartSymbol.get(parentAnalyze); } function isStructureMode(meta) { return plugable_record_1.plugableRecord.getUnsafe(meta.data, dataKey).isStructureMode; } exports.isStructureMode = isStructureMode; function createPartSymbol(input) { const parts = input['-st-parts'] || {}; const states = input['-st-states'] || {}; return { ...input, _kind: 'part', '-st-parts': parts, '-st-states': states }; } exports.createPartSymbol = createPartSymbol; function setPart(symbol, parentId, partName, mapTo) { const partSymbol = createPartSymbol({ name: partName, id: parentId + '::' + partName, mapTo, }); symbol['-st-parts'][partName] = partSymbol; return partSymbol; } exports.setPart = setPart; function getParts(symbol) { return symbol['-st-parts']; } exports.getParts = getParts; function getPart(symbol, name) { return symbol['-st-parts'][name]; } exports.getPart = getPart; function getPartNames(symbol) { return Object.keys(symbol['-st-parts']); } exports.getPartNames = getPartNames; function getSymbolId(symbol) { return symbol._kind === 'class' ? '.' + symbol.name : symbol.id; } function isMatch(result) { return result.match; } function analyzeStAtRule(atRule, context) { // cache const { analyzedDefs } = plugable_record_1.plugableRecord.getUnsafe(context.meta.data, dataKey); if (analyzedDefs.has(atRule)) { return analyzedDefs.get(atRule); } // parse const params = (0, css_value_parser_1.parseCSSValue)(atRuleFullParams(atRule)); const def = params.length === 0 ? undefined : parseClassDefinition(atRule, params) || parsePseudoElementDefinition(context, atRule, params) || parseStateDefinition(context, atRule, params); if (!def) { if (atRule.parent?.type === 'root') { context.diagnostics.report(exports.diagnostics.UNSUPPORTED_TOP_DEF(), { node: atRule, }); } else { context.diagnostics.report(exports.diagnostics.INVALID_ST_DEF(atRule.params), { node: atRule, }); } return; } // validate switch (def.type) { case 'topLevelClass': { if (!validateTopLevelClass({ def, atRule, context })) { return; } break; } case 'part': { if (!validatePart({ def, atRule, context })) { return; } break; } case 'state': { if (!validateState({ def, atRule, context })) { return; } break; } } if (!isMatch(def)) { return; } analyzedDefs.set(atRule, def); return def; } function validateTopLevelClass({ def, atRule, context, }) { const { declaredClasses } = plugable_record_1.plugableRecord.getUnsafe(context.meta.data, dataKey); if (!def.ranges || !def.params) { // should always be provided by parser return false; } if (!def.name) { // ToDo: fix type to have name required return false; } if (def.ranges.extend.length && !def.extendedClass) { context.diagnostics.report(exports.diagnostics.MISSING_EXTEND(), { node: atRule, word: (0, css_value_parser_1.stringifyCSSValue)(def.ranges.extend), }); } if (def.ranges.leftoverValue.find((node) => node.type !== 'comment' && node.type !== 'space')) { const unexpectedValue = (0, css_value_parser_1.stringifyCSSValue)(def.ranges.leftoverValue).trim(); context.diagnostics.report(exports.diagnostics.UNEXPECTED_EXTRA_VALUE(unexpectedValue), { node: atRule, word: unexpectedValue, }); return false; } if (atRule.parent?.type !== 'root') { context.diagnostics.report(exports.diagnostics.CLASS_OUT_OF_CONTEXT(), { node: atRule, }); return false; } const existingSymbol = STSymbol.get(context.meta, def.name); if (existingSymbol?._kind === 'import') { context.diagnostics.report(exports.diagnostics.OVERRIDE_IMPORTED_CLASS(), { node: atRule, }); return false; } if (declaredClasses.has(def.name)) { // ToDo: use st-symbol redeclare api; improve st-symbol/css-class "final" marking and diagnostics const srcWord = '.' + def.name; context.diagnostics.report(exports.diagnostics.REDECLARE('class', srcWord), { node: atRule, word: srcWord, }); return false; } if (def.ranges.mapArrow.length) { if (!def.mappedSelector) { // report missing selector const arrowEnd = def.ranges.mapArrow[def.ranges.mapArrow.length - 1]; for (let i = def.params.length - 1; i >= 0; i--) { const node = def.params[i]; if (node === arrowEnd) { break; } else if ((0, css_value_seeker_1.isExactLiteral)(node, ',')) { context.diagnostics.report(exports.diagnostics.MULTI_MAPPED_SELECTOR(), { node: atRule, }); return false; } } context.diagnostics.report(exports.diagnostics.MISSING_MAPPED_SELECTOR(), { node: atRule, }); return false; } const globalNode = findGlobalPseudo(def, true); if (!globalNode || !globalNode.nodes || globalNode.nodes.length !== 1) { context.diagnostics.report(exports.diagnostics.GLOBAL_MAPPING_LIMITATION(), { node: atRule, word: (0, css_selector_parser_1.stringifySelectorAst)(def.mappedSelector).trim(), }); return false; } } return true; } function validatePart({ def, atRule, context, }) { if (!def.parentAnalyze) { context.diagnostics.report(exports.diagnostics.ELEMENT_OUT_OF_CONTEXT(), { node: atRule, }); return false; } if (!def.mappedSelector) { if (!def.mappedArrow) { context.diagnostics.report(exports.diagnostics.MISSING_MAPPING(), { node: atRule, }); return false; } // report missing selector const arrowEnd = def.ranges.mapArrow[def.ranges.mapArrow.length - 1]; for (let i = def.params.length - 1; i >= 0; i--) { const node = def.params[i]; if (node === arrowEnd) { break; } else if ((0, css_value_seeker_1.isExactLiteral)(node, ',')) { context.diagnostics.report(exports.diagnostics.MULTI_MAPPED_SELECTOR(), { node: atRule, }); return false; } } context.diagnostics.report(exports.diagnostics.MISSING_MAPPED_SELECTOR(), { node: atRule, }); return false; } if (validateNestingInMapping(def.mappedSelector, context, atRule)) { return false; } return true; } function validateState({ def, atRule, context, }) { if (!def.parentAnalyze) { context.diagnostics.report(exports.diagnostics.STATE_OUT_OF_CONTEXT(), { node: atRule, }); return false; } const [amountToActualValue] = (0, css_value_seeker_1.findAnything)(def.ranges.leftoverValue, 0); if (amountToActualValue) { const unexpectedValue = (0, css_value_parser_1.stringifyCSSValue)(def.ranges.leftoverValue).trim(); context.diagnostics.report(exports.diagnostics.UNEXPECTED_EXTRA_VALUE(unexpectedValue), { node: atRule, word: unexpectedValue, }); return false; } return true; } function findGlobalPseudo(def, checkAfter = false) { if (!def.mappedSelector) { return; } let globalNode = undefined; let foundUnexpectedSelector = false; for (const node of def.mappedSelector.nodes) { if (node.type === 'pseudo_class' && node.value === 'global' && !globalNode) { globalNode = node; if (!checkAfter) { break; } } else if (node.type !== 'comment') { foundUnexpectedSelector = true; } } return foundUnexpectedSelector ? undefined : globalNode; } function parseStateDefinition(context, atRule, params) { const result = { type: 'state', params, match: true, ranges: { leftoverValue: [] }, }; let index = 0; const { analyzedDefs } = plugable_record_1.plugableRecord.getUnsafe(context.meta.data, dataKey); // name const [amountToName, nameNode] = (0, css_value_seeker_1.findNextPseudoClassNode)(params, 0); if (nameNode) { result.name = nameNode.value; } else { // not a pseudo-state definition return; } index += amountToName; // parent const parentRule = atRule.parent; const parentAnalyze = parentRule && analyzedDefs.get(parentRule); if (parentAnalyze && (parentAnalyze.type === 'topLevelClass' || parentAnalyze.type === 'part')) { result.parentAnalyze = parentAnalyze; } // state const [amountToStateDef, stateDef] = STCustomState.parseStateValue(params.slice(index - 1), atRule, context.diagnostics); if (stateDef !== undefined) { index += amountToStateDef; result.stateDef = stateDef; } else { result.match = false; } // leftover const amountTaken = index - 1; result.ranges.leftoverValue.push(...params.slice(amountTaken)); const [amountToUnexpected] = (0, css_value_seeker_1.findAnything)(params, index); if (amountToUnexpected) { result.match = false; } return result; } function parsePseudoElementDefinition(context, atRule, params) { const { analyzedDefs } = plugable_record_1.plugableRecord.getUnsafe(context.meta.data, dataKey); const result = { type: 'part', params, match: true, name: '', ranges: { pseudoElement: [], mapArrow: [], mapTo: [], leftoverValue: [] }, }; let index = 0; // collect pseudo element name const [amountToName, nameNode, nameInspectAmount] = (0, css_value_seeker_1.findPseudoElementNode)(params, 0); result.ranges.pseudoElement.push(...params.slice(index, index + nameInspectAmount)); index += amountToName; if (nameNode) { result.name = nameNode.value; } else { // not a pseudo-element definition return false; } // get symbol to extend const parentRule = atRule.parent; const parentAnalyze = parentRule && analyzedDefs.get(parentRule); if (parentAnalyze?.type === 'topLevelClass' || parentAnalyze?.type === 'part') { result.parentAnalyze = parentAnalyze; } // collect mapped selector const [amountToMapping, mappingOpenNode, mapArrowInspectAmount] = (0, css_value_seeker_1.findFatArrow)(params, index, { stopOnFail: false, }); result.ranges.mapArrow.push(...params.slice(index, index + mapArrowInspectAmount)); index += amountToMapping; if (mappingOpenNode) { result.mappedArrow = true; // selector result.ranges.mapTo.push(...params.slice(index)); index = params.length - 1; const selectorStr = atRuleFullParams(atRule).slice(mappingOpenNode.end); const mappedSelectors = (0, selector_1.parseSelectorWithCache)(selectorStr.trim()); const filteredSelector = mappedSelectors.length === 1 && filterCommentsAndSpaces(mappedSelectors[0]); if (filteredSelector && filteredSelector.nodes.length) { result.mappedSelector = filteredSelector; } } else { result.match = false; } // check unexpected extra result.ranges.leftoverValue.push(...params.slice(index + 1)); return result; } function parseClassDefinition(atRule, params) { const result = { type: 'topLevelClass', params, match: true, ranges: { class: [], extend: [], mapArrow: [], mapTo: [], leftoverValue: [] }, name: '', }; let index = 0; // top level class const [amountToClass, classNameNode, classInspectedAmount] = (0, css_value_seeker_1.findNextClassNode)(params, index, { stopOnFail: true, }); result.ranges.class.push(...params.slice(index, index + classInspectedAmount)); index += amountToClass; if (classNameNode) { result.name = classNameNode.value; } else { // not a class definition return false; } // collect extends class const [amountToExtends, extendsNode, extendInspectAmount] = (0, css_value_seeker_1.findNextPseudoClassNode)(params, index, { name: 'is', stopOnFail: true, stopOnMatch: (_node, index, nodes) => { const [amountToFatArrow] = (0, css_value_seeker_1.findFatArrow)(nodes, index, { stopOnFail: true }); return amountToFatArrow > 0; }, }); if (extendsNode) { result.ranges.extend.push(...params.slice(index, index + extendInspectAmount)); index += amountToExtends; if (extendsNode.type === 'call') { const [amountToExtendedClass, nameNode] = (0, css_value_seeker_1.findNextClassNode)(extendsNode.args, 0, { stopOnFail: true, }); if (amountToExtendedClass) { index += amountToExtends; // check leftover nodes const [amountToUnexpectedNode] = (0, css_value_seeker_1.findAnything)(extendsNode.args, amountToExtendedClass); if (!amountToUnexpectedNode) { result.extendedClass = nameNode.value; } } } } // collect mapped selector const [amountToMapping, mappingOpenNode, mapArrowInspectAmount] = (0, css_value_seeker_1.findFatArrow)(params, index, { stopOnFail: false, }); if (mappingOpenNode) { result.ranges.mapArrow.push(...params.slice(index, index + mapArrowInspectAmount)); index += amountToMapping; // selector result.ranges.mapTo.push(...params.slice(index)); index = params.length; const selectorStr = atRuleFullParams(atRule).slice(mappingOpenNode.end); const mappedSelectors = (0, selector_1.parseSelectorWithCache)(selectorStr); const filteredSelector = mappedSelectors.length === 1 && filterCommentsAndSpaces(mappedSelectors[0]); if (filteredSelector && filteredSelector.nodes.length) { result.mappedSelector = filteredSelector; } } // unexpected extra value result.ranges.leftoverValue.push(...params.slice(index)); return result; } function validateNestingInMapping(selector, context, atRule) { // check for unsupported & anywhere except first let invalid = false; let passedActualSelector = false; (0, selector_1.walkSelector)(selector, (node) => { if (passedActualSelector && node.type === 'nesting') { context.diagnostics.report(exports.diagnostics.MAPPING_UNSUPPORTED_NESTING(), { node: atRule, }); invalid = true; return selector_1.walkSelector.stopAll; } else if (node.type !== 'comment' && node.type !== 'selector') { passedActualSelector = true; } return; }); return invalid; } function atRuleFullParams(atRule) { const afterName = atRule.raws.afterName || ''; const between = atRule.raws.between || ''; return afterName + atRule.params + between; } function filterCommentsAndSpaces(selector) { const filteredSelector = { ...selector, after: '', before: '', nodes: selector.nodes.filter((node) => node.type !== 'comment'), }; return filteredSelector; } //# sourceMappingURL=st-structure.js.map