From fad10876791880902ee0c6febdfb665f716eab23 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Wed, 26 Aug 2026 12:55:57 -0500 Subject: [PATCH 01/14] ADFA-5296: Add Javadoc mode to the kdoc-to-json plugin Setting "javadoc-mode": true in the plugin config replaces the Dokka-shaped JSON output with JSON that mirrors the api/ tree the javadoc tool produces: the same file layout, the same page sections, and the same member anchors. Only JSON is written (plus javadoc's plain-text element-list); rendering stays the downstream template engine's job. Layout: //.json, package-summary.json and module-summary.json, plus the global index files (index, allclasses-index, allpackages-index, deprecated-list, constant-values, index-files/index-N, element-list). Module directories appear only for a genuinely multi-module run, matching javadoc's own modular/non-modular split. Links between pages are relative to the page they appear on, as javadoc's are. New files: javadoc/JavadocPaths javadoc's path scheme and erased member anchors ((double,double), toArray(java.lang.Object[])) javadoc/JavadocModelIndex one whole-run pass building the type graph in both directions, so the hierarchy closures and inherited-member groups a javadoc page needs are precomputed rather than walked per page javadoc/JavadocDocs block-tag extraction and doc-comment rendering javadoc/JavadocDtos the page DTOs, kept separate from the Dokka-shaped hierarchy so neither constrains the other javadoc/JavadocMapper Dokka model -> javadoc pages javadoc/JavadocRenderer writes the tree JsonFilters omitFields/omitNulls, now shared by both renderers Reconciling Dokka's model with javadoc's view needed four corrections: - Dokka merges a private Java field and its accessors into one Kotlin-style property; that is unfolded back so getWidth() is a method and the private field is undocumented, as javadoc has it. - Dokka reports an interface default method only as "not abstract"; the default keyword is recovered from that. - Dokka files @param under "", not "U". - Constant values arrive wrapped as IntegerConstant(value=4) and are unwrapped to their literals. The config key is spelled javadoc-mode, which needs both @JsonProperty and @SerialName: Dokka parses pluginsConfiguration with Jackson, while JsonRenderer's manual fallback uses kotlinx.serialization, and either annotation alone leaves the key silently ignored on one of the two paths. Tests: tests/test_javadoc_mode.sh (58 assertions) drives a new Java example, examples/example-java-library, covering the layout, class/interface/enum/ annotation/exception pages, override and inherited-member derivation, the global index files, and cross-page link scoping. Full suite: 15/15 scripts pass. README section 10 documents the mode, the recommended documentedVisibilities setting for javadoc parity, and the limitations that come from Dokka's model (no JPMS, no annotation-element defaults, no records). Co-Authored-By: Claude Opus 5 --- Dokka-plugin-kdoc2json/README.md | 129 +++ .../example-java-library/build.gradle.kts | 58 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + .../examples/example-java-library/gradlew | 248 ++++++ .../examples/example-java-library/gradlew.bat | 82 ++ .../example-java-library/settings.gradle.kts | 1 + .../com/example/shapes/AbstractShape.java | 46 + .../main/java/com/example/shapes/Corner.java | 35 + .../java/com/example/shapes/Measured.java | 30 + .../java/com/example/shapes/Rectangle.java | 122 +++ .../main/java/com/example/shapes/Shape.java | 47 + .../com/example/shapes/ShapeException.java | 20 + .../main/java/com/example/shapes/Square.java | 18 + .../java/com/example/shapes/package-info.java | 9 + .../com/example/shapes/spi/ShapeFactory.java | 23 + .../com/example/shapes/spi/package-info.java | 6 + .../kdoc-to-json/build.gradle.kts | 5 + .../src/main/kotlin/JsonFilters.kt | 46 + .../src/main/kotlin/JsonPluginConfig.kt | 19 +- .../src/main/kotlin/JsonRenderer.kt | 64 +- .../src/main/kotlin/javadoc/JavadocDocs.kt | 241 +++++ .../src/main/kotlin/javadoc/JavadocDtos.kt | 387 +++++++++ .../src/main/kotlin/javadoc/JavadocExtras.kt | 15 + .../src/main/kotlin/javadoc/JavadocMapper.kt | 821 ++++++++++++++++++ .../main/kotlin/javadoc/JavadocModelIndex.kt | 372 ++++++++ .../src/main/kotlin/javadoc/JavadocPaths.kt | 119 +++ .../main/kotlin/javadoc/JavadocRenderer.kt | 501 +++++++++++ Dokka-plugin-kdoc2json/tests/lib.sh | 49 ++ .../tests/test_javadoc_mode.sh | 181 ++++ 30 files changed, 3662 insertions(+), 41 deletions(-) create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/build.gradle.kts create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/gradle/wrapper/gradle-wrapper.jar create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/gradle/wrapper/gradle-wrapper.properties create mode 100755 Dokka-plugin-kdoc2json/examples/example-java-library/gradlew create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/gradlew.bat create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/settings.gradle.kts create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/AbstractShape.java create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Corner.java create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Measured.java create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Rectangle.java create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Shape.java create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/ShapeException.java create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Square.java create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/package-info.java create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/spi/ShapeFactory.java create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/spi/package-info.java create mode 100644 Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonFilters.kt create mode 100644 Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDocs.kt create mode 100644 Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt create mode 100644 Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocExtras.kt create mode 100644 Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt create mode 100644 Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocModelIndex.kt create mode 100644 Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocPaths.kt create mode 100644 Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocRenderer.kt create mode 100755 Dokka-plugin-kdoc2json/tests/test_javadoc_mode.sh diff --git a/Dokka-plugin-kdoc2json/README.md b/Dokka-plugin-kdoc2json/README.md index 086eed20..85f7a2ef 100644 --- a/Dokka-plugin-kdoc2json/README.md +++ b/Dokka-plugin-kdoc2json/README.md @@ -110,6 +110,7 @@ dokka { | `classDiscriminator` | String | `"kind"` | The JSON key used to discriminate between polymorphic `Documentable` types (e.g., `"kind": "class"`). Must not collide with an existing DTO field name (e.g. `"type"` or `"name"`), or serialization will fail. | | `prettyPrint` | Boolean | `false` | If `true`, formats the written JSON files with indentation for human readability instead of compact single-line output. | | `sourceSetWhitelist` | List | `[]` | A list of source set names (matching the values that appear in the output `sourceSets` field, e.g. `["jvm"]`). If non-empty, any Documentable that isn't present in at least one whitelisted source set has its output file omitted, and a message is logged with the symbol's name and its `sourceSets`. Leave empty to disable filtering (default: all source sets included). | +| `javadoc-mode` | Boolean | `false` | If `true`, emit **javadoc-shaped** JSON mirroring the `api/` tree of the `javadoc` tool instead of Dokka-shaped JSON. See [§10](#10-javadoc-mode). Note the kebab-case key -- it is spelled that way in the config, unlike every other option here. | > **`omitNulls` also strips *empty* values, not just `null`.** Despite the name, `omitNulls: true` removes a key whenever its value is `null`, `""`, `[]`, or `{}` (see the filter in `JsonRenderer.filterJson`) — so with it enabled, `"functions": []` doesn't appear at all rather than appearing as an empty array. Consumers must treat a **missing** key as equivalent to its empty value (e.g. `functions is defined and functions is not empty`, as in the Pebble example in §8), not assume every key is always present. @@ -207,3 +208,131 @@ For example, to render a table of functions for a class: This writes HTML output to `/html/latest/all-libs` and JSON output to `/json/latest/all-libs` (`output-dir` defaults to `scripts/kotlin/build-output`). > **Provenance / staleness warning:** `scripts/kotlin/build.gradle.kts` was derived from the `kotlin-stdlib-docs/build.gradle.kts` in JetBrains' `kotlin` repo as of commit [`cfcb49fd0113`](https://github.com/JetBrains/kotlin/commit/cfcb49fd0113d2300a2b677c4fc2e16dddff7df5) ("[stdlib] Update Dokka to 2.2.0-Beta and migrate to DGPv2"). That upstream file is not under our control and can change — new source sets, Dokka API changes, or a different doc-build structure could all require re-diffing our modifications against a newer upstream version. If `build-kotlin-stdlib.sh` starts failing against a newer `kotlin` checkout, compare `scripts/kotlin/build.gradle.kts` against the current upstream `kotlin-stdlib-docs/build.gradle.kts` and re-apply the `useJsonPlugin`/`dokkaGenerateModuleJson` additions by hand. + +--- + +## 10. Javadoc Mode + +Setting `"javadoc-mode": true` replaces the plugin's whole output with JSON that mirrors what the +`javadoc` tool produces under its `api/` directory -- same file layout, same page sections, same +member anchors. It is intended for documenting **Java** sources (the JDK's own API docs being the +motivating case) where the downstream templates expect javadoc's structure rather than Dokka's. + +Only JSON is written. The one non-JSON file is `element-list`, which javadoc itself emits as a +plain-text manifest and external tooling reads to resolve links into the output. No HTML pages are +produced -- rendering stays the job of the downstream template engine. + +> The key is spelled `javadoc-mode`, not `javadocMode`. Every other option in this block is +> camelCase; this one is deliberately kebab-case. + +### Output layout + +``` +index.json overview: the run's modules and packages +element-list javadoc's plain-text manifest (not JSON) +allclasses-index.json every documented type +allpackages-index.json every documented package +deprecated-list.json deprecated elements, grouped by kind +constant-values.json static final fields, grouped by package then type +index-files/index-N.json the A-Z index, one file per letter +/module-summary.json module page +//package-summary.json package page +//.json type page +``` + +The leading `/` segment appears only when a single Dokka run genuinely contains more than +one module, matching javadoc's own split between modular and non-modular builds. A single-module +run also writes `module-summary.json` at the root: javadoc omits a module page entirely for a +non-modular build, but Dokka always has a module, and its documentation would otherwise be dropped. + +#### Multi-module builds, and what that means for JPMS + +Be aware of how Dokka structures a multi-module Gradle build, because it decides which of the two +layouts above you get: + +- **One Dokka run, one module** (all sources in a single project) -- packages are written flat at + the output root, and all the global index files cover the whole run. This is the layout the + tests exercise. +- **One Dokka run per module** (a Gradle subproject each) -- Dokka runs the renderer separately + for each module, into that module's own output directory, and then makes an aggregating pass. + Each per-module run therefore sees one module, writes its packages flat inside its own + directory, and writes global index files *scoped to that module*. The aggregating pass sees only + module references, and writes just the overview `index.json` linking to each module. + + The net tree matches javadoc's `//...` shape, but the index files are per-module + rather than run-wide, and cross-module links are not resolved -- each run only knows its own + types. Merging those per-module indexes into run-wide ones is a downstream step this plugin does + not perform. + +Note also that **Dokka has no JPMS model**: a Dokka "module" is a build-level grouping. Reproducing +the JDK's own `java.base/`, `java.desktop/` … directories therefore requires the documentation +build to be organised with one Dokka module per JPMS module; it cannot be inferred from +`module-info.java`. + +All links between pages are **relative to the page they appear on** (`../lang/Object.json`), as +javadoc's are, so the tree can be served from any prefix. This includes links inside rendered doc +comments. A link to something the run does not document resolves to `null` (or, inside a comment, +degrades to plain text) rather than becoming a dead `href`. + +### Page shape + +Type pages carry the sections a javadoc class page has: the type signature and its parts +(`modifiers`, `typeParameters`, `superclass`, `superinterfaces`), the hierarchy closures +(`inheritance`, `allImplementedInterfaces`, `allSuperinterfaces`, `directKnownSubclasses`, +`allKnownSubinterfaces`, `allKnownImplementingClasses`), the doc comment and its block tags +(`description`, `since`, `seeAlso`, `authors`, `versions`, `deprecated`, `tags`), the member +tables (`nestedTypes`, `enumConstants`, `fields`, `constructors`, `methods`, `annotationElements`) +and the inherited-member groups (`inheritedFields`, `inheritedMethods`). + +Structured data is primary: types, modifiers, parameters, throws clauses and override +relationships are all discrete fields. Each declaration also carries a flat `signature` string +(`public default Shape scaled(double factor) throws IllegalArgumentException`) as a +convenience -- ignore it if you would rather compose signatures in the template. + +Member `anchor` values follow javadoc's scheme: a bare name for a field, `name(erasedParamTypes)` +for an executable, and `(...)` for a constructor -- so `toArray(java.lang.Object[])`, not +`toArray(T[])`. `overrides` and `specifiedBy` are derived from those same erased signatures. + +Doc-comment text is HTML, because a javadoc comment's body already is (`

`, ``, ``, +`
`). That matches the convention the default output mode already uses. + +### Recommended Dokka settings + +javadoc documents public **and protected** members by default; Dokka documents only public. For +parity, set this on the consuming project: + +```kotlin +dokka { + dokkaSourceSets.configureEach { + documentedVisibilities.set(setOf(VisibilityModifier.Public, VisibilityModifier.Protected)) + } +} +``` + +`examples/example-java-library` is a working Java example wired up this way; `tests/test_javadoc_mode.sh` +drives it. + +### Known limitations + +These are places where Dokka's model does not carry something a real javadoc page shows. Each is a +missing *input*, not a gap in the mapping: + +| Limitation | Effect | +| --- | --- | +| Dokka has no JPMS model | `JdModulePage.requires`/`uses`/`provides` are always empty, and a package's "exported to" is unavailable. A Dokka "module" is a build-level grouping, not a JPMS module. | +| Dokka does not record annotation-element defaults | Annotation elements are reported as one `annotationElements` list rather than being split into javadoc's Required/Optional tables. `defaultValue` is populated only when Dokka does supply it. | +| Dokka has no `record` class kind | Java records are documented as classes; `recordComponents` stays empty. | +| Dokka merges a private field and its accessors into one property | Unfolded back into methods so `getWidth()` is a method and the private field is not documented, as javadoc has it. Note this means a *public* field that happens to have a same-named accessor pair is reported through its accessors. | +| Inherited members depend on Dokka's inheritance propagation | If Dokka does not attach `InheritedMember`, those members appear as declared rather than in an inherited group. | + +### What Javadoc mode does not change + +`omitFields`, `omitNulls`, `prettyPrint`, `logLevel`, `logFile` and `sourceSetWhitelist` all behave +as documented in [§3](#3-configuration-options). `replaceHtmlExtension` and `classDiscriminator` +have no effect: javadoc-mode pages are written with `.json` links throughout and none of its DTOs +are polymorphic. Javadoc mode also skips the `LinkPostProcessor` pass, since it resolves every link +itself rather than rewriting Dokka's. + +Pages are serialized with `encodeDefaults = true`, so every documented key is present on every page +even when empty -- a template can test a field without also testing whether it exists. Enabling +`omitNulls` strips the empty ones back out if you prefer that. diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/build.gradle.kts b/Dokka-plugin-kdoc2json/examples/example-java-library/build.gradle.kts new file mode 100644 index 00000000..243a968d --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/build.gradle.kts @@ -0,0 +1,58 @@ +import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.gradle.engine.plugins.DokkaPluginParametersBaseSpec +import javax.inject.Inject + +// A Java-only sibling of examples/example-data-processor, used by tests/test_javadoc_mode.sh. +// Javadoc mode mirrors the output of the `javadoc` tool, so it needs Java sources exercising the +// constructs a javadoc page actually has sections for: generic interfaces and their implementors, +// an abstract base class, an enum, an annotation type, a checked exception, nested types, +// compile-time constants, deprecation, and the full set of javadoc block tags. +plugins { + java + // Must match the dokka-core/dokka-base version kdoc-to-json was compiled against. + id("org.jetbrains.dokka") version "2.2.0-Beta" +} + +repositories { + // Lets Gradle find the locally published kdoc-to-json plugin. + mavenLocal() + mavenCentral() +} + +dependencies { + dokkaPlugin("org.appdevforall.dokka:kdoc-to-json:1.0.0-SNAPSHOT") +} + +@OptIn(InternalDokkaApi::class) +abstract class JsonOutputPluginParameters @Inject constructor( + name: String +) : DokkaPluginParametersBaseSpec(name, "org.appdevforall.dokka.kdoc2json.JsonOutputPlugin") { + // As in example-data-processor: point KDOC2JSON_TEST_CONFIG at a JSON file to drive this + // project through an arbitrary plugin config without editing this build script. + override fun jsonEncode(): String { + val overridePath = System.getenv("KDOC2JSON_TEST_CONFIG") + if (overridePath != null) { + return File(overridePath).readText() + } + return """{ + "logLevel": "debug", + "javadoc-mode": true, + "prettyPrint": true + }""" + } +} + +dokka { + dokkaSourceSets.configureEach { + // javadoc documents public *and* protected members by default; Dokka documents only + // public. Without this, Javadoc mode would silently omit every protected member that a + // real javadoc build would have shown. + documentedVisibilities.set(setOf(VisibilityModifier.Public, VisibilityModifier.Protected)) + } + + pluginsConfiguration { + registerBinding(JsonOutputPluginParameters::class, JsonOutputPluginParameters::class) + register("org.appdevforall.dokka.kdoc2json.JsonOutputPlugin") { } + } +} diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/gradle/wrapper/gradle-wrapper.jar b/Dokka-plugin-kdoc2json/examples/example-java-library/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + 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/Dokka-plugin-kdoc2json/examples/example-java-library/gradlew.bat b/Dokka-plugin-kdoc2json/examples/example-java-library/gradlew.bat new file mode 100644 index 00000000..aa5f10b0 --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/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/Dokka-plugin-kdoc2json/examples/example-java-library/settings.gradle.kts b/Dokka-plugin-kdoc2json/examples/example-java-library/settings.gradle.kts new file mode 100644 index 00000000..27680a63 --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "javalib" diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/AbstractShape.java b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/AbstractShape.java new file mode 100644 index 00000000..c4b70277 --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/AbstractShape.java @@ -0,0 +1,46 @@ +package com.example.shapes; + +/** + * Skeletal implementation of {@link Shape} that supplies the parts every shape shares. + * + * @param the unit of measure areas are reported in + * @since 1.0 + */ +public abstract class AbstractShape implements Shape { + + /** Identifies this shape for diagnostics; never {@code null}. */ + protected final String name; + + /** + * Creates a shape with the given diagnostic name. + * + * @param name the shape's name + */ + protected AbstractShape(String name) { + this.name = name; + } + + /** + * {@inheritDoc} + * + *

This implementation always reports four sides.

+ */ + @Override + public int sides() { + return 4; + } + + /** + * Returns this shape's diagnostic name. + * + * @return the name passed to the constructor + */ + public String getName() { + return name; + } + + @Override + public String toString() { + return name + "[" + area() + "]"; + } +} diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Corner.java b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Corner.java new file mode 100644 index 00000000..67171053 --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Corner.java @@ -0,0 +1,35 @@ +package com.example.shapes; + +/** + * The four corners of an axis-aligned bounding box. + * + * @since 1.0 + */ +public enum Corner { + + /** The top-left corner. */ + TOP_LEFT, + + /** The top-right corner. */ + TOP_RIGHT, + + /** The bottom-left corner. */ + BOTTOM_LEFT, + + /** The bottom-right corner. */ + BOTTOM_RIGHT; + + /** + * Returns the corner diagonally opposite this one. + * + * @return the opposite corner + */ + public Corner opposite() { + switch (this) { + case TOP_LEFT: return BOTTOM_RIGHT; + case TOP_RIGHT: return BOTTOM_LEFT; + case BOTTOM_LEFT: return TOP_RIGHT; + default: return TOP_LEFT; + } + } +} diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Measured.java b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Measured.java new file mode 100644 index 00000000..31e6d4ca --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Measured.java @@ -0,0 +1,30 @@ +package com.example.shapes; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a type whose measurements have been verified against a reference implementation. + * + * @since 1.2 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Measured { + + /** + * The tolerance the measurements were verified to. + * + * @return the absolute tolerance + */ + double tolerance(); + + /** + * Who performed the verification. + * + * @return the verifier's name, or the empty string if unrecorded + */ + String verifiedBy() default ""; +} diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Rectangle.java b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Rectangle.java new file mode 100644 index 00000000..fe1c0916 --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Rectangle.java @@ -0,0 +1,122 @@ +package com.example.shapes; + +/** + * An axis-aligned rectangle measured in {@code double} units. + * + *

Example:

+ *
{@code
+ * Rectangle r = new Rectangle(3.0, 4.0);
+ * assert r.area() == 12.0;
+ * }
+ * + * @since 1.0 + * @see Shape + */ +public class Rectangle extends AbstractShape { + + /** A rectangle of zero width and height. */ + public static final String EMPTY_LABEL = "empty"; + + /** The number of sides a rectangle always has. */ + public static final int SIDE_COUNT = 4; + + private final double width; + private final double height; + + /** + * Creates a rectangle of the given dimensions. + * + * @param width the width, must not be negative + * @param height the height, must not be negative + * @throws IllegalArgumentException if either dimension is negative + */ + public Rectangle(double width, double height) { + super("rectangle"); + if (width < 0 || height < 0) { + throw new IllegalArgumentException("dimensions must not be negative"); + } + this.width = width; + this.height = height; + } + + /** Creates a unit square. */ + public Rectangle() { + this(1.0, 1.0); + } + + @Override + public Double area() { + return width * height; + } + + /** + * Returns the rectangle's width. + * + * @return the width in unspecified units + */ + public double getWidth() { + return width; + } + + /** + * Returns the rectangle's height. + * + * @return the height in unspecified units + */ + public double getHeight() { + return height; + } + + /** + * Returns the perimeter. + * + * @return twice the sum of width and height + * @deprecated Use {@link #getWidth()} and {@link #getHeight()} and compute it directly. + * Scheduled for removal in 3.0. + */ + @Deprecated(since = "2.0", forRemoval = true) + public double perimeter() { + return 2 * (width + height); + } + + /** + * A builder for {@link Rectangle} instances. + * + *

Nested to exercise javadoc's nested-type sections.

+ */ + public static final class Builder { + private double width; + private double height; + + /** + * Sets the width. + * + * @param width the width + * @return this builder + */ + public Builder width(double width) { + this.width = width; + return this; + } + + /** + * Sets the height. + * + * @param height the height + * @return this builder + */ + public Builder height(double height) { + this.height = height; + return this; + } + + /** + * Builds the rectangle. + * + * @return a new rectangle + */ + public Rectangle build() { + return new Rectangle(width, height); + } + } +} diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Shape.java b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Shape.java new file mode 100644 index 00000000..27506c20 --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Shape.java @@ -0,0 +1,47 @@ +package com.example.shapes; + +/** + * A closed geometric figure with a computable area. + * + *

Implementations are expected to be immutable; the {@link #area()} of a shape must not change + * over its lifetime. This mirrors the contract style used throughout the JDK's own collection + * interfaces.

+ * + * @param the unit of measure areas are reported in + * @author Docs Pipeline + * @since 1.0 + * @see Rectangle + */ +public interface Shape { + + /** The maximum number of sides any shape in this library may declare. */ + int MAX_SIDES = 64; + + /** + * Returns the area enclosed by this shape. + * + * @return the enclosed area, never negative + */ + U area(); + + /** + * Returns the number of sides this shape has. + * + * @return the side count, between 0 and {@value #MAX_SIDES} + */ + int sides(); + + /** + * Scales this shape by the given factor. + * + * @param factor the scaling factor, must be positive + * @return a new scaled shape + * @throws IllegalArgumentException if {@code factor} is not positive + */ + default Shape scaled(double factor) { + if (factor <= 0) { + throw new IllegalArgumentException("factor must be positive"); + } + return this; + } +} diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/ShapeException.java b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/ShapeException.java new file mode 100644 index 00000000..562a1849 --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/ShapeException.java @@ -0,0 +1,20 @@ +package com.example.shapes; + +/** + * Thrown when a shape cannot be constructed from the supplied measurements. + * + * @since 1.0 + */ +public class ShapeException extends IllegalArgumentException { + + private static final long serialVersionUID = 1L; + + /** + * Creates an exception with the given detail message. + * + * @param message the detail message + */ + public ShapeException(String message) { + super(message); + } +} diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Square.java b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Square.java new file mode 100644 index 00000000..0681f28c --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/Square.java @@ -0,0 +1,18 @@ +package com.example.shapes; + +/** + * A rectangle whose sides are all equal. + * + * @since 1.1 + */ +public class Square extends Rectangle { + + /** + * Creates a square with the given side length. + * + * @param side the length of each side + */ + public Square(double side) { + super(side, side); + } +} diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/package-info.java b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/package-info.java new file mode 100644 index 00000000..5e5783f9 --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/package-info.java @@ -0,0 +1,9 @@ +/** + * Geometric shapes and the operations over them. + * + *

The central abstraction is {@link com.example.shapes.Shape}, implemented by + * {@link com.example.shapes.Rectangle} and its subclasses.

+ * + * @since 1.0 + */ +package com.example.shapes; diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/spi/ShapeFactory.java b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/spi/ShapeFactory.java new file mode 100644 index 00000000..a9f3e237 --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/spi/ShapeFactory.java @@ -0,0 +1,23 @@ +package com.example.shapes.spi; + +import com.example.shapes.Shape; + +/** + * Service provider interface for creating shapes from a textual specification. + * + *

Exists in a second package so Javadoc mode's package tables and cross-package links have + * something to resolve.

+ * + * @since 1.2 + */ +public interface ShapeFactory { + + /** + * Parses a shape from its textual form. + * + * @param specification the shape specification + * @return the parsed shape + * @throws java.text.ParseException if the specification is malformed + */ + Shape parse(String specification) throws java.text.ParseException; +} diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/spi/package-info.java b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/spi/package-info.java new file mode 100644 index 00000000..1a39dbd5 --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/com/example/shapes/spi/package-info.java @@ -0,0 +1,6 @@ +/** + * Extension points for supplying shapes from outside this library. + * + * @since 1.2 + */ +package com.example.shapes.spi; diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/build.gradle.kts b/Dokka-plugin-kdoc2json/kdoc-to-json/build.gradle.kts index 3343a62e..92e67de4 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/build.gradle.kts +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/build.gradle.kts @@ -14,6 +14,11 @@ repositories { dependencies { compileOnly("org.jetbrains.dokka:dokka-core:2.2.0-Beta") compileOnly("org.jetbrains.dokka:dokka-base:2.2.0-Beta") + // Dokka deserializes this plugin's config block with Jackson, not kotlinx.serialization + // (see org.jetbrains.dokka.utilities.parseJson), so a config key whose JSON spelling + // differs from its Kotlin property name needs @JsonProperty to be seen -- @SerialName + // alone is ignored on that path. compileOnly: Dokka already brings Jackson at runtime. + compileOnly("com.fasterxml.jackson.core:jackson-annotations:2.15.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") } diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonFilters.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonFilters.kt new file mode 100644 index 00000000..ceeb9abf --- /dev/null +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonFilters.kt @@ -0,0 +1,46 @@ +package org.appdevforall.dokka.kdoc2json + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive + +/** + * The `omitFields` / `omitNulls` post-processing both renderers apply to a page before writing it. + * + * Lives outside [JsonRenderer] so Javadoc mode honours exactly the same two config options with + * exactly the same semantics, rather than reimplementing them and drifting. + */ +internal object JsonFilters { + + /** Strips [omitFields] keys everywhere, and (when [omitNulls]) null/empty values with them. */ + fun filterJson(element: JsonElement, omitFields: List, omitNulls: Boolean): JsonElement { + if (omitFields.isEmpty() && !omitNulls) return element + + return when (element) { + is JsonObject -> { + val filteredMap = element.entries + .filterNot { omitFields.contains(it.key) } + .mapNotNull { (key, value) -> + val filteredValue = filterJson(value, omitFields, omitNulls) + if (omitNulls && isNullOrEmpty(filteredValue)) null else key to filteredValue + } + .toMap() + JsonObject(filteredMap) + } + is JsonArray -> { + val mapped = element.map { filterJson(it, omitFields, omitNulls) } + if (omitNulls) JsonArray(mapped.filterNot { isNullOrEmpty(it) }) else JsonArray(mapped) + } + else -> element + } + } + + /** `omitNulls` drops empty values too, not just nulls -- see the note in the plugin README. */ + fun isNullOrEmpty(element: JsonElement): Boolean = + element is JsonNull || + (element is JsonPrimitive && element.isString && element.content.isEmpty()) || + (element is JsonArray && element.isEmpty()) || + (element is JsonObject && element.isEmpty()) +} diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonPluginConfig.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonPluginConfig.kt index 61e3273a..67b93332 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonPluginConfig.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonPluginConfig.kt @@ -1,5 +1,7 @@ package org.appdevforall.dokka.kdoc2json +import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import org.jetbrains.dokka.plugability.ConfigurableBlock @@ -12,5 +14,18 @@ data class JsonPluginConfig( val omitNulls: Boolean = false, val classDiscriminator: String = "kind", val prettyPrint: Boolean = false, - val sourceSetWhitelist: List = emptyList() -) : ConfigurableBlock \ No newline at end of file + val sourceSetWhitelist: List = emptyList(), + // Opt-in "Javadoc mode": instead of Dokka-shaped JSON at Dokka's own page paths, emit + // javadoc-shaped JSON laid out like the `api/` tree that the `javadoc` tool produces + // (module-summary / package-summary / pages plus the global index files). + // + // Spelled kebab-case in the config on purpose -- that is the documented spelling of the + // switch -- even though every other option here is camelCase. That costs two annotations + // rather than one, because this config block is read by two different deserializers: + // Dokka's own pluginsConfiguration parsing uses Jackson (@JsonProperty), while + // JsonRenderer's manual fallback uses kotlinx.serialization (@SerialName). Dropping either + // would leave "javadoc-mode" silently ignored on one of the two paths. + @JsonProperty("javadoc-mode") + @SerialName("javadoc-mode") + val javadocMode: Boolean = false +) : ConfigurableBlock diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonRenderer.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonRenderer.kt index 67e2b63d..e7ea1162 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonRenderer.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonRenderer.kt @@ -3,6 +3,7 @@ package org.appdevforall.dokka.kdoc2json import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.* import org.appdevforall.dokka.kdoc2json.dtos.* +import org.appdevforall.dokka.kdoc2json.javadoc.JavadocRenderer import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.model.* import org.jetbrains.dokka.pages.PageNode @@ -42,13 +43,32 @@ class JsonRenderer(private val context: DokkaContext) : Renderer { val finalConfig = config ?: JsonPluginConfig() val logger = PluginLogger(context.logger, finalConfig.logLevel, finalConfig.logFile) + logger.info("Initializing JSON Renderer with config: $finalConfig") + + if (finalConfig.javadocMode) { + // Javadoc mode replaces the whole output: a different file layout, a different page + // shape, and its own link resolution -- so it takes over here rather than trying to + // post-process the Dokka-shaped output into javadoc's structure. It also has no use + // for the location provider, the package-list, or the LinkPostProcessor pass below, + // all of which exist to serve Dokka's own page paths. + logger.info("javadoc-mode enabled: emitting javadoc-shaped JSON instead of Dokka-shaped JSON.") + JavadocRenderer( + config = finalConfig, + logger = logger, + outputDir = context.configuration.outputDir, + moduleReferences = context.configuration.modules.map { + it.name to it.relativePathToOutputDirectory.invariantSeparatorsPath + } + ).render(root) + logger.info("JSON rendering completed (javadoc mode).") + return + } + val json = Json { prettyPrint = finalConfig.prettyPrint classDiscriminator = finalConfig.classDiscriminator } - logger.info("Initializing JSON Renderer with config: $finalConfig") - val locationProvider = context.plugin() .querySingle { locationProviderFactory } .getLocationProvider(root) @@ -220,43 +240,9 @@ class JsonRenderer(private val context: DokkaContext) : Renderer { logger.info("JSON rendering completed.") } - // --- RECURSIVE JSON AST FILTER --- - private fun filterJson(element: JsonElement, omitFields: List, omitNulls: Boolean): JsonElement { - if (omitFields.isEmpty() && !omitNulls) return element - - return when (element) { - is JsonObject -> { - val filteredMap = element.entries - .filterNot { omitFields.contains(it.key) } - .mapNotNull { (key, value) -> - val filteredValue = filterJson(value, omitFields, omitNulls) - if (omitNulls && isNullOrEmpty(filteredValue)) { - null - } else { - key to filteredValue - } - } - .toMap() - JsonObject(filteredMap) - } - is JsonArray -> { - val mapped = element.map { filterJson(it, omitFields, omitNulls) } - if (omitNulls) { - JsonArray(mapped.filterNot { isNullOrEmpty(it) }) - } else { - JsonArray(mapped) - } - } - else -> element - } - } - - private fun isNullOrEmpty(element: JsonElement): Boolean { - return element is JsonNull || - (element is JsonPrimitive && element.isString && element.content.isEmpty()) || - (element is JsonArray && element.isEmpty()) || - (element is JsonObject && element.isEmpty()) - } + // Delegates to JsonFilters so Javadoc mode applies identical omitFields/omitNulls semantics. + private fun filterJson(element: JsonElement, omitFields: List, omitNulls: Boolean): JsonElement = + JsonFilters.filterJson(element, omitFields, omitNulls) private fun passesSourceSetWhitelist( sourceSets: Set, diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDocs.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDocs.kt new file mode 100644 index 00000000..ef40f032 --- /dev/null +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDocs.kt @@ -0,0 +1,241 @@ +package org.appdevforall.dokka.kdoc2json.javadoc + +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.doc.* +// kotlin.Deprecated is a default import and would otherwise win over the star import here. +import org.jetbrains.dokka.model.doc.Deprecated as DeprecatedTag + +/** + * The javadoc block tags of one declaration, pulled out of Dokka's [DocumentationNode] and sorted + * into the buckets a javadoc page actually renders. + * + * @param params `@param` text keyed by parameter name -- type parameters included, keyed by their + * bare name (Dokka strips the angle brackets `@param ` is written with). + * @param other every block tag with no dedicated bucket (`@apiNote`, `@implSpec`, `@serial`, ...), + * in source order, so nothing in the source comment is silently dropped. + */ +class JavadocDocBundle( + val description: String? = null, + val params: Map = emptyMap(), + val returns: String? = null, + val throws: List> = emptyList(), + val seeAlso: List> = emptyList(), + val since: List = emptyList(), + val authors: List = emptyList(), + val versions: List = emptyList(), + val deprecated: String? = null, + val isDeprecatedTagPresent: Boolean = false, + val other: List = emptyList() +) + +/** + * Turns Dokka doc trees into the HTML strings javadoc pages carry, and sorts block tags into + * [JavadocDocBundle]. + * + * Output is HTML rather than Markdown because that is what a javadoc comment already contains and + * what the existing renderer emits, so a downstream template can drop either into a page unchanged. + * + * @param resolveLink maps a link target to a URL relative to the page being written; returns null + * for a target this run doesn't document, in which case the link degrades to plain text rather + * than becoming a dead `href`. + */ +class JavadocDocs(private val resolveLink: (DRI) -> String?) { + + /** + * Picks the doc comment to render. Javadoc has no notion of source sets, so where Dokka has + * several this takes the first that actually carries tags, which for a Java run is the only one. + */ + fun bundleFor(doc: Documentable): JavadocDocBundle { + val node: DocumentationNode = doc.documentation.entries + .firstOrNull { it.value.children.isNotEmpty() } + ?.value + ?: return JavadocDocBundle() + + var description: String? = null + val params = LinkedHashMap() + var returns: String? = null + val throws = mutableListOf>() + val seeAlso = mutableListOf>() + val since = mutableListOf() + val authors = mutableListOf() + val versions = mutableListOf() + var deprecated: String? = null + var deprecatedPresent = false + val other = mutableListOf() + + node.children.forEach { tag -> + val text = render(tag.root).trim() + when (tag) { + is Description -> description = listOfNotNull(description?.takeIf { it.isNotBlank() }, text) + .filter { it.isNotBlank() } + .joinToString("\n") + .ifBlank { null } + is Param -> params[tag.name] = text + is Return -> returns = text + is Throws -> throws += Triple(tag.name, tag.exceptionAddress, text) + is See -> seeAlso += Triple(tag.name, tag.address, text) + is Since -> since += unwrapParagraph(text) + is Author -> authors += unwrapParagraph(text) + is Version -> versions += unwrapParagraph(text) + is DeprecatedTag -> { + deprecatedPresent = true + deprecated = text.ifBlank { null } + } + is CustomTagWrapper -> other += JdTag(tag.name, text) + else -> other += JdTag(tag::class.java.simpleName, text) + } + } + + return JavadocDocBundle( + description = description?.ifBlank { null }, + params = params, + returns = returns?.ifBlank { null }, + throws = throws, + seeAlso = seeAlso, + since = since.filter { it.isNotBlank() }, + authors = authors.filter { it.isNotBlank() }, + versions = versions.filter { it.isNotBlank() }, + deprecated = deprecated, + isDeprecatedTagPresent = deprecatedPresent, + other = other.filter { it.text.isNotBlank() } + ) + } + + /** + * Renders one doc tree back to HTML. + * + * JDK javadoc comments are full HTML -- tables, definition lists, `
` -- so + * structural tags and their attributes are preserved rather than flattened to their text. + * Anything Dokka parsed into a tag this doesn't know is emitted as its children, which loses + * the wrapper but never the content. + */ + fun render(tag: DocTag): String { + val children = tag.children.joinToString("") { render(it) } + return when (tag) { + is Text -> escapeHtmlText(tag.body) + is Br -> "
" + is HorizontalRule -> "
" + is Img -> "" + is CodeBlock -> "
$children
" + is CodeInline -> "$children" + is A -> "$children" + is DocumentationLink -> { + val href = resolveLink(tag.dri) + // An unresolvable {@link} degrades to its own text rather than an href to nowhere: + // javadoc-mode output is meant to be servable as-is, and a dead link is worse than + // a plain-text mention of the symbol. + if (href == null) children else "$children" + } + is CustomDocTag -> children + else -> { + val htmlName = HTML_TAG_NAMES[tag::class.java.simpleName] + if (htmlName == null) children else "<$htmlName${attributes(tag.params)}>$children" + } + } + } + + companion object { + /** + * Dokka doc-tag class name -> HTML element name, for every tag that is just a wrapper + * around its children. Tags needing special handling (links, images, code, line breaks) + * are matched by type in [render] instead and are deliberately absent here. + */ + private val HTML_TAG_NAMES: Map = mapOf( + "P" to "p", "B" to "b", "I" to "i", "Em" to "em", "Strong" to "strong", + "BlockQuote" to "blockquote", "Pre" to "pre", "Ul" to "ul", "Ol" to "ol", "Li" to "li", + "H1" to "h1", "H2" to "h2", "H3" to "h3", "H4" to "h4", "H5" to "h5", "H6" to "h6", + "Dl" to "dl", "Dt" to "dt", "Dd" to "dd", "Div" to "div", "Span" to "span", + "Table" to "table", "THead" to "thead", "TBody" to "tbody", "TFoot" to "tfoot", + "Tr" to "tr", "Td" to "td", "Th" to "th", "Caption" to "caption", + "Sub" to "sub", "Sup" to "sup", "Small" to "small", "Big" to "big", "Var" to "var", + "Tt" to "tt", "U" to "u", "Strikethrough" to "del", "Cite" to "cite", "Code" to "code", + "Dfn" to "dfn", "Mark" to "mark", "Font" to "font", "Menu" to "menu", "Dir" to "dir", + "Section" to "section", "Main" to "main", "Nav" to "nav", "Header" to "header", + "Footer" to "footer", "Listing" to "listing" + ) + + /** + * Strips a single enclosing `

` from a one-paragraph fragment. + * + * Dokka wraps every tag body in a paragraph, but `@since`, `@author` and `@version` are + * plain text in javadoc ("1.0", not "

1.0

"). Anything with internal structure is + * left exactly as it is. + */ + fun unwrapParagraph(html: String): String { + val trimmed = html.trim() + if (!trimmed.startsWith("

") || !trimmed.endsWith("

")) return trimmed + val inner = trimmed.removePrefix("

").removeSuffix("

") + return if (inner.contains("

", ignoreCase = true)) trimmed else inner.trim() + } + + /** Renders a tag's attributes back into HTML, in the order Dokka recorded them. */ + private fun attributes(params: Map): String = + params.entries.joinToString("") { (key, value) -> + " $key=\"${escapeHtmlAttribute(value)}\"" + } + + private fun escapeHtmlText(value: String): String = + value.replace("&", "&").replace("<", "<").replace(">", ">") + + // "&" first, or escaping the rest afterwards would double-escape a """ that was + // already literally present in the source text. "<" and ">" are escaped as well as the + // quotes: an unescaped ">" inside an attribute value would otherwise look like the end of + // the tag to anything scanning the markup, firstSentence's depth tracking included. + private fun escapeHtmlAttribute(value: String): String = + value.replace("&", "&") + .replace("\"", """) + .replace("<", "<") + .replace(">", ">") + + /** + * The leading sentence of an HTML description, as javadoc shows it in a summary table. + * + * Cuts at the first `.` that sits outside a tag and is followed by whitespace (or ends the + * text), then closes any element the cut left open so the fragment is still well-formed + * HTML. Returns null when there is no description to summarise. + */ + fun firstSentence(html: String?): String? { + if (html.isNullOrBlank()) return null + var depth = 0 + var cut = -1 + for (i in html.indices) { + when (html[i]) { + '<' -> depth++ + '>' -> if (depth > 0) depth-- + '.' -> if (depth == 0) { + val next = html.getOrNull(i + 1) + if (next == null || next.isWhitespace()) { + cut = i + 1 + } + } + } + if (cut >= 0) break + } + val fragment = if (cut >= 0) html.substring(0, cut) else html + return closeOpenTags(fragment.trim()).ifBlank { null } + } + + private val TAG_REGEX = Regex("<\\s*(/?)\\s*([a-zA-Z][a-zA-Z0-9]*)[^>]*?(/?)\\s*>") + private val VOID_ELEMENTS = setOf("br", "hr", "img", "input", "meta", "link", "wbr") + + /** Appends closing tags for any element left open by a truncated HTML fragment. */ + private fun closeOpenTags(fragment: String): String { + val open = ArrayDeque() + TAG_REGEX.findAll(fragment).forEach { match -> + val closing = match.groupValues[1] == "/" + val name = match.groupValues[2].lowercase() + val selfClosing = match.groupValues[3] == "/" + if (name in VOID_ELEMENTS || selfClosing) return@forEach + if (closing) { + // Tolerate stray/mismatched closers instead of corrupting the stack. + if (open.isNotEmpty() && open.last() == name) open.removeLast() + else open.remove(name) + } else { + open.addLast(name) + } + } + return fragment + open.reversed().joinToString("") { "" } + } + } +} diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt new file mode 100644 index 00000000..e2397c85 --- /dev/null +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt @@ -0,0 +1,387 @@ +package org.appdevforall.dokka.kdoc2json.javadoc + +import kotlinx.serialization.Serializable + +/** + * DTOs mirroring the pages the `javadoc` tool emits under its `api/` output directory. + * + * These are deliberately *not* part of the `DocumentableDto` hierarchy in + * `dtos/SemanticModelDtos.kt`: that hierarchy mirrors Dokka's own AST, whereas everything here + * mirrors what a javadoc page actually presents (inheritance closures, inherited-member groups, + * summary tables, the global index pages). Keeping them separate means Javadoc mode can add + * javadoc-specific concepts without perturbing the default output's schema. + * + * Field naming follows the javadoc page section it comes from, so a consumer holding a javadoc + * page open next to the JSON can match them up section by section. + */ + +// --- Shared building blocks --- + +/** + * A reference to a type. [display] is the javadoc-style rendering including type arguments and + * array dimensions (e.g. `List`, `int[]`, `? extends Number`); [qualifiedName] and [url] are + * populated only when the referenced type is part of this documentation run. + */ +@Serializable +data class JdTypeRef( + val display: String, + val qualifiedName: String? = null, + val url: String? = null, + val kind: String? = null +) + +/** A resolved `@see` / `@link` reference. [url] is null when the target isn't documented here. */ +@Serializable +data class JdSeeRef( + val label: String, + val url: String? = null, + val qualifiedName: String? = null +) + +/** A javadoc block tag this mapper has no dedicated field for (`@apiNote`, `@implSpec`, ...). */ +@Serializable +data class JdTag( + val name: String, + val text: String +) + +@Serializable +data class JdDeprecation( + val comment: String? = null, + val forRemoval: Boolean = false, + val since: String? = null +) + +/** A method/constructor parameter, paired with its `@param` text when the source documents one. */ +@Serializable +data class JdParameter( + val name: String, + val type: JdTypeRef, + val description: String? = null, + val annotations: List = emptyList() +) + +/** A type parameter declaration, paired with its `@param ` text. */ +@Serializable +data class JdTypeParameter( + val name: String, + val bounds: List = emptyList(), + val description: String? = null +) + +/** One entry of a `@throws`/`@exception` list. */ +@Serializable +data class JdThrows( + val type: JdTypeRef, + val description: String? = null +) + +/** + * A pointer at another member -- used for javadoc's "Specified by:" / "Overrides:" notes and for + * the members listed in an inherited-member group. + */ +@Serializable +data class JdMemberRef( + val name: String, + val signature: String, + val url: String? = null, + val declaringType: JdTypeRef? = null +) + +/** + * One "Methods declared in class X" / "Fields declared in interface Y" group, as javadoc renders + * them at the bottom of a summary table. + */ +@Serializable +data class JdInheritedMembers( + val declaringType: JdTypeRef, + val members: List = emptyList() +) + +// --- Members --- + +/** A field, an enum constant, or a record component's backing field. */ +@Serializable +data class JdField( + val name: String, + val anchor: String, + val modifiers: List = emptyList(), + val type: JdTypeRef, + val signature: String, + val url: String? = null, + val description: String? = null, + val firstSentence: String? = null, + val constantValue: String? = null, + val since: List = emptyList(), + val seeAlso: List = emptyList(), + val deprecated: JdDeprecation? = null, + val annotations: List = emptyList(), + val tags: List = emptyList() +) + +/** + * A constructor, a method, or an annotation element. [returnType] is null for constructors; + * [defaultValue] is populated only for annotation elements that declare a `default`. + */ +@Serializable +data class JdExecutable( + val name: String, + val anchor: String, + val kind: String, + val modifiers: List = emptyList(), + val typeParameters: List = emptyList(), + val returnType: JdTypeRef? = null, + val parameters: List = emptyList(), + val exceptions: List = emptyList(), + val signature: String, + val url: String? = null, + val description: String? = null, + val firstSentence: String? = null, + val returns: String? = null, + val specifiedBy: List = emptyList(), + val overrides: JdMemberRef? = null, + val since: List = emptyList(), + val seeAlso: List = emptyList(), + val deprecated: JdDeprecation? = null, + val annotations: List = emptyList(), + val defaultValue: String? = null, + val tags: List = emptyList() +) + +/** A nested type as listed in an enclosing type's "Nested Class Summary". */ +@Serializable +data class JdNestedTypeRef( + val name: String, + val qualifiedName: String, + val kind: String, + val modifiers: List = emptyList(), + val url: String? = null, + val firstSentence: String? = null, + val deprecated: JdDeprecation? = null +) + +// --- Pages --- + +/** One `.json` page -- the javadoc class/interface/enum/record/annotation page. */ +@Serializable +data class JdClassPage( + val page: String = "class", + val kind: String, + val name: String, + val simpleName: String, + val qualifiedName: String, + val packageName: String, + val moduleName: String? = null, + /** + * This page's own path, relative to the output root. Note the asymmetry with every *link* + * URL in these DTOs, which is relative to the page it appears on, the way javadoc links are. + */ + val url: String, + val modifiers: List = emptyList(), + val signature: String, + val typeParameters: List = emptyList(), + val superclass: JdTypeRef? = null, + val superinterfaces: List = emptyList(), + /** Superclass chain from `java.lang.Object` down to (and including) this type. */ + val inheritance: List = emptyList(), + val allImplementedInterfaces: List = emptyList(), + val allSuperinterfaces: List = emptyList(), + val directKnownSubclasses: List = emptyList(), + val allKnownSubinterfaces: List = emptyList(), + val allKnownImplementingClasses: List = emptyList(), + val enclosingType: JdTypeRef? = null, + val isFunctionalInterface: Boolean = false, + val description: String? = null, + val firstSentence: String? = null, + val since: List = emptyList(), + val seeAlso: List = emptyList(), + val authors: List = emptyList(), + val versions: List = emptyList(), + val deprecated: JdDeprecation? = null, + val annotations: List = emptyList(), + val tags: List = emptyList(), + val nestedTypes: List = emptyList(), + val recordComponents: List = emptyList(), + val enumConstants: List = emptyList(), + val fields: List = emptyList(), + val constructors: List = emptyList(), + val methods: List = emptyList(), + /** + * The elements of an annotation type. + * + * javadoc splits these into "Required" and "Optional" tables by whether the element declares + * a `default`. Dokka's model does not carry annotation-element default values, so making that + * split here would mean labelling every element "required" whether it is or not; instead they + * are reported as one list and each element's [JdExecutable.defaultValue] is populated when + * (and only when) Dokka does supply it. + */ + val annotationElements: List = emptyList(), + val inheritedNestedTypes: List = emptyList(), + val inheritedFields: List = emptyList(), + val inheritedMethods: List = emptyList() +) + +/** One entry in a package page's type table, or in `allclasses-index.json`. */ +@Serializable +data class JdTypeSummary( + val name: String, + val qualifiedName: String, + val kind: String, + val packageName: String, + val moduleName: String? = null, + val url: String? = null, + val firstSentence: String? = null, + val deprecated: JdDeprecation? = null +) + +/** One `package-summary.json` page. */ +@Serializable +data class JdPackagePage( + val page: String = "package", + val name: String, + val moduleName: String? = null, + /** This page's own path, relative to the output root -- see [JdClassPage.url]. */ + val url: String, + val description: String? = null, + val firstSentence: String? = null, + val since: List = emptyList(), + val seeAlso: List = emptyList(), + val deprecated: JdDeprecation? = null, + val tags: List = emptyList(), + val interfaces: List = emptyList(), + val classes: List = emptyList(), + val enums: List = emptyList(), + val records: List = emptyList(), + val exceptions: List = emptyList(), + val annotationTypes: List = emptyList(), + /** Every type in the package, in one list, regardless of which table above it also appears in. */ + val allTypes: List = emptyList() +) + +/** One entry in a module page's package table, or in `allpackages-index.json`. */ +@Serializable +data class JdPackageSummary( + val name: String, + val moduleName: String? = null, + val url: String? = null, + val firstSentence: String? = null, + val deprecated: JdDeprecation? = null +) + +/** + * One `module-summary.json` page. + * + * `requires` / `uses` / `provides` / `exportedTo` are always empty: they come from a JPMS + * `module-info.java` descriptor, which Dokka's model does not carry (a Dokka "module" is a + * build-level grouping, not a JPMS module). The fields exist so a consumer's shape matches + * javadoc's module page and so they can be filled in later without a schema break. + */ +@Serializable +data class JdModulePage( + val page: String = "module", + val name: String, + /** This page's own path, relative to the output root -- see [JdClassPage.url]. */ + val url: String, + val description: String? = null, + val firstSentence: String? = null, + val since: List = emptyList(), + val seeAlso: List = emptyList(), + val deprecated: JdDeprecation? = null, + val tags: List = emptyList(), + val packages: List = emptyList(), + val requires: List = emptyList(), + val uses: List = emptyList(), + val provides: List = emptyList() +) + +/** `index.json` -- javadoc's overview page. */ +@Serializable +data class JdOverviewPage( + val page: String = "overview", + val title: String? = null, + val modules: List = emptyList(), + val packages: List = emptyList() +) + +@Serializable +data class JdModuleSummary( + val name: String, + val url: String? = null, + val firstSentence: String? = null +) + +/** `allclasses-index.json`. */ +@Serializable +data class JdAllClassesIndex( + val page: String = "all-classes", + val types: List = emptyList() +) + +/** `allpackages-index.json`. */ +@Serializable +data class JdAllPackagesIndex( + val page: String = "all-packages", + val packages: List = emptyList() +) + +/** One row of `deprecated-list.json`, grouped under the javadoc section it belongs to. */ +@Serializable +data class JdDeprecatedEntry( + val element: String, + val kind: String, + val url: String? = null, + val comment: String? = null, + val forRemoval: Boolean = false, + val since: String? = null +) + +/** `deprecated-list.json`, keyed by javadoc's section names (`classes`, `methods`, ...). */ +@Serializable +data class JdDeprecatedList( + val page: String = "deprecated-list", + val sections: Map> = emptyMap() +) + +@Serializable +data class JdConstantField( + val name: String, + val modifiers: List = emptyList(), + val type: JdTypeRef, + val value: String, + val url: String? = null +) + +@Serializable +data class JdConstantsForType( + val qualifiedName: String, + val url: String? = null, + val fields: List = emptyList() +) + +/** `constant-values.json`, grouped by package then by declaring type, as javadoc groups it. */ +@Serializable +data class JdConstantValues( + val page: String = "constant-values", + val packages: Map> = emptyMap() +) + +/** One entry of the A-Z index that javadoc splits across `index-files/index-N.html`. */ +@Serializable +data class JdIndexEntry( + val label: String, + val kind: String, + val url: String? = null, + val containingElement: String? = null, + val firstSentence: String? = null, + val deprecated: Boolean = false +) + +/** One `index-files/index-N.json` page. */ +@Serializable +data class JdIndexPage( + val page: String = "index", + val letter: String, + val index: Int, + val letters: List = emptyList(), + val entries: List = emptyList() +) diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocExtras.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocExtras.kt new file mode 100644 index 00000000..e94e05b9 --- /dev/null +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocExtras.kt @@ -0,0 +1,15 @@ +package org.appdevforall.dokka.kdoc2json.javadoc + +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.model.properties.WithExtraProperties + +/** + * Reads a documentable's extras without caring which concrete subtype it is. + * + * Dokka declares `extra` on [WithExtraProperties] rather than on [Documentable], so every caller + * would otherwise need its own cast; the star projection is safe here because extras are only ever + * read, never added. + */ +internal fun Documentable.extrasOrEmpty(): PropertyContainer<*> = + (this as? WithExtraProperties<*>)?.extra ?: PropertyContainer.empty() diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt new file mode 100644 index 00000000..ebfcd87a --- /dev/null +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt @@ -0,0 +1,821 @@ +package org.appdevforall.dokka.kdoc2json.javadoc + +import org.appdevforall.dokka.kdoc2json.PluginLogger +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* + +/** + * Builds javadoc-shaped page DTOs from Dokka's model. + * + * Every method that produces a page takes the output-relative path of the file being written, + * because javadoc links relatively (`../lang/Object.json`) and a link is therefore only meaningful + * with respect to the page it appears on. [PageScope] binds that path once and carries it through + * type references, member links and rendered doc comments. + */ +class JavadocMapper( + private val index: JavadocModelIndex, + private val logger: PluginLogger +) { + + companion object { + /** Java modifier order, as javadoc prints it; anything unrecognized is appended after. */ + private val MODIFIER_ORDER = listOf( + "public", "protected", "private", "abstract", "default", "static", "final", + "sealed", "non-sealed", "transient", "volatile", "synchronized", "native", "strictfp" + ) + + /** Dokka spells a "no modifier" visibility/modifier as an empty or Kotlin-only name. */ + private val NON_JAVA_MODIFIERS = setOf("", "open", "empty", "final_kotlin") + + private const val OBJECT_SIMPLE_NAME = "Object" + } + + // Anchors of the members each type declares itself, used to derive Overrides/Specified by. + // Keyed by type key; built on demand because most runs only touch part of the graph. + private val declaredMemberAnchors = mutableMapOf>() + + /** A single output file, and everything that has to be resolved relative to it. */ + inner class PageScope(private val fromFile: String) { + + val docs = JavadocDocs { dri -> linkFor(dri) } + + /** Output-relative path [targetFile], expressed relative to this page. */ + fun url(targetFile: String, anchor: String? = null): String { + val relative = index.paths.relativeUrl(fromFile, targetFile) + return if (anchor.isNullOrBlank()) relative else "$relative#$anchor" + } + + /** A link to whatever [dri] points at, or null when this run doesn't document it. */ + fun linkFor(dri: DRI): String? { + val ownerKey = JavadocModelIndex.keyOf(dri) + val type = index.typeForKey(ownerKey) ?: return null + val callable = dri.callable ?: return url(type.filePath) + val isConstructor = isConstructorCallableName(callable.name, type.simpleName) + return url(type.filePath, index.paths.memberAnchor(dri, isConstructor)) + } + + /** A reference to a documented type by key, or a name-only reference when undocumented. */ + fun typeRefForKey(key: String, display: String = key.substringAfterLast('.')): JdTypeRef { + val type = index.typeForKey(key) + return JdTypeRef( + display = if (type != null) type.classNames else display, + qualifiedName = key, + url = type?.let { url(it.filePath) }, + kind = type?.kind + ) + } + + /** A reference to a type *use*, keeping its type arguments and array dimensions. */ + fun typeRef(bound: Bound): JdTypeRef { + val dri = boundDri(bound) + val type = dri?.let { index.typeForKey(JavadocModelIndex.keyOf(it)) } + return JdTypeRef( + display = renderBound(bound), + qualifiedName = dri?.let { JavadocModelIndex.keyOf(it) }, + url = type?.let { url(it.filePath) }, + kind = type?.kind + ) + } + + fun seeRefs(bundle: JavadocDocBundle): List = bundle.seeAlso.map { (name, address, text) -> + JdSeeRef( + // Dokka puts the referenced symbol in the tag's name and any trailing label in + // its body; javadoc shows the label when there is one. + label = text.ifBlank { name }, + url = address?.let { linkFor(it) }, + qualifiedName = address?.let { JavadocModelIndex.keyOf(it) } + ) + } + + fun throwsList(bundle: JavadocDocBundle): List = bundle.throws.map { (name, address, text) -> + JdThrows( + type = JdTypeRef( + display = name.substringAfterLast('.'), + qualifiedName = address?.let { JavadocModelIndex.keyOf(it) } ?: name, + url = address?.let { linkFor(it) }, + kind = address?.let { index.typeForKey(JavadocModelIndex.keyOf(it))?.kind } + ), + description = text.ifBlank { null } + ) + } + } + + fun scope(fromFile: String) = PageScope(fromFile) + + // ------------------------------------------------------------------ pages + + fun classPage(type: JdType): JdClassPage { + val scope = PageScope(type.filePath) + val doc = type.documentable + val bundle = scope.docs.bundleFor(doc) + + val generics = (doc as? WithGenerics)?.generics.orEmpty() + val superclassKey = index.superclassOf(type.key) + val declaredInterfaceKeys = index.directInterfacesOf(type.key) + + // Supertype *uses* keep their type arguments (`AbstractList`), which the key-only + // hierarchy maps can't carry, so they are read back off the documentable here. + val supertypeUses = (doc as? WithSupertypes)?.supertypes?.values?.flatten() + ?.distinctBy { JavadocModelIndex.keyOf(it.typeConstructor.dri) } + .orEmpty() + .associate { JavadocModelIndex.keyOf(it.typeConstructor.dri) to it.typeConstructor } + + val superclassRef = superclassKey?.let { key -> + supertypeUses[key]?.let { scope.typeRef(it) } ?: scope.typeRefForKey(key) + } + val superinterfaceRefs = declaredInterfaceKeys.map { key -> + supertypeUses[key]?.let { scope.typeRef(it) } ?: scope.typeRefForKey(key) + } + + val members = membersOf(type, scope) + val modifiers = modifiersOf(doc) + + val nestedTypes = doc.classlikes + .mapNotNull { index.typeFor(it.dri) } + .sortedBy { it.simpleName } + .map { nested -> + // Rendered in *this* page's scope, not the nested type's own, so the links inside + // the summary resolve relative to the page the summary appears on. + val nestedBundle = scope.docs.bundleFor(nested.documentable) + JdNestedTypeRef( + name = nested.classNames, + qualifiedName = nested.qualifiedName, + kind = nested.kind, + modifiers = modifiersOf(nested.documentable), + url = scope.url(nested.filePath), + firstSentence = JavadocDocs.firstSentence(nestedBundle.description), + deprecated = deprecationOf(nested.documentable, nestedBundle) + ) + } + + val isInterfaceLike = type.kind == "interface" || type.kind == "annotation" + val inheritanceRefs = + if (isInterfaceLike) emptyList() + else index.inheritanceChain(type.key).map { scope.typeRefForKey(it) } + + return JdClassPage( + kind = if (index.isException(type.key)) "exception" else type.kind, + name = type.classNames, + simpleName = type.simpleName, + qualifiedName = type.qualifiedName, + packageName = type.packageName, + moduleName = type.moduleName, + url = type.filePath, + modifiers = modifiers, + signature = classSignature(type, modifiers, generics, superclassRef, superinterfaceRefs, scope), + typeParameters = typeParameters(generics, bundle, scope), + superclass = superclassRef, + superinterfaces = superinterfaceRefs, + inheritance = inheritanceRefs, + allImplementedInterfaces = + if (isInterfaceLike) emptyList() + else index.allSuperinterfaces(type.key).map { scope.typeRefForKey(it) }, + allSuperinterfaces = + if (isInterfaceLike) index.allSuperinterfaces(type.key).map { scope.typeRefForKey(it) } + else emptyList(), + directKnownSubclasses = index.directKnownSubclasses(type.key).map { scope.typeRefForKey(it) }, + allKnownSubinterfaces = index.allKnownSubinterfaces(type.key).map { scope.typeRefForKey(it) }, + allKnownImplementingClasses = index.allKnownImplementingClasses(type.key).map { scope.typeRefForKey(it) }, + enclosingType = index.enclosingTypeOf(type)?.let { scope.typeRefForKey(it.key) }, + isFunctionalInterface = isFunctionalInterface(type, members.methods), + description = bundle.description, + firstSentence = JavadocDocs.firstSentence(bundle.description), + since = bundle.since, + seeAlso = scope.seeRefs(bundle), + authors = bundle.authors, + versions = bundle.versions, + deprecated = deprecationOf(doc, bundle), + annotations = annotationNamesOf(doc), + tags = bundle.other, + nestedTypes = nestedTypes, + enumConstants = members.enumConstants, + fields = members.fields, + constructors = members.constructors, + methods = members.methods, + annotationElements = members.annotationElements, + inheritedFields = members.inheritedFields, + inheritedMethods = members.inheritedMethods, + inheritedNestedTypes = emptyList() + ) + } + + fun packagePage(pkg: JdPackage, typesInPackage: List): JdPackagePage { + val scope = PageScope(pkg.filePath) + val bundle = pkg.documentables + .map { scope.docs.bundleFor(it) } + .firstOrNull { it.description != null || it.other.isNotEmpty() } + ?: JavadocDocBundle() + + val summaries = typesInPackage.sortedBy { it.classNames }.map { typeSummary(it, scope) } + fun of(vararg kinds: String) = summaries.filter { it.kind in kinds } + + return JdPackagePage( + name = pkg.name, + moduleName = pkg.moduleName, + url = pkg.filePath, + description = bundle.description, + firstSentence = JavadocDocs.firstSentence(bundle.description), + since = bundle.since, + seeAlso = scope.seeRefs(bundle), + deprecated = pkg.documentables.firstNotNullOfOrNull { deprecationOf(it, bundle) }, + tags = bundle.other, + interfaces = of("interface"), + classes = of("class", "object"), + enums = of("enum"), + records = of("record"), + exceptions = of("exception"), + annotationTypes = of("annotation"), + allTypes = summaries + ) + } + + fun modulePage(module: JdModule, packagesInModule: List): JdModulePage { + val scope = PageScope(module.filePath) + val bundle = module.documentables + .map { scope.docs.bundleFor(it) } + .firstOrNull { it.description != null || it.other.isNotEmpty() } + ?: JavadocDocBundle() + + return JdModulePage( + name = module.name, + url = module.filePath, + description = bundle.description, + firstSentence = JavadocDocs.firstSentence(bundle.description), + since = bundle.since, + seeAlso = scope.seeRefs(bundle), + deprecated = module.documentables.firstNotNullOfOrNull { deprecationOf(it, bundle) }, + tags = bundle.other, + packages = packagesInModule.map { packageSummary(it, scope) } + ) + } + + // ------------------------------------------------------------- summaries + + fun typeSummary(type: JdType, scope: PageScope): JdTypeSummary { + val bundle = scope.docs.bundleFor(type.documentable) + return JdTypeSummary( + name = type.classNames, + qualifiedName = type.qualifiedName, + kind = if (index.isException(type.key)) "exception" else type.kind, + packageName = type.packageName, + moduleName = type.moduleName, + url = scope.url(type.filePath), + firstSentence = JavadocDocs.firstSentence(bundle.description), + deprecated = deprecationOf(type.documentable, bundle) + ) + } + + fun packageSummary(pkg: JdPackage, scope: PageScope): JdPackageSummary { + val bundle = pkg.documentables + .map { scope.docs.bundleFor(it) } + .firstOrNull { it.description != null } + ?: JavadocDocBundle() + return JdPackageSummary( + name = pkg.name, + moduleName = pkg.moduleName, + url = scope.url(pkg.filePath), + firstSentence = JavadocDocs.firstSentence(bundle.description), + deprecated = pkg.documentables.firstNotNullOfOrNull { deprecationOf(it, bundle) } + ) + } + + fun moduleSummary(module: JdModule, scope: PageScope): JdModuleSummary { + val bundle = module.documentables + .map { scope.docs.bundleFor(it) } + .firstOrNull { it.description != null } + ?: JavadocDocBundle() + return JdModuleSummary( + name = module.name, + url = scope.url(module.filePath), + firstSentence = JavadocDocs.firstSentence(bundle.description) + ) + } + + // --------------------------------------------------------------- members + + /** Everything a class page lists, split the way javadoc splits it into tables. */ + class ClassMembers( + val enumConstants: List = emptyList(), + val fields: List = emptyList(), + val constructors: List = emptyList(), + val methods: List = emptyList(), + val annotationElements: List = emptyList(), + val inheritedFields: List = emptyList(), + val inheritedMethods: List = emptyList() + ) + + /** + * The members of a type, sorted into javadoc's four buckets (declared/inherited x field/method). + * + * Dokka merges a private Java field and its accessor pair into a single `DProperty` with a + * non-null `getter`, the way a Kotlin property looks. javadoc shows the opposite: the private + * field is not documented at all and `getWidth()` is a *method*. So a property that carries + * accessors is unfolded back into its accessor methods, and only an accessor-less property -- + * a genuine Java field -- is reported as a field. + */ + private class SplitMembers( + val declaredFields: List = emptyList(), + val inheritedFields: List> = emptyList(), + val declaredMethods: List = emptyList(), + val inheritedMethods: List> = emptyList() + ) + + private val splitMembersCache = mutableMapOf() + + private fun splitMembers(type: JdType): SplitMembers = splitMembersCache.getOrPut(type.key) { + val doc = type.documentable + + val declaredFields = mutableListOf() + val inheritedFields = mutableListOf>() + val declaredMethods = mutableListOf() + val inheritedMethods = mutableListOf>() + + doc.properties.forEach { property -> + val from = inheritedFromKey(property, type.key) + val accessors = listOfNotNull(property.getter, property.setter) + if (accessors.isNotEmpty()) { + accessors.forEach { accessor -> + if (from == null) declaredMethods += accessor else inheritedMethods += from to accessor + } + } else { + if (from == null) declaredFields += property else inheritedFields += from to property + } + } + + doc.functions.forEach { function -> + val from = inheritedFromKey(function, type.key) + if (from == null) declaredMethods += function else inheritedMethods += from to function + } + + SplitMembers(declaredFields, inheritedFields, declaredMethods, inheritedMethods) + } + + fun membersOf(type: JdType, scope: PageScope): ClassMembers { + val doc = type.documentable + val split = splitMembers(type) + + val constructors = (doc as? WithConstructors)?.constructors.orEmpty() + val enumEntries = (doc as? DEnum)?.entries.orEmpty() + + val isAnnotation = doc is DAnnotation + val executables = split.declaredMethods.map { executable(it, type, scope, isConstructor = false) } + + return ClassMembers( + enumConstants = enumEntries.map { enumConstant(it, type, scope) }, + fields = split.declaredFields.map { field(it, type, scope) }.sortedBy { it.name }, + constructors = constructors.map { executable(it, type, scope, isConstructor = true) }, + methods = if (isAnnotation) emptyList() else executables.sortedBy { it.anchor }, + annotationElements = if (!isAnnotation) emptyList() else executables.sortedBy { it.name }, + inheritedFields = groupInherited(split.inheritedFields, scope) { property, owner -> + memberRef(property.name, property.dri, owner, scope, isConstructor = false) + }, + inheritedMethods = groupInherited(split.inheritedMethods, scope) { function, owner -> + memberRef(function.name, function.dri, owner, scope, isConstructor = false) + } + ) + } + + private fun groupInherited( + members: List>, + scope: PageScope, + toRef: (T, String) -> JdMemberRef + ): List = + members.groupBy({ it.first }, { it.second }) + .toSortedMap() + .map { (ownerKey, owned) -> + JdInheritedMembers( + declaringType = scope.typeRefForKey(ownerKey), + members = owned.map { toRef(it, ownerKey) }.sortedBy { it.signature } + ) + } + + private fun memberRef( + name: String?, + dri: DRI, + ownerKey: String, + scope: PageScope, + isConstructor: Boolean + ): JdMemberRef { + val anchor = index.paths.memberAnchor(dri, isConstructor) + val owner = index.typeForKey(ownerKey) + return JdMemberRef( + name = name.orEmpty(), + signature = anchor, + url = owner?.let { scope.url(it.filePath, anchor) }, + declaringType = scope.typeRefForKey(ownerKey) + ) + } + + private fun field(property: DProperty, owner: JdType, scope: PageScope): JdField { + val bundle = scope.docs.bundleFor(property) + val modifiers = modifiersOf(property) + val typeRef = scope.typeRef(property.type) + val anchor = index.paths.memberAnchor(property.dri, isConstructor = false) + return JdField( + name = property.name, + anchor = anchor, + modifiers = modifiers, + type = typeRef, + signature = (modifiers + typeRef.display + property.name).joinToString(" "), + url = scope.url(owner.filePath, anchor), + description = bundle.description, + firstSentence = JavadocDocs.firstSentence(bundle.description), + constantValue = defaultValueOf(property), + since = bundle.since, + seeAlso = scope.seeRefs(bundle), + deprecated = deprecationOf(property, bundle), + annotations = annotationNamesOf(property), + tags = bundle.other + ) + } + + private fun enumConstant(entry: DEnumEntry, owner: JdType, scope: PageScope): JdField { + val bundle = scope.docs.bundleFor(entry) + val anchor = entry.name + return JdField( + name = entry.name, + anchor = anchor, + modifiers = listOf("public", "static", "final"), + type = scope.typeRefForKey(owner.key, owner.classNames), + signature = "public static final ${owner.simpleName} ${entry.name}", + url = scope.url(owner.filePath, anchor), + description = bundle.description, + firstSentence = JavadocDocs.firstSentence(bundle.description), + since = bundle.since, + seeAlso = scope.seeRefs(bundle), + deprecated = deprecationOf(entry, bundle), + annotations = annotationNamesOf(entry), + tags = bundle.other + ) + } + + private fun executable( + function: DFunction, + owner: JdType, + scope: PageScope, + isConstructor: Boolean + ): JdExecutable { + val bundle = scope.docs.bundleFor(function) + val modifiers = run { + val found = modifierSetOf(function) + // Dokka reports an interface's default methods simply as "not abstract" and never + // emits the `default` keyword. In Java an interface method that is neither abstract + // nor static is exactly a default method, so the keyword is recovered here rather + // than being lost from the signature. + if (owner.kind == "interface" && "abstract" !in found && "static" !in found) { + found += "default" + } + orderModifiers(found) + } + val anchor = index.paths.memberAnchor(function.dri, isConstructor) + val returnType = if (isConstructor) null else scope.typeRef(function.type) + + val parameters = function.parameters.map { parameter -> + JdParameter( + name = parameter.name.orEmpty(), + type = scope.typeRef(parameter.type), + description = parameter.name?.let { bundle.params[it] }?.ifBlank { null }, + annotations = annotationNamesOf(parameter) + ) + } + + val declaredThrows = scope.throwsList(bundle) + val kind = when { + isConstructor -> "constructor" + owner.kind == "annotation" -> "annotationElement" + else -> "method" + } + + val (overrides, specifiedBy) = + if (isConstructor) null to emptyList() else overrideInfo(owner, anchor, scope) + + return JdExecutable( + name = if (isConstructor) owner.simpleName else function.name, + anchor = anchor, + kind = kind, + modifiers = modifiers, + typeParameters = typeParameters(function.generics, bundle, scope), + returnType = returnType, + parameters = parameters, + exceptions = declaredThrows, + signature = executableSignature( + if (isConstructor) owner.simpleName else function.name, + modifiers, function.generics, returnType, parameters, declaredThrows, scope + ), + url = scope.url(owner.filePath, anchor), + description = bundle.description, + firstSentence = JavadocDocs.firstSentence(bundle.description), + returns = bundle.returns, + specifiedBy = specifiedBy, + overrides = overrides, + since = bundle.since, + seeAlso = scope.seeRefs(bundle), + deprecated = deprecationOf(function, bundle), + annotations = annotationNamesOf(function), + defaultValue = defaultValueOf(function), + tags = bundle.other + ) + } + + /** + * javadoc's "Overrides:" (nearest superclass declaring the same erased signature) and + * "Specified by:" (every superinterface declaring it). Both are derived from the anchor, + * which already encodes name plus erased parameter types -- exactly the identity Java uses + * to decide whether one method overrides another. + */ + private fun overrideInfo( + owner: JdType, + anchor: String, + scope: PageScope + ): Pair> { + val overriddenIn = index.superclassChain(owner.key).firstOrNull { anchor in anchorsDeclaredIn(it) } + val specifiedIn = index.allSuperinterfaces(owner.key).filter { anchor in anchorsDeclaredIn(it) } + + fun refTo(ownerKey: String): JdMemberRef? { + val target = index.typeForKey(ownerKey) ?: return null + return JdMemberRef( + name = anchor.substringBefore('('), + signature = anchor, + url = scope.url(target.filePath, anchor), + declaringType = scope.typeRefForKey(ownerKey) + ) + } + + return (overriddenIn?.let { refTo(it) }) to specifiedIn.mapNotNull { refTo(it) } + } + + private fun anchorsDeclaredIn(key: String): Set = declaredMemberAnchors.getOrPut(key) { + val type = index.typeForKey(key) ?: return@getOrPut emptySet() + val split = splitMembers(type) + val anchors = mutableSetOf() + split.declaredMethods.forEach { anchors += index.paths.memberAnchor(it.dri, isConstructor = false) } + split.declaredFields.forEach { anchors += index.paths.memberAnchor(it.dri, isConstructor = false) } + anchors + } + + // ------------------------------------------------------------ signatures + + private fun classSignature( + type: JdType, + modifiers: List, + generics: List, + superclass: JdTypeRef?, + superinterfaces: List, + scope: PageScope + ): String { + val keyword = when (type.kind) { + "interface" -> "interface" + "enum" -> "enum" + "annotation" -> "@interface" + else -> "class" + } + val typeParams = if (generics.isEmpty()) "" else + generics.joinToString(",", "<", ">") { renderTypeParameterDeclaration(it, scope) } + + return buildString { + append((modifiers + keyword).joinToString(" ")) + append(' ') + append(type.classNames) + append(typeParams) + // An interface's parents are all spelled `extends`; a class extends one and + // implements the rest. + if (type.kind == "interface" || type.kind == "annotation") { + val parents = listOfNotNull(superclass) + superinterfaces + if (parents.isNotEmpty()) append(parents.joinToString(", ", " extends ") { it.display }) + } else { + superclass?.let { append(" extends ${it.display}") } + if (superinterfaces.isNotEmpty()) { + append(superinterfaces.joinToString(", ", " implements ") { it.display }) + } + } + } + } + + private fun executableSignature( + name: String, + modifiers: List, + generics: List, + returnType: JdTypeRef?, + parameters: List, + exceptions: List, + scope: PageScope + ): String = buildString { + if (modifiers.isNotEmpty()) append(modifiers.joinToString(" ")).append(' ') + if (generics.isNotEmpty()) { + append(generics.joinToString(",", "<", "> ") { renderTypeParameterDeclaration(it, scope) }) + } + returnType?.let { append(it.display).append(' ') } + append(name) + append(parameters.joinToString(", ", "(", ")") { "${it.type.display} ${it.name}" }) + if (exceptions.isNotEmpty()) { + append(exceptions.joinToString(", ", " throws ") { it.type.display }) + } + } + + private fun renderTypeParameterDeclaration(generic: DTypeParameter, scope: PageScope): String { + // `extends Object` is implicit in Java and javadoc omits it, so an Object-only bound is + // dropped rather than printed. + val bounds = generic.bounds.map { renderBound(it) }.filter { it != OBJECT_SIMPLE_NAME } + val name = generic.variantTypeParameter.let { generic.name } + return if (bounds.isEmpty()) name else "$name extends ${bounds.joinToString(" & ")}" + } + + private fun typeParameters( + generics: List, + bundle: JavadocDocBundle, + scope: PageScope + ): List = generics.map { generic -> + JdTypeParameter( + name = generic.name, + bounds = generic.bounds.map { scope.typeRef(it) }, + // Dokka keeps the angle brackets a type parameter's @param was written with, so + // `@param ...` is filed under "" rather than "U". + description = (bundle.params["<${generic.name}>"] ?: bundle.params[generic.name]) + ?.ifBlank { null } + ) + } + + // ------------------------------------------------------------- type text + + /** Renders a [Bound] the way javadoc spells a type in a signature. */ + fun renderBound(bound: Bound): String = when (bound) { + is TypeParameter -> bound.presentableName ?: bound.name + is Nullable -> renderBound(bound.inner) + is DefinitelyNonNullable -> renderBound(bound.inner) + is TypeAliased -> renderBound(bound.typeAlias) + is PrimitiveJavaType -> bound.name + is JavaObject -> OBJECT_SIMPLE_NAME + is Void -> "void" + is Dynamic -> "dynamic" + is UnresolvedBound -> bound.name + is GenericTypeConstructor -> renderConstructor(bound.dri, bound.projections, bound.presentableName) + is FunctionalTypeConstructor -> renderConstructor(bound.dri, bound.projections, bound.presentableName) + } + + private fun renderConstructor(dri: DRI, projections: List, presentableName: String?): String { + val key = JavadocModelIndex.keyOf(dri) + // Dokka models a Java array as a single-argument `kotlin.Array`. + if (key == "kotlin.Array") { + val element = projections.firstOrNull()?.let { renderProjection(it) } ?: OBJECT_SIMPLE_NAME + return "$element[]" + } + PRIMITIVE_ARRAYS[key]?.let { return it } + val name = presentableName ?: dri.classNames ?: key.substringAfterLast('.') + if (projections.isEmpty()) return name + return name + projections.joinToString(",", "<", ">") { renderProjection(it) } + } + + private fun renderProjection(projection: Projection): String = when (projection) { + is Star -> "?" + is Covariance<*> -> "? extends ${renderBound(projection.inner)}" + is Contravariance<*> -> "? super ${renderBound(projection.inner)}" + is Invariance<*> -> renderBound(projection.inner) + is Bound -> renderBound(projection) + } + + // ------------------------------------------------------------- modifiers + + /** + * The Java modifier list for a declaration, in javadoc's order. + * + * Three Dokka sources are merged: `visibility`, `modifier` (final/abstract) and the + * `AdditionalModifiers` extra (static, synchronized, transient, volatile, native, default...). + * Modifiers Dokka reports that aren't Java keywords are kept at the end rather than dropped, + * so nothing from the model is lost when the source is Kotlin. + */ + fun modifiersOf(doc: Documentable): List = orderModifiers(modifierSetOf(doc)) + + private fun modifierSetOf(doc: Documentable): MutableSet { + val found = LinkedHashSet() + + (doc as? WithVisibility)?.visibility?.values?.forEach { visibility -> + visibility.name.lowercase().takeIf { it !in NON_JAVA_MODIFIERS }?.let { found += it } + } + (doc as? WithAbstraction)?.modifier?.values?.forEach { modifier -> + modifier.name.lowercase().takeIf { it !in NON_JAVA_MODIFIERS }?.let { found += it } + } + doc.extrasOrEmpty().allOfType().forEach { additional -> + additional.content.values.flatten().forEach { found += it.name.lowercase() } + } + return found + } + + /** Puts a modifier set into javadoc's print order, keeping anything unrecognized at the end. */ + private fun orderModifiers(found: Set): List = + MODIFIER_ORDER.filter { it in found } + found.filterNot { it in MODIFIER_ORDER }.sorted() + + // ------------------------------------------------------------------ misc + + /** + * A declaration's deprecation as it should read *on [scope]'s page*. + * + * The comment is a rendered doc fragment and can contain links, so it cannot be lifted from + * one page onto another -- a global index page has to re-render it against its own location + * or the links inside it point at the wrong place. + */ + fun deprecationFor(doc: Documentable, scope: PageScope): JdDeprecation? = + deprecationOf(doc, scope.docs.bundleFor(doc)) + + private fun deprecationOf(doc: Documentable, bundle: JavadocDocBundle): JdDeprecation? { + val annotation = deprecatedAnnotation(doc) + if (!bundle.isDeprecatedTagPresent && annotation == null) return null + return JdDeprecation( + comment = bundle.deprecated, + forRemoval = annotation?.get("forRemoval")?.contains("true") == true, + since = annotation?.get("since")?.trim('"')?.ifBlank { null } + ) + } + + /** The parameters of a `@Deprecated`/`@kotlin.Deprecated` annotation, if one is present. */ + private fun deprecatedAnnotation(doc: Documentable): Map? = + doc.extrasOrEmpty().allOfType() + .flatMap { it.directAnnotations.values.flatten() } + .firstOrNull { it.dri.classNames == "Deprecated" } + ?.params + ?.mapValues { it.value.toString() } + + private fun annotationNamesOf(doc: Documentable): List = + doc.extrasOrEmpty().allOfType() + .flatMap { it.directAnnotations.values.flatten() } + .mapNotNull { it.dri.classNames } + .distinct() + .map { "@$it" } + + /** + * A constant field's value, or an annotation element's `default`. Dokka records both in the + * same `DefaultValue` extra, which is read reflectively because its accessor name has moved + * between Dokka versions (see `ModelMapper.mapExtras` for the same treatment). + */ + private fun defaultValueOf(doc: Documentable): String? { + val extra = doc.extrasOrEmpty().allOfType() + .firstOrNull { it::class.java.simpleName == "DefaultValue" } ?: return null + return try { + val accessor = extra::class.java.methods + .firstOrNull { it.name == "getValue" || it.name == "getExpression" } ?: return null + when (val value = accessor.invoke(extra)) { + null -> null + is Map<*, *> -> value.values.firstOrNull()?.let { renderExpression(it) } + else -> renderExpression(value) + } + } catch (e: Exception) { + logger.debug("javadoc-mode: could not read DefaultValue for ${doc.dri}: ${e.message}") + null + } + } + + /** + * Renders one of Dokka's `Expression` values (`IntegerConstant`, `StringConstant`, ...) as the + * literal javadoc would print. Their `toString` is the data-class form -- `IntegerConstant( + * value=4)` -- so the wrapped value is unwrapped reflectively, string constants being quoted + * the way javadoc's constant-values page quotes them. + */ + private fun renderExpression(expression: Any): String { + val unwrapped = try { + expression::class.java.methods + .firstOrNull { it.name == "getValue" && it.parameterCount == 0 } + ?.invoke(expression) + } catch (e: Exception) { + logger.debug("javadoc-mode: could not unwrap expression ${expression::class.java.simpleName}: ${e.message}") + null + } ?: return expression.toString() + + return if (expression::class.java.simpleName == "StringConstant") "\"$unwrapped\"" else unwrapped.toString() + } + + /** + * The key of the type a member was inherited from, or null when [ownerKey] declares it itself. + * Dokka attaches this as the `InheritedMember` extra when it copies members down a hierarchy. + */ + private fun inheritedFromKey(doc: Documentable, ownerKey: String): String? { + val inherited = doc.extrasOrEmpty().allOfType().firstOrNull() ?: return null + val from = inherited.inheritedFrom.values.firstOrNull { it != null } ?: return null + val fromKey = JavadocModelIndex.keyOf(from) + return if (fromKey == ownerKey || fromKey.isBlank()) null else fromKey + } + + /** javadoc marks an interface with exactly one abstract method as a functional interface. */ + private fun isFunctionalInterface(type: JdType, methods: List): Boolean { + if (type.kind != "interface") return false + return methods.count { "abstract" in it.modifiers || ("default" !in it.modifiers && "static" !in it.modifiers) } == 1 + } + + private fun boundDri(bound: Bound): DRI? = when (bound) { + is GenericTypeConstructor -> bound.dri + is FunctionalTypeConstructor -> bound.dri + is TypeParameter -> null + is Nullable -> boundDri(bound.inner) + is DefinitelyNonNullable -> boundDri(bound.inner) + is TypeAliased -> boundDri(bound.typeAlias) + else -> null + } + + private fun isConstructorCallableName(callableName: String, simpleName: String): Boolean = + callableName == "" || callableName == simpleName + + private val PRIMITIVE_ARRAYS = mapOf( + "kotlin.IntArray" to "int[]", "kotlin.LongArray" to "long[]", + "kotlin.ShortArray" to "short[]", "kotlin.ByteArray" to "byte[]", + "kotlin.CharArray" to "char[]", "kotlin.BooleanArray" to "boolean[]", + "kotlin.FloatArray" to "float[]", "kotlin.DoubleArray" to "double[]" + ) +} diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocModelIndex.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocModelIndex.kt new file mode 100644 index 00000000..f63b6f62 --- /dev/null +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocModelIndex.kt @@ -0,0 +1,372 @@ +package org.appdevforall.dokka.kdoc2json.javadoc + +import org.appdevforall.dokka.kdoc2json.PluginLogger +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.DAnnotation +import org.jetbrains.dokka.model.DClass +import org.jetbrains.dokka.model.DClasslike +import org.jetbrains.dokka.model.DEnum +import org.jetbrains.dokka.model.DInterface +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.model.DObject +import org.jetbrains.dokka.model.DPackage +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.WithSupertypes +import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.pages.WithDocumentables + +/** A type this documentation run covers, plus everything the renderer needs to place it. */ +class JdType( + val documentable: DClasslike, + val key: String, + val qualifiedName: String, + /** Dotted name relative to the package, e.g. `Map.Entry`. */ + val classNames: String, + val simpleName: String, + val packageName: String, + val moduleName: String?, + val kind: String, + val filePath: String +) + +class JdPackage( + val name: String, + val moduleName: String?, + val documentables: List, + val filePath: String +) + +class JdModule( + val name: String, + val documentables: List, + val filePath: String +) + +/** + * A whole-run view of the documented model. + * + * A javadoc page is not derivable from its own declaration alone: "All Implemented Interfaces", + * "Direct Known Subclasses", "All Known Implementing Classes" and the inherited-member groups are + * all *global* facts about the type graph. So Javadoc mode does one collection pass up front, + * builds the hierarchy in both directions, and then renders every page against this index. + * + * All closures are computed once in [build] rather than on demand: a per-page closure walk would + * be quadratic in the number of types, which is the difference between usable and unusable on a + * JDK-sized run. + * + * Types outside the run (e.g. `java.lang.Object` when only one package is documented) are simply + * absent, which is also what javadoc does -- it links only what it documents. + */ +class JavadocModelIndex private constructor( + val paths: JavadocPaths, + val modules: List, + val packages: List, + val types: List, + private val byKey: Map, + private val superclassByKey: Map, + private val interfacesByKey: Map>, + private val allSuperinterfacesByKey: Map>, + private val directSubclassesByKey: Map>, + private val subinterfacesByKey: Map>, + private val implementorsByKey: Map>, + private val exceptionKeys: Set +) { + + companion object { + const val OBJECT_FQN = "java.lang.Object" + private const val THROWABLE_FQN = "java.lang.Throwable" + + /** Normalized cross-reference key: a type's fully qualified dotted name. */ + fun keyOf(dri: DRI): String { + val pkg = dri.packageName.orEmpty() + val cls = dri.classNames.orEmpty() + return if (pkg.isBlank()) cls else "$pkg.$cls" + } + + fun build( + root: PageNode, + logger: PluginLogger, + sourceSetWhitelist: List + ): JavadocModelIndex { + val collected = collectDocumentables(root) + + val moduleDocs = collected.filterIsInstance() + // Module directories only when this run genuinely spans several modules -- javadoc + // likewise flattens packages to the output root for a non-modular build. + val useModuleDirs = moduleDocs.distinctBy { it.name }.size > 1 + val paths = JavadocPaths(useModuleDirs) + + val moduleOfPackage = mutableMapOf() + moduleDocs.forEach { module -> + module.packages.forEach { pkg -> + moduleOfPackage.putIfAbsent(pkg.dri.packageName.orEmpty(), module.name) + } + } + + fun passesWhitelist(doc: Documentable): Boolean { + if (sourceSetWhitelist.isEmpty()) return true + return doc.sourceSets.any { it.sourceSetID.toString().substringAfterLast("/") in sourceSetWhitelist } + } + + // --- Types --- + val types = mutableListOf() + val byKey = mutableMapOf() + collected.filterIsInstance().forEach { doc -> + if (!passesWhitelist(doc)) { + logger.info("javadoc-mode: omitting '${doc.name}' (source sets not in whitelist $sourceSetWhitelist)") + return@forEach + } + val key = keyOf(doc.dri) + if (byKey.containsKey(key)) return@forEach + val packageName = doc.dri.packageName.orEmpty() + val classNames = doc.dri.classNames ?: doc.name ?: return@forEach + val moduleName = moduleOfPackage[packageName] + val type = JdType( + documentable = doc, + key = key, + qualifiedName = key, + classNames = classNames, + simpleName = classNames.substringAfterLast('.'), + packageName = packageName, + moduleName = moduleName, + kind = kindOf(doc), + filePath = paths.classFile(packageName, classNames, moduleName) + ) + types += type + byKey[key] = type + } + + // --- Direct hierarchy --- + val superclassByKey = mutableMapOf() + val interfacesByKey = mutableMapOf>() + + types.forEach { type -> + val doc = type.documentable + if (doc !is WithSupertypes) { + interfacesByKey[type.key] = emptyList() + return@forEach + } + val supers = doc.supertypes.values.flatten().distinctBy { keyOf(it.typeConstructor.dri) } + val ifaces = mutableListOf() + supers.forEach { supertype -> + val superKey = keyOf(supertype.typeConstructor.dri) + if (superKey == type.key) return@forEach + // Prefer what the supertype actually *is* over the kind recorded at the use + // site; the recorded kind only has to be trusted for types we don't document. + val known = byKey[superKey] + val isInterface = when { + known != null -> known.kind == "interface" || known.kind == "annotation" + else -> supertype.kind.toString().uppercase().contains("INTERFACE") + } + if (isInterface) { + ifaces += superKey + } else { + // A class has at most one superclass; keep the first and ignore any + // duplicate a cross-source-set merge might have produced. + superclassByKey.putIfAbsent(type.key, superKey) + } + } + interfacesByKey[type.key] = ifaces.distinct() + } + + val directSubclasses = mutableMapOf>() + types.forEach { type -> + superclassByKey[type.key]?.let { superKey -> + directSubclasses.getOrPut(superKey) { mutableListOf() } += type.key + } + } + + // --- Transitive interface closure, memoized across the whole graph --- + val closureMemo = mutableMapOf>() + val inProgress = mutableSetOf() + + fun closureOf(key: String): List { + closureMemo[key]?.let { return it } + // Guards against a cycle in a malformed/merged hierarchy; without it a cyclic + // `extends` chain would recurse until the stack blew. + if (!inProgress.add(key)) return emptyList() + val result = LinkedHashSet() + interfacesByKey[key].orEmpty().forEach { iface -> + result += iface + result += closureOf(iface) + } + superclassByKey[key]?.let { result += closureOf(it) } + inProgress.remove(key) + val list = result.toList() + closureMemo[key] = list + return list + } + + val allSuperinterfacesByKey = types.associate { it.key to closureOf(it.key) } + + val subinterfaces = mutableMapOf>() + val implementors = mutableMapOf>() + types.forEach { type -> + val bucket = if (type.kind == "interface") subinterfaces else implementors + allSuperinterfacesByKey[type.key].orEmpty().forEach { iface -> + bucket.getOrPut(iface) { mutableListOf() } += type.key + } + } + + // --- Exception classification (javadoc tables exceptions separately) --- + val exceptionKeys = mutableSetOf() + types.forEach { type -> + if (isExceptionType(type, superclassByKey)) exceptionKeys += type.key + } + + // --- Packages and modules --- + val packages = collected.filterIsInstance() + .groupBy { it.dri.packageName.orEmpty() } + .map { (name, docs) -> + val moduleName = moduleOfPackage[name] + JdPackage(name, moduleName, docs, paths.packageFile(name, moduleName)) + } + .sortedBy { it.name } + + val modules = moduleDocs.groupBy { it.name } + .map { (name, docs) -> JdModule(name, docs, paths.moduleFile(name)) } + .sortedBy { it.name } + + logger.info( + "javadoc-mode: indexed ${types.size} types, ${packages.size} packages, " + + "${modules.size} module(s); module directories=$useModuleDirs" + ) + + return JavadocModelIndex( + paths = paths, + modules = modules, + packages = packages, + types = types.sortedBy { it.qualifiedName }, + byKey = byKey, + superclassByKey = superclassByKey, + interfacesByKey = interfacesByKey, + allSuperinterfacesByKey = allSuperinterfacesByKey, + directSubclassesByKey = directSubclasses.mapValues { it.value.distinct().sorted() }, + subinterfacesByKey = subinterfaces.mapValues { it.value.distinct().sorted() }, + implementorsByKey = implementors.mapValues { it.value.distinct().sorted() }, + exceptionKeys = exceptionKeys + ) + } + + /** + * Whether a type belongs in javadoc's "Exception Classes" table. + * + * The reliable signal is a `java.lang.Throwable` ancestor, but a run that documents only + * part of a codebase often stops short of it -- the chain ends at, say, an undocumented + * `java.lang.RuntimeException`. Dokka's own `ExceptionInSupertypes` extra covers most of + * that gap; the trailing name check is the last resort for the remainder, and only ever + * looks at *ancestors*, never at the type's own name, so a class merely called + * `ExceptionHandler` isn't miscategorised. + */ + private fun isExceptionType(type: JdType, superclassByKey: Map): Boolean { + if (type.documentable.extrasOrEmpty().allOfType() + .any { it::class.java.simpleName == "ExceptionInSupertypes" } + ) { + return true + } + val ancestors = mutableListOf() + val seen = mutableSetOf(type.key) + var current = superclassByKey[type.key] + while (current != null && seen.add(current)) { + ancestors += current + current = superclassByKey[current] + } + return ancestors.any { + it == THROWABLE_FQN || it.endsWith("Exception") || it.endsWith("Error") + } + } + + private fun kindOf(doc: DClasslike): String = when (doc) { + is DInterface -> "interface" + is DEnum -> "enum" + is DAnnotation -> "annotation" + is DObject -> "object" + is DClass -> "class" + else -> "class" + } + + /** + * Every documentable reachable from the page tree, deduplicated. + * + * Walking pages alone is not enough: a package's classlikes and a class's nested types + * hang off the *documentable* tree, and Dokka does not always give every one of them its + * own page, so both trees are traversed. + */ + private fun collectDocumentables(root: PageNode): List { + val seen = LinkedHashMap() + + fun visitDocumentable(doc: Documentable) { + val id = "${doc::class.java.simpleName}|${doc.dri}" + if (seen.putIfAbsent(id, doc) != null) return + when (doc) { + is DModule -> doc.packages.forEach { visitDocumentable(it) } + is DPackage -> { + doc.classlikes.forEach { visitDocumentable(it) } + doc.typealiases.forEach { visitDocumentable(it) } + } + is DClasslike -> doc.classlikes.forEach { visitDocumentable(it) } + else -> Unit + } + } + + fun visitPage(node: PageNode) { + if (node is WithDocumentables) node.documentables.forEach { visitDocumentable(it) } + node.children.forEach { visitPage(it) } + } + + visitPage(root) + return seen.values.toList() + } + } + + fun typeFor(dri: DRI): JdType? = byKey[keyOf(dri)] + + fun typeForKey(key: String): JdType? = byKey[key] + + fun superclassOf(key: String): String? = superclassByKey[key] + + fun directInterfacesOf(key: String): List = interfacesByKey[key].orEmpty() + + /** + * The superclass chain, outermost ancestor first and [key] itself last -- the order javadoc + * prints its inheritance tree in. Cyclic input terminates rather than looping. + */ + fun inheritanceChain(key: String): List { + val chain = mutableListOf() + val seen = mutableSetOf() + var current: String? = key + while (current != null && seen.add(current)) { + chain += current + current = superclassByKey[current] + } + return chain.reversed() + } + + /** Superclass chain excluding [key] itself, nearest ancestor first. */ + fun superclassChain(key: String): List = inheritanceChain(key).dropLast(1).reversed() + + /** + * Every interface reachable from [key] through superclasses and interface extension. Backs + * both "All Implemented Interfaces" (for a class) and "All Superinterfaces" (for an interface). + */ + fun allSuperinterfaces(key: String): List = allSuperinterfacesByKey[key].orEmpty() + + fun directKnownSubclasses(key: String): List = directSubclassesByKey[key].orEmpty() + + /** Interfaces that extend [key], directly or transitively. */ + fun allKnownSubinterfaces(key: String): List = subinterfacesByKey[key].orEmpty() + + /** Classes, enums and objects that implement [key], directly or transitively. */ + fun allKnownImplementingClasses(key: String): List = implementorsByKey[key].orEmpty() + + /** True when [key] is a `Throwable` subtype, which javadoc tables separately. */ + fun isException(key: String): Boolean = key in exceptionKeys + + /** The enclosing type of a nested type, or null for a top-level one. */ + fun enclosingTypeOf(type: JdType): JdType? { + if (!type.classNames.contains('.')) return null + val outer = type.classNames.substringBeforeLast('.') + val outerKey = if (type.packageName.isBlank()) outer else "${type.packageName}.$outer" + return byKey[outerKey] + } +} diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocPaths.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocPaths.kt new file mode 100644 index 00000000..cf66b403 --- /dev/null +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocPaths.kt @@ -0,0 +1,119 @@ +package org.appdevforall.dokka.kdoc2json.javadoc + +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.JavaClassReference +import org.jetbrains.dokka.links.RecursiveType +import org.jetbrains.dokka.links.StarProjection +import org.jetbrains.dokka.links.TypeConstructor +import org.jetbrains.dokka.links.TypeParam +import org.jetbrains.dokka.links.TypeReference +import org.jetbrains.dokka.links.Vararg + +/** + * Reproduces javadoc's on-disk layout and anchor scheme, with `.json` in place of `.html`. + * + * javadoc lays its `api/` tree out as: + * + * ``` + * //.html (module dir only for a modular run) + * //package-summary.html + * /module-summary.html + * ``` + * + * and links between those pages relatively (`../lang/Object.html`), which is what makes the tree + * self-contained when it is served from an arbitrary prefix. [relativeUrl] does the same thing. + * + * @param useModuleDirs whether page paths carry a leading `/` segment. Mirrors javadoc's + * own split: a modular run gets module directories, a non-modular one puts packages at the root. + */ +class JavadocPaths(private val useModuleDirs: Boolean) { + + companion object { + const val EXTENSION = "json" + const val PACKAGE_SUMMARY = "package-summary" + const val MODULE_SUMMARY = "module-summary" + + /** Dokka models a Java array as a one-argument `kotlin.Array` type constructor. */ + private val ARRAY_FQNS = setOf("kotlin.Array", "java.lang.Array") + } + + private fun prefix(moduleName: String?): String = + if (useModuleDirs && !moduleName.isNullOrBlank()) "$moduleName/" else "" + + private fun packageDir(packageName: String?): String = + if (packageName.isNullOrBlank()) "" else packageName.replace('.', '/') + "/" + + /** `java.base/java/util/Map.Entry.json`. Nested types keep their dotted name, as javadoc does. */ + fun classFile(packageName: String?, classNames: String, moduleName: String?): String = + "${prefix(moduleName)}${packageDir(packageName)}$classNames.$EXTENSION" + + fun packageFile(packageName: String?, moduleName: String?): String = + "${prefix(moduleName)}${packageDir(packageName)}$PACKAGE_SUMMARY.$EXTENSION" + + fun moduleFile(moduleName: String): String = + if (useModuleDirs) "$moduleName/$MODULE_SUMMARY.$EXTENSION" else "$MODULE_SUMMARY.$EXTENSION" + + /** + * A javadoc-style relative link from the page at [fromFile] to the page at [toFile], both + * given as output-dir-relative paths. Returns just the file name when they share a directory. + */ + fun relativeUrl(fromFile: String, toFile: String): String { + val fromDir = fromFile.split('/').dropLast(1) + val toParts = toFile.split('/') + val toDir = toParts.dropLast(1) + + var common = 0 + while (common < fromDir.size && common < toDir.size && fromDir[common] == toDir[common]) { + common++ + } + val up = List(fromDir.size - common) { ".." } + val down = toDir.drop(common) + toParts.last() + return (up + down).joinToString("/") + } + + /** + * javadoc's member anchor: the bare name for a field, and `name(erasedParamTypes)` for an + * executable -- with constructors spelled `(...)`, as javadoc has done since JDK 18. + * + * The parameter types are *erased* and fully qualified, so ` T[] toArray(T[] a)` anchors as + * `toArray(java.lang.Object[])`. That erasure is exactly what Dokka's DRI already carries, so + * the anchors here line up with the ones in a real javadoc build. + */ + fun memberAnchor(dri: DRI, isConstructor: Boolean): String { + val callable = dri.callable ?: return dri.classNames?.substringAfterLast('.') ?: "" + val name = if (isConstructor) "" else callable.name + if (isField(callable)) return name + val params = callable.params.joinToString(",") { erasedTypeName(it) } + return "$name($params)" + } + + /** + * A field's DRI carries no parameter list and is flagged `isProperty`; Dokka also emits + * zero-arg *methods* though, so `isProperty` is what actually separates the two. + */ + private fun isField(callable: Callable): Boolean = callable.isProperty + + /** Renders one DRI parameter type the way javadoc spells it inside a member anchor. */ + fun erasedTypeName(ref: TypeReference): String = when (ref) { + is TypeConstructor -> { + val fqn = ref.fullyQualifiedName + if (fqn in ARRAY_FQNS) { + // A raw `kotlin.Array` with no argument can't be rendered as `X[]`; fall back to + // Object[] rather than emitting a bare "[]". + val inner = ref.params.firstOrNull()?.let { erasedTypeName(it) } ?: "java.lang.Object" + "$inner[]" + } else { + fqn + } + } + is JavaClassReference -> ref.name + // A type variable erases to its leftmost bound, or to Object when unbounded. + is TypeParam -> ref.bounds.firstOrNull()?.let { erasedTypeName(it) } ?: "java.lang.Object" + is org.jetbrains.dokka.links.Nullable -> erasedTypeName(ref.wrapped) + is Vararg -> "${erasedTypeName(ref.elementType)}[]" + is StarProjection -> "java.lang.Object" + is RecursiveType -> "java.lang.Object" + else -> ref.toString() + } +} diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocRenderer.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocRenderer.kt new file mode 100644 index 00000000..8343e45a --- /dev/null +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocRenderer.kt @@ -0,0 +1,501 @@ +package org.appdevforall.dokka.kdoc2json.javadoc + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.serializer +import org.appdevforall.dokka.kdoc2json.JsonFilters +import org.appdevforall.dokka.kdoc2json.JsonPluginConfig +import org.appdevforall.dokka.kdoc2json.PluginLogger +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.pages.RootPageNode +import java.io.File + +/** + * Writes the Javadoc-mode output tree. + * + * The layout mirrors what the `javadoc` tool produces under its `api/` directory, with `.json` + * in place of `.html`: + * + * ``` + * index.json overview: every module (or package) in the run + * element-list javadoc's plain-text manifest of modules/packages + * allclasses-index.json every documented type + * allpackages-index.json every documented package + * deprecated-list.json deprecated elements, grouped by kind + * constant-values.json static final fields with constant values + * index-files/index-N.json the A-Z index, one file per letter + * /module-summary.json (module directories only for a multi-module run) + * //package-summary.json + * //.json + * ``` + * + * Only JSON (plus javadoc's own plain-text `element-list`) is written -- no HTML. Rendering the + * pages is the downstream template engine's job. + */ +class JavadocRenderer( + private val config: JsonPluginConfig, + private val logger: PluginLogger, + private val outputDir: File, + /** + * The modules of a multi-module Dokka run, as `name to relative output path`. Non-empty only + * in the aggregating run that Dokka performs after the per-module ones; empty otherwise. + */ + private val moduleReferences: List> = emptyList() +) { + + /** + * Javadoc-mode pages are written with `encodeDefaults = true` so every documented key is + * present on every page, even when empty -- a template can then test a field without also + * testing whether it exists. Callers who do want the empty keys gone still get that from + * `omitNulls`, which is applied afterwards, so the choice stays theirs rather than being + * baked into the serializer. No class discriminator is configured because none of the + * javadoc DTOs are polymorphic; their `kind` fields are ordinary data. + */ + private val json = Json { + prettyPrint = config.prettyPrint + encodeDefaults = true + } + + /** What a global index page needs about one member, without holding the whole class page. */ + private class MemberRecord( + val documentable: Documentable, + val label: String, + val kind: String, + val anchor: String, + val owner: JdType, + val deprecated: JdDeprecation? + ) + + /** A `static final` field carrying a compile-time constant, for `constant-values.json`. */ + private class ConstantRecord( + val owner: JdType, + val name: String, + val anchor: String, + val modifiers: List, + val typeDisplay: String, + val typeQualifiedName: String?, + val value: String + ) + + fun render(root: RootPageNode) { + val index = JavadocModelIndex.build(root, logger, config.sourceSetWhitelist) + if (index.types.isEmpty() && index.packages.isEmpty()) { + // Dokka's aggregating pass over a multi-module build sees only module references, no + // documentables -- the real pages were written by the per-module runs. Emit just the + // overview, which is the one page that pass is actually responsible for. + if (moduleReferences.isNotEmpty()) { + writeAggregateOverview() + return + } + logger.warn("javadoc-mode: no documented types or packages were found; nothing to write.") + return + } + val mapper = JavadocMapper(index, logger) + + val members = mutableListOf() + val constants = mutableListOf() + + writeClassPages(index, mapper, members, constants) + writePackagePages(index, mapper) + writeModulePages(index, mapper) + writeOverview(index, mapper) + writeAllClassesIndex(index, mapper) + writeAllPackagesIndex(index, mapper) + writeDeprecatedList(index, mapper, members) + writeConstantValues(index, constants) + writeAlphabeticalIndex(index, mapper, members) + writeElementList(index) + + logger.info( + "javadoc-mode: wrote ${index.types.size} type page(s), ${index.packages.size} package " + + "page(s), ${index.modules.size} module page(s) and the global index files." + ) + } + + /** + * The overview page for a multi-module run, listing each module's own output. + * + * Only the module list is available here; each module's descriptions and index files live in + * its own output directory, written by that module's run. + */ + private fun writeAggregateOverview() { + logger.info("javadoc-mode: multi-module aggregation pass; writing the overview only.") + write( + "index.${JavadocPaths.EXTENSION}", + JdOverviewPage( + modules = moduleReferences + .sortedBy { it.first } + .map { (name, path) -> + JdModuleSummary( + name = name, + url = "$path/${JavadocPaths.MODULE_SUMMARY}.${JavadocPaths.EXTENSION}" + ) + } + ) + ) + } + + // ------------------------------------------------------------ page passes + + private fun writeClassPages( + index: JavadocModelIndex, + mapper: JavadocMapper, + members: MutableList, + constants: MutableList + ) { + index.types.forEach { type -> + val page = runCatching { mapper.classPage(type) }.getOrElse { error -> + // One bad page must not cost every other page, matching JsonRenderer's own + // per-page resilience. + logger.warn("javadoc-mode: failed to build page for '${type.qualifiedName}': ${error.message}") + return@forEach + } + write(type.filePath, page) + harvest(index, type, page, members, constants) + } + } + + /** Collects the per-member facts the global index pages are built from. */ + private fun harvest( + index: JavadocModelIndex, + type: JdType, + page: JdClassPage, + members: MutableList, + constants: MutableList + ) { + val doc = type.documentable + + // Anchor -> declaration, first occurrence winning. `doc.functions` can also carry members + // Dokka copied down from a supertype; the page only lists the declared ones, so matching + // by anchor and keeping the first keeps the declaration rather than the inherited copy. + fun byAnchor(items: List, isConstructor: Boolean): Map { + val result = LinkedHashMap() + items.forEach { result.putIfAbsent(index.paths.memberAnchor(it.dri, isConstructor), it) } + return result + } + + val propertyByName = doc.properties.associateBy { it.name } + val functionByAnchor = byAnchor(doc.functions, isConstructor = false) + val constructorByAnchor = byAnchor( + (doc as? org.jetbrains.dokka.model.WithConstructors)?.constructors.orEmpty(), + isConstructor = true + ) + val enumEntryByName = + (doc as? org.jetbrains.dokka.model.DEnum)?.entries?.associateBy { it.name }.orEmpty() + + fun recordField(field: JdField, kind: String, source: Documentable?) { + if (source != null) { + members += MemberRecord(source, field.name, kind, field.anchor, type, field.deprecated) + } + val value = field.constantValue + if (value != null && "static" in field.modifiers && "final" in field.modifiers) { + constants += ConstantRecord( + owner = type, + name = field.name, + anchor = field.anchor, + modifiers = field.modifiers, + typeDisplay = field.type.display, + typeQualifiedName = field.type.qualifiedName, + value = value + ) + } + } + + page.fields.forEach { recordField(it, "field", propertyByName[it.name]) } + page.enumConstants.forEach { recordField(it, "enumConstant", enumEntryByName[it.name]) } + (page.methods + page.annotationElements).forEach { executable -> + functionByAnchor[executable.anchor]?.let { + members += MemberRecord( + it, executable.name, executable.kind, executable.anchor, type, executable.deprecated + ) + } + } + page.constructors.forEach { executable -> + constructorByAnchor[executable.anchor]?.let { + members += MemberRecord( + it, executable.name, "constructor", executable.anchor, type, executable.deprecated + ) + } + } + } + + private fun writePackagePages(index: JavadocModelIndex, mapper: JavadocMapper) { + val typesByPackage = index.types.groupBy { it.packageName } + index.packages.forEach { pkg -> + write(pkg.filePath, mapper.packagePage(pkg, typesByPackage[pkg.name].orEmpty())) + } + } + + private fun writeModulePages(index: JavadocModelIndex, mapper: JavadocMapper) { + index.modules.forEach { module -> + val packages = index.packages.filter { it.moduleName == module.name } + write(module.filePath, mapper.modulePage(module, packages)) + } + } + + private fun writeOverview(index: JavadocModelIndex, mapper: JavadocMapper) { + val path = "index.${JavadocPaths.EXTENSION}" + val scope = mapper.scope(path) + write( + path, + JdOverviewPage( + title = index.modules.singleOrNull()?.name, + modules = index.modules.map { mapper.moduleSummary(it, scope) }, + packages = index.packages.map { mapper.packageSummary(it, scope) } + ) + ) + } + + private fun writeAllClassesIndex(index: JavadocModelIndex, mapper: JavadocMapper) { + val path = "allclasses-index.${JavadocPaths.EXTENSION}" + val scope = mapper.scope(path) + write( + path, + JdAllClassesIndex( + types = index.types + .map { mapper.typeSummary(it, scope) } + .sortedWith(compareBy({ it.name.lowercase() }, { it.qualifiedName })) + ) + ) + } + + private fun writeAllPackagesIndex(index: JavadocModelIndex, mapper: JavadocMapper) { + val path = "allpackages-index.${JavadocPaths.EXTENSION}" + val scope = mapper.scope(path) + write(path, JdAllPackagesIndex(packages = index.packages.map { mapper.packageSummary(it, scope) })) + } + + private fun writeDeprecatedList( + index: JavadocModelIndex, + mapper: JavadocMapper, + members: List + ) { + val path = "deprecated-list.${JavadocPaths.EXTENSION}" + val scope = mapper.scope(path) + val sections = linkedMapOf>() + + fun add(section: String, entry: JdDeprecatedEntry) { + sections.getOrPut(section) { mutableListOf() } += entry + } + + index.types.forEach { type -> + val summary = mapper.typeSummary(type, scope) + val deprecation = summary.deprecated ?: return@forEach + // javadoc gives exceptions, interfaces, enums and annotations their own sections. + add( + when (summary.kind) { + "interface" -> "interfaces" + "enum" -> "enums" + "annotation" -> "annotationTypes" + "exception" -> "exceptions" + else -> "classes" + }, + JdDeprecatedEntry( + element = type.qualifiedName, + kind = summary.kind, + url = scope.url(type.filePath), + comment = deprecation.comment, + forRemoval = deprecation.forRemoval, + since = deprecation.since + ) + ) + } + + members.forEach { member -> + if (member.deprecated == null) return@forEach + // Re-rendered against this page rather than reusing the class page's copy, whose + // links are relative to the class page. + val deprecation = mapper.deprecationFor(member.documentable, scope) ?: return@forEach + add( + when (member.kind) { + "constructor" -> "constructors" + "field" -> "fields" + "enumConstant" -> "enumConstants" + "annotationElement" -> "annotationElements" + else -> "methods" + }, + JdDeprecatedEntry( + element = "${member.owner.qualifiedName}.${member.anchor}", + kind = member.kind, + url = scope.url(member.owner.filePath, member.anchor), + comment = deprecation.comment, + forRemoval = deprecation.forRemoval, + since = deprecation.since + ) + ) + } + + write( + path, + JdDeprecatedList(sections = sections.mapValues { (_, entries) -> entries.sortedBy { it.element } }) + ) + } + + private fun writeConstantValues(index: JavadocModelIndex, constants: List) { + val path = "constant-values.${JavadocPaths.EXTENSION}" + val paths = index.paths + val byPackage = constants + .groupBy { it.owner.packageName } + .toSortedMap() + .mapValues { (_, records) -> + records.groupBy { it.owner } + .toList() + .sortedBy { it.first.qualifiedName } + .map { (owner, fields) -> + JdConstantsForType( + qualifiedName = owner.qualifiedName, + url = paths.relativeUrl(path, owner.filePath), + fields = fields.sortedBy { it.name }.map { record -> + JdConstantField( + name = record.name, + modifiers = record.modifiers, + type = JdTypeRef( + display = record.typeDisplay, + qualifiedName = record.typeQualifiedName, + url = record.typeQualifiedName + ?.let { index.typeForKey(it) } + ?.let { paths.relativeUrl(path, it.filePath) } + ), + value = record.value, + url = "${paths.relativeUrl(path, owner.filePath)}#${record.anchor}" + ) + } + ) + } + } + write(path, JdConstantValues(packages = byPackage)) + } + + /** + * javadoc's A-Z index, split one file per letter under `index-files/`. + * + * Every documented element -- module, package, type, field, constructor, method -- gets an + * entry, which is what makes the index usable as a search backing store downstream. + */ + private fun writeAlphabeticalIndex( + index: JavadocModelIndex, + mapper: JavadocMapper, + members: List + ) { + class PendingEntry( + val label: String, + val kind: String, + val filePath: String, + val anchor: String?, + val containingElement: String?, + val documentable: Documentable?, + val deprecated: Boolean + ) + + val pending = mutableListOf() + + index.modules.forEach { + pending += PendingEntry(it.name, "module", it.filePath, null, null, it.documentables.firstOrNull(), false) + } + index.packages.forEach { + pending += PendingEntry(it.name, "package", it.filePath, null, it.moduleName, it.documentables.firstOrNull(), false) + } + index.types.forEach { type -> + pending += PendingEntry( + label = type.classNames, + kind = if (index.isException(type.key)) "exception" else type.kind, + filePath = type.filePath, + anchor = null, + containingElement = type.packageName, + documentable = type.documentable, + deprecated = false + ) + } + members.forEach { member -> + pending += PendingEntry( + label = if (member.kind == "constructor") member.owner.simpleName else member.label, + kind = member.kind, + filePath = member.owner.filePath, + anchor = member.anchor, + containingElement = member.owner.qualifiedName, + documentable = member.documentable, + deprecated = member.deprecated != null + ) + } + + val grouped = pending + .sortedWith(compareBy({ it.label.lowercase() }, { it.containingElement.orEmpty() }, { it.kind })) + .groupBy { groupLabelFor(it.label) } + + // Symbols sort ahead of letters, which is also where javadoc puts them. + val letters = grouped.keys.sortedWith(compareBy({ it != SYMBOL_GROUP }, { it })) + + letters.forEachIndexed { position, letter -> + val number = position + 1 + val path = "index-files/index-$number.${JavadocPaths.EXTENSION}" + val scope = mapper.scope(path) + val entries = grouped.getValue(letter).map { entry -> + JdIndexEntry( + label = entry.label, + kind = entry.kind, + url = scope.url(entry.filePath, entry.anchor), + containingElement = entry.containingElement, + firstSentence = entry.documentable + ?.let { JavadocDocs.firstSentence(scope.docs.bundleFor(it).description) }, + deprecated = entry.deprecated + ) + } + write(path, JdIndexPage(letter = letter, index = number, letters = letters, entries = entries)) + } + } + + /** + * javadoc's plain-text manifest of what this documentation covers -- the file downstream + * tooling reads to resolve external links into this output. Modular runs list each module + * with `module:` followed by its packages; non-modular runs list packages alone. + */ + private fun writeElementList(index: JavadocModelIndex) { + val content = buildString { + if (index.modules.size > 1) { + index.modules.forEach { module -> + appendLine("module:${module.name}") + index.packages.filter { it.moduleName == module.name } + .map { it.name } + .sorted() + .forEach { appendLine(it) } + } + // A package Dokka surfaced without attaching it to a module would otherwise be + // absent from the manifest entirely; list it unqualified rather than lose it. + index.packages.filter { it.moduleName == null } + .map { it.name } + .sorted() + .forEach { appendLine(it) } + } else { + index.packages.map { it.name }.sorted().forEach { appendLine(it) } + } + } + val file = File(outputDir, "element-list") + file.parentFile?.mkdirs() + file.writeText(content) + } + + // ------------------------------------------------------------------ i/o + + private inline fun write(relativePath: String, value: T) { + try { + val element: JsonElement = json.encodeToJsonElement(serializer(), value) + val filtered = JsonFilters.filterJson(element, config.omitFields, config.omitNulls) + val file = File(outputDir, relativePath) + file.parentFile?.mkdirs() + file.writeText(json.encodeToString(JsonElement.serializer(), filtered)) + logger.debug("javadoc-mode: wrote $relativePath") + } catch (e: Exception) { + logger.warn("javadoc-mode: failed to write $relativePath: ${e.message}") + } + } + + private companion object { + const val SYMBOL_GROUP = "SYMBOLS" + + /** The A-Z bucket a label belongs to; anything not starting with a letter is a symbol. */ + fun groupLabelFor(label: String): String { + val first = label.firstOrNull() ?: return SYMBOL_GROUP + return if (first.isLetter()) first.uppercaseChar().toString() else SYMBOL_GROUP + } + } +} diff --git a/Dokka-plugin-kdoc2json/tests/lib.sh b/Dokka-plugin-kdoc2json/tests/lib.sh index 44a881f3..66ce6884 100755 --- a/Dokka-plugin-kdoc2json/tests/lib.sh +++ b/Dokka-plugin-kdoc2json/tests/lib.sh @@ -10,6 +10,10 @@ ROOT_DIR="$(cd "$TESTS_DIR/.." && pwd)" PLUGIN_DIR="$ROOT_DIR/kdoc-to-json" EXAMPLE_DIR="$ROOT_DIR/examples/example-data-processor" OUTPUT_DIR="$EXAMPLE_DIR/build/dokka/html" +# Javadoc mode mirrors the output of the `javadoc` tool, so it is exercised against a +# Java-only example rather than the Kotlin one every other test uses. +JAVA_EXAMPLE_DIR="$ROOT_DIR/examples/example-java-library" +JAVA_OUTPUT_DIR="$JAVA_EXAMPLE_DIR/build/dokka/html" TMP_DIR="$(mktemp -d /tmp/kdoc2json_test.XXXXXX)" trap 'rm -rf "$TMP_DIR"' EXIT @@ -58,6 +62,25 @@ run_dokka() { LAST_GRADLE_LOG="$gradle_log" } +# run_dokka_java '' is run_dokka against examples/example-java-library, the +# Java-source example Javadoc mode is tested with. Afterwards $JAVA_OUTPUT_DIR reflects only +# this run. Aborts the whole script if the Dokka build itself fails, like run_dokka. +run_dokka_java() { + local config_json="$1" + local config_file="$TMP_DIR/config-java-$RANDOM.json" + local gradle_log="$TMP_DIR/gradle-java-$RANDOM.log" + printf '%s' "$config_json" >"$config_file" + + rm -rf "$JAVA_EXAMPLE_DIR/build/dokka" + + if ! (cd "$JAVA_EXAMPLE_DIR" && KDOC2JSON_TEST_CONFIG="$config_file" ./gradlew --console=plain dokkaGenerate) >"$gradle_log" 2>&1; then + echo "FATAL: dokkaGenerate failed for config: $config_json" >&2 + cat "$gradle_log" >&2 + exit 1 + fi + LAST_GRADLE_LOG="$gradle_log" +} + # run_dokka_expect_failure '' is run_dokka's counterpart for tests # that assert the build SHOULD fail (e.g. a genuinely malformed config, or a # classDiscriminator collision). Never aborts the script on a Dokka failure -- @@ -171,6 +194,32 @@ assert_no_local_html_urls() { fi } +# assert_json evaluates a Python expression +# against the parsed JSON document (bound to `d`) and compares its str() to . Lets a +# test assert on structure -- a field's value, a list's contents -- instead of grepping for a +# substring that might match somewhere unrelated in the file. +assert_json() { + local path="$1" expr="$2" expected="$3" desc="$4" + local actual + actual=$(python3 -c " +import json, sys +try: + d = json.load(open(sys.argv[1])) +except Exception as e: + print('' % e) + sys.exit(0) +try: + print($expr) +except Exception as e: + print('' % e) +" "$path" 2>/dev/null) + if [[ "$actual" == "$expected" ]]; then + pass "$desc" + else + fail "$desc (expected '$expected', got '$actual')" + fi +} + assert_gt() { local actual="$1" threshold="$2" desc="$3" if [[ "$actual" -gt "$threshold" ]]; then diff --git a/Dokka-plugin-kdoc2json/tests/test_javadoc_mode.sh b/Dokka-plugin-kdoc2json/tests/test_javadoc_mode.sh new file mode 100755 index 00000000..c52ee707 --- /dev/null +++ b/Dokka-plugin-kdoc2json/tests/test_javadoc_mode.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# Exercises the "javadoc-mode" config option against examples/example-java-library. +# +# Javadoc mode's contract is that the output *mirrors the javadoc tool's own api/ tree* -- both +# where files land and what each page contains -- so these assertions are written against real +# javadoc behaviour: package directories rather than Dokka's `com.example.shapes/-rectangle/` +# layout, `(double,double)` member anchors, an inheritance closure, inherited-member groups, +# and the global index files (allclasses-index, deprecated-list, constant-values, index-files, +# element-list). +set -uo pipefail + +TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$TESTS_DIR/lib.sh" + +publish_plugin + +JD_ON='{"logLevel":"debug","javadoc-mode":true,"prettyPrint":true}' +JD_OFF='{"logLevel":"debug","prettyPrint":true}' + +echo "==> javadoc-mode disabled: output keeps Dokka's own layout" +run_dokka_java "$JD_OFF" +assert_file_exists "$JAVA_OUTPUT_DIR/com.example.shapes/-rectangle/index.json" \ + "default mode still writes Dokka-shaped pages" +assert_file_not_exists "$JAVA_OUTPUT_DIR/com/example/shapes/Rectangle.json" \ + "default mode writes no javadoc-shaped pages" +assert_file_not_exists "$JAVA_OUTPUT_DIR/allclasses-index.json" \ + "default mode writes no javadoc index files" + +echo +echo "==> javadoc-mode enabled: layout mirrors javadoc's api/ tree" +run_dokka_java "$JD_ON" + +# Package-as-directory layout, and a nested type kept as Outer.Nested in the enclosing package. +assert_file_exists "$JAVA_OUTPUT_DIR/com/example/shapes/Rectangle.json" \ + "class page lands at /.json" +assert_file_exists "$JAVA_OUTPUT_DIR/com/example/shapes/Rectangle.Builder.json" \ + "nested type keeps its dotted name, as javadoc does" +assert_file_exists "$JAVA_OUTPUT_DIR/com/example/shapes/package-summary.json" \ + "package page lands at package-summary.json" +assert_file_exists "$JAVA_OUTPUT_DIR/com/example/shapes/spi/package-summary.json" \ + "a second package gets its own package-summary.json" +assert_file_not_exists "$JAVA_OUTPUT_DIR/com.example.shapes/-rectangle/index.json" \ + "Dokka-shaped pages are not written in javadoc mode" + +# The global index files javadoc emits at the root of api/. +for f in index.json allclasses-index.json allpackages-index.json deprecated-list.json \ + constant-values.json element-list index-files/index-1.json; do + assert_file_exists "$JAVA_OUTPUT_DIR/$f" "javadoc index file $f is written" +done + +# No HTML is ever produced -- rendering is the downstream template engine's job. +html_count=$(find "$JAVA_OUTPUT_DIR" -name '*.html' 2>/dev/null | wc -l | tr -d ' ') +assert_eq "$html_count" "0" "javadoc mode writes no HTML files" + +echo +echo "==> class page content mirrors a javadoc class page" +RECT="$JAVA_OUTPUT_DIR/com/example/shapes/Rectangle.json" +assert_json "$RECT" "d['qualifiedName']" "com.example.shapes.Rectangle" "qualified name" +assert_json "$RECT" "d['signature']" "public class Rectangle extends AbstractShape" \ + "type signature reads as javadoc prints it" +assert_json "$RECT" "d['superclass']['qualifiedName']" "com.example.shapes.AbstractShape" "superclass" +assert_json "$RECT" "[t['qualifiedName'] for t in d['inheritance']]" \ + "['com.example.shapes.AbstractShape', 'com.example.shapes.Rectangle']" \ + "inheritance tree runs ancestor-first, ending at this type" +assert_json "$RECT" "[t['qualifiedName'] for t in d['allImplementedInterfaces']]" \ + "['com.example.shapes.Shape']" \ + "All Implemented Interfaces closes over the superclass chain" +assert_json "$RECT" "[t['qualifiedName'] for t in d['directKnownSubclasses']]" \ + "['com.example.shapes.Square']" "Direct Known Subclasses" +assert_json "$RECT" "[n['qualifiedName'] for n in d['nestedTypes']]" \ + "['com.example.shapes.Rectangle.Builder']" "nested type summary" + +# javadoc has used (...) for constructor anchors since JDK 18, with erased parameter types. +assert_json "$RECT" "sorted(c['anchor'] for c in d['constructors'])" \ + "['()', '(double,double)']" "constructor anchors use javadoc's (...) form" + +# A private field with a public getter is a *method* in javadoc, not a field. Dokka merges the +# pair into a Kotlin-style property, so this is the regression guard for unfolding it back. +assert_json "$RECT" "sorted(m['name'] for m in d['methods'])" \ + "['area', 'getHeight', 'getWidth', 'perimeter']" \ + "Java accessors stay methods rather than becoming synthetic properties" +assert_json "$RECT" "sorted(f['name'] for f in d['fields'])" \ + "['EMPTY_LABEL', 'SIDE_COUNT']" "only real fields are listed as fields" + +# Overrides / Specified by, derived from erased signatures. +assert_json "$RECT" "[s['declaringType']['qualifiedName'] for m in d['methods'] if m['name']=='area' for s in m['specifiedBy']]" \ + "['com.example.shapes.Shape']" "Specified by points at the declaring interface" +# javadoc groups inherited members per declaring type, interfaces included -- Rectangle inherits +# scaled() from the Shape interface as well as the AbstractShape methods. +assert_json "$RECT" "sorted(g['declaringType']['qualifiedName'] for g in d['inheritedMethods'])" \ + "['com.example.shapes.AbstractShape', 'com.example.shapes.Shape']" \ + "inherited methods are grouped by declaring type, classes and interfaces alike" + +# Deprecation carries javadoc's since/forRemoval, not just the comment. +assert_json "$RECT" "[ (m['deprecated']['forRemoval'], m['deprecated']['since']) for m in d['methods'] if m['name']=='perimeter' ]" \ + "[(True, '2.0')]" "@Deprecated(since, forRemoval) is captured" + +echo +echo "==> interface, enum, annotation and exception pages" +SHAPE="$JAVA_OUTPUT_DIR/com/example/shapes/Shape.json" +assert_json "$SHAPE" "d['kind']" "interface" "interface kind" +assert_json "$SHAPE" "[m['signature'] for m in d['methods'] if m['name']=='scaled']" \ + "['public default Shape scaled(double factor) throws IllegalArgumentException']" \ + "a non-abstract interface method is recovered as 'default'" +assert_json "$SHAPE" "d['typeParameters'][0]['description'] is not None" "True" \ + "@param is attached to the type parameter" +assert_json "$SHAPE" "d['since']" "['1.0']" "@since is plain text, not a wrapped paragraph" +assert_json "$SHAPE" "d['authors']" "['Docs Pipeline']" "@author is captured" +assert_json "$SHAPE" "d['isFunctionalInterface']" "False" \ + "an interface with two abstract methods is not functional" + +FACTORY="$JAVA_OUTPUT_DIR/com/example/shapes/spi/ShapeFactory.json" +assert_json "$FACTORY" "d['isFunctionalInterface']" "True" \ + "an interface with one abstract method is functional" +assert_json "$FACTORY" "[e['type']['qualifiedName'] for e in d['methods'][0]['exceptions']]" \ + "['java.text.ParseException']" "@throws is captured with its resolved type" + +CORNER="$JAVA_OUTPUT_DIR/com/example/shapes/Corner.json" +assert_json "$CORNER" "d['kind']" "enum" "enum kind" +assert_json "$CORNER" "[e['name'] for e in d['enumConstants']]" \ + "['TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', 'BOTTOM_RIGHT']" \ + "enum constants keep declaration order" + +MEASURED="$JAVA_OUTPUT_DIR/com/example/shapes/Measured.json" +assert_json "$MEASURED" "d['kind']" "annotation" "annotation kind" +assert_json "$MEASURED" "d['signature']" "public @interface Measured" "annotation signature" +assert_json "$MEASURED" "sorted(e['name'] for e in d['annotationElements'])" \ + "['tolerance', 'verifiedBy']" "annotation elements are listed" + +EXC="$JAVA_OUTPUT_DIR/com/example/shapes/ShapeException.json" +assert_json "$EXC" "d['kind']" "exception" \ + "a Throwable subtype is tabled as an exception, as javadoc does" + +echo +echo "==> package, module and global index pages" +PKG="$JAVA_OUTPUT_DIR/com/example/shapes/package-summary.json" +assert_json "$PKG" "[t['name'] for t in d['interfaces']]" "['Shape']" "package interface table" +assert_json "$PKG" "[t['name'] for t in d['exceptions']]" "['ShapeException']" "package exception table" +assert_json "$PKG" "[t['name'] for t in d['annotationTypes']]" "['Measured']" "package annotation table" +# Shape, AbstractShape, Rectangle, Rectangle.Builder, Square, Corner, Measured, ShapeException. +assert_json "$PKG" "len(d['allTypes'])" "8" "allTypes lists every type in the package" + +ALL="$JAVA_OUTPUT_DIR/allclasses-index.json" +# The eight in com.example.shapes plus ShapeFactory in com.example.shapes.spi. +assert_json "$ALL" "len(d['types'])" "9" "allclasses-index covers every documented type" +assert_json "$ALL" "[t['url'] for t in d['types'] if t['name']=='Shape']" \ + "['com/example/shapes/Shape.json']" "index links are relative to the index page" + +DEP="$JAVA_OUTPUT_DIR/deprecated-list.json" +assert_json "$DEP" "[e['element'] for e in d['sections']['methods']]" \ + "['com.example.shapes.Rectangle.perimeter()']" "deprecated methods are listed" +# Regression guard: the comment is a rendered fragment and must be re-rendered relative to the +# page it lands on, not lifted verbatim off the class page (where the href is just +# "Rectangle.json#getWidth()"). Asserted on the parsed value, since prettyPrint escapes the +# quotes in the raw file. +assert_json "$DEP" "'href=\"com/example/shapes/Rectangle.json#getWidth()\"' in d['sections']['methods'][0]['comment']" \ + "True" "links inside a deprecation comment resolve from the index page" + +CONST="$JAVA_OUTPUT_DIR/constant-values.json" +assert_json "$CONST" "sorted(f['value'] for t in d['packages']['com.example.shapes'] for f in t['fields'])" \ + "['\"empty\"', '4', '64']" "constant values are unwrapped to their literals" + +assert_contains "$JAVA_OUTPUT_DIR/element-list" "com.example.shapes.spi" \ + "element-list names every documented package" + +IDX="$JAVA_OUTPUT_DIR/index-files/index-1.json" +assert_json "$IDX" "d['entries'][0]['url'].startswith('../')" "True" \ + "index-files entries link back out of their own directory" +assert_json "$IDX" "len(d['letters']) > 1" "True" "index pages carry the full letter list" + +echo +echo "==> javadoc mode honours the shared output options" +run_dokka_java '{"logLevel":"debug","javadoc-mode":true,"omitNulls":true,"omitFields":["firstSentence"]}' +assert_not_contains "$JAVA_OUTPUT_DIR/com/example/shapes/Rectangle.json" '"firstSentence"' \ + "omitFields strips keys from javadoc-mode pages" +assert_not_contains "$JAVA_OUTPUT_DIR/com/example/shapes/Rectangle.json" '"seeAlso":[]' \ + "omitNulls strips empty values from javadoc-mode pages" +assert_eq "$(line_count "$JAVA_OUTPUT_DIR/com/example/shapes/Rectangle.json")" "1" \ + "prettyPrint off yields compact single-line JSON" + +summarize_and_exit From aa6679bfde1f761effc5744f95405934262a5123 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Wed, 26 Aug 2026 14:11:03 -0500 Subject: [PATCH 02/14] ADFA-5296: Generate the JDK API docs as javadoc-shaped JSON Adds scripts/java/, which reproduces the JDK's own api/ tree (the docs in SourceDocs/JavaDocs/html/api) as JSON, and the JPMS support in the plugin that the module-per-directory layout depends on. Measured on JDK 17: a full run takes ~90 seconds and writes 4,988 JSON files. Against the official docs it matches exactly on structure -- 60/60 modules, 224/224 packages, 4,672/4,672 types, none missing and none extra -- and 4,305 of those 4,672 types (92%) match javadoc's member anchors exactly. The 367 that differ are understood and documented in README section 11; neither direction is a missing page. scripts/java: stage_jdk_sources.py unpacks lib/src.zip and keeps only what javadoc documents. javadoc's rule turns out to be exact: a package appears in api/ iff its module exports it unqualified. For java.base that is 53 exports and 53 documented packages, nothing left over either side. jdk-docs/ a Dokka project registering each module directory as a source root; nothing is compiled, only analysed build-jdk-json-docs.sh driver for the three steps compare_with_javadoc.py parity checker: modules, packages, types, and (with --members) the member anchors of every type Plugin changes: - JPMS modules are read from module-info.java, which Dokka's model does not carry at all. That supplies the //.json layout, the module page's requires/exports/opens/uses/provides, and its description. A source root holding a module-info.java *is* a module root, so nothing else is affected. Source roots arrive as directories or as expanded file lists depending on how Dokka was configured; both are handled. - {@inheritDoc} is resolved by the plugin instead of by Dokka. Dokka's own resolver (InheritDocTagResolver.resolveThrowsTag -> PsiElementToHtmlConverter.toInheritDocHtml) recurses without bound on much of the JDK and dies with a StackOverflowError; a larger stack only buys time (-Xss64m fails after 42s, -Xss512m after 3m28s). The staging script rewrites the tag to an inert marker and the mapper resolves it by walking the same supertype chain "Overrides:" and "Specified by:" come from. All 3,214 occurrences across the JDK still resolve, and a *missing* @param/@return/@throws is now inherited too, as javadoc does. - Member anchors erase type arguments. Dokka builds a Java DRI from the PSI canonical text, which keeps them, so addAll(java.util.Collection) has to become addAll(java.util.Collection) to match a real javadoc build. This alone took member parity from 80% to 92%. Tests: test_javadoc_mode.sh grows to 65 assertions, covering module-info parsing via a module-info.java added to examples/example-java-library. Full suite 15/15 scripts pass. Co-Authored-By: Claude Opus 5 --- Dokka-plugin-kdoc2json/README.md | 152 +++++++++-- .../src/main/java/module-info.java | 18 ++ .../src/main/kotlin/JsonRenderer.kt | 3 +- .../src/main/kotlin/javadoc/JavadocDtos.kt | 44 ++- .../src/main/kotlin/javadoc/JavadocMapper.kt | 252 +++++++++++++++++- .../main/kotlin/javadoc/JavadocModelIndex.kt | 81 +++++- .../src/main/kotlin/javadoc/JavadocPaths.kt | 26 +- .../main/kotlin/javadoc/JavadocRenderer.kt | 29 +- .../src/main/kotlin/javadoc/JpmsModuleInfo.kt | 184 +++++++++++++ .../scripts/java/build-jdk-json-docs.sh | 110 ++++++++ .../scripts/java/compare_with_javadoc.py | 215 +++++++++++++++ .../scripts/java/jdk-docs/build.gradle.kts | 86 ++++++ .../scripts/java/jdk-docs/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + .../scripts/java/jdk-docs/gradlew | 248 +++++++++++++++++ .../scripts/java/jdk-docs/gradlew.bat | 82 ++++++ .../scripts/java/jdk-docs/settings.gradle.kts | 1 + .../scripts/java/stage_jdk_sources.py | 194 ++++++++++++++ .../tests/test_javadoc_mode.sh | 18 ++ 20 files changed, 1686 insertions(+), 69 deletions(-) create mode 100644 Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/module-info.java create mode 100644 Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JpmsModuleInfo.kt create mode 100755 Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh create mode 100755 Dokka-plugin-kdoc2json/scripts/java/compare_with_javadoc.py create mode 100644 Dokka-plugin-kdoc2json/scripts/java/jdk-docs/build.gradle.kts create mode 100644 Dokka-plugin-kdoc2json/scripts/java/jdk-docs/gradle.properties create mode 100644 Dokka-plugin-kdoc2json/scripts/java/jdk-docs/gradle/wrapper/gradle-wrapper.jar create mode 100644 Dokka-plugin-kdoc2json/scripts/java/jdk-docs/gradle/wrapper/gradle-wrapper.properties create mode 100755 Dokka-plugin-kdoc2json/scripts/java/jdk-docs/gradlew create mode 100644 Dokka-plugin-kdoc2json/scripts/java/jdk-docs/gradlew.bat create mode 100644 Dokka-plugin-kdoc2json/scripts/java/jdk-docs/settings.gradle.kts create mode 100755 Dokka-plugin-kdoc2json/scripts/java/stage_jdk_sources.py diff --git a/Dokka-plugin-kdoc2json/README.md b/Dokka-plugin-kdoc2json/README.md index 85f7a2ef..03d52e71 100644 --- a/Dokka-plugin-kdoc2json/README.md +++ b/Dokka-plugin-kdoc2json/README.md @@ -240,34 +240,36 @@ index-files/index-N.json the A-Z index, one file per letter //.json type page ``` -The leading `/` segment appears only when a single Dokka run genuinely contains more than -one module, matching javadoc's own split between modular and non-modular builds. A single-module -run also writes `module-summary.json` at the root: javadoc omits a module page entirely for a -non-modular build, but Dokka always has a module, and its documentation would otherwise be dropped. - -#### Multi-module builds, and what that means for JPMS - -Be aware of how Dokka structures a multi-module Gradle build, because it decides which of the two -layouts above you get: - -- **One Dokka run, one module** (all sources in a single project) -- packages are written flat at - the output root, and all the global index files cover the whole run. This is the layout the - tests exercise. -- **One Dokka run per module** (a Gradle subproject each) -- Dokka runs the renderer separately - for each module, into that module's own output directory, and then makes an aggregating pass. - Each per-module run therefore sees one module, writes its packages flat inside its own - directory, and writes global index files *scoped to that module*. The aggregating pass sees only - module references, and writes just the overview `index.json` linking to each module. - - The net tree matches javadoc's `//...` shape, but the index files are per-module - rather than run-wide, and cross-module links are not resolved -- each run only knows its own - types. Merging those per-module indexes into run-wide ones is a downstream step this plugin does - not perform. - -Note also that **Dokka has no JPMS model**: a Dokka "module" is a build-level grouping. Reproducing -the JDK's own `java.base/`, `java.desktop/` … directories therefore requires the documentation -build to be organised with one Dokka module per JPMS module; it cannot be inferred from -`module-info.java`. +#### How modules are determined + +Dokka's own model has no notion of JPMS -- a Dokka "module" is a build-level grouping -- so +Javadoc mode reads `module-info.java` directly instead. Every configured source root is checked +for one; a root that has one *is* a JPMS module root, which makes this self-validating (an +ordinary `src/main/java` has no `module-info.java`, so a non-modular project is unaffected). + +From each descriptor the plugin takes the module name, its doc comment, and its `requires`, +`exports`, `opens`, `uses` and `provides` directives -- everything javadoc's module-summary page +is built from. A package is attributed to the module that declares it, falling back to whichever +module's source root the declaration's file sits under. + +The leading `/` segment appears when the run contains more than one module -- more than +one JPMS module if the sources are modular, otherwise more than one Dokka module. That mirrors +javadoc's own split between modular and non-modular builds. A single-module run also writes +`module-summary.json` at the root: javadoc omits a module page entirely for a non-modular build, +but the module's documentation would otherwise be dropped. + +To document a modular codebase, then, give Dokka **one source root per module directory** and let +the descriptors do the rest -- see `scripts/java/` for a worked example that does this for the +entire JDK. + +#### Multi-module Gradle builds + +Separately from JPMS, a *Gradle* multi-module build makes Dokka run the renderer once per +subproject into that subproject's own output directory, then make an aggregating pass. Each +per-module run therefore writes global index files scoped to its own module, and the aggregating +pass writes only the overview `index.json` linking to each. Merging those per-module indexes into +run-wide ones is a downstream step this plugin does not perform. Documenting a modular codebase +from a single Dokka run (as `scripts/java/` does) avoids this entirely. All links between pages are **relative to the page they appear on** (`../lang/Object.json`), as javadoc's are, so the tree can be served from any prefix. This includes links inside rendered doc @@ -319,7 +321,7 @@ missing *input*, not a gap in the mapping: | Limitation | Effect | | --- | --- | -| Dokka has no JPMS model | `JdModulePage.requires`/`uses`/`provides` are always empty, and a package's "exported to" is unavailable. A Dokka "module" is a build-level grouping, not a JPMS module. | +| Dokka has no JPMS model | Worked around by parsing `module-info.java` directly (see above), so `requires`/`exports`/`opens`/`uses`/`provides` and the module description *are* populated for modular sources. Without `module-info.java` in a source root those sections are empty. | | Dokka does not record annotation-element defaults | Annotation elements are reported as one `annotationElements` list rather than being split into javadoc's Required/Optional tables. `defaultValue` is populated only when Dokka does supply it. | | Dokka has no `record` class kind | Java records are documented as classes; `recordComponents` stays empty. | | Dokka merges a private field and its accessors into one property | Unfolded back into methods so `getWidth()` is a method and the private field is not documented, as javadoc has it. Note this means a *public* field that happens to have a same-named accessor pair is reported through its accessors. | @@ -336,3 +338,95 @@ itself rather than rewriting Dokka's. Pages are serialized with `encodeDefaults = true`, so every documented key is present on every page even when empty -- a template can test a field without also testing whether it exists. Enabling `omitNulls` strips the empty ones back out if you prefer that. + +--- + +## 11. Reproducing the JDK API Docs (`scripts/java`) + +`scripts/java/` builds the JDK's own API documentation as javadoc-shaped JSON -- the JSON +counterpart of the `api/` tree in `SourceDocs/JavaDocs/html/api`. + +```bash +# Document the JDK that JAVA_HOME points at (use a JDK 17 to match SourceDocs/JavaDocs) +scripts/java/build-jdk-json-docs.sh -j /path/to/jdk-17 -o /path/to/output/api + +# Quick check on two small modules instead of all 60 +scripts/java/build-jdk-json-docs.sh -j /path/to/jdk-17 -m java.sql,java.transaction.xa +``` + +### How it works + +1. **`stage_jdk_sources.py`** unpacks the JDK's `lib/src.zip` and keeps only what javadoc + documents. javadoc's rule turns out to be exact: a package appears in `api/` if and only if its + module `exports` it **unqualified**. For JDK 17's `java.base`, the 53 unqualified exports are + precisely the 53 documented packages, with nothing left over on either side. The script also + drops the modules the JDK's own docs build filters out (`jdk.internal.*`, `jdk.unsupported*`, + `jdk.random`), leaving 60 modules and 224 packages -- exactly what the official docs contain. + + Each module becomes its own directory in the staging tree, with its `module-info.java` copied + alongside. Everything left behind still resolves from the JDK on the analysis classpath. + +2. **`jdk-docs/`** is a Dokka project that registers each staged module directory as a source root + and runs the plugin in javadoc mode. Nothing is compiled -- Dokka only analyses. + +3. **`compare_with_javadoc.py`** checks the result against the official HTML, level by level: + + ```bash + python3 scripts/java/compare_with_javadoc.py /SourceDocs/JavaDocs/html/api + python3 scripts/java/compare_with_javadoc.py --members + ``` + + `--members` compares the member anchors of every type. That is the sharpest of the checks: + javadoc's anchor encodes a member's name and its erased parameter types, so a matching anchor + set means the two sides agree on the members, their signatures and their overloads -- not + merely on the page count. + +### Measured result (JDK 17) + +A full run takes **about 90 seconds** and writes **4,988 JSON files**. Against the official docs in +`SourceDocs/JavaDocs/html/api`: + +| Level | Result | +| --- | --- | +| modules | **60 / 60** — no missing, no extra | +| packages | **224 / 224** — no missing, no extra | +| types | **4,672 / 4,672** — no missing, no extra | +| member anchors | 4,305 / 4,672 types match *exactly* (92%) | + +The 367 types whose member sets differ do so for two understood reasons, neither of which is a +missing page: + +- **893 anchors we have that javadoc doesn't** (330 types). javadoc folds an override whose entire + doc comment is `{@inheritDoc}` — adding nothing of its own — into the superclass's "Methods + declared in…" list instead of giving it a detail section. `java.awt.Frame.setBackground` is a + typical case. We document them as the declared members they are, so this is extra data, not lost + data. +- **481 anchors javadoc has that we don't** (37 types). Where a class extends an *undocumented* + supertype (a package-private base like `java.awt.AttributeValue`), javadoc pulls that supertype's + members up and shows them as if declared. We only document what the source declares. + +### Two Dokka problems this works around + +Both were found running the JDK through it, and both are in Dokka rather than in this plugin: + +1. **Unbounded recursion in `{@inheritDoc}`.** Dokka's `InheritDocTagResolver.resolveThrowsTag` → + `PsiElementToHtmlConverter.toInheritDocHtml` recurses until the stack dies, reproducibly, on + much of the JDK (`java.io` and `java.util` among others). A bigger stack only buys time: + `-Xss64m` fails after 42 s, `-Xss512m` after 3m28s. `stage_jdk_sources.py` therefore rewrites + `{@inheritDoc}` to an inert marker before Dokka parses it, and the plugin resolves the marker + itself, walking the same supertype chain javadoc walks — so all 3,214 occurrences across the JDK + still resolve, and the plugin additionally inherits a *missing* `@param`/`@return`/`@throws` the + way javadoc does. Pass `--keep-inherit-doc` to re-check whether a newer Dokka has fixed this. +2. **Type arguments in DRI parameter types.** Dokka builds a Java DRI from the PSI type's canonical + text, which carries type arguments, so a naive anchor comes out as + `addAll(java.util.Collection)` where javadoc uses the erasure, + `addAll(java.util.Collection)`. `JavadocPaths.eraseGenerics` strips them while keeping array + brackets. This alone moved member-anchor parity from 80% to 92%. + +### Notes + +- Dokka generates in a *worker process*, not the Gradle daemon, so `org.gradle.jvmargs` does not + size it — `dokkaGeneratorIsolation` in `jdk-docs/build.gradle.kts` does. Override with + `-PdokkaWorkerHeap` / `-PdokkaWorkerStack` if needed. +- Use a JDK whose version matches the docs you are reproducing. Source and docs from different + update releases differ in small ways that are real, not bugs. diff --git a/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/module-info.java b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/module-info.java new file mode 100644 index 00000000..81be309d --- /dev/null +++ b/Dokka-plugin-kdoc2json/examples/example-java-library/src/main/java/module-info.java @@ -0,0 +1,18 @@ +/** + * Defines a small geometry library. + * + *

Present so the plugin's Javadoc mode has a real {@code module-info.java} to read: the module + * page's requires / exports / uses / provides sections come from here, not from Dokka's model.

+ * + * @uses com.example.shapes.spi.ShapeFactory + * @since 1.0 + */ +module com.example.shapes { + requires transitive java.logging; + requires static java.sql; + + exports com.example.shapes; + exports com.example.shapes.spi; + + uses com.example.shapes.spi.ShapeFactory; +} diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonRenderer.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonRenderer.kt index e7ea1162..8a07b954 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonRenderer.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/JsonRenderer.kt @@ -58,7 +58,8 @@ class JsonRenderer(private val context: DokkaContext) : Renderer { outputDir = context.configuration.outputDir, moduleReferences = context.configuration.modules.map { it.name to it.relativePathToOutputDirectory.invariantSeparatorsPath - } + }, + sourceRoots = context.configuration.sourceSets.flatMap { it.sourceRoots }.distinct() ).render(root) logger.info("JSON rendering completed (javadoc mode).") return diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt index e2397c85..5906834f 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt @@ -268,13 +268,40 @@ data class JdPackageSummary( val deprecated: JdDeprecation? = null ) +/** One `requires` directive on a module page. */ +@Serializable +data class JdModuleRequires( + val module: String, + val isTransitive: Boolean = false, + val isStatic: Boolean = false, + val url: String? = null +) + +/** + * One `exports` or `opens` directive. [to] is empty for an unqualified directive; javadoc shows a + * populated [to] in its "Exported To" / "Opened To" column, and does not document those packages. + */ +@Serializable +data class JdModuleExport( + val packageName: String, + val to: List = emptyList(), + val url: String? = null +) + +/** One `provides ... with ...` directive. */ +@Serializable +data class JdModuleProvides( + val service: JdTypeRef, + val implementations: List = emptyList() +) + /** * One `module-summary.json` page. * - * `requires` / `uses` / `provides` / `exportedTo` are always empty: they come from a JPMS - * `module-info.java` descriptor, which Dokka's model does not carry (a Dokka "module" is a - * build-level grouping, not a JPMS module). The fields exist so a consumer's shape matches - * javadoc's module page and so they can be filled in later without a schema break. + * The JPMS sections -- [requires], [exports], [opens], [uses], [provides] -- are read from the + * module's `module-info.java`, which Dokka's own model does not carry (a Dokka "module" is a + * build-level grouping, not a JPMS module). They are populated whenever the run's source roots + * are JPMS module roots, and are empty otherwise. */ @Serializable data class JdModulePage( @@ -288,10 +315,13 @@ data class JdModulePage( val seeAlso: List = emptyList(), val deprecated: JdDeprecation? = null, val tags: List = emptyList(), + /** The module's documented packages -- those it exports unqualified. */ val packages: List = emptyList(), - val requires: List = emptyList(), - val uses: List = emptyList(), - val provides: List = emptyList() + val requires: List = emptyList(), + val exports: List = emptyList(), + val opens: List = emptyList(), + val uses: List = emptyList(), + val provides: List = emptyList() ) /** `index.json` -- javadoc's overview page. */ diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt index ebfcd87a..26c96001 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt @@ -28,6 +28,27 @@ class JavadocMapper( private val NON_JAVA_MODIFIERS = setOf("", "open", "empty", "final_kotlin") private const val OBJECT_SIMPLE_NAME = "Object" + + /** A javadoc inline tag: `{@code x}`, `{@link a.B#c label}`, `{@docRoot}`. */ + private val INLINE_TAG = Regex("""\{@(\w+)\s*([^}]*)\}""") + + /** + * Stand-in for `{@inheritDoc}`, substituted into the sources before analysis. + * + * Dokka's own `{@inheritDoc}` resolver recurses without bound on parts of the JDK + * (`InheritDocTagResolver.resolveThrowsTag` -> `toInheritDocHtml`) and takes the whole run + * down with a StackOverflowError. Rewriting the tag to an inert text marker before Dokka + * sees it sidesteps that, and this mapper resolves the marker itself -- walking the same + * supertype chain "Overrides:" and "Specified by:" are derived from, which is what javadoc + * does. See scripts/java/stage_jdk_sources.py. + */ + const val INHERIT_DOC_MARKER = "ADFAINHERITDOC" + + /** Depth cap for chained `{@inheritDoc}`, in case a hierarchy is cyclic after merging. */ + private const val MAX_INHERIT_DEPTH = 16 + + /** The stand-in occupying a paragraph of its own, the usual way `{@inheritDoc}` is written. */ + private val MARKER_PARAGRAPH = Regex("""

\s*$INHERIT_DOC_MARKER\s*

""") } // Anchors of the members each type declares itself, used to derive Overrides/Specified by. @@ -77,6 +98,30 @@ class JavadocMapper( ) } + /** + * Resolves a javadoc reference written as text -- `java.sql.Driver`, `Connection#close()` + * -- to a URL, or null when this run doesn't document it. Used for the inline tags in + * comment text the plugin parsed itself. + */ + fun linkForReference(reference: String): String? { + val typePart = reference.substringBefore('#').trim().trimEnd('.') + val memberPart = reference.substringAfter('#', "").trim() + val type = index.typeForKey(typePart) + // A bare `#member` reference, or a simple name, can't be resolved without a + // context type; only fully qualified references are linked. + ?: return null + if (memberPart.isEmpty()) return url(type.filePath) + // The text form carries the *declared* parameter types, which are not necessarily the + // erased ones the anchor uses, so only the no-arg form is linked precisely. + return url(type.filePath, memberPart) + } + + /** A relative path from this page back to the output root, for `{@docRoot}`. */ + fun pathToRoot(): String { + val depth = fromFile.count { it == '/' } + return if (depth == 0) "." else List(depth) { ".." }.joinToString("/") + } + fun seeRefs(bundle: JavadocDocBundle): List = bundle.seeAlso.map { (name, address, text) -> JdSeeRef( // Dokka puts the referenced symbol in the tag's name and any trailing label in @@ -236,19 +281,93 @@ class JavadocMapper( .firstOrNull { it.description != null || it.other.isNotEmpty() } ?: JavadocDocBundle() + val jpms = module.jpms + // A JPMS module's documentation lives in module-info.java, which Dokka does not read, so + // it is preferred over whatever the Dokka module happens to carry (usually nothing). + val description = jpms?.description?.let { renderJavadocText(it, scope) } ?: bundle.description + val documentedPackages = packagesInModule.map { packageSummary(it, scope) } + val documentedByName = documentedPackages.associateBy { it.name } + return JdModulePage( name = module.name, url = module.filePath, - description = bundle.description, - firstSentence = JavadocDocs.firstSentence(bundle.description), - since = bundle.since, + description = description, + firstSentence = JavadocDocs.firstSentence(description), + since = jpms?.since?.takeIf { it.isNotEmpty() } ?: bundle.since, seeAlso = scope.seeRefs(bundle), deprecated = module.documentables.firstNotNullOfOrNull { deprecationOf(it, bundle) }, - tags = bundle.other, - packages = packagesInModule.map { packageSummary(it, scope) } + tags = jpms?.tags?.takeIf { it.isNotEmpty() } ?: bundle.other, + packages = documentedPackages, + requires = jpms?.requires.orEmpty().map { requires -> + JdModuleRequires( + module = requires.module, + isTransitive = requires.isTransitive, + isStatic = requires.isStatic, + url = index.modules.firstOrNull { it.name == requires.module } + ?.let { scope.url(it.filePath) } + ) + }, + exports = jpms?.exports.orEmpty().map { export -> + JdModuleExport( + packageName = export.packageName, + to = export.to, + // A qualified export is not documented, so it has no page to link to. + url = documentedByName[export.packageName]?.url + ) + }, + opens = jpms?.opens.orEmpty().map { opens -> + JdModuleExport( + packageName = opens.packageName, + to = opens.to, + url = documentedByName[opens.packageName]?.url + ) + }, + uses = jpms?.uses.orEmpty().map { scope.typeRefForKey(it) }, + provides = jpms?.provides.orEmpty().map { provides -> + JdModuleProvides( + service = scope.typeRefForKey(provides.service), + implementations = provides.implementations.map { scope.typeRefForKey(it) } + ) + } ) } + /** + * Renders raw javadoc comment text -- text this plugin read itself rather than getting from + * Dokka, i.e. `module-info.java`'s doc comment. + * + * Only the inline tags are handled: block-level HTML in a javadoc comment is already HTML and + * passes through untouched. An `{@link}` whose target this run does not document degrades to + * `` rather than becoming a dead link, matching what [JavadocDocs] does for the doc + * trees Dokka hands over. + */ + private fun renderJavadocText(raw: String, scope: PageScope): String { + var result = INLINE_TAG.replace(raw) { match -> + val tag = match.groupValues[1] + val body = match.groupValues[2].trim() + when (tag) { + "code", "literal" -> { + val escaped = body.replace("&", "&").replace("<", "<").replace(">", ">") + if (tag == "code") "$escaped" else escaped + } + "link", "linkplain" -> { + val target = body.substringBefore(' ').trim() + val label = body.substringAfter(' ', "").trim().ifBlank { target.substringAfterLast('.') } + val href = scope.linkForReference(target) + val text = if (tag == "link") "$label" else label + if (href == null) text else "$text" + } + // {@docRoot} is a path back to the documentation root, which is exactly what a + // relative link from this page to the root looks like. + "docRoot" -> scope.pathToRoot() + else -> body + } + } + // Collapse the blank lines a stripped block-tag section can leave behind. + result = result.trim() + return result + } + // ------------------------------------------------------------- summaries fun typeSummary(type: JdType, scope: PageScope): JdTypeSummary { @@ -449,6 +568,101 @@ class JavadocMapper( ) } + /** + * The nearest ancestor declaring the same erased signature, and its declaration -- the method + * `{@inheritDoc}` inherits from. Superclasses are searched before interfaces, as javadoc does. + */ + private fun inheritedFrom(owner: JdType, anchor: String): Pair? { + val ancestors = index.superclassChain(owner.key) + index.allSuperinterfaces(owner.key) + ancestors.forEach { key -> + val type = index.typeForKey(key) ?: return@forEach + val declaration = splitMembers(type).declaredMethods.firstOrNull { + index.paths.memberAnchor(it.dri, isConstructor = false) == anchor + } + if (declaration != null) return type to declaration + } + return null + } + + /** + * Replaces [INHERIT_DOC_MARKER] in [text] with the corresponding text from the method this one + * overrides, recursing when the ancestor's own comment inherits in turn. [select] picks which + * part of the ancestor's comment to pull in, so one walk serves the description, `@return`, + * `@param` and `@throws`. + */ + private fun resolveInheritDoc( + text: String?, + owner: JdType, + anchor: String, + scope: PageScope, + depth: Int = 0, + select: (JavadocDocBundle) -> String? + ): String? { + if (text == null || !text.contains(INHERIT_DOC_MARKER)) return text + if (depth >= MAX_INHERIT_DEPTH) return clean(text.replace(INHERIT_DOC_MARKER, "")) + + val parent = inheritedFrom(owner, anchor) + val inherited = parent?.let { (parentType, declaration) -> + // Rendered in the *current* page's scope, so links in the inherited prose resolve + // relative to the page it is being shown on. + resolveInheritDoc( + select(scope.docs.bundleFor(declaration)), parentType, anchor, scope, depth + 1, select + ) + } + val block = inherited.orEmpty() + // Where the marker occupies a paragraph of its own -- `{@inheritDoc}` on its own line, + // which is how javadoc comments almost always write it -- that whole paragraph is + // replaced by the inherited block, wrapper included. Splicing inside the existing

+ // would nest paragraphs whenever the inherited prose runs to more than one. + var result = MARKER_PARAGRAPH.replace(text) { block } + // Any marker left is inline within a sentence, so the inherited fragment's own enclosing + //

comes off before it is spliced in. + if (result.contains(INHERIT_DOC_MARKER)) { + result = result.replace(INHERIT_DOC_MARKER, JavadocDocs.unwrapParagraph(block)) + } + return clean(result) + } + + /** + * As [resolveInheritDoc], but an *absent* value is treated as an implicit `{@inheritDoc}`. + * + * javadoc inherits a missing `@param`/`@return`/`@throws` from the overridden method even + * without the tag being written out, so a method that documents only some of its parameters + * still shows text for the rest. + */ + private fun inheritIfAbsent( + text: String?, + owner: JdType, + anchor: String, + scope: PageScope, + select: (JavadocDocBundle) -> String? + ): String? = resolveInheritDoc(text ?: INHERIT_DOC_MARKER, owner, anchor, scope, select = select) + + /** + * The summary sentence for a declaration as it should read on [scope]'s page. + * + * Global index pages re-render summaries against their own location; passing [owner] and + * [anchor] for a member runs the same `{@inheritDoc}` resolution there as on the member's own + * page, instead of leaking an unresolved marker into the index. + */ + fun summaryFor( + doc: Documentable, + scope: PageScope, + owner: JdType? = null, + anchor: String? = null + ): String? { + val raw = scope.docs.bundleFor(doc).description + val resolved = + if (owner != null && anchor != null) { + resolveInheritDoc(raw ?: INHERIT_DOC_MARKER, owner, anchor, scope) { it.description } + } else { + raw + } + return JavadocDocs.firstSentence(resolved) + } + + private fun clean(text: String): String? = text.trim().ifBlank { null } + private fun executable( function: DFunction, owner: JdType, @@ -474,12 +688,20 @@ class JavadocMapper( JdParameter( name = parameter.name.orEmpty(), type = scope.typeRef(parameter.type), - description = parameter.name?.let { bundle.params[it] }?.ifBlank { null }, + description = inheritIfAbsent( + parameter.name?.let { bundle.params[it] }?.ifBlank { null }, owner, anchor, scope + ) { parent -> parent.params[parameter.name.orEmpty()] }, annotations = annotationNamesOf(parameter) ) } - val declaredThrows = scope.throwsList(bundle) + val declaredThrows = scope.throwsList(bundle).map { thrown -> + thrown.copy( + description = inheritIfAbsent(thrown.description, owner, anchor, scope) { parent -> + parent.throws.firstOrNull { it.first.substringAfterLast('.') == thrown.type.display }?.third + } + ) + } val kind = when { isConstructor -> "constructor" owner.kind == "annotation" -> "annotationElement" @@ -489,6 +711,8 @@ class JavadocMapper( val (overrides, specifiedBy) = if (isConstructor) null to emptyList() else overrideInfo(owner, anchor, scope) + val description = resolveInheritDoc(bundle.description, owner, anchor, scope) { it.description } + return JdExecutable( name = if (isConstructor) owner.simpleName else function.name, anchor = anchor, @@ -503,9 +727,9 @@ class JavadocMapper( modifiers, function.generics, returnType, parameters, declaredThrows, scope ), url = scope.url(owner.filePath, anchor), - description = bundle.description, - firstSentence = JavadocDocs.firstSentence(bundle.description), - returns = bundle.returns, + description = description, + firstSentence = JavadocDocs.firstSentence(description), + returns = inheritIfAbsent(bundle.returns, owner, anchor, scope) { it.returns }, specifiedBy = specifiedBy, overrides = overrides, since = bundle.since, @@ -513,7 +737,13 @@ class JavadocMapper( deprecated = deprecationOf(function, bundle), annotations = annotationNamesOf(function), defaultValue = defaultValueOf(function), - tags = bundle.other + tags = bundle.other.map { tag -> + tag.copy( + text = resolveInheritDoc(tag.text, owner, anchor, scope) { parent -> + parent.other.firstOrNull { it.name == tag.name }?.text + }.orEmpty() + ) + } ) } diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocModelIndex.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocModelIndex.kt index f63b6f62..f5430108 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocModelIndex.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocModelIndex.kt @@ -13,7 +13,9 @@ import org.jetbrains.dokka.model.DPackage import org.jetbrains.dokka.model.Documentable import org.jetbrains.dokka.model.WithSupertypes import org.jetbrains.dokka.pages.PageNode +import org.jetbrains.dokka.model.WithSources import org.jetbrains.dokka.pages.WithDocumentables +import java.io.File /** A type this documentation run covers, plus everything the renderer needs to place it. */ class JdType( @@ -39,7 +41,9 @@ class JdPackage( class JdModule( val name: String, val documentables: List, - val filePath: String + val filePath: String, + /** The JPMS descriptor this module was built from, when the sources are modular. */ + val jpms: JpmsModuleInfo? = null ) /** @@ -62,6 +66,8 @@ class JavadocModelIndex private constructor( val modules: List, val packages: List, val types: List, + /** True when page paths carry a leading `/` segment. */ + val useModuleDirs: Boolean, private val byKey: Map, private val superclassByKey: Map, private val interfacesByKey: Map>, @@ -86,23 +92,54 @@ class JavadocModelIndex private constructor( fun build( root: PageNode, logger: PluginLogger, - sourceSetWhitelist: List + sourceSetWhitelist: List, + sourceRoots: Collection = emptyList() ): JavadocModelIndex { val collected = collectDocumentables(root) val moduleDocs = collected.filterIsInstance() - // Module directories only when this run genuinely spans several modules -- javadoc + val jpmsModules = JpmsModuleScanner.scan(sourceRoots, logger) + + // Module directories when the run genuinely spans several modules -- JPMS modules read + // off module-info.java if the sources are modular, Dokka modules otherwise. javadoc // likewise flattens packages to the output root for a non-modular build. - val useModuleDirs = moduleDocs.distinctBy { it.name }.size > 1 + val useModuleDirs = + if (jpmsModules.isNotEmpty()) jpmsModules.size > 1 + else moduleDocs.distinctBy { it.name }.size > 1 val paths = JavadocPaths(useModuleDirs) + // A package belongs to whichever module declares it, which module-info.java states + // outright. Qualified exports count too: the package is still *in* that module even + // though javadoc won't document it. val moduleOfPackage = mutableMapOf() - moduleDocs.forEach { module -> - module.packages.forEach { pkg -> - moduleOfPackage.putIfAbsent(pkg.dri.packageName.orEmpty(), module.name) + jpmsModules.forEach { module -> + (module.exports + module.opens).forEach { export -> + moduleOfPackage.putIfAbsent(export.packageName, module.name) + } + } + if (jpmsModules.isEmpty()) { + moduleDocs.forEach { module -> + module.packages.forEach { pkg -> + moduleOfPackage.putIfAbsent(pkg.dri.packageName.orEmpty(), module.name) + } } } + // Fallback for a modular project that documents a package its module never exports: + // the source file still sits under exactly one module's source root. + val moduleRoots = jpmsModules.map { it.sourceRoot.absolutePath.trimEnd(File.separatorChar) to it.name } + fun moduleForSourcePath(path: String?): String? { + if (path.isNullOrBlank() || moduleRoots.isEmpty()) return null + val normalized = File(path).absolutePath + return moduleRoots.firstOrNull { (rootPath, _) -> + normalized.startsWith(rootPath + File.separatorChar) + }?.second + } + + fun moduleFor(packageName: String, doc: Documentable): String? = + moduleOfPackage[packageName] + ?: moduleForSourcePath((doc as? WithSources)?.sources?.values?.firstOrNull()?.path) + fun passesWhitelist(doc: Documentable): Boolean { if (sourceSetWhitelist.isEmpty()) return true return doc.sourceSets.any { it.sourceSetID.toString().substringAfterLast("/") in sourceSetWhitelist } @@ -120,7 +157,7 @@ class JavadocModelIndex private constructor( if (byKey.containsKey(key)) return@forEach val packageName = doc.dri.packageName.orEmpty() val classNames = doc.dri.classNames ?: doc.name ?: return@forEach - val moduleName = moduleOfPackage[packageName] + val moduleName = moduleFor(packageName, doc) val type = JdType( documentable = doc, key = key, @@ -215,17 +252,36 @@ class JavadocModelIndex private constructor( } // --- Packages and modules --- + // A package with no exports entry falls back to whichever module its own types + // resolved to, so the two never disagree about where the package page belongs. + val moduleOfTypePackage = types.groupBy { it.packageName } + .mapValues { (_, inPackage) -> inPackage.firstNotNullOfOrNull { it.moduleName } } + val packages = collected.filterIsInstance() .groupBy { it.dri.packageName.orEmpty() } .map { (name, docs) -> - val moduleName = moduleOfPackage[name] + val moduleName = moduleOfPackage[name] ?: moduleOfTypePackage[name] JdPackage(name, moduleName, docs, paths.packageFile(name, moduleName)) } .sortedBy { it.name } - val modules = moduleDocs.groupBy { it.name } - .map { (name, docs) -> JdModule(name, docs, paths.moduleFile(name)) } - .sortedBy { it.name } + val modules = if (jpmsModules.isNotEmpty()) { + val dokkaModuleByName = moduleDocs.groupBy { it.name } + jpmsModules + .map { jpms -> + JdModule( + name = jpms.name, + documentables = dokkaModuleByName[jpms.name].orEmpty(), + filePath = paths.moduleFile(jpms.name), + jpms = jpms + ) + } + .sortedBy { it.name } + } else { + moduleDocs.groupBy { it.name } + .map { (name, docs) -> JdModule(name, docs, paths.moduleFile(name)) } + .sortedBy { it.name } + } logger.info( "javadoc-mode: indexed ${types.size} types, ${packages.size} packages, " + @@ -237,6 +293,7 @@ class JavadocModelIndex private constructor( modules = modules, packages = packages, types = types.sortedBy { it.qualifiedName }, + useModuleDirs = useModuleDirs, byKey = byKey, superclassByKey = superclassByKey, interfacesByKey = interfacesByKey, diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocPaths.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocPaths.kt index cf66b403..a8014d83 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocPaths.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocPaths.kt @@ -94,10 +94,32 @@ class JavadocPaths(private val useModuleDirs: Boolean) { */ private fun isField(callable: Callable): Boolean = callable.isProperty + /** + * Drops type arguments while keeping array brackets: `List[]` -> `List[]`. + * + * Dokka builds a Java DRI from the PSI type's canonical text, which carries the type + * arguments; javadoc's anchors use the *erasure*, so `addAll(java.util.Collection)` has to become `addAll(java.util.Collection)` or the anchor won't match a real javadoc + * build's. + */ + private fun eraseGenerics(name: String): String { + if ('<' !in name) return name + val result = StringBuilder(name.length) + var depth = 0 + name.forEach { character -> + when (character) { + '<' -> depth++ + '>' -> if (depth > 0) depth-- + else -> if (depth == 0) result.append(character) + } + } + return result.toString() + } + /** Renders one DRI parameter type the way javadoc spells it inside a member anchor. */ fun erasedTypeName(ref: TypeReference): String = when (ref) { is TypeConstructor -> { - val fqn = ref.fullyQualifiedName + val fqn = eraseGenerics(ref.fullyQualifiedName) if (fqn in ARRAY_FQNS) { // A raw `kotlin.Array` with no argument can't be rendered as `X[]`; fall back to // Object[] rather than emitting a bare "[]". @@ -107,7 +129,7 @@ class JavadocPaths(private val useModuleDirs: Boolean) { fqn } } - is JavaClassReference -> ref.name + is JavaClassReference -> eraseGenerics(ref.name) // A type variable erases to its leftmost bound, or to Object when unbounded. is TypeParam -> ref.bounds.firstOrNull()?.let { erasedTypeName(it) } ?: "java.lang.Object" is org.jetbrains.dokka.links.Nullable -> erasedTypeName(ref.wrapped) diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocRenderer.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocRenderer.kt index 8343e45a..3837185a 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocRenderer.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocRenderer.kt @@ -40,7 +40,12 @@ class JavadocRenderer( * The modules of a multi-module Dokka run, as `name to relative output path`. Non-empty only * in the aggregating run that Dokka performs after the per-module ones; empty otherwise. */ - private val moduleReferences: List> = emptyList() + private val moduleReferences: List> = emptyList(), + /** + * The run's configured source roots. Scanned for `module-info.java`, which is where the JPMS + * module structure javadoc lays its `api/` tree out by actually lives -- Dokka's model has none. + */ + private val sourceRoots: Collection = emptyList() ) { /** @@ -78,7 +83,7 @@ class JavadocRenderer( ) fun render(root: RootPageNode) { - val index = JavadocModelIndex.build(root, logger, config.sourceSetWhitelist) + val index = JavadocModelIndex.build(root, logger, config.sourceSetWhitelist, sourceRoots) if (index.types.isEmpty() && index.packages.isEmpty()) { // Dokka's aggregating pass over a multi-module build sees only module references, no // documentables -- the real pages were written by the per-module runs. Emit just the @@ -384,7 +389,9 @@ class JavadocRenderer( val anchor: String?, val containingElement: String?, val documentable: Documentable?, - val deprecated: Boolean + val deprecated: Boolean, + /** Set for a member, so its summary resolves `{@inheritDoc}` as its own page does. */ + val owner: JdType? = null ) val pending = mutableListOf() @@ -414,7 +421,8 @@ class JavadocRenderer( anchor = member.anchor, containingElement = member.owner.qualifiedName, documentable = member.documentable, - deprecated = member.deprecated != null + deprecated = member.deprecated != null, + owner = member.owner ) } @@ -435,8 +443,9 @@ class JavadocRenderer( kind = entry.kind, url = scope.url(entry.filePath, entry.anchor), containingElement = entry.containingElement, - firstSentence = entry.documentable - ?.let { JavadocDocs.firstSentence(scope.docs.bundleFor(it).description) }, + firstSentence = entry.documentable?.let { + mapper.summaryFor(it, scope, entry.owner, entry.anchor) + }, deprecated = entry.deprecated ) } @@ -482,7 +491,13 @@ class JavadocRenderer( val filtered = JsonFilters.filterJson(element, config.omitFields, config.omitNulls) val file = File(outputDir, relativePath) file.parentFile?.mkdirs() - file.writeText(json.encodeToString(JsonElement.serializer(), filtered)) + // Belt and braces: the {@inheritDoc} stand-in is resolved wherever it is meant to be, + // but one leaking into published output would be visible corruption, so any straggler + // is dropped here rather than shipped. + file.writeText( + json.encodeToString(JsonElement.serializer(), filtered) + .replace(JavadocMapper.INHERIT_DOC_MARKER, "") + ) logger.debug("javadoc-mode: wrote $relativePath") } catch (e: Exception) { logger.warn("javadoc-mode: failed to write $relativePath: ${e.message}") diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JpmsModuleInfo.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JpmsModuleInfo.kt new file mode 100644 index 00000000..76c210e4 --- /dev/null +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JpmsModuleInfo.kt @@ -0,0 +1,184 @@ +package org.appdevforall.dokka.kdoc2json.javadoc + +import org.appdevforall.dokka.kdoc2json.PluginLogger +import java.io.File + +/** One `requires` directive. */ +class JpmsRequires(val module: String, val isTransitive: Boolean, val isStatic: Boolean) + +/** One `exports` or `opens` directive; [to] is empty for an unqualified one. */ +class JpmsExports(val packageName: String, val to: List) + +/** One `provides ... with ...` directive. */ +class JpmsProvides(val service: String, val implementations: List) + +/** + * A JPMS module descriptor, read straight from a `module-info.java` in a source root. + * + * Dokka's model has no notion of JPMS: a Dokka "module" is a build-level grouping, so the module + * directories, the requires/exports/uses/provides tables and the module description that javadoc's + * module-summary page is built from are simply absent from it. They are all right there in + * `module-info.java` though, so Javadoc mode reads that file directly rather than doing without. + * + * @param description the module's doc comment with its block tags removed, still carrying javadoc + * inline tags (`{@link ...}`, `{@code ...}`). Resolving those needs the type index, so it is + * left to [JavadocMapper] rather than done here. + */ +class JpmsModuleInfo( + val name: String, + val sourceRoot: File, + val description: String?, + val since: List, + val requires: List, + val exports: List, + val opens: List, + val uses: List, + val provides: List, + val tags: List +) { + /** Packages this module exports to everyone -- exactly the set javadoc documents. */ + val exportedPackages: List get() = exports.filter { it.to.isEmpty() }.map { it.packageName } +} + +/** + * Finds and parses the `module-info.java` at the root of each configured source root. + * + * A source root holding a `module-info.java` *is* a JPMS module root, which makes this a + * self-validating signal: an ordinary `src/main/java` root has no such file, so nothing is + * misidentified as a module and non-modular projects are unaffected. + */ +object JpmsModuleScanner { + + // A directive is terminated by ';', and '[^;]' matches newlines, so multi-line `to` and + // `with` lists are handled without needing DOT_MATCHES_ALL. + private val MODULE_DECL = Regex("""\bmodule\s+([\w.]+)\s*\{""") + private val REQUIRES = Regex("""\brequires\s+((?:transitive\s+|static\s+)*)([\w.]+)\s*;""") + private val EXPORTS = Regex("""\bexports\s+([\w.]+)\s*(?:to\s+([^;]+?))?\s*;""") + private val OPENS = Regex("""\bopens\s+([\w.]+)\s*(?:to\s+([^;]+?))?\s*;""") + private val USES = Regex("""\buses\s+([\w.$]+)\s*;""") + private val PROVIDES = Regex("""\bprovides\s+([\w.$]+)\s+with\s+([^;]+?)\s*;""") + + private val BLOCK_COMMENT = Regex("""/\*.*?\*/""", RegexOption.DOT_MATCHES_ALL) + private val LINE_COMMENT = Regex("""//[^\n]*""") + private val DOC_COMMENT = Regex("""/\*\*(.*?)\*/""", RegexOption.DOT_MATCHES_ALL) + private val BLOCK_TAG = Regex("""^\s*@(\w+)\s*(.*)$""") + + private const val MODULE_INFO = "module-info.java" + + fun scan(sourceRoots: Collection, logger: PluginLogger): List { + logger.debug("javadoc-mode: scanning ${sourceRoots.size} source root entry/entries for $MODULE_INFO") + val found = LinkedHashMap() + sourceRoots.forEach { entry -> + // Dokka hands over source roots either as directories or, when the Gradle plugin has + // already expanded a source set, as the individual files in them. Both spellings of + // "here is a module root" are accepted. + val moduleInfo = when { + entry.isDirectory -> File(entry, MODULE_INFO) + entry.name == MODULE_INFO -> entry + else -> return@forEach + } + if (!moduleInfo.isFile) return@forEach + val root = moduleInfo.parentFile ?: return@forEach + try { + val parsed = parse(moduleInfo, root) + // Two source roots for the same module (e.g. a split main/generated layout) would + // otherwise fight over the mapping; the first wins, as it does for packages. + if (found.putIfAbsent(parsed.name, parsed) != null) { + logger.warn("javadoc-mode: module '${parsed.name}' declared in more than one source root; using the first.") + } + } catch (e: Exception) { + logger.warn("javadoc-mode: could not parse ${moduleInfo.path}: ${e.message}") + } + } + if (found.isNotEmpty()) { + logger.info("javadoc-mode: found ${found.size} JPMS module descriptor(s): ${found.keys.sorted().joinToString(", ")}") + } + return found.values.toList() + } + + private fun parse(moduleInfo: File, root: File): JpmsModuleInfo { + val text = moduleInfo.readText() + val (description, since, tags) = parseDocComment(text) + + // Comments are stripped before the directives are read, so a commented-out `exports` is + // never mistaken for a live one. + val body = LINE_COMMENT.replace(BLOCK_COMMENT.replace(text, " "), " ") + val name = MODULE_DECL.find(body)?.groupValues?.get(1) ?: root.name + + fun moduleList(raw: String?): List = + raw?.split(',')?.map { it.trim() }?.filter { it.isNotEmpty() }.orEmpty() + + return JpmsModuleInfo( + name = name, + sourceRoot = root, + description = description, + since = since, + requires = REQUIRES.findAll(body).map { match -> + val modifiers = match.groupValues[1] + JpmsRequires( + module = match.groupValues[2], + isTransitive = modifiers.contains("transitive"), + isStatic = modifiers.contains("static") + ) + }.toList(), + exports = EXPORTS.findAll(body).map { + JpmsExports(it.groupValues[1], moduleList(it.groupValues[2].ifBlank { null })) + }.toList(), + opens = OPENS.findAll(body).map { + JpmsExports(it.groupValues[1], moduleList(it.groupValues[2].ifBlank { null })) + }.toList(), + uses = USES.findAll(body).map { it.groupValues[1] }.toList(), + provides = PROVIDES.findAll(body).map { + JpmsProvides(it.groupValues[1], moduleList(it.groupValues[2])) + }.toList(), + tags = tags + ) + } + + /** + * Pulls the module's doc comment apart into description, `@since`, and every other block tag. + * + * The comment taken is the last one before the `module` declaration -- `module-info.java` + * opens with a license header, which must not be mistaken for the module's documentation. + */ + private fun parseDocComment(text: String): Triple, List> { + val declarationAt = MODULE_DECL.find(text)?.range?.first ?: text.length + val comment = DOC_COMMENT.findAll(text) + .lastOrNull { it.range.last < declarationAt } + ?.groupValues?.get(1) + ?: return Triple(null, emptyList(), emptyList()) + + val lines = comment.lines().map { it.trim().removePrefix("*").let { l -> if (l.startsWith(" ")) l.substring(1) else l } } + + val descriptionLines = mutableListOf() + val since = mutableListOf() + val tags = mutableListOf() + var currentTag: String? = null + val currentText = StringBuilder() + + fun flush() { + val tag = currentTag ?: return + val value = currentText.toString().trim() + if (tag == "since") since += value else tags += JdTag(tag, value) + currentTag = null + currentText.setLength(0) + } + + lines.forEach { line -> + val match = BLOCK_TAG.find(line) + if (match != null) { + flush() + currentTag = match.groupValues[1] + currentText.append(match.groupValues[2]) + } else if (currentTag != null) { + currentText.append('\n').append(line) + } else { + descriptionLines += line + } + } + flush() + + val description = descriptionLines.joinToString("\n").trim().ifBlank { null } + return Triple(description, since.filter { it.isNotBlank() }, tags.filter { it.text.isNotBlank() || it.name == "moduleGraph" }) + } +} diff --git a/Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh b/Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh new file mode 100755 index 00000000..74d1373b --- /dev/null +++ b/Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# Generates the JDK's API documentation as javadoc-shaped JSON, mirroring the api/ tree of the +# official docs (the ones in SourceDocs/JavaDocs/html/api). +# +# Three steps: +# 1. stage_jdk_sources.py unpacks the JDK's lib/src.zip and keeps only what javadoc documents: +# one directory per JPMS module, containing that module's unqualified-exported packages. +# 2. jdk-docs/ runs Dokka over that tree with kdoc-to-json in javadoc-mode. +# 3. The result is copied to the output directory. +# +# The plugin reads each module's module-info.java back out of the staging tree, which is what +# gives the output its //.json layout and fills in the module pages' +# requires / exports / uses / provides sections. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +PLUGIN_DIR="$ROOT_DIR/kdoc-to-json" +PROJECT_DIR="$SCRIPT_DIR/jdk-docs" + +usage() { + cat >&2 <] [-o ] [-w ] [-m ] [--skip-publish] + + -j JDK whose lib/src.zip to document. Defaults to \$JDK_SOURCE_HOME, else \$JAVA_HOME. + Use a JDK matching the docs you want to reproduce -- the docs under + SourceDocs/JavaDocs are Java SE 17. + -o Where to write the JSON tree. Default: $SCRIPT_DIR/build-output/api + -w Scratch directory for the extracted and staged sources. + Default: $SCRIPT_DIR/build-output/work + -m Comma-separated module names to document instead of all of them. Useful for a quick + check: -m java.sql,java.transaction.xa takes seconds rather than many minutes. + --skip-publish Don't republish kdoc-to-json to mavenLocal first. +USAGE + exit 1 +} + +JDK_HOME="${JDK_SOURCE_HOME:-${JAVA_HOME:-}}" +OUTPUT_DIR="$SCRIPT_DIR/build-output/api" +WORK_DIR="$SCRIPT_DIR/build-output/work" +MODULES="" +SKIP_PUBLISH=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + -j) JDK_HOME="$2"; shift 2 ;; + -o) OUTPUT_DIR="$2"; shift 2 ;; + -w) WORK_DIR="$2"; shift 2 ;; + -m) MODULES="$2"; shift 2 ;; + --skip-publish) SKIP_PUBLISH=1; shift ;; + -h|--help) usage ;; + *) echo "Unknown argument: $1" >&2; usage ;; + esac +done + +if [[ -z "$JDK_HOME" ]]; then + echo "Error: no JDK given. Pass -j , or set JDK_SOURCE_HOME or JAVA_HOME." >&2 + exit 1 +fi + +SRC_ZIP="$JDK_HOME/lib/src.zip" +if [[ ! -f "$SRC_ZIP" ]]; then + echo "Error: $SRC_ZIP not found -- that JDK doesn't ship sources." >&2 + exit 1 +fi + +STAGING_DIR="$WORK_DIR/staged" +EXTRACT_DIR="$WORK_DIR/src-extracted" +mkdir -p "$WORK_DIR" + +echo "==> JDK sources: $SRC_ZIP" +"$JDK_HOME/bin/java" -version 2>&1 | head -1 | sed 's/^/ /' + +stage_args=("$SRC_ZIP" "$STAGING_DIR" --extract-to "$EXTRACT_DIR") +if [[ -n "$MODULES" ]]; then + stage_args+=(--modules "$MODULES") +fi +python3 "$SCRIPT_DIR/stage_jdk_sources.py" "${stage_args[@]}" + +if [[ "$SKIP_PUBLISH" != "1" ]]; then + echo "==> Publishing kdoc-to-json to mavenLocal" + (cd "$PLUGIN_DIR" && ./gradlew --console=plain -q publishToMavenLocal) +fi + +echo "==> Running Dokka in javadoc-mode over the staged sources" +echo " (the whole JDK is ~4,800 files across 60 modules; this takes a while)" +(cd "$PROJECT_DIR" && ./gradlew --console=plain dokkaGenerate -PjdkSources="$STAGING_DIR") + +GENERATED="$PROJECT_DIR/build/dokka/html" +if [[ ! -d "$GENERATED" ]]; then + echo "Error: Dokka produced no output at $GENERATED" >&2 + exit 1 +fi + +echo "==> Copying output to $OUTPUT_DIR" +rm -rf "$OUTPUT_DIR" +mkdir -p "$(dirname "$OUTPUT_DIR")" +cp -R "$GENERATED" "$OUTPUT_DIR" + +json_count=$(find "$OUTPUT_DIR" -name '*.json' | wc -l | tr -d ' ') +module_count=$(find "$OUTPUT_DIR" -name 'module-summary.json' | wc -l | tr -d ' ') +package_count=$(find "$OUTPUT_DIR" -name 'package-summary.json' | wc -l | tr -d ' ') + +echo +echo "Done: $json_count JSON files -- $module_count modules, $package_count packages." +echo " Output: $OUTPUT_DIR" +echo " Plugin log: $PROJECT_DIR/build/dokka_json.log" +echo +echo "Compare against the official docs with:" +echo " python3 $SCRIPT_DIR/compare_with_javadoc.py $OUTPUT_DIR /SourceDocs/JavaDocs/html/api" diff --git a/Dokka-plugin-kdoc2json/scripts/java/compare_with_javadoc.py b/Dokka-plugin-kdoc2json/scripts/java/compare_with_javadoc.py new file mode 100755 index 00000000..8f5ee264 --- /dev/null +++ b/Dokka-plugin-kdoc2json/scripts/java/compare_with_javadoc.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""Compares a Javadoc-mode JSON tree against the official javadoc HTML it is meant to mirror. + +Reports, at each level of the api/ tree, what is in one side and not the other: + + modules -- directories with a module-summary page + packages -- directories with a package-summary page + types -- class/interface/enum/record/annotation pages + members -- the anchors on each type page (fields, constructors, methods) + +Member anchors are the sharpest check of the four: javadoc's anchor encodes a member's name and +its *erased* parameter types, so a matching anchor set means the two sides agree on the members, +their signatures, and their overload resolution -- not merely on the page count. + +Exit status is 0 when every level matches, 1 otherwise. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +# javadoc's own non-API pages, which have no JSON counterpart by design. +NON_TYPE_PAGES = { + "package-summary", "package-tree", "package-use", "module-summary", "module-graph", + "allclasses-index", "allpackages-index", "constant-values", "deprecated-list", + "help-doc", "index", "overview-tree", "serialized-form", "system-properties", + "search", "new-list", "preview-list", +} +SKIP_DIRS = {"index-files", "class-use", "doc-files", "legal", "resources", "specs"} + +MEMBER_ANCHOR = re.compile(r'

str: + return (text.replace("<", "<").replace(">", ">") + .replace(""", '"').replace("&", "&")) + + +def html_modules(api: Path) -> set[str]: + return {p.parent.name for p in api.glob("*/module-summary.html")} + + +def json_modules(out: Path) -> set[str]: + return {p.parent.name for p in out.glob("*/module-summary.json")} + + +def _relative_package(page: Path, root: Path) -> str | None: + rel = page.parent.relative_to(root) + parts = rel.parts + if not parts: + return None + # Drop the leading module directory when the tree is modular. + return ".".join(parts[1:]) if len(parts) > 1 else ".".join(parts) + + +def html_packages(api: Path, modular: bool) -> set[str]: + result = set() + for page in api.rglob("package-summary.html"): + if any(part in SKIP_DIRS for part in page.parts): + continue + parts = page.parent.relative_to(api).parts + result.add(".".join(parts[1:] if modular else parts)) + return result + + +def json_packages(out: Path, modular: bool) -> set[str]: + result = set() + for page in out.rglob("package-summary.json"): + parts = page.parent.relative_to(out).parts + result.add(".".join(parts[1:] if modular else parts)) + return result + + +def html_types(api: Path, modular: bool) -> set[str]: + result = set() + for page in api.rglob("*.html"): + if any(part in SKIP_DIRS for part in page.parts): + continue + if page.stem in NON_TYPE_PAGES: + continue + parts = page.parent.relative_to(api).parts + package = ".".join(parts[1:] if modular else parts) + if not package: + continue + result.add(f"{package}.{page.stem}") + return result + + +def json_types(out: Path, modular: bool) -> set[str]: + result = set() + for page in out.rglob("*.json"): + if any(part in SKIP_DIRS for part in page.parts): + continue + if page.stem in NON_TYPE_PAGES: + continue + parts = page.parent.relative_to(out).parts + package = ".".join(parts[1:] if modular else parts) + if not package: + continue + result.add(f"{package}.{page.stem}") + return result + + +def html_member_anchors(page: Path) -> set[str]: + text = page.read_text(encoding="utf-8", errors="replace") + return {unescape(a) for a in MEMBER_ANCHOR.findall(text)} + + +def json_member_anchors(page: Path) -> set[str]: + data = json.loads(page.read_text(encoding="utf-8")) + anchors = set() + for key in ("fields", "enumConstants", "constructors", "methods", "annotationElements"): + for member in data.get(key) or []: + anchors.add(member["anchor"]) + return anchors + + +def report(label: str, expected: set[str], actual: set[str], limit: int) -> bool: + missing = sorted(expected - actual) + extra = sorted(actual - expected) + status = "OK " if not missing and not extra else "DIFF" + print(f"[{status}] {label}: {len(actual)}/{len(expected)} " + f"(missing {len(missing)}, extra {len(extra)})") + for name in missing[:limit]: + print(f" missing: {name}") + if len(missing) > limit: + print(f" ... and {len(missing) - limit} more missing") + for name in extra[:limit]: + print(f" extra: {name}") + if len(extra) > limit: + print(f" ... and {len(extra) - limit} more extra") + return not missing and not extra + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("json_dir", type=Path, help="the generated javadoc-mode JSON tree") + parser.add_argument("html_dir", type=Path, help="the official javadoc api/ directory") + parser.add_argument("--limit", type=int, default=10, help="how many names to list per difference") + parser.add_argument("--members", action="store_true", + help="also compare the member anchors of every type (slower)") + args = parser.parse_args() + + for path in (args.json_dir, args.html_dir): + if not path.is_dir(): + print(f"Error: {path} is not a directory", file=sys.stderr) + return 2 + + ok = True + expected_modules = html_modules(args.html_dir) + actual_modules = json_modules(args.json_dir) + modular = bool(expected_modules) + ok &= report("modules", expected_modules, actual_modules, args.limit) + ok &= report("packages", + html_packages(args.html_dir, modular), + json_packages(args.json_dir, modular), args.limit) + + expected_types = html_types(args.html_dir, modular) + actual_types = json_types(args.json_dir, modular) + ok &= report("types", expected_types, actual_types, args.limit) + + if args.members: + shared = sorted(expected_types & actual_types) + html_index = {} + for page in args.html_dir.rglob("*.html"): + if page.stem in NON_TYPE_PAGES or any(p in SKIP_DIRS for p in page.parts): + continue + parts = page.parent.relative_to(args.html_dir).parts + package = ".".join(parts[1:] if modular else parts) + if package: + html_index[f"{package}.{page.stem}"] = page + json_index = {} + for page in args.json_dir.rglob("*.json"): + if page.stem in NON_TYPE_PAGES or any(p in SKIP_DIRS for p in page.parts): + continue + parts = page.parent.relative_to(args.json_dir).parts + package = ".".join(parts[1:] if modular else parts) + if package: + json_index[f"{package}.{page.stem}"] = page + + total = matched = 0 + differing: list[tuple[str, int, int]] = [] + for name in shared: + try: + expected = html_member_anchors(html_index[name]) + actual = json_member_anchors(json_index[name]) + except Exception as exc: # a malformed page shouldn't abort the whole comparison + print(f" error reading {name}: {exc}") + continue + total += 1 + if expected == actual: + matched += 1 + else: + differing.append((name, len(expected - actual), len(actual - expected))) + + print(f"[{'OK ' if matched == total else 'DIFF'}] member anchors: " + f"{matched}/{total} types match exactly") + for name, missing, extra in differing[:args.limit]: + print(f" {name}: missing {missing}, extra {extra}") + if len(differing) > args.limit: + print(f" ... and {len(differing) - args.limit} more types differ") + ok &= matched == total + + print() + print("MATCH" if ok else "DIFFERENCES FOUND") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/build.gradle.kts b/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/build.gradle.kts new file mode 100644 index 00000000..f432b8c4 --- /dev/null +++ b/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/build.gradle.kts @@ -0,0 +1,86 @@ +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier +import org.jetbrains.dokka.gradle.engine.plugins.DokkaPluginParametersBaseSpec +import javax.inject.Inject + +// Generates the JDK's API documentation as javadoc-shaped JSON. +// +// Sources come from a staging tree produced by ../stage_jdk_sources.py: one directory per JPMS +// module, containing only the packages that module exports unqualified -- which is exactly the +// set the `javadoc` tool documents. Each module directory is registered as its own source root, +// so it is a valid package root and so the plugin can attribute every declaration back to its +// module from `module-info.java`. +// +// Nothing is compiled here. Dokka only needs to *analyse* the sources, and the JDK is not +// buildable as an ordinary Gradle project; the java plugin is applied solely because Dokka's +// Gradle plugin hangs its source sets off one. +plugins { + id("org.jetbrains.dokka") version "2.2.0-Beta" +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + dokkaPlugin("org.appdevforall.dokka:kdoc-to-json:1.0.0-SNAPSHOT") +} + +val stagedSources: String = (findProperty("jdkSources") as String?) + ?: error("Set -PjdkSources= (see scripts/java/stage_jdk_sources.py)") + +val stagedModules: List = file(stagedSources) + .listFiles { f: File -> f.isDirectory && File(f, "module-info.java").isFile } + ?.sortedBy { it.name } + ?: error("No JPMS module directories found under $stagedSources") + +@OptIn(InternalDokkaApi::class) +abstract class JsonOutputPluginParameters @Inject constructor( + name: String +) : DokkaPluginParametersBaseSpec(name, "org.appdevforall.dokka.kdoc2json.JsonOutputPlugin") { + override fun jsonEncode(): String { + System.getenv("KDOC2JSON_TEST_CONFIG")?.let { return File(it).readText() } + return """{ + "logLevel": "info", + "logFile": "build/dokka_json.log", + "javadoc-mode": true, + "omitNulls": true + }""" + } +} + +dokka { + moduleName.set("jdk") + + // Dokka generates in a *worker*, not in the Gradle daemon, so `org.gradle.jvmargs` in + // gradle.properties does not size it -- the worker inherits a default heap and, analysing the + // whole JDK in one pass, dies with an OutOfMemoryError at around 2.5 GB. Give it a process of + // its own with room to work. + dokkaGeneratorIsolation.set( + ProcessIsolation { + maxHeapSize.set(providers.gradleProperty("dokkaWorkerHeap").orElse("24g")) + // The JDK's deeply generic types drive Dokka's analysis into recursion far past what + // the default ~1 MB thread stack survives -- without this it dies with a + // StackOverflowError about a minute in. + jvmArgs.add(providers.gradleProperty("dokkaWorkerStack").orElse("-Xss64m")) + } + ) + + dokkaSourceSets.register("jdk") { + sourceRoots.from(stagedModules) + // javadoc documents public and protected members; Dokka defaults to public only. + documentedVisibilities.set(setOf(VisibilityModifier.Public, VisibilityModifier.Protected)) + // The JDK's own sources are the whole API surface -- there is nothing to link out to. + enableJdkDocumentationLink.set(false) + enableKotlinStdLibDocumentationLink.set(false) + jdkVersion.set(17) + } + + pluginsConfiguration { + registerBinding(JsonOutputPluginParameters::class, JsonOutputPluginParameters::class) + register("org.appdevforall.dokka.kdoc2json.JsonOutputPlugin") { } + } +} + +logger.lifecycle("jdk-api-docs: ${stagedModules.size} module source roots from $stagedSources") diff --git a/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/gradle.properties b/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/gradle.properties new file mode 100644 index 00000000..3773a24d --- /dev/null +++ b/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/gradle.properties @@ -0,0 +1,3 @@ +# The Dokka *worker* does the heavy lifting and is sized by dokkaGeneratorIsolation in +# build.gradle.kts, not from here. The daemon itself only needs enough to run the build. +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g diff --git a/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/gradle/wrapper/gradle-wrapper.jar b/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + 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/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/gradlew.bat b/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/gradlew.bat new file mode 100644 index 00000000..aa5f10b0 --- /dev/null +++ b/Dokka-plugin-kdoc2json/scripts/java/jdk-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/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/settings.gradle.kts b/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/settings.gradle.kts new file mode 100644 index 00000000..ae169cfc --- /dev/null +++ b/Dokka-plugin-kdoc2json/scripts/java/jdk-docs/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "jdk-api-docs" diff --git a/Dokka-plugin-kdoc2json/scripts/java/stage_jdk_sources.py b/Dokka-plugin-kdoc2json/scripts/java/stage_jdk_sources.py new file mode 100755 index 00000000..88015f9b --- /dev/null +++ b/Dokka-plugin-kdoc2json/scripts/java/stage_jdk_sources.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Stages the JDK's *documented* sources for a Javadoc-mode Dokka run. + +`src.zip` ships every source file in the JDK, including internal packages that the official API +docs deliberately leave out. javadoc's rule is exact and easy to reproduce: a package appears in +`api/` if and only if its module `exports` it *unqualified* (an `exports ... to ...` directive is +a targeted export and is not documented). Verified against the JDK 17 docs in +SourceDocs/JavaDocs: for java.base, the 53 unqualified exports are precisely the 53 documented +packages, with nothing on either side left over. + +So this script copies, per module, only the source files of unqualified-exported packages, plus +that module's `module-info.java` (which the plugin reads back for the module page's requires / +uses / provides / exports sections). The result is a tree of one directory per module, each a +valid package root: + + /java.base/module-info.java + /java.base/java/lang/Object.java + /java.sql/java/sql/Connection.java + +Everything left behind still resolves at analysis time from the JDK on the compile classpath, so +dropping it costs nothing but analysis time. +""" + +from __future__ import annotations + +import argparse +import re +import shutil +import sys +import zipfile +from pathlib import Path + +# `exports ;` with no `to` clause. The `to` form is a qualified export -- visible only to the +# named modules, and never part of the published API docs. +UNQUALIFIED_EXPORT = re.compile(r"^\s*exports\s+([\w.]+)\s*;", re.MULTILINE) + +# Dokka's {@inheritDoc} resolver recurses without bound on parts of the JDK +# (InheritDocTagResolver.resolveThrowsTag -> PsiElementToHtmlConverter.toInheritDocHtml) and brings +# the whole run down with a StackOverflowError -- reproducibly, on java.io and java.util among +# others. Rewriting the tag to an inert text marker before Dokka parses it avoids that; the plugin +# then resolves the marker itself, walking the same supertype chain javadoc walks. Nothing is lost: +# 3,214 occurrences across the JDK are still resolved, just by us instead of by Dokka. +INHERIT_DOC = re.compile(r"\{@inheritDoc\}") +INHERIT_DOC_MARKER = "ADFAINHERITDOC" # must match JavadocMapper.INHERIT_DOC_MARKER + +BLOCK_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL) +LINE_COMMENT = re.compile(r"//[^\n]*") +MODULE_DECL = re.compile(r"\bmodule\s+([\w.]+)\s*\{", re.MULTILINE) + +# Modules the JDK's own docs build leaves out (make/Docs.gmk's MODULES_FILTER). They ship in +# src.zip but never appear in api/, so staging them would produce module pages the official docs +# don't have. Verified against the JDK 17 docs: excluding exactly these makes the staged module +# set identical to the documented one. +EXCLUDED_MODULE_PREFIXES = ("jdk.internal.",) +EXCLUDED_MODULES = frozenset({"jdk.unsupported", "jdk.unsupported.desktop", "jdk.random"}) + + +def is_excluded(name: str) -> bool: + return name in EXCLUDED_MODULES or name.startswith(EXCLUDED_MODULE_PREFIXES) + + +def strip_comments(source: str) -> str: + """Removes comments so a commented-out directive is never mistaken for a live one.""" + return LINE_COMMENT.sub("", BLOCK_COMMENT.sub("", source)) + + +def parse_module_info(path: Path) -> tuple[str, list[str]]: + """Returns (module name, unqualified exported packages) for one module-info.java.""" + body = strip_comments(path.read_text(encoding="utf-8", errors="replace")) + match = MODULE_DECL.search(body) + name = match.group(1) if match else path.parent.name + return name, sorted(set(UNQUALIFIED_EXPORT.findall(body))) + + +def extract_sources(src_zip: Path, destination: Path) -> None: + if destination.exists() and any(destination.iterdir()): + print(f" reusing already-extracted sources at {destination}") + return + destination.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(src_zip) as archive: + archive.extractall(destination) + + +def copy_source(source: Path, target: Path, rewrite_inherit_doc: bool) -> None: + """Copies one .java file, optionally neutralising {@inheritDoc} on the way through.""" + if not rewrite_inherit_doc: + shutil.copy2(source, target) + return + text = source.read_text(encoding="utf-8", errors="replace") + rewritten = INHERIT_DOC.sub(INHERIT_DOC_MARKER, text) + if rewritten == text: + shutil.copy2(source, target) + else: + target.write_text(rewritten, encoding="utf-8") + + +def stage( + extracted: Path, + staging: Path, + only: set[str] | None, + excluded: set[str], + rewrite_inherit_doc: bool = True, +) -> tuple[int, int, int]: + if staging.exists(): + shutil.rmtree(staging) + staging.mkdir(parents=True) + + modules = staged_packages = staged_files = 0 + skipped: list[str] = [] + + for module_info in sorted(extracted.glob("*/module-info.java")): + module_dir = module_info.parent + name, exports = parse_module_info(module_info) + if only and name not in only: + continue + if not only and (is_excluded(name) or name in excluded): + skipped.append(name) + continue + + target_module = staging / name + target_module.mkdir(parents=True, exist_ok=True) + shutil.copy2(module_info, target_module / "module-info.java") + modules += 1 + + for package in exports: + source_package = module_dir / Path(*package.split(".")) + if not source_package.is_dir(): + # A package can be exported by one module but live in another's directory in + # src.zip (or not ship sources at all); skip rather than fail the whole run. + print(f" warning: {name} exports {package}, but no sources found", file=sys.stderr) + continue + target_package = target_module / Path(*package.split(".")) + target_package.mkdir(parents=True, exist_ok=True) + # Non-recursive on purpose: a Java package is exactly one directory, and a + # subdirectory is a *different* package that must be exported in its own right. + files = [f for f in source_package.iterdir() if f.suffix == ".java" and f.is_file()] + for java_file in files: + copy_source(java_file, target_package / java_file.name, rewrite_inherit_doc) + staged_packages += 1 + staged_files += len(files) + + if skipped: + print(f" skipped {len(skipped)} undocumented module(s): {', '.join(sorted(skipped))}") + return modules, staged_packages, staged_files + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("src_zip", type=Path, help="path to the JDK's lib/src.zip") + parser.add_argument("staging", type=Path, help="directory to write the staged source tree to") + parser.add_argument("--extract-to", type=Path, default=None, + help="where to unpack src.zip (default: /../src-extracted)") + parser.add_argument("--modules", default=None, + help="comma-separated module names to stage; overrides the exclusion " + "list, so naming an excluded module stages it (default: all " + "documented modules)") + parser.add_argument("--keep-inherit-doc", action="store_true", + help="leave {@inheritDoc} tags as they are. Dokka's own resolver crashes " + "with a StackOverflowError on much of the JDK, so this is expected to " + "fail; it exists to re-check whether a newer Dokka has fixed the bug") + parser.add_argument("--exclude-modules", default="", + help="extra comma-separated module names to leave out, on top of the " + "ones the JDK's own docs build filters") + args = parser.parse_args() + + if not args.src_zip.is_file(): + print(f"Error: {args.src_zip} not found", file=sys.stderr) + return 1 + + extracted = args.extract_to or args.staging.parent / "src-extracted" + only = {m.strip() for m in args.modules.split(",")} if args.modules else None + excluded = {m.strip() for m in args.exclude_modules.split(",") if m.strip()} + + print(f"==> Extracting {args.src_zip}") + extract_sources(args.src_zip, extracted) + + print(f"==> Staging exported packages into {args.staging}") + modules, packages, files = stage( + extracted, args.staging, only, excluded, rewrite_inherit_doc=not args.keep_inherit_doc + ) + + if modules == 0: + print("Error: no modules staged -- is this a modular JDK's src.zip?", file=sys.stderr) + return 1 + + print(f" {modules} modules, {packages} exported packages, {files} source files") + if not args.keep_inherit_doc: + print(" {@inheritDoc} rewritten to an inert marker; the plugin resolves it (see the " + "comment on INHERIT_DOC above)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Dokka-plugin-kdoc2json/tests/test_javadoc_mode.sh b/Dokka-plugin-kdoc2json/tests/test_javadoc_mode.sh index c52ee707..1c8b552e 100755 --- a/Dokka-plugin-kdoc2json/tests/test_javadoc_mode.sh +++ b/Dokka-plugin-kdoc2json/tests/test_javadoc_mode.sh @@ -140,6 +140,24 @@ assert_json "$PKG" "[t['name'] for t in d['annotationTypes']]" "['Measured']" "p # Shape, AbstractShape, Rectangle, Rectangle.Builder, Square, Corner, Measured, ShapeException. assert_json "$PKG" "len(d['allTypes'])" "8" "allTypes lists every type in the package" +# module-summary.json is built from module-info.java, which Dokka's model does not carry at all -- +# these assertions are the guard on that separate parsing path. +MOD="$JAVA_OUTPUT_DIR/module-summary.json" +assert_json "$MOD" "d['name']" "com.example.shapes" \ + "module name comes from module-info.java, not from Dokka's module name" +assert_json "$MOD" "d['since']" "['1.0']" "the module's @since is captured" +assert_json "$MOD" "[(r['module'], r['isTransitive'], r['isStatic']) for r in d['requires']]" \ + "[('java.logging', True, False), ('java.sql', False, True)]" \ + "requires keeps its transitive/static modifiers" +assert_json "$MOD" "[(e['packageName'], e['to']) for e in d['exports']]" \ + "[('com.example.shapes', []), ('com.example.shapes.spi', [])]" "exports directives" +assert_json "$MOD" "[u['qualifiedName'] for u in d['uses']]" \ + "['com.example.shapes.spi.ShapeFactory']" "uses resolves to the documented service type" +assert_json "$MOD" "'module-info.java' in d['description']" "True" \ + "javadoc inline tags in the module comment are rendered" +assert_json "$MOD" "sorted(p['name'] for p in d['packages'])" \ + "['com.example.shapes', 'com.example.shapes.spi']" "module lists its documented packages" + ALL="$JAVA_OUTPUT_DIR/allclasses-index.json" # The eight in com.example.shapes plus ShapeFactory in com.example.shapes.spi. assert_json "$ALL" "len(d['types'])" "9" "allclasses-index covers every documented type" From ec3633f53efb23ee23afbbb7c6bd2776ad2efea8 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Wed, 26 Aug 2026 15:22:12 -0500 Subject: [PATCH 03/14] javadoc mode for kdoc-to-json --- Dokka-plugin-kdoc2json/README.md | 68 ++++++++++ .../src/main/kotlin/javadoc/JavadocDocs.kt | 56 ++++++-- .../src/main/kotlin/javadoc/JavadocDtos.kt | 6 + .../src/main/kotlin/javadoc/JavadocMapper.kt | 123 +++++++++++++++++- .../scripts/java/build-jdk-json-docs.sh | 13 ++ .../scripts/java/stage_jdk_sources.py | 8 ++ 6 files changed, 260 insertions(+), 14 deletions(-) diff --git a/Dokka-plugin-kdoc2json/README.md b/Dokka-plugin-kdoc2json/README.md index 03d52e71..6c5493bc 100644 --- a/Dokka-plugin-kdoc2json/README.md +++ b/Dokka-plugin-kdoc2json/README.md @@ -430,3 +430,71 @@ Both were found running the JDK through it, and both are in Dokka rather than in `-PdokkaWorkerHeap` / `-PdokkaWorkerStack` if needed. - Use a JDK whose version matches the docs you are reproducing. Source and docs from different update releases differ in small ways that are real, not bugs. + +--- + +## 12. Rendering the JSON to HTML (`pebble-renderer`) + +`pebble-renderer/` turns a javadoc-mode JSON tree into browsable HTML using Pebble templates that +follow the official javadoc page structure. It is the reference consumer of the JSON: if a field +is in the JSON, a template here shows it. + +```bash +# After scripts/java/build-jdk-json-docs.sh +pebble-renderer/render.sh scripts/java/build-output/api scripts/java/build-output/html + +# Then browse it +(cd scripts/java/build-output/html && python3 -m http.server 8000) +``` + +### How links work + +The HTML tree mirrors the JSON tree file-for-file, `Foo.json` becoming `Foo.html` in the same +directory. Every link in the JSON is already relative to the page it appears on, so **the path is +correct as-is and only the extension needs changing**. Two Pebble filters do that: + +| Filter | Use | What it does | +| --- | --- | --- | +| `href` | `{{ type.url \| href }}` | rewrites one URL's trailing `.json` to `.html`, preserving any `#anchor` | +| `doc` | `{{ description \| doc }}` | rewrites every `href` *inside* a block of documentation HTML, and marks it safe so it isn't escaped | + +Doc text needs the second filter because a javadoc comment's body is HTML that can itself contain +links. Autoescaping stays on everywhere else, so names and signatures are escaped by default. + +### Templates + +One per page kind, selected by the JSON's `page` field: + +| `page` | Template | Renders | +| --- | --- | --- | +| `class` | `class.peb` | class/interface/enum/annotation/exception page | +| `package` | `package-summary.peb` | package page | +| `module` | `module-summary.peb` | module page, including the JPMS tables | +| `overview` | `overview.peb` | `index.html` | +| `all-classes` / `all-packages` | `all-classes.peb` / `all-packages.peb` | the global indexes | +| `deprecated-list` | `deprecated-list.peb` | deprecated API | +| `constant-values` | `constant-values.peb` | constant field values | +| `index` | `index-page.peb` | one A-Z index page | + +`base.peb` holds the shared skeleton and navigation; `macros.peb` holds the fragments (type links, +signatures, notes, member details). Class names follow the official javadoc output (`top-nav`, +`summary-table`, `col-first`, `member-signature`, `notes`, `inheritance`, …), and +`static/stylesheet.css` styles those names -- it is a readable approximation of javadoc's look, +written here rather than copied from the JDK. + +Two Pebble details worth knowing before editing a template, because both fail *silently*: + +- `{% import "macros" %}` pulls macros into the importing template's own namespace. Call them by + bare name (`{{ typeLink(ref) }}`); a Jinja/Twig-style `macros.typeLink(...)` renders nothing. +- `loop.index` is **0-based**, unlike Jinja's. + +### Measured result (JDK 17) + +Rendering all 4,988 pages takes about two seconds. Of the **450,575** internal links in the +output, **99.88% resolve**; the 555 that don't break down as: + +| Count | Cause | +| --- | --- | +| 251 | `doc-files/` pages -- javadoc copies these from the JDK's build repository, and `src.zip` does not ship them, so they cannot be produced from this source at all | +| 227 | links out of `api/` into `specs/` and `legal/`, which are siblings of `api/` in the official docs and outside what this pipeline generates | +| 77 | hand-written relative links in doc comments that are still rebased imperfectly when a summary sentence is shown on a different page than the one that declares it | diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDocs.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDocs.kt index ef40f032..82a5e59b 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDocs.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDocs.kt @@ -40,7 +40,41 @@ class JavadocDocBundle( * for a target this run doesn't document, in which case the link degrades to plain text rather * than becoming a dead `href`. */ -class JavadocDocs(private val resolveLink: (DRI) -> String?) { +class JavadocDocs( + private val resolveLink: (DRI) -> String?, + /** + * What `{@docRoot}` expands to on the page being rendered -- the relative path back to the + * documentation root. JDK comments use it inside raw `` markup, which + * Dokka hands over as a literal attribute value, so it has to be substituted here or the link + * ships with the tag still in it. + */ + private val docRoot: String = ".", + /** + * Rebases a *relative* href written by hand in a doc comment (``). + * + * Such an href is relative to the page that *declares* the comment. When the same comment is + * shown somewhere else -- a summary on an index page -- it has to be re-expressed relative to + * the page it now appears on, or it points at nothing. The identity default is correct while + * rendering a declaration on its own page. + */ + private val rebaseRelativeHref: (String) -> String = { it } +) { + + /** Applies [rebaseRelativeHref] to an `href` attribute, leaving every other attribute alone. */ + private fun rebased(params: Map): Map { + val href = params["href"] ?: return params + if (!isRelative(href)) return params + return params.toMutableMap().apply { put("href", rebaseRelativeHref(href)) } + } + + /** True for an href that resolves against the current page rather than a root or a host. */ + private fun isRelative(href: String): Boolean = + href.isNotBlank() && + !href.startsWith("#") && + !href.startsWith("/") && + !href.contains("://") && + !href.startsWith("mailto:") && + !href.startsWith(DOC_ROOT_TAG) /** * Picks the doc comment to render. Javadoc has no notion of source sets, so where Dokka has @@ -116,10 +150,10 @@ class JavadocDocs(private val resolveLink: (DRI) -> String?) { is Text -> escapeHtmlText(tag.body) is Br -> "
" is HorizontalRule -> "
" - is Img -> "" - is CodeBlock -> "
" - is CodeInline -> "$children" - is A -> "$children" + is Img -> "" + is CodeBlock -> "
$children
" + is CodeInline -> "$children" + is A -> "$children" is DocumentationLink -> { val href = resolveLink(tag.dri) // An unresolvable {@link} degrades to its own text rather than an href to nowhere: @@ -130,7 +164,7 @@ class JavadocDocs(private val resolveLink: (DRI) -> String?) { is CustomDocTag -> children else -> { val htmlName = HTML_TAG_NAMES[tag::class.java.simpleName] - if (htmlName == null) children else "<$htmlName${attributes(tag.params)}>$children" + if (htmlName == null) children else "<$htmlName${attributes(tag.params, docRoot)}>$children" } } } @@ -169,12 +203,18 @@ class JavadocDocs(private val resolveLink: (DRI) -> String?) { return if (inner.contains("

", ignoreCase = true)) trimmed else inner.trim() } + private const val DOC_ROOT_TAG = "{@docRoot}" + /** Renders a tag's attributes back into HTML, in the order Dokka recorded them. */ - private fun attributes(params: Map): String = + private fun attributes(params: Map, docRoot: String): String = params.entries.joinToString("") { (key, value) -> - " $key=\"${escapeHtmlAttribute(value)}\"" + " $key=\"${escapeHtmlAttribute(expandDocRoot(value, docRoot))}\"" } + /** Substitutes javadoc's `{@docRoot}` in a raw attribute value. */ + fun expandDocRoot(value: String, docRoot: String): String = + if (DOC_ROOT_TAG in value) value.replace(DOC_ROOT_TAG, docRoot) else value + private fun escapeHtmlText(value: String): String = value.replace("&", "&").replace("<", "<").replace(">", ">") diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt index 5906834f..6345d52c 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt @@ -172,6 +172,10 @@ data class JdClassPage( val qualifiedName: String, val packageName: String, val moduleName: String? = null, + /** Link to this type's module page, relative to this page. Null for a non-modular run. */ + val moduleUrl: String? = null, + /** Link to this type's package page, relative to this page. */ + val packageUrl: String? = null, /** * This page's own path, relative to the output root. Note the asymmetry with every *link* * URL in these DTOs, which is relative to the page it appears on, the way javadoc links are. @@ -240,6 +244,8 @@ data class JdPackagePage( val page: String = "package", val name: String, val moduleName: String? = null, + /** Link to this package's module page, relative to this page. Null for a non-modular run. */ + val moduleUrl: String? = null, /** This page's own path, relative to the output root -- see [JdClassPage.url]. */ val url: String, val description: String? = null, diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt index 26c96001..0749c9e0 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt @@ -58,7 +58,45 @@ class JavadocMapper( /** A single output file, and everything that has to be resolved relative to it. */ inner class PageScope(private val fromFile: String) { - val docs = JavadocDocs { dri -> linkFor(dri) } + val docs = JavadocDocs(resolveLink = { dri -> linkFor(dri) }, docRoot = pathToRoot()) + + /** + * A renderer for a comment that *belongs* to the page at [declaringFile] but is being + * shown on this one -- a summary sentence on an index page. Hand-written relative links + * inside it are rebased from that page's directory to this one's. + */ + fun docsFrom(declaringFile: String): JavadocDocs { + if (declaringFile == fromFile) return docs + return JavadocDocs( + resolveLink = { dri -> linkFor(dri) }, + docRoot = pathToRoot(), + rebaseRelativeHref = { href -> rebase(declaringFile, href) } + ) + } + + /** Re-expresses [href], written relative to [declaringFile], relative to this page. */ + private fun rebase(declaringFile: String, href: String): String { + val anchorAt = href.indexOf('#') + val path = if (anchorAt < 0) href else href.substring(0, anchorAt) + val anchor = if (anchorAt < 0) "" else href.substring(anchorAt) + if (path.isEmpty()) return href + val declaringDir = declaringFile.substringBeforeLast('/', "") + val absolute = normalize(if (declaringDir.isEmpty()) path else "$declaringDir/$path") + return index.paths.relativeUrl(fromFile, absolute) + anchor + } + + /** Collapses `.` and `..` segments so the result can be compared against page paths. */ + private fun normalize(path: String): String { + val parts = mutableListOf() + path.split('/').forEach { segment -> + when (segment) { + "", "." -> Unit + ".." -> if (parts.isNotEmpty()) parts.removeAt(parts.size - 1) + else -> parts += segment + } + } + return parts.joinToString("/") + } /** Output-relative path [targetFile], expressed relative to this page. */ fun url(targetFile: String, anchor: String? = null): String { @@ -205,6 +243,9 @@ class JavadocMapper( qualifiedName = type.qualifiedName, packageName = type.packageName, moduleName = type.moduleName, + moduleUrl = moduleUrlFor(type.moduleName, scope), + packageUrl = index.packages.firstOrNull { it.name == type.packageName } + ?.let { scope.url(it.filePath) }, url = type.filePath, modifiers = modifiers, signature = classSignature(type, modifiers, generics, superclassRef, superinterfaceRefs, scope), @@ -240,7 +281,7 @@ class JavadocMapper( annotationElements = members.annotationElements, inheritedFields = members.inheritedFields, inheritedMethods = members.inheritedMethods, - inheritedNestedTypes = emptyList() + inheritedNestedTypes = inheritedNestedTypes(type, scope) ) } @@ -257,6 +298,7 @@ class JavadocMapper( return JdPackagePage( name = pkg.name, moduleName = pkg.moduleName, + moduleUrl = moduleUrlFor(pkg.moduleName, scope), url = pkg.filePath, description = bundle.description, firstSentence = JavadocDocs.firstSentence(bundle.description), @@ -371,7 +413,7 @@ class JavadocMapper( // ------------------------------------------------------------- summaries fun typeSummary(type: JdType, scope: PageScope): JdTypeSummary { - val bundle = scope.docs.bundleFor(type.documentable) + val bundle = scope.docsFrom(type.filePath).bundleFor(type.documentable) return JdTypeSummary( name = type.classNames, qualifiedName = type.qualifiedName, @@ -651,7 +693,7 @@ class JavadocMapper( owner: JdType? = null, anchor: String? = null ): String? { - val raw = scope.docs.bundleFor(doc).description + val raw = scope.docsFrom(owner?.filePath ?: "").bundleFor(doc).description val resolved = if (owner != null && anchor != null) { resolveInheritDoc(raw ?: INHERIT_DOC_MARKER, owner, anchor, scope) { it.description } @@ -661,6 +703,54 @@ class JavadocMapper( return JavadocDocs.firstSentence(resolved) } + /** + * Link to a module's page, relative to [scope]'s page. + * + * Where the module page sits depends on whether the run uses module directories, so the link + * is resolved from the index rather than assembled from the module name in a template. + */ + private fun moduleUrlFor(moduleName: String?, scope: PageScope): String? { + if (moduleName == null) return null + return index.modules.firstOrNull { it.name == moduleName }?.let { scope.url(it.filePath) } + } + + /** + * javadoc's "Nested classes/interfaces declared in class X" groups. + * + * Unlike fields and methods, Dokka does not copy a supertype's nested types down onto the + * subtype, so there is no `InheritedMember` to read: the groups are walked out of the + * hierarchy directly. A nested type the subtype redeclares under the same simple name shadows + * the inherited one and is left out, as it is in javadoc. + */ + private fun inheritedNestedTypes(type: JdType, scope: PageScope): List { + val shadowed = type.documentable.classlikes.mapNotNull { it.name }.toMutableSet() + val alreadyListed = mutableSetOf() + + return (index.superclassChain(type.key) + index.allSuperinterfaces(type.key)) + .mapNotNull { index.typeForKey(it) } + .mapNotNull { ancestor -> + val nested = ancestor.documentable.classlikes + .mapNotNull { index.typeFor(it.dri) } + .filter { it.simpleName !in shadowed && alreadyListed.add(it.qualifiedName) } + .sortedBy { it.simpleName } + if (nested.isEmpty()) { + null + } else { + JdInheritedMembers( + declaringType = scope.typeRefForKey(ancestor.key), + members = nested.map { inner -> + JdMemberRef( + name = inner.classNames, + signature = inner.classNames, + url = scope.url(inner.filePath), + declaringType = scope.typeRefForKey(ancestor.key) + ) + } + ) + } + } + } + private fun clean(text: String): String? = text.trim().ifBlank { null } private fun executable( @@ -711,7 +801,9 @@ class JavadocMapper( val (overrides, specifiedBy) = if (isConstructor) null to emptyList() else overrideInfo(owner, anchor, scope) - val description = resolveInheritDoc(bundle.description, owner, anchor, scope) { it.description } + // A method that documents only its tags -- or has no comment at all -- still shows the + // overridden method's description in javadoc, so an absent description inherits too. + val description = inheritIfAbsent(bundle.description, owner, anchor, scope) { it.description } return JdExecutable( name = if (isConstructor) owner.simpleName else function.name, @@ -1029,8 +1121,20 @@ class JavadocMapper( return methods.count { "abstract" in it.modifiers || ("default" !in it.modifiers && "static" !in it.modifiers) } == 1 } + /** + * The DRI a type *use* should link to. + * + * An array links to its element type, which is what javadoc does -- `BodyPublisher[]` links to + * `BodyPublisher`. Dokka models an array as `kotlin.Array`, a type nothing documents, so + * without this unwrapping every array-typed parameter and return renders as dead text. + */ private fun boundDri(bound: Bound): DRI? = when (bound) { - is GenericTypeConstructor -> bound.dri + is GenericTypeConstructor -> + if (JavadocModelIndex.keyOf(bound.dri) == "kotlin.Array") { + bound.projections.firstOrNull()?.let { projectionBound(it) }?.let { boundDri(it) } + } else { + bound.dri + } is FunctionalTypeConstructor -> bound.dri is TypeParameter -> null is Nullable -> boundDri(bound.inner) @@ -1039,6 +1143,13 @@ class JavadocMapper( else -> null } + /** The bound inside a projection, or null for a star projection. */ + private fun projectionBound(projection: Projection): Bound? = when (projection) { + is Bound -> projection + is Variance<*> -> projection.inner + else -> null + } + private fun isConstructorCallableName(callableName: String, simpleName: String): Boolean = callableName == "" || callableName == simpleName diff --git a/Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh b/Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh index 74d1373b..9cfaab09 100755 --- a/Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh +++ b/Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh @@ -92,6 +92,19 @@ if [[ ! -d "$GENERATED" ]]; then exit 1 fi +# Dokka only writes JSON, so the staged doc-files/ directories have to be carried across +# separately. The HTML renderer copies every non-JSON file through untouched, so putting them in +# the JSON tree is enough to get them into the rendered output too. +echo "==> Copying doc-files/ alongside the generated JSON" +doc_file_count=0 +while IFS= read -r dir; do + rel="${dir#"$STAGING_DIR"/}" + mkdir -p "$GENERATED/$rel" + cp -R "$dir"/. "$GENERATED/$rel"/ + doc_file_count=$((doc_file_count + 1)) +done < <(find "$STAGING_DIR" -type d -name doc-files) +echo " $doc_file_count doc-files directory/ies" + echo "==> Copying output to $OUTPUT_DIR" rm -rf "$OUTPUT_DIR" mkdir -p "$(dirname "$OUTPUT_DIR")" diff --git a/Dokka-plugin-kdoc2json/scripts/java/stage_jdk_sources.py b/Dokka-plugin-kdoc2json/scripts/java/stage_jdk_sources.py index 88015f9b..f9adcde9 100755 --- a/Dokka-plugin-kdoc2json/scripts/java/stage_jdk_sources.py +++ b/Dokka-plugin-kdoc2json/scripts/java/stage_jdk_sources.py @@ -136,6 +136,14 @@ def stage( files = [f for f in source_package.iterdir() if f.suffix == ".java" and f.is_file()] for java_file in files: copy_source(java_file, target_package / java_file.name, rewrite_inherit_doc) + # javadoc copies each package's doc-files/ directory into the output verbatim -- + # supplementary pages the comments link to (java.lang/doc-files/ValueBased.html and + # the like). They are staged here so the build can copy them through, or those links + # land on nothing. + doc_files = source_package / "doc-files" + if doc_files.is_dir(): + shutil.copytree(doc_files, target_package / "doc-files", dirs_exist_ok=True) + staged_packages += 1 staged_files += len(files) From 7192c6ea2ed2a8c1d552a27ef90ae4d0f8d2c26f Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Mon, 31 Aug 2026 15:47:56 -0500 Subject: [PATCH 04/14] ADFA-5296: Restore pebble-renderer, add module graphs and the missing module tables Re-adds the whole pebble-renderer/ tree. It was committed in 327c485a, but the later squash to ec3633f5 was made after a `git reset`, which had turned those files back into untracked ones -- so `commit -a` skipped them and the branch lost all 25 files while they survived on disk. Nothing else was affected; the plugin-side work was carried over intact. Then closes the gaps found by diffing the rendered HTML against SourceDocs/JavaDocs/html/api section by section: module-graph.svg Missing on all 60 module pages -- the bug that started this. Generated from the JSON's requires. All 60 now have node sets identical to the originals (57/60 also match edge counts; graphviz applies a transitive reduction we don't). The rules were derived by testing candidates against all 60 rather than assumed, and both are narrower than they look: the graph is the `requires transitive` closure plus java.base -- a plain `requires` does not propagate readability, and java.base is drawn although no module-info.java declares it. inheritedNestedTypes Never populated, ~700 groups. Dokka does not copy a supertype's nested types down the way it does fields and methods, so there is no InheritedMember to read and the groups are walked out of the hierarchy instead. indirectExports javadoc's "Indirect Exports" table, 16 module pages: the exporting modules in the readability closure. 16/16 exact. indirectRequires javadoc's "Indirect Requires" table, 3 module pages: that closure minus the direct requires. 3/3 exact. tag labels ~2,400 notes rendered under their source name. @apiNote now reads "API Note:", @implSpec "Implementation Requirements:", and so on. The data was always in `tags`; only the heading was wrong. Likewise "Enclosing interface:" now follows the enclosing type's kind. Module pages now have no section the originals have. Structural parity is unchanged at 60/60 modules, 224/224 packages, 4,672/4,672 types, and 453,862 links and images resolve at 99.88%. Two gaps are not reproducible from this source and are recorded as such: doc-files/ (93 files; javadoc copies them from the JDK build repo, and src.zip does not ship them) and serialVersionUID (private fields, shown only on serialized-form.html). Page kinds outside this pipeline -- class-use/, package-use, the tree pages, serialized-form, search -- are listed in README section 11 rather than silently absent. Full suite: 15/15 scripts pass. Co-Authored-By: Claude Opus 5 --- Dokka-plugin-kdoc2json/README.md | 45 +++ .../src/main/kotlin/javadoc/JavadocDtos.kt | 21 ++ .../src/main/kotlin/javadoc/JavadocMapper.kt | 51 +++ .../pebble-renderer/build.gradle.kts | 34 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + .../pebble-renderer/gradlew | 248 +++++++++++++++ .../pebble-renderer/gradlew.bat | 82 +++++ .../pebble-renderer/render.sh | 34 ++ .../pebble-renderer/settings.gradle.kts | 1 + .../pebble-renderer/src/main/.DS_Store | Bin 0 -> 8196 bytes .../docs/render/JavadocExtension.java | 89 ++++++ .../docs/render/JavadocHtmlRenderer.java | 207 ++++++++++++ .../docs/render/ModuleGraphWriter.java | 202 ++++++++++++ .../src/main/resources/.DS_Store | Bin 0 -> 10244 bytes .../src/main/resources/static/stylesheet.css | 167 ++++++++++ .../src/main/resources/templates.zip | Bin 0 -> 16517 bytes .../main/resources/templates/all-classes.peb | 19 ++ .../main/resources/templates/all-packages.peb | 19 ++ .../src/main/resources/templates/base.peb | 43 +++ .../src/main/resources/templates/class.peb | 298 ++++++++++++++++++ .../resources/templates/constant-values.peb | 25 ++ .../resources/templates/deprecated-list.peb | 27 ++ .../main/resources/templates/index-page.peb | 22 ++ .../src/main/resources/templates/macros.peb | 158 ++++++++++ .../resources/templates/module-summary.peb | 157 +++++++++ .../src/main/resources/templates/overview.peb | 38 +++ .../resources/templates/package-summary.peb | 67 ++++ 28 files changed, 2063 insertions(+) create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/build.gradle.kts create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/gradle/wrapper/gradle-wrapper.jar create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/gradle/wrapper/gradle-wrapper.properties create mode 100755 Dokka-plugin-kdoc2json/pebble-renderer/gradlew create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/gradlew.bat create mode 100755 Dokka-plugin-kdoc2json/pebble-renderer/render.sh create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/settings.gradle.kts create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/.DS_Store create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/java/org/appdevforall/docs/render/JavadocExtension.java create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/java/org/appdevforall/docs/render/JavadocHtmlRenderer.java create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/java/org/appdevforall/docs/render/ModuleGraphWriter.java create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/.DS_Store create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/static/stylesheet.css create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates.zip create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-classes.peb create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-packages.peb create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/base.peb create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/class.peb create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/constant-values.peb create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/deprecated-list.peb create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/index-page.peb create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/macros.peb create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/module-summary.peb create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/overview.peb create mode 100644 Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/package-summary.peb diff --git a/Dokka-plugin-kdoc2json/README.md b/Dokka-plugin-kdoc2json/README.md index 6c5493bc..0d4d6e53 100644 --- a/Dokka-plugin-kdoc2json/README.md +++ b/Dokka-plugin-kdoc2json/README.md @@ -498,3 +498,48 @@ output, **99.88% resolve**; the 555 that don't break down as: | 251 | `doc-files/` pages -- javadoc copies these from the JDK's build repository, and `src.zip` does not ship them, so they cannot be produced from this source at all | | 227 | links out of `api/` into `specs/` and `legal/`, which are siblings of `api/` in the official docs and outside what this pipeline generates | | 77 | hand-written relative links in doc comments that are still rebased imperfectly when a summary sentence is shown on a different page than the one that declares it | + +### Comparison against the official docs + +Beyond the counts above, the rendered HTML was diffed section by section against +`SourceDocs/JavaDocs/html/api`. What that turned up, and where it stands: + +**Fixed** + +| Gap | Scale | Now | +| --- | --- | --- | +| `module-graph.svg` missing entirely | all 60 module pages | Generated from the JSON's `requires`. All 60 have node sets identical to the originals; 57/60 also match edge counts (graphviz applies a transitive reduction we don't). | +| Inherited nested types never emitted | ~700 groups | `inheritedNestedTypes` is now computed from the hierarchy -- Dokka, unlike for fields and methods, does not copy nested types down, so there is no `InheritedMember` to read. | +| "Indirect Exports" table | 16 module pages | Added as `indirectExports`. | +| "Indirect Requires" table | 3 module pages | Added as `indirectRequires`. | +| Block tags shown by their source name | ~2,400 notes | `@apiNote` now renders as "API Note:", `@implSpec` as "Implementation Requirements:", and so on, as javadoc does. The data was always in the JSON's `tags`. | +| "Enclosing class:" on a nested interface | 111 pages | Uses the enclosing type's own kind. | + +The module graph rules were derived by checking candidates against all 60 originals rather than +assumed. Both turned out to be narrower than they look: the graph draws the **`requires transitive` +closure plus `java.base`** (a plain `requires` does not propagate readability -- `java.naming` +plainly requires `java.security.sasl` and its graph shows neither), and `java.base` is drawn even +though no `module-info.java` declares it. Same for the tables: **Indirect Requires** is that +closure minus the direct requires (3/3 exact), **Indirect Exports** is the exporting modules in it +(16/16 exact). + +**Not reproducible from this source** + +| Gap | Scale | Why | +| --- | --- | --- | +| `doc-files/` pages and images | 93 files, 251 links | javadoc copies these from the JDK's build repository. `src.zip` does not contain them, so no pipeline reading `src.zip` can produce them. | +| `serialVersionUID` values | 1,174 | Shown only on `serialized-form.html`, and read from private fields Dokka does not model. | + +**Out of scope (page kinds this pipeline does not generate)** + +`class-use/` (4,672 pages), `package-use` (224), `package-tree`/`overview-tree` (225), +`serialized-form`, `system-properties`, `help-doc`, `new-list`, `preview-list`, `search` and its +`.js` index. These are cross-reference and navigation pages rather than API data; everything they +present is derivable from the JSON already emitted. + +**Deliberate differences** + +Our module pages show `Requires`, `Provides`, `Uses` and `Exports` tables on more modules than +javadoc does -- javadoc suppresses some rows (for instance a `provides` whose implementation class +is not itself documented, as in `java.smartcardio`). That is extra data rather than missing data, +so it is left in. diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt index 6345d52c..0e94c52a 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt @@ -294,6 +294,17 @@ data class JdModuleExport( val url: String? = null ) +/** + * One row of a module page's "Indirect Exports" table: a module readable through this one, and the + * packages it exports. + */ +@Serializable +data class JdIndirectExport( + val module: String, + val moduleUrl: String? = null, + val packages: List = emptyList() +) + /** One `provides ... with ...` directive. */ @Serializable data class JdModuleProvides( @@ -324,7 +335,17 @@ data class JdModulePage( /** The module's documented packages -- those it exports unqualified. */ val packages: List = emptyList(), val requires: List = emptyList(), + /** + * Modules a consumer of this one also reads, reached through `requires transitive` but not + * required directly -- javadoc's "Indirect Requires" table. + */ + val indirectRequires: List = emptyList(), val exports: List = emptyList(), + /** + * Packages that become part of this module's API surface because it re-exports the modules + * providing them -- javadoc's "Indirect Exports" table. + */ + val indirectExports: List = emptyList(), val opens: List = emptyList(), val uses: List = emptyList(), val provides: List = emptyList() diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt index 0749c9e0..1eb68ba6 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt @@ -364,6 +364,27 @@ class JavadocMapper( url = documentedByName[opens.packageName]?.url ) }, + indirectRequires = indirectlyReadable(module).map { name -> + JdModuleRequires(module = name, isTransitive = true, url = moduleUrlFor(name, scope)) + }, + indirectExports = readableThrough(module) + .mapNotNull { name -> index.modules.firstOrNull { it.name == name } } + .filter { it.jpms?.exportedPackages?.isNotEmpty() == true } + .sortedBy { it.name } + .map { readable -> + JdIndirectExport( + module = readable.name, + moduleUrl = scope.url(readable.filePath), + packages = readable.jpms?.exportedPackages.orEmpty().sorted().map { packageName -> + JdPackageSummary( + name = packageName, + moduleName = readable.name, + url = index.packages.firstOrNull { it.name == packageName } + ?.let { scope.url(it.filePath) } + ) + } + ) + }, uses = jpms?.uses.orEmpty().map { scope.typeRefForKey(it) }, provides = jpms?.provides.orEmpty().map { provides -> JdModuleProvides( @@ -751,6 +772,36 @@ class JavadocMapper( } } + /** + * Every module a consumer of [module] also gets to read: the `requires transitive` closure. + * + * Only `transitive` edges carry readability onward, so a plain `requires` is not followed and + * the walk is seeded with the transitive requires alone -- seeding it with *all* direct + * requires is what makes the result disagree with javadoc. Checked against every JDK module + * page that has one of these tables. + */ + private fun readableThrough(module: JdModule): Set { + val result = LinkedHashSet() + val seed = module.jpms?.requires.orEmpty().filter { it.isTransitive }.map { it.module } + val seen = seed.toMutableSet() + val work = ArrayDeque(seed) + while (work.isNotEmpty()) { + val current = work.removeFirst() + if (current == module.name) continue + result += current + index.modules.firstOrNull { it.name == current }?.jpms?.requires.orEmpty() + .filter { it.isTransitive } + .forEach { if (seen.add(it.module)) work += it.module } + } + return result + } + + /** [readableThrough] minus what this module already requires directly. */ + private fun indirectlyReadable(module: JdModule): List { + val direct = module.jpms?.requires.orEmpty().map { it.module }.toSet() + return readableThrough(module).filterNot { it in direct || it == module.name }.sorted() + } + private fun clean(text: String): String? = text.trim().ifBlank { null } private fun executable( diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/build.gradle.kts b/Dokka-plugin-kdoc2json/pebble-renderer/build.gradle.kts new file mode 100644 index 00000000..0c32b854 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/build.gradle.kts @@ -0,0 +1,34 @@ +// Renders the plugin's javadoc-mode JSON into browsable HTML using Pebble templates that follow +// the official javadoc page structure. +// +// Deliberately plain Java: it has to build on whatever JDK is around (including ones too new for +// the Kotlin version the plugin itself is pinned to), and there is nothing here that needs Kotlin. +plugins { + application +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("io.pebbletemplates:pebble:3.2.2") + implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2") +} + +java { + // Compatibility rather than a toolchain: this has to build with whatever JDK is on the + // machine (a toolchain would demand a specific one be installed and registered), and the + // code targets nothing newer than 17. + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +application { + mainClass.set("org.appdevforall.docs.render.JavadocHtmlRenderer") +} + +tasks.named("run") { + // Lets the driver script pass " " through as -Pargs="..." + (findProperty("args") as String?)?.let { args = it.split(" ").filter { a -> a.isNotEmpty() } } +} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/gradle/wrapper/gradle-wrapper.jar b/Dokka-plugin-kdoc2json/pebble-renderer/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 zcma&OWmKeVvL#I6?i3D%6z=Zs?ofE*?rw#G$eqJB ziT4y8-Y@s9rkH0Tz>ll(^xkcTl)CY?rS&9VNd66Yc)g^6)JcWaY(5$5gt z8gr3SBXUTN;~cBgz&})qX%#!Fxom2Yau_`&8)+6aSN7YY+pS410rRUU*>J}qL0TnJ zRxt*7QeUqTh8j)Q&iavh<}L+$Jqz))<`IfKussVk%%Ah-Ti?Eo0hQH!rK%K=#EAw0 zwq@@~XNUXRnv8$;zv<6rCRJ6fPD^hfrh;0K?n z=p!u^3xOgWZ%f3+?+>H)9+w^$Tn1e;?UpVMJb!!;f)`6f&4|8mr+g)^@x>_rvnL0< zvD0Hu_N>$(Li7|Jgu0mRh&MV+<}`~Wi*+avM01E)Jtg=)-vViQKax!GeDc!xv$^mL z{#OVBA$U{(Zr8~Xm|cP@odkHC*1R8z6hcLY#N@3E-A8XEvpt066+3t9L_6Zg6j@9Q zj$$%~yO-OS6PUVrM2s)(T4#6=JpI_@Uz+!6=GdyVU?`!F=d;8#ZB@(5g7$A0(`eqY z8_i@3w$0*es5mrSjhW*qzrl!_LQWs4?VfLmo1Sd@Ztt53+etwzAT^8ow_*7Jp`Y|l z*UgSEwvxq+FYO!O*aLf-PinZYne7Ib6ny3u>MjQz=((r3NTEeU4=-i0LBq3H-VJH< z^>1RE3_JwrclUn9vb7HcGUaFRA0QHcnE;6)hnkp%lY1UII#WPAv?-;c?YH}LWB8Nl z{sx-@Z;QxWh9fX8SxLZk8;kMFlGD3Jc^QZVL4nO)1I$zQwvwM&_!kW+LMf&lApv#< zur|EyC|U@5OQuph$TC_ZU`{!vJp`13e9alaR0Dbn5ikLFH7>eIz4QbV|C=%7)F=qo z_>M&5N)d)7G(A%c>}UCrW!Ql_6_A{?R7&CL`;!KOb3 z8Z=$YkV-IF;c7zs{3-WDEFJzuakFbd*4LWd<_kBE8~BFcv}js_2OowRNzWCtCQ6&k z{&~Me92$m*@e0ANcWKuz)?YjB*VoSTx??-3Cc0l2U!X^;Bv@m87eKHukAljrD54R+ zE;@_w4NPe1>3`i5Qy*3^E9x#VB6?}v=~qIprrrd5|DFkg;v5ixo0IsBmik8=Y;zv2 z%Bcf%NE$a44bk^`i4VwDLTbX=q@j9;JWT9JncQ!+Y%2&HHk@1~*L8-{ZpY?(-a9J-1~<1ltr9i~D9`P{XTIFWA6IG8c4;6bFw*lzU-{+?b&%OcIoCiw00n>A1ra zFPE$y@>ebbZlf(sN_iWBzQKDV zmmaLX#zK!@ZdvCANfwV}9@2O&w)!5gSgQzHdk2Q`jG6KD7S+1R5&F)j6QTD^=hq&7 zHUW+r^da^%V(h(wonR(j?BOiC!;y=%nJvz?*aW&5E87qq;2z`EI(f zBJNNSMFF9U{sR-af5{IY&AtoGcoG)Iq-S^v{7+t0>7N(KRoPj;+2N5;9o_nxIGjJ@ z7bYQK)bX)vEhy~VL%N6g^NE@D5VtV+Q8U2%{ji_=6+i^G%xeskEhH>Sqr194PJ$fB zu1y^){?9Vkg(FY2h)3ZHrw0Z<@;(gd_dtF#6y_;Iwi{yX$?asr?0N0_B*CifEi7<6 zq`?OdQjCYbhVcg+7MSgIM|pJRu~`g?g3x?Tl+V}#$It`iD1j+!x+!;wS0+2e>#g?Z z*EA^k7W{jO1r^K~cD#5pamp+o@8&yw6;%b|uiT?{Wa=4+9<}aXWUuL#ZwN1a;lQod zW{pxWCYGXdEq9qAmvAB904}?97=re$>!I%wxPV#|f#@A*Y=qa%zHlDv^yWbR03%V0 zprLP+b(#fBqxI%FiF*-n8HtH6$8f(P6!H3V^ysgd8de-N(@|K!A< z^qP}jp(RaM9kQ(^K(U8O84?D)aU(g?1S8iWwe)gqpHCaFlJxb*ilr{KTnu4_@5{K- z)n=CCeCrPHO0WHz)dDtkbZfUfVBd?53}K>C5*-wC4hpDN8cGk3lu-ypq+EYpb_2H; z%vP4@&+c2p;thaTs$dc^1CDGlPG@A;yGR5@$UEqk6p58qpw#7lc<+W(WR;(vr(D>W z#(K$vE#uBkT=*q&uaZwzz=P5mjiee6>!lV?c}QIX%ZdkO1dHg>Fa#xcGT6~}1*2m9 zkc7l3ItD6Ie~o_aFjI$Ri=C!8uF4!Ky7iG9QTrxVbsQroi|r)SAon#*B*{}TB-?=@ z8~jJs;_R2iDd!$+n$%X6FO&PYS{YhDAS+U2o4su9x~1+U3z7YN5o0qUK&|g^klZ6X zj_vrM5SUTnz5`*}Hyts9ADwLu#x_L=nv$Z0`HqN`Zo=V>OQI)fh01n~*a%01%cx%0 z4LTFVjmW+ipVQv5rYcn3;d2o4qunWUY!p+?s~X~(ost@WR@r@EuDOSs8*MT4fiP>! zkfo^!PWJJ1MHgKS2D_hc?Bs?isSDO61>ebl$U*9*QY(b=i&rp3@3GV@z>KzcZOxip z^dzA~44;R~cnhWz7s$$v?_8y-k!DZys}Q?4IkSyR!)C0j$(Gm|t#e3|QAOFaV2}36 z?dPNY;@I=FaCwylc_;~kXlZsk$_eLkNb~TIl8QQ`mmH&$*zwwR8zHU*sId)rxHu*K z;yZWa8UmCwju%aSNLwD5fBl^b0Ux1%q8YR*uG`53Mi<`5uA^Dc6Ync)J3N7;zQ*75)hf%a@{$H+%S?SGT)ks60)?6j$ zspl|4Ad6@%-r1t*$tT(en!gIXTUDcsj?28ZEzz)dH)SV3bZ+pjMaW0oc~rOPZP@g! zb9E+ndeVO_Ib9c_>{)`01^`ZS198 z)(t=+{Azi11$eu%aU7jbwuQrO`vLOixuh~%4z@mKr_Oc;F%Uq01fA)^W&y+g16e?rkLhTxV!EqC%2}sx_1u7IBq|}Be&7WI z4I<;1-9tJsI&pQIhj>FPkQV9{(m!wYYV@i5h?A0#BN2wqlEwNDIq06|^2oYVa7<~h zI_OLan0Do*4R5P=a3H9`s5*>xU}_PSztg`+2mv)|3nIy=5#Z$%+@tZnr> zLcTI!Mxa`PY7%{;KW~!=;*t)R_sl<^b>eNO@w#fEt(tPMg_jpJpW$q_DoUlkY|uo> z0-1{ouA#;t%spf*7VjkK&$QrvwUERKt^Sdo)5@?qAP)>}Y!h4(JQ!7{wIdkA+|)bv z&8hBwoX4v|+fie}iTslaBX^i*TjwO}f{V)8*!dMmRPi%XAWc8<_IqK1jUsApk)+~R zNFTCD-h>M5Y{qTQ&0#j@I@tmXGj%rzhTW5%Bkh&sSc=$Fv;M@1y!zvYG5P2(2|(&W zlcbR1{--rJ&s!rB{G-sX5^PaM@3EqWVz_y9cwLR9xMig&9gq(voeI)W&{d6j1jh&< zARXi&APWE1FQWh7eoZjuP z;vdgX>zep^{{2%hem;e*gDJhK1Hj12nBLIJoL<=0+8SVEBx7!4Ea+hBY;A1gBwvY<)tj~T=H`^?3>zeWWm|LAwo*S4Z%bDVUe z6r)CH1H!(>OH#MXFJ2V(U(qxD{4Px2`8qfFLG+=a;B^~Te_Z!r3RO%Oc#ZAHKQxV5 zRYXxZ9T2A%NVJIu5Pu7!Mj>t%YDO$T@M=RR(~mi%sv(YXVl`yMLD;+WZ{vG9(@P#e zMo}ZiK^7^h6TV%cG+;jhJ0s>h&VERs=tuZz^Tlu~%d{ZHtq6hX$V9h)Bw|jVCMudd zwZ5l7In8NT)qEPGF$VSKg&fb0%R2RnUnqa){)V(X(s0U zkCdVZe6wy{+_WhZh3qLp245Y2RR$@g-!9PjJ&4~0cFSHMUn=>dapv)hy}|y91ZWTV zCh=z*!S3_?`$&-eZ6xIXUq8RGl9oK0BJw*TdU6A`LJqX9eS3X@F)g$jLkBWFscPhR zpCv8#KeAc^y>>Y$k^=r|K(DTC}T$0#jQBOwB#@`P6~*IuW_8JxCG}J4va{ zsZzt}tt+cv7=l&CEuVtjD6G2~_Meh%p4RGuY?hSt?(sreO_F}8r7Kp$qQdvCdZnDQ zxzc*qchE*E2=WK)^oRNa>Ttj`fpvF-JZ5tu5>X1xw)J@1!IqWjq)ESBG?J|ez`-Tc zi5a}GZx|w-h%5lNDE_3ho0hEXMoaofo#Z;$8|2;EDF&*L+e$u}K=u?pb;dv$SXeQM zD-~7P0i_`Wk$#YP$=hw3UVU+=^@Kuy$>6?~gIXx636jh{PHly_a2xNYe1l60`|y!7 z(u%;ILuW0DDJ)2%y`Zc~hOALnj1~txJtcdD#o4BCT68+8gZe`=^te6H_egxY#nZH&P*)hgYaoJ^qtmpeea`35Fw)cy!w@c#v6E29co8&D9CTCl%^GV|X;SpneSXzV~LXyRn-@K0Df z{tK-nDWA!q38M1~`xUIt_(MO^R(yNY#9@es9RQbY@Ia*xHhD&=k^T+ zJi@j2I|WcgW=PuAc>hs`(&CvgjL2a9Rx zCbZyUpi8NWUOi@S%t+Su4|r&UoU|ze9SVe7p@f1GBkrjkkq)T}X%Qo1g!SQ{O{P?m z-OfGyyWta+UCXH+-+(D^%kw#A1-U;?9129at7MeCCzC{DNgO zeSqsV>W^NIfTO~4({c}KUiuoH8A*J!Cb0*sp*w-Bg@YfBIPZFH!M}C=S=S7PLLcIG zs7K77g~W)~^|+mx9onzMm0qh(f~OsDTzVmRtz=aZTllgR zGUn~_5hw_k&rll<4G=G+`^Xlnw;jNYDJz@bE?|r866F2hA9v0-8=JO3g}IHB#b`hy zA42a0>{0L7CcabSD+F7?pGbS1KMvT{@1_@k!_+Ki|5~EMGt7T%u=79F)8xEiL5!EJ zzuxQ`NBliCoJMJdwu|);zRCD<5Sf?Y>U$trQ-;xj6!s5&w=9E7)%pZ+1Nh&8nCCwM zv5>Ket%I?cxr3vVva`YeR?dGxbG@pi{H#8@kFEf0Jq6~K4>kt26*bxv=P&jyE#e$| zDJB_~imk^-z|o!2njF2hL*|7sHCnzluhJjwLQGDmC)Y9 zr9ZN`s)uCd^XDvn)VirMgW~qfn1~SaN^7vcX#K1G`==UGaDVVx$0BQnubhX|{e z^i0}>k-;BP#Szk{cFjO{2x~LjK{^Upqd&<+03_iMLp0$!6_$@TbX>8U-f*-w-ew1?`CtD_0y_Lo|PfKi52p?`5$Jzx0E8`M0 zNIb?#!K$mM4X%`Ry_yhG5k@*+n4||2!~*+&pYLh~{`~o(W|o64^NrjP?-1Lgu?iK^ zTX6u3?#$?R?N!{599vg>G8RGHw)Hx&=|g4599y}mXNpM{EPKKXB&+m?==R3GsIq?G zL5fH={=zawB(sMlDBJ+{dgb)Vx3pu>L=mDV0{r1Qs{0Pn%TpopH{m(By4;{FBvi{I z$}x!Iw~MJOL~&)p93SDIfP3x%ROjg}X{Sme#hiJ&Yk&a;iR}V|n%PriZBY8SX2*;6 z4hdb^&h;Xz%)BDACY5AUsV!($lib4>11UmcgXKWpzRL8r2Srl*9Y(1uBQsY&hO&uv znDNff0tpHlLISam?o(lOp#CmFdH<6HmA0{UwfU#Y{8M+7od8b8|B|7ZYR9f<#+V|ZSaCQvI$~es~g(Pv{2&m_rKSB2QQ zMvT}$?Ll>V+!9Xh5^iy3?UG;dF-zh~RL#++roOCsW^cZ&({6q|?Jt6`?S8=16Y{oH zp50I7r1AC1(#{b`Aq5cw>ypNggHKM9vBx!W$eYIzD!4KbLsZGr2o8>g<@inmS3*>J zx8oG((8f!ei|M@JZB`p7+n<Q}?>h249<`7xJ?u}_n;Gq(&km#1ULN87CeTO~FY zS_Ty}0TgQhV zOh3T7{{x&LSYGQfKR1PDIkP!WnfC1$l+fs@Di+d4O=eVKeF~2fq#1<8hEvpwuqcaH z4A8u~r^gnY3u6}zj*RHjk{AHhrrDqaj?|6GaVJbV%o-nATw}ASFr!f`Oz|u_QPkR# z0mDudY1dZRlk@TyQ?%Eti=$_WNFtLpSx9=S^be{wXINp%MU?a`F66LNU<c;0&ngifmP9i;bj6&hdGMW^Kf8e6ZDXbQD&$QAAMo;OQ)G zW(qlHh;}!ZP)JKEjm$VZjTs@hk&4{?@+NADuYrr!R^cJzU{kGc1yB?;7mIyAWwhbeA_l_lw-iDVi7wcFurf5 z#Uw)A@a9fOf{D}AWE%<`s1L_AwpZ?F!Vac$LYkp<#A!!`XKaDC{A%)~K#5z6>Hv@V zBEqF(D5?@6r3Pwj$^krpPDCjB+UOszqUS;b2n>&iAFcw<*im2(b3|5u6SK!n9Sg4I z0KLcwA6{Mq?p%t>aW0W!PQ>iUeYvNjdKYqII!CE7SsS&Rj)eIw-K4jtI?II+0IdGq z2WT|L3RL?;GtGgt1LWfI4Ka`9dbZXc$TMJ~8#Juv@K^1RJN@yzdLS8$AJ(>g!U9`# zx}qr7JWlU+&m)VG*Se;rGisutS%!6yybi%B`bv|9rjS(xOUIvbNz5qtvC$_JYY+c& za*3*2$RUH8p%pSq>48xR)4qsp!Q7BEiJ*`^>^6INRbC@>+2q9?x(h0bpc>GaNFi$K zPH$6!#(~{8@0QZk=)QnM#I=bDx5vTvjm$f4K}%*s+((H2>tUTf==$wqyoI`oxI7>C z&>5fe)Yg)SmT)eA(|j@JYR1M%KixxC-Eceknf-;N=jJTwKvk#@|J^&5H0c+%KxHUI z6dQbwwVx3p?X<_VRVb2fStH?HH zFR@Mp=qX%#L3XL)+$PXKV|o|#DpHAoqvj6uQKe@M-mnhCSou7Dj4YuO6^*V`m)1lf z;)@e%1!Qg$10w8uEmz{ENb$^%u}B;J7sDd zump}onoD#!l=agcBR)iG!3AF0-63%@`K9G(CzKrm$VJ{v7^O9Ps7Zej|3m= zVXlR&yW6=Y%mD30G@|tf=yC7-#L!16Q=dq&@beWgaIL40k0n% z)QHrp2Jck#evLMM1RGt3WvQ936ZC9vEje0nFMfvmOHVI+&okB_K|l-;|4vW;qk>n~ z+|kk8#`K?x`q>`(f6A${wfw9Cx(^)~tX7<#TpxR#zYG2P+FY~mG{tnEkv~d6oUQA+ z&hNTL=~Y@rF`v-RZlts$nb$3(OL1&@Y11hhL9+zUb6)SP!;CD)^GUtUpCHBE`j1te zAGud@miCVFLk$fjsrcpjsadP__yj9iEZUW{Ll7PPi<$R;m1o!&Xdl~R_v0;oDX2z^!&8}zNGA}iYG|k zmehMd1%?R)u6R#<)B)1oe9TgYH5-CqUT8N7K-A-dm3hbm_W21p%8)H{O)xUlBVb+iUR}-v5dFaCyfSd zC6Bd7=N4A@+Bna=!-l|*_(nWGDpoyU>nH=}IOrLfS+-d40&(Wo*dDB9nQiA2Tse$R z;uq{`X7LLzP)%Y9aHa4YQ%H?htkWd3Owv&UYbr5NUDAH^<l@Z0Cx%`N+B*i!!1u>D8%;Qt1$ zE5O0{-`9gdDxZ!`0m}ywH!;c{oBfL-(BH<&SQ~smbcobU!j49O^f4&IIYh~f+hK*M zZwTp%{ZSAhMFj1qFaOA+3)p^gnXH^=)`NTYgTu!CLpEV2NF=~-`(}7p^Eof=@VUbd z_9U|8qF7Rueg&$qpSSkN%%%DpbV?8E8ivu@ensI0toJ7Eas^jyFReQ1JeY9plb^{m z&eQO)qPLZQ6O;FTr*aJq=$cMN)QlQO@G&%z?BKUs1&I^`lq>=QLODwa`(mFGC`0H< zOlc*|N?B5&!U6BuJvkL?s1&nsi$*5cCv7^j_*l&$-sBmRS85UIrE--7eD8Gr3^+o? zqG-Yl4S&E;>H>k^a0GdUI(|n1`ws@)1%sq2XBdK`mqrNq_b4N{#VpouCXLzNvjoFv zo9wMQ6l0+FT+?%N(ka*;%m~(?338bu32v26!{r)|w8J`EL|t$}TA4q_FJRX5 zCPa{hc_I(7TGE#@rO-(!$1H3N-C0{R$J=yPCXCtGk{4>=*B56JdXU9cQVwB`6~cQZ zf^qK21x_d>X%dT!!)CJQ3mlHA@ z{Prkgfs6=Tz%63$6Zr8CO0Ak3A)Cv#@BVKr&aiKG7RYxY$Yx>Bj#3gJk*~Ps-jc1l z;4nltQwwT4@Z)}Pb!3xM?+EW0qEKA)sqzw~!C6wd^{03-9aGf3Jmt=}w-*!yXupLf z;)>-7uvWN4Unn8b4kfIza-X=x*e4n5pU`HtgpFFd))s$C@#d>aUl3helLom+RYb&g zI7A9GXLRZPl}iQS*d$Azxg-VgcUr*lpLnbPKUV{QI|bsG{8bLG<%CF( zMoS4pRDtLVYOWG^@ox^h8xL~afW_9DcE#^1eEC1SVSb1BfDi^@g?#f6e%v~Aw>@w- zIY0k+2lGWNV|aA*e#`U3=+oBDmGeInfcL)>*!w|*;mWiKNG6wP6AW4-4imN!W)!hE zA02~S1*@Q`fD*+qX@f3!2yJX&6FsEfPditB%TWo3=HA;T3o2IrjS@9SSxv%{{7&4_ zdS#r4OU41~GYMiib#z#O;zohNbhJknrPPZS6sN$%HB=jUnlCO_w5Gw5EeE@KV>soy z2EZ?Y|4RQDDjt5y!WBlZ(8M)|HP<0YyG|D%RqD+K#e7-##o3IZxS^wQ5{Kbzb6h(i z#(wZ|^ei>8`%ta*!2tJzwMv+IFHLF`zTU8E^Mu!R*45_=ccqI};Zbyxw@U%a#2}%f zF>q?SrUa_a4H9l+uW8JHh2Oob>NyUwG=QH~-^ZebU*R@67DcXdz2{HVB4#@edz?B< z5!rQH3O0>A&ylROO%G^fimV*LX7>!%re{_Sm6N>S{+GW1LCnGImHRoF@csnFzn@P0 zM=jld0z%oz;j=>c7mMwzq$B^2mae7NiG}%>(wtmsDXkWk{?BeMpTrIt3Mizq?vRsf zi_WjNp+61uV(%gEU-Vf0;>~vcDhe(dzWdaf#4mH3o^v{0EWhj?E?$5v02sV@xL0l4 zX0_IMFtQ44PfWBbPYN#}qxa%=J%dlR{O!KyZvk^g5s?sTNycWYPJ^FK(nl3k?z-5t z39#hKrdO7V(@!TU)LAPY&ngnZ1MzLEeEiZznn7e-jLCy8LO zu^7_#z*%I-BjS#Pg-;zKWWqX-+Ly$T!4`vTe5ZOV0j?TJVA*2?*=82^GVlZIuH%9s zXiV&(T(QGHHah=s&7e|6y?g+XxZGmK55`wGV>@1U)Th&=JTgJq>4mI&Av2C z)w+kRoj_dA!;SfTfkgMPO>7Dw6&1*Hi1q?54Yng`JO&q->^CX21^PrU^JU#CJ_qhV zSG>afB%>2fx<~g8p=P8Yzxqc}s@>>{g7}F!;lCXvF#RV)^fyYb_)iKVCz1xEq=fJ| z0a7DMCK*FuP=NM*5h;*D`R4y$6cpW-E&-i{v`x=Jbk_xSn@2T3q!3HoAOB`@5Vg6) z{PW|@9o!e;v1jZ2{=Uw6S6o{g82x6g=k!)cFSC*oemHaVjg?VpEmtUuD2_J^A~$4* z3O7HsbA6wxw{TP5Kk)(Vm?gKo+_}11vbo{Tp_5x79P~#F)ahQXT)tSH5;;14?s)On zel1J>1x>+7;g1Iz2FRpnYz;sD0wG9Q!vuzE9yKi3@4a9Nh1!GGN?hA)!mZEnnHh&i zf?#ZEN2sFbf~kV;>K3UNj1&vFhc^sxgj8FCL4v>EOYL?2uuT`0eDH}R zmtUJMxVrV5H{L53hu3#qaWLUa#5zY?f5ozIn|PkMWNP%n zWB5!B0LZB0kLw$k39=!akkE9Q>F4j+q434jB4VmslQ;$ zKiO#FZ`p|dKS716jpcvR{QJkSNfDVhr2%~eHrW;fU45>>snr*S8Vik-5eN5k*c2Mp zyxvX&_cFbB6lODXznHHT|rsURe2!swomtrqc~w5 zymTM8!w`1{04CBprR!_F{5LB+2_SOuZN{b*!J~1ZiPpP-M;);!ce!rOPDLtgR@Ie1 zPreuqm4!H)hYePcW1WZ0Fyaqe%l}F~Orr)~+;mkS&pOhP5Ebb`cnUt!X_QhP4_4p( z8YKQCDKGIy>?WIFm3-}Br2-N`T&FOi?t)$hjphB9wOhBXU#Hb+zm&We_-O)s(wc`2 z8?VsvU;J>Ju7n}uUb3s1yPx_F*|FlAi=Ge=-kN?1;`~6szP%$3B0|8Sqp%ebM)F8v zADFrbeT0cgE>M0DMV@_Ze*GHM>q}wWMzt|GYC%}r{OXRG3Ij&<+nx9;4jE${Fj_r* z`{z1AW_6Myd)i6e0E-h&m{{CvzH=Xg!&(bLYgRMO_YVd8JU7W+7MuGWNE=4@OvP9+ zxi^vqS@5%+#gf*Z@RVyU9N1sO-(rY$24LGsg1>w>s6ST^@)|D9>cT50maXLUD{Fzf zt~tp{OSTEKg3ZSQyQQ5r51){%=?xlZ54*t1;Ow)zLe3i?8tD8YyY^k%M)e`V*r+vL zPqUf&m)U+zxps+NprxMHF{QSxv}>lE{JZETNk1&F+R~bp{_T$dbXL2UGnB|hgh*p4h$clt#6;NO~>zuyY@C-MD@)JCc5XrYOt`wW7! z_ti2hhZBMJNbn0O-uTxl_b6Hm313^fG@e;RrhIUK9@# z+DHGv_Ow$%S8D%RB}`doJjJy*aOa5mGHVHz0e0>>O_%+^56?IkA5eN+L1BVCp4~m=1eeL zb;#G!#^5G%6Mw}r1KnaKsLvJB%HZL)!3OxT{k$Yo-XrJ?|7{s4!H+S2o?N|^Z z)+?IE9H7h~Vxn5hTis^3wHYuOU84+bWd)cUKuHapq=&}WV#OxHpLab`NpwHm8LmOo zjri+!k;7j_?FP##CpM+pOVx*0wExEex z@`#)K<-ZrGyArK;a%Km`^+We|eT+#MygHOT6lXBmz`8|lyZOwL1+b+?Z$0OhMEp3R z&J=iRERpv~TC=p2-BYLC*?4 zxvPs9V@g=JT0>zky5Poj=fW_M!c)Xxz1<=&_ZcL=LMZJqlnO1P^xwGGW*Z+yTBvbV z-IFe6;(k1@$1;tS>{%pXZ_7w+i?N4A2=TXnGf=YhePg8bH8M|Lk-->+w8Y+FjZ;L=wSGwxfA`gqSn)f(XNuSm>6Y z@|#e-)I(PQ^G@N`%|_DZSb4_pkaEF0!-nqY+t#pyA>{9^*I-zw4SYA1_z2Bs$XGUZbGA;VeMo%CezHK0lO={L%G)dI-+8w?r9iexdoB{?l zbJ}C?huIhWXBVs7oo{!$lOTlvCLZ_KN1N+XJGuG$rh<^eUQIqcI7^pmqhBSaOKNRq zrx~w^?9C?*&rNwP_SPYmo;J-#!G|{`$JZK7DxsM3N^8iR4vvn>E4MU&Oe1DKJvLc~ zCT>KLZ1;t@My zRj_2hI^61T&LIz)S!+AQIV23n1>ng+LUvzv;xu!4;wpqb#EZz;F)BLUzT;8UA1x*6vJ zicB!3Mj03s*kGV{g`fpC?V^s(=JG-k1EMHbkdP4P*1^8p_TqO|;!Zr%GuP$8KLxuf z=pv*H;kzd;P|2`JmBt~h6|GxdU~@weK5O=X&5~w$HpfO}@l-T7@vTCxVOwCkoPQv8 z@aV_)I5HQtfs7^X=C03zYmH4m0S!V@JINm6#(JmZRHBD?T!m^DdiZJrhKpBcur2u1 zf9e4%k$$vcFopK5!CC`;ww(CKL~}mlxK_Pv!cOsFgVkNIghA2Au@)t6;Y3*2gK=5d z?|@1a)-(sQ%uFOmJ7v2iG&l&m^u&^6DJM#XzCrF%r>{2XKyxLD2rgWBD;i(!e4InDQBDg==^z;AzT2z~OmV0!?Z z0S9pX$+E;w3WN;v&NYT=+G8hf=6w0E1$0AOr61}eOvE8W1jX%>&Mjo7&!ulawgzLH zbcb+IF(s^3aj12WSi#pzIpijJJzkP?JzRawnxmNDSUR#7!29vHULCE<3Aa#be}ie~d|!V+ z%l~s9Odo$G&fH!t!+`rUT0T9DulF!Yq&BfQWFZV1L9D($r4H(}Gnf6k3^wa7g5|Ws zj7%d`!3(0bb55yhC6@Q{?H|2os{_F%o=;-h{@Yyyn*V7?{s%Grvpe!H^kl6tF4Zf5 z{Jv1~yZ*iIWL_9C*8pBMQArfJJ0d9Df6Kl#wa}7Xa#Ef_5B7=X}DzbQXVPfCwTO@9+@;A^Ti6il_C>g?A-GFwA0#U;t4;wOm-4oS})h z5&on>NAu67O?YCQr%7XIzY%LS4bha9*e*4bU4{lGCUmO2UQ2U)QOqClLo61Kx~3dI zmV3*(P6F_Tr-oP%x!0kTnnT?Ep5j;_IQ^pTRp=e8dmJtI4YgWd0}+b2=ATkOhgpXe z;jmw+FBLE}UIs4!&HflFr4)vMFOJ19W4f2^W(=2)F%TAL)+=F>IE$=e=@j-*bFLSg z)wf|uFQu+!=N-UzSef62u0-C8Zc7 zo6@F)c+nZA{H|+~7i$DCU0pL{0Ye|fKLuV^w!0Y^tT$isu%i1Iw&N|tX3kwFKJN(M zXS`k9js66o$r)x?TWL}Kxl`wUDUpwFx(w4Yk%49;$sgVvT~n8AgfG~HUcDt1TRo^s zdla@6heJB@JV z!vK;BUMznhzGK6PVtj0)GB=zTv6)Q9Yt@l#fv7>wKovLobMV-+(8)NJmyF8R zcB|_K7=FJGGn^X@JdFaat0uhKjp3>k#^&xE_}6NYNG?kgTp>2Iu?ElUjt4~E-?`Du z?mDCS9wbuS%fU?5BU@Ijx>1HG*N?gIP+<~xE4u=>H`8o((cS5M6@_OK%jSjFHirQK zN9@~NXFx*jS{<|bgSpC|SAnA@I)+GB=2W|JJChLI_mx+-J(mSJ!b)uUom6nH0#2^(L@JBlV#t zLl?j54s`Y3vE^c_3^Hl0TGu*tw_n?@HyO@ZrENxA+^!)OvUX28gDSF*xFtQzM$A+O zCG=n#6~r|3zt=8%GuG} z<#VCZ%2?3Q(Ad#Y7GMJ~{U3>E{5e@z6+rgZLX{Cxk^p-7dip^d29;2N1_mm4QkASo z-L`GWWPCq$uCo;X_BmGIpJFBlhl<8~EG{vOD1o|X$aB9KPhWO_cKiU*$HWEgtf=fn zsO%9bp~D2c@?*K9jVN@_vhR03>M_8h!_~%aN!Cnr?s-!;U3SVfmhRwk11A^8Ns`@KeE}+ zN$H}a1U6E;*j5&~Og!xHdfK5M<~xka)x-0N)K_&e7AjMz`toDzasH+^1bZlC!n()crk9kg@$(Y{wdKvbuUd04N^8}t1iOgsKF zGa%%XWx@WoVaNC1!|&{5ZbkopFre-Lu(LCE5HWZBoE#W@er9W<>R=^oYxBvypN#x3 zq#LC8&q)GFP=5^-bpHj?LW=)-g+3_)Ylps!3^YQ{9~O9&K)xgy zMkCWaApU-MI~e^cV{Je75Qr7eF%&_H)BvfyKL=gIA>;OSq(y z052BFz3E(Prg~09>|_Z@!qj}@;8yxnw+#Ej0?Rk<y}4ghbD569B{9hSFr*^ygZ zr6j7P#gtZh6tMk6?4V$*Jgz+#&ug;yOr>=qdI#9U&^am2qoh4Jy}H2%a|#Fs{E(5r z%!ijh;VuGA6)W)cJZx+;9Bp1LMUzN~x_8lQ#D3+sL{be-Jyeo@@dv7XguJ&S5vrH` z>QxOMWn7N-T!D@1(@4>ZlL^y5>m#0!HKovs12GRav4z!>p(1~xok8+_{| z#Ae4{9#NLh#Vj2&JuIn5$d6t@__`o}umFo(n0QxUtd2GKCyE+erwXY?`cm*h&^9*8 zJ+8x6fRZI-e$CRygofIQN^dWysCxgkyr{(_oBwwSRxZora1(%(aC!5BTtj^+YuevI zx?)H#(xlALUp6QJ!=l9N__$cxBZ5p&7;qD3PsXRFVd<({Kh+mShFWJNpy`N@ab7?9 zv5=klvCJ4bx|-pvOO2-+G)6O?$&)ncA#Urze2rlBfp#htudhx-NeRnJ@u%^_bfw4o z4|{b8SkPV3b>Wera1W(+N@p9H>dc6{cnkh-sgr?e%(YkWvK+0YXVwk0=d`)}*47*B z5JGkEdVix!w7-<%r0JF~`ZMMPe;f0EQHuYHxya`puazyph*ZSb1mJAt^k4549BfS; zK7~T&lRb=W{s&t`DJ$B}s-eH1&&-wEOH1KWsKn0a(ZI+G!v&W4A*cl>qAvUv6pbUR z#(f#EKV8~hk&8oayBz4vaswc(?qw1vn`yC zZQDl2PCB-&Uu@g9ZQHhO+v(W0bNig{-k0;;`+wM@#@J)8r?qOYs#&vUna8ILxN7S{ zp1s41KnR8miQJtJtOr|+qk}wrLt+N*z#5o`TmD1)E&QD(Vh&pjZJ_J*0!8dy_ z>^=@v=J)C`x&gjqAYu`}t^S=DFCtc0MkBU2zf|69?xW`Ck~(6zLD)gSE{7n~6w8j_ zoH&~$ED2k5-yRa0!r8fMRy z;QjBYUaUnpd}mf%iVFPR%Dg9!d>g`01m~>2s))`W|5!kc+_&Y>wD@@C9%>-lE`WB0 zOIf%FVD^cj#2hCkFgi-fgzIfOi+ya)MZK@IZhHT5FVEaSbv-oDDs0W)pA0&^nM0TW zmgJmd7b1R7b0a`UwWJYZXp4AJPteYLH>@M|xZFKwm!t3D3&q~av?i)WvAKHE{RqpD{{%OhYkK?47}+}` zrR2(Iv9bhVa;cDzJ%6ntcSbx7v7J@Y4x&+eWSKZ*eR7_=CVIUSB$^lfYe@g+p|LD{ zPSpQmxx@b$%d!05|H}WzBT4_cq?@~dvy<7s&QWtieJ9)hd4)$SZz}#H2UTi$CkFWW|I)v_-NjuH!VypONC=1`A=rm_jfzQ8Fu~1r8i{q-+S_j$ z#u^t&Xnfi5tZtl@^!fUJhx@~Cg0*vXMK}D{>|$#T*+mj(J_@c{jXBF|rm4-8%Z2o! z2z0o(4%8KljCm^>6HDK!{jI7p+RAPcty_~GZ~R_+=+UzZ0qzOwD=;YeZt*?3%UGdr z`c|BPE;yUbnyARUl&XWSNJ<+uRt%!xPF&K;(l$^JcA_CMH6)FZt{>6ah$|(9$2fc~ z=CD00uHM{qv;{Zk9FR0~u|3|Eiqv9?z2#^GqylT5>6JNZwKqKBzzQpKU2_pmtD;CT zi%Ktau!Y2Tldfu&b0UgmF(SSBID)15*r08eoUe#bT_K-G4VecJL2Pa=6D1K6({zj6 za(2Z{r!FY5W^y{qZ}08+h9f>EKd&PN90f}Sc0ejf%kB4+f#T8Q1=Pj=~#pi$U zp#5rMR%W25>k?<$;$x72pkLibu1N|jX4cWjD3q^Pk3js!uK6h7!dlvw24crL|MZs_ zb%Y%?Fyp0bY0HkG^XyS76Ts*|Giw{31LR~+WU5NejqfPr73Rp!xQ1mLgq@mdWncLy z%8}|nzS4P&`^;zAR-&nm5f;D-%yNQPwq4N7&yULM8bkttkD)hVU>h>t47`{8?n2&4 zjEfL}UEagLUYwdx0sB2QXGeRmL?sZ%J!XM`$@ODc2!y|2#7hys=b$LrGbvvjx`Iqi z&RDDm3YBrlKhl`O@%%&rhLWZ*ABFz2nHu7k~3@e4)kO3%$=?GEFUcCF=6-1n!x^vmu+Ai*amgXH+Rknl6U>#9w;A} zn2xanZSDu`4%%x}+~FG{Wbi1jo@wqBc5(5Xl~d0KW(^Iu(U3>WB@-(&vn_PJt9{1`e9Iic@+{VPc`vP776L*viP{wYB2Iff8hB%E3|o zGMOu)tJX!`qJ}ZPzq7>=`*9TmETN7xwU;^AmFZ-ckZjV5B2T09pYliaqGFY|X#E-8 z20b>y?(r-Fn5*WZ-GsK}4WM>@TTqsxvSYWL6>18q8Q`~JO1{vLND2wg@58OaU!EvT z1|o+f1mVXz2EKAbL!Q=QWQKDZpV|jznuJ}@-)1&cdo z^&~b4Mx{*1gurlH;Vhk5g_cM&6LOHS2 zRkLfO#HabR1JD4Vc2t828dCUG#DL}f5QDSBg?o)IYYi@_xVwR2w_ntlpAW0NWk$F1 z$If?*lP&Ka1oWfl!)1c3fl`g*lMW3JOn#)R1+tfwrs`aiFUgz3;XIJ>{QFxLCkK30 zNS-)#DON3yb!7LBHQJ$)4y%TN82DC2-9tOIqzhZ27@WY^<6}vXCWcR5iN{LN8{0u9 zNXayqD=G|e?O^*ms*4P?G%o@J1tN9_76e}E#66mr89%W_&w4n66~R;X_vWD(oArwj z4CpY`)_mH2FvDuxgT+akffhX0b_slJJ*?Jn3O3~moqu2Fs1oL*>7m=oVek2bnprnW zixkaIFU%+3XhNA@@9hyhFwqsH2bM|`P?G>i<-gy>NflhrN{$9?LZ1ynSE_Mj0rADF zhOz4FnK}wpLmQuV zgO4_Oz9GBu_NN>cPLA=`SP^$gxAnj;WjJnBi%Q1zg`*^cG;Q)#3Gv@c^j6L{arv>- zAW%8WrSAVY1sj$=umcAf#ZgC8UGZGoamK}hR7j6}i8#np8ruUlvgQ$j+AQglFsQQq zOjyHf22pxh9+h#n$21&$h?2uq0>C9P?P=Juw0|;oE~c$H{#RGfa>| zj)Iv&uOnaf@foiBJ}_;zyPHcZt1U~nOcNB{)og8Btv+;f@PIT*xz$x!G?u0Di$lo7 zOugtQ$Wx|C($fyJTZE1JvR~i7LP{ zbdIwqYghQAJi9p}V&$=*2Azev$6K@pyblphgpv8^9bN!?V}{BkC!o#bl&AP!3DAjM zmWFsvn2fKWCfjcAQmE+=c3Y7j@#7|{;;0f~PIodmq*;W9Fiak|gil6$w3%b_Pr6K_ zJEG@&!J%DgBZJDCMn^7mk`JV0&l07Bt`1ymM|;a)MOWz*bh2#d{i?SDe9IcHs7 zjCrnyQ*Y5GzIt}>`bD91o#~5H?4_nckAgotN{2%!?wsSl|LVmJht$uhGa+HiH>;av z8c?mcMYM7;mvWr6noUR{)gE!=i7cZUY7e;HXa221KkRoc2UB>s$Y(k%NzTSEr>W(u z<(4mcc)4rB_&bPzX*1?*ra%VF}P1nwiP5cykJ&W{!OTlz&Td0pOkVp+wc z@k=-Hg=()hNg=Q!Ub%`BONH{ z_=ZFgetj@)NvppAK2>8r!KAgi>#%*7;O-o9MOOfQjV-n@BX6;Xw;I`%HBkk20v`qoVd0)}L6_49y1IhR z_OS}+eto}OPVRn*?UHC{eGyFU7JkPz!+gX4P>?h3QOwGS63fv4D1*no^6PveUeE5% zlehjv_3_^j^C({a2&RSoVlOn71D8WwMu9@Nb@=E_>1R*ve3`#TF(NA0?d9IR_tm=P zOP-x;gS*vtyE1Cm zG0L?2nRUFj#aLr-R1fX*$sXhad)~xdA*=hF3zPZhha<2O$Ps+F07w*3#MTe?)T8|A!P!v+a|ot{|^$q(TX`35O{WI0RbU zCj?hgOv=Z)xV?F`@HKI11IKtT^ocP78cqHU!YS@cHI@{fPD?YXL)?sD~9thOAv4JM|K8OlQhPXgnevF=F7GKD2#sZW*d za}ma31wLm81IZxX(W#A9mBvLZr|PoLnP>S4BhpK8{YV_}C|p<)4#yO{#ISbco92^3 zv&kCE(q9Wi;9%7>>PQ!zSkM%qqqLZW7O`VXvcj;WcJ`2~v?ZTYB@$Q&^CTfvy?1r^ z;Cdi+PTtmQwHX_7Kz?r#1>D zS5lWU(Mw_$B&`ZPmqxpIvK<~fbXq?x20k1~9az-Q!uR78mCgRj*eQ>zh3c$W}>^+w^dIr-u{@s30J=)1zF8?Wn|H`GS<=>Om|DjzC{}Jt?{!fSJe*@$H zg>wFnlT)k#T?LslW zu$^7Uy~$SQ21cE?3Ijl+bLfuH^U5P^$@~*UY#|_`uvAIe(+wD2eF}z_y!pvomuVO; zS^9fbdv)pcm-B@CW|Upm<7s|0+$@@<&*>$a{aW+oJ%f+VMO<#wa)7n|JL5egEgoBv zl$BY(NQjE0#*nv=!kMnp&{2Le#30b)Ql2e!VkPLK*+{jv77H7)xG7&=aPHL7LK9ER z5lfHxBI5O{-3S?GU4X6$yVk>lFn;ApnwZybdC-GAvaznGW-lScIls-P?Km2mF>%B2 zkcrXTk+__hj-3f48U%|jX9*|Ps41U_cd>2QW81Lz9}%`mTDIhE)jYI$q$ma7Y-`>% z8=u+Oftgcj%~TU}3nP8&h7k+}$D-CCgS~wtWvM|UU77r^pUw3YCV80Ou*+bH0!mf0 zxzUq4ed6y>oYFz7+l18PGGzhB^pqSt)si=9M>~0(Bx9*5r~W7sa#w+_1TSj3Jn9mW zMuG9BxN=}4645Cpa#SVKjFst;9UUY@O<|wpnZk$kE+to^4!?0@?Cwr3(>!NjYbu?x z1!U-?0_O?k!NdM^-rIQ8p)%?M+2xkhltt*|l=%z2WFJhme7*2xD~@zk#`dQR$6Lmd zb3LOD4fdt$Cq>?1<%&Y^wTWX=eHQ49Xl_lFUA(YQYHGHhd}@!VpYHHm=(1-O=yfK#kKe|2Xc*9}?BDFN zD7FJM-AjVi)T~OG)hpSWqH>vlb41V#^G2B_EvYlWhDB{Z;Q9-0)ja(O+By`31=biA zG&Fs#5!%_mHi|E4Nm$;vVQ!*>=_F;ZC=1DTPB#CICS5fL2T3XmzyHu?bI;m7D4@#; ztr~;dGYwb?m^VebuULtS4lkC_7>KCS)F@)0OdxZIFZp@FM_pHnJes8YOvwB|++#G( z&dm*OP^cz95Wi15vh`Q+yB>R{8zqEhz5of>Po$9LNE{xS<)lg2*roP*sQ}3r3t<}; zPbDl{lk{pox~2(XY5=qg0z!W-x^PJ`VVtz$git7?)!h>`91&&hESZy1KCJ2nS^yMH z!=Q$eTyRi68rKxdDsdt+%J_&lapa{ds^HV9Ngp^YDvtq&-Xp}60B_w@Ma>_1TTC;^ zpbe!#gH}#fFLkNo#|`jcn?5LeUYto%==XBk6Ik0kc4$6Z+L3x^4=M6OI1=z5u#M%0 z0E`kevJEpJjvvN>+g`?gtnbo$@p4VumliZV3Z%CfXXB&wPS^5C+7of2tyVkMwNWBiTE2 z8CdPu3i{*vR-I(NY5syRR}I1TJOV@DJy-Xmvxn^IInF>Tx2e)eE9jVSz69$6T`M9-&om!T+I znia!ZWJRB28o_srWlAxtz4VVft8)cYloIoVF=pL zugnk@vFLXQ_^7;%hn9x;Vq?lzg7%CQR^c#S)Oc-8d=q_!2ZVH764V z!wDKSgP}BrVV6SfCLZnYe-7f;igDs9t+K*rbMAKsp9L$Kh<6Z;e7;xxced zn=FGY<}CUz31a2G}$Q(`_r~75PzM4l_({Hg&b@d8&jC}B?2<+ed`f#qMEWi z`gm!STV9E4sLaQX+sp5Nu9*;9g12naf5?=P9p@H@f}dxYprH+3ju)uDFt^V{G0APn zS;16Dk{*fm6&BCg#2vo?7cbkkI4R`S9SSEJ=#KBk3rl69SxnCnS#{*$!^T9UUmO#&XXKjHKBqLdt^3yVvu8yn|{ zZ#%1CP)8t-PAz(+_g?xyq;C2<9<5Yy<~C74Iw(y>uUL$+$mp(DRcCWbCKiGCZw@?_ zdomfp+C5xt;j5L@VfhF*xvZdXwA5pcdsG>G<8II-|1dhAgzS&KArcb0BD4ZZ#WfiEY{hkCq5%z9@f|!EwTm;UEjKJsUo696V>h zy##eXYX}GUu%t{Gql8vVZKkNhQeQ4C%n|RmxL4ee5$cgwlU+?V7a?(jI#&3wid+Kz5+x^G!bb#$q>QpR#BZ}Xo5UW^ zD&I`;?(a}Oys7-`I^|AkN?{XLZNa{@27Dv^s4pGowuyhHuXc zuctKG2x0{WCvg_sGN^n9myJ}&FXyGmUQnW7fR$=bj$AHR88-q$D!*8MNB{YvTTEyS zn22f@WMdvg5~o_2wkjItJN@?mDZ9UUlat2zCh(zVE=dGi$rjXF7&}*sxac^%HFD`Y zTM5D3u5x**{bW!68DL1A!s&$2XG@ytB~dX-?BF9U@XZABO`a|LM1X3HWCllgl0+uL z04S*PX$%|^WAq%jkzp~%9HyYIF{Ym?k)j3nMwPZ=hlCg9!G+t>tf0o|J2%t1 ztC+`((dUplgm3`+0JN~}&FRRJ3?l*>Y&TfjS>!ShS`*MwO{WIbAZR#<%M|4c4^dY8 z{Rh;-!qhY=dz5JthbWoovLY~jNaw>%tS4gHVlt5epV8ekXm#==Po$)}mh^u*cE>q7*kvX&gq)(AHoItMYH6^s6f(deNw%}1=7O~bTHSj1rm2|Cq+3M z93djjdomWCTCYu!3Slx2bZVy#CWDozNedIHbqa|otsUl+ut?>a;}OqPfQA05Yim_2 zs@^BjPoFHOYNc6VbNaR5QZfSMh2S*`BGwcHMM(1@w{-4jVqE8Eu0Bi%d!E*^Rj?cR z7qgxkINXZR)K^=fh{pc0DCKtrydVbVILI>@Y0!Jm>x-xM!gu%dehm?cC6ok_msDVA*J#{75%4IZt}X|tIVPReZS#aCvuHkZxc zHVMtUhT(wp09+w9j9eRqz~LtuSNi2rQx_QgQ(}jBt7NqyT&ma61ldD(s9x%@q~PQl zp6N*?=N$BtvjQ_xIT{+vhb1>{pM0Arde0!X-y))A4znDrVx8yrP3B1(7bKPE5jR@5 zwpzwT4cu~_qUG#zYMZ_!2Tkl9zP>M%cy>9Y(@&VoB84#%>amTAH{(hL4cDYt!^{8L z645F>BWO6QaFJ-{C-i|-d%j7#&7)$X7pv#%9J6da#9FB5KyDhkA+~)G0^87!^}AP>XaCSScr;kL;Z%RSPD2CgoJ;gpYT5&6NUK$86$T?jRH=w8nI9Z534O?5fk{kd z`(-t$8W|#$3>xoMfXvV^-A(Q~$8SKDE^!T;J+rQXP71XZ(kCCbP%bAQ1|%$%Ov9_a zyC`QP3uPvFoBqr_+$HenHklqyIr>PU_Fk5$2C+0eYy^~7U&(!B&&P2%7#mBUhM!z> z_B$Ko?{Pf6?)gpYs~N*y%-3!1>o-4;@1Zz9VQHh)j5U1aL-Hyu@1d?X;jtDBNk*vMXPn@ z+u@wxHN*{uHR!*g*4Xo&w;5A+=Pf9w#PeZ^x@UD?iQ&${K2c}UQgLRik-rKM#Y5rdDphdcNTF~cCX&9ViRP}`>L)QA4zNXeG)KXFzSDa6 zd^St;inY6J_i=5mcGTx4_^Ys`M3l%Q==f>{8S1LEHn{y(kbxn5g1ezt4CELqy)~TV6{;VW>O9?5^ ztcoxHRa0jQY7>wwHWcxA-BCwzsP>63Kt&3fy*n#Cha687CQurXaRQnf5wc9o8v7Rw zNwGr2fac;Wr-Ldehn7tF^(-gPJwPt@VR1f;AmKgxN&YPL;j=0^xKM{!wuU|^mh3NE zy35quf}MeL!PU;|{OW_x$TBothLylT-J>_x6p}B_jW1L>k)ps6n%7Rh z96mPkJIM0QFNYUM2H}YF5bs%@Chs6#pEnloQhEl?J-)es!(SoJpEPoMTdgA14-#mC zghayD-DJWtUu`TD8?4mR)w5E`^EHbsz2EjH5aQLYRcF{l7_Q5?CEEvzDo(zjh|BKg z3aJl_n#j&eFHsUw4~lxqnr!6NL*se)6H=A+T1e3xUJGQrd}oSPwSy5+$tt{2t5J5@(lFxl43amsARG74iyNC}uuS zd2$=(r6RdamdGx^eatX@F2D8?U23tDpR+Os?0Gq2&^dF+$9wiWf?=mDWfjo4LfRwL zI#SRV9iSz>XCSgEj!cW&9H-njJopYiYuq|2w<5R2!nZ27DyvU4UDrHpoNQZiGPkp@ z1$h4H46Zn~eqdj$pWrv;*t!rTYTfZ1_bdkZmVVIRC21YeU$iS-*XMNK`#p8Z_DJx| zk3Jssf^XP7v0X?MWFO{rACltn$^~q(M9rMYoVxG$15N;nP)A98k^m3CJx8>6}NrUd@wp-E#$Q0uUDQT5GoiK_R{ z<{`g;8s>UFLpbga#DAf%qbfi`WN1J@6IA~R!YBT}qp%V-j!ybkR{uY0X|x)gmzE0J z&)=eHPjBxJvrZSOmt|)hC+kIMI;qgOnuL3mbNR0g^<%|>9x7>{}>a2qYSZAGPt4it?8 zNcLc!Gy0>$jaU?}ZWxK78hbhzE+etM`67*-*x4DN>1_&{@5t7_c*n(qz>&K{Y?10s zXsw2&nQev#SUSd|D8w7ZD2>E<%g^; zV{yE_O}gq?Q|zL|jdqB^zcx7vo(^})QW?QKacx$yR zhG|XH|8$vDZNIfuxr-sYFR{^csEI*IM#_gd;9*C+SysUFejP0{{z7@P?1+&_o6=7V|EJLQun^XEMS)w(=@eMi5&bbH*a0f;iC~2J74V2DZIlLUHD&>mlug5+v z6xBN~8-ovZylyH&gG#ptYsNlT?-tzOh%V#Y33zlsJ{AIju`CjIgf$@gr8}JugRq^c zAVQ3;&uGaVlVw}SUSWnTkH_6DISN&k2QLMBe9YU=sA+WiX@z)FoSYX`^k@B!j;ZeC zf&**P?HQG6Rk98hZ*ozn6iS-dG}V>jQhb3?4NJB*2F?6N7Nd;EOOo;xR7acylLaLy z9)^lykX39d@8@I~iEVar4jmjjLWhR0d=EB@%I;FZM$rykBNN~jf>#WbH4U{MqhhF6 zU??@fSO~4EbU4MaeQ_UXQcFyO*Rae|VAPLYMJEU`Q_Q_%s2*>$#S^)&7er+&`9L=1 z4q4ao07Z2Vsa%(nP!kJ590YmvrWg+YrgXYs_lv&B5EcoD`%uL79WyYA$0>>qi6ov7 z%`ia~J^_l{p39EY zv>>b}Qs8vxsu&WcXEt8B#FD%L%ZpcVtY!rqVTHe;$p9rbb5O{^rFMB>auLn-^;s+-&P1#h~mf~YLg$8M9 zZ4#87;e-Y6x6QO<{McUzhy(%*6| z)`D~A(TJ$>+0H+mct(jfgL4x%^oC^T#u(bL)`E2tBI#V1kSikAWmOOYrO~#-cc_8! zCe|@1&mN2{*ceeiBldHCdrURk4>V}79_*TVP3aCyV*5n@jiNbOm+~EQ_}1#->_tI@ zqXv+jj2#8xJtW508rzFrYcJxoek@iW6SR@1%a%Bux&;>25%`j3UI`0DaUr7l79`B1 zqqUARhW1^h6=)6?;@v>xrZNM;t}{yY3P@|L}ey@gG( z9r{}WoYN(9TW&dE2dEJIXkyHA4&pU6ki=rx&l2{DLGbVmg4%3Dlfvn!GB>EVaY_%3+Df{fBiqJV>~Xf8A0aqUjgpa} zoF8YXO&^_x*Ej}nw-$-F@(ddB>%RWoPUj?p8U{t0=n>gAI83y<9Ce@Q#3&(soJ{64 z37@Vij1}5fmzAuIUnXX`EYe;!H-yTVTmhAy;y8VZeB#vD{vw9~P#DiFiKQ|kWwGFZ z=jK;JX*A;Jr{#x?n8XUOLS;C%f|zj-7vXtlf_DtP7bpurBeX%Hjwr z4lI-2TdFpzkjgiv!8Vfv`=SP+s=^i3+N~1ELNWUbH|ytVu>EyPN_3(4TM^QE1swRo zoV7Y_g)a>28+hZG0e7g%@2^s>pzR4^fzR-El}ARTmtu!zjZLuX%>#OoU3}|rFjJg} zQ2TmaygxJ#sbHVyiA5KE+yH0LREWr%^C*yR|@gM$nK2P zo}M}PV0v))uJh&33N>#aU376@ZH79u(Yw`EQ2hM3SJs9f99+cO6_pNW$j$L-CtAfe zYfM)ccwD!P%LiBk!eCD?fHCGvgMQ%Q2oT_gmf?OY=A>&PaZQOq4eT=lwbaf}33LCH zFD|)lu{K7$8n9gX#w4~URjZxWm@wlH%oL#G|I~Fb-v^0L0TWu+`B+ZG!yII)w05DU z>GO?n(TN+B=>HdxVDSlIH76pta$_LhbBg;eZ`M7OGcqt||qi zogS72W1IN%=)5JCyOHWoFP7pOFK0L*OAh=i%&VW&4^LF@R;+K)t^S!96?}^+5QBIs zjJNTCh)?)4k^H^g1&jc>gysM`y^8Rm3qsvkr$9AeWwYpa$b22=yAd1t<*{ zaowSEFP+{y?Ob}8&cwfqoy4Pb9IA~VnM3u!trIK$&&0Op#Ql4j>(EW?UNUv#*iH1$ z^j>+W{afcd`{e&`-A{g}{JnIzYib)!T56IT@YEs{4|`sMpW3c8@UCoIJv`XsAw!XC z34|Il$LpW}CIHFC5e*)}00I5{%OL*WZRGzC0?_}-9{#ue?-ug^ zLE|uv-~6xnSs_2_&CN9{9vyc!Xgtn36_g^wI0C4s0s^;8+p?|mm;Odt3`2ZjwtK;l zfd6j)*Fr#53>C6Y8(N5?$H0ma;BCF3HCjUs7rpb2Kf*x3Xcj#O8mvs#&33i+McX zQpBxD8!O{5Y8D&0*QjD=Yhl9%M0)&_vk}bmN_Ud^BPN;H=U^bn&(csl-pkA+GyY0Z zKV7sU_4n;}uR78ouo8O%g*V;79KY?3d>k6%gpcmQsKk&@Vkw9yna_3asGt`0Hmj59 z%0yiF*`jXhByBI9QsD=+>big5{)BGe&+U2gAARGe3ID)xrid~QN_{I>k}@tzL!Md_ z&=7>TWciblF@EMC3t4-WX{?!m!G6$M$1S?NzF*2KHMP3Go4=#ZHkeIv{eEd;s-yD# z_jU^Ba06TZqvV|Yd;Z_sN%$X=!T+&?#p+OQIHS%!LO`Hx0q_Y0MyGYFNoM{W;&@0@ zLM^!X4KhdtsET5G<0+|q0oqVXMW~-7LW9Bg}=E$YtNh1#1D^6Mz(V9?2g~I1( zoz9Cz=8Hw98zVLwC2AQvp@pBeKyidn6Xu0-1SY1((^Hu*-!HxFUPs)yJ+i`^BC>PC zjwd0mygOVK#d2pRC9LxqGc6;Ui>f{YW9Bvb>33bp^NcnZoH~w9(lM5@JiIlfa-6|k ziy31UoMN%fvQfhi8^T+=yrP{QEyb-jK~>$A4SZT-N56NYEbpvO&yUme&pWKs3^94D zH{oXnUTb3T@H+RgzML*lejx`WAyw*?K7B-I(VJx($2!NXYm%3`=F~TbLv3H<{>D?A zJo-FDYdSA-(Y%;4KUP2SpHKAIcv9-ld(UEJE7=TKp|Gryn;72?0LHqAN^fk6%8PCW z{g_-t)G5uCIf0I`*F0ZNl)Z>))MaLMpXgqWgj-y;R+@A+AzDjsTqw2Mo9ULKA3c70 z!7SOkMtZb+MStH>9MnvNV0G;pwSW9HgP+`tg}e{ij0H6Zt5zJ7iw`hEnvye!XbA@!~#%vIkzowCOvq5I5@$3wtc*w2R$7!$*?}vg4;eDyJ_1=ixJuEp3pUS27W?qq(P^8$_lU!mRChT}ctvZz4p!X^ zOSp|JOAi~f?UkwH#9k{0smZ7-#=lK6X3OFEMl7%)WIcHb=#ZN$L=aD`#DZKOG4p4r zwlQ~XDZ`R-RbF&hZZhu3(67kggsM-F4Y_tI^PH8PMJRcs7NS9ogF+?bZB*fcpJ z=LTM4W=N9yepVvTj&Hu~0?*vR1HgtEvf8w%Q;U0^`2@e8{SwgX5d(cQ|1(!|i$km! zvY03MK}j`sff;*-%mN~ST>xU$6Bu?*Hm%l@0dk;j@%>}jsgDcQ)Hn*UfuThz9(ww_ zasV`rSrp_^bp-0sx>i35FzJwA!d6cZ5#5#nr@GcPEjNnFHIrtUYm1^Z$;{d&{hQV9 z6EfFHaIS}46p^5I-D_EcwwzUUuO}mqRh&T7r9sfw`)G^Q%oHxEs~+XoM?8e*{-&!7 z7$m$lg9t9KP9282eke608^Q2E%H-xm|oJ8=*SyEo} z@&;TQ3K)jgspgKHyGiKVMCz>xmC=H5Fy3!=TP)-R3|&1S-B)!6q50wfLHKM@7Bq6E z44CY%G;GY>tC`~yh!qv~YdXw! zSkquvYNs6k1r7>Eza?Vkkxo6XRS$W7EzL&A`o>=$HXgBp{L(i^$}t`NcnAxzbH8Ht z2!;`bhKIh`f1hIFcI5bHI=ueKdzmB9)!z$s-BT4ItyY|NaA_+o=jO%MU5as9 zc2)aLP>N%u>wlaXTK!p)r?+~)L+0eCGb5{8WIk7K52$nufnQ+m8YF+GQc&{^(zh-$ z#wyWV*Zh@d!b(WwXqvfhQX)^aoHTBkc;4ossV3&Ut*k>AI|m+{#kh4B!`3*<)EJVj zwrxK>99v^k4&Y&`Awm>|exo}NvewV%E+@vOc>5>%H#BK9uaE2$vje zWYM5fKuOTtn96B_2~~!xJPIcXF>E_;yO8AwpJ4)V`Hht#wbO3Ung~@c%%=FX4)q+9 z99#>VC2!4l`~0WHs9FI$Nz+abUq# zz`Of97})Su=^rGp2S$)7N3rQCj#0%2YO<R&p>$<#lgXcUj=4H_{oAYiT3 z44*xDn-$wEzRw7#@6aD)EGO$0{!C5Z^7#yl1o;k0PhN=aVUQu~eTQ^Xy{z8Ow6tk83 z4{5xe%(hx)%nD&|e*6sTWH`4W&U!Jae#U4TnICheJmsw{l|CH?UA{a6?2GNgpZLyzU2UlFu1ZVwlALmh_DOs03J^Cjh1im`E3?9&zvNmg(MuMw&0^Lu$(#CJ*q6DjlKsY-RMJ^8yIY|{SQZ*9~CH|u9L z`R78^r=EbbR*_>5?-)I+$6i}G)%mN(`!X72KaV(MNUP7Nv3MS9S|Pe!%N2AeOt5zG zVJ;jI4HZ$W->Ai_4X+`9c(~m=@ek*m`ZQbv3ryI-AD#AH=`x$~WeW~M{Js57(K7(v ze5`};LG|%C_tmd>bkufMWmAo&B+DT9ZV~h(4jg0>^aeAqL`PEUzJJtI8W1M!bQWpv zvN(d}E1@nlYa!L!!A*RN!(Q3F%J?5PvQ0udu?q-T)j3JKV~NL>KRb~w-lWc685uS6 z=S#aR&B8Sc8>cGJ!!--?kwsJTUUm`Jk?7`H z7PrO~xgBrSW2_tTlCq1LH8*!o?pj?qxy8}(=r_;G18POrFh#;buWR0qU24+XUaVZ0 z?(sXcr@-YqvkCmHr{U2oPogHL{r#3r49TeR<{SJX1pcUqyWPrkYz^X8#QW~?F)R5i z>p^!i<;qM8Nf{-fd6!_&V*e_9qP6q(s<--&1Ttj01j0w>bXY7y1W*%Auu&p|XSOH=)V7Bd4fUKh&T1)@cvqhuD-d=?w}O zjI%i(f|thk0Go*!d7D%0^ztBfE*V=(ZIN84f5HU}T9?ulmEYzT5usi=DeuI*d|;M~ zp_=Cx^!4k#=m_qSPBr5EK~E?3J{dWWPH&oCcNepYVqL?nh4D5ynfWip$m*YlZ8r^Z zuFEUL-nW!3qjRCLIWPT0x)FDL7>Yt7@8dA?R2kF@WE>ysMY+)lTsgNM#3VbXVGL}F z1O(>q>2a+_`6r5Xv$NZAnp=Kgnr3)cL(^=8ypEeOf3q8(HGe@7Tt59;yFl||w|mnO zHDxg2G3z8=(6wjj9kbcEY@Z0iOd7Gq5GiPS5% z*sF1J<#daxDV2Z8H>wxOF<;yKzMeTaSOp_|XkS9Sfn6Mpe9UBi1cSTieGG5$O;ZLIIJ60Y>SN4vC?=yE_CWlo(EEE$e4j?z&^FM%kNmRtlbEL^dPPgvs9sbK5fGw*r@ z+!EU@u$T8!nZh?Fdf_qk$VuHk^yVw`h`_#KoS*N%epIIOfQUy_&V}VWDGp3tplMbf z5Se1sJUC$7N0F1-9jdV2mmGK{-}fu|Nv;12jDy0<-kf^AmkDnu6j~TPWOgy1MT68|D z=4=50jVbUKdKaQgD`eWGr3I&^<6uhkjz$YwItY8%Yp9{z4-{6g{73<_b*@XJ4Nm3-3z z?BW3{aY_ccRjb@W1)i5nLg|7BnWS!B`_Uo9CWaE`Ij327QH?i)9A}4Ug4wmxVVa^b z-4+m%-wwOl7cKH7+=x&nrCrbEC)Q$fpg&V83#uEH;C=GNMz`ps@^RxK%T*8%OPnC` z{WO~J%nxYJ`x|N%?&i7?;{_8t^jM&=50HlaOQj8fS}_`moH$c;vI<|cruPFnpT8yU zS%rPOCUSd5Zdb(zwk`hqwTQn)*&n)uYsP*F_(~xEWq}C= zv30kFmZFwJZ@ELVX3?$dXQh|icO7UrL*_5G=I^xXjImz`ZPp>?g#tf(ej~KaIU0algsG!IS09;>?MvqGg#c{i+}qY|{P8W~O%#>|gFd z<1dr$-oxyRGN17yZo1OwLnzwYs0|;IS_nymNB0IlSzPQ%-r`?T=;_XQ^~&#}b|AB} zkNbN5uB?-sUB-T5QLlg%Uk3)uHB;>VIzGe9_J9 zaeISkQm!v(9d(0ML^b9fR^sfHFlH?7Mvddt37OuR{|O0{uv)(&-6<87W4 zyO>s!=cPgP3O&7xxU5DlIPw_o3O>6o6Qb?JWs3qw#p3sBc3g$?Dx zi(6D+DYgV;GrUis-CL%Qe{nvZnwaVXmbhH(|GFh|Q)k=1uvA$I@1DXI7bKlQ@8D6P zS?(*?><>)G49q0wr;NajpxP4W2G)kHl6^=Z>hrNEI4Mwd_$O6$1dXF;Q#hE(-eeW6 zz03GJF%Wl?HO=_ztv5*zRlcU~{+{k%#N59mgm~eK>P!QZ6E?#Cu^2)+K8m@ySvZ*5 z|HDT}BkF@3!l(0%75G=1u2hETXEj!^1Z$!)!lyGXlWD!_vqGE$Z)#cUVBqlORW>0^ zDjyVTxwKHKG|0}j-`;!R-p>}qQfBl(?($7pP<+Y8QE#M8SCDq~k<+>Q^Zf@cT_WdX3~BSe z+|KK|7OL5Hm5(NFP~j>Ct3*$wi0n0!xl=(C61`q&cec@mFlH(sy%+RH<=s)8aAPN`SfJdkAQjdv82G5iRdv8 zh{9wHUZaniSEpslXl^_ODh}mypC?b*9FzLjb~H@3DFSe;D(A-K3t3eOTB(m~I6C;(-lKAvit(70k`%@+O*Ztdz;}|_TS~B?Tpmi=QKC^m_ z2YpEaT3iiz*;T~ap1yiA)a`dKMwu`^UhIUeltNQ1Yjo=q@bI@&3zH?rVUg=IxLy-ni zyxDu%-Fr{H6owTjZU2O5>nDb=q&Jz_TjeSq%!2m40x&U6w~GQ({quPL73IsJS;f`$ zsuhioqCBj(gJ>2hoo)Gou7(WP*pX)f=Y=!=k!&1K?EYY%jJ~X&DnK{^saPQK<1BJ z_A`_{%ZozcB(3w$z^To^6d|XuT@=X~wtW!+{4ID@N{AB~J6AL5vuY>JwvWCNFKsKh zd}@>q@_WV#QZ&UJ0#?X(pXR!oyXOEG3rqzHbCzGLONDb042i$})fM@XF)uSP(DHUc z^&{|$*xe{cs?Gp8=B%RY3L7#$ve$?TWh>MZdxF1zH1v}1z+$Ov#G7?%D)bBCyDe*% zSeKSpETC2V1){II>@UwJi>4uBN+iAx+82E~gb|Cr&8E^i&)A!uv-g?jzH99wU}8+# z$nh>yvb;TwZmS@7LrvuCu_d0-WxFNI&C7%sWuTL%YU!l|I1{|->=dlOeHOCtUO#zkS3ESO8LHV4hTdQL5EdV zuWD33fFPH}HPrW^s$Qn1Xgp&AT6<-He{{4%eIu3rN=iK|9mURdKXfB&Q?qGok%!cs ze53UP{Z!TO-Y@q2;;k2avA3`lm4OoN4@S*k=UA)7H;qZ`d8`XaYFCv?Ba+uGW@r5v z&&{nf(24WSBOhc7!qF^@0cz;XcUynNaj6w2349;s!K{KVqs5yS{ z7VubS`2OzT^5#1~6Tt^RTvt9-J|D2F>y~>2;jeF>g`hx5l%B3H=aLExQihuYngzlnBTYOTHJQMzl>kwqN5JYs)Ej zblA@ntkUS~xi+}y6|(81helS}Q~&VB37qyV|S3Y=><^1wh%msQM?fz z<58MX(=|PSUKCF#)dbhR%D&xgCD?$aR0qen+wpp6 zst}vX18!Be96TD??j1HsHTUx(a&@F?=gT`Q$oJFFyrh^;zgz!(NlAHGn0cJy@us=w zNhC#l5G;H}+>49Nsh12=ZPO2r*2OBQe5kpb&1?*PIBFitK8}FUfb~S-#hKfF0o#&d z#3aPkB$9scYku&kA6{0xHnBV#&Wei5J>5T-XX-gUXEPo+9b7WL=*XESc(3BshL`aj zXp}QIp*40}oWJt*l043e8_5;H5PI5c)U&IEw5dF(4zjX0y_lk9 zAp@!mK>WUqHo)-jop=DoK>&no>kAD=^qIE7qis&_*4~ z6q^EF$D@R~3_xseCG>Ikb6Gfofb$g|75PPyyZN&tiRxqovo_k zO|HA|sgy#B<32gyU9x^&)H$1jvw@qp+1b(eGAb)O%O!&pyX@^nQd^9BQ4{(F8<}|A zhF&)xusQhtoXOOhic=8#Xtt5&slLia3c*a?dIeczyTbC#>FTfiLST57nc3@Y#v_Eg#VUv zT8cKH#f3=1PNj!Oroz_MAR*pow%Y0*6YCYmUy^7`^r|j23Q~^*TW#cU7CHf0eAD_0 zEWEVddxFgQ7=!nEBQ|ibaScslvhuUk^*%b#QUNrEB{3PG@uTxNwW}Bs4$nS9wc(~O zG7Iq>aMsYkcr!9#A;HNsJrwTDYkK8ikdj{M;N$sN6BqJ<8~z>T20{J8Z2rRUuH7~3 z=tgS`AgxbBOMg87UT4Lwge`*Y=01Dvk>)^{Iu+n6fuVX4%}>?3czOGR$0 zpp*wp>bsFFSV`V;r_m+TZns$ZprIi`OUMhe^cLE$2O+pP3nP!YB$ry}2THx2QJs3< za1;>d-AggCarrQ>&Z!d@;mW+!q6eXhb&`GbzUDSxpl8AJ#Cm#tuc)_xh(2NV=5XMs zrf_ozRYO$NkC=pKFX5OH8v1>0i9Z$ec`~Mf+_jQ68spn(CJwclDhEEkH2Qw;${J$clv__nUjn5jA0wCLEnu1j;v!0vB>Ri6m9`;R{JMS%^)4FC zU0Z44+u$I$w=Bj|iu4DT5h~sS`C*zbmX?@-crY}E+hy>}2~C0Nn(EKk@5^qO4@l@! z6O0lr%tzGC`D^)8xU3FnMZVm0kX1sBWhaQyzVoXFWwr%Ny?=2M{5s#5i7fTu3gEkG zc{(Pr$v=;`Y#&`y*J}#M9ux>0?xu!`$9cUKm#Bdd_&S#LPTS?ZPV6zN6>W6JTS~-LfjL{mB=b(KMk3 z2HjBSlJeyUVqDd=Mt!=hpYsvby2GL&3~zm;0{^nZJq+4vb?5HH4wufvr}IX42sHeK zm@x?HN$8TsTavXs)tLDFJtY9b)y~Tl@7z4^I8oUQq4JckH@~CVQ;FoK(+e0XAM>1O z(ei}h?)JQp>)d=6ng-BZF1Z5hsAKW@mXq+hU?r8I(*%`tnIIOXw7V6ZK(T9RFJJe@ zZS!aC+p)Gf2Ujc=a6hx4!A1Th%YH!Lb^xpI!Eu` zmJO{9rw){B1Ql18d%F%da+Tbu1()?o(zT7StYqK6_w`e+fjXq5L^y(0 z09QA6H4oFj59c2wR~{~>jUoDzDdKz}5#onYPJRwa`SUO)Pd4)?(ENBaFVLJr6Kvz= zhTtXqbx09C1z~~iZt;g^9_2nCZ{};-b4dQJbv8HsWHXPVg^@(*!@xycp#R?a|L!+` zY5w))JWV`Gls(=}shH0#r*;~>_+-P5Qc978+QUd>J%`fyn{*TsiG-dWMiJXNgwBaT zJ=wgYFt+1ACW)XwtNx)Q9tA2LPoB&DkL16P)ERWQlY4%Y`-5aM9mZ{eKPUgI!~J3Z zkMd5A_p&v?V-o-6TUa8BndiX?ooviev(DKw=*bBVOW|=zps9=Yl|-R5@yJe*BPzN}a0mUsLn{4LfjB_oxpv(mwq# zSY*%E{iB)sNvWfzg-B!R!|+x(Q|b@>{-~cFvdDHA{F2sFGA5QGiIWy#3?P2JIpPKg6ncI^)dvqe`_|N=8 '} + 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/Dokka-plugin-kdoc2json/pebble-renderer/gradlew.bat b/Dokka-plugin-kdoc2json/pebble-renderer/gradlew.bat new file mode 100644 index 00000000..aa5f10b0 --- /dev/null +++ b/Dokka-plugin-kdoc2json/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/Dokka-plugin-kdoc2json/pebble-renderer/render.sh b/Dokka-plugin-kdoc2json/pebble-renderer/render.sh new file mode 100755 index 00000000..627aac7e --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/render.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Renders a javadoc-mode JSON tree to browsable HTML. +# +# ./render.sh +# +# Builds the renderer if needed, then walks the JSON tree writing one .html per .json at the same +# relative path. Because the trees mirror each other file-for-file, the relative links already in +# the JSON resolve as soon as their .json extension is swapped for .html, which is what the +# templates' `href` and `doc` filters do. +set -euo pipefail + +if [ $# -lt 2 ]; then + echo "Usage: $0 " >&2 + echo >&2 + echo "Example, after scripts/java/build-jdk-json-docs.sh:" >&2 + echo " $0 ../scripts/java/build-output/api ../scripts/java/build-output/html" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +JSON_DIR="$(cd "$1" && pwd)" +mkdir -p "$2" +HTML_DIR="$(cd "$2" && pwd)" + +echo "==> Building the renderer" +(cd "$SCRIPT_DIR" && ./gradlew --console=plain -q installDist) + +echo "==> Rendering $JSON_DIR -> $HTML_DIR" +"$SCRIPT_DIR/build/install/pebble-renderer/bin/pebble-renderer" "$JSON_DIR" "$HTML_DIR" + +echo +echo "Open it with:" +echo " (cd \"$HTML_DIR\" && python3 -m http.server 8000)" +echo " then browse http://localhost:8000/index.html" diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/settings.gradle.kts b/Dokka-plugin-kdoc2json/pebble-renderer/settings.gradle.kts new file mode 100644 index 00000000..518ab513 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "pebble-renderer" diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/.DS_Store b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..81dcca77acd55234e7583aac6b00f2c4f7cc88e5 GIT binary patch literal 8196 zcmeHMU2GIp6u#fIzziMe07Vwu2@8cFWPwtUw)~jwpHlt`Y)iN0XW88u>A-ZR?96Tp zrEz283kb$1jsHApH1c3Ti7%R{h(0QsV2lq$jrxL#zNkESX6`J3{=5)lY@D0SJ@?*o z&%JZbe&4-&b{S)6DQN2$i!jDSdOWFAQ8hvF^xiYB2*F4tNsv8b1(s(yrZXoyG;Ve{ z5Jn)3Kp25A0$~Kg2>cf!KzFuR#7XXbt_|xj0$~LHml5#JhZsGcOa!vTrGIo#<);87 zy%fMN)TTVZw}}NZ5y%pkzA3HAvj+sG2vQ7ibJEAW=_C_@EOE)r8Mrw^urq=T1^(`2 z7xSkxB)AOgFalu&#z(-Tk3?BJdb?p%%Mf zX^wB3+@Mk!O{2Ww5Ul?Nt+Bh5n`E`8$m6f<2nM&-$Z9HQ4-Alt zPB7NoqN<0zZFgoIw{uXp=#{Podp}CLKBqsE*E@#@jKS!38udR?8uyIaHN&-iB}HR5 zdC}`F*Q{y24~rrTtJS31Eh=A#X+<%tqCo_pkCpR4tSx^uwTV@{S^HtGt4QNCY zTF{BT*oPzzq8|p1!ND+2VFah~6wcsjoWprMhnMjxUc&{viFa@bm+>w>z%_h^&+#R0 z;(PpnA8`wR;xF75rU?s#h_FytD#V4=!WyALXcRUHn}yv%r;rjd!ZE=XhPeYmnRJJE zzg!C^sWriILZl0h6XD{-q>Dbbtz-L+sYALOvpLP?Kd>OSeAU_wja!@Vz>(v(4B5Mh z;G_Q@&iLr}(J=@?4|NDk!Yh#?Zq({mO0=-e+STB3OYsLir=uhTVj759QrBj5IRHLc;(o4B`YK{xnAM1WI@kX9@A= z@eH2D3wRMP;T61&Hwg7_;cdeFd$@x4@ew}8ReTzt|69WTFZgW?o_Cdz%q4!G7|+FI z-ZE|LC~3<~xYuw>^eXsUM!fvLee&P`r-U!zONJ2$BXEBaKxJF9t%YnSJIlOWJ4VkD zdc5MzD{<+YQ01rNB>i-pjm@|CGM} F{{}q6fEWM( literal 0 HcmV?d00001 diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/java/org/appdevforall/docs/render/JavadocExtension.java b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/java/org/appdevforall/docs/render/JavadocExtension.java new file mode 100644 index 00000000..130198e8 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/java/org/appdevforall/docs/render/JavadocExtension.java @@ -0,0 +1,89 @@ +package org.appdevforall.docs.render; + +import io.pebbletemplates.pebble.extension.AbstractExtension; +import io.pebbletemplates.pebble.extension.Filter; +import io.pebbletemplates.pebble.extension.escaper.SafeString; +import io.pebbletemplates.pebble.template.EvaluationContext; +import io.pebbletemplates.pebble.template.PebbleTemplate; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The two filters every template needs. + * + * The JSON links to `.json` files, because that is what the plugin writes. The HTML output mirrors + * that tree file-for-file with `.html` names instead, so a link is correct in HTML as soon as its + * extension is swapped -- the relative path itself already points at the right place. Both filters + * here do that swap; they differ only in whether they are handed a bare URL or a block of + * documentation HTML with URLs inside it. + */ +public final class JavadocExtension extends AbstractExtension { + + @Override + public Map getFilters() { + return Map.of( + "href", new HrefFilter(), + "doc", new DocFilter() + ); + } + + /** `.json` -> `.html`, leaving any `#anchor` and any absolute URL alone. */ + static String toHtmlLink(String url) { + if (url == null || url.isEmpty()) return url; + if (url.startsWith("http://") || url.startsWith("https://")) return url; + int hash = url.indexOf('#'); + String path = hash < 0 ? url : url.substring(0, hash); + String fragment = hash < 0 ? "" : url.substring(hash); + if (path.endsWith(".json")) { + path = path.substring(0, path.length() - ".json".length()) + ".html"; + } + return path + fragment; + } + + /** Rewrites a single URL, e.g. `{{ type.url | href }}`. */ + static final class HrefFilter implements Filter { + @Override + public List getArgumentNames() { + return null; + } + + @Override + public Object apply(Object input, Map args, PebbleTemplate self, + EvaluationContext context, int lineNumber) { + return input == null ? null : toHtmlLink(input.toString()); + } + } + + /** + * Rewrites every `href` inside a block of documentation HTML and marks the result safe. + * + * Doc text arrives as HTML already -- a javadoc comment's body is HTML -- so it must not be + * escaped, but the links it contains still point at `.json`. Marking it safe here rather than + * writing `| raw` at each use keeps the "this is trusted HTML" decision in one place. + */ + static final class DocFilter implements Filter { + private static final Pattern HREF = Pattern.compile("href=\"([^\"]*)\""); + + @Override + public List getArgumentNames() { + return null; + } + + @Override + public Object apply(Object input, Map args, PebbleTemplate self, + EvaluationContext context, int lineNumber) { + if (input == null) return null; + Matcher matcher = HREF.matcher(input.toString()); + StringBuilder result = new StringBuilder(); + while (matcher.find()) { + String replacement = "href=\"" + toHtmlLink(matcher.group(1)) + "\""; + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + return new SafeString(result.toString()); + } + } +} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/java/org/appdevforall/docs/render/JavadocHtmlRenderer.java b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/java/org/appdevforall/docs/render/JavadocHtmlRenderer.java new file mode 100644 index 00000000..c6348eb3 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/java/org/appdevforall/docs/render/JavadocHtmlRenderer.java @@ -0,0 +1,207 @@ +package org.appdevforall.docs.render; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.pebbletemplates.pebble.PebbleEngine; +import io.pebbletemplates.pebble.loader.ClasspathLoader; +import io.pebbletemplates.pebble.template.PebbleTemplate; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.Map; + +/** + * Turns a javadoc-mode JSON tree into browsable HTML. + * + * Every JSON page carries a {@code page} field naming its kind, which selects the Pebble template; + * the parsed JSON becomes the template context directly, so a template reads the same field names + * that appear in the JSON. Output mirrors the input tree exactly, with {@code .json} swapped for + * {@code .html}, which is what makes the relative links in the JSON resolve once rewritten. + * + *

+ *   java -jar pebble-renderer.jar <json-dir> <html-dir>
+ * 
+ */ +public final class JavadocHtmlRenderer { + + /** {@code page} field value -> template. A page kind with no entry here is skipped. */ + private static final Map TEMPLATES = Map.of( + "class", "class", + "package", "package-summary", + "module", "module-summary", + "overview", "overview", + "all-classes", "all-classes", + "all-packages", "all-packages", + "deprecated-list", "deprecated-list", + "constant-values", "constant-values", + "index", "index-page" + ); + + private final ObjectMapper json = new ObjectMapper(); + private final PebbleEngine engine; + + private JavadocHtmlRenderer() { + this.engine = new PebbleEngine.Builder() + .loader(new ClasspathLoader() {{ + setPrefix("templates"); + setSuffix(".peb"); + }}) + // The doc text in the JSON is already HTML (a javadoc comment's body is), and the + // templates mark those values with |raw. Autoescaping stays on so everything else + // -- names, signatures, modifiers -- is escaped by default rather than by memory. + .autoEscaping(true) + .strictVariables(false) + .extension(new JavadocExtension()) + .build(); + } + + public static void main(String[] args) throws Exception { + if (args.length < 2) { + System.err.println("Usage: JavadocHtmlRenderer "); + System.exit(2); + } + Path source = Path.of(args[0]).toAbsolutePath().normalize(); + Path target = Path.of(args[1]).toAbsolutePath().normalize(); + if (!Files.isDirectory(source)) { + System.err.println("Not a directory: " + source); + System.exit(2); + } + int written = new JavadocHtmlRenderer().renderTree(source, target); + System.out.println("Wrote " + written + " HTML pages to " + target); + } + + private int renderTree(Path source, Path target) throws IOException { + Files.createDirectories(target); + copyStaticAssets(target); + // Built before the pages are written: a module page embeds its graph, so the graph has to + // exist, and drawing one needs every module's requires, not just its own. + ModuleGraphWriter graphs = new ModuleGraphWriter(readModuleRequires(source)); + + int[] counters = {0, 0}; + Files.walkFileTree(source, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Path relative = source.relativize(file); + if (file.getFileName().toString().endsWith(".json")) { + if (renderPage(file, relative, target, graphs)) counters[0]++; else counters[1]++; + } else { + // element-list and anything else non-JSON is carried across untouched. + Path copy = target.resolve(relative); + Files.createDirectories(copy.getParent()); + Files.copy(file, copy, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + return FileVisitResult.CONTINUE; + } + }); + if (counters[1] > 0) { + System.out.println("Skipped " + counters[1] + " JSON file(s) with no matching template."); + } + return counters[0]; + } + + /** Every module's direct `requires`, read from the module pages before rendering starts. */ + @SuppressWarnings("unchecked") + private Map> readModuleRequires(Path source) throws IOException { + Map> result = new LinkedHashMap<>(); + try (var paths = Files.walk(source)) { + for (Path file : (Iterable) paths.filter(p -> p.getFileName().toString() + .equals("module-summary.json"))::iterator) { + Map data = readPage(file); + if (data == null) continue; + Object name = data.get("name"); + if (name == null) continue; + // Only `requires transitive` -- see ModuleGraphWriter.requires for why. + Set required = new LinkedHashSet<>(); + Object requires = data.get("requires"); + if (requires instanceof List list) { + for (Object entry : list) { + if (entry instanceof Map map && map.get("module") != null + && Boolean.TRUE.equals(map.get("isTransitive"))) { + required.add(map.get("module").toString()); + } + } + } + result.put(name.toString(), required); + } + } + return result; + } + + private boolean renderPage(Path file, Path relative, Path target, ModuleGraphWriter graphs) + throws IOException { + Map data = readPage(file); + if (data == null) return false; + + Object kind = data.get("page"); + String templateName = kind == null ? null : TEMPLATES.get(kind.toString()); + if (templateName == null) { + System.err.println("No template for page kind '" + kind + "' (" + relative + ")"); + return false; + } + + Map context = new LinkedHashMap<>(data); + // Depth of this page below the output root, so templates can reach shared assets and the + // top-level index pages regardless of how deep they sit. + context.put("pathToRoot", pathToRoot(relative)); + context.put("pageKind", kind.toString()); + + Path out = target.resolve(withHtmlExtension(relative)); + Files.createDirectories(out.getParent()); + + if ("module".equals(kind.toString()) && data.get("name") != null) { + graphs.write(out.getParent(), data.get("name").toString()); + context.put("hasModuleGraph", true); + } + PebbleTemplate template = engine.getTemplate(templateName); + try (Writer writer = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) { + template.evaluate(writer, context); + } catch (IOException e) { + throw e; + } catch (RuntimeException e) { + throw new IOException("Failed rendering " + relative + ": " + e.getMessage(), e); + } + return true; + } + + @SuppressWarnings("unchecked") + private Map readPage(Path file) { + try { + return json.readValue(file.toFile(), Map.class); + } catch (IOException e) { + System.err.println("Could not read " + file + ": " + e.getMessage()); + return null; + } + } + + private static Path withHtmlExtension(Path relative) { + String name = relative.getFileName().toString(); + String renamed = name.substring(0, name.length() - ".json".length()) + ".html"; + Path parent = relative.getParent(); + return parent == null ? Path.of(renamed) : parent.resolve(renamed); + } + + /** {@code ""} at the root, {@code "../"} one level down, and so on. */ + private static String pathToRoot(Path relative) { + int depth = relative.getNameCount() - 1; + return "../".repeat(Math.max(0, depth)); + } + + private void copyStaticAssets(Path target) throws IOException { + for (String asset : new String[]{"stylesheet.css"}) { + try (InputStream in = getClass().getResourceAsStream("/static/" + asset)) { + if (in == null) continue; + Files.copy(in, target.resolve(asset), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + } + } +} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/java/org/appdevforall/docs/render/ModuleGraphWriter.java b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/java/org/appdevforall/docs/render/ModuleGraphWriter.java new file mode 100644 index 00000000..b49dfd6e --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/java/org/appdevforall/docs/render/ModuleGraphWriter.java @@ -0,0 +1,202 @@ +package org.appdevforall.docs.render; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Draws the `module-graph.svg` that each javadoc module page embeds. + * + * The official ones are produced by graphviz, which isn't a dependency here, so these are laid out + * directly: a module's transitive `requires` closure, arranged in rows by depth. They carry the + * same information and the same visual language as the originals -- text-only nodes, grey arrows, + * and javadoc's two module colours -- without being byte-identical to a graphviz rendering. + * + * The colour rule is javadoc's: modules that make up the Java SE platform (`java.se` and + * everything it requires) are orange, JDK-specific modules blue. + */ +final class ModuleGraphWriter { + + private static final String SE_COLOR = "#e76f00"; + private static final String JDK_COLOR = "#437291"; + private static final String EDGE_COLOR = "#999999"; + private static final String FONT = "DejaVuSans"; + private static final int FONT_SIZE = 12; + private static final int ROW_HEIGHT = 42; + private static final int CHAR_WIDTH = 7; // approximate advance for the 12pt font + private static final int COLUMN_GAP = 24; + private static final int MARGIN = 8; + + /** + * module name -> the modules it `requires transitive`. + * + * Plain `requires` is deliberately absent: javadoc's module graph draws the *readability* + * graph, which only propagates through `requires transitive`. `java.naming` plainly requires + * `java.security.sasl` and its graph shows only itself and `java.base` -- verified against all + * 60 JDK module graphs, which this rule reproduces exactly. + */ + private final Map> requires; + private final Set platformModules; + + private static final String BASE_MODULE = "java.base"; + + ModuleGraphWriter(Map> transitiveRequires) { + this.requires = transitiveRequires; + this.platformModules = platformModules(transitiveRequires); + } + + + + /** + * The Java SE platform: `java.se`, everything it requires, and `java.base`, which every module + * requires implicitly. Empty when the run has no `java.se`, in which case every node is drawn + * in the JDK colour. + */ + private static Set platformModules(Map> requires) { + Set platform = new HashSet<>(); + Set se = requires.get("java.se"); + if (se == null) return platform; + platform.add("java.se"); + platform.add(BASE_MODULE); + platform.addAll(se); + return platform; + } + + /** Writes `/module-graph.svg` for [moduleName]. */ + void write(Path moduleDir, String moduleName) throws IOException { + List> rows = layout(moduleName); + if (rows.isEmpty()) return; + Files.createDirectories(moduleDir); + Files.writeString(moduleDir.resolve("module-graph.svg"), render(moduleName, rows), + StandardCharsets.UTF_8); + } + + /** + * Groups the transitive closure into rows by longest distance from the root, so a module is + * always drawn below everything that requires it. + */ + private List> layout(String root) { + Map depth = new HashMap<>(); + depth.put(root, 0); + Deque queue = new ArrayDeque<>(); + queue.add(root); + // Longest-path depth needs re-visiting when a longer route to a node turns up, which is + // why this is a worklist rather than a plain BFS. + while (!queue.isEmpty()) { + String current = queue.poll(); + int next = depth.get(current) + 1; + for (String required : requires.getOrDefault(current, Set.of())) { + if (!requires.containsKey(required)) continue; // undocumented module + if (next > depth.getOrDefault(required, -1)) { + depth.put(required, next); + queue.add(required); + } + } + } + // Every module reads java.base implicitly. JPMS grants that without it being written, so + // it is absent from module-info.java and from the JSON, but javadoc still draws it (while + // leaving it out of the Requires *table*). + if (!root.equals(BASE_MODULE) && requires.containsKey(BASE_MODULE)) { + int deepest = depth.values().stream().mapToInt(Integer::intValue).max().orElse(0); + depth.put(BASE_MODULE, deepest + 1); + } + + Map> byDepth = new TreeMap<>(); + depth.forEach((name, level) -> byDepth.computeIfAbsent(level, k -> new ArrayList<>()).add(name)); + List> rows = new ArrayList<>(); + byDepth.values().forEach(row -> { + row.sort(Comparator.naturalOrder()); + rows.add(row); + }); + return rows; + } + + private String render(String root, List> rows) { + Map centres = new HashMap<>(); // name -> {cx, cy} + int width = 0; + for (List row : rows) { + int rowWidth = row.stream().mapToInt(n -> n.length() * CHAR_WIDTH).sum() + + COLUMN_GAP * Math.max(0, row.size() - 1); + width = Math.max(width, rowWidth); + } + width += MARGIN * 2; + int height = rows.size() * ROW_HEIGHT + MARGIN * 2; + + for (int level = 0; level < rows.size(); level++) { + List row = rows.get(level); + int rowWidth = row.stream().mapToInt(n -> n.length() * CHAR_WIDTH).sum() + + COLUMN_GAP * Math.max(0, row.size() - 1); + int x = (width - rowWidth) / 2; + int y = MARGIN + level * ROW_HEIGHT + FONT_SIZE; + for (String name : row) { + int nodeWidth = name.length() * CHAR_WIDTH; + centres.put(name, new int[]{x + nodeWidth / 2, y}); + x += nodeWidth + COLUMN_GAP; + } + } + + StringBuilder svg = new StringBuilder(); + svg.append("\n"); + svg.append("\n"); + svg.append("").append(escape(root)).append("\n"); + svg.append("\n"); + svg.append("\n"); + + // Edges first so the labels sit on top of them. + Set drawn = new LinkedHashSet<>(); + centres.keySet().stream().sorted().forEach(from -> { + Set targets = new LinkedHashSet<>(); + requires.getOrDefault(from, Set.of()).stream() + .filter(centres::containsKey) + .forEach(targets::add); + // A module with no transitive requires of its own reads java.base directly, and that + // is the edge javadoc draws for it. + if (targets.isEmpty() && !from.equals(BASE_MODULE) && centres.containsKey(BASE_MODULE)) { + targets.add(BASE_MODULE); + } + for (String to : targets) { + if (!drawn.add(from + "->" + to)) continue; + int[] a = centres.get(from); + int[] b = centres.get(to); + if (b[1] <= a[1]) continue; // only draw downwards, never back up a cycle + svg.append("\n"); + } + }); + + centres.keySet().stream().sorted().forEach(name -> { + int[] c = centres.get(name); + String colour = platformModules.contains(name) ? SE_COLOR : JDK_COLOR; + svg.append("").append(escape(name)) + .append("\n"); + }); + + svg.append("\n"); + return svg.toString(); + } + + private static String escape(String value) { + return value.replace("&", "&").replace("<", "<").replace(">", ">"); + } +} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/.DS_Store b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9f2fe98099091027fd3ceca28a9e78e3ba4898c9 GIT binary patch literal 10244 zcmeHMYit}>6~5o~BQwb~o}_N$WOwm8PU<#n;%AdMNyFM}hbE2f#B0~i)7i(+OuIYd z?yT*!PQW06`XB{BB!MXR&y7qeW zgAfRjW~7-j=iYnnoqP6t=ezeVV+_4h*>=XFj4_RFAx(oG6)sUP?j;xZOGG8eo-vzc z(lg##w@Ic^!{|65cXNyZ)p=9t3JP>#w@W8wWM1F|UEo9u6 zb4tq8K@YhFAX!bt@}hUz2S}T!FXO(PQ&O5zpKA92-xU591InE0qoO&w{6W#nDq(QDW+`4Do6u6Yf+P(1ec`*L~5 zeMC(Iekx!$luhf=@;T+a>4Z9ys7jXR^m1uV`tlXV4Uw_&Lx+zT6Xxn^`uL&AqU}1~ z-B!UPIbqg$G|9ag*EvwIMEzLC%}wPUZ^B%aN#!zAxm3~W$>oyvgBF$5d6|Nn%N=k% zTXb`1yl9;&in=9L^zoH`W`*Ve+O^v_D{5h0z&1TReFv|}EwoMUZ#G}cZDZk;CR>K&%kHWz8#3ztxXO|Hf&85;-&dlm+obBC4WNcdb z-@MB&yrbTTjAz_@o~G#eWj(!-$Z;|3{*+f7nzS7HqznGod#LWPcQRG5hGr-X{nC9@ zDt}Tc7e@VAyXeYF^2Q*|;*jXBC!3XXSl6(&E|QFlYUa8bMj3(x;fsaighBm2dY(G1}3T&30WskEb*>mg%>_zrV_8ay(dxQNA zKrM7E#|qSA9X23_+p!JX(T!dV;UI>Q#8I4tjeFtY6duGZPUE9^1Ruv)Jcdu>bNB+j zh$rzZzKZAYJidB%3Uw?!$0;N~9z@^h8p7JmPod-vUS{lM+gN(Hc+ zZeA1JxT&>k`yD;=fv6(46s!v&Ame@!T4dbIxG0O+iwcQGV+p0@kQ^nzSf@0BTXUIe zz24ZM5n#+3rM4KiX#^FsR;jV@7LCAZa;X~a;ZB_(XjUmT7T%_7CX}+n*rgK~&1$7a zjJU3wOe*0-xL+ecncCHg^h@j&_8R*$dxs)@C2m9mTF^%Ey&HF8kRn{f_b85I0x6`C zMIH_&aX-cSX`I1_Fo%!eQHu4)@CkehpTTGG1U`?a@HEBym+%#e_HW=kzKQSQ`*;yQ z^yB{56!m|=n`MzaP>NwC@m6^xPbUjb&UNmgauNMHQKZH4Y?9?9U0TdraNK=O=(4Mm z=(4l3^FO4^3Nu?kn&XvG91@&)^!h>ZyQJB(XW!tkXIum$TA{Ea!<+jnNcp)(MM?Qh}@qHHBICfKo0}g}aToOdInFOBE+V64>Q=NaVRtBD7c27BFYYVn8Dfq{Yx8 z=2Q-83LVt6cbW6z>b2_{k|Uu|1A2wcssG?0CDC4evl9IBRtFVyufEwUYTdk<4P|*`RU3zWdVPF ztupc&1%2+nevr46KYQoH@#^xY;>mLMEMe7LCJ0oM)Yi1#Xf$i&ic^s9EiI(h%C)9Y z-y3UeG&zA*HEwHfG|Vcwh84B9$4CV&Xob?f!!XQhxtbN~?)Q<(M7~hfi9YH{aOhPF z`pOi#6}u?N2XTM`eMC~|2_n#pq)?X#v>++;gG8VomK1uH2=tSZLZ6@|qoB}dDDb~5 zDfB$Pg>U0KM4%UF+4vLu6aoUj#cTKz-YyI3!BRLYiN973>M|-_Y!qnM!UI)m(5N{7 z@4x*2|JUNvf-wUR1RnSwc>vXY$-Z71thB=~A6J~UN9jI6x41;RIVGhDJ>+pbNqHPk zia1xim2T12KLL^NaldR%Noh`Pq&Z1-n*U$_Ga&E$d&g}{*Dt~TpRp@C>8iYV3HJY2 LcwBzP`~QCdp^}j5 literal 0 HcmV?d00001 diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/static/stylesheet.css b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/static/stylesheet.css new file mode 100644 index 00000000..2dc1a11b --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/static/stylesheet.css @@ -0,0 +1,167 @@ +/* + * Styling for the javadoc-mode HTML output. + * + * Written against the class names the official javadoc doclet emits (top-nav, sub-nav, header, + * summary-table, col-first/col-second/col-last, member-signature, notes, inheritance, ...) so the + * generated markup and the real thing describe the same structure. It is a readable approximation + * of javadoc's look, not a copy of the JDK's own stylesheet. + */ + +:root { + --body-font: 'DejaVu Sans', Arial, Helvetica, sans-serif; + --code-font: 'DejaVu Sans Mono', monospace; + --text: #353833; + --link: #4a6782; + --link-active: #bb7a2a; + --nav-bg: #4d7a97; + --nav-fg: #ffffff; + --subnav-bg: #dee3e9; + --border: #ededed; + --even-row: #ffffff; + --odd-row: #eeeeef; + --header-bg: #dee3e9; + --deprecated: #9c3328; +} + +* { box-sizing: border-box; } + +body { + background-color: #ffffff; + color: var(--text); + font-family: var(--body-font); + font-size: 14px; + margin: 0; + padding: 0; + line-height: 1.45; +} + +a { color: var(--link); text-decoration: none; } +a:hover, a:focus { color: var(--link-active); text-decoration: underline; } + +code, .member-signature, .type-signature, .package-signature { font-family: var(--code-font); font-size: 13px; } + +/* --- Navigation ------------------------------------------------------- */ + +.top-nav { + background-color: var(--nav-bg); + color: var(--nav-fg); + padding: 0 1rem; +} +.top-nav .nav-list { list-style: none; display: flex; flex-wrap: wrap; margin: 0; padding: 0.4rem 0; gap: 1.25rem; } +.top-nav .nav-list a { color: var(--nav-fg); font-weight: bold; font-size: 13px; } +.top-nav .nav-list a:hover { color: #bb7a2a; } + +.sub-nav { background-color: var(--subnav-bg); padding: 0 1rem; min-height: 1.8rem; } +.sub-nav-list { list-style: none; display: flex; flex-wrap: wrap; margin: 0; padding: 0.3rem 0; gap: 0.4rem; font-size: 13px; } +.sub-nav-list li + li::before { content: "\00a0/\00a0"; color: #666; } + +/* --- Layout ----------------------------------------------------------- */ + +.flex-content { padding: 0 1rem 2rem; max-width: 1400px; } +main { display: block; } + +.header { margin: 1rem 0; } +h1.title { font-size: 1.5rem; margin: 0.3rem 0; font-weight: normal; } +.sub-title { font-size: 13px; margin: 0.2rem 0; } +.package-label-in-type, .module-label-in-package { font-weight: bold; } + +h2 { font-size: 1.15rem; border-bottom: 1px solid #bbb; padding-bottom: 0.2rem; margin-top: 1.6rem; } +h3 { font-size: 1rem; margin: 1.1rem 0 0.3rem; } + +hr { border: none; border-top: 1px solid var(--border); margin: 1rem 0; } + +/* --- Class description ------------------------------------------------ */ + +.inheritance { margin-left: 1.2rem; font-size: 13px; } +.class-description > .inheritance:first-of-type { margin-left: 0; } + +.type-signature, .package-signature { + margin: 0.6rem 0; + padding: 0.5rem; + background-color: #f7f7f7; + border-left: 3px solid var(--nav-bg); + white-space: pre-wrap; +} +.modifiers, .return-type, .type-parameters { color: #4a6782; } +.element-name { font-weight: bold; } + +.block { margin: 0.4rem 0; } +.block p:first-child { margin-top: 0; } + +dl.notes { margin: 0.6rem 0; } +dl.notes dt { font-weight: bold; margin-top: 0.5rem; font-size: 13px; } +dl.notes dd { margin: 0.1rem 0 0.1rem 1.5rem; } +ul.see-list { list-style: none; margin: 0; padding: 0; } + +.deprecation-block { + border: 1px solid var(--deprecated); + border-left-width: 4px; + padding: 0.4rem 0.6rem; + margin: 0.6rem 0; + background-color: #fdf3f2; +} +.deprecated-label { color: var(--deprecated); font-weight: bold; } +.deprecation-comment { margin-top: 0.3rem; } + +/* --- Summary and detail lists ----------------------------------------- */ + +.summary-list, .details-list, .member-list { list-style: none; margin: 0; padding: 0; } +.member-list > li { border-top: 1px solid var(--border); padding-top: 0.5rem; margin-top: 0.8rem; } + +.caption { margin-top: 1rem; } +.caption span { + display: inline-block; + background-color: var(--nav-bg); + color: #fff; + padding: 0.25rem 0.8rem; + font-weight: bold; + font-size: 13px; + border-radius: 3px 3px 0 0; +} + +.summary-table { display: grid; border: 1px solid var(--border); font-size: 13px; } +.two-column-summary { grid-template-columns: minmax(20%, max-content) minmax(20%, auto); } +.three-column-summary { grid-template-columns: minmax(15%, max-content) minmax(15%, max-content) minmax(20%, auto); } + +.table-header { background-color: var(--header-bg); font-weight: bold; padding: 0.4rem 0.6rem; } +.summary-table > div { padding: 0.4rem 0.6rem; overflow-wrap: anywhere; } +.even-row-color { background-color: var(--even-row); } +.odd-row-color { background-color: var(--odd-row); } +.col-first, .col-second, .col-constructor-name { font-family: var(--code-font); } +.col-deprecated-item-name { font-family: var(--code-font); } + +.inherited-list { margin: 0.8rem 0; font-size: 13px; } +.inherited-list h3 { font-size: 0.9rem; background-color: var(--subnav-bg); padding: 0.3rem 0.5rem; margin-bottom: 0.3rem; font-weight: bold; } +.inherited-list code { overflow-wrap: anywhere; } + +/* --- Index ------------------------------------------------------------ */ + +.contents-list { margin: 0.5rem 0; font-family: var(--code-font); } +.contents-list a { margin-right: 0.4rem; } +dl.index dt { margin-top: 0.6rem; } +dl.index dd { margin-left: 1.5rem; } +.member-name-link { font-weight: bold; } + +/* --- Footer ----------------------------------------------------------- */ + +footer { margin-top: 2rem; font-size: 12px; color: #666; } + +@media screen and (max-width: 800px) { + .two-column-summary, .three-column-summary { grid-template-columns: 1fr; } + .table-header { display: none; } + .summary-table > div { border-bottom: 1px solid var(--border); } +} + +/* --- Module graph ------------------------------------------------------ */ + +/* javadoc shows a 100px-high thumbnail and reveals the full-size graph on hover. */ +.module-graph { position: relative; display: inline-block; } +.module-graph span { display: none; } +.module-graph:hover span { + display: block; + position: absolute; + top: 0; + left: 0; + z-index: 10; + background-color: #fff; +} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates.zip b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates.zip new file mode 100644 index 0000000000000000000000000000000000000000..feeaaea1d398b32603cdb629376adce0ab0c9007 GIT binary patch literal 16517 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 F{{S0{UsM19 literal 0 HcmV?d00001 diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-classes.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-classes.peb new file mode 100644 index 00000000..6f37e425 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-classes.peb @@ -0,0 +1,19 @@ +{% extends "base" %} +{% block title %}All Classes and Interfaces{% endblock %} +{% block bodyClass %}all-classes-index-page{% endblock %} +{% block content %} +

All Classes and Interfaces

+
+
Classes, Interfaces, Enums and Annotation Interfaces
+
+
Class
+
Package
+
Description
+{% for type in types %} + +
{{ type.packageName }}
+
{{ type.firstSentence | doc }}
+{% endfor %} +
+
+{% endblock %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-packages.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-packages.peb new file mode 100644 index 00000000..644e7381 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-packages.peb @@ -0,0 +1,19 @@ +{% extends "base" %} +{% block title %}All Packages{% endblock %} +{% block bodyClass %}all-packages-index-page{% endblock %} +{% block content %} +

All Packages

+
+
Package Summary
+
+
Module
+
Package
+
Description
+{% for pkg in packages %} +
{{ pkg.moduleName }}
+ +
{{ pkg.firstSentence | doc }}
+{% endfor %} +
+
+{% endblock %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/base.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/base.peb new file mode 100644 index 00000000..59fe4a96 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/base.peb @@ -0,0 +1,43 @@ +{# + The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the + footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, + ...) so the accompanying stylesheet and the real one describe the same structure. +#} + + + +{% block title %}Documentation{% endblock %} + + + + +
+
+ +
+
+
+{% block content %}{% endblock %} +
+
+
+ +
+
+
+ + diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/class.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/class.peb new file mode 100644 index 00000000..043f0735 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/class.peb @@ -0,0 +1,298 @@ +{% extends "base" %} +{% import "macros" %} + +{% block title %}{{ name }} ({{ moduleName | default('API') }}){% endblock %} +{% block bodyClass %}class-declaration-page{% endblock %} + +{% block subnav %} + +{% endblock %} + +{% block content %} +
+{% if packageName is not empty %} + +{% endif %} +

{{ kind | capitalize }} {{ name }}{% if typeParameters is not empty %}<{% for t in typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}>{% endif %}

+
+ +
+
+ +{# The inheritance tree, indented one step per level, exactly as javadoc draws it. #} +{% if inheritance is not empty and inheritance | length > 1 %} +{# javadoc nests one div per level so each generation indents further than the last. The final + entry is this class itself, shown as plain text rather than a link to the page you are on. #} +
+{%- for ancestor in inheritance -%} +
{% if loop.last %}{{ ancestor.qualifiedName }}{% else %}{{ typeLink(ancestor) }}{% endif %} +{%- endfor -%} +{%- for ancestor in inheritance -%}
{%- endfor -%} +
+{% endif %} + +{% if typeParameters is not empty and typeParameters | first is not null %} +{% set documentedTypeParams = false %} +{% for t in typeParameters %}{% if t.description is not empty %}{% set documentedTypeParams = true %}{% endif %}{% endfor %} +{% if documentedTypeParams %} +
+
Type Parameters:
+{% for t in typeParameters %}{% if t.description is not empty %}
{{ t.name }} - {{ t.description | doc }}
{% endif %}{% endfor %} +
+{% endif %} +{% endif %} + +{% if allImplementedInterfaces is not empty %} +
All Implemented Interfaces:
{{ typeList(allImplementedInterfaces) }}
+{% endif %} +{% if allSuperinterfaces is not empty %} +
All Superinterfaces:
{{ typeList(allSuperinterfaces) }}
+{% endif %} +{% if allKnownSubinterfaces is not empty %} +
All Known Subinterfaces:
{{ typeList(allKnownSubinterfaces) }}
+{% endif %} +{% if allKnownImplementingClasses is not empty %} +
All Known Implementing Classes:
{{ typeList(allKnownImplementingClasses) }}
+{% endif %} +{% if directKnownSubclasses is not empty %} +
Direct Known Subclasses:
{{ typeList(directKnownSubclasses) }}
+{% endif %} +{% if enclosingType is not empty %} +
Enclosing {{ enclosingType.kind | default('class') }}:
{{ typeLink(enclosingType) }}
+{% endif %} +{% if isFunctionalInterface %} +
Functional Interface:
This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.
+{% endif %} + +
+
{{ signature }}
+{% if description is not empty %}
{{ description | doc }}
{% endif %} +{% if deprecated is not empty %} +
Deprecated{% if deprecated.forRemoval %}, for removal: This API element is subject to removal in a future version{% endif %}. +{% if deprecated.since is not empty %}Since {{ deprecated.since }}.{% endif %} +{% if deprecated.comment is not empty %}
{{ deprecated.comment | doc }}
{% endif %} +
+{% endif %} +{% if since is not empty or seeAlso is not empty or tags is not empty or authors is not empty or versions is not empty %} +
+{% if since is not empty %}
Since:
{{ since | join(', ') }}
{% endif %} +{% for tag in tags %}
{{ tagLabel(tag.name) }}
{{ tag.text | doc }}
{% endfor %} +{% if authors is not empty %}
Author:
{{ authors | join(', ') }}
{% endif %} +{% if versions is not empty %}
Version:
{{ versions | join(', ') }}
{% endif %} +{% if seeAlso is not empty %} +
See Also:
+
    {% for see in seeAlso %}
  • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
  • {% endfor %}
+{% endif %} +
+{% endif %} +
+ +
+
    + +{% if nestedTypes is not empty or inheritedNestedTypes is not empty %} +
  • +
    +

    Nested Class Summary

    +{% if nestedTypes is not empty %} +
    Nested Classes
    +
    +
    Modifier and Type
    +
    Class
    +
    Description
    +{% for nested in nestedTypes %} +
    {{ nested.modifiers | join(' ') }} {{ nested.kind }}
    + +
    {{ nested.firstSentence | doc }}
    +{% endfor %} +
    +{% endif %} +{% for group in inheritedNestedTypes %} +
    +

    Nested classes/interfaces declared in {{ group.declaringType.kind | default('class') }} {{ typeLink(group.declaringType) }}

    +{% for member in group.members %}{{ memberLink(member) }}{% if not loop.last %}, {% endif %}{% endfor %} +
    +{% endfor %} +
    +
  • +{% endif %} + +{% if enumConstants is not empty %} +
  • +
    +

    Enum Constant Summary

    +
    Enum Constants
    +
    +
    Enum Constant
    +
    Description
    +{% for field in enumConstants %} + +
    {{ field.firstSentence | doc }}
    +{% endfor %} +
    +
    +
  • +{% endif %} + +{% if fields is not empty or inheritedFields is not empty %} +
  • +
    +

    Field Summary

    +{% if fields is not empty %} +
    Fields
    +
    +
    Modifier and Type
    +
    Field
    +
    Description
    +{% for field in fields %} +
    {{ field.modifiers | join(' ') }} {{ typeLink(field.type) }}
    + +
    {{ field.firstSentence | doc }}
    +{% endfor %} +
    +{% endif %} +{% for group in inheritedFields %} +
    +

    Fields declared in {{ group.declaringType.kind | default('class') }} {{ typeLink(group.declaringType) }}

    +{% for member in group.members %}{{ memberLink(member) }}{% if not loop.last %}, {% endif %}{% endfor %} +
    +{% endfor %} +
    +
  • +{% endif %} + +{% if constructors is not empty %} +
  • +
    +

    Constructor Summary

    +
    Constructors
    +
    +
    Constructor
    +
    Description
    +{% for ctor in constructors %} +
    {{ ctor.name }}{{ parameters(ctor.parameters) }}
    +
    {{ ctor.firstSentence | doc }}
    +{% endfor %} +
    +
    +
  • +{% endif %} + +{% if annotationElements is not empty %} +
  • +
    +

    Element Summary

    +
    Elements
    +
    +
    Modifier and Type
    +
    Element
    +
    Description
    +{% for element in annotationElements %} +
    {{ typeLink(element.returnType) }}
    + +
    {{ element.firstSentence | doc }}
    +{% endfor %} +
    +
    +
  • +{% endif %} + +{% if methods is not empty or inheritedMethods is not empty %} +
  • +
    +

    Method Summary

    +{% if methods is not empty %} +
    All Methods
    +
    +
    Modifier and Type
    +
    Method
    +
    Description
    +{% for method in methods %} +
    {{ method.modifiers | join(' ') }} {{ typeLink(method.returnType) }}
    +
    {{ method.name }}{{ parameters(method.parameters) }}
    +
    {{ method.firstSentence | doc }}
    +{% endfor %} +
    +{% endif %} +{% for group in inheritedMethods %} +
    +

    Methods declared in {{ group.declaringType.kind | default('class') }} {{ typeLink(group.declaringType) }}

    +{% for member in group.members %}{{ memberLink(member) }}{% if not loop.last %}, {% endif %}{% endfor %} +
    +{% endfor %} +
    +
  • +{% endif %} + +
+
+ +
+
    + +{% if enumConstants is not empty %} +
  • +
    +

    Enum Constant Details

    +
      +{% for field in enumConstants %}{{ fieldDetail(field) }}{% endfor %} +
    +
    +
  • +{% endif %} + +{% if fields is not empty %} +
  • +
    +

    Field Details

    +
      +{% for field in fields %}{{ fieldDetail(field) }}{% endfor %} +
    +
    +
  • +{% endif %} + +{% if constructors is not empty %} +
  • +
    +

    Constructor Details

    +
      +{% for ctor in constructors %}{{ executableDetail(ctor) }}{% endfor %} +
    +
    +
  • +{% endif %} + +{% if annotationElements is not empty %} +
  • +
    +

    Element Details

    +
      +{% for element in annotationElements %}{{ executableDetail(element) }}{% endfor %} +
    +
    +
  • +{% endif %} + +{% if methods is not empty %} +
  • +
    +

    Method Details

    +
      +{% for method in methods %}{{ executableDetail(method) }}{% endfor %} +
    +
    +
  • +{% endif %} + +
+
+{% endblock %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/constant-values.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/constant-values.peb new file mode 100644 index 00000000..79a7bef2 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/constant-values.peb @@ -0,0 +1,25 @@ +{% extends "base" %} +{% block title %}Constant Field Values{% endblock %} +{% block bodyClass %}constants-summary-page{% endblock %} +{% block content %} +

Constant Field Values

Contents

+{# Pebble iterates a map as entries: entry.key is the package, entry.value its types. #} +{% for group in packages %} +
+

{{ group.key }}

+{% for type in group.value %} +
{% if type.url is not empty %}{{ type.qualifiedName }}{% else %}{{ type.qualifiedName }}{% endif %}
+
+
Modifier and Type
+
Constant Field
+
Value
+{% for field in type.fields %} +
{{ field.modifiers | join(' ') }} {{ field.type.display }}
+
{% if field.url is not empty %}{{ field.name }}{% else %}{{ field.name }}{% endif %}
+
{{ field.value }}
+{% endfor %} +
+{% endfor %} +
+{% endfor %} +{% endblock %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/deprecated-list.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/deprecated-list.peb new file mode 100644 index 00000000..ea45ad99 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/deprecated-list.peb @@ -0,0 +1,27 @@ +{% extends "base" %} +{% block title %}Deprecated List{% endblock %} +{% block bodyClass %}deprecated-list-page{% endblock %} +{% block content %} +

Deprecated API

Contents

+{% if sections is empty %} +
No deprecated API in this documentation.
+{% endif %} +{# Pebble iterates a map as entries, so the section name is entry.key. #} +{% for section in sections %} +
+
{{ section.key }}
+
+
Element
+
Description
+{% for entry in section.value %} +
{% if entry.url is not empty %}{{ entry.element }}{% else %}{{ entry.element }}{% endif %}
+
+{% if entry.forRemoval %}Terminally deprecated.{% endif %} +{% if entry.since is not empty %}Since {{ entry.since }}.{% endif %} +{% if entry.comment is not empty %}
{{ entry.comment | doc }}
{% endif %} +
+{% endfor %} +
+
+{% endfor %} +{% endblock %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/index-page.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/index-page.peb new file mode 100644 index 00000000..b03e70d0 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/index-page.peb @@ -0,0 +1,22 @@ +{% extends "base" %} +{% block title %}{{ letter }}-Index{% endblock %} +{% block bodyClass %}index-page{% endblock %} +{% block content %} +
+

Index

+
+{# Pebble's loop.index is 0-based; the index files are numbered from 1. #} +{% for l in letters %}{{ l }}{% if not loop.last %} {% endif %}{% endfor %} +
+
+

{{ letter }}

+
+{% for entry in entries %} +
{% if entry.url is not empty %}{{ entry.label }}{% else %}{{ entry.label }}{% endif %} +{% if entry.containingElement is not empty %} - {{ entry.kind }} in {{ entry.containingElement }}{% else %} - {{ entry.kind }}{% endif %} +{% if entry.deprecated %}Deprecated.{% endif %} +
+
{% if entry.firstSentence is not empty %}
{{ entry.firstSentence | doc }}
{% endif %}
+{% endfor %} +
+{% endblock %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/macros.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/macros.peb new file mode 100644 index 00000000..24d92a47 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/macros.peb @@ -0,0 +1,158 @@ +{# + Shared fragments. + + Every macro here is called by BARE NAME, both from inside this file and from the templates that + import it. Pebble's {% import %} pulls macros straight into the importing template's namespace -- + unlike Jinja/Twig, there is no `macros.` prefix. A prefixed call resolves to nothing and renders + empty, silently, which is easy to miss. + Every one of these is defensive about missing keys: the plugin's `omitNulls` + option drops null and empty values entirely, so a template that assumes a key exists breaks + depending on how the JSON was generated. +#} + +{# + javadoc's display label for a block tag. The JSON carries the tag as written in the source + (`apiNote`), because that is the data; javadoc prints a spelled-out heading ("API Note:"). Any + tag not listed here falls back to its own name, which is what javadoc does for a custom tag. +#} +{% macro tagLabel(name) %} +{%- if name == 'apiNote' -%}API Note: +{%- elseif name == 'implSpec' -%}Implementation Requirements: +{%- elseif name == 'implNote' -%}Implementation Note: +{%- elseif name == 'jls' -%}See Java Language Specification: +{%- elseif name == 'jvms' -%}See Java Virtual Machine Specification: +{%- elseif name == 'serialData' -%}Serial Data: +{%- elseif name == 'serialField' -%}Serial Field: +{%- elseif name == 'serial' -%}Serial: +{%- elseif name == 'toolGuide' -%}Tool Guides: +{%- elseif name == 'revised' -%}Revised: +{%- elseif name == 'spec' -%}External Specifications: +{%- else -%}{{ name }}: +{%- endif -%} +{% endmacro %} + +{# A type reference: linked when this run documents the type, plain text when it doesn't. #} +{% macro typeLink(ref) %} +{%- if ref is not empty -%} +{%- if ref.url is not empty -%} +{{ ref.display }} +{%- else -%} +{{ ref.display }} +{%- endif -%} +{%- endif -%} +{% endmacro %} + +{# A comma-separated list of type references. #} +{% macro typeList(refs) %} +{%- for ref in refs -%}{{ typeLink(ref) }}{% if not loop.last %}, {% endif %}{%- endfor -%} +{% endmacro %} + +{# A member reference, as used by "Overrides:", "Specified by:" and the inherited-member lists. #} +{% macro memberLink(ref) %} +{%- if ref.url is not empty -%}{{ ref.name }}{%- else -%}{{ ref.name }}{%- endif -%} +{% endmacro %} + +{# One "dt/dd" note row, as javadoc renders Since / See Also / Overrides and friends. #} +{% macro note(label, body) %} +
{{ label }}
+
{{ body | raw }}
+{% endmacro %} + +{# The modifier prefix of a signature, e.g. "public static final". #} +{% macro modifiers(list) %} +{%- if list is not empty %}{{ list | join(' ') }} {% endif -%} +{% endmacro %} + +{# The parameter list of an executable, with linked parameter types. #} +{% macro parameters(params) %} +({%- for p in params -%}{{ typeLink(p.type) }} {{ p.name }}{% if not loop.last %}, {% endif %}{%- endfor -%}) +{% endmacro %} + +{# The `throws` clause of a signature. #} +{% macro throwsClause(exceptions) %} +{%- if exceptions is not empty %} throws {% for e in exceptions %}{{ typeLink(e.type) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif -%} +{% endmacro %} + +{# Zebra striping, which javadoc drives off the row index. #} +{% macro rowColor(index) %}{% if index is odd %}odd-row-color{% else %}even-row-color{% endif %}{% endmacro %} + +{# The block tags shared by every documented element: since, see also, deprecation, custom tags. #} +{% macro commonNotes(item) %} +{% if item.deprecated is not empty %} +
Deprecated{% if item.deprecated.forRemoval %}, for removal: This API element is subject to removal in a future version{% endif %}. +{% if item.deprecated.since is not empty %}Since {{ item.deprecated.since }}.{% endif %} +{% if item.deprecated.comment is not empty %}
{{ item.deprecated.comment | doc }}
{% endif %} +
+{% endif %} +{% if item.since is not empty or item.seeAlso is not empty or item.tags is not empty or item.authors is not empty or item.versions is not empty %} +
+{% if item.since is not empty %}
Since:
{{ item.since | join(', ') }}
{% endif %} +{% for tag in item.tags %}
{{ tagLabel(tag.name) }}
{{ tag.text | doc }}
{% endfor %} +{% if item.authors is not empty %}
Author:
{{ item.authors | join(', ') }}
{% endif %} +{% if item.versions is not empty %}
Version:
{{ item.versions | join(', ') }}
{% endif %} +{% if item.seeAlso is not empty %} +
See Also:
+
    {% for see in item.seeAlso %}
  • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
  • {% endfor %}
+{% endif %} +
+{% endif %} +{% endmacro %} + +{# One field/enum-constant entry in the Details section. #} +{% macro fieldDetail(field) %} +
  • +
    +

    {{ field.name }}

    +
    {{ modifiers(field.modifiers) }}{{ typeLink(field.type) }} {{ field.name }}
    +{% if field.description is not empty %}
    {{ field.description | doc }}
    {% endif %} +{{ commonNotes(field) }} +{% if field.constantValue is not empty %} +
    Constant Field Value:
    {{ field.constantValue }}
    +{% endif %} +
    +
  • +{% endmacro %} + +{# One constructor/method/annotation-element entry in the Details section. #} +{% macro executableDetail(member) %} +
  • +
    +

    {{ member.name }}

    +
    {{ modifiers(member.modifiers) }}{% if member.typeParameters is not empty %}<{% for t in member.typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}> {% endif %}{% if member.returnType is not empty %}{{ typeLink(member.returnType) }} {% endif %}{{ member.name }}{{ parameters(member.parameters) }}{{ throwsClause(member.exceptions) }}
    +{% if member.description is not empty %}
    {{ member.description | doc }}
    {% endif %} +{% if member.specifiedBy is not empty or member.overrides is not empty or member.typeParameters is not empty or member.parameters is not empty or member.returns is not empty or member.exceptions is not empty %} +
    +{% for spec in member.specifiedBy %} +
    Specified by:
    +
    {{ memberLink(spec) }} in {{ spec.declaringType.kind | default('interface') }} {{ typeLink(spec.declaringType) }}
    +{% endfor %} +{% if member.overrides is not empty %} +
    Overrides:
    +
    {{ memberLink(member.overrides) }} in {{ member.overrides.declaringType.kind | default('class') }} {{ typeLink(member.overrides.declaringType) }}
    +{% endif %} +{% set hasTypeParamDocs = false %} +{% for t in member.typeParameters %}{% if t.description is not empty %}{% set hasTypeParamDocs = true %}{% endif %}{% endfor %} +{% if hasTypeParamDocs %} +
    Type Parameters:
    +{% for t in member.typeParameters %}{% if t.description is not empty %}
    {{ t.name }} - {{ t.description | doc }}
    {% endif %}{% endfor %} +{% endif %} +{% set hasParamDocs = false %} +{% for p in member.parameters %}{% if p.description is not empty %}{% set hasParamDocs = true %}{% endif %}{% endfor %} +{% if hasParamDocs %} +
    Parameters:
    +{% for p in member.parameters %}{% if p.description is not empty %}
    {{ p.name }} - {{ p.description | doc }}
    {% endif %}{% endfor %} +{% endif %} +{% if member.returns is not empty %}
    Returns:
    {{ member.returns | doc }}
    {% endif %} +{% if member.exceptions is not empty %} +
    Throws:
    +{% for e in member.exceptions %}
    {{ typeLink(e.type) }}{% if e.description is not empty %} - {{ e.description | doc }}{% endif %}
    {% endfor %} +{% endif %} +
    +{% endif %} +{% if member.defaultValue is not empty %} +
    Default:
    {{ member.defaultValue }}
    +{% endif %} +{{ commonNotes(member) }} +
    +
  • +{% endmacro %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/module-summary.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/module-summary.peb new file mode 100644 index 00000000..5434e73f --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/module-summary.peb @@ -0,0 +1,157 @@ +{% extends "base" %} +{% import "macros" %} + +{% block title %}{{ name }}{% endblock %} +{% block bodyClass %}module-declaration-page{% endblock %} + +{% block subnav %} + +{% endblock %} + +{% block content %} +
    +

    Module {{ name }}

    +
    + +
    +{% if description is not empty %}
    {{ description | doc }}
    {% endif %} +{% if hasModuleGraph %} +
    +
    Module Graph:
    +
    Module graph for {{ name }}Module graph for {{ name }}
    +
    +{% endif %} +{% if since is not empty or tags is not empty or seeAlso is not empty %} +
    +{% if since is not empty %}
    Since:
    {{ since | join(', ') }}
    {% endif %} +{% for tag in tags %}{% if tag.name != 'moduleGraph' and tag.name != 'uses' and tag.name != 'provides' %}
    {{ tagLabel(tag.name) }}
    {{ tag.text | doc }}
    {% endif %}{% endfor %} +{% if seeAlso is not empty %}
    See Also:
      {% for see in seeAlso %}
    • {{ see.label }}
    • {% endfor %}
    {% endif %} +
    +{% endif %} +
    + +
    +
      + +{% if requires is not empty %} +
    • +
      +

      Modules

      +
      Requires
      +
      +
      Modifier
      +
      Module
      +
      Description
      +{% for req in requires %} +
      {% if req.isTransitive %}transitive{% endif %}{% if req.isStatic %}static{% endif %}
      +
      {% if req.url is not empty %}{{ req.module }}{% else %}{{ req.module }}{% endif %}
      +
      +{% endfor %} +
      +
      +
    • +{% endif %} + +{% if indirectRequires is not empty %} +
    • +
      +{% if requires is empty %}

      Modules

      {% endif %} +
      Indirect Requires
      +
      +
      Modifier
      +
      Module
      +
      Description
      +{% for req in indirectRequires %} +
      transitive
      +
      {% if req.url is not empty %}{{ req.module }}{% else %}{{ req.module }}{% endif %}
      +
      +{% endfor %} +
      +
      +
    • +{% endif %} + +{% if exports is not empty %} +
    • +
      +

      Packages

      +
      Exports
      +
      +
      Package
      +
      Exported To Modules
      +
      Description
      +{% for export in exports %} +
      {% if export.url is not empty %}{{ export.packageName }}{% else %}{{ export.packageName }}{% endif %}
      +
      {% if export.to is not empty %}{{ export.to | join(', ') }}{% else %}All Modules{% endif %}
      +
      +{% endfor %} +
      +
      +
    • +{% endif %} + +{% if indirectExports is not empty %} +
    • +
      +
      Indirect Exports
      +
      +
      From
      +
      Packages
      +{% for entry in indirectExports %} +
      {% if entry.moduleUrl is not empty %}{{ entry.module }}{% else %}{{ entry.module }}{% endif %}
      +
      {% for pkg in entry.packages %}{% if pkg.url is not empty %}{{ pkg.name }}{% else %}{{ pkg.name }}{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}
      +{% endfor %} +
      +
      +
    • +{% endif %} + +{% if opens is not empty %} +
    • +
      +

      Opens

      +
      +
      Package
      +
      Opened To Modules
      +{% for open in opens %} +
      {% if open.url is not empty %}{{ open.packageName }}{% else %}{{ open.packageName }}{% endif %}
      +
      {% if open.to is not empty %}{{ open.to | join(', ') }}{% else %}All Modules{% endif %}
      +{% endfor %} +
      +
      +
    • +{% endif %} + +{% if uses is not empty or provides is not empty %} +
    • +
      +

      Services

      +{% if uses is not empty %} +
      Uses
      +
      +
      Type
      +
      Description
      +{% for use in uses %} +
      {{ typeLink(use) }}
      +
      +{% endfor %} +
      +{% endif %} +{% if provides is not empty %} +
      Provides
      +
      +
      Type
      +
      Implementations
      +{% for provide in provides %} +
      {{ typeLink(provide.service) }}
      +
      {% for impl in provide.implementations %}{{ typeLink(impl) }}{% if not loop.last %}, {% endif %}{% endfor %}
      +{% endfor %} +
      +{% endif %} +
      +
    • +{% endif %} + +
    +
    +{% endblock %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/overview.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/overview.peb new file mode 100644 index 00000000..b10108ae --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/overview.peb @@ -0,0 +1,38 @@ +{% extends "base" %} +{% block title %}Overview{% endblock %} +{% block bodyClass %}package-index-page{% endblock %} +{% block content %} +
    +

    {{ title | default('API Documentation') }}

    +
    +
    +{% if modules is not empty %} +
    +
    Modules
    +
    +
    Module
    +
    Description
    +{% for module in modules %} + +
    {{ module.firstSentence | doc }}
    +{% endfor %} +
    +
    +{% endif %} +{% if packages is not empty %} +
    +
    Packages
    +
    +
    Module
    +
    Package
    +
    Description
    +{% for pkg in packages %} +
    {{ pkg.moduleName }}
    + +
    {{ pkg.firstSentence | doc }}
    +{% endfor %} +
    +
    +{% endif %} +
    +{% endblock %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/package-summary.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/package-summary.peb new file mode 100644 index 00000000..bd059701 --- /dev/null +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/package-summary.peb @@ -0,0 +1,67 @@ +{% extends "base" %} +{% import "macros" %} + +{% block title %}{{ name }}{% endblock %} +{% block bodyClass %}package-declaration-page{% endblock %} + +{% block subnav %} + +{% endblock %} + +{% macro typeTable(caption, rows) %} +{% if rows is not empty %} +
    {{ caption }}
    +
    +
    Class
    +
    Description
    +{% for row in rows %} + +
    {{ row.firstSentence | doc }}
    +{% endfor %} +
    +{% endif %} +{% endmacro %} + +{% block content %} +
    +{% if moduleName is not empty %} +
    Module {% if moduleUrl is not empty %}{{ moduleName }}{% else %}{{ moduleName }}{% endif %}
    +{% endif %} +

    Package {{ name }}

    +
    + +
    +
    package {{ name }}
    +{% if description is not empty %}
    {{ description | doc }}
    {% endif %} +{% if deprecated is not empty %} +
    Deprecated. +{% if deprecated.comment is not empty %}
    {{ deprecated.comment | doc }}
    {% endif %}
    +{% endif %} +{% if since is not empty or seeAlso is not empty or tags is not empty %} +
    +{% if since is not empty %}
    Since:
    {{ since | join(', ') }}
    {% endif %} +{% for tag in tags %}
    {{ tagLabel(tag.name) }}
    {{ tag.text | doc }}
    {% endfor %} +{% if seeAlso is not empty %}
    See Also:
      {% for see in seeAlso %}
    • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
    • {% endfor %}
    {% endif %} +
    +{% endif %} +
    + +
    +
      +
    • +
      +

      Package Contents

      +{{ typeTable("Interfaces", interfaces) }} +{{ typeTable("Classes", classes) }} +{{ typeTable("Enum Classes", enums) }} +{{ typeTable("Record Classes", records) }} +{{ typeTable("Exception Classes", exceptions) }} +{{ typeTable("Annotation Interfaces", annotationTypes) }} +
      +
    • +
    +
    +{% endblock %} From 79262e1f6eb27084d3e36a79dd24e44957544e81 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Mon, 31 Aug 2026 15:57:35 -0500 Subject: [PATCH 05/14] ADFA-5296: Fill in module descriptions on the overview and exports tables Two empty columns, both visible on the first page you land on: - The overview listed all 60 modules with a blank Description. moduleSummary was reading the Dokka module's documentation, which is empty for a JPMS module; the real description lives in module-info.java, as it does on the module page itself. - A module page's Exports (and Opens) table had a blank Description for every package. JdModuleExport now carries the exported package's summary sentence, which is what javadoc shows in that column. Structural parity unchanged: 60/60 modules, 224/224 packages, 4,672/4,672 types; 453,875 links and images resolve at 99.88%. Full suite 15/15. Co-Authored-By: Claude Opus 5 --- .../src/main/kotlin/javadoc/JavadocDtos.kt | 4 +++- .../src/main/kotlin/javadoc/JavadocMapper.kt | 15 ++++++++++++--- .../main/resources/templates/module-summary.peb | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt index 0e94c52a..a6140de3 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt @@ -291,7 +291,9 @@ data class JdModuleRequires( data class JdModuleExport( val packageName: String, val to: List = emptyList(), - val url: String? = null + val url: String? = null, + /** The exported package's summary sentence, which javadoc shows in this table's last column. */ + val firstSentence: String? = null ) /** diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt index 1eb68ba6..2b45f698 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt @@ -350,18 +350,22 @@ class JavadocMapper( ) }, exports = jpms?.exports.orEmpty().map { export -> + val documented = documentedByName[export.packageName] JdModuleExport( packageName = export.packageName, to = export.to, // A qualified export is not documented, so it has no page to link to. - url = documentedByName[export.packageName]?.url + url = documented?.url, + firstSentence = documented?.firstSentence ) }, opens = jpms?.opens.orEmpty().map { opens -> + val documented = documentedByName[opens.packageName] JdModuleExport( packageName = opens.packageName, to = opens.to, - url = documentedByName[opens.packageName]?.url + url = documented?.url, + firstSentence = documented?.firstSentence ) }, indirectRequires = indirectlyReadable(module).map { name -> @@ -466,10 +470,15 @@ class JavadocMapper( .map { scope.docs.bundleFor(it) } .firstOrNull { it.description != null } ?: JavadocDocBundle() + // As on the module page itself, module-info.java's own comment is the real source of a + // module's description -- Dokka's module carries none -- so the overview's Description + // column is empty without this. + val description = module.jpms?.description?.let { renderJavadocText(it, scope) } + ?: bundle.description return JdModuleSummary( name = module.name, url = scope.url(module.filePath), - firstSentence = JavadocDocs.firstSentence(bundle.description) + firstSentence = JavadocDocs.firstSentence(description) ) } diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/module-summary.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/module-summary.peb index 5434e73f..d6699a3a 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/module-summary.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/module-summary.peb @@ -83,7 +83,7 @@ {% for export in exports %}
    {% if export.url is not empty %}{{ export.packageName }}{% else %}{{ export.packageName }}{% endif %}
    {% if export.to is not empty %}{{ export.to | join(', ') }}{% else %}All Modules{% endif %}
    -
    +
    {{ export.firstSentence | doc }}
    {% endfor %}
    $children
    From 8f7511b7b83a24f6a63fbfc4221d908b70a1497d Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Mon, 31 Aug 2026 16:18:03 -0500 Subject: [PATCH 06/14] ADFA-5296: Audit the information content of every page type against the originals Started from the overview listing all 224 packages as well as the 60 modules, where javadoc lists only the modules, then diffed every other page type rather than assuming that was the only case. index.html Modules only. javadoc's overview lists packages only for a non-modular run; the full package list already has its own page. constant-values.html 3,138 of the JDK's 3,463 constants -> 3,458. Every one of the 325 absent was inherited from an *undocumented* supertype: java.util.jar.JarEntry's 40 CEN*/END* constants come from the package-private java.util.zip.ZipConstants. class pages Same root cause, and it also accounted for ~480 missing member anchors. A member inherited from a type this run does not document is now reported as declared, which is what javadoc does -- an "inherited from" group pointing at a page that does not exist is a dead end. Member anchors 4,305 -> 4,327 of 4,672 types. deprecated-list.html Section headings were the raw JSON keys ("classes", "enumConstants"); now javadoc's titles, plus a contents list. package pages "Related Packages" was missing entirely. Present on 206 pages, 181 identical to the originals. The rule is parent + children + siblings, but only while the result stays at five or fewer -- that condition is javadoc's, not an invention: java.nio.channels lists its siblings while java.util.concurrent and java.lang.annotation list none, because java.util and java.lang have too many children. Five reproduces 181/190; no cut-off reproduces 95. allclasses-index Listed all 4,672 types; javadoc indexes the public API, so the 167 protected nested types are now left out. They keep their pages, reachable from their enclosing class, exactly as in the originals. JdTypeSummary gains `modifiers` and JdPackagePage gains `relatedPackages`, so both decisions are made from data in the JSON rather than guessed in a template. Two counts still differ, both small and both showing more rather than less: allclasses-index has 4,506 against javadoc's 4,402, and the A-Z index 54,248 against 55,483. Neither reduced to a rule that held across all 60 modules, so they are left alone rather than tuned to fit. test_javadoc_mode.sh's annotation-element assertion was asserting the old behaviour and is updated: in the small example java.lang.Object is not documented, so its methods are pulled up there, while the JDK build documents java.lang and keeps them as inherited groups. Structural parity unchanged: 60/60 modules, 224/224 packages, 4,672/4,672 types; 455,440 links and images at 99.88%. Full suite 15/15, 66 javadoc-mode assertions. Co-Authored-By: Claude Opus 5 --- Dokka-plugin-kdoc2json/README.md | 24 ++++++++++ .../src/main/kotlin/javadoc/JavadocDtos.kt | 6 ++- .../src/main/kotlin/javadoc/JavadocMapper.kt | 44 ++++++++++++++++++- .../main/resources/templates/all-classes.peb | 4 +- .../resources/templates/deprecated-list.peb | 21 ++++++++- .../src/main/resources/templates/overview.peb | 4 +- .../resources/templates/package-summary.peb | 19 +++++++- .../tests/test_javadoc_mode.sh | 9 +++- 8 files changed, 121 insertions(+), 10 deletions(-) diff --git a/Dokka-plugin-kdoc2json/README.md b/Dokka-plugin-kdoc2json/README.md index 0d4d6e53..f40339f5 100644 --- a/Dokka-plugin-kdoc2json/README.md +++ b/Dokka-plugin-kdoc2json/README.md @@ -543,3 +543,27 @@ Our module pages show `Requires`, `Provides`, `Uses` and `Exports` tables on mor javadoc does -- javadoc suppresses some rows (for instance a `provides` whose implementation class is not itself documented, as in `java.smartcardio`). That is extra data rather than missing data, so it is left in. + +### Page-by-page content audit + +Every page type was diffed against the originals, not just the class pages. What that changed: + +| Page | Was | Now | +| --- | --- | --- | +| `index.html` | listed all 224 packages *as well as* the 60 modules | modules only, as javadoc does. The package list has its own page; javadoc's overview shows packages only for a non-modular run, which is what the template now keys on. | +| `constant-values.html` | 3,138 of the JDK's 3,463 constants | 3,458. The 325 absent were all inherited from *undocumented* supertypes -- `java.util.jar.JarEntry`'s 40 `CEN*`/`END*` constants come from the package-private `java.util.zip.ZipConstants`. | +| class pages | same 325-constant cause, plus ~480 members | a member inherited from a type this run does not document is now shown as declared, which is what javadoc does: an "inherited from" group pointing at a page that does not exist is a dead end. Member-anchor parity 4,305 -> 4,327 of 4,672 types. | +| `deprecated-list.html` | section headings were the raw JSON keys (`classes`, `enumConstants`) | javadoc's titles ("Deprecated Classes", "Deprecated Enum Constants"), plus a contents list | +| package pages | no "Related Packages" table | present on 206 pages, 181 matching the originals exactly | +| `allclasses-index.html` | all 4,672 types | 4,506 -- javadoc indexes the public API, so the 167 protected nested types are left out (they keep their pages, reachable from the enclosing class) | + +"Related Packages" is the parent, the direct children, and -- only when the result stays at five or +fewer -- the siblings. That size condition is javadoc's own: `java.nio.channels` lists its siblings +`java.nio.charset` and `java.nio.file`, while `java.util.concurrent` and `java.lang.annotation` +list none, because `java.util` and `java.lang` have too many children for the table to stay +useful. Five reproduces 181 of the 190 originals; no cut-off at all reproduces 95. + +Two counts still differ, both small and both in the direction of showing more rather than less: +`allclasses-index.html` lists 4,506 against javadoc's 4,402, and the A-Z index has 54,248 entries +against 55,483. Neither reduced to a rule that held across all 60 modules, so they are left as they +are rather than tuned to fit. diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt index a6140de3..811d9f07 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocDtos.kt @@ -235,7 +235,9 @@ data class JdTypeSummary( val moduleName: String? = null, val url: String? = null, val firstSentence: String? = null, - val deprecated: JdDeprecation? = null + val deprecated: JdDeprecation? = null, + /** The type's own modifiers, so a consumer can index only the public API as javadoc does. */ + val modifiers: List = emptyList() ) /** One `package-summary.json` page. */ @@ -254,6 +256,8 @@ data class JdPackagePage( val seeAlso: List = emptyList(), val deprecated: JdDeprecation? = null, val tags: List = emptyList(), + /** The parent, child and sibling packages javadoc lists under "Related Packages". */ + val relatedPackages: List = emptyList(), val interfaces: List = emptyList(), val classes: List = emptyList(), val enums: List = emptyList(), diff --git a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt index 2b45f698..770ff6bd 100644 --- a/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt +++ b/Dokka-plugin-kdoc2json/kdoc-to-json/src/main/kotlin/javadoc/JavadocMapper.kt @@ -47,6 +47,9 @@ class JavadocMapper( /** Depth cap for chained `{@inheritDoc}`, in case a hierarchy is cyclic after merging. */ private const val MAX_INHERIT_DEPTH = 16 + /** Above this many related packages javadoc drops the siblings -- see [relatedPackages]. */ + private const val MAX_RELATED_PACKAGES = 5 + /** The stand-in occupying a paragraph of its own, the usual way `{@inheritDoc}` is written. */ private val MARKER_PARAGRAPH = Regex("""

    \s*$INHERIT_DOC_MARKER\s*

    """) } @@ -306,6 +309,7 @@ class JavadocMapper( seeAlso = scope.seeRefs(bundle), deprecated = pkg.documentables.firstNotNullOfOrNull { deprecationOf(it, bundle) }, tags = bundle.other, + relatedPackages = relatedPackages(pkg).map { packageSummary(it, scope) }, interfaces = of("interface"), classes = of("class", "object"), enums = of("enum"), @@ -447,7 +451,8 @@ class JavadocMapper( moduleName = type.moduleName, url = scope.url(type.filePath), firstSentence = JavadocDocs.firstSentence(bundle.description), - deprecated = deprecationOf(type.documentable, bundle) + deprecated = deprecationOf(type.documentable, bundle), + modifiers = modifiersOf(type.documentable) ) } @@ -811,6 +816,35 @@ class JavadocMapper( return readableThrough(module).filterNot { it in direct || it == module.name }.sorted() } + /** + * The packages javadoc lists under "Related Packages": the parent, the direct children, and -- + * only when the result stays small -- the siblings. + * + * The size condition is javadoc's, not an invention: `java.nio.channels` lists its siblings + * `java.nio.charset` and `java.nio.file`, while `java.util.concurrent` and + * `java.lang.annotation` list none, because `java.util` and `java.lang` have too many + * children for the table to stay useful. A cut-off of five reproduces 181 of the 190 JDK + * package pages that have this table. + */ + private fun relatedPackages(pkg: JdPackage): List { + val name = pkg.name + val parentName = name.substringBeforeLast('.', "") + + fun childrenOf(prefix: String) = index.packages.filter { + it.name != prefix && + it.name.startsWith("$prefix.") && + !it.name.removePrefix("$prefix.").contains('.') + } + + val parent = index.packages.filter { it.name == parentName } + val children = childrenOf(name) + val siblings = if (parentName.isEmpty()) emptyList() else childrenOf(parentName).filter { it.name != name } + + val core = parent + children + val related = if (core.size + siblings.size <= MAX_RELATED_PACKAGES) core + siblings else core + return related.distinctBy { it.name }.sortedBy { it.name } + } + private fun clean(text: String): String? = text.trim().ifBlank { null } private fun executable( @@ -1172,7 +1206,13 @@ class JavadocMapper( val inherited = doc.extrasOrEmpty().allOfType().firstOrNull() ?: return null val from = inherited.inheritedFrom.values.firstOrNull { it != null } ?: return null val fromKey = JavadocModelIndex.keyOf(from) - return if (fromKey == ownerKey || fromKey.isBlank()) null else fromKey + if (fromKey == ownerKey || fromKey.isBlank()) return null + // A member inherited from a type this run does not document is shown by javadoc as if the + // subtype declared it -- there is no page to send the reader to, so an "inherited from" + // group would be a dead end. java.util.jar.JarEntry gets its 40 CEN*/END*/LOC* constants + // this way, from the package-private java.util.zip.ZipConstants. + if (index.typeForKey(fromKey) == null) return null + return fromKey } /** javadoc marks an interface with exactly one abstract method as a functional interface. */ diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-classes.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-classes.peb index 6f37e425..2bede7c9 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-classes.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-classes.peb @@ -9,11 +9,11 @@
    Class
    Package
    Description
    -{% for type in types %} +{% for type in types %}{% if type.modifiers is empty or type.modifiers contains 'public' %}
    {{ type.packageName }}
    {{ type.firstSentence | doc }}
    -{% endfor %} +{% endif %}{% endfor %} {% endblock %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/deprecated-list.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/deprecated-list.peb index ea45ad99..5489722e 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/deprecated-list.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/deprecated-list.peb @@ -1,15 +1,32 @@ {% extends "base" %} {% block title %}Deprecated List{% endblock %} {% block bodyClass %}deprecated-list-page{% endblock %} +{# The JSON keys each section by element kind; javadoc heads them with a title. #} +{% macro sectionTitle(kind) %} +{%- if kind == 'classes' -%}Deprecated Classes +{%- elseif kind == 'interfaces' -%}Deprecated Interfaces +{%- elseif kind == 'enums' -%}Deprecated Enum Classes +{%- elseif kind == 'exceptions' -%}Deprecated Exception Classes +{%- elseif kind == 'annotationTypes' -%}Deprecated Annotation Interfaces +{%- elseif kind == 'fields' -%}Deprecated Fields +{%- elseif kind == 'methods' -%}Deprecated Methods +{%- elseif kind == 'constructors' -%}Deprecated Constructors +{%- elseif kind == 'enumConstants' -%}Deprecated Enum Constants +{%- elseif kind == 'annotationElements' -%}Deprecated Annotation Elements +{%- else -%}Deprecated {{ kind }} +{%- endif -%} +{% endmacro %} + {% block content %} -

    Deprecated API

    Contents

    +

    Deprecated API

    Contents

    +
    {% if sections is empty %}
    No deprecated API in this documentation.
    {% endif %} {# Pebble iterates a map as entries, so the section name is entry.key. #} {% for section in sections %}
    -
    {{ section.key }}
    +
    {{ sectionTitle(section.key) }}
    Element
    Description
    diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/overview.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/overview.peb index b10108ae..70aaa31b 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/overview.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/overview.peb @@ -19,7 +19,9 @@
    {% endif %} -{% if packages is not empty %} +{# Only for a non-modular run: with modules present javadoc's overview lists just + the modules, and allpackages-index.html carries the package list. #} +{% if packages is not empty and modules is empty %}
    Packages
    diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/package-summary.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/package-summary.peb index bd059701..554fb1f1 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/package-summary.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/package-summary.peb @@ -51,9 +51,26 @@
      +{% if relatedPackages is not empty %} +
    • + +
    • +{% endif %}
    • -

      Package Contents

      +

      Classes and Interfaces

      {{ typeTable("Interfaces", interfaces) }} {{ typeTable("Classes", classes) }} {{ typeTable("Enum Classes", enums) }} diff --git a/Dokka-plugin-kdoc2json/tests/test_javadoc_mode.sh b/Dokka-plugin-kdoc2json/tests/test_javadoc_mode.sh index 1c8b552e..176106aa 100755 --- a/Dokka-plugin-kdoc2json/tests/test_javadoc_mode.sh +++ b/Dokka-plugin-kdoc2json/tests/test_javadoc_mode.sh @@ -124,8 +124,15 @@ assert_json "$CORNER" "[e['name'] for e in d['enumConstants']]" \ MEASURED="$JAVA_OUTPUT_DIR/com/example/shapes/Measured.json" assert_json "$MEASURED" "d['kind']" "annotation" "annotation kind" assert_json "$MEASURED" "d['signature']" "public @interface Measured" "annotation signature" -assert_json "$MEASURED" "sorted(e['name'] for e in d['annotationElements'])" \ +# The type's own elements must be listed. equals/hashCode/toString/annotationType come along too: +# they are inherited from java.lang.annotation.Annotation and java.lang.Object, which this small +# example does not document, and a member inherited from an *undocumented* type is shown as +# declared -- javadoc does the same, since there is no page to link the reader to. In a run that +# documents java.lang (the JDK build) they are inherited-member groups instead. +assert_json "$MEASURED" "sorted(e['name'] for e in d['annotationElements'] if e['name'] in ('tolerance','verifiedBy'))" \ "['tolerance', 'verifiedBy']" "annotation elements are listed" +assert_json "$MEASURED" "'annotationType' in [e['name'] for e in d['annotationElements']]" "True" \ + "members inherited from an undocumented supertype are pulled up, as javadoc does" EXC="$JAVA_OUTPUT_DIR/com/example/shapes/ShapeException.json" assert_json "$EXC" "d['kind']" "exception" \ From 90b60c8c4d858eb1e6878152a8f437d075e6e378 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Tue, 1 Sep 2026 15:13:59 -0500 Subject: [PATCH 07/14] ADFA-5296: Replace the Java API docs in documentation.db with javadoc-mode JSON Swaps the 4,988 scraped-HTML rows under j/html/api/ for the JSON the plugin's Javadoc mode produces, and installs the nine Pebble templates that render it. Follows the arrangement the Kotlin website docs already use: `path` stays the URL the browser asks for, `contentTypeID` stays text/html because that is the type of the *served* page, the blob is JSON compressed against the database's shared Brotli dictionary (ADFA-5153), and `templateId` names the template that turns one into the other. flatten_templates.py generates the database's templates from pebble-renderer's rather than leaving a second set to be maintained by hand. The two run Pebble in different environments and the database's is narrower: templates are stored one per row with no loader that resolves {% extends %} / {% import %} by name, and only built-in filters exist. Every template already in the database is self-contained, so that is the contract. The flattener inlines the parent and the imported macros, drops the `href` filter and turns `doc` into `raw`. Three things are rewritten into the JSON so the templates need nothing the reader does not already pass: - `.json` links become `.html`, since the row's path is what is requested. Done on the *parsed* JSON: in the raw text a link inside documentation HTML is href=\"List.json\" with escaped quotes, and a regex over the unparsed form silently misses it -- which it did, on ten links in the first attempt. - `pathToRoot` is injected, since the templates need it for the stylesheet and the top nav and the reader passes only the JSON. - The `page` field picks the template, keeping that mapping out of the reader. Rows with no JSON counterpart are LEFT ALONE rather than deleted. That is about half of them -- class-use/ (4,672), package-use (224), the tree pages (225), serialized-form, help-doc -- and they are working documentation that nothing in the new pages links to. Deleting them would take information out of the database, so it takes an explicit --delete-missing. element-list is not a page and is copied through unchanged with no template. Verified against /Users/alex/documentation.db: 4,988 rows updated, 1 passed through, 0 added, 0 deleted, 5,331 left as HTML; all nine page kinds render from the database through a stock Pebble engine with no custom filters; all 40 internal links on a rendered class page resolve to real rows; no .json link leaks anywhere; PRAGMA quick_check ok. A timestamped backup is taken before anything is written. Co-Authored-By: Claude Opus 5 --- scripts/sync_java_docs/README.md | 58 +++ .../db-templates/javadoc-all-classes.peb | 59 +++ .../db-templates/javadoc-all-packages.peb | 59 +++ .../db-templates/javadoc-class.peb | 453 ++++++++++++++++++ .../db-templates/javadoc-constant-values.peb | 65 +++ .../db-templates/javadoc-deprecated-list.peb | 82 ++++ .../db-templates/javadoc-index.peb | 62 +++ .../db-templates/javadoc-module.peb | 312 ++++++++++++ .../db-templates/javadoc-overview.peb | 80 ++++ .../db-templates/javadoc-package.peb | 237 +++++++++ scripts/sync_java_docs/flatten_templates.py | 112 +++++ .../sync_java_docs/sync_javadoc_json_to_db.py | 303 ++++++++++++ 12 files changed, 1882 insertions(+) create mode 100644 scripts/sync_java_docs/README.md create mode 100644 scripts/sync_java_docs/db-templates/javadoc-all-classes.peb create mode 100644 scripts/sync_java_docs/db-templates/javadoc-all-packages.peb create mode 100644 scripts/sync_java_docs/db-templates/javadoc-class.peb create mode 100644 scripts/sync_java_docs/db-templates/javadoc-constant-values.peb create mode 100644 scripts/sync_java_docs/db-templates/javadoc-deprecated-list.peb create mode 100644 scripts/sync_java_docs/db-templates/javadoc-index.peb create mode 100644 scripts/sync_java_docs/db-templates/javadoc-module.peb create mode 100644 scripts/sync_java_docs/db-templates/javadoc-overview.peb create mode 100644 scripts/sync_java_docs/db-templates/javadoc-package.peb create mode 100755 scripts/sync_java_docs/flatten_templates.py create mode 100755 scripts/sync_java_docs/sync_javadoc_json_to_db.py diff --git a/scripts/sync_java_docs/README.md b/scripts/sync_java_docs/README.md new file mode 100644 index 00000000..8fb231f8 --- /dev/null +++ b/scripts/sync_java_docs/README.md @@ -0,0 +1,58 @@ +# Java API docs → documentation.db + +Replaces the scraped Java API HTML in `documentation.db` with the JSON that the kdoc-to-json +plugin's Javadoc mode produces, and installs the Pebble templates that render it. + +```bash +# 1. Generate the JSON (see Dokka-plugin-kdoc2json/scripts/java) +Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh -j /path/to/jdk-17 + +# 2. Flatten the renderer's templates into standalone ones for the database +python3 scripts/sync_java_docs/flatten_templates.py + +# 3. Look at what would change, then do it +python3 scripts/sync_java_docs/sync_javadoc_json_to_db.py --db documentation.db --dry-run +python3 scripts/sync_java_docs/sync_javadoc_json_to_db.py --db documentation.db +``` + +## How a page is stored + +The same arrangement the Kotlin website docs already use, and it is worth being explicit about +because the pieces disagree with each other at first glance: + +| Column | Value | Why | +| --- | --- | --- | +| `path` | `j/html/api/…/ArrayList.html` | unchanged — it is the URL a browser asks for | +| `content` | **JSON**, shared-dictionary Brotli | the data; the template turns it into a page | +| `contentTypeID` | `text/html` | the type of the *served* page, not of the blob | +| `templateId` | one of the nine `javadoc-*.peb` rows | chosen from the JSON's `page` field | + +## Why the templates are flattened + +`pebble-renderer/` and the database's reader run Pebble in different environments, and the +database's is narrower: templates are stored one per row with no loader that resolves +`{% extends %}` / `{% import %}` by name, and only Pebble's built-in filters exist. Every template +already in the database is self-contained, so that is the contract. + +`flatten_templates.py` therefore generates the database copies from the renderer's rather than +having a second set maintained by hand: it inlines the parent template and the imported macros, +drops the `href` filter and turns `doc` into the built-in `raw`. + +Three things are rewritten into the JSON on the way in, all so the templates need nothing beyond +what the reader already passes: + +- **`.json` links become `.html`**, since the row's path is what the browser requests. This is done + on the *parsed* JSON, because in the raw text a link inside documentation HTML is + `href=\"List.json\"` with escaped quotes, and a regex over the unparsed form misses it. +- **`pathToRoot` is injected**, since the templates need it for the stylesheet and the top nav and + the reader passes nothing but the JSON. +- The `page` field selects the template, so that mapping lives here and not in the reader. + +## What it leaves alone + +About half the rows under `j/html/api/` are page kinds this pipeline does not generate — +`class-use/` (4,672), `package-use` (224), the tree pages (225), `serialized-form`, `help-doc`. +They are working documentation, nothing in the new pages links to them, and deleting them would +take information out of the database. They are left as HTML unless you pass `--delete-missing`. + +`element-list` is not a page and is copied through unchanged with no template. diff --git a/scripts/sync_java_docs/db-templates/javadoc-all-classes.peb b/scripts/sync_java_docs/db-templates/javadoc-all-classes.peb new file mode 100644 index 00000000..afc6b55b --- /dev/null +++ b/scripts/sync_java_docs/db-templates/javadoc-all-classes.peb @@ -0,0 +1,59 @@ +{# + The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the + footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, + ...) so the accompanying stylesheet and the real one describe the same structure. +#} + + + +All Classes and Interfaces + + + + +
      +
      + +
      +
      +
      + +

      All Classes and Interfaces

      +
      +
      Classes, Interfaces, Enums and Annotation Interfaces
      +
      +
      Class
      +
      Package
      +
      Description
      +{% for type in types %}{% if type.modifiers is empty or type.modifiers contains 'public' %} + +
      {{ type.packageName }}
      +
      {{ type.firstSentence | raw }}
      +{% endif %}{% endfor %} +
      +
      + +
      +
      +
      + +
      +
      +
      + + + diff --git a/scripts/sync_java_docs/db-templates/javadoc-all-packages.peb b/scripts/sync_java_docs/db-templates/javadoc-all-packages.peb new file mode 100644 index 00000000..9edbd334 --- /dev/null +++ b/scripts/sync_java_docs/db-templates/javadoc-all-packages.peb @@ -0,0 +1,59 @@ +{# + The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the + footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, + ...) so the accompanying stylesheet and the real one describe the same structure. +#} + + + +All Packages + + + + +
      +
      + +
      +
      +
      + +

      All Packages

      +
      +
      Package Summary
      +
      +
      Module
      +
      Package
      +
      Description
      +{% for pkg in packages %} +
      {{ pkg.moduleName }}
      + +
      {{ pkg.firstSentence | raw }}
      +{% endfor %} +
      +
      + +
      +
      +
      + +
      +
      +
      + + + diff --git a/scripts/sync_java_docs/db-templates/javadoc-class.peb b/scripts/sync_java_docs/db-templates/javadoc-class.peb new file mode 100644 index 00000000..9c7dd237 --- /dev/null +++ b/scripts/sync_java_docs/db-templates/javadoc-class.peb @@ -0,0 +1,453 @@ +{# + The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the + footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, + ...) so the accompanying stylesheet and the real one describe the same structure. +#} + + + +{{ name }} ({{ moduleName | default('API') }}) + + + + +
      +
      + +
      +
      +
      + +
      +{% if packageName is not empty %} + +{% endif %} +

      {{ kind | capitalize }} {{ name }}{% if typeParameters is not empty %}<{% for t in typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}>{% endif %}

      +
      + +
      +
      + +{# The inheritance tree, indented one step per level, exactly as javadoc draws it. #} +{% if inheritance is not empty and inheritance | length > 1 %} +{# javadoc nests one div per level so each generation indents further than the last. The final + entry is this class itself, shown as plain text rather than a link to the page you are on. #} +
      +{%- for ancestor in inheritance -%} +
      {% if loop.last %}{{ ancestor.qualifiedName }}{% else %}{{ typeLink(ancestor) }}{% endif %} +{%- endfor -%} +{%- for ancestor in inheritance -%}
      {%- endfor -%} +
      +{% endif %} + +{% if typeParameters is not empty and typeParameters | first is not null %} +{% set documentedTypeParams = false %} +{% for t in typeParameters %}{% if t.description is not empty %}{% set documentedTypeParams = true %}{% endif %}{% endfor %} +{% if documentedTypeParams %} +
      +
      Type Parameters:
      +{% for t in typeParameters %}{% if t.description is not empty %}
      {{ t.name }} - {{ t.description | raw }}
      {% endif %}{% endfor %} +
      +{% endif %} +{% endif %} + +{% if allImplementedInterfaces is not empty %} +
      All Implemented Interfaces:
      {{ typeList(allImplementedInterfaces) }}
      +{% endif %} +{% if allSuperinterfaces is not empty %} +
      All Superinterfaces:
      {{ typeList(allSuperinterfaces) }}
      +{% endif %} +{% if allKnownSubinterfaces is not empty %} +
      All Known Subinterfaces:
      {{ typeList(allKnownSubinterfaces) }}
      +{% endif %} +{% if allKnownImplementingClasses is not empty %} +
      All Known Implementing Classes:
      {{ typeList(allKnownImplementingClasses) }}
      +{% endif %} +{% if directKnownSubclasses is not empty %} +
      Direct Known Subclasses:
      {{ typeList(directKnownSubclasses) }}
      +{% endif %} +{% if enclosingType is not empty %} +
      Enclosing {{ enclosingType.kind | default('class') }}:
      {{ typeLink(enclosingType) }}
      +{% endif %} +{% if isFunctionalInterface %} +
      Functional Interface:
      This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.
      +{% endif %} + +
      +
      {{ signature }}
      +{% if description is not empty %}
      {{ description | raw }}
      {% endif %} +{% if deprecated is not empty %} +
      Deprecated{% if deprecated.forRemoval %}, for removal: This API element is subject to removal in a future version{% endif %}. +{% if deprecated.since is not empty %}Since {{ deprecated.since }}.{% endif %} +{% if deprecated.comment is not empty %}
      {{ deprecated.comment | raw }}
      {% endif %} +
      +{% endif %} +{% if since is not empty or seeAlso is not empty or tags is not empty or authors is not empty or versions is not empty %} +
      +{% if since is not empty %}
      Since:
      {{ since | join(', ') }}
      {% endif %} +{% for tag in tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} +{% if authors is not empty %}
      Author:
      {{ authors | join(', ') }}
      {% endif %} +{% if versions is not empty %}
      Version:
      {{ versions | join(', ') }}
      {% endif %} +{% if seeAlso is not empty %} +
      See Also:
      +
        {% for see in seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      +{% endif %} +
      +{% endif %} +
      + +
      +
        + +{% if nestedTypes is not empty or inheritedNestedTypes is not empty %} +
      • +
        +

        Nested Class Summary

        +{% if nestedTypes is not empty %} +
        Nested Classes
        +
        +
        Modifier and Type
        +
        Class
        +
        Description
        +{% for nested in nestedTypes %} +
        {{ nested.modifiers | join(' ') }} {{ nested.kind }}
        + +
        {{ nested.firstSentence | raw }}
        +{% endfor %} +
        +{% endif %} +{% for group in inheritedNestedTypes %} +
        +

        Nested classes/interfaces declared in {{ group.declaringType.kind | default('class') }} {{ typeLink(group.declaringType) }}

        +{% for member in group.members %}{{ memberLink(member) }}{% if not loop.last %}, {% endif %}{% endfor %} +
        +{% endfor %} +
        +
      • +{% endif %} + +{% if enumConstants is not empty %} +
      • +
        +

        Enum Constant Summary

        +
        Enum Constants
        +
        +
        Enum Constant
        +
        Description
        +{% for field in enumConstants %} + +
        {{ field.firstSentence | raw }}
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if fields is not empty or inheritedFields is not empty %} +
      • +
        +

        Field Summary

        +{% if fields is not empty %} +
        Fields
        +
        +
        Modifier and Type
        +
        Field
        +
        Description
        +{% for field in fields %} +
        {{ field.modifiers | join(' ') }} {{ typeLink(field.type) }}
        + +
        {{ field.firstSentence | raw }}
        +{% endfor %} +
        +{% endif %} +{% for group in inheritedFields %} +
        +

        Fields declared in {{ group.declaringType.kind | default('class') }} {{ typeLink(group.declaringType) }}

        +{% for member in group.members %}{{ memberLink(member) }}{% if not loop.last %}, {% endif %}{% endfor %} +
        +{% endfor %} +
        +
      • +{% endif %} + +{% if constructors is not empty %} +
      • +
        +

        Constructor Summary

        +
        Constructors
        +
        +
        Constructor
        +
        Description
        +{% for ctor in constructors %} +
        {{ ctor.name }}{{ parameters(ctor.parameters) }}
        +
        {{ ctor.firstSentence | raw }}
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if annotationElements is not empty %} +
      • +
        +

        Element Summary

        +
        Elements
        +
        +
        Modifier and Type
        +
        Element
        +
        Description
        +{% for element in annotationElements %} +
        {{ typeLink(element.returnType) }}
        + +
        {{ element.firstSentence | raw }}
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if methods is not empty or inheritedMethods is not empty %} +
      • +
        +

        Method Summary

        +{% if methods is not empty %} +
        All Methods
        +
        +
        Modifier and Type
        +
        Method
        +
        Description
        +{% for method in methods %} +
        {{ method.modifiers | join(' ') }} {{ typeLink(method.returnType) }}
        +
        {{ method.name }}{{ parameters(method.parameters) }}
        +
        {{ method.firstSentence | raw }}
        +{% endfor %} +
        +{% endif %} +{% for group in inheritedMethods %} +
        +

        Methods declared in {{ group.declaringType.kind | default('class') }} {{ typeLink(group.declaringType) }}

        +{% for member in group.members %}{{ memberLink(member) }}{% if not loop.last %}, {% endif %}{% endfor %} +
        +{% endfor %} +
        +
      • +{% endif %} + +
      +
      + +
      +
        + +{% if enumConstants is not empty %} +
      • +
        +

        Enum Constant Details

        +
          +{% for field in enumConstants %}{{ fieldDetail(field) }}{% endfor %} +
        +
        +
      • +{% endif %} + +{% if fields is not empty %} +
      • +
        +

        Field Details

        +
          +{% for field in fields %}{{ fieldDetail(field) }}{% endfor %} +
        +
        +
      • +{% endif %} + +{% if constructors is not empty %} +
      • +
        +

        Constructor Details

        +
          +{% for ctor in constructors %}{{ executableDetail(ctor) }}{% endfor %} +
        +
        +
      • +{% endif %} + +{% if annotationElements is not empty %} +
      • +
        +

        Element Details

        +
          +{% for element in annotationElements %}{{ executableDetail(element) }}{% endfor %} +
        +
        +
      • +{% endif %} + +{% if methods is not empty %} +
      • +
        +

        Method Details

        +
          +{% for method in methods %}{{ executableDetail(method) }}{% endfor %} +
        +
        +
      • +{% endif %} + +
      +
      + +
      +
      +
      + +
      +
      +
      + + + + +{% macro tagLabel(name) %} +{%- if name == 'apiNote' -%}API Note: +{%- elseif name == 'implSpec' -%}Implementation Requirements: +{%- elseif name == 'implNote' -%}Implementation Note: +{%- elseif name == 'jls' -%}See Java Language Specification: +{%- elseif name == 'jvms' -%}See Java Virtual Machine Specification: +{%- elseif name == 'serialData' -%}Serial Data: +{%- elseif name == 'serialField' -%}Serial Field: +{%- elseif name == 'serial' -%}Serial: +{%- elseif name == 'toolGuide' -%}Tool Guides: +{%- elseif name == 'revised' -%}Revised: +{%- elseif name == 'spec' -%}External Specifications: +{%- else -%}{{ name }}: +{%- endif -%} +{% endmacro %} +{% macro typeLink(ref) %} +{%- if ref is not empty -%} +{%- if ref.url is not empty -%} +{{ ref.display }} +{%- else -%} +{{ ref.display }} +{%- endif -%} +{%- endif -%} +{% endmacro %} +{% macro typeList(refs) %} +{%- for ref in refs -%}{{ typeLink(ref) }}{% if not loop.last %}, {% endif %}{%- endfor -%} +{% endmacro %} +{% macro memberLink(ref) %} +{%- if ref.url is not empty -%}{{ ref.name }}{%- else -%}{{ ref.name }}{%- endif -%} +{% endmacro %} +{% macro note(label, body) %} +
      {{ label }}
      +
      {{ body | raw }}
      +{% endmacro %} +{% macro modifiers(list) %} +{%- if list is not empty %}{{ list | join(' ') }} {% endif -%} +{% endmacro %} +{% macro parameters(params) %} +({%- for p in params -%}{{ typeLink(p.type) }} {{ p.name }}{% if not loop.last %}, {% endif %}{%- endfor -%}) +{% endmacro %} +{% macro throwsClause(exceptions) %} +{%- if exceptions is not empty %} throws {% for e in exceptions %}{{ typeLink(e.type) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif -%} +{% endmacro %} +{% macro rowColor(index) %}{% if index is odd %}odd-row-color{% else %}even-row-color{% endif %}{% endmacro %} +{% macro commonNotes(item) %} +{% if item.deprecated is not empty %} +
      Deprecated{% if item.deprecated.forRemoval %}, for removal: This API element is subject to removal in a future version{% endif %}. +{% if item.deprecated.since is not empty %}Since {{ item.deprecated.since }}.{% endif %} +{% if item.deprecated.comment is not empty %}
      {{ item.deprecated.comment | raw }}
      {% endif %} +
      +{% endif %} +{% if item.since is not empty or item.seeAlso is not empty or item.tags is not empty or item.authors is not empty or item.versions is not empty %} +
      +{% if item.since is not empty %}
      Since:
      {{ item.since | join(', ') }}
      {% endif %} +{% for tag in item.tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} +{% if item.authors is not empty %}
      Author:
      {{ item.authors | join(', ') }}
      {% endif %} +{% if item.versions is not empty %}
      Version:
      {{ item.versions | join(', ') }}
      {% endif %} +{% if item.seeAlso is not empty %} +
      See Also:
      +
        {% for see in item.seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      +{% endif %} +
      +{% endif %} +{% endmacro %} +{% macro fieldDetail(field) %} +
    • +
      +

      {{ field.name }}

      +
      {{ modifiers(field.modifiers) }}{{ typeLink(field.type) }} {{ field.name }}
      +{% if field.description is not empty %}
      {{ field.description | raw }}
      {% endif %} +{{ commonNotes(field) }} +{% if field.constantValue is not empty %} +
      Constant Field Value:
      {{ field.constantValue }}
      +{% endif %} +
      +
    • +{% endmacro %} +{% macro executableDetail(member) %} +
    • +
      +

      {{ member.name }}

      +
      {{ modifiers(member.modifiers) }}{% if member.typeParameters is not empty %}<{% for t in member.typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}> {% endif %}{% if member.returnType is not empty %}{{ typeLink(member.returnType) }} {% endif %}{{ member.name }}{{ parameters(member.parameters) }}{{ throwsClause(member.exceptions) }}
      +{% if member.description is not empty %}
      {{ member.description | raw }}
      {% endif %} +{% if member.specifiedBy is not empty or member.overrides is not empty or member.typeParameters is not empty or member.parameters is not empty or member.returns is not empty or member.exceptions is not empty %} +
      +{% for spec in member.specifiedBy %} +
      Specified by:
      +
      {{ memberLink(spec) }} in {{ spec.declaringType.kind | default('interface') }} {{ typeLink(spec.declaringType) }}
      +{% endfor %} +{% if member.overrides is not empty %} +
      Overrides:
      +
      {{ memberLink(member.overrides) }} in {{ member.overrides.declaringType.kind | default('class') }} {{ typeLink(member.overrides.declaringType) }}
      +{% endif %} +{% set hasTypeParamDocs = false %} +{% for t in member.typeParameters %}{% if t.description is not empty %}{% set hasTypeParamDocs = true %}{% endif %}{% endfor %} +{% if hasTypeParamDocs %} +
      Type Parameters:
      +{% for t in member.typeParameters %}{% if t.description is not empty %}
      {{ t.name }} - {{ t.description | raw }}
      {% endif %}{% endfor %} +{% endif %} +{% set hasParamDocs = false %} +{% for p in member.parameters %}{% if p.description is not empty %}{% set hasParamDocs = true %}{% endif %}{% endfor %} +{% if hasParamDocs %} +
      Parameters:
      +{% for p in member.parameters %}{% if p.description is not empty %}
      {{ p.name }} - {{ p.description | raw }}
      {% endif %}{% endfor %} +{% endif %} +{% if member.returns is not empty %}
      Returns:
      {{ member.returns | raw }}
      {% endif %} +{% if member.exceptions is not empty %} +
      Throws:
      +{% for e in member.exceptions %}
      {{ typeLink(e.type) }}{% if e.description is not empty %} - {{ e.description | raw }}{% endif %}
      {% endfor %} +{% endif %} +
      +{% endif %} +{% if member.defaultValue is not empty %} +
      Default:
      {{ member.defaultValue }}
      +{% endif %} +{{ commonNotes(member) }} +
      +
    • +{% endmacro %} \ No newline at end of file diff --git a/scripts/sync_java_docs/db-templates/javadoc-constant-values.peb b/scripts/sync_java_docs/db-templates/javadoc-constant-values.peb new file mode 100644 index 00000000..60656623 --- /dev/null +++ b/scripts/sync_java_docs/db-templates/javadoc-constant-values.peb @@ -0,0 +1,65 @@ +{# + The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the + footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, + ...) so the accompanying stylesheet and the real one describe the same structure. +#} + + + +Constant Field Values + + + + +
      +
      + +
      +
      +
      + +

      Constant Field Values

      Contents

      +{# Pebble iterates a map as entries: entry.key is the package, entry.value its types. #} +{% for group in packages %} +
      +

      {{ group.key }}

      +{% for type in group.value %} +
      {% if type.url is not empty %}{{ type.qualifiedName }}{% else %}{{ type.qualifiedName }}{% endif %}
      +
      +
      Modifier and Type
      +
      Constant Field
      +
      Value
      +{% for field in type.fields %} +
      {{ field.modifiers | join(' ') }} {{ field.type.display }}
      +
      {% if field.url is not empty %}{{ field.name }}{% else %}{{ field.name }}{% endif %}
      +
      {{ field.value }}
      +{% endfor %} +
      +{% endfor %} +
      +{% endfor %} + +
      +
      +
      + +
      +
      +
      + + + diff --git a/scripts/sync_java_docs/db-templates/javadoc-deprecated-list.peb b/scripts/sync_java_docs/db-templates/javadoc-deprecated-list.peb new file mode 100644 index 00000000..2058c59c --- /dev/null +++ b/scripts/sync_java_docs/db-templates/javadoc-deprecated-list.peb @@ -0,0 +1,82 @@ +{# + The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the + footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, + ...) so the accompanying stylesheet and the real one describe the same structure. +#} + + + +Deprecated List + + + + +
      +
      + +
      +
      +
      + +

      Deprecated API

      Contents

      +
      +{% if sections is empty %} +
      No deprecated API in this documentation.
      +{% endif %} +{# Pebble iterates a map as entries, so the section name is entry.key. #} +{% for section in sections %} +
      +
      {{ sectionTitle(section.key) }}
      +
      +
      Element
      +
      Description
      +{% for entry in section.value %} +
      {% if entry.url is not empty %}{{ entry.element }}{% else %}{{ entry.element }}{% endif %}
      +
      +{% if entry.forRemoval %}Terminally deprecated.{% endif %} +{% if entry.since is not empty %}Since {{ entry.since }}.{% endif %} +{% if entry.comment is not empty %}
      {{ entry.comment | raw }}
      {% endif %} +
      +{% endfor %} +
      +
      +{% endfor %} + +
      +
      +
      + +
      +
      +
      + + + +{% macro sectionTitle(kind) %} +{%- if kind == 'classes' -%}Deprecated Classes +{%- elseif kind == 'interfaces' -%}Deprecated Interfaces +{%- elseif kind == 'enums' -%}Deprecated Enum Classes +{%- elseif kind == 'exceptions' -%}Deprecated Exception Classes +{%- elseif kind == 'annotationTypes' -%}Deprecated Annotation Interfaces +{%- elseif kind == 'fields' -%}Deprecated Fields +{%- elseif kind == 'methods' -%}Deprecated Methods +{%- elseif kind == 'constructors' -%}Deprecated Constructors +{%- elseif kind == 'enumConstants' -%}Deprecated Enum Constants +{%- elseif kind == 'annotationElements' -%}Deprecated Annotation Elements +{%- else -%}Deprecated {{ kind }} +{%- endif -%} +{% endmacro %} \ No newline at end of file diff --git a/scripts/sync_java_docs/db-templates/javadoc-index.peb b/scripts/sync_java_docs/db-templates/javadoc-index.peb new file mode 100644 index 00000000..5d3f2387 --- /dev/null +++ b/scripts/sync_java_docs/db-templates/javadoc-index.peb @@ -0,0 +1,62 @@ +{# + The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the + footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, + ...) so the accompanying stylesheet and the real one describe the same structure. +#} + + + +{{ letter }}-Index + + + + +
      +
      + +
      +
      +
      + +
      +

      Index

      +
      +{# Pebble's loop.index is 0-based; the index files are numbered from 1. #} +{% for l in letters %}{{ l }}{% if not loop.last %} {% endif %}{% endfor %} +
      +
      +

      {{ letter }}

      +
      +{% for entry in entries %} +
      {% if entry.url is not empty %}{{ entry.label }}{% else %}{{ entry.label }}{% endif %} +{% if entry.containingElement is not empty %} - {{ entry.kind }} in {{ entry.containingElement }}{% else %} - {{ entry.kind }}{% endif %} +{% if entry.deprecated %}Deprecated.{% endif %} +
      +
      {% if entry.firstSentence is not empty %}
      {{ entry.firstSentence | raw }}
      {% endif %}
      +{% endfor %} +
      + +
      +
      +
      + +
      +
      +
      + + + diff --git a/scripts/sync_java_docs/db-templates/javadoc-module.peb b/scripts/sync_java_docs/db-templates/javadoc-module.peb new file mode 100644 index 00000000..896b410e --- /dev/null +++ b/scripts/sync_java_docs/db-templates/javadoc-module.peb @@ -0,0 +1,312 @@ +{# + The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the + footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, + ...) so the accompanying stylesheet and the real one describe the same structure. +#} + + + +{{ name }} + + + + +
      +
      + +
      +
      +
      + +
      +

      Module {{ name }}

      +
      + +
      +{% if description is not empty %}
      {{ description | raw }}
      {% endif %} +{% if hasModuleGraph %} +
      +
      Module Graph:
      +
      Module graph for {{ name }}Module graph for {{ name }}
      +
      +{% endif %} +{% if since is not empty or tags is not empty or seeAlso is not empty %} +
      +{% if since is not empty %}
      Since:
      {{ since | join(', ') }}
      {% endif %} +{% for tag in tags %}{% if tag.name != 'moduleGraph' and tag.name != 'uses' and tag.name != 'provides' %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endif %}{% endfor %} +{% if seeAlso is not empty %}
      See Also:
        {% for see in seeAlso %}
      • {{ see.label }}
      • {% endfor %}
      {% endif %} +
      +{% endif %} +
      + +
      +
        + +{% if requires is not empty %} +
      • +
        +

        Modules

        +
        Requires
        +
        +
        Modifier
        +
        Module
        +
        Description
        +{% for req in requires %} +
        {% if req.isTransitive %}transitive{% endif %}{% if req.isStatic %}static{% endif %}
        +
        {% if req.url is not empty %}{{ req.module }}{% else %}{{ req.module }}{% endif %}
        +
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if indirectRequires is not empty %} +
      • +
        +{% if requires is empty %}

        Modules

        {% endif %} +
        Indirect Requires
        +
        +
        Modifier
        +
        Module
        +
        Description
        +{% for req in indirectRequires %} +
        transitive
        +
        {% if req.url is not empty %}{{ req.module }}{% else %}{{ req.module }}{% endif %}
        +
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if exports is not empty %} +
      • +
        +

        Packages

        +
        Exports
        +
        +
        Package
        +
        Exported To Modules
        +
        Description
        +{% for export in exports %} +
        {% if export.url is not empty %}{{ export.packageName }}{% else %}{{ export.packageName }}{% endif %}
        +
        {% if export.to is not empty %}{{ export.to | join(', ') }}{% else %}All Modules{% endif %}
        +
        {{ export.firstSentence | raw }}
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if indirectExports is not empty %} +
      • +
        +
        Indirect Exports
        +
        +
        From
        +
        Packages
        +{% for entry in indirectExports %} +
        {% if entry.moduleUrl is not empty %}{{ entry.module }}{% else %}{{ entry.module }}{% endif %}
        +
        {% for pkg in entry.packages %}{% if pkg.url is not empty %}{{ pkg.name }}{% else %}{{ pkg.name }}{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if opens is not empty %} +
      • +
        +

        Opens

        +
        +
        Package
        +
        Opened To Modules
        +{% for open in opens %} +
        {% if open.url is not empty %}{{ open.packageName }}{% else %}{{ open.packageName }}{% endif %}
        +
        {% if open.to is not empty %}{{ open.to | join(', ') }}{% else %}All Modules{% endif %}
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if uses is not empty or provides is not empty %} +
      • +
        +

        Services

        +{% if uses is not empty %} +
        Uses
        +
        +
        Type
        +
        Description
        +{% for use in uses %} +
        {{ typeLink(use) }}
        +
        +{% endfor %} +
        +{% endif %} +{% if provides is not empty %} +
        Provides
        +
        +
        Type
        +
        Implementations
        +{% for provide in provides %} +
        {{ typeLink(provide.service) }}
        +
        {% for impl in provide.implementations %}{{ typeLink(impl) }}{% if not loop.last %}, {% endif %}{% endfor %}
        +{% endfor %} +
        +{% endif %} +
        +
      • +{% endif %} + +
      +
      + +
      +
      +
      + +
      +
      +
      + + + + +{% macro tagLabel(name) %} +{%- if name == 'apiNote' -%}API Note: +{%- elseif name == 'implSpec' -%}Implementation Requirements: +{%- elseif name == 'implNote' -%}Implementation Note: +{%- elseif name == 'jls' -%}See Java Language Specification: +{%- elseif name == 'jvms' -%}See Java Virtual Machine Specification: +{%- elseif name == 'serialData' -%}Serial Data: +{%- elseif name == 'serialField' -%}Serial Field: +{%- elseif name == 'serial' -%}Serial: +{%- elseif name == 'toolGuide' -%}Tool Guides: +{%- elseif name == 'revised' -%}Revised: +{%- elseif name == 'spec' -%}External Specifications: +{%- else -%}{{ name }}: +{%- endif -%} +{% endmacro %} +{% macro typeLink(ref) %} +{%- if ref is not empty -%} +{%- if ref.url is not empty -%} +{{ ref.display }} +{%- else -%} +{{ ref.display }} +{%- endif -%} +{%- endif -%} +{% endmacro %} +{% macro typeList(refs) %} +{%- for ref in refs -%}{{ typeLink(ref) }}{% if not loop.last %}, {% endif %}{%- endfor -%} +{% endmacro %} +{% macro memberLink(ref) %} +{%- if ref.url is not empty -%}{{ ref.name }}{%- else -%}{{ ref.name }}{%- endif -%} +{% endmacro %} +{% macro note(label, body) %} +
      {{ label }}
      +
      {{ body | raw }}
      +{% endmacro %} +{% macro modifiers(list) %} +{%- if list is not empty %}{{ list | join(' ') }} {% endif -%} +{% endmacro %} +{% macro parameters(params) %} +({%- for p in params -%}{{ typeLink(p.type) }} {{ p.name }}{% if not loop.last %}, {% endif %}{%- endfor -%}) +{% endmacro %} +{% macro throwsClause(exceptions) %} +{%- if exceptions is not empty %} throws {% for e in exceptions %}{{ typeLink(e.type) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif -%} +{% endmacro %} +{% macro rowColor(index) %}{% if index is odd %}odd-row-color{% else %}even-row-color{% endif %}{% endmacro %} +{% macro commonNotes(item) %} +{% if item.deprecated is not empty %} +
      Deprecated{% if item.deprecated.forRemoval %}, for removal: This API element is subject to removal in a future version{% endif %}. +{% if item.deprecated.since is not empty %}Since {{ item.deprecated.since }}.{% endif %} +{% if item.deprecated.comment is not empty %}
      {{ item.deprecated.comment | raw }}
      {% endif %} +
      +{% endif %} +{% if item.since is not empty or item.seeAlso is not empty or item.tags is not empty or item.authors is not empty or item.versions is not empty %} +
      +{% if item.since is not empty %}
      Since:
      {{ item.since | join(', ') }}
      {% endif %} +{% for tag in item.tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} +{% if item.authors is not empty %}
      Author:
      {{ item.authors | join(', ') }}
      {% endif %} +{% if item.versions is not empty %}
      Version:
      {{ item.versions | join(', ') }}
      {% endif %} +{% if item.seeAlso is not empty %} +
      See Also:
      +
        {% for see in item.seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      +{% endif %} +
      +{% endif %} +{% endmacro %} +{% macro fieldDetail(field) %} +
    • +
      +

      {{ field.name }}

      +
      {{ modifiers(field.modifiers) }}{{ typeLink(field.type) }} {{ field.name }}
      +{% if field.description is not empty %}
      {{ field.description | raw }}
      {% endif %} +{{ commonNotes(field) }} +{% if field.constantValue is not empty %} +
      Constant Field Value:
      {{ field.constantValue }}
      +{% endif %} +
      +
    • +{% endmacro %} +{% macro executableDetail(member) %} +
    • +
      +

      {{ member.name }}

      +
      {{ modifiers(member.modifiers) }}{% if member.typeParameters is not empty %}<{% for t in member.typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}> {% endif %}{% if member.returnType is not empty %}{{ typeLink(member.returnType) }} {% endif %}{{ member.name }}{{ parameters(member.parameters) }}{{ throwsClause(member.exceptions) }}
      +{% if member.description is not empty %}
      {{ member.description | raw }}
      {% endif %} +{% if member.specifiedBy is not empty or member.overrides is not empty or member.typeParameters is not empty or member.parameters is not empty or member.returns is not empty or member.exceptions is not empty %} +
      +{% for spec in member.specifiedBy %} +
      Specified by:
      +
      {{ memberLink(spec) }} in {{ spec.declaringType.kind | default('interface') }} {{ typeLink(spec.declaringType) }}
      +{% endfor %} +{% if member.overrides is not empty %} +
      Overrides:
      +
      {{ memberLink(member.overrides) }} in {{ member.overrides.declaringType.kind | default('class') }} {{ typeLink(member.overrides.declaringType) }}
      +{% endif %} +{% set hasTypeParamDocs = false %} +{% for t in member.typeParameters %}{% if t.description is not empty %}{% set hasTypeParamDocs = true %}{% endif %}{% endfor %} +{% if hasTypeParamDocs %} +
      Type Parameters:
      +{% for t in member.typeParameters %}{% if t.description is not empty %}
      {{ t.name }} - {{ t.description | raw }}
      {% endif %}{% endfor %} +{% endif %} +{% set hasParamDocs = false %} +{% for p in member.parameters %}{% if p.description is not empty %}{% set hasParamDocs = true %}{% endif %}{% endfor %} +{% if hasParamDocs %} +
      Parameters:
      +{% for p in member.parameters %}{% if p.description is not empty %}
      {{ p.name }} - {{ p.description | raw }}
      {% endif %}{% endfor %} +{% endif %} +{% if member.returns is not empty %}
      Returns:
      {{ member.returns | raw }}
      {% endif %} +{% if member.exceptions is not empty %} +
      Throws:
      +{% for e in member.exceptions %}
      {{ typeLink(e.type) }}{% if e.description is not empty %} - {{ e.description | raw }}{% endif %}
      {% endfor %} +{% endif %} +
      +{% endif %} +{% if member.defaultValue is not empty %} +
      Default:
      {{ member.defaultValue }}
      +{% endif %} +{{ commonNotes(member) }} +
      +
    • +{% endmacro %} \ No newline at end of file diff --git a/scripts/sync_java_docs/db-templates/javadoc-overview.peb b/scripts/sync_java_docs/db-templates/javadoc-overview.peb new file mode 100644 index 00000000..b3da79c2 --- /dev/null +++ b/scripts/sync_java_docs/db-templates/javadoc-overview.peb @@ -0,0 +1,80 @@ +{# + The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the + footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, + ...) so the accompanying stylesheet and the real one describe the same structure. +#} + + + +Overview + + + + +
      +
      + +
      +
      +
      + +
      +

      {{ title | default('API Documentation') }}

      +
      +
      +{% if modules is not empty %} +
      +
      Modules
      +
      +
      Module
      +
      Description
      +{% for module in modules %} + +
      {{ module.firstSentence | raw }}
      +{% endfor %} +
      +
      +{% endif %} +{# Only for a non-modular run: with modules present javadoc's overview lists just + the modules, and allpackages-index.html carries the package list. #} +{% if packages is not empty and modules is empty %} +
      +
      Packages
      +
      +
      Module
      +
      Package
      +
      Description
      +{% for pkg in packages %} +
      {{ pkg.moduleName }}
      + +
      {{ pkg.firstSentence | raw }}
      +{% endfor %} +
      +
      +{% endif %} +
      + +
      +
      +
      + +
      +
      +
      + + + diff --git a/scripts/sync_java_docs/db-templates/javadoc-package.peb b/scripts/sync_java_docs/db-templates/javadoc-package.peb new file mode 100644 index 00000000..f0d3e02c --- /dev/null +++ b/scripts/sync_java_docs/db-templates/javadoc-package.peb @@ -0,0 +1,237 @@ +{# + The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the + footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, + ...) so the accompanying stylesheet and the real one describe the same structure. +#} + + + +{{ name }} + + + + +
      +
      + +
      +
      +
      + +
      +{% if moduleName is not empty %} +
      Module {% if moduleUrl is not empty %}{{ moduleName }}{% else %}{{ moduleName }}{% endif %}
      +{% endif %} +

      Package {{ name }}

      +
      + +
      +
      package {{ name }}
      +{% if description is not empty %}
      {{ description | raw }}
      {% endif %} +{% if deprecated is not empty %} +
      Deprecated. +{% if deprecated.comment is not empty %}
      {{ deprecated.comment | raw }}
      {% endif %}
      +{% endif %} +{% if since is not empty or seeAlso is not empty or tags is not empty %} +
      +{% if since is not empty %}
      Since:
      {{ since | join(', ') }}
      {% endif %} +{% for tag in tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} +{% if seeAlso is not empty %}
      See Also:
        {% for see in seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      {% endif %} +
      +{% endif %} +
      + +
      +
        +{% if relatedPackages is not empty %} +
      • + +
      • +{% endif %} +
      • +
        +

        Classes and Interfaces

        +{{ typeTable("Interfaces", interfaces) }} +{{ typeTable("Classes", classes) }} +{{ typeTable("Enum Classes", enums) }} +{{ typeTable("Record Classes", records) }} +{{ typeTable("Exception Classes", exceptions) }} +{{ typeTable("Annotation Interfaces", annotationTypes) }} +
        +
      • +
      +
      + +
      +
      +
      + +
      +
      +
      + + + +{% macro typeTable(caption, rows) %} +{% if rows is not empty %} +
      {{ caption }}
      +
      +
      Class
      +
      Description
      +{% for row in rows %} + +
      {{ row.firstSentence | raw }}
      +{% endfor %} +
      +{% endif %} +{% endmacro %} +{% macro tagLabel(name) %} +{%- if name == 'apiNote' -%}API Note: +{%- elseif name == 'implSpec' -%}Implementation Requirements: +{%- elseif name == 'implNote' -%}Implementation Note: +{%- elseif name == 'jls' -%}See Java Language Specification: +{%- elseif name == 'jvms' -%}See Java Virtual Machine Specification: +{%- elseif name == 'serialData' -%}Serial Data: +{%- elseif name == 'serialField' -%}Serial Field: +{%- elseif name == 'serial' -%}Serial: +{%- elseif name == 'toolGuide' -%}Tool Guides: +{%- elseif name == 'revised' -%}Revised: +{%- elseif name == 'spec' -%}External Specifications: +{%- else -%}{{ name }}: +{%- endif -%} +{% endmacro %} +{% macro typeLink(ref) %} +{%- if ref is not empty -%} +{%- if ref.url is not empty -%} +{{ ref.display }} +{%- else -%} +{{ ref.display }} +{%- endif -%} +{%- endif -%} +{% endmacro %} +{% macro typeList(refs) %} +{%- for ref in refs -%}{{ typeLink(ref) }}{% if not loop.last %}, {% endif %}{%- endfor -%} +{% endmacro %} +{% macro memberLink(ref) %} +{%- if ref.url is not empty -%}{{ ref.name }}{%- else -%}{{ ref.name }}{%- endif -%} +{% endmacro %} +{% macro note(label, body) %} +
      {{ label }}
      +
      {{ body | raw }}
      +{% endmacro %} +{% macro modifiers(list) %} +{%- if list is not empty %}{{ list | join(' ') }} {% endif -%} +{% endmacro %} +{% macro parameters(params) %} +({%- for p in params -%}{{ typeLink(p.type) }} {{ p.name }}{% if not loop.last %}, {% endif %}{%- endfor -%}) +{% endmacro %} +{% macro throwsClause(exceptions) %} +{%- if exceptions is not empty %} throws {% for e in exceptions %}{{ typeLink(e.type) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif -%} +{% endmacro %} +{% macro rowColor(index) %}{% if index is odd %}odd-row-color{% else %}even-row-color{% endif %}{% endmacro %} +{% macro commonNotes(item) %} +{% if item.deprecated is not empty %} +
      Deprecated{% if item.deprecated.forRemoval %}, for removal: This API element is subject to removal in a future version{% endif %}. +{% if item.deprecated.since is not empty %}Since {{ item.deprecated.since }}.{% endif %} +{% if item.deprecated.comment is not empty %}
      {{ item.deprecated.comment | raw }}
      {% endif %} +
      +{% endif %} +{% if item.since is not empty or item.seeAlso is not empty or item.tags is not empty or item.authors is not empty or item.versions is not empty %} +
      +{% if item.since is not empty %}
      Since:
      {{ item.since | join(', ') }}
      {% endif %} +{% for tag in item.tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} +{% if item.authors is not empty %}
      Author:
      {{ item.authors | join(', ') }}
      {% endif %} +{% if item.versions is not empty %}
      Version:
      {{ item.versions | join(', ') }}
      {% endif %} +{% if item.seeAlso is not empty %} +
      See Also:
      +
        {% for see in item.seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      +{% endif %} +
      +{% endif %} +{% endmacro %} +{% macro fieldDetail(field) %} +
    • +
      +

      {{ field.name }}

      +
      {{ modifiers(field.modifiers) }}{{ typeLink(field.type) }} {{ field.name }}
      +{% if field.description is not empty %}
      {{ field.description | raw }}
      {% endif %} +{{ commonNotes(field) }} +{% if field.constantValue is not empty %} +
      Constant Field Value:
      {{ field.constantValue }}
      +{% endif %} +
      +
    • +{% endmacro %} +{% macro executableDetail(member) %} +
    • +
      +

      {{ member.name }}

      +
      {{ modifiers(member.modifiers) }}{% if member.typeParameters is not empty %}<{% for t in member.typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}> {% endif %}{% if member.returnType is not empty %}{{ typeLink(member.returnType) }} {% endif %}{{ member.name }}{{ parameters(member.parameters) }}{{ throwsClause(member.exceptions) }}
      +{% if member.description is not empty %}
      {{ member.description | raw }}
      {% endif %} +{% if member.specifiedBy is not empty or member.overrides is not empty or member.typeParameters is not empty or member.parameters is not empty or member.returns is not empty or member.exceptions is not empty %} +
      +{% for spec in member.specifiedBy %} +
      Specified by:
      +
      {{ memberLink(spec) }} in {{ spec.declaringType.kind | default('interface') }} {{ typeLink(spec.declaringType) }}
      +{% endfor %} +{% if member.overrides is not empty %} +
      Overrides:
      +
      {{ memberLink(member.overrides) }} in {{ member.overrides.declaringType.kind | default('class') }} {{ typeLink(member.overrides.declaringType) }}
      +{% endif %} +{% set hasTypeParamDocs = false %} +{% for t in member.typeParameters %}{% if t.description is not empty %}{% set hasTypeParamDocs = true %}{% endif %}{% endfor %} +{% if hasTypeParamDocs %} +
      Type Parameters:
      +{% for t in member.typeParameters %}{% if t.description is not empty %}
      {{ t.name }} - {{ t.description | raw }}
      {% endif %}{% endfor %} +{% endif %} +{% set hasParamDocs = false %} +{% for p in member.parameters %}{% if p.description is not empty %}{% set hasParamDocs = true %}{% endif %}{% endfor %} +{% if hasParamDocs %} +
      Parameters:
      +{% for p in member.parameters %}{% if p.description is not empty %}
      {{ p.name }} - {{ p.description | raw }}
      {% endif %}{% endfor %} +{% endif %} +{% if member.returns is not empty %}
      Returns:
      {{ member.returns | raw }}
      {% endif %} +{% if member.exceptions is not empty %} +
      Throws:
      +{% for e in member.exceptions %}
      {{ typeLink(e.type) }}{% if e.description is not empty %} - {{ e.description | raw }}{% endif %}
      {% endfor %} +{% endif %} +
      +{% endif %} +{% if member.defaultValue is not empty %} +
      Default:
      {{ member.defaultValue }}
      +{% endif %} +{{ commonNotes(member) }} +
      +
    • +{% endmacro %} \ No newline at end of file diff --git a/scripts/sync_java_docs/flatten_templates.py b/scripts/sync_java_docs/flatten_templates.py new file mode 100755 index 00000000..a28ab575 --- /dev/null +++ b/scripts/sync_java_docs/flatten_templates.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Flattens the pebble-renderer templates into standalone templates for documentation.db. + +The renderer in `pebble-renderer/` and the reader that serves documentation.db run Pebble in two +different environments, and the database's is the narrower one: + + * Templates are stored one per row in `Templates`, with no loader that can resolve + `{% extends "base" %}` or `{% import "macros" %}` by name. Every template already in the + database (page.peb, nav.peb, layout.pebble) is self-contained, so that is the contract. + * Only Pebble's built-in filters are available. The renderer's `href` and `doc` filters are Java + classes that ship with it and are not there. + +Rather than maintain a second, divergent copy of the templates by hand, this generates them: + + * `{% extends %}` is resolved by substituting the child's `{% block %}` bodies into the parent. + * `{% import %}` is resolved by appending the imported macro definitions. + * `| href` is dropped and `| doc` becomes `| raw`. Both are safe because sync_javadoc_json_to_db.py + rewrites the JSON's `.json` links to `.html` before insertion, which is the only thing those + filters did beyond marking documentation HTML as trusted. + +Run it whenever the pebble-renderer templates change. +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +BLOCK_RE = re.compile(r"\{%-?\s*block\s+(\w+)\s*-?%\}(.*?)\{%-?\s*endblock\s*-?%\}", re.DOTALL) +EXTENDS_RE = re.compile(r"\{%-?\s*extends\s+\"([^\"]+)\"\s*-?%\}") +IMPORT_RE = re.compile(r"\{%-?\s*import\s+\"([^\"]+)\"\s*-?%\}") +MACRO_RE = re.compile(r"\{%-?\s*macro\s+\w+.*?\{%-?\s*endmacro\s*-?%\}", re.DOTALL) + +# page kind (the JSON's `page` field) -> source template +PAGES = { + "class": "class", + "package": "package-summary", + "module": "module-summary", + "overview": "overview", + "all-classes": "all-classes", + "all-packages": "all-packages", + "deprecated-list": "deprecated-list", + "constant-values": "constant-values", + "index": "index-page", +} + + +def load(directory: Path, name: str) -> str: + return (directory / f"{name}.peb").read_text(encoding="utf-8") + + +def flatten(directory: Path, name: str) -> str: + source = load(directory, name) + + imported_macros: list[str] = [] + for imported in IMPORT_RE.findall(source): + imported_macros.extend(MACRO_RE.findall(load(directory, imported))) + source = IMPORT_RE.sub("", source) + + extends = EXTENDS_RE.search(source) + if extends: + parent = load(directory, extends.group(1)) + blocks = {n: b for n, b in BLOCK_RE.findall(source)} + # The parent's own block bodies are the defaults for blocks the child doesn't override. + parent = BLOCK_RE.sub(lambda m: blocks.get(m.group(1), m.group(2)), parent) + # Anything outside a block in a child template is discarded by Pebble, and macros the + # child defines itself must survive, so they are carried over explicitly. + own_macros = MACRO_RE.findall(EXTENDS_RE.sub("", source)) + source = parent + "\n" + "\n".join(own_macros) + + source = "\n".join([source, *imported_macros]) + + # The two renderer-only filters. `href` did nothing but swap the extension, which the sync + # script now does to the data itself; `doc` additionally marked the value as trusted HTML, + # which is Pebble's built-in `raw`. + source = re.sub(r"\|\s*href\b", "", source) + source = re.sub(r"\|\s*doc\b", "| raw", source) + + if re.search(r"\{%-?\s*(extends|import|include)\b", source): + raise SystemExit(f"{name}: template still references another template after flattening") + if re.search(r"\|\s*(href|doc)\b", source): + raise SystemExit(f"{name}: template still uses a renderer-only filter after flattening") + return source + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--templates", type=Path, + default=Path(__file__).resolve().parents[2] / "Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates", + help="the pebble-renderer template directory to flatten") + parser.add_argument("--out", type=Path, default=Path(__file__).resolve().parent / "db-templates", + help="where to write the flattened templates") + args = parser.parse_args() + + if not args.templates.is_dir(): + print(f"Error: {args.templates} is not a directory", file=sys.stderr) + return 2 + + args.out.mkdir(parents=True, exist_ok=True) + for kind, name in sorted(PAGES.items()): + flattened = flatten(args.templates, name) + target = args.out / f"javadoc-{kind}.peb" + target.write_text(flattened, encoding="utf-8") + print(f" {target.name:32s} {len(flattened):6d} bytes (page kind '{kind}')") + print(f"Wrote {len(PAGES)} template(s) to {args.out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/sync_java_docs/sync_javadoc_json_to_db.py b/scripts/sync_java_docs/sync_javadoc_json_to_db.py new file mode 100755 index 00000000..559aa877 --- /dev/null +++ b/scripts/sync_java_docs/sync_javadoc_json_to_db.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +"""Replaces the Java API documentation in documentation.db with javadoc-mode JSON. + +The Java docs currently sit in `Content` as scraped HTML under `j/html/api/`. This swaps each of +those rows' blob for the JSON the kdoc-to-json plugin's Javadoc mode produces, and points the row +at a Pebble template that renders it -- the same arrangement the Kotlin website docs already use +(`contentTypeID` stays text/html because that is what the *served* page is; the blob is JSON and +`templateId` names the template that turns one into the other). + +For every existing Content row under `j/html/api/`: + - Find the matching file in the JSON tree: strip `j/html/api/`, swap `.html` for `.json`. + - If it exists, rewrite it for the database (below), compress it the way the rest of the + database is compressed, and update `content`, `contentTypeID` and `templateId`. + - If it doesn't, leave the row alone. javadoc emits page kinds this pipeline does not + (class-use/, package-use, the tree pages, serialized-form) -- about half the rows -- and those + are working documentation whose paths the new pages do not link to anyway. Deleting them would + remove information from the database rather than add it, so it takes an explicit + --delete-missing. + +Three things are rewritten on the way in, all so the templates can be plain Pebble with no custom +filters (documentation.db's reader has none, and none of the templates already in there use any): + - `.json` links become `.html`, because the row's *path* -- and so the URL a browser asks for -- + ends in `.html`. The link text inside a doc comment is rewritten too, which is what the + renderer's `doc` filter did. + - `pathToRoot` is injected, since the templates need it for the stylesheet and the top nav and + the reader passes nothing but the JSON itself. + - `page` selects the template, so the mapping lives here rather than in the reader. + +A timestamped backup is taken before anything is written. +""" + +from __future__ import annotations + +import argparse +import atexit +import json +import re +import shutil +import sqlite3 +import subprocess +import sys +import tempfile +from datetime import datetime, timezone +from pathlib import Path + +PREFIX = "j/html/api/" + +# Only consulted with --delete-missing. A change in the plugin's output layout would otherwise gut +# the Java docs and report it as a clean run. +MAX_DELETE_FRACTION = 0.35 + +# The JSON's `page` field -> the template that renders it. Names match flatten_templates.py. +TEMPLATE_FOR_PAGE = { + "class": "javadoc-class.peb", + "package": "javadoc-package.peb", + "module": "javadoc-module.peb", + "overview": "javadoc-overview.peb", + "all-classes": "javadoc-all-classes.peb", + "all-packages": "javadoc-all-packages.peb", + "deprecated-list": "javadoc-deprecated-list.peb", + "constant-values": "javadoc-constant-values.peb", + "index": "javadoc-index.peb", +} + +# `.json` at the end of a string, or followed by a quote or a fragment. Applied to *parsed* JSON +# strings, so the quote here is a real one rather than a backslash-escaped one in the raw text. +JSON_EXTENSION = re.compile(r'\.json(?=["#]|$)') + + +def backup_database(db_path: str) -> str: + stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + backup = f"{db_path}.bak.{stamp}" + shutil.copy2(db_path, backup) + return backup + + +def load_compression_dictionary(conn): + """This database's shared Brotli dictionary (ADFA-5153), or None.""" + if conn.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='CompressionDictionary'" + ).fetchone() is None: + return None + row = conn.execute("SELECT data FROM CompressionDictionary WHERE id = 1").fetchone() + return row[0] if row and row[0] else None + + +class DictionaryBrotli: + """Compresses against a raw Brotli dictionary via the `brotli` CLI -- the Python package + exposes no dictionary parameter. Mirrors sync_kdoc_json_to_db.DictionaryBrotli.""" + + def __init__(self, dictionary_data: bytes): + path = shutil.which("brotli") + if path is None: + raise RuntimeError( + "this database uses a shared Brotli dictionary (ADFA-5153), which needs the " + "`brotli` command-line tool; install it (brew install brotli) and retry" + ) + self._brotli = path + self._dir = Path(tempfile.mkdtemp(prefix="sync-javadoc-brotli-")) + self._dict = self._dir / "dictionary.bin" + self._dict.write_bytes(dictionary_data) + atexit.register(lambda: shutil.rmtree(self._dir, ignore_errors=True)) + + def compress(self, data: bytes) -> bytes: + result = subprocess.run( + [self._brotli, "-D", str(self._dict), "-c"], + input=data, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, + ) + if result.returncode != 0: + raise RuntimeError(f"brotli failed: {result.stderr.decode(errors='replace').strip()}") + return result.stdout + + +def rewrite_links(value): + """Recursively swap `.json` for `.html` in every string of a parsed JSON document. + + Rewriting the parsed strings rather than the raw text is what makes this reliable: in the raw + text a link inside documentation HTML is `href=\\"List.json\\"`, whose quotes are escaped, and + a regex written against the unparsed form silently misses them. + """ + if isinstance(value, str): + return JSON_EXTENSION.sub(".html", value) + if isinstance(value, list): + return [rewrite_links(v) for v in value] + if isinstance(value, dict): + return {k: rewrite_links(v) for k, v in value.items()} + return value + + +def path_to_root(content_path: str) -> str: + """`j/html/api/java.base/java/util/ArrayList.html` -> `../../../`. + + Relative to the page, exactly as the file renderer computes it, so the templates that read it + behave identically whether they are serving from disk or from the database. + """ + depth = content_path[len(PREFIX):].count("/") + return "../" * depth + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("json_root", help="the javadoc-mode JSON tree (the api/ directory)") + parser.add_argument("--db", default="documentation.db", help="path to documentation.db") + parser.add_argument("--templates", type=Path, default=Path(__file__).resolve().parent / "db-templates", + help="directory of flattened templates to install") + parser.add_argument("--dry-run", action="store_true", help="report what would change, write nothing") + parser.add_argument("--delete-missing", action="store_true", + help="also delete rows with no JSON counterpart (class-use/, package-use, " + "the tree pages...). Off by default: those are working pages this " + "pipeline simply does not regenerate.") + args = parser.parse_args() + + json_root = Path(args.json_root) + if not json_root.is_dir(): + print(f"Error: '{json_root}' is not a directory.", file=sys.stderr) + return 2 + if not Path(args.db).is_file(): + print(f"Error: database '{args.db}' not found.", file=sys.stderr) + return 2 + if not args.templates.is_dir(): + print(f"Error: template directory '{args.templates}' not found; run flatten_templates.py first.", + file=sys.stderr) + return 2 + + if args.dry_run: + print("Dry run: no backup will be made and nothing will be written.") + else: + print(f"Backed up database to: {backup_database(args.db)}") + + conn = sqlite3.connect(args.db) + cur = conn.cursor() + + dictionary = load_compression_dictionary(conn) + compressor = DictionaryBrotli(dictionary) if dictionary else None + print(f"Compression: {'shared-dictionary Brotli' if compressor else 'plain Brotli'}") + + # --- templates ------------------------------------------------------- + template_ids: dict[str, int] = {} + for page_kind, filename in sorted(TEMPLATE_FOR_PAGE.items()): + source = (args.templates / filename).read_text(encoding="utf-8") + existing = cur.execute("SELECT id FROM Templates WHERE name = ?", (filename,)).fetchone() + if args.dry_run: + template_ids[page_kind] = existing[0] if existing else -1 + print(f" [{'UPDATE' if existing else 'INSERT'} TEMPLATE] {filename} ({len(source)} bytes)") + continue + if existing: + cur.execute("UPDATE Templates SET content = ? WHERE id = ?", (source.encode(), existing[0])) + template_ids[page_kind] = existing[0] + else: + cur.execute("INSERT INTO Templates (name, content) VALUES (?, ?)", (filename, source.encode())) + template_ids[page_kind] = cur.lastrowid + print(f"Installed {len(TEMPLATE_FOR_PAGE)} template(s).") + + html_type = cur.execute("SELECT id FROM ContentTypes WHERE value = 'text/html'").fetchone()[0] + + rows = cur.execute( + "SELECT id, path, contentTypeID FROM Content WHERE path LIKE ?", (PREFIX + "%",) + ).fetchall() + print(f"Found {len(rows)} existing Content row(s) under '{PREFIX}'.") + + updated = deleted = kept = 0 + delete_ids: list[int] = [] + unknown_pages: set[str] = set() + + passed_through = 0 + for row_id, path, content_type in rows: + source_file = json_root / (path[len(PREFIX):][: -len(".html")] + ".json") \ + if path.endswith(".html") else json_root / path[len(PREFIX):] + if not source_file.is_file(): + if args.delete_missing: + delete_ids.append(row_id) + deleted += 1 + else: + kept += 1 + continue + + # javadoc's plain-text manifest (element-list) and any other non-JSON file the tree + # carries is stored as-is: it is not a page, has no template, and needs no rewriting. + if source_file.suffix != ".json": + raw = source_file.read_bytes() + blob = compressor.compress(raw) if compressor else raw + if not args.dry_run: + cur.execute("UPDATE Content SET content = ?, templateId = 0 WHERE id = ?", (blob, row_id)) + passed_through += 1 + continue + + document = json.loads(source_file.read_text(encoding="utf-8")) + page_kind = document.get("page") + if page_kind not in TEMPLATE_FOR_PAGE: + unknown_pages.add(str(page_kind)) + kept += 1 + continue + + document = rewrite_links(document) + document["pathToRoot"] = path_to_root(path) + payload = json.dumps(document, separators=(",", ":")).encode("utf-8") + blob = compressor.compress(payload) if compressor else payload + + if not args.dry_run: + cur.execute( + "UPDATE Content SET content = ?, contentTypeID = ?, templateId = ? WHERE id = ?", + (blob, html_type, template_ids[page_kind], row_id), + ) + updated += 1 + + # --- new pages the JSON has and the database doesn't ------------------ + existing_paths = {path for _, path, _ in rows} + added = 0 + language_id = cur.execute("SELECT id FROM Languages WHERE value = 'en-US'").fetchone()[0] + for source_file in sorted(json_root.rglob("*.json")): + content_path = PREFIX + str(source_file.relative_to(json_root))[: -len(".json")] + ".html" + if content_path in existing_paths: + continue + document = json.loads(source_file.read_text(encoding="utf-8")) + page_kind = document.get("page") + if page_kind not in TEMPLATE_FOR_PAGE: + continue + document = rewrite_links(document) + document["pathToRoot"] = path_to_root(content_path) + payload = json.dumps(document, separators=(",", ":")).encode("utf-8") + blob = compressor.compress(payload) if compressor else payload + if not args.dry_run: + cur.execute( + "INSERT INTO Content (path, languageID, content, contentTypeID, templateId) " + "VALUES (?, ?, ?, ?, ?)", + (content_path, language_id, blob, html_type, template_ids[page_kind]), + ) + added += 1 + + if args.delete_missing and rows and deleted / len(rows) >= MAX_DELETE_FRACTION: + conn.rollback() + print( + f"\nAborting: {deleted} of {len(rows)} rows ({deleted / len(rows):.0%}) resolved to no " + f"JSON file, at or above the {MAX_DELETE_FRACTION:.0%} guard. Nothing was written.", + file=sys.stderr, + ) + if unknown_pages: + print(f"Unrecognised page kinds: {sorted(unknown_pages)}", file=sys.stderr) + return 1 + + if delete_ids and not args.dry_run: + cur.executemany("DELETE FROM Content WHERE id = ?", [(i,) for i in delete_ids]) + + if not args.dry_run: + cur.execute("INSERT INTO LastChange (documentationSet, who) VALUES (?, ?)", + ("java", "sync_javadoc_json_to_db.py")) + conn.commit() + conn.close() + + print(f"\nDone: updated {updated}, added {added}, deleted {deleted}, " + f"passed through unchanged {passed_through}.") + if kept: + print(f"Left {kept} row(s) as HTML -- no JSON counterpart (class-use/, package-use, the " + f"tree pages...). Pass --delete-missing to remove them instead.") + if unknown_pages: + print(f"Note: {len(unknown_pages)} unrecognised page kind(s) treated as deletions: {sorted(unknown_pages)}") + if args.dry_run: + print("(dry run - nothing was written)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From cf027da3c8b11a859e061f5c33590fb446ca8173 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Tue, 1 Sep 2026 15:22:46 -0500 Subject: [PATCH 08/14] ADFA-5296: Serve the Java docs from a single template instead of nine The web server reading documentation.db can only load one template per page, so nine per-page-kind templates cannot work. Replaced with one javadoc.peb that branches on the JSON's own `page` field, which every row already carries. This suits the templates better than the split did. All nine page kinds already shared a skeleton and a macro set, so flattening them separately duplicated both nine times; composing them into one emits each exactly once -- 41 KB against the 64 KB the nine came to. flatten_templates.py now composes rather than flattens: the base skeleton is emitted once with every {% block %} replaced by an if/elseif chain over `page`, falling back to the base's own body for a kind that doesn't override it, and each macro is emitted once. It checks the page templates share no macro names rather than assuming it, and still fails loudly if an extends/import, a renderer-only filter, or an unresolved block survives. sync_javadoc_json_to_db.py installs the single template and deletes the nine it previously created, so no Content row is left pointing at a template that is no longer there. Verified on /Users/alex/documentation.db: template and content both read back out of the database and rendered through a stock Pebble engine -- 9/9 page kinds render, titles are right, no .json link leaks, all 40 internal links on a rendered class page resolve to real rows, no row references a missing template, PRAGMA quick_check ok. Templates table is back to 5 rows. Co-Authored-By: Claude Opus 5 --- scripts/sync_java_docs/README.md | 26 +- .../db-templates/javadoc-all-classes.peb | 59 -- .../db-templates/javadoc-all-packages.peb | 59 -- .../db-templates/javadoc-class.peb | 453 ---------- .../db-templates/javadoc-constant-values.peb | 65 -- .../db-templates/javadoc-deprecated-list.peb | 82 -- .../db-templates/javadoc-index.peb | 62 -- .../db-templates/javadoc-module.peb | 312 ------- .../db-templates/javadoc-overview.peb | 80 -- .../db-templates/javadoc-package.peb | 237 ----- .../sync_java_docs/db-templates/javadoc.peb | 831 ++++++++++++++++++ scripts/sync_java_docs/flatten_templates.py | 158 ++-- .../sync_java_docs/sync_javadoc_json_to_db.py | 78 +- 13 files changed, 997 insertions(+), 1505 deletions(-) delete mode 100644 scripts/sync_java_docs/db-templates/javadoc-all-classes.peb delete mode 100644 scripts/sync_java_docs/db-templates/javadoc-all-packages.peb delete mode 100644 scripts/sync_java_docs/db-templates/javadoc-class.peb delete mode 100644 scripts/sync_java_docs/db-templates/javadoc-constant-values.peb delete mode 100644 scripts/sync_java_docs/db-templates/javadoc-deprecated-list.peb delete mode 100644 scripts/sync_java_docs/db-templates/javadoc-index.peb delete mode 100644 scripts/sync_java_docs/db-templates/javadoc-module.peb delete mode 100644 scripts/sync_java_docs/db-templates/javadoc-overview.peb delete mode 100644 scripts/sync_java_docs/db-templates/javadoc-package.peb create mode 100644 scripts/sync_java_docs/db-templates/javadoc.peb diff --git a/scripts/sync_java_docs/README.md b/scripts/sync_java_docs/README.md index 8fb231f8..2e7024a0 100644 --- a/scripts/sync_java_docs/README.md +++ b/scripts/sync_java_docs/README.md @@ -7,7 +7,7 @@ plugin's Javadoc mode produces, and installs the Pebble templates that render it # 1. Generate the JSON (see Dokka-plugin-kdoc2json/scripts/java) Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh -j /path/to/jdk-17 -# 2. Flatten the renderer's templates into standalone ones for the database +# 2. Compose the renderer's templates into the single one the database serves from python3 scripts/sync_java_docs/flatten_templates.py # 3. Look at what would change, then do it @@ -25,18 +25,26 @@ because the pieces disagree with each other at first glance: | `path` | `j/html/api/…/ArrayList.html` | unchanged — it is the URL a browser asks for | | `content` | **JSON**, shared-dictionary Brotli | the data; the template turns it into a page | | `contentTypeID` | `text/html` | the type of the *served* page, not of the blob | -| `templateId` | one of the nine `javadoc-*.peb` rows | chosen from the JSON's `page` field | +| `templateId` | the one `javadoc.peb` row | it branches on the JSON's `page` field | -## Why the templates are flattened +## Why there is one template, and why it is generated `pebble-renderer/` and the database's reader run Pebble in different environments, and the -database's is narrower: templates are stored one per row with no loader that resolves -`{% extends %}` / `{% import %}` by name, and only Pebble's built-in filters exist. Every template -already in the database is self-contained, so that is the contract. +database's is narrower on three counts: -`flatten_templates.py` therefore generates the database copies from the renderer's rather than -having a second set maintained by hand: it inlines the parent template and the imported macros, -drops the `href` filter and turns `doc` into the built-in `raw`. +- The reader **loads a single template per page** and a `Content` row names exactly one, so all + nine page kinds have to share it. +- There is no loader that resolves `{% extends %}` / `{% import %}` by name, so it must be + self-contained. Every template already in the database is. +- Only Pebble's built-in filters exist; the renderer's `href` and `doc` are Java classes that ship + with it. + +`flatten_templates.py` therefore composes `javadoc.peb` from the renderer's templates rather than +leaving a second set to be maintained by hand. It emits the base skeleton once with every +`{% block %}` replaced by an if/elseif chain over `page`, emits each macro once (checking the page +templates share no macro names rather than assuming it), drops the `href` filter and turns `doc` +into the built-in `raw`. Editing the database copy directly is a mistake -- edit the renderer's +templates and re-run. Three things are rewritten into the JSON on the way in, all so the templates need nothing beyond what the reader already passes: diff --git a/scripts/sync_java_docs/db-templates/javadoc-all-classes.peb b/scripts/sync_java_docs/db-templates/javadoc-all-classes.peb deleted file mode 100644 index afc6b55b..00000000 --- a/scripts/sync_java_docs/db-templates/javadoc-all-classes.peb +++ /dev/null @@ -1,59 +0,0 @@ -{# - The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the - footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, - ...) so the accompanying stylesheet and the real one describe the same structure. -#} - - - -All Classes and Interfaces - - - - -
      -
      - -
      -
      -
      - -

      All Classes and Interfaces

      -
      -
      Classes, Interfaces, Enums and Annotation Interfaces
      -
      -
      Class
      -
      Package
      -
      Description
      -{% for type in types %}{% if type.modifiers is empty or type.modifiers contains 'public' %} - -
      {{ type.packageName }}
      -
      {{ type.firstSentence | raw }}
      -{% endif %}{% endfor %} -
      -
      - -
      -
      -
      - -
      -
      -
      - - - diff --git a/scripts/sync_java_docs/db-templates/javadoc-all-packages.peb b/scripts/sync_java_docs/db-templates/javadoc-all-packages.peb deleted file mode 100644 index 9edbd334..00000000 --- a/scripts/sync_java_docs/db-templates/javadoc-all-packages.peb +++ /dev/null @@ -1,59 +0,0 @@ -{# - The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the - footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, - ...) so the accompanying stylesheet and the real one describe the same structure. -#} - - - -All Packages - - - - -
      -
      - -
      -
      -
      - -

      All Packages

      -
      -
      Package Summary
      -
      -
      Module
      -
      Package
      -
      Description
      -{% for pkg in packages %} -
      {{ pkg.moduleName }}
      - -
      {{ pkg.firstSentence | raw }}
      -{% endfor %} -
      -
      - -
      -
      -
      - -
      -
      -
      - - - diff --git a/scripts/sync_java_docs/db-templates/javadoc-class.peb b/scripts/sync_java_docs/db-templates/javadoc-class.peb deleted file mode 100644 index 9c7dd237..00000000 --- a/scripts/sync_java_docs/db-templates/javadoc-class.peb +++ /dev/null @@ -1,453 +0,0 @@ -{# - The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the - footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, - ...) so the accompanying stylesheet and the real one describe the same structure. -#} - - - -{{ name }} ({{ moduleName | default('API') }}) - - - - -
      -
      - -
      -
      -
      - -
      -{% if packageName is not empty %} - -{% endif %} -

      {{ kind | capitalize }} {{ name }}{% if typeParameters is not empty %}<{% for t in typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}>{% endif %}

      -
      - -
      -
      - -{# The inheritance tree, indented one step per level, exactly as javadoc draws it. #} -{% if inheritance is not empty and inheritance | length > 1 %} -{# javadoc nests one div per level so each generation indents further than the last. The final - entry is this class itself, shown as plain text rather than a link to the page you are on. #} -
      -{%- for ancestor in inheritance -%} -
      {% if loop.last %}{{ ancestor.qualifiedName }}{% else %}{{ typeLink(ancestor) }}{% endif %} -{%- endfor -%} -{%- for ancestor in inheritance -%}
      {%- endfor -%} -
      -{% endif %} - -{% if typeParameters is not empty and typeParameters | first is not null %} -{% set documentedTypeParams = false %} -{% for t in typeParameters %}{% if t.description is not empty %}{% set documentedTypeParams = true %}{% endif %}{% endfor %} -{% if documentedTypeParams %} -
      -
      Type Parameters:
      -{% for t in typeParameters %}{% if t.description is not empty %}
      {{ t.name }} - {{ t.description | raw }}
      {% endif %}{% endfor %} -
      -{% endif %} -{% endif %} - -{% if allImplementedInterfaces is not empty %} -
      All Implemented Interfaces:
      {{ typeList(allImplementedInterfaces) }}
      -{% endif %} -{% if allSuperinterfaces is not empty %} -
      All Superinterfaces:
      {{ typeList(allSuperinterfaces) }}
      -{% endif %} -{% if allKnownSubinterfaces is not empty %} -
      All Known Subinterfaces:
      {{ typeList(allKnownSubinterfaces) }}
      -{% endif %} -{% if allKnownImplementingClasses is not empty %} -
      All Known Implementing Classes:
      {{ typeList(allKnownImplementingClasses) }}
      -{% endif %} -{% if directKnownSubclasses is not empty %} -
      Direct Known Subclasses:
      {{ typeList(directKnownSubclasses) }}
      -{% endif %} -{% if enclosingType is not empty %} -
      Enclosing {{ enclosingType.kind | default('class') }}:
      {{ typeLink(enclosingType) }}
      -{% endif %} -{% if isFunctionalInterface %} -
      Functional Interface:
      This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.
      -{% endif %} - -
      -
      {{ signature }}
      -{% if description is not empty %}
      {{ description | raw }}
      {% endif %} -{% if deprecated is not empty %} -
      Deprecated{% if deprecated.forRemoval %}, for removal: This API element is subject to removal in a future version{% endif %}. -{% if deprecated.since is not empty %}Since {{ deprecated.since }}.{% endif %} -{% if deprecated.comment is not empty %}
      {{ deprecated.comment | raw }}
      {% endif %} -
      -{% endif %} -{% if since is not empty or seeAlso is not empty or tags is not empty or authors is not empty or versions is not empty %} -
      -{% if since is not empty %}
      Since:
      {{ since | join(', ') }}
      {% endif %} -{% for tag in tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} -{% if authors is not empty %}
      Author:
      {{ authors | join(', ') }}
      {% endif %} -{% if versions is not empty %}
      Version:
      {{ versions | join(', ') }}
      {% endif %} -{% if seeAlso is not empty %} -
      See Also:
      -
        {% for see in seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      -{% endif %} -
      -{% endif %} -
      - -
      -
        - -{% if nestedTypes is not empty or inheritedNestedTypes is not empty %} -
      • -
        -

        Nested Class Summary

        -{% if nestedTypes is not empty %} -
        Nested Classes
        -
        -
        Modifier and Type
        -
        Class
        -
        Description
        -{% for nested in nestedTypes %} -
        {{ nested.modifiers | join(' ') }} {{ nested.kind }}
        - -
        {{ nested.firstSentence | raw }}
        -{% endfor %} -
        -{% endif %} -{% for group in inheritedNestedTypes %} -
        -

        Nested classes/interfaces declared in {{ group.declaringType.kind | default('class') }} {{ typeLink(group.declaringType) }}

        -{% for member in group.members %}{{ memberLink(member) }}{% if not loop.last %}, {% endif %}{% endfor %} -
        -{% endfor %} -
        -
      • -{% endif %} - -{% if enumConstants is not empty %} -
      • -
        -

        Enum Constant Summary

        -
        Enum Constants
        -
        -
        Enum Constant
        -
        Description
        -{% for field in enumConstants %} - -
        {{ field.firstSentence | raw }}
        -{% endfor %} -
        -
        -
      • -{% endif %} - -{% if fields is not empty or inheritedFields is not empty %} -
      • -
        -

        Field Summary

        -{% if fields is not empty %} -
        Fields
        -
        -
        Modifier and Type
        -
        Field
        -
        Description
        -{% for field in fields %} -
        {{ field.modifiers | join(' ') }} {{ typeLink(field.type) }}
        - -
        {{ field.firstSentence | raw }}
        -{% endfor %} -
        -{% endif %} -{% for group in inheritedFields %} -
        -

        Fields declared in {{ group.declaringType.kind | default('class') }} {{ typeLink(group.declaringType) }}

        -{% for member in group.members %}{{ memberLink(member) }}{% if not loop.last %}, {% endif %}{% endfor %} -
        -{% endfor %} -
        -
      • -{% endif %} - -{% if constructors is not empty %} -
      • -
        -

        Constructor Summary

        -
        Constructors
        -
        -
        Constructor
        -
        Description
        -{% for ctor in constructors %} -
        {{ ctor.name }}{{ parameters(ctor.parameters) }}
        -
        {{ ctor.firstSentence | raw }}
        -{% endfor %} -
        -
        -
      • -{% endif %} - -{% if annotationElements is not empty %} -
      • -
        -

        Element Summary

        -
        Elements
        -
        -
        Modifier and Type
        -
        Element
        -
        Description
        -{% for element in annotationElements %} -
        {{ typeLink(element.returnType) }}
        - -
        {{ element.firstSentence | raw }}
        -{% endfor %} -
        -
        -
      • -{% endif %} - -{% if methods is not empty or inheritedMethods is not empty %} -
      • -
        -

        Method Summary

        -{% if methods is not empty %} -
        All Methods
        -
        -
        Modifier and Type
        -
        Method
        -
        Description
        -{% for method in methods %} -
        {{ method.modifiers | join(' ') }} {{ typeLink(method.returnType) }}
        -
        {{ method.name }}{{ parameters(method.parameters) }}
        -
        {{ method.firstSentence | raw }}
        -{% endfor %} -
        -{% endif %} -{% for group in inheritedMethods %} -
        -

        Methods declared in {{ group.declaringType.kind | default('class') }} {{ typeLink(group.declaringType) }}

        -{% for member in group.members %}{{ memberLink(member) }}{% if not loop.last %}, {% endif %}{% endfor %} -
        -{% endfor %} -
        -
      • -{% endif %} - -
      -
      - -
      -
        - -{% if enumConstants is not empty %} -
      • -
        -

        Enum Constant Details

        -
          -{% for field in enumConstants %}{{ fieldDetail(field) }}{% endfor %} -
        -
        -
      • -{% endif %} - -{% if fields is not empty %} -
      • -
        -

        Field Details

        -
          -{% for field in fields %}{{ fieldDetail(field) }}{% endfor %} -
        -
        -
      • -{% endif %} - -{% if constructors is not empty %} -
      • -
        -

        Constructor Details

        -
          -{% for ctor in constructors %}{{ executableDetail(ctor) }}{% endfor %} -
        -
        -
      • -{% endif %} - -{% if annotationElements is not empty %} -
      • -
        -

        Element Details

        -
          -{% for element in annotationElements %}{{ executableDetail(element) }}{% endfor %} -
        -
        -
      • -{% endif %} - -{% if methods is not empty %} -
      • -
        -

        Method Details

        -
          -{% for method in methods %}{{ executableDetail(method) }}{% endfor %} -
        -
        -
      • -{% endif %} - -
      -
      - -
      -
      -
      - -
      -
      -
      - - - - -{% macro tagLabel(name) %} -{%- if name == 'apiNote' -%}API Note: -{%- elseif name == 'implSpec' -%}Implementation Requirements: -{%- elseif name == 'implNote' -%}Implementation Note: -{%- elseif name == 'jls' -%}See Java Language Specification: -{%- elseif name == 'jvms' -%}See Java Virtual Machine Specification: -{%- elseif name == 'serialData' -%}Serial Data: -{%- elseif name == 'serialField' -%}Serial Field: -{%- elseif name == 'serial' -%}Serial: -{%- elseif name == 'toolGuide' -%}Tool Guides: -{%- elseif name == 'revised' -%}Revised: -{%- elseif name == 'spec' -%}External Specifications: -{%- else -%}{{ name }}: -{%- endif -%} -{% endmacro %} -{% macro typeLink(ref) %} -{%- if ref is not empty -%} -{%- if ref.url is not empty -%} -{{ ref.display }} -{%- else -%} -{{ ref.display }} -{%- endif -%} -{%- endif -%} -{% endmacro %} -{% macro typeList(refs) %} -{%- for ref in refs -%}{{ typeLink(ref) }}{% if not loop.last %}, {% endif %}{%- endfor -%} -{% endmacro %} -{% macro memberLink(ref) %} -{%- if ref.url is not empty -%}{{ ref.name }}{%- else -%}{{ ref.name }}{%- endif -%} -{% endmacro %} -{% macro note(label, body) %} -
      {{ label }}
      -
      {{ body | raw }}
      -{% endmacro %} -{% macro modifiers(list) %} -{%- if list is not empty %}{{ list | join(' ') }} {% endif -%} -{% endmacro %} -{% macro parameters(params) %} -({%- for p in params -%}{{ typeLink(p.type) }} {{ p.name }}{% if not loop.last %}, {% endif %}{%- endfor -%}) -{% endmacro %} -{% macro throwsClause(exceptions) %} -{%- if exceptions is not empty %} throws {% for e in exceptions %}{{ typeLink(e.type) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif -%} -{% endmacro %} -{% macro rowColor(index) %}{% if index is odd %}odd-row-color{% else %}even-row-color{% endif %}{% endmacro %} -{% macro commonNotes(item) %} -{% if item.deprecated is not empty %} -
      Deprecated{% if item.deprecated.forRemoval %}, for removal: This API element is subject to removal in a future version{% endif %}. -{% if item.deprecated.since is not empty %}Since {{ item.deprecated.since }}.{% endif %} -{% if item.deprecated.comment is not empty %}
      {{ item.deprecated.comment | raw }}
      {% endif %} -
      -{% endif %} -{% if item.since is not empty or item.seeAlso is not empty or item.tags is not empty or item.authors is not empty or item.versions is not empty %} -
      -{% if item.since is not empty %}
      Since:
      {{ item.since | join(', ') }}
      {% endif %} -{% for tag in item.tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} -{% if item.authors is not empty %}
      Author:
      {{ item.authors | join(', ') }}
      {% endif %} -{% if item.versions is not empty %}
      Version:
      {{ item.versions | join(', ') }}
      {% endif %} -{% if item.seeAlso is not empty %} -
      See Also:
      -
        {% for see in item.seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      -{% endif %} -
      -{% endif %} -{% endmacro %} -{% macro fieldDetail(field) %} -
    • -
      -

      {{ field.name }}

      -
      {{ modifiers(field.modifiers) }}{{ typeLink(field.type) }} {{ field.name }}
      -{% if field.description is not empty %}
      {{ field.description | raw }}
      {% endif %} -{{ commonNotes(field) }} -{% if field.constantValue is not empty %} -
      Constant Field Value:
      {{ field.constantValue }}
      -{% endif %} -
      -
    • -{% endmacro %} -{% macro executableDetail(member) %} -
    • -
      -

      {{ member.name }}

      -
      {{ modifiers(member.modifiers) }}{% if member.typeParameters is not empty %}<{% for t in member.typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}> {% endif %}{% if member.returnType is not empty %}{{ typeLink(member.returnType) }} {% endif %}{{ member.name }}{{ parameters(member.parameters) }}{{ throwsClause(member.exceptions) }}
      -{% if member.description is not empty %}
      {{ member.description | raw }}
      {% endif %} -{% if member.specifiedBy is not empty or member.overrides is not empty or member.typeParameters is not empty or member.parameters is not empty or member.returns is not empty or member.exceptions is not empty %} -
      -{% for spec in member.specifiedBy %} -
      Specified by:
      -
      {{ memberLink(spec) }} in {{ spec.declaringType.kind | default('interface') }} {{ typeLink(spec.declaringType) }}
      -{% endfor %} -{% if member.overrides is not empty %} -
      Overrides:
      -
      {{ memberLink(member.overrides) }} in {{ member.overrides.declaringType.kind | default('class') }} {{ typeLink(member.overrides.declaringType) }}
      -{% endif %} -{% set hasTypeParamDocs = false %} -{% for t in member.typeParameters %}{% if t.description is not empty %}{% set hasTypeParamDocs = true %}{% endif %}{% endfor %} -{% if hasTypeParamDocs %} -
      Type Parameters:
      -{% for t in member.typeParameters %}{% if t.description is not empty %}
      {{ t.name }} - {{ t.description | raw }}
      {% endif %}{% endfor %} -{% endif %} -{% set hasParamDocs = false %} -{% for p in member.parameters %}{% if p.description is not empty %}{% set hasParamDocs = true %}{% endif %}{% endfor %} -{% if hasParamDocs %} -
      Parameters:
      -{% for p in member.parameters %}{% if p.description is not empty %}
      {{ p.name }} - {{ p.description | raw }}
      {% endif %}{% endfor %} -{% endif %} -{% if member.returns is not empty %}
      Returns:
      {{ member.returns | raw }}
      {% endif %} -{% if member.exceptions is not empty %} -
      Throws:
      -{% for e in member.exceptions %}
      {{ typeLink(e.type) }}{% if e.description is not empty %} - {{ e.description | raw }}{% endif %}
      {% endfor %} -{% endif %} -
      -{% endif %} -{% if member.defaultValue is not empty %} -
      Default:
      {{ member.defaultValue }}
      -{% endif %} -{{ commonNotes(member) }} -
      -
    • -{% endmacro %} \ No newline at end of file diff --git a/scripts/sync_java_docs/db-templates/javadoc-constant-values.peb b/scripts/sync_java_docs/db-templates/javadoc-constant-values.peb deleted file mode 100644 index 60656623..00000000 --- a/scripts/sync_java_docs/db-templates/javadoc-constant-values.peb +++ /dev/null @@ -1,65 +0,0 @@ -{# - The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the - footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, - ...) so the accompanying stylesheet and the real one describe the same structure. -#} - - - -Constant Field Values - - - - -
      -
      - -
      -
      -
      - -

      Constant Field Values

      Contents

      -{# Pebble iterates a map as entries: entry.key is the package, entry.value its types. #} -{% for group in packages %} -
      -

      {{ group.key }}

      -{% for type in group.value %} -
      {% if type.url is not empty %}{{ type.qualifiedName }}{% else %}{{ type.qualifiedName }}{% endif %}
      -
      -
      Modifier and Type
      -
      Constant Field
      -
      Value
      -{% for field in type.fields %} -
      {{ field.modifiers | join(' ') }} {{ field.type.display }}
      -
      {% if field.url is not empty %}{{ field.name }}{% else %}{{ field.name }}{% endif %}
      -
      {{ field.value }}
      -{% endfor %} -
      -{% endfor %} -
      -{% endfor %} - -
      -
      -
      - -
      -
      -
      - - - diff --git a/scripts/sync_java_docs/db-templates/javadoc-deprecated-list.peb b/scripts/sync_java_docs/db-templates/javadoc-deprecated-list.peb deleted file mode 100644 index 2058c59c..00000000 --- a/scripts/sync_java_docs/db-templates/javadoc-deprecated-list.peb +++ /dev/null @@ -1,82 +0,0 @@ -{# - The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the - footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, - ...) so the accompanying stylesheet and the real one describe the same structure. -#} - - - -Deprecated List - - - - -
      -
      - -
      -
      -
      - -

      Deprecated API

      Contents

      -
      -{% if sections is empty %} -
      No deprecated API in this documentation.
      -{% endif %} -{# Pebble iterates a map as entries, so the section name is entry.key. #} -{% for section in sections %} -
      -
      {{ sectionTitle(section.key) }}
      -
      -
      Element
      -
      Description
      -{% for entry in section.value %} -
      {% if entry.url is not empty %}{{ entry.element }}{% else %}{{ entry.element }}{% endif %}
      -
      -{% if entry.forRemoval %}Terminally deprecated.{% endif %} -{% if entry.since is not empty %}Since {{ entry.since }}.{% endif %} -{% if entry.comment is not empty %}
      {{ entry.comment | raw }}
      {% endif %} -
      -{% endfor %} -
      -
      -{% endfor %} - -
      -
      -
      - -
      -
      -
      - - - -{% macro sectionTitle(kind) %} -{%- if kind == 'classes' -%}Deprecated Classes -{%- elseif kind == 'interfaces' -%}Deprecated Interfaces -{%- elseif kind == 'enums' -%}Deprecated Enum Classes -{%- elseif kind == 'exceptions' -%}Deprecated Exception Classes -{%- elseif kind == 'annotationTypes' -%}Deprecated Annotation Interfaces -{%- elseif kind == 'fields' -%}Deprecated Fields -{%- elseif kind == 'methods' -%}Deprecated Methods -{%- elseif kind == 'constructors' -%}Deprecated Constructors -{%- elseif kind == 'enumConstants' -%}Deprecated Enum Constants -{%- elseif kind == 'annotationElements' -%}Deprecated Annotation Elements -{%- else -%}Deprecated {{ kind }} -{%- endif -%} -{% endmacro %} \ No newline at end of file diff --git a/scripts/sync_java_docs/db-templates/javadoc-index.peb b/scripts/sync_java_docs/db-templates/javadoc-index.peb deleted file mode 100644 index 5d3f2387..00000000 --- a/scripts/sync_java_docs/db-templates/javadoc-index.peb +++ /dev/null @@ -1,62 +0,0 @@ -{# - The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the - footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, - ...) so the accompanying stylesheet and the real one describe the same structure. -#} - - - -{{ letter }}-Index - - - - -
      -
      - -
      -
      -
      - -
      -

      Index

      -
      -{# Pebble's loop.index is 0-based; the index files are numbered from 1. #} -{% for l in letters %}{{ l }}{% if not loop.last %} {% endif %}{% endfor %} -
      -
      -

      {{ letter }}

      -
      -{% for entry in entries %} -
      {% if entry.url is not empty %}{{ entry.label }}{% else %}{{ entry.label }}{% endif %} -{% if entry.containingElement is not empty %} - {{ entry.kind }} in {{ entry.containingElement }}{% else %} - {{ entry.kind }}{% endif %} -{% if entry.deprecated %}Deprecated.{% endif %} -
      -
      {% if entry.firstSentence is not empty %}
      {{ entry.firstSentence | raw }}
      {% endif %}
      -{% endfor %} -
      - -
      -
      -
      - -
      -
      -
      - - - diff --git a/scripts/sync_java_docs/db-templates/javadoc-module.peb b/scripts/sync_java_docs/db-templates/javadoc-module.peb deleted file mode 100644 index 896b410e..00000000 --- a/scripts/sync_java_docs/db-templates/javadoc-module.peb +++ /dev/null @@ -1,312 +0,0 @@ -{# - The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the - footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, - ...) so the accompanying stylesheet and the real one describe the same structure. -#} - - - -{{ name }} - - - - -
      -
      - -
      -
      -
      - -
      -

      Module {{ name }}

      -
      - -
      -{% if description is not empty %}
      {{ description | raw }}
      {% endif %} -{% if hasModuleGraph %} -
      -
      Module Graph:
      -
      Module graph for {{ name }}Module graph for {{ name }}
      -
      -{% endif %} -{% if since is not empty or tags is not empty or seeAlso is not empty %} -
      -{% if since is not empty %}
      Since:
      {{ since | join(', ') }}
      {% endif %} -{% for tag in tags %}{% if tag.name != 'moduleGraph' and tag.name != 'uses' and tag.name != 'provides' %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endif %}{% endfor %} -{% if seeAlso is not empty %}
      See Also:
        {% for see in seeAlso %}
      • {{ see.label }}
      • {% endfor %}
      {% endif %} -
      -{% endif %} -
      - -
      -
        - -{% if requires is not empty %} -
      • -
        -

        Modules

        -
        Requires
        -
        -
        Modifier
        -
        Module
        -
        Description
        -{% for req in requires %} -
        {% if req.isTransitive %}transitive{% endif %}{% if req.isStatic %}static{% endif %}
        -
        {% if req.url is not empty %}{{ req.module }}{% else %}{{ req.module }}{% endif %}
        -
        -{% endfor %} -
        -
        -
      • -{% endif %} - -{% if indirectRequires is not empty %} -
      • -
        -{% if requires is empty %}

        Modules

        {% endif %} -
        Indirect Requires
        -
        -
        Modifier
        -
        Module
        -
        Description
        -{% for req in indirectRequires %} -
        transitive
        -
        {% if req.url is not empty %}{{ req.module }}{% else %}{{ req.module }}{% endif %}
        -
        -{% endfor %} -
        -
        -
      • -{% endif %} - -{% if exports is not empty %} -
      • -
        -

        Packages

        -
        Exports
        -
        -
        Package
        -
        Exported To Modules
        -
        Description
        -{% for export in exports %} -
        {% if export.url is not empty %}{{ export.packageName }}{% else %}{{ export.packageName }}{% endif %}
        -
        {% if export.to is not empty %}{{ export.to | join(', ') }}{% else %}All Modules{% endif %}
        -
        {{ export.firstSentence | raw }}
        -{% endfor %} -
        -
        -
      • -{% endif %} - -{% if indirectExports is not empty %} -
      • -
        -
        Indirect Exports
        -
        -
        From
        -
        Packages
        -{% for entry in indirectExports %} -
        {% if entry.moduleUrl is not empty %}{{ entry.module }}{% else %}{{ entry.module }}{% endif %}
        -
        {% for pkg in entry.packages %}{% if pkg.url is not empty %}{{ pkg.name }}{% else %}{{ pkg.name }}{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}
        -{% endfor %} -
        -
        -
      • -{% endif %} - -{% if opens is not empty %} -
      • -
        -

        Opens

        -
        -
        Package
        -
        Opened To Modules
        -{% for open in opens %} -
        {% if open.url is not empty %}{{ open.packageName }}{% else %}{{ open.packageName }}{% endif %}
        -
        {% if open.to is not empty %}{{ open.to | join(', ') }}{% else %}All Modules{% endif %}
        -{% endfor %} -
        -
        -
      • -{% endif %} - -{% if uses is not empty or provides is not empty %} -
      • -
        -

        Services

        -{% if uses is not empty %} -
        Uses
        -
        -
        Type
        -
        Description
        -{% for use in uses %} -
        {{ typeLink(use) }}
        -
        -{% endfor %} -
        -{% endif %} -{% if provides is not empty %} -
        Provides
        -
        -
        Type
        -
        Implementations
        -{% for provide in provides %} -
        {{ typeLink(provide.service) }}
        -
        {% for impl in provide.implementations %}{{ typeLink(impl) }}{% if not loop.last %}, {% endif %}{% endfor %}
        -{% endfor %} -
        -{% endif %} -
        -
      • -{% endif %} - -
      -
      - -
      -
      -
      - -
      -
      -
      - - - - -{% macro tagLabel(name) %} -{%- if name == 'apiNote' -%}API Note: -{%- elseif name == 'implSpec' -%}Implementation Requirements: -{%- elseif name == 'implNote' -%}Implementation Note: -{%- elseif name == 'jls' -%}See Java Language Specification: -{%- elseif name == 'jvms' -%}See Java Virtual Machine Specification: -{%- elseif name == 'serialData' -%}Serial Data: -{%- elseif name == 'serialField' -%}Serial Field: -{%- elseif name == 'serial' -%}Serial: -{%- elseif name == 'toolGuide' -%}Tool Guides: -{%- elseif name == 'revised' -%}Revised: -{%- elseif name == 'spec' -%}External Specifications: -{%- else -%}{{ name }}: -{%- endif -%} -{% endmacro %} -{% macro typeLink(ref) %} -{%- if ref is not empty -%} -{%- if ref.url is not empty -%} -{{ ref.display }} -{%- else -%} -{{ ref.display }} -{%- endif -%} -{%- endif -%} -{% endmacro %} -{% macro typeList(refs) %} -{%- for ref in refs -%}{{ typeLink(ref) }}{% if not loop.last %}, {% endif %}{%- endfor -%} -{% endmacro %} -{% macro memberLink(ref) %} -{%- if ref.url is not empty -%}{{ ref.name }}{%- else -%}{{ ref.name }}{%- endif -%} -{% endmacro %} -{% macro note(label, body) %} -
      {{ label }}
      -
      {{ body | raw }}
      -{% endmacro %} -{% macro modifiers(list) %} -{%- if list is not empty %}{{ list | join(' ') }} {% endif -%} -{% endmacro %} -{% macro parameters(params) %} -({%- for p in params -%}{{ typeLink(p.type) }} {{ p.name }}{% if not loop.last %}, {% endif %}{%- endfor -%}) -{% endmacro %} -{% macro throwsClause(exceptions) %} -{%- if exceptions is not empty %} throws {% for e in exceptions %}{{ typeLink(e.type) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif -%} -{% endmacro %} -{% macro rowColor(index) %}{% if index is odd %}odd-row-color{% else %}even-row-color{% endif %}{% endmacro %} -{% macro commonNotes(item) %} -{% if item.deprecated is not empty %} -
      Deprecated{% if item.deprecated.forRemoval %}, for removal: This API element is subject to removal in a future version{% endif %}. -{% if item.deprecated.since is not empty %}Since {{ item.deprecated.since }}.{% endif %} -{% if item.deprecated.comment is not empty %}
      {{ item.deprecated.comment | raw }}
      {% endif %} -
      -{% endif %} -{% if item.since is not empty or item.seeAlso is not empty or item.tags is not empty or item.authors is not empty or item.versions is not empty %} -
      -{% if item.since is not empty %}
      Since:
      {{ item.since | join(', ') }}
      {% endif %} -{% for tag in item.tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} -{% if item.authors is not empty %}
      Author:
      {{ item.authors | join(', ') }}
      {% endif %} -{% if item.versions is not empty %}
      Version:
      {{ item.versions | join(', ') }}
      {% endif %} -{% if item.seeAlso is not empty %} -
      See Also:
      -
        {% for see in item.seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      -{% endif %} -
      -{% endif %} -{% endmacro %} -{% macro fieldDetail(field) %} -
    • -
      -

      {{ field.name }}

      -
      {{ modifiers(field.modifiers) }}{{ typeLink(field.type) }} {{ field.name }}
      -{% if field.description is not empty %}
      {{ field.description | raw }}
      {% endif %} -{{ commonNotes(field) }} -{% if field.constantValue is not empty %} -
      Constant Field Value:
      {{ field.constantValue }}
      -{% endif %} -
      -
    • -{% endmacro %} -{% macro executableDetail(member) %} -
    • -
      -

      {{ member.name }}

      -
      {{ modifiers(member.modifiers) }}{% if member.typeParameters is not empty %}<{% for t in member.typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}> {% endif %}{% if member.returnType is not empty %}{{ typeLink(member.returnType) }} {% endif %}{{ member.name }}{{ parameters(member.parameters) }}{{ throwsClause(member.exceptions) }}
      -{% if member.description is not empty %}
      {{ member.description | raw }}
      {% endif %} -{% if member.specifiedBy is not empty or member.overrides is not empty or member.typeParameters is not empty or member.parameters is not empty or member.returns is not empty or member.exceptions is not empty %} -
      -{% for spec in member.specifiedBy %} -
      Specified by:
      -
      {{ memberLink(spec) }} in {{ spec.declaringType.kind | default('interface') }} {{ typeLink(spec.declaringType) }}
      -{% endfor %} -{% if member.overrides is not empty %} -
      Overrides:
      -
      {{ memberLink(member.overrides) }} in {{ member.overrides.declaringType.kind | default('class') }} {{ typeLink(member.overrides.declaringType) }}
      -{% endif %} -{% set hasTypeParamDocs = false %} -{% for t in member.typeParameters %}{% if t.description is not empty %}{% set hasTypeParamDocs = true %}{% endif %}{% endfor %} -{% if hasTypeParamDocs %} -
      Type Parameters:
      -{% for t in member.typeParameters %}{% if t.description is not empty %}
      {{ t.name }} - {{ t.description | raw }}
      {% endif %}{% endfor %} -{% endif %} -{% set hasParamDocs = false %} -{% for p in member.parameters %}{% if p.description is not empty %}{% set hasParamDocs = true %}{% endif %}{% endfor %} -{% if hasParamDocs %} -
      Parameters:
      -{% for p in member.parameters %}{% if p.description is not empty %}
      {{ p.name }} - {{ p.description | raw }}
      {% endif %}{% endfor %} -{% endif %} -{% if member.returns is not empty %}
      Returns:
      {{ member.returns | raw }}
      {% endif %} -{% if member.exceptions is not empty %} -
      Throws:
      -{% for e in member.exceptions %}
      {{ typeLink(e.type) }}{% if e.description is not empty %} - {{ e.description | raw }}{% endif %}
      {% endfor %} -{% endif %} -
      -{% endif %} -{% if member.defaultValue is not empty %} -
      Default:
      {{ member.defaultValue }}
      -{% endif %} -{{ commonNotes(member) }} -
      -
    • -{% endmacro %} \ No newline at end of file diff --git a/scripts/sync_java_docs/db-templates/javadoc-overview.peb b/scripts/sync_java_docs/db-templates/javadoc-overview.peb deleted file mode 100644 index b3da79c2..00000000 --- a/scripts/sync_java_docs/db-templates/javadoc-overview.peb +++ /dev/null @@ -1,80 +0,0 @@ -{# - The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the - footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, - ...) so the accompanying stylesheet and the real one describe the same structure. -#} - - - -Overview - - - - -
      -
      - -
      -
      -
      - -
      -

      {{ title | default('API Documentation') }}

      -
      -
      -{% if modules is not empty %} -
      -
      Modules
      -
      -
      Module
      -
      Description
      -{% for module in modules %} - -
      {{ module.firstSentence | raw }}
      -{% endfor %} -
      -
      -{% endif %} -{# Only for a non-modular run: with modules present javadoc's overview lists just - the modules, and allpackages-index.html carries the package list. #} -{% if packages is not empty and modules is empty %} -
      -
      Packages
      -
      -
      Module
      -
      Package
      -
      Description
      -{% for pkg in packages %} -
      {{ pkg.moduleName }}
      - -
      {{ pkg.firstSentence | raw }}
      -{% endfor %} -
      -
      -{% endif %} -
      - -
      -
      -
      - -
      -
      -
      - - - diff --git a/scripts/sync_java_docs/db-templates/javadoc-package.peb b/scripts/sync_java_docs/db-templates/javadoc-package.peb deleted file mode 100644 index f0d3e02c..00000000 --- a/scripts/sync_java_docs/db-templates/javadoc-package.peb +++ /dev/null @@ -1,237 +0,0 @@ -{# - The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the - footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, - ...) so the accompanying stylesheet and the real one describe the same structure. -#} - - - -{{ name }} - - - - -
      -
      - -
      -
      -
      - -
      -{% if moduleName is not empty %} -
      Module {% if moduleUrl is not empty %}{{ moduleName }}{% else %}{{ moduleName }}{% endif %}
      -{% endif %} -

      Package {{ name }}

      -
      - -
      -
      package {{ name }}
      -{% if description is not empty %}
      {{ description | raw }}
      {% endif %} -{% if deprecated is not empty %} -
      Deprecated. -{% if deprecated.comment is not empty %}
      {{ deprecated.comment | raw }}
      {% endif %}
      -{% endif %} -{% if since is not empty or seeAlso is not empty or tags is not empty %} -
      -{% if since is not empty %}
      Since:
      {{ since | join(', ') }}
      {% endif %} -{% for tag in tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} -{% if seeAlso is not empty %}
      See Also:
        {% for see in seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      {% endif %} -
      -{% endif %} -
      - -
      -
        -{% if relatedPackages is not empty %} -
      • - -
      • -{% endif %} -
      • -
        -

        Classes and Interfaces

        -{{ typeTable("Interfaces", interfaces) }} -{{ typeTable("Classes", classes) }} -{{ typeTable("Enum Classes", enums) }} -{{ typeTable("Record Classes", records) }} -{{ typeTable("Exception Classes", exceptions) }} -{{ typeTable("Annotation Interfaces", annotationTypes) }} -
        -
      • -
      -
      - -
      -
      -
      - -
      -
      -
      - - - -{% macro typeTable(caption, rows) %} -{% if rows is not empty %} -
      {{ caption }}
      -
      -
      Class
      -
      Description
      -{% for row in rows %} - -
      {{ row.firstSentence | raw }}
      -{% endfor %} -
      -{% endif %} -{% endmacro %} -{% macro tagLabel(name) %} -{%- if name == 'apiNote' -%}API Note: -{%- elseif name == 'implSpec' -%}Implementation Requirements: -{%- elseif name == 'implNote' -%}Implementation Note: -{%- elseif name == 'jls' -%}See Java Language Specification: -{%- elseif name == 'jvms' -%}See Java Virtual Machine Specification: -{%- elseif name == 'serialData' -%}Serial Data: -{%- elseif name == 'serialField' -%}Serial Field: -{%- elseif name == 'serial' -%}Serial: -{%- elseif name == 'toolGuide' -%}Tool Guides: -{%- elseif name == 'revised' -%}Revised: -{%- elseif name == 'spec' -%}External Specifications: -{%- else -%}{{ name }}: -{%- endif -%} -{% endmacro %} -{% macro typeLink(ref) %} -{%- if ref is not empty -%} -{%- if ref.url is not empty -%} -{{ ref.display }} -{%- else -%} -{{ ref.display }} -{%- endif -%} -{%- endif -%} -{% endmacro %} -{% macro typeList(refs) %} -{%- for ref in refs -%}{{ typeLink(ref) }}{% if not loop.last %}, {% endif %}{%- endfor -%} -{% endmacro %} -{% macro memberLink(ref) %} -{%- if ref.url is not empty -%}{{ ref.name }}{%- else -%}{{ ref.name }}{%- endif -%} -{% endmacro %} -{% macro note(label, body) %} -
      {{ label }}
      -
      {{ body | raw }}
      -{% endmacro %} -{% macro modifiers(list) %} -{%- if list is not empty %}{{ list | join(' ') }} {% endif -%} -{% endmacro %} -{% macro parameters(params) %} -({%- for p in params -%}{{ typeLink(p.type) }} {{ p.name }}{% if not loop.last %}, {% endif %}{%- endfor -%}) -{% endmacro %} -{% macro throwsClause(exceptions) %} -{%- if exceptions is not empty %} throws {% for e in exceptions %}{{ typeLink(e.type) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif -%} -{% endmacro %} -{% macro rowColor(index) %}{% if index is odd %}odd-row-color{% else %}even-row-color{% endif %}{% endmacro %} -{% macro commonNotes(item) %} -{% if item.deprecated is not empty %} -
      Deprecated{% if item.deprecated.forRemoval %}, for removal: This API element is subject to removal in a future version{% endif %}. -{% if item.deprecated.since is not empty %}Since {{ item.deprecated.since }}.{% endif %} -{% if item.deprecated.comment is not empty %}
      {{ item.deprecated.comment | raw }}
      {% endif %} -
      -{% endif %} -{% if item.since is not empty or item.seeAlso is not empty or item.tags is not empty or item.authors is not empty or item.versions is not empty %} -
      -{% if item.since is not empty %}
      Since:
      {{ item.since | join(', ') }}
      {% endif %} -{% for tag in item.tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} -{% if item.authors is not empty %}
      Author:
      {{ item.authors | join(', ') }}
      {% endif %} -{% if item.versions is not empty %}
      Version:
      {{ item.versions | join(', ') }}
      {% endif %} -{% if item.seeAlso is not empty %} -
      See Also:
      -
        {% for see in item.seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      -{% endif %} -
      -{% endif %} -{% endmacro %} -{% macro fieldDetail(field) %} -
    • -
      -

      {{ field.name }}

      -
      {{ modifiers(field.modifiers) }}{{ typeLink(field.type) }} {{ field.name }}
      -{% if field.description is not empty %}
      {{ field.description | raw }}
      {% endif %} -{{ commonNotes(field) }} -{% if field.constantValue is not empty %} -
      Constant Field Value:
      {{ field.constantValue }}
      -{% endif %} -
      -
    • -{% endmacro %} -{% macro executableDetail(member) %} -
    • -
      -

      {{ member.name }}

      -
      {{ modifiers(member.modifiers) }}{% if member.typeParameters is not empty %}<{% for t in member.typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}> {% endif %}{% if member.returnType is not empty %}{{ typeLink(member.returnType) }} {% endif %}{{ member.name }}{{ parameters(member.parameters) }}{{ throwsClause(member.exceptions) }}
      -{% if member.description is not empty %}
      {{ member.description | raw }}
      {% endif %} -{% if member.specifiedBy is not empty or member.overrides is not empty or member.typeParameters is not empty or member.parameters is not empty or member.returns is not empty or member.exceptions is not empty %} -
      -{% for spec in member.specifiedBy %} -
      Specified by:
      -
      {{ memberLink(spec) }} in {{ spec.declaringType.kind | default('interface') }} {{ typeLink(spec.declaringType) }}
      -{% endfor %} -{% if member.overrides is not empty %} -
      Overrides:
      -
      {{ memberLink(member.overrides) }} in {{ member.overrides.declaringType.kind | default('class') }} {{ typeLink(member.overrides.declaringType) }}
      -{% endif %} -{% set hasTypeParamDocs = false %} -{% for t in member.typeParameters %}{% if t.description is not empty %}{% set hasTypeParamDocs = true %}{% endif %}{% endfor %} -{% if hasTypeParamDocs %} -
      Type Parameters:
      -{% for t in member.typeParameters %}{% if t.description is not empty %}
      {{ t.name }} - {{ t.description | raw }}
      {% endif %}{% endfor %} -{% endif %} -{% set hasParamDocs = false %} -{% for p in member.parameters %}{% if p.description is not empty %}{% set hasParamDocs = true %}{% endif %}{% endfor %} -{% if hasParamDocs %} -
      Parameters:
      -{% for p in member.parameters %}{% if p.description is not empty %}
      {{ p.name }} - {{ p.description | raw }}
      {% endif %}{% endfor %} -{% endif %} -{% if member.returns is not empty %}
      Returns:
      {{ member.returns | raw }}
      {% endif %} -{% if member.exceptions is not empty %} -
      Throws:
      -{% for e in member.exceptions %}
      {{ typeLink(e.type) }}{% if e.description is not empty %} - {{ e.description | raw }}{% endif %}
      {% endfor %} -{% endif %} -
      -{% endif %} -{% if member.defaultValue is not empty %} -
      Default:
      {{ member.defaultValue }}
      -{% endif %} -{{ commonNotes(member) }} -
      -
    • -{% endmacro %} \ No newline at end of file diff --git a/scripts/sync_java_docs/db-templates/javadoc.peb b/scripts/sync_java_docs/db-templates/javadoc.peb new file mode 100644 index 00000000..40829c91 --- /dev/null +++ b/scripts/sync_java_docs/db-templates/javadoc.peb @@ -0,0 +1,831 @@ +{# + The Java API documentation template for documentation.db. + + GENERATED by scripts/sync_java_docs/flatten_templates.py from the templates in + Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates -- edit those and + re-run it rather than editing this file. + + One template covers every page kind because the reader loads a single template per page + and a Content row names exactly one. Each section below picks its markup from the JSON's + own `page` field: class, package, module, overview, all-classes, all-packages, + deprecated-list, constant-values, index. + + Context: the page's JSON, plus `pathToRoot`, which sync_javadoc_json_to_db.py injects. +#} +{# + The page skeleton every javadoc page shares: the two navigation bars, the flex wrapper, and the + footer. Class names follow the official javadoc output (top-nav, sub-nav, flex-content, header, + ...) so the accompanying stylesheet and the real one describe the same structure. +#} + + + +{% if page == "class" %}{{ name }} ({{ moduleName | default('API') }}){% elseif page == "package" %}{{ name }}{% elseif page == "module" %}{{ name }}{% elseif page == "overview" %}Overview{% elseif page == "all-classes" %}All Classes and Interfaces{% elseif page == "all-packages" %}All Packages{% elseif page == "deprecated-list" %}Deprecated List{% elseif page == "constant-values" %}Constant Field Values{% elseif page == "index" %}{{ letter }}-Index{% else %}Documentation{% endif %} + + + + +
      +
      + +
      +
      +
      +{% if page == "class" %} +
      +{% if packageName is not empty %} + +{% endif %} +

      {{ kind | capitalize }} {{ name }}{% if typeParameters is not empty %}<{% for t in typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}>{% endif %}

      +
      + +
      +
      + +{# The inheritance tree, indented one step per level, exactly as javadoc draws it. #} +{% if inheritance is not empty and inheritance | length > 1 %} +{# javadoc nests one div per level so each generation indents further than the last. The final + entry is this class itself, shown as plain text rather than a link to the page you are on. #} +
      +{%- for ancestor in inheritance -%} +
      {% if loop.last %}{{ ancestor.qualifiedName }}{% else %}{{ typeLink(ancestor) }}{% endif %} +{%- endfor -%} +{%- for ancestor in inheritance -%}
      {%- endfor -%} +
      +{% endif %} + +{% if typeParameters is not empty and typeParameters | first is not null %} +{% set documentedTypeParams = false %} +{% for t in typeParameters %}{% if t.description is not empty %}{% set documentedTypeParams = true %}{% endif %}{% endfor %} +{% if documentedTypeParams %} +
      +
      Type Parameters:
      +{% for t in typeParameters %}{% if t.description is not empty %}
      {{ t.name }} - {{ t.description | raw }}
      {% endif %}{% endfor %} +
      +{% endif %} +{% endif %} + +{% if allImplementedInterfaces is not empty %} +
      All Implemented Interfaces:
      {{ typeList(allImplementedInterfaces) }}
      +{% endif %} +{% if allSuperinterfaces is not empty %} +
      All Superinterfaces:
      {{ typeList(allSuperinterfaces) }}
      +{% endif %} +{% if allKnownSubinterfaces is not empty %} +
      All Known Subinterfaces:
      {{ typeList(allKnownSubinterfaces) }}
      +{% endif %} +{% if allKnownImplementingClasses is not empty %} +
      All Known Implementing Classes:
      {{ typeList(allKnownImplementingClasses) }}
      +{% endif %} +{% if directKnownSubclasses is not empty %} +
      Direct Known Subclasses:
      {{ typeList(directKnownSubclasses) }}
      +{% endif %} +{% if enclosingType is not empty %} +
      Enclosing {{ enclosingType.kind | default('class') }}:
      {{ typeLink(enclosingType) }}
      +{% endif %} +{% if isFunctionalInterface %} +
      Functional Interface:
      This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.
      +{% endif %} + +
      +
      {{ signature }}
      +{% if description is not empty %}
      {{ description | raw }}
      {% endif %} +{% if deprecated is not empty %} +
      Deprecated{% if deprecated.forRemoval %}, for removal: This API element is subject to removal in a future version{% endif %}. +{% if deprecated.since is not empty %}Since {{ deprecated.since }}.{% endif %} +{% if deprecated.comment is not empty %}
      {{ deprecated.comment | raw }}
      {% endif %} +
      +{% endif %} +{% if since is not empty or seeAlso is not empty or tags is not empty or authors is not empty or versions is not empty %} +
      +{% if since is not empty %}
      Since:
      {{ since | join(', ') }}
      {% endif %} +{% for tag in tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} +{% if authors is not empty %}
      Author:
      {{ authors | join(', ') }}
      {% endif %} +{% if versions is not empty %}
      Version:
      {{ versions | join(', ') }}
      {% endif %} +{% if seeAlso is not empty %} +
      See Also:
      +
        {% for see in seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      +{% endif %} +
      +{% endif %} +
      + +
      +
        + +{% if nestedTypes is not empty or inheritedNestedTypes is not empty %} +
      • +
        +

        Nested Class Summary

        +{% if nestedTypes is not empty %} +
        Nested Classes
        +
        +
        Modifier and Type
        +
        Class
        +
        Description
        +{% for nested in nestedTypes %} +
        {{ nested.modifiers | join(' ') }} {{ nested.kind }}
        + +
        {{ nested.firstSentence | raw }}
        +{% endfor %} +
        +{% endif %} +{% for group in inheritedNestedTypes %} +
        +

        Nested classes/interfaces declared in {{ group.declaringType.kind | default('class') }} {{ typeLink(group.declaringType) }}

        +{% for member in group.members %}{{ memberLink(member) }}{% if not loop.last %}, {% endif %}{% endfor %} +
        +{% endfor %} +
        +
      • +{% endif %} + +{% if enumConstants is not empty %} +
      • +
        +

        Enum Constant Summary

        +
        Enum Constants
        +
        +
        Enum Constant
        +
        Description
        +{% for field in enumConstants %} + +
        {{ field.firstSentence | raw }}
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if fields is not empty or inheritedFields is not empty %} +
      • +
        +

        Field Summary

        +{% if fields is not empty %} +
        Fields
        +
        +
        Modifier and Type
        +
        Field
        +
        Description
        +{% for field in fields %} +
        {{ field.modifiers | join(' ') }} {{ typeLink(field.type) }}
        + +
        {{ field.firstSentence | raw }}
        +{% endfor %} +
        +{% endif %} +{% for group in inheritedFields %} +
        +

        Fields declared in {{ group.declaringType.kind | default('class') }} {{ typeLink(group.declaringType) }}

        +{% for member in group.members %}{{ memberLink(member) }}{% if not loop.last %}, {% endif %}{% endfor %} +
        +{% endfor %} +
        +
      • +{% endif %} + +{% if constructors is not empty %} +
      • +
        +

        Constructor Summary

        +
        Constructors
        +
        +
        Constructor
        +
        Description
        +{% for ctor in constructors %} +
        {{ ctor.name }}{{ parameters(ctor.parameters) }}
        +
        {{ ctor.firstSentence | raw }}
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if annotationElements is not empty %} +
      • +
        +

        Element Summary

        +
        Elements
        +
        +
        Modifier and Type
        +
        Element
        +
        Description
        +{% for element in annotationElements %} +
        {{ typeLink(element.returnType) }}
        + +
        {{ element.firstSentence | raw }}
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if methods is not empty or inheritedMethods is not empty %} +
      • +
        +

        Method Summary

        +{% if methods is not empty %} +
        All Methods
        +
        +
        Modifier and Type
        +
        Method
        +
        Description
        +{% for method in methods %} +
        {{ method.modifiers | join(' ') }} {{ typeLink(method.returnType) }}
        +
        {{ method.name }}{{ parameters(method.parameters) }}
        +
        {{ method.firstSentence | raw }}
        +{% endfor %} +
        +{% endif %} +{% for group in inheritedMethods %} +
        +

        Methods declared in {{ group.declaringType.kind | default('class') }} {{ typeLink(group.declaringType) }}

        +{% for member in group.members %}{{ memberLink(member) }}{% if not loop.last %}, {% endif %}{% endfor %} +
        +{% endfor %} +
        +
      • +{% endif %} + +
      +
      + +
      +
        + +{% if enumConstants is not empty %} +
      • +
        +

        Enum Constant Details

        +
          +{% for field in enumConstants %}{{ fieldDetail(field) }}{% endfor %} +
        +
        +
      • +{% endif %} + +{% if fields is not empty %} +
      • +
        +

        Field Details

        +
          +{% for field in fields %}{{ fieldDetail(field) }}{% endfor %} +
        +
        +
      • +{% endif %} + +{% if constructors is not empty %} +
      • +
        +

        Constructor Details

        +
          +{% for ctor in constructors %}{{ executableDetail(ctor) }}{% endfor %} +
        +
        +
      • +{% endif %} + +{% if annotationElements is not empty %} +
      • +
        +

        Element Details

        +
          +{% for element in annotationElements %}{{ executableDetail(element) }}{% endfor %} +
        +
        +
      • +{% endif %} + +{% if methods is not empty %} +
      • +
        +

        Method Details

        +
          +{% for method in methods %}{{ executableDetail(method) }}{% endfor %} +
        +
        +
      • +{% endif %} + +
      +
      +{% elseif page == "package" %} +
      +{% if moduleName is not empty %} +
      Module {% if moduleUrl is not empty %}{{ moduleName }}{% else %}{{ moduleName }}{% endif %}
      +{% endif %} +

      Package {{ name }}

      +
      + +
      +
      package {{ name }}
      +{% if description is not empty %}
      {{ description | raw }}
      {% endif %} +{% if deprecated is not empty %} +
      Deprecated. +{% if deprecated.comment is not empty %}
      {{ deprecated.comment | raw }}
      {% endif %}
      +{% endif %} +{% if since is not empty or seeAlso is not empty or tags is not empty %} +
      +{% if since is not empty %}
      Since:
      {{ since | join(', ') }}
      {% endif %} +{% for tag in tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} +{% if seeAlso is not empty %}
      See Also:
        {% for see in seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      {% endif %} +
      +{% endif %} +
      + +
      +
        +{% if relatedPackages is not empty %} +
      • + +
      • +{% endif %} +
      • +
        +

        Classes and Interfaces

        +{{ typeTable("Interfaces", interfaces) }} +{{ typeTable("Classes", classes) }} +{{ typeTable("Enum Classes", enums) }} +{{ typeTable("Record Classes", records) }} +{{ typeTable("Exception Classes", exceptions) }} +{{ typeTable("Annotation Interfaces", annotationTypes) }} +
        +
      • +
      +
      +{% elseif page == "module" %} +
      +

      Module {{ name }}

      +
      + +
      +{% if description is not empty %}
      {{ description | raw }}
      {% endif %} +{% if hasModuleGraph %} +
      +
      Module Graph:
      +
      Module graph for {{ name }}Module graph for {{ name }}
      +
      +{% endif %} +{% if since is not empty or tags is not empty or seeAlso is not empty %} +
      +{% if since is not empty %}
      Since:
      {{ since | join(', ') }}
      {% endif %} +{% for tag in tags %}{% if tag.name != 'moduleGraph' and tag.name != 'uses' and tag.name != 'provides' %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endif %}{% endfor %} +{% if seeAlso is not empty %}
      See Also:
        {% for see in seeAlso %}
      • {{ see.label }}
      • {% endfor %}
      {% endif %} +
      +{% endif %} +
      + +
      +
        + +{% if requires is not empty %} +
      • +
        +

        Modules

        +
        Requires
        +
        +
        Modifier
        +
        Module
        +
        Description
        +{% for req in requires %} +
        {% if req.isTransitive %}transitive{% endif %}{% if req.isStatic %}static{% endif %}
        +
        {% if req.url is not empty %}{{ req.module }}{% else %}{{ req.module }}{% endif %}
        +
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if indirectRequires is not empty %} +
      • +
        +{% if requires is empty %}

        Modules

        {% endif %} +
        Indirect Requires
        +
        +
        Modifier
        +
        Module
        +
        Description
        +{% for req in indirectRequires %} +
        transitive
        +
        {% if req.url is not empty %}{{ req.module }}{% else %}{{ req.module }}{% endif %}
        +
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if exports is not empty %} +
      • +
        +

        Packages

        +
        Exports
        +
        +
        Package
        +
        Exported To Modules
        +
        Description
        +{% for export in exports %} +
        {% if export.url is not empty %}{{ export.packageName }}{% else %}{{ export.packageName }}{% endif %}
        +
        {% if export.to is not empty %}{{ export.to | join(', ') }}{% else %}All Modules{% endif %}
        +
        {{ export.firstSentence | raw }}
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if indirectExports is not empty %} +
      • +
        +
        Indirect Exports
        +
        +
        From
        +
        Packages
        +{% for entry in indirectExports %} +
        {% if entry.moduleUrl is not empty %}{{ entry.module }}{% else %}{{ entry.module }}{% endif %}
        +
        {% for pkg in entry.packages %}{% if pkg.url is not empty %}{{ pkg.name }}{% else %}{{ pkg.name }}{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if opens is not empty %} +
      • +
        +

        Opens

        +
        +
        Package
        +
        Opened To Modules
        +{% for open in opens %} +
        {% if open.url is not empty %}{{ open.packageName }}{% else %}{{ open.packageName }}{% endif %}
        +
        {% if open.to is not empty %}{{ open.to | join(', ') }}{% else %}All Modules{% endif %}
        +{% endfor %} +
        +
        +
      • +{% endif %} + +{% if uses is not empty or provides is not empty %} +
      • +
        +

        Services

        +{% if uses is not empty %} +
        Uses
        +
        +
        Type
        +
        Description
        +{% for use in uses %} +
        {{ typeLink(use) }}
        +
        +{% endfor %} +
        +{% endif %} +{% if provides is not empty %} +
        Provides
        +
        +
        Type
        +
        Implementations
        +{% for provide in provides %} +
        {{ typeLink(provide.service) }}
        +
        {% for impl in provide.implementations %}{{ typeLink(impl) }}{% if not loop.last %}, {% endif %}{% endfor %}
        +{% endfor %} +
        +{% endif %} +
        +
      • +{% endif %} + +
      +
      +{% elseif page == "overview" %} +
      +

      {{ title | default('API Documentation') }}

      +
      +
      +{% if modules is not empty %} +
      +
      Modules
      +
      +
      Module
      +
      Description
      +{% for module in modules %} + +
      {{ module.firstSentence | raw }}
      +{% endfor %} +
      +
      +{% endif %} +{# Only for a non-modular run: with modules present javadoc's overview lists just + the modules, and allpackages-index.html carries the package list. #} +{% if packages is not empty and modules is empty %} +
      +
      Packages
      +
      +
      Module
      +
      Package
      +
      Description
      +{% for pkg in packages %} +
      {{ pkg.moduleName }}
      + +
      {{ pkg.firstSentence | raw }}
      +{% endfor %} +
      +
      +{% endif %} +
      +{% elseif page == "all-classes" %} +

      All Classes and Interfaces

      +
      +
      Classes, Interfaces, Enums and Annotation Interfaces
      +
      +
      Class
      +
      Package
      +
      Description
      +{% for type in types %}{% if type.modifiers is empty or type.modifiers contains 'public' %} + +
      {{ type.packageName }}
      +
      {{ type.firstSentence | raw }}
      +{% endif %}{% endfor %} +
      +
      +{% elseif page == "all-packages" %} +

      All Packages

      +
      +
      Package Summary
      +
      +
      Module
      +
      Package
      +
      Description
      +{% for pkg in packages %} +
      {{ pkg.moduleName }}
      + +
      {{ pkg.firstSentence | raw }}
      +{% endfor %} +
      +
      +{% elseif page == "deprecated-list" %} +

      Deprecated API

      Contents

      +
      +{% if sections is empty %} +
      No deprecated API in this documentation.
      +{% endif %} +{# Pebble iterates a map as entries, so the section name is entry.key. #} +{% for section in sections %} +
      +
      {{ sectionTitle(section.key) }}
      +
      +
      Element
      +
      Description
      +{% for entry in section.value %} +
      {% if entry.url is not empty %}{{ entry.element }}{% else %}{{ entry.element }}{% endif %}
      +
      +{% if entry.forRemoval %}Terminally deprecated.{% endif %} +{% if entry.since is not empty %}Since {{ entry.since }}.{% endif %} +{% if entry.comment is not empty %}
      {{ entry.comment | raw }}
      {% endif %} +
      +{% endfor %} +
      +
      +{% endfor %} +{% elseif page == "constant-values" %} +

      Constant Field Values

      Contents

      +{# Pebble iterates a map as entries: entry.key is the package, entry.value its types. #} +{% for group in packages %} +
      +

      {{ group.key }}

      +{% for type in group.value %} +
      {% if type.url is not empty %}{{ type.qualifiedName }}{% else %}{{ type.qualifiedName }}{% endif %}
      +
      +
      Modifier and Type
      +
      Constant Field
      +
      Value
      +{% for field in type.fields %} +
      {{ field.modifiers | join(' ') }} {{ field.type.display }}
      +
      {% if field.url is not empty %}{{ field.name }}{% else %}{{ field.name }}{% endif %}
      +
      {{ field.value }}
      +{% endfor %} +
      +{% endfor %} +
      +{% endfor %} +{% elseif page == "index" %} +
      +

      Index

      +
      +{# Pebble's loop.index is 0-based; the index files are numbered from 1. #} +{% for l in letters %}{{ l }}{% if not loop.last %} {% endif %}{% endfor %} +
      +
      +

      {{ letter }}

      +
      +{% for entry in entries %} +
      {% if entry.url is not empty %}{{ entry.label }}{% else %}{{ entry.label }}{% endif %} +{% if entry.containingElement is not empty %} - {{ entry.kind }} in {{ entry.containingElement }}{% else %} - {{ entry.kind }}{% endif %} +{% if entry.deprecated %}Deprecated.{% endif %} +
      +
      {% if entry.firstSentence is not empty %}
      {{ entry.firstSentence | raw }}
      {% endif %}
      +{% endfor %} +
      +{% else %}{% endif %} +
      +
      +
      + +
      +
      +
      + + + +{% macro commonNotes(item) %} +{% if item.deprecated is not empty %} +
      Deprecated{% if item.deprecated.forRemoval %}, for removal: This API element is subject to removal in a future version{% endif %}. +{% if item.deprecated.since is not empty %}Since {{ item.deprecated.since }}.{% endif %} +{% if item.deprecated.comment is not empty %}
      {{ item.deprecated.comment | raw }}
      {% endif %} +
      +{% endif %} +{% if item.since is not empty or item.seeAlso is not empty or item.tags is not empty or item.authors is not empty or item.versions is not empty %} +
      +{% if item.since is not empty %}
      Since:
      {{ item.since | join(', ') }}
      {% endif %} +{% for tag in item.tags %}
      {{ tagLabel(tag.name) }}
      {{ tag.text | raw }}
      {% endfor %} +{% if item.authors is not empty %}
      Author:
      {{ item.authors | join(', ') }}
      {% endif %} +{% if item.versions is not empty %}
      Version:
      {{ item.versions | join(', ') }}
      {% endif %} +{% if item.seeAlso is not empty %} +
      See Also:
      +
        {% for see in item.seeAlso %}
      • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
      • {% endfor %}
      +{% endif %} +
      +{% endif %} +{% endmacro %} +{% macro executableDetail(member) %} +
    • +
      +

      {{ member.name }}

      +
      {{ modifiers(member.modifiers) }}{% if member.typeParameters is not empty %}<{% for t in member.typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}> {% endif %}{% if member.returnType is not empty %}{{ typeLink(member.returnType) }} {% endif %}{{ member.name }}{{ parameters(member.parameters) }}{{ throwsClause(member.exceptions) }}
      +{% if member.description is not empty %}
      {{ member.description | raw }}
      {% endif %} +{% if member.specifiedBy is not empty or member.overrides is not empty or member.typeParameters is not empty or member.parameters is not empty or member.returns is not empty or member.exceptions is not empty %} +
      +{% for spec in member.specifiedBy %} +
      Specified by:
      +
      {{ memberLink(spec) }} in {{ spec.declaringType.kind | default('interface') }} {{ typeLink(spec.declaringType) }}
      +{% endfor %} +{% if member.overrides is not empty %} +
      Overrides:
      +
      {{ memberLink(member.overrides) }} in {{ member.overrides.declaringType.kind | default('class') }} {{ typeLink(member.overrides.declaringType) }}
      +{% endif %} +{% set hasTypeParamDocs = false %} +{% for t in member.typeParameters %}{% if t.description is not empty %}{% set hasTypeParamDocs = true %}{% endif %}{% endfor %} +{% if hasTypeParamDocs %} +
      Type Parameters:
      +{% for t in member.typeParameters %}{% if t.description is not empty %}
      {{ t.name }} - {{ t.description | raw }}
      {% endif %}{% endfor %} +{% endif %} +{% set hasParamDocs = false %} +{% for p in member.parameters %}{% if p.description is not empty %}{% set hasParamDocs = true %}{% endif %}{% endfor %} +{% if hasParamDocs %} +
      Parameters:
      +{% for p in member.parameters %}{% if p.description is not empty %}
      {{ p.name }} - {{ p.description | raw }}
      {% endif %}{% endfor %} +{% endif %} +{% if member.returns is not empty %}
      Returns:
      {{ member.returns | raw }}
      {% endif %} +{% if member.exceptions is not empty %} +
      Throws:
      +{% for e in member.exceptions %}
      {{ typeLink(e.type) }}{% if e.description is not empty %} - {{ e.description | raw }}{% endif %}
      {% endfor %} +{% endif %} +
      +{% endif %} +{% if member.defaultValue is not empty %} +
      Default:
      {{ member.defaultValue }}
      +{% endif %} +{{ commonNotes(member) }} +
      +
    • +{% endmacro %} +{% macro fieldDetail(field) %} +
    • +
      +

      {{ field.name }}

      +
      {{ modifiers(field.modifiers) }}{{ typeLink(field.type) }} {{ field.name }}
      +{% if field.description is not empty %}
      {{ field.description | raw }}
      {% endif %} +{{ commonNotes(field) }} +{% if field.constantValue is not empty %} +
      Constant Field Value:
      {{ field.constantValue }}
      +{% endif %} +
      +
    • +{% endmacro %} +{% macro memberLink(ref) %} +{%- if ref.url is not empty -%}{{ ref.name }}{%- else -%}{{ ref.name }}{%- endif -%} +{% endmacro %} +{% macro modifiers(list) %} +{%- if list is not empty %}{{ list | join(' ') }} {% endif -%} +{% endmacro %} +{% macro note(label, body) %} +
      {{ label }}
      +
      {{ body | raw }}
      +{% endmacro %} +{% macro parameters(params) %} +({%- for p in params -%}{{ typeLink(p.type) }} {{ p.name }}{% if not loop.last %}, {% endif %}{%- endfor -%}) +{% endmacro %} +{% macro rowColor(index) %}{% if index is odd %}odd-row-color{% else %}even-row-color{% endif %}{% endmacro %} +{% macro sectionTitle(kind) %} +{%- if kind == 'classes' -%}Deprecated Classes +{%- elseif kind == 'interfaces' -%}Deprecated Interfaces +{%- elseif kind == 'enums' -%}Deprecated Enum Classes +{%- elseif kind == 'exceptions' -%}Deprecated Exception Classes +{%- elseif kind == 'annotationTypes' -%}Deprecated Annotation Interfaces +{%- elseif kind == 'fields' -%}Deprecated Fields +{%- elseif kind == 'methods' -%}Deprecated Methods +{%- elseif kind == 'constructors' -%}Deprecated Constructors +{%- elseif kind == 'enumConstants' -%}Deprecated Enum Constants +{%- elseif kind == 'annotationElements' -%}Deprecated Annotation Elements +{%- else -%}Deprecated {{ kind }} +{%- endif -%} +{% endmacro %} +{% macro tagLabel(name) %} +{%- if name == 'apiNote' -%}API Note: +{%- elseif name == 'implSpec' -%}Implementation Requirements: +{%- elseif name == 'implNote' -%}Implementation Note: +{%- elseif name == 'jls' -%}See Java Language Specification: +{%- elseif name == 'jvms' -%}See Java Virtual Machine Specification: +{%- elseif name == 'serialData' -%}Serial Data: +{%- elseif name == 'serialField' -%}Serial Field: +{%- elseif name == 'serial' -%}Serial: +{%- elseif name == 'toolGuide' -%}Tool Guides: +{%- elseif name == 'revised' -%}Revised: +{%- elseif name == 'spec' -%}External Specifications: +{%- else -%}{{ name }}: +{%- endif -%} +{% endmacro %} +{% macro throwsClause(exceptions) %} +{%- if exceptions is not empty %} throws {% for e in exceptions %}{{ typeLink(e.type) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif -%} +{% endmacro %} +{% macro typeLink(ref) %} +{%- if ref is not empty -%} +{%- if ref.url is not empty -%} +{{ ref.display }} +{%- else -%} +{{ ref.display }} +{%- endif -%} +{%- endif -%} +{% endmacro %} +{% macro typeList(refs) %} +{%- for ref in refs -%}{{ typeLink(ref) }}{% if not loop.last %}, {% endif %}{%- endfor -%} +{% endmacro %} +{% macro typeTable(caption, rows) %} +{% if rows is not empty %} +
      {{ caption }}
      +
      +
      Class
      +
      Description
      +{% for row in rows %} + +
      {{ row.firstSentence | raw }}
      +{% endfor %} +
      +{% endif %} +{% endmacro %} \ No newline at end of file diff --git a/scripts/sync_java_docs/flatten_templates.py b/scripts/sync_java_docs/flatten_templates.py index a28ab575..c0dc253e 100755 --- a/scripts/sync_java_docs/flatten_templates.py +++ b/scripts/sync_java_docs/flatten_templates.py @@ -1,22 +1,26 @@ #!/usr/bin/env python3 -"""Flattens the pebble-renderer templates into standalone templates for documentation.db. +"""Builds the single Pebble template that documentation.db serves the Java API docs from. The renderer in `pebble-renderer/` and the reader that serves documentation.db run Pebble in two different environments, and the database's is the narrower one: - * Templates are stored one per row in `Templates`, with no loader that can resolve - `{% extends "base" %}` or `{% import "macros" %}` by name. Every template already in the - database (page.peb, nav.peb, layout.pebble) is self-contained, so that is the contract. - * Only Pebble's built-in filters are available. The renderer's `href` and `doc` filters are Java - classes that ship with it and are not there. + * The reader loads **one template per page** and cannot resolve `{% extends "base" %}` or + `{% import "macros" %}` by name, so the template has to be self-contained. + * A `Content` row names exactly one template, and the reader can only read one at a time, so all + nine page kinds have to share it and pick their markup from the JSON's own `page` field. + * Only Pebble's built-in filters exist. The renderer's `href` and `doc` filters are Java classes + that ship with it and are not there. -Rather than maintain a second, divergent copy of the templates by hand, this generates them: +Rather than maintain a second, hand-written copy of the templates, this composes them: - * `{% extends %}` is resolved by substituting the child's `{% block %}` bodies into the parent. - * `{% import %}` is resolved by appending the imported macro definitions. - * `| href` is dropped and `| doc` becomes `| raw`. Both are safe because sync_javadoc_json_to_db.py - rewrites the JSON's `.json` links to `.html` before insertion, which is the only thing those - filters did beyond marking documentation HTML as trusted. + * The base skeleton is emitted once, with each `{% block %}` replaced by an if/elseif chain over + `page` -- so `{% block content %}` becomes "if page == 'class' … elseif page == 'module' …". + A page kind that doesn't override a block falls back to the base's own body for it. + * Every macro is emitted once. The page templates and macros.peb share no macro names, which + this checks rather than assumes. + * `| href` is dropped and `| doc` becomes `| raw`. Both are safe because + sync_javadoc_json_to_db.py rewrites the JSON's `.json` links to `.html` before insertion, which + is all those filters did beyond marking documentation HTML as trusted. Run it whenever the pebble-renderer templates change. """ @@ -31,9 +35,11 @@ BLOCK_RE = re.compile(r"\{%-?\s*block\s+(\w+)\s*-?%\}(.*?)\{%-?\s*endblock\s*-?%\}", re.DOTALL) EXTENDS_RE = re.compile(r"\{%-?\s*extends\s+\"([^\"]+)\"\s*-?%\}") IMPORT_RE = re.compile(r"\{%-?\s*import\s+\"([^\"]+)\"\s*-?%\}") -MACRO_RE = re.compile(r"\{%-?\s*macro\s+\w+.*?\{%-?\s*endmacro\s*-?%\}", re.DOTALL) +MACRO_RE = re.compile(r"\{%-?\s*macro\s+(\w+).*?\{%-?\s*endmacro\s*-?%\}", re.DOTALL) -# page kind (the JSON's `page` field) -> source template +OUTPUT_NAME = "javadoc.peb" + +# The JSON's `page` field -> the renderer template holding that page's markup. PAGES = { "class": "class", "package": "package-summary", @@ -51,47 +57,91 @@ def load(directory: Path, name: str) -> str: return (directory / f"{name}.peb").read_text(encoding="utf-8") -def flatten(directory: Path, name: str) -> str: - source = load(directory, name) - - imported_macros: list[str] = [] - for imported in IMPORT_RE.findall(source): - imported_macros.extend(MACRO_RE.findall(load(directory, imported))) - source = IMPORT_RE.sub("", source) - - extends = EXTENDS_RE.search(source) - if extends: - parent = load(directory, extends.group(1)) - blocks = {n: b for n, b in BLOCK_RE.findall(source)} - # The parent's own block bodies are the defaults for blocks the child doesn't override. - parent = BLOCK_RE.sub(lambda m: blocks.get(m.group(1), m.group(2)), parent) - # Anything outside a block in a child template is discarded by Pebble, and macros the - # child defines itself must survive, so they are carried over explicitly. - own_macros = MACRO_RE.findall(EXTENDS_RE.sub("", source)) - source = parent + "\n" + "\n".join(own_macros) - - source = "\n".join([source, *imported_macros]) - - # The two renderer-only filters. `href` did nothing but swap the extension, which the sync - # script now does to the data itself; `doc` additionally marked the value as trusted HTML, - # which is Pebble's built-in `raw`. - source = re.sub(r"\|\s*href\b", "", source) - source = re.sub(r"\|\s*doc\b", "| raw", source) - - if re.search(r"\{%-?\s*(extends|import|include)\b", source): - raise SystemExit(f"{name}: template still references another template after flattening") - if re.search(r"\|\s*(href|doc)\b", source): - raise SystemExit(f"{name}: template still uses a renderer-only filter after flattening") - return source +def blocks_of(source: str) -> dict[str, str]: + return {name: body for name, body in BLOCK_RE.findall(source)} + + +def macros_of(source: str) -> list[tuple[str, str]]: + return [(m.group(1), m.group(0)) for m in MACRO_RE.finditer(source)] + + +def build(directory: Path) -> str: + base = load(directory, "base") + base_blocks = blocks_of(base) + + page_blocks: dict[str, dict[str, str]] = {} + macros: dict[str, str] = {} + + for macro_name, macro_source in macros_of(load(directory, "macros")): + macros[macro_name] = macro_source + + for kind, template in PAGES.items(): + source = load(directory, template) + page_blocks[kind] = blocks_of(source) + for macro_name, macro_source in macros_of(source): + if macro_name in macros and macros[macro_name] != macro_source: + raise SystemExit( + f"macro '{macro_name}' is defined differently in {template}.peb and elsewhere; " + "the single template can only hold one definition" + ) + macros[macro_name] = macro_source + + def dispatch(block_name: str) -> str: + """An if/elseif chain over `page` for one block, defaulting to the base's own body.""" + branches = [] + for kind in PAGES: + body = page_blocks[kind].get(block_name) + if body is None: + continue + keyword = "if" if not branches else "elseif" + branches.append(f'{{% {keyword} page == "{kind}" %}}{body}') + if not branches: + return base_blocks.get(block_name, "") + default = base_blocks.get(block_name, "") + return "".join(branches) + f"{{% else %}}{default}{{% endif %}}" + + composed = BLOCK_RE.sub(lambda m: dispatch(m.group(1)), base) + composed = IMPORT_RE.sub("", EXTENDS_RE.sub("", composed)) + + header = ( + "{#\n" + " The Java API documentation template for documentation.db.\n" + "\n" + " GENERATED by scripts/sync_java_docs/flatten_templates.py from the templates in\n" + " Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates -- edit those and\n" + " re-run it rather than editing this file.\n" + "\n" + " One template covers every page kind because the reader loads a single template per page\n" + " and a Content row names exactly one. Each section below picks its markup from the JSON's\n" + " own `page` field: class, package, module, overview, all-classes, all-packages,\n" + " deprecated-list, constant-values, index.\n" + "\n" + " Context: the page's JSON, plus `pathToRoot`, which sync_javadoc_json_to_db.py injects.\n" + "#}\n" + ) + composed = header + composed + "\n" + "\n".join(macros[name] for name in sorted(macros)) + + composed = re.sub(r"\|\s*href\b", "", composed) + composed = re.sub(r"\|\s*doc\b", "| raw", composed) + + for pattern, complaint in ( + (r"\{%-?\s*(extends|import|include)\b", "still references another template"), + (r"\|\s*(href|doc)\b", "still uses a renderer-only filter"), + (r"\{%-?\s*block\b", "still contains an unresolved block"), + ): + if re.search(pattern, composed): + raise SystemExit(f"{OUTPUT_NAME}: {complaint}") + return composed def main() -> int: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("--templates", type=Path, - default=Path(__file__).resolve().parents[2] / "Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates", - help="the pebble-renderer template directory to flatten") + default=Path(__file__).resolve().parents[2] + / "Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates", + help="the pebble-renderer template directory to compose from") parser.add_argument("--out", type=Path, default=Path(__file__).resolve().parent / "db-templates", - help="where to write the flattened templates") + help="where to write the composed template") args = parser.parse_args() if not args.templates.is_dir(): @@ -99,12 +149,10 @@ def main() -> int: return 2 args.out.mkdir(parents=True, exist_ok=True) - for kind, name in sorted(PAGES.items()): - flattened = flatten(args.templates, name) - target = args.out / f"javadoc-{kind}.peb" - target.write_text(flattened, encoding="utf-8") - print(f" {target.name:32s} {len(flattened):6d} bytes (page kind '{kind}')") - print(f"Wrote {len(PAGES)} template(s) to {args.out}") + composed = build(args.templates) + target = args.out / OUTPUT_NAME + target.write_text(composed, encoding="utf-8") + print(f"Wrote {target} ({len(composed)} bytes) covering {len(PAGES)} page kinds.") return 0 diff --git a/scripts/sync_java_docs/sync_javadoc_json_to_db.py b/scripts/sync_java_docs/sync_javadoc_json_to_db.py index 559aa877..63c2dce6 100755 --- a/scripts/sync_java_docs/sync_javadoc_json_to_db.py +++ b/scripts/sync_java_docs/sync_javadoc_json_to_db.py @@ -24,7 +24,8 @@ renderer's `doc` filter did. - `pathToRoot` is injected, since the templates need it for the stylesheet and the top nav and the reader passes nothing but the JSON itself. - - `page` selects the template, so the mapping lives here rather than in the reader. + - `page` is left in the JSON, because the single template branches on it: the reader can only + load one template per page, so all nine page kinds share one and choose their markup from it. A timestamped backup is taken before anything is written. """ @@ -49,18 +50,23 @@ # the Java docs and report it as a clean run. MAX_DELETE_FRACTION = 0.35 -# The JSON's `page` field -> the template that renders it. Names match flatten_templates.py. -TEMPLATE_FOR_PAGE = { - "class": "javadoc-class.peb", - "package": "javadoc-package.peb", - "module": "javadoc-module.peb", - "overview": "javadoc-overview.peb", - "all-classes": "javadoc-all-classes.peb", - "all-packages": "javadoc-all-packages.peb", - "deprecated-list": "javadoc-deprecated-list.peb", - "constant-values": "javadoc-constant-values.peb", - "index": "javadoc-index.peb", -} +# The one template every Java page uses; it branches on the JSON's `page` field. Matches +# flatten_templates.OUTPUT_NAME. +TEMPLATE_NAME = "javadoc.peb" + +# Page kinds that template knows how to render. A row whose JSON says anything else is left alone +# rather than pointed at a template that would not produce a page. +KNOWN_PAGES = frozenset({ + "class", "package", "module", "overview", "all-classes", + "all-packages", "deprecated-list", "constant-values", "index", +}) + +# Templates this script installed before it used a single one. Removed on sight so a stale row +# cannot go on being referenced. +SUPERSEDED_TEMPLATES = tuple(f"javadoc-{kind}.peb" for kind in [ + "class", "package", "module", "overview", "all-classes", + "all-packages", "deprecated-list", "constant-values", "index", +]) # `.json` at the end of a string, or followed by a quote or a fragment. Applied to *parsed* JSON # strings, so the quote here is a real one rather than a backslash-escaped one in the raw text. @@ -142,7 +148,7 @@ def main() -> int: parser.add_argument("json_root", help="the javadoc-mode JSON tree (the api/ directory)") parser.add_argument("--db", default="documentation.db", help="path to documentation.db") parser.add_argument("--templates", type=Path, default=Path(__file__).resolve().parent / "db-templates", - help="directory of flattened templates to install") + help="directory holding the composed javadoc.peb") parser.add_argument("--dry-run", action="store_true", help="report what would change, write nothing") parser.add_argument("--delete-missing", action="store_true", help="also delete rows with no JSON counterpart (class-use/, package-use, " @@ -174,22 +180,30 @@ def main() -> int: compressor = DictionaryBrotli(dictionary) if dictionary else None print(f"Compression: {'shared-dictionary Brotli' if compressor else 'plain Brotli'}") - # --- templates ------------------------------------------------------- - template_ids: dict[str, int] = {} - for page_kind, filename in sorted(TEMPLATE_FOR_PAGE.items()): - source = (args.templates / filename).read_text(encoding="utf-8") - existing = cur.execute("SELECT id FROM Templates WHERE name = ?", (filename,)).fetchone() + # --- template -------------------------------------------------------- + source = (args.templates / TEMPLATE_NAME).read_text(encoding="utf-8") + existing = cur.execute("SELECT id FROM Templates WHERE name = ?", (TEMPLATE_NAME,)).fetchone() + if args.dry_run: + template_id = existing[0] if existing else -1 + print(f" [{'UPDATE' if existing else 'INSERT'} TEMPLATE] {TEMPLATE_NAME} ({len(source)} bytes)") + elif existing: + cur.execute("UPDATE Templates SET content = ? WHERE id = ?", (source.encode(), existing[0])) + template_id = existing[0] + else: + cur.execute("INSERT INTO Templates (name, content) VALUES (?, ?)", (TEMPLATE_NAME, source.encode())) + template_id = cur.lastrowid + print(f"Installed template '{TEMPLATE_NAME}' ({len(source)} bytes) for all page kinds.") + + stale = [row[0] for row in cur.execute( + f"SELECT name FROM Templates WHERE name IN ({','.join('?' * len(SUPERSEDED_TEMPLATES))})", + SUPERSEDED_TEMPLATES, + )] + if stale: if args.dry_run: - template_ids[page_kind] = existing[0] if existing else -1 - print(f" [{'UPDATE' if existing else 'INSERT'} TEMPLATE] {filename} ({len(source)} bytes)") - continue - if existing: - cur.execute("UPDATE Templates SET content = ? WHERE id = ?", (source.encode(), existing[0])) - template_ids[page_kind] = existing[0] + print(f" [DELETE TEMPLATE] {len(stale)} superseded per-page template(s): {', '.join(sorted(stale))}") else: - cur.execute("INSERT INTO Templates (name, content) VALUES (?, ?)", (filename, source.encode())) - template_ids[page_kind] = cur.lastrowid - print(f"Installed {len(TEMPLATE_FOR_PAGE)} template(s).") + cur.executemany("DELETE FROM Templates WHERE name = ?", [(n,) for n in stale]) + print(f"Removed {len(stale)} superseded per-page template(s).") html_type = cur.execute("SELECT id FROM ContentTypes WHERE value = 'text/html'").fetchone()[0] @@ -226,7 +240,7 @@ def main() -> int: document = json.loads(source_file.read_text(encoding="utf-8")) page_kind = document.get("page") - if page_kind not in TEMPLATE_FOR_PAGE: + if page_kind not in KNOWN_PAGES: unknown_pages.add(str(page_kind)) kept += 1 continue @@ -239,7 +253,7 @@ def main() -> int: if not args.dry_run: cur.execute( "UPDATE Content SET content = ?, contentTypeID = ?, templateId = ? WHERE id = ?", - (blob, html_type, template_ids[page_kind], row_id), + (blob, html_type, template_id, row_id), ) updated += 1 @@ -253,7 +267,7 @@ def main() -> int: continue document = json.loads(source_file.read_text(encoding="utf-8")) page_kind = document.get("page") - if page_kind not in TEMPLATE_FOR_PAGE: + if page_kind not in KNOWN_PAGES: continue document = rewrite_links(document) document["pathToRoot"] = path_to_root(content_path) @@ -263,7 +277,7 @@ def main() -> int: cur.execute( "INSERT INTO Content (path, languageID, content, contentTypeID, templateId) " "VALUES (?, ?, ?, ?, ?)", - (content_path, language_id, blob, html_type, template_ids[page_kind]), + (content_path, language_id, blob, html_type, template_id), ) added += 1 From c3d3171347edc5210fa171225f765c8766b3aeda Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Wed, 2 Sep 2026 14:04:26 -0500 Subject: [PATCH 09/14] ADFA-5296: Write the Java rows with plain Brotli so the app can decode them The rows were being compressed against the database's shared Brotli dictionary, matching the rest of the database. They decode fine with `brotli -D`, but not in the app: writing with the CLI's -D needs the reader to attach the same dictionary the same way, and it does not. --plain-brotli writes rows a stock decoder reads on its own. That is the documented fallback -- readers fall back to a plain decode for rows that are not dictionary-compressed -- and it costs +3.5% on these rows (12.23 MB -> 12.66 MB). The dictionary path is kept for whoever confirms how the reader attaches it. Also removes four Pebble features from the templates that no template already in the database uses: `is odd`, `join`, `capitalize` and `contains`. Filters, tests and operators come from extensions and a restricted engine can lack them, so the template now stays inside the surface the working templates demonstrate -- filters default/first/length/raw and the `empty` test, nothing else. All 51 uses were replaced with core syntax; re-rendering all 4,988 pages shows the only output change is the h1 wording, which now reads "Exception Class Foo" and "Enum Class Foo" as javadoc does rather than "Exception Foo". The LastChange label was "java"; the convention in that table is content- matching the path prefix, so it is now content-j. Verified: all 4,988 Java rows decode with a stock no-dictionary decoder, all ten page kinds render from the database through stock Pebble, nothing outside j/html/api/ changed, no rows added or removed, PRAGMA quick_check ok. Co-Authored-By: Claude Opus 5 --- .../main/resources/templates/all-classes.peb | 8 +- .../main/resources/templates/all-packages.peb | 6 +- .../src/main/resources/templates/class.peb | 14 +-- .../resources/templates/constant-values.peb | 6 +- .../resources/templates/deprecated-list.peb | 4 +- .../src/main/resources/templates/macros.peb | 10 +- .../resources/templates/module-summary.peb | 26 ++--- .../src/main/resources/templates/overview.peb | 10 +- .../resources/templates/package-summary.peb | 12 +-- scripts/sync_java_docs/README.md | 12 +++ .../sync_java_docs/db-templates/javadoc.peb | 96 +++++++++---------- .../sync_java_docs/sync_javadoc_json_to_db.py | 41 ++++++-- 12 files changed, 142 insertions(+), 103 deletions(-) diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-classes.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-classes.peb index 2bede7c9..4f18d523 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-classes.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-classes.peb @@ -9,10 +9,10 @@
      Class
      Package
      Description
      -{% for type in types %}{% if type.modifiers is empty or type.modifiers contains 'public' %} - -
      {{ type.packageName }}
      -
      {{ type.firstSentence | doc }}
      +{% for type in types %}{% set isPublic = false %}{% for modifier in type.modifiers %}{% if modifier == 'public' %}{% set isPublic = true %}{% endif %}{% endfor %}{% if type.modifiers is empty or isPublic %} + +
      {{ type.packageName }}
      +
      {{ type.firstSentence | doc }}
      {% endif %}{% endfor %}
    diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-packages.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-packages.peb index 644e7381..4fce9f27 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-packages.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/all-packages.peb @@ -10,9 +10,9 @@
    Package
    Description
    {% for pkg in packages %} -
    {{ pkg.moduleName }}
    - -
    {{ pkg.firstSentence | doc }}
    +
    {{ pkg.moduleName }}
    + +
    {{ pkg.firstSentence | doc }}
    {% endfor %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/class.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/class.peb index 043f0735..2998f8d2 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/class.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/class.peb @@ -21,7 +21,7 @@ {% if packageName is not empty %} {% endif %} -

    {{ kind | capitalize }} {{ name }}{% if typeParameters is not empty %}<{% for t in typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}>{% endif %}

    +

    {% if kind == "interface" %}Interface{% elseif kind == "enum" %}Enum Class{% elseif kind == "annotation" %}Annotation Interface{% elseif kind == "exception" %}Exception Class{% elseif kind == "record" %}Record Class{% else %}Class{% endif %} {{ name }}{% if typeParameters is not empty %}<{% for t in typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}>{% endif %}

    @@ -83,10 +83,10 @@ {% endif %} {% if since is not empty or seeAlso is not empty or tags is not empty or authors is not empty or versions is not empty %}
    -{% if since is not empty %}
    Since:
    {{ since | join(', ') }}
    {% endif %} +{% if since is not empty %}
    Since:
    {% for joinItem in since %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} {% for tag in tags %}
    {{ tagLabel(tag.name) }}
    {{ tag.text | doc }}
    {% endfor %} -{% if authors is not empty %}
    Author:
    {{ authors | join(', ') }}
    {% endif %} -{% if versions is not empty %}
    Version:
    {{ versions | join(', ') }}
    {% endif %} +{% if authors is not empty %}
    Author:
    {% for joinItem in authors %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} +{% if versions is not empty %}
    Version:
    {% for joinItem in versions %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} {% if seeAlso is not empty %}
    See Also:
      {% for see in seeAlso %}
    • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
    • {% endfor %}
    @@ -109,7 +109,7 @@
    Class
    Description
    {% for nested in nestedTypes %} -
    {{ nested.modifiers | join(' ') }} {{ nested.kind }}
    +
    {% for joinItem in nested.modifiers %}{{ joinItem }}{% if not loop.last %} {% endif %}{% endfor %} {{ nested.kind }}
    {{ nested.firstSentence | doc }}
    {% endfor %} @@ -153,7 +153,7 @@
    Field
    Description
    {% for field in fields %} -
    {{ field.modifiers | join(' ') }} {{ typeLink(field.type) }}
    +
    {% for joinItem in field.modifiers %}{{ joinItem }}{% if not loop.last %} {% endif %}{% endfor %} {{ typeLink(field.type) }}
    {{ field.firstSentence | doc }}
    {% endfor %} @@ -216,7 +216,7 @@
    Method
    Description
    {% for method in methods %} -
    {{ method.modifiers | join(' ') }} {{ typeLink(method.returnType) }}
    +
    {% for joinItem in method.modifiers %}{{ joinItem }}{% if not loop.last %} {% endif %}{% endfor %} {{ typeLink(method.returnType) }}
    {{ method.name }}{{ parameters(method.parameters) }}
    {{ method.firstSentence | doc }}
    {% endfor %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/constant-values.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/constant-values.peb index 79a7bef2..00ce83fd 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/constant-values.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/constant-values.peb @@ -14,9 +14,9 @@
    Constant Field
    Value
    {% for field in type.fields %} -
    {{ field.modifiers | join(' ') }} {{ field.type.display }}
    -
    {% if field.url is not empty %}{{ field.name }}{% else %}{{ field.name }}{% endif %}
    -
    {{ field.value }}
    +
    {% for joinItem in field.modifiers %}{{ joinItem }}{% if not loop.last %} {% endif %}{% endfor %} {{ field.type.display }}
    +
    {% if field.url is not empty %}{{ field.name }}{% else %}{{ field.name }}{% endif %}
    +
    {{ field.value }}
    {% endfor %} {% endfor %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/deprecated-list.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/deprecated-list.peb index 5489722e..091aa82c 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/deprecated-list.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/deprecated-list.peb @@ -31,8 +31,8 @@
    Element
    Description
    {% for entry in section.value %} -
    {% if entry.url is not empty %}{{ entry.element }}{% else %}{{ entry.element }}{% endif %}
    -
    +
    {% if entry.url is not empty %}{{ entry.element }}{% else %}{{ entry.element }}{% endif %}
    +
    {% if entry.forRemoval %}Terminally deprecated.{% endif %} {% if entry.since is not empty %}Since {{ entry.since }}.{% endif %} {% if entry.comment is not empty %}
    {{ entry.comment | doc }}
    {% endif %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/macros.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/macros.peb index 24d92a47..90aa6877 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/macros.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/macros.peb @@ -60,7 +60,7 @@ {# The modifier prefix of a signature, e.g. "public static final". #} {% macro modifiers(list) %} -{%- if list is not empty %}{{ list | join(' ') }} {% endif -%} +{%- if list is not empty %}{% for joinItem in list %}{{ joinItem }}{% if not loop.last %} {% endif %}{% endfor %} {% endif -%} {% endmacro %} {# The parameter list of an executable, with linked parameter types. #} @@ -74,7 +74,7 @@ {% endmacro %} {# Zebra striping, which javadoc drives off the row index. #} -{% macro rowColor(index) %}{% if index is odd %}odd-row-color{% else %}even-row-color{% endif %}{% endmacro %} +{% macro rowColor(index) %}{% if index % 2 == 1 %}odd-row-color{% else %}even-row-color{% endif %}{% endmacro %} {# The block tags shared by every documented element: since, see also, deprecation, custom tags. #} {% macro commonNotes(item) %} @@ -86,10 +86,10 @@ {% endif %} {% if item.since is not empty or item.seeAlso is not empty or item.tags is not empty or item.authors is not empty or item.versions is not empty %}
    -{% if item.since is not empty %}
    Since:
    {{ item.since | join(', ') }}
    {% endif %} +{% if item.since is not empty %}
    Since:
    {% for joinItem in item.since %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} {% for tag in item.tags %}
    {{ tagLabel(tag.name) }}
    {{ tag.text | doc }}
    {% endfor %} -{% if item.authors is not empty %}
    Author:
    {{ item.authors | join(', ') }}
    {% endif %} -{% if item.versions is not empty %}
    Version:
    {{ item.versions | join(', ') }}
    {% endif %} +{% if item.authors is not empty %}
    Author:
    {% for joinItem in item.authors %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} +{% if item.versions is not empty %}
    Version:
    {% for joinItem in item.versions %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} {% if item.seeAlso is not empty %}
    See Also:
      {% for see in item.seeAlso %}
    • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
    • {% endfor %}
    diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/module-summary.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/module-summary.peb index d6699a3a..5e185553 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/module-summary.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/module-summary.peb @@ -23,7 +23,7 @@ {% endif %} {% if since is not empty or tags is not empty or seeAlso is not empty %}
    -{% if since is not empty %}
    Since:
    {{ since | join(', ') }}
    {% endif %} +{% if since is not empty %}
    Since:
    {% for joinItem in since %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} {% for tag in tags %}{% if tag.name != 'moduleGraph' and tag.name != 'uses' and tag.name != 'provides' %}
    {{ tagLabel(tag.name) }}
    {{ tag.text | doc }}
    {% endif %}{% endfor %} {% if seeAlso is not empty %}
    See Also:
      {% for see in seeAlso %}
    • {{ see.label }}
    • {% endfor %}
    {% endif %}
    @@ -43,9 +43,9 @@
    Module
    Description
    {% for req in requires %} -
    {% if req.isTransitive %}transitive{% endif %}{% if req.isStatic %}static{% endif %}
    -
    {% if req.url is not empty %}{{ req.module }}{% else %}{{ req.module }}{% endif %}
    -
    +
    {% if req.isTransitive %}transitive{% endif %}{% if req.isStatic %}static{% endif %}
    +
    {% if req.url is not empty %}{{ req.module }}{% else %}{{ req.module }}{% endif %}
    +
    {% endfor %}
    @@ -81,9 +81,9 @@
    Exported To Modules
    Description
    {% for export in exports %} -
    {% if export.url is not empty %}{{ export.packageName }}{% else %}{{ export.packageName }}{% endif %}
    -
    {% if export.to is not empty %}{{ export.to | join(', ') }}{% else %}All Modules{% endif %}
    -
    {{ export.firstSentence | doc }}
    +
    {% if export.url is not empty %}{{ export.packageName }}{% else %}{{ export.packageName }}{% endif %}
    +
    {% if export.to is not empty %}{% for joinItem in export.to %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}{% else %}All Modules{% endif %}
    +
    {{ export.firstSentence | doc }}
    {% endfor %}
    @@ -114,8 +114,8 @@
    Package
    Opened To Modules
    {% for open in opens %} -
    {% if open.url is not empty %}{{ open.packageName }}{% else %}{{ open.packageName }}{% endif %}
    -
    {% if open.to is not empty %}{{ open.to | join(', ') }}{% else %}All Modules{% endif %}
    +
    {% if open.url is not empty %}{{ open.packageName }}{% else %}{{ open.packageName }}{% endif %}
    +
    {% if open.to is not empty %}{% for joinItem in open.to %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}{% else %}All Modules{% endif %}
    {% endfor %} @@ -132,8 +132,8 @@
    Type
    Description
    {% for use in uses %} -
    {{ typeLink(use) }}
    -
    +
    {{ typeLink(use) }}
    +
    {% endfor %} {% endif %} @@ -143,8 +143,8 @@
    Type
    Implementations
    {% for provide in provides %} -
    {{ typeLink(provide.service) }}
    -
    {% for impl in provide.implementations %}{{ typeLink(impl) }}{% if not loop.last %}, {% endif %}{% endfor %}
    +
    {{ typeLink(provide.service) }}
    +
    {% for impl in provide.implementations %}{{ typeLink(impl) }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endfor %} {% endif %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/overview.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/overview.peb index 70aaa31b..e4af8446 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/overview.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/overview.peb @@ -13,8 +13,8 @@
    Module
    Description
    {% for module in modules %} - -
    {{ module.firstSentence | doc }}
    + +
    {{ module.firstSentence | doc }}
    {% endfor %} @@ -29,9 +29,9 @@
    Package
    Description
    {% for pkg in packages %} -
    {{ pkg.moduleName }}
    - -
    {{ pkg.firstSentence | doc }}
    +
    {{ pkg.moduleName }}
    + +
    {{ pkg.firstSentence | doc }}
    {% endfor %} diff --git a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/package-summary.peb b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/package-summary.peb index 554fb1f1..f0bec379 100644 --- a/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/package-summary.peb +++ b/Dokka-plugin-kdoc2json/pebble-renderer/src/main/resources/templates/package-summary.peb @@ -18,8 +18,8 @@
    Class
    Description
    {% for row in rows %} - -
    {{ row.firstSentence | doc }}
    + +
    {{ row.firstSentence | doc }}
    {% endfor %} {% endif %} @@ -42,7 +42,7 @@ {% endif %} {% if since is not empty or seeAlso is not empty or tags is not empty %}
    -{% if since is not empty %}
    Since:
    {{ since | join(', ') }}
    {% endif %} +{% if since is not empty %}
    Since:
    {% for joinItem in since %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} {% for tag in tags %}
    {{ tagLabel(tag.name) }}
    {{ tag.text | doc }}
    {% endfor %} {% if seeAlso is not empty %}
    See Also:
      {% for see in seeAlso %}
    • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
    • {% endfor %}
    {% endif %}
    @@ -60,9 +60,9 @@
    Package
    Description
    {% for pkg in relatedPackages %} -
    {{ pkg.moduleName }}
    - -
    {{ pkg.firstSentence | doc }}
    +
    {{ pkg.moduleName }}
    + +
    {{ pkg.firstSentence | doc }}
    {% endfor %} diff --git a/scripts/sync_java_docs/README.md b/scripts/sync_java_docs/README.md index 2e7024a0..097ab08a 100644 --- a/scripts/sync_java_docs/README.md +++ b/scripts/sync_java_docs/README.md @@ -56,6 +56,18 @@ what the reader already passes: the reader passes nothing but the JSON. - The `page` field selects the template, so that mapping lives here and not in the reader. +## Compression: use `--plain-brotli` unless you know the reader attaches the dictionary + +The database's other rows are compressed against a shared Brotli dictionary (ADFA-5153), and this +script will do the same by default. That turned out **not** to work with the app: a row written +with the `brotli` CLI's `-D` needs the reader to attach the same dictionary the same way, and it +does not, so the content failed to decode. + +`--plain-brotli` writes rows a stock Brotli decoder can read on its own, which is the documented +fallback — readers fall back to a plain decode for rows that are not dictionary-compressed. It +costs **+3.5%** on these rows (12.23 MB → 12.66 MB), which is a small price for content that +actually decodes. Prefer it until someone confirms how the reader attaches the dictionary. + ## What it leaves alone About half the rows under `j/html/api/` are page kinds this pipeline does not generate — diff --git a/scripts/sync_java_docs/db-templates/javadoc.peb b/scripts/sync_java_docs/db-templates/javadoc.peb index 40829c91..72b06246 100644 --- a/scripts/sync_java_docs/db-templates/javadoc.peb +++ b/scripts/sync_java_docs/db-templates/javadoc.peb @@ -67,7 +67,7 @@ {% if packageName is not empty %} {% endif %} -

    {{ kind | capitalize }} {{ name }}{% if typeParameters is not empty %}<{% for t in typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}>{% endif %}

    +

    {% if kind == "interface" %}Interface{% elseif kind == "enum" %}Enum Class{% elseif kind == "annotation" %}Annotation Interface{% elseif kind == "exception" %}Exception Class{% elseif kind == "record" %}Record Class{% else %}Class{% endif %} {{ name }}{% if typeParameters is not empty %}<{% for t in typeParameters %}{{ t.name }}{% if not loop.last %},{% endif %}{% endfor %}>{% endif %}

    @@ -129,10 +129,10 @@ {% endif %} {% if since is not empty or seeAlso is not empty or tags is not empty or authors is not empty or versions is not empty %}
    -{% if since is not empty %}
    Since:
    {{ since | join(', ') }}
    {% endif %} +{% if since is not empty %}
    Since:
    {% for joinItem in since %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} {% for tag in tags %}
    {{ tagLabel(tag.name) }}
    {{ tag.text | raw }}
    {% endfor %} -{% if authors is not empty %}
    Author:
    {{ authors | join(', ') }}
    {% endif %} -{% if versions is not empty %}
    Version:
    {{ versions | join(', ') }}
    {% endif %} +{% if authors is not empty %}
    Author:
    {% for joinItem in authors %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} +{% if versions is not empty %}
    Version:
    {% for joinItem in versions %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} {% if seeAlso is not empty %}
    See Also:
      {% for see in seeAlso %}
    • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
    • {% endfor %}
    @@ -155,7 +155,7 @@
    Class
    Description
    {% for nested in nestedTypes %} -
    {{ nested.modifiers | join(' ') }} {{ nested.kind }}
    +
    {% for joinItem in nested.modifiers %}{{ joinItem }}{% if not loop.last %} {% endif %}{% endfor %} {{ nested.kind }}
    {{ nested.firstSentence | raw }}
    {% endfor %} @@ -199,7 +199,7 @@
    Field
    Description
    {% for field in fields %} -
    {{ field.modifiers | join(' ') }} {{ typeLink(field.type) }}
    +
    {% for joinItem in field.modifiers %}{{ joinItem }}{% if not loop.last %} {% endif %}{% endfor %} {{ typeLink(field.type) }}
    {{ field.firstSentence | raw }}
    {% endfor %} @@ -262,7 +262,7 @@
    Method
    Description
    {% for method in methods %} -
    {{ method.modifiers | join(' ') }} {{ typeLink(method.returnType) }}
    +
    {% for joinItem in method.modifiers %}{{ joinItem }}{% if not loop.last %} {% endif %}{% endfor %} {{ typeLink(method.returnType) }}
    {{ method.name }}{{ parameters(method.parameters) }}
    {{ method.firstSentence | raw }}
    {% endfor %} @@ -358,7 +358,7 @@ {% endif %} {% if since is not empty or seeAlso is not empty or tags is not empty %}
    -{% if since is not empty %}
    Since:
    {{ since | join(', ') }}
    {% endif %} +{% if since is not empty %}
    Since:
    {% for joinItem in since %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} {% for tag in tags %}
    {{ tagLabel(tag.name) }}
    {{ tag.text | raw }}
    {% endfor %} {% if seeAlso is not empty %}
    See Also:
      {% for see in seeAlso %}
    • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
    • {% endfor %}
    {% endif %}
    @@ -376,9 +376,9 @@
    Package
    Description
    {% for pkg in relatedPackages %} -
    {{ pkg.moduleName }}
    - -
    {{ pkg.firstSentence | raw }}
    +
    {{ pkg.moduleName }}
    + +
    {{ pkg.firstSentence | raw }}
    {% endfor %} @@ -412,7 +412,7 @@ {% endif %} {% if since is not empty or tags is not empty or seeAlso is not empty %}
    -{% if since is not empty %}
    Since:
    {{ since | join(', ') }}
    {% endif %} +{% if since is not empty %}
    Since:
    {% for joinItem in since %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} {% for tag in tags %}{% if tag.name != 'moduleGraph' and tag.name != 'uses' and tag.name != 'provides' %}
    {{ tagLabel(tag.name) }}
    {{ tag.text | raw }}
    {% endif %}{% endfor %} {% if seeAlso is not empty %}
    See Also:
      {% for see in seeAlso %}
    • {{ see.label }}
    • {% endfor %}
    {% endif %}
    @@ -432,9 +432,9 @@
    Module
    Description
    {% for req in requires %} -
    {% if req.isTransitive %}transitive{% endif %}{% if req.isStatic %}static{% endif %}
    -
    {% if req.url is not empty %}{{ req.module }}{% else %}{{ req.module }}{% endif %}
    -
    +
    {% if req.isTransitive %}transitive{% endif %}{% if req.isStatic %}static{% endif %}
    +
    {% if req.url is not empty %}{{ req.module }}{% else %}{{ req.module }}{% endif %}
    +
    {% endfor %} @@ -470,9 +470,9 @@
    Exported To Modules
    Description
    {% for export in exports %} -
    {% if export.url is not empty %}{{ export.packageName }}{% else %}{{ export.packageName }}{% endif %}
    -
    {% if export.to is not empty %}{{ export.to | join(', ') }}{% else %}All Modules{% endif %}
    -
    {{ export.firstSentence | raw }}
    +
    {% if export.url is not empty %}{{ export.packageName }}{% else %}{{ export.packageName }}{% endif %}
    +
    {% if export.to is not empty %}{% for joinItem in export.to %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}{% else %}All Modules{% endif %}
    +
    {{ export.firstSentence | raw }}
    {% endfor %} @@ -503,8 +503,8 @@
    Package
    Opened To Modules
    {% for open in opens %} -
    {% if open.url is not empty %}{{ open.packageName }}{% else %}{{ open.packageName }}{% endif %}
    -
    {% if open.to is not empty %}{{ open.to | join(', ') }}{% else %}All Modules{% endif %}
    +
    {% if open.url is not empty %}{{ open.packageName }}{% else %}{{ open.packageName }}{% endif %}
    +
    {% if open.to is not empty %}{% for joinItem in open.to %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}{% else %}All Modules{% endif %}
    {% endfor %} @@ -521,8 +521,8 @@
    Type
    Description
    {% for use in uses %} -
    {{ typeLink(use) }}
    -
    +
    {{ typeLink(use) }}
    +
    {% endfor %} {% endif %} @@ -532,8 +532,8 @@
    Type
    Implementations
    {% for provide in provides %} -
    {{ typeLink(provide.service) }}
    -
    {% for impl in provide.implementations %}{{ typeLink(impl) }}{% if not loop.last %}, {% endif %}{% endfor %}
    +
    {{ typeLink(provide.service) }}
    +
    {% for impl in provide.implementations %}{{ typeLink(impl) }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endfor %} {% endif %} @@ -555,8 +555,8 @@
    Module
    Description
    {% for module in modules %} - -
    {{ module.firstSentence | raw }}
    + +
    {{ module.firstSentence | raw }}
    {% endfor %} @@ -571,9 +571,9 @@
    Package
    Description
    {% for pkg in packages %} -
    {{ pkg.moduleName }}
    - -
    {{ pkg.firstSentence | raw }}
    +
    {{ pkg.moduleName }}
    + +
    {{ pkg.firstSentence | raw }}
    {% endfor %} @@ -587,10 +587,10 @@
    Class
    Package
    Description
    -{% for type in types %}{% if type.modifiers is empty or type.modifiers contains 'public' %} - -
    {{ type.packageName }}
    -
    {{ type.firstSentence | raw }}
    +{% for type in types %}{% set isPublic = false %}{% for modifier in type.modifiers %}{% if modifier == 'public' %}{% set isPublic = true %}{% endif %}{% endfor %}{% if type.modifiers is empty or isPublic %} + +
    {{ type.packageName }}
    +
    {{ type.firstSentence | raw }}
    {% endif %}{% endfor %} @@ -603,9 +603,9 @@
    Package
    Description
    {% for pkg in packages %} -
    {{ pkg.moduleName }}
    - -
    {{ pkg.firstSentence | raw }}
    +
    {{ pkg.moduleName }}
    + +
    {{ pkg.firstSentence | raw }}
    {% endfor %} @@ -623,8 +623,8 @@
    Element
    Description
    {% for entry in section.value %} -
    {% if entry.url is not empty %}{{ entry.element }}{% else %}{{ entry.element }}{% endif %}
    -
    +
    {% if entry.url is not empty %}{{ entry.element }}{% else %}{{ entry.element }}{% endif %}
    +
    {% if entry.forRemoval %}Terminally deprecated.{% endif %} {% if entry.since is not empty %}Since {{ entry.since }}.{% endif %} {% if entry.comment is not empty %}
    {{ entry.comment | raw }}
    {% endif %} @@ -646,9 +646,9 @@
    Constant Field
    Value
    {% for field in type.fields %} -
    {{ field.modifiers | join(' ') }} {{ field.type.display }}
    -
    {% if field.url is not empty %}{{ field.name }}{% else %}{{ field.name }}{% endif %}
    -
    {{ field.value }}
    +
    {% for joinItem in field.modifiers %}{{ joinItem }}{% if not loop.last %} {% endif %}{% endfor %} {{ field.type.display }}
    +
    {% if field.url is not empty %}{{ field.name }}{% else %}{{ field.name }}{% endif %}
    +
    {{ field.value }}
    {% endfor %}
    {% endfor %} @@ -692,10 +692,10 @@ {% endif %} {% if item.since is not empty or item.seeAlso is not empty or item.tags is not empty or item.authors is not empty or item.versions is not empty %}
    -{% if item.since is not empty %}
    Since:
    {{ item.since | join(', ') }}
    {% endif %} +{% if item.since is not empty %}
    Since:
    {% for joinItem in item.since %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} {% for tag in item.tags %}
    {{ tagLabel(tag.name) }}
    {{ tag.text | raw }}
    {% endfor %} -{% if item.authors is not empty %}
    Author:
    {{ item.authors | join(', ') }}
    {% endif %} -{% if item.versions is not empty %}
    Version:
    {{ item.versions | join(', ') }}
    {% endif %} +{% if item.authors is not empty %}
    Author:
    {% for joinItem in item.authors %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} +{% if item.versions is not empty %}
    Version:
    {% for joinItem in item.versions %}{{ joinItem }}{% if not loop.last %}, {% endif %}{% endfor %}
    {% endif %} {% if item.seeAlso is not empty %}
    See Also:
      {% for see in item.seeAlso %}
    • {% if see.url is not empty %}{{ see.label }}{% else %}{{ see.label }}{% endif %}
    • {% endfor %}
    @@ -762,7 +762,7 @@ {%- if ref.url is not empty -%}{{ ref.name }}{%- else -%}{{ ref.name }}{%- endif -%} {% endmacro %} {% macro modifiers(list) %} -{%- if list is not empty %}{{ list | join(' ') }} {% endif -%} +{%- if list is not empty %}{% for joinItem in list %}{{ joinItem }}{% if not loop.last %} {% endif %}{% endfor %} {% endif -%} {% endmacro %} {% macro note(label, body) %}
    {{ label }}
    @@ -771,7 +771,7 @@ {% macro parameters(params) %} ({%- for p in params -%}{{ typeLink(p.type) }} {{ p.name }}{% if not loop.last %}, {% endif %}{%- endfor -%}) {% endmacro %} -{% macro rowColor(index) %}{% if index is odd %}odd-row-color{% else %}even-row-color{% endif %}{% endmacro %} +{% macro rowColor(index) %}{% if index % 2 == 1 %}odd-row-color{% else %}even-row-color{% endif %}{% endmacro %} {% macro sectionTitle(kind) %} {%- if kind == 'classes' -%}Deprecated Classes {%- elseif kind == 'interfaces' -%}Deprecated Interfaces @@ -823,8 +823,8 @@
    Class
    Description
    {% for row in rows %} - -
    {{ row.firstSentence | raw }}
    + +
    {{ row.firstSentence | raw }}
    {% endfor %}
    {% endif %} diff --git a/scripts/sync_java_docs/sync_javadoc_json_to_db.py b/scripts/sync_java_docs/sync_javadoc_json_to_db.py index 63c2dce6..dfb58d2f 100755 --- a/scripts/sync_java_docs/sync_javadoc_json_to_db.py +++ b/scripts/sync_java_docs/sync_javadoc_json_to_db.py @@ -90,6 +90,28 @@ def load_compression_dictionary(conn): return row[0] if row and row[0] else None +class PlainBrotli: + """Compresses without the shared dictionary. + + A row written this way is larger than the rest of the database, but it decodes with nothing + but a stock Brotli decoder, which is what makes it the safe choice when the reader cannot + attach the dictionary the way the `brotli` CLI's -D expects. + """ + + def __init__(self): + path = shutil.which("brotli") + if path is None: + raise RuntimeError("needs the `brotli` command-line tool; install it (brew install brotli)") + self._brotli = path + + def compress(self, data: bytes) -> bytes: + result = subprocess.run([self._brotli, "-c"], input=data, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + if result.returncode != 0: + raise RuntimeError(f"brotli failed: {result.stderr.decode(errors='replace').strip()}") + return result.stdout + + class DictionaryBrotli: """Compresses against a raw Brotli dictionary via the `brotli` CLI -- the Python package exposes no dictionary parameter. Mirrors sync_kdoc_json_to_db.DictionaryBrotli.""" @@ -150,6 +172,11 @@ def main() -> int: parser.add_argument("--templates", type=Path, default=Path(__file__).resolve().parent / "db-templates", help="directory holding the composed javadoc.peb") parser.add_argument("--dry-run", action="store_true", help="report what would change, write nothing") + parser.add_argument("--plain-brotli", action="store_true", + help="compress without the shared dictionary. Rows come out larger, but a " + "reader that cannot attach the dictionary still decodes them -- " + "readers fall back to a plain decode. Use this if dictionary-" + "compressed rows fail to decode in the app.") parser.add_argument("--delete-missing", action="store_true", help="also delete rows with no JSON counterpart (class-use/, package-use, " "the tree pages...). Off by default: those are working pages this " @@ -176,9 +203,9 @@ def main() -> int: conn = sqlite3.connect(args.db) cur = conn.cursor() - dictionary = load_compression_dictionary(conn) - compressor = DictionaryBrotli(dictionary) if dictionary else None - print(f"Compression: {'shared-dictionary Brotli' if compressor else 'plain Brotli'}") + dictionary = None if args.plain_brotli else load_compression_dictionary(conn) + compressor = DictionaryBrotli(dictionary) if dictionary else PlainBrotli() + print(f"Compression: {'shared-dictionary Brotli' if dictionary else 'plain Brotli'}") # --- template -------------------------------------------------------- source = (args.templates / TEMPLATE_NAME).read_text(encoding="utf-8") @@ -232,7 +259,7 @@ def main() -> int: # carries is stored as-is: it is not a page, has no template, and needs no rewriting. if source_file.suffix != ".json": raw = source_file.read_bytes() - blob = compressor.compress(raw) if compressor else raw + blob = compressor.compress(raw) if not args.dry_run: cur.execute("UPDATE Content SET content = ?, templateId = 0 WHERE id = ?", (blob, row_id)) passed_through += 1 @@ -248,7 +275,7 @@ def main() -> int: document = rewrite_links(document) document["pathToRoot"] = path_to_root(path) payload = json.dumps(document, separators=(",", ":")).encode("utf-8") - blob = compressor.compress(payload) if compressor else payload + blob = compressor.compress(payload) if not args.dry_run: cur.execute( @@ -272,7 +299,7 @@ def main() -> int: document = rewrite_links(document) document["pathToRoot"] = path_to_root(content_path) payload = json.dumps(document, separators=(",", ":")).encode("utf-8") - blob = compressor.compress(payload) if compressor else payload + blob = compressor.compress(payload) if not args.dry_run: cur.execute( "INSERT INTO Content (path, languageID, content, contentTypeID, templateId) " @@ -297,7 +324,7 @@ def main() -> int: if not args.dry_run: cur.execute("INSERT INTO LastChange (documentationSet, who) VALUES (?, ?)", - ("java", "sync_javadoc_json_to_db.py")) + ("content-j", "sync_javadoc_json_to_db.py")) conn.commit() conn.close() From dcaa6c2ec3a8af08df96741627a5254fbe2d8971 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Wed, 2 Sep 2026 14:27:26 -0500 Subject: [PATCH 10/14] ADFA-5296: Prune the fields no template reads from the stored Java JSON Cuts the stored Java rows by 7.9% (12.66 MB -> 11.67 MB), which finally puts them below the HTML they replaced: 12.49 MB -> 11.67 MB, -6.6%. A member's `signature` and `url` are never read. fieldDetail and executableDetail compose a signature from modifiers/typeParameters/returnType/ parameters/exceptions themselves, and a summary table links to a member on the page being rendered as "#" + anchor. Rendering ArrayList from the pruned row is byte-identical to the unpruned render bar one blank line, which is the proof they were dead weight. Three fields measured as tempting but kept, because the reader cannot rebuild them: a type reference's `url` (-8.5% if dropped) needs the target's module and package to reconstruct, which costs more than the relative path it replaces; `firstSentence` (-4.8%) cannot be extracted from `description` with the filters available; and the class page's own `signature` is read directly. My earlier -20.9% estimate assumed all three could go, which was wrong -- 7.9% is what is actually safe. Pruning happens in the sync script rather than the plugin, alongside the link rewriting and pathToRoot injection it already does, so the on-disk JSON stays complete for the file renderer and for any other consumer. Finished with a plain VACUUM to return the 985 freed pages to the filesystem; page_size is untouched. Final database is 248,095,744 bytes, 787 KB below where it started. Co-Authored-By: Claude Opus 5 --- scripts/sync_java_docs/README.md | 11 ++++++ .../sync_java_docs/sync_javadoc_json_to_db.py | 38 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/scripts/sync_java_docs/README.md b/scripts/sync_java_docs/README.md index 097ab08a..087ddeca 100644 --- a/scripts/sync_java_docs/README.md +++ b/scripts/sync_java_docs/README.md @@ -68,6 +68,17 @@ fallback — readers fall back to a plain decode for rows that are not dictionar costs **+3.5%** on these rows (12.23 MB → 12.66 MB), which is a small price for content that actually decodes. Prefer it until someone confirms how the reader attaches the dictionary. +## What is dropped on the way in + +Member-level `signature` and `url` are pruned, because no template reads them: the detail macros +compose a member's signature from its parts, and a summary table links to a member on the page +being rendered as `#` + `anchor`. Worth 7.9% of the Java rows. + +Not pruned, because they *are* read and cannot be recomputed with the filters the reader has: the +class page's own `signature`, `JdConstantField.url`, every type reference's `url` (working one out +needs the target's module and package, which costs more than the string it replaces), and every +`firstSentence`. + ## What it leaves alone About half the rows under `j/html/api/` are page kinds this pipeline does not generate — diff --git a/scripts/sync_java_docs/sync_javadoc_json_to_db.py b/scripts/sync_java_docs/sync_javadoc_json_to_db.py index dfb58d2f..932608f3 100755 --- a/scripts/sync_java_docs/sync_javadoc_json_to_db.py +++ b/scripts/sync_java_docs/sync_javadoc_json_to_db.py @@ -26,6 +26,8 @@ the reader passes nothing but the JSON itself. - `page` is left in the JSON, because the single template branches on it: the reader can only load one template per page, so all nine page kinds share one and choose their markup from it. + - Member-level `signature` and `url` are dropped, because no template reads them -- the detail + macros compose a member's signature from its parts and link it by anchor. See PRUNED_* below. A timestamped backup is taken before anything is written. """ @@ -72,6 +74,24 @@ # strings, so the quote here is a real one rather than a backslash-escaped one in the raw text. JSON_EXTENSION = re.compile(r'\.json(?=["#]|$)') +# Fields the template provably never reads, dropped to keep the stored blob small. Worth 7.9% of +# the Java rows. +# +# A member's `signature` is redundant: `fieldDetail` and `executableDetail` compose the signature +# from `modifiers`, `typeParameters`, `returnType`, `parameters` and `exceptions` themselves. A +# member's `url` is redundant too: the summary tables link to it as "#" + `anchor`, because the +# member is on the page being rendered. +# +# What is NOT pruned, because it *is* read: the class page's own `signature` (the type-signature +# block), `JdConstantField.url` (constant-values links to another page), every type reference's +# `url`, and every `firstSentence` -- a summary sentence cannot be recomputed from `description` +# with the filters the reader has. +PRUNED_IN_MEMBER_LISTS = frozenset({"signature", "url"}) +MEMBER_LISTS = frozenset({"methods", "constructors", "annotationElements", "fields", "enumConstants"}) +# JdMemberRef carries a `signature`; memberLink renders `name` and links by `url`. +PRUNED_IN_REFERENCES = frozenset({"signature"}) +REFERENCE_KEYS = frozenset({"specifiedBy", "members", "overrides"}) + def backup_database(db_path: str) -> str: stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") @@ -155,6 +175,20 @@ def rewrite_links(value): return value +def prune_unread(value, key=None): + """Drops the fields listed above, wherever they appear beneath a member list or reference.""" + if isinstance(value, dict): + return { + k: prune_unread(v, k) + for k, v in value.items() + if not (key in MEMBER_LISTS and k in PRUNED_IN_MEMBER_LISTS) + and not (key in REFERENCE_KEYS and k in PRUNED_IN_REFERENCES) + } + if isinstance(value, list): + return [prune_unread(v, key) for v in value] + return value + + def path_to_root(content_path: str) -> str: """`j/html/api/java.base/java/util/ArrayList.html` -> `../../../`. @@ -272,7 +306,7 @@ def main() -> int: kept += 1 continue - document = rewrite_links(document) + document = prune_unread(rewrite_links(document)) document["pathToRoot"] = path_to_root(path) payload = json.dumps(document, separators=(",", ":")).encode("utf-8") blob = compressor.compress(payload) @@ -296,7 +330,7 @@ def main() -> int: page_kind = document.get("page") if page_kind not in KNOWN_PAGES: continue - document = rewrite_links(document) + document = prune_unread(rewrite_links(document)) document["pathToRoot"] = path_to_root(content_path) payload = json.dumps(document, separators=(",", ":")).encode("utf-8") blob = compressor.compress(payload) From 1389aa5114fc67540911a6842398673d057c5dac Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Thu, 3 Sep 2026 15:19:13 -0500 Subject: [PATCH 11/14] ADFA-5296: Add build-java-docs workflows, mirroring the Kotlin pair build-java-docs.yaml and build-java-docs-local.yaml stand in the same relation to each other as build-kotlin-docs.yaml and build-kotlin-docs-local.yaml on fix/ADFA-4739: same structure, same inputs where they mean the same thing, same Slack baton messages, same dry_run default of true, and the local one differing only in that it reads documentation.db from db_path and writes it back to db_path / output_dir instead of talking to Google Drive. No WIF, Drive API or associated secrets appear anywhere in the local file. Three steps rather than Kotlin's five, because the Java pipeline is shorter: build-jdk-json-docs.sh (stage the JDK's exported sources, run Dokka in javadoc-mode) -> flatten_templates.py (compose the single database template) -> sync_javadoc_json_to_db.py. There is no images-zip input: that is a Writerside thing with no Java equivalent, so the local variant needs only db_path and output_dir. Two checks stand in for the Kotlin blacklist verification: a parity comparison against the reference javadoc in SourceDocs/JavaDocs, which fails on any missing or extra module/package/type, and a decode check that every stored row reads back with a stock no-dictionary Brotli decoder. The second guards a silent failure -- a row that is written but cannot be decoded looks fine in the database and blank in the app, which is exactly what happened during this ticket. build-jdk-json-docs.sh gains --dokka-worker-heap, forwarded to Gradle as -PdokkaWorkerHeap and echoed when set. Dokka's worker is not the Gradle daemon, so org.gradle.jvmargs and GRADLE_OPTS do not size it; an environment-based override fails silently, which is why this is an explicit flag. The workflows leave it unset by default: 24g is a ceiling rather than a reservation, so it is fine on a smaller runner as long as real usage fits in RAM. Caveat: I could not confirm on this machine that the override actually reaches the worker, so nothing in the happy path depends on it. If a run OOMs, set dokka_worker_heap and check the "Dokka worker heap" line the script logs to confirm it applied. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-java-docs-local.yaml | 323 ++++++++++++++++ .github/workflows/build-java-docs.yaml | 346 ++++++++++++++++++ .../scripts/java/build-jdk-json-docs.sh | 20 +- 3 files changed, 687 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/build-java-docs-local.yaml create mode 100644 .github/workflows/build-java-docs.yaml diff --git a/.github/workflows/build-java-docs-local.yaml b/.github/workflows/build-java-docs-local.yaml new file mode 100644 index 00000000..79bc95d4 --- /dev/null +++ b/.github/workflows/build-java-docs-local.yaml @@ -0,0 +1,323 @@ +name: Build Java Docs (Local) + +# Local-filesystem counterpart of build-java-docs.yaml: same three steps +# (build-jdk-json-docs -> flatten_templates -> sync_javadoc_json_to_db), but +# reads its documentation.db input from a path on the runner's own disk +# (db_path) instead of Google Drive, and writes its outputs (the updated +# database, a run-numbered copy) back to disk (output_dir / db_path) instead of +# uploading them to Drive. No GCP Workload Identity Federation, Drive API, or +# associated secrets are used anywhere in this file. The Kotlin equivalent is +# build-kotlin-docs-local.yaml; this file deliberately mirrors its shape. +# +# Since a GitHub-hosted runner is a fresh, disposable VM with no access to +# anyone's actual local disk, db_path/output_dir only make sense here against a +# self-hosted runner, or when this workflow is run locally (e.g. via +# https://github.com/nektos/act) with those host paths bind-mounted into the +# job's container at the paths you pass as inputs. +# +# CAUTION when invoking act by hand: act does NOT apply workflow_dispatch input +# defaults. Every "default:" below applies on real GitHub and is simply absent +# under act, so an input you don't pass arrives empty. That matters most for +# dry_run, whose default is true: with it unset, "${{ !inputs.dry_run }}" +# evaluates to true and the final step writes the rebuilt database back over +# db_path. Always pass --input dry_run=true/false explicitly. +# +# What it does: unpacks the JDK's lib/src.zip, keeps only the packages javadoc +# documents (those a module `exports` unqualified), runs Dokka with +# kdoc-to-json in javadoc-mode over them, composes the single Pebble template +# the database serves those pages from, and replaces the j/html/api/ rows with +# the resulting JSON. +# +# NOTE ON MEMORY: Dokka generates in a *worker process*, which +# org.gradle.jvmargs does not size - jdk-docs/build.gradle.kts sizes it via +# dokkaGeneratorIsolation, defaulting to a 24g maximum. That is a ceiling, not +# a reservation, so it is fine on a smaller runner as long as the build's real +# usage fits in RAM; documenting the whole JDK is ~4,800 source files in one +# analysis pass. The default is therefore left alone here. If a run does die +# with an OutOfMemoryError, set dokka_worker_heap below, which is forwarded to +# Gradle as -PdokkaWorkerHeap - and check the "Dokka worker heap" line the +# build script logs to confirm it was actually applied, since nothing else +# reports it. +# +# NOTE ON JDK VERSION: java_version picks both the JDK whose sources are +# documented AND the JDK the Gradle builds run on. It must be 21 or lower: +# kdoc-to-json is pinned to Kotlin 1.9.24, whose compiler cannot run on a newer +# JDK at all (it fails parsing the version string). It must also be a JDK, not +# a JRE, since the sources come from its lib/src.zip. +# +# Optional secret (Slack notifications are skipped with a warning if unset) - +# same as build-java-docs.yaml: +# SLACK_WEBHOOK_URL - Incoming Webhook URL for the "Notify Slack" steps +# below ("Grabbing baton" on start, "...Dropping +# baton" on finish - org shorthand for lock +# acquire/release, since this workflow mutates a +# single shared local file, db_path). + +permissions: + contents: read + +# This workflow overwrites a single shared local file (db_path) - never let +# two runs race to write it at the same time. +concurrency: + group: build-java-docs-local + cancel-in-progress: false + +on: + workflow_dispatch: + inputs: + java_version: + description: >- + JDK whose lib/src.zip is documented, and which the Gradle builds run + on. Must be 21 or lower (kdoc-to-json is pinned to Kotlin 1.9.24, + which cannot run on a newer JDK). Keep this in step with the + reference docs in SourceDocs/JavaDocs, which are Java SE 17. + required: false + default: '17' + db_path: + description: >- + Path on this runner's disk to the input documentation.db. Read + directly (no download/unzip) and, unless dry_run is true, written + back to this same path when the run finishes. + required: true + output_dir: + description: >- + Directory on this runner's disk to write outputs into: a + run-numbered copy of the built database + (documentation-db-.db). Created if it doesn't already + exist. + required: false + default: 'build-java-docs-output' + modules: + description: >- + Comma-separated JPMS module names to document instead of all of + them, e.g. "java.sql,java.transaction.xa". Leave empty for the whole + JDK. A subset run takes seconds rather than minutes and is the quick + way to smoke-test a change; it will also make the parity check below + fail, so pair it with verify_parity=false. + required: false + default: '' + dokka_worker_heap: + description: >- + Max heap for Dokka's worker process, e.g. 6g (see NOTE ON MEMORY + above). Leave empty to use the build's own default; set it only if a + run dies with an OutOfMemoryError. + required: false + default: '' + verify_parity: + description: >- + Compare the generated tree against the reference javadoc HTML in + SourceDocs/JavaDocs/html/api and fail on any missing or extra + module/package/type. Only meaningful for a full java_version=17 run; + turn it off for a subset or a different JDK. + required: false + default: true + type: boolean + delete_missing: + description: >- + Also delete j/html/api/ rows that have no JSON counterpart - + class-use/, package-use, the tree pages, serialized-form. That is + about half the rows. They are working documentation nothing in the + new pages links to, so the default is to leave them alone. + required: false + default: false + type: boolean + dry_run: + description: >- + If true, build and verify everything but do NOT write the result + back to db_path - the input file on disk is left untouched. Set to + false only once you trust a given version/module combination. + required: false + default: true + type: boolean + +jobs: + build-java-docs-local: + runs-on: ubuntu-latest + timeout-minutes: 120 + env: + DB_PATH: ${{ inputs.db_path }} + OUTPUT_DIR: ${{ inputs.output_dir }} + JAVA_VERSION: ${{ inputs.java_version }} + MODULES: ${{ inputs.modules }} + DOKKA_WORKER_HEAP: ${{ inputs.dokka_worker_heap }} + DELETE_MISSING: ${{ inputs.delete_missing }} + # Where build-jdk-json-docs.sh writes the JSON tree and its scratch + # staging copy of the JDK sources. + JSON_OUT: ${{ github.workspace }}/java-json-build/api + WORK_DIR: ${{ github.workspace }}/java-json-build/work + REFERENCE_API: ${{ github.workspace }}/SourceDocs/JavaDocs/html/api + steps: + - name: Checkout OfflineDocumentationTools + uses: actions/checkout@v4 + + - name: Resolve local file paths + run: | + if [ ! -f "$DB_PATH" ]; then + echo "Error: db_path '$DB_PATH' does not exist on this runner - for a self-hosted runner this must be a path on that machine; for act, bind-mount it into the container so it's visible at this exact path" >&2 + exit 1 + fi + mkdir -p "$OUTPUT_DIR" + echo "Resolved DB_PATH: $DB_PATH" + echo "Resolved OUTPUT_DIR: $OUTPUT_DIR" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Set up JDK (documented sources + the kdoc-to-json / jdk-docs Gradle builds) + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ inputs.java_version }} + + - name: Verify the JDK ships sources + run: | + # The whole pipeline reads lib/src.zip; a JRE or a stripped JDK has + # none, and the failure would otherwise surface as an empty staging + # tree several minutes later. + if [ ! -f "$JAVA_HOME/lib/src.zip" ]; then + echo "Error: $JAVA_HOME/lib/src.zip not found - java_version must name a JDK that ships sources, not a JRE" >&2 + exit 1 + fi + echo "Documenting sources from $JAVA_HOME/lib/src.zip" + "$JAVA_HOME/bin/java" -version + + - name: Install system dependencies + run: | + sudo apt-get update -y + # brotli: the CLI, not the Python package. sync_javadoc_json_to_db.py + # shells out to it because no Python binding exposes a custom + # dictionary (ADFA-5153), and it is also what writes the plain-Brotli + # rows this pipeline stores. + sudo apt-get install -y unzip sqlite3 brotli + + - name: Install Python dependencies + run: | + pip install -r requirements.txt + + - name: Copy documentation.db from local disk + run: | + cp "$DB_PATH" documentation.db + sqlite3 documentation.db "SELECT 1;" > /dev/null + echo "DB_SIZE=$(stat -c%s documentation.db 2>/dev/null || stat -f%z documentation.db)" >> "$GITHUB_ENV" + + - name: 'Notify Slack: build started' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + run: | + if [ -z "$SLACK_WEBHOOK_URL" ]; then + echo "SLACK_WEBHOOK_URL not set - skipping Slack notification" >&2 + else + curl -sS -X POST -H 'Content-type: application/json' \ + --data '{"text": "Grabbing baton"}' \ + "$SLACK_WEBHOOK_URL" || echo "warning: Slack notification failed" >&2 + fi + + - name: 'Step 1/3: build-jdk-json-docs.sh (stage JDK sources -> javadoc-mode JSON)' + run: | + ARGS=(-j "$JAVA_HOME" -o "$JSON_OUT" -w "$WORK_DIR") + [ -n "$MODULES" ] && ARGS+=(-m "$MODULES") + # Passed as a real Gradle -P property by the script, not through the + # environment: the worker is not the Gradle daemon, so GRADLE_OPTS and + # org.gradle.jvmargs do not size it, and an environment-based override + # fails silently rather than loudly. + [ -n "$DOKKA_WORKER_HEAP" ] && ARGS+=(--dokka-worker-heap "$DOKKA_WORKER_HEAP") + Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh "${ARGS[@]}" + + - name: 'Step 2/3: flatten_templates.py (compose the single database template)' + run: python3 scripts/sync_java_docs/flatten_templates.py + + - name: 'Step 3/3: sync_javadoc_json_to_db.py (replace j/html/api content)' + run: | + ARGS=("$JSON_OUT" --db documentation.db --plain-brotli) + [ "$DELETE_MISSING" = "true" ] && ARGS+=(--delete-missing) + # --plain-brotli, not the shared dictionary: a row written with the + # brotli CLI's -D needs the reader to attach the same dictionary the + # same way, and it does not, so dictionary-compressed rows fail to + # decode in the app. See scripts/sync_java_docs/README.md. + python3 scripts/sync_java_docs/sync_javadoc_json_to_db.py "${ARGS[@]}" + + - name: Parity verification against the reference javadoc + if: ${{ inputs.verify_parity }} + run: | + # The Java analogue of build-kotlin-docs.yaml's blacklist check: prove + # the generated tree still covers every module, package and type the + # real javadoc output has. Member-level differences are understood and + # documented (README section 11), so only the structural levels gate + # the build. + python3 Dokka-plugin-kdoc2json/scripts/java/compare_with_javadoc.py \ + "$JSON_OUT" "$REFERENCE_API" + + - name: Summary + run: | + python3 - documentation.db <<'PYEOF' + import sqlite3 + import sys + + conn = sqlite3.connect(sys.argv[1]) + + def count(where, params=()): + return conn.execute(f"SELECT count(*) FROM Content WHERE {where}", params).fetchone()[0] + + templated = count("path LIKE ? AND templateId != 0", ("j/html/api/%",)) + print(f"Database: {sys.argv[1]}") + print(f" j/html/api/* rows: {count('path LIKE ?', ('j/html/api/%',))}") + print(f" of those, JSON + template rows: {templated}") + print(f" still raw HTML rows: {count('path LIKE ? AND templateId = 0', ('j/html/api/%',))}") + print(f" module pages : {count('path LIKE ?', ('j/html/api/%/module-summary.html',))}") + print(f" package pages : {count('path LIKE ?', ('j/html/api/%/package-summary.html',))}") + stored = conn.execute( + "SELECT SUM(length(content)) FROM Content WHERE path LIKE ? AND templateId != 0", + ("j/html/api/%",)).fetchone()[0] or 0 + print(f" stored bytes of the JSON rows : {stored:,}") + if templated == 0: + print("FAIL: no Java rows are pointing at a template - the sync did nothing.") + sys.exit(1) + conn.close() + PYEOF + + - name: Verify the stored rows decode and render + run: | + # The failure mode this guards against is silent: a row that is + # written but cannot be decoded by a reader without the shared + # dictionary looks fine in the database and blank in the app. + python3 - documentation.db <<'PYEOF' + import sqlite3, subprocess, sys + conn = sqlite3.connect(sys.argv[1]) + rows = conn.execute( + "SELECT path, content FROM Content WHERE path LIKE 'j/html/api/%' AND templateId != 0" + ).fetchall() + bad = [p for p, b in rows + if subprocess.run(["brotli", "-d", "-c"], input=b, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0] + print(f"{len(rows)} row(s) checked with a stock (no-dictionary) Brotli decode; {len(bad)} failed") + for p in bad[:10]: + print(f" {p}") + sys.exit(1 if bad else 0) + PYEOF + sqlite3 documentation.db "PRAGMA quick_check;" | head -1 + + - name: Write built database to output_dir + run: | + cp documentation.db "$OUTPUT_DIR/documentation-db-${{ github.run_number }}.db" + echo "Wrote $OUTPUT_DIR/documentation-db-${{ github.run_number }}.db" + + - name: Write updated database back to db_path + if: ${{ !inputs.dry_run }} + run: | + cp documentation.db "$DB_PATH" + echo "Wrote updated documentation.db back to $DB_PATH" + + - name: 'Notify Slack: build complete' + if: ${{ !inputs.dry_run }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + run: | + if [ -z "$SLACK_WEBHOOK_URL" ]; then + echo "SLACK_WEBHOOK_URL not set - skipping Slack notification" >&2 + else + curl -sS -X POST -H 'Content-type: application/json' \ + --data '{"text": "Updated Java documentation. Dropping baton"}' \ + "$SLACK_WEBHOOK_URL" || echo "warning: Slack notification failed" >&2 + fi diff --git a/.github/workflows/build-java-docs.yaml b/.github/workflows/build-java-docs.yaml new file mode 100644 index 00000000..caf05426 --- /dev/null +++ b/.github/workflows/build-java-docs.yaml @@ -0,0 +1,346 @@ +name: Build Java Docs + +# CI counterpart of the Java pipeline in Dokka-plugin-kdoc2json/scripts/java and +# scripts/sync_java_docs - same three steps (build-jdk-json-docs -> +# flatten_templates -> sync_javadoc_json_to_db), but sourcing the JDK it +# documents from the runner's own toolchain instead of a developer's machine, +# and reading/writing the real database on Google Drive (GOOGLE_DRIVE_FILE_ID) +# instead of a local copy. The Kotlin equivalent is build-kotlin-docs.yaml; +# this file deliberately mirrors its shape. +# +# What it does: unpacks the JDK's lib/src.zip, keeps only the packages javadoc +# documents (those a module `exports` unqualified), runs Dokka with +# kdoc-to-json in javadoc-mode over them, composes the single Pebble template +# the database serves those pages from, and replaces the j/html/api/ rows with +# the resulting JSON. +# +# NOTE ON MEMORY: Dokka generates in a *worker process*, which +# org.gradle.jvmargs does not size - jdk-docs/build.gradle.kts sizes it via +# dokkaGeneratorIsolation, defaulting to a 24g maximum. That is a ceiling, not +# a reservation, so it is fine on a smaller runner as long as the build's real +# usage fits in RAM; documenting the whole JDK is ~4,800 source files in one +# analysis pass. The default is therefore left alone here. If a run does die +# with an OutOfMemoryError, set dokka_worker_heap below, which is forwarded to +# Gradle as -PdokkaWorkerHeap - and check the "Dokka worker heap" line the +# build script logs to confirm it was actually applied, since nothing else +# reports it. +# +# NOTE ON JDK VERSION: java_version picks both the JDK whose sources are +# documented AND the JDK the Gradle builds run on. It must be 21 or lower: +# kdoc-to-json is pinned to Kotlin 1.9.24, whose compiler cannot run on a newer +# JDK at all (it fails parsing the version string). It must also be a JDK, not +# a JRE, since the sources come from its lib/src.zip. +# +# Required secrets (already configured - see docdb-regression-test.yaml for +# their other use in this repo): +# GCP_WIF_PROVIDER - Workload Identity Federation provider name +# GCP_WIF_SERVICE_ACCOUNT - Service account email for WIF (needs write +# access - not just view - to the database file, +# since this workflow overwrites it) +# GOOGLE_DRIVE_FILE_ID - File ID of the production documentation.db +# (stored on Drive as a zip) +# +# Optional secret (Slack notifications are skipped with a warning if unset): +# SLACK_WEBHOOK_URL - Incoming Webhook URL for the "Notify Slack" steps +# below ("Grabbing baton" on start, "...Dropping +# baton" on finish - org shorthand for lock +# acquire/release, since this workflow mutates a +# single shared Drive file). + +permissions: + contents: read + id-token: write + +# This workflow overwrites a single shared Drive file - never let two runs +# race to upload against each other. +concurrency: + group: build-java-docs + cancel-in-progress: false + +on: + workflow_dispatch: + inputs: + java_version: + description: >- + JDK whose lib/src.zip is documented, and which the Gradle builds run + on. Must be 21 or lower (kdoc-to-json is pinned to Kotlin 1.9.24, + which cannot run on a newer JDK). Keep this in step with the + reference docs in SourceDocs/JavaDocs, which are Java SE 17. + required: false + default: '17' + modules: + description: >- + Comma-separated JPMS module names to document instead of all of + them, e.g. "java.sql,java.transaction.xa". Leave empty for the whole + JDK. A subset run takes seconds rather than minutes and is the quick + way to smoke-test a change; it will also make the parity check below + fail, so pair it with verify_parity=false. + required: false + default: '' + dokka_worker_heap: + description: >- + Max heap for Dokka's worker process, e.g. 6g (see NOTE ON MEMORY + above). Leave empty to use the build's own default; set it only if a + run dies with an OutOfMemoryError. + required: false + default: '' + verify_parity: + description: >- + Compare the generated tree against the reference javadoc HTML in + SourceDocs/JavaDocs/html/api and fail on any missing or extra + module/package/type. Only meaningful for a full java_version=17 run; + turn it off for a subset or a different JDK. + required: false + default: true + type: boolean + delete_missing: + description: >- + Also delete j/html/api/ rows that have no JSON counterpart - + class-use/, package-use, the tree pages, serialized-form. That is + about half the rows. They are working documentation nothing in the + new pages links to, so the default is to leave them alone. + required: false + default: false + type: boolean + dry_run: + description: >- + If true, build and verify everything but do NOT upload the result + back to Google Drive - the production database is left untouched. + Set to false only once you trust a given version/module combination. + required: false + default: true + type: boolean + +jobs: + build-java-docs: + runs-on: ubuntu-latest + timeout-minutes: 120 + env: + DB_FILE_ID_SECRET: ${{ secrets.GOOGLE_DRIVE_FILE_ID }} + # --- Hard-coded override for one-off manual testing ------------------ + # Fill in with a literal Google Drive file ID to bypass the secret + # resolution above for a quick, repeatable test run (e.g. against a + # scratch copy of the database on Drive). Leave empty ('') for normal + # operation. + TEST_DB_FILE_ID: '' + JAVA_VERSION: ${{ inputs.java_version }} + MODULES: ${{ inputs.modules }} + DOKKA_WORKER_HEAP: ${{ inputs.dokka_worker_heap }} + DELETE_MISSING: ${{ inputs.delete_missing }} + # Where build-jdk-json-docs.sh writes the JSON tree and its scratch + # staging copy of the JDK sources. + JSON_OUT: ${{ github.workspace }}/java-json-build/api + WORK_DIR: ${{ github.workspace }}/java-json-build/work + REFERENCE_API: ${{ github.workspace }}/SourceDocs/JavaDocs/html/api + steps: + - name: Checkout OfflineDocumentationTools + uses: actions/checkout@v4 + + - name: Resolve Google Drive file ID + run: | + DB_FILE_ID="${TEST_DB_FILE_ID:-$DB_FILE_ID_SECRET}" + if [ -z "$DB_FILE_ID" ]; then + echo "Error: no database file ID resolved - set the GOOGLE_DRIVE_FILE_ID secret, or TEST_DB_FILE_ID above for a test run" >&2 + exit 1 + fi + echo "Resolved DB_FILE_ID: ${DB_FILE_ID:+(set)}" + echo "DB_FILE_ID=$DB_FILE_ID" >> "$GITHUB_ENV" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Set up JDK (documented sources + the kdoc-to-json / jdk-docs Gradle builds) + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ inputs.java_version }} + + - name: Verify the JDK ships sources + run: | + # The whole pipeline reads lib/src.zip; a JRE or a stripped JDK has + # none, and the failure would otherwise surface as an empty staging + # tree several minutes later. + if [ ! -f "$JAVA_HOME/lib/src.zip" ]; then + echo "Error: $JAVA_HOME/lib/src.zip not found - java_version must name a JDK that ships sources, not a JRE" >&2 + exit 1 + fi + echo "Documenting sources from $JAVA_HOME/lib/src.zip" + "$JAVA_HOME/bin/java" -version + + - name: Install system dependencies + run: | + sudo apt-get update -y + # brotli: the CLI, not the Python package. sync_javadoc_json_to_db.py + # shells out to it because no Python binding exposes a custom + # dictionary (ADFA-5153), and it is also what writes the plain-Brotli + # rows this pipeline stores. + sudo apt-get install -y unzip zip sqlite3 brotli + + - name: Install Python dependencies + run: | + pip install -r requirements.txt + # google-api-python-client & friends: Drive download/upload, same + # libraries check-tools/download_database.py already depends on. + pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib + + - name: Authenticate to Google Cloud using Workload Identity Federation + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }} + service_account: ${{ secrets.GCP_WIF_SERVICE_ACCOUNT }} + access_token_scopes: | + https://www.googleapis.com/auth/drive.file + + - name: Download current documentation.db from Google Drive + run: | + python3 check-tools/download_database.py "$DB_FILE_ID" documentation.zip + unzip -o documentation.zip + if [ ! -f documentation.db ]; then + found="$(find . -maxdepth 2 -name documentation.db | head -n1)" + [ -n "$found" ] && mv "$found" documentation.db + fi + test -f documentation.db + sqlite3 documentation.db "SELECT 1;" > /dev/null + rm -f documentation.zip + echo "DB_SIZE=$(stat -c%s documentation.db 2>/dev/null || stat -f%z documentation.db)" >> "$GITHUB_ENV" + + - name: 'Notify Slack: build started' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + run: | + if [ -z "$SLACK_WEBHOOK_URL" ]; then + echo "SLACK_WEBHOOK_URL not set - skipping Slack notification" >&2 + else + curl -sS -X POST -H 'Content-type: application/json' \ + --data '{"text": "Grabbing baton"}' \ + "$SLACK_WEBHOOK_URL" || echo "warning: Slack notification failed" >&2 + fi + + - name: 'Step 1/3: build-jdk-json-docs.sh (stage JDK sources -> javadoc-mode JSON)' + run: | + ARGS=(-j "$JAVA_HOME" -o "$JSON_OUT" -w "$WORK_DIR") + [ -n "$MODULES" ] && ARGS+=(-m "$MODULES") + # Passed as a real Gradle -P property by the script, not through the + # environment: the worker is not the Gradle daemon, so GRADLE_OPTS and + # org.gradle.jvmargs do not size it, and an environment-based override + # fails silently rather than loudly. + [ -n "$DOKKA_WORKER_HEAP" ] && ARGS+=(--dokka-worker-heap "$DOKKA_WORKER_HEAP") + Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh "${ARGS[@]}" + + - name: 'Step 2/3: flatten_templates.py (compose the single database template)' + run: python3 scripts/sync_java_docs/flatten_templates.py + + - name: 'Step 3/3: sync_javadoc_json_to_db.py (replace j/html/api content)' + run: | + ARGS=("$JSON_OUT" --db documentation.db --plain-brotli) + [ "$DELETE_MISSING" = "true" ] && ARGS+=(--delete-missing) + # --plain-brotli, not the shared dictionary: a row written with the + # brotli CLI's -D needs the reader to attach the same dictionary the + # same way, and it does not, so dictionary-compressed rows fail to + # decode in the app. See scripts/sync_java_docs/README.md. + python3 scripts/sync_java_docs/sync_javadoc_json_to_db.py "${ARGS[@]}" + + - name: Parity verification against the reference javadoc + if: ${{ inputs.verify_parity }} + run: | + # The Java analogue of build-kotlin-docs.yaml's blacklist check: prove + # the generated tree still covers every module, package and type the + # real javadoc output has. Member-level differences are understood and + # documented (README section 11), so only the structural levels gate + # the build. + python3 Dokka-plugin-kdoc2json/scripts/java/compare_with_javadoc.py \ + "$JSON_OUT" "$REFERENCE_API" + + - name: Summary + run: | + python3 - documentation.db <<'PYEOF' + import sqlite3 + import sys + + conn = sqlite3.connect(sys.argv[1]) + + def count(where, params=()): + return conn.execute(f"SELECT count(*) FROM Content WHERE {where}", params).fetchone()[0] + + templated = count("path LIKE ? AND templateId != 0", ("j/html/api/%",)) + print(f"Database: {sys.argv[1]}") + print(f" j/html/api/* rows: {count('path LIKE ?', ('j/html/api/%',))}") + print(f" of those, JSON + template rows: {templated}") + print(f" still raw HTML rows: {count('path LIKE ? AND templateId = 0', ('j/html/api/%',))}") + print(f" module pages : {count('path LIKE ?', ('j/html/api/%/module-summary.html',))}") + print(f" package pages : {count('path LIKE ?', ('j/html/api/%/package-summary.html',))}") + stored = conn.execute( + "SELECT SUM(length(content)) FROM Content WHERE path LIKE ? AND templateId != 0", + ("j/html/api/%",)).fetchone()[0] or 0 + print(f" stored bytes of the JSON rows : {stored:,}") + if templated == 0: + print("FAIL: no Java rows are pointing at a template - the sync did nothing.") + sys.exit(1) + conn.close() + PYEOF + + - name: Verify the stored rows decode and render + run: | + # The failure mode this guards against is silent: a row that is + # written but cannot be decoded by a reader without the shared + # dictionary looks fine in the database and blank in the app. + python3 - documentation.db <<'PYEOF' + import sqlite3, subprocess, sys + conn = sqlite3.connect(sys.argv[1]) + rows = conn.execute( + "SELECT path, content FROM Content WHERE path LIKE 'j/html/api/%' AND templateId != 0" + ).fetchall() + bad = [p for p, b in rows + if subprocess.run(["brotli", "-d", "-c"], input=b, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0] + print(f"{len(rows)} row(s) checked with a stock (no-dictionary) Brotli decode; {len(bad)} failed") + for p in bad[:10]: + print(f" {p}") + sys.exit(1 if bad else 0) + PYEOF + sqlite3 documentation.db "PRAGMA quick_check;" | head -1 + + - name: Upload built database as workflow artifact + uses: actions/upload-artifact@v4 + with: + name: documentation-db-${{ github.run_number }} + path: documentation.db + retention-days: 14 + + - name: Zip updated database for upload + if: ${{ !inputs.dry_run }} + run: zip -j documentation.zip documentation.db + + - name: Upload updated database to Google Drive + if: ${{ !inputs.dry_run }} + run: | + python3 - <<'PYEOF' + import os + from google.auth import default + from googleapiclient.discovery import build + from googleapiclient.http import MediaFileUpload + + file_id = os.environ["DB_FILE_ID"] + credentials, _ = default() + service = build("drive", "v3", credentials=credentials) + media = MediaFileUpload("documentation.zip", mimetype="application/zip", resumable=True) + updated = service.files().update( + fileId=file_id, media_body=media, fields="id, modifiedTime, md5Checksum" + ).execute() + print(f"Uploaded new revision of {file_id}: {updated}") + PYEOF + + - name: 'Notify Slack: build complete' + if: ${{ !inputs.dry_run }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + run: | + if [ -z "$SLACK_WEBHOOK_URL" ]; then + echo "SLACK_WEBHOOK_URL not set - skipping Slack notification" >&2 + else + curl -sS -X POST -H 'Content-type: application/json' \ + --data '{"text": "Updated Java documentation. Dropping baton"}' \ + "$SLACK_WEBHOOK_URL" || echo "warning: Slack notification failed" >&2 + fi diff --git a/Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh b/Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh index 9cfaab09..57a48b6c 100755 --- a/Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh +++ b/Dokka-plugin-kdoc2json/scripts/java/build-jdk-json-docs.sh @@ -20,7 +20,8 @@ PROJECT_DIR="$SCRIPT_DIR/jdk-docs" usage() { cat >&2 <] [-o ] [-w ] [-m ] [--skip-publish] +Usage: $0 [-j ] [-o ] [-w ] [-m ] + [--dokka-worker-heap ] [--skip-publish] -j JDK whose lib/src.zip to document. Defaults to \$JDK_SOURCE_HOME, else \$JAVA_HOME. Use a JDK matching the docs you want to reproduce -- the docs under @@ -30,6 +31,14 @@ Usage: $0 [-j ] [-o ] [-w ] [-m ] [--sk Default: $SCRIPT_DIR/build-output/work -m Comma-separated module names to document instead of all of them. Useful for a quick check: -m java.sql,java.transaction.xa takes seconds rather than many minutes. + --dokka-worker-heap + Max heap for Dokka's *worker* process, e.g. 6g. Forwarded to Gradle as + -PdokkaWorkerHeap, which jdk-docs/build.gradle.kts reads. It has to be a + real Gradle command-line property: the worker is not the Gradle daemon, + so neither GRADLE_OPTS nor org.gradle.jvmargs sizes it, and passing it + through the environment instead fails silently -- the build just runs at + the 24g default and dies wherever that is too much. Default: unset, so + build.gradle.kts's own default applies. --skip-publish Don't republish kdoc-to-json to mavenLocal first. USAGE exit 1 @@ -39,6 +48,7 @@ JDK_HOME="${JDK_SOURCE_HOME:-${JAVA_HOME:-}}" OUTPUT_DIR="$SCRIPT_DIR/build-output/api" WORK_DIR="$SCRIPT_DIR/build-output/work" MODULES="" +DOKKA_WORKER_HEAP="" SKIP_PUBLISH=0 while [[ $# -gt 0 ]]; do @@ -47,6 +57,7 @@ while [[ $# -gt 0 ]]; do -o) OUTPUT_DIR="$2"; shift 2 ;; -w) WORK_DIR="$2"; shift 2 ;; -m) MODULES="$2"; shift 2 ;; + --dokka-worker-heap) DOKKA_WORKER_HEAP="$2"; shift 2 ;; --skip-publish) SKIP_PUBLISH=1; shift ;; -h|--help) usage ;; *) echo "Unknown argument: $1" >&2; usage ;; @@ -84,7 +95,12 @@ fi echo "==> Running Dokka in javadoc-mode over the staged sources" echo " (the whole JDK is ~4,800 files across 60 modules; this takes a while)" -(cd "$PROJECT_DIR" && ./gradlew --console=plain dokkaGenerate -PjdkSources="$STAGING_DIR") +gradle_args=(--console=plain dokkaGenerate -PjdkSources="$STAGING_DIR") +if [[ -n "$DOKKA_WORKER_HEAP" ]]; then + gradle_args+=(-PdokkaWorkerHeap="$DOKKA_WORKER_HEAP") + echo " Dokka worker heap: $DOKKA_WORKER_HEAP" +fi +(cd "$PROJECT_DIR" && ./gradlew "${gradle_args[@]}") GENERATED="$PROJECT_DIR/build/dokka/html" if [[ ! -d "$GENERATED" ]]; then From ce8159493072794f4ba4c0500e3c00354cdf8b3f Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Thu, 3 Sep 2026 15:24:43 -0500 Subject: [PATCH 12/14] ADFA-5296: Add run-build-java-docs-with-act.sh, the local driver for the Java workflow Mirrors run-build-kotlin-docs-with-act.sh: bind-mounts the host paths into the job container at fixed locations, passes every workflow input explicitly, and runs build-java-docs-local.yaml through act. It drives the *local* workflow, not the Drive one. WIF validates the OIDC token's issuer against GitHub's own endpoint for a specific repo and run, so act cannot mint a token GCP will accept and build-java-docs.yaml can never get past its auth step locally, whatever secrets are supplied. Passing every input explicitly is the point rather than an accident: act does not apply workflow_dispatch defaults, and for dry_run that inverts the intended behaviour -- "${{ !inputs.dry_run }}" on an empty value is true, so the step that writes the database back over --db-path would run on a plain invocation. Two things differ from the Kotlin script, both because the Java pipeline is different rather than by choice. There is no --images-zip-path, since that is a Writerside input with no Java equivalent, so only the database and the output directory are mounted. And --modules is rejected unless --no-verify-parity comes with it: a subset run cannot match the full reference docs in SourceDocs/JavaDocs, so the parity check would fail by construction. The header also notes container memory, which is the likely first failure under act: documenting the whole JDK analyses ~4,800 files in one pass inside the container, and Docker Desktop and colima both default their VM to a few GB. Verified with act's dry-run: both workflows parse and every step resolves, the eight inputs the script passes are exactly the eight the workflow declares, and the dry_run gating works -- "Write updated database back to db_path" and "Notify Slack: build complete" appear only with dry_run=false. Argument validation was exercised for each error path. Neither workflow has been run for real: the Drive one needs the live secrets, and a full local run needs a container VM sized for the JDK analysis. Co-Authored-By: Claude Opus 5 --- run-build-java-docs-with-act.sh | 215 ++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100755 run-build-java-docs-with-act.sh diff --git a/run-build-java-docs-with-act.sh b/run-build-java-docs-with-act.sh new file mode 100755 index 00000000..697b159d --- /dev/null +++ b/run-build-java-docs-with-act.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# Runs the "Build Java Docs (Local)" GitHub Actions workflow +# (.github/workflows/build-java-docs-local.yaml) locally via act +# (https://github.com/nektos/act). +# +# This drives the *local* workflow, not build-java-docs.yaml. The Drive +# workflow authenticates to Google Cloud with Workload Identity Federation, +# and WIF validates the OIDC token's issuer against GitHub's own token +# endpoint for a specific repo and run - act cannot mint a token GCP will +# accept, so that workflow can never get past its auth step locally no matter +# what secrets you supply. build-java-docs-local.yaml exists precisely to be +# runnable here: it reads its documentation.db from disk and writes its outputs +# back to disk, and is otherwise step-for-step identical to the Drive workflow +# (same build-jdk-json-docs -> flatten_templates -> sync_javadoc_json_to_db, +# same parity and decode verification). +# +# The Kotlin counterpart is run-build-kotlin-docs-with-act.sh; this mirrors it. +# +# Requires: +# - act (https://github.com/nektos/act#installation) on PATH +# - a running Docker daemon (act executes each step inside a container) +# +# CONTAINER MEMORY: documenting the whole JDK analyses ~4,800 source files in +# one pass, inside the job container. Docker Desktop and colima both default +# their VM to a few GB, which may not be enough - if a run dies with an +# OutOfMemoryError, give the VM more memory, use --modules to document a +# subset, or set --dokka-worker-heap. That last one is forwarded to Gradle as +# -PdokkaWorkerHeap and the build logs a "Dokka worker heap" line when it is +# applied, which is the way to confirm it took effect. +# +# Secrets: none are required. SLACK_WEBHOOK_URL is the only secret this +# workflow reads, and it is optional - the two "Notify Slack" steps print a +# skip notice and continue when it is unset. Export it if you want to see them +# actually fire ("build complete" additionally needs --live, since it is gated +# on dry_run being false). GitHub never exposes a stored secret's value through +# any API or CLI, so if you do want the real webhook you have to supply your +# own copy of the value. +# +# Inputs are host paths, bind-mounted into the job container at fixed +# locations and passed to the workflow as those in-container paths (a +# GitHub-hosted runner has no access to your disk, so the workflow only ever +# sees the mounted paths). Note this means the host paths must live somewhere +# your container runtime is allowed to share - under $HOME is safe for both +# colima and Docker Desktop; /tmp on macOS often is not. +# +# Usage: +# ./run-build-java-docs-with-act.sh --db-path PATH [options] [-- ] +# +# Options: +# --db-path PATH Host path to the input documentation.db (required). +# With --live this file is overwritten in place. +# --output-dir PATH Host directory for outputs - a run-numbered copy +# of the built database. Created if absent. +# (default: ./build-java-docs-output) +# --live dry_run=false: write the rebuilt database back +# over --db-path when the run finishes. Also +# required for the "build complete" Slack +# notification to fire. Default is dry_run=true. +# --java-version V JDK whose lib/src.zip is documented, and which +# the Gradle builds run on (default: 17). Must be +# 21 or lower: kdoc-to-json is pinned to Kotlin +# 1.9.24, whose compiler cannot run on a newer JDK. +# --modules LIST Comma-separated JPMS modules to document instead +# of all of them, e.g. "java.sql,java.xml". Turns a +# multi-minute run into seconds, which is the way +# to smoke-test a change. Requires +# --no-verify-parity, since a subset cannot match +# the full reference docs. +# --dokka-worker-heap SIZE Max heap for Dokka's worker process, e.g. 6g. +# Only needed if a run OOMs (see CONTAINER MEMORY). +# --no-verify-parity verify_parity=false: skip the comparison against +# the reference javadoc in SourceDocs/JavaDocs. +# --delete-missing delete_missing=true: also delete j/html/api/ rows +# with no JSON counterpart (class-use/, +# package-use, the tree pages). About half the +# rows. Default is to leave them alone. +# +# Every workflow input is passed explicitly on every run, including the ones +# whose YAML "default:" would cover them. act does not apply +# workflow_dispatch input defaults - an input you don't pass arrives empty - +# and for dry_run that inverts the intended behaviour: "${{ !inputs.dry_run }}" +# on an empty value is true, so the step that writes the database back over +# --db-path would run. Passing all of them keeps a local run's semantics +# identical to a real dispatch. +# +# On Apple Silicon act warns about container architecture; append +# `-- --container-architecture linux/arm64` if you want to silence it (the +# default works). +set -euo pipefail + +if ! command -v act >/dev/null 2>&1; then + echo "error: act is required - see https://github.com/nektos/act#installation" >&2 + exit 1 +fi + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKFLOW="$REPO_ROOT/.github/workflows/build-java-docs-local.yaml" + +# Where the host paths below get bind-mounted inside the job container, and +# therefore what the workflow itself is told its inputs are. +CONTAINER_DB_PATH="/mnt/act-inputs/documentation.db" +CONTAINER_OUTPUT_DIR="/mnt/act-output" + +DB_PATH="" +OUTPUT_DIR="$REPO_ROOT/build-java-docs-output" +# Mirrors build-java-docs-local.yaml's own default. Restated here because act +# does not apply workflow_dispatch defaults (see the note above); passing the +# input unconditionally is what keeps a local run equivalent to a real one. +JAVA_VERSION="17" +MODULES="" +DOKKA_WORKER_HEAP="" +VERIFY_PARITY="true" +DELETE_MISSING="false" +DRY_RUN="true" + +EXTRA_ACT_ARGS=() + +while [ $# -gt 0 ]; do + case "$1" in + --db-path) DB_PATH="$2"; shift 2 ;; + --output-dir) OUTPUT_DIR="$2"; shift 2 ;; + --live) DRY_RUN="false"; shift ;; + --java-version) JAVA_VERSION="$2"; shift 2 ;; + --modules) MODULES="$2"; shift 2 ;; + --dokka-worker-heap) DOKKA_WORKER_HEAP="$2"; shift 2 ;; + --no-verify-parity) VERIFY_PARITY="false"; shift ;; + --delete-missing) DELETE_MISSING="true"; shift ;; + --) shift; EXTRA_ACT_ARGS+=("$@"); break ;; + *) echo "error: unrecognized argument '$1'" >&2; exit 1 ;; + esac +done + +if [ -z "$DB_PATH" ]; then + echo "error: --db-path is required (host path to the documentation.db to build against)" >&2 + exit 1 +fi +if [ ! -f "$DB_PATH" ]; then + echo "error: --db-path '$DB_PATH' does not exist or is not a file" >&2 + exit 1 +fi +DB_PATH="$(cd "$(dirname "$DB_PATH")" && pwd)/$(basename "$DB_PATH")" + +if [ -n "$MODULES" ] && [ "$VERIFY_PARITY" = "true" ]; then + echo "error: --modules documents only part of the JDK, so the parity check against the" >&2 + echo "error: full reference docs in SourceDocs/JavaDocs is guaranteed to fail. Pass" >&2 + echo "error: --no-verify-parity alongside it." >&2 + exit 1 +fi + +mkdir -p "$OUTPUT_DIR" +OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)" + +# One -v per input. Mounting the database individually (rather than its parent +# directory) keeps the container's view to exactly what the run needs, and lets +# --db-path and --output-dir live in unrelated places on the host. +# +# act takes --container-options as one string and splits it with shell-style +# quoting rules, so each mount spec is emitted double-quoted: an unquoted join +# would break the moment a host path contained a space. +CONTAINER_OPTIONS="" +add_mount() { CONTAINER_OPTIONS+=" -v \"$1:$2\""; } +add_mount "$DB_PATH" "$CONTAINER_DB_PATH" +add_mount "$OUTPUT_DIR" "$CONTAINER_OUTPUT_DIR" + +if [ "$DRY_RUN" = "true" ]; then + echo "note: dry_run=true - '$DB_PATH' will NOT be modified; the built database is" >&2 + echo "note: written to '$OUTPUT_DIR' only. The 'build started' Slack notification" >&2 + echo "note: still fires (if SLACK_WEBHOOK_URL is set) but 'build complete' is gated" >&2 + echo "note: on dry_run=false. Pass --live to write back and see it." >&2 +else + echo "WARNING: --live - '$DB_PATH' will be OVERWRITTEN in place when the run finishes." >&2 +fi + +# The workflow reads SLACK_WEBHOOK_URL and tolerates it being unset, so pass +# it through when it's in the environment and stay silent when it isn't. +# +# SECRET_ARGS and EXTRA_ACT_ARGS are expanded below as +# ${arr[@]+"${arr[@]}"} rather than plain "${arr[@]}": macOS still ships bash +# 3.2, where `set -u` treats an empty array's "${arr[@]}" as an unbound +# variable and aborts. Both arrays are empty on a normal run. +SECRET_ARGS=() +if [ -n "${SLACK_WEBHOOK_URL:-}" ]; then + SECRETS_FILE="$(mktemp)" + trap 'rm -f "$SECRETS_FILE"' EXIT + printf 'SLACK_WEBHOOK_URL=%s\n' "$SLACK_WEBHOOK_URL" > "$SECRETS_FILE" + SECRET_ARGS=(--secret-file "$SECRETS_FILE") +fi + +echo "== Running $WORKFLOW via act ==" +echo " db_path $DB_PATH -> $CONTAINER_DB_PATH" +echo " output_dir $OUTPUT_DIR -> $CONTAINER_OUTPUT_DIR" +echo " java_version=$JAVA_VERSION modules=${MODULES:-(all)} dry_run=$DRY_RUN" +echo " verify_parity=$VERIFY_PARITY delete_missing=$DELETE_MISSING" +echo " dokka_worker_heap=${DOKKA_WORKER_HEAP:-(build default)}" + +# --container-daemon-socket - : act otherwise bind-mounts the host's Docker +# socket into the job container so steps can run Docker themselves. Nothing in +# this workflow does, and the mount outright fails on runtimes whose socket +# isn't a plain bind-mountable file - under colima it aborts the run with +# "error while creating mount source path ...: operation not supported". +act workflow_dispatch \ + -W "$WORKFLOW" \ + -P ubuntu-latest=catthehacker/ubuntu:act-latest \ + --container-daemon-socket - \ + --container-options "$CONTAINER_OPTIONS" \ + --input db_path="$CONTAINER_DB_PATH" \ + --input output_dir="$CONTAINER_OUTPUT_DIR" \ + --input java_version="$JAVA_VERSION" \ + --input modules="$MODULES" \ + --input dokka_worker_heap="$DOKKA_WORKER_HEAP" \ + --input verify_parity="$VERIFY_PARITY" \ + --input delete_missing="$DELETE_MISSING" \ + --input dry_run="$DRY_RUN" \ + ${SECRET_ARGS[@]+"${SECRET_ARGS[@]}"} \ + ${EXTRA_ACT_ARGS[@]+"${EXTRA_ACT_ARGS[@]}"} From eb1698076507ed849b33d2e98f260a0c4dc8e1e4 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Thu, 3 Sep 2026 16:10:47 -0500 Subject: [PATCH 13/14] ADFA-5296: Record what the local workflow run actually showed Ran build-java-docs-local.yaml through act. A two-module subset succeeds end to end: staging, Dokka, the template composition and the sync all run, the sync reports "updated 110" for the 110 pages it built, the decode check passes 4,988 rows with a stock no-dictionary Brotli decode, the built database is written to output_dir, and dry_run=true leaves the mounted input byte-identical. The full JDK does not fit a 7.7 GB container VM, and no heap setting makes it: (no flag, 24g ceiling) exit 137 after 1m17 kernel SIGKILL --dokka-worker-heap 6g exit 137 after 2m26 kernel SIGKILL --dokka-worker-heap 5g exit 137 after 2m33 kernel SIGKILL --dokka-worker-heap 4g "Java heap space" JVM hit its own cap Below ~5g the analysis genuinely needs more; at ~5g and up the process no longer fits alongside the Gradle daemon. The fix is a bigger VM (colima start --memory 12), not a smaller heap. Both numbers and that conclusion are now in the script header and in both workflow headers. This also settles --dokka-worker-heap, which I previously said I could not confirm: the two failure modes are the proof. Without the flag the kernel kills the process from outside; with 4g the JVM reports "Java heap space", which it can only do by having honoured a 4g ceiling. The build logs a "Dokka worker heap" line when the flag is applied, and that line appeared. My earlier attempts to verify this were badly designed - a successful build proves nothing when the default would also succeed. Still unrun: the Drive workflow, which needs the live secrets, and a full local run, which needs a larger container VM. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-java-docs-local.yaml | 5 +++- .github/workflows/build-java-docs.yaml | 5 +++- run-build-java-docs-with-act.sh | 26 +++++++++++++++----- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-java-docs-local.yaml b/.github/workflows/build-java-docs-local.yaml index 79bc95d4..9e9801d2 100644 --- a/.github/workflows/build-java-docs-local.yaml +++ b/.github/workflows/build-java-docs-local.yaml @@ -37,7 +37,10 @@ name: Build Java Docs (Local) # with an OutOfMemoryError, set dokka_worker_heap below, which is forwarded to # Gradle as -PdokkaWorkerHeap - and check the "Dokka worker heap" line the # build script logs to confirm it was actually applied, since nothing else -# reports it. +# reports it. "Java heap space" means the JVM hit the cap you set; exit 137 +# means the kernel killed it from outside, i.e. the machine is too small rather +# than the cap too low. On a 7.7 GB container VM the full JDK could not be made +# to fit at any setting - see run-build-java-docs-with-act.sh for the numbers. # # NOTE ON JDK VERSION: java_version picks both the JDK whose sources are # documented AND the JDK the Gradle builds run on. It must be 21 or lower: diff --git a/.github/workflows/build-java-docs.yaml b/.github/workflows/build-java-docs.yaml index caf05426..8f9da251 100644 --- a/.github/workflows/build-java-docs.yaml +++ b/.github/workflows/build-java-docs.yaml @@ -23,7 +23,10 @@ name: Build Java Docs # with an OutOfMemoryError, set dokka_worker_heap below, which is forwarded to # Gradle as -PdokkaWorkerHeap - and check the "Dokka worker heap" line the # build script logs to confirm it was actually applied, since nothing else -# reports it. +# reports it. "Java heap space" means the JVM hit the cap you set; exit 137 +# means the kernel killed it from outside, i.e. the machine is too small rather +# than the cap too low. On a 7.7 GB container VM the full JDK could not be made +# to fit at any setting - see run-build-java-docs-with-act.sh for the numbers. # # NOTE ON JDK VERSION: java_version picks both the JDK whose sources are # documented AND the JDK the Gradle builds run on. It must be 21 or lower: diff --git a/run-build-java-docs-with-act.sh b/run-build-java-docs-with-act.sh index 697b159d..60b48895 100755 --- a/run-build-java-docs-with-act.sh +++ b/run-build-java-docs-with-act.sh @@ -21,12 +21,26 @@ # - a running Docker daemon (act executes each step inside a container) # # CONTAINER MEMORY: documenting the whole JDK analyses ~4,800 source files in -# one pass, inside the job container. Docker Desktop and colima both default -# their VM to a few GB, which may not be enough - if a run dies with an -# OutOfMemoryError, give the VM more memory, use --modules to document a -# subset, or set --dokka-worker-heap. That last one is forwarded to Gradle as -# -PdokkaWorkerHeap and the build logs a "Dokka worker heap" line when it is -# applied, which is the way to confirm it took effect. +# one pass, inside the job container, and that does NOT fit a small container +# VM. Measured on a colima VM with 7.7 GB, --modules unset: +# +# (no flag, 24g ceiling) exit 137 after 1m17 - kernel SIGKILL: the JVM +# grows until the VM is out +# --dokka-worker-heap 6g exit 137 after 2m26 - same +# --dokka-worker-heap 5g exit 137 after 2m33 - same +# --dokka-worker-heap 4g "Java heap space" - the JVM hit its own cap; +# 4g is too little for the job +# +# So there is no heap setting that works at 7.7 GB: below ~5g the analysis +# genuinely needs more, and at ~5g and up the process no longer fits alongside +# the Gradle daemon. Raise the container VM instead (colima start --memory 12, +# or Docker Desktop's Resources pane) - or use --modules to document a subset, +# which runs in about a minute and is enough to exercise the whole pipeline. +# +# Those two failure modes are also how to tell whether --dokka-worker-heap took +# effect at all: "Java heap space" means the JVM hit the cap you set, exit 137 +# means it was killed from outside. The build additionally logs a "Dokka worker +# heap" line whenever the flag is applied. # # Secrets: none are required. SLACK_WEBHOOK_URL is the only secret this # workflow reads, and it is optional - the two "Notify Slack" steps print a From b0bd3a295cb4f08a6e0a444964a0a81f40f48abf Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Thu, 3 Sep 2026 16:54:02 -0500 Subject: [PATCH 14/14] ADFA-5296: Record the working configuration for a full local run The full JDK does run under act after all - it just needs a container VM with room. On a 15.6 GB colima VM with --dokka-worker-heap 8g, build-java-docs-local completes every step in about four minutes: 2m30 generating, 1m15 syncing. parity verification 60/60 modules, 224/224 packages, 4,672/4,672 types, MATCH sync updated 4,988, added 0, deleted 0, 1 passed through decode check 4,988 rows, 0 failures with a stock decoder dry_run=true mounted input byte-identical afterwards The database it produced matches the one built by hand earlier: same 4,988 rows, same single javadoc.peb template, 11,666,337 bytes against 11,666,362. The 25-byte difference is the JDK, not the pipeline - the container's Temurin 17 stages 4,842 source files where the local Homebrew 17.0.20.1 stages 4,845. Headers now carry the working configuration alongside the earlier failure table, and note that a GitHub-hosted runner is not a container in this sense and has its own memory, so those figures are a floor rather than a prediction. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-java-docs-local.yaml | 7 +++++-- .github/workflows/build-java-docs.yaml | 7 +++++-- run-build-java-docs-with-act.sh | 15 ++++++++++----- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-java-docs-local.yaml b/.github/workflows/build-java-docs-local.yaml index 9e9801d2..6b84b863 100644 --- a/.github/workflows/build-java-docs-local.yaml +++ b/.github/workflows/build-java-docs-local.yaml @@ -39,8 +39,11 @@ name: Build Java Docs (Local) # build script logs to confirm it was actually applied, since nothing else # reports it. "Java heap space" means the JVM hit the cap you set; exit 137 # means the kernel killed it from outside, i.e. the machine is too small rather -# than the cap too low. On a 7.7 GB container VM the full JDK could not be made -# to fit at any setting - see run-build-java-docs-with-act.sh for the numbers. +# than the cap too low. For reference, the full JDK could not be made to fit a +# 7.7 GB container VM at any setting, and runs in ~4 minutes on a 15.6 GB one +# with dokka_worker_heap=8g - see run-build-java-docs-with-act.sh for the +# numbers. A GitHub-hosted runner is not a container in this sense and has its +# own memory; treat those figures as a floor, not a prediction. # # NOTE ON JDK VERSION: java_version picks both the JDK whose sources are # documented AND the JDK the Gradle builds run on. It must be 21 or lower: diff --git a/.github/workflows/build-java-docs.yaml b/.github/workflows/build-java-docs.yaml index 8f9da251..9d0760a2 100644 --- a/.github/workflows/build-java-docs.yaml +++ b/.github/workflows/build-java-docs.yaml @@ -25,8 +25,11 @@ name: Build Java Docs # build script logs to confirm it was actually applied, since nothing else # reports it. "Java heap space" means the JVM hit the cap you set; exit 137 # means the kernel killed it from outside, i.e. the machine is too small rather -# than the cap too low. On a 7.7 GB container VM the full JDK could not be made -# to fit at any setting - see run-build-java-docs-with-act.sh for the numbers. +# than the cap too low. For reference, the full JDK could not be made to fit a +# 7.7 GB container VM at any setting, and runs in ~4 minutes on a 15.6 GB one +# with dokka_worker_heap=8g - see run-build-java-docs-with-act.sh for the +# numbers. A GitHub-hosted runner is not a container in this sense and has its +# own memory; treat those figures as a floor, not a prediction. # # NOTE ON JDK VERSION: java_version picks both the JDK whose sources are # documented AND the JDK the Gradle builds run on. It must be 21 or lower: diff --git a/run-build-java-docs-with-act.sh b/run-build-java-docs-with-act.sh index 60b48895..065debc5 100755 --- a/run-build-java-docs-with-act.sh +++ b/run-build-java-docs-with-act.sh @@ -31,11 +31,16 @@ # --dokka-worker-heap 4g "Java heap space" - the JVM hit its own cap; # 4g is too little for the job # -# So there is no heap setting that works at 7.7 GB: below ~5g the analysis -# genuinely needs more, and at ~5g and up the process no longer fits alongside -# the Gradle daemon. Raise the container VM instead (colima start --memory 12, -# or Docker Desktop's Resources pane) - or use --modules to document a subset, -# which runs in about a minute and is enough to exercise the whole pipeline. +# So no heap setting works at 7.7 GB: below ~5g the analysis genuinely needs +# more, and at ~5g and up the process no longer fits alongside the Gradle +# daemon. Raise the container VM instead. Measured working configuration: +# +# colima start --memory 16 (docker reports 15.6 GB) +# --dokka-worker-heap 8g full JDK, all 60 modules, ~4 minutes: +# 2m30 generating, 1m15 syncing +# +# Or use --modules to document a subset, which runs in about a minute on the +# smaller VM and is enough to exercise the whole pipeline. # # Those two failure modes are also how to tell whether --dokka-worker-heap took # effect at all: "Java heap space" means the JVM hit the cap you set, exit 137