@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.
- package/dist/lib/browser/index.mjs +2533 -1744
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +2533 -1744
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts +6 -2
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +6 -2
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts +2 -2
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/blocks.d.ts.map +1 -1
- package/dist/types/src/extensions/busy.d.ts +13 -0
- package/dist/types/src/extensions/busy.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts +5 -1
- package/dist/types/src/extensions/dnd.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +2 -2
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/fader.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +6 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
- package/dist/types/src/extensions/marker.d.ts +40 -0
- package/dist/types/src/extensions/marker.d.ts.map +1 -0
- package/dist/types/src/extensions/pending/index.d.ts +3 -0
- package/dist/types/src/extensions/pending/index.d.ts.map +1 -0
- package/dist/types/src/extensions/pending/pending-text-stream.d.ts +75 -0
- package/dist/types/src/extensions/pending/pending-text-stream.d.ts.map +1 -0
- package/dist/types/src/extensions/pending/pending-text-stream.test.d.ts +2 -0
- package/dist/types/src/extensions/pending/pending-text-stream.test.d.ts.map +1 -0
- package/dist/types/src/extensions/pending/pending-text.d.ts +46 -0
- package/dist/types/src/extensions/pending/pending-text.d.ts.map +1 -0
- package/dist/types/src/extensions/pending/pending-text.test.d.ts +2 -0
- package/dist/types/src/extensions/pending/pending-text.test.d.ts.map +1 -0
- package/dist/types/src/extensions/pos.d.ts +49 -0
- package/dist/types/src/extensions/pos.d.ts.map +1 -0
- package/dist/types/src/extensions/pos.test.d.ts +2 -0
- package/dist/types/src/extensions/pos.test.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +1 -1
- package/dist/types/src/extensions/selection.d.ts +12 -1
- package/dist/types/src/extensions/selection.d.ts.map +1 -1
- package/dist/types/src/extensions/selection.test.d.ts +2 -0
- package/dist/types/src/extensions/selection.test.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/index.d.ts +2 -2
- package/dist/types/src/extensions/tags/index.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/stub.d.ts +35 -0
- package/dist/types/src/extensions/tags/stub.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/widgets/anchor.d.ts +13 -0
- package/dist/types/src/extensions/tags/widgets/anchor.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/widgets/index.d.ts +2 -0
- package/dist/types/src/extensions/tags/widgets/index.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-tags.d.ts +9 -5
- package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
- package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
- package/dist/types/src/extensions/typewriter.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +37 -35
- package/src/defaults.ts +3 -1
- package/src/extensions/autocomplete/placeholder.ts +11 -1
- package/src/extensions/automerge/automerge.test.tsx +25 -53
- package/src/extensions/automerge/automerge.ts +7 -3
- package/src/extensions/automerge/cursor.ts +7 -2
- package/src/extensions/automerge/sync.ts +2 -2
- package/src/extensions/automerge/update-automerge.ts +2 -2
- package/src/extensions/blocks.ts +36 -36
- package/src/extensions/busy.ts +32 -0
- package/src/extensions/comments.ts +5 -9
- package/src/extensions/dnd.ts +127 -3
- package/src/extensions/factories.test.ts +1 -1
- package/src/extensions/factories.ts +2 -2
- package/src/extensions/index.ts +6 -1
- package/src/extensions/markdown/styles.ts +7 -6
- package/src/extensions/marker.ts +143 -0
- package/src/extensions/pending/index.ts +6 -0
- package/src/extensions/pending/pending-text-stream.test.ts +133 -0
- package/src/extensions/pending/pending-text-stream.ts +269 -0
- package/src/extensions/pending/pending-text.test.ts +134 -0
- package/src/extensions/pending/pending-text.ts +264 -0
- package/src/extensions/pos.test.ts +97 -0
- package/src/extensions/pos.ts +206 -0
- package/src/extensions/scrolling/auto-scroll.ts +12 -8
- package/src/extensions/selection.test.ts +26 -0
- package/src/extensions/selection.ts +14 -15
- package/src/extensions/tags/DESIGN.md +58 -0
- package/src/extensions/tags/index.ts +2 -2
- package/src/extensions/tags/stub.ts +80 -0
- package/src/extensions/tags/widgets/anchor.ts +30 -0
- package/src/extensions/{preview → tags/widgets}/index.ts +1 -1
- package/src/extensions/tags/xml-tags.ts +116 -74
- package/src/extensions/tags/xml-util.test.ts +1 -1
- package/src/index.ts +1 -1
- package/src/styles/theme.ts +0 -1
- package/dist/types/src/extensions/preview/index.d.ts +0 -2
- package/dist/types/src/extensions/preview/index.d.ts.map +0 -1
- package/dist/types/src/extensions/preview/preview.d.ts +0 -34
- package/dist/types/src/extensions/preview/preview.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/fader.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/typewriter.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/typewriter.test.d.ts.map +0 -1
- package/src/extensions/preview/preview.ts +0 -242
- /package/dist/types/src/extensions/{tags/fader.d.ts → fader.d.ts} +0 -0
- /package/dist/types/src/extensions/{tags/typewriter.d.ts → typewriter.d.ts} +0 -0
- /package/dist/types/src/extensions/{tags/typewriter.test.d.ts → typewriter.test.d.ts} +0 -0
- /package/src/extensions/{tags/fader.ts → fader.ts} +0 -0
- /package/src/extensions/{tags/typewriter.test.ts → typewriter.test.ts} +0 -0
- /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 ``.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
package/src/styles/theme.ts
CHANGED
|
@@ -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 +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
|
-
*  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
|
-
// 
|
|
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
|
-
*  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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|