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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- The Playground demos (release, main build, and WordPress.org Preview) open on a demo page with copy-ready samples of every accepted input format, an empty Bibliography block to paste them into, brief notes on the key features, and three example bibliographies (Chicago notes-bibliography with Cite / Export, APA 7, IEEE). The Welcome Guide no longer covers the page. The page is defined once in `playground/demo-content.json` and `playground/demo-content.php`, and `npm run playground:build` writes it into every Blueprint.

### Changed

- The nine citation styles and their locales are rewritten in full from each style manual:
- Chicago notes-bibliography and author-date (17th ed.);
- APA 7;
- MLA 9;
- Harvard (Cite Them Right);
- IEEE;
- Vancouver (*Citing Medicine*);
- OSCOLA;
- ABNT NBR 6023.

The earlier files were about 2 KB stand-ins. They initialized every given name and dropped volume, issue, pages, editors, translators, editions, and access dates. Every manual's author-list rules now apply: "and" or "&", "et al." thresholds, and APA's 21+ ellipsis. So do its journal, chapter, webpage, thesis, report, conference-paper, and preprint forms.

The en-US, en-GB, and pt-BR locales now carry month names, quotation marks, and role terms. pt-BR was an English copy, so ABNT printed "and" and "accessed"; it is now written in Portuguese. Harvard now uses British conventions.

The styles are still written for Borges and licensed GPL-2.0-or-later, not copied from the CC BY-SA CSL repository. `docs/csl-styles.md` lists each source and the few deliberate deviations.

Existing bibliographies keep their saved text until an entry is added or edited or the style is changed.

### Fixed

- Organization authors (CSL `literal` names, such as "Open Research Alliance") no longer vanish from formatted entries.
- An entry after one shortened to "et al." no longer loses the "and" before its last author. Each entry is now formatted on its own.
- A bibliography block no longer opens as invalid ("Attempt Block Recovery") when an editor using a different language from the one who saved it opens the post. `save()` used to write the Cite / Export labels ("Cite / Export", "Copy citation", "Copied", "RIS", "CSL-JSON", "BibTeX", "BibLaTeX") and the "Link to publication" link label into post content in the saving editor's language. The editor checks saved markup against `save()` in the *current* editor's language, so the two did not match. Saved markup is now the same in every language: the panel labels are stored in English and translated for visitors on the server as the block renders (`includes/frontend-labels.php`, a `render_block` filter), so they are translated without JavaScript and the view script loads no `wp-i18n`, and a link whose citation has no title or container title carries no `aria-label`, so screen readers announce its visible URL. Existing posts still validate in any language, because the new deprecation reads the labels back from the saved markup. The exception is blocks saved in the oldest markup shapes (before the biblioentry role was removed) that contain a linked URL in a citation with no title or container title: those still validate only in the language they were saved in, as before. They switch to the new markup the next time they are saved. The PHP port of `save()` (`includes/save-markup.php`) matches.
- In all 19 bundled translations, seven strings were compiled as "Add citations" even though they mean something else: "Copy citation", "Copy citation: %s", "Copied citation.", "Edit citation: %s", "Delete citation: %s", "Added 1 citation.", and "Added 1 citation. %s". These were stale fuzzy matches, and `wp i18n make-mo` compiles fuzzy entries. The translations are cleared so these strings fall back to English, and the `.mo` files are rebuilt.
- Pasting a whole reference-manager `.bib` export no longer loses or misreads records:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Borges is a static-output block: formatted bibliography HTML, JSON-LD, and COinS

| Metric | Value |
|---|---|
| First-party PHP | ~1,978 LOC main plugin file; ~6,428 LOC total with `includes/` |
| First-party PHP | ~2,064 LOC main plugin file; ~6,514 LOC total with `includes/` |
| JS source (`src/`) | ~9,956 LOC |
| Frontend runtime shipped to visitors | `view.js` ~1.4 KB + `style-index.css` ~2.9 KB, no script dependencies, enqueued only when the block is present |
| Installed footprint | ~1.9 MB (`vendor/` ~792 KB, translations 724 KB, build assets ~324 KB) |
Expand Down
2 changes: 1 addition & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ These handle:
- `plugin-doi`: DOI string → CSL-JSON fallback support when direct CrossRef fetch is unavailable
- `plugin-bibtex`: BibTeX string → CSL-JSON

Formatted bibliography strings are generated locally through `citeproc-php` using plugin-owned GPL-compatible CSL style and locale fixtures. Do not bundle the official CSL style or locale repositories in the WordPress.org release package.
Formatted bibliography strings are generated locally through `citeproc-php` using plugin-owned GPL-2.0-or-later CSL styles and locales, written from each style manual's rules (see `docs/csl-styles.md`). Do not bundle the official CSL style or locale repositories in the WordPress.org release package.

