date data
This commit is contained in:
parent
1a532f881b
commit
237380c4be
|
|
@ -31,9 +31,11 @@ import Hakyll hiding (trim)
|
||||||
import Backlinks (backlinksField)
|
import Backlinks (backlinksField)
|
||||||
import Dingbat (dingbatField)
|
import Dingbat (dingbatField)
|
||||||
import SimilarLinks (similarLinksField)
|
import SimilarLinks (similarLinksField)
|
||||||
import Stability (stabilityField, lastReviewedField, versionHistoryField,
|
import Stability (stabilityField, lastReviewedField, lastReviewedIsoField,
|
||||||
|
versionHistoryField,
|
||||||
versionHistoryPrimaryField, versionHistoryRestField,
|
versionHistoryPrimaryField, versionHistoryRestField,
|
||||||
versionHistoryRangeField)
|
versionHistoryRangeField, versionHistoryRangeStartField,
|
||||||
|
versionHistoryRangeEndField, versionHistoryCommitsField)
|
||||||
import Utils (authorSlugify, authorNameOf, trim)
|
import Utils (authorSlugify, authorNameOf, trim)
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
@ -377,6 +379,7 @@ epistemicCtx =
|
||||||
<> confidenceTrendField
|
<> confidenceTrendField
|
||||||
<> stabilityField
|
<> stabilityField
|
||||||
<> lastReviewedField
|
<> lastReviewedField
|
||||||
|
<> lastReviewedIsoField
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
-- Essay context
|
-- Essay context
|
||||||
|
|
@ -398,6 +401,9 @@ essayCtx =
|
||||||
<> versionHistoryPrimaryField
|
<> versionHistoryPrimaryField
|
||||||
<> versionHistoryRestField
|
<> versionHistoryRestField
|
||||||
<> versionHistoryRangeField
|
<> versionHistoryRangeField
|
||||||
|
<> versionHistoryRangeStartField
|
||||||
|
<> versionHistoryRangeEndField
|
||||||
|
<> versionHistoryCommitsField
|
||||||
<> dateField "date-created" "%-d %B %Y"
|
<> dateField "date-created" "%-d %B %Y"
|
||||||
<> dateField "date-modified" "%-d %B %Y"
|
<> dateField "date-modified" "%-d %B %Y"
|
||||||
<> constField "math" "true"
|
<> constField "math" "true"
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,14 @@
|
||||||
module Stability
|
module Stability
|
||||||
( stabilityField
|
( stabilityField
|
||||||
, lastReviewedField
|
, lastReviewedField
|
||||||
|
, lastReviewedIsoField
|
||||||
, versionHistoryField
|
, versionHistoryField
|
||||||
, versionHistoryPrimaryField
|
, versionHistoryPrimaryField
|
||||||
, versionHistoryRestField
|
, versionHistoryRestField
|
||||||
, versionHistoryRangeField
|
, versionHistoryRangeField
|
||||||
|
, versionHistoryRangeStartField
|
||||||
|
, versionHistoryRangeEndField
|
||||||
|
, versionHistoryCommitsField
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Exception (catch, IOException)
|
import Control.Exception (catch, IOException)
|
||||||
|
|
@ -159,12 +163,30 @@ lastReviewedField = field "last-reviewed" $ \item -> do
|
||||||
Nothing -> fail "no last-reviewed"
|
Nothing -> fail "no last-reviewed"
|
||||||
Just d -> return d
|
Just d -> return d
|
||||||
|
|
||||||
|
-- | Raw-ISO companion to @$last-reviewed$@ — for hover-popup
|
||||||
|
-- @data-date-start@ attribute. Falls back to the frontmatter value for
|
||||||
|
-- pinned files (which is expected to already be ISO, the same convention
|
||||||
|
-- used by 'lastReviewedField' before it applied 'fmtIso').
|
||||||
|
lastReviewedIsoField :: Context String
|
||||||
|
lastReviewedIsoField = field "last-reviewed-iso" $ \item -> do
|
||||||
|
let srcPath = toFilePath (itemIdentifier item)
|
||||||
|
meta <- getMetadata (itemIdentifier item)
|
||||||
|
mIso <- unsafeCompiler $ do
|
||||||
|
ignored <- readIgnore
|
||||||
|
if srcPath `elem` ignored
|
||||||
|
then return $ lookupString "last-reviewed" meta
|
||||||
|
else listToMaybe <$> gitDates srcPath
|
||||||
|
case mIso of
|
||||||
|
Nothing -> fail "no last-reviewed ISO"
|
||||||
|
Just d -> return d
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
-- Version history
|
-- Version history
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
data VHEntry = VHEntry
|
data VHEntry = VHEntry
|
||||||
{ vhDate :: String
|
{ vhDate :: String -- human-readable, e.g. "12 April 2026"
|
||||||
|
, vhDateIso :: String -- raw ISO, e.g. "2026-04-12"
|
||||||
, vhMessage :: Maybe String -- Nothing for git-log-only entries
|
, vhMessage :: Maybe String -- Nothing for git-log-only entries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,7 +201,7 @@ parseFmHistory meta =
|
||||||
parseOne (Object o) =
|
parseOne (Object o) =
|
||||||
case getString =<< KM.lookup "date" o of
|
case getString =<< KM.lookup "date" o of
|
||||||
Nothing -> Nothing
|
Nothing -> Nothing
|
||||||
Just d -> Just $ VHEntry (fmtIso d) (getString =<< KM.lookup "note" o)
|
Just d -> Just $ VHEntry (fmtIso d) d (getString =<< KM.lookup "note" o)
|
||||||
parseOne _ = Nothing
|
parseOne _ = Nothing
|
||||||
|
|
||||||
getString (String t) = Just (T.unpack t)
|
getString (String t) = Just (T.unpack t)
|
||||||
|
|
@ -187,7 +209,7 @@ parseFmHistory meta =
|
||||||
|
|
||||||
-- | Get git log for a file as version history entries (date-only, no message).
|
-- | Get git log for a file as version history entries (date-only, no message).
|
||||||
gitLogHistory :: FilePath -> IO [VHEntry]
|
gitLogHistory :: FilePath -> IO [VHEntry]
|
||||||
gitLogHistory fp = map (\d -> VHEntry (fmtIso d) Nothing) <$> gitDates fp
|
gitLogHistory fp = map (\d -> VHEntry (fmtIso d) d Nothing) <$> gitDates fp
|
||||||
|
|
||||||
-- | Maximum entries shown by default in the version-history footer block.
|
-- | Maximum entries shown by default in the version-history footer block.
|
||||||
-- The remainder is revealed via a <details>/<summary> expand affordance,
|
-- The remainder is revealed via a <details>/<summary> expand affordance,
|
||||||
|
|
@ -213,11 +235,12 @@ vhItems tag =
|
||||||
zipWith (\i e -> Item (fromFilePath (tag ++ "-" ++ show (i :: Int))) e)
|
zipWith (\i e -> Item (fromFilePath (tag ++ "-" ++ show (i :: Int))) e)
|
||||||
[1..]
|
[1..]
|
||||||
|
|
||||||
-- | Shared sub-context for version-history entries: @$vh-date$@ and
|
-- | Shared sub-context for version-history entries: @$vh-date$@,
|
||||||
-- (optionally) @$vh-message$@.
|
-- @$vh-date-iso$@ (raw ISO for hover popups), and (optionally) @$vh-message$@.
|
||||||
vhEntryCtx :: Context VHEntry
|
vhEntryCtx :: Context VHEntry
|
||||||
vhEntryCtx =
|
vhEntryCtx =
|
||||||
field "vh-date" (return . vhDate . itemBody)
|
field "vh-date" (return . vhDate . itemBody)
|
||||||
|
<> field "vh-date-iso" (return . vhDateIso . itemBody)
|
||||||
<> field "vh-message" (\i -> case vhMessage (itemBody i) of
|
<> field "vh-message" (\i -> case vhMessage (itemBody i) of
|
||||||
Nothing -> fail "no message"
|
Nothing -> fail "no message"
|
||||||
Just m -> return m)
|
Just m -> return m)
|
||||||
|
|
@ -277,3 +300,40 @@ versionHistoryRangeField = field "version-history-range" $ \item -> do
|
||||||
in if newD == oldD
|
in if newD == oldD
|
||||||
then return newD
|
then return newD
|
||||||
else return (oldD ++ " \x2013 " ++ newD)
|
else return (oldD ++ " \x2013 " ++ newD)
|
||||||
|
|
||||||
|
-- | Raw-ISO start date (oldest entry) for hover-popup machine use.
|
||||||
|
versionHistoryRangeStartField :: Context String
|
||||||
|
versionHistoryRangeStartField =
|
||||||
|
field "version-history-range-start" $ \item -> do
|
||||||
|
entries <- loadVersionHistory item
|
||||||
|
case entries of
|
||||||
|
[] -> fail "no version-history start"
|
||||||
|
_ -> return (vhDateIso (last entries))
|
||||||
|
|
||||||
|
-- | Raw-ISO end date (newest entry) for hover-popup machine use.
|
||||||
|
-- Only resolves when the range spans more than one calendar day — single-day
|
||||||
|
-- histories don't need an end attribute on the popup trigger.
|
||||||
|
versionHistoryRangeEndField :: Context String
|
||||||
|
versionHistoryRangeEndField =
|
||||||
|
field "version-history-range-end" $ \item -> do
|
||||||
|
entries <- loadVersionHistory item
|
||||||
|
case entries of
|
||||||
|
[] -> fail "no version-history end"
|
||||||
|
[_] -> fail "single-day history — no end"
|
||||||
|
(newest : more) ->
|
||||||
|
let oldest = last (newest : more)
|
||||||
|
in if vhDateIso newest == vhDateIso oldest
|
||||||
|
then fail "single-day history — no end"
|
||||||
|
else return (vhDateIso newest)
|
||||||
|
|
||||||
|
-- | Commit count — used by the frontmatter popup to surface the *density*
|
||||||
|
-- of attention the piece has received. Deliberately only wired into the
|
||||||
|
-- metadata-strip date link, not the aftermatter list (where it would be
|
||||||
|
-- redundant next to the enumeration of entries).
|
||||||
|
versionHistoryCommitsField :: Context String
|
||||||
|
versionHistoryCommitsField =
|
||||||
|
field "version-history-commits" $ \item -> do
|
||||||
|
entries <- loadVersionHistory item
|
||||||
|
case entries of
|
||||||
|
[] -> fail "no commits"
|
||||||
|
_ -> return (show (length entries))
|
||||||
|
|
|
||||||
|
|
@ -241,3 +241,52 @@
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
DATE POPUP — compact, humanised span + "ago" framing, plus
|
||||||
|
optional cadence line for the frontmatter history link.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.link-popup:has(.popup-date) {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 260px;
|
||||||
|
padding: 0.5rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-date {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-date-primary {
|
||||||
|
color: var(--text);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-date-cadence {
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-variant-caps: all-small-caps;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle cue that the date is interactive. Dotted underline is the standard
|
||||||
|
convention for explanatory hover; kept faint so it doesn't shout. */
|
||||||
|
.date-hover {
|
||||||
|
cursor: help;
|
||||||
|
text-decoration-line: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
text-decoration-color: var(--border-muted);
|
||||||
|
text-underline-offset: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Don't decorate container elements (li, dd, time) that already fit into
|
||||||
|
a list with their own layout — the text decoration looks noisy on them. */
|
||||||
|
li.date-hover,
|
||||||
|
dd.date-hover {
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,13 @@
|
||||||
var provider = getProvider(el.getAttribute('href') || '');
|
var provider = getProvider(el.getAttribute('href') || '');
|
||||||
if (provider) bind(el, provider);
|
if (provider) bind(el, provider);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Date hover popups — any element tagged with data-date-start.
|
||||||
|
Handles the frontmatter range link, version-history list items,
|
||||||
|
last-reviewed in the epistemic block, blog post dates, etc. */
|
||||||
|
root.querySelectorAll('[data-date-start]').forEach(function (el) {
|
||||||
|
bind(el, dateContent);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public re-init hook used by transclude.js after it injects new
|
/* Public re-init hook used by transclude.js after it injects new
|
||||||
|
|
@ -772,6 +779,94 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
Date popups — explain a date or date range in human terms.
|
||||||
|
Single date: "6 months ago"
|
||||||
|
Range: "~4 weeks · started 6 months ago"
|
||||||
|
Frontmatter: range + "17 revisions" cadence line (only when
|
||||||
|
data-date-commits is present on the trigger).
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
function dateContent(target) {
|
||||||
|
var startAttr = target.getAttribute('data-date-start');
|
||||||
|
if (!startAttr) return Promise.resolve(null);
|
||||||
|
var start = parseIsoDate(startAttr);
|
||||||
|
if (!start) return Promise.resolve(null);
|
||||||
|
|
||||||
|
var endAttr = target.getAttribute('data-date-end');
|
||||||
|
var end = endAttr ? parseIsoDate(endAttr) : null;
|
||||||
|
var commits = target.getAttribute('data-date-commits');
|
||||||
|
|
||||||
|
var today = new Date();
|
||||||
|
var lines = [];
|
||||||
|
|
||||||
|
if (end) {
|
||||||
|
var spanDays = daysBetween(start, end);
|
||||||
|
var agoDays = daysBetween(start, today);
|
||||||
|
/* "~" prefix when we've rounded to a unit larger than days. */
|
||||||
|
var span = humanDuration(spanDays, true);
|
||||||
|
var ago = humanAgo(agoDays);
|
||||||
|
lines.push(
|
||||||
|
'<div class="popup-date-primary">'
|
||||||
|
+ esc(span) + ' · started ' + esc(ago)
|
||||||
|
+ '</div>');
|
||||||
|
if (commits && /^\d+$/.test(commits)) {
|
||||||
|
var n = parseInt(commits, 10);
|
||||||
|
lines.push(
|
||||||
|
'<div class="popup-date-cadence">'
|
||||||
|
+ n + ' revision' + (n === 1 ? '' : 's')
|
||||||
|
+ '</div>');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var days = daysBetween(start, today);
|
||||||
|
lines.push(
|
||||||
|
'<div class="popup-date-primary">'
|
||||||
|
+ esc(humanAgo(days)) + '</div>');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve('<div class="popup-date">' + lines.join('') + '</div>');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse "YYYY-MM-DD" (UTC midnight) to a Date. Returns null on failure. */
|
||||||
|
function parseIsoDate(s) {
|
||||||
|
var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s);
|
||||||
|
if (!m) return null;
|
||||||
|
var d = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
|
||||||
|
return isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Whole-day difference between two Dates, floored (never negative). */
|
||||||
|
function daysBetween(a, b) {
|
||||||
|
var ms = Math.abs(b.getTime() - a.getTime());
|
||||||
|
return Math.floor(ms / 86400000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "5 days" / "3 weeks" / "4 months" / "2 years" — the unit is chosen
|
||||||
|
to match the magnitude so the number stays small and readable.
|
||||||
|
`approx` prefixes "~" when the returned unit is coarser than days. */
|
||||||
|
function humanDuration(days, approx) {
|
||||||
|
if (days <= 1) return '1 day';
|
||||||
|
if (days < 14) return days + ' days';
|
||||||
|
if (days < 60) {
|
||||||
|
var w = Math.round(days / 7);
|
||||||
|
return (approx ? '~' : '') + w + ' week' + (w === 1 ? '' : 's');
|
||||||
|
}
|
||||||
|
if (days < 365) {
|
||||||
|
var mo = Math.round(days / 30);
|
||||||
|
return (approx ? '~' : '') + mo + ' month' + (mo === 1 ? '' : 's');
|
||||||
|
}
|
||||||
|
var y = Math.round(days / 365);
|
||||||
|
return (approx ? '~' : '') + y + ' year' + (y === 1 ? '' : 's');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Past-tense phrasing for a date N days in the past. */
|
||||||
|
function humanAgo(days) {
|
||||||
|
if (days <= 0) return 'today';
|
||||||
|
if (days === 1) return 'yesterday';
|
||||||
|
if (days < 14) return days + ' days ago';
|
||||||
|
return humanDuration(days, true) + ' ago';
|
||||||
|
}
|
||||||
|
|
||||||
/* Defer to the shared utility (loaded synchronously from
|
/* Defer to the shared utility (loaded synchronously from
|
||||||
templates/partials/head.html) so this file cannot drift from
|
templates/partials/head.html) so this file cannot drift from
|
||||||
annotations.js, semantic-search.js, or build/Utils.hs. */
|
annotations.js, semantic-search.js, or build/Utils.hs. */
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<main id="markdownBody" data-pagefind-body>
|
<main id="markdownBody" data-pagefind-body>
|
||||||
<h1 class="page-title">$title$</h1>
|
<h1 class="page-title">$title$</h1>
|
||||||
$if(date)$
|
$if(date)$
|
||||||
<p class="post-date"><time datetime="$date-iso$">$date$</time></p>
|
<p class="post-date"><time class="date-hover" datetime="$date-iso$" data-date-start="$date-iso$">$date$</time></p>
|
||||||
$endif$
|
$endif$
|
||||||
$body$
|
$body$
|
||||||
$if(backlinks)$
|
$if(backlinks)$
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,14 @@
|
||||||
</div>
|
</div>
|
||||||
$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">$if(version-history-range)$$version-history-range$$else$History$endif$</a>
|
$if(version-history-range)$
|
||||||
|
<a href="#version-history" class="date-hover"
|
||||||
|
data-date-start="$version-history-range-start$"
|
||||||
|
$if(version-history-range-end)$data-date-end="$version-history-range-end$"$endif$
|
||||||
|
$if(version-history-commits)$data-date-commits="$version-history-commits$"$endif$>$version-history-range$</a>
|
||||||
|
$else$
|
||||||
|
<a href="#version-history">History</a>
|
||||||
|
$endif$
|
||||||
$if(status)$<a href="#epistemic">Epistemic</a>$endif$
|
$if(status)$<a href="#epistemic">Epistemic</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$
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
<h3><a href="/colophon.html#living-documents">Epistemic</a></h3>
|
<h3><a href="/colophon.html#living-documents">Epistemic</a></h3>
|
||||||
<dl class="ep-expanded">
|
<dl class="ep-expanded">
|
||||||
<dt data-ep-term="stability">Stability</dt><dd>$stability$</dd>
|
<dt data-ep-term="stability">Stability</dt><dd>$stability$</dd>
|
||||||
$if(last-reviewed)$<dt>Last reviewed</dt><dd>$last-reviewed$</dd>$endif$
|
$if(last-reviewed)$<dt>Last reviewed</dt><dd class="date-hover"$if(last-reviewed-iso)$ data-date-start="$last-reviewed-iso$"$endif$>$last-reviewed$</dd>$endif$
|
||||||
$if(confidence-trend)$<dt data-ep-term="confidence">Confidence trend</dt><dd>$confidence-trend$</dd>$endif$
|
$if(confidence-trend)$<dt data-ep-term="confidence">Confidence trend</dt><dd>$confidence-trend$</dd>$endif$
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
$if(version-history-primary)$
|
$if(version-history-primary)$
|
||||||
<ul class="version-history-list">
|
<ul class="version-history-list">
|
||||||
$for(version-history-primary)$
|
$for(version-history-primary)$
|
||||||
<li>$vh-date$$if(vh-message)$ · $vh-message$$endif$</li>
|
<li class="date-hover" data-date-start="$vh-date-iso$">$vh-date$$if(vh-message)$ · $vh-message$$endif$</li>
|
||||||
$endfor$
|
$endfor$
|
||||||
</ul>
|
</ul>
|
||||||
$if(version-history-rest)$
|
$if(version-history-rest)$
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
<summary>More</summary>
|
<summary>More</summary>
|
||||||
<ul class="version-history-list">
|
<ul class="version-history-list">
|
||||||
$for(version-history-rest)$
|
$for(version-history-rest)$
|
||||||
<li>$vh-date$$if(vh-message)$ · $vh-message$$endif$</li>
|
<li class="date-hover" data-date-start="$vh-date-iso$">$vh-date$$if(vh-message)$ · $vh-message$$endif$</li>
|
||||||
$endfor$
|
$endfor$
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.6 KiB |
Loading…
Reference in New Issue