@dxos/ui-editor 0.9.0 → 0.9.1-staging.ee54ba693a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/lib/browser/index.mjs +2533 -1744
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +2533 -1744
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/defaults.d.ts.map +1 -1
  8. package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -1
  9. package/dist/types/src/extensions/automerge/automerge.d.ts +6 -2
  10. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  11. package/dist/types/src/extensions/automerge/cursor.d.ts +6 -2
  12. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  13. package/dist/types/src/extensions/automerge/sync.d.ts +2 -2
  14. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  15. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  16. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  17. package/dist/types/src/extensions/blocks.d.ts.map +1 -1
  18. package/dist/types/src/extensions/busy.d.ts +13 -0
  19. package/dist/types/src/extensions/busy.d.ts.map +1 -0
  20. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  21. package/dist/types/src/extensions/dnd.d.ts +5 -1
  22. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  23. package/dist/types/src/extensions/factories.d.ts +2 -2
  24. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  25. package/dist/types/src/extensions/fader.d.ts.map +1 -0
  26. package/dist/types/src/extensions/index.d.ts +6 -1
  27. package/dist/types/src/extensions/index.d.ts.map +1 -1
  28. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
  29. package/dist/types/src/extensions/marker.d.ts +40 -0
  30. package/dist/types/src/extensions/marker.d.ts.map +1 -0
  31. package/dist/types/src/extensions/pending/index.d.ts +3 -0
  32. package/dist/types/src/extensions/pending/index.d.ts.map +1 -0
  33. package/dist/types/src/extensions/pending/pending-text-stream.d.ts +75 -0
  34. package/dist/types/src/extensions/pending/pending-text-stream.d.ts.map +1 -0
  35. package/dist/types/src/extensions/pending/pending-text-stream.test.d.ts +2 -0
  36. package/dist/types/src/extensions/pending/pending-text-stream.test.d.ts.map +1 -0
  37. package/dist/types/src/extensions/pending/pending-text.d.ts +46 -0
  38. package/dist/types/src/extensions/pending/pending-text.d.ts.map +1 -0
  39. package/dist/types/src/extensions/pending/pending-text.test.d.ts +2 -0
  40. package/dist/types/src/extensions/pending/pending-text.test.d.ts.map +1 -0
  41. package/dist/types/src/extensions/pos.d.ts +49 -0
  42. package/dist/types/src/extensions/pos.d.ts.map +1 -0
  43. package/dist/types/src/extensions/pos.test.d.ts +2 -0
  44. package/dist/types/src/extensions/pos.test.d.ts.map +1 -0
  45. package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +1 -1
  46. package/dist/types/src/extensions/selection.d.ts +12 -1
  47. package/dist/types/src/extensions/selection.d.ts.map +1 -1
  48. package/dist/types/src/extensions/selection.test.d.ts +2 -0
  49. package/dist/types/src/extensions/selection.test.d.ts.map +1 -0
  50. package/dist/types/src/extensions/tags/index.d.ts +2 -2
  51. package/dist/types/src/extensions/tags/index.d.ts.map +1 -1
  52. package/dist/types/src/extensions/tags/stub.d.ts +35 -0
  53. package/dist/types/src/extensions/tags/stub.d.ts.map +1 -0
  54. package/dist/types/src/extensions/tags/widgets/anchor.d.ts +13 -0
  55. package/dist/types/src/extensions/tags/widgets/anchor.d.ts.map +1 -0
  56. package/dist/types/src/extensions/tags/widgets/index.d.ts +2 -0
  57. package/dist/types/src/extensions/tags/widgets/index.d.ts.map +1 -0
  58. package/dist/types/src/extensions/tags/xml-tags.d.ts +9 -5
  59. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
  60. package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
  61. package/dist/types/src/extensions/typewriter.test.d.ts.map +1 -0
  62. package/dist/types/src/index.d.ts +1 -1
  63. package/dist/types/src/index.d.ts.map +1 -1
  64. package/dist/types/src/styles/theme.d.ts.map +1 -1
  65. package/dist/types/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +37 -35
  67. package/src/defaults.ts +3 -1
  68. package/src/extensions/autocomplete/placeholder.ts +11 -1
  69. package/src/extensions/automerge/automerge.test.tsx +25 -53
  70. package/src/extensions/automerge/automerge.ts +7 -3
  71. package/src/extensions/automerge/cursor.ts +7 -2
  72. package/src/extensions/automerge/sync.ts +2 -2
  73. package/src/extensions/automerge/update-automerge.ts +2 -2
  74. package/src/extensions/blocks.ts +36 -36
  75. package/src/extensions/busy.ts +32 -0
  76. package/src/extensions/comments.ts +5 -9
  77. package/src/extensions/dnd.ts +127 -3
  78. package/src/extensions/factories.test.ts +1 -1
  79. package/src/extensions/factories.ts +2 -2
  80. package/src/extensions/index.ts +6 -1
  81. package/src/extensions/markdown/styles.ts +7 -6
  82. package/src/extensions/marker.ts +143 -0
  83. package/src/extensions/pending/index.ts +6 -0
  84. package/src/extensions/pending/pending-text-stream.test.ts +133 -0
  85. package/src/extensions/pending/pending-text-stream.ts +269 -0
  86. package/src/extensions/pending/pending-text.test.ts +134 -0
  87. package/src/extensions/pending/pending-text.ts +264 -0
  88. package/src/extensions/pos.test.ts +97 -0
  89. package/src/extensions/pos.ts +206 -0
  90. package/src/extensions/scrolling/auto-scroll.ts +12 -8
  91. package/src/extensions/selection.test.ts +26 -0
  92. package/src/extensions/selection.ts +14 -15
  93. package/src/extensions/tags/DESIGN.md +58 -0
  94. package/src/extensions/tags/index.ts +2 -2
  95. package/src/extensions/tags/stub.ts +80 -0
  96. package/src/extensions/tags/widgets/anchor.ts +30 -0
  97. package/src/extensions/{preview → tags/widgets}/index.ts +1 -1
  98. package/src/extensions/tags/xml-tags.ts +116 -74
  99. package/src/extensions/tags/xml-util.test.ts +1 -1
  100. package/src/index.ts +1 -1
  101. package/src/styles/theme.ts +0 -1
  102. package/dist/types/src/extensions/preview/index.d.ts +0 -2
  103. package/dist/types/src/extensions/preview/index.d.ts.map +0 -1
  104. package/dist/types/src/extensions/preview/preview.d.ts +0 -34
  105. package/dist/types/src/extensions/preview/preview.d.ts.map +0 -1
  106. package/dist/types/src/extensions/tags/fader.d.ts.map +0 -1
  107. package/dist/types/src/extensions/tags/typewriter.d.ts.map +0 -1
  108. package/dist/types/src/extensions/tags/typewriter.test.d.ts.map +0 -1
  109. package/src/extensions/preview/preview.ts +0 -242
  110. /package/dist/types/src/extensions/{tags/fader.d.ts → fader.d.ts} +0 -0
  111. /package/dist/types/src/extensions/{tags/typewriter.d.ts → typewriter.d.ts} +0 -0
  112. /package/dist/types/src/extensions/{tags/typewriter.test.d.ts → typewriter.test.d.ts} +0 -0
  113. /package/src/extensions/{tags/fader.ts → fader.ts} +0 -0
  114. /package/src/extensions/{tags/typewriter.test.ts → typewriter.test.ts} +0 -0
  115. /package/src/extensions/{tags/typewriter.ts → typewriter.ts} +0 -0
