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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// spellings (Schema, Flavors, shape) still read, so `migrate` can load a root
|
|
6
6
|
// it is about to move.
|
|
7
7
|
import { parseFrontmatter } from './frontmatter.js';
|
|
8
|
-
import { groupingProperty, STANDARD_CORE, unitOf } from './framework-model.js';
|
|
8
|
+
import { coreRequires, groupingProperty, STANDARD_CORE, unitOf } from './framework-model.js';
|
|
9
9
|
import { DEFAULT_NAMING, isNaming } from './naming.js';
|
|
10
10
|
import { section, sections, stripInlineCode } from './markdown.js';
|
|
11
11
|
import path from 'node:path';
|
|
@@ -64,6 +64,7 @@ function parseCollections(body) {
|
|
|
64
64
|
const out = [];
|
|
65
65
|
for (const { heading, body: text } of sections(block, 3)) {
|
|
66
66
|
const collection = {
|
|
67
|
+
type: 'collection',
|
|
67
68
|
name: heading.text,
|
|
68
69
|
description: firstParagraph(text),
|
|
69
70
|
framing: false,
|
|
@@ -175,9 +176,12 @@ function parseProperties(block, core, owner = core ? 'eidos' : 'custom') {
|
|
|
175
176
|
continue;
|
|
176
177
|
}
|
|
177
178
|
const type = (cells[1] ?? 'Text').trim();
|
|
179
|
+
// the 4.x table had no Required column: the core requires what the
|
|
180
|
+
// standard says it does, and every other property is optional
|
|
181
|
+
const required = core && coreRequires(name);
|
|
178
182
|
const property = core || cells.length < 4
|
|
179
|
-
? { name, type, appliesTo: 'all', meaning: (cells[Math.min(cells.length, standard) - 1] ?? '').trim(), core, owner }
|
|
180
|
-
: { name, type, appliesTo: parseAppliesTo(cells[2] ?? ''), meaning: (cells[3] ?? '').trim(), core, owner };
|
|
183
|
+
? { name, type, appliesTo: 'all', required, meaning: (cells[Math.min(cells.length, standard) - 1] ?? '').trim(), core, owner }
|
|
184
|
+
: { name, type, appliesTo: parseAppliesTo(cells[2] ?? ''), required, meaning: (cells[3] ?? '').trim(), core, owner };
|
|
181
185
|
const tools = {};
|
|
182
186
|
for (let index = standard; index < cells.length; index += 1) {
|
|
183
187
|
const tool = header[index];
|
|
@@ -304,6 +308,8 @@ export function parseFrameworkMarkdown(text, root, file) {
|
|
|
304
308
|
naming,
|
|
305
309
|
namingError,
|
|
306
310
|
topLevel: parseTopLevel(body),
|
|
311
|
+
// the markdown form predates folder types: every folder it declares is a collection
|
|
312
|
+
folders: collections,
|
|
307
313
|
collections,
|
|
308
314
|
schema: { core: core.length > 0 ? core : STANDARD_CORE, custom, tools },
|
|
309
315
|
vocabulary: parseVocabulary(body),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// The framework model: everything a framework document declares, whichever
|
|
2
2
|
// of `.eidos/Framework.md` or `.yaml` it is. The version and naming
|
|
3
|
-
// convention, the Top-Level docs, the
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// convention, the Top-Level docs, the Folders (every folder at the root with
|
|
4
|
+
// its type; a collection carries its variants and grouping), the Properties
|
|
5
|
+
// table (`schema` here, `properties` on disk), and the Vocabulary.
|
|
6
6
|
import { kebab } from './naming.js';
|
|
7
7
|
export const FRAMEWORK_DIR = '.eidos';
|
|
8
8
|
// This tool's name, as the standard has a tool sign what it keeps in a
|
|
@@ -19,6 +19,35 @@ export const FRAMEWORK_FILES = [
|
|
|
19
19
|
{ name: 'Framework.yml', format: 'yaml' },
|
|
20
20
|
];
|
|
21
21
|
export const PROPERTY_TYPES = ['Text', 'List', 'Number', 'Checkbox', 'Date', 'Date & time'];
|
|
22
|
+
export function readValueStyle(value) {
|
|
23
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
24
|
+
return null;
|
|
25
|
+
const given = value;
|
|
26
|
+
const text = (field) => (typeof field === 'string' || typeof field === 'number' ? String(field).trim() : '');
|
|
27
|
+
const style = {};
|
|
28
|
+
if (text(given['shape']) !== '')
|
|
29
|
+
style.shape = text(given['shape']);
|
|
30
|
+
if (text(given['color']) !== '')
|
|
31
|
+
style.color = text(given['color']);
|
|
32
|
+
if (given['fill'] === 'solid' || given['fill'] === 'tint' || given['fill'] === 'none')
|
|
33
|
+
style.fill = given['fill'];
|
|
34
|
+
if (text(given['fill_color']) !== '')
|
|
35
|
+
style.fill_color = text(given['fill_color']);
|
|
36
|
+
if (given['size'] === 'small' || given['size'] === 'medium' || given['size'] === 'large')
|
|
37
|
+
style.size = given['size'];
|
|
38
|
+
return Object.keys(style).length > 0 ? style : null;
|
|
39
|
+
}
|
|
40
|
+
// Every folder at the root is declared with one of the standard's three
|
|
41
|
+
// types (Eidos 5.3.0): a collection holds blueprints; an assets folder holds
|
|
42
|
+
// files that are not markdown; an other folder is whatever its description
|
|
43
|
+
// says. The standard reads nothing inside the last two.
|
|
44
|
+
export const FOLDER_TYPES = ['collection', 'assets', 'other'];
|
|
45
|
+
export function isFolderType(value) {
|
|
46
|
+
return typeof value === 'string' && FOLDER_TYPES.includes(value);
|
|
47
|
+
}
|
|
48
|
+
export function isCollection(folder) {
|
|
49
|
+
return folder.type === 'collection';
|
|
50
|
+
}
|
|
22
51
|
// The unit a collection speaks in, the word for one of its blueprints: what
|
|
23
52
|
// its template files are named for (`<unit>.<variant>.md`), read off the
|
|
24
53
|
// default variant. A collection whose unit is `frame` is the framing one, this
|
|
@@ -33,13 +62,19 @@ export class FrameworkError extends Error {
|
|
|
33
62
|
this.name = 'FrameworkError';
|
|
34
63
|
}
|
|
35
64
|
}
|
|
36
|
-
// The standard's own four (Eidos 5.
|
|
65
|
+
// The standard's own four (Eidos 5.3.0), two of them required, used when a
|
|
66
|
+
// framework document carries no core block.
|
|
37
67
|
export const STANDARD_CORE = [
|
|
38
|
-
{ name: 'id', type: 'Text', appliesTo: 'all', meaning: 'Stable, unique identity, in any form
|
|
39
|
-
{ name: 'title', type: 'Text', appliesTo: 'all', meaning: 'Human-readable name.', core: true, owner: 'eidos' },
|
|
40
|
-
{ name: 'summary', type: 'Text', appliesTo: 'all', meaning: 'One
|
|
41
|
-
{ name: 'variant', type: 'Text', appliesTo: 'all', meaning:
|
|
68
|
+
{ name: 'id', type: 'Text', appliesTo: 'all', required: true, meaning: 'Stable, unique identity, in any form. Assigned once, never changed.', core: true, owner: 'eidos' },
|
|
69
|
+
{ name: 'title', type: 'Text', appliesTo: 'all', required: true, meaning: 'Human-readable name.', core: true, owner: 'eidos' },
|
|
70
|
+
{ name: 'summary', type: 'Text', appliesTo: 'all', required: false, meaning: 'One line: what this blueprint is. Feeds the index; absent, the index flags it.', core: true, owner: 'eidos' },
|
|
71
|
+
{ name: 'variant', type: 'Text', appliesTo: 'all', required: false, meaning: "Which variant this blueprint follows. Absent = the collection's default.", core: true, owner: 'eidos' },
|
|
42
72
|
];
|
|
73
|
+
// Whether the standard's core requires a property of this name: what an
|
|
74
|
+
// absent `required` on a core row means, since the block is the standard's.
|
|
75
|
+
export function coreRequires(name) {
|
|
76
|
+
return STANDARD_CORE.some((property) => property.name === name && property.required);
|
|
77
|
+
}
|
|
43
78
|
// Every property of the Properties table, whichever block owns it, core first.
|
|
44
79
|
export function allProperties(schema) {
|
|
45
80
|
return [...schema.core, ...schema.custom, ...Object.values(schema.tools).flat()];
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// The YAML framework document, `.eidos/Framework.yaml`: the same model as the
|
|
2
2
|
// markdown form, as fields in the frontmatter's snake_case, for a root that
|
|
3
3
|
// scripts and agents read.
|
|
4
|
-
// Paths are relative to `.eidos/`, as the markdown links are.
|
|
4
|
+
// Paths are relative to `.eidos/`, as the markdown links are. Every folder
|
|
5
|
+
// at the root is declared under `folders` with its type (a 5.2.x root's
|
|
6
|
+
// `collections` still reads, every entry a collection). A structured
|
|
5
7
|
// document also carries the generated index under `index`, one list per
|
|
6
8
|
// collection, which `eidos index` rewrites in place without disturbing
|
|
7
9
|
// anything a person wrote around it.
|
|
8
10
|
import { Document, isMap, isPair, isScalar, parse as parseYaml, parseDocument } from 'yaml';
|
|
9
|
-
import { groupingProperty, STANDARD_CORE, TOOL_KEY, unitOf } from './framework-model.js';
|
|
11
|
+
import { readValueStyle, coreRequires, groupingProperty, isCollection, isFolderType, STANDARD_CORE, TOOL_KEY, unitOf } from './framework-model.js';
|
|
10
12
|
import { DEFAULT_NAMING, isNaming } from './naming.js';
|
|
11
|
-
const STANDARD_PROPERTY_KEYS = new Set(['name', 'type', 'applies_to', 'meaning']);
|
|
13
|
+
const STANDARD_PROPERTY_KEYS = new Set(['name', 'type', 'applies_to', 'required', 'options', 'meaning']);
|
|
12
14
|
function isPlain(value) {
|
|
13
15
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
16
|
}
|
|
@@ -89,13 +91,32 @@ function readVocabulary(value, problems) {
|
|
|
89
91
|
});
|
|
90
92
|
return out;
|
|
91
93
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
// One entry of `folders` (or of a 5.2.x root's `collections`, every one a
|
|
95
|
+
// collection). A type outside the standard's three is a fault; the folder is
|
|
96
|
+
// still read as declared (as `other`) so the root is not also reported as
|
|
97
|
+
// undeclared.
|
|
98
|
+
function readFolder(value, key, index, problems) {
|
|
99
|
+
const where = `${key}[${index}]`;
|
|
94
100
|
if (!isPlain(value) || text(value['name']) === '') {
|
|
95
|
-
problems.push(`${where}: a
|
|
101
|
+
problems.push(`${where}: a folder needs a name`);
|
|
96
102
|
return null;
|
|
97
103
|
}
|
|
98
104
|
const name = text(value['name']);
|
|
105
|
+
const declared = value['type'];
|
|
106
|
+
let type = 'collection';
|
|
107
|
+
if (isFolderType(declared)) {
|
|
108
|
+
type = declared;
|
|
109
|
+
}
|
|
110
|
+
else if (key === 'folders') {
|
|
111
|
+
problems.push(`${where} (${name}): type must be one of collection, assets, other${declared === undefined || declared === null ? '' : `, not ${text(declared)}`}`);
|
|
112
|
+
type = 'other';
|
|
113
|
+
}
|
|
114
|
+
if (type !== 'collection') {
|
|
115
|
+
return { type, name, description: text(value['description']) };
|
|
116
|
+
}
|
|
117
|
+
return readCollection(value, where, name, problems);
|
|
118
|
+
}
|
|
119
|
+
function readCollection(value, where, name, problems) {
|
|
99
120
|
const variants = list(value['variants'] ?? value['flavors'])
|
|
100
121
|
.map((variant, position) => readVariant(variant, `${where} (${name}) variants[${position}]`, problems))
|
|
101
122
|
.filter((variant) => variant !== null);
|
|
@@ -106,6 +127,7 @@ function readCollection(value, index, problems) {
|
|
|
106
127
|
}
|
|
107
128
|
}
|
|
108
129
|
const collection = {
|
|
130
|
+
type: 'collection',
|
|
109
131
|
name,
|
|
110
132
|
description: text(value['description']),
|
|
111
133
|
framing: false,
|
|
@@ -154,7 +176,34 @@ function readProperty(value, where, core, problems, owner = core ? 'eidos' : 'cu
|
|
|
154
176
|
problems.push(`${where}: applies_to must be all or a list of collections`);
|
|
155
177
|
}
|
|
156
178
|
}
|
|
157
|
-
|
|
179
|
+
// `required` is true or false, absent meaning false; on the standard's own
|
|
180
|
+
// block an absent key means what the core for this version says, so a root
|
|
181
|
+
// written before the key existed still requires its id and title.
|
|
182
|
+
const declaredRequired = value['required'];
|
|
183
|
+
let required = declaredRequired === true;
|
|
184
|
+
if (declaredRequired === undefined || declaredRequired === null) {
|
|
185
|
+
required = core && coreRequires(text(value['name']));
|
|
186
|
+
}
|
|
187
|
+
else if (typeof declaredRequired !== 'boolean') {
|
|
188
|
+
problems.push(`${where}: required must be true or false`);
|
|
189
|
+
}
|
|
190
|
+
const property = { name: text(value['name']), type: text(value['type']) || 'Text', appliesTo: applies, required, meaning: text(value['meaning']), core, owner };
|
|
191
|
+
// `options` closes the value to a declared set: a non-empty list on a Text
|
|
192
|
+
// or List property. An empty list is a fault, since the absent key already
|
|
193
|
+
// says the set is open; on any other type the list means nothing.
|
|
194
|
+
const declaredOptions = value['options'];
|
|
195
|
+
if (declaredOptions !== undefined && declaredOptions !== null) {
|
|
196
|
+
const options = Array.isArray(declaredOptions) ? declaredOptions.map(text).filter((option) => option !== '') : [];
|
|
197
|
+
if (!Array.isArray(declaredOptions) || options.length === 0) {
|
|
198
|
+
problems.push(`${where}: options must be a non-empty list of values`);
|
|
199
|
+
}
|
|
200
|
+
else if (!['text', 'list'].includes(property.type.toLowerCase())) {
|
|
201
|
+
problems.push(`${where}: options apply to a Text or List property, not ${property.type}`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
property.options = options;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
158
207
|
const tools = {};
|
|
159
208
|
for (const [key, field] of Object.entries(value)) {
|
|
160
209
|
if (!STANDARD_PROPERTY_KEYS.has(key) && key !== 'canvas' && field !== undefined && field !== null) {
|
|
@@ -185,6 +234,8 @@ function readStringMap(value) {
|
|
|
185
234
|
}
|
|
186
235
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
187
236
|
}
|
|
237
|
+
// `styles` maps a value to a whole node style; the first canvases wrote a
|
|
238
|
+
// `shape` map and a `color` map instead, which still read, folded into it.
|
|
188
239
|
export function readCanvasHint(value) {
|
|
189
240
|
if (!isPlain(value)) {
|
|
190
241
|
return null;
|
|
@@ -193,13 +244,24 @@ export function readCanvasHint(value) {
|
|
|
193
244
|
if (value['show'] === true) {
|
|
194
245
|
hint.show = true;
|
|
195
246
|
}
|
|
247
|
+
const styles = {};
|
|
248
|
+
if (isPlain(value['styles'])) {
|
|
249
|
+
for (const [key, item] of Object.entries(value['styles'])) {
|
|
250
|
+
const style = readValueStyle(item);
|
|
251
|
+
if (style)
|
|
252
|
+
styles[key] = style;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
196
255
|
const shape = readStringMap(value['shape']);
|
|
197
|
-
|
|
198
|
-
|
|
256
|
+
for (const [key, item] of Object.entries(shape ?? {})) {
|
|
257
|
+
styles[key] = { ...(styles[key] ?? {}), shape: styles[key]?.shape ?? item };
|
|
199
258
|
}
|
|
200
259
|
const color = readStringMap(value['color']);
|
|
201
|
-
|
|
202
|
-
|
|
260
|
+
for (const [key, item] of Object.entries(color ?? {})) {
|
|
261
|
+
styles[key] = { ...(styles[key] ?? {}), color: styles[key]?.color ?? item };
|
|
262
|
+
}
|
|
263
|
+
if (Object.keys(styles).length > 0) {
|
|
264
|
+
hint.styles = styles;
|
|
203
265
|
}
|
|
204
266
|
return Object.keys(hint).length > 0 ? hint : null;
|
|
205
267
|
}
|
|
@@ -247,12 +309,16 @@ export function parseFrameworkStructured(source, root, file) {
|
|
|
247
309
|
namingError = `naming: ${text(declaredNaming)} is not one of kebab-case, TitleCase, Title Case`;
|
|
248
310
|
}
|
|
249
311
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
312
|
+
// `folders` since 5.3.0; a root not yet migrated still declares `collections`,
|
|
313
|
+
// every one a collection, and reads so that `migrate` and `check` can see it.
|
|
314
|
+
const foldersKey = document['folders'] === undefined && document['collections'] !== undefined ? 'collections' : 'folders';
|
|
315
|
+
const folders = list(document[foldersKey])
|
|
316
|
+
.map((folder, index) => readFolder(folder, foldersKey, index, problems))
|
|
317
|
+
.filter((folder) => folder !== null);
|
|
318
|
+
if (!Array.isArray(document[foldersKey]) && !error && isPlain(value)) {
|
|
319
|
+
problems.push(`${foldersKey} must be a list`);
|
|
255
320
|
}
|
|
321
|
+
const collections = folders.filter(isCollection);
|
|
256
322
|
const schema = isPlain(document['properties']) ? document['properties'] : isPlain(document['schema']) ? document['schema'] : {};
|
|
257
323
|
const core = list(schema['core'])
|
|
258
324
|
.map((property, index) => readProperty(property, `schema.core[${index}]`, true, problems))
|
|
@@ -285,16 +351,22 @@ export function parseFrameworkStructured(source, root, file) {
|
|
|
285
351
|
naming,
|
|
286
352
|
namingError,
|
|
287
353
|
topLevel,
|
|
354
|
+
folders,
|
|
288
355
|
collections,
|
|
289
356
|
schema: { core: core.length > 0 ? core : STANDARD_CORE, custom, tools },
|
|
290
357
|
vocabulary: readVocabulary(document['vocabulary'], problems),
|
|
291
358
|
problems,
|
|
292
359
|
};
|
|
293
360
|
}
|
|
361
|
+
// The standard's block as a document writes it: `required` only where it is
|
|
362
|
+
// true, since absent means false. `migrate` writes the same rows.
|
|
363
|
+
export function coreEntries(core) {
|
|
364
|
+
return core.map(({ name, type, required, options, meaning, tools }) => ({ name, type, ...(required ? { required: true } : {}), ...(options ? { options } : {}), meaning, ...(tools ?? {}) }));
|
|
365
|
+
}
|
|
294
366
|
// The document form of a framework: what `eidos framework --json` prints and
|
|
295
367
|
// what a YAML file holds, without the generated index.
|
|
296
368
|
// Keys land in the order a reader expects: version and naming, then the
|
|
297
|
-
// top-level docs, the
|
|
369
|
+
// top-level docs, the folders, and the schema; `index` is added last by
|
|
298
370
|
// `eidos index`.
|
|
299
371
|
export function frameworkToDocument(framework) {
|
|
300
372
|
const document = {};
|
|
@@ -311,11 +383,15 @@ export function frameworkToDocument(framework) {
|
|
|
311
383
|
return entry;
|
|
312
384
|
});
|
|
313
385
|
}
|
|
314
|
-
document.
|
|
315
|
-
const entry = { name:
|
|
316
|
-
if (
|
|
317
|
-
entry.description =
|
|
386
|
+
document.folders = framework.folders.map((folder) => {
|
|
387
|
+
const entry = { name: folder.name, type: folder.type };
|
|
388
|
+
if (folder.description) {
|
|
389
|
+
entry.description = folder.description;
|
|
390
|
+
}
|
|
391
|
+
if (!isCollection(folder)) {
|
|
392
|
+
return entry;
|
|
318
393
|
}
|
|
394
|
+
const collection = folder;
|
|
319
395
|
if (collection.canvas) {
|
|
320
396
|
if (collection.canvas.mode === 'file') {
|
|
321
397
|
entry.canvas = 'file';
|
|
@@ -354,16 +430,16 @@ export function frameworkToDocument(framework) {
|
|
|
354
430
|
}
|
|
355
431
|
return entry;
|
|
356
432
|
});
|
|
357
|
-
const entry = ({ name, type, appliesTo: applies, meaning, canvas, tools }) => {
|
|
433
|
+
const entry = ({ name, type, appliesTo: applies, required, options, meaning, canvas, tools }) => {
|
|
358
434
|
const fields = { ...(tools ?? {}) };
|
|
359
435
|
if (canvas) {
|
|
360
436
|
const own = isPlain(fields[TOOL_KEY]) ? fields[TOOL_KEY] : {};
|
|
361
437
|
fields[TOOL_KEY] = { ...own, canvas };
|
|
362
438
|
}
|
|
363
|
-
return { name, type, applies_to: applies, meaning, ...fields };
|
|
439
|
+
return { name, type, applies_to: applies, ...(required ? { required: true } : {}), ...(options ? { options } : {}), meaning, ...fields };
|
|
364
440
|
};
|
|
365
441
|
document.properties = {
|
|
366
|
-
core: framework.schema.core
|
|
442
|
+
core: coreEntries(framework.schema.core),
|
|
367
443
|
custom: framework.schema.custom.map(entry),
|
|
368
444
|
};
|
|
369
445
|
if (Object.keys(framework.schema.tools).length > 0) {
|
|
@@ -379,12 +455,12 @@ export function frameworkToDocument(framework) {
|
|
|
379
455
|
}
|
|
380
456
|
return document;
|
|
381
457
|
}
|
|
382
|
-
const YAML_GUIDANCE = {
|
|
458
|
+
export const YAML_GUIDANCE = {
|
|
383
459
|
eidos_version: ' The Eidos version this framework targets; migrate reads and bumps it.',
|
|
384
460
|
naming: ' How files, folders, and links are named: kebab-case | TitleCase | Title Case. Settled once.',
|
|
385
|
-
top_level: '
|
|
386
|
-
|
|
387
|
-
properties: " The Properties table, one block per owner. `core` is the standard's and moves with eidos_version; `custom` is yours: name, type (Text | List | Number | Checkbox | Date | Date & time), applies_to (all or a list), meaning; `tools.<tool>` is a block a tool declared and alone writes.\n A key past those
|
|
461
|
+
top_level: ' Every file at the root, one entry each, in the order they are read (the browser lands on the first): a README, a Roadmap, a Vision.',
|
|
462
|
+
folders: " Every folder at the root, one entry each, with its type: collection | assets | other. A collection 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; an assets or other folder is a name, a type, and a description, and nothing inside it is read.\n Add a group under `grouping.groups` with a one-line description; add a variant with a template file in templates/.",
|
|
463
|
+
properties: " The Properties table, one block per owner. `core` is the standard's and moves with eidos_version; `custom` is yours: name, type (Text | List | Number | Checkbox | Date | Date & time), applies_to (all or a list), required (true puts it on every new blueprint and notes one that lacks it; absent = false, written when it has a value), options (the closed set a Text value is one of, or a List's elements are, in order; absent = any value), meaning; `tools.<tool>` is a block a tool declared and alone writes.\n A key past those six on a row is a tool's, named for the tool; `eidosmd.canvas` is how this CLI styles a canvas node by the property's value.",
|
|
388
464
|
vocabulary: " The root's own terms, one entry each: term, means (one line), not (the near-misses, each with why it is a different thing), and see (the blueprint that defines it in full) when one does. Absent = none declared.",
|
|
389
465
|
versions: " Snapshots of the root, taken on purpose, newest first: version (the root's own number), commit (the sha that is the snapshot), tag (blueprints/<version>, when made). Absent = none recorded, the normal state.",
|
|
390
466
|
index: ' Generated by `eidos index`; never hand-edited. One list per collection, in the order the markdown index would use.',
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// every seed and scaffold already uses, so a generated file diffs cleanly
|
|
4
4
|
// against a hand-written one.
|
|
5
5
|
import { parse as parseYaml } from 'yaml';
|
|
6
|
-
import { splitFrontmatter } from './markdown.js';
|
|
6
|
+
import { normalizeNewlines, splitFrontmatter } from './markdown.js';
|
|
7
7
|
// One `key: value` per line, read without YAML: what an unquoted summary with
|
|
8
8
|
// a colon in it still means to a person, and what Obsidian will refuse to show.
|
|
9
9
|
function parseLeniently(frontmatter) {
|
|
@@ -114,3 +114,63 @@ export function formatFrontmatter(entries) {
|
|
|
114
114
|
lines.push('---');
|
|
115
115
|
return lines.join('\n') + '\n';
|
|
116
116
|
}
|
|
117
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
118
|
+
// The lines a top-level key occupies inside [from, to): its own line and
|
|
119
|
+
// every line beneath it that is indented or a `- ` item, so a block list
|
|
120
|
+
// or a folded scalar moves as one.
|
|
121
|
+
export function keySpan(lines, from, to, key) {
|
|
122
|
+
const head = new RegExp(`^${escapeRegExp(key)}\\s*:`);
|
|
123
|
+
for (let index = from; index < to; index += 1) {
|
|
124
|
+
if (!head.test(lines[index] ?? ''))
|
|
125
|
+
continue;
|
|
126
|
+
let end = index + 1;
|
|
127
|
+
while (end < to && /^(\s+\S|-\s)/.test(lines[end] ?? ''))
|
|
128
|
+
end += 1;
|
|
129
|
+
return { start: index, end };
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
// Whether a file's frontmatter carries the key at the top level.
|
|
134
|
+
export function hasKey(text, key) {
|
|
135
|
+
const lines = normalizeNewlines(text).split('\n');
|
|
136
|
+
const close = closingFence(lines);
|
|
137
|
+
return close !== null && keySpan(lines, 1, close, key) !== null;
|
|
138
|
+
}
|
|
139
|
+
function closingFence(lines) {
|
|
140
|
+
if (lines[0]?.trim() !== '---')
|
|
141
|
+
return null;
|
|
142
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
143
|
+
if (lines[index]?.trim() === '---')
|
|
144
|
+
return index;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
// The file with one key edited in its frontmatter. A file with no block, or
|
|
149
|
+
// a rename or delete of a key it lacks, comes back unchanged; a set of a key
|
|
150
|
+
// it lacks appends the key at the end of the block.
|
|
151
|
+
export function editFrontmatterKey(text, edit) {
|
|
152
|
+
const lines = normalizeNewlines(text).split('\n');
|
|
153
|
+
const close = closingFence(lines);
|
|
154
|
+
if (close === null)
|
|
155
|
+
return text;
|
|
156
|
+
const span = keySpan(lines, 1, close, edit.key);
|
|
157
|
+
if (edit.kind === 'rename') {
|
|
158
|
+
if (!span)
|
|
159
|
+
return text;
|
|
160
|
+
const line = lines[span.start] ?? '';
|
|
161
|
+
lines[span.start] = line.replace(new RegExp(`^${escapeRegExp(edit.key)}(\\s*:)`), `${edit.to}$1`);
|
|
162
|
+
return lines.join('\n');
|
|
163
|
+
}
|
|
164
|
+
if (edit.kind === 'delete') {
|
|
165
|
+
if (!span)
|
|
166
|
+
return text;
|
|
167
|
+
lines.splice(span.start, span.end - span.start);
|
|
168
|
+
return lines.join('\n');
|
|
169
|
+
}
|
|
170
|
+
const rendered = formatFrontmatter([[edit.key, edit.value]]).split('\n').slice(1, -2);
|
|
171
|
+
if (span)
|
|
172
|
+
lines.splice(span.start, span.end - span.start, ...rendered);
|
|
173
|
+
else
|
|
174
|
+
lines.splice(close, 0, ...rendered);
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
package/dist/src/core/git.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// The little git the CLI needs: the commit a root sits at, whether a commit
|
|
2
|
-
// exists,
|
|
3
|
-
//
|
|
4
|
-
// or the root is not in a
|
|
2
|
+
// exists, a tag on one, and a move that keeps history. Every call is
|
|
3
|
+
// read-only except `gitTag` and `moveFile`, and every one answers null or
|
|
4
|
+
// false rather than throwing where git is absent or the root is not in a
|
|
5
|
+
// repository.
|
|
5
6
|
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { renameSync } from 'node:fs';
|
|
6
8
|
import path from 'node:path';
|
|
9
|
+
import { readSettings } from './settings.js';
|
|
7
10
|
function git(root, args) {
|
|
8
11
|
try {
|
|
9
12
|
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
|
|
@@ -12,6 +15,28 @@ function git(root, args) {
|
|
|
12
15
|
return null;
|
|
13
16
|
}
|
|
14
17
|
}
|
|
18
|
+
// A command run for its effect: true when git exits clean, whatever it prints.
|
|
19
|
+
function gitRun(root, args) {
|
|
20
|
+
try {
|
|
21
|
+
execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'ignore', 'ignore'] });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Whether git tracks this path (a file, or a folder with tracked files in it).
|
|
29
|
+
export function gitTracks(root, absolute) {
|
|
30
|
+
return git(root, ['ls-files', '--error-unmatch', '--', absolute]) !== null;
|
|
31
|
+
}
|
|
32
|
+
// A file or folder moved the way the root wants: with git on for the root
|
|
33
|
+
// and the path tracked, `git mv`, so history follows the rename; otherwise,
|
|
34
|
+
// or when git refuses, a plain rename.
|
|
35
|
+
export function moveFile(root, from, to) {
|
|
36
|
+
if (readSettings(root).shared.features.git && gitTracks(root, from) && gitRun(root, ['mv', '--', from, to]))
|
|
37
|
+
return;
|
|
38
|
+
renameSync(from, to);
|
|
39
|
+
}
|
|
15
40
|
export function gitHead(root) {
|
|
16
41
|
return git(root, ['rev-parse', 'HEAD']);
|
|
17
42
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Rewriting the markdown links a move breaks. When a folder or a file moves,
|
|
2
|
+
// every `[text](path)` anywhere in the root whose target lands inside the
|
|
3
|
+
// moved path is rewritten to point at the new place, relative to where the
|
|
4
|
+
// file holding it ends up. It is a path substitution and nothing else: a
|
|
5
|
+
// link's text is untouched, a link in a fenced code block, in inline code,
|
|
6
|
+
// or inside a tool's region is left as it is, and a filename typed in prose
|
|
7
|
+
// without link syntax is not a link.
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { regionLines } from './regions.js';
|
|
10
|
+
export function movePath(absolute, moves) {
|
|
11
|
+
for (const move of moves) {
|
|
12
|
+
if (absolute === move.from)
|
|
13
|
+
return move.to;
|
|
14
|
+
if (absolute.startsWith(move.from + path.sep))
|
|
15
|
+
return path.join(move.to, path.relative(move.from, absolute));
|
|
16
|
+
}
|
|
17
|
+
return absolute;
|
|
18
|
+
}
|
|
19
|
+
const LINK = /\[([^\]]*)\]\(\s*(<[^>]*>|[^)\s]+)((?:\s+"[^"]*")?)\s*\)/g;
|
|
20
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
21
|
+
function isFileTarget(target) {
|
|
22
|
+
return target !== '' && !/^[a-z][a-z0-9+.-]*:/i.test(target) && !target.startsWith('#') && !target.startsWith('//');
|
|
23
|
+
}
|
|
24
|
+
// The spans of inline code on a line, so a link quoted in code is left alone.
|
|
25
|
+
function codeSpans(line) {
|
|
26
|
+
const out = [];
|
|
27
|
+
const pattern = /`+[^`]*`+/g;
|
|
28
|
+
let match = pattern.exec(line);
|
|
29
|
+
while (match !== null) {
|
|
30
|
+
out.push([match.index, match.index + match[0].length]);
|
|
31
|
+
match = pattern.exec(line);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
// The text of a file with every link a move breaks rewritten. `before` is
|
|
36
|
+
// where the file is now and `after` where it will be once the moves are
|
|
37
|
+
// made, so a file inside a moved folder gets its outward links right too.
|
|
38
|
+
export function rewriteLinks(text, before, after, moves) {
|
|
39
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
40
|
+
const owned = regionLines(text);
|
|
41
|
+
let count = 0;
|
|
42
|
+
let fence = null;
|
|
43
|
+
const out = lines.map((line, index) => {
|
|
44
|
+
if (owned.has(index))
|
|
45
|
+
return line;
|
|
46
|
+
const fenced = FENCE.exec(line);
|
|
47
|
+
if (fence) {
|
|
48
|
+
if (fenced && fenced[1] && fenced[1][0] === fence.char && fenced[1].length >= fence.length && line.trim() === fenced[1])
|
|
49
|
+
fence = null;
|
|
50
|
+
return line;
|
|
51
|
+
}
|
|
52
|
+
if (fenced && fenced[1]) {
|
|
53
|
+
fence = { char: fenced[1][0] ?? '`', length: fenced[1].length };
|
|
54
|
+
return line;
|
|
55
|
+
}
|
|
56
|
+
const code = codeSpans(line);
|
|
57
|
+
return line.replace(LINK, (whole, label, rawTarget, title, offset) => {
|
|
58
|
+
if (code.some(([start, end]) => offset >= start && offset < end))
|
|
59
|
+
return whole;
|
|
60
|
+
const wrapped = rawTarget.startsWith('<') && rawTarget.endsWith('>');
|
|
61
|
+
const target = wrapped ? rawTarget.slice(1, -1) : rawTarget;
|
|
62
|
+
const hash = target.indexOf('#');
|
|
63
|
+
const file = hash === -1 ? target : target.slice(0, hash);
|
|
64
|
+
const anchor = hash === -1 ? '' : target.slice(hash);
|
|
65
|
+
if (!isFileTarget(file))
|
|
66
|
+
return whole;
|
|
67
|
+
let decoded = file;
|
|
68
|
+
try {
|
|
69
|
+
decoded = decodeURIComponent(file);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return whole;
|
|
73
|
+
}
|
|
74
|
+
const absolute = path.resolve(path.dirname(before), decoded);
|
|
75
|
+
const moved = movePath(absolute, moves);
|
|
76
|
+
let relative = path.relative(path.dirname(after), moved).split(path.sep).join('/');
|
|
77
|
+
if (relative === decoded.replace(/\\/g, '/'))
|
|
78
|
+
return whole;
|
|
79
|
+
if (relative === '')
|
|
80
|
+
relative = path.basename(moved);
|
|
81
|
+
count += 1;
|
|
82
|
+
const encoded = wrapped ? `<${relative}>` : /%20/.test(file) ? relative.replace(/ /g, '%20') : /\s/.test(relative) ? `<${relative}>` : relative;
|
|
83
|
+
return `[${label}](${encoded}${anchor}${title})`;
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
return { text: out.join('\n'), count };
|
|
87
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Small markdown readers shared by the framework, shape, and blueprint parsers:
|
|
2
2
|
// splitting frontmatter from body, and finding headings and sections outside
|
|
3
|
-
// code fences and
|
|
3
|
+
// code fences, HTML comments, and the regions a tool owns.
|
|
4
|
+
import { regionLines } from './regions.js';
|
|
4
5
|
export function normalizeNewlines(text) {
|
|
5
6
|
return text.replace(/\r\n?/g, '\n');
|
|
6
7
|
}
|
|
@@ -21,37 +22,43 @@ export function splitFrontmatter(text) {
|
|
|
21
22
|
}
|
|
22
23
|
return { frontmatter: null, body: lines.join('\n') };
|
|
23
24
|
}
|
|
24
|
-
// Every line with a flag saying whether it sits inside a fenced code block
|
|
25
|
-
//
|
|
25
|
+
// Every line with a flag saying whether it sits inside a fenced code block, an
|
|
26
|
+
// HTML comment, or a tool's region, so a `## heading` quoted in guidance, or
|
|
27
|
+
// one a tool wrote inside its region, is not read as one of the person's.
|
|
26
28
|
function visibleLines(text) {
|
|
27
29
|
const out = [];
|
|
30
|
+
const owned = regionLines(text);
|
|
28
31
|
let inFence = false;
|
|
29
32
|
let inComment = false;
|
|
30
|
-
|
|
33
|
+
normalizeNewlines(text).split('\n').forEach((line, index) => {
|
|
31
34
|
const trimmed = line.trim();
|
|
35
|
+
if (owned.has(index)) {
|
|
36
|
+
out.push({ line, visible: false });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
32
39
|
if (!inComment && /^(```|~~~)/.test(trimmed)) {
|
|
33
40
|
inFence = !inFence;
|
|
34
41
|
out.push({ line, visible: false });
|
|
35
|
-
|
|
42
|
+
return;
|
|
36
43
|
}
|
|
37
44
|
if (inFence) {
|
|
38
45
|
out.push({ line, visible: false });
|
|
39
|
-
|
|
46
|
+
return;
|
|
40
47
|
}
|
|
41
48
|
if (!inComment && trimmed.startsWith('<!--')) {
|
|
42
49
|
inComment = !trimmed.includes('-->');
|
|
43
50
|
out.push({ line, visible: false });
|
|
44
|
-
|
|
51
|
+
return;
|
|
45
52
|
}
|
|
46
53
|
if (inComment) {
|
|
47
54
|
if (trimmed.includes('-->')) {
|
|
48
55
|
inComment = false;
|
|
49
56
|
}
|
|
50
57
|
out.push({ line, visible: false });
|
|
51
|
-
|
|
58
|
+
return;
|
|
52
59
|
}
|
|
53
60
|
out.push({ line, visible: true });
|
|
54
|
-
}
|
|
61
|
+
});
|
|
55
62
|
return out;
|
|
56
63
|
}
|
|
57
64
|
export function headings(text) {
|