@templatical/template-tools 0.45.0 → 0.46.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.
- package/dist/bin.js +1 -1
- package/dist/index.js +1 -1
- package/dist/live/index.d.ts +1 -1
- package/dist/live/index.js +1 -1
- package/dist/live/index.js.map +1 -1
- package/dist/{src-GcX5tdei.js → src-De5svMDM.js} +2 -2
- package/dist/{src-GcX5tdei.js.map → src-De5svMDM.js.map} +1 -1
- package/package.json +12 -12
- package/schema.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as validateTemplate, i as schema, r as runQualityLint, t as applyOperation } from "./src-
|
|
2
|
+
import { a as validateTemplate, i as schema, r as runQualityLint, t as applyOperation } from "./src-De5svMDM.js";
|
|
3
3
|
import { DEFAULT_PORT, WORKING_DIR, listWorkingFiles, openBrowser, pidfilePath, processAlive, readPidfile, readWorkingFile, startBridgePreferring } from "./live/index.js";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as applyOperation, n as getColumnCount, r as runQualityLint, i as schema, a as validateTemplate } from "./src-
|
|
1
|
+
export { t as applyOperation, n as getColumnCount, r as runQualityLint, i as schema, a as validateTemplate } from "./src-De5svMDM.js";
|
package/dist/live/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Server } from "node:http";
|
|
2
2
|
//#region src/live/index.d.ts
|
|
3
|
-
export declare const EDITOR_VERSION = "0.
|
|
3
|
+
export declare const EDITOR_VERSION = "0.46.0";
|
|
4
4
|
export declare const DEFAULT_PORT = 4747;
|
|
5
5
|
export declare const WORKING_DIR = ".templatical";
|
|
6
6
|
export declare const PID_FILE: string;
|
package/dist/live/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
//#region src/live/index.ts
|
|
7
7
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const EDITOR_VERSION = "0.
|
|
8
|
+
const EDITOR_VERSION = "0.46.0";
|
|
9
9
|
const DEFAULT_PORT = 4747;
|
|
10
10
|
const WORKING_DIR = ".templatical";
|
|
11
11
|
const PID_FILE = join(WORKING_DIR, "live-server.pid");
|
package/dist/live/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/live/index.ts"],"sourcesContent":["// The local live-preview bridge.\n//\n// It serves live/index.html (which loads the REAL Templatical editor from the\n// CDN), streams agent-driven template updates to the page over Server-Sent\n// Events, and buffers the page's hand-edits so the caller can detect divergence\n// (a user edit in the browser) before overwriting.\n//\n// This package's own `live` CLI command drives it today, over localhost HTTP.\n// It stays side-effect free at import time and writes nothing to stdout so\n// that a future stdio-based caller — an MCP server reserving stdout for\n// JSON-RPC — can be added without auditing this module first: a stray write\n// here would corrupt that protocol. Keep all human-facing output in the\n// callers (cli/output.ts is the CLI's own single writer;\n// tests/cli-output-discipline.test.ts enforces that no other module under\n// src/ touches stdout).\n\nimport { createServer, type Server, type ServerResponse } from \"node:http\";\nimport { spawn } from \"node:child_process\";\nimport { existsSync, readFileSync, readdirSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst here = dirname(fileURLToPath(import.meta.url));\n\n// The CDN editor version the live harness loads. It is PINNED to this package's\n// schema version: schema.json is generated from @templatical/types, and types +\n// editor bump in lockstep (changesets fixed group), so the editor at this\n// version has the same block model the schema describes.\n//\n// Kept in step automatically by scripts/sync-pins.mjs at release time.\n// tests/cdn-pin.test.ts is the safety net that fails CI if it ever drifts.\nexport const EDITOR_VERSION = \"0.45.0\";\n\nexport const DEFAULT_PORT = 4747;\nexport const WORKING_DIR = \".templatical\";\nexport const PID_FILE = join(WORKING_DIR, \"live-server.pid\");\n\n// Resolved from this module rather than the cwd so it works identically from\n// src/live/ (tests) and dist/live/ (published) — both sit two levels under the\n// package root, where live/index.html ships.\nconst HARNESS_FILE = resolve(here, \"../../live/index.html\");\n\n// --------------------------------------------------------------------------\n// Pure helpers\n// --------------------------------------------------------------------------\n\n/** Order-insensitive structural equality for parsed-JSON template content. */\nexport function deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== \"object\" || typeof b !== \"object\") return false;\n const aArr = Array.isArray(a);\n if (aArr !== Array.isArray(b)) return false;\n if (aArr) {\n const aList = a as unknown[];\n const bList = b as unknown[];\n if (aList.length !== bList.length) return false;\n for (let i = 0; i < aList.length; i++) {\n if (!deepEqual(aList[i], bList[i])) return false;\n }\n return true;\n }\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const ak = Object.keys(aObj);\n const bk = Object.keys(bObj);\n if (ak.length !== bk.length) return false;\n for (const k of ak) {\n if (!Object.prototype.hasOwnProperty.call(bObj, k)) return false;\n if (!deepEqual(aObj[k], bObj[k])) return false;\n }\n return true;\n}\n\n/** Read + parse the working file, or return null when it is absent/unparseable. */\nexport function readWorkingFile(path: string): unknown {\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf8\"));\n } catch {\n return null;\n }\n}\n\n/** List the working templates in a project's `.templatical/` folder, by name. */\nexport function listWorkingFiles(cwd: string): string[] {\n const dir = resolve(cwd, WORKING_DIR);\n if (!existsSync(dir)) return [];\n return readdirSync(dir)\n .filter((f) => f.endsWith(\".json\"))\n .sort();\n}\n\nasync function readJsonBody(req: NodeJS.ReadableStream): Promise<\n | {\n content?: unknown;\n baseline?: boolean;\n blockId?: string;\n parentBlockId?: unknown;\n blockType?: unknown;\n label?: unknown;\n text?: string;\n consumeAnnotations?: boolean;\n }\n | undefined\n> {\n const chunks: Buffer[] = [];\n for await (const chunk of req) chunks.push(chunk as Buffer);\n const raw = Buffer.concat(chunks).toString(\"utf8\");\n return raw ? JSON.parse(raw) : undefined;\n}\n\n/**\n * Open a URL in the user's default browser — best-effort and cross-platform.\n * A silent no-op where there's no browser (headless / CI / sandbox); callers\n * always surface the URL too, so nothing is lost when this does nothing.\n */\nexport function openBrowser(url: string): void {\n const [cmd, cmdArgs]: [string, string[]] =\n process.platform === \"darwin\"\n ? [\"open\", [url]]\n : process.platform === \"win32\"\n ? [\"cmd\", [\"/c\", \"start\", \"\", url]]\n : [\"xdg-open\", [url]];\n try {\n const child = spawn(cmd, cmdArgs, { stdio: \"ignore\", detached: true });\n child.on(\"error\", () => {}); // no browser available — ignore\n child.unref();\n } catch {\n /* ignore — the URL is surfaced by the caller regardless */\n }\n}\n\n// --------------------------------------------------------------------------\n// Pidfile helpers (used by this package's own `live` CLI command; a future\n// stdio-based MCP server would own its process lifecycle directly and would\n// not need them)\n// --------------------------------------------------------------------------\n\nexport interface PidfileInfo {\n pid: number;\n port: number;\n}\n\nexport function pidfilePath(cwd: string): string {\n return resolve(cwd, PID_FILE);\n}\n\nexport function readPidfile(cwd: string): PidfileInfo | null {\n const p = pidfilePath(cwd);\n if (!existsSync(p)) return null;\n try {\n return JSON.parse(readFileSync(p, \"utf8\")) as PidfileInfo;\n } catch {\n return null;\n }\n}\n\nexport function processAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * A note the user left in the browser, against a block or the template as a\n * whole. Delivered on GET /content - the call the agent already makes before\n * every change - so notes need no polling and no separate endpoint to drain.\n */\nexport interface Annotation {\n id: string;\n /** null for a note about the template rather than one block. */\n blockId: string | null;\n /** The next data-block-id above the target. null at the top level and for a template note. */\n parentBlockId: string | null;\n blockType: string | null;\n /** Snapshot of the block's text at save time, already truncated. */\n label: string;\n text: string;\n createdAt: number;\n}\n\nconst NOTE_TEXT_MAX = 2000;\nconst NOTE_LABEL_MAX = 80;\nconst NOTE_BLOCK_TYPE_MAX = 40;\nconst NOTE_QUEUE_MAX = 40;\n\nfunction readNoteText(value: unknown): { text: string } | { error: string } {\n const text = typeof value === \"string\" ? value.trim() : \"\";\n if (!text) return { error: \"An annotation needs non-empty `text`.\" };\n if (text.length > NOTE_TEXT_MAX) {\n return { error: \"A note can be at most 2000 characters.\" };\n }\n return { text };\n}\n\nfunction readOptionalId(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction readBlockType(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n return value.trim().slice(0, NOTE_BLOCK_TYPE_MAX);\n}\n\nfunction readLabel(value: unknown): string {\n if (typeof value !== \"string\") return \"\";\n return value.replace(/\\s+/g, \" \").trim().slice(0, NOTE_LABEL_MAX);\n}\n\nfunction annotationIdFromPath(pathname: string): string | null {\n const prefix = \"/annotations/\";\n if (!pathname.startsWith(prefix)) return null;\n const raw = pathname.slice(prefix.length);\n if (!raw || raw.includes(\"/\")) return null;\n try {\n return decodeURIComponent(raw);\n } catch {\n return null;\n }\n}\n\nfunction noteNotFound(res: ServerResponse): void {\n res.writeHead(404, { \"content-type\": \"text/plain\" });\n res.end(\"No note with that id.\");\n}\n\n// --------------------------------------------------------------------------\n// Bridge server\n// --------------------------------------------------------------------------\n\nexport interface BridgeHandle {\n server: Server;\n port: number;\n url: string;\n /** The absolute path of the working file this bridge is serving. */\n workingPath: string;\n /** The page's latest state, for callers that drive the bridge in-process. */\n getEditorState: () => {\n divergent: boolean;\n content: unknown;\n annotations: Annotation[];\n };\n /** Re-read the working file and push it to every connected page. */\n reload: (options?: { consumeAnnotations?: boolean }) => {\n ok: boolean;\n clients: number;\n consumed: boolean;\n };\n close: () => Promise<void>;\n}\n\nexport interface StartBridgeOptions {\n cwd?: string;\n port?: number;\n file?: string;\n harnessFile?: string;\n}\n\n/**\n * Start the bridge HTTP server. Pure server + in-memory sync state; callers own\n * process lifecycle. Resolves once listening.\n *\n * The returned handle exposes `getEditorState` and `reload` directly, so an\n * in-process caller (the MCP server) never has to make an HTTP request to its\n * own bridge — that is the whole reason live mode can work inside a sandboxed\n * agent. The equivalent HTTP endpoints stay for the skill's out-of-process CLI.\n */\nexport function startBridge({\n cwd = process.cwd(),\n port = 0,\n file = join(WORKING_DIR, \"template.json\"),\n harnessFile = HARNESS_FILE,\n}: StartBridgeOptions = {}): Promise<BridgeHandle> {\n const workingPath = isAbsolute(file) ? file : resolve(cwd, file);\n\n // Sync state. `baseline` is the editor's NORMALIZED view of the caller's last\n // content (captured by the page right after it applies an update), so the\n // editor's own load-time normalization never reads as a user edit.\n // `editorCurrent` is the page's latest getContent(); `divergent` is true when\n // it structurally differs from `baseline` — i.e. the user hand-edited.\n const state: {\n baseline: unknown;\n editorCurrent: unknown;\n divergent: boolean;\n annotations: Annotation[];\n } = {\n baseline: null,\n editorCurrent: null,\n divergent: false,\n annotations: [],\n };\n let nextAnnotationNumber = 1;\n function mintAnnotationId(): string {\n const n = nextAnnotationNumber++;\n return `a${n}-${Date.now().toString(36)}`;\n }\n const clients = new Set<ServerResponse>();\n\n function broadcastTemplate(content: unknown): void {\n const payload = `event: template\\ndata: ${JSON.stringify(content)}\\n\\n`;\n for (const res of clients) res.write(payload);\n }\n\n function getEditorState(): {\n divergent: boolean;\n content: unknown;\n annotations: Annotation[];\n } {\n return {\n divergent: state.divergent,\n content: state.editorCurrent,\n annotations: state.annotations,\n };\n }\n\n function reload(options?: { consumeAnnotations?: boolean }): {\n ok: boolean;\n clients: number;\n consumed: boolean;\n } {\n // The freshly-written file is the new baseline, so any pending user edit is\n // superseded. Notes stay: a plain reload is an ordinary edit, and clearing\n // them here would drop a queue the agent has not applied. consumeAnnotations\n // clears only after the file was actually read — clearing first would drop\n // the notes and push nothing when the file is missing.\n const content = readWorkingFile(workingPath);\n state.baseline = null;\n state.editorCurrent = null;\n state.divergent = false;\n const consumed = options?.consumeAnnotations === true && content !== null;\n if (consumed) state.annotations = [];\n if (content !== null) broadcastTemplate(content);\n return { ok: true, clients: clients.size, consumed };\n }\n\n const server = createServer(async (req, res) => {\n const { pathname } = new URL(req.url ?? \"/\", \"http://localhost\");\n const method = req.method ?? \"GET\";\n\n try {\n if (\n method === \"GET\" &&\n (pathname === \"/\" || pathname === \"/index.html\")\n ) {\n const html = readFileSync(harnessFile, \"utf8\").replaceAll(\n \"{{EDITOR_VERSION}}\",\n EDITOR_VERSION,\n );\n res.writeHead(200, { \"content-type\": \"text/html; charset=utf-8\" });\n res.end(html);\n return;\n }\n\n if (method === \"GET\" && pathname === \"/template\") {\n const content = readWorkingFile(workingPath);\n if (content === null) {\n res.writeHead(204).end(); // no working file yet — page inits empty\n return;\n }\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(content));\n return;\n }\n\n if (method === \"GET\" && pathname === \"/events\") {\n res.writeHead(200, {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache\",\n connection: \"keep-alive\",\n });\n res.write(\"event: ready\\ndata: {}\\n\\n\");\n clients.add(res);\n const ping = setInterval(() => res.write(\": ping\\n\\n\"), 15000);\n req.on(\"close\", () => {\n clearInterval(ping);\n clients.delete(res);\n });\n return;\n }\n\n if (method === \"POST\" && pathname === \"/content\") {\n const body = await readJsonBody(req);\n const content = body?.content;\n if (body?.baseline) {\n // Post-apply snapshot: this is the editor's normalized view of the\n // caller's content, not a user edit.\n state.baseline = content ?? null;\n state.editorCurrent = content ?? null;\n state.divergent = false;\n } else {\n state.editorCurrent = content ?? null;\n state.divergent =\n state.baseline !== null && !deepEqual(content, state.baseline);\n }\n res.writeHead(204).end();\n return;\n }\n\n if (method === \"GET\" && pathname === \"/content\") {\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(getEditorState()));\n return;\n }\n\n if (method === \"POST\" && pathname === \"/annotations\") {\n const body = await readJsonBody(req);\n const parsed = readNoteText(body?.text);\n if (\"error\" in parsed) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(parsed.error);\n return;\n }\n if (state.annotations.length >= NOTE_QUEUE_MAX) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"The note queue is full (40).\");\n return;\n }\n const blockId = readOptionalId(body?.blockId);\n let parentBlockId = readOptionalId(body?.parentBlockId);\n if (parentBlockId === blockId) parentBlockId = null;\n const annotation: Annotation = {\n id: mintAnnotationId(),\n blockId,\n parentBlockId,\n blockType: readBlockType(body?.blockType),\n label: readLabel(body?.label),\n text: parsed.text,\n createdAt: Date.now(),\n };\n state.annotations.push(annotation);\n res.writeHead(201, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(annotation));\n return;\n }\n\n const noteId = annotationIdFromPath(pathname);\n if (noteId && (method === \"PUT\" || method === \"DELETE\")) {\n const index = state.annotations.findIndex((note) => note.id === noteId);\n if (index < 0) {\n noteNotFound(res);\n return;\n }\n if (method === \"DELETE\") {\n state.annotations.splice(index, 1);\n res.writeHead(204).end();\n return;\n }\n const body = await readJsonBody(req);\n const parsed = readNoteText(body?.text);\n if (\"error\" in parsed) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(parsed.error);\n return;\n }\n const updated: Annotation = {\n ...state.annotations[index],\n text: parsed.text,\n };\n state.annotations[index] = updated;\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(updated));\n return;\n }\n\n if (method === \"POST\" && pathname === \"/reload\") {\n const body = await readJsonBody(req);\n const result = reload({\n consumeAnnotations: body?.consumeAnnotations === true,\n });\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(result));\n return;\n }\n\n res.writeHead(404, { \"content-type\": \"text/plain\" }).end(\"Not found\");\n } catch (err) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(`Bad request: ${(err as Error).message}`);\n }\n });\n\n return new Promise((resolvePromise, rejectPromise) => {\n // listen() reports failures (e.g. EADDRINUSE) as an 'error' event, not a\n // throw — without this the error would be unhandled and crash the process.\n const onListenError = (err: Error) => rejectPromise(err);\n server.once(\"error\", onListenError);\n server.listen(port, \"127.0.0.1\", () => {\n server.removeListener(\"error\", onListenError);\n const address = server.address();\n const actualPort =\n typeof address === \"object\" && address !== null ? address.port : port;\n resolvePromise({\n server,\n port: actualPort,\n url: `http://localhost:${actualPort}/`,\n workingPath,\n getEditorState,\n reload,\n close: () =>\n new Promise<void>((r) => {\n for (const c of clients) c.end();\n clients.clear();\n server.close(() => r());\n }),\n });\n });\n });\n}\n\nexport interface PreferringHandle extends BridgeHandle {\n fellBack: boolean;\n preferredPort?: number;\n}\n\n/**\n * Start the bridge on `preferredPort`, falling back to an OS-assigned free port\n * when that one is occupied — so a busy port never fails the launch. The page\n * uses relative URLs, so the actual port is discovered, not assumed. The\n * returned handle carries `fellBack: true` when it landed on a different port.\n */\nexport async function startBridgePreferring({\n preferredPort = DEFAULT_PORT,\n ...opts\n}: StartBridgeOptions & {\n preferredPort?: number;\n} = {}): Promise<PreferringHandle> {\n try {\n const handle = await startBridge({ ...opts, port: preferredPort });\n return { ...handle, fellBack: false };\n } catch (err) {\n if ((err as NodeJS.ErrnoException)?.code !== \"EADDRINUSE\") throw err;\n const handle = await startBridge({ ...opts, port: 0 });\n return { ...handle, fellBack: true, preferredPort };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/live/index.ts"],"sourcesContent":["// The local live-preview bridge.\n//\n// It serves live/index.html (which loads the REAL Templatical editor from the\n// CDN), streams agent-driven template updates to the page over Server-Sent\n// Events, and buffers the page's hand-edits so the caller can detect divergence\n// (a user edit in the browser) before overwriting.\n//\n// This package's own `live` CLI command drives it today, over localhost HTTP.\n// It stays side-effect free at import time and writes nothing to stdout so\n// that a future stdio-based caller — an MCP server reserving stdout for\n// JSON-RPC — can be added without auditing this module first: a stray write\n// here would corrupt that protocol. Keep all human-facing output in the\n// callers (cli/output.ts is the CLI's own single writer;\n// tests/cli-output-discipline.test.ts enforces that no other module under\n// src/ touches stdout).\n\nimport { createServer, type Server, type ServerResponse } from \"node:http\";\nimport { spawn } from \"node:child_process\";\nimport { existsSync, readFileSync, readdirSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst here = dirname(fileURLToPath(import.meta.url));\n\n// The CDN editor version the live harness loads. It is PINNED to this package's\n// schema version: schema.json is generated from @templatical/types, and types +\n// editor bump in lockstep (changesets fixed group), so the editor at this\n// version has the same block model the schema describes.\n//\n// Kept in step automatically by scripts/sync-pins.mjs at release time.\n// tests/cdn-pin.test.ts is the safety net that fails CI if it ever drifts.\nexport const EDITOR_VERSION = \"0.46.0\";\n\nexport const DEFAULT_PORT = 4747;\nexport const WORKING_DIR = \".templatical\";\nexport const PID_FILE = join(WORKING_DIR, \"live-server.pid\");\n\n// Resolved from this module rather than the cwd so it works identically from\n// src/live/ (tests) and dist/live/ (published) — both sit two levels under the\n// package root, where live/index.html ships.\nconst HARNESS_FILE = resolve(here, \"../../live/index.html\");\n\n// --------------------------------------------------------------------------\n// Pure helpers\n// --------------------------------------------------------------------------\n\n/** Order-insensitive structural equality for parsed-JSON template content. */\nexport function deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== \"object\" || typeof b !== \"object\") return false;\n const aArr = Array.isArray(a);\n if (aArr !== Array.isArray(b)) return false;\n if (aArr) {\n const aList = a as unknown[];\n const bList = b as unknown[];\n if (aList.length !== bList.length) return false;\n for (let i = 0; i < aList.length; i++) {\n if (!deepEqual(aList[i], bList[i])) return false;\n }\n return true;\n }\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const ak = Object.keys(aObj);\n const bk = Object.keys(bObj);\n if (ak.length !== bk.length) return false;\n for (const k of ak) {\n if (!Object.prototype.hasOwnProperty.call(bObj, k)) return false;\n if (!deepEqual(aObj[k], bObj[k])) return false;\n }\n return true;\n}\n\n/** Read + parse the working file, or return null when it is absent/unparseable. */\nexport function readWorkingFile(path: string): unknown {\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf8\"));\n } catch {\n return null;\n }\n}\n\n/** List the working templates in a project's `.templatical/` folder, by name. */\nexport function listWorkingFiles(cwd: string): string[] {\n const dir = resolve(cwd, WORKING_DIR);\n if (!existsSync(dir)) return [];\n return readdirSync(dir)\n .filter((f) => f.endsWith(\".json\"))\n .sort();\n}\n\nasync function readJsonBody(req: NodeJS.ReadableStream): Promise<\n | {\n content?: unknown;\n baseline?: boolean;\n blockId?: string;\n parentBlockId?: unknown;\n blockType?: unknown;\n label?: unknown;\n text?: string;\n consumeAnnotations?: boolean;\n }\n | undefined\n> {\n const chunks: Buffer[] = [];\n for await (const chunk of req) chunks.push(chunk as Buffer);\n const raw = Buffer.concat(chunks).toString(\"utf8\");\n return raw ? JSON.parse(raw) : undefined;\n}\n\n/**\n * Open a URL in the user's default browser — best-effort and cross-platform.\n * A silent no-op where there's no browser (headless / CI / sandbox); callers\n * always surface the URL too, so nothing is lost when this does nothing.\n */\nexport function openBrowser(url: string): void {\n const [cmd, cmdArgs]: [string, string[]] =\n process.platform === \"darwin\"\n ? [\"open\", [url]]\n : process.platform === \"win32\"\n ? [\"cmd\", [\"/c\", \"start\", \"\", url]]\n : [\"xdg-open\", [url]];\n try {\n const child = spawn(cmd, cmdArgs, { stdio: \"ignore\", detached: true });\n child.on(\"error\", () => {}); // no browser available — ignore\n child.unref();\n } catch {\n /* ignore — the URL is surfaced by the caller regardless */\n }\n}\n\n// --------------------------------------------------------------------------\n// Pidfile helpers (used by this package's own `live` CLI command; a future\n// stdio-based MCP server would own its process lifecycle directly and would\n// not need them)\n// --------------------------------------------------------------------------\n\nexport interface PidfileInfo {\n pid: number;\n port: number;\n}\n\nexport function pidfilePath(cwd: string): string {\n return resolve(cwd, PID_FILE);\n}\n\nexport function readPidfile(cwd: string): PidfileInfo | null {\n const p = pidfilePath(cwd);\n if (!existsSync(p)) return null;\n try {\n return JSON.parse(readFileSync(p, \"utf8\")) as PidfileInfo;\n } catch {\n return null;\n }\n}\n\nexport function processAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * A note the user left in the browser, against a block or the template as a\n * whole. Delivered on GET /content - the call the agent already makes before\n * every change - so notes need no polling and no separate endpoint to drain.\n */\nexport interface Annotation {\n id: string;\n /** null for a note about the template rather than one block. */\n blockId: string | null;\n /** The next data-block-id above the target. null at the top level and for a template note. */\n parentBlockId: string | null;\n blockType: string | null;\n /** Snapshot of the block's text at save time, already truncated. */\n label: string;\n text: string;\n createdAt: number;\n}\n\nconst NOTE_TEXT_MAX = 2000;\nconst NOTE_LABEL_MAX = 80;\nconst NOTE_BLOCK_TYPE_MAX = 40;\nconst NOTE_QUEUE_MAX = 40;\n\nfunction readNoteText(value: unknown): { text: string } | { error: string } {\n const text = typeof value === \"string\" ? value.trim() : \"\";\n if (!text) return { error: \"An annotation needs non-empty `text`.\" };\n if (text.length > NOTE_TEXT_MAX) {\n return { error: \"A note can be at most 2000 characters.\" };\n }\n return { text };\n}\n\nfunction readOptionalId(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction readBlockType(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n return value.trim().slice(0, NOTE_BLOCK_TYPE_MAX);\n}\n\nfunction readLabel(value: unknown): string {\n if (typeof value !== \"string\") return \"\";\n return value.replace(/\\s+/g, \" \").trim().slice(0, NOTE_LABEL_MAX);\n}\n\nfunction annotationIdFromPath(pathname: string): string | null {\n const prefix = \"/annotations/\";\n if (!pathname.startsWith(prefix)) return null;\n const raw = pathname.slice(prefix.length);\n if (!raw || raw.includes(\"/\")) return null;\n try {\n return decodeURIComponent(raw);\n } catch {\n return null;\n }\n}\n\nfunction noteNotFound(res: ServerResponse): void {\n res.writeHead(404, { \"content-type\": \"text/plain\" });\n res.end(\"No note with that id.\");\n}\n\n// --------------------------------------------------------------------------\n// Bridge server\n// --------------------------------------------------------------------------\n\nexport interface BridgeHandle {\n server: Server;\n port: number;\n url: string;\n /** The absolute path of the working file this bridge is serving. */\n workingPath: string;\n /** The page's latest state, for callers that drive the bridge in-process. */\n getEditorState: () => {\n divergent: boolean;\n content: unknown;\n annotations: Annotation[];\n };\n /** Re-read the working file and push it to every connected page. */\n reload: (options?: { consumeAnnotations?: boolean }) => {\n ok: boolean;\n clients: number;\n consumed: boolean;\n };\n close: () => Promise<void>;\n}\n\nexport interface StartBridgeOptions {\n cwd?: string;\n port?: number;\n file?: string;\n harnessFile?: string;\n}\n\n/**\n * Start the bridge HTTP server. Pure server + in-memory sync state; callers own\n * process lifecycle. Resolves once listening.\n *\n * The returned handle exposes `getEditorState` and `reload` directly, so an\n * in-process caller (the MCP server) never has to make an HTTP request to its\n * own bridge — that is the whole reason live mode can work inside a sandboxed\n * agent. The equivalent HTTP endpoints stay for the skill's out-of-process CLI.\n */\nexport function startBridge({\n cwd = process.cwd(),\n port = 0,\n file = join(WORKING_DIR, \"template.json\"),\n harnessFile = HARNESS_FILE,\n}: StartBridgeOptions = {}): Promise<BridgeHandle> {\n const workingPath = isAbsolute(file) ? file : resolve(cwd, file);\n\n // Sync state. `baseline` is the editor's NORMALIZED view of the caller's last\n // content (captured by the page right after it applies an update), so the\n // editor's own load-time normalization never reads as a user edit.\n // `editorCurrent` is the page's latest getContent(); `divergent` is true when\n // it structurally differs from `baseline` — i.e. the user hand-edited.\n const state: {\n baseline: unknown;\n editorCurrent: unknown;\n divergent: boolean;\n annotations: Annotation[];\n } = {\n baseline: null,\n editorCurrent: null,\n divergent: false,\n annotations: [],\n };\n let nextAnnotationNumber = 1;\n function mintAnnotationId(): string {\n const n = nextAnnotationNumber++;\n return `a${n}-${Date.now().toString(36)}`;\n }\n const clients = new Set<ServerResponse>();\n\n function broadcastTemplate(content: unknown): void {\n const payload = `event: template\\ndata: ${JSON.stringify(content)}\\n\\n`;\n for (const res of clients) res.write(payload);\n }\n\n function getEditorState(): {\n divergent: boolean;\n content: unknown;\n annotations: Annotation[];\n } {\n return {\n divergent: state.divergent,\n content: state.editorCurrent,\n annotations: state.annotations,\n };\n }\n\n function reload(options?: { consumeAnnotations?: boolean }): {\n ok: boolean;\n clients: number;\n consumed: boolean;\n } {\n // The freshly-written file is the new baseline, so any pending user edit is\n // superseded. Notes stay: a plain reload is an ordinary edit, and clearing\n // them here would drop a queue the agent has not applied. consumeAnnotations\n // clears only after the file was actually read — clearing first would drop\n // the notes and push nothing when the file is missing.\n const content = readWorkingFile(workingPath);\n state.baseline = null;\n state.editorCurrent = null;\n state.divergent = false;\n const consumed = options?.consumeAnnotations === true && content !== null;\n if (consumed) state.annotations = [];\n if (content !== null) broadcastTemplate(content);\n return { ok: true, clients: clients.size, consumed };\n }\n\n const server = createServer(async (req, res) => {\n const { pathname } = new URL(req.url ?? \"/\", \"http://localhost\");\n const method = req.method ?? \"GET\";\n\n try {\n if (\n method === \"GET\" &&\n (pathname === \"/\" || pathname === \"/index.html\")\n ) {\n const html = readFileSync(harnessFile, \"utf8\").replaceAll(\n \"{{EDITOR_VERSION}}\",\n EDITOR_VERSION,\n );\n res.writeHead(200, { \"content-type\": \"text/html; charset=utf-8\" });\n res.end(html);\n return;\n }\n\n if (method === \"GET\" && pathname === \"/template\") {\n const content = readWorkingFile(workingPath);\n if (content === null) {\n res.writeHead(204).end(); // no working file yet — page inits empty\n return;\n }\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(content));\n return;\n }\n\n if (method === \"GET\" && pathname === \"/events\") {\n res.writeHead(200, {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache\",\n connection: \"keep-alive\",\n });\n res.write(\"event: ready\\ndata: {}\\n\\n\");\n clients.add(res);\n const ping = setInterval(() => res.write(\": ping\\n\\n\"), 15000);\n req.on(\"close\", () => {\n clearInterval(ping);\n clients.delete(res);\n });\n return;\n }\n\n if (method === \"POST\" && pathname === \"/content\") {\n const body = await readJsonBody(req);\n const content = body?.content;\n if (body?.baseline) {\n // Post-apply snapshot: this is the editor's normalized view of the\n // caller's content, not a user edit.\n state.baseline = content ?? null;\n state.editorCurrent = content ?? null;\n state.divergent = false;\n } else {\n state.editorCurrent = content ?? null;\n state.divergent =\n state.baseline !== null && !deepEqual(content, state.baseline);\n }\n res.writeHead(204).end();\n return;\n }\n\n if (method === \"GET\" && pathname === \"/content\") {\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(getEditorState()));\n return;\n }\n\n if (method === \"POST\" && pathname === \"/annotations\") {\n const body = await readJsonBody(req);\n const parsed = readNoteText(body?.text);\n if (\"error\" in parsed) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(parsed.error);\n return;\n }\n if (state.annotations.length >= NOTE_QUEUE_MAX) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"The note queue is full (40).\");\n return;\n }\n const blockId = readOptionalId(body?.blockId);\n let parentBlockId = readOptionalId(body?.parentBlockId);\n if (parentBlockId === blockId) parentBlockId = null;\n const annotation: Annotation = {\n id: mintAnnotationId(),\n blockId,\n parentBlockId,\n blockType: readBlockType(body?.blockType),\n label: readLabel(body?.label),\n text: parsed.text,\n createdAt: Date.now(),\n };\n state.annotations.push(annotation);\n res.writeHead(201, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(annotation));\n return;\n }\n\n const noteId = annotationIdFromPath(pathname);\n if (noteId && (method === \"PUT\" || method === \"DELETE\")) {\n const index = state.annotations.findIndex((note) => note.id === noteId);\n if (index < 0) {\n noteNotFound(res);\n return;\n }\n if (method === \"DELETE\") {\n state.annotations.splice(index, 1);\n res.writeHead(204).end();\n return;\n }\n const body = await readJsonBody(req);\n const parsed = readNoteText(body?.text);\n if (\"error\" in parsed) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(parsed.error);\n return;\n }\n const updated: Annotation = {\n ...state.annotations[index],\n text: parsed.text,\n };\n state.annotations[index] = updated;\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(updated));\n return;\n }\n\n if (method === \"POST\" && pathname === \"/reload\") {\n const body = await readJsonBody(req);\n const result = reload({\n consumeAnnotations: body?.consumeAnnotations === true,\n });\n res.writeHead(200, { \"content-type\": \"application/json\" });\n res.end(JSON.stringify(result));\n return;\n }\n\n res.writeHead(404, { \"content-type\": \"text/plain\" }).end(\"Not found\");\n } catch (err) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(`Bad request: ${(err as Error).message}`);\n }\n });\n\n return new Promise((resolvePromise, rejectPromise) => {\n // listen() reports failures (e.g. EADDRINUSE) as an 'error' event, not a\n // throw — without this the error would be unhandled and crash the process.\n const onListenError = (err: Error) => rejectPromise(err);\n server.once(\"error\", onListenError);\n server.listen(port, \"127.0.0.1\", () => {\n server.removeListener(\"error\", onListenError);\n const address = server.address();\n const actualPort =\n typeof address === \"object\" && address !== null ? address.port : port;\n resolvePromise({\n server,\n port: actualPort,\n url: `http://localhost:${actualPort}/`,\n workingPath,\n getEditorState,\n reload,\n close: () =>\n new Promise<void>((r) => {\n for (const c of clients) c.end();\n clients.clear();\n server.close(() => r());\n }),\n });\n });\n });\n}\n\nexport interface PreferringHandle extends BridgeHandle {\n fellBack: boolean;\n preferredPort?: number;\n}\n\n/**\n * Start the bridge on `preferredPort`, falling back to an OS-assigned free port\n * when that one is occupied — so a busy port never fails the launch. The page\n * uses relative URLs, so the actual port is discovered, not assumed. The\n * returned handle carries `fellBack: true` when it landed on a different port.\n */\nexport async function startBridgePreferring({\n preferredPort = DEFAULT_PORT,\n ...opts\n}: StartBridgeOptions & {\n preferredPort?: number;\n} = {}): Promise<PreferringHandle> {\n try {\n const handle = await startBridge({ ...opts, port: preferredPort });\n return { ...handle, fellBack: false };\n } catch (err) {\n if ((err as NodeJS.ErrnoException)?.code !== \"EADDRINUSE\") throw err;\n const handle = await startBridge({ ...opts, port: 0 });\n return { ...handle, fellBack: true, preferredPort };\n }\n}\n"],"mappings":"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"}
|
|
@@ -218,7 +218,7 @@ const schema = {
|
|
|
218
218
|
}
|
|
219
219
|
},
|
|
220
220
|
"additionalProperties": false,
|
|
221
|
-
"description": "Optional outer frame for a section. When present, the section is rendered inside an `mj-wrapper` — a
|
|
221
|
+
"description": "Optional outer frame for a section. When present, the section is rendered inside an `mj-wrapper` — a band (its own background + padding) that frames the section, e.g. a white card sitting on a colored band. The band is as wide as the email body (`settings.width`), not the recipient's viewport."
|
|
222
222
|
},
|
|
223
223
|
"TitleBlock": {
|
|
224
224
|
"type": "object",
|
|
@@ -1459,4 +1459,4 @@ function applyOperation(content, payload) {
|
|
|
1459
1459
|
//#endregion
|
|
1460
1460
|
export { validateTemplate as a, schema as i, getColumnCount as n, runQualityLint as r, applyOperation as t };
|
|
1461
1461
|
|
|
1462
|
-
//# sourceMappingURL=src-
|
|
1462
|
+
//# sourceMappingURL=src-De5svMDM.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src-GcX5tdei.js","names":["rawSchema"],"sources":["../schema.json","../src/validate.ts","../src/operations.ts"],"sourcesContent":["","// Structural validation of a Templatical template against the generated JSON\n// Schema, plus the @templatical/quality lint layered on top.\n//\n// Structural validation is discriminator-aware: each block is checked against\n// the subschema for its declared `type` (e.g. a block with `\"type\": \"button\"`\n// is validated as a ButtonBlock), so errors are precise (\"blocks[2] (button)\n// must have required property 'url'\") instead of the raw anyOf's \"must match\n// exactly one schema in anyOf\".\n\nimport Ajv, { type ValidateFunction, type ErrorObject } from \"ajv\";\nimport { lintTemplate, type LintIssue } from \"@templatical/quality\";\nimport type { TemplateContent } from \"@templatical/types\";\nimport rawSchema from \"../schema.json\";\n\ninterface SchemaDefinition {\n properties?: { type?: { const?: unknown } } & Record<string, unknown>;\n [key: string]: unknown;\n}\n\ninterface SchemaDocument {\n definitions: Record<string, SchemaDefinition>;\n [key: string]: unknown;\n}\n\n/** The generated JSON Schema for `TemplateContent`, as a plain object. */\nexport const schema = rawSchema as unknown as SchemaDocument;\n\nconst ajv = new Ajv({ allErrors: true, strict: false });\n\n// Map a block's `type` discriminator (e.g. \"button\", \"social\") to its schema\n// definition name (e.g. \"ButtonBlock\", \"SocialIconsBlock\"), derived from the\n// schema itself so it never drifts from the generated defs.\nconst typeToDef: Record<string, string> = {};\nfor (const [name, def] of Object.entries(schema.definitions)) {\n const constType = def?.properties?.type?.const;\n if (typeof constType === \"string\") {\n typeToDef[constType] = name;\n }\n}\n\nconst validatorCache = new Map<string, ValidateFunction>();\n\nfunction validatorFor(defName: string): ValidateFunction {\n const cached = validatorCache.get(defName);\n if (cached) return cached;\n\n const definitions = structuredClone(schema.definitions);\n // A section's `children` holds nested blocks (Block[][]); we recurse into\n // them separately for precise paths, so stub the deep check here.\n if (defName === \"SectionBlock\") {\n const section = definitions.SectionBlock;\n if (section?.properties) {\n section.properties.children = { type: \"array\" };\n }\n }\n const compiled = ajv.compile({\n $ref: `#/definitions/${defName}`,\n definitions,\n });\n validatorCache.set(defName, compiled);\n return compiled;\n}\n\nconst settingsValidator = ajv.compile({\n $ref: \"#/definitions/TemplateSettings\",\n definitions: structuredClone(schema.definitions),\n});\n\ninterface FlatBlock {\n path: string;\n block: Record<string, unknown> | null;\n}\n\nfunction flattenBlocks(\n blocks: unknown,\n basePath: string,\n out: FlatBlock[],\n): void {\n if (!Array.isArray(blocks)) return;\n blocks.forEach((block, i) => {\n const path = `${basePath}[${i}]`;\n out.push({ path, block: block ?? null });\n if (block?.type === \"section\" && Array.isArray(block.children)) {\n block.children.forEach((column: unknown, ci: number) => {\n flattenBlocks(column, `${path}.children[${ci}]`, out);\n });\n }\n });\n}\n\nfunction formatAjvErrors(\n errors: ErrorObject[] | null | undefined,\n prefix: string,\n): string[] {\n return (errors ?? []).map((e) => {\n const where = `${prefix}${e.instancePath}`;\n const extra =\n e.keyword === \"additionalProperties\"\n ? ` (${(e.params as { additionalProperty?: string }).additionalProperty})`\n : \"\";\n return `${where} ${e.message}${extra}`;\n });\n}\n\nexport interface ValidationResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Structural validation. Synchronous, depends only on ajv + the committed\n * schema.json (no build of the workspace packages required).\n */\nexport function validateTemplate(data: unknown): ValidationResult {\n const errors: string[] = [];\n\n if (data === null || typeof data !== \"object\" || Array.isArray(data)) {\n return { valid: false, errors: [\"(root) must be an object\"] };\n }\n\n const doc = data as Record<string, unknown>;\n\n if (!Array.isArray(doc.blocks)) {\n errors.push(\"blocks must be an array\");\n }\n if (doc.settings === null || typeof doc.settings !== \"object\") {\n errors.push(\"settings must be an object\");\n } else if (!settingsValidator(doc.settings)) {\n errors.push(...formatAjvErrors(settingsValidator.errors, \"settings\"));\n }\n\n const flat: FlatBlock[] = [];\n flattenBlocks(doc.blocks, \"blocks\", flat);\n for (const { path, block } of flat) {\n const type = block?.type;\n const defName = typeof type === \"string\" ? typeToDef[type] : undefined;\n if (!defName) {\n const known = Object.keys(typeToDef).join(\", \");\n errors.push(\n `${path} has unknown or missing block type ${JSON.stringify(type)} (expected one of: ${known})`,\n );\n continue;\n }\n const validate = validatorFor(defName);\n if (!validate(block)) {\n errors.push(...formatAjvErrors(validate.errors, `${path} (${type})`));\n }\n }\n\n return { valid: errors.length === 0, errors };\n}\n\nexport interface QualityLintResult {\n issues: LintIssue[];\n /** Set when the linter itself threw — the template is structurally suspect. */\n error?: string;\n}\n\n/**\n * Quality layer — accessibility / structure / link linting.\n *\n * Assumes structurally-valid input, so callers should run `validateTemplate`\n * first; the try/catch is a guard against a malformed template crashing the\n * linter rather than a substitute for that ordering.\n */\nexport function runQualityLint(data: unknown): QualityLintResult {\n try {\n return { issues: lintTemplate(data as TemplateContent) ?? [] };\n } catch (err) {\n return { issues: [], error: (err as Error).message };\n }\n}\n","// A pure reducer over the template operation vocabulary.\n//\n// `applyOperation(content, payload) -> { ok, content, error }`. The input is\n// never mutated: every operation works on a `safeClone` and returns a new\n// document, so a rejected operation leaves the caller's copy provably untouched.\n//\n// Two deliberate differences from the editor's own mutators in\n// `@templatical/core` (`src/editor.ts`), which this mirrors:\n//\n// 1. **Failure is explicit.** Core silently `return`s on an invalid operation,\n// which is right for a UI — a drag that can't drop simply doesn't. An agent\n// gets no such feedback, so a silent no-op would read as success and the\n// next instruction would build on a state that never happened. Every\n// rejection here carries a reason.\n// 2. **No collaboration locks.** `lockedBlocks` is a Cloud concern; this runs\n// against a local working file with a single writer.\n//\n// Core is FSL and cannot be imported here, so the shared invariants — above all\n// the section-into-column refusal (issue #292) — are reimplemented. That is a\n// real drift risk: a guard added to core will not appear here. `tests/\n// operations.test.ts` states each invariant against both descriptions so the\n// pair has to be reconciled deliberately.\n//\n// Note the payload keys are camelCase (`blockId`, `targetSectionId`), matching\n// `@templatical/types` and core. Cloud's MCP tools emit snake_case in their own\n// `data` payloads, so this reducer is NOT wire-compatible with Cloud broadcasts\n// and is not meant to be.\n\nimport { safeClone } from \"@templatical/types\";\nimport type {\n Block,\n ColumnLayout,\n TemplateOperationPayload,\n SectionBlock,\n TemplateContent,\n TemplateSettings,\n} from \"@templatical/types\";\n\n/** Columns a layout declares. Mirrors the helper in core's editor. */\nexport function getColumnCount(layout: ColumnLayout): number {\n if (layout === \"1\") return 1;\n if (layout === \"3\") return 3;\n return 2;\n}\n\nfunction findBlockById(blocks: Block[], id: string): Block | null {\n for (const block of blocks) {\n if (block.id === id) return block;\n if (block.type === \"section\") {\n for (const column of block.children) {\n const found = findBlockById(column, id);\n if (found) return found;\n }\n }\n }\n return null;\n}\n\ninterface ParentRef {\n blocks: Block[];\n sectionId?: string;\n columnIndex?: number;\n}\n\nfunction findBlockParent(\n blocks: Block[],\n id: string,\n parent: ParentRef = { blocks },\n): ParentRef | null {\n for (const block of blocks) {\n if (block.id === id) return parent;\n if (block.type === \"section\") {\n for (let colIdx = 0; colIdx < block.children.length; colIdx++) {\n const result = findBlockParent(block.children[colIdx], id, {\n blocks: block.children[colIdx],\n sectionId: block.id,\n columnIndex: colIdx,\n });\n if (result) return result;\n }\n }\n }\n return null;\n}\n\nexport interface OperationResult {\n ok: boolean;\n /** The new document on success; the unchanged input on failure. */\n content: TemplateContent;\n error?: string;\n}\n\nfunction reject(content: TemplateContent, error: string): OperationResult {\n return { ok: false, content, error };\n}\n\n/**\n * Resolve the array an operation targets, enforcing the column rules.\n * Returns a string on rejection so callers can surface the reason.\n */\nfunction resolveTarget(\n content: TemplateContent,\n targetSectionId: string | undefined,\n columnIndex: number,\n): Block[] | string {\n if (!targetSectionId) return content.blocks;\n\n const section = findBlockById(content.blocks, targetSectionId);\n if (!section) return `No block with id \"${targetSectionId}\".`;\n if (section.type !== \"section\") {\n return `Block \"${targetSectionId}\" is a ${section.type}, not a section — it has no columns.`;\n }\n const count = getColumnCount((section as SectionBlock).columns);\n if (columnIndex < 0 || columnIndex >= count) {\n return `Column ${columnIndex} is out of range for a \"${(section as SectionBlock).columns}\" section (${count} column(s)).`;\n }\n const children = section as SectionBlock;\n children.children[columnIndex] = children.children[columnIndex] || [];\n return children.children[columnIndex];\n}\n\nfunction insertAt(target: Block[], block: Block, index?: number): void {\n if (index !== undefined && index < target.length) {\n target.splice(index, 0, block);\n } else {\n target.push(block);\n }\n}\n\ntype Data = Record<string, unknown>;\n\n/** Apply one operation, returning a new document. Never mutates the input. */\nexport function applyOperation(\n content: TemplateContent,\n payload: TemplateOperationPayload,\n): OperationResult {\n const data = (payload.data ?? {}) as Data;\n\n switch (payload.operation) {\n case \"setContent\": {\n const next = data.content as TemplateContent | undefined;\n if (!next || typeof next !== \"object\" || !Array.isArray(next.blocks)) {\n return reject(\n content,\n \"setContent needs `content` with a blocks array.\",\n );\n }\n return { ok: true, content: safeClone(next) };\n }\n\n case \"updateSettings\": {\n const updates = data.settings as Partial<TemplateSettings> | undefined;\n if (!updates || typeof updates !== \"object\") {\n return reject(content, \"updateSettings needs a `settings` object.\");\n }\n const draft = safeClone(content);\n draft.settings = { ...draft.settings, ...updates };\n return { ok: true, content: draft };\n }\n\n case \"addBlock\": {\n const block = data.block as Block | undefined;\n if (!block || typeof block !== \"object\" || typeof block.id !== \"string\") {\n return reject(content, \"addBlock needs a `block` with an id.\");\n }\n const targetSectionId = data.targetSectionId as string | undefined;\n\n // Sections cannot nest inside a column — MJML forbids `mj-section` inside\n // `mj-column`, so the renderer drops them on export (issue #292). Reject\n // up front rather than lose the content silently at render time.\n if (targetSectionId && block.type === \"section\") {\n return reject(\n content,\n \"A section cannot be nested inside a section column — MJML would drop it on export. Add it at the top level instead.\",\n );\n }\n\n const draft = safeClone(content);\n if (findBlockById(draft.blocks, block.id)) {\n return reject(content, `A block with id \"${block.id}\" already exists.`);\n }\n const target = resolveTarget(\n draft,\n targetSectionId,\n (data.columnIndex as number | undefined) ?? 0,\n );\n if (typeof target === \"string\") return reject(content, target);\n\n insertAt(target, safeClone(block), data.index as number | undefined);\n return { ok: true, content: draft };\n }\n\n case \"updateBlock\": {\n const blockId = data.blockId as string | undefined;\n const updates = data.updates as Partial<Block> | undefined;\n if (!blockId) return reject(content, \"updateBlock needs a `blockId`.\");\n if (!updates || typeof updates !== \"object\") {\n return reject(content, \"updateBlock needs an `updates` object.\");\n }\n const draft = safeClone(content);\n const block = findBlockById(draft.blocks, blockId);\n if (!block) return reject(content, `No block with id \"${blockId}\".`);\n if (\"type\" in updates && updates.type !== block.type) {\n // Changing type in place would leave the block carrying the previous\n // type's fields, which no longer validate against its new subschema.\n return reject(\n content,\n `Cannot change a block's type (${block.type} → ${String(updates.type)}). Delete it and add the new block instead.`,\n );\n }\n Object.assign(block, updates);\n return { ok: true, content: draft };\n }\n\n case \"updateBlockStyle\": {\n const blockId = data.blockId as string | undefined;\n const styles = data.styles as Record<string, unknown> | undefined;\n if (!blockId) {\n return reject(content, \"updateBlockStyle needs a `blockId`.\");\n }\n if (!styles || typeof styles !== \"object\") {\n return reject(content, \"updateBlockStyle needs a `styles` object.\");\n }\n const draft = safeClone(content);\n const block = findBlockById(draft.blocks, blockId);\n if (!block) return reject(content, `No block with id \"${blockId}\".`);\n // Merge rather than replace, so a caller can set one property without\n // restating padding and every other style the block already carries.\n block.styles = {\n ...block.styles,\n ...styles,\n } as Block[\"styles\"];\n return { ok: true, content: draft };\n }\n\n case \"deleteBlock\": {\n const blockId = data.blockId as string | undefined;\n if (!blockId) return reject(content, \"deleteBlock needs a `blockId`.\");\n const draft = safeClone(content);\n const parent = findBlockParent(draft.blocks, blockId);\n if (!parent) return reject(content, `No block with id \"${blockId}\".`);\n const index = parent.blocks.findIndex((b) => b.id === blockId);\n parent.blocks.splice(index, 1);\n return { ok: true, content: draft };\n }\n\n case \"moveBlock\": {\n const blockId = data.blockId as string | undefined;\n const index = data.index as number | undefined;\n if (!blockId) return reject(content, \"moveBlock needs a `blockId`.\");\n if (typeof index !== \"number\" || index < 0) {\n return reject(content, \"moveBlock needs a non-negative `index`.\");\n }\n const targetSectionId = data.targetSectionId as string | undefined;\n\n const draft = safeClone(content);\n const parent = findBlockParent(draft.blocks, blockId);\n if (!parent) return reject(content, `No block with id \"${blockId}\".`);\n const oldIndex = parent.blocks.findIndex((b) => b.id === blockId);\n\n if (targetSectionId && parent.blocks[oldIndex].type === \"section\") {\n return reject(\n content,\n \"A section cannot be moved into a section column — MJML would drop it on export.\",\n );\n }\n if (targetSectionId === blockId) {\n return reject(content, \"A block cannot be moved into itself.\");\n }\n\n // Resolve the target BEFORE splicing the source: an invalid target would\n // otherwise leave the block removed and unrecoverable.\n const target = resolveTarget(\n draft,\n targetSectionId,\n (data.columnIndex as number | undefined) ?? 0,\n );\n if (typeof target === \"string\") return reject(content, target);\n\n const [block] = parent.blocks.splice(oldIndex, 1);\n target.splice(index, 0, block);\n return { ok: true, content: draft };\n }\n\n default: {\n return reject(\n content,\n `Unknown operation \"${String(payload.operation)}\".`,\n );\n }\n }\n}\n"],"mappings":";;;;;;ACyBA,MAAa,SAASA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
|
|
1
|
+
{"version":3,"file":"src-De5svMDM.js","names":["rawSchema"],"sources":["../schema.json","../src/validate.ts","../src/operations.ts"],"sourcesContent":["","// Structural validation of a Templatical template against the generated JSON\n// Schema, plus the @templatical/quality lint layered on top.\n//\n// Structural validation is discriminator-aware: each block is checked against\n// the subschema for its declared `type` (e.g. a block with `\"type\": \"button\"`\n// is validated as a ButtonBlock), so errors are precise (\"blocks[2] (button)\n// must have required property 'url'\") instead of the raw anyOf's \"must match\n// exactly one schema in anyOf\".\n\nimport Ajv, { type ValidateFunction, type ErrorObject } from \"ajv\";\nimport { lintTemplate, type LintIssue } from \"@templatical/quality\";\nimport type { TemplateContent } from \"@templatical/types\";\nimport rawSchema from \"../schema.json\";\n\ninterface SchemaDefinition {\n properties?: { type?: { const?: unknown } } & Record<string, unknown>;\n [key: string]: unknown;\n}\n\ninterface SchemaDocument {\n definitions: Record<string, SchemaDefinition>;\n [key: string]: unknown;\n}\n\n/** The generated JSON Schema for `TemplateContent`, as a plain object. */\nexport const schema = rawSchema as unknown as SchemaDocument;\n\nconst ajv = new Ajv({ allErrors: true, strict: false });\n\n// Map a block's `type` discriminator (e.g. \"button\", \"social\") to its schema\n// definition name (e.g. \"ButtonBlock\", \"SocialIconsBlock\"), derived from the\n// schema itself so it never drifts from the generated defs.\nconst typeToDef: Record<string, string> = {};\nfor (const [name, def] of Object.entries(schema.definitions)) {\n const constType = def?.properties?.type?.const;\n if (typeof constType === \"string\") {\n typeToDef[constType] = name;\n }\n}\n\nconst validatorCache = new Map<string, ValidateFunction>();\n\nfunction validatorFor(defName: string): ValidateFunction {\n const cached = validatorCache.get(defName);\n if (cached) return cached;\n\n const definitions = structuredClone(schema.definitions);\n // A section's `children` holds nested blocks (Block[][]); we recurse into\n // them separately for precise paths, so stub the deep check here.\n if (defName === \"SectionBlock\") {\n const section = definitions.SectionBlock;\n if (section?.properties) {\n section.properties.children = { type: \"array\" };\n }\n }\n const compiled = ajv.compile({\n $ref: `#/definitions/${defName}`,\n definitions,\n });\n validatorCache.set(defName, compiled);\n return compiled;\n}\n\nconst settingsValidator = ajv.compile({\n $ref: \"#/definitions/TemplateSettings\",\n definitions: structuredClone(schema.definitions),\n});\n\ninterface FlatBlock {\n path: string;\n block: Record<string, unknown> | null;\n}\n\nfunction flattenBlocks(\n blocks: unknown,\n basePath: string,\n out: FlatBlock[],\n): void {\n if (!Array.isArray(blocks)) return;\n blocks.forEach((block, i) => {\n const path = `${basePath}[${i}]`;\n out.push({ path, block: block ?? null });\n if (block?.type === \"section\" && Array.isArray(block.children)) {\n block.children.forEach((column: unknown, ci: number) => {\n flattenBlocks(column, `${path}.children[${ci}]`, out);\n });\n }\n });\n}\n\nfunction formatAjvErrors(\n errors: ErrorObject[] | null | undefined,\n prefix: string,\n): string[] {\n return (errors ?? []).map((e) => {\n const where = `${prefix}${e.instancePath}`;\n const extra =\n e.keyword === \"additionalProperties\"\n ? ` (${(e.params as { additionalProperty?: string }).additionalProperty})`\n : \"\";\n return `${where} ${e.message}${extra}`;\n });\n}\n\nexport interface ValidationResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Structural validation. Synchronous, depends only on ajv + the committed\n * schema.json (no build of the workspace packages required).\n */\nexport function validateTemplate(data: unknown): ValidationResult {\n const errors: string[] = [];\n\n if (data === null || typeof data !== \"object\" || Array.isArray(data)) {\n return { valid: false, errors: [\"(root) must be an object\"] };\n }\n\n const doc = data as Record<string, unknown>;\n\n if (!Array.isArray(doc.blocks)) {\n errors.push(\"blocks must be an array\");\n }\n if (doc.settings === null || typeof doc.settings !== \"object\") {\n errors.push(\"settings must be an object\");\n } else if (!settingsValidator(doc.settings)) {\n errors.push(...formatAjvErrors(settingsValidator.errors, \"settings\"));\n }\n\n const flat: FlatBlock[] = [];\n flattenBlocks(doc.blocks, \"blocks\", flat);\n for (const { path, block } of flat) {\n const type = block?.type;\n const defName = typeof type === \"string\" ? typeToDef[type] : undefined;\n if (!defName) {\n const known = Object.keys(typeToDef).join(\", \");\n errors.push(\n `${path} has unknown or missing block type ${JSON.stringify(type)} (expected one of: ${known})`,\n );\n continue;\n }\n const validate = validatorFor(defName);\n if (!validate(block)) {\n errors.push(...formatAjvErrors(validate.errors, `${path} (${type})`));\n }\n }\n\n return { valid: errors.length === 0, errors };\n}\n\nexport interface QualityLintResult {\n issues: LintIssue[];\n /** Set when the linter itself threw — the template is structurally suspect. */\n error?: string;\n}\n\n/**\n * Quality layer — accessibility / structure / link linting.\n *\n * Assumes structurally-valid input, so callers should run `validateTemplate`\n * first; the try/catch is a guard against a malformed template crashing the\n * linter rather than a substitute for that ordering.\n */\nexport function runQualityLint(data: unknown): QualityLintResult {\n try {\n return { issues: lintTemplate(data as TemplateContent) ?? [] };\n } catch (err) {\n return { issues: [], error: (err as Error).message };\n }\n}\n","// A pure reducer over the template operation vocabulary.\n//\n// `applyOperation(content, payload) -> { ok, content, error }`. The input is\n// never mutated: every operation works on a `safeClone` and returns a new\n// document, so a rejected operation leaves the caller's copy provably untouched.\n//\n// Two deliberate differences from the editor's own mutators in\n// `@templatical/core` (`src/editor.ts`), which this mirrors:\n//\n// 1. **Failure is explicit.** Core silently `return`s on an invalid operation,\n// which is right for a UI — a drag that can't drop simply doesn't. An agent\n// gets no such feedback, so a silent no-op would read as success and the\n// next instruction would build on a state that never happened. Every\n// rejection here carries a reason.\n// 2. **No collaboration locks.** `lockedBlocks` is a Cloud concern; this runs\n// against a local working file with a single writer.\n//\n// Core is FSL and cannot be imported here, so the shared invariants — above all\n// the section-into-column refusal (issue #292) — are reimplemented. That is a\n// real drift risk: a guard added to core will not appear here. `tests/\n// operations.test.ts` states each invariant against both descriptions so the\n// pair has to be reconciled deliberately.\n//\n// Note the payload keys are camelCase (`blockId`, `targetSectionId`), matching\n// `@templatical/types` and core. Cloud's MCP tools emit snake_case in their own\n// `data` payloads, so this reducer is NOT wire-compatible with Cloud broadcasts\n// and is not meant to be.\n\nimport { safeClone } from \"@templatical/types\";\nimport type {\n Block,\n ColumnLayout,\n TemplateOperationPayload,\n SectionBlock,\n TemplateContent,\n TemplateSettings,\n} from \"@templatical/types\";\n\n/** Columns a layout declares. Mirrors the helper in core's editor. */\nexport function getColumnCount(layout: ColumnLayout): number {\n if (layout === \"1\") return 1;\n if (layout === \"3\") return 3;\n return 2;\n}\n\nfunction findBlockById(blocks: Block[], id: string): Block | null {\n for (const block of blocks) {\n if (block.id === id) return block;\n if (block.type === \"section\") {\n for (const column of block.children) {\n const found = findBlockById(column, id);\n if (found) return found;\n }\n }\n }\n return null;\n}\n\ninterface ParentRef {\n blocks: Block[];\n sectionId?: string;\n columnIndex?: number;\n}\n\nfunction findBlockParent(\n blocks: Block[],\n id: string,\n parent: ParentRef = { blocks },\n): ParentRef | null {\n for (const block of blocks) {\n if (block.id === id) return parent;\n if (block.type === \"section\") {\n for (let colIdx = 0; colIdx < block.children.length; colIdx++) {\n const result = findBlockParent(block.children[colIdx], id, {\n blocks: block.children[colIdx],\n sectionId: block.id,\n columnIndex: colIdx,\n });\n if (result) return result;\n }\n }\n }\n return null;\n}\n\nexport interface OperationResult {\n ok: boolean;\n /** The new document on success; the unchanged input on failure. */\n content: TemplateContent;\n error?: string;\n}\n\nfunction reject(content: TemplateContent, error: string): OperationResult {\n return { ok: false, content, error };\n}\n\n/**\n * Resolve the array an operation targets, enforcing the column rules.\n * Returns a string on rejection so callers can surface the reason.\n */\nfunction resolveTarget(\n content: TemplateContent,\n targetSectionId: string | undefined,\n columnIndex: number,\n): Block[] | string {\n if (!targetSectionId) return content.blocks;\n\n const section = findBlockById(content.blocks, targetSectionId);\n if (!section) return `No block with id \"${targetSectionId}\".`;\n if (section.type !== \"section\") {\n return `Block \"${targetSectionId}\" is a ${section.type}, not a section — it has no columns.`;\n }\n const count = getColumnCount((section as SectionBlock).columns);\n if (columnIndex < 0 || columnIndex >= count) {\n return `Column ${columnIndex} is out of range for a \"${(section as SectionBlock).columns}\" section (${count} column(s)).`;\n }\n const children = section as SectionBlock;\n children.children[columnIndex] = children.children[columnIndex] || [];\n return children.children[columnIndex];\n}\n\nfunction insertAt(target: Block[], block: Block, index?: number): void {\n if (index !== undefined && index < target.length) {\n target.splice(index, 0, block);\n } else {\n target.push(block);\n }\n}\n\ntype Data = Record<string, unknown>;\n\n/** Apply one operation, returning a new document. Never mutates the input. */\nexport function applyOperation(\n content: TemplateContent,\n payload: TemplateOperationPayload,\n): OperationResult {\n const data = (payload.data ?? {}) as Data;\n\n switch (payload.operation) {\n case \"setContent\": {\n const next = data.content as TemplateContent | undefined;\n if (!next || typeof next !== \"object\" || !Array.isArray(next.blocks)) {\n return reject(\n content,\n \"setContent needs `content` with a blocks array.\",\n );\n }\n return { ok: true, content: safeClone(next) };\n }\n\n case \"updateSettings\": {\n const updates = data.settings as Partial<TemplateSettings> | undefined;\n if (!updates || typeof updates !== \"object\") {\n return reject(content, \"updateSettings needs a `settings` object.\");\n }\n const draft = safeClone(content);\n draft.settings = { ...draft.settings, ...updates };\n return { ok: true, content: draft };\n }\n\n case \"addBlock\": {\n const block = data.block as Block | undefined;\n if (!block || typeof block !== \"object\" || typeof block.id !== \"string\") {\n return reject(content, \"addBlock needs a `block` with an id.\");\n }\n const targetSectionId = data.targetSectionId as string | undefined;\n\n // Sections cannot nest inside a column — MJML forbids `mj-section` inside\n // `mj-column`, so the renderer drops them on export (issue #292). Reject\n // up front rather than lose the content silently at render time.\n if (targetSectionId && block.type === \"section\") {\n return reject(\n content,\n \"A section cannot be nested inside a section column — MJML would drop it on export. Add it at the top level instead.\",\n );\n }\n\n const draft = safeClone(content);\n if (findBlockById(draft.blocks, block.id)) {\n return reject(content, `A block with id \"${block.id}\" already exists.`);\n }\n const target = resolveTarget(\n draft,\n targetSectionId,\n (data.columnIndex as number | undefined) ?? 0,\n );\n if (typeof target === \"string\") return reject(content, target);\n\n insertAt(target, safeClone(block), data.index as number | undefined);\n return { ok: true, content: draft };\n }\n\n case \"updateBlock\": {\n const blockId = data.blockId as string | undefined;\n const updates = data.updates as Partial<Block> | undefined;\n if (!blockId) return reject(content, \"updateBlock needs a `blockId`.\");\n if (!updates || typeof updates !== \"object\") {\n return reject(content, \"updateBlock needs an `updates` object.\");\n }\n const draft = safeClone(content);\n const block = findBlockById(draft.blocks, blockId);\n if (!block) return reject(content, `No block with id \"${blockId}\".`);\n if (\"type\" in updates && updates.type !== block.type) {\n // Changing type in place would leave the block carrying the previous\n // type's fields, which no longer validate against its new subschema.\n return reject(\n content,\n `Cannot change a block's type (${block.type} → ${String(updates.type)}). Delete it and add the new block instead.`,\n );\n }\n Object.assign(block, updates);\n return { ok: true, content: draft };\n }\n\n case \"updateBlockStyle\": {\n const blockId = data.blockId as string | undefined;\n const styles = data.styles as Record<string, unknown> | undefined;\n if (!blockId) {\n return reject(content, \"updateBlockStyle needs a `blockId`.\");\n }\n if (!styles || typeof styles !== \"object\") {\n return reject(content, \"updateBlockStyle needs a `styles` object.\");\n }\n const draft = safeClone(content);\n const block = findBlockById(draft.blocks, blockId);\n if (!block) return reject(content, `No block with id \"${blockId}\".`);\n // Merge rather than replace, so a caller can set one property without\n // restating padding and every other style the block already carries.\n block.styles = {\n ...block.styles,\n ...styles,\n } as Block[\"styles\"];\n return { ok: true, content: draft };\n }\n\n case \"deleteBlock\": {\n const blockId = data.blockId as string | undefined;\n if (!blockId) return reject(content, \"deleteBlock needs a `blockId`.\");\n const draft = safeClone(content);\n const parent = findBlockParent(draft.blocks, blockId);\n if (!parent) return reject(content, `No block with id \"${blockId}\".`);\n const index = parent.blocks.findIndex((b) => b.id === blockId);\n parent.blocks.splice(index, 1);\n return { ok: true, content: draft };\n }\n\n case \"moveBlock\": {\n const blockId = data.blockId as string | undefined;\n const index = data.index as number | undefined;\n if (!blockId) return reject(content, \"moveBlock needs a `blockId`.\");\n if (typeof index !== \"number\" || index < 0) {\n return reject(content, \"moveBlock needs a non-negative `index`.\");\n }\n const targetSectionId = data.targetSectionId as string | undefined;\n\n const draft = safeClone(content);\n const parent = findBlockParent(draft.blocks, blockId);\n if (!parent) return reject(content, `No block with id \"${blockId}\".`);\n const oldIndex = parent.blocks.findIndex((b) => b.id === blockId);\n\n if (targetSectionId && parent.blocks[oldIndex].type === \"section\") {\n return reject(\n content,\n \"A section cannot be moved into a section column — MJML would drop it on export.\",\n );\n }\n if (targetSectionId === blockId) {\n return reject(content, \"A block cannot be moved into itself.\");\n }\n\n // Resolve the target BEFORE splicing the source: an invalid target would\n // otherwise leave the block removed and unrecoverable.\n const target = resolveTarget(\n draft,\n targetSectionId,\n (data.columnIndex as number | undefined) ?? 0,\n );\n if (typeof target === \"string\") return reject(content, target);\n\n const [block] = parent.blocks.splice(oldIndex, 1);\n target.splice(index, 0, block);\n return { ok: true, content: draft };\n }\n\n default: {\n return reject(\n content,\n `Unknown operation \"${String(payload.operation)}\".`,\n );\n }\n }\n}\n"],"mappings":";;;;;;ACyBA,MAAa,SAASA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@templatical/template-tools",
|
|
3
3
|
"description": "CLI and library for Templatical email templates — validate, render, edit, import, and live-preview",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.46.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"templatical": "./dist/bin.js"
|
|
7
7
|
},
|
|
8
8
|
"bugs": "https://github.com/templatical/sdk/issues",
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@templatical/quality": "0.
|
|
11
|
-
"@templatical/renderer": "0.
|
|
12
|
-
"@templatical/types": "0.
|
|
10
|
+
"@templatical/quality": "0.46.0",
|
|
11
|
+
"@templatical/renderer": "0.46.0",
|
|
12
|
+
"@templatical/types": "0.46.0",
|
|
13
13
|
"ajv": "^8.20.0"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"module": "./dist/index.js",
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@templatical/import-beefree": "0.
|
|
52
|
-
"@templatical/import-chamaileon": "0.
|
|
53
|
-
"@templatical/import-easy-email-pro": "0.
|
|
54
|
-
"@templatical/import-html": "0.
|
|
55
|
-
"@templatical/import-mjml": "0.
|
|
56
|
-
"@templatical/import-stripo": "0.
|
|
57
|
-
"@templatical/import-topol": "0.
|
|
58
|
-
"@templatical/import-unlayer": "0.
|
|
51
|
+
"@templatical/import-beefree": "0.46.0",
|
|
52
|
+
"@templatical/import-chamaileon": "0.46.0",
|
|
53
|
+
"@templatical/import-easy-email-pro": "0.46.0",
|
|
54
|
+
"@templatical/import-html": "0.46.0",
|
|
55
|
+
"@templatical/import-mjml": "0.46.0",
|
|
56
|
+
"@templatical/import-stripo": "0.46.0",
|
|
57
|
+
"@templatical/import-topol": "0.46.0",
|
|
58
|
+
"@templatical/import-unlayer": "0.46.0",
|
|
59
59
|
"mjml": "^5.4.1"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
package/schema.json
CHANGED
|
@@ -325,7 +325,7 @@
|
|
|
325
325
|
}
|
|
326
326
|
},
|
|
327
327
|
"additionalProperties": false,
|
|
328
|
-
"description": "Optional outer frame for a section. When present, the section is rendered inside an `mj-wrapper` — a
|
|
328
|
+
"description": "Optional outer frame for a section. When present, the section is rendered inside an `mj-wrapper` — a band (its own background + padding) that frames the section, e.g. a white card sitting on a colored band. The band is as wide as the email body (`settings.width`), not the recipient's viewport."
|
|
329
329
|
},
|
|
330
330
|
"TitleBlock": {
|
|
331
331
|
"type": "object",
|