UNPKG

use-theme-editor

Version:

Zero configuration CSS variables based theme editor

193 lines (162 loc) 6.21 kB
// eslint-disable-next-line no-undef import {collectRuleVars, definedValues} from './collectRuleVars'; // For now only extract from the same domain. export const isSameDomain = ({ href }) => !href || href.indexOf(window.location.origin) === 0; // For now hard coded exclude of WP core files. Could be made configurable. export const isNotCoreFile = ({ href }) => !href || !href.includes('wp-includes'); const sourceMapConsumers = {}; const warmupConsumers = sheets => sheets.map(sheet => getConsumer(sheet)); const failedConsumers = []; const getConsumer = async sheetUrl => { // console.log(sheetUrl) if (failedConsumers.includes(sheetUrl)) { return; } if (!sourceMapConsumers[sheetUrl]) { const mapUrl = sheetUrl.split('?')[0] + '.map'; try { let data = await (await fetch(mapUrl)).json(); sourceMapConsumers[sheetUrl] = await new window.sourceMap.SourceMapConsumer(data); } catch (e) { // console.log(e); failedConsumers.push(sheetUrl); return; } } return sourceMapConsumers[sheetUrl]; }; const getLineVarPositions = (varName, sourceMapConsumer, sheet) => (positions, line) => { const {text, index} = line; if (/^\s*--/.test(text) ) { // Skip custom properties, to preserve same logic and ordering as extractPageVariables. return positions; } const occurrenceStart = `var(${varName}`; const [pre, ...matches] = text.split(occurrenceStart); let column = pre.length; for (const match of matches) { const line = index + 1; if (!/^[\w-]/.test(match)) { return [ ...positions, { ...sourceMapConsumer.originalPositionFor({line, column}), generated: { line, column, sheet, }, } ]; } column += occurrenceStart.length + match.length; } return positions; }; const sheetLines = {}; const getSheetLines = async sheetUrl => { if (!(sheetUrl in sheetLines)) { const sheetText = await (await fetch(sheetUrl)).text(); sheetLines[sheetUrl] = sheetText.split('\n').reduce((linesWithVar, line, index) => { if (line.includes('var(')) { linesWithVar.push({text: line, index}); } return linesWithVar; }, []); } return sheetLines[sheetUrl]; }; const warmupLines = sheets => sheets.map(sheet=>getSheetLines(sheet)); const getVarPositions = (sheet, varName, sourceMapConsumer) => { if (!sheet) { return []; } const lines = sheetLines[sheet]; if (!lines) { // Shouldn't happen but just in case. console.warn('no lines', sheet, Object.keys(sheetLines)); return []; } return lines.reduce(getLineVarPositions(varName, sourceMapConsumer, sheet), []); }; const collectSheetVars = async (vars, sheet) => { let rules; if (isSameDomain(sheet)) { rules = sheet.cssRules; } else { try { const sheetContent = await (await fetch(sheet.href)).text(); const style = document.createElement('style'); style.innerText = sheetContent; document.head.appendChild(style); rules = document.styleSheets[document.styleSheets.length - 1].cssRules; document.head.removeChild(style); } catch (e) { return await vars; } } return [...rules].reduce((sheetVars, rule) => collectRuleVars(sheetVars, rule, sheet), await vars); }; const isUsed = (vars,sheet) => { return vars.some(({usages}) => usages.some(usage => usage.sheet === sheet.href)); }; const warmup = async (vars, sheets) => { const filteredSheets = sheets.filter(s => s.href && isUsed(vars, s) && isSameDomain(s)); // console.log(filteredSheets) const promises = filteredSheets.map(sheet => { return [ ...warmupConsumers([sheet.href]), ...warmupLines([sheet.href]) ]; }); // return; if (promises.length === 0) { return await Promise.allSettled([]); } const joined = promises.reduce((a,p) => [...a, ...p] ,[]); return await Promise.allSettled(joined); }; let activeScopes = []; export let rootScopes = []; export const sourceReferences = {} export const extractPageVariables = async() => { const startTime = performance.now(); const sheets = [...document.styleSheets]; const asObject = await sheets.reduce(collectSheetVars, {}); activeScopes = Object.keys(definedValues).filter(scopeSelector => document.querySelectorAll(scopeSelector).length > 0); // We can only know for sure on active scopes whether they only apply to the root element. // Should catch most cases though. rootScopes = activeScopes.filter(scopeSelector => document.querySelector(scopeSelector) === document.documentElement); const allVars = Object.entries(asObject).map(([k, v]) => ({ name: k, sourceReferences: Object.keys(asObject).filter( otherV => asObject[otherV].usages.some(u => u.fullValue.includes(`var(${k},`) || u.fullValue.includes(`var${k})`)) ), ...v, })); // console.log(allVars.filter(v => v.sourceReferences.length > 0)); await warmup(allVars, sheets); const promises = allVars.map((cssVar) => { const {name, usages} = cssVar; const sheetsUsingIt = [...new Set(usages.filter(u => u.sheet).map(u => u.sheet))]; const positions = sheetsUsingIt.reduce((positions, sheet) => { const sourceMapConsumer = sourceMapConsumers[sheet]; if (!sourceMapConsumer) { return positions; } const newPositions = getVarPositions(sheet, name, sourceMapConsumer); return [...positions, ...newPositions]; }, []); return { ...cssVar, usages: cssVar.usages.map((u, i) => ({...u, position: positions[i]})), positions, }; }); const results = await Promise.allSettled( promises ); Object.values(sourceMapConsumers).forEach(consumer => consumer && consumer.destroy()); const duration = performance.now() - startTime; console.info(`Extracted data in ${duration}ms`); return await results.filter(wasFulfilled).map(result => result.value); }; const wasFulfilled = result => 'fulfilled' === result.status;