@aiquants/spreadsheet 0.16.0 → 0.16.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.
package/dist/index.d.cts CHANGED
@@ -670,6 +670,22 @@ export declare type HtmlTableResult = {
670
670
  readonly outcome: "none";
671
671
  };
672
672
 
673
+ /**
674
+ * Yields the rows of a text that may be only the beginning of a TSV (a file read partway, a stream not yet ended), in
675
+ * exactly iterateTsvRows's grammar, but only those the rest of the text cannot change: every row the text ends with a
676
+ * line terminator, up to the first field that opens a quote the text does not close (whether it is one quoted field or a
677
+ * literal one split at its line breaks is the rest's to decide). So the rows of any beginning of a TSV are the first
678
+ * rows of the whole: a reader of a stream can show them as final. Empty text yields no row. Unpadded.
679
+ * TSV の先頭だけかもしれない文字 (途中まで読んだファイル、まだ終わらないストリーム) の行を、iterateTsvRows とちょうど同じ文法で、
680
+ * ただし文字の続きが変えられない行だけ出す処理。文字が改行で終える行すべてを、文字が閉じない引用を開く最初の値まで出す (それが 1 つの
681
+ * 引用の値か、改行で割れる文字どおりの値かは続きが決める)。そのため TSV のどの先頭の行も全体の最初の行で、ストリームの読み手は
682
+ * それを確定したものとして見せられる。空の文字は行を出さない。埋めない。
683
+ *
684
+ * @param text - The beginning of a TSV text, as read. 読んだままの TSV の文字の先頭。
685
+ * @returns The rows the rest of the text cannot change, unpadded. 文字の続きが変えられない行 (埋めない)。
686
+ */
687
+ export declare function iterateTsvPrefixRows(text: string): Generator<string[]>;
688
+
673
689
  /**
674
690
  * Yields the rows of a TSV text one at a time, in exactly parseTsv's grammar (the same quoting rule, line terminators
675
691
  * and final-terminator handling; empty text is one row with one empty cell), WITHOUT padding them to a common width. A
package/dist/index.d.ts CHANGED
@@ -670,6 +670,22 @@ export declare type HtmlTableResult = {
670
670
  readonly outcome: "none";
671
671
  };
672
672
 
673
+ /**
674
+ * Yields the rows of a text that may be only the beginning of a TSV (a file read partway, a stream not yet ended), in
675
+ * exactly iterateTsvRows's grammar, but only those the rest of the text cannot change: every row the text ends with a
676
+ * line terminator, up to the first field that opens a quote the text does not close (whether it is one quoted field or a
677
+ * literal one split at its line breaks is the rest's to decide). So the rows of any beginning of a TSV are the first
678
+ * rows of the whole: a reader of a stream can show them as final. Empty text yields no row. Unpadded.
679
+ * TSV の先頭だけかもしれない文字 (途中まで読んだファイル、まだ終わらないストリーム) の行を、iterateTsvRows とちょうど同じ文法で、
680
+ * ただし文字の続きが変えられない行だけ出す処理。文字が改行で終える行すべてを、文字が閉じない引用を開く最初の値まで出す (それが 1 つの
681
+ * 引用の値か、改行で割れる文字どおりの値かは続きが決める)。そのため TSV のどの先頭の行も全体の最初の行で、ストリームの読み手は
682
+ * それを確定したものとして見せられる。空の文字は行を出さない。埋めない。
683
+ *
684
+ * @param text - The beginning of a TSV text, as read. 読んだままの TSV の文字の先頭。
685
+ * @returns The rows the rest of the text cannot change, unpadded. 文字の続きが変えられない行 (埋めない)。
686
+ */
687
+ export declare function iterateTsvPrefixRows(text: string): Generator<string[]>;
688
+
673
689
  /**
674
690
  * Yields the rows of a TSV text one at a time, in exactly parseTsv's grammar (the same quoting rule, line terminators
675
691
  * and final-terminator handling; empty text is one row with one empty cell), WITHOUT padding them to a common width. A
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { serializeClipboardTsv as hu, measureTsv as gu, parseTsv as mu } from "./tsv.js";
2
- import { iterateTsvRows as r0, serializeTsv as o0 } from "./tsv.js";
2
+ import { iterateTsvPrefixRows as r0, iterateTsvRows as o0, serializeTsv as s0 } from "./tsv.js";
3
3
  import { S as be, c as Wr, a as Fo, b as wu, d as yu, s as No } from "./view-DycJZi_t.js";
4
- import { e as i0 } from "./view-DycJZi_t.js";
4
+ import { e as l0 } from "./view-DycJZi_t.js";
5
5
  import { VIRTUAL_SCROLL_LABEL_CATALOGS as Ai, resolveVirtualScrollLocale as Ul, VIRTUAL_SCROLL_LABEL_KEYS as oo, resolveVirtualScrollLabels as vu, useWheelBridge as ms, FenwickMapTree as Cu, VirtualGrid as bu } from "@aiquants/virtualscroll";
6
6
  import { jsxs as Mt, jsx as J, Fragment as Xl } from "react/jsx-runtime";
7
7
  import { useRef as R, useCallback as S, useMemo as zt, useLayoutEffect as Jt, useEffect as Ht, useState as se, useId as jn, memo as Gl, useSyncExternalStore as so, forwardRef as Ru, useImperativeHandle as Su } from "react";
@@ -8409,7 +8409,7 @@ export {
8409
8409
  t0 as Spreadsheet,
8410
8410
  De as canonicalizeFormat,
8411
8411
  Wr as columnIndexToName,
8412
- i0 as columnNameToIndex,
8412
+ l0 as columnNameToIndex,
8413
8413
  Ed as detectClipSource,
8414
8414
  pn as enumerateRangeCells,
8415
8415
  Jw as estimatePasteCellCount,
@@ -8417,7 +8417,8 @@ export {
8417
8417
  gr as formatCellValue,
8418
8418
  Gw as formatEquals,
8419
8419
  co as fromR1C1,
8420
- r0 as iterateTsvRows,
8420
+ r0 as iterateTsvPrefixRows,
8421
+ o0 as iterateTsvRows,
8421
8422
  vt as makeRange,
8422
8423
  gu as measureTsv,
8423
8424
  nc as mergeFormat,
@@ -8433,7 +8434,7 @@ export {
8433
8434
  ud as serializeClipPayload,
8434
8435
  hu as serializeClipboardTsv,
8435
8436
  Dp as serializeHtmlTable,
8436
- o0 as serializeTsv,
8437
+ s0 as serializeTsv,
8437
8438
  kf as shiftFormula,
8438
8439
  xs as toR1C1,
8439
8440
  ug as validateCellText
package/dist/tsv.cjs CHANGED
@@ -1,8 +1,8 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=(e,t)=>`${e.map(n=>n.map(t).join(" ")).join(`\r
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=(e,n)=>`${e.map(t=>t.map(n).join(" ")).join(`\r
2
2
  `)}\r
3
- `,f=e=>o(e,v),h=e=>o(e,T),v=e=>/[\t\r\n]/.test(e)||e.startsWith('"')?`"${e.replaceAll('"','""')}"`:e,T=e=>/[\t\r\n]/.test(e)?`"${e.replaceAll('"','""')}"`:e,c=e=>e.endsWith(`\r
3
+ `,p=e=>c(e,w),v=e=>c(e,m),w=e=>/[\t\r\n]/.test(e)||e.startsWith('"')?`"${e.replaceAll('"','""')}"`:e,m=e=>/[\t\r\n]/.test(e)?`"${e.replaceAll('"','""')}"`:e,a=e=>e.endsWith(`\r
4
4
  `)?e.slice(0,-2):e.endsWith(`
5
- `)||e.endsWith("\r")?e.slice(0,-1):e,l=Symbol("row end");function*a(e){let t=0;for(;;){const n=e[t]==='"'?m(e,t):null;if(n!==null)yield n.value,t=n.nextIndex;else{const s=w(e,t);yield e.slice(t,s),t=s}if(t>=e.length){yield l;return}e[t]===" "?t+=1:(t+=e[t]==="\r"&&e[t+1]===`
6
- `?2:1,yield l)}}function*u(e){const t=c(e);if(t===""){yield[""];return}let n=[];for(const s of a(t))s===l?(yield n,n=[]):n.push(s)}const p=e=>{const t=Array.from(u(e));let n=0;for(const s of t)s.length>n&&(n=s.length);return t.map(s=>s.length===n?s:[...s,...Array(n-s.length).fill("")])},g=e=>{const t=c(e);if(t==="")return{rows:1,cols:1};let n=0,s=0,r=0;for(const i of a(t))i===l?(n+=1,r>s&&(s=r),r=0):r+=1;return{rows:n,cols:s}},m=(e,t)=>{let n="",s=t+1;for(;s<e.length;)if(e[s]==='"')if(e[s+1]==='"')n+='"',s+=2;else{const r=s+1;return(r>=e.length||e[r]===" "||e[r]===`
7
- `||e[r]==="\r")&&/[\t\r\n"]/.test(n)?{value:n,nextIndex:r}:null}else n+=e[s],s+=1;return null},w=(e,t)=>{for(let n=t;n<e.length;n+=1)if(e[n]===" "||e[n]===`
8
- `||e[n]==="\r")return n;return e.length};exports.iterateTsvRows=u;exports.measureTsv=g;exports.parseTsv=p;exports.serializeClipboardTsv=h;exports.serializeTsv=f;
5
+ `)||e.endsWith("\r")?e.slice(0,-1):e,l=Symbol("row end");function*o(e,n){let t=0;for(;;){const s=e[t]==='"'?R(e,t):T;if(s===h&&n==="prefix")return;if(typeof s=="object")yield s.value,t=s.nextIndex;else{const r=j(e,t);yield e.slice(t,r),t=r}if(t>=e.length){n==="whole"&&(yield l);return}e[t]===" "?t+=1:(t+=e[t]==="\r"&&e[t+1]===`
6
+ `?2:1,yield l)}}function*u(e){const n=a(e);if(n===""){yield[""];return}yield*f(o(n,"whole"))}function*d(e){yield*f(o(e,"prefix"))}function*f(e){let n=[];for(const t of e)t===l?(yield n,n=[]):n.push(t)}const g=e=>{const n=Array.from(u(e));let t=0;for(const s of n)s.length>t&&(t=s.length);return n.map(s=>s.length===t?s:[...s,...Array(t-s.length).fill("")])},O=e=>{const n=a(e);if(n==="")return{rows:1,cols:1};let t=0,s=0,r=0;for(const i of o(n,"whole"))i===l?(t+=1,r>s&&(s=r),r=0):r+=1;return{rows:t,cols:s}},T=Symbol("literal field"),h=Symbol("open quote"),R=(e,n)=>{let t="",s=n+1;for(;;){const r=e.indexOf('"',s);if(r<0)return h;if(t+=e.slice(s,r),e[r+1]==='"'){t+='"',s=r+2;continue}const i=r+1;return(i>=e.length||e[i]===" "||e[i]===`
7
+ `||e[i]==="\r")&&/[\t\r\n"]/.test(t)?{value:t,nextIndex:i}:T}},j=(e,n)=>{for(let t=n;t<e.length;t+=1)if(e[t]===" "||e[t]===`
8
+ `||e[t]==="\r")return t;return e.length};exports.iterateTsvPrefixRows=d;exports.iterateTsvRows=u;exports.measureTsv=O;exports.parseTsv=g;exports.serializeClipboardTsv=v;exports.serializeTsv=p;
package/dist/tsv.d.cts CHANGED
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Yields the rows of a text that may be only the beginning of a TSV (a file read partway, a stream not yet ended), in
3
+ * exactly iterateTsvRows's grammar, but only those the rest of the text cannot change: every row the text ends with a
4
+ * line terminator, up to the first field that opens a quote the text does not close (whether it is one quoted field or a
5
+ * literal one split at its line breaks is the rest's to decide). So the rows of any beginning of a TSV are the first
6
+ * rows of the whole: a reader of a stream can show them as final. Empty text yields no row. Unpadded.
7
+ * TSV の先頭だけかもしれない文字 (途中まで読んだファイル、まだ終わらないストリーム) の行を、iterateTsvRows とちょうど同じ文法で、
8
+ * ただし文字の続きが変えられない行だけ出す処理。文字が改行で終える行すべてを、文字が閉じない引用を開く最初の値まで出す (それが 1 つの
9
+ * 引用の値か、改行で割れる文字どおりの値かは続きが決める)。そのため TSV のどの先頭の行も全体の最初の行で、ストリームの読み手は
10
+ * それを確定したものとして見せられる。空の文字は行を出さない。埋めない。
11
+ *
12
+ * @param text - The beginning of a TSV text, as read. 読んだままの TSV の文字の先頭。
13
+ * @returns The rows the rest of the text cannot change, unpadded. 文字の続きが変えられない行 (埋めない)。
14
+ */
15
+ export declare function iterateTsvPrefixRows(text: string): Generator<string[]>;
16
+
1
17
  /**
2
18
  * Yields the rows of a TSV text one at a time, in exactly parseTsv's grammar (the same quoting rule, line terminators
3
19
  * and final-terminator handling; empty text is one row with one empty cell), WITHOUT padding them to a common width. A
package/dist/tsv.d.ts CHANGED
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Yields the rows of a text that may be only the beginning of a TSV (a file read partway, a stream not yet ended), in
3
+ * exactly iterateTsvRows's grammar, but only those the rest of the text cannot change: every row the text ends with a
4
+ * line terminator, up to the first field that opens a quote the text does not close (whether it is one quoted field or a
5
+ * literal one split at its line breaks is the rest's to decide). So the rows of any beginning of a TSV are the first
6
+ * rows of the whole: a reader of a stream can show them as final. Empty text yields no row. Unpadded.
7
+ * TSV の先頭だけかもしれない文字 (途中まで読んだファイル、まだ終わらないストリーム) の行を、iterateTsvRows とちょうど同じ文法で、
8
+ * ただし文字の続きが変えられない行だけ出す処理。文字が改行で終える行すべてを、文字が閉じない引用を開く最初の値まで出す (それが 1 つの
9
+ * 引用の値か、改行で割れる文字どおりの値かは続きが決める)。そのため TSV のどの先頭の行も全体の最初の行で、ストリームの読み手は
10
+ * それを確定したものとして見せられる。空の文字は行を出さない。埋めない。
11
+ *
12
+ * @param text - The beginning of a TSV text, as read. 読んだままの TSV の文字の先頭。
13
+ * @returns The rows the rest of the text cannot change, unpadded. 文字の続きが変えられない行 (埋めない)。
14
+ */
15
+ export declare function iterateTsvPrefixRows(text: string): Generator<string[]>;
16
+
1
17
  /**
2
18
  * Yields the rows of a TSV text one at a time, in exactly parseTsv's grammar (the same quoting rule, line terminators
3
19
  * and final-terminator handling; empty text is one row with one empty cell), WITHOUT padding them to a common width. A
package/dist/tsv.js CHANGED
@@ -1,70 +1,76 @@
1
- const o = (e, t) => (
1
+ const c = (e, t) => (
2
2
  // 終端 CRLF は必須 (Excel 実挙動)。parseTsv の「終端の改行 1 つを剥がす」規約と対 —
3
3
  // 終端なしだと [["a"],[""]] が [["a"]] に潰れ (cut-move で clear されるべき先に旧値が残る)、
4
4
  // [[""]] は "" になって §6.5.4-5 の空ガードに飲まれ「空セル貼り付けで clear」が不発になる
5
5
  `${e.map((n) => n.map(t).join(" ")).join(`\r
6
6
  `)}\r
7
7
  `
8
- ), T = (e) => o(e, a), g = (e) => o(e, f), a = (e) => /[\t\r\n]/.test(e) || e.startsWith('"') ? `"${e.replaceAll('"', '""')}"` : e, f = (e) => /[\t\r\n]/.test(e) ? `"${e.replaceAll('"', '""')}"` : e, c = (e) => e.endsWith(`\r
8
+ ), d = (e) => c(e, p), x = (e) => c(e, w), p = (e) => /[\t\r\n]/.test(e) || e.startsWith('"') ? `"${e.replaceAll('"', '""')}"` : e, w = (e) => /[\t\r\n]/.test(e) ? `"${e.replaceAll('"', '""')}"` : e, f = (e) => e.endsWith(`\r
9
9
  `) ? e.slice(0, -2) : e.endsWith(`
10
10
  `) || e.endsWith("\r") ? e.slice(0, -1) : e, l = /* @__PURE__ */ Symbol("row end");
