diff --git a/JavaTemplated/.gitignore b/JavaTemplated/.gitignore new file mode 100644 index 00000000..d29657b9 --- /dev/null +++ b/JavaTemplated/.gitignore @@ -0,0 +1,13 @@ +# Local checkout of https://github.com/openjdk/jdk17 - fetched fresh per the README, not committed. +jdk17/ + +# Gradle build output, both subprojects. +*/build/ +*/.gradle/ + +# Generated docs - these are build output, not source. +dokka-java-base-docs/build/dokka-json-output/ +html-output/ + +# Bisection tool run logs (not needed to build, only useful while actively debugging). +*.log diff --git a/JavaTemplated/README.md b/JavaTemplated/README.md new file mode 100644 index 00000000..0329d377 --- /dev/null +++ b/JavaTemplated/README.md @@ -0,0 +1,137 @@ +# JavaTemplated + +Generates HTML API documentation for OpenJDK 17's `java.base` module: Dokka (with the +`kdoc-to-json` plugin from [`../Dokka-plugin-kdoc2json`](../Dokka-plugin-kdoc2json)) +produces JSON, then a small Pebble-based renderer turns that JSON into HTML. + +``` +jdk17 source --[Dokka + kdoc-to-json]--> JSON --[Pebble template]--> HTML +``` + +## Layout + +- **`dokka-java-base-docs/`** - the Dokka project. `build.gradle.kts` points Dokka's Java + source set at a `jdk17` checkout (see below) and applies `kdoc-to-json` so the + `dokkaGenerateHtml` task writes JSON instead of HTML. Also bakes in a required + workaround - see "The Dokka bug" below. + - `bisect_inheritdoc.py` - the tool that found every file in that workaround. Rerun it + if a JDK/Dokka/plugin version bump reintroduces the crash on a new file. +- **`pebble-renderer/`** - a small Kotlin CLI (`RenderHtml.kt`) that walks a directory of + `*.json` files, evaluates each one through a Pebble template, and writes the result to + the same relative path with a `.html` extension - so the output tree mirrors the JSON + tree. +- **`peb.peb.txt`** - the actual Pebble template consumed by `pebble-renderer`. Renders + Dokka's `kind`-discriminated JSON shapes (module / package / class / function / + property / ...) into styled HTML pages, with internal cross-references rewritten from + `.json` to `.html` so links between generated pages resolve correctly. + +Not checked in - fetched or generated locally (see `.gitignore`): +- `jdk17/` - the OpenJDK source, cloned fresh as a sibling of `dokka-java-base-docs/`. +- `dokka-java-base-docs/build/dokka-json-output/` - Dokka's JSON output. +- `html-output/` (or wherever you point the renderer) - the final HTML. + +## Prerequisites + +A JDK 17 install, distinct from whatever this machine's default `java` is - the Kotlin +Gradle plugin `kdoc-to-json` depends on doesn't support newer JDK targets, and +`compileJava`/`compileKotlin` need to agree on one. `bisect_inheritdoc.py` finds one +automatically (via macOS's `java_home -v 17`, overridable with `JDK17_HOME`); for the raw +`./gradlew` invocations below, export it yourself, e.g.: + +```bash +export JAVA_HOME=$(/usr/libexec/java_home -v 17) # macOS +# or, if you don't have one: brew install openjdk@17 +``` + +## 1. Fetch the JDK 17 source + +```bash +cd JavaTemplated +git clone --no-checkout --depth 1 --filter=blob:none https://github.com/openjdk/jdk17.git jdk17 +cd jdk17 +git sparse-checkout init --cone +git sparse-checkout set src/java.base +git checkout +``` + +(A full checkout works too; `dokka-java-base-docs/build.gradle.kts` only reads +`src/java.base/share/classes`. The sparse checkout above just avoids pulling ~30 other +modules you don't need.) + +## 2. Build and publish the kdoc-to-json plugin + +```bash +cd ../../Dokka-plugin-kdoc2json/kdoc-to-json +./gradlew clean publishToMavenLocal +``` + +This publishes `org.appdevforall.dokka:kdoc-to-json:1.0.0-SNAPSHOT` to `~/.m2`, which +`dokka-java-base-docs/build.gradle.kts` depends on. + +## 3. Generate the JSON + +```bash +cd ../../JavaTemplated/dokka-java-base-docs +export JAVA_TOOL_OPTIONS="-Xss256m" # see "The Dokka bug" below +./gradlew dokkaGenerateHtml +``` + +(The task is still named `dokkaGenerateHtml` - `kdoc-to-json` overrides Dokka's renderer +to emit JSON instead, it doesn't rename the task.) Output lands in +`build/dokka-json-output/` - 22,209 files, ~215MB for the full module. + +## 4. Render the JSON to HTML + +```bash +cd ../pebble-renderer +./gradlew run --args="../dokka-java-base-docs/build/dokka-json-output ../html-output" +``` + +The Pebble template defaults to `../peb.peb.txt` (this project's sibling above); pass a +third arg to `--args` to use a different one. See `pebble-renderer`'s own usage text +(`./gradlew run` with no args) for details. + +## The Dokka bug + +A full `java.base` run crashes with `java.lang.StackOverflowError` deep in Dokka's own +Java `{@inheritDoc}` resolver +(`org.jetbrains.dokka.analysis.java.parsers.doctag.PsiElementToHtmlConverter`) - a real, +still-open upstream bug ([kotlin/dokka#2171](https://github.com/Kotlin/dokka/issues/2171)), +not something introduced by `kdoc-to-json` or this project's config. Confirmed independent +of stack size (crashes identically at a 256MB thread stack, 512x the JVM default) and +**not** fixed by Dokka 2.2.0 GA (`kdoc-to-json` normally targets 2.2.0-Beta). + +`bisect_inheritdoc.py` binary-searches subsets of a package's source files (via a staging +directory of symlinks passed as Dokka's sole `sourceRoots` entry - the only mechanism that +actually excludes a file from analysis; Dokka re-walks each `sourceRoots` directory from +disk rather than respecting Gradle-level `FileTree`/`suppressedFiles` filters) to find the +minimal file or file-pair that reproduces the crash. + +`dokka-java-base-docs/build.gradle.kts` hard-excludes the 9 pairs (18 files) found this +way - each a class alongside its immediate super/interface, both carrying heavy +`{@inheritDoc}` javadoc: + +| Pair | Package | +| --- | --- | +| `Executable` / `Constructor` | `java.lang.reflect` | +| `AccessibleObject` / `Field` | `java.lang.reflect` | +| `BufferedReader` / `LineNumberReader` | `java.io` | +| `AbstractList` / `AbstractSequentialList` | `java.util` | +| `NavigableMap` / `TreeMap` | `java.util` | +| `NavigableSet` / `TreeSet` | `java.util` | +| `ConcurrentNavigableMap` / `ConcurrentSkipListMap` | `java.util.concurrent` | +| `ScheduledThreadPoolExecutor` / `ThreadPoolExecutor` | `java.util.concurrent` | +| `BlockingDeque` / `LinkedBlockingDeque` | `java.util.concurrent` | + +That's 18 of ~2,750 `java.base` source files (0.65%) missing their own page; everything +else - including the *other* half of each pair above - documents normally. If this needs +revisiting, rerun `bisect_inheritdoc.py ` for any package that fails and fold the +newly found files into the exclude list in `build.gradle.kts`. + +## Not carried over from the original investigation + +A one-off fork of `kdoc-to-json` rebuilt against Dokka 2.2.0 GA (to test whether a newer +Dokka fixed the bug above - it didn't) isn't included here; it added nothing to the +working pipeline. Raw bisection/build logs from that investigation also aren't included - +none of it is needed to build the docs, only `bisect_inheritdoc.py` itself is kept, for +future maintenance. diff --git a/JavaTemplated/dokka-java-base-docs/bisect_inheritdoc.py b/JavaTemplated/dokka-java-base-docs/bisect_inheritdoc.py new file mode 100644 index 00000000..0ac5d1cc --- /dev/null +++ b/JavaTemplated/dokka-java-base-docs/bisect_inheritdoc.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""Binary-search bisection to find the minimal set of .java files in a java.base package +that trigger Dokka's StackOverflowError (github.com/Kotlin/dokka/issues/2171 - an +unresolvable {@inheritDoc}/related tag during Java analysis). + +IMPORTANT: uses TRUE inclusion via a staging directory of symlinks passed as the sole +sourceRoots entry (-PabsSourceRoot). An earlier version of this script used the +`suppressedFiles` Dokka option to exclude candidates instead, which turned out to be a +dead end: suppressedFiles only filters final output pages - the analysis phase where the +crash happens still processes every file under sourceRoots regardless of it. Confirmed by +direct experiment: suppressing 268/269 files still crashed identically, but truly +restricting sourceRoots to that same 1 file (via a symlink staging dir) built successfully. + +Usage: python3 bisect_inheritdoc.py [java/io ...] +""" +import glob +import os +import shutil +import subprocess +import sys +import tempfile + +PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) +JDK_BASE_CLASSES = os.path.abspath( + os.path.join(PROJECT_DIR, "..", "jdk17", "src", "java.base", "share", "classes") +) +GRADLEW = os.path.join(PROJECT_DIR, "gradlew") + + +def find_jdk17_home(): + """The Kotlin Gradle plugin kdoc-to-json depends on doesn't support this machine's + default JDK as a compile target if it's newer than 17, so JAVA_HOME must be pinned to + an actual JDK 17 install. Set JDK17_HOME to override; otherwise this tries macOS's + `java_home` locator (install one with e.g. `brew install openjdk@17` if it fails).""" + override = os.environ.get("JDK17_HOME") + if override: + return override + try: + result = subprocess.run( + ["/usr/libexec/java_home", "-v", "17"], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except Exception as e: + raise RuntimeError( + "Could not locate a JDK 17 installation via /usr/libexec/java_home. Install " + "one (e.g. `brew install openjdk@17`) or set the JDK17_HOME environment " + "variable to its home directory." + ) from e + + +ENV = dict(os.environ) +ENV["JAVA_HOME"] = find_jdk17_home() +ENV["JAVA_TOOL_OPTIONS"] = "-Xss256m" + +test_count = 0 + +# Files already confirmed (via prior bisection runs on java/lang, java/io, java/util in +# isolation) to trigger github.com/Kotlin/dokka/issues/2171 - excluded up front so a +# broader run (e.g. the whole "java" top-level dir, all 9 subpackages together) looks for +# NEW combos instead of rediscovering these. +KNOWN_BAD = { + os.path.join(JDK_BASE_CLASSES, rel) + for rel in [ + "java/lang/reflect/Constructor.java", + "java/lang/reflect/Executable.java", + "java/lang/reflect/AccessibleObject.java", + "java/lang/reflect/Field.java", + "java/io/BufferedReader.java", + "java/io/LineNumberReader.java", + "java/util/AbstractList.java", + "java/util/AbstractSequentialList.java", + "java/util/NavigableMap.java", + "java/util/TreeMap.java", + "java/util/concurrent/ConcurrentNavigableMap.java", + "java/util/concurrent/ConcurrentSkipListMap.java", + "java/util/concurrent/ScheduledThreadPoolExecutor.java", + "java/util/concurrent/ThreadPoolExecutor.java", + ] +} + + +def all_java_files(pkg): + files = sorted( + glob.glob(os.path.join(JDK_BASE_CLASSES, pkg, "**", "*.java"), recursive=True) + ) + return [f for f in files if f not in KNOWN_BAD] + + +def make_staging_dir(included): + staging = tempfile.mkdtemp(prefix="dokka-stage-", dir="/tmp") + for f in included: + rel = os.path.relpath(f, JDK_BASE_CLASSES) + dest = os.path.join(staging, rel) + os.makedirs(os.path.dirname(dest), exist_ok=True) + os.symlink(f, dest) + return staging + + +def test_fails(included): + """True if this exact set of files, as the ONLY sourceRoots content, reproduces the + StackOverflowError. False if it builds successfully. Raises on anything else.""" + global test_count + test_count += 1 + staging = make_staging_dir(included) + try: + proc = subprocess.run( + [GRADLEW, "dokkaGenerateHtml", f"-PabsSourceRoot={staging}"], + cwd=PROJECT_DIR, + env=ENV, + capture_output=True, + text=True, + timeout=300, + ) + finally: + shutil.rmtree(staging, ignore_errors=True) + + out = proc.stdout + proc.stderr + names = ", ".join(os.path.basename(f) for f in included) if len(included) <= 8 else "" + print(f" [test {test_count}] {len(included)} files {names} -> ", end="", flush=True) + if "BUILD SUCCESSFUL" in out: + print("PASS") + return False + if "StackOverflowError" in out: + print("FAIL (StackOverflowError)") + return True + print("UNEXPECTED RESULT") + print(out[-4000:]) + raise RuntimeError(f"Unexpected gradle result for {len(included)} files") + + +def shrink_with_anchor(pool, anchor): + """pool + anchor is known to fail. Binary-search pool (keeping the full anchor fixed + and present in every test) down to a minimal subset that, combined with anchor, still + fails. Scales as O(log n) instead of the combinatorial blowup of trying every subset.""" + if len(pool) <= 1: + return pool + mid = len(pool) // 2 + left, right = pool[:mid], pool[mid:] + if test_fails(left + anchor): + return shrink_with_anchor(left, anchor) + if test_fails(right + anchor): + return shrink_with_anchor(right, anchor) + return pool # both halves need each other too - can't shrink further this way + + +def find_minimal_combo(left, right): + """left+right is confirmed to fail, but neither alone does - the crash needs files + from both sides. Alternately shrink each side while anchoring the other, converging + on a small (not always provably minimal, but tight) cross-cutting combo.""" + print(f" -- {len(left) + len(right)} files fail together but neither half alone does; " + f"anchored-shrinking to find the cross-cutting combo") + min_left = shrink_with_anchor(left, right) + min_right = shrink_with_anchor(right, min_left) + # One more pass: min_right may be small enough now to shrink min_left further. + min_left = shrink_with_anchor(min_left, min_right) + combo = min_left + min_right + assert test_fails(combo), "combo lost the failure during minimization - bug in shrink logic" + return combo + + +def find_bad(candidates): + if not candidates: + return [] + if not test_fails(candidates): + return [] + if len(candidates) == 1: + print(f" >>> BAD FILE: {candidates[0]}") + return candidates + mid = len(candidates) // 2 + left, right = candidates[:mid], candidates[mid:] + bad_left = find_bad(left) + bad_right = find_bad(right) + if bad_left or bad_right: + return bad_left + bad_right + combo = find_minimal_combo(left, right) + print(f" >>> MINIMAL FAILING COMBO ({len(combo)} files): {[os.path.basename(f) for f in combo]}") + # BUG FIX: finding one combo does NOT prove the rest of this branch is clean - a + # second, independent problem could be hiding in the same left+right split. Recurse + # into what's left after removing the combo to be sure. (This was missing in the + # first few runs, which is why re-testing "java" with all found files excluded still + # crashed - at least one more combo was hiding, unverified, in an already-resolved + # branch.) + combo_set = set(combo) + remaining = [f for f in candidates if f not in combo_set] + more_bad = find_bad(remaining) + return combo + more_bad + + +def main(): + packages = sys.argv[1:] + if not packages: + print(__doc__) + sys.exit(1) + + results = {} + for pkg in packages: + print(f"=== Bisecting {pkg} ===", flush=True) + files = all_java_files(pkg) + print(f" {len(files)} total .java files under {pkg}") + bad = find_bad(files) + results[pkg] = bad + print(f" {pkg}: found {len(bad)} minimal-failing file(s) after {test_count} tests total so far") + + print("\n=== SUMMARY ===") + for pkg, bad in results.items(): + print(f"{pkg}: {len(bad)} file(s) in minimal failing set") + for f in bad: + print(f" - {f}") + + +if __name__ == "__main__": + main() diff --git a/JavaTemplated/dokka-java-base-docs/build.gradle.kts b/JavaTemplated/dokka-java-base-docs/build.gradle.kts new file mode 100644 index 00000000..a975cf5c --- /dev/null +++ b/JavaTemplated/dokka-java-base-docs/build.gradle.kts @@ -0,0 +1,129 @@ +// Standalone Dokka project used to generate JSON API docs for the OpenJDK 17 `java.base` +// module (https://github.com/openjdk/jdk17, src/java.base) via the kdoc-to-json plugin +// (../../ADFA/DocsPipeline/OfflineDocumentationTools/Dokka-plugin-kdoc2json). No Kotlin/Java +// compilation happens here - Dokka only reads the source tree - so no `java`/`kotlin` plugin +// is applied, just `org.jetbrains.dokka` with a manually-declared source set. +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.gradle.engine.plugins.DokkaPluginParametersBaseSpec +import javax.inject.Inject + +plugins { + // Must match the dokka-core/dokka-base version kdoc-to-json/build.gradle.kts was + // compiled against, or the plugin may fail to load or behave unexpectedly. + id("org.jetbrains.dokka") version "2.2.0" +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + dokkaPlugin("org.appdevforall.dokka:kdoc-to-json:1.0.0-dokka2.2-SNAPSHOT") +} + +@OptIn(InternalDokkaApi::class) +abstract class JsonOutputPluginParameters @Inject constructor( + name: String +) : DokkaPluginParametersBaseSpec(name, "org.appdevforall.dokka.kdoc2json.JsonOutputPlugin") { + override fun jsonEncode(): String = """{ + "logLevel": "debug", + "logFile": "build/dokka_json.log", + "omitFields": ["sources"], + "replaceHtmlExtension": true, + "omitNulls": true, + "prettyPrint": true + }""" +} + +// jdk17 checkout lives one directory up, sparse-checked-out to just src/java.base. +val jdkBaseClasses = file("../jdk17/src/java.base/share/classes") + +// Bisection knob: -PsrcRoots=java/util,java/lang restricts the source set to just those +// dirs (relative to jdkBaseClasses) instead of the full java/javax/jdk/sun/com set, so a +// StackOverflowError can be narrowed down to a specific package without editing this file. +val srcRootsProp = (findProperty("srcRoots") as String?) +val defaultRoots = listOf("java", "javax", "jdk", "sun", "com") +val selectedRoots = srcRootsProp?.split(",")?.map { it.trim() } ?: defaultRoots + +// Every pair below triggers https://github.com/Kotlin/dokka/issues/2171 (a genuine +// StackOverflowError deep in Dokka's own Java {@inheritDoc} resolver, confirmed via +// bisect_inheritdoc.py - reproduces even at 256MB thread stack, so it's a true infinite +// loop, not just deep recursion). Each pair is a class alongside its immediate +// super/interface, both carrying heavy {@inheritDoc} javadoc - excluding either file in a +// pair is enough to avoid the crash, at the cost of that one class's page. Confirmed newer +// Dokka (2.2.0 GA vs the 2.2.0-Beta kdoc-to-json normally targets) does NOT fix this. +val dokka2171ExcludedFiles = listOf( + "java/lang/reflect/Constructor.java", + "java/lang/reflect/Executable.java", + "java/lang/reflect/AccessibleObject.java", + "java/lang/reflect/Field.java", + "java/io/BufferedReader.java", + "java/io/LineNumberReader.java", + "java/util/AbstractList.java", + "java/util/AbstractSequentialList.java", + "java/util/NavigableMap.java", + "java/util/TreeMap.java", + "java/util/concurrent/ConcurrentNavigableMap.java", + "java/util/concurrent/ConcurrentSkipListMap.java", + "java/util/concurrent/ScheduledThreadPoolExecutor.java", + "java/util/concurrent/ThreadPoolExecutor.java", + "java/util/NavigableSet.java", + "java/util/TreeSet.java", + "java/util/concurrent/BlockingDeque.java", + "java/util/concurrent/LinkedBlockingDeque.java", +) + +dokka { + moduleName.set("java.base") + + dokkaSourceSets.create("java_base") { + displayName.set("java.base") + jdkVersion.set(17) + + // Bisection knob: -PabsSourceRoot=/tmp/staging-dir overrides everything below with a + // single arbitrary directory (e.g. a staging tree of symlinks mirroring the real + // java/lang/... package layout for just a candidate subset of files) - used to + // binary-search for the specific file(s) that trigger the crash above. NOTE: + // suppressedFiles is NOT used for this, because it only filters final output pages - + // the analysis phase where the crash happens still processes every file under + // sourceRoots regardless of suppressedFiles, so excluding candidates that way doesn't + // actually change what gets analyzed. + val absSourceRoot = (findProperty("absSourceRoot") as String?) + if (absSourceRoot != null) { + sourceRoots.from(file(absSourceRoot)) + } else { + // A fileTree(dir) { exclude(...) } does NOT work here - confirmed by direct + // experiment. Dokka's Java analysis re-walks each sourceRoots entry as a real + // directory on disk rather than iterating Gradle's already-filtered FileTree, so + // Gradle-level excludes are silently ignored and the excluded files get analyzed + // anyway. The only mechanism that reliably keeps a file out of analysis is for it + // to not physically exist under the sourceRoot on disk - so build a staging + // directory of symlinks (mirroring the real java/javax/jdk/sun/com package + // layout) that just omits the known-bad files, and hand Dokka that single + // directory. Rebuilt fresh on every configure - cheap (symlinks only, no copying). + val stagingDir = layout.buildDirectory.dir("dokka-source-staging").get().asFile + stagingDir.deleteRecursively() + selectedRoots.forEach { root -> + jdkBaseClasses.resolve(root).walkTopDown().filter { it.isFile }.forEach { src -> + val rel = src.relativeTo(jdkBaseClasses).invariantSeparatorsPath + if (rel !in dokka2171ExcludedFiles) { + val dest = stagingDir.resolve(rel) + dest.parentFile.mkdirs() + java.nio.file.Files.createSymbolicLink(dest.toPath(), src.toPath()) + } + } + } + sourceRoots.from(stagingDir) + } + } + + pluginsConfiguration { + registerBinding(JsonOutputPluginParameters::class, JsonOutputPluginParameters::class) + register("org.appdevforall.dokka.kdoc2json.JsonOutputPlugin") { } + } + + dokkaPublications.html { + outputDirectory.set(layout.buildDirectory.dir("dokka-json-output")) + } +} diff --git a/JavaTemplated/dokka-java-base-docs/gradle/wrapper/gradle-wrapper.jar b/JavaTemplated/dokka-java-base-docs/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..1b33c55b Binary files /dev/null and b/JavaTemplated/dokka-java-base-docs/gradle/wrapper/gradle-wrapper.jar differ diff --git a/JavaTemplated/dokka-java-base-docs/gradle/wrapper/gradle-wrapper.properties b/JavaTemplated/dokka-java-base-docs/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..df6a6ad7 --- /dev/null +++ b/JavaTemplated/dokka-java-base-docs/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/JavaTemplated/dokka-java-base-docs/gradlew b/JavaTemplated/dokka-java-base-docs/gradlew new file mode 100755 index 00000000..b9bb139f --- /dev/null +++ b/JavaTemplated/dokka-java-base-docs/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/JavaTemplated/dokka-java-base-docs/gradlew.bat b/JavaTemplated/dokka-java-base-docs/gradlew.bat new file mode 100644 index 00000000..aa5f10b0 --- /dev/null +++ b/JavaTemplated/dokka-java-base-docs/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/JavaTemplated/dokka-java-base-docs/settings.gradle.kts b/JavaTemplated/dokka-java-base-docs/settings.gradle.kts new file mode 100644 index 00000000..a16c4cc4 --- /dev/null +++ b/JavaTemplated/dokka-java-base-docs/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "dokka-java-base-docs" diff --git a/JavaTemplated/peb.peb.txt b/JavaTemplated/peb.peb.txt new file mode 100644 index 00000000..ca669eba --- /dev/null +++ b/JavaTemplated/peb.peb.txt @@ -0,0 +1,487 @@ +{# --- MACROS --- #} +{%- macro renderPlatforms(sourceSets) -%} +{%- if sourceSets is defined and sourceSets is not null and sourceSets is not empty %} +
+{%- for sourceSet in sourceSets %} +{%- set parts = sourceSet | split("/") %} +{{ parts | last }} +{%- endfor %} +
+{%- endif %} +{%- endmacro -%} + +{%- macro renderDoc(docMap) -%} +{%- if docMap != null %} +{%- for entry in docMap %} +{%- if loop.first %} +{%- for tag in entry.value %} +
+{%- if tag.type == "Description" %} +
{{ tag.text | replace({".json": ".html"}) | raw }}
+{%- elseif tag.type == "Param" %} +
+@param {{ tag.name }}{{ " " }}{{ tag.text | replace({".json": ".html"}) | raw }} +
+{%- elseif tag.type == "Return" %} +
+@return{{ " " }}{{ tag.text | replace({".json": ".html"}) | raw }} +
+{%- elseif tag.type == "CustomTagWrapper" and tag.name == "Since Kotlin" %} +

