boxwood 0.75.0 → 0.77.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 (60) hide show
  1. package/README.md +17 -13
  2. package/index.js +46 -82
  3. package/package.json +4 -4
  4. package/benchmark/fixtures/basic/boxwood.js +0 -3
  5. package/benchmark/fixtures/basic/data.json +0 -1
  6. package/benchmark/fixtures/basic/handlebars.hbs +0 -1
  7. package/benchmark/fixtures/basic/lodash.ejs +0 -1
  8. package/benchmark/fixtures/basic/mustache.mst +0 -1
  9. package/benchmark/fixtures/basic/underscore.ejs +0 -1
  10. package/benchmark/fixtures/basic/vanilla.js +0 -3
  11. package/benchmark/fixtures/div/boxwood.js +0 -5
  12. package/benchmark/fixtures/div/data.json +0 -1
  13. package/benchmark/fixtures/div/handlebars.hbs +0 -1
  14. package/benchmark/fixtures/div/lodash.ejs +0 -1
  15. package/benchmark/fixtures/div/mustache.mst +0 -1
  16. package/benchmark/fixtures/div/underscore.ejs +0 -1
  17. package/benchmark/fixtures/div/vanilla.js +0 -4
  18. package/benchmark/fixtures/escape/boxwood.js +0 -3
  19. package/benchmark/fixtures/escape/data.json +0 -3
  20. package/benchmark/fixtures/escape/handlebars.hbs +0 -1
  21. package/benchmark/fixtures/escape/lodash.ejs +0 -1
  22. package/benchmark/fixtures/escape/mustache.mst +0 -1
  23. package/benchmark/fixtures/escape/underscore.ejs +0 -1
  24. package/benchmark/fixtures/escape/vanilla.js +0 -5
  25. package/benchmark/fixtures/friends/boxwood.js +0 -56
  26. package/benchmark/fixtures/friends/data.json +0 -30
  27. package/benchmark/fixtures/friends/handlebars.hbs +0 -45
  28. package/benchmark/fixtures/friends/lodash.ejs +0 -48
  29. package/benchmark/fixtures/friends/mustache.mst +0 -45
  30. package/benchmark/fixtures/friends/underscore.ejs +0 -48
  31. package/benchmark/fixtures/friends/vanilla.js +0 -36
  32. package/benchmark/fixtures/if/boxwood.js +0 -21
  33. package/benchmark/fixtures/if/data.json +0 -12
  34. package/benchmark/fixtures/if/handlebars.hbs +0 -13
  35. package/benchmark/fixtures/if/lodash.ejs +0 -12
  36. package/benchmark/fixtures/if/mustache.mst +0 -13
  37. package/benchmark/fixtures/if/underscore.ejs +0 -12
  38. package/benchmark/fixtures/if/vanilla.js +0 -14
  39. package/benchmark/fixtures/projects/boxwood.js +0 -20
  40. package/benchmark/fixtures/projects/data.json +0 -26
  41. package/benchmark/fixtures/projects/handlebars.hbs +0 -15
  42. package/benchmark/fixtures/projects/lodash.ejs +0 -17
  43. package/benchmark/fixtures/projects/mustache.mst +0 -16
  44. package/benchmark/fixtures/projects/underscore.ejs +0 -17
  45. package/benchmark/fixtures/projects/vanilla.js +0 -21
  46. package/benchmark/fixtures/search/boxwood.js +0 -18
  47. package/benchmark/fixtures/search/data.json +0 -35
  48. package/benchmark/fixtures/search/handlebars.hbs +0 -18
  49. package/benchmark/fixtures/search/lodash.ejs +0 -22
  50. package/benchmark/fixtures/search/mustache.mst +0 -18
  51. package/benchmark/fixtures/search/underscore.ejs +0 -22
  52. package/benchmark/fixtures/search/vanilla.js +0 -26
  53. package/benchmark/fixtures/todos/boxwood.js +0 -9
  54. package/benchmark/fixtures/todos/data.json +0 -9
  55. package/benchmark/fixtures/todos/handlebars.hbs +0 -7
  56. package/benchmark/fixtures/todos/lodash.ejs +0 -7
  57. package/benchmark/fixtures/todos/mustache.mst +0 -7
  58. package/benchmark/fixtures/todos/underscore.ejs +0 -7
  59. package/benchmark/fixtures/todos/vanilla.js +0 -12
  60. package/benchmark/index.js +0 -112
