create-berna-stencil 2.5.0 → 2.6.1

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 (188) hide show
  1. package/.eleventy.js +1 -1
  2. package/.gitignore +5 -4
  3. package/README.md +5 -6
  4. package/_tools/assistant.js +92 -88
  5. package/_tools/modules/constants.js +23 -0
  6. package/_tools/modules/pageComponents.js +77 -0
  7. package/_tools/modules/updateData.js +25 -44
  8. package/_tools/modules/updateOutputPath.js +59 -63
  9. package/_tools/modules/updatePage.js +131 -104
  10. package/_tools/modules/utils.js +16 -19
  11. package/_tools/modules/validation.js +20 -0
  12. package/_tools/res/templates/template.njk +2 -2
  13. package/bin/create.js +76 -14
  14. package/docs/Assistant CLI.md +3 -3
  15. package/docs/Components.md +4 -4
  16. package/docs/Creating pages.md +3 -3
  17. package/docs/Styling with SCSS.md +1 -1
  18. package/package.json +50 -65
  19. package/src/backend/_core/vendor/composer/autoload_static.php +10 -10
  20. package/src/backend/_core/vendor/composer/installed.json +6 -6
  21. package/src/backend/_core/vendor/composer/installed.php +2 -2
  22. package/src/backend/_core/vendor/graham-campbell/result-type/.gitattributes +9 -0
  23. package/src/backend/_core/vendor/graham-campbell/result-type/.github/CODE_OF_CONDUCT.md +132 -0
  24. package/src/backend/_core/vendor/graham-campbell/result-type/.github/CONTRIBUTING.md +31 -0
  25. package/src/backend/_core/vendor/graham-campbell/result-type/.github/FUNDING.yml +2 -0
  26. package/src/backend/_core/vendor/graham-campbell/result-type/.github/SECURITY.md +14 -0
  27. package/src/backend/_core/vendor/graham-campbell/result-type/.github/workflows/stale.yml +11 -0
  28. package/src/backend/_core/vendor/graham-campbell/result-type/.github/workflows/tests.yml +40 -0
  29. package/src/backend/_core/vendor/graham-campbell/result-type/CHANGELOG.md +53 -0
  30. package/src/backend/_core/vendor/graham-campbell/result-type/LICENSE +21 -21
  31. package/src/backend/_core/vendor/graham-campbell/result-type/README.md +42 -0
  32. package/src/backend/_core/vendor/graham-campbell/result-type/composer.json +33 -33
  33. package/src/backend/_core/vendor/graham-campbell/result-type/phpunit.xml.dist +13 -0
  34. package/src/backend/_core/vendor/graham-campbell/result-type/src/Error.php +121 -121
  35. package/src/backend/_core/vendor/graham-campbell/result-type/src/Result.php +69 -69
  36. package/src/backend/_core/vendor/graham-campbell/result-type/src/Success.php +120 -120
  37. package/src/backend/_core/vendor/graham-campbell/result-type/tests/ResultTest.php +95 -0
  38. package/src/backend/_core/vendor/phpoption/phpoption/.gitattributes +13 -0
  39. package/src/backend/_core/vendor/phpoption/phpoption/.github/CODE_OF_CONDUCT.md +132 -0
  40. package/src/backend/_core/vendor/phpoption/phpoption/.github/CONTRIBUTING.md +30 -0
  41. package/src/backend/_core/vendor/phpoption/phpoption/.github/FUNDING.yml +2 -0
  42. package/src/backend/_core/vendor/phpoption/phpoption/.github/SECURITY.md +14 -0
  43. package/src/backend/_core/vendor/phpoption/phpoption/.github/workflows/static.yml +40 -0
  44. package/src/backend/_core/vendor/phpoption/phpoption/.github/workflows/tests.yml +40 -0
  45. package/src/backend/_core/vendor/phpoption/phpoption/LICENSE +200 -200
  46. package/src/backend/_core/vendor/phpoption/phpoption/Makefile +17 -0
  47. package/src/backend/_core/vendor/phpoption/phpoption/README.md +201 -0
  48. package/src/backend/_core/vendor/phpoption/phpoption/composer.json +50 -50
  49. package/src/backend/_core/vendor/phpoption/phpoption/phpstan-baseline.neon +44 -0
  50. package/src/backend/_core/vendor/phpoption/phpoption/phpstan.neon.dist +7 -0
  51. package/src/backend/_core/vendor/phpoption/phpoption/phpunit.xml.dist +13 -0
  52. package/src/backend/_core/vendor/phpoption/phpoption/src/PhpOption/LazyOption.php +175 -175
  53. package/src/backend/_core/vendor/phpoption/phpoption/src/PhpOption/None.php +136 -136
  54. package/src/backend/_core/vendor/phpoption/phpoption/src/PhpOption/Option.php +434 -434
  55. package/src/backend/_core/vendor/phpoption/phpoption/src/PhpOption/Some.php +169 -169
  56. package/src/backend/_core/vendor/phpoption/phpoption/tests/PhpOption/Tests/EnsureTest.php +72 -0
  57. package/src/backend/_core/vendor/phpoption/phpoption/tests/PhpOption/Tests/LazyOptionTest.php +357 -0
  58. package/src/backend/_core/vendor/phpoption/phpoption/tests/PhpOption/Tests/NoneTest.php +153 -0
  59. package/src/backend/_core/vendor/phpoption/phpoption/tests/PhpOption/Tests/OptionTest.php +166 -0
  60. package/src/backend/_core/vendor/phpoption/phpoption/tests/PhpOption/Tests/SomeTest.php +194 -0
  61. package/src/backend/_core/vendor/phpoption/phpoption/tests/bootstrap.php +8 -0
  62. package/src/backend/_core/vendor/phpoption/phpoption/vendor-bin/phpstan/composer.json +8 -0
  63. package/src/backend/_core/vendor/symfony/polyfill-ctype/Ctype.php +232 -232
  64. package/src/backend/_core/vendor/symfony/polyfill-ctype/LICENSE +19 -19
  65. package/src/backend/_core/vendor/symfony/polyfill-ctype/README.md +12 -12
  66. package/src/backend/_core/vendor/symfony/polyfill-ctype/bootstrap.php +50 -50
  67. package/src/backend/_core/vendor/symfony/polyfill-ctype/bootstrap80.php +46 -46
  68. package/src/backend/_core/vendor/symfony/polyfill-ctype/composer.json +38 -38
  69. package/src/backend/_core/vendor/symfony/polyfill-mbstring/LICENSE +19 -19
  70. package/src/backend/_core/vendor/symfony/polyfill-mbstring/Mbstring.php +1077 -1077
  71. package/src/backend/_core/vendor/symfony/polyfill-mbstring/README.md +13 -13
  72. package/src/backend/_core/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php +119 -119
  73. package/src/backend/_core/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php +1397 -1397
  74. package/src/backend/_core/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php +5 -5
  75. package/src/backend/_core/vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php +1489 -1489
  76. package/src/backend/_core/vendor/symfony/polyfill-mbstring/bootstrap.php +171 -171
  77. package/src/backend/_core/vendor/symfony/polyfill-mbstring/bootstrap80.php +167 -167
  78. package/src/backend/_core/vendor/symfony/polyfill-mbstring/composer.json +39 -39
  79. package/src/backend/_core/vendor/symfony/polyfill-php80/LICENSE +19 -19
  80. package/src/backend/_core/vendor/symfony/polyfill-php80/Php80.php +115 -115
  81. package/src/backend/_core/vendor/symfony/polyfill-php80/PhpToken.php +106 -106
  82. package/src/backend/_core/vendor/symfony/polyfill-php80/README.md +25 -25
  83. package/src/backend/_core/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php +31 -31
  84. package/src/backend/_core/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php +16 -16
  85. package/src/backend/_core/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php +20 -20
  86. package/src/backend/_core/vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php +16 -16
  87. package/src/backend/_core/vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php +16 -16
  88. package/src/backend/_core/vendor/symfony/polyfill-php80/bootstrap.php +42 -42
  89. package/src/backend/_core/vendor/symfony/polyfill-php80/composer.json +37 -37
  90. package/src/backend/_core/vendor/vlucas/phpdotenv/.editorconfig +15 -0
  91. package/src/backend/_core/vendor/vlucas/phpdotenv/.gitattributes +15 -0
  92. package/src/backend/_core/vendor/vlucas/phpdotenv/.github/CODE_OF_CONDUCT.md +132 -0
  93. package/src/backend/_core/vendor/vlucas/phpdotenv/.github/CONTRIBUTING.md +30 -0
  94. package/src/backend/_core/vendor/vlucas/phpdotenv/.github/FUNDING.yml +2 -0
  95. package/src/backend/_core/vendor/vlucas/phpdotenv/.github/SECURITY.md +14 -0
  96. package/src/backend/_core/vendor/vlucas/phpdotenv/.github/workflows/static.yml +40 -0
  97. package/src/backend/_core/vendor/vlucas/phpdotenv/.github/workflows/tests.yml +70 -0
  98. package/src/backend/_core/vendor/vlucas/phpdotenv/LICENSE +30 -30
  99. package/src/backend/_core/vendor/vlucas/phpdotenv/Makefile +17 -0
  100. package/src/backend/_core/vendor/vlucas/phpdotenv/README.md +370 -0
  101. package/src/backend/_core/vendor/vlucas/phpdotenv/UPGRADING.md +196 -0
  102. package/src/backend/_core/vendor/vlucas/phpdotenv/composer.json +60 -60
  103. package/src/backend/_core/vendor/vlucas/phpdotenv/phpstan-baseline.neon +157 -0
  104. package/src/backend/_core/vendor/vlucas/phpdotenv/phpstan.neon.dist +7 -0
  105. package/src/backend/_core/vendor/vlucas/phpdotenv/phpunit.xml.dist +13 -0
  106. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Dotenv.php +267 -267
  107. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Exception/ExceptionInterface.php +12 -12
  108. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Exception/InvalidEncodingException.php +12 -12
  109. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Exception/InvalidFileException.php +12 -12
  110. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Exception/InvalidPathException.php +12 -12
  111. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Exception/ValidationException.php +12 -12
  112. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Loader/Loader.php +48 -48
  113. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Loader/LoaderInterface.php +20 -20
  114. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Loader/Resolver.php +65 -65
  115. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Parser/Entry.php +59 -59
  116. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Parser/EntryParser.php +299 -299
  117. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Parser/Lexer.php +58 -58
  118. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Parser/Lines.php +127 -127
  119. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Parser/Parser.php +53 -53
  120. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Parser/ParserInterface.php +19 -19
  121. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Parser/Value.php +88 -88
  122. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/AdapterInterface.php +15 -15
  123. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/ApacheAdapter.php +89 -89
  124. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/ArrayAdapter.php +80 -80
  125. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php +88 -88
  126. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/GuardedWriter.php +85 -85
  127. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/ImmutableWriter.php +110 -110
  128. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/MultiReader.php +48 -48
  129. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/MultiWriter.php +64 -64
  130. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/PutenvAdapter.php +91 -91
  131. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/ReaderInterface.php +17 -17
  132. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/ReplacingWriter.php +104 -104
  133. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php +88 -88
  134. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/Adapter/WriterInterface.php +27 -27
  135. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/AdapterRepository.php +107 -107
  136. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/RepositoryBuilder.php +272 -272
  137. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Repository/RepositoryInterface.php +51 -51
  138. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Store/File/Paths.php +44 -44
  139. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Store/File/Reader.php +81 -81
  140. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Store/FileStore.php +72 -72
  141. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Store/StoreBuilder.php +141 -141
  142. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Store/StoreInterface.php +17 -17
  143. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Store/StringStore.php +37 -37
  144. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Util/Regex.php +112 -112
  145. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Util/Str.php +108 -108
  146. package/src/backend/_core/vendor/vlucas/phpdotenv/src/Validator.php +207 -207
  147. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/DotenvTest.php +387 -0
  148. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/Loader/LoaderTest.php +86 -0
  149. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/Parser/EntryParserTest.php +234 -0
  150. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/Parser/LexerTest.php +40 -0
  151. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/Parser/LinesTest.php +53 -0
  152. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/Parser/ParserTest.php +98 -0
  153. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/Repository/Adapter/ArrayAdapterTest.php +57 -0
  154. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/Repository/Adapter/EnvConstAdapterTest.php +75 -0
  155. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/Repository/Adapter/PutenvAdapterTest.php +52 -0
  156. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/Repository/Adapter/ServerConstAdapterTest.php +75 -0
  157. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/Repository/RepositoryTest.php +305 -0
  158. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/Store/StoreTest.php +141 -0
  159. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/Dotenv/ValidatorTest.php +479 -0
  160. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/.env +5 -0
  161. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/assertions.env +18 -0
  162. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/booleans.env +33 -0
  163. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/commented.env +15 -0
  164. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/empty.env +1 -0
  165. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/example.env +1 -0
  166. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/exported.env +7 -0
  167. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/immutable.env +1 -0
  168. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/integers.env +17 -0
  169. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/large.env +2 -0
  170. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/multibyte.env +3 -0
  171. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/multiline.env +14 -0
  172. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/multiple.env +4 -0
  173. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/mutable.env +1 -0
  174. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/nested.env +15 -0
  175. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/quoted.env +11 -0
  176. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/specialchars.env +8 -0
  177. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/unicodevarnames.env +2 -0
  178. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/utf8-with-bom-encoding.env +3 -0
  179. package/src/backend/_core/vendor/vlucas/phpdotenv/tests/fixtures/env/windows.env +1 -0
  180. package/src/backend/_core/vendor/vlucas/phpdotenv/vendor-bin/phpstan/composer.json +15 -0
  181. package/src/backend/{config.example.php → config.php} +4 -13
  182. package/src/frontend/data/site.json +44 -44
  183. package/src/frontend/index.njk +2 -2
  184. package/src/frontend/{components/layouts → layouts}/base.njk +1 -1
  185. package/_tools/buildJs.js +0 -28
  186. package/_tools/cleanOutput.js +0 -20
  187. package/_tools/modules/updateIncludes.js +0 -81
  188. /package/src/frontend/{components/layouts/includes.njk → layouts/pageComponents.njk} +0 -0
