mrmd-editor 0.7.0 → 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 (61) 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/execution.js +69 -15
  8. package/src/frontmatter-updater.js +204 -74
  9. package/src/grammar.js +758 -0
  10. package/src/index.js +1120 -55
  11. package/src/keymap.js +11 -2
  12. package/src/markdown/block-decorations.js +108 -5
  13. package/src/markdown/facets.js +37 -0
  14. package/src/markdown/html-inline.js +9 -5
  15. package/src/markdown/index.js +13 -3
  16. package/src/markdown/inline-commands.js +256 -0
  17. package/src/markdown/inline-model.js +578 -0
  18. package/src/markdown/inline-state.js +103 -0
  19. package/src/markdown/renderer.js +219 -12
  20. package/src/markdown/styles.js +290 -3
  21. package/src/markdown/widgets/alert-title.js +10 -8
  22. package/src/markdown/widgets/frontmatter.js +0 -6
  23. package/src/markdown/widgets/index.js +1 -0
  24. package/src/markdown/widgets/list-marker.js +29 -0
  25. package/src/markdown/wysiwyg.js +1158 -0
  26. package/src/mrp-types.js +2 -0
  27. package/src/output-widget.js +532 -18
  28. package/src/page-view-pagination.js +127 -0
  29. package/src/runtime-lsp.js +1757 -150
  30. package/src/section-controls/commands.js +617 -0
  31. package/src/section-controls/index.js +63 -0
  32. package/src/section-controls/plugin.js +165 -0
  33. package/src/section-controls/widgets.js +936 -0
  34. package/src/shell/ai-menu.js +11 -0
  35. package/src/shell/components/context-panel.js +572 -0
  36. package/src/shell/components/status-bar.js +218 -8
  37. package/src/shell/dialogs/file-picker.js +211 -0
  38. package/src/shell/layouts/studio.js +229 -14
  39. package/src/shell/orchestrator-client.js +114 -0
  40. package/src/shell/styles.js +62 -0
  41. package/src/spellcheck.js +166 -0
  42. package/src/tables/README.md +97 -0
  43. package/src/tables/commands/insert-linked-table.js +122 -0
  44. package/src/tables/commands/open-table-workspace.js +43 -0
  45. package/src/tables/index.js +24 -0
  46. package/src/tables/jobs/client.js +158 -0
  47. package/src/tables/parsing/anchors.js +82 -0
  48. package/src/tables/parsing/linked-table-blocks.js +61 -0
  49. package/src/tables/state/linked-table-state.js +68 -0
  50. package/src/tables/widgets/linked-table-source-banner.js +77 -0
  51. package/src/tables/widgets/linked-table-widget.js +256 -0
  52. package/src/tables/workspace/controller.js +616 -0
  53. package/src/term-pty-client.js +111 -7
  54. package/src/term-widget.js +43 -3
  55. package/src/widgets/theme-utils.js +24 -16
  56. package/src/widgets/theme.js +1535 -1
  57. package/src/runtime-codelens/detector.js +0 -279
  58. package/src/runtime-codelens/index.js +0 -76
  59. package/src/runtime-codelens/plugin.js +0 -142
  60. package/src/runtime-codelens/styles.js +0 -184
  61. package/src/runtime-codelens/widgets.js +0 -216
