@niicojs/excel 0.1.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/src/types.ts ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Cell value types - what a cell can contain
3
+ */
4
+ export type CellValue = number | string | boolean | Date | null | CellError;
5
+
6
+ /**
7
+ * Represents an Excel error value
8
+ */
9
+ export interface CellError {
10
+ error: ErrorType;
11
+ }
12
+
13
+ export type ErrorType = '#NULL!' | '#DIV/0!' | '#VALUE!' | '#REF!' | '#NAME?' | '#NUM!' | '#N/A' | '#GETTING_DATA';
14
+
15
+ /**
16
+ * Discriminator for cell content type
17
+ */
18
+ export type CellType = 'number' | 'string' | 'boolean' | 'date' | 'error' | 'empty';
19
+
20
+ /**
21
+ * Style definition for cells
22
+ */
23
+ export interface CellStyle {
24
+ bold?: boolean;
25
+ italic?: boolean;
26
+ underline?: boolean | 'single' | 'double';
27
+ strike?: boolean;
28
+ fontSize?: number;
29
+ fontName?: string;
30
+ fontColor?: string;
31
+ fill?: string;
32
+ border?: BorderStyle;
33
+ alignment?: Alignment;
34
+ numberFormat?: string;
35
+ }
36
+
37
+ export interface BorderStyle {
38
+ top?: BorderType;
39
+ bottom?: BorderType;
40
+ left?: BorderType;
41
+ right?: BorderType;
42
+ }
43
+
44
+ export type BorderType = 'thin' | 'medium' | 'thick' | 'double' | 'dotted' | 'dashed';
45
+
46
+ export interface Alignment {
47
+ horizontal?: 'left' | 'center' | 'right' | 'justify';
48
+ vertical?: 'top' | 'middle' | 'bottom';
49
+ wrapText?: boolean;
50
+ textRotation?: number;
51
+ }
52
+
53
+ /**
54
+ * Cell address with 0-indexed row and column
55
+ */
56
+ export interface CellAddress {
57
+ row: number;
58
+ col: number;
59
+ }
60
+
61
+ /**
62
+ * Range address with start and end cells
63
+ */
64
+ export interface RangeAddress {
65
+ start: CellAddress;
66
+ end: CellAddress;
67
+ }
68
+
69
+ /**
70
+ * Internal cell data representation
71
+ */
72
+ export interface CellData {
73
+ /** Cell type: n=number, s=string (shared), str=inline string, b=boolean, e=error, d=date */
74
+ t?: 'n' | 's' | 'str' | 'b' | 'e' | 'd';
75
+ /** Raw value */
76
+ v?: number | string | boolean;
77
+ /** Formula (without leading =) */
78
+ f?: string;
79
+ /** Style index */
80
+ s?: number;
81
+ /** Formatted text (cached) */
82
+ w?: string;
83
+ /** Number format */
84
+ z?: string;
85
+ /** Array formula range */
86
+ F?: string;
87
+ /** Dynamic array formula flag */
88
+ D?: boolean;
89
+ /** Shared formula index */
90
+ si?: number;
91
+ }
92
+
93
+ /**
94
+ * Sheet definition from workbook.xml
95
+ */
96
+ export interface SheetDefinition {
97
+ name: string;
98
+ sheetId: number;
99
+ rId: string;
100
+ }
101
+
102
+ /**
103
+ * Relationship definition
104
+ */
105
+ export interface Relationship {
106
+ id: string;
107
+ type: string;
108
+ target: string;
109
+ }
110
+
111
+ /**
112
+ * Pivot table aggregation functions
113
+ */
114
+ export type AggregationType = 'sum' | 'count' | 'average' | 'min' | 'max';
115
+
116
+ /**
117
+ * Configuration for a value field in a pivot table
118
+ */
119
+ export interface PivotValueConfig {
120
+ /** Source field name (column header) */
121
+ field: string;
122
+ /** Aggregation function */
123
+ aggregation: AggregationType;
124
+ /** Display name (e.g., "Sum of Sales") */
125
+ name?: string;
126
+ }
127
+
128
+ /**
129
+ * Configuration for creating a pivot table
130
+ */
131
+ export interface PivotTableConfig {
132
+ /** Name of the pivot table */
133
+ name: string;
134
+ /** Source data range with sheet name (e.g., "Sheet1!A1:D100") */
135
+ source: string;
136
+ /** Target cell where pivot table will be placed (e.g., "Sheet2!A3") */
137
+ target: string;
138
+ /** Refresh the pivot table data when the file is opened (default: true) */
139
+ refreshOnLoad?: boolean;
140
+ }
141
+
142
+ /**
143
+ * Internal representation of a pivot cache field
144
+ */
145
+ export interface PivotCacheField {
146
+ /** Field name (from header row) */
147
+ name: string;
148
+ /** Field index (0-based) */
149
+ index: number;
150
+ /** Whether this field contains numbers */
151
+ isNumeric: boolean;
152
+ /** Whether this field contains dates */
153
+ isDate: boolean;
154
+ /** Unique string values (for shared items) */
155
+ sharedItems: string[];
156
+ /** Min numeric value */
157
+ minValue?: number;
158
+ /** Max numeric value */
159
+ maxValue?: number;
160
+ }
161
+
162
+ /**
163
+ * Pivot field axis assignment
164
+ */
165
+ export type PivotFieldAxis = 'row' | 'column' | 'filter' | 'value';
@@ -0,0 +1,118 @@
1
+ import type { CellAddress, RangeAddress } from '../types';
2
+
3
+ /**
4
+ * Converts a column index (0-based) to Excel column letters (A, B, ..., Z, AA, AB, ...)
5
+ * @param col - 0-based column index
6
+ * @returns Column letter(s)
7
+ */
8
+ export const colToLetter = (col: number): string => {
9
+ let result = '';
10
+ let n = col;
11
+ while (n >= 0) {
12
+ result = String.fromCharCode((n % 26) + 65) + result;
13
+ n = Math.floor(n / 26) - 1;
14
+ }
15
+ return result;
16
+ };
17
+
18
+ /**
19
+ * Converts Excel column letters to a 0-based column index
20
+ * @param letters - Column letter(s) like 'A', 'B', 'AA'
21
+ * @returns 0-based column index
22
+ */
23
+ export const letterToCol = (letters: string): number => {
24
+ const upper = letters.toUpperCase();
25
+ let col = 0;
26
+ for (let i = 0; i < upper.length; i++) {
27
+ col = col * 26 + (upper.charCodeAt(i) - 64);
28
+ }
29
+ return col - 1;
30
+ };
31
+
32
+ /**
33
+ * Parses an Excel cell address (e.g., 'A1', '$B$2') to row/col indices
34
+ * @param address - Cell address string
35
+ * @returns CellAddress with 0-based row and col
36
+ */
37
+ export const parseAddress = (address: string): CellAddress => {
38
+ // Remove $ signs for absolute references
39
+ const clean = address.replace(/\$/g, '');
40
+ const match = clean.match(/^([A-Z]+)(\d+)$/i);
41
+ if (!match) {
42
+ throw new Error(`Invalid cell address: ${address}`);
43
+ }
44
+ const col = letterToCol(match[1].toUpperCase());
45
+ const row = parseInt(match[2], 10) - 1; // Convert to 0-based
46
+ return { row, col };
47
+ };
48
+
49
+ /**
50
+ * Converts row/col indices to an Excel cell address
51
+ * @param row - 0-based row index
52
+ * @param col - 0-based column index
53
+ * @returns Cell address string like 'A1'
54
+ */
55
+ export const toAddress = (row: number, col: number): string => {
56
+ return `${colToLetter(col)}${row + 1}`;
57
+ };
58
+
59
+ /**
60
+ * Parses an Excel range (e.g., 'A1:B10') to start/end addresses
61
+ * @param range - Range string
62
+ * @returns RangeAddress with start and end
63
+ */
64
+ export const parseRange = (range: string): RangeAddress => {
65
+ const parts = range.split(':');
66
+ if (parts.length === 1) {
67
+ // Single cell range
68
+ const addr = parseAddress(parts[0]);
69
+ return { start: addr, end: addr };
70
+ }
71
+ if (parts.length !== 2) {
72
+ throw new Error(`Invalid range: ${range}`);
73
+ }
74
+ return {
75
+ start: parseAddress(parts[0]),
76
+ end: parseAddress(parts[1]),
77
+ };
78
+ };
79
+
80
+ /**
81
+ * Converts a RangeAddress to a range string
82
+ * @param range - RangeAddress object
83
+ * @returns Range string like 'A1:B10'
84
+ */
85
+ export const toRange = (range: RangeAddress): string => {
86
+ const start = toAddress(range.start.row, range.start.col);
87
+ const end = toAddress(range.end.row, range.end.col);
88
+ if (start === end) {
89
+ return start;
90
+ }
91
+ return `${start}:${end}`;
92
+ };
93
+
94
+ /**
95
+ * Normalizes a range so start is always top-left and end is bottom-right
96
+ */
97
+ export const normalizeRange = (range: RangeAddress): RangeAddress => {
98
+ return {
99
+ start: {
100
+ row: Math.min(range.start.row, range.end.row),
101
+ col: Math.min(range.start.col, range.end.col),
102
+ },
103
+ end: {
104
+ row: Math.max(range.start.row, range.end.row),
105
+ col: Math.max(range.start.col, range.end.col),
106
+ },
107
+ };
108
+ };
109
+
110
+ /**
111
+ * Checks if an address is within a range
112
+ */
113
+ export const isInRange = (addr: CellAddress, range: RangeAddress): boolean => {
114
+ const norm = normalizeRange(range);
115
+ return (
116
+ addr.row >= norm.start.row && addr.row <= norm.end.row && addr.col >= norm.start.col && addr.col <= norm.end.col
117
+ );
118
+ };
@@ -0,0 +1,147 @@
1
+ import { XMLParser, XMLBuilder } from 'fast-xml-parser';
2
+
3
+ // Parser options that preserve structure and attributes
4
+ const parserOptions = {
5
+ ignoreAttributes: false,
6
+ attributeNamePrefix: '@_',
7
+ textNodeName: '#text',
8
+ preserveOrder: true,
9
+ commentPropName: '#comment',
10
+ cdataPropName: '#cdata',
11
+ trimValues: false,
12
+ parseTagValue: false,
13
+ parseAttributeValue: false,
14
+ };
15
+
16
+ // Builder options matching parser for round-trip compatibility
17
+ const builderOptions = {
18
+ ignoreAttributes: false,
19
+ attributeNamePrefix: '@_',
20
+ textNodeName: '#text',
21
+ preserveOrder: true,
22
+ commentPropName: '#comment',
23
+ cdataPropName: '#cdata',
24
+ format: false,
25
+ suppressEmptyNode: false,
26
+ suppressBooleanAttributes: false,
27
+ };
28
+
29
+ const parser = new XMLParser(parserOptions);
30
+ const builder = new XMLBuilder(builderOptions);
31
+
32
+ /**
33
+ * Parses an XML string into a JavaScript object
34
+ * Preserves element order and attributes for round-trip compatibility
35
+ */
36
+ export const parseXml = (xml: string): XmlNode[] => {
37
+ return parser.parse(xml);
38
+ };
39
+
40
+ /**
41
+ * Converts a JavaScript object back to an XML string
42
+ */
43
+ export const stringifyXml = (obj: XmlNode[]): string => {
44
+ return builder.build(obj);
45
+ };
46
+
47
+ /**
48
+ * XML node type from fast-xml-parser with preserveOrder
49
+ * Each node is an object with a single key (the tag name)
50
+ * containing an array of child nodes, plus optional :@
51
+ * for attributes
52
+ */
53
+ export interface XmlNode {
54
+ [tagName: string]: XmlNode[] | string | Record<string, string> | undefined;
55
+ }
56
+
57
+ /**
58
+ * Finds the first element with the given tag name in the XML tree
59
+ */
60
+ export const findElement = (nodes: XmlNode[], tagName: string): XmlNode | undefined => {
61
+ for (const node of nodes) {
62
+ if (tagName in node) {
63
+ return node;
64
+ }
65
+ }
66
+ return undefined;
67
+ };
68
+
69
+ /**
70
+ * Finds all elements with the given tag name (immediate children only)
71
+ */
72
+ export const findElements = (nodes: XmlNode[], tagName: string): XmlNode[] => {
73
+ return nodes.filter((node) => tagName in node);
74
+ };
75
+
76
+ /**
77
+ * Gets the children of an element
78
+ */
79
+ export const getChildren = (node: XmlNode, tagName: string): XmlNode[] => {
80
+ const children = node[tagName];
81
+ if (Array.isArray(children)) {
82
+ return children;
83
+ }
84
+ return [];
85
+ };
86
+
87
+ /**
88
+ * Gets an attribute value from a node
89
+ */
90
+ export const getAttr = (node: XmlNode, name: string): string | undefined => {
91
+ const attrs = node[':@'] as Record<string, string> | undefined;
92
+ return attrs?.[`@_${name}`];
93
+ };
94
+
95
+ /**
96
+ * Sets an attribute value on a node
97
+ */
98
+ export const setAttr = (node: XmlNode, name: string, value: string): void => {
99
+ if (!node[':@']) {
100
+ node[':@'] = {};
101
+ }
102
+ (node[':@'] as Record<string, string>)[`@_${name}`] = value;
103
+ };
104
+
105
+ /**
106
+ * Gets the text content of a node
107
+ */
108
+ export const getText = (node: XmlNode, tagName: string): string | undefined => {
109
+ const children = getChildren(node, tagName);
110
+ for (const child of children) {
111
+ if ('#text' in child) {
112
+ return child['#text'] as string;
113
+ }
114
+ }
115
+ return undefined;
116
+ };
117
+
118
+ /**
119
+ * Creates a new XML element
120
+ */
121
+ export const createElement = (tagName: string, attrs?: Record<string, string>, children?: XmlNode[]): XmlNode => {
122
+ const node: XmlNode = {
123
+ [tagName]: children || [],
124
+ };
125
+ if (attrs && Object.keys(attrs).length > 0) {
126
+ const attrObj: Record<string, string> = {};
127
+ for (const [key, value] of Object.entries(attrs)) {
128
+ attrObj[`@_${key}`] = value;
129
+ }
130
+ node[':@'] = attrObj;
131
+ }
132
+ return node;
133
+ };
134
+
135
+ /**
136
+ * Creates a text node
137
+ */
138
+ export const createText = (text: string): XmlNode => {
139
+ return { '#text': text } as unknown as XmlNode;
140
+ };
141
+
142
+ /**
143
+ * Adds XML declaration to the start of an XML string
144
+ */
145
+ export const addXmlDeclaration = (xml: string): string => {
146
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${xml}`;
147
+ };
@@ -0,0 +1,61 @@
1
+ import { unzip, zip, strFromU8, strToU8 } from 'fflate';
2
+
3
+ export type ZipFiles = Map<string, Uint8Array>;
4
+
5
+ /**
6
+ * Reads a ZIP file and returns a map of path -> content
7
+ * @param data - ZIP file as Uint8Array
8
+ * @returns Promise resolving to a map of file paths to contents
9
+ */
10
+ export const readZip = (data: Uint8Array): Promise<ZipFiles> => {
11
+ return new Promise((resolve, reject) => {
12
+ unzip(data, (err, result) => {
13
+ if (err) {
14
+ reject(err);
15
+ return;
16
+ }
17
+ const files = new Map<string, Uint8Array>();
18
+ for (const [path, content] of Object.entries(result)) {
19
+ files.set(path, content);
20
+ }
21
+ resolve(files);
22
+ });
23
+ });
24
+ };
25
+
26
+ /**
27
+ * Creates a ZIP file from a map of path -> content
28
+ * @param files - Map of file paths to contents
29
+ * @returns Promise resolving to ZIP file as Uint8Array
30
+ */
31
+ export const writeZip = (files: ZipFiles): Promise<Uint8Array> => {
32
+ return new Promise((resolve, reject) => {
33
+ const zipData: Record<string, Uint8Array> = {};
34
+ for (const [path, content] of files) {
35
+ zipData[path] = content;
36
+ }
37
+ zip(zipData, (err, result) => {
38
+ if (err) {
39
+ reject(err);
40
+ return;
41
+ }
42
+ resolve(result);
43
+ });
44
+ });
45
+ };
46
+
47
+ /**
48
+ * Reads a file from the ZIP as a UTF-8 string
49
+ */
50
+ export const readZipText = (files: ZipFiles, path: string): string | undefined => {
51
+ const data = files.get(path);
52
+ if (!data) return undefined;
53
+ return strFromU8(data);
54
+ };
55
+
56
+ /**
57
+ * Writes a UTF-8 string to the ZIP files map
58
+ */
59
+ export const writeZipText = (files: ZipFiles, path: string, content: string): void => {
60
+ files.set(path, strToU8(content));
61
+ };