css-inline-stream
Version:
Inline CSS classes into styles from HTML streams
217 lines (194 loc) • 7.7 kB
JavaScript
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;