@script-it/json-views-core 0.1.0-beta.1

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/THIRD_PARTY_NOTICES.txt +4917 -0
  4. package/dist/annotation-capabilities.d.ts +17 -0
  5. package/dist/annotation-capabilities.js +21 -0
  6. package/dist/atomic-value.d.ts +3 -0
  7. package/dist/atomic-value.js +6 -0
  8. package/dist/date-type.d.ts +48 -0
  9. package/dist/date-type.js +244 -0
  10. package/dist/diagnostics.d.ts +8 -0
  11. package/dist/diagnostics.js +252 -0
  12. package/dist/document-session.d.ts +52 -0
  13. package/dist/document-session.js +164 -0
  14. package/dist/formats/conversion.d.ts +17 -0
  15. package/dist/formats/conversion.js +75 -0
  16. package/dist/formats/csv-format.d.ts +31 -0
  17. package/dist/formats/csv-format.js +269 -0
  18. package/dist/formats/document-format.d.ts +35 -0
  19. package/dist/formats/document-format.js +1 -0
  20. package/dist/formats/json-format.d.ts +12 -0
  21. package/dist/formats/json-format.js +109 -0
  22. package/dist/html-view-bindings.d.ts +4 -0
  23. package/dist/html-view-bindings.js +32 -0
  24. package/dist/html-view-limits.d.ts +1 -0
  25. package/dist/html-view-limits.js +1 -0
  26. package/dist/index.d.ts +19 -0
  27. package/dist/index.js +19 -0
  28. package/dist/inference-fields.d.ts +5 -0
  29. package/dist/inference-fields.js +23 -0
  30. package/dist/inference.d.ts +26 -0
  31. package/dist/inference.js +304 -0
  32. package/dist/json-limits.d.ts +8 -0
  33. package/dist/json-limits.js +25 -0
  34. package/dist/json-path.d.ts +14 -0
  35. package/dist/json-path.js +84 -0
  36. package/dist/json-source-patcher.d.ts +97 -0
  37. package/dist/json-source-patcher.js +778 -0
  38. package/dist/metadata.d.ts +137 -0
  39. package/dist/metadata.js +952 -0
  40. package/dist/safe-pattern.d.ts +5 -0
  41. package/dist/safe-pattern.js +20 -0
  42. package/dist/schema-validation.d.ts +3 -0
  43. package/dist/schema-validation.js +4 -0
  44. package/dist/source-default.d.ts +2 -0
  45. package/dist/source-default.js +39 -0
  46. package/dist/table-suitability.d.ts +5 -0
  47. package/dist/table-suitability.js +55 -0
  48. package/dist/type-registry.d.ts +57 -0
  49. package/dist/type-registry.js +238 -0
  50. package/dist/value-comparison.d.ts +18 -0
  51. package/dist/value-comparison.js +133 -0
  52. package/dist/view-model.d.ts +18 -0
  53. package/dist/view-model.js +134 -0
  54. package/package.json +52 -0
  55. package/schema/jsonviews.schema.json +102 -0
