From ec18c0e958c290423eb99445b3db23d7853df59a Mon Sep 17 00:00:00 2001 From: lc285652 Date: Thu, 24 Sep 2026 15:07:05 +0800 Subject: [PATCH 1/3] feat: add Java API reference (javadoc) Adds api-reference/java-api, a standalone project that generates the Javadoc site for the official Java SDK (org.zvec:zvec-java) into public/api-reference/java/, mirroring how the Python (mkdocs) and Node.js (typedoc) references are built. - build.sh resolves the latest release from Maven metadata, downloads the -sources.jar, drops the examples package and runs javadoc. - The org.bytedeco:javacpp artifact the JNI bindings are annotated with is put on the classpath: javadoc 9+ reports unresolved symbols as errors. - Output is pinned to English via -J-Duser.language/-J-Duser.country so host locale cannot leak into the generated UI. - Javadoc 11 dropped frame-based output, so the script requires JDK 17+ and prefers 21 to match CI (setup-java temurin 21). - Adds the Java card to /api-reference (en/zh), mentions Java in llms.txt and builds the reference in both website workflows. --- .github/workflows/build_static_website.yml | 8 ++ .github/workflows/pr-preview-build.yml | 8 ++ .gitignore | 4 +- api-reference/java-api/README.md | 38 +++++++ api-reference/java-api/build.sh | 110 +++++++++++++++++++++ api-reference/java-api/styles/extra.css | 33 +++++++ app/[lang]/(home)/api-reference/page.tsx | 18 +++- app/llms.txt/route.ts | 2 +- 8 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 api-reference/java-api/README.md create mode 100755 api-reference/java-api/build.sh create mode 100644 api-reference/java-api/styles/extra.css diff --git a/.github/workflows/build_static_website.yml b/.github/workflows/build_static_website.yml index 632b91653..9f8ce597d 100644 --- a/.github/workflows/build_static_website.yml +++ b/.github/workflows/build_static_website.yml @@ -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 @@ -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 diff --git a/.github/workflows/pr-preview-build.yml b/.github/workflows/pr-preview-build.yml index d8219dc8b..bb6266009 100644 --- a/.github/workflows/pr-preview-build.yml +++ b/.github/workflows/pr-preview-build.yml @@ -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 @@ -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 diff --git a/.gitignore b/.gitignore index 757db3a33..a021c01c6 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ \ No newline at end of file +public/api-reference/python/ +public/api-reference/java/ diff --git a/api-reference/java-api/README.md b/api-reference/java-api/README.md new file mode 100644 index 000000000..33c40f95e --- /dev/null +++ b/api-reference/java-api/README.md @@ -0,0 +1,38 @@ +# 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); +- runs `javadoc` with English output regardless of the host locale, then 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. diff --git a/api-reference/java-api/build.sh b/api-reference/java-api/build.sh new file mode 100755 index 000000000..e06921faa --- /dev/null +++ b/api-reference/java-api/build.sh @@ -0,0 +1,110 @@ +#!/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:.*\(.*\).*:\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:.*\(.*\).*:\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" + +# -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 sys + +out = pathlib.Path(sys.argv[1]) +inject = ( + '' + '' + '' +) +count = 0 +for html in out.rglob('*.html'): + text = html.read_text(encoding='utf-8', errors='replace') + if '' in text: + html.write_text(text.replace('', inject, 1), encoding='utf-8') + count += 1 +print(f"==> Injected branding into {count} html files") +PYINJECT + +echo "==> Java API reference written to ${OUT} (zvec-java ${VERSION})" diff --git a/api-reference/java-api/styles/extra.css b/api-reference/java-api/styles/extra.css new file mode 100644 index 000000000..c44e93eed --- /dev/null +++ b/api-reference/java-api/styles/extra.css @@ -0,0 +1,33 @@ +/* Light site branding on top of the default Javadoc stylesheet. */ +body { + font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, sans-serif; +} + +code, pre, tt { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', + monospace; +} + +a:link, a:visited { + color: #0f766e; + text-decoration: none; +} + +a:hover, a:focus { + color: #115e59; + text-decoration: underline; +} + +.topNav, .bottomNav { + background: #f8fafc; +} + +.topNav a:link, .topNav a:visited, +.bottomNav a:link, .bottomNav a:visited { + color: #334155; +} + +.header h1.title, div.details h2 { + font-weight: 650; +} diff --git a/app/[lang]/(home)/api-reference/page.tsx b/app/[lang]/(home)/api-reference/page.tsx index cf70cdf47..faaec4bf3 100644 --- a/app/[lang]/(home)/api-reference/page.tsx +++ b/app/[lang]/(home)/api-reference/page.tsx @@ -1,7 +1,7 @@ import { i18n } from '@/lib/i18n'; import { SITE_URL } from '@/lib/constants'; import type { Metadata } from 'next'; -import { FaPython, FaNodeJs } from "react-icons/fa"; +import { FaPython, FaNodeJs, FaJava } from "react-icons/fa"; import { ArrowUpRight } from 'lucide-react'; @@ -72,6 +72,8 @@ export default async function Home({ params }: { params: Promise<{ lang: string; pythonDescription: 'Classes, schemas, indexes, and query APIs for Python.', nodejs: 'Node.js API Reference', nodejsDescription: 'TypeScript-first APIs for Node.js applications.', + java: 'Java API Reference', + javaDescription: 'Javadoc for the official Java SDK (Maven: org.zvec:zvec-java).', }, zh: { title: 'Zvec API 参考', @@ -81,6 +83,8 @@ export default async function Home({ params }: { params: Promise<{ lang: string; pythonDescription: 'Python 的类、Schema、索引与查询接口。', nodejs: 'Node.js API 参考', nodejsDescription: '面向 Node.js 应用的 TypeScript 优先接口。', + java: 'Java API 参考', + javaDescription: '官方 Java SDK 的 Javadoc(Maven:org.zvec:zvec-java)。', }, }; const t = translations[lang as keyof typeof translations] || translations.en; @@ -119,6 +123,18 @@ export default async function Home({ params }: { params: Promise<{ lang: string; + + +
+

