boxwood 2.2.4 → 2.4.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/README.md CHANGED
@@ -14,10 +14,7 @@ Unlike traditional template engines, Boxwood templates are **just JavaScript fun
14
14
  const HomePage = ({ posts }) => {
15
15
  return Div([
16
16
  H1("Blog"),
17
- posts.map(post => Article([
18
- H2(post.title),
19
- P(post.summary)
20
- ]))
17
+ posts.map((post) => Article([H2(post.title), P(post.summary)])),
21
18
  ])
22
19
  }
23
20
  ```
@@ -25,48 +22,59 @@ const HomePage = ({ posts }) => {
25
22
  ## Key Advantages
26
23
 
27
24
  ### Zero Learning Curve
25
+
28
26
  If you know JavaScript, you already know Boxwood. Use `map`, `filter`, `if/else`, and all standard JS features naturally.
29
27
 
30
28
  ### IDE Support
29
+
31
30
  Get autocomplete, refactoring, and go-to-definition out of the box. Your templates are just code, so your editor understands them.
32
31
 
33
32
  ### True Composition
33
+
34
34
  Components are functions. Compose them like functions. No slots, no special APIs - just parameters and return values.
35
35
 
36
36
  ### Performance
37
+
37
38
  No template parsing at runtime. Templates are already JavaScript functions, eliminating parsing overhead.
38
39
 
39
40
  ### Security Helpers
41
+
40
42
  - Automatic HTML escaping by default
41
43
  - Basic sanitization for loaded SVG/HTML files
42
44
  - Path traversal protection for file operations
43
45
  - Remember: security is ultimately your responsibility
44
46
 
45
47
  ### Integrated CSS Management
48
+
46
49
  - Automatic CSS scoping with hash-based class names
47
50
  - CSS-in-JS with zero runtime
48
51
  - Critical CSS inlining
49
52
  - Automatic minification
50
53
 
51
54
  ### Built-in i18n Support
55
+
52
56
  First-class internationalization support with a simple, component-friendly API for multi-language applications.
53
57
 
54
58
  ### Asset Handling
59
+
55
60
  - Inline images as base64
56
61
  - SVG loading with automatic sanitization
57
62
  - JSON data loading
58
63
  - Raw HTML imports with XSS protection
59
64
 
60
65
  ### SEO Friendly
66
+
61
67
  - Pure server-side rendering - search engines see fully rendered HTML
62
68
  - Lightning fast pages with inlined critical CSS
63
69
  - Minimal payload size improves Core Web Vitals scores
64
70
  - No client-side hydration delays
65
71
 
66
72
  ### Minimal Footprint
67
- Single file implementation (~950 lines). No complex build process or heavy dependencies.
73
+
74
+ Short implementation. No complex build process or heavy dependencies.
68
75
 
69
76
  ### Testable by Design
77
+
70
78
  Templates are pure functions - easy to unit test with any testing framework.
71
79
 
72
80
  ## Table of Contents
@@ -91,10 +99,7 @@ Create a template file:
91
99
  const { Div, H1, P } = require("boxwood")
92
100
 
93
101
  module.exports = ({ name, message }) => {
94
- return Div([
95
- H1(`Hello, ${name}!`),
96
- P(message)
97
- ])
102
+ return Div([H1(`Hello, ${name}!`), P(message)])
98
103
  }
99
104
  ```
100
105
 
@@ -105,9 +110,9 @@ Compile and render it:
105
110
  const { compile } = require("boxwood")
106
111
 
107
112
  const { template } = compile("./templates/greeting.js")
