boxwood 0.63.1 → 0.65.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.
Files changed (2) hide show
  1. package/index.js +228 -210
  2. package/package.json +3 -3
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
- const { join } = require('path')
2
- const { readFileSync } = require('fs')
3
- const csstree = require('css-tree')
4
- const toHash = require('string-hash')
1
+ const { join } = require("path")
2
+ const { readFileSync } = require("fs")
3
+ const csstree = require("css-tree")
4
+ const toHash = require("string-hash")
5
5
 
6
6
  async function compile(path) {
7
7
  const fn = require(path)
@@ -12,20 +12,23 @@ async function compile(path) {
12
12
  const styles = []
13
13
  const scripts = []
14
14
  const walk = (node) => {
15
- if (node.name === 'head') {
15
+ if (!node) {
16
+ return
17
+ }
18
+ if (node.name === "head") {
16
19
  nodes.head = node
17
20
  }
18
- if (node.name === 'body') {
21
+ if (node.name === "body") {
19
22
  nodes.body = node
20
23
  }
21
- if (node.name === 'style') {
24
+ if (node.name === "style") {
22
25
  const css = node.children
23
26
  if (!styles.includes(css)) {
24
27
  styles.push(css)
25
28
  }
26
29
  node.ignore = true
27
30
  }
28
- if (node.name === 'script') {
31
+ if (node.name === "script") {
29
32
  const js = node.children
30
33
  if (!scripts.includes(js)) {
31
34
  scripts.push(js)
@@ -35,23 +38,23 @@ async function compile(path) {
35
38
  if (Array.isArray(node)) {
36
39
  node.forEach(walk)
37
40
  } else if (Array.isArray(node.children)) {
38
- node.children.forEach(walk)
41
+ node.children.filter(Boolean).forEach(walk)
39
42
  }
40
43
  }
41
44
  walk(tree)
42
45
  if (nodes.head) {
43
46
  if (styles.length > 0) {
44
47
  nodes.head.children.push({
45
- name: 'style',
46
- children: styles.join(''),
48
+ name: "style",
49
+ children: styles.join(""),
47
50
  })
48
51
  }
49
52
  }
50
53
  if (nodes.body) {
51
54
  if (scripts.length > 0) {
52
55
  nodes.body.children.push({
53
- name: 'script',
54
- children: scripts.join(''),
56
+ name: "script",
57
+ children: scripts.join(""),
55
58
  })
56
59
  }
57
60
  }
@@ -61,11 +64,11 @@ async function compile(path) {
61
64
  }
62
65
 
63
66
  const ENTITIES = {
64
- '&': '&',
65
- '<': '&lt;',
66
- '>': '&gt;',
67
- "'": '&#39;',
68
- '"': '&quot;',
67
+ "&": "&amp;",
68
+ "<": "&lt;",
69
+ ">": "&gt;",
70
+ "'": "&#39;",
71
+ '"': "&quot;",
69
72
  }
70
73
 
71
74
  const REGEXP = /[&<>'"]/g
@@ -77,47 +80,47 @@ const escapeHTML = (string) => {
77
80
  }
78
81
 
79
82
  const BOOLEAN_ATTRIBUTES = [
80
- 'async',
81
- 'autofocus',
82
- 'autoplay',
83
- 'border',
84
- 'challenge',
85
- 'checked',
86
- 'compact',
87
- 'contenteditable',
88
- 'controls',
89
- 'default',
90
- 'defer',
91
- 'disabled',
92
- 'formnovalidate',
93
- 'frameborder',
94
- 'hidden',
95
- 'indeterminate',
96
- 'ismap',
97
- 'loop',
98
- 'multiple',
99
- 'muted',
100
- 'nohref',
101
- 'noresize',
102
- 'noshade',
103
- 'novalidate',
104
- 'nowrap',
105
- 'open',
106
- 'readonly',
107
- 'required',
108
- 'reversed',
109
- 'scoped',
110
- 'scrolling',
111
- 'seamless',
112
- 'selected',
113
- 'sortable',
114
- 'spellcheck',
115
- 'translate',
83
+ "async",
84
+ "autofocus",
85
+ "autoplay",
86
+ "border",
87
+ "challenge",
88
+ "checked",
89
+ "compact",
90
+ "contenteditable",
91
+ "controls",
92
+ "default",
93
+ "defer",
94
+ "disabled",
95
+ "formnovalidate",
96
+ "frameborder",
97
+ "hidden",
98
+ "indeterminate",
99
+ "ismap",
100
+ "loop",
101
+ "multiple",
102
+ "muted",
103
+ "nohref",
104
+ "noresize",
105
+ "noshade",
106
+ "novalidate",
107
+ "nowrap",
108
+ "open",
109
+ "readonly",
110
+ "required",
111
+ "reversed",
112
+ "scoped",
113
+ "scrolling",
114
+ "seamless",
115
+ "selected",
116
+ "sortable",
117
+ "spellcheck",
118
+ "translate",
116
119
  ]
117
120
 
118
121
  const ALIASES = {
119
- className: 'class',
120
- htmlFor: 'for'
122
+ className: "class",
123
+ htmlFor: "for",
121
124
  }
122
125
 
123
126
  const attributes = (options) => {
@@ -129,59 +132,61 @@ const attributes = (options) => {
129
132
  result.push(key)
130
133
  } else {
131
134
  const name = ALIASES[key] || key
132
- result.push(name + '=' + '"' + options[key] + '"')
135
+ const value = options[key]
136
+ const content = Array.isArray(value) ? classes(...value) : value
137
+ result.push(name + "=" + '"' + content + '"')
133
138
  }
134
139
  }
135
140
  }
136
- return result.join(' ')
141
+ return result.join(" ")
137
142
  }
138
143
 
139
144
  const SELF_CLOSING_TAGS = [
140
- 'area',
141
- 'base',
142
- 'br',
143
- 'col',
144
- 'command',
145
- 'embed',
146
- 'hr',
147
- 'img',
148
- 'input',
149
- 'keygen',
150
- 'link',
151
- 'meta',
152
- 'param',
153
- 'source',
154
- 'track',
155
- 'wbr',
156
- '!DOCTYPE html',
145
+ "area",
146
+ "base",
147
+ "br",
148
+ "col",
149
+ "command",
150
+ "embed",
151
+ "hr",
152
+ "img",
153
+ "input",
154
+ "keygen",
155
+ "link",
156
+ "meta",
157
+ "param",
158
+ "source",
159
+ "track",
160
+ "wbr",
161
+ "!DOCTYPE html",
157
162
  ]
158
163
 
159
164
  const isUnescapedTag = (name) => {
160
- return !['script', 'style', 'template'].includes(name)
165
+ return !["script", "style", "template"].includes(name)
161
166
  }
162
167
 
163
168
  const render = (input, escape = true) => {
164
169
  if (input.ignore) {
165
- return ''
170
+ return ""
166
171
  }
167
172
  if (Array.isArray(input)) {
168
- return input.filter(Boolean).map(render).join('')
173
+ return input.filter(Boolean).map(render).join("")
169
174
  }
170
- if (typeof input === 'number') {
175
+ if (typeof input === "number") {
171
176
  return input.toString()
172
177
  }
173
- if (typeof input === 'string') {
178
+ if (typeof input === "string") {
174
179
  return escape ? escapeHTML(input) : input
175
180
  }
176
- if (input.name === 'fragment') {
181
+ if (input.name === "fragment") {
177
182
  return render(input.children)
178
183
  }
179
- if (input.name === 'raw') {
184
+ if (input.name === "raw") {
180
185
  return render(input.children, false)
181
186
  }
182
187
  if (SELF_CLOSING_TAGS.includes(input.name)) {
183
188
  if (input.attributes) {
184
- return `<${input.name} ` + attributes(input.attributes) + '>'
189
+ return `<${input.name} ` + attributes(input.attributes) + ">"
185
190
  }
186
191
  return `<${input.name}>`
187
192
  }
@@ -189,7 +194,7 @@ const render = (input, escape = true) => {
189
194
  return (
190
195
  `<${input.name} ` +
191
196
  attributes(input.attributes) +
192
- '>' +
197
+ ">" +
193
198
  render(input.children, isUnescapedTag(input.name)) +
194
199
  `</${input.name}>`
195
200
  )
@@ -200,22 +205,26 @@ const render = (input, escape = true) => {
200
205
  )
201
206
  }
202
207
  if (input.children) {
203
- return `<${input.name}>` + render(input.children, isUnescapedTag(input.name)) + `</${input.name}>`
208
+ return (
209
+ `<${input.name}>` +
210
+ render(input.children, isUnescapedTag(input.name)) +
211
+ `</${input.name}>`
212
+ )
204
213
  }
205
214
  return `<${input.name}></${input.name}>`
206
215
  }
207
216
 
208
217
  const fragment = (children) => {
209
- return { name: 'fragment', children }
218
+ return { name: "fragment", children }
210
219
  }
211
220
 
212
221
  const raw = (children) => {
213
- return { name: 'raw', children }
222
+ return { name: "raw", children }
214
223
  }
215
224
 
216
225
  raw.load = function () {
217
226
  const path = join(...arguments)
218
- const content = readFileSync(path, 'utf8')
227
+ const content = readFileSync(path, "utf8")
219
228
  return raw(content)
220
229
  }
221
230
 
@@ -245,7 +254,7 @@ const tag = (a, b, c) => {
245
254
  }
246
255
 
247
256
  function css(inputs) {
248
- let result = ''
257
+ let result = ""
249
258
  for (let i = 0, ilen = inputs.length; i < ilen; i += 1) {
250
259
  const input = inputs[i]
251
260
  const value = arguments[i + 1]
@@ -260,7 +269,7 @@ function css(inputs) {
260
269
  const classes = {}
261
270
 
262
271
  csstree.walk(tree, (node) => {
263
- if (node.type === 'ClassSelector') {
272
+ if (node.type === "ClassSelector") {
264
273
  const name = `__${node.name}__${hash}`
265
274
  classes[node.name] = name
266
275
  node.name = name
@@ -269,18 +278,20 @@ function css(inputs) {
269
278
 
270
279
  return {
271
280
  ...classes,
272
- css: tag('style', csstree.generate(tree)),
281
+ css: tag("style", csstree.generate(tree)),
273
282
  }
274
283
  }
275
284
 
276
285
  css.load = function () {
277
286
  const path = join(...arguments)
278
- const content = readFileSync(path, 'utf8')
279
- return css`${content}`
287
+ const content = readFileSync(path, "utf8")
288
+ return css`
289
+ ${content}
290
+ `
280
291
  }
281
292
 
282
293
  function js(inputs) {
283
- let result = ''
294
+ let result = ""
284
295
  for (let i = 0, ilen = inputs.length; i < ilen; i += 1) {
285
296
  const input = inputs[i]
286
297
  const value = arguments[i + 1]
@@ -291,130 +302,130 @@ function js(inputs) {
291
302
  }
292
303
  }
293
304
  return {
294
- js: tag('script', result),
305
+ js: tag("script", result),
295
306
  }
296
307
  }
297
308
 
298
309
  js.load = function () {
299
310
  const path = join(...arguments)
300
- const content = readFileSync(path, 'utf8')
311
+ const content = readFileSync(path, "utf8")
301
312
  return js`${content}`
302
313
  }
303
314
 
304
315
  const node = (name) => (options, children) => tag(name, options, children)
305
- const doctype = node('!DOCTYPE html')
316
+ const doctype = node("!DOCTYPE html")
306
317
 
307
318
  const nodes = [
308
- 'a',
309
- 'abbr',
310
- 'address',
311
- 'area',
312
- 'article',
313
- 'aside',
314
- 'audio',
315
- 'b',
316
- 'base',
317
- 'bdi',
318
- 'bdo',
319
- 'blockquote',
320
- 'body',
321
- 'br',
322
- 'button',
323
- 'canvas',
324
- 'caption',
325
- 'cite',
326
- 'code',
327
- 'col',
328
- 'colgroup',
329
- 'data',
330
- 'datalist',
331
- 'dd',
332
- 'del',
333
- 'details',
334
- 'dfn',
335
- 'dialog',
336
- 'div',
337
- 'dl',
338
- 'dt',
339
- 'em',
340
- 'embed',
341
- 'fieldset',
342
- 'figcaption',
343
- 'figure',
344
- 'footer',
345
- 'form',
346
- 'h1',
347
- 'h2',
348
- 'h3',
349
- 'h4',
350
- 'h5',
351
- 'h6',
352
- 'head',
353
- 'header',
354
- 'hr',
355
- 'html',
356
- 'i',
357
- 'iframe',
358
- 'img',
359
- 'input',
360
- 'ins',
361
- 'kbd',
362
- 'label',
363
- 'legend',
364
- 'li',
365
- 'link',
366
- 'main',
367
- 'map',
368
- 'mark',
369
- 'meta',
370
- 'meter',
371
- 'nav',
372
- 'noscript',
373
- 'object',
374
- 'ol',
375
- 'optgroup',
376
- 'option',
377
- 'output',
378
- 'p',
379
- 'param',
380
- 'picture',
381
- 'pre',
382
- 'progress',
383
- 'q',
384
- 'rp',
385
- 'rt',
386
- 'ruby',
387
- 's',
388
- 'samp',
389
- 'script',
390
- 'section',
391
- 'select',
392
- 'small',
393
- 'source',
394
- 'span',
395
- 'strong',
396
- 'style',
397
- 'sub',
398
- 'summary',
399
- 'sup',
400
- 'svg',
401
- 'table',
402
- 'tbody',
403
- 'td',
404
- 'template',
405
- 'textarea',
406
- 'tfoot',
407
- 'th',
408
- 'thead',
409
- 'time',
410
- 'title',
411
- 'tr',
412
- 'track',
413
- 'u',
414
- 'ul',
415
- 'var',
416
- 'video',
417
- 'wbr',
319
+ "a",
320
+ "abbr",
321
+ "address",
322
+ "area",
323
+ "article",
324
+ "aside",
325
+ "audio",
326
+ "b",
327
+ "base",
328
+ "bdi",
329
+ "bdo",
330
+ "blockquote",
331
+ "body",
332
+ "br",
333
+ "button",
334
+ "canvas",
335
+ "caption",
336
+ "cite",
337
+ "code",
338
+ "col",
339
+ "colgroup",
340
+ "data",
341
+ "datalist",
342
+ "dd",
343
+ "del",
344
+ "details",
345
+ "dfn",
346
+ "dialog",
347
+ "div",
348
+ "dl",
349
+ "dt",
350
+ "em",
351
+ "embed",
352
+ "fieldset",
353
+ "figcaption",
354
+ "figure",
355
+ "footer",
356
+ "form",
357
+ "h1",
358
+ "h2",
359
+ "h3",
360
+ "h4",
361
+ "h5",
362
+ "h6",
363
+ "head",
364
+ "header",
365
+ "hr",
366
+ "html",
367
+ "i",
368
+ "iframe",
369
+ "img",
370
+ "input",
371
+ "ins",
372
+ "kbd",
373
+ "label",
374
+ "legend",
375
+ "li",
376
+ "link",
377
+ "main",
378
+ "map",
379
+ "mark",
380
+ "meta",
381
+ "meter",
382
+ "nav",
383
+ "noscript",
384
+ "object",
385
+ "ol",
386
+ "optgroup",
387
+ "option",
388
+ "output",
389
+ "p",
390
+ "param",
391
+ "picture",
392
+ "pre",
393
+ "progress",
394
+ "q",
395
+ "rp",
396
+ "rt",
397
+ "ruby",
398
+ "s",
399
+ "samp",
400
+ "script",
401
+ "section",
402
+ "select",
403
+ "small",
404
+ "source",
405
+ "span",
406
+ "strong",
407
+ "style",
408
+ "sub",
409
+ "summary",
410
+ "sup",
411
+ "svg",
412
+ "table",
413
+ "tbody",
414
+ "td",
415
+ "template",
416
+ "textarea",
417
+ "tfoot",
418
+ "th",
419
+ "thead",
420
+ "time",
421
+ "title",
422
+ "tr",
423
+ "track",
424
+ "u",
425
+ "ul",
426
+ "var",
427
+ "video",
428
+ "wbr",
418
429
  ].reduce((result, name) => {
419
430
  result[name] = node(name)
420
431
  return result
@@ -428,9 +439,9 @@ function classes() {
428
439
  continue
429
440
  }
430
441
  const type = typeof arg
431
- if (type === 'string') {
442
+ if (type === "string") {
432
443
  array.push(arg)
433
- } else if (type === 'object') {
444
+ } else if (type === "object") {
434
445
  for (const key in arg) {
435
446
  if (arg[key]) {
436
447
  array.push(key)
@@ -438,7 +449,13 @@ function classes() {
438
449
  }
439
450
  }
440
451
  }
441
- return array.join(' ')
452
+ return array.join(" ")
453
+ }
454
+
455
+ function i18n(translations) {
456
+ return function translate(language, key) {
457
+ return translations[key][language]
458
+ }
442
459
  }
443
460
 
444
461
  module.exports = {
@@ -451,5 +468,6 @@ module.exports = {
451
468
  css,
452
469
  js,
453
470
  tag,
471
+ i18n,
454
472
  ...nodes,
455
473
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "boxwood",
3
- "version": "0.63.1",
3
+ "version": "0.65.0",
4
4
  "description": "Compile HTML templates into JS",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -45,11 +45,11 @@
45
45
  },
46
46
  "homepage": "https://github.com/buxlabs/boxwood#readme",
47
47
  "devDependencies": {
48
- "ava": "^5.1.0",
48
+ "ava": "^5.2.0",
49
49
  "benchmark": "2.1.4",
50
50
  "express": "^4.18.2",
51
51
  "handlebars": "^4.7.7",
52
- "jsdom": "^20.0.3",
52
+ "jsdom": "^21.1.0",
53
53
  "lodash.template": "4.5.0",
54
54
  "mustache": "^4.2.0",
55
55
  "nyc": "15.1.0",