nibula 2.0.2 → 2.2.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/.eleventy.js +68 -8
- package/CHANGELOG.md +108 -0
- package/bin/create.js +2 -25
- package/docs/Components.md +88 -47
- package/docs/Creating pages.md +4 -2
- package/docs/Deploy.md +185 -127
- package/docs/Head and SEO.md +99 -57
- package/docs/Project structure.md +3 -2
- package/docs/Styling with SCSS.md +3 -0
- package/package.json +3 -1
- package/src/frontend/layouts/base.njk +1 -1
- package/src/frontend/routes/404.njk +8 -3
- package/src/frontend/routes/index.njk +8 -3
- package/src/frontend/scss/_global.scss +3 -0
- package/src/frontend/scss/modules/_markdown.scss +1598 -0
- package/tools/assistant.js +21 -2
- package/tools/cli/prompt.js +99 -13
- package/tools/config/messages.json +1 -0
- package/tools/config/settings.json +10 -4
- package/tools/lib/files.js +5 -0
- package/tools/lib/layouts.js +15 -0
- package/tools/lib/pageActions.js +13 -5
- package/tools/lib/paths.js +1 -0
- package/tools/res/templates/template.njk +8 -3
package/tools/assistant.js
CHANGED
|
@@ -5,11 +5,13 @@ const { validatePageName, validateOutputPath, checkRequiredFiles } = require('./
|
|
|
5
5
|
const { addPage, removePage, renamePage } = require('./lib/pageActions');
|
|
6
6
|
const { pageExists } = require('./lib/pageArtifacts');
|
|
7
7
|
const { updateOutputPath, getCurrentOutputPath } = require('./lib/outputPath');
|
|
8
|
-
const {
|
|
8
|
+
const { availableLayouts } = require('./lib/layouts');
|
|
9
|
+
const { ask, confirm, askChoice, close } = require('./cli/prompt');
|
|
9
10
|
const ui = require('./cli/ui');
|
|
10
11
|
|
|
11
12
|
const NEW_LINE = '\n';
|
|
12
13
|
const NO_PREFIX = '';
|
|
14
|
+
const NO_LAYOUT = '';
|
|
13
15
|
|
|
14
16
|
async function askPageName(prompt) {
|
|
15
17
|
const raw = await ask(prompt);
|
|
@@ -24,9 +26,26 @@ async function askPageName(prompt) {
|
|
|
24
26
|
return name;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
async function askLayout() {
|
|
30
|
+
const layouts = availableLayouts();
|
|
31
|
+
if (layouts.length === 0) return NO_LAYOUT;
|
|
32
|
+
if (layouts.length === 1) return layouts[0];
|
|
33
|
+
|
|
34
|
+
const choices = layouts.map((name) => ({ label: name, value: name }));
|
|
35
|
+
return askChoice(message('cli.layoutPrompt'), choices);
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
async function handleCreate() {
|
|
28
39
|
const name = await askPageName(ui.promptLine(message('cli.createPrompt')));
|
|
29
|
-
if (name)
|
|
40
|
+
if (!name) return;
|
|
41
|
+
|
|
42
|
+
const layoutName = await askLayout();
|
|
43
|
+
if (layoutName === null) {
|
|
44
|
+
ui.notice(message('cli.cancelled'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
addPage(name, layoutName);
|
|
30
49
|
}
|
|
31
50
|
|
|
32
51
|
async function handleRemove() {
|
package/tools/cli/prompt.js
CHANGED
|
@@ -1,30 +1,58 @@
|
|
|
1
1
|
const readline = require('readline');
|
|
2
2
|
const settings = require('../config/settings.json');
|
|
3
|
-
const { color } = require('../lib/colors');
|
|
3
|
+
const { color, paint } = require('../lib/colors');
|
|
4
4
|
|
|
5
5
|
const CONTROL_CHARACTERS_PATTERN = /[\x00-\x1F\x7F]/g;
|
|
6
6
|
const AFFIRMATIVE_PATTERN = /^y(es)?$/i;
|
|
7
7
|
const CONFIRM_HINT = settings.cli.confirmHint;
|
|
8
8
|
const EMPTY = '';
|
|
9
|
+
const NEW_LINE = '\n';
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const CURSOR_UP_PREFIX = '\x1B[';
|
|
12
|
+
const CURSOR_UP_SUFFIX = 'A';
|
|
13
|
+
const CLEAR_LINE = '\x1B[K';
|
|
14
|
+
const CHOICE_INDENT = ' ';
|
|
15
|
+
|
|
16
|
+
const SELECTED_SYMBOL = settings.cli.symbols.selected;
|
|
17
|
+
const UNSELECTED_SYMBOL = settings.cli.symbols.unselected;
|
|
18
|
+
const SELECTED_COLOR = settings.cli.colors.selected;
|
|
19
|
+
|
|
20
|
+
const EXIT_KEYS = ['c', 'd'];
|
|
21
|
+
const UP_KEY = 'up';
|
|
22
|
+
const DOWN_KEY = 'down';
|
|
23
|
+
const CONFIRM_KEYS = ['return', 'enter'];
|
|
24
|
+
|
|
25
|
+
let activeInterface = null;
|
|
15
26
|
|
|
16
27
|
function sanitizeInput(value) {
|
|
17
28
|
return (value ?? EMPTY).replace(CONTROL_CHARACTERS_PATTERN, EMPTY).trim();
|
|
18
29
|
}
|
|
19
30
|
|
|
31
|
+
function cursorUp(lines) {
|
|
32
|
+
return `${CURSOR_UP_PREFIX}${lines}${CURSOR_UP_SUFFIX}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
function ask(prompt) {
|
|
21
36
|
return new Promise((resolve) => {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
resolve(sanitizeInput(answer));
|
|
37
|
+
const readlineInterface = readline.createInterface({
|
|
38
|
+
input: process.stdin,
|
|
39
|
+
output: process.stdout,
|
|
40
|
+
terminal: true,
|
|
27
41
|
});
|
|
42
|
+
|
|
43
|
+
activeInterface = readlineInterface;
|
|
44
|
+
let settled = false;
|
|
45
|
+
|
|
46
|
+
const finish = (value) => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
activeInterface = null;
|
|
50
|
+
readlineInterface.close();
|
|
51
|
+
resolve(value);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
readlineInterface.on('close', () => finish(null));
|
|
55
|
+
readlineInterface.question(prompt, (answer) => finish(sanitizeInput(answer)));
|
|
28
56
|
});
|
|
29
57
|
}
|
|
30
58
|
|
|
@@ -33,8 +61,66 @@ async function confirm(prompt) {
|
|
|
33
61
|
return AFFIRMATIVE_PATTERN.test((answer ?? EMPTY).trim());
|
|
34
62
|
}
|
|
35
63
|
|
|
64
|
+
function askChoice(prompt, choices) {
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
let selectedIndex = 0;
|
|
67
|
+
let settled = false;
|
|
68
|
+
|
|
69
|
+
process.stdout.write(`${prompt}${NEW_LINE}${NEW_LINE}`);
|
|
70
|
+
|
|
71
|
+
const render = (firstRender) => {
|
|
72
|
+
if (!firstRender) process.stdout.write(cursorUp(choices.length));
|
|
73
|
+
|
|
74
|
+
const output = choices.map((choice, index) => {
|
|
75
|
+
const selected = index === selectedIndex;
|
|
76
|
+
const symbol = selected ? SELECTED_SYMBOL : UNSELECTED_SYMBOL;
|
|
77
|
+
const label = `${CHOICE_INDENT}${symbol} ${choice.label}`;
|
|
78
|
+
const line = selected ? paint(SELECTED_COLOR, label) : label;
|
|
79
|
+
return `${line}${CLEAR_LINE}${NEW_LINE}`;
|
|
80
|
+
}).join(EMPTY);
|
|
81
|
+
|
|
82
|
+
process.stdout.write(output);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const finish = (value) => {
|
|
86
|
+
if (settled) return;
|
|
87
|
+
settled = true;
|
|
88
|
+
process.stdin.removeListener('keypress', onKeyPress);
|
|
89
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
90
|
+
process.stdin.pause();
|
|
91
|
+
resolve(value);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function onKeyPress(character, key) {
|
|
95
|
+
if (key.ctrl && EXIT_KEYS.includes(key.name)) {
|
|
96
|
+
finish(null);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (key.name === UP_KEY) {
|
|
100
|
+
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : choices.length - 1;
|
|
101
|
+
render(false);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (key.name === DOWN_KEY) {
|
|
105
|
+
selectedIndex = selectedIndex < choices.length - 1 ? selectedIndex + 1 : 0;
|
|
106
|
+
render(false);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (CONFIRM_KEYS.includes(key.name)) {
|
|
110
|
+
finish(choices[selectedIndex].value);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
readline.emitKeypressEvents(process.stdin);
|
|
115
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
116
|
+
process.stdin.resume();
|
|
117
|
+
process.stdin.on('keypress', onKeyPress);
|
|
118
|
+
render(true);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
36
122
|
function close() {
|
|
37
|
-
|
|
123
|
+
if (activeInterface) activeInterface.close();
|
|
38
124
|
}
|
|
39
125
|
|
|
40
|
-
module.exports = { ask, confirm, close };
|
|
126
|
+
module.exports = { ask, confirm, askChoice, close };
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"renameToPrompt": "New name: ",
|
|
69
69
|
"outputPathPrompt": "New output path: ",
|
|
70
70
|
"menuPrompt": "Choose an option: ",
|
|
71
|
+
"layoutPrompt": "Select a layout (arrow keys, Enter to confirm):",
|
|
71
72
|
"currentOutputPath": "Current path: \"{path}\"",
|
|
72
73
|
"confirmRemove": "This permanently deletes all files for \"{name}\".",
|
|
73
74
|
"cancelled": "Cancelled.",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"stylePages": "src/frontend/scss/pages",
|
|
14
14
|
"pagesData": "src/frontend/data/pages.json",
|
|
15
15
|
"siteData": "src/frontend/data/site.json",
|
|
16
|
-
"templates": "tools/res/templates"
|
|
16
|
+
"templates": "tools/res/templates",
|
|
17
|
+
"layouts": "src/frontend/layouts"
|
|
17
18
|
},
|
|
18
19
|
"languages": {
|
|
19
20
|
"javascript": {
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"routeExtension": "njk",
|
|
70
71
|
"styleTemplate": "template.scss",
|
|
71
72
|
"routeTemplate": "template.njk",
|
|
73
|
+
"layoutExtension": "njk",
|
|
72
74
|
"outputFiles": [
|
|
73
75
|
"js/pages/{camelName}.js",
|
|
74
76
|
"css/pages/{camelName}.css",
|
|
@@ -95,7 +97,8 @@
|
|
|
95
97
|
},
|
|
96
98
|
"frontMatter": {
|
|
97
99
|
"title": "title: \"{camelName}\"",
|
|
98
|
-
"permalink": "permalink: \"/{pageName}/\""
|
|
100
|
+
"permalink": "permalink: \"/{pageName}/\"",
|
|
101
|
+
"layout": "layout: {layoutName}"
|
|
99
102
|
},
|
|
100
103
|
"cli": {
|
|
101
104
|
"title": "Nibula CLI",
|
|
@@ -105,7 +108,9 @@
|
|
|
105
108
|
"prompt": "\u276f",
|
|
106
109
|
"error": "\u2716",
|
|
107
110
|
"warning": "\u26a0",
|
|
108
|
-
"bullet": "-"
|
|
111
|
+
"bullet": "-",
|
|
112
|
+
"selected": "\u25c9",
|
|
113
|
+
"unselected": "*"
|
|
109
114
|
},
|
|
110
115
|
"colors": {
|
|
111
116
|
"box": "cyan",
|
|
@@ -113,7 +118,8 @@
|
|
|
113
118
|
"prompt": "cyan",
|
|
114
119
|
"error": "red",
|
|
115
120
|
"warning": "yellow",
|
|
116
|
-
"notice": "dim"
|
|
121
|
+
"notice": "dim",
|
|
122
|
+
"selected": "cyan"
|
|
117
123
|
},
|
|
118
124
|
"menu": [
|
|
119
125
|
{ "key": "1", "action": "createPage", "label": "Create page" },
|
package/tools/lib/files.js
CHANGED
|
@@ -46,6 +46,10 @@ function removeFile(target) {
|
|
|
46
46
|
return true;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function readDirectory(directoryPath) {
|
|
50
|
+
return fs.readdirSync(directoryPath);
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
function removeDirectory(target) {
|
|
50
54
|
if (!exists(target)) return false;
|
|
51
55
|
fs.rmSync(target, { recursive: true, force: true });
|
|
@@ -71,6 +75,7 @@ module.exports = {
|
|
|
71
75
|
copyFile,
|
|
72
76
|
moveFile,
|
|
73
77
|
removeFile,
|
|
78
|
+
readDirectory,
|
|
74
79
|
removeDirectory,
|
|
75
80
|
removeDirectoryIfEmpty,
|
|
76
81
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const settings = require('../config/settings.json');
|
|
2
|
+
const { PATHS } = require('./paths');
|
|
3
|
+
const { exists, readDirectory } = require('./files');
|
|
4
|
+
|
|
5
|
+
const LAYOUT_SUFFIX = `.${settings.page.layoutExtension}`;
|
|
6
|
+
|
|
7
|
+
function availableLayouts() {
|
|
8
|
+
if (!exists(PATHS.layouts)) return [];
|
|
9
|
+
|
|
10
|
+
return readDirectory(PATHS.layouts)
|
|
11
|
+
.filter((name) => name.endsWith(LAYOUT_SUFFIX))
|
|
12
|
+
.sort();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { availableLayouts };
|
package/tools/lib/pageActions.js
CHANGED
|
@@ -11,13 +11,18 @@ const { readPagesData, addPageData, removePageData, renamePageData } = require('
|
|
|
11
11
|
const FRONT_MATTER_PATTERNS = Object.freeze({
|
|
12
12
|
title: /^title:.*$/m,
|
|
13
13
|
permalink: /^permalink:.*$/m,
|
|
14
|
+
layout: /^layout:.*$/m,
|
|
14
15
|
});
|
|
15
16
|
|
|
17
|
+
const ROUTE_FRONT_MATTER_KEYS = ['title', 'permalink'];
|
|
18
|
+
const RENAME_FRONT_MATTER_KEYS = ['permalink'];
|
|
19
|
+
const LAYOUT_KEY = 'layout';
|
|
20
|
+
|
|
16
21
|
function templatePath(name) {
|
|
17
22
|
return path.join(PATHS.templates, name);
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
function applyFrontMatter(filePath, values, keys
|
|
25
|
+
function applyFrontMatter(filePath, values, keys) {
|
|
21
26
|
let content = readText(filePath);
|
|
22
27
|
|
|
23
28
|
for (const key of keys) {
|
|
@@ -27,14 +32,17 @@ function applyFrontMatter(filePath, values, keys = Object.keys(FRONT_MATTER_PATT
|
|
|
27
32
|
writeText(filePath, content);
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
function addPage(pageName) {
|
|
35
|
+
function addPage(pageName, layoutName) {
|
|
31
36
|
if (pageExists(pageName)) {
|
|
32
37
|
log('page.alreadyExists', { name: pageName });
|
|
33
38
|
return;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
const { camelName, source } = getPageArtifacts(pageName);
|
|
37
|
-
const values = { camelName, pageName };
|
|
42
|
+
const values = { camelName, pageName, layoutName };
|
|
43
|
+
const frontMatterKeys = layoutName
|
|
44
|
+
? [...ROUTE_FRONT_MATTER_KEYS, LAYOUT_KEY]
|
|
45
|
+
: ROUTE_FRONT_MATTER_KEYS;
|
|
38
46
|
|
|
39
47
|
const creations = [
|
|
40
48
|
{ destination: source.style, template: settings.page.styleTemplate, isRoute: false },
|
|
@@ -52,7 +60,7 @@ function addPage(pageName) {
|
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
copyFile(templateFile, destination);
|
|
55
|
-
if (isRoute) applyFrontMatter(destination, values);
|
|
63
|
+
if (isRoute) applyFrontMatter(destination, values, frontMatterKeys);
|
|
56
64
|
|
|
57
65
|
log('page.fileCreated', { path: destination });
|
|
58
66
|
}
|
|
@@ -95,7 +103,7 @@ function renamePage(oldName, newName) {
|
|
|
95
103
|
moveFile(source, destination);
|
|
96
104
|
log('page.fileRenamed', { source, destination });
|
|
97
105
|
|
|
98
|
-
if (isRoute) applyFrontMatter(destination, values,
|
|
106
|
+
if (isRoute) applyFrontMatter(destination, values, RENAME_FRONT_MATTER_KEYS);
|
|
99
107
|
}
|
|
100
108
|
} catch (error) {
|
|
101
109
|
log('page.renameFailed', { error: error.message });
|
package/tools/lib/paths.js
CHANGED
|
@@ -47,6 +47,7 @@ const PATHS = Object.freeze({
|
|
|
47
47
|
get siteData() { return resolveFromProject(settings.paths.siteData); },
|
|
48
48
|
get pagesData() { return resolveFromProject(settings.paths.pagesData); },
|
|
49
49
|
get templates() { return resolveFromPackage(settings.paths.templates); },
|
|
50
|
+
get layouts() { return resolveFromProject(settings.paths.layouts); },
|
|
50
51
|
});
|
|
51
52
|
|
|
52
53
|
function getNotInsideProjectMessage() {
|
|
@@ -4,7 +4,12 @@ permalink: ""
|
|
|
4
4
|
layout: base.njk
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
{# To keep order, create a new component in components/ and include it here #}
|
|
8
|
+
{% include "component.njk" ignore missing %}
|
|
9
9
|
|
|
10
|
-
{
|
|
10
|
+
{# Markdown files live in components/ too. Pass the data the file needs #}
|
|
11
|
+
{#
|
|
12
|
+
<article class="markdown-body">
|
|
13
|
+
{% renderFile "your-file.md" | markdownPath, { site: site } %}
|
|
14
|
+
</article>
|
|
15
|
+
#}
|