/* * vcss.js * Copyright (C) 2025 veypi * * Distributed under terms of the GPL license. * simple css parser */ class CSSParser { constructor() { this.scopeAttribute = ''; this.scopeBody = '' this.scopedKeyframes = new Map(); // 存储已处理的keyframe映射 } /** * 解析CSS文本并添加作用域 * @param {string} cssText - CSS文本 * @param {string} scope - 作用域标识符 * @returns {string} - 处理后的CSS文本 */ parse(cssText, scope) { this.scopeAttribute = `[vrefof="${scope}"]`; this.scopeBody = `[vref="${scope}"]` this.scopedKeyframes.clear(); this.scopeSuffix = scope.replace(/[^a-zA-Z0-9]/g, ''); // 清理作用域后缀 // 移除注释 cssText = this.removeComments(cssText); // 第一遍:收集所有keyframes名称 this.collectKeyframes(cssText); // 第二遍:解析CSS规则并替换动画名称 return this.parseRules(cssText); } /** * 收集所有keyframes名称 * @param {string} cssText */ collectKeyframes(cssText) { const keyframeRegex = /@keyframes\s+([^\s{]+)/gi; let match; while ((match = keyframeRegex.exec(cssText)) !== null) { const originalName = match[1]; const scopedName = originalName + '-' + this.scopeSuffix; this.scopedKeyframes.set(originalName, scopedName); } } /** * 移除CSS注释 * @param {string} cssText * @returns {string} */ removeComments(cssText) { return cssText.replace(/\/\*[\s\S]*?\*\//g, ''); } /** * 解析CSS规则 * @param {string} cssText * @returns {string} */ parseRules(cssText) { let result = ''; let i = 0; while (i < cssText.length) { // 跳过空白字符 while (i < cssText.length && /\s/.test(cssText[i])) { result += cssText[i]; i++; } if (i >= cssText.length) break; // 检查是否是@规则 if (cssText[i] === '@') { const atRule = this.parseAtRule(cssText, i); result += atRule.content; i = atRule.endIndex; } else { // 普通CSS规则 const rule = this.parseNormalRule(cssText, i); result += rule.content; i = rule.endIndex; } } return result; } /** * 解析@规则(如@media, @keyframes等) * @param {string} cssText * @param {number} startIndex * @returns {object} */ parseAtRule(cssText, startIndex) { let i = startIndex; let atRuleContent = ''; // 读取@规则名称和参数 while (i < cssText.length && cssText[i] !== '{') { atRuleContent += cssText[i]; i++; } if (i >= cssText.length) { return { content: atRuleContent, endIndex: i }; } // 检查是否是@keyframes或@media const atRuleName = atRuleContent.toLowerCase().trim(); if (atRuleName.startsWith('@keyframes')) { return this.parseKeyframes(cssText, startIndex); } else if (atRuleName.startsWith('@media')) { return this.parseMedia(cssText, startIndex); } else if (atRuleName.startsWith('@supports')) { return this.parseSupports(cssText, startIndex); } else { // 其他@规则,查找匹配的大括号 const braceContent = this.findMatchingBrace(cssText, i); return { content: atRuleContent + braceContent.content, endIndex: braceContent.endIndex }; } } /** * 解析@keyframes规则 * @param {string} cssText * @param {number} startIndex * @returns {object} */ parseKeyframes(cssText, startIndex) { let i = startIndex; let result = ''; // 读取@keyframes声明 while (i < cssText.length && cssText[i] !== '{') { result += cssText[i]; i++; } if (i >= cssText.length) { return { content: result, endIndex: i }; } // 处理keyframes内容 const braceContent = this.findMatchingBrace(cssText, i); const keyframesContent = braceContent.content; // 为keyframes名称添加作用域 const keyframeName = this.extractKeyframeName(result); const scopedKeyframeName = this.scopedKeyframes.get(keyframeName); if (scopedKeyframeName) { result = result.replace(keyframeName, scopedKeyframeName); } result += keyframesContent; return { content: result, endIndex: braceContent.endIndex }; } /** * 提取keyframe名称 * @param {string} keyframeDeclaration * @returns {string} */ extractKeyframeName(keyframeDeclaration) { const match = keyframeDeclaration.match(/@keyframes\s+([^\s{]+)/i); return match ? match[1] : ''; } /** * 解析@media规则 * @param {string} cssText * @param {number} startIndex * @returns {object} */ parseMedia(cssText, startIndex) { let i = startIndex; let result = ''; // 读取@media声明 while (i < cssText.length && cssText[i] !== '{') { result += cssText[i]; i++; } if (i >= cssText.length) { return { content: result, endIndex: i }; } result += '{'; i++; // 跳过开始的 '{' // 解析@media内部的CSS规则 let braceLevel = 1; let innerCss = ''; while (i < cssText.length && braceLevel > 0) { if (cssText[i] === '{') { braceLevel++; } else if (cssText[i] === '}') { braceLevel--; if (braceLevel === 0) { break; } } innerCss += cssText[i]; i++; } // 递归处理@media内部的CSS规则 const processedInnerCss = this.parseRules(innerCss); result += processedInnerCss; if (i < cssText.length && cssText[i] === '}') { result += '}'; i++; } return { content: result, endIndex: i }; } /** * 解析@supports规则 * @param {string} cssText * @param {number} startIndex * @returns {object} */ parseSupports(cssText, startIndex) { return this.parseMedia(cssText, startIndex); } /** * 解析普通CSS规则 * @param {string} cssText * @param {number} startIndex * @returns {object} */ parseNormalRule(cssText, startIndex) { let i = startIndex; let selector = ''; // 读取选择器 while (i < cssText.length && cssText[i] !== '{') { selector += cssText[i]; i++; } if (i >= cssText.length) { return { content: selector, endIndex: i }; } // 处理选择器添加作用域 const scopedSelector = this.addScopeToSelector(selector.trim()); // 读取CSS规则体并处理动画名称 const braceContent = this.findMatchingBrace(cssText, i); const processedContent = this.processRuleContent(braceContent.content); return { content: scopedSelector + processedContent, endIndex: braceContent.endIndex }; } /** * 处理CSS规则内容,替换动画名称 * @param {string} content * @returns {string} */ processRuleContent(content) { let processedContent = content; // 处理 animation 属性 processedContent = processedContent.replace( /animation\s*:\s*([^;]+);/gi, (match, animationValue) => { const processedValue = this.processAnimationValue(animationValue); return `animation: ${processedValue};`; } ); // 处理 animation-name 属性 processedContent = processedContent.replace( /animation-name\s*:\s*([^;]+);/gi, (match, animationNames) => { const processedNames = this.processAnimationNames(animationNames); return `animation-name: ${processedNames};`; } ); return processedContent; } /** * 处理animation属性值 * @param {string} animationValue * @returns {string} */ processAnimationValue(animationValue) { // animation 可能包含多个动画,用逗号分隔 const animations = animationValue.split(',').map(anim => anim.trim()); return animations.map(animation => { const parts = animation.split(/\s+/); // 第一个非时间、非数字、非关键字的值通常是动画名称 for (let i = 0; i < parts.length; i++) { const part = parts[i]; // 跳过时间值 (如 0.3s, 200ms) if (/^\d+(\.\d+)?(s|ms)$/.test(part)) continue; // 跳过数字值 (如 iteration count) if (/^\d+(\.\d+)?$/.test(part)) continue; // 跳过CSS关键字 if (['ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear', 'infinite', 'normal', 'reverse', 'alternate', 'alternate-reverse', 'forwards', 'backwards', 'both', 'running', 'paused'].includes(part)) continue; // 跳过贝塞尔曲线 if (part.startsWith('cubic-bezier(')) continue; // 这应该是动画名称 const scopedName = this.scopedKeyframes.get(part); if (scopedName) { parts[i] = scopedName; } break; } return parts.join(' '); }).join(', '); } /** * 处理animation-name属性值 * @param {string} animationNames * @returns {string} */ processAnimationNames(animationNames) { return animationNames.split(',').map(name => { const trimmedName = name.trim(); const scopedName = this.scopedKeyframes.get(trimmedName); return scopedName || trimmedName; }).join(', '); } /** * 为选择器添加作用域 * @param {string} selector * @returns {string} */ addScopeToSelector(selector) { if (!selector.trim()) return selector; // 分割多个选择器(用逗号分隔) const selectors = selector.split(',').map(sel => sel.trim()); const scopedSelectors = selectors.map(sel => { return this.addScopeToSingleSelector(sel); }); return scopedSelectors.join(', '); } /** * 为单个选择器添加作用域 * @param {string} selector * @returns {string} */ addScopeToSingleSelector(selector) { if (!selector.trim()) return selector; // 处理伪元素选择器 - 在伪元素前添加作用域 if (selector.includes('::')) { const parts = selector.split('::'); const mainPart = parts[0]; const pseudoElement = '::' + parts.slice(1).join('::'); // 为主要部分添加作用域 const scopedMain = this.addScopeToSelectorPart(mainPart); return scopedMain + pseudoElement; } // 处理伪类选择器 - 在伪类前添加作用域 if (selector.includes(':') && !selector.includes('::')) { const pseudoMatch = selector.match(/^([^:]+)(:.+)$/); if (pseudoMatch) { const mainPart = pseudoMatch[1]; const pseudoClass = pseudoMatch[2]; // 为主要部分添加作用域 const scopedMain = this.addScopeToSelectorPart(mainPart); return scopedMain + pseudoClass; } } // 处理特殊选择器 if (selector === '*' || selector.startsWith('@')) { return selector; } // 处理复合选择器 return this.addScopeToSelectorPart(selector); } /** * 为选择器部分添加作用域 * @param {string} selectorPart * @returns {string} */ addScopeToSelectorPart(selectorPart) { // 处理组合器选择器(>、+、~、空格) const combinatorRegex = /(\s*[>+~]\s*|\s+)/; if (combinatorRegex.test(selectorPart)) { // 分割选择器,但保留组合器 const parts = selectorPart.split(combinatorRegex); if (/^body(?:$|[:\[ ])/.test(parts[0])) { parts[0] = this.scopeBody + parts[0].slice(4) return parts.join(''); } if (/^:root(?:$|[:\[ ])/.test(parts[0])) { parts[0] = this.scopeBody + parts[0].slice(5) return parts.join(''); } // 只在最后一个选择器部分添加作用域 for (let i = parts.length - 1; i >= 0; i--) { if (parts[i].trim() && !combinatorRegex.test(parts[i])) { let tag = parts[i].trim() if (/^body(?:$|[:\[ ])/.test(tag)) { parts[i] = this.scopeBody + tag.slice(4) } else if (/^:root(?:$:[$:\[ ])/.test(tag)) { parts[i] = this.scopeBody + tag.slice(5) } else { parts[i] = parts[i].trim() + this.scopeAttribute; } break; } } return parts.join(''); } selectorPart = selectorPart.trim() let tag = selectorPart.trim() if (/^body(?:$|[:\[ ])/.test(tag)) { return this.scopeBody + tag.slice(4) } else if (/^:root(?:$:[$:\[ ])/.test(tag)) { return this.scopeBody + tag.slice(5) } else { return tag + this.scopeAttribute; } } /** * 查找匹配的大括号 * @param {string} cssText * @param {number} startIndex * @returns {object} */ findMatchingBrace(cssText, startIndex) { let i = startIndex; let content = ''; let braceLevel = 0; while (i < cssText.length) { content += cssText[i]; if (cssText[i] === '{') { braceLevel++; } else if (cssText[i] === '}') { braceLevel--; if (braceLevel === 0) { i++; break; } } i++; } return { content: content, endIndex: i }; } } // 使用示例 const parser = new CSSParser(); // 示例CSS const cssText = ` body{} .container::before, .a { content: ""; animation-name: fadeIn, slideUp; animation: slideIn 0.5s infinite alternate; } @keyframes slideIn { from { transform: translateX(-100%); } to { transform: translateX(0); } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } } @media (max-width: 768px) { .container { font-size: 14px; animation: slideIn 0.2s ease-out; } .item { margin: 5px; } } .box > .child { color: green; } .box + .sibling { margin-top: 20px; } `; // 解析并添加作用域 // console.log(parser.parse(cssText, 'comasd-123')); // 导出类 export default parser; export { CSSParser }