diff --git a/bench/index.js b/bench/index.js index 321f573..6485288 100644 --- a/bench/index.js +++ b/bench/index.js @@ -1,13 +1,11 @@ /* eslint-disable no-unused-vars */ - -import { html } from "../src/index.js"; +import { html } from "ghtml"; import { Bench } from "tinybench"; import { writeFileSync } from "node:fs"; import { Buffer } from "node:buffer"; -let result = ""; - const bench = new Bench({ time: 500 }); +let result = ""; bench.add("simple HTML formatting", () => { result = html`
Hello, world!
`; diff --git a/bin/example/assets/script.js b/bin/example/assets/script.js index cdf6b44..ed212d9 100644 --- a/bin/example/assets/script.js +++ b/bin/example/assets/script.js @@ -1 +1 @@ -globalThis.console.warn("Hello World!"); +globalThis.console.log("Hello World!"); diff --git a/bin/example/routes/index.js b/bin/example/routes/index.js index 041ae89..8a1ebfc 100644 --- a/bin/example/routes/index.js +++ b/bin/example/routes/index.js @@ -1,5 +1,3 @@ -/* eslint-disable n/no-missing-import, require-await */ - import { html } from "ghtml"; export default async (fastify) => { @@ -21,7 +19,7 @@ export default async (fastify) => { !${inner} - `; + `; }); fastify.get("/", async (request, reply) => { diff --git a/bin/example/server.js b/bin/example/server.js index 0f2e03d..aa73469 100644 --- a/bin/example/server.js +++ b/bin/example/server.js @@ -1,5 +1,3 @@ -/* eslint-disable n/no-missing-import */ - import Fastify from "fastify"; const fastify = Fastify(); @@ -17,10 +15,7 @@ await fastify.register(import("@fastify/static"), { // Routes fastify.register(import("./routes/index.js")); -fastify.listen({ port: 5050 }, (err, address) => { - if (err) { - throw err; - } +// Listen +const address = await fastify.listen({ port: 5050 }); - globalThis.console.warn(`Server listening at ${address}`); -}); +globalThis.console.log(`Server listening at ${address}`); diff --git a/bin/src/index.js b/bin/src/index.js index bdebf92..d771402 100755 --- a/bin/src/index.js +++ b/bin/src/index.js @@ -18,7 +18,7 @@ const parseArguments = (args) => { } if (!roots || !refs) { - globalThis.console.error( + globalThis.console.log( 'Usage: npx ghtml --roots="base/path/to/scan/assets/1/,base/path/to/scan/assets/2/" --refs="views/path/to/append/hashes/1/,views/path/to/append/hashes/2/" [--prefix="/optional/prefix/"]', ); process.exit(1); @@ -31,10 +31,10 @@ const main = async () => { const { roots, refs, prefix } = parseArguments(process.argv.slice(2)); try { - globalThis.console.warn(`Generating hashes and updating file paths...`); - globalThis.console.warn(`Scanning files in: ${roots}`); - globalThis.console.warn(`Updating files in: ${refs}`); - globalThis.console.warn(`Using prefix: ${prefix}`); + globalThis.console.log(`Generating hashes and updating file paths...`); + globalThis.console.log(`Scanning files in: ${roots}`); + globalThis.console.log(`Updating files in: ${refs}`); + globalThis.console.log(`Using prefix: ${prefix}`); await generateHashesAndReplace({ roots, @@ -42,11 +42,11 @@ const main = async () => { prefix, }); - globalThis.console.warn( + globalThis.console.log( "Hash generation and file updates completed successfully.", ); } catch (error) { - globalThis.console.error(`Error occurred: ${error.message}`); + globalThis.console.log(`Error occurred: ${error.message}`); process.exit(1); } }; diff --git a/bin/src/utils.js b/bin/src/utils.js index c538e3d..58e8818 100644 --- a/bin/src/utils.js +++ b/bin/src/utils.js @@ -1,5 +1,3 @@ -/* eslint-disable no-await-in-loop */ - import { Glob } from "glob"; import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; @@ -8,11 +6,13 @@ import { win32, posix } from "node:path"; const generateFileHash = async (filePath) => { try { const fileBuffer = await readFile(filePath); + return createHash("md5").update(fileBuffer).digest("hex").slice(0, 16); } catch (err) { if (err.code !== "ENOENT") { throw err; } + return ""; } }; @@ -26,6 +26,7 @@ const updateFilePathsWithHashes = async ( ) => { for (let ref of refs) { ref = ref.replaceAll(win32.sep, posix.sep); + if (!ref.endsWith("/")) { ref += "/"; } @@ -84,11 +85,13 @@ export const generateHashesAndReplace = async ({ skipPatterns = ["**/node_modules/**"], }) => { const fileHashes = new Map(); + roots = Array.isArray(roots) ? roots : [roots]; refs = Array.isArray(refs) ? refs : [refs]; for (let root of roots) { root = root.replaceAll(win32.sep, posix.sep); + if (!root.endsWith("/")) { root += "/"; } @@ -114,6 +117,7 @@ export const generateHashesAndReplace = async ({ for (let i = 0; i < files.length; ++i) { const fileRelativePath = posix.relative(root, files[i]); + fileHashes.set(fileRelativePath, hashes[i]); } } diff --git a/eslint.config.js b/eslint.config.js index 1f1b2d3..f170edf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,3 +1,14 @@ import grules from "grules"; -export default [...grules]; +export default [ + ...grules, + { + rules: { + "no-await-in-loop": "off", + "require-unicode-regexp": "off", + }, + }, + { + ignores: ["bin/example/**/*.js"], + }, +]; diff --git a/package.json b/package.json index 3b19b57..b21ad6d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test": "npm run lint && c8 --100 node --test test/*.js", "lint": "eslint . && prettier --check .", "lint:fix": "eslint --fix . && prettier --write .", - "typescript": "tsc src/*.js --allowJs --declaration --emitDeclarationOnly" + "typescript": "tsc src/*.js --allowJs --declaration --emitDeclarationOnly --skipLibCheck" }, "dependencies": { "glob": "^10.4.5" @@ -27,7 +27,7 @@ "devDependencies": { "@fastify/pre-commit": "^2.1.0", "c8": "^10.1.2", - "grules": "^0.25.0", + "grules": "^0.25.3", "tinybench": "^2.9.0", "typescript": ">=5.6.2" }, diff --git a/src/html.js b/src/html.js deleted file mode 100644 index 003851d..0000000 --- a/src/html.js +++ /dev/null @@ -1,248 +0,0 @@ -/* eslint-disable no-await-in-loop, require-unicode-regexp */ - -const escapeRegExp = /["&'<=>]/g; - -const escapeFunction = (string) => { - let escaped = ""; - let start = 0; - - while (escapeRegExp.test(string)) { - const i = escapeRegExp.lastIndex - 1; - - switch (string.charCodeAt(i)) { - case 34: // " - escaped += string.slice(start, i) + """; - break; - case 38: // & - escaped += string.slice(start, i) + "&"; - break; - case 39: // ' - escaped += string.slice(start, i) + "'"; - break; - case 60: // < - escaped += string.slice(start, i) + "<"; - break; - case 61: // = - escaped += string.slice(start, i) + "="; - break; - case 62: // > - escaped += string.slice(start, i) + ">"; - break; - } - - start = escapeRegExp.lastIndex; - } - - return escaped + string.slice(start); -}; - -/** - * @param {TemplateStringsArray} literals literals - * @param {...any} expressions expressions - * @returns {string} string - */ -export const html = (literals, ...expressions) => { - let accumulator = ""; - - for (let i = 0; i !== expressions.length; ++i) { - let literal = literals.raw[i]; - let string = Array.isArray(expressions[i]) - ? expressions[i].join("") - : String(expressions[i] ?? ""); - - if (literal && literal.charCodeAt(literal.length - 1) === 33) { - literal = literal.slice(0, -1); - } else { - string &&= escapeFunction(string); - } - - accumulator += literal + string; - } - - return accumulator + literals.raw[expressions.length]; -}; - -/** - * @param {TemplateStringsArray} literals literals - * @param {...any} expressions expressions - * @yields {string} string - * @returns {Generator} Generator - */ -export const htmlGenerator = function* (literals, ...expressions) { - for (let i = 0; i !== expressions.length; ++i) { - let expression = expressions[i]; - let literal = literals.raw[i]; - let string; - - if (typeof expression === "string") { - string = expression; - } else if (expression === undefined || expression === null) { - string = ""; - } else { - if (expression[Symbol.iterator]) { - const isRaw = - Boolean(literal) && literal.charCodeAt(literal.length - 1) === 33; - - if (isRaw) { - literal = literal.slice(0, -1); - } - - if (literal) { - yield literal; - } - - for (expression of expression) { - if (typeof expression === "string") { - string = expression; - } else { - if (expression === undefined || expression === null) { - continue; - } - - if (expression[Symbol.iterator]) { - for (expression of expression) { - if (expression === undefined || expression === null) { - continue; - } - - string = String(expression); - - if (string) { - if (!isRaw) { - string = escapeFunction(string); - } - - yield string; - } - } - - continue; - } - - string = String(expression); - } - - if (string) { - if (!isRaw) { - string = escapeFunction(string); - } - - yield string; - } - } - - continue; - } - - string = String(expression); - } - - if (literal && literal.charCodeAt(literal.length - 1) === 33) { - literal = literal.slice(0, -1); - } else { - string &&= escapeFunction(string); - } - - if (literal || string) { - yield literal + string; - } - } - - if (literals.raw[expressions.length]) { - yield literals.raw[expressions.length]; - } -}; - -/** - * @param {TemplateStringsArray} literals literals - * @param {...any} expressions expressions - * @yields {string} string - * @returns {AsyncGenerator} AsyncGenerator - */ -export const htmlAsyncGenerator = async function* (literals, ...expressions) { - for (let i = 0; i !== expressions.length; ++i) { - let expression = await expressions[i]; - let literal = literals.raw[i]; - let string; - - if (typeof expression === "string") { - string = expression; - } else if (expression === undefined || expression === null) { - string = ""; - } else { - if (expression[Symbol.iterator] || expression[Symbol.asyncIterator]) { - const isRaw = - Boolean(literal) && literal.charCodeAt(literal.length - 1) === 33; - - if (isRaw) { - literal = literal.slice(0, -1); - } - - if (literal) { - yield literal; - } - - for await (expression of expression) { - if (typeof expression === "string") { - string = expression; - } else { - if (expression === undefined || expression === null) { - continue; - } - - if ( - expression[Symbol.iterator] || - expression[Symbol.asyncIterator] - ) { - for await (expression of expression) { - if (expression === undefined || expression === null) { - continue; - } - - string = String(expression); - - if (string) { - if (!isRaw) { - string = escapeFunction(string); - } - - yield string; - } - } - - continue; - } - - string = String(expression); - } - - if (string) { - if (!isRaw) { - string = escapeFunction(string); - } - - yield string; - } - } - - continue; - } - - string = String(expression); - } - - if (literal && literal.charCodeAt(literal.length - 1) === 33) { - literal = literal.slice(0, -1); - } else { - string &&= escapeFunction(string); - } - - if (literal || string) { - yield literal + string; - } - } - - if (literals.raw[expressions.length]) { - yield literals.raw[expressions.length]; - } -}; diff --git a/src/index.js b/src/index.js index 81f6727..3bd8a95 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,246 @@ -export { html, htmlGenerator, htmlAsyncGenerator } from "./html.js"; +const escapeRegExp = /["&'<=>]/g; + +const escapeFunction = (string) => { + let escaped = ""; + let start = 0; + + while (escapeRegExp.test(string)) { + const i = escapeRegExp.lastIndex - 1; + + switch (string.charCodeAt(i)) { + case 34: + escaped += string.slice(start, i) + """; // " + break; + case 38: + escaped += string.slice(start, i) + "&"; // & + break; + case 39: + escaped += string.slice(start, i) + "'"; // ' + break; + case 60: + escaped += string.slice(start, i) + "<"; // < + break; + case 61: + escaped += string.slice(start, i) + "="; // = + break; + default: { + escaped += string.slice(start, i) + ">"; // > + } + } + + start = escapeRegExp.lastIndex; + } + + return escaped + string.slice(start); +}; + +/** + * @param {TemplateStringsArray} literals literals + * @param {...any} expressions expressions + * @returns {string} string + */ +export const html = (literals, ...expressions) => { + let accumulator = ""; + + for (let i = 0; i !== expressions.length; ++i) { + let literal = literals.raw[i]; + let string = Array.isArray(expressions[i]) + ? expressions[i].join("") + : String(expressions[i] ?? ""); + + if (literal && literal.charCodeAt(literal.length - 1) === 33) { + literal = literal.slice(0, -1); + } else { + string &&= escapeFunction(string); + } + + accumulator += literal + string; + } + + return accumulator + literals.raw[expressions.length]; +}; + +/** + * @param {TemplateStringsArray} literals literals + * @param {...any} expressions expressions + * @yields {string} string + * @returns {Generator} Generator + */ +export const htmlGenerator = function* (literals, ...expressions) { + for (let i = 0; i !== expressions.length; ++i) { + let expression = expressions[i]; + let literal = literals.raw[i]; + let string; + + if (typeof expression === "string") { + string = expression; + } else if (expression === undefined || expression === null) { + string = ""; + } else { + if (expression[Symbol.iterator]) { + const isRaw = + Boolean(literal) && literal.charCodeAt(literal.length - 1) === 33; + + if (isRaw) { + literal = literal.slice(0, -1); + } + + if (literal) { + yield literal; + } + + for (expression of expression) { + if (typeof expression === "string") { + string = expression; + } else { + if (expression === undefined || expression === null) { + continue; + } + + if (expression[Symbol.iterator]) { + for (expression of expression) { + if (expression === undefined || expression === null) { + continue; + } + + string = String(expression); + + if (string) { + if (!isRaw) { + string = escapeFunction(string); + } + + yield string; + } + } + + continue; + } + + string = String(expression); + } + + if (string) { + if (!isRaw) { + string = escapeFunction(string); + } + + yield string; + } + } + + continue; + } + + string = String(expression); + } + + if (literal && literal.charCodeAt(literal.length - 1) === 33) { + literal = literal.slice(0, -1); + } else { + string &&= escapeFunction(string); + } + + if (literal || string) { + yield literal + string; + } + } + + if (literals.raw[expressions.length]) { + yield literals.raw[expressions.length]; + } +}; + +/** + * @param {TemplateStringsArray} literals literals + * @param {...any} expressions expressions + * @yields {string} string + * @returns {AsyncGenerator} AsyncGenerator + */ +export const htmlAsyncGenerator = async function* (literals, ...expressions) { + for (let i = 0; i !== expressions.length; ++i) { + let expression = await expressions[i]; + let literal = literals.raw[i]; + let string; + + if (typeof expression === "string") { + string = expression; + } else if (expression === undefined || expression === null) { + string = ""; + } else { + if (expression[Symbol.iterator] || expression[Symbol.asyncIterator]) { + const isRaw = + Boolean(literal) && literal.charCodeAt(literal.length - 1) === 33; + + if (isRaw) { + literal = literal.slice(0, -1); + } + + if (literal) { + yield literal; + } + + for await (expression of expression) { + if (typeof expression === "string") { + string = expression; + } else { + if (expression === undefined || expression === null) { + continue; + } + + if ( + expression[Symbol.iterator] || + expression[Symbol.asyncIterator] + ) { + for await (expression of expression) { + if (expression === undefined || expression === null) { + continue; + } + + string = String(expression); + + if (string) { + if (!isRaw) { + string = escapeFunction(string); + } + + yield string; + } + } + + continue; + } + + string = String(expression); + } + + if (string) { + if (!isRaw) { + string = escapeFunction(string); + } + + yield string; + } + } + + continue; + } + + string = String(expression); + } + + if (literal && literal.charCodeAt(literal.length - 1) === 33) { + literal = literal.slice(0, -1); + } else { + string &&= escapeFunction(string); + } + + if (literal || string) { + yield literal + string; + } + } + + if (literals.raw[expressions.length]) { + yield literals.raw[expressions.length]; + } +};