From f3cc331b90b083b2885c51b781a120000908c0ed Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Wed, 17 Jun 2026 00:46:32 -0600 Subject: [PATCH 1/3] docs: salvage Phase-2 research and export plans Recovered from stale branch claude/clever-colden-617e8b (tip 054df8f, 78 commits behind main). These Phase-2 (frontend cite/export) planning docs were the only unique content on that branch; all its code/test work is already in main. Part 1 of 2. Co-Authored-By: Claude Opus 4.8 --- .../02-01-PLAN.md | 123 ++++ .../02-02-PLAN.md | 200 +++++++ .../02-RESEARCH.md | 560 ++++++++++++++++++ 3 files changed, 883 insertions(+) create mode 100644 .planning/phases/02-frontend-cite-and-export-affordances/02-01-PLAN.md create mode 100644 .planning/phases/02-frontend-cite-and-export-affordances/02-02-PLAN.md create mode 100644 .planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md diff --git a/.planning/phases/02-frontend-cite-and-export-affordances/02-01-PLAN.md b/.planning/phases/02-frontend-cite-and-export-affordances/02-01-PLAN.md new file mode 100644 index 0000000..c9d2ee7 --- /dev/null +++ b/.planning/phases/02-frontend-cite-and-export-affordances/02-01-PLAN.md @@ -0,0 +1,123 @@ +--- +phase: 02-frontend-cite-and-export-affordances +plan: "02-01" +type: tdd +wave: 1 +depends_on: [] +files_modified: + - src/lib/export-single.js + - src/lib/export-single.test.js +autonomous: true +requirements: + - REQ-FE-02 + - REQ-FE-06 + +must_haves: + truths: + - "buildSingleRisContent(cslItem) returns a valid RIS string for one entry" + - "buildSingleBibtexContent(cslItem) returns a valid BibTeX string for one entry" + - "buildSingleCslJsonContent(cslItem) returns a valid JSON string for one entry" + - "Functions accept a non-DOI entry (thesis) without errors" + - "A reader can receive a single-entry export in RIS, BibTeX, or CSL-JSON format by clicking a per-entry button" + artifacts: + - path: "src/lib/export-single.js" + provides: "Per-entry export helpers (RIS, BibTeX, CSL-JSON)" + exports: + - buildSingleRisContent + - buildSingleBibtexContent + - buildSingleCslJsonContent + - path: "src/lib/export-single.test.js" + provides: "Unit tests for all three per-entry helpers" + min_lines: 60 + key_links: + - from: "src/lib/export-single.js" + to: "src/lib/export.js" + via: "buildRisExportContent, buildBibtexExportContent imports" + pattern: "from './export'" +--- + + +Create the per-entry export helpers that the frontend view script will call to generate BibTeX, RIS, and CSL-JSON content for a single citation. + +Purpose: The existing export.js functions operate on arrays with sorting. The frontend needs thin, single-entry wrappers that accept one CSL-JSON object and return the formatted export string without sort side-effects. +Output: src/lib/export-single.js with three tested functions; src/lib/export-single.test.js with full unit coverage. + + + +@/Users/danknauss/.claude/get-shit-done/workflows/execute-plan.md +@/Users/danknauss/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md + + + + +```javascript +// src/lib/export.js — relevant exports + +export function buildRisExportContent(citations, citationStyle) +// citations: Array of { id: string, csl: Object } — pass [{id: cslItem.id || 'citation', csl: cslItem}] +// citationStyle: string — for single entry pass any valid style; sort has no effect on one item +// returns: string (RIS formatted, synchronous) + +export async function buildBibtexExportContent(citations, citationStyle, { CiteCtor } = {}) +// Same citation record shape; async (loads @citation-js/core dynamically) +// CiteCtor: optional test injection to replace Cite class +// returns: Promise + +export function buildCslJsonExportContent(citations, citationStyle) +// returns: string (JSON array with trailing newline) + +export const RIS_EXPORT_MIME_TYPE +export const BIBTEX_EXPORT_MIME_TYPE +export const CSL_JSON_EXPORT_MIME_TYPE +``` + + + + + Per-entry export helpers + src/lib/export-single.js, src/lib/export-single.test.js + + - buildSingleRisContent({ type: 'thesis', title: 'My Thesis', id: 'thesis-1' }) returns string starting with 'TY - THES' + - buildSingleRisContent({ type: 'article-journal', title: 'Paper', DOI: '10.1/x', id: 'j1' }) contains 'DO - 10.1/x' + - buildSingleRisContent for non-DOI entry returns valid RIS (no crash, no empty string) + - buildSingleBibtexContent({ type: 'book', title: 'Book Title', id: 'b1' }) resolves to string containing '@book' + - buildSingleBibtexContent accepts CiteCtor injection for testing (no real @citation-js load) + - buildSingleCslJsonContent({ type: 'article-journal', title: 'T', id: 'c1' }) returns JSON string parseable as array of length 1 + - buildSingleCslJsonContent preserves all fields from the input cslItem + - All three functions accept a cslItem with no id property (fall back to 'citation' as id) + + + TDD red-green-refactor. Tests first, then minimal implementation. + + src/lib/export-single.js: + - Import buildRisExportContent, buildBibtexExportContent, buildCslJsonExportContent from './export' + - buildSingleRisContent(cslItem): wraps cslItem as [{ id: cslItem.id || 'citation', csl: cslItem }], calls buildRisExportContent with 'chicago-notes-bibliography' style, returns result + - buildSingleBibtexContent(cslItem, { CiteCtor } = {}): same wrapping, calls buildBibtexExportContent async, passes CiteCtor through for test injection + - buildSingleCslJsonContent(cslItem): returns JSON.stringify([cslItem], null, 2) + '\n' directly (no sort needed, simpler than going through buildCslJsonExportContent which wraps into citation records) + + Note on buildSingleCslJsonContent: The base buildCslJsonExportContent maps citation.csl — for single-entry frontend use it is simpler and more predictable to serialize the cslItem array directly. This avoids the citation-record wrapping round-trip and is consistent with what the frontend embed already stores. + + + + +npm test -- --testPathPattern="export-single" --passWithNoTests=false + + + +- src/lib/export-single.js exports buildSingleRisContent, buildSingleBibtexContent, buildSingleCslJsonContent +- All unit tests pass (red commit then green commit per TDD cycle) +- buildSingleRisContent is synchronous; buildSingleBibtexContent is async +- Non-DOI thesis entry produces valid RIS output without errors +- CiteCtor injection works in buildSingleBibtexContent (no real @citation-js load in tests) + + + +After completion, create `.planning/phases/02-frontend-cite-and-export-affordances/02-01-SUMMARY.md` + diff --git a/.planning/phases/02-frontend-cite-and-export-affordances/02-02-PLAN.md b/.planning/phases/02-frontend-cite-and-export-affordances/02-02-PLAN.md new file mode 100644 index 0000000..0cd359d --- /dev/null +++ b/.planning/phases/02-frontend-cite-and-export-affordances/02-02-PLAN.md @@ -0,0 +1,200 @@ +--- +phase: 02-frontend-cite-and-export-affordances +plan: "02-02" +type: tdd +wave: 1 +depends_on: [] +files_modified: + - src/save-markup.js + - block.json + - webpack.config.js +autonomous: true +requirements: + - REQ-FE-01 + - REQ-FE-03 + - REQ-FE-04 + - REQ-FE-05 + +must_haves: + truths: + - "Each
  • in rendered save markup has a data-csl attribute containing escaped JSON" + - "data-csl JSON contains the full csl object for that entry" + - "Angle-bracket characters in title fields are escaped as \\u003c in data-csl" + - "block.json declares viewScript pointing to build/view.js" + - "webpack.config.js includes a 'view' entry for src/view.js" + - "Save markup without JS is fully readable (bibliography text unaffected)" + artifacts: + - path: "src/save-markup.js" + provides: "data-csl attribute on each
  • " + contains: "data-csl" + - path: "block.json" + provides: "viewScript registration" + contains: "viewScript" + - path: "webpack.config.js" + provides: "view entry point" + contains: "view:" + key_links: + - from: "block.json viewScript" + to: "build/view.js" + via: "file:./build/view.js reference" + pattern: "viewScript.*file:.*build/view" + - from: "src/save-markup.js
  • " + to: "data-csl attribute" + via: "JSON.stringify(citation.csl).replace" + pattern: "data-csl" +--- + + +Embed per-entry CSL-JSON into save markup and wire the viewScript entry point so WordPress will auto-enqueue the frontend script when the block is present. + +Purpose: The view.js (Plan 03) needs two things from this plan: (1) CSL-JSON data co-located with each bibliography entry in saved HTML, and (2) WordPress to auto-enqueue view.js only on pages containing the block. +Output: Modified save-markup.js with data-csl on each li; block.json with viewScript; webpack.config.js with view entry. No render_callback added. + + + +@/Users/danknauss/.claude/get-shit-done/workflows/execute-plan.md +@/Users/danknauss/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md +@src/save-markup.js +@src/save.test.js + + + + +```jsx +
  • +``` + + + + + +"editorScript": "file:./build/index.js", +"editorStyle": "file:./build/index.css", +"style": "file:./build/style-index.css" + + + +entry: async () => { + return { + ...base, + validation: path.resolve(__dirname, 'src/validation.js'), + }; +} + + + + + + + + Task 1: Add data-csl attribute to save-markup li elements + src/save-markup.js, src/save.test.js + + - save output for a single citation includes data-csl attribute on the li element + - data-csl value is a valid JSON object matching the citation's csl property + - a citation with title containing angle brackets (e.g. "A title") has data-csl with < escaped as < + - the existing bibliography text content is unchanged (no regression) + - citations without csl.language still render (lang attribute remains optional) + + + In src/save-markup.js, inside the renderBibliographySave function, add a data-csl attribute to each li element: + + ```jsx +
  • + ``` + + The .replace(/ + + npm test -- --testPathPattern="src/save.test" --passWithNoTests=false + + + - src/save-markup.js has data-csl on every li + - New tests pass: data-csl present, JSON parses correctly, angle brackets escaped as < + - All existing save.test.js tests still pass + + + + + Task 2: Wire viewScript in block.json and webpack.config.js + block.json, webpack.config.js + + 1. In block.json, add "viewScript" field after "style": + "viewScript": "file:./build/view.js" + + WordPress 6.1+ auto-enqueues this script on frontend pages where the block is present. The generated handle is "bibliography-builder/bibliography-view-script". No PHP enqueue code is needed. + + 2. In webpack.config.js, add the view entry to the entry async function: + ```js + entry: async () => { + const base = typeof defaultEntry === 'function' ? await defaultEntry() : defaultEntry; + return { + ...base, + validation: path.resolve(__dirname, 'src/validation.js'), + view: path.resolve(__dirname, 'src/view.js'), + }; + }, + ``` + + Note: src/view.js does not exist yet (Plan 03 creates it). webpack will fail to build until view.js is created. This is expected — Plan 03 runs in Wave 2 after this plan. The build does not need to pass until Plan 03 completes. + + The viewScript field in block.json tells WordPress to load build/view.js only when the block is rendered on a page. This is zero-overhead for pages without bibliography blocks and requires no PHP changes — consistent with the no-render_callback requirement. + + + node -e "const b=require('./block.json'); if(!b.viewScript) process.exit(1); console.log('viewScript:', b.viewScript)" && node -e "require('./webpack.config.js')" && echo 'webpack config parses' + + + - block.json has "viewScript": "file:./build/view.js" + - webpack.config.js entry async function includes view: path.resolve(__dirname, 'src/view.js') + - webpack.config.js is syntactically valid (node -e require parses without error) + - No PHP files modified + + + + + + +Run after both tasks: +- `npm test -- --testPathPattern="src/save.test"` — all save tests pass including new data-csl cases +- `node -e "const b=require('./block.json'); console.log('viewScript:', b.viewScript)"` — prints the viewScript value +- `node -e "require('./webpack.config.js')" && echo 'webpack config parses'` — webpack config is syntactically valid +- `grep -n "view:" webpack.config.js` — shows the view entry + + + +- Every li in save markup output has data-csl with valid, <-escaped JSON +- block.json viewScript field is present and points to file:./build/view.js +- webpack.config.js entry includes view entry and is syntactically valid +- All existing save tests still pass, new data-csl tests pass +- No PHP files changed, no render_callback added + + + +After completion, create `.planning/phases/02-frontend-cite-and-export-affordances/02-02-SUMMARY.md` + diff --git a/.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md b/.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md new file mode 100644 index 0000000..03d10d2 --- /dev/null +++ b/.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md @@ -0,0 +1,560 @@ +# Phase 2: Frontend Cite and Export Affordances - Research + +**Researched:** 2026-06-04 +**Domain:** WordPress static block, progressive-enhancement frontend interactivity, citation export (BibTeX/RIS/CSL-JSON), Clipboard API, Blob download +**Confidence:** HIGH + +--- + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|-----------------| +| REQ-FE-01 | Visible per-entry Cite/Export UI on the public bibliography frontend | viewScript + data-attribute hydration pattern; Google Scholar-style cite affordance | +| REQ-FE-02 | BibTeX, RIS, and CSL-JSON download/copy per entry | `buildBibtexExportContent`, `buildRisExportContent`, `buildCslJsonExportContent` in `src/lib/export.js` are single-entry capable after trivial per-entry extraction; Blob+anchor download pattern | +| REQ-FE-03 | No render_callback — all output stays in static save() | viewScript hydrates HTML already saved by save.js; no PHP render required for JS-layer behavior | +| REQ-FE-04 | No-JS bibliography remains fully readable | Existing `` + `
  • ` structure is complete text; cite/export controls are additive DOM elements hidden until JS runs or styled as graceful degradation | +| REQ-FE-05 | Preserves plugin-deactivation resilience | Citation data embedded in `data-csl` attributes is inert HTML; controls can be hidden by default (CSS: `display:none`) and shown by JS; deactivation removes JS but leaves readable bibliography markup intact | +| REQ-FE-06 | Mendeley/Zotero manual-import acceptance | Visible per-entry RIS and BibTeX download buttons directly serve the gap confirmed in Mendeley testing (non-DOI entries missed by auto-detection) | + + +--- + +## Summary + +This phase adds optional Scholar-like Cite/Export controls to the already-saved static bibliography frontend. The plugin uses a static `save()` function — no PHP `render_callback` — which constrains the approach: all interactive behavior must come from JavaScript that hydrates the saved HTML at runtime. + +The cleanest solution for this project is a `viewScript` frontend bundle registered in `block.json`. WordPress automatically enqueues `viewScript` only when the block is present on a page (since WP 6.1), making it zero-cost for posts without bibliography blocks. The script reads per-entry CSL-JSON data from attributes embedded during save and renders Cite/Export controls progressively. The WordPress Interactivity API is available but not required; it introduces a `data-wp-interactive` namespace coupling into saved block markup that becomes dead HTML if the plugin is deactivated — contrary to the deactivation-resilience requirement. Vanilla JS with `data-csl` attributes is the preferred approach. + +The existing `src/lib/export.js` already contains `buildBibtexExportContent`, `buildRisExportContent`, `buildCslJsonExportContent`, and `buildCslJsonExportContent`. These functions operate on arrays of citations and sort them by style; they need a thin wrapper to operate on a single CSL item. The existing `copyTextToClipboard` in `src/lib/clipboard.js` already handles the `navigator.clipboard` + `execCommand` fallback and is injectable for testing. + +**Primary recommendation:** Add a `viewScript` file (`src/view.js`) that reads per-entry CSL-JSON from `data-csl` attributes, builds a per-entry modal/details panel with Copy and Download buttons (BibTeX, RIS, CSL-JSON), and uses the existing export utilities. Embed per-entry CSL-JSON into the save markup using a `data-csl` attribute on each `
  • `. Keep controls hidden with CSS until JS initialises them. + +--- + +## Standard Stack + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| `@citation-js/core` | 0.7.18 (already in deps) | BibTeX generation | Already bundled; `buildBibtexExportContent` uses it async | +| `@citation-js/plugin-bibtex` | 0.7.18 (already in deps) | BibTeX/BibLaTeX format | Already bundled async chunk `citation-plugin-bibtex.js` | +| WordPress `viewScript` | WP 6.1+ | Frontend-only JS enqueue | Auto-enqueued when block present; zero overhead otherwise | +| Clipboard API (`navigator.clipboard`) | Baseline (2025) | Copy text to clipboard | `copyTextToClipboard` in `clipboard.js` already wraps it with fallback | + +### Supporting +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| `Blob` + `URL.createObjectURL` | Baseline | File download in browser | Already used by `downloadTextExport` in `export.js` | +| `
    `/`` HTML | HTML5 native | No-JS-accessible disclosure widget | Use as the cite panel toggle; degrades to always-open without CSS | + +### Alternatives Considered +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| `viewScript` + vanilla JS | WordPress Interactivity API (`viewScriptModule`) | Interactivity API requires `data-wp-interactive` in saved markup, which becomes dead/inert HTML post-deactivation. Also requires WP 6.5+ minimum. Not worth the constraint for this use case. | +| `data-csl` attribute per `
  • ` | Existing `