@backmatter/core 0.0.26 → 0.0.27

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.26",
3
+ "version": "0.0.27",
4
4
  "description": "Experimental Backmatter SDK. APIs will break. Not ready for production.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -49,12 +49,38 @@ export type PreviewStatusMessage = Envelope & {
49
49
  message?: string;
50
50
  };
51
51
 
52
+ export function normalizePointerQuote(quote: string): string {
53
+ return quote.replace(/\s+/g, " ").trim().slice(0, 240);
54
+ }
55
+
56
+ export type PreviewPointMessage = Envelope & {
57
+ type: "backmatter:point";
58
+ on: boolean;
59
+ };
60
+
61
+ export type PreviewGrabMessage = Envelope & {
62
+ type: "backmatter:grab";
63
+ blockId: string;
64
+ blockType: string | null;
65
+ blockIndex: number | null;
66
+ quote: string;
67
+ shift: boolean;
68
+ };
69
+
70
+ export type PreviewHighlightMessage = Envelope & {
71
+ type: "backmatter:highlight";
72
+ blockId: string | null;
73
+ };
74
+
52
75
  export type PreviewChannelMessage =
53
76
  | PreviewHelloMessage
54
77
  | PreviewDocMessage
55
78
  | PreviewNavigateMessage
56
79
  | PreviewRemovedMessage
57
80
  | PreviewStatusMessage
81
+ | PreviewPointMessage
82
+ | PreviewGrabMessage
83
+ | PreviewHighlightMessage
58
84
  | PreviewHotkeyMessage;
59
85
 
60
86
  export type PreviewTransport = {
@@ -139,6 +165,36 @@ export function parsePreviewMessage(data: unknown): PreviewChannelMessage | null
139
165
  ...(typeof data.message === "string" ? { message: data.message } : {}),
140
166
  };
141
167
  }
168
+ case "backmatter:point": {
169
+ if (typeof data.on !== "boolean") return null;
170
+ return { type: "backmatter:point", ...envelope, on: data.on };
171
+ }
172
+ case "backmatter:grab": {
173
+ if (typeof data.blockId !== "string" || data.blockId.length === 0) return null;
174
+ if (data.blockType !== null && typeof data.blockType !== "string") return null;
175
+ if (
176
+ data.blockIndex !== null &&
177
+ (typeof data.blockIndex !== "number" || !Number.isInteger(data.blockIndex) || data.blockIndex < 0)
178
+ ) {
179
+ return null;
180
+ }
181
+ if (typeof data.quote !== "string" || typeof data.shift !== "boolean") return null;
182
+ return {
183
+ type: "backmatter:grab",
184
+ ...envelope,
185
+ blockId: data.blockId,
186
+ blockType: data.blockType,
187
+ blockIndex: data.blockIndex,
188
+ quote: normalizePointerQuote(data.quote),
189
+ shift: data.shift,
190
+ };
191
+ }
192
+ case "backmatter:highlight": {
193
+ if (data.blockId !== null && (typeof data.blockId !== "string" || data.blockId.length === 0)) {
194
+ return null;
195
+ }
196
+ return { type: "backmatter:highlight", ...envelope, blockId: data.blockId };
197
+ }
142
198
  default:
143
199
  return null;
144
200
  }
@@ -5,9 +5,11 @@
5
5
  // that only a tombstone (`raw: ""`) from SSE still wins.
6
6
 
7
7
  import {
8
+ normalizePointerQuote,
8
9
  samePreviewRef,
9
10
  type PreviewChannelMessage,
10
11
  type PreviewChannelRef,
12
+ type PreviewGrabMessage,
11
13
  type PreviewTransport,
12
14
  } from "./preview-channel";
13
15
 
@@ -35,6 +37,8 @@ export type PreviewPageHandlers = {
35
37
  onError?: (message: string) => void;
36
38
  onRemoved?: () => void;
37
39
  onNavigate?: (path: string) => void;
40
+ onPoint?: (on: boolean) => void;
41
+ onHighlight?: (blockId: string | null) => void;
38
42
  };
