specpress
Version:
Export PDF and/or DOCX files from a subset of Markdown, ASN.1 and JSON files
265 lines • 9.97 kB
JavaScript
// JsonTools.ts — JSON I/O, encoder, schema validation
import { readFileSync, existsSync, writeFileSync, statSync, readdirSync } from "node:fs";
import { join, resolve } from "node:path";
import { createRequire } from "node:module";
import { logger } from "./Logger.js";
const require = createRequire(import.meta.url);
const Ajv = require("ajv");
//////////////////////////////
// Exceptions
export class InvalidJsonIndentationLevelException extends RangeError {
constructor(message) { super(message); this.name = "InvalidJsonIndentationLevelException"; }
}
//////////////////////////////
// JSON read limits
export class JsonReadLimits {
maxFileSize;
maxTotalSize;
totalBytesRead = 0;
constructor(maxFileSize = 10 * 1024 * 1024, maxTotalSize = 100 * 1024 * 1024) {
this.maxFileSize = maxFileSize;
this.maxTotalSize = maxTotalSize;
}
reset() { this.totalBytesRead = 0; }
}
export const jsonReadLimits = new JsonReadLimits();
//////////////////////////////
// JSON I/O functions
export function LoadJsonFileToDict(aJsonFile) {
if (aJsonFile === null || aJsonFile === undefined) {
throw new Error("aJsonFile shall not be None");
}
if (typeof aJsonFile !== "string") {
throw new Error(`Cannot handle aJsonFile of type '${typeof aJsonFile}'`);
}
let fileSize;
try {
fileSize = statSync(aJsonFile).size;
}
catch (e) {
throw new Error(`Error when accessing file '${aJsonFile}': ${e}`);
}
if (fileSize > jsonReadLimits.maxFileSize) {
throw new Error(`File '${aJsonFile}' is ${fileSize} bytes, exceeding the per-file limit of ${jsonReadLimits.maxFileSize} bytes.`);
}
if (jsonReadLimits.totalBytesRead + fileSize > jsonReadLimits.maxTotalSize) {
throw new Error(`Reading '${aJsonFile}' (${fileSize} bytes) would exceed the cumulative limit of ${jsonReadLimits.maxTotalSize} bytes (already read: ${jsonReadLimits.totalBytesRead} bytes).`);
}
jsonReadLimits.totalBytesRead += fileSize;
let content;
try {
content = readFileSync(aJsonFile, "utf-8");
}
catch (e) {
throw new Error(`Error when loading JSON from file '${aJsonFile}': ${e}`);
}
let data;
try {
data = JSON.parse(content);
}
catch (e) {
throw new Error(`Error: ${e} when loading JSON from file '${aJsonFile}'`);
}
if (data === null || data === undefined) {
throw new Error(`Error: Could not load JSON from file '${aJsonFile}'`);
}
return data;
}
function findFilesRecursive(dir, pattern) {
const results = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory())
results.push(...findFilesRecursive(fullPath, pattern));
else if (pattern.test(entry.name))
results.push(fullPath);
}
return results;
}
export function GetAllJsonFilesInFolder(aTopLevelDirectory) {
const dirPath = resolve(aTopLevelDirectory);
if (!existsSync(dirPath)) {
throw new Error(`The source path does not exist: ${aTopLevelDirectory}`);
}
const fileList = findFilesRecursive(dirPath, /\.json$/i).sort();
logger.log(`Found ${fileList.length} JSON-files in directory '${aTopLevelDirectory}'`);
return fileList;
}
export function GetJsonFilesByPattern(aRootFolder, aPattern) {
const dirPath = resolve(aRootFolder);
if (!existsSync(dirPath)) {
throw new Error(`The source path does not exist: ${aRootFolder}`);
}
const regex = new RegExp('^' + aPattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$', 'i');
const fileList = findFilesRecursive(dirPath, regex).sort();
logger.log(`Found ${fileList.length} JSON-files matching '${aPattern}' in '${aRootFolder}'`);
return fileList;
}
export function LoadSchema(aJsonFile) {
const schemaDict = LoadJsonFileToDict(aJsonFile);
const ajv = new Ajv();
return ajv.compile(schemaDict);
}
export class SchemaValidationException extends Error {
constructor(message) { super(message); this.name = "SchemaValidationException"; }
}
export function ValidateSchema(aJsonData, aCompiledSchema, aDescriptor, abortOnError = true) {
if (aCompiledSchema === null || aCompiledSchema === undefined) {
return true;
}
try {
if (!aCompiledSchema(aJsonData)) {
const errors = aCompiledSchema.errors;
const messages = [];
if (errors && errors.length > 0) {
for (const oneError of errors) {
const path = oneError.instancePath || "/";
messages.push(`${aDescriptor}: Schema validation error at '${path}': ${oneError.message}`);
}
}
else {
messages.push(`${aDescriptor}: Schema validation failed (no details available).`);
}
if (abortOnError) {
throw new SchemaValidationException(messages.join("\n"));
}
for (const oneMessage of messages) {
logger.log(oneMessage);
}
return false;
}
}
catch (e) {
if (e instanceof SchemaValidationException)
throw e;
const msg = `${aDescriptor}: Schema validation error: ${e}`;
if (abortOnError)
throw new SchemaValidationException(msg);
logger.log(msg);
return false;
}
return true;
}
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
// RAN4JsonEncoder //
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
export class JsonObject {
}
export class RAN4JsonEncoder {
filePath;
buffer = [];
indent;
isBufferEmpty = true;
static oneLinePerElement(aListElement) {
if (aListElement === null || aListElement === undefined)
return false;
if (aListElement instanceof JsonObject || Array.isArray(aListElement))
return true;
return false;
}
constructor(aJsonFile) {
if (aJsonFile === null || aJsonFile === undefined) {
throw new Error(`Could not open file '${aJsonFile}' for JSON output.`);
}
this.filePath = aJsonFile;
this.indent = " ";
}
write(aStr, aLevel) {
if (aStr === null || aStr === undefined || aStr === "")
return;
if (aLevel < 0) {
throw new InvalidJsonIndentationLevelException("aLevel shall not be smaller than 0");
}
let s = aStr;
let terminated = false;
if (s.endsWith("\n")) {
terminated = true;
s = s.slice(0, -1);
}
s = s.replaceAll("\n", "\n" + this.indent.repeat(aLevel));
if (terminated) {
s = s + "\n";
}
if (this.isBufferEmpty) {
s = this.indent.repeat(aLevel) + s;
}
this.buffer.push(s);
this.isBufferEmpty = aStr.endsWith("\n");
}
writeValue(aValue, aLevel = 0, aSuffix = "") {
if (aValue instanceof JsonObject) {
this.write("\n", aLevel);
aValue.toJSON(this, aLevel);
this.write(aSuffix, 0);
}
else if (Array.isArray(aValue)) {
this.write("[", aLevel);
let isFirst = true;
let aListElement = null;
for (aListElement of aValue) {
if (isFirst)
isFirst = false;
else {
this.write(",", aLevel + 1);
if (!RAN4JsonEncoder.oneLinePerElement(aListElement)) {
this.write(" ", aLevel + 1);
}
}
this.writeValue(aListElement, aLevel + 1);
}
if (RAN4JsonEncoder.oneLinePerElement(aListElement)) {
this.write("\n", aLevel);
}
this.write("]" + aSuffix, aLevel);
}
else if (typeof aValue === "object" && aValue !== null && !Array.isArray(aValue)) {
this.write("{", aLevel);
let isFirst = true;
for (const aDictKey of Object.keys(aValue).sort()) {
if (isFirst) {
this.writeKeyAndValue(aDictKey, aValue[aDictKey], aLevel + 1, "\n");
isFirst = false;
}
else {
this.writeKeyAndValue(aDictKey, aValue[aDictKey], aLevel + 1, ",\n");
}
}
this.write("\n}" + aSuffix, aLevel);
}
else if (typeof aValue === "boolean") {
this.write(`${aValue}${aSuffix}`, aLevel);
}
else if (typeof aValue === "string") {
this.write(`"${aValue}"${aSuffix}`, aLevel);
}
else if (typeof aValue === "number") {
if (Number.isInteger(aValue)) {
this.write(`${aValue}${aSuffix}`, aLevel);
}
else {
// Match Python's %g formatting: strip trailing zeros
this.write(`${parseFloat(aValue.toPrecision(6))}${aSuffix}`, aLevel);
}
}
else {
throw new Error(`Cannot handle aValue of type '${typeof aValue}' (${aValue})`);
}
}
writeKey(aKey, aLevel, aPrefix = "") {
this.write(`${aPrefix}"${aKey}": `, aLevel);
}
writeKeyAndValue(aKey, aValue, aLevel, aPrefix = "", aSuffix = "") {
this.writeKey(aKey, aLevel, aPrefix);
this.writeValue(aValue, aLevel, aSuffix);
}
flush() {
if (!this.isBufferEmpty) {
this.write("\n", 0);
}
writeFileSync(this.filePath, this.buffer.join(""), "utf-8");
this.buffer = [];
}
}
//# sourceMappingURL=JsonTools.js.map