eidosmd 0.2.0 → 0.3.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.
- package/browser/dist/assets/index-D_Xs1hAc.css +1 -0
- package/browser/dist/assets/index-K_EgH2M8.js +46 -0
- package/browser/dist/index.html +2 -2
- package/dist/src/commands/check.js +9 -3
- package/dist/src/commands/configure.js +201 -0
- package/dist/src/commands/framework.js +15 -4
- package/dist/src/commands/init.js +4 -0
- package/dist/src/commands/property.js +125 -0
- package/dist/src/commands/setup.js +24 -12
- package/dist/src/commands/version.js +2 -2
- package/dist/src/core/canvas.js +59 -49
- package/dist/src/core/check.js +101 -26
- package/dist/src/core/edits.js +1381 -0
- package/dist/src/core/framework-markdown.js +9 -3
- package/dist/src/core/framework-model.js +43 -8
- package/dist/src/core/framework-structured.js +104 -28
- package/dist/src/core/frontmatter.js +61 -1
- package/dist/src/core/git.js +28 -3
- package/dist/src/core/links.js +87 -0
- package/dist/src/core/markdown.js +16 -9
- package/dist/src/core/migrate.js +91 -15
- package/dist/src/core/regions.js +117 -0
- package/dist/src/core/scaffold.js +15 -9
- package/dist/src/core/seed.js +56 -31
- package/dist/src/core/server.js +204 -31
- package/dist/src/core/settings.js +63 -12
- package/dist/src/core/store.js +73 -17
- package/dist/src/core/versions.js +10 -5
- package/dist/src/program.js +296 -11
- package/instructions/authoring.md +4 -2
- package/instructions/configuring.md +27 -11
- package/instructions/overview.md +6 -4
- package/instructions/validating.md +6 -4
- package/package.json +1 -1
- package/standard/EIDOS.md +135 -193
- package/standard/seeds/README.md +12 -16
- package/standard/seeds/book/Framework.yaml +30 -50
- package/standard/seeds/book/README.md +2 -1
- package/standard/seeds/book/_gitignore +7 -1
- package/standard/seeds/research/Framework.yaml +30 -50
- package/standard/seeds/research/README.md +2 -1
- package/standard/seeds/research/_gitignore +7 -1
- package/standard/seeds/software/Framework.yaml +31 -51
- package/standard/seeds/software/README.md +2 -1
- package/standard/seeds/software/_gitignore +7 -1
- package/browser/dist/assets/index-C2NMN_D4.css +0 -1
- package/browser/dist/assets/index-C65k1ihb.js +0 -46
package/dist/src/core/migrate.js
CHANGED
|
@@ -1,23 +1,40 @@
|
|
|
1
|
-
// `eidos migrate`: the mechanical hop from
|
|
2
|
-
// carries
|
|
3
|
-
// becomes `.eidos/`, `shapes/` becomes `templates/`, the framework
|
|
4
|
-
// takes the 5.0.0 keys, every blueprint's `flavor` becomes
|
|
5
|
-
// `eidos_version` is set. A root that kept the markdown
|
|
6
|
-
// Framework.yaml in its place, the index inside it. A
|
|
7
|
-
// with frontmatter loses the block; one named off its
|
|
8
|
-
// only the owner can say which name is right.
|
|
9
|
-
|
|
1
|
+
// `eidos migrate`: the mechanical hop from an older root to the standard this
|
|
2
|
+
// CLI carries. From 4.x, five names move on disk and nothing is dropped:
|
|
3
|
+
// `_eidos/` becomes `.eidos/`, `shapes/` becomes `templates/`, the framework
|
|
4
|
+
// document takes the 5.0.0 keys, every blueprint's `flavor` becomes
|
|
5
|
+
// `variant`, and `eidos_version` is set. A root that kept the markdown
|
|
6
|
+
// Framework.md gets Framework.yaml in its place, the index inside it. A
|
|
7
|
+
// template that opens with frontmatter loses the block; one named off its
|
|
8
|
+
// unit is noted, since only the owner can say which name is right. From
|
|
9
|
+
// 5.0.0 nothing on disk moves: the version is bumped and the standard's core
|
|
10
|
+
// block is rewritten with its required rows, every region carried as found.
|
|
11
|
+
// 5.2.0 named the one personal file a tool keeps, `local.yaml`, and the one
|
|
12
|
+
// line in `.eidos/.gitignore` that covers every tool's: the hop adds that
|
|
13
|
+
// line, and this CLI's own `.gitignore` beside its local file, which did the
|
|
14
|
+
// same job before, goes. 5.3.0 declares every folder at the root with a
|
|
15
|
+
// type: `collections` becomes `folders` and every entry gains
|
|
16
|
+
// `type: collection`; anything at the root the document does not declare is
|
|
17
|
+
// the owner's to declare or move, so it is reported, never touched.
|
|
18
|
+
import { existsSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
10
19
|
import path from 'node:path';
|
|
11
20
|
import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
|
|
12
|
-
import { FRAMEWORK_DIR, FRAMEWORK_FILES, STANDARD_CORE, unitOf } from './framework-model.js';
|
|
21
|
+
import { FRAMEWORK_DIR, FRAMEWORK_FILES, isCollection, STANDARD_CORE, unitOf } from './framework-model.js';
|
|
13
22
|
import { convertToYaml } from './convert.js';
|
|
14
|
-
import { annotateYaml } from './framework-structured.js';
|
|
23
|
+
import { annotateYaml, coreEntries, YAML_GUIDANCE } from './framework-structured.js';
|
|
24
|
+
import { undeclaredAtRoot } from './check.js';
|
|
15
25
|
import { parseFramework } from './framework.js';
|
|
16
26
|
import { parseVersions } from './framework-markdown.js';
|
|
17
27
|
import { stripFrontmatter } from './template.js';
|
|
28
|
+
import { LOCAL_IGNORE_PATTERN, rootIgnoresLocal, settingsDir } from './settings.js';
|
|
18
29
|
import { readVersions, writeVersions, versionsFile } from './versions.js';
|
|
19
30
|
import { standardVersion } from '../paths.js';
|
|
20
31
|
export const LEGACY_DIR = '_eidos';
|
|
32
|
+
// The comment the seeds write above the pattern, so a migrated root reads as a fresh one does.
|
|
33
|
+
const LOCAL_IGNORE_NOTE = "# Eidos: a tool's local.yaml is personal and never shared.\n# A tool that keeps a folder under plugins/ may keep its personal settings there, one machine's (a\n# viewer command, an editor, a key path); everything else in its folder is the root's. This one\n# line covers every tool, so no tool writes a .gitignore of its own.\n";
|
|
34
|
+
// The guidance this CLI wrote above `collections` before 5.3.0. A document
|
|
35
|
+
// carrying it word for word gets the `folders` guidance in its place; any
|
|
36
|
+
// other comment there is the owner's or the seed's and stays.
|
|
37
|
+
const COLLECTIONS_GUIDANCE = " The collections. Each declares its variants (one default; each a template file named <unit>.<variant>.md under templates/, the unit being the word for one of its blueprints) and its grouping.\n Add a group under `grouping.groups` with a one-line description; add a variant with a template file in templates/.";
|
|
21
38
|
export class MigrationError extends Error {
|
|
22
39
|
constructor(message) {
|
|
23
40
|
super(message);
|
|
@@ -67,23 +84,49 @@ const renameKey = (pair, to) => {
|
|
|
67
84
|
};
|
|
68
85
|
// The YAML form, key by key, comments kept: `schema` → `properties`,
|
|
69
86
|
// `flavors` → `variants`, each variant's `shape` → `template` (with its path),
|
|
70
|
-
// `unit` gone,
|
|
87
|
+
// `unit` gone, `collections` → `folders` with `type: collection` on every
|
|
88
|
+
// entry, the core block rewritten, the version set. A document that still
|
|
89
|
+
// had the 4.x keys gets this CLI's guidance comments in their place; one
|
|
90
|
+
// already on 5.x keeps every comment it had, the seed's included.
|
|
71
91
|
export function migrateYamlDocument(text, version) {
|
|
72
92
|
const document = parseDocument(text);
|
|
73
93
|
const top = document.contents;
|
|
74
94
|
if (!isMap(top))
|
|
75
95
|
return text;
|
|
76
96
|
// the root's versions left the framework document in 5.0.0; migrateRoot moves them
|
|
97
|
+
const legacy = top.items.some((pair) => keyOf(pair) === 'schema' || keyOf(pair) === 'versions');
|
|
77
98
|
top.items = top.items.filter((pair) => keyOf(pair) !== 'versions');
|
|
78
99
|
for (const pair of top.items) {
|
|
79
100
|
const key = keyOf(pair);
|
|
80
101
|
if (key === 'schema')
|
|
81
102
|
renameKey(pair, 'properties');
|
|
82
|
-
|
|
103
|
+
if (key === 'collections') {
|
|
104
|
+
renameKey(pair, 'folders');
|
|
105
|
+
if (isScalar(pair.key) && pair.key.commentBefore === COLLECTIONS_GUIDANCE)
|
|
106
|
+
pair.key.commentBefore = YAML_GUIDANCE['folders'] ?? null;
|
|
107
|
+
if (isSeq(pair.value)) {
|
|
108
|
+
for (const folder of pair.value.items) {
|
|
109
|
+
if (!isMap(folder) || folder.has('type'))
|
|
110
|
+
continue;
|
|
111
|
+
const map = folder;
|
|
112
|
+
// the type sits beside the name, the way the seeds write it
|
|
113
|
+
const at = map.items.findIndex((entry) => keyOf(entry) === 'name');
|
|
114
|
+
map.items.splice(at === -1 ? 0 : at + 1, 0, document.createPair('type', 'collection'));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// the standard's own block is migrate's to rewrite: its rows are this version's core
|
|
83
119
|
if ((key === 'schema' || key === 'properties') && isMap(pair.value)) {
|
|
84
120
|
for (const entry of pair.value.items) {
|
|
85
121
|
if (keyOf(entry) === 'core' && isSeq(entry.value) && entry.value.items.length > 0) {
|
|
86
|
-
|
|
122
|
+
// one row per line, the flow style the seeds write the block in
|
|
123
|
+
const rows = document.createNode(coreEntries(STANDARD_CORE));
|
|
124
|
+
if (isSeq(rows)) {
|
|
125
|
+
for (const row of rows.items)
|
|
126
|
+
if (isMap(row))
|
|
127
|
+
row.flow = true;
|
|
128
|
+
}
|
|
129
|
+
entry.value = rows;
|
|
87
130
|
}
|
|
88
131
|
}
|
|
89
132
|
}
|
|
@@ -115,7 +158,8 @@ export function migrateYamlDocument(text, version) {
|
|
|
115
158
|
}
|
|
116
159
|
}
|
|
117
160
|
}
|
|
118
|
-
|
|
161
|
+
if (legacy)
|
|
162
|
+
annotateYaml(document);
|
|
119
163
|
return document.toString({ lineWidth: 0 });
|
|
120
164
|
}
|
|
121
165
|
// What a 4.x document recorded under `versions`, in either form, for the move
|
|
@@ -210,6 +254,19 @@ export function migrateRoot(root, options) {
|
|
|
210
254
|
if (held.includes(`${LEGACY_DIR}/`))
|
|
211
255
|
act(`point .gitignore at ${FRAMEWORK_DIR}/`, () => writeFileSync(ignore, held.replace(new RegExp(`${LEGACY_DIR}/`, 'g'), `${FRAMEWORK_DIR}/`), 'utf8'));
|
|
212
256
|
}
|
|
257
|
+
// 5.2.0: one line keeps every tool's personal file out of the repository,
|
|
258
|
+
// and this CLI's private .gitignore, which covered only its own, is retired.
|
|
259
|
+
const frameworkIgnore = path.join(live, '.gitignore');
|
|
260
|
+
if (!rootIgnoresLocal(root)) {
|
|
261
|
+
act(`add ${LOCAL_IGNORE_PATTERN} to ${FRAMEWORK_DIR}/.gitignore, the one line every tool's personal file needs`, () => {
|
|
262
|
+
const held = existsSync(frameworkIgnore) ? readFileSync(frameworkIgnore, 'utf8') : '# Eidos: me.md is personal and never shared.\nme.md\n';
|
|
263
|
+
writeFileSync(frameworkIgnore, `${held.replace(/\s*$/, '')}\n\n${LOCAL_IGNORE_NOTE}${LOCAL_IGNORE_PATTERN}\n`, 'utf8');
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const ownIgnore = path.join(settingsDir(root), '.gitignore');
|
|
267
|
+
if (existsSync(ownIgnore) && readFileSync(ownIgnore, 'utf8').trim() === 'local.yaml') {
|
|
268
|
+
act(`remove ${rel(root, ownIgnore)}; the root's .gitignore covers it now`, () => rmSync(ownIgnore, { force: true }));
|
|
269
|
+
}
|
|
213
270
|
const framework = parseFramework(next, found.format, root, path.join(dir, found.name));
|
|
214
271
|
// The markdown form left the standard in 5.0.0: the same framework is written
|
|
215
272
|
// as data, and the markdown document and each collection's index.md go.
|
|
@@ -218,6 +275,25 @@ export function migrateRoot(root, options) {
|
|
|
218
275
|
convertToYaml(framework, false);
|
|
219
276
|
});
|
|
220
277
|
}
|
|
278
|
+
// 5.3.0: everything at the root is declared, and only the owner can say
|
|
279
|
+
// what an undeclared entry is or whether a declared one with nothing behind
|
|
280
|
+
// it should stay; each is a note, never a move.
|
|
281
|
+
const undeclared = undeclaredAtRoot(framework);
|
|
282
|
+
// on a dry run the framework folder may still be the old one
|
|
283
|
+
for (const entry of undeclared.folders.filter((name) => name !== LEGACY_DIR))
|
|
284
|
+
notes.push(`${entry}/ is at the root and no folder declares it; declare it (eidos configure:folder add ${entry} --type assets|other, or configure:collection add) or move it`);
|
|
285
|
+
for (const entry of undeclared.files)
|
|
286
|
+
notes.push(`${entry} is at the root and top_level does not list it; declare it (eidos configure:doc add) or move it`);
|
|
287
|
+
for (const folder of framework.folders) {
|
|
288
|
+
if (!existsSync(path.join(root, folder.name)))
|
|
289
|
+
notes.push(`${folder.name}/ is declared as ${isCollection(folder) ? 'a collection' : `an ${folder.type} folder`} but is not there; create it or remove the entry`);
|
|
290
|
+
}
|
|
291
|
+
for (const doc of framework.topLevel) {
|
|
292
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(doc.path))
|
|
293
|
+
continue;
|
|
294
|
+
if (!existsSync(path.resolve(dir, doc.path.split('#')[0] ?? '')))
|
|
295
|
+
notes.push(`top_level lists ${doc.title} at ${doc.path}, which is not there; write it or remove the entry`);
|
|
296
|
+
}
|
|
221
297
|
for (const collection of framework.collections) {
|
|
222
298
|
for (const file of markdownFiles(path.join(root, collection.name))) {
|
|
223
299
|
const content = readFileSync(file, 'utf8');
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Regions: the span inside a markdown file that a tool owns, fenced by two
|
|
2
|
+
// HTML comments carrying its name (Eidos 5.1.0). `<!-- <tool>:<region> <args> -->`
|
|
3
|
+
// opens one and `<!-- /<tool>:<region> -->` closes it, each alone on its line;
|
|
4
|
+
// what sits between is the tool's, rewritten wholesale by it and read by
|
|
5
|
+
// nothing else. The standard reads none of the contents: a check faults only
|
|
6
|
+
// an opener with no closer, a heading or a link inside is not the person's,
|
|
7
|
+
// and a renderer that does not know the tool shows the contents as they are.
|
|
8
|
+
// A tool's identifier and a region's name: lowercase letters, digits, and hyphens.
|
|
9
|
+
const NAME = '[a-z0-9][a-z0-9-]*';
|
|
10
|
+
// Both closers HTML allows, `-->` and `--!>`, end a marker.
|
|
11
|
+
const OPENER = new RegExp(`^<!--\\s*(${NAME}):(${NAME})(?:\\s+(.*?))?\\s*--!?>$`);
|
|
12
|
+
const CLOSER = new RegExp(`^<!--\\s*/(${NAME}):(${NAME})\\s*--!?>$`);
|
|
13
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
14
|
+
function parseArgs(text) {
|
|
15
|
+
const args = {};
|
|
16
|
+
for (const token of (text ?? '').split(/\s+/)) {
|
|
17
|
+
const eq = token.indexOf('=');
|
|
18
|
+
if (eq > 0)
|
|
19
|
+
args[token.slice(0, eq)] = token.slice(eq + 1);
|
|
20
|
+
}
|
|
21
|
+
return args;
|
|
22
|
+
}
|
|
23
|
+
// Every region in the text, in order. A marker inside a fenced code block
|
|
24
|
+
// (three or more backticks or tildes, closed by the same) is text; a region
|
|
25
|
+
// closes at the first closer naming its tool and region, and the lines
|
|
26
|
+
// between are not read for markers, so another tool's marker there is content.
|
|
27
|
+
export function parseRegions(text) {
|
|
28
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
29
|
+
const out = [];
|
|
30
|
+
let fence = null;
|
|
31
|
+
let index = 0;
|
|
32
|
+
while (index < lines.length) {
|
|
33
|
+
const line = (lines[index] ?? '').trim();
|
|
34
|
+
if (fence) {
|
|
35
|
+
const closing = FENCE.exec(lines[index] ?? '');
|
|
36
|
+
if (closing && closing[1] && closing[1][0] === fence.char && closing[1].length >= fence.length && (lines[index] ?? '').trim() === closing[1])
|
|
37
|
+
fence = null;
|
|
38
|
+
index += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const opening = FENCE.exec(lines[index] ?? '');
|
|
42
|
+
if (opening && opening[1]) {
|
|
43
|
+
fence = { char: opening[1][0] ?? '`', length: opening[1].length };
|
|
44
|
+
index += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const opener = OPENER.exec(line);
|
|
48
|
+
if (!opener || !opener[1] || !opener[2]) {
|
|
49
|
+
index += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const region = { tool: opener[1], region: opener[2], args: parseArgs(opener[3]), open: index, close: null, contents: '' };
|
|
53
|
+
let end = index + 1;
|
|
54
|
+
while (end < lines.length) {
|
|
55
|
+
const closer = CLOSER.exec((lines[end] ?? '').trim());
|
|
56
|
+
if (closer && closer[1] === region.tool && closer[2] === region.region) {
|
|
57
|
+
region.close = end;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
end += 1;
|
|
61
|
+
}
|
|
62
|
+
region.contents = lines.slice(index + 1, region.close ?? lines.length).join('\n');
|
|
63
|
+
out.push(region);
|
|
64
|
+
index = (region.close ?? lines.length) + 1;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
// The zero-based lines every region occupies, markers included; an unclosed
|
|
69
|
+
// one runs to the end of the text.
|
|
70
|
+
export function regionLines(text) {
|
|
71
|
+
const out = new Set();
|
|
72
|
+
const total = text.replace(/\r\n?/g, '\n').split('\n').length;
|
|
73
|
+
for (const region of parseRegions(text)) {
|
|
74
|
+
for (let line = region.open; line <= (region.close ?? total - 1); line += 1)
|
|
75
|
+
out.add(line);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
// The text with every region's lines blanked, line count kept, so what the
|
|
80
|
+
// standard does read (headings, links, prose) is only the person's.
|
|
81
|
+
export function stripRegions(text) {
|
|
82
|
+
const inside = regionLines(text);
|
|
83
|
+
if (inside.size === 0)
|
|
84
|
+
return text;
|
|
85
|
+
return text
|
|
86
|
+
.replace(/\r\n?/g, '\n')
|
|
87
|
+
.split('\n')
|
|
88
|
+
.map((line, index) => (inside.has(index) ? '' : line))
|
|
89
|
+
.join('\n');
|
|
90
|
+
}
|
|
91
|
+
function escapeHtml(text) {
|
|
92
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
93
|
+
}
|
|
94
|
+
// The text with each region replaced by one HTML block that shows its
|
|
95
|
+
// contents as they are: a `<pre>` carrying the tool and region name, which a
|
|
96
|
+
// markdown renderer passes through whole (an HTML block that opens with
|
|
97
|
+
// `<pre` runs to `</pre>`, blank lines included), so nothing inside is
|
|
98
|
+
// rendered as markdown. A renderer that knows the tool draws its own view.
|
|
99
|
+
export function regionsAsHtml(text) {
|
|
100
|
+
const regions = parseRegions(text);
|
|
101
|
+
if (regions.length === 0)
|
|
102
|
+
return text;
|
|
103
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
104
|
+
const out = [];
|
|
105
|
+
let index = 0;
|
|
106
|
+
for (const region of regions) {
|
|
107
|
+
out.push(...lines.slice(index, region.open));
|
|
108
|
+
const args = Object.entries(region.args)
|
|
109
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
110
|
+
.join(' ');
|
|
111
|
+
const attributes = [`data-tool="${escapeHtml(region.tool)}"`, `data-region="${escapeHtml(region.region)}"`, args !== '' ? `data-args="${escapeHtml(args)}"` : '', region.close === null ? 'data-unclosed=""' : ''].filter((attribute) => attribute !== '');
|
|
112
|
+
out.push('', `<pre class="region" ${attributes.join(' ')}>${escapeHtml(region.contents)}</pre>`, '');
|
|
113
|
+
index = (region.close ?? lines.length) + 1;
|
|
114
|
+
}
|
|
115
|
+
out.push(...lines.slice(index));
|
|
116
|
+
return out.join('\n');
|
|
117
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
// Scaffolding one blueprint: frontmatter generated from the
|
|
2
|
-
// apply to its collection
|
|
3
|
-
//
|
|
1
|
+
// Scaffolding one blueprint: frontmatter generated from the required
|
|
2
|
+
// properties that apply to its collection plus any optional one given a
|
|
3
|
+
// value, body rendered from its variant's template, filename in the
|
|
4
|
+
// framework's convention with a permanent kebab-case id inside. Born
|
|
4
5
|
// conforming; the prose is still the owner's.
|
|
5
6
|
import { existsSync } from 'node:fs';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { blueprintId } from './blueprint.js';
|
|
9
|
+
import { optionProblem } from './check.js';
|
|
8
10
|
import { defaultVariant, findCollection, findVariant, propertiesFor } from './framework.js';
|
|
9
11
|
import { convert, kebab } from './naming.js';
|
|
10
12
|
import { loadTemplate } from './template.js';
|
|
@@ -15,7 +17,8 @@ export class ScaffoldError extends Error {
|
|
|
15
17
|
this.name = 'ScaffoldError';
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
|
-
|
|
20
|
+
// A value typed on a command line, as the property's declared type holds it.
|
|
21
|
+
export function coerce(type, raw) {
|
|
19
22
|
switch (type.toLowerCase()) {
|
|
20
23
|
case 'list':
|
|
21
24
|
return raw
|
|
@@ -84,20 +87,23 @@ export function scaffoldBlueprint(framework, existing, options) {
|
|
|
84
87
|
title,
|
|
85
88
|
summary: options.summary ?? '',
|
|
86
89
|
variant: variant.name,
|
|
87
|
-
status: 'Draft',
|
|
88
90
|
date_created: options.date,
|
|
89
91
|
date_modified: options.date,
|
|
90
92
|
};
|
|
91
93
|
if (group !== null && collection.grouping?.property) {
|
|
92
94
|
values[collection.grouping.property] = group;
|
|
93
95
|
}
|
|
94
|
-
const
|
|
96
|
+
const declared = new Map(propertiesFor(framework, collection.name).map((property) => [property.name, property]));
|
|
97
|
+
const types = new Map([...declared].map(([name, property]) => [name, property.type]));
|
|
95
98
|
for (const [key, raw] of Object.entries(options.set)) {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
99
|
+
const property = declared.get(key);
|
|
100
|
+
if (property === undefined) {
|
|
98
101
|
notes.push(`property '${key}' is not in the Properties table for ${collection.name}; written anyway`);
|
|
99
102
|
}
|
|
100
|
-
values[key] = coerce(type ?? 'Text', raw);
|
|
103
|
+
values[key] = coerce(property?.type ?? 'Text', raw);
|
|
104
|
+
const offList = property ? optionProblem(property, values[key]) : null;
|
|
105
|
+
if (offList)
|
|
106
|
+
notes.push(`'${key}: ${offList.off.join(', ')}' is not one of its options (${offList.options.join(', ')}); written anyway, and check reports it`);
|
|
101
107
|
}
|
|
102
108
|
let content = blankBlueprint(framework, collection.name, values, shape.text, title);
|
|
103
109
|
const extra = Object.keys(options.set).filter((key) => !types.has(key));
|
package/dist/src/core/seed.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Installing a seed: copy a starting framework into a new root's `.eidos/`,
|
|
2
|
-
// set the naming convention, the starting groups, and the
|
|
3
|
-
// its Framework.yaml (the seed's own comments kept), scaffold
|
|
4
|
-
// collection
|
|
2
|
+
// set the naming convention, the starting groups, and the folder names in
|
|
3
|
+
// its Framework.yaml (the seed's own comments kept), scaffold every declared
|
|
4
|
+
// folder (a collection's with its groups, an assets folder empty), and drop
|
|
5
|
+
// one blank blueprint per framing variant.
|
|
5
6
|
// It writes no prose: the README's one-liner, each group's description, and
|
|
6
7
|
// every scaffolded blueprint's summary and body stay the owner's.
|
|
7
8
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
@@ -12,7 +13,7 @@ import { buildIndexes } from './index-leaf.js';
|
|
|
12
13
|
import { listBlueprints } from './blueprint.js';
|
|
13
14
|
import { convert, kebab, titleize } from './naming.js';
|
|
14
15
|
import { renderTemplate } from './template.js';
|
|
15
|
-
import { TOOL_KEY } from './framework-model.js';
|
|
16
|
+
import { isCollection, TOOL_KEY } from './framework-model.js';
|
|
16
17
|
import { DEFAULT_CANVAS_HINTS } from './canvas.js';
|
|
17
18
|
import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
|
|
18
19
|
const GITIGNORE_SHIPPED = '_gitignore';
|
|
@@ -76,13 +77,16 @@ class Writer {
|
|
|
76
77
|
}
|
|
77
78
|
const mapsIn = (value) => (isSeq(value) ? value.items.filter((item) => isMap(item)) : []);
|
|
78
79
|
const nameOf = (map) => String(map.get('name') ?? '');
|
|
79
|
-
// The
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
// The `folders` list as the seed wrote it (`collections` in a seed older than 5.3.0).
|
|
81
|
+
const foldersOf = (document) => document.get('folders') ?? document.get('collections');
|
|
82
|
+
// The seed's folders and the properties scoped to its collections, renamed
|
|
83
|
+
// into the root's convention; every other key, and every comment, stays as
|
|
84
|
+
// the seed wrote it.
|
|
85
|
+
function renameFolders(document, renames) {
|
|
86
|
+
for (const folder of mapsIn(foldersOf(document))) {
|
|
87
|
+
const to = renames.get(nameOf(folder));
|
|
84
88
|
if (to !== undefined)
|
|
85
|
-
|
|
89
|
+
folder.set('name', to);
|
|
86
90
|
}
|
|
87
91
|
const properties = document.get('properties');
|
|
88
92
|
if (!isMap(properties))
|
|
@@ -123,16 +127,26 @@ function renameInText(text, from, to) {
|
|
|
123
127
|
const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
124
128
|
return text.replace(new RegExp(`(?<![\\w-])${escaped}(?!\\w)`, 'g'), to);
|
|
125
129
|
}
|
|
126
|
-
// A
|
|
127
|
-
|
|
130
|
+
// A value worth writing: anything but nothing, an empty string, or an empty list.
|
|
131
|
+
function hasValue(value) {
|
|
132
|
+
return value !== undefined && value !== null && value !== '' && !(Array.isArray(value) && value.length === 0);
|
|
133
|
+
}
|
|
134
|
+
// A blank blueprint: every required property that applies to the collection
|
|
135
|
+
// (with the value given, else blank, `[]` for a list), every optional one
|
|
136
|
+
// that was given a value, then the variant's template as the body. An
|
|
137
|
+
// optional property left out is not a gap, so the file carries nothing it
|
|
138
|
+
// doesn't need.
|
|
128
139
|
export function blankBlueprint(framework, collectionName, values, shapeTemplate, title) {
|
|
129
|
-
const entries =
|
|
140
|
+
const entries = [];
|
|
141
|
+
for (const property of propertiesFor(framework, collectionName)) {
|
|
130
142
|
const value = values[property.name];
|
|
131
|
-
if ((value
|
|
132
|
-
|
|
143
|
+
if (hasValue(value)) {
|
|
144
|
+
entries.push([property.name, value]);
|
|
145
|
+
}
|
|
146
|
+
else if (property.required) {
|
|
147
|
+
entries.push([property.name, property.type.toLowerCase() === 'list' ? [] : '']);
|
|
133
148
|
}
|
|
134
|
-
|
|
135
|
-
});
|
|
149
|
+
}
|
|
136
150
|
return formatFrontmatter(entries) + '\n' + renderTemplate(shapeTemplate, title);
|
|
137
151
|
}
|
|
138
152
|
export function installSeed(options) {
|
|
@@ -178,9 +192,9 @@ export function installSeed(options) {
|
|
|
178
192
|
// convention, edited in place so the seed's comments travel with it.
|
|
179
193
|
document.set('naming', naming);
|
|
180
194
|
for (const collection of leftOut) {
|
|
181
|
-
const
|
|
182
|
-
if (isSeq(
|
|
183
|
-
|
|
195
|
+
const folders = foldersOf(document);
|
|
196
|
+
if (isSeq(folders))
|
|
197
|
+
folders.items = folders.items.filter((item) => !(isMap(item) && nameOf(item) === collection.name));
|
|
184
198
|
for (const variant of collection.variants)
|
|
185
199
|
writer.remove(path.join(root, '.eidos', variant.template));
|
|
186
200
|
}
|
|
@@ -189,7 +203,7 @@ export function installSeed(options) {
|
|
|
189
203
|
if (!grouped || !grouped.grouping) {
|
|
190
204
|
throw new InstallError(`no collection in '${options.seed}' declares a grouping`);
|
|
191
205
|
}
|
|
192
|
-
const groupedNode = mapsIn(document
|
|
206
|
+
const groupedNode = mapsIn(foldersOf(document)).find((collection) => nameOf(collection) === grouped.name);
|
|
193
207
|
const grouping = groupedNode?.get('grouping');
|
|
194
208
|
if (!isMap(grouping)) {
|
|
195
209
|
throw new InstallError(`${frameworkSrc}: ${grouped.name} declares a grouping the document does not carry`);
|
|
@@ -226,6 +240,8 @@ export function installSeed(options) {
|
|
|
226
240
|
}
|
|
227
241
|
docs.push({ target, body });
|
|
228
242
|
}
|
|
243
|
+
// Every declared folder takes the naming convention, a collection and an
|
|
244
|
+
// assets folder alike; the files inside an assets folder keep their names.
|
|
229
245
|
const folders = new Map();
|
|
230
246
|
const renames = new Map();
|
|
231
247
|
if (readme !== null && leftOut.length > 0) {
|
|
@@ -234,21 +250,27 @@ export function installSeed(options) {
|
|
|
234
250
|
.filter((line) => !leftOut.some((collection) => line.includes(`[${collection.name}]`)))
|
|
235
251
|
.join('\n');
|
|
236
252
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
253
|
+
const plain = seedFramework.folders.filter((folder) => !isCollection(folder));
|
|
254
|
+
for (const declared of [...installing, ...plain]) {
|
|
255
|
+
// a seed writes a collection's name in Title Case and its assets folder in
|
|
256
|
+
// lowercase; the convention is applied to the title-cased word either way
|
|
257
|
+
const folder = convert(titleize(declared.name), naming);
|
|
258
|
+
folders.set(declared.name, folder);
|
|
259
|
+
if (folder !== declared.name) {
|
|
260
|
+
renames.set(declared.name, folder);
|
|
242
261
|
if (readme !== null) {
|
|
243
|
-
readme = renameInText(readme,
|
|
262
|
+
readme = renameInText(readme, declared.name, folder);
|
|
244
263
|
}
|
|
245
264
|
}
|
|
246
265
|
}
|
|
247
|
-
|
|
266
|
+
renameFolders(document, renames);
|
|
248
267
|
applyDefaultCanvasHints(document);
|
|
249
268
|
let serialized = document.toString({ lineWidth: 0 });
|
|
269
|
+
// comments name the collections too, so the word follows; an assets folder's
|
|
270
|
+
// name is also a type (`type: assets`), so only its entry is renamed
|
|
250
271
|
for (const [from, to] of renames) {
|
|
251
|
-
|
|
272
|
+
if (installing.some((collection) => collection.name === from))
|
|
273
|
+
serialized = renameInText(serialized, from, to);
|
|
252
274
|
}
|
|
253
275
|
const frameworkFile = path.join(root, '.eidos', 'Framework.yaml');
|
|
254
276
|
writer.write(frameworkFile, serialized);
|
|
@@ -259,9 +281,12 @@ export function installSeed(options) {
|
|
|
259
281
|
for (const doc of docs) {
|
|
260
282
|
writer.write(path.join(root, doc.target), doc.body);
|
|
261
283
|
}
|
|
262
|
-
// 3. a folder per
|
|
284
|
+
// 3. a folder per declared folder; blank blueprints for the framing collection.
|
|
263
285
|
const scaffolded = [];
|
|
264
286
|
const groupsInConvention = options.groups.map((group) => convert(group, naming));
|
|
287
|
+
for (const folder of plain) {
|
|
288
|
+
writer.mkdir(path.join(root, folders.get(folder.name) ?? folder.name));
|
|
289
|
+
}
|
|
265
290
|
for (const collection of installing) {
|
|
266
291
|
const folderName = folders.get(collection.name) ?? collection.name;
|
|
267
292
|
const folder = path.join(root, folderName);
|
|
@@ -285,7 +310,6 @@ export function installSeed(options) {
|
|
|
285
310
|
id: kebab(variant.name),
|
|
286
311
|
title,
|
|
287
312
|
variant: variant.name,
|
|
288
|
-
status: 'Draft',
|
|
289
313
|
date_created: options.date,
|
|
290
314
|
date_modified: options.date,
|
|
291
315
|
};
|
|
@@ -306,6 +330,7 @@ export function installSeed(options) {
|
|
|
306
330
|
naming,
|
|
307
331
|
frameworkFile,
|
|
308
332
|
collections: installing.map((collection) => folders.get(collection.name) ?? collection.name),
|
|
333
|
+
folders: plain.map((folder) => folders.get(folder.name) ?? folder.name),
|
|
309
334
|
groupedCollection: grouped ? (folders.get(grouped.name) ?? grouped.name) : null,
|
|
310
335
|
groups: groupsInConvention,
|
|
311
336
|
scaffolded,
|