Skip to content
Merged
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ Leave `:base-path` as `""` only for a site served at a domain root.
into a published site, so a project that could write `["../../.ssh"]` would
publish it.

An entry is copied to the site root under its own name, so `docs/images` ends
up at `<base-path>/images`. When the homepage is rendered from `README.md`, its
`src` and `href` values that start with `docs/<entry>/` (or `./docs/<entry>/`)
are rewritten to that copy. The README's images then show on the site and on
GitHub alike, and a path into a directory that isn't listed stays as written.

Markdown pages get GitHub's alerts too. A blockquote opening with `[!NOTE]`,
`[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` renders as a titled
callout.

## Building

```bash
Expand Down
22 changes: 22 additions & 0 deletions resources/static/css/screen.css
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,28 @@ nav.toc {
color: var(--text-dim);
}

/* GitHub alerts (> [!NOTE] and friends, site.markdown/github-alerts). Each
kind borrows a syntax-highlighting token, which already has a light and a
dark value; the fallback is the dark one, for the moment before the head
script sets data-theme. */
.doc-content .markdown-alert {
--alert-color: var(--code-attr, #79c0ff);
border-left: 3px solid var(--alert-color);
margin: 0 0 1.25rem;
padding: 0.25rem 0 0.25rem 1rem;
}
.doc-content .markdown-alert-tip { --alert-color: var(--code-name, #7ee787); }
.doc-content .markdown-alert-important { --alert-color: var(--code-title, #d2a8ff); }
.doc-content .markdown-alert-warning { --alert-color: var(--code-builtin, #ffa657); }
.doc-content .markdown-alert-caution { --alert-color: var(--code-keyword, #ff7b72); }
.doc-content .markdown-alert > p { margin: 0 0 0.5rem; }
.doc-content .markdown-alert > p:last-child { margin-bottom: 0; }
.doc-content .markdown-alert-title {
color: var(--alert-color);
font-weight: 700;
letter-spacing: 0.04em;
}

.doc-content hr {
border: none;
border-top: 1px solid var(--border);
Expand Down
28 changes: 24 additions & 4 deletions src/site/generic_home.clj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@
[html]
(str/replace html #"href='(?!https?://|/)([^']*\.html[^']*)'" "href='guide/$1'"))

(defn rewrite-readme-asset-links
"README.md's relative links are relative to the repository root, which the
site does not mirror. A directory under docs/ named in :asset-dirs is
copied to the site root under its own name (site.core/copy-project-assets!),
so the docs/images/logo.svg a README shows on GitHub is
<site-base>/images/logo.svg on the site. Rewrites each src and href that
starts with docs/<name>/ or ./docs/<name>/, in either quote style, for each
configured name. Anything else is left as it was, since a path the build
does not copy has no site URL to point at."
[html asset-names site-base]
(reduce (fn [h n]
(str/replace h (re-pattern (str "(src|href)=([\"'])(?:\\./)?docs/"
(java.util.regex.Pattern/quote n) "/"))
(fn [[_ attr q]] (str attr "=" q site-base "/" n "/"))))
html
asset-names))

(defn rendered-source
"What the homepage's source document rendered to, so a caller can both
build the page and index it without rendering the markdown twice.
Expand All @@ -52,17 +69,20 @@

Only the guide/index.md case gets its links rewritten and then
guide/-prefixed. README-sourced content keeps whatever the default
rewrite produced: its links are relative to the repository root, which
is a different context and out of scope here.
rewrite produced, since its links are relative to the repository root,
except that links into a configured asset directory point at the copy the
build makes (rewrite-readme-asset-links).

The three-arity form takes an already-computed rendered-source, for a
caller that needs the same render for something else."
([project site-ctx] (render project site-ctx (rendered-source project)))
([_project site-ctx {:keys [source body-html toc-html]}]
([project site-ctx {:keys [source body-html toc-html]}]
(let [content (case source
nil (str "<h1>" (:site-title site-ctx) "</h1><p>No documentation yet.</p>")
:guide-index (prefix-guide-links body-html)
body-html)]
(rewrite-readme-asset-links body-html
(map fs/file-name (:asset-dirs project))
(:site-base site-ctx)))]
(selmer/render-file "generic-home.html"
(merge site-ctx
{:page "home"
Expand Down
42 changes: 36 additions & 6 deletions src/site/markdown.clj
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,34 @@
[html]
(str/replace html html-or-stray-lt (fn [m] (if (= m "<") "&lt;" m))))

(def ^:private alert-titles
{"note" "Note" "tip" "Tip" "important" "Important" "warning" "Warning" "caution" "Caution"})

(def ^:private alert-blockquote
;; markdown-clj renders `> [!WARNING]` and the lines after it as ONE
;; paragraph, so the marker is the start of the blockquote's first <p>.
#"(?is)<blockquote>\s*<p>\s*\[!(note|tip|important|warning|caution)\]\s*(.*?)</blockquote>")

(defn github-alerts
"Renders GitHub's alert blockquotes (`> [!NOTE]`, TIP, IMPORTANT, WARNING,
CAUTION) the way GitHub does: a div.markdown-alert.markdown-alert-<kind>
with a p.markdown-alert-title naming the kind, instead of a plain
blockquote that shows the literal [!WARNING]. Any other blockquote is left
alone, and so is a marker inside code, which markdown-clj has already
escaped.

Two differences from GitHub, both from working on rendered HTML: a marker
followed by text on its own line (`> [!TIP] text`) becomes an alert here and
stays a quote there, and a blockquote nested inside an alert ends the alert
at the inner quote's close."
[html]
(str/replace html alert-blockquote
(fn [[_ kind body]]
(let [k (str/lower-case kind)]
(str "<div class=\"markdown-alert markdown-alert-" k "\">"
"<p class=\"markdown-alert-title\">" (alert-titles k) "</p>"
"<p>" body "</div>")))))

(defn render-markdown
"markdown -> HTML. Does NOT use markdown-clj's own :heading-anchors —
its id-generation embeds literal entity text (&#95; etc.) in id
Expand All @@ -294,13 +322,15 @@
Runs escape-intraword-underscores then unwrap-hard-wraps first (see
their docstrings) so snake_case identifiers don't get misread as
emphasis and hard-wrapped source paragraphs/list-items don't trip
markdown-clj's line-oriented parsing."
markdown-clj's line-oriented parsing. GitHub alert blockquotes become
callouts last (github-alerts)."
[markdown-text]
(escape-stray-lt
(md/md-to-html-string (-> markdown-text
escape-intraword-underscores
unwrap-hard-wraps)
:code-style (fn [lang] (str "class=\"" lang "\"")))))
(github-alerts
(escape-stray-lt
(md/md-to-html-string (-> markdown-text
escape-intraword-underscores
unwrap-hard-wraps)
:code-style (fn [lang] (str "class=\"" lang "\""))))))

(defn assign-heading-ids
"Adds a unique id= to every <h1>/<h2>/<h3>, computed from its OWN
Expand Down
24 changes: 21 additions & 3 deletions test/site/core_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,18 @@
{:out :doc :home :site :index}. opts: :home-template? (ship a project
homepage template), :assets? (ship an asset directory), :search (copied
into the site map when given), :readme? (ship a README.md and no
guide/index.md, so the README is the homepage source). :index is the
guide/index.md, so the README is the homepage source), :readme-text
(that README's markdown, in place of the default). :index is the
parsed search-documents.json, or nil when none was written."
([base] (build-fixture-site! base {}))
([base {:keys [home-template? assets? diagram? mermaid-override readme? search no-h1?]}]
([base {:keys [home-template? assets? diagram? mermaid-override readme? readme-text search no-h1?]}]
(let [tmp (fs/create-temp-dir {:prefix "jltc-site"})
docs (io/file (str tmp) "docs")
guide (io/file docs "guide")
templates (io/file docs "templates")]
(fs/create-dirs guide)
(if readme?
(spit (io/file (str tmp) "README.md") "# Readme Title\n\n## Usage\n\nRun it.\n")
(spit (io/file (str tmp) "README.md") (or readme-text "# Readme Title\n\n## Usage\n\nRun it.\n"))
(spit (io/file guide "index.md")
(if diagram?
"# Intro\n\nHello.\n\n```mermaid\nflowchart LR\n a --> b\n```\n"
Expand Down Expand Up @@ -184,6 +185,23 @@
(let [{:keys [out]} (build-fixture-site! "/some-lib" {:assets? true})]
(is (fs/exists? (io/file out "media" "x.gif")))))

(deftest a-readme-homepage-points-asset-links-at-the-copied-directory
;; A README's paths are right on GitHub (docs/media/x.gif from the repo
;; root), but the build copies docs/media to <site-base>/media, so the
;; homepage rendered from the README needs the site's path.
(let [{:keys [home out]}
(build-fixture-site! "/some-lib"
{:readme? true :assets? true
:readme-text (str "# R\n\n<p align=\"center\"><img src=\"docs/media/x.gif\"></p>\n\n"
"![a](./docs/media/x.gif) [l](docs/media/x.gif) ![o](docs/other/y.png)\n")})]
(testing "raw HTML and markdown images, with or without ./, and links"
(is (= 2 (count (re-seq #"src=\"/some-lib/media/x\.gif\"" home))))
(is (str/includes? home "href='/some-lib/media/x.gif'"))
(is (not (str/includes? home "docs/media/"))))
(testing "a path the build does not copy is left as written"
(is (str/includes? home "src=\"docs/other/y.png\"")))
(is (fs/exists? (io/file out "media" "x.gif")))))

(deftest a-missing-asset-dir-warns-instead-of-failing-the-build
;; A project may generate assets from a pipeline that has not run in
;; this checkout. That should not take the docs down.
Expand Down
28 changes: 28 additions & 0 deletions test/site/markdown_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,31 @@
(testing "code spans were already escaped and are not escaped twice"
(is (str/includes? (md/render-markdown "use `Vec<T>` here\n")
"<code>Vec&lt;T&gt;</code>"))))

(deftest github-alerts-render-every-kind-as-a-callout
;; GitHub draws `> [!WARNING]` as a titled callout; a plain blockquote
;; showed the literal marker.
(are [marker kind title]
(let [html (md/render-markdown (str "> " marker "\n> **Bold** body\n"))]
(and (str/includes? html (str "<div class=\"markdown-alert markdown-alert-" kind "\">"))
(str/includes? html (str "<p class=\"markdown-alert-title\">" title "</p>"))
(str/includes? html "<strong>Bold</strong> body")
(not (str/includes? html "[!"))
(not (str/includes? html "blockquote"))))
"[!NOTE]" "note" "Note"
"[!TIP]" "tip" "Tip"
"[!IMPORTANT]" "important" "Important"
"[!WARNING]" "warning" "Warning"
"[!CAUTION]" "caution" "Caution"
"[!note]" "note" "Note"))

(deftest github-alerts-leave-other-quotes-and-code-alone
(testing "a blockquote without a marker stays a blockquote"
(is (str/includes? (md/render-markdown "> just a quote\n") "<blockquote>")))
(testing "an unknown marker stays a blockquote"
(is (str/includes? (md/render-markdown "> [!DANGER]\n> x\n") "[!DANGER]")))
(testing "a marker inside a fenced code block is code, not an alert"
(let [html (md/render-markdown "```\n> [!NOTE]\n> x\n```\n")]
;; markdown-clj writes the brackets in code as entities
(is (not (str/includes? html "markdown-alert")))
(is (str/includes? html "<pre><code>&gt; &#91;!NOTE&#93;")))))
Loading