@@ -15,13 +15,12 @@ import {
15
15
  } from '@codemirror/view';
16
16
  import { type FunctionComponent } from 'react';
17
17
 
18
- import { invariant } from '@dxos/invariant';
19
18
  import { log } from '@dxos/log';
20
- import { Domino } from '@dxos/ui';
21
19
 
22
20
  import { type Range } from '../../types';
23
21
  import { decorationSetToArray } from '../../util';
24
22
  import { crawlerLineEffect } from '../scrolling';
23
+ import { StubWidget, type XmlWidgetNotifier } from './stub';
25
24
  import { nodeToJson } from './xml-util';
26
25
 
27
26
  /**
@@ -98,6 +97,13 @@ export type XmlWidgetDef = {
98
97
  * Streaming tags use `cm-xml-<from>` so the same portal id is kept when the closing tag arrives.
99
98
  */
100
99
  Component?: FunctionComponent<XmlWidgetProps>;
100
+
101
+ /**
102
+ * URL scheme prefixes that trigger this widget on Link/Image markdown nodes.
103
+ * Example: `[‘dxn:’, ‘echo:’]` matches `[label](dxn:…)` and `![label](echo:…)`.
104
+ * Block widgets are created for Image nodes; inline widgets for Link nodes.
105
+ */
106
+ urlSchemes?: string[];
101
107
  };
