260 lines
8.4 KiB
JavaScript
260 lines
8.4 KiB
JavaScript
const usercssMeta = require('usercss-meta');
|
|
const less = require('less');
|
|
const stylus = require('stylus');
|
|
|
|
/**
|
|
* Extracts all global variable and mixin definitions from CSS.
|
|
* @param {string} css - The CSS string.
|
|
* @returns {string} The extracted global definitions.
|
|
*/
|
|
function extractGlobalDefinitions(css) {
|
|
const globalDefinitionRegex = /(@[\w-]+\s*(?:{[^}]*}|;))/g;
|
|
return (css.match(globalDefinitionRegex) || []).join('\n');
|
|
}
|
|
|
|
/**
|
|
* Extracts variable definitions from metadata.
|
|
* @param {object} metadata - Metadata containing variable definitions.
|
|
* @returns {object} A map of variable names to their default values.
|
|
*/
|
|
function extractMetadataVars(metadata) {
|
|
if (!metadata?.vars) return {};
|
|
return Object.fromEntries(
|
|
Object.entries(metadata.vars).map(([key, value]) => [key, value.default || value.value || null])
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Extracts content enclosed within matching braces starting from a given position.
|
|
* @param {string} css - The CSS string.
|
|
* @param {number} startPos - The starting position to search for braces.
|
|
* @returns {object|null} The content and ending position of the matched braces.
|
|
*/
|
|
function extractBracedContent(css, startPos) {
|
|
const braceMatch = matchBraces(css, startPos);
|
|
if (!braceMatch) return null;
|
|
|
|
return {
|
|
content: css.substring(braceMatch.start + 1, braceMatch.end - 1).trim(),
|
|
end: braceMatch.end,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Matches a pair of braces in a string starting from a given position.
|
|
* @param {string} content - The string content.
|
|
* @param {number} start - The starting position to search for braces.
|
|
* @returns {object|null} The start and end positions of the matched braces.
|
|
*/
|
|
function matchBraces(content, start) {
|
|
const openBrace = content.indexOf('{', start);
|
|
if (openBrace === -1) return null;
|
|
|
|
let braceCount = 1, pos = openBrace + 1;
|
|
while (braceCount > 0 && pos < content.length) {
|
|
if (content[pos] === '{') braceCount++;
|
|
if (content[pos] === '}') braceCount--;
|
|
pos++;
|
|
}
|
|
|
|
return braceCount === 0 ? { start: openBrace, end: pos } : null;
|
|
}
|
|
|
|
/**
|
|
* Parses domain rules from the provided CSS string.
|
|
* @param {string} css - The CSS string.
|
|
* @param {number} startPos - The starting position to search for domain rules.
|
|
* @returns {object|null} The domains and the rule start position.
|
|
*/
|
|
function parseDomainRule(css, startPos) {
|
|
const domainRuleRegex = /@-moz-document\s+domain\(\s*'([^']+)'(?:\s*,\s*'([^']+)')*\s*\)/g;
|
|
domainRuleRegex.lastIndex = startPos;
|
|
const match = domainRuleRegex.exec(css);
|
|
if (!match) return null;
|
|
|
|
const domains = match[0]
|
|
.match(/['"][^'"]+['"]/g) // Extract all single- or double-quoted domain values
|
|
.map(domain => domain.replace(/['"]/g, '').trim()); // Remove quotes and trim whitespace
|
|
|
|
return {
|
|
domains,
|
|
ruleStart: match.index + match[0].length - 1,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parses @-moz-document rules and extracts domain-specific CSS.
|
|
* @param {string} css - The CSS string.
|
|
* @returns {object} A map of domains to their associated CSS.
|
|
*/
|
|
function parseMozRules(css) {
|
|
const rules = {};
|
|
let currentPos = 0;
|
|
|
|
// Helper to extract global definitions (CSS outside @-moz-document)
|
|
const globalCode = extractGlobalDefinitions(css);
|
|
|
|
while (currentPos < css.length) {
|
|
// Match @-moz-document syntax and extract domains
|
|
const domainMatch = css.slice(currentPos).match(/@-moz-document\s+(.*?){/s);
|
|
if (!domainMatch) break;
|
|
|
|
const domainsStr = domainMatch[1];
|
|
const ruleStart = currentPos + domainMatch.index + domainMatch[0].length - 1;
|
|
|
|
// Extract the content inside the braces for this @-moz-document rule
|
|
const bracedContent = extractBracedContent(css, ruleStart);
|
|
if (!bracedContent) break;
|
|
|
|
// Parse the domains and add the CSS to each
|
|
const domainList = domainsStr.match(/domain\("([^"]+)"\)/g) || [];
|
|
const domains = domainList.map((d) => d.match(/domain\("([^"]+)"\)/)[1]);
|
|
|
|
for (const domain of domains) {
|
|
rules[domain] = `${globalCode}\n${bracedContent.content}`;
|
|
}
|
|
|
|
currentPos = bracedContent.end;
|
|
}
|
|
|
|
return rules;
|
|
}
|
|
|
|
/**
|
|
* Parses metadata from the provided CSS string.
|
|
* @param {string} css - The CSS string.
|
|
* @returns {object} Parsed metadata.
|
|
*/
|
|
function parseCSS(css) {
|
|
try {
|
|
const normalizedCss = css.replace(/\r\n?/g, '\n');
|
|
const nocommentsCss = normalizedCss.replace(/\/\*[\s\S]*?\*\//g, ''); // Remove comments
|
|
return {
|
|
...usercssMeta.parse(normalizedCss),
|
|
css: nocommentsCss,
|
|
};
|
|
} catch (error) {
|
|
throw new Error(`Failed to parse CSS metadata: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compiles CSS code with a preprocessor if specified in the metadata.
|
|
* @param {string} code - The CSS code.
|
|
* @param {object} metadata - Metadata containing preprocessor information.
|
|
* @param {object} [userVars={}] - User-defined variables to override defaults.
|
|
* @returns {Promise<{compiledCss: string, sites: object}>} The compiled CSS code and domain-specific mapping.
|
|
*/
|
|
async function compileStyle(code, metadata, userVars = {}) {
|
|
try {
|
|
// Extract and merge variables
|
|
const vars = {
|
|
...extractMetadataVars(metadata),
|
|
...userVars
|
|
};
|
|
|
|
// Generate full code with user variables
|
|
const fullCode = [
|
|
'// User variables',
|
|
Object.entries(vars).map(([key, value]) => `@${key}: ${value};`).join('\n'),
|
|
'// Main code',
|
|
code
|
|
].join('\n\n');
|
|
|
|
let compiledCode;
|
|
|
|
switch (metadata?.preprocessor?.toLowerCase()) {
|
|
case 'less':
|
|
compiledCode = await compileLess(fullCode, vars);
|
|
break;
|
|
case 'stylus':
|
|
compiledCode = await compileStylus(fullCode, vars);
|
|
break;
|
|
case 'sass':
|
|
throw Error('SASS preprocessor not supported yet. Skipping compilation.');
|
|
case 'scss':
|
|
throw Error('SCSS preprocessor not supported yet. Skipping compilation.');
|
|
default:
|
|
compiledCode = code; // Return unmodified for plain CSS/unknown preprocessor
|
|
}
|
|
|
|
// Parse domain rules
|
|
const domainRules = parseMozRules(compiledCode);
|
|
|
|
// Compile each domain's CSS if needed
|
|
const compiledRules = {};
|
|
for (const [domain, css] of Object.entries(domainRules)) {
|
|
switch (metadata.preprocessor?.toLowerCase()) {
|
|
case 'less':
|
|
compiledRules[domain] = await compileLess(css, vars);
|
|
break;
|
|
case 'stylus':
|
|
compiledRules[domain] = await compileStylus(css, vars);
|
|
break;
|
|
default:
|
|
compiledRules[domain] = css;
|
|
}
|
|
}
|
|
|
|
// Combine all CSS
|
|
const combinedCss = Object.entries(compiledRules)
|
|
.map(([domain, compiledCss]) => `/* ${domain} */\n${compiledCss}`)
|
|
.join('\n\n');
|
|
|
|
return {
|
|
compiledCss: combinedCss,
|
|
sites: compiledRules, // Map of domains to their CSS
|
|
};
|
|
|
|
} catch (error) {
|
|
//console.error('Style compilation error:', error);
|
|
return {
|
|
error
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compiles LESS code to CSS.
|
|
* @param {string} code - The LESS code.
|
|
* @param {object} vars - User-defined variables.
|
|
* @returns {Promise<string>} The compiled CSS.
|
|
*/
|
|
async function compileLess(code, vars = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
less.render(code, {
|
|
math: 'parens-division',
|
|
javascriptEnabled: true,
|
|
compress: false,
|
|
globalVars: vars
|
|
}, (err, output) => {
|
|
if (err) return reject(err);
|
|
resolve(output.css);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Compiles Stylus code to CSS.
|
|
* @param {string} code - The Stylus code.
|
|
* @returns {Promise<string>} The compiled CSS.
|
|
*/
|
|
async function compileStylus(code, vars = {}) {
|
|
console.log(vars, code);
|
|
return new Promise((resolve, reject) => {
|
|
var stlus = stylus(code);
|
|
stlus.set('compress', false);
|
|
for (const [key, value] of Object.entries(vars)) {
|
|
stlus.define(key, value);
|
|
}
|
|
stlus.render((err, output) => {
|
|
if (err) return reject(err);
|
|
resolve(output);
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
parseCSS,
|
|
compileStyle
|
|
}; |