@@ -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
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Banner shown when one linked table is in local markdown/source view.
3
+ */
4
+
5
+ import { WidgetType } from '@codemirror/view';
6
+ import { dispatchLinkedTableAction } from '../commands/open-table-workspace.js';
7
+
8
+ function cloneValue(value) {
9
+ if (Array.isArray(value)) return value.map(cloneValue);
10
+ if (value && typeof value === 'object') {
11
+ const out = {};
12
+ for (const key of Object.keys(value)) out[key] = cloneValue(value[key]);
13
+ return out;
14
+ }
15
+ return value;
16
+ }
17
+
18
+ function buildActionDetail(block, action, extra = {}) {
19
+ return {
20
+ action,
21
+ tableId: block.spec.id,
22
+ label: block.spec.label || block.spec.id,
23
+ spec: cloneValue(block.spec),
24
+ headerFrom: block.headerFrom,
25
+ headerTo: block.headerTo,
26
+ snapshotFrom: block.snapshotFrom,
27
+ snapshotTo: block.snapshotTo,
28
+ tableFrom: block.tableFrom,
29
+ tableTo: block.tableTo,
30
+ startLine: block.startLine,
31
+ endLine: block.endLine,
32
+ ...extra,
33
+ };
34
+ }
35
+
36
+ export class LinkedTableSourceBannerWidget extends WidgetType {
37
+ constructor(block) {
38
+ super();
39
+ this.block = block;
40
+ }
41
+
42
+ eq(other) {
43
+ return other?.block?.spec?.id === this.block.spec.id;
44
+ }
45
+
46
+ toDOM(view) {
47
+ const container = document.createElement('div');
48
+ container.className = 'cm-linked-table-source-banner';
49
+ container.dataset.tableId = this.block.spec.id;
50
+
51
+ const text = document.createElement('div');
52
+ text.className = 'cm-linked-table-source-banner-text';
53
+ text.textContent = `${this.block.spec.label || this.block.spec.id}: markdown source view`;
54
+ container.appendChild(text);
55
+
56
+ const button = document.createElement('button');
57
+ button.className = 'cm-linked-table-source-banner-action';
58
+ button.type = 'button';
59
+ button.textContent = 'Return to linked view';
60
+ button.addEventListener('click', (event) => {
61
+ event.preventDefault();
62
+ event.stopPropagation();
63
+ dispatchLinkedTableAction(view, buildActionDetail(this.block, 'close-markdown'));
64
+ });
65
+ container.appendChild(button);
66
+
67
+ return container;
68
+ }
69
+
70
+ ignoreEvent() {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ export default {
76
+ LinkedTableSourceBannerWidget,
77
+ };
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Linked-table widget for the embedded document view.
3
+ */
4
+
5
+ import { WidgetType } from '@codemirror/view';
6
+ import { TableWidget, parseTable } from '../../markdown/widgets/table.js';
7
+ import { dispatchLinkedTableAction } from '../commands/open-table-workspace.js';
8
+
9
+ function stripCaptionMarkers(text) {
10
+ const trimmed = String(text || '').trim();
11
+ if (!trimmed) return '';
12
+ if ((trimmed.startsWith('_') && trimmed.endsWith('_')) || (trimmed.startsWith('*') && trimmed.endsWith('*'))) {
13
+ return trimmed.slice(1, -1).trim();
14
+ }
15
+ return trimmed;
16
+ }
17
+
18
+ function cloneValue(value) {
19
+ if (Array.isArray(value)) return value.map(cloneValue);
20
+ if (value && typeof value === 'object') {
21
+ const out = {};
22
+ for (const key of Object.keys(value)) out[key] = cloneValue(value[key]);
23
+ return out;
24
+ }
25
+ return value;
26
+ }
27
+
28
+ function getSortableHeaderCells(table) {
29
+ const headerRow = table?.rows?.find((row) => row.isHeader && !row.isDelimiter);
30
+ if (!headerRow) return [];
31
+ return headerRow.cells.filter((cell) => !cell.hidden && cell.content.trim() !== '' && cell.content.trim() !== '>' && cell.content.trim() !== '^');
32
+ }
33
+
34
+ function inferFormats(spec) {
35
+ return Array.from(new Set((spec?.sources || []).map((source) => String(source.format || source.kind || '').trim()).filter(Boolean)));
36
+ }
37
+
38
+ function buildActionDetail(baseDetail, action, extra = {}) {
39
+ return {
40
+ ...baseDetail,
41
+ action,
42
+ ...extra,
43
+ };
44
+ }
45
+
46
+ function formatMaterializedAt(value) {
47
+ if (!value) return '';
48
+ const date = new Date(value);
49
+ if (Number.isNaN(date.getTime())) return String(value);
50
+ return date.toLocaleString();
51
+ }
52
+
53
+ export class LinkedTableWidget extends WidgetType {
54
+ constructor(block, parsedTable, contentHash, options = {}) {
55
+ super();
56
+ this.block = block;
57
+ this.parsedTable = parsedTable;
58
+ this.contentHash = contentHash;
59
+ this.options = options;
60
+ }
61
+
62
+ eq(other) {
63
+ return other?.contentHash === this.contentHash;
64
+ }
65
+
66
+ _baseDetail() {
67
+ return {
68
+ tableId: this.block.spec.id,
69
+ label: this.block.spec.label || this.block.spec.id,
70
+ spec: cloneValue(this.block.spec),
71
+ headerFrom: this.block.headerFrom,
72
+ headerTo: this.block.headerTo,
73
+ snapshotFrom: this.block.snapshotFrom,
74
+ snapshotTo: this.block.snapshotTo,
75
+ tableFrom: this.block.tableFrom,
76
+ tableTo: this.block.tableTo,
77
+ startLine: this.block.startLine,
78
+ endLine: this.block.endLine,
79
+ };
80
+ }
81
+
82
+ _dispatch(view, action, extra = {}) {
83
+ return dispatchLinkedTableAction(view, buildActionDetail(this._baseDetail(), action, extra));
84
+ }
85
+
86
+ _buildChrome(view) {
87
+ const chrome = document.createElement('div');
88
+ chrome.className = 'cm-linked-table-chrome';
89
+
90
+ const left = document.createElement('div');
91
+ left.className = 'cm-linked-table-chrome-left';
92
+
93
+ const title = document.createElement('div');
94
+ title.className = 'cm-linked-table-title';
95
+ title.textContent = this.block.spec.label || this.block.spec.id || 'Linked table';
96
+ left.appendChild(title);
97
+
98
+ const badges = document.createElement('div');
99
+ badges.className = 'cm-linked-table-badges';
100
+
101
+ const linkedBadge = document.createElement('span');
102
+ linkedBadge.className = 'cm-linked-table-badge cm-linked-table-badge-linked';
103
+ linkedBadge.textContent = 'Linked';
104
+ badges.appendChild(linkedBadge);
105
+
106
+ const engineBadge = document.createElement('span');
107
+ engineBadge.className = 'cm-linked-table-badge';
108
+ engineBadge.textContent = this.block.spec.engine || 'engine';
109
+ badges.appendChild(engineBadge);
110
+
111
+ const sourceCountBadge = document.createElement('span');
112
+ sourceCountBadge.className = 'cm-linked-table-badge';
113
+ sourceCountBadge.textContent = `${(this.block.spec.sources || []).length} source${(this.block.spec.sources || []).length === 1 ? '' : 's'}`;
114
+ badges.appendChild(sourceCountBadge);
115
+
116
+ for (const format of inferFormats(this.block.spec)) {
117
+ const badge = document.createElement('span');
118
+ badge.className = 'cm-linked-table-badge';
119
+ badge.textContent = format;
120
+ badges.appendChild(badge);
121
+ }
122
+
123
+ const statusBadge = document.createElement('span');
124
+ statusBadge.className = 'cm-linked-table-badge cm-linked-table-status-badge cm-linked-table-status-fresh';
125
+ statusBadge.textContent = 'Fresh';
126
+ const materializedAt = this.block.spec?.snapshot?.materializedAt;
127
+ if (materializedAt) {
128
+ statusBadge.title = `Last materialized ${formatMaterializedAt(materializedAt)}`;
129
+ }
130
+ badges.appendChild(statusBadge);
131
+
132
+ left.appendChild(badges);
133
+ chrome.appendChild(left);
134
+
135
+ const right = document.createElement('div');
136
+ right.className = 'cm-linked-table-actions';
137
+
138
+ const makeButton = (label, action, title, extra = {}) => {
139
+ const button = document.createElement('button');
140
+ button.className = 'cm-linked-table-action';
141
+ button.type = 'button';
142
+ button.textContent = label;
143
+ button.dataset.linkedTableAction = action;
144
+ if (title) button.title = title;
145
+ button.addEventListener('click', (event) => {
146
+ event.preventDefault();
147
+ event.stopPropagation();
148
+ this._dispatch(view, action, extra);
149
+ });
150
+ return button;
151
+ };
152
+
153
+ right.appendChild(makeButton('Open grid', 'open-grid', 'Open full linked-table workspace'));
154
+ right.appendChild(makeButton('Open source', 'open-source', 'Open the primary linked-table source file'));
155
+ right.appendChild(makeButton('Reveal source', 'reveal-source', 'Reveal the primary linked-table source in the host file manager'));
156
+ right.appendChild(makeButton('Open markdown', 'open-markdown', 'Open raw markdown for this linked table'));
157
+ right.appendChild(makeButton('Refresh', 'refresh', 'Refresh linked table materialization'));
158
+
159
+ chrome.appendChild(right);
160
+ return chrome;
161
+ }
162
+
163
+ _buildCaption(text, position) {
164
+ const captionText = stripCaptionMarkers(text);
165
+ if (!captionText) return null;
166
+ const el = document.createElement('div');
167
+ el.className = `cm-linked-table-caption cm-linked-table-caption-${position}`;
168
+ el.textContent = captionText;
169
+ return el;
170
+ }
171
+
172
+ _decorateSortableHeaders(view, tableContainer) {
173
+ const sortableHeaders = getSortableHeaderCells(this.parsedTable);
174
+ if (sortableHeaders.length === 0) return;
175
+
176
+ const headerRow = tableContainer.querySelector('thead tr');
177
+ if (!headerRow) return;
178
+
179
+ const domHeaders = Array.from(headerRow.querySelectorAll('th'));
180
+ const count = Math.min(domHeaders.length, sortableHeaders.length);
181
+
182
+ for (let index = 0; index < count; index++) {
183
+ const th = domHeaders[index];
184
+ const headerCell = sortableHeaders[index];
185
+ const column = headerCell.content.trim();
186
+ if (!column) continue;
187
+
188
+ th.classList.add('cm-linked-table-sortable');
189
+ th.title = `Sort by ${column}`;
190
+ th.dataset.linkedTableColumn = column;
191
+ th.dataset.linkedTableSortDirection = th.dataset.linkedTableSortDirection || 'none';
192
+
193
+ th.addEventListener('click', (event) => {
194
+ if (th.getAttribute('aria-disabled') === 'true') {
195
+ event.preventDefault();
196
+ event.stopPropagation();
197
+ return;
198
+ }
199
+
200
+ event.preventDefault();
201
+ event.stopPropagation();
202
+
203
+ const current = th.dataset.linkedTableSortDirection || 'none';
204
+ const next = current === 'asc' ? 'desc' : 'asc';
205
+ th.dataset.linkedTableSortDirection = next;
206
+ this._dispatch(view, 'sort', { column, direction: next });
207
+ });
208
+ }
209
+ }
210
+
211
+ toDOM(view) {
212
+ const container = document.createElement('div');
213
+ container.className = 'cm-linked-table-widget';
214
+ container.dataset.tableId = this.block.spec.id;
215
+ container.dataset.engine = this.block.spec.engine || '';
216
+ container.dataset.materializedAt = this.block.spec?.snapshot?.materializedAt || '';
217
+
218
+ container.appendChild(this._buildChrome(view));
219
+
220
+ const aboveCaption = this._buildCaption(this.block.captionAboveText, 'above');
221
+ if (aboveCaption) container.appendChild(aboveCaption);
222
+
223
+ const body = document.createElement('div');
224
+ body.className = 'cm-linked-table-body';
225
+
226
+ const renderedTable = new TableWidget(this.parsedTable, `linked-${this.block.spec.id}`).toDOM(view);
227
+ body.appendChild(renderedTable);
228
+ this._decorateSortableHeaders(view, renderedTable);
229
+
230
+ container.appendChild(body);
231
+
232
+ const belowCaption = this._buildCaption(this.block.captionBelowText, 'below');
233
+ if (belowCaption) container.appendChild(belowCaption);
234
+
235
+ return container;
236
+ }
237
+
238
+ ignoreEvent() {
239
+ return false;
240
+ }
241
+
242
+ get estimatedHeight() {
243
+ return this.options.estimatedHeight || -1;
244
+ }
245
+ }
246
+
247
+ export function createLinkedTableWidgetFromBlock(block, contentHash, options = {}) {
248
+ const parsed = parseTable(block.tableLines || []);
249
+ if (!parsed || !parsed.rows || parsed.rows.length === 0) return null;
250
+ return new LinkedTableWidget(block, parsed, contentHash, options);
251
+ }
252
+
253
+ export default {
254
+ LinkedTableWidget,
255
+ createLinkedTableWidgetFromBlock,
256
+ };