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:
parent
85e0061d6c
commit
05e37256b7
3 changed files with 53 additions and 3 deletions
|
|
@ -411,7 +411,18 @@
|
||||||
fmTextarea.placeholder = '';
|
fmTextarea.placeholder = '';
|
||||||
applyFrontMatterPlaceholder(fmTextarea);
|
applyFrontMatterPlaceholder(fmTextarea);
|
||||||
fmBody.appendChild(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(fmHeader);
|
||||||
|
fmSection.appendChild(fmWarn);
|
||||||
fmSection.appendChild(fmBody);
|
fmSection.appendChild(fmBody);
|
||||||
sidebar.appendChild(fmSection);
|
sidebar.appendChild(fmSection);
|
||||||
|
|
||||||
|
|
@ -736,14 +747,49 @@
|
||||||
}, 250);
|
}, 250);
|
||||||
editor.on('change', onChange);
|
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 () {
|
var onFmChange = debounce(async function () {
|
||||||
if (currentInstance !== instance) return;
|
if (currentInstance !== instance) return;
|
||||||
var body = editor.getMarkdown();
|
var body = editor.getMarkdown();
|
||||||
var h = await hashContent(assembleContent(fmTextarea.value, body));
|
var h = await hashContent(assembleContent(fmTextarea.value, body));
|
||||||
if (currentInstance !== instance) return;
|
if (currentInstance !== instance) return;
|
||||||
markDirty(h !== instance.hash);
|
markDirty(h !== instance.hash);
|
||||||
|
checkFilenameMismatch();
|
||||||
}, 250);
|
}, 250);
|
||||||
fmTextarea.addEventListener('input', onFmChange);
|
fmTextarea.addEventListener('input', onFmChange);
|
||||||
|
checkFilenameMismatch(); // initial state on load
|
||||||
|
|
||||||
// ── Save ───────────────────────────────────────────────────────────
|
// ── Save ───────────────────────────────────────────────────────────
|
||||||
// Mark a successful write: adopt the new server ETag (so the next
|
// Mark a successful write: adopt the new server ETag (so the next
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,10 @@ func RecognizedFrontMatter() []FrontMatterField {
|
||||||
return []FrontMatterField{
|
return []FrontMatterField{
|
||||||
{"doctype", "report | letter | specification"},
|
{"doctype", "report | letter | specification"},
|
||||||
{"numbering", "true to number headings (default false)"},
|
{"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)"},
|
{"date", "document date (free text)"},
|
||||||
{"custom_header", "extra line shown in the document header"},
|
{"custom_header", "extra line shown in the document header"},
|
||||||
{"client", "overrides the .zddc convert: cascade"},
|
{"client", "overrides the .zddc convert: cascade"},
|
||||||
|
|
|
||||||
|
|
@ -90,8 +90,9 @@ func TestServeFrontMatterTemplate(t *testing.T) {
|
||||||
if len(payload.Fields) == 0 {
|
if len(payload.Fields) == 0 {
|
||||||
t.Fatal("fields empty")
|
t.Fatal("fields empty")
|
||||||
}
|
}
|
||||||
// The two keys with no other source are the ones authors most need hinted.
|
// doctype/numbering have no other source; revision/status are template
|
||||||
for _, want := range []string{"doctype", "numbering"} {
|
// 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) {
|
if !strings.Contains(payload.Placeholder, want) {
|
||||||
t.Errorf("placeholder missing %q: %q", want, payload.Placeholder)
|
t.Errorf("placeholder missing %q: %q", want, payload.Placeholder)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue