Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e06204d6e0 | ||
|
|
8209d4844d | ||
|
|
dc84a2b053 | ||
|
|
3944f688f4 | ||
|
|
96e2de60ae | ||
|
|
b9de2eb346 |
No files matched your search
@@ -0,0 +1,20 @@
|
||||
version: 2
|
||||
build:
|
||||
os: ubuntu-24.04
|
||||
tools:
|
||||
python: "3.13"
|
||||
jobs:
|
||||
build:
|
||||
html:
|
||||
- uv run --no-project --with-requirements .github/docs/requirements.txt sphinx-build -W --keep-going -b html -c .github/docs docs "$READTHEDOCS_OUTPUT/html"
|
||||
submodules:
|
||||
include:
|
||||
- docs
|
||||
python:
|
||||
install:
|
||||
- method: uv
|
||||
command: pip
|
||||
requirements: .github/docs/requirements.txt
|
||||
sphinx:
|
||||
configuration: .github/docs/conf.py
|
||||
fail_on_warning: true
|
||||
@@ -0,0 +1,43 @@
|
||||
# Building the 0.7.4 documentation
|
||||
|
||||
The `docs` submodule remains pinned to the wiki revision shipped with 0.7.4.
|
||||
This configuration renders that historical user guide without installing
|
||||
ArchiveBox, importing Django, or updating the wiki checkout.
|
||||
|
||||
Prerequisites: [uv](https://docs.astral.sh/uv/getting-started/installation/) 0.11.3 and Python 3.13, matching CI and Read the Docs. `uv` can install Python when needed.
|
||||
|
||||
```sh
|
||||
git submodule update --init docs
|
||||
uv run --no-project --python 3.13 --with-requirements .github/docs/requirements.txt \
|
||||
sphinx-build -W --keep-going -b html -c .github/docs docs /tmp/archivebox-docs
|
||||
```
|
||||
|
||||
`conf.py` adapts GitHub wiki links, heading levels, and obsolete local anchors
|
||||
in memory. Configuration options without a prose section link to their exact
|
||||
0.7.4 source definition. The original user documentation is not rewritten.
|
||||
Generated Python autodoc scaffolding is outside this standalone user guide;
|
||||
its legacy Django-dependent builder is not loaded.
|
||||
|
||||
Read the Docs uses `.github/.readthedocs.yaml` and the same strict HTML command.
|
||||
Keep its project configuration-file setting pointed at that path.
|
||||
|
||||
`requirements.txt` pins the complete documentation dependency set. To update it,
|
||||
edit `requirements.in` and run:
|
||||
|
||||
```sh
|
||||
uv pip compile --universal --python-version 3.13 .github/docs/requirements.in \
|
||||
--output-file .github/docs/requirements.txt
|
||||
```
|
||||
|
||||
## Live version picker
|
||||
|
||||
Read the Docs loads `https://docs.archivebox.io/docs-live-picker/_static/archivebox-versions.js`
|
||||
through Addons > Custom script. The hidden `docs-live-picker` version keeps this
|
||||
asset available independently of future main or release documentation builds. This applies the release policy and puts `dev`
|
||||
after `latest` across existing versions without rebuilding them. The script uses
|
||||
the published version inventory and preserves existing navigation URLs.
|
||||
|
||||
The hosting checkout command checks out the original main/tag commit, then overlays
|
||||
only the historical Sphinx build files from pinned docs repair commits. Application
|
||||
source, release tags, and pinned wiki revisions remain those of the selected release.
|
||||
Keep the docs repair branches available for fetching these configuration commits.
|
||||
@@ -0,0 +1,200 @@
|
||||
"""Build the pinned 0.7.4 user guide without importing ArchiveBox or Django."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
from markdown_it import MarkdownIt
|
||||
from myst_parser.mdit_to_docutils.base import default_slugify
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DOCS = ROOT / "docs"
|
||||
project = "ArchiveBox"
|
||||
author = "ArchiveBox contributors"
|
||||
copyright = "ArchiveBox contributors"
|
||||
html_show_sphinx = False
|
||||
release = json.loads((ROOT / "package.json").read_text())["version"]
|
||||
version = release
|
||||
extensions = ["myst_parser", "sphinx_rtd_theme"]
|
||||
source_suffix = {".rst": "restructuredtext", ".md": "markdown"}
|
||||
root_doc = "index"
|
||||
# These are generated autodoc scaffolding, not standalone user documentation.
|
||||
exclude_patterns = [
|
||||
"archivebox*.rst",
|
||||
"modules.rst",
|
||||
"_Sidebar.md",
|
||||
"_Footer.md",
|
||||
".git",
|
||||
]
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_options = {
|
||||
"navigation_depth": 3,
|
||||
"collapse_navigation": False,
|
||||
"version_selector": True,
|
||||
"language_selector": False,
|
||||
}
|
||||
html_context = {"current_version": os.environ.get("READTHEDOCS_VERSION", release)}
|
||||
html_logo = str(DOCS / "logo.png")
|
||||
html_static_path = [str(DOCS / "_static"), str(ROOT / ".github/docs/static")]
|
||||
html_title = f"ArchiveBox {release} documentation"
|
||||
myst_heading_anchors = 6
|
||||
|
||||
|
||||
def rewrite_prose(text, transform):
|
||||
"""Rewrite navigation outside fenced and indented code blocks."""
|
||||
lines = text.splitlines(keepends=True)
|
||||
output = []
|
||||
start = 0
|
||||
for token in MarkdownIt().parse(text):
|
||||
if token.type not in {"fence", "code_block"}:
|
||||
continue
|
||||
first, last = token.map
|
||||
output.append(transform("".join(lines[start:first])))
|
||||
output.append("".join(lines[first:last]))
|
||||
start = last
|
||||
output.append(transform("".join(lines[start:])))
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def prepare_source(app, docname, source):
|
||||
"""Adapt GitHub wiki navigation in memory, preserving the pinned sources."""
|
||||
text = source[0]
|
||||
if docname == "Contents":
|
||||
text = re.sub(
|
||||
r"API Reference\n#############.*?(?=Meta\n####)", "", text, flags=re.DOTALL
|
||||
)
|
||||
text += "\n.. toctree::\n :hidden:\n\n Home\n"
|
||||
if docname == "index":
|
||||
text = text.replace("archivebox info", "archivebox status")
|
||||
text = text.replace(
|
||||
"pip install archivebox", f"pip install archivebox=={release}"
|
||||
)
|
||||
text = text.replace(
|
||||
"ArchiveBox/ArchiveBox/tree/master",
|
||||
f"ArchiveBox/ArchiveBox/tree/v{release}",
|
||||
)
|
||||
text = text.replace(
|
||||
"`Github <https://github.com/ArchiveBox/ArchiveBox/issues>`_",
|
||||
"`GitHub issues <https://github.com/ArchiveBox/ArchiveBox/issues>`_",
|
||||
)
|
||||
text = text.replace(
|
||||
"==========\nArchiveBox\n==========",
|
||||
f"ArchiveBox {release}\n" + "=" * (11 + len(release)),
|
||||
)
|
||||
if Path(app.env.doc2path(docname)).suffix == ".md":
|
||||
|
||||
def wiki_link(match):
|
||||
parts = match.group(1).split("|", 1)
|
||||
label, target = (parts[0], parts[-1])
|
||||
page, separator, anchor = unquote(target).partition("#")
|
||||
page = page.replace(" ", "-")
|
||||
suffix = f"#{anchor}" if separator else ""
|
||||
return f"[{label}]({page}.md{suffix})"
|
||||
|
||||
text = rewrite_prose(
|
||||
text, lambda prose: re.sub(r"\[\[([^\]\n]+)\]\]", wiki_link, prose)
|
||||
)
|
||||
source[0] = text
|
||||
|
||||
|
||||
# GitHub wiki headings allow skipped levels; normalize their hierarchy before MyST
|
||||
# parses them, without changing fenced code blocks or the historical source files.
|
||||
def normalize_markdown(app, docname, source):
|
||||
if Path(app.env.doc2path(docname)).suffix != ".md":
|
||||
return
|
||||
text = source[0]
|
||||
if docname in {"Home", "README", "Donations", "Upgrading-or-Merging-Archives"}:
|
||||
text = (
|
||||
"# "
|
||||
+ (
|
||||
"ArchiveBox overview"
|
||||
if docname == "README"
|
||||
else docname.replace("-", " ")
|
||||
)
|
||||
+ "\n\n"
|
||||
+ text
|
||||
)
|
||||
lines = text.splitlines()
|
||||
stack = []
|
||||
for token in MarkdownIt().parse(text):
|
||||
if token.type != "heading_open":
|
||||
continue
|
||||
original = int(token.tag[1])
|
||||
while stack and stack[-1] >= original:
|
||||
stack.pop()
|
||||
stack.append(original)
|
||||
line = token.map[0]
|
||||
lines[line] = re.sub(r"^#{1,6}(?=\s)", "#" * len(stack), lines[line]).replace(
|
||||
"\ufe0f", ""
|
||||
)
|
||||
text = "\n".join(lines) + "\n"
|
||||
|
||||
def link(match):
|
||||
target = match.group(1)
|
||||
if "://" in target or target.startswith("mailto:"):
|
||||
return match.group(0)
|
||||
page, sep, anchor = target.partition("#")
|
||||
if page and (DOCS / (page + ".md")).exists():
|
||||
page += ".md"
|
||||
# MyST resolves Markdown GitHub slugs (e.g. background--motivation)
|
||||
# to rendered section IDs; raw HTML links below need explicit rewrites.
|
||||
anchor = anchor.lower()
|
||||
if (docname == "Configuration" and not page) or page == "Configuration.md":
|
||||
other = (DOCS / "Configuration.md").read_text()
|
||||
for heading in re.findall(r"^#+\s+(.+)$", other, re.MULTILINE):
|
||||
if anchor in [
|
||||
word.lower() for word in re.findall(r"`([^`]+)`", heading)
|
||||
]:
|
||||
anchor = default_slugify(heading.replace("`", ""))
|
||||
break
|
||||
aliases = {"templates_dir": "custom_templates_dir"}
|
||||
anchor = aliases.get(anchor, anchor)
|
||||
if anchor in {
|
||||
"search_backend_timeout",
|
||||
"search_backend_engine",
|
||||
"use_curl",
|
||||
}:
|
||||
code = (ROOT / "archivebox/config.py").read_text().splitlines()
|
||||
number = next(
|
||||
i + 1 for i, line in enumerate(code) if anchor.upper() in line
|
||||
)
|
||||
return (
|
||||
"](https://github.com/ArchiveBox/ArchiveBox/blob/v0.7.4/archivebox/config.py#L"
|
||||
+ str(number)
|
||||
+ ")"
|
||||
)
|
||||
if (
|
||||
docname == "README"
|
||||
and anchor == "saves-lots-of-useful-stuff-for-each-imported-link"
|
||||
):
|
||||
anchor = "output-formats"
|
||||
if docname == "Web-Archiving-Community":
|
||||
anchor = {
|
||||
"blogs": "blogs-friends-of-archivebox",
|
||||
"articles": "articles-we-like-about-internet-archiving",
|
||||
}.get(anchor, anchor)
|
||||
return "](" + page + ("#" + anchor if sep else "") + ")"
|
||||
|
||||
def rewrite_links(prose):
|
||||
prose = re.sub(r"\]\(([^)\s]+)\)", link, prose)
|
||||
# Raw HTML links are outside MyST's cross-reference resolver.
|
||||
html_anchors = {
|
||||
"background--motivation": "background-motivation",
|
||||
"Caveats": "caveats",
|
||||
"contents": "web-archiving-community",
|
||||
"%EF%B8%8F-cli-usage": "cli-usage",
|
||||
}
|
||||
for old, new in html_anchors.items():
|
||||
prose = prose.replace(f'href="#{old}"', f'href="#{new}"')
|
||||
prose = prose.replace("#️-cli-usage", "#cli-usage")
|
||||
return prose
|
||||
|
||||
text = rewrite_prose(text, rewrite_links)
|
||||
source[0] = text
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.connect("source-read", prepare_source)
|
||||
app.connect("source-read", normalize_markdown)
|
||||
@@ -0,0 +1,3 @@
|
||||
Sphinx==8.2.3
|
||||
myst-parser==4.0.1
|
||||
sphinx-rtd-theme==3.1.0
|
||||
@@ -0,0 +1,75 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --universal --python-version 3.13 .github/docs/requirements.in --output-file .github/docs/requirements.txt
|
||||
alabaster==1.0.0
|
||||
# via sphinx
|
||||
babel==2.18.0
|
||||
# via sphinx
|
||||
certifi==2026.7.22
|
||||
# via requests
|
||||
charset-normalizer==3.5.1
|
||||
# via requests
|
||||
colorama==0.4.6 ; sys_platform == 'win32'
|
||||
# via sphinx
|
||||
docutils==0.21.2
|
||||
# via
|
||||
# myst-parser
|
||||
# sphinx
|
||||
# sphinx-rtd-theme
|
||||
idna==3.20
|
||||
# via requests
|
||||
imagesize==2.0.1
|
||||
# via sphinx
|
||||
jinja2==3.1.6
|
||||
# via
|
||||
# myst-parser
|
||||
# sphinx
|
||||
markdown-it-py==3.0.0
|
||||
# via
|
||||
# mdit-py-plugins
|
||||
# myst-parser
|
||||
markupsafe==3.0.3
|
||||
# via jinja2
|
||||
mdit-py-plugins==0.6.1
|
||||
# via myst-parser
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
myst-parser==4.0.1
|
||||
# via -r .github/docs/requirements.in
|
||||
packaging==26.3
|
||||
# via sphinx
|
||||
pygments==2.21.0
|
||||
# via sphinx
|
||||
pyyaml==6.0.3
|
||||
# via myst-parser
|
||||
requests==2.34.2
|
||||
# via sphinx
|
||||
roman-numerals==4.1.0
|
||||
# via roman-numerals-py
|
||||
roman-numerals-py==4.1.0
|
||||
# via sphinx
|
||||
snowballstemmer==3.1.1
|
||||
# via sphinx
|
||||
sphinx==8.2.3
|
||||
# via
|
||||
# -r .github/docs/requirements.in
|
||||
# myst-parser
|
||||
# sphinx-rtd-theme
|
||||
# sphinxcontrib-jquery
|
||||
sphinx-rtd-theme==3.1.0
|
||||
# via -r .github/docs/requirements.in
|
||||
sphinxcontrib-applehelp==2.0.0
|
||||
# via sphinx
|
||||
sphinxcontrib-devhelp==2.0.0
|
||||
# via sphinx
|
||||
sphinxcontrib-htmlhelp==2.1.0
|
||||
# via sphinx
|
||||
sphinxcontrib-jquery==4.1
|
||||
# via sphinx-rtd-theme
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
# via sphinx
|
||||
sphinxcontrib-qthelp==2.0.0
|
||||
# via sphinx
|
||||
sphinxcontrib-serializinghtml==2.0.0
|
||||
# via sphinx
|
||||
urllib3==2.8.0
|
||||
# via requests
|
||||
@@ -0,0 +1,106 @@
|
||||
/* ArchiveBox RTD picker policy. No requests, credentials, or documentation edits.
|
||||
* RTD event is read-only input: internal flyout rendering precedes the public
|
||||
* event, so changing its payload is not a supported rendering hook.
|
||||
* Targets: addons readthedocs-flyout shadow dl.versions, RTD theme 3.1 select,
|
||||
* and the legacy theme's Versions definition list. Other navigation is untouched.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function parse(slug) {
|
||||
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-?rc(\d*))?$/i.exec(slug);
|
||||
if (!match) return null;
|
||||
return { slug, major: +match[1], minor: +match[2], patch: +match[3],
|
||||
rc: match[4] === undefined ? null : +(match[4] || 0) };
|
||||
}
|
||||
function compare(a, b) {
|
||||
return a.major - b.major || a.minor - b.minor || a.patch - b.patch ||
|
||||
(a.rc === null ? Infinity : a.rc) - (b.rc === null ? Infinity : b.rc) ||
|
||||
a.slug.localeCompare(b.slug);
|
||||
}
|
||||
function select(versions) {
|
||||
const published = versions.filter(v => v.active !== false && v.built !== false && !v.hidden);
|
||||
const parsed = published.map(v => parse(v.slug)).filter(Boolean);
|
||||
const minors = [...new Set(parsed.sort((a, b) => compare(b, a))
|
||||
.map(v => `${v.major}.${v.minor}`))];
|
||||
const winners = minors.map((minor, index) => parsed.filter(v =>
|
||||
`${v.major}.${v.minor}` === minor && (index < 2 ? v.rc !== null : v.rc === null))
|
||||
.sort((a, b) => compare(b, a))[0]).filter(Boolean).map(v => v.slug);
|
||||
return ["latest", "dev"].filter(slug => published.some(v => v.slug === slug)).concat(winners);
|
||||
}
|
||||
|
||||
// Export only the pure policy for Node verification; browser has no new globals.
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = { parse, select };
|
||||
return;
|
||||
}
|
||||
const project = document.querySelector('meta[name="readthedocs-project-slug"]')?.content;
|
||||
if (project && project !== "archivebox") return;
|
||||
if (!document.querySelector('meta[name="readthedocs-addons-api-version"]')) {
|
||||
const meta = document.createElement("meta");
|
||||
meta.name = "readthedocs-addons-api-version";
|
||||
meta.content = "1";
|
||||
document.head.append(meta);
|
||||
}
|
||||
let wanted = null;
|
||||
const watched = new WeakSet();
|
||||
function reconcile(container) {
|
||||
if (!wanted) return;
|
||||
const isSelect = container.tagName === "SELECT";
|
||||
const children = [...container.children].filter(n => isSelect ? n.tagName === "OPTION" : n.tagName === "DD");
|
||||
const slug = n => isSelect ? n.value : n.querySelector("a")?.textContent.trim();
|
||||
const selected = isSelect ? container.value : null;
|
||||
const kept = wanted.map(s => children.find(n => slug(n) === s)).filter(Boolean);
|
||||
const current = children.filter(n => wanted.includes(slug(n)));
|
||||
for (const node of children) if (!wanted.includes(slug(node))) node.remove();
|
||||
if (kept.some((node, i) => node !== current[i])) {
|
||||
for (const node of kept) container.append(node);
|
||||
}
|
||||
if (isSelect) {
|
||||
container.value = selected;
|
||||
if (!kept.some(n => n.value === selected)) container.selectedIndex = -1;
|
||||
}
|
||||
}
|
||||
function watch(container) {
|
||||
reconcile(container);
|
||||
if (watched.has(container)) return;
|
||||
watched.add(container);
|
||||
new MutationObserver(() => reconcile(container)).observe(container, { childList: true, subtree: true });
|
||||
}
|
||||
function scan() {
|
||||
if (!wanted) return;
|
||||
document.querySelectorAll("div.switch-menus > div.version-switch select").forEach(watch);
|
||||
document.querySelectorAll(".rst-other-versions dl").forEach(dl => {
|
||||
if (dl.querySelector("dt")?.textContent.trim() === "Versions") watch(dl);
|
||||
});
|
||||
document.querySelectorAll("readthedocs-flyout").forEach(host => {
|
||||
const root = host.shadowRoot;
|
||||
if (!root) return;
|
||||
root.querySelectorAll("dl.versions").forEach(watch);
|
||||
if (!watched.has(root)) {
|
||||
watched.add(root);
|
||||
new MutationObserver(() => root.querySelectorAll("dl.versions").forEach(watch))
|
||||
.observe(root, { childList: true, subtree: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
customElements.whenDefined("readthedocs-flyout").then(scan);
|
||||
let discovery;
|
||||
let stopDiscovery;
|
||||
function ready(eventData) {
|
||||
const config = eventData.data();
|
||||
if (config.projects?.current?.slug !== "archivebox" || !Array.isArray(config.versions?.active)) return;
|
||||
wanted = select(config.versions.active);
|
||||
if (!wanted.length) return;
|
||||
scan();
|
||||
// Discover asynchronously inserted picker containers for at most 10 seconds.
|
||||
// Afterwards, observers remain scoped to known picker/shadow containers only.
|
||||
discovery?.disconnect();
|
||||
clearTimeout(stopDiscovery);
|
||||
discovery = new MutationObserver(scan);
|
||||
discovery.observe(document.body, { childList: true, subtree: true });
|
||||
stopDiscovery = setTimeout(() => discovery.disconnect(), 10000);
|
||||
}
|
||||
document.addEventListener("readthedocs-addons-data-ready", event => ready(event.detail));
|
||||
if (globalThis.ReadTheDocsEventData) ready(globalThis.ReadTheDocsEventData);
|
||||
})();
|
||||
@@ -3,8 +3,9 @@ name: Build Docker image
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
branches-ignore:
|
||||
- docs
|
||||
- 'docs/**'
|
||||
tags:
|
||||
- 'v*'
|
||||
# pull_request:
|
||||
@@ -14,6 +15,8 @@ env:
|
||||
|
||||
jobs:
|
||||
buildx:
|
||||
# Also prevent a manual dispatch on a docs repair branch from publishing.
|
||||
if: github.ref != 'refs/heads/docs' && !startsWith(github.ref, 'refs/heads/docs/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Historical Sphinx documentation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths: ['.github/docs/**', '.github/.readthedocs.yaml', '.github/workflows/docs.yml', 'docs', 'README.md', 'package.json', 'archivebox/config.py']
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['.github/docs/**', '.github/.readthedocs.yaml', '.github/workflows/docs.yml', 'docs', 'README.md', 'package.json', 'archivebox/config.py']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- run: git submodule update --init docs
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
||||
with:
|
||||
version: '0.11.3'
|
||||
python-version: '3.13'
|
||||
- name: Build the pinned historical user guide
|
||||
env:
|
||||
READTHEDOCS: 'True'
|
||||
READTHEDOCS_VERSION: latest
|
||||
run: uv run --no-project --with-requirements .github/docs/requirements.txt sphinx-build -W --keep-going -b html -c .github/docs docs "$RUNNER_TEMP/docs-html"
|
||||
- name: Verify the version selector
|
||||
run: |
|
||||
grep -q 'class="version-switch"' "$RUNNER_TEMP/docs-html/index.html"
|
||||
grep -q 'readthedocs-addons-data-ready' "$RUNNER_TEMP/docs-html/_static/js/versions.js"
|
||||
Reference in new issue
Block a user