coralite 0.5.0 → 0.6.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/bin/coralite.js CHANGED
@@ -1,17 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { render } from 'dom-serializer'
4
- import { getHTML, parseHTMLDocument, parseModule, createComponent, getSubDirectory } from '#lib'
3
+ import { getSubDirectory, getPkg, coralite } from '#lib'
5
4
  import { Command } from 'commander'
6
5
  import { join } from 'node:path'
7
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'
6
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
7
+ import kleur from 'kleur'
8
8
 
9
- /**
10
- * @import { CoraliteModule } from '#types'
11
- */
12
-
13
- const pkgPath = new URL('../package.json', import.meta.url)
14
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
9
+ const pkg = await getPkg()
15
10
  const program = new Command()
16
11
 
17
12
  program
@@ -21,7 +16,8 @@ program
21
16
  .requiredOption('-t, --templates <path>', 'Path to templates directory')
22
17
  .requiredOption('-p, --pages <path>', 'Path to pages directory')
23
18
  .requiredOption('-o, --output <path>', 'Output directory for the generated site')
24
- .option('-d, --dry', 'Run in dry-run mode')
19
+ .option('-i, --ignore-attribute <key=value...>', 'Ignore elements by attribute name value pair', [])
20
+ .option('-d, --dry-run', 'Run in dry-run mode')
25
21
 
26
22
  program.parse(process.argv)
