@simplysm/excel 14.0.100 → 14.0.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/dist/biff/biff-model-factory.d.ts +32 -0
  2. package/dist/biff/biff-model-factory.d.ts.map +1 -0
  3. package/dist/biff/biff-model-factory.js +88 -0
  4. package/dist/biff/biff-model-factory.js.map +1 -0
  5. package/dist/biff/biff-ptg.d.ts +14 -0
  6. package/dist/biff/biff-ptg.d.ts.map +1 -0
  7. package/dist/biff/biff-ptg.js +277 -0
  8. package/dist/biff/biff-ptg.js.map +1 -0
  9. package/dist/biff/biff-records.d.ts +163 -0
  10. package/dist/biff/biff-records.d.ts.map +1 -0
  11. package/dist/biff/biff-records.js +579 -0
  12. package/dist/biff/biff-records.js.map +1 -0
  13. package/dist/biff/biff-shared-string-model.d.ts +13 -0
  14. package/dist/biff/biff-shared-string-model.d.ts.map +1 -0
  15. package/dist/biff/biff-shared-string-model.js +41 -0
  16. package/dist/biff/biff-shared-string-model.js.map +1 -0
  17. package/dist/biff/biff-style-model.d.ts +39 -0
  18. package/dist/biff/biff-style-model.d.ts.map +1 -0
  19. package/dist/biff/biff-style-model.js +182 -0
  20. package/dist/biff/biff-style-model.js.map +1 -0
  21. package/dist/biff/biff-workbook-model.d.ts +17 -0
  22. package/dist/biff/biff-workbook-model.d.ts.map +1 -0
  23. package/dist/biff/biff-workbook-model.js +64 -0
  24. package/dist/biff/biff-workbook-model.js.map +1 -0
  25. package/dist/biff/biff-worksheet-model.d.ts +69 -0
  26. package/dist/biff/biff-worksheet-model.d.ts.map +1 -0
  27. package/dist/biff/biff-worksheet-model.js +495 -0
  28. package/dist/biff/biff-worksheet-model.js.map +1 -0
  29. package/dist/biff/biff12-codec.d.ts +149 -0
  30. package/dist/biff/biff12-codec.d.ts.map +1 -0
  31. package/dist/biff/biff12-codec.js +262 -0
  32. package/dist/biff/biff12-codec.js.map +1 -0
  33. package/dist/excel-cell.d.ts +0 -6
  34. package/dist/excel-cell.d.ts.map +1 -1
  35. package/dist/excel-cell.js +2 -24
  36. package/dist/excel-cell.js.map +1 -1
  37. package/dist/excel-col.js.map +1 -1
  38. package/dist/excel-row.js.map +1 -1
  39. package/dist/excel-workbook.d.ts +4 -21
  40. package/dist/excel-workbook.d.ts.map +1 -1
  41. package/dist/excel-workbook.js +16 -49
  42. package/dist/excel-workbook.js.map +1 -1
  43. package/dist/excel-worksheet.d.ts +0 -11
  44. package/dist/excel-worksheet.d.ts.map +1 -1
  45. package/dist/excel-worksheet.js +9 -46
  46. package/dist/excel-worksheet.js.map +1 -1
  47. package/dist/excel-wrapper.d.ts +0 -10
  48. package/dist/excel-wrapper.d.ts.map +1 -1
  49. package/dist/excel-wrapper.js +0 -10
  50. package/dist/excel-wrapper.js.map +1 -1
  51. package/dist/models/excel-format.d.ts +3 -0
  52. package/dist/models/excel-format.d.ts.map +1 -0
  53. package/dist/models/excel-format.js +2 -0
  54. package/dist/models/excel-format.js.map +1 -0
  55. package/dist/models/excel-model-factory.d.ts +33 -0
  56. package/dist/models/excel-model-factory.d.ts.map +1 -0
  57. package/dist/models/excel-model-factory.js +2 -0
  58. package/dist/models/excel-model-factory.js.map +1 -0
  59. package/dist/models/excel-model.d.ts +12 -0
  60. package/dist/models/excel-model.d.ts.map +1 -0
  61. package/dist/models/excel-model.js +2 -0
  62. package/dist/models/excel-model.js.map +1 -0
  63. package/dist/models/i-content-type-model.d.ts +7 -0
  64. package/dist/models/i-content-type-model.d.ts.map +1 -0
  65. package/dist/models/i-content-type-model.js +2 -0
  66. package/dist/models/i-content-type-model.js.map +1 -0
  67. package/dist/models/i-drawing-model.d.ts +23 -0
  68. package/dist/models/i-drawing-model.d.ts.map +1 -0
  69. package/dist/models/i-drawing-model.js +2 -0
  70. package/dist/models/i-drawing-model.js.map +1 -0
  71. package/dist/models/i-relationship-model.d.ts +16 -0
  72. package/dist/models/i-relationship-model.d.ts.map +1 -0
  73. package/dist/models/i-relationship-model.js +2 -0
  74. package/dist/models/i-relationship-model.js.map +1 -0
  75. package/dist/models/i-shared-string-model.d.ts +9 -0
  76. package/dist/models/i-shared-string-model.d.ts.map +1 -0
  77. package/dist/models/i-shared-string-model.js +2 -0
  78. package/dist/models/i-shared-string-model.js.map +1 -0
  79. package/dist/models/i-style-model.d.ts +18 -0
  80. package/dist/models/i-style-model.d.ts.map +1 -0
  81. package/dist/models/i-style-model.js +2 -0
  82. package/dist/models/i-style-model.js.map +1 -0
  83. package/dist/models/i-workbook-model.d.ts +17 -0
  84. package/dist/models/i-workbook-model.d.ts.map +1 -0
  85. package/dist/models/i-workbook-model.js +2 -0
  86. package/dist/models/i-workbook-model.js.map +1 -0
  87. package/dist/models/i-worksheet-model.d.ts +39 -0
  88. package/dist/models/i-worksheet-model.d.ts.map +1 -0
  89. package/dist/models/i-worksheet-model.js +2 -0
  90. package/dist/models/i-worksheet-model.js.map +1 -0
  91. package/dist/models/shared/excel-cf-spec.d.ts +13 -0
  92. package/dist/models/shared/excel-cf-spec.d.ts.map +1 -0
  93. package/dist/models/shared/excel-cf-spec.js +2 -0
  94. package/dist/models/shared/excel-cf-spec.js.map +1 -0
  95. package/dist/models/shared/excel-style.d.ts +24 -0
  96. package/dist/models/shared/excel-style.d.ts.map +1 -0
  97. package/dist/models/shared/excel-style.js +38 -0
  98. package/dist/models/shared/excel-style.js.map +1 -0
  99. package/dist/types.d.ts +0 -13
  100. package/dist/types.d.ts.map +1 -1
  101. package/dist/types.js.map +1 -1
  102. package/dist/utils/excel-style-data.d.ts +3 -5
  103. package/dist/utils/excel-style-data.d.ts.map +1 -1
  104. package/dist/utils/excel-style-data.js +2 -16
  105. package/dist/utils/excel-style-data.js.map +1 -1
  106. package/dist/utils/zip-cache.d.ts +46 -9
  107. package/dist/utils/zip-cache.d.ts.map +1 -1
  108. package/dist/utils/zip-cache.js +138 -52
  109. package/dist/utils/zip-cache.js.map +1 -1
  110. package/dist/xml/excel-xml-content-type.d.ts +8 -4
  111. package/dist/xml/excel-xml-content-type.d.ts.map +1 -1
  112. package/dist/xml/excel-xml-content-type.js +13 -6
  113. package/dist/xml/excel-xml-content-type.js.map +1 -1
  114. package/dist/xml/excel-xml-drawing.d.ts +8 -4
  115. package/dist/xml/excel-xml-drawing.d.ts.map +1 -1
  116. package/dist/xml/excel-xml-drawing.js +14 -7
  117. package/dist/xml/excel-xml-drawing.js.map +1 -1
  118. package/dist/xml/excel-xml-relationship.d.ts +12 -4
  119. package/dist/xml/excel-xml-relationship.d.ts.map +1 -1
  120. package/dist/xml/excel-xml-relationship.js +22 -12
  121. package/dist/xml/excel-xml-relationship.js.map +1 -1
  122. package/dist/xml/excel-xml-shared-string.d.ts +8 -4
  123. package/dist/xml/excel-xml-shared-string.d.ts.map +1 -1
  124. package/dist/xml/excel-xml-shared-string.js +16 -9
  125. package/dist/xml/excel-xml-shared-string.js.map +1 -1
  126. package/dist/xml/excel-xml-style.d.ts +11 -22
  127. package/dist/xml/excel-xml-style.d.ts.map +1 -1
  128. package/dist/xml/excel-xml-style.js +66 -92
  129. package/dist/xml/excel-xml-style.js.map +1 -1
  130. package/dist/xml/excel-xml-unknown.d.ts +6 -5
  131. package/dist/xml/excel-xml-unknown.d.ts.map +1 -1
  132. package/dist/xml/excel-xml-unknown.js +7 -4
  133. package/dist/xml/excel-xml-unknown.js.map +1 -1
  134. package/dist/xml/excel-xml-workbook.d.ts +9 -4
  135. package/dist/xml/excel-xml-workbook.d.ts.map +1 -1
  136. package/dist/xml/excel-xml-workbook.js +28 -20
  137. package/dist/xml/excel-xml-workbook.js.map +1 -1
  138. package/dist/xml/excel-xml-worksheet.d.ts +12 -7
  139. package/dist/xml/excel-xml-worksheet.d.ts.map +1 -1
  140. package/dist/xml/excel-xml-worksheet.js +65 -50
  141. package/dist/xml/excel-xml-worksheet.js.map +1 -1
  142. package/dist/xml/xml-model-factory.d.ts +25 -0
  143. package/dist/xml/xml-model-factory.d.ts.map +1 -0
  144. package/dist/xml/xml-model-factory.js +66 -0
  145. package/dist/xml/xml-model-factory.js.map +1 -0
  146. package/package.json +2 -2
  147. package/src/biff/biff-model-factory.ts +101 -0
  148. package/src/biff/biff-ptg.ts +289 -0
  149. package/src/biff/biff-records.ts +686 -0
  150. package/src/biff/biff-shared-string-model.ts +52 -0
  151. package/src/biff/biff-style-model.ts +219 -0
  152. package/src/biff/biff-workbook-model.ts +83 -0
  153. package/src/biff/biff-worksheet-model.ts +545 -0
  154. package/src/biff/biff12-codec.ts +286 -0
  155. package/src/excel-cell.ts +12 -46
  156. package/src/excel-col.ts +3 -3
  157. package/src/excel-row.ts +3 -3
  158. package/src/excel-workbook.ts +26 -69
  159. package/src/excel-worksheet.ts +36 -107
  160. package/src/excel-wrapper.ts +0 -10
  161. package/src/models/excel-format.ts +2 -0
  162. package/src/models/excel-model-factory.ts +33 -0
  163. package/src/models/excel-model.ts +12 -0
  164. package/src/models/i-content-type-model.ts +7 -0
  165. package/src/models/i-drawing-model.ts +13 -0
  166. package/src/models/i-relationship-model.ts +13 -0
  167. package/src/models/i-shared-string-model.ts +9 -0
  168. package/src/models/i-style-model.ts +18 -0
  169. package/src/models/i-workbook-model.ts +17 -0
  170. package/src/models/i-worksheet-model.ts +40 -0
  171. package/src/models/shared/excel-cf-spec.ts +24 -0
  172. package/src/models/shared/excel-style.ts +65 -0
  173. package/src/types.ts +0 -13
  174. package/src/utils/excel-style-data.ts +4 -25
  175. package/src/utils/zip-cache.ts +189 -58
  176. package/src/xml/excel-xml-content-type.ts +18 -8
  177. package/src/xml/excel-xml-drawing.ts +19 -9
  178. package/src/xml/excel-xml-relationship.ts +28 -14
  179. package/src/xml/excel-xml-shared-string.ts +20 -11
  180. package/src/xml/excel-xml-style.ts +74 -116
  181. package/src/xml/excel-xml-unknown.ts +8 -4
  182. package/src/xml/excel-xml-workbook.ts +34 -22
  183. package/src/xml/excel-xml-worksheet.ts +73 -53
  184. package/src/xml/xml-model-factory.ts +83 -0
