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 +26 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/bin/cli.js +129 -0
- package/dist/bem.html +12 -0
- package/examples/bem.js +81 -0
- package/examples/react.js +109 -0
- package/examples/slots.js +103 -0
- package/package.json +42 -0
- package/src/engine/TemplateEngine.js +249 -0
- package/src/extensions/bem.js +108 -0
- package/src/extensions/react.js +102 -0
- package/src/handlers/FileHandler.js +116 -0
- package/src/helpers/createLogger.js +19 -0
- package/src/helpers/mergeNodeExtensionOptions.js +8 -0
- package/src/index.js +12 -0
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>
|
package/examples/bem.js
ADDED
|
@@ -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;
|
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
|
+
};
|