{t.java}

+

{t.javaDescription}

+
+ +
+ ); diff --git a/app/llms.txt/route.ts b/app/llms.txt/route.ts index 8a9a5a788..e17ab6913 100644 --- a/app/llms.txt/route.ts +++ b/app/llms.txt/route.ts @@ -99,7 +99,7 @@ Use these URLs to feed individual pages as context to AI coding agents (Cursor, ## Optional - [Full documentation](${SITE_URL}/llms-full.txt): Complete documentation content in plain text -- [API Reference](${SITE_URL}/api-reference/): Python and Node.js API reference +- [API Reference](${SITE_URL}/api-reference/): Python, Node.js, and Java API reference `; return new Response(content, { From e7bdde979e7089cc393944245894caad107b2740 Mon Sep 17 00:00:00 2001 From: lc285652 Date: Thu, 24 Sep 2026 15:34:23 +0800 Subject: [PATCH 2/3] fix: restore javadoc "invalid input" markers in the Java API reference Doc comments that contain raw angle brackets (the JNI bindings mention C++ types such as std::shared_ptr*) are not valid HTML, so javadoc replaces the offending character with a marker span and renders it in the page: "Internally maps to std::shared_ptr invalid input: '<' zvec::Collection>*". Replace the marker with the character javadoc reported. The surrounding text is already escaped correctly, so the comment reads as intended again. Logs how many markers were restored and warns if any invalid-tag markup survives, so a future change to the marker format cannot fail silently. --- api-reference/java-api/README.md | 7 +++++-- api-reference/java-api/build.sh | 26 ++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/api-reference/java-api/README.md b/api-reference/java-api/README.md index 33c40f95e..38e164c46 100644 --- a/api-reference/java-api/README.md +++ b/api-reference/java-api/README.md @@ -31,8 +31,11 @@ The script: - 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); -- runs `javadoc` with English output regardless of the host locale, then injects - the site favicon plus `styles/extra.css` for light branding. +- runs `javadoc` with English output regardless of the host locale; +- restores the characters javadoc replaces with a visible `invalid input` marker + when a doc comment contains raw angle brackets (the SDK mentions C++ types + such as `std::shared_ptr*`); +- 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. diff --git a/api-reference/java-api/build.sh b/api-reference/java-api/build.sh index e06921faa..d19020f67 100755 --- a/api-reference/java-api/build.sh +++ b/api-reference/java-api/build.sh @@ -90,6 +90,7 @@ curl -fsS -o build/deps/javacpp.jar \ cp styles/extra.css "${OUT}/zvec-extra.css" python3 - "${OUT}" <<'PYINJECT' import pathlib +import re import sys out = pathlib.Path(sys.argv[1]) @@ -98,13 +99,30 @@ inject = ( '' '' ) -count = 0 +# Doc comments such as "std::shared_ptr*" 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("invalid input: '(.*?)'") + +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 '' in text: - html.write_text(text.replace('', inject, 1), encoding='utf-8') - count += 1 -print(f"==> Injected branding into {count} html files") + text = text.replace('', 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})" From e1dc7826293e207f37011aef0120b8836e0b792e Mon Sep 17 00:00:00 2001 From: lc285652 Date: Sat, 10 Oct 2026 15:44:03 +0800 Subject: [PATCH 3/3] fix: clean Doxygen leftovers before generating the Java API reference JavaCPP copies the Doxygen comments of zvec/c_api.h verbatim into the generated bindings, and javadoc renders whatever it cannot parse: - a raw '<' that cannot start an HTML tag (std::shared_ptr*) makes javadoc emit "invalid input: '<'" and print that marker in the page; - commands javadoc does not know (\brief, \note) are printed literally, which leaked "\brief" into the generated pages. Fix both at the source instead of patching the output: scripts/clean_doc_comments.py rewrites the doc comments of the downloaded sources before javadoc runs. It only touches /** ... */ comments, skips string and char literals (the sources contain a "\u2014" message), and leaves {@code ...} / {@link ...} spans alone because javadoc already escapes their content. javadoc warnings for zvec-java 0.7.0 drop from 6 "invalid input" to 0, and the marker repair from the previous commit now reports "Restored 0" and stays as a fallback. The two remaining "@param ... is not a parameter name" warnings are upstream: c_api.h documents a deleted_count parameter that zvec_collection_delete_by_filter does not take, and JavaCPP renames the capacity parameter of zvec_byte_array_create to _capacity. --- api-reference/java-api/README.md | 10 +- api-reference/java-api/build.sh | 5 + .../java-api/scripts/clean_doc_comments.py | 137 ++++++++++++++++++ 3 files changed, 149 insertions(+), 3 deletions(-) create mode 100755 api-reference/java-api/scripts/clean_doc_comments.py diff --git a/api-reference/java-api/README.md b/api-reference/java-api/README.md index 38e164c46..747a5c038 100644 --- a/api-reference/java-api/README.md +++ b/api-reference/java-api/README.md @@ -31,10 +31,14 @@ The script: - 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*`), and rewrites the + `\brief` / `\note` commands javadoc does not understand; - runs `javadoc` with English output regardless of the host locale; -- restores the characters javadoc replaces with a visible `invalid input` marker - when a doc comment contains raw angle brackets (the SDK mentions C++ types - such as `std::shared_ptr*`); +- 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 diff --git a/api-reference/java-api/build.sh b/api-reference/java-api/build.sh index d19020f67..ce08f9f57 100755 --- a/api-reference/java-api/build.sh +++ b/api-reference/java-api/build.sh @@ -71,6 +71,11 @@ 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 \ diff --git a/api-reference/java-api/scripts/clean_doc_comments.py b/api-reference/java-api/scripts/clean_doc_comments.py new file mode 100755 index 000000000..9cc69a900 --- /dev/null +++ b/api-reference/java-api/scripts/clean_doc_comments.py @@ -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*``) + 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 [--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('<') + 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()