Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .planning/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,12 +410,11 @@ or pull requests are open. Rough payoff-per-effort order for the 1.11 line:
runs on pull requests), the `main-preview` Playground build, and the
live resolver checks (DOI, PMID, PMCID, arXiv, ISBN, catalog, Internet
Archive, Abilities)
2. **Phase 05 M5: write abilities** — unblocked
- M0–M3 shipped in 1.7.0 and the Abilities API is in core (WordPress
6.9), so nothing in the design memo's sequencing table holds M5 back.
Only the six read-only abilities in `includes/abilities.php` exist
today. Write abilities should wrap the M2/M3 write-route helpers and
inherit their opt-in filter, `edit_post`, dry-run, and `If-Match` rules
2. **Phase 05 M5: write abilities** — done (unreleased)
- six write abilities in `includes/write-abilities.php` wrap the M2/M3
route callbacks and inherit their opt-in filter, `edit_post`, dry-run,
and `If-Match` (`etag` input) rules. Tier 4 (bulk) is the only Phase 05
milestone left
3. **Reference-manager export corpus** — pending todo
`2026-06-23-test-reference-manager-exports.md`; **needs the owner**
- a hand-authored corpus landed in #91
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ This decision should be revisited once Tier 2 is prototyped and the static-save
| M2 | Tier 2 add/update/delete/reorder behind an opt-in filter — **done (unreleased)**; see `docs/rest-write-routes.md` | M1 + static-save spike |
| M3 | Reformat, reorder, ETag (Tier 2 complete) — **done (unreleased)**: reorder and ETag shipped with M2, block settings and reformat (Tier 3) with M3 | M2 validated |
| M4 | Bulk routes (Tier 4) | M3 + rate-limiting design |
| M5 | Abilities registration (Tier 5) | WP Abilities API stable |
| M5 | Abilities registration (Tier 5) — **done (unreleased)**: six write abilities wrapping the M2/M3 routes | WP Abilities API stable |

**2026-09-24 update:** the Abilities API is in core as of WordPress 6.9, so M5's blocker is gone for the read side. The read-only cut shipped ahead of the other milestones, in 1.6.0: `borges/get-bibliographies`, `borges/export-bibliography`, and `borges/validate-citations` in `includes/abilities.php`, using core's `wp_register_ability()` on `wp_abilities_api_init` with `readonly` annotations. The memo's `bibliography/read` naming was replaced by core's required `namespace/action` form under the `borges/` namespace. Writable abilities stay behind M0–M3.

**2026-10-14 update:** the write abilities shipped behind the write-routes filter in `includes/write-abilities.php`: `borges/add-citations`, `borges/update-citation`, `borges/delete-citation`, `borges/reorder-citations`, `borges/update-bibliography-settings`, and `borges/reformat-bibliography`. Rather than re-implement the routes, each ability builds the request its route would receive and calls the route's callback, so permissions, dry runs, and the ETag check cannot drift between the two surfaces. `etag` in the input is sent as `If-Match`. Tier 4 (bulk) is the only milestone left.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Write abilities** (Phase 05, M5), on WordPress 6.9+ and only when the write routes are enabled with the `bibliography_builder_enable_write_routes` filter: `borges/add-citations`, `borges/update-citation`, `borges/delete-citation`, `borges/reorder-citations`, `borges/update-bibliography-settings`, and `borges/reformat-bibliography`. Each calls the matching write route's own code, with the same rules: `edit_post` on the post, a preview unless `dry_run` is false, and an `etag` from a read or dry run to save. See `docs/rest-write-routes.md`.

### Fixed

