react-native-nitro-xlsx 0.1.1 → 0.1.4

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/NOTICE CHANGED
@@ -4,7 +4,8 @@ This software uses third-party open source components listed below, with full li
4
4
  ================================================================================
5
5
  1. OpenXLSX
6
6
  Repository: https://github.com/troldal/OpenXLSX
7
- License: BSD 3-Clause License
7
+ Version: 0.5.1
8
+ License: BSD 3-Clause License (SPDX: BSD-3-Clause)
8
9
  Copyright (c) 2020 Kenneth Troldal Balslev
9
10
 
10
11
  Full BSD 3-Clause License text:
@@ -27,33 +28,66 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
27
28
  2. react-native-nitro-modules
28
29
  Repository: https://github.com/mrousavy/nitro
29
30
  Version: 0.58.3
30
- License: MIT License
31
+ License: MIT License (SPDX: MIT)
31
32
  Copyright (c) 2024 Marc Rousavy
32
33
 
34
+ Full MIT License text:
35
+
36
+ MIT License
37
+
38
+ Copyright (c) 2024 Marc Rousavy
39
+ Permission is hereby granted, free of charge, to any person obtaining a copy
40
+ of this software and associated documentation files (the "Software"), to deal
41
+ in the Software without restriction, including without limitation the rights
42
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
43
+ copies of the Software, and to permit persons to whom the Software is
44
+ furnished to do so, subject to the following conditions:
45
+
46
+ The above copyright notice and this permission notice shall be included in all
47
+ copies or substantial portions of the Software.
48
+
49
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
50
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
51
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
52
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
53
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
54
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
55
+ SOFTWARE.
56
+
33
57
  ================================================================================
34
58
  3. pugixml (OpenXLSX dependency)
35
59
  Repository: https://github.com/zeux/pugixml
36
- License: MIT License
60
+ Version: 1.16
61
+ License: MIT License (SPDX: MIT)
37
62
  Copyright (c) 2006-2024 Arseny Kapoulkine
38
63
 
39
- ================================================================================
40
- 4. Zippy (OpenXLSX dependency)
41
- Repository: https://github.com/troldal/Zippy
42
- License: MIT License
43
- Copyright (c) 2019 Kenneth Troldal Balslev
64
+ Full MIT License text:
65
+
66
+ MIT License
67
+
68
+ Copyright (c) 2006-2026 Arseny Kapoulkine
69
+
70
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
71
+
72
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
73
+
74
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
44
75
 
45
76
  ================================================================================
46
- 5. miniz (OpenXLSX dependency)
77
+ 4. miniz (OpenXLSX dependency)
47
78
  Repository: https://github.com/richgel999/miniz
48
- License: MIT License
79
+ Version: 3.1.2
80
+ License: MIT License (SPDX: MIT)
49
81
  Copyright (c) 2013-2014 RAD Game Tools and Valve Software
50
82
  Copyright (c) 2010-2014 Rich Geldreich and Tenacious Software LLC
51
83
 
52
- Full standard MIT License text:
84
+ Full MIT License text:
53
85
 
54
- MIT License
86
+ Copyright 2013-2014 RAD Game Tools and Valve Software
87
+ Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC
88
+
89
+ All Rights Reserved.
55
90
 
56
- Copyright (c) 2024 Marc Rousavy
57
91
  Permission is hereby granted, free of charge, to any person obtaining a copy
58
92
  of this software and associated documentation files (the "Software"), to deal
59
93
  in the Software without restriction, including without limitation the rights
@@ -61,13 +95,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
61
95
  copies of the Software, and to permit persons to whom the Software is
62
96
  furnished to do so, subject to the following conditions:
63
97
 
64
- The above copyright notice and this permission notice shall be included in all
65
- copies or substantial portions of the Software.
98
+ The above copyright notice and this permission notice shall be included in
99
+ all copies or substantial portions of the Software.
66
100
 
67
101
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
68
102
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
69
103
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
70
104
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
71
105
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
72
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
73
- SOFTWARE.
106
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
107
+ THE SOFTWARE.
package/NitroXlsx.podspec CHANGED
@@ -11,9 +11,13 @@ Pod::Spec.new do |s|
11
11
  s.authors = package["author"]
12
12
 
13
13
  s.platforms = { :ios => "15.1" }
14
- s.source = { :git => "", :tag => "#{s.version}" }
14
+ s.source = { :git => "https://github.com/zydbkqf/react-native-nitro-xlsx.git", :tag => "#{s.version}" }
15
15
 
16
- s.exclude_files = "cpp/tests/**/*"
16
+ s.exclude_files = [
17
+ "cpp/tests/**/*",
18
+ "OpenXLSX/ThirdParty/miniz/tests/**/*",
19
+ "OpenXLSX/ThirdParty/miniz/examples/**/*",
20
+ ]
17
21
 
18
22
  s.source_files = [
19
23
  "cpp/**/*.{h,hpp,cpp}",
@@ -49,61 +53,20 @@ Pod::Spec.new do |s|
49
53
  "HEADER_SEARCH_PATHS" => "$(inherited) ${PODS_TARGET_SRCROOT}/OpenXLSX ${PODS_TARGET_SRCROOT}/OpenXLSX/OpenXLSX ${PODS_TARGET_SRCROOT}/OpenXLSX/OpenXLSX/headers ${PODS_TARGET_SRCROOT}/OpenXLSX/ThirdParty/pugixml/src ${PODS_TARGET_SRCROOT}/OpenXLSX/ThirdParty/miniz",
50
54
  }
51
55
 
52
- s.prepare_command = <<-CMD
53
- if [ ! -d "OpenXLSX" ]; then
54
- git clone --depth 1 --branch v0.5.1 https://github.com/troldal/OpenXLSX.git
55
- fi
56
- mkdir -p OpenXLSX/ThirdParty
57
- if [ ! -d "OpenXLSX/ThirdParty/pugixml" ]; then
58
- git clone --depth 1 --branch v1.15 https://github.com/zeux/pugixml.git OpenXLSX/ThirdParty/pugixml
59
- fi
60
- if [ ! -d "OpenXLSX/ThirdParty/miniz" ]; then
61
- git clone --depth 1 --branch 3.0.2 https://github.com/richgel999/miniz.git OpenXLSX/ThirdParty/miniz
62
- fi
63
- if [ ! -f "OpenXLSX/ThirdParty/miniz/miniz_export.h" ]; then
64
- cat > OpenXLSX/ThirdParty/miniz/miniz_export.h << 'EOF'
65
- #ifndef MINIZ_EXPORT_H
66
- #define MINIZ_EXPORT_H
67
-
68
- #ifndef MINIZ_EXPORT
69
- #define MINIZ_EXPORT
70
- #endif
71
-
72
- #ifndef MINIZ_DEPRECATED
73
- #define MINIZ_DEPRECATED
74
- #endif
75
-
76
- #ifndef MINIZ_DEPRECATED_EXPORT
77
- #define MINIZ_DEPRECATED_EXPORT MINIZ_EXPORT MINIZ_DEPRECATED
78
- #endif
79
-
80
- #ifndef MINIZ_NO_EXPORT
81
- #define MINIZ_NO_EXPORT
82
- #endif
83
-
84
- #endif
85
- EOF
86
- fi
87
- if [ ! -f "OpenXLSX/OpenXLSX/headers/OpenXLSX-Exports.hpp" ]; then
88
- cat > OpenXLSX/OpenXLSX/headers/OpenXLSX-Exports.hpp << 'EOF'
89
- #ifndef OPENXLSX_EXPORTS_HPP
90
- #define OPENXLSX_EXPORTS_HPP
91
-
92
- #ifndef OPENXLSX_EXPORT
93
- #define OPENXLSX_EXPORT
94
- #endif
95
-
96
- #ifndef OPENXLSX_HIDDEN
97
- #define OPENXLSX_HIDDEN
98
- #endif
99
-
100
- #endif
101
- EOF
102
- fi
103
- if [ ! -d "OpenXLSX/OpenXLSX/headers/OpenXLSX" ]; then
104
- cd OpenXLSX/OpenXLSX/headers && ln -sf . OpenXLSX && cd -
105
- fi
106
- CMD
56
+ # Prepare OpenXLSX and its dependencies (pugixml, miniz) via a Node script
57
+ # so the same logic can be reused from both CocoaPods and Android CMake.
58
+ s.prepare_command = "node #{__dir__}/scripts/prepare-openxlsx.js"
59
+
60
+ # Swift Package Manager declaration for React Native 0.87+.
61
+ # This exposes NitroXlsx as a local Swift package; the heavy lifting for
62
+ # OpenXLSX is still done by the prepare command above.
63
+ if ENV['NITRO_XLSX_USE_SPM'] == '1' && defined?(SPM) && SPM.respond_to?(:dependency)
64
+ SPM.dependency(s,
65
+ url: File.join(__dir__, "."),
66
+ requirement: { kind: 'upToNextMajorVersion', minimumVersion: s.version.to_s },
67
+ products: ['NitroXlsx']
68
+ )
69
+ end
107
70
 
