mirror of
https://github.com/readest/readest.git
synced 2026-04-28 03:20:45 +00:00
* chore: bump jsdom to the latest version * feat(pdf): support TTS and annotation on PDFs, closes #2149 & closes #3462
This commit is contained in:
parent
93b96d64eb
commit
8850e6c00f
34 changed files with 841 additions and 252 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -46,4 +46,8 @@ fastlane/report.xml
|
|||
|
||||
# nix
|
||||
result*
|
||||
|
||||
|
||||
.playwright-mcp/
|
||||
.claude/
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,12 @@ Platform-specific code lives in `src-tauri/src/{macos,windows,android,ios}/`. Cu
|
|||
|
||||
## Project Rules
|
||||
|
||||
### Test-First Development
|
||||
|
||||
- Always write a failing unit test **before** implementing a fix.
|
||||
- Run the test to confirm it reproduces the bug or fails as expected, then apply the fix and verify the test passes.
|
||||
- Run the full test suite (`pnpm test`) after changes to ensure no regressions.
|
||||
|
||||
### TypeScript
|
||||
|
||||
- Never use the `any` type. Use `unknown`, proper types, or generics instead.
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@
|
|||
"start-web:vinext": "dotenv -e .env.web -- vinext start",
|
||||
"build-tauri": "dotenv -e .env.tauri -- next build",
|
||||
"i18n:extract": "i18next-scanner --config i18next-scanner.config.cjs",
|
||||
"lint": "eslint .",
|
||||
"lint": "tsc --noEmit && eslint .",
|
||||
"test": "dotenv -e .env -e .env.test.local vitest",
|
||||
"test:browser": "vitest --config vitest.browser.config.mts --watch=false",
|
||||
"test:tauri": "vitest --config vitest.tauri.config.mts --watch=false",
|
||||
"test:tauri": "bash scripts/test-tauri.sh",
|
||||
"test:pr:web": "pnpm test -- --watch=false && pnpm test:browser",
|
||||
"test:pr:tauri": "pnpm test -- --watch=false && bash scripts/test-tauri.sh",
|
||||
"test:all": "pnpm test -- --watch=false && pnpm test:browser && bash scripts/test-tauri.sh",
|
||||
|
|
@ -213,7 +213,7 @@
|
|||
"eslint-config-next": "16.1.6",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"i18next-scanner": "^4.6.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"mkdirp": "^3.0.1",
|
||||
"node-env-run": "^4.0.2",
|
||||
"playwright": "^1.58.2",
|
||||
|
|
|
|||
|
|
@ -68,4 +68,4 @@ while ! curl -sf "http://127.0.0.1:${WEBDRIVER_PORT}/status" >/dev/null 2>&1; do
|
|||
done
|
||||
|
||||
echo "WebDriver is ready. Running Tauri tests..."
|
||||
pnpm test:tauri
|
||||
pnpm vitest --config vitest.tauri.config.mts --watch=false
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ describe('ProofreadPopup Component', () => {
|
|||
key: 'test-book',
|
||||
text: 'test word',
|
||||
cfi: 'epubcfi(/6/2[chapter1]!/4/1:0)',
|
||||
index: 0,
|
||||
range: {
|
||||
deleteContents: vi.fn(),
|
||||
insertNode: vi.fn(),
|
||||
|
|
@ -49,7 +50,7 @@ describe('ProofreadPopup Component', () => {
|
|||
startOffset: 5,
|
||||
endOffset: 9,
|
||||
} as unknown as Range,
|
||||
index: 0,
|
||||
page: 1,
|
||||
},
|
||||
position: { point: { x: 100, y: 100 } },
|
||||
trianglePosition: { point: { x: 100, y: 100 } },
|
||||
|
|
|
|||
|
|
@ -6,18 +6,6 @@ import type { BookDoc } from '@/libs/document';
|
|||
import type { FoliateView } from '@/types/view';
|
||||
import { wrappedFoliateView } from '@/types/view';
|
||||
|
||||
// jsdom's Blob doesn't implement arrayBuffer(), polyfill it via FileReader
|
||||
if (typeof Blob.prototype.arrayBuffer !== 'function') {
|
||||
Blob.prototype.arrayBuffer = function () {
|
||||
return new Promise((res, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => res(reader.result as ArrayBuffer);
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsArrayBuffer(this);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Register a stub paginator custom element so View.open() doesn't fail in jsdom
|
||||
if (!customElements.get('foliate-paginator')) {
|
||||
customElements.define(
|
||||
|
|
@ -38,7 +26,7 @@ let view: FoliateView;
|
|||
let totalSections: number;
|
||||
|
||||
const loadEPUB = async () => {
|
||||
const epubPath = resolve(__dirname, '../fixtures/sample-alice.epub');
|
||||
const epubPath = resolve(__dirname, '../fixtures/data/sample-alice.epub');
|
||||
const buffer = readFileSync(epubPath);
|
||||
const file = new File([buffer], 'sample-alice.epub', { type: 'application/epub+zip' });
|
||||
const loader = new DocumentLoader(file);
|
||||
|
|
|
|||
241
apps/readest-app/src/__tests__/document/pdf-cfi.test.ts
Normal file
241
apps/readest-app/src/__tests__/document/pdf-cfi.test.ts
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve, join } from 'path';
|
||||
import { parse, toRange, fromRange } from 'foliate-js/epubcfi.js';
|
||||
import { DocumentLoader } from '@/libs/document';
|
||||
import type { BookDoc } from '@/libs/document';
|
||||
|
||||
const vendorDir = join(process.cwd(), 'public/vendor');
|
||||
|
||||
/**
|
||||
* Tests EPUB CFI resolution with a real PDF loaded via DocumentLoader.
|
||||
*
|
||||
* Reference CFIs captured from sample-alice.pdf in a full browser environment:
|
||||
* - index 3: epubcfi(/6/8!/4/4,/94/1:0,/118/1:10)
|
||||
* - index 4: epubcfi(/6/10!/4/4/42,/1:0,/1:46)
|
||||
*
|
||||
* In jsdom the canvas 2d context is unavailable, so createDocument() uses a
|
||||
* fallback that produces one <span> per getTextContent() item. The element
|
||||
* count differs from the full TextLayer render, so these tests generate CFIs
|
||||
* from the fallback DOM and verify round-trip resolution.
|
||||
*/
|
||||
describe('PDF CFI resolution with real document', () => {
|
||||
let book: BookDoc;
|
||||
let doc3: Document;
|
||||
let doc4: Document;
|
||||
|
||||
/** Shift the spine-level part from a parsed CFI (as view.js resolveCFI does). */
|
||||
const shiftSpine = (parts: ReturnType<typeof parse>) => {
|
||||
(parts.parent ?? parts).shift();
|
||||
return parts;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await import('foliate-js/pdf.js');
|
||||
const pdfjsLib = (globalThis as Record<string, unknown>)['pdfjsLib'] as {
|
||||
GlobalWorkerOptions: { workerSrc: string };
|
||||
};
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
`file://${join(vendorDir, 'pdfjs/pdf.worker.min.mjs')}`,
|
||||
).href;
|
||||
|
||||
const pdfPath = resolve(__dirname, '../fixtures/data/sample-alice.pdf');
|
||||
const buffer = readFileSync(pdfPath);
|
||||
const file = new File([buffer], 'sample-alice.pdf', { type: 'application/pdf' });
|
||||
const loader = new DocumentLoader(file);
|
||||
const result = await loader.open();
|
||||
book = result.book;
|
||||
|
||||
doc3 = await book.sections[3]!.createDocument();
|
||||
doc4 = await book.sections[4]!.createDocument();
|
||||
}, 30_000);
|
||||
|
||||
// ---------- DOM structure -------------------------------------------------
|
||||
|
||||
it('should have textLayer, canvas, and annotationLayer wrappers', () => {
|
||||
expect(doc3.querySelector('#canvas')).toBeTruthy();
|
||||
expect(doc3.querySelector('.textLayer')).toBeTruthy();
|
||||
expect(doc3.querySelector('.annotationLayer')).toBeTruthy();
|
||||
|
||||
const textLayer = doc3.querySelector('.textLayer')!;
|
||||
expect(textLayer.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// ---------- Round-trip: fromRange → parse → toRange -----------------------
|
||||
|
||||
it('should round-trip a range CFI on page 3', () => {
|
||||
const textLayer = doc3.querySelector('.textLayer')!;
|
||||
const spans = textLayer.querySelectorAll('span');
|
||||
// Find a span with enough text content
|
||||
let targetSpan: Element | null = null;
|
||||
for (const span of spans) {
|
||||
if (span.firstChild && span.firstChild.textContent!.length >= 10) {
|
||||
targetSpan = span;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(targetSpan).toBeTruthy();
|
||||
|
||||
const srcRange = doc3.createRange();
|
||||
srcRange.setStart(targetSpan!.firstChild!, 0);
|
||||
srcRange.setEnd(targetSpan!.firstChild!, 10);
|
||||
const expectedText = srcRange.toString();
|
||||
|
||||
const cfi = fromRange(srcRange);
|
||||
const parts = parse(cfi);
|
||||
const resolved = toRange(doc3, parts);
|
||||
|
||||
expect(resolved).toBeInstanceOf(Range);
|
||||
expect(resolved!.toString()).toBe(expectedText);
|
||||
});
|
||||
|
||||
it('should round-trip a range CFI on page 4', () => {
|
||||
const textLayer = doc4.querySelector('.textLayer')!;
|
||||
const spans = textLayer.querySelectorAll('span');
|
||||
let targetSpan: Element | null = null;
|
||||
for (const span of spans) {
|
||||
if (span.firstChild && span.firstChild.textContent!.length >= 10) {
|
||||
targetSpan = span;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(targetSpan).toBeTruthy();
|
||||
|
||||
const srcRange = doc4.createRange();
|
||||
srcRange.setStart(targetSpan!.firstChild!, 0);
|
||||
srcRange.setEnd(targetSpan!.firstChild!, 10);
|
||||
const expectedText = srcRange.toString();
|
||||
|
||||
const cfi = fromRange(srcRange);
|
||||
const parts = parse(cfi);
|
||||
const resolved = toRange(doc4, parts);
|
||||
|
||||
expect(resolved).toBeInstanceOf(Range);
|
||||
expect(resolved!.toString()).toBe(expectedText);
|
||||
});
|
||||
|
||||
it('should round-trip a multi-span range CFI', () => {
|
||||
const textLayer = doc3.querySelector('.textLayer')!;
|
||||
const spans = textLayer.querySelectorAll('span');
|
||||
// Select a range spanning two different spans
|
||||
const span1 = spans[0]!;
|
||||
const span2 = spans[2]!;
|
||||
expect(span1.firstChild).toBeTruthy();
|
||||
expect(span2.firstChild).toBeTruthy();
|
||||
|
||||
const srcRange = doc3.createRange();
|
||||
srcRange.setStart(span1.firstChild!, 0);
|
||||
const endOffset = Math.min(5, span2.firstChild!.textContent!.length);
|
||||
srcRange.setEnd(span2.firstChild!, endOffset);
|
||||
const expectedText = srcRange.toString();
|
||||
|
||||
const cfi = fromRange(srcRange);
|
||||
const parts = parse(cfi);
|
||||
const resolved = toRange(doc3, parts);
|
||||
|
||||
expect(resolved).toBeInstanceOf(Range);
|
||||
expect(resolved!.toString()).toBe(expectedText);
|
||||
});
|
||||
|
||||
it('should round-trip a collapsed (point) CFI', () => {
|
||||
const textLayer = doc3.querySelector('.textLayer')!;
|
||||
const span = textLayer.querySelector('span')!;
|
||||
expect(span.firstChild).toBeTruthy();
|
||||
|
||||
const srcRange = doc3.createRange();
|
||||
srcRange.setStart(span.firstChild!, 3);
|
||||
srcRange.collapse(true);
|
||||
|
||||
const cfi = fromRange(srcRange);
|
||||
const parts = parse(cfi);
|
||||
const resolved = toRange(doc3, parts);
|
||||
|
||||
expect(resolved).toBeInstanceOf(Range);
|
||||
expect(resolved!.collapsed).toBe(true);
|
||||
});
|
||||
|
||||
// ---------- Cross-page CFI mismatch ---------------------------------------
|
||||
|
||||
it('should fail to resolve a page 3 CFI on page 4 document', () => {
|
||||
const textLayer = doc3.querySelector('.textLayer')!;
|
||||
const spans = textLayer.querySelectorAll('span');
|
||||
// Pick the last span so its index likely exceeds page 4's element count
|
||||
const lastSpan = spans[spans.length - 1]!;
|
||||
expect(lastSpan.firstChild).toBeTruthy();
|
||||
|
||||
const srcRange = doc3.createRange();
|
||||
srcRange.setStart(lastSpan.firstChild!, 0);
|
||||
const endOffset = Math.min(5, lastSpan.firstChild!.textContent!.length);
|
||||
srcRange.setEnd(lastSpan.firstChild!, endOffset);
|
||||
|
||||
const cfi = fromRange(srcRange);
|
||||
const parts = parse(cfi);
|
||||
const range = toRange(doc4, parts);
|
||||
// May resolve to a different node or return null depending on DOM sizes;
|
||||
// either way it should NOT match the original text
|
||||
if (range) {
|
||||
expect(range.toString()).not.toBe(srcRange.toString());
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- Reference CFI format verification -----------------------------
|
||||
|
||||
it('should parse real browser-captured CFIs correctly', () => {
|
||||
// These CFIs were captured from sample-alice.pdf in a full browser with
|
||||
// TextLayer. They verify that the CFI format is structurally valid.
|
||||
const cfi3 = 'epubcfi(/6/8!/4/4,/94/1:0,/118/1:10)';
|
||||
const cfi4 = 'epubcfi(/6/10!/4/4/42,/1:0,/1:46)';
|
||||
|
||||
const parts3 = parse(cfi3);
|
||||
expect(parts3.parent).toBeTruthy();
|
||||
expect(parts3.start).toBeTruthy();
|
||||
expect(parts3.end).toBeTruthy();
|
||||
|
||||
const parts4 = parse(cfi4);
|
||||
expect(parts4.parent).toBeTruthy();
|
||||
expect(parts4.start).toBeTruthy();
|
||||
expect(parts4.end).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should encode the correct section index in CFI spine step', () => {
|
||||
// Section index 3 → spine step /6/8, section index 4 → spine step /6/10
|
||||
const cfi3 = 'epubcfi(/6/8!/4/4,/94/1:0,/118/1:10)';
|
||||
const cfi4 = 'epubcfi(/6/10!/4/4/42,/1:0,/1:46)';
|
||||
|
||||
expect(cfi3).toContain('/6/8!');
|
||||
expect(cfi4).toContain('/6/10!');
|
||||
|
||||
// Spine step /6/N: N = (index + 1) * 2
|
||||
// index 3 → (3+1)*2 = 8, index 4 → (4+1)*2 = 10
|
||||
const parts3 = parse(cfi3);
|
||||
expect(parts3.parent[0][0].index).toBe(6);
|
||||
expect(parts3.parent[0][1].index).toBe(8);
|
||||
|
||||
const parts4 = parse(cfi4);
|
||||
expect(parts4.parent[0][0].index).toBe(6);
|
||||
expect(parts4.parent[0][1].index).toBe(10);
|
||||
});
|
||||
|
||||
// ---------- Out-of-bounds CFI indices -------------------------------------
|
||||
|
||||
it('should return null when CFI child indices exceed the DOM', () => {
|
||||
const cfi = 'epubcfi(/6/8!/4/4,/9000/1:0,/9002/1:5)';
|
||||
const parts = shiftSpine(parse(cfi));
|
||||
const range = toRange(doc3, parts);
|
||||
expect(range).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for a simple CFI with an unreachable node', () => {
|
||||
const cfi = 'epubcfi(/6/8!/4/9000/1:0)';
|
||||
const parts = shiftSpine(parse(cfi));
|
||||
const range = toRange(doc3, parts);
|
||||
expect(range).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when start resolves but end does not', () => {
|
||||
const cfi = 'epubcfi(/6/8!/4/4,/2/1:0,/9999/1:5)';
|
||||
const parts = shiftSpine(parse(cfi));
|
||||
const range = toRange(doc3, parts);
|
||||
expect(range).toBeNull();
|
||||
});
|
||||
});
|
||||
266
apps/readest-app/src/__tests__/document/pdf-tts.test.ts
Normal file
266
apps/readest-app/src/__tests__/document/pdf-tts.test.ts
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve, join } from 'path';
|
||||
import { textWalker } from 'foliate-js/text-walker.js';
|
||||
import { TTS } from 'foliate-js/tts.js';
|
||||
import { createRejectFilter } from '@/utils/node';
|
||||
import { DocumentLoader } from '@/libs/document';
|
||||
import type { BookDoc } from '@/libs/document';
|
||||
|
||||
// The @pdfjs alias in vitest.config.mts resolves to public/vendor/pdfjs,
|
||||
// mirroring how foliate-js/pdf.js does `import '@pdfjs/pdf.min.mjs'`.
|
||||
const vendorDir = join(process.cwd(), 'public/vendor');
|
||||
|
||||
/** Strip all XML/SSML tags to get plain text content */
|
||||
const stripTags = (ssml: string): string => ssml.replace(/<[^>]+\/?>/g, '').trim();
|
||||
|
||||
const highlight = vi.fn();
|
||||
|
||||
/**
|
||||
* Build a document that mimics a rendered PDF page with text layer,
|
||||
* matching the structure that pdf.js produces in the iframe.
|
||||
*/
|
||||
const createPDFTextLayerDoc = (textSpans: string[], annotationText?: string): Document => {
|
||||
const parser = new DOMParser();
|
||||
const spans = textSpans.map((t) => `<span>${t}</span>`).join('');
|
||||
const annotation = annotationText
|
||||
? `<div class="annotationLayer"><a href="#">${annotationText}</a></div>`
|
||||
: '<div class="annotationLayer"></div>';
|
||||
const html =
|
||||
`<!DOCTYPE html><html lang="en">` +
|
||||
`<body>` +
|
||||
`<div id="canvas"><canvas></canvas></div>` +
|
||||
`<div class="textLayer">${spans}</div>` +
|
||||
`${annotation}` +
|
||||
`</body></html>`;
|
||||
return parser.parseFromString(html, 'text/html');
|
||||
};
|
||||
|
||||
/** Node filter matching what TTSController uses for PDFs */
|
||||
const pdfNodeFilter = createRejectFilter({
|
||||
tags: ['rt', 'canvas'],
|
||||
classes: ['annotationLayer'],
|
||||
contents: [{ tag: 'a', content: /^[\[\(]?[\*\d]+[\)\]]?$/ }],
|
||||
});
|
||||
|
||||
describe('PDF TTS', () => {
|
||||
describe('TTS with PDF text layer document', () => {
|
||||
it('should generate SSML from text layer spans', () => {
|
||||
const doc = createPDFTextLayerDoc(['Alice was beginning to get very ', 'tired of sitting']);
|
||||
const tts = new TTS(doc, textWalker, pdfNodeFilter, highlight, 'word');
|
||||
const ssml = tts.start();
|
||||
|
||||
expect(ssml).toBeTruthy();
|
||||
const text = stripTags(ssml!);
|
||||
expect(text).toContain('Alice');
|
||||
expect(text).toContain('tired');
|
||||
});
|
||||
|
||||
it('should filter out canvas content', () => {
|
||||
const doc = createPDFTextLayerDoc(['Hello world']);
|
||||
const tts = new TTS(doc, textWalker, pdfNodeFilter, highlight, 'word');
|
||||
const ssml = tts.start();
|
||||
|
||||
expect(ssml).toBeTruthy();
|
||||
expect(stripTags(ssml!)).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('should filter out annotation layer text', () => {
|
||||
const doc = createPDFTextLayerDoc(['Main text content'], 'Link annotation text');
|
||||
const tts = new TTS(doc, textWalker, pdfNodeFilter, highlight, 'word');
|
||||
|
||||
const allText: string[] = [];
|
||||
let ssml = tts.start();
|
||||
while (ssml) {
|
||||
allText.push(stripTags(ssml));
|
||||
ssml = tts.next();
|
||||
}
|
||||
const combined = allText.join(' ');
|
||||
|
||||
expect(combined).toContain('Main text content');
|
||||
expect(combined).not.toContain('Link annotation text');
|
||||
});
|
||||
|
||||
it('should produce valid SSML with speak root element', () => {
|
||||
const doc = createPDFTextLayerDoc(['Test content']);
|
||||
const tts = new TTS(doc, textWalker, pdfNodeFilter, highlight, 'word');
|
||||
const ssml = tts.start();
|
||||
|
||||
expect(ssml).toBeTruthy();
|
||||
expect(ssml).toContain('<speak');
|
||||
expect(ssml).toContain('</speak>');
|
||||
});
|
||||
|
||||
it('should include mark elements in SSML output', () => {
|
||||
const doc = createPDFTextLayerDoc(['Some text with multiple words']);
|
||||
const tts = new TTS(doc, textWalker, pdfNodeFilter, highlight, 'word');
|
||||
const ssml = tts.start();
|
||||
|
||||
expect(ssml).toBeTruthy();
|
||||
expect(ssml).toContain('<mark');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PDF node filter', () => {
|
||||
it('should reject canvas elements', () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
expect(pdfNodeFilter(canvas)).toBe(NodeFilter.FILTER_REJECT);
|
||||
});
|
||||
|
||||
it('should reject annotationLayer elements', () => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'annotationLayer';
|
||||
expect(pdfNodeFilter(div)).toBe(NodeFilter.FILTER_REJECT);
|
||||
});
|
||||
|
||||
it('should reject rt elements', () => {
|
||||
const rt = document.createElement('rt');
|
||||
expect(pdfNodeFilter(rt)).toBe(NodeFilter.FILTER_REJECT);
|
||||
});
|
||||
|
||||
it('should skip regular div elements', () => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'textLayer';
|
||||
expect(pdfNodeFilter(div)).toBe(NodeFilter.FILTER_SKIP);
|
||||
});
|
||||
|
||||
it('should accept text nodes', () => {
|
||||
const text = document.createTextNode('hello');
|
||||
expect(pdfNodeFilter(text)).toBe(NodeFilter.FILTER_ACCEPT);
|
||||
});
|
||||
|
||||
it('should reject footnote-like anchor content', () => {
|
||||
const a = document.createElement('a');
|
||||
a.textContent = '[1]';
|
||||
expect(pdfNodeFilter(a)).toBe(NodeFilter.FILTER_REJECT);
|
||||
});
|
||||
|
||||
it('should not reject normal anchor content', () => {
|
||||
const a = document.createElement('a');
|
||||
a.textContent = 'Click here for more';
|
||||
expect(pdfNodeFilter(a)).toBe(NodeFilter.FILTER_SKIP);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DocumentLoader with sample-alice.pdf', () => {
|
||||
let book: BookDoc;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Override workerSrc to an absolute file path so the pdfjs fake-worker
|
||||
// can import it inside jsdom (the module-level code in pdf.js sets it
|
||||
// to a URL path that only works in a real browser).
|
||||
// Import pdf.js first to trigger the @pdfjs side-effect that sets globalThis.pdfjsLib.
|
||||
await import('foliate-js/pdf.js');
|
||||
const pdfjsLib = (globalThis as Record<string, unknown>)['pdfjsLib'] as {
|
||||
GlobalWorkerOptions: { workerSrc: string };
|
||||
};
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
`file://${join(vendorDir, 'pdfjs/pdf.worker.min.mjs')}`,
|
||||
).href;
|
||||
|
||||
const pdfPath = resolve(__dirname, '../fixtures/data/sample-alice.pdf');
|
||||
const buffer = readFileSync(pdfPath);
|
||||
const file = new File([buffer], 'sample-alice.pdf', { type: 'application/pdf' });
|
||||
const loader = new DocumentLoader(file);
|
||||
const result = await loader.open();
|
||||
book = result.book;
|
||||
expect(result.format).toBe('PDF');
|
||||
}, 30_000);
|
||||
|
||||
it('should load the sample PDF and return a book object', () => {
|
||||
expect(book).toBeTruthy();
|
||||
expect(book.rendition.layout).toBe('pre-paginated');
|
||||
});
|
||||
|
||||
it('should have sections matching the number of pages', () => {
|
||||
expect(book.sections).toBeTruthy();
|
||||
expect(book.sections.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should extract metadata', () => {
|
||||
expect(book.metadata).toBeTruthy();
|
||||
// sample-alice.pdf should have a title
|
||||
expect(book.metadata.title).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should provide createDocument on every section', () => {
|
||||
for (const section of book.sections) {
|
||||
expect(typeof section.createDocument).toBe('function');
|
||||
}
|
||||
});
|
||||
|
||||
it('should generate TTS SSML from createDocument output', async () => {
|
||||
const doc = await book.sections[0]!.createDocument();
|
||||
const tts = new TTS(doc, textWalker, undefined, highlight, 'word');
|
||||
const ssml = tts.start();
|
||||
|
||||
expect(ssml).toBeTruthy();
|
||||
expect(ssml).toContain('<speak');
|
||||
expect(ssml).toContain('<mark');
|
||||
|
||||
const text = stripTags(ssml!);
|
||||
expect(text.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should navigate through all TTS blocks of a page', async () => {
|
||||
const doc = await book.sections[0]!.createDocument();
|
||||
const tts = new TTS(doc, textWalker, undefined, highlight, 'word');
|
||||
|
||||
const blocks: string[] = [];
|
||||
let ssml = tts.start();
|
||||
while (ssml) {
|
||||
blocks.push(stripTags(ssml));
|
||||
ssml = tts.next();
|
||||
}
|
||||
|
||||
expect(blocks.length).toBeGreaterThan(0);
|
||||
for (const block of blocks) {
|
||||
expect(block.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should produce createDocument output for multiple pages', async () => {
|
||||
const pagesToTest = Math.min(book.sections.length, 3);
|
||||
|
||||
for (let i = 0; i < pagesToTest; i++) {
|
||||
const doc = await book.sections[i]!.createDocument();
|
||||
expect(doc).toBeTruthy();
|
||||
|
||||
const tts = new TTS(doc, textWalker, undefined, highlight, 'word');
|
||||
const ssml = tts.start();
|
||||
expect(ssml).toBeTruthy();
|
||||
expect(stripTags(ssml!).length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return consistent text across repeated createDocument calls', async () => {
|
||||
const doc1 = await book.sections[0]!.createDocument();
|
||||
const doc2 = await book.sections[0]!.createDocument();
|
||||
|
||||
const tts1 = new TTS(doc1, textWalker, undefined, highlight, 'word');
|
||||
const tts2 = new TTS(doc2, textWalker, undefined, highlight, 'word');
|
||||
|
||||
expect(stripTags(tts1.start()!)).toBe(stripTags(tts2.start()!));
|
||||
});
|
||||
|
||||
it('should work with sentence granularity on real PDF content', async () => {
|
||||
const doc = await book.sections[0]!.createDocument();
|
||||
const tts = new TTS(doc, textWalker, undefined, highlight, 'sentence');
|
||||
const ssml = tts.start();
|
||||
|
||||
expect(ssml).toBeTruthy();
|
||||
expect(stripTags(ssml!).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should call highlight callback when marking words from PDF', async () => {
|
||||
highlight.mockClear();
|
||||
const doc = await book.sections[0]!.createDocument();
|
||||
const tts = new TTS(doc, textWalker, undefined, highlight, 'word');
|
||||
tts.start();
|
||||
|
||||
const range = tts.setMark('0');
|
||||
expect(range).toBeTruthy();
|
||||
expect(highlight).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,18 +1,7 @@
|
|||
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { textWalker } from 'foliate-js/text-walker.js';
|
||||
import { TTS } from 'foliate-js/tts.js';
|
||||
|
||||
beforeAll(() => {
|
||||
if (typeof CSS === 'undefined' || !CSS.escape) {
|
||||
Object.defineProperty(globalThis, 'CSS', {
|
||||
value: {
|
||||
escape: (s: string) => s.replace(/([^\w-])/g, '\\$1'),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const createHTMLDoc = (bodyHTML: string, attrs: Record<string, string> = {}): Document => {
|
||||
const parser = new DOMParser();
|
||||
const attrStr = Object.entries(attrs)
|
||||
|
|
|
|||
BIN
apps/readest-app/src/__tests__/fixtures/data/sample-alice.pdf
Normal file
BIN
apps/readest-app/src/__tests__/fixtures/data/sample-alice.pdf
Normal file
Binary file not shown.
BIN
apps/readest-app/src/__tests__/fixtures/data/sample-paper.pdf
Normal file
BIN
apps/readest-app/src/__tests__/fixtures/data/sample-paper.pdf
Normal file
Binary file not shown.
|
|
@ -171,9 +171,11 @@ describe('TxtToEpubConverter', () => {
|
|||
const reader = new ZipReader(new BlobReader(blob));
|
||||
try {
|
||||
const entries = await reader.getEntries();
|
||||
const chapterEntry = entries.find((entry) => entry.filename === 'OEBPS/chapter1.xhtml');
|
||||
const chapterEntry = entries.find((entry) => entry.filename === 'OEBPS/chapter1.xhtml') as {
|
||||
getData?: (writer: unknown) => Promise<string>;
|
||||
};
|
||||
expect(chapterEntry).toBeDefined();
|
||||
const chapterContent = await chapterEntry!.getData(new TextWriter());
|
||||
const chapterContent = await chapterEntry?.getData?.(new TextWriter());
|
||||
expect(chapterContent).toContain('lang="zh"');
|
||||
expect(chapterContent).toContain('xml:lang="zh"');
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -657,7 +657,7 @@ const FoliateViewer: React.FC<{
|
|||
role='document'
|
||||
aria-label={_('Book Content')}
|
||||
className={clsx(
|
||||
'foliate-viewer h-[100%] w-[100%] focus:outline-none',
|
||||
'foliate-viewer absolute h-[100%] w-[100%] focus:outline-none',
|
||||
viewState?.loading && 'bg-base-100',
|
||||
)}
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey, setIsDropdownOpen }) => {
|
|||
)}
|
||||
onClick={resetZoom}
|
||||
>
|
||||
{zoomLevel}%
|
||||
{Math.round(zoomLevel)}%
|
||||
</button>
|
||||
<button
|
||||
title={_('Zoom In')}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { findTocItemBS } from '@/utils/toc';
|
|||
import { throttle } from '@/utils/throttle';
|
||||
import { runSimpleCC } from '@/utils/simplecc';
|
||||
import { getWordCount } from '@/utils/word';
|
||||
import { isCfiInLocation } from '@/utils/cfi';
|
||||
import { getIndexFromCfi, isCfiInLocation } from '@/utils/cfi';
|
||||
import { TransformContext } from '@/services/transformers/types';
|
||||
import { transformContent } from '@/services/transformService';
|
||||
import { getHighlightColorHex } from '../../utils/annotatorUtil';
|
||||
|
|
@ -273,7 +273,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
|||
detail.doc?.addEventListener('selectionchange', handleSelectionchange.bind(null, doc, index));
|
||||
|
||||
// For PDF selections, enable right-click context menu to directly open translator popup.
|
||||
if (bookData.book?.format === 'PDF') {
|
||||
if (bookData.isFixedLayout) {
|
||||
detail.doc?.addEventListener('contextmenu', (e: Event) => {
|
||||
try {
|
||||
const sel = doc.getSelection?.();
|
||||
|
|
@ -281,7 +281,14 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
|||
const range = sel.getRangeAt(0);
|
||||
const text = sel.toString();
|
||||
if (text.trim()) {
|
||||
setSelection({ key: bookKey, text, range, index, cfi: view?.getCFI(index, range) });
|
||||
setSelection({
|
||||
key: bookKey,
|
||||
text,
|
||||
range,
|
||||
index,
|
||||
cfi: view?.getCFI(index, range),
|
||||
page: index + 1,
|
||||
});
|
||||
// Show translation popup preferentially for PDF right-click
|
||||
setShowAnnotPopup(false);
|
||||
setShowDeepLPopup(true);
|
||||
|
|
@ -303,6 +310,22 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
|||
detail.doc?.addEventListener('contextmenu', handleContextmenu);
|
||||
};
|
||||
|
||||
const onCreateOverlay = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
const { booknotes = [] } = getConfig(bookKey)!;
|
||||
booknotes
|
||||
.filter(
|
||||
(booknote) =>
|
||||
booknote.type === 'annotation' &&
|
||||
!booknote.deletedAt &&
|
||||
getIndexFromCfi(booknote.cfi) === detail.index,
|
||||
)
|
||||
.map((annotation) => {
|
||||
console.log('Adding annotation to overlay', annotation);
|
||||
view?.addAnnotation(annotation);
|
||||
});
|
||||
};
|
||||
|
||||
const onDrawAnnotation = (event: Event) => {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const isBwEink = viewSettings.isEink && !viewSettings.isColorEink;
|
||||
|
|
@ -366,8 +389,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
|||
note: note ?? '',
|
||||
rect: isNote ? detail.rect : undefined,
|
||||
cfi,
|
||||
range,
|
||||
index,
|
||||
range,
|
||||
page: annotation.page || progress.page,
|
||||
};
|
||||
if (isNote) {
|
||||
setShowAnnotationNotes(true);
|
||||
|
|
@ -390,7 +414,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
|||
handleUpToPopup();
|
||||
};
|
||||
|
||||
useFoliateEvents(view, { onLoad, onDrawAnnotation, onShowAnnotation });
|
||||
useFoliateEvents(view, { onLoad, onCreateOverlay, onDrawAnnotation, onShowAnnotation });
|
||||
|
||||
useEffect(() => {
|
||||
handleShowPopup(showingPopup);
|
||||
|
|
@ -608,9 +632,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
|||
id: uniqueId(),
|
||||
type: 'excerpt',
|
||||
cfi,
|
||||
text: selection.text,
|
||||
note: '',
|
||||
page: progress.page,
|
||||
text: selection.text,
|
||||
page: selection.page,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
|
@ -729,7 +753,12 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
|||
if (!selection || !selection.text) return;
|
||||
setShowAnnotPopup(false);
|
||||
setEditingAnnotation(null);
|
||||
eventDispatcher.dispatch('tts-speak', { bookKey, range: selection.range, oneTime });
|
||||
eventDispatcher.dispatch('tts-speak', {
|
||||
bookKey,
|
||||
oneTime,
|
||||
range: selection.range,
|
||||
index: selection.index,
|
||||
});
|
||||
};
|
||||
|
||||
const handleProofread = () => {
|
||||
|
|
@ -867,14 +896,12 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
|||
tooltipText: selectionAnnotated ? _('Delete Highlight') : _(label),
|
||||
Icon: selectionAnnotated ? RiDeleteBinLine : Icon,
|
||||
onClick: handleHighlight,
|
||||
disabled: bookData.book?.format === 'PDF',
|
||||
};
|
||||
case 'annotate':
|
||||
return {
|
||||
tooltipText: _(label),
|
||||
Icon,
|
||||
onClick: handleAnnotate,
|
||||
disabled: bookData.book?.format === 'PDF',
|
||||
};
|
||||
case 'search':
|
||||
return {
|
||||
|
|
@ -894,7 +921,6 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
|||
tooltipText: _(label),
|
||||
Icon,
|
||||
onClick: handleSpeakText,
|
||||
disabled: bookData.book?.format === 'PDF',
|
||||
};
|
||||
case 'proofread':
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -129,7 +129,6 @@ const Notebook: React.FC = ({}) => {
|
|||
if (!sideBarBookKey) return;
|
||||
const view = getView(sideBarBookKey);
|
||||
const config = getConfig(sideBarBookKey)!;
|
||||
const progress = getProgress(sideBarBookKey)!;
|
||||
|
||||
const cfi = view?.getCFI(selection.index, selection.range);
|
||||
if (!cfi) return;
|
||||
|
|
@ -140,7 +139,7 @@ const Notebook: React.FC = ({}) => {
|
|||
type: 'annotation',
|
||||
cfi,
|
||||
note,
|
||||
page: progress.page,
|
||||
page: selection.page,
|
||||
text: selection.text,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export const useAnnotationEditor = ({
|
|||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getConfig, saveConfig, updateBooknotes } = useBookDataStore();
|
||||
const { getView, getViewsById } = useReaderStore();
|
||||
const { getView, getProgress, getViewsById } = useReaderStore();
|
||||
|
||||
const view = getView(bookKey);
|
||||
const editingAnnotationRef = useRef(annotation);
|
||||
|
|
@ -144,14 +144,16 @@ export const useAnnotationEditor = ({
|
|||
|
||||
if (newCfi && newText) {
|
||||
const config = getConfig(bookKey)!;
|
||||
const progress = getProgress(bookKey)!;
|
||||
const { booknotes: annotations = [] } = config;
|
||||
const existingIndex = annotations.findIndex(
|
||||
(a) => a.id === editingAnnotationRef.current.id && !a.deletedAt,
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
const existingAnnotation = annotations[existingIndex]!;
|
||||
const updatedAnnotation: BookNote = {
|
||||
...annotations[existingIndex]!,
|
||||
...existingAnnotation,
|
||||
cfi: newCfi,
|
||||
text: newText,
|
||||
updatedAt: Date.now(),
|
||||
|
|
@ -174,8 +176,9 @@ export const useAnnotationEditor = ({
|
|||
annotated: true,
|
||||
text: newText,
|
||||
cfi: newCfi,
|
||||
range: newRange,
|
||||
index: targetIndex,
|
||||
range: newRange,
|
||||
page: existingAnnotation.page || progress.page,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ type FoliateEventHandler = {
|
|||
onRelocate?: (event: Event) => void;
|
||||
onLinkClick?: (event: Event) => void;
|
||||
onRendererRelocate?: (event: Event) => void;
|
||||
onCreateOverlay?: (event: Event) => void;
|
||||
onDrawAnnotation?: (event: Event) => void;
|
||||
onShowAnnotation?: (event: Event) => void;
|
||||
};
|
||||
|
|
@ -15,6 +16,7 @@ export const useFoliateEvents = (view: FoliateView | null, handlers?: FoliateEve
|
|||
const onRelocate = handlers?.onRelocate;
|
||||
const onLinkClick = handlers?.onLinkClick;
|
||||
const onRendererRelocate = handlers?.onRendererRelocate;
|
||||
const onCreateOverlay = handlers?.onCreateOverlay;
|
||||
const onDrawAnnotation = handlers?.onDrawAnnotation;
|
||||
const onShowAnnotation = handlers?.onShowAnnotation;
|
||||
|
||||
|
|
@ -24,6 +26,7 @@ export const useFoliateEvents = (view: FoliateView | null, handlers?: FoliateEve
|
|||
if (onRelocate) view.addEventListener('relocate', onRelocate);
|
||||
if (onLinkClick) view.addEventListener('link', onLinkClick);
|
||||
if (onRendererRelocate) view.renderer.addEventListener('relocate', onRendererRelocate);
|
||||
if (onCreateOverlay) view.addEventListener('create-overlay', onCreateOverlay);
|
||||
if (onDrawAnnotation) view.addEventListener('draw-annotation', onDrawAnnotation);
|
||||
if (onShowAnnotation) view.addEventListener('show-annotation', onShowAnnotation);
|
||||
|
||||
|
|
@ -32,6 +35,7 @@ export const useFoliateEvents = (view: FoliateView | null, handlers?: FoliateEve
|
|||
if (onRelocate) view.removeEventListener('relocate', onRelocate);
|
||||
if (onLinkClick) view.removeEventListener('link', onLinkClick);
|
||||
if (onRendererRelocate) view.renderer.removeEventListener('relocate', onRendererRelocate);
|
||||
if (onCreateOverlay) view.removeEventListener('create-overlay', onCreateOverlay);
|
||||
if (onDrawAnnotation) view.removeEventListener('draw-annotation', onDrawAnnotation);
|
||||
if (onShowAnnotation) view.removeEventListener('show-annotation', onShowAnnotation);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ export const useParagraphMode = ({ bookKey, viewRef }: UseParagraphModeProps) =>
|
|||
if (docIndex !== undefined && renderer.goTo) {
|
||||
renderer.goTo({ index: docIndex, anchor: range });
|
||||
} else {
|
||||
view.renderer.scrollToAnchor(range);
|
||||
view.renderer.scrollToAnchor?.(range);
|
||||
}
|
||||
focusResetTimerRef.current = setTimeout(() => {
|
||||
isFocusingRef.current = false;
|
||||
|
|
@ -382,7 +382,7 @@ export const useParagraphMode = ({ bookKey, viewRef }: UseParagraphModeProps) =>
|
|||
docIndex,
|
||||
};
|
||||
}
|
||||
view.renderer.scrollToAnchor(range);
|
||||
view.renderer.scrollToAnchor?.(range);
|
||||
}
|
||||
}
|
||||
eventDispatcher.dispatch('paragraph-mode-disabled', { bookKey: bookKeyRef.current });
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp
|
|||
|
||||
const range = anchor(doc);
|
||||
if (!view.renderer.scrolled) {
|
||||
view.renderer.scrollToAnchor(range);
|
||||
view.renderer.scrollToAnchor?.(range);
|
||||
} else {
|
||||
const rect = range.getBoundingClientRect();
|
||||
const { start, size, viewSize, sideProp } = view.renderer;
|
||||
|
|
@ -162,7 +162,7 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp
|
|||
const startInPrevView = offsetStart < start + headerScrollOverlap + scrollingOverlap;
|
||||
if (endInNextView || startInPrevView) {
|
||||
const scrollTo = offsetStart - headerScrollOverlap - scrollingOverlap;
|
||||
view.renderer.scrollToAnchor(scrollTo / viewSize);
|
||||
view.renderer.scrollToAnchor?.(scrollTo / viewSize);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -319,7 +319,7 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp
|
|||
useEffect(() => {
|
||||
const ttsHighlightOptions = viewSettings?.ttsHighlightOptions;
|
||||
if (ttsControllerRef.current && ttsHighlightOptions) {
|
||||
ttsControllerRef.current.initViewTTS(
|
||||
ttsControllerRef.current.updateHighlightOptions(
|
||||
getTTSHighlightOptions(ttsHighlightOptions, viewSettings!.isEink),
|
||||
);
|
||||
}
|
||||
|
|
@ -355,7 +355,7 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp
|
|||
|
||||
// handleTTSSpeak / handleTTSStop (plain functions, registered once at mount via closure)
|
||||
const handleTTSSpeak = async (event: CustomEvent) => {
|
||||
const { bookKey: ttsBookKey, range, oneTime = false } = event.detail;
|
||||
const { bookKey: ttsBookKey, range, index, oneTime = false } = event.detail;
|
||||
if (bookKey !== ttsBookKey) return;
|
||||
|
||||
const view = getView(bookKey);
|
||||
|
|
@ -364,16 +364,9 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp
|
|||
const bookData = getBookData(bookKey);
|
||||
const { location } = progress || {};
|
||||
if (!view || !progress || !viewSettings || !bookData || !bookData.book) return;
|
||||
if (bookData.book?.format === 'PDF') {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message: _('TTS not supported for PDF'),
|
||||
type: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const ttsSpeakRange = range as Range | null;
|
||||
let ttsFromRange = ttsSpeakRange;
|
||||
let ttsFromIndex = typeof index === 'number' ? index : null;
|
||||
if (!ttsFromRange && viewSettings.ttsLocation) {
|
||||
const ttsCfi = viewSettings.ttsLocation;
|
||||
if (isCfiInLocation(ttsCfi, location)) {
|
||||
|
|
@ -381,14 +374,16 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp
|
|||
const { doc } = view.renderer.getContents().find((x) => x.index === index) || {};
|
||||
if (doc) {
|
||||
ttsFromRange = anchor(doc);
|
||||
ttsFromIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ttsFromRange) {
|
||||
if (!ttsFromRange || !ttsFromIndex) {
|
||||
ttsFromRange = progress.range;
|
||||
ttsFromIndex = progress.index;
|
||||
}
|
||||
|
||||
const currentSection = view.renderer.getContents()[0];
|
||||
const currentSection = view.renderer.getContents().find((x) => x.index === ttsFromIndex);
|
||||
if (ttsFromRange && currentSection) {
|
||||
const ttsLocation = view.getCFI(currentSection?.index || 0, ttsFromRange);
|
||||
viewSettings.ttsLocation = ttsLocation;
|
||||
|
|
@ -427,7 +422,8 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp
|
|||
setTtsController(ttsController);
|
||||
|
||||
await ttsController.init();
|
||||
await ttsController.initViewTTS(
|
||||
await ttsController.initViewTTS(ttsFromIndex);
|
||||
ttsController.updateHighlightOptions(
|
||||
getTTSHighlightOptions(viewSettings.ttsHighlightOptions, viewSettings.isEink),
|
||||
);
|
||||
const ssml =
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { isPointerInsideSelection, TextSelection } from '@/utils/sel';
|
||||
|
|
@ -13,8 +14,11 @@ export const useTextSelector = (
|
|||
handleDismissPopup: () => void,
|
||||
) => {
|
||||
const { appService } = useEnv();
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { getView, getViewSettings, getProgress } = useReaderStore();
|
||||
const view = getView(bookKey);
|
||||
const bookData = getBookData(bookKey);
|
||||
const progress = getProgress(bookKey)!;
|
||||
const osPlatform = getOSPlatform();
|
||||
|
||||
const isPopuped = useRef(false);
|
||||
|
|
@ -49,6 +53,7 @@ export const useTextSelector = (
|
|||
key: bookKey,
|
||||
text: await getAnnotationText(range),
|
||||
cfi: view?.getCFI(index, range),
|
||||
page: bookData?.isFixedLayout ? index + 1 : progress.page,
|
||||
range,
|
||||
index,
|
||||
});
|
||||
|
|
@ -66,6 +71,7 @@ export const useTextSelector = (
|
|||
key: bookKey,
|
||||
text: await getAnnotationText(range),
|
||||
cfi: view?.getCFI(index, range),
|
||||
page: bookData?.isFixedLayout ? index + 1 : progress.page,
|
||||
range,
|
||||
index,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -65,14 +65,14 @@ export type BookMetadata = {
|
|||
|
||||
export interface BookDoc {
|
||||
metadata: BookMetadata;
|
||||
rendition?: {
|
||||
rendition: {
|
||||
layout?: 'pre-paginated' | 'reflowable';
|
||||
spread?: 'auto' | 'none';
|
||||
viewport?: { width: number; height: number };
|
||||
};
|
||||
dir: string;
|
||||
toc?: Array<TOCItem>;
|
||||
sections?: Array<SectionItem>;
|
||||
sections: Array<SectionItem>;
|
||||
transformTarget?: EventTarget;
|
||||
splitTOCHref(href: string): Array<string | number>;
|
||||
getCover(): Promise<Blob | null>;
|
||||
|
|
|
|||
|
|
@ -113,7 +113,9 @@ export class TTSController extends EventTarget {
|
|||
const { style, color } = this.options;
|
||||
overlayer?.remove(HIGHLIGHT_KEY);
|
||||
overlayer?.add(HIGHLIGHT_KEY, visibleRange, Overlayer[style], { color });
|
||||
} catch {}
|
||||
} catch (e) {
|
||||
console.error('Failed to highlight range', e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -122,14 +124,15 @@ export class TTSController extends EventTarget {
|
|||
overlayer?.remove(HIGHLIGHT_KEY);
|
||||
}
|
||||
|
||||
async initViewTTS(options?: TTSHighlightOptions) {
|
||||
if (options) {
|
||||
this.options.style = options.style;
|
||||
this.options.color = options.color;
|
||||
}
|
||||
const currentSectionIndex = this.view.renderer.getContents()[0]?.index ?? 0;
|
||||
updateHighlightOptions(options: TTSHighlightOptions) {
|
||||
this.options.style = options.style;
|
||||
this.options.color = options.color;
|
||||
}
|
||||
|
||||
async initViewTTS(index?: number) {
|
||||
if (this.#ttsSectionIndex === -1) {
|
||||
await this.#initTTSForSection(currentSectionIndex);
|
||||
const fromSectionIndex = (index || this.view.renderer.getContents()[0]?.index) ?? 0;
|
||||
await this.#initTTSForSection(fromSectionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -362,13 +362,13 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
|||
location,
|
||||
sectionHref: tocItem?.href,
|
||||
sectionLabel: tocItem?.label,
|
||||
sectionId: tocItem?.id,
|
||||
section,
|
||||
pageinfo,
|
||||
timeinfo,
|
||||
index: section.current,
|
||||
range,
|
||||
page: pageInfo.current + 1, // 1-based page number
|
||||
},
|
||||
} as BookProgress,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -307,12 +307,12 @@ export interface ViewSettings
|
|||
|
||||
export interface BookProgress {
|
||||
location: string;
|
||||
sectionId: number;
|
||||
sectionHref: string;
|
||||
sectionLabel: string;
|
||||
section: PageInfo;
|
||||
pageinfo: PageInfo;
|
||||
timeinfo: TimeInfo;
|
||||
index: number;
|
||||
range: Range;
|
||||
page: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export interface FoliateView extends HTMLElement {
|
|||
) => Promise<void>;
|
||||
book: BookDoc;
|
||||
tts: TTS | null;
|
||||
isFixedLayout: boolean;
|
||||
language: {
|
||||
locale?: LocaleWithTextInfo;
|
||||
isCJK?: boolean;
|
||||
|
|
@ -80,7 +81,7 @@ export interface FoliateView extends HTMLElement {
|
|||
goTo?: (params: { index: number; anchor?: number | RangeAnchor }) => void;
|
||||
setStyles?: (css: string) => void;
|
||||
getContents: () => { doc: Document; index?: number; overlayer?: unknown }[];
|
||||
scrollToAnchor: (anchor: number | Range) => void;
|
||||
scrollToAnchor?: (anchor: number | Range) => void;
|
||||
addEventListener: (
|
||||
type: string,
|
||||
listener: EventListener,
|
||||
|
|
|
|||
|
|
@ -8,3 +8,12 @@ export function isCfiInLocation(cfi: string, location: string | null | undefined
|
|||
|
||||
return CFI.compare(cfi, start) >= 0 && CFI.compare(cfi, end) <= 0;
|
||||
}
|
||||
|
||||
export function getIndexFromCfi(cfi: string): number | null {
|
||||
try {
|
||||
const parts = CFI.parse(cfi);
|
||||
return CFI.fake.toIndex((parts.parent ?? parts).shift());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import nunjucks from 'nunjucks';
|
|||
export type NoteTemplateData = {
|
||||
title: string;
|
||||
author: string;
|
||||
exportDate: number;
|
||||
exportDate: number | string;
|
||||
chapters: {
|
||||
title: string;
|
||||
annotations: {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export interface Position {
|
|||
export interface TextSelection {
|
||||
key: string;
|
||||
text: string;
|
||||
page: number;
|
||||
range: Range;
|
||||
index: number;
|
||||
cfi?: string;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
import path from 'path';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths(), react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
// The @pdfjs alias from tsconfig only resolves within the app's own
|
||||
// source files. foliate-js/pdf.js lives outside that scope, so Vite
|
||||
// needs an explicit alias to find the vendored pdfjs build.
|
||||
'@pdfjs': path.resolve(__dirname, 'public/vendor/pdfjs'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
|
|
|
|||
|
|
@ -1,49 +1,3 @@
|
|||
// localStorage mock
|
||||
if (typeof window !== 'undefined' && !window.localStorage) {
|
||||
const storage: Record<string, string> = {};
|
||||
window.localStorage = {
|
||||
getItem: (key: string) => storage[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
storage[key] = value;
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete storage[key];
|
||||
},
|
||||
clear: () => {
|
||||
Object.keys(storage).forEach((key) => delete storage[key]);
|
||||
},
|
||||
get length() {
|
||||
return Object.keys(storage).length;
|
||||
},
|
||||
key: (index: number) => {
|
||||
const keys = Object.keys(storage);
|
||||
return keys[index] || null;
|
||||
},
|
||||
} as Storage;
|
||||
} else if (typeof window !== 'undefined' && window.localStorage && !window.localStorage.getItem) {
|
||||
// If localStorage exists but getItem is not a function, replace it
|
||||
const storage: Record<string, string> = {};
|
||||
window.localStorage = {
|
||||
getItem: (key: string) => storage[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
storage[key] = value;
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete storage[key];
|
||||
},
|
||||
clear: () => {
|
||||
Object.keys(storage).forEach((key) => delete storage[key]);
|
||||
},
|
||||
get length() {
|
||||
return Object.keys(storage).length;
|
||||
},
|
||||
key: (index: number) => {
|
||||
const keys = Object.keys(storage);
|
||||
return keys[index] || null;
|
||||
},
|
||||
} as Storage;
|
||||
}
|
||||
|
||||
// matchMedia mock
|
||||
if (typeof window !== 'undefined' && !window.matchMedia) {
|
||||
window.matchMedia = (query: string) =>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit e327bfd1ea396f43b0362da5c14d3ee0a416ca91
|
||||
Subproject commit b00c540fbcb9a778e1d6c66bf163124a7e313182
|
||||
325
pnpm-lock.yaml
generated
325
pnpm-lock.yaml
generated
|
|
@ -496,8 +496,8 @@ importers:
|
|||
specifier: ^4.6.0
|
||||
version: 4.6.0(typescript@5.9.3)
|
||||
jsdom:
|
||||
specifier: ^26.1.0
|
||||
version: 26.1.0
|
||||
specifier: ^28.1.0
|
||||
version: 28.1.0(@noble/hashes@1.8.0)
|
||||
mkdirp:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
|
|
@ -542,7 +542,7 @@ importers:
|
|||
version: 5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vitest:
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(@vitest/browser-playwright@4.0.18)(@vitest/browser-webdriverio@4.0.18)(jiti@1.21.7)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(@vitest/browser-playwright@4.0.18)(@vitest/browser-webdriverio@4.0.18)(jiti@1.21.7)(jsdom@28.1.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
wrangler:
|
||||
specifier: ^4.60.0
|
||||
version: 4.60.0
|
||||
|
|
@ -583,6 +583,9 @@ importers:
|
|||
|
||||
packages:
|
||||
|
||||
'@acemir/cssom@0.9.31':
|
||||
resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==}
|
||||
|
||||
'@ai-sdk/gateway@2.0.29':
|
||||
resolution: {integrity: sha512-1b7E9F/B5gex/1uCkhs+sGIbH0KsZOItHnNz3iY5ir+nc4ZUA6WOU5Cu2w1USlc+3UVbhf+H+iNLlxVjLe4VvQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -636,8 +639,15 @@ packages:
|
|||
'@antfu/install-pkg@1.1.0':
|
||||
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
|
||||
'@asamuzakjp/css-color@5.0.1':
|
||||
resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
'@asamuzakjp/dom-selector@6.8.1':
|
||||
resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==}
|
||||
|
||||
'@asamuzakjp/nwsapi@2.3.9':
|
||||
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
|
||||
|
||||
'@assistant-ui/react-ai-sdk@1.1.21':
|
||||
resolution: {integrity: sha512-TRJx6fDoIqUpkl1LuRZB2QHGZ7pXAc4qtX05h+z0yYIMY4Y2/XzwxlitpcS+NSo1PS8YCgZaM9HO1y66H/il2Q==}
|
||||
|
|
@ -1233,6 +1243,10 @@ packages:
|
|||
'@braintree/sanitize-url@7.1.1':
|
||||
resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==}
|
||||
|
||||
'@bramus/specificity@2.4.2':
|
||||
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
|
||||
hasBin: true
|
||||
|
||||
'@chevrotain/cst-dts-gen@11.0.3':
|
||||
resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==}
|
||||
|
||||
|
|
@ -1298,33 +1312,36 @@ packages:
|
|||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@csstools/color-helpers@5.1.0':
|
||||
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
|
||||
engines: {node: '>=18'}
|
||||
'@csstools/color-helpers@6.0.2':
|
||||
resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
'@csstools/css-calc@2.1.4':
|
||||
resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
|
||||
engines: {node: '>=18'}
|
||||
'@csstools/css-calc@3.1.1':
|
||||
resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@csstools/css-parser-algorithms': ^3.0.5
|
||||
'@csstools/css-tokenizer': ^3.0.4
|
||||
'@csstools/css-parser-algorithms': ^4.0.0
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-color-parser@3.1.0':
|
||||
resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
|
||||
engines: {node: '>=18'}
|
||||
'@csstools/css-color-parser@4.0.2':
|
||||
resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@csstools/css-parser-algorithms': ^3.0.5
|
||||
'@csstools/css-tokenizer': ^3.0.4
|
||||
'@csstools/css-parser-algorithms': ^4.0.0
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-parser-algorithms@3.0.5':
|
||||
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
|
||||
engines: {node: '>=18'}
|
||||
'@csstools/css-parser-algorithms@4.0.0':
|
||||
resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@csstools/css-tokenizer': ^3.0.4
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-tokenizer@3.0.4':
|
||||
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
|
||||
engines: {node: '>=18'}
|
||||
'@csstools/css-syntax-patches-for-csstree@1.1.0':
|
||||
resolution: {integrity: sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA==}
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0':
|
||||
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
'@discoveryjs/json-ext@0.5.7':
|
||||
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
|
||||
|
|
@ -1849,6 +1866,15 @@ packages:
|
|||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@exodus/bytes@1.15.0':
|
||||
resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
'@noble/hashes': ^1.8.0 || ^2.0.0
|
||||
peerDependenciesMeta:
|
||||
'@noble/hashes':
|
||||
optional: true
|
||||
|
||||
'@fabianlars/tauri-plugin-oauth@2.0.0':
|
||||
resolution: {integrity: sha512-I1s08ZXrsFuYfNWusAcpLyiCfr5TCvaBrRuKfTG+XQrcaqnAcwjdWH0U5J9QWuMDLwCUMnVxdobtMJzPR8raxQ==}
|
||||
|
||||
|
|
@ -4957,6 +4983,9 @@ packages:
|
|||
resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
bidi-js@1.0.3:
|
||||
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
||||
|
||||
big.js@5.2.2:
|
||||
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
||||
|
||||
|
|
@ -5342,6 +5371,10 @@ packages:
|
|||
css-to-react-native@3.2.0:
|
||||
resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==}
|
||||
|
||||
css-tree@3.2.1:
|
||||
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||
|
||||
css-value@0.0.1:
|
||||
resolution: {integrity: sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==}
|
||||
|
||||
|
|
@ -5354,9 +5387,9 @@ packages:
|
|||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
cssstyle@4.6.0:
|
||||
resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
|
||||
engines: {node: '>=18'}
|
||||
cssstyle@6.2.0:
|
||||
resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
|
@ -5536,9 +5569,9 @@ packages:
|
|||
resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
data-urls@5.0.0:
|
||||
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
|
||||
engines: {node: '>=18'}
|
||||
data-urls@7.0.0:
|
||||
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
|
||||
|
|
@ -6527,9 +6560,9 @@ packages:
|
|||
resolution: {integrity: sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||
engines: {node: '>=18'}
|
||||
html-encoding-sniffer@6.0.0:
|
||||
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
|
@ -6952,9 +6985,9 @@ packages:
|
|||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
jsdom@26.1.0:
|
||||
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
|
||||
engines: {node: '>=18'}
|
||||
jsdom@28.1.0:
|
||||
resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
canvas: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
|
|
@ -7177,8 +7210,8 @@ packages:
|
|||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.2.4:
|
||||
resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
|
||||
lru-cache@11.2.6:
|
||||
resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
|
|
@ -7279,6 +7312,9 @@ packages:
|
|||
mdast-util-to-string@4.0.0:
|
||||
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
|
||||
|
||||
mdn-data@2.27.1:
|
||||
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
||||
|
||||
media-typer@1.1.0:
|
||||
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -7639,9 +7675,6 @@ packages:
|
|||
chokidar:
|
||||
optional: true
|
||||
|
||||
nwsapi@2.2.23:
|
||||
resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -7812,6 +7845,9 @@ packages:
|
|||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
parse5@8.0.0:
|
||||
resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -8498,9 +8534,6 @@ packages:
|
|||
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
rrweb-cssom@0.8.0:
|
||||
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
|
||||
|
||||
rsc-html-stream@0.0.7:
|
||||
resolution: {integrity: sha512-v9+fuY7usTgvXdNl8JmfXCvSsQbq2YMd60kOeeMIqCJFZ69fViuIxztHei7v5mlMMa2h3SqS+v44Gu9i9xANZA==}
|
||||
|
||||
|
|
@ -9062,11 +9095,11 @@ packages:
|
|||
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tldts-core@6.1.86:
|
||||
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
|
||||
tldts-core@7.0.25:
|
||||
resolution: {integrity: sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==}
|
||||
|
||||
tldts@6.1.86:
|
||||
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
|
||||
tldts@7.0.25:
|
||||
resolution: {integrity: sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==}
|
||||
hasBin: true
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
|
|
@ -9085,8 +9118,8 @@ packages:
|
|||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tough-cookie@5.1.2:
|
||||
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
||||
tough-cookie@6.0.0:
|
||||
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tr46@0.0.3:
|
||||
|
|
@ -9095,9 +9128,9 @@ packages:
|
|||
tr46@1.0.1:
|
||||
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
||||
|
||||
tr46@5.1.1:
|
||||
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||
engines: {node: '>=18'}
|
||||
tr46@6.0.0:
|
||||
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
trigram-utils@2.0.1:
|
||||
resolution: {integrity: sha512-nfWIXHEaB+HdyslAfMxSqWKDdmqY9I32jS7GnqpdWQnLH89r6A5sdk3fDVYqGAZ0CrT8ovAFSAo6HRiWcWNIGQ==}
|
||||
|
|
@ -9577,9 +9610,9 @@ packages:
|
|||
webidl-conversions@4.0.2:
|
||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||
|
||||
webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
webidl-conversions@8.0.1:
|
||||
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
webpack-bundle-analyzer@4.10.1:
|
||||
resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==}
|
||||
|
|
@ -9611,9 +9644,13 @@ packages:
|
|||
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
whatwg-url@14.2.0:
|
||||
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
|
||||
engines: {node: '>=18'}
|
||||
whatwg-mimetype@5.0.0:
|
||||
resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
whatwg-url@16.0.1:
|
||||
resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
|
@ -9865,6 +9902,8 @@ packages:
|
|||
|
||||
snapshots:
|
||||
|
||||
'@acemir/cssom@0.9.31': {}
|
||||
|
||||
'@ai-sdk/gateway@2.0.29(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.1
|
||||
|
|
@ -9925,13 +9964,23 @@ snapshots:
|
|||
package-manager-detector: 1.6.0
|
||||
tinyexec: 1.0.2
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
'@asamuzakjp/css-color@5.0.1':
|
||||
dependencies:
|
||||
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
lru-cache: 10.4.3
|
||||
'@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
lru-cache: 11.2.6
|
||||
|
||||
'@asamuzakjp/dom-selector@6.8.1':
|
||||
dependencies:
|
||||
'@asamuzakjp/nwsapi': 2.3.9
|
||||
bidi-js: 1.0.3
|
||||
css-tree: 3.2.1
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
lru-cache: 11.2.6
|
||||
|
||||
'@asamuzakjp/nwsapi@2.3.9': {}
|
||||
|
||||
'@assistant-ui/react-ai-sdk@1.1.21(@assistant-ui/react@0.11.56(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)))(@types/react@19.2.9)(assistant-cloud@0.1.13)(react@19.2.4)':
|
||||
dependencies:
|
||||
|
|
@ -11410,6 +11459,10 @@ snapshots:
|
|||
|
||||
'@braintree/sanitize-url@7.1.1': {}
|
||||
|
||||
'@bramus/specificity@2.4.2':
|
||||
dependencies:
|
||||
css-tree: 3.2.1
|
||||
|
||||
'@chevrotain/cst-dts-gen@11.0.3':
|
||||
dependencies:
|
||||
'@chevrotain/gast': 11.0.3
|
||||
|
|
@ -11458,25 +11511,27 @@ snapshots:
|
|||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
||||
'@csstools/color-helpers@5.1.0': {}
|
||||
'@csstools/color-helpers@6.0.2': {}
|
||||
|
||||
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
||||
'@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
||||
'@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/color-helpers': 5.1.0
|
||||
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
'@csstools/color-helpers': 6.0.2
|
||||
'@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
|
||||
'@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/css-tokenizer': 3.0.4
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-tokenizer@3.0.4': {}
|
||||
'@csstools/css-syntax-patches-for-csstree@1.1.0': {}
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0': {}
|
||||
|
||||
'@discoveryjs/json-ext@0.5.7': {}
|
||||
|
||||
|
|
@ -11791,6 +11846,10 @@ snapshots:
|
|||
'@eslint/core': 0.17.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@exodus/bytes@1.15.0(@noble/hashes@1.8.0)':
|
||||
optionalDependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
|
||||
'@fabianlars/tauri-plugin-oauth@2.0.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.10.1
|
||||
|
|
@ -14550,7 +14609,7 @@ snapshots:
|
|||
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.7)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
playwright: 1.58.2
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(@vitest/browser-playwright@4.0.18)(@vitest/browser-webdriverio@4.0.18)(jiti@1.21.7)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(@vitest/browser-playwright@4.0.18)(@vitest/browser-webdriverio@4.0.18)(jiti@1.21.7)(jsdom@28.1.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- msw
|
||||
|
|
@ -14560,7 +14619,7 @@ snapshots:
|
|||
'@vitest/browser-webdriverio@4.0.18(vite@7.3.1(@types/node@22.19.7)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18)(webdriverio@9.24.0)':
|
||||
dependencies:
|
||||
'@vitest/browser': 4.0.18(vite@7.3.1(@types/node@22.19.7)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18)
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(@vitest/browser-playwright@4.0.18)(@vitest/browser-webdriverio@4.0.18)(jiti@1.21.7)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(@vitest/browser-playwright@4.0.18)(@vitest/browser-webdriverio@4.0.18)(jiti@1.21.7)(jsdom@28.1.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
webdriverio: 9.24.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
|
|
@ -14577,7 +14636,7 @@ snapshots:
|
|||
pngjs: 7.0.0
|
||||
sirv: 3.0.2
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(@vitest/browser-playwright@4.0.18)(@vitest/browser-webdriverio@4.0.18)(jiti@1.21.7)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(@vitest/browser-playwright@4.0.18)(@vitest/browser-webdriverio@4.0.18)(jiti@1.21.7)(jsdom@28.1.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
ws: 8.19.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
|
|
@ -15252,6 +15311,10 @@ snapshots:
|
|||
|
||||
basic-ftp@5.2.0: {}
|
||||
|
||||
bidi-js@1.0.3:
|
||||
dependencies:
|
||||
require-from-string: 2.0.2
|
||||
|
||||
big.js@5.2.2: {}
|
||||
|
||||
bignumber.js@9.3.1: {}
|
||||
|
|
@ -15676,16 +15739,23 @@ snapshots:
|
|||
css-color-keywords: 1.0.0
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
css-tree@3.2.1:
|
||||
dependencies:
|
||||
mdn-data: 2.27.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
css-value@0.0.1: {}
|
||||
|
||||
css-what@6.2.2: {}
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
cssstyle@4.6.0:
|
||||
cssstyle@6.2.0:
|
||||
dependencies:
|
||||
'@asamuzakjp/css-color': 3.2.0
|
||||
rrweb-cssom: 0.8.0
|
||||
'@asamuzakjp/css-color': 5.0.1
|
||||
'@csstools/css-syntax-patches-for-csstree': 1.1.0
|
||||
css-tree: 3.2.1
|
||||
lru-cache: 11.2.6
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
|
|
@ -15890,10 +15960,12 @@ snapshots:
|
|||
|
||||
data-uri-to-buffer@6.0.2: {}
|
||||
|
||||
data-urls@5.0.0:
|
||||
data-urls@7.0.0(@noble/hashes@1.8.0):
|
||||
dependencies:
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.2.0
|
||||
whatwg-mimetype: 5.0.0
|
||||
whatwg-url: 16.0.1(@noble/hashes@1.8.0)
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
dependencies:
|
||||
|
|
@ -17271,9 +17343,11 @@ snapshots:
|
|||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0):
|
||||
dependencies:
|
||||
whatwg-encoding: 3.1.1
|
||||
'@exodus/bytes': 1.15.0(@noble/hashes@1.8.0)
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
|
|
@ -17698,32 +17772,32 @@ snapshots:
|
|||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsdom@26.1.0:
|
||||
jsdom@28.1.0(@noble/hashes@1.8.0):
|
||||
dependencies:
|
||||
cssstyle: 4.6.0
|
||||
data-urls: 5.0.0
|
||||
'@acemir/cssom': 0.9.31
|
||||
'@asamuzakjp/dom-selector': 6.8.1
|
||||
'@bramus/specificity': 2.4.2
|
||||
'@exodus/bytes': 1.15.0(@noble/hashes@1.8.0)
|
||||
cssstyle: 6.2.0
|
||||
data-urls: 7.0.0(@noble/hashes@1.8.0)
|
||||
decimal.js: 10.6.0
|
||||
html-encoding-sniffer: 4.0.0
|
||||
html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0)
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
nwsapi: 2.2.23
|
||||
parse5: 7.3.0
|
||||
rrweb-cssom: 0.8.0
|
||||
parse5: 8.0.0
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 5.1.2
|
||||
tough-cookie: 6.0.0
|
||||
undici: 7.22.0
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 7.0.0
|
||||
whatwg-encoding: 3.1.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.2.0
|
||||
ws: 8.19.0
|
||||
webidl-conversions: 8.0.1
|
||||
whatwg-mimetype: 5.0.0
|
||||
whatwg-url: 16.0.1(@noble/hashes@1.8.0)
|
||||
xml-name-validator: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- '@noble/hashes'
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
|
|
@ -17936,7 +18010,7 @@ snapshots:
|
|||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.4: {}
|
||||
lru-cache@11.2.6: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
|
|
@ -18139,6 +18213,8 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
|
||||
mdn-data@2.27.1: {}
|
||||
|
||||
media-typer@1.1.0: {}
|
||||
|
||||
memoize-one@5.2.1: {}
|
||||
|
|
@ -18632,8 +18708,6 @@ snapshots:
|
|||
optionalDependencies:
|
||||
chokidar: 3.6.0
|
||||
|
||||
nwsapi@2.2.23: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-hash@3.0.0: {}
|
||||
|
|
@ -18832,6 +18906,10 @@ snapshots:
|
|||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
parse5@8.0.0:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-data-parser@0.1.0: {}
|
||||
|
|
@ -18848,7 +18926,7 @@ snapshots:
|
|||
|
||||
path-scurry@2.0.1:
|
||||
dependencies:
|
||||
lru-cache: 11.2.4
|
||||
lru-cache: 11.2.6
|
||||
minipass: 7.1.2
|
||||
|
||||
path-to-regexp@6.3.0: {}
|
||||
|
|
@ -19556,8 +19634,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
rrweb-cssom@0.8.0: {}
|
||||
|
||||
rsc-html-stream@0.0.7: {}
|
||||
|
||||
run-async@4.0.6: {}
|
||||
|
|
@ -20258,11 +20334,11 @@ snapshots:
|
|||
|
||||
tinyrainbow@3.0.3: {}
|
||||
|
||||
tldts-core@6.1.86: {}
|
||||
tldts-core@7.0.25: {}
|
||||
|
||||
tldts@6.1.86:
|
||||
tldts@7.0.25:
|
||||
dependencies:
|
||||
tldts-core: 6.1.86
|
||||
tldts-core: 7.0.25
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
|
|
@ -20279,9 +20355,9 @@ snapshots:
|
|||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tough-cookie@5.1.2:
|
||||
tough-cookie@6.0.0:
|
||||
dependencies:
|
||||
tldts: 6.1.86
|
||||
tldts: 7.0.25
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
|
|
@ -20289,7 +20365,7 @@ snapshots:
|
|||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
tr46@5.1.1:
|
||||
tr46@6.0.0:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
|
|
@ -20710,7 +20786,7 @@ snapshots:
|
|||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@22.19.7)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(@vitest/browser-playwright@4.0.18)(@vitest/browser-webdriverio@4.0.18)(jiti@1.21.7)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.7)(@vitest/browser-playwright@4.0.18)(@vitest/browser-webdriverio@4.0.18)(jiti@1.21.7)(jsdom@28.1.0(@noble/hashes@1.8.0))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.18
|
||||
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.7)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
|
|
@ -20737,7 +20813,7 @@ snapshots:
|
|||
'@types/node': 22.19.7
|
||||
'@vitest/browser-playwright': 4.0.18(playwright@1.58.2)(vite@7.3.1(@types/node@22.19.7)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18)
|
||||
'@vitest/browser-webdriverio': 4.0.18(vite@7.3.1(@types/node@22.19.7)(jiti@1.21.7)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18)(webdriverio@9.24.0)
|
||||
jsdom: 26.1.0
|
||||
jsdom: 28.1.0(@noble/hashes@1.8.0)
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
|
|
@ -20860,7 +20936,7 @@ snapshots:
|
|||
|
||||
webidl-conversions@4.0.2: {}
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
webidl-conversions@8.0.1: {}
|
||||
|
||||
webpack-bundle-analyzer@4.10.1:
|
||||
dependencies:
|
||||
|
|
@ -20923,10 +20999,15 @@ snapshots:
|
|||
|
||||
whatwg-mimetype@4.0.0: {}
|
||||
|
||||
whatwg-url@14.2.0:
|
||||
whatwg-mimetype@5.0.0: {}
|
||||
|
||||
whatwg-url@16.0.1(@noble/hashes@1.8.0):
|
||||
dependencies:
|
||||
tr46: 5.1.1
|
||||
webidl-conversions: 7.0.0
|
||||
'@exodus/bytes': 1.15.0(@noble/hashes@1.8.0)
|
||||
tr46: 6.0.0
|
||||
webidl-conversions: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue