Skip to content

Commit 68b7330

Browse files
committed
Merge branch 'printable-cheat-sheet'
This modifies the CSS a bit to make the print-out of the Git Cheat Sheet a bit more pleasant, and then adds a script that uses Playwright to render the HTML page to a PDF file. Signed-off-by: Johannes Schindelin <[email protected]>
2 parents ca9815d + 21ad6a0 commit 68b7330

File tree

8 files changed

+179
-5
lines changed

8 files changed

+179
-5
lines changed

.github/actions/deploy-to-github-pages/action.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ runs:
8888
npm install node-html-parser &&
8989
node ./script/graphviz-ssr.js
9090
91+
- name: offer PDF version of the cheat sheet
92+
shell: bash
93+
run: |
94+
npm install @playwright/test &&
95+
node script/html-to-pdf.js -i public/cheat-sheet.html
96+
9197
- name: run Pagefind ${{ env.PAGEFIND_VERSION }} to build the search index
9298
shell: bash
9399
run: npx -y pagefind@${{ env.PAGEFIND_VERSION }} --site public --write-playground
@@ -297,4 +303,4 @@ runs:
297303
if: always() && steps.playwright.outputs.result != ''
298304
with:
299305
name: playwright-report
300-
path: playwright-report/
306+
path: playwright-report/

.github/workflows/ci.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ jobs:
3838
npm install node-html-parser &&
3939
node ./script/graphviz-ssr.js
4040
41+
- name: Install @playwright/test
42+
run: npm install @playwright/test
43+
- name: offer PDF version of the cheat sheet
44+
run: node script/html-to-pdf.js -i public/cheat-sheet.html
45+
4146
- name: run Pagefind ${{ env.PAGEFIND_VERSION }} to build the search index
4247
run: npx -y pagefind@${{ env.PAGEFIND_VERSION }} --site public --write-playground
4348

@@ -83,8 +88,6 @@ jobs:
8388
output: lychee.md
8489
jobSummary: true
8590

86-
- name: Install @playwright/test
87-
run: npm install @playwright/test
8891
- name: Run Playwright tests
8992
id: playwright
9093
env:
@@ -96,4 +99,4 @@ jobs:
9699
if: always() && steps.playwright.outputs.result != ''
97100
with:
98101
name: playwright-report
99-
path: playwright-report/
102+
path: playwright-report/

assets/js/application.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ $(document).ready(function() {
3737
Downloads.init();
3838
DownloadBox.init();
3939
PostelizeAnchor.init();
40+
Print.init();
4041
});
4142

4243
function onPopState(fn) {
@@ -650,7 +651,7 @@ var DarkMode = {
650651
|| (!prefersDarkScheme && currentTheme === "dark")) {
651652
button.attr("src", `${baseURLPrefix}images/light-mode.svg`);
652653
}
653-
button.css("display", "block");
654+
button.addClass('active');
654655

655656
button.on('click', function(e) {
656657
e.preventDefault();
@@ -782,6 +783,27 @@ var PostelizeAnchor = {
782783
},
783784
}
784785

786+
var Print = {
787+
init: function() {
788+
Print.tagline = $("#tagline");
789+
Print.scrollToTop = $("#scrollToTop");
790+
window.matchMedia("print").addListener((mediaQueryList) => {
791+
Print.toggle(mediaQueryList.matches);
792+
});
793+
},
794+
toggle: function(enable) {
795+
if (enable) {
796+
Print.taglineBackup = Print.tagline.html();
797+
Print.tagline.html("--print-out");
798+
Print.scrollToTopDisplay = Print.scrollToTop.attr("display");
799+
Print.scrollToTop.attr("display", "none");
800+
} else {
801+
Print.tagline.html(Print.taglineBackup || "--as-git-as-it-gets");
802+
Print.scrollToTop.attr("display", Print.scrollToTopDisplay);
803+
}
804+
}
805+
}
806+
785807
// Scroll to Top
786808
$('#scrollToTop').removeClass('no-js');
787809
$(window).on('scroll', function() {

assets/sass/application.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ $baseurl: "{{ .Site.BaseURL }}{{ if (and (ne .Site.BaseURL "/") (ne .Site.BaseUR
3030
@import 'cheat-sheet';
3131
@import 'dark-mode';
3232
@import 'git-turns-20';
33+
@import 'print';
3334

3435
code {
3536
display: inline;
@@ -63,3 +64,7 @@ pre {
6364
align-self: center;
6465
margin: 5px;
6566
}
67+
68+
#dark-mode-button.active {
69+
display: block;
70+
}
File renamed without changes.
File renamed without changes.

assets/sass/print.scss

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
@media print {
2+
.inner {
3+
// The `position` of the `inner` class is defined as `relative`, which
4+
// causes funny issues when printing, for example tens of empty pages in
5+
// the middle. Let's suppress that.
6+
position: inherit;
7+
margin-bottom: 0;
8+
}
9+
10+
#main {
11+
margin-bottom: 0;
12+
}
13+
14+
footer {
15+
margin-top: 0;
16+
}
17+
18+
aside, .sidebar-btn, #search-container, #reference-version, #dark-mode-button, .site-source {
19+
display: none;
20+
}
21+
22+
section {
23+
break-inside: avoid-page;
24+
}
25+
26+
div#main {
27+
box-decoration-break: clone;
28+
}
29+
30+
#dark-mode-button {
31+
display: none !important;
32+
}
33+
}
34+
35+
div#main .pdf-link img {
36+
float: right;
37+
height: 36px;
38+
}

