@tts-tools/xmlbundle
Version:
Module to bundle/unbundle the XML UI files for Tabletop Simulator.
107 lines (106 loc) • 3.67 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.unbundle = exports.bundle = void 0;
const fs_1 = require("fs");
const INCLUDE_REGEX = /^([\t ]*)<Include src=(["'])(.+)\2\s*\/>/im;
const BORDER_REGEX = /([ \t]*)<!-- include (.*?) -->\r?\n(.*?)<!-- include \2 -->/gs;
/**
* Bundles the given XML by resolving `Include` nodes with a `src` attribute.
*/
const bundle = (xmlUi, includePath) => {
if (typeof includePath === "string") {
includePath = [includePath];
}
return resolve(xmlUi, includePath, [], true);
};
exports.bundle = bundle;
/**
* Unbundles the given XML by replacing comments generated from `bundle` with their respective `Include` node and `src`
* attribute.
*/
const unbundle = (xmlUi) => {
const result = {
bundles: {},
root: unbundleContent(xmlUi),
};
result.bundles = unbundleFrom(xmlUi);
return result;
};
exports.unbundle = unbundle;
const unbundleFrom = (xmlBundle) => {
let bundles = {};
for (const match of xmlBundle.matchAll(BORDER_REGEX)) {
let [_, indent, name, content] = match;
name = name.replace(".xml", "");
bundles = {
...bundles,
[name]: { name, content: unbundleContent(content, indent) },
...unbundleFrom(content),
};
}
return bundles;
};
const unbundleContent = (xmlBundle, indent) => {
const replacement = '$1<Include src="$2" />';
let base = xmlBundle.replaceAll(BORDER_REGEX, replacement);
if (indent) {
const regex = new RegExp(`^${indent}`, "gm");
base = base.replaceAll(regex, "");
}
return base;
};
const resolve = (xmlUi, rootPaths, alreadyResolved, topLevel) => {
let resolved = xmlUi;
let match = resolved.match(INCLUDE_REGEX);
while (match) {
let resolvedInclude = readInclude(match[3], rootPaths, alreadyResolved);
if (topLevel) {
alreadyResolved = [];
}
const indent = match[1] ?? "";
resolvedInclude = resolvedInclude
.split("\n")
.map((line) => (line ? indent + line : line))
.join("\n");
const start = match.index;
const end = start + match[0].length;
resolved = resolved.substring(0, start) + resolvedInclude + resolved.substring(end);
match = resolved.match(INCLUDE_REGEX);
}
return resolved;
};
const getFilePath = (fileName) => {
fileName = fileName.toLowerCase();
if (!fileName.endsWith(".xml")) {
fileName += ".xml";
}
let filePath = fileName.match(/(.+)\//);
if (filePath) {
filePath = "/" + filePath[1];
}
else {
filePath = "";
}
return { subPath: filePath, fileName: fileName };
};
const readInclude = (file, rootPaths, alreadyResolved) => {
const { subPath, fileName } = getFilePath(file);
const border = `<!-- include ${file} -->`;
const [filePath, root] = findFromRoots(fileName, rootPaths);
if (alreadyResolved.includes(filePath)) {
throw new Error(`Cycle detected! File "${filePath}" was already included before.`);
}
alreadyResolved.push(filePath);
const includeContent = (0, fs_1.readFileSync)(filePath, { encoding: "utf-8" });
const resolved = resolve(includeContent, [root + subPath], alreadyResolved, false);
return `${border}\n${resolved}\n${border}`;
};
const findFromRoots = (file, rootPaths) => {
for (const root of rootPaths) {
const fileName = `${root}/${file}`;
if ((0, fs_1.existsSync)(fileName)) {
return [fileName, root];
}
}
throw new Error(`Can not resolve file '${file}'!`);
};