@liminis/editor 0.1.0 → 0.2.0

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 (35) hide show
  1. package/README.md +126 -14
  2. package/dist/annotations/types.d.ts +16 -0
  3. package/dist/app/App.d.ts +1 -1
  4. package/dist/app/App.js +3 -3
  5. package/dist/app/editor/AnchorScrollPlugin.js +2 -34
  6. package/dist/app/editor/AnnotationSurface.js +2 -1
  7. package/dist/app/editor/CorrectionPanelPlugin.js +10 -10
  8. package/dist/app/editor/DocumentOutline.d.ts +17 -0
  9. package/dist/app/editor/DocumentOutline.js +32 -0
  10. package/dist/app/editor/DragHandlePlugin.js +1 -1
  11. package/dist/app/editor/Editor.d.ts +10 -1
  12. package/dist/app/editor/Editor.js +3 -2
  13. package/dist/app/editor/OutlinePlugin.d.ts +14 -0
  14. package/dist/app/editor/OutlinePlugin.js +133 -0
  15. package/dist/app/editor/SelectionContextMenuPlugin.js +4 -4
  16. package/dist/app/editor/annotation-marks.d.ts +20 -0
  17. package/dist/app/editor/annotation-marks.js +40 -0
  18. package/dist/app/editor/documentOutlineHandle.d.ts +87 -0
  19. package/dist/app/editor/documentOutlineHandle.js +86 -0
  20. package/dist/app/editor/nodes/C4Component.js +6 -6
  21. package/dist/app/editor/nodes/DiagramContextMenu.js +5 -5
  22. package/dist/app/editor/scrollContainer.d.ts +18 -0
  23. package/dist/app/editor/scrollContainer.js +47 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.js +6 -0
  26. package/dist/markdown/stringify.js +65 -0
  27. package/dist/styles.css +342 -221
  28. package/docs/decisions/adr-078.md +37 -2
  29. package/docs/decisions/adr-085.md +166 -0
  30. package/docs/decisions/adr-086.md +147 -0
  31. package/docs/decisions/adr-087.md +216 -0
  32. package/docs/decisions/adr-088.md +136 -0
  33. package/docs/decisions/adr-089.md +121 -0
  34. package/docs/editor-api.md +53 -1
  35. package/package.json +23 -46
