cite-wide

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:

MARKDOWN
[^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:

MARKDOWN
[^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:

InputBeforeAfter
"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:

TS
/([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:

TS
/(?<=\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.md says date. Author. [Title](url). Publisher. The code emits date. "[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

FileWhat
src/utils/citationDate.tsNew. formatCitationDate() — the one date formatter
src/utils/citationSpacing.tsNew. assureSpaceBeforeInlineCitations() — the one spacing rule
src/services/urlCitationService.tsCalls the date helper; inline toLocaleDateString removed
src/services/citationService.tsassureSpacingBetweenCitations delegates; allowlist and ][ rule removed
src/services/llmCitationParserService.tsnormalizeInlineCitationSpacing delegates; loop removed
context-v/issues/Paste-LLM-Content-Drops-Structure-And-Emits-Link-Only-Citations.mdNew. Three defects, live artifact, proposed pipeline
context-v/issues/User-Definable-Citation-Format-Templates.mdNew. 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