108
71
  load 'nitrogen/generated/ios/NitroXlsx+autolinking.rb'
109
72
  add_nitrogen_files(s)
package/Package.swift ADDED
@@ -0,0 +1,31 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ // NOTE: This Package.swift is a Swift Package Manager manifest stub for
5
+ // React Native 0.87+ compatibility. The library is still primarily built
6
+ // through CocoaPods (NitroXlsx.podspec), where OpenXLSX, pugixml and miniz
7
+ // are cloned and compiled from source by scripts/prepare-openxlsx.js.
8
+ //
9
+ // The local package reference below lets RN 0.87's spm_dependency helper
10
+ // register NitroXlsx in the Pods project when SwiftPM mode is enabled.
11
+ // Full SPM-based compilation of the C++ sources is not yet supported.
12
+
13
+ let package = Package(
14
+ name: "NitroXlsx",
15
+ platforms: [
16
+ .iOS(.v15)
17
+ ],
18
+ products: [
19
+ .library(
20
+ name: "NitroXlsx",
21
+ targets: ["NitroXlsx"]
22
+ )
23
+ ],
24
+ targets: [
25
+ .target(
26
+ name: "NitroXlsx",
27
+ path: "ios",
28
+ sources: ["dummy.swift"]
29
+ )
30
+ ]
31
+ )
package/README.md CHANGED
@@ -22,6 +22,8 @@ npm install react-native-nitro-xlsx
22
22
  yarn add react-native-nitro-xlsx
23
23
  ```
24
24
 
25
+ > **Note**: Native dependencies (OpenXlsx) are downloaded from GitHub during the first build. Please make sure you have a stable network connection.
26
+
25
27
  ### iOS
26
28
 
27
29
  ```sh
@@ -34,6 +36,8 @@ No additional setup needed.
34
36
 
35
37
  ## Usage
36
38
 
39
+ > **Note**: Row and column indices are **1-based** (following Excel convention).
40
+
37
41
  ### Creating a Workbook
38
42
 
39
43
  ```typescript
@@ -53,27 +57,27 @@ headerFormat.setFgColor(Colors.BLUE);
53
57
  headerFormat.setAlign(Align.CENTER);
54
58
 
55
59
  // Write headers
56
- sheet.writeString(0, 0, 'Name', headerFormat);
57
- sheet.writeString(0, 1, 'Price', headerFormat);
58
- sheet.writeString(0, 2, 'Total', headerFormat);
60
+ sheet.writeString(1, 1, 'Name', headerFormat);
61
+ sheet.writeString(1, 2, 'Price', headerFormat);
62
+ sheet.writeString(1, 3, 'Total', headerFormat);
59
63
 
60
64
  // Write data
61
- sheet.writeString(1, 0, 'Apple');
62
- sheet.writeNumber(1, 1, 1.50);
63
- sheet.writeFormula(1, 2, '=B2*1.1');
65
+ sheet.writeString(2, 1, 'Apple');
66
+ sheet.writeNumber(2, 2, 1.50);
67
+ sheet.writeFormula(2, 3, '=B2*1.1');
64
68
 
65
- sheet.writeString(2, 0, 'Orange');
66
- sheet.writeNumber(2, 1, 2.00);
67
- sheet.writeFormula(2, 2, '=B3*1.1');
69
+ sheet.writeString(3, 1, 'Orange');
70
+ sheet.writeNumber(3, 2, 2.00);
71
+ sheet.writeFormula(3, 3, '=B3*1.1');
68
72
 
69
73
  // Date example
70
74
  const date = new Date(2024, 0, 15);
71
75
  const excelDate = (date.getTime() / 86400000) + 25569; // Convert to Excel date
72
- sheet.writeDatetime(3, 0, excelDate);
76
+ sheet.writeDatetime(4, 1, excelDate);
73
77
 
74
78
  // Set column widths
75
- sheet.setColumn(0, 0, 15);
76
- sheet.setColumn(1, 2, 12);
79
+ sheet.setColumn(1, 1, 15);
80
+ sheet.setColumn(2, 3, 12);
77
81
 
78
82
  // Get the buffer and do something with it
79
83
  const buffer = await workbook.getBuffer();
@@ -104,8 +108,8 @@ const lastRow = sheet.getLastRow();
104
108
  const lastCol = sheet.getLastColumn();
105
109
 
106
110
  // Read cell values
107
- for (let row = 0; row <= lastRow; row++) {
108
- for (let col = 0; col <= lastCol; col++) {
111
+ for (let row = 1; row <= lastRow; row++) {
112
+ for (let col = 1; col <= lastCol; col++) {
109
113
  const value = sheet.getCellValue(row, col);
110
114
  const type = sheet.getCellType(row, col);
111
115
  const strValue = sheet.getCellString(row, col);
@@ -114,19 +118,61 @@ for (let row = 0; row <= lastRow; row++) {
114
118
  }
115
119
 
116
120
  // Read cell format
117
- const format = sheet.getCellFormat(0, 0);
121
+ const format = sheet.getCellFormat(1, 1);
118
122
  const isBold = format.getIsBold();
119
123
  const fontName = format.getFontName();
120
124
  const fontSize = format.getFontSize();
121
125
  ```
122
126
 
127
+ ### Working with JSON
128
+
129
+ #### Export to JSON (`toJSON`)
130
+
131
+ Convert worksheet data to a JSON array. By default, the first row is used as object keys.
132
+
133
+ ```typescript
134
+ import { NitroXlsx, toJSON } from 'react-native-nitro-xlsx';
135
+
136
+ const workbook = NitroXlsx.openWorkbook('/path/to/data.xlsx');
137
+
138
+ // Use the first row as keys (default)
139
+ const data = toJSON(workbook);
140
+ // Result: [{ "Name": "Alice", "Age": 30 }, { "Name": "Bob", "Age": 25 }]
141
+
142
+ // Provide custom keys
143
+ const data = toJSON(workbook, ['firstName', 'yearsOld']);
144
+ // Result: [{ "firstName": "Alice", "yearsOld": 30 }, { "firstName": "Bob", "yearsOld": 25 }]
145
+ ```
146
+
147
+ #### Import from JSON (`fromJSON`)
148
+
149
+ Create a workbook from a JSON array. The first object's keys become the header row.
150
+
151
+ ```typescript
152
+ import { NitroXlsx } from 'react-native-nitro-xlsx';
153
+
154
+ const data = [
155
+ { Name: 'Alice', Age: 30, Active: true },
156
+ { Name: 'Bob', Age: 25, Active: false },
157
+ ];
158
+
159
+ const workbook = NitroXlsx.fromJSON(data);
160
+ // The workbook now has a worksheet with:
161
+ // Row 1 (header): Name | Age | Active
162
+ // Row 2: Alice | 30 | true
163
+ // Row 3: Bob | 25 | false
164
+
165
+ const buffer = await workbook.getBuffer();
166
+ ```
167
+
123
168
  ## API