108
- const html = template({
113
+ const html = template({
109
114
  name: "World",
110
- message: "Welcome to Boxwood"
115
+ message: "Welcome to Boxwood",
111
116
  })
112
117
 
113
118
  console.log(html)
@@ -119,40 +124,41 @@ console.log(html)
119
124
  Boxwood includes built-in Express support:
120
125
 
121
126
  ```js
122
- import express from 'express'
123
- import engine from 'boxwood/adapters/express'
124
- import crypto from 'crypto'
127
+ import express from "express"
128
+ import engine from "boxwood/adapters/express"
129
+ import crypto from "crypto"
125
130
 
126
131
  const app = express()
127
132
 
128
133
  // Register Boxwood as template engine
129
- app.engine('js', engine())
130
- app.set('views', './views')
131
- app.set('view engine', 'js')
134
+ app.engine("js", engine())
135
+ app.set("views", "./views")
136
+ app.set("view engine", "js")
132
137
 
133
138
  // CSP (Content Security Policy) nonce for inline scripts
134
139
  // A nonce is a unique random value generated for each request that allows
135
140
  // specific inline scripts to execute while blocking potential XSS attacks
136
141
  app.use((req, res, next) => {
137
142
  // Generate a cryptographically secure random nonce
138
- res.locals.nonce = crypto.randomBytes(16).toString('base64')
139
-
143
+ res.locals.nonce = crypto.randomBytes(16).toString("base64")
144
+
140
145
  // Set CSP header - only scripts with this exact nonce can execute
141
146
  res.setHeader(
142
- 'Content-Security-Policy',
147
+ "Content-Security-Policy",
143
148
  `script-src 'nonce-${res.locals.nonce}' 'strict-dynamic';`
144
149
  )
145
150
  next()
146
151
  })
147
152
 
148
153
  // Render templates - nonce is automatically injected into all inline scripts
149
- app.get('/', (req, res) => {
150
- res.render('home', { title: 'Welcome' })
154
+ app.get("/", (req, res) => {
155
+ res.render("home", { title: "Welcome" })
151
156
  // Boxwood automatically adds nonce="${res.locals.nonce}" to script tags
152
157
  })
153
158
  ```
154
159
 
155
160
  The Express adapter automatically:
161
+
156
162
  - Handles template caching in production
157
163
  - Hot reloads templates in development
158
164
  - Injects CSP nonces from `res.locals.nonce` into all inline scripts and styles
@@ -171,6 +177,7 @@ A Content Security Policy (CSP) nonce is a security feature that helps prevent C
171
177
  - Attackers can't guess the nonce, so injected scripts are blocked
172
178
 
173
179
  Example output:
180
+
174
181
  ```html
175
182
  <!-- HTTP Header -->
176
183
  Content-Security-Policy: script-src 'nonce-rAnd0m123' 'strict-dynamic';
@@ -205,10 +212,13 @@ const styles = css`
205
212
  `
206
213
 
207
214
  const Button = ({ variant, children }) => {
208
- return ButtonTag({
209
- // className accepts arrays - falsy values are automatically filtered
210
- className: [styles.button, variant === 'secondary' && styles.secondary]
211
- }, children)
215
+ return ButtonTag(
216
+ {
217
+ // className accepts arrays - falsy values are automatically filtered
218
+ className: [styles.button, variant === "secondary" && styles.secondary],
219
+ },
220
+ children
221
+ )
212
222
  }
213
223
 
214
224
  module.exports = component(Button, { styles })
@@ -223,12 +233,12 @@ const { component, i18n, H1, P } = require("boxwood")
223
233
  const Welcome = ({ translate, username }) => {
224
234
  return [
225
235
  H1(translate("greeting").replace("{name}", username)),
226
- P(translate("intro"))
236
+ P(translate("intro")),
227
237
  ]
228
238
  }
229
239
 
230
- module.exports = component(Welcome, {
231
- i18n: i18n.load(__dirname)
240
+ module.exports = component(Welcome, {
241
+ i18n: i18n.load(__dirname),
232
242
  })
233
243
  ```
234
244
 
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const { join, resolve, sep: separator } = require("path")
2
2
  const { readFileSync, realpathSync, lstatSync } = require("fs")
3
3
  const csstree = require("css-tree")
4
+ const { createHash } = require("./utilities/hash")
4
5
 
5
6
  function compile(path) {
6
7
  const fn = require(path)
@@ -485,7 +486,7 @@ const tag = (a, b, c) => {
485
486
  }
486
487
  const name = a
487
488
  const attributes = b
488
- const children = c || []
489
+ const children = typeof c === "number" ? c : c || []
489
490
  return {
490
491
  name,
491
492
  children,
@@ -493,16 +494,6 @@ const tag = (a, b, c) => {
493
494
  }
494
495
  }
495
496
 
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)
504
- }
505
-
506
497
  function decamelize(string) {
507
498
  return string.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()
508
499
  }
@@ -561,9 +552,8 @@ function css(inputs) {
561
552
 
562
553
  csstree.walk(tree, (node) => {
563
554
  if (node.type === "ClassSelector") {
564
- // Generate hash based on the CSS content and class name
565
- const hash = hashDJB2(result + node.name).slice(0, 6)
566
- const name = `${node.name}_${hash}`
555
+ const hash = createHash(result + node.name)
556
+ const name = hash
567
557
  classes[node.name] = name
568
558
  node.name = name
569
559
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "boxwood",
3
- "version": "2.2.4",
3
+ "version": "2.4.0",
4
4
  "description": "Compile HTML templates into JS",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,25 @@
1
+ const { css, component, Div } = require("../..")
2
+
3
+ function Center({ className, style, height, width } = {}, children) {
4
+ const styleObject = {
5
+ display: "flex",
6
+ "justify-content": "center",
7
+ "align-items": "center",
8
+ "flex-direction": "column",
9
+ width: width || "100%",
10
+ height: height || "100dvh",
11
+ }
12
+
13
+ const styles = css`
14
+ .center {
15
+ ${css.create(styleObject).toString()}
16
+ }
17
+ `
18
+
19
+ return [
20
+ Div({ className: [className, styles.center], style }, children),
21
+ styles.css,
22
+ ]
23
+ }
24
+
25
+ module.exports = component(Center)
package/ui/index.js CHANGED
@@ -1,9 +1,11 @@
1
+ const Center = require("./center")
1
2
  const Container = require("./container")
2
3
  const Grid = require("./grid")
3
4
  const Group = require("./group")
4
5
  const Stack = require("./stack")
5
6
 
6
7
  module.exports = {
8
+ Center,
7
9
  Container,
8
10
  Grid,
9
11
  Group,
@@ -0,0 +1,16 @@
1
+ let index = 0
2
+ const map = new Map()
3
+
4
+ function createHash(string) {
5
+ if (map.has(string)) {
6
+ return map.get(string)
7
+ }
8
+ index++
9
+ const hash = "c" + index
10
+ map.set(string, hash)
11
+ return hash
12
+ }
13
+
14
+ module.exports = {
15
+ createHash,
16
+ }