fix: skip rt elements in annotation and search for Japanese books, closes #904 (#1004)

This commit is contained in:
Huang Xin
2025-04-30 17:36:52 +08:00
committed by GitHub
parent 7de1bdf824
commit ed368e1205
5 changed files with 60 additions and 5 deletions
@@ -22,7 +22,13 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useFoliateEvents } from '../../hooks/useFoliateEvents';
import { useNotesSync } from '../../hooks/useNotesSync';
import { getPopupPosition, getPosition, Position, TextSelection } from '@/utils/sel';
import {
getPopupPosition,
getPosition,
getTextFromRange,
Position,
TextSelection,
} from '@/utils/sel';
import { eventDispatcher } from '@/utils/event';
import { findTocItemBS } from '@/utils/toc';
import { transformContent } from '@/services/transformService';
@@ -48,6 +54,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const bookData = getBookData(bookKey)!;
const view = getView(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const primaryLang = bookData.book?.primaryLanguage || 'en';
const isShowingPopup = useRef(false);
const isTextSelected = useRef(false);
@@ -98,6 +105,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
transformers: ['punctuation'],
reversePunctuationTransform: true,
};
const getAnnotationText = (range: Range) => {
return getTextFromRange(range, primaryLang.startsWith('ja') ? ['rt'] : []);
};
const makeSelection = async (sel: Selection, rebuildRange = false) => {
isTextSelected.current = true;
const range = sel.getRangeAt(0);
@@ -105,7 +115,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
sel.removeAllRanges();
sel.addRange(range);
}
transformCtx['content'] = sel.toString();
transformCtx['content'] = getAnnotationText(range);
setSelection({ key: bookKey, text: await transformContent(transformCtx), range, index });
};
// FIXME: extremely hacky way to dismiss system selection tools on iOS
@@ -117,7 +127,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
setTimeout(async () => {
if (!isTextSelected.current) return;
sel.addRange(range);
transformCtx['content'] = range.toString();
transformCtx['content'] = getAnnotationText(range);
setSelection({ key: bookKey, text: await transformContent(transformCtx), range, index });
}, 40);
}, 0);
@@ -32,6 +32,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
const _ = useTranslation();
const { envConfig } = useEnv();
const { settings } = useSettingsStore();
const { getBookData } = useBookDataStore();
const { getConfig, saveConfig } = useBookDataStore();
const { getView, getProgress } = useReaderStore();
const [searchTerm, setSearchTerm] = useState(term);
@@ -39,7 +40,9 @@ const SearchBar: React.FC<SearchBarProps> = ({
const view = getView(bookKey)!;
const config = getConfig(bookKey)!;
const bookData = getBookData(bookKey)!;
const progress = getProgress(bookKey)!;
const primaryLang = bookData.book?.primaryLanguage || 'en';
const searchConfig = config.searchConfig! as BookSearchConfig;
const queuedSearchTerm = useRef('');
@@ -120,12 +123,30 @@ const SearchBar: React.FC<SearchBarProps> = ({
}
};
const createAcceptNode = ({ withRT = true } = {}) => {
return (node: Node): number => {
if (node.nodeType === Node.ELEMENT_NODE) {
const name = (node as Element).tagName.toLowerCase();
if (name === 'script' || name === 'style' || (!withRT && name === 'rt')) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_SKIP;
}
return NodeFilter.FILTER_ACCEPT;
};
};
const handleSearch = async (term: string) => {
console.log('searching for:', term);
isSearchPending.current = true;
const { section } = progress;
const index = searchConfig.scope === 'section' ? section.current : undefined;
const generator = await view.search({ ...searchConfig, query: term, index });
const generator = await view.search({
...searchConfig,
index,
query: term,
acceptNode: createAcceptNode({ withRT: !primaryLang.startsWith('ja') }),
});
const results: BookSearchResult[] = [];
for await (const result of generator) {
if (typeof result === 'string') {
+1
View File
@@ -143,6 +143,7 @@ export interface BookSearchConfig {
matchDiacritics: boolean;
index?: number;
query?: string;
acceptNode?: (node: Node) => number;
}
export interface SearchExcerpt {
+23
View File
@@ -159,3 +159,26 @@ export const getPopupPosition = (
return { point: popupPoint, dir: position.dir } as Position;
};
export const getTextFromRange = (range: Range, rejects: string[] = []): string => {
const clonedRange = range.cloneRange();
const fragment = clonedRange.cloneContents();
const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
const parent = node.parentElement;
if (rejects.includes(parent?.tagName.toLowerCase() || '')) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
},
});
let text = '';
let node: Text | null;
while ((node = walker.nextNode() as Text | null)) {
text += node.nodeValue ?? '';
}
return text;
};