coralite 0.0.0-development
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/.github/docs/continue-commit-convention.md +11 -0
- package/.github/workflows/publish.yml +59 -0
- package/.idea/coralite.iml +12 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/.vscode/settings.json +5 -0
- package/LICENSE +373 -0
- package/README.md +58 -0
- package/bin/coralite.js +104 -0
- package/commitlint.config.js +3 -0
- package/jsconfig.json +8 -0
- package/lib/coralite.js +38 -0
- package/lib/get-html.js +64 -0
- package/lib/html-module.js +75 -0
- package/lib/index.js +8 -0
- package/lib/parse.js +820 -0
- package/lib/path-utils.js +26 -0
- package/lib/tags.js +145 -0
- package/package.json +62 -0
- package/release.config.js +11 -0
- package/types/index.js +110 -0
package/lib/parse.js
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
import { Parser } from 'htmlparser2'
|
|
2
|
+
import { aggregate } from './html-module.js'
|
|
3
|
+
import vm from 'node:vm'
|
|
4
|
+
import { invalidCustomTags, validTags } from './tags.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @import {Module} from 'node:vm'
|
|
8
|
+
* @import {
|
|
9
|
+
* HTMLData,
|
|
10
|
+
* CoraliteDocument,
|
|
11
|
+
* CoralitePath,
|
|
12
|
+
* CoraliteToken,
|
|
13
|
+
* CoraliteModule,
|
|
14
|
+
* CoraliteTextNode,
|
|
15
|
+
* CoraliteElement,
|
|
16
|
+
* CoraliteSlotElement,
|
|
17
|
+
* CoraliteModuleSlotElement,
|
|
18
|
+
* CoraliteDocumentTokens,
|
|
19
|
+
* CoraliteDocumentRoot,
|
|
20
|
+
* CoraliteDirective} from '#types'
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const customElementTagRegExp = /^[^-].*[-._a-z0-9\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}]*$/ui
|
|
24
|
+
|
|
25
|
+
const customElementTagTokenRegExp = /^[^-].*[-._a-z0-9\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}\{\}]*$/ui
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses HTML content into a Coralite document structure.
|
|
29
|
+
*
|
|
30
|
+
* @param {HTMLData} html - The HTML data containing the content to parse
|
|
31
|
+
* @param {CoralitePath} path - The path object containing the file path information
|
|
32
|
+
* @returns {CoraliteDocument} An object representing the parsed document structure
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```javascript
|
|
36
|
+
* // Example usage:
|
|
37
|
+
* const html = {
|
|
38
|
+
* name: 'index.html',
|
|
39
|
+
* parentPath: 'path/to/parent',
|
|
40
|
+
* content: '<div>Content</div>'
|
|
41
|
+
* };
|
|
42
|
+
*
|
|
43
|
+
* const path = {
|
|
44
|
+
* pages: 'path/to/pages',
|
|
45
|
+
* components: 'path/to/components'
|
|
46
|
+
* };
|
|
47
|
+
*
|
|
48
|
+
* const document = parseHTMLDocument(html, path);
|
|
49
|
+
* // document.root will contain parsed elements and text nodes
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function parseHTMLDocument (html, path) {
|
|
53
|
+
// root element reference
|
|
54
|
+
/** @type {CoraliteDocumentRoot} */
|
|
55
|
+
const root = {
|
|
56
|
+
type: 'root',
|
|
57
|
+
children: []
|
|
58
|
+
}
|
|
59
|
+
const rootChildren = root.children
|
|
60
|
+
// stack to keep track of current element hierarchy
|
|
61
|
+
const stack = []
|
|
62
|
+
const customElements = []
|
|
63
|
+
/** @type {Object.<string, CoraliteSlotElement[]>} */
|
|
64
|
+
const customElementSlots = {}
|
|
65
|
+
|
|
66
|
+
const parser = new Parser({
|
|
67
|
+
onprocessinginstruction (name, data) {
|
|
68
|
+
rootChildren.push({
|
|
69
|
+
type: 'directive',
|
|
70
|
+
name,
|
|
71
|
+
data
|
|
72
|
+
})
|
|
73
|
+
},
|
|
74
|
+
onopentag (originalName, attributes) {
|
|
75
|
+
const element = createElement(originalName, attributes, customElements, stack, rootChildren)
|
|
76
|
+
|
|
77
|
+
if (attributes.slot) {
|
|
78
|
+
const customElement = customElements[customElements.length - 1]
|
|
79
|
+
const slot = {
|
|
80
|
+
name: attributes.slot,
|
|
81
|
+
customElement,
|
|
82
|
+
element
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!customElementSlots[customElement.name]) {
|
|
86
|
+
customElementSlots[customElement.name] = [slot]
|
|
87
|
+
} else {
|
|
88
|
+
customElementSlots[customElement.name].unshift(slot)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// remove slot attribute
|
|
92
|
+
delete attributes.slot
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
ontext (text) {
|
|
96
|
+
if (text.trim()) {
|
|
97
|
+
createTextNode(text, stack)
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
onclosetag () {
|
|
101
|
+
// remove current element from stack as we're done with its children
|
|
102
|
+
stack.pop()
|
|
103
|
+
},
|
|
104
|
+
oncomment (comment) {
|
|
105
|
+
stack[stack.length - 1].children.push({
|
|
106
|
+
type: 'comment',
|
|
107
|
+
data: comment,
|
|
108
|
+
parent: stack[stack.length - 1]
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
parser.write(html.content)
|
|
114
|
+
parser.end()
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
name: html.name,
|
|
118
|
+
parentPath: html.parentPath,
|
|
119
|
+
root,
|
|
120
|
+
customElements,
|
|
121
|
+
customElementSlots,
|
|
122
|
+
path
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parses HTML string containing meta tags and extracts associated metadata.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} string - HTML content containing meta tags
|
|
130
|
+
* @returns {Object.<string, CoraliteToken[]>}
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```javascript
|
|
134
|
+
* // Example usage:
|
|
135
|
+
* const html = `<meta name="title" content="Finding Nemo">`;
|
|
136
|
+
* const meta = parseHTMLMeta(html);
|
|
137
|
+
*
|
|
138
|
+
* // Output will be an object like:
|
|
139
|
+
* //{
|
|
140
|
+
* // "title": [ { name: 'title', content: 'Finding Nemo' } ],
|
|
141
|
+
* //}
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export function parseHTMLMeta (string) {
|
|
145
|
+
// stack to keep track of current element hierarchy
|
|
146
|
+
const stack = []
|
|
147
|
+
/** @type {Object.<string, CoraliteToken[]>} */
|
|
148
|
+
const meta = {}
|
|
149
|
+
|
|
150
|
+
const parser = new Parser({
|
|
151
|
+
onopentag (name, attributes) {
|
|
152
|
+
if (name === 'meta') {
|
|
153
|
+
if (attributes.content) {
|
|
154
|
+
if (attributes.property) {
|
|
155
|
+
addMetadata(meta, attributes.property, attributes.content)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (attributes.name) {
|
|
159
|
+
addMetadata(meta, attributes.name, attributes.content)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
onclosetag (name) {
|
|
165
|
+
if (name === 'head') {
|
|
166
|
+
// end on closing head tag
|
|
167
|
+
return parser.end()
|
|
168
|
+
}
|
|
169
|
+
// remove current element from stack as we're done with its children
|
|
170
|
+
stack.pop()
|
|
171
|
+
},
|
|
172
|
+
oncomment (comment) {
|
|
173
|
+
stack[stack.length - 1].children.push({
|
|
174
|
+
type: 'comment',
|
|
175
|
+
data: comment,
|
|
176
|
+
parent: stack[stack.length - 1]
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
parser.write(string)
|
|
182
|
+
parser.end()
|
|
183
|
+
|
|
184
|
+
return meta
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Parses HTML string containing meta tags or generates Coralite module structure from markup.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} string - HTML content containing meta tags or module markup
|
|
192
|
+
* @returns {CoraliteModule} - Parsed module information, including template, script, tokens, and slot configurations
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```javascript
|
|
196
|
+
* // Example usage:
|
|
197
|
+
* const html = `<template id="home">
|
|
198
|
+
* <slot name="default">Hello</slot>
|
|
199
|
+
* </template>`;
|
|
200
|
+
* const module = parseModule(html);
|
|
201
|
+
*
|
|
202
|
+
* // Module object structure will be:
|
|
203
|
+
* //{
|
|
204
|
+
* // id: 'home',
|
|
205
|
+
* // template: { ... },
|
|
206
|
+
* // script: undefined,
|
|
207
|
+
* // tokens: [],
|
|
208
|
+
* // customElements: [],
|
|
209
|
+
* // slotElements: {
|
|
210
|
+
* // 'home': {
|
|
211
|
+
* // 'default': {
|
|
212
|
+
* // name: 'slot',
|
|
213
|
+
* // element: {}
|
|
214
|
+
* // }
|
|
215
|
+
* // }
|
|
216
|
+
* //}
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export function parseModule (string) {
|
|
220
|
+
// root element reference
|
|
221
|
+
const root = []
|
|
222
|
+
// stack to keep track of current element hierarchy
|
|
223
|
+
const stack = []
|
|
224
|
+
const customElements = []
|
|
225
|
+
/** @type {Object.<string, Object.<string,CoraliteModuleSlotElement>>} */
|
|
226
|
+
const slotElements = {}
|
|
227
|
+
|
|
228
|
+
/** @type {CoraliteDocumentTokens} */
|
|
229
|
+
const documentTokens = {
|
|
230
|
+
attributes: [],
|
|
231
|
+
textNodes: []
|
|
232
|
+
}
|
|
233
|
+
let templateId = ''
|
|
234
|
+
|
|
235
|
+
const parser = new Parser({
|
|
236
|
+
onopentag (originalName, attributes) {
|
|
237
|
+
const element = createElement(originalName, attributes, customElements, stack, root)
|
|
238
|
+
const attributeNames = Object.keys(attributes)
|
|
239
|
+
|
|
240
|
+
// collect tokens
|
|
241
|
+
if (attributeNames.length) {
|
|
242
|
+
for (let i = 0; i < attributeNames.length; i++) {
|
|
243
|
+
const name = attributeNames[i]
|
|
244
|
+
const tokens = getTokensFromString(attributes[name])
|
|
245
|
+
|
|
246
|
+
// store attribute tokens
|
|
247
|
+
if (tokens.length) {
|
|
248
|
+
documentTokens.attributes.push({
|
|
249
|
+
name,
|
|
250
|
+
tokens,
|
|
251
|
+
element
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (element.name === 'template') {
|
|
258
|
+
if (!attributes.id) {
|
|
259
|
+
throw new Error('Template requires an "id"')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let idHasToken = false
|
|
263
|
+
// check if template id contains a token
|
|
264
|
+
for (let i = 0; i < documentTokens.attributes.length; i++) {
|
|
265
|
+
const token = documentTokens.attributes[i]
|
|
266
|
+
|
|
267
|
+
if (token.name === 'id') {
|
|
268
|
+
idHasToken = true
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!customElementTagRegExp.test(attributes.id)
|
|
274
|
+
|| (idHasToken && !customElementTagTokenRegExp.test(attributes.id))) {
|
|
275
|
+
throw new Error('Invalid template id: "' + originalName + '" it must match following the pattern https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
templateId = attributes.id
|
|
279
|
+
} else if (element.name === 'slot') {
|
|
280
|
+
const name = attributes.name || 'default'
|
|
281
|
+
|
|
282
|
+
if (slotElements[templateId] && slotElements[templateId][name]) {
|
|
283
|
+
throw new Error('Slot names must be unique: "' + name + '"')
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const slot = {
|
|
287
|
+
name,
|
|
288
|
+
element
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!slotElements[templateId]) {
|
|
292
|
+
slotElements[templateId] = {
|
|
293
|
+
[name]: slot
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
slotElements[templateId][name] = slot
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
ontext (text) {
|
|
301
|
+
if (text.trim()) {
|
|
302
|
+
const textNode = createTextNode(text, stack)
|
|
303
|
+
const tokens = getTokensFromString(text)
|
|
304
|
+
|
|
305
|
+
// store tokens
|
|
306
|
+
if (tokens.length) {
|
|
307
|
+
documentTokens.textNodes.push({
|
|
308
|
+
tokens,
|
|
309
|
+
textNode
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
onclosetag () {
|
|
315
|
+
// remove current element from stack as we're done with its children
|
|
316
|
+
stack.pop()
|
|
317
|
+
},
|
|
318
|
+
oncdatastart (csb) {
|
|
319
|
+
console.log(csb)
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
oncomment (comment) {
|
|
323
|
+
stack[stack.length - 1].children.push({
|
|
324
|
+
type: 'comment',
|
|
325
|
+
data: comment,
|
|
326
|
+
parent: stack[stack.length - 1]
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
parser.write(string)
|
|
332
|
+
parser.end()
|
|
333
|
+
|
|
334
|
+
/** @type {CoraliteElement} */
|
|
335
|
+
let template
|
|
336
|
+
let script
|
|
337
|
+
|
|
338
|
+
for (let i = 0; i < root.length; i++) {
|
|
339
|
+
const node = root[i]
|
|
340
|
+
|
|
341
|
+
if (node.type === 'text') {
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (node.name === 'template') {
|
|
346
|
+
if (template) {
|
|
347
|
+
throw new Error('One template element is permitted')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
template = node
|
|
351
|
+
|
|
352
|
+
} else if (node.name == 'script') {
|
|
353
|
+
if (node.attribs.type !== 'module') {
|
|
354
|
+
throw new Error('Script tag must contain the `type="module"` attribute')
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
script = node.children[0].data
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!template) {
|
|
362
|
+
throw new Error('Template element is missing')
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
id: template.attribs.id,
|
|
367
|
+
template,
|
|
368
|
+
script,
|
|
369
|
+
tokens: documentTokens,
|
|
370
|
+
customElements,
|
|
371
|
+
slotElements
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @param {Object} options
|
|
377
|
+
* @param {string} options.id - id - Unique identifier for the component
|
|
378
|
+
* @param {Object.<string, (string | (CoraliteTextNode | CoraliteElement)[])>} options.values - Token values available for replacement
|
|
379
|
+
* @param {Object.<string, CoraliteModuleSlotElement[]>} [options.customElementSlots = {}] - Custom slots and their configurations for the component
|
|
380
|
+
* @param {Object.<string, CoraliteModule>} options.components - Mapping of component IDs to their module definitions
|
|
381
|
+
* @param {CoraliteDocument} options.document - Current document being processed
|
|
382
|
+
* @returns {Promise<CoraliteElement>}
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```javascript
|
|
386
|
+
* // Example usage:
|
|
387
|
+
* const component = await createComponent({
|
|
388
|
+
* id: 'my-component',
|
|
389
|
+
* values: {
|
|
390
|
+
* 'some-token': 'value-for-token'
|
|
391
|
+
* },
|
|
392
|
+
* customElementSlots: {
|
|
393
|
+
* 'slot-name': [
|
|
394
|
+
* { name: 'sub-slot', element: 'p' }
|
|
395
|
+
* ]
|
|
396
|
+
* },
|
|
397
|
+
* components: {
|
|
398
|
+
* 'my-component': {
|
|
399
|
+
* id: 'my-component',
|
|
400
|
+
* template: someTemplate,
|
|
401
|
+
* script: someScript,
|
|
402
|
+
* tokens: someTokens
|
|
403
|
+
* }
|
|
404
|
+
* },
|
|
405
|
+
* document: documentInstance
|
|
406
|
+
* })
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
export async function createComponent ({
|
|
410
|
+
id,
|
|
411
|
+
values,
|
|
412
|
+
customElementSlots = {},
|
|
413
|
+
components,
|
|
414
|
+
document
|
|
415
|
+
}) {
|
|
416
|
+
let component = components[id]
|
|
417
|
+
|
|
418
|
+
if (!component) {
|
|
419
|
+
throw new Error('Could not find component: "' + id +'"')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
component = structuredClone(component)
|
|
423
|
+
const template = component.template
|
|
424
|
+
const computedTokens = []
|
|
425
|
+
|
|
426
|
+
for (let i = 0; i < component.tokens.attributes.length; i++) {
|
|
427
|
+
const item = component.tokens.attributes[i]
|
|
428
|
+
|
|
429
|
+
for (let i = 0; i < item.tokens.length; i++) {
|
|
430
|
+
const token = item.tokens[i]
|
|
431
|
+
let value = values[token.name]
|
|
432
|
+
|
|
433
|
+
if (value == null) {
|
|
434
|
+
if (component.script) {
|
|
435
|
+
computedTokens.push({
|
|
436
|
+
type: 'element',
|
|
437
|
+
node: item.element,
|
|
438
|
+
name: token.name,
|
|
439
|
+
content: token.content
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
continue
|
|
443
|
+
} else {
|
|
444
|
+
console.error('Token "' + token.name +'" was empty used on "' + component.id + '"')
|
|
445
|
+
value = ''
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// replace token with value
|
|
450
|
+
item.element.attribs[item.name] = item.element.attribs[item.name].replace(token.content, value)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (let i = 0; i < component.tokens.textNodes.length; i++) {
|
|
455
|
+
const item = component.tokens.textNodes[i]
|
|
456
|
+
|
|
457
|
+
for (let i = 0; i < item.tokens.length; i++) {
|
|
458
|
+
const token = item.tokens[i]
|
|
459
|
+
let value = values[token.name]
|
|
460
|
+
|
|
461
|
+
if (value == null) {
|
|
462
|
+
if (component.script) {
|
|
463
|
+
computedTokens.push({
|
|
464
|
+
type: 'textNode',
|
|
465
|
+
node: item.textNode,
|
|
466
|
+
name: token.name,
|
|
467
|
+
content: token.content
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
continue
|
|
471
|
+
} else {
|
|
472
|
+
console.error('Token "' + token.name +'" was empty used on "' + component.id + '"')
|
|
473
|
+
value = ''
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// replace token with value
|
|
478
|
+
item.textNode.data = item.textNode.data.replace(token.content, value)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// merge values from component script
|
|
483
|
+
if (component.script) {
|
|
484
|
+
const computedValues = await parseScript(component, values, components, document)
|
|
485
|
+
|
|
486
|
+
values = Object.assign(values, computedValues)
|
|
487
|
+
|
|
488
|
+
for (let i = 0; i < computedTokens.length; i++) {
|
|
489
|
+
const computedToken = computedTokens[i]
|
|
490
|
+
const node = computedToken.node
|
|
491
|
+
const value = values[computedToken.name]
|
|
492
|
+
|
|
493
|
+
if (computedToken.type === 'attribute') {
|
|
494
|
+
// replace token string
|
|
495
|
+
node.attribs[computedToken.name] = node.attribs[computedToken.name].replace(computedToken.content, value)
|
|
496
|
+
} else {
|
|
497
|
+
if (Array.isArray(value)) {
|
|
498
|
+
// inject nodes
|
|
499
|
+
const textSplit = node.data.split(computedToken.content)
|
|
500
|
+
const childIndex = node.parent.children.indexOf(node)
|
|
501
|
+
|
|
502
|
+
node.parent.children.splice(childIndex, 1,
|
|
503
|
+
{
|
|
504
|
+
type: 'text',
|
|
505
|
+
data: textSplit[0],
|
|
506
|
+
parent: node.parent
|
|
507
|
+
},
|
|
508
|
+
...value,
|
|
509
|
+
{
|
|
510
|
+
type: 'text',
|
|
511
|
+
data: textSplit[1],
|
|
512
|
+
parent: node.parent
|
|
513
|
+
}
|
|
514
|
+
)
|
|
515
|
+
} else {
|
|
516
|
+
// replace token string
|
|
517
|
+
node.data = node.data.replace(computedToken.content, value)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// replace nested customElements
|
|
524
|
+
const customElements = component.customElements
|
|
525
|
+
let childIndex
|
|
526
|
+
|
|
527
|
+
for (let i = 0; i < customElements.length; i++) {
|
|
528
|
+
const customElement = customElements[i]
|
|
529
|
+
const component = await createComponent({
|
|
530
|
+
id: customElement.name,
|
|
531
|
+
values: Object.assign(values, customElement.attribs),
|
|
532
|
+
customElementSlots,
|
|
533
|
+
components,
|
|
534
|
+
document
|
|
535
|
+
})
|
|
536
|
+
const children = customElement.parent.children
|
|
537
|
+
|
|
538
|
+
if (!childIndex) {
|
|
539
|
+
childIndex = customElement.parentChildIndex
|
|
540
|
+
} else {
|
|
541
|
+
childIndex = children.indexOf(customElement, customElement.parentChildIndex)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// replace custom element with component
|
|
545
|
+
children.splice(childIndex, 1, ...component.children)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const slots = customElementSlots[id]
|
|
549
|
+
|
|
550
|
+
if (slots) {
|
|
551
|
+
const componentSlots = component.slotElements[id]
|
|
552
|
+
const usedSlots = {}
|
|
553
|
+
const slotIndexes = {}
|
|
554
|
+
|
|
555
|
+
for (let i = 0; i < slots.length; i++) {
|
|
556
|
+
const slot = slots[i]
|
|
557
|
+
const name = slot.name
|
|
558
|
+
|
|
559
|
+
if (componentSlots[name]) {
|
|
560
|
+
const componentSlot = componentSlots[name]
|
|
561
|
+
const parentChildren = componentSlot.element.parent.children
|
|
562
|
+
let slotIndex = slotIndexes[name]
|
|
563
|
+
let deleteCount = 0
|
|
564
|
+
|
|
565
|
+
if (slotIndex == null) {
|
|
566
|
+
slotIndex = parentChildren.indexOf(componentSlot.element, componentSlot.element.parentChildIndex)
|
|
567
|
+
deleteCount = 1
|
|
568
|
+
slotIndexes[name] = slotIndex
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
parentChildren.splice(slotIndex, deleteCount, slot.element)
|
|
572
|
+
usedSlots[slot.name] = true
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
for (const name in componentSlots) {
|
|
577
|
+
if (Object.prototype.hasOwnProperty.call(componentSlots, name)) {
|
|
578
|
+
if (!usedSlots[name]) {
|
|
579
|
+
const componentSlot = componentSlots[name]
|
|
580
|
+
const element = componentSlot.element
|
|
581
|
+
const parent = element.parent
|
|
582
|
+
const children = element.children || [{
|
|
583
|
+
type: 'text',
|
|
584
|
+
data: ''
|
|
585
|
+
}]
|
|
586
|
+
const slotIndex = parent.children.indexOf(componentSlot.element, componentSlot.element.parentChildIndex)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
for (let i = 0; i < children.length; i++) {
|
|
590
|
+
const childNode = children[i]
|
|
591
|
+
childNode.parent = parent
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// replace unused slots with default values
|
|
595
|
+
parent.children.splice(slotIndex, 1, ...children)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return template
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Parses a Coralite module script and compiles it into JavaScript.
|
|
606
|
+
*
|
|
607
|
+
* @param {CoraliteModule} component - The Coralite module to parse
|
|
608
|
+
* @param {Object.<string, string>} values - Replacement tokens for the component
|
|
609
|
+
* @param {Object.<string, CoraliteModule>} components - Mapping of other components that might be referenced
|
|
610
|
+
* @param {CoraliteDocument} document - The current document being processed
|
|
611
|
+
* @returns {Promise<Object.<string,(string|(CoraliteElement|CoraliteTextNode)[])>>}
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* ```javascript
|
|
615
|
+
* // Example usage:
|
|
616
|
+
* const parsedResult = await parseScript({
|
|
617
|
+
* id: 'my-component',
|
|
618
|
+
* template: `<div>Hello World</div>`,
|
|
619
|
+
* script: `export default function () {
|
|
620
|
+
* return '<div>Hello World</div>';
|
|
621
|
+
* }`,
|
|
622
|
+
* tokens: { 'hello': 'Hello' }
|
|
623
|
+
* }, {
|
|
624
|
+
* 'my-component': {
|
|
625
|
+
* id: 'my-component',
|
|
626
|
+
* template: `<div>${tokens.hello}</div>`,
|
|
627
|
+
* script: `export default function () {
|
|
628
|
+
* return '<div>Hello</div>';
|
|
629
|
+
* }`
|
|
630
|
+
* }
|
|
631
|
+
* }, document);
|
|
632
|
+
* ```
|
|
633
|
+
*/
|
|
634
|
+
export async function parseScript (component, values, components, document) {
|
|
635
|
+
const contextifiedObject = vm.createContext({
|
|
636
|
+
coralite: {
|
|
637
|
+
tokens: values,
|
|
638
|
+
/**
|
|
639
|
+
* @param {Object} options
|
|
640
|
+
* @param {Object.<string, (string | function)>} options.tokens
|
|
641
|
+
* @returns {Promise<Object.<string, string>>}
|
|
642
|
+
*/
|
|
643
|
+
async defineComponent (options) {
|
|
644
|
+
/** @type {Object.<string, string>} */
|
|
645
|
+
const values = {}
|
|
646
|
+
|
|
647
|
+
if (options.tokens) {
|
|
648
|
+
for (const key in options.tokens) {
|
|
649
|
+
if (Object.prototype.hasOwnProperty.call(options.tokens, key)) {
|
|
650
|
+
const token = options.tokens[key]
|
|
651
|
+
|
|
652
|
+
if (typeof token === 'function') {
|
|
653
|
+
values[key] = await token()
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return values
|
|
660
|
+
},
|
|
661
|
+
/**
|
|
662
|
+
* @param {Object} options
|
|
663
|
+
* @param {string} options.componentId
|
|
664
|
+
* @param {string} options.path
|
|
665
|
+
*/
|
|
666
|
+
async aggregate (options) {
|
|
667
|
+
const component = components[options.componentId]
|
|
668
|
+
|
|
669
|
+
if (!component) {
|
|
670
|
+
throw new Error('Aggregate: no component found by the id: ' + options.componentId)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return await aggregate(options, values, components, document)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
const script = new vm.SourceTextModule(component.script, {
|
|
679
|
+
context: contextifiedObject
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
await script.link(moduleLinker)
|
|
683
|
+
await script.evaluate()
|
|
684
|
+
|
|
685
|
+
// @ts-ignore
|
|
686
|
+
if (script.namespace.default) {
|
|
687
|
+
// @ts-ignore
|
|
688
|
+
return await script.namespace.default
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
throw new Error('Script found module export: "' + component.id + '"')
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* @param {string} specifier - The specifier of the requested module
|
|
696
|
+
* @param {Module} referencingModule - The Module object link() is called on.
|
|
697
|
+
*/
|
|
698
|
+
async function moduleLinker (specifier, referencingModule) {
|
|
699
|
+
if (specifier === 'coralite') {
|
|
700
|
+
return new vm.SourceTextModule(`
|
|
701
|
+
export const tokens = coralite.tokens
|
|
702
|
+
export const defineComponent = coralite.defineComponent
|
|
703
|
+
export const aggregate = coralite.aggregate
|
|
704
|
+
export default { tokens, defineComponent, aggregate };
|
|
705
|
+
`, { context: referencingModule.context })
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
throw new Error(`Unable to resolve dependency: ${specifier}`)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Extract attributes from string
|
|
713
|
+
* @param {string} string
|
|
714
|
+
* @returns {CoraliteToken[]}
|
|
715
|
+
*/
|
|
716
|
+
function getTokensFromString (string) {
|
|
717
|
+
const matches = string.matchAll(/\{{[^}]*\}}/g)
|
|
718
|
+
const result = []
|
|
719
|
+
|
|
720
|
+
for (const match of matches) {
|
|
721
|
+
const token = match[0]
|
|
722
|
+
|
|
723
|
+
result.push({
|
|
724
|
+
name: token.slice(2, token.length -2).trim(),
|
|
725
|
+
content: token
|
|
726
|
+
})
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return result
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* In place add metadata to meta
|
|
734
|
+
* @param {Object.<string, CoraliteToken[]>} meta
|
|
735
|
+
* @param {string} name
|
|
736
|
+
* @param {string} content
|
|
737
|
+
*/
|
|
738
|
+
function addMetadata (meta, name, content) {
|
|
739
|
+
let entry = meta[name]
|
|
740
|
+
|
|
741
|
+
if (!entry) {
|
|
742
|
+
meta[name] = []
|
|
743
|
+
entry = meta[name]
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// add og graph
|
|
747
|
+
entry.push({
|
|
748
|
+
name,
|
|
749
|
+
content
|
|
750
|
+
})
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* @param {string} name
|
|
755
|
+
* @param {Object.<string, string>} attributes
|
|
756
|
+
* @param {CoraliteElement[]} customElements
|
|
757
|
+
* @param {CoraliteElement[]} stack
|
|
758
|
+
* @param {(CoraliteTextNode | CoraliteElement | CoraliteDirective)[]} root
|
|
759
|
+
*/
|
|
760
|
+
function createElement (name, attributes, customElements, stack, root) {
|
|
761
|
+
const sanitisedName = name.toLowerCase()
|
|
762
|
+
const parentIndex = stack.length - 1
|
|
763
|
+
/** @type {CoraliteElement} */
|
|
764
|
+
const element = {
|
|
765
|
+
type: 'tag',
|
|
766
|
+
name: sanitisedName,
|
|
767
|
+
attribs: attributes,
|
|
768
|
+
children: []
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (!validTags[sanitisedName]) {
|
|
772
|
+
if (invalidCustomTags[sanitisedName]) {
|
|
773
|
+
throw new Error('Element name is reserved: "'+ sanitisedName +'"')
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (customElementTagRegExp.test(sanitisedName)) {
|
|
777
|
+
// store custom elements
|
|
778
|
+
customElements.push(element)
|
|
779
|
+
} else {
|
|
780
|
+
throw new Error('Invalid custom element tag name: "' + sanitisedName + '" https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name')
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (parentIndex === -1) {
|
|
785
|
+
root.push(element)
|
|
786
|
+
} else {
|
|
787
|
+
const parent = stack[parentIndex]
|
|
788
|
+
|
|
789
|
+
element.parent = parent
|
|
790
|
+
element.parentChildIndex = parent.children.length
|
|
791
|
+
|
|
792
|
+
// add element to its parent's children
|
|
793
|
+
parent.children.push(element)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// push element to stack as it may have children
|
|
797
|
+
stack.push(element)
|
|
798
|
+
|
|
799
|
+
return element
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* @param {string} text
|
|
804
|
+
* @param {CoraliteElement[]} stack
|
|
805
|
+
* @returns {CoraliteTextNode}
|
|
806
|
+
*/
|
|
807
|
+
function createTextNode (text, stack) {
|
|
808
|
+
// store if contains data
|
|
809
|
+
const parentIndex = stack.length - 1
|
|
810
|
+
const textNode = {
|
|
811
|
+
type: 'text',
|
|
812
|
+
data: text,
|
|
813
|
+
parent: stack[parentIndex]
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
stack[parentIndex].children.push(textNode)
|
|
817
|
+
|
|
818
|
+
return textNode
|
|
819
|
+
}
|
|
820
|
+
|