Skip to content
Merged
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
27 changes: 27 additions & 0 deletions .github/scripts/doc-version-selector.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#c2pa-doc-version-selector {
display: inline-flex;
align-items: center;
gap: 0.4rem;
white-space: nowrap;
}

#c2pa-doc-version-selector select {
max-width: 10rem;
padding: 0.25rem;
border: 1px solid currentColor;
border-radius: 0.25rem;
color: inherit;
background: transparent;
font: inherit;
}

#c2pa-doc-version-selector option {
color: CanvasText;
background: Canvas;
}

@media print {
#c2pa-doc-version-selector {
display: none;
}
}
29 changes: 29 additions & 0 deletions .github/scripts/doc-version-selector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
(function () {
"use strict";

const script = document.currentScript;
const versions = JSON.parse(script.dataset.versions);
const assets = new URL(".", script.src);
const root = new URL(script.dataset.prefix ? "../../" : "./", assets);
const container = document.getElementById("library-version");
if (!container) {
throw new Error("Dokka did not provide a library-version header slot.");
}
const label = document.createElement("label");
label.id = "c2pa-doc-version-selector";
label.textContent = "Version: ";
const select = document.createElement("select");
select.setAttribute("aria-label", "API documentation version");
for (const tag of versions) {
const option = document.createElement("option");
option.value = new URL("versions/" + tag + "/", root).href;
option.textContent = tag;
option.selected = tag === script.dataset.version;
select.appendChild(option);
}
select.addEventListener("change", function () {
window.location.assign(select.value);
});
label.appendChild(select);
container.replaceChildren(label);
})();
138 changes: 138 additions & 0 deletions .github/scripts/generate-versioned-docs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
PACKAGE_INDEX=c2pa-android/org.contentauth.c2pa/index.html

fail() {
printf '%s\n' "$*" >&2
return 1
}

load_versions() {
local filename="$1"
jq -e --arg pattern '^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9][A-Za-z0-9.-]*)?\z' '
if type != "object" then false
elif (.versions | type) != "array" then false
else .versions | length > 0 and
all(.[]; if type == "string" then test($pattern) else false end) and
(length == (unique | length))
end
' "$filename" >/dev/null || { fail "Documentation versions must be unique release tags in a non-empty list."; return 1; }
jq -r '.versions[]' "$filename"
}

verify_release() {
local tag="$1" release
release="$(gh api "repos/contentauth/c2pa-android/releases/tags/$tag")" || return 1
printf '%s' "$release" | jq -e --arg tag "$tag" '
type == "object" and .tag_name == $tag and .draft == false
' >/dev/null || { fail "$tag is not a published GitHub release."; return 1; }
}

export_release() {
local root="$1" tag="$2" destination="$3" commit entry mode path
commit="$(git -C "$root" rev-parse --verify "refs/tags/$tag^{commit}")" || return 1
git -C "$root" ls-tree -rz --full-tree "$commit" > "$destination.tree" || return 1
while IFS= read -r -d '' entry; do
mode="${entry%% *}"
path="${entry#*$'\t'}"
case "$mode" in
100644|100755) ;;
*) fail "Unsafe source entry in release $tag: $path"; return 1 ;;
esac
case "/$path/" in
//*|*/../*|*/./*|*/.git/*|*\\*|*:*) fail "Unsafe source path in release $tag: $path"; return 1 ;;
esac
done < "$destination.tree"
git -C "$root" archive --format=tar "$commit" > "$destination.tar" || return 1
tar -xf "$destination.tar" -C "$destination" || return 1
local required
for required in gradlew gradle/wrapper/gradle-wrapper.jar library/build.gradle.kts library/MODULE.md; do
[[ -f "$destination/$required" ]] || { fail "Release $tag is missing its Gradle wrapper or Kotlin API sources."; return 1; }
done
[[ -n "$(find "$destination/library/src/main/kotlin" -type f -name '*.kt' -print -quit)" ]] || {
fail "Release $tag is missing Kotlin API sources."; return 1;
}
}

build_docs() {
local stage="$1"
(
cd "$stage"
unset GH_TOKEN GITHUB_TOKEN
./gradlew --no-daemon --console=plain :library:dokkaGeneratePublicationHtml --rerun-tasks
) || return 1
[[ -f "$stage/build/docs/index.html" && -f "$stage/build/docs/$PACKAGE_INDEX" ]] || {
fail "Dokka did not generate the library and C2PA package landing pages."; return 1;
}
}