---

Expand Down
2 changes: 1 addition & 1 deletion THIRD-PARTY-NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ Borges Bibliography Builder is licensed GPL-2.0-or-later. The release package in
- `myclabs/php-enum` — MIT License — https://github.com/myclabs/php-enum
- `@citation-js/core`, `@citation-js/plugin-doi`, and `@citation-js/plugin-bibtex` are used at build time/editor runtime through the bundled editor script and are MIT licensed — https://citation.js.org/

The Composer path packages named `citation-style-language/styles` and `citation-style-language/locales` in this repository are **not** the official CSL project packages. They contain project-authored, minimal CSL style and locale fixtures licensed GPL-2.0-or-later for this plugin. They are included so the WordPress.org release does not bundle the official CC BY-SA CSL style or locale repositories.
The Composer path packages named `citation-style-language/styles` and `citation-style-language/locales` in this repository are **not** the official CSL project packages. They contain CSL styles and locales written for this plugin from the style manuals' own rules (see docs/csl-styles.md), licensed GPL-2.0-or-later. They are included so the WordPress.org release does not bundle the official CC BY-SA CSL style or locale repositories.

The source tree and release package must not include dependencies or files licensed only as CPAL-1.0, AGPL-1.0, or CC-BY-SA-3.0.
94 changes: 90 additions & 4 deletions bibliography-builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ function bibliography_builder_get_formatter_style_definitions() {
),
'harvard' => array(
'template' => 'harvard1',
'locale' => 'en-US',
'locale' => 'en-GB',
'family' => 'author-date',
),
'ieee' => array(
Expand Down Expand Up @@ -1112,6 +1112,27 @@ function bibliography_builder_normalize_formatted_text( $text, $style ) {
$text = preg_replace( '/\bp\.\s+p{1,2}\.\s+/u', 'p. ', $text );
}

// citeproc-php puts a space between a name and a label whose prefix is
// ", " ("Lindqvist , eds."); a space before a comma is never wanted.
$text = str_replace( ' ,', ',', $text );

// American styles put commas and periods inside closing quotes. citeproc-php
// only does so for an element's own suffix, not for a group delimiter
// (“Title”, in IEEE).
if ( isset( $style['locale'] ) && 'en-US' === $style['locale'] ) {
$text = str_replace( array( '”,', '”.' ), array( ',”', '.”' ), $text );
}

// citeproc-php initializes only capitalized parts of a hyphenated given
// name, so "Ji-woo" comes out "J.- woo". Initialize the second part too.
$text = preg_replace_callback(
'/(\p{Lu})(\.?)- (\p{Ll})\p{Ll}*/u',
static function ( $parts ) {
return $parts[1] . $parts[2] . '-' . mb_strtoupper( $parts[3], 'UTF-8' ) . $parts[2];
},
$text
);

return str_replace( 'and et al.', 'et al.', $text );
}

Expand Down Expand Up @@ -1169,6 +1190,63 @@ function bibliography_builder_extract_citeproc_entries( $html, $style ) {
return $entries;
}

/**
* Adapt a CSL-JSON item to what citeproc-php 2.7 can render.
*
* - Literal names (organizations, one-name authors): citeproc-php renders a
* name only from its `family` part, so `{ "literal": "Open Research
* Alliance" }` would vanish. As a family-only name it prints whole, never
* inverted or initialized.
* - `page-first`: citeproc-php does not derive it from `page`, and OSCOLA
* cites a journal article by its first page.
*
* @param array $item CSL-JSON item.
* @return array
*/
function bibliography_builder_prepare_csl_for_formatter( $item ) {
$name_variables = array(
'author',
'collection-editor',
'composer',
'container-author',
'director',
'editor',
'editorial-director',
'illustrator',
'interviewer',
'original-author',
'recipient',
'reviewed-author',
'translator',
);

foreach ( $name_variables as $variable ) {
if ( empty( $item[ $variable ] ) || ! is_array( $item[ $variable ] ) ) {
continue;
}

foreach ( $item[ $variable ] as $position => $name ) {
$is_literal = is_array( $name ) && empty( $name['family'] )
&& isset( $name['literal'] ) && is_string( $name['literal'] );
$literal = $is_literal ? trim( $name['literal'] ) : '';

if ( '' !== $literal ) {
$item[ $variable ][ $position ] = array( 'family' => $literal );
}
}
}

if ( empty( $item['page-first'] ) && isset( $item['page'] ) && is_scalar( $item['page'] ) ) {
$first = trim( preg_split( '/[-\x{2013}\x{2014},&]/u', (string) $item['page'] )[0] );

if ( '' !== $first ) {
$item['page-first'] = $first;
}
}

return $item;
}

