epistemic v2
This commit is contained in:
parent
866001ba7d
commit
a5495035be
20
Makefile
20
Makefile
|
|
@ -1,4 +1,4 @@
|
||||||
.PHONY: build deploy sign download-model convert-images watch clean dev
|
.PHONY: build deploy sign download-model convert-images pdf-thumbs watch clean dev
|
||||||
|
|
||||||
# Source .env for GITHUB_TOKEN and GITHUB_REPO if it exists.
|
# Source .env for GITHUB_TOKEN and GITHUB_REPO if it exists.
|
||||||
# .env format: KEY=value (one per line, no `export` prefix, no quotes needed).
|
# .env format: KEY=value (one per line, no `export` prefix, no quotes needed).
|
||||||
|
|
@ -10,6 +10,7 @@ build:
|
||||||
@git diff --cached --quiet || git commit -m "auto: $$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
@git diff --cached --quiet || git commit -m "auto: $$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
@date +%s > data/build-start.txt
|
@date +%s > data/build-start.txt
|
||||||
@./tools/convert-images.sh
|
@./tools/convert-images.sh
|
||||||
|
@$(MAKE) -s pdf-thumbs
|
||||||
cabal run site -- build
|
cabal run site -- build
|
||||||
pagefind --site _site
|
pagefind --site _site
|
||||||
@if [ -d .venv ]; then \
|
@if [ -d .venv ]; then \
|
||||||
|
|
@ -35,6 +36,23 @@ download-model:
|
||||||
convert-images:
|
convert-images:
|
||||||
@./tools/convert-images.sh
|
@./tools/convert-images.sh
|
||||||
|
|
||||||
|
# Generate first-page thumbnails for PDFs in static/papers/ (also runs in build).
|
||||||
|
# Requires pdftoppm: pacman -S poppler / apt install poppler-utils
|
||||||
|
# Thumbnails are written as static/papers/foo.thumb.png alongside each PDF.
|
||||||
|
# Skipped silently when pdftoppm is not installed or static/papers/ is empty.
|
||||||
|
pdf-thumbs:
|
||||||
|
@if command -v pdftoppm >/dev/null 2>&1; then \
|
||||||
|
find static/papers -name '*.pdf' 2>/dev/null | while read pdf; do \
|
||||||
|
thumb="$${pdf%.pdf}.thumb"; \
|
||||||
|
if [ ! -f "$${thumb}.png" ] || [ "$$pdf" -nt "$${thumb}.png" ]; then \
|
||||||
|
echo " pdf-thumb $$pdf"; \
|
||||||
|
pdftoppm -r 100 -f 1 -l 1 -png -singlefile "$$pdf" "$$thumb"; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
else \
|
||||||
|
echo "pdf-thumbs: pdftoppm not found — install poppler (skipping)"; \
|
||||||
|
fi
|
||||||
|
|
||||||
deploy: build sign
|
deploy: build sign
|
||||||
@if [ -z "$(GITHUB_TOKEN)" ] || [ -z "$(GITHUB_REPO)" ]; then \
|
@if [ -z "$(GITHUB_TOKEN)" ] || [ -z "$(GITHUB_REPO)" ]; then \
|
||||||
echo "Skipping GitHub push: set GITHUB_TOKEN and GITHUB_REPO in .env"; \
|
echo "Skipping GitHub push: set GITHUB_TOKEN and GITHUB_REPO in .env"; \
|
||||||
|
|
|
||||||
43
WRITING.md
43
WRITING.md
|
|
@ -272,6 +272,49 @@ not derivable from the page title.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## PDF Embeds
|
||||||
|
|
||||||
|
Embed a hosted PDF in a full PDF.js viewer (page navigation, zoom, text
|
||||||
|
selection) using the `{{pdf:...}}` directive on its own line:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
{{pdf:/papers/smith2023.pdf}}
|
||||||
|
{{pdf:/papers/smith2023.pdf#5}} ← open at page 5 (bare integer)
|
||||||
|
{{pdf:/papers/smith2023.pdf#page=5}} ← explicit form, same result
|
||||||
|
```
|
||||||
|
|
||||||
|
The directive produces an iframe pointing at the vendored PDF.js viewer at
|
||||||
|
`/pdfjs/web/viewer.html?file=...`. The PDF must be served from the same
|
||||||
|
origin (i.e. it must be a file you host).
|
||||||
|
|
||||||
|
**Storing papers.** Drop PDFs in `static/papers/`; Hakyll's static rule copies
|
||||||
|
everything under `static/` to `_site/` unchanged. Reference them as
|
||||||
|
`/papers/filename.pdf`.
|
||||||
|
|
||||||
|
**One-time vendor setup.** PDF.js is not included in the repo. Install it once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install pdfjs-dist
|
||||||
|
mkdir -p static/pdfjs
|
||||||
|
cp -r node_modules/pdfjs-dist/web static/pdfjs/
|
||||||
|
cp -r node_modules/pdfjs-dist/build static/pdfjs/
|
||||||
|
rm -rf node_modules package-lock.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Then commit `static/pdfjs/` (it is static and changes only when you want to
|
||||||
|
upgrade PDF.js). Hakyll's existing `static/**` rule copies it through without
|
||||||
|
any new build rules.
|
||||||
|
|
||||||
|
**Optional: disable the "Open file" button** in the viewer. Edit
|
||||||
|
`static/pdfjs/web/viewer.html` and set `AppOptions.set('disableOpenFile', true)`
|
||||||
|
in the `webViewerLoad` callback, or add a thin CSS rule to `viewer.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
#openFile, #secondaryOpenFile { display: none !important; }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Math
|
## Math
|
||||||
|
|
||||||
Pandoc parses LaTeX math and wraps it in `class="math inline"` / `class="math display"`
|
Pandoc parses LaTeX math and wraps it in `class="math inline"` / `class="math display"`
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ affiliationField = listFieldWith "affiliation-links" ctx $ \item -> do
|
||||||
parseEntry s = case break (== '|') s of
|
parseEntry s = case break (== '|') s of
|
||||||
(name, '|' : url) -> (trim name, trim url)
|
(name, '|' : url) -> (trim name, trim url)
|
||||||
(name, _) -> (trim name, "")
|
(name, _) -> (trim name, "")
|
||||||
trim = reverse . dropWhile (== ' ') . reverse . dropWhile (== ' ')
|
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
-- Build time field
|
-- Build time field
|
||||||
|
|
@ -178,11 +177,37 @@ confidenceTrendField = field "confidence-trend" $ \item -> do
|
||||||
| otherwise -> return "\x2192" -- →
|
| otherwise -> return "\x2192" -- →
|
||||||
_ -> return "\x2192"
|
_ -> return "\x2192"
|
||||||
|
|
||||||
|
-- | @$overall-score$@: weighted composite of confidence (50 %),
|
||||||
|
-- evidence quality (30 %), and importance (20 %), expressed as an
|
||||||
|
-- integer on a 0–100 scale.
|
||||||
|
-- Returns @noResult@ when any contributing field is absent, so
|
||||||
|
-- @$if(overall-score)$@ guards the template safely.
|
||||||
|
--
|
||||||
|
-- Formula: raw = conf/100·0.5 + ev/5·0.3 + imp/5·0.2 (0–1)
|
||||||
|
-- score = clamp₀₋₁₀₀(round(raw · 100))
|
||||||
|
overallScoreField :: Context String
|
||||||
|
overallScoreField = field "overall-score" $ \item -> do
|
||||||
|
meta <- getMetadata (itemIdentifier item)
|
||||||
|
let readInt s = readMaybe s :: Maybe Int
|
||||||
|
case ( readInt =<< lookupString "confidence" meta
|
||||||
|
, readInt =<< lookupString "evidence" meta
|
||||||
|
, readInt =<< lookupString "importance" meta
|
||||||
|
) of
|
||||||
|
(Just conf, Just ev, Just imp) ->
|
||||||
|
let raw :: Double
|
||||||
|
raw = fromIntegral conf / 100.0 * 0.5
|
||||||
|
+ fromIntegral ev / 5.0 * 0.3
|
||||||
|
+ fromIntegral imp / 5.0 * 0.2
|
||||||
|
score = max 0 (min 100 (round (raw * 100.0) :: Int))
|
||||||
|
in return (show score)
|
||||||
|
_ -> fail "overall-score: confidence, evidence, or importance not set"
|
||||||
|
|
||||||
-- | All epistemic context fields composed.
|
-- | All epistemic context fields composed.
|
||||||
epistemicCtx :: Context String
|
epistemicCtx :: Context String
|
||||||
epistemicCtx =
|
epistemicCtx =
|
||||||
dotsField "importance-dots" "importance"
|
dotsField "importance-dots" "importance"
|
||||||
<> dotsField "evidence-dots" "evidence"
|
<> dotsField "evidence-dots" "evidence"
|
||||||
|
<> overallScoreField
|
||||||
<> confidenceTrendField
|
<> confidenceTrendField
|
||||||
<> stabilityField
|
<> stabilityField
|
||||||
<> lastReviewedField
|
<> lastReviewedField
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import qualified Filters.Dropcaps as Dropcaps
|
||||||
import qualified Filters.Math as Math
|
import qualified Filters.Math as Math
|
||||||
import qualified Filters.Wikilinks as Wikilinks
|
import qualified Filters.Wikilinks as Wikilinks
|
||||||
import qualified Filters.Transclusion as Transclusion
|
import qualified Filters.Transclusion as Transclusion
|
||||||
|
import qualified Filters.EmbedPdf as EmbedPdf
|
||||||
import qualified Filters.Code as Code
|
import qualified Filters.Code as Code
|
||||||
import qualified Filters.Images as Images
|
import qualified Filters.Images as Images
|
||||||
|
|
||||||
|
|
@ -33,6 +34,7 @@ applyAll
|
||||||
. Images.apply
|
. Images.apply
|
||||||
|
|
||||||
-- | Apply source-level preprocessors to the raw Markdown string.
|
-- | Apply source-level preprocessors to the raw Markdown string.
|
||||||
-- Run before 'readPandocWith'.
|
-- Order matters: EmbedPdf must run before Transclusion, because the
|
||||||
|
-- transclusion parser would otherwise treat {{pdf:...}} as a broken slug.
|
||||||
preprocessSource :: String -> String
|
preprocessSource :: String -> String
|
||||||
preprocessSource = Transclusion.preprocess . Wikilinks.preprocess
|
preprocessSource = Transclusion.preprocess . EmbedPdf.preprocess . Wikilinks.preprocess
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
{-# LANGUAGE GHC2021 #-}
|
||||||
|
-- | Source-level preprocessor for inline PDF embeds.
|
||||||
|
--
|
||||||
|
-- Rewrites block-level @{{pdf:...}}@ directives to raw HTML that renders the
|
||||||
|
-- named file inside a vendored PDF.js viewer iframe.
|
||||||
|
--
|
||||||
|
-- Syntax (must be the sole content of a line after trimming):
|
||||||
|
--
|
||||||
|
-- > {{pdf:/papers/foo.pdf}} — embed from page 1
|
||||||
|
-- > {{pdf:/papers/foo.pdf#5}} — start at page 5 (bare integer)
|
||||||
|
-- > {{pdf:/papers/foo.pdf#page=5}} — start at page 5 (explicit form)
|
||||||
|
--
|
||||||
|
-- The file path must be root-relative (begins with @/@).
|
||||||
|
-- PDF.js is expected to be vendored at @/pdfjs/web/viewer.html@.
|
||||||
|
module Filters.EmbedPdf (preprocess) where
|
||||||
|
|
||||||
|
import Data.Char (isDigit)
|
||||||
|
import Data.List (isPrefixOf, isSuffixOf)
|
||||||
|
|
||||||
|
-- | Apply PDF-embed substitution to the raw Markdown source string.
|
||||||
|
preprocess :: String -> String
|
||||||
|
preprocess = unlines . map processLine . lines
|
||||||
|
|
||||||
|
processLine :: String -> String
|
||||||
|
processLine line =
|
||||||
|
case parseDirective (trim line) of
|
||||||
|
Nothing -> line
|
||||||
|
Just (filePath, pageHash) -> renderEmbed filePath pageHash
|
||||||
|
|
||||||
|
-- | Parse a @{{pdf:/path/to/file.pdf}}@ or @{{pdf:/path.pdf#N}}@ directive.
|
||||||
|
-- Returns @(filePath, pageHash)@ where @pageHash@ is either @""@ or @"#page=N"@.
|
||||||
|
parseDirective :: String -> Maybe (String, String)
|
||||||
|
parseDirective s
|
||||||
|
| not ("{{pdf:" `isPrefixOf` s) = Nothing
|
||||||
|
| not ("}}" `isSuffixOf` s) = Nothing
|
||||||
|
| otherwise =
|
||||||
|
let inner = take (length s - 2) (drop 6 s) -- strip "{{pdf:" and "}}"
|
||||||
|
(path, frag) = break (== '#') inner
|
||||||
|
in if null path
|
||||||
|
then Nothing
|
||||||
|
else Just (path, parsePageHash frag)
|
||||||
|
|
||||||
|
-- | Convert the fragment part of the directive (e.g. @#5@ or @#page=5@) to a
|
||||||
|
-- PDF.js-compatible @#page=N@ hash, or @""@ if absent/invalid.
|
||||||
|
parsePageHash :: String -> String
|
||||||
|
parsePageHash ('#' : rest)
|
||||||
|
| "page=" `isPrefixOf` rest =
|
||||||
|
let n = takeWhile isDigit (drop 5 rest)
|
||||||
|
in if null n then "" else "#page=" ++ n
|
||||||
|
| all isDigit rest && not (null rest) = "#page=" ++ rest
|
||||||
|
parsePageHash _ = ""
|
||||||
|
|
||||||
|
-- | Render the HTML for a PDF embed.
|
||||||
|
renderEmbed :: String -> String -> String
|
||||||
|
renderEmbed filePath pageHash =
|
||||||
|
let viewerUrl = "/pdfjs/web/viewer.html?file=" ++ encodeQueryValue filePath ++ pageHash
|
||||||
|
in "<div class=\"pdf-embed-wrapper\">"
|
||||||
|
++ "<iframe class=\"pdf-embed\""
|
||||||
|
++ " src=\"" ++ viewerUrl ++ "\""
|
||||||
|
++ " title=\"PDF document\""
|
||||||
|
++ " loading=\"lazy\""
|
||||||
|
++ " allowfullscreen></iframe>"
|
||||||
|
++ "</div>"
|
||||||
|
|
||||||
|
-- | Percent-encode characters that would break a query-string value.
|
||||||
|
-- Slashes are left unencoded so root-relative paths remain readable and
|
||||||
|
-- work correctly with PDF.js's internal fetch.
|
||||||
|
encodeQueryValue :: String -> String
|
||||||
|
encodeQueryValue = concatMap enc
|
||||||
|
where
|
||||||
|
enc ' ' = "%20"
|
||||||
|
enc '&' = "%26"
|
||||||
|
enc '?' = "%3F"
|
||||||
|
enc '+' = "%2B"
|
||||||
|
enc '"' = "%22"
|
||||||
|
enc c = [c]
|
||||||
|
|
||||||
|
-- | Strip leading and trailing spaces.
|
||||||
|
trim :: String -> String
|
||||||
|
trim = f . f
|
||||||
|
where f = reverse . dropWhile (== ' ')
|
||||||
|
|
@ -16,8 +16,25 @@ import Text.Pandoc.Definition
|
||||||
import Text.Pandoc.Walk (walk)
|
import Text.Pandoc.Walk (walk)
|
||||||
|
|
||||||
-- | Apply link classification to the entire document.
|
-- | Apply link classification to the entire document.
|
||||||
|
-- Two passes: PDF links first (rewrites href to viewer URL), then external
|
||||||
|
-- link classification (operates on http/https, so no overlap).
|
||||||
apply :: Pandoc -> Pandoc
|
apply :: Pandoc -> Pandoc
|
||||||
apply = walk classifyLink
|
apply = walk classifyLink . walk classifyPdfLink
|
||||||
|
|
||||||
|
-- | Rewrite root-relative PDF links to open via the vendored PDF.js viewer.
|
||||||
|
-- Preserves the original path in @data-pdf-src@ so the popup thumbnail
|
||||||
|
-- provider can locate the corresponding @.thumb.png@ file.
|
||||||
|
-- Skips links that are already pointing at the viewer (idempotent).
|
||||||
|
classifyPdfLink :: Inline -> Inline
|
||||||
|
classifyPdfLink (Link (ident, classes, kvs) ils (url, title))
|
||||||
|
| "/" `T.isPrefixOf` url
|
||||||
|
, ".pdf" `T.isSuffixOf` T.toLower url
|
||||||
|
, "pdf-link" `notElem` classes =
|
||||||
|
let viewerUrl = "/pdfjs/web/viewer.html?file=" <> encodeQueryValue url
|
||||||
|
classes' = classes ++ ["pdf-link"]
|
||||||
|
kvs' = kvs ++ [("data-pdf-src", url)]
|
||||||
|
in Link (ident, classes', kvs') ils (viewerUrl, title)
|
||||||
|
classifyPdfLink x = x
|
||||||
|
|
||||||
classifyLink :: Inline -> Inline
|
classifyLink :: Inline -> Inline
|
||||||
classifyLink (Link (ident, classes, kvs) ils (url, title))
|
classifyLink (Link (ident, classes, kvs) ils (url, title))
|
||||||
|
|
@ -49,3 +66,16 @@ domainIcon url
|
||||||
| "doi.org" `T.isInfixOf` url = "doi"
|
| "doi.org" `T.isInfixOf` url = "doi"
|
||||||
| "github.com" `T.isInfixOf` url = "github"
|
| "github.com" `T.isInfixOf` url = "github"
|
||||||
| otherwise = "external"
|
| otherwise = "external"
|
||||||
|
|
||||||
|
-- | Percent-encode characters that would break a @?file=@ query-string value.
|
||||||
|
-- Slashes are intentionally left unencoded so root-relative paths remain
|
||||||
|
-- readable and work correctly with PDF.js's internal fetch.
|
||||||
|
encodeQueryValue :: Text -> Text
|
||||||
|
encodeQueryValue = T.concatMap enc
|
||||||
|
where
|
||||||
|
enc ' ' = "%20"
|
||||||
|
enc '&' = "%26"
|
||||||
|
enc '?' = "%3F"
|
||||||
|
enc '+' = "%2B"
|
||||||
|
enc '"' = "%22"
|
||||||
|
enc c = T.singleton c
|
||||||
|
|
|
||||||
|
|
@ -605,6 +605,15 @@ nav.site-nav {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-underline-offset: 2px;
|
text-underline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
/* When the score box is present, suppress the link-level underline and
|
||||||
|
restore it only on the label text — leaves the gap and box undecorated. */
|
||||||
|
.meta-pagelinks a:has(.meta-overall-score):hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.meta-pagelinks a:has(.meta-overall-score):hover .ep-link-label {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
.meta-pagelinks a + a::before {
|
.meta-pagelinks a + a::before {
|
||||||
content: " · ";
|
content: " · ";
|
||||||
color: var(--border);
|
color: var(--border);
|
||||||
|
|
@ -860,6 +869,24 @@ details[open] > .ep-summary::before { content: "▾\00a0"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Overall score percentage in the top metadata nav (adjacent to Epistemic link).
|
||||||
|
Renders as a compact numeric chip: "72%" in small-caps sans, separated from
|
||||||
|
the link text by a faint centerdot. */
|
||||||
|
.meta-overall-score {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid var(--border-muted);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0.05em 0.5em;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
vertical-align: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
PAGINATION NAV
|
PAGINATION NAV
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
@ -1393,3 +1420,31 @@ pre:hover .copy-btn,
|
||||||
/* Copy button: always visible on touch (no persistent hover) */
|
/* Copy button: always visible on touch (no persistent hover) */
|
||||||
.copy-btn { opacity: 1; }
|
.copy-btn { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
PDF EMBED
|
||||||
|
Inline PDF.js viewer iframes rendered from {{pdf:...}} directives.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.pdf-embed-wrapper {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-embed {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 70vh;
|
||||||
|
min-height: 420px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.pdf-embed {
|
||||||
|
height: 60vh;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,31 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Epistemic preview popup — content cloned from #epistemic; ep-* styles inherited */
|
||||||
|
.popup-epistemic {
|
||||||
|
min-width: 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PDF thumbnail popup — first-page image generated by pdftoppm at build time */
|
||||||
|
.link-popup:has(.popup-pdf) {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-pdf {
|
||||||
|
line-height: 0; /* collapse whitespace gap below <img> */
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-pdf-thumb {
|
||||||
|
display: block;
|
||||||
|
width: 320px;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 420px;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: top left;
|
||||||
|
}
|
||||||
|
|
||||||
/* PGP signature popup */
|
/* PGP signature popup */
|
||||||
.popup-sig pre {
|
.popup-sig pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,11 @@
|
||||||
bind(el, citationContent);
|
bind(el, citationContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Epistemic jump link — preview of the status/confidence/dot block */
|
||||||
|
root.querySelectorAll('a[href="#epistemic"]').forEach(function (el) {
|
||||||
|
bind(el, epistemicContent);
|
||||||
|
});
|
||||||
|
|
||||||
/* Internal links — absolute (/foo) and relative (../../foo) same-origin hrefs.
|
/* Internal links — absolute (/foo) and relative (../../foo) same-origin hrefs.
|
||||||
relativizeUrls in Hakyll makes index-page links relative, so we must match both. */
|
relativizeUrls in Hakyll makes index-page links relative, so we must match both. */
|
||||||
root.querySelectorAll('a[href^="/"], a[href^="./"], a[href^="../"]').forEach(function (el) {
|
root.querySelectorAll('a[href^="/"], a[href^="./"], a[href^="../"]').forEach(function (el) {
|
||||||
|
|
@ -71,10 +76,16 @@
|
||||||
if (!inAuthors && !isBacklink) {
|
if (!inAuthors && !isBacklink) {
|
||||||
if (el.closest('nav, #toc, footer, .page-meta-footer, .metadata')) return;
|
if (el.closest('nav, #toc, footer, .page-meta-footer, .metadata')) return;
|
||||||
if (el.classList.contains('cite-link') || el.classList.contains('meta-tag')) return;
|
if (el.classList.contains('cite-link') || el.classList.contains('meta-tag')) return;
|
||||||
|
if (el.classList.contains('pdf-link')) return;
|
||||||
}
|
}
|
||||||
bind(el, internalContent);
|
bind(el, internalContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* PDF links — rewritten to viewer URL by Links.hs; thumbnail on hover */
|
||||||
|
root.querySelectorAll('a.pdf-link[data-pdf-src]').forEach(function (el) {
|
||||||
|
bind(el, pdfContent);
|
||||||
|
});
|
||||||
|
|
||||||
/* PGP signature links in footer */
|
/* PGP signature links in footer */
|
||||||
root.querySelectorAll('a.footer-sig-link').forEach(function (el) {
|
root.querySelectorAll('a.footer-sig-link').forEach(function (el) {
|
||||||
bind(el, sigContent);
|
bind(el, sigContent);
|
||||||
|
|
@ -550,6 +561,48 @@
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Epistemic jump link — reads the #epistemic section already in the DOM
|
||||||
|
and renders a compact summary: status/confidence/dots + expanded DL.
|
||||||
|
All ep-* classes are already styled via components.css. */
|
||||||
|
function epistemicContent() {
|
||||||
|
var section = document.getElementById('epistemic');
|
||||||
|
if (!section) return Promise.resolve(null);
|
||||||
|
|
||||||
|
var html = '<div class="popup-epistemic">';
|
||||||
|
|
||||||
|
var compact = section.querySelector('.ep-compact');
|
||||||
|
if (compact) {
|
||||||
|
html += '<div class="ep-compact">' + compact.innerHTML + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var expanded = section.querySelector('.ep-expanded');
|
||||||
|
if (expanded) {
|
||||||
|
html += '<dl class="ep-expanded">' + expanded.innerHTML + '</dl>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
return Promise.resolve(html || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Local PDF — shows the build-time first-page thumbnail (.thumb.png).
|
||||||
|
Returns null (no popup) if the thumbnail file does not exist. */
|
||||||
|
function pdfContent(target) {
|
||||||
|
var src = target.dataset.pdfSrc;
|
||||||
|
if (!src) return Promise.resolve(null);
|
||||||
|
var thumb = src.replace(/\.pdf$/i, '.thumb.png');
|
||||||
|
if (cache[thumb]) return Promise.resolve(cache[thumb]);
|
||||||
|
/* HEAD request: verify thumbnail exists before committing to a popup. */
|
||||||
|
return fetch(thumb, { method: 'HEAD', credentials: 'same-origin' })
|
||||||
|
.then(function (r) {
|
||||||
|
if (!r.ok) return null;
|
||||||
|
return store(thumb,
|
||||||
|
'<div class="popup-pdf">'
|
||||||
|
+ '<img class="popup-pdf-thumb" src="' + esc(thumb) + '" alt="PDF first page">'
|
||||||
|
+ '</div>');
|
||||||
|
})
|
||||||
|
.catch(function () { return null; });
|
||||||
|
}
|
||||||
|
|
||||||
/* PGP signature — fetch the .sig file and display ASCII armor */
|
/* PGP signature — fetch the .sig file and display ASCII armor */
|
||||||
function sigContent(target) {
|
function sigContent(target) {
|
||||||
var href = target.getAttribute('href');
|
var href = target.getAttribute('href');
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
$endif$
|
$endif$
|
||||||
<nav class="meta-row meta-pagelinks" aria-label="Page sections">
|
<nav class="meta-row meta-pagelinks" aria-label="Page sections">
|
||||||
<a href="#version-history">History</a>
|
<a href="#version-history">History</a>
|
||||||
$if(status)$<a href="#epistemic">Epistemic</a>$endif$
|
$if(status)$<a href="#epistemic">$if(overall-score)$<span class="ep-link-label">Epistemic</span><span class="meta-overall-score" title="Overall score: $overall-score$/100">$overall-score$%</span>$else$Epistemic$endif$</a>$endif$
|
||||||
$if(bibliography)$<a href="#bibliography">Bibliography</a>$endif$
|
$if(bibliography)$<a href="#bibliography">Bibliography</a>$endif$
|
||||||
$if(backlinks)$<a href="#backlinks">Backlinks</a>$endif$
|
$if(backlinks)$<a href="#backlinks">Backlinks</a>$endif$
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
$if(status)$
|
$if(status)$
|
||||||
<div class="meta-footer-section meta-footer-epistemic" id="epistemic">
|
<div class="meta-footer-section meta-footer-epistemic" id="epistemic">
|
||||||
<h3>Epistemic</h3>
|
<h3><a href="/colophon.html#living-documents">Epistemic</a></h3>
|
||||||
<div class="ep-compact">
|
<div class="ep-compact">
|
||||||
<span class="ep-status">$status$</span>
|
<span class="ep-status">$status$</span>
|
||||||
$if(confidence)$<span class="ep-confidence" title="Confidence">$confidence$%</span>$endif$
|
$if(confidence)$<span class="ep-confidence" title="Confidence">$confidence$%</span>$endif$
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue