@simplysm/excel 1.0.138 → 13.0.0-beta.2

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 (207) hide show
  1. package/.cache/typecheck-browser.tsbuildinfo +1 -0
  2. package/.cache/typecheck-node.tsbuildinfo +1 -0
  3. package/.cache/typecheck-tests-browser.tsbuildinfo +1 -0
  4. package/.cache/typecheck-tests-node.tsbuildinfo +1 -0
  5. package/README.md +491 -0
  6. package/dist/core-common/src/common.types.d.ts +74 -0
  7. package/dist/core-common/src/common.types.d.ts.map +1 -0
  8. package/dist/core-common/src/env.d.ts +6 -0
  9. package/dist/core-common/src/env.d.ts.map +1 -0
  10. package/dist/core-common/src/errors/argument-error.d.ts +25 -0
  11. package/dist/core-common/src/errors/argument-error.d.ts.map +1 -0
  12. package/dist/core-common/src/errors/not-implemented-error.d.ts +29 -0
  13. package/dist/core-common/src/errors/not-implemented-error.d.ts.map +1 -0
  14. package/dist/core-common/src/errors/sd-error.d.ts +27 -0
  15. package/dist/core-common/src/errors/sd-error.d.ts.map +1 -0
  16. package/dist/core-common/src/errors/timeout-error.d.ts +31 -0
  17. package/dist/core-common/src/errors/timeout-error.d.ts.map +1 -0
  18. package/dist/core-common/src/extensions/arr-ext.d.ts +15 -0
  19. package/dist/core-common/src/extensions/arr-ext.d.ts.map +1 -0
  20. package/dist/core-common/src/extensions/arr-ext.helpers.d.ts +19 -0
  21. package/dist/core-common/src/extensions/arr-ext.helpers.d.ts.map +1 -0
  22. package/dist/core-common/src/extensions/arr-ext.types.d.ts +215 -0
  23. package/dist/core-common/src/extensions/arr-ext.types.d.ts.map +1 -0
  24. package/dist/core-common/src/extensions/map-ext.d.ts +57 -0
  25. package/dist/core-common/src/extensions/map-ext.d.ts.map +1 -0
  26. package/dist/core-common/src/extensions/set-ext.d.ts +36 -0
  27. package/dist/core-common/src/extensions/set-ext.d.ts.map +1 -0
  28. package/dist/core-common/src/features/debounce-queue.d.ts +53 -0
  29. package/dist/core-common/src/features/debounce-queue.d.ts.map +1 -0
  30. package/dist/core-common/src/features/event-emitter.d.ts +66 -0
  31. package/dist/core-common/src/features/event-emitter.d.ts.map +1 -0
  32. package/dist/core-common/src/features/serial-queue.d.ts +47 -0
  33. package/dist/core-common/src/features/serial-queue.d.ts.map +1 -0
  34. package/dist/core-common/src/index.d.ts +32 -0
  35. package/dist/core-common/src/index.d.ts.map +1 -0
  36. package/dist/core-common/src/types/date-only.d.ts +152 -0
  37. package/dist/core-common/src/types/date-only.d.ts.map +1 -0
  38. package/dist/core-common/src/types/date-time.d.ts +96 -0
  39. package/dist/core-common/src/types/date-time.d.ts.map +1 -0
  40. package/dist/core-common/src/types/lazy-gc-map.d.ts +80 -0
  41. package/dist/core-common/src/types/lazy-gc-map.d.ts.map +1 -0
  42. package/dist/core-common/src/types/time.d.ts +68 -0
  43. package/dist/core-common/src/types/time.d.ts.map +1 -0
  44. package/dist/core-common/src/types/uuid.d.ts +35 -0
  45. package/dist/core-common/src/types/uuid.d.ts.map +1 -0
  46. package/dist/core-common/src/utils/bytes.d.ts +51 -0
  47. package/dist/core-common/src/utils/bytes.d.ts.map +1 -0
  48. package/dist/core-common/src/utils/date-format.d.ts +90 -0
  49. package/dist/core-common/src/utils/date-format.d.ts.map +1 -0
  50. package/dist/core-common/src/utils/json.d.ts +34 -0
  51. package/dist/core-common/src/utils/json.d.ts.map +1 -0
  52. package/dist/core-common/src/utils/num.d.ts +60 -0
  53. package/dist/core-common/src/utils/num.d.ts.map +1 -0
  54. package/dist/core-common/src/utils/obj.d.ts +258 -0
  55. package/dist/core-common/src/utils/obj.d.ts.map +1 -0
  56. package/dist/core-common/src/utils/path.d.ts +23 -0
  57. package/dist/core-common/src/utils/path.d.ts.map +1 -0
  58. package/dist/core-common/src/utils/primitive.d.ts +18 -0
  59. package/dist/core-common/src/utils/primitive.d.ts.map +1 -0
  60. package/dist/core-common/src/utils/str.d.ts +103 -0
  61. package/dist/core-common/src/utils/str.d.ts.map +1 -0
  62. package/dist/core-common/src/utils/template-strings.d.ts +84 -0
  63. package/dist/core-common/src/utils/template-strings.d.ts.map +1 -0
  64. package/dist/core-common/src/utils/transferable.d.ts +47 -0
  65. package/dist/core-common/src/utils/transferable.d.ts.map +1 -0
  66. package/dist/core-common/src/utils/wait.d.ts +19 -0
  67. package/dist/core-common/src/utils/wait.d.ts.map +1 -0
  68. package/dist/core-common/src/utils/xml.d.ts +36 -0
  69. package/dist/core-common/src/utils/xml.d.ts.map +1 -0
  70. package/dist/core-common/src/zip/sd-zip.d.ts +80 -0
  71. package/dist/core-common/src/zip/sd-zip.d.ts.map +1 -0
  72. package/dist/excel/src/excel-cell.d.ts +68 -0
  73. package/dist/excel/src/excel-cell.d.ts.map +1 -0
  74. package/dist/excel/src/excel-col.d.ts +19 -0
  75. package/dist/excel/src/excel-col.d.ts.map +1 -0
  76. package/dist/excel/src/excel-row.d.ts +17 -0
  77. package/dist/excel/src/excel-row.d.ts.map +1 -0
  78. package/dist/excel/src/excel-workbook.d.ts +66 -0
  79. package/dist/excel/src/excel-workbook.d.ts.map +1 -0
  80. package/dist/excel/src/excel-worksheet.d.ts +102 -0
  81. package/dist/excel/src/excel-worksheet.d.ts.map +1 -0
  82. package/dist/excel/src/excel-wrapper.d.ts +42 -0
  83. package/dist/excel/src/excel-wrapper.d.ts.map +1 -0
  84. package/dist/excel/src/index.d.ts +9 -0
  85. package/dist/excel/src/index.d.ts.map +1 -0
  86. package/dist/excel/src/types.d.ts +445 -0
  87. package/dist/excel/src/types.d.ts.map +1 -0
  88. package/dist/excel/src/utils/excel-utils.d.ts +50 -0
  89. package/dist/excel/src/utils/excel-utils.d.ts.map +1 -0
  90. package/dist/excel/src/utils/zip-cache.d.ts +23 -0
  91. package/dist/excel/src/utils/zip-cache.d.ts.map +1 -0
  92. package/dist/excel/src/xml/excel-xml-content-type.d.ts +12 -0
  93. package/dist/excel/src/xml/excel-xml-content-type.d.ts.map +1 -0
  94. package/dist/excel/src/xml/excel-xml-drawing.d.ts +26 -0
  95. package/dist/excel/src/xml/excel-xml-drawing.d.ts.map +1 -0
  96. package/dist/excel/src/xml/excel-xml-relationship.d.ts +18 -0
  97. package/dist/excel/src/xml/excel-xml-relationship.d.ts.map +1 -0
  98. package/dist/excel/src/xml/excel-xml-shared-string.d.ts +19 -0
  99. package/dist/excel/src/xml/excel-xml-shared-string.d.ts.map +1 -0
  100. package/dist/excel/src/xml/excel-xml-style.d.ts +31 -0
  101. package/dist/excel/src/xml/excel-xml-style.d.ts.map +1 -0
  102. package/dist/excel/src/xml/excel-xml-unknown.d.ts +11 -0
  103. package/dist/excel/src/xml/excel-xml-unknown.d.ts.map +1 -0
  104. package/dist/excel/src/xml/excel-xml-workbook.d.ts +22 -0
  105. package/dist/excel/src/xml/excel-xml-workbook.d.ts.map +1 -0
  106. package/dist/excel/src/xml/excel-xml-worksheet.d.ts +103 -0
  107. package/dist/excel/src/xml/excel-xml-worksheet.d.ts.map +1 -0
  108. package/dist/excel-cell.js +261 -0
  109. package/dist/excel-cell.js.map +7 -0
  110. package/dist/excel-col.js +36 -0
  111. package/dist/excel-col.js.map +7 -0
  112. package/dist/excel-row.js +31 -0
  113. package/dist/excel-row.js.map +7 -0
  114. package/dist/excel-workbook.js +137 -0
  115. package/dist/excel-workbook.js.map +7 -0
  116. package/dist/excel-worksheet.js +279 -0
  117. package/dist/excel-worksheet.js.map +7 -0
  118. package/dist/excel-wrapper.js +220 -0
  119. package/dist/excel-wrapper.js.map +7 -0
  120. package/dist/index.js +9 -15
  121. package/dist/index.js.map +7 -1
  122. package/dist/types.js +1 -0
  123. package/dist/types.js.map +7 -0
  124. package/dist/utils/excel-utils.js +162 -0
  125. package/dist/utils/excel-utils.js.map +7 -0
  126. package/dist/utils/zip-cache.js +74 -0
  127. package/dist/utils/zip-cache.js.map +7 -0
  128. package/dist/xml/excel-xml-content-type.js +57 -0
  129. package/dist/xml/excel-xml-content-type.js.map +7 -0
  130. package/dist/xml/excel-xml-drawing.js +77 -0
  131. package/dist/xml/excel-xml-drawing.js.map +7 -0
  132. package/dist/xml/excel-xml-relationship.js +72 -0
  133. package/dist/xml/excel-xml-relationship.js.map +7 -0
  134. package/dist/xml/excel-xml-shared-string.js +61 -0
  135. package/dist/xml/excel-xml-shared-string.js.map +7 -0
  136. package/dist/xml/excel-xml-style.js +313 -0
  137. package/dist/xml/excel-xml-style.js.map +7 -0
  138. package/dist/xml/excel-xml-unknown.js +11 -0
  139. package/dist/xml/excel-xml-unknown.js.map +7 -0
  140. package/dist/xml/excel-xml-workbook.js +94 -0
  141. package/dist/xml/excel-xml-workbook.js.map +7 -0
  142. package/dist/xml/excel-xml-worksheet.js +405 -0
  143. package/dist/xml/excel-xml-worksheet.js.map +7 -0
  144. package/package.json +13 -7
  145. package/src/excel-cell.ts +326 -0
  146. package/src/excel-col.ts +43 -0
  147. package/src/excel-row.ts +37 -0
  148. package/src/excel-workbook.ts +206 -0
  149. package/src/excel-worksheet.ts +380 -0
  150. package/src/excel-wrapper.ts +219 -0
  151. package/src/index.ts +13 -9
  152. package/src/types.ts +396 -0
  153. package/src/utils/excel-utils.ts +201 -0
  154. package/src/utils/zip-cache.ts +103 -0
  155. package/src/xml/excel-xml-content-type.ts +64 -0
  156. package/src/xml/excel-xml-drawing.ts +87 -0
  157. package/src/xml/excel-xml-relationship.ts +86 -0
  158. package/src/xml/excel-xml-shared-string.ts +80 -0
  159. package/src/xml/excel-xml-style.ts +393 -0
  160. package/src/xml/excel-xml-unknown.ts +11 -0
  161. package/src/xml/excel-xml-workbook.ts +112 -0
  162. package/src/xml/excel-xml-worksheet.ts +544 -0
  163. package/tests/excel-cell.spec.ts +407 -0
  164. package/tests/excel-col.spec.ts +112 -0
  165. package/tests/excel-row.spec.ts +71 -0
  166. package/tests/excel-workbook.spec.ts +166 -0
  167. package/tests/excel-worksheet.spec.ts +389 -0
  168. package/tests/excel-wrapper.spec.ts +275 -0
  169. package/tests/fixtures/logo.png +0 -0
  170. package/tests/image-insert.spec.ts +188 -0
  171. package/tests/utils/excel-utils.spec.ts +240 -0
  172. package/dist/ExcelCell.d.ts +0 -13
  173. package/dist/ExcelCell.js +0 -161
  174. package/dist/ExcelCell.js.map +0 -1
  175. package/dist/ExcelCellStyle.d.ts +0 -31
  176. package/dist/ExcelCellStyle.js +0 -312
  177. package/dist/ExcelCellStyle.js.map +0 -1
  178. package/dist/ExcelColumn.d.ts +0 -8
  179. package/dist/ExcelColumn.js +0 -49
  180. package/dist/ExcelColumn.js.map +0 -1
  181. package/dist/ExcelRow.d.ts +0 -7
  182. package/dist/ExcelRow.js +0 -21
  183. package/dist/ExcelRow.js.map +0 -1
  184. package/dist/ExcelWorkbook.d.ts +0 -24
  185. package/dist/ExcelWorkbook.js +0 -418
  186. package/dist/ExcelWorkbook.js.map +0 -1
  187. package/dist/ExcelWorksheet.d.ts +0 -14
  188. package/dist/ExcelWorksheet.js +0 -31
  189. package/dist/ExcelWorksheet.js.map +0 -1
  190. package/dist/index.d.ts +0 -9
  191. package/dist/utils/ExcelUtils.d.ts +0 -14
  192. package/dist/utils/ExcelUtils.js +0 -66
  193. package/dist/utils/ExcelUtils.js.map +0 -1
  194. package/dist/utils/XmlConvert.d.ts +0 -4
  195. package/dist/utils/XmlConvert.js +0 -64
  196. package/dist/utils/XmlConvert.js.map +0 -1
  197. package/src/ExcelCell.ts +0 -163
  198. package/src/ExcelCellStyle.ts +0 -297
  199. package/src/ExcelColumn.ts +0 -46
  200. package/src/ExcelRow.ts +0 -17
  201. package/src/ExcelWorkbook.ts +0 -369
  202. package/src/ExcelWorksheet.ts +0 -27
  203. package/src/utils/ExcelUtils.ts +0 -68
  204. package/src/utils/XmlConvert.ts +0 -20
  205. package/tsconfig.build.json +0 -18
  206. package/tsconfig.json +0 -18
  207. package/tslint.json +0 -5