render_docs() {
local source="$1" output="$2" tag="$3" versions="$4" prefix="$5"
local page relative directory assets additions escaped_tag escaped_versions escaped_prefix
mkdir -p "$output"
cp -R "$source/." "$output/"
escaped_tag="$(printf '%s' "$tag" | jq -Rr '@html')"
escaped_versions="$(printf '%s' "$versions" | jq -Rr '@html')"
escaped_prefix="$(printf '%s' "$prefix" | jq -Rr '@html')"
find "$output" -type f -name '*.html' -print0 > "$output.pages"
while IFS= read -r -d '' page; do
relative="${page#"$output/"}"
[[ "$relative" != navigation.html ]] || continue
assets=""
directory="$relative"
while [[ "$directory" == */* ]]; do
assets="../$assets"
directory="${directory#*/}"
done
additions="<link rel=\"stylesheet\" href=\"${assets}doc-version-selector.css\">
<script defer src=\"${assets}doc-version-selector.js\" data-version=\"$escaped_tag\" data-versions=\"$escaped_versions\" data-prefix=\"$escaped_prefix\"></script>"
DOC_VERSION_ADDITIONS="$additions" awk '
BEGIN { additions = ENVIRON["DOC_VERSION_ADDITIONS"] }
{
if (index($0, "id=\"library-version\"")) slot = 1
position = index($0, "</head>")
if (position) {
head = 1
printf "%s%s\n%s\n", substr($0, 1, position - 1), additions, substr($0, position)
} else print
}
END { if (!head || !slot) exit 1 }
' "$page" > "$page.tmp" || { fail "Dokka page is missing its head or version slot: $relative"; return 1; }
mv "$page.tmp" "$page"
done < "$output.pages"
rm "$output.pages"
cp "$ROOT/.github/scripts/doc-version-selector.js" "$ROOT/.github/scripts/doc-version-selector.css" "$output/"
}

build_site() (
set -euo pipefail
local versions=() tag versions_json workspace output stage default_docs=""
mkdir -p "$ROOT/build"
output="$ROOT/build/docs-versioned"
workspace="$(mktemp -d "$ROOT/build/versioned-docs-XXXXXX")"
trap 'status=$?; if [[ -d "$workspace/previous" && ! -e "$output" ]]; then mv "$workspace/previous" "$output" || status=1; fi; rm -rf -- "$workspace"; exit "$status"' EXIT
load_versions "$ROOT/docs/versions.json" > "$workspace/versions"
while IFS= read -r tag; do versions+=("$tag"); done < "$workspace/versions"
versions_json="$(jq -c '.versions' "$ROOT/docs/versions.json")"
for tag in "${versions[@]}"; do verify_release "$tag"; done
for tag in "${versions[@]}"; do
stage="$workspace/$tag"
mkdir "$stage"
export_release "$ROOT" "$tag" "$stage"
build_docs "$stage"
[[ "$tag" != "${versions[0]}" ]] || default_docs="$stage/build/docs"
render_docs "$stage/build/docs" "$workspace/site/versions/$tag" "$tag" "$versions_json" "versions/$tag"
done
render_docs "$default_docs" "$workspace/default" "${versions[0]}" "$versions_json" ""
cp -R "$workspace/default/." "$workspace/site/"
touch "$workspace/site/.nojekyll"
if [[ -e "$output" ]]; then mv "$output" "$workspace/previous"; fi
mv "$workspace/site" "$output"
printf 'Versioned Dokka documentation generated at: %s\n' "$output"
)

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
build_site
fi
73 changes: 73 additions & 0 deletions .github/scripts/test-doc-version-selector.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const vm = require("node:vm");

const source = fs.readFileSync(path.join(__dirname, "doc-version-selector.js"), "utf8");
const versions = ["0.0.15", "0.0.14"];

function element() {
return {
children: [],
appendChild(child) { this.children.push(child); },
replaceChildren(child) { this.children = [child]; },
setAttribute(name, value) { this[name] = value; },
addEventListener(event, listener) { this[event] = listener; }
};
}

for (const base of [
"https://contentauth.github.io/c2pa-android/",
"https://docs.example.test/nested/sdk/"
]) {
for (const prefix of ["", ...versions.map(tag => "versions/" + tag)]) {
test("Dokka version selector: " + base + (prefix || "default"), function () {
const tag = prefix ? prefix.split("/").pop() : versions[0];
const container = element();
let destination;
vm.runInNewContext(source, {
URL,
document: {
currentScript: {
src: new URL((prefix ? prefix + "/" : "") + "doc-version-selector.js", base).href,
dataset: { versions: JSON.stringify(versions), version: tag, prefix }
},
getElementById(id) {
assert.equal(id, "library-version");
return container;
},
createElement: element
},
window: { location: { assign(url) { destination = url; } } }
});
assert.equal(container.children.length, 1);
const label = container.children[0];
assert.equal(label.id, "c2pa-doc-version-selector");
const select = label.children[0];
assert.equal(select["aria-label"], "API documentation version");
assert.equal(select.children.length, versions.length);
for (const [index, option] of select.children.entries()) {
assert.equal(option.value, base + "versions/" + versions[index] + "/");
assert.equal(option.selected, versions[index] === tag);
assert.equal(option.textContent, versions[index]);
}
select.value = select.children[1].value;
select.change();
assert.equal(destination, base + "versions/0.0.14/");
});
}
}

test("A missing Dokka version slot fails explicitly", function () {
assert.throws(() => vm.runInNewContext(source, {
URL,
document: {
currentScript: {
src: "https://contentauth.github.io/c2pa-android/doc-version-selector.js",
dataset: { versions: JSON.stringify(versions), version: versions[0], prefix: "" }
},
getElementById() { return null; }
}
}), /Dokka did not provide a library-version header slot/);
});
Loading
Loading