102
108
 
103
109
  export type XmlWidgetRegistry = Record<string, XmlWidgetDef>;
@@ -145,10 +151,7 @@ export type XmlWidgetState = {
145
151
  Component: FunctionComponent<XmlWidgetProps>;
146
152
  };
147
153
 
148
- export interface XmlWidgetNotifier {
149
- mounted(widget: XmlWidgetState): void;
150
- unmounted(id: string): void;
151
- }
154
+ export type { XmlWidgetNotifier };
152
155
 
153
156
  /**
154
157
  * Context state.
@@ -207,7 +210,7 @@ export type XmlTagsOptions = {
207
210
  * Basic mechanism:
208
211
  * - Decorations are created from XML tags that matched the provided Widget registry.
209
212
  * - Native widgets are rendered inline.
210
- * - React/Solid widgets are rendered in portals outside of the editor via the PlaceholderWidget.
213
+ * - React/Solid widgets are rendered in portals outside of the editor via the StubWidget.
211
214
  * - Widget state can be update via effects.
212
215
  * - NOTE: Widget state may be updated BEFORE the widget is mounted.
213
216
  */
@@ -244,6 +247,18 @@ const createWidgetMap = (setWidgets?: (widgets: XmlWidgetState[]) => void): XmlW
244
247
  widgets.delete(id);
245
248
  setWidgets?.([...widgets.values()]);
246
249
  },
250
+ reconcile: (liveIds: Set<string>) => {
251
+ let changed = false;
252
+ for (const id of [...widgets.keys()]) {
253
+ if (!liveIds.has(id)) {
254
+ widgets.delete(id);
255
+ changed = true;
256
+ }
257
+ }
258
+ if (changed) {
259
+ setWidgets?.([...widgets.values()]);
260
+ }
261
+ },
247
262
  } satisfies XmlWidgetNotifier;
248
263
 
249
264
  return notifier;