script/html-to-pdf.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs')
4+
const path = require('path')
5+
const url = require('url')
6+
const { chromium } = require('playwright')
7+
8+
const insertPDFLink = (htmlPath) => {
9+
const html = fs.readFileSync(htmlPath, 'utf-8')
10+
if (html.includes('class="pdf-link"')) {
11+
return
12+
}
13+
// get baseURL prefix via the `favicon.ico` link, it's in the top-level directory
14+
const match = html.match(/<link href="(.*?)favicon\.ico"/)
15+
if (!match) throw new Error('Failed to determine baseURL prefix from favicon.ico link')
16+
const img = `<img src="${match[1]}/images/pdf.png" />`
17+
const updatedHtml = html.replace(
18+
/<h1/,
19+
`<a class="pdf-link" href="${path.basename(htmlPath, '.html')}.pdf">${img}</a>$&`
20+
)
21+
if (updatedHtml === html) throw new Error('Failed to insert PDF link, no <h1> found')
22+
fs.writeFileSync(htmlPath, updatedHtml, 'utf-8')
23+
}
24+
25+
const htmlToPDF = async (htmlPath, options) => {
26+
if (!htmlPath.endsWith('.html')) {
27+
throw new Error(`Input file must have the '.html' extension: ${htmlPath}`)
28+
}
29+
if (!fs.existsSync(htmlPath)) {
30+
throw new Error(`Input file does not exist: ${htmlPath}`)
31+
}
32+
const outputPath = htmlPath.replace(/\.html$/, '.pdf')
33+
if (!options.force && fs.existsSync(outputPath)) {
34+
throw new Error(`Output file already exists: ${outputPath}`)
35+
}
36+
37+
const browser = await chromium.launch({ channel: 'chrome' })
38+
const page = await browser.newPage()
39+
40+
const htmlPathURL = url.pathToFileURL(htmlPath).toString()
41+
console.log(`Processing ${htmlPathURL}...`)
42+
43+
// Work around HUGO_RELATIVEURLS=false by rewriting the absolute URLs
44+
const baseURLPrefix = htmlPathURL.substring(0, htmlPathURL.lastIndexOf('/public/') + 8)
45+
await page.route(/^file:\/\//, async (route, req) => {
46+
// This _will_ be a correct URL when deployed to https://whatevers/, but
47+
// this script runs before deployment, on a file:/// URL, where we need to
48+
// be a bit clever to give the browser the file it needs.
49+
const original = req.url()
50+
if (original === htmlPathURL) {
51+
// Work around rerouted `.css` and `.js` files... Symptom: "has an
52+
// integrity attribute, but the resource requires the request to be CORS
53+
// enabled to check the integrity, and it is not. The resource has been
54+
// blocked because the integrity cannot be enforced."
55+
const body =
56+
fs.readFileSync(htmlPath, "utf-8")
57+
// strip out the `integrity="sha256-..."` attributes
58+
.replace(/(\/application\.[^"/]+") integrity="sha256-[^"]+"/g, "$1")
59+
await route.fulfill({ headers: { "Content-Type": "text/html" }, body })
60+
return
61+
}
62+
63+
const url =
64+
original.startsWith(baseURLPrefix)
65+
? original
66+
: original.replace(/^file:\/\/\/([A-Za-z]:\/)?(git-scm\.com\/)?/, baseURLPrefix)
67+
console.error(`::notice::Rewrote ${original} to ${url}`)
68+
await route.continue({ url })
69+
})
70+
71+
await page.goto(htmlPathURL, { waitUntil: 'load' })
72+
73+
await page.pdf({
74+
path: outputPath,
75+
format: 'A4',
76+
margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' },
77+
})
78+
await browser.close()
79+
80+
if (options.insertPDFLink) insertPDFLink(htmlPath)
81+
}
82+
83+
const args = process.argv.slice(2)
84+
const options = {}
85+
while (args?.[0].startsWith('-')) {
86+
const arg = args.shift()
87+
if (arg === '--force' || arg === '-f') options.force = true
88+
else if (arg === '--insert-pdf-link' || arg === '-i') options.insertPDFLink = true
89+
else throw new Error(`Unknown argument: ${arg}`)
90+
}
91+
92+
if (args.length !== 1) {
93+
process.stderr.write('Usage: html-to-pdf.js [--force] [--insert-pdf-link] <input-file.html>\n')
94+
process.exit(1)
95+
}
96+
97+
htmlToPDF(args[0], options).catch(e => {
98+
process.stderr.write(`${e.stack}\n`)
99+
process.exit(1)
100+
})

0 commit comments

Comments
 (0)