@@ -0,0 +1,686 @@
1
+ import type { ICfRuleSpec } from "../models/shared/excel-cf-spec";
2
+ import type { ExcelCellType } from "../types";
3
+ import { ExcelUtils } from "../utils/excel-utils";
4
+ import type { IRecord } from "./biff12-codec";
5
+ import {
6
+ concatBytes,
7
+ decodeRkNumber,
8
+ encodeRecord,
9
+ encodeXLWideString,
10
+ readAllRecords,
11
+ readCellPrefix,
12
+ readUint32LE,
13
+ readXLWideString,
14
+ REC,
15
+ writeCellPrefix,
16
+ writeUint32LE,
17
+ } from "./biff12-codec";
18
+ import { encodeFormula } from "./biff-ptg";
19
+
20
+ /**
21
+ * BIFF12 레코드 단위 reader.
22
+ * adtek `biff12.ts` 의 reader 로직을 포팅. 프리미티브는 `biff12-codec.ts`.
23
+ */
24
+
25
+ /** `"rId1"` → `1`. 형식 불일치 시 undefined. */
26
+ export function parseRelId(relId: string): number | undefined {
27
+ const m = /^rId(\d+)$/.exec(relId);
28
+ return m ? parseInt(m[1], 10) : undefined;
29
+ }
30
+
31
+ /**
32
+ * workbook.bin → 시트 항목 (BrtBundleSh) 순서대로.
33
+ *
34
+ * BrtBundleSh payload ([MS-XLSB] 2.4.301): hsState(4) + iTabID(4) + XLWideString strRelID + XLWideString strName.
35
+ */
36
+ export function readWorkbookSheets(buf: Uint8Array): { name: string; relId: string }[] {
37
+ const out: { name: string; relId: string }[] = [];
38
+ for (const rec of readAllRecords(buf)) {
39
+ if (rec.type !== REC.BrtBundleSh) continue;
40
+ const rel = readXLWideString(rec.payload, 8);
41
+ const name = readXLWideString(rec.payload, 8 + rel.bytesRead);
42
+ out.push({ name: name.value, relId: rel.value });
43
+ }
44
+ return out;
45
+ }
46
+
47
+ /** sharedStrings.bin → 문자열 배열 (BrtSSTItem 만 순서대로). payload: flags(1) + XLWideString. */
48
+ export function readSharedStrings(buf: Uint8Array): string[] {
49
+ const out: string[] = [];
50
+ for (const rec of readAllRecords(buf)) {
51
+ if (rec.type !== REC.BrtSSTItem) continue;
52
+ const { value } = readXLWideString(rec.payload, 1);
53
+ out.push(value);
54
+ }
55
+ return out;
56
+ }
57
+
58
+ /** 디코드된 셀 1개. `val === undefined` 는 blank(스타일만). `cellType` 은 OOXML 시맨틱. */
59
+ export interface IDecodedBiffCell {
60
+ col: number;
61
+ /** cell 의 iStyleRef (cellXFs 인덱스). Short cell 은 0. */
62
+ styleId: number;
63
+ cellType: ExcelCellType | undefined;
64
+ val: string | undefined;
65
+ }
66
+
67
+ function readFloat64(payload: Uint8Array, off: number): number {
68
+ const view = new DataView(payload.buffer, payload.byteOffset + off, 8);
69
+ return view.getFloat64(0, true);
70
+ }
71
+
72
+ /**
73
+ * Cell record 1개를 OOXML 시맨틱으로 디코드.
74
+ *
75
+ * - Full cell (Brt*) : payload 첫 4 byte = col, 8 byte prefix 후 값.
76
+ * - Short cell (BrtShort*) : col 정보 없음 (이전 셀 + 1), 4 byte prefix 후 값.
77
+ *
78
+ * SST 인덱스 셀(Isst)은 `cellType="s"`, `val=인덱스문자열` 로 반환 — SST 조회는 상위(ExcelCell)가 수행.
79
+ * inline string(St)은 `cellType="str"`. 숫자(Real/Rk)는 `cellType=undefined`(number).
80
+ *
81
+ * 대상 외 record 는 undefined.
82
+ */
83
+ export function decodeBiffCell(rec: IRecord, prevCol: number): IDecodedBiffCell | undefined {
84
+ const p = rec.payload;
85
+ const fullStyle = (): number => readCellPrefix(p).iStyleRef;
86
+ switch (rec.type) {
87
+ case REC.BrtCellIsst:
88
+ return {
89
+ col: readUint32LE(p, 0),
90
+ styleId: fullStyle(),
91
+ cellType: "s",
92
+ val: readUint32LE(p, 8).toString(),
93
+ };
94
+ case REC.BrtShortIsst:
95
+ return { col: prevCol + 1, styleId: 0, cellType: "s", val: readUint32LE(p, 4).toString() };
96
+ case REC.BrtCellSt:
97
+ return {
98
+ col: readUint32LE(p, 0),
99
+ styleId: fullStyle(),
100
+ cellType: "str",
101
+ val: readXLWideString(p, 8).value,
102
+ };
103
+ case REC.BrtShortSt:
104
+ return { col: prevCol + 1, styleId: 0, cellType: "str", val: readXLWideString(p, 4).value };
105
+ case REC.BrtCellReal:
106
+ return {
107
+ col: readUint32LE(p, 0),
108
+ styleId: fullStyle(),
109
+ cellType: undefined,
110
+ val: readFloat64(p, 8).toString(),
111
+ };
112
+ case REC.BrtShortReal:
113
+ return { col: prevCol + 1, styleId: 0, cellType: undefined, val: readFloat64(p, 4).toString() };
114
+ case REC.BrtCellRk:
115
+ return {
116
+ col: readUint32LE(p, 0),
117
+ styleId: fullStyle(),
118
+ cellType: undefined,
119
+ val: decodeRkNumber(readUint32LE(p, 8)).toString(),
120
+ };
121
+ case REC.BrtShortRk:
122
+ return {
123
+ col: prevCol + 1,
124
+ styleId: 0,
125
+ cellType: undefined,
126
+ val: decodeRkNumber(readUint32LE(p, 4)).toString(),
127
+ };
128
+ case REC.BrtCellBool:
129
+ return { col: readUint32LE(p, 0), styleId: fullStyle(), cellType: "b", val: p[8] ? "1" : "0" };
130
+ case REC.BrtCellBlank:
131
+ return { col: readUint32LE(p, 0), styleId: fullStyle(), cellType: undefined, val: undefined };
132
+ case REC.BrtShortBlank:
133
+ return { col: prevCol + 1, styleId: 0, cellType: undefined, val: undefined };
134
+ default:
135
+ return undefined;
136
+ }
137
+ }
138
+
139
+ //#region Encoders (Stage 3 writer)
140
+
141
+ /** payload 없는 begin/end 마커 record. */
142
+ export function encodeMarker(type: number): Uint8Array {
143
+ return encodeRecord(type, new Uint8Array(0));
144
+ }
145
+
146
+ /**
147
+ * BrtBundleSh — 워크북의 시트 항목.
148
+ * payload: hsState(4) + iTabID(4) + XLWideString strRelID + XLWideString strName.
149
+ */
150
+ export function encodeBrtBundleSh(relId: string, name: string, tabId: number): Uint8Array {
151
+ const rel = encodeXLWideString(relId);
152
+ const nm = encodeXLWideString(name);
153
+ const payload = new Uint8Array(8 + rel.length + nm.length);
154
+ writeUint32LE(payload, 0, 0); // hsState = visible
155
+ writeUint32LE(payload, 4, tabId);
156
+ payload.set(rel, 8);
157
+ payload.set(nm, 8 + rel.length);
158
+ return encodeRecord(REC.BrtBundleSh, payload);
159
+ }
160
+
161
+ /** BrtBeginSst — count(4) + uniqueCount(4). */
162
+ export function encodeBrtBeginSst(count: number, uniqueCount: number): Uint8Array {
163
+ const payload = new Uint8Array(8);
164
+ writeUint32LE(payload, 0, count);
165
+ writeUint32LE(payload, 4, uniqueCount);
166
+ return encodeRecord(REC.BrtBeginSst, payload);
167
+ }
168
+
169
+ /** BrtSSTItem — flags(1) + XLWideString. */
170
+ export function encodeBrtSSTItem(value: string): Uint8Array {
171
+ const wide = encodeXLWideString(value);
172
+ const payload = new Uint8Array(1 + wide.length);
173
+ payload[0] = 0; // flags
174
+ payload.set(wide, 1);
175
+ return encodeRecord(REC.BrtSSTItem, payload);
176
+ }
177
+
178
+ /** BrtCellIsst — cell prefix(8) + sharedString 인덱스(4). */
179
+ export function encodeBrtCellIsst(col: number, iStyleRef: number, sstIndex: number): Uint8Array {
180
+ const payload = new Uint8Array(8 + 4);
181
+ writeCellPrefix(payload, 0, col, iStyleRef);
182
+ writeUint32LE(payload, 8, sstIndex);
183
+ return encodeRecord(REC.BrtCellIsst, payload);
184
+ }
185
+
186
+ /** BrtCellReal — cell prefix(8) + IEEE754 double(8). */
187
+ export function encodeBrtCellReal(col: number, iStyleRef: number, value: number): Uint8Array {
188
+ const payload = new Uint8Array(8 + 8);
189
+ writeCellPrefix(payload, 0, col, iStyleRef);
190
+ new DataView(payload.buffer, payload.byteOffset + 8, 8).setFloat64(0, value, true);
191
+ return encodeRecord(REC.BrtCellReal, payload);
192
+ }
193
+
194
+ /** BrtCellSt — cell prefix(8) + XLWideString (inline string). */
195
+ export function encodeBrtCellSt(col: number, iStyleRef: number, value: string): Uint8Array {
196
+ const wide = encodeXLWideString(value);
197
+ const payload = new Uint8Array(8 + wide.length);
198
+ writeCellPrefix(payload, 0, col, iStyleRef);
199
+ payload.set(wide, 8);
200
+ return encodeRecord(REC.BrtCellSt, payload);
201
+ }
202
+
203
+ /** BrtCellBool — cell prefix(8) + boolean(1). */
204
+ export function encodeBrtCellBool(col: number, iStyleRef: number, value: boolean): Uint8Array {
205
+ const payload = new Uint8Array(9);
206
+ writeCellPrefix(payload, 0, col, iStyleRef);
207
+ payload[8] = value ? 1 : 0;
208
+ return encodeRecord(REC.BrtCellBool, payload);
209
+ }
210
+
211
+ /** BrtCellBlank — cell prefix(8) (style 만 가진 빈 셀). */
212
+ export function encodeBrtCellBlank(col: number, iStyleRef: number): Uint8Array {
213
+ const payload = new Uint8Array(8);
214
+ writeCellPrefix(payload, 0, col, iStyleRef);
215
+ return encodeRecord(REC.BrtCellBlank, payload);
216
+ }
217
+
218
+ /**
219
+ * BrtRowHdr — [MS-XLSB] 2.4.726.
220
+ * payload(25): rwT(4) + ixfe(4) + miyRw(2) + 패딩/flags(3) + ncolspan(4) + colFirst(4) + colLast(4).
221
+ */
222
+ export function encodeBrtRowHdr(rowIndex: number, colFirst: number, colLast: number): Uint8Array {
223
+ const payload = new Uint8Array(25);
224
+ writeUint32LE(payload, 0, rowIndex);
225
+ writeUint32LE(payload, 4, 0); // ixfe
226
+ payload[8] = 0x40; // miyRw 0x0140 = 16pt
227
+ payload[9] = 0x01;
228
+ writeUint32LE(payload, 13, 1); // ncolspan
229
+ writeUint32LE(payload, 17, colFirst);
230
+ writeUint32LE(payload, 21, colLast);
231
+ return encodeRecord(REC.BrtRowHdr, payload);
232
+ }
233
+
234
+ /** begin 컨테이너 레코드 (count u32 payload). */
235
+ export function encodeBeginCount(type: number, count: number): Uint8Array {
236
+ const payload = new Uint8Array(4);
237
+ writeUint32LE(payload, 0, count);
238
+ return encodeRecord(type, payload);
239
+ }
240
+
241
+ /** BrtFmt — numFmtId(u16) + XLWideString formatCode. */
242
+ export function encodeBrtFmt(numFmtId: number, formatCode: string): Uint8Array {
243
+ const code = encodeXLWideString(formatCode);
244
+ const payload = new Uint8Array(2 + code.length);
245
+ payload[0] = numFmtId & 0xff;
246
+ payload[1] = (numFmtId >>> 8) & 0xff;
247
+ payload.set(code, 2);
248
+ return encodeRecord(REC.BrtFmt, payload);
249
+ }
250
+
251
+ /**
252
+ * cellXF용 BrtXF(16B). ixfeParent=0(cellStyleXFs[0] 참조), iFmt=numFmtId, font/fill/border=0.
253
+ * flags(@12)=0x1010 은 정답지 기본값.
254
+ */
255
+ export function encodeCellXF(numFmtId: number): Uint8Array {
256
+ const p = new Uint8Array(16);
257
+ p[2] = numFmtId & 0xff;
258
+ p[3] = (numFmtId >>> 8) & 0xff;
259
+ p[12] = 0x10;
260
+ p[13] = 0x10;
261
+ return encodeRecord(REC.BrtXF, p);
262
+ }
263
+
264
+ /** BrtWsDim — UncheckedRfX(16): rwFirst, rwLast, colFirst, colLast. */
265
+ export function encodeBrtWsDim(
266
+ rwFirst: number,
267
+ rwLast: number,
268
+ colFirst: number,
269
+ colLast: number,
270
+ ): Uint8Array {
271
+ const payload = new Uint8Array(16);
272
+ writeUint32LE(payload, 0, rwFirst);
273
+ writeUint32LE(payload, 4, rwLast);
274
+ writeUint32LE(payload, 8, colFirst);
275
+ writeUint32LE(payload, 12, colLast);
276
+ return encodeRecord(REC.BrtWsDim, payload);
277
+ }
278
+
279
+ /** BrtMergeCell — UncheckedRfX(16): rwFirst, rwLast, colFirst, colLast. */
280
+ export function encodeBrtMergeCell(
281
+ s: { r: number; c: number },
282
+ e: { r: number; c: number },
283
+ ): Uint8Array {
284
+ const payload = new Uint8Array(16);
285
+ writeUint32LE(payload, 0, s.r);
286
+ writeUint32LE(payload, 4, e.r);
287
+ writeUint32LE(payload, 8, s.c);
288
+ writeUint32LE(payload, 12, e.c);
289
+ return encodeRecord(REC.BrtMergeCell, payload);
290
+ }
291
+
292
+ /**
293
+ * BrtColInfo — colFirst(u32) + colLast(u32) + coldx(u32) + ixfe(u32) + flags(u16).
294
+ * coldx 는 1/256 문자 단위 너비. flags bit1(fUserSet)=커스텀 너비.
295
+ */
296
+ export function encodeBrtColInfo(colFirst: number, colLast: number, coldx: number): Uint8Array {
297
+ const payload = new Uint8Array(18);
298
+ writeUint32LE(payload, 0, colFirst);
299
+ writeUint32LE(payload, 4, colLast);
300
+ writeUint32LE(payload, 8, coldx);
301
+ writeUint32LE(payload, 12, 0); // ixfe
302
+ payload[16] = 0x02; // fUserSet
303
+ payload[17] = 0x00;
304
+ return encodeRecord(REC.BrtColInfo, payload);
305
+ }
306
+
307
+ /** BrtFileVersion — 빈 새 워크북(통합 문서1.xlsb) 정답지 byte. */
308
+ export function encodeBrtFileVersion(): Uint8Array {
309
+ return encodeRecord(
310
+ REC.BrtFileVersion,
311
+ Uint8Array.from([
312
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
313
+ 0x00, 0x02, 0x00, 0x00, 0x00, 0x78, 0x00, 0x6c, 0x00, 0x01, 0x00, 0x00, 0x00, 0x37, 0x00,
314
+ 0x01, 0x00, 0x00, 0x00, 0x37, 0x00, 0x05, 0x00, 0x00, 0x00, 0x33, 0x00, 0x30, 0x00, 0x30,
315
+ 0x00, 0x32, 0x00, 0x36, 0x00,
316
+ ]),
317
+ );
318
+ }
319
+
320
+ /** BrtWbProp — 통합 문서 속성. 빈 새 워크북 정답지 byte(12B, codeName 없음). */
321
+ export function encodeBrtWbProp(): Uint8Array {
322
+ return encodeRecord(
323
+ REC.BrtWbProp,
324
+ Uint8Array.from([0x20, 0x00, 0x01, 0x00, 0x3c, 0x16, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00]),
325
+ );
326
+ }
327
+
328
+ /** BrtBookView — workbook 창 뷰. 빈 새 워크북 정답지 byte. */
329
+ export function encodeBrtBookView(): Uint8Array {
330
+ return encodeRecord(
331
+ REC.BrtBookView,
332
+ Uint8Array.from([
333
+ 0x65, 0x04, 0x00, 0x00, 0x93, 0x12, 0x00, 0x00, 0xfc, 0x6c, 0x00, 0x00, 0x95, 0x5b, 0x00,
334
+ 0x00, 0x58, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78,
335
+ ]),
336
+ );
337
+ }
338
+
339
+ /** BrtSheetFormatPr — 시트 기본 서식(행높이 등). 빈 새 워크북 정답지 byte. Excel 필수. */
340
+ export function encodeBrtSheetFormatPr(): Uint8Array {
341
+ return encodeRecord(
342
+ REC.BrtSheetFormatPr,
343
+ Uint8Array.from([0xff, 0xff, 0xff, 0xff, 0x08, 0x00, 0x4a, 0x01, 0x00, 0x00, 0x00, 0x00]),
344
+ );
345
+ }
346
+
347
+ /** BrtSel — worksheet 선택 영역. boa-sample 정답지 byte 템플릿(pane bottomRight, A1 선택). */
348
+ export function encodeBrtSel(): Uint8Array {
349
+ return encodeRecord(
350
+ REC.BrtSel,
351
+ Uint8Array.from([
352
+ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
353
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
354
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
355
+ ]),
356
+ );
357
+ }
358
+
359
+ /** BrtBeginAFilter — ref RfX(16): autoFilter 범위. */
360
+ export function encodeBrtBeginAFilter(
361
+ s: { r: number; c: number },
362
+ e: { r: number; c: number },
363
+ ): Uint8Array {
364
+ const payload = new Uint8Array(16);
365
+ writeUint32LE(payload, 0, s.r);
366
+ writeUint32LE(payload, 4, e.r);
367
+ writeUint32LE(payload, 8, s.c);
368
+ writeUint32LE(payload, 12, e.c);
369
+ return encodeRecord(REC.BrtBeginAFilter, payload);
370
+ }
371
+
372
+ // 빈 새 워크북 정답지(통합 문서1.xlsb) BrtBeginWsView. grbit 0x03dc(격자선 ON). @18 wScale 은 encode 가 덮어쓴다.
373
+ const WSVIEW_TEMPLATE = Uint8Array.from(
374
+ "dc 03 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 00 64 00 00 00 00 00 00 00 00 00 00 00"
375
+ .split(" ")
376
+ .map((x) => parseInt(x, 16)),
377
+ );
378
+
379
+ /**
380
+ * BrtBeginWsView — 정답지(boa-sample) 30B 템플릿. wScale(zoom, u16@18)만 치환.
381
+ * @param zoom 확대비율(%). 0/100 은 기본.
382
+ */
383
+ export function encodeBrtBeginWsView(zoom: number): Uint8Array {
384
+ const p = WSVIEW_TEMPLATE.slice();
385
+ p[18] = zoom & 0xff;
386
+ p[19] = (zoom >>> 8) & 0xff;
387
+ return encodeRecord(REC.BrtBeginWsView, p);
388
+ }
389
+
390
+ /**
391
+ * BrtPane(29B) — xnumXSplit(double@0) + ynumYSplit(double@8) + rwTop(u32@16) + colLeft(u32@20)
392
+ * + pnnAcc(u32@24) + flags(u8@28). flags=3(fFrozen|fFrozenNoSplit).
393
+ * pnnAcc: 0=bottomRight, 1=topRight, 2=bottomLeft, 3=topLeft.
394
+ */
395
+ export function encodeBrtPane(
396
+ xSplit: number,
397
+ ySplit: number,
398
+ rwTop: number,
399
+ colLeft: number,
400
+ pnnAcc: number,
401
+ ): Uint8Array {
402
+ const p = new Uint8Array(29);
403
+ const dv = new DataView(p.buffer);
404
+ dv.setFloat64(0, xSplit, true);
405
+ dv.setFloat64(8, ySplit, true);
406
+ writeUint32LE(p, 16, rwTop);
407
+ writeUint32LE(p, 20, colLeft);
408
+ writeUint32LE(p, 24, pnnAcc);
409
+ p[28] = 0x03;
410
+ return encodeRecord(REC.BrtPane, p);
411
+ }
412
+
413
+ /**
414
+ * BrtWsProp — [MS-XLSB] 2.4.823. 레이아웃(23B): flags(3B@0) + brtcolorTab(BrtColor 8B@3)
415
+ * + 상수영역(8B@11, ff×8) + codeName(XLWideString@19, 빈 문자열 4B).
416
+ * BrtColor(@3): byteA(bit0=fValidRGB, bits1-7=xColorType) + index(1) + nTintAndShade(i16)
417
+ * + R + G + B + A.
418
+ *
419
+ * 정답지(`.tmp/탭색.xlsb` sheet1.bin BrtWsProp): 빈 워크북 대비 byte0 bit1(0x02) set,
420
+ * @3=07(theme), @4=05(theme idx accent2), @7-10=E9 71 32 FF(resolved RGBA). 본 함수는 사용자
421
+ * 입력 RGB 를 RGB type(0x05)으로 인코딩(index 0, tint 0).
422
+ */
423
+ export function encodeBrtWsProp(tabColorArgb?: string): Uint8Array {
424
+ // 빈 새 워크북(통합 문서1.xlsb) 정답지 byte(23B, tabColor auto, codeName 없음).
425
+ const p = Uint8Array.from([
426
+ 0xc9, 0x04, 0x02, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
427
+ 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
428
+ ]);
429
+ if (tabColorArgb != null) {
430
+ // 정답지 색상상태 flag: byte0 bit1 set.
431
+ p[0] |= 0x02;
432
+ // brtcolorTab(@3, 8B): byteA + index + nTintAndShade(2) + R + G + B + A
433
+ p[3] = 0x05; // fValidRGB(0x01) | xColorType=RGB(2<<1)
434
+ p[4] = 0x00; // index (RGB 에서는 무시)
435
+ p[5] = 0x00;
436
+ p[6] = 0x00; // nTintAndShade = 0
437
+ p[7] = parseInt(tabColorArgb.slice(2, 4), 16); // R
438
+ p[8] = parseInt(tabColorArgb.slice(4, 6), 16); // G
439
+ p[9] = parseInt(tabColorArgb.slice(6, 8), 16); // B
440
+ p[10] = parseInt(tabColorArgb.slice(0, 2), 16); // A
441
+ }
442
+ return encodeRecord(REC.BrtWsProp, p);
443
+ }
444
+
445
+ /** BrtDrawing — XLWideString(r:id). 워크시트의 drawing 파트 참조. */
446
+ export function encodeBrtDrawing(relId: string): Uint8Array {
447
+ return encodeRecord(REC.BrtDrawing, encodeXLWideString(relId));
448
+ }
449
+
450
+ /**
451
+ * BrtFmlaNum — 수식 셀(숫자 결과).
452
+ * cellprefix(8) + xnum(double 8, 캐시값=0) + grbit(2) + CellParsedFormula(cce u32 + rgce + cb u32=0).
453
+ */
454
+ export function encodeBrtFmlaNum(col: number, iStyleRef: number, rgce: Uint8Array): Uint8Array {
455
+ const payload = new Uint8Array(8 + 8 + 2 + 4 + rgce.length + 4);
456
+ writeCellPrefix(payload, 0, col, iStyleRef);
457
+ // xnum(8) = 0, grbit(2) = 0
458
+ writeUint32LE(payload, 18, rgce.length); // cce
459
+ payload.set(rgce, 22);
460
+ writeUint32LE(payload, 22 + rgce.length, 0); // cb
461
+ return encodeRecord(REC.BrtFmlaNum, payload);
462
+ }
463
+
464
+ // --- 조건부 서식 / dxf : [MS-XLSB] 정확 인코딩 ---
465
+
466
+ /**
467
+ * XFPropColor([MS-XLSB] 2.5.161, 8B) — A|xclrType(1) + icv(1) + nTintShade(i16) + LongRGBA(R,G,B,A).
468
+ * ARGB "AARRGGBB" 를 RGB type(xclrType=2)으로 인코딩.
469
+ */
470
+ function encodeXFPropColor(argb: string): Uint8Array {
471
+ return Uint8Array.from([
472
+ 0x05, // fValidRGBA(0x01) | xclrType=RGB(2<<1)
473
+ 0x00, // icv (RGB 에서 무시)
474
+ 0x00,
475
+ 0x00, // nTintShade = 0
476
+ parseInt(argb.slice(2, 4), 16), // R
477
+ parseInt(argb.slice(4, 6), 16), // G
478
+ parseInt(argb.slice(6, 8), 16), // B
479
+ parseInt(argb.slice(0, 2), 16), // A
480
+ ]);
481
+ }
482
+
483
+ /** XFProp([MS-XLSB] 2.5.159) — xfPropType(u16) + cb(u16, 구조체 전체 크기=4+blob) + blob. */
484
+ function encodeXFProp(type: number, blob: Uint8Array): Uint8Array {
485
+ const out = new Uint8Array(4 + blob.length);
486
+ out[0] = type & 0xff;
487
+ out[1] = (type >>> 8) & 0xff;
488
+ const cb = 4 + blob.length;
489
+ out[2] = cb & 0xff;
490
+ out[3] = (cb >>> 8) & 0xff;
491
+ out.set(blob, 4);
492
+ return out;
493
+ }
494
+
495
+ /**
496
+ * BrtDXF — [MS-XLSB] 2.4.359. header(u16, @bit15 fNewBorder) + XFProps(reserved u16 + cprops u16 + XFProp[]).
497
+ * background → FillPattern(0x00=solid) + bgColor(0x02), fontColor → text color(0x05), bold → Bold(0x19, 700).
498
+ * CF 채우기는 xlsx dxf 와 동일하게 patternType=solid + bgColor 조합(색은 bgColor 에). XFPropColor(RGB), ARGB "AARRGGBB".
499
+ */
500
+ export function encodeBrtDXF(background?: string, fontColor?: string, bold?: boolean): Uint8Array {
501
+ const props: Uint8Array[] = [];
502
+ if (background != null) {
503
+ props.push(encodeXFProp(0x00, Uint8Array.from([0x01]))); // FillPattern = solid(1)
504
+ props.push(encodeXFProp(0x02, encodeXFPropColor(background))); // bgColor (채우기 색)
505
+ }
506
+ if (fontColor != null) {
507
+ props.push(encodeXFProp(0x05, encodeXFPropColor(fontColor))); // text color
508
+ }
509
+ if (bold === true) {
510
+ props.push(encodeXFProp(0x19, Uint8Array.from([0xbc, 0x02]))); // Bold = 700
511
+ }
512
+ const body = concatBytes(props);
513
+ const payload = new Uint8Array(6 + body.length);
514
+ payload[1] = 0x80; // header: fNewBorder(bit15) — 빈 워크북/boa 정답지 기본값
515
+ payload[4] = props.length & 0xff; // cprops
516
+ payload[5] = (props.length >>> 8) & 0xff;
517
+ payload.set(body, 6);
518
+ return encodeRecord(REC.BrtDXF, payload);
519
+ }
520
+
521
+ /**
522
+ * BrtBeginConditionalFormatting — [MS-XLSB] 2.4.34.
523
+ * ccf(u32, 규칙 수) + flags(u32, fPivot 등=0) + SqRfX(crfx u32 + crfx×RfX{rwFirst,rwLast,colFirst,colLast}).
524
+ * sqref 는 공백 구분 다중 범위 가능.
525
+ */
526
+ export function encodeBrtBeginConditionalFormatting(sqref: string, ccf: number): Uint8Array {
527
+ const ranges = sqref
528
+ .trim()
529
+ .split(/\s+/)
530
+ .filter((s) => s.length > 0)
531
+ .map((s) => ExcelUtils.parseRangeAddr(s));
532
+ const payload = new Uint8Array(8 + 4 + ranges.length * 16);
533
+ writeUint32LE(payload, 0, ccf);
534
+ writeUint32LE(payload, 4, 0); // flags
535
+ writeUint32LE(payload, 8, ranges.length); // crfx
536
+ let off = 12;
537
+ for (const r of ranges) {
538
+ writeUint32LE(payload, off, r.s.r); // rwFirst
539
+ writeUint32LE(payload, off + 4, r.e.r); // rwLast
540
+ writeUint32LE(payload, off + 8, r.s.c); // colFirst
541
+ writeUint32LE(payload, off + 12, r.e.c); // colLast
542
+ off += 16;
543
+ }
544
+ return encodeRecord(REC.BrtBeginConditionalFormatting, payload);
545
+ }
546
+
547
+ /** CFParsedFormula([MS-XLSB] 2.5.98.6) — cce(u32) + rgce + cb(u32=rgcb 길이=0). */
548
+ function encodeCFParsedFormula(rgce: Uint8Array): Uint8Array {
549
+ const out = new Uint8Array(4 + rgce.length + 4);
550
+ writeUint32LE(out, 0, rgce.length);
551
+ out.set(rgce, 4);
552
+ // 끝 4byte(rgcb 길이) = 0
553
+ return out;
554
+ }
555
+
556
+ /** XLNullableWideString — NULL 은 cchCharacters=0xFFFFFFFF, 그 외는 XLWideString 과 동일. */
557
+ function encodeXLNullableWideString(s?: string): Uint8Array {
558
+ if (s == null) return Uint8Array.from([0xff, 0xff, 0xff, 0xff]);
559
+ return encodeXLWideString(s);
560
+ }
561
+
562
+ /** CFOper([MS-XLSB] 2.5.15) — cellIs operator → iParam. */
563
+ const CF_OPER: Record<string, number> = {
564
+ between: 1,
565
+ notBetween: 2,
566
+ equal: 3,
567
+ notEqual: 4,
568
+ greaterThan: 5,
569
+ lessThan: 6,
570
+ greaterThanOrEqual: 7,
571
+ lessThanOrEqual: 8,
572
+ };
573
+
574
+ /** CFTextOper([MS-XLSB] 2.5.17) — text rule operator → iParam. */
575
+ const CF_TEXTOPER: Record<string, number> = {
576
+ containsText: 0,
577
+ notContains: 1,
578
+ beginsWith: 2,
579
+ endsWith: 3,
580
+ };
581
+
582
+ /**
583
+ * CONTAINSTEXT 규칙의 구현 수식 rgce. Ptg 시퀀스([MS-XLS] Ftab / [MS-XLSB] 2.5.97).
584
+ * PtgRefN row/col=0(상대) → 범위 내 각 셀이 자기 자신을 참조(범위 무관). ref-class 토큰(0x41/0x42/0x4c).
585
+ *
586
+ * - contains(0): NOT(ISERROR(SEARCH(text, 셀))) ← boa 정답지 검증
587
+ * - notContains(1): ISERROR(SEARCH(text, 셀)) ← boa 정답지 검증
588
+ * - beginsWith(2): LEFT(셀, LEN(text)) = text ← Ftab LEFT=0x73, LEN=0x20 ([MS-XLSB] 확정)
589
+ * - endsWith(3): RIGHT(셀, LEN(text)) = text ← Ftab RIGHT=0x74, LEN=0x20 ([MS-XLSB] 확정)
590
+ *
591
+ * @param textOper CF_TEXTOPER (0=contains, 1=notContains, 2=beginsWith, 3=endsWith)
592
+ */
593
+ function encodeCfTextRgce(textOper: number, text: string): Uint8Array {
594
+ const ptgStr = (s: string): number[] => {
595
+ const b = [0x17, s.length & 0xff, (s.length >>> 8) & 0xff];
596
+ for (const ch of s) {
597
+ const c = ch.charCodeAt(0);
598
+ b.push(c & 0xff, (c >>> 8) & 0xff);
599
+ }
600
+ return b;
601
+ };
602
+ const ptgRefN = [0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0]; // row u32=0 + col u16=0xC000(상대)
603
+
604
+ if (textOper === 0 || textOper === 1) {
605
+ const out = [...ptgStr(text), ...ptgRefN, 0x42, 0x02, 0x52, 0x00, 0x41, 0x03, 0x00]; // SEARCH(82),ISERROR(3)
606
+ if (textOper === 0) out.push(0x41, 0x26, 0x00); // NOT(38) — contains 만
607
+ return Uint8Array.from(out);
608
+ }
609
+
610
+ // beginsWith / endsWith: LEFT|RIGHT(셀, LEN(text)) = text
611
+ const fn = textOper === 2 ? 0x73 : 0x74; // LEFT=0x73 / RIGHT=0x74
612
+ return Uint8Array.from([
613
+ ...ptgRefN, // 셀
614
+ ...ptgStr(text), // text
615
+ 0x41, 0x20, 0x00, // PtgFunc LEN(32) → LEN(text)
616
+ 0x42, 0x02, fn, 0x00, // PtgFuncVar LEFT|RIGHT argc=2 → LEFT/RIGHT(셀, LEN(text))
617
+ ...ptgStr(text), // text
618
+ 0x0b, // PtgEq → ... = text
619
+ ]);
620
+ }
621
+
622
+ /**
623
+ * BrtBeginCFRule — [MS-XLSB] 2.4.23.
624
+ * iType(4) + iTemplate(4) + dxfId(4) + iPri(4) + iParam(4) + reserved1(4) + reserved2(4)
625
+ * + flags(2) + cbFmla1(4) + cbFmla2(4) + cbFmla3(4) + strParam(XLNullableWideString)
626
+ * + rgce1?(CFParsedFormula) + rgce2?.
627
+ *
628
+ * - cellIs: iType=CELLIS(1), iTemplate=EXPR(0), iParam=CFOper, rgce1(+between/notBetween 시 rgce2).
629
+ * - expression: iType=EXPRIS(2), iTemplate=FMLA(1), rgce1.
630
+ * - text(contains/notContains/begins/ends): iType=EXPRIS(2), iTemplate=CONTAINSTEXT(8),
631
+ * iParam=CFTextOper, strParam=text. rgce1(검색식)은 함수 Ptg 라 미인코딩(cbFmla1=0) — Excel 이
632
+ * strParam+CFTextOper 로 규칙을 재구성하도록 위임(Excel 호환 검증 대상).
633
+ */
634
+ export function encodeBrtBeginCFRule(
635
+ dxfId: number,
636
+ priority: number,
637
+ spec: ICfRuleSpec,
638
+ ): Uint8Array {
639
+ let iType: number;
640
+ let iTemplate: number;
641
+ let iParam = 0;
642
+ let strParam: string | undefined;
643
+ let rgce1: Uint8Array | undefined;
644
+ let rgce2: Uint8Array | undefined;
645
+
646
+ if (spec.type === "cellIs") {
647
+ iType = 1; // CF_TYPE_CELLIS
648
+ iTemplate = 0; // CF_TEMPLATE_EXPR
649
+ iParam = CF_OPER[spec.operator ?? "equal"];
650
+ rgce1 = encodeFormula(spec.formula[0]);
651
+ if (spec.operator === "between" || spec.operator === "notBetween") {
652
+ rgce2 = encodeFormula(spec.formula[1]);
653
+ }
654
+ } else if (spec.type === "expression") {
655
+ iType = 2; // CF_TYPE_EXPRIS
656
+ iTemplate = 1; // CF_TEMPLATE_FMLA
657
+ rgce1 = encodeFormula(spec.formula[0]);
658
+ } else {
659
+ iType = 2; // CF_TYPE_EXPRIS
660
+ iTemplate = 8; // CF_TEMPLATE_CONTAINSTEXT
661
+ iParam = CF_TEXTOPER[spec.operator ?? "containsText"];
662
+ strParam = spec.text;
663
+ // contains/notContains 는 boa 검증, beginsWith/endsWith 는 Ftab 추측(Excel 검증 대상).
664
+ rgce1 = encodeCfTextRgce(iParam, spec.text ?? "");
665
+ }
666
+
667
+ const cf1 = encodeCFParsedFormula(rgce1);
668
+ const cf2 = rgce2 != null ? encodeCFParsedFormula(rgce2) : undefined;
669
+
670
+ const head = new Uint8Array(42);
671
+ writeUint32LE(head, 0, iType);
672
+ writeUint32LE(head, 4, iTemplate);
673
+ writeUint32LE(head, 8, dxfId);
674
+ writeUint32LE(head, 12, priority);
675
+ writeUint32LE(head, 16, iParam);
676
+ // reserved1@20, reserved2@24, flags@28(2byte) = 0
677
+ writeUint32LE(head, 30, cf1.length); // cbFmla1
678
+ writeUint32LE(head, 34, cf2 != null ? cf2.length : 0); // cbFmla2
679
+ writeUint32LE(head, 38, 0); // cbFmla3
680
+
681
+ const parts: Uint8Array[] = [head, encodeXLNullableWideString(strParam), cf1];
682
+ if (cf2 != null) parts.push(cf2);
683
+ return encodeRecord(REC.BrtBeginCFRule, concatBytes(parts));
684
+ }
685
+
686
+ //#endregion