Preview markdown files formatted, with a raw toggle (#780)

* Preview markdown files formatted, with a raw toggle

Markdown files are typed text/markdown (libmagic reports them as text/plain, or
as a programming language when they hold code) and keep the text indexer, so
they stay deep-searchable.  Existing files switch type on their next refresh.

The file preview renders markdown with markdown-it into the page, so it follows
the theme tokens and the night media filter.  A Formatted/Raw toggle shows the
source instead, and the choice is remembered.  Relative links and images
resolve against the file's directory; URLs that climb out of /media/ are
dropped.  Raw HTML in a file is escaped rather than parsed, and the output is
also sanitized with DOMPurify.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Do not load images from other sites in the markdown preview

An image from another site (http:, https:, or protocol-relative //) loaded as
soon as the preview opened, telling that site who was reading the file and
when.  Such an image now becomes a link labeled with its alt text, so the
reader chooses whether to follow it.  Inside a link (a README badge) it becomes
the outer link's text, since links cannot nest.  Media and embedded data:
images still load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lrnselfreliance 2026-10-05 13:26:37 -06:00 • committed by Roland
parent 9739dc9e1b
commit 04779b8557
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
13 changed files with 828 additions and 10 deletions

View file

@ -316,6 +316,83 @@ pre.wrap-text {
height: 100%;
}
/* A markdown file preview, formatted or raw. Scrolls rather than fitting like .preview-fit, because
it is a document to read, not media to frame. Colors come from the theme tokens. */
.markdown-preview {
height: calc(100dvh - 21rem);
min-height: 15rem;
overflow: auto;
margin-top: 0.5em;
padding: 0 0.25em;
}
.markdown-raw {
margin: 0;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.markdown-body {
max-width: 50rem;
line-height: 1.6;
overflow-wrap: break-word;
}
.markdown-body > :first-child {
margin-top: 0;
}
.markdown-body h1, .markdown-body h2 {
padding-bottom: 0.25em;
border-bottom: 1px solid var(--table-line);
}
.markdown-body img {
max-width: 100%;
}
.markdown-body blockquote {
margin: 0 0 1em;
padding: 0 1em;
color: var(--muted);
border-left: 0.25em solid var(--border);
}
.markdown-body code {
padding: 0.1em 0.3em;
border-radius: 0.25em;
background-color: var(--head);
}
.markdown-body pre {
padding: 0.75em 1em;
overflow-x: auto;
border-radius: 0.25em;
background-color: var(--head);
}
.markdown-body pre code {
padding: 0;
background-color: transparent;
}
.markdown-body table {
display: block;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
}
.markdown-body th, .markdown-body td {
padding: 0.3em 0.75em;
border: 1px solid var(--table-line);
}
.markdown-body hr {
border: none;
border-top: 1px solid var(--border);
}
td.file-path {
cursor: pointer;
}