/**
* Format CSL-JSON items as plain-text bibliography entries.
*
Expand Down Expand Up @@ -1216,7 +1294,7 @@ function bibliography_builder_format_csl_items( $csl_items, $style_key ) {
$prepared_items = array();

foreach ( array_values( $csl_items ) as $index => $item ) {
$item_array = is_array( $item ) ? $item : array();
$item_array = bibliography_builder_prepare_csl_for_formatter( is_array( $item ) ? $item : array() );
$item_array['id'] = 'bibliography-builder-format-' . $index;
$prepared_items[] = $item_array;
}
Expand Down Expand Up @@ -1246,9 +1324,17 @@ function bibliography_builder_format_csl_items( $csl_items, $style_key ) {
),
);

// One formatter per entry: citeproc-php keeps per-render state on its
// parsed name elements (once one entry is cut to "et al.", later entries
// in the same render lose their "and"), and no style here needs
// cross-entry context such as disambiguation or author substitution.
try {
$formatter = new \Seboettg\CiteProc\CiteProc( $style_xml, $style['locale'], $markup_extension );
$html = $formatter->render( $items_for_formatter, 'bibliography' );
$html = '';

foreach ( $items_for_formatter as $item_for_formatter ) {
$formatter = new \Seboettg\CiteProc\CiteProc( $style_xml, $style['locale'], $markup_extension );
$html .= $formatter->render( array( $item_for_formatter ), 'bibliography' );
}
} catch ( Throwable $error ) {
return new WP_Error(
'bibliography_builder_formatter_failed',
Expand Down
84 changes: 84 additions & 0 deletions docs/csl-styles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Bundled citation styles

Borges formats bibliographies with `citeproc-php` against nine CSL styles and three locales that live in this repository:

- styles: `packages/citation-style-language-styles/`
- locales: `packages/citation-style-language-locales/`

## Why they are project-authored

The CSL project's official styles and locales are licensed CC BY-SA 3.0, which is not GPL-compatible. WordPress.org requires GPL-compatible code and assets, so Borges must not bundle them (see `THIRD-PARTY-NOTICES.txt` and `SPEC.md`).

Instead, every style here was written for Borges from its style manual's own rules, and is licensed GPL-2.0-or-later. Style rules themselves are not copyrightable. The official CSL files were not used as a source.

Earlier releases shipped about 2 KB stand-ins. Those initialized every given name, dropped volume, issue and pages, and ignored editors, translators, editions and access dates. The locales had no month names or quotation marks, and pt-BR was a copy of English.

## Styles

| Key | File | Source | Notes |
| --- | --- | --- | --- |
| `chicago-notes-bibliography` | `chicago-notes-bibliography.csl` | Chicago Manual of Style, 17th ed., ch. 14 (bibliography entries) | First author inverted, "and" before the last name, 11+ authors cut to 7 + "et al." |
| `chicago-author-date` | `chicago-author-date.csl` | Chicago Manual of Style, 17th ed., ch. 15 | Year after the author; journal form `27 (2): 97–111` |
| `apa-7` | `apa.csl` | APA Publication Manual, 7th ed., ch. 9–10 | Initials, `&`, up to 20 authors, 21+ as the first 19, "…", and the last; no publisher place |
| `mla-9` | `modern-language-association.csl` | MLA Handbook, 9th ed., ch. 5 | 3+ authors as "First, et al."; MLA month abbreviations (June, July, Sept.) |
| `harvard` | `harvard1.csl` | Cite Them Right, 12th ed. | en-GB; initials without spaces, 4+ authors "et al.", `(eds)`, `edn`, single quotes, "Available at: … (Accessed: …)" |
| `ieee` | `ieee.csl` | IEEE Reference Guide (2023) | Initials first; 7+ authors as first + "et al."; `doi:` form; "[Online]. Available:" |
| `vancouver` | `vancouver.csl` | NLM *Citing Medicine*, 2nd ed. (ICMJE) | `Green S`; 7+ authors as 6 + "et al."; `2020 Apr;27(2):97-111`; "[Internet] … [cited …]" |
| `oscola` | `oscola.csl` | OSCOLA, 4th ed. (bibliography) | en-GB; `Alvarez MI`, 4+ "and others"; `(2020) 27 Journal 97`; `Smith v Jones [2019] UKSC 12`; no final full stop |
| `abnt` | `abnt.csl` | ABNT NBR 6023:2018 | pt-BR; SURNAMES in capitals, `;` between authors, 4+ "et al."; `3. ed.`, `(org.)`, `In:`, `[S. l.]`, `[s. d.]`, "Disponível em: … Acesso em: …" |

Deviations shared by every style, all forced by how the block stores and renders entries:

- **Titles keep their stored capitalization.** APA's sentence case is not applied, and neither is ABNT's capitalized first word for title-first entries. The block re-applies italics by finding the stored title text in the formatted entry, so a case-transformed title would lose its italics.
- **URLs and DOIs stay linkable.** The block links `https://` URLs up to the next space. So OSCOLA prints URLs without angle brackets, and ABNT prints DOIs as `https://doi.org/` links.
- **No Chicago 3-em dash for repeated authors.** It depends on list order, and the block sorts entries itself, after formatting.
- **No citation numbers.** Numeric styles number with the list element.
- **Journal titles print as stored.** They are not abbreviated (IEEE, Vancouver).

## Writing or changing a style

Follow these conventions. Each one works around citeproc-php 2.7 behavior (see `docs/external-eccentricities.md`):

1. **Suffix each part with `.` or `,` inside a space-delimited group.** citeproc-php moves punctuation inside a closing quote, and drops a doubled period ("eds.."), only for an element's own suffix, and only when it is exactly `.`, `,` or `;`. It never does this for a group delimiter.
2. **Use `<text term="editor" form="verb"/>` before the names for role phrases.** A `<label>` placed before a name loses its trailing space ("Translated byHelen").
3. **Choose creators explicitly when later conditions test them.** After `<substitute>`, citeproc-php treats the substituted variable as absent in every later `<if variable>`, so an edited book lost its title. Substitution is safe only when no later condition tests the substituted variable.
4. **Wrap the children of a nested `<choose>` in a delimited `<group>`.** A `<choose>` inside another branch joins its children with no delimiter ("15.Available").
5. **Omit `page-range-format` for en-dash styles.** Any `page-range-format` makes citeproc-php join the range with a hyphen, while none gives a full en-dash range ("97–111"). Vancouver (`minimal`) and ABNT (`expanded`) set one because they want hyphens.
6. **Use explicit `<date-part>` children.** Never use `form="text"` dates. See the "localized dates" entry.
7. **Keep sort keys in separate `sort-*` macros.** The `<sort>` blocks exist for the PHP/JS sort-coordination tests (`tests/phpunit/SortCoordinationTest.php`). Rendering a display macro as a sort key corrupts its name state, so the "and" disappears.

The formatter (`bibliography_builder_format_csl_items()`) also adapts input and output:

- **Per-entry rendering.** Each entry is rendered with its own formatter, because citeproc-php keeps name state across entries: once one entry is cut to "et al.", later entries lose their "and".
- **`bibliography_builder_prepare_csl_for_formatter()`**:
- maps `{ "literal": … }` names to family-only names, because citeproc-php drops names without a family part;
- derives `page-first`, for OSCOLA.
- **`bibliography_builder_normalize_formatted_text()`**:
- removes the space citeproc-php puts before a label's comma;
- fully initializes hyphenated given names ("J.-W.", not "J.- woo");
- moves commas and periods inside closing quotes for `en-US` styles.

## Testing

`tests/phpunit/CslStyleGoldenTest.php` renders `tests/fixtures/csl-styles/items.json` in every style and compares the result with `tests/fixtures/csl-styles/<style-key>.txt`.

The corpus covers:

- books with one or two authors, a translator, and editors only;
- a chapter;
- journal articles with 1, 3, 8 and 22 authors;
- a magazine and a newspaper article;
- webpages with and without a date;
- a thesis, a report, a conference paper and a preprint;
- an entry with no author and no date;
- a case and a statute.

After an intended change:

```sh
BORGES_WRITE_STYLE_GOLDENS=1 composer test:php -- --filter CslStyleGolden
```

Then review the golden diff against the manual.

The same test fails when `vendor/citation-style-language/` is out of date with `packages/`. The formatter reads the vendor copy, so run `composer install` after editing a style.
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-09-25** against the locale-independ

| Metric | Value | Re-derivation command |
|---|---|---|
| Main plugin file (`bibliography-builder.php`) | **1,978** | `wc -l bibliography-builder.php` |
| All first-party PHP (excl. vendor, tests, scripts, playground, packages, output, node_modules, generated `build/`) | **6,428** | `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,064** | `wc -l bibliography-builder.php` |
| All first-party PHP (excl. vendor, tests, scripts, playground, packages, output, node_modules, generated `build/`) | **6,514** | `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`) | **9,956** | `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
Loading
Loading