UNPKG

@mpxjs/webpack-plugin

Version:

mpx compile core

246 lines (222 loc) 6.73 kB
interface Token { type: string value: string | number } interface ExpressionNode { type: 'NUMBER' value: number } export class ExpressionParser { private tokens: Token[] private formatter: (val: string) => number private functions: { [key: string]: (...args: number[]) => number } private current: number constructor (input: string, formatter: (val: string) => number = val => parseFloat(val), functions: { [key: string]: (...args: number[]) => number } = {}) { this.tokens = this.tokenize(input) this.formatter = formatter this.functions = functions this.current = 0 } tokenize (input: string): Token[] { const tokens: Token[] = [] const regex = /(\d+\.?\d*(?:px|rpx|%|vw|vh)?|[+\-*/(),]|\b[a-zA-Z_][a-zA-Z0-9_]*\b)/g let match: RegExpExecArray | null while ((match = regex.exec(input))) { if (/^\d+\.?\d*(?:px|rpx|%|vw|vh)?$/.test(match[0])) { const lastToken = tokens[tokens.length - 1] const last2Token = tokens[tokens.length - 2] if (lastToken?.type === '-' && (!last2Token || /^[+\-*/(,]$/.test(last2Token?.type))) { tokens.pop() tokens.push({ type: 'NUMBER', value: '-' + match[0] }) } else { tokens.push({ type: 'NUMBER', value: match[0] }) } } else { tokens.push({ type: match[0], value: match[0] }) } } return tokens } parse (): ExpressionNode { return this.expression() } private expression (): ExpressionNode { let node = this.term() while (this.current < this.tokens.length && (this.tokens[this.current].type === '+' || this.tokens[this.current].type === '-')) { const operator = this.tokens[this.current].type this.current++ const right = this.term() node = this.applyOperator(operator, node, right) } return node } private term (): ExpressionNode { let node = this.factor() while (this.current < this.tokens.length && (this.tokens[this.current].type === '*' || this.tokens[this.current].type === '/')) { const operator = this.tokens[this.current].type this.current++ const right = this.factor() node = this.applyOperator(operator, node, right) } return node } private factor (): ExpressionNode { const token = this.tokens[this.current] if (token.type === 'NUMBER') { this.current++ const numericValue = this.formatter(token.value as string) return { type: 'NUMBER', value: numericValue } } else if (token.type === '(') { this.current++ const node = this.expression() if (this.tokens[this.current].type !== ')') { throw new Error('Expected closing parenthesis') } this.current++ return node } else if (this.functions[token.type]) { this.current++ if (this.tokens[this.current].type !== '(') { throw new Error('Expected opening parenthesis after function') } this.current++ const args = this.parseArguments() if (this.tokens[this.current].type !== ')') { throw new Error('Expected closing parenthesis') } this.current++ return this.applyFunction(token.type, args) } throw new Error(`Unexpected token: ${token.type}`) } private parseArguments (): ExpressionNode[] { const args: ExpressionNode[] = [] while (this.current < this.tokens.length && this.tokens[this.current].type !== ')') { args.push(this.expression()) if (this.tokens[this.current].type === ',') { this.current++ } } return args } private applyOperator (operator: string, left: ExpressionNode, right: ExpressionNode): ExpressionNode { const leftVal = left.value const rightVal = right.value let result: number switch (operator) { case '+': result = leftVal + rightVal; break case '-': result = leftVal - rightVal; break case '*': result = leftVal * rightVal; break case '/': result = leftVal / rightVal; break default: throw new Error(`Unknown operator: ${operator}`) } return { type: 'NUMBER', value: result } } private applyFunction (func: string, args: ExpressionNode[]): ExpressionNode { if (args.some(arg => arg.type !== 'NUMBER')) { throw new Error('Function arguments must be numbers') } const numericArgs = args.map(arg => arg.value) if (this.functions[func]) { return { type: 'NUMBER', value: this.functions[func].apply(null, numericArgs) } } else { throw new Error(`Unknown function: ${func}`) } } } interface FuncInfo { start: number end: number args: string[] } export function parseFunc (str: string, funcName: string): FuncInfo[] { const regex = new RegExp(`${funcName}\\(`, 'g') const result: FuncInfo[] = [] let match: RegExpExecArray | null while ((match = regex.exec(str)) !== null) { const start = match.index let i = start + funcName.length + 1 let depth = 1 const args: string[] = [] let arg = '' while (depth && i < str.length) { if (depth === 1 && (str[i] === ',' || str[i] === ')')) { args.push(arg.trim()) arg = '' } else { arg += str[i] } switch (str[i]) { case '(': depth++ break case ')': depth-- break default: // Do nothing } i++ } const end = regex.lastIndex = i result.push({ start, end, args }) } return result } interface Replacement { start: number end: number content: string } export class ReplaceSource { private _source: string private _replacements: Replacement[] constructor (source: string) { this._source = source this._replacements = [] } replace (start: number, end: number, content: string): void { this._replacements.push({ start, end, content }) } source (): string { if (this._replacements.length === 0) { return this._source } let current = this._source let pos = 0 const result: string[] = [] for (const replacement of this._replacements) { const start = Math.floor(replacement.start) const end = Math.floor(replacement.end) + 1 if (pos < start) { const offset = start - pos result.push(current.slice(0, offset)) current = current.slice(offset) pos = start } result.push(replacement.content) if (pos < end) { const offset = end - pos current = current.slice(offset) pos = end } } result.push(current) return result.join('') } }