Phase 05 M2: block-range locator and opt-in citation write routes - #104
Merged
Merged
Conversation
includes/block-locator.php finds each bibliography block's exact byte range in post_content with WP_Block_Parser's delimiter grammar (same document-order indexing as the read routes), refuses malformed structure, and splices one block in place, re-locating afterwards to prove no other byte moved. Tested against a vendored copy of core's real parser (tests/phpunit/wp-block-parser/). includes/write-routes.php adds, when a site opts in with the bibliography_builder_enable_write_routes filter: POST …/bibliographies/<ref>/citations add (skips duplicates) PATCH …/bibliographies/<ref>/citations/<id> change fields DELETE …/bibliographies/<ref>/citations/<id> remove, returns entry PUT …/bibliographies/<ref>/citations/order reorder (numeric) All need edit_post, are dry runs unless dry_run=false, and need If-Match with the post's content-hash ETag (428 missing, 412 stale). A write rebuilds the markup with the save() port, splices only that block, and saves through wp_update_post() as a normal revision. The read routes send the ETag while writes are enabled. Docs: docs/rest-write-routes.md; design memo, CLAUDE.md, README, CHANGELOG, metrics updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbVuMPN7YTcV27vwsG3LZ2
dknauss
marked this pull request as ready for review
September 26, 2026 17:21
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #104 +/- ##
==========================================
+ Coverage 86.75% 87.20% +0.45%
==========================================
Files 52 54 +2
Lines 5406 5847 +441
Branches 597 597
==========================================
+ Hits 4690 5099 +409
- Misses 235 267 +32
Partials 481 481 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
6 of 9 tasks
dknauss
pushed a commit
that referenced
this pull request
Sep 26, 2026
Full GPL CSL styles and locales (#103), Phase 05 M0-M3 (stable IDs, review routes and abilities, opt-in citation and block write routes: #104, #106), the save-markup language fix, reference-manager .bib fixes, the Playground demo page (#102), and the runtime matrix on the release package with formatter-parity and write-route checks (#105). Version bumped in the plugin header, block.json, readme.txt Stable tag, the POT header, and the package manifests. CHANGELOG [Unreleased] is dated as 1.7.0; readme.txt gains a user-facing changelog entry, an Upgrade Notice, and an FAQ note on the opt-in write routes. README highlights and STATE/ROADMAP move to the 1.7.0 baseline. Tested up to stays 7.1: WordPress latest is 7.1.2, which CI's runtime matrix exercises, including formatter parity across all nine styles. package-release.sh now also prunes .git directories from vendor/. When Composer falls back from dist to a git clone (as it did in this sandbox), the ZIP otherwise carried 4 MB of git metadata. Release checklist gates run locally: lint:js, lint:css, lint:i18n, npm audit (0), composer audit (none), Jest 873 passed / 2 skipped, PHPUnit 318 OK (the 4 vendor deprecations already on main), build, package:release (552,718-byte ZIP). citeproc-php v2.7.1 is current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbVuMPN7YTcV27vwsG3LZ2
dknauss
added a commit
that referenced
this pull request
Sep 26, 2026
* chore(release): 1.7.0 Full GPL CSL styles and locales (#103), Phase 05 M0-M3 (stable IDs, review routes and abilities, opt-in citation and block write routes: #104, #106), the save-markup language fix, reference-manager .bib fixes, the Playground demo page (#102), and the runtime matrix on the release package with formatter-parity and write-route checks (#105). Version bumped in the plugin header, block.json, readme.txt Stable tag, the POT header, and the package manifests. CHANGELOG [Unreleased] is dated as 1.7.0; readme.txt gains a user-facing changelog entry, an Upgrade Notice, and an FAQ note on the opt-in write routes. README highlights and STATE/ROADMAP move to the 1.7.0 baseline. Tested up to stays 7.1: WordPress latest is 7.1.2, which CI's runtime matrix exercises, including formatter parity across all nine styles. package-release.sh now also prunes .git directories from vendor/. When Composer falls back from dist to a git clone (as it did in this sandbox), the ZIP otherwise carried 4 MB of git metadata. Release checklist gates run locally: lint:js, lint:css, lint:i18n, npm audit (0), composer audit (none), Jest 873 passed / 2 skipped, PHPUnit 318 OK (the 4 vendor deprecations already on main), build, package:release (552,718-byte ZIP). citeproc-php v2.7.1 is current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbVuMPN7YTcV27vwsG3LZ2 * Parse each CSL style once per format request CiteProc::render() calls init() every time, which builds a new Context, reads and parses the locale file, and rebuilds the style's object tree: about 3 ms, against well under 1 ms to render an entry. With one render per entry (the fix that stopped an "et al." entry from dropping the next entry's "and"), 50 entries took about 90 ms, against about 8 ms in 1.6.0 with its short stand-in styles. Bibliography_Builder_Reusable_CiteProc parses once, records every property of every object in the parsed style tree, and before each later render restores them, resets the Context's per-render lists, and zeroes Layout's static cited-item counter. Each entry renders against the state a fresh parse would give it: 50 entries now take about 25 ms. The nine style goldens still match byte for byte. A new test formats the corpus in reverse order, putting the "et al." and APA 21+ entries ahead of the short author lists, and requires every entry to match its golden line; with the restore disabled, both the goldens and the new test fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbVuMPN7YTcV27vwsG3LZ2 * Drop the #[\Override] attribute from the reusable formatter PHP 7.4 reads an attribute on its own line as a comment (php -l on 7.4 passes, and so does the PHP 7.4 runtime smoke), but Copilot flagged it as a 7.4 parse error, and compatibility scanners can do the same. It does nothing at runtime, so drop it, and turn off Psalm's MissingOverrideAttribute, which cannot apply to a plugin that supports PHP 7.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbVuMPN7YTcV27vwsG3LZ2 --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
What changed?
Phase 05, M2: the first REST routes that write bibliographies, as planned in the design memo (Tier 2).
1. Block-range locator (
includes/block-locator.php)post_content. It usesWP_Block_Parser's delimiter grammar and the same document-order indexing as the read routes'{index}, so nested blocks are handled.409: a stray closer, a mismatched closer, or a block left unclosed. A write never guesses where a block ends.WP_Block_Parser(tests/phpunit/wp-block-parser/, GPL, test-only), not the bootstrap's cannedparse_blocks()mock. So it's checked on every push, not only in the runtime matrix.2. Citation write routes (
includes/write-routes.php)The routes are off by default. A site opts in with
add_filter( 'bibliography_builder_enable_write_routes', '__return_true' );, typically from an mu-plugin; this filter is the design memo's "companion-plugin flag". They live under/wp-json/bibliography/v1/posts/<id>/bibliographies/<ref>/citations:POST…/citationsPATCH…/citations/<citation_id>nullremoves a field;idandtypeare locked. Works like the editor's field editor.DELETE…/citations/<citation_id>PUT…/citations/order409.Every route:
edit_poston the post;dry_run=false;If-Matchwith the post's ETag to write:428without it,412when it's stale. The ETag is a hash of the content, notpost_modified_gmt, so two saves in the same second can't collide.A write rebuilds the block's markup with the PHP port of
save(), splices only that block, and saves throughwp_update_post()as an ordinary revision. While the routes are on, the read routes send the ETag.Docs:
docs/rest-write-routes.md: enabling, routes, responses, and caveats;CLAUDE.md, the README, the CHANGELOG, and the metrics.Why? The design memo's M2. Its prerequisites are now all on
main: the PHPsave()port (#97), locale-independent markup (#98, #99), and full citation styles (#103).Known gap: BibTeX and BibLaTeX export strings need citation-js, so only the editor computes them. With per-entry Cite / Export on, entries written over REST show RIS and CSL-JSON links until the post is next saved in the editor, which then adds the missing strings. This is documented.
Validation
composer test:phppasses (310). It includes 9 locator tests against the real parser and 14 write-route tests. The route tests assert that every written block's markup is byte-identical to whatsave()renders for its attributes. The suite also passes with--order-by=random.composer lint:phpandcomposer analyze:phppass.npm run lint:jsandnpm run test(872, also with--randomize) pass. No JS changed.verify-metrics.shpasses.Tested end to end in a real WordPress: Playground CLI 3.1.40, WordPress trunk, this build mounted, and the routes enabled by an mu-plugin. The steps ran through core's REST server with
rest_do_request():428withoutIf-Match,412with a stale one;409;Afterwards the block editor loaded all 44 blocks as valid, with no unsaved changes and no page errors.
Checklist
.planning/phases/05-writable-bibliography-rest/05-DESIGN-MEMO.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01KbVuMPN7YTcV27vwsG3LZ2
Generated by Claude Code