Skip to content

fix(lightnovelwp): parse chapter text when bottomnav is absent - #2593

Merged
rajarsheechatterjee merged 2 commits into
lnreader:masterfrom
hamedhani1998:fix/lightnovelwp-chapter-content-v2
Sep 29, 2026
Merged

rajarsheechatterjee merged 2 commits into
lnreader:masterfrom
hamedhani1998:fix/lightnovelwp-chapter-content-v2

Conversation

@hamedhani1998

Copy link
Copy Markdown
Contributor

Problem

parseChapter matched the chapter text with:

data.match(/<div.*?class="epcontent ([^]*?)<div.*?class="?bottomnav/g)?.[0]

It relied on a bottomnav div to close the epcontent block. Current versions of the theme no longer emit that wrapper — the block is now class="epcontent entry-content" with no bottomnav anywhere in the document.

The result is a silent failure: the match returns undefined, the ?. collapses to undefined, and parseChapter returns ''. No error is raised and no exception is thrown — the source just shows every chapter as blank text.

Fix

Close the epcontent block at whichever structural landmark follows it (bottomnav, a <script>, or </body>), and fall back to the whole document when there is no epcontent block at all:

const content = data.match(
  /<div[^>]*class="[^"]*\bepcontent\b[^"]*"[^>]*>([^]*?)(?=<div[^>]*class="?bottomnav|<script|<\/body)/,
);
return (content?.[1] ?? data).match(/<p[^>]*>([^]*?)<\/p>/g)?.join('\n') || '';

This also drops the regex's hard-coded epcontent class spelling, which would not match when epcontent is not followed by a space and another class name.

Verification

Against real captured markup from Kol Novel, whose current theme has no bottomnav:

  • before: 0 bytes, 0 paragraphs
  • after: 14,122 bytes / 8,466 chars / 117 paragraphs

Backward compatibility was checked rather than assumed, since this template backs 27 active sources:

  • on a page that still has epcontent + bottomnav, old and new expressions produce byte-identical output (97 bytes)
  • on a page without bottomnav: old → 0 bytes, new → 60 bytes
  • on a page with no epcontent at all, the new code falls back instead of throwing

parseNovels and parseNovel are untouched and still parse 20 novels and 737 chapters respectively on the same pages.

🤖 Generated with Claude Code

parseChapter anchored the chapter text on a `bottomnav` div to close the
`epcontent` block. Current versions of the theme no longer emit that wrapper,
so the match returned undefined and every chapter parsed as an empty string —
no error, just blank text on affected sources.

Close the block at whichever structural landmark follows it (bottomnav, a
script tag, or </body>), and fall back to the whole document when no epcontent
block is present at all. This also drops the regex's hard-coded `epcontent `
class spelling, which would not match `class="epcontent entry-content"` when
epcontent is the only class.

Output is byte-identical to the old expression on pages that still have
bottomnav, verified against captured markup, so the other lightnovelwp
sources are unaffected.

Kol Novel was the source that surfaced this.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 2/5

[Medium risk] Fixes chapter text parsing for a light novel plugin.

The PR should not merge until chapter extraction is bounded to the content block and missing-block responses are handled without treating page-wide paragraphs as chapter text.

Findings

  1. P1 Inline scripts truncate chapters ▶
  2. P1 Footer text enters chapters ▶
  3. P1 Error pages become chapter text ▶

Summary

The PR replaces the bottomnav-dependent chapter regex with an epcontent match that can stop at other landmarks and falls back to scanning the document. The new boundaries can truncate chapters at inline scripts or include paragraphs outside the chapter block; the fallback can also present unrelated page text as a chapter.

Reviews (1) · Last reviewed commit: "fix(lightnovelwp): parse chapter text wh..."

// block off at the next structural landmark instead, whichever is
// present, and fall back to the whole document when none of them is.
const content = data.match(
/<div[^>]*class="[^"]*\bepcontent\b[^"]*"[^>]*>([^]*?)(?=<div[^>]*class="?bottomnav|<script|<\/body)/,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Inline scripts truncate chapters If a chapter contains an inline <script> before later paragraphs, this match stops at the script. Paragraph extraction then uses only the text before it, so the rest of the chapter silently disappears—even on pages where bottomnav previously let those paragraphs be parsed.

// block off at the next structural landmark instead, whichever is
// present, and fall back to the whole document when none of them is.
const content = data.match(
/<div[^>]*class="[^"]*\bepcontent\b[^"]*"[^>]*>([^]*?)(?=<div[^>]*class="?bottomnav|<script|<\/body)/,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Footer text enters chapters If a page has no bottomnav and its first script appears after the chapter block, this match continues past the closing epcontent div. Any paragraphs in intervening recommendations, comments, or other sections are returned as chapter text.

.match(/<div.*?class="epcontent ([^]*?)<div.*?class="?bottomnav/g)?.[0]
.match(/<p[^>]*>([^]*?)<\/p>/g)
?.join('\n') || ''
(content?.[1] ?? data).match(/<p[^>]*>([^]*?)<\/p>/g)?.join('\n') || ''

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Error pages become chapter text If a chapter request returns a 200-status login or error page without epcontent, this fallback extracts paragraphs from the entire response. Notices or other page text are then shown as the chapter instead of producing the previous empty result.

Follow-up to the bottomnav fix. Cutting the document at the next `<script>`
or `</body>` guessed the boundary from surrounding text, which is wrong in
both directions on a real page:

- an inline `<script>` inside the block ends the match early, so paragraphs
  after it are dropped — measured on Kol Novel, the first script after the
  block cuts 6 of 96 paragraphs
- when no script intervenes the match runs past the block's real close, so
  paragraphs from recommendations and comments are appended to the chapter —
  the same page yields 145 paragraphs that way against 96 inside the block

Walk the markup with the parser instead and close on the element that actually
ends the block, tracking div depth. This drops the first and last false
landmark in one change rather than picking a better guess.

Also return nothing when there is no epcontent block at all. A 200 response
from a login wall or an error page has paragraphs but no chapter, and
falling back to the whole document showed that page's text as the chapter.

Verified against captured Kol Novel markup: 96 paragraphs inside the block, and
an error page now yields an empty chapter rather than its notice text.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@hamedhani1998

Copy link
Copy Markdown
Contributor Author

All three review findings were right, and the first round of this PR was wrong. Rewritten rather than patched.

The version that was up for review cut the document at the first <script> or </body> after the epcontent block. Measuring that regex against a real Kol Novel chapter page:

boundary used paragraphs returned
first <script> after the block (cuts at offset 68855) 98 of 104
no script intervenes, so the match runs to </body> 145
paragraphs actually inside the epcontent element 96

So both failure modes were live at once: the script boundary truncated the tail, and the </body> boundary overran the real close and pulled 49 paragraphs in from the recommendations and comment sections below the chapter. The overrun was not in the review — I found it by checking what the 145 paragraphs actually were.

Now the parser walks the markup with htmlparser2 and tracks div depth, closing on the element that actually ends the block. No text landmark is guessed at all, which removes the script truncation and the overrun together.

The regex:

/<div[^>]*class="[^"]*\bepcontent\b[^"]*"[^>]*>([^]*?)(?=<div[^>]*class="?bottomnav|<script|<\/body)/

becomes a depth-tracked walk — enter on a div whose class matches epcontent as a whole word, increment on each nested div, exit when depth returns to zero.

On the error-page finding — agreed, and it is the one that needed a decision rather than a tweak. A 200 response from a login wall or an error page has paragraphs but no chapter, so falling back to the whole document showed the notice as chapter text. With no epcontent block present there is nothing to return, so it returns an empty string. That matches the old behaviour on those pages instead of replacing silence with a misleading message.

Verified against the captured page: 96 paragraphs, the same set the element actually contains. An error page and a login wall both return empty. parseNovels and parseNovel are untouched (20 novels, 737 chapters).

🤖 Generated with Claude Code

@RibatTRW

Copy link
Copy Markdown
Contributor

Heads-up: #2596 and #2597 opened today report the same empty Kol Novel chapters (including Mushoku Tensei intro). Your depth-walk rewrite looks like the cover. If your verification confirms, consider adding Closes #2596, #2597 to the body. #2529 also claims Kol Novel coverage — worth a note on overlap to avoid double-landing.

@hamedhani1998

Copy link
Copy Markdown
Contributor Author

Verified the rewrite against live sites rather than the captured page. The three findings are addressed. Two things need your call before this merges — one is a bug in the rewrite, the other is an overlap with #2529 that is larger than it looks.

The three findings, re-checked

Each finding reproduced on a page built to exhibit it, run against the branch implementation:

finding result
inline <script> truncates the chapter 3 of 3 paragraphs returned, including those after the script
footer/recommendation text enters the chapter 1 paragraph returned, comment section excluded
error page becomes chapter text empty string, no epcontent present
(back-compat) page that still has bottomnav both paragraphs returned

The depth-tracked walk is the right call — closing on a structural landmark was the actual defect, and the script-truncation and overrun findings were both real consequences of guessing one.

1. The walk misses a live source, because epcontent is not always a <div>

TCSega (teamchmantranslations.com) serves:

<article class="epcontent entry-content reading-content dakesu-reading-area" itemprop="text">

onopentag enters on name === 'div' && !inside, so it never enters this block: 0 paragraphs returned against the 26 the element contains. Pre-existing rather than a regression — the old bottomnav regex also returned 0 here — but it is the same silent-empty-chapter defect this PR exists to close, on a source that is up.

Tracking the depth of the tag that actually carries the class recovers it:

onopentag(name, attribs) {
  if (!inside) {
    if (/(^|\s)epcontent(\s|$)/.test(attribs['class'] || '')) {
      inside = true;
      rootTag = name;          // whichever tag carries the class
      depth = 1;
    }
    return;
  }
  if (name === rootTag) depth++;
  else if (name === 'p') isReadingParagraph = true;
},

with the close branch testing name === rootTag. Across the 9 of 36 sources that currently answer (the rest are dead, blocked, or have no chapters):

source tag in element current walk name-agnostic
TCSega article 26 0 26
universalnovel div 131 131 131
kodekslibrary div 113 113 113
blumeverse div 140 140 140
knoxt div 54 53 53
lazygirltranslations div 58 58 58
dobynovels div 46 46 46
transweaver div 33 33 33
hyacinthbloom div 4 4 4

One site recovered, eight byte-identical, none worse.

Two counts that look alarming and are not, so nobody re-derives them: kodekslibrary has 220 <p> in the block but 113 real ones — the other 107 are &nbsp; spacers, which the old code was returning as chapter text. knoxt returns 53 against 54 because one is a spacer.

2. #2529 already fixes this, and the two conflict directly

#2529 touches the same two files — lightnovelwp/template.ts and madara/template.ts — and fixes the same defects:

  • its lightnovelwp change already handles the <article> case, by selecting $('.epcontent') with cheerio instead of hard-coding <div>, which is why it does not have the TCSega problem
  • its madara change replaces the same .text-left || .text-right || … chain I hit, where a cheerio selection is always truthy so || never falls through
  • it carries the same h1.nhv-novel-title / .nhv-novel-cover / .nhv-novel-synopsis fallbacks that are the Riwyat half of my local work

Two different approaches to the same parseChapter bug on the same lines. Whichever lands second has to reconcile them, and the depth-tracked walk is the stronger of the two — it is what makes the three findings pass — so the reconciliation is worth doing deliberately rather than by whichever merge order Git picks.

On the two body edits you asked for: Closes #2596, #2597 is right, and I checked it does not collide — #2529 closes a different set of Kol Novel reports (#2568, #2576, #2578), so these two are still unclaimed and adding them here is the only place they get closed. I have not edited the body myself, since this is your branch; say the word and I will, or leave it to you.

The overlap note is the part worth your call.

@rajarsheechatterjee
rajarsheechatterjee merged commit 4673688 into lnreader:master Sep 29, 2026
3 checks passed
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Oct 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants