@aiquants/spreadsheet 0.15.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/src/tsv.ts CHANGED
@@ -1,13 +1,20 @@
1
1
  /**
2
- * @fileoverview The TSV codec the grid copies and pastes with (Excel's real behaviour: one trailing line terminator
3
- * dropped, empty text read as one empty cell, ragged rows padded with "", only fields holding a tab or a line break
4
- * quoted), React-free, so a host reads or writes a grid's text before the grid itself is loaded — a
5
- * server-rendered placeholder table, a lazily imported grid. Published as `@aiquants/spreadsheet/tsv`, which loads
6
- * none of the grid; the barrel exports the same two functions.
2
+ * @fileoverview The TSV codec the grid copies and pastes with (one trailing line terminator dropped, empty text read as
3
+ * one empty cell, ragged rows padded with "", a field quoted when it holds a tab or a line break or starts with `"`, so
4
+ * `parseTsv(serializeTsv(m))` equals `m` for every rectangular matrix of strings of at least 1 × 1) and the dimension scan `measureTsv`
5
+ * (the rows × columns `parseTsv` would build, without building them), React-free, so a host reads, writes or sizes a
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 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
9
+ * writer `serializeClipboardTsv` (Excel's form) is for text/plain on the clipboard; files and stored bodies use the strict
10
+ * `serializeTsv`.
7
11
  *
8
- * グリッドがコピーと貼り付けに使う TSV の符号化 (Excel の実際の振る舞い: 末尾の改行 1 つを除く・空の文字は空のセル 1 つ・
9
- * 不揃いの行は "" で埋める・引用符で囲むのはタブか改行を含む値だけ) を React を読まずに渡すモジュール。ホストがグリッドを
10
- * 読み込む前に (サーバーで描く代わりの表、遅延で読み込むグリッド) グリッドの文字を読み書きできる。グリッドを読み込まない
11
- * `@aiquants/spreadsheet/tsv` として公開し、barrel も同じ 2 つの関数を公開する。
12
+ * グリッドがコピーと貼り付けに使う TSV の符号化 (末尾の改行 1 つを除く・空の文字は空のセル 1 つ・不揃いの行は "" で埋める・
13
+ * タブか改行を含むか `"` で始まる値を引用符で囲むので、文字列の少なくとも 1 × 1 のどの矩形行列 `m` でも `parseTsv(serializeTsv(m))` が `m` に
14
+ * 等しい) と寸法の走査 `measureTsv` (`parseTsv` が組む行 × 列を、組まずに返す) を React を読まずに渡すモジュール。ホストが
15
+ * グリッドを読み込む前に (サーバーで描く代わりの表、遅延で読み込むグリッド) グリッドの文字を読み書きし寸法を測れる。グリッドを
16
+ * 読み込まない `@aiquants/spreadsheet/tsv` として公開し、barrel も同じ 6 つの関数を公開する (`iterateTsvRows` は行を遅延で、埋めずに出し、
17
+ * `iterateTsvPrefixRows` は TSV の先頭だけかもしれない文字の行のうち、続きが変えられない行を出す)。クリップボードの text/plain は
18
+ * Excel の形の `serializeClipboardTsv`、ファイルと保存する本文は厳密な `serializeTsv` を使う。
12
19
  */
13
- export { parseTsv, serializeTsv } from "./clipboard/tsv.ts"
20
+ export { iterateTsvPrefixRows, iterateTsvRows, measureTsv, parseTsv, serializeClipboardTsv, serializeTsv } from "./clipboard/tsv.ts"
package/src/view.ts CHANGED
@@ -1,13 +1,18 @@
1
1
  /**
2
- * @fileoverview The grid's fixed view geometry and its zoom rules, React-free, so a host can size a box for the grid and
3
- * drive the zoom from its own controls with exactly the numbers the grid itself uses. Published as
4
- * `@aiquants/spreadsheet/view`, which loads none of the grid.
2
+ * @fileoverview The grid's fixed view geometry, its defaults and its zoom ladder, React-free, so a host can size a box
3
+ * for the grid, paint a static stand-in of its first screen (column letters through `columnIndexToName`) and drive the
4
+ * zoom from its own controls with exactly the numbers the grid itself uses (each value is defined once here and the
5
+ * grid reads it from here). Published as `@aiquants/spreadsheet/view`, which loads none of the grid: its only module
6
+ * dependency is the column-name codec, which imports nothing.
5
7
  *
6
- * グリッドの固定の寸法と拡大率の規則 (React を読まない)。ホストがグリッドの箱の大きさを決め、自前の操作部品から拡大率を
7
- * 動かすときに、グリッド自身と同じ数を使えるようにするモジュール。グリッドを読み込まない `@aiquants/spreadsheet/view` として
8
- * 公開する。
8
+ * グリッドの固定の寸法・既定値・拡大率の段 (React を読まない)。ホストがグリッドの箱の大きさを決め、最初の画面の静的な代わり
9
+ * (列名は `columnIndexToName`) を描き、自前の操作部品から拡大率を動かすときに、グリッド自身と同じ数を使えるようにする
10
+ * モジュール (値はどれもここで 1 度だけ定義し、グリッドもここから読む)。グリッドを読み込まない `@aiquants/spreadsheet/view` と
11
+ * して公開する。読むモジュールは何も import しない列名の符号化だけ。
9
12
  */
