@backmatter/core 0.0.31 → 0.0.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backmatter/core",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "Experimental Backmatter SDK. APIs will break. Not ready for production.",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/src/model.ts CHANGED
@@ -257,45 +257,7 @@ export function serializeSchema(
257
257
  return snapshot;
258
258
  }
259
259
 
260
- /** Remove `{locale}` path/route segments from rows stored before locale removal. */
261
- export function stripLocaleTemplateSegments(template: string): string {
262
- return template.split("/").filter((part) => part !== "{locale}").join("/");
263
- }
264
-
265
- function migrateLegacySchemaInput(input: unknown): unknown {
266
- if (!input || typeof input !== "object") return input;
267
- const value = input as Record<string, unknown>;
268
- if (!Array.isArray(value.models)) return input;
269
- return {
270
- ...value,
271
- models: value.models.map((raw) => {
272
- if (!raw || typeof raw !== "object") return raw;
273
- const model = raw as Record<string, unknown>;
274
- const next: Record<string, unknown> = { ...model };
275
- if (typeof model.path === "string") {
276
- next.path = stripLocaleTemplateSegments(model.path);
277
- }
278
- if (typeof model.route === "string") {
279
- next.route = stripLocaleTemplateSegments(model.route);
280
- }
281
- if (model.fields && typeof model.fields === "object" && !Array.isArray(model.fields)) {
282
- const fields = { ...(model.fields as Record<string, unknown>) };
283
- for (const [key, snap] of Object.entries(fields)) {
284
- const kind =
285
- snap && typeof snap === "object"
286
- ? (snap as { kind?: string }).kind
287
- : undefined;
288
- if (kind === "locale") delete fields[key];
289
- }
290
- next.fields = fields;
291
- }
292
- return next;
293
- }),
294
- };
295
- }
296
-
297
260
  export function parseSchemaSnapshot(input: unknown): SchemaSnapshot {
298
- input = migrateLegacySchemaInput(input);
299
261
  if (!input || typeof input !== "object") {
300
262
  throw new Error("schema must be an object");
301
263
  }
@@ -59,11 +59,17 @@ export type PreviewPointMessage = Envelope & {
59
59
 
60
60
  export type PreviewGrabMessage = Envelope & {
61
61
  type: "backmatter:grab";
62
- blockId: string;
62
+ /** Null when the click is not inside a page block. */
63
+ blockId: string | null;
63
64
  blockType: string | null;
64
65
  blockIndex: number | null;
65
66
  quote: string;
66
67
  shift: boolean;
68
+ component?: string;
69
+ file?: string;
70
+ line?: number;
71
+ column?: number;
72
+ stack?: string[];
67
73
  };
68
74
 
69
75
  export type PreviewHighlightMessage = Envelope & {
@@ -119,6 +125,37 @@ function readEnvelope(value: Record<string, unknown>): Envelope | null {
119
125
  return { v: PREVIEW_CHANNEL_VERSION, ref };
120
126
  }
121
127
 
128
+ const GRAB_FILE = /^[\w./@+-]+\.(?:tsx|ts|jsx|js|mdx)$/;
129
+
130
+ function readGrabPlace(data: Record<string, unknown>): Pick<
131
+ PreviewGrabMessage,
132
+ "component" | "file" | "line" | "column" | "stack"
133
+ > {
134
+ const component =
135
+ typeof data.component === "string" && data.component.length > 0 && data.component.length <= 80
136
+ ? data.component
137
+ : undefined;
138
+ const file =
139
+ typeof data.file === "string" && data.file.length <= 240 && GRAB_FILE.test(data.file) && !data.file.includes("..")
140
+ ? data.file
141
+ : undefined;
142
+ const line = typeof data.line === "number" && Number.isInteger(data.line) && data.line > 0 ? data.line : undefined;
143
+ const column =
144
+ typeof data.column === "number" && Number.isInteger(data.column) && data.column > 0 ? data.column : undefined;
145
+ const stack = Array.isArray(data.stack)
146
+ ? data.stack
147
+ .filter((item): item is string => typeof item === "string" && item.length > 0 && item.length <= 80)
148
+ .slice(0, 4)
149
+ : [];
150
+ return {
151
+ ...(component ? { component } : {}),
152
+ ...(file ? { file } : {}),
153
+ ...(line ? { line } : {}),
154
+ ...(column ? { column } : {}),
155
+ ...(stack.length > 0 ? { stack } : {}),
156
+ };
157
+ }
158
+
122
159
  /** Parse a channel payload. Legacy hotkey messages (no `v`) still parse. */
123
160
  export function parsePreviewMessage(data: unknown): PreviewChannelMessage | null {
124
161
  const hotkey = parsePreviewHotkey(data);
@@ -167,7 +204,8 @@ export function parsePreviewMessage(data: unknown): PreviewChannelMessage | null
167
204
  return { type: "backmatter:point", ...envelope, on: data.on };
168
205
  }
169
206
  case "backmatter:grab": {
170
- if (typeof data.blockId !== "string" || data.blockId.length === 0) return null;
207
+ const blockId = data.blockId === null ? null : typeof data.blockId === "string" ? data.blockId : undefined;
208
+ if (blockId === undefined || blockId === "") return null;
171
209
  if (data.blockType !== null && typeof data.blockType !== "string") return null;
172
210
  if (
173
211
  data.blockIndex !== null &&
@@ -176,14 +214,17 @@ export function parsePreviewMessage(data: unknown): PreviewChannelMessage | null
176
214
  return null;
177
215
  }
178
216
  if (typeof data.quote !== "string" || typeof data.shift !== "boolean") return null;
217
+ const place = readGrabPlace(data);
218
+ if (blockId === null && !normalizePointerQuote(data.quote) && !place.component && !place.file) return null;
179
219
  return {
180
220
  type: "backmatter:grab",
181
221
  ...envelope,
182
- blockId: data.blockId,
222
+ blockId,
183
223
  blockType: data.blockType,
184
224
  blockIndex: data.blockIndex,
185
225
  quote: normalizePointerQuote(data.quote),
186
226
  shift: data.shift,
227
+ ...place,
187
228
  };
188
229
  }
189
230
  case "backmatter:highlight": {