@@ -355,6 +370,18 @@ const createWidgetUpdatePlugin = (
355
370
  const widgetStateMap = update.state.field(widgetStateMapStateField);
356
371
  const { decorations } = update.state.field(widgetDecorationsField);
357
372
 
373
+ // Prune widgets orphaned by a rebuild (position-keyed ids change when an edit shifts a node).
374
+ if (update.docChanged) {
375
+ const liveIds = new Set<string>();
376
+ for (const range of decorationSetToArray(decorations)) {
377
+ const widget = range.value?.spec?.widget;
378
+ if (widget instanceof StubWidget) {
379
+ liveIds.add(widget.id);
380
+ }
381
+ }
382
+ notifier.reconcile(liveIds);
383
+ }
384
+
358
385
  // Check for widget update effects and re-render widgets.
359
386
  for (const effect of update.transactions.flatMap((tr) => tr.effects)) {
360
387
  if (effect.is(xmlTagUpdateEffect)) {
@@ -366,7 +393,7 @@ const createWidgetUpdatePlugin = (
366
393
  const widget = deco?.spec?.widget;
367
394
 
368
395
  // NOTE: If the widget has not yet been mounted, then the root will be null.
369
- if (widget && widget instanceof PlaceholderWidget && widget.id === effect.value.id && widget.root) {
396
+ if (widget && widget instanceof StubWidget && widget.id === effect.value.id && widget.root) {
370
397
  const props = { ...widget.props, ...widgetState };
371
398
  notifier.mounted({ id: widget.id, props, root: widget.root, Component: widget.Component });
372
399
  }
@@ -381,17 +408,30 @@ const createWidgetUpdatePlugin = (
381
408
  * Builds and maintains decorations for XML widgets.
382
409
  * Must be a StateField because block decorations cannot be provided via ViewPlugin.
383
410
  */
384
- const createWidgetDecorationsField = (registry: XmlWidgetRegistry = {}, notifier: XmlWidgetNotifier) =>
385
- StateField.define<WidgetDecorationSet>({
411
+ const createWidgetDecorationsField = (registry: XmlWidgetRegistry = {}, notifier: XmlWidgetNotifier) => {
412
+ // Multiple registry entries may claim the same URL scheme (e.g. one block, one inline).
413
+ const urlSchemeMap: Map<string, [string, XmlWidgetDef][]> = new Map();
414
+ for (const [tag, def] of Object.entries(registry)) {
415
+ for (const scheme of def.urlSchemes ?? []) {
416
+ const existing = urlSchemeMap.get(scheme);
417
+ if (existing) {
418
+ existing.push([tag, def]);
419
+ } else {
420
+ urlSchemeMap.set(scheme, [[tag, def]]);
421
+ }
422
+ }
423
+ }
424
+
425
+ return StateField.define<WidgetDecorationSet>({
386
426
  create: (state) => {
387
- return buildDecorations(state, { from: 0, to: state.doc.length }, registry, notifier);
427
+ return buildDecorations(state, { from: 0, to: state.doc.length }, registry, notifier, urlSchemeMap);
388
428
  },
389
429
  update: ({ from, streamingFrom, decorations }, tr) => {
390
430
  // Check for reset effect.
391
431
  for (const effect of tr.effects) {
392
432
  if (effect.is(xmlTagResetEffect)) {
393
433
  if (tr.docChanged) {
394
- return buildDecorations(tr.state, { from: 0, to: tr.state.doc.length }, registry, notifier);
434
+ return buildDecorations(tr.state, { from: 0, to: tr.state.doc.length }, registry, notifier, urlSchemeMap);
395
435
  }
396
436
  return { from: 0, decorations: Decoration.none };
397
437
  }
@@ -404,11 +444,17 @@ const createWidgetDecorationsField = (registry: XmlWidgetRegistry = {}, notifier
404
444
  if (reset) {
405
445
  log('document reset', { from, to: state.doc.length });
406
446
  // Full rebuild from start.
407
- return buildDecorations(state, { from: 0, to: state.doc.length }, registry, notifier);
447
+ return buildDecorations(state, { from: 0, to: state.doc.length }, registry, notifier, urlSchemeMap);
408
448
  } else {
409
449
  // Rebuild from the streaming tag start (if active) so the tree walk can detect completion.
410
450
  const rebuildFrom = streamingFrom ?? from;
411
- const result = buildDecorations(state, { from: rebuildFrom, to: state.doc.length }, registry, notifier);
451
+ const result = buildDecorations(
452
+ state,
453
+ { from: rebuildFrom, to: state.doc.length },
454
+ registry,
455
+ notifier,
456
+ urlSchemeMap,
457
+ );
412
458
  return {
413
459
  from: result.from,
414
460
  streamingFrom: result.streamingFrom,
@@ -428,6 +474,7 @@ const createWidgetDecorationsField = (registry: XmlWidgetRegistry = {}, notifier
428
474
  EditorView.atomicRanges.of((view) => view.state.field(field).decorations || Decoration.none),
429
475
  ],
430
476
  });
477
+ };
431
478
 
432
479
  /**
433
480
  * Creates widget decorations for XML tags in the document using the syntax tree.
@@ -438,6 +485,7 @@ const buildDecorations = (
438
485
  range: Range,
439
486
  registry: XmlWidgetRegistry,
440
487
  notifier: XmlWidgetNotifier,
488
+ urlSchemeMap: Map<string, [string, XmlWidgetDef][]>,
441
489
  ): WidgetDecorationSet => {
442
490
  const context = state.field(widgetContextStateField, false);
443
491
  const widgetStateMap = state.field(widgetStateMapStateField, false) ?? {};
@@ -488,7 +536,7 @@ const buildDecorations = (
488
536
  const widget: WidgetType | undefined = factory
489
537
  ? (factory(props) ?? undefined)
490
538
  : Component
491
- ? new PlaceholderWidget(widgetId, Component, props, notifier)
539
+ ? new StubWidget(widgetId, Component, props, notifier, false, !!block)
492
540
  : undefined;
493
541
 
494
542
  // Add decoration.
@@ -517,6 +565,58 @@ const buildDecorations = (
517
565
  // Don't descend into children.
518
566
  return false;
519
567
  }
568
+
569
+ case 'Image':
570
+ case 'Link': {
571
+ if (urlSchemeMap.size === 0) {
572
+ return false;
573
+ }
574
+ const urlNode = node.node.getChild('URL');
575
+ const markNodes = node.node.getChildren('LinkMark');
576
+ if (!urlNode || markNodes.length < 2) {
577
+ return false;
578
+ }
579
+ const dxn = state.sliceDoc(urlNode.from, urlNode.to);
580
+ const isBlock = node.type.name === 'Image';
581
+ const defs = [...urlSchemeMap.entries()].find(([scheme]) => dxn.startsWith(scheme))?.[1];
582
+ if (!defs) {
583
+ return false;
584
+ }
585
+ const matched = defs.find(([, d]: [string, XmlWidgetDef]) => !!d.block === isBlock);
586
+ if (!matched) {
587
+ return false;
588
+ }
589
+ const [tag, def] = matched;
590
+ const label = state.sliceDoc(markNodes[0].to, markNodes[1].from);
591
+ const nodeRange = { from: node.node.from, to: node.node.to };
592
+ const widgetId = `cm-url-${node.from}-${dxn}`;
593
+ const widgetState = widgetStateMap[widgetId];
594
+ const props: XmlWidgetProps = {
595
+ id: widgetId,
596
+ _tag: node.type.name === 'Image' ? 'image' : 'link',
597
+ range: nodeRange,
598
+ context,
599
+ label,
600
+ dxn,
601
+ block: isBlock,
602
+ suggest: isBlock,
603
+ ...widgetState,
604
+ };
605
+ const widget: WidgetType | undefined = def.factory
606
+ ? (def.factory(props) ?? undefined)
607
+ : def.Component
608
+ ? new StubWidget(widgetId, def.Component, props, notifier, false, isBlock)
609
+ : undefined;
610
+ if (widget) {
611
+ builder.add(
612
+ nodeRange.from,
613
+ nodeRange.to,
614
+ Decoration.replace({ widget, block: isBlock, atomic: true, inclusive: true, tag }),
615
+ );
616
+ last = nodeRange.to - 1;
617
+ }
618
+ return false;
619
+ }
520
620
  }
521
621
  },
522
622
  });
@@ -567,7 +667,7 @@ const buildDecorations = (
567
667
  const widget: WidgetType | undefined = def.factory
568
668
  ? (def.factory(mergedProps) ?? undefined)
569
669
  : def.Component
570
- ? new PlaceholderWidget(widgetId, def.Component, mergedProps, notifier, true)
670
+ ? new StubWidget(widgetId, def.Component, mergedProps, notifier, true)
571
671
  : undefined;
572
672
 
573
673
  if (widget) {
@@ -598,61 +698,3 @@ const buildDecorations = (
598
698
 
599
699
  return { from: last, streamingFrom, decorations: builder.finish() };
600
700
  };
601
-
602
- /**
603
- * Placeholder for widgets.
604
- */
605
- class PlaceholderWidget<TProps extends XmlWidgetProps> extends WidgetType {
606
- #root: HTMLElement | null = null;
607
- #view: EditorView | undefined;
608
-
609
- constructor(
610
- readonly id: string,
611
- readonly Component: FunctionComponent<TProps>,
612
- readonly props: TProps,
613
- readonly notifier: XmlWidgetNotifier,
614
- readonly streaming?: boolean,
615
- ) {
616
- super();
617
- invariant(id);
618
- }
619
-
620
- get root(): HTMLElement | null {
621
- return this.#root;
622
- }
623
-
624
- override eq(other: this) {
625
- // Streaming widgets always need updating (content changes on each tick).
626
- if (this.streaming) {
627
- return false;
628
- }
629
-
630
- return this.id === other.id;
631
- }
632
-
633
- override ignoreEvent() {
634
- return true;
635
- }
636
-
637
- override toDOM(view: EditorView) {
638
- this.#view = view;
639
- // NOTE: Set min-height to avoid jumps while scrolling.
640
- this.#root = Domino.of('div').classNames('min-h-[24px]').root;
641
- const props = Object.assign({}, this.props, { view }) as TProps;
642
- this.notifier.mounted({ id: this.id, root: this.#root, props, Component: this.Component });
643
- return this.#root;
644
- }
645
-
646
- override updateDOM(dom: HTMLElement) {
647
- this.#root = dom;
648
- const props = Object.assign({}, this.props, { view: this.#view }) as TProps;
649
- this.notifier.mounted({ id: this.id, root: this.#root, props, Component: this.Component });
650
- return true;
651
- }
652
-
653
- override destroy(_dom: HTMLElement) {
654
- this.notifier.unmounted(this.id);
655
- this.#root = null;
656
- this.#view = undefined;
657
- }
658
- }
@@ -11,7 +11,7 @@ import { trim } from '@dxos/util';
11
11
  import { extendedMarkdown } from './extended-markdown';
12
12
  import TEXT from './testing/text.md?raw';
13
13
  import { xmlTags } from './xml-tags';
14
- import { nodeToJson, type Tag } from './xml-util';
14
+ import { type Tag, nodeToJson } from './xml-util';
15
15
 
16
16
  type ParsedElement = Tag & {
17
17
  /** Element spans the entire document (opening through closing tag). */
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- export { type Extension, EditorState } from '@codemirror/state';
5
+ export { EditorState, type Extension } from '@codemirror/state';
6
6
  export { EditorView, keymap } from '@codemirror/view';
7
7
  export { tags } from '@lezer/highlight';
8
8
 
@@ -151,7 +151,6 @@ export const baseTheme = EditorView.baseTheme({
151
151
  '.cm-gutter': {},
152
152
  '.cm-gutter.cm-lineNumbers': {
153
153
  paddingRight: '4px',
154
- borderRight: '1px solid var(--color-subdued-separator)',
155
154
  color: 'var(--color-subdued)',
156
155
  },
157
156
  '.cm-gutter.cm-lineNumbers .cm-gutterElement': {
@@ -1,2 +0,0 @@
1
- export * from './preview';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/preview/index.ts"],"names":[],"mappings":"AAIA,cAAc,WAAW,CAAC"}
@@ -1,34 +0,0 @@
1
- import { type EditorState, type Extension } from '@codemirror/state';
2
- import { type SyntaxNode } from '@lezer/common';
3
- import { type Database } from '@dxos/echo';
4
- export type PreviewBlock = {
5
- link: PreviewLinkRef;
6
- el: HTMLElement;
7
- };
8
- export type PreviewLinkRef = {
9
- suggest?: boolean;
10
- block?: boolean;
11
- label: string;
12
- dxn: string;
13
- };
14
- export type PreviewLinkTarget = {
15
- label: string;
16
- text?: string;
17
- object?: any;
18
- };
19
- export type PreviewOptions = {
20
- db?: Database.Database;
21
- addBlockContainer?: (block: PreviewBlock) => void;
22
- removeBlockContainer?: (block: PreviewBlock) => void;
23
- };
24
- /**
25
- * Create preview decorations.
26
- */
27
- export declare const preview: (options?: PreviewOptions) => Extension;
28
- /**
29
- * Link references.
30
- * [Label](echo:/123) Inline reference
31
- * ![Label](echo:/123) Block reference
32
- */
33
- export declare const getLinkRef: (state: EditorState, node: SyntaxNode) => PreviewLinkRef | undefined;
34
- //# sourceMappingURL=preview.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/preview/preview.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAA4C,MAAM,mBAAmB,CAAC;AAE/G,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAE,KAAK,QAAQ,EAAU,MAAM,YAAY,CAAC;AAGnD,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,EAAE,EAAE,WAAW,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,GAAG,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC;IACvB,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAClD,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CACtD,CAAC;AAIF;;GAEG;AACH,eAAO,MAAM,OAAO,aAAa,cAAc,KAAQ,SA8BtD,CAAC;AA0FF;;;;GAIG;AACH,eAAO,MAAM,UAAU,UAAW,WAAW,QAAQ,UAAU,KAAG,cAAc,GAAG,SAclF,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"fader.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/tags/fader.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAA2B,MAAM,mBAAmB,CAAC;AAO5E,MAAM,MAAM,YAAY,GAAG;IACzB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,KAAK,aAAa,YAAY,KAAQ,SA6KlD,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"typewriter.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/tags/typewriter.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,SAAS,EAIf,MAAM,mBAAmB,CAAC;AAK3B,4FAA4F;AAC5F,eAAO,MAAM,gBAAgB,qDAA+B,CAAC;AAE7D;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,sDAAgC,CAAC;AAiCtE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,qEAAqE;IACrE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,mFAAmF;IACnF,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,UAAU,aAAa,iBAAiB,KAAQ,SA0N5D,CAAC;AA6MF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,WAAY,MAAM,qBAAc,MAoD5D,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"typewriter.test.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/tags/typewriter.test.ts"],"names":[],"mappings":""}
@@ -1,242 +0,0 @@
1
- //
2
- // Copyright 2023 DXOS.org
3
- //
4
-
5
- import { syntaxTree } from '@codemirror/language';
6
- import { type EditorState, type Extension, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
7
- import { Decoration, type DecorationSet, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
8
- import { type SyntaxNode } from '@lezer/common';
9
-
10
- import { type Database, Entity } from '@dxos/echo';
11
- import { EID, URI } from '@dxos/keys';
12
-
13
- export type PreviewBlock = {
14
- link: PreviewLinkRef;
15
- el: HTMLElement;
16
- };
17
-
18
- export type PreviewLinkRef = {
19
- suggest?: boolean;
20
- block?: boolean;
21
- label: string;
22
- dxn: string;
23
- };
24
-
25
- export type PreviewLinkTarget = {
26
- label: string;
27
- text?: string;
28
- object?: any;
29
- };
30
-
31
- export type PreviewOptions = {
32
- db?: Database.Database;
33
- addBlockContainer?: (block: PreviewBlock) => void;
34
- removeBlockContainer?: (block: PreviewBlock) => void;
35
- };
36
-
37
- const labelResolvedEffect = StateEffect.define<void>();
38
-
39
- /**
40
- * Create preview decorations.
41
- */
42
- export const preview = (options: PreviewOptions = {}): Extension => {
43
- // Mutable ref so the StateField's onLoad callback can dispatch into the view.
44
- const viewRef: { current: EditorView | undefined } = { current: undefined };
45
-
46
- return [
47
- // NOTE: Atomic block decorations must be created from a state field, now a widget, otherwise it results in the following error:
48
- // "Block decorations may not be specified via plugins".
49
- StateField.define<DecorationSet>({
50
- create: (state) => buildDecorations(state, options, viewRef),
51
- update: (decorations, tr) => {
52
- if (tr.docChanged || tr.effects.some((effect) => effect.is(labelResolvedEffect))) {
53
- return buildDecorations(tr.state, options, viewRef);
54
- }
55
-
56
- return decorations.map(tr.changes);
57
- },
58
- provide: (field) => [
59
- EditorView.decorations.from(field),
60
- EditorView.atomicRanges.of((view) => view.state.field(field)),
61
- ],
62
- }),
63
- ViewPlugin.define((view) => {
64
- viewRef.current = view;
65
- return {
66
- destroy() {
67
- viewRef.current = undefined;
68
- },
69
- };
70
- }),
71
- ];
72
- };
73
-
74
- /**
75
- * Resolve a DXN to a display label using the database.
76
- */
77
- const resolveLabel = (
78
- db: Database.Database,
79
- dxnStr: string,
80
- viewRef: { current: EditorView | undefined },
81
- ): string | undefined => {
82
- const echoUri = EID.tryParse(dxnStr);
83
- const dxnRef = echoUri ?? (dxnStr.startsWith('dxn:') ? URI.make(dxnStr) : undefined);
84
- if (!dxnRef) {
85
- return;
86
- }
87
-
88
- const ref = db.makeRef(dxnRef);
89
- const target = ref.target;
90
- if (target) {
91
- return Entity.getLabel(target);
92
- }
93
-
94
- // Object not loaded yet — schedule a decoration rebuild when it resolves.
95
- void ref.tryLoad().then(() => {
96
- viewRef.current?.dispatch({ effects: labelResolvedEffect.of(undefined) });
97
- });
98
- };
99
-
100
- /**
101
- * Echo references are represented as markdown reference links.
102
- * https://www.markdownguide.org/basic-syntax/#reference-style-links
103
- */
104
- const buildDecorations = (
105
- state: EditorState,
106
- options: PreviewOptions,
107
- viewRef: { current: EditorView | undefined },
108
- ): DecorationSet => {
109
- const builder = new RangeSetBuilder<Decoration>();
110
-
111
- syntaxTree(state).iterate({
112
- enter: (node) => {
113
- switch (node.name) {
114
- //
115
- // Inline widget.
116
- // [Label](echo:/123)
117
- //
118
- case 'Link': {
119
- const link = getLinkRef(state, node.node);
120
- if (link) {
121
- const resolved = options.db ? resolveLabel(options.db, link.dxn, viewRef) : undefined;
122
- const displayLink = resolved ? { ...link, label: resolved } : link;
123
- builder.add(
124
- node.from,
125
- node.to,
126
- Decoration.replace({
127
- widget: new PreviewInlineWidget(options, displayLink),
128
- side: 1,
129
- }),
130
- );
131
- }
132
- return false;
133
- }
134
-
135
- //
136
- // Block widget (transclusion).
137
- // ![Label](echo:/123)
138
- //
139
- case 'Image': {
140
- if (options.addBlockContainer && options.removeBlockContainer) {
141
- const link = getLinkRef(state, node.node);
142
- if (link) {
143
- builder.add(
144
- node.from,
145
- node.to,
146
- Decoration.replace({
147
- block: true,
148
- widget: new PreviewBlockWidget(options, link),
149
- }),
150
- );
151
- }
152
- }
153
- return false;
154
- }
155
- }
156
- },
157
- });
158
-
159
- return builder.finish();
160
- };
161
-
162
- /**
163
- * Link references.
164
- * [Label](echo:/123) Inline reference
165
- * ![Label](echo:/123) Block reference
166
- */
167
- export const getLinkRef = (state: EditorState, node: SyntaxNode): PreviewLinkRef | undefined => {
168
- const mark = node.getChildren('LinkMark');
169
- const urlNode = node.getChild('URL');
170
- if (mark && urlNode) {
171
- const dxn = state.sliceDoc(urlNode.from, urlNode.to);
172
- if (dxn.startsWith('dxn:') || dxn.startsWith('echo:')) {
173
- const label = state.sliceDoc(mark[0].to, mark[1].from);
174
- return {
175
- block: state.sliceDoc(mark[0].from, mark[0].from + 1) === '!',
176
- label,
177
- dxn,
178
- };
179
- }
180
- }
181
- };
182
-
183
- /**
184
- * Inline widget.
185
- * [Label](echo:/123)
186
- */
187
- class PreviewInlineWidget extends WidgetType {
188
- constructor(
189
- readonly _options: PreviewOptions,
190
- readonly _link: PreviewLinkRef,
191
- ) {
192
- super();
193
- }
194
-
195
- // override ignoreEvent() {
196
- // return false;
197
- // }
198
-
199
- override eq(other: this) {
200
- return this._link.dxn === other._link.dxn && this._link.label === other._link.label;
201
- }
202
-
203
- override toDOM(_view: EditorView) {
204
- const root = document.createElement('dx-anchor');
205
- root.classList.add('dx-tag--anchor');
206
- root.textContent = this._link.label;
207
- root.setAttribute('dxn', this._link.dxn);
208
- return root;
209
- }
210
- }
211
-
212
- /**
213
- * Block widget (e.g., for surfaces).
214
- * ![Label][echo:/123]
215
- */
216
- class PreviewBlockWidget extends WidgetType {
217
- constructor(
218
- readonly _options: PreviewOptions,
219
- readonly _link: PreviewLinkRef,
220
- ) {
221
- super();
222
- }
223
-
224
- // override ignoreEvent() {
225
- // return true;
226
- // }
227
-
228
- override eq(other: this) {
229
- return this._link.dxn === other._link.dxn;
230
- }
231
-
232
- override toDOM(_view: EditorView) {
233
- const root = document.createElement('div');
234
- root.classList.add('cm-preview-block', 'dx-density-md');
235
- this._options.addBlockContainer?.({ link: this._link, el: root });
236
- return root;
237
- }
238
-
239
- override destroy(root: HTMLDivElement) {
240
- this._options.removeBlockContainer?.({ link: this._link, el: root });
241
- }
242
- }
File without changes