- **Names with particles sort where each style puts them.** A surname that opens with a lowercase particle, such as "van Duijnhoven", "de la Cruz", "von Neumann", or "d'Alembert", was sorted under the particle in every style. Chicago (both styles) and Vancouver sort by the surname proper, so Chicago lists "Duijnhoven, Elza van" under D, and the formatter now renders it that way. APA, MLA, Harvard, and the other styles keep the particle in front and still sort under V. A capitalized form ("Van Buren") is part of the surname and always sorts under V. Names are stored as entered, so exports and Edit fields are unchanged. Bibliographies saved earlier stay valid in the editor and take the new order the next time they are saved. ([#142](https://github.com/dknauss/Borges/issues/142))
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ A Gutenberg block plugin that accepts DOI identifiers, PubMed/PMID records, BibT
- `GET /posts/{post_id}/bibliographies` — list all bibliography blocks in post
- `GET /posts/{post_id}/bibliographies/{index}` — single bibliography; supports `?format=json|text|csl-json`
- Write routes (Phase 05 M2 and M3, `includes/write-routes.php`): POST/PATCH/DELETE/PUT under `/posts/{post_id}/bibliographies/{ref}/citations`, plus PATCH `/posts/{post_id}/bibliographies/{ref}` (block settings) and POST `…/{ref}/reformat` (style switch, mirroring the editor's `handleCitationStyleChange`). They are **off unless** the `bibliography_builder_enable_write_routes` filter returns true. They need `edit_post`, are dry runs unless `dry_run=false`, and need `If-Match` (the post's ETag, a hash of its content). A write rebuilds markup with the `save()` port and splices only that block via `includes/block-locator.php`. See `docs/rest-write-routes.md`.
- Write abilities (Phase 05 M5, `includes/write-abilities.php`): `borges/add-citations`, `update-citation`, `delete-citation`, `reorder-citations`, `update-bibliography-settings`, `reformat-bibliography`. Registered only when the write-routes filter is on; each builds the matching route's request and calls that route's callback, so `edit_post`, dry run, and `etag` (sent as `If-Match`) behave identically.
- The PMID, PMCID, arXiv, ISBN, catalog, and Internet Archive route callbacks, their provider constants (fixed upstream URLs, timeout, cache TTLs), and the NCBI cache helpers live in `includes/resolvers.php`, loaded by `require_once` from the main file. Route registration and the `edit_posts` permission callbacks stay in `bibliography-builder.php`.
- `includes/google-books-key.php` reads an optional Google Books API key from the `BIBLIOGRAPHY_BUILDER_GOOGLE_BOOKS_API_KEY` constant, then the `bibliography_builder_google_books_api_key` filter. It is added only to the ISBN resolver's Google Books fallback request. There is deliberately no option or settings screen: `verify-metrics.sh` enforces that the plugin persists no settings.
- Read-only Abilities API integration (WordPress 6.9+) lives in `includes/abilities.php`: `borges/get-bibliographies`, `borges/export-bibliography`, `borges/validate-citations` in a `bibliography` category. They reuse the REST read/permission helpers; on older WordPress the `wp_abilities_api_*` hooks never fire.
Expand Down
1 change: 1 addition & 0 deletions bibliography-builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2074,6 +2074,7 @@ function bibliography_builder_block_init() {
require_once BIBLIOGRAPHY_BUILDER_PLUGIN_DIR . 'includes/frontend-json-ld.php';
require_once BIBLIOGRAPHY_BUILDER_PLUGIN_DIR . 'includes/block-locator.php';
require_once BIBLIOGRAPHY_BUILDER_PLUGIN_DIR . 'includes/write-routes.php';
require_once BIBLIOGRAPHY_BUILDER_PLUGIN_DIR . 'includes/write-abilities.php';
add_filter( 'rest_pre_serve_request', 'bibliography_builder_rest_pre_serve_request', 10, 4 );

/**
Expand Down
4 changes: 2 additions & 2 deletions docs/current-metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ Source and LOC figures last verified: **2026-10-06** against the heading-level b

| Metric | Value | Re-derivation command |
|---|---|---|
| Main plugin file (`bibliography-builder.php`) | **2,207** | `wc -l bibliography-builder.php` |
| All first-party PHP (excl. vendor, tests, scripts, playground, packages, output, node_modules, generated `build/`) | **9,697** | `find . -name '*.php' -not -path './vendor/*' -not -path './node_modules/*' -not -path './tests/*' -not -path './packages/*' -not -path './scripts/*' -not -path './playground/*' -not -path './output/*' -not -path './build/*' -print0 \| xargs -0 wc -l \| tail -1` |
| Main plugin file (`bibliography-builder.php`) | **2,208** | `wc -l bibliography-builder.php` |
| All first-party PHP (excl. vendor, tests, scripts, playground, packages, output, node_modules, generated `build/`) | **10,084** | `find . -name '*.php' -not -path './vendor/*' -not -path './node_modules/*' -not -path './tests/*' -not -path './packages/*' -not -path './scripts/*' -not -path './playground/*' -not -path './output/*' -not -path './build/*' -print0 \| xargs -0 wc -l \| tail -1` |
| JS source (`src/`, excl. `*.test.js`) | **11,381** | `find ./src -name '*.js' -not -name '*.test.js' -print0 \| xargs -0 wc -l \| tail -1` |
| Shipped frontend runtime (`build/view.js`, minified) | **1,449 bytes** | `npm run build` then `wc -c < build/view.js` |

Expand Down
17 changes: 16 additions & 1 deletion docs/rest-write-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,21 @@ The stored CSL-JSON is not changed. If any entry has CSL-JSON the formatter cann
- **HTML filtering:** WordPress filters saved HTML for users without `unfiltered_html`, in exactly the same way as it does for the editor.
- **intl:** the PHP `intl` extension is required. Without it, writes return `501`.

## Abilities

On WordPress 6.9 and later, the same writes are also available as abilities through the Abilities API (`includes/write-abilities.php`). They are registered only when the write routes are enabled, in the `bibliography` category next to the read-only abilities:

| Ability | Route it wraps | Input beyond the common fields |
|---|---|---|
| `borges/add-citations` | `POST …/citations` | `items` |
| `borges/update-citation` | `PATCH …/citations/<citation_id>` | `citation_id`, `fields` |
| `borges/delete-citation` | `DELETE …/citations/<citation_id>` | `citation_id` |
| `borges/reorder-citations` | `PUT …/citations/order` | `ids` |
| `borges/update-bibliography-settings` | `PATCH …/bibliographies/<ref>` | `settings` |
| `borges/reformat-bibliography` | `POST …/reformat` | `style` |

Every ability takes `post_id` and either `index` or `bibliography_id` to name the block, plus `dry_run` (default `true`) and `etag`. Each one builds the request its route would get and calls the route's own callback, so the rules are the same: `edit_post` on the post, a preview unless `dry_run` is `false`, and to save, `etag` set to the ETag from a read or dry run. Without `etag` a save is refused with status `428`, and with a stale one `412`. The output is the route's response body, `etag` included. Only `borges/delete-citation` is annotated as destructive.

## Still to come

Later milestones are bulk routes across posts (Tier 4) and write abilities (Tier 5). See the design memo's sequencing table.
The remaining milestone is bulk routes across posts (Tier 4). See the design memo's sequencing table.
Loading
Loading