Offer the manual as PDF and EPUB, laid out for print
Benchmarks / CPU and I/O (per commit) (push) Successful in 3m5s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Successful in 1h5m29s
Build and test / Layer separation (push) Successful in 29s
🐳 Android image / Build and push (push) Successful in 5s
Build and test / android-image (push) Successful in 8s
🐳 Windows image / Build and push (push) Successful in 5s
Build and test / windows-image (push) Successful in 7s
Manual pages / Publish the manual (push) Successful in 1m2s
Traceability / Requirement traces (push) Successful in 1m1s
Build and test / Android (aarch64) (push) Successful in 23m57s
Build and test / Windows (x86_64, cross) (push) Successful in 55m49s
Build and test / Publish the release (push) Skipped
Benchmarks / CPU and I/O (per commit) (push) Successful in 3m5s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Successful in 1h5m29s
Build and test / Layer separation (push) Successful in 29s
🐳 Android image / Build and push (push) Successful in 5s
Build and test / android-image (push) Successful in 8s
🐳 Windows image / Build and push (push) Successful in 5s
Build and test / windows-image (push) Successful in 7s
Manual pages / Publish the manual (push) Successful in 1m2s
Traceability / Requirement traces (push) Successful in 1m1s
Build and test / Android (aarch64) (push) Successful in 23m57s
Build and test / Windows (x86_64, cross) (push) Successful in 55m49s
Build and test / Publish the release (push) Skipped
The page gains a print stylesheet: A4 with a cover, a contents page whose entries carry their page numbers, a chapter to a page with its name at the head of each page and the page count at the foot, figures and table rows never split, and the light palette. A line above the title links the PDF and the EPUB on the published site, absolutely, since an installed copy has none beside it. The Manual pages workflow makes both: tools/manual/pdf.py renders the page with WeasyPrint from JPEG copies of its pictures, which takes the PDF from 28 MB to 8, and pandoc makes the EPUB from the Markdown.
This commit is contained in:
@@ -39,6 +39,16 @@ use pulldown_cmark::{CowStr, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
|
||||
/// is where the reader of a design document is anyway.
|
||||
pub const FORGE_TREE: &str = "https://gitea.tourolle.paris/dtourolle/DarkRoom/src/branch/master/";
|
||||
|
||||
/// Where the manual is published, with its PDF and EPUB beside the page
|
||||
/// (`.gitea/workflows/manual-pages.yml`). The page links them absolutely: an
|
||||
/// installed copy has no PDF next to it, and a download is a thing done
|
||||
/// online anyway.
|
||||
pub const PAGES_SITE: &str = "https://pages.tourolle.paris/dtourolle/darkroom/";
|
||||
|
||||
/// The downloads' file names on [`PAGES_SITE`], which the workflow writes.
|
||||
pub const PDF_NAME: &str = "darkroom-manual.pdf";
|
||||
pub const EPUB_NAME: &str = "darkroom-manual.epub";
|
||||
|
||||
/// The manual's directory inside the repository, for resolving its links.
|
||||
const MANUAL_DIR: &str = "docs/manual";
|
||||
|
||||
@@ -274,11 +284,22 @@ pub fn render_html(markdown: &str) -> String {
|
||||
page.push_str("<style>\n");
|
||||
page.push_str(STYLE);
|
||||
page.push_str("</style>\n</head>\n<body>\n<div class=\"page\">\n");
|
||||
// The PDF's first page. Hidden on screen, where the title heads the text.
|
||||
page.push_str(&format!(
|
||||
"<header class=\"cover\"><p class=\"cover-title\">{}</p>\
|
||||
<p class=\"cover-sub\">The DarkRoom manual · {PAGES_SITE}</p></header>\n",
|
||||
escape(&title)
|
||||
));
|
||||
page.push_str(
|
||||
"<nav class=\"toc\" aria-label=\"Contents\">\n<p class=\"toc-title\">Contents</p>\n",
|
||||
);
|
||||
page.push_str(&toc);
|
||||
page.push_str("</nav>\n<main>\n");
|
||||
page.push_str(&format!(
|
||||
"<p class=\"downloads\">Also as <a href=\"{PAGES_SITE}{PDF_NAME}\" download>PDF</a> \
|
||||
· <a href=\"{PAGES_SITE}{EPUB_NAME}\" download>EPUB</a> · \
|
||||
<a href=\"{PAGES_SITE}\">online</a></p>\n"
|
||||
));
|
||||
page.push_str(&body);
|
||||
page.push_str("</main>\n</div>\n</body>\n</html>\n");
|
||||
page
|
||||
@@ -420,11 +441,45 @@ figcaption { margin-top: 0.4rem; color: var(--ink-dim); font-size: 0.9rem; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 0.95rem; }
|
||||
th, td { text-align: left; padding: 0.4rem 0.6rem; border-bottom: 1px solid var(--rule); vertical-align: top; }
|
||||
th { color: var(--ink-dim); font-weight: 600; }
|
||||
td img { aspect-ratio: auto 1 / 1; }
|
||||
.downloads { margin: 0 0 1.2rem; color: var(--ink-dim); font-size: 0.9rem; }
|
||||
.cover { display: none; }
|
||||
@media (max-width: 52rem) {
|
||||
.page { grid-template-columns: minmax(0, 1fr); gap: 1rem; padding: 1rem 16px 3rem; }
|
||||
.toc { position: static; max-height: none; border: 1px solid var(--rule); border-radius: 6px; padding: 0.8rem 1rem; background: var(--panel); }
|
||||
body { font-size: 16px; }
|
||||
}
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 18mm 16mm 20mm;
|
||||
@top-right { content: string(chapter); font: 8.5pt system-ui, sans-serif; color: #5c5955; }
|
||||
@bottom-center { content: counter(page) " / " counter(pages); font: 8.5pt system-ui, sans-serif; color: #5c5955; }
|
||||
}
|
||||
@page :first { @top-right { content: none; } @bottom-center { content: none; } }
|
||||
@media print {
|
||||
:root { --bg: #fff; --ink: #1d1c1a; --ink-dim: #5c5955; --rule: #dedad4; --accent: #8a4b12; --panel: #f1eee9; --mark: transparent; }
|
||||
body { font-size: 10.5pt; line-height: 1.5; }
|
||||
.page { display: block; padding: 0; }
|
||||
.downloads { display: none; }
|
||||
.cover { display: block; break-after: page; padding-top: 80mm; }
|
||||
.cover-title { font-size: 34pt; font-weight: 700; margin: 0 0 6mm; }
|
||||
.cover-sub { color: var(--ink-dim); margin: 0; }
|
||||
.toc { position: static; max-height: none; overflow: visible; break-after: page; font-size: 10pt; }
|
||||
.toc-title { font-size: 9pt; }
|
||||
.toc a { color: var(--ink); }
|
||||
.toc a::after { content: leader('.') target-counter(attr(href), page); color: var(--ink-dim); }
|
||||
.toc ul ul a { color: var(--ink-dim); }
|
||||
main { display: contents; }
|
||||
h1 { font-size: 24pt; }
|
||||
h2 { break-before: page; border-top: none; padding-top: 0; margin-top: 0; string-set: chapter content(); }
|
||||
h2, h3 { break-after: avoid; }
|
||||
figure, tr, img { break-inside: avoid; }
|
||||
figure img, main img { max-height: 100mm; width: auto; max-width: 100%; margin: 0 auto; }
|
||||
figure { margin: 4mm 0; }
|
||||
td img { max-height: 60mm; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
p, li { orphans: 3; widows: 3; }
|
||||
}
|
||||
"#;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -486,6 +541,19 @@ mod tests {
|
||||
assert_eq!(rewrite_link("https://x.org/"), "https://x.org/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_page_offers_its_downloads_from_the_published_site() {
|
||||
let html = render_html("# Title\n");
|
||||
assert!(
|
||||
html.contains(&format!("href=\"{PAGES_SITE}{PDF_NAME}\"")),
|
||||
"{html}"
|
||||
);
|
||||
assert!(
|
||||
html.contains(&format!("href=\"{PAGES_SITE}{EPUB_NAME}\"")),
|
||||
"{html}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The real manual: one page, every section in the contents, nothing lost.
|
||||
#[test]
|
||||
fn the_real_manual_renders_with_its_contents() {
|
||||
|
||||
Reference in New Issue
Block a user