27
23
  program.on('error', (err) => {
@@ -29,81 +25,52 @@ program.on('error', (err) => {
29
25
  })
30
26
 
31
27
  const options = program.opts()
32
- const templatesPath = options.templates
33
- const pagesPath = options.pages
34
- const outputDir = options.output
35
- const dryRun = options.dry
36
-
37
- const htmlTemplates = await getHTML({
38
- path: templatesPath,
39
- recursive: true
40
- })
41
- const htmlPages = await getHTML({
42
- path: pagesPath,
43
- recursive: true
44
- })
28
+ const pages = options.pages
29
+ const output = options.output
30
+ const ignoreByAttribute = []
45
31
 
46
- /** @type {Object.<string, CoraliteModule>} */
47
- const coraliteModules = {}
32
+ for (let i = 0; i < options.ignoreAttribute.length; i++) {
33
+ const pair = options.ignoreAttribute[i].split('=')
48
34
 
49
- // create templates
50
- for (let i = 0; i < htmlTemplates.length; i++) {
51
- const html = htmlTemplates[i]
52
- const coraliteModule = parseModule(html.content)
35
+ if (pair.length !== 2) {
36
+ throw new Error('Ignore attribute "' + pair[0] + '" expected a value but found none')
37
+ }
53
38
 
54
- coraliteModules[coraliteModule.id] = coraliteModule
39
+ ignoreByAttribute.push(pair)
55
40
  }
56
41
 
57
- for (let i = 0; i < htmlPages.length; i++) {
58
- const html = htmlPages[i]
59
- const document = parseHTMLDocument(html, {
60
- pages: pagesPath,
61
- templates: templatesPath
62
- })
63
-
64
- for (let i = 0; i < document.customElements.length; i++) {
65
- const customElement = document.customElements[i]
66
- const component = await createComponent({
67
- id: customElement.name,
68
- values: customElement.attribs,
69
- element: customElement,
70
- components: coraliteModules,
71
- document
72
- })
42
+ const documents = await coralite({
43
+ templates: options.templates,
44
+ pages,
45
+ ignoreByAttribute
46
+ })
73
47
 
74
- for (let i = 0; i < component.children.length; i++) {
75
- // update component parent
76
- component.children[i].parent = customElement.parent
77
- }
78
- const index = customElement.parent.children.indexOf(customElement, customElement.parentChildIndex)
79
- // replace custom element with template
80
- customElement.parent.children.splice(index, 1, ...component.children)
81
- }
48
+ if (options.dryRun) {
49
+ const PAD = ' '
50
+ const border = '─'.repeat(Math.min(process.stdout.columns, 36) / 2)
82
51
 
83
- // render document
84
- // @ts-ignore
85
- const content = render(document.root)
52
+ for (let i = 0; i < documents.length; i++) {
53
+ const document = documents[i]
86
54
 
87
- if (!dryRun) {
55
+ process.stdout.write('\n' + PAD + kleur.green('Document is ready!\n\n'))
56
+ process.stdout.write(PAD + `${kleur.bold('- Path:')} ${join(document.item.parentPath, document.item.name)}\n`)
57
+ process.stdout.write(PAD + `${kleur.bold('- Built in:')} ${Math.floor(document.duration)}ms\n\n`)
58
+ process.stdout.write(border + kleur.inverse(' Content start ') + border + '\n\n')
59
+ process.stdout.write(document.html)
60
+ process.stdout.write('\n\n' + border + kleur.inverse(' Content end ') + border + '\n')
61
+ }
62
+ } else {
63
+ for (let i = 0; i < documents.length; i++) {
64
+ const document = documents[i]
88
65
  // get pages sub directory
89
- const subDir = getSubDirectory(pagesPath, html.parentPath)
90
- const dir = join(outputDir, subDir)
91
-
92
- try {
93
- if (!existsSync(dir)) {
94
- // create directory
95
- mkdirSync(dir)
96
- }
66
+ const subDir = getSubDirectory(pages, document.item.parentPath)
67
+ const dir = join(output, subDir)
97
68
 
98
- writeFileSync(join(dir, html.name), content)
99
- // file written successfully
100
- } catch (err) {
101
- console.error(err)
69
+ if (!existsSync(dir)) {
70
+ // create directory
71
+ mkdirSync(dir)
102
72
  }
103
- } else {
104
- console.log('Document')
105
- console.log('Path: ' + join(html.parentPath, html.name))
106
- console.log('Content')
107
- console.log(content)
73
+
74
+ writeFileSync(join(dir, document.item.name), document.html)
108
75
  }
109
76
  }
package/eslint.config.js CHANGED
@@ -110,7 +110,8 @@ export default [
110
110
  {
111
111
  ignores: [
112
112
  '**/dist/',
113
- '**/.history/'
113
+ '**/.history/',
114
+ '**/playwright-report/'
114
115
  ]
115
116
  }
116
117
  ]
package/lib/coralite.js CHANGED
@@ -1,5 +1,13 @@
1
+ import { createComponent, parseHTMLDocument, parseModule, getHTML } from '#lib'
2
+ import render from 'dom-serializer'
3
+
1
4
  /**
2
- * @import { CoraliteElement, CoraliteTextNode } from '#types'
5
+ * @import {
6
+ * CoraliteElement,
7
+ * CoraliteTextNode,
8
+ * CoraliteAggregateTemplate,
9
+ * CoraliteAnyNode,
10
+ * CoraliteModule } from '#types'
3
11
  */
4
12
 
5
13
  /**
@@ -7,6 +15,11 @@
7
15
  * The HTML module code is run in the `parseScript` function in parse.js
8
16
  */
9
17
 
18
+ /**
19
+ * @callback DefineComponentSlot
20
+ * @param {CoraliteAnyNode[]} nodes
21
+ */
22
+
10
23
  /**
11
24
  * @type {Object.<string, string>}
12
25
  */
@@ -17,8 +30,8 @@ export const tokens = {}
17
30
  *
18
31
  * @param {Object} options
19
32
  * @param {string} [options.id] - Optional component id, if not defined, the id will be extracted from the first top level element with the id attribute
20
- * @param {Object.<string, (string | Function)>} options.tokens - Token names and values are either strings or functions representing the corresponding tokens' content or behavior.
21
- * @param {Object.<string, Function>} options.slots - Middleware for slot content
33
+ * @param {Object.<string, (string | Function)>} [options.tokens] - Token names and values are either strings or functions representing the corresponding tokens' content or behavior.
34
+ * @param {Object.<string, DefineComponentSlot>} [options.slots] - Middleware for slot content
22
35
  * @returns {Promise<Object.<string, string>>}
23
36
  */
24
37
  export async function defineComponent (options) {
@@ -30,10 +43,90 @@ export async function defineComponent (options) {
30
43
  * Aggregates HTML content from specified paths into a single collection of components.
31
44
  *
32
45
  * @param {Object} options - Configuration object for the aggregation process
33
- * @param {string} options.componentId - Unique identifier for the component used for each document
46
+ * @param {CoraliteAggregateTemplate | string} options.template - Templates used to display the result
34
47
  * @param {string} options.path - The path to aggregate, relative to pages directory
35
48
  * @returns {Promise<(CoraliteElement | CoraliteTextNode)[]>}
36
49
  */
37
50
  export async function aggregate (options) {
38
51
  return []
39
52
  }
53
+
54
+ /**
55
+ * @param {Object} options
56
+ * @param {string} options.templates
57
+ * @param {string} options.pages
58
+ * @param {Array<string[]>} [options.ignoreByAttribute]
59
+ */
60
+ export async function coralite ({
61
+ templates,
62
+ pages,
63
+ ignoreByAttribute
64
+ }) {
65
+ const startTime = performance.now()
66
+ const htmlTemplates = await getHTML({
67
+ path: templates,
68
+ recursive: true
69
+ })
70
+ const htmlPages = await getHTML({
71
+ path: pages,
72
+ recursive: true
73
+ })
74
+
75
+ /** @type {Object.<string, CoraliteModule>} */
76
+ const coraliteModules = {}
77
+ const documents = []
78
+
79
+ // create templates
80
+ for (let i = 0; i < htmlTemplates.length; i++) {
81
+ const html = htmlTemplates[i]
82
+ const coraliteModule = parseModule(html.content)
83
+
84
+ coraliteModules[coraliteModule.id] = coraliteModule
85
+ }
86
+
87
+ for (let i = 0; i < htmlPages.length; i++) {
88
+ const html = htmlPages[i]
89
+ const document = parseHTMLDocument(html, {
90
+ pages,
91
+ templates
92
+ }, ignoreByAttribute)
93
+
94
+ for (let i = 0; i < document.customElements.length; i++) {
95
+ const customElement = document.customElements[i]
96
+ const component = await createComponent({
97
+ id: customElement.name,
98
+ values: customElement.attribs,
99
+ element: customElement,
100
+ components: coraliteModules,
101
+ document
102
+ })
103
+
104
+ if (!component) {
105
+ // skip if no component
106
+ continue
107
+ }
108
+
109
+ for (let i = 0; i < component.children.length; i++) {
110
+ // update component parent
111
+ component.children[i].parent = customElement.parent
112
+ }
113
+ const index = customElement.parent.children.indexOf(customElement, customElement.parentChildIndex)
114
+ // replace custom element with template
115
+ customElement.parent.children.splice(index, 1, ...component.children)
116
+ }
117
+
118
+ // render document
119
+ // @ts-ignore
120
+ const result = render(document.root)
121
+
122
+ documents.push({
123
+ item: document,
124
+ html: result,
125
+ duration: performance.now() - startTime
126
+ })
127
+ }
128
+
129
+ return documents
130
+ }
131
+
132
+ export default coralite
package/lib/get-pkg.js ADDED
@@ -0,0 +1,22 @@
1
+ import { readFile } from 'node:fs/promises'
2
+
3
+ /**
4
+ * @typedef {Object} PackageJson
5
+ * @property {string} name
6
+ * @property {string} description
7
+ * @property {string} version
8
+ */
9
+
10
+ /**
11
+ * Get package.json
12
+ * @return {Promise<PackageJson>}
13
+ */
14
+ export async function getPkg () {
15
+ try {
16
+ const pkgPath = await readFile(new URL('../package.json', import.meta.url), { encoding: 'utf8' })
17
+
18
+ return JSON.parse(pkgPath)
19
+ } catch (err) {
20
+ console.error(err.message)
21
+ }
22
+ }
@@ -11,7 +11,8 @@ import { createComponent, parseHTMLMeta } from './parse.js'
11
11
  *
12
12
  * @param {Object} options - Configuration object for the aggregation process
13
13
  * @param {string} options.path - The path to aggregate, relative to pages directory
14
- * @param {string} options.componentId - Unique identifier for the component
14
+ * @param {Object} options.template - Templates used to display the result
15
+ * @param {string} options.template.item - Unique identifier for the component used for each document
15
16
  * @param {boolean} [options.recursive] - Whether to recursively search subdirectories
16
17
  * @param {CoraliteTokenOptions} [options.tokens] - Token configuration options
17
18
  * @param {CoraliteModuleValues} values - Default token values
@@ -24,7 +25,7 @@ import { createComponent, parseHTMLMeta } from './parse.js'
24
25
  * aggregate({
25
26
  * path: 'button',
26
27
  * recursive: true,
27
- * componentId: 'my-component'
28
+ * template: { item: 'my-component' }
28
29
  * }, {
29
30
  * className: 'btn'
30
31
  * }, components, document);
@@ -62,7 +63,7 @@ export async function aggregate (options, values, components, document) {
62
63
  }
63
64
 
64
65
  const component = await createComponent({
65
- id: options.componentId,
66
+ id: options.template.item,
66
67
  values: pageValues,
67
68
  components,
68
69
  document
package/lib/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import getHTML from './get-html.js'
2
+ import coralite from './coralite.js'
2
3
 
3
4
  export * from './parse.js'
4
5
  export * from './path-utils.js'
6
+ export * from './get-pkg.js'
5
7
 
6
8
  export {
7
- getHTML
9
+ getHTML,
10
+ coralite
8
11
  }
package/lib/parse.js CHANGED
@@ -18,6 +18,7 @@ import { invalidCustomTags, validTags } from './tags.js'
18
18
  * CoraliteDocumentRoot,
19
19
  * CoraliteContentNode,
20
20
  * CoraliteModuleValues,
21
+ * CoraliteAggregateTemplate
21
22
  * } from '#types'
22
23
  */
23
24
 
@@ -30,6 +31,7 @@ const customElementTagTokenRegExp = /^[^-].*[-._a-z0-9\u00B7\u00C0-\u00D6\u00D8-
30
31
  *
31
32
  * @param {HTMLData} html - The HTML data containing the content to parse
32
33
  * @param {CoralitePath} path - The path object containing the file path information
34
+ * @param {Array<string[]>} [ignoreByAttribute] - Ignore element with attribute name value pair
33
35
  * @returns {CoraliteDocument} An object representing the parsed document structure
34
36
  *
35
37
  * @example
@@ -50,13 +52,14 @@ const customElementTagTokenRegExp = /^[^-].*[-._a-z0-9\u00B7\u00C0-\u00D6\u00D8-
50
52
  * // document.root will contain parsed elements and text nodes
51
53
  * ```
52
54
  */
53
- export function parseHTMLDocument (html, path) {
55
+ export function parseHTMLDocument (html, path, ignoreByAttribute) {
54
56
  // root element reference
55
57
  /** @type {CoraliteDocumentRoot} */
56
58
  const root = {
57
59
  type: 'root',
58
60
  children: []
59
61
  }
62
+
60
63
  // stack to keep track of current element hierarchy
61
64
  /** @type {CoraliteContentNode[]} */
62
65
  const stack = [root]
@@ -74,6 +77,16 @@ export function parseHTMLDocument (html, path) {
74
77
  const parent = stack[stack.length - 1]
75
78
  const element = createElement(originalName, attributes, customElements, parent)
76
79
 
80
+ if (ignoreByAttribute) {
81
+ for (let i = 0; i < ignoreByAttribute.length; i++) {
82
+ const [key, value] = ignoreByAttribute[i]
83
+
84
+ if (attributes[key] && attributes[key].includes(value)) {
85
+ element.remove = true
86
+ }
87
+ }
88
+ }
89
+
77
90
  // push element to stack as it may have children
78
91
  stack.push(element)
79
92
  },
@@ -83,6 +96,15 @@ export function parseHTMLDocument (html, path) {
83
96
  createTextNode(text, parent)
84
97
  },
85
98
  onclosetag () {
99
+ const element = stack[stack.length - 1]
100
+
101
+ // @ts-ignore
102
+ if (element.remove) {
103
+ // remove element from tree
104
+ // @ts-ignore
105
+ element.parent.children.pop()
106
+ }
107
+
86
108
  // remove current element from stack as we're done with its children
87
109
  stack.pop()
88
110
  },
@@ -399,7 +421,7 @@ export function parseModule (string) {
399
421
  * @param {Object.<string, CoraliteModule>} options.components - Mapping of component IDs to their module definitions
400
422
  * @param {CoraliteElement} [options.element] - Mapping of component IDs to their module definitions
401
423
  * @param {CoraliteDocument} options.document - Current document being processed
402
- * @returns {Promise<CoraliteElement>}
424
+ * @returns {Promise<CoraliteElement | void>}
403
425
  *
404
426
  * @example
405
427
  * ```
@@ -431,7 +453,7 @@ export async function createComponent ({
431
453
  let component = components[id]
432
454
 
433
455
  if (!component) {
434
- throw new Error('Could not find component: "' + id +'"')
456
+ return console.warn('Could not find component "' + id +'" used in document "' + document.parentPath + '/' + document.name + '"')
435
457
  }
436
458
 
437
459
  component = structuredClone(component)
@@ -625,6 +647,7 @@ export async function createComponent ({
625
647
  */
626
648
  export async function parseScript (component, values, element, components, document) {
627
649
  const contextifiedObject = vm.createContext({
650
+ crypto,
628
651
  coralite: {
629
652
  tokens: values,
630
653
  /**
@@ -692,15 +715,31 @@ export async function parseScript (component, values, element, components, docum
692
715
  return values
693
716
  },
694
717
  /**
718
+ * @overload
695
719
  * @param {Object} options
696
- * @param {string} options.componentId
720
+ * @param {string} options.template - Templates used to display the result
721
+ * @param {string} options.path
722
+ */
723
+
724
+ /**
725
+ * @param {Object} options
726
+ * @param {CoraliteAggregateTemplate} options.template - Templates used to display the result
697
727
  * @param {string} options.path
698
728
  */
699
729
  async aggregate (options) {
700
- const component = components[options.componentId]
730
+ /** @type {string} */
731
+ let templateId
732
+
733
+ if (typeof options.template === 'string') {
734
+ templateId = options.template
735
+ } else {
736
+ templateId = options.template.item
737
+ }
738
+
739
+ const component = components[templateId]
701
740
 
702
741
  if (!component) {
703
- throw new Error('Aggregate: no component found by the id: ' + options.componentId)
742
+ throw new Error('Aggregate: no template found by the id: ' + templateId)
704
743
  }
705
744
 
706
745
  return await aggregate(options, values, components, document)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coralite",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "HTML modules static site generator",
5
5
  "main": "./lib/coralite.js",
6
6
  "type": "module",
@@ -31,7 +31,7 @@
31
31
  "test-e2e": "playwright test",
32
32
  "test-e2e-report": "playwright show-report",
33
33
  "test-e2e-ui": "playwright test --ui",
34
- "html": "node --experimental-vm-modules bin/coralite.js -t tests/fixtures/templates -p tests/fixtures/pages -o dist",
34
+ "html": "node --experimental-vm-modules bin/coralite.js -t tests/fixtures/templates -p tests/fixtures/pages -o dist -i 'data-dev=true'",
35
35
  "server": "sirv dist --dev --port 3000"
36
36
  },
37
37
  "bin": "bin/coralite.js",
@@ -40,8 +40,14 @@
40
40
  "#types": "./types/index.js"
41
41
  },
42
42
  "exports": {
43
- "default": "./lib/coralite.js",
44
- "types": "./types/index.js"
43
+ ".": {
44
+ "default": "./lib/coralite.js",
45
+ "types": "./types/index.js"
46
+ },
47
+ "./utils": {
48
+ "default": "./lib/index.js",
49
+ "types": "./types/index.js"
50
+ }
45
51
  },
46
52
  "devDependencies": {
47
53
  "@commitlint/cli": "^19.6.1",
@@ -50,6 +56,7 @@
50
56
  "@stylistic/eslint-plugin-js": "^2.12.1",
51
57
  "@stylistic/eslint-plugin-plus": "^2.12.1",
52
58
  "@types/node": "^22.10.5",
59
+ "kleur": "^4.1.5",
53
60
  "sirv-cli": "^3.0.0"
54
61
  },
55
62
  "engines": {
package/types/index.js CHANGED
@@ -72,6 +72,7 @@
72
72
  * @property {CoraliteContentNode} parent - Parent element
73
73
  * @property {number} [parentChildIndex] - Position in parent's child list
74
74
  * @property {Object[]} [slots]
75
+ * @property {boolean} [remove] - Mark element to be removed from stack
75
76
  */
76
77
 
77
78
 
@@ -123,3 +124,8 @@
123
124
  * @property {CoraliteElement[]} customElements - Custom elements defined in the document
124
125
  * @property {CoralitePath} path - Document's file path
125
126
  */
127
+
128
+ /**
129
+ * @typedef {Object} CoraliteAggregateTemplate - Templates used to display the result
130
+ * @property {string} item - Unique identifier for the component used for each document
131
+ */