import { createHash } from 'node:crypto' import { createWriteStream } from 'node:fs' import { mkdir, readFile, stat, writeFile } from 'node:fs/promises' import path from 'node:path' export async function readJson(filePath) { return JSON.parse(await readFile(filePath, 'utf8')) } export async function writeJson(filePath, value) { await mkdir(path.dirname(filePath), { recursive: true }) await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8') } export async function exists(filePath) { try { await stat(filePath) return true } catch { return false } } export async function downloadFile(url, filePath) { await mkdir(path.dirname(filePath), { recursive: true }) const response = await fetch(url) if (!response.ok || !response.body) { throw new Error(`Download failed ${response.status} ${response.statusText}: ${url}`) } const output = createWriteStream(filePath) await new Promise((resolve, reject) => { response.body.pipeTo( new WritableStream({ write(chunk) { output.write(Buffer.from(chunk)) }, close() { output.end(resolve) }, abort(reason) { output.destroy(reason) reject(reason) }, }), ).catch(reject) }) } export async function sha256File(filePath) { const data = await readFile(filePath) return createHash('sha256').update(data).digest('hex') } export function normalizeStrong(value) { const match = /^([GH])0*([0-9]+)$/i.exec(value) return match ? `${match[1].toUpperCase()}${Number(match[2])}` : value.toUpperCase() } export function cleanUsfmText(input) { return input .replace(/…/g, '...') .replace(/…/g, '...') .replace(/’/g, "'") .replace(/“/g, '"') .replace(/”/g, '"') .replace(/\\f [\s\S]*?\\f\*/g, '') .replace(/\\x [\s\S]*?\\x\*/g, '') .replace(/\\add\s+([^\\]+)\\add\*/g, '$1') .replace(/\\nd\s+([\s\S]*?)\\nd\*/g, '$1') .replace(/\\[+\w]+\*/g, '') .replace(/\\[a-z0-9+]+(?:\s+)?/gi, '') .replace(/\s+/g, ' ') .replace(/\s+([,.;:?!])/g, '$1') .trim() }