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.
- package/.claude/settings.local.json +10 -0
- package/README.md +143 -101
- package/examples/typescript-example.ts +49 -0
- package/index.d.ts +545 -0
- package/index.js +168 -64
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -3,24 +3,71 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/boxwood)
|
|
4
4
|
[](https://github.com/buxlabs/boxwood/actions)
|
|
5
5
|
|
|
6
|
-
>
|
|
6
|
+
> It's just JavaScript™ - A template engine that gets out of your way
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## Why Boxwood?
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
57
|
-
const
|
|
58
|
-
const banner = require("./banner")
|
|
90
|
+
// templates/greeting.js
|
|
91
|
+
const { Div, H1, P } = require("boxwood")
|
|
59
92
|
|
|
60
|
-
module.exports = () => {
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
93
|
-
const {
|
|
104
|
+
// app.js
|
|
105
|
+
const { compile } = require("boxwood")
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
//
|
|
102
|
-
const { component, css,
|
|
103
|
-
|
|
104
|
-
const styles = css
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
166
|
+
### Asset Loading
|
|
138
167
|
|
|
139
|
-
|
|
168
|
+
```js
|
|
169
|
+
const { Img, Svg } = require("boxwood")
|
|
140
170
|
|
|
141
|
-
|
|
171
|
+
// Load and inline images
|
|
172
|
+
const Logo = Img.load("./assets/logo.png")
|
|
142
173
|
|
|
143
|
-
|
|
174
|
+
// Load and sanitize SVGs
|
|
175
|
+
const Icon = Svg.load("./assets/icon.svg")
|
|
144
176
|
|
|
145
|
-
|
|
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
|
-
|
|
186
|
+
Boxwood provides basic security features:
|
|
148
187
|
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
">": ">",
|
|
97
|
-
"'": "'",
|
|
98
|
-
'"': """,
|
|
99
|
-
}
|
|
106
|
+
const escapeHTML = (string) => {
|
|
107
|
+
// Convert to string to handle non-string inputs safely
|
|
108
|
+
string = String(string)
|
|
100
109
|
|
|
101
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
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 = "&"
|
|
132
|
+
break
|
|
133
|
+
case "<":
|
|
134
|
+
replacement = "<"
|
|
135
|
+
break
|
|
136
|
+
case ">":
|
|
137
|
+
replacement = ">"
|
|
138
|
+
break
|
|
139
|
+
case "'":
|
|
140
|
+
replacement = "'"
|
|
141
|
+
break
|
|
142
|
+
case '"':
|
|
143
|
+
replacement = """
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
336
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
435
|
-
function
|
|
436
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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": "
|
|
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\"",
|