x3d-tidy
Version:
X3D Converter, Beautifier and Minimizer
335 lines (295 loc) • 9.48 kB
JavaScript
"use strict";
const
X3D = require ("x_ite-node"),
pkg = require ("../package.json"),
infer = require ("./infer"),
metadata = require ("./metadata"),
yargs = require ("yargs"),
{ hideBin } = require ("yargs/helpers"),
url = require ("url"),
path = require ("path"),
fs = require ("fs"),
zlib = require ("zlib"),
colors = require ("colors"),
DEBUG = false;
main ();
async function main ()
{
try
{
await convert ();
process .exit ();
}
catch (error)
{
console .error (colors .red (error .message || error));
process .exit (1);
}
}
async function convert ()
{
const args = yargs (hideBin (process .argv))
.scriptName ("x3d-tidy")
.usage ("$0 [options] input-file output-file [input-file output-file ...]")
.wrap (yargs () .terminalWidth ())
.command ("X3D converter, beautifier and minimizer")
.version (pkg .version)
.alias ("v", "version")
.fail ((msg, error, yargs) =>
{
console .error (colors .red (msg));
process .exit (1);
})
.option ("double",
{
type: "number",
alias: "d",
description: "Set double precision, default is 15.",
array: true,
default: [15],
requiresArg: true,
})
.option ("extension",
{
type: "string",
alias: "e",
description: `Set output file extension(s), e.g. ".x3dv" or ".tidy.x3d". The output file will have the same basename as the input file.`,
array: true,
requiresArg: true,
implies: "input",
conflicts: "output",
})
.option ("float",
{
type: "number",
alias: "f",
description: "Set float precision, default is 7.",
array: true,
default: [7],
requiresArg: true,
})
.option ("log",
{
type: "boolean",
alias: "l",
description: `Log output filenames to stdout.`,
implies: "input",
})
.option ("infer",
{
type: "boolean",
alias: "r",
description: "If set, infer profile and components from used nodes.",
array: true,
default: [false],
})
.option ("input",
{
type: "string",
alias: "i",
description: "Set input file(s). If there are less input files than output files, the last input file is used for the remaining output files.",
array: true,
requiresArg: true,
demandOption: true,
})
.option ("metadata",
{
type: "boolean",
alias: "m",
description: "If set, remove metadata nodes.",
array: true,
default: [false],
})
.option ("output",
{
type: "string",
alias: "o",
description: `Set output file(s). To output it to stdout use only the extension, e.g. ".x3dv".`,
array: true,
requiresArg: true,
implies: "input",
conflicts: "extension",
})
.option ("style",
{
type: "string",
alias: "s",
description: `Set output style, default is "TIDY". "TIDY" results in a good readable file, but with larger size, whereas "CLEAN" result in the smallest size possible by removing all redundant whitespaces. The other values are somewhere in between.`,
choices: ["TIDY", "COMPACT", "SMALL", "CLEAN"],
array: true,
requiresArg: true,
default: ["TIDY"],
})
.check (args =>
{
if (!args .output && !args .extension)
throw new Error ("Missing argument output or extension.");
return true;
})
.example ([
[
"npx x3d-tidy -i file.x3d -o file.x3dv",
"Convert an XML encoded file to a VRML encoded file."
],
[
"npx x3d-tidy -s CLEAN -i file.x3d -o file.x3dv file.x3dj",
"Convert an XML encoded file to a VRML encoded file and a JSON encoded file with smallest size possible by removing redundant whitespaces"
],
])
.help ()
.alias ("help", "h") .argv;
// Fixes an issue with URL, if it matches a drive letter.
args .input = args .input .map (input => input .replace (/^([A-Za-z]:)/, "file://$1"));
const
browser = X3D .createBrowser () .browser,
scenes = new Map ();
browser .setBrowserOption ("PrimitiveQuality", "HIGH");
browser .setBrowserOption ("TextureQuality", "HIGH");
browser .setBrowserOption ("LoadUrlObjects", false);
browser .setBrowserOption ("Mute", true);
browser .endUpdate ();
await browser .loadComponents (browser .getProfile ("Full"));
const argc = Math .max (args .input .length, args .output ?.length ?? args .extension ?.length);
for (let i = 0; i < argc; ++ i)
{
// Create input filename.
const input = new URL (arg (args .input, i), url .pathToFileURL (path .join (process .cwd (), "/")));
// Create output filename.
let output;
if (args .output)
{
output = path .resolve (process .cwd (), arg (args .output, i));
}
else if (args .extension)
{
const
filename = url .fileURLToPath (input),
extension = arg (args .extension, i);
output = `${filename .slice (0, -path. extname (filename) .length)}${extension}`;
}
if (args .log)
console .log (output);
// Load scene.
const
scene = scenes .get (input .href) ?? await browser .createX3DFromURL (new X3D .MFString (input)),
generator = scene .getMetaData ("generator") ?.filter (value => !value .startsWith (pkg .name)) ?? [ ];
scenes .set (input .href, scene);
generator .unshift (`${pkg .name} V${pkg .version}, ${pkg .homepage}`);
scene .setMetaData ("generator", generator);
scene .setMetaData ("modified", new Date () .toUTCString ());
// Output scene.
if (arg (args .infer, i))
infer (scene);
if (arg (args .metadata, i))
metadata (scene);
const options =
{
scene: scene,
style: arg (args .style, i),
precision: arg (args .float, i),
doublePrecision: arg (args .double, i),
};
if (path .extname (output))
fs .writeFileSync (output, getContents ({ ... options, type: path .extname (output) }));
else
console .log (getContents ({ ... options, type: path .basename (output) }));
}
scenes .forEach (scene => scene .dispose ());
browser .dispose ();
}
function arg (arg, i)
{
return arg [i] ?? arg .at (-1);
}
function getContents ({ scene, type, style, precision, doublePrecision })
{
switch (type .toLowerCase ())
{
default:
case ".x3d":
return scene .toXMLString ({ style: style || "TIDY", precision, doublePrecision });
case ".x3dz":
return zlib .gzipSync (scene .toXMLString ({ style: style || "CLEAN", precision, doublePrecision }));
case ".x3dv":
return scene .toVRMLString ({ style: style || "TIDY", precision, doublePrecision });
case ".x3dvz":
return zlib .gzipSync (scene .toVRMLString ({ style: style || "CLEAN", precision, doublePrecision }));
case ".x3dj":
return scene .toJSONString ({ style: style || "TIDY", precision, doublePrecision });
case ".x3djz":
return zlib .gzipSync (scene .toJSONString ({ style: style || "CLEAN", precision, doublePrecision }));
case ".html":
return getHTML (scene);
}
}
function getHTML (scene)
{
return /* html */ `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script defer src="https://cdn.jsdelivr.net/npm/x_ite@latest/dist/x_ite.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400&family=Source+Sans+3:ital,wght@0,400&display=swap");
@media (prefers-color-scheme: light) {
:root {
--text-color: rgb(42, 42, 42);
--background-color: white;
--link-color: rgb(0, 86, 178);
--link-hover-color: rgb(210, 96, 58);
}
}
@media (prefers-color-scheme: dark) {
:root {
--text-color: rgb(175, 176, 177);
--background-color: rgb(27, 27, 30);
--link-color: rgb(82, 108, 150);
--link-hover-color: rgb(210, 96, 58);
}
}
body {
color-scheme: light dark;
box-sizing: border-box;
display: flex;
flex-direction: column;
margin: 0px;
padding: 0px;
height: 100vh;
background-color: var(--background-color);
color: var(--text-color);
font-family: "Source Sans 3", sans-serif;
font-size: 1.08rem;
}
body > * {
flex: 0 0 auto;
padding: 0px 1rem;
}
h1 {
font-family: Lato, sans-serif;
}
x3d-canvas {
flex: 1 1 auto;
box-sizing: border-box;
border-top: 1px solid color-mix(in srgb, var(--text-color), transparent 90%);
border-bottom: 1px solid color-mix(in srgb, var(--text-color), transparent 90%);
padding: 0px;
width: 100%;
height: 100%;
}
a {
color: var(--link-color);
}
a:hover {
color: var(--link-hover-color);
}
</style>
</head>
<body>
<h1>${path .basename (new URL (scene .worldURL) .pathname)}</h1>
<x3d-canvas>
${scene .toXMLString ({ html: true, indent: " " .repeat (6) }) .trimEnd ()}
</x3d-canvas>
<p>Made with <a href="https://www.npmjs.com/package/x3d-tidy" target="_blank">x3d-tidy</a>. If local files are not loaded <a href="https://create3000.github.io/x_ite/setup-a-localhost-server">consider setup a localhost server</a> or use <a href="https://create3000.github.io/x_ite/dom-integration">DOM integration methods</a>.</p>
</body>
</html>`;
}