@lapis-notes/csv 0.0.0-stage → 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 (105) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE.md +4 -0
  3. package/README.md +19 -2
  4. package/dist/app.css +210 -0
  5. package/dist/csv-settings.d.ts +13 -0
  6. package/dist/csv-settings.js +28 -0
  7. package/dist/index.d.ts +7 -0
  8. package/dist/index.js +7 -0
  9. package/dist/lib/CsvSheet.svelte +58 -0
  10. package/dist/lib/CsvSheet.svelte.d.ts +11 -0
  11. package/dist/lib/CsvTable.svelte +58 -0
  12. package/dist/lib/CsvTable.svelte.d.ts +11 -0
  13. package/dist/lib/codemirror/csv-language-extensions.d.ts +3 -0
  14. package/dist/lib/codemirror/csv-language-extensions.js +11 -0
  15. package/dist/lib/codemirror/csv-language.d.ts +3 -0
  16. package/dist/lib/codemirror/csv-language.js +24 -0
  17. package/dist/lib/codemirror/csv-rainbow-theme.d.ts +3 -0
  18. package/dist/lib/codemirror/csv-rainbow-theme.js +15 -0
  19. package/dist/lib/codemirror/csv-source-extensions.d.ts +7 -0
  20. package/dist/lib/codemirror/csv-source-extensions.js +9 -0
  21. package/dist/lib/codemirror/csv.parser.d.ts +2 -0
  22. package/dist/lib/codemirror/csv.parser.js +16 -0
  23. package/dist/lib/codemirror/csv.parser.terms.d.ts +1 -0
  24. package/dist/lib/codemirror/csv.parser.terms.js +2 -0
  25. package/dist/lib/codemirror/syntax.grammar +36 -0
  26. package/dist/lib/components/csv-cell.svelte +137 -0
  27. package/dist/lib/components/csv-cell.svelte.d.ts +11 -0
  28. package/dist/lib/components/csv-data-table-column-head.svelte +76 -0
  29. package/dist/lib/components/csv-data-table-column-head.svelte.d.ts +17 -0
  30. package/dist/lib/components/csv-data-table-column-header.svelte +159 -0
  31. package/dist/lib/components/csv-data-table-column-header.svelte.d.ts +23 -0
  32. package/dist/lib/components/csv-data-table-data-cell.svelte +72 -0
  33. package/dist/lib/components/csv-data-table-data-cell.svelte.d.ts +18 -0
  34. package/dist/lib/components/csv-data-table-drag-overlay.svelte +119 -0
  35. package/dist/lib/components/csv-data-table-drag-overlay.svelte.d.ts +13 -0
  36. package/dist/lib/components/csv-data-table-row-actions.svelte +47 -0
  37. package/dist/lib/components/csv-data-table-row-actions.svelte.d.ts +8 -0
  38. package/dist/lib/components/csv-data-table-row-grip-cell.svelte +102 -0
  39. package/dist/lib/components/csv-data-table-row-grip-cell.svelte.d.ts +19 -0
  40. package/dist/lib/components/csv-data-table-row.svelte +38 -0
  41. package/dist/lib/components/csv-data-table-row.svelte.d.ts +11 -0
  42. package/dist/lib/components/csv-data-table.svelte +903 -0
  43. package/dist/lib/components/csv-data-table.svelte.d.ts +21 -0
  44. package/dist/lib/components/csv-find.svelte +382 -0
  45. package/dist/lib/components/csv-find.svelte.d.ts +24 -0
  46. package/dist/lib/components/csv-row-virtualizer.svelte.d.ts +9 -0
  47. package/dist/lib/components/csv-row-virtualizer.svelte.js +59 -0
  48. package/dist/lib/components/csv-source-view.svelte +127 -0
  49. package/dist/lib/components/csv-source-view.svelte.d.ts +10 -0
  50. package/dist/lib/components/csv-status-bar.svelte +101 -0
  51. package/dist/lib/components/csv-status-bar.svelte.d.ts +14 -0
  52. package/dist/lib/components/csv-table-dnd.d.ts +9 -0
  53. package/dist/lib/components/csv-table-dnd.js +12 -0
  54. package/dist/lib/components/csv-table-drag-measure.d.ts +7 -0
  55. package/dist/lib/components/csv-table-drag-measure.js +38 -0
  56. package/dist/lib/components/csv-table-selection.d.ts +28 -0
  57. package/dist/lib/components/csv-table-selection.js +178 -0
  58. package/dist/lib/components/csv-table-view.svelte +265 -0
  59. package/dist/lib/components/csv-table-view.svelte.d.ts +16 -0
  60. package/dist/lib/csv-plugin.d.ts +9 -0
  61. package/dist/lib/csv-plugin.js +107 -0
  62. package/dist/lib/csv-view-settings.d.ts +43 -0
  63. package/dist/lib/csv-view-settings.js +86 -0
  64. package/dist/lib/ids.d.ts +8 -0
  65. package/dist/lib/ids.js +19 -0
  66. package/dist/lib/model/csv-column-sort.d.ts +16 -0
  67. package/dist/lib/model/csv-column-sort.js +81 -0
  68. package/dist/lib/model/csv-grid.d.ts +31 -0
  69. package/dist/lib/model/csv-grid.js +161 -0
  70. package/dist/lib/model/csv-search.d.ts +38 -0
  71. package/dist/lib/model/csv-search.js +101 -0
  72. package/dist/lib/model/csv-table-model.d.ts +10 -0
  73. package/dist/lib/model/csv-table-model.js +18 -0
  74. package/dist/lib/model/parse-csv.d.ts +10 -0
  75. package/dist/lib/model/parse-csv.js +13 -0
  76. package/dist/lib/model/serialize-csv.d.ts +5 -0
  77. package/dist/lib/model/serialize-csv.js +10 -0
  78. package/dist/lib/sheet/csv-sheet-toolbar.svelte +244 -0
  79. package/dist/lib/sheet/csv-sheet-toolbar.svelte.d.ts +20 -0
  80. package/dist/lib/sheet/csv-sheet-view.svelte +444 -0
  81. package/dist/lib/sheet/csv-sheet-view.svelte.d.ts +18 -0
  82. package/dist/lib/sheet/csv-sheet.svelte +1121 -0
  83. package/dist/lib/sheet/csv-sheet.svelte.d.ts +37 -0
  84. package/dist/lib/sheet/sheet-glyph.svelte +57 -0
  85. package/dist/lib/sheet/sheet-glyph.svelte.d.ts +7 -0
  86. package/dist/lib/sheet/sheet-history.d.ts +14 -0
  87. package/dist/lib/sheet/sheet-history.js +33 -0
  88. package/dist/lib/sheet/sheet-layout.d.ts +121 -0
  89. package/dist/lib/sheet/sheet-layout.js +336 -0
  90. package/dist/lib/sheet/sheet-paint.d.ts +45 -0
  91. package/dist/lib/sheet/sheet-paint.js +274 -0
  92. package/dist/lib/sheet/sheet-theme.d.ts +2 -0
  93. package/dist/lib/sheet/sheet-theme.js +26 -0
  94. package/dist/lib/views/csv-view-mode.d.ts +3 -0
  95. package/dist/lib/views/csv-view-mode.js +6 -0
  96. package/dist/lib/views/csv-view.d.ts +24 -0
  97. package/dist/lib/views/csv-view.js +125 -0
  98. package/dist/styles/csv-find.css +313 -0
  99. package/dist/styles/csv-table.css +882 -0
  100. package/dist/styles.css +37 -0
  101. package/manifest.json +45 -0
  102. package/package.json +86 -3
  103. package/registry-content/overview.md +3 -0
  104. package/registry.json +322 -0
  105. package/styles.css +1 -0
