@aiquants/spreadsheet 0.8.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 +1103 -0
- package/LICENSE.md +155 -0
- package/README.md +817 -0
- package/dist/index.cjs +21 -0
- package/dist/index.d.cts +1960 -0
- package/dist/index.d.ts +1960 -0
- package/dist/index.js +7711 -0
- package/dist/styles/spreadsheet.css +2 -0
- package/dist/styles/spreadsheet.standalone.css +3 -0
- package/package.json +94 -0
- package/src/Spreadsheet.tsx +2313 -0
- package/src/clipboard/clipPayload.ts +1242 -0
- package/src/clipboard/fixtures/excel.fixture.ts +301 -0
- package/src/clipboard/fixtures/html.fixture.ts +90 -0
- package/src/clipboard/fixtures/sheets.fixture.ts +83 -0
- package/src/clipboard/htmlInterop.ts +1694 -0
- package/src/clipboard/numberFormatCodec.ts +870 -0
- package/src/clipboard/pasteSpecial.ts +608 -0
- package/src/clipboard/tiling.ts +370 -0
- package/src/clipboard/tsv.ts +186 -0
- package/src/clipboard/useClipboard.ts +1154 -0
- package/src/commands/selfCalc.ts +279 -0
- package/src/commands/useCommands.ts +946 -0
- package/src/editor/useCellEditor.ts +708 -0
- package/src/grid/CellView.tsx +51 -0
- package/src/grid/GridBody.tsx +489 -0
- package/src/header/ColumnHeader.tsx +167 -0
- package/src/header/RowHeader.tsx +144 -0
- package/src/header/columnName.ts +31 -0
- package/src/header/resizeGates.ts +146 -0
- package/src/header/resizeStrips.ts +79 -0
- package/src/header/useHeaderResize.ts +598 -0
- package/src/header/useHeaderSelection.ts +454 -0
- package/src/index.ts +122 -0
- package/src/keyboard/compositionGuard.ts +53 -0
- package/src/keyboard/ensureCellVisible.ts +77 -0
- package/src/keyboard/shortcuts.ts +718 -0
- package/src/keyboard/useGridKeyboard.ts +940 -0
- package/src/model/axisModel.ts +186 -0
- package/src/model/cellAnnotations.ts +92 -0
- package/src/model/cellCoord.ts +114 -0
- package/src/model/cellFormat.ts +562 -0
- package/src/model/cellPaint.ts +537 -0
- package/src/model/conditionalFormat.ts +805 -0
- package/src/model/dataAdapter.ts +478 -0
- package/src/model/dataValidation.ts +542 -0
- package/src/model/formulaRefs.ts +984 -0
- package/src/model/frozenPartition.ts +100 -0
- package/src/model/labels.ts +228 -0
- package/src/model/viewStore.ts +124 -0
- package/src/overlay/SelectionOverlay.tsx +1425 -0
- package/src/overlay/useFillHandle.ts +432 -0
- package/src/popover/CommentPopover.tsx +109 -0
- package/src/popover/LinkPopover.tsx +108 -0
- package/src/popover/ListPopover.tsx +161 -0
- package/src/popover/MessagePopover.tsx +36 -0
- package/src/popover/NotePopover.tsx +106 -0
- package/src/popover/NoteTip.tsx +26 -0
- package/src/popover/PopoverLayer.tsx +210 -0
- package/src/popover/placement.ts +138 -0
- package/src/popover/usePopovers.ts +642 -0
- package/src/selection/selectionState.ts +239 -0
- package/src/selection/useAutoScroller.ts +226 -0
- package/src/selection/usePointDrag.ts +273 -0
- package/src/selection/useSelectionEngine.ts +687 -0
- package/src/styles/components.entry.css +10 -0
- package/src/styles/spreadsheet.css +1029 -0
- package/src/styles/standalone.entry.css +9 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Effective cell format → cell paint (inline style + state attributes) and the
|
|
3
|
+
* editing sink's paint, interned per format object (v0.8 rich cells). Pure and React-free: the
|
|
4
|
+
* grid spreads `paint.style` / `paint.attrs` into the cell props and the editor applies
|
|
5
|
+
* `EditPaint.style` to the sink. Style keys are React / CSSOM camelCase property names so both
|
|
6
|
+
* consumers can spread or `Object.assign` them without a translation pass. Engine contract
|
|
7
|
+
* (each clause is mutation-gated — spec 07 §5.9):
|
|
8
|
+
* - TOTAL fail-soft: an unparseable colour, an unknown border style, a non-positive / non-finite
|
|
9
|
+
* font size, a blank font family or an out-of-vocabulary alignment is SKIPPED WHOLE (never
|
|
10
|
+
* substituted) while the rest of the format still paints; a format that paints nothing yields
|
|
11
|
+
* null — the v0.1 identity path (no attribute, no style).
|
|
12
|
+
* - Border OWNERSHIP folding: a cell draws its own right / bottom sides and FOLDS its right
|
|
13
|
+
* neighbour's left side and its below neighbour's top side onto them (own side wins on
|
|
14
|
+
* conflict); its own left / top sides are drawn only when the cell sits on the grid's left /
|
|
15
|
+
* top edge (`leftEdge` / `topEdge` — no visible neighbour to fold into; the caller passes the
|
|
16
|
+
* next VISIBLE neighbour's base format, so hidden tracks never own a line). Every grid line is
|
|
17
|
+
* therefore painted by exactly one cell and neighbours never double a border. Borders are
|
|
18
|
+
* painted as inline `borderRight` / `borderBottom` (/ `borderLeft` / `borderTop`) shorthands
|
|
19
|
+
* that override `.aqsp-cell`'s grid-line border in place — the cell box is border-box, so
|
|
20
|
+
* widths up to 3 px never shift layout.
|
|
21
|
+
* - INTERNING: an interior cell whose neighbours fold nothing is a pure function of its format
|
|
22
|
+
* object, so its paint (or its null) is interned in a WeakMap keyed by that object — the same
|
|
23
|
+
* format object always yields the same paint identity (per-slice memos and React props stay
|
|
24
|
+
* stable). Formats are treated as immutable values; paints are frozen.
|
|
25
|
+
* - Colour normalisation: hex 3 / 4 / 6 / 8, rgb() / rgba() (legacy comma and modern space
|
|
26
|
+
* syntax, integer or percentage channels, number or percentage alpha), the 148 CSS named
|
|
27
|
+
* colours and `transparent` → lowercase "#rrggbb" ("#rrggbbaa" only when the alpha is not
|
|
28
|
+
* opaque); anything else → null.
|
|
29
|
+
* - Font size is declared in pt and painted as CSS px (pt × 4 / 3); the sink's line box is the
|
|
30
|
+
* rounded px so the overlay can centre the caret line exactly like the cell's flex centring.
|
|
31
|
+
*
|
|
32
|
+
* 実効セル書式 → セル塗り (インラインスタイル + 状態属性) と編集シンクの塗り、書式 object 毎に
|
|
33
|
+
* intern (v0.8 リッチセル)。純関数・React 非依存: グリッドは `paint.style` / `paint.attrs` をセル
|
|
34
|
+
* props へ展開し、エディタは `EditPaint.style` をシンクへ適用。style のキーは React / CSSOM の
|
|
35
|
+
* camelCase プロパティ名 — 両消費者が変換なしに展開 / `Object.assign` できる。エンジン契約 (全条項
|
|
36
|
+
* を変異検証で封印 — spec 07 §5.9): 全経路 fail-soft (解釈不能な色 / 未知の罫線種 / 非正・非有限
|
|
37
|
+
* フォントサイズ / 空白のフォント族 / 語彙外の揃えは**丸ごとスキップ**し代替しない — 残りは塗る。
|
|
38
|
+
* 何も塗らない書式は null = v0.1 恒等経路)、罫線所有の畳込み (自セルは right / bottom を描き、
|
|
39
|
+
* 右隣の left と下隣の top を自分の right / bottom へ畳む — 競合は自セル優先。自分の left / top は
|
|
40
|
+
* グリッドの左端 / 上端 (`leftEdge` / `topEdge`) に居るときだけ描き、呼び手は次の**可視**隣接の
|
|
41
|
+
* base 書式を渡すので非表示トラックが線を持つことはない。各グリッド線を塗るセルは常に 1 つ。罫線
|
|
42
|
+
* はインラインの `borderRight` / `borderBottom` (/ `borderLeft` / `borderTop`) 短縮形で
|
|
43
|
+
* `.aqsp-cell` のグリッド線をその場で上書き — セル箱は border-box なので 3 px までの幅でレイアウト
|
|
44
|
+
* は動かない)、intern (隣接からの畳込みが無い内部セルは書式 object の純関数なので塗り (null も)
|
|
45
|
+
* を WeakMap で intern — 同 object は常に同 paint identity。書式は不変値として扱い、塗りは凍結)、
|
|
46
|
+
* 色正規化 (hex 3 / 4 / 6 / 8・rgb() / rgba() のカンマ / 空白構文・整数 / 百分率チャネル・148 の
|
|
47
|
+
* CSS 色名 + transparent → 小文字 "#rrggbb"、非不透明のときだけ "#rrggbbaa"、他は null)、
|
|
48
|
+
* フォントサイズは pt 宣言 → CSS px (pt × 4 / 3)、シンクの行箱は丸めた px。
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { alignToJustify, BORDER_DEFAULT_COLOR, type CellAlign, type CellBorder, type CellBorderStyle, type CellFormat, type CellVerticalAlign } from "./cellFormat.ts"
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The paint of one cell: inline style (React / CSSOM camelCase keys) and `data-aqsp-*` state
|
|
55
|
+
* attributes, both frozen. Spread into the cell props by the grid.
|
|
56
|
+
* セル 1 つの塗り: インラインスタイル (React / CSSOM の camelCase キー) と `data-aqsp-*` 状態属性、
|
|
57
|
+
* どちらも凍結。グリッドがセル props へ展開する。
|
|
58
|
+
*/
|
|
59
|
+
export type CellPaint = { readonly style: Readonly<Record<string, string>>; readonly attrs: Readonly<Record<string, string>> }
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The editing sink's paint: inline style, the line box height in px the overlay centres
|
|
63
|
+
* (`round(pt × 4 / 3)`, DEFAULT_LINE_BOX_PX when the format sets no valid size) and the vertical
|
|
64
|
+
* alignment the overlay honours when placing that line box (`middle` = the cell's flex centring).
|
|
65
|
+
* 編集シンクの塗り: インラインスタイル、オーバーレイが中央に置く行箱の高さ px (`round(pt × 4 / 3)`、
|
|
66
|
+
* 有効なサイズ指定が無ければ DEFAULT_LINE_BOX_PX)、その行箱を置くときにオーバーレイが従う垂直揃え
|
|
67
|
+
* (`middle` = セルの flex 中央)。
|
|
68
|
+
*/
|
|
69
|
+
export type EditPaint = { readonly style: Readonly<Record<string, string>>; readonly lineBoxPx: number; readonly verticalAlign: CellVerticalAlign }
|
|
70
|
+
|
|
71
|
+
/** Line box of the default 13 px cell font — the sink's line box when no valid font size is set. / 既定 13 px セルフォントの行箱 — 有効なフォントサイズ指定が無いときのシンクの行箱。 */
|
|
72
|
+
export const DEFAULT_LINE_BOX_PX = 13
|
|
73
|
+
|
|
74
|
+
/** The sink's paint while idle and for any format that paints nothing (returned BY IDENTITY so callers can skip the write). / 待機中および何も塗らない書式のシンク塗り (identity で返すため呼び手は書込を省ける)。 */
|
|
75
|
+
export const IDLE_EDIT_PAINT: EditPaint = Object.freeze({ style: Object.freeze({}), lineBoxPx: DEFAULT_LINE_BOX_PX, verticalAlign: "middle" })
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The 148 CSS named colours (CSS Color Level 4 — includes the gray / grey pairs and
|
|
79
|
+
* rebeccapurple) plus `transparent`, keyed by lowercase name. A closed vocabulary: anything not
|
|
80
|
+
* listed is not a colour (null), never guessed.
|
|
81
|
+
* CSS 色名 148 種 (CSS Color Level 4 — gray / grey の対と rebeccapurple を含む) + `transparent`、
|
|
82
|
+
* 小文字名をキーとする。閉じた語彙: 未掲載は色でない (null)、推測しない。
|
|
83
|
+
*/
|
|
84
|
+
const NAMED_COLOR_TABLE = {
|
|
85
|
+
aliceblue: "#f0f8ff",
|
|
86
|
+
antiquewhite: "#faebd7",
|
|
87
|
+
aqua: "#00ffff",
|
|
88
|
+
aquamarine: "#7fffd4",
|
|
89
|
+
azure: "#f0ffff",
|
|
90
|
+
beige: "#f5f5dc",
|
|
91
|
+
bisque: "#ffe4c4",
|
|
92
|
+
black: "#000000",
|
|
93
|
+
blanchedalmond: "#ffebcd",
|
|
94
|
+
blue: "#0000ff",
|
|
95
|
+
blueviolet: "#8a2be2",
|
|
96
|
+
brown: "#a52a2a",
|
|
97
|
+
burlywood: "#deb887",
|
|
98
|
+
cadetblue: "#5f9ea0",
|
|
99
|
+
chartreuse: "#7fff00",
|
|
100
|
+
chocolate: "#d2691e",
|
|
101
|
+
coral: "#ff7f50",
|
|
102
|
+
cornflowerblue: "#6495ed",
|
|
103
|
+
cornsilk: "#fff8dc",
|
|
104
|
+
crimson: "#dc143c",
|
|
105
|
+
cyan: "#00ffff",
|
|
106
|
+
darkblue: "#00008b",
|
|
107
|
+
darkcyan: "#008b8b",
|
|
108
|
+
darkgoldenrod: "#b8860b",
|
|
109
|
+
darkgray: "#a9a9a9",
|
|
110
|
+
darkgreen: "#006400",
|
|
111
|
+
darkgrey: "#a9a9a9",
|
|
112
|
+
darkkhaki: "#bdb76b",
|
|
113
|
+
darkmagenta: "#8b008b",
|
|
114
|
+
darkolivegreen: "#556b2f",
|
|
115
|
+
darkorange: "#ff8c00",
|
|
116
|
+
darkorchid: "#9932cc",
|
|
117
|
+
darkred: "#8b0000",
|
|
118
|
+
darksalmon: "#e9967a",
|
|
119
|
+
darkseagreen: "#8fbc8f",
|
|
120
|
+
darkslateblue: "#483d8b",
|
|
121
|
+
darkslategray: "#2f4f4f",
|
|
122
|
+
darkslategrey: "#2f4f4f",
|
|
123
|
+
darkturquoise: "#00ced1",
|
|
124
|
+
darkviolet: "#9400d3",
|
|
125
|
+
deeppink: "#ff1493",
|
|
126
|
+
deepskyblue: "#00bfff",
|
|
127
|
+
dimgray: "#696969",
|
|
128
|
+
dimgrey: "#696969",
|
|
129
|
+
dodgerblue: "#1e90ff",
|
|
130
|
+
firebrick: "#b22222",
|
|
131
|
+
floralwhite: "#fffaf0",
|
|
132
|
+
forestgreen: "#228b22",
|
|
133
|
+
fuchsia: "#ff00ff",
|
|
134
|
+
gainsboro: "#dcdcdc",
|
|
135
|
+
ghostwhite: "#f8f8ff",
|
|
136
|
+
gold: "#ffd700",
|
|
137
|
+
goldenrod: "#daa520",
|
|
138
|
+
gray: "#808080",
|
|
139
|
+
green: "#008000",
|
|
140
|
+
greenyellow: "#adff2f",
|
|
141
|
+
grey: "#808080",
|
|
142
|
+
honeydew: "#f0fff0",
|
|
143
|
+
hotpink: "#ff69b4",
|
|
144
|
+
indianred: "#cd5c5c",
|
|
145
|
+
indigo: "#4b0082",
|
|
146
|
+
ivory: "#fffff0",
|
|
147
|
+
khaki: "#f0e68c",
|
|
148
|
+
lavender: "#e6e6fa",
|
|
149
|
+
lavenderblush: "#fff0f5",
|
|
150
|
+
lawngreen: "#7cfc00",
|
|
151
|
+
lemonchiffon: "#fffacd",
|
|
152
|
+
lightblue: "#add8e6",
|
|
153
|
+
lightcoral: "#f08080",
|
|
154
|
+
lightcyan: "#e0ffff",
|
|
155
|
+
lightgoldenrodyellow: "#fafad2",
|
|
156
|
+
lightgray: "#d3d3d3",
|
|
157
|
+
lightgreen: "#90ee90",
|
|
158
|
+
lightgrey: "#d3d3d3",
|
|
159
|
+
lightpink: "#ffb6c1",
|
|
160
|
+
lightsalmon: "#ffa07a",
|
|
161
|
+
lightseagreen: "#20b2aa",
|
|
162
|
+
lightskyblue: "#87cefa",
|
|
163
|
+
lightslategray: "#778899",
|
|
164
|
+
lightslategrey: "#778899",
|
|
165
|
+
lightsteelblue: "#b0c4de",
|
|
166
|
+
lightyellow: "#ffffe0",
|
|
167
|
+
lime: "#00ff00",
|
|
168
|
+
limegreen: "#32cd32",
|
|
169
|
+
linen: "#faf0e6",
|
|
170
|
+
magenta: "#ff00ff",
|
|
171
|
+
maroon: "#800000",
|
|
172
|
+
mediumaquamarine: "#66cdaa",
|
|
173
|
+
mediumblue: "#0000cd",
|
|
174
|
+
mediumorchid: "#ba55d3",
|
|
175
|
+
mediumpurple: "#9370db",
|
|
176
|
+
mediumseagreen: "#3cb371",
|
|
177
|
+
mediumslateblue: "#7b68ee",
|
|
178
|
+
mediumspringgreen: "#00fa9a",
|
|
179
|
+
mediumturquoise: "#48d1cc",
|
|
180
|
+
mediumvioletred: "#c71585",
|
|
181
|
+
midnightblue: "#191970",
|
|
182
|
+
mintcream: "#f5fffa",
|
|
183
|
+
mistyrose: "#ffe4e1",
|
|
184
|
+
moccasin: "#ffe4b5",
|
|
185
|
+
navajowhite: "#ffdead",
|
|
186
|
+
navy: "#000080",
|
|
187
|
+
oldlace: "#fdf5e6",
|
|
188
|
+
olive: "#808000",
|
|
189
|
+
olivedrab: "#6b8e23",
|
|
190
|
+
orange: "#ffa500",
|
|
191
|
+
orangered: "#ff4500",
|
|
192
|
+
orchid: "#da70d6",
|
|
193
|
+
palegoldenrod: "#eee8aa",
|
|
194
|
+
palegreen: "#98fb98",
|
|
195
|
+
paleturquoise: "#afeeee",
|
|
196
|
+
palevioletred: "#db7093",
|
|
197
|
+
papayawhip: "#ffefd5",
|
|
198
|
+
peachpuff: "#ffdab9",
|
|
199
|
+
peru: "#cd853f",
|
|
200
|
+
pink: "#ffc0cb",
|
|
201
|
+
plum: "#dda0dd",
|
|
202
|
+
powderblue: "#b0e0e6",
|
|
203
|
+
purple: "#800080",
|
|
204
|
+
rebeccapurple: "#663399",
|
|
205
|
+
red: "#ff0000",
|
|
206
|
+
rosybrown: "#bc8f8f",
|
|
207
|
+
royalblue: "#4169e1",
|
|
208
|
+
saddlebrown: "#8b4513",
|
|
209
|
+
salmon: "#fa8072",
|
|
210
|
+
sandybrown: "#f4a460",
|
|
211
|
+
seagreen: "#2e8b57",
|
|
212
|
+
seashell: "#fff5ee",
|
|
213
|
+
sienna: "#a0522d",
|
|
214
|
+
silver: "#c0c0c0",
|
|
215
|
+
skyblue: "#87ceeb",
|
|
216
|
+
slateblue: "#6a5acd",
|
|
217
|
+
slategray: "#708090",
|
|
218
|
+
slategrey: "#708090",
|
|
219
|
+
snow: "#fffafa",
|
|
220
|
+
springgreen: "#00ff7f",
|
|
221
|
+
steelblue: "#4682b4",
|
|
222
|
+
tan: "#d2b48c",
|
|
223
|
+
teal: "#008080",
|
|
224
|
+
thistle: "#d8bfd8",
|
|
225
|
+
tomato: "#ff6347",
|
|
226
|
+
turquoise: "#40e0d0",
|
|
227
|
+
violet: "#ee82ee",
|
|
228
|
+
wheat: "#f5deb3",
|
|
229
|
+
white: "#ffffff",
|
|
230
|
+
whitesmoke: "#f5f5f5",
|
|
231
|
+
yellow: "#ffff00",
|
|
232
|
+
yellowgreen: "#9acd32",
|
|
233
|
+
transparent: "#00000000",
|
|
234
|
+
} as const
|
|
235
|
+
|
|
236
|
+
/** Name → normalised hex lookup (a Map so prototype keys such as "constructor" can never resolve). / 色名 → 正規化 hex の索引 (Map なので "constructor" 等の prototype キーは決して解決しない)。 */
|
|
237
|
+
const NAMED_COLORS: ReadonlyMap<string, string> = new Map(Object.entries(NAMED_COLOR_TABLE))
|
|
238
|
+
|
|
239
|
+
/** Hex colour grammar: exactly 3 / 4 / 6 / 8 hex digits after "#" (5 and 7 never match). / hex 色文法: "#" の後に 3 / 4 / 6 / 8 桁ちょうど (5 桁・7 桁は不成立)。 */
|
|
240
|
+
const HEX_COLOR = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/
|
|
241
|
+
|
|
242
|
+
/** rgb() / rgba() envelope; the argument text is tokenised separately. / rgb() / rgba() の外形。引数文字列は別途トークン化。 */
|
|
243
|
+
const RGB_FUNCTION = /^rgba?\((.*)\)$/
|
|
244
|
+
|
|
245
|
+
/** Percentage token grammar shared by channels and alpha. / チャネルと alpha が共有する百分率トークン文法。 */
|
|
246
|
+
const PERCENT_TOKEN = /^(\d+(?:\.\d+)?)%$/
|
|
247
|
+
|
|
248
|
+
/** One byte → two lowercase hex digits. / 1 バイト → 小文字 hex 2 桁。 */
|
|
249
|
+
const byteToHex = (value: number): string => value.toString(16).padStart(2, "0")
|
|
250
|
+
|
|
251
|
+
/** One rgb() channel token: integer 0-255 or percentage 0-100 % (rounded to a byte); null outside the grammar or range. / rgb() チャネル 1 トークン: 整数 0-255 か百分率 0-100 % (バイトへ丸め)。文法 / 範囲外は null。 */
|
|
252
|
+
const parseChannel = (token: string): number | null => {
|
|
253
|
+
const percent = PERCENT_TOKEN.exec(token)
|
|
254
|
+
if (percent !== null) {
|
|
255
|
+
const value = Number(percent[1])
|
|
256
|
+
return value <= 100 ? Math.round((value * 255) / 100) : null
|
|
257
|
+
}
|
|
258
|
+
// 整数のみ (CSS は小数チャネルも受けるが、正規化の語彙は整数バイトに閉じる)
|
|
259
|
+
if (!/^\d+$/.test(token)) {
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
const value = Number(token)
|
|
263
|
+
return value <= 255 ? value : null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The alpha token: number 0-1 or percentage 0-100 % → byte (rounded); null outside the grammar or range. / alpha トークン: 数値 0-1 か百分率 0-100 % → バイト (丸め)。文法 / 範囲外は null。 */
|
|
267
|
+
const parseAlpha = (token: string): number | null => {
|
|
268
|
+
const percent = PERCENT_TOKEN.exec(token)
|
|
269
|
+
if (percent !== null) {
|
|
270
|
+
const value = Number(percent[1])
|
|
271
|
+
return value <= 100 ? Math.round((value * 255) / 100) : null
|
|
272
|
+
}
|
|
273
|
+
if (!/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(token)) {
|
|
274
|
+
return null
|
|
275
|
+
}
|
|
276
|
+
const value = Number(token)
|
|
277
|
+
return value <= 1 ? Math.round(value * 255) : null
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Normalises the argument text of rgb() / rgba(): 3 channels + optional alpha, comma or whitespace (+ "/") separated. / rgb() / rgba() の引数文字列を正規化: 3 チャネル + 任意 alpha、カンマまたは空白 (+ "/") 区切り。 */
|
|
281
|
+
const normalizeRgbArguments = (inner: string): string | null => {
|
|
282
|
+
// 旧構文 (カンマ) と新構文 (空白 + "/") を同じトークン列へ。空引数・末尾区切りは空トークンになり不正
|
|
283
|
+
const tokens = inner.trim().split(/\s*[,/]\s*|\s+/)
|
|
284
|
+
if (tokens.length !== 3 && tokens.length !== 4) {
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
const red = parseChannel(tokens[0])
|
|
288
|
+
const green = parseChannel(tokens[1])
|
|
289
|
+
const blue = parseChannel(tokens[2])
|
|
290
|
+
if (red === null || green === null || blue === null) {
|
|
291
|
+
return null
|
|
292
|
+
}
|
|
293
|
+
const rgb = `#${byteToHex(red)}${byteToHex(green)}${byteToHex(blue)}`
|
|
294
|
+
if (tokens.length === 3) {
|
|
295
|
+
return rgb
|
|
296
|
+
}
|
|
297
|
+
const alpha = parseAlpha(tokens[3])
|
|
298
|
+
if (alpha === null) {
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
// 不透明 alpha は 6 桁へ畳む (同じ色は同じ文字列 — intern キーとしての正準性)
|
|
302
|
+
return alpha === 255 ? rgb : `${rgb}${byteToHex(alpha)}`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* CSS colour text → lowercase "#rrggbb" ("#rrggbbaa" when not opaque) or null when the text is
|
|
307
|
+
* not a colour this engine knows (hex 3 / 4 / 6 / 8, rgb() / rgba(), the CSS named colours,
|
|
308
|
+
* `transparent`). Whitespace-trimmed and case-insensitive. null means "ignore" — the caller
|
|
309
|
+
* skips the property whole; nothing is ever substituted.
|
|
310
|
+
* CSS 色文字列 → 小文字 "#rrggbb" (非不透明なら "#rrggbbaa")、このエンジンが知らない文字列は null
|
|
311
|
+
* (hex 3 / 4 / 6 / 8・rgb() / rgba()・CSS 色名・`transparent`)。前後空白は除き大小文字は無視。
|
|
312
|
+
* null は「無視」— 呼び手はプロパティを丸ごとスキップし、代替は決してしない。
|
|
313
|
+
*/
|
|
314
|
+
export const normalizeColor = (text: string): string | null => {
|
|
315
|
+
const t = text.trim().toLowerCase()
|
|
316
|
+
const hex = HEX_COLOR.exec(t)
|
|
317
|
+
if (hex !== null) {
|
|
318
|
+
const digits = hex[1]
|
|
319
|
+
// 3 / 4 桁は各 nibble を倍化 (#abc → #aabbcc)。8 桁の alpha "ff" は不透明 = 6 桁へ畳む
|
|
320
|
+
const expanded = digits.length <= 4 ? digits.replace(/./g, (nibble) => nibble + nibble) : digits
|
|
321
|
+
return expanded.length === 8 && expanded.endsWith("ff") ? `#${expanded.slice(0, 6)}` : `#${expanded}`
|
|
322
|
+
}
|
|
323
|
+
const fn = RGB_FUNCTION.exec(t)
|
|
324
|
+
if (fn !== null) {
|
|
325
|
+
return normalizeRgbArguments(fn[1])
|
|
326
|
+
}
|
|
327
|
+
const named = NAMED_COLORS.get(t)
|
|
328
|
+
return named === undefined ? null : named
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Border style → CSS width (px) and line style (thin 1 / medium 2 / thick 3 solid, dashed 1, dotted 1, double 3, hair 1 dotted). / 罫線種 → CSS 幅 (px) と線種。 */
|
|
332
|
+
const BORDER_LINE: Readonly<Record<CellBorderStyle, { readonly width: number; readonly line: string }>> = {
|
|
333
|
+
thin: { width: 1, line: "solid" },
|
|
334
|
+
medium: { width: 2, line: "solid" },
|
|
335
|
+
thick: { width: 3, line: "solid" },
|
|
336
|
+
dashed: { width: 1, line: "dashed" },
|
|
337
|
+
dotted: { width: 1, line: "dotted" },
|
|
338
|
+
double: { width: 3, line: "double" },
|
|
339
|
+
hair: { width: 1, line: "dotted" },
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* CSS border shorthand value ("1px solid #000000" etc.) for one border side, or null when the
|
|
344
|
+
* border is skipped whole: an unknown style, or a colour that does not normalise. An absent
|
|
345
|
+
* colour is the stated default BORDER_DEFAULT_COLOR (not a fallback for a bad one).
|
|
346
|
+
* 罫線 1 辺の CSS border 短縮値 ("1px solid #000000" 等)、丸ごとスキップなら null: 未知の線種、
|
|
347
|
+
* または正規化できない色。色の省略は宣言済み既定 BORDER_DEFAULT_COLOR (不正な色の代替ではない)。
|
|
348
|
+
*/
|
|
349
|
+
export const borderCss = (border: CellBorder): string | null => {
|
|
350
|
+
// 語彙外の線種は丸ごとスキップ — Object.hasOwn で prototype キー ("constructor") も弾く
|
|
351
|
+
if (!Object.hasOwn(BORDER_LINE, border.style)) {
|
|
352
|
+
return null
|
|
353
|
+
}
|
|
354
|
+
const line = BORDER_LINE[border.style]
|
|
355
|
+
// 色の省略は宣言済み既定、指定があれば正規化 — 解釈不能なら辺ごとスキップ (既定への置換はしない)
|
|
356
|
+
const color = border.color === undefined ? BORDER_DEFAULT_COLOR : normalizeColor(border.color)
|
|
357
|
+
return color === null ? null : `${line.width}px ${line.line} ${color}`
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Vertical alignment → the flex cell's align-items. / 垂直揃え → flex セルの align-items。 */
|
|
361
|
+
const VERTICAL_ALIGN_ITEMS: Readonly<Record<CellVerticalAlign, string>> = { top: "flex-start", middle: "center", bottom: "flex-end" }
|
|
362
|
+
|
|
363
|
+
/** Closed-vocabulary guard for align (anything else is skipped whole). / align の閉じた語彙ガード (他は丸ごとスキップ)。 */
|
|
364
|
+
const isCellAlign = (align: unknown): align is CellAlign => align === "left" || align === "center" || align === "right"
|
|
365
|
+
|
|
366
|
+
/** Closed-vocabulary guard for verticalAlign. / verticalAlign の閉じた語彙ガード。 */
|
|
367
|
+
const isVerticalAlign = (align: unknown): align is CellVerticalAlign => align === "top" || align === "middle" || align === "bottom"
|
|
368
|
+
|
|
369
|
+
/** Font size in pt → CSS px (pt × 4 / 3) when finite and positive; null otherwise (skipped whole, never substituted). / pt → CSS px (pt × 4 / 3)、有限かつ正のときだけ。他は null (丸ごとスキップ・代替しない)。 */
|
|
370
|
+
const fontPx = (pt: number | undefined): number | null => (typeof pt === "number" && Number.isFinite(pt) && pt > 0 ? (pt * 4) / 3 : null)
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Writes the text-level paint shared by the cell and the sink — colours (normalised), weight,
|
|
374
|
+
* style, decoration, family, size and wrap — into `style`, skipping every invalid value whole.
|
|
375
|
+
* セルとシンクが共有する文字水準の塗り — 色 (正規化済み)・太さ・斜体・装飾・フォント族・サイズ・
|
|
376
|
+
* 折返し — を `style` へ書く。無効な値は丸ごとスキップ。
|
|
377
|
+
*/
|
|
378
|
+
const writeTextPaint = (effective: CellFormat, style: Record<string, string>): void => {
|
|
379
|
+
// 色は正規化できたときだけ (解釈不能は丸ごとスキップ)
|
|
380
|
+
if (typeof effective.background === "string") {
|
|
381
|
+
const background = normalizeColor(effective.background)
|
|
382
|
+
if (background !== null) {
|
|
383
|
+
style.background = background
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (typeof effective.color === "string") {
|
|
387
|
+
const color = normalizeColor(effective.color)
|
|
388
|
+
if (color !== null) {
|
|
389
|
+
style.color = color
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// 真偽値は厳密に true のみ (truthy な非真偽値は語彙外 = スキップ)
|
|
393
|
+
if (effective.bold === true) {
|
|
394
|
+
style.fontWeight = "bold"
|
|
395
|
+
}
|
|
396
|
+
if (effective.italic === true) {
|
|
397
|
+
style.fontStyle = "italic"
|
|
398
|
+
}
|
|
399
|
+
// 下線と取消線は 1 つの text-decoration へ合成
|
|
400
|
+
const underline = effective.underline === true
|
|
401
|
+
const strikethrough = effective.strikethrough === true
|
|
402
|
+
if (underline || strikethrough) {
|
|
403
|
+
style.textDecoration = underline && strikethrough ? "underline line-through" : underline ? "underline" : "line-through"
|
|
404
|
+
}
|
|
405
|
+
// 空白だけのフォント族は無指定と同じ (CSSOM が拒む値を書かない)
|
|
406
|
+
if (typeof effective.fontFamily === "string" && effective.fontFamily.trim() !== "") {
|
|
407
|
+
style.fontFamily = effective.fontFamily
|
|
408
|
+
}
|
|
409
|
+
const px = fontPx(effective.fontSize)
|
|
410
|
+
if (px !== null) {
|
|
411
|
+
style.fontSize = `${px}px`
|
|
412
|
+
}
|
|
413
|
+
if (effective.wrap === true) {
|
|
414
|
+
style.whiteSpace = "pre-wrap"
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** One side's CSS: the cell's own border when valid (own wins on conflict), else the neighbour's folded side, else null. / 1 辺の CSS: 自セルの辺が有効ならそれ (競合は自セル優先)、無ければ隣接から畳んだ辺、無ければ null。 */
|
|
419
|
+
const resolveSide = (own: CellBorder | undefined, folded: CellBorder | undefined): string | null => {
|
|
420
|
+
const ownCss = own === undefined ? null : borderCss(own)
|
|
421
|
+
if (ownCss !== null) {
|
|
422
|
+
return ownCss
|
|
423
|
+
}
|
|
424
|
+
// 自セルの辺が無指定 / 無効なら隣接の辺を畳む
|
|
425
|
+
return folded === undefined ? null : borderCss(folded)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Builds a cell paint from scratch (no interning). / セル塗りを素から構築 (intern なし)。 */
|
|
429
|
+
const buildCellPaint = (effective: CellFormat | undefined, foldRight: CellBorder | undefined, foldBottom: CellBorder | undefined, leftEdge: boolean, topEdge: boolean): CellPaint | null => {
|
|
430
|
+
const style: Record<string, string> = {}
|
|
431
|
+
const attrs: Record<string, string> = {}
|
|
432
|
+
if (effective !== undefined) {
|
|
433
|
+
// 明示 align だけ塗る — 未指定の自動揃え (値依存) は呼び手が resolveCellAlign で解決する
|
|
434
|
+
if (isCellAlign(effective.align)) {
|
|
435
|
+
attrs["data-aqsp-align"] = effective.align
|
|
436
|
+
style.justifyContent = alignToJustify(effective.align)
|
|
437
|
+
}
|
|
438
|
+
if (isVerticalAlign(effective.verticalAlign)) {
|
|
439
|
+
attrs["data-aqsp-valign"] = effective.verticalAlign
|
|
440
|
+
style.alignItems = VERTICAL_ALIGN_ITEMS[effective.verticalAlign]
|
|
441
|
+
}
|
|
442
|
+
if (effective.wrap === true) {
|
|
443
|
+
attrs["data-aqsp-wrap"] = ""
|
|
444
|
+
}
|
|
445
|
+
writeTextPaint(effective, style)
|
|
446
|
+
}
|
|
447
|
+
// 罫線所有: right / bottom は自セル優先で隣接の辺を畳む。left / top は端のセルだけが直接描く
|
|
448
|
+
const borders = effective?.borders
|
|
449
|
+
const right = resolveSide(borders?.right, foldRight)
|
|
450
|
+
if (right !== null) {
|
|
451
|
+
style.borderRight = right
|
|
452
|
+
}
|
|
453
|
+
const bottom = resolveSide(borders?.bottom, foldBottom)
|
|
454
|
+
if (bottom !== null) {
|
|
455
|
+
style.borderBottom = bottom
|
|
456
|
+
}
|
|
457
|
+
if (leftEdge) {
|
|
458
|
+
const left = resolveSide(borders?.left, undefined)
|
|
459
|
+
if (left !== null) {
|
|
460
|
+
style.borderLeft = left
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (topEdge) {
|
|
464
|
+
const top = resolveSide(borders?.top, undefined)
|
|
465
|
+
if (top !== null) {
|
|
466
|
+
style.borderTop = top
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// 何も塗らなければ null (v0.1 恒等経路)
|
|
470
|
+
if (Object.keys(style).length === 0 && Object.keys(attrs).length === 0) {
|
|
471
|
+
return null
|
|
472
|
+
}
|
|
473
|
+
return Object.freeze({ style: Object.freeze(style), attrs: Object.freeze(attrs) })
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Interned paints (or nulls) of interior cells, keyed by format object identity. / 内部セルの intern 済み塗り (null 含む)、書式 object の identity がキー。 */
|
|
477
|
+
const paintCache = new WeakMap<CellFormat, CellPaint | null>()
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Computes the paint of one cell from its effective format and the BASE formats of its right and
|
|
481
|
+
* below neighbours (border folding — own right / bottom win), plus whether the cell sits on the
|
|
482
|
+
* grid's left / top edge (only then it draws its own left / top). Interned by `effective`
|
|
483
|
+
* identity in a WeakMap when the cell is interior and neither neighbour folds a border (the
|
|
484
|
+
* common case — the same format object always yields the same paint identity); computed per call
|
|
485
|
+
* otherwise. Returns null when nothing paints — the v0.1 identity path.
|
|
486
|
+
* 実効書式と右 / 下隣の base 書式 (罫線畳込み — 自セルの right / bottom 優先)、およびグリッドの
|
|
487
|
+
* 左端 / 上端に居るか (そのときだけ自分の left / top を描く) からセル塗りを計算。内部セルで両隣が
|
|
488
|
+
* 辺を畳まないとき (通常の場合 — 同 object は常に同 paint identity) は `effective` の identity で
|
|
489
|
+
* WeakMap に intern、それ以外は呼出毎に計算。何も塗らなければ null = v0.1 恒等経路。
|
|
490
|
+
*/
|
|
491
|
+
export const resolveCellPaint = (effective: CellFormat | undefined, rightBase: CellFormat | undefined, belowBase: CellFormat | undefined, leftEdge: boolean, topEdge: boolean): CellPaint | null => {
|
|
492
|
+
// 隣接から畳み込む辺: 右隣の left → 自分の right、下隣の top → 自分の bottom
|
|
493
|
+
const foldRight = rightBase?.borders?.left
|
|
494
|
+
const foldBottom = belowBase?.borders?.top
|
|
495
|
+
// 畳込みも端所有も無い内部セルは書式 object の純関数 — WeakMap intern の対象
|
|
496
|
+
const interior = !(leftEdge || topEdge) && foldRight === undefined && foldBottom === undefined
|
|
497
|
+
if (effective === undefined) {
|
|
498
|
+
return interior ? null : buildCellPaint(undefined, foldRight, foldBottom, leftEdge, topEdge)
|
|
499
|
+
}
|
|
500
|
+
if (!interior) {
|
|
501
|
+
return buildCellPaint(effective, foldRight, foldBottom, leftEdge, topEdge)
|
|
502
|
+
}
|
|
503
|
+
const cached = paintCache.get(effective)
|
|
504
|
+
if (cached !== undefined) {
|
|
505
|
+
return cached
|
|
506
|
+
}
|
|
507
|
+
const paint = buildCellPaint(effective, undefined, undefined, false, false)
|
|
508
|
+
paintCache.set(effective, paint)
|
|
509
|
+
return paint
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* The editing sink's paint for a cell format: the text-level style (colours, weight, style,
|
|
514
|
+
* decoration, family, size, wrap — alignment is NOT included, the editor's own text-align path
|
|
515
|
+
* owns it), the line box `round(pt × 4 / 3)` (DEFAULT_LINE_BOX_PX without a valid size) and the
|
|
516
|
+
* vertical alignment (`middle` when unset or out of vocabulary — the cell's flex centring).
|
|
517
|
+
* Returns IDLE_EDIT_PAINT by identity for undefined and for any format that changes nothing.
|
|
518
|
+
* セル書式に対する編集シンクの塗り: 文字水準の style (色・太さ・斜体・装飾・フォント族・サイズ・
|
|
519
|
+
* 折返し — 揃えは含めない、シンクの text-align はエディタ既存経路の所有)、行箱 `round(pt × 4 / 3)`
|
|
520
|
+
* (有効なサイズが無ければ DEFAULT_LINE_BOX_PX)、垂直揃え (未指定 / 語彙外は `middle` = セルの flex
|
|
521
|
+
* 中央)。undefined と何も変えない書式には IDLE_EDIT_PAINT を identity で返す。
|
|
522
|
+
*/
|
|
523
|
+
export const resolveEditPaint = (effective: CellFormat | undefined): EditPaint => {
|
|
524
|
+
if (effective === undefined) {
|
|
525
|
+
return IDLE_EDIT_PAINT
|
|
526
|
+
}
|
|
527
|
+
const style: Record<string, string> = {}
|
|
528
|
+
writeTextPaint(effective, style)
|
|
529
|
+
const px = fontPx(effective.fontSize)
|
|
530
|
+
const lineBoxPx = px === null ? DEFAULT_LINE_BOX_PX : Math.round(px)
|
|
531
|
+
const verticalAlign = isVerticalAlign(effective.verticalAlign) ? effective.verticalAlign : "middle"
|
|
532
|
+
// 書式があっても何も塗らず既定の行箱 / 中央揃えなら IDLE と identity 同一 (呼び手の「書込不要」判定に使える)
|
|
533
|
+
if (Object.keys(style).length === 0 && lineBoxPx === DEFAULT_LINE_BOX_PX && verticalAlign === "middle") {
|
|
534
|
+
return IDLE_EDIT_PAINT
|
|
535
|
+
}
|
|
536
|
+
return Object.freeze({ style: Object.freeze(style), lineBoxPx, verticalAlign })
|
|
537
|
+
}
|