date data
This commit is contained in:
parent
1a532f881b
commit
237380c4be
|
|
@ -31,9 +31,11 @@ import Hakyll hiding (trim)
|
|||
import Backlinks (backlinksField)
|
||||
import Dingbat (dingbatField)
|
||||
import SimilarLinks (similarLinksField)
|
||||
import Stability (stabilityField, lastReviewedField, versionHistoryField,
|
||||
import Stability (stabilityField, lastReviewedField, lastReviewedIsoField,
|
||||
versionHistoryField,
|
||||
versionHistoryPrimaryField, versionHistoryRestField,
|
||||
versionHistoryRangeField)
|
||||
versionHistoryRangeField, versionHistoryRangeStartField,
|
||||
versionHistoryRangeEndField, versionHistoryCommitsField)
|
||||
import Utils (authorSlugify, authorNameOf, trim)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
|
@ -377,6 +379,7 @@ epistemicCtx =
|
|||
<> confidenceTrendField
|
||||
<> stabilityField
|
||||
<> lastReviewedField
|
||||
<> lastReviewedIsoField
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Essay context
|
||||
|
|
@ -398,6 +401,9 @@ essayCtx =
|
|||
<> versionHistoryPrimaryField
|
||||
<> versionHistoryRestField
|
||||
<> versionHistoryRangeField
|
||||
<> versionHistoryRangeStartField
|
||||
<> versionHistoryRangeEndField
|
||||
<> versionHistoryCommitsField
|
||||
<> dateField "date-created" "%-d %B %Y"
|
||||
<> dateField "date-modified" "%-d %B %Y"
|
||||
<> constField "math" "true"
|
||||
|
|
|
|||
|
|
@ -18,10 +18,14 @@
|
|||
module Stability
|
||||
( stabilityField
|
||||
, lastReviewedField
|
||||
, lastReviewedIsoField
|
||||
, versionHistoryField
|
||||
, versionHistoryPrimaryField
|
||||
, versionHistoryRestField
|
||||
, versionHistoryRangeField
|
||||
, versionHistoryRangeStartField
|
||||
, versionHistoryRangeEndField
|
||||
, versionHistoryCommitsField
|
||||
) where
|
||||
|
||||
import Control.Exception (catch, IOException)
|
||||
|
|
@ -159,12 +163,30 @@ lastReviewedField = field "last-reviewed" $ \item -> do
|
|||
Nothing -> fail "no last-reviewed"
|
||||
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
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +201,7 @@ parseFmHistory meta =
|
|||
parseOne (Object o) =
|
||||
case getString =<< KM.lookup "date" o of
|
||||
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
|
||||
|
||||
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).
|
||||
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.
|
||||
-- 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)
|
||||
[1..]
|
||||
|
||||
-- | Shared sub-context for version-history entries: @$vh-date$@ and
|
||||
-- (optionally) @$vh-message$@.
|
||||
-- | Shared sub-context for version-history entries: @$vh-date$@,
|
||||
-- @$vh-date-iso$@ (raw ISO for hover popups), and (optionally) @$vh-message$@.
|
||||
vhEntryCtx :: Context VHEntry
|
||||
vhEntryCtx =
|
||||
field "vh-date" (return . vhDate . itemBody)
|
||||
<> field "vh-date-iso" (return . vhDateIso . itemBody)
|
||||
<> field "vh-message" (\i -> case vhMessage (itemBody i) of
|
||||
Nothing -> fail "no message"
|
||||
Just m -> return m)
|
||||
|
|
@ -277,3 +300,40 @@ versionHistoryRangeField = field "version-history-range" $ \item -> do
|
|||
in if newD == oldD
|
||||
then return 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;
|
||||
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') || '');
|
||||
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
|
||||
|
|
@ -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
|
||||
templates/partials/head.html) so this file cannot drift from
|
||||
annotations.js, semantic-search.js, or build/Utils.hs. */
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<main id="markdownBody" data-pagefind-body>
|
||||
<h1 class="page-title">$title$</h1>
|
||||
$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$
|
||||
$body$
|
||||
$if(backlinks)$
|
||||
|
|
|
|||
|
|
@ -30,7 +30,14 @@
|
|||
</div>
|
||||
$endif$
|
||||
<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(bibliography)$<a href="#bibliography">Bibliography</a>$endif$
|
||||
$if(backlinks)$<a href="#backlinks">Backlinks</a>$endif$
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
<h3><a href="/colophon.html#living-documents">Epistemic</a></h3>
|
||||
<dl class="ep-expanded">
|
||||
<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$
|
||||
</dl>
|
||||
</div>
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
$if(version-history-primary)$
|
||||
<ul class="version-history-list">
|
||||
$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$
|
||||
</ul>
|
||||
$if(version-history-rest)$
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
<summary>More</summary>
|
||||
<ul class="version-history-list">
|
||||
$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$
|
||||
</ul>
|
||||
</details>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.6 KiB |
Loading…
Reference in New Issue