124
169
 
125
170
  ### NitroXlsx
126
171
 
127
- - `createWorkbook(): XlsxWorkbook` - Create a new workbook
172
+ - `createWorkbook(tempDir?: string): XlsxWorkbook` - Create a new workbook. On Android, pass the app's cache directory (e.g., `RNFS.CachesDirectoryPath`) to avoid sandbox write permission issues.
128
173
  - `openWorkbook(path: string): XlsxWorkbook` - Open workbook from file path
129
174
  - `openWorkbookFromBuffer(buffer: ArrayBuffer): XlsxWorkbook` - Open workbook from buffer
175
+ - `fromJSON(data: Array<Record<string, string | number>>): XlsxWorkbook` - Create a workbook from a JSON array (static method)
130
176
 
131
177
  ### XlsxWorkbook
132
178
 
@@ -137,9 +183,12 @@ const fontSize = format.getFontSize();
137
183
  - `getWorksheetCount(): number` - Get number of worksheets
138
184
  - `addCellFormat(): XlsxCellFormat` - Add a new cell format
139
185
  - `getBuffer(): Promise<ArrayBuffer>` - Generate and return the XLSX file as a buffer
186
+ - `toJSON(keys?: string[]): Array<Record<string, string | number>>` - Convert worksheet data to a JSON array
140
187
 
141
188
  ### XlsxWorksheet
142
189
 
190
+ > **Note**: All row and column indices are **1-based** (following Excel convention).
191
+
143
192
  #### Basic Write Methods
144
193
  | Method | Description |
145
194
  |--------|-------------|
@@ -5,18 +5,31 @@ set(CMAKE_CXX_STANDARD 20)
5
5
  set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
6
 
7
7
  set(OPENXLSX_DIR ${CMAKE_SOURCE_DIR}/../OpenXLSX)
8
+
9
+ # Prepare OpenXLSX and its dependencies (pugixml, miniz) using the same
10
+ # Node script that CocoaPods' prepare_command runs on iOS.
11
+ find_program(NODE_EXECUTABLE node REQUIRED)
12
+ execute_process(
13
+ COMMAND ${NODE_EXECUTABLE} ${CMAKE_SOURCE_DIR}/../scripts/prepare-openxlsx.js
14
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/..
15
+ RESULT_VARIABLE PREPARE_RESULT
16
+ )
17
+ if(NOT PREPARE_RESULT EQUAL 0)
18
+ message(FATAL_ERROR
19
+ "Failed to prepare OpenXLSX dependencies.\n"
20
+ "Please ensure Node.js is installed and run:\n"
21
+ " node scripts/prepare-openxlsx.js"
22
+ )
23
+ endif()
24
+
25
+ # Verify OpenXLSX is available
8
26
  if(NOT EXISTS ${OPENXLSX_DIR}/CMakeLists.txt)
9
- include(FetchContent)
10
- FetchContent_Declare(
11
- OpenXLSX
12
- GIT_REPOSITORY https://github.com/troldal/OpenXLSX.git
13
- GIT_TAG v0.5.1
27
+ message(FATAL_ERROR
28
+ "OpenXLSX not found at ${OPENXLSX_DIR}.\n"
29
+ "Please manually clone OpenXLSX:\n"
30
+ " cd ${CMAKE_SOURCE_DIR}/.. && git clone --depth 1 --branch v0.5.1 https://github.com/troldal/OpenXLSX.git OpenXLSX\n"
31
+ "Then clean CMake cache and rebuild."
14
32
  )
15
- FetchContent_GetProperties(OpenXLSX)
16
- if(NOT OpenXLSX_POPULATED)
17
- FetchContent_Populate(OpenXLSX)
18
- endif()
19
- set(OPENXLSX_DIR ${openxlsx_SOURCE_DIR})
20
33
  endif()
21
34
 
22
35
  set(OPENXLSX_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
@@ -4,6 +4,11 @@ buildscript {
4
4
  }
5
5
  }
6
6
 
7
+ def reactNativeArchitectures() {
8
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
9
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
10
+ }
11
+
7
12
  apply plugin: 'com.android.library'
8
13
  apply plugin: 'kotlin-android'
9
14
 
@@ -16,6 +21,7 @@ android {
16
21
  externalNativeBuild {
17
22
  cmake {
18
23
  arguments "-DANDROID_STL=c++_shared"
24
+ abiFilters (*reactNativeArchitectures())
19
25
  }
20
26
  }
21
27
  }
@@ -12,6 +12,7 @@ class NitroXlsxPackage : ReactPackage {
12
12
  }
13
13
  }
14
14
 
15
+ @Suppress("OVERRIDE_DEPRECATION")
15
16
  override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
16
17
  return emptyList()
17
18
  }
@@ -1,5 +1,98 @@
1
1
  #include "HybridNitroXlsx.hpp"
2
2
  #include <fstream>
