feat(editor): add revision/status/tracking_number FM hints + filename-mismatch warning

Per review: the doctype templates render $revision$, $status$, $tracking_number$
and $title$, so they belong in the recognised front-matter list — added them
(alongside the existing title) to convert.RecognizedFrontMatter.

These four are the document's canonical identity, sourced from the ZDDC
filename. Policy (chosen): the filename WINS — the rendered doc always uses the
filename-derived value (the HTML/PDF templates read it from the filename-derived
pandoc -V flags, which override YAML metadata). Front matter must not silently
diverge, so:
  - their hints now read "set by the filename (the filename wins on mismatch)";
  - the markdown editor shows a non-blocking warning when front matter sets one
    of the four to a value differing from the filename (gated on a conventional
    ZDDC filename — non-conventional files have no canonical identity, so front
    matter stays free there).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-06-05 08:34:28 -05:00
parent 85e0061d6c
commit 05e37256b7
3 changed files with 53 additions and 3 deletions

View file

@ -411,7 +411,18 @@
fmTextarea.placeholder = '';
applyFrontMatterPlaceholder(fmTextarea);
fmBody.appendChild(fmTextarea);
// Non-blocking warning shown when front matter disagrees with the
// canonical filename on an identity field (tracking_number / revision /
// status / title). The filename always wins in the rendered doc; this
// just tells the author their front-matter value is being ignored.
var fmWarn = document.createElement('div');
fmWarn.className = 'md-fm__warn';
fmWarn.hidden = true;
fmWarn.style.cssText = 'color:#92400e;background:#fffbeb;border:1px solid '
+ '#fcd34d;border-radius:4px;padding:4px 8px;margin:0 0 4px;font-size:'
+ '0.78rem;line-height:1.4;';
fmSection.appendChild(fmHeader);
fmSection.appendChild(fmWarn);
fmSection.appendChild(fmBody);
sidebar.appendChild(fmSection);
@ -736,14 +747,49 @@
}, 250);
editor.on('change', onChange);
// Identity fields are sourced from the canonical ZDDC filename; setting
// a different value in front matter is ignored at render (the filename
// wins). Surface a mismatch so the author isn't silently overridden.
// Maps the front-matter key to the parseFilename field.
var IDENTITY_FIELDS = [
{ fm: 'title', fn: 'title', label: 'title' },
{ fm: 'tracking_number', fn: 'trackingNumber', label: 'tracking number' },
{ fm: 'revision', fn: 'revision', label: 'revision' },
{ fm: 'status', fn: 'status', label: 'status' }
];
function checkFilenameMismatch() {
var z = window.zddc;
var fn = (z && z.parseFilename) ? z.parseFilename(node.name) : null;
// Only meaningful for a conventional ZDDC filename (it always has a
// tracking number). Non-conventional files have no canonical
// identity, so front matter is free — no warning.
if (!fn || !fn.trackingNumber) { fmWarn.hidden = true; return; }
var data = parseFrontMatter('---\n' + fmTextarea.value + '\n---\n').data || {};
var clashes = [];
IDENTITY_FIELDS.forEach(function (f) {
if (!(f.fm in data)) return;
var got = String(data[f.fm] == null ? '' : data[f.fm]).trim();
var want = String(fn[f.fn] == null ? '' : fn[f.fn]).trim();
if (got !== '' && want !== '' && got !== want) {
clashes.push(f.label + ' “' + got + '” ≠ filename “' + want + '”');
}
});
if (!clashes.length) { fmWarn.hidden = true; fmWarn.textContent = ''; return; }
fmWarn.textContent = '⚠ Front matter disagrees with the filename (the '
+ 'filename wins): ' + clashes.join('; ') + '.';
fmWarn.hidden = false;
}
var onFmChange = debounce(async function () {
if (currentInstance !== instance) return;
var body = editor.getMarkdown();
var h = await hashContent(assembleContent(fmTextarea.value, body));
if (currentInstance !== instance) return;
markDirty(h !== instance.hash);
checkFilenameMismatch();
}, 250);
fmTextarea.addEventListener('input', onFmChange);
checkFilenameMismatch(); // initial state on load
// ── Save ───────────────────────────────────────────────────────────
// Mark a successful write: adopt the new server ETag (so the next

View file

@ -78,7 +78,10 @@ func RecognizedFrontMatter() []FrontMatterField {
return []FrontMatterField{
{"doctype", "report | letter | specification"},
{"numbering", "true to number headings (default false)"},
{"title", "overrides the filename-derived title"},
{"title", "set by the filename (the filename wins on mismatch)"},
{"tracking_number", "set by the filename (the filename wins on mismatch)"},
{"revision", "set by the filename (the filename wins on mismatch)"},
{"status", "set by the filename (the filename wins on mismatch)"},
{"date", "document date (free text)"},
{"custom_header", "extra line shown in the document header"},
{"client", "overrides the .zddc convert: cascade"},

View file

@ -90,8 +90,9 @@ func TestServeFrontMatterTemplate(t *testing.T) {
if len(payload.Fields) == 0 {
t.Fatal("fields empty")
}
// The two keys with no other source are the ones authors most need hinted.
for _, want := range []string{"doctype", "numbering"} {
// doctype/numbering have no other source; revision/status are template
// fields an author can override here — all must be communicated.
for _, want := range []string{"doctype", "numbering", "revision", "status", "tracking_number"} {
if !strings.Contains(payload.Placeholder, want) {
t.Errorf("placeholder missing %q: %q", want, payload.Placeholder)
}