package/README.md CHANGED
@@ -56,7 +56,7 @@ const layout = require("./layout")
56
56
  const banner = require("./banner")
57
57
 
58
58
  module.exports = () => {
59
- return layout([
59
+ return layout({ language: "en" }, [
60
60
  banner({
61
61
  title: "Hello, world!",
62
62
  description: "Lorem ipsum dolor sit amet",
@@ -67,22 +67,24 @@ module.exports = () => {
67
67
 
68
68
  ```js
69
69
  // example/layout/index.js
70
-
71
- const { component, css, html, head, body } = require("boxwood")
70
+ const { component, css, html, body } = require("boxwood")
72
71
  const head = require("./head")
73
72
 
74
73
  const styles = css.load(__dirname)
75
74
 
76
75
  module.exports = component(
77
- (children) => {
78
- return html([head(), body({ className: styles.layout }, children)])
76
+ ({ language }, children) => {
77
+ return html({ lang: language }, [
78
+ head(),
79
+ body({ className: styles.layout }, children),
80
+ ])
79
81
  },
80
82
  { styles }
81
83
  )
82
84
  ```
83
85
 
84
86
  ```js
85
- // example/head/index.js
87
+ // example/layout/head/index.js
86
88
  const { head, title } = require("boxwood")
87
89
 
88
90
  module.exports = () => {
@@ -113,20 +115,22 @@ const test = require("node:test")
113
115
  const assert = require("node:assert")
114
116
  const { compile } = require("boxwood")
115
117
 
116
- test("banner renders a title", () => {
118
+ test("banner renders a title", async () => {
117
119
  const { template } = await compile(__dirname)
118
- const html = template({ title: 'foo' })
119
- assert(html.includes('<h1>foo</h1>'))
120
+ const html = template({ title: "foo" })
121
+ assert(html.includes("<h1>foo</h1>"))
120
122
  })
121
123
 
122
- test('banner renders an optional description', () => {
124
+ test("banner renders an optional description", async () => {
123
125
  const { template } = await compile(__dirname)
124
- const html = template({ title: 'foo', description: 'bar' })
125
- assert(html.includes('<h1>foo</h1>'))
126
- assert(html.includes('<p>bar</p>'))
126
+ const html = template({ title: "foo", description: "bar" })
127
+ assert(html.includes("<h1>foo</h1>"))
128
+ assert(html.includes("<p>bar</p>"))
127
129
  })
128
130
  ```
129
131
 
132
+ You can check the `test` dir for more examples.
133
+
130
134
  ## Maintainers
131
135
 
132
136
  [@emilos](https://github.com/emilos)
package/index.js CHANGED
@@ -2,7 +2,6 @@ const { join } = require("path")
2
2
  const { readFileSync } = require("fs")
3
3
  const csstree = require("css-tree")
4
4
  const toHash = require("string-hash")
5
- const YAML = require("yaml")
6
5
 
7
6
  async function compile(path) {
8
7
  const fn = require(path)
@@ -47,7 +46,7 @@ async function compile(path) {
47
46
  if (Array.isArray(node)) {
48
47
  node.forEach(walk)
49
48
  } else if (Array.isArray(node.children)) {
50
- node.children.filter(Boolean).forEach(walk)
49
+ node.children.forEach(walk)
51
50
  }
52
51
  }
53
52
  walk(tree)
@@ -175,24 +174,27 @@ const isUnescapedTag = (name) => {
175
174
  }
176
175
 
177
176
  const render = (input, escape = true) => {
178
- if (input.ignore) {
177
+ if (
178
+ typeof input === "undefined" ||
179
+ typeof input === "boolean" ||
180
+ input === null ||
181
+ input.ignore
182
+ ) {
179
183
  return ""
180
184
  }
181
- if (Array.isArray(input)) {
182
- return input
183
- .filter(Boolean)
184
- .map((input) => render(input))
185
- .join("")
186
- }
187
185
  if (typeof input === "number") {
188
186
  return input.toString()
189
187
  }
190
188
  if (typeof input === "string") {
191
- return escape ? escapeHTML(input) : input
189
+ if (escape) {
190
+ return escapeHTML(input)
191
+ }
192
+ return input
192
193
  }
193
- if (input.name === "fragment") {
194
- return render(input.children)
194
+ if (Array.isArray(input)) {
195
+ return input.map((input) => render(input)).join("")
195
196
  }
197
+
196
198
  if (input.name === "raw") {
197
199
  return render(input.children, false)
198
200
  }
@@ -202,32 +204,14 @@ const render = (input, escape = true) => {
202
204
  }
203
205
  return `<${input.name}>`
204
206
  }
205
- if (input.attributes && input.children) {
206
- return (
207
- `<${input.name} ` +
208
- attributes(input.attributes) +
209
- ">" +
210
- render(input.children, isUnescapedTag(input.name)) +
211
- `</${input.name}>`
212
- )
213
- }
214
- if (input.attributes) {
215
- return (
216
- `<${input.name} ` + attributes(input.attributes) + `></${input.name}>`
217
- )
218
- }
219
- if (input.children) {
220
- return (
221
- `<${input.name}>` +
222
- render(input.children, isUnescapedTag(input.name)) +
223
- `</${input.name}>`
224
- )
225
- }
226
- return `<${input.name}></${input.name}>`
227
- }
228
207
 
229
- const fragment = (children) => {
230
- return { name: "fragment", children }
208
+ return (
209
+ `<${input.name}` +
210
+ (input.attributes ? " " + attributes(input.attributes) : "") +
211
+ ">" +
212
+ render(input.children, isUnescapedTag(input.name)) +
213
+ `</${input.name}>`
214
+ )
231
215
  }
232
216
 
233
217
  const raw = (children) => {
@@ -504,15 +488,6 @@ function classes() {
504
488
  return array.join(" ")
505
489
  }
506
490
 
507
- const yaml = {
508
- load() {
509
- const path = join(...arguments)
510
- const file = path.endsWith(".yaml") ? path : join(path, "index.yaml")
511
- const content = readFileSync(file, "utf8")
512
- return YAML.parse(content)
513
- },
514
- }
515
-
516
491
  const json = {
517
492
  load() {
518
493
  const path = join(...arguments)
@@ -530,23 +505,20 @@ function i18n(translations) {
530
505
 
531
506
  i18n.load = function () {
532
507
  const path = join(...arguments)
533
- const data = path.endsWith(".yaml") ? yaml.load(path) : json.load(path)
508
+ const data = json.load(path)
534
509
  return function translate(language, key) {
535
- if (!key) {
536
- throw new Error(`TranslationError[${key}][${language}]: key is undefined`)
537
- }
538
510
  if (!language) {
539
- throw new Error(
540
- `TranslationError[${key}][${language}]: language is undefined`
541
- )
511
+ throw new Error(`TranslationError: language is undefined`)
542
512
  }
543
- const translation = data[key][language]
544
- if (!translation) {
513
+ if (!key) {
514
+ throw new Error(`TranslationError: key is undefined`)
515
+ }
516
+ if (!data[key] || !data[key][language]) {
545
517
  throw new Error(
546
- `TranslationError[${key}][${language}]: translation is undefined`
518
+ `TranslationError: translation [${key}][${language}] is undefined`
547
519
  )
548
520
  }
549
- return translation
521
+ return data[key][language]
550
522
  }
551
523
  }
552
524
 
@@ -556,24 +528,24 @@ function component(fn, { styles, i18n, scripts } = {}) {
556
528
  return fn({}, a)
557
529
  }
558
530
  if (i18n) {
531
+ if (!a || !a.language) {
532
+ throw new Error(
533
+ `TranslationError: language is undefined for component:\n${fn.toString()}`
534
+ )
535
+ }
559
536
  const { language } = a
560
537
  function translate(key) {
561
538
  if (!key) {
562
539
  throw new Error(
563
- `TranslationError[${key}][${language}]: key is undefined for component:\n${fn.toString()}`
540
+ `TranslationError: key is undefined for component:\n${fn.toString()}`
564
541
  )
565
542
  }
566
- if (!language) {
543
+ if (!i18n[key] || !i18n[key][language]) {
567
544
  throw new Error(
568
- `TranslationError[${key}][${language}]: language is undefined for component:\n${fn.toString()}`
545
+ `TranslationError: translation [${key}][${language}] is undefined for component:\n${fn.toString()}`
569
546
  )
570
547
  }
571
548
  const translation = i18n[key][language]
572
- if (!translation) {
573
- throw new Error(
574
- `TranslationError[${key}][${language}]: translation is undefined for component:\n${fn.toString()}`
575
- )
576
- }
577
549
  return translation
578
550
  }
579
551
  return fn({ ...a, translate }, b || [])
@@ -582,25 +554,19 @@ function component(fn, { styles, i18n, scripts } = {}) {
582
554
  }
583
555
  return function (a, b) {
584
556
  const tree = execute(a, b)
585
- if (styles && scripts) {
586
- if (Array.isArray(tree)) {
587
- return tree.concat(styles.css, scripts.js)
588
- }
589
- return [tree, styles.css, scripts.js]
590
- }
557
+ let nodes = Array.isArray(tree) ? tree : [tree]
558
+
591
559
  if (styles) {
592
- if (Array.isArray(tree)) {
593
- return tree.concat(styles.css)
594
- }
595
- return [tree, styles.css]
560
+ const data = Array.isArray(styles) ? styles : [styles]
561
+ nodes = nodes.concat(data.map((style) => style.css))
596
562
  }
563
+
597
564
  if (scripts) {
598
- if (Array.isArray(tree)) {
599
- return tree.concat(scripts.js)
600
- }
601
- return [tree, scripts.js]
565
+ const data = Array.isArray(scripts) ? scripts : [scripts]
566
+ nodes = nodes.concat(data.map((script) => script.js))
602
567
  }
603
- return tree
568
+
569
+ return nodes
604
570
  }
605
571
  }
606
572
 
@@ -610,11 +576,9 @@ module.exports = {
610
576
  classes,
611
577
  doctype,
612
578
  escape: escapeHTML,
613
- fragment,
614
579
  raw,
615
580
  css,
616
581
  js,
617
- yaml,
618
582
  json,
619
583
  tag,
620
584
  i18n,
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "boxwood",
3
- "version": "0.75.0",
3
+ "version": "0.77.0",
4
4
  "description": "Compile HTML templates into JS",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "node --test",
7
+ "test": "node --test --test-reporter=dot",
8
+ "test:debug": "node --test --test-reporter=spec",
8
9
  "coverage": "c8 npm test",
9
10
  "benchmark": "node --test benchmark/index.js",
10
11
  "watch": "npm test -- --watch",
@@ -62,7 +63,6 @@
62
63
  },
63
64
  "dependencies": {
64
65
  "css-tree": "^2.3.1",
65
- "string-hash": "^1.1.3",
66
- "yaml": "^2.4.2"
66
+ "string-hash": "^1.1.3"
67
67
  }
68
68
  }
@@ -1,3 +0,0 @@
1
- module.exports = function () {
2
- return 'foo'
3
- }
@@ -1 +0,0 @@
1
- {}
@@ -1 +0,0 @@
1
- foo
@@ -1 +0,0 @@
1
- foo
@@ -1 +0,0 @@
1
- foo
@@ -1 +0,0 @@
1
- foo
@@ -1,3 +0,0 @@
1
- module.exports = function () {
2
- return 'foo'
3
- }
@@ -1,5 +0,0 @@
1
- const { div } = require("../../..")
2
-
3
- module.exports = function () {
4
- return div("foo")
5
- }
@@ -1 +0,0 @@
1
- {}
@@ -1 +0,0 @@
1
- <div>foo</div>
@@ -1 +0,0 @@
1
- <div>foo</div>
@@ -1 +0,0 @@
1
- <div>foo</div>
@@ -1 +0,0 @@
1
- <div>foo</div>
@@ -1,4 +0,0 @@
1
-
2
- module.exports = function () {
3
- return '<div>foo</div>'
4
- }
@@ -1,3 +0,0 @@
1
- module.exports = function ({ foo }) {
2
- return foo
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "foo": "<img>"
3
- }
@@ -1 +0,0 @@
1
- {{foo}}
@@ -1 +0,0 @@
1
- <%- foo %>
@@ -1 +0,0 @@
1
- {{foo}}
@@ -1 +0,0 @@
1
- <%- foo %>
@@ -1,5 +0,0 @@
1
- const { escape } = require("../../..")
2
-
3
- module.exports = function ({ foo }) {
4
- return escape(foo)
5
- }
@@ -1,56 +0,0 @@
1
- const {
2
- fragment,
3
- doctype,
4
- html,
5
- head,
6
- body,
7
- meta,
8
- img,
9
- title,
10
- div,
11
- ul,
12
- li,
13
- a,
14
- } = require("../../..")
15
-
16
- module.exports = function ({ friends }) {
17
- return fragment([
18
- doctype(),
19
- html({ lang: "en" }, [
20
- head([meta({ charset: "UTF-8" }), title("Friends")]),
21
- body([
22
- div(
23
- { class: "friends" },
24
- friends.map((friend) => {
25
- return div({ class: "friend" }, [
26
- ul([
27
- li(`Name: ${friend.name}`),
28
- li(`Balance: ${friend.balance}`),
29
- li(`Age: ${friend.age}`),
30
- li(`Address: ${friend.address}`),
31
- li(["Image: ", img({ src: friend.picture })]),
32
- li(`Company: ${friend.company}`),
33
- li([
34
- "Email: ",
35
- a({ href: `mailto:${friend.email}` }, friend.email),
36
- ]),
37
- li(`About: ${friend.about}`),
38
- friend.tags.length &&
39
- li(["Tags: ", ul(friend.tags.map((tag) => li(tag)))]),
40
- friend.friends.length &&
41
- li([
42
- "Friends: ",
43
- ul(
44
- friend.friends.map((friend) =>
45
- li(`${friend.name} (${friend.id})`)
46
- )
47
- ),
48
- ]),
49
- ]),
50
- ])
51
- })
52
- ),
53
- ]),
54
- ]),
55
- ])
56
- }
@@ -1,30 +0,0 @@
1
- {
2
- "friends": [
3
- {
4
- "name": "Jan Kowalski",
5
- "id": 1,
6
- "balance": "100 PLN",
7
- "age": 50,
8
- "address": "Warszawa",
9
- "picture": "jankowalski.png",
10
- "email": "jan@kowalski.pl",
11
- "about": "I like fishing",
12
- "company": "PKO",
13
- "tags": ["fisherman"],
14
- "friends": [
15
- {
16
- "name": "Piotr Nowak",
17
- "id": 2,
18
- "balance": "80 PLN",
19
- "age": 40,
20
- "address": "Kraków",
21
- "picture": "piotrnowak.png",
22
- "email": "piotr@nowak.pl",
23
- "about": "I like cooking",
24
- "company": "ING",
25
- "tags": ["chef"]
26
- }
27
- ]
28
- }
29
- ]
30
- }
@@ -1,45 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>Friends</title>
6
- </head>
7
- <body>
8
- <div class="friends">
9
- {{#each friends}}
10
- <div class="friend">
11
- <ul>
12
- <li>Name: {{this.name}}</li>
13
- <li>Balance: {{this.balance}}</li>
14
- <li>Age: {{this.age}}</li>
15
- <li>Address: {{this.address}}</li>
16
- <li>Image: <img src="{{this.picture}}"></li>
17
- <li>Company: {{this.company}}</li>
18
- <li>Email: <a href="mailto:{{this.email}}">{{this.email}}</a></li>
19
- <li>About: {{this.about}}</li>
20
- {{#if tags.length}}
21
- <li>
22
- Tags:
23
- <ul>
24
- {{#each tags}}
25
- <li>{{this}}</li>
26
- {{/each}}
27
- </ul>
28
- </li>
29
- {{/if}}
30
- {{#if friends.length}}
31
- <li>
32
- Friends:
33
- <ul>
34
- {{#each friends}}
35
- <li>{{this.name}} ({{this.id}})</li>
36
- {{/each}}
37
- </ul>
38
- </li>
39
- {{/if}}
40
- </ul>
41
- </div>
42
- {{/each}}
43
- </div>
44
- </body>
45
- </html>
@@ -1,48 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>Friends</title>
6
- </head>
7
- <body>
8
- <div class="friends">
9
- <% for (var i = 0, ilen = friends.length; i < ilen; i += 1) { %>
10
- <% var friend = friends[i]; %>
11
- <div class="friend">
12
- <ul>
13
- <li>Name: <%- friend.name %></li>
14
- <li>Balance: <%- friend.balance %></li>
15
- <li>Age: <%- friend.age %></li>
16
- <li>Address: <%- friend.address %></li>
17
- <li>Image: <img src="<%- friend.picture %>"></li>
18
- <li>Company: <%- friend.company %></li>
19
- <li>Email: <a href="mailto:<%- friend.email %>"><%- friend.email %></a></li>
20
- <li>About: <%- friend.about %></li>
21
- <% if (friend.tags.length) { %>
22
- <li>
23
- Tags:
24
- <ul>
25
- <% for (var j = 0, jlen = friend.tags.length; j < jlen; j += 1) { %>
26
- <% var tag = friend.tags[i]; %>
27
- <li><%- tag %></li>
28
- <% } %>
29
- </ul>
30
- </li>
31
- <% } %>
32
- <% if (friend.friends.length) { %>
33
- <li>
34
- Friends:
35
- <ul>
36
- <% for (var k = 0, klen = friend.friends.length; k < klen; k += 1) { %>
37
- <% var fr = friend.friends[i] %>
38
- <li><%- fr.name %> (<%- fr.id %>)</li>
39
- <% } %>
40
- </ul>
41
- </li>
42
- <% } %>
43
- </ul>
44
- </div>
45
- <% } %>
46
- </div>
47
- </body>
48
- </html>
@@ -1,45 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>Friends</title>
6
- </head>
7
- <body>
8
- <div class="friends">
9
- {{#friends}}
10
- <div class="friend">
11
- <ul>
12
- <li>Name: {{name}}</li>
13
- <li>Balance: {{balance}}</li>
14
- <li>Age: {{age}}</li>
15
- <li>Address: {{address}}</li>
16
- <li>Image: <img src="{{picture}}"></li>
17
- <li>Company: {{company}}</li>
18
- <li>Email: <a href="mailto:{{email}}">{{email}}</a></li>
19
- <li>About: {{about}}</li>
20
- {{#tags.length}}
21
- <li>
22
- Tags:
23
- <ul>
24
- {{#tags}}
25
- <li>{{.}}</li>
26
- {{/tags}}
27
- </ul>
28
- </li>
29
- {{/tags.length}}
30
- {{#friends.length}}
31
- <li>
32
- Friends:
33
- <ul>
34
- {{#friends}}
35
- <li>{{name}} ({{id}})</li>
36
- {{/friends}}
37
- </ul>
38
- </li>
39
- {{/friends.length}}
40
- </ul>
41
- </div>
42
- {{/friends}}
43
- </div>
44
- </body>
45
- </html>
@@ -1,48 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>Friends</title>
6
- </head>
7
- <body>
8
- <div class="friends">
9
- <% for (var i = 0, ilen = friends.length; i < ilen; i += 1) { %>
10
- <% var friend = friends[i]; %>
11
- <div class="friend">
12
- <ul>
13
- <li>Name: <%- friend.name %></li>
14
- <li>Balance: <%- friend.balance %></li>
15
- <li>Age: <%- friend.age %></li>
16
- <li>Address: <%- friend.address %></li>
17
- <li>Image: <img src="<%- friend.picture %>"></li>
18
- <li>Company: <%- friend.company %></li>
19
- <li>Email: <a href="mailto:<%- friend.email %>"><%- friend.email %></a></li>
20
- <li>About: <%- friend.about %></li>
21
- <% if (friend.tags.length) { %>
22
- <li>
23
- Tags:
24
- <ul>
25
- <% for (var j = 0, jlen = friend.tags.length; j < jlen; j += 1) { %>
26
- <% var tag = friend.tags[i]; %>
27
- <li><%- tag %></li>
28
- <% } %>
29
- </ul>
30
- </li>
31
- <% } %>
32
- <% if (friend.friends.length) { %>
33
- <li>
34
- Friends:
35
- <ul>
36
- <% for (var k = 0, klen = friend.friends.length; k < klen; k += 1) { %>
37
- <% var fr = friend.friends[i] %>
38
- <li><%- fr.name %> (<%- fr.id %>)</li>
39
- <% } %>
40
- </ul>
41
- </li>
42
- <% } %>
43
- </ul>
44
- </div>
45
- <% } %>
46
- </div>
47
- </body>
48
- </html>