@@ -0,0 +1,64 @@
1
+ import type { ExcelXml, ExcelXmlContentTypeData } from "../types";
2
+
3
+ /**
4
+ * [Content_Types].xml 파일을 관리하는 클래스.
5
+ * 파일별 MIME 타입 정보를 관리한다.
6
+ */
7
+ export class ExcelXmlContentType implements ExcelXml {
8
+ data: ExcelXmlContentTypeData;
9
+
10
+ constructor(data?: ExcelXmlContentTypeData) {
11
+ if (data == null) {
12
+ this.data = {
13
+ Types: {
14
+ $: {
15
+ xmlns: "http://schemas.openxmlformats.org/package/2006/content-types",
16
+ },
17
+ Default: [
18
+ {
19
+ $: {
20
+ Extension: "rels",
21
+ ContentType: "application/vnd.openxmlformats-package.relationships+xml",
22
+ },
23
+ },
24
+ {
25
+ $: {
26
+ Extension: "xml",
27
+ ContentType: "application/xml",
28
+ },
29
+ },
30
+ ],
31
+ Override: [
32
+ {
33
+ $: {
34
+ PartName: "/xl/workbook.xml",
35
+ ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
36
+ },
37
+ },
38
+ ],
39
+ },
40
+ };
41
+ } else {
42
+ this.data = data;
43
+ }
44
+ }
45
+
46
+ add(partName: string, contentType: string): this {
47
+ // 중복 체크
48
+ const exists = this.data.Types.Override.some((item) => item.$.PartName === partName);
49
+ if (exists) {
50
+ return this;
51
+ }
52
+
53
+ this.data.Types.Override.push({
54
+ $: {
55
+ PartName: partName,
56
+ ContentType: contentType,
57
+ },
58
+ });
59
+
60
+ return this;
61
+ }
62
+
63
+ cleanup(): void {}
64
+ }
@@ -0,0 +1,87 @@
1
+ import type { ExcelXml, ExcelXmlDrawingData } from "../types";
2
+
3
+ /**
4
+ * xl/drawings/drawing*.xml 파일을 관리하는 클래스.
5
+ * 이미지 삽입 시 위치 및 참조 정보를 처리한다.
6
+ */
7
+ export class ExcelXmlDrawing implements ExcelXml {
8
+ data: ExcelXmlDrawingData;
9
+
10
+ constructor(data?: ExcelXmlDrawingData) {
11
+ if (data == null) {
12
+ this.data = {
13
+ wsDr: {
14
+ $: {
15
+ "xmlns": "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing",
16
+ "xmlns:a": "http://schemas.openxmlformats.org/drawingml/2006/main",
17
+ "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
18
+ },
19
+ twoCellAnchor: [],
20
+ },
21
+ };
22
+ } else {
23
+ this.data = data;
24
+ }
25
+ }
26
+
27
+ addPicture(opts: {
28
+ from: { r: number; c: number; rOff?: number | string; cOff?: number | string };
29
+ to: { r: number; c: number; rOff?: number | string; cOff?: number | string };
30
+ blipRelId: string;
31
+ }): void {
32
+ this.data.wsDr.twoCellAnchor = this.data.wsDr.twoCellAnchor ?? [];
33
+
34
+ const anchors = this.data.wsDr.twoCellAnchor;
35
+ const picId = anchors.length + 1;
36
+ const name = `Picture ${picId}`;
37
+
38
+ this.data.wsDr.twoCellAnchor.push({
39
+ from: [
40
+ {
41
+ col: [opts.from.c.toString()],
42
+ colOff: [opts.from.cOff != null ? opts.from.cOff.toString() : "0"],
43
+ row: [opts.from.r.toString()],
44
+ rowOff: [opts.from.rOff != null ? opts.from.rOff.toString() : "0"],
45
+ },
46
+ ],
47
+ to: [
48
+ {
49
+ col: [opts.to.c.toString()],
50
+ colOff: [opts.to.cOff != null ? opts.to.cOff.toString() : "0"],
51
+ row: [opts.to.r.toString()],
52
+ rowOff: [opts.to.rOff != null ? opts.to.rOff.toString() : "0"],
53
+ },
54
+ ],
55
+ pic: [
56
+ {
57
+ nvPicPr: [
58
+ {
59
+ cNvPr: [{ $: { id: picId.toString(), name } }],
60
+ cNvPicPr: [{ "a:picLocks": [{ $: { noChangeAspect: "1" } }] }],
61
+ },
62
+ ],
63
+ blipFill: [
64
+ {
65
+ "a:blip": [{ $: { "r:embed": opts.blipRelId } }],
66
+ "a:stretch": [{ "a:fillRect": [] }],
67
+ },
68
+ ],
69
+ spPr: [
70
+ {
71
+ "a:xfrm": [
72
+ {
73
+ "a:off": [{ $: { x: "0", y: "0" } }],
74
+ "a:ext": [{ $: { cx: "0", cy: "0" } }],
75
+ },
76
+ ],
77
+ "a:prstGeom": [{ "$": { prst: "rect" }, "a:avLst": [] }],
78
+ },
79
+ ],
80
+ },
81
+ ],
82
+ clientData: [{}],
83
+ });
84
+ }
85
+
86
+ cleanup(): void {}
87
+ }
@@ -0,0 +1,86 @@
1
+ import "@simplysm/core-common";
2
+ import { numParseInt } from "@simplysm/core-common";
3
+ import type { ExcelRelationshipData, ExcelXml, ExcelXmlRelationshipData } from "../types";
4
+
5
+ /**
6
+ * *.rels 파일을 관리하는 클래스.
7
+ * 파일 간의 참조 관계를 처리한다.
8
+ */
9
+ export class ExcelXmlRelationship implements ExcelXml {
10
+ data: ExcelXmlRelationshipData;
11
+
12
+ constructor(data?: ExcelXmlRelationshipData) {
13
+ if (data == null) {
14
+ this.data = {
15
+ Relationships: {
16
+ $: {
17
+ xmlns: "http://schemas.openxmlformats.org/package/2006/relationships",
18
+ },
19
+ },
20
+ };
21
+ } else {
22
+ this.data = data;
23
+ }
24
+ }
25
+
26
+ getTargetByRelId(rId: number): string | undefined {
27
+ return (this.data.Relationships.Relationship ?? []).single((rel) => this._getRelId(rel) === rId)?.$.Target;
28
+ }
29
+
30
+ add(target: string, type: string): this {
31
+ this.addAndGetId(target, type);
32
+ return this;
33
+ }
34
+
35
+ addAndGetId(target: string, type: string): number {
36
+ this.data.Relationships.Relationship = this.data.Relationships.Relationship ?? [];
37
+
38
+ const newId = (this._lastId ?? 0) + 1;
39
+
40
+ this.data.Relationships.Relationship.push({
41
+ $: {
42
+ Id: `rId${newId}`,
43
+ Target: target,
44
+ Type: type,
45
+ },
46
+ });
47
+
48
+ return newId;
49
+ }
50
+
51
+ insert(rId: number, target: string, type: string): this {
52
+ this.data.Relationships.Relationship = this.data.Relationships.Relationship ?? [];
53
+
54
+ const shiftRels = this.data.Relationships.Relationship.filter((rel) => this._getRelId(rel) >= rId);
55
+ for (const shiftRel of shiftRels) {
56
+ shiftRel.$.Id = `rId${this._getRelId(shiftRel) + 1}`;
57
+ }
58
+
59
+ this.data.Relationships.Relationship.push({
60
+ $: {
61
+ Id: `rId${rId}`,
62
+ Target: target,
63
+ Type: type,
64
+ },
65
+ });
66
+
67
+ return this;
68
+ }
69
+
70
+ cleanup(): void {}
71
+
72
+ private get _lastId(): number | undefined {
73
+ const rels = this.data.Relationships.Relationship;
74
+ if (!rels || rels.length === 0) return undefined;
75
+ const maxRel = rels.orderByDesc((rel) => this._getRelId(rel)).first();
76
+ return maxRel ? this._getRelId(maxRel) : undefined;
77
+ }
78
+
79
+ private _getRelId(rel: ExcelRelationshipData): number {
80
+ const match = /[0-9]+$/.exec(rel.$.Id);
81
+ if (match == null) {
82
+ throw new Error(`잘못된 관계 ID 형식입니다: ${rel.$.Id}`);
83
+ }
84
+ return numParseInt(match[0])!;
85
+ }
86
+ }
@@ -0,0 +1,80 @@
1
+ import type {
2
+ ExcelXml,
3
+ ExcelXmlSharedStringData,
4
+ ExcelXmlSharedStringDataSi,
5
+ ExcelXmlSharedStringDataText,
6
+ } from "../types";
7
+ import "@simplysm/core-common";
8
+
9
+ /**
10
+ * xl/sharedStrings.xml 파일을 관리하는 클래스.
11
+ * 문자열 중복을 방지하여 파일 크기를 최적화한다.
12
+ */
13
+ export class ExcelXmlSharedString implements ExcelXml {
14
+ data: ExcelXmlSharedStringData;
15
+
16
+ private readonly _stringIndexesMap: Map<string, number[]>;
17
+
18
+ constructor(data?: ExcelXmlSharedStringData) {
19
+ if (data === undefined) {
20
+ this.data = {
21
+ sst: {
22
+ $: {
23
+ xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
24
+ },
25
+ },
26
+ };
27
+ } else {
28
+ this.data = data;
29
+ }
30
+
31
+ this._stringIndexesMap = this.data.sst.si
32
+ ? this.data.sst.si
33
+ .map((tag, id) => ({ id, tag }))
34
+ .filter((item) => !this._getHasInnerStyleOnSiTag(item.tag))
35
+ .toArrayMap(
36
+ (item) => this._getStringFromSiTag(item.tag),
37
+ (item) => item.id,
38
+ )
39
+ : new Map<string, number[]>();
40
+ }
41
+
42
+ getIdByString(str: string): number | undefined {
43
+ return this._stringIndexesMap.get(str)?.[0];
44
+ }
45
+
46
+ getStringById(id: number): string | undefined {
47
+ const si = this.data.sst.si?.[id];
48
+ return si != null ? this._getStringFromSiTag(si) : undefined;
49
+ }
50
+
51
+ add(str: string): number {
52
+ this.data.sst.si = this.data.sst.si ?? [];
53
+ const newLength = this.data.sst.si.push({ t: [str] });
54
+ const arr = this._stringIndexesMap.getOrCreate(str, []);
55
+ arr.push(newLength - 1);
56
+ return newLength - 1;
57
+ }
58
+
59
+ cleanup(): void {}
60
+
61
+ private _getStringFromSiTag(si: ExcelXmlSharedStringDataSi): string {
62
+ if ("t" in si) {
63
+ return this._getStringFromTTag(si.t);
64
+ } else {
65
+ return si.r.map((item) => this._getStringFromTTag(item.t)).join("");
66
+ }
67
+ }
68
+
69
+ private _getStringFromTTag(t: ExcelXmlSharedStringDataText): string {
70
+ const firstItem = t[0];
71
+ if (typeof firstItem === "string") {
72
+ return firstItem;
73
+ }
74
+ return firstItem._ ?? " ";
75
+ }
76
+
77
+ private _getHasInnerStyleOnSiTag(si: ExcelXmlSharedStringDataSi): boolean {
78
+ return Object.keys(si).some((item) => item !== "t");
79
+ }
80
+ }
@@ -0,0 +1,393 @@
1
+ import type {
2
+ ExcelBorderPosition,
3
+ ExcelHorizontalAlign,
4
+ ExcelVerticalAlign,
5
+ ExcelXml,
6
+ ExcelXmlStyleData,
7
+ ExcelXmlStyleDataBorder,
8
+ ExcelXmlStyleDataFill,
9
+ ExcelXmlStyleDataXf,
10
+ } from "../types";
11
+ import "@simplysm/core-common";
12
+ import { numParseInt, objClone, objEqual } from "@simplysm/core-common";
13
+
14
+ export interface ExcelStyle {
15
+ numFmtId?: string;
16
+ numFmtCode?: string;
17
+ border?: ExcelBorderPosition[];
18
+ background?: string;
19
+ verticalAlign?: ExcelVerticalAlign;
20
+ horizontalAlign?: ExcelHorizontalAlign;
21
+ }
22
+
23
+ /**
24
+ * xl/styles.xml 파일을 관리하는 클래스.
25
+ * 숫자 형식, 배경색, 테두리, 정렬 등의 스타일을 처리한다.
26
+ */
27
+ export class ExcelXmlStyle implements ExcelXml {
28
+ data: ExcelXmlStyleData;
29
+
30
+ constructor(data?: ExcelXmlStyleData) {
31
+ if (data === undefined) {
32
+ this.data = {
33
+ styleSheet: {
34
+ $: {
35
+ xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
36
+ },
37
+ fonts: [
38
+ {
39
+ $: { count: "1" },
40
+ font: [{}],
41
+ },
42
+ ],
43
+ fills: [
44
+ {
45
+ $: { count: "2" },
46
+ fill: [
47
+ { patternFill: [{ $: { patternType: "none" } }] },
48
+ { patternFill: [{ $: { patternType: "gray125" } }] },
49
+ ],
50
+ },
51
+ ],
52
+ borders: [
53
+ {
54
+ $: { count: "1" },
55
+ border: [{}],
56
+ },
57
+ ],
58
+ cellXfs: [
59
+ {
60
+ $: { count: "1" },
61
+ xf: [{ $: { numFmtId: "0" } }],
62
+ },
63
+ ],
64
+ },
65
+ };
66
+ } else {
67
+ this.data = data;
68
+ }
69
+ }
70
+
71
+ add(style: ExcelStyle): string {
72
+ const newXf: ExcelXmlStyleDataXf = { $: {} };
73
+
74
+ if (style.numFmtId !== undefined) {
75
+ newXf.$.numFmtId = style.numFmtId;
76
+ }
77
+
78
+ if (style.numFmtCode !== undefined) {
79
+ newXf.$.numFmtId = this._setNumFmtCode(style.numFmtCode);
80
+ newXf.$.applyNumberFormat = "1";
81
+ }
82
+
83
+ if (style.background !== undefined) {
84
+ const newFill: ExcelXmlStyleDataFill = {
85
+ patternFill: [
86
+ {
87
+ $: { patternType: "solid" },
88
+ fgColor: [{ $: { rgb: style.background.toUpperCase() } }],
89
+ },
90
+ ],
91
+ };
92
+
93
+ newXf.$.applyFill = "1";
94
+ newXf.$.fillId = this._getSameOrCreateFill(newFill);
95
+ }
96
+
97
+ if (style.border !== undefined) {
98
+ const newBorder = this._createBorderFromPositions(style.border);
99
+ newXf.$.applyBorder = "1";
100
+ newXf.$.borderId = this._getSameOrCreateBorder(newBorder);
101
+ }
102
+
103
+ this._applyAlignment(newXf, style);
104
+
105
+ return this._getSameOrCreateXf(newXf);
106
+ }
107
+
108
+ addWithClone(id: string, style: ExcelStyle): string {
109
+ const idNum = numParseInt(id);
110
+ if (idNum == null) {
111
+ throw new Error(`잘못된 스타일 ID: ${id}`);
112
+ }
113
+ const xfArray = this.data.styleSheet.cellXfs[0].xf;
114
+ if (idNum < 0 || idNum >= xfArray.length) {
115
+ throw new Error(`존재하지 않는 스타일 ID: ${id} (범위: 0-${xfArray.length - 1})`);
116
+ }
117
+ const prevXf = xfArray[idNum];
118
+ const cloneXf = objClone(prevXf);
119
+
120
+ if (style.numFmtId !== undefined) {
121
+ cloneXf.$.numFmtId = style.numFmtId;
122
+ }
123
+
124
+ if (style.numFmtCode !== undefined) {
125
+ cloneXf.$.numFmtId = this._setNumFmtCode(style.numFmtCode);
126
+ cloneXf.$.applyNumberFormat = "1";
127
+ }
128
+
129
+ if (style.background !== undefined) {
130
+ const fillIdNum = cloneXf.$.fillId !== undefined ? numParseInt(cloneXf.$.fillId) : undefined;
131
+ const prevFill = fillIdNum !== undefined ? this.data.styleSheet.fills[0].fill[fillIdNum] : undefined;
132
+
133
+ if (prevFill != null) {
134
+ const cloneFill = objClone(prevFill);
135
+ cloneFill.patternFill[0].$.patternType = "solid";
136
+
137
+ if (cloneFill.patternFill[0].fgColor == null) {
138
+ cloneFill.patternFill[0].fgColor = [{ $: { rgb: style.background } }];
139
+ } else {
140
+ cloneFill.patternFill[0].fgColor[0].$.rgb = style.background;
141
+ }
142
+
143
+ cloneXf.$.applyFill = "1";
144
+ cloneXf.$.fillId = this._getSameOrCreateFill(cloneFill);
145
+ } else {
146
+ const newFill: ExcelXmlStyleDataFill = {
147
+ patternFill: [
148
+ {
149
+ $: { patternType: "solid" },
150
+ fgColor: [{ $: { rgb: style.background.toUpperCase() } }],
151
+ },
152
+ ],
153
+ };
154
+ cloneXf.$.applyFill = "1";
155
+ cloneXf.$.fillId = this._getSameOrCreateFill(newFill);
156
+ }
157
+ }
158
+
159
+ if (style.border !== undefined) {
160
+ const borderIdNum = cloneXf.$.borderId !== undefined ? numParseInt(cloneXf.$.borderId) : undefined;
161
+ const prevBorder = borderIdNum !== undefined ? this.data.styleSheet.borders[0].border[borderIdNum] : undefined;
162
+
163
+ if (prevBorder != null) {
164
+ const cloneBorder = objClone(prevBorder);
165
+ this._applyBorderPosition(cloneBorder, "left", style.border.includes("left"));
166
+ this._applyBorderPosition(cloneBorder, "right", style.border.includes("right"));
167
+ this._applyBorderPosition(cloneBorder, "top", style.border.includes("top"));
168
+ this._applyBorderPosition(cloneBorder, "bottom", style.border.includes("bottom"));
169
+
170
+ cloneXf.$.applyBorder = "1";
171
+ cloneXf.$.borderId = this._getSameOrCreateBorder(cloneBorder);
172
+ } else {
173
+ const newBorder = this._createBorderFromPositions(style.border);
174
+ cloneXf.$.applyBorder = "1";
175
+ cloneXf.$.borderId = this._getSameOrCreateBorder(newBorder);
176
+ }
177
+ }
178
+
179
+ this._applyAlignment(cloneXf, style);
180
+
181
+ return this._getSameOrCreateXf(cloneXf);
182
+ }
183
+
184
+ get(id: string): ExcelStyle {
185
+ const idNum = numParseInt(id);
186
+ if (idNum == null) {
187
+ throw new Error(`잘못된 스타일 ID: ${id}`);
188
+ }
189
+ const xf = this.data.styleSheet.cellXfs[0].xf[idNum] as ExcelXmlStyleDataXf | undefined;
190
+
191
+ const result: ExcelStyle = {};
192
+
193
+ if (xf !== undefined) {
194
+ result.numFmtId = xf.$.numFmtId;
195
+
196
+ if (xf.$.fillId !== undefined) {
197
+ const fillIdNum = numParseInt(xf.$.fillId);
198
+ if (fillIdNum != null) {
199
+ const fill = this.data.styleSheet.fills[0].fill[fillIdNum] as ExcelXmlStyleDataFill | undefined;
200
+ if (fill == null) {
201
+ throw new Error(
202
+ `존재하지 않는 fill ID: ${xf.$.fillId} (범위: 0-${this.data.styleSheet.fills[0].fill.length - 1})`,
203
+ );
204
+ }
205
+ result.background = fill.patternFill[0].fgColor?.[0].$.rgb;
206
+ }
207
+ }
208
+
209
+ if (xf.$.borderId !== undefined) {
210
+ const borderIdNum = numParseInt(xf.$.borderId);
211
+ if (borderIdNum == null) {
212
+ throw new Error(`잘못된 border ID: ${xf.$.borderId}`);
213
+ }
214
+ const border = this.data.styleSheet.borders[0].border[borderIdNum] as ExcelXmlStyleDataBorder | undefined;
215
+ if (border == null) {
216
+ throw new Error(
217
+ `존재하지 않는 border ID: ${xf.$.borderId} (범위: 0-${this.data.styleSheet.borders[0].border.length - 1})`,
218
+ );
219
+ }
220
+ if (border.top != null || border.left != null || border.right != null || border.bottom != null) {
221
+ result.border = [];
222
+ if (border.left != null) {
223
+ result.border.push("left");
224
+ }
225
+ if (border.right != null) {
226
+ result.border.push("right");
227
+ }
228
+ if (border.top != null) {
229
+ result.border.push("top");
230
+ }
231
+ if (border.bottom != null) {
232
+ result.border.push("bottom");
233
+ }
234
+ }
235
+ }
236
+
237
+ result.verticalAlign = xf.alignment?.[0].$.vertical;
238
+ result.horizontalAlign = xf.alignment?.[0].$.horizontal;
239
+ }
240
+
241
+ return result;
242
+ }
243
+
244
+ getNumFmtCode(numFmtId: string): string | undefined {
245
+ return (this.data.styleSheet.numFmts?.[0].numFmt ?? []).single((item) => item.$.numFmtId === numFmtId)?.$
246
+ .formatCode;
247
+ }
248
+
249
+ cleanup(): void {
250
+ const result = {} as ExcelXmlStyleData["styleSheet"];
251
+
252
+ // 순서 정렬 (numFmts 맨위로)
253
+
254
+ if (this.data.styleSheet.numFmts != null) {
255
+ result.numFmts = this.data.styleSheet.numFmts;
256
+ }
257
+
258
+ const styleSheetRec = this.data.styleSheet as Record<string, unknown>;
259
+ const resultRec = result as Record<string, unknown>;
260
+ for (const key of Object.keys(styleSheetRec)) {
261
+ if (key === "numFmts") continue;
262
+
263
+ resultRec[key] = styleSheetRec[key];
264
+ }
265
+
266
+ this.data.styleSheet = result;
267
+ }
268
+
269
+ //#region Private Methods
270
+
271
+ private _setNumFmtCode(numFmtCode: string): string {
272
+ // 이미 해당 code가 있으면 넘기기
273
+ const existsNumFmtId = (this.data.styleSheet.numFmts?.[0].numFmt ?? []).single(
274
+ (item) => item.$.formatCode === numFmtCode,
275
+ )?.$.numFmtId;
276
+ if (existsNumFmtId != null) {
277
+ return existsNumFmtId;
278
+ }
279
+
280
+ this.data.styleSheet.numFmts = this.data.styleSheet.numFmts ?? [
281
+ {
282
+ $: { count: "0" },
283
+ numFmt: [],
284
+ },
285
+ ];
286
+
287
+ this.data.styleSheet.numFmts[0].numFmt = this.data.styleSheet.numFmts[0].numFmt ?? [];
288
+
289
+ // Excel 사용자 정의 숫자 형식은 ID 180 이상부터 시작한다 (0-163: 내장 형식, 164-179: 예약됨)
290
+ const numFmts = this.data.styleSheet.numFmts[0].numFmt;
291
+ const maxItem =
292
+ numFmts.length > 0 ? numFmts.orderByDesc((item) => numParseInt(item.$.numFmtId) ?? 180).first() : undefined;
293
+ const maxId = maxItem ? (numParseInt(maxItem.$.numFmtId) ?? 180) : 180;
294
+ const nextNumFmtId = (maxId + 1).toString();
295
+ this.data.styleSheet.numFmts[0].numFmt.push({
296
+ $: {
297
+ numFmtId: nextNumFmtId,
298
+ formatCode: numFmtCode,
299
+ },
300
+ });
301
+ this.data.styleSheet.numFmts[0].$.count = (
302
+ (numParseInt(this.data.styleSheet.numFmts[0].$.count) ?? 0) + 1
303
+ ).toString();
304
+
305
+ return nextNumFmtId;
306
+ }
307
+
308
+ private _applyAlignment(xf: ExcelXmlStyleDataXf, style: ExcelStyle): void {
309
+ if (style.verticalAlign !== undefined) {
310
+ xf.$.applyAlignment = "1";
311
+ if (xf.alignment == null) {
312
+ xf.alignment = [{ $: { vertical: style.verticalAlign } }];
313
+ } else {
314
+ xf.alignment[0].$.vertical = style.verticalAlign;
315
+ }
316
+ }
317
+
318
+ if (style.horizontalAlign !== undefined) {
319
+ xf.$.applyAlignment = "1";
320
+ if (xf.alignment == null) {
321
+ xf.alignment = [{ $: { horizontal: style.horizontalAlign } }];
322
+ } else {
323
+ xf.alignment[0].$.horizontal = style.horizontalAlign;
324
+ }
325
+ }
326
+ }
327
+
328
+ private _createBorderFromPositions(positions: ExcelBorderPosition[]): ExcelXmlStyleDataBorder {
329
+ return {
330
+ ...(positions.includes("left") ? { left: [{ $: { style: "thin" }, color: [{ $: { rgb: "00000000" } }] }] } : {}),
331
+ ...(positions.includes("right")
332
+ ? { right: [{ $: { style: "thin" }, color: [{ $: { rgb: "00000000" } }] }] }
333
+ : {}),
334
+ ...(positions.includes("top") ? { top: [{ $: { style: "thin" }, color: [{ $: { rgb: "00000000" } }] }] } : {}),
335
+ ...(positions.includes("bottom")
336
+ ? { bottom: [{ $: { style: "thin" }, color: [{ $: { rgb: "00000000" } }] }] }
337
+ : {}),
338
+ };
339
+ }
340
+
341
+ private _applyBorderPosition(border: ExcelXmlStyleDataBorder, position: ExcelBorderPosition, enabled: boolean): void {
342
+ if (enabled) {
343
+ const existing = border[position];
344
+ if (existing == null) {
345
+ border[position] = [{ $: { style: "thin" }, color: [{ $: { rgb: "00000000" } }] }];
346
+ } else if (existing[0].color == null) {
347
+ existing[0].color = [{ $: { rgb: "00000000" } }];
348
+ } else {
349
+ existing[0].color[0].$.rgb = "00000000";
350
+ }
351
+ } else {
352
+ delete border[position];
353
+ }
354
+ }
355
+
356
+ private _getSameOrCreateXf(xfItem: ExcelXmlStyleDataXf): string {
357
+ const prevSameXf = this.data.styleSheet.cellXfs[0].xf.single((item) => objEqual(item, xfItem));
358
+
359
+ if (prevSameXf != null) {
360
+ return this.data.styleSheet.cellXfs[0].xf.indexOf(prevSameXf).toString();
361
+ } else {
362
+ this.data.styleSheet.cellXfs[0].xf.push(xfItem);
363
+ this.data.styleSheet.cellXfs[0].$.count = this.data.styleSheet.cellXfs[0].xf.length.toString();
364
+ return (this.data.styleSheet.cellXfs[0].xf.length - 1).toString();
365
+ }
366
+ }
367
+
368
+ private _getSameOrCreateFill(fillItem: ExcelXmlStyleDataFill): string {
369
+ const prevSameFill = this.data.styleSheet.fills[0].fill.single((item) => objEqual(item, fillItem));
370
+
371
+ if (prevSameFill != null) {
372
+ return this.data.styleSheet.fills[0].fill.indexOf(prevSameFill).toString();
373
+ } else {
374
+ this.data.styleSheet.fills[0].fill.push(fillItem);
375
+ this.data.styleSheet.fills[0].$.count = this.data.styleSheet.fills[0].fill.length.toString();
376
+ return (this.data.styleSheet.fills[0].fill.length - 1).toString();
377
+ }
378
+ }
379
+
380
+ private _getSameOrCreateBorder(borderItem: ExcelXmlStyleDataBorder): string {
381
+ const prevSameBorder = this.data.styleSheet.borders[0].border.single((item) => objEqual(item, borderItem));
382
+
383
+ if (prevSameBorder != null) {
384
+ return this.data.styleSheet.borders[0].border.indexOf(prevSameBorder).toString();
385
+ } else {
386
+ this.data.styleSheet.borders[0].border.push(borderItem);
387
+ this.data.styleSheet.borders[0].$.count = this.data.styleSheet.borders[0].border.length.toString();
388
+ return (this.data.styleSheet.borders[0].border.length - 1).toString();
389
+ }
390
+ }
391
+
392
+ //#endregion
393
+ }