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.
- package/index.js +237 -210
- package/package.json +5 -4
package/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
const { join } = require(
|
|
2
|
-
const { readFileSync } = require(
|
|
3
|
-
const csstree = require(
|
|
4
|
-
const toHash = require(
|
|
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) {
|
|
16
|
-
|
|
16
|
+
if (!node) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
if (node.name === "head") {
|
|
17
20
|
nodes.head = node
|
|
18
21
|
}
|
|
19
|
-
if (node.name ===
|
|
22
|
+
if (node.name === "body") {
|
|
20
23
|
nodes.body = node
|
|
21
24
|
}
|
|
22
|
-
if (node.name ===
|
|
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 ===
|
|
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:
|
|
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:
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
"'":
|
|
69
|
-
'"':
|
|
68
|
+
"&": "&",
|
|
69
|
+
"<": "<",
|
|
70
|
+
">": ">",
|
|
71
|
+
"'": "'",
|
|
72
|
+
'"': """,
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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:
|
|
121
|
-
htmlFor:
|
|
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 +
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 ![
|
|
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 ===
|
|
176
|
+
if (typeof input === "number") {
|
|
174
177
|
return input.toString()
|
|
175
178
|
}
|
|
176
|
-
if (typeof input ===
|
|
179
|
+
if (typeof input === "string") {
|
|
177
180
|
return escape ? escapeHTML(input) : input
|
|
178
181
|
}
|
|
179
|
-
if (input.name ===
|
|
182
|
+
if (input.name === "fragment") {
|
|
180
183
|
return render(input.children)
|
|
181
184
|
}
|
|
182
|
-
if (input.name ===
|
|
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
|
|
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:
|
|
219
|
+
return { name: "fragment", children }
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
const raw = (children) => {
|
|
216
|
-
return { name:
|
|
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,
|
|
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 ===
|
|
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(
|
|
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,
|
|
282
|
-
return css
|
|
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(
|
|
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,
|
|
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(
|
|
317
|
+
const doctype = node("!DOCTYPE html")
|
|
309
318
|
|
|
310
319
|
const nodes = [
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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 ===
|
|
443
|
+
if (type === "string") {
|
|
435
444
|
array.push(arg)
|
|
436
|
-
} else if (type ===
|
|
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.
|
|
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.
|
|
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": "^
|
|
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
|
}
|