@@ -36,10 +36,10 @@ const itemStyle = {
36
36
  function SelectionContextMenu({ visible, x, y, selectedText, onChatAboutThis, annotationAffordances, onCreateAnnotation, onClose, }) {
37
37
  const menuRef = useRef(null);
38
38
  const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
39
- const bgColor = `var(--vscode-menu-background, ${isDark ? '#252526' : '#ffffff'})`;
40
- const borderColor = `var(--vscode-menu-border, ${isDark ? '#454545' : '#d4d4d4'})`;
41
- const textColor = `var(--vscode-menu-foreground, ${isDark ? '#cccccc' : '#333333'})`;
42
- const hoverBg = `var(--vscode-menu-selectionBackground, ${isDark ? '#094771' : '#e8e8e8'})`;
39
+ const bgColor = `var(--liminis-editor-menu-background, var(--vscode-menu-background, ${isDark ? '#252526' : '#ffffff'}))`;
40
+ const borderColor = `var(--liminis-editor-menu-border, var(--vscode-menu-border, ${isDark ? '#454545' : '#d4d4d4'}))`;
41
+ const textColor = `var(--liminis-editor-menu-foreground, var(--vscode-menu-foreground, ${isDark ? '#cccccc' : '#333333'}))`;
42
+ const hoverBg = `var(--liminis-editor-menu-selectionBackground, var(--vscode-menu-selectionBackground, ${isDark ? '#094771' : '#e8e8e8'}))`;
43
43
  useEffect(() => {
44
44
  if (!visible)
45
45
  return;
@@ -157,6 +157,26 @@ export declare function markElementsForId(editor: LexicalEditor, id: string): HT
157
157
  * are in document order, and ids with no live mark are simply absent.
158
158
  */
159
159
  export declare function markElementsByAnnotationId(editor: LexicalEditor, ids: ReadonlySet<string>): Map<string, HTMLElement[]>;
160
+ /**
161
+ * Current on-screen geometry (#73) for every live mark of the requested
162
+ * annotations, keyed by annotation id — one `DOMRect` per constituent
163
+ * `MarkNode`, in document order, computed fresh at call time via
164
+ * `getBoundingClientRect()` (viewport-relative, matching the only existing
165
+ * precedent for rect delivery in this package, `AnnotationCreateEvent.rect`
166
+ * in `AnnotationPlugin.tsx`). Ids with no currently live mark are simply
167
+ * absent from the result, never an error.
168
+ *
169
+ * `ids` omitted (or `undefined`) returns geometry for every annotation that
170
+ * currently has at least one live mark, mirroring
171
+ * {@link collectLiveAnchorSnapshots}'s own "no ids given -> collect every
172
+ * live id" behavior.
173
+ *
174
+ * Two different annotation ids can legitimately return an identical rect:
175
+ * overlapping annotations may share one `MarkNode` (`MarkNode.getIDs()` can
176
+ * hold more than one id), and that shared element's rect is reported under
177
+ * each id it carries. That is expected, not a bug.
178
+ */
179
+ export declare function getMarkRects(editor: LexicalEditor, ids?: readonly string[]): Map<string, DOMRect[]>;
160
180
  /**
161
181
  * Removes every `MarkNode` wrapping `id` (annotation deleted, or composer
162
182
  * cancelled before submit). A mark shared with other annotation ids
@@ -710,6 +710,46 @@ export function markElementsByAnnotationId(editor, ids) {
710
710
  });
711
711
  return byId;
712
712
  }
713
+ /**
714
+ * Current on-screen geometry (#73) for every live mark of the requested
715
+ * annotations, keyed by annotation id — one `DOMRect` per constituent
716
+ * `MarkNode`, in document order, computed fresh at call time via
717
+ * `getBoundingClientRect()` (viewport-relative, matching the only existing
718
+ * precedent for rect delivery in this package, `AnnotationCreateEvent.rect`
719
+ * in `AnnotationPlugin.tsx`). Ids with no currently live mark are simply
720
+ * absent from the result, never an error.
721
+ *
722
+ * `ids` omitted (or `undefined`) returns geometry for every annotation that
723
+ * currently has at least one live mark, mirroring
724
+ * {@link collectLiveAnchorSnapshots}'s own "no ids given -> collect every
725
+ * live id" behavior.
726
+ *
727
+ * Two different annotation ids can legitimately return an identical rect:
728
+ * overlapping annotations may share one `MarkNode` (`MarkNode.getIDs()` can
729
+ * hold more than one id), and that shared element's rect is reported under
730
+ * each id it carries. That is expected, not a bug.
731
+ */
732
+ export function getMarkRects(editor, ids) {
733
+ return editor.getEditorState().read(() => {
734
+ let targetIds;
735
+ if (ids === undefined) {
736
+ targetIds = new Set();
737
+ for (const markNode of collectMarkNodesInOrder($getRoot())) {
738
+ for (const id of markNode.getIDs())
739
+ targetIds.add(id);
740
+ }
741
+ }
742
+ else {
743
+ targetIds = new Set(ids);
744
+ }
745
+ const elementsById = markElementsByAnnotationId(editor, targetIds);
746
+ const rectsById = new Map();
747
+ for (const [id, elements] of elementsById) {
748
+ rectsById.set(id, elements.map((element) => element.getBoundingClientRect()));
749
+ }
750
+ return rectsById;
751
+ });
752
+ }
713
753
  /**
714
754
  * Removes every `MarkNode` wrapping `id` (annotation deleted, or composer
715
755
  * cancelled before submit). A mark shared with other annotation ids
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The controller shared between one `<Editor documentOutlineHandle={…}>` and
3
+ * one `<DocumentOutline handle={…}>`. `OutlinePlugin` feeds it from inside the
4
+ * Lexical tree; `DocumentOutline` reads it via `useSyncExternalStore`.
5
+ *
6
+ * A plain `MutableRefObject` (the pattern `annotationEditorHandleRef` uses)
7
+ * does not fit here: `DocumentOutline` must re-render live as the reader
8
+ * scrolls and the document changes, and mutating a ref's `.current` does not
9
+ * propagate to a sibling component reading the same ref. An external-store
10
+ * object solves that, and — because the consumer creates one per editor
11
+ * instance — it is naturally scoped to that instance, so mounting more than
12
+ * one editor at once never aggregates headings across them.
13
+ */
14
+ /** A single heading captured for display. */
15
+ export interface OutlineEntry {
16
+ /**
17
+ * Position within the document's H1–H5 heading order. Identity for
18
+ * selection and active-tracking is by this index, not by `text`, so
19
+ * duplicate heading titles are handled correctly.
20
+ */
21
+ index: number;
22
+ /** Heading level. H6 is out of scope — the editor theme has no class for it. */
23
+ level: 1 | 2 | 3 | 4 | 5;
24
+ /** Heading text with inline content (including inline code) reduced to plain text. */
25
+ text: string;
26
+ }
27
+ export interface DocumentOutlineSnapshot {
28
+ entries: OutlineEntry[];
29
+ /** Index of the entry whose heading is at the top of the viewport, or `null` when there is no active heading (e.g. no headings at all). */
30
+ activeIndex: number | null;
31
+ }
32
+ /**
33
+ * Not exported, so nothing outside this module can reference it. Its only
34
+ * purpose is to make `DocumentOutlineHandle` nominal instead of structural:
35
+ * without it, any object with matching method names would type-check as a
36
+ * handle, and `OutlinePlugin`'s cast to `OutlineHandleImpl` (to reach
37
+ * `connect`/`disconnect`/`publish`) would throw at runtime for a hand-rolled
38
+ * one. Only `OutlineHandleImpl`, constructed exclusively by
39
+ * `createDocumentOutlineHandle()`, can supply this property.
40
+ */
41
+ declare const HANDLE_BRAND: unique symbol;
42
+ /**
43
+ * The public surface a consumer reads and passes around. Create one with
44
+ * `createDocumentOutlineHandle()` and pass the same instance to both
45
+ * `<Editor documentOutlineHandle>` and `<DocumentOutline handle>`.
46
+ */
47
+ export interface DocumentOutlineHandle {
48
+ /** Brand only, not a real property to read — see `HANDLE_BRAND`. */
49
+ readonly [HANDLE_BRAND]: true;
50
+ /** React external-store subscription — see `useSyncExternalStore`. */
51
+ subscribe(onStoreChange: () => void): () => void;
52
+ /** React external-store snapshot. Stable by reference until the outline actually changes. */
53
+ getSnapshot(): DocumentOutlineSnapshot;
54
+ /** Scroll the editor to the heading at `index`. No-ops if no editor is currently connected, or `index` is out of range. */
55
+ scrollToHeading(index: number): void;
56
+ }
57
+ /**
58
+ * The concrete object `createDocumentOutlineHandle()` returns. Consumers only
59
+ * ever see the `DocumentOutlineHandle` surface above; `OutlinePlugin` — the
60
+ * only other reader of an instance, always produced by
61
+ * `createDocumentOutlineHandle()` — reaches `connect`/`disconnect`/`publish`
62
+ * directly on this class.
63
+ */
64
+ export declare class OutlineHandleImpl implements DocumentOutlineHandle {
65
+ readonly [HANDLE_BRAND]: true;
66
+ private snapshot;
67
+ private readonly listeners;
68
+ private scrollImpl;
69
+ subscribe: (onStoreChange: () => void) => (() => void);
70
+ getSnapshot: () => DocumentOutlineSnapshot;
71
+ scrollToHeading: (index: number) => void;
72
+ /** Called by `OutlinePlugin` while its editor is mounted. */
73
+ connect(scrollImpl: (index: number) => void): void;
74
+ /** Called by `OutlinePlugin` on unmount, so a stale handle no-ops rather than scrolling a torn-down editor. */
75
+ disconnect(): void;
76
+ /** Called by `OutlinePlugin` whenever headings or the active heading change. */
77
+ publish(next: DocumentOutlineSnapshot): void;
78
+ }
79
+ /**
80
+ * Create a controller shared between one `<Editor documentOutlineHandle={…}>`
81
+ * and one `<DocumentOutline handle={…}>`. Create it once per editor instance
82
+ * (e.g. via `useState(() => createDocumentOutlineHandle())`) and pass the
83
+ * same object to both — that is what scopes the outline to that editor, even
84
+ * when a consumer mounts several editors at once.
85
+ */
86
+ export declare function createDocumentOutlineHandle(): DocumentOutlineHandle;
87
+ export {};
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The controller shared between one `<Editor documentOutlineHandle={…}>` and
3
+ * one `<DocumentOutline handle={…}>`. `OutlinePlugin` feeds it from inside the
4
+ * Lexical tree; `DocumentOutline` reads it via `useSyncExternalStore`.
5
+ *
6
+ * A plain `MutableRefObject` (the pattern `annotationEditorHandleRef` uses)
7
+ * does not fit here: `DocumentOutline` must re-render live as the reader
8
+ * scrolls and the document changes, and mutating a ref's `.current` does not
9
+ * propagate to a sibling component reading the same ref. An external-store
10
+ * object solves that, and — because the consumer creates one per editor
11
+ * instance — it is naturally scoped to that instance, so mounting more than
12
+ * one editor at once never aggregates headings across them.
13
+ */
14
+ /**
15
+ * Not exported, so nothing outside this module can reference it. Its only
16
+ * purpose is to make `DocumentOutlineHandle` nominal instead of structural:
17
+ * without it, any object with matching method names would type-check as a
18
+ * handle, and `OutlinePlugin`'s cast to `OutlineHandleImpl` (to reach
19
+ * `connect`/`disconnect`/`publish`) would throw at runtime for a hand-rolled
20
+ * one. Only `OutlineHandleImpl`, constructed exclusively by
21
+ * `createDocumentOutlineHandle()`, can supply this property.
22
+ */
23
+ const HANDLE_BRAND = Symbol('DocumentOutlineHandle');
24
+ const EMPTY_SNAPSHOT = { entries: [], activeIndex: null };
25
+ function snapshotsEqual(a, b) {
26
+ if (a.activeIndex !== b.activeIndex)
27
+ return false;
28
+ if (a.entries.length !== b.entries.length)
29
+ return false;
30
+ for (let i = 0; i < a.entries.length; i++) {
31
+ const x = a.entries[i];
32
+ const y = b.entries[i];
33
+ if (x.index !== y.index || x.level !== y.level || x.text !== y.text)
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+ /**
39
+ * The concrete object `createDocumentOutlineHandle()` returns. Consumers only
40
+ * ever see the `DocumentOutlineHandle` surface above; `OutlinePlugin` — the
41
+ * only other reader of an instance, always produced by
42
+ * `createDocumentOutlineHandle()` — reaches `connect`/`disconnect`/`publish`
43
+ * directly on this class.
44
+ */
45
+ export class OutlineHandleImpl {
46
+ [HANDLE_BRAND] = true;
47
+ snapshot = EMPTY_SNAPSHOT;
48
+ listeners = new Set();
49
+ scrollImpl = null;
50
+ subscribe = (onStoreChange) => {
51
+ this.listeners.add(onStoreChange);
52
+ return () => {
53
+ this.listeners.delete(onStoreChange);
54
+ };
55
+ };
56
+ getSnapshot = () => this.snapshot;
57
+ scrollToHeading = (index) => {
58
+ this.scrollImpl?.(index);
59
+ };
60
+ /** Called by `OutlinePlugin` while its editor is mounted. */
61
+ connect(scrollImpl) {
62
+ this.scrollImpl = scrollImpl;
63
+ }
64
+ /** Called by `OutlinePlugin` on unmount, so a stale handle no-ops rather than scrolling a torn-down editor. */
65
+ disconnect() {
66
+ this.scrollImpl = null;
67
+ }
68
+ /** Called by `OutlinePlugin` whenever headings or the active heading change. */
69
+ publish(next) {
70
+ if (snapshotsEqual(this.snapshot, next))
71
+ return;
72
+ this.snapshot = next;
73
+ for (const listener of this.listeners)
74
+ listener();
75
+ }
76
+ }
77
+ /**
78
+ * Create a controller shared between one `<Editor documentOutlineHandle={…}>`
79
+ * and one `<DocumentOutline handle={…}>`. Create it once per editor instance
80
+ * (e.g. via `useState(() => createDocumentOutlineHandle())`) and pass the
81
+ * same object to both — that is what scopes the outline to that editor, even
82
+ * when a consumer mounts several editors at once.
83
+ */
84
+ export function createDocumentOutlineHandle() {
85
+ return new OutlineHandleImpl();
86
+ }
@@ -181,11 +181,11 @@ function C4DiagramDisplay({ code, nodeKey, onDoubleClick, isEditable, }) {
181
181
  border: 'none',
182
182
  cursor: 'pointer',
183
183
  backgroundColor: isEditingLayout
184
- ? 'var(--color-primary-100, rgba(59, 130, 246, 0.1))'
185
- : 'var(--color-muted-100, rgba(128, 128, 128, 0.1))',
184
+ ? 'var(--liminis-editor-primary-100, var(--color-primary-100, rgba(59, 130, 246, 0.1)))'
185
+ : 'var(--liminis-editor-muted-100, var(--color-muted-100, rgba(128, 128, 128, 0.1)))',
186
186
  color: isEditingLayout
187
- ? 'var(--color-primary, #3b82f6)'
188
- : 'var(--color-muted-foreground, #6b7280)',
187
+ ? 'var(--liminis-editor-primary, var(--color-primary, #3b82f6))'
188
+ : 'var(--liminis-editor-muted-foreground, var(--color-muted-foreground, #6b7280))',
189
189
  }, children: _jsx(Move, { size: 16 }) }), manualLayout && (_jsx("button", { type: "button", onClick: (e) => {
190
190
  e.stopPropagation();
191
191
  handleResetLayout();
@@ -198,8 +198,8 @@ function C4DiagramDisplay({ code, nodeKey, onDoubleClick, isEditable, }) {
198
198
  borderRadius: '4px',
199
199
  border: 'none',
200
200
  cursor: 'pointer',
201
- backgroundColor: 'var(--color-muted-100, rgba(128, 128, 128, 0.1))',
202
- color: 'var(--color-muted-foreground, #6b7280)',
201
+ backgroundColor: 'var(--liminis-editor-muted-100, var(--color-muted-100, rgba(128, 128, 128, 0.1)))',
202
+ color: 'var(--liminis-editor-muted-foreground, var(--color-muted-foreground, #6b7280))',
203
203
  }, children: _jsx(RotateCcw, { size: 16 }) }))] })), isEditingLayout && parseResult.diagram ? (_jsx(C4InteractiveRenderer, { diagram: parseResult.diagram, isDarkMode: isDarkMode, isEditMode: isEditingLayout, manualPositions: manualPositions, onPositionChange: handlePositionChange })) : (_jsx(C4Renderer, { layout: layout, isDarkMode: isDarkMode })), _jsx(DiagramContextMenu, { ...contextMenu.props, onEditText: isEditable ? onDoubleClick : undefined, onEditLayout: isEditable ? () => setIsEditingLayout(prev => !prev) : undefined, onResetLayout: isEditable ? handleResetLayout : undefined, isEditingLayout: isEditingLayout, hasManualLayout: !!manualLayout })] }));
