diff --git a/.github/workflows/website-docs.yml b/.github/workflows/website-docs.yml
index d904386352c..b71a1040d4f 100644
--- a/.github/workflows/website-docs.yml
+++ b/.github/workflows/website-docs.yml
@@ -36,6 +36,10 @@ on:
# The Hugo build embeds JavaDocs produced by this script, so a change
# to how the docs are generated must redeploy the website too.
- '.github/scripts/build_javadocs.sh'
+ # The doclet that renders the API pages themselves. It is outside the
+ # maven reactor and built only by this workflow, so nothing else would
+ # catch a change to it.
+ - 'maven/javadoc-hugo-doclet/**'
- '.github/workflows/website-docs.yml'
push:
branches: [main, master]
@@ -61,6 +65,10 @@ on:
# The Hugo build embeds JavaDocs produced by this script, so a change
# to how the docs are generated must redeploy the website too.
- '.github/scripts/build_javadocs.sh'
+ # The doclet that renders the API pages themselves. It is outside the
+ # maven reactor and built only by this workflow, so nothing else would
+ # catch a change to it.
+ - 'maven/javadoc-hugo-doclet/**'
- '.github/workflows/website-docs.yml'
workflow_dispatch:
inputs:
@@ -206,6 +214,41 @@ jobs:
# on their actual date.
HUGO_BUILD_FUTURE: ${{ github.event_name == 'pull_request' && 'true' || 'false' }}
+ - name: Validate API reference output
+ run: |
+ set -euo pipefail
+ test -f docs/website/public/javadoc/index.html
+ # Pages are published at the directory URL, because Cloudflare Pages
+ # will not serve a .html one: see check-javadoc-urls.sh.
+ test -f docs/website/public/javadoc/com/codename1/ui/Component/index.html
+ test -f docs/website/public/javadoc/com/codename1/ui/package-summary/index.html
+ test -f docs/website/public/javadoc-search.json
+ # The fragment encoding is the compatibility surface, so assert one of
+ # the awkward spellings rather than only that the page exists. This is
+ # a canary for the parity gate rather than a duplicate of it: the gate
+ # compares two trees, and a gate that ends up comparing nothing reports
+ # nothing missing.
+ #
+ # Quotes are optional because the site is built with --minify and the
+ # minifier drops them wherever HTML allows: this page really does carry
+ # id=createImage(byte[],int,int) with no quotes at all.
+ grep -Eq 'id="?createImage\(byte\[\],int,int\)"?' \
+ docs/website/public/javadoc/com/codename1/ui/Image/index.html
+ # The old integration copied the standard doclet's tree into static/
+ # and scoped its stylesheet with a script. Nothing should reintroduce
+ # either: they are what broke dark mode.
+ test ! -e docs/website/static/javadoc
+ test ! -e docs/website/assets/js/cn1-javadoc.js
+
+ - name: Validate API URLs on the Pages runtime
+ run: |
+ set -euo pipefail
+ # Asks the server rather than the filesystem. Everything else was green
+ # on a build where clicking any class left the site: Pages redirects
+ # /x.html to /x before an asset is considered, so the page published at
+ # the extension was one nobody could reach.
+ scripts/website/check-javadoc-urls.sh docs/website/public 8789
+
- name: Validate Port Status page output
run: |
set -euo pipefail
diff --git a/.gitignore b/.gitignore
index 59ea2a7976a..1ea2c0ac45b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -114,6 +114,15 @@ dependency-reduced-pom.xml
# docs/developer-guide on every run, so a committed copy is both duplicated
# source and guaranteed stale. 485 files of it were committed by accident once.
/docs/website/static/developer-guide/
+# The API reference is generated into the content tree by the Hugo doclet on
+# every run of scripts/website/build.sh, one file per documented type. Roughly
+# 1850 generated pages have no business in the history: the sources they are
+# rendered from are already committed.
+/docs/website/content/javadoc/
+/docs/website/static/javadoc-search.json
+# Left behind by the previous integration, which copied the whole standard
+# doclet tree into static/ and served it through a scoping script.
+/docs/website/static/javadoc/
/docs/website/.hugo_build.lock
/docs/website/hugo_stats.json
/docs/website/resources/
diff --git a/docs/website/assets/css/extended/cn1-api.css b/docs/website/assets/css/extended/cn1-api.css
new file mode 100644
index 00000000000..eeb174a2dd7
--- /dev/null
+++ b/docs/website/assets/css/extended/cn1-api.css
@@ -0,0 +1,479 @@
+/*
+ * API reference pages.
+ *
+ * Everything here is expressed in the site's own design tokens, so dark mode is
+ * whatever body.dark already says it is. That is the point of the rewrite: the
+ * previous integration copied javadoc's stylesheet in, rewrote every selector to
+ * scope it under one div, and then tried to force roughly thirty of javadoc's
+ * own custom properties back onto theme colours with !important. It lost that
+ * fight in dark mode, because a stylesheet that was never built to be themed
+ * cannot be themed from the outside.
+ */
+
+.cn1-api {
+ max-width: var(--cn1-shell);
+}
+
+.cn1-api__header {
+ border-bottom: 1px solid var(--cn1-border);
+ padding-bottom: 1rem;
+ margin-bottom: 1.5rem;
+}
+
+.cn1-api__crumbs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+ font-family: var(--cn1-mono);
+ font-size: 0.85rem;
+ color: var(--cn1-muted);
+ margin-bottom: 0.5rem;
+}
+
+.cn1-api__crumbs a {
+ color: var(--cn1-accent);
+}
+
+.cn1-api__title {
+ font-family: var(--cn1-mono);
+ word-break: break-word;
+}
+
+.cn1-api__kind {
+ display: inline-block;
+ font-family: var(--cn1-font);
+ font-size: 0.75em;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--cn1-muted);
+ margin-right: 0.5rem;
+ vertical-align: middle;
+}
+
+.cn1-api__intro {
+ color: var(--cn1-muted);
+}
+
+/* Inheritance chain, indented one step per level the way javadoc stacks it. */
+.cn1-api__inheritance {
+ list-style: none;
+ padding: 0;
+ margin: 0.75rem 0 0;
+ font-family: var(--cn1-mono);
+ font-size: 0.85rem;
+}
+
+.cn1-api__inheritance li {
+ padding-left: 1.1rem;
+ position: relative;
+ color: var(--cn1-muted);
+}
+
+.cn1-api__inheritance li + li::before {
+ content: "\2514";
+ position: absolute;
+ left: 0.2rem;
+ color: var(--cn1-border-strong);
+}
+
+.cn1-api__inheritance li + li {
+ margin-left: 0.8rem;
+}
+
+.cn1-api__inheritance .is-current {
+ color: var(--cn1-text);
+ font-weight: 600;
+}
+
+.cn1-api__relations {
+ font-size: 0.9rem;
+ color: var(--cn1-muted);
+ margin: 0.6rem 0 0;
+}
+
+.cn1-api__relations > span {
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ font-size: 0.72rem;
+ margin-right: 0.4rem;
+}
+
+/* -------------------------------------------------------------- signatures */
+
+.cn1-api-signature {
+ font-family: var(--cn1-mono);
+ font-size: 0.88rem;
+ line-height: 1.7;
+ background: none;
+ padding: 0;
+ color: var(--cn1-text);
+ overflow-wrap: anywhere;
+}
+
+.cn1-api-mod {
+ color: var(--cn1-muted);
+}
+
+.cn1-api-name {
+ font-weight: 600;
+ color: var(--cn1-text);
+}
+
+.cn1-api-arg {
+ color: var(--cn1-text);
+}
+
+.cn1-api-punct {
+ color: var(--cn1-muted);
+}
+
+.cn1-api-const {
+ color: var(--cn1-success);
+}
+
+a.cn1-api-type,
+.cn1-api-type {
+ color: var(--cn1-accent);
+}
+
+.cn1-api-table__member a {
+ text-decoration: none;
+}
+
+.cn1-api-table__member a:hover .cn1-api-name {
+ text-decoration: underline;
+}
+
+/* ----------------------------------------------------------------- tables */
+
+.cn1-api-summary {
+ margin: 2rem 0;
+}
+
+.cn1-api-summary__title,
+.cn1-api-details__title {
+ font-size: 1.15rem;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--cn1-muted);
+ border-bottom: 1px solid var(--cn1-border);
+ padding-bottom: 0.4rem;
+ margin-bottom: 0.8rem;
+}
+
+.cn1-api-table {
+ width: 100%;
+ border-collapse: collapse;
+ display: table;
+ margin: 0;
+}
+
+.cn1-api-table td {
+ border-bottom: 1px solid var(--cn1-border);
+ padding: 0.6rem 0.8rem 0.6rem 0;
+ vertical-align: top;
+}
+
+.cn1-api-table tr:hover td {
+ background: var(--cn1-surface-soft);
+}
+
+.cn1-api-table__member {
+ width: 45%;
+ min-width: 16rem;
+}
+
+.cn1-api-table__summary {
+ color: var(--cn1-muted);
+ font-size: 0.92rem;
+}
+
+.cn1-api-table__summary p {
+ margin: 0;
+}
+
+.cn1-api-table tr.is-deprecated .cn1-api-name {
+ text-decoration: line-through;
+}
+
+.cn1-api-tag {
+ display: inline-block;
+ font-size: 0.68rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--cn1-surface);
+ background: var(--cn1-muted);
+ border-radius: var(--cn1-radius-sm);
+ padding: 0.05rem 0.4rem;
+ margin-right: 0.3rem;
+ vertical-align: 1px;
+}
+
+/* ---------------------------------------------------------------- members */
+
+.cn1-api-member {
+ border-left: 3px solid var(--cn1-border);
+ padding: 0.2rem 0 0.2rem 1rem;
+ margin: 1.6rem 0;
+ /* Anchored jumps land under the sticky header without this. */
+ scroll-margin-top: 5rem;
+}
+
+.cn1-api-member:target {
+ border-left-color: var(--cn1-accent);
+ background: var(--cn1-accent-soft);
+ border-radius: 0 var(--cn1-radius-sm) var(--cn1-radius-sm) 0;
+}
+
+.cn1-api-member__name {
+ font-family: var(--cn1-mono);
+ font-size: 1.05rem;
+ margin: 0 0 0.35rem;
+}
+
+/* A zero-size marker carrying an alternate spelling of the same fragment. */
+.cn1-api-alias {
+ display: block;
+ height: 0;
+ scroll-margin-top: 5rem;
+}
+
+.cn1-api-block {
+ margin: 0.9rem 0 0;
+}
+
+.cn1-api-block__title {
+ font-size: 0.72rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.07em;
+ color: var(--cn1-muted);
+ margin: 0 0 0.3rem;
+}
+
+.cn1-api-params {
+ margin: 0;
+ display: grid;
+ grid-template-columns: minmax(6rem, max-content) 1fr;
+ gap: 0.25rem 1rem;
+}
+
+.cn1-api-params dt {
+ font-family: var(--cn1-mono);
+ font-size: 0.85rem;
+}
+
+.cn1-api-params dt code {
+ background: none;
+ padding: 0;
+ color: var(--cn1-text);
+ font-weight: 600;
+}
+
+.cn1-api-params__type {
+ color: var(--cn1-muted);
+ font-weight: 400;
+}
+
+.cn1-api-params dd {
+ margin: 0;
+ color: var(--cn1-text);
+}
+
+.cn1-api-params dd p {
+ margin: 0;
+}
+
+.cn1-api-undocumented {
+ color: var(--cn1-muted);
+ font-style: italic;
+}
+
+.cn1-api-see {
+ margin: 0;
+ padding-left: 1.1rem;
+}
+
+/* Typography for comment prose.
+ *
+ * Documentation comments are markdown and go through the same goldmark as the
+ * rest of the site, but they are rendered into these containers rather than
+ * into PaperMod's .post-content, so none of the theme's element spacing reaches
+ * them. Without this every paragraph, list and heading in a comment runs
+ * together into one block of text. */
+.cn1-api-prose p,
+.cn1-api-prose ul,
+.cn1-api-prose ol,
+.cn1-api-prose blockquote,
+.cn1-api-prose table,
+.cn1-api-prose pre {
+ margin: 0 0 0.85rem;
+}
+
+.cn1-api-prose ul,
+.cn1-api-prose ol {
+ padding-left: 1.4rem;
+}
+
+.cn1-api-prose li {
+ margin-bottom: 0.25rem;
+}
+
+.cn1-api-prose h1,
+.cn1-api-prose h2,
+.cn1-api-prose h3,
+.cn1-api-prose h4,
+.cn1-api-prose h5,
+.cn1-api-prose h6 {
+ margin: 1.4rem 0 0.5rem;
+ line-height: 1.3;
+}
+
+/* A comment's own headings must never outrank the page structure around them,
+ however deep the author nested them. */
+.cn1-api-prose h1,
+.cn1-api-prose h2,
+.cn1-api-prose h3 {
+ font-size: 1.05rem;
+}
+
+.cn1-api-prose h4,
+.cn1-api-prose h5,
+.cn1-api-prose h6 {
+ font-size: 0.95rem;
+}
+
+.cn1-api-prose img {
+ max-width: 100%;
+ height: auto;
+ border-radius: var(--cn1-radius-sm);
+}
+
+.cn1-api-prose blockquote {
+ border-left: 3px solid var(--cn1-border);
+ padding-left: 0.9rem;
+ color: var(--cn1-muted);
+}
+
+.cn1-api-prose table {
+ display: block;
+ overflow-x: auto;
+ max-width: 100%;
+}
+
+.cn1-api-prose > :first-child {
+ margin-top: 0;
+}
+
+.cn1-api-prose > :last-child {
+ margin-bottom: 0;
+}
+
+.cn1-api-deprecated {
+ border-left: 3px solid var(--cn1-accent);
+ background: var(--cn1-surface-soft);
+ border-radius: 0 var(--cn1-radius-sm) var(--cn1-radius-sm) 0;
+ padding: 0.6rem 0.9rem;
+ margin: 0.6rem 0;
+ font-size: 0.92rem;
+}
+
+.cn1-api-deprecated p {
+ margin: 0;
+}
+
+.cn1-api-inherited__group {
+ margin: 0.9rem 0;
+}
+
+.cn1-api-inherited__from {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--cn1-muted);
+ margin: 0 0 0.25rem;
+}
+
+.cn1-api-inherited__list {
+ margin: 0;
+ font-family: var(--cn1-mono);
+ font-size: 0.82rem;
+ line-height: 1.9;
+ color: var(--cn1-muted);
+}
+
+.cn1-api-inherited__list code {
+ background: none;
+ padding: 0;
+}
+
+@media (max-width: 720px) {
+ /* Two columns of signature and prose do not fit a phone, so the summary
+ tables become stacked blocks rather than scrolling sideways. */
+ .cn1-api-table,
+ .cn1-api-table tbody,
+ .cn1-api-table tr,
+ .cn1-api-table td {
+ display: block;
+ width: auto;
+ }
+
+ .cn1-api-table td {
+ border-bottom: none;
+ padding: 0.15rem 0;
+ }
+
+ .cn1-api-table tr {
+ border-bottom: 1px solid var(--cn1-border);
+ padding: 0.6rem 0;
+ }
+
+ .cn1-api-table__member {
+ min-width: 0;
+ }
+
+ .cn1-api-params {
+ grid-template-columns: 1fr;
+ gap: 0.1rem;
+ }
+
+ .cn1-api-params dd {
+ margin-bottom: 0.5rem;
+ }
+}
+
+/* The sentence that a third of the See-also entries carry after the reference.
+ It is prose about the link, not part of the name, so it is not inside it. */
+.cn1-api-see__note {
+ color: var(--cn1-muted);
+ font-size: 0.9em;
+}
+
+/* The repository's own @warning tag: a safety note, not prose. */
+.cn1-api-warning {
+ border-left: 3px solid var(--cn1-accent);
+ background: var(--cn1-surface-soft);
+ border-radius: 0 var(--cn1-radius-sm) var(--cn1-radius-sm) 0;
+ padding: 0.6rem 0.9rem;
+ margin: 0.6rem 0;
+ font-size: 0.92rem;
+}
+
+.cn1-api-warning p {
+ margin: 0;
+ display: inline;
+}
+
+.cn1-api__annotations {
+ font-family: var(--cn1-mono);
+ font-size: 0.82rem;
+ color: var(--cn1-muted);
+ margin: 0 0 0.2rem;
+}
+
+.cn1-api__annotations code {
+ background: none;
+ padding: 0;
+}
diff --git a/docs/website/assets/css/extended/cn1-javadoc-embed.css b/docs/website/assets/css/extended/cn1-javadoc-embed.css
deleted file mode 100644
index 887b2c51f3a..00000000000
--- a/docs/website/assets/css/extended/cn1-javadoc-embed.css
+++ /dev/null
@@ -1,169 +0,0 @@
-.cn1-javadoc-page .main {
- max-width: 100vw;
- width: 100vw;
- margin: 0;
- padding-left: 0;
- padding-right: 0;
-}
-
-body.cn1-javadoc-page .main:has(> .post-single--api-javadoc) {
- max-width: 100vw !important;
- width: 100vw !important;
- margin: 0 !important;
- padding-left: 0 !important;
- padding-right: 0 !important;
-}
-
-.post-single--api-javadoc {
- max-width: 100vw;
- width: 100%;
- margin: 0;
- padding-left: 0;
- padding-right: 0;
-}
-
-.cn1-javadoc-links {
- display: flex;
- align-items: center;
- gap: 0.55rem;
- margin: 0.4rem 0 1rem;
- font-size: 0.95rem;
-}
-
-.cn1-javadoc {
- border: 1px solid var(--border);
- border-radius: 0;
- padding: 0.7rem;
- background: var(--entry);
- color: var(--content);
- overflow: visible;
- --code-bg: color-mix(in srgb, var(--entry) 90%, var(--theme) 10%);
- --body-text-color: var(--content) !important;
- --block-text-color: var(--content) !important;
- --body-background-color: var(--entry) !important;
- --section-background-color: var(--entry) !important;
- --detail-background-color: var(--entry) !important;
- --code-background-color: var(--code-bg);
- --pre-background-color: var(--code-bg);
- --pre-text-color: var(--content);
- --snippet-background-color: var(--code-bg);
- --snippet-text-color: var(--content);
- --table-header-color: color-mix(in srgb, var(--entry) 70%, var(--theme) 30%) !important;
- --even-row-color: var(--entry) !important;
- --odd-row-color: color-mix(in srgb, var(--entry) 90%, var(--theme) 10%) !important;
- --border-color: var(--border) !important;
- --title-color: var(--primary) !important;
- --link-color: var(--primary) !important;
- --link-color-active: var(--primary) !important;
- --toc-background-color: var(--theme) !important;
- --toc-hover-color: color-mix(in srgb, var(--entry) 80%, var(--theme) 20%) !important;
- --toc-highlight-color: color-mix(in srgb, var(--entry) 70%, var(--theme) 30%) !important;
- --navbar-background-color: color-mix(in srgb, var(--entry) 70%, #1f4a75 30%) !important;
- --navbar-text-color: var(--primary) !important;
- --subnav-background-color: color-mix(in srgb, var(--entry) 78%, var(--theme) 22%) !important;
- --subnav-link-color: var(--primary) !important;
- --selected-background-color: color-mix(in srgb, var(--primary) 26%, var(--entry) 74%) !important;
- --selected-text-color: var(--content) !important;
- --search-input-background-color: var(--entry) !important;
- --search-input-text-color: var(--content) !important;
- --search-input-placeholder-color: var(--secondary) !important;
-}
-
-.cn1-javadoc .main-grid {
- width: 100% !important;
- max-width: none !important;
- margin: 0;
-}
-
-.cn1-javadoc .header,
-.cn1-javadoc .sub-nav,
-.cn1-javadoc .top-nav,
-.cn1-javadoc .flex-content,
-.cn1-javadoc .main-content,
-.cn1-javadoc .contentContainer {
- width: 100% !important;
- max-width: none !important;
- overflow: visible !important;
-}
-
-.cn1-javadoc .table-header,
-.cn1-javadoc .table-sub-heading-color,
-.cn1-javadoc .summary-table div[class^="col-"],
-.cn1-javadoc .summary-table div[class*=" col-"] {
- min-height: 2.25rem;
- height: auto !important;
- line-height: 1.35 !important;
- padding-top: 0.4rem !important;
- padding-bottom: 0.4rem !important;
- overflow: visible !important;
-}
-
-.cn1-javadoc input[type="search"],
-.cn1-javadoc input#search-input,
-.cn1-javadoc #search-input,
-.cn1-javadoc .search-input {
- pointer-events: auto !important;
- position: relative;
- z-index: 30;
-}
-
-.cn1-javadoc-page .ui-autocomplete,
-.cn1-javadoc-page .ui-menu {
- background: var(--entry) !important;
- color: var(--content) !important;
- border: 1px solid var(--border) !important;
- box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28) !important;
- opacity: 1 !important;
- max-width: min(92vw, 48rem) !important;
- overflow: auto !important;
- z-index: 2000 !important;
-}
-
-.cn1-javadoc-page .ui-menu .ui-menu-item-wrapper {
- color: var(--content) !important;
-}
-
-.cn1-javadoc-page .ui-menu .ui-state-active,
-.cn1-javadoc-page .ui-menu .ui-menu-item-wrapper.ui-state-active {
- background: color-mix(in srgb, var(--primary) 22%, var(--entry) 78%) !important;
- border-color: var(--border) !important;
- color: var(--content) !important;
-}
-
-.cn1-javadoc pre,
-.cn1-javadoc code,
-.cn1-javadoc pre code,
-.cn1-javadoc .snippet-container,
-.cn1-javadoc .snippet-container pre,
-.cn1-javadoc .block pre,
-.cn1-javadoc .block code {
- background: var(--code-bg) !important;
- color: var(--content) !important;
-}
-
-.cn1-javadoc a {
- color: var(--primary);
-}
-
-body.dark .cn1-javadoc {
- background: var(--theme);
- --code-bg: #141f31;
- --body-background-color: var(--theme) !important;
- --section-background-color: var(--theme) !important;
- --detail-background-color: var(--theme) !important;
- --table-header-color: color-mix(in srgb, var(--theme) 70%, #1b2b40 30%) !important;
- --even-row-color: var(--theme) !important;
- --odd-row-color: color-mix(in srgb, var(--theme) 86%, #0d1726 14%) !important;
- --toc-background-color: #101a2a !important;
- --toc-hover-color: #172439 !important;
- --toc-highlight-color: #1d2d45 !important;
- --navbar-background-color: #1b2d45 !important;
- --subnav-background-color: #152235 !important;
- --subnav-link-color: #cfe2ff !important;
- --link-color: #b7d2ff !important;
- --link-color-active: #d6e6ff !important;
- --title-color: #e3edff !important;
- --search-input-background-color: #111b2b !important;
- --search-input-text-color: #e9eef7 !important;
- --search-input-placeholder-color: #8ea1bc !important;
-}
diff --git a/docs/website/assets/css/extended/cn1-search.css b/docs/website/assets/css/extended/cn1-search.css
index c22f30b6808..59f4651e79b 100644
--- a/docs/website/assets/css/extended/cn1-search.css
+++ b/docs/website/assets/css/extended/cn1-search.css
@@ -132,3 +132,76 @@
color: var(--cn1-search-soft-text);
white-space: nowrap;
}
+
+/* API reference hits, listed above the page results on the search page. */
+.cn1-search-api {
+ margin: 1.5rem 0 2rem;
+}
+
+.cn1-search-api__title {
+ font-size: 0.78rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--cn1-muted);
+ margin: 0 0 0.5rem;
+}
+
+.cn1-search-api__results {
+ border: 1px solid var(--cn1-border);
+ border-radius: var(--cn1-radius);
+ overflow: hidden;
+}
+
+.cn1-search-api__hit {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 0.15rem 0.7rem;
+ padding: 0.55rem 0.8rem;
+ border-bottom: 1px solid var(--cn1-border);
+ text-decoration: none;
+ color: inherit;
+}
+
+.cn1-search-api__hit:last-child {
+ border-bottom: none;
+}
+
+.cn1-search-api__hit:hover,
+.cn1-search-api__hit:focus-visible {
+ background: var(--cn1-surface-soft);
+}
+
+.cn1-search-api__name {
+ font-family: var(--cn1-mono);
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: var(--cn1-accent);
+ background: none;
+ padding: 0;
+ overflow-wrap: anywhere;
+}
+
+.cn1-search-api__kind {
+ grid-column: 2;
+ grid-row: 1;
+ font-size: 0.68rem;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--cn1-muted);
+ align-self: center;
+}
+
+.cn1-search-api__context {
+ grid-column: 1 / -1;
+ font-family: var(--cn1-mono);
+ font-size: 0.75rem;
+ color: var(--cn1-muted);
+ overflow-wrap: anywhere;
+}
+
+.cn1-search-api__summary {
+ grid-column: 1 / -1;
+ font-size: 0.85rem;
+ color: var(--cn1-muted);
+}
diff --git a/docs/website/assets/js/cn1-javadoc.js b/docs/website/assets/js/cn1-javadoc.js
deleted file mode 100644
index 32f1511f43b..00000000000
--- a/docs/website/assets/js/cn1-javadoc.js
+++ /dev/null
@@ -1,139 +0,0 @@
-(() => {
- const root = document.querySelector(".cn1-javadoc");
- if (!root) return;
- document.body.classList.add("cn1-javadoc-page");
-
- if (window.location.pathname === "/javadoc") {
- window.history.replaceState(null, "", "/javadoc/");
- }
-
- let currentDocPath = "/javadoc/_index-raw.html";
-
- const reviveSearchUi = () => {
- const inputs = root.querySelectorAll('input[type="search"], input#search-input, #search-input');
- inputs.forEach((input) => {
- input.removeAttribute("disabled");
- input.readOnly = false;
- input.tabIndex = 0;
- input.style.pointerEvents = "auto";
- input.style.position = "relative";
- input.style.zIndex = "30";
- });
- clampSearchPopup();
- };
-
- const clampSearchPopup = () => {
- document.querySelectorAll(".ui-autocomplete").forEach((menu) => {
- menu.style.maxWidth = "min(92vw, 48rem)";
- menu.style.background = "var(--entry)";
- menu.style.color = "var(--content)";
- menu.style.border = "1px solid var(--border)";
- menu.style.boxShadow = "0 10px 24px rgba(0, 0, 0, 0.28)";
- menu.style.opacity = "1";
- menu.style.transform = "translateX(0)";
- const rect = menu.getBoundingClientRect();
- let dx = 0;
- if (rect.right > window.innerWidth - 8) dx = (window.innerWidth - 8) - rect.right;
- if (rect.left < 8) dx = 8 - rect.left;
- if (dx !== 0) menu.style.transform = `translateX(${dx}px)`;
- });
- };
-
- const ensureSearchAssets = (fetchPath) => {
- window.pathtoroot = calcPathToRoot(fetchPath);
- reviveSearchUi();
- };
-
- const resolveHref = (rawHref) => {
- if (!rawHref) return null;
- if (rawHref.startsWith("http://") || rawHref.startsWith("https://") || rawHref.startsWith("//")) {
- return rawHref;
- }
- if (rawHref.startsWith("/")) {
- return `${window.location.origin}${rawHref}`;
- }
- const base = `${window.location.origin}${currentDocPath}`;
- try {
- return new URL(rawHref, base).toString();
- } catch (_e) {
- return null;
- }
- };
-
- const toRoute = (href) => {
- let url;
- try {
- url = new URL(href, window.location.href);
- } catch (_e) {
- return null;
- }
- if (url.origin !== window.location.origin) return null;
- if (!url.pathname.startsWith("/javadoc")) return null;
- if (url.pathname === "/javadoc" || url.pathname === "/javadoc/") {
- return { fetchPath: "/javadoc/_index-raw.html", browserPath: "/javadoc/" };
- }
- if (!url.pathname.endsWith(".html")) return null;
- return (url.pathname === "/javadoc/index.html" || url.pathname === "/javadoc/_index-raw.html")
- ? { fetchPath: "/javadoc/_index-raw.html", browserPath: "/javadoc/" }
- : { fetchPath: url.pathname, browserPath: url.pathname };
- };
-
- const calcPathToRoot = (fetchPath) => {
- const rel = fetchPath.replace(/^\/javadoc\//, "");
- const depth = Math.max(0, rel.split("/").length - 1);
- return depth === 0 ? "./" : "../".repeat(depth);
- };
-
- const loadIntoContainer = async (route, pushState) => {
- const res = await fetch(route.fetchPath, { credentials: "same-origin" });
- if (!res.ok) return;
- const html = await res.text();
- const doc = new DOMParser().parseFromString(html, "text/html");
- if (!doc.body) return;
- root.innerHTML = doc.body.innerHTML;
- currentDocPath = route.fetchPath;
- ensureSearchAssets(route.fetchPath);
- reviveSearchUi();
- if (pushState) {
- window.history.pushState({ cn1Javadoc: route.browserPath }, "", route.browserPath);
- }
- window.scrollTo({ top: 0, behavior: "auto" });
- };
-
- root.addEventListener("click", (event) => {
- const link = event.target.closest("a[href]");
- if (!link || link.target || link.hasAttribute("download")) return;
- const rawHref = link.getAttribute("href");
- if (!rawHref || rawHref.startsWith("#")) return;
- const resolved = resolveHref(rawHref);
- if (!resolved) return;
- const route = toRoute(resolved);
- if (!route) return;
- event.preventDefault();
- loadIntoContainer(route, true).catch(() => {});
- });
-
- document.addEventListener("click", (event) => {
- const link = event.target.closest(".ui-autocomplete a[href], .ui-menu a[href]");
- if (!link || link.target || link.hasAttribute("download")) return;
- const rawHref = link.getAttribute("href");
- if (!rawHref || rawHref.startsWith("#")) return;
- const resolved = resolveHref(rawHref);
- if (!resolved) return;
- const route = toRoute(resolved);
- if (!route) return;
- event.preventDefault();
- loadIntoContainer(route, true).catch(() => {});
- }, true);
-
- window.addEventListener("popstate", () => {
- const route = toRoute(window.location.href);
- if (!route) return;
- loadIntoContainer(route, false).catch(() => {});
- });
- window.addEventListener("resize", clampSearchPopup);
- root.addEventListener("input", clampSearchPopup, true);
- root.addEventListener("focusin", clampSearchPopup, true);
-
- ensureSearchAssets(currentDocPath);
-})();
diff --git a/docs/website/content/api.md b/docs/website/content/api.md
deleted file mode 100644
index ab3384aaf87..00000000000
--- a/docs/website/content/api.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: "API"
-date: 2015-03-03
-slug: "javadoc"
-layout: "api-javadoc"
-description: "Codename One JavaDoc"
-aliases:
- - /api/
----
diff --git a/docs/website/layouts/_default/api-javadoc.html b/docs/website/layouts/_default/api-javadoc.html
deleted file mode 100644
index 9b93803c2b6..00000000000
--- a/docs/website/layouts/_default/api-javadoc.html
+++ /dev/null
@@ -1,21 +0,0 @@
-{{- define "main" -}}
-
-
-
-
- {{- $javadocFragment := "generated/javadoc-content.html" -}}
- {{- if fileExists $javadocFragment -}}
- {{- readFile $javadocFragment | safeHTML -}}
- {{- else -}}
-
- JavaDoc content isn't generated in this build.
- Run the website build with WEBSITE_INCLUDE_JAVADOCS=true.
-
- {{- end -}}
-
-
-{{- end -}}
diff --git a/docs/website/layouts/_default/search.html b/docs/website/layouts/_default/search.html
index 011a3087172..9d2e7f5e28a 100644
--- a/docs/website/layouts/_default/search.html
+++ b/docs/website/layouts/_default/search.html
@@ -10,11 +10,18 @@
{{ .Title }}
Search
- Note: this index does not cover the Developer Guide .
+ Note: API types and members are matched by name and listed separately below.
+ This index does not cover the Developer Guide .
The guide is a single large document and would match almost every query —
search it directly using your browser's find-in-page (Ctrl/Cmd‑F).
Loading search index…
+
+
+
@@ -189,4 +196,128 @@
});
})();
+
+
{{- end -}}
diff --git a/docs/website/layouts/javadoc/overview.html b/docs/website/layouts/javadoc/overview.html
new file mode 100644
index 00000000000..bfb81b1c6b1
--- /dev/null
+++ b/docs/website/layouts/javadoc/overview.html
@@ -0,0 +1,27 @@
+{{- define "main" -}}
+{{- $j := .Params.javadoc -}}
+
+
+
+
+
+
+ {{- range $j.packages }}
+
+ {{ .name }}
+ {{ with .summary }}{{ $.RenderString . }}{{ end }}
+
+ {{- end }}
+
+
+
+
+{{- end -}}
diff --git a/docs/website/layouts/javadoc/package.html b/docs/website/layouts/javadoc/package.html
new file mode 100644
index 00000000000..f74fd757c93
--- /dev/null
+++ b/docs/website/layouts/javadoc/package.html
@@ -0,0 +1,39 @@
+{{- define "main" -}}
+{{- $j := .Params.javadoc -}}
+
+
+
+ {{- if $j.deprecated }}
+
+ Deprecated.
+ {{- with $j.deprecatedText }} {{ $.RenderString . }}{{ end }}
+
+ {{- end }}
+
+ {{- with $j.description }}
+ {{ $.RenderString . }}
+ {{- end }}
+
+
+ Types
+
+
+ {{- range $j.types }}
+
+ {{ .kind }} {{ .name }}
+ {{ with .summary }}{{ $.RenderString . }}{{ end }}
+
+ {{- end }}
+
+
+
+
+{{- end -}}
diff --git a/docs/website/layouts/javadoc/type.html b/docs/website/layouts/javadoc/type.html
new file mode 100644
index 00000000000..a21fe976ca1
--- /dev/null
+++ b/docs/website/layouts/javadoc/type.html
@@ -0,0 +1,188 @@
+{{- define "main" -}}
+{{- $j := .Params.javadoc -}}
+
+
+
+ {{- if $j.deprecated }}
+
+ Deprecated.
+ {{- with $j.deprecatedText }} {{ $.RenderString . }}{{ end }}
+
+ {{- end }}
+
+ {{- with $j.description }}
+ {{ $.RenderString . }}
+ {{- end }}
+
+ {{- range $j.warnings }}
+
+ Warning. {{ $.RenderString . }}
+
+ {{- end }}
+
+ {{- if $j.seeAlso }}
+
+
See also
+
+ {{- range $j.seeAlso }}
+
+ {{- if .url }}{{ .label }}
+ {{- else if .prose }}{{ $.RenderString .label }}
+ {{- else }}{{ .label }}{{ end }}
+ {{- with .note }} {{ . }} {{ end -}}
+
+ {{- end }}
+
+
+ {{- end }}
+
+ {{- if $j.typeParameterDocs }}
+
+
Type parameters
+
+ {{- range $j.typeParameterDocs }}
+ {{ .name }}
+ {{ $.RenderString .doc }}
+ {{- end }}
+
+
+ {{- end }}
+
+ {{- if $j.nested }}
+
+ Nested types
+
+
+ {{- range $j.nested }}
+
+ {{ .kind }} {{ .name }}
+ {{ with .summary }}{{ $.RenderString . }}{{ end }}
+
+ {{- end }}
+
+
+
+ {{- end }}
+
+ {{ partial "javadoc/summary-table.html" (dict "rows" $j.enumConstants "title" "Enum constants" "page" .) }}
+ {{ partial "javadoc/summary-table.html" (dict "rows" $j.fields "title" "Fields" "page" .) }}
+ {{ partial "javadoc/summary-table.html" (dict "rows" $j.constructors "title" "Constructors" "page" .) }}
+ {{ partial "javadoc/summary-table.html" (dict "rows" $j.methods "title" "Methods" "page" .) }}
+
+ {{- if $j.inheritedNested }}
+
+ Inherited nested types
+ {{- range $j.inheritedNested }}
+
+
From
+ {{- if .url }} {{ .from }} {{ else }} {{ .from }}{{ end }}
+
+ {{- range $i, $m := .members }}{{ if $i }}, {{ end -}}
+ {{- if $m.url }}{{ $m.name }} {{ else }}{{ $m.name }}{{ end -}}
+ {{- end }}
+
+
+ {{- end }}
+
+ {{- end }}
+
+ {{- if $j.inheritedFields }}
+
+ Inherited fields
+ {{- range $j.inheritedFields }}
+
+
From
+ {{- if .url }} {{ .from }} {{ else }} {{ .from }}{{ end }}
+
+ {{- range $i, $m := .members }}{{ if $i }}, {{ end -}}
+ {{- if $m.url }}{{ $m.name }} {{ else }}{{ $m.name }}{{ end -}}
+ {{- end }}
+
+
+ {{- end }}
+
+ {{- end }}
+
+ {{- if $j.inherited }}
+
+ Inherited methods
+ {{- range $j.inherited }}
+
+
From
+ {{- if .url }} {{ .from }} {{ else }} {{ .from }}{{ end }}
+
+ {{- range $i, $m := .members }}{{ if $i }}, {{ end -}}
+ {{- if $m.url }}{{ $m.name }} {{ else }}{{ $m.name }}{{ end -}}
+ {{- end }}
+
+
+ {{- end }}
+
+ {{- end }}
+
+ {{- if $j.enumConstants }}
+
+ Enum constant details
+ {{- range $j.enumConstants }}{{ partial "javadoc/member-detail.html" (dict "member" . "page" $) }}{{ end }}
+
+ {{- end }}
+
+ {{- if $j.fields }}
+
+ Field details
+ {{- range $j.fields }}{{ partial "javadoc/member-detail.html" (dict "member" . "page" $) }}{{ end }}
+
+ {{- end }}
+
+ {{- if $j.constructors }}
+
+ Constructor details
+ {{- range $j.constructors }}{{ partial "javadoc/member-detail.html" (dict "member" . "page" $) }}{{ end }}
+
+ {{- end }}
+
+ {{- if $j.methods }}
+
+ Method details
+ {{- range $j.methods }}{{ partial "javadoc/member-detail.html" (dict "member" . "page" $) }}{{ end }}
+
+ {{- end }}
+
+{{- end -}}
diff --git a/docs/website/layouts/partials/footer.html b/docs/website/layouts/partials/footer.html
index 705cf4cb249..9e154fb2b0b 100644
--- a/docs/website/layouts/partials/footer.html
+++ b/docs/website/layouts/partials/footer.html
@@ -78,7 +78,6 @@ Community
{{- $cn1Demo := resources.Get "js/cn1-demo.js" | minify | fingerprint }}
{{- $cn1Pricing := resources.Get "js/cn1-pricing.js" | minify | fingerprint }}
{{- $cn1Crisp := resources.Get "js/cn1-crisp.js" | minify | fingerprint }}
-{{- $cn1Javadoc := resources.Get "js/cn1-javadoc.js" | minify | fingerprint }}
{{- $cn1PortStatus := resources.Get "js/cn1-port-status.js" | minify | fingerprint }}
{{- if or (hasPrefix (.RelPermalink | default "") "/demo-") (eq .Layout "demos") }}
@@ -88,9 +87,6 @@ Community
{{- end }}
-{{- if eq .Layout "api-javadoc" -}}
-
-{{- end -}}
{{- if eq .Layout "port-status" -}}
{{- end -}}
diff --git a/docs/website/layouts/partials/javadoc/member-detail.html b/docs/website/layouts/partials/javadoc/member-detail.html
new file mode 100644
index 00000000000..5fa7aa48e52
--- /dev/null
+++ b/docs/website/layouts/partials/javadoc/member-detail.html
@@ -0,0 +1,80 @@
+{{- /* The full entry for one member. Called with a dict of "member" and "page",
+ because prose has to be rendered through the page that owns it. */ -}}
+{{- $m := .member -}}
+{{- $page := .page -}}
+
+ {{- /* Javadoc answers to both the declared and the erased spelling of a
+ signature containing a type variable, and links in the wild use both,
+ so every alternate identifier gets an anchor of its own. */ -}}
+ {{- range $i, $a := $m.anchors }}{{ if $i }} {{ end }}{{ end }}
+ {{ $m.name }}
+ {{ partial "javadoc/signature.html" $m }}
+
+ {{- if $m.deprecated }}
+
+ Deprecated.
+ {{- with $m.deprecatedText }} {{ $page.RenderString . }}{{ end }}
+
+ {{- end }}
+
+ {{- with $m.description }}
+ {{ $page.RenderString . }}
+ {{- end }}
+
+ {{- range $m.warnings }}
+
+ Warning. {{ $page.RenderString . }}
+
+ {{- end }}
+
+ {{- if $m.parameters }}
+ {{- $documented := false }}
+ {{- range $m.parameters }}{{ if .doc }}{{ $documented = true }}{{ end }}{{ end }}
+ {{- if $documented }}
+
+
Parameters
+
+ {{- range $m.parameters }}
+ {{ .name }}{{ with .type.label }} {{ . }} {{ end }}
+ {{ with .doc }}{{ $page.RenderString . }}{{ else }}Not documented. {{ end }}
+ {{- end }}
+
+
+ {{- end }}
+ {{- end }}
+
+ {{- with $m.returns }}
+
+
Returns
+
{{ $page.RenderString . }}
+
+ {{- end }}
+
+ {{- if $m.throws }}
+
+
Throws
+
+ {{- range $m.throws }}
+ {{ if .url }}{{ .name }} {{ else }}{{ .name }}{{ end }}
+ {{ with .doc }}{{ $page.RenderString . }}{{ end }}
+ {{- end }}
+
+
+ {{- end }}
+
+ {{- if $m.seeAlso }}
+
+
See also
+
+ {{- range $m.seeAlso }}
+
+ {{- if .url }}{{ .label }}
+ {{- else if .prose }}{{ $page.RenderString .label }}
+ {{- else }}{{ .label }}{{ end }}
+ {{- with .note }} {{ . }} {{ end -}}
+
+ {{- end }}
+
+
+ {{- end }}
+
diff --git a/docs/website/layouts/partials/javadoc/signature.html b/docs/website/layouts/partials/javadoc/signature.html
new file mode 100644
index 00000000000..1bec1d548a9
--- /dev/null
+++ b/docs/website/layouts/partials/javadoc/signature.html
@@ -0,0 +1,30 @@
+{{- /* A member's declaration, rendered as markup rather than as a code fence so
+ that every type in it links to its own page. Called with a member map.
+
+ Fields and executables are told apart by whether the doclet set a
+ "parameters" key: it always does for a method or constructor, including
+ one that takes none, and never does for a field. */ -}}
+
+ {{- with .modifiers }}{{ . }} {{ end -}}
+ {{- if isset . "parameters" -}}
+ {{- with .typeParameters }}{{ . }} {{ end -}}
+ {{- with .returnType }}{{ partial "javadoc/type-ref.html" . }} {{ end -}}
+ {{ .name }} (
+ {{- range $i, $p := .parameters -}}
+ {{- if $p.type.label -}}
+ {{- if $i }}, {{ end -}}
+ {{ partial "javadoc/type-ref.html" $p.type }} {{ $p.name }}
+ {{- end -}}
+ {{- end -}}
+ )
+ {{- with .declaredThrows }}
+ throws
+ {{- range $i, $t := . }}{{ if $i }}, {{ end }}{{ partial "javadoc/type-ref.html" $t }}{{ end -}}
+ {{- end }}
+ {{- with .defaultValue }} default {{ . }} {{ end -}}
+ {{- else -}}
+ {{- with .fieldType }}{{ partial "javadoc/type-ref.html" . }} {{ end -}}
+ {{ .name }}
+ {{- with .constant }} = {{ . }} {{ end -}}
+ {{- end -}}
+
diff --git a/docs/website/layouts/partials/javadoc/summary-table.html b/docs/website/layouts/partials/javadoc/summary-table.html
new file mode 100644
index 00000000000..7d5192dcfb1
--- /dev/null
+++ b/docs/website/layouts/partials/javadoc/summary-table.html
@@ -0,0 +1,22 @@
+{{- /* A summary table for one member group. Called with "rows", "title", "page". */ -}}
+{{- if .rows }}
+{{- $page := .page }}
+
+{{- end }}
diff --git a/docs/website/layouts/partials/javadoc/type-ref.html b/docs/website/layouts/partials/javadoc/type-ref.html
new file mode 100644
index 00000000000..90bded2ff96
--- /dev/null
+++ b/docs/website/layouts/partials/javadoc/type-ref.html
@@ -0,0 +1,5 @@
+{{- /* One type reference: linked when we publish a page for it, plain when we do not.
+ Called with a {label, url} map from the doclet. */ -}}
+{{- if and . .label -}}
+{{- if .url -}}{{ .label }} {{- else -}}{{ .label }} {{- end -}}
+{{- end -}}
diff --git a/maven/javadoc-hugo-doclet/pom.xml b/maven/javadoc-hugo-doclet/pom.xml
new file mode 100644
index 00000000000..1e8c90a7f2c
--- /dev/null
+++ b/maven/javadoc-hugo-doclet/pom.xml
@@ -0,0 +1,63 @@
+
+
+ 4.0.0
+
+ com.codenameone
+ codenameone-javadoc-hugo-doclet
+ 8.0-SNAPSHOT
+ Codename One Hugo JavaDoc Doclet
+ Renders the framework API documentation as Hugo content. A build-time tool, never shipped inside an application
+
+
+
+ 25
+ UTF-8
+ 5.9.3
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.jupiter.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.jupiter.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+
+
diff --git a/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/CommentRenderer.java b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/CommentRenderer.java
new file mode 100644
index 00000000000..7babe6d8005
--- /dev/null
+++ b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/CommentRenderer.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.doclet.hugo;
+
+import com.sun.source.doctree.DocTree;
+import com.sun.source.doctree.EntityTree;
+import com.sun.source.doctree.LinkTree;
+import com.sun.source.doctree.LiteralTree;
+import com.sun.source.doctree.RawTextTree;
+import com.sun.source.doctree.ReferenceTree;
+import com.sun.source.doctree.TextTree;
+import com.sun.source.util.DocTreePath;
+import com.sun.source.util.DocTrees;
+import java.util.List;
+import javax.lang.model.element.Element;
+
+/**
+ * Turns a documentation comment body into the markdown that goes into the page.
+ *
+ * For this codebase the job is mostly to get out of the way. 1972 of the 2024
+ * core sources use markdown documentation comments, and JDK 23 and later hand a
+ * markdown comment to a doclet as a single {@code RawTextTree} of kind
+ * {@link DocTree.Kind#MARKDOWN} holding the author's text verbatim. That text is
+ * copied straight through and rendered by the same goldmark that renders the
+ * rest of the site, so a code fence, a table or a list in a comment looks like a
+ * code fence, a table or a list anywhere else on the site. Deliberately no
+ * markdown library is involved here: a second implementation could only
+ * disagree with the one that actually renders the page.
+ *
+ *
What does need translating is the inline tags, which markdown has no
+ * spelling for. They are converted to markdown links against the real resolved
+ * element, so an unqualified {@code {@link #paintDirty()}} lands on the right
+ * page.
+ */
+final class CommentRenderer {
+
+ /** Resolves a documented element to its site URL, or null when it has no page. */
+ interface Links {
+ String url(Element target);
+ }
+
+ private final DocTrees trees;
+ private final Links links;
+
+ CommentRenderer(DocTrees trees, Links links) {
+ this.trees = trees;
+ this.links = links;
+ }
+
+ /**
+ * Renders a run of documentation nodes.
+ *
+ * @param nodes the nodes, typically a comment's full body or one block tag's content
+ * @param path the path of the enclosing comment, used to resolve references;
+ * references are left as plain text when this is null
+ * @return markdown
+ */
+ String render(List extends DocTree> nodes, DocTreePath path) {
+ StringBuilder out = new StringBuilder();
+ for (DocTree node : nodes) {
+ append(out, node, path);
+ }
+ return out.toString();
+ }
+
+ private void append(StringBuilder out, DocTree node, DocTreePath path) {
+ switch (node.getKind()) {
+ case MARKDOWN -> out.append(((RawTextTree) node).getContent());
+ case TEXT -> out.append(((TextTree) node).getBody());
+ // {@code} and {@literal} are both LiteralTree; only the kind differs.
+ case CODE -> appendCode(out, ((LiteralTree) node).getBody().getBody());
+ case LITERAL -> out.append(escape(((LiteralTree) node).getBody().getBody()));
+ case ENTITY -> out.append("&").append(((EntityTree) node).getName()).append(";");
+ case LINK -> appendLink(out, (LinkTree) node, path, true);
+ case LINK_PLAIN -> appendLink(out, (LinkTree) node, path, false);
+ // START_ELEMENT, END_ELEMENT and anything else a legacy comment can hold
+ // are passed through as written. Nothing in CodenameOne/src still uses a
+ // /** */ comment, so this is the tail: Ports/CLDC11 and whatever a future
+ // contributor writes by hand.
+ default -> out.append(node.toString());
+ }
+ }
+
+ /** Wraps text in enough backticks that its own backticks survive. */
+ private void appendCode(StringBuilder out, String body) {
+ // A {@code} spanning lines is a code block, and the javadoc idiom for one
+ // is
{@code ... } . Rendering it as an inline span put the whole
+ // example on one line between backticks.
+ if (body.indexOf('\n') >= 0) {
+ String fence = "```";
+ while (body.contains(fence)) {
+ fence += "`";
+ }
+ out.append('\n').append(fence).append('\n')
+ .append(body.strip())
+ .append('\n').append(fence).append('\n');
+ return;
+ }
+ int longest = 0;
+ int run = 0;
+ for (int i = 0; i < body.length(); i++) {
+ run = body.charAt(i) == '`' ? run + 1 : 0;
+ longest = Math.max(longest, run);
+ }
+ String fence = "`".repeat(longest + 1);
+ out.append(fence);
+ // A code span whose content starts or ends with a backtick needs padding
+ // spaces, which markdown strips again when it renders.
+ if (body.startsWith("`") || body.endsWith("`")) {
+ out.append(' ').append(body).append(' ');
+ } else {
+ out.append(body);
+ }
+ out.append(fence);
+ }
+
+ private void appendLink(StringBuilder out, LinkTree link, DocTreePath path, boolean code) {
+ ReferenceTree reference = link.getReference();
+ String label = render(link.getLabel(), path).strip();
+ if (label.isEmpty()) {
+ label = defaultLabel(reference);
+ }
+
+ String url = resolve(reference, path);
+ String text = code ? "`" + label + "`" : label;
+ if (url == null) {
+ out.append(text);
+ return;
+ }
+ out.append('[').append(text).append("](").append(markdownUrl(url)).append(')');
+ }
+
+ /** The URL a reference points at, or null when it resolves to nothing we publish. */
+ private String resolve(ReferenceTree reference, DocTreePath path) {
+ if (reference == null || path == null) {
+ return null;
+ }
+ DocTreePath referencePath = DocTreePath.getPath(path, reference);
+ if (referencePath == null) {
+ return null;
+ }
+ Element target = trees.getElement(referencePath);
+ return target == null ? null : links.url(target);
+ }
+
+ /**
+ * The text javadoc shows when a link carries no explicit label: the reference
+ * with its package qualifiers dropped and its leading {@code #} removed.
+ */
+ private static String defaultLabel(ReferenceTree reference) {
+ if (reference == null) {
+ return "";
+ }
+ String signature = reference.getSignature().strip();
+ int hash = signature.indexOf('#');
+ String type = hash < 0 ? signature : signature.substring(0, hash);
+ String member = hash < 0 ? "" : signature.substring(hash + 1);
+
+ // The type name starts at the first segment that begins with a capital,
+ // and everything from there is kept: Pose.Landmark is a nested type, not
+ // a package called Pose. Testing the character *before* each dot instead
+ // read "Pose" as a package segment because it ends in a lower case
+ // letter, and shortened Pose.Landmark.getName() to Landmark.getName().
+ String simpleType = type;
+ int cut = -1;
+ int segmentStart = 0;
+ for (int i = 0; i <= type.length(); i++) {
+ if (i == type.length() || type.charAt(i) == '.') {
+ if (segmentStart < type.length()
+ && Character.isUpperCase(type.charAt(segmentStart))) {
+ cut = segmentStart;
+ break;
+ }
+ segmentStart = i + 1;
+ } else if (type.charAt(i) == '(') {
+ break;
+ }
+ }
+ if (cut > 0) {
+ simpleType = type.substring(cut);
+ }
+
+ if (member.isEmpty()) {
+ return simpleType;
+ }
+ return simpleType.isEmpty() ? member : simpleType + "." + member;
+ }
+
+ /**
+ * Makes a URL safe to sit in a markdown link destination.
+ *
+ * A constructor's fragment is {@code #(...)}, and markdown reads
+ * angle brackets in a destination as a delimiter of its own, so
+ * {@code [x](/p/T.html#(int))} comes out mangled -- the one link on the
+ * site that pointed at a constructor from inside a comment arrived as
+ * {@code <init>}. Percent encoding them is transparent to the
+ * browser, which decodes a fragment before matching it against an id.
+ *
+ * Parentheses are deliberately left alone: they are balanced in every
+ * signature javadoc emits, and CommonMark allows balanced parentheses in a
+ * destination.
+ */
+ private static String markdownUrl(String url) {
+ return url.replace("<", "%3C").replace(">", "%3E").replace(" ", "%20");
+ }
+
+ /** Escapes the characters markdown would otherwise treat as syntax. */
+ private static String escape(String text) {
+ StringBuilder out = new StringBuilder(text.length());
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+ if ("\\`*_{}[]()#+-.!<>|~".indexOf(c) >= 0) {
+ out.append('\\');
+ }
+ out.append(c);
+ }
+ return out.toString();
+ }
+}
diff --git a/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/DocReader.java b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/DocReader.java
new file mode 100644
index 00000000000..fc8fe4d129b
--- /dev/null
+++ b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/DocReader.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.doclet.hugo;
+
+import com.sun.source.doctree.BlockTagTree;
+import com.sun.source.doctree.DeprecatedTree;
+import com.sun.source.doctree.DocCommentTree;
+import com.sun.source.doctree.DocTree;
+import com.sun.source.doctree.InheritDocTree;
+import com.sun.source.doctree.ParamTree;
+import com.sun.source.doctree.ReturnTree;
+import com.sun.source.doctree.SeeTree;
+import com.sun.source.doctree.ThrowsTree;
+import com.sun.source.util.DocTreePath;
+import com.sun.source.util.DocTrees;
+import com.sun.source.util.TreePath;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.ElementFilter;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/**
+ * Reads one element's documentation and merges the two ways this codebase
+ * expresses it.
+ *
+ *
Both forms are live: 816 {@code @param} tags against 9068
+ * {@code #### Parameters} headings, 1036 {@code @return} against 7224
+ * {@code #### Returns}. The markdown form is parsed by {@link MarkdownSections}
+ * and wins where the two overlap, because it is what the author most recently
+ * wrote; block tags fill in anything it did not cover.
+ *
+ *
Two tags are dropped rather than rendered. {@code @since} and the
+ * {@code #### Since} heading are dropped because Codename One does not publish
+ * availability metadata at all -- {@code scripts/check-since-tags.sh} fails the
+ * build over one in a source file, on the grounds that a guessed version is
+ * worse than no version. {@code @hidden} is not dropped but obeyed: the element
+ * disappears from the output entirely.
+ */
+final class DocReader {
+
+ /** Guards against a cycle in overriding chains while resolving inherited docs. */
+ private static final int MAX_INHERIT_DEPTH = 16;
+
+ private final DocTrees trees;
+ private final Elements elements;
+ private final Types types;
+ private final CommentRenderer renderer;
+
+ /**
+ * Memo of everything already read.
+ *
+ *
Not an optimisation of last resort: every element is read at least twice
+ * over -- once to decide whether {@code @hidden} keeps it out of the index,
+ * again to render it, and once more for each summary row that quotes it --
+ * and reading a method walks its whole supertype chain looking for the
+ * declaration it inherits from. Without this the generator re-walks the
+ * hierarchy of roughly 1850 types several times each.
+ */
+ private final Map cache = new HashMap<>();
+
+ DocReader(DocTrees trees, Elements elements, Types types, CommentRenderer renderer) {
+ this.trees = trees;
+ this.elements = elements;
+ this.types = types;
+ this.renderer = renderer;
+ }
+
+ ElementDoc read(Element element) {
+ ElementDoc cached = cache.get(element);
+ if (cached != null) {
+ return cached;
+ }
+ ElementDoc doc = read(element, 0);
+ cache.put(element, doc);
+ return doc;
+ }
+
+ private ElementDoc read(Element element, int depth) {
+ ElementDoc doc = new ElementDoc();
+ DocCommentTree comment = trees.getDocCommentTree(element);
+
+ if (comment == null) {
+ // An undocumented override still documents itself through its parent,
+ // which is the behaviour every Java developer expects from javadoc.
+ ElementDoc inherited = inherit(element, depth);
+ ElementDoc result = inherited == null ? doc : adoptParameterNames(element, inherited);
+ markAnnotationDeprecation(element, result);
+ return result;
+ }
+
+ DocTreePath path = pathOf(element, comment);
+ // Goldmark drops raw HTML rather than rendering it, so the leftovers in
+ // the comments that were converted to markdown have to be dealt with
+ // before anything else reads the body.
+ doc.description = LegacyHtml.convert(renderer.render(comment.getFullBody(), path));
+
+ MarkdownSections.Result sections =
+ MarkdownSections.parse(doc.description, element instanceof ExecutableElement);
+ doc.description = sections.description();
+ doc.parameters.addAll(sections.parameters());
+ doc.exceptions.addAll(sections.exceptions());
+ doc.seeAlso.addAll(sections.seeAlso());
+ doc.returns = sections.returns();
+ if (sections.deprecated() != null) {
+ doc.deprecated = true;
+ doc.deprecatedText = sections.deprecated();
+ }
+
+ readBlockTags(element, comment, path, doc);
+ markAnnotationDeprecation(element, doc);
+ resolveInheritDoc(element, doc, depth);
+ return doc;
+ }
+
+ /**
+ * Re-labels an inherited doc with this element's own parameter names.
+ *
+ * An override with no comment of its own takes its parent's whole
+ * documentation, and the parent named the parameters as it saw fit.
+ * {@code GridBagLayout.addLayoutComponent} calls its first parameter
+ * "constraints" where {@code Layout} calls it "value", so the page looked up
+ * "constraints", found nothing, and printed "Not documented" beside it while
+ * the other two inherited normally.
+ *
+ *
Copies rather than edits: the doc handed back belongs to the parent and
+ * is its own cached answer.
+ */
+ private ElementDoc adoptParameterNames(Element element, ElementDoc inherited) {
+ if (!(element instanceof ExecutableElement executable)) {
+ return inherited;
+ }
+ ExecutableElement overridden = findOverridden(executable);
+ if (overridden == null) {
+ return inherited;
+ }
+ List extends VariableElement> mine = executable.getParameters();
+ List extends VariableElement> theirs = overridden.getParameters();
+
+ ElementDoc copy = new ElementDoc();
+ copy.description = inherited.description;
+ copy.returns = inherited.returns;
+ // Deprecation is deliberately NOT carried over. Java does not inherit
+ // @Deprecated and neither do the standard pages: ScaleImageLabel's
+ // setPreferredH and setPreferredW carry only @Override, and taking the
+ // parent's flag with its prose put a Deprecated banner on both. The
+ // override's own tag or annotation decides, and markAnnotationDeprecation
+ // runs after this.
+ copy.hidden = inherited.hidden;
+ copy.exceptions.addAll(inherited.exceptions);
+ copy.seeAlso.addAll(inherited.seeAlso);
+
+ for (int i = 0; i < mine.size(); i++) {
+ String name = mine.get(i).getSimpleName().toString();
+ String text = i < theirs.size()
+ ? inherited.parameterText(theirs.get(i).getSimpleName().toString())
+ : null;
+ if (text == null) {
+ text = inherited.parameterText(name);
+ }
+ if (text != null) {
+ copy.addParameter(new MarkdownSections.NamedText(name, text));
+ }
+ }
+ // Anything the parent documented that is not a parameter of this method
+ // -- a type parameter, most often -- carries over as written.
+ for (MarkdownSections.NamedText parameter : inherited.parameters) {
+ if (parameter.name().startsWith("<")) {
+ copy.addParameter(parameter);
+ }
+ }
+ return copy;
+ }
+
+ /**
+ * Marks an element deprecated because it is annotated, tag or no tag.
+ *
+ *
{@code @Deprecated} and {@code @deprecated} are independent: the
+ * annotation is what the compiler warns on, the tag is what explains it, and
+ * an API may carry either. 23 files here carry the annotation, and
+ * {@code com.codename1.ui.util.MutableResouce} carries it with no tag at all,
+ * so reading only the documentation lost its deprecated marking entirely
+ * while the standard pages showed it.
+ *
+ *
Only ever sets the flag. A comment that documented a deprecation keeps
+ * whatever text it gave.
+ *
+ *
Note the site marks 299 more members deprecated than the standard pages
+ * do, and that is correct rather than a leak. Those carry a
+ * {@code #### Deprecated} section, which the standard doclet renders as an
+ * ordinary heading inside the description because it cannot see the
+ * convention -- the same reason it shows no parameter tables. Measured: every
+ * one of the 299 has deprecation text, and none is an undocumented override
+ * inheriting the flag from its parent.
+ */
+ private void markAnnotationDeprecation(Element element, ElementDoc doc) {
+ if (elements.isDeprecated(element)) {
+ doc.deprecated = true;
+ }
+ }
+
+ private void readBlockTags(Element element, DocCommentTree comment, DocTreePath path,
+ ElementDoc doc) {
+ for (DocTree tag : comment.getBlockTags()) {
+ switch (tag.getKind()) {
+ case PARAM -> {
+ ParamTree param = (ParamTree) tag;
+ // Type parameter documentation has no column in the rendered
+ // signature table, so it is folded into the description rather
+ // than silently dropped.
+ String name = param.getName().getName().toString();
+ String text = takeTrailingSections(
+ renderer.render(param.getDescription(), path), element, doc);
+ doc.addParameter(new MarkdownSections.NamedText(
+ param.isTypeParameter() ? "<" + name + ">" : name, text));
+ }
+ case RETURN -> {
+ String text = takeTrailingSections(
+ renderer.render(((ReturnTree) tag).getDescription(), path), element, doc);
+ if (doc.returns == null && !text.isEmpty()) {
+ doc.returns = text;
+ }
+ }
+ case THROWS, EXCEPTION -> {
+ ThrowsTree thrown = (ThrowsTree) tag;
+ doc.addException(new MarkdownSections.NamedText(
+ thrown.getExceptionName().getSignature(),
+ takeTrailingSections(
+ renderer.render(thrown.getDescription(), path), element, doc)));
+ }
+ case SEE -> {
+ String text = renderer.render(((SeeTree) tag).getReference(), path).strip();
+ if (!text.isEmpty()) {
+ doc.seeAlso.add(text);
+ }
+ }
+ case DEPRECATED -> {
+ doc.deprecated = true;
+ // The house convention writes @deprecated under a
+ // "#### Deprecated" heading, and anything that follows --
+ // "#### See also" and its bullets -- is still inside the tag
+ // as far as the JDK is concerned. Storing the body whole put
+ // those headings inside the deprecation banner and left the
+ // references unresolved: CellRenderer and
+ // ImageDownloadService both lost their See-also entirely.
+ String text = takeTrailingSections(
+ renderer.render(((DeprecatedTree) tag).getBody(), path), element, doc);
+ if (!text.isEmpty()) {
+ doc.deprecatedText = text;
+ }
+ }
+ case HIDDEN -> doc.hidden = true;
+ default -> readUnknownTag(tag, path, doc);
+ }
+ }
+ }
+
+ /**
+ * Splits the sections a block tag's body ran on into, and returns what is
+ * genuinely the tag's own text.
+ *
+ *
The house convention writes a block tag underneath a heading of the
+ * same name, and everything after it -- another heading and its bullets --
+ * is still inside that tag as far as the JDK is concerned. Storing a body
+ * whole therefore swallows whatever followed it: 205 methods rendered a
+ * "#### Throws" heading and its bullet inside their Returns text and emitted
+ * no exception row at all, com.codename1.ui.CN.requestFullScreen among them.
+ *
+ *
Applied to every tag with a body rather than to @deprecated alone,
+ * which is where this was first noticed and fixed one tag too narrowly.
+ */
+ private String takeTrailingSections(String body, Element element, ElementDoc doc) {
+ MarkdownSections.Result split =
+ MarkdownSections.parse(body, element instanceof ExecutableElement);
+ doc.seeAlso.addAll(split.seeAlso());
+ for (MarkdownSections.NamedText parameter : split.parameters()) {
+ doc.addParameter(parameter);
+ }
+ for (MarkdownSections.NamedText thrown : split.exceptions()) {
+ doc.addException(thrown);
+ }
+ if (doc.returns == null && split.returns() != null) {
+ doc.returns = split.returns();
+ }
+ if (split.deprecated() != null) {
+ doc.deprecated = true;
+ if (doc.deprecatedText.isEmpty()) {
+ doc.deprecatedText = split.deprecated();
+ }
+ }
+ return split.description().strip();
+ }
+
+ /**
+ * Tags with no case of their own.
+ *
+ *
Silence here loses content. This repository writes its own
+ * {@code @warning} tag -- 22 of them, and they are safety notes:
+ * {@code Body.createFixture} warns that the function is locked during
+ * callbacks -- and every one was being dropped on the floor because the tag
+ * is not one javadoc knows.
+ *
+ *
Only the tags that are deliberately not published are discarded, and
+ * they are named. Anything else keeps its text.
+ */
+ private void readUnknownTag(DocTree tag, DocTreePath path, ElementDoc doc) {
+ if (!(tag instanceof BlockTagTree named)) {
+ return;
+ }
+ String name = named.getTagName();
+ if ("hidden".equals(name)) {
+ doc.hidden = true;
+ return;
+ }
+ // @author and @version are not published, and @since is rejected in
+ // sources outright by scripts/check-since-tags.sh: a guessed version is
+ // worse than none. Serial tags describe a mechanism this toolkit has no
+ // support for at all.
+ if (DROPPED_TAGS.contains(name)) {
+ return;
+ }
+
+ String body = tag.toString();
+ int at = body.indexOf('@');
+ if (at >= 0) {
+ int space = body.indexOf(' ', at);
+ body = space < 0 ? "" : body.substring(space + 1);
+ }
+ body = body.strip();
+ if (body.isEmpty()) {
+ return;
+ }
+ if ("warning".equals(name)) {
+ doc.warnings.add(body);
+ } else {
+ // Keep it visible rather than lose it; the page has no better place.
+ doc.description = doc.description.isBlank()
+ ? body
+ : doc.description + "\n\n" + body;
+ }
+ }
+
+ /** Tags this generator publishes nowhere, on purpose. */
+ private static final java.util.Set DROPPED_TAGS = java.util.Set.of(
+ "author", "version", "since", "serial", "serialData", "serialField");
+
+ /**
+ * Fills in whatever the element left to its parent.
+ *
+ * Covers both spellings of the same intent: an explicit
+ * {@code {@inheritDoc}} in the description, and a description, return or
+ * parameter the override simply did not write.
+ */
+ private void resolveInheritDoc(Element element, ElementDoc doc, int depth) {
+ boolean wantsDescription = containsInheritDoc(element) || doc.description.isBlank();
+ // A marker inside a structured section is a request too. An override that
+ // writes "#### Returns" with {@inheritDoc} under it leaves doc.returns
+ // non-null, so testing only for null published the marker itself --
+ // BubbleTransition.copy and FlipTransition.copy both showed a literal
+ // {@inheritDoc} where the parent's text belonged.
+ boolean wantsDetail = doc.returns == null
+ || isInheritDoc(doc.returns)
+ || hasUndocumentedParameter(element, doc);
+ if (!wantsDescription && !wantsDetail) {
+ return;
+ }
+
+ ElementDoc parent = inherit(element, depth);
+ if (parent == null) {
+ return;
+ }
+
+ if (wantsDescription) {
+ if (doc.description.isBlank()) {
+ doc.description = parent.description;
+ } else {
+ doc.description = doc.description.replace(INHERIT_DOC_MARKER, parent.description);
+ }
+ }
+ if (doc.returns == null || isInheritDoc(doc.returns)) {
+ doc.returns = parent.returns;
+ }
+ if (element instanceof ExecutableElement executable) {
+ // Paired by position, not by name. An override is free to rename a
+ // parameter, and looking the parent's text up under the child's name
+ // then finds nothing: GridBagLayout.addLayoutComponent calls its
+ // first parameter "constraints" where Layout calls it "value", and
+ // that one parameter came out undocumented while the rest inherited.
+ ExecutableElement overridden = findOverridden(executable);
+ List extends VariableElement> parameters = executable.getParameters();
+ for (int i = 0; i < parameters.size(); i++) {
+ String name = parameters.get(i).getSimpleName().toString();
+ String own = doc.parameterText(name);
+ if (own != null && !isInheritDoc(own)) {
+ continue;
+ }
+ String inherited = null;
+ if (overridden != null && i < overridden.getParameters().size()) {
+ inherited = parent.parameterText(
+ overridden.getParameters().get(i).getSimpleName().toString());
+ }
+ if (inherited == null) {
+ inherited = parent.parameterText(name);
+ }
+ final String finalName = name;
+ if (inherited != null) {
+ doc.parameters.removeIf(existing -> existing.name().equals(finalName));
+ doc.addParameter(new MarkdownSections.NamedText(name, inherited));
+ } else if (own != null) {
+ // Nothing to inherit: drop the marker rather than publish it.
+ doc.parameters.removeIf(existing -> existing.name().equals(finalName));
+ }
+ }
+ }
+ if (doc.returns != null && isInheritDoc(doc.returns)) {
+ doc.returns = null;
+ }
+ for (MarkdownSections.NamedText exception : parent.exceptions) {
+ doc.addException(exception);
+ }
+ }
+
+ private boolean hasUndocumentedParameter(Element element, ElementDoc doc) {
+ if (!(element instanceof ExecutableElement executable)) {
+ return false;
+ }
+ for (var parameter : executable.getParameters()) {
+ if (doc.parameterText(parameter.getSimpleName().toString()) == null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * The rendered form of {@code {@inheritDoc}}.
+ *
+ *
{@link CommentRenderer} has no element context, so it renders the tag
+ * through its default branch as its own source text. That text is the marker
+ * this class substitutes into, which keeps the renderer free of any
+ * inheritance knowledge.
+ */
+ private static final String INHERIT_DOC_MARKER = "{@inheritDoc}";
+
+ /** Whether a documented value is nothing but an inherit marker. */
+ private static boolean isInheritDoc(String text) {
+ return text != null && text.strip().equals(INHERIT_DOC_MARKER);
+ }
+
+ private boolean containsInheritDoc(Element element) {
+ DocCommentTree comment = trees.getDocCommentTree(element);
+ if (comment == null) {
+ return false;
+ }
+ for (DocTree node : comment.getFullBody()) {
+ if (node instanceof InheritDocTree) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** The documentation of the method this one overrides, or null when there is none. */
+ private ElementDoc inherit(Element element, int depth) {
+ if (depth >= MAX_INHERIT_DEPTH || !(element instanceof ExecutableElement method)) {
+ return null;
+ }
+ ExecutableElement overridden = findOverridden(method);
+ return overridden == null ? null : read(overridden, depth + 1);
+ }
+
+ /**
+ * The first method this one overrides, searching superclasses before
+ * interfaces, which is the order javadoc documents.
+ */
+ private ExecutableElement findOverridden(ExecutableElement method) {
+ TypeElement owner = Refs.enclosingType(method);
+ if (owner == null) {
+ return null;
+ }
+ List supertypes = new ArrayList<>();
+ collectSupertypes(owner.asType(), supertypes, new ArrayList<>());
+ for (TypeElement supertype : supertypes) {
+ for (ExecutableElement candidate : ElementFilter.methodsIn(supertype.getEnclosedElements())) {
+ if (elements.overrides(method, candidate, owner)) {
+ return candidate;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void collectSupertypes(TypeMirror type, List out, List seen) {
+ for (TypeMirror supertype : types.directSupertypes(type)) {
+ if (!(supertype instanceof DeclaredType declared)
+ || !(declared.asElement() instanceof TypeElement element)) {
+ continue;
+ }
+ String name = element.getQualifiedName().toString();
+ if (seen.contains(name)) {
+ continue;
+ }
+ seen.add(name);
+ out.add(element);
+ collectSupertypes(supertype, out, seen);
+ }
+ }
+
+ /** The comment's path, needed to resolve references, or null when unavailable. */
+ private DocTreePath pathOf(Element element, DocCommentTree comment) {
+ TreePath treePath = trees.getPath(element);
+ return treePath == null ? null : new DocTreePath(treePath, comment);
+ }
+}
diff --git a/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/ElementDoc.java b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/ElementDoc.java
new file mode 100644
index 00000000000..7efb8146fd7
--- /dev/null
+++ b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/ElementDoc.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.doclet.hugo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** One element's documentation, after tags and markdown sections have been merged. */
+final class ElementDoc {
+
+ String description = "";
+ final List parameters = new ArrayList<>();
+ final List exceptions = new ArrayList<>();
+ final List seeAlso = new ArrayList<>();
+ /** Bodies of {@code @warning}, the repository's own safety-note tag. */
+ final List warnings = new ArrayList<>();
+ String returns;
+ boolean deprecated;
+ String deprecatedText = "";
+ boolean hidden;
+
+ boolean isEmpty() {
+ return description.isBlank() && parameters.isEmpty() && exceptions.isEmpty()
+ && seeAlso.isEmpty() && warnings.isEmpty() && returns == null && !deprecated;
+ }
+
+ /** Adds a parameter unless one of that name is already documented. */
+ void addParameter(MarkdownSections.NamedText parameter) {
+ for (MarkdownSections.NamedText existing : parameters) {
+ if (existing.name().equals(parameter.name())) {
+ return;
+ }
+ }
+ parameters.add(parameter);
+ }
+
+ /** Adds an exception unless one of that type is already documented. */
+ void addException(MarkdownSections.NamedText exception) {
+ for (MarkdownSections.NamedText existing : exceptions) {
+ if (existing.name().equals(exception.name())) {
+ return;
+ }
+ }
+ exceptions.add(exception);
+ }
+
+ /** The documented text for a parameter, or null when it is undocumented. */
+ String parameterText(String name) {
+ for (MarkdownSections.NamedText parameter : parameters) {
+ if (parameter.name().equals(name)) {
+ return parameter.text();
+ }
+ }
+ return null;
+ }
+}
diff --git a/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/HugoDoclet.java b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/HugoDoclet.java
new file mode 100644
index 00000000000..d1985bddaaa
--- /dev/null
+++ b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/HugoDoclet.java
@@ -0,0 +1,1724 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.doclet.hugo;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+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.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.ExecutableType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.ElementFilter;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+import jdk.javadoc.doclet.Doclet;
+import jdk.javadoc.doclet.DocletEnvironment;
+import jdk.javadoc.doclet.Reporter;
+
+/**
+ * Renders the Codename One API as Hugo content instead of as a standalone HTML
+ * site.
+ *
+ * The website used to embed the standard doclet's output by copying the whole
+ * generated tree into {@code static/}, prefixing every selector of javadoc's
+ * stylesheet with a scoping class, and fetching deep pages into a {@code div}
+ * with a script that faked {@code window.pathtoroot} and re-enabled the search
+ * box javadoc had disabled. Themed pages fought a stylesheet that was never
+ * built to be themed, so dark mode broke.
+ *
+ *
This doclet emits a page per type as a Hugo content file whose front matter
+ * is the API model and whose prose is markdown. The site's own templates render
+ * it, so the API pages are the same pages as the rest of the site -- same theme,
+ * same dark mode, same typography, same search. The standard doclet still runs
+ * alongside this one to produce the downloadable zip.
+ *
+ *
The generated URLs match the standard doclet's exactly, fragments included;
+ * see {@link Refs}.
+ */
+public final class HugoDoclet implements Doclet {
+
+ private Reporter reporter;
+ private Path contentRoot;
+ private Path searchIndex;
+
+ private DocletEnvironment environment;
+ private Elements elements;
+ private Types types;
+ private Refs refs;
+ private DocReader docReader;
+ private TypeNames typeNames;
+
+ /** Every type we publish a page for, by qualified name. */
+ private final Map documented = new LinkedHashMap<>();
+ /** Direct subtypes, keyed by the qualified name of the supertype. */
+ private final Map> subtypes = new LinkedHashMap<>();
+ /** Rows for the search index. */
+ private final List> searchRows = new ArrayList<>();
+
+ @Override
+ public void init(Locale locale, Reporter reporter) {
+ this.reporter = reporter;
+ }
+
+ @Override
+ public String getName() {
+ return "HugoDoclet";
+ }
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latest();
+ }
+
+ @Override
+ public Set extends Option> getSupportedOptions() {
+ return Set.of(
+ new SimpleOption("-d", "", "Hugo content directory to generate into",
+ value -> contentRoot = Path.of(value)),
+ new SimpleOption("--search-index", "", "JSON search index to write",
+ value -> searchIndex = Path.of(value)));
+ }
+
+ @Override
+ public boolean run(DocletEnvironment environment) {
+ if (contentRoot == null) {
+ reporter.print(javax.tools.Diagnostic.Kind.ERROR, "-d is required");
+ return false;
+ }
+ this.environment = environment;
+ this.elements = environment.getElementUtils();
+ this.types = environment.getTypeUtils();
+ this.refs = new Refs(types);
+
+ CommentRenderer.Links links = this::urlOf;
+ this.typeNames = new TypeNames(links);
+ this.docReader = new DocReader(environment.getDocTrees(), elements, types,
+ new CommentRenderer(environment.getDocTrees(), links));
+
+ try {
+ index(environment);
+ for (TypeElement type : documented.values()) {
+ writeTypePage(type);
+ }
+ writePackagePages();
+ writeOverview();
+ writeSearchIndex();
+ } catch (IOException failure) {
+ reporter.print(javax.tools.Diagnostic.Kind.ERROR,
+ "failed to write Hugo content: " + failure + "\n" + stackTrace(failure));
+ return false;
+ }
+ return true;
+ }
+
+ // ---------------------------------------------------------------- indexing
+
+ /**
+ * Collects the types that get a page, before anything is rendered.
+ *
+ * Link resolution needs the whole set up front: a comment on the first type
+ * processed can reference the last, and a reference to a type we do not
+ * publish has to render as text rather than as a link into a 404.
+ */
+ private void index(DocletEnvironment environment) {
+ for (Element element : environment.getIncludedElements()) {
+ if (element instanceof TypeElement type) {
+ collectType(type);
+ }
+ }
+ for (TypeElement type : documented.values()) {
+ recordSubtypeEdges(type);
+ }
+ }
+
+ /**
+ * Records this type under its nearest published ancestors.
+ *
+ *
Stopping at a direct parent that happens to have no page breaks the
+ * graph: the package private AbstractVisionAnalyzer sits between
+ * VisionAnalyzer and all seven of its public implementations, and every one
+ * of them was invisible from the interface's page. An unpublished type is
+ * walked through rather than treated as the end of the line.
+ */
+ private void recordSubtypeEdges(TypeElement type) {
+ // directSupertypes() hands an interface java.lang.Object, which the
+ // language does not, so seeding from it made every interface a known
+ // subtype of Object: the Object page listed 1406 of them, AdCallback and
+ // OnUserEarnedRewardListener included. allSupertypes() already skips it.
+ Deque queue = new ArrayDeque<>(realSupertypes(type.asType()));
+ Set visited = new LinkedHashSet<>();
+ while (!queue.isEmpty()) {
+ TypeMirror supertype = queue.removeFirst();
+ if (!(supertype instanceof DeclaredType declared)
+ || !(declared.asElement() instanceof TypeElement parent)
+ || !visited.add(parent.getQualifiedName().toString())) {
+ continue;
+ }
+ if (documented.containsKey(parent.getQualifiedName().toString())) {
+ subtypes.computeIfAbsent(parent.getQualifiedName().toString(),
+ key -> new ArrayList<>()).add(type);
+ } else {
+ // Filtered at every hop, not only the first. HTMLCallback reaches
+ // Object through the package private CSSParserCallback, so
+ // filtering the seed alone still left it a subtype of Object.
+ queue.addAll(realSupertypes(supertype));
+ }
+ }
+ }
+
+ /**
+ * The supertypes a type actually has.
+ *
+ * {@code directSupertypes()} hands an interface {@code java.lang.Object},
+ * which the language does not: an interface extends only its
+ * superinterfaces. Seeding the subtype graph from that made every interface
+ * a known subtype of Object, and the Object page listed 1406 of them.
+ *
+ *
Filtered at every hop rather than only at the seed, because an
+ * interface reaches Object through an unpublished one it is walked through:
+ * com.codename1.ui.html.HTMLCallback survived the first attempt that way.
+ *
+ *
There is no unit test for this. java.lang.Object comes from
+ * Ports/CLDC11 and cannot be part of the doclet's own fixture, so the edge
+ * it is about is never recorded there and any assertion passes with the fix
+ * removed. The evidence is the full run: Object's known subtypes went from
+ * 1406 to 1089, and the number of them that are interfaces from 314 to 0.
+ */
+ private List realSupertypes(TypeMirror type) {
+ boolean isInterface = type instanceof DeclaredType declared
+ && isInterfaceLike(declared.asElement());
+ // An annotation type has no supertypes worth publishing. It implicitly
+ // extends java.lang.annotation.Annotation, and the standard reference
+ // says so nowhere: no superinterface line, no inherited members, no
+ // mention of Annotation at all on the page. Reporting it put four
+ // methods under "Inherited methods" that nobody calls that way.
+ if (type instanceof DeclaredType annotation
+ && annotation.asElement().getKind() == ElementKind.ANNOTATION_TYPE) {
+ return List.of();
+ }
+ List out = new ArrayList<>();
+ for (TypeMirror supertype : types.directSupertypes(type)) {
+ if (isInterface && supertype instanceof DeclaredType declared
+ && declared.asElement() instanceof TypeElement element
+ && element.getQualifiedName().contentEquals("java.lang.Object")) {
+ continue;
+ }
+ out.add(supertype);
+ }
+ return out;
+ }
+
+ /**
+ * Whether a type is an interface in the language's sense.
+ *
+ * ANNOTATION_TYPE is its own ElementKind and is not INTERFACE, so a bare
+ * comparison against INTERFACE quietly excludes every annotation: they were
+ * still listed as known subtypes of Object, 78 of them, and their pages
+ * claimed to inherit clone(), wait() and the rest. An annotation interface
+ * inherits none of that.
+ */
+ private static boolean isInterfaceLike(Element element) {
+ return element.getKind() == ElementKind.INTERFACE
+ || element.getKind() == ElementKind.ANNOTATION_TYPE;
+ }
+
+ private void collectType(TypeElement type) {
+ if (isHidden(type) || !isVisible(type)) {
+ return;
+ }
+ documented.put(type.getQualifiedName().toString(), type);
+ for (TypeElement nested : ElementFilter.typesIn(type.getEnclosedElements())) {
+ collectType(nested);
+ }
+ }
+
+ /**
+ * Whether an element carries {@code @hidden}, which removes it from the API
+ * entirely.
+ *
+ *
Reads the block tags directly rather than going through
+ * {@link DocReader}. Reading an element there renders its whole comment,
+ * which resolves the links in it -- and this runs during indexing, while the
+ * set of published types is still being filled in. The rendered text is then
+ * cached, so a reference to a type indexed later was frozen as an unlinked
+ * code span: com.codename1.annotations.AppIntent linked [IntentEntity] and
+ * not [IntentParam], purely because of the order the two were reached.
+ */
+ private boolean isHidden(Element element) {
+ return isHiddenByTag(element);
+ }
+
+ /**
+ * The same question asked straight off the comment's block tags.
+ *
+ *
{@link #urlOf} cannot go through {@link DocReader}: reading an element
+ * renders its comment, rendering resolves the links in it, and resolving a
+ * link calls back here. Two elements referring to each other is enough to
+ * recurse until the stack ends, and the whole generation dies with a
+ * StackOverflowError rather than a message anyone could act on. This reads
+ * the tags and renders nothing.
+ */
+ private boolean isHiddenByTag(Element element) {
+ com.sun.source.doctree.DocCommentTree comment =
+ environment.getDocTrees().getDocCommentTree(element);
+ if (comment == null) {
+ return false;
+ }
+ for (com.sun.source.doctree.DocTree tag : comment.getBlockTags()) {
+ if (tag.getKind() == com.sun.source.doctree.DocTree.Kind.HIDDEN) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Whether an element is part of the published API.
+ *
+ *
The generator runs javadoc with {@code -protected}, so javadoc has already
+ * filtered the source set; this is the belt to that braces, and it also keeps
+ * package private nested types out of an otherwise public type's page.
+ */
+ private static boolean isVisible(Element element) {
+ return element.getModifiers().contains(Modifier.PUBLIC)
+ || element.getModifiers().contains(Modifier.PROTECTED);
+ }
+
+ /** The page URL of an element, or null when it is not something we publish. */
+ private String urlOf(Element target) {
+ if (target == null) {
+ return null;
+ }
+ if (target instanceof PackageElement pkg) {
+ return environment.isIncluded(pkg) ? Refs.packageUrl(pkg) : null;
+ }
+ if (target instanceof TypeElement type) {
+ return documented.containsKey(type.getQualifiedName().toString())
+ ? Refs.typeUrl(type) : null;
+ }
+ TypeElement owner = Refs.enclosingType(target);
+ if (owner == null || !documented.containsKey(owner.getQualifiedName().toString())) {
+ return null;
+ }
+ // A reference can resolve to a member the page does not render, and a
+ // link to an anchor that was never written is a link that goes nowhere.
+ // The generator runs javadoc with -protected, so a {@link #position}
+ // landing on a private field -- com.codename1.maps.MarkerOptions has one
+ // beside a public method of the same name -- produced exactly that.
+ if (!isVisible(target) || isHiddenByTag(target)) {
+ return null;
+ }
+ List anchors = refs.anchors(target);
+ return Refs.typeUrl(owner) + "#" + anchors.get(0);
+ }
+
+ // ------------------------------------------------------------ type pages
+
+ private void writeTypePage(TypeElement type) throws IOException {
+ ElementDoc doc = docReader.read(type);
+ Map api = new LinkedHashMap<>();
+
+ api.put("kind", kindOf(type));
+ api.put("qualified", type.getQualifiedName().toString());
+ api.put("simple", Refs.nestedDisplayName(type));
+ // "public final enum E" and "public abstract annotation A" are not
+ // declarations Java would accept: final is implicit on an enum and
+ // abstract on an annotation, and the keyword is @interface.
+ api.put("modifiers", TypeNames.declarationModifiers(type));
+ api.put("keyword", keywordOf(type));
+ // @Target and @Retention define how an annotation may be used at all, and
+ // AppIntent published neither. Only annotations that ask to be documented
+ // are shown, which is the rule javadoc follows.
+ api.put("annotations", documentedAnnotations(type));
+ api.put("typeParameters", typeNames.typeParameters(type.getTypeParameters()));
+
+ PackageElement pkg = Refs.packageOf(type);
+ api.put("package", Map.of(
+ "name", pkg.getQualifiedName().toString(),
+ "url", Refs.packageUrl(pkg)));
+
+ api.put("inheritance", inheritanceChain(type));
+ api.put("interfaces", interfacesOf(type));
+ api.put("subclasses", subclassesOf(type));
+
+ api.put("deprecated", doc.deprecated);
+ api.put("deprecatedText", doc.deprecatedText);
+ api.put("description", doc.description);
+ api.put("warnings", List.copyOf(doc.warnings));
+ api.put("seeAlso", seeAlsoRefs(doc, type));
+ // Type parameter documentation has nowhere to sit in the declaration
+ // string, so it was being read and then dropped. Exactly one tag in the
+ // tree carries any text today (VisionCameraView); the other two are
+ // empty. Kept anyway, because silently discarding what an author wrote is
+ // the defect, not the size of it.
+ api.put("typeParameterDocs", typeParameterDocs(doc));
+
+ // javadoc documents a package private supertype's members on the visible
+ // subclass rather than dropping them, because the supertype has no page
+ // to link to. com.codename1.ads.InterstitialAd is the case here: it
+ // declares nothing itself and inherits everything from the package
+ // private AbstractFullScreenAd, so treating those as merely "inherited"
+ // left seven documented methods with no page anywhere on the site.
+ List hidden = undocumentedSupertypes(type);
+ List owned = new ArrayList<>(type.getEnclosedElements());
+ // A member is only promoted if no published ancestor already supplies it.
+ // A package private interface can sit behind a documented class that
+ // implements it, and then every descendant was being handed the
+ // interface's abstract methods as its own: GameSceneView declared
+ // "public abstract setup(GraphicsDevice)" though it is concrete and
+ // inherits GameView's implementation, which is what the standard page
+ // lists.
+ Set suppliedByDocumented = signaturesFromDocumentedSupertypes(type);
+ for (TypeElement supertype : hidden) {
+ for (Element member : supertype.getEnclosedElements()) {
+ // Only an ABSTRACT one is dropped. An abstract method promoted
+ // off an unpublished interface shows a concrete class declaring
+ // something it does not declare -- GameSceneView read
+ // "public abstract setup(GraphicsDevice)" while inheriting
+ // GameView's implementation. A concrete one is the
+ // implementation and has to stay: CompoundAnimation is
+ // unpublished and overrides flush(), so UIMutation would lose the
+ // method that actually runs.
+ if (member instanceof ExecutableElement method
+ && method.getModifiers().contains(Modifier.ABSTRACT)
+ && suppliedByDocumented.contains(signatureKey(method))) {
+ continue;
+ }
+ owned.add(member);
+ }
+ }
+
+ api.put("nested", nestedRows(type));
+ // An enum constant is a field in the model and not one on the page: the
+ // standard reference gives it its own summary and detail sections and no
+ // field summary at all, where this rendered "public static final
+ // BindAttr TEXT" under Fields.
+ List enumConstants = new ArrayList<>();
+ List plainFields = new ArrayList<>();
+ for (VariableElement field : ElementFilter.fieldsIn(owned)) {
+ (field.getKind() == ElementKind.ENUM_CONSTANT ? enumConstants : plainFields).add(field);
+ }
+ api.put("enumConstants", memberRows(enumConstants, type));
+ api.put("fields", memberRows(plainFields, type));
+ api.put("constructors", executableRows(
+ ElementFilter.constructorsIn(type.getEnclosedElements()), type));
+ api.put("methods", executableRows(dedupeBySignature(ElementFilter.methodsIn(owned)), type));
+ api.put("inherited", inheritedMembers(type, hidden));
+ // javadoc renders "Fields inherited from class X" beside the methods
+ // block. Without it a subtype such as Label showed none of Component's
+ // constants -- CENTER, TOP, the cursor values -- even though this page
+ // links references to them.
+ api.put("inheritedFields", inheritedFields(type, hidden));
+ // A public nested type is addressable through a subtype, and javadoc
+ // lists it: Dialog omitted Form.TabIterator entirely.
+ api.put("inheritedNested", inheritedNested(type, hidden));
+
+ Map frontMatter = new LinkedHashMap<>();
+ frontMatter.put("title", Refs.nestedDisplayName(type));
+ frontMatter.put("url", Refs.typeUrl(type));
+ frontMatter.put("description", TypeNames.summary(doc.description));
+ frontMatter.put("layout", "type");
+ // No alias: the .html spelling redirects here on its own, and the
+ // directory spelling IS this page now.
+ frontMatter.put("aliases", List.of());
+ frontMatter.put("javadoc", api);
+
+ write(contentRoot.resolve(Refs.typeContentPath(type)), Json.write(frontMatter));
+ addSearchRows(type, doc, owned);
+ }
+
+/**
+ * Every supertype we publish no page for, anywhere in the hierarchy.
+ *
+ * The walk continues through documented supertypes rather than stopping
+ * at them, because an undocumented ancestor can sit behind a documented one:
+ * the constants of the package private {@code CSSParserCallback} are
+ * inherited by the public {@code HTMLCallback} and again by
+ * {@code DefaultHTMLCallback}, and javadoc documents them on both, since
+ * there is no page anywhere in the chain for an "inherited from" link to
+ * point at. Only undocumented types are collected; a documented one is
+ * walked through and then listed as inherited.
+ */
+ private List undocumentedSupertypes(TypeElement type) {
+ List out = new ArrayList<>();
+ collectUndocumented(type.asType(), new LinkedHashSet<>(), out);
+ return out;
+ }
+
+ private void collectUndocumented(TypeMirror type, Set visited, List out) {
+ // Through the same filter as every other walk, so an interface cannot
+ // promote Object's members and an annotation promotes nothing at all.
+ for (TypeMirror supertype : realSupertypes(type)) {
+ if (!(supertype instanceof DeclaredType declared)
+ || !(declared.asElement() instanceof TypeElement element)
+ || !visited.add(element.getQualifiedName().toString())) {
+ continue;
+ }
+ if (!documented.containsKey(element.getQualifiedName().toString())) {
+ out.add(element);
+ }
+ collectUndocumented(supertype, visited, out);
+ }
+ }
+
+ /**
+ * The method signatures a published ancestor of this type already provides.
+ *
+ * Those are inherited, and the page says so in its inherited block. What
+ * they must not be is promoted a second time as the type's own members off
+ * some unpublished interface further up.
+ */
+ private Set signaturesFromDocumentedSupertypes(TypeElement type) {
+ Set out = new LinkedHashSet<>();
+ Set visited = new LinkedHashSet<>();
+ for (TypeMirror supertype : allSupertypes(type.asType(), visited)) {
+ if (!(supertype instanceof DeclaredType declared)
+ || !(declared.asElement() instanceof TypeElement element)
+ || !documented.containsKey(element.getQualifiedName().toString())) {
+ continue;
+ }
+ // Only a published CLASS actually supplies the member. A published
+ // interface merely declares it, and the implementation can still be
+ // on the unpublished class in between: the vision analysers get
+ // process() from the package private AbstractVisionAnalyzer while
+ // VisionAnalyzer only names it, and suppressing on the interface
+ // took the method off all seven pages.
+ if (isInterfaceLike(element)) {
+ continue;
+ }
+ for (ExecutableElement method : ElementFilter.methodsIn(element.getEnclosedElements())) {
+ // And only a CONCRETE one supplies anything. ComponentAnimation
+ // declares flush() abstract and the package private
+ // CompoundAnimation implements it, so UIMutation has to keep the
+ // promoted implementation: an abstract declaration higher up is
+ // not something a reader can call.
+ if (method.getModifiers().contains(Modifier.ABSTRACT)) {
+ continue;
+ }
+ out.add(signatureKey(method));
+ }
+ }
+ return out;
+ }
+
+ /** Keeps the first declaration of each signature, so an override wins over what it overrides. */
+ private List dedupeBySignature(List methods) {
+ Set seen = new LinkedHashSet<>();
+ List out = new ArrayList<>();
+ for (ExecutableElement method : methods) {
+ if (seen.add(signatureKey(method))) {
+ out.add(method);
+ }
+ }
+ return out;
+ }
+
+ /** The documented type parameters, as {@code } entries lifted by the reader. */
+ private List> typeParameterDocs(ElementDoc doc) {
+ List> out = new ArrayList<>();
+ for (MarkdownSections.NamedText parameter : doc.parameters) {
+ if (!parameter.name().startsWith("<") || parameter.text().isBlank()) {
+ continue;
+ }
+ Map row = new LinkedHashMap<>();
+ row.put("name", parameter.name());
+ row.put("doc", parameter.text());
+ out.add(row);
+ }
+ return out;
+ }
+
+ /** The declaration's annotations that carry {@code @Documented}. */
+ private List documentedAnnotations(TypeElement type) {
+ List out = new ArrayList<>();
+ for (javax.lang.model.element.AnnotationMirror mirror : type.getAnnotationMirrors()) {
+ Element annotation = mirror.getAnnotationType().asElement();
+ if (!(annotation instanceof TypeElement declared)) {
+ continue;
+ }
+ String name = declared.getQualifiedName().toString();
+ // Deprecation already has a banner of its own on the page.
+ if (name.equals("java.lang.Deprecated") || !isDocumented(declared)) {
+ continue;
+ }
+ // javadoc writes @Retention(CLASS); the mirror's toString is fully
+ // qualified, which buries the name it is worth showing.
+ out.add(mirror.toString().replace("@" + name, "@" + declared.getSimpleName()));
+ }
+ return out;
+ }
+
+ private static boolean isDocumented(TypeElement annotation) {
+ for (javax.lang.model.element.AnnotationMirror mirror : annotation.getAnnotationMirrors()) {
+ Element element = mirror.getAnnotationType().asElement();
+ if (element instanceof TypeElement declared
+ && declared.getQualifiedName().contentEquals("java.lang.annotation.Documented")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** The keyword a declaration of this type would actually use. */
+ private static String keywordOf(TypeElement type) {
+ return switch (type.getKind()) {
+ case INTERFACE -> "interface";
+ case ENUM -> "enum";
+ case ANNOTATION_TYPE -> "@interface";
+ case RECORD -> "record";
+ default -> "class";
+ };
+ }
+
+ private String kindOf(TypeElement type) {
+ return switch (type.getKind()) {
+ case INTERFACE -> "interface";
+ case ENUM -> "enum";
+ case ANNOTATION_TYPE -> "annotation";
+ case RECORD -> "record";
+ default -> "class";
+ };
+ }
+
+ /** Superclasses from the immediate parent outwards, the way javadoc stacks them. */
+ private List> inheritanceChain(TypeElement type) {
+ List> chain = new ArrayList<>();
+ TypeMirror current = type.getSuperclass();
+ Set seen = new LinkedHashSet<>();
+ while (current != null && current.getKind() == TypeKind.DECLARED) {
+ DeclaredType declared = (DeclaredType) current;
+ if (!(declared.asElement() instanceof TypeElement element)
+ || !seen.add(element.getQualifiedName().toString())) {
+ break;
+ }
+ // A package private implementation class cannot be named by a
+ // consumer and has no page, and the standard doclet leaves it out of
+ // the tree: PoseDetector's hierarchy is Object then PoseDetector, not
+ // Object then AbstractVisionAnalyzer then PoseDetector. The
+ // walk still goes through it, so the substitution it carries reaches
+ // the next visible ancestor.
+ if (documented.containsKey(element.getQualifiedName().toString())) {
+ chain.add(typeNames.reference(current));
+ }
+ // Step through the mirror rather than the declaration. Asking the
+ // element for its superclass answers with the type variables as
+ // declared, so the substitution is lost one level up and everything
+ // above it: com.codename1.io.Properties extends HashMap, and its ancestry read AbstractMap, naming variables
+ // that mean nothing there.
+ current = superclassOf(current);
+ }
+ java.util.Collections.reverse(chain);
+ return chain;
+ }
+
+ /** The superclass of a parameterized type, with its arguments substituted in. */
+ private TypeMirror superclassOf(TypeMirror type) {
+ for (TypeMirror supertype : realSupertypes(type)) {
+ if (supertype instanceof DeclaredType declared
+ && !isInterfaceLike(declared.asElement())) {
+ return supertype;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Every interface in the type's contract, the way the standard reference
+ * lists them under "All Implemented Interfaces".
+ *
+ * Reading only the declaration hides everything a class gets from its
+ * superclass: CheckBox declares none of its own, so it was published with no
+ * interfaces at all while it really carries ActionSource, TextHolder,
+ * SelectableIconHolder, Animation and more.
+ */
+ private List> interfacesOf(TypeElement type) {
+ List> out = new ArrayList<>();
+ Set seen = new LinkedHashSet<>();
+ Deque queue = new ArrayDeque<>(realSupertypes(type.asType()));
+ while (!queue.isEmpty()) {
+ TypeMirror supertype = queue.removeFirst();
+ if (!(supertype instanceof DeclaredType declared)
+ || !(declared.asElement() instanceof TypeElement element)) {
+ continue;
+ }
+ boolean isInterface = isInterfaceLike(element);
+ if (isInterface && !seen.add(element.getQualifiedName().toString())) {
+ continue;
+ }
+ // A package private interface is not part of a contract a consumer
+ // can use, has no page, and the standard reference leaves it out:
+ // GameView listed SpriteRenderer.Updatable and Ads listed
+ // CSSParserCallback, both unlinked. The traversal still goes through
+ // them, so their public superinterfaces are still found.
+ if (isInterface && documented.containsKey(element.getQualifiedName().toString())) {
+ out.add(typeNames.reference(supertype));
+ }
+ queue.addAll(realSupertypes(supertype));
+ }
+ out.sort(Comparator.comparing(row -> String.valueOf(row.get("label"))));
+ return out;
+ }
+
+ /**
+ * The subtypes a page advertises.
+ *
+ * Direct children for a class, which is javadoc's "Direct Known
+ * Subclasses", and the whole subtype graph for an interface, which is its
+ * "All Known Subinterfaces" and "All Known Implementing Classes". Listing
+ * only direct children of an interface hides most of what a reader is
+ * looking for: java.util.Collection named four types where the standard page
+ * names Deque, NavigableSet, ArrayList, Vector and the rest.
+ */
+ private List> subclassesOf(TypeElement type) {
+ List children = isInterfaceLike(type)
+ ? transitiveSubtypes(type)
+ : new ArrayList<>(subtypes.getOrDefault(type.getQualifiedName().toString(), List.of()));
+ children.sort(Comparator.comparing(child -> child.getQualifiedName().toString()));
+
+ List> out = new ArrayList<>();
+ for (TypeElement child : children) {
+ out.add(Map.of("label", Refs.nestedDisplayName(child), "url", Refs.typeUrl(child)));
+ }
+ return out;
+ }
+
+ private List transitiveSubtypes(TypeElement type) {
+ List out = new ArrayList<>();
+ Set seen = new LinkedHashSet<>();
+ Deque queue = new ArrayDeque<>(
+ subtypes.getOrDefault(type.getQualifiedName().toString(), List.of()));
+ while (!queue.isEmpty()) {
+ TypeElement next = queue.removeFirst();
+ if (!seen.add(next.getQualifiedName().toString())) {
+ continue;
+ }
+ out.add(next);
+ queue.addAll(subtypes.getOrDefault(next.getQualifiedName().toString(), List.of()));
+ }
+ return out;
+ }
+
+ private List> nestedRows(TypeElement type) {
+ List> out = new ArrayList<>();
+ for (TypeElement nested : ElementFilter.typesIn(type.getEnclosedElements())) {
+ if (!documented.containsKey(nested.getQualifiedName().toString())) {
+ continue;
+ }
+ ElementDoc doc = docReader.read(nested);
+ out.add(new LinkedHashMap<>(Map.of(
+ "name", Refs.nestedDisplayName(nested),
+ "url", Refs.typeUrl(nested),
+ "kind", kindOf(nested),
+ "summary", TypeNames.summary(doc.description))));
+ }
+ return out;
+ }
+
+ private List> memberRows(List fields, TypeElement owner) {
+ List> out = new ArrayList<>();
+ for (VariableElement field : fields) {
+ if (!isVisible(field)) {
+ continue;
+ }
+ ElementDoc doc = docReader.read(field);
+ if (doc.hidden) {
+ continue;
+ }
+ Map row = baseRow(field, doc, owner);
+ boolean isEnumConstant = field.getKind() == ElementKind.ENUM_CONSTANT;
+ row.put("enumConstant", isEnumConstant);
+ if (isEnumConstant) {
+ // "public static final BindAttr TEXT" is how the model spells it
+ // and not how anyone writes or reads it.
+ row.put("modifiers", "");
+ }
+ row.put("fieldType", isEnumConstant ? null : typeNames.reference(field.asType()));
+ Object constant = field.getConstantValue();
+ row.put("constant", Literals.of(constant));
+ out.add(row);
+ }
+ return out;
+ }
+
+ private List> executableRows(
+ List extends ExecutableElement> members, TypeElement owner) {
+ List> out = new ArrayList<>();
+ for (ExecutableElement member : members) {
+ if (!isVisible(member)) {
+ continue;
+ }
+ ElementDoc doc = docReader.read(member);
+ if (doc.hidden) {
+ continue;
+ }
+ Map row = baseRow(member, doc, owner);
+ // As a member of THIS type, not as declared. A promoted method comes
+ // from a supertype that may bind its type parameters here:
+ // PoseDetector promotes AbstractVisionAnalyzer.process and returns
+ // AsyncResource, though PoseDetector declares no T at all.
+ ExecutableType asMember = asMemberOf(owner, member);
+ row.put("typeParameters", typeNames.typeParameters(member.getTypeParameters()));
+ row.put("returnType", member.getKind() == ElementKind.CONSTRUCTOR
+ ? null
+ : typeNames.reference(asMember == null
+ ? member.getReturnType() : asMember.getReturnType()));
+ row.put("parameters", parameterRows(member, doc, asMember));
+ row.put("throws", throwsRows(member, doc));
+ // Only what the signature actually declares. The rows above also
+ // carry exceptions a comment documented without declaring, which
+ // belong in the prose but not in the declaration.
+ List> declaredThrows = new ArrayList<>();
+ for (TypeMirror thrown : asMember == null
+ ? member.getThrownTypes() : asMember.getThrownTypes()) {
+ declaredThrows.add(typeNames.reference(thrown));
+ }
+ row.put("declaredThrows", declaredThrows);
+ row.put("returns", doc.returns);
+ // An annotation element without its default reads as required when it
+ // is not: IntentParam.required() defaults to true and
+ // AppIntent.timeoutSeconds() to 20, and neither was shown anywhere.
+ javax.lang.model.element.AnnotationValue fallback = member.getDefaultValue();
+ row.put("defaultValue", fallback == null ? null : Literals.of(fallback.getValue()));
+ out.add(row);
+ }
+ return out;
+ }
+
+ private Map baseRow(Element member, ElementDoc doc, TypeElement owner) {
+ List anchors = refs.anchors(member);
+ Map row = new LinkedHashMap<>();
+ row.put("name", displayName(member, owner));
+ row.put("anchor", anchors.get(0));
+ // Javadoc answers to both the declared and the erased spelling of a
+ // signature containing a type variable, and links in the wild use both.
+ row.put("anchors", anchors);
+ row.put("modifiers", TypeNames.modifiers(member));
+ row.put("deprecated", doc.deprecated);
+ row.put("deprecatedText", doc.deprecatedText);
+ row.put("description", doc.description);
+ row.put("summary", TypeNames.summary(doc.description));
+ // C: the page being written, not the member's declaring type. A member
+ // promoted off a package private supertype is declared somewhere that has
+ // no page, so resolving its references against that type produced links
+ // into a file the generator deliberately never writes.
+ row.put("seeAlso", seeAlsoRefs(doc, owner));
+ row.put("warnings", List.copyOf(doc.warnings));
+ return row;
+ }
+
+ /**
+ * What a member is called on the page.
+ *
+ * {@code getSimpleName()} answers {@code } for a constructor, which
+ * is the JVM's name for it and not something to show a reader: every class
+ * page listed its constructors as {@code (String)}. The anchor keeps
+ * that spelling, because javadoc's fragment really is {@code ()}.
+ */
+ private static String displayName(Element member, TypeElement owner) {
+ if (member.getKind() != ElementKind.CONSTRUCTOR) {
+ return member.getSimpleName().toString();
+ }
+ TypeElement declaring = Refs.enclosingType(member);
+ return (declaring == null ? owner : declaring).getSimpleName().toString();
+ }
+
+ /** The member's type as seen through the type whose page this is, or null. */
+ private ExecutableType asMemberOf(TypeElement owner, ExecutableElement member) {
+ if (owner == null || !(owner.asType() instanceof DeclaredType declared)) {
+ return null;
+ }
+ try {
+ return (ExecutableType) types.asMemberOf(declared, member);
+ } catch (IllegalArgumentException notAMember) {
+ // Not reachable from this type after all; the declaration stands.
+ return null;
+ }
+ }
+
+ private List> parameterRows(ExecutableElement member, ElementDoc doc,
+ ExecutableType asMember) {
+ List> out = new ArrayList<>();
+ List extends VariableElement> parameters = member.getParameters();
+ List extends TypeMirror> substituted =
+ asMember == null ? null : asMember.getParameterTypes();
+ for (int i = 0; i < parameters.size(); i++) {
+ VariableElement parameter = parameters.get(i);
+ String name = parameter.getSimpleName().toString();
+ Map row = new LinkedHashMap<>();
+ row.put("name", name);
+ TypeMirror parameterType = substituted != null && i < substituted.size()
+ ? substituted.get(i) : parameter.asType();
+ Map reference = typeNames.reference(parameterType);
+ if (member.isVarArgs() && i == parameters.size() - 1) {
+ // The declared type is an array; the source spelling is an ellipsis.
+ String label = String.valueOf(reference.get("label"));
+ reference.put("label", label.endsWith("[]")
+ ? label.substring(0, label.length() - 2) + "..." : label);
+ }
+ row.put("type", reference);
+ String text = doc.parameterText(name);
+ row.put("doc", text == null ? "" : text);
+ out.add(row);
+ }
+ // Type parameter documentation has nowhere to sit in the signature table,
+ // so it is carried separately rather than dropped.
+ for (MarkdownSections.NamedText documented : doc.parameters) {
+ if (documented.name().startsWith("<")) {
+ Map row = new LinkedHashMap<>();
+ row.put("name", documented.name());
+ // Not Map.of: it rejects a null value and took the whole
+ // generation down with a NullPointerException the moment any
+ // method documented a type parameter. Nothing in the framework
+ // does today, which is the only reason this was not a crash.
+ Map noType = new LinkedHashMap<>();
+ noType.put("label", "");
+ noType.put("url", null);
+ row.put("type", noType);
+ row.put("doc", documented.text());
+ out.add(row);
+ }
+ }
+ return out;
+ }
+
+ private List> throwsRows(ExecutableElement member, ElementDoc doc) {
+ List> out = new ArrayList<>();
+ // Matched on the simple name as well as the written one: a Throws section
+ // routinely names java.io.IOException where the signature, having
+ // imported it, declares IOException. Comparing the strings as written
+ // listed the same exception twice, once with prose and once without.
+ Set named = new LinkedHashSet<>();
+ for (MarkdownSections.NamedText documented : doc.exceptions) {
+ named.add(documented.name());
+ named.add(simpleName(documented.name()));
+ Map row = new LinkedHashMap<>();
+ row.put("name", documented.name());
+ row.put("url", exceptionUrl(member, documented.name()));
+ row.put("doc", documented.text());
+ out.add(row);
+ }
+ // A declared exception with no prose still belongs in the throws list.
+ for (TypeMirror thrown : member.getThrownTypes()) {
+ String label = typeNames.label(thrown);
+ if (named.contains(label) || named.contains(simpleName(label))) {
+ continue;
+ }
+ Map row = new LinkedHashMap<>();
+ row.put("name", label);
+ row.put("url", typeNames.url(thrown));
+ row.put("doc", "");
+ out.add(row);
+ }
+ return out;
+ }
+
+ /**
+ * The page for an exception a Throws section names.
+ *
+ * The method's own throws clause is consulted first, because a simple
+ * name is ambiguous across the API and picking the first global match sends
+ * the reader somewhere else entirely: {@code java.text.Format.parseObject}
+ * declares {@code java.text.ParseException} and documents it as
+ * {@code ParseException}, which was resolving to
+ * {@code com.codename1.l10n.ParseException}.
+ */
+ private String exceptionUrl(ExecutableElement member, String name) {
+ String wanted = simpleName(name);
+ for (TypeMirror thrown : member.getThrownTypes()) {
+ if (thrown instanceof DeclaredType declared
+ && declared.asElement() instanceof TypeElement element
+ && (element.getQualifiedName().contentEquals(name)
+ || element.getSimpleName().contentEquals(wanted))) {
+ return documented.containsKey(element.getQualifiedName().toString())
+ ? Refs.typeUrl(element) : null;
+ }
+ }
+ return urlOfTypeNamed(name);
+ }
+
+ /** A signature with every parameter type reduced to its simple name. */
+ private static String simplifySignature(String signature) {
+ int open = signature.indexOf('(');
+ if (open < 0 || !signature.endsWith(")")) {
+ return signature;
+ }
+ String name = signature.substring(0, open);
+ String inside = signature.substring(open + 1, signature.length() - 1);
+ List parts = new ArrayList<>();
+ for (String part : inside.split(",")) {
+ String trimmed = part.strip();
+ if (!trimmed.isEmpty()) {
+ parts.add(simpleName(trimmed));
+ }
+ }
+ return name + "(" + String.join(",", parts) + ")";
+ }
+
+ /** The last segment of a dotted name. */
+ private static String simpleName(String name) {
+ int dot = name.lastIndexOf('.');
+ return dot < 0 ? name : name.substring(dot + 1);
+ }
+
+ /** Resolves a bare exception name from a markdown Throws bullet to a page. */
+ private String urlOfTypeNamed(String name) {
+ if (name.indexOf('.') > 0) {
+ TypeElement exact = documented.get(name);
+ return exact == null ? null : Refs.typeUrl(exact);
+ }
+ String suffix = "." + name;
+ for (Map.Entry entry : documented.entrySet()) {
+ if (entry.getKey().endsWith(suffix)) {
+ return Refs.typeUrl(entry.getValue());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * See-also entries, resolved to links where they name something we publish.
+ *
+ * These are markdown bullets rather than {@code @see} tags, so nothing has
+ * resolved them and {@link SeeAlsoRef} has to read the reference out of the
+ * text. Three shapes matter, in descending order of how often they occur:
+ * a local member ({@code #drawRoundRect}), a qualified member
+ * ({@code Display#supportsNativeImageCache()}) and a bare type name.
+ */
+ private List> seeAlsoRefs(ElementDoc doc, TypeElement context) {
+ List> out = new ArrayList<>();
+ for (String entry : doc.seeAlso) {
+ SeeAlsoRef reference = SeeAlsoRef.parse(entry);
+ Map row = new LinkedHashMap<>();
+ if (!reference.isReference()) {
+ // Prose, or a markdown link that is already a link. Flagged so the
+ // template renders it as markdown; wrapping it in a code span
+ // showed the nine MDN links as literal [text](url).
+ row.put("label", reference.label());
+ row.put("url", null);
+ row.put("note", "");
+ row.put("prose", true);
+ out.add(row);
+ continue;
+ }
+
+ TypeElement owner = reference.type().isEmpty()
+ ? context
+ : lookupType(reference.type(), context);
+ String url = null;
+ if (owner != null) {
+ url = reference.member().isEmpty()
+ ? Refs.typeUrl(owner)
+ : memberUrl(owner, reference);
+ }
+ row.put("label", reference.text());
+ row.put("url", url);
+ row.put("prose", false);
+ // The trailing sentence a third of these carry, kept beside the link
+ // rather than folded into it: it is prose about the reference, not
+ // part of the name being linked.
+ row.put("note", reference.label());
+ out.add(row);
+ }
+ return out;
+ }
+
+ /**
+ * A documented type named either fully or by its simple name.
+ *
+ * A simple name is ambiguous across the API and the first match in
+ * iteration order is not an answer: {@code List} is both
+ * {@code com.codename1.ui.List} and {@code java.util.List}, and
+ * {@code java.util.AbstractList} referring to {@code List#size} was being
+ * sent to the UI widget. The link resolves, so the internal link check
+ * cannot see it -- only reading the page shows it is the wrong class.
+ *
+ *
The package the reference was written in decides, which is what the
+ * language would do with an unqualified name.
+ */
+ private TypeElement lookupType(String name, TypeElement context) {
+ TypeElement exact = documented.get(name);
+ if (exact != null) {
+ return exact;
+ }
+ // What the source file imported, before any global guess. Without this
+ // the answer is whichever documented type with that simple name happens
+ // to come first in iteration order, and there are two called Style:
+ // com.codename1.ui.plaf.Style and the nested
+ // com.codename1.charts.compat.Paint.Style. Component and
+ // ComponentSelector reach the right one today, but by luck rather than
+ // by rule, and a reader following "Style#stripMarginAndPadding()" into
+ // an unrelated enum would have no way to tell.
+ TypeElement imported = importedType(context, name);
+ if (imported != null) {
+ return imported;
+ }
+ if (context != null) {
+ PackageElement pkg = Refs.packageOf(context);
+ if (pkg != null) {
+ TypeElement sibling = documented.get(pkg.getQualifiedName() + "." + name);
+ if (sibling != null) {
+ return sibling;
+ }
+ }
+ }
+ String suffix = "." + name;
+ for (Map.Entry entry : documented.entrySet()) {
+ if (entry.getKey().endsWith(suffix)) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+ /** The type this simple name refers to through the context file's imports. */
+ private TypeElement importedType(TypeElement context, String name) {
+ if (context == null) {
+ return null;
+ }
+ com.sun.source.util.TreePath path = environment.getDocTrees().getPath(context);
+ if (path == null) {
+ return null;
+ }
+ for (com.sun.source.tree.ImportTree imported : path.getCompilationUnit().getImports()) {
+ if (imported.isStatic()) {
+ continue;
+ }
+ String qualified = imported.getQualifiedIdentifier().toString();
+ if (qualified.endsWith("." + name)) {
+ TypeElement found = documented.get(qualified);
+ if (found != null) {
+ return found;
+ }
+ } else if (qualified.endsWith(".*")) {
+ TypeElement found =
+ documented.get(qualified.substring(0, qualified.length() - 1) + name);
+ if (found != null) {
+ return found;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * The URL of the member a reference names, matched as precisely as the
+ * reference allows.
+ *
+ * Name alone is not enough. {@code #clear(int)} against a type that
+ * declares {@code clear()} first would otherwise link to the wrong overload,
+ * which is worse than not linking at all: the reader follows it and lands on
+ * a method that is not the one the author meant. So an exact identifier match
+ * is tried first, then the argument count, and only a reference that wrote no
+ * parameter list at all falls back to the first member of that name.
+ */
+ private String memberUrl(TypeElement owner, SeeAlsoRef reference) {
+ // A reference is written against the type the reader is looking at, but
+ // the member is very often declared further up: "#CENTER" on Label means
+ // Component.CENTER, and "#getEditingDelegate()" on Picker likewise. Over
+ // the framework that is roughly half of the member references that named
+ // something real, so searching only the enclosing type leaves them dead.
+ List candidates = new ArrayList<>();
+ collectNamed(owner, reference.member(), candidates);
+ Set visited = new LinkedHashSet<>();
+ for (TypeMirror supertype : allSupertypes(owner.asType(), visited)) {
+ if (supertype instanceof DeclaredType declared
+ && declared.asElement() instanceof TypeElement parent) {
+ collectNamed(parent, reference.member(), candidates);
+ }
+ }
+ if (candidates.isEmpty()) {
+ return null;
+ }
+
+ if (reference.hasParameterList()) {
+ String wanted = reference.member() + "(" + String.join(",", reference.parameters()) + ")";
+ for (Element candidate : candidates) {
+ if (refs.anchors(candidate).contains(wanted)) {
+ return anchorUrl(owner, candidate);
+ }
+ // A reference may write the erased spelling where the anchor
+ // writes it fully qualified: Collection#toArray(Object[]) against
+ // toArray(java.lang.Object[]). The alias is already there; it was
+ // only ever compared as written.
+ for (String anchor : refs.anchors(candidate)) {
+ if (simplifySignature(anchor).equals(simplifySignature(wanted))) {
+ return anchorUrl(owner, candidate);
+ }
+ }
+ }
+ // The reference may spell the types simply where the identifier spells
+ // them fully -- Component#paintShadows(Graphics, int, int) against
+ // paintShadows(com.codename1.ui.Graphics,int,int) -- so compare the
+ // types by their simple names.
+ //
+ // Arity alone is not enough, and settling for it was worse than not
+ // linking at all: Vector#remove(Object) resolved to remove(int), and
+ // Arrays#sort(Object[], int, int) to the byte[] overload. Both send
+ // the reader to a method the author did not mean.
+ for (Element candidate : candidates) {
+ if (candidate instanceof ExecutableElement executable
+ && sameParameterTypes(executable, reference.parameters())) {
+ return anchorUrl(owner, candidate);
+ }
+ }
+ // A parameter list that matches no overload is a stale reference --
+ // Transform documents "#setScale()" and declares only the two and
+ // three argument forms. Linking to an arbitrary overload would hide
+ // that and send the reader to a method the author did not mean, so it
+ // stays unlinked, which is what the standard pages showed anyway.
+ return null;
+ }
+ return anchorUrl(owner, candidates.get(0));
+ }
+
+ /**
+ * Whether a method's parameters are the ones a reference names, compared by
+ * simple type name so that an imported spelling matches a qualified one.
+ */
+ private boolean sameParameterTypes(ExecutableElement method, List written) {
+ List extends VariableElement> parameters = method.getParameters();
+ if (parameters.size() != written.size()) {
+ return false;
+ }
+ for (int i = 0; i < parameters.size(); i++) {
+ String actual = simpleName(typeNames.label(parameters.get(i).asType()));
+ String wanted = simpleName(written.get(i).strip());
+ if (method.isVarArgs() && i == parameters.size() - 1) {
+ // The declaration is an array; the reference may write either.
+ actual = actual.endsWith("[]") ? actual.substring(0, actual.length() - 2) : actual;
+ wanted = wanted.endsWith("...") ? wanted.substring(0, wanted.length() - 3)
+ : wanted.endsWith("[]") ? wanted.substring(0, wanted.length() - 2) : wanted;
+ }
+ if (!stripGenerics(actual).equals(stripGenerics(wanted))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** Drops a type argument list, which a reference may or may not write. */
+ private static String stripGenerics(String label) {
+ int open = label.indexOf('<');
+ if (open < 0) {
+ return label;
+ }
+ int close = label.lastIndexOf('>');
+ String tail = close >= 0 && close + 1 < label.length() ? label.substring(close + 1) : "";
+ return label.substring(0, open) + tail;
+ }
+
+ private void collectNamed(TypeElement type, String name, List out) {
+ for (Element member : type.getEnclosedElements()) {
+ if (isVisible(member) && !(member instanceof TypeElement)
+ && member.getSimpleName().contentEquals(name)) {
+ out.add(member);
+ }
+ }
+ }
+
+ /**
+ * The page a member is addressed on.
+ *
+ * An inherited member lives on the page of the type that declares it, the
+ * way javadoc links it. The exception is a member promoted off an
+ * undocumented supertype: that type has no page, so the member was rendered
+ * onto the referring type and is addressed there.
+ */
+ private String anchorUrl(TypeElement referring, Element member) {
+ TypeElement home = Refs.enclosingType(member);
+ String anchor = refs.anchors(member).get(0);
+ if (home != null && documented.containsKey(home.getQualifiedName().toString())) {
+ return Refs.typeUrl(home) + "#" + anchor;
+ }
+ return Refs.typeUrl(referring) + "#" + anchor;
+ }
+
+ /**
+ * Members a type gets from its supertypes, grouped by where they come from --
+ * the "Methods inherited from" blocks javadoc renders.
+ */
+ private List> inheritedMembers(TypeElement type, List promoted) {
+ List> out = new ArrayList<>();
+ Set declared = new LinkedHashSet<>();
+ // Bucketed by simple name so the override test below only ever compares
+ // methods that could possibly be the same one.
+ Map> seenByName = new LinkedHashMap<>();
+ for (ExecutableElement method : ElementFilter.methodsIn(type.getEnclosedElements())) {
+ declared.add(signatureKey(method));
+ remember(seenByName, method);
+ }
+ Set promotedNames = new LinkedHashSet<>();
+ for (TypeElement supertype : promoted) {
+ promotedNames.add(supertype.getQualifiedName().toString());
+ for (ExecutableElement method : ElementFilter.methodsIn(supertype.getEnclosedElements())) {
+ declared.add(signatureKey(method));
+ remember(seenByName, method);
+ }
+ }
+
+ Set visited = new LinkedHashSet<>();
+ for (TypeMirror supertype : allSupertypes(type.asType(), visited)) {
+ if (!(supertype instanceof DeclaredType declaredType)
+ || !(declaredType.asElement() instanceof TypeElement parent)) {
+ continue;
+ }
+ // Already rendered as this type's own members just above.
+ if (promotedNames.contains(parent.getQualifiedName().toString())) {
+ continue;
+ }
+ List> members = new ArrayList<>();
+ for (ExecutableElement method : ElementFilter.methodsIn(parent.getEnclosedElements())) {
+ if (!isVisible(method) || !declared.add(signatureKey(method))) {
+ continue;
+ }
+ // The erased signature is not enough once generics are
+ // substituted along the hierarchy: Enum.compareTo(E) erases to
+ // compareTo(java.lang.Enum) and Comparable.compareTo(T) to
+ // compareTo(java.lang.Object), so every enum listed compareTo
+ // twice even though the first implements the second.
+ if (overridesSomethingSeen(seenByName, method, type)) {
+ continue;
+ }
+ remember(seenByName, method);
+ Map row = new LinkedHashMap<>();
+ row.put("name", method.getSimpleName().toString());
+ String url = urlOf(method);
+ row.put("url", url);
+ members.add(row);
+ }
+ if (members.isEmpty()) {
+ continue;
+ }
+ Map group = new LinkedHashMap<>();
+ group.put("from", Refs.nestedDisplayName(parent));
+ group.put("url", documented.containsKey(parent.getQualifiedName().toString())
+ ? Refs.typeUrl(parent) : null);
+ group.put("members", members);
+ out.add(group);
+ }
+ return out;
+ }
+
+ /** The same grouping as {@link #inheritedMembers}, for nested types. */
+ private List> inheritedNested(TypeElement type, List promoted) {
+ List> out = new ArrayList<>();
+ Set declared = new LinkedHashSet<>();
+ for (TypeElement nested : ElementFilter.typesIn(type.getEnclosedElements())) {
+ declared.add(nested.getSimpleName().toString());
+ }
+ Set promotedNames = new LinkedHashSet<>();
+ for (TypeElement supertype : promoted) {
+ promotedNames.add(supertype.getQualifiedName().toString());
+ }
+
+ Set visited = new LinkedHashSet<>();
+ for (TypeMirror supertype : allSupertypes(type.asType(), visited)) {
+ if (!(supertype instanceof DeclaredType declaredType)
+ || !(declaredType.asElement() instanceof TypeElement parent)
+ || promotedNames.contains(parent.getQualifiedName().toString())) {
+ continue;
+ }
+ List> members = new ArrayList<>();
+ for (TypeElement nested : ElementFilter.typesIn(parent.getEnclosedElements())) {
+ if (!isVisible(nested) || !declared.add(nested.getSimpleName().toString())
+ || !documented.containsKey(nested.getQualifiedName().toString())) {
+ continue;
+ }
+ Map row = new LinkedHashMap<>();
+ row.put("name", Refs.nestedDisplayName(nested));
+ row.put("url", Refs.typeUrl(nested));
+ members.add(row);
+ }
+ if (members.isEmpty()) {
+ continue;
+ }
+ Map group = new LinkedHashMap<>();
+ group.put("from", Refs.nestedDisplayName(parent));
+ group.put("url", documented.containsKey(parent.getQualifiedName().toString())
+ ? Refs.typeUrl(parent) : null);
+ group.put("members", members);
+ out.add(group);
+ }
+ return out;
+ }
+
+ /** The same grouping as {@link #inheritedMembers}, for fields. */
+ private List> inheritedFields(TypeElement type, List promoted) {
+ List> out = new ArrayList<>();
+ Set declared = new LinkedHashSet<>();
+ for (VariableElement field : ElementFilter.fieldsIn(type.getEnclosedElements())) {
+ declared.add(field.getSimpleName().toString());
+ }
+ Set promotedNames = new LinkedHashSet<>();
+ for (TypeElement supertype : promoted) {
+ promotedNames.add(supertype.getQualifiedName().toString());
+ for (VariableElement field : ElementFilter.fieldsIn(supertype.getEnclosedElements())) {
+ declared.add(field.getSimpleName().toString());
+ }
+ }
+
+ Set visited = new LinkedHashSet<>();
+ for (TypeMirror supertype : allSupertypes(type.asType(), visited)) {
+ if (!(supertype instanceof DeclaredType declaredType)
+ || !(declaredType.asElement() instanceof TypeElement parent)
+ || promotedNames.contains(parent.getQualifiedName().toString())) {
+ continue;
+ }
+ List> members = new ArrayList<>();
+ for (VariableElement field : ElementFilter.fieldsIn(parent.getEnclosedElements())) {
+ if (!isVisible(field) || !declared.add(field.getSimpleName().toString())) {
+ continue;
+ }
+ Map row = new LinkedHashMap<>();
+ row.put("name", field.getSimpleName().toString());
+ row.put("url", urlOf(field));
+ members.add(row);
+ }
+ if (members.isEmpty()) {
+ continue;
+ }
+ Map group = new LinkedHashMap<>();
+ group.put("from", Refs.nestedDisplayName(parent));
+ group.put("url", documented.containsKey(parent.getQualifiedName().toString())
+ ? Refs.typeUrl(parent) : null);
+ group.put("members", members);
+ out.add(group);
+ }
+ return out;
+ }
+
+ private List allSupertypes(TypeMirror type, Set visited) {
+ List out = new ArrayList<>();
+ // types.directSupertypes() hands an interface java.lang.Object, which the
+ // language does not: an interface inherits none of its methods, and the
+ // standard pages list none. Without this an interface page such as
+ // SuccessCallback claimed ten inherited Object methods, protected
+ // clone() among them.
+ boolean isInterface = type instanceof DeclaredType declaredType
+ && isInterfaceLike(declaredType.asElement());
+ for (TypeMirror supertype : realSupertypes(type)) {
+ if (!(supertype instanceof DeclaredType declared)
+ || !(declared.asElement() instanceof TypeElement element)
+ || !visited.add(element.getQualifiedName().toString())) {
+ continue;
+ }
+ out.add(supertype);
+ out.addAll(allSupertypes(supertype, visited));
+ }
+ return out;
+ }
+
+ private static void remember(Map> seen, ExecutableElement method) {
+ seen.computeIfAbsent(method.getSimpleName().toString(), key -> new ArrayList<>()).add(method);
+ }
+
+ /** Whether a method already listed implements or overrides this one. */
+ private boolean overridesSomethingSeen(Map> seen,
+ ExecutableElement candidate, TypeElement type) {
+ for (ExecutableElement earlier : seen.getOrDefault(
+ candidate.getSimpleName().toString(), List.of())) {
+ if (elements.overrides(earlier, candidate, type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Name plus erased parameter types: what makes one method the same as another. */
+ private String signatureKey(ExecutableElement method) {
+ List anchors = refs.anchors(method);
+ return anchors.get(anchors.size() - 1);
+ }
+
+ // --------------------------------------------------------- package pages
+
+ private void writePackagePages() throws IOException {
+ Map> byPackage = new TreeMap<>();
+ for (TypeElement type : documented.values()) {
+ // Nested types are listed on their outer type's page, exactly as javadoc
+ // lists them, so only top level types get a row in the package summary.
+ if (type.getEnclosingElement() instanceof TypeElement) {
+ continue;
+ }
+ byPackage.computeIfAbsent(Refs.packageOf(type).getQualifiedName().toString(),
+ key -> new ArrayList<>()).add(type);
+ }
+
+ for (Map.Entry> entry : byPackage.entrySet()) {
+ PackageElement pkg = elements.getPackageElement(entry.getKey());
+ if (pkg == null) {
+ continue;
+ }
+ ElementDoc doc = docReader.read(pkg);
+ List members = entry.getValue();
+ members.sort(Comparator.comparing(type -> type.getSimpleName().toString()));
+
+ List> rows = new ArrayList<>();
+ for (TypeElement type : members) {
+ rows.add(new LinkedHashMap<>(Map.of(
+ "name", Refs.nestedDisplayName(type),
+ "url", Refs.typeUrl(type),
+ "kind", kindOf(type),
+ "summary", TypeNames.summary(docReader.read(type).description))));
+ }
+
+ Map api = new LinkedHashMap<>();
+ api.put("kind", "package");
+ api.put("qualified", entry.getKey());
+ // A package can be deprecated, and com.codename1.ui.layouts.mig is:
+ // its comment warns not to rely on the integration in production.
+ // DocReader lifts that out of the description, so omitting the fields
+ // here dropped the warning off the page entirely.
+ api.put("deprecated", doc.deprecated);
+ api.put("deprecatedText", doc.deprecatedText);
+ api.put("description", doc.description);
+ api.put("types", rows);
+
+ Map frontMatter = new LinkedHashMap<>();
+ frontMatter.put("title", entry.getKey());
+ frontMatter.put("url", Refs.packageUrl(pkg));
+ frontMatter.put("description", TypeNames.summary(doc.description));
+ frontMatter.put("layout", "package");
+ // Deliberately no alias on the bare package directory. It would put an
+ // index.html in com/codename1/ui/list/, which is the same directory as
+ // the com.codename1.ui.List type page on a case insensitive filesystem,
+ // and one silently overwrote the other. Nothing links to a bare package
+ // directory anyway; the summary is what the guide and javadoc name.
+ frontMatter.put("aliases", List.of());
+ frontMatter.put("javadoc", api);
+
+ write(contentRoot.resolve(Refs.packageContentPath(pkg)), Json.write(frontMatter));
+ }
+ }
+
+ /** The API index at {@code /javadoc/}, listing every package. */
+ private void writeOverview() throws IOException {
+ Set packageNames = new java.util.TreeSet<>();
+ for (TypeElement type : documented.values()) {
+ packageNames.add(Refs.packageOf(type).getQualifiedName().toString());
+ }
+
+ List> rows = new ArrayList<>();
+ for (String name : packageNames) {
+ PackageElement pkg = elements.getPackageElement(name);
+ if (pkg == null) {
+ continue;
+ }
+ rows.add(new LinkedHashMap<>(Map.of(
+ "name", name,
+ "url", Refs.packageUrl(pkg),
+ "summary", TypeNames.summary(docReader.read(pkg).description))));
+ }
+
+ Map api = new LinkedHashMap<>();
+ api.put("kind", "overview");
+ api.put("packages", rows);
+ api.put("typeCount", documented.size());
+
+ Map frontMatter = new LinkedHashMap<>();
+ frontMatter.put("title", "API");
+ frontMatter.put("url", "/javadoc/");
+ frontMatter.put("description", "Codename One API reference");
+ frontMatter.put("layout", "overview");
+ // The website has linked the API from /api/ since 2015.
+ frontMatter.put("aliases", List.of("/api/"));
+ frontMatter.put("javadoc", api);
+
+ write(contentRoot.resolve("_index.md"), Json.write(frontMatter));
+ }
+
+ // -------------------------------------------------------------- search
+
+ private void addSearchRows(TypeElement type, ElementDoc doc, List owned) {
+ Map row = new LinkedHashMap<>();
+ row.put("n", Refs.nestedDisplayName(type));
+ row.put("p", Refs.packageOf(type).getQualifiedName().toString());
+ row.put("u", Refs.typeUrl(type));
+ row.put("k", kindOf(type));
+ // Plain text: the results list escapes what it is given rather than
+ // rendering it, which is right for a value that came out of a comment,
+ // so markdown left here is displayed as its own source.
+ row.put("s", TypeNames.plainSummary(doc.description));
+
+ // Members are two strings each -- the label to show and the fragment to
+ // jump to -- nested under their type rather than repeated as standalone
+ // rows. The flat form, with a URL, a package and a summary per member,
+ // came to 9.7MB over 31519 entries, which is not a file a browser should
+ // download to answer one search. Grouping removes the repetition and
+ // dropping member summaries removes the bulk; the type summaries stay,
+ // because those are what a result list actually shows.
+ // The same list the page renders, promoted members included. Scanning
+ // only the type's own elements left InterstitialAd searchable by its
+ // constructor alone, while its page showed load(), isLoaded() and show()
+ // at anchors nothing could find.
+ List members = new ArrayList<>();
+ Set seen = new LinkedHashSet<>();
+ for (Element member : owned) {
+ if (!isVisible(member) || member instanceof TypeElement) {
+ continue;
+ }
+ // Constructors are not inherited and the page renders only its own,
+ // so a promoted one is a search hit pointing at an fragment
+ // that does not exist: ComponentAnimation.UIMutation was offering
+ // CompoundAnimation's two constructors.
+ if (member.getKind() == ElementKind.CONSTRUCTOR
+ && !type.equals(Refs.enclosingType(member))) {
+ continue;
+ }
+ if (docReader.read(member).hidden) {
+ continue;
+ }
+ String anchor = refs.anchors(member).get(0);
+ if (!seen.add(anchor)) {
+ continue;
+ }
+ String label = member instanceof ExecutableElement executable
+ ? displayName(member, type) + "(" + parameterLabels(executable) + ")"
+ : member.getSimpleName().toString();
+ members.add(List.of(label, anchor));
+ }
+ row.put("m", members);
+ searchRows.add(row);
+ }
+
+ /**
+ * Parameter types as a reader would write them, for the search index.
+ *
+ * The last parameter of a varargs method is an array in the model, so
+ * asList(T... array) was offered as asList(T[]) -- and byte[]... as
+ * byte[][], which says something different. 1198 labels carried the array
+ * spelling and none carried an ellipsis.
+ */
+ private String parameterLabels(ExecutableElement executable) {
+ List parameters = new ArrayList<>(executable.getParameters());
+ List out = new ArrayList<>();
+ for (int i = 0; i < parameters.size(); i++) {
+ String label = typeNames.label(parameters.get(i).asType());
+ if (executable.isVarArgs() && i == parameters.size() - 1 && label.endsWith("[]")) {
+ label = label.substring(0, label.length() - 2) + "...";
+ }
+ out.add(label);
+ }
+ return String.join(", ", out);
+ }
+
+ private void writeSearchIndex() throws IOException {
+ if (searchIndex == null) {
+ return;
+ }
+ Map document = new LinkedHashMap<>();
+ document.put("generator", "HugoDoclet");
+ document.put("types", searchRows);
+ write(searchIndex, Json.writeCompact(document));
+
+ int memberCount = 0;
+ for (Map row : searchRows) {
+ Object members = row.get("m");
+ if (members instanceof List> list) {
+ memberCount += list.size();
+ }
+ }
+ reporter.print(javax.tools.Diagnostic.Kind.NOTE,
+ "Hugo javadoc: " + documented.size() + " types, "
+ + memberCount + " searchable members");
+ }
+
+ // --------------------------------------------------------------- output
+
+ private static void write(Path target, String content) throws IOException {
+ Path parent = target.getParent();
+ if (parent != null) {
+ Files.createDirectories(parent);
+ }
+ try {
+ Files.writeString(target, content, StandardCharsets.UTF_8);
+ } catch (IOException failure) {
+ throw new IOException("writing " + target + ": " + failure, failure);
+ }
+ }
+
+ /** A doclet option that takes exactly one argument. */
+ private record SimpleOption(String name, String parameters, String description,
+ java.util.function.Consumer action) implements Option {
+ @Override
+ public int getArgumentCount() {
+ return 1;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public Kind getKind() {
+ return Kind.STANDARD;
+ }
+
+ @Override
+ public List getNames() {
+ return List.of(name);
+ }
+
+ @Override
+ public String getParameters() {
+ return parameters;
+ }
+
+ @Override
+ public boolean process(String option, List arguments) {
+ action.accept(arguments.get(0));
+ return true;
+ }
+ }
+
+ static String stackTrace(Throwable failure) {
+ StringWriter text = new StringWriter();
+ failure.printStackTrace(new PrintWriter(text));
+ return text.toString();
+ }
+}
diff --git a/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/Json.java b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/Json.java
new file mode 100644
index 00000000000..24bc79b14c1
--- /dev/null
+++ b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/Json.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.doclet.hugo;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A minimal JSON writer, used to emit Hugo front matter.
+ *
+ * Hugo accepts front matter as TOML, YAML or JSON, and this generator emits
+ * JSON deliberately. Almost every value written here is prose lifted verbatim
+ * out of a documentation comment: it contains quotes, colons, backslashes,
+ * leading dashes, blank lines and occasionally something that reads as a YAML
+ * anchor. Escaping all of that correctly for YAML is a source of silent
+ * corruption, whereas JSON has exactly one escaping rule and no significant
+ * whitespace at all.
+ */
+final class Json {
+
+ private Json() {
+ }
+
+ static String write(Map value) {
+ StringBuilder out = new StringBuilder();
+ writeValue(out, value, 0);
+ out.append('\n');
+ return out.toString();
+ }
+
+ /**
+ * The same JSON with no indentation, for the search index.
+ *
+ * Front matter is indented because a person reads it while debugging a
+ * page. The search index is only ever read by a browser, and pretty printing
+ * tens of thousands of nested entries costs more bytes than the entries.
+ */
+ static String writeCompact(Map value) {
+ StringBuilder out = new StringBuilder();
+ writeValue(out, value, COMPACT);
+ return out.toString();
+ }
+
+ /** Depth sentinel meaning "emit no whitespace at all". */
+ private static final int COMPACT = Integer.MIN_VALUE;
+
+ private static void writeValue(StringBuilder out, Object value, int depth) {
+ if (value == null) {
+ out.append("null");
+ } else if (value instanceof Map, ?> map) {
+ writeObject(out, map, depth);
+ } else if (value instanceof List> list) {
+ writeArray(out, list, depth);
+ } else if (value instanceof Boolean || value instanceof Integer || value instanceof Long) {
+ out.append(value);
+ } else {
+ writeString(out, value.toString());
+ }
+ }
+
+ private static void writeObject(StringBuilder out, Map, ?> map, int depth) {
+ if (map.isEmpty()) {
+ out.append("{}");
+ return;
+ }
+ boolean compact = depth == COMPACT;
+ out.append(compact ? "{" : "{\n");
+ boolean first = true;
+ for (Map.Entry, ?> entry : map.entrySet()) {
+ if (!first) {
+ out.append(compact ? "," : ",\n");
+ }
+ first = false;
+ indent(out, depth + 1);
+ writeString(out, entry.getKey().toString());
+ out.append(compact ? ":" : ": ");
+ writeValue(out, entry.getValue(), compact ? COMPACT : depth + 1);
+ }
+ if (!compact) {
+ out.append('\n');
+ indent(out, depth);
+ }
+ out.append('}');
+ }
+
+ private static void writeArray(StringBuilder out, List> list, int depth) {
+ if (list.isEmpty()) {
+ out.append("[]");
+ return;
+ }
+ boolean compact = depth == COMPACT;
+ out.append(compact ? "[" : "[\n");
+ for (int i = 0; i < list.size(); i++) {
+ if (i > 0) {
+ out.append(compact ? "," : ",\n");
+ }
+ indent(out, depth + 1);
+ writeValue(out, list.get(i), compact ? COMPACT : depth + 1);
+ }
+ if (!compact) {
+ out.append('\n');
+ indent(out, depth);
+ }
+ out.append(']');
+ }
+
+ private static void indent(StringBuilder out, int depth) {
+ if (depth > 0) {
+ out.append(" ".repeat(depth));
+ }
+ }
+
+ private static void writeString(StringBuilder out, String value) {
+ out.append('"');
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ switch (c) {
+ case '"' -> out.append("\\\"");
+ case '\\' -> out.append("\\\\");
+ case '\n' -> out.append("\\n");
+ case '\r' -> out.append("\\r");
+ case '\t' -> out.append("\\t");
+ case '\b' -> out.append("\\b");
+ case '\f' -> out.append("\\f");
+ default -> {
+ // Control characters have to be escaped for the JSON to parse at
+ // all, and a stray one in a comment must never reach a generated
+ // file: scripts/check-control-characters.py rejects the whole
+ // tree over a single raw control byte.
+ //
+ // A lone surrogate is escaped for a harder reason: it has no
+ // UTF-8 encoding at all, so writing one out throws rather than
+ // producing a bad file, and one emoji cut in half by a summary
+ // would abort the whole generation.
+ if (c < 0x20 || c == 0x7f || isLoneSurrogate(value, i)) {
+ out.append(String.format("\\u%04x", (int) c));
+ } else {
+ out.append(c);
+ }
+ }
+ }
+ }
+ out.append('"');
+ }
+
+ /** Whether the character at the index is a surrogate without its partner. */
+ private static boolean isLoneSurrogate(String value, int index) {
+ char c = value.charAt(index);
+ if (Character.isHighSurrogate(c)) {
+ return index + 1 >= value.length() || !Character.isLowSurrogate(value.charAt(index + 1));
+ }
+ if (Character.isLowSurrogate(c)) {
+ return index == 0 || !Character.isHighSurrogate(value.charAt(index - 1));
+ }
+ return false;
+ }
+}
diff --git a/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/LegacyHtml.java b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/LegacyHtml.java
new file mode 100644
index 00000000000..1428ed30978
--- /dev/null
+++ b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/LegacyHtml.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.doclet.hugo;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Deals with the HTML left in comments that were converted to markdown.
+ *
+ * Goldmark renders with {@code unsafe = false}, so raw HTML in a comment body
+ * is dropped rather than rendered, and dropped silently: the build logs a
+ * warning per page and the content simply is not there. 13 of the 2024 core
+ * sources still carry some, and what disappeared included a whole {@code
}
+ * of tuning values in
+ * {@code com.codename1.gaming.physics.box2d.common.Settings}.
+ *
+ * Turning goldmark's {@code unsafe} on is not the answer, and not only
+ * because it would change how 45 pages of existing blog content render. Half of
+ * this HTML is not markup at all -- {@code com.codename1.util.regex.RE}
+ * documents a substitution string as {@code $0 } and
+ * {@code PushBuilder} writes {@code ;} to mean one value
+ * followed by another. Rendering those is how the standard pages lose them:
+ * the browser eats {@code } as an unknown element and shows a bare
+ * semicolon.
+ *
+ * So the two cases are separated. A structural tag carrying no attributes
+ * becomes its markdown equivalent, which covers 129 of the 140 tags in the API
+ * and renders them properly. Everything else is escaped, which makes it visible
+ * exactly as written -- better than both the old pipeline, which dropped it, and
+ * the standard doclet, which mangles it.
+ */
+final class LegacyHtml {
+
+ /** Tags that are real markup here, all of which appear without attributes. */
+ private static final Map BLOCK = Map.of(
+ "p", "\n\n",
+ "ul", "\n\n",
+ "ol", "\n\n",
+ "pre", "\n\n",
+ "br", "\n",
+ "li", "\n- ");
+
+ private static final Map INLINE = Map.of(
+ "b", "**",
+ "strong", "**",
+ "i", "*",
+ "em", "*");
+
+ private static final Set HEADINGS = Set.of("h1", "h2", "h3", "h4", "h5", "h6");
+
+ private LegacyHtml() {
+ }
+
+ /**
+ * Converts the structural HTML in a markdown body and escapes the rest.
+ *
+ * Fenced blocks and code spans are copied through untouched: HTML inside
+ * one is an example being shown, and goldmark already keeps it literal.
+ */
+ static String convert(String markdown) {
+ if (markdown == null || markdown.indexOf('<') < 0) {
+ return markdown;
+ }
+
+ String text = fencePreBlocks(markdown);
+ StringBuilder out = new StringBuilder(text.length() + 32);
+ String markdownText = text;
+ int i = 0;
+ int n = markdownText.length();
+ while (i < n) {
+ char c = markdownText.charAt(i);
+
+ int fence = fenceLength(markdownText, i);
+ if (fence > 0) {
+ int end = markdownText.indexOf(markdownText.substring(i, i + fence), i + fence);
+ int stop = end < 0 ? n : end + fence;
+ out.append(markdownText, i, stop);
+ i = stop;
+ continue;
+ }
+ if (c == '`') {
+ int ticks = 0;
+ while (i + ticks < n && markdownText.charAt(i + ticks) == '`') {
+ ticks++;
+ }
+ String delimiter = "`".repeat(ticks);
+ int end = markdownText.indexOf(delimiter, i + ticks);
+ int stop = end < 0 ? n : end + ticks;
+ out.append(markdownText, i, stop);
+ i = stop;
+ continue;
+ }
+ if (c != '<') {
+ out.append(c);
+ i++;
+ continue;
+ }
+
+ // A bare "<" is arithmetic, not markup: "a < b" must stay as written.
+ if (!opensATag(markdownText, i)) {
+ out.append(c);
+ i++;
+ continue;
+ }
+ int close = markdownText.indexOf('>', i);
+ if (close < 0) {
+ out.append(c);
+ i++;
+ continue;
+ }
+ String raw = markdownText.substring(i, close + 1);
+ String replacement = replacementFor(raw);
+ if (replacement == null) {
+ // Not structural markup: show it as the author wrote it.
+ out.append("<").append(raw, 1, raw.length());
+ } else {
+ out.append(replacement);
+ }
+ i = close + 1;
+ }
+ return out.toString();
+ }
+
+ /** Whether the "<" at this index begins something tag shaped. */
+ private static boolean opensATag(String text, int index) {
+ int next = index + 1;
+ if (next < text.length() && text.charAt(next) == '/') {
+ next++;
+ }
+ if (next >= text.length() || !Character.isLetter(text.charAt(next))) {
+ return false;
+ }
+ return !opensAnAutolink(text, index);
+ }
+
+ /**
+ * Whether this is a markdown autolink rather than a tag.
+ *
+ *
{@code } starts with a letter too, so a check
+ * for "letter after the angle bracket" claimed it and escaped it:
+ * com.codename1.security.Otp documents the key URI format that way and the
+ * page showed the URL as literal angle-bracketed text instead of a link.
+ * CommonMark's rule is a scheme followed by a colon, which is what this
+ * looks for.
+ */
+ private static boolean opensAnAutolink(String text, int index) {
+ int i = index + 1;
+ int start = i;
+ while (i < text.length()) {
+ char c = text.charAt(i);
+ if (c == ':') {
+ return i > start && i - start <= 32;
+ }
+ boolean schemeChar = Character.isLetterOrDigit(c) || c == '+' || c == '.' || c == '-';
+ if (!schemeChar) {
+ return false;
+ }
+ i++;
+ }
+ return false;
+ }
+
+ /**
+ * Turns each {@code } block into a fenced code block.
+ *
+ * Mapping the tags to blank lines kept the text but lost the formatting
+ * that was the whole point of the tag: {@code CommerceManager}'s multi-line
+ * usage example collapsed into a paragraph. Content that is already fenced
+ * -- which is what {@code
{@code ...} } becomes once the comment
+ * renderer has been through it -- is passed along instead of being wrapped
+ * again, because a fence inside a fence is not a code block.
+ */
+ private static String fencePreBlocks(String markdown) {
+ String lower = markdown.toLowerCase(Locale.ROOT);
+ if (!lower.contains("")) {
+ return markdown;
+ }
+ StringBuilder out = new StringBuilder(markdown.length() + 32);
+ int i = 0;
+ while (true) {
+ int open = lower.indexOf("", i);
+ if (open < 0) {
+ out.append(markdown, i, markdown.length());
+ return out.toString();
+ }
+ int close = lower.indexOf(" ", open);
+ if (close < 0) {
+ out.append(markdown, i, markdown.length());
+ return out.toString();
+ }
+ out.append(markdown, i, open);
+ String inner = markdown.substring(open + "".length(), close);
+ if (inner.contains("```")) {
+ out.append('\n').append(inner.strip()).append('\n');
+ } else {
+ out.append("\n```\n").append(trimBlankLines(inner)).append("\n```\n");
+ }
+ i = close + " ".length();
+ }
+ }
+
+ /** Drops leading and trailing blank lines without touching indentation. */
+ private static String trimBlankLines(String text) {
+ String[] lines = text.split("\n", -1);
+ int start = 0;
+ int end = lines.length;
+ while (start < end && lines[start].isBlank()) {
+ start++;
+ }
+ while (end > start && lines[end - 1].isBlank()) {
+ end--;
+ }
+ return String.join("\n", java.util.Arrays.asList(lines).subList(start, end));
+ }
+
+ /** The markdown for a tag, or null when it should be escaped instead. */
+ private static String replacementFor(String raw) {
+ String inner = raw.substring(1, raw.length() - 1).trim();
+ if (inner.endsWith("/")) {
+ inner = inner.substring(0, inner.length() - 1).trim();
+ }
+ boolean closing = inner.startsWith("/");
+ if (closing) {
+ inner = inner.substring(1).trim();
+ }
+ // An attribute means the tag is doing something this cannot reproduce --
+ // is the documented example, not a link to follow.
+ if (inner.isEmpty() || inner.chars().anyMatch(Character::isWhitespace)) {
+ return null;
+ }
+ String name = inner.toLowerCase(Locale.ROOT);
+
+ if (HEADINGS.contains(name)) {
+ // Levelled down deliberately: a heading inside a comment must not
+ // outrank the page structure the templates put around it.
+ return closing ? "\n" : "\n\n#### ";
+ }
+ String inline = INLINE.get(name);
+ if (inline != null) {
+ return inline;
+ }
+ String block = BLOCK.get(name);
+ if (block == null) {
+ return null;
+ }
+ if (closing) {
+ // is not valid HTML but is written anyway, and it means the same
+ // single break as . closes an item that the next "- " opens.
+ if ("br".equals(name)) {
+ return "\n";
+ }
+ return "li".equals(name) ? "" : "\n\n";
+ }
+ return block;
+ }
+
+ /** The length of a code fence opening at this position, or 0. */
+ private static int fenceLength(String text, int index) {
+ if (index > 0 && text.charAt(index - 1) != '\n') {
+ return 0;
+ }
+ char c = text.charAt(index);
+ if (c != '`' && c != '~') {
+ return 0;
+ }
+ int run = 0;
+ while (index + run < text.length() && text.charAt(index + run) == c) {
+ run++;
+ }
+ return run >= 3 ? run : 0;
+ }
+}
diff --git a/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/Literals.java b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/Literals.java
new file mode 100644
index 00000000000..29c7b0407b4
--- /dev/null
+++ b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/Literals.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.doclet.hugo;
+
+/**
+ * Renders a compile-time constant the way it would be written in Java source.
+ *
+ * The value arrives from {@code VariableElement.getConstantValue()} as a live
+ * object, so {@code String.valueOf} on a String hands back the characters with
+ * no quotes and no escaping. Placed into a declaration that is displayed as
+ * code, {@code DateFormatPatterns.RFC2822} then reads
+ * {@code = EEE, dd MMM yyyy HH:mm:ss Z}, which is not a value any reader could
+ * copy, and is not what the constant-values page of the standard docs shows.
+ *
+ *
Control characters are the sharper case. A constant such as a field holding
+ * a tab or a unit separator would otherwise travel through the front matter as a
+ * real control character and be rendered raw into the page. Every one of them is
+ * written as an escape here instead, which is the same rule the repository
+ * applies to its own sources -- see scripts/check-control-characters.py, which
+ * fails the build over a single raw control byte.
+ */
+final class Literals {
+
+ private Literals() {
+ }
+
+ /**
+ * The source spelling of a constant, or null when the field has no constant
+ * value (which is what the templates test to decide whether to show one).
+ */
+ static String of(Object constant) {
+ if (constant == null) {
+ return null;
+ }
+ if (constant instanceof String text) {
+ return quote(text, '"');
+ }
+ // An annotation element's array default arrives as a list of
+ // AnnotationValue. Rendering it with toString gives Java nothing it would
+ // recognise; "String[] options() default {}" is written {} in source.
+ if (constant instanceof java.util.List> values) {
+ StringBuilder out = new StringBuilder("{");
+ for (int i = 0; i < values.size(); i++) {
+ if (i > 0) {
+ out.append(", ");
+ }
+ Object element = values.get(i);
+ if (element instanceof javax.lang.model.element.AnnotationValue annotation) {
+ element = annotation.getValue();
+ }
+ out.append(of(element));
+ }
+ return out.append('}').toString();
+ }
+ if (constant instanceof Character character) {
+ return quote(String.valueOf(character), '\'');
+ }
+ // A long and a float are ambiguous without their suffix: 1 and 1L are
+ // different declarations, and javadoc writes the suffix for that reason.
+ if (constant instanceof Long) {
+ return constant + "L";
+ }
+ // Infinity and NaN have no literal spelling in Java, so toString gives
+ // text no source could contain. javadoc writes the expression that
+ // produces the value, and so does this: Numeric.min() and Numeric.max()
+ // default to negative and positive infinity.
+ if (constant instanceof Double value) {
+ if (value.isNaN()) {
+ return "0.0/0.0";
+ }
+ if (value.isInfinite()) {
+ return value > 0 ? "1.0/0.0" : "-1.0/0.0";
+ }
+ return String.valueOf(value);
+ }
+ if (constant instanceof Float value) {
+ if (value.isNaN()) {
+ return "0.0f/0.0f";
+ }
+ if (value.isInfinite()) {
+ return value > 0 ? "1.0f/0.0f" : "-1.0f/0.0f";
+ }
+ return value + "f";
+ }
+ // An enum-valued annotation default arrives as the constant's element, and
+ // its toString is the bare constant name. "default DEFAULT" says nothing
+ // about which enum, and the build-hint annotations are full of enums that
+ // each define a DEFAULT.
+ if (constant instanceof javax.lang.model.element.VariableElement variable
+ && variable.getKind() == javax.lang.model.element.ElementKind.ENUM_CONSTANT) {
+ javax.lang.model.element.Element owner = variable.getEnclosingElement();
+ return owner == null
+ ? variable.getSimpleName().toString()
+ : owner.getSimpleName() + "." + variable.getSimpleName();
+ }
+ return String.valueOf(constant);
+ }
+
+ private static String quote(String text, char delimiter) {
+ StringBuilder out = new StringBuilder(text.length() + 2);
+ out.append(delimiter);
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+ switch (c) {
+ case '\\' -> out.append("\\\\");
+ case '\n' -> out.append("\\n");
+ case '\r' -> out.append("\\r");
+ case '\t' -> out.append("\\t");
+ case '\b' -> out.append("\\b");
+ case '\f' -> out.append("\\f");
+ default -> {
+ if (c == delimiter) {
+ out.append('\\').append(c);
+ } else if (c < 0x20 || c == 0x7f) {
+ out.append(String.format("\\u%04x", (int) c));
+ } else {
+ out.append(c);
+ }
+ }
+ }
+ }
+ return out.append(delimiter).toString();
+ }
+}
diff --git a/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/MarkdownSections.java b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/MarkdownSections.java
new file mode 100644
index 00000000000..00dc80b9467
--- /dev/null
+++ b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/MarkdownSections.java
@@ -0,0 +1,588 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.doclet.hugo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Recovers the structure that the framework's documentation comments express as
+ * markdown prose rather than as javadoc block tags.
+ *
+ *
Almost none of this codebase uses {@code @param} and {@code @return}. The
+ * overwhelming majority writes a {@code #### Parameters} heading followed by a
+ * bullet list, and a {@code #### Returns} heading followed by a paragraph --
+ * measured over CodenameOne/src and Ports/CLDC11/src, 9068 occurrences of the
+ * heading against 816 of the tag, and 7224 against 1036. The standard doclet
+ * has no idea those are parameters: it renders them as an {@code
} buried
+ * inside the method description, which is why the generated pages carry no
+ * parameter tables at all.
+ *
+ * Parsing them back into structure is what lets the Hugo templates render a
+ * real signature block. The convention is machine-regular, so this is reliable,
+ * but it is still a convention rather than a language feature and the parser is
+ * written defensively: only the six headings in {@link #STRUCTURAL} are claimed,
+ * and every other heading -- {@code #### Example}, {@code #### Threading},
+ * {@code #### Platform support} and a long tail of one-offs -- stays in the
+ * description exactly where the author put it.
+ */
+public final class MarkdownSections {
+
+ /**
+ * The headings this parser treats as structure rather than prose.
+ *
+ *
Matched case insensitively against the heading text with any trailing
+ * colon removed. Nothing is added here without checking what the sources
+ * actually contain: a heading claimed by mistake silently disappears from
+ * the description and reappears somewhere the author did not intend.
+ */
+ private static final List STRUCTURAL =
+ List.of("parameters", "returns", "throws", "see also", "deprecated", "since");
+
+ /**
+ * The headings that only mean anything on a method or a constructor.
+ *
+ * A package and a type have no parameters, no return value and throw
+ * nothing, so a heading of that name in one of their comments is prose about
+ * something else. {@code com.codename1.db}'s package documentation has a
+ * "Parameters" section describing how the database binds its arguments, and
+ * claiming it lifted the whole section out of the page and then dropped it,
+ * because a package page has nowhere to put a parameter table.
+ *
+ *
Deprecation and See-also stay structural everywhere: a package can be
+ * deprecated, and does refer to other API.
+ */
+ private static final List EXECUTABLE_ONLY =
+ List.of("parameters", "returns", "throws");
+
+ /** Parsed result: the prose that stays, plus whatever structure was lifted out of it. */
+ public static final class Result {
+ private final String description;
+ private final List parameters;
+ private final List exceptions;
+ private final List seeAlso;
+ private final String returns;
+ private final String deprecated;
+
+ Result(String description, List parameters, List exceptions,
+ List seeAlso, String returns, String deprecated) {
+ this.description = description;
+ this.parameters = List.copyOf(parameters);
+ this.exceptions = List.copyOf(exceptions);
+ this.seeAlso = List.copyOf(seeAlso);
+ this.returns = returns;
+ this.deprecated = deprecated;
+ }
+
+ public String description() {
+ return description;
+ }
+
+ public List parameters() {
+ return parameters;
+ }
+
+ public List exceptions() {
+ return exceptions;
+ }
+
+ public List seeAlso() {
+ return seeAlso;
+ }
+
+ /** Return description, or null when the comment documented none. */
+ public String returns() {
+ return returns;
+ }
+
+ /** Deprecation note, or null when the comment carried no deprecation section. */
+ public String deprecated() {
+ return deprecated;
+ }
+ }
+
+ /** A bullet of the form {@code - `name`: text}, split into its two halves. */
+ public record NamedText(String name, String text) {
+ }
+
+ private MarkdownSections() {
+ }
+
+ /**
+ * Splits a markdown documentation body into prose and structure.
+ *
+ * @param markdown the raw comment body, exactly as the doclet received it
+ * @return the parse, never null; a body with no structural headings yields a
+ * result whose description is the input and whose lists are empty
+ */
+ public static Result parse(String markdown) {
+ return parse(markdown, true);
+ }
+
+ /**
+ * @param executable whether the comment documents a method or constructor;
+ * false for a package, a type or a field, whose comments
+ * cannot carry parameters, a return value or exceptions
+ */
+ public static Result parse(String markdown, boolean executable) {
+ if (markdown == null || markdown.isBlank()) {
+ return new Result("", List.of(), List.of(), List.of(), null, null);
+ }
+
+ List lines = List.of(markdown.split("\n", -1));
+ List prose = new ArrayList<>();
+ List parameters = new ArrayList<>();
+ List exceptions = new ArrayList<>();
+ List seeAlso = new ArrayList<>();
+ String returns = null;
+ String deprecated = null;
+
+ boolean inFence = false;
+ String fenceMarker = null;
+
+ int i = 0;
+ while (i < lines.size()) {
+ String line = lines.get(i);
+ String fence = fenceOpener(line);
+
+ if (inFence) {
+ prose.add(line);
+ if (fence != null && fence.startsWith(fenceMarker)) {
+ inFence = false;
+ fenceMarker = null;
+ }
+ i++;
+ continue;
+ }
+ if (fence != null) {
+ inFence = true;
+ fenceMarker = fence;
+ prose.add(line);
+ i++;
+ continue;
+ }
+
+ int level = headingLevel(line);
+ String key = level > 0 ? headingKey(line, level) : null;
+ if (key == null || !STRUCTURAL.contains(key)
+ || (!executable && EXECUTABLE_ONLY.contains(key))) {
+ prose.add(line);
+ i++;
+ continue;
+ }
+
+ int end = sectionEnd(lines, i + 1, level);
+ String body = join(lines.subList(i + 1, end));
+ switch (key) {
+ // A section with no named bullet documents nothing in particular:
+ // SampleQuery.setUnit explains under "Throws" that validation
+ // happens later, and lifting that produced a blank exception row
+ // with the prose stranded beside an empty label. Left where the
+ // author put it instead.
+ case "parameters" -> {
+ if (containsBullet(body)) {
+ parameters.addAll(bullets(body));
+ } else {
+ prose.addAll(lines.subList(i, end));
+ }
+ }
+ case "throws" -> {
+ if (containsBullet(body)) {
+ exceptions.addAll(bullets(body));
+ } else {
+ prose.addAll(lines.subList(i, end));
+ }
+ }
+ case "see also" -> {
+ for (String item : bulletTexts(body)) {
+ seeAlso.add(item);
+ }
+ }
+ case "returns" -> {
+ if (returns == null && !body.isBlank()) {
+ returns = body.strip();
+ }
+ }
+ case "deprecated" -> {
+ if (deprecated == null) {
+ // A deprecation section with no body still marks the element
+ // deprecated, so an empty string is meaningfully different
+ // from the null that means "no section at all".
+ deprecated = body.strip();
+ }
+ }
+ // "since" is parsed only so that it is consumed rather than left in
+ // the prose. Codename One does not publish availability metadata --
+ // scripts/check-since-tags.sh rejects it in sources outright -- and a
+ // guessed version is worse than no version, so the text is dropped.
+ default -> {
+ }
+ }
+ i = end;
+ }
+
+ return new Result(trimBlankEdges(prose), parameters, exceptions, seeAlso, returns, deprecated);
+ }
+
+ /**
+ * Whether a section is a list at all.
+ *
+ * The test is the presence of a bullet, not whether a name could be read
+ * out of one. A bullet whose text defeats the name parser is still a
+ * parameter the author wrote and has to stay in the table; only a section
+ * with no list in it is prose, which is what
+ * com.codename1.health.SampleQuery.setUnit writes under "Throws" when it
+ * explains that validation happens later.
+ */
+ private static boolean containsBullet(String body) {
+ for (String line : body.split("\n", -1)) {
+ if (bulletMarker(line) != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * The index one past the last line belonging to a section opened at the given
+ * heading level: the next heading of the same or shallower level, or the end.
+ * Headings inside fenced code are not headings.
+ */
+ private static int sectionEnd(List lines, int from, int level) {
+ boolean inFence = false;
+ String fenceMarker = null;
+ for (int i = from; i < lines.size(); i++) {
+ String line = lines.get(i);
+ String fence = fenceOpener(line);
+ if (inFence) {
+ if (fence != null && fence.startsWith(fenceMarker)) {
+ inFence = false;
+ fenceMarker = null;
+ }
+ continue;
+ }
+ if (fence != null) {
+ inFence = true;
+ fenceMarker = fence;
+ continue;
+ }
+ int candidate = headingLevel(line);
+ if (candidate > 0 && candidate <= level) {
+ return i;
+ }
+ }
+ return lines.size();
+ }
+
+ /**
+ * The fence marker a line opens or closes, or null when the line is not a
+ * fence.
+ *
+ * A backtick fence's info string may not itself contain a backtick, so a
+ * line that opens with a run and closes it again further along is an inline
+ * code span, not a fence. {@code LocalNotification.setAlertSound} writes one
+ * of exactly that shape, and treating it as an opener left the parser inside
+ * a fence for the rest of the comment: the {@code #### Parameters} heading
+ * below it was never recognised and the parameter lost its documentation.
+ */
+ private static String fenceOpener(String line) {
+ String trimmed = line.stripLeading();
+ if (trimmed.startsWith("```")) {
+ int run = 0;
+ while (run < trimmed.length() && trimmed.charAt(run) == '`') {
+ run++;
+ }
+ return trimmed.indexOf('`', run) >= 0 ? null : "```";
+ }
+ if (trimmed.startsWith("~~~")) {
+ return "~~~";
+ }
+ return null;
+ }
+
+ /** ATX heading level, or 0 when the line is not an ATX heading. */
+ private static int headingLevel(String line) {
+ String trimmed = line.stripLeading();
+ // More than three leading spaces would be an indented code block, and a
+ // heading marker must be followed by a space to be a heading at all.
+ if (line.length() - trimmed.length() > 3) {
+ return 0;
+ }
+ int hashes = 0;
+ while (hashes < trimmed.length() && trimmed.charAt(hashes) == '#') {
+ hashes++;
+ }
+ if (hashes == 0 || hashes > 6) {
+ return 0;
+ }
+ if (hashes == trimmed.length()) {
+ return hashes;
+ }
+ return trimmed.charAt(hashes) == ' ' ? hashes : 0;
+ }
+
+ /** The comparable form of a heading's text: lower case, no trailing colon or hashes. */
+ private static String headingKey(String line, int level) {
+ String text = line.stripLeading().substring(level).strip();
+ while (text.endsWith("#")) {
+ text = text.substring(0, text.length() - 1).strip();
+ }
+ while (text.endsWith(":")) {
+ text = text.substring(0, text.length() - 1).strip();
+ }
+ return text.toLowerCase(Locale.ROOT);
+ }
+
+ /** Splits a bullet list into {@code name} / {@code text} pairs. */
+ private static List bullets(String body) {
+ List out = new ArrayList<>();
+ for (String item : bulletTexts(body)) {
+ out.add(splitNamed(item));
+ }
+ return out;
+ }
+
+ /**
+ * The raw text of each top level bullet, with continuation lines folded in.
+ *
+ * A body that is not a list at all yields a single item holding the whole
+ * body, so a hand written {@code #### Returns}-style paragraph under
+ * {@code #### Parameters} is not silently dropped.
+ */
+ private static List bulletTexts(String body) {
+ List out = new ArrayList<>();
+ StringBuilder current = null;
+ boolean sawBullet = false;
+ boolean inFence = false;
+ String fenceMarker = null;
+
+ for (String line : body.split("\n", -1)) {
+ String fence = fenceOpener(line);
+ if (inFence) {
+ if (current != null) {
+ current.append('\n').append(line);
+ }
+ if (fence != null && fence.startsWith(fenceMarker)) {
+ inFence = false;
+ fenceMarker = null;
+ }
+ continue;
+ }
+
+ String marker = bulletMarker(line);
+ if (marker != null) {
+ sawBullet = true;
+ if (current != null) {
+ out.add(current.toString().strip());
+ }
+ current = new StringBuilder(line.stripLeading().substring(marker.length()).strip());
+ if (fence != null) {
+ inFence = true;
+ fenceMarker = fence;
+ }
+ continue;
+ }
+ if (current != null) {
+ current.append('\n').append(line);
+ if (fence != null) {
+ inFence = true;
+ fenceMarker = fence;
+ }
+ }
+ }
+ if (current != null) {
+ out.add(current.toString().strip());
+ }
+ if (!sawBullet && !body.isBlank()) {
+ return List.of(body.strip());
+ }
+ return out;
+ }
+
+ /** The bullet marker a top level list item opens with, or null. */
+ private static String bulletMarker(String line) {
+ String trimmed = line.stripLeading();
+ // Indentation beyond three spaces belongs to a nested list or a
+ // continuation, both of which fold into the item already open.
+ if (line.length() - trimmed.length() > 3) {
+ return null;
+ }
+ if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || trimmed.startsWith("+ ")) {
+ return trimmed.substring(0, 2);
+ }
+ return null;
+ }
+
+ /**
+ * Splits {@code `name`: text} into its halves.
+ *
+ * The colon is only a separator when it follows something that looks like
+ * an identifier, so a bullet that is a plain sentence containing a colon is
+ * kept whole under an empty name rather than being cut in two.
+ */
+ static NamedText splitNamed(String item) {
+ String text = item.strip();
+ // A markdown reference link is how JEP 467 comments name a Java element,
+ // and the renderer has already turned it into [`Name`](url) by the time
+ // this sees it. com.codename1.nfc.NdefMessage.parse documents its
+ // exception that way: without this the bullet produced a nameless entry
+ // holding the prose, and the declared exception was then listed again
+ // with no documentation at all.
+ if (text.startsWith("[")) {
+ int close = text.indexOf(']');
+ if (close > 1) {
+ String label = text.substring(1, close).replace("`", "").strip();
+ String rest = text.substring(close + 1);
+ if (rest.startsWith("(")) {
+ int url = matchingParen(rest);
+ if (url > 0) {
+ rest = rest.substring(url + 1);
+ }
+ }
+ int lastDot = label.lastIndexOf('.');
+ String simple = lastDot < 0 ? label : label.substring(lastDot + 1);
+ if (isIdentifierish(simple)) {
+ return new NamedText(label, stripSeparator(rest));
+ }
+ }
+ }
+ if (text.startsWith("`")) {
+ int close = text.indexOf('`', 1);
+ if (close > 1) {
+ String name = text.substring(1, close);
+ String rest = text.substring(close + 1).stripLeading();
+ if (rest.startsWith(":")) {
+ rest = rest.substring(1).stripLeading();
+ } else if (rest.startsWith("-")) {
+ rest = rest.substring(1).stripLeading();
+ }
+ if (isIdentifierish(name)) {
+ return new NamedText(name, stripRedundantTag(name, rest));
+ }
+ }
+ }
+ int colon = text.indexOf(':');
+ if (colon > 0) {
+ String name = text.substring(0, colon).strip();
+ if (isIdentifierish(name)) {
+ return new NamedText(name, stripRedundantTag(name, text.substring(colon + 1).stripLeading()));
+ }
+ }
+ return new NamedText("", text);
+ }
+
+ /**
+ * Removes a block tag the conversion left duplicated inside the bullet text.
+ *
+ *
1157 bullets across 184 files read {@code - `reverse`: @param reverse
+ * creates a new transition...}: the name is given twice, once as the bullet
+ * and again as a tag the markdown no longer needs. The standard pages show
+ * that verbatim too, because to javadoc it is prose.
+ *
+ *
Conservative on purpose. The tag is only removed when it names the same
+ * thing the bullet does, so a description that genuinely opens with an
+ * at-sign is left alone.
+ */
+ private static String stripRedundantTag(String name, String text) {
+ String rest = text.stripLeading();
+ for (String tag : List.of("@param", "@throws", "@exception")) {
+ String prefix = tag + " " + name;
+ if (rest.startsWith(prefix)) {
+ String after = rest.substring(prefix.length());
+ if (after.isEmpty() || Character.isWhitespace(after.charAt(0))) {
+ return stripSeparator(after);
+ }
+ }
+ }
+ for (String tag : List.of("@return", "@returns")) {
+ if (rest.startsWith(tag)) {
+ String after = rest.substring(tag.length());
+ if (after.isEmpty() || Character.isWhitespace(after.charAt(0))) {
+ return stripSeparator(after);
+ }
+ }
+ }
+ return text;
+ }
+
+ /** Drops the dash some of these use between the tag and the description. */
+ private static String stripSeparator(String text) {
+ String rest = text.stripLeading();
+ if (rest.startsWith("- ")) {
+ rest = rest.substring(2).stripLeading();
+ }
+ return rest;
+ }
+
+ /** The index of the parenthesis closing the one at position 0, or -1. */
+ private static int matchingParen(String text) {
+ int depth = 0;
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+ if (c == '(') {
+ depth++;
+ } else if (c == ')') {
+ depth--;
+ if (depth == 0) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /** Whether a candidate name could be a parameter or an exception type. */
+ private static boolean isIdentifierish(String candidate) {
+ if (candidate.isEmpty() || candidate.length() > 120) {
+ return false;
+ }
+ if (!Character.isJavaIdentifierStart(candidate.charAt(0))) {
+ return false;
+ }
+ for (int i = 1; i < candidate.length(); i++) {
+ char c = candidate.charAt(i);
+ if (!Character.isJavaIdentifierPart(c) && c != '.' && c != '<' && c != '>' && c != '[' && c != ']') {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static String join(List lines) {
+ return String.join("\n", lines);
+ }
+
+ /** Joins prose lines and strips the blank lines a lifted section leaves behind. */
+ private static String trimBlankEdges(List lines) {
+ int start = 0;
+ int end = lines.size();
+ while (start < end && lines.get(start).isBlank()) {
+ start++;
+ }
+ while (end > start && lines.get(end - 1).isBlank()) {
+ end--;
+ }
+ return join(lines.subList(start, end));
+ }
+}
diff --git a/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/Refs.java b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/Refs.java
new file mode 100644
index 00000000000..893ca3bcd97
--- /dev/null
+++ b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/Refs.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.doclet.hugo;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Types;
+
+/**
+ * Page paths and fragment identifiers, in the exact shapes the standard doclet
+ * emits.
+ *
+ * This is the compatibility surface of the whole generator. 304 distinct
+ * {@code /javadoc/...} URLs are linked from the website content and the
+ * developer guide, plus an unknown number from outside the project, and a
+ * fragment is part of the URL. Anything this class spells differently from
+ * javadoc is a link that used to work and now does not, which is why the
+ * encoding below was read off real javadoc output rather than assumed:
+ *
+ *
+ * id="<init>()" constructors
+ * id="f" fields, the bare name
+ * id="g(java.util.List)" type arguments erased away
+ * id="arr(byte[],int[][])" arrays keep their brackets
+ * id="va(java.lang.String...)" varargs keep the ellipsis...
+ * id="of(T...)" AND id="of(java.lang.Object[])" ...but the erasure does not
+ * id="inner(p.A.B)" nested types are dot separated
+ * id="t(T)" AND id="t(java.lang.Object)" a type variable gets both spellings
+ *
+ *
+ * Note the last one: javadoc emits two identifiers for a method with a type
+ * variable parameter, the declared spelling and the erasure, so a link written
+ * either way resolves. {@link #anchors} returns both for the same reason.
+ */
+final class Refs {
+
+ private final Types types;
+
+ Refs(Types types) {
+ this.types = types;
+ }
+
+ /**
+ * Every fragment identifier a member should answer to, the primary one first.
+ *
+ * @param member a field, constructor or method
+ * @return one identifier for a field, and one or two for an executable
+ */
+ List anchors(Element member) {
+ if (!(member instanceof ExecutableElement executable)) {
+ return List.of(member.getSimpleName().toString());
+ }
+
+ String name = member.getKind() == ElementKind.CONSTRUCTOR
+ ? ""
+ : member.getSimpleName().toString();
+
+ String declared = name + "(" + String.join(",", parameterTypes(executable, false)) + ")";
+ String erased = name + "(" + String.join(",", parameterTypes(executable, true)) + ")";
+ return declared.equals(erased) ? List.of(declared) : List.of(declared, erased);
+ }
+
+ private List parameterTypes(ExecutableElement executable, boolean erase) {
+ List extends VariableElement> parameters = executable.getParameters();
+ List out = new ArrayList<>(parameters.size());
+ for (int i = 0; i < parameters.size(); i++) {
+ TypeMirror type = parameters.get(i).asType();
+ if (erase) {
+ type = types.erasure(type);
+ }
+ // The declared spelling keeps the ellipsis; the erased spelling does
+ // not. javadoc gives Stream.of(T... values) both "of(T...)" and
+ // "of(java.lang.Object[])", so writing the ellipsis into the erased
+ // form loses the second address entirely.
+ boolean varargs = !erase && executable.isVarArgs() && i == parameters.size() - 1;
+ out.add(anchorType(type, varargs));
+ }
+ return out;
+ }
+
+ /** A single parameter type as javadoc spells it inside a fragment identifier. */
+ private String anchorType(TypeMirror type, boolean varargs) {
+ if (type instanceof ArrayType array) {
+ return anchorType(array.getComponentType(), false) + (varargs ? "..." : "[]");
+ }
+ if (type instanceof DeclaredType declared && declared.asElement() instanceof TypeElement element) {
+ // Reading the qualified name off the element rather than the mirror is
+ // what erases the type arguments: List becomes java.util.List,
+ // and the nested p.A.B keeps its dots.
+ return element.getQualifiedName().toString();
+ }
+ return type.toString();
+ }
+
+ /**
+ * The site path of a documented type's page.
+ *
+ * The directory form, not {@code Outer.Inner.html}, because Cloudflare
+ * Pages will not serve a {@code .html} URL: its own html_handling redirects
+ * {@code /x.html} to {@code /x} before any asset is considered, and the
+ * site's redirect table separately maps {@code /*.html} to {@code /:splat/}.
+ * Publishing the page at the extension was therefore a page nobody could
+ * reach -- every API link 301'd away from it, and the alias it landed on
+ * bounced the reader onto the production domain.
+ *
+ *
The javadoc spelling still works and still lands here: the redirect
+ * table turns {@code /javadoc/com/codename1/ui/Label.html} into
+ * {@code /javadoc/com/codename1/ui/Label/}, fragment intact, which is this
+ * URL. That is checked end to end against the Pages runtime rather than
+ * assumed; see scripts/website/check-javadoc-urls.sh.
+ */
+ static String typeUrl(TypeElement type) {
+ PackageElement pkg = packageOf(type);
+ String dir = pkg.isUnnamed() ? "" : pkg.getQualifiedName().toString().replace('.', '/') + "/";
+ return "/javadoc/" + dir + fileName(type) + "/";
+ }
+
+ /** The site path of a package's summary page, in the same directory form. */
+ static String packageUrl(PackageElement pkg) {
+ return "/javadoc/" + pkg.getQualifiedName().toString().replace('.', '/') + "/package-summary/";
+ }
+
+ /** The content file a type's page is generated into, relative to the content root. */
+ static String typeContentPath(TypeElement type) {
+ PackageElement pkg = packageOf(type);
+ String dir = pkg.isUnnamed() ? "" : pkg.getQualifiedName().toString().replace('.', '/') + "/";
+ return dir + fileName(type) + ".md";
+ }
+
+ /** The content file a package's summary is generated into. */
+ static String packageContentPath(PackageElement pkg) {
+ return pkg.getQualifiedName().toString().replace('.', '/') + "/package-summary.md";
+ }
+
+ /**
+ * A type's page file name: the simple name for a top level type, and the outer
+ * names joined by dots for a nested one.
+ */
+ private static String fileName(TypeElement type) {
+ StringBuilder name = new StringBuilder(type.getSimpleName().toString());
+ Element parent = type.getEnclosingElement();
+ while (parent instanceof TypeElement outer) {
+ name.insert(0, outer.getSimpleName().toString() + ".");
+ parent = outer.getEnclosingElement();
+ }
+ return name.toString();
+ }
+
+ /** The display name of a nested type, qualified by its outer types but not its package. */
+ static String nestedDisplayName(TypeElement type) {
+ return fileName(type);
+ }
+
+ static PackageElement packageOf(Element element) {
+ Element current = element;
+ while (current != null && !(current instanceof PackageElement)) {
+ current = current.getEnclosingElement();
+ }
+ return (PackageElement) current;
+ }
+
+ /** The type a member belongs to, or null when the element is not a member. */
+ static TypeElement enclosingType(Element member) {
+ Element current = member.getEnclosingElement();
+ while (current != null && !(current instanceof TypeElement)) {
+ current = current.getEnclosingElement();
+ }
+ return (TypeElement) current;
+ }
+}
diff --git a/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/SeeAlsoRef.java b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/SeeAlsoRef.java
new file mode 100644
index 00000000000..682f510c508
--- /dev/null
+++ b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/SeeAlsoRef.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.doclet.hugo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * One entry of a {@code #### See also} list, split into the reference it names
+ * and whatever prose follows it.
+ *
+ *
These are not {@code @see} block tags. They are markdown bullets, so the
+ * doclet receives them as text and nothing has resolved them: the reference has
+ * to be parsed here. Measured over CodenameOne/src and Ports/CLDC11/src, the
+ * list is 1180 local member references ({@code #drawRoundRect}), 426 qualified
+ * ones ({@code Display#supportsNativeImageCache()}), 238 bare type names, and a
+ * tail of 50 that carry trailing prose or are prose outright.
+ *
+ *
Both member forms routinely carry a parameter list, and roughly a third of
+ * the local ones carry a trailing sentence, as in
+ * {@code #isShapeSupported(java.lang.Object) to determine if the graphics
+ * context supports drawing}. Splitting on the first space is wrong for a
+ * signature containing one, so the split tracks parenthesis depth.
+ */
+final class SeeAlsoRef {
+
+ private final String type;
+ private final String member;
+ private final List parameters;
+ private final boolean hasParameterList;
+ private final String label;
+
+ private SeeAlsoRef(String type, String member, List parameters,
+ boolean hasParameterList, String label) {
+ this.type = type;
+ this.member = member;
+ this.parameters = parameters;
+ this.hasParameterList = hasParameterList;
+ this.label = label;
+ }
+
+ /** The type half, empty when the reference is local to the current type. */
+ String type() {
+ return type;
+ }
+
+ /** The member half, empty when the reference names a type. */
+ String member() {
+ return member;
+ }
+
+ /** The parameter types as written, empty when none were given. */
+ List parameters() {
+ return parameters;
+ }
+
+ /**
+ * Whether the reference wrote a parameter list at all.
+ *
+ * Distinct from an empty list: {@code #clear()} names the no-argument
+ * overload specifically, while {@code #clear} names the method and leaves
+ * the choice open.
+ */
+ boolean hasParameterList() {
+ return hasParameterList;
+ }
+
+ /** The trailing prose, empty when the entry was only a reference. */
+ String label() {
+ return label;
+ }
+
+ /** Whether this entry looks like a reference at all rather than a sentence. */
+ boolean isReference() {
+ return !type.isEmpty() || !member.isEmpty();
+ }
+
+ /** The reference as written, without the trailing prose. */
+ String text() {
+ StringBuilder out = new StringBuilder(type);
+ if (!member.isEmpty()) {
+ out.append('#').append(member);
+ if (hasParameterList) {
+ out.append('(').append(String.join(", ", parameters)).append(')');
+ }
+ }
+ return out.toString();
+ }
+
+ static SeeAlsoRef parse(String entry) {
+ String text = entry == null ? "" : entry.strip();
+ if (text.isEmpty()) {
+ return new SeeAlsoRef("", "", List.of(), false, "");
+ }
+
+ // A markdown link is already a link; leave it whole for the caller to
+ // render as markdown rather than trying to read a Java reference out of
+ // it. Nine entries take this shape, all of them pointing at MDN.
+ if (text.startsWith("[") || text.contains("](")) {
+ return new SeeAlsoRef("", "", List.of(), false, text);
+ }
+
+ // Six entries wrap the reference in backticks, which is how a reference
+ // is written everywhere else in a markdown comment. Without stripping
+ // them the leading backtick is not a Java identifier start, so the whole
+ // entry falls through to prose and renders with the backticks showing.
+ if (text.length() > 1 && text.startsWith("`") && text.endsWith("`")) {
+ text = text.substring(1, text.length() - 1).strip();
+ }
+
+ int split = referenceEnd(text);
+ String reference = text.substring(0, split);
+ String label = text.substring(split).strip();
+
+ String typePart = reference;
+ String memberPart = "";
+ int hash = reference.indexOf('#');
+ if (hash >= 0) {
+ typePart = reference.substring(0, hash);
+ memberPart = reference.substring(hash + 1);
+ }
+
+ List parameters = new ArrayList<>();
+ boolean hasList = false;
+ int open = memberPart.indexOf('(');
+ if (open >= 0) {
+ hasList = true;
+ int close = memberPart.lastIndexOf(')');
+ String inside = close > open ? memberPart.substring(open + 1, close) : "";
+ memberPart = memberPart.substring(0, open);
+ for (String parameter : splitParameters(inside)) {
+ if (!parameter.isBlank()) {
+ parameters.add(parameter.strip());
+ }
+ }
+ }
+
+ if (!isReferenceLike(typePart) || !isReferenceLike(memberPart)) {
+ return new SeeAlsoRef("", "", List.of(), false, text);
+ }
+ return new SeeAlsoRef(typePart.strip(), memberPart.strip(), parameters, hasList, label);
+ }
+
+ /** The index where the reference stops and any trailing prose begins. */
+ private static int referenceEnd(String text) {
+ int depth = 0;
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+ if (c == '(') {
+ depth++;
+ } else if (c == ')') {
+ depth = Math.max(0, depth - 1);
+ } else if (Character.isWhitespace(c) && depth == 0) {
+ return i;
+ }
+ }
+ return text.length();
+ }
+
+ /** Splits a parameter list on commas that are not inside nested generics. */
+ private static List splitParameters(String inside) {
+ List out = new ArrayList<>();
+ StringBuilder current = new StringBuilder();
+ int depth = 0;
+ for (int i = 0; i < inside.length(); i++) {
+ char c = inside.charAt(i);
+ if (c == '<') {
+ depth++;
+ } else if (c == '>') {
+ depth = Math.max(0, depth - 1);
+ }
+ if (c == ',' && depth == 0) {
+ out.add(current.toString());
+ current.setLength(0);
+ continue;
+ }
+ current.append(c);
+ }
+ out.add(current.toString());
+ return out;
+ }
+
+ /** Whether a half of the reference could be a Java name rather than prose. */
+ private static boolean isReferenceLike(String candidate) {
+ String text = candidate.strip();
+ if (text.isEmpty()) {
+ return true;
+ }
+ if (!Character.isJavaIdentifierStart(text.charAt(0))) {
+ return false;
+ }
+ for (int i = 1; i < text.length(); i++) {
+ char c = text.charAt(i);
+ if (!Character.isJavaIdentifierPart(c) && c != '.' && c != '<' && c != '>'
+ && c != '[' && c != ']') {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/TypeNames.java b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/TypeNames.java
new file mode 100644
index 00000000000..447228b5933
--- /dev/null
+++ b/maven/javadoc-hugo-doclet/src/main/java/com/codename1/doclet/hugo/TypeNames.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.doclet.hugo;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
+import javax.lang.model.type.WildcardType;
+
+/** Renders types the way a reader wants to read them, and links them where we publish them. */
+final class TypeNames {
+
+ private final CommentRenderer.Links links;
+
+ TypeNames(CommentRenderer.Links links) {
+ this.links = links;
+ }
+
+ /**
+ * A type reference for the templates: the source-like label, and the URL of
+ * the page documenting its raw type.
+ *
+ * Only the raw type carries a link. {@code Map>}
+ * renders whole and links to Map, rather than being decomposed into three
+ * separately linked fragments -- the extra links are not worth the template
+ * complexity, and the label still says exactly what the type is.
+ */
+ Map reference(TypeMirror type) {
+ Map out = new LinkedHashMap<>();
+ out.put("label", label(type));
+ out.put("url", url(type));
+ return out;
+ }
+
+ /** A source-like rendering using simple names: {@code Map>}. */
+ String label(TypeMirror type) {
+ if (type == null) {
+ return "";
+ }
+ switch (type.getKind()) {
+ case ARRAY:
+ return label(((ArrayType) type).getComponentType()) + "[]";
+ case DECLARED: {
+ DeclaredType declared = (DeclaredType) type;
+ String name = declared.asElement() instanceof TypeElement element
+ ? Refs.nestedDisplayName(element)
+ : declared.asElement().getSimpleName().toString();
+ List extends TypeMirror> arguments = declared.getTypeArguments();
+ if (arguments.isEmpty()) {
+ return name;
+ }
+ List rendered = new ArrayList<>(arguments.size());
+ for (TypeMirror argument : arguments) {
+ rendered.add(label(argument));
+ }
+ return name + "<" + String.join(", ", rendered) + ">";
+ }
+ case WILDCARD: {
+ WildcardType wildcard = (WildcardType) type;
+ if (wildcard.getExtendsBound() != null) {
+ return "? extends " + label(wildcard.getExtendsBound());
+ }
+ if (wildcard.getSuperBound() != null) {
+ return "? super " + label(wildcard.getSuperBound());
+ }
+ return "?";
+ }
+ case TYPEVAR:
+ return ((TypeVariable) type).asElement().getSimpleName().toString();
+ default:
+ return type.toString();
+ }
+ }
+
+ /** The page documenting a type's raw form, or null when we do not publish it. */
+ String url(TypeMirror type) {
+ TypeMirror current = type;
+ while (current instanceof ArrayType array) {
+ current = array.getComponentType();
+ }
+ if (current instanceof DeclaredType declared && declared.asElement() instanceof TypeElement element) {
+ return links.url(element);
+ }
+ return null;
+ }
+
+ /**
+ * The modifiers a declaration would actually write.
+ *
+ * The model reports the implicit ones too, and writing them out produces
+ * source no compiler would take: an enum is always final, an interface and
+ * an annotation are always abstract, and a nested one is always static.
+ */
+ static String declarationModifiers(Element type) {
+ String all = modifiers(type);
+ return switch (type.getKind()) {
+ case ENUM -> strip(all, "final", "static");
+ case INTERFACE, ANNOTATION_TYPE -> strip(all, "abstract", "static");
+ case RECORD -> strip(all, "final", "static");
+ default -> all;
+ };
+ }
+
+ private static String strip(String modifiers, String... implicit) {
+ List kept = new ArrayList<>();
+ for (String word : modifiers.split(" ")) {
+ if (!word.isEmpty() && !List.of(implicit).contains(word)) {
+ kept.add(word);
+ }
+ }
+ return String.join(" ", kept);
+ }
+
+ /** The declared modifiers in the order the language writes them. */
+ static String modifiers(Element element) {
+ List ordered = new ArrayList<>();
+ for (Modifier modifier : List.of(Modifier.PUBLIC, Modifier.PROTECTED, Modifier.PRIVATE,
+ Modifier.ABSTRACT, Modifier.DEFAULT, Modifier.STATIC, Modifier.FINAL,
+ Modifier.TRANSIENT, Modifier.VOLATILE, Modifier.SYNCHRONIZED, Modifier.NATIVE,
+ Modifier.STRICTFP)) {
+ if (element.getModifiers().contains(modifier)) {
+ ordered.add(modifier.toString());
+ }
+ }
+ return String.join(" ", ordered);
+ }
+
+ /** The {@code } clause of a generic declaration, or an empty string. */
+ String typeParameters(List extends javax.lang.model.element.TypeParameterElement> parameters) {
+ if (parameters.isEmpty()) {
+ return "";
+ }
+ List rendered = new ArrayList<>(parameters.size());
+ for (javax.lang.model.element.TypeParameterElement parameter : parameters) {
+ StringBuilder text = new StringBuilder(parameter.getSimpleName().toString());
+ List bounds = new ArrayList<>();
+ for (TypeMirror bound : parameter.getBounds()) {
+ String rendering = label(bound);
+ // Every type variable has java.lang.Object as an implicit bound, and
+ // writing it out turns every generic signature into noise.
+ if (!"Object".equals(rendering)) {
+ bounds.add(rendering);
+ }
+ }
+ if (!bounds.isEmpty()) {
+ text.append(" extends ").append(String.join(" & ", bounds));
+ }
+ rendered.add(text.toString());
+ }
+ return "<" + String.join(", ", rendered) + ">";
+ }
+
+ /**
+ * A summary with its markdown reduced to the words it was marking up.
+ *
+ * The search results list is plain text -- the page escapes what it is
+ * given rather than rendering it, which is right for a value that came out
+ * of a comment -- so a summary still carrying markdown shows its own source:
+ * 622 of the 2096 types displayed backticks, emphasis markers or a whole
+ * link destination in the results list.
+ *
+ *
The page-side summaries stay markdown, because Hugo renders those.
+ */
+ static String plainSummary(String markdown) {
+ String text = summary(markdown);
+ if (text.isEmpty()) {
+ return text;
+ }
+ //  and [label](url) both reduce to the text a reader sees.
+ // Scanned rather than matched with a regex: a member URL ends in a
+ // signature, so its destination contains balanced parentheses and
+ // stopping at the first ")" left one behind -- AdError arrived in the
+ // index as "AdListener.onFailedToLoad(AdError))".
+ text = unlink(text);
+
+ // Everything below rewrites markup, and a code span contains none: its
+ // content is literal by definition. JSONWriter.ArrayBuilder documents
+ // itself as "Fluent builder for `[ ..., ..., ... ]`", and treating those
+ // brackets as reference shorthand left the search result reading
+ // "Fluent builder for ..., ..., ... ." So each span is lifted out,
+ // stripped of its delimiters, and put back untouched afterwards.
+ List spans = new ArrayList<>();
+ StringBuilder masked = new StringBuilder(text.length());
+ int i = 0;
+ while (i < text.length()) {
+ char c = text.charAt(i);
+ if (c != '`') {
+ masked.append(c);
+ i++;
+ continue;
+ }
+ int ticks = 0;
+ while (i + ticks < text.length() && text.charAt(i + ticks) == '`') {
+ ticks++;
+ }
+ String fence = "`".repeat(ticks);
+ int end = text.indexOf(fence, i + ticks);
+ if (end < 0) {
+ masked.append(c);
+ i++;
+ continue;
+ }
+ // NUL cannot appear in a doc comment that compiles, so it cannot
+ // collide with the text being masked.
+ masked.append('\0').append(spans.size()).append('\0');
+ spans.add(text.substring(i + ticks, end));
+ i = end + ticks;
+ }
+ text = masked.toString();
+
+ // A reference link written as [Type] names that type.
+ text = text.replaceAll("\\[([^\\]]*)\\]", "$1");
+ text = text.replaceAll("\\*\\*([^*]+)\\*\\*", "$1");
+ text = text.replaceAll("(?Code spans are copied through whole. Brackets inside one are literal
+ * text, not a reference: JSONWriter.ArrayBuilder documents itself as
+ * "Fluent builder for `[ ..., ..., ... ]`", and stripping the brackets there
+ * left the search result reading "Fluent builder for ..., ..., ... ."
+ */
+ private static String unlink(String text) {
+ StringBuilder out = new StringBuilder(text.length());
+ int i = 0;
+ while (i < text.length()) {
+ char c = text.charAt(i);
+ if (c == '`') {
+ int ticks = 0;
+ while (i + ticks < text.length() && text.charAt(i + ticks) == '`') {
+ ticks++;
+ }
+ String fence = "`".repeat(ticks);
+ int end = text.indexOf(fence, i + ticks);
+ int stop = end < 0 ? text.length() : end + ticks;
+ out.append(text, i, stop);
+ i = stop;
+ continue;
+ }
+ if (c != '[' && !(c == '!' && i + 1 < text.length() && text.charAt(i + 1) == '[')) {
+ out.append(c);
+ i++;
+ continue;
+ }
+ int open = c == '!' ? i + 1 : i;
+ int close = text.indexOf(']', open);
+ if (close < 0 || close + 1 >= text.length() || text.charAt(close + 1) != '(') {
+ out.append(c);
+ i++;
+ continue;
+ }
+ int depth = 0;
+ int j = close + 1;
+ while (j < text.length()) {
+ if (text.charAt(j) == '(') {
+ depth++;
+ } else if (text.charAt(j) == ')') {
+ depth--;
+ if (depth == 0) {
+ break;
+ }
+ }
+ j++;
+ }
+ if (j >= text.length()) {
+ out.append(c);
+ i++;
+ continue;
+ }
+ out.append(text, open + 1, close);
+ i = j + 1;
+ }
+ return out.toString();
+ }
+
+ /**
+ * A cut index that never falls between the halves of a surrogate pair.
+ *
+ * Cutting one in half produces a lone surrogate, which is not
+ * representable in UTF-8: writing it out fails with
+ * UnmappableCharacterException rather than producing a broken file, so the
+ * whole generation aborts on one emoji in one comment.
+ */
+ private static int safeCut(String text, int limit) {
+ return Character.isHighSurrogate(text.charAt(limit - 1)) ? limit - 1 : limit;
+ }
+
+ /**
+ * Whether the full stop at this index belongs to an abbreviation.
+ *
+ *
"(e.g. a bad ad unit id)" is one sentence, and AdError's constants were
+ * being cut at the "g." -- leaving the summary ending mid-parenthesis in the
+ * package table and in search.
+ */
+ private static boolean isAbbreviation(String text, int dot) {
+ int start = dot;
+ while (start > 0 && !Character.isWhitespace(text.charAt(start - 1))) {
+ start--;
+ }
+ String word = text.substring(start, dot).toLowerCase(Locale.ROOT);
+ // "e.g", "i.e" and friends, plus any single letter: the "e." of "e.g."
+ // is itself a full stop followed by a space in some house styles.
+ return word.length() <= 1 || ABBREVIATIONS.contains(word);
+ }
+
+ private static final java.util.Set ABBREVIATIONS = java.util.Set.of(
+ "e.g", "i.e", "etc", "cf", "vs", "approx", "resp", "al", "fig", "no");
+
+ /** Whether every parenthesis opened before this point has been closed. */
+ private static boolean parenthesesBalanced(String text, int end) {
+ int depth = 0;
+ for (int i = 0; i < end; i++) {
+ char c = text.charAt(i);
+ if (c == '(') {
+ depth++;
+ } else if (c == ')') {
+ depth--;
+ }
+ }
+ return depth <= 0;
+ }
+
+ /** Whether a delimiter appears an odd number of times, so a span is left open. */
+ private static boolean unbalanced(String text, String delimiter) {
+ int count = 0;
+ int at = text.indexOf(delimiter);
+ while (at >= 0) {
+ count++;
+ at = text.indexOf(delimiter, at + delimiter.length());
+ }
+ return count % 2 != 0;
+ }
+
+ /**
+ * The first sentence of a description, for summary tables.
+ *
+ *