Skip to content

Commit 8a361c6

Browse files
committed
chore: simplify docs, render REST API page from OpenAPI spec file.
1 parent be4c23b commit 8a361c6

12 files changed

Lines changed: 430 additions & 2700 deletions

File tree

_static/custom.css

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@
1212
margin: 0 auto;
1313
}
1414

15-
/* Table font sizing */
15+
.mermaid-container {
16+
position: relative;
17+
}
18+
19+
.mermaid-fullscreen-btn {
20+
position: absolute;
21+
}
22+
1623
.rst-content table.docutils td,
1724
.rst-content table.docutils th {
1825
font-size: 0.8rem;
1926
}
2027

21-
/* Sphinx-Needs dark mode overrides for Furo */
2228
@media (prefers-color-scheme: dark) {
2329
body:not([data-theme="light"]) {
2430
--sn-color-need-bg: #1e1e2e;
@@ -49,7 +55,6 @@ body[data-theme="dark"] {
4955
--sn-color-debug-btn-border: #999;
5056
}
5157

52-
/* Graphviz/needflow dark mode adaptation */
5358
@media (prefers-color-scheme: dark) {
5459
body:not([data-theme="light"]) .graphviz svg text {
5560
fill: #d4d4d4 !important;
@@ -82,7 +87,6 @@ body[data-theme="dark"] .graphviz svg .edge polygon[fill="#999999"] {
8287
fill: #888 !important;
8388
}
8489

85-
/* Needpie chart: dark_background style blends on dark, needs inversion on light */
8690
img[id^="needpie-"] {
8791
border-radius: 8px;
8892
max-width: 100%;

_static/mermaid-panzoom.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
(() => {
2+
function attachPanZoom(svg) {
3+
if (!svg || svg.dataset.panzoomAttached === "true") {
4+
return;
5+
}
6+
7+
const container = svg.closest(".mermaid-container-fullscreen");
8+
if (!container) {
9+
return;
10+
}
11+
12+
svg.dataset.panzoomAttached = "true";
13+
14+
// Keep panning predictable inside fullscreen modal.
15+
container.style.overflow = "hidden";
16+
container.style.touchAction = "none";
17+
18+
let scale = 1;
19+
let tx = 0;
20+
let ty = 0;
21+
let dragging = false;
22+
let dragOffsetX = 0;
23+
let dragOffsetY = 0;
24+
25+
const minScale = 0.25;
26+
const maxScale = 6;
27+
28+
const applyTransform = () => {
29+
svg.style.transformOrigin = "0 0";
30+
svg.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`;
31+
};
32+
33+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
34+
35+
applyTransform();
36+
svg.style.cursor = "grab";
37+
38+
svg.addEventListener(
39+
"wheel",
40+
(event) => {
41+
event.preventDefault();
42+
43+
const rect = container.getBoundingClientRect();
44+
const cx = event.clientX - rect.left;
45+
const cy = event.clientY - rect.top;
46+
47+
const factor = event.deltaY < 0 ? 1.12 : 0.88;
48+
const nextScale = clamp(scale * factor, minScale, maxScale);
49+
50+
// Zoom around cursor position.
51+
tx = cx - ((cx - tx) * nextScale) / scale;
52+
ty = cy - ((cy - ty) * nextScale) / scale;
53+
scale = nextScale;
54+
55+
applyTransform();
56+
},
57+
{ passive: false }
58+
);
59+
60+
svg.addEventListener("pointerdown", (event) => {
61+
if (event.button !== 0) {
62+
return;
63+
}
64+
dragging = true;
65+
dragOffsetX = event.clientX - tx;
66+
dragOffsetY = event.clientY - ty;
67+
svg.style.cursor = "grabbing";
68+
if (svg.setPointerCapture) {
69+
svg.setPointerCapture(event.pointerId);
70+
}
71+
event.preventDefault();
72+
});
73+
74+
svg.addEventListener("pointermove", (event) => {
75+
if (!dragging) {
76+
return;
77+
}
78+
tx = event.clientX - dragOffsetX;
79+
ty = event.clientY - dragOffsetY;
80+
applyTransform();
81+
});
82+
83+
const stopDragging = (event) => {
84+
if (!dragging) {
85+
return;
86+
}
87+
dragging = false;
88+
svg.style.cursor = "grab";
89+
if (event && svg.releasePointerCapture) {
90+
try {
91+
svg.releasePointerCapture(event.pointerId);
92+
} catch {
93+
// No-op: pointer may already be released.
94+
}
95+
}
96+
};
97+
98+
svg.addEventListener("pointerup", stopDragging);
99+
svg.addEventListener("pointercancel", stopDragging);
100+
svg.addEventListener("pointerleave", stopDragging);
101+
102+
// Double-click to reset view.
103+
svg.addEventListener("dblclick", (event) => {
104+
event.preventDefault();
105+
scale = 1;
106+
tx = 0;
107+
ty = 0;
108+
applyTransform();
109+
});
110+
}
111+
112+
function initFullscreenPanZoom() {
113+
const installForActiveModal = () => {
114+
document
115+
.querySelectorAll(".mermaid-fullscreen-modal.active .mermaid svg")
116+
.forEach(attachPanZoom);
117+
};
118+
119+
const observer = new MutationObserver(() => {
120+
installForActiveModal();
121+
});
122+
123+
observer.observe(document.body, {
124+
subtree: true,
125+
childList: true,
126+
attributes: true,
127+
attributeFilter: ["class"],
128+
});
129+
130+
document.addEventListener("click", (event) => {
131+
if (!event.target.closest(".mermaid-fullscreen-btn")) {
132+
return;
133+
}
134+
// Wait for extension script to clone and insert the SVG into modal.
135+
requestAnimationFrame(() => {
136+
requestAnimationFrame(installForActiveModal);
137+
});
138+
});
139+
140+
installForActiveModal();
141+
}
142+
143+
if (document.readyState === "loading") {
144+
document.addEventListener("DOMContentLoaded", initFullscreenPanZoom);
145+
} else {
146+
initFullscreenPanZoom();
147+
}
148+
})();

_static/openapi.yaml

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1826,6 +1826,26 @@ paths:
18261826
summary: Create annotation
18271827
description: Creates a new text highlight annotation.
18281828
operationId: createAnnotation
1829+
requestBody:
1830+
required: true
1831+
content:
1832+
application/json:
1833+
schema:
1834+
$ref: '#/components/schemas/CreateAnnotationRequest'
1835+
responses:
1836+
'201':
1837+
description: Annotation created
1838+
content:
1839+
application/json:
1840+
schema:
1841+
$ref: '#/components/schemas/Annotation'
1842+
'400':
1843+
$ref: '#/components/responses/ValidationError'
1844+
'401':
1845+
$ref: '#/components/responses/UnauthorizedError'
1846+
'404':
1847+
$ref: '#/components/responses/NotFoundError'
1848+
18291849

18301850
/books/{book_id}/annotations/export:
18311851
parameters:
@@ -1882,27 +1902,6 @@ paths:
18821902
'404':
18831903
$ref: '#/components/responses/NotFoundError'
18841904

1885-
/annotations/{annotation_id}:
1886-
requestBody:
1887-
required: true
1888-
content:
1889-
application/json:
1890-
schema:
1891-
$ref: '#/components/schemas/CreateAnnotationRequest'
1892-
responses:
1893-
'201':
1894-
description: Annotation created
1895-
content:
1896-
application/json:
1897-
schema:
1898-
$ref: '#/components/schemas/Annotation'
1899-
'400':
1900-
$ref: '#/components/responses/ValidationError'
1901-
'401':
1902-
$ref: '#/components/responses/UnauthorizedError'
1903-
'404':
1904-
$ref: '#/components/responses/NotFoundError'
1905-
19061905
/annotations/{annotation_id}:
19071906
parameters:
19081907
- $ref: '#/components/parameters/AnnotationIdParam'

api/index.rst

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,9 @@
1-
API
2-
===
1+
REST API
2+
========
33

4-
This section contains the complete REST API documentation for the Papyrus Server.
5-
6-
Overview
7-
--------
8-
9-
The Papyrus Server provides a RESTful API for:
10-
11-
- **Authentication** -- User registration, login, OAuth, and session management
12-
- **Books** -- CRUD operations for book metadata and file references
13-
- **Organization** -- Shelves, tags, and series management
14-
- **Annotations** -- Highlights, notes, and bookmarks
15-
- **Progress** -- Reading sessions and statistics
16-
- **Goals** -- Reading goal tracking
17-
- **Sync** -- Cross-device synchronization
18-
- **Storage** -- File storage backend configuration
19-
- **Files** -- File upload/download (when server is file backend)
20-
21-
Authentication
22-
--------------
23-
24-
Most endpoints require authentication via JWT Bearer token:
25-
26-
.. code-block:: text
27-
28-
Authorization: Bearer <access_token>
29-
30-
Obtain tokens through the ``/auth/login`` or ``/auth/oauth/google`` endpoints. Access tokens expire after 1 hour. Use the refresh token to obtain new access tokens via ``/auth/refresh``.
31-
32-
Base URL
33-
--------
34-
35-
.. list-table::
36-
:header-rows: 1
37-
38-
* - Environment
39-
- URL
40-
* - Production
41-
- ``https://api.papyrus.app/v1``
42-
* - Staging
43-
- ``https://staging-api.papyrus.app/v1``
44-
* - Local
45-
- ``http://localhost:8080/v1``
46-
47-
Download specification
48-
----------------------
49-
50-
- `OpenAPI Specification (YAML) <_static/openapi.yaml>`_ -- Full API specification file
4+
.. openapi:: /_static/openapi.yaml
5+
:examples:
6+
:group:
517

528
Related documentation
539
---------------------

conf.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
1-
"""Sphinx configuration for Papyrus documentation."""
2-
31
project = "Papyrus"
4-
copyright = "2025, Papyrus"
5-
author = "Papyrus"
6-
7-
extensions = [
8-
"sphinx_needs",
9-
"sphinxcontrib.mermaid",
10-
"sphinx.ext.graphviz",
11-
]
2+
copyright = "2026, Papyrus"
3+
author = "Karolis Strazdas"
124

13-
# -- Theme -------------------------------------------------------------------
5+
extensions = ["sphinx_needs", "sphinxcontrib.mermaid", "sphinx.ext.graphviz", "sphinxcontrib.openapi"]
146

157
html_theme = "furo"
168
html_static_path = ["_static"]
179
html_css_files = ["custom.css"]
10+
html_js_files = ["mermaid-panzoom.js"]
1811
html_title = "Papyrus"
1912

2013
html_theme_options = {
@@ -28,8 +21,6 @@
2821
},
2922
}
3023

31-
# -- Sphinx-Needs ------------------------------------------------------------
32-
3324
needs_types = [
3425
dict(
3526
directive="fr",
@@ -105,20 +96,11 @@
10596

10697
needs_id_required = True
10798
needs_id_regex = r"^(FR|NFR|UC)-[0-9]+_[0-9]+(_[0-9]+)?$"
108-
10999
needs_default_style = ""
110100
needs_role_need_template = "{title} ({id})"
111-
112-
# -- Graphviz ----------------------------------------------------------------
113-
114101
graphviz_output_format = "svg"
115-
116-
# -- Mermaid -----------------------------------------------------------------
117-
118102
mermaid_output_format = "raw"
119103

120-
# -- General -----------------------------------------------------------------
121-
122104
exclude_patterns = [
123105
"_build",
124106
"src",

0 commit comments

Comments
 (0)