Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/build_static_website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ jobs:
package-lock.json
api-reference/nodejs-api/package-lock.json

- name: Set up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21

- name: Install Dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -53,6 +59,8 @@ jobs:
npm install @zvec/zvec@latest
cp node_modules/@zvec/zvec/src/index.d.ts ./
npx typedoc
cd ../java-api
./build.sh
cd ..

- name: Build Website
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/pr-preview-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ jobs:
package-lock.json
api-reference/nodejs-api/package-lock.json

- name: Set up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21

- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -54,6 +60,8 @@ jobs:
npm install @zvec/zvec@latest
cp node_modules/@zvec/zvec/src/index.d.ts ./
npx typedoc
cd ../java-api
./build.sh

- name: Build website
run: npm run build
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ __pycache__/
.vscode/

api-reference/nodejs-api/index.d.ts
api-reference/java-api/build/

public/api-reference/nodejs/
public/api-reference/python/
public/api-reference/python/
public/api-reference/java/
45 changes: 45 additions & 0 deletions api-reference/java-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Zvec Java API Reference

This is the official Java API reference documentation for the Zvec Java SDK
(`org.zvec:zvec-java`), built with [Javadoc](https://docs.oracle.com/en/java/javase/21/docs/specs/javadoc/javadoc.html)
from the sources published on Maven Central.

## Requirements

- JDK 17 or newer — CI builds with Temurin 21. Javadoc 11 dropped frame-based
output, so the generated site looks completely different between JDKs; the
build script picks `$JAVADOC`, then JDK 21, then any JDK 17+, and refuses to
run on anything older.
- `curl`, `unzip` and `python3` on `PATH`.

## Instructions

### 1. Build the Static Site

```bash
./build.sh
```

This command creates a **/public/api-reference/java/** directory containing all
the static assets. Since the output consists only of static files, you can host
it on virtually any web server or platform (e.g., GitHub Pages).

The script:

- resolves the latest `zvec-java` release from Maven metadata (override with
`ZVEC_JAVA_VERSION=0.7.0 ./build.sh`);
- downloads the `-sources.jar` together with the `org.bytedeco:javacpp` artifact
the JNI bindings are annotated with (Javadoc 9+ reports unresolved symbols as
errors), and drops the `examples` package (guides, not API surface);
- cleans the Doxygen leftovers JavaCPP copies out of `zvec/c_api.h`
(`scripts/clean_doc_comments.py`): escapes angle brackets that javadoc would
otherwise render as a visible `invalid input` marker (the header mentions C++
types such as `std::shared_ptr<zvec::Collection>*`), and rewrites the
`\brief` / `\note` commands javadoc does not understand;
- runs `javadoc` with English output regardless of the host locale;
- restores any character javadoc still replaced with an `invalid input` marker,
as a fallback should a future comment slip past the cleaning step;
- injects the site favicon plus `styles/extra.css` for light branding.

Generated files should not be edited by hand — change this project and rebuild
instead.
133 changes: 133 additions & 0 deletions api-reference/java-api/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# Generate the Zvec Java API reference (Javadoc) into public/api-reference/java/.
set -euo pipefail

cd "$(dirname "$0")"

OUT="../../public/api-reference/java"
CENTRAL="https://repo1.maven.org/maven2/org/zvec/zvec-java"
VERSION="${ZVEC_JAVA_VERSION:-$(curl -fsS "$CENTRAL/maven-metadata.xml" | sed -n 's:.*<release>\(.*\)</release>.*:\1:p')}"

if [ -z "${VERSION}" ]; then
echo "error: could not resolve the latest zvec-java release version" >&2
exit 1
fi

# Javadoc 11 dropped frame-based output, so the generated site looks completely
# different between JDKs. Pin one: $JAVADOC wins, then the JDK CI builds with
# (21), then any JDK 17+, then whatever is on PATH.
JAVADOC_BIN=""
if [ -n "${JAVADOC:-}" ]; then
JAVADOC_BIN="$JAVADOC"
elif [ -x /usr/libexec/java_home ]; then
for spec in 21 17+; do
if candidate="$(/usr/libexec/java_home -v "${spec}" 2>/dev/null)"; then
JAVADOC_BIN="${candidate}/bin/javadoc"
break
fi
done
fi
if [ -z "${JAVADOC_BIN}" ]; then
JAVADOC_BIN="$(command -v javadoc || true)"
fi

if [ -z "${JAVADOC_BIN}" ] || [ ! -x "${JAVADOC_BIN}" ]; then
echo "error: no javadoc binary found; set JAVADOC=/path/to/javadoc" >&2
exit 1
fi

raw_version="$("$JAVADOC_BIN" -J-version 2>&1 | head -1)"
major="${raw_version#*version \"}"
major="${major%%\"*}"
case "${major}" in
1.*) major="${major#1.}"; major="${major%%.*}" ;;
*) major="${major%%.*}" ;;
esac

if ! [ "${major:-0}" -ge 17 ] 2>/dev/null; then
echo "error: ${JAVADOC_BIN} is JDK ${major:-?}; javadoc needs JDK 17+ (set JAVADOC=/path/to/javadoc)" >&2
exit 1
fi

echo "==> Building Javadoc for org.zvec:zvec-java:${VERSION} with ${JAVADOC_BIN}"

rm -rf build "${OUT}"
mkdir -p build/src build/deps "${OUT}"

curl -fsS -o build/zvec-java.pom "${CENTRAL}/${VERSION}/zvec-java-${VERSION}.pom"
curl -fsS -o build/sources.jar "${CENTRAL}/${VERSION}/zvec-java-${VERSION}-sources.jar"
unzip -q -o build/sources.jar -d build/src

# Examples are guides, not API surface.
rm -rf build/src/org/zvec/binding/examples

# Javadoc 9+ reports unresolved symbols as errors, so fetch the JavaCPP artifact
# the JNI bindings are annotated with.
JAVACPP_VERSION="${JAVACPP_VERSION:-$(sed -n 's:.*<javacpp.version>\(.*\)</javacpp.version>.*:\1:p' build/zvec-java.pom | head -1)}"
if [ -z "${JAVACPP_VERSION}" ]; then
echo "error: could not resolve the javacpp version from the zvec-java POM" >&2
exit 1
fi
curl -fsS -o build/deps/javacpp.jar \
"https://repo1.maven.org/maven2/org/bytedeco/javacpp/${JAVACPP_VERSION}/javacpp-${JAVACPP_VERSION}.jar"

# JavaCPP copies the Doxygen comments of zvec/c_api.h verbatim, and javadoc
# renders what it cannot parse: escape stray angle brackets and drop the
# Doxygen-only commands before generating.
python3 scripts/clean_doc_comments.py build/src

# -J-Duser.* keeps docs and tool messages in English regardless of host locale.
"$JAVADOC_BIN" \
-J-Duser.language=en -J-Duser.country=US \
-quiet \
-encoding UTF-8 -charset UTF-8 -docencoding UTF-8 \
-Xdoclint:none \
-use -splitindex \
-windowtitle "Zvec Java API Reference" \
-doctitle "Zvec Java API Reference" \
-header "Zvec Java ${VERSION}" \
-d "${OUT}" \
-sourcepath build/src \
-classpath build/deps/javacpp.jar \
-subpackages org.zvec

# Site branding: favicon plus light styling on top of the default Javadoc theme.
cp styles/extra.css "${OUT}/zvec-extra.css"
python3 - "${OUT}" <<'PYINJECT'
import pathlib
import re
import sys

out = pathlib.Path(sys.argv[1])
inject = (
'<link rel="icon" href="/favicon.ico">'
'<link rel="stylesheet" href="/api-reference/java/zvec-extra.css">'
'</head>'
)
# Doc comments such as "std::shared_ptr<zvec::Collection>*" are not valid HTML,
# so javadoc swaps the offending character for a marker that ends up rendered in
# the page. Restore the character it reported; the text around it is already
# escaped correctly.
MARKER = re.compile("<span class=\"invalid-tag\">invalid input: '(.*?)'</span>")

branding = repaired = 0
leftover = []
for html in out.rglob('*.html'):
text = html.read_text(encoding='utf-8', errors='replace')
original = text
text, hits = MARKER.subn(lambda match: match.group(1), text)
repaired += hits
if '</head>' in text:
text = text.replace('</head>', inject, 1)
branding += 1
if text != original:
html.write_text(text, encoding='utf-8')
if 'invalid-tag' in text:
leftover.append(str(html))
print(f"==> Injected branding into {branding} html files")
print(f"==> Restored {repaired} javadoc 'invalid input' markers")
if leftover:
print(f"warning: {len(leftover)} files still contain invalid-tag markup", file=sys.stderr)
PYINJECT

echo "==> Java API reference written to ${OUT} (zvec-java ${VERSION})"
137 changes: 137 additions & 0 deletions api-reference/java-api/scripts/clean_doc_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""Clean Doxygen leftovers in Java doc comments before javadoc runs.

JavaCPP copies the Doxygen comments of ``zvec/c_api.h`` verbatim into the
generated bindings, but javadoc reads a doc comment as HTML and only knows
javadoc tags. Two things therefore leak into the published pages:

* a raw ``<`` that cannot start an HTML tag (``std::shared_ptr<zvec::Collection>*``)
makes javadoc render its own ``invalid input: '<'`` marker in the text;
* commands javadoc does not know (``\\brief``, ``\\note``) are printed literally.

Both are fixed here, in the sources, so javadoc never produces the broken
output in the first place. Only ``/** ... */`` comments are touched, string
and char literals are skipped, and ``{@code ...}`` / ``{@link ...}`` spans are
left alone because javadoc already escapes their content.

Usage: ``clean_doc_comments.py <source-root> [--dry-run]``
"""

import pathlib
import re
import sys

INLINE_TAG = re.compile(r'\{@[^{}]*\}')
VALID_TAG = re.compile(r'<(?:!--|![A-Za-z]|/?[A-Za-z][A-Za-z0-9-]*(?=[\s/>]))')
DOXYGEN_COMMANDS = (
(re.compile(r'\\brief\s*'), ''),
(re.compile(r'\\note\s*'), 'Note: '),
)


def escape_invalid_lt(text):
out = []
index = 0
escaped = 0
while True:
pos = text.find('<', index)
if pos == -1:
out.append(text[index:])
break
out.append(text[index:pos])
if VALID_TAG.match(text, pos):
out.append('<')
else:
out.append('&lt;')
escaped += 1
index = pos + 1
return ''.join(out), escaped


def clean_comment(comment):
parts = INLINE_TAG.split(comment)
tags = INLINE_TAG.findall(comment)
escaped = rewritten = 0
out = []
for index, part in enumerate(parts):
cleaned = part
for pattern, replacement in DOXYGEN_COMMANDS:
cleaned, hits = pattern.subn(replacement, cleaned)
rewritten += hits
cleaned, hits = escape_invalid_lt(cleaned)
escaped += hits
out.append(cleaned)
if index < len(tags):
out.append(tags[index])
return ''.join(out), escaped, rewritten


def clean_source(source):
out = []
index = 0
size = len(source)
escaped = rewritten = 0
while index < size:
char = source[index]
following = source[index + 1] if index + 1 < size else ''
if char in '"\'':
end = index + 1
while end < size:
if source[end] == '\\':
end += 2
continue
if source[end] == char:
end += 1
break
end += 1
out.append(source[index:end])
index = end
elif char == '/' and following == '/':
end = source.find('\n', index)
end = size if end == -1 else end
out.append(source[index:end])
index = end
elif char == '/' and following == '*':
start = index + 3 if source.startswith('/**', index) else index + 2
end = source.find('*/', start)
end = size if end == -1 else end + 2
chunk = source[index:end]
if source.startswith('/**', index):
chunk, hit_escape, hit_rewrite = clean_comment(chunk)
escaped += hit_escape
rewritten += hit_rewrite
out.append(chunk)
index = end
else:
out.append(char)
index += 1
return ''.join(out), escaped, rewritten


def main():
root = pathlib.Path(sys.argv[1])
dry_run = '--dry-run' in sys.argv
files = escaped_total = rewritten_total = 0
for path in sorted(root.rglob('*.java')):
source = path.read_text(encoding='utf-8')
cleaned, escaped, rewritten = clean_source(source)
escaped_total += escaped
rewritten_total += rewritten
if cleaned == source:
continue
files += 1
if dry_run:
print(f"--- {path}")
for before, after in zip(source.splitlines(), cleaned.splitlines()):
if before != after:
print(f"-{before}\n+{after}")
else:
path.write_text(cleaned, encoding='utf-8')
verb = 'Would clean' if dry_run else 'Cleaned'
print(
f"==> {verb} doc comments in {files} files: "
f"escaped {escaped_total} '<', rewrote {rewritten_total} Doxygen commands"
)


if __name__ == '__main__':
main()
Loading
Loading