UNPKG

specpress

Version:

Export PDF and/or DOCX files from a subset of Markdown, ASN.1 and JSON files

265 lines 9.97 kB
// 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