diff --git a/BUILD.md b/BUILD.md
index 3e01cd2..f07dc91 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -278,3 +278,59 @@ Visual Studio 2017 поддерживает работу с cmake без пре
}
]
```
+
+## WebAssembly (браузерный просмотр таблиц)
+
+### Что нужно для сборки
+
+1. Активированный Emscripten (`em++` в `PATH`)
+2. Boost, собранный под target Emscripten (нужны `filesystem`, `system`, `regex`)
+
+git clone https://github.com/emscripten-core/emsdk.git
+cd emsdk
+./emsdk install latest
+./emsdk activate latest
+source ./emsdk_env.sh
+
+curl -L -o boost1.88.tar.gz
+tar xf boost1.88.tar.gz
+cd boost1.88
+
+./bootstrap.sh
+cat > user-config.jam <<'EOF'
+using clang : emscripten : em++ ;
+EOF
+
+export BOOST_WASM_ROOT="$HOME/opt/boost-wasm"
+mkdir -p "$BOOST_WASM_ROOT"
+
+./b2 -j"$(nproc)" \
+ toolset=clang-emscripten \
+ target-os=emscripten \
+ variant=release \
+ link=static runtime-link=static \
+ threading=single \
+ --with-system --with-filesystem --with-regex \
+ cxxflags="-O3" \
+ install --prefix="$BOOST_WASM_ROOT"
+
+### Сборка WASM
+
+```sh
+BOOST_WASM_ROOT=/path/to/boost-wasm ./web/build-wasm.sh
+```
+После успешной сборки появятся:
+
+- `web/parser.js`
+- `web/parser.wasm`
+
+### Локальный запуск
+
+```sh
+cd web
+python3 -m http.server 8080
+```
+
+Открыть в браузере:
+
+`http://localhost:8080`
diff --git a/web/app.js b/web/app.js
new file mode 100644
index 0000000..db9b6c5
--- /dev/null
+++ b/web/app.js
@@ -0,0 +1,381 @@
+const statusEl = document.getElementById("status");
+const errorEl = document.getElementById("error");
+const fileInput = document.getElementById("fileInput");
+const openBtn = document.getElementById("openBtn");
+const tablesEl = document.getElementById("tables");
+const sortFieldEl = document.getElementById("sortField");
+const sortDirEl = document.getElementById("sortDir");
+const tableMetaEl = document.getElementById("tableMeta");
+const tableContentHeadEl = document.getElementById("tableContentHead");
+const tableContentBodyEl = document.getElementById("tableContentBody");
+
+let worker = null;
+let requestSeq = 1;
+const pendingRequests = new Map();
+let currentTables = [];
+let selectedTableName = "";
+const TABLE_PREVIEW_LIMIT = 100;
+
+function setStatus(message) {
+ statusEl.textContent = message;
+}
+
+function showError(message) {
+ errorEl.style.display = "block";
+ errorEl.textContent = message;
+}
+
+function clearError() {
+ errorEl.style.display = "none";
+ errorEl.textContent = "";
+}
+
+function clearTables() {
+ tablesEl.replaceChildren();
+}
+
+function clearTableContent() {
+ tableContentHeadEl.replaceChildren();
+ tableContentBodyEl.replaceChildren();
+ tableMetaEl.textContent = "Select a table to preview first 100 rows.";
+}
+
+function formatSize(bytes) {
+ if (!Number.isFinite(bytes) || bytes < 0) return "0 B";
+
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
+ let value = bytes;
+ let unitIndex = 0;
+
+ while (value >= 1024 && unitIndex < units.length - 1) {
+ value /= 1024;
+ unitIndex += 1;
+ }
+
+ return `${value.toFixed(unitIndex === 0 ? 0 : 2)} ${units[unitIndex]}`;
+}
+
+function extractName(value, depth = 0) {
+ if (depth > 10 || value == null) return "";
+ if (typeof value === "string") return value;
+
+ if (Array.isArray(value)) {
+ for (const item of value) {
+ const candidate = extractName(item, depth + 1);
+ if (candidate) return candidate;
+ }
+ return "";
+ }
+
+ if (typeof value === "object") {
+ const preferredKeys = [
+ "name",
+ "Name",
+ "tableName",
+ "table_name",
+ "table",
+ "title",
+ "value",
+ ];
+ for (const key of preferredKeys) {
+ if (key in value) {
+ const candidate = extractName(value[key], depth + 1);
+ if (candidate) return candidate;
+ }
+ }
+
+ for (const nestedValue of Object.values(value)) {
+ const candidate = extractName(nestedValue, depth + 1);
+ if (candidate) return candidate;
+ }
+ }
+
+ return "";
+}
+
+function extractSize(value, depth = 0) {
+ if (depth > 3 || value == null) return 0;
+ if (typeof value === "number" && Number.isFinite(value)) return value;
+ if (typeof value === "string") {
+ const parsed = Number(value);
+ return Number.isFinite(parsed) ? parsed : 0;
+ }
+
+ if (Array.isArray(value)) {
+ for (const item of value) {
+ const candidate = extractSize(item, depth + 1);
+ if (candidate > 0) return candidate;
+ }
+ return 0;
+ }
+
+ if (typeof value === "object") {
+ const preferredKeys = ["size", "totalSize", "bytes", "value"];
+ for (const key of preferredKeys) {
+ if (key in value) {
+ const candidate = extractSize(value[key], depth + 1);
+ if (candidate > 0) return candidate;
+ }
+ }
+ }
+
+ return 0;
+}
+
+function normalizeTables(parsed) {
+ if (!Array.isArray(parsed)) {
+ if (parsed && Array.isArray(parsed.tables)) {
+ return normalizeTables(parsed.tables);
+ }
+ throw new Error("Table JSON format is unsupported. Expected array or {tables:[...]}");
+ }
+
+ return parsed.map((item) => {
+ if (typeof item === "string") {
+ return { name: item, size: 0, raw: item };
+ }
+
+ const rawName = item?.name ?? item;
+ const rawSize = item?.size ?? item;
+ const normalizedName = extractName(rawName) || extractName(item) || "(unknown)";
+ return {
+ name: normalizedName === "[object Object]" ? "(unknown)" : normalizedName,
+ size: extractSize(rawSize),
+ raw: item,
+ };
+ });
+}
+
+function sortedTables() {
+ const sortField = sortFieldEl.value;
+ const isAsc = sortDirEl.value === "asc";
+ const sign = isAsc ? 1 : -1;
+ const rows = [...currentTables];
+
+ rows.sort((a, b) => {
+ if (sortField === "size") {
+ if (a.size !== b.size) {
+ return (a.size - b.size) * sign;
+ }
+ return a.name.localeCompare(b.name) * sign;
+ }
+
+ const nameCmp = a.name.localeCompare(b.name);
+ if (nameCmp !== 0) {
+ return nameCmp * sign;
+ }
+ return (a.size - b.size) * sign;
+ });
+
+ return rows;
+}
+
+function renderTables() {
+ clearTables();
+
+ for (const table of sortedTables()) {
+ const tr = document.createElement("tr");
+ tr.classList.toggle("active", table.name === selectedTableName);
+
+ const nameTd = document.createElement("td");
+ const fallbackName =
+ extractName(table.raw) ||
+ (table.raw && typeof table.raw === "object" ? JSON.stringify(table.raw) : "");
+ const displayName =
+ typeof table.name === "string" && table.name !== "[object Object]"
+ ? table.name
+ : (fallbackName || "(unknown)");
+ nameTd.textContent = displayName;
+
+ const sizeTd = document.createElement("td");
+ sizeTd.textContent = formatSize(table.size);
+ sizeTd.className = "size";
+
+ tr.appendChild(nameTd);
+ tr.appendChild(sizeTd);
+ tr.addEventListener("click", () => {
+ void loadTableContent(table.name);
+ });
+ tablesEl.appendChild(tr);
+ }
+}
+
+function renderTableContent(payload) {
+ tableContentHeadEl.replaceChildren();
+ tableContentBodyEl.replaceChildren();
+
+ const fields = Array.isArray(payload?.fields) ? payload.fields : [];
+ const rows = Array.isArray(payload?.rows) ? payload.rows : [];
+
+ const headerRow = document.createElement("tr");
+ const rowNumTh = document.createElement("th");
+ rowNumTh.className = "rownum";
+ rowNumTh.textContent = "#";
+ headerRow.appendChild(rowNumTh);
+
+ for (const field of fields) {
+ const th = document.createElement("th");
+ th.textContent = String(field?.name ?? "");
+ headerRow.appendChild(th);
+ }
+ tableContentHeadEl.appendChild(headerRow);
+
+ for (const row of rows) {
+ const tr = document.createElement("tr");
+ if (row?.deleted) {
+ tr.classList.add("deleted");
+ }
+
+ const rowNumTd = document.createElement("td");
+ rowNumTd.className = "rownum";
+ rowNumTd.textContent = String(row?.row ?? "");
+ tr.appendChild(rowNumTd);
+
+ const values = Array.isArray(row?.values) ? row.values : [];
+ for (const value of values) {
+ const td = document.createElement("td");
+ td.textContent = String(value ?? "");
+ tr.appendChild(td);
+ }
+
+ tableContentBodyEl.appendChild(tr);
+ }
+
+ const offset = Number(payload?.offset) || 0;
+ const totalRows = Number(payload?.totalRows) || 0;
+ const shownTo = Math.min(offset + rows.length, totalRows);
+ tableMetaEl.textContent =
+ `${payload?.table ?? ""}: showing ${offset}-${shownTo} of ${totalRows} rows`;
+}
+
+function initWorker() {
+ worker = new Worker("./wasm_worker.js");
+
+ worker.addEventListener("message", (event) => {
+ const message = event.data || {};
+ const request = pendingRequests.get(message.id);
+ if (!request) {
+ return;
+ }
+ pendingRequests.delete(message.id);
+ if (message.ok) {
+ request.resolve(message.result);
+ } else {
+ request.reject(new Error(String(message.error || "Unknown worker error")));
+ }
+ });
+
+ worker.addEventListener("error", (event) => {
+ const message = event.message || "WASM worker crashed";
+ for (const request of pendingRequests.values()) {
+ request.reject(new Error(message));
+ }
+ pendingRequests.clear();
+ });
+}
+
+function workerCall(method, params = {}) {
+ if (!worker) {
+ return Promise.reject(new Error("WASM worker is not initialized"));
+ }
+
+ return new Promise((resolve, reject) => {
+ const id = requestSeq++;
+ pendingRequests.set(id, { resolve, reject });
+ worker.postMessage({ id, method, params });
+ });
+}
+
+async function loadTableContent(tableName) {
+ clearError();
+ setStatus(`Loading rows from ${tableName}...`);
+ try {
+ const payload = await workerCall("getTableRows", {
+ tableName,
+ offset: 0,
+ limit: TABLE_PREVIEW_LIMIT,
+ });
+ selectedTableName = tableName;
+ renderTables();
+ renderTableContent(payload);
+ setStatus(`Loaded ${tableName} preview`);
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ showError(message);
+ setStatus("Failed to load table content.");
+ }
+}
+
+async function onOpenClick() {
+ clearError();
+ clearTables();
+ clearTableContent();
+
+ const file = fileInput.files?.[0];
+ if (!file) {
+ showError("Please choose a .1CD file first.");
+ return;
+ }
+
+ openBtn.disabled = true;
+ try {
+ setStatus("Mounting selected file in WORKERFS...");
+ await workerCall("openFile", { file });
+
+ setStatus("Loading table list...");
+ const parsed = await workerCall("listTables");
+ currentTables = normalizeTables(parsed);
+ selectedTableName = "";
+ renderTables();
+ clearTableContent();
+ setStatus(`Loaded ${currentTables.length} tables from ${file.name}`);
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ showError(message);
+ setStatus("Failed to open file.");
+ } finally {
+ openBtn.disabled = false;
+ }
+}
+
+async function main() {
+ clearError();
+ clearTables();
+ clearTableContent();
+ openBtn.disabled = true;
+
+ try {
+ initWorker();
+ await workerCall("init");
+ setStatus("WASM worker runtime is ready.");
+ openBtn.disabled = false;
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err);
+ showError(`WASM init error: ${message}`);
+ setStatus("Runtime initialization failed.");
+ }
+}
+
+fileInput.addEventListener("change", () => {
+ clearError();
+});
+
+openBtn.addEventListener("click", () => {
+ void onOpenClick();
+});
+
+sortFieldEl.addEventListener("change", () => {
+ renderTables();
+});
+
+sortDirEl.addEventListener("change", () => {
+ renderTables();
+});
+
+window.addEventListener("beforeunload", () => {
+ if (worker) {
+ worker.terminate();
+ }
+});
+
+void main();
diff --git a/web/build-wasm.sh b/web/build-wasm.sh
new file mode 100755
index 0000000..7be79b6
--- /dev/null
+++ b/web/build-wasm.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Build tool1cd WASM API for browser usage.
+# Output: web/parser.js + web/parser.wasm
+#
+# Prereqs:
+# 1) Emscripten activated (em++ available)
+# 2) Boost built for Emscripten (filesystem/system/regex)
+#
+# Example:
+#BOOST_WASM_ROOT=$HOME/opt/boost-wasm ./web/build-wasm.sh
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+OUT_DIR="$ROOT_DIR/web"
+SRC_DIR="$ROOT_DIR/src/tool1cd"
+
+BOOST_WASM_ROOT="${BOOST_WASM_ROOT:-}"
+if [[ -z "${BOOST_WASM_ROOT}" ]]; then
+ echo "ERROR: Set BOOST_WASM_ROOT to your Boost-for-WASM prefix." >&2
+ exit 1
+fi
+
+if ! command -v em++ >/dev/null 2>&1; then
+ echo "ERROR: em++ not found. Activate emsdk first." >&2
+ exit 1
+fi
+
+TOOL1CD_SOURCES=(
+ main.cpp MessageRegistration.cpp Class_1CD.cpp
+ Common.cpp ConfigStorage.cpp Parse_tree.cpp TempStream.cpp Base64.cpp UZLib.cpp Messenger.cpp
+ V8Object.cpp Field.cpp Index.cpp Table.cpp TableFiles.cpp TableFileStream.cpp
+ MemBlock.cpp CRC32.cpp Packdata.cpp PackDirectory.cpp FieldType.cpp DetailedException.cpp
+ BinaryDecimalNumber.cpp save_depot_config.cpp save_part_depot_config.cpp
+ SupplierConfig.cpp TableRecord.cpp BinaryGuid.cpp TableIterator.cpp SupplierConfigBuilder.cpp
+ cfapi/V8File.cpp cfapi/V8Catalog.cpp cfapi/TV8FileStream.cpp cfapi/APIcfBase.cpp cfapi/V8Time.cpp
+ SystemClasses/String.cpp SystemClasses/System.Classes.cpp SystemClasses/System.cpp
+ SystemClasses/System.IOUtils.cpp SystemClasses/TFileStream.cpp SystemClasses/TMemoryStream.cpp
+ SystemClasses/TStream.cpp SystemClasses/TStreamReader.cpp SystemClasses/TStreamWriter.cpp
+ SystemClasses/System.SysUtils.cpp SystemClasses/GetTickCount.cpp
+)
+
+ABS_SOURCES=()
+for src in "${TOOL1CD_SOURCES[@]}"; do
+ ABS_SOURCES+=("$SRC_DIR/$src")
+done
+ABS_SOURCES+=("$OUT_DIR/wasm_api.cpp")
+
+mkdir -p "$OUT_DIR"
+
+em++ \
+ "${ABS_SOURCES[@]}" \
+ -std=c++11 -O2 \
+ -I"$SRC_DIR" \
+ -I"$BOOST_WASM_ROOT/include" \
+ -L"$BOOST_WASM_ROOT/lib" \
+ -lboost_filesystem -lboost_system -lboost_regex \
+ -sUSE_ZLIB=1 \
+ -lworkerfs.js \
+ -sFORCE_FILESYSTEM=1 \
+ -sALLOW_MEMORY_GROWTH=1 \
+ -sENVIRONMENT=web,worker \
+ -sMODULARIZE=0 \
+ -sEXPORT_ES6=0 \
+ -sNO_EXIT_RUNTIME=1 \
+ -sEXPORTED_FUNCTIONS="['_malloc','_free','_onecd_open','_onecd_list_tables_json','_onecd_get_table_rows_json','_onecd_last_error','_onecd_close','_onecd_free_string']" \
+ -sEXPORTED_RUNTIME_METHODS="['FS','cwrap','UTF8ToString']" \
+ -o "$OUT_DIR/parser.js"
+
+echo "Built: $OUT_DIR/parser.js and $OUT_DIR/parser.wasm"
diff --git a/web/favicon.ico b/web/favicon.ico
new file mode 100644
index 0000000..2e06bbb
Binary files /dev/null and b/web/favicon.ico differ
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..b4ed28e
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,262 @@
+
+
+
+
+
+ 1CD Tables Viewer
+
+
+
+
+
+
1CD Tables Viewer
+
+
+
+
+
+
+
Loading WASM runtime...
+
+
+
Tables
+
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ Size |
+
+
+
+
+
Click a table name to view rows.
+
+
Table Content
+
Select a table to preview first 100 rows.
+
+
Expected WASM exports: onecd_open, onecd_list_tables_json, onecd_get_table_rows_json, onecd_free_string
+
+
+
+
+
+
diff --git a/web/wasm_api.cpp b/web/wasm_api.cpp
new file mode 100644
index 0000000..58193d1
--- /dev/null
+++ b/web/wasm_api.cpp
@@ -0,0 +1,305 @@
+/*
+Tool1CD library provides access to 1CD database files.
+ Copyright © 2009-2017 awa
+ Copyright © 2017-2018 E8 Tools contributors
+
+ This file is part of Tool1CD Library.
+
+ Tool1CD Library is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Tool1CD Library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with Tool1CD Library. If not, see .
+*/
+
+#include "Class_1CD.h"
+#include "Field.h"
+#include "Table.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef __EMSCRIPTEN__
+#include
+#else
+#define EMSCRIPTEN_KEEPALIVE
+#endif
+
+namespace {
+
+std::unique_ptr g_db;
+std::string g_last_error;
+
+char *dup_cstr(const std::string &s) {
+ char *out = static_cast(std::malloc(s.size() + 1));
+ if (!out) {
+ out = static_cast(std::malloc(1));
+ if (!out) return nullptr; // is it possible?
+ out[0] = '\0';
+ return out;
+ }
+
+ std::memcpy(out, s.c_str(), s.size() + 1);
+ return out;
+}
+
+std::string json_escape(const std::string& s) {
+ std::string out;
+ out.reserve(s.size() + 16);
+
+ for (char ch : s) {
+ switch (ch) {
+ case '"': out += "\\\""; break;
+ case '\\': out += "\\\\"; break;
+ case '\b': out += "\\b"; break;
+ case '\f': out += "\\f"; break;
+ case '\n': out += "\\n"; break;
+ case '\r': out += "\\r"; break;
+ case '\t': out += "\\t"; break;
+ default:
+ if (static_cast(ch) < 0x20) {
+ out += "\\u00";
+ const char* hex = "0123456789abcdef";
+ out += hex[(static_cast(ch) >> 4) & 0x0F];
+ out += hex[static_cast(ch) & 0x0F];
+ } else {
+ out += ch;
+ }
+ break;
+ }
+ }
+
+ return out;
+}
+
+void set_error(const std::string& message) {
+ g_last_error = message;
+}
+
+uint64_t object_size(const V8Object* file) {
+ return file ? file->get_len() : 0;
+}
+
+uint64_t table_total_size(const Table* table) {
+ if (!table) {
+ return 0;
+ }
+
+ return object_size(table->get_file_data())
+ + object_size(table->get_file_blob())
+ + object_size(table->get_file_index());
+}
+
+Table* find_table_by_name(const std::string& table_name) {
+ if (!g_db || !g_db->is_open()) {
+ return nullptr;
+ }
+
+ const int table_count = g_db->get_numtables();
+ for (int i = 0; i < table_count; ++i) {
+ Table* table = g_db->get_table(i);
+ if (table && table->get_name() == table_name) {
+ return table;
+ }
+ }
+
+ return nullptr;
+}
+
+} // namespace
+
+extern "C" {
+EMSCRIPTEN_KEEPALIVE int onecd_open(const char *path) {
+ if (!path || !*path) {
+ set_error("onecd_open: empty path");
+ return -1;
+ }
+
+ try {
+ std::unique_ptr db(new T_1CD());
+ db->open(path, false);
+ g_db = std::move(db);
+ g_last_error.clear();
+ return 0;
+ } catch (const std::exception &ex) {
+ set_error(ex.what());
+ return -2;
+ } catch (...) {
+ set_error("onecd_open: unknown error");
+ return -3;
+ }
+}
+
+EMSCRIPTEN_KEEPALIVE const char* onecd_list_tables_json() {
+ if (!g_db || !g_db->is_open()) {
+ set_error("Database is not open");
+ return dup_cstr("[]");
+ }
+
+ try {
+ const int table_count = g_db->get_numtables();
+ std::string json;
+ json.reserve(static_cast(table_count) * 64 + 2);
+ json.push_back('[');
+
+ for (int i = 0; i < table_count; ++i) {
+ if (i > 0) {
+ json.push_back(',');
+ }
+
+ Table* table = g_db->get_table(i);
+ const std::string table_name = table ? table->get_name() : std::string();
+ const uint64_t table_size = table_total_size(table);
+
+ json += "{\"name\":\"";
+ json += json_escape(table_name);
+ json += "\",\"size\":";
+ json += std::to_string(table_size);
+ json.push_back('}');
+ }
+
+ json.push_back(']');
+ g_last_error.clear();
+ return dup_cstr(json);
+ } catch (const std::exception& ex) {
+ set_error(ex.what());
+ return dup_cstr("[]");
+ } catch (...) {
+ set_error("onecd_list_tables_json: unknown error");
+ return dup_cstr("[]");
+ }
+}
+
+EMSCRIPTEN_KEEPALIVE const char* onecd_get_table_rows_json(const char* table_name, int offset, int limit) {
+ if (!g_db || !g_db->is_open()) {
+ set_error("Database is not open");
+ return dup_cstr("{\"error\":\"Database is not open\"}");
+ }
+
+ if (!table_name || !*table_name) {
+ set_error("onecd_get_table_rows_json: empty table name");
+ return dup_cstr("{\"error\":\"empty table name\"}");
+ }
+
+ if (offset < 0) {
+ offset = 0;
+ }
+ if (limit <= 0) {
+ limit = 100;
+ }
+ if (limit > 200) {
+ limit = 200;
+ }
+
+ try {
+ Table* table = find_table_by_name(table_name);
+ if (!table) {
+ set_error(std::string("Table not found: ") + table_name);
+ return dup_cstr("{\"error\":\"table not found\"}");
+ }
+
+ const uint32_t total_rows = table->get_phys_numrecords();
+ const int field_count = table->get_num_fields();
+ const int start = offset > static_cast(total_rows) ? static_cast(total_rows) : offset;
+ const int end = (start + limit) > static_cast(total_rows)
+ ? static_cast(total_rows)
+ : (start + limit);
+
+ std::string json;
+ json.reserve(static_cast(field_count) * 64 + static_cast(end - start) * 256 + 256);
+ json += "{\"table\":\"";
+ json += json_escape(table->get_name());
+ json += "\",\"offset\":";
+ json += std::to_string(start);
+ json += ",\"limit\":";
+ json += std::to_string(limit);
+ json += ",\"totalRows\":";
+ json += std::to_string(total_rows);
+ json += ",\"fields\":[";
+
+ for (int i = 0; i < field_count; ++i) {
+ if (i > 0) {
+ json.push_back(',');
+ }
+ Field* field = table->get_field(i);
+ json += "{\"name\":\"";
+ json += json_escape(field ? field->get_name() : std::string());
+ json += "\"}";
+ }
+
+ json += "],\"rows\":[";
+ std::vector record_buf(static_cast(table->get_recordlen()));
+
+ for (int row = start; row < end; ++row) {
+ if (row > start) {
+ json.push_back(',');
+ }
+
+ table->get_record(static_cast(row), record_buf.data());
+
+ json += "{\"row\":";
+ json += std::to_string(row);
+ json += ",\"deleted\":";
+ json += (record_buf[0] != '\0') ? "true" : "false";
+ json += ",\"values\":[";
+
+ for (int col = 0; col < field_count; ++col) {
+ if (col > 0) {
+ json.push_back(',');
+ }
+
+ Field* field = table->get_field(col);
+ std::string value;
+ try {
+ value = field ? field->get_presentation(record_buf.data()) : std::string();
+ } catch (const std::exception& ex) {
+ value = std::string("{ERROR: ") + ex.what() + "}";
+ } catch (...) {
+ value = "{ERROR}";
+ }
+
+ json.push_back('"');
+ json += json_escape(value);
+ json.push_back('"');
+ }
+
+ json += "]}";
+ }
+
+ json += "]}";
+ g_last_error.clear();
+ return dup_cstr(json);
+ } catch (const std::exception& ex) {
+ set_error(ex.what());
+ return dup_cstr("{\"error\":\"internal error\"}");
+ } catch (...) {
+ set_error("onecd_get_table_rows_json: unknown error");
+ return dup_cstr("{\"error\":\"unknown error\"}");
+ }
+}
+
+EMSCRIPTEN_KEEPALIVE const char* onecd_last_error() {
+ return dup_cstr(g_last_error);
+}
+
+EMSCRIPTEN_KEEPALIVE void onecd_close() {
+ g_db.reset();
+ g_last_error.clear();
+}
+
+EMSCRIPTEN_KEEPALIVE void onecd_free_string(const char* ptr) {
+ std::free(const_cast(ptr));
+}
+
+} // extern "C"