diff --git a/build.ts b/build.ts index 3277c70..43a72db 100644 --- a/build.ts +++ b/build.ts @@ -5,7 +5,7 @@ export {}; console.time('build'); // Minified browser bundle which includes "regular mode" functions, utils, and store. -const out = await Bun.build({ +const out1 = await Bun.build({ entrypoints: ['src/browser.ts'], outdir: 'dist', target: 'browser', @@ -49,4 +49,4 @@ const out5 = await Bun.build({ }); console.timeEnd('build'); -console.log(out, out2, out3, out4, out5); +console.log(out1, out2, out3, out4, out5); diff --git a/src/runtime/macro.ts b/src/runtime/macro.ts index 4e7abdd..a73d946 100644 --- a/src/runtime/macro.ts +++ b/src/runtime/macro.ts @@ -26,105 +26,151 @@ export interface CompileOptions { * @param template - HTML template string. * @param options - Compile options. */ -export async function compile( +export function compile( template: string, { keepComments, keepSpaces }: CompileOptions = {}, - // @ts-expect-error - Bun macros always result in synchronous inlined data. ): { html: string; k: readonly string[]; d: readonly number[] } { - const rewriter = new HTMLRewriter(); const k: string[] = []; const d: number[] = []; let distance = 0; let whitespaceSensitiveBlock = false; let root: boolean | undefined; - rewriter.on('*', { - element(node) { - if (!root) { - if (root === undefined) { - root = true; - node.onEndTag(() => { - root = false; - }); - } else { - // eslint-disable-next-line no-console - console.error( - 'Expected template to have a single root element:', - template, - ); + const html = new HTMLRewriter() + .on('*', { + element(node) { + if (!root) { + if (root === undefined) { + root = true; + node.onEndTag(() => { + root = false; + }); + } else { + // eslint-disable-next-line no-console + console.error( + 'Expected template to have a single root element:', + template, + ); + } } - } - if (node.tagName === 'pre' || node.tagName === 'code') { - whitespaceSensitiveBlock = true; - node.onEndTag(() => { - whitespaceSensitiveBlock = false; - }); - } - for (const [name] of node.attributes) { - if (name[0] === '@') { - k.push(name.slice(1)); - d.push(distance); - distance = 0; - node.removeAttribute(name); - break; + if (node.tagName === 'pre' || node.tagName === 'code') { + whitespaceSensitiveBlock = true; + node.onEndTag(() => { + whitespaceSensitiveBlock = false; + }); } - } - distance++; - }, - // This text handler is invoked twice for each Text node: first with the - // actual text, then with an empty last chunk. This behaviour stems from - // the fact that the Response provided to HTMLRewriter.transform() is not - // streamed; otherwise, there could be multiple chunks before the last one. - text(chunk) { - if (!chunk.lastInTextNode) { - const text = chunk.text.trim(); - if (!text) { - if (!whitespaceSensitiveBlock) { - chunk.remove(); + for (const [name] of node.attributes) { + if (name[0] === '@') { + k.push(name.slice(1)); + d.push(distance); + distance = 0; + node.removeAttribute(name); + break; } - return; - } - if (text[0] === '@') { - k.push(text.slice(1)); - d.push(distance); - distance = 0; - // replace with single space which renders a Text node at runtime - chunk.replace(' ', { html: true }); - } else if (!whitespaceSensitiveBlock) { - // reduce any whitespace to a single space - chunk.replace((keepSpaces ? chunk.text : text).replace(/\s+/g, ' '), { - html: true, - }); } distance++; - } - }, - comments(node) { - if (keepComments) { - // TODO: Add documentation that the build/runtime mode also supports - // using comments as refs. Requires the keepComments option to be true. - const text = node.text.trim(); - if (text[0] === '@') { - k.push(text.slice(1)); - d.push(distance); - distance = 0; - // TODO: Use empty comment once lol-html supports it (less alloc than node.replace) - // node.text = ''; - // TODO: use node.replace() once lol-html fixes it for comments - // node.replace('', { html: true }); + }, + // This text handler is invoked twice for each Text node: first with the + // actual text, then with an empty last chunk. This behaviour stems from + // the fact that the data provided to HTMLRewriter.transform() can be + // streamed; where the last empty chunk signals the end of the text. + text(chunk) { + if (!chunk.lastInTextNode) { + const text = chunk.text.trim(); + if (!text) { + if (!whitespaceSensitiveBlock) { + chunk.remove(); + } + return; + } + if (text[0] === '@') { + k.push(text.slice(1)); + d.push(distance); + distance = 0; + // replace with single space which renders a Text node at runtime + chunk.replace(' ', { html: true }); + } else if (!whitespaceSensitiveBlock) { + // reduce any whitespace to a single space + chunk.replace( + (keepSpaces ? chunk.text : text).replace(/\s+/g, ' '), + { + html: true, + }, + ); + } + distance++; + } + }, + comments(node) { + if (keepComments) { + // TODO: Add documentation that the build/runtime mode also supports + // using comments as refs. Requires the keepComments option to be true. + const text = node.text.trim(); + if (text[0] === '@') { + k.push(text.slice(1)); + d.push(distance); + distance = 0; + // TODO: Use empty comment once lol-html supports it (less alloc than node.replace) + // node.text = ''; + // TODO: use node.replace() once lol-html fixes it for comments + // node.replace('', { html: true }); + node.remove(); + node.after('', { html: true }); + } + distance++; + } else { node.remove(); - node.after('', { html: true }); } - distance++; - } else { - node.remove(); - } - }, - }); - - const res = rewriter.transform(new Response(template.trim())); - const html = await res.text(); + }, + }) + .transform(template.trim()); return { html, k, d }; } + +// FIXME: Decide whether to keep the minifyHTML macro or not. + +// /** +// * Bun macro which minifies whitespace in a HTML string at build-time. +// * @param html - Static HTML code string. +// * @param options - Compile options. +// */ +// export function minifyHTML( +// html: string, +// { keepComments, keepSpaces }: CompileOptions = {}, +// ): string { +// let whitespaceSensitiveBlock = false; +// +// return new HTMLRewriter() +// .on('*', { +// element(node) { +// if (node.tagName === 'pre' || node.tagName === 'code') { +// whitespaceSensitiveBlock = true; +// node.onEndTag(() => { +// whitespaceSensitiveBlock = false; +// }); +// } +// }, +// text(chunk) { +// // see above explanation +// if (!chunk.lastInTextNode) { +// const text = chunk.text.trim(); +// if (!text && !whitespaceSensitiveBlock) { +// chunk.remove(); +// } else if (!whitespaceSensitiveBlock) { +// // reduce any whitespace to a single space +// chunk.replace( +// (keepSpaces ? chunk.text : text).replace(/\s+/g, ' '), +// ); +// } +// } +// }, +// comments(node) { +// if (!keepComments) { +// node.remove(); +// } +// }, +// }) +// .transform(html.trim()); +// }