mechanica-shared 2.0.0-alpha.0 → 2.0.0-alpha.3

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.
@@ -0,0 +1,86 @@
1
+ import { parse, stringify } from "yaml";
2
+ //#region src/block-format.ts
3
+ /**
4
+ * Codec for the composed-block file format (`.mech/blocks/<id>.block.yml`) — a
5
+ * single YAML document holding a {@link ComposedBlockDefinition}. Kept a
6
+ * separate entry point (`mechanica-shared/block-format`), deliberately NOT
7
+ * re-exported from the barrel: it pulls in the YAML parser, and the barrel is
8
+ * imported by the client runtime, which never parses these files (only the dev
9
+ * store, the plugin's collect step, and the CLI do). Mirrors the `page-format`
10
+ * rule. See PLAN.md § 3.
11
+ */
12
+ /** Thrown by {@link parseComposedBlock} with a human-readable reason. */
13
+ var ComposedBlockParseError = class ComposedBlockParseError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "ComposedBlockParseError";
17
+ Object.setPrototypeOf(this, ComposedBlockParseError.prototype);
18
+ }
19
+ };
20
+ function isPlainObject(value) {
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ }
23
+ /** Validate the minimal shape of a content node (recurses into children). */
24
+ function assertContentBlock(node, where) {
25
+ if (!isPlainObject(node)) throw new ComposedBlockParseError(`${where}: expected a mapping`);
26
+ if (typeof node.blockId !== "string" || node.blockId === "") throw new ComposedBlockParseError(`${where}: missing "blockId"`);
27
+ if (node.data !== void 0 && !isPlainObject(node.data)) throw new ComposedBlockParseError(`${where}: "data" must be a mapping`);
28
+ const children = node.children;
29
+ if (children === void 0) return;
30
+ if (Array.isArray(children)) children.forEach((child, i) => assertContentBlock(child, `${where} › child ${i}`));
31
+ else if (isPlainObject(children)) for (const [slot, list] of Object.entries(children)) {
32
+ if (!Array.isArray(list)) throw new ComposedBlockParseError(`${where}: slot "${slot}" must be a list`);
33
+ list.forEach((child, i) => assertContentBlock(child, `${where} › ${slot}[${i}]`));
34
+ }
35
+ else throw new ComposedBlockParseError(`${where}: "children" must be a list or a slot mapping`);
36
+ }
37
+ /**
38
+ * Parse a `.block.yml` document into a {@link ComposedBlockDefinition}.
39
+ *
40
+ * @param fallbackId Used as the id when the document omits one — the store and
41
+ * plugin pass the filename base, so a hand-authored file may
42
+ * omit `id` and be identified by its filename.
43
+ */
44
+ function parseComposedBlock(text, fallbackId) {
45
+ let doc;
46
+ try {
47
+ doc = parse(text);
48
+ } catch (error) {
49
+ throw new ComposedBlockParseError(`invalid YAML: ${error instanceof Error ? error.message : error}`);
50
+ }
51
+ if (doc == null) doc = {};
52
+ if (!isPlainObject(doc)) throw new ComposedBlockParseError("expected a top-level mapping");
53
+ const id = typeof doc.id === "string" && doc.id !== "" ? doc.id : fallbackId;
54
+ if (!id) throw new ComposedBlockParseError("missing \"id\"");
55
+ if (typeof doc.name !== "string" || doc.name === "") throw new ComposedBlockParseError("missing \"name\"");
56
+ const template = doc.template ?? [];
57
+ if (!Array.isArray(template)) throw new ComposedBlockParseError("\"template\" must be a list");
58
+ template.forEach((node, i) => assertContentBlock(node, `template[${i}]`));
59
+ if (doc.props !== void 0 && !isPlainObject(doc.props)) throw new ComposedBlockParseError("\"props\" must be a mapping");
60
+ if (doc.previewData !== void 0 && !isPlainObject(doc.previewData)) throw new ComposedBlockParseError("\"previewData\" must be a mapping");
61
+ const def = {
62
+ id,
63
+ name: doc.name,
64
+ template
65
+ };
66
+ if (typeof doc.icon === "string") def.icon = doc.icon;
67
+ if (typeof doc.category === "string") def.category = doc.category;
68
+ if (isPlainObject(doc.props)) def.props = doc.props;
69
+ if (isPlainObject(doc.previewData)) def.previewData = doc.previewData;
70
+ return def;
71
+ }
72
+ /** Serialize a {@link ComposedBlockDefinition} to a `.block.yml` document. */
73
+ function serializeComposedBlock(def) {
74
+ const ordered = {
75
+ id: def.id,
76
+ name: def.name
77
+ };
78
+ if (def.icon) ordered.icon = def.icon;
79
+ if (def.category) ordered.category = def.category;
80
+ if (def.props && Object.keys(def.props).length) ordered.props = def.props;
81
+ if (def.previewData && Object.keys(def.previewData).length) ordered.previewData = def.previewData;
82
+ ordered.template = def.template ?? [];
83
+ return stringify(ordered, { lineWidth: 0 });
84
+ }
85
+ //#endregion
86
+ export { ComposedBlockParseError, parseComposedBlock, serializeComposedBlock };