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
123 changes: 123 additions & 0 deletions .planning/phases/02-frontend-cite-and-export-affordances/02-01-PLAN.md
Original file line number Diff line number Diff line change
@@ -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'"
---

<objective>
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.
</objective>

<execution_context>
@/Users/danknauss/.claude/get-shit-done/workflows/execute-plan.md
@/Users/danknauss/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-frontend-cite-and-export-affordances/02-RESEARCH.md

<interfaces>
<!-- Key exports from src/lib/export.js that export-single.js imports. -->

```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<string>

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
```
</interfaces>
</context>

<feature>
<name>Per-entry export helpers</name>
<files>src/lib/export-single.js, src/lib/export-single.test.js</files>
<behavior>
- 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)
</behavior>
<implementation>
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.
</implementation>
</feature>

<verification>
<automated>npm test -- --testPathPattern="export-single" --passWithNoTests=false</automated>
</verification>

<success_criteria>
- 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)
</success_criteria>

<output>
After completion, create `.planning/phases/02-frontend-cite-and-export-affordances/02-01-SUMMARY.md`
</output>
200 changes: 200 additions & 0 deletions .planning/phases/02-frontend-cite-and-export-affordances/02-02-PLAN.md
Original file line number Diff line number Diff line change
@@ -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 <li> 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 <li>"
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 <li>"
to: "data-csl attribute"
via: "JSON.stringify(citation.csl).replace"
pattern: "data-csl"
---

<objective>
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.
</objective>

<execution_context>
@/Users/danknauss/.claude/get-shit-done/workflows/execute-plan.md
@/Users/danknauss/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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

<interfaces>
<!-- Current <li> structure in save-markup.js renderBibliographySave -->

```jsx
<li
key={citation.id}
role={includeDeprecatedBiblioEntryRole ? 'doc-biblioentry' : undefined}
id={`ref-${citation.id}`}
lang={citation.csl.language || undefined}
>
```

<!-- New <li> must add data-csl attribute. citation.csl is the CSL-JSON object. -->
<!-- XSS escape: JSON.stringify(citation.csl).replace(/</g, '\\u003c') -->

<!-- block.json current script fields -->
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/style-index.css"
<!-- ADD: "viewScript": "file:./build/view.js" -->

<!-- webpack.config.js current entry -->
entry: async () => {
return {
...base,
validation: path.resolve(__dirname, 'src/validation.js'),
};
}
<!-- ADD: view: path.resolve(__dirname, 'src/view.js') -->
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
<name>Task 1: Add data-csl attribute to save-markup li elements</name>
<files>src/save-markup.js, src/save.test.js</files>
<behavior>
- 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 <em> title") has data-csl with < escaped as \\u003c
- the existing bibliography text content is unchanged (no regression)
- citations without csl.language still render (lang attribute remains optional)
</behavior>
<action>
In src/save-markup.js, inside the renderBibliographySave function, add a data-csl attribute to each li element:

```jsx
<li
key={citation.id}
role={includeDeprecatedBiblioEntryRole ? 'doc-biblioentry' : undefined}
id={`ref-${citation.id}`}
lang={citation.csl.language || undefined}
data-csl={JSON.stringify(citation.csl).replace(/</g, '\\u003c')}
>
```

The .replace(/</g, '\\u003c') call escapes angle brackets as Unicode escape sequences (\\u003c), consistent with RESEARCH.md Pattern 1 and Pitfall 2. This prevents angle brackets in title or other fields from breaking HTML attribute parsing. This is the same approach used in src/lib/jsonld.js buildCslJsonString.

Extend src/save.test.js with new test cases:
1. Test that the rendered li element has a data-csl attribute
2. Test that the data-csl value parses as JSON matching the citation's csl object
3. Test that angle brackets in the title are escaped as \\u003c in data-csl (not as a raw < character)

Note: src/save-markup.test.js does not exist — add tests to src/save.test.js which already has the mock setup and createCitation helper. Write tests FIRST (RED), then add the data-csl attribute (GREEN).

Backward compatibility: This is a non-breaking addition. Old saved blocks without data-csl on their li elements are valid — view.js (Plan 03) will skip entries lacking the attribute. No block deprecation entry is required.
</action>
<verify>
<automated>npm test -- --testPathPattern="src/save.test" --passWithNoTests=false</automated>
</verify>
<done>
- src/save-markup.js has data-csl on every li
- New tests pass: data-csl present, JSON parses correctly, angle brackets escaped as \\u003c
- All existing save.test.js tests still pass
</done>
</task>

<task type="auto">
<name>Task 2: Wire viewScript in block.json and webpack.config.js</name>
<files>block.json, webpack.config.js</files>
<action>
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.
</action>
<verify>
<automated>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'</automated>
</verify>
<done>
- 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
</done>
</task>

</tasks>

<verification>
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
</verification>

<success_criteria>
- Every li in save markup output has data-csl with valid, \\u003c-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
</success_criteria>

<output>
After completion, create `.planning/phases/02-frontend-cite-and-export-affordances/02-02-SUMMARY.md`
</output>
Loading
Loading