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
2 changes: 2 additions & 0 deletions src/LayoutBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,8 @@ class LayoutBuilder {
listContext: this._accessibilityListStack.length > 0 ? {
depth: this._accessibilityListStack.length,
itemIndex: this._accessibilityListStack[this._accessibilityListStack.length - 1].itemIndex,
listNode: this._accessibilityListStack[this._accessibilityListStack.length - 1].node,
parentItemIndices: this._accessibilityListStack.slice(0, -1).map(e => e.itemIndex),
isFirstInItem: isFirstLine && this._accessibilityListStack[this._accessibilityListStack.length - 1].isFirstNodeInItem,
isLastInItem: line.lastLineInParagraph && this._accessibilityListStack[this._accessibilityListStack.length - 1].isLastNodeInItem
} : null,
Expand Down
39 changes: 34 additions & 5 deletions src/Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ class Renderer {

// Accessibility: manage logical structure elements based on this line's context
if (tagger && line._accessibilityContext) {
if (line.inlines) {
tagger._currentLineText = line.inlines.map(i => i.text || '').join('');
}
_manageAccessibilityStructures(tagger, taggerState, line._accessibilityContext);
}

Expand Down Expand Up @@ -664,22 +667,48 @@ function _manageAccessibilityStructures(tagger, state, ctx) {
// ==================== LIST MANAGEMENT ====================

if (curLC && !prevLC) {
// Entering a list for the first time
// Entering a list for the first time (or re-entering after mid-page close)
tagger.beginList();
tagger.beginListItem();
} else if (curLC && prevLC) {
if (curLC.depth > prevLC.depth) {
// Nested list starting
tagger.beginList();
tagger.beginListItem();
} else if (curLC.depth < prevLC.depth) {
// Returning from nested list(s) — close each inner list level explicitly.
// Without this, currentList stays as the innermost L element and new items
// get added to the wrong list in the structure tree.
for (let d = prevLC.depth; d > curLC.depth; d--) {
tagger.endList();
}
// prevLC.itemIndex is the inner list's counter and is not comparable to
// curLC.itemIndex. Use parentItemIndices to get the outer list's item index
// from when we were inside the nested list.
const prevOuterItemIndex = prevLC.parentItemIndices[curLC.depth - 1];
if (curLC.itemIndex !== prevOuterItemIndex) {
tagger.beginListItem();
}
} else if (curLC.listNode !== prevLC.listNode) {
// Same depth but a different list — close the previous list and start a new one.
// Without this, adjacent {ul:[...]} blocks at the same depth are silently merged
// into a single L element because !curLC && prevLC never fires between them.
tagger.endList();
tagger.beginList();
tagger.beginListItem();
} else if (curLC.itemIndex !== prevLC.itemIndex) {
// New item at same depth (previous item was closed by processLineEnd)
// Same list, same depth, new item (previous item was closed by processLineEnd)
tagger.beginListItem();
}
// depth decrease is handled by processLineEnd + the next beginList/beginListItem
} else if (!curLC && prevLC) {
// Left all list nesting mid-page — close remaining lists now rather than
// deferring to the next page-change _closeAllOpenStructures call.
// This prevents stale L/LI/LBody references from accumulating and being
// pushed onto the listStack when a subsequent list begins on the same page.
while (tagger.getListDepth() > 0) {
tagger.endList();
}
}
// Leaving a list (prevLC && !curLC) is handled by tagger._closeAllOpenStructures on page change
// and by finalise() at document end

state.prevListContext = curLC || null;

Expand Down
39 changes: 33 additions & 6 deletions src/accessibilityTagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class AccessibilityTagger {

// Current figure
this._currentFigure = null;

// Set by the renderer before each _manageAccessibilityStructures call; used in warnings
this._currentLineText = '';
}

/**
Expand Down Expand Up @@ -532,30 +535,54 @@ class AccessibilityTagger {

/**
* Get the current parent element for adding new child structures.
* Priority order: cell > LBody > Sect
* Priority order: cell > row (TOC) > LBody > BlockQuote > Sect > Document
*
* Skips and nulls any reference that was cascade-ended by PDFKit without
* the tagger explicitly closing it, which would otherwise cause
* "Cannot add child to already-ended structure element" errors.
*
* @returns {object|null} The current PDFKit struct element to use as parent
*/
_getCurrentParent() {
if (this.currentCell) {
return this.currentCell;
if (!this.currentCell._ended) { return this.currentCell; }
console.warn(`AccessibilityTagger: currentCell was cascade-ended unexpectedly (line text: "${this._currentLineText}")`);
this.currentCell = null;
}
// For TOC tables, content goes directly under TOCI (the row), not a cell
if (this.tableIsTOC && this.currentRow) {
return this.currentRow;
if (!this.currentRow._ended) { return this.currentRow; }
console.warn(`AccessibilityTagger: currentRow (TOC) was cascade-ended unexpectedly (line text: "${this._currentLineText}")`);
this.currentRow = null;
}
if (this.currentLBody) {
return this.currentLBody;
if (!this.currentLBody._ended) { return this.currentLBody; }
console.warn(`AccessibilityTagger: currentLBody was cascade-ended unexpectedly (line text: "${this._currentLineText}")`);
this.currentLBody = null;
}
if (this.currentBlockQuote) {
return this.currentBlockQuote;
if (!this.currentBlockQuote._ended) { return this.currentBlockQuote; }
console.warn(`AccessibilityTagger: currentBlockQuote was cascade-ended unexpectedly (line text: "${this._currentLineText}")`);
this.currentBlockQuote = null;
}
if (this.currentSect) {
return this.currentSect;
if (!this.currentSect._ended) { return this.currentSect; }
console.warn(`AccessibilityTagger: currentSect was cascade-ended unexpectedly (line text: "${this._currentLineText}")`);
this.currentSect = null;
}
return this.documentElement;
}

/**
* Returns the current list nesting depth as the tagger sees it.
* Used by the renderer to close the right number of list levels.
*
* @returns {number} The number of currently open list levels.
*/
getListDepth() {
return this.listStack.length + (this.currentList ? 1 : 0);
}

/**
* Close all currently open structures (for page transitions etc.)
*/
Expand Down
Loading