@kitajs/html
Advanced tools
Comparing version 1.4.7 to 2.0.0
@@ -108,15 +108,58 @@ /// <reference path="./jsx.d.ts" /> | ||
/** | ||
* Compiles html with the given arguments specified with $name syntax. | ||
* Compiles a **clean component** into a super fast component. This does not | ||
* support unclean components / props processing. | ||
* | ||
* @param {string} html | ||
* @returns {function} the compiled function which | ||
* A **clean component** is a component that does not process props before | ||
* applying them to the element. This means that the props are applied to the | ||
* element as is, and you need to process them before passing them to the | ||
* component. | ||
* | ||
* @example | ||
* ```tsx | ||
* // Clean component, render as is | ||
* function Clean(props: CleanProps<{ repeated: string }>) { | ||
* return <div>{props.repeated}</div> | ||
* } | ||
* | ||
* // Calculation is done before passing to the component | ||
* html = <Clean name={'a'.repeat(5)} /> | ||
* | ||
* // Unclean component, process before render | ||
* function Unclean(props: { repeat: string; n: number }) { | ||
* return <div>{props.repeat.repeat(props.n)}</div> | ||
* } | ||
* | ||
* // Calculation is done inside the component, thus cannot be used with .compile() | ||
* html = <Unclean repeat="a" n={5} /> | ||
* ``` | ||
* | ||
* @param {(proxy: any) => string} htmlFn | ||
* @param {boolean} [strict=true] if we should throw an error when a property is not found. | ||
* @param {string | undefined} [separator] the string used to interpolate and separate parameters | ||
* @returns {function} the compiled template function | ||
* @this {void} | ||
*/ | ||
export function compile<A extends string[] = []>( | ||
export function compile< | ||
P extends | ||
| Record<string, any> | ||
| string[] | ||
| ((args: Record<string, string>) => JSX.Element) = {} | ||
>( | ||
this: void, | ||
html: string | ||
): (args: Record<A[number], number | string | boolean>) => JSX.Element | ||
factory: ( | ||
args: Record< | ||
P extends string[] | ||
? P[number] | ||
: P extends (args: infer U) => JSX.Element | ||
? keyof U | ||
: keyof P, | ||
string | ||
> | ||
) => JSX.Element, | ||
strict?: boolean, | ||
separator?: string | ||
): typeof factory | ||
/** | ||
* Here for interop with preact and many build systems. | ||
* Here for interop with `preact` and many build systems. | ||
*/ | ||
@@ -123,0 +166,0 @@ export const h: typeof createElement |
84
index.js
@@ -106,2 +106,3 @@ /// <reference path="./jsx.d.ts" /> | ||
continue | ||
// Non breaking space | ||
case '\u00A0': | ||
@@ -353,19 +354,42 @@ escaped += value.slice(start, end) + ' ' | ||
* | ||
* @param {string} html | ||
* @returns {function} the compiled function which | ||
* @param {(proxy: any) => string} htmlFn | ||
* @param {boolean} [strict=true] if we should throw an error when a property is not found. | ||
* @returns {function} the compiled template function | ||
* @this {void} | ||
*/ | ||
function compile (html) { | ||
function compile (htmlFn, strict = true, separator = '/*\x00*/') { | ||
if (typeof htmlFn !== 'function') { | ||
throw new Error('The first argument must be a function.') | ||
} | ||
const properties = new Set() | ||
const html = htmlFn(new Proxy({}, { | ||
get (_, name) { | ||
// Adds the property to the set of known properties. | ||
properties.add(name) | ||
// Uses ` to avoid content being escaped. | ||
return `\`${separator} + (args[${separator}\`${name.toString()}\`${separator}] || ${strict ? `throwPropertyNotFound(${separator}\`${name.toString()}\`${separator})` : `${separator}\`\`${separator}`}) + ${separator}\`` | ||
} | ||
})) | ||
const sepLength = separator.length | ||
const length = html.length | ||
let body = 'return ' | ||
// Adds the throwPropertyNotFound function if strict | ||
let body = '' | ||
let nextStart = 0 | ||
let paramEnd = 0 | ||
let index = 0 | ||
// Escapes every ` without separator | ||
for (; index < length; index++) { | ||
// Escapes the backtick character because it will be used to wrap the string | ||
// in a template literal. | ||
if (html[index] === '`') { | ||
body += '`' + html.slice(nextStart, index) + '\\``+' | ||
if ( | ||
html[index] === '`' && | ||
html.slice(index - sepLength, index) !== separator && | ||
html.slice(index + 1, index + sepLength + 1) !== separator | ||
) { | ||
body += html.slice(nextStart, index) + '\\`' | ||
nextStart = index + 1 | ||
@@ -378,38 +402,28 @@ continue | ||
if (html[index] === '\\') { | ||
body += '`' + html.slice(nextStart, index) + '\\\\`+' | ||
body += html.slice(nextStart, index) + '\\\\' | ||
nextStart = index + 1 | ||
continue | ||
} | ||
} | ||
// Skip non $ characters | ||
if (html[index] !== '$') { | ||
continue | ||
} | ||
// Adds the remaining string | ||
body += html.slice(nextStart) | ||
// Finds the end index of the current variable | ||
paramEnd = index | ||
while ( | ||
html[++paramEnd] !== undefined && | ||
// @ts-expect-error - this indexing is safe. | ||
html[paramEnd].match(/[a-zA-Z0-9]/) | ||
); | ||
body += | ||
'`' + | ||
html.slice(nextStart, index) + | ||
'`+(args["' + | ||
html.slice(index + 1, paramEnd) + | ||
'"] || "' + | ||
html.slice(index, paramEnd) + | ||
'")+' | ||
nextStart = paramEnd | ||
index = paramEnd | ||
if (strict) { | ||
// eslint-disable-next-line no-new-func | ||
return Function('args', | ||
// Checks for args presence | ||
'if (args === undefined) { throw new Error("The arguments object was not provided.") };\n' + | ||
// Function to throw when a property is not found | ||
'function throwPropertyNotFound(name) { throw new Error("Property " + name + " was not provided.") };\n' + | ||
// Concatenates the body | ||
`return \`${body}\`` | ||
) | ||
} | ||
// Adds the remaining string | ||
body += '`' + html.slice(nextStart) + '`' | ||
// eslint-disable-next-line no-new-func | ||
return Function('args', body) | ||
return Function('args', | ||
// Adds a empty args object when it is not present | ||
'if (args === undefined) { args = Object.create(null) };\n' + `return \`${body}\`` | ||
) | ||
} | ||
@@ -416,0 +430,0 @@ |
@@ -30,3 +30,3 @@ // This file is a result from many sources, including: RFCs, typescript dom lib, w3schools, and others. | ||
spellcheck?: undefined | string | boolean | ||
tabindex?: undefined | string | ||
tabindex?: undefined | number | string | ||
title?: undefined | string | ||
@@ -33,0 +33,0 @@ translate?: undefined | string | boolean |
{ | ||
"name": "@kitajs/html", | ||
"version": "1.4.7", | ||
"version": "2.0.0", | ||
"description": "Fast and type safe HTML templates using TypeScript.", | ||
@@ -31,3 +31,3 @@ "main": "index.js", | ||
"test": "tsc && node --test dist/test", | ||
"bench": "tsc && node dist/benchmark", | ||
"bench": "tsc && node --expose-gc dist/benchmark", | ||
"format": "prettier --write .", | ||
@@ -34,0 +34,0 @@ "lint": "standard" |
156
README.md
@@ -39,3 +39,5 @@ <br /> | ||
- [Migrating from HTML](#migrating-from-html) | ||
- [Compiling html](#compiling-html) | ||
- [Base HTML templates](#base-html-templates) | ||
- [Compiling HTML](#compiling-html) | ||
- [Clean Components](#clean-components) | ||
- [Fragments](#fragments) | ||
@@ -224,34 +226,105 @@ - [Supported HTML](#supported-html) | ||
## Compiling html | ||
### Base HTML templates | ||
When you have static html, is simple to get amazing performances, just save it to a constant and reuse it. However, if you need to hydrate the html with dynamic values in a super fast way, you can use the `compile` property to compile the html and reuse it later. | ||
Often you will have a "template" html with doctype, things on the head, body and so on... The layout is also a very good component to be compiled. Here is a effective example on how to do it:. | ||
```tsx | ||
import html from '@kitajs/html' | ||
export const Layout = html.compile<html.PropsWithChildren>((p) => ( | ||
<> | ||
{'<!doctype html>'} | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Document</title> | ||
{p.head} | ||
</head> | ||
<body>{p.children}</body> | ||
</html> | ||
</> | ||
)) | ||
const compiled = html.compile<['param1', 'param2']>( | ||
<div> | ||
<div>$param1</div> | ||
<div>$param2</div> | ||
<div>$notFound</div> | ||
</div>, | ||
// or | ||
<MyComponent param1="$param1" param2="$param2" /> | ||
const html = ( | ||
<Layout | ||
head={ | ||
<> | ||
<link rel="stylesheet" href="/style.css" /> | ||
<script src="/script.js" /> | ||
</> | ||
}> | ||
<div>Hello World</div> | ||
</Layout> | ||
) | ||
``` | ||
const html = compiled({ param1: 'Hello', param2: 'World!' }) | ||
// formatted html to make it easier to read | ||
// <div> | ||
// <div>Hello</div> | ||
// <div>World!</div> | ||
// <div>$notFound</div> | ||
// </div> | ||
<br /> | ||
## Compiling HTML | ||
Compiles a **clean component** into a super fast component. This does not | ||
support unclean components / props processing. | ||
This mode works just like prepared statements in SQL. Compiled components can give up to [**2000**](#performance) times faster html generation. This is a opt-in feature that you may not be able to use everywhere! | ||
```tsx | ||
import html from '@kitajs/html' | ||
function Component(props: PropsWithChildren<{ name: string }>) { | ||
return <div>Hello {props.name}</div> | ||
} | ||
compiled = html.compile<typeof Component>(Component) | ||
compiled({ name: 'World' }) | ||
// <div>Hello World</div> | ||
compiled = html.compile((p) => <div>Hello {p.name}</div>) | ||
compiled({ name: 'World' }) | ||
// <div>Hello World</div> | ||
``` | ||
This makes the html generation around [**_1500_**](#performance) times faster than just using normal jsx. | ||
Properties passed for compiled components **ARE NOT** what will be passed as argument to the generated function. | ||
Variables that were not passed to the `compile` function are ignored **silently**, this way you can reuse the result into another `compile` function or just because the your _"`$val`"_ was supposed to be a static value. | ||
```tsx | ||
compiled = html.compile((t) => { | ||
// THIS WILL NOT print 123, but a string used by .compile instead | ||
console.log(t.asd) | ||
return <div></div> | ||
}) | ||
compiled({ asd: 123 }) | ||
``` | ||
That's the reason on why you cannot compile unclean components, as they need to process the props before rendering. | ||
<br /> | ||
### Clean Components | ||
A **clean component** is a component that does not process props before | ||
applying them to the element. This means that the props are applied to the | ||
element as is, and you need to process them before passing them to the | ||
component. | ||
```tsx | ||
// Clean component, render as is | ||
function Clean(props: CleanProps<{ repeated: string }>) { | ||
return <div>{props.repeated}</div> | ||
} | ||
// Calculation is done before passing to the component | ||
html = <Clean name={'a'.repeat(5)} /> | ||
// Unclean component, process before render | ||
function Unclean(props: { repeat: string; n: number }) { | ||
return <div>{props.repeat.repeat(props.n)}</div> | ||
} | ||
// Calculation is done inside the component, thus cannot be used with .compile() | ||
html = <Unclean repeat="a" n={5} /> | ||
``` | ||
<br /> | ||
## Fragments | ||
@@ -364,17 +437,38 @@ | ||
This package is just a string builder on steroids, as you can see [how this works](#how-it-works). However we are running a benchmark with an JSX HTML with about 10K characters to see how it performs. | ||
This package is just a string builder on steroids, as you can see [how this works](#how-it-works). This means that most way to isolate performance differences is to micro benchmark. | ||
You can run this yourself by running `pnpm bench`. | ||
You can run this yourself by running `pnpm bench`. The bench below was with a Apple M1 Pro 8gb. | ||
```java | ||
// Apple M1 Pro 8gb | ||
```markdown | ||
# Benchmark | ||
@kitajs/html: | ||
44 767 ops/s, ±0.17% | 99.91% slower | ||
- 2023-09-11T00:53:49.607Z | ||
- Node: v18.16.0 | ||
- V8: 10.2.154.26-node.26 | ||
- OS: darwin | ||
- Arch: arm64 | ||
@kitajs/html - compiled: | ||
48 124 728 ops/s, ±0.48% | fastest | ||
## Hello World | ||
typed-html: | ||
19 199 ops/s, ±0.45% | slowest, 99.96% slower | ||
| Runs | @kitajs/html | typed-html | + | .compile() | + / @kitajs/html | + / typed-html | | ||
| ------ | ------------ | ---------- | ----- | ---------- | ---------------- | -------------- | | ||
| 10 | 0.0063ms | 0.0107ms | 1.68x | 0.0013ms | 5.07x | 8.53x | | ||
| 10000 | 1.632ms | 4.848ms | 2.97x | 0.9131ms | 1.79x | 5.31x | | ||
| 100000 | 9.4629ms | 19.367ms | 2.05x | 2.3115ms | 4.09x | 8.38x | | ||
## Many Props | ||
| Runs | @kitajs/html | typed-html | + | .compile() | + / @kitajs/html | + / typed-html | | ||
| ------ | ------------ | ----------- | ----- | ---------- | ---------------- | -------------- | | ||
| 10 | 0.4629ms | 1.3898ms | 3x | 0.0025ms | 182.19x | 547.04x | | ||
| 10000 | 372.5842ms | 840.7459ms | 2.26x | 0.6308ms | 590.66x | 1332.84x | | ||
| 100000 | 3438.7935ms | 7706.0509ms | 2.24x | 3.7163ms | 925.32x | 2073.56x | | ||
## Big Component | ||
| Runs | @kitajs/html | typed-html | + | .compile() | + / @kitajs/html | + / typed-html | | ||
| ------ | ------------ | ----------- | ----- | ---------- | ---------------- | -------------- | | ||
| 10 | 0.3075ms | 0.8844ms | 2.88x | 0.0037ms | 81.99x | 235.85x | | ||
| 10000 | 222.5096ms | 521.0473ms | 2.34x | 0.7118ms | 312.61x | 732.02x | | ||
| 100000 | 2211.6316ms | 5229.3416ms | 2.36x | 4.1123ms | 537.82x | 1271.65x | | ||
``` | ||
@@ -381,0 +475,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
62779
1149
544