UNPKG

css-inline-stream

Version:

Inline CSS classes into styles from HTML streams

217 lines (194 loc) 7.7 kB
const htmlparser2 = require('htmlparser2'); const { WritableStream: WritableParserStream } = require('htmlparser2/lib/WritableStream'); const { PassThrough } = require('stream'); const { DomHandler } = htmlparser2; const serialize = require('dom-serializer').default; const cssSelect = require('css-select'); module.exports = function inlineCss(htmlStream) { const outputStream = new PassThrough(); const handler = new DomHandler((error, dom) => { if (error) { console.error('Error parsing HTML:', error); return; } const styleMap = new Map(); // Traverse the DOM and inline CSS /** * * @param {import('domhandler').ChildNode} node */ function inlineStyles(node) { if (node.type === 'style') { // Process style attributes and inline them if (node.attribs && node.attribs.style) { node.attribs.style = node.attribs.style.replace(/;$/, ''); node.attribs.style = node.attribs.style.split(';').map(rule => { const [property, value] = rule.split(':'); return `${property}:${value}`; }).join(';'); } // Process style tags and inline their styles if (node.name === 'style' && node.children.length) { const styleContent = node.children[0]?.data; const newStyleMap = cssRuleparser(styleContent); for (const [k, v] of newStyleMap.entries()) { cssRuleparser.mergeSelectorRule(k, v, styleMap); } // ({styleContent}) // Process the style content using PostCSS or other CSS processing tools // ... // Inline the processed styles into the parent element's style attribute // ... // node.parent.attribs.style = `${node.parent.attribs.style || ''};${processedStyleContent}`; // node.parent.children = node.parent.children.filter(n => n !== node); const siblings = node.parent.children; siblings.splice(siblings.indexOf(node), 1) } node.children.forEach(inlineStyles); } else if (node.type === 'tag') { const nodeBefore = serialize(node); node.children && node.children.forEach(inlineStyles); const transformed = transformClassesToStyles(node, styleMap, node.name === 'body'); const nodeNow = serialize(transformed); } } dom.forEach(node => inlineStyles(node)); // Serialize the modified DOM back to HTML const html = serialize(dom); outputStream.write(html); outputStream.end(); }); const parser = new WritableParserStream(handler) htmlStream.pipe(parser); return outputStream; } const { selectOne } = require('css-select'); const cssRuleparser = require('./css-ruleparser'); const parseStyle = require('./parse-style'); function transformClassesToStyles(node, styleMap) { // Keep track of computed styles for the node // let computedStyles = {}; const elementStyles = new Map(); // Process each selector in order to maintain precedence for (const [selector, styles] of styleMap.entries()) { try { const matchedElements = cssSelect.selectAll(selector, node, { cacheResults: true }); // Parse the CSS text into a style object // const styles = parseStyle(cssText); // Apply styles to matched elements matchedElements.forEach(element => { if (!elementStyles.has(element)) { elementStyles.set(element, {}); } // Merge new styles, overwriting existing ones to maintain precedence Object.assign(elementStyles.get(element), styles); }); } catch (e) { // console.warn("CSS Selection failed:", e); } } const rootStyle = styleMap.get(':root') || {}; // If we found any matching styles, apply them to the node elementStyles.forEach((styles, element) => { // Convert styles object back to string element.$style = replaceVars({ ...rootStyle, ...styles, ...(element.attribs.style ? parseStyle(element.attribs.style) : {}), }); const styleString = stringifyStyle(element.$style); if (!element.children.length ) { let target = element; while (target) { removeInheritedProperties(target); target = target.parent; } } // Set or merge with existing inline styles element.attribs.style = styleString; // Remove class attribute delete element.attribs.class; }); // elementStyles.forEach((styles, element) => { // }) // node.children && node.children.forEach(c => transformClassesToStyles(c, styleMap)) return node; } function replaceVars(o) { const vars = {}; for (const key in o) { if (!key.startsWith('--')) continue; vars[key] = o[key]; } const ret = {}; for (const key in o) { if (key.startsWith('--')) continue; ret[key] = replaceCssVariables(o[key], vars); } return ret; } const INHERITED_PROPS = [ "font-family", "font-size", "font-weight", "font-style", "font-variant", "line-height", "letter-spacing", "word-spacing", "color", "background-color", "text-align", "text-indent", "text-transform", "white-space", "list-style-type", "list-style-position", "list-style-image", "border-collapse", "border-spacing", "caption-side", "empty-cells", "visibility", "cursor" ]; function removeInheritedProperties(node) { if (!node?.attribs?.style || !node.parent) return node; let target = node; const styles = node.$style || parseStyle(node.attribs.style); while(target = target.parent) { const targetStyles = target.$style || parseStyle(node.attribs.style); const targetKeys = Object.keys(targetStyles); const keys = targetKeys.filter(k => INHERITED_PROPS.includes(k) && targetStyles[k]); for (const k of keys) { if (targetStyles[k] === styles[k]) { // console.log("removing:", k, styles[k]); // console.log("target:", target.name, targetStyles); // console.log("node:", node.name, styles); delete styles[k]; } } } node.$style = styles; node.attribs.style = stringifyStyle(node.$style); return node; } function replaceCssVariables(cssString, variables) { // Create a regular expression to match CSS variable references const variableRegex = /var\((--[a-z-A-Z0-9]+)\)/g; let hasVar; // Replace variable references with their values from the `variables` object const ret = cssString.replace(variableRegex, (match, variableName) => { hasVar = true; return variables[variableName] || match; // If the variable isn't found, return the original match }); return ret; } // Helper function to stringify style object function stringifyStyle(styleObj) { return Object.entries(styleObj) .map(([property, value]) => `${property}: ${value}`) .join('; '); } // module.exports = transformClassesToStyles;