Two bugs, one lesson: the citation format lived in four places at once
Dates came out backwards and citations glued themselves to quotation marks. Both bugs traced to the same thing — we'd written the format down in the spec, then written it down again in the code, four separate times.
Why Care?
If you cite things in Obsidian, you have probably noticed that a footnote marker glued to the preceding character stops behaving like a footnote. No hover preview. No click-to-jump. It renders as literal text and quietly stops being a citation at all.
Cite Wide shipped two commands that produced exactly that, and a third that formatted every date backwards. Both got fixed today. But the interesting part isn't the fixes — it's that they turned out to be the same bug wearing two costumes.
We have a written citation spec. We also had the citation format written into the code, in four separate places, each slightly out of step with the spec and with each other. That is the actual defect. The dates and the spacing were just where it surfaced first.
What Was Built / Updated
Dates now read the way the spec always said they should
Extract citation from URL was formatting publication dates month-first and
dropping the day entirely:
[^01f3ut]: Apr 2022. "[GMV Retention | Andreessen Horowitz](https://a16z.com/…)". Olivia Moore. The culprit was a one-liner — toLocaleDateString('en-US', { year: 'numeric', month: 'short' })
— which does precisely what it says and nothing we wanted. It now reads:
[^01f3ut]: 2022, Apr 28. "[GMV Retention | Andreessen Horowitz](https://a16z.com/…)". Olivia Moore. Year-major, comma after the year, zero-padded day. Which is what
Lossless-Citation-Spec.md had specified since 2024 — the code had simply
drifted from a document nobody re-read. Closes
#33.
Citations stop gluing themselves to quotation marks
Assure Spacing for Anchor Link behavior was supposed to guarantee one
space before every inline citation. It worked on plain words and on seven
punctuation marks, and silently gave up on everything else:
| Input | Before | After |
"the metric."[^abc] | unchanged ❌ | "the metric." [^abc] |
“the metric.”[^abc] | unchanged ❌ | “the metric.” [^abc] |
(RFC 7950)[^abc] | unchanged ❌ | (RFC 7950) [^abc] |
a hard problem—[^abc] | unchanged ❌ | a hard problem— [^abc] |
grew 40%[^abc] | unchanged ❌ | grew 40% [^abc] |
**bolded**[^abc] | unchanged ❌ | **bolded** [^abc] |
the café[^abc] | unchanged ❌ | the café [^abc] |
That last row is the one that gives the game away. The rule was written as an allowlist of characters permitted to precede a citation:
/([A-Za-z0-9.,:;!?])\s*(\[\^[^\]]+\])/g é is not in A-Za-z, so every accented word in every language kept its
citation glued. An allowlist of "characters that count as content" can only
ever be incomplete — the set is all of Unicode minus whitespace. Eight of
sixteen representative cases failed. Closes
#23, which had been
closed once already while still broken.
A markdown-breaking bug we found on the way
The same function opened with replace(/\](\s*)\[/g, '] [') — inserting a
space at any ][ boundary. That turns the markdown reference link
[text][ref] into [text] [ref], which is no longer a link. Anyone using
reference-style links in a document they ran this command on had them
quietly broken.
Gone. Citation-to-citation adjacency ([^a][^b]) still works, because ]
is non-whitespace and the new rule looks specifically for a [^ marker.
Two issues filed for what string manipulation cannot fix
Pasting a Google AI Overview through Paste LLM Content surfaced two
problems no regex will solve, so they are written up rather than patched:
[Paste LLM Content drops structure and emits link-only citations] — Google hands you bare URLs, so every reference definition becomes
[^hex]: [https://url](https://url), a link whose visible text is its own href. There is no title, author, date, or publisher in the pasted text to recover. Fixing it means going to the page and fetching them. Also catalogued: headings and bullets arriving as flat prose, and the same URL cited twice getting two different hex codes.[User-definable citation format templates] — our house style is one person's style, hardcoded into a plugin published to a marketplace where most users want APA or Chicago or their own. Templating is the feature; centralizing the format is the prerequisite.
What Changed in Approach
Both fixes were the same move: delete the second copy.
The date format lived in four places — urlCitationService.formatCitation(),
citationDate.ts, the LLM parser's bare-URL fallback, and
citationFileService's frontmatter writer. The spacing rule lived in two:
Assure Spacing command ──► citationService.assureSpacingBetweenCitations()
└─ allowlist regex ❌ broken
Paste LLM Content ───────► llmCitationParser.normalizeInlineCitationSpacing()
└─ (\S) regex ✅ correct Two implementations of one rule, and the user-facing command happened to call the wrong one. Nobody chose that. It is just what happens when the same knowledge is written down twice and only one copy gets maintained.
So the spacing rule is now stated once, in utils/citationSpacing.ts, and
both services call it. Same for dates in utils/citationDate.ts.
The rewritten rule is also stated the way we'd say it out loud — one space between any non-whitespace character and an inline citation — as a zero-width insertion rather than a character class:
/(?<=\S)(?=\[\^[a-z0-9]+\](?!:))/gi Nothing is consumed, so a chain like text[^a][^b][^c] resolves in a single
pass. The old parser version needed an iterate-until-stable loop with a
safety counter to do the same job.
Two behaviors in the date formatter are worth knowing because they look like bugs until you know why:
Precision is preserved, not invented. A source that published 2025-04
renders 2025, Apr — we don't fabricate a day the publisher never stated.
UTC getters throughout. A date-only string like 2025-04-06 parses as
UTC midnight, so local-time getters render it as Apr 05 anywhere west of
Greenwich. That off-by-one was latent behind the old format — which never
showed a day, so it never showed the error — and would have surfaced the
moment days started rendering.
Open Items
Fenced code blocks are still not tracked. A literal
foo[^bar]inside a code fence gets spaced like prose. Pre-existing in both implementations; scoped out rather than folded into a spacing fix.The spec and the implementation disagree on field order.
Lossless-Citation-Spec.mdsaysdate. Author. [Title](url). Publisher.The code emitsdate. "[Title | Site](url)". Author. [Site](url).The code matches current practice; the spec does not. One of the two should move.The date format is now centralized but still hardcoded — which is exactly the state the templating issue exists to fix.
Files Touched
| File | What |
src/utils/citationDate.ts | New. formatCitationDate() — the one date formatter |
src/utils/citationSpacing.ts | New. assureSpaceBeforeInlineCitations() — the one spacing rule |
src/services/urlCitationService.ts | Calls the date helper; inline toLocaleDateString removed |
src/services/citationService.ts | assureSpacingBetweenCitations delegates; allowlist and ][ rule removed |
src/services/llmCitationParserService.ts | normalizeInlineCitationSpacing delegates; loop removed |
context-v/issues/Paste-LLM-Content-Drops-Structure-And-Emits-Link-Only-Citations.md | New. Three defects, live artifact, proposed pipeline |
context-v/issues/User-Definable-Citation-Format-Templates.md | New. Design issue — segment templates, CSL question, round-trip constraint |
Verification: 20 spacing cases asserted correct and idempotent, covering
quotes, parens, dashes, %, bold, non-ASCII, chains, reference definitions
at line start and indented, markdown reference links, and wikilinks. Date
formatting checked across ISO, date-only, year-month, year-only, RFC 1123,
and unparseable input. pnpm build clean — tsc, eslint, esbuild.
Reference
[[Lossless-Citation-Spec]] — the format both fixes were converging on
[[Paste-LLM-Content-Drops-Structure-And-Emits-Link-Only-Citations]]
[[User-Definable-Citation-Format-Templates]]