@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,107 @@
1
+ import { Plugin, } from "@lapis-notes/api";
2
+ import { mount, unmount } from "svelte";
3
+ import manifestSpec from "../../manifest.json";
4
+ import CsvSheet from "./CsvSheet.svelte";
5
+ import CsvTable from "./CsvTable.svelte";
6
+ import { CSV_EXTENSIONS, CSV_FILENAME_PATTERNS, CSV_PLUGIN_ID, CSV_VIEW_TYPE, } from "./ids";
7
+ import { csvSourceExtensions } from "./codemirror/csv-source-extensions";
8
+ import { readDefaultCsvView, registerCsvViewSettings } from "./csv-view-settings";
9
+ import { CsvFileView } from "./views/csv-view";
10
+ export { CSV_PLUGIN_ID, CSV_VIEW_TYPE, CSV_FILENAME_PATTERNS };
11
+ const tableExtensions = new Set(CSV_EXTENSIONS);
12
+ function isTableFile(file) {
13
+ return tableExtensions.has(file?.extension.toLowerCase() ?? "");
14
+ }
15
+ function mountEmbed(app, containerEl, file) {
16
+ containerEl.style.display = "flex";
17
+ containerEl.style.flexDirection = "column";
18
+ containerEl.style.width = "100%";
19
+ containerEl.style.height = "auto";
20
+ containerEl.style.minHeight = "0";
21
+ containerEl.style.overflow = "auto";
22
+ const presentation = readDefaultCsvView(app);
23
+ const component = presentation === "canvas"
24
+ ? mount(CsvSheet, {
25
+ target: containerEl,
26
+ props: { app, file, role: "embed" },
27
+ })
28
+ : mount(CsvTable, {
29
+ target: containerEl,
30
+ props: { app, file, role: "embed" },
31
+ });
32
+ return {
33
+ destroy: () => {
34
+ void unmount(component);
35
+ },
36
+ };
37
+ }
38
+ export class CSVPlugin extends Plugin {
39
+ constructor(app, manifest = manifestSpec) {
40
+ super(app, manifest);
41
+ }
42
+ onload() {
43
+ this.registerView(CSV_VIEW_TYPE, (leaf) => new CsvFileView(leaf), {
44
+ kind: "file",
45
+ });
46
+ this.registerEditorView({
47
+ id: `${CSV_VIEW_TYPE}-file`,
48
+ viewType: CSV_VIEW_TYPE,
49
+ label: "CSV",
50
+ filenamePatterns: [...CSV_FILENAME_PATTERNS],
51
+ priority: "exclusive",
52
+ });
53
+ this.registerExtensions([...CSV_EXTENSIONS], CSV_VIEW_TYPE);
54
+ this.registerEditorExtension(() => csvSourceExtensions(this.app), CSV_VIEW_TYPE);
55
+ for (const extension of CSV_EXTENSIONS) {
56
+ this.register(this.app.embedRegistry.register(extension, ({ app, containerEl, state }) => {
57
+ const file = state.file;
58
+ if (!isTableFile(file) || file.extension.toLowerCase() !== extension)
59
+ return;
60
+ return mountEmbed(app, containerEl, file);
61
+ }));
62
+ }
63
+ this.addCommand({
64
+ id: "open-csv",
65
+ name: "Open CSV",
66
+ icon: "lucide-table",
67
+ callback: () => this.openTable(this.app.workspace.activeLeaf),
68
+ });
69
+ this.addCommand({
70
+ id: "toggle-csv-source",
71
+ name: "Toggle CSV source",
72
+ icon: "lucide-file-code",
73
+ checkCallback: (checking) => {
74
+ const view = this.app.workspace.activeLeaf?.view;
75
+ if (!(view instanceof CsvFileView))
76
+ return false;
77
+ if (!checking)
78
+ view.toggleMode();
79
+ return true;
80
+ },
81
+ });
82
+ this.addCommand({
83
+ id: "show-csv-canvas",
84
+ name: "Show CSV canvas",
85
+ icon: "lucide-grid-3x3",
86
+ checkCallback: (checking) => {
87
+ const view = this.app.workspace.activeLeaf?.view;
88
+ if (!(view instanceof CsvFileView))
89
+ return false;
90
+ if (!checking)
91
+ view.showCanvas();
92
+ return true;
93
+ },
94
+ });
95
+ registerCsvViewSettings(this);
96
+ }
97
+ openTable(leaf) {
98
+ const file = this.app.workspace.getActiveFile();
99
+ if (!isTableFile(file) || !leaf)
100
+ return;
101
+ void leaf.setViewState({
102
+ type: CSV_VIEW_TYPE,
103
+ state: { file: file.path },
104
+ });
105
+ }
106
+ }
107
+ export default CSVPlugin;
@@ -0,0 +1,43 @@
1
+ import type { App, Plugin } from "@lapis-notes/api";
2
+ export type CsvTablePresentation = "table" | "canvas";
3
+ export declare const CSV_SETTINGS_SECTION_ID = "lapis-csv";
4
+ export declare const CSV_SETTING_IDS: {
5
+ readonly defaultView: "lapis-csv.defaultView";
6
+ };
7
+ export declare const CSV_CONFIGURATION_SCHEMA: {
8
+ id: string;
9
+ title: string;
10
+ type: "object";
11
+ properties: {
12
+ "lapis-csv.defaultView": {
13
+ title: string;
14
+ description: string;
15
+ type: "string";
16
+ default: string;
17
+ enum: string[];
18
+ };
19
+ };
20
+ };
21
+ export declare function isCsvTablePresentation(value: unknown): value is CsvTablePresentation;
22
+ export declare function readDefaultCsvView(app: App): CsvTablePresentation;
23
+ export declare function createCsvSettingsSection(): {
24
+ id: string;
25
+ title: string;
26
+ description: string;
27
+ icon: "table";
28
+ order: number;
29
+ navigationGroupId: string;
30
+ sourcePluginId: string;
31
+ fields: {
32
+ id: "lapis-csv.defaultView";
33
+ title: string;
34
+ description: string;
35
+ type: "enum";
36
+ default: "canvas";
37
+ options: {
38
+ value: string;
39
+ label: string;
40
+ }[];
41
+ }[];
42
+ };
43
+ export declare function registerCsvViewSettings(plugin: Plugin): void;
@@ -0,0 +1,86 @@
1
+ import { getWorkspaceHostBinding } from "@lapis-notes/api/workspace-host";
2
+ import { CSV_PLUGIN_ID } from "./ids";
3
+ export const CSV_SETTINGS_SECTION_ID = "lapis-csv";
4
+ export const CSV_SETTING_IDS = {
5
+ defaultView: "lapis-csv.defaultView",
6
+ };
7
+ const DEFAULT_VIEW_FIELD = {
8
+ title: "Default table view",
9
+ description: "Open a CSV or TSV as the table or the canvas sheet.",
10
+ type: "string",
11
+ default: "canvas",
12
+ enum: ["table", "canvas"],
13
+ };
14
+ export const CSV_CONFIGURATION_SCHEMA = {
15
+ id: "lapis-csv",
16
+ title: "CSV",
17
+ type: "object",
18
+ properties: {
19
+ [CSV_SETTING_IDS.defaultView]: DEFAULT_VIEW_FIELD,
20
+ },
21
+ };
22
+ function settingsController(app) {
23
+ try {
24
+ return getWorkspaceHostBinding(app.workspace).controller
25
+ .settings;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
31
+ export function isCsvTablePresentation(value) {
32
+ return value === "table" || value === "canvas";
33
+ }
34
+ export function readDefaultCsvView(app) {
35
+ const live = settingsController(app)?.get(CSV_SETTING_IDS.defaultView);
36
+ if (isCsvTablePresentation(live))
37
+ return live;
38
+ const stored = app.configuration
39
+ ?.getConfiguration()
40
+ .get(CSV_SETTING_IDS.defaultView, "canvas");
41
+ return isCsvTablePresentation(stored) ? stored : "canvas";
42
+ }
43
+ export function createCsvSettingsSection() {
44
+ return {
45
+ id: CSV_SETTINGS_SECTION_ID,
46
+ title: "CSV",
47
+ description: "How CSV and TSV files open.",
48
+ icon: "table",
49
+ order: 29,
50
+ navigationGroupId: "core-plugins",
51
+ sourcePluginId: CSV_PLUGIN_ID,
52
+ fields: [
53
+ {
54
+ id: CSV_SETTING_IDS.defaultView,
55
+ title: DEFAULT_VIEW_FIELD.title,
56
+ description: DEFAULT_VIEW_FIELD.description,
57
+ type: "enum",
58
+ default: "canvas",
59
+ options: [
60
+ { value: "table", label: "Table" },
61
+ { value: "canvas", label: "Canvas" },
62
+ ],
63
+ },
64
+ ],
65
+ };
66
+ }
67
+ export function registerCsvViewSettings(plugin) {
68
+ const schema = plugin.app.configuration?.schema;
69
+ if (schema) {
70
+ schema.register(CSV_CONFIGURATION_SCHEMA);
71
+ plugin.register(() => {
72
+ schema.unregister(CSV_CONFIGURATION_SCHEMA);
73
+ });
74
+ void plugin.app.configuration?.materializeSchemaDefaults?.();
75
+ }
76
+ const settings = settingsController(plugin.app);
77
+ if (!settings)
78
+ return;
79
+ try {
80
+ const dispose = getWorkspaceHostBinding(plugin.app.workspace).controller.registerSettingsSection(createCsvSettingsSection());
81
+ plugin.register(dispose);
82
+ }
83
+ catch {
84
+ return;
85
+ }
86
+ }
@@ -0,0 +1,8 @@
1
+ export declare const CSV_PLUGIN_ID = "lapis-csv";
2
+ export declare const CSV_VIEW_TYPE = "lapis-csv";
3
+ export declare const CSV_EXTENSIONS: readonly ["csv", "tsv"];
4
+ export declare const CSV_FILENAME_PATTERNS: readonly ["*.csv", "*.tsv"];
5
+ export declare const SAMPLE_CSV_PATH = "Sample.csv";
6
+ export declare const SAMPLE_READING_NOTE_PATH = "Notes/Staff readings.md";
7
+ export declare const SAMPLE_PLAIN_NOTE_PATH = "Notes/Welcome.md";
8
+ export declare function sampleReadingMarkdown(): string;
@@ -0,0 +1,19 @@
1
+ export const CSV_PLUGIN_ID = "lapis-csv";
2
+ export const CSV_VIEW_TYPE = "lapis-csv";
3
+ export const CSV_EXTENSIONS = ["csv", "tsv"];
4
+ export const CSV_FILENAME_PATTERNS = ["*.csv", "*.tsv"];
5
+ export const SAMPLE_CSV_PATH = "Sample.csv";
6
+ export const SAMPLE_READING_NOTE_PATH = "Notes/Staff readings.md";
7
+ export const SAMPLE_PLAIN_NOTE_PATH = "Notes/Welcome.md";
8
+ export function sampleReadingMarkdown() {
9
+ return [
10
+ "# Staff readings",
11
+ "",
12
+ "The south basin kept the late tide.",
13
+ "",
14
+ "![[Sample.csv]]",
15
+ "",
16
+ "![[Welcome.md]]",
17
+ "",
18
+ ].join("\n");
19
+ }
@@ -0,0 +1,16 @@
1
+ import type { CsvGrid } from "./csv-grid";
2
+ export type CsvColumnSortDirection = "asc" | "desc";
3
+ export type CsvColumnSort = {
4
+ index: number;
5
+ direction: CsvColumnSortDirection;
6
+ baseline: string[][];
7
+ /** Baseline index for each current visual row. */
8
+ order: number[];
9
+ };
10
+ export declare function nextCsvColumnSortDirection(state: CsvColumnSort | null, index: number): CsvColumnSortDirection | "clear";
11
+ export declare function applyCsvColumnSort(grid: CsvGrid, state: CsvColumnSort | null, index: number, direction: CsvColumnSortDirection | "clear", headerRow: boolean): CsvColumnSort | null;
12
+ export declare function toggleCsvColumnSort(grid: CsvGrid, state: CsvColumnSort | null, index: number, headerRow: boolean): CsvColumnSort | null;
13
+ export declare function noteCsvColumnSortEdit(state: CsvColumnSort, row: number, col: number, value: string): void;
14
+ export declare function noteCsvColumnInserted(state: CsvColumnSort | null, index: number): CsvColumnSort | null;
15
+ export declare function noteCsvColumnDeleted(state: CsvColumnSort | null, index: number): CsvColumnSort | null;
16
+ export declare function noteCsvColumnMoved(state: CsvColumnSort | null, from: number, to: number): CsvColumnSort | null;
@@ -0,0 +1,81 @@
1
+ export function nextCsvColumnSortDirection(state, index) {
2
+ if (!state || state.index !== index)
3
+ return "asc";
4
+ if (state.direction === "asc")
5
+ return "desc";
6
+ return "clear";
7
+ }
8
+ export function applyCsvColumnSort(grid, state, index, direction, headerRow) {
9
+ if (direction === "clear") {
10
+ if (state)
11
+ grid.replaceRows(state.baseline);
12
+ return null;
13
+ }
14
+ const baseline = state?.baseline ?? grid.getRows();
15
+ if (state)
16
+ grid.replaceRows(baseline);
17
+ const permutation = grid.sortColumn(index, direction === "asc", headerRow ? 1 : 0);
18
+ return { index, direction, baseline, order: permutation };
19
+ }
20
+ export function toggleCsvColumnSort(grid, state, index, headerRow) {
21
+ return applyCsvColumnSort(grid, state, index, nextCsvColumnSortDirection(state, index), headerRow);
22
+ }
23
+ export function noteCsvColumnSortEdit(state, row, col, value) {
24
+ const source = state.order[row];
25
+ const line = source == null ? undefined : state.baseline[source];
26
+ if (!line)
27
+ return;
28
+ while (line.length <= col)
29
+ line.push("");
30
+ line[col] = value;
31
+ }
32
+ export function noteCsvColumnInserted(state, index) {
33
+ if (!state)
34
+ return null;
35
+ return {
36
+ ...state,
37
+ baseline: state.baseline.map((row) => {
38
+ const next = [...row];
39
+ next.splice(index, 0, "");
40
+ return next;
41
+ }),
42
+ index: state.index >= index ? state.index + 1 : state.index,
43
+ };
44
+ }
45
+ export function noteCsvColumnDeleted(state, index) {
46
+ if (!state || state.index === index)
47
+ return null;
48
+ return {
49
+ ...state,
50
+ baseline: state.baseline.map((row) => {
51
+ const next = [...row];
52
+ if (index >= 0 && index < next.length)
53
+ next.splice(index, 1);
54
+ return next;
55
+ }),
56
+ index: state.index > index ? state.index - 1 : state.index,
57
+ };
58
+ }
59
+ export function noteCsvColumnMoved(state, from, to) {
60
+ if (!state || from === to)
61
+ return state;
62
+ return {
63
+ ...state,
64
+ baseline: state.baseline.map((row) => {
65
+ const next = [...row];
66
+ const [cell] = next.splice(from, 1);
67
+ next.splice(to, 0, cell ?? "");
68
+ return next;
69
+ }),
70
+ index: movedColumnIndex(state.index, from, to),
71
+ };
72
+ }
73
+ function movedColumnIndex(current, from, to) {
74
+ if (current === from)
75
+ return to;
76
+ if (from < current && to >= current)
77
+ return current - 1;
78
+ if (from > current && to <= current)
79
+ return current + 1;
80
+ return current;
81
+ }
@@ -0,0 +1,31 @@
1
+ import type { CsvDelimiter } from "../../csv-settings";
2
+ export type CsvGridOptions = {
3
+ delimiter: CsvDelimiter;
4
+ headerRow: boolean;
5
+ };
6
+ export declare class CsvGrid {
7
+ private rows;
8
+ private constructor();
9
+ static fromText(text: string, options: CsvGridOptions): CsvGrid;
10
+ static empty(): CsvGrid;
11
+ clone(): CsvGrid;
12
+ getRowCount(): number;
13
+ getColCount(): number;
14
+ getRows(): string[][];
15
+ getCell(row: number, col: number): string;
16
+ updateCell(row: number, col: number, value: string): void;
17
+ insertRowAt(index: number): void;
18
+ deleteRowAt(index: number): void;
19
+ insertColumnAt(index: number): void;
20
+ deleteColumnAt(index: number): void;
21
+ moveColumnAt(from: number, to: number): void;
22
+ moveRowAt(from: number, to: number): void;
23
+ clearCellsAt(coords: Array<[row: number, col: number]>): void;
24
+ ensureHeaderRow(initialLabels?: string[]): void;
25
+ replaceRows(rows: readonly string[][]): void;
26
+ /** Reorders body rows and returns the previous index now occupying each row. */
27
+ sortColumn(index: number, ascending: boolean, startRow?: number): number[];
28
+ toText(options: Pick<CsvGridOptions, "delimiter">): string;
29
+ private ensureSize;
30
+ private trimTrailingEmpty;
31
+ }
@@ -0,0 +1,161 @@
1
+ import { parseCsvText } from "./parse-csv";
2
+ import { serializeCsvRows } from "./serialize-csv";
3
+ export class CsvGrid {
4
+ rows;
5
+ constructor(rows) {
6
+ this.rows = rows.map((row) => [...row]);
7
+ }
8
+ static fromText(text, options) {
9
+ const { rows } = parseCsvText(text, { delimiter: options.delimiter });
10
+ const normalized = rows.length ? rows : [[""]];
11
+ return new CsvGrid(normalized);
12
+ }
13
+ static empty() {
14
+ return new CsvGrid([[""]]);
15
+ }
16
+ clone() {
17
+ return new CsvGrid(this.rows);
18
+ }
19
+ getRowCount() {
20
+ return this.rows.length;
21
+ }
22
+ getColCount() {
23
+ return this.rows.reduce((max, row) => Math.max(max, row.length), 0);
24
+ }
25
+ getRows() {
26
+ return this.rows.map((row) => [...row]);
27
+ }
28
+ getCell(row, col) {
29
+ return this.rows[row]?.[col] ?? "";
30
+ }
31
+ updateCell(row, col, value) {
32
+ this.ensureSize(row + 1, col + 1);
33
+ this.rows[row][col] = value;
34
+ this.trimTrailingEmpty();
35
+ }
36
+ insertRowAt(index) {
37
+ const width = this.getColCount();
38
+ const row = Array.from({ length: Math.max(width, 1) }, () => "");
39
+ const clamped = Math.max(0, Math.min(index, this.rows.length));
40
+ this.rows.splice(clamped, 0, row);
41
+ }
42
+ deleteRowAt(index) {
43
+ if (this.rows.length <= 1) {
44
+ this.rows = [[""]];
45
+ return;
46
+ }
47
+ if (index >= 0 && index < this.rows.length) {
48
+ this.rows.splice(index, 1);
49
+ }
50
+ }
51
+ insertColumnAt(index) {
52
+ const clamped = Math.max(0, Math.min(index, this.getColCount()));
53
+ for (const row of this.rows) {
54
+ row.splice(clamped, 0, "");
55
+ }
56
+ if (this.rows.length === 0) {
57
+ this.rows.push([""]);
58
+ }
59
+ }
60
+ deleteColumnAt(index) {
61
+ const width = this.getColCount();
62
+ if (width <= 1) {
63
+ this.rows = this.rows.map(() => [""]);
64
+ return;
65
+ }
66
+ for (const row of this.rows) {
67
+ if (index >= 0 && index < row.length) {
68
+ row.splice(index, 1);
69
+ }
70
+ }
71
+ }
72
+ moveColumnAt(from, to) {
73
+ if (from === to || from < 0 || to < 0) {
74
+ return;
75
+ }
76
+ const width = this.getColCount();
77
+ if (from >= width || to >= width) {
78
+ return;
79
+ }
80
+ for (const row of this.rows) {
81
+ const [cell] = row.splice(from, 1);
82
+ row.splice(to, 0, cell);
83
+ }
84
+ }
85
+ moveRowAt(from, to) {
86
+ if (from === to || from < 0 || to < 0) {
87
+ return;
88
+ }
89
+ if (from >= this.rows.length || to >= this.rows.length) {
90
+ return;
91
+ }
92
+ const [row] = this.rows.splice(from, 1);
93
+ this.rows.splice(to, 0, row);
94
+ }
95
+ clearCellsAt(coords) {
96
+ for (const [row, col] of coords) {
97
+ if (row >= 0 && col >= 0 && row < this.rows.length) {
98
+ if (col < this.rows[row].length) {
99
+ this.rows[row][col] = "";
100
+ }
101
+ }
102
+ }
103
+ this.trimTrailingEmpty();
104
+ }
105
+ ensureHeaderRow(initialLabels) {
106
+ if (this.rows.length === 0) {
107
+ const width = Math.max(this.getColCount(), initialLabels?.length ?? 1);
108
+ this.rows.push(Array.from({ length: width }, (_, column) => initialLabels?.[column] ?? ""));
109
+ return;
110
+ }
111
+ const width = this.getColCount();
112
+ this.rows.unshift(Array.from({ length: width }, (_, column) => initialLabels?.[column] ?? ""));
113
+ }
114
+ replaceRows(rows) {
115
+ this.rows = rows.map((row) => [...row]);
116
+ }
117
+ /** Reorders body rows and returns the previous index now occupying each row. */
118
+ sortColumn(index, ascending, startRow = 0) {
119
+ const current = this.rows;
120
+ const order = current.map((_, rowIndex) => rowIndex);
121
+ const body = order.slice(startRow);
122
+ body.sort((left, right) => {
123
+ const a = current[left]?.[index] ?? "";
124
+ const b = current[right]?.[index] ?? "";
125
+ const cmp = a.localeCompare(b, undefined, {
126
+ numeric: true,
127
+ sensitivity: "base",
128
+ });
129
+ return ascending ? cmp : -cmp;
130
+ });
131
+ const next = [...order.slice(0, startRow), ...body];
132
+ this.rows = next.map((rowIndex) => [...(current[rowIndex] ?? [])]);
133
+ return next;
134
+ }
135
+ toText(options) {
136
+ const rows = this.getRows();
137
+ if (rows.length === 0) {
138
+ return "";
139
+ }
140
+ return serializeCsvRows(rows, { delimiter: options.delimiter });
141
+ }
142
+ ensureSize(rowCount, colCount) {
143
+ while (this.rows.length < rowCount) {
144
+ this.rows.push(Array.from({ length: colCount }, () => ""));
145
+ }
146
+ for (const row of this.rows) {
147
+ while (row.length < colCount) {
148
+ row.push("");
149
+ }
150
+ }
151
+ }
152
+ trimTrailingEmpty() {
153
+ while (this.rows.length > 1) {
154
+ const last = this.rows[this.rows.length - 1];
155
+ if (last.some((cell) => cell.trim().length > 0)) {
156
+ break;
157
+ }
158
+ this.rows.pop();
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,38 @@
1
+ import { SearchQuery } from "@codemirror/search";
2
+ import { type Text } from "@codemirror/state";
3
+ export type CsvCellMatch = {
4
+ row: number;
5
+ col: number;
6
+ from: number;
7
+ to: number;
8
+ };
9
+ export type CsvSearchOptions = {
10
+ search: string;
11
+ replace?: string;
12
+ caseSensitive?: boolean;
13
+ wholeWord?: boolean;
14
+ regexp?: boolean;
15
+ };
16
+ export declare function csvSearchQuery(options: CsvSearchOptions): SearchQuery;
17
+ /** Matches in row-major order. An empty or invalid query matches nothing. */
18
+ export declare function csvCellMatches(rows: readonly (readonly string[])[], query: SearchQuery): CsvCellMatch[];
19
+ export declare function csvDocumentMatches(doc: Text, query: SearchQuery): Array<{
20
+ from: number;
21
+ to: number;
22
+ }>;
23
+ /** Replacement text for one match, including regular-expression substitutions. */
24
+ export declare function searchReplacement(query: SearchQuery, matched: string): string;
25
+ export declare function replaceCellText(cell: string, ranges: readonly {
26
+ from: number;
27
+ to: number;
28
+ }[], query: SearchQuery): string;
29
+ export declare function csvBodyCounts(rowCount: number, colCount: number, headerRow: boolean): {
30
+ columns: number;
31
+ rows: number;
32
+ };
33
+ /** Data rows that contain a match. Header matches do not count. */
34
+ export declare function csvShownRowCount(rowCount: number, headerRow: boolean, matches: readonly {
35
+ row: number;
36
+ }[]): number;
37
+ export declare function csvStatusLabel(columns: number, rows: number, shownRows?: number): string;
38
+ export declare function isCsvFindShortcut(event: KeyboardEvent): boolean;