@dxos/react-ui-editor 0.6.13 → 0.6.14-main.69511f5

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 (133) hide show
  1. package/dist/lib/browser/index.mjs +769 -705
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +5672 -0
  5. package/dist/lib/node/index.cjs.map +7 -0
  6. package/dist/lib/node/meta.json +1 -0
  7. package/dist/lib/node-esm/index.mjs +5654 -0
  8. package/dist/lib/node-esm/index.mjs.map +7 -0
  9. package/dist/lib/node-esm/meta.json +1 -0
  10. package/dist/types/src/InputMode.stories.d.ts +11 -11
  11. package/dist/types/src/InputMode.stories.d.ts.map +1 -1
  12. package/dist/types/src/TextEditor.stories.d.ts +4 -1
  13. package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
  14. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  15. package/dist/types/src/defaults.d.ts.map +1 -1
  16. package/dist/types/src/extensions/autocomplete.d.ts +2 -1
  17. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  18. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -1
  19. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  20. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  21. package/dist/types/src/extensions/awareness/awareness.d.ts +2 -2
  22. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
  23. package/dist/types/src/extensions/command/state.d.ts +2 -2
  24. package/dist/types/src/extensions/command/state.d.ts.map +1 -1
  25. package/dist/types/src/extensions/comments.d.ts +1 -1
  26. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  27. package/dist/types/src/extensions/debug.d.ts +2 -2
  28. package/dist/types/src/extensions/debug.d.ts.map +1 -1
  29. package/dist/types/src/extensions/factories.d.ts +1 -0
  30. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  31. package/dist/types/src/extensions/focus.d.ts +7 -0
  32. package/dist/types/src/extensions/focus.d.ts.map +1 -0
  33. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  34. package/dist/types/src/extensions/index.d.ts +2 -4
  35. package/dist/types/src/extensions/index.d.ts.map +1 -1
  36. package/dist/types/src/extensions/listener.d.ts +1 -0
  37. package/dist/types/src/extensions/listener.d.ts.map +1 -1
  38. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  39. package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -1
  40. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  41. package/dist/types/src/extensions/markdown/image.d.ts +3 -6
  42. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
  43. package/dist/types/src/extensions/markdown/link.d.ts +1 -1
  44. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  45. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
  46. package/dist/types/src/extensions/modes.d.ts +3 -3
  47. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  48. package/dist/types/src/extensions/{state.d.ts → selection.d.ts} +8 -4
  49. package/dist/types/src/extensions/selection.d.ts.map +1 -0
  50. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  51. package/dist/types/src/index.d.ts +1 -0
  52. package/dist/types/src/index.d.ts.map +1 -1
  53. package/dist/types/src/styles/markdown.d.ts +1 -2
  54. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  55. package/dist/types/src/styles/theme.d.ts.map +1 -1
  56. package/dist/types/src/types.d.ts.map +1 -0
  57. package/dist/types/src/{extensions → util}/cursor.d.ts +9 -3
  58. package/dist/types/src/util/cursor.d.ts.map +1 -0
  59. package/dist/types/src/util/debug.d.ts +17 -0
  60. package/dist/types/src/util/debug.d.ts.map +1 -0
  61. package/dist/types/src/util/dom.d.ts.map +1 -0
  62. package/dist/types/src/util/facet.d.ts +3 -0
  63. package/dist/types/src/util/facet.d.ts.map +1 -0
  64. package/dist/types/src/util/index.d.ts +6 -0
  65. package/dist/types/src/util/index.d.ts.map +1 -0
  66. package/dist/types/src/{extensions/util → util}/react.d.ts +1 -1
  67. package/dist/types/src/util/react.d.ts.map +1 -0
  68. package/package.json +46 -41
  69. package/src/InputMode.stories.tsx +8 -8
  70. package/src/TextEditor.stories.tsx +100 -75
  71. package/src/components/Toolbar/Toolbar.tsx +8 -11
  72. package/src/defaults.ts +0 -2
  73. package/src/extensions/annotations.ts +1 -1
  74. package/src/extensions/autocomplete.ts +9 -8
  75. package/src/extensions/automerge/automerge.stories.tsx +2 -2
  76. package/src/extensions/automerge/{automerge.spec.tsx → automerge.test.tsx} +1 -0
  77. package/src/extensions/automerge/automerge.ts +2 -2
  78. package/src/extensions/automerge/cursor.ts +1 -1
  79. package/src/extensions/awareness/awareness.ts +3 -5
  80. package/src/extensions/command/hint.ts +1 -1
  81. package/src/extensions/command/state.ts +3 -4
  82. package/src/extensions/comments.ts +45 -47
  83. package/src/extensions/debug.ts +2 -2
  84. package/src/extensions/factories.ts +5 -1
  85. package/src/extensions/focus.ts +35 -0
  86. package/src/extensions/folding.tsx +7 -5
  87. package/src/extensions/index.ts +2 -4
  88. package/src/extensions/listener.ts +1 -0
  89. package/src/extensions/markdown/changes.test.ts +1 -3
  90. package/src/extensions/markdown/decorate.ts +50 -7
  91. package/src/extensions/markdown/formatting.test.ts +1 -3
  92. package/src/extensions/markdown/highlight.ts +0 -5
  93. package/src/extensions/markdown/image.ts +53 -42
  94. package/src/extensions/markdown/link.ts +3 -2
  95. package/src/extensions/markdown/parser.test.ts +1 -2
  96. package/src/extensions/markdown/styles.ts +10 -0
  97. package/src/extensions/markdown/table.ts +3 -3
  98. package/src/extensions/modes.ts +6 -5
  99. package/src/extensions/{state.ts → selection.ts} +20 -16
  100. package/src/hooks/useTextEditor.ts +35 -32
  101. package/src/index.ts +1 -0
  102. package/src/styles/markdown.ts +1 -3
  103. package/src/styles/theme.ts +3 -1
  104. package/src/{extensions → util}/cursor.ts +11 -8
  105. package/src/{util.ts → util/debug.ts} +25 -2
  106. package/src/util/facet.ts +13 -0
  107. package/src/{extensions/util → util}/index.ts +3 -2
  108. package/src/{extensions/util → util}/react.tsx +6 -1
  109. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +0 -2
  110. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +0 -1
  111. package/dist/types/src/extensions/cursor.d.ts.map +0 -1
  112. package/dist/types/src/extensions/doc.d.ts +0 -6
  113. package/dist/types/src/extensions/doc.d.ts.map +0 -1
  114. package/dist/types/src/extensions/state.d.ts.map +0 -1
  115. package/dist/types/src/extensions/types.d.ts.map +0 -1
  116. package/dist/types/src/extensions/util/dom.d.ts.map +0 -1
  117. package/dist/types/src/extensions/util/error.d.ts +0 -2
  118. package/dist/types/src/extensions/util/error.d.ts.map +0 -1
  119. package/dist/types/src/extensions/util/index.d.ts +0 -5
  120. package/dist/types/src/extensions/util/index.d.ts.map +0 -1
  121. package/dist/types/src/extensions/util/overlap.d.ts +0 -8
  122. package/dist/types/src/extensions/util/overlap.d.ts.map +0 -1
  123. package/dist/types/src/extensions/util/react.d.ts.map +0 -1
  124. package/dist/types/src/util.d.ts +0 -7
  125. package/dist/types/src/util.d.ts.map +0 -1
  126. package/src/extensions/automerge/automerge.test.ts +0 -13
  127. package/src/extensions/doc.ts +0 -17
  128. package/src/extensions/util/error.ts +0 -15
  129. package/src/extensions/util/overlap.ts +0 -12
  130. /package/dist/types/src/{extensions/types.d.ts → types.d.ts} +0 -0
  131. /package/dist/types/src/{extensions/util → util}/dom.d.ts +0 -0
  132. /package/src/{extensions/types.ts → types.ts} +0 -0
  133. /package/src/{extensions/util → util}/dom.ts +0 -0