3
+ #include <stdexcept>
4
+ #include <cstring>
5
+ #include <cstdlib>
6
+
7
+ namespace {
8
+
9
+ struct JsonParser {
10
+ const char* p;
11
+ explicit JsonParser(const char* s) : p(s) {}
12
+
13
+ void skipWs() { while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') ++p; }
14
+
15
+ char peek() { skipWs(); return *p; }
16
+
17
+ void expect(char c) {
18
+ skipWs();
19
+ if (*p != c) throw std::runtime_error(std::string("Expected '") + c + "' but got '" + *p + "'");
20
+ ++p;
21
+ }
22
+
23
+ std::string parseString() {
24
+ expect('"');
25
+ std::string s;
26
+ while (*p && *p != '"') {
27
+ if (*p == '\\') {
28
+ ++p;
29
+ switch (*p) {
30
+ case '"': s += '"'; break;
31
+ case '\\': s += '\\'; break;
32
+ case '/': s += '/'; break;
33
+ case 'b': s += '\b'; break;
34
+ case 'f': s += '\f'; break;
35
+ case 'n': s += '\n'; break;
36
+ case 'r': s += '\r'; break;
37
+ case 't': s += '\t'; break;
38
+ case 'u': {
39
+ ++p;
40
+ char hex[5] = {};
41
+ std::memcpy(hex, p, 4);
42
+ p += 4;
43
+ unsigned long cp = std::strtoul(hex, nullptr, 16);
44
+ if (cp < 0x80) {
45
+ s += static_cast<char>(cp);
46
+ } else if (cp < 0x800) {
47
+ s += static_cast<char>(0xC0 | (cp >> 6));
48
+ s += static_cast<char>(0x80 | (cp & 0x3F));
49
+ } else {
50
+ s += static_cast<char>(0xE0 | (cp >> 12));
51
+ s += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
52
+ s += static_cast<char>(0x80 | (cp & 0x3F));
53
+ }
54
+ continue;
55
+ }
56
+ default: s += '\\'; s += *p; break;
57
+ }
58
+ } else {
59
+ s += *p;
60
+ }
61
+ ++p;
62
+ }
63
+ if (*p == '"') ++p;
64
+ return s;
65
+ }
66
+
67
+ enum ValueType { T_STRING, T_NUMBER, T_BOOL, T_NULL };
68
+
69
+ struct Value {
70
+ ValueType type;
71
+ std::string str;
72
+ double num = 0;
73
+ bool boolean = false;
74
+ };
75
+
76
+ Value parseValue() {
77
+ skipWs();
78
+ if (*p == '"') return { T_STRING, parseString(), 0, false };
79
+ if (*p == 't') { p += 4; return { T_BOOL, "", 0, true }; }
80
+ if (*p == 'f') { p += 5; return { T_BOOL, "", 0, false }; }
81
+ if (*p == 'n') { p += 4; return { T_NULL, "", 0, false }; }
82
+
83
+ // number
84
+ Value v{ T_NUMBER, "", 0, false };
85
+ const char* start = p;
86
+ if (*p == '-') ++p;
87
+ while (*p >= '0' && *p <= '9') ++p;
88
+ if (*p == '.') { ++p; while (*p >= '0' && *p <= '9') ++p; }
89
+ if (*p == 'e' || *p == 'E') { ++p; if (*p == '+' || *p == '-') ++p; while (*p >= '0' && *p <= '9') ++p; }
90
+ v.num = std::strtod(start, nullptr);
91
+ return v;
92
+ }
93
+ };
94
+
95
+ } // anonymous namespace
3
96
 
4
97
  namespace margelo::nitro::xlsx {
5
98
 
@@ -10,8 +103,8 @@ HybridNitroXlsx::HybridNitroXlsx()
10
103
  HybridNitroXlsx::~HybridNitroXlsx() {
11
104
  }
12
105
 
13
- std::shared_ptr<HybridXlsxWorkbookSpec> HybridNitroXlsx::createWorkbook() {
14
- return std::make_shared<HybridXlsxWorkbook>();
106
+ std::shared_ptr<HybridXlsxWorkbookSpec> HybridNitroXlsx::createWorkbook(const std::optional<std::string>& tempDir) {
107
+ return std::make_shared<HybridXlsxWorkbook>(tempDir);
15
108
  }
16
109
 
17
110
  std::shared_ptr<HybridXlsxWorkbookSpec> HybridNitroXlsx::openWorkbook(const std::string& path) {
@@ -26,4 +119,88 @@ std::shared_ptr<HybridXlsxWorkbookSpec> HybridNitroXlsx::openWorkbookFromBuffer(
26
119
  return workbook;
27
120
  }
28
121
 
122
+ std::shared_ptr<HybridXlsxWorkbookSpec> HybridNitroXlsx::fromJSON(const std::string& data) {
123
+ auto workbook = std::make_shared<HybridXlsxWorkbook>();
124
+ auto sheet = workbook->addWorksheet("Sheet1");
125
+
126
+ JsonParser parser(data.c_str());
127
+ parser.skipWs();
128
+ parser.expect('[');
129
+
130
+ if (parser.peek() == ']') return workbook;
131
+
132
+ // Parse first object to extract keys
133
+ parser.expect('{');
134
+
135
+ std::vector<std::string> keys;
136
+ unsigned int col = 1;
137
+
138
+ while (parser.peek() != '}') {
139
+ std::string key = parser.parseString();
140
+ keys.push_back(key);
141
+ parser.expect(':');
142
+ auto val = parser.parseValue();
143
+
144
+ switch (val.type) {
145
+ case JsonParser::T_STRING:
146
+ sheet->writeString(1, col, val.str, std::nullopt);
147
+ break;
148
+ case JsonParser::T_NUMBER:
149
+ sheet->writeNumber(1, col, val.num, std::nullopt);
150
+ break;
151
+ case JsonParser::T_BOOL:
152
+ sheet->writeBoolean(1, col, val.boolean, std::nullopt);
153
+ break;
154
+ default:
155
+ break;
156
+ }
157
+ ++col;
158
+ if (parser.peek() == ',') ++parser.p;
159
+ }
160
+ parser.expect('}');
161
+
162
+ unsigned int row = 2;
163
+
164
+ while (parser.peek() == ',') {
165
+ ++parser.p;
166
+ parser.expect('{');
167
+
168
+ while (parser.peek() != '}') {
169
+ std::string key = parser.parseString();
170
+ parser.expect(':');
171
+ auto val = parser.parseValue();
172
+
173
+ unsigned int targetCol = 0;
174
+ for (size_t k = 0; k < keys.size(); ++k) {
175
+ if (keys[k] == key) { targetCol = static_cast<unsigned int>(k + 1); break; }
176
+ }
177
+ if (targetCol == 0) {
178
+ keys.push_back(key);
179
+ targetCol = static_cast<unsigned int>(keys.size());
180
+ }
181
+
182
+ switch (val.type) {
183
+ case JsonParser::T_STRING:
184
+ sheet->writeString(row, targetCol, val.str, std::nullopt);
185
+ break;
186
+ case JsonParser::T_NUMBER:
187
+ sheet->writeNumber(row, targetCol, val.num, std::nullopt);
188
+ break;
189
+ case JsonParser::T_BOOL:
190
+ sheet->writeBoolean(row, targetCol, val.boolean, std::nullopt);
191
+ break;
192
+ default:
193
+ break;
194
+ }
195
+
196
+ if (parser.peek() == ',') ++parser.p;
197
+ }
198
+ parser.expect('}');
199
+ ++row;
200
+ }
201
+
202
+ parser.expect(']');
203
+ return workbook;
204
+ }
205
+
29
206
  }
@@ -11,9 +11,10 @@ public:
11
11
  HybridNitroXlsx();
12
12
  ~HybridNitroXlsx() override;
13
13
 
14
- std::shared_ptr<HybridXlsxWorkbookSpec> createWorkbook() override;
14
+ std::shared_ptr<HybridXlsxWorkbookSpec> createWorkbook(const std::optional<std::string>& tempDir = std::nullopt) override;
15
15
  std::shared_ptr<HybridXlsxWorkbookSpec> openWorkbook(const std::string& path) override;
16
16
  std::shared_ptr<HybridXlsxWorkbookSpec> openWorkbookFromBuffer(const std::shared_ptr<ArrayBuffer>& buffer) override;
17
+ std::shared_ptr<HybridXlsxWorkbookSpec> fromJSON(const std::string& data) override;
17
18
  };
18
19
 
19
20
  }
@@ -1,6 +1,7 @@
1
1
  #include "HybridXlsxCellFormat.hpp"
2
2
  #include <sstream>
3
3
  #include <iomanip>
4
+ #include <cctype>
4
5
 
5
6
  namespace margelo::nitro::xlsx {
6
7
 
@@ -10,7 +11,12 @@ static std::string doubleToHexColor(double color) {
10
11
  uint32_t argb = c | 0xFF000000;
11
12
  std::stringstream ss;
12
13
  ss << std::hex << std::setfill('0') << std::setw(8) << argb;
13
- return ss.str();
14
+ std::string result = ss.str();
15
+ // Convert to uppercase (OpenXLSX expects uppercase hex)
16
+ for (char& ch : result) {
17
+ ch = std::toupper(ch);
18
+ }
19
+ return result;
14
20
  }
15
21
 
16
22
  HybridXlsxCellFormat::HybridXlsxCellFormat()
@@ -21,11 +27,23 @@ HybridXlsxCellFormat::~HybridXlsxCellFormat() {
21
27
  }
22
28
 
23
29
  OpenXLSX::XLStyleIndex HybridXlsxCellFormat::applyToDocument(OpenXLSX::XLStyles& styles) {
24
- OpenXLSX::XLCellFormat cellFormat;
30
+ if (_styleIndex.has_value()) {
31
+ return *_styleIndex;
32
+ }
33
+
34
+ // Create cell format in document first to get a valid XML node.
35
+ // OpenXLSX default-constructed style objects have empty XML nodes;
36
+ // setters on empty nodes silently fail. We must create the node in
37
+ // the document first, then modify the live node via operator[].
38
+ OpenXLSX::XLCellFormat emptyCellFormat;
39
+ OpenXLSX::XLStyleIndex cellFormatIndex = styles.cellFormats().create(emptyCellFormat);
40
+ OpenXLSX::XLCellFormat cellFormat = styles.cellFormats()[cellFormatIndex];
25
41
 
26
42
  // Font
27
43
  if (!_fontName.empty() || _fontSize > 0 || _fontColor > 0 || _bold || _italic || _underline > 0 || _strikeout || _fontScript > 0) {
28
- OpenXLSX::XLFont font;
44
+ OpenXLSX::XLFont emptyFont;
45
+ OpenXLSX::XLStyleIndex fontIndex = styles.fonts().create(emptyFont);
46
+ OpenXLSX::XLFont font = styles.fonts()[fontIndex];
29
47
  if (!_fontName.empty()) font.setFontName(_fontName);
30
48
  if (_fontSize > 0) font.setFontSize(static_cast<size_t>(_fontSize));
31
49
  if (_fontColor > 0) font.setFontColor(OpenXLSX::XLColor(doubleToHexColor(_fontColor)));
@@ -34,49 +52,55 @@ OpenXLSX::XLStyleIndex HybridXlsxCellFormat::applyToDocument(OpenXLSX::XLStyles&
34
52
  if (_underline > 0) font.setUnderline(static_cast<OpenXLSX::XLUnderlineStyle>(_underline));
35
53
  if (_strikeout) font.setStrikethrough();
36
54
  if (_fontScript > 0) font.setVertAlign(static_cast<OpenXLSX::XLVerticalAlignRunStyle>(_fontScript));
37
- cellFormat.setFontIndex(styles.fonts().create(font));
55
+ cellFormat.setFontIndex(fontIndex);
38
56
  cellFormat.setApplyFont(true);
39
57
  }
40
58
 
41
59
  // Alignment
42
60
  if (_textWrap || _rotation != 0 || _indent > 0 || _align > 0 || _verticalAlign > 0 || _shrink) {
43
- auto alignment = cellFormat.alignment(true);
44
- if (_align > 0) alignment.setHorizontal(static_cast<OpenXLSX::XLAlignmentStyle>(_align));
45
- if (_verticalAlign > 0) alignment.setVertical(static_cast<OpenXLSX::XLAlignmentStyle>(_verticalAlign));
46
- if (_textWrap) alignment.setWrapText();
47
- if (_rotation != 0) alignment.setTextRotation(static_cast<uint16_t>(_rotation));
48
- if (_indent > 0) alignment.setIndent(static_cast<uint32_t>(_indent));
49
- if (_shrink) alignment.setShrinkToFit();
61
+ if (_align > 0) cellFormat.alignment(true).setHorizontal(static_cast<OpenXLSX::XLAlignmentStyle>(_align));
62
+ if (_verticalAlign > 0) cellFormat.alignment(true).setVertical(static_cast<OpenXLSX::XLAlignmentStyle>(_verticalAlign));
63
+ if (_textWrap) cellFormat.alignment(true).setWrapText();
64
+ if (_rotation != 0) cellFormat.alignment(true).setTextRotation(static_cast<uint16_t>(_rotation));
65
+ if (_indent > 0) cellFormat.alignment(true).setIndent(static_cast<uint32_t>(_indent));
66
+ if (_shrink) cellFormat.alignment(true).setShrinkToFit();
50
67
  cellFormat.setApplyAlignment(true);
51
68
  }
52
69
 
53
70
  // Number format
54
71
  if (!_numFormat.empty()) {
55
- OpenXLSX::XLNumberFormat numFormat;
72
+ OpenXLSX::XLNumberFormat emptyNumFormat;
73
+ OpenXLSX::XLStyleIndex numFmtIndex = styles.numberFormats().create(emptyNumFormat);
74
+ OpenXLSX::XLNumberFormat numFormat = styles.numberFormats()[numFmtIndex];
56
75
  numFormat.setFormatCode(_numFormat);
57
- OpenXLSX::XLStyleIndex numFmtIndex = styles.numberFormats().create(numFormat);
58
- uint32_t numFmtId = styles.numberFormats().numberFormatIdFromIndex(numFmtIndex);
59
- cellFormat.setNumberFormatId(numFmtId);
76
+ // Assign a unique custom numFmtId (>= 164 for user-defined formats)
77
+ static uint32_t nextCustomNumFmtId = 164;
78
+ numFormat.setNumberFormatId(nextCustomNumFmtId++);
79
+ cellFormat.setNumberFormatId(numFormat.numberFormatId());
60
80
  cellFormat.setApplyNumberFormat(true);
61
81
  }
62
82
 
63
83
  // Fill
64
84
  if (_bgColor > 0 || _fgColor > 0 || _pattern > 0) {
65
- OpenXLSX::XLFill fill;
85
+ OpenXLSX::XLFill emptyFill;
86
+ OpenXLSX::XLStyleIndex fillIndex = styles.fills().create(emptyFill);
87
+ OpenXLSX::XLFill fill = styles.fills()[fillIndex];
66
88
  fill.setPatternType(_pattern > 0
67
89
  ? static_cast<OpenXLSX::XLPatternType>(_pattern)
68
90
  : OpenXLSX::XLPatternType::XLPatternSolid);
69
91
  if (_bgColor > 0) fill.setBackgroundColor(OpenXLSX::XLColor(doubleToHexColor(_bgColor)));
70
92
  if (_fgColor > 0) fill.setColor(OpenXLSX::XLColor(doubleToHexColor(_fgColor)));
71
- cellFormat.setFillIndex(styles.fills().create(fill));
93
+ cellFormat.setFillIndex(fillIndex);
72
94
  cellFormat.setApplyFill(true);
73
95
  }
74
96
 
75
97
  // Border
76
98
  if (_border > 0 || _bottom > 0 || _top > 0 || _left > 0 || _right > 0 || _diagonal > 0) {
77
- OpenXLSX::XLBorder border;
99
+ OpenXLSX::XLBorder emptyBorder;
100
+ OpenXLSX::XLStyleIndex borderIndex = styles.borders().create(emptyBorder);
101
+ OpenXLSX::XLBorder border = styles.borders()[borderIndex];
78
102
  // TODO: Implement border styling with OpenXLSX v0.5.1 API
79
- cellFormat.setBorderIndex(styles.borders().create(border));
103
+ cellFormat.setBorderIndex(borderIndex);
80
104
  cellFormat.setApplyBorder(true);
81
105
  }
82
106
 
@@ -86,7 +110,8 @@ OpenXLSX::XLStyleIndex HybridXlsxCellFormat::applyToDocument(OpenXLSX::XLStyles&
86
110
  cellFormat.setApplyProtection(true);
87
111
  }
88
112
 
89
- return styles.cellFormats().create(cellFormat);
113
+ _styleIndex = cellFormatIndex;
114
+ return cellFormatIndex;
90
115
  }
91
116
 
92
117
  void HybridXlsxCellFormat::applyToCell(OpenXLSX::XLCell& cell, OpenXLSX::XLStyleIndex styleIndex) {
@@ -68,6 +68,7 @@ public:
68
68
  double getBgColor() override;
69
69
 
70
70
  private:
71
+ std::optional<OpenXLSX::XLStyleIndex> _styleIndex;
71
72
  std::string _fontName;
72
73
  double _fontSize = 0;
73
74
  double _fontColor = 0;
@@ -4,12 +4,43 @@
4
4
  #include <sstream>
5
5
  #include <cstdlib>
6
6
  #include <unistd.h>
7
+ #include <cmath>
8
+ #include <cstring>
9
+ #include <iomanip>
7
10
 
8
11
  #ifdef __ANDROID__
9
12
  #include <android/api-level.h>
10
13
  #include <sys/stat.h>
11
14
  #endif
12
15
 
16
+ namespace {
17
+
18
+ void writeJsonString(std::ostringstream& out, const std::string& s) {
19
+ out << '"';
20
+ for (char c : s) {
21
+ switch (c) {
22
+ case '"': out << "\\\""; break;
23
+ case '\\': out << "\\\\"; break;
24
+ case '\b': out << "\\b"; break;
25
+ case '\f': out << "\\f"; break;
26
+ case '\n': out << "\\n"; break;
27
+ case '\r': out << "\\r"; break;
28
+ case '\t': out << "\\t"; break;
29
+ default:
30
+ if (static_cast<unsigned char>(c) < 0x20) {
31
+ out << "\\u" << std::hex << std::setw(4) << std::setfill('0')
32
+ << static_cast<int>(static_cast<unsigned char>(c))
33
+ << std::dec;
34
+ } else {
35
+ out << c;
36
+ }
37
+ }
38
+ }
39
+ out << '"';
40
+ }
41
+
42
+ } // anonymous namespace
43
+
13
44
  namespace margelo::nitro::xlsx {
14
45
 
15
46
  namespace {
@@ -40,10 +71,14 @@ namespace {
40
71
  }
41
72
  }
42
73
 
43
- HybridXlsxWorkbook::HybridXlsxWorkbook()
74
+ HybridXlsxWorkbook::HybridXlsxWorkbook(const std::optional<std::string>& tempDir)
44
75
  : HybridObject("XlsxWorkbook"), HybridXlsxWorkbookSpec(), _finalized(false) {
45
76
  _workbook = std::make_unique<OpenXLSX::XLDocument>();
46
- _tempDir = getTempDir();
77
+ if (tempDir.has_value()) {
78
+ _tempDir = *tempDir;
79
+ } else {
80
+ _tempDir = getTempDir();
81
+ }
47
82
  }
48
83
 
49
84
  HybridXlsxWorkbook::~HybridXlsxWorkbook() {
@@ -93,7 +128,8 @@ std::shared_ptr<HybridXlsxWorksheetSpec> HybridXlsxWorkbook::addWorksheet(const
93
128
  sheetName = *name;
94
129
  auto it = _worksheetNames.find(sheetName);
95
130
  if (it != _worksheetNames.end()) {
96
- throw std::runtime_error("Sheet named '" + sheetName + "' already exists");
131
+ // Worksheet already exists (e.g., default Sheet1 created by OpenXLSX), return it
132
+ return it->second;
97
133
  }
98
134
  } else {
99
135
  int idx = 1;
@@ -181,4 +217,85 @@ std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>> HybridXlsxWorkbook::getBu
181
217
  return promise;
182
218
  }
183
219
 
220
+ std::string HybridXlsxWorkbook::toJSON(const std::optional<std::vector<std::string>>& keys) {
221
+ if (_worksheets.empty()) {
222
+ return "[]";
223
+ }
224
+ auto& ws = _worksheets[0];
225
+ auto lastRow = static_cast<unsigned int>(ws->getLastRow());
226
+ auto lastCol = static_cast<unsigned int>(ws->getLastColumn());
227
+ if (lastRow == 0 || lastCol == 0) {
228
+ return "[]";
229
+ }
230
+
231
+ unsigned int dataStartRow = 1;
232
+ std::vector<std::string> resolvedKeys;
233
+
234
+ if (keys.has_value()) {
235
+ resolvedKeys = *keys;
236
+ } else {
237
+ dataStartRow = 2;
238
+ for (unsigned int c = 1; c <= lastCol; ++c) {
239
+ auto val = ws->getCellValue(1, c);
240
+ if (auto* s = std::get_if<std::string>(&val)) {
241
+ resolvedKeys.push_back(*s);
242
+ } else if (auto* d = std::get_if<double>(&val)) {
243
+ resolvedKeys.push_back(std::to_string(static_cast<int>(*d)));
244
+ } else {
245
+ resolvedKeys.push_back("col" + std::to_string(c));
246
+ }
247
+ }
248
+ }
249
+
250
+ if (resolvedKeys.empty()) {
251
+ return "[]";
252
+ }
253
+
254
+ std::ostringstream out;
255
+ out << "[";
256
+
257
+ bool firstRow = true;
258
+
259
+ for (unsigned int r = dataStartRow; r <= lastRow; ++r) {
260
+ bool hasData = false;
261
+ for (unsigned int c = 1; c <= std::min(static_cast<unsigned int>(resolvedKeys.size()), lastCol); ++c) {
262
+ auto cellType = ws->getCellType(r, c);
263
+ if (cellType != CellType::EMPTY) {
264
+ hasData = true;
265
+ break;
266
+ }
267
+ }
268
+ if (!hasData) continue;
269
+
270
+ if (!firstRow) out << ",";
271
+ firstRow = false;
272
+
273
+ out << "{";
274
+ unsigned int keyCount = std::min(static_cast<unsigned int>(resolvedKeys.size()), lastCol);
275
+ for (unsigned int c = 0; c < keyCount; ++c) {
276
+ if (c > 0) out << ",";
277
+ writeJsonString(out, resolvedKeys[c]);
278
+ out << ":";
279
+ auto val = ws->getCellValue(r, c + 1);
280
+ if (auto* s = std::get_if<std::string>(&val)) {
281
+ writeJsonString(out, *s);
282
+ } else if (auto* d = std::get_if<double>(&val)) {
283
+ if (std::isnan(*d) || std::isinf(*d)) {
284
+ out << "null";
285
+ } else {
286
+ out << *d;
287
+ }
288
+ } else if (auto* b = std::get_if<bool>(&val)) {
289
+ out << (*b ? "true" : "false");
290
+ } else {
291
+ out << "null";
292
+ }
293
+ }
294
+ out << "}";
295
+ }
296
+
297
+ out << "]";
298
+ return out.str();
299
+ }
300
+
184
301
  }
@@ -13,7 +13,7 @@ namespace margelo::nitro::xlsx {
13
13
 
14
14
  class HybridXlsxWorkbook : public HybridXlsxWorkbookSpec {
15
15
  public:
16
- HybridXlsxWorkbook();
16
+ HybridXlsxWorkbook(const std::optional<std::string>& tempDir = std::nullopt);
17
17
  ~HybridXlsxWorkbook() override;
18
18
 
19
19
  void openFromFile(const std::string& path);
@@ -26,6 +26,7 @@ public:
26
26
  double getWorksheetCount() override;
27
27
  std::shared_ptr<HybridXlsxCellFormatSpec> addCellFormat() override;
28
28
  std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>> getBuffer() override;
29
+ std::string toJSON(const std::optional<std::vector<std::string>>& keys) override;
29
30
 
30
31
  private:
31
32
  std::unique_ptr<OpenXLSX::XLDocument> _workbook;
@@ -75,6 +75,7 @@ void HybridXlsxWorksheet::writeFormulaBoolean(double row, double col, const std:
75
75
  void HybridXlsxWorksheet::writeArrayFormula(double firstRow, double firstCol, double lastRow, double lastCol, const std::string& formula, const std::optional<std::shared_ptr<HybridXlsxCellFormatSpec>>& format) {
76
76
  OpenXLSX::XLCell cell = _worksheet.cell(static_cast<unsigned int>(firstRow), static_cast<unsigned int>(firstCol));
77
77
  cell.formula() = formula;
78
+ applyFormat(cell, format);
78
79
  }
79
80
 
80
81
  void HybridXlsxWorksheet::writeDatetime(double row, double col, double datetime, const std::optional<std::shared_ptr<HybridXlsxCellFormatSpec>>& format) {
@@ -93,10 +94,33 @@ void HybridXlsxWorksheet::setColumn(double firstCol, double lastCol, double widt
93
94
  for (unsigned int c = static_cast<unsigned int>(firstCol); c <= static_cast<unsigned int>(lastCol); ++c) {
94
95
  _worksheet.column(c).setWidth(static_cast<float>(width));
95
96
  }
97
+ if (format.has_value()) {
98
+ // Apply format to entire column range by setting it on cells in the first row
99
+ // (OpenXLSX does not support column-level style; apply to first row cells as a hint)
100
+ auto hybridFormat = std::dynamic_pointer_cast<HybridXlsxCellFormat>(*format);
101
+ if (hybridFormat) {
102
+ auto styleIndex = hybridFormat->applyToDocument(_doc.styles());
103
+ for (unsigned int c = static_cast<unsigned int>(firstCol); c <= static_cast<unsigned int>(lastCol); ++c) {
104
+ OpenXLSX::XLCell cell = _worksheet.cell(1, c);
105
+ hybridFormat->applyToCell(cell, styleIndex);
106
+ }
107
+ }
108
+ }
96
109
  }
97
110
 
98
111
  void HybridXlsxWorksheet::setRow(double row, double height, const std::optional<std::shared_ptr<HybridXlsxCellFormatSpec>>& format) {
99
112
  _worksheet.row(static_cast<unsigned int>(row)).setHeight(static_cast<float>(height));
113
+ if (format.has_value()) {
114
+ auto hybridFormat = std::dynamic_pointer_cast<HybridXlsxCellFormat>(*format);
115
+ if (hybridFormat) {
116
+ auto styleIndex = hybridFormat->applyToDocument(_doc.styles());
117
+ unsigned int colCount = _worksheet.columnCount();
118
+ for (unsigned int c = 1; c <= colCount; ++c) {
119
+ OpenXLSX::XLCell cell = _worksheet.cell(static_cast<unsigned int>(row), c);
120
+ hybridFormat->applyToCell(cell, styleIndex);
121
+ }
122
+ }
123
+ }
100
124
  }
101
125
 
102
126
  void HybridXlsxWorksheet::mergeRange(double firstRow, double firstCol, double lastRow, double lastCol, const std::string& value, const std::optional<std::shared_ptr<HybridXlsxCellFormatSpec>>& format) {
@@ -104,7 +128,9 @@ void HybridXlsxWorksheet::mergeRange(double firstRow, double firstCol, double la
104
128
  OpenXLSX::XLCellReference bottomRight(static_cast<unsigned int>(lastRow), static_cast<unsigned int>(lastCol));
105
129
  OpenXLSX::XLCellRange range = _worksheet.range(topLeft, bottomRight);
106
130
  _worksheet.mergeCells(range);
107
- _worksheet.cell(static_cast<unsigned int>(firstRow), static_cast<unsigned int>(firstCol)).value() = value;
131
+ OpenXLSX::XLCell cell = _worksheet.cell(static_cast<unsigned int>(firstRow), static_cast<unsigned int>(firstCol));
132
+ cell.value() = value;
133
+ applyFormat(cell, format);
108
134
  }
109
135
 
110
136
  void HybridXlsxWorksheet::mergeRangeNum(double firstRow, double firstCol, double lastRow, double lastCol, double value, const std::optional<std::shared_ptr<HybridXlsxCellFormatSpec>>& format) {
@@ -112,7 +138,9 @@ void HybridXlsxWorksheet::mergeRangeNum(double firstRow, double firstCol, double
112
138
  OpenXLSX::XLCellReference bottomRight(static_cast<unsigned int>(lastRow), static_cast<unsigned int>(lastCol));
113
139
  OpenXLSX::XLCellRange range = _worksheet.range(topLeft, bottomRight);
114
140
  _worksheet.mergeCells(range);
115
- _worksheet.cell(static_cast<unsigned int>(firstRow), static_cast<unsigned int>(firstCol)).value() = value;
141
+ OpenXLSX::XLCell cell = _worksheet.cell(static_cast<unsigned int>(firstRow), static_cast<unsigned int>(firstCol));
142
+ cell.value() = value;
143
+ applyFormat(cell, format);
116
144
  }
117
145
 
118
146
  void HybridXlsxWorksheet::insertImage(double row, double col, const std::string& path, std::optional<double> xOffset, std::optional<double> yOffset, std::optional<double> xScale, std::optional<double> yScale) {
@@ -0,0 +1,2 @@
1
+ // This file exists solely so that SwiftPM has at least one source file
2
+ // for the NitroXlsx target. The actual build is done via CocoaPods.
@@ -17,6 +17,7 @@ namespace margelo::nitro::xlsx {
17
17
  prototype.registerHybridMethod("createWorkbook", &HybridNitroXlsxSpec::createWorkbook);
18
18
  prototype.registerHybridMethod("openWorkbook", &HybridNitroXlsxSpec::openWorkbook);
19
19
  prototype.registerHybridMethod("openWorkbookFromBuffer", &HybridNitroXlsxSpec::openWorkbookFromBuffer);
20
+ prototype.registerHybridMethod("fromJSON", &HybridNitroXlsxSpec::fromJSON);
20
21
  });
21
22
  }
22
23
 
@@ -19,6 +19,7 @@ namespace margelo::nitro::xlsx { class HybridXlsxWorkbookSpec; }
19
19
  #include <memory>
20
20
  #include "HybridXlsxWorkbookSpec.hpp"
21
21
  #include <string>
22
+ #include <optional>
22
23
  #include <NitroModules/ArrayBuffer.hpp>
23
24
 
24
25
  namespace margelo::nitro::xlsx {
@@ -52,9 +53,10 @@ namespace margelo::nitro::xlsx {
52
53
 
53
54
  public:
54
55
  // Methods
55
- virtual std::shared_ptr<HybridXlsxWorkbookSpec> createWorkbook() = 0;
56
+ virtual std::shared_ptr<HybridXlsxWorkbookSpec> createWorkbook(const std::optional<std::string>& tempDir) = 0;
56
57
  virtual std::shared_ptr<HybridXlsxWorkbookSpec> openWorkbook(const std::string& path) = 0;
57
58
  virtual std::shared_ptr<HybridXlsxWorkbookSpec> openWorkbookFromBuffer(const std::shared_ptr<ArrayBuffer>& buffer) = 0;
59
+ virtual std::shared_ptr<HybridXlsxWorkbookSpec> fromJSON(const std::string& data) = 0;
58
60
 
59
61
  protected:
60
62
  // Hybrid Setup
@@ -21,6 +21,7 @@ namespace margelo::nitro::xlsx {
21
21
  prototype.registerHybridMethod("getWorksheetCount", &HybridXlsxWorkbookSpec::getWorksheetCount);
22
22
  prototype.registerHybridMethod("addCellFormat", &HybridXlsxWorkbookSpec::addCellFormat);
23
23
  prototype.registerHybridMethod("getBuffer", &HybridXlsxWorkbookSpec::getBuffer);
24
+ prototype.registerHybridMethod("toJSON", &HybridXlsxWorkbookSpec::toJSON);
24
25
  });
25
26
  }
26
27
 
@@ -25,6 +25,7 @@ namespace margelo::nitro::xlsx { class HybridXlsxCellFormatSpec; }
25
25
  #include "HybridXlsxCellFormatSpec.hpp"
26
26
  #include <NitroModules/ArrayBuffer.hpp>
27
27
  #include <NitroModules/Promise.hpp>
28
+ #include <vector>
28
29
 
29
30
  namespace margelo::nitro::xlsx {
30
31
 
@@ -64,6 +65,7 @@ namespace margelo::nitro::xlsx {
64
65
  virtual double getWorksheetCount() = 0;
65
66
  virtual std::shared_ptr<HybridXlsxCellFormatSpec> addCellFormat() = 0;
66
67
  virtual std::shared_ptr<Promise<std::shared_ptr<ArrayBuffer>>> getBuffer() = 0;
68
+ virtual std::string toJSON(const std::optional<std::vector<std::string>>& keys) = 0;
67
69
 
68
70
  protected:
69
71
  // Hybrid Setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-xlsx",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "Fast XLSX file reading and writing for React Native using OpenXLSX and Nitro Modules",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/zydbkqf/react-native-nitro-xlsx",
@@ -10,6 +10,7 @@
10
10
  "types": "src/index.ts",
11
11
  "source": "src/index.ts",
12
12
  "scripts": {
13
+ "postinstall": "node scripts/prepare-openxlsx.js",
13
14
  "specs": "nitrogen",
14
15
  "test": "jest",
15
16
  "test:cpp": "cd cpp/tests && [ -n \"$OPENXLSX_PATH\" ] || OPENXLSX_PATH=$HOME/OpenXLSX; cmake -B build -DOPENXLSX_PATH=$OPENXLSX_PATH && cmake --build build && cd build && ctest --output-on-failure",
@@ -36,6 +37,8 @@
36
37
  "!cpp/tests",
37
38
  "android",
38
39
  "ios",
40
+ "scripts",
41
+ "Package.swift",
39
42
  "nitro.json",
40
43
  "nitrogen",
41
44
  "NitroXlsx.podspec",
@@ -0,0 +1,109 @@
1
+ import { execSync } from 'child_process';
2
+ import { existsSync, mkdirSync, writeFileSync, symlinkSync, readFileSync } from 'fs';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const rootDir = dirname(__dirname);
9
+
10
+ function run(command, options = {}) {
11
+ console.log(`> ${command}`);
12
+ execSync(command, { stdio: 'inherit', ...options });
13
+ }
14
+
15
+ function ensureDir(dir) {
16
+ if (!existsSync(dir)) {
17
+ mkdirSync(dir, { recursive: true });
18
+ }
19
+ }
20
+
21
+ function cloneRepo(url, tag, dest) {
22
+ if (existsSync(dest)) {
23
+ console.log(`Already exists: ${dest}`);
24
+ return;
25
+ }
26
+ run(`git clone --depth 1 --branch ${tag} ${url} ${dest}`);
27
+ }
28
+
29
+ function writeFileIfNotExists(path, content) {
30
+ if (existsSync(path)) {
31
+ console.log(`Already exists: ${path}`);
32
+ return;
33
+ }
34
+ writeFileSync(path, content);
35
+ console.log(`Created: ${path}`);
36
+ }
37
+
38
+ function createSymlinkIfNotExists(target, link) {
39
+ if (existsSync(link)) {
40
+ console.log(`Already exists: ${link}`);
41
+ return;
42
+ }
43
+ symlinkSync(target, link);
44
+ console.log(`Created symlink: ${link} -> ${target}`);
45
+ }
46
+
47
+ function getPackageVersion() {
48
+ const packageJsonPath = join(rootDir, 'package.json');
49
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
50
+ return packageJson.version || '0.1.0';
51
+ }
52
+
53
+ const openxlsxDir = join(rootDir, 'OpenXLSX');
54
+ const thirdPartyDir = join(openxlsxDir, 'ThirdParty');
55
+ const pugixmlDir = join(thirdPartyDir, 'pugixml');
56
+ const minizDir = join(thirdPartyDir, 'miniz');
57
+
58
+ // Clone OpenXLSX and dependencies
59
+ cloneRepo('https://github.com/troldal/OpenXLSX.git', 'v0.5.1', openxlsxDir);
60
+ ensureDir(thirdPartyDir);
61
+ cloneRepo('https://github.com/zeux/pugixml.git', 'v1.16', pugixmlDir);
62
+ cloneRepo('https://github.com/richgel999/miniz.git', '3.1.2', minizDir);
63
+
64
+ // Generate miniz_export.h
65
+ const minizExportHeader = `#ifndef MINIZ_EXPORT_H
66
+ #define MINIZ_EXPORT_H
67
+
68
+ #ifndef MINIZ_EXPORT
69
+ #define MINIZ_EXPORT
70
+ #endif
71
+
72
+ #ifndef MINIZ_DEPRECATED
73
+ #define MINIZ_DEPRECATED
74
+ #endif
75
+
76
+ #ifndef MINIZ_DEPRECATED_EXPORT
77
+ #define MINIZ_DEPRECATED_EXPORT MINIZ_EXPORT MINIZ_DEPRECATED
78
+ #endif
79
+
80
+ #ifndef MINIZ_NO_EXPORT
81
+ #define MINIZ_NO_EXPORT
82
+ #endif
83
+
84
+ #endif
85
+ `;
86
+ writeFileIfNotExists(join(minizDir, 'miniz_export.h'), minizExportHeader);
87
+
88
+ // Generate OpenXLSX-Exports.hpp
89
+ const openxlsxExportsHeader = `#ifndef OPENXLSX_EXPORTS_HPP
90
+ #define OPENXLSX_EXPORTS_HPP
91
+
92
+ #ifndef OPENXLSX_EXPORT
93
+ #define OPENXLSX_EXPORT
94
+ #endif
95
+
96
+ #ifndef OPENXLSX_HIDDEN
97
+ #define OPENXLSX_HIDDEN
98
+ #endif
99
+
100
+ #endif
101
+ `;
102
+ const headersDir = join(openxlsxDir, 'OpenXLSX', 'headers');
103
+ ensureDir(headersDir);
104
+ writeFileIfNotExists(join(headersDir, 'OpenXLSX-Exports.hpp'), openxlsxExportsHeader);
105
+
106
+ // Create symlink for #include <OpenXLSX/XXX.hpp> resolution
107
+ createSymlinkIfNotExists('.', join(headersDir, 'OpenXLSX'));
108
+
109
+ console.log(`OpenXLSX dependencies prepared successfully for react-native-nitro-xlsx v${getPackageVersion()}.`);
package/src/index.ts CHANGED
@@ -1,7 +1,19 @@
1
1
  import { NitroModules } from 'react-native-nitro-modules'
2
- import type { NitroXlsx as NitroXlsxSpec } from './specs/NitroXlsx.nitro'
2
+ import type { NitroXlsx as NitroXlsxSpec, XlsxWorkbook as XlsxWorkbookSpec } from './specs/NitroXlsx.nitro'
3
3
 
4
- export const NitroXlsx = NitroModules.createHybridObject<NitroXlsxSpec>('NitroXlsx')
4
+ const NitroXlsxBridge = NitroModules.createHybridObject<NitroXlsxSpec>('NitroXlsx')
5
+
6
+ type JsonRecord = Record<string, string | number>
7
+
8
+ export const NitroXlsx = Object.assign(NitroXlsxBridge, {
9
+ fromJSON(data: JsonRecord[]): XlsxWorkbookSpec {
10
+ return NitroXlsxBridge.fromJSON(JSON.stringify(data))
11
+ },
12
+ })
13
+
14
+ export function toJSON(workbook: XlsxWorkbookSpec, keys?: string[]): JsonRecord[] {
15
+ return JSON.parse(workbook.toJSON(keys))
16
+ }
5
17
 
6
18
  export type { XlsxWorkbook, XlsxWorksheet, XlsxCellFormat } from './specs/NitroXlsx.nitro'
7
19
 
@@ -10,6 +10,7 @@ export interface XlsxWorkbook extends HybridObject<{ ios: 'c++'; android: 'c++'
10
10
  getWorksheetCount(): number
11
11
  addCellFormat(): XlsxCellFormat
12
12
  getBuffer(): Promise<ArrayBuffer>
13
+ toJSON(keys?: string[]): string
13
14
  }
14
15
 
15
16
  export interface XlsxWorksheet extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
@@ -148,7 +149,8 @@ export interface XlsxCellFormat extends HybridObject<{ ios: 'c++'; android: 'c++
148
149
  }
149
150
 
150
151
  export interface NitroXlsx extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
151
- createWorkbook(): XlsxWorkbook
152
+ createWorkbook(tempDir?: string): XlsxWorkbook
152
153
  openWorkbook(path: string): XlsxWorkbook
153
154
  openWorkbookFromBuffer(buffer: ArrayBuffer): XlsxWorkbook
155
+ fromJSON(data: string): XlsxWorkbook
154
156
  }