10
13
 
14
+ export { columnIndexToName } from "./header/columnName.ts"
15
+
11
16
  /** The height of the column-header band in CSS px (`--aqsp-col-header-height` in the stylesheet). 列見出しの帯の高さ (CSS px。スタイルシートの `--aqsp-col-header-height`)。 */
12
17
  export const SPREADSHEET_COLUMN_HEADER_HEIGHT_PX = 24
13
18
 
@@ -21,6 +26,16 @@ export const SPREADSHEET_ROW_HEADER_WIDTH_PX = 64
21
26
  */
22
27
  export const SPREADSHEET_SCROLLBAR_SIZE_PX = 12
23
28
 
29
+ /** The default column width in CSS px, the width of every column the host does not size. 既定の列幅 (CSS px。ホストが寸法を決めない列の幅)。 */
30
+ export const SPREADSHEET_DEFAULT_COLUMN_WIDTH_PX = 100
31
+
32
+ /**
33
+ * The default of `maxOperationCells` (ADR-12): the largest number of cells one operation (paste, fill, clear, format)
34
+ * may touch before the grid refuses it.
35
+ * `maxOperationCells` の既定値 (ADR-12)。1 つの操作 (貼り付け・フィル・消去・書式) が触れてよいセル数の上限で、超えるとグリッドが拒む。
36
+ */
37
+ export const SPREADSHEET_MAX_OPERATION_CELLS = 100_000
38
+
24
39
  /** The smallest package zoom. パッケージの拡大率の下限。 */
25
40
  export const SPREADSHEET_ZOOM_MIN = 0.25
26
41
 
@@ -48,15 +63,53 @@ export const clampSpreadsheetZoom = (value: number): number => {
48
63
  }
49
64
 
