@aiquants/spreadsheet 0.16.0 → 0.17.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.
- package/CHANGELOG.md +84 -1
- package/README.md +55 -17
- package/dist/index.cjs +8 -8
- package/dist/index.d.cts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +4249 -4172
- package/dist/styles/spreadsheet.css +1 -1
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/dist/tsv.cjs +6 -6
- package/dist/tsv.d.cts +16 -0
- package/dist/tsv.d.ts +16 -0
- package/dist/tsv.js +48 -41
- package/package.json +3 -3
- package/src/Spreadsheet.tsx +280 -92
- package/src/clipboard/tsv.ts +97 -45
- package/src/header/useHeaderResize.ts +137 -68
- package/src/index.ts +2 -2
- package/src/styles/spreadsheet.css +20 -0
- package/src/tsv.ts +5 -3
package/src/clipboard/tsv.ts
CHANGED
|
@@ -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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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 解釈の採否は
|
|
127
|
-
const quoted = body[i] === '"' ?
|
|
128
|
-
|
|
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
|
|
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
|
-
*
|
|
238
|
-
*
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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. / 次の区切り位置 (無ければ終端)。 */
|
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
* byte-identical (sign +1, guide `offsetOf + pendingSize`, both writers). Column autofit
|
|
26
26
|
* measures the union `[0, R_eff) ∪ [rowStart, rowEnd] ∪ [rowCount − T_eff, rowCount)` (each
|
|
27
27
|
* filtered by sizeOf > 0 — ADR-21, band counts from a judgment-time `getFrozenSize()` read)
|
|
28
|
-
* and the measured-cell budget gate counts band rows.
|
|
28
|
+
* and the measured-cell budget gate counts band rows. v0.17: autofit measures with the cells' base
|
|
29
|
+
* font, read from the body pane (`fontSourceRef`), so the keyboard twin and the imperative all-rows
|
|
30
|
+
* fit (`autofitColumnsInAllRows` — every row of the named columns, no fan-out, gated at
|
|
31
|
+
* maxOperationCells measured cells) work whether the header bands are shown or not.
|
|
29
32
|
*
|
|
30
33
|
* ヘッダーリサイズ / unhide 機械 (§7 PT-1)。単一フックが両帯を結線 (§7.4 の機械勘定 = 1)。
|
|
31
34
|
* 帯へ wire-once の native capture pointerdown を 1 本 (ストリップはリスナーを持たない —
|
|
@@ -49,7 +52,10 @@
|
|
|
49
52
|
* 即時書込) が予定動く辺 `offsetOf(index) + initialSize − pendingSize` に置く。leading パスは
|
|
50
53
|
* byte 恒等 (符号 +1、ガイド `offsetOf + pendingSize`、両書き手)。列 autofit の測定行集合は
|
|
51
54
|
* 和集合 `[0, R_eff) ∪ [rowStart, rowEnd] ∪ [rowCount − T_eff, rowCount)` (各 sizeOf > 0 —
|
|
52
|
-
* ADR-21。帯件数は判断時 `getFrozenSize()` 読み)、計測セル予算ゲートは帯行を算入する。
|
|
55
|
+
* ADR-21。帯件数は判断時 `getFrozenSize()` 読み)、計測セル予算ゲートは帯行を算入する。v0.17: autofit はセルの
|
|
56
|
+
* 基本の書体を本体ペイン (`fontSourceRef`) から読むので、キーの双子と命令の全部の行の合わせ
|
|
57
|
+
* (`autofitColumnsInAllRows` — 名指した列の全部の行、ファンアウトなし、測るセルは maxOperationCells まで) は
|
|
58
|
+
* 見出しの帯の有無に依らず働く。
|
|
53
59
|
*/
|
|
54
60
|
|
|
55
61
|
import type { VirtualGridHandle } from "@aiquants/virtualscroll"
|
|
@@ -86,6 +92,12 @@ type UseHeaderResizeParams = {
|
|
|
86
92
|
enabled: boolean
|
|
87
93
|
colBandRef: RefObject<HTMLElement | null>
|
|
88
94
|
rowBandRef: RefObject<HTMLElement | null>
|
|
95
|
+
/**
|
|
96
|
+
* The element whose computed font is the cells' base font (the body pane, the cells' parent): autofit measures with it,
|
|
97
|
+
* so it works whether the header bands are shown or not.
|
|
98
|
+
* 計算済みの書体がセルの基本の書体になる要素 (セルの親の本体ペイン)。autofit はこれで測るので、見出しの帯の有無に依らない。
|
|
99
|
+
*/
|
|
100
|
+
fontSourceRef: RefObject<HTMLElement | null>
|
|
89
101
|
colAxis: AxisModel
|
|
90
102
|
rowAxis: AxisModel
|
|
91
103
|
viewStore: ViewStore
|
|
@@ -119,25 +131,25 @@ type UseHeaderResizeParams = {
|
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
/**
|
|
122
|
-
* The canvas `font` shorthand of a painted format over the
|
|
123
|
-
* bold / pt size (× 4/3 px, like cellPaint) / family from the format, the
|
|
124
|
-
* otherwise. Interned per format object (the paint intern keeps one object per distinct
|
|
134
|
+
* The canvas `font` shorthand of a painted format over the cells' base computed style (the body
|
|
135
|
+
* pane's): italic / bold / pt size (× 4/3 px, like cellPaint) / family from the format, the base
|
|
136
|
+
* values otherwise. Interned per format object (the paint intern keeps one object per distinct
|
|
125
137
|
* painted format, so this is one string per distinct font).
|
|
126
|
-
* 塗り書式の canvas `font` 短縮形 (
|
|
127
|
-
* cellPaint と同式) / フォント族を重ねる)。書式 object 毎に intern (塗り intern が書式毎に
|
|
128
|
-
*
|
|
138
|
+
* 塗り書式の canvas `font` 短縮形 (セルの基本の computed style — 本体ペインのもの — の上に italic / bold /
|
|
139
|
+
* pt サイズ (× 4/3 px — cellPaint と同式) / フォント族を重ねる)。書式 object 毎に intern (塗り intern が書式毎に
|
|
140
|
+
* 1 object を保つので、異なりフォント毎に 1 文字列)。
|
|
129
141
|
*/
|
|
130
|
-
const fontKeyOf = (painted: CellFormat, fontKeys: WeakMap<CellFormat, string>,
|
|
142
|
+
const fontKeyOf = (painted: CellFormat, fontKeys: WeakMap<CellFormat, string>, baseStyle: Pick<CSSStyleDeclaration, "fontStyle" | "fontWeight" | "fontSize" | "fontFamily">): string => {
|
|
131
143
|
const cached = fontKeys.get(painted)
|
|
132
144
|
if (cached !== undefined) {
|
|
133
145
|
return cached
|
|
134
146
|
}
|
|
135
147
|
// 真偽値は厳密 true のみ (fail-soft — cellPaint と同じ判定)
|
|
136
|
-
const style = painted.italic === true ? "italic" :
|
|
137
|
-
const weight = painted.bold === true ? "bold" :
|
|
138
|
-
// pt は cellPaint と同じ × 4/3 で px へ (非有限 /
|
|
139
|
-
const size = typeof painted.fontSize === "number" && Number.isFinite(painted.fontSize) && painted.fontSize > 0 ? `${(painted.fontSize * 4) / 3}px` :
|
|
140
|
-
const family = typeof painted.fontFamily === "string" && painted.fontFamily !== "" ? painted.fontFamily :
|
|
148
|
+
const style = painted.italic === true ? "italic" : baseStyle.fontStyle
|
|
149
|
+
const weight = painted.bold === true ? "bold" : baseStyle.fontWeight
|
|
150
|
+
// pt は cellPaint と同じ × 4/3 で px へ (非有限 / 非正は基本の書体の既定のまま)
|
|
151
|
+
const size = typeof painted.fontSize === "number" && Number.isFinite(painted.fontSize) && painted.fontSize > 0 ? `${(painted.fontSize * 4) / 3}px` : baseStyle.fontSize
|
|
152
|
+
const family = typeof painted.fontFamily === "string" && painted.fontFamily !== "" ? painted.fontFamily : baseStyle.fontFamily
|
|
141
153
|
const font = `${style} ${weight} ${size} ${family}`
|
|
142
154
|
fontKeys.set(painted, font)
|
|
143
155
|
return font
|
|
@@ -184,6 +196,14 @@ export type HeaderResizeMachine = SelectionEngine & {
|
|
|
184
196
|
stepTrack: (axis: "row" | "col", index: number, direction: 1 | -1) => boolean
|
|
185
197
|
/** Fits column `index` to its content, as the strip double-click does (false when renderCell is set). / 列 `index` を中身に合わせる (ストリップのダブルクリックと同じ。renderCell があれば false)。 */
|
|
186
198
|
autofitColumn: (index: number) => boolean
|
|
199
|
+
/**
|
|
200
|
+
* Fits the listed columns to the widest text of every row (no fan-out; hidden columns keep their width), gated at
|
|
201
|
+
* maxOperationCells measured cells (false after onOperationRejected; false when renderCell is set or no font source is
|
|
202
|
+
* mounted).
|
|
203
|
+
* 名指した列を全部の行で最も広い文字に合わせる (ファンアウトなし。隠した列は幅を保つ)。測るセルは maxOperationCells まで
|
|
204
|
+
* (超えると onOperationRejected の後に false。renderCell があるか書体の元が未装着なら false)。
|
|
205
|
+
*/
|
|
206
|
+
autofitColumnsInAllRows: (columns: readonly number[]) => boolean
|
|
187
207
|
}
|
|
188
208
|
|
|
189
209
|
/**
|
|
@@ -323,44 +343,28 @@ export const useHeaderResize = (params: UseHeaderResizeParams): HeaderResizeMach
|
|
|
323
343
|
[collectTargets, applyEntries],
|
|
324
344
|
)
|
|
325
345
|
|
|
326
|
-
/**
|
|
327
|
-
|
|
328
|
-
|
|
346
|
+
/**
|
|
347
|
+
* The measuring core of both autofit entries: fits `targets` to the widest painted text of `measuredRows` (per-column
|
|
348
|
+
* canvas measurement in each cell's painted font over the cells' base font from fontSourceRef, bucketed by font key),
|
|
349
|
+
* gated at `measureLimit` measured cells. No row to measure, no 2d context (jsdom) or an all-empty column set changes
|
|
350
|
+
* nothing and still counts as a run (true).
|
|
351
|
+
* 両方の autofit の入口が共有する計測の芯。`targets` を `measuredRows` の最も広い塗りの文字に合わせる (fontSourceRef のセルの
|
|
352
|
+
* 基本の書体の上で、セルごとの塗りの書体で列ごとに canvas で測り、書体の鍵で束ねる)。測るセルは `measureLimit` まで。測る行が無い・
|
|
353
|
+
* 2d の文脈が無い (jsdom)・全部が空の列は何も変えずに走ったものとする (true)。
|
|
354
|
+
*
|
|
355
|
+
* @param targets - The columns to fit. 合わせる列。
|
|
356
|
+
* @param measuredRows - The rows to measure, each with a size above 0. 測る行 (どれも大きさ 0 より大きい)。
|
|
357
|
+
* @param measureLimit - The most measured cells. 測るセルの上限。
|
|
358
|
+
* @param pressedIndex - The pressed track for the §7.2 zero-guard, or null for an imperative fit. §7.2 の 0 化ガードの押下トラック (命令の合わせでは null)。
|
|
359
|
+
* @returns Whether the fit ran (false after a refusal or without a mounted font source). 走ったか (拒んだとき・書体の元が未装着なら false)。
|
|
360
|
+
*/
|
|
361
|
+
const fitColumns = useCallback(
|
|
362
|
+
(targets: readonly number[], measuredRows: readonly number[], measureLimit: number, pressedIndex: number | null): boolean => {
|
|
329
363
|
const p = paramsRef.current
|
|
330
|
-
const
|
|
331
|
-
if (
|
|
364
|
+
const fontSource = p.fontSourceRef.current
|
|
365
|
+
if (fontSource === null) {
|
|
332
366
|
return false
|
|
333
367
|
}
|
|
334
|
-
// 帯件数は判断時ハンドル読み (ADR-21 / ADR-19 の判断時規律 — getFrozenSize().rows /
|
|
335
|
-
// .trailingRows はエンジンクランプ済み実効値)。装着前 (null) は帯集合なし — 測定集合が
|
|
336
|
-
// 窓のみに縮む v0.8 恒等の能力分岐 (値代替フォールバックではない)
|
|
337
|
-
const handle = p.handleRef.current
|
|
338
|
-
const frozenSize = handle === null ? null : handle.getFrozenSize()
|
|
339
|
-
const leadingRows = frozenSize === null ? 0 : frozenSize.rows
|
|
340
|
-
const trailingRows = frozenSize === null ? 0 : frozenSize.trailingRows
|
|
341
|
-
// 測定行集合 = [0, R_eff) ∪ [rowStart, rowEnd] ∪ [rowCount − T_eff, rowCount) の順序付き
|
|
342
|
-
// 和集合 (ADR-21 — v0.9 §4.14)。幅 0 行は計測しない (sizeOf > 0 フィルタ)、重複は Set で
|
|
343
|
-
// 去重 (R = T = 0 は窓のみ = v0.8 恒等)
|
|
344
|
-
const { rowStart, rowEnd } = p.viewStore.get()
|
|
345
|
-
const last = Math.min(rowEnd, p.rowAxis.count - 1)
|
|
346
|
-
const measuredRows: number[] = []
|
|
347
|
-
const seen = new Set<number>()
|
|
348
|
-
const pushRow = (row: number): void => {
|
|
349
|
-
if (!seen.has(row) && p.rowAxis.sizeOf(row) > 0) {
|
|
350
|
-
seen.add(row)
|
|
351
|
-
measuredRows.push(row)
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
for (let row = 0; row < leadingRows; row += 1) {
|
|
355
|
-
pushRow(row)
|
|
356
|
-
}
|
|
357
|
-
for (let row = rowStart; row <= last; row += 1) {
|
|
358
|
-
pushRow(row)
|
|
359
|
-
}
|
|
360
|
-
// 末尾帯の開始 = rowCount − T_eff (エンジンクランプ済みのため常に ≥ R_eff ≥ 0 — §1.2)
|
|
361
|
-
for (let row = p.rowAxis.count - trailingRows; row < p.rowAxis.count; row += 1) {
|
|
362
|
-
pushRow(row)
|
|
363
|
-
}
|
|
364
368
|
// 計測対象の可視行がゼロ (全帯 + 窓が非表示) は no-op — Excel も計測対象
|
|
365
369
|
// なしでは幅を変えない (0 計測 → 9px 潰しの破壊を作らない。R1(B) 所見)
|
|
366
370
|
// 測る行が無い / 測る書体が無い / 全空列は「当てるものが無い」実行で、拒否ではない (true)
|
|
@@ -368,30 +372,28 @@ export const useHeaderResize = (params: UseHeaderResizeParams): HeaderResizeMach
|
|
|
368
372
|
return true
|
|
369
373
|
}
|
|
370
374
|
// 上限ゲートは計測セル数単位 (§7.3 — トラック数単位では 16k 列 × 可視 40 行が素通り
|
|
371
|
-
// する)
|
|
372
|
-
// 帯行も算入する (ADR-21 — measuredRows が帯行を含む)
|
|
375
|
+
// する)。帯行も算入する (ADR-21 — measuredRows が帯行を含む)
|
|
373
376
|
const measuredCells = targets.length * measuredRows.length
|
|
374
|
-
const measureLimit = Math.floor(p.maxOperationCells / AUTOFIT_MEASURE_DIVISOR)
|
|
375
377
|
if (measuredCells > measureLimit) {
|
|
376
378
|
p.onOperationRejected?.("resize", measuredCells, "limit")
|
|
377
379
|
return false
|
|
378
380
|
}
|
|
379
|
-
// canvas 生成 + computed font
|
|
381
|
+
// canvas 生成 + computed font 取得は合わせ 1 回につき 1 度 (§7.3)。font は
|
|
380
382
|
// ロングハンド合成の単一経路 (Firefox は font ショートハンドの computed 値を空で
|
|
381
383
|
// 返すため、合成だけが全ブラウザで決定的)
|
|
382
384
|
const context = document.createElement("canvas").getContext("2d")
|
|
383
385
|
if (context === null) {
|
|
384
386
|
return true // 実ブラウザでは常に成立 (jsdom は 2d 未実装 — 計測面は E2E がピン)
|
|
385
387
|
}
|
|
386
|
-
const
|
|
387
|
-
// 既定フォント (書式なしセル) —
|
|
388
|
-
const baseFont = `${
|
|
388
|
+
const baseStyle = getComputedStyle(fontSource)
|
|
389
|
+
// 既定フォント (書式なしセル) — 本体ペインの computed style のロングハンド合成 (セルは本体ペインの書体を継ぐ)
|
|
390
|
+
const baseFont = `${baseStyle.fontStyle} ${baseStyle.fontWeight} ${baseStyle.fontSize} ${baseStyle.fontFamily}`
|
|
389
391
|
// 見出しの行は書式でなくスタイルシートが太くする — 測る書体もセルと同じ太さにする。スタイルシートが読まれていなければ
|
|
390
|
-
//
|
|
391
|
-
const headerWeight =
|
|
392
|
-
const headerStyle = { fontStyle:
|
|
392
|
+
// 見出しの行も本体と同じ太さで描かれるので、本体の太さがそのまま実際の描画の値
|
|
393
|
+
const headerWeight = baseStyle.getPropertyValue("--_aqsp-header-row-font-weight").trim()
|
|
394
|
+
const headerStyle = { fontStyle: baseStyle.fontStyle, fontWeight: headerWeight === "" ? baseStyle.fontWeight : headerWeight, fontSize: baseStyle.fontSize, fontFamily: baseStyle.fontFamily }
|
|
393
395
|
const headerFont = `${headerStyle.fontStyle} ${headerStyle.fontWeight} ${headerStyle.fontSize} ${headerStyle.fontFamily}`
|
|
394
|
-
// CF は幅に効く規則 (bold / italic 設定) と stopIfTrue の障壁だけを 1
|
|
396
|
+
// CF は幅に効く規則 (bold / italic 設定) と stopIfTrue の障壁だけを 1 回の合わせで 1 回抽出し列毎に
|
|
395
397
|
// スライスする (実測 155 → 56 ms)。障壁を残すため走査は塗りと同じ位置で止まり bold / italic が
|
|
396
398
|
// 厳密一致する。getConditionalFormats は getCellFormat を要する (root がマウント時に保証)
|
|
397
399
|
const cfRules = p.adapter.getConditionalFormats === undefined ? null : widthRelevantRules(p.adapter.getConditionalFormats())
|
|
@@ -411,7 +413,7 @@ export const useHeaderResize = (params: UseHeaderResizeParams): HeaderResizeMach
|
|
|
411
413
|
// 塗り書式 (base ⊕ 幅に効く CF) — raw は既読なので thunk は再読しない
|
|
412
414
|
const painted = p.adapter.getCellFormat === undefined ? undefined : resolvePaintedFormat(p.adapter, colIndex, memo, row, col, () => raw)
|
|
413
415
|
const isHeaderRow = row < p.headerRows
|
|
414
|
-
const font = painted === undefined ? (isHeaderRow ? headerFont : baseFont) : isHeaderRow ? fontKeyOf(painted, headerFontKeys, headerStyle) : fontKeyOf(painted, fontKeys,
|
|
416
|
+
const font = painted === undefined ? (isHeaderRow ? headerFont : baseFont) : isHeaderRow ? fontKeyOf(painted, headerFontKeys, headerStyle) : fontKeyOf(painted, fontKeys, baseStyle)
|
|
415
417
|
const text = formatCellValue(raw, painted === undefined ? undefined : painted.value)
|
|
416
418
|
const bucket = buckets.get(font)
|
|
417
419
|
if (bucket === undefined) {
|
|
@@ -446,7 +448,51 @@ export const useHeaderResize = (params: UseHeaderResizeParams): HeaderResizeMach
|
|
|
446
448
|
}
|
|
447
449
|
return applyEntries("col", entries, pressedIndex)
|
|
448
450
|
},
|
|
449
|
-
[
|
|
451
|
+
[applyEntries],
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
/** Column autofit (§7.3 / v0.8 §4.2.10 / ADR-21): per-column canvas measurement over the union of both frozen bands and the visible window — `[0, R_eff) ∪ [rowStart, rowEnd] ∪ [rowCount − T_eff, rowCount)`, each filtered by sizeOf > 0 — in each cell's painted font, bucketed by font key. / 列 autofit (§7.3 / v0.8 §4.2.10 / ADR-21): 両凍結帯 + 可視ウィンドウの和集合 `[0, R_eff) ∪ [rowStart, rowEnd] ∪ [rowCount − T_eff, rowCount)` (各 sizeOf > 0) をセルの塗りフォントで計測、フォントキーでバケット化。 */
|
|
455
|
+
const autofitColumns = useCallback(
|
|
456
|
+
(pressedIndex: number): boolean => {
|
|
457
|
+
const p = paramsRef.current
|
|
458
|
+
const targets = collectTargets("col", pressedIndex)
|
|
459
|
+
if (targets === null) {
|
|
460
|
+
return false
|
|
461
|
+
}
|
|
462
|
+
// 帯件数は判断時ハンドル読み (ADR-21 / ADR-19 の判断時規律 — getFrozenSize().rows /
|
|
463
|
+
// .trailingRows はエンジンクランプ済み実効値)。装着前 (null) は帯集合なし — 測定集合が
|
|
464
|
+
// 窓のみに縮む v0.8 恒等の能力分岐 (値代替フォールバックではない)
|
|
465
|
+
const handle = p.handleRef.current
|
|
466
|
+
const frozenSize = handle === null ? null : handle.getFrozenSize()
|
|
467
|
+
const leadingRows = frozenSize === null ? 0 : frozenSize.rows
|
|
468
|
+
const trailingRows = frozenSize === null ? 0 : frozenSize.trailingRows
|
|
469
|
+
// 測定行集合 = [0, R_eff) ∪ [rowStart, rowEnd] ∪ [rowCount − T_eff, rowCount) の順序付き
|
|
470
|
+
// 和集合 (ADR-21 — v0.9 §4.14)。幅 0 行は計測しない (sizeOf > 0 フィルタ)、重複は Set で
|
|
471
|
+
// 去重 (R = T = 0 は窓のみ = v0.8 恒等)
|
|
472
|
+
const { rowStart, rowEnd } = p.viewStore.get()
|
|
473
|
+
const last = Math.min(rowEnd, p.rowAxis.count - 1)
|
|
474
|
+
const measuredRows: number[] = []
|
|
475
|
+
const seen = new Set<number>()
|
|
476
|
+
const pushRow = (row: number): void => {
|
|
477
|
+
if (!seen.has(row) && p.rowAxis.sizeOf(row) > 0) {
|
|
478
|
+
seen.add(row)
|
|
479
|
+
measuredRows.push(row)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
for (let row = 0; row < leadingRows; row += 1) {
|
|
483
|
+
pushRow(row)
|
|
484
|
+
}
|
|
485
|
+
for (let row = rowStart; row <= last; row += 1) {
|
|
486
|
+
pushRow(row)
|
|
487
|
+
}
|
|
488
|
+
// 末尾帯の開始 = rowCount − T_eff (エンジンクランプ済みのため常に ≥ R_eff ≥ 0 — §1.2)
|
|
489
|
+
for (let row = p.rowAxis.count - trailingRows; row < p.rowAxis.count; row += 1) {
|
|
490
|
+
pushRow(row)
|
|
491
|
+
}
|
|
492
|
+
// 上限は autofit 専用の低い係数 (§13 (e) の設計返却 — 実測根拠は定数 docstring)。帯行も算入する (ADR-21)
|
|
493
|
+
return fitColumns(targets, measuredRows, Math.floor(p.maxOperationCells / AUTOFIT_MEASURE_DIVISOR), pressedIndex)
|
|
494
|
+
},
|
|
495
|
+
[collectTargets, fitColumns],
|
|
450
496
|
)
|
|
451
497
|
|
|
452
498
|
const stepTrack = useCallback(
|
|
@@ -473,20 +519,42 @@ export const useHeaderResize = (params: UseHeaderResizeParams): HeaderResizeMach
|
|
|
473
519
|
const autofitColumn = useCallback(
|
|
474
520
|
(index: number): boolean => {
|
|
475
521
|
const p = paramsRef.current
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (!p.autofitEnabled || band === null) {
|
|
522
|
+
// renderCell のカスタム DOM は measureText で測れない (帯のダブルクリックと同じ逸脱)。書体の元の未装着は芯が false を返す
|
|
523
|
+
if (!p.autofitEnabled) {
|
|
479
524
|
return false
|
|
480
525
|
}
|
|
481
526
|
// 隠れた列は隠れたまま (戻すのは再表示だけ) — 走ったが何も変えない
|
|
482
527
|
if (p.colAxis.sizeOf(index) === 0) {
|
|
483
528
|
return true
|
|
484
529
|
}
|
|
485
|
-
return autofitColumns(
|
|
530
|
+
return autofitColumns(index)
|
|
486
531
|
},
|
|
487
532
|
[autofitColumns],
|
|
488
533
|
)
|
|
489
534
|
|
|
535
|
+
const autofitColumnsInAllRows = useCallback(
|
|
536
|
+
(columns: readonly number[]): boolean => {
|
|
537
|
+
const p = paramsRef.current
|
|
538
|
+
if (!p.autofitEnabled) {
|
|
539
|
+
return false
|
|
540
|
+
}
|
|
541
|
+
// 隠れた列は隠れたまま (戻すのは再表示だけ)。重複は 1 回
|
|
542
|
+
const targets = [...new Set(columns)].filter((col) => p.colAxis.sizeOf(col) > 0)
|
|
543
|
+
if (targets.length === 0) {
|
|
544
|
+
return true
|
|
545
|
+
}
|
|
546
|
+
const measuredRows: number[] = []
|
|
547
|
+
for (let row = 0; row < p.rowAxis.count; row += 1) {
|
|
548
|
+
if (p.rowAxis.sizeOf(row) > 0) {
|
|
549
|
+
measuredRows.push(row)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// ホストが一度だけ呼ぶ合わせなので、上限は操作の予算そのもの (画面の操作の 1/4 の係数は打鍵ごとの応答のためのもの)
|
|
553
|
+
return fitColumns(targets, measuredRows, p.maxOperationCells, null)
|
|
554
|
+
},
|
|
555
|
+
[fitColumns],
|
|
556
|
+
)
|
|
557
|
+
|
|
490
558
|
const { enabled, colBandRef, rowBandRef } = params
|
|
491
559
|
useEffect(() => {
|
|
492
560
|
const colBand = colBandRef.current
|
|
@@ -680,7 +748,7 @@ export const useHeaderResize = (params: UseHeaderResizeParams): HeaderResizeMach
|
|
|
680
748
|
}
|
|
681
749
|
return
|
|
682
750
|
}
|
|
683
|
-
autofitColumns(
|
|
751
|
+
autofitColumns(hit.index)
|
|
684
752
|
}
|
|
685
753
|
|
|
686
754
|
const onLostPointerCapture = (event: PointerEvent) => {
|
|
@@ -717,6 +785,7 @@ export const useHeaderResize = (params: UseHeaderResizeParams): HeaderResizeMach
|
|
|
717
785
|
() => ({
|
|
718
786
|
stepTrack,
|
|
719
787
|
autofitColumn,
|
|
788
|
+
autofitColumnsInAllRows,
|
|
720
789
|
isDragging: () => draggingRef.current,
|
|
721
790
|
// リサイズ / unhide は選択矩形を配送しない — 配送モードは常に加算
|
|
722
791
|
getDragMode: (): "add" | "subtract" | "point" => "add",
|
|
@@ -728,6 +797,6 @@ export const useHeaderResize = (params: UseHeaderResizeParams): HeaderResizeMach
|
|
|
728
797
|
},
|
|
729
798
|
cancelDrag: cleanupDrag,
|
|
730
799
|
}),
|
|
731
|
-
[cleanupDrag, stepTrack, autofitColumn],
|
|
800
|
+
[cleanupDrag, stepTrack, autofitColumn, autofitColumnsInAllRows],
|
|
732
801
|
)
|
|
733
802
|
}
|
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 が公開面にある以上、その引数型も名指しできなければならない
|
|
@@ -122,5 +122,5 @@ export { NoteTip, type NoteTipProps } from "./popover/NoteTip.tsx"
|
|
|
122
122
|
// PopoverContext.anchorRect の型 (層座標系の矩形) — 配置エンジン placePopover 本体は非公開のまま
|
|
123
123
|
export type { PopoverRect } from "./popover/placement.ts"
|
|
124
124
|
export type { CommentPopoverContext, LinkPopoverContext, ListPopoverContext, MessagePopoverContext, NotePopoverContext, NoteTipContext, PopoverContext, PopoverKind, PopoverOverrides } from "./popover/usePopovers.ts"
|
|
125
|
-
export { DEFAULT_CUSTOM_COLORS, type ScrollToCellOptions, Spreadsheet, type SpreadsheetHandle, type SpreadsheetProps } from "./Spreadsheet.tsx"
|
|
125
|
+
export { DEFAULT_CUSTOM_COLORS, type ScrollToCellOptions, Spreadsheet, type SpreadsheetContentSize, type SpreadsheetHandle, type SpreadsheetProps } from "./Spreadsheet.tsx"
|
|
126
126
|
export type { SelectionState } from "./selection/selectionState.ts"
|
|
@@ -113,6 +113,26 @@
|
|
|
113
113
|
transform-origin: 0 0;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/* 見出し (列の記号・行の番号・角) を出さないグリッド (headings={false}): 格子の 2 本の帯を 0 にし、本体を根の左上から始める */
|
|
117
|
+
.aqsp-root[data-aqsp-headings-hidden] .aqsp-zoom {
|
|
118
|
+
grid-template-columns: 0 minmax(0, 1fr);
|
|
119
|
+
grid-template-rows: 0 minmax(0, 1fr);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* キーの移動で選んだセルをページの中で見せる印 (位置と大きさは revealCellInPage が書く)。根の直下でズームの包みの外に置き、見えず
|
|
124
|
+
* 押せない。scrollIntoView は可視性に依らず箱の位置へスクロールする
|
|
125
|
+
*/
|
|
126
|
+
.aqsp-reveal {
|
|
127
|
+
position: absolute;
|
|
128
|
+
top: 0;
|
|
129
|
+
left: 0;
|
|
130
|
+
width: 0;
|
|
131
|
+
height: 0;
|
|
132
|
+
pointer-events: none;
|
|
133
|
+
visibility: hidden;
|
|
134
|
+
}
|
|
135
|
+
|
|
116
136
|
.aqsp-corner {
|
|
117
137
|
grid-column: 1;
|
|
118
138
|
grid-row: 1;
|
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
|
|
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 も同じ
|
|
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"
|