create-template-html-css 1.4.3 → 1.6.2

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/INSERT-QUICK-REFERENCE.md +147 -0
  3. package/README.md +33 -2
  4. package/RELEASE-NOTES-v1.6.1.md +129 -0
  5. package/RELEASE-STATUS.md +203 -0
  6. package/RELEASE-v1.6.2.md +169 -0
  7. package/SECURITY-AUDIT.md +157 -0
  8. package/TEST-REPORT.md +110 -0
  9. package/VERIFICATION-REPORT.md +162 -0
  10. package/bin/cli.js +416 -247
  11. package/demo/css/accordion-component.css +135 -0
  12. package/demo/css/button-component.css +110 -0
  13. package/demo/css/card-component.css +381 -0
  14. package/demo/index.html +169 -0
  15. package/demo/js/accordion-component.js +31 -0
  16. package/demo/js/button-component.js +17 -0
  17. package/demo/js/card-component.js +124 -0
  18. package/package.json +6 -3
  19. package/src/generator.js +165 -64
  20. package/src/index.js +1 -1
  21. package/src/inserter.js +352 -146
  22. package/templates/accordion/index.html +67 -0
  23. package/templates/accordion/script.js +29 -0
  24. package/templates/accordion/style.css +133 -0
  25. package/templates/counter/index.html +46 -0
  26. package/templates/counter/script.js +88 -0
  27. package/templates/counter/style.css +164 -0
  28. package/templates/tabs/index.html +83 -0
  29. package/templates/tabs/script.js +46 -0
  30. package/templates/tabs/style.css +173 -0
  31. package/templates/todo-list/index.html +45 -0
  32. package/templates/todo-list/script.js +69 -0
  33. package/templates/todo-list/style.css +138 -0
  34. package/v1.6.2-IMPROVEMENTS.md +185 -0
  35. package/CONTRIBUTING.md +0 -62
  36. package/INSERT-DEMO.md +0 -171
  37. package/QUICKSTART.md +0 -195
  38. package/SECURITY.md +0 -95
  39. package/SHOWCASE.html +0 -342
  40. package/test-insert.html +0 -54