204
204
  }
205
205
  // =============================================================================
@@ -75,11 +75,11 @@ const separatorStyle = {
75
75
  export function DiagramContextMenu({ visible, x, y, onCopyImage, onClose, onEditText, onEditLayout, onResetLayout, isEditingLayout, hasManualLayout, }) {
76
76
  const menuRef = useRef(null);
77
77
  const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
78
- const bgColor = `var(--vscode-menu-background, ${isDark ? '#252526' : '#ffffff'})`;
79
- const borderColor = `var(--vscode-menu-border, ${isDark ? '#454545' : '#d4d4d4'})`;
80
- const textColor = `var(--vscode-menu-foreground, ${isDark ? '#cccccc' : '#333333'})`;
81
- const hoverBg = `var(--vscode-menu-selectionBackground, ${isDark ? '#094771' : '#e8e8e8'})`;
82
- const separatorColor = `var(--vscode-menu-separatorBackground, ${isDark ? '#454545' : '#d4d4d4'})`;
78
+ const bgColor = `var(--liminis-editor-menu-background, var(--vscode-menu-background, ${isDark ? '#252526' : '#ffffff'}))`;
79
+ const borderColor = `var(--liminis-editor-menu-border, var(--vscode-menu-border, ${isDark ? '#454545' : '#d4d4d4'}))`;
80
+ const textColor = `var(--liminis-editor-menu-foreground, var(--vscode-menu-foreground, ${isDark ? '#cccccc' : '#333333'}))`;
81
+ const hoverBg = `var(--liminis-editor-menu-selectionBackground, var(--vscode-menu-selectionBackground, ${isDark ? '#094771' : '#e8e8e8'}))`;
82
+ const separatorColor = `var(--liminis-editor-menu-separatorBackground, var(--vscode-menu-separatorBackground, ${isDark ? '#454545' : '#d4d4d4'}))`;
83
83
  // Close on click outside or Escape
84
84
  useEffect(() => {
85
85
  if (!visible)
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared DOM helpers for locating an editor's scroll container and scrolling
3
+ * an element into view within it. `AnchorScrollPlugin` (anchor-link
4
+ * navigation) and `OutlinePlugin` (table-of-contents navigation) both need to
5
+ * find "the nearest thing that actually scrolls" without the host declaring
6
+ * one explicitly (FR-006) — this is the one place that logic lives.
7
+ */
8
+ /**
9
+ * Find the scrollable container for an element: the known editor scroll ids
10
+ * first, then the nearest ancestor that actually scrolls (robust to host
11
+ * markup).
12
+ */
13
+ export declare function scrollContainerFor(element: HTMLElement): HTMLElement | null;
14
+ /**
15
+ * Scroll `target` into view within its scroll container (or the viewport, if
16
+ * none is found), positioning it near the top rather than flush against it.
17
+ */
18
+ export declare function scrollElementIntoView(target: HTMLElement, margin?: number): void;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared DOM helpers for locating an editor's scroll container and scrolling
3
+ * an element into view within it. `AnchorScrollPlugin` (anchor-link
4
+ * navigation) and `OutlinePlugin` (table-of-contents navigation) both need to
5
+ * find "the nearest thing that actually scrolls" without the host declaring
6
+ * one explicitly (FR-006) — this is the one place that logic lives.
7
+ */
8
+ /**
9
+ * Find the scrollable container for an element: the known editor scroll ids
10
+ * first, then the nearest ancestor that actually scrolls (robust to host
11
+ * markup).
12
+ */
13
+ export function scrollContainerFor(element) {
14
+ // Assumes the two known ids are never nested inside one another; neither
15
+ // known host does. If that ever changes, the closer one should win instead.
16
+ const byId = element.closest('#editor-scroll-container') ||
17
+ element.closest('#editor-panel-scroll-container');
18
+ if (byId)
19
+ return byId;
20
+ let el = element.parentElement;
21
+ while (el) {
22
+ const overflowY = getComputedStyle(el).overflowY;
23
+ if (/(auto|scroll)/.test(overflowY) && el.scrollHeight > el.clientHeight)
24
+ return el;
25
+ el = el.parentElement;
26
+ }
27
+ return null;
28
+ }
29
+ /**
30
+ * Scroll `target` into view within its scroll container (or the viewport, if
31
+ * none is found), positioning it near the top rather than flush against it.
32
+ */
33
+ export function scrollElementIntoView(target, margin = 16) {
34
+ const container = scrollContainerFor(target);
35
+ if (container) {
36
+ // rect-based offset is robust regardless of the target's offsetParent;
37
+ // leave a small margin so it sits near the top, not flush against it.
38
+ const top = Math.max(0, target.getBoundingClientRect().top -
39
+ container.getBoundingClientRect().top +
40
+ container.scrollTop -
41
+ margin);
42
+ container.scrollTo({ top, behavior: 'smooth' });
43
+ }
44
+ else {
45
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
46
+ }
47
+ }
package/dist/index.d.ts CHANGED
@@ -18,6 +18,10 @@ export type { CursorState } from './app/App.js';
18
18
  export { Editor } from './app/editor/index.js';
19
19
  export type { SweepFn } from './app/editor/AmbientCorrectionPlugin.js';
20
20
  export type { SelectionContextMenuEvent } from './app/editor/SelectionContextMenuPlugin.js';
21
+ export { DocumentOutline } from './app/editor/DocumentOutline.js';
22
+ export type { DocumentOutlineProps } from './app/editor/DocumentOutline.js';
23
+ export { createDocumentOutlineHandle } from './app/editor/documentOutlineHandle.js';
24
+ export type { DocumentOutlineHandle, DocumentOutlineSnapshot, OutlineEntry, } from './app/editor/documentOutlineHandle.js';
21
25
  export { OPEN_ANNOTATION_COMPOSER_COMMAND } from './app/editor/annotationCommands.js';
22
26
  export type { AnnotationCreateEvent } from './app/editor/AnnotationPlugin.js';
23
27
  export type { Annotation, AnnotationKind, AnnotationKindConfig, AnnotationKindConfigs, AnnotationCreateAffordance, AnnotationMarkerStyle, AnnotationPresentation, AnnotationEditorHandle, MarkerTarget, } from './annotations/types.js';
package/dist/index.js CHANGED
@@ -15,6 +15,12 @@ export { createHostMessageApi, useHostMessages } from './host/messages.js';
15
15
  // --- Components ------------------------------------------------------------
16
16
  export { App } from './app/App.js';
17
17
  export { Editor } from './app/editor/index.js';
18
+ // --- Document outline (issue #69) -------------------------------------------
19
+ // `createDocumentOutlineHandle()` makes the controller shared between one
20
+ // `<Editor documentOutlineHandle={…}>` and one `<DocumentOutline handle={…}>`
21
+ // — create it once per editor instance and pass the same object to both.
22
+ export { DocumentOutline } from './app/editor/DocumentOutline.js';
23
+ export { createDocumentOutlineHandle } from './app/editor/documentOutlineHandle.js';
18
24
  // --- Annotations, React surface (ADR-077) ---------------------------------
19
25
  // The kind-configuration types a host needs to turn the mechanism on, plus the
20
26
  // create-event shape. The DOM-free anchor model, resolver and marker-target
@@ -230,6 +230,65 @@ function normalizeWikiLinkNodes(node) {
230
230
  }
231
231
  return node;
232
232
  }
233
+ /**
234
+ * Widen every GFM table delimiter-row cell mdast-util-to-markdown emits at
235
+ * its library-enforced minimum (a bare `-`, or `:-`, `-:`, `:-:` with
236
+ * alignment colons) up to the conventional three-hyphen form GFM,
237
+ * remark-stringify, and Prettier all produce (`---`, `:---`, `---:`,
238
+ * `:---:`). `tablePipeAlign: false` (see the gfmToMarkdown() call below) is
239
+ * the one option controlling both column-width padding and this dash-count
240
+ * minimum in markdown-table — there is no way to keep the former's "no
241
+ * cell-width padding" behavior while asking for the latter's wider default
242
+ * separately, so this text-level post-process patches it after the fact
243
+ * (#60).
244
+ *
245
+ * A GFM table's delimiter row is always exactly the *second* line of a
246
+ * contiguous run of pipe-led lines (the first is the header row). Widening
247
+ * is restricted to that position rather than to any line that merely looks
248
+ * delimiter-shaped — a body row whose cells' entire content happens to be a
249
+ * bare `-` (e.g. `| - | - |`) renders identically to a delimiter row, and a
250
+ * shape-only match would silently rewrite that user content instead of
251
+ * leaving it alone (caught in review — see PR #61). Only the exact
252
+ * single-dash form the library deterministically emits matches at that
253
+ * position, so an already-conventional delimiter row (3+ dashes) is left
254
+ * untouched — idempotence for free. Lines inside fenced code blocks or `$$`
255
+ * math blocks are excluded from the pipe-line run entirely, mirroring the
256
+ * intraword-underscore post-process below, so a fenced block that happens
257
+ * to contain a line shaped like a delimiter row is not rewritten. The
258
+ * pipe-line check also tolerates a leading run of `>` blockquote markers
259
+ * (each optionally preceded/followed by spaces or tabs) before the opening
260
+ * pipe, since a table nested in a blockquote (or a blockquote-wrapped list)
261
+ * serializes with a `> ` prefix on every line.
262
+ */
263
+ function widenTableDelimiterDashes(markdown) {
264
+ const protectedRegionPattern = /^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1[ \t]*$|\$\$[\s\S]*?\$\$/gmu;
265
+ const protectedRanges = [];
266
+ let region;
267
+ while ((region = protectedRegionPattern.exec(markdown)) !== null) {
268
+ protectedRanges.push([region.index, region.index + region[0].length]);
269
+ }
270
+ const pipeLinePattern = /^(?:[ \t]*>)*[ \t]*\|/;
271
+ const delimiterRowPattern = /^(?:[ \t]*>)*[ \t]*\|(?:\s*:?-:?\s*\|)+[ \t]*$/;
272
+ const lines = markdown.split('\n');
273
+ let offset = 0;
274
+ let pipeRunLength = 0;
275
+ for (let i = 0; i < lines.length; i++) {
276
+ const line = lines[i];
277
+ const lineStart = offset;
278
+ const lineEnd = lineStart + line.length;
279
+ offset = lineEnd + 1; // account for the '\n' consumed by split()
280
+ const inProtectedRegion = protectedRanges.some(([start, end]) => lineStart < end && lineEnd > start);
281
+ if (inProtectedRegion || !pipeLinePattern.test(line)) {
282
+ pipeRunLength = 0;
283
+ continue;
284
+ }
285
+ pipeRunLength += 1;
286
+ if (pipeRunLength === 2 && delimiterRowPattern.test(line)) {
287
+ lines[i] = line.replace(/-/g, '---');
288
+ }
289
+ }
290
+ return lines.join('\n');
291
+ }
233
292
  export function stringifyMarkdown(root, options = {}) {
234
293
  // Pre-process: add checkbox text to ordered list items (GFM only outputs for unordered)
235
294
  let processedRoot = addCheckboxTextToOrderedLists(root);
@@ -330,6 +389,12 @@ export function stringifyMarkdown(root, options = {}) {
330
389
  },
331
390
  ],
332
391
  });
392
+ // Post-process: widen table delimiter-row dashes to the conventional
393
+ // three-hyphen width (#60). Must run first, before any of the other
394
+ // backslash-stripping/restoring post-processes below, since none of them
395
+ // touch dashes and this step's own verbatim-region skip logic is simplest
396
+ // to reason about against toMarkdown()'s raw output.
397
+ result = widenTableDelimiterDashes(result);
333
398
  // Post-process: convert escaped spaces back to regular spaces
334
399
  // mdast-util-to-markdown escapes leading/trailing spaces at paragraph
335
400
  // boundaries as &#x20; which looks ugly in raw markdown