Skip to content
Open
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
6 changes: 6 additions & 0 deletions .github/actions/deploy-to-github-pages/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ runs:
npm install node-html-parser &&
node ./script/graphviz-ssr.js

- name: offer PDF version of the cheat sheet
shell: bash
run: |
npm install @playwright/test &&
node script/html-to-pdf.js -i public/cheat-sheet.html

- name: run Pagefind ${{ env.PAGEFIND_VERSION }} to build the search index
shell: bash
run: npx -y pagefind@${{ env.PAGEFIND_VERSION }} --site public --write-playground
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ jobs:
npm install node-html-parser &&
node ./script/graphviz-ssr.js

- name: Install @playwright/test
run: npm install @playwright/test
- name: offer PDF version of the cheat sheet
run: node script/html-to-pdf.js -i public/cheat-sheet.html

- name: run Pagefind ${{ env.PAGEFIND_VERSION }} to build the search index
run: npx -y pagefind@${{ env.PAGEFIND_VERSION }} --site public --write-playground

Expand Down Expand Up @@ -83,8 +88,6 @@ jobs:
output: lychee.md
jobSummary: true

- name: Install @playwright/test
run: npm install @playwright/test
- name: Run Playwright tests
id: playwright
env:
Expand Down
24 changes: 23 additions & 1 deletion assets/js/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ $(document).ready(function() {
Downloads.init();
DownloadBox.init();
PostelizeAnchor.init();
Print.init();
});

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

button.on('click', function(e) {
e.preventDefault();
Expand Down Expand Up @@ -808,6 +809,27 @@ var Graphviz = {
}
}

var Print = {
init: function() {
Print.tagline = $("#tagline");
Print.scrollToTop = $("#scrollToTop");
window.matchMedia("print").addListener((mediaQueryList) => {
Print.toggle(mediaQueryList.matches);
});
},
toggle: function(enable) {
if (enable) {
Print.taglineBackup = Print.tagline.html();
Print.tagline.html("--print-out");
Print.scrollToTopDisplay = Print.scrollToTop.attr("display");
Print.scrollToTop.attr("display", "none");
} else {
Print.tagline.html(Print.taglineBackup || "--as-git-as-it-gets");
Print.scrollToTop.attr("display", Print.scrollToTopDisplay);
}
}
}

// Scroll to Top
$('#scrollToTop').removeClass('no-js');
$(window).on('scroll', function() {
Expand Down
5 changes: 5 additions & 0 deletions assets/sass/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ $baseurl: "{{ .Site.BaseURL }}{{ if (and (ne .Site.BaseURL "/") (ne .Site.BaseUR
@import 'cheat-sheet';
@import 'dark-mode';
@import 'git-turns-20';
@import 'print';

code {
display: inline;
Expand Down Expand Up @@ -63,3 +64,7 @@ pre {
align-self: center;
margin: 5px;
}

#dark-mode-button.active {
display: block;
}
8 changes: 4 additions & 4 deletions assets/sass/cheat-sheet.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@
border-radius: 10px;
border: 1px solid var(--callout-color);
display: flex;
font-size: 1rem;
font-size: 1em;
flex-direction: column;
gap: 5px;

p {
font-size: 1rem;
font-size: 1em;
margin: 0 8px;
}

h3 {
color: var(--font-color);
line-height: 1em;
font-size: 1.1rem;
font-size: 1.1em;
margin-bottom: 8px;
font-weight: 500;
}
Expand Down Expand Up @@ -71,7 +71,7 @@
grid-column: 1/3;

margin-top: 0;
font-size: 1.2rem;
font-size: 1.2em;
font-weight: 700;
line-height: 1.2;
color: var(--orange);
Expand Down
File renamed without changes.
File renamed without changes.
145 changes: 145 additions & 0 deletions assets/sass/print.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
@media print {
.inner {
// The `position` of the `inner` class is defined as `relative`, which
// causes funny issues when printing, for example tens of empty pages in
// the middle. Let's suppress that.
position: inherit;

width: inherit;
margin-bottom: 0;
}

#main {
margin-bottom: 0;

/* Improve readability */
line-height: 1.4;
text-align: justify;

/* Ensure it uses full page width */
width: 100%;
border: none;
padding: 0;
}

footer {
padding: 0;
margin-top: 0;
}

aside, .sidebar-btn, #search-container, #reference-version, #dark-mode-button, .site-source {
display: none;
}

section {
break-inside: avoid-page;
}

div#main {
box-decoration-break: clone;
}

.pdf-link, #dark-mode-button {
display: none !important;
}

.cheat-sheet {
font-size: 8pt;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do you think about putting this code in the print stylesheet vs the cheat-sheet.scss style sheet?

I had a similar question about dark mode stuff -- for me if I'm editing the "cheat sheet" page it feels easier if all of the CSS for that page (dark mode, print, etc) are in the same place. But I think there are some advantages to having a "dark mode" or "print" stylesheet that have all of the dark mode print settings that I'm not thinking of.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it makes sense to me to move the cheat-sheet specific stuff into cheat-sheet.scss, and leave the generic print-related stuff in print.scss.


section {
border: 1px solid black;
display: block;
gap: 0;
margin-bottom: 0;

.item {
background: none;
border: none;

h3 {
margin-bottom: 0;
}

code {
background: none;
line-height: inherit;
padding: 1px;
}
}
}
.commit-reference {
background: none;
border-left: 0.5px;

dl {
gap: inherit;

dt,dt::before,dd {
line-height: inherit;
font-size: 0.7em !important;
}
}
}

p {
font-size: inherit;
line-height: inherit;
margin: 1px;
}
}
}

