mrmd-editor 0.7.1 → 0.8.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 (58) hide show
  1. package/package.json +3 -1
  2. package/src/commands.js +112 -4
  3. package/src/comment-syntax.js +364 -39
  4. package/src/config/handlers.js +1 -2
  5. package/src/config/schema.js +46 -4
  6. package/src/document-template.js +2236 -0
  7. package/src/frontmatter-updater.js +204 -74
  8. package/src/grammar.js +758 -0
  9. package/src/index.js +1074 -55
  10. package/src/keymap.js +11 -2
  11. package/src/markdown/block-decorations.js +108 -5
  12. package/src/markdown/facets.js +37 -0
  13. package/src/markdown/html-inline.js +9 -5
  14. package/src/markdown/index.js +13 -3
  15. package/src/markdown/inline-commands.js +256 -0
  16. package/src/markdown/inline-model.js +578 -0
  17. package/src/markdown/inline-state.js +103 -0
  18. package/src/markdown/renderer.js +219 -12
  19. package/src/markdown/styles.js +290 -3
  20. package/src/markdown/widgets/alert-title.js +10 -8
  21. package/src/markdown/widgets/frontmatter.js +0 -6
  22. package/src/markdown/widgets/index.js +1 -0
  23. package/src/markdown/widgets/list-marker.js +29 -0
  24. package/src/markdown/wysiwyg.js +1158 -0
  25. package/src/mrp-types.js +2 -0
  26. package/src/output-widget.js +532 -18
  27. package/src/page-view-pagination.js +127 -0
  28. package/src/runtime-lsp.js +1757 -150
  29. package/src/section-controls/commands.js +617 -0
  30. package/src/section-controls/index.js +63 -0
  31. package/src/section-controls/plugin.js +165 -0
  32. package/src/section-controls/widgets.js +936 -0
  33. package/src/shell/ai-menu.js +11 -0
  34. package/src/shell/components/context-panel.js +572 -0
  35. package/src/shell/components/status-bar.js +10 -2
  36. package/src/shell/layouts/studio.js +206 -14
  37. package/src/shell/orchestrator-client.js +69 -0
  38. package/src/spellcheck.js +166 -0
  39. package/src/tables/README.md +97 -0
  40. package/src/tables/commands/insert-linked-table.js +122 -0
  41. package/src/tables/commands/open-table-workspace.js +43 -0
  42. package/src/tables/index.js +24 -0
  43. package/src/tables/jobs/client.js +158 -0
  44. package/src/tables/parsing/anchors.js +82 -0
  45. package/src/tables/parsing/linked-table-blocks.js +61 -0
  46. package/src/tables/state/linked-table-state.js +68 -0
  47. package/src/tables/widgets/linked-table-source-banner.js +77 -0
  48. package/src/tables/widgets/linked-table-widget.js +256 -0
  49. package/src/tables/workspace/controller.js +616 -0
  50. package/src/term-pty-client.js +51 -2
  51. package/src/term-widget.js +43 -3
  52. package/src/widgets/theme-utils.js +24 -16
  53. package/src/widgets/theme.js +1015 -1
  54. package/src/runtime-codelens/detector.js +0 -279
  55. package/src/runtime-codelens/index.js +0 -76
  56. package/src/runtime-codelens/plugin.js +0 -142
  57. package/src/runtime-codelens/styles.js +0 -184
  58. package/src/runtime-codelens/widgets.js +0 -216