Since Kotlin

{{ tag.text | replace({".json": ".html"}) | raw }}
+{%- elseif tag.type == "Sample" %} +
+

Samples

+
+{# Not filtered - this is literal sample source code, not doc prose, so a coincidental + ".json" substring here shouldn't be rewritten. #} +
{{ tag.text | raw }}
+
+
+{%- else %} +
+@{{ tag.type }}{{ " " }}{%- if tag.name != null %}{{ tag.name }}{%- endif %}{{ " " }}{{ tag.text | replace({".json": ".html"}) | raw }} +
+{%- endif %} +
+{%- endfor %} +{%- endif %} +{%- endfor %} +{%- endif %} +{%- endmacro -%} + +{%- macro renderType(typeDto) -%} +{%- if typeDto != null %} +{%- if typeDto.kind == "Star" %} +* +{%- elseif typeDto.kind == "Covariance" or typeDto.kind == "Contravariance" or typeDto.kind == "Invariance" %} +{{- renderType(typeDto.inner) }} +{%- elseif typeDto.kind == "Nullable" or typeDto.kind == "DefinitelyNonNullable" %} +{{- renderType(typeDto.inner) }}{%- if typeDto.kind == "Nullable" %}?{%- endif %} +{%- elseif typeDto.kind == "TypeAliased" %} +{{- renderType(typeDto.typeAlias) }} +{%- elseif typeDto.kind == "JavaObject" %} +Object +{%- elseif typeDto.kind == "Void" %} +Void +{%- elseif typeDto.kind == "Dynamic" %} +dynamic +{%- elseif typeDto.kind == "FunctionalTypeConstructor" %} +{%- if typeDto.isSuspendable %}suspend{{ " " }}{%- endif %} +{%- if typeDto.isExtensionFunction %} +{{- renderType(typeDto.projections | first) }}.({%- if typeDto.projections | length > 2 %}{%- for proj in typeDto.projections | slice(1, typeDto.projections | length - 1) %}{{ renderType(proj) }}{%- if not loop.last %},{{ " " }}{%- endif %}{%- endfor %}{%- endif %}) ->{{ " " }}{{- renderType(typeDto.projections | last) }} +{%- else %} +({%- if typeDto.projections | length > 1 %}{%- for proj in typeDto.projections | slice(0, typeDto.projections | length - 1) %}{{ renderType(proj) }}{%- if not loop.last %},{{ " " }}{%- endif %}{%- endfor %}{%- endif %}) ->{{ " " }}{{- renderType(typeDto.projections | last) }} +{%- endif %} +{%- else %} +{%- set display = "Unknown" %} +{%- if typeDto.name != null and typeDto.name != "" %} +{%- set display = typeDto.name %} +{%- elseif typeDto.dri != null %} +{%- set driParts = typeDto.dri | split("/") %} +{%- if driParts | length > 1 and driParts[1] != "" %} +{%- set display = driParts[1] | replace({"%20": " "}) %} +{%- endif %} +{%- endif %} +{%- if typeDto.presentableName != null and typeDto.presentableName != "" %} +{{- typeDto.presentableName }}:{{ " " }}{%- endif %} +{%- if typeDto.url != null %} +{{ display }} +{%- else %} +{{ display }} +{%- endif %} +{%- if typeDto.projections is defined and typeDto.projections is not empty %} +< +{%- for proj in typeDto.projections %} +{{- renderType(proj) }}{%- if not loop.last %},{{ " " }}{%- endif %} +{%- endfor %} +> +{%- endif %} +{%- endif %} +{%- endif %} +{%- endmacro -%} + +{%- macro renderTableSignature(member) -%} +
+{%- if member.extras is defined and member.extras.annotations is not empty %} +{%- for entry in member.extras.annotations %} +{%- if loop.first %} +{%- for anno in entry.value %} +{%- set annoName = anno.dri | split("/") %} +
+{%- if anno.url != null %} +@{{ annoName[1] }} +{%- else %} +@{{ annoName[1] }} +{%- endif %} +
+{%- endfor %} +{%- endif %} +{%- endfor %} +{%- endif %} + +{%- if member.modifier is defined and member.modifier is not empty %} +{%- for entry in member.modifier %} +{%- if loop.first and entry.value != "final" and entry.value != "public" %} +{{ entry.value }}{{ " " }} +{%- endif %} +{%- endfor %} +{%- endif %} + +{%- if member.extras is defined and member.extras.additionalModifiers is not empty %} +{%- for entry in member.extras.additionalModifiers %} +{%- if loop.first %} +{%- for mod in entry.value %} +{{ mod }}{{ " " }} +{%- endfor %} +{%- endif %} +{%- endfor %} +{%- endif %} + +{%- if member.kind == "function" %} +fun{{ " " }} +{%- if member.generics is defined and member.generics is not empty %} +< +{%- for gen in member.generics %} +{{- gen.name }} +{%- if not loop.last %},{{ " " }}{%- endif %} +{%- endfor %} +>{{ " " }} +{%- endif %} +{%- if member.receiver is defined and member.receiver is not null %} +{{- renderType(member.receiver.type) }}. +{%- endif %} +{{- member.name }}( +{%- if member.parameters is defined and member.parameters is not null %} +{%- for param in member.parameters %} +{{- param.name }}:{{ " " }}{{- renderType(param.type) }} +{%- if param.extras is defined and param.extras.defaultValues is not empty %} +{{ " = " }} +{%- for dEntry in param.extras.defaultValues %} +{%- if loop.first %}{{ dEntry.value | replace({"\"": """}) }}{%- endif %} +{%- endfor %} +{%- endif %} +{%- if not loop.last %},{{ " " }}{%- endif %} +{%- endfor %} +{%- endif %} +) +{%- if member.type is defined and member.type is not null %} +:{{ " " }}{{- renderType(member.type) }} +{%- endif %} +{%- elseif member.kind == "property" %} +val{{ " " }} +{%- if member.receiver is defined and member.receiver is not null %} +{{- renderType(member.receiver.type) }}. +{%- endif %} +{{- member.name }} +{%- if member.type is defined and member.type is not null %} +:{{ " " }}{{- renderType(member.type) }} +{%- endif %} +{%- elseif member.kind == "typeAlias" %} +typealias {{ member.name }} ={{ " " }} +{%- if member.underlyingType is defined and member.underlyingType is not null %} +{%- for entry in member.underlyingType %} +{%- if loop.first %} +{{- renderType(entry.value) }} +{%- endif %} +{%- endfor %} +{%- endif %} +{%- elseif member.kind == "class" or member.kind == "interface" or member.kind == "object" or member.kind == "enum" %} +{{ member.kind }} {{ member.name }} +{%- endif %} +
+{%- endmacro -%} + +{%- macro renderMembers(title, members) -%} +{%- if members is not empty %} +
+

{{ title }}

+ + + + + + + + +{%- for member in members %} +{%- if member.extras is not defined or not member.extras.isObviousMember %} + + + + +{%- endif %} +{%- endfor %} + +
NameSummary
+{{ member.name | default('Unknown') }} +{{- renderPlatforms(member.sourceSets) }} + +{{- renderTableSignature(member) }} +{%- if member.documentation is defined and member.documentation is not null %} +{%- for entry in member.documentation %} +{%- if loop.first %} +{%- for tag in entry.value %} +{%- if tag.type == "Description" %} +
{{ tag.text | replace({".json": ".html"}) | raw }}
+{%- endif %} +{%- endfor %} +{%- endif %} +{%- endfor %} +{%- endif %} +
+
+{%- endif %} +{%- endmacro -%} + +{# --- HTML TEMPLATE --- #} + + + + + +{{ name }} + + + +
+ +
+
+

+ +{%- if kind == "allTypes" %} +Index +{%- else %} +{{ kind }} +{%- endif %} +{{ " " }}{{ name }} +

+{{- renderPlatforms(sourceSets) }} + +{%- if kind == "function" or kind == "property" or kind == "typeAlias" or kind == "class" or kind == "interface" or kind == "object" or kind == "enum" %} +
+{{- renderTableSignature(_context) }} +
+{%- endif %} + +
+{{- renderDoc(documentation) }} +
+ +{%- if modules is defined and modules is not null %} +{{- renderMembers("Modules", modules) }} +{%- endif %} +{%- if packages is defined and packages is not null %} +{{- renderMembers("Packages", packages) }} +{%- endif %} +{%- if classlikes is defined and classlikes is not null %} +{{- renderMembers("Types", classlikes) }} +{%- endif %} +{%- if typeAliases is defined and typeAliases is not null %} +{{- renderMembers("Type Aliases", typeAliases) }} +{%- endif %} +{%- if constructors is defined and constructors is not null %} +{{- renderMembers("Constructors", constructors) }} +{%- endif %} +{%- if properties is defined and properties is not null %} +{{- renderMembers("Properties", properties) }} +{%- endif %} +{%- if functions is defined and functions is not null %} +{{- renderMembers("Functions", functions) }} +{%- endif %} +{%- if entries is defined and entries is not null %} +{{- renderMembers("Enum Entries", entries) }} +{%- endif %} + +{# --- NEW: ALL TYPES INDEX RENDERER --- #} +{%- if types is defined and types is not null %} +
+

Index of All Types

+ + + + + + + + +{%- for typeEntry in types %} + + + + +{%- endfor %} + +
TypePlatforms
+{{ typeEntry.kind }} +{{ typeEntry.name }} + +{{- renderPlatforms(typeEntry.sourceSets) }} +
+
+{%- endif %} + +{%- if kind == "module" %} +
+

Index

+All Types +
+ +{%- endif %} +
+ + \ No newline at end of file diff --git a/JavaTemplated/pebble-renderer/build.gradle.kts b/JavaTemplated/pebble-renderer/build.gradle.kts new file mode 100644 index 00000000..eba2d054 --- /dev/null +++ b/JavaTemplated/pebble-renderer/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + kotlin("jvm") version "1.9.24" + application +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("io.pebbletemplates:pebble:4.1.2") + implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2") +} + +application { + mainClass.set("RenderHtmlKt") +} diff --git a/JavaTemplated/pebble-renderer/gradle/wrapper/gradle-wrapper.jar b/JavaTemplated/pebble-renderer/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..1b33c55b Binary files /dev/null and b/JavaTemplated/pebble-renderer/gradle/wrapper/gradle-wrapper.jar differ diff --git a/JavaTemplated/pebble-renderer/gradle/wrapper/gradle-wrapper.properties b/JavaTemplated/pebble-renderer/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..df6a6ad7 --- /dev/null +++ b/JavaTemplated/pebble-renderer/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/JavaTemplated/pebble-renderer/gradlew b/JavaTemplated/pebble-renderer/gradlew new file mode 100755 index 00000000..b9bb139f --- /dev/null +++ b/JavaTemplated/pebble-renderer/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/JavaTemplated/pebble-renderer/gradlew.bat b/JavaTemplated/pebble-renderer/gradlew.bat new file mode 100644 index 00000000..aa5f10b0 --- /dev/null +++ b/JavaTemplated/pebble-renderer/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/JavaTemplated/pebble-renderer/settings.gradle.kts b/JavaTemplated/pebble-renderer/settings.gradle.kts new file mode 100644 index 00000000..518ab513 --- /dev/null +++ b/JavaTemplated/pebble-renderer/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "pebble-renderer" diff --git a/JavaTemplated/pebble-renderer/src/main/kotlin/RenderHtml.kt b/JavaTemplated/pebble-renderer/src/main/kotlin/RenderHtml.kt new file mode 100644 index 00000000..71b2a144 --- /dev/null +++ b/JavaTemplated/pebble-renderer/src/main/kotlin/RenderHtml.kt @@ -0,0 +1,82 @@ +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import io.pebbletemplates.pebble.PebbleEngine +import io.pebbletemplates.pebble.loader.FileLoader +import java.io.File +import java.io.StringWriter + +/** + * Renders every *.json file under an input directory through a single Pebble template, + * writing each result to the same relative path under an output directory with a .html + * extension instead of .json - i.e. the output tree mirrors the input tree. + * + * Usage: RenderHtml [templatePath] + * inputDir directory to recursively search for *.json files + * outputDir directory to write the mirrored *.html tree into + * templatePath path to the Pebble template (default: ../peb.peb.txt, i.e. + * JavaTemplated/peb.peb.txt - this project's sibling in the repo) + */ +fun main(args: Array) { + if (args.size < 2) { + System.err.println("Usage: RenderHtml [templatePath]") + kotlin.system.exitProcess(1) + } + + val inputDir = File(args[0]).absoluteFile + val outputDir = File(args[1]).absoluteFile + // Relative to the current working directory, which is this project's directory when + // invoked the documented way (`cd pebble-renderer && ./gradlew run ...`) - making + // JavaTemplated/peb.peb.txt, this project's sibling in the repo, the default. + val templateFile = File(args.getOrElse(2) { "../peb.peb.txt" }).absoluteFile + + require(inputDir.isDirectory) { "Input directory not found: $inputDir" } + require(templateFile.isFile) { "Template file not found: $templateFile" } + + val loader = FileLoader(templateFile.parentFile.absolutePath) + val engine = PebbleEngine.Builder().loader(loader).build() + val template = engine.getTemplate(templateFile.name) + + val mapper = ObjectMapper() + val mapType = object : TypeReference>() {} + + var rendered = 0 + var failed = 0 + val failures = mutableListOf>() + + inputDir.walkTopDown() + .filter { it.isFile && it.extension == "json" } + .forEach { jsonFile -> + val relative = jsonFile.relativeTo(inputDir).path + val outFile = File(outputDir, relative.removeSuffix(".json") + ".html") + + try { + val context: Map = mapper.readValue(jsonFile, mapType) + + outFile.parentFile.mkdirs() + outFile.bufferedWriter().use { writer -> + template.evaluate(writer, context) + } + rendered++ + } catch (e: Exception) { + failed++ + failures.add(jsonFile to e) + } + + val total = rendered + failed + if (total % 1000 == 0) { + println("...$total files processed ($rendered rendered, $failed failed)") + } + } + + println() + println("Done. Rendered $rendered file(s) to $outputDir") + if (failed > 0) { + println("Failed to render $failed file(s):") + failures.take(20).forEach { (file, e) -> + println(" - ${file.relativeTo(inputDir)}: ${e::class.simpleName}: ${e.message}") + } + if (failures.size > 20) { + println(" ... and ${failures.size - 20} more") + } + } +}