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 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 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 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 - **Abuse (scanner) hits**: a 404 for a telltale path — any URL segment
starting with a dot (`/.env`, `/.git/config`) or ending in `.php` starting with a dot (`/.env`, `/.git/config`) or ending in `.php`
classifies the source IP as abuse immediately, and ten plain 404s from one 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 * 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, * every internal page that crawler visited. Rows are sorted by most recent hit
* most active crawler first, rather than by most recent hit. * first, with total hits as a tie-breaker.
* ``clients`` maps client hashes to client records. * ``clients`` maps client hashes to client records.
*/ */
export function formatCrawlerRows(crawlers, clients, pageTree, now = Date.now()) { 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 n
} }
return [...groups.values()] 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) .slice(0, 10)
.map((g) => { .map((g) => {
const client = g.client || {} 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 later rules win at equal specificity. The analytics dashboard uses the
same breakout directly on its container (div.wide — it is the page's 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} 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), figure:has(.wide),
div.wide, div.wide,
pre:has(.wide) { pre.wide {
width: 100vw; width: 100vw;
max-width: none; max-width: none;
margin-inline: calc(50% - 50vw); margin-inline: calc(50% - 50vw);
@@ -939,7 +939,7 @@ pre:has(.wide) {
/* Full bleed means edge to edge — no rounded corners. */ /* Full bleed means edge to edge — no rounded corners. */
figure:has(.wide) img, figure:has(.wide) img,
pre:has(.wide) { pre.wide {
border-radius: 0; 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 1fr + 4fr grid, i.e. 20vw — plus main's padding, and spans on to the
right viewport edge. */ right viewport edge. */
body:has(.multicol) figure:has(.wide), body:has(.multicol) figure:has(.wide),
body:has(.multicol) pre:has(.wide) { body:has(.multicol) pre.wide {
margin-inline: calc(-20vw - 1.25rem) 0; 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 window keeps its overlay scrollbars while editing, so — unlike a classic
scrollbar — they take no layout space and the vw math stays exact. */ scrollbar — they take no layout space and the vw math stays exact. */
body.editing figure:has(.wide), body.editing figure:has(.wide),
body.editing pre:has(.wide) { body.editing pre.wide {
width: calc(100vw - var(--editor-w)); width: calc(100vw - var(--editor-w));
margin-inline: calc(50% - (100vw - var(--editor-w)) / 2); 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 /* 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. */ editor, and the bleed also crosses main's 1.25rem left padding. */
body.editing:has(.multicol) figure:has(.wide), 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; 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. */ where the editing rules above apply instead. */
@media (max-width: 102rem) { @media (max-width: 102rem) {
body:has(#sidebar):not(.editing) figure:has(.wide), 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; margin-inline: -13.25rem 0;
} }
} }
+20 -3
View File
@@ -235,9 +235,26 @@ import "overlayscrollbars/overlayscrollbars.css";
btn.textContent = "copy"; btn.textContent = "copy";
btn.addEventListener("click", async () => { btn.addEventListener("click", async () => {
const code = pre.querySelector("code"); const code = pre.querySelector("code");
await navigator.clipboard.writeText( const text = (code || pre).textContent.replace(/\n$/, "");
(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.textContent = "copied";
btn.classList.add("copied"); btn.classList.add("copied");
setTimeout(() => { setTimeout(() => {
+26
View File
@@ -78,6 +78,31 @@ def _highlight(text: str, lang: str, _attrs: str) -> str:
return highlight(text, lexer, _formatter) 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( def _image_rule(
self: RendererHTML, self: RendererHTML,
tokens, tokens,
@@ -286,6 +311,7 @@ md = (
.use(superscript_plugin) .use(superscript_plugin)
) )
md.add_render_rule("image", _image_rule) 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. # GFM alerts (`> [!NOTE]` etc.), built into markdown-it-py's blockquote rule.
md.options["alerts"] = True md.options["alerts"] = True
# Block attrs must be stripped before the typographer curlifies their quotes. # Block attrs must be stripped before the typographer curlifies their quotes.