Fixes to markdown extensions/styling. Sort crawlers most recent first.

This commit is contained in:
2026-08-27 01:27:49 +00:00
parent 5c2433b766
commit 3518ac9ac7
5 changed files with 57 additions and 14 deletions
+1 -1
View File
@@ -100,7 +100,7 @@ falsy values are omitted):
country/city are filled in asynchronously, just like for real visits. In
the analytics viewer, crawler hits are grouped by client hash and shown as
a trail of internal pages that crawler visited; the crawler table lists
the most active crawlers first rather than the most recent hits.
the most recent crawler first, with the most active as a tie-breaker.
- **Abuse (scanner) hits**: a 404 for a telltale path — any URL segment
starting with a dot (`/.env`, `/.git/config`) or ending in `.php`
classifies the source IP as abuse immediately, and ten plain 404s from one
+3 -3
View File
@@ -349,8 +349,8 @@ export function mainDomain(host, limit = 24) {
/**
* Group raw crawler hits by client hash and format each group as a row showing
* every internal page that crawler visited. Rows are sorted by total hits,
* most active crawler first, rather than by most recent hit.
* every internal page that crawler visited. Rows are sorted by most recent hit
* first, with total hits as a tie-breaker.
* ``clients`` maps client hashes to client records.
*/
export function formatCrawlerRows(crawlers, clients, pageTree, now = Date.now()) {
@@ -380,7 +380,7 @@ export function formatCrawlerRows(crawlers, clients, pageTree, now = Date.now())
return n
}
return [...groups.values()]
.sort((a, b) => totalHits(b) - totalHits(a) || b.lastStart - a.lastStart)
.sort((a, b) => b.lastStart - a.lastStart || totalHits(b) - totalHits(a))
.slice(0, 10)
.map((g) => {
const client = g.client || {}
+7 -7
View File
@@ -928,10 +928,10 @@ figure:has(img[width]) {
later rules win at equal specificity. The analytics dashboard uses the
same breakout directly on its container (div.wide — it is the page's
whole content, not a figure), and code blocks via a trailing {.wide}
line (fence attrs land on <code>, hence pre:has(.wide)). */
line (fence block attrs land on <pre> itself). */
figure:has(.wide),
div.wide,
pre:has(.wide) {
pre.wide {
width: 100vw;
max-width: none;
margin-inline: calc(50% - 50vw);
@@ -939,7 +939,7 @@ pre:has(.wide) {
/* Full bleed means edge to edge — no rounded corners. */
figure:has(.wide) img,
pre:has(.wide) {
pre.wide {
border-radius: 0;
}
@@ -948,7 +948,7 @@ pre:has(.wide) {
1fr + 4fr grid, i.e. 20vw — plus main's padding, and spans on to the
right viewport edge. */
body:has(.multicol) figure:has(.wide),
body:has(.multicol) pre:has(.wide) {
body:has(.multicol) pre.wide {
margin-inline: calc(-20vw - 1.25rem) 0;
}
@@ -956,7 +956,7 @@ body:has(.multicol) pre:has(.wide) {
window keeps its overlay scrollbars while editing, so — unlike a classic
scrollbar — they take no layout space and the vw math stays exact. */
body.editing figure:has(.wide),
body.editing pre:has(.wide) {
body.editing pre.wide {
width: calc(100vw - var(--editor-w));
margin-inline: calc(50% - (100vw - var(--editor-w)) / 2);
}
@@ -964,7 +964,7 @@ body.editing pre:has(.wide) {
/* Editing + multicol: the left gutter is 1/5 of the space right of the
editor, and the bleed also crosses main's 1.25rem left padding. */
body.editing:has(.multicol) figure:has(.wide),
body.editing:has(.multicol) pre:has(.wide) {
body.editing:has(.multicol) pre.wide {
margin-inline: calc((100vw - var(--editor-w)) / -5 - 1.25rem) 0;
}
@@ -977,7 +977,7 @@ body.editing:has(.multicol) pre:has(.wide) {
where the editing rules above apply instead. */
@media (max-width: 102rem) {
body:has(#sidebar):not(.editing) figure:has(.wide),
body:has(#sidebar):not(.editing) pre:has(.wide) {
body:has(#sidebar):not(.editing) pre.wide {
margin-inline: -13.25rem 0;
}
}
+20 -3
View File
@@ -235,9 +235,26 @@ import "overlayscrollbars/overlayscrollbars.css";
btn.textContent = "copy";
btn.addEventListener("click", async () => {
const code = pre.querySelector("code");
await navigator.clipboard.writeText(
(code || pre).textContent.replace(/\n$/, ""),
);
const text = (code || pre).textContent.replace(/\n$/, "");
// navigator.clipboard exists only in secure contexts (https or
// localhost); viewing over plain http needs the textarea fallback.
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(text);
} else {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.cssText = "position:fixed;opacity:0";
document.body.append(ta);
ta.select();
document.execCommand("copy");
ta.remove();
}
} catch {
btn.textContent = "failed";
setTimeout(() => (btn.textContent = "copy"), 1500);
return;
}
btn.textContent = "copied";
btn.classList.add("copied");
setTimeout(() => {
+26
View File
@@ -78,6 +78,31 @@ def _highlight(text: str, lang: str, _attrs: str) -> str:
return highlight(text, lexer, _formatter)
def _fence_rule(
self: RendererHTML,
tokens,
idx: int,
options,
env: dict,
) -> str:
"""Render a fenced code block.
Like the default fence rule, but block attributes (a trailing `{...}`
line, applied to the fence token by _block_attrs) go on the <pre> — the
block element — instead of the <code>, which keeps only the language
class. This is what makes e.g. `{.wide}` or `{style="..."}` after a
code fence style the block itself.
"""
token = tokens[idx]
info = token.info.strip() if token.info else ""
lang = info.split(maxsplit=1)[0] if info else ""
highlighted = (_highlight(token.content, lang, "")
or escapeHtml(token.content))
code_class = f' class="{options.langPrefix}{lang}"' if lang else ""
return (f"<pre{self.renderAttrs(token)}><code{code_class}>"
f"{highlighted}</code></pre>\n")
def _image_rule(
self: RendererHTML,
tokens,
@@ -286,6 +311,7 @@ md = (
.use(superscript_plugin)
)
md.add_render_rule("image", _image_rule)
md.add_render_rule("fence", _fence_rule)
# GFM alerts (`> [!NOTE]` etc.), built into markdown-it-py's blockquote rule.
md.options["alerts"] = True
# Block attrs must be stripped before the typographer curlifies their quotes.