@@ -5,7 +5,6 @@
5
5
  import { invertedEffects } from '@codemirror/commands';
6
6
  import {
7
7
  type Extension,
8
- Facet,
9
8
  StateEffect,
10
9
  StateField,
11
10
  type Text,
@@ -29,18 +28,14 @@ import { debounce, type UnsubscribeCallback } from '@dxos/async';
29
28
  import { log } from '@dxos/log';
30
29
  import { nonNullable } from '@dxos/util';
31
30
 
32
- import { Cursor } from './cursor';
33
- import { type Comment, type Range } from './types';
34
- import { overlap } from './util';
35
- import { callbackWrapper } from '../util';
31
+ import { documentId } from './selection';
32
+ import { type Comment, type Range } from '../types';
33
+ import { Cursor, overlap, singleValueFacet, callbackWrapper } from '../util';
36
34
 
37
35
  //
38
36
  // State management.
39
37
  //
40
38
 
41
- // TODO(wittjosiah): Factor out, not comments-specific.
42
- const documentId = Facet.define<string | undefined, string | undefined>({ combine: (values) => values[0] });
43
-
44
39
  type CommentState = {
45
40
  comment: Comment;
46
41
  range: Range;
@@ -369,7 +364,7 @@ export type CommentsOptions = {
369
364
  onHover?: (el: Element, shortcut: string) => void;
370
365
  };
371
366
 
372
- const optionsFacet = Facet.define<CommentsOptions, CommentsOptions>({ combine: (providers) => providers[0] });
367
+ const optionsFacet = singleValueFacet<CommentsOptions>();
373
368
 
374
369
  /**
375
370
  * Comment threads.
@@ -389,7 +384,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
389
384
 
390
385
  return [
391
386
  optionsFacet.of(options),
392
- documentId.of(options.id),
387
+ options.id ? documentId.of(options.id) : undefined,
393
388
  commentsState,
394
389
  commentsDecorations,
395
390
  handleCommentClick,
@@ -398,45 +393,43 @@ export const comments = (options: CommentsOptions = {}): Extension => {
398
393
  //
399
394
  // Keymap.
400
395
  //
401
- options.onCreate
402
- ? keymap.of([
403
- {
404
- key: shortcut,
405
- run: callbackWrapper(createComment),
406
- },
407
- ])
408
- : [],
396
+ options.onCreate &&
397
+ keymap.of([
398
+ {
399
+ key: shortcut,
400
+ run: callbackWrapper(createComment),
401
+ },
402
+ ]),
409
403
 
410
404
  //
411
405
  // Hover tooltip (for key shortcut hints, etc.)
412
406
  // TODO(burdon): Factor out to generic hints extension for current selection/line.
413
407
  //
414
- options.onHover
415
- ? hoverTooltip(
416
- (view, pos) => {
417
- const selection = view.state.selection.main;
418
- if (selection && pos >= selection.from && pos <= selection.to) {
419
- return {
420
- pos: selection.from,
421
- end: selection.to,
422
- above: true,
423
- create: () => {
424
- const el = document.createElement('div');
425
- options.onHover!(el, shortcut);
426
- return { dom: el, offset: { x: 0, y: 8 } };
427
- },
428
- };
429
- }
408
+ options.onHover &&
409
+ hoverTooltip(
410
+ (view, pos) => {
411
+ const selection = view.state.selection.main;
412
+ if (selection && pos >= selection.from && pos <= selection.to) {
413
+ return {
414
+ pos: selection.from,
415
+ end: selection.to,
416
+ above: true,
417
+ create: () => {
418
+ const el = document.createElement('div');
419
+ options.onHover!(el, shortcut);
420
+ return { dom: el, offset: { x: 0, y: 8 } };
421
+ },
422
+ };
423
+ }
430
424
 
431
- return null;
432
- },
433
- {
434
- // TODO(burdon): Hide on change triggered immediately?
435
- // hideOnChange: true,
436
- hoverTime: 1_000,
437
- },
438
- )
439
- : [],
425
+ return null;
426
+ },
427
+ {
428
+ // TODO(burdon): Hide on change triggered immediately?
429
+ // hideOnChange: true,
430
+ hoverTime: 1_000,
431
+ },
432
+ ),
440
433
 
441
434
  //
442
435
  // Track deleted ranges and update ranges for decorations.
@@ -511,8 +504,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
511
504
  }
512
505
  }),
513
506
 
514
- options.onUpdate ? trackPastedComments(options.onUpdate) : [],
515
- ];
507
+ options.onUpdate && trackPastedComments(options.onUpdate),
508
+ ].filter(nonNullable);
516
509
  };
517
510
 
518
511
  //
@@ -553,9 +546,13 @@ export const scrollThreadIntoView = (view: EditorView, id: string, center = true
553
546
  * Query the editor state for the active formatting at the selection.
554
547
  */
555
548
  export const selectionOverlapsComment = (state: EditorState): boolean => {
556
- const { selection } = state;
557
- const commentState = state.field(commentsState);
549
+ // May not be defined if thread plugin not installed.
550
+ const commentState = state.field(commentsState, false);
551
+ if (commentState === undefined) {
552
+ return false;
553
+ }
558
554
 
555
+ const { selection } = state;
559
556
  for (const range of selection.ranges) {
560
557
  if (commentState.comments.some(({ range: commentRange }) => overlap(commentRange, range))) {
561
558
  return true;
@@ -597,6 +594,7 @@ class ExternalCommentSync implements PluginValue {
597
594
  };
598
595
  }
599
596
 
597
+ // TODO(burdon): Needs comment.
600
598
  export const createExternalCommentSync = (
601
599
  id: string,
602
600
  subscribe: (sink: () => void) => UnsubscribeCallback,
@@ -3,10 +3,10 @@
3
3
  //
4
4
 
5
5
  import { syntaxTree } from '@codemirror/language';
6
- import { type EditorState, type RangeSet, StateField, type Transaction } from '@codemirror/state';
6
+ import { type EditorState, type Extension, type RangeSet, StateField, type Transaction } from '@codemirror/state';
7
7
 
8
8
  // eslint-disable-next-line no-console
9
- export const debugNodeLogger = (log: (...args: any[]) => void = console.log) => {
9
+ export const debugNodeLogger = (log: (...args: any[]) => void = console.log): Extension => {
10
10
  const logTokens = (state: EditorState) => syntaxTree(state).iterate({ enter: (node) => log(node.type) });
11
11
  return StateField.define<any>({
12
12
  create: (state) => logTokens(state),
@@ -31,7 +31,8 @@ import { type HuePalette, hueTokens } from '@dxos/react-ui-theme';
31
31
  import { hexToHue, isNotFalsy } from '@dxos/util';
32
32
 
33
33
  import { automerge } from './automerge';
34
- import { awareness, SpaceAwarenessProvider } from './awareness';
34
+ import { SpaceAwarenessProvider, awareness } from './awareness';
35
+ import { focus } from './focus';
35
36
  import { type ThemeStyles, defaultTheme } from '../styles';
36
37
 
37
38
  //
@@ -52,6 +53,7 @@ export type BasicExtensionsOptions = {
52
53
  dropCursor?: boolean;
53
54
  drawSelection?: boolean;
54
55
  editable?: boolean;
56
+ focus?: boolean;
55
57
  highlightActiveLine?: boolean;
56
58
  history?: boolean;
57
59
  indentWithTab?: boolean;
@@ -72,6 +74,7 @@ const defaultBasicOptions: BasicExtensionsOptions = {
72
74
  closeBrackets: true,
73
75
  drawSelection: true,
74
76
  editable: true,
77
+ focus: true,
75
78
  history: true,
76
79
  keymap: 'standard',
77
80
  lineWrapping: true,
@@ -98,6 +101,7 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
98
101
  props.closeBrackets && closeBrackets(),
99
102
  props.dropCursor && dropCursor(),
100
103
  props.drawSelection && drawSelection({ cursorBlinkRate: 1_200 }),
104
+ props.focus && focus,
101
105
  props.highlightActiveLine && highlightActiveLine(),
102
106
  props.history && history(),
103
107
  props.lineNumbers && lineNumbers(),
@@ -0,0 +1,35 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { StateEffect, StateField } from '@codemirror/state';
6
+ import { EditorView } from '@codemirror/view';
7
+
8
+ const focusEffect = StateEffect.define<boolean>();
9
+
10
+ export const focusField = StateField.define<boolean>({
11
+ create: () => false,
12
+ update: (value, tr) => {
13
+ for (const effect of tr.effects) {
14
+ if (effect.is(focusEffect)) {
15
+ return effect.value;
16
+ }
17
+ }
18
+ return value;
19
+ },
20
+ });
21
+
22
+ /**
23
+ * Manage focus.
24
+ */
25
+ export const focus = [
26
+ focusField,
27
+ EditorView.domEventHandlers({
28
+ focus: (event, view) => {
29
+ setTimeout(() => view.dispatch({ effects: focusEffect.of(true) }));
30
+ },
31
+ blur: (event, view) => {
32
+ setTimeout(() => view.dispatch({ effects: focusEffect.of(false) }));
33
+ },
34
+ }),
35
+ ];
@@ -8,16 +8,15 @@ import { EditorView } from '@codemirror/view';
8
8
  import React from 'react';
9
9
 
10
10
  import { Icon } from '@dxos/react-ui';
11
- import { getSize } from '@dxos/react-ui-theme';
12
11
 
13
- import { createElement, renderRoot } from './util';
12
+ import { createElement, renderRoot } from '../util';
14
13
 
15
14
  export type FoldingOptions = {};
16
15
 
17
16
  /**
18
17
  * https://codemirror.net/examples/gutter
19
18
  */
20
- // TODO(burdon): Remember folding state.
19
+ // TODO(burdon): Remember folding state (to state).
21
20
  export const folding = (_props: FoldingOptions = {}): Extension => [
22
21
  codeFolding({
23
22
  placeholderDOM: () => {
@@ -26,9 +25,11 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
26
25
  }),
27
26
  foldGutter({
28
27
  markerDOM: (open) => {
28
+ // TODO(burdon): Use sprite directly.
29
+ const el = createElement('div', { className: 'flex h-full items-center' });
29
30
  return renderRoot(
30
- createElement('div', { className: 'flex h-full items-center' }),
31
- <Icon icon='ph--caret-right--regular' classNames={[getSize(3), 'mx-3 cursor-pointer', open && 'rotate-90']} />,
31
+ el,
32
+ <Icon icon='ph--caret-right--regular' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
32
33
  );
33
34
  },
34
35
  }),
@@ -36,6 +37,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
36
37
  '.cm-foldGutter': {
37
38
  opacity: 0.3,
38
39
  transition: 'opacity 0.3s',
40
+ width: '32px',
39
41
  },
40
42
  '.cm-foldGutter:hover': {
41
43
  opacity: 1,
@@ -9,16 +9,14 @@ export * from './awareness';
9
9
  export * from './blast';
10
10
  export * from './command';
11
11
  export * from './comments';
12
- export * from './cursor';
13
12
  export * from './debug';
14
- export * from './doc';
15
13
  export * from './dnd';
16
14
  export * from './factories';
15
+ export * from './focus';
17
16
  export * from './folding';
18
17
  export * from './listener';
19
18
  export * from './markdown';
20
19
  export * from './mention';
21
20
  export * from './modes';
22
- export * from './state';
23
- export * from './types';
21
+ export * from './selection';
24
22
  export * from './typewriter';
@@ -12,6 +12,7 @@ export type ListenerOptions = {
12
12
 
13
13
  /**
14
14
  * Event listener.
15
+ * @deprecated Use EditorView.updateListener and listen for specific update events.
15
16
  */
16
17
  export const listener = ({ onFocus, onChange }: ListenerOptions): Extension => {
17
18
  const extensions: Extension[] = [];
@@ -2,9 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { expect } from 'chai';
6
-
7
- import { describe, test } from '@dxos/test';
5
+ import { describe, expect, test } from 'vitest';
8
6
 
9
7
  import { createLinkLabel } from './changes';
10
8
 
@@ -15,7 +15,7 @@ import { image } from './image';
15
15
  import { formattingStyles, bulletListIndentationWidth, orderedListIndentationWidth } from './styles';
16
16
  import { table } from './table';
17
17
  import { theme, type HeadingLevel } from '../../styles';
18
- import { wrapWithCatch } from '../util';
18
+ import { wrapWithCatch } from '../../util';
19
19
 
20
20
  /**
21
21
  * Unicode characters.
@@ -45,7 +45,7 @@ class HorizontalRuleWidget extends WidgetType {
45
45
  class LinkButton extends WidgetType {
46
46
  constructor(
47
47
  private readonly url: string,
48
- private readonly render: (el: Element, url: string) => void,
48
+ private readonly render: (el: HTMLElement, url: string) => void,
49
49
  ) {
50
50
  super();
51
51
  }
@@ -54,6 +54,7 @@ class LinkButton extends WidgetType {
54
54
  return this.url === other.url;
55
55
  }
56
56
 
57
+ // TODO(burdon): Create icon and link directly without react?
57
58
  override toDOM(view: EditorView) {
58
59
  const el = document.createElement('span');
59
60
  this.render(el, this.url);
@@ -127,6 +128,7 @@ class TextWidget extends WidgetType {
127
128
  }
128
129
 
129
130
  const hide = Decoration.replace({});
131
+ const blockQuote = Decoration.line({ class: mx('cm-blockquote') });
130
132
  const fencedCodeLine = Decoration.line({ class: mx('cm-code cm-codeblock-line') });
131
133
  const fencedCodeLineFirst = Decoration.line({ class: mx('cm-code cm-codeblock-line', 'cm-codeblock-first') });
132
134
  const fencedCodeLineLast = Decoration.line({ class: mx('cm-code cm-codeblock-line', 'cm-codeblock-last') });
@@ -199,7 +201,7 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
199
201
  return listLevels[listLevels.length - 1];
200
202
  };
201
203
 
202
- // const count = 0;
204
+ // let count = 0;
203
205
  const enterNode = (node: SyntaxNodeRef) => {
204
206
  // console.log(`[${count++}]`, { node: node.name, from: node.from, to: node.to });
205
207
  switch (node.name) {
@@ -329,7 +331,36 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
329
331
  break;
330
332
  }
331
333
 
334
+ //
335
+ // Blockquote > QuoteMark > Paragraph
336
+ //
337
+
338
+ case 'Blockquote': {
339
+ const editing = editingRange(state, node, focus);
340
+ const quoteMark = node.node.getChild('QuoteMark');
341
+ const paragraph = node.node.getChild('Paragraph');
342
+ if (!editing && quoteMark && paragraph) {
343
+ atomicDeco.add(quoteMark.from, paragraph.from, hide);
344
+ }
345
+
346
+ for (const block of view.viewportLineBlocks) {
347
+ if (block.to < node.from) {
348
+ continue;
349
+ }
350
+ if (block.from > node.to) {
351
+ break;
352
+ }
353
+
354
+ deco.add(block.from, block.from, blockQuote);
355
+ }
356
+
357
+ break;
358
+ }
359
+
360
+ //
332
361
  // CommentBlock
362
+ //
363
+
333
364
  case 'CommentBlock': {
334
365
  const editing = editingRange(state, node, focus);
335
366
  for (const block of view.viewportLineBlocks) {
@@ -339,21 +370,27 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
339
370
  if (block.from > node.to) {
340
371
  break;
341
372
  }
342
- const first = block.from <= node.from;
343
- const last = block.to >= node.to && /^(\s>)*-->$/.test(state.doc.sliceString(block.from, block.to));
373
+
374
+ const isFirst = block.from <= node.from;
375
+ const isLast = block.to >= node.to && /^(\s>)*-->$/.test(state.doc.sliceString(block.from, block.to));
376
+
344
377
  deco.add(
345
378
  block.from,
346
379
  block.from,
347
- first ? commentBlockLineFirst : last ? commentBlockLineLast : commentBlockLine,
380
+ isFirst ? commentBlockLineFirst : isLast ? commentBlockLineLast : commentBlockLine,
348
381
  );
349
- if (!editing && (first || last)) {
382
+
383
+ if (!editing && (isFirst || isLast)) {
350
384
  atomicDeco.add(block.from, block.to, hide);
351
385
  }
352
386
  }
353
387
  break;
354
388
  }
355
389
 
390
+ //
356
391
  // FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
392
+ //
393
+
357
394
  case 'FencedCode': {
358
395
  for (const block of view.viewportLineBlocks) {
359
396
  if (block.to < node.from) {
@@ -375,7 +412,10 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
375
412
  return false;
376
413
  }
377
414
 
415
+ //
378
416
  // Link > [LinkMark, URL]
417
+ //
418
+
379
419
  case 'Link': {
380
420
  const marks = node.node.getChildren('LinkMark');
381
421
  const urlNode = node.node.getChild('URL');
@@ -412,7 +452,10 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
412
452
  break;
413
453
  }
414
454
 
455
+ //
415
456
  // HR
457
+ //
458
+
416
459
  case 'HorizontalRule': {
417
460
  if (!editingRange(state, node, focus)) {
418
461
  deco.add(node.from, node.to, horizontalRule);
@@ -4,9 +4,7 @@
4
4
 
5
5
  import { markdownLanguage } from '@codemirror/lang-markdown';
6
6
  import { EditorState, type StateCommand } from '@codemirror/state';
7
- import { expect } from 'chai';
8
-
9
- import { describe, test } from '@dxos/test';
7
+ import { describe, expect, test } from 'vitest';
10
8
 
11
9
  import {
12
10
  addBlockquote,
@@ -168,11 +168,6 @@ export const markdownHighlightStyle = (_options: HighlightOptions = {}) => {
168
168
  class: theme.code,
169
169
  },
170
170
 
171
- {
172
- tag: [markdownTags.QuoteMark],
173
- class: theme.blockquote,
174
- },
175
-
176
171
  {
177
172
  tag: [markdownTags.TableCell],
178
173
  class: 'font-mono',
@@ -3,43 +3,52 @@
3
3
  //
4
4
 
5
5
  import { syntaxTree } from '@codemirror/language';
6
- import { type EditorState, type Extension, StateField, type Transaction, type Range } from '@codemirror/state';
6
+ import { type EditorState, type Extension, type Range, StateField, type Transaction } from '@codemirror/state';
7
7
  import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
8
8
 
9
+ import { focusField } from '../focus';
10
+
9
11
  export type ImageOptions = {};
10
12
 
13
+ /**
14
+ * Create image decorations.
15
+ */
11
16
  export const image = (_options: ImageOptions = {}): Extension => {
12
- return StateField.define<DecorationSet>({
13
- create: (state) => {
14
- return Decoration.set(buildDecorations(0, state.doc.length, state));
15
- },
16
- update: (value: DecorationSet, tr: Transaction) => {
17
- if (!tr.docChanged && !tr.selection) {
18
- return value;
19
- }
17
+ return [
18
+ StateField.define<DecorationSet>({
19
+ create: (state) => {
20
+ // Process all images.
21
+ return Decoration.set(buildDecorations(0, state.doc.length, state));
22
+ },
23
+ update: (value: DecorationSet, tr: Transaction) => {
24
+ if (!tr.docChanged && !tr.selection) {
25
+ return value;
26
+ }
20
27
 
21
- // Find range of changes and cursor changes.
22
- const cursor = tr.state.selection.main.head;
23
- const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
24
- let from = Math.min(cursor, oldCursor);
25
- let to = Math.max(cursor, oldCursor);
26
- tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
27
- from = Math.min(from, fromB);
28
- to = Math.max(to, toB);
29
- });
30
-
31
- // Expand to cover lines.
32
- from = tr.state.doc.lineAt(from).from;
33
- to = tr.state.doc.lineAt(to).to;
34
- return value.map(tr.changes).update({
35
- filterFrom: from,
36
- filterTo: to,
37
- filter: () => false,
38
- add: buildDecorations(from, to, tr.state),
39
- });
40
- },
41
- provide: (field) => EditorView.decorations.from(field),
42
- });
28
+ // Find range of changes and cursor changes.
29
+ const cursor = tr.state.selection.main.head;
30
+ const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
31
+ let from = Math.min(cursor, oldCursor);
32
+ let to = Math.max(cursor, oldCursor);
33
+ tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
34
+ from = Math.min(from, fromB);
35
+ to = Math.max(to, toB);
36
+ });
37
+
38
+ // Expand to cover lines.
39
+ from = tr.state.doc.lineAt(from).from;
40
+ to = tr.state.doc.lineAt(to).to;
41
+
42
+ return value.map(tr.changes).update({
43
+ filterFrom: from,
44
+ filterTo: to,
45
+ filter: () => false,
46
+ add: buildDecorations(from, to, tr.state),
47
+ });
48
+ },
49
+ provide: (field) => EditorView.decorations.from(field),
50
+ }),
51
+ ];
43
52
  };
44
53
 
45
54
  const preloaded = new Set<string>();
@@ -55,15 +64,19 @@ const preloadImage = (url: string) => {
55
64
  const buildDecorations = (from: number, to: number, state: EditorState) => {
56
65
  const decorations: Range<Decoration>[] = [];
57
66
  const cursor = state.selection.main.head;
58
-
59
67
  syntaxTree(state).iterate({
60
68
  enter: (node) => {
61
69
  if (node.name === 'Image') {
62
70
  const urlNode = node.node.getChild('URL');
63
71
  if (urlNode) {
64
- const hide = state.readOnly || cursor < node.from || cursor > node.to;
72
+ const hide = state.readOnly || cursor < node.from || cursor > node.to || !state.field(focusField);
73
+
65
74
  const url = state.sliceDoc(urlNode.from, urlNode.to);
66
- // TODO(burdon): Doesn't load if scrolling with mouse.
75
+ // Some plugins might be using custom URLs; avoid attempts to render those URLs.
76
+ if (url.match(/^https?:\/\//) === null && url.match(/^file?:\/\//) === null) {
77
+ return;
78
+ }
79
+
67
80
  preloadImage(url);
68
81
  decorations.push(
69
82
  Decoration.replace({
@@ -94,14 +107,12 @@ class ImageWidget extends WidgetType {
94
107
  const img = document.createElement('img');
95
108
  img.setAttribute('src', this._url);
96
109
  img.setAttribute('class', 'cm-image');
97
- // Images are hidden until successfully loaded to avoid flickering effects.
98
- img.onload = () => img.classList.add('cm-loaded-image');
110
+ // If focused, hide image until successfully loaded to avoid flickering effects.
111
+ if (view.state.field(focusField)) {
112
+ img.onload = () => img.classList.add('cm-loaded-image');
113
+ } else {
114
+ img.classList.add('cm-loaded-image');
115
+ }
99
116
  return img;
100
117
  }
101
118
  }
102
-
103
- export type ImageUploadOptions = {
104
- onSelect: () => { url: string };
105
- };
106
-
107
- export const imageUpload = (options: ImageOptions = {}) => {};
@@ -9,8 +9,8 @@ import { type SyntaxNode } from '@lezer/common';
9
9
 
10
10
  import { tooltipContent } from '@dxos/react-ui-theme';
11
11
 
12
- export const linkTooltip = (render: (el: Element, url: string) => void) =>
13
- hoverTooltip((view, pos, side) => {
12
+ export const linkTooltip = (render: (el: HTMLElement, url: string) => void) => {
13
+ return hoverTooltip((view, pos, side) => {
14
14
  const syntax = syntaxTree(view.state).resolveInner(pos, side);
15
15
  let link = null;
16
16
  for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
@@ -35,3 +35,4 @@ export const linkTooltip = (render: (el: Element, url: string) => void) =>
35
35
  },
36
36
  };
37
37
  });
38
+ };
@@ -5,8 +5,7 @@
5
5
  // @ts-ignore
6
6
  import { testTree } from '@lezer/generator/test';
7
7
  import { parser } from '@lezer/markdown';
8
-
9
- import { describe, test } from '@dxos/test';
8
+ import { describe, test } from 'vitest';
10
9
 
11
10
  describe('parser', () => {
12
11
  // test.only('list-mark', () => {
@@ -39,6 +39,16 @@ export const formattingStyles = EditorView.theme({
39
39
  width: `${orderedListIndentationWidth}px`,
40
40
  },
41
41
 
42
+ /**
43
+ * Blockquote.
44
+ */
45
+ '& .cm-blockquote': {
46
+ background: 'var(--dx-cmCodeblock)',
47
+ borderLeft: '2px solid var(--dx-cmSeparator)',
48
+ paddingLeft: '1rem',
49
+ margin: '0',
50
+ },
51
+
42
52
  /**
43
53
  * Code and codeblocks.
44
54
  */