@@ -0,0 +1,52 @@
1
+ export interface JsonDocumentSource {
2
+ content: string;
3
+ revision?: string;
4
+ }
5
+ export interface JsonDocumentSnapshot extends JsonDocumentSource {
6
+ documentId: string;
7
+ acknowledgedContent: string;
8
+ dirty: boolean;
9
+ saving: boolean;
10
+ error?: Error;
11
+ }
12
+ export interface JsonDocumentCommitContext {
13
+ documentId: string;
14
+ baseContent: string;
15
+ baseRevision?: string;
16
+ }
17
+ export type JsonDocumentSave = (content: string, context: JsonDocumentCommitContext) => Promise<void | JsonDocumentSource>;
18
+ export type DocumentContentValidator = (content: string) => void;
19
+ export declare class JsonDocumentConflictError extends Error {
20
+ constructor();
21
+ }
22
+ /** One document's draft, acknowledged source and in-flight persistence operation. */
23
+ export declare class JsonDocumentSession {
24
+ private snapshot;
25
+ private observed;
26
+ private readonly listeners;
27
+ private readonly acknowledgements;
28
+ private generation;
29
+ private pending;
30
+ private conflicted;
31
+ private readonly validateContent;
32
+ constructor(source: JsonDocumentSource & {
33
+ id: string;
34
+ validateContent?: DocumentContentValidator;
35
+ });
36
+ readonly getSnapshot: () => JsonDocumentSnapshot;
37
+ readonly subscribe: (listener: () => void) => (() => void);
38
+ private publish;
39
+ private remember;
40
+ private isAcknowledgedEcho;
41
+ /** Receive a persisted host snapshot. Authoritative reloads can intentionally restore
42
+ * a historical identity; opaque revisions alone cannot distinguish that from an echo.
43
+ */
44
+ receive(source: JsonDocumentSource, options?: {
45
+ authoritative?: boolean;
46
+ }): void;
47
+ /** Source is stored exactly; format-aware clients can report validity separately. */
48
+ edit(content: string): void;
49
+ /** Discard the current draft and restore the latest acknowledged host source. */
50
+ reset(): void;
51
+ commit(buildNext: (source: string) => string, save: JsonDocumentSave): Promise<void>;
52
+ }
@@ -0,0 +1,164 @@
1
+ export class JsonDocumentConflictError extends Error {
2
+ constructor() {
3
+ super('The document changed outside this editor. Your draft is preserved; reload the saved document before retrying.');
4
+ this.name = 'JsonDocumentConflictError';
5
+ }
6
+ }
7
+ /** One document's draft, acknowledged source and in-flight persistence operation. */
8
+ export class JsonDocumentSession {
9
+ snapshot;
10
+ observed;
11
+ listeners = new Set();
12
+ acknowledgements = [];
13
+ generation = 0;
14
+ pending;
15
+ conflicted = false;
16
+ validateContent;
17
+ constructor(source) {
18
+ this.validateContent = source.validateContent ?? (() => undefined);
19
+ this.observed = { content: source.content, revision: source.revision };
20
+ this.snapshot = {
21
+ documentId: source.id,
22
+ content: source.content,
23
+ acknowledgedContent: source.content,
24
+ revision: source.revision,
25
+ dirty: false,
26
+ saving: false,
27
+ };
28
+ this.remember(source);
29
+ }
30
+ getSnapshot = () => this.snapshot;
31
+ subscribe = (listener) => {
32
+ this.listeners.add(listener);
33
+ return () => { this.listeners.delete(listener); };
34
+ };
35
+ publish(next) {
36
+ this.snapshot = { ...this.snapshot, ...next };
37
+ this.snapshot.dirty = this.snapshot.content !== this.snapshot.acknowledgedContent;
38
+ this.listeners.forEach((listener) => listener());
39
+ }
40
+ remember(source) {
41
+ const existing = this.acknowledgements.findIndex((entry) => (entry.content === source.content && entry.revision === source.revision));
42
+ if (existing >= 0)
43
+ this.acknowledgements.splice(existing, 1);
44
+ this.acknowledgements.push({ content: source.content, revision: source.revision });
45
+ // Retain recent accepted identities to recognize delayed host echoes.
46
+ if (this.acknowledgements.length > 32)
47
+ this.acknowledgements.shift();
48
+ }
49
+ isAcknowledgedEcho(source) {
50
+ return this.acknowledgements.some((entry) => entry.content === source.content
51
+ && (source.revision === undefined || entry.revision === source.revision));
52
+ }
53
+ /** Receive a persisted host snapshot. Authoritative reloads can intentionally restore
54
+ * a historical identity; opaque revisions alone cannot distinguish that from an echo.
55
+ */
56
+ receive(source, options = {}) {
57
+ if (!options.authoritative && source.content === this.observed.content && source.revision === this.observed.revision)
58
+ return;
59
+ if (!options.authoritative && this.isAcknowledgedEcho(source)) {
60
+ this.observed = { ...source };
61
+ return;
62
+ }
63
+ if (source.content === this.pending?.content) {
64
+ // Matching in-flight observations remain provisional until persistence resolves.
65
+ this.pending.observed = {
66
+ content: source.content,
67
+ revision: source.revision ?? (this.pending.observed?.content === source.content ? this.pending.observed.revision : undefined),
68
+ };
69
+ return;
70
+ }
71
+ this.observed = { ...source };
72
+ if (source.content === this.snapshot.acknowledgedContent && !this.pending) {
73
+ if (source.revision !== undefined)
74
+ this.publish({ revision: source.revision });
75
+ this.remember({ content: source.content, revision: this.snapshot.revision });
76
+ return;
77
+ }
78
+ const preserveDraft = (this.snapshot.dirty && source.content !== this.snapshot.content) || this.pending !== undefined;
79
+ this.conflicted = preserveDraft;
80
+ if (this.pending) {
81
+ this.pending.conflicted = true;
82
+ this.pending.observed = { ...source };
83
+ }
84
+ this.remember(source);
85
+ this.publish({
86
+ acknowledgedContent: source.content,
87
+ revision: source.revision,
88
+ content: preserveDraft ? this.snapshot.content : source.content,
89
+ error: preserveDraft ? new JsonDocumentConflictError() : undefined,
90
+ });
91
+ }
92
+ /** Source is stored exactly; format-aware clients can report validity separately. */
93
+ edit(content) {
94
+ if (content === this.snapshot.content)
95
+ return;
96
+ this.generation += 1;
97
+ this.publish({ content, error: this.conflicted ? this.snapshot.error : undefined });
98
+ }
99
+ /** Discard the current draft and restore the latest acknowledged host source. */
100
+ reset() {
101
+ if (this.pending)
102
+ throw new Error('Wait for the current save before reloading the document');
103
+ this.generation += 1;
104
+ this.conflicted = false;
105
+ this.publish({ content: this.snapshot.acknowledgedContent, error: undefined });
106
+ }
107
+ async commit(buildNext, save) {
108
+ if (this.pending)
109
+ throw new Error('A save is already in progress');
110
+ if (this.conflicted)
111
+ throw new JsonDocumentConflictError();
112
+ let content;
113
+ try {
114
+ content = buildNext(this.snapshot.content);
115
+ this.validateContent(content);
116
+ }
117
+ catch (error) {
118
+ this.publish({ error: error instanceof Error ? error : new Error(String(error)) });
119
+ throw error;
120
+ }
121
+ if (content === this.snapshot.acknowledgedContent) {
122
+ this.edit(content);
123
+ this.publish({ error: undefined });
124
+ return;
125
+ }
126
+ const context = {
127
+ documentId: this.snapshot.documentId,
128
+ baseContent: this.snapshot.acknowledgedContent,
129
+ baseRevision: this.snapshot.revision,
130
+ };
131
+ if (content !== this.snapshot.content)
132
+ this.generation += 1;
133
+ const pending = { content, generation: this.generation, conflicted: false };
134
+ this.pending = pending;
135
+ this.publish({ content, saving: true, error: undefined });
136
+ try {
137
+ const result = await save(content, context);
138
+ const response = result ?? { content, revision: this.snapshot.revision };
139
+ if (pending.conflicted && pending.observed?.content !== response.content)
140
+ throw new JsonDocumentConflictError();
141
+ // A matching host observation during the request is newer evidence than its response.
142
+ // Revisions are opaque: only an identical accepted content can inherit that revision.
143
+ const accepted = pending.observed?.content === response.content && pending.observed.revision !== undefined
144
+ ? { content: response.content, revision: pending.observed.revision } : response;
145
+ this.remember(response);
146
+ this.remember(accepted);
147
+ this.conflicted = false;
148
+ this.publish({
149
+ error: undefined,
150
+ acknowledgedContent: accepted.content,
151
+ revision: accepted.revision,
152
+ content: this.generation === pending.generation ? accepted.content : this.snapshot.content,
153
+ });
154
+ }
155
+ catch (error) {
156
+ this.publish({ error: error instanceof Error ? error : new Error(String(error)) });
157
+ throw error;
158
+ }
159
+ finally {
160
+ this.pending = undefined;
161
+ this.publish({ saving: false });
162
+ }
163
+ }
164
+ }
@@ -0,0 +1,17 @@
1
+ import type { StructuredDocumentFormat } from './document-format.js';
2
+ export interface ConvertToJsonViewsDocumentOptions {
3
+ root: unknown;
4
+ metadata?: unknown;
5
+ sourceFormat: StructuredDocumentFormat;
6
+ /** Authoritative JSON source when available; preserves exact data tokens. */
7
+ source?: string;
8
+ }
9
+ export declare function rebaseJsonViewsRootPath(path: string): string;
10
+ /** Rebase only specification-defined path positions. Filter values and extension
11
+ * payloads are user data, even when they contain properties named path/title.
12
+ */
13
+ export declare function rebaseJsonViewsMetadata(metadata: unknown): Record<string, unknown>;
14
+ /** Converts an array-root JSON or CSV-normalized root into the canonical envelope.
15
+ * Pass source for imported JSON; a parsed root alone cannot retain source spelling.
16
+ */
17
+ export declare function convertToJsonViewsDocument({ root, metadata, sourceFormat, source }: ConvertToJsonViewsDocumentOptions): string;
@@ -0,0 +1,75 @@
1
+ import { stringifyJsonValue, upsertJsonObjectPropertyInSource } from '../json-source-patcher.js';
2
+ function isRecord(value) {
3
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
4
+ }
5
+ function versionOneMetadata(value) {
6
+ if (value === undefined)
7
+ return { version: 1, schema: {}, views: [] };
8
+ if (!isRecord(value))
9
+ throw new TypeError('Annotations must be an object');
10
+ if (value.version !== undefined && value.version !== 1)
11
+ throw new TypeError('Only version 1 annotations can be converted');
12
+ return { ...value, version: 1 };
13
+ }
14
+ export function rebaseJsonViewsRootPath(path) {
15
+ if (path === '$')
16
+ return '$.data';
17
+ return path.startsWith('$') ? `$.data${path.slice(1)}` : path;
18
+ }
19
+ function rebaseField(value) {
20
+ if (!isRecord(value))
21
+ return value;
22
+ return { ...value, ...(typeof value.path === 'string' ? { path: rebaseJsonViewsRootPath(value.path) } : {}) };
23
+ }
24
+ /** Rebase only specification-defined path positions. Filter values and extension
25
+ * payloads are user data, even when they contain properties named path/title.
26
+ */
27
+ export function rebaseJsonViewsMetadata(metadata) {
28
+ const source = versionOneMetadata(metadata);
29
+ const views = Array.isArray(source.views) ? source.views.map((value) => {
30
+ if (!isRecord(value))
31
+ return value;
32
+ const view = { ...value };
33
+ for (const key of ['path', 'groupBy', 'orderPath']) {
34
+ if (typeof view[key] === 'string')
35
+ view[key] = rebaseJsonViewsRootPath(view[key]);
36
+ }
37
+ for (const key of ['columns', 'sort']) {
38
+ if (Array.isArray(view[key]))
39
+ view[key] = view[key].map(rebaseField);
40
+ }
41
+ if (isRecord(view.filter) && Array.isArray(view.filter.rules)) {
42
+ view.filter = { ...view.filter, rules: view.filter.rules.map(rebaseField) };
43
+ }
44
+ return view;
45
+ }) : source.views ?? [];
46
+ const schema = isRecord(source.schema)
47
+ ? Object.fromEntries(Object.entries(source.schema).map(([path, descriptor]) => [rebaseJsonViewsRootPath(path), descriptor]))
48
+ : source.schema ?? {};
49
+ return { ...source, schema, views };
50
+ }
51
+ /** Converts an array-root JSON or CSV-normalized root into the canonical envelope.
52
+ * Pass source for imported JSON; a parsed root alone cannot retain source spelling.
53
+ */
54
+ export function convertToJsonViewsDocument({ root, metadata, sourceFormat, source }) {
55
+ const authoritative = source === undefined ? root : JSON.parse(source);
56
+ if (sourceFormat === 'json-object') {
57
+ if (!isRecord(authoritative))
58
+ throw new TypeError('Object-root conversion requires a JSON object');
59
+ const annotations = versionOneMetadata(metadata);
60
+ if (source !== undefined)
61
+ return upsertJsonObjectPropertyInSource(source, [], '$jsonviews', annotations);
62
+ return `${stringifyJsonValue({ ...authoritative, $jsonviews: annotations }, 2)}\n`;
63
+ }
64
+ if (!Array.isArray(authoritative))
65
+ throw new TypeError('CSV and array-root conversion requires an array root');
66
+ if (sourceFormat === 'json-array' && source !== undefined) {
67
+ const newline = source.includes('\r\n') ? '\r\n' : '\n';
68
+ const indent = (value) => value.replace(/\r?\n/g, `${newline} `);
69
+ const metadataSource = indent(stringifyJsonValue(rebaseJsonViewsMetadata(metadata), 2));
70
+ const dataSource = indent(source.trim());
71
+ const trailing = /\r?\n$/.test(source) ? newline : '';
72
+ return `{${newline} "$jsonviews": ${metadataSource},${newline} "data": ${dataSource}${newline}}${trailing}`;
73
+ }
74
+ return `${stringifyJsonValue({ $jsonviews: rebaseJsonViewsMetadata(metadata), data: authoritative }, 2)}\n`;
75
+ }
@@ -0,0 +1,31 @@
1
+ import type { DocumentDiagnostic, StructuredDocumentAdapter, ValueReplacement } from './document-format.js';
2
+ export interface CsvCellState {
3
+ start: number;
4
+ end: number;
5
+ raw: string;
6
+ value: string;
7
+ quoted: boolean;
8
+ }
9
+ export interface CsvRecordState {
10
+ start: number;
11
+ end: number;
12
+ separatorStart: number;
13
+ separatorEnd: number;
14
+ cells: CsvCellState[];
15
+ }
16
+ export interface CsvDocumentState {
17
+ headers: string[];
18
+ records: CsvRecordState[];
19
+ lineEnding: '\n' | '\r\n';
20
+ bom: boolean;
21
+ finalNewline: boolean;
22
+ editable: boolean;
23
+ }
24
+ export declare function inspectCsvSource(source: string): {
25
+ root: Array<Record<string, unknown>>;
26
+ state: CsvDocumentState;
27
+ diagnostics: DocumentDiagnostic[];
28
+ };
29
+ export declare function validateCsvSource(source: string): void;
30
+ export declare const csvFormat: StructuredDocumentAdapter<CsvDocumentState>;
31
+ export type { ValueReplacement };
@@ -0,0 +1,269 @@
1
+ import { describeValuePath } from '../json-path.js';
2
+ import { sourceDiagnosticHelp } from '../diagnostics.js';
3
+ function parseCsv(source) {
4
+ const diagnostics = [];
5
+ const records = [];
6
+ const bom = source.charCodeAt(0) === 0xfeff;
7
+ let index = bom ? 1 : 0;
8
+ let firstLineEnding;
9
+ let fatal;
10
+ while (index < source.length) {
11
+ const recordStart = index;
12
+ const cells = [];
13
+ let recordEnd = index;
14
+ let separatorStart = index;
15
+ let separatorEnd = index;
16
+ let finished = false;
17
+ while (!finished) {
18
+ const start = index;
19
+ let value = '';
20
+ let quoted = false;
21
+ if (source[index] === '"') {
22
+ quoted = true;
23
+ index += 1;
24
+ let closed = false;
25
+ while (index < source.length) {
26
+ if (source[index] !== '"') {
27
+ value += source[index++];
28
+ continue;
29
+ }
30
+ if (source[index + 1] === '"') {
31
+ value += '"';
32
+ index += 2;
33
+ continue;
34
+ }
35
+ index += 1;
36
+ closed = true;
37
+ break;
38
+ }
39
+ if (!closed) {
40
+ fatal = new SyntaxError('Unterminated quoted CSV cell');
41
+ diagnostics.push({ code: 'unterminated-quote', message: fatal.message, severity: 'error', start, end: index });
42
+ }
43
+ if (closed && index < source.length && source[index] !== ',' && source[index] !== '\n' && source[index] !== '\r') {
44
+ fatal = new SyntaxError('Unexpected content after a quoted CSV cell');
45
+ diagnostics.push({ code: 'invalid-quoted-cell', message: fatal.message, severity: 'error', start, end: index + 1 });
46
+ while (index < source.length && source[index] !== ',' && source[index] !== '\n' && source[index] !== '\r')
47
+ index += 1;
48
+ }
49
+ }
50
+ else {
51
+ while (index < source.length && source[index] !== ',' && source[index] !== '\n' && source[index] !== '\r') {
52
+ if (source[index] === '"' && !fatal) {
53
+ fatal = new SyntaxError('Quotes inside an unquoted CSV cell are invalid');
54
+ diagnostics.push({ code: 'invalid-quote', message: fatal.message, severity: 'error', start: index, end: index + 1 });
55
+ }
56
+ value += source[index++];
57
+ }
58
+ }
59
+ cells.push({ start, end: index, raw: source.slice(start, index), value, quoted });
60
+ if (source[index] === ',') {
61
+ index += 1;
62
+ if (index === source.length) {
63
+ cells.push({ start: index, end: index, raw: '', value: '', quoted: false });
64
+ recordEnd = index;
65
+ separatorStart = index;
66
+ separatorEnd = index;
67
+ finished = true;
68
+ }
69
+ continue;
70
+ }
71
+ recordEnd = index;
72
+ separatorStart = index;
73
+ if (source[index] === '\r' && source[index + 1] === '\n') {
74
+ firstLineEnding ??= '\r\n';
75
+ index += 2;
76
+ }
77
+ else if (source[index] === '\n') {
78
+ firstLineEnding ??= '\n';
79
+ index += 1;
80
+ }
81
+ else if (source[index] === '\r') {
82
+ firstLineEnding ??= '\n';
83
+ diagnostics.push({ code: 'bare-cr', message: 'Bare carriage returns are not a supported CSV line ending', severity: 'warning', start: index, end: index + 1 });
84
+ index += 1;
85
+ }
86
+ separatorEnd = index;
87
+ finished = true;
88
+ }
89
+ records.push({ start: recordStart, end: recordEnd, separatorStart, separatorEnd, cells });
90
+ }
91
+ const headerRecord = records[0];
92
+ const headers = headerRecord?.cells.map((cell) => cell.value) ?? [];
93
+ if (!headerRecord)
94
+ diagnostics.push({ code: 'missing-header', message: 'CSV requires a header record', severity: 'error' });
95
+ const seen = new Set();
96
+ headers.forEach((header, column) => {
97
+ const cell = headerRecord.cells[column];
98
+ if (!header)
99
+ diagnostics.push({ code: 'missing-header', message: `Column ${column + 1} has an empty header`, severity: 'error', path: [column], start: cell.start, end: cell.end });
100
+ else if (seen.has(header))
101
+ diagnostics.push({ code: 'duplicate-header', message: `Header ${JSON.stringify(header)} is duplicated`, severity: 'error', path: [column], start: cell.start, end: cell.end });
102
+ seen.add(header);
103
+ });
104
+ records.slice(1).forEach((record, row) => {
105
+ if (record.cells.length !== headers.length)
106
+ diagnostics.push({
107
+ code: 'irregular-row',
108
+ message: `Row ${row + 2} has ${record.cells.length} cells; expected ${headers.length}`,
109
+ severity: 'warning', path: [row], start: record.start, end: record.end,
110
+ });
111
+ });
112
+ const editable = Boolean(headerRecord) && headers.length > 0 && headers.every(Boolean) && new Set(headers).size === headers.length && !fatal;
113
+ return {
114
+ state: {
115
+ headers,
116
+ records,
117
+ lineEnding: firstLineEnding ?? '\n',
118
+ bom,
119
+ finalNewline: Boolean(records.at(-1) && records.at(-1).separatorEnd > records.at(-1).separatorStart),
120
+ editable,
121
+ },
122
+ diagnostics: diagnostics.map((item) => ({ ...item, help: sourceDiagnosticHelp(item.code) })),
123
+ ...(fatal ? { fatal } : {}),
124
+ };
125
+ }
126
+ function safelyRoundTripsNumber(value) {
127
+ if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value))
128
+ return false;
129
+ const number = Number(value);
130
+ if (!Number.isFinite(number) || Object.is(number, -0))
131
+ return false;
132
+ if (Number.isInteger(number) && !Number.isSafeInteger(number))
133
+ return false;
134
+ return String(number) === value;
135
+ }
136
+ function inferredColumns(state) {
137
+ return state.headers.map((_header, column) => {
138
+ const values = state.records.slice(1).map((record) => record.cells[column]?.value ?? '').filter((value) => value !== '');
139
+ if (values.length > 0 && values.every((value) => value === 'true' || value === 'false'))
140
+ return 'boolean';
141
+ if (values.length > 0 && values.every(safelyRoundTripsNumber))
142
+ return 'number';
143
+ return 'string';
144
+ });
145
+ }
146
+ function normalizedRoot(state) {
147
+ const types = inferredColumns(state);
148
+ return state.records.slice(1).map((record) => Object.fromEntries(record.cells.map((cell, column) => {
149
+ const original = state.headers[column];
150
+ let key = original || `$column${column + 1}`;
151
+ if (state.headers.indexOf(key) !== column)
152
+ key = `${key} [${column + 1}]`;
153
+ const value = cell.value === '' ? '' : types[column] === 'number' ? Number(cell.value) : types[column] === 'boolean' ? cell.value === 'true' : cell.value;
154
+ return [key, value];
155
+ })));
156
+ }
157
+ function assertEditable(state) {
158
+ if (!state.editable)
159
+ throw new TypeError('Repair missing or duplicate CSV headers before structured editing');
160
+ }
161
+ function csvScalar(value) {
162
+ if (typeof value === 'string')
163
+ return value;
164
+ if (typeof value === 'boolean')
165
+ return value ? 'true' : 'false';
166
+ if (typeof value === 'number' && Number.isFinite(value))
167
+ return String(value);
168
+ throw new TypeError('CSV cells can contain only strings, finite numbers, or booleans');
169
+ }
170
+ function encodeCell(value, forceQuote = false) {
171
+ const text = csvScalar(value);
172
+ const quote = forceQuote || /[",\r\n]/.test(text) || /^\s|\s$/.test(text);
173
+ return quote ? `"${text.replace(/"/g, '""')}"` : text;
174
+ }
175
+ function cellForPath(state, path) {
176
+ assertEditable(state);
177
+ if (path.length !== 2 || typeof path[0] !== 'number' || !Number.isSafeInteger(path[0]) || path[0] < 0 || typeof path[1] !== 'string') {
178
+ throw new TypeError(`CSV path ${describeValuePath(path)} must identify a row and header`);
179
+ }
180
+ const column = state.headers.indexOf(path[1]);
181
+ const cell = state.records[path[0] + 1]?.cells[column];
182
+ if (column < 0 || !cell)
183
+ throw new RangeError(`CSV path ${describeValuePath(path)} does not exist`);
184
+ return cell;
185
+ }
186
+ function applyEdits(source, edits) {
187
+ const ordered = edits.sort((left, right) => left.start - right.start || left.end - right.end);
188
+ for (let index = 1; index < ordered.length; index += 1) {
189
+ if (ordered[index].start < ordered[index - 1].end || ordered[index].start === ordered[index - 1].start)
190
+ throw new TypeError('CSV edits overlap');
191
+ }
192
+ let cursor = 0;
193
+ const parts = [];
194
+ ordered.forEach((edit) => {
195
+ parts.push(source.slice(cursor, edit.start), edit.text);
196
+ cursor = edit.end;
197
+ });
198
+ parts.push(source.slice(cursor));
199
+ return parts.join('');
200
+ }
201
+ export function inspectCsvSource(source) {
202
+ const parsed = parseCsv(source);
203
+ return { root: normalizedRoot(parsed.state), state: parsed.state, diagnostics: parsed.diagnostics };
204
+ }
205
+ export function validateCsvSource(source) {
206
+ const parsed = parseCsv(source);
207
+ if (parsed.fatal)
208
+ throw parsed.fatal;
209
+ }
210
+ export const csvFormat = {
211
+ format: 'csv',
212
+ inspect: inspectCsvSource,
213
+ validate: validateCsvSource,
214
+ replace(source, state, path, value) {
215
+ const cell = cellForPath(state, path);
216
+ return applyEdits(source, [{ start: cell.start, end: cell.end, text: encodeCell(value, cell.quoted) }]);
217
+ },
218
+ replaceMany(source, state, changes) {
219
+ const edits = changes.map(({ path, value }) => {
220
+ const cell = cellForPath(state, path);
221
+ return { start: cell.start, end: cell.end, text: encodeCell(value, cell.quoted) };
222
+ });
223
+ return applyEdits(source, edits);
224
+ },
225
+ removeMany(source, state, paths) {
226
+ assertEditable(state);
227
+ const rows = [...new Set(paths.map((path) => {
228
+ if (path.length !== 1 || typeof path[0] !== 'number' || !Number.isSafeInteger(path[0]) || path[0] < 0)
229
+ throw new TypeError(`CSV path ${describeValuePath(path)} must identify a row`);
230
+ if (!state.records[path[0] + 1])
231
+ throw new RangeError(`CSV row ${path[0]} does not exist`);
232
+ return path[0];
233
+ }))].sort((a, b) => a - b);
234
+ const edits = [];
235
+ for (let cursor = 0; cursor < rows.length; cursor++) {
236
+ const first = rows[cursor];
237
+ let last = first;
238
+ while (rows[cursor + 1] === last + 1)
239
+ last = rows[++cursor];
240
+ const start = state.records[first + 1];
241
+ const end = state.records[last + 1];
242
+ edits.push(end.separatorEnd > end.separatorStart
243
+ ? { start: start.start, end: end.separatorEnd, text: '' }
244
+ : { start: state.records[first].separatorStart, end: end.end, text: '' });
245
+ }
246
+ return applyEdits(source, edits);
247
+ },
248
+ append(source, state, path, value) {
249
+ assertEditable(state);
250
+ if (path.length !== 0 || value === null || typeof value !== 'object' || Array.isArray(value)) {
251
+ throw new TypeError('CSV rows must be appended to the document root as objects');
252
+ }
253
+ const row = value;
254
+ if (Object.keys(row).some((key) => !state.headers.includes(key)))
255
+ throw new TypeError('CSV rows cannot contain properties missing from the header');
256
+ const encoded = state.headers.map((header) => encodeCell(Object.prototype.hasOwnProperty.call(row, header) ? row[header] : '')).join(',');
257
+ const separator = state.lineEnding;
258
+ return state.finalNewline ? `${source}${encoded}${separator}` : `${source}${separator}${encoded}`;
259
+ },
260
+ canRepresent(value) {
261
+ try {
262
+ csvScalar(value);
263
+ return { representable: true };
264
+ }
265
+ catch {
266
+ return { representable: false, reason: 'CSV cells cannot represent nested values or null distinctly from an empty cell' };
267
+ }
268
+ },
269
+ };
@@ -0,0 +1,35 @@
1
+ import type { ValuePath } from '../json-path.js';
2
+ export type StructuredDocumentFormat = 'json-object' | 'json-array' | 'csv';
3
+ export interface DocumentDiagnostic {
4
+ code: string;
5
+ message: string;
6
+ severity: 'warning' | 'error';
7
+ path?: ValuePath;
8
+ start?: number;
9
+ end?: number;
10
+ help?: import('../annotation-capabilities.js').JsonViewDiagnosticHelp;
11
+ }
12
+ export interface ValueReplacement {
13
+ path: ValuePath;
14
+ value: unknown;
15
+ }
16
+ export type FormatCapability = {
17
+ representable: true;
18
+ } | {
19
+ representable: false;
20
+ reason: string;
21
+ };
22
+ export interface StructuredDocumentAdapter<TState> {
23
+ readonly format: StructuredDocumentFormat;
24
+ inspect(source: string): {
25
+ root: unknown;
26
+ state: TState;
27
+ diagnostics: DocumentDiagnostic[];
28
+ };
29
+ validate(source: string): void;
30
+ replace(source: string, state: TState, path: ValuePath, value: unknown): string;
31
+ replaceMany(source: string, state: TState, changes: readonly ValueReplacement[]): string;
32
+ removeMany(source: string, state: TState, paths: readonly ValuePath[]): string;
33
+ append(source: string, state: TState, path: ValuePath, value: unknown): string;
34
+ canRepresent(value: unknown): FormatCapability;
35
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { ValuePath } from '../json-path.js';
2
+ import type { StructuredDocumentAdapter, ValueReplacement } from './document-format.js';
3
+ export interface JsonDocumentState {
4
+ root: unknown;
5
+ }
6
+ export declare const jsonObjectFormat: StructuredDocumentAdapter<JsonDocumentState>;
7
+ export declare const jsonArrayFormat: StructuredDocumentAdapter<JsonDocumentState>;
8
+ export declare function jsonFormatForSource(source: string): StructuredDocumentAdapter<JsonDocumentState>;
9
+ export declare function jsonFormatForRoot(root: unknown): StructuredDocumentAdapter<JsonDocumentState>;
10
+ /** Pretty-print JSON without changing numeric spelling, duplicate keys, or string contents. */
11
+ export declare function formatJsonSource(source: string): string;
12
+ export type { ValuePath, ValueReplacement };