Skip to content

Instantly share code, notes, and snippets.

@FireController1847
Last active March 1, 2026 05:45
Show Gist options
  • Select an option

  • Save FireController1847/0b442ec9b19b55b9e0332b9f9b2a7b9f to your computer and use it in GitHub Desktop.

Select an option

Save FireController1847/0b442ec9b19b55b9e0332b9f9b2a7b9f to your computer and use it in GitHub Desktop.
GitHub “Print-friendly Markdown” helper
/**
* =============================================================================
* GitHub Markdown → Print Preparation Utility
* =============================================================================
*
* What This Is
* ------------
* A console-executable DOM transformation script that:
*
* • Isolates the rendered Markdown section of a GitHub file page
* • Removes surrounding GitHub UI chrome
* • Optimizes layout for printing or PDF export
* • Prevents awkward page splits (headings, tables, code blocks)
* • Auto-sizes table columns while preserving full-width layout
* • Applies anti-orphan rules to headings
*
* The result: a clean, professional, pagination-safe print output.
*
* -----------------------------------------------------------------------------
* How To Use
* -----------------------------------------------------------------------------
*
* 1. Open any GitHub file page that displays rendered Markdown.
* 2. Open your browser DevTools console.
* - Chrome / Edge: F12 → Console
* - Firefox: F12 → Console
* 3. Paste this entire script into the console.
* 4. Press Enter.
* 5. Open the browser Print dialog (Ctrl/Cmd + P).
* - Enable "Print backgrounds" for best visual fidelity.
* 6. Print or Save as PDF.
*
* -----------------------------------------------------------------------------
* What It Modifies
* -----------------------------------------------------------------------------
*
* • Clears the existing page body and replaces it with cloned markdown.
* • Applies inline print styles for pagination control.
* • Does NOT modify GitHub permanently (page refresh restores everything).
*
* -----------------------------------------------------------------------------
* Safety Notes
* -----------------------------------------------------------------------------
*
* • This script runs entirely client-side.
* • No network requests are made.
* • No persistent changes occur.
* • Refreshing the page restores the original DOM.
*
* -----------------------------------------------------------------------------
* Intended Use
* -----------------------------------------------------------------------------
*
* Designed for:
* - Printing technical documentation
* - Exporting clean PDFs of READMEs
* - Generating printable internal documentation
* - Archival documentation snapshots
*
* -----------------------------------------------------------------------------
* Development Notes (Vibe-Coded Edition)
* -----------------------------------------------------------------------------
*
* This script was vibe-coded and built collaboratively with ChatGPT.
*
* It prioritizes practical effectiveness over architectural purity.
* It works well. It solves the problem. It ships.
*
* It is not intended to represent production-level engineering rigor,
* enterprise design patterns, or the author's formal coding standards.
*
* Think of it as:
* High-leverage utility > pristine abstraction.
*
* If it prints beautifully, it has fulfilled its purpose.
*
* =============================================================================
*/
(() => {
const LOG_NS = "[gh-print-prep]";
const now = () => (performance?.now?.() ?? Date.now()).toFixed(1);
const log = (...a) => console.log(`${LOG_NS} ${now()}ms`, ...a);
const info = (...a) => console.info(`${LOG_NS} ${now()}ms`, ...a);
const warn = (...a) => console.warn(`${LOG_NS} ${now()}ms`, ...a);
const dbg = (...a) => console.debug(`${LOG_NS} ${now()}ms`, ...a);
const err = (...a) => console.error(`${LOG_NS} ${now()}ms`, ...a);
const group = (label, fn) => {
console.groupCollapsed(`${LOG_NS} ${now()}ms ${label}`);
try {
return fn();
} finally {
console.groupEnd();
}
};
// Tip: In Chrome/Edge you can toggle verbose debug output in the console filters.
info("Booting… preparing to isolate GitHub-rendered markdown and apply print rules.");
// ===========================================================================
// helpers
// ===========================================================================
const qs = (s, r = document) => r.querySelector(s);
const qsa = (s, r = document) => Array.from(r.querySelectorAll(s));
const isEl = (n) => n && n.nodeType === 1;
const setStyle = (el, styleObj, label = "style") => {
if (!el) return;
Object.assign(el.style, styleObj);
dbg(`Applied ${label}:`, styleObj, "→", el);
};
const avoidInside = (el) => {
if (!el) return;
el.style.pageBreakInside = "avoid";
el.style.breakInside = "avoid";
dbg("Applied break-inside: avoid →", el);
};
const avoidAfter = (el) => {
if (!el) return;
el.style.pageBreakAfter = "avoid";
el.style.breakAfter = "avoid";
dbg("Applied break-after: avoid →", el);
};
const avoidBefore = (el) => {
if (!el) return;
el.style.pageBreakBefore = "avoid";
el.style.breakBefore = "avoid";
dbg("Applied break-before: avoid →", el);
};
const nextMeaningfulEl = (el) => {
let n = el && el.nextSibling;
while (n) {
if (isEl(n)) return n;
n = n.nextSibling;
}
return null;
};
const prevMeaningfulEl = (el) => {
let n = el && el.previousSibling;
while (n) {
if (isEl(n)) return n;
n = n.previousSibling;
}
return null;
};
const containsTable = (el) =>
!!(el && (el.tagName?.toLowerCase() === "table" || el.querySelector?.("table")));
// ===========================================================================
// 1) isolate GitHub rendered markdown section
// ===========================================================================
group("Step 1 — Locate rendered markdown container", () => {
dbg("Searching for markdown section candidates…");
const selectors = [
'section[class*="BlobContent-module__blobContentSectionMarkdown__"]',
'section[class*="BlobContent-module__blobContentSection__"]',
'[class*="DirectoryRichtextContent-module__SharedMarkdownContent__"]',
];
selectors.forEach((sel) => dbg("Candidate selector:", sel));
const section =
qs(selectors[0]) ||
qs(selectors[1]) ||
qs(selectors[2]);
if (!section) {
err("Markdown section not found. Selectors tried:", selectors);
return;
}
info("Markdown section found:", section);
group("Isolate DOM — clone markdown and wipe surrounding page chrome", () => {
const clone = section.cloneNode(true);
dbg("Cloned markdown section:", clone);
// Capture metadata BEFORE we wipe GitHub chrome
const captureMeta = () => {
// Commit hash (prefer visible 7-char link; fallback to href)
let hash7 = null;
const commitLink = qs('a[href*="/commit/"]');
if (commitLink) {
const txt = (commitLink.textContent || "").trim();
if (/^[0-9a-f]{7}$/i.test(txt)) hash7 = txt;
if (!hash7) {
const href = commitLink.getAttribute("href") || "";
const m = href.match(/\/commit\/([0-9a-f]{7,40})/i);
if (m) hash7 = m[1].slice(0, 7);
}
}
// Commit datetime (GitHub uses <relative-time datetime="...">)
let iso = null;
let title = null;
let display = null;
const rt = qs("relative-time[datetime]");
if (rt) {
iso = rt.getAttribute("datetime");
title = rt.getAttribute("title") || null;
display = (rt.textContent || "").trim() || null;
}
// Fallback: some pages use <time datetime="...">
if (!iso) {
const t = qs('time[datetime]');
if (t) {
iso = t.getAttribute("datetime");
title = t.getAttribute("title") || null;
display = (t.textContent || "").trim() || null;
}
}
// Filename from URL (best), fallback to document.title
const seg = (location.pathname || "").split("/").filter(Boolean);
const mode = seg[2] || "";
const isRepoRoot = seg.length === 2; // /owner/repo
const isFileRoute = mode === "blob" || mode === "tree" || mode === "blame" || mode === "raw";
const filePath = isFileRoute ? seg.slice(4).join("/") : seg.slice(2).join("/");
const fileParts = filePath ? filePath.split("/").filter(Boolean) : [];
const filename =
isRepoRoot ? "README.md"
: (fileParts.length ? fileParts[fileParts.length - 1] : null) || document.title || null;
if (isRepoRoot) {
// breadcrumb should be repo › README.md
// fileParts should include README.md so Step 6 renders it
return { hash7, iso, title, display, filePath: "", fileParts: ["README.md"], filename };
}
return { hash7, iso, title, display, filePath, fileParts, filename };
};
const capturedMeta = captureMeta();
dbg("Captured meta (pre-wipe):", capturedMeta);
dbg("Clearing document.body.innerHTML (this will remove GitHub chrome)…");
document.body.innerHTML = "";
setStyle(document.documentElement, { margin: "0", padding: "0" }, "documentElement base");
setStyle(document.body, { margin: "0", padding: "32px", maxWidth: "none" }, "body base");
setStyle(clone, { maxWidth: "none", width: "auto", margin: "0 auto" }, "clone container");
document.body.appendChild(clone);
info("Clone appended. Page is now markdown-only for printing.");
group("Print color preservation", () => {
// Preserve colors as-authored (best with "Print backgrounds" enabled)
const setPrintColor = (el) => {
el.style.setProperty("-webkit-print-color-adjust", "exact");
el.style.setProperty("print-color-adjust", "exact");
dbg("Set print-color-adjust to exact for:", el);
};
setPrintColor(document.documentElement);
setPrintColor(document.body);
info("Print color adjustment configured (exact).");
});
// Make section available to later steps by stashing on window (no global vars elsewhere).
window.__GH_PRINT_PREP__ = { cloneRoot: clone, meta: capturedMeta };
dbg("Stashed cloneRoot on window.__GH_PRINT_PREP__ for later steps.");
});
});
// If isolation failed, bail (window stash won't exist).
if (!window.__GH_PRINT_PREP__?.cloneRoot) {
warn("Aborting: markdown isolation did not complete successfully.");
return;
}
const root = window.__GH_PRINT_PREP__.cloneRoot;
// ===========================================================================
// 2) group GitHub heading blocks with the following table wrapper
// ===========================================================================
group("Step 2 — Keep headings attached to their tables", () => {
let grouped = 0;
dbg("Collecting heading blocks (.markdown-heading preferred; fallback to h1..h6)...");
const headingBlocks = qsa(".markdown-heading", root);
if (headingBlocks.length === 0) {
dbg("No .markdown-heading found; falling back to raw heading tags.");
qsa("h1,h2,h3,h4,h5,h6", root).forEach((h) => headingBlocks.push(h));
}
info(`Heading blocks detected: ${headingBlocks.length}`);
for (const hb of headingBlocks) {
const next = nextMeaningfulEl(hb);
if (!next) {
dbg("Heading has no meaningful next sibling; skipping:", hb);
continue;
}
const isTableWrapper =
next.classList?.contains("markdown-accessibility-table") || containsTable(next);
if (!isTableWrapper) {
dbg("Next block is not table-ish; skipping wrap for:", hb, "→ next:", next);
continue;
}
dbg("Wrapping heading with its table-ish next block:", { heading: hb, next });
const wrapper = document.createElement("div");
wrapper.className = "keep-heading-with-table";
avoidInside(wrapper);
hb.parentNode.insertBefore(wrapper, hb);
wrapper.appendChild(hb);
wrapper.appendChild(next);
grouped++;
dbg("Wrapped heading+table pair. Total so far:", grouped);
}
window.__GH_PRINT_PREP__.groupedHeadingTablePairs = grouped;
info(`Completed heading+table grouping. Total wrapped pairs: ${grouped}`);
});
// ===========================================================================
// 2b) keep heading block with the immediate following content block (usually <p>)
// ===========================================================================
group("Step 2b — Keep headings attached to their immediate intro block", () => {
let groupedIntro = 0;
const introJoinableTags = new Set(["p", "ul", "ol", "pre", "blockquote", "div"]);
const headings = qsa(".markdown-heading", root);
info(`Scanning ${headings.length} markdown-heading blocks for intro pairing…`);
headings.forEach((hb, i) => {
if (hb.closest?.(".keep-heading-with-table")) {
dbg(`(${i}) Already in keep-heading-with-table; skipping.`, hb);
return;
}
if (hb.closest?.(".keep-heading-with-next")) {
dbg(`(${i}) Already in keep-heading-with-next; skipping.`, hb);
return;
}
const next = nextMeaningfulEl(hb);
if (!next) {
dbg(`(${i}) No next meaningful element; skipping.`, hb);
return;
}
const nextIsTableish =
next.classList?.contains("markdown-accessibility-table") ||
next.tagName?.toLowerCase() === "table" ||
next.querySelector?.("table");
if (nextIsTableish) {
dbg(`(${i}) Next is table-ish; Step 2 handles this; skipping.`, { hb, next });
return;
}
// Leave "heading -> heading" to be handled by 2c (recursive prepend)
if (next.classList?.contains("markdown-heading")) {
dbg(`(${i}) Heading followed by heading; leaving for Step 2c.`, { hb, next });
return;
}
const tag = next.tagName?.toLowerCase();
if (!tag || !introJoinableTags.has(tag)) {
dbg(`(${i}) Next tag not joinable (${tag}); skipping.`, { hb, next });
return;
}
if (tag === "div") {
const looksLikeContainer =
next.querySelector?.("p,ul,ol,pre,table,blockquote,div") &&
next.querySelectorAll?.("*").length > 20;
if (looksLikeContainer) {
dbg(`(${i}) Next div looks like a large container; skipping to avoid page jumps.`, next);
return;
}
}
dbg(`(${i}) Wrapping heading with ONE immediate intro block.`, { hb, next, tag });
const wrapper = document.createElement("div");
wrapper.className = "keep-heading-with-next";
avoidInside(wrapper);
hb.parentNode.insertBefore(wrapper, hb);
wrapper.appendChild(hb);
wrapper.appendChild(next);
groupedIntro++;
dbg(`(${i}) Wrapped heading+next. Total intro wraps so far:`, groupedIntro);
});
window.__GH_PRINT_PREP__.groupedHeadingNextPairs = groupedIntro;
info(`Completed heading+next grouping. Total wrapped intro pairs: ${groupedIntro}`);
});
// ===========================================================================
// 2c) prepend ALL consecutive preceding headings into each keep-wrapper
// ===========================================================================
group("Step 2c — Merge consecutive heading chains into the nearest keep-wrapper", () => {
let groupedRecursiveHeadings = 0;
const keepWrappers = qsa(".keep-heading-with-next, .keep-heading-with-table", root);
info(`Found keep-wrappers: ${keepWrappers.length}`);
keepWrappers.forEach((wrap, idx) => {
dbg(`Wrapper[${idx}] scanning previous siblings for heading chains…`, wrap);
const collected = [];
let prev = prevMeaningfulEl(wrap);
while (prev && prev.classList?.contains("markdown-heading")) {
// Don't steal headings that are already inside some keep wrapper
if (prev.closest?.(".keep-heading-with-next, .keep-heading-with-table")) {
dbg("Encountered a heading already owned by another wrapper; stopping chain capture.", prev);
break;
}
collected.push(prev);
dbg("Captured preceding heading:", prev);
prev = prevMeaningfulEl(prev);
}
if (collected.length === 0) {
dbg(`Wrapper[${idx}] no preceding heading chain found.`);
return;
}
dbg(`Wrapper[${idx}] captured ${collected.length} headings. Reinserting in document order…`);
const frag = document.createDocumentFragment();
for (const h of collected.reverse()) frag.appendChild(h);
wrap.insertBefore(frag, wrap.firstChild);
groupedRecursiveHeadings += collected.length;
// Ensure wrapper still has anti-split (in case anything modified it)
avoidInside(wrap);
info(`Wrapper[${idx}] prepended ${collected.length} headings. Running total: ${groupedRecursiveHeadings}`);
});
window.__GH_PRINT_PREP__.prependedHeadingCount = groupedRecursiveHeadings;
info(`Completed heading-chain prepends. Total headings prepended: ${groupedRecursiveHeadings}`);
});
// ===========================================================================
// 3) tables: full width + split-safe tweaks (AUTO-SIZE COLUMNS)
// ===========================================================================
group("Step 3 — Table print optimization (auto-sized columns + split-safe rows)", () => {
const tables = qsa("table", root);
info(`Tables detected: ${tables.length}`);
tables.forEach((t, idx) => {
dbg(`Table[${idx}] applying print rules…`, t);
// Preserve outer edge borders during print fragmentation (no explicit border values)
t.style.borderCollapse = "separate";
t.style.borderSpacing = "0";
// Fill width but allow columns to size to content
t.style.display = "table";
t.style.width = "100%";
t.style.maxWidth = "100%";
// KEY CHANGE: let the browser auto-size columns (no fixed layout)
t.style.tableLayout = "auto";
// Wrapping: prefer normal word breaking; only break long tokens if needed
t.style.wordBreak = "normal";
const rows = qsa("tr", t);
dbg(`Table[${idx}] rows: ${rows.length} → applying break-inside:avoid`);
rows.forEach(avoidInside);
const cells = qsa("td,th", t);
dbg(`Table[${idx}] cells: ${cells.length} → applying cell break rules + wrapping safety net`);
cells.forEach((c) => {
avoidInside(c);
c.style.overflowWrap = "break-word";
c.style.wordBreak = "normal";
});
const thead = qs("thead", t);
if (thead) {
thead.style.display = "table-header-group";
dbg(`Table[${idx}] thead detected; forcing repeat header group in print.`, thead);
}
const tfoot = qs("tfoot", t);
if (tfoot) {
tfoot.style.display = "table-footer-group";
dbg(`Table[${idx}] tfoot detected; forcing repeat footer group in print.`, tfoot);
}
});
info("Table optimization complete.");
});
// ===========================================================================
// 4) code blocks: avoid splitting
// ===========================================================================
group("Step 4 — Code blocks: keep intact across pages", () => {
const codeBlocks = qsa("pre, pre > code, .highlight, .sourceCode", root);
info(`Code-ish blocks detected: ${codeBlocks.length}`);
codeBlocks.forEach((el, idx) => {
dbg(`CodeBlock[${idx}] applying avoidInside…`, el);
avoidInside(el);
if (el.tagName?.toLowerCase() === "pre") {
el.style.overflow = "visible";
dbg(`CodeBlock[${idx}] <pre> overflow set to visible for print clarity.`);
}
});
info("Code block print rules complete.");
});
// ===========================================================================
// 5) headings: avoid orphans, but don't double-constrain heading+table pairs
// ===========================================================================
group("Step 5 — Heading anti-orphan rules", () => {
const headings = qsa("h1,h2,h3,h4,h5,h6", root);
info(`Headings detected: ${headings.length}`);
headings.forEach((h, idx) => {
dbg(`Heading[${idx}] applying avoidAfter…`, h);
avoidAfter(h);
const next = h.nextElementSibling;
const wrappedWithTable = !!h.closest?.(".keep-heading-with-table");
const nextIsTableish =
next &&
(next.classList?.contains("markdown-accessibility-table") ||
next.tagName?.toLowerCase() === "table" ||
next.querySelector?.("table"));
if (!wrappedWithTable && !nextIsTableish && next) {
dbg(`Heading[${idx}] applying avoidBefore to immediate next content block…`, next);
avoidBefore(next);
} else {
dbg(
`Heading[${idx}] skip avoidBefore (wrappedWithTable=${wrappedWithTable}, nextIsTableish=${!!nextIsTableish}).`
);
}
});
info("Heading anti-orphan rules complete.");
});
// ===========================================================================
// 6) First-page fancy header (in-flow): filename + commit subtitle + breadcrumb + GitHub badge
// - Appears only on page 1 because it's NOT fixed (normal flow)
// - Theme-aware: white-ish in dark mode, black in print
// - Tightens ONLY the top margin of the first H1
// ===========================================================================
group("Step 6 — First-page header (filename + commit + breadcrumb + repo)", () => {
const meta = window.__GH_PRINT_PREP__?.meta || {};
dbg("Step 6 using meta:", meta);
// Avoid double-install
if (window.__GH_PRINT_PREP__?.firstPageHeaderInstalled) {
warn("Step 6 skipped: header already installed.");
return;
}
// Parse owner/repo for right-side label
const seg = (location.pathname || "").split("/").filter(Boolean);
const owner = seg[0] || "";
const repo = seg[1] || "";
const repoFull = (owner && repo) ? `${owner}/${repo}` : "";
const filename = meta.filename || (meta.fileParts?.length ? meta.fileParts.at(-1) : "") || document.title || "(file)";
const suggestedPdfName = (() => {
const cleanRepo = repo || "repo";
const cleanFile = filename || "document";
return `${cleanFile}${meta.hash7 ? ` (${meta.hash7})` : ""}`;
})();
// Suggest filename via document.title
document.title = suggestedPdfName;
const formatCommitDate = () => {
// Prefer title (includes timezone label), else iso, else display
if (meta.title) return meta.title;
if (meta.iso) {
try {
const d = new Date(meta.iso);
return d.toLocaleString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
timeZoneName: "short",
});
} catch { }
}
return meta.display || null;
};
const subtitleText = (() => {
const hash = meta.hash7 ? String(meta.hash7) : null;
const dateStr = formatCommitDate();
if (!hash && !dateStr) return null;
if (hash && dateStr) return `${hash} · ${dateStr}`;
return hash || dateStr;
})();
// -------------------------
// Header DOM (underline style, in-flow)
// -------------------------
const header = document.createElement("div");
header.className = "gh-first-page-header";
avoidInside(header);
// -------------------------
// First-page-only top pull-up (does NOT affect later pages)
// -------------------------
const firstPageWrap = document.createElement("div");
firstPageWrap.className = "gh-first-page-wrap";
// Pull just the FIRST PAGE upward by N pixels.
// Tune this number: 12..28 is usually the sweet spot.
const PULL_UP_PX = 26;
setStyle(firstPageWrap, {
marginTop: `-${PULL_UP_PX}px`,
}, "first-page pull-up wrapper");
// Underline only (GitHub-ish)
setStyle(header, {
display: "block",
width: "100%",
boxSizing: "border-box",
padding: "0 0 10px 0",
margin: "0 0 12px 0",
borderBottom: "1px solid rgba(0,0,0,0.25)",
}, "first-page header base");
// Big centered filename
const filenameLine = document.createElement("div");
filenameLine.className = "gh-first-page-filename";
filenameLine.textContent = filename;
// (Your updated style)
setStyle(filenameLine, {
width: "100%",
textAlign: "center",
fontFamily: "ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial",
fontSize: "2em",
fontWeight: "700",
letterSpacing: "0.2px",
margin: "0 0 2px 0",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}, "filename line");
// Subtitle: [HASH7] · [DATETIME]
const subtitleLine = document.createElement("div");
subtitleLine.className = "gh-first-page-subtitle";
subtitleLine.textContent = subtitleText || "";
setStyle(subtitleLine, {
width: "100%",
textAlign: "center",
fontFamily: "ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial",
fontSize: "14px", // fixed px tends to print sharper than em here
fontWeight: "500",
letterSpacing: "0px", // avoid fractional spacing blur
margin: "0 0 2em 0",
display: subtitleText ? "block" : "none", // hide without opacity
overflow: "hidden",
}, "subtitle line");
// Row: breadcrumb left, GitHub badge right
const row = document.createElement("div");
setStyle(row, {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "16px",
width: "100%",
minWidth: "0",
}, "header row");
// Left: full breadcrumb
const breadcrumb = document.createElement("div");
breadcrumb.className = "gh-breadcrumb";
setStyle(breadcrumb, {
display: "flex",
flexWrap: "wrap",
alignItems: "baseline",
gap: "6px",
fontFamily: "ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial",
fontSize: "12px",
lineHeight: "1.2",
minWidth: "0",
}, "breadcrumb");
const makeSpan = (text, extraStyle = {}) => {
const s = document.createElement("span");
s.textContent = text;
setStyle(s, Object.assign({ whiteSpace: "nowrap" }, extraStyle));
return s;
};
const repoName = repo || meta.fileParts?.[0] || "(repo)";
const repoSpan = makeSpan(repoName, { fontWeight: "600" });
breadcrumb.appendChild(repoSpan);
const parts = meta.fileParts?.length ? meta.fileParts : (meta.filePath ? meta.filePath.split("/").filter(Boolean) : ["(path)"]);
parts.forEach((p) => {
const sep = makeSpan("›", {
margin: "0 2px",
userSelect: "none",
});
sep.className = "gh-breadcrumb-sep";
breadcrumb.appendChild(sep);
breadcrumb.appendChild(makeSpan(p));
});
// Right: GitHub logo + owner/repo
const right = document.createElement("div");
setStyle(right, {
display: "flex",
alignItems: "center",
gap: "8px",
flex: "0 0 auto",
fontFamily: "ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial",
fontSize: "12px",
maxWidth: "45%",
whiteSpace: "normal",
justifyContent: "flex-end",
textAlign: "right",
minWidth: "0",
}, "github badge");
const ghSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
ghSvg.setAttribute("viewBox", "0 0 16 16");
ghSvg.setAttribute("aria-hidden", "true");
ghSvg.setAttribute("focusable", "false");
ghSvg.innerHTML =
`<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z"></path>`;
ghSvg.setAttribute("fill", "currentColor");
setStyle(ghSvg, { width: "14px", height: "14px", flex: "0 0 auto" }, "gh svg");
const ghText = document.createElement("span");
ghText.textContent = (repoFull || "").replaceAll("/", "/\u200B");
setStyle(ghText, { fontWeight: "600" }, "gh text");
if (repoFull) {
right.appendChild(ghSvg);
right.appendChild(ghText);
}
row.appendChild(breadcrumb);
row.appendChild(right);
header.appendChild(filenameLine);
header.appendChild(subtitleLine);
header.appendChild(row);
// Move root into wrapper so both header + the start of content shift up together.
document.body.insertBefore(firstPageWrap, root);
firstPageWrap.appendChild(header);
firstPageWrap.appendChild(root);
info("Inserted first-page header above markdown clone:", header);
// -------------------------
// Only trim TOP margin on first H1 (do not touch bottom)
// -------------------------
const firstH1 = qs("h1", root);
if (firstH1) {
setStyle(firstH1, {
marginTop: "8px", // top only
}, "first H1 top trim");
dbg("Trimmed first H1 top margin:", firstH1);
}
// -------------------------
// Theme-aware colors: dark mode = white-ish; print = black
// -------------------------
const mqlDark = window.matchMedia?.("(prefers-color-scheme: dark)");
const applyTheme = (mode /* 'screen' | 'print' */) => {
const dark = (mode !== "print") && !!mqlDark?.matches;
const fg = dark ? "rgba(255,255,255,0.90)" : "rgba(0,0,0,0.80)";
const fgStrong = dark ? "rgba(255,255,255,0.95)" : "rgba(0,0,0,0.88)";
const rule = dark ? "rgba(255,255,255,0.22)" : "rgba(0,0,0,0.25)";
const sepColor = dark ? "rgba(255,255,255,0.45)" : "rgba(0,0,0,0.40)";
qsa(".gh-breadcrumb-sep", header).forEach((s) => {
s.style.color = sepColor;
});
header.style.borderBottom = `1px solid ${rule}`;
filenameLine.style.color = fgStrong;
subtitleLine.style.color = dark ? "rgba(255,255,255,0.70)" : "rgba(0,0,0,0.60)";
breadcrumb.style.color = fg;
repoSpan.style.color = fgStrong;
right.style.color = fg;
};
applyTheme("screen");
// Reduce top "print margin feel" we control (browser margins still exist)
const originalPadTop = document.body.style.paddingTop;
const originalMarginTop = document.body.style.marginTop;
const beforePrint = () => {
applyTheme("print");
// shrink only the TOP padding so the first page starts closer to top
document.body.style.paddingTop = "16px";
document.body.style.marginTop = "0px";
void header.offsetHeight;
};
const afterPrint = () => {
document.body.style.paddingTop = originalPadTop;
document.body.style.marginTop = originalMarginTop;
applyTheme("screen");
};
window.addEventListener("beforeprint", beforePrint);
window.addEventListener("afterprint", afterPrint);
const mqlPrint = window.matchMedia?.("print");
if (mqlPrint && typeof mqlPrint.addEventListener === "function") {
mqlPrint.addEventListener("change", (e) => (e.matches ? beforePrint() : afterPrint()));
} else if (mqlPrint && typeof mqlPrint.addListener === "function") {
mqlPrint.addListener((e) => (e.matches ? beforePrint() : afterPrint()));
}
if (mqlDark && typeof mqlDark.addEventListener === "function") {
mqlDark.addEventListener("change", () => applyTheme("screen"));
} else if (mqlDark && typeof mqlDark.addListener === "function") {
mqlDark.addListener(() => applyTheme("screen"));
}
window.__GH_PRINT_PREP__.firstPageHeaderInstalled = true;
info("Step 6 complete.");
});
// ===========================================================================
// FINAL SUMMARY (one clean line, plus a nice expanded object)
// ===========================================================================
const summary = {
groupedHeadingTablePairs: window.__GH_PRINT_PREP__.groupedHeadingTablePairs ?? 0,
groupedHeadingNextPairs: window.__GH_PRINT_PREP__.groupedHeadingNextPairs ?? 0,
prependedHeadingCount: window.__GH_PRINT_PREP__.prependedHeadingCount ?? 0,
tables: qsa("table", root).length,
codeBlocks: qsa("pre, pre > code, .highlight, .sourceCode", root).length,
headings: qsa("h1,h2,h3,h4,h5,h6", root).length,
};
log(
`Done: isolated markdown; grouped ${summary.groupedHeadingTablePairs} heading+table pairs; ` +
`grouped ${summary.groupedHeadingNextPairs} heading+next blocks; ` +
`prepended ${summary.prependedHeadingCount} preceding headings into keep-wrappers; ` +
`tables full-width with auto-sized columns; split-safe rules applied.`
);
dbg("Run summary:", summary);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment