@agentshouse/mdmodel 0.1.0-c4fe1bc82bc55 → 0.1.0-c520180b7a33f
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/API.md +264 -250
- package/README.md +21 -22
- package/dist/blocks.d.ts.map +1 -1
- package/dist/blocks.js +35 -20
- package/dist/definition.d.ts +17 -1
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js +321 -45
- package/dist/divide.d.ts.map +1 -1
- package/dist/divide.js +27 -17
- package/dist/document.d.ts +6 -3
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +326 -171
- package/dist/event.d.ts +78 -0
- package/dist/event.d.ts.map +1 -0
- package/dist/event.js +230 -0
- package/dist/fields.d.ts +2 -0
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +7 -0
- package/dist/index.d.ts +24 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -7
- package/dist/kinds.d.ts +19 -0
- package/dist/kinds.d.ts.map +1 -0
- package/dist/kinds.js +161 -0
- package/dist/links.d.ts +3 -0
- package/dist/links.d.ts.map +1 -1
- package/dist/links.js +108 -51
- package/dist/paths.d.ts +10 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +118 -12
- package/dist/pattern.d.ts +10 -0
- package/dist/pattern.d.ts.map +1 -0
- package/dist/pattern.js +355 -0
- package/dist/project.d.ts +1 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +38 -15
- package/dist/query/language.d.ts +223 -0
- package/dist/query/language.d.ts.map +1 -0
- package/dist/query/language.js +565 -0
- package/dist/query/match.d.ts +14 -0
- package/dist/query/match.d.ts.map +1 -0
- package/dist/query/match.js +121 -0
- package/dist/query/rows.d.ts +32 -0
- package/dist/query/rows.d.ts.map +1 -0
- package/dist/query/rows.js +161 -0
- package/dist/query/sql.d.ts +39 -0
- package/dist/query/sql.d.ts.map +1 -0
- package/dist/query/sql.js +279 -0
- package/dist/references.d.ts +2 -2
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +6 -1
- package/dist/reserved.d.ts +5 -0
- package/dist/reserved.d.ts.map +1 -0
- package/dist/reserved.js +20 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +57 -6
- package/dist/types.d.ts +57 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validate.d.ts +31 -2
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +400 -115
- package/dist/yaml.d.ts +1 -2
- package/dist/yaml.d.ts.map +1 -1
- package/dist/yaml.js +75 -32
- package/fixtures/README.md +27 -7
- package/fixtures/cases/conditional-section/context.json +1 -0
- package/fixtures/cases/conditional-section/expected.json +6 -0
- package/fixtures/cases/conditional-section/library/questions/hosting.md +9 -0
- package/fixtures/cases/conditional-section/library/questions/payments.md +12 -0
- package/fixtures/cases/conditional-section/schema.yaml +14 -0
- package/fixtures/cases/cross-room-link/context.json +1 -0
- package/fixtures/cases/cross-room-link/expected.json +1 -0
- package/fixtures/cases/cross-room-link/library/clients/acme.md +6 -0
- package/fixtures/cases/cross-room-link/schema.yaml +3 -0
- package/fixtures/cases/cross-room-reference/context.json +1 -0
- package/fixtures/cases/cross-room-reference/expected.json +1 -0
- package/fixtures/cases/cross-room-reference/library/clients/acme.md +9 -0
- package/fixtures/cases/cross-room-reference/schema.yaml +9 -0
- package/fixtures/cases/link-into-a-reserved-name/context.json +1 -0
- package/fixtures/cases/link-into-a-reserved-name/expected.json +5 -0
- package/fixtures/cases/link-into-a-reserved-name/library/notes/a.md +6 -0
- package/fixtures/cases/link-into-a-reserved-name/schema.yaml +3 -0
- package/fixtures/cases/link-into-a-reserved-name-from-an-untyped-document/context.json +1 -0
- package/fixtures/cases/link-into-a-reserved-name-from-an-untyped-document/expected.json +5 -0
- package/fixtures/cases/link-into-a-reserved-name-from-an-untyped-document/library/notes/b.md +5 -0
- package/fixtures/cases/member-on-an-untyped-document/context.json +1 -0
- package/fixtures/cases/member-on-an-untyped-document/expected.json +6 -0
- package/fixtures/cases/member-on-an-untyped-document/library/note.md +4 -0
- package/fixtures/cases/member-on-another-type/context.json +1 -0
- package/fixtures/cases/member-on-another-type/expected.json +6 -0
- package/fixtures/cases/member-on-another-type/library/note.md +4 -0
- package/fixtures/cases/member-on-another-type/schema.yaml +7 -0
- package/fixtures/cases/reference-into-a-reserved-name/context.json +1 -0
- package/fixtures/cases/reference-into-a-reserved-name/expected.json +6 -0
- package/fixtures/cases/reference-into-a-reserved-name/library/notes/a.md +4 -0
- package/fixtures/cases/reference-into-a-reserved-name/library/settings/brief.md +1 -0
- package/fixtures/cases/reference-into-a-reserved-name/schema.yaml +3 -0
- package/fixtures/cases/room-md-declares-a-type/context.json +1 -0
- package/fixtures/cases/room-md-declares-a-type/expected.json +6 -0
- package/fixtures/cases/room-md-declares-a-type/library/ROOM.md +5 -0
- package/fixtures/cases/typed-path-names-another-type/context.json +11 -0
- package/fixtures/cases/typed-path-names-another-type/expected.json +6 -0
- package/fixtures/cases/typed-path-names-another-type/library/settings/prefs/one.md +3 -0
- package/fixtures/cases/typed-path-names-another-type/schema.yaml +6 -0
- package/fixtures/cases/typed-path-names-its-type/context.json +11 -0
- package/fixtures/cases/typed-path-names-its-type/expected.json +3 -0
- package/fixtures/cases/typed-path-names-its-type/library/settings/prefs/deeper/two.md +1 -0
- package/fixtures/cases/typed-path-names-its-type/library/settings/prefs/one.md +4 -0
- package/fixtures/cases/typed-path-names-its-type/schema.yaml +6 -0
- package/fixtures/cases/typed-path-names-no-type/context.json +11 -0
- package/fixtures/cases/typed-path-names-no-type/expected.json +6 -0
- package/fixtures/cases/typed-path-names-no-type/library/settings/prefs/one.md +1 -0
- package/fixtures/cases/typed-path-names-no-type/schema.yaml +6 -0
- package/fixtures/cases/unnamed-typed-path-checks-its-fields/context.json +11 -0
- package/fixtures/cases/unnamed-typed-path-checks-its-fields/expected.json +6 -0
- package/fixtures/cases/unnamed-typed-path-checks-its-fields/library/rooms/team/tasks/plan.md +4 -0
- package/fixtures/cases/unnamed-typed-path-checks-its-fields/schema.yaml +5 -0
- package/fixtures/cases/unnamed-typed-path-names-a-type/context.json +11 -0
- package/fixtures/cases/unnamed-typed-path-names-a-type/expected.json +6 -0
- package/fixtures/cases/unnamed-typed-path-names-a-type/library/rooms/team/tasks/plan.md +6 -0
- package/fixtures/cases/unnamed-typed-path-names-a-type/schema.yaml +5 -0
- package/fixtures/cases/unnamed-typed-path-reads-its-type/context.json +11 -0
- package/fixtures/cases/unnamed-typed-path-reads-its-type/expected.json +3 -0
- package/fixtures/cases/unnamed-typed-path-reads-its-type/library/rooms/team/plan.md +1 -0
- package/fixtures/cases/unnamed-typed-path-reads-its-type/library/rooms/team/tasks/plan.md +5 -0
- package/fixtures/cases/unnamed-typed-path-reads-its-type/schema.yaml +5 -0
- package/fixtures/cases/unnamed-typed-path-without-a-body/context.json +11 -0
- package/fixtures/cases/unnamed-typed-path-without-a-body/expected.json +5 -0
- package/fixtures/cases/unnamed-typed-path-without-a-body/library/rooms/team/tasks/plan.md +5 -0
- package/fixtures/cases/unnamed-typed-path-without-a-body/schema.yaml +5 -0
- package/fixtures/division.json +26 -26
- package/fixtures/queries.json +913 -0
- package/package.json +10 -3
- package/dist/condition.d.ts +0 -149
- package/dist/condition.d.ts.map +0 -1
- package/dist/condition.js +0 -491
package/dist/document.js
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
1
|
import { ARRAY_INDEX, pointerSegments } from './definition.js';
|
|
2
|
-
import { hrefOccurrences } from './links.js';
|
|
3
|
-
import { relative, retarget, SHELL_ROOMS, shellTarget, target } from './paths.js';
|
|
2
|
+
import { destinations, hrefOccurrences, linkInventory, } from './links.js';
|
|
3
|
+
import { destinationPath, relative, retarget, roomPath, roomTarget, SHELL_ROOMS, shellTarget, target, } from './paths.js';
|
|
4
4
|
import { references } from './references.js';
|
|
5
5
|
import { headingSpans, sectionsRecord } from './sections.js';
|
|
6
6
|
import { refusal } from './refuse.js';
|
|
7
7
|
import { bodyStartAfterYaml, bodyStartOf, equalValue, extractFrontmatter, frontmatterValue, jsValue, lineEnding, mapEntry, parseYamlMapping, serializeJs, serializeKey, YamlError, } from './yaml.js';
|
|
8
|
-
const
|
|
8
|
+
const HELD = Symbol('held');
|
|
9
|
+
function heldOf(document) {
|
|
10
|
+
return document[HELD];
|
|
11
|
+
}
|
|
12
|
+
export function inventoryOf(document) {
|
|
13
|
+
const held = heldOf(document);
|
|
14
|
+
if (held?.inventory !== undefined)
|
|
15
|
+
return held.inventory;
|
|
16
|
+
const inventory = linkInventory(document.content.slice(bodyStartOf(document.content)));
|
|
17
|
+
if (held !== undefined)
|
|
18
|
+
held.inventory = inventory;
|
|
19
|
+
return inventory;
|
|
20
|
+
}
|
|
9
21
|
function refuse(file, rule, location) {
|
|
10
22
|
return location === undefined ? refusal(file, rule) : refusal(file, rule, { location });
|
|
11
23
|
}
|
|
12
24
|
function freezeDocument(content, type, fields, preamble, sections, internal) {
|
|
13
|
-
const
|
|
25
|
+
const held = { internal, inventory: undefined };
|
|
26
|
+
return Object.freeze(Object.defineProperty({
|
|
14
27
|
content,
|
|
15
28
|
type,
|
|
16
29
|
fields: Object.freeze(fields),
|
|
17
30
|
preamble,
|
|
18
31
|
sections: Object.freeze(sections),
|
|
19
|
-
});
|
|
20
|
-
internals.set(document, internal);
|
|
21
|
-
return document;
|
|
32
|
+
}, HELD, { value: held }));
|
|
22
33
|
}
|
|
23
34
|
function declaredType(fields) {
|
|
24
35
|
const value = fields.type;
|
|
@@ -28,19 +39,10 @@ function declaredType(fields) {
|
|
|
28
39
|
return undefined;
|
|
29
40
|
return value;
|
|
30
41
|
}
|
|
31
|
-
|
|
42
|
+
function opened(content, file) {
|
|
32
43
|
const fence = extractFrontmatter(content);
|
|
33
|
-
if (fence.kind === 'none')
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
ok: true,
|
|
37
|
-
document: freezeDocument(content, undefined, {}, content.slice(0, spans[0]?.headingStart), sectionsRecord(spans, content), {
|
|
38
|
-
root: null,
|
|
39
|
-
yamlEnd: 0,
|
|
40
|
-
bodyStart: 0,
|
|
41
|
-
}),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
+
if (fence.kind === 'none')
|
|
45
|
+
return { ok: true, root: null, fields: {}, yamlEnd: 0 };
|
|
44
46
|
if (fence.kind === 'unclosed')
|
|
45
47
|
return { ok: false, refusal: refuse(file, 'frontmatter-fence') };
|
|
46
48
|
try {
|
|
@@ -49,17 +51,7 @@ export function parse(content, file = '') {
|
|
|
49
51
|
if (fields === null || typeof fields !== 'object' || Array.isArray(fields)) {
|
|
50
52
|
return { ok: false, refusal: refuse(file, 'frontmatter-fence') };
|
|
51
53
|
}
|
|
52
|
-
|
|
53
|
-
const spans = headingSpans(content, bodyStart);
|
|
54
|
-
const record = fields;
|
|
55
|
-
return {
|
|
56
|
-
ok: true,
|
|
57
|
-
document: freezeDocument(content, declaredType(record), record, content.slice(bodyStart, spans[0]?.headingStart), sectionsRecord(spans, content), {
|
|
58
|
-
root,
|
|
59
|
-
yamlEnd: fence.yamlEnd,
|
|
60
|
-
bodyStart,
|
|
61
|
-
}),
|
|
62
|
-
};
|
|
54
|
+
return { ok: true, root, fields: fields, yamlEnd: fence.yamlEnd };
|
|
63
55
|
}
|
|
64
56
|
catch (error) {
|
|
65
57
|
if (error instanceof YamlError)
|
|
@@ -67,66 +59,29 @@ export function parse(content, file = '') {
|
|
|
67
59
|
throw error;
|
|
68
60
|
}
|
|
69
61
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return { outcome: 'refuse', refusal: refuse('', 'frontmatter-fence', name) };
|
|
83
|
-
}
|
|
84
|
-
let nextContent;
|
|
85
|
-
const existing = mapEntry(internal.root, name);
|
|
86
|
-
if (existing !== undefined) {
|
|
87
|
-
nextContent = splice(document.content, existing.value.start, existing.value.end, serializeJs(value));
|
|
88
|
-
}
|
|
89
|
-
else if (document.content[internal.root.start] === '{') {
|
|
90
|
-
nextContent = splice(document.content, internal.root.end - 1, internal.root.end - 1, `${internal.root.entries.length === 0 ? '' : ', '}${serializeKey(name)}: ${serializeJs(value)}`);
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
const nl = lineEnding(document.content);
|
|
94
|
-
const lineStart = document.content.lastIndexOf('\n', internal.root.start - 1) + 1;
|
|
95
|
-
const indent = document.content.slice(lineStart, internal.root.start);
|
|
96
|
-
const line = `${indent}${serializeKey(name)}: ${serializeJs(value)}${nl}`;
|
|
97
|
-
const before = document.content.slice(0, internal.yamlEnd);
|
|
98
|
-
const prefix = before.endsWith('\n') || before.endsWith('\r\n') ? '' : nl;
|
|
99
|
-
nextContent = splice(document.content, internal.yamlEnd, internal.yamlEnd, `${prefix}${line}`);
|
|
100
|
-
}
|
|
101
|
-
const parsed = parse(nextContent);
|
|
102
|
-
if (!parsed.ok)
|
|
103
|
-
return { outcome: 'refuse', refusal: parsed.refusal };
|
|
104
|
-
return { outcome: 'applied', document: parsed.document };
|
|
105
|
-
}
|
|
106
|
-
function applySection(document, name, value, prior) {
|
|
107
|
-
const internal = internals.get(document);
|
|
108
|
-
if (internal === undefined)
|
|
109
|
-
return { outcome: 'refuse', refusal: refuse('', 'required-section', name) };
|
|
110
|
-
const spans = headingSpans(document.content, internal.bodyStart);
|
|
111
|
-
const span = spans.find((item) => item.name === name);
|
|
112
|
-
if (span === undefined)
|
|
113
|
-
return { outcome: 'refuse', refusal: refuse('', 'required-section', name) };
|
|
114
|
-
const current = document.content.slice(span.bodyStart, span.bodyEnd);
|
|
115
|
-
if (current !== prior)
|
|
116
|
-
return { outcome: 'stale', document };
|
|
117
|
-
const parsed = parse(splice(document.content, span.bodyStart, span.bodyEnd, value));
|
|
118
|
-
if (!parsed.ok)
|
|
119
|
-
return { outcome: 'refuse', refusal: parsed.refusal };
|
|
120
|
-
return { outcome: 'applied', document: parsed.document };
|
|
62
|
+
export function parseFrontmatter(content, file = '') {
|
|
63
|
+
const read = opened(content, file);
|
|
64
|
+
if (!read.ok)
|
|
65
|
+
return read;
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
frontmatter: Object.freeze({
|
|
69
|
+
content,
|
|
70
|
+
type: declaredType(read.fields),
|
|
71
|
+
fields: Object.freeze(read.fields),
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
121
74
|
}
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
75
|
+
export function parse(content, file = '') {
|
|
76
|
+
const read = opened(content, file);
|
|
77
|
+
if (!read.ok)
|
|
78
|
+
return read;
|
|
79
|
+
const bodyStart = read.root === null ? 0 : bodyStartAfterYaml(content, read.yamlEnd);
|
|
80
|
+
const spans = headingSpans(content, bodyStart);
|
|
81
|
+
return {
|
|
82
|
+
ok: true,
|
|
83
|
+
document: freezeDocument(content, declaredType(read.fields), read.fields, content.slice(bodyStart, spans[0]?.headingStart), sectionsRecord(spans, content), { root: read.root, yamlEnd: read.yamlEnd, bodyStart }),
|
|
84
|
+
};
|
|
130
85
|
}
|
|
131
86
|
function fitsBare(to) {
|
|
132
87
|
if (to === '' || to.startsWith('<'))
|
|
@@ -155,58 +110,34 @@ function destinationText(to, bracketed) {
|
|
|
155
110
|
return `<${to}>`;
|
|
156
111
|
return null;
|
|
157
112
|
}
|
|
158
|
-
function
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
for (const match of hrefOccurrences(content.slice(bodyStart))) {
|
|
163
|
-
if (match.href !== href)
|
|
164
|
-
continue;
|
|
165
|
-
const text = destinationText(written, match.bracketed);
|
|
166
|
-
if (text === null)
|
|
167
|
-
return refuse('', 'unwritable-link', written);
|
|
168
|
-
rewritten += content.slice(cursor, bodyStart + match.start);
|
|
169
|
-
rewritten += text;
|
|
170
|
-
cursor = bodyStart + match.end;
|
|
171
|
-
}
|
|
172
|
-
return rewritten + content.slice(cursor);
|
|
173
|
-
}
|
|
174
|
-
function shellRelinked(content, bodyStart, file, handle) {
|
|
175
|
-
const hrefs = new Set(hrefOccurrences(content.slice(bodyStart)).map(({ href }) => href));
|
|
176
|
-
let held = content;
|
|
177
|
-
for (const href of hrefs) {
|
|
113
|
+
function shellRelinked(body, file, handle) {
|
|
114
|
+
const occurrences = hrefOccurrences(body);
|
|
115
|
+
const written = new Map();
|
|
116
|
+
for (const href of new Set(occurrences.map((occurrence) => occurrence.href))) {
|
|
178
117
|
const path = shellTarget(href, handle);
|
|
179
118
|
if (path === null)
|
|
180
119
|
continue;
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
return
|
|
184
|
-
|
|
120
|
+
const to = retarget(href, relative(file, path));
|
|
121
|
+
if (destinationText(to, false) === null)
|
|
122
|
+
return refuse('', 'unwritable-link', to);
|
|
123
|
+
written.set(href, to);
|
|
185
124
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return { outcome: 'applied', document: parsed.document };
|
|
197
|
-
}
|
|
198
|
-
function applyLink(document, href, to) {
|
|
199
|
-
const content = document.content;
|
|
200
|
-
const bodyStart = bodyStartOf(content);
|
|
201
|
-
if (!hrefOccurrences(content.slice(bodyStart)).some((occurrence) => occurrence.href === href))
|
|
202
|
-
return { outcome: 'stale', document };
|
|
203
|
-
return reparsed(document, relinked(content, bodyStart, href, to));
|
|
125
|
+
let rewritten = '';
|
|
126
|
+
let cursor = 0;
|
|
127
|
+
for (const match of occurrences) {
|
|
128
|
+
const to = written.get(match.href);
|
|
129
|
+
if (to === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
rewritten += body.slice(cursor, match.start) + destinationText(to, match.bracketed);
|
|
132
|
+
cursor = match.end;
|
|
133
|
+
}
|
|
134
|
+
return rewritten + body.slice(cursor);
|
|
204
135
|
}
|
|
205
|
-
function nodeAt(root, pointer) {
|
|
136
|
+
function nodeAt(root, pointer, entryOf = mapEntry) {
|
|
206
137
|
let node = root;
|
|
207
138
|
for (const key of pointerSegments(pointer)) {
|
|
208
139
|
if (node?.kind === 'map')
|
|
209
|
-
node =
|
|
140
|
+
node = entryOf(node, key)?.value;
|
|
210
141
|
else if (node?.kind === 'seq' && ARRAY_INDEX.test(key))
|
|
211
142
|
node = node.items[Number(key)];
|
|
212
143
|
else
|
|
@@ -221,37 +152,36 @@ function scalarText(source, value) {
|
|
|
221
152
|
return JSON.stringify(value);
|
|
222
153
|
return serializeJs(value);
|
|
223
154
|
}
|
|
224
|
-
function
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
const parsed = parse(splice(document.content, node.start, node.end, text));
|
|
231
|
-
if (!parsed.ok)
|
|
232
|
-
return { outcome: 'refuse', refusal: parsed.refusal };
|
|
233
|
-
return { outcome: 'applied', document: parsed.document };
|
|
234
|
-
}
|
|
235
|
-
export function relativize(document, file, handle, definition) {
|
|
236
|
-
let held = document;
|
|
155
|
+
export function relativize(document, file, handle, definition, kept = new Set()) {
|
|
156
|
+
const relativized = (href) => {
|
|
157
|
+
const path = shellTarget(href, handle);
|
|
158
|
+
return path === null || kept.has(`${SHELL_ROOMS}${handle}/${path}`) ? null : path;
|
|
159
|
+
};
|
|
160
|
+
const changes = [];
|
|
237
161
|
for (const { pointer, value } of definition === undefined
|
|
238
162
|
? []
|
|
239
163
|
: references(document, definition)) {
|
|
240
|
-
const path =
|
|
241
|
-
if (path
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
164
|
+
const path = relativized(value);
|
|
165
|
+
if (path !== null)
|
|
166
|
+
changes.push({
|
|
167
|
+
kind: 'reference',
|
|
168
|
+
pointer,
|
|
169
|
+
value: retarget(value, relative(file, path)),
|
|
170
|
+
prior: value,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
for (const href of new Set(destinations(inventoryOf(document)).map(({ href }) => href))) {
|
|
174
|
+
const path = relativized(href);
|
|
175
|
+
if (path !== null)
|
|
176
|
+
changes.push({ kind: 'link', href, to: relative(file, path) });
|
|
247
177
|
}
|
|
248
|
-
return
|
|
178
|
+
return apply(document, changes);
|
|
249
179
|
}
|
|
250
180
|
export function relativizeBody(body, file, handle) {
|
|
251
|
-
const relinkedBody = shellRelinked(body,
|
|
181
|
+
const relinkedBody = shellRelinked(body, file, handle);
|
|
252
182
|
if (typeof relinkedBody !== 'string')
|
|
253
183
|
return { outcome: 'refuse', refusal: relinkedBody };
|
|
254
|
-
const elsewhere = hrefOccurrences(relinkedBody).find(({ href }) => href
|
|
184
|
+
const elsewhere = hrefOccurrences(relinkedBody).find(({ href }) => destinationPath(href)?.startsWith(SHELL_ROOMS) === true && roomTarget(href) === null);
|
|
255
185
|
if (elsewhere !== undefined)
|
|
256
186
|
return { outcome: 'refuse', refusal: refuse(file, 'unresolved-link', elsewhere.href) };
|
|
257
187
|
return { outcome: 'applied', body: relinkedBody };
|
|
@@ -260,11 +190,11 @@ export function relinkBody(body, file, moves) {
|
|
|
260
190
|
let rewritten = '';
|
|
261
191
|
let cursor = 0;
|
|
262
192
|
for (const match of hrefOccurrences(body)) {
|
|
263
|
-
const from = target(file, match.href);
|
|
193
|
+
const from = target(file, match.href) ?? roomPath(match.href);
|
|
264
194
|
const to = from === null ? undefined : moves.get(from);
|
|
265
195
|
if (to === undefined)
|
|
266
196
|
continue;
|
|
267
|
-
const written = retarget(match.href, relative(file, to));
|
|
197
|
+
const written = retarget(match.href, from.startsWith(SHELL_ROOMS) || to.startsWith(SHELL_ROOMS) ? to : relative(file, to));
|
|
268
198
|
const text = destinationText(written, match.bracketed);
|
|
269
199
|
if (text === null)
|
|
270
200
|
return { outcome: 'refuse', refusal: refuse(file, 'unwritable-link', written) };
|
|
@@ -273,10 +203,10 @@ export function relinkBody(body, file, moves) {
|
|
|
273
203
|
}
|
|
274
204
|
return { outcome: 'applied', body: rewritten + body.slice(cursor) };
|
|
275
205
|
}
|
|
276
|
-
function linksIn(
|
|
206
|
+
function linksIn(occurrences, file) {
|
|
277
207
|
const found = [];
|
|
278
208
|
const seen = new Set();
|
|
279
|
-
for (const occurrence of
|
|
209
|
+
for (const occurrence of occurrences) {
|
|
280
210
|
if (seen.has(occurrence.href))
|
|
281
211
|
continue;
|
|
282
212
|
const resolved = target(file, occurrence.href);
|
|
@@ -288,25 +218,250 @@ function linksIn(text, file) {
|
|
|
288
218
|
return found;
|
|
289
219
|
}
|
|
290
220
|
export function links(document, file) {
|
|
291
|
-
return linksIn(
|
|
221
|
+
return linksIn(destinations(inventoryOf(document)), file);
|
|
292
222
|
}
|
|
293
223
|
export function bodyLinks(body, file) {
|
|
294
|
-
return linksIn(body, file);
|
|
224
|
+
return linksIn(hrefOccurrences(body), file);
|
|
295
225
|
}
|
|
296
|
-
|
|
226
|
+
function internalOf(document) {
|
|
227
|
+
const held = heldOf(document);
|
|
228
|
+
if (held !== undefined)
|
|
229
|
+
return held.internal;
|
|
230
|
+
const read = opened(document.content, '');
|
|
231
|
+
if (!read.ok)
|
|
232
|
+
return read.refusal;
|
|
233
|
+
return {
|
|
234
|
+
root: read.root,
|
|
235
|
+
yamlEnd: read.yamlEnd,
|
|
236
|
+
bodyStart: read.root === null ? 0 : bodyStartAfterYaml(document.content, read.yamlEnd),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function partOf(change) {
|
|
240
|
+
if (change.kind === 'field' || change.kind === 'section')
|
|
241
|
+
return `${change.kind} ${change.name}`;
|
|
297
242
|
if (change.kind === 'link')
|
|
298
|
-
return
|
|
243
|
+
return `link ${change.href}`;
|
|
299
244
|
if (change.kind === 'reference')
|
|
300
|
-
return
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
245
|
+
return `reference ${change.pointer}`;
|
|
246
|
+
return 'preamble';
|
|
247
|
+
}
|
|
248
|
+
function planner(document, internal) {
|
|
249
|
+
const content = document.content;
|
|
250
|
+
const keyed = new Map();
|
|
251
|
+
let spans;
|
|
252
|
+
let occurrences;
|
|
253
|
+
let ending;
|
|
254
|
+
let added = 0;
|
|
255
|
+
const entryOf = (map, key) => {
|
|
256
|
+
let index = keyed.get(map);
|
|
257
|
+
if (index === undefined) {
|
|
258
|
+
index = new Map();
|
|
259
|
+
for (const entry of map.entries)
|
|
260
|
+
if (!index.has(entry.key))
|
|
261
|
+
index.set(entry.key, entry);
|
|
262
|
+
keyed.set(map, index);
|
|
263
|
+
}
|
|
264
|
+
return index.get(key);
|
|
265
|
+
};
|
|
266
|
+
const field = (name, value, prior, at) => {
|
|
267
|
+
const root = internal.root;
|
|
268
|
+
if (root === null)
|
|
269
|
+
return refuse('', 'frontmatter-fence', name);
|
|
270
|
+
if (!equalValue(document.fields[name], prior))
|
|
271
|
+
return 'stale';
|
|
272
|
+
if (!frontmatterValue(value))
|
|
273
|
+
return refuse('', 'frontmatter-fence', name);
|
|
274
|
+
const existing = entryOf(root, name);
|
|
275
|
+
if (existing !== undefined) {
|
|
276
|
+
const held = existing.value;
|
|
277
|
+
const inline = held.start > existing.after && !/[\r\n]/.test(content.slice(existing.after, held.start));
|
|
278
|
+
return inline
|
|
279
|
+
? [{ start: held.start, end: held.end, text: serializeJs(value), at }]
|
|
280
|
+
: [{ start: existing.after, end: held.end, text: ` ${serializeJs(value)}`, at }];
|
|
281
|
+
}
|
|
282
|
+
const entry = `${serializeKey(name)}: ${serializeJs(value)}`;
|
|
283
|
+
added += 1;
|
|
284
|
+
if (content[root.start] === '{') {
|
|
285
|
+
const separator = root.entries.length === 0 && added === 1 ? '' : ', ';
|
|
286
|
+
return [{ start: root.end - 1, end: root.end - 1, text: `${separator}${entry}`, at }];
|
|
287
|
+
}
|
|
288
|
+
const lineStart = content.lastIndexOf('\n', root.start - 1) + 1;
|
|
289
|
+
ending ??= lineEnding(content);
|
|
290
|
+
const line = `${content.slice(lineStart, root.start)}${entry}${ending}`;
|
|
291
|
+
return [{ start: internal.yamlEnd, end: internal.yamlEnd, text: line, at, entry: true }];
|
|
292
|
+
};
|
|
293
|
+
const section = (name, value, prior, at) => {
|
|
294
|
+
if (spans === undefined) {
|
|
295
|
+
spans = new Map();
|
|
296
|
+
for (const span of headingSpans(content, internal.bodyStart))
|
|
297
|
+
if (!spans.has(span.name))
|
|
298
|
+
spans.set(span.name, span);
|
|
299
|
+
}
|
|
300
|
+
const span = spans.get(name);
|
|
301
|
+
if (span === undefined)
|
|
302
|
+
return refuse('', 'required-section', name);
|
|
303
|
+
if (content.slice(span.bodyStart, span.bodyEnd) !== prior)
|
|
304
|
+
return 'stale';
|
|
305
|
+
return [{ start: span.bodyStart, end: span.bodyEnd, text: value, at }];
|
|
306
|
+
};
|
|
307
|
+
const link = (href, to, at) => {
|
|
308
|
+
if (occurrences === undefined) {
|
|
309
|
+
occurrences = new Map();
|
|
310
|
+
for (const occurrence of destinations(inventoryOf(document))) {
|
|
311
|
+
const same = occurrences.get(occurrence.href);
|
|
312
|
+
if (same === undefined)
|
|
313
|
+
occurrences.set(occurrence.href, [occurrence]);
|
|
314
|
+
else
|
|
315
|
+
same.push(occurrence);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const found = occurrences.get(href);
|
|
319
|
+
if (found === undefined)
|
|
320
|
+
return 'stale';
|
|
321
|
+
const written = retarget(href, to);
|
|
322
|
+
if (destinationText(written, false) === null)
|
|
323
|
+
return refuse('', 'unwritable-link', written);
|
|
324
|
+
return found.map((occurrence) => ({
|
|
325
|
+
start: internal.bodyStart + occurrence.start,
|
|
326
|
+
end: internal.bodyStart + occurrence.end,
|
|
327
|
+
text: destinationText(written, occurrence.bracketed),
|
|
328
|
+
at,
|
|
329
|
+
}));
|
|
330
|
+
};
|
|
331
|
+
const reference = (pointer, value, prior, at) => {
|
|
332
|
+
const node = internal.root === null ? undefined : nodeAt(internal.root, pointer, entryOf);
|
|
333
|
+
if (node?.kind !== 'scalar' || node.value !== prior)
|
|
334
|
+
return 'stale';
|
|
335
|
+
return [
|
|
336
|
+
{ start: node.start, end: node.end, text: scalarText(content[node.start], value), at },
|
|
337
|
+
];
|
|
338
|
+
};
|
|
339
|
+
return (change, at) => {
|
|
340
|
+
if (change.kind === 'field')
|
|
341
|
+
return field(change.name, change.value, change.prior, at);
|
|
342
|
+
if (change.kind === 'section')
|
|
343
|
+
return section(change.name, change.value, change.prior, at);
|
|
344
|
+
if (change.kind === 'link')
|
|
345
|
+
return link(change.href, change.to, at);
|
|
346
|
+
if (change.kind === 'reference')
|
|
347
|
+
return reference(change.pointer, change.value, change.prior, at);
|
|
348
|
+
if (document.preamble !== change.prior)
|
|
349
|
+
return 'stale';
|
|
350
|
+
const start = internal.bodyStart;
|
|
351
|
+
return [{ start, end: start + document.preamble.length, text: change.value, at }];
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function overlapping(edits, stale) {
|
|
355
|
+
let reach = -1;
|
|
356
|
+
let owner = -1;
|
|
357
|
+
for (const edit of edits) {
|
|
358
|
+
if (edit.start < reach)
|
|
359
|
+
stale.add(Math.max(edit.at, owner));
|
|
360
|
+
if (edit.end > reach) {
|
|
361
|
+
reach = edit.end;
|
|
362
|
+
owner = edit.at;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function firstHeadings(content, bodyStart) {
|
|
367
|
+
const first = new Map();
|
|
368
|
+
for (const span of headingSpans(content, bodyStart)) {
|
|
369
|
+
if (!first.has(span.name))
|
|
370
|
+
first.set(span.name, span.headingStart);
|
|
371
|
+
}
|
|
372
|
+
return first;
|
|
373
|
+
}
|
|
374
|
+
function hidden(before, bodyStart, after, shifts) {
|
|
375
|
+
const placed = firstHeadings(after, bodyStartOf(after));
|
|
376
|
+
let shift = 0;
|
|
377
|
+
let index = 0;
|
|
378
|
+
for (const [name, at] of firstHeadings(before, bodyStart)) {
|
|
379
|
+
while (index < shifts.length && shifts[index][0] <= at) {
|
|
380
|
+
shift += shifts[index][1];
|
|
381
|
+
index += 1;
|
|
382
|
+
}
|
|
383
|
+
if (placed.get(name) !== at + shift)
|
|
384
|
+
return name;
|
|
385
|
+
}
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
function spliced(source, edits) {
|
|
389
|
+
const shifts = [];
|
|
390
|
+
const pieces = [];
|
|
391
|
+
let cursor = 0;
|
|
392
|
+
let entered = false;
|
|
393
|
+
let ended = false;
|
|
394
|
+
for (const edit of edits) {
|
|
395
|
+
const kept = source.slice(cursor, edit.start);
|
|
396
|
+
pieces.push(kept);
|
|
397
|
+
if (kept !== '')
|
|
398
|
+
ended = kept.endsWith('\n');
|
|
399
|
+
let text = edit.text;
|
|
400
|
+
if (edit.entry === true && !entered) {
|
|
401
|
+
entered = true;
|
|
402
|
+
if (!ended)
|
|
403
|
+
text = `${lineEnding(source)}${text}`;
|
|
404
|
+
}
|
|
405
|
+
pieces.push(text);
|
|
406
|
+
if (text !== '')
|
|
407
|
+
ended = text.endsWith('\n');
|
|
408
|
+
shifts.push([edit.start, text.length - (edit.end - edit.start)]);
|
|
409
|
+
cursor = edit.end;
|
|
410
|
+
}
|
|
411
|
+
pieces.push(source.slice(cursor));
|
|
412
|
+
return { content: pieces.join(''), shifts };
|
|
413
|
+
}
|
|
414
|
+
export function apply(document, changes) {
|
|
415
|
+
const internal = internalOf(document);
|
|
416
|
+
if (!('bodyStart' in internal))
|
|
417
|
+
return { outcome: 'refuse', refusal: internal };
|
|
418
|
+
const plan = planner(document, internal);
|
|
419
|
+
const edits = [];
|
|
420
|
+
const stale = new Set();
|
|
421
|
+
const named = new Set();
|
|
422
|
+
for (const [at, change] of changes.entries()) {
|
|
423
|
+
const planned = plan(change, at);
|
|
424
|
+
if (planned === 'stale') {
|
|
425
|
+
stale.add(at);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (!Array.isArray(planned))
|
|
429
|
+
return { outcome: 'refuse', refusal: planned };
|
|
430
|
+
const part = partOf(change);
|
|
431
|
+
if (named.has(part)) {
|
|
432
|
+
stale.add(at);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
named.add(part);
|
|
436
|
+
for (const edit of planned)
|
|
437
|
+
edits.push(edit);
|
|
438
|
+
}
|
|
439
|
+
edits.sort((left, right) => left.start - right.start || left.end - right.end || left.at - right.at);
|
|
440
|
+
overlapping(edits, stale);
|
|
441
|
+
if (stale.size > 0)
|
|
442
|
+
return { outcome: 'stale', document, stale: [...stale].sort((left, right) => left - right) };
|
|
443
|
+
const source = document.content;
|
|
444
|
+
const { content } = spliced(source, edits);
|
|
445
|
+
if (content === source)
|
|
446
|
+
return { outcome: 'applied', document };
|
|
447
|
+
const replaced = edits.filter((edit) => {
|
|
448
|
+
const kind = changes[edit.at].kind;
|
|
449
|
+
return kind === 'section' || kind === 'preamble';
|
|
450
|
+
});
|
|
451
|
+
if (replaced.length > 0) {
|
|
452
|
+
const checked = spliced(source, replaced);
|
|
453
|
+
const name = hidden(source, internal.bodyStart, checked.content, checked.shifts);
|
|
454
|
+
if (name !== undefined)
|
|
455
|
+
return { outcome: 'refuse', refusal: refuse('', 'overlapping-section', name) };
|
|
456
|
+
}
|
|
457
|
+
const parsed = parse(content);
|
|
458
|
+
if (!parsed.ok)
|
|
459
|
+
return { outcome: 'refuse', refusal: parsed.refusal };
|
|
460
|
+
return { outcome: 'applied', document: parsed.document };
|
|
306
461
|
}
|
|
307
462
|
export function serialize(document) {
|
|
308
463
|
return document.content;
|
|
309
464
|
}
|
|
310
465
|
export function frontmatter(document) {
|
|
311
|
-
return
|
|
466
|
+
return heldOf(document)?.internal.root ?? null;
|
|
312
467
|
}
|
package/dist/event.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type Definition } from './definition.js';
|
|
2
|
+
import type { Candidate } from './query/match.js';
|
|
3
|
+
import type { Document, VerifiedModel } from './types.js';
|
|
4
|
+
export type FactKind = 'string' | 'number' | 'boolean' | 'date' | 'time' | 'enum' | 'reference';
|
|
5
|
+
type Scalar = string | number | boolean;
|
|
6
|
+
export type Fact = {
|
|
7
|
+
readonly kind: FactKind;
|
|
8
|
+
readonly value: Scalar;
|
|
9
|
+
readonly member?: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: 'prose';
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: 'null';
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: 'unreadable';
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: 'list';
|
|
18
|
+
readonly items: readonly Fact[];
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: 'object';
|
|
21
|
+
readonly fields: Readonly<Record<string, Fact>>;
|
|
22
|
+
};
|
|
23
|
+
export type DocumentFacts = {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly path: string;
|
|
26
|
+
readonly type: string | null;
|
|
27
|
+
readonly fields: Readonly<Record<string, Fact>>;
|
|
28
|
+
readonly sections: readonly string[];
|
|
29
|
+
};
|
|
30
|
+
export declare const TEXT_FIELD = "/text";
|
|
31
|
+
export declare const WRITE_DOORS: readonly ['browser', 'agent', 'client', 'function'];
|
|
32
|
+
export type WriteDoor = (typeof WRITE_DOORS)[number];
|
|
33
|
+
export type Actor = {
|
|
34
|
+
readonly user?: string;
|
|
35
|
+
readonly site?: string;
|
|
36
|
+
readonly app?: string;
|
|
37
|
+
readonly door?: WriteDoor;
|
|
38
|
+
};
|
|
39
|
+
export type DocumentEvent = {
|
|
40
|
+
readonly before: DocumentFacts | null;
|
|
41
|
+
readonly after: DocumentFacts | null;
|
|
42
|
+
readonly changed: readonly string[];
|
|
43
|
+
readonly by: Actor;
|
|
44
|
+
readonly moved_between_rooms: boolean;
|
|
45
|
+
};
|
|
46
|
+
export type ItemFacts = {
|
|
47
|
+
readonly delivery: string;
|
|
48
|
+
readonly source: string | null;
|
|
49
|
+
readonly address: string | null;
|
|
50
|
+
readonly container: string | null;
|
|
51
|
+
readonly author: string | null;
|
|
52
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
53
|
+
readonly json: {
|
|
54
|
+
readonly value: unknown;
|
|
55
|
+
} | null;
|
|
56
|
+
};
|
|
57
|
+
export type MessageFacts = {
|
|
58
|
+
readonly sender: string;
|
|
59
|
+
readonly agent_function: string | null;
|
|
60
|
+
readonly path: string | null;
|
|
61
|
+
};
|
|
62
|
+
export type Resolved = {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly member?: string;
|
|
65
|
+
};
|
|
66
|
+
export declare function facts(document: Pick<Document, 'type' | 'fields' | 'sections'>, at: {
|
|
67
|
+
readonly id: string;
|
|
68
|
+
readonly path: string;
|
|
69
|
+
}, model: VerifiedModel, resolve: (target: string) => Resolved | undefined): DocumentFacts;
|
|
70
|
+
export declare function prosePaths(facts: DocumentFacts): string[];
|
|
71
|
+
export declare function members(document: DocumentFacts, path: string): readonly string[];
|
|
72
|
+
export declare function pointed(value: unknown, pointer: string): unknown;
|
|
73
|
+
export declare function valuedChange(definition: Definition, pointer: string): boolean;
|
|
74
|
+
export declare function datePointers(definition: Definition): string[];
|
|
75
|
+
export declare function memberPointers(definition: Definition, memberType: string): string[];
|
|
76
|
+
export declare function candidateOf(document: DocumentFacts): Candidate;
|
|
77
|
+
export {};
|
|
78
|
+
//# sourceMappingURL=event.d.ts.map
|