@dxos/ui-editor 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6
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 +711 -521
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +711 -521
- 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 +3 -10
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/auto-scroll.d.ts +6 -0
- package/dist/types/src/extensions/auto-scroll.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/folding.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +3 -2
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +3 -0
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
- package/dist/types/src/extensions/preview/preview.d.ts +3 -1
- package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
- package/dist/types/src/extensions/scroll-past-end.d.ts +3 -0
- package/dist/types/src/extensions/scroll-past-end.d.ts.map +1 -0
- package/dist/types/src/extensions/scroller.d.ts +66 -0
- package/dist/types/src/extensions/scroller.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/streamer.d.ts +1 -1
- package/dist/types/src/styles/index.d.ts +0 -2
- package/dist/types/src/styles/index.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts +15 -0
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/src/util/cursor.d.ts +1 -1
- package/dist/types/src/util/cursor.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +32 -32
- package/src/defaults.ts +19 -21
- package/src/extensions/annotations.ts +1 -1
- package/src/extensions/auto-scroll.ts +129 -0
- package/src/extensions/automerge/automerge.test.tsx +2 -2
- package/src/extensions/automerge/automerge.ts +6 -5
- package/src/extensions/blocks.ts +5 -5
- package/src/extensions/comments.ts +5 -5
- package/src/extensions/dnd.ts +2 -2
- package/src/extensions/factories.ts +7 -8
- package/src/extensions/folding.ts +3 -20
- package/src/extensions/index.ts +3 -2
- package/src/extensions/markdown/bundle.ts +23 -9
- package/src/extensions/markdown/decorate.ts +15 -11
- package/src/extensions/markdown/highlight.ts +15 -7
- package/src/extensions/markdown/link.ts +27 -33
- package/src/extensions/markdown/parser.test.ts +0 -1
- package/src/extensions/markdown/styles.ts +36 -9
- package/src/extensions/markdown/table.ts +24 -2
- package/src/extensions/outliner/outliner.ts +3 -3
- package/src/extensions/preview/preview.ts +62 -15
- package/src/extensions/scroll-past-end.ts +32 -0
- package/src/extensions/scroller.ts +233 -0
- package/src/extensions/selection.ts +1 -1
- package/src/extensions/tags/streamer.ts +2 -2
- package/src/extensions/tags/xml-tags.ts +7 -4
- package/src/styles/index.ts +0 -2
- package/src/styles/theme.ts +116 -34
- package/src/util/cursor.ts +1 -1
- package/dist/types/src/extensions/autoscroll.d.ts +0 -20
- package/dist/types/src/extensions/autoscroll.d.ts.map +0 -1
- package/dist/types/src/extensions/scrolling.d.ts +0 -78
- package/dist/types/src/extensions/scrolling.d.ts.map +0 -1
- package/dist/types/src/styles/markdown.d.ts +0 -8
- package/dist/types/src/styles/markdown.d.ts.map +0 -1
- package/dist/types/src/styles/tokens.d.ts +0 -3
- package/dist/types/src/styles/tokens.d.ts.map +0 -1
- package/src/extensions/autoscroll.ts +0 -165
- package/src/extensions/scrolling.ts +0 -189
- package/src/styles/markdown.ts +0 -26
- package/src/styles/tokens.ts +0 -17
|
@@ -8,7 +8,6 @@ import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate
|
|
|
8
8
|
import { type SyntaxNodeRef } from '@lezer/common';
|
|
9
9
|
|
|
10
10
|
import { invariant } from '@dxos/invariant';
|
|
11
|
-
import { mx } from '@dxos/ui-theme';
|
|
12
11
|
|
|
13
12
|
import { type HeadingLevel, markdownTheme } from '../../styles';
|
|
14
13
|
import { type RenderCallback } from '../../types';
|
|
@@ -56,7 +55,6 @@ class LinkButton extends WidgetType {
|
|
|
56
55
|
return this.url === other.url;
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
// TODO(burdon): Create icon and link directly without react?
|
|
60
58
|
override toDOM(view: EditorView) {
|
|
61
59
|
const el = document.createElement('span');
|
|
62
60
|
this.render(el, { url: this.url }, view);
|
|
@@ -132,8 +130,8 @@ class TextWidget extends WidgetType {
|
|
|
132
130
|
const hide = Decoration.replace({});
|
|
133
131
|
const blockQuote = Decoration.line({ class: 'cm-blockquote' });
|
|
134
132
|
const fencedCodeLine = Decoration.line({ class: 'cm-code cm-codeblock-line' });
|
|
135
|
-
const fencedCodeLineFirst = Decoration.line({ class:
|
|
136
|
-
const fencedCodeLineLast = Decoration.line({ class:
|
|
133
|
+
const fencedCodeLineFirst = Decoration.line({ class: 'cm-code cm-codeblock-line cm-codeblock-start' });
|
|
134
|
+
const fencedCodeLineLast = Decoration.line({ class: 'cm-code cm-codeblock-line cm-codeblock-end' });
|
|
137
135
|
const commentBlockLine = fencedCodeLine;
|
|
138
136
|
const commentBlockLineFirst = fencedCodeLineFirst;
|
|
139
137
|
const commentBlockLineLast = fencedCodeLineLast;
|
|
@@ -244,14 +242,14 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
|
244
242
|
headers
|
|
245
243
|
.slice(from - 1)
|
|
246
244
|
.map((level) => level?.number ?? 0)
|
|
247
|
-
.join('.') + ' ';
|
|
245
|
+
.join('.') + '). ';
|
|
248
246
|
|
|
249
247
|
if (num.length) {
|
|
250
248
|
atomicDecoRanges.push({
|
|
251
249
|
from: mark.from,
|
|
252
250
|
to: mark.from + len,
|
|
253
251
|
deco: Decoration.replace({
|
|
254
|
-
widget: new TextWidget(num, markdownTheme.heading(level)),
|
|
252
|
+
widget: new TextWidget(num, markdownTheme.heading(level).className),
|
|
255
253
|
}),
|
|
256
254
|
});
|
|
257
255
|
}
|
|
@@ -440,11 +438,11 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
|
440
438
|
|
|
441
439
|
decoRanges.push({
|
|
442
440
|
from: marks[0].to,
|
|
443
|
-
to: marks[1].from,
|
|
441
|
+
to: !editing && options.renderLinkButton ? node.to : marks[1].from,
|
|
444
442
|
deco: Decoration.mark({
|
|
445
443
|
tagName: 'a',
|
|
446
444
|
attributes: {
|
|
447
|
-
class: 'cm-link',
|
|
445
|
+
class: options.renderLinkButton ? 'cm-link cm-link-with-button' : 'cm-link',
|
|
448
446
|
href: url,
|
|
449
447
|
rel: 'noreferrer',
|
|
450
448
|
target: '_blank',
|
|
@@ -457,7 +455,9 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
|
457
455
|
from: marks[1].from,
|
|
458
456
|
to: node.to,
|
|
459
457
|
deco: options.renderLinkButton
|
|
460
|
-
? Decoration.replace({
|
|
458
|
+
? Decoration.replace({
|
|
459
|
+
widget: new LinkButton(url, options.renderLinkButton),
|
|
460
|
+
})
|
|
461
461
|
: hide,
|
|
462
462
|
});
|
|
463
463
|
}
|
|
@@ -527,8 +527,12 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
|
527
527
|
}
|
|
528
528
|
|
|
529
529
|
const atomicDeco = new RangeSetBuilder<Decoration>();
|
|
530
|
-
for (const { from, to, deco
|
|
531
|
-
|
|
530
|
+
for (const { from, to, deco } of atomicDecoRanges) {
|
|
531
|
+
// Skip replace decorations that span line breaks (not allowed by CodeMirror plugins).
|
|
532
|
+
if (from < to && state.doc.lineAt(from).number !== state.doc.lineAt(to).number) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
atomicDeco.add(from, to, deco);
|
|
532
536
|
}
|
|
533
537
|
|
|
534
538
|
return {
|
|
@@ -145,13 +145,21 @@ export const markdownHighlightStyle = (_options: HighlightOptions = {}) => {
|
|
|
145
145
|
class: 'font-mono',
|
|
146
146
|
},
|
|
147
147
|
|
|
148
|
-
// Headings
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
{
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
// Headings — use CSS properties only (no class:) so CodeMirror generates scoped CSS via
|
|
149
|
+
// StyleModule that overrides vscodeDarkStyle's t.heading rule. When class: is present,
|
|
150
|
+
// HighlightStyle silently ignores all other CSS properties (they're mutually exclusive).
|
|
151
|
+
// Font sizes use Tailwind v4 CSS variables so nothing is hardcoded.
|
|
152
|
+
{
|
|
153
|
+
tag: tags.heading,
|
|
154
|
+
color: 'var(--color-cm-heading) !important',
|
|
155
|
+
fontWeight: '300',
|
|
156
|
+
},
|
|
157
|
+
{ tag: tags.heading1, ...markdownTheme.heading(1) },
|
|
158
|
+
{ tag: tags.heading2, ...markdownTheme.heading(2) },
|
|
159
|
+
{ tag: tags.heading3, ...markdownTheme.heading(3) },
|
|
160
|
+
{ tag: tags.heading4, ...markdownTheme.heading(4) },
|
|
161
|
+
{ tag: tags.heading5, ...markdownTheme.heading(5) },
|
|
162
|
+
{ tag: tags.heading6, ...markdownTheme.heading(6) },
|
|
155
163
|
|
|
156
164
|
// Emphasis.
|
|
157
165
|
{ tag: tags.emphasis, class: 'italic' },
|
|
@@ -12,39 +12,33 @@ import { tooltipContent } from '@dxos/ui-theme';
|
|
|
12
12
|
import { type RenderCallback } from '../../types';
|
|
13
13
|
|
|
14
14
|
export const linkTooltip = (renderTooltip: RenderCallback<{ url: string }>) => {
|
|
15
|
-
return hoverTooltip(
|
|
16
|
-
(view
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
15
|
+
return hoverTooltip((view, pos, side) => {
|
|
16
|
+
const syntax = syntaxTree(view.state).resolveInner(pos, side);
|
|
17
|
+
let link = null;
|
|
18
|
+
for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
|
|
19
|
+
link = node.name === 'Link' ? node : null;
|
|
20
|
+
}
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const url = link && link.getChild('URL');
|
|
23
|
+
if (!url || !link) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
// NOTE: 0 = default of 300ms.
|
|
47
|
-
hoverTime: 1,
|
|
48
|
-
},
|
|
49
|
-
);
|
|
27
|
+
const urlText = view.state.sliceDoc(url.from, url.to);
|
|
28
|
+
if (urlText.startsWith('dxn')) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
pos: link.from,
|
|
34
|
+
end: link.to,
|
|
35
|
+
above: true,
|
|
36
|
+
create: () => {
|
|
37
|
+
const el = document.createElement('div');
|
|
38
|
+
el.className = tooltipContent({});
|
|
39
|
+
renderTooltip(el, { url: urlText }, view);
|
|
40
|
+
return { dom: el, offset: { x: 0, y: 4 } };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
});
|
|
50
44
|
};
|
|
@@ -18,7 +18,7 @@ export const formattingStyles = EditorView.theme({
|
|
|
18
18
|
width: '100%',
|
|
19
19
|
height: '0',
|
|
20
20
|
verticalAlign: 'middle',
|
|
21
|
-
borderTop: '1px solid var(--
|
|
21
|
+
borderTop: '1px solid var(--color-cm-separator)',
|
|
22
22
|
opacity: 0.5,
|
|
23
23
|
},
|
|
24
24
|
|
|
@@ -43,20 +43,45 @@ export const formattingStyles = EditorView.theme({
|
|
|
43
43
|
* Blockquote.
|
|
44
44
|
*/
|
|
45
45
|
'& .cm-blockquote': {
|
|
46
|
-
background: 'var(--
|
|
47
|
-
borderLeft: '2px solid var(--
|
|
46
|
+
background: 'var(--color-cm-codeblock)',
|
|
47
|
+
borderLeft: '2px solid var(--color-cm-separator)',
|
|
48
48
|
paddingLeft: '1rem',
|
|
49
|
-
margin:
|
|
49
|
+
margin: 0,
|
|
50
50
|
},
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Code and codeblocks.
|
|
54
54
|
*/
|
|
55
|
+
'& code': {
|
|
56
|
+
fontFamily: fontMono,
|
|
57
|
+
whiteSpace: 'nowrap',
|
|
58
|
+
color: 'var(--color-cm-code)',
|
|
59
|
+
},
|
|
55
60
|
'& .cm-code': {
|
|
56
61
|
fontFamily: fontMono,
|
|
62
|
+
whiteSpace: 'nowrap',
|
|
63
|
+
color: 'var(--color-cm-code)',
|
|
64
|
+
},
|
|
65
|
+
// Inline code spans (triggered by backticks) use `cm-code-inline` + `font-mono`.
|
|
66
|
+
// Different monospace font metrics can slightly overflow the fixed CodeMirror line box,
|
|
67
|
+
// so constrain them to the target 24px height.
|
|
68
|
+
'& .cm-code-inline': {
|
|
69
|
+
fontFamily: fontMono,
|
|
70
|
+
height: '24px',
|
|
71
|
+
display: 'inline-flex',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
overflow: 'hidden',
|
|
74
|
+
color: 'var(--color-cm-code)',
|
|
75
|
+
},
|
|
76
|
+
'& .cm-code-mark': {
|
|
77
|
+
fontFamily: fontMono,
|
|
78
|
+
height: '24px',
|
|
79
|
+
display: 'inline-flex',
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
overflow: 'hidden',
|
|
57
82
|
},
|
|
58
83
|
'& .cm-codeblock-line': {
|
|
59
|
-
background: 'var(--
|
|
84
|
+
background: 'var(--color-cm-codeblock)',
|
|
60
85
|
paddingInline: '1rem !important',
|
|
61
86
|
},
|
|
62
87
|
'& .cm-codeblock-start': {
|
|
@@ -87,16 +112,18 @@ export const formattingStyles = EditorView.theme({
|
|
|
87
112
|
*/
|
|
88
113
|
'.cm-table *': {
|
|
89
114
|
fontFamily: fontMono,
|
|
115
|
+
lineHeight: 1.5,
|
|
90
116
|
textDecoration: 'none !important',
|
|
91
117
|
},
|
|
92
118
|
'.cm-table-head': {
|
|
93
119
|
padding: '2px 16px 2px 0px',
|
|
94
120
|
textAlign: 'left',
|
|
95
|
-
borderBottom: '1px solid var(--
|
|
96
|
-
color: 'var(--
|
|
121
|
+
borderBottom: '1px solid var(--color-cm-separator)',
|
|
122
|
+
color: 'var(--color-subdued)',
|
|
97
123
|
},
|
|
98
124
|
'.cm-table-cell': {
|
|
99
125
|
padding: '2px 16px 2px 0px',
|
|
126
|
+
verticalAlign: 'top',
|
|
100
127
|
},
|
|
101
128
|
|
|
102
129
|
/**
|
|
@@ -113,12 +140,12 @@ export const formattingStyles = EditorView.theme({
|
|
|
113
140
|
},
|
|
114
141
|
'.cm-image-with-loader': {
|
|
115
142
|
display: 'block',
|
|
116
|
-
opacity:
|
|
143
|
+
opacity: 0,
|
|
117
144
|
transitionDuration: '350ms',
|
|
118
145
|
transitionProperty: 'opacity',
|
|
119
146
|
},
|
|
120
147
|
'.cm-image-with-loader.cm-loaded-image': {
|
|
121
|
-
opacity:
|
|
148
|
+
opacity: 1,
|
|
122
149
|
},
|
|
123
150
|
'.cm-image-wrapper': {
|
|
124
151
|
'grid-template-columns': '1fr',
|
|
@@ -107,6 +107,28 @@ const update = (state: EditorState, _options: TableOptions) => {
|
|
|
107
107
|
return builder.finish();
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
+
/** Renders cell text into el, processing inline markdown (bold, italic, code). */
|
|
111
|
+
const renderCellContent = (el: HTMLElement, text: string): void => {
|
|
112
|
+
const parts = text.split(/(`[^`\n]+`|\*\*[^*\n]+\*\*|__[^_\n]+__|\*[^*\n]+\*|_[^_\n]+_)/);
|
|
113
|
+
for (const part of parts) {
|
|
114
|
+
if (part.length > 2 && part.startsWith('`') && part.endsWith('`')) {
|
|
115
|
+
const code = document.createElement('code');
|
|
116
|
+
code.textContent = part.slice(1, -1);
|
|
117
|
+
el.appendChild(code);
|
|
118
|
+
} else if ((part.startsWith('**') && part.endsWith('**')) || (part.startsWith('__') && part.endsWith('__'))) {
|
|
119
|
+
const strong = document.createElement('strong');
|
|
120
|
+
strong.textContent = part.slice(2, -2);
|
|
121
|
+
el.appendChild(strong);
|
|
122
|
+
} else if ((part.startsWith('*') && part.endsWith('*')) || (part.startsWith('_') && part.endsWith('_'))) {
|
|
123
|
+
const em = document.createElement('em');
|
|
124
|
+
em.textContent = part.slice(1, -1);
|
|
125
|
+
el.appendChild(em);
|
|
126
|
+
} else {
|
|
127
|
+
el.appendChild(document.createTextNode(part));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
110
132
|
class TableWidget extends WidgetType {
|
|
111
133
|
constructor(readonly _table: Table) {
|
|
112
134
|
super();
|
|
@@ -132,7 +154,7 @@ class TableWidget extends WidgetType {
|
|
|
132
154
|
this._table.header?.forEach((cell) => {
|
|
133
155
|
const th = document.createElement('th');
|
|
134
156
|
th.setAttribute('class', 'cm-table-head');
|
|
135
|
-
tr.appendChild(th)
|
|
157
|
+
renderCellContent(tr.appendChild(th), cell);
|
|
136
158
|
});
|
|
137
159
|
|
|
138
160
|
const body = table.appendChild(document.createElement('tbody'));
|
|
@@ -141,7 +163,7 @@ class TableWidget extends WidgetType {
|
|
|
141
163
|
row.forEach((cell) => {
|
|
142
164
|
const td = document.createElement('td');
|
|
143
165
|
td.setAttribute('class', 'cm-table-cell');
|
|
144
|
-
tr.appendChild(td)
|
|
166
|
+
renderCellContent(tr.appendChild(td), cell);
|
|
145
167
|
});
|
|
146
168
|
});
|
|
147
169
|
|
|
@@ -61,7 +61,7 @@ export const outliner = (_options: OutlinerProps = {}): Extension => [
|
|
|
61
61
|
decorateMarkdown({ listPaddingLeft: 8 }),
|
|
62
62
|
|
|
63
63
|
// Researve space for menu.
|
|
64
|
-
EditorView.contentAttributes.of({ class: '
|
|
64
|
+
EditorView.contentAttributes.of({ class: 'w-full !mr-[3rem]' }),
|
|
65
65
|
];
|
|
66
66
|
|
|
67
67
|
/**
|
|
@@ -157,10 +157,10 @@ const decorations = () => [
|
|
|
157
157
|
},
|
|
158
158
|
|
|
159
159
|
'.cm-list-item-focused': {
|
|
160
|
-
borderColor: 'var(--
|
|
160
|
+
borderColor: 'var(--color-neutral-focus-indicator)',
|
|
161
161
|
},
|
|
162
162
|
'&:focus-within .cm-list-item-selected': {
|
|
163
|
-
borderColor: 'var(--
|
|
163
|
+
borderColor: 'var(--color-separator)',
|
|
164
164
|
},
|
|
165
165
|
}),
|
|
166
166
|
),
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { syntaxTree } from '@codemirror/language';
|
|
6
|
-
import { type EditorState, type Extension, RangeSetBuilder, StateField } from '@codemirror/state';
|
|
7
|
-
import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
|
|
6
|
+
import { type EditorState, type Extension, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
|
7
|
+
import { Decoration, type DecorationSet, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
|
8
8
|
import { type SyntaxNode } from '@lezer/common';
|
|
9
9
|
|
|
10
|
+
import { type Database, DXN, Entity } from '@dxos/echo';
|
|
11
|
+
|
|
10
12
|
export type PreviewBlock = {
|
|
11
13
|
link: PreviewLinkRef;
|
|
12
14
|
el: HTMLElement;
|
|
@@ -16,7 +18,7 @@ export type PreviewLinkRef = {
|
|
|
16
18
|
suggest?: boolean;
|
|
17
19
|
block?: boolean;
|
|
18
20
|
label: string;
|
|
19
|
-
|
|
21
|
+
dxn: string;
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
export type PreviewLinkTarget = {
|
|
@@ -26,22 +28,28 @@ export type PreviewLinkTarget = {
|
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
export type PreviewOptions = {
|
|
31
|
+
db?: Database.Database;
|
|
29
32
|
addBlockContainer?: (block: PreviewBlock) => void;
|
|
30
33
|
removeBlockContainer?: (block: PreviewBlock) => void;
|
|
31
34
|
};
|
|
32
35
|
|
|
36
|
+
const labelResolvedEffect = StateEffect.define<void>();
|
|
37
|
+
|
|
33
38
|
/**
|
|
34
39
|
* Create preview decorations.
|
|
35
40
|
*/
|
|
36
41
|
export const preview = (options: PreviewOptions = {}): Extension => {
|
|
42
|
+
// Mutable ref so the StateField's onLoad callback can dispatch into the view.
|
|
43
|
+
const viewRef: { current: EditorView | undefined } = { current: undefined };
|
|
44
|
+
|
|
37
45
|
return [
|
|
38
46
|
// NOTE: Atomic block decorations must be created from a state field, now a widget, otherwise it results in the following error:
|
|
39
47
|
// "Block decorations may not be specified via plugins".
|
|
40
48
|
StateField.define<DecorationSet>({
|
|
41
|
-
create: (state) => buildDecorations(state, options),
|
|
49
|
+
create: (state) => buildDecorations(state, options, viewRef),
|
|
42
50
|
update: (decorations, tr) => {
|
|
43
|
-
if (tr.docChanged) {
|
|
44
|
-
return buildDecorations(tr.state, options);
|
|
51
|
+
if (tr.docChanged || tr.effects.some((effect) => effect.is(labelResolvedEffect))) {
|
|
52
|
+
return buildDecorations(tr.state, options, viewRef);
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
return decorations.map(tr.changes);
|
|
@@ -51,14 +59,51 @@ export const preview = (options: PreviewOptions = {}): Extension => {
|
|
|
51
59
|
EditorView.atomicRanges.of((view) => view.state.field(field)),
|
|
52
60
|
],
|
|
53
61
|
}),
|
|
62
|
+
ViewPlugin.define((view) => {
|
|
63
|
+
viewRef.current = view;
|
|
64
|
+
return {
|
|
65
|
+
destroy() {
|
|
66
|
+
viewRef.current = undefined;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}),
|
|
54
70
|
];
|
|
55
71
|
};
|
|
56
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a DXN to a display label using the database.
|
|
75
|
+
*/
|
|
76
|
+
const resolveLabel = (
|
|
77
|
+
db: Database.Database,
|
|
78
|
+
dxnStr: string,
|
|
79
|
+
viewRef: { current: EditorView | undefined },
|
|
80
|
+
): string | undefined => {
|
|
81
|
+
const dxn = DXN.tryParse(dxnStr);
|
|
82
|
+
if (!dxn) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ref = db.makeRef(dxn);
|
|
87
|
+
const target = ref.target;
|
|
88
|
+
if (target) {
|
|
89
|
+
return Entity.getLabel(target);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Object not loaded yet — schedule a decoration rebuild when it resolves.
|
|
93
|
+
void ref.tryLoad().then(() => {
|
|
94
|
+
viewRef.current?.dispatch({ effects: labelResolvedEffect.of(undefined) });
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
57
98
|
/**
|
|
58
99
|
* Echo references are represented as markdown reference links.
|
|
59
100
|
* https://www.markdownguide.org/basic-syntax/#reference-style-links
|
|
60
101
|
*/
|
|
61
|
-
const buildDecorations = (
|
|
102
|
+
const buildDecorations = (
|
|
103
|
+
state: EditorState,
|
|
104
|
+
options: PreviewOptions,
|
|
105
|
+
viewRef: { current: EditorView | undefined },
|
|
106
|
+
): DecorationSet => {
|
|
62
107
|
const builder = new RangeSetBuilder<Decoration>();
|
|
63
108
|
|
|
64
109
|
syntaxTree(state).iterate({
|
|
@@ -71,11 +116,13 @@ const buildDecorations = (state: EditorState, options: PreviewOptions): Decorati
|
|
|
71
116
|
case 'Link': {
|
|
72
117
|
const link = getLinkRef(state, node.node);
|
|
73
118
|
if (link) {
|
|
119
|
+
const resolved = options.db ? resolveLabel(options.db, link.dxn, viewRef) : undefined;
|
|
120
|
+
const displayLink = resolved ? { ...link, label: resolved } : link;
|
|
74
121
|
builder.add(
|
|
75
122
|
node.from,
|
|
76
123
|
node.to,
|
|
77
124
|
Decoration.replace({
|
|
78
|
-
widget: new PreviewInlineWidget(options,
|
|
125
|
+
widget: new PreviewInlineWidget(options, displayLink),
|
|
79
126
|
side: 1,
|
|
80
127
|
}),
|
|
81
128
|
);
|
|
@@ -119,13 +166,13 @@ export const getLinkRef = (state: EditorState, node: SyntaxNode): PreviewLinkRef
|
|
|
119
166
|
const mark = node.getChildren('LinkMark');
|
|
120
167
|
const urlNode = node.getChild('URL');
|
|
121
168
|
if (mark && urlNode) {
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
169
|
+
const dxn = state.sliceDoc(urlNode.from, urlNode.to);
|
|
170
|
+
if (dxn.startsWith('dxn:')) {
|
|
124
171
|
const label = state.sliceDoc(mark[0].to, mark[1].from);
|
|
125
172
|
return {
|
|
126
173
|
block: state.sliceDoc(mark[0].from, mark[0].from + 1) === '!',
|
|
127
174
|
label,
|
|
128
|
-
|
|
175
|
+
dxn,
|
|
129
176
|
};
|
|
130
177
|
}
|
|
131
178
|
}
|
|
@@ -148,14 +195,14 @@ class PreviewInlineWidget extends WidgetType {
|
|
|
148
195
|
// }
|
|
149
196
|
|
|
150
197
|
override eq(other: this) {
|
|
151
|
-
return this._link.
|
|
198
|
+
return this._link.dxn === other._link.dxn && this._link.label === other._link.label;
|
|
152
199
|
}
|
|
153
200
|
|
|
154
201
|
override toDOM(_view: EditorView) {
|
|
155
202
|
const root = document.createElement('dx-anchor');
|
|
156
203
|
root.classList.add('dx-tag--anchor');
|
|
157
204
|
root.textContent = this._link.label;
|
|
158
|
-
root.setAttribute('
|
|
205
|
+
root.setAttribute('dxn', this._link.dxn);
|
|
159
206
|
return root;
|
|
160
207
|
}
|
|
161
208
|
}
|
|
@@ -177,12 +224,12 @@ class PreviewBlockWidget extends WidgetType {
|
|
|
177
224
|
// }
|
|
178
225
|
|
|
179
226
|
override eq(other: this) {
|
|
180
|
-
return this._link.
|
|
227
|
+
return this._link.dxn === other._link.dxn;
|
|
181
228
|
}
|
|
182
229
|
|
|
183
230
|
override toDOM(_view: EditorView) {
|
|
184
231
|
const root = document.createElement('div');
|
|
185
|
-
root.classList.add('cm-preview-block', 'density-fine');
|
|
232
|
+
root.classList.add('cm-preview-block', 'dx-density-fine');
|
|
186
233
|
this._options.addBlockContainer?.({ link: this._link, el: root });
|
|
187
234
|
return root;
|
|
188
235
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom scroll-past-end extension that accounts for the actual height of the last line.
|
|
10
|
+
* The built-in CodeMirror `scrollPastEnd` uses `defaultLineHeight` which doesn't account
|
|
11
|
+
* for taller elements like headings, block widgets, etc.
|
|
12
|
+
*/
|
|
13
|
+
const scrollPastEndPlugin = ViewPlugin.fromClass(
|
|
14
|
+
class {
|
|
15
|
+
height = 1000;
|
|
16
|
+
attrs: { style: string } | null = { style: 'padding-bottom: 1000px' };
|
|
17
|
+
|
|
18
|
+
update({ view }: { view: EditorView }) {
|
|
19
|
+
const lastLineBlock = view.lineBlockAt(view.state.doc.length);
|
|
20
|
+
const height = view.dom.clientHeight - lastLineBlock.height - view.documentPadding.top - 0.5;
|
|
21
|
+
if (height >= 0 && height !== this.height) {
|
|
22
|
+
this.height = height;
|
|
23
|
+
this.attrs = { style: `padding-bottom: ${height}px` };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const scrollPastEnd = (): Extension => [
|
|
30
|
+
scrollPastEndPlugin,
|
|
31
|
+
EditorView.contentAttributes.of((view) => view.plugin(scrollPastEndPlugin)?.attrs ?? null),
|
|
32
|
+
];
|