package/.eleventy.js CHANGED
@@ -87,7 +87,7 @@ module.exports = function (eleventyConfig) {
87
87
  input: "src/frontend",
88
88
  output: OUTPUT_DIR,
89
89
  includes: "components",
90
- layouts: "components/layouts",
90
+ layouts: "layouts",
91
91
  data: "data",
92
92
  },
93
93
  };
package/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
- node_modules/
2
- src/backend/_core/vendor/
3
- out/
4
- src/backend/config.php
1
+
2
+ node_modules/
3
+ src/backend/_core/vendor/
4
+ out/
5
+ src/backend/config.php
package/README.md CHANGED
@@ -1,18 +1,22 @@
1
1
  # ✏️ Berna-Stencil
2
2
  **Berna-Stencil** is an open source static site generator built on top of [Eleventy](https://www.11ty.dev/), designed with one clear mission: make web development simple, approachable, and enjoyable — for everyone.
3
3
 
4
+ Berna-Stencil stays deliberately close to plain, vanilla web development — you work with real HTML, CSS, and JavaScript, not a heavy abstraction layer to learn first. That means a gentle, fast learning curve: the skills you build here are the same ones you'd use anywhere on the web.
5
+
4
6
  Whether you're a seasoned developer looking for a clean starting point or a beginner taking your first steps into the world of web creation, Berna-Stencil gives you everything you need, right out of the box. No complicated setup, no hours spent reading documentation, no frustration — just a solid, well-structured foundation ready to become your next website.
5
7
 
6
8
  ### ✨ Why Berna-Stencil?
7
9
 
8
10
  Building a website from scratch involves a lot of moving parts: templating engines, build pipelines, asset management, backend logic, project structure. Berna-Stencil takes care of all of that for you, so you can focus on what actually matters — **your content and your ideas**.
9
11
 
12
+ Because it keeps you close to the fundamentals instead of hiding them, you spend your time actually learning the web — not memorizing a framework's conventions that stop being useful the moment you switch tool.
13
+
10
14
  - 🔧 **Zero-config ready** — clone, install, and you're live in minutes
11
15
  - 🔗 **Integrated backend** — essential server-side functionality included, no extra setup required
12
16
  - 📁 **Scalable structure** — a clean, opinionated project layout that grows with your needs
13
17
  - 🌍 **Open source** — free to use, free to modify, free to share
14
18
 
15
- ![Version](https://img.shields.io/badge/version-2.5.0-blue)
19
+ ![Version](https://img.shields.io/badge/version-2.6.1-blue)
16
20
  ![License](https://img.shields.io/badge/license-Apache--2.0-blue)
17
21
  ![Eleventy](https://img.shields.io/badge/11ty-v3.1.2-black)
18
22
 
@@ -33,11 +37,6 @@ Building a website from scratch involves a lot of moving parts: templating engin
33
37
  cd your-project
34
38
  ```
35
39
 
36
- * Install all the node_modules
37
- ```bash
38
- npm install
39
- ```
40
-
41
40
  * Run the command to launch the live server and build the site, then visit localhost:8080:
42
41
  ```bash
43
42
  npm run serve
@@ -1,146 +1,150 @@
1
1
  const readline = require('readline');
2
- const { addPage, removePage, renamePage } = require('./modules/updatePage');
3
- const { updateOutputPath, getCurrentOutputPath } = require('./modules/updateOutputPath');
4
2
 
5
- const c = {
6
- reset: "\x1b[0m",
7
- bold: "\x1b[1m",
8
- dim: "\x1b[2m",
9
- red: "\x1b[31m",
10
- green: "\x1b[32m",
11
- yellow: "\x1b[33m",
12
- magenta: "\x1b[35m",
13
- cyan: "\x1b[36m"
3
+ const { addPage, removePage, renamePage, pageExists } = require('./modules/updatePage');
4
+ const { updateOutputPath, getCurrentOutputPath } = require('./modules/updateOutputPath');
5
+ const { validatePageName, validateOutputPath } = require('./modules/validation');
6
+ const { toKebabCase } = require('./modules/utils');
7
+
8
+ const color = {
9
+ reset: '\x1b[0m',
10
+ bold: '\x1b[1m',
11
+ dim: '\x1b[2m',
12
+ red: '\x1b[31m',
13
+ green: '\x1b[32m',
14
+ yellow: '\x1b[33m',
15
+ magenta: '\x1b[35m',
16
+ cyan: '\x1b[36m',
14
17
  };
15
18
 
16
- const readerInterface = readline.createInterface({
19
+ const rl = readline.createInterface({
17
20
  input: process.stdin,
18
21
  output: process.stdout,
19
- terminal: true
22
+ terminal: true,
20
23
  });
21
24
 
22
- const PROTECTED_PAGES = ['homepage', '404'];
23
- const MAX_NAME_LENGTH = 50;
24
-
25
- function toKebabCase(str) {
26
- return str.trim().toLowerCase()
27
- .replace(/[^a-z0-9\s_-]/g, '')
28
- .replace(/[\s_]+/g, '-')
29
- .replace(/-+/g, '-')
30
- .replace(/^-+|-+$/g, '');
31
- }
32
-
33
- function validatePageName(name) {
34
- if (!name) return 'Invalid name.';
35
- if (name.length > MAX_NAME_LENGTH) return `Name must be ${MAX_NAME_LENGTH} characters or fewer.`;
36
- if (!/^[a-z0-9-]+$/.test(name)) return 'Page name can only contain lowercase letters, numbers, and hyphens.';
37
- if (/^\d/.test(name)) return 'Page name cannot start with a number.';
38
- if (PROTECTED_PAGES.includes(name)) return `"${name}" is a protected page name.`;
39
- return null;
40
- }
41
-
42
- function validateOutputPath(input) {
43
- if (!input.trim()) return 'Invalid path.';
44
- if (input.includes('..')) return 'Path cannot contain "..".';
45
- if (/[<>|?*"']/.test(input)) return 'Path contains invalid characters.';
46
- return null;
25
+ function sanitizeInput(value) {
26
+ return (value ?? '').replace(/[\x00-\x1F\x7F]/g, '').trim();
47
27
  }
48
28
 
49
- function sanitizeInput(str) {
50
- return str.replace(/[\x00-\x1F\x7F]/g, '').trim();
29
+ function ask(prompt) {
30
+ return new Promise((resolve) => {
31
+ const onClose = () => resolve(null);
32
+ rl.once('close', onClose);
33
+ rl.question(prompt, (answer) => {
34
+ rl.off('close', onClose);
35
+ resolve(sanitizeInput(answer));
36
+ });
37
+ });
51
38
  }
52
39
 
53
- function ask(prompt) {
54
- return new Promise(resolve =>
55
- readerInterface.question(prompt, answer => resolve(sanitizeInput(answer)))
56
- );
40
+ async function confirm(prompt) {
41
+ const answer = await ask(`${prompt} ${color.dim}[y/N]${color.reset} `);
42
+ return /^y(es)?$/i.test((answer ?? '').trim());
57
43
  }
58
44
 
59
45
  async function askPageName(prompt) {
60
46
  const raw = await ask(prompt);
47
+ if (raw === null) return null;
48
+
61
49
  const name = toKebabCase(raw);
62
50
  const error = validatePageName(name);
63
51
  if (error) {
64
- console.log(`\n${c.red}✖ ${error}${c.reset}`);
52
+ console.log(`\n${color.red}✖ ${error}${color.reset}`);
65
53
  return null;
66
54
  }
67
55
  return name;
68
56
  }
69
57
 
70
- async function handleCreateRequest() {
71
- const name = await askPageName(`\n${c.green}❯${c.reset} Enter the name of the new page: `);
72
- if (name) addPage(name, null);
58
+ async function handleCreate() {
59
+ const name = await askPageName(`\n${color.green}❯${color.reset} Name of the new page: `);
60
+ if (name) addPage(name);
73
61
  }
74
62
 
75
- async function handleRemoveRequest() {
76
- const name = await askPageName(`\n${c.red}❯${c.reset} Enter the name of the page to remove: `);
77
- if (name) removePage(name);
63
+ async function handleRemove() {
64
+ const name = await askPageName(`\n${color.red}❯${color.reset} Name of the page to remove: `);
65
+ if (!name) return;
66
+
67
+ if (!pageExists(name)) {
68
+ console.log(`\n${color.yellow}⚠ Page "${name}" does not exist.${color.reset}`);
69
+ return;
70
+ }
71
+
72
+ const confirmed = await confirm(`This permanently deletes all files for "${name}".`);
73
+ if (!confirmed) {
74
+ console.log(`\n${color.dim}Cancelled.${color.reset}`);
75
+ return;
76
+ }
77
+ removePage(name);
78
78
  }
79
79
 
80
- async function handleRenameRequest() {
81
- const oldName = await askPageName(`\n${c.yellow}❯${c.reset} Enter the name of the page to rename: `);
80
+ async function handleRename() {
81
+ const oldName = await askPageName(`\n${color.yellow}❯${color.reset} Page to rename: `);
82
82
  if (!oldName) return;
83
83
 
84
- const newName = await askPageName(`${c.yellow}❯${c.reset} Enter the new name: `);
84
+ const newName = await askPageName(`${color.yellow}❯${color.reset} New name: `);
85
85
  if (!newName) return;
86
86
 
87
87
  if (oldName === newName) {
88
- console.log(`\n${c.yellow}⚠ Old and new name are the same.${c.reset}`);
88
+ console.log(`\n${color.yellow}⚠ Old and new name are the same.${color.reset}`);
89
89
  return;
90
90
  }
91
-
92
91
  renamePage(oldName, newName);
93
92
  }
94
93
 
95
- async function handleOutputPathRequest() {
94
+ async function handleOutputPath() {
96
95
  const current = getCurrentOutputPath();
97
- const label = current ? `\n${c.dim}Current path: "${current}"${c.reset}\n` : '\n';
98
- const input = await ask(`${label}${c.magenta}❯${c.reset} Enter the new output path: `);
96
+ const label = current ? `\n${color.dim}Current path: "${current}"${color.reset}\n` : '\n';
97
+
98
+ const input = await ask(`${label}${color.magenta}❯${color.reset} New output path: `);
99
+ if (input === null) return;
99
100
 
100
101
  const error = validateOutputPath(input);
101
102
  if (error) {
102
- console.log(`\n${c.red}✖ ${error}${c.reset}`);
103
- } else {
104
- updateOutputPath(input);
103
+ console.log(`\n${color.red}✖ ${error}${color.reset}`);
104
+ return;
105
105
  }
106
+ updateOutputPath(input);
106
107
  }
107
108
 
108
109
  const MENU_ACTIONS = {
109
- '1': handleCreateRequest,
110
- '2': handleRemoveRequest,
111
- '3': handleRenameRequest,
112
- '4': handleOutputPathRequest,
110
+ '1': handleCreate,
111
+ '2': handleRemove,
112
+ '3': handleRename,
113
+ '4': handleOutputPath,
113
114
  };
114
115
 
115
- async function displayMainMenu() {
116
- console.log(`\n${c.cyan}${c.bold}╭────────────────────────╮`);
116
+ function renderMenu() {
117
+ console.log(`\n${color.cyan}${color.bold}╭────────────────────────╮`);
117
118
  console.log(`│ Berna-Stencil CLI │`);
118
- console.log(`╰────────────────────────╯${c.reset}\n`);
119
- console.log(` ${c.green}1.${c.reset} Create page`);
120
- console.log(` ${c.red}2.${c.reset} Remove page`);
121
- console.log(` ${c.yellow}3.${c.reset} Rename page`);
122
- console.log(` ${c.magenta}4.${c.reset} Configure output path`);
123
- console.log(`\n ${c.dim}CTRL/CMD + C to exit${c.reset}\n`);
124
-
125
- const choice = (await ask(`${c.cyan}❯${c.reset} Choose an option: `)).trim();
126
-
127
- if (choice === '0') {
128
- readerInterface.close();
129
- process.exit(0);
130
- }
119
+ console.log(`╰────────────────────────╯${color.reset}\n`);
120
+ console.log(` ${color.green}1.${color.reset} Create page`);
121
+ console.log(` ${color.red}2.${color.reset} Remove page`);
122
+ console.log(` ${color.yellow}3.${color.reset} Rename page`);
123
+ console.log(` ${color.magenta}4.${color.reset} Configure output path`);
124
+ console.log(` ${color.dim}CTRL + C to exit\n`);
125
+ }
126
+
127
+ async function main() {
128
+ while (true) {
129
+ renderMenu();
130
+
131
+ const choice = await ask(`${color.cyan}❯${color.reset} Choose an option: `);
132
+
133
+ const action = MENU_ACTIONS[choice];
134
+ if (!action) {
135
+ console.log(`\n${color.red}✖ Invalid option.${color.reset}`);
136
+ continue;
137
+ }
131
138
 
132
- const action = MENU_ACTIONS[choice];
133
- if (action) {
134
139
  try {
135
140
  await action();
136
141
  } catch (err) {
137
- console.log(`\n${c.red}✖ Unexpected error: ${err.message}${c.reset}`);
142
+ console.log(`\n${color.red}✖ Unexpected error: ${err.message}${color.reset}`);
138
143
  }
139
- } else {
140
- console.log(`\n${c.red}✖ Invalid option.${c.reset}`);
141
144
  }
142
145
 
143
- displayMainMenu();
146
+ rl.close();
147
+ process.exit(0);
144
148
  }
145
149
 
146
- displayMainMenu();
150
+ main();
@@ -0,0 +1,23 @@
1
+ const path = require('path');
2
+
3
+ const ROOT = path.resolve(__dirname, '..', '..');
4
+ const FRONTEND_DIR = path.join(ROOT, 'src', 'frontend');
5
+
6
+ const MAX_PAGE_NAME_LENGTH = 50;
7
+ const PROTECTED_PAGES = Object.freeze(['homepage', '404']);
8
+
9
+ const PATHS = Object.freeze({
10
+ root: ROOT,
11
+ routes: path.join(FRONTEND_DIR, '_routes'),
12
+ scssPages: path.join(FRONTEND_DIR, 'scss', 'pages'),
13
+ jsPages: path.join(FRONTEND_DIR, 'js', 'pages'),
14
+ tsPages: path.join(FRONTEND_DIR, 'ts', 'pages'),
15
+ siteData: path.join(FRONTEND_DIR, 'data', 'site.json'),
16
+ pageComponents: path.join(FRONTEND_DIR, 'layouts', 'pageComponents.njk'),
17
+ templates: path.join(ROOT, '_tools', 'res', 'templates'),
18
+ eleventyConfig: path.join(ROOT, '.eleventy.js'),
19
+ packageJson: path.join(ROOT, 'package.json'),
20
+ tsconfig: path.join(ROOT, 'tsconfig.json'),
21
+ });
22
+
23
+ module.exports = { PATHS, MAX_PAGE_NAME_LENGTH, PROTECTED_PAGES };
@@ -0,0 +1,77 @@
1
+ const fs = require('fs');
2
+ const { PATHS } = require('./constants');
3
+ const { toCamelCase } = require('./utils');
4
+
5
+ function readPageComponents() {
6
+ if (!fs.existsSync(PATHS.pageComponents)) {
7
+ console.log(`[skip] not found: ${PATHS.pageComponents}`);
8
+ return null;
9
+ }
10
+ return fs.readFileSync(PATHS.pageComponents, 'utf8');
11
+ }
12
+
13
+ function writePageComponents(content) {
14
+ fs.writeFileSync(PATHS.pageComponents, content);
15
+ }
16
+
17
+ function elifBlockRegex(camelName) {
18
+ return new RegExp(
19
+ `[ \\t]*\\{%\\s*elif\\s+title\\s*==\\s*"${camelName}"\\s*%\\}[\\s\\S]*?(?=[ \\t]*\\{%\\s*(?:elif|else|endif))`,
20
+ );
21
+ }
22
+
23
+ function addPageBlock(pageName) {
24
+ const content = readPageComponents();
25
+ if (!content) return;
26
+
27
+ const camelName = toCamelCase(pageName);
28
+ if (content.includes(`{% elif title == "${camelName}" %}`)) return;
29
+
30
+ if (!content.includes('{% else %}')) {
31
+ console.log('[skip] no {% else %} anchor in pageComponents.njk');
32
+ return;
33
+ }
34
+
35
+ const block =
36
+ `{% elif title == "${camelName}" %}\n` +
37
+ ` {#{% include "component.njk" %}#}\n\n`;
38
+
39
+ writePageComponents(content.replace('{% else %}', `${block}{% else %}`));
40
+ console.log(`[updated] page block added for "${camelName}"`);
41
+ }
42
+
43
+ function removePageBlock(pageName) {
44
+ const content = readPageComponents();
45
+ if (!content) return;
46
+
47
+ const camelName = toCamelCase(pageName);
48
+ if (!elifBlockRegex(camelName).test(content)) {
49
+ console.log(`[skip] page block for "${camelName}" not found`);
50
+ return;
51
+ }
52
+
53
+ const updated = content
54
+ .replace(elifBlockRegex(camelName), '')
55
+ .replace(/\n\s*\n\s*\n/g, '\n\n');
56
+
57
+ writePageComponents(updated);
58
+ console.log(`[cleaned] page block removed for "${camelName}"`);
59
+ }
60
+
61
+ function renamePageBlock(oldName, newName) {
62
+ const content = readPageComponents();
63
+ if (!content) return;
64
+
65
+ const oldLine = `{% elif title == "${toCamelCase(oldName)}" %}`;
66
+ const newLine = `{% elif title == "${toCamelCase(newName)}" %}`;
67
+
68
+ if (!content.includes(oldLine)) {
69
+ console.log(`[skip] page block for "${toCamelCase(oldName)}" not found`);
70
+ return;
71
+ }
72
+
73
+ writePageComponents(content.replace(oldLine, newLine));
74
+ console.log(`[renamed] page block "${toCamelCase(oldName)}" → "${toCamelCase(newName)}"`);
75
+ }
76
+
77
+ module.exports = { addPageBlock, removePageBlock, renamePageBlock };
@@ -1,72 +1,60 @@
1
- const fileSystem = require('fs');
1
+ const fs = require('fs');
2
+ const { PATHS } = require('./constants');
2
3
  const { toCamelCase, toNiceTitle } = require('./utils');
3
4
 
4
- const SITE_DATA_PATH = 'src/frontend/data/site.json';
5
-
6
- // Returns the parsed site.json content, or null if the file doesn't exist
7
5
  function readSiteData() {
8
- if (!fileSystem.existsSync(SITE_DATA_PATH)) return null;
9
- return JSON.parse(fileSystem.readFileSync(SITE_DATA_PATH, 'utf8'));
6
+ if (!fs.existsSync(PATHS.siteData)) {
7
+ console.log(`[skip] not found: ${PATHS.siteData}`);
8
+ return null;
9
+ }
10
+ try {
11
+ const data = JSON.parse(fs.readFileSync(PATHS.siteData, 'utf8'));
12
+ if (!data.pages || typeof data.pages !== 'object') data.pages = {};
13
+ return data;
14
+ } catch (err) {
15
+ console.log(`[error] cannot parse site.json: ${err.message}`);
16
+ return null;
17
+ }
10
18
  }
11
19
 
12
- // Serializes and writes the data object back to site.json
13
20
  function writeSiteData(data) {
14
- fileSystem.writeFileSync(SITE_DATA_PATH, JSON.stringify(data, null, 2));
21
+ fs.writeFileSync(PATHS.siteData, `${JSON.stringify(data, null, 2)}\n`);
15
22
  }
16
23
 
17
- // --- Public API ---
18
-
19
- // Adds a new page record to site.json
20
- // Skips silently if the file doesn't exist or the record is already present
21
24
  function addSiteData(pageName) {
22
25
  const data = readSiteData();
23
26
  if (!data) return;
24
27
 
25
28
  const camelName = toCamelCase(pageName);
26
-
27
29
  if (data.pages[camelName]) {
28
- console.log(`[SKIP] Record "${camelName}" already exists.`);
30
+ console.log(`[skip] record "${camelName}" already exists`);
29
31
  return;
30
32
  }
31
33
 
32
- // Build the default page record with SEO metadata and empty CDN arrays
33
34
  data.pages[camelName] = {
34
- seo: {
35
- title: toNiceTitle(pageName),
36
- description: 'description',
37
- },
38
- cdn: {
39
- css: [],
40
- js: []
41
- }
35
+ seo: { title: toNiceTitle(pageName), description: 'description' },
36
+ cdn: { css: [], js: [] },
42
37
  };
43
38
 
44
39
  writeSiteData(data);
45
- console.log(`[UPDATED] Record "${camelName}" added.`);
40
+ console.log(`[updated] record "${camelName}" added`);
46
41
  }
47
42
 
48
- // Removes a page record from site.json
49
- // Skips silently if the file doesn't exist or the record is not found
50
43
  function removeSiteData(pageName) {
51
44
  const data = readSiteData();
52
45
  if (!data) return;
53
46
 
54
47
  const camelName = toCamelCase(pageName);
55
-
56
48
  if (!data.pages[camelName]) {
57
- console.log(`[SKIP] Record "${camelName}" not found.`);
49
+ console.log(`[skip] record "${camelName}" not found`);
58
50
  return;
59
51
  }
60
52
 
61
53
  delete data.pages[camelName];
62
-
63
54
  writeSiteData(data);
64
- console.log(`[CLEANED] Record "${camelName}" removed.`);
55
+ console.log(`[cleaned] record "${camelName}" removed`);
65
56
  }
66
57
 
67
- // Renames a page record in site.json
68
- // Preserves all existing fields (cdn, etc.) and only updates the SEO title
69
- // Skips if the source record doesn't exist or the target name is already taken
70
58
  function renameSiteData(oldName, newName) {
71
59
  const data = readSiteData();
72
60
  if (!data) return;
@@ -75,29 +63,22 @@ function renameSiteData(oldName, newName) {
75
63
  const newCamel = toCamelCase(newName);
76
64
 
77
65
  if (!data.pages[oldCamel]) {
78
- console.log(`[SKIP] Record "${oldCamel}" not found.`);
66
+ console.log(`[skip] record "${oldCamel}" not found`);
79
67
  return;
80
68
  }
81
-
82
69
  if (data.pages[newCamel]) {
83
- console.log(`[SKIP] Record "${newCamel}" already exists.`);
70
+ console.log(`[skip] record "${newCamel}" already exists`);
84
71
  return;
85
72
  }
86
73
 
87
- // Spread the existing record to preserve cdn and any future fields,
88
- // then override only the seo.title with the new page name
89
74
  data.pages[newCamel] = {
90
75
  ...data.pages[oldCamel],
91
- seo: {
92
- ...data.pages[oldCamel].seo,
93
- title: toNiceTitle(newName),
94
- }
76
+ seo: { ...data.pages[oldCamel].seo, title: toNiceTitle(newName) },
95
77
  };
96
-
97
78
  delete data.pages[oldCamel];
98
79
 
99
80
  writeSiteData(data);
100
- console.log(`[UPDATED] Record "${oldCamel}" renamed to "${newCamel}".`);
81
+ console.log(`[updated] record "${oldCamel}" renamed to "${newCamel}"`);
101
82
  }
102
83
 
103
84
  module.exports = { addSiteData, removeSiteData, renameSiteData };