feat(cjk): support inline annotation(warichu, Gezhu) layout (#3934)

* feat(warichu): support warichu (割注) inline annotation layout

- Add warichu HTML transformer that converts <span class="warichu"> and
  <warichu> elements into .warichu-pending placeholders during content load
- Implement runtime layout (layoutWarichu / relayoutWarichu) that measures
  column position and splits text into small inline-block chunks (2 chars
  each) so CSS column boundaries can break between them, preventing large
  blank gaps in vertical-rl pagination
- Use column stride (column-width + column-gap) for accurate position
  measurement across column boundaries
- Hook into stabilized event for initial layout and relayout on resize
- Add warichu CSS styles (inline-block chunks, half-size font, vertical align)

* fix(warichu): correct HTML slicing edge cases

- sliceHtml: re-emit tags that were already open before the slice start
  so the result stays well-formed. Previously a slice past an opening
  tag produced an orphan closing tag (e.g. "<b>Hello</b>"[3,5] →
  "lo</b>" instead of "<b>lo</b>").
- sliceHtml / removeFirstVisibleChar / removeLastVisibleChar: treat HTML
  entities (e.g. &amp;) as one visible character so they aren't split or
  truncated mid-entity (e.g. removeFirstVisibleChar("&amp;rest") →
  "amp;rest").
- buildNodes: drop a duplicate chunk.appendChild(l2).
- Add unit tests covering the above for the three pure helpers.

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
This commit is contained in:
loveheaven
2026-04-27 19:12:03 +08:00
committed by Huang Xin
parent fa61f40262
commit ebbbf104b2
6 changed files with 985 additions and 1 deletions
@@ -35,6 +35,7 @@ import {
transformStylesheet,
} from '@/utils/style';
import { mountAdditionalFonts, mountCustomFont } from '@/styles/fonts';
import { layoutWarichu, relayoutWarichu } from '@/utils/warichu';
import { getBookDirFromLanguage, getBookDirFromWritingMode } from '@/utils/book';
import { getIndexFromCfi } from '@/utils/cfi';
import { useUICSS } from '@/hooks/useUICSS';
@@ -175,6 +176,7 @@ const FoliateViewer: React.FC<{
'sanitizer',
'simplecc',
'proofread',
'warichu',
],
};
return Promise.resolve(transformContent(ctx));
@@ -329,6 +331,19 @@ const FoliateViewer: React.FC<{
const stabilizedHandler = useCallback(() => {
setLoading(false);
// Layout/relayout warichu after paginator has set column-width via columnize()
const contents = viewRef.current?.renderer?.getContents?.() || [];
for (const { doc } of contents) {
if (doc) {
const hasPending = doc.querySelectorAll('.warichu-pending').length > 0;
const hasExisting = doc.querySelectorAll('.warichu-head').length > 0;
if (hasPending) {
layoutWarichu(doc);
} else if (hasExisting) {
relayoutWarichu(doc);
}
}
}
}, []);
const docRelocateHandler = (event: Event) => {