package/src/inserter.js CHANGED
@@ -1,146 +1,352 @@
1
- const fs = require('fs').promises;
2
- const path = require('path');
3
-
4
- // Security: Validate component name against whitelist
5
- const VALID_COMPONENTS = ['button', 'card', 'form', 'navigation', 'modal', 'footer', 'hero', 'slider', 'table', 'spinner', 'animated-card', 'typing-effect', 'fade-gallery', 'grid-layout', 'masonry-grid', 'dashboard-grid', 'flex-layout', 'flex-cards', 'flex-dashboard'];
6
-
7
- /**
8
- * Extracts indentation from a line
9
- */
10
- function getIndentation(line) {
11
- const match = line.match(/^(\s*)/);
12
- return match ? match[1] : '';
13
- }
14
-
15
- /**
16
- * Checks if a component is already inserted in the HTML
17
- */
18
- function isComponentAlreadyInserted(htmlContent, component) {
19
- const commentPattern = new RegExp(`<!-- ${component.toUpperCase()} Component -->`, 'i');
20
- return commentPattern.test(htmlContent);
21
- }
22
-
23
- /**
24
- * Gets the indentation level used in an HTML file
25
- */
26
- function getHtmlIndentation(htmlContent) {
27
- // Look for any indented line to determine the standard indentation
28
- const match = htmlContent.match(/\n(\s+)\S/);
29
- return match ? match[1] : ' '; // default to 4 spaces
30
- }
31
-
32
- async function insertComponent(options) {
33
- const { component, targetFile, styleMode, scriptMode } = options;
34
-
35
- // Security: Validate component name
36
- if (!VALID_COMPONENTS.includes(component)) {
37
- throw new Error(`Invalid component: ${component}. Must be one of: ${VALID_COMPONENTS.join(', ')}`);
38
- }
39
-
40
- // Check if target file exists
41
- const targetPath = path.resolve(process.cwd(), targetFile);
42
- try {
43
- await fs.access(targetPath);
44
- } catch {
45
- throw new Error(`Target file not found: ${targetFile}`);
46
- }
47
-
48
- // Read target HTML file
49
- let htmlContent = await fs.readFile(targetPath, 'utf-8');
50
-
51
- // Check if component is already inserted
52
- if (isComponentAlreadyInserted(htmlContent, component)) {
53
- throw new Error(`Component "${component}" is already inserted in this file`);
54
- }
55
-
56
- // Validate HTML structure
57
- if (!htmlContent.includes('</body>')) {
58
- throw new Error('Target HTML file does not have a closing </body> tag');
59
- }
60
-
61
- if (!htmlContent.includes('</head>')) {
62
- throw new Error('Target HTML file does not have a </head> tag');
63
- }
64
-
65
- // Get component templates
66
- const templateDir = path.join(__dirname, '..', 'templates', component);
67
- const componentHtml = await fs.readFile(path.join(templateDir, 'index.html'), 'utf-8');
68
- const componentCss = await fs.readFile(path.join(templateDir, 'style.css'), 'utf-8');
69
-
70
- // Extract component body content
71
- const bodyMatch = componentHtml.match(/<body[^>]*>([\s\S]*)<\/body>/i);
72
- if (!bodyMatch) {
73
- throw new Error('Invalid component template structure');
74
- }
75
-
76
- let componentBody = bodyMatch[1].trim();
77
-
78
- // Get indentation used in the HTML file
79
- const baseIndent = getHtmlIndentation(htmlContent);
80
-
81
- // Normalize component body indentation
82
- const lines = componentBody.split('\n').map(line => {
83
- if (line.trim() === '') return '';
84
- return baseIndent + line.trim();
85
- }).join('\n');
86
- componentBody = lines;
87
-
88
- // Insert component HTML before closing </body> tag
89
- htmlContent = htmlContent.replace('</body>', `${baseIndent}<!-- ${component.toUpperCase()} Component -->\n${componentBody}\n\n</body>`);
90
-
91
- // Handle CSS
92
- if (styleMode === 'inline') {
93
- // Normalize CSS indentation
94
- const normalizedCss = componentCss.split('\n').map(line => {
95
- if (line.trim() === '') return '';
96
- return baseIndent + ' ' + line.trim();
97
- }).join('\n');
98
-
99
- htmlContent = htmlContent.replace('</head>', `${baseIndent}<style id="${component}-styles">\n${baseIndent} /* ${component.toUpperCase()} Component Styles */\n${normalizedCss}\n${baseIndent}</style>\n</head>`);
100
- } else if (styleMode === 'separate') {
101
- // Create separate CSS file
102
- const cssFileName = `${component}-component.css`;
103
- const cssPath = path.join(path.dirname(targetPath), cssFileName);
104
- await fs.writeFile(cssPath, `/* ${component.toUpperCase()} Component Styles */\n\n${componentCss}`);
105
-
106
- // Add link to CSS file
107
- htmlContent = htmlContent.replace('</head>', `${baseIndent}<link rel="stylesheet" href="${cssFileName}">\n</head>`);
108
- }
109
-
110
- // Handle JavaScript
111
- try {
112
- const componentJs = await fs.readFile(path.join(templateDir, 'script.js'), 'utf-8');
113
-
114
- if (scriptMode === 'inline') {
115
- // Normalize JS indentation
116
- const normalizedJs = componentJs.split('\n').map(line => {
117
- if (line.trim() === '') return '';
118
- return baseIndent + ' ' + line.trim();
119
- }).join('\n');
120
-
121
- htmlContent = htmlContent.replace('</body>', `${baseIndent}<script id="${component}-script">\n${baseIndent} // ${component.toUpperCase()} Component Script\n${normalizedJs}\n${baseIndent}</script>\n</body>`);
122
- } else if (scriptMode === 'separate') {
123
- // Create separate JS file
124
- const jsFileName = `${component}-component.js`;
125
- const jsPath = path.join(path.dirname(targetPath), jsFileName);
126
- await fs.writeFile(jsPath, `// ${component.toUpperCase()} Component Script\n\n${componentJs}`);
127
-
128
- // Add script tag
129
- htmlContent = htmlContent.replace('</body>', `${baseIndent}<script src="${jsFileName}" id="${component}-script"></script>\n</body>`);
130
- }
131
- } catch (error) {
132
- // No JavaScript file for this component, skip
133
- }
134
-
135
- // Write updated HTML
136
- await fs.writeFile(targetPath, htmlContent);
137
-
138
- return {
139
- targetFile: targetPath,
140
- component,
141
- styleMode,
142
- scriptMode
143
- };
144
- }
145
-
146
- module.exports = { insertComponent };
1
+ const fs = require("fs").promises;
2
+ const path = require("path");
3
+
4
+ // Security: Validate component name against whitelist
5
+ const VALID_COMPONENTS = [
6
+ "button",
7
+ "card",
8
+ "form",
9
+ "navigation",
10
+ "modal",
11
+ "footer",
12
+ "hero",
13
+ "slider",
14
+ "table",
15
+ "spinner",
16
+ "animated-card",
17
+ "typing-effect",
18
+ "fade-gallery",
19
+ "grid-layout",
20
+ "masonry-grid",
21
+ "dashboard-grid",
22
+ "flex-layout",
23
+ "flex-cards",
24
+ "flex-dashboard",
25
+ "todo-list",
26
+ "counter",
27
+ "accordion",
28
+ "tabs",
29
+ ];
30
+
31
+ /**
32
+ * Extracts indentation from a line
33
+ */
34
+ function getIndentation(line) {
35
+ const match = line.match(/^(\s*)/);
36
+ return match ? match[1] : "";
37
+ }
38
+
39
+ /**
40
+ * Checks if a component is already inserted in the HTML
41
+ */
42
+ function isComponentAlreadyInserted(htmlContent, component) {
43
+ const commentPattern = new RegExp(
44
+ `<!-- ${component.toUpperCase()} Component -->`,
45
+ "i",
46
+ );
47
+ return commentPattern.test(htmlContent);
48
+ }
49
+
50
+ /**
51
+ * Formats HTML content with prettier
52
+ */
53
+ async function formatHtml(htmlContent) {
54
+ const prettier = require("prettier");
55
+ try {
56
+ return await prettier.format(htmlContent, { parser: "html" });
57
+ } catch (error) {
58
+ return htmlContent;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Formats CSS content with prettier
64
+ */
65
+ async function formatCss(cssContent) {
66
+ const prettier = require("prettier");
67
+ try {
68
+ return await prettier.format(cssContent, { parser: "css" });
69
+ } catch (error) {
70
+ return cssContent;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Formats JavaScript content with prettier
76
+ */
77
+ async function formatJs(jsContent) {
78
+ const prettier = require("prettier");
79
+ try {
80
+ return await prettier.format(jsContent, { parser: "babel" });
81
+ } catch (error) {
82
+ return jsContent;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Gets the indentation level used in an HTML file
88
+ */
89
+ function getHtmlIndentation(htmlContent) {
90
+ // Look for any indented line to determine the standard indentation
91
+ const match = htmlContent.match(/\n(\s+)\S/);
92
+ return match ? match[1] : " "; // default to 4 spaces
93
+ }
94
+
95
+ /**
96
+ * Validates that HTML file has proper structure
97
+ * @param {string} htmlContent - The HTML content to validate
98
+ * @returns {Object} Object with valid property and any errors
99
+ */
100
+ function validateHtmlStructure(htmlContent) {
101
+ const errors = [];
102
+
103
+ if (!htmlContent.includes("<!DOCTYPE")) {
104
+ errors.push("Missing DOCTYPE declaration");
105
+ }
106
+
107
+ if (!htmlContent.includes("<html")) {
108
+ errors.push("Missing <html> tag");
109
+ }
110
+
111
+ if (!htmlContent.includes("<head>") && !htmlContent.includes("<head ")) {
112
+ errors.push("Missing <head> tag");
113
+ }
114
+
115
+ if (!htmlContent.includes("</head>")) {
116
+ errors.push("Missing closing </head> tag");
117
+ }
118
+
119
+ if (!htmlContent.includes("<body>") && !htmlContent.includes("<body ")) {
120
+ errors.push("Missing <body> tag");
121
+ }
122
+
123
+ if (!htmlContent.includes("</body>")) {
124
+ errors.push("Missing closing </body> tag");
125
+ }
126
+
127
+ return {
128
+ valid: errors.length === 0,
129
+ errors,
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Creates a backup of the original file before insertion
135
+ * @param {string} targetPath - Path to the original file
136
+ * @returns {Promise<string>} Path to the backup file
137
+ */
138
+ async function createBackup(targetPath) {
139
+ const backupPath = `${targetPath}.backup`;
140
+ const content = await fs.readFile(targetPath, "utf-8");
141
+ await fs.writeFile(backupPath, content, "utf-8");
142
+ return backupPath;
143
+ }
144
+
145
+ async function insertComponent(options) {
146
+ const {
147
+ component,
148
+ targetFile,
149
+ styleMode,
150
+ scriptMode,
151
+ createBackup: shouldBackup = false,
152
+ } = options;
153
+
154
+ // Security: Validate component name
155
+ if (!VALID_COMPONENTS.includes(component)) {
156
+ throw new Error(
157
+ `Invalid component: ${component}. Must be one of: ${VALID_COMPONENTS.join(", ")}`,
158
+ );
159
+ }
160
+
161
+ // Check if target file exists
162
+ const targetPath = path.resolve(process.cwd(), targetFile);
163
+ try {
164
+ await fs.access(targetPath);
165
+ } catch {
166
+ throw new Error(`Target file not found: ${targetFile}`);
167
+ }
168
+
169
+ // Read target HTML file
170
+ let htmlContent = await fs.readFile(targetPath, "utf-8");
171
+
172
+ // Validate HTML structure
173
+ const validation = validateHtmlStructure(htmlContent);
174
+ if (!validation.valid) {
175
+ throw new Error(
176
+ `Invalid HTML structure:\n - ${validation.errors.join("\n - ")}`,
177
+ );
178
+ }
179
+
180
+ // Check if component is already inserted
181
+ if (isComponentAlreadyInserted(htmlContent, component)) {
182
+ throw new Error(
183
+ `Component "${component}" is already inserted in this file`,
184
+ );
185
+ }
186
+
187
+ // Create backup if requested
188
+ let backupPath = null;
189
+ if (shouldBackup) {
190
+ backupPath = await createBackup(targetPath);
191
+ }
192
+
193
+ if (!htmlContent.includes("</head>")) {
194
+ throw new Error("Target HTML file does not have a </head> tag");
195
+ }
196
+
197
+ // Get component templates
198
+ const templateDir = path.join(__dirname, "..", "templates", component);
199
+ const componentHtml = await fs.readFile(
200
+ path.join(templateDir, "index.html"),
201
+ "utf-8",
202
+ );
203
+
204
+ // Try to read CSS from css/ subfolder, fall back to root
205
+ let componentCss;
206
+ try {
207
+ componentCss = await fs.readFile(
208
+ path.join(templateDir, "css", "style.css"),
209
+ "utf-8",
210
+ );
211
+ } catch {
212
+ componentCss = await fs.readFile(
213
+ path.join(templateDir, "style.css"),
214
+ "utf-8",
215
+ );
216
+ }
217
+
218
+ // Extract component body content (only the inner content, not the body tags)
219
+ const bodyMatch = componentHtml.match(/<body[^>]*>\s*([\s\S]*?)\s*<\/body>/i);
220
+ if (!bodyMatch) {
221
+ throw new Error("Invalid component template structure");
222
+ }
223
+
224
+ let componentBody = bodyMatch[1].trim();
225
+
226
+ // Remove any script and style tags that might be in the body
227
+ componentBody = componentBody
228
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
229
+ .trim();
230
+
231
+ // Get indentation used in the HTML file
232
+ const baseIndent = getHtmlIndentation(htmlContent);
233
+
234
+ // Normalize component body indentation
235
+ const lines = componentBody
236
+ .split("\n")
237
+ .map((line) => {
238
+ if (line.trim() === "") return "";
239
+ return baseIndent + line.trim();
240
+ })
241
+ .join("\n");
242
+ componentBody = lines;
243
+
244
+ // Insert component HTML before closing </body> tag
245
+ htmlContent = htmlContent.replace(
246
+ "</body>",
247
+ `${baseIndent}<!-- ${component.toUpperCase()} Component -->\n${componentBody}\n\n</body>`,
248
+ );
249
+
250
+ // Handle CSS
251
+ if (styleMode === "inline") {
252
+ // Normalize CSS indentation
253
+ const normalizedCss = componentCss
254
+ .split("\n")
255
+ .map((line) => {
256
+ if (line.trim() === "") return "";
257
+ return baseIndent + " " + line.trim();
258
+ })
259
+ .join("\n");
260
+
261
+ htmlContent = htmlContent.replace(
262
+ "</head>",
263
+ `${baseIndent}<style id="${component}-styles">\n${baseIndent} /* ${component.toUpperCase()} Component Styles */\n${normalizedCss}\n${baseIndent}</style>\n</head>`,
264
+ );
265
+ } else if (styleMode === "separate") {
266
+ // Create css directory if needed
267
+ const cssDir = path.join(path.dirname(targetPath), "css");
268
+ await fs.mkdir(cssDir, { recursive: true });
269
+
270
+ // Create separate CSS file in css/ folder
271
+ const cssFileName = `${component}-component.css`;
272
+ const cssPath = path.join(cssDir, cssFileName);
273
+ const formattedCss = await formatCss(
274
+ `/* ${component.toUpperCase()} Component Styles */\n\n${componentCss}`,
275
+ );
276
+ await fs.writeFile(cssPath, formattedCss);
277
+
278
+ // Add link to CSS file
279
+ htmlContent = htmlContent.replace(
280
+ "</head>",
281
+ `${baseIndent}<link rel="stylesheet" href="css/${cssFileName}">\n</head>`,
282
+ );
283
+ }
284
+
285
+ // Handle JavaScript
286
+ try {
287
+ // Try to read JS from js/ subfolder, fall back to root
288
+ let componentJs;
289
+ try {
290
+ componentJs = await fs.readFile(
291
+ path.join(templateDir, "js", "script.js"),
292
+ "utf-8",
293
+ );
294
+ } catch {
295
+ componentJs = await fs.readFile(
296
+ path.join(templateDir, "script.js"),
297
+ "utf-8",
298
+ );
299
+ }
300
+
301
+ if (scriptMode === "inline") {
302
+ // Normalize JS indentation
303
+ const normalizedJs = componentJs
304
+ .split("\n")
305
+ .map((line) => {
306
+ if (line.trim() === "") return "";
307
+ return baseIndent + " " + line.trim();
308
+ })
309
+ .join("\n");
310
+
311
+ htmlContent = htmlContent.replace(
312
+ "</body>",
313
+ `${baseIndent}<script id="${component}-script">\n${baseIndent} // ${component.toUpperCase()} Component Script\n${normalizedJs}\n${baseIndent}</script>\n</body>`,
314
+ );
315
+ } else if (scriptMode === "separate") {
316
+ // Create js directory if needed
317
+ const jsDir = path.join(path.dirname(targetPath), "js");
318
+ await fs.mkdir(jsDir, { recursive: true });
319
+
320
+ // Create separate JS file in js/ folder
321
+ const jsFileName = `${component}-component.js`;
322
+ const jsPath = path.join(jsDir, jsFileName);
323
+ const formattedJs = await formatJs(
324
+ `// ${component.toUpperCase()} Component Script\n\n${componentJs}`,
325
+ );
326
+ await fs.writeFile(jsPath, formattedJs);
327
+
328
+ // Add script tag
329
+ htmlContent = htmlContent.replace(
330
+ "</body>",
331
+ `${baseIndent}<script src="js/${jsFileName}" id="${component}-script"></script>\n</body>`,
332
+ );
333
+ }
334
+ } catch (error) {
335
+ // No JavaScript file for this component, skip
336
+ }
337
+
338
+ // Write updated HTML with prettier formatting
339
+ const formattedHtml = await formatHtml(htmlContent);
340
+ await fs.writeFile(targetPath, formattedHtml);
341
+
342
+ return {
343
+ targetFile: targetPath,
344
+ component,
345
+ styleMode,
346
+ scriptMode,
347
+ backupPath: backupPath || null,
348
+ success: true,
349
+ };
350
+ }
351
+
352
+ module.exports = { insertComponent, validateHtmlStructure, createBackup };
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{name}} - Accordion Component</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <div class="accordion-wrapper">
12
+ <h1>Frequently Asked Questions</h1>
13
+
14
+ <div class="accordion">
15
+ <div class="accordion-item">
16
+ <button class="accordion-header">
17
+ <span>What is this accordion component?</span>
18
+ <span class="icon">+</span>
19
+ </button>
20
+ <div class="accordion-content">
21
+ <div class="accordion-body">
22
+ This is a fully functional accordion component that uses DOM manipulation to toggle content visibility. Click on any header to expand or collapse the content.
23
+ </div>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="accordion-item">
28
+ <button class="accordion-header">
29
+ <span>How does it work?</span>
30
+ <span class="icon">+</span>
31
+ </button>
32
+ <div class="accordion-content">
33
+ <div class="accordion-body">
34
+ The accordion uses JavaScript to toggle classes on DOM elements. When you click a header, it adds or removes the 'active' class to expand or collapse the content section.
35
+ </div>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="accordion-item">
40
+ <button class="accordion-header">
41
+ <span>Can I customize the content?</span>
42
+ <span class="icon">+</span>
43
+ </button>
44
+ <div class="accordion-content">
45
+ <div class="accordion-body">
46
+ Absolutely! You can easily modify the HTML to add your own questions and answers. The styling and functionality will work with any content you add.
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ <div class="accordion-item">
52
+ <button class="accordion-header">
53
+ <span>Is it mobile responsive?</span>
54
+ <span class="icon">+</span>
55
+ </button>
56
+ <div class="accordion-content">
57
+ <div class="accordion-body">
58
+ Yes! This accordion is fully responsive and works great on all screen sizes - desktop, tablet, and mobile devices.
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ <script src="script.js"></script>
66
+ </body>
67
+ </html>
@@ -0,0 +1,29 @@
1
+ // Accordion Component
2
+
3
+ const accordionHeaders = document.querySelectorAll('.accordion-header');
4
+
5
+ // Toggle accordion
6
+ function toggleAccordion(e) {
7
+ const header = e.currentTarget;
8
+ const item = header.parentElement;
9
+
10
+ // Close all other items
11
+ document.querySelectorAll('.accordion-item').forEach(accordionItem => {
12
+ if (accordionItem !== item) {
13
+ accordionItem.classList.remove('active');
14
+ }
15
+ });
16
+
17
+ // Toggle current item
18
+ item.classList.toggle('active');
19
+ }
20
+
21
+ // Add event listeners
22
+ accordionHeaders.forEach(header => {
23
+ header.addEventListener('click', toggleAccordion);
24
+ });
25
+
26
+ // Optional: Open first item by default
27
+ if (accordionHeaders.length > 0) {
28
+ accordionHeaders[0].parentElement.classList.add('active');
29
+ }