11
- function* u(e) {
12
- let t = 0;
11
+ function* o(e, t) {
12
+ let n = 0;
13
13
  for (; ; ) {
14
- const n = e[t] === '"' ? p(e, t) : null;
15
- if (n !== null)
16
- yield n.value, t = n.nextIndex;
14
+ const r = e[n] === '"' ? v(e, n) : a;
15
+ if (r === h && t === "prefix") return;
16
+ if (typeof r == "object")
17
+ yield r.value, n = r.nextIndex;
17
18
  else {
18
- const r = v(e, t);
19
- yield e.slice(t, r), t = r;
19
+ const s = m(e, n);
20
+ yield e.slice(n, s), n = s;
20
21
  }
21
- if (t >= e.length) {
22
- yield l;
22
+ if (n >= e.length) {
23
+ t === "whole" && (yield l);
23
24
  return;
24
25
  }
25
- e[t] === " " ? t += 1 : (t += e[t] === "\r" && e[t + 1] === `
26
+ e[n] === " " ? n += 1 : (n += e[n] === "\r" && e[n + 1] === `
26
27
  ` ? 2 : 1, yield l);
27
28
  }
28
29
  }
29
- function* h(e) {
30
- const t = c(e);
30
+ function* T(e) {
31
+ const t = f(e);
31
32
  if (t === "") {
32
33
  yield [""];
33
34
  return;
34
35
  }
35
- let n = [];
36
- for (const r of u(t))
37
- r === l ? (yield n, n = []) : n.push(r);
36
+ yield* u(o(t, "whole"));
37
+ }
38
+ function* A(e) {
39
+ yield* u(o(e, "prefix"));
40
+ }
41
+ function* u(e) {
42
+ let t = [];
43
+ for (const n of e)
44
+ n === l ? (yield t, t = []) : t.push(n);
38
45
  }
39
- const w = (e) => {
40
- const t = Array.from(h(e));
46
+ const E = (e) => {
47
+ const t = Array.from(T(e));
41
48
  let n = 0;
42
49
  for (const r of t)
43
50
  r.length > n && (n = r.length);
44
51
  return t.map((r) => r.length === n ? r : [...r, ...Array(n - r.length).fill("")]);
45
- }, m = (e) => {
46
- const t = c(e);
52
+ }, O = (e) => {
53
+ const t = f(e);
47
54
  if (t === "")
48
55
  return { rows: 1, cols: 1 };
49
56
  let n = 0, r = 0, s = 0;
50
- for (const i of u(t))
57
+ for (const i of o(t, "whole"))
51
58
  i === l ? (n += 1, s > r && (r = s), s = 0) : s += 1;
52
59
  return { rows: n, cols: r };
53
- }, p = (e, t) => {
60
+ }, a = /* @__PURE__ */ Symbol("literal field"), h = /* @__PURE__ */ Symbol("open quote"), v = (e, t) => {
54
61
  let n = "", r = t + 1;
55
- for (; r < e.length; )
56
- if (e[r] === '"')
57
- if (e[r + 1] === '"')
58
- n += '"', r += 2;
59
- else {
60
- const s = r + 1;
61
- return (s >= e.length || e[s] === " " || e[s] === `
62
- ` || e[s] === "\r") && /[\t\r\n"]/.test(n) ? { value: n, nextIndex: s } : null;
63
- }
64
- else
65
- n += e[r], r += 1;
66
- return null;
67
- }, v = (e, t) => {
62
+ for (; ; ) {
63
+ const s = e.indexOf('"', r);
64
+ if (s < 0) return h;
65
+ if (n += e.slice(r, s), e[s + 1] === '"') {
66
+ n += '"', r = s + 2;
67
+ continue;
68
+ }
69
+ const i = s + 1;
70
+ return (i >= e.length || e[i] === " " || e[i] === `
71
+ ` || e[i] === "\r") && /[\t\r\n"]/.test(n) ? { value: n, nextIndex: i } : a;
72
+ }
73
+ }, m = (e, t) => {
68
74
  for (let n = t; n < e.length; n += 1)
69
75
  if (e[n] === " " || e[n] === `
70
76
  ` || e[n] === "\r")
@@ -72,9 +78,10 @@ const w = (e) => {
72
78
  return e.length;
73
79
  };
74
80
  export {
75
- h as iterateTsvRows,
76
- m as measureTsv,
77
- w as parseTsv,
78
- g as serializeClipboardTsv,
79
- T as serializeTsv
81
+ A as iterateTsvPrefixRows,
82
+ T as iterateTsvRows,
83
+ O as measureTsv,
84
+ E as parseTsv,
85
+ x as serializeClipboardTsv,
86
+ d as serializeTsv
80
87
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiquants/spreadsheet",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Excel-grade spreadsheet grid for React: multi-cell selection, TSV/HTML clipboard, in-place editing on top of @aiquants/virtualscroll",
5
5
  "sideEffects": [
6
6
  "**/*.css"
@@ -56,12 +56,12 @@
56
56
  "LICENSE.md"
57
57
  ],
58
58
  "peerDependencies": {
59
- "@aiquants/virtualscroll": "^3.22.0",
59
+ "@aiquants/virtualscroll": "^3.22.1",
60
60
  "react": "^19.2.7",
61
61
  "react-dom": "^19.2.7"
62
62
  },
63
63
  "devDependencies": {
64
- "@aiquants/virtualscroll": "3.22.0",
64
+ "@aiquants/virtualscroll": "3.22.1",
65
65
  "@playwright/test": "^1.59.1",
66
66
  "@tailwindcss/cli": "^4.1.18",
67
67
  "@testing-library/jest-dom": "^6.9.1",
@@ -14,10 +14,12 @@
14
14
  * before: a cell Excel emitted untouched such as `"hi"` (content without any of the four) stays
15
15
  * `"hi"`. The one raw shape that changed reading is a cell that looks exactly like a quoted field
16
16
  * holding a doubled quote (a literal `"a""b"` now reads `a"b`), which no Excel cell produces
17
- * unquoted. Both readers share ONE grammar walk (`scanTsv`): `parseTsv` materialises the matrix,
17
+ * unquoted. Every reader shares ONE grammar walk (`scanTsv`): `parseTsv` materialises the matrix,
18
18
  * `measureTsv` only counts rows × columns so the ADR-12 paste gate can refuse an over-cap
19
19
  * clipboard BEFORE any row array exists (a newline-only clipboard costs ~hundreds of bytes of
20
- * matrix per input byte when materialised).
20
+ * matrix per input byte when materialised), and `iterateTsvPrefixRows` reads a text that may be
21
+ * only a TSV's beginning, stopping where the rest could change a row: at a row without its line
22
+ * terminator, and at a quote the text does not close, which the whole text reads as a literal.
21
23
  *
22
24
  * クリップボード TSV の直列化 / 解析 (§6.5.1)。文字列の少なくとも 1 × 1 のどの矩形行列でも往復が厳密
23
25
  * (`parseTsv(serializeTsv(m))` が `m` と深く等しい)。`serializeClipboardTsv` はクリップボードの text/plain の
@@ -31,9 +33,11 @@
31
33
  * それ以外は文字どおりの値なので、Excel の素の出力は従来どおりに読める (Excel が囲まずに出した
32
34
  * `"hi"` は中身に 4 つのどれも無いため `"hi"` のまま)。読みが変わった素の形は、二重化した引用符を
33
35
  * 持つ引用のフィールドとそっくりな値 (文字どおりの `"a""b"` は `a"b` と読む) だけで、Excel が囲まずに
34
- * 出すセルには無い。読み手 2 つは唯一の文法走査 (`scanTsv`) を共有: `parseTsv` は行列を実体化し、
36
+ * 出すセルには無い。全ての読み手は唯一の文法走査 (`scanTsv`) を共有: `parseTsv` は行列を実体化し、
35
37
  * `measureTsv` は行 × 列だけを数える — ADR-12 の paste ゲートが上限超のクリップボードを行配列の
36
38
  * 実体化**前**に拒否できる (改行だけのクリップボードは実体化で入力 1 byte あたり数百 byte の行列費用になる)。
39
+ * `iterateTsvPrefixRows` は TSV の先頭だけかもしれない文字を読み、続きが行を変えうる所で止まる (改行の無い行と、
40
+ * 文字が閉じない引用。文字全体ではこれを literal と読む)。
37
41
  */
38
42
 
39
43
  /**
@@ -109,23 +113,37 @@ const stripTrailingTerminator = (text: string): string => (text.endsWith("\r\n")
109
113
  const ROW_END: unique symbol = Symbol("row end")
110
114
 
111
115
  /**
112
- * The ONE grammar walk every reader shares (parseTsv through iterateTsvRows, and measureTsv): yields each field of
113
- * `body` in order and ROW_END after the last field of every row (the last row included), adopting the quoted reading only
114
- * under the two-part condition of tryReadQuotedField. Lazy: nothing past the token a consumer takes is read. `body` must
115
- * be terminator-stripped and non-empty.
116
- * 全ての読み手 (iterateTsvRows を通る parseTsv と measureTsv) が共有する唯一の文法走査: `body` の値を順に出し、各行の最後の
117
- * 値の後に ROW_END を出す (最終行も)。quoted 解釈の採否は tryReadQuotedField の 2 条件だけ。遅延で、受け手が取った印より先は
118
- * 読まない。`body` は終端剥がし済み・非空であること。
116
+ * What a walk reads: `whole`, a whole TSV text, terminator-stripped and non-empty; `prefix`, a text that may be only
117
+ * the beginning of one (a file read partway), whose readings end at the first one the rest could change.
118
+ * 走査が読むもの。`whole` は TSV の文字全体 (終端を剥がした非空の文字)、`prefix` は TSV の先頭だけかもしれない文字 (途中まで読んだ
119
+ * ファイル) で、続きが変えうる最初の読みで終わる。
120
+ */
121
+ type TsvExtent = "whole" | "prefix"
122
+
123
+ /**
124
+ * The ONE grammar walk every reader shares (parseTsv through iterateTsvRows, measureTsv, and iterateTsvPrefixRows): yields
125
+ * each field of `body` in order and ROW_END after the last field of every row, adopting the quoted reading only under the
126
+ * two-part condition of readQuotedField. Of a `whole` text the last row ends with the text; of a `prefix` the walk stops
127
+ * at the first field whose quote the text does not close (the rest decides whether it is one quoted field or a literal
128
+ * one), and a row the text does not end with a line terminator gets no ROW_END, as the rest may continue it. Lazy:
129
+ * nothing past the token a consumer takes is read.
130
+ * 全ての読み手 (iterateTsvRows を通る parseTsv・measureTsv・iterateTsvPrefixRows) が共有する唯一の文法走査: `body` の値を順に
131
+ * 出し、各行の最後の値の後に ROW_END を出す。quoted 解釈の採否は readQuotedField の 2 条件だけ。`whole` の文字では最後の行が
132
+ * 文字とともに終わり、`prefix` では文字が閉じない引用の値で走査を止め (それが 1 つの引用の値か文字どおりの値かは続きが決める)、
133
+ * 文字が改行で終えない行には ROW_END を出さない (続きがその行を延ばしうる)。遅延で、受け手が取った印より先は読まない。
119
134
  *
120
- * @param body - The terminator-stripped TSV text. 終端を剥がした TSV の文字列。
135
+ * @param body - The TSV text: terminator-stripped and non-empty for `whole`, as read for `prefix`. TSV の文字列 (`whole` は終端を剥がした非空の文字、`prefix` は読んだまま)。
136
+ * @param extent - Whether `body` is the whole text or only its beginning. `body` が文字全体か、その先頭だけか。
121
137
  * @returns The field / row-end tokens. 値と行末の印。
122
138
  */
123
- function* scanTsv(body: string): Generator<string | typeof ROW_END> {
139
+ function* scanTsv(body: string, extent: TsvExtent): Generator<string | typeof ROW_END> {
124
140
  let i = 0
125
141
  while (true) {
126
- // quoted 解釈の採否は tryReadQuotedField の 2 条件だけが決める (不成立は literal — Excel が囲まずに出した `"hi"` を壊さない)
127
- const quoted = body[i] === '"' ? tryReadQuotedField(body, i) : null
128
- if (quoted !== null) {
142
+ // quoted 解釈の採否は readQuotedField の 2 条件だけが決める (不成立は literal — Excel が囲まずに出した `"hi"` を壊さない)
143
+ const quoted = body[i] === '"' ? readQuotedField(body, i) : LITERAL_FIELD
144
+ // 閉じない引用は、続きが閉じれば 1 つの値、閉じなければ改行で割れる literal。先頭だけの文字ではまだ決まらない
145
+ if (quoted === OPEN_QUOTE && extent === "prefix") return
146
+ if (typeof quoted === "object") {
129
147
  yield quoted.value
130
148
  i = quoted.nextIndex
131
149
  } else {
@@ -134,7 +152,7 @@ function* scanTsv(body: string): Generator<string | typeof ROW_END> {
134
152
  i = end
135
153
  }
136
154
  if (i >= body.length) {
137
- yield ROW_END
155
+ if (extent === "whole") yield ROW_END
138
156
  return
139
157
  }
140
158
  if (body[i] === "\t") {
@@ -165,8 +183,37 @@ export function* iterateTsvRows(text: string): Generator<string[]> {
165
183
  yield [""]
166
184
  return
167
185
  }
186
+ yield* rowsOf(scanTsv(body, "whole"))
187
+ }
188
+
189
+ /**
190
+ * Yields the rows of a text that may be only the beginning of a TSV (a file read partway, a stream not yet ended), in
191
+ * exactly iterateTsvRows's grammar, but only those the rest of the text cannot change: every row the text ends with a
192
+ * line terminator, up to the first field that opens a quote the text does not close (whether it is one quoted field or a
193
+ * literal one split at its line breaks is the rest's to decide). So the rows of any beginning of a TSV are the first
194
+ * rows of the whole: a reader of a stream can show them as final. Empty text yields no row. Unpadded.
195
+ * TSV の先頭だけかもしれない文字 (途中まで読んだファイル、まだ終わらないストリーム) の行を、iterateTsvRows とちょうど同じ文法で、
196
+ * ただし文字の続きが変えられない行だけ出す処理。文字が改行で終える行すべてを、文字が閉じない引用を開く最初の値まで出す (それが 1 つの
197
+ * 引用の値か、改行で割れる文字どおりの値かは続きが決める)。そのため TSV のどの先頭の行も全体の最初の行で、ストリームの読み手は
198
+ * それを確定したものとして見せられる。空の文字は行を出さない。埋めない。
199
+ *
200
+ * @param text - The beginning of a TSV text, as read. 読んだままの TSV の文字の先頭。
201
+ * @returns The rows the rest of the text cannot change, unpadded. 文字の続きが変えられない行 (埋めない)。
202
+ */
203
+ export function* iterateTsvPrefixRows(text: string): Generator<string[]> {
204
+ yield* rowsOf(scanTsv(text, "prefix"))
205
+ }
206
+
207
+ /**
208
+ * Groups a walk's tokens into rows; fields after the last ROW_END (a row the walk did not end) are dropped.
209
+ * 走査の印を行にまとめる処理 (最後の ROW_END の後の値 — 走査が終えなかった行 — は捨てる)。
210
+ *
211
+ * @param tokens - The walk's tokens. 走査の印。
212
+ * @returns The rows. 行。
213
+ */
214
+ function* rowsOf(tokens: Iterable<string | typeof ROW_END>): Generator<string[]> {
168
215
  let row: string[] = []
169
- for (const token of scanTsv(body)) {
216
+ for (const token of tokens) {
170
217
  if (token === ROW_END) {
171
218
  yield row
172
219
  row = []
@@ -219,7 +266,7 @@ export const measureTsv = (text: string): { rows: number; cols: number } => {
219
266
  let rows = 0
220
267
  let cols = 0
221
268
  let current = 0
222
- for (const token of scanTsv(body)) {
269
+ for (const token of scanTsv(body, "whole")) {
223
270
  if (token === ROW_END) {
224
271
  rows += 1
225
272
  if (current > cols) {
@@ -233,41 +280,46 @@ export const measureTsv = (text: string): { rows: number; cols: number } => {
233
280
  return { rows, cols }
234
281
  }
235
282
 
283
+ /** readQuotedField's answer for a field read as a literal. readQuotedField が文字どおりに読む値に返す答え。 */
284
+ const LITERAL_FIELD: unique symbol = Symbol("literal field")
285
+
236
286
  /**
237
- * Reads the quoted field that starts at `start`, adopted ONLY when it closes properly and its
238
- * content holds a tab, CR, LF or `"` (exactly the fields `serializeTsv` quotes); otherwise null,
239
- * and the caller reads a literal.
240
- * 位置 `start` から始まる引用のフィールドを読む処理。正しく閉じ、中身がタブ・CR・LF・`"` のどれかを含む
241
- * とき (`serializeTsv` が囲む値とちょうど同じ) だけ採用し、そうでなければ null (呼び手は文字どおりに読む)。
287
+ * readQuotedField's answer for a quote the text does not close: a literal of a whole text, undecided in a prefix.
288
+ * readQuotedField が、文字の閉じない引用に返す答え (文字全体では literal、先頭だけの文字ではまだ決まらない)。
289
+ */
290
+ const OPEN_QUOTE: unique symbol = Symbol("open quote")
291
+
292
+ /**
293
+ * Reads the quoted field that starts at `start`, adopted ONLY when it closes properly and its content holds a tab, CR,
294
+ * LF or `"` (exactly the fields `serializeTsv` quotes); a quote that closes otherwise is a literal (LITERAL_FIELD), and a
295
+ * quote the text never closes is OPEN_QUOTE, which a whole text reads as a literal too. The content is gathered a run
296
+ * between quotes at a time.
297
+ * 位置 `start` から始まる引用のフィールドを読む処理。正しく閉じ、中身がタブ・CR・LF・`"` のどれかを含むとき (`serializeTsv` が
298
+ * 囲む値とちょうど同じ) だけ採用し、ほかの閉じ方は文字どおり (LITERAL_FIELD)、文字が閉じない引用は OPEN_QUOTE (文字全体では
299
+ * これも文字どおりに読む)。中身は引用符の間の区間ごとにまとめて集める。
242
300
  *
243
- * @param body - The terminator-stripped TSV text. 終端を剥がした TSV の文字列。
301
+ * @param body - The TSV text. TSV の文字列。
244
302
  * @param start - The index of the opening quote. 開きの引用符の位置。
245
- * @returns The unescaped value and the index after the closing quote, or null. 剥がした値と閉じの直後の位置、または null。
303
+ * @returns The unescaped value and the index after the closing quote, LITERAL_FIELD, or OPEN_QUOTE. 剥がした値と閉じの直後の位置、LITERAL_FIELD、または OPEN_QUOTE。
246
304
  */
247
- const tryReadQuotedField = (body: string, start: number): { value: string; nextIndex: number } | null => {
305
+ const readQuotedField = (body: string, start: number): { value: string; nextIndex: number } | typeof LITERAL_FIELD | typeof OPEN_QUOTE => {
248
306
  let value = ""
249
307
  let i = start + 1
250
- while (i < body.length) {
251
- if (body[i] === '"') {
252
- if (body[i + 1] === '"') {
253
- value += '"'
254
- i += 2
255
- } else {
256
- const next = i + 1
257
- // 成立条件は 2 つとも必要: (a) 閉じ引用の直後が区切りか終端、(b) 中身がタブ・改行・`"` を含む
258
- // (書き手が囲む値の集合と同じ)。(b) が無いと、Excel が囲まずに出した literal `"hi"` を剥がしてしまう
259
- const isBoundary = next >= body.length || body[next] === "\t" || body[next] === "\n" || body[next] === "\r"
260
- if (isBoundary && /[\t\r\n"]/.test(value)) {
261
- return { value, nextIndex: next }
262
- }
263
- return null
264
- }
265
- } else {
266
- value += body[i]
267
- i += 1
308
+ while (true) {
309
+ const quote = body.indexOf('"', i)
310
+ if (quote < 0) return OPEN_QUOTE
311
+ value += body.slice(i, quote)
312
+ if (body[quote + 1] === '"') {
313
+ value += '"'
314
+ i = quote + 2
315
+ continue
268
316
  }
317
+ const next = quote + 1
318
+ // 成立条件は 2 つとも必要: (a) 閉じ引用の直後が区切りか終端、(b) 中身がタブ・改行・`"` を含む
319
+ // (書き手が囲む値の集合と同じ)。(b) が無いと、Excel が囲まずに出した literal `"hi"` を剥がしてしまう
320
+ const isBoundary = next >= body.length || body[next] === "\t" || body[next] === "\n" || body[next] === "\r"
321
+ return isBoundary && /[\t\r\n"]/.test(value) ? { value, nextIndex: next } : LITERAL_FIELD
269
322
  }
270
- return null // 閉じ引用が無い → literal 扱い
271
323
  }
272
324
 
273
325
  /** Next tab/newline position or end. / 次の区切り位置 (無ければ終端)。 */
package/src/index.ts CHANGED
@@ -46,7 +46,7 @@ export { type HtmlTableKind, type HtmlTableResult, parseHtmlTable, serializeHtml
46
46
  export type { NumberFormatDecodeOptions } from "./clipboard/numberFormatCodec.ts"
47
47
  export type { PasteMode, PasteOperation, PasteOptions } from "./clipboard/pasteSpecial.ts"
48
48
  export { estimatePasteCellCount, expandPaste } from "./clipboard/tiling.ts"
49
- export { iterateTsvRows, measureTsv, parseTsv, serializeClipboardTsv, serializeTsv } from "./clipboard/tsv.ts"
49
+ export { iterateTsvPrefixRows, iterateTsvRows, measureTsv, parseTsv, serializeClipboardTsv, serializeTsv } from "./clipboard/tsv.ts"
50
50
  export type { ClipInfo, PasteOutcome } from "./clipboard/useClipboard.ts"
51
51
  export type { BorderPreset } from "./commands/useCommands.ts"
52
52
  // renderCell の ctx 型 — prop が公開面にある以上、その引数型も名指しできなければならない
package/src/tsv.ts CHANGED
@@ -4,7 +4,8 @@
4
4
  * `parseTsv(serializeTsv(m))` equals `m` for every rectangular matrix of strings of at least 1 × 1) and the dimension scan `measureTsv`
5
5
  * (the rows × columns `parseTsv` would build, without building them), React-free, so a host reads, writes or sizes a
6
6
  * grid's text before the grid itself is loaded — a server-rendered placeholder table, a lazily imported grid. Published
7
- * as `@aiquants/spreadsheet/tsv`, which loads none of the grid; the barrel exports the same five functions (`iterateTsvRows` yields the rows lazily, unpadded). The clipboard
7
+ * as `@aiquants/spreadsheet/tsv`, which loads none of the grid; the barrel exports the same six functions (`iterateTsvRows` yields the rows lazily, unpadded, and
8
+ * `iterateTsvPrefixRows` the rows of a text that may be only a TSV's beginning, those the rest cannot change). The clipboard
8
9
  * writer `serializeClipboardTsv` (Excel's form) is for text/plain on the clipboard; files and stored bodies use the strict
9
10
  * `serializeTsv`.
10
11
  *
@@ -12,7 +13,8 @@
12
13
  * タブか改行を含むか `"` で始まる値を引用符で囲むので、文字列の少なくとも 1 × 1 のどの矩形行列 `m` でも `parseTsv(serializeTsv(m))` が `m` に
13
14
  * 等しい) と寸法の走査 `measureTsv` (`parseTsv` が組む行 × 列を、組まずに返す) を React を読まずに渡すモジュール。ホストが
14
15
  * グリッドを読み込む前に (サーバーで描く代わりの表、遅延で読み込むグリッド) グリッドの文字を読み書きし寸法を測れる。グリッドを
15
- * 読み込まない `@aiquants/spreadsheet/tsv` として公開し、barrel も同じ 5 つの関数を公開する (`iterateTsvRows` は行を遅延で、埋めずに出す)。クリップボードの text/plain は
16
+ * 読み込まない `@aiquants/spreadsheet/tsv` として公開し、barrel も同じ 6 つの関数を公開する (`iterateTsvRows` は行を遅延で、埋めずに出し、
17
+ * `iterateTsvPrefixRows` は TSV の先頭だけかもしれない文字の行のうち、続きが変えられない行を出す)。クリップボードの text/plain は
16
18
  * Excel の形の `serializeClipboardTsv`、ファイルと保存する本文は厳密な `serializeTsv` を使う。
17
19
  */
18
- export { iterateTsvRows, measureTsv, parseTsv, serializeClipboardTsv, serializeTsv } from "./clipboard/tsv.ts"
20
+ export { iterateTsvPrefixRows, iterateTsvRows, measureTsv, parseTsv, serializeClipboardTsv, serializeTsv } from "./clipboard/tsv.ts"