39
43
 
40
44
  type Job = {
@@ -47,6 +51,8 @@ export type PreviewPageSession = {
47
51
  close(): void;
48
52
  /** True after the first `doc` for this page's ref. */
49
53
  editorAttached(): boolean;
54
+ grab(grab: Omit<PreviewGrabMessage, "type" | "v" | "ref">): void;
55
+ point(on: boolean): void;
50
56
  };
51
57
 
52
58
  /**
@@ -154,6 +160,12 @@ export function createPreviewPage(options: {
154
160
  case "backmatter:navigate":
155
161
  handlers.onNavigate?.(message.path);
156
162
  break;
163
+ case "backmatter:point":
164
+ handlers.onPoint?.(message.on);
165
+ break;
166
+ case "backmatter:highlight":
167
+ handlers.onHighlight?.(message.blockId);
168
+ break;
157
169
  default:
158
170
  break;
159
171
  }
@@ -186,6 +198,18 @@ export function createPreviewPage(options: {
186
198
 
187
199
  return {
188
200
  editorAttached: () => attached,
201
+ grab(grab) {
202
+ send({
203
+ type: "backmatter:grab",
204
+ v: 1,
205
+ ref: options.ref,
206
+ ...grab,
207
+ quote: normalizePointerQuote(grab.quote),
208
+ });
209
+ },
210
+ point(on) {
211
+ send({ type: "backmatter:point", v: 1, ref: options.ref, on });
212
+ },
189
213
  close() {
190
214
  if (closed) return;
191
215
  closed = true;
@@ -215,6 +239,8 @@ export type PreviewEditorSession = {
215
239
  setDoc(doc: PreviewRenderDoc): void;
216
240
  navigate(path: string): void;
217
241
  removed(): void;
242
+ point(on: boolean): void;
243
+ highlight(blockId: string | null): void;
218
244
  close(): void;
219
245
  };
220
246
 
@@ -225,7 +251,9 @@ export type PreviewEditorSession = {
225
251
  export function createPreviewEditor(options: {
226
252
  transport: PreviewTransport;
227
253
  ref: PreviewChannelRef | (() => PreviewChannelRef);
228
- onHello?: (ref: PreviewChannelRef) => void;
254
+ onHello?: (ref: PreviewChannelRef, capabilities?: string[]) => void;
255
+ onGrab?: (grab: PreviewGrabMessage) => void;
256
+ onPoint?: (on: boolean) => void;
229
257
  onStatus?: (status: { seq: number; state: "rendered" | "error"; message?: string }) => void;
230
258
  schedule?: PreviewSchedule;
231
259
  }): PreviewEditorSession {
@@ -254,14 +282,21 @@ export function createPreviewEditor(options: {
254
282
 
255
283
  const unsubscribe = options.transport.subscribe((message) => {
256
284
  if (closed) return;
257
- if (message.type === "backmatter:hello") options.onHello?.(message.ref);
258
- if (message.type === "backmatter:status" && samePreviewRef(message.ref, currentRef())) {
285
+ if (message.type === "backmatter:hello") {
286
+ options.onHello?.(message.ref, message.capabilities);
287
+ return;
288
+ }
289
+ if (message.type === "backmatter:hotkey") return;
290
+ if (!samePreviewRef(message.ref, currentRef())) return;
291
+ if (message.type === "backmatter:status") {
259
292
  options.onStatus?.({
260
293
  seq: message.seq,
261
294
  state: message.state,
262
295
  ...(message.message ? { message: message.message } : {}),
263
296
  });
264
297
  }
298
+ if (message.type === "backmatter:grab") options.onGrab?.(message);
299
+ if (message.type === "backmatter:point") options.onPoint?.(message.on);
265
300
  });
266
301
 
267
302
  return {
@@ -279,6 +314,14 @@ export function createPreviewEditor(options: {
279
314
  if (closed) return;
280
315
  options.transport.send({ type: "backmatter:removed", v: 1, ref: currentRef() });
281
316
  },
317
+ point(on) {
318
+ if (closed) return;
319
+ options.transport.send({ type: "backmatter:point", v: 1, ref: currentRef(), on });
320
+ },
321
+ highlight(blockId) {
322
+ if (closed) return;
323
+ options.transport.send({ type: "backmatter:highlight", v: 1, ref: currentRef(), blockId });
324
+ },
282
325
  close() {
283
326
  if (closed) return;
284
327
  closed = true;
@@ -0,0 +1,63 @@
1
+ // packages/core/src/preview-pointer.ts
2
+
3
+ export type PointerArmSource = "editor" | "devbar";
4
+
5
+ export type PointerHit = {
6
+ blockId: string | null;
7
+ blockType: string | null;
8
+ blockIndex: number | null;
9
+ quote: string;
10
+ shift: boolean;
11
+ };
12
+
13
+ export type RoutedGrab = {
14
+ blockId: string;
15
+ blockType: string | null;
16
+ blockIndex: number | null;
17
+ quote: string;
18
+ shift: boolean;
19
+ };
20
+
21
+ export type PointerRoute =
22
+ | { to: "editor"; grab: RoutedGrab }
23
+ | { to: "devbar"; grab: Omit<PointerHit, "shift"> }
24
+ | { to: "none" };
25
+
26
+ export function routePointerHit(source: PointerArmSource | null, hit: PointerHit): PointerRoute {
27
+ if (hit.shift && hit.blockId) {
28
+ return {
29
+ to: "editor",
30
+ grab: {
31
+ blockId: hit.blockId,
32
+ blockType: hit.blockType,
33
+ blockIndex: hit.blockIndex,
34
+ quote: hit.quote,
35
+ shift: true,
36
+ },
37
+ };
38
+ }
39
+ if (source === "devbar") {
40
+ return {
41
+ to: "devbar",
42
+ grab: {
43
+ blockId: hit.blockId,
44
+ blockType: hit.blockType,
45
+ blockIndex: hit.blockIndex,
46
+ quote: hit.quote,
47
+ },
48
+ };
49
+ }
50
+ if (source === "editor" && hit.blockId) {
51
+ return {
52
+ to: "editor",
53
+ grab: {
54
+ blockId: hit.blockId,
55
+ blockType: hit.blockType,
56
+ blockIndex: hit.blockIndex,
57
+ quote: hit.quote,
58
+ shift: false,
59
+ },
60
+ };
61
+ }
62
+ return { to: "none" };
63
+ }
package/src/preview.ts CHANGED
@@ -304,6 +304,7 @@ export {
304
304
  createWindowPreviewTransport,
305
305
  isAllowedPreviewOrigin,
306
306
  normalizePreviewOrigin,
307
+ normalizePointerQuote,
307
308
  parsePreviewMessage,
308
309
  PREVIEW_CHANNEL_VERSION,
309
310
  samePreviewRef,
@@ -313,14 +314,26 @@ export type {
313
314
  PreviewChannelMessage,
314
315
  PreviewChannelRef,
315
316
  PreviewDocMessage,
317
+ PreviewGrabMessage,
316
318
  PreviewHelloMessage,
319
+ PreviewHighlightMessage,
317
320
  PreviewMessageEvent,
318
321
  PreviewNavigateMessage,
322
+ PreviewPointMessage,
319
323
  PreviewRemovedMessage,
320
324
  PreviewStatusMessage,
321
325
  PreviewTransport,
322
326
  PreviewWindowLike,
323
327
  } from "./preview-channel";
328
+ export {
329
+ routePointerHit,
330
+ } from "./preview-pointer";
331
+ export type {
332
+ PointerArmSource,
333
+ PointerHit,
334
+ PointerRoute,
335
+ RoutedGrab,
336
+ } from "./preview-pointer";
324
337
  export { createPreviewEditor, createPreviewPage } from "./preview-client";
325
338
  export type {
326
339
  PreviewEditorSession,