@hoardodile/sdk-types 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +59 -0
  3. package/dist/image-variant.d.ts +90 -0
  4. package/dist/image-variant.js +115 -0
  5. package/dist/image-variant.js.map +1 -0
  6. package/dist/index.d.ts +772 -0
  7. package/dist/index.js +353 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/manifest-Dk6_xyNy.d.ts +204 -0
  10. package/dist/media-exts.d.ts +92 -0
  11. package/dist/media-exts.js +160 -0
  12. package/dist/media-exts.js.map +1 -0
  13. package/dist/plugin-asset-limits.d.ts +16 -0
  14. package/dist/plugin-asset-limits.js +13 -0
  15. package/dist/plugin-asset-limits.js.map +1 -0
  16. package/dist/plugin-capabilities.d.ts +64 -0
  17. package/dist/plugin-capabilities.js +37 -0
  18. package/dist/plugin-capabilities.js.map +1 -0
  19. package/dist/plugin.d.ts +49 -0
  20. package/dist/plugin.js +12 -0
  21. package/dist/plugin.js.map +1 -0
  22. package/dist/resource.d.ts +26 -0
  23. package/dist/resource.js +9 -0
  24. package/dist/resource.js.map +1 -0
  25. package/dist/result.d.ts +47 -0
  26. package/dist/result.js +20 -0
  27. package/dist/result.js.map +1 -0
  28. package/dist/schema.d.ts +29 -0
  29. package/dist/schema.js +124 -0
  30. package/dist/schema.js.map +1 -0
  31. package/dist/template.d.ts +67 -0
  32. package/dist/template.js +137 -0
  33. package/dist/template.js.map +1 -0
  34. package/dist/text-limits.d.ts +11 -0
  35. package/dist/text-limits.js +7 -0
  36. package/dist/text-limits.js.map +1 -0
  37. package/package.json +102 -0
  38. package/src/file-list.ts +14 -0
  39. package/src/image-variant.test.ts +140 -0
  40. package/src/image-variant.ts +234 -0
  41. package/src/index.ts +115 -0
  42. package/src/manifest.ts +186 -0
  43. package/src/media-exts.ts +245 -0
  44. package/src/plugin-asset-limits.ts +23 -0
  45. package/src/plugin-asset.ts +127 -0
  46. package/src/plugin-capabilities.ts +91 -0
  47. package/src/plugin-definition.test.ts +117 -0
  48. package/src/plugin-definition.ts +902 -0
  49. package/src/plugin.ts +54 -0
  50. package/src/read-range.ts +12 -0
  51. package/src/resource.ts +28 -0
  52. package/src/result.test.ts +64 -0
  53. package/src/result.ts +73 -0
  54. package/src/schema.ts +29 -0
  55. package/src/template.test.ts +116 -0
  56. package/src/template.ts +199 -0
  57. package/src/text-limits.ts +11 -0
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Plugin-facing runtime limits: the read cap plugins must respect and
3
+ * the fan-out bounds plugins should stay within when probing. Sandbox
4
+ * tuning (watchdog, timeouts, memory) is app-side and lives in
5
+ * `@hoardodile/host` instead.
6
+ */
7
+ /**
8
+ * Upper bound for a single `readFile` call, full or ranged. Anything
9
+ * bigger must go through byte ranges (`readFileChunks`) so neither the
10
+ * host process nor the plugin worker buffers it whole.
11
+ */
12
+ declare const PLUGIN_READ_FILE_MAX_BYTES: number;
13
+ /**
14
+ * Parallel image probes a plugin hook may fan out across the host.
15
+ * Host-side probes run sharp concurrently; keep them bounded.
16
+ */
17
+ declare const PLUGIN_IMAGE_PROBE_CONCURRENCY = 8;
18
+ /**
19
+ * Parallel video probes a plugin hook may fan out. Each spawns an
20
+ * ffprobe process host-side, so videos are bound tighter than images.
21
+ */
22
+ declare const PLUGIN_VIDEO_PROBE_CONCURRENCY = 4;
23
+ /**
24
+ * Parallel audio probes a plugin hook may fan out. Shares the ffprobe
25
+ * spawn budget with video, so it carries the same bound.
26
+ */
27
+ declare const PLUGIN_AUDIO_PROBE_CONCURRENCY = 4;
28
+ /**
29
+ * How many audio files a `coverLocal` hook scans looking for embedded
30
+ * artwork before settling for the first audio file. Albums carry the
31
+ * same artwork on every track, so the scan almost always stops at the
32
+ * first probe; the cap keeps a pathological archive from spawning one
33
+ * ffprobe per track.
34
+ */
35
+ declare const PLUGIN_AUDIO_COVER_SCAN_LIMIT = 8;
36
+ /**
37
+ * Chunk size for batch `statFiles` calls: the host resolves each chunk
38
+ * in one RPC round-trip, so a 100-file archive costs ~13 round-trips
39
+ * instead of 100.
40
+ */
41
+ declare const PLUGIN_STAT_CONCURRENCY = 8;
42
+ /**
43
+ * Batch size for animation scans in `searchMeta` hooks: probes run
44
+ * concurrently within a batch, and the early-exit check happens between
45
+ * batches.
46
+ */
47
+ declare const PLUGIN_ANIMATION_SCAN_BATCH = 8;
48
+
49
+ export { PLUGIN_ANIMATION_SCAN_BATCH, PLUGIN_AUDIO_COVER_SCAN_LIMIT, PLUGIN_AUDIO_PROBE_CONCURRENCY, PLUGIN_IMAGE_PROBE_CONCURRENCY, PLUGIN_READ_FILE_MAX_BYTES, PLUGIN_STAT_CONCURRENCY, PLUGIN_VIDEO_PROBE_CONCURRENCY };
package/dist/plugin.js ADDED
@@ -0,0 +1,12 @@
1
+ // src/plugin.ts
2
+ var PLUGIN_READ_FILE_MAX_BYTES = 128 * 1024 * 1024;
3
+ var PLUGIN_IMAGE_PROBE_CONCURRENCY = 8;
4
+ var PLUGIN_VIDEO_PROBE_CONCURRENCY = 4;
5
+ var PLUGIN_AUDIO_PROBE_CONCURRENCY = 4;
6
+ var PLUGIN_AUDIO_COVER_SCAN_LIMIT = 8;
7
+ var PLUGIN_STAT_CONCURRENCY = 8;
8
+ var PLUGIN_ANIMATION_SCAN_BATCH = 8;
9
+
10
+ export { PLUGIN_ANIMATION_SCAN_BATCH, PLUGIN_AUDIO_COVER_SCAN_LIMIT, PLUGIN_AUDIO_PROBE_CONCURRENCY, PLUGIN_IMAGE_PROBE_CONCURRENCY, PLUGIN_READ_FILE_MAX_BYTES, PLUGIN_STAT_CONCURRENCY, PLUGIN_VIDEO_PROBE_CONCURRENCY };
11
+ //# sourceMappingURL=plugin.js.map
12
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin.ts"],"names":[],"mappings":";AAYO,IAAM,0BAAA,GAA6B,MAAM,IAAA,GAAO;AAMhD,IAAM,8BAAA,GAAiC;AAMvC,IAAM,8BAAA,GAAiC;AAMvC,IAAM,8BAAA,GAAiC;AASvC,IAAM,6BAAA,GAAgC;AAOtC,IAAM,uBAAA,GAA0B;AAOhC,IAAM,2BAAA,GAA8B","file":"plugin.js","sourcesContent":["/**\n * Plugin-facing runtime limits: the read cap plugins must respect and\n * the fan-out bounds plugins should stay within when probing. Sandbox\n * tuning (watchdog, timeouts, memory) is app-side and lives in\n * `@hoardodile/host` instead.\n */\n\n/**\n * Upper bound for a single `readFile` call, full or ranged. Anything\n * bigger must go through byte ranges (`readFileChunks`) so neither the\n * host process nor the plugin worker buffers it whole.\n */\nexport const PLUGIN_READ_FILE_MAX_BYTES = 128 * 1024 * 1024\n\n/**\n * Parallel image probes a plugin hook may fan out across the host.\n * Host-side probes run sharp concurrently; keep them bounded.\n */\nexport const PLUGIN_IMAGE_PROBE_CONCURRENCY = 8\n\n/**\n * Parallel video probes a plugin hook may fan out. Each spawns an\n * ffprobe process host-side, so videos are bound tighter than images.\n */\nexport const PLUGIN_VIDEO_PROBE_CONCURRENCY = 4\n\n/**\n * Parallel audio probes a plugin hook may fan out. Shares the ffprobe\n * spawn budget with video, so it carries the same bound.\n */\nexport const PLUGIN_AUDIO_PROBE_CONCURRENCY = 4\n\n/**\n * How many audio files a `coverLocal` hook scans looking for embedded\n * artwork before settling for the first audio file. Albums carry the\n * same artwork on every track, so the scan almost always stops at the\n * first probe; the cap keeps a pathological archive from spawning one\n * ffprobe per track.\n */\nexport const PLUGIN_AUDIO_COVER_SCAN_LIMIT = 8\n\n/**\n * Chunk size for batch `statFiles` calls: the host resolves each chunk\n * in one RPC round-trip, so a 100-file archive costs ~13 round-trips\n * instead of 100.\n */\nexport const PLUGIN_STAT_CONCURRENCY = 8\n\n/**\n * Batch size for animation scans in `searchMeta` hooks: probes run\n * concurrently within a batch, and the early-exit check happens between\n * batches.\n */\nexport const PLUGIN_ANIMATION_SCAN_BATCH = 8\n"]}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Plugin-facing resource caps. The preview policy (`exceedsPreviewThresholds`,
3
+ * which consumes the two caps below) lives with its caller in
4
+ * `@hoardodile/sdk-server/helpers`; the cover cap below is consumed by
5
+ * the app's thumb pipeline, the CLI's workbench renders and the media
6
+ * helpers in `@hoardodile/host`. Character image-area caps stay
7
+ * app-internal in `@hoardodile/shared`.
8
+ */
9
+ /**
10
+ * Schema version stamped onto every `SearchMeta` payload. Plugins
11
+ * that build search-meta MUST emit this exact value so the host can
12
+ * detect format drift across plugin upgrades.
13
+ */
14
+ declare const SEARCH_META_VERSION = 1;
15
+ /** Max pixel area for resource covers; larger images are scaled down. */
16
+ declare const RESOURCE_COVER_MAX_AREA = 300000;
17
+ /** Max pixel area for preview variants served to resource previews. */
18
+ declare const RESOURCE_PREVIEW_MAX_AREA = 4000000;
19
+ /**
20
+ * Byte-size threshold for preview eligibility. An image whose
21
+ * area is at or below the cap may still qualify for preview when
22
+ * its byte size exceeds this value.
23
+ */
24
+ declare const RESOURCE_PREVIEW_SIZE_THRESHOLD = 1000000;
25
+
26
+ export { RESOURCE_COVER_MAX_AREA, RESOURCE_PREVIEW_MAX_AREA, RESOURCE_PREVIEW_SIZE_THRESHOLD, SEARCH_META_VERSION };
@@ -0,0 +1,9 @@
1
+ // src/resource.ts
2
+ var SEARCH_META_VERSION = 1;
3
+ var RESOURCE_COVER_MAX_AREA = 3e5;
4
+ var RESOURCE_PREVIEW_MAX_AREA = 4e6;
5
+ var RESOURCE_PREVIEW_SIZE_THRESHOLD = 1e6;
6
+
7
+ export { RESOURCE_COVER_MAX_AREA, RESOURCE_PREVIEW_MAX_AREA, RESOURCE_PREVIEW_SIZE_THRESHOLD, SEARCH_META_VERSION };
8
+ //# sourceMappingURL=resource.js.map
9
+ //# sourceMappingURL=resource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/resource.ts"],"names":[],"mappings":";AAcO,IAAM,mBAAA,GAAsB;AAG5B,IAAM,uBAAA,GAA0B;AAGhC,IAAM,yBAAA,GAA4B;AAOlC,IAAM,+BAAA,GAAkC","file":"resource.js","sourcesContent":["/**\n * Plugin-facing resource caps. The preview policy (`exceedsPreviewThresholds`,\n * which consumes the two caps below) lives with its caller in\n * `@hoardodile/sdk-server/helpers`; the cover cap below is consumed by\n * the app's thumb pipeline, the CLI's workbench renders and the media\n * helpers in `@hoardodile/host`. Character image-area caps stay\n * app-internal in `@hoardodile/shared`.\n */\n\n/**\n * Schema version stamped onto every `SearchMeta` payload. Plugins\n * that build search-meta MUST emit this exact value so the host can\n * detect format drift across plugin upgrades.\n */\nexport const SEARCH_META_VERSION = 1\n\n/** Max pixel area for resource covers; larger images are scaled down. */\nexport const RESOURCE_COVER_MAX_AREA = 300_000\n\n/** Max pixel area for preview variants served to resource previews. */\nexport const RESOURCE_PREVIEW_MAX_AREA = 4_000_000\n\n/**\n * Byte-size threshold for preview eligibility. An image whose\n * area is at or below the cap may still qualify for preview when\n * its byte size exceeds this value.\n */\nexport const RESOURCE_PREVIEW_SIZE_THRESHOLD = 1_000_000\n"]}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared `ok: true/false` result vocabulary (Rust's `Result` in spirit,
3
+ * spread payloads in shape): every site that answers "did it work?" —
4
+ * detections, parses, validations, benchmark runs — uses one type
5
+ * family, one pair of constructors and one pair of guards instead of
6
+ * hand-rolling its own union.
7
+ *
8
+ * The payloads are spread onto the marker rather than carried in a
9
+ * `value`/`error` channel: `ok({ start, end })` is literally
10
+ * `{ ok: true, start, end }`. This keeps every existing consumer's
11
+ * field access (`r.start`, `r.code`, `r.failure`) and every `toEqual`
12
+ * assertion working unchanged, and lets the plugin-facing `{ ok: true }`
13
+ * literal stay the contract.
14
+ */
15
+ type Ok<TPayload extends object = object> = {
16
+ readonly ok: true;
17
+ } & TPayload;
18
+ type Err<TPayload extends object = object> = {
19
+ readonly ok: false;
20
+ } & TPayload;
21
+ type Result<TOk extends object = object, TErr extends object = object> = Ok<TOk> | Err<TErr>;
22
+ /**
23
+ * Build the success variant; `ok()` alone yields `{ ok: true }`. The
24
+ * cast is the constructor boundary: the runtime value is exactly
25
+ * `{ ok: true, ...payload }`, which the generic spread cannot prove.
26
+ */
27
+ declare function ok<TPayload extends object = object>(payload?: TPayload): Ok<TPayload>;
28
+ /**
29
+ * Build the failure variant; `err()` alone yields `{ ok: false }`. See
30
+ * {@link ok} for the constructor-boundary cast.
31
+ */
32
+ declare function err<TPayload extends object = object>(payload?: TPayload): Err<TPayload>;
33
+ /** Narrow a result to its success variant. */
34
+ declare function isOk<TOk extends object, TErr extends object>(result: Result<TOk, TErr>): result is Ok<TOk>;
35
+ /** Narrow a result to its failure variant. */
36
+ declare function isErr<TOk extends object, TErr extends object>(result: Result<TOk, TErr>): result is Err<TErr>;
37
+ /**
38
+ * Destructure a result through one of two handlers — the pattern-match
39
+ * combinator. Both handlers must produce `R`; the chosen one receives
40
+ * the spread payload of its variant.
41
+ */
42
+ declare function matchResult<TOk extends object, TErr extends object, R>(result: Result<TOk, TErr>, handlers: {
43
+ readonly ok: (payload: Ok<TOk>) => R;
44
+ readonly err: (payload: Err<TErr>) => R;
45
+ }): R;
46
+
47
+ export { type Err, type Ok, type Result, err, isErr, isOk, matchResult, ok };
package/dist/result.js ADDED
@@ -0,0 +1,20 @@
1
+ // src/result.ts
2
+ function ok(payload) {
3
+ return { ok: true, ...payload };
4
+ }
5
+ function err(payload) {
6
+ return { ok: false, ...payload };
7
+ }
8
+ function isOk(result) {
9
+ return result.ok === true;
10
+ }
11
+ function isErr(result) {
12
+ return result.ok === false;
13
+ }
14
+ function matchResult(result, handlers) {
15
+ return isOk(result) ? handlers.ok(result) : handlers.err(result);
16
+ }
17
+
18
+ export { err, isErr, isOk, matchResult, ok };
19
+ //# sourceMappingURL=result.js.map
20
+ //# sourceMappingURL=result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/result.ts"],"names":[],"mappings":";AA6BO,SAAS,GACf,OAAA,EACe;AACf,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,GAAG,OAAA,EAAQ;AAC/B;AAMO,SAAS,IACf,OAAA,EACgB;AAChB,EAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,GAAG,OAAA,EAAQ;AAChC;AAGO,SAAS,KACf,MAAA,EACoB;AACpB,EAAA,OAAO,OAAO,EAAA,KAAO,IAAA;AACtB;AAGO,SAAS,MACf,MAAA,EACsB;AACtB,EAAA,OAAO,OAAO,EAAA,KAAO,KAAA;AACtB;AAOO,SAAS,WAAA,CACf,QACA,QAAA,EAII;AACJ,EAAA,OAAO,IAAA,CAAK,MAAM,CAAA,GAAI,QAAA,CAAS,GAAG,MAAM,CAAA,GAAI,QAAA,CAAS,GAAA,CAAI,MAAM,CAAA;AAChE","file":"result.js","sourcesContent":["/**\n * Shared `ok: true/false` result vocabulary (Rust's `Result` in spirit,\n * spread payloads in shape): every site that answers \"did it work?\" —\n * detections, parses, validations, benchmark runs — uses one type\n * family, one pair of constructors and one pair of guards instead of\n * hand-rolling its own union.\n *\n * The payloads are spread onto the marker rather than carried in a\n * `value`/`error` channel: `ok({ start, end })` is literally\n * `{ ok: true, start, end }`. This keeps every existing consumer's\n * field access (`r.start`, `r.code`, `r.failure`) and every `toEqual`\n * assertion working unchanged, and lets the plugin-facing `{ ok: true }`\n * literal stay the contract.\n */\nexport type Ok<TPayload extends object = object> = {\n\treadonly ok: true\n} & TPayload\nexport type Err<TPayload extends object = object> = {\n\treadonly ok: false\n} & TPayload\nexport type Result<TOk extends object = object, TErr extends object = object> =\n\t| Ok<TOk>\n\t| Err<TErr>\n\n/**\n * Build the success variant; `ok()` alone yields `{ ok: true }`. The\n * cast is the constructor boundary: the runtime value is exactly\n * `{ ok: true, ...payload }`, which the generic spread cannot prove.\n */\nexport function ok<TPayload extends object = object>(\n\tpayload?: TPayload,\n): Ok<TPayload> {\n\treturn { ok: true, ...payload } as Ok<TPayload>\n}\n\n/**\n * Build the failure variant; `err()` alone yields `{ ok: false }`. See\n * {@link ok} for the constructor-boundary cast.\n */\nexport function err<TPayload extends object = object>(\n\tpayload?: TPayload,\n): Err<TPayload> {\n\treturn { ok: false, ...payload } as Err<TPayload>\n}\n\n/** Narrow a result to its success variant. */\nexport function isOk<TOk extends object, TErr extends object>(\n\tresult: Result<TOk, TErr>,\n): result is Ok<TOk> {\n\treturn result.ok === true\n}\n\n/** Narrow a result to its failure variant. */\nexport function isErr<TOk extends object, TErr extends object>(\n\tresult: Result<TOk, TErr>,\n): result is Err<TErr> {\n\treturn result.ok === false\n}\n\n/**\n * Destructure a result through one of two handlers — the pattern-match\n * combinator. Both handlers must produce `R`; the chosen one receives\n * the spread payload of its variant.\n */\nexport function matchResult<TOk extends object, TErr extends object, R>(\n\tresult: Result<TOk, TErr>,\n\thandlers: {\n\t\treadonly ok: (payload: Ok<TOk>) => R\n\t\treadonly err: (payload: Err<TErr>) => R\n\t},\n): R {\n\treturn isOk(result) ? handlers.ok(result) : handlers.err(result)\n}\n"]}
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ export { C as CoverKindUi, a as CoverKindUiMap, P as PluginManifest, b as PluginManifestId, c as PluginManifestUi, d as PluginPermissions, S as SearchKind, i as iconRef, l as localeString, p as pluginManifest, e as pluginManifestId, f as pluginManifestUi, g as pluginPermissions, s as searchKind } from './manifest-Dk6_xyNy.js';
3
+
4
+ /**
5
+ * The zod schema layer of the plugin contract: the manifest schema and
6
+ * the wire anchor envelope. Import this subpath
7
+ * (`@hoardodile/sdk-types/schema`) only where a runtime validator is
8
+ * actually needed — the host, the server, and tooling. The root entry
9
+ * re-exports the inferred types only, so plugin bundles never pull zod.
10
+ */
11
+
12
+ /**
13
+ * Wire/storage envelope for a message or danmaku anchor. Carries only
14
+ * the plugin-defined location payload in `data`; the host never
15
+ * interprets its contents. The anchor's resource is host state — the SDK
16
+ * injects it from the iframe's binding and the server derives it from
17
+ * the row's `anchor_resource_id` column — so plugins never see a resId
18
+ * here, and a plugin that sends one is rejected (strict).
19
+ *
20
+ * Plugin code works with the raw location data (`PluginSchema["anchor"]`)
21
+ * directly; the SDK wraps it into this envelope when it crosses the
22
+ * wire.
23
+ */
24
+ declare const anchorData: z.ZodObject<{
25
+ data: z.ZodOptional<z.ZodUnknown>;
26
+ }, z.core.$strict>;
27
+ type AnchorData = z.infer<typeof anchorData>;
28
+
29
+ export { type AnchorData, anchorData };
package/dist/schema.js ADDED
@@ -0,0 +1,124 @@
1
+ import { z } from 'zod';
2
+
3
+ // src/schema.ts
4
+ var pluginManifestId = z.string().uuid();
5
+ var pluginPermissions = z.object({
6
+ /** Read/write the resource's source metadata. */
7
+ sourceMeta: z.boolean().default(false),
8
+ /** Produce and store search metadata facets. */
9
+ searchMeta: z.boolean().default(false),
10
+ /** Create/list danmaku for resources this plugin renders. */
11
+ danmaku: z.boolean().default(false),
12
+ /** Create/list messages for resources this plugin renders. */
13
+ message: z.boolean().default(false),
14
+ /** Produce content hashes for duplicate detection / image similarity. */
15
+ imageHashes: z.boolean().default(false),
16
+ /**
17
+ * List and extract archive (zip/tar/7z/…) entries. The only API
18
+ * surface with a write side effect (the host's extraction cache), so
19
+ * it is denied by default.
20
+ */
21
+ container: z.boolean().default(false),
22
+ /**
23
+ * The plugin asset vault: user-consented downloads into the plugin's
24
+ * own `vault/` directory plus the vault read/delete methods. Denied
25
+ * by default — every download needs this capability AND the user's
26
+ * per-request approval.
27
+ */
28
+ download: z.boolean().default(false)
29
+ });
30
+ var localeString = z.record(z.string(), z.string());
31
+ var iconRef = z.string().min(1);
32
+ var templateValue = z.string();
33
+ var coverKindUi = z.object({
34
+ tl: z.array(templateValue).optional(),
35
+ tr: z.array(templateValue).optional(),
36
+ bl: z.array(templateValue).optional(),
37
+ br: z.array(templateValue).optional()
38
+ });
39
+ var coverKindUiMap = z.object({
40
+ image: coverKindUi.optional(),
41
+ video: coverKindUi.optional(),
42
+ audio: coverKindUi.optional(),
43
+ default: coverKindUi.optional()
44
+ });
45
+ var searchKind = z.object({
46
+ key: z.string().min(1),
47
+ /** i18n label key shown as the facet group's title. */
48
+ label: z.string().min(1),
49
+ /** Optional icon asset path in the plugin zip. */
50
+ icon: templateValue.optional()
51
+ });
52
+ var searchUi = z.object({
53
+ kinds: z.array(searchKind)
54
+ });
55
+ var messageUi = z.object({
56
+ /**
57
+ * Template string for message anchor chip labels. Rendered by the
58
+ * host's template engine. Supports `{{data.field}}`, `{{duration(ms)}}`,
59
+ * `{{inc(n)}}`, `{{t('key')}}`, etc.
60
+ */
61
+ anchor: z.string().min(1).optional()
62
+ });
63
+ var pluginManifestUi = z.object({
64
+ /**
65
+ * Preferred preview surface height (any CSS length, e.g. "85vh").
66
+ * Applied by both the resource detail page and the preview dialog.
67
+ */
68
+ height: z.string().min(1).optional(),
69
+ /**
70
+ * Preferred preview surface aspect ratio (e.g. "16/9"), capped by the
71
+ * host at 70vh. Intended for video-centric plugins; takes precedence
72
+ * over `height`. When neither is set the host falls back to 60vh.
73
+ */
74
+ aspect: z.string().min(1).optional(),
75
+ /**
76
+ * Cover template blocks per content kind. When present, the host
77
+ * renders the resource cover from the plugin's file templates
78
+ * instead of the built-in thumbnail pipeline.
79
+ */
80
+ card: coverKindUiMap.optional(),
81
+ /** Search facet kinds; enables the plugin's search integration. */
82
+ search: searchUi.optional(),
83
+ /**
84
+ * Anchor chip label template for messages; declares message-anchor
85
+ * support in the host UI.
86
+ */
87
+ message: messageUi.optional(),
88
+ /**
89
+ * Whether the plugin iframe inherits the host's app font (default true).
90
+ * Set to false for plugins that must render with their own fonts.
91
+ */
92
+ inheritFont: z.boolean().optional()
93
+ });
94
+ var pluginManifest = z.object({
95
+ id: pluginManifestId,
96
+ /** Display name shown in the plugins list and resource badges. */
97
+ name: z.string().min(1),
98
+ /** One-line description shown in the plugins list. */
99
+ description: z.string().min(1),
100
+ /** Icon asset path inside the plugin zip. */
101
+ icon: iconRef.optional(),
102
+ /** Semantic plugin version; shown to users on the plugin card. */
103
+ version: z.string().min(1),
104
+ /** Declared capabilities (see {@link pluginPermissions}). */
105
+ permissions: pluginPermissions,
106
+ /**
107
+ * Localized label tables: `{ labelKey: { locale: label } }`, e.g.
108
+ * `{ "cover.open": { "en": "Open", "zh-CN": "打开" } }` — labels are
109
+ * referenced from templates with `{{t('labelKey')}}` (see
110
+ * {@link localeString}).
111
+ */
112
+ i18n: z.record(z.string(), localeString).optional(),
113
+ /** UI preferences (see {@link pluginManifestUi}). */
114
+ ui: pluginManifestUi.optional()
115
+ });
116
+
117
+ // src/schema.ts
118
+ var anchorData = z.object({
119
+ data: z.unknown().optional()
120
+ }).strict();
121
+
122
+ export { anchorData, iconRef, localeString, pluginManifest, pluginManifestId, pluginManifestUi, pluginPermissions, searchKind };
123
+ //# sourceMappingURL=schema.js.map
124
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/manifest.ts","../src/schema.ts"],"names":["z"],"mappings":"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","file":"schema.js","sourcesContent":["import { z } from \"zod\"\n\n/**\n * Plugin manifest UUID (v4). Generated once when scaffolding a plugin\n * (e.g. `crypto.randomUUID()`) and never reused across plugins — the\n * server keys installed plugins by this id.\n */\nexport const pluginManifestId = z.string().uuid()\nexport type PluginManifestId = z.infer<typeof pluginManifestId>\n\n/**\n * Declared plugin capabilities. Each flag gates the corresponding API\n * surface: a plugin without `danmaku` gets no danmaku methods and the\n * host enforces the permission at the capability guard, so a manifest\n * that does not declare a capability cannot call it.\n */\nexport const pluginPermissions = z.object({\n\t/** Read/write the resource's source metadata. */\n\tsourceMeta: z.boolean().default(false),\n\t/** Produce and store search metadata facets. */\n\tsearchMeta: z.boolean().default(false),\n\t/** Create/list danmaku for resources this plugin renders. */\n\tdanmaku: z.boolean().default(false),\n\t/** Create/list messages for resources this plugin renders. */\n\tmessage: z.boolean().default(false),\n\t/** Produce content hashes for duplicate detection / image similarity. */\n\timageHashes: z.boolean().default(false),\n\t/**\n\t * List and extract archive (zip/tar/7z/…) entries. The only API\n\t * surface with a write side effect (the host's extraction cache), so\n\t * it is denied by default.\n\t */\n\tcontainer: z.boolean().default(false),\n\t/**\n\t * The plugin asset vault: user-consented downloads into the plugin's\n\t * own `vault/` directory plus the vault read/delete methods. Denied\n\t * by default — every download needs this capability AND the user's\n\t * per-request approval.\n\t */\n\tdownload: z.boolean().default(false),\n})\nexport type PluginPermissions = z.infer<typeof pluginPermissions>\n\n/**\n * Label key → locale table: `{ \"cover.open\": { \"en\": \"Open\", \"zh-CN\": \"打开\" } }`.\n * The host's template engine resolves `t('cover.open')` against the\n * resource's locale from this map.\n */\nexport const localeString = z.record(z.string(), z.string())\n\n/** Icon reference: an asset path inside the plugin zip (`assets/icon.svg`). */\nexport const iconRef = z.string().min(1)\n\n/**\n * Corner template slot: a string rendered by the host's template engine\n * over the resource scope. The engine supports `{{data.field}}` paths,\n * pipes (`bytes`, `duration`, `number`, `inc`), comparisons\n * (`eq`/`ne`/`gt`/`lt`/`gte`/`lte`), `if(cond, a, b)`, `join`,\n * `t('key')` for i18n, `icon('Icon')`, `asset('path')`,\n * `kind(...)`, and `searchKindIcons()` (the plugin's search kinds).\n * Unknown expressions render as the empty string.\n */\nconst templateValue = z.string()\n\n/**\n * Corner template slots for one content kind. Templates are rendered by\n * the host's template engine over the resource's file list; supported\n * directives include `{{data.field}}`, `{{duration(ms)}}`, `{{inc(n)}}`\n * and `{{t('key')}}`.\n */\nconst coverKindUi = z.object({\n\ttl: z.array(templateValue).optional(),\n\ttr: z.array(templateValue).optional(),\n\tbl: z.array(templateValue).optional(),\n\tbr: z.array(templateValue).optional(),\n})\n\n/**\n * Cover templates per content kind. A plugin declares the kinds it can\n * produce; the host picks the block matching the resource's cover type\n * (`image`/`video`/`audio`/`default`) and renders each corner as\n * specified, or falls back to the default cover when no block matches.\n */\nconst coverKindUiMap = z.object({\n\timage: coverKindUi.optional(),\n\tvideo: coverKindUi.optional(),\n\taudio: coverKindUi.optional(),\n\tdefault: coverKindUi.optional(),\n})\n\n/**\n * A search facet kind: a named dimension with an icon, rendered as a\n * facet group in the host's search UI. `key` becomes the facet key in\n * the search metadata the plugin produces.\n */\nexport const searchKind = z.object({\n\tkey: z.string().min(1),\n\t/** i18n label key shown as the facet group's title. */\n\tlabel: z.string().min(1),\n\t/** Optional icon asset path in the plugin zip. */\n\ticon: templateValue.optional(),\n})\nexport type SearchKind = z.infer<typeof searchKind>\n\nconst searchUi = z.object({\n\tkinds: z.array(searchKind),\n})\n\nconst messageUi = z.object({\n\t/**\n\t * Template string for message anchor chip labels. Rendered by the\n\t * host's template engine. Supports `{{data.field}}`, `{{duration(ms)}}`,\n\t * `{{inc(n)}}`, `{{t('key')}}`, etc.\n\t */\n\tanchor: z.string().min(1).optional(),\n})\n\n/**\n * Manifest-declared UI preferences. These shape how the host app\n * presents the plugin's iframe without the plugin shipping any host\n * integration code.\n */\nexport const pluginManifestUi = z.object({\n\t/**\n\t * Preferred preview surface height (any CSS length, e.g. \"85vh\").\n\t * Applied by both the resource detail page and the preview dialog.\n\t */\n\theight: z.string().min(1).optional(),\n\t/**\n\t * Preferred preview surface aspect ratio (e.g. \"16/9\"), capped by the\n\t * host at 70vh. Intended for video-centric plugins; takes precedence\n\t * over `height`. When neither is set the host falls back to 60vh.\n\t */\n\taspect: z.string().min(1).optional(),\n\t/**\n\t * Cover template blocks per content kind. When present, the host\n\t * renders the resource cover from the plugin's file templates\n\t * instead of the built-in thumbnail pipeline.\n\t */\n\tcard: coverKindUiMap.optional(),\n\t/** Search facet kinds; enables the plugin's search integration. */\n\tsearch: searchUi.optional(),\n\t/**\n\t * Anchor chip label template for messages; declares message-anchor\n\t * support in the host UI.\n\t */\n\tmessage: messageUi.optional(),\n\t/**\n\t * Whether the plugin iframe inherits the host's app font (default true).\n\t * Set to false for plugins that must render with their own fonts.\n\t */\n\tinheritFont: z.boolean().optional(),\n})\nexport type PluginManifestUi = z.infer<typeof pluginManifestUi>\nexport type CoverKindUi = z.infer<typeof coverKindUi>\nexport type CoverKindUiMap = z.infer<typeof coverKindUiMap>\n\n/**\n * The plugin manifest contract — the single schema validated everywhere\n * via its parse: the server at install time, the build CLI, and the\n * workbench. A manifest lives at the zip root of a built plugin next to\n * `main.js` and `index.html`.\n */\nexport const pluginManifest = z.object({\n\tid: pluginManifestId,\n\t/** Display name shown in the plugins list and resource badges. */\n\tname: z.string().min(1),\n\t/** One-line description shown in the plugins list. */\n\tdescription: z.string().min(1),\n\t/** Icon asset path inside the plugin zip. */\n\ticon: iconRef.optional(),\n\t/** Semantic plugin version; shown to users on the plugin card. */\n\tversion: z.string().min(1),\n\t/** Declared capabilities (see {@link pluginPermissions}). */\n\tpermissions: pluginPermissions,\n\t/**\n\t * Localized label tables: `{ labelKey: { locale: label } }`, e.g.\n\t * `{ \"cover.open\": { \"en\": \"Open\", \"zh-CN\": \"打开\" } }` — labels are\n\t * referenced from templates with `{{t('labelKey')}}` (see\n\t * {@link localeString}).\n\t */\n\ti18n: z.record(z.string(), localeString).optional(),\n\t/** UI preferences (see {@link pluginManifestUi}). */\n\tui: pluginManifestUi.optional(),\n})\nexport type PluginManifest = z.infer<typeof pluginManifest>\n","/**\n * The zod schema layer of the plugin contract: the manifest schema and\n * the wire anchor envelope. Import this subpath\n * (`@hoardodile/sdk-types/schema`) only where a runtime validator is\n * actually needed — the host, the server, and tooling. The root entry\n * re-exports the inferred types only, so plugin bundles never pull zod.\n */\nimport { z } from \"zod\"\n\nexport * from \"./manifest.ts\"\n\n/**\n * Wire/storage envelope for a message or danmaku anchor. Carries only\n * the plugin-defined location payload in `data`; the host never\n * interprets its contents. The anchor's resource is host state — the SDK\n * injects it from the iframe's binding and the server derives it from\n * the row's `anchor_resource_id` column — so plugins never see a resId\n * here, and a plugin that sends one is rejected (strict).\n *\n * Plugin code works with the raw location data (`PluginSchema[\"anchor\"]`)\n * directly; the SDK wraps it into this envelope when it crosses the\n * wire.\n */\nexport const anchorData = z\n\t.object({\n\t\tdata: z.unknown().optional(),\n\t})\n\t.strict()\nexport type AnchorData = z.infer<typeof anchorData>\n"]}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The host's cover/message template grammar: fragment splitting, the
3
+ * expression tokeniser and the recursive-descent parser. Pure logic
4
+ * with no DOM or React — shared verbatim by the web renderer
5
+ * (`apps/web/src/features/res/template/render.ts`) and the CLI's
6
+ * build-time template lint (`packages/cli`), so the linter can never
7
+ * drift from what the engine actually renders.
8
+ *
9
+ * The parser is deliberately lenient: a partial parse recovers and the
10
+ * evaluator renders what it can (bad expressions render as the empty
11
+ * string). Strictness belongs to the lint tooling, which inspects the
12
+ * tokens and the AST on top of this grammar.
13
+ */
14
+ type TemplateFragment = {
15
+ readonly kind: "text";
16
+ readonly value: string;
17
+ } | {
18
+ readonly kind: "expr";
19
+ readonly source: string;
20
+ };
21
+ /** Split a template into literal text and `{{...}}` expression fragments. */
22
+ declare function parseTemplateFragments(template: string): readonly TemplateFragment[];
23
+ type TemplateToken = {
24
+ readonly kind: "ident";
25
+ readonly value: string;
26
+ } | {
27
+ readonly kind: "dot";
28
+ } | {
29
+ readonly kind: "lparen";
30
+ } | {
31
+ readonly kind: "rparen";
32
+ } | {
33
+ readonly kind: "comma";
34
+ } | {
35
+ readonly kind: "string";
36
+ readonly value: string;
37
+ } | {
38
+ readonly kind: "eof";
39
+ };
40
+ /** Tokenise one expression body (the inside of `{{...}}`). */
41
+ declare function tokeniseExpression(source: string): TemplateToken[];
42
+ type TemplateExpr = {
43
+ readonly kind: "path";
44
+ readonly segments: readonly string[];
45
+ } | {
46
+ readonly kind: "call";
47
+ readonly name: string;
48
+ readonly args: readonly TemplateArg[];
49
+ };
50
+ type TemplateArg = {
51
+ readonly kind: "expr";
52
+ readonly expr: TemplateExpr;
53
+ } | {
54
+ readonly kind: "string";
55
+ readonly value: string;
56
+ };
57
+ /**
58
+ * Parse one expression body (`{{...}}` contents) into an AST. Returns
59
+ * `undefined` when the expression does not start with an identifier —
60
+ * a call or path head is required. Note the parser is lenient about
61
+ * the *tail*: unbalanced parentheses recover into a partial AST (the
62
+ * evaluator renders what it can); use {@link tokeniseExpression} and
63
+ * check paren balance yourself when strictness matters.
64
+ */
65
+ declare function parseTemplateExpression(source: string): TemplateExpr | undefined;
66
+
67
+ export { type TemplateArg, type TemplateExpr, type TemplateFragment, type TemplateToken, parseTemplateExpression, parseTemplateFragments, tokeniseExpression };
@@ -0,0 +1,137 @@
1
+ // src/template.ts
2
+ var TEMPLATE_RE = /\{\{(.*?)\}\}/g;
3
+ function parseTemplateFragments(template) {
4
+ const fragments = [];
5
+ let lastIndex = 0;
6
+ for (const match of template.matchAll(TEMPLATE_RE)) {
7
+ const start = match.index ?? 0;
8
+ if (start > lastIndex) {
9
+ fragments.push({ kind: "text", value: template.slice(lastIndex, start) });
10
+ }
11
+ fragments.push({ kind: "expr", source: match[1] ?? "" });
12
+ lastIndex = start + match[0].length;
13
+ }
14
+ if (lastIndex < template.length) {
15
+ fragments.push({ kind: "text", value: template.slice(lastIndex) });
16
+ }
17
+ return fragments;
18
+ }
19
+ function tokeniseExpression(source) {
20
+ const tokens = [];
21
+ let i = 0;
22
+ while (i < source.length) {
23
+ const ch = source[i];
24
+ if (/\s/.test(ch)) {
25
+ i++;
26
+ continue;
27
+ }
28
+ if (ch === ".") {
29
+ tokens.push({ kind: "dot" });
30
+ i++;
31
+ continue;
32
+ }
33
+ if (ch === "(") {
34
+ tokens.push({ kind: "lparen" });
35
+ i++;
36
+ continue;
37
+ }
38
+ if (ch === ")") {
39
+ tokens.push({ kind: "rparen" });
40
+ i++;
41
+ continue;
42
+ }
43
+ if (ch === ",") {
44
+ tokens.push({ kind: "comma" });
45
+ i++;
46
+ continue;
47
+ }
48
+ if (ch === "'") {
49
+ let j = i + 1;
50
+ while (j < source.length && source[j] !== "'") {
51
+ j++;
52
+ }
53
+ tokens.push({ kind: "string", value: source.slice(i + 1, j) });
54
+ i = j + 1;
55
+ continue;
56
+ }
57
+ if (/[A-Za-z0-9_]/.test(ch)) {
58
+ let j = i;
59
+ while (j < source.length && /[A-Za-z0-9_]/.test(source[j])) {
60
+ j++;
61
+ }
62
+ tokens.push({ kind: "ident", value: source.slice(i, j) });
63
+ i = j;
64
+ continue;
65
+ }
66
+ i++;
67
+ }
68
+ tokens.push({ kind: "eof" });
69
+ return tokens;
70
+ }
71
+ var Parser = class {
72
+ tokens;
73
+ pos = 0;
74
+ constructor(tokens) {
75
+ this.tokens = tokens;
76
+ }
77
+ peek() {
78
+ return this.tokens[this.pos] ?? { kind: "eof" };
79
+ }
80
+ advance() {
81
+ const t = this.tokens[this.pos];
82
+ this.pos++;
83
+ return t ?? { kind: "eof" };
84
+ }
85
+ };
86
+ function parseExpr(parser) {
87
+ const t = parser.peek();
88
+ if (t.kind !== "ident") return void 0;
89
+ parser.advance();
90
+ const next = parser.peek();
91
+ if (next.kind === "lparen") {
92
+ parser.advance();
93
+ const args = [];
94
+ if (parser.peek().kind !== "rparen") {
95
+ while (true) {
96
+ const arg = parseArg(parser);
97
+ if (arg === void 0) break;
98
+ args.push(arg);
99
+ if (parser.peek().kind === "comma") {
100
+ parser.advance();
101
+ continue;
102
+ }
103
+ break;
104
+ }
105
+ }
106
+ if (parser.peek().kind === "rparen") {
107
+ parser.advance();
108
+ }
109
+ return { kind: "call", name: t.value, args };
110
+ }
111
+ const segments = [t.value];
112
+ while (parser.peek().kind === "dot") {
113
+ parser.advance();
114
+ const seg = parser.peek();
115
+ if (seg.kind !== "ident") break;
116
+ parser.advance();
117
+ segments.push(seg.value);
118
+ }
119
+ return { kind: "path", segments };
120
+ }
121
+ function parseArg(parser) {
122
+ const t = parser.peek();
123
+ if (t.kind === "string") {
124
+ parser.advance();
125
+ return { kind: "string", value: t.value };
126
+ }
127
+ const expr = parseExpr(parser);
128
+ if (expr === void 0) return void 0;
129
+ return { kind: "expr", expr };
130
+ }
131
+ function parseTemplateExpression(source) {
132
+ return parseExpr(new Parser(tokeniseExpression(source)));
133
+ }
134
+
135
+ export { parseTemplateExpression, parseTemplateFragments, tokeniseExpression };
136
+ //# sourceMappingURL=template.js.map
137
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/template.ts"],"names":[],"mappings":"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","file":"template.js","sourcesContent":["/**\n * The host's cover/message template grammar: fragment splitting, the\n * expression tokeniser and the recursive-descent parser. Pure logic\n * with no DOM or React — shared verbatim by the web renderer\n * (`apps/web/src/features/res/template/render.ts`) and the CLI's\n * build-time template lint (`packages/cli`), so the linter can never\n * drift from what the engine actually renders.\n *\n * The parser is deliberately lenient: a partial parse recovers and the\n * evaluator renders what it can (bad expressions render as the empty\n * string). Strictness belongs to the lint tooling, which inspects the\n * tokens and the AST on top of this grammar.\n */\n\nconst TEMPLATE_RE = /\\{\\{(.*?)\\}\\}/g\n\nexport type TemplateFragment =\n\t| { readonly kind: \"text\"; readonly value: string }\n\t| { readonly kind: \"expr\"; readonly source: string }\n\n/** Split a template into literal text and `{{...}}` expression fragments. */\nexport function parseTemplateFragments(\n\ttemplate: string,\n): readonly TemplateFragment[] {\n\tconst fragments: TemplateFragment[] = []\n\tlet lastIndex = 0\n\tfor (const match of template.matchAll(TEMPLATE_RE)) {\n\t\tconst start = match.index ?? 0\n\t\tif (start > lastIndex) {\n\t\t\tfragments.push({ kind: \"text\", value: template.slice(lastIndex, start) })\n\t\t}\n\t\tfragments.push({ kind: \"expr\", source: match[1] ?? \"\" })\n\t\tlastIndex = start + match[0].length\n\t}\n\tif (lastIndex < template.length) {\n\t\tfragments.push({ kind: \"text\", value: template.slice(lastIndex) })\n\t}\n\treturn fragments\n}\n\nexport type TemplateToken =\n\t| { readonly kind: \"ident\"; readonly value: string }\n\t| { readonly kind: \"dot\" }\n\t| { readonly kind: \"lparen\" }\n\t| { readonly kind: \"rparen\" }\n\t| { readonly kind: \"comma\" }\n\t| { readonly kind: \"string\"; readonly value: string }\n\t| { readonly kind: \"eof\" }\n\n/** Tokenise one expression body (the inside of `{{...}}`). */\nexport function tokeniseExpression(source: string): TemplateToken[] {\n\tconst tokens: TemplateToken[] = []\n\tlet i = 0\n\twhile (i < source.length) {\n\t\tconst ch = source[i]!\n\t\tif (/\\s/.test(ch)) {\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\t\tif (ch === \".\") {\n\t\t\ttokens.push({ kind: \"dot\" })\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\t\tif (ch === \"(\") {\n\t\t\ttokens.push({ kind: \"lparen\" })\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\t\tif (ch === \")\") {\n\t\t\ttokens.push({ kind: \"rparen\" })\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\t\tif (ch === \",\") {\n\t\t\ttokens.push({ kind: \"comma\" })\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\t\tif (ch === \"'\") {\n\t\t\tlet j = i + 1\n\t\t\twhile (j < source.length && source[j] !== \"'\") {\n\t\t\t\tj++\n\t\t\t}\n\t\t\ttokens.push({ kind: \"string\", value: source.slice(i + 1, j) })\n\t\t\ti = j + 1\n\t\t\tcontinue\n\t\t}\n\t\tif (/[A-Za-z0-9_]/.test(ch)) {\n\t\t\tlet j = i\n\t\t\twhile (j < source.length && /[A-Za-z0-9_]/.test(source[j]!)) {\n\t\t\t\tj++\n\t\t\t}\n\t\t\ttokens.push({ kind: \"ident\", value: source.slice(i, j) })\n\t\t\ti = j\n\t\t\tcontinue\n\t\t}\n\t\t// Unrecognised character — skip; the evaluator renders the\n\t\t// resulting expression as the empty string.\n\t\ti++\n\t}\n\ttokens.push({ kind: \"eof\" })\n\treturn tokens\n}\n\nexport type TemplateExpr =\n\t| { readonly kind: \"path\"; readonly segments: readonly string[] }\n\t| {\n\t\t\treadonly kind: \"call\"\n\t\t\treadonly name: string\n\t\t\treadonly args: readonly TemplateArg[]\n\t }\n\nexport type TemplateArg =\n\t| { readonly kind: \"expr\"; readonly expr: TemplateExpr }\n\t| { readonly kind: \"string\"; readonly value: string }\n\nclass Parser {\n\treadonly tokens: TemplateToken[]\n\tpos = 0\n\tconstructor(tokens: TemplateToken[]) {\n\t\tthis.tokens = tokens\n\t}\n\n\tpeek(): TemplateToken {\n\t\treturn this.tokens[this.pos] ?? { kind: \"eof\" }\n\t}\n\n\tadvance(): TemplateToken {\n\t\tconst t = this.tokens[this.pos]\n\t\tthis.pos++\n\t\treturn t ?? { kind: \"eof\" }\n\t}\n}\n\nfunction parseExpr(parser: Parser): TemplateExpr | undefined {\n\tconst t = parser.peek()\n\tif (t.kind !== \"ident\") return undefined\n\tparser.advance()\n\n\tconst next = parser.peek()\n\tif (next.kind === \"lparen\") {\n\t\t// call\n\t\tparser.advance() // consume (\n\t\tconst args: TemplateArg[] = []\n\t\tif (parser.peek().kind !== \"rparen\") {\n\t\t\twhile (true) {\n\t\t\t\tconst arg = parseArg(parser)\n\t\t\t\tif (arg === undefined) break\n\t\t\t\targs.push(arg)\n\t\t\t\tif (parser.peek().kind === \"comma\") {\n\t\t\t\t\tparser.advance()\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif (parser.peek().kind === \"rparen\") {\n\t\t\tparser.advance()\n\t\t}\n\t\treturn { kind: \"call\", name: t.value, args }\n\t}\n\n\t// path\n\tconst segments = [t.value]\n\twhile (parser.peek().kind === \"dot\") {\n\t\tparser.advance()\n\t\tconst seg = parser.peek()\n\t\tif (seg.kind !== \"ident\") break\n\t\tparser.advance()\n\t\tsegments.push(seg.value)\n\t}\n\treturn { kind: \"path\", segments }\n}\n\nfunction parseArg(parser: Parser): TemplateArg | undefined {\n\tconst t = parser.peek()\n\tif (t.kind === \"string\") {\n\t\tparser.advance()\n\t\treturn { kind: \"string\", value: t.value }\n\t}\n\tconst expr = parseExpr(parser)\n\tif (expr === undefined) return undefined\n\treturn { kind: \"expr\", expr }\n}\n\n/**\n * Parse one expression body (`{{...}}` contents) into an AST. Returns\n * `undefined` when the expression does not start with an identifier —\n * a call or path head is required. Note the parser is lenient about\n * the *tail*: unbalanced parentheses recover into a partial AST (the\n * evaluator renders what it can); use {@link tokeniseExpression} and\n * check paren balance yourself when strictness matters.\n */\nexport function parseTemplateExpression(\n\tsource: string,\n): TemplateExpr | undefined {\n\treturn parseExpr(new Parser(tokeniseExpression(source)))\n}\n"]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Plugin-facing text-length limits: the input caps plugins that render
3
+ * composers need. App-side field limits live in
4
+ * `@hoardodile/schemas/text-limits` instead.
5
+ */
6
+ /** Danmaku body cap enforced by the host. */
7
+ declare const MAX_DANMAKU_TEXT_LENGTH = 100;
8
+ /** Comment/message body cap enforced by the host. */
9
+ declare const MAX_COMMENT_BODY_LENGTH = 10000;
10
+
11
+ export { MAX_COMMENT_BODY_LENGTH, MAX_DANMAKU_TEXT_LENGTH };
@@ -0,0 +1,7 @@
1
+ // src/text-limits.ts
2
+ var MAX_DANMAKU_TEXT_LENGTH = 100;
3
+ var MAX_COMMENT_BODY_LENGTH = 1e4;
4
+
5
+ export { MAX_COMMENT_BODY_LENGTH, MAX_DANMAKU_TEXT_LENGTH };
6
+ //# sourceMappingURL=text-limits.js.map
7
+ //# sourceMappingURL=text-limits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/text-limits.ts"],"names":[],"mappings":";AAOO,IAAM,uBAAA,GAA0B;AAGhC,IAAM,uBAAA,GAA0B","file":"text-limits.js","sourcesContent":["/**\n * Plugin-facing text-length limits: the input caps plugins that render\n * composers need. App-side field limits live in\n * `@hoardodile/schemas/text-limits` instead.\n */\n\n/** Danmaku body cap enforced by the host. */\nexport const MAX_DANMAKU_TEXT_LENGTH = 100\n\n/** Comment/message body cap enforced by the host. */\nexport const MAX_COMMENT_BODY_LENGTH = 10_000\n"]}