ThemesJsGenerator.java

/* ========================================================================
 * PlantUML : a free UML diagram generator
 * ========================================================================
 *
 * (C) Copyright 2009-2021, Arnaud Roques
 *
 * Project Info:  https://plantuml.com
 *
 * If you like this project or if you find it useful, you can support us at:
 *
 * https://plantuml.com/patreon (only 1$ per month!)
 * https://plantuml.com/paypal
 *
 * This file is part of PlantUML.
 *
 * PlantUML is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PlantUML distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
 * License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 *
 * Original Author:  Arnaud Roques
 *
 *
 */
package net.sourceforge.plantuml.theme;

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.Collections;
import java.util.List;

/**
 * Generates <code>src/main/resources/teavm/themes.js</code>, the companion
 * script that makes the bundled themes available to the TeaVM (browser) build.
 * <p>
 * The browser build has no classpath, so
 * {@link ThemeUtils#loadBundledOrLocalTheme} cannot reach
 * <code>/themes/puml-theme-*.puml</code>. This generator writes those same
 * files into a JS map on <code>PLANTUML_THEMES</code>, which the engine loads
 * on demand through {@code TeaVmScriptLoader}, exactly like
 * <code>emoji.js</code> and <code>openiconic.js</code>.
 * <p>
 * Run it after adding or editing a theme:
 *
 * <pre>
 * java net.sourceforge.plantuml.theme.ThemesJsGenerator
 * </pre>
 */
public final class ThemesJsGenerator {

	private static final String THEMES_DIR = "src/main/resources/themes";

	private static final String OUTPUT = "src/main/resources/teavm/themes.js";

	private static final String THEME_FILE_PREFIX = "puml-theme-";

	private static final String THEME_FILE_SUFFIX = ".puml";

	public static void main(String[] args) throws IOException {
		final File themesDir = new File(THEMES_DIR);
		if (themesDir.isDirectory() == false)
			throw new IOException("Themes directory does not exist: " + themesDir.getAbsolutePath());

		final File output = new File(OUTPUT);
		if (output.getParentFile().isDirectory() == false)
			throw new IOException("Target directory does not exist: " + output.getParentFile());

		final List<String> names = listThemeNames(themesDir);

		// Written with explicit \n line endings so the output is byte-identical on
		// every platform (PrintStream.println would use the platform separator).
		final StringBuilder out = new StringBuilder();
		out.append("// themes.js - Generated by ThemesJsGenerator.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_THEMES = g.PLANTUML_THEMES || {};\n");
		out.append("\n");
		for (String name : names) {
			final File file = new File(themesDir, ThemeUtils.getFilename(name));
			if (file.isFile() == false)
				throw new IOException("Theme file not found: " + file.getAbsolutePath());

			final String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)
					.replace("\r\n", "\n");
			out.append("g.PLANTUML_THEMES[").append(toJsLiteral(name)).append("]=").append(toJsLiteral(content))
					.append(";\n");
		}
		out.append("})();\n");
		Files.write(output.toPath(), out.toString().getBytes(StandardCharsets.UTF_8));

		System.out.println("Generated " + output.getAbsolutePath() + " (" + names.size() + " themes)");
	}

	/**
	 * Lists the bundled theme names by scanning the themes directory, so that a
	 * newly added puml-theme-*.puml file is picked up without any other step.
	 */
	private static List<String> listThemeNames(File themesDir) throws IOException {
		final String[] files = themesDir.list();
		if (files == null)
			throw new IOException("Cannot list " + themesDir.getAbsolutePath());

		final List<String> result = new ArrayList<>();
		for (String file : files)
			if (file.startsWith(THEME_FILE_PREFIX) && file.endsWith(THEME_FILE_SUFFIX))
				result.add(file.substring(THEME_FILE_PREFIX.length(),
						file.length() - THEME_FILE_SUFFIX.length()));

		Collections.sort(result);
		return result;
	}

	/**
	 * Renders a string as a JS double-quoted literal. Everything outside printable
	 * ASCII is escaped as \\uXXXX so the generated file stays pure ASCII.
	 */
	private static String toJsLiteral(String value) {
		final StringBuilder sb = new StringBuilder("\"");
		for (int i = 0; i < value.length(); i++) {
			final char c = value.charAt(i);
			switch (c) {
			case '"':
				sb.append("\\\"");
				break;
			case '\\':
				sb.append("\\\\");
				break;
			case '\n':
				sb.append("\\n");
				break;
			case '\r':
				sb.append("\\r");
				break;
			case '\t':
				sb.append("\\t");
				break;
			default:
				if (c < 0x20 || c > 0x7E)
					sb.append(String.format("\\u%04x", (int) c));
				else
					sb.append(c);
			}
		}
		return sb.append('"').toString();
	}

	private ThemesJsGenerator() {
	}

}