@media print and (min-width: 231mm) {
div#content {
width: 297mm;
}

header {
padding: 0;
#logo img {
max-height: 23px;
width: auto;
height: auto;
}
}

/* landscape orientation */
@page {
size: A4 landscape;
width: 297mm;
height: 210mm;
margin: 1cm;
}

.cheat-sheet {
/* Multi-column layout */
column-count: 6;
column-gap: 1em;

h1 {
column-span: all;
position: absolute;
top: -10px;
width: 100%;
font-size: 16px;
text-align: center;
}

div.item {
display: block;
width: 100%;
padding: 1px;
margin-bottom: 1px;
break-inside: avoid;

label {
margin-top: 2px;
}
}
}
}

div#main .pdf-link img {
float: right;
height: 36px;
}
103 changes: 103 additions & 0 deletions script/html-to-pdf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env node

const fs = require('fs')
const path = require('path')
const url = require('url')
const { chromium } = require('playwright')

const insertPDFLink = (htmlPath) => {
const html = fs.readFileSync(htmlPath, 'utf-8')
if (html.includes('class="pdf-link"')) {
return
}
// get baseURL prefix via the `favicon.ico` link, it's in the top-level directory
const match = html.match(/<link href="(.*?)favicon\.ico"/)
if (!match) throw new Error('Failed to determine baseURL prefix from favicon.ico link')
const img = `<img src="${match[1].replace(/\/$/, '')}/images/pdf.png" />`
const updatedHtml = html.replace(
/<h1/,
`<a class="pdf-link" href="${path.basename(htmlPath, '.html')}.pdf">${img}</a>$&`
)
if (updatedHtml === html) throw new Error('Failed to insert PDF link, no <h1> found')
fs.writeFileSync(htmlPath, updatedHtml, 'utf-8')
}

const htmlToPDF = async (htmlPath, options) => {
if (!htmlPath.endsWith('.html')) {
throw new Error(`Input file must have the '.html' extension: ${htmlPath}`)
}
if (!fs.existsSync(htmlPath)) {
throw new Error(`Input file does not exist: ${htmlPath}`)
}
const outputPath = htmlPath.replace(/\.html$/, '.pdf')
if (!options.force && fs.existsSync(outputPath)) {
throw new Error(`Output file already exists: ${outputPath}`)
}

const browser = await chromium.launch({ channel: 'chrome', ...(options.devtools ? { devtools: true } : {}) })
const page = await browser.newPage()

const htmlPathURL = url.pathToFileURL(htmlPath).toString()
console.log(`Processing ${htmlPathURL}...`)

// Work around HUGO_RELATIVEURLS=false by rewriting the absolute URLs
const baseURLPrefix = htmlPathURL.substring(0, htmlPathURL.lastIndexOf('/public/') + 8)
await page.route(/^file:\/\//, async (route, req) => {
// This _will_ be a correct URL when deployed to https://whatevers/, but
// this script runs before deployment, on a file:/// URL, where we need to
// be a bit clever to give the browser the file it needs.
const original = req.url()
if (original === htmlPathURL) {
// Work around rerouted `.css` and `.js` files... Symptom: "has an
// integrity attribute, but the resource requires the request to be CORS
// enabled to check the integrity, and it is not. The resource has been
// blocked because the integrity cannot be enforced."
const body =
fs.readFileSync(htmlPath, "utf-8")
// strip out the `integrity="sha256-..."` attributes
.replace(/(\/application\.[^"/]+") integrity="sha256-[^"]+"/g, "$1")
await route.fulfill({ headers: { "Content-Type": "text/html" }, body })
return
}

const url =
original.startsWith(baseURLPrefix)
? original
: original.replace(/^file:\/\/\/([A-Za-z]:\/)?(git-scm\.com\/)?/, baseURLPrefix)
if (url !== original) console.error(`::notice::Rewrote ${original} to ${url}`)
await route.continue({ url })
})

await page.goto(htmlPathURL, { waitUntil: 'load' })

await page.pdf({
path: outputPath,
format: 'A4',
landscape: true,
margin: { top: '0cm', bottom: '0cm', left: '0cm', right: '0cm' },
})
if (options.devtools) await new Promise((resolve) => { setTimeout(resolve, 5 * 60 * 1000) })
await browser.close()

if (options.insertPDFLink) insertPDFLink(htmlPath)
}

const args = process.argv.slice(2)
const options = {}
while (args?.[0].startsWith('-')) {
const arg = args.shift()
if (arg === '--force' || arg === '-f') options.force = true
else if (arg === '--insert-pdf-link' || arg === '-i') options.insertPDFLink = true
else if (arg === '--devtools' || arg === '-d') options.devtools = true
else throw new Error(`Unknown argument: ${arg}`)
}

if (args.length !== 1) {
process.stderr.write('Usage: html-to-pdf.js [--force] [--insert-pdf-link] <input-file.html>\n')
process.exit(1)
}

htmlToPDF(args[0], options).catch(e => {
process.stderr.write(`${e.stack}\n`)
process.exit(1)
})
Loading