50
65
  /**
51
- * The 0.1-grid step of the zoom gestures (Alt+Wheel and Ctrl+Alt+= / -), unclamped and free of float drift. From an
52
- * off-grid value (the 0.25 bound or a host's 1.25) the step lands on the nearest detent in the travel direction (0.25
53
- * goes to 0.3 up and 0.2 down); rounding would skip a detent (0.25 to 0.4). Clamp the result with clampSpreadsheetZoom.
54
- * 拡大縮小の操作 (Alt+ホイールと Ctrl+Alt+= / -) の 0.1 刻み (範囲で切らず、浮動小数のずれを持ち込まない)。刻みの外の値
55
- * (下限の 0.25 やホストの 1.25) からは、進む向きの最寄りの刻みへ着く (0.25 から上は 0.3、下は 0.2)。丸めると刻みを飛ばす
56
- * (0.25 から 0.4) ため使わない。結果は clampSpreadsheetZoom で切る。
66
+ * The ratio between neighbouring steps of the zoom ladder: four steps double the zoom.
67
+ * 拡大率の段の隣どうしの比。4 段で拡大率が 2 倍になる。
68
+ */
69
+ const ZOOM_LADDER_RATIO = 2 ** (1 / 4)
70
+
71
+ /**
72
+ * The grid every ladder step is rounded to: hundredths, so each step reads as a whole percentage.
73
+ * 段の値を丸める格子。100 分の 1 で、どの段も整数の百分率として読める。
74
+ */
75
+ const ZOOM_LADDER_GRID = 100
76
+
77
+ /**
78
+ * The zoom ladder of the gestures (Alt+Wheel and Ctrl+Alt+= / -): the geometric series `2^(k/4)` for every integer `k`
79
+ * from −8 to 8, rounded to hundredths, so it runs from SPREADSHEET_ZOOM_MIN (0.25) through 1 to SPREADSHEET_ZOOM_MAX (4)
80
+ * in steps of about 19 % and every power of two is a step.
81
+ * 拡大縮小の操作 (Alt+ホイールと Ctrl+Alt+= / -) の段。−8 から 8 までの整数 `k` の等比数列 `2^(k/4)` を 100 分の 1 に丸めた
82
+ * もので、SPREADSHEET_ZOOM_MIN (0.25) から 1 を通って SPREADSHEET_ZOOM_MAX (4) まで約 19 % ずつ進み、2 の累乗はどれも段に乗る。
83
+ */
84
+ export const SPREADSHEET_ZOOM_STEPS: readonly number[] = Object.freeze(Array.from({ length: 17 }, (_, index) => Math.round(ZOOM_LADDER_RATIO ** (index - 8) * ZOOM_LADDER_GRID) / ZOOM_LADDER_GRID))
85
+
86
+ /**
87
+ * The tolerance under which a zoom counts as standing on a ladder step, so a value that carries float drift (0.59 + 1e-12)
88
+ * moves to the next step instead of the one it already shows.
89
+ * 拡大率が段の上にあるとみなす許容差。浮動小数のずれを持つ値 (0.59 + 1e-12) が、すでに表している段でなく次の段へ動くための幅。
90
+ */
91
+ const ZOOM_STEP_TOLERANCE = 1e-9
92
+
93
+ /**
94
+ * The next step of SPREADSHEET_ZOOM_STEPS in the travel direction, from any finite value, including a value off the
95
+ * ladder (a host's 1.25 goes to 1.41 up and 1.19 down). The input is clamped first, as clampSpreadsheetZoom does, and
96
+ * the step is taken from the clamped value: 9 zooms out to 3.36 and 0.01 zooms in to 0.3, and at a bound the result stays
97
+ * at that bound (in from SPREADSHEET_ZOOM_MAX, out from SPREADSHEET_ZOOM_MIN). A non-finite value fails fast.
98
+ * 進む向きの SPREADSHEET_ZOOM_STEPS の次の段を返す処理。段の外の値 (ホストの 1.25 は上で 1.41、下で 1.19) を含む、どの有限の
99
+ * 値からでも求める。入力を先に clampSpreadsheetZoom と同じく範囲へ切り、切った値から一歩進む (9 からの縮小は 3.36、0.01 からの
100
+ * 拡大は 0.3)。端では端に留まる (SPREADSHEET_ZOOM_MAX からの拡大、SPREADSHEET_ZOOM_MIN からの縮小)。有限でない値はすぐに投げる。
57
101
  *
58
102
  * @param value - The current zoom. 今の拡大率。
59
103
  * @param direction - 1 to zoom in, -1 to zoom out. 拡大は 1、縮小は -1。
60
- * @returns The next detent in that direction. その向きの次の刻み。
104
+ * @returns The next ladder step in that direction from the clamped input. 範囲へ切った入力からのその向きの次の段。
105
+ * @throws {RangeError} When the value is not finite. 有限でない場合。
61
106
  */
