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