@@ -0,0 +1,444 @@
1
+ <script lang="ts">
2
+ import { cn } from "@lapis-notes/ui";
3
+ import type { App, TFile } from "@lapis-notes/api";
4
+ import { onMount } from "svelte";
5
+ import type { Attachment } from "svelte/attachments";
6
+ import {
7
+ noteCsvColumnDeleted,
8
+ noteCsvColumnMoved,
9
+ noteCsvColumnSortEdit,
10
+ toggleCsvColumnSort,
11
+ type CsvColumnSort,
12
+ } from "../model/csv-column-sort";
13
+ import {
14
+ emptySheetHistory,
15
+ recordSheetEdit,
16
+ redoSheetEdit,
17
+ undoSheetEdit,
18
+ } from "./sheet-history";
19
+ import { CsvGrid } from "../model/csv-grid";
20
+ import { parseCsvText } from "../model/parse-csv";
21
+ import {
22
+ csvBodyCounts,
23
+ csvCellMatches,
24
+ csvSearchQuery,
25
+ csvShownRowCount,
26
+ isCsvFindShortcut,
27
+ } from "../model/csv-search";
28
+ import {
29
+ resolveDelimiter,
30
+ type CsvPluginSettings,
31
+ } from "../../csv-settings";
32
+ import CsvFind from "../components/csv-find.svelte";
33
+ import CsvStatusBar from "../components/csv-status-bar.svelte";
34
+ import CsvSheet from "./csv-sheet.svelte";
35
+ import {
36
+ SHEET_DEFAULT_COLUMN_WIDTH,
37
+ readSessionWidths,
38
+ writeSessionWidths,
39
+ } from "./sheet-layout";
40
+
41
+ let {
42
+ app,
43
+ file,
44
+ content,
45
+ settings,
46
+ class: className,
47
+ onContentChange,
48
+ onShowTable,
49
+ onToggleSource,
50
+ onSettingsChange,
51
+ }: {
52
+ app: App;
53
+ file: TFile;
54
+ content: string;
55
+ settings: CsvPluginSettings;
56
+ class?: string;
57
+ onContentChange?: (text: string) => void;
58
+ onShowTable?: () => void;
59
+ onToggleSource?: () => void;
60
+ onSettingsChange?: (patch: { headerRow?: boolean }) => void;
61
+ } = $props();
62
+
63
+ let incoming = $state<string | null>(null);
64
+ let searchOpen = $state(false);
65
+ let searchRequest = $state(0);
66
+ let searchValue = $state("");
67
+ let replaceValue = $state("");
68
+ let caseSensitive = $state(false);
69
+ let wholeWord = $state(false);
70
+ let regexp = $state(false);
71
+ let active = $state(0);
72
+ let columnSort = $state<CsvColumnSort | null>(null);
73
+ let widthOverride = $state<{ path: string; widths: number[] } | null>(null);
74
+ let history = $state(emptySheetHistory<SheetSnapshot>());
75
+
76
+ type SheetSnapshot = {
77
+ text: string;
78
+ widths: number[];
79
+ sort: CsvColumnSort | null;
80
+ };
81
+
82
+ const text = $derived(incoming ?? content);
83
+ const delimiter = $derived(resolveDelimiter(settings, file.extension));
84
+ const parsed = $derived.by(() => {
85
+ const source = text;
86
+ const result = parseCsvText(source, { delimiter });
87
+ return {
88
+ errors: result.errors.map(
89
+ (error) => `Row ${error.row ?? "?"}: ${error.message}`,
90
+ ),
91
+ grid:
92
+ result.rows.length > 0
93
+ ? CsvGrid.fromText(source, {
94
+ delimiter,
95
+ headerRow: settings.headerRow,
96
+ })
97
+ : CsvGrid.empty(),
98
+ };
99
+ });
100
+ const rows = $derived(parsed.grid.getRows());
101
+ const counts = $derived(
102
+ csvBodyCounts(
103
+ parsed.grid.getRowCount(),
104
+ parsed.grid.getColCount(),
105
+ settings.headerRow,
106
+ ),
107
+ );
108
+ const searchQuery = $derived(
109
+ csvSearchQuery({
110
+ search: searchValue,
111
+ replace: replaceValue,
112
+ caseSensitive,
113
+ wholeWord,
114
+ regexp,
115
+ }),
116
+ );
117
+ const filtering = $derived(
118
+ searchOpen && searchQuery.search.length > 0 && searchQuery.valid,
119
+ );
120
+ const searchMatches = $derived.by(() => {
121
+ if (!filtering) return [];
122
+ return csvCellMatches(rows, searchQuery);
123
+ });
124
+ const matchedRows = $derived(
125
+ new Set(searchMatches.map((match) => match.row)),
126
+ );
127
+ const bodyRows = $derived.by(() => {
128
+ const start = settings.headerRow ? 1 : 0;
129
+ const indexes: number[] = [];
130
+ for (let index = start; index < rows.length; index += 1) {
131
+ if (!filtering || matchedRows.has(index)) indexes.push(index);
132
+ }
133
+ return indexes;
134
+ });
135
+ const shownRows = $derived(
136
+ filtering
137
+ ? csvShownRowCount(rows.length, settings.headerRow, searchMatches)
138
+ : undefined,
139
+ );
140
+ const searchCell = $derived.by(() => {
141
+ if (searchMatches.length === 0) return null;
142
+ const match = searchMatches[Math.min(active, searchMatches.length - 1)];
143
+ return match ? { row: match.row, col: match.col } : null;
144
+ });
145
+ const widths = $derived.by(() => {
146
+ const count = parsed.grid.getColCount();
147
+ const stored = widthOverride;
148
+ if (
149
+ stored &&
150
+ stored.path === file.path &&
151
+ stored.widths.length === count
152
+ ) {
153
+ return stored.widths;
154
+ }
155
+ return readSessionWidths(file.path, count);
156
+ });
157
+
158
+ function copySort(sort: CsvColumnSort | null): CsvColumnSort | null {
159
+ if (!sort) return null;
160
+ return {
161
+ index: sort.index,
162
+ direction: sort.direction,
163
+ baseline: sort.baseline.map((row) => [...row]),
164
+ order: [...sort.order],
165
+ };
166
+ }
167
+
168
+ function snapshot(): SheetSnapshot {
169
+ return { text, widths: [...widths], sort: copySort(columnSort) };
170
+ }
171
+
172
+ function publish(nextGrid: CsvGrid, before: SheetSnapshot) {
173
+ const next = nextGrid.toText({ delimiter });
174
+ if (before.text !== next) history = recordSheetEdit(history, before);
175
+ incoming = next;
176
+ onContentChange?.(next);
177
+ }
178
+
179
+ function restore(entry: SheetSnapshot) {
180
+ incoming = entry.text;
181
+ columnSort = copySort(entry.sort);
182
+ rememberWidths([...entry.widths]);
183
+ onContentChange?.(entry.text);
184
+ }
185
+
186
+ function undoSheet() {
187
+ const step = undoSheetEdit(history, snapshot());
188
+ if (!step) return;
189
+ history = step.history;
190
+ restore(step.value);
191
+ }
192
+
193
+ function redoSheet() {
194
+ const step = redoSheetEdit(history, snapshot());
195
+ if (!step) return;
196
+ history = step.history;
197
+ restore(step.value);
198
+ }
199
+
200
+ function rememberWidths(next: number[]) {
201
+ widthOverride = { path: file.path, widths: next };
202
+ writeSessionWidths(file.path, next);
203
+ }
204
+
205
+ function changeCell(row: number, col: number, value: string) {
206
+ const before = snapshot();
207
+ const next = parsed.grid.clone();
208
+ next.updateCell(row, col, value);
209
+ if (columnSort) noteCsvColumnSortEdit(columnSort, row, col, value);
210
+ publish(next, before);
211
+ }
212
+
213
+ function sortColumn(col: number) {
214
+ const before = snapshot();
215
+ const next = parsed.grid.clone();
216
+ columnSort = toggleCsvColumnSort(
217
+ next,
218
+ columnSort,
219
+ col,
220
+ settings.headerRow,
221
+ );
222
+ publish(next, before);
223
+ }
224
+
225
+ function deleteRow(index: number) {
226
+ const before = snapshot();
227
+ const next = parsed.grid.clone();
228
+ next.deleteRowAt(index);
229
+ columnSort = null;
230
+ publish(next, before);
231
+ }
232
+
233
+ function deleteColumn(index: number) {
234
+ const before = snapshot();
235
+ const next = parsed.grid.clone();
236
+ next.deleteColumnAt(index);
237
+ columnSort = noteCsvColumnDeleted(columnSort, index);
238
+ const stored = widths.filter((_, column) => column !== index);
239
+ rememberWidths(stored.length > 0 ? stored : [SHEET_DEFAULT_COLUMN_WIDTH]);
240
+ publish(next, before);
241
+ }
242
+
243
+ function moveColumn(from: number, to: number) {
244
+ if (from === to || to < 0 || to >= parsed.grid.getColCount()) return;
245
+ const before = snapshot();
246
+ const next = parsed.grid.clone();
247
+ next.moveColumnAt(from, to);
248
+ columnSort = noteCsvColumnMoved(columnSort, from, to);
249
+ const stored = [...widths];
250
+ const [width] = stored.splice(from, 1);
251
+ stored.splice(to, 0, width ?? SHEET_DEFAULT_COLUMN_WIDTH);
252
+ rememberWidths(stored);
253
+ publish(next, before);
254
+ }
255
+
256
+ function moveRow(from: number, to: number) {
257
+ if (from === to || to < 0 || to >= parsed.grid.getRowCount()) return;
258
+ const before = snapshot();
259
+ const next = parsed.grid.clone();
260
+ next.moveRowAt(from, to);
261
+ columnSort = null;
262
+ publish(next, before);
263
+ }
264
+
265
+ function clearCoordinates(coords: Array<[number, number]>) {
266
+ const before = snapshot();
267
+ const next = parsed.grid.clone();
268
+ next.clearCellsAt(coords);
269
+ if (columnSort) {
270
+ for (const [row, col] of coords) {
271
+ noteCsvColumnSortEdit(columnSort, row, col, "");
272
+ }
273
+ }
274
+ publish(next, before);
275
+ }
276
+
277
+ function clearColumn(col: number) {
278
+ const coords: Array<[number, number]> = [];
279
+ for (let row = 0; row < parsed.grid.getRowCount(); row += 1) {
280
+ coords.push([row, col]);
281
+ }
282
+ clearCoordinates(coords);
283
+ }
284
+
285
+ function clearRow(row: number) {
286
+ const coords: Array<[number, number]> = [];
287
+ for (let col = 0; col < parsed.grid.getColCount(); col += 1) {
288
+ coords.push([row, col]);
289
+ }
290
+ clearCoordinates(coords);
291
+ }
292
+
293
+ function clearCell(row: number, col: number) {
294
+ changeCell(row, col, "");
295
+ }
296
+
297
+ function clearSheet() {
298
+ const coords: Array<[number, number]> = [];
299
+ for (let row = 0; row < parsed.grid.getRowCount(); row += 1) {
300
+ for (let col = 0; col < parsed.grid.getColCount(); col += 1) {
301
+ coords.push([row, col]);
302
+ }
303
+ }
304
+ clearCoordinates(coords);
305
+ }
306
+
307
+ function deleteSheet() {
308
+ const before = snapshot();
309
+ const next = parsed.grid.clone();
310
+ while (next.getRowCount() > 1) next.deleteRowAt(next.getRowCount() - 1);
311
+ if (!settings.headerRow) {
312
+ const coords: Array<[number, number]> = [];
313
+ for (let col = 0; col < next.getColCount(); col += 1) coords.push([0, col]);
314
+ next.clearCellsAt(coords);
315
+ }
316
+ columnSort = null;
317
+ publish(next, before);
318
+ }
319
+
320
+ function replaceCells(edits: { row: number; col: number; value: string }[]) {
321
+ const before = snapshot();
322
+ const next = parsed.grid.clone();
323
+ for (const edit of edits) {
324
+ next.updateCell(edit.row, edit.col, edit.value);
325
+ if (columnSort) noteCsvColumnSortEdit(columnSort, edit.row, edit.col, edit.value);
326
+ }
327
+ publish(next, before);
328
+ }
329
+
330
+ function openSearch() {
331
+ searchOpen = true;
332
+ searchRequest += 1;
333
+ }
334
+
335
+ function toggleSearch() {
336
+ if (searchOpen) {
337
+ searchOpen = false;
338
+ return;
339
+ }
340
+ openSearch();
341
+ }
342
+
343
+ function onFindShortcut(event: KeyboardEvent) {
344
+ if (!isCsvFindShortcut(event)) return;
345
+ event.preventDefault();
346
+ event.stopPropagation();
347
+ openSearch();
348
+ }
349
+
350
+ function onHistoryShortcut(event: KeyboardEvent) {
351
+ const target = event.target;
352
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) return;
353
+ const meta = event.metaKey || event.ctrlKey;
354
+ if (!meta || event.altKey) return;
355
+ const key = event.key.toLowerCase();
356
+ if (key === "z" && !event.shiftKey) {
357
+ event.preventDefault();
358
+ undoSheet();
359
+ } else if ((key === "z" && event.shiftKey) || key === "y") {
360
+ event.preventDefault();
361
+ redoSheet();
362
+ }
363
+ }
364
+
365
+ const findShortcut: Attachment<HTMLElement> = (node) => {
366
+ node.addEventListener("keydown", onFindShortcut, true);
367
+ node.addEventListener("keydown", onHistoryShortcut);
368
+ return () => {
369
+ node.removeEventListener("keydown", onFindShortcut, true);
370
+ node.removeEventListener("keydown", onHistoryShortcut);
371
+ };
372
+ };
373
+
374
+ onMount(() => {
375
+ const currentApp = app;
376
+ const currentFile = file;
377
+ const modified = currentApp.vault.on("modify", (candidate) => {
378
+ if (candidate.path !== currentFile.path) return;
379
+ void currentApp.vault.cachedRead(currentFile).then((next) => {
380
+ incoming = next;
381
+ });
382
+ });
383
+ return () => {
384
+ currentApp.vault.offref(modified);
385
+ };
386
+ });
387
+ </script>
388
+
389
+ <div class={cn("csv-sheet-view", className)} {@attach findShortcut}>
390
+ {#if parsed.errors.length > 0}
391
+ <div class="csv-table-view__errors" role="status">
392
+ {#each parsed.errors as error (error)}
393
+ <p>{error}</p>
394
+ {/each}
395
+ </div>
396
+ {/if}
397
+ <CsvFind
398
+ bind:open={searchOpen}
399
+ bind:searchValue
400
+ bind:replaceValue
401
+ bind:caseSensitive
402
+ bind:wholeWord
403
+ bind:regexp
404
+ bind:active
405
+ {searchRequest}
406
+ {rows}
407
+ onReplaceCells={replaceCells}
408
+ />
409
+ <CsvSheet
410
+ grid={parsed.grid}
411
+ headerRow={settings.headerRow}
412
+ {bodyRows}
413
+ {searchCell}
414
+ sort={columnSort}
415
+ {widths}
416
+ onWidths={rememberWidths}
417
+ onCellChange={changeCell}
418
+ onSort={sortColumn}
419
+ onDeleteRow={deleteRow}
420
+ onDeleteColumn={deleteColumn}
421
+ onMoveColumn={moveColumn}
422
+ onMoveRow={moveRow}
423
+ onClearColumn={clearColumn}
424
+ onClearRow={clearRow}
425
+ onClearCell={clearCell}
426
+ onClearSheet={clearSheet}
427
+ onDeleteSheet={deleteSheet}
428
+ canUndo={history.past.length > 0}
429
+ canRedo={history.future.length > 0}
430
+ onUndo={undoSheet}
431
+ onRedo={redoSheet}
432
+ onHeaderRow={(headerRow) => onSettingsChange?.({ headerRow })}
433
+ />
434
+ <CsvStatusBar
435
+ columns={counts.columns}
436
+ rows={counts.rows}
437
+ {shownRows}
438
+ {searchOpen}
439
+ mode="canvas"
440
+ onToggleSearch={toggleSearch}
441
+ onShowTable={onShowTable}
442
+ onToggleMode={onToggleSource}
443
+ />
444
+ </div>
@@ -0,0 +1,18 @@
1
+ import type { App, TFile } from "@lapis-notes/api";
2
+ import { type CsvPluginSettings } from "../../csv-settings";
3
+ type $$ComponentProps = {
4
+ app: App;
5
+ file: TFile;
6
+ content: string;
7
+ settings: CsvPluginSettings;
8
+ class?: string;
9
+ onContentChange?: (text: string) => void;
10
+ onShowTable?: () => void;
11
+ onToggleSource?: () => void;
12
+ onSettingsChange?: (patch: {
13
+ headerRow?: boolean;
14
+ }) => void;
15
+ };
16
+ declare const CsvSheetView: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type CsvSheetView = ReturnType<typeof CsvSheetView>;
18
+ export default CsvSheetView;