62
- export const stepSpreadsheetZoom = (value: number, direction: 1 | -1): number => (direction > 0 ? (Math.floor(value * 10 + 1e-6) + 1) / 10 : (Math.ceil(value * 10 - 1e-6) - 1) / 10)
107
+ export const stepSpreadsheetZoom = (value: number, direction: 1 | -1): number => {
108
+ const current = clampSpreadsheetZoom(value)
109
+ if (direction > 0) {
110
+ const next = SPREADSHEET_ZOOM_STEPS.find((step) => step > current + ZOOM_STEP_TOLERANCE)
111
+ return next === undefined ? SPREADSHEET_ZOOM_MAX : next
112
+ }
113
+ const previous = SPREADSHEET_ZOOM_STEPS.findLast((step) => step < current - ZOOM_STEP_TOLERANCE)
114
+ return previous === undefined ? SPREADSHEET_ZOOM_MIN : previous
115
+ }
@@ -1,91 +0,0 @@
1
- const o = (e) => (
2
- // 終端 CRLF は必須 (Excel 実挙動)。parseTsv の「終端の改行 1 つを剥がす」規約と対 —
3
- // 終端なしだと [["a"],[""]] が [["a"]] に潰れ (cut-move で clear されるべき先に旧値が残る)、
4
- // [[""]] は "" になって §6.5.4-5 の空ガードに飲まれ「空セル貼り付けで clear」が不発になる
5
- `${e.map((l) => l.map(h).join(" ")).join(`\r
6
- `)}\r
7
- `
8
- ), h = (e) => /[\t\r\n]/.test(e) ? `"${e.replaceAll('"', '""')}"` : e, u = (e) => e.endsWith(`\r
9
- `) ? e.slice(0, -2) : e.endsWith(`
10
- `) || e.endsWith("\r") ? e.slice(0, -1) : e, a = (e, l, t) => {
11
- let n = 0;
12
- for (; ; ) {
13
- let s;
14
- if (e[n] === '"') {
15
- const r = f(e, n);
16
- if (r !== null)
17
- s = r.value, n = r.nextIndex;
18
- else {
19
- const i = c(e, n);
20
- s = e.slice(n, i), n = i;
21
- }
22
- } else {
23
- const r = c(e, n);
24
- s = e.slice(n, r), n = r;
25
- }
26
- if (l(s), n >= e.length) {
27
- t();
28
- return;
29
- }
30
- e[n] === " " ? n += 1 : (n += e[n] === "\r" && e[n + 1] === `
31
- ` ? 2 : 1, t());
32
- }
33
- }, p = (e) => {
34
- const l = u(e);
35
- if (l === "")
36
- return [[""]];
37
- const t = [];
38
- let n = [];
39
- a(
40
- l,
41
- (r) => {
42
- n.push(r);
43
- },
44
- () => {
45
- t.push(n), n = [];
46
- }
47
- );
48
- let s = 0;
49
- for (const r of t)
50
- r.length > s && (s = r.length);
51
- return t.map((r) => r.length === s ? r : [...r, ...Array(s - r.length).fill("")]);
52
- }, g = (e) => {
53
- const l = u(e);
54
- if (l === "")
55
- return { rows: 1, cols: 1 };
56
- let t = 0, n = 0, s = 0;
57
- return a(
58
- l,
59
- () => {
60
- s += 1;
61
- },
62
- () => {
63
- t += 1, s > n && (n = s), s = 0;
64
- }
65
- ), { rows: t, cols: n };
66
- }, f = (e, l) => {
67
- let t = "", n = l + 1;
68
- for (; n < e.length; )
69
- if (e[n] === '"')
70
- if (e[n + 1] === '"')
71
- t += '"', n += 2;
72
- else {
73
- const s = n + 1;
74
- return (s >= e.length || e[s] === " " || e[s] === `
75
- ` || e[s] === "\r") && /[\t\r\n]/.test(t) ? { value: t, nextIndex: s } : null;
76
- }
77
- else
78
- t += e[n], n += 1;
79
- return null;
80
- }, c = (e, l) => {
81
- for (let t = l; t < e.length; t += 1)
82
- if (e[t] === " " || e[t] === `
83
- ` || e[t] === "\r")
84
- return t;
85
- return e.length;
86
- };
87
- export {
88
- g as m,
89
- p,
90
- o as s
91
- };
@@ -1,8 +0,0 @@
1
- "use strict";const h=e=>`${e.map(l=>l.map(f).join(" ")).join(`\r
2
- `)}\r
3
- `,f=e=>/[\t\r\n]/.test(e)?`"${e.replaceAll('"','""')}"`:e,u=e=>e.endsWith(`\r
4
- `)?e.slice(0,-2):e.endsWith(`
5
- `)||e.endsWith("\r")?e.slice(0,-1):e,a=(e,l,t)=>{let n=0;for(;;){let s;if(e[n]==='"'){const r=p(e,n);if(r!==null)s=r.value,n=r.nextIndex;else{const i=c(e,n);s=e.slice(n,i),n=i}}else{const r=c(e,n);s=e.slice(n,r),n=r}if(l(s),n>=e.length){t();return}e[n]===" "?n+=1:(n+=e[n]==="\r"&&e[n+1]===`
6
- `?2:1,t())}},o=e=>{const l=u(e);if(l==="")return[[""]];const t=[];let n=[];a(l,r=>{n.push(r)},()=>{t.push(n),n=[]});let s=0;for(const r of t)r.length>s&&(s=r.length);return t.map(r=>r.length===s?r:[...r,...Array(s-r.length).fill("")])},g=e=>{const l=u(e);if(l==="")return{rows:1,cols:1};let t=0,n=0,s=0;return a(l,()=>{s+=1},()=>{t+=1,s>n&&(n=s),s=0}),{rows:t,cols:n}},p=(e,l)=>{let t="",n=l+1;for(;n<e.length;)if(e[n]==='"')if(e[n+1]==='"')t+='"',n+=2;else{const s=n+1;return(s>=e.length||e[s]===" "||e[s]===`
7
- `||e[s]==="\r")&&/[\t\r\n]/.test(t)?{value:t,nextIndex:s}:null}else t+=e[n],n+=1;return null},c=(e,l)=>{for(let t=l;t<e.length;t+=1)if(e[t]===" "||e[t]===`
8
- `||e[t]==="\r")return t;return e.length};exports.measureTsv=g;exports.parseTsv=o;exports.serializeTsv=h;