js-template-engine 1.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/.eslintrc.js ADDED
@@ -0,0 +1,26 @@
1
+ module.exports = {
2
+ env: {
3
+ node: true,
4
+ browser: true,
5
+ commonjs: true,
6
+ es2021: true,
7
+ },
8
+ extends: "eslint:recommended",
9
+ overrides: [
10
+ {
11
+ env: {
12
+ node: true,
13
+ },
14
+ files: [".eslintrc.{js,cjs}"],
15
+ parserOptions: {
16
+ sourceType: "script",
17
+ },
18
+ },
19
+ ],
20
+ parserOptions: {
21
+ ecmaVersion: "latest",
22
+ },
23
+ rules: {
24
+ "no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
25
+ },
26
+ };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Björn Djurnamn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # JS Template Engine
2
+
3
+ A dynamic templating engine that translates JavaScript or JSON data into structured templates across multiple languages. At its core this tool generates HTML templates, but the concept is modular and can be extended to render templates for any framework or templating language imaginable.
4
+
5
+ ## Features
6
+
7
+ - Ideal for UI libraries: Maintain one single source of truth and avoid double maintaining different language variations of your components.
8
+ - Customizable: Does it not yet support your templating language of choice? The abstract logic allows you to create and use your own extensions.
9
+ - Native Extensions: There's a growing ecosystem of extensions, i.e. React to generate JSX components and BEM to enforce consistent class naming.
10
+ - CLI Interface: A convenient CLI tool that can both process single JSON files and traverse through nested folder structures from the command line.
11
+ - Flexible Configuration: Customize the output directory, apply framework-specific extensions, and more through CLI options or configuration files.
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ npm install js-template-engine
17
+ ```
18
+
19
+ Or if you prefer using Yarn:
20
+
21
+ ```sh
22
+ yarn add js-template-engine
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### CLI
28
+
29
+ The JS Template Engine CLI provides a straightforward way to render templates from JSON files:
30
+
31
+ ```sh
32
+ js-template-engine render <sourcePath> [options]
33
+ ```
34
+
35
+ **Arguments:**
36
+
37
+ - `<sourcePath>`: The path to the JSON file or directory containing JSON templates you wish to render.
38
+
39
+ **Options:**
40
+
41
+ - `--outputDir`, `-o`: Specify the output directory for rendered templates.
42
+ - `--extensions`, `-e`: Choose extensions to use for template processing (e.g., react, bem).
43
+ - `--name`, `-n`: Set a base name for output files.
44
+ - `--componentName`, `-c`: Define a component name for framework-specific templates (useful for React).
45
+ - `--verbose`, `-v`: Enable verbose logging for more detailed output.
46
+
47
+ ### Examples
48
+
49
+ Feel free to check out the [examples folder](examples), to get a better idea of some of the core concepts and extensions. The provided examples can be run using:
50
+
51
+ ```sh
52
+ npm run example:react
53
+ npm run example:bem
54
+ npm run example:slots
55
+
56
+ ```
57
+
58
+ Or if you prefer using Yarn:
59
+
60
+ ```sh
61
+ yarn example:react
62
+ yarn example:bem
63
+ yarn example:slots
64
+ ```
65
+
66
+ ### API
67
+
68
+ You can also use JS Template Engine programmatically in your Node.js projects. This is how you could define and process your template using the BEM extension:
69
+
70
+ ```javascript
71
+ const { TemplateEngine, BemExtension } = require("js-template-engine");
72
+
73
+ const templateEngine = new TemplateEngine();
74
+ const bemExtension = new BemExtension();
75
+
76
+ const breadcrumbsTemplate = [
77
+ {
78
+ tag: "nav",
79
+ extensions: {
80
+ bem: {
81
+ block: "breadcrumbs",
82
+ },
83
+ },
84
+ children: [
85
+ {
86
+ tag: "ul",
87
+
88
+ extensions: {
89
+ bem: {
90
+ element: "list",
91
+ },
92
+ },
93
+ children: [
94
+ {
95
+ tag: "li",
96
+
97
+ extensions: {
98
+ bem: {
99
+ element: "item",
100
+ },
101
+ },
102
+ children: [
103
+ {
104
+ tag: "a",
105
+ extensions: {
106
+ bem: {
107
+ element: "text",
108
+ },
109
+ },
110
+ attributes: {
111
+ href: "/",
112
+ },
113
+ children: [
114
+ {
115
+ type: "text",
116
+ content: "Home",
117
+ },
118
+ ],
119
+ },
120
+ ],
121
+ },
122
+ {
123
+ tag: "li",
124
+ extensions: {
125
+ bem: {
126
+ element: "item",
127
+ modifier: "current",
128
+ },
129
+ },
130
+ children: [
131
+ {
132
+ tag: "span",
133
+ extensions: {
134
+ bem: {
135
+ element: "text",
136
+ },
137
+ },
138
+ children: [
139
+ {
140
+ type: "text",
141
+ content: "About",
142
+ },
143
+ ],
144
+ },
145
+ ],
146
+ },
147
+ ],
148
+ },
149
+ ],
150
+ },
151
+ ];
152
+
153
+ templateEngine.render(breadcrumbsTemplate, {
154
+ name: "breadcrumbs",
155
+ extensions: [bemExtension],
156
+ });
157
+ ```
158
+
159
+ This is what it would result in:
160
+
161
+ ```html
162
+ <nav class="breadcrumbs">
163
+ <ul class="breadcrumbs__list">
164
+ <li class="breadcrumbs__item">
165
+ <a href="/" class="breadcrumbs__text">Home</a>
166
+ </li>
167
+ <li class="breadcrumbs__item breadcrumbs__item--current">
168
+ <span class="breadcrumbs__text">About</span>
169
+ </li>
170
+ </ul>
171
+ </nav>
172
+ ```
173
+
174
+ ## Contributing
175
+
176
+ Contributions are welcome! Feel free to open pull requests or issues to suggest features, report bugs, or contribute to the code.
177
+
178
+ ## Reporting Issues
179
+
180
+ Found a bug or have a suggestion? Please use the GitHub Issues page to report issues or request features.
181
+
182
+ ## License
183
+
184
+ This project is licensed under the MIT License - see the LICENSE file for details.
package/bin/cli.js ADDED
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const process = require("process");
6
+ const yargs = require("yargs/yargs");
7
+ const { hideBin } = require("yargs/helpers");
8
+ const TemplateEngine = require("../src/engine/TemplateEngine");
9
+ const {
10
+ getSourcePathType,
11
+ processFile,
12
+ processDirectory,
13
+ } = require("../src/handlers/FileHandler");
14
+ const createLogger = require("../src/helpers/createLogger");
15
+
16
+ function loadExtensions() {
17
+ const extensions = {};
18
+ const fullPath = path.join(__dirname, "..", "src", "extensions");
19
+
20
+ fs.readdirSync(fullPath).forEach((file) => {
21
+ if (path.extname(file) !== ".js") return;
22
+
23
+ const extName = path.basename(file, ".js");
24
+ const ExtensionClass = require(path.join(fullPath, file));
25
+ extensions[extName] = ExtensionClass;
26
+ });
27
+
28
+ return extensions;
29
+ }
30
+
31
+ const availableExtensions = loadExtensions();
32
+
33
+ // Utility function for error handling and extension validation
34
+ function validateExtensions(requestedExtensions, availableExtensions) {
35
+ const missingExtensions = requestedExtensions.filter(
36
+ (ext) => !availableExtensions[ext]
37
+ );
38
+ if (missingExtensions.length > 0) {
39
+ console.error(
40
+ "One or more specified extensions could not be loaded:",
41
+ missingExtensions.join(", ")
42
+ );
43
+ return false;
44
+ }
45
+ return true;
46
+ }
47
+
48
+ yargs(hideBin(process.argv))
49
+ .command(
50
+ "render <sourcePath> [options]",
51
+ "Render templates from JSON to HTML/JSX",
52
+ (yargs) => {
53
+ yargs
54
+ .positional("sourcePath", {
55
+ describe: "Source file or directory containing JSON templates",
56
+ type: "string",
57
+ })
58
+ .option("outputDir", {
59
+ alias: "o",
60
+ describe: "Output directory for rendered templates",
61
+ type: "string",
62
+ })
63
+ .option("extensions", {
64
+ alias: "e",
65
+ describe: "Extensions to use for template processing",
66
+ type: "array",
67
+ default: [],
68
+ })
69
+ .option("name", {
70
+ alias: "n",
71
+ describe: "Base name for output files",
72
+ type: "string",
73
+ })
74
+ .option("componentName", {
75
+ alias: "c",
76
+ describe: "Component name for framework-specific templates",
77
+ type: "string",
78
+ })
79
+ .option("verbose", {
80
+ alias: "v",
81
+ type: "boolean",
82
+ description: "Run with verbose logging",
83
+ });
84
+ },
85
+ async (argv) => {
86
+ const { verbose } = argv;
87
+ const logger = createLogger(verbose, "cli");
88
+ logger.info("Starting template rendering process...");
89
+
90
+ // Validate requested extensions before attempting to use them
91
+ if (!validateExtensions(argv.extensions, availableExtensions)) {
92
+ // Exit if validation fails
93
+ return;
94
+ }
95
+
96
+ // Instantiate extensions with verbosity
97
+ const extensions = argv.extensions.map(
98
+ (extension) => new availableExtensions[extension](verbose)
99
+ );
100
+
101
+ const { name, componentName } = argv;
102
+ const sourcePath = path.join(process.cwd(), argv.sourcePath);
103
+ const outputDir = argv.outputDir ?? "";
104
+ const sourcePathType = await getSourcePathType(sourcePath);
105
+ const templateEngine = new TemplateEngine();
106
+
107
+ if (sourcePathType === "directory") {
108
+ await processDirectory(
109
+ sourcePath,
110
+ outputDir,
111
+ extensions,
112
+ templateEngine,
113
+ verbose
114
+ );
115
+ } else if (sourcePathType === "file") {
116
+ await processFile(
117
+ sourcePath,
118
+ outputDir,
119
+ extensions,
120
+ templateEngine,
121
+ name,
122
+ componentName,
123
+ verbose
124
+ );
125
+ }
126
+ }
127
+ )
128
+ .demandCommand(1, "You need at least one command before moving on")
129
+ .help().argv;
package/dist/bem.html ADDED
@@ -0,0 +1,12 @@
1
+ <nav class="nav">
2
+ <ul class="nav__list">
3
+ <li class="nav__item nav__item--active">
4
+ <a href="/" class="nav__link"><span class="nav__link-text">Home</span></a>
5
+ </li>
6
+ <li class="nav__item">
7
+ <a href="/about" class="nav__link"
8
+ ><span class="nav__link-text">About</span></a
9
+ >
10
+ </li>
11
+ </ul>
12
+ </nav>
@@ -0,0 +1,81 @@
1
+ // Example of using the TemplateEngine class to render a simple HTML template with a class naming convention using the BEM extension.
2
+ // You can run this example with `npm run example:slots` or `yarn example:slots`.
3
+
4
+ const { TemplateEngine, BemExtension } = require("../src");
5
+ const verbose = true;
6
+
7
+ const templateEngine = new TemplateEngine();
8
+ const bemExtension = new BemExtension(verbose);
9
+
10
+ const bem = bemExtension.setNodeExtensionOptionsShortcut;
11
+
12
+ /*
13
+ Using the BEM extension options shortcut allows us to write:
14
+
15
+ {
16
+ tag: "nav",
17
+ ...bem({ block: "nav" }),
18
+ }
19
+
20
+ instead of:
21
+
22
+ {
23
+ tag: "nav",
24
+ extensions: {
25
+ bem: {
26
+ block: "nav",
27
+ },
28
+ },
29
+ }
30
+ */
31
+
32
+ // Data
33
+ const navigationItems = [
34
+ { name: "Home", url: "/", iconName: "home" },
35
+ { name: "About", url: "/about", iconName: "about" },
36
+ ];
37
+
38
+ // Templates
39
+ const navigationTemplate = [
40
+ {
41
+ tag: "nav",
42
+ ...bem({ block: "nav" }),
43
+ children: [
44
+ {
45
+ tag: "ul",
46
+ ...bem({ element: "list" }),
47
+ children: navigationItems.map((item, itemIndex) => ({
48
+ tag: "li",
49
+ ...bem({
50
+ element: "item",
51
+ modifiers: itemIndex === 0 ? ["active"] : [],
52
+ }),
53
+ children: [
54
+ {
55
+ tag: "a",
56
+ ...bem({ element: "link" }),
57
+ attributes: { href: item.url },
58
+ children: [
59
+ {
60
+ tag: "span",
61
+ ...bem({ element: "link-text" }),
62
+ children: [{ type: "text", content: item.name }],
63
+ },
64
+ ],
65
+ },
66
+ ],
67
+ })),
68
+ },
69
+ ],
70
+ },
71
+ ];
72
+
73
+ // Render
74
+ (async () => {
75
+ await templateEngine.render(navigationTemplate, {
76
+ name: "bem",
77
+ extensions: [bemExtension], // Only BEM extension is needed for this example
78
+ writeOutputFile: true,
79
+ verbose,
80
+ });
81
+ })();
@@ -0,0 +1,109 @@
1
+ // Example of using the TemplateEngine class to render a simple JSX component with using the React extension.
2
+ // You can run this example with `npm run example:react` or `yarn example:react`.
3
+
4
+ const { TemplateEngine, ReactExtension } = require("../src");
5
+ const verbose = true;
6
+
7
+ const templateEngine = new TemplateEngine();
8
+ const reactExtension = new ReactExtension(verbose);
9
+
10
+ const initialTodos = [
11
+ { id: 1, text: "Learn JavaScript", completed: false },
12
+ { id: 2, text: "Build a todo app", completed: false },
13
+ ];
14
+
15
+ const todoAppTemplate = [
16
+ {
17
+ tag: "div",
18
+ attributes: { class: "todo-app" },
19
+ children: [
20
+ {
21
+ tag: "input",
22
+ attributes: {
23
+ type: "text",
24
+ id: "todoInput",
25
+ placeholder: "Add a new todo",
26
+ },
27
+ },
28
+ {
29
+ tag: "button",
30
+ attributes: {
31
+ onclick: "handleAddTodo",
32
+ },
33
+ children: [{ type: "text", content: "Add" }],
34
+ extensions: {
35
+ react: {
36
+ tag: "DefaultButton",
37
+ attributes: {
38
+ color: "primary",
39
+ label: "Add",
40
+ },
41
+ expressionAttributes: {
42
+ onClick: "handleAddTodo",
43
+ },
44
+ },
45
+ },
46
+ },
47
+ {
48
+ tag: "ul",
49
+ attributes: { id: "todoList" },
50
+ children: initialTodos.map((todo) => ({
51
+ tag: "li",
52
+ children: [{ type: "text", content: todo.text }],
53
+ attributes: {
54
+ onclick: "this.parentNode.removeChild(this);",
55
+ },
56
+ extensions: {
57
+ react: {
58
+ tag: "TodoCard",
59
+ expressionAttributes: {
60
+ onClick: "() => handleRemoveTodo()",
61
+ },
62
+ },
63
+ },
64
+ })),
65
+ },
66
+ {
67
+ tag: "script",
68
+ children: [
69
+ {
70
+ type: "text",
71
+ content: `
72
+ function handleAddTodo() {
73
+ const todoList = document.getElementById('todoList');
74
+ const newTodoText = document.getElementById('todoInput').value;
75
+ const newTodoItem = document.createElement('li');
76
+
77
+ newTodoItem.textContent = newTodoText;
78
+ todoList.appendChild(newTodoItem);
79
+
80
+ document.getElementById('todoInput').value = ''; // Clear the input field
81
+ }
82
+ `,
83
+ },
84
+ ],
85
+ extensions: {
86
+ react: {
87
+ ignore: true,
88
+ },
89
+ },
90
+ },
91
+ ],
92
+ },
93
+ ];
94
+
95
+ // Render
96
+ (async () => {
97
+ await templateEngine.render(todoAppTemplate, {
98
+ name: "html-todo-app",
99
+ writeOutputFile: true,
100
+ });
101
+
102
+ await templateEngine.render(todoAppTemplate, {
103
+ name: "react-todo-app",
104
+ extensions: [reactExtension], // Only React extension is needed for this example
105
+ outputDir: "dist", // React extension defaults to "dist/react"
106
+ writeOutputFile: true,
107
+ verbose,
108
+ });
109
+ })();
@@ -0,0 +1,103 @@
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
+ })();
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "js-template-engine",
3
+ "version": "1.0.0",
4
+ "description": "A dynamic templating engine that translates JavaScript or JSON data into structured templates across multiple languages.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "js-template-engine": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "example:react": "node examples/react.js",
11
+ "example:bem": "node examples/bem.js",
12
+ "example:slots": "node examples/slots.js"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/djurnamn/js-template-engine.git"
17
+ },
18
+ "keywords": [
19
+ "javascript",
20
+ "js",
21
+ "template",
22
+ "templating",
23
+ "engine",
24
+ "json",
25
+ "react",
26
+ "bem"
27
+ ],
28
+ "author": "Björn Djurnamn <bjorn@djurnamn.co>",
29
+ "license": "MIT",
30
+ "bugs": {
31
+ "url": "https://github.com/djurnamn/js-template-engine/issues"
32
+ },
33
+ "homepage": "https://github.com/djurnamn/js-template-engine",
34
+ "dependencies": {
35
+ "signale": "^1.4.0",
36
+ "yargs": "^17.7.2"
37
+ },
38
+ "devDependencies": {
39
+ "eslint": "^8.0.1",
40
+ "prettier": "^3.2.5"
41
+ }
42
+ }
@@ -0,0 +1,249 @@
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;
@@ -0,0 +1,108 @@
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;
@@ -0,0 +1,102 @@
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;
@@ -0,0 +1,116 @@
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
+ };
@@ -0,0 +1,19 @@
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;
@@ -0,0 +1,8 @@
1
+ const mergeNodeExtensionOptions = (...extensionOptions) => ({
2
+ extensions: Object.assign(
3
+ {},
4
+ ...extensionOptions.map((option) => option.extensions)
5
+ ),
6
+ });
7
+
8
+ module.exports = mergeNodeExtensionOptions;
package/src/index.js ADDED
@@ -0,0 +1,12 @@
1
+ const TemplateEngine = require("./engine/TemplateEngine");
2
+ const BemExtension = require("./extensions/bem");
3
+ const ReactExtension = require("./extensions/react");
4
+ const { processFile, processDirectory } = require("./handlers/FileHandler");
5
+
6
+ module.exports = {
7
+ TemplateEngine,
8
+ BemExtension,
9
+ ReactExtension,
10
+ processFile,
11
+ processDirectory,
12
+ };