Skip to content

Add in-text and margin citations - #256

Merged
dknauss merged 6 commits into
mainfrom
feat/citations
Nov 6, 2026
Merged

dknauss merged 6 commits into
mainfrom
feat/citations

Conversation

@dknauss

@dknauss dknauss commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

This adds a Cite control to the rich-text toolbar. It links a short citation in any paragraph to an entry in a Borges bibliography on the same post. It works in running text and in a theme's margin-note paragraph style, such as Marginalia's Gloss, with no new block. This is the "margin citations" item from SPEC.md (#255).

  • Picking an entry: the popover (toolbar → More → Cite, reachable by keyboard) lists every entry of every bibliography in the post, including nested blocks. It's grouped by bibliography when there is more than one, searchable by author, year, title or number, and has an optional page or other locator field with a live preview.
  • Short form: it follows the bibliography's style family.
    • Chicago notes and OSCOLA: a Chicago short note ("Illich, Tools for Conviviality, 12"; quotation marks for articles and chapters).
    • Author-date: "(Illich 1973, 12)", and "(Illich 12)" in MLA.
    • Numeric: "[2]".
    • It's plain text you can edit after inserting it.
  • Selected text and editing: selected text can be kept as the link text or replaced with the short form. With the cursor in an existing citation, Cite reopens on its entry, which you can update or remove.
  • Markup: <a class="bibliography-builder-citation h-cite" href="#ref-{id}" data-bibliography-ref="{id}">. It works with no JavaScript, and it survives wp_kses_post for authors without unfiltered_html (checked).
  • No deprecation: the bibliography block's saved markup is unchanged.
  • ID stability: citation IDs survive re-sorting, style changes, field edits and de-duplication. A link breaks if its entry is deleted (even if pasted again), or if its block is duplicated, since the copy gets new IDs. SPEC.md § "In-Text and Margin Citations" documents this.
  • Package: adds @wordpress/rich-text to devDependencies (already in the lockfile at 7.48.1). Lint needs it as a declared dependency.

How it was checked

  • 37 new Jest tests: short forms for each family, author counts, missing fields, locators, entry collection and search, and the format itself (apply, wrap, replace, update, remove, saved-markup round-trip). The full suite passes (1,293 tests). build, ESLint, Stylelint, typecheck:js, lint:i18n and Prettier also pass.
  • In WordPress Playground (latest WordPress, PHP 8.3) with Playwright:
    • I cited selected text and a margin note in a floated "Gloss"-style paragraph, then saved and reloaded: no block warnings and no console errors.
    • On the front end, the link jumps to its entry, which gets a dotted :target outline. It also works with JavaScript off.
  • Not run: PHP tests (no PHP changed), browser tests of the numeric and author-date styles (Jest covers them), and the real Marginalia theme.
  • npm ci needs --legacy-peer-deps with npm 10.9.9 because of an eslint peer-range conflict that is already on main, not caused by this branch.

Follow-ups

  • Back-links from entries to their citations (render time).
  • Selecting a citation in the editor scrolls to its entry.
  • Flagging citations made stale by an entry edit (style changes are handled below).
  • Year suffixes and per-style locator labels.

Updating citations after a style change

Each citation now saves its page, chapter, or section as data-locator. wp_kses_post keeps it, which was checked in Playground as an Author. The Cite popover's field is now labelled "Page, chapter or section (optional)", and it fills in again when reopened on a citation.

The bibliography's sidebar has a "Citations in this post" panel. It counts the citations that point at the bibliography and offers Update citations. Changing the style offers the same action in a notice.

The update rewrites every citation the editor generated, in any block, nested ones and table cells included. It uses the current style and numbering, keeps each citation's locator, and makes all the changes as one undoable edit.

  • Reworded citations are left alone unless "Also replace citations I reworded" is ticked.
  • Citations made before data-locator existed have their locator read back from text that matches a short form, and then saved.
  • Citations whose entry is gone are left alone and counted.

This also fixes Codex's finding: two citations side by side ("[1][2]") are now two ranges, so updating or removing one no longer changes the other.

https://claude.ai/code/session_01WPtgA1cnwP9mYKwaWniaFS


Generated by Claude Code

dknauss and others added 2 commits November 6, 2026 03:31
A "Cite" rich-text format (bibliography-builder/citation) links a short
citation in any paragraph, including a theme's margin-note style, to an
entry in a Borges bibliography on the same post. The popover lists every
entry of every bibliography block in the post, searchable, with an
optional locator. The short form follows the style family: a Chicago
short note, (Family Year, locator), or [n].

The saved link is a plain fragment link to save()'s <li id="ref-{id}">,
with the h-cite class and data-bibliography-ref, which survive kses for
every role. The bibliography block's save() is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPtgA1cnwP9mYKwaWniaFS
verify-metrics.sh checks docs/current-metrics.md against the tree; the new
format and its helpers add 1,084 lines of source (13608 -> 14692).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPtgA1cnwP9mYKwaWniaFS
@codecov

codecov Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.53110% with 119 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.64%. Comparing base (887ccfa) to head (855980c).

Files with missing lines Patch % Lines
src/formats/citation.js 60.00% 17 Missing and 21 partials ⚠️
src/lib/citation-update.js 78.06% 5 Missing and 29 partials ⚠️
src/lib/citation-short-form.js 65.51% 1 Missing and 29 partials ⚠️
src/lib/citation-entries.js 84.61% 0 Missing and 6 partials ⚠️
src/components/citation-update-panel.js 64.28% 4 Missing and 1 partial ⚠️
src/hooks/use-citation-updates.js 83.33% 0 Missing and 3 partials ⚠️
src/edit.js 77.77% 2 Missing ⚠️
src/index.js 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main     #256      +/-   ##
============================================
- Coverage     89.32%   88.64%   -0.68%     
  Complexity       67       67              
============================================
  Files            96      102       +6     
  Lines         10550    10968     +418     
  Branches        846      962     +116     
============================================
+ Hits           9424     9723     +299     
- Misses          417      447      +30     
- Partials        709      798      +89     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

dknauss commented Nov 6, 2026

Copy link
Copy Markdown
Owner Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T15:27:14.160822Z 147a2e8 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e35816f769

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/formats/citation.js Outdated
Comment on lines +94 to +98
const has = ( index ) =>
Array.isArray( formats[ index ] ) &&
formats[ index ].some(
( format ) => format.type === CITATION_FORMAT_NAME
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop citation ranges at a change of reference

When two different citations are adjacent, such as [1][2], this predicate treats them as one citation because it checks only the format type, not the reference attributes. A caret inside [1] consequently produces a range covering both links: updating with the default replacement deletes [2], updating without replacement retargets both citations, and removing unlinks both. Compare each neighboring citation's reference with the active citation before expanding the range.

Useful? React with 👍 / 👎.

Save each citation's locator as data-locator, prefill it when the Cite
popover reopens, and add an "Update citations" action: a "Citations in
this post" sidebar panel on the bibliography block, and a notice after a
style change. The update rewrites the citations the editor generated
(in any block, nested ones included) in the current style and
numbering, keeps locators, recovers them from the text of older
citations, leaves reworded ones alone unless asked, and counts
citations whose entry is gone. It is one undoable edit.

Also fix getCitationRange() treating adjacent citations to different
entries ("[1][2]") as one: the range now grows only over characters
citing the same ref, using the active citation's ref at a boundary.

Rename the locator field to "Page, chapter or section (optional)" with
help text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPtgA1cnwP9mYKwaWniaFS

dknauss commented Nov 6, 2026

Copy link
Copy Markdown
Owner Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9582206eaa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/formats/citation.js
Comment on lines +211 to +214
if ( hasText && ! replaceText ) {
return applyFormat(
value,
getCitationFormat( entry.id, locator ),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove existing link formats before applying a citation

When the selected text already contains a normal hyperlink, inserting a citation with the default “keep text” behavior adds the citation format without removing core/link. Both formats serialize as <a> elements, producing invalid nested links rather than replacing the existing destination. On HTML reload, the browser repairs those nested anchors, which can leave the visible citation text linked to the original URL instead of its bibliography entry. Remove the existing link format over the citation range before applying the citation format.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d271328: applyCitation() now removes core/link over the cited range before applying the citation format, so the text carries one link, the citation. A test covers citing part of an existing link: the cited part becomes the citation, and the rest keeps its URL as a separate, non-nested link.


Generated by Claude Code

Applying a citation to text that already carried core/link nested one <a>
inside another, which browsers split apart on reload, leaving the text linked
to the old URL. Remove core/link over the cited range first. Found by Codex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPtgA1cnwP9mYKwaWniaFS

dknauss commented Nov 6, 2026

Copy link
Copy Markdown
Owner Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d271328ddb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/formats/citation.js Outdated
return start === end ? null : { start, end };
}

const isSame = ( index ) => refAt( index ) === anchorRef;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop citation ranges when the locator changes

When adjacent citations refer to the same entry but different pages, such as [1, 12][1, 20], comparing only ref still merges them into one editable range. A caret inside the first citation therefore causes the default Update action to replace both citations with one, losing the second page reference; Remove also unlinks both. The different-reference case from the earlier review is fixed, but a direct check with identical references and different locator attributes still returns the combined range. Include the locator in the boundary comparison so these distinct citations remain independently editable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 147a2e8: the range now grows only over characters with the same ref and the same locator, so [1, 12][1, 20] are two citations. A test checks both ranges and that removing the second leaves the first linked.


Generated by Claude Code

Adjacent citations of one entry at different pages ("[1, 12][1, 20]")
merged into one range, so updating or removing one changed both. A citation
range now grows only over characters with the same ref and locator. Found by
Codex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPtgA1cnwP9mYKwaWniaFS

dknauss commented Nov 6, 2026

Copy link
Copy Markdown
Owner Author

@codex review


Generated by Claude Code

dknauss commented Nov 6, 2026

Copy link
Copy Markdown
Owner Author

Package release artifact failed at d271328 on infrastructure, not code: Composer got HTTP 504 from api.github.com downloading citeproc-php (the same outage hitting #258). CI for the new head, 147a2e8, is running; if packaging fails there on 504s again, I will re-run it once GitHub recovers.


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 147a2e8afe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Conflicts:
#	CHANGELOG.md
#	SPEC.md
#	docs/current-metrics.md
@dknauss
dknauss merged commit c6fe690 into main Nov 6, 2026
22 checks passed
@dknauss
dknauss deleted the feat/citations branch November 6, 2026 16:19
@dknauss dknauss mentioned this pull request Nov 6, 2026
dknauss added a commit that referenced this pull request Nov 7, 2026
In-text and margin citations with Update citations after a style change
(#256); annotated bibliographies (#258); h-cite microformats (#257); full
citation records in Yoast SEO's graph and Yoast context fix (#252, #254);
RIS journal abbreviations; sentence case in Harvard; the Playground demo
example of citations and annotations (#259).


Claude-Session: https://claude.ai/code/session_01WPtgA1cnwP9mYKwaWniaFS

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants