JlatexmathJsGenerator.java

package net.sourceforge.plantuml.math;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;

// ::remove file when JAVA8 or __MIT__ or __EPL__ or __BSD__ or __ASL__ or __LGPL__

/**
 * Generates <code>src/main/teavm/generated/jlatexmath.js</code>, the companion
 * script that gives the resources of JLaTeXMath (the XML files and the glyph
 * outlines, the same files as the C build embeds) to the browser build, which
 * has no class loader. It fills <code>PLANTUML_JLATEXMATH</code>, a map from the
 * path under <code>org/scilab/forge/jlatexmath/</code> to the Base64 of the file.
 * <p>
 * Loaded on demand, only by a diagram that has a formula, like
 * <code>emoji.js</code>. It is GPL (as the library): it is not in the packages
 * of the MIT, EPL, BSD, ASL and LGPL variants.
 * <p>
 * Run it after changing the resources of the library:
 *
 * <pre>
 * java net.sourceforge.plantuml.math.JlatexmathJsGenerator
 * </pre>
 */
public final class JlatexmathJsGenerator {

	static final String RESOURCES_DIR = "src/main/resources/org/scilab/forge/jlatexmath";

	static final String OUTPUT = "src/main/teavm/generated/jlatexmath.js";

	private JlatexmathJsGenerator() {
	}

	public static void main(String[] args) throws IOException {
		final File output = new File(OUTPUT);
		if (output.getParentFile().isDirectory() == false)
			throw new IOException("Target directory does not exist: " + output.getParentFile());

		final String content = generate(new File(RESOURCES_DIR));
		Files.write(output.toPath(), content.getBytes(StandardCharsets.UTF_8));
		System.out.println("Generated " + output.getAbsolutePath());
	}

	/** The content of jlatexmath.js (explicit \n, identical on every platform). */
	static String generate(File dir) throws IOException {
		if (dir.isDirectory() == false)
			throw new IOException("Resources directory does not exist: " + dir.getAbsolutePath());

		final List<String> names = new ArrayList<String>();
		collect(dir, "", names);
		Collections.sort(names);

		final StringBuilder out = new StringBuilder();
		out.append("// jlatexmath.js - Generated by JlatexmathJsGenerator.main()\n");
		out.append("// Do not edit manually\n");
		out.append("(function () {\n");
		out.append("var g = (typeof globalThis !== 'undefined') ? globalThis"
				+ " : ((typeof self !== 'undefined') ? self : this);\n");
		out.append("g.PLANTUML_JLATEXMATH = g.PLANTUML_JLATEXMATH || {};\n");
		out.append("\n");
		for (String name : names) {
			byte[] data = Files.readAllBytes(new File(dir, name).toPath());
			if (name.endsWith(".xml"))
				data = removeCarriageReturns(data);
			out.append("g.PLANTUML_JLATEXMATH[\"").append(name).append("\"]=\"")
					.append(Base64.getEncoder().encodeToString(data)).append("\";\n");
		}
		out.append("})();\n");
		return out.toString();
	}

	/**
	 * jlatexmath.js must be the same on every platform: with a CRLF checkout (git
	 * autocrlf on Windows) the XML files have CRLF, and the generated file would
	 * not be the committed one. A CR is only a whitespace in these XML files and
	 * is never part of a UTF-8 multi-byte character, so removing it is harmless.
	 * Not applied to the binary glyphs.
	 */
	static byte[] removeCarriageReturns(byte[] data) {
		int count = 0;
		for (byte b : data)
			if (b == '\r')
				count++;

		if (count == 0)
			return data;

		final byte[] result = new byte[data.length - count];
		int i = 0;
		for (byte b : data)
			if (b != '\r')
				result[i++] = b;

		return result;
	}

	private static void collect(File dir, String prefix, List<String> names) throws IOException {
		final File[] files = dir.listFiles();
		if (files == null)
			throw new IOException("Cannot list " + dir.getAbsolutePath());

		for (File file : files) {
			final String name = prefix + file.getName();
			if (file.isDirectory())
				collect(file, name + "/", names);
			// Same files as the C build embeds (copyNativeSources)
			else if (name.endsWith(".xml") || name.endsWith("/glyphs.bin"))
				names.add(name);
		}
	}

}