js-template-engine 1.0.1 → 2.0.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/examples/slots.js DELETED
@@ -1,103 +0,0 @@
1
- // Example of using the TemplateEngine class to render a simple HTML template with slots.
2
- // You can run this example with `npm run example:slots` or `yarn example:slots`.
3
-
4
- const { TemplateEngine } = require("../src");
5
- const verbose = true;
6
-
7
- const templateEngine = new TemplateEngine();
8
-
9
- // Data
10
- const stylesheetEntries = [
11
- { href: "style.css", rel: "stylesheet" },
12
- { href: "theme.css", rel: "stylesheet" },
13
- ];
14
-
15
- const scriptEntries = [
16
- { src: "app.js", defer: true },
17
- { src: "analytics.js", async: true },
18
- ];
19
-
20
- // Templates
21
- const htmlTemplate = [
22
- {
23
- tag: "html",
24
- attributes: {
25
- lang: "en",
26
- },
27
- children: [
28
- {
29
- tag: "head",
30
- children: [
31
- {
32
- tag: "meta",
33
- attributes: {
34
- charset: "UTF-8",
35
- },
36
- },
37
- {
38
- tag: "title",
39
- children: [
40
- {
41
- type: "text",
42
- content: "Hello, World!",
43
- },
44
- ],
45
- },
46
- {
47
- type: "slot",
48
- name: "head-end",
49
- },
50
- ],
51
- },
52
- {
53
- tag: "body",
54
- children: [
55
- {
56
- type: "slot",
57
- name: "body-beginning",
58
- },
59
- {
60
- tag: "h1",
61
- attributes: {
62
- class: "title",
63
- },
64
- children: [
65
- {
66
- type: "text",
67
- content: "Hello, World!",
68
- },
69
- ],
70
- },
71
- {
72
- type: "slot",
73
- name: "body-end",
74
- },
75
- ],
76
- },
77
- ],
78
- },
79
- ];
80
-
81
- // Template formatting functions
82
- const stylesheets = stylesheetEntries.map((stylesheetEntry) => ({
83
- tag: "link",
84
- attributes: { ...stylesheetEntry },
85
- }));
86
-
87
- const scripts = scriptEntries.map((script) => ({
88
- tag: "script",
89
- attributes: { ...script },
90
- }));
91
-
92
- // Render
93
- (async () => {
94
- await templateEngine.render(htmlTemplate, {
95
- name: "slots",
96
- slots: {
97
- "head-end": stylesheets,
98
- "body-end": scripts,
99
- },
100
- writeOutputFile: true,
101
- verbose,
102
- });
103
- })();
@@ -1,249 +0,0 @@
1
- const path = require("path");
2
- const prettier = require("prettier");
3
- const { writeOutputFile } = require("../handlers/FileHandler");
4
- const createLogger = require("../helpers/createLogger");
5
-
6
- const selfClosingTags = [
7
- "area",
8
- "base",
9
- "br",
10
- "col",
11
- "command",
12
- "embed",
13
- "hr",
14
- "img",
15
- "input",
16
- "keygen",
17
- "link",
18
- "meta",
19
- "param",
20
- "source",
21
- "track",
22
- "wbr",
23
- ];
24
-
25
- class TemplateEngine {
26
- constructor() {}
27
-
28
- mergeOptions = (options) => {
29
- let defaultOptions = {
30
- attributeFormatter: (attribute, value) => ` ${attribute}="${value}"`,
31
- fileExtension: ".html",
32
- filename: options.name ?? "untitled",
33
- outputDir: "dist",
34
- preferSelfClosingTags: false,
35
- prettierParser: "html",
36
- writeOutputFile: false,
37
- verbose: false,
38
- };
39
-
40
- if (options.extensions) {
41
- options.extensions.forEach((extension) => {
42
- if (extension.optionsHandler) {
43
- defaultOptions = extension.optionsHandler(defaultOptions, options);
44
- }
45
- });
46
- }
47
-
48
- return { ...defaultOptions, ...options };
49
- };
50
-
51
- applyExtensionOverrides = (node, currentExtensionKey) => {
52
- if (node.extensions && node.extensions[currentExtensionKey]) {
53
- const extensionOverrides = node.extensions[currentExtensionKey];
54
-
55
- Object.keys(extensionOverrides).forEach((key) => {
56
- if (key === "ignore") {
57
- // special handling for 'ignore' or other meta properties if necessary
58
- } else {
59
- node[key] = extensionOverrides[key];
60
- }
61
- });
62
- }
63
-
64
- return node;
65
- };
66
-
67
- render = async (
68
- nodes,
69
- options = {},
70
- isRoot = true,
71
- ancestorNodesContext = []
72
- ) => {
73
- options = isRoot ? this.mergeOptions(options) : options;
74
- let template = "";
75
-
76
- const { verbose } = options;
77
- const logger = createLogger(verbose, "render");
78
-
79
- if (isRoot) {
80
- logger.info("Starting template rendering process...");
81
- }
82
-
83
- for (let node of nodes) {
84
- const currentNodeContext = [...ancestorNodesContext, node];
85
-
86
- let shouldIgnoreNode = false;
87
-
88
- if (options.extensions && node.extensions) {
89
- logger.info(`Processing extensions for node: ${node.tag || "text"}`);
90
- for (const extension of options.extensions) {
91
- const currentExtensionKey = extension.key;
92
-
93
- if (node.extensions[currentExtensionKey]) {
94
- logger.info(
95
- `Applying overrides from extension: ${currentExtensionKey}`
96
- );
97
- node = this.applyExtensionOverrides(node, currentExtensionKey);
98
- }
99
-
100
- if (node.extensions[currentExtensionKey]?.ignore) {
101
- logger.info(`Node ignored by extension: ${currentExtensionKey}`);
102
- shouldIgnoreNode = true;
103
- break;
104
- }
105
-
106
- if (!shouldIgnoreNode) {
107
- logger.info(
108
- `Calling nodeHandler from extension: ${currentExtensionKey}`
109
- );
110
- node = extension.nodeHandler(node, ancestorNodesContext);
111
- }
112
- }
113
- }
114
-
115
- if (shouldIgnoreNode) {
116
- logger.info(`Node ignored: ${node.tag || "text"}. Skipping rendering.`);
117
- continue; // Skip rendering this node
118
- }
119
-
120
- // Constructing the template string for this node
121
- logger.info(`Rendering node: ${node.tag || "text"}`);
122
-
123
- if (node.tag) {
124
- const isSelfClosing =
125
- (node.selfClosing ||
126
- options.preferSelfClosingTags ||
127
- selfClosingTags.includes(node.tag)) &&
128
- !node.children;
129
-
130
- if (isSelfClosing) {
131
- template += `<${node.tag}`;
132
-
133
- if (node.attributes || node.expressionAttributes) {
134
- logger.info(`Processing attributes for node: ${node.tag}`);
135
- }
136
-
137
- if (node.attributes) {
138
- for (const attribute in node.attributes) {
139
- template += options.attributeFormatter(
140
- attribute,
141
- node.attributes[attribute],
142
- false // indicating this is a standard attribute, not an expression
143
- );
144
- }
145
- }
146
- if (node.expressionAttributes) {
147
- for (const attribute in node.expressionAttributes) {
148
- template += options.attributeFormatter(
149
- attribute,
150
- node.expressionAttributes[attribute],
151
- true // indicating this is an expression attribute
152
- );
153
- }
154
- }
155
- template += " />";
156
- } else {
157
- template += `<${node.tag}`;
158
- if (node.attributes) {
159
- for (const attribute in node.attributes) {
160
- template += options.attributeFormatter(
161
- attribute,
162
- node.attributes[attribute],
163
- false // standard attribute
164
- );
165
- }
166
- }
167
- if (node.expressionAttributes) {
168
- for (const attribute in node.expressionAttributes) {
169
- template += options.attributeFormatter(
170
- attribute,
171
- node.expressionAttributes[attribute],
172
- true // expression attribute
173
- );
174
- }
175
- }
176
- template += ">";
177
-
178
- if (node.children) {
179
- logger.info(`Rendering children for node: ${node.tag}`);
180
- template += await this.render(
181
- node.children,
182
- options,
183
- false,
184
- currentNodeContext
185
- );
186
- }
187
-
188
- template += `</${node.tag}>`;
189
- }
190
- } else if (node.type === "text") {
191
- // Direct text node handling
192
- logger.info(`Adding text content: "${node.content}"`);
193
- template += node.content;
194
- } else if (
195
- node.type === "slot" &&
196
- node.name &&
197
- options.slots &&
198
- options.slots[node.name]
199
- ) {
200
- logger.info(`Processing slot: ${node.name}`);
201
- template += await this.render(
202
- options.slots[node.name],
203
- options,
204
- false,
205
- currentNodeContext
206
- );
207
- }
208
- }
209
-
210
- if (isRoot) {
211
- logger.info("Finalizing template rendering...");
212
- if (options.extensions) {
213
- for (const extension of options.extensions) {
214
- if (extension.rootHandler) {
215
- template = extension.rootHandler(template, options);
216
- break;
217
- }
218
- }
219
- }
220
-
221
- template = await prettier.format(template, {
222
- parser: options.prettierParser,
223
- });
224
-
225
- const outputDir = path.join(
226
- process.cwd(),
227
- options.outputDir ? options.outputDir : "dist"
228
- );
229
-
230
- const outputPath = path.join(
231
- outputDir,
232
- `${options.filename}${options.fileExtension}`
233
- );
234
-
235
- if (options.writeOutputFile) {
236
- writeOutputFile(template, outputPath, verbose);
237
- logger.success(
238
- `Template rendering complete. Output saved to: ${outputPath}`
239
- );
240
- }
241
-
242
- return template;
243
- }
244
-
245
- return template;
246
- };
247
- }
248
-
249
- module.exports = TemplateEngine;
@@ -1,108 +0,0 @@
1
- const createLogger = require("../helpers/createLogger");
2
-
3
- class BemExtension {
4
- constructor(verbose = false) {
5
- this.key = "bem";
6
- this.logger = createLogger(verbose, "BemExtension");
7
- }
8
-
9
- setNodeExtensionOptionsShortcut = ({
10
- block,
11
- element,
12
- modifiers,
13
- modifier,
14
- }) =>
15
- block || element || modifiers || modifier
16
- ? {
17
- extensions: {
18
- bem: {
19
- ...(block ? { block } : {}),
20
- ...(element ? { element } : {}),
21
- ...(modifiers ? { modifiers } : {}),
22
- ...(modifier ? { modifier } : {}),
23
- },
24
- },
25
- }
26
- : {};
27
-
28
- nodeHandler = (node, ancestorNodesContext) => {
29
- if (node.ignoreBem) {
30
- this.logger.info(
31
- `Node ignored due to ignoreBem flag: ${JSON.stringify(node)}`
32
- );
33
- return node;
34
- }
35
-
36
- this.logger.info(`Processing node: ${node.tag}`);
37
-
38
- // use ancestorNodesContext to find the closest node where block is defined
39
- if (node.tag) {
40
- const closestAncestorBlockNode = ancestorNodesContext
41
- .slice()
42
- .reverse()
43
- .find((ancestorNode) => ancestorNode.block);
44
-
45
- const block = node?.block;
46
- const element = node?.element;
47
- const modifiers = [
48
- ...new Set([
49
- ...(node.modifiers ? node.modifiers : []),
50
- ...(node.modifier ? [node.modifier] : []),
51
- ]),
52
- ];
53
- const inheritedBlock = closestAncestorBlockNode?.block;
54
-
55
- if (inheritedBlock && !block) {
56
- this.logger.info(
57
- `Inheriting BEM block from ancestor: ${inheritedBlock}`
58
- );
59
- }
60
-
61
- const bemClasses = this.getBemClasses(
62
- block,
63
- element,
64
- modifiers,
65
- inheritedBlock
66
- );
67
-
68
- this.logger.info(
69
- `Generated BEM classes: ${bemClasses} for node: ${node.tag}`
70
- );
71
-
72
- if (node.attributes) {
73
- node.attributes.class = node.attributes.class
74
- ? `${bemClasses} ${node.attributes.class}` // TODO: maybe add options to append/prepend?
75
- : bemClasses;
76
- } else {
77
- node.attributes = { class: bemClasses };
78
- }
79
- }
80
-
81
- return node;
82
- };
83
-
84
- getBemClasses = (block, element, modifiers, inheritedBlock) => {
85
- let bemClasses = [];
86
- const blockToUse = block ?? inheritedBlock ?? "untitled-block";
87
- let root;
88
-
89
- // determine the root class based on the presence of block and element
90
- if (element) {
91
- root = `${blockToUse}__${element}`; // element present, use block__element
92
- } else if (block) {
93
- root = blockToUse; // only block present, use block
94
- } else {
95
- root = `${blockToUse}__untitled-element`; // neither block nor element defined, fallback
96
- }
97
-
98
- bemClasses.push(root);
99
-
100
- modifiers.forEach((modifier) => {
101
- bemClasses.push(`${root}--${modifier}`);
102
- });
103
-
104
- return bemClasses.join(" ");
105
- };
106
- }
107
-
108
- module.exports = BemExtension;
@@ -1,102 +0,0 @@
1
- const createLogger = require("../helpers/createLogger");
2
-
3
- class ReactExtension {
4
- constructor(verbose = false) {
5
- this.key = "react";
6
- this.logger = createLogger(verbose, "ReactExtension");
7
- }
8
-
9
- sanitizeComponentName = (componentName) => {
10
- return componentName
11
- .split("-")
12
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
13
- .join("");
14
- };
15
-
16
- optionsHandler = (defaultOptions, options) => {
17
- return {
18
- ...defaultOptions,
19
- attributeFormatter: (attribute, value, isExpression = false) => {
20
- if (!isExpression) {
21
- return ` ${attribute}="${value}"`;
22
- } else {
23
- return ` ${attribute}={${value}}`;
24
- }
25
- },
26
- filename: options.componentName ?? options.name ?? "UntitledComponent",
27
- fileExtension: ".jsx",
28
- outputDir: "dist/react",
29
- preferSelfClosingTags: true,
30
- prettierParser: "babel",
31
- };
32
- };
33
-
34
- nodeHandler = (node) => {
35
- const reactConfig = node.extensions && node.extensions.react;
36
-
37
- if (reactConfig) {
38
- this.logger.info(
39
- `Processing React extension for node: ${node.tag || "text"}`
40
- );
41
-
42
- if (node.attributes) {
43
- if (node.attributes.class) {
44
- this.logger.info(
45
- `Transforming 'class' to 'className' for React compatibility.`
46
- );
47
- node.attributes.className = node.attributes.class;
48
- delete node.attributes.class;
49
- }
50
-
51
- // TODO: handle other attributes that need to be renamed, maybe use a map?
52
- if (node.attributes.onclick) {
53
- node.attributes.onclick = node.attributes.onClick;
54
- delete node.attributes.onclick;
55
- }
56
- }
57
-
58
- if (reactConfig.attributes) {
59
- node.attributes = { ...node.attributes, ...reactConfig.attributes };
60
- }
61
-
62
- if (reactConfig.expressionAttributes) {
63
- node.expressionAttributes = reactConfig.expressionAttributes;
64
- }
65
-
66
- // Log if there are specific overrides from reactConfig
67
- if (reactConfig.tag) {
68
- this.logger.info(
69
- `Overriding node tag with React component: ${reactConfig.tag}`
70
- );
71
- }
72
- if (reactConfig.tag) {
73
- node.tag = reactConfig.tag;
74
- }
75
- }
76
-
77
- return node;
78
- };
79
-
80
- rootHandler = (htmlContent, rendererOptions) => {
81
- const rawName =
82
- rendererOptions.componentName ??
83
- rendererOptions.name ??
84
- "UntitledComponent";
85
- const componentName = this.sanitizeComponentName(rawName);
86
- this.logger.info(`Generating React component: ${componentName}`);
87
-
88
- return `
89
- import React from 'react';
90
-
91
- function ${componentName}() {
92
- return (
93
- ${htmlContent}
94
- );
95
- }
96
-
97
- export default ${componentName};
98
- `.trim();
99
- };
100
- }
101
-
102
- module.exports = ReactExtension;
@@ -1,116 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const createLogger = require("../helpers/createLogger");
4
-
5
- function readJsonFile(sourcePath) {
6
- try {
7
- const fileContent = fs.readFileSync(sourcePath, "utf8");
8
-
9
- return JSON.parse(fileContent);
10
- } catch (error) {
11
- console.error(`Error reading or parsing JSON file ${sourcePath}:`, error);
12
- throw error;
13
- }
14
- }
15
-
16
- function writeOutputFile(template, outputPath, verbose = false) {
17
- const logger = createLogger(verbose, "writeOutputFile");
18
- const outputDir = path.dirname(outputPath);
19
-
20
- if (!fs.existsSync(outputDir)) {
21
- logger.info(`Creating output directory: ${outputDir}`);
22
- fs.mkdirSync(outputDir, { recursive: true });
23
- }
24
-
25
- fs.writeFileSync(outputPath, template, "utf8");
26
- }
27
-
28
- async function getSourcePathType(sourcePath) {
29
- const stats = fs.statSync(sourcePath);
30
-
31
- if (stats.isDirectory()) {
32
- return "directory";
33
- }
34
-
35
- if (stats.isFile()) {
36
- return "file";
37
- }
38
- }
39
-
40
- async function processFile(
41
- sourcePath,
42
- outputDir,
43
- extensions,
44
- templateEngine,
45
- name,
46
- componentName,
47
- verbose
48
- ) {
49
- const logger = createLogger(verbose, "processFile");
50
- logger.info(`Processing file: ${sourcePath}`);
51
-
52
- const templateData = readJsonFile(sourcePath);
53
- const filenameWithoutExtension = path.basename(sourcePath, ".json");
54
-
55
- try {
56
- await templateEngine.render(templateData, {
57
- name: name ?? filenameWithoutExtension,
58
- componentName,
59
- outputDir,
60
- extensions,
61
- writeOutputFile: true,
62
- verbose,
63
- });
64
- logger.success(`Successfully processed file: ${sourcePath}`);
65
- } catch (error) {
66
- logger.error(`Error processing file ${sourcePath}: ${error.message}`);
67
- }
68
- }
69
-
70
- async function processDirectory(
71
- sourceDir,
72
- outputDir,
73
- extensions,
74
- templateEngine,
75
- verbose
76
- ) {
77
- const logger = createLogger(verbose, "processDirectory");
78
- logger.info(`Processing directory: ${sourceDir}`);
79
- const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
80
-
81
- for (const entry of entries) {
82
- const sourceEntryPath = path.join(sourceDir, entry.name);
83
- const outputEntryPath = path.join(outputDir ?? "dist", entry.name);
84
-
85
- if (entry.isDirectory()) {
86
- logger.info(`Entering directory: ${entry.name}`);
87
- await processDirectory(
88
- sourceEntryPath,
89
- outputEntryPath,
90
- extensions,
91
- templateEngine,
92
- verbose
93
- );
94
- } else if (entry.isFile() && path.extname(entry.name) === ".json") {
95
- logger.info(`Found JSON file: ${entry.name}`);
96
- const templateData = readJsonFile(sourceEntryPath);
97
- const filenameWithoutExtension = path.basename(entry.name, ".json");
98
-
99
- await templateEngine.render(templateData, {
100
- name: filenameWithoutExtension,
101
- outputDir,
102
- extensions,
103
- writeOutputFile: true,
104
- verbose,
105
- });
106
- }
107
- }
108
- }
109
-
110
- module.exports = {
111
- readJsonFile,
112
- writeOutputFile,
113
- getSourcePathType,
114
- processFile,
115
- processDirectory,
116
- };
@@ -1,19 +0,0 @@
1
- const signale = require("signale");
2
-
3
- const createLogger = (verbose = false, prefix = "") => {
4
- const logger = {};
5
- const methods = ["info", "warn", "error", "debug", "success"];
6
-
7
- methods.forEach((method) => {
8
- logger[method] = (...args) => {
9
- if (verbose) {
10
- signale[method](prefix ? `[${prefix}]` : "", ...args);
11
- }
12
- // If not verbose, do nothing
13
- };
14
- });
15
-
16
- return logger;
17
- };
18
-
19
- module.exports = createLogger;
@@ -1,8 +0,0 @@
1
- const mergeNodeExtensionOptions = (...extensionOptions) => ({
2
- extensions: Object.assign(
3
- {},
4
- ...extensionOptions.map((option) => option.extensions)
5
- ),
6
- });
7
-
8
- module.exports = mergeNodeExtensionOptions;