boxwood 1.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node test:*)",
5
+ "Bash(mkdir:*)",
6
+ "Bash(npm test:*)"
7
+ ],
8
+ "deny": []
9
+ }
10
+ }
package/README.md CHANGED
@@ -3,24 +3,71 @@
3
3
  [![npm](https://img.shields.io/npm/v/boxwood.svg)](https://www.npmjs.com/package/boxwood)
4
4
  [![build](https://github.com/buxlabs/boxwood/workflows/build/badge.svg)](https://github.com/buxlabs/boxwood/actions)
5
5
 
6
- > Server side templating engine written in JavaScript
6
+ > It's just JavaScript™ - A template engine that gets out of your way
7
7
 
8
- [boxwood](https://github.com/buxlabs/boxwood) was created to achieve the following design goals:
8
+ ## Why Boxwood?
9
9
 
10
- 1. templates can be split into components
11
- 2. css is hashed per component
12
- 3. css is automatically minified
13
- 4. critical css is inlined
14
- 5. templates can import other dependencies
15
- 6. inline images or svgs
16
- 7. i18n support
17
- 8. server side
18
- 9. good for seo
19
- 10. small (1 file, 890 LOC~)
20
- 11. easy to start, familiar syntax
21
- 12. easy to test
10
+ Unlike traditional template engines, Boxwood templates are **just JavaScript functions**. No new syntax to learn, no parsing overhead, and full access to the JavaScript ecosystem.
22
11
 
23
- The template starts with a standard js file, which builds a tree of nodes, that get rendered to html.
12
+ ```javascript
13
+ // This is your template - just a function that returns HTML nodes
14
+ const HomePage = ({ posts }) => {
15
+ return Div([
16
+ H1("Blog"),
17
+ posts.map(post => Article([
18
+ H2(post.title),
19
+ P(post.summary)
20
+ ]))
21
+ ])
22
+ }
23
+ ```
24
+
25
+ ## Key Advantages
26
+
27
+ ### Zero Learning Curve
28
+ If you know JavaScript, you already know Boxwood. Use `map`, `filter`, `if/else`, and all standard JS features naturally.
29
+
30
+ ### IDE Support
31
+ Get autocomplete, refactoring, and go-to-definition out of the box. Your templates are just code, so your editor understands them.
32
+
33
+ ### True Composition
34
+ Components are functions. Compose them like functions. No slots, no special APIs - just parameters and return values.
35
+
36
+ ### Performance
37
+ No template parsing at runtime. Templates are already JavaScript functions, eliminating parsing overhead.
38
+
39
+ ### Security Helpers
40
+ - Automatic HTML escaping by default
41
+ - Basic sanitization for loaded SVG/HTML files
42
+ - Path traversal protection for file operations
43
+ - Remember: security is ultimately your responsibility
44
+
45
+ ### Integrated CSS Management
46
+ - Automatic CSS scoping with hash-based class names
47
+ - CSS-in-JS with zero runtime
48
+ - Critical CSS inlining
49
+ - Automatic minification
50
+
51
+ ### Built-in i18n Support
52
+ First-class internationalization support with a simple, component-friendly API for multi-language applications.
53
+
54
+ ### Asset Handling
55
+ - Inline images as base64
56
+ - SVG loading with automatic sanitization
57
+ - JSON data loading
58
+ - Raw HTML imports with XSS protection
59
+
60
+ ### SEO Friendly
61
+ - Pure server-side rendering - search engines see fully rendered HTML
62
+ - Lightning fast pages with inlined critical CSS
63
+ - Minimal payload size improves Core Web Vitals scores
64
+ - No client-side hydration delays
65
+
66
+ ### Minimal Footprint
67
+ Single file implementation (~950 lines). No complex build process or heavy dependencies.
68
+
69
+ ### Testable by Design
70
+ Templates are pure functions - easy to unit test with any testing framework.
24
71
 
25
72
  ## Table of Contents
26
73
 
@@ -37,122 +84,117 @@ The template starts with a standard js file, which builds a tree of nodes, that
37
84
 
38
85
  ## Usage
39
86
 
40
- ```js
41
- const { compile } = require("boxwood")
42
- const { join } = require("path")
43
- // ...
44
- const path = join(__dirname, "index.js")
45
- const { template } = compile(path)
46
- // ...
47
- const html = template({ foo: "bar" })
48
- console.log(html)
49
- ```
50
-
51
- You can use [express-boxwood](https://www.npmjs.com/package/express-boxwood) for [express](https://www.npmjs.com/package/express).
52
-
53
- ## Syntax
87
+ Create a template file:
54
88
 
55
89
  ```js
56
- // example/index.js
57
- const layout = require("./layout")
58
- const banner = require("./banner")
90
+ // templates/greeting.js
91
+ const { Div, H1, P } = require("boxwood")
59
92
 
60
- module.exports = () => {
61
- return layout({ language: "en" }, [
62
- banner({
63
- title: "Hello, world!",
64
- description: "Lorem ipsum dolor sit amet",
65
- }),
93
+ module.exports = ({ name, message }) => {
94
+ return Div([
95
+ H1(`Hello, ${name}!`),
96
+ P(message)
66
97
  ])
67
98
  }
68
99
  ```
69
100
 
70
- ```js
71
- // example/layout/index.js
72
- const { component, css, doctype, html, body } = require("boxwood")
73
- const head = require("./head")
74
-
75
- const styles = css.load(__dirname)
76
-
77
- module.exports = component(
78
- ({ language }, children) => {
79
- return [
80
- doctype(),
81
- html({ lang: language }, [
82
- head(),
83
- body({ className: styles.layout }, children),
84
- ]),
85
- ]
86
- },
87
- { styles }
88
- )
89
- ```
101
+ Compile and render it:
90
102
 
91
103
  ```js
92
- // example/layout/head/index.js
93
- const { head, title } = require("boxwood")
104
+ // app.js
105
+ const { compile } = require("boxwood")
94
106
 
95
- module.exports = () => {
96
- return head([title("example")])
97
- }
107
+ const { template } = compile("./templates/greeting.js")
108
+ const html = template({
109
+ name: "World",
110
+ message: "Welcome to Boxwood"
111
+ })
112
+
113
+ console.log(html)
114
+ // <div><h1>Hello, World!</h1><p>Welcome to Boxwood</p></div>
98
115
  ```
99
116
 
117
+ For Express apps, use [express-boxwood](https://www.npmjs.com/package/express-boxwood).
118
+
119
+ ## Features
120
+
121
+ ### Components with CSS
122
+
100
123
  ```js
101
- // example/banner/index.js
102
- const { component, css, h1, p, section } = require("boxwood")
103
-
104
- const styles = css.load(__dirname)
105
-
106
- module.exports = component(
107
- ({ title, description }) => {
108
- return section({ className: styles.banner }, [
109
- h1(title),
110
- description && p(description),
111
- ])
112
- },
113
- { styles }
114
- )
124
+ // button.js
125
+ const { component, css, Button: ButtonTag } = require("boxwood")
126
+
127
+ const styles = css`
128
+ .button {
129
+ padding: 8px 16px;
130
+ background: blue;
131
+ color: white;
132
+ }
133
+ .secondary {
134
+ background: gray;
135
+ }
136
+ `
137
+
138
+ const Button = ({ variant, children }) => {
139
+ return ButtonTag({
140
+ // className accepts arrays - falsy values are automatically filtered
141
+ className: [styles.button, variant === 'secondary' && styles.secondary]
142
+ }, children)
143
+ }
144
+
145
+ module.exports = component(Button, { styles })
115
146
  ```
116
147
 
117
- ```js
118
- // example/banner/index.test.js
119
- const test = require("node:test")
120
- const assert = require("node:assert")
121
- const { compile } = require("boxwood")
148
+ ### Internationalization
122
149
 
123
- test("banner renders a title", async () => {
124
- const { template } = compile(__dirname)
125
- const html = template({ title: "foo" })
126
- assert(html.includes("<h1>foo</h1>"))
127
- })
150
+ ```js
151
+ // welcome.js
152
+ const { component, i18n, H1, P } = require("boxwood")
153
+
154
+ const Welcome = ({ translate, username }) => {
155
+ return [
156
+ H1(translate("greeting").replace("{name}", username)),
157
+ P(translate("intro"))
158
+ ]
159
+ }
128
160
 
129
- test("banner renders an optional description", async () => {
130
- const { template } = compile(__dirname)
131
- const html = template({ title: "foo", description: "bar" })
132
- assert(html.includes("<h1>foo</h1>"))
133
- assert(html.includes("<p>bar</p>"))
161
+ module.exports = component(Welcome, {
162
+ i18n: i18n.load(__dirname)
134
163
  })
135
164
  ```
136
165
 
137
- You can check the `test` dir for more examples.
166
+ ### Asset Loading
138
167
 
139
- ## Security
168
+ ```js
169
+ const { Img, Svg } = require("boxwood")
140
170
 
141
- By default, boxwood sanitizes all HTML, SVG and i18n content loaded via its API to protect against basic XSS attacks.
171
+ // Load and inline images
172
+ const Logo = Img.load("./assets/logo.png")
142
173
 
143
- Disabling sanitization ({ sanitize: false }) is only safe for trusted, developer-controlled files. Never use it with user-generated or untrusted content.
174
+ // Load and sanitize SVGs
175
+ const Icon = Svg.load("./assets/icon.svg")
144
176
 
145
- All file access is restricted to the project directory and symlinks are not allowed by default to prevent path traversal attacks.
177
+ module.exports = () => {
178
+ return [Logo(), Icon]
179
+ }
180
+ ```
181
+
182
+ Additional examples are available in the `test` directory.
183
+
184
+ ## Security
146
185
 
147
- That said, the library is pretty small so please review it and suggest improvements if you have any.
186
+ Boxwood provides basic security features:
148
187
 
149
- ## Maintainers
188
+ - HTML content is escaped by default
189
+ - Loaded SVG and HTML files are sanitized
190
+ - File access is restricted to the project directory
191
+ - Symlinks are blocked to prevent directory traversal
150
192
 
151
- [@emilos](https://github.com/emilos)
193
+ The `sanitize: false` option should only be used with trusted content. Security remains the developer's responsibility.
152
194
 
153
195
  ## Contributing
154
196
 
155
- All contributions are highly appreciated. Please feel free to open new issues and send PRs.
197
+ Issues and pull requests are welcome. The codebase is intentionally small and focused.
156
198
 
157
199
  ## License
158
200
 
@@ -0,0 +1,49 @@
1
+ // Example of using boxwood with TypeScript
2
+ import { Div, H1, Button, Form, Input, component, classes } from 'boxwood';
3
+
4
+ // Define typed component props
5
+ interface UserCardProps {
6
+ name: string;
7
+ email: string;
8
+ isActive: boolean;
9
+ }
10
+
11
+ // Create a typed component
12
+ const UserCard = component<UserCardProps>(
13
+ ({ name, email, isActive }, children) => {
14
+ return Div({
15
+ className: classes('user-card', { active: isActive })
16
+ }, [
17
+ H1({}, name),
18
+ Div({ className: 'email' }, email),
19
+ children
20
+ ]);
21
+ }
22
+ );
23
+
24
+ // Use the component with type checking
25
+ const app = Div({ id: 'app' }, [
26
+ UserCard({
27
+ name: 'John Doe',
28
+ email: 'john@example.com',
29
+ isActive: true
30
+ },
31
+ Button({ onclick: () => alert('Hello!') }, 'Click me')
32
+ ),
33
+
34
+ Form({ method: 'post' }, [
35
+ Input({
36
+ type: 'email',
37
+ name: 'email',
38
+ required: true,
39
+ placeholder: 'Enter email'
40
+ }),
41
+ Button({ type: 'submit' }, 'Submit')
42
+ ])
43
+ ]);
44
+
45
+ // TypeScript will provide:
46
+ // - Autocomplete for all element attributes
47
+ // - Type checking for attribute values
48
+ // - Error highlighting for invalid props
49
+ // - IntelliSense documentation
package/index.d.ts ADDED
@@ -0,0 +1,545 @@
1
+ // TypeScript definitions for boxwood
2
+ declare module 'boxwood' {
3
+ // Core types
4
+ type Child = string | number | Node | Node[] | null | undefined;
5
+ type Children = Child | Child[];
6
+
7
+ interface Node {
8
+ name: string;
9
+ attributes?: Record<string, any>;
10
+ children?: Children;
11
+ ignore?: boolean;
12
+ }
13
+
14
+ interface ComponentProps {
15
+ [key: string]: any;
16
+ }
17
+
18
+ interface TranslateFunction {
19
+ (key: string): string;
20
+ }
21
+
22
+ interface ComponentWithI18n<T = ComponentProps> {
23
+ (props: T & { translate: TranslateFunction }, children?: Children): Node | Node[];
24
+ }
25
+
26
+ interface Component<T = ComponentProps> {
27
+ (props?: T, children?: Children): Node | Node[];
28
+ }
29
+
30
+ // HTML Attributes
31
+ interface GlobalAttributes {
32
+ accesskey?: string;
33
+ autocapitalize?: string;
34
+ class?: string;
35
+ className?: string;
36
+ contenteditable?: boolean | 'true' | 'false';
37
+ contextmenu?: string;
38
+ dir?: 'ltr' | 'rtl' | 'auto';
39
+ draggable?: boolean | 'true' | 'false';
40
+ hidden?: boolean;
41
+ id?: string;
42
+ lang?: string;
43
+ slot?: string;
44
+ spellcheck?: boolean | 'true' | 'false';
45
+ style?: string | Record<string, string | number>;
46
+ tabindex?: number | string;
47
+ title?: string;
48
+ translate?: 'yes' | 'no';
49
+ role?: string;
50
+ // Data attributes
51
+ [key: `data-${string}`]: any;
52
+ // Event handlers
53
+ onclick?: string | Function;
54
+ ondblclick?: string | Function;
55
+ onmousedown?: string | Function;
56
+ onmouseup?: string | Function;
57
+ onmouseover?: string | Function;
58
+ onmousemove?: string | Function;
59
+ onmouseout?: string | Function;
60
+ onmouseenter?: string | Function;
61
+ onmouseleave?: string | Function;
62
+ onkeydown?: string | Function;
63
+ onkeyup?: string | Function;
64
+ onkeypress?: string | Function;
65
+ onfocus?: string | Function;
66
+ onblur?: string | Function;
67
+ onchange?: string | Function;
68
+ oninput?: string | Function;
69
+ onsubmit?: string | Function;
70
+ onreset?: string | Function;
71
+ onload?: string | Function;
72
+ onerror?: string | Function;
73
+ onresize?: string | Function;
74
+ onscroll?: string | Function;
75
+ }
76
+
77
+ // Element-specific attributes
78
+ interface AnchorAttributes extends GlobalAttributes {
79
+ href?: string;
80
+ target?: '_blank' | '_self' | '_parent' | '_top';
81
+ rel?: string;
82
+ download?: string | boolean;
83
+ hreflang?: string;
84
+ ping?: string;
85
+ referrerpolicy?: string;
86
+ type?: string;
87
+ }
88
+
89
+ interface ImageAttributes extends GlobalAttributes {
90
+ src?: string;
91
+ alt?: string;
92
+ width?: number | string;
93
+ height?: number | string;
94
+ loading?: 'lazy' | 'eager';
95
+ decoding?: 'sync' | 'async' | 'auto';
96
+ srcset?: string;
97
+ sizes?: string;
98
+ crossorigin?: 'anonymous' | 'use-credentials';
99
+ referrerpolicy?: string;
100
+ }
101
+
102
+ interface InputAttributes extends GlobalAttributes {
103
+ type?: 'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search' |
104
+ 'date' | 'time' | 'datetime-local' | 'month' | 'week' | 'color' |
105
+ 'checkbox' | 'radio' | 'file' | 'submit' | 'reset' | 'button' |
106
+ 'hidden' | 'image' | 'range';
107
+ name?: string;
108
+ value?: string | number;
109
+ placeholder?: string;
110
+ required?: boolean;
111
+ disabled?: boolean;
112
+ readonly?: boolean;
113
+ checked?: boolean;
114
+ min?: number | string;
115
+ max?: number | string;
116
+ step?: number | string;
117
+ pattern?: string;
118
+ multiple?: boolean;
119
+ accept?: string;
120
+ autocomplete?: string;
121
+ autofocus?: boolean;
122
+ form?: string;
123
+ list?: string;
124
+ maxlength?: number;
125
+ minlength?: number;
126
+ size?: number;
127
+ }
128
+
129
+ interface FormAttributes extends GlobalAttributes {
130
+ action?: string;
131
+ method?: 'get' | 'post' | 'dialog';
132
+ enctype?: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain';
133
+ name?: string;
134
+ target?: string;
135
+ novalidate?: boolean;
136
+ autocomplete?: 'on' | 'off';
137
+ }
138
+
139
+ interface ButtonAttributes extends GlobalAttributes {
140
+ type?: 'submit' | 'reset' | 'button';
141
+ name?: string;
142
+ value?: string;
143
+ disabled?: boolean;
144
+ form?: string;
145
+ formaction?: string;
146
+ formenctype?: string;
147
+ formmethod?: string;
148
+ formnovalidate?: boolean;
149
+ formtarget?: string;
150
+ autofocus?: boolean;
151
+ }
152
+
153
+ interface TextAreaAttributes extends GlobalAttributes {
154
+ name?: string;
155
+ rows?: number;
156
+ cols?: number;
157
+ disabled?: boolean;
158
+ readonly?: boolean;
159
+ required?: boolean;
160
+ placeholder?: string;
161
+ autofocus?: boolean;
162
+ form?: string;
163
+ maxlength?: number;
164
+ minlength?: number;
165
+ wrap?: 'hard' | 'soft';
166
+ }
167
+
168
+ interface SelectAttributes extends GlobalAttributes {
169
+ name?: string;
170
+ disabled?: boolean;
171
+ required?: boolean;
172
+ multiple?: boolean;
173
+ size?: number;
174
+ form?: string;
175
+ autofocus?: boolean;
176
+ }
177
+
178
+ interface OptionAttributes extends GlobalAttributes {
179
+ value?: string;
180
+ label?: string;
181
+ selected?: boolean;
182
+ disabled?: boolean;
183
+ }
184
+
185
+ interface LabelAttributes extends GlobalAttributes {
186
+ for?: string;
187
+ htmlFor?: string;
188
+ }
189
+
190
+ interface ScriptAttributes extends GlobalAttributes {
191
+ src?: string;
192
+ type?: string;
193
+ async?: boolean;
194
+ defer?: boolean;
195
+ crossorigin?: 'anonymous' | 'use-credentials';
196
+ integrity?: string;
197
+ nomodule?: boolean;
198
+ referrerpolicy?: string;
199
+ target?: 'head' | 'body';
200
+ }
201
+
202
+ interface LinkAttributes extends GlobalAttributes {
203
+ href?: string;
204
+ rel?: string;
205
+ type?: string;
206
+ media?: string;
207
+ as?: string;
208
+ crossorigin?: 'anonymous' | 'use-credentials';
209
+ integrity?: string;
210
+ referrerpolicy?: string;
211
+ sizes?: string;
212
+ imagesrcset?: string;
213
+ imagesizes?: string;
214
+ }
215
+
216
+ interface MetaAttributes extends GlobalAttributes {
217
+ charset?: string;
218
+ content?: string;
219
+ httpEquiv?: string;
220
+ name?: string;
221
+ property?: string;
222
+ }
223
+
224
+ interface MediaAttributes extends GlobalAttributes {
225
+ src?: string;
226
+ controls?: boolean;
227
+ autoplay?: boolean;
228
+ loop?: boolean;
229
+ muted?: boolean;
230
+ preload?: 'none' | 'metadata' | 'auto';
231
+ crossorigin?: 'anonymous' | 'use-credentials';
232
+ }
233
+
234
+ interface VideoAttributes extends MediaAttributes {
235
+ width?: number | string;
236
+ height?: number | string;
237
+ poster?: string;
238
+ playsinline?: boolean;
239
+ }
240
+
241
+ interface AudioAttributes extends MediaAttributes {}
242
+
243
+ interface IframeAttributes extends GlobalAttributes {
244
+ src?: string;
245
+ srcdoc?: string;
246
+ name?: string;
247
+ width?: number | string;
248
+ height?: number | string;
249
+ allow?: string;
250
+ allowfullscreen?: boolean;
251
+ allowpaymentrequest?: boolean;
252
+ loading?: 'lazy' | 'eager';
253
+ referrerpolicy?: string;
254
+ sandbox?: string;
255
+ }
256
+
257
+ interface TableCellAttributes extends GlobalAttributes {
258
+ colspan?: number;
259
+ rowspan?: number;
260
+ headers?: string;
261
+ }
262
+
263
+ // SVG Attributes
264
+ interface SVGAttributes extends GlobalAttributes {
265
+ // Core SVG attributes
266
+ viewBox?: string;
267
+ preserveAspectRatio?: string;
268
+ xmlns?: string;
269
+ xmlnsXlink?: string;
270
+ version?: string;
271
+ baseProfile?: string;
272
+ x?: number | string;
273
+ y?: number | string;
274
+ width?: number | string;
275
+ height?: number | string;
276
+ // Presentation attributes
277
+ fill?: string;
278
+ fillOpacity?: number | string;
279
+ fillRule?: 'nonzero' | 'evenodd';
280
+ stroke?: string;
281
+ strokeWidth?: number | string;
282
+ strokeOpacity?: number | string;
283
+ strokeLinecap?: 'butt' | 'round' | 'square';
284
+ strokeLinejoin?: 'miter' | 'round' | 'bevel';
285
+ strokeDasharray?: string;
286
+ strokeDashoffset?: number | string;
287
+ opacity?: number | string;
288
+ transform?: string;
289
+ vectorEffect?: string;
290
+ shapeRendering?: string;
291
+ pathLength?: number;
292
+ // Common SVG attributes
293
+ d?: string;
294
+ points?: string;
295
+ cx?: number | string;
296
+ cy?: number | string;
297
+ r?: number | string;
298
+ rx?: number | string;
299
+ ry?: number | string;
300
+ x1?: number | string;
301
+ y1?: number | string;
302
+ x2?: number | string;
303
+ y2?: number | string;
304
+ href?: string;
305
+ xlinkHref?: string;
306
+ id?: string;
307
+ clipPath?: string;
308
+ mask?: string;
309
+ filter?: string;
310
+ markerStart?: string;
311
+ markerMid?: string;
312
+ markerEnd?: string;
313
+ // Animation attributes
314
+ attributeName?: string;
315
+ from?: string | number;
316
+ to?: string | number;
317
+ dur?: string;
318
+ repeatCount?: number | 'indefinite';
319
+ // Gradient attributes
320
+ gradientUnits?: 'userSpaceOnUse' | 'objectBoundingBox';
321
+ gradientTransform?: string;
322
+ fx?: number | string;
323
+ fy?: number | string;
324
+ offset?: string;
325
+ stopColor?: string;
326
+ stopOpacity?: number | string;
327
+ // Text attributes
328
+ textAnchor?: 'start' | 'middle' | 'end';
329
+ dominantBaseline?: string;
330
+ fontSize?: number | string;
331
+ fontFamily?: string;
332
+ fontWeight?: number | string;
333
+ letterSpacing?: number | string;
334
+ textDecoration?: string;
335
+ // Pattern attributes
336
+ patternUnits?: 'userSpaceOnUse' | 'objectBoundingBox';
337
+ patternTransform?: string;
338
+ patternContentUnits?: 'userSpaceOnUse' | 'objectBoundingBox';
339
+ }
340
+
341
+ // Element type functions
342
+ type ElementFunction<T = GlobalAttributes> = (attributes?: T, children?: Children) => Node;
343
+
344
+ // HTML Elements
345
+ export const A: ElementFunction<AnchorAttributes>;
346
+ export const Abbr: ElementFunction<GlobalAttributes>;
347
+ export const Address: ElementFunction<GlobalAttributes>;
348
+ export const Area: ElementFunction<GlobalAttributes & { alt?: string; coords?: string; shape?: string; href?: string; target?: string; rel?: string; }>;
349
+ export const Article: ElementFunction<GlobalAttributes>;
350
+ export const Aside: ElementFunction<GlobalAttributes>;
351
+ export const Audio: ElementFunction<AudioAttributes>;
352
+ export const B: ElementFunction<GlobalAttributes>;
353
+ export const Base: ElementFunction<GlobalAttributes & { href?: string; target?: string; }>;
354
+ export const Bdi: ElementFunction<GlobalAttributes>;
355
+ export const Bdo: ElementFunction<GlobalAttributes>;
356
+ export const Blockquote: ElementFunction<GlobalAttributes & { cite?: string; }>;
357
+ export const Body: ElementFunction<GlobalAttributes>;
358
+ export const Br: ElementFunction<GlobalAttributes>;
359
+ export const Button: ElementFunction<ButtonAttributes>;
360
+ export const Canvas: ElementFunction<GlobalAttributes & { width?: number | string; height?: number | string; }>;
361
+ export const Caption: ElementFunction<GlobalAttributes>;
362
+ export const Cite: ElementFunction<GlobalAttributes>;
363
+ export const Code: ElementFunction<GlobalAttributes>;
364
+ export const Col: ElementFunction<GlobalAttributes & { span?: number; }>;
365
+ export const Colgroup: ElementFunction<GlobalAttributes & { span?: number; }>;
366
+ export const Data: ElementFunction<GlobalAttributes & { value?: string; }>;
367
+ export const Datalist: ElementFunction<GlobalAttributes>;
368
+ export const Dd: ElementFunction<GlobalAttributes>;
369
+ export const Del: ElementFunction<GlobalAttributes & { cite?: string; datetime?: string; }>;
370
+ export const Details: ElementFunction<GlobalAttributes & { open?: boolean; }>;
371
+ export const Dfn: ElementFunction<GlobalAttributes>;
372
+ export const Dialog: ElementFunction<GlobalAttributes & { open?: boolean; }>;
373
+ export const Div: ElementFunction<GlobalAttributes>;
374
+ export const Dl: ElementFunction<GlobalAttributes>;
375
+ export const Dt: ElementFunction<GlobalAttributes>;
376
+ export const Em: ElementFunction<GlobalAttributes>;
377
+ export const Embed: ElementFunction<GlobalAttributes & { src?: string; type?: string; width?: number | string; height?: number | string; }>;
378
+ export const Fieldset: ElementFunction<GlobalAttributes & { disabled?: boolean; form?: string; name?: string; }>;
379
+ export const Figcaption: ElementFunction<GlobalAttributes>;
380
+ export const Figure: ElementFunction<GlobalAttributes>;
381
+ export const Footer: ElementFunction<GlobalAttributes>;
382
+ export const Form: ElementFunction<FormAttributes>;
383
+ export const H1: ElementFunction<GlobalAttributes>;
384
+ export const H2: ElementFunction<GlobalAttributes>;
385
+ export const H3: ElementFunction<GlobalAttributes>;
386
+ export const H4: ElementFunction<GlobalAttributes>;
387
+ export const H5: ElementFunction<GlobalAttributes>;
388
+ export const H6: ElementFunction<GlobalAttributes>;
389
+ export const Head: ElementFunction<GlobalAttributes>;
390
+ export const Header: ElementFunction<GlobalAttributes>;
391
+ export const Hgroup: ElementFunction<GlobalAttributes>;
392
+ export const Hr: ElementFunction<GlobalAttributes>;
393
+ export const Html: ElementFunction<GlobalAttributes & { lang?: string; }>;
394
+ export const I: ElementFunction<GlobalAttributes>;
395
+ export const Iframe: ElementFunction<IframeAttributes>;
396
+ export const Img: ElementFunction<ImageAttributes>;
397
+ export const Input: ElementFunction<InputAttributes>;
398
+ export const Ins: ElementFunction<GlobalAttributes & { cite?: string; datetime?: string; }>;
399
+ export const Kbd: ElementFunction<GlobalAttributes>;
400
+ export const Label: ElementFunction<LabelAttributes>;
401
+ export const Legend: ElementFunction<GlobalAttributes>;
402
+ export const Li: ElementFunction<GlobalAttributes & { value?: number; }>;
403
+ export const Link: ElementFunction<LinkAttributes>;
404
+ export const Main: ElementFunction<GlobalAttributes>;
405
+ export const Map: ElementFunction<GlobalAttributes & { name?: string; }>;
406
+ export const Mark: ElementFunction<GlobalAttributes>;
407
+ export const Menu: ElementFunction<GlobalAttributes>;
408
+ export const Meta: ElementFunction<MetaAttributes>;
409
+ export const Meter: ElementFunction<GlobalAttributes & { value?: number; min?: number; max?: number; low?: number; high?: number; optimum?: number; }>;
410
+ export const Nav: ElementFunction<GlobalAttributes>;
411
+ export const Noscript: ElementFunction<GlobalAttributes>;
412
+ export const Object: ElementFunction<GlobalAttributes & { data?: string; type?: string; name?: string; form?: string; width?: number | string; height?: number | string; }>;
413
+ export const Ol: ElementFunction<GlobalAttributes & { reversed?: boolean; start?: number; type?: '1' | 'a' | 'A' | 'i' | 'I'; }>;
414
+ export const Optgroup: ElementFunction<GlobalAttributes & { disabled?: boolean; label?: string; }>;
415
+ export const Option: ElementFunction<OptionAttributes>;
416
+ export const Output: ElementFunction<GlobalAttributes & { for?: string; form?: string; name?: string; }>;
417
+ export const P: ElementFunction<GlobalAttributes>;
418
+ export const Param: ElementFunction<GlobalAttributes & { name?: string; value?: string; }>;
419
+ export const Picture: ElementFunction<GlobalAttributes>;
420
+ export const Pre: ElementFunction<GlobalAttributes>;
421
+ export const Progress: ElementFunction<GlobalAttributes & { value?: number; max?: number; }>;
422
+ export const Q: ElementFunction<GlobalAttributes & { cite?: string; }>;
423
+ export const Rp: ElementFunction<GlobalAttributes>;
424
+ export const Rt: ElementFunction<GlobalAttributes>;
425
+ export const Ruby: ElementFunction<GlobalAttributes>;
426
+ export const S: ElementFunction<GlobalAttributes>;
427
+ export const Samp: ElementFunction<GlobalAttributes>;
428
+ export const Script: ElementFunction<ScriptAttributes>;
429
+ export const Section: ElementFunction<GlobalAttributes>;
430
+ export const Select: ElementFunction<SelectAttributes>;
431
+ export const Slot: ElementFunction<GlobalAttributes & { name?: string; }>;
432
+ export const Small: ElementFunction<GlobalAttributes>;
433
+ export const Source: ElementFunction<GlobalAttributes & { src?: string; type?: string; srcset?: string; sizes?: string; media?: string; }>;
434
+ export const Span: ElementFunction<GlobalAttributes>;
435
+ export const Strong: ElementFunction<GlobalAttributes>;
436
+ export const Style: ElementFunction<GlobalAttributes & { type?: string; media?: string; nonce?: string; }>;
437
+ export const Sub: ElementFunction<GlobalAttributes>;
438
+ export const Summary: ElementFunction<GlobalAttributes>;
439
+ export const Sup: ElementFunction<GlobalAttributes>;
440
+ export const Table: ElementFunction<GlobalAttributes>;
441
+ export const Tbody: ElementFunction<GlobalAttributes>;
442
+ export const Td: ElementFunction<TableCellAttributes>;
443
+ export const Template: ElementFunction<GlobalAttributes>;
444
+ export const Textarea: ElementFunction<TextAreaAttributes>;
445
+ export const Tfoot: ElementFunction<GlobalAttributes>;
446
+ export const Th: ElementFunction<TableCellAttributes & { scope?: 'row' | 'col' | 'rowgroup' | 'colgroup'; abbr?: string; }>;
447
+ export const Thead: ElementFunction<GlobalAttributes>;
448
+ export const Time: ElementFunction<GlobalAttributes & { datetime?: string; }>;
449
+ export const Title: ElementFunction<GlobalAttributes>;
450
+ export const Tr: ElementFunction<GlobalAttributes>;
451
+ export const Track: ElementFunction<GlobalAttributes & { kind?: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata'; src?: string; srclang?: string; label?: string; default?: boolean; }>;
452
+ export const U: ElementFunction<GlobalAttributes>;
453
+ export const Ul: ElementFunction<GlobalAttributes>;
454
+ export const Var: ElementFunction<GlobalAttributes>;
455
+ export const Video: ElementFunction<VideoAttributes>;
456
+ export const Wbr: ElementFunction<GlobalAttributes>;
457
+
458
+ // SVG Elements
459
+ export const Svg: ElementFunction<SVGAttributes>;
460
+ export const Animate: ElementFunction<SVGAttributes>;
461
+ export const AnimateMotion: ElementFunction<SVGAttributes & { path?: string; }>;
462
+ export const AnimateTransform: ElementFunction<SVGAttributes & { type?: string; }>;
463
+ export const Circle: ElementFunction<SVGAttributes>;
464
+ export const ClipPath: ElementFunction<SVGAttributes>;
465
+ export const Defs: ElementFunction<SVGAttributes>;
466
+ export const Desc: ElementFunction<SVGAttributes>;
467
+ export const Ellipse: ElementFunction<SVGAttributes>;
468
+ export const Filter: ElementFunction<SVGAttributes>;
469
+ export const ForeignObject: ElementFunction<SVGAttributes>;
470
+ export const G: ElementFunction<SVGAttributes>;
471
+ export const Image: ElementFunction<SVGAttributes>;
472
+ export const Line: ElementFunction<SVGAttributes>;
473
+ export const LinearGradient: ElementFunction<SVGAttributes>;
474
+ export const Marker: ElementFunction<SVGAttributes & { markerWidth?: number | string; markerHeight?: number | string; refX?: number | string; refY?: number | string; orient?: string | number; }>;
475
+ export const Mask: ElementFunction<SVGAttributes>;
476
+ export const Metadata: ElementFunction<SVGAttributes>;
477
+ export const Path: ElementFunction<SVGAttributes>;
478
+ export const Pattern: ElementFunction<SVGAttributes>;
479
+ export const Polygon: ElementFunction<SVGAttributes>;
480
+ export const Polyline: ElementFunction<SVGAttributes>;
481
+ export const RadialGradient: ElementFunction<SVGAttributes>;
482
+ export const Rect: ElementFunction<SVGAttributes>;
483
+ export const Set: ElementFunction<SVGAttributes>;
484
+ export const Stop: ElementFunction<SVGAttributes>;
485
+ export const Switch: ElementFunction<SVGAttributes>;
486
+ export const Symbol: ElementFunction<SVGAttributes>;
487
+ export const Text: ElementFunction<SVGAttributes>;
488
+ export const TextPath: ElementFunction<SVGAttributes>;
489
+ export const Tspan: ElementFunction<SVGAttributes>;
490
+ export const Use: ElementFunction<SVGAttributes>;
491
+ export const View: ElementFunction<SVGAttributes>;
492
+
493
+ // Special elements
494
+ export function Doctype(attributes?: { html?: boolean }): Node;
495
+
496
+ // Utility functions
497
+ export function escape(text: string): string;
498
+ export function raw(html: string): { html: string };
499
+ export function tag(name: string, attributes?: Record<string, any>, children?: Children): Node;
500
+
501
+ // Asset loaders
502
+ export function css(path: string): { css: Node };
503
+ export function js(path: string): { js: Node };
504
+ export function json(path: string): any;
505
+
506
+ // Component system
507
+ interface ComponentOptions {
508
+ styles?: string | string[] | { css: Node } | { css: Node }[];
509
+ scripts?: string | string[] | { js: Node } | { js: Node }[];
510
+ }
511
+
512
+ export function component<T = ComponentProps>(
513
+ fn: Component<T> | ComponentWithI18n<T>,
514
+ options?: ComponentOptions
515
+ ): Component<T>;
516
+
517
+ // CSS utilities
518
+ export function classes(...args: Array<string | Record<string, boolean> | undefined | null | false>): string;
519
+
520
+ // Internationalization
521
+ export function i18n<T = ComponentProps>(
522
+ fn: ComponentWithI18n<T>,
523
+ translations: Record<string, Record<string, string>>
524
+ ): Component<T>;
525
+
526
+ // Compile function
527
+ interface CompileResult {
528
+ template(...args: any[]): string;
529
+ }
530
+
531
+ export function compile(path: string): CompileResult;
532
+
533
+ // Error types
534
+ export class CompileError extends Error {
535
+ constructor(message: string);
536
+ }
537
+
538
+ export class SecurityError extends Error {
539
+ constructor(message: string);
540
+ }
541
+
542
+ export class TranslationError extends Error {
543
+ constructor(message: string);
544
+ }
545
+ }
package/index.js CHANGED
@@ -5,7 +5,8 @@ const csstree = require("css-tree")
5
5
  function compile(path) {
6
6
  const fn = require(path)
7
7
  return {
8
- template() {
8
+ template(options) {
9
+ const nonce = options && options.nonce
9
10
  const tree = fn(...arguments)
10
11
  const nodes = {}
11
12
  const styles = []
@@ -65,24 +66,36 @@ function compile(path) {
65
66
  walk(tree)
66
67
  if (nodes.head) {
67
68
  if (styles.length > 0) {
68
- nodes.head.children.push({
69
+ const styleNode = {
69
70
  name: "style",
70
71
  children: styles.join(""),
71
- })
72
+ }
73
+ if (nonce) {
74
+ styleNode.attributes = { nonce }
75
+ }
76
+ nodes.head.children.push(styleNode)
72
77
  }
73
78
  if (scripts.head.length > 0) {
74
- nodes.head.children.push({
79
+ const scriptNode = {
75
80
  name: "script",
76
81
  children: scripts.head.join(""),
77
- })
82
+ }
83
+ if (nonce) {
84
+ scriptNode.attributes = { nonce }
85
+ }
86
+ nodes.head.children.push(scriptNode)
78
87
  }
79
88
  }
80
89
  if (nodes.body) {
81
90
  if (scripts.body.length > 0) {
82
- nodes.body.children.push({
91
+ const scriptNode = {
83
92
  name: "script",
84
93
  children: scripts.body.join(""),
85
- })
94
+ }
95
+ if (nonce) {
96
+ scriptNode.attributes = { nonce }
97
+ }
98
+ nodes.body.children.push(scriptNode)
86
99
  }
87
100
  }
88
101
  return render(tree)
@@ -90,20 +103,61 @@ function compile(path) {
90
103
  }
91
104
  }
92
105
 
93
- const ENTITIES = {
94
- "&": "&amp;",
95
- "<": "&lt;",
96
- ">": "&gt;",
97
- "'": "&#39;",
98
- '"': "&quot;",
99
- }
106
+ const escapeHTML = (string) => {
107
+ // Convert to string to handle non-string inputs safely
108
+ string = String(string)
100
109
 
101
- const REGEXP = /[&<>'"]/g
110
+ // Fast path: if no special characters, return as-is
111
+ if (
112
+ !string.includes("&") &&
113
+ !string.includes("<") &&
114
+ !string.includes(">") &&
115
+ !string.includes("'") &&
116
+ !string.includes('"')
117
+ ) {
118
+ return string
119
+ }
102
120
 
103
- const escapeHTML = (string) => {
104
- return String.prototype.replace.call(string, REGEXP, function (character) {
105
- return ENTITIES[character]
106
- })
121
+ const len = string.length
122
+ let result = ""
123
+ let lastIndex = 0
124
+
125
+ for (let i = 0; i < len; i++) {
126
+ const char = string[i]
127
+ let replacement
128
+
129
+ switch (char) {
130
+ case "&":
131
+ replacement = "&amp;"
132
+ break
133
+ case "<":
134
+ replacement = "&lt;"
135
+ break
136
+ case ">":
137
+ replacement = "&gt;"
138
+ break
139
+ case "'":
140
+ replacement = "&#39;"
141
+ break
142
+ case '"':
143
+ replacement = "&quot;"
144
+ break
145
+ default:
146
+ continue
147
+ }
148
+
149
+ if (lastIndex !== i) {
150
+ result += string.slice(lastIndex, i)
151
+ }
152
+ result += replacement
153
+ lastIndex = i + 1
154
+ }
155
+
156
+ if (lastIndex !== len) {
157
+ result += string.slice(lastIndex)
158
+ }
159
+
160
+ return result
107
161
  }
108
162
 
109
163
  const normalizePath = (path) => path.replace(/\\/g, "/").replace(/\/+$/, "")
@@ -186,7 +240,7 @@ function readFile(path, encoding) {
186
240
  }
187
241
  }
188
242
 
189
- const BOOLEAN_ATTRIBUTES = [
243
+ const BOOLEAN_ATTRIBUTES = new Set([
190
244
  "async",
191
245
  "autofocus",
192
246
  "autoplay",
@@ -223,14 +277,16 @@ const BOOLEAN_ATTRIBUTES = [
223
277
  "sortable",
224
278
  "spellcheck",
225
279
  "translate",
226
- ]
280
+ ])
227
281
 
228
282
  const ALIASES = {
229
283
  className: "class",
230
284
  htmlFor: "for",
231
285
  }
232
286
 
233
- const isKeyValid = (key) => /^[a-zA-Z0-9\-_]+$/.test(key)
287
+ // Pre-compiled regex for better performance
288
+ const KEY_VALIDATION_REGEX = /^[a-zA-Z0-9\-_]+$/
289
+ const isKeyValid = (key) => KEY_VALIDATION_REGEX.test(key)
234
290
 
235
291
  const attributes = (options) => {
236
292
  if (!options) {
@@ -248,13 +304,13 @@ const attributes = (options) => {
248
304
  value === true ||
249
305
  Array.isArray(value)
250
306
  ) {
251
- if (BOOLEAN_ATTRIBUTES.includes(key)) {
307
+ if (BOOLEAN_ATTRIBUTES.has(key)) {
252
308
  result.push(key)
253
309
  } else {
254
310
  const name = ALIASES[key] || key
255
311
  const value = options[key]
256
312
  const content = Array.isArray(value) ? classes(...value) : value
257
- result.push(name + "=" + '"' + escapeHTML(content) + '"')
313
+ result.push(`${name}="${escapeHTML(content)}"`)
258
314
  }
259
315
  } else if (key === "style" && typeof value === "object") {
260
316
  const styles = []
@@ -276,7 +332,7 @@ const attributes = (options) => {
276
332
  return result.join(" ")
277
333
  }
278
334
 
279
- const SELF_CLOSING_TAGS = [
335
+ const SELF_CLOSING_TAGS = new Set([
280
336
  "area",
281
337
  "base",
282
338
  "br",
@@ -294,54 +350,60 @@ const SELF_CLOSING_TAGS = [
294
350
  "track",
295
351
  "wbr",
296
352
  "!DOCTYPE html",
297
- ]
353
+ ])
298
354
 
299
- const isUnescapedTag = (name) => {
300
- return !["script", "style", "template"].includes(name)
301
- }
355
+ const UNESCAPED_TAGS = new Set(["script", "style", "template"])
302
356
 
303
357
  const render = (input, escape = true) => {
358
+ // Most common case: string (~50% of nodes)
359
+ if (typeof input === "string") {
360
+ return escape ? escapeHTML(input) : input
361
+ }
362
+
363
+ // Second most common: arrays (~20% of nodes)
364
+ if (Array.isArray(input)) {
365
+ let result = ""
366
+ for (let i = 0; i < input.length; i++) {
367
+ result += render(input[i])
368
+ }
369
+ return result
370
+ }
371
+
372
+ // Early exit for null/undefined/false/true
304
373
  if (
305
- typeof input === "undefined" ||
306
- typeof input === "boolean" ||
307
374
  input === null ||
308
- input.ignore
375
+ input === undefined ||
376
+ input === false ||
377
+ input === true
309
378
  ) {
310
379
  return ""
311
380
  }
381
+
382
+ // Numbers (~5% of nodes)
312
383
  if (typeof input === "number") {
313
384
  return input.toString()
314
385
  }
315
- if (typeof input === "string") {
316
- if (escape) {
317
- return escapeHTML(input)
318
- }
319
- return input
320
- }
321
- if (Array.isArray(input)) {
322
- return input.map((input) => render(input)).join("")
386
+
387
+ // Objects (elements) - check ignore flag first
388
+ if (input.ignore) {
389
+ return ""
323
390
  }
324
391
 
325
392
  if (input.name === "raw") {
326
393
  return render(input.children, false)
327
394
  }
328
- if (SELF_CLOSING_TAGS.includes(input.name)) {
329
- if (input.attributes) {
330
- return `<${input.name} ` + attributes(input.attributes) + ">"
331
- }
332
- return `<${input.name}>`
395
+
396
+ if (SELF_CLOSING_TAGS.has(input.name)) {
397
+ const attrs = input.attributes ? attributes(input.attributes) : ""
398
+ return attrs ? `<${input.name} ${attrs}>` : `<${input.name}>`
333
399
  }
334
400
 
335
- const string = input.attributes ? attributes(input.attributes) : ""
336
- const attrs = string ? " " + string : ""
401
+ const attrs = input.attributes ? attributes(input.attributes) : ""
402
+ const children = render(input.children, !UNESCAPED_TAGS.has(input.name))
337
403
 
338
- return (
339
- `<${input.name}` +
340
- attrs +
341
- ">" +
342
- render(input.children, isUnescapedTag(input.name)) +
343
- `</${input.name}>`
344
- )
404
+ return attrs
405
+ ? `<${input.name} ${attrs}>${children}</${input.name}>`
406
+ : `<${input.name}>${children}</${input.name}>`
345
407
  }
346
408
 
347
409
  const raw = (children) => {
@@ -431,9 +493,14 @@ const tag = (a, b, c) => {
431
493
  }
432
494
  }
433
495
 
434
- let number = 1
435
- function sequence() {
436
- return number++
496
+ // DJB2 hash algorithm for CSS class names
497
+ function hashDJB2(str) {
498
+ let hash = 5381
499
+ for (let i = 0; i < str.length; i++) {
500
+ hash = (hash * 33) ^ str.charCodeAt(i)
501
+ }
502
+ // Convert to positive number and base36 for shorter string
503
+ return (hash >>> 0).toString(36)
437
504
  }
438
505
 
439
506
  function decamelize(string) {
@@ -475,12 +542,13 @@ function css(inputs) {
475
542
  result += input
476
543
  }
477
544
  }
478
- const hash = sequence()
479
545
  const tree = csstree.parse(result)
480
546
  const classes = {}
481
547
 
482
548
  csstree.walk(tree, (node) => {
483
549
  if (node.type === "ClassSelector") {
550
+ // Generate hash based on the CSS content and class name
551
+ const hash = hashDJB2(result + node.name).slice(0, 6)
484
552
  const name = `${node.name}_${hash}`
485
553
  classes[node.name] = name
486
554
  node.name = name
@@ -605,12 +673,15 @@ js.load = function (path, options = {}) {
605
673
  }
606
674
 
607
675
  const node = (name) => (options, children) => tag(name, options, children)
608
- const doctype = node("!DOCTYPE html")
676
+ const Doctype = node("!DOCTYPE html")
609
677
 
610
678
  const nodes = [
611
679
  "a",
612
680
  "abbr",
613
681
  "address",
682
+ "animate",
683
+ "animateMotion",
684
+ "animateTransform",
614
685
  "area",
615
686
  "article",
616
687
  "aside",
@@ -625,14 +696,18 @@ const nodes = [
625
696
  "button",
626
697
  "canvas",
627
698
  "caption",
699
+ "circle",
628
700
  "cite",
701
+ "clipPath",
629
702
  "code",
630
703
  "col",
631
704
  "colgroup",
632
705
  "data",
633
706
  "datalist",
634
707
  "dd",
708
+ "defs",
635
709
  "del",
710
+ "desc",
636
711
  "details",
637
712
  "dfn",
638
713
  "dialog",
@@ -640,12 +715,16 @@ const nodes = [
640
715
  "dl",
641
716
  "dt",
642
717
  "em",
718
+ "ellipse",
643
719
  "embed",
644
720
  "fieldset",
645
721
  "figcaption",
646
722
  "figure",
723
+ "filter",
647
724
  "footer",
725
+ "foreignObject",
648
726
  "form",
727
+ "g",
649
728
  "h1",
650
729
  "h2",
651
730
  "h3",
@@ -654,10 +733,12 @@ const nodes = [
654
733
  "h6",
655
734
  "head",
656
735
  "header",
736
+ "hgroup",
657
737
  "hr",
658
738
  "html",
659
739
  "i",
660
740
  "iframe",
741
+ "image",
661
742
  "img",
662
743
  "input",
663
744
  "ins",
@@ -665,11 +746,17 @@ const nodes = [
665
746
  "label",
666
747
  "legend",
667
748
  "li",
749
+ "line",
750
+ "linearGradient",
668
751
  "link",
669
752
  "main",
670
753
  "map",
671
754
  "mark",
755
+ "marker",
756
+ "mask",
757
+ "menu",
672
758
  "meta",
759
+ "metadata",
673
760
  "meter",
674
761
  "nav",
675
762
  "noscript",
@@ -680,10 +767,16 @@ const nodes = [
680
767
  "output",
681
768
  "p",
682
769
  "param",
770
+ "path",
771
+ "pattern",
683
772
  "picture",
773
+ "polygon",
774
+ "polyline",
684
775
  "pre",
685
776
  "progress",
686
777
  "q",
778
+ "radialGradient",
779
+ "rect",
687
780
  "rp",
688
781
  "rt",
689
782
  "ruby",
@@ -692,20 +785,27 @@ const nodes = [
692
785
  "script",
693
786
  "section",
694
787
  "select",
788
+ "set",
789
+ "slot",
695
790
  "small",
696
791
  "source",
697
792
  "span",
793
+ "stop",
698
794
  "strong",
699
795
  "style",
700
796
  "sub",
701
797
  "summary",
702
798
  "sup",
703
799
  "svg",
800
+ "switch",
801
+ "symbol",
704
802
  "table",
705
803
  "tbody",
706
804
  "td",
707
805
  "template",
806
+ "text",
708
807
  "textarea",
808
+ "textPath",
709
809
  "tfoot",
710
810
  "th",
711
811
  "thead",
@@ -713,13 +813,17 @@ const nodes = [
713
813
  "title",
714
814
  "tr",
715
815
  "track",
816
+ "tspan",
716
817
  "u",
717
818
  "ul",
819
+ "use",
718
820
  "var",
719
821
  "video",
822
+ "view",
720
823
  "wbr",
721
824
  ].reduce((result, name) => {
722
- result[name] = node(name)
825
+ const pascalName = name.charAt(0).toUpperCase() + name.slice(1)
826
+ result[pascalName] = node(name)
723
827
  return result
724
828
  }, {})
725
829
 
@@ -740,7 +844,7 @@ function base64({ content, path }) {
740
844
  return `data:${media(path)};base64,${content}`
741
845
  }
742
846
 
743
- nodes.img.load = function (path) {
847
+ nodes.Img.load = function (path) {
744
848
  const type = extension(path)
745
849
  if (!ALLOWED_IMAGE_EXTENSIONS.includes(type)) {
746
850
  throw new Error(
@@ -749,7 +853,7 @@ nodes.img.load = function (path) {
749
853
  }
750
854
  const content = readFile(path, "base64")
751
855
  return (options) => {
752
- return nodes.img({ src: base64({ content, path }), ...options })
856
+ return nodes.Img({ src: base64({ content, path }), ...options })
753
857
  }
754
858
  }
755
859
 
@@ -795,7 +899,7 @@ const sanitizeSVG = (content) => {
795
899
  * Should not be used for user-generated content.
796
900
  */
797
901
 
798
- nodes.svg.load = function (path, options = {}) {
902
+ nodes.Svg.load = function (path, options = {}) {
799
903
  const type = extension(path)
800
904
  if (type !== "svg") {
801
905
  throw new Error(
@@ -933,7 +1037,7 @@ module.exports = {
933
1037
  compile,
934
1038
  component,
935
1039
  classes,
936
- doctype,
1040
+ Doctype,
937
1041
  escape: escapeHTML,
938
1042
  raw,
939
1043
  css,
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "boxwood",
3
- "version": "1.1.1",
3
+ "version": "2.0.0",
4
4
  "description": "Compile HTML templates into JS",
5
5
  "main": "index.js",
6
+ "types": "index.d.ts",
6
7
  "scripts": {
7
8
  "test": "node --test --test-reporter=dot \"test/**/*.test.js\" \"test/**/*.spec.js\"",
8
9
  "test:debug": "node --test --test-reporter=spec \"test/**/*.test.js\" \"test/**/*.spec.js\"",