@niicojs/excel 0.3.0 → 0.3.1

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.
@@ -1,121 +1,121 @@
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 rowNumber = +match[2];
45
- if (rowNumber <= 0) throw new Error(`Invalid cell address: ${address}`);
46
-
47
- const col = letterToCol(match[1].toUpperCase());
48
- const row = rowNumber - 1; // Convert to 0-based
49
- return { row, col };
50
- };
51
-
52
- /**
53
- * Converts row/col indices to an Excel cell address
54
- * @param row - 0-based row index
55
- * @param col - 0-based column index
56
- * @returns Cell address string like 'A1'
57
- */
58
- export const toAddress = (row: number, col: number): string => {
59
- return `${colToLetter(col)}${row + 1}`;
60
- };
61
-
62
- /**
63
- * Parses an Excel range (e.g., 'A1:B10') to start/end addresses
64
- * @param range - Range string
65
- * @returns RangeAddress with start and end
66
- */
67
- export const parseRange = (range: string): RangeAddress => {
68
- const parts = range.split(':');
69
- if (parts.length === 1) {
70
- // Single cell range
71
- const addr = parseAddress(parts[0]);
72
- return { start: addr, end: addr };
73
- }
74
- if (parts.length !== 2) {
75
- throw new Error(`Invalid range: ${range}`);
76
- }
77
- return {
78
- start: parseAddress(parts[0]),
79
- end: parseAddress(parts[1]),
80
- };
81
- };
82
-
83
- /**
84
- * Converts a RangeAddress to a range string
85
- * @param range - RangeAddress object
86
- * @returns Range string like 'A1:B10'
87
- */
88
- export const toRange = (range: RangeAddress): string => {
89
- const start = toAddress(range.start.row, range.start.col);
90
- const end = toAddress(range.end.row, range.end.col);
91
- if (start === end) {
92
- return start;
93
- }
94
- return `${start}:${end}`;
95
- };
96
-
97
- /**
98
- * Normalizes a range so start is always top-left and end is bottom-right
99
- */
100
- export const normalizeRange = (range: RangeAddress): RangeAddress => {
101
- return {
102
- start: {
103
- row: Math.min(range.start.row, range.end.row),
104
- col: Math.min(range.start.col, range.end.col),
105
- },
106
- end: {
107
- row: Math.max(range.start.row, range.end.row),
108
- col: Math.max(range.start.col, range.end.col),
109
- },
110
- };
111
- };
112
-
113
- /**
114
- * Checks if an address is within a range
115
- */
116
- export const isInRange = (addr: CellAddress, range: RangeAddress): boolean => {
117
- const norm = normalizeRange(range);
118
- return (
119
- addr.row >= norm.start.row && addr.row <= norm.end.row && addr.col >= norm.start.col && addr.col <= norm.end.col
120
- );
121
- };
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 rowNumber = +match[2];
45
+ if (rowNumber <= 0) throw new Error(`Invalid cell address: ${address}`);
46
+
47
+ const col = letterToCol(match[1].toUpperCase());
48
+ const row = rowNumber - 1; // Convert to 0-based
49
+ return { row, col };
50
+ };
51
+
52
+ /**
53
+ * Converts row/col indices to an Excel cell address
54
+ * @param row - 0-based row index
55
+ * @param col - 0-based column index
56
+ * @returns Cell address string like 'A1'
57
+ */
58
+ export const toAddress = (row: number, col: number): string => {
59
+ return `${colToLetter(col)}${row + 1}`;
60
+ };
61
+
62
+ /**
63
+ * Parses an Excel range (e.g., 'A1:B10') to start/end addresses
64
+ * @param range - Range string
65
+ * @returns RangeAddress with start and end
66
+ */
67
+ export const parseRange = (range: string): RangeAddress => {
68
+ const parts = range.split(':');
69
+ if (parts.length === 1) {
70
+ // Single cell range
71
+ const addr = parseAddress(parts[0]);
72
+ return { start: addr, end: addr };
73
+ }
74
+ if (parts.length !== 2) {
75
+ throw new Error(`Invalid range: ${range}`);
76
+ }
77
+ return {
78
+ start: parseAddress(parts[0]),
79
+ end: parseAddress(parts[1]),
80
+ };
81
+ };
82
+
83
+ /**
84
+ * Converts a RangeAddress to a range string
85
+ * @param range - RangeAddress object
86
+ * @returns Range string like 'A1:B10'
87
+ */
88
+ export const toRange = (range: RangeAddress): string => {
89
+ const start = toAddress(range.start.row, range.start.col);
90
+ const end = toAddress(range.end.row, range.end.col);
91
+ if (start === end) {
92
+ return start;
93
+ }
94
+ return `${start}:${end}`;
95
+ };
96
+
97
+ /**
98
+ * Normalizes a range so start is always top-left and end is bottom-right
99
+ */
100
+ export const normalizeRange = (range: RangeAddress): RangeAddress => {
101
+ return {
102
+ start: {
103
+ row: Math.min(range.start.row, range.end.row),
104
+ col: Math.min(range.start.col, range.end.col),
105
+ },
106
+ end: {
107
+ row: Math.max(range.start.row, range.end.row),
108
+ col: Math.max(range.start.col, range.end.col),
109
+ },
110
+ };
111
+ };
112
+
113
+ /**
114
+ * Checks if an address is within a range
115
+ */
116
+ export const isInRange = (addr: CellAddress, range: RangeAddress): boolean => {
117
+ const norm = normalizeRange(range);
118
+ return (
119
+ addr.row >= norm.start.row && addr.row <= norm.end.row && addr.col >= norm.start.col && addr.col <= norm.end.col
120
+ );
121
+ };
package/src/utils/xml.ts CHANGED
@@ -1,140 +1,140 @@
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
- };
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
+ };