import path from 'path'; import { createFeature, FeatureContext } from './feature'; import { plugableRecord } from '../helpers/plugable-record'; import { filename2varname, string2varname } from '../helpers/string'; import { stripQuotation } from '../helpers/string'; import valueParser from 'postcss-value-parser'; import { murmurhash3_32_gc } from '../murmurhash'; import { createDiagnosticReporter, Diagnostics } from '../diagnostics'; import type { AtRule } from 'postcss'; export const diagnostics = { INVALID_NAMESPACE_DEF: createDiagnosticReporter( '11007', 'error', () => 'invalid @st-namespace' ), EMPTY_NAMESPACE_DEF: createDiagnosticReporter( '11008', 'error', () => '@st-namespace must contain at least one character or digit' ), EXTRA_DEFINITION: createDiagnosticReporter( '11012', 'error', () => '@st-namespace must contain a single string definition' ), INVALID_NAMESPACE_VALUE: createDiagnosticReporter( '11013', 'error', () => '@st-namespace must contain only letters, numbers or dashes' ), INVALID_NAMESPACE_REFERENCE: createDiagnosticReporter( '11010', 'error', () => 'st-namespace-reference dose not have any value' ), NATIVE_OVERRIDE_DEPRECATION: createDiagnosticReporter( '11014', 'info', () => '@namespace will stop working in version 6, use @st-namespace instead' ), }; const dataKey = plugableRecord.key<{ namespaces: string[]; usedNativeNamespace: string[]; usedNativeNamespaceNodes: AtRule[]; foundStNamespace: boolean; }>('namespace'); // HOOKS export const hooks = createFeature({ metaInit({ meta }) { plugableRecord.set(meta.data, dataKey, { namespaces: [], usedNativeNamespace: [], usedNativeNamespaceNodes: [], foundStNamespace: false, }); }, analyzeAtRule({ context, atRule }) { const isSTNamespace = atRule.name === 'st-namespace'; const isNamespace = atRule.name === 'namespace'; if (!isSTNamespace && !isNamespace) { return; } const data = plugableRecord.getUnsafe(context.meta.data, dataKey); if (data.foundStNamespace && isNamespace) { // ignore @namespace once @st-namespace was found return; } const diag = isSTNamespace ? context.diagnostics : undefined; const match = parseNamespace(atRule, diag); if (match) { data.namespaces.push(match); if (isNamespace) { data.usedNativeNamespace.push(atRule.params); data.usedNativeNamespaceNodes.push(atRule); } else { // clear @namespace matches once @st-namespace if found data.usedNativeNamespace.length = 0; data.usedNativeNamespaceNodes.length = 0; // mark to prevent any further @namespace collection data.foundStNamespace = true; } } }, analyzeDone(context) { const { usedNativeNamespaceNodes } = plugableRecord.getUnsafe(context.meta.data, dataKey); for (const node of usedNativeNamespaceNodes) { context.diagnostics.report(diagnostics.NATIVE_OVERRIDE_DEPRECATION(), { node, }); } }, prepareAST({ context, node, toRemove }) { // remove @st-namespace or @namespace that was used as @st-namespace const { usedNativeNamespace } = plugableRecord.getUnsafe(context.meta.data, dataKey); if ( node.type === 'atrule' && (node.name === 'st-namespace' || (node.name === 'namespace' && usedNativeNamespace.includes(node.params))) ) { toRemove.push(node); } }, }); // API export function parseNamespace(node: AtRule, diag?: Diagnostics): string | undefined { const { nodes } = valueParser(node.params); if (!nodes.length) { // empty params (not even empty quotes) diag?.report(diagnostics.EMPTY_NAMESPACE_DEF(), { node }); return; } let isInvalid = false; let namespace: string | undefined = undefined; for (const valueNode of nodes) { switch (valueNode.type) { case 'string': { if (namespace === undefined) { // first namespace if (!isInvalid) { namespace = stripQuotation(valueNode.value); } } else { // extra definition - mark as invalid and clear namespace diag?.report(diagnostics.EXTRA_DEFINITION(), { node, word: valueParser.stringify(valueNode), }); isInvalid = true; namespace = undefined; } break; } case 'comment': case 'space': // do nothing break; default: { // invalid definition - mark as invalid and clear namespace diag?.report(diagnostics.EXTRA_DEFINITION(), { node, word: valueParser.stringify(valueNode), }); isInvalid = true; namespace = undefined; } } } if (namespace === undefined) { // no namespace found diag?.report(diagnostics.INVALID_NAMESPACE_DEF(), { node, }); return; } if (namespace !== undefined && namespace.trim() === '') { // empty namespace found diag?.report(diagnostics.EMPTY_NAMESPACE_DEF(), { node, }); return; } // check namespace is a valid ident start with no conflicts with stylable namespacing const transformedNamespace = string2varname(namespace); if (namespace !== transformedNamespace) { // invalid namespace found diag?.report(diagnostics.INVALID_NAMESPACE_VALUE(), { node, word: namespace, }); return; } return namespace; } export function defaultProcessNamespace(namespace: string, origin: string, _source?: string) { return namespace + murmurhash3_32_gc(origin); // .toString(36); } export function setMetaNamespace( context: FeatureContext, resolveNamespace: typeof defaultProcessNamespace ): void { const meta = context.meta; // resolve namespace const { namespaces } = plugableRecord.getUnsafe(meta.data, dataKey); const namespace = namespaces[namespaces.length - 1] || filename2varname(path.basename(meta.source)) || 's'; // resolve path origin let pathToSource: string | undefined; let length = meta.sourceAst.nodes.length; while (length--) { const node = meta.sourceAst.nodes[length]; if (node.type === 'comment' && node.text.includes('st-namespace-reference')) { const i = node.text.indexOf('='); if (i === -1) { context.diagnostics.report(diagnostics.INVALID_NAMESPACE_REFERENCE(), { node, }); } else { pathToSource = stripQuotation(node.text.slice(i + 1)); } break; } } // generate final namespace meta.namespace = resolveNamespace( namespace, pathToSource ? path.resolve(path.dirname(meta.source), pathToSource) : meta.source, meta.source ); }