UNPKG

@hyperse/html-webpack-plugin-loader

Version:

A custom template loader that parses HTML templates for the `html-webpack-plugin` package

410 lines (405 loc) 11.7 kB
import { serialize, parseFragment, parse } from 'parse5'; // src/parser/TemplateParser.ts var parseDocument = (htmlSource) => { const document = parse(htmlSource); const html = document.childNodes.find( (node) => node.nodeName === "html" ); const head = html?.childNodes.find( (node) => node.nodeName === "head" ); const body = html?.childNodes.find( (node) => node.nodeName === "body" ); if (!head || !body) { throw new Error("Invalid HTML template: missing <head> or <body> tags"); } return { document, html, head, body }; }; var upsertFavicon = (head, href, rel = "icon", attributes = {}) => { const existingLinkIndex = head.childNodes.findIndex( (node) => node.nodeName === "link" && node.attrs?.find( (attr) => attr.name === "rel" && attr.value === rel ) ); if (existingLinkIndex > -1) { head.childNodes.splice(existingLinkIndex, 1); } const attributesString = Object.entries(attributes).map(([key, value]) => `${key}="${value}"`).join(" "); const linkNode = parseFragment( `<link rel="${rel}" href="${href}" ${attributesString}>` ).childNodes[0]; head.childNodes.push(linkNode); }; var upsertHeadInlineScripts = (head, scripts) => { scripts.forEach((script) => { const existingScriptIndex = head.childNodes.findIndex( (node) => node.nodeName === "script" && node.attrs?.find( (attr) => attr.name === "id" && attr.value === script.id ) ); if (existingScriptIndex > -1) { head.childNodes.splice(existingScriptIndex, 1); } }); const sortedScripts = [...scripts].sort( (a, b) => (a.order ?? 0) - (b.order ?? 0) ); const scriptTags = sortedScripts.map((script) => { const scriptNode = parseFragment( `<script id="${script.id}">${script.content}</script>` ).childNodes[0]; return scriptNode; }); const beginningScripts = sortedScripts.reduce((acc, script, index) => { if (script.position === "beginning") { acc.push(scriptTags[index]); } return acc; }, []); const endScripts = sortedScripts.reduce( (acc, script, index) => { if (script.position === "end") { acc.push(scriptTags[index]); } return acc; }, [] ); beginningScripts.reverse().forEach((scriptNode) => { head.childNodes.unshift(scriptNode); }); endScripts.forEach((scriptNode) => { head.childNodes.push(scriptNode); }); }; var upsertHeadInlineStyles = (head, styles) => { const sortedStyles = [...styles].sort( (a, b) => (a.order ?? 0) - (b.order ?? 0) ); sortedStyles.forEach((style) => { const existingStyleIndex = head.childNodes.findIndex( (node) => node.nodeName === "style" && node.attrs?.find( (attr) => attr.name === "id" && attr.value === style.id ) ); if (existingStyleIndex > -1) { head.childNodes.splice(existingStyleIndex, 1); } }); const styleTags = sortedStyles.map((style) => { const styleNode = parseFragment( `<style id="${style.id}">${style.content}</style>` ).childNodes[0]; return styleNode; }); const beginningStyles = sortedStyles.reduce((acc, style, index) => { if (style.position === "beginning") { acc.push(styleTags[index]); } return acc; }, []); const endStyles = sortedStyles.reduce( (acc, style, index) => { if (style.position === "end") { acc.push(styleTags[index]); } return acc; }, [] ); beginningStyles.reverse().forEach((styleNode) => { head.childNodes.unshift(styleNode); }); endStyles.forEach((styleNode) => { head.childNodes.push(styleNode); }); }; var upsertHeadMetaTags = (head, tags) => { tags.forEach((tag) => { const parsedTag = parseFragment(tag).childNodes[0]; if (parsedTag.nodeName !== "meta") return; const parsedNameAttr = parsedTag.attrs?.find( (attr) => attr.name === "name" ); if (!parsedNameAttr) return; const existingTagIndex = head.childNodes.findIndex((node) => { if (node.nodeName !== "meta") return false; const nodeElement = node; const nodeNameAttr = nodeElement.attrs?.find( (attr) => attr.name === "name" ); return nodeNameAttr?.value === parsedNameAttr.value; }); if (existingTagIndex > -1) { head.childNodes.splice(existingTagIndex, 1); } }); const parsedTags = tags.map( (tag) => parseFragment(tag).childNodes[0] ); head.childNodes.unshift(...parsedTags); }; var upsertHeadStyles = (head, styles) => { const sortedStyles = [...styles].sort( (a, b) => (a.order ?? 0) - (b.order ?? 0) ); sortedStyles.forEach((style) => { const existingStyleIndex = head.childNodes.findIndex( (node) => node.nodeName === "link" && node.attrs?.find( (attr) => attr.name === "rel" && attr.value === "stylesheet" ) && node.attrs?.find( (attr) => attr.name === "id" && attr.value === style.id ) ); if (existingStyleIndex > -1) { head.childNodes.splice(existingStyleIndex, 1); } }); const styleTags = sortedStyles.map((style) => { const styleNode = parseFragment( `<link rel="stylesheet" href="${style.href}" id="${style.id}"></link>` ).childNodes[0]; return styleNode; }); const beginningStyles = sortedStyles.reduce((acc, style, index) => { if (style.position === "beginning") { acc.push(styleTags[index]); } return acc; }, []); const endStyles = sortedStyles.reduce( (acc, style, index) => { if (style.position === "end") { acc.push(styleTags[index]); } return acc; }, [] ); beginningStyles.reverse().forEach((styleNode) => { head.childNodes.unshift(styleNode); }); endStyles.forEach((styleNode) => { head.childNodes.push(styleNode); }); }; var upsertScripts = (element, scripts) => { const sortedScripts = [...scripts].sort( (a, b) => (a.order ?? 0) - (b.order ?? 0) ); sortedScripts.forEach((script) => { const existingScriptIndex = element.childNodes.findIndex( (node) => node.nodeName === "script" && node.attrs?.find( (attr) => attr.name === "id" && attr.value === script.id ) ); if (existingScriptIndex > -1) { element.childNodes.splice(existingScriptIndex, 1); } }); const scriptTags = sortedScripts.map((script) => { const scriptNode = parseFragment( `<script id="${script.id}" src="${script.src}"></script>` ).childNodes[0]; if (script.type) { scriptNode.attrs?.push({ name: "type", value: script.type }); } if (script.async) { scriptNode.attrs?.push({ name: "async", value: "true" }); } if (script.defer) { scriptNode.attrs?.push({ name: "defer", value: "true" }); } if (script.crossOrigin) { scriptNode.attrs?.push({ name: "crossorigin", value: script.crossOrigin }); } if (script.integrity) { scriptNode.attrs?.push({ name: "integrity", value: script.integrity }); } if (script.nonce) { scriptNode.attrs?.push({ name: "nonce", value: script.nonce }); } return scriptNode; }); const beginningScripts = sortedScripts.reduce((acc, script, index) => { if (script.position === "beginning") { acc.push(scriptTags[index]); } return acc; }, []); const endScripts = sortedScripts.reduce( (acc, script, index) => { if (script.position === "end") { acc.push(scriptTags[index]); } return acc; }, [] ); beginningScripts.reverse().forEach((scriptNode) => { element.childNodes.unshift(scriptNode); }); endScripts.forEach((scriptNode) => { element.childNodes.push(scriptNode); }); }; var upsertTitle = (head, title) => { const titleNode = head.childNodes?.find( (node) => node.nodeName === "title" ); if (titleNode) { titleNode.childNodes = [ { nodeName: "#text", value: title, parentNode: titleNode } ]; } else { const newTitle = parseFragment("<title></title>").childNodes[0]; newTitle.childNodes = [ { nodeName: "#text", value: title, parentNode: newTitle } ]; head.childNodes.unshift(newTitle); } }; // src/parser/TemplateParser.ts var TemplateParser = class { constructor(htmlSource) { const { document, head, body } = parseDocument(htmlSource); this.document = document; this.head = head; this.body = body; } /** * Upsert the title tag * @param title - The title to upsert * @returns The TemplateParser instance */ upsertTitleTag(title) { upsertTitle(this.head, title); return this; } /** * Upsert the favicon tag * @param href - The favicon to upsert * @param rel - The rel attribute of the favicon tag * @param attributes - The attributes of the favicon tag * @returns The TemplateParser instance */ upsertFaviconTag(href, rel = "icon", attributes = {}) { upsertFavicon(this.head, href, rel, attributes); return this; } /** * Upsert the head before html tags * @param tags - The tags to upsert * @returns The TemplateParser instance */ upsertHeadMetaTags(tags) { upsertHeadMetaTags(this.head, tags); return this; } /** * Upsert the head before styles * @param styles - The styles to upsert * @returns The TemplateParser instance */ upsertHeadStyles(styles) { upsertHeadStyles(this.head, styles); return this; } /** * Upsert the head inline styles * @param styles - The styles to upsert * @returns The TemplateParser instance */ upsertHeadInlineStyles(styles) { upsertHeadInlineStyles(this.head, styles); return this; } /** * Upsert the head before scripts * @param scripts - The scripts to upsert * @returns The TemplateParser instance */ upsertHeadScripts(scripts) { upsertScripts(this.head, scripts); return this; } /** * Upsert the inline scripts * @param scripts - The scripts to upsert * @returns The TemplateParser instance */ upsertHeadInlineScripts(scripts) { upsertHeadInlineScripts(this.head, scripts); return this; } /** * Upsert the body after scripts * @param scripts - The scripts to upsert * @returns The TemplateParser instance */ upsertBodyScripts(scripts) { upsertScripts(this.body, scripts); return this; } /** * Serialize the document to html string * @returns The serialized html string */ serialize() { return serialize(this.document); } /** * Parse a fragment of html * @param html - The html fragment to parse * @returns The parsed document fragment */ parseFragment(html) { return parseFragment(html); } }; // src/parser/parseTemplate.ts var parseTemplate = (htmlSource, options = {}) => { const parser = new TemplateParser(htmlSource); if (options.title) { parser.upsertTitleTag(options.title); } if (options.favicon) { parser.upsertFaviconTag( options.favicon.href, options.favicon.rel, options.favicon.attributes ); } if (options.headMetaTags?.length) { parser.upsertHeadMetaTags(options.headMetaTags); } if (options.headStyles?.length) { parser.upsertHeadStyles(options.headStyles); } if (options.headInlineStyles?.length) { parser.upsertHeadInlineStyles(options.headInlineStyles); } if (options.headScripts?.length) { parser.upsertHeadScripts(options.headScripts); } if (options.headInlineScripts?.length) { parser.upsertHeadInlineScripts(options.headInlineScripts); } if (options.bodyScripts?.length) { parser.upsertBodyScripts(options.bodyScripts); } return parser; }; export { TemplateParser, parseTemplate };