@@ -0,0 +1,97 @@
1
+ # mrmd-editor/src/tables
2
+
3
+ Internal linked-table UI/editor adapter subsystem.
4
+
5
+ This folder is where MRMD-specific editor integration lives.
6
+ It should depend on pure table packages, but the pure packages must not depend back on this folder.
7
+
8
+ ## Ownership
9
+
10
+ - linked-table block detection inside editor documents
11
+ - small/document view widget
12
+ - active-inline interactions
13
+ - full grid workspace shell
14
+ - editor commands for linked tables
15
+ - browser-side table job client
16
+
17
+ ## Planned tree
18
+
19
+ ```text
20
+ mrmd-editor/src/tables/
21
+ index.js
22
+ facets.js
23
+ commands/
24
+ insert-linked-table.js
25
+ open-table-workspace.js
26
+ open-table-source.js
27
+ refresh-linked-table.js
28
+ parsing/
29
+ linked-table-blocks.js
30
+ anchors.js
31
+ state/
32
+ linked-table-state.js
33
+ table-workspace-state.js
34
+ decorations/
35
+ linked-table-decorations.js
36
+ widgets/
37
+ linked-table-widget.js
38
+ linked-table-chrome.js
39
+ linked-table-status.js
40
+ workspace/
41
+ controller.js
42
+ layout.js
43
+ result-grid.js
44
+ source-grid.js
45
+ panels/
46
+ join-panel.js
47
+ bind-rows-panel.js
48
+ bind-cols-panel.js
49
+ formula-panel.js
50
+ document-view-panel.js
51
+ source-info-panel.js
52
+ jobs/
53
+ client.js
54
+ status.js
55
+ styles.js
56
+ ```
57
+
58
+ ## Current phase
59
+
60
+ The first editor-side shell is now wired through the first real sort/import/source/workspace paths for:
61
+ - linked-table block detection from markdown/spec headers
62
+ - small embedded linked-table widget rendering
63
+ - linked badge/chrome row
64
+ - header-sort event dispatch
65
+ - explicit `Open grid` / `Open source` / `Reveal source` / `Open markdown` / `Refresh` action dispatch
66
+ - browser-side `tableJobs` client + wait/status helpers
67
+ - Yjs block-anchor creation for linked-table jobs
68
+ - controller wiring from header-sort widget actions to real `tableJobs` requests
69
+ - host-backed linked-table import insertion helpers in the editor layer
70
+ - first studio/status-bar UI command path for linked-table import
71
+ - block-local markdown-source reveal with a visible `Return to linked view` path
72
+ - first widget job-status badge updates for linked tables
73
+ - basic source-mtime stale detection for visible linked tables
74
+ - first real `Open grid` workspace panel hook (implemented in the Electron app for now)
75
+ - no reveal-on-click for linked tables in the markdown block-decoration path
76
+
77
+ ## First slice here
78
+
79
+ Phase 1 editor work should only prove:
80
+ - linked-table block detection
81
+ - small embedded widget rendering
82
+ - linked badge/chrome row
83
+ - header-sort action
84
+ - `Open grid` shell
85
+ - no reveal-on-click for linked tables
86
+
87
+ Known gaps after the first manual app test:
88
+ - latency is visible because the first materialization path is still simple and mostly unoptimized
89
+ - widget chrome status is still only a first-pass badge layer, not the full stale/progress/error design yet
90
+ - the new workspace is still an MVP inspection shell, not yet the full editing/data-workbench experience
91
+
92
+ ## Non-goals for the first slice
93
+
94
+ - full grid editing richness
95
+ - source editor tabs
96
+ - join/bind UI end to end
97
+ - raw-source editing UI
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Linked-table import/insert helpers.
3
+ */
4
+
5
+ function getHostApi(hostApi) {
6
+ if (hostApi) return hostApi;
7
+ if (typeof window !== 'undefined') return window.electronAPI || null;
8
+ return null;
9
+ }
10
+
11
+ function ensureTrailingNewlinePair(text, side) {
12
+ const value = String(text || '');
13
+ if (!value) return '';
14
+
15
+ if (side === 'before') {
16
+ if (value.endsWith('\n\n')) return '';
17
+ if (value.endsWith('\n')) return '\n';
18
+ return '\n\n';
19
+ }
20
+
21
+ if (value.startsWith('\n\n')) return '';
22
+ if (value.startsWith('\n')) return '\n';
23
+ return '\n\n';
24
+ }
25
+
26
+ export function canImportLinkedTableFromHost(hostApi) {
27
+ const host = getHostApi(hostApi);
28
+ return typeof host?.table?.importDelimited === 'function';
29
+ }
30
+
31
+ export function normalizeLinkedTableBlockInsertion(docText, from, to, blockMarkdown) {
32
+ const fullText = String(docText || '');
33
+ const safeFrom = Math.max(0, Math.min(fullText.length, Number.isInteger(from) ? from : 0));
34
+ const safeTo = Math.max(safeFrom, Math.min(fullText.length, Number.isInteger(to) ? to : safeFrom));
35
+ const before = fullText.slice(0, safeFrom);
36
+ const after = fullText.slice(safeTo);
37
+ const block = String(blockMarkdown || '').trim();
38
+
39
+ if (!block) {
40
+ return {
41
+ from: safeFrom,
42
+ to: safeTo,
43
+ insert: '',
44
+ selectionAnchor: safeFrom,
45
+ };
46
+ }
47
+
48
+ const prefix = before.length > 0 ? ensureTrailingNewlinePair(before, 'before') : '';
49
+ const suffix = after.length > 0 ? ensureTrailingNewlinePair(after, 'after') : '';
50
+ const insert = `${prefix}${block}${suffix}`;
51
+
52
+ return {
53
+ from: safeFrom,
54
+ to: safeTo,
55
+ insert,
56
+ selectionAnchor: safeFrom + insert.length,
57
+ };
58
+ }
59
+
60
+ export function insertLinkedTableBlock(editor, blockMarkdown, options = {}) {
61
+ const view = editor?.view || options.view;
62
+ if (!view?.state || typeof view.dispatch !== 'function') {
63
+ throw new Error('insertLinkedTableBlock requires an editor/view with dispatch support');
64
+ }
65
+
66
+ const selection = options.selection || view.state.selection?.main || { from: 0, to: 0 };
67
+ const docText = view.state.doc?.toString?.() || '';
68
+ const normalized = normalizeLinkedTableBlockInsertion(docText, selection.from, selection.to, blockMarkdown);
69
+
70
+ view.dispatch({
71
+ changes: {
72
+ from: normalized.from,
73
+ to: normalized.to,
74
+ insert: normalized.insert,
75
+ },
76
+ selection: {
77
+ anchor: normalized.selectionAnchor,
78
+ },
79
+ scrollIntoView: true,
80
+ });
81
+
82
+ return normalized;
83
+ }
84
+
85
+ export async function importLinkedTableFromHost(editor, options = {}) {
86
+ const host = getHostApi(options.hostApi);
87
+ if (!canImportLinkedTableFromHost(host)) {
88
+ throw new Error('Linked-table import requires an Electron host exposing `electronAPI.table.importDelimited()`');
89
+ }
90
+
91
+ const projectRoot = options.projectRoot || editor?.getLinkedTableHostContext?.()?.projectRoot || null;
92
+ const documentPath = options.documentPath || editor?.getLinkedTableHostContext?.()?.documentPath || null;
93
+ const sourceFilePath = options.sourceFilePath;
94
+
95
+ if (!projectRoot) throw new Error('Linked-table import requires a project root');
96
+ if (!documentPath) throw new Error('Linked-table import requires a document path');
97
+ if (!sourceFilePath) throw new Error('Linked-table import requires a source file path');
98
+
99
+ const result = await host.table.importDelimited({
100
+ projectRoot,
101
+ documentPath,
102
+ sourceFilePath,
103
+ tableId: options.tableId,
104
+ label: options.label,
105
+ cacheFormat: options.cacheFormat,
106
+ maxRows: options.maxRows,
107
+ overflow: options.overflow,
108
+ });
109
+
110
+ insertLinkedTableBlock(editor, result.blockMarkdown, {
111
+ selection: options.selection,
112
+ });
113
+
114
+ return result;
115
+ }
116
+
117
+ export default {
118
+ canImportLinkedTableFromHost,
119
+ normalizeLinkedTableBlockInsertion,
120
+ insertLinkedTableBlock,
121
+ importLinkedTableFromHost,
122
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Linked-table workspace command/event helpers.
3
+ */
4
+
5
+ export const LINKED_TABLE_EVENT = 'mrmd-linked-table-action';
6
+
7
+ /**
8
+ * Dispatch a linked-table UI action from the editor surface.
9
+ * Host/app code can listen on `view.dom` or `window`.
10
+ *
11
+ * @param {import('@codemirror/view').EditorView} view
12
+ * @param {Object} detail
13
+ * @returns {boolean}
14
+ */
15
+ export function dispatchLinkedTableAction(view, detail) {
16
+ if (!view?.dom) return false;
17
+ const event = new CustomEvent(LINKED_TABLE_EVENT, {
18
+ bubbles: true,
19
+ detail,
20
+ });
21
+ view.dom.dispatchEvent(event);
22
+ return true;
23
+ }
24
+
25
+ /**
26
+ * Convenience helper for opening the full linked-table workspace.
27
+ *
28
+ * @param {import('@codemirror/view').EditorView} view
29
+ * @param {Object} detail
30
+ * @returns {boolean}
31
+ */
32
+ export function openLinkedTableWorkspace(view, detail) {
33
+ return dispatchLinkedTableAction(view, {
34
+ action: 'open-grid',
35
+ ...detail,
36
+ });
37
+ }
38
+
39
+ export default {
40
+ LINKED_TABLE_EVENT,
41
+ dispatchLinkedTableAction,
42
+ openLinkedTableWorkspace,
43
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Linked-table editor integration exports.
3
+ */
4
+
5
+ export { findLinkedTableBlocksInState, getLinkedTableBlockRange, isRangeInsideLinkedTable } from './parsing/linked-table-blocks.js';
6
+ export { createLinkedTableBlockAnchor, resolveLinkedTableBlockAnchor } from './parsing/anchors.js';
7
+ export {
8
+ linkedTableMarkdownState,
9
+ revealLinkedTableMarkdownEffect,
10
+ hideLinkedTableMarkdownEffect,
11
+ clearLinkedTableMarkdownEffect,
12
+ isLinkedTableMarkdownOpen,
13
+ } from './state/linked-table-state.js';
14
+ export { LINKED_TABLE_EVENT, dispatchLinkedTableAction, openLinkedTableWorkspace } from './commands/open-table-workspace.js';
15
+ export {
16
+ canImportLinkedTableFromHost,
17
+ normalizeLinkedTableBlockInsertion,
18
+ insertLinkedTableBlock,
19
+ importLinkedTableFromHost,
20
+ } from './commands/insert-linked-table.js';
21
+ export { TableJobsClient, TABLE_JOB_STATUS, createTableJobsClient } from './jobs/client.js';
22
+ export { LinkedTableController, createLinkedTableController } from './workspace/controller.js';
23
+ export { LinkedTableWidget } from './widgets/linked-table-widget.js';
24
+ export { LinkedTableSourceBannerWidget } from './widgets/linked-table-source-banner.js';
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Browser-side linked-table job coordination.
3
+ *
4
+ * Mirrors the monitor execution coordination pattern but for `tableJobs`.
5
+ */
6
+
7
+ import * as Y from 'yjs';
8
+
9
+ export const TABLE_JOB_STATUS = {
10
+ REQUESTED: 'requested',
11
+ CLAIMED: 'claimed',
12
+ RUNNING: 'running',
13
+ WRITING: 'writing',
14
+ COMPLETED: 'completed',
15
+ ERROR: 'error',
16
+ CANCELLED: 'cancelled',
17
+ };
18
+
19
+ export class TableJobsClient {
20
+ constructor(ydoc, clientId = ydoc?.clientID) {
21
+ this.ydoc = ydoc;
22
+ this.clientId = clientId;
23
+ this.jobs = ydoc.getMap('tableJobs');
24
+ this._statusCallbacks = new Map();
25
+ this._observers = new Set();
26
+ this._setupObserver();
27
+ }
28
+
29
+ static generateJobId() {
30
+ return `tablejob-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
31
+ }
32
+
33
+ _setupObserver() {
34
+ const observer = (event) => {
35
+ event.changes.keys.forEach((change, jobId) => {
36
+ const job = this.jobs.get(jobId);
37
+ const callback = this._statusCallbacks.get(jobId);
38
+ if (callback && job) {
39
+ callback(job.status, job);
40
+ }
41
+ });
42
+ };
43
+
44
+ this.jobs.observe(observer);
45
+ this._observers.add(observer);
46
+ }
47
+
48
+ requestJob({ tableId, blockAnchor = null, spec = null, jobType = 'applyOps', opList = [], metadata = {} }) {
49
+ const jobId = TableJobsClient.generateJobId();
50
+ this.jobs.set(jobId, {
51
+ id: jobId,
52
+ tableId,
53
+ jobType,
54
+ status: TABLE_JOB_STATUS.REQUESTED,
55
+ requestedBy: this.clientId,
56
+ requestedAt: Date.now(),
57
+ claimedBy: null,
58
+ claimedAt: null,
59
+ completedAt: null,
60
+ blockAnchor,
61
+ spec,
62
+ opList,
63
+ metadata,
64
+ result: null,
65
+ error: null,
66
+ });
67
+ return jobId;
68
+ }
69
+
70
+ requestSort({ tableId, blockAnchor = null, spec = null, column, direction = 'asc', metadata = {} }) {
71
+ return this.requestJob({
72
+ tableId,
73
+ blockAnchor,
74
+ spec,
75
+ jobType: 'applyOps',
76
+ opList: [{ type: 'sort', column, direction }],
77
+ metadata,
78
+ });
79
+ }
80
+
81
+ requestRefresh({ tableId, blockAnchor = null, spec = null, metadata = {} }) {
82
+ return this.requestJob({
83
+ tableId,
84
+ blockAnchor,
85
+ spec,
86
+ jobType: 'refresh',
87
+ opList: [],
88
+ metadata,
89
+ });
90
+ }
91
+
92
+ getJob(jobId) {
93
+ return this.jobs.get(jobId);
94
+ }
95
+
96
+ cancelJob(jobId) {
97
+ const job = this.jobs.get(jobId);
98
+ if (!job) return;
99
+ if ([TABLE_JOB_STATUS.COMPLETED, TABLE_JOB_STATUS.ERROR, TABLE_JOB_STATUS.CANCELLED].includes(job.status)) {
100
+ return;
101
+ }
102
+ this.jobs.set(jobId, {
103
+ ...job,
104
+ status: TABLE_JOB_STATUS.CANCELLED,
105
+ completedAt: Date.now(),
106
+ });
107
+ }
108
+
109
+ onStatusChange(jobId, callback) {
110
+ this._statusCallbacks.set(jobId, callback);
111
+ return () => {
112
+ this._statusCallbacks.delete(jobId);
113
+ };
114
+ }
115
+
116
+ waitForStatus(jobId, targetStatus, timeout = 30000) {
117
+ const statuses = Array.isArray(targetStatus) ? targetStatus : [targetStatus];
118
+
119
+ return new Promise((resolve, reject) => {
120
+ const current = this.getJob(jobId);
121
+ if (current && statuses.includes(current.status)) {
122
+ resolve(current);
123
+ return;
124
+ }
125
+
126
+ let timeoutId = null;
127
+ const unsubscribe = this.onStatusChange(jobId, (status, job) => {
128
+ if (!statuses.includes(status)) return;
129
+ if (timeoutId) clearTimeout(timeoutId);
130
+ unsubscribe();
131
+ resolve(job);
132
+ });
133
+
134
+ timeoutId = setTimeout(() => {
135
+ unsubscribe();
136
+ reject(new Error(`Timeout waiting for table job status ${statuses.join('/')} on ${jobId}`));
137
+ }, timeout);
138
+ });
139
+ }
140
+
141
+ destroy() {
142
+ for (const observer of this._observers) {
143
+ this.jobs.unobserve(observer);
144
+ }
145
+ this._observers.clear();
146
+ this._statusCallbacks.clear();
147
+ }
148
+ }
149
+
150
+ export function createTableJobsClient(ydoc) {
151
+ return new TableJobsClient(ydoc, ydoc.clientID);
152
+ }
153
+
154
+ export default {
155
+ TABLE_JOB_STATUS,
156
+ TableJobsClient,
157
+ createTableJobsClient,
158
+ };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Linked-table Yjs anchor helpers.
3
+ *
4
+ * These anchors let table jobs survive surrounding document edits.
5
+ */
6
+
7
+ import * as Y from 'yjs';
8
+
9
+ function assertRange(range) {
10
+ const from = range?.headerFrom ?? range?.from;
11
+ const to = range?.snapshotTo ?? range?.to;
12
+
13
+ if (!Number.isInteger(from) || from < 0) {
14
+ throw new TypeError('Linked-table anchor range must include a non-negative `from`/`headerFrom`');
15
+ }
16
+ if (!Number.isInteger(to) || to < from) {
17
+ throw new TypeError('Linked-table anchor range must include a `to`/`snapshotTo` >= `from`');
18
+ }
19
+
20
+ return { from, to };
21
+ }
22
+
23
+ function toRelativePositionJson(yText, index, assoc) {
24
+ const relPos = Y.createRelativePositionFromTypeIndex(yText, index, assoc);
25
+ return Y.relativePositionToJSON(relPos);
26
+ }
27
+
28
+ function toAbsoluteIndex(ydoc, relPosJson) {
29
+ if (!relPosJson) return null;
30
+
31
+ try {
32
+ const relPos = Y.createRelativePositionFromJSON(relPosJson);
33
+ const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, ydoc);
34
+ return absPos?.index ?? null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Create a Yjs-stable anchor for one linked-table block.
42
+ *
43
+ * Start uses right association so inserts at the exact start stay outside the block.
44
+ * End uses left association so inserts at the exact end stay outside the block.
45
+ */
46
+ export function createLinkedTableBlockAnchor(yText, range, options = {}) {
47
+ if (!yText?.doc) {
48
+ throw new TypeError('createLinkedTableBlockAnchor requires a Y.Text attached to a Y.Doc');
49
+ }
50
+
51
+ const resolved = assertRange(range);
52
+
53
+ return {
54
+ type: 'linked-table-block-anchor-v1',
55
+ tableId: options.tableId || range?.tableId || range?.spec?.id || null,
56
+ from: toRelativePositionJson(yText, resolved.from, 1),
57
+ to: toRelativePositionJson(yText, resolved.to, -1),
58
+ createdAt: Date.now(),
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Resolve a linked-table anchor back to absolute document offsets.
64
+ */
65
+ export function resolveLinkedTableBlockAnchor(ydoc, anchor) {
66
+ if (!ydoc || !anchor) return null;
67
+
68
+ const from = toAbsoluteIndex(ydoc, anchor.from);
69
+ const to = toAbsoluteIndex(ydoc, anchor.to);
70
+ if (!Number.isInteger(from) || !Number.isInteger(to) || to < from) return null;
71
+
72
+ return {
73
+ from,
74
+ to,
75
+ tableId: anchor.tableId || null,
76
+ };
77
+ }
78
+
79
+ export default {
80
+ createLinkedTableBlockAnchor,
81
+ resolveLinkedTableBlockAnchor,
82
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Linked-table block parsing helpers for the editor.
3
+ *
4
+ * Bridges CodeMirror editor state/doc text to the pure `mrmd-table-spec`
5
+ * block-discovery layer.
6
+ */
7
+
8
+ import { findLinkedTableBlocks } from '../../../../mrmd-table-spec/src/index.js';
9
+
10
+ function splitLines(text) {
11
+ return String(text || '').split(/\r?\n/);
12
+ }
13
+
14
+ /**
15
+ * Find linked-table blocks in the current editor state.
16
+ * Enriches pure spec blocks with table text/lines for widget rendering.
17
+ *
18
+ * @param {import('@codemirror/state').EditorState} state
19
+ * @returns {Array<Object>}
20
+ */
21
+ export function findLinkedTableBlocksInState(state) {
22
+ const text = state.doc.toString();
23
+ return findLinkedTableBlocks(text).map((block) => ({
24
+ ...block,
25
+ headerText: text.slice(block.headerFrom, block.headerTo),
26
+ tableText: text.slice(block.tableFrom, block.tableTo),
27
+ tableLines: splitLines(text.slice(block.tableFrom, block.tableTo)),
28
+ }));
29
+ }
30
+
31
+ /**
32
+ * Get the full replacement range for a linked table block.
33
+ * Includes hidden metadata header + visible snapshot region.
34
+ *
35
+ * @param {Object} block
36
+ * @returns {{from:number,to:number}}
37
+ */
38
+ export function getLinkedTableBlockRange(block) {
39
+ return {
40
+ from: block.headerFrom,
41
+ to: block.snapshotTo,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Whether a normal markdown table range is covered by a linked-table block.
47
+ * Used to suppress the legacy plain-table renderer for linked snapshots.
48
+ *
49
+ * @param {{from:number,to:number}} range
50
+ * @param {Array<Object>} linkedBlocks
51
+ * @returns {boolean}
52
+ */
53
+ export function isRangeInsideLinkedTable(range, linkedBlocks) {
54
+ return linkedBlocks.some((block) => range.from >= block.tableFrom && range.to <= block.tableTo);
55
+ }
56
+
57
+ export default {
58
+ findLinkedTableBlocksInState,
59
+ getLinkedTableBlockRange,
60
+ isRangeInsideLinkedTable,
61
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Linked-table local editor state.
3
+ *
4
+ * Phase 1 uses this to reveal one linked table's raw markdown without forcing
5
+ * the whole editor into global source mode.
6
+ */
7
+
8
+ import { StateEffect, StateField } from '@codemirror/state';
9
+
10
+ export const revealLinkedTableMarkdownEffect = StateEffect.define();
11
+ export const hideLinkedTableMarkdownEffect = StateEffect.define();
12
+ export const clearLinkedTableMarkdownEffect = StateEffect.define();
13
+
14
+ function normalizeTableId(value) {
15
+ const tableId = String(value || '').trim();
16
+ return tableId || null;
17
+ }
18
+
19
+ export const linkedTableMarkdownState = StateField.define({
20
+ create() {
21
+ return new Set();
22
+ },
23
+
24
+ update(value, tr) {
25
+ let next = value;
26
+
27
+ for (const effect of tr.effects) {
28
+ if (effect.is(clearLinkedTableMarkdownEffect)) {
29
+ if (next.size > 0) next = new Set();
30
+ continue;
31
+ }
32
+
33
+ if (effect.is(revealLinkedTableMarkdownEffect)) {
34
+ const tableId = normalizeTableId(effect.value?.tableId ?? effect.value);
35
+ if (tableId && !next.has(tableId)) {
36
+ next = new Set(next);
37
+ next.add(tableId);
38
+ }
39
+ continue;
40
+ }
41
+
42
+ if (effect.is(hideLinkedTableMarkdownEffect)) {
43
+ const tableId = normalizeTableId(effect.value?.tableId ?? effect.value);
44
+ if (tableId && next.has(tableId)) {
45
+ next = new Set(next);
46
+ next.delete(tableId);
47
+ }
48
+ }
49
+ }
50
+
51
+ return next;
52
+ },
53
+ });
54
+
55
+ export function isLinkedTableMarkdownOpen(state, tableId) {
56
+ const normalized = normalizeTableId(tableId);
57
+ if (!normalized) return false;
58
+ const revealed = state.field(linkedTableMarkdownState, false);
59
+ return !!(revealed && revealed.has(normalized));
60
+ }
61
+
62
+ export default {
63
+ linkedTableMarkdownState,
64
+ revealLinkedTableMarkdownEffect,
65
+ hideLinkedTableMarkdownEffect,
66
+ clearLinkedTableMarkdownEffect,
67
+ isLinkedTableMarkdownOpen,
68
+ };