From 3ac1a1a45bd933f826a2133a3b20f527c214ed7b Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Wed, 1 Jul 2026 11:16:30 +0900 Subject: [PATCH] fix(reader): remember last read position for markdown files (#4871) Markdown sections were created with `cfi: ''`, but foliate-js builds a location CFI as `section.cfi ?? CFI.fake.fromIndex(index)`. Nullish coalescing does not fall back for an empty string, so every saved position collapsed to a section-less CFI that resolves to no section. Reopening a `.md` book then fell back to the start even though the library still showed the correct read percentage. Set each section's `cfi` to `CFI.fake.fromIndex(index)`, the same fake spine CFI foliate synthesizes for single-file formats that omit it (e.g. fb2), so positions round-trip across reopens. Fixes #4862 Co-authored-by: Claude Opus 4.8 (1M context) --- apps/readest-app/src/__tests__/utils/md.test.ts | 16 ++++++++++++++++ apps/readest-app/src/utils/md.ts | 8 +++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/readest-app/src/__tests__/utils/md.test.ts b/apps/readest-app/src/__tests__/utils/md.test.ts index ebf92cf8..dfe0c156 100644 --- a/apps/readest-app/src/__tests__/utils/md.test.ts +++ b/apps/readest-app/src/__tests__/utils/md.test.ts @@ -1,6 +1,8 @@ import { describe, it, expect, vi } from 'vitest'; import { makeMarkdownBook } from '@/utils/md'; import type { BookDoc } from '@/libs/document'; +import { CFI } from '@/libs/document'; +import { getIndexFromCfi } from '@/utils/cfi'; // makeMarkdownBook returns a foliate book with a few methods (resolveHref, // isExternal, destroy) and a per-section load() that the BookDoc type does not @@ -80,6 +82,20 @@ describe('makeMarkdownBook', () => { } }); + it('gives each section a CFI base that round-trips to its index (resume position)', async () => { + // foliate-js view.getCFI does `section.cfi ?? CFI.fake.fromIndex(index)`, + // so an empty-string cfi defeats the fallback and the generated location + // CFI loses its spine step. Reopening then resolves to no section and the + // reader falls back to the start. Verify each section carries a base CFI + // that survives the getCFI -> resolveCFI round-trip. + const book = await make('# One\n\na\n\n# Two\n\nb\n\n# Three\n\nc\n'); + book.sections.forEach((section, index) => { + const baseCFI = section.cfi ?? CFI.fake.fromIndex(index); + const locationCFI = CFI.joinIndir(baseCFI, '/4/2:3'); + expect(getIndexFromCfi(locationCFI)).toBe(index); + }); + }); + it('produces XHTML that parses without errors despite void tags', async () => { const book = await make('# H\n\nline one \nline two\n\n---\n\n![alt](https://e.com/i.png)\n'); const doc = await book.sections[0]!.createDocument(); diff --git a/apps/readest-app/src/utils/md.ts b/apps/readest-app/src/utils/md.ts index 5b31315b..1345cffb 100644 --- a/apps/readest-app/src/utils/md.ts +++ b/apps/readest-app/src/utils/md.ts @@ -1,6 +1,7 @@ import { marked } from 'marked'; import type { BookDoc, SectionItem } from '@/libs/document'; +import { CFI } from '@/libs/document'; import { sanitizeHtml } from './sanitize'; // Render a standalone Markdown (.md) file into an in-memory foliate-js book at @@ -158,7 +159,12 @@ export async function makeMarkdownBook(file: File): Promise { const urls: (string | undefined)[] = new Array(xhtml.length).fill(undefined); const sections: MarkdownSection[] = xhtml.map((str, index) => ({ id: String(index), - cfi: '', + // A per-section spine CFI base. foliate-js builds location CFIs as + // `section.cfi ?? CFI.fake.fromIndex(index)`; an empty string defeats that + // `??` fallback, so every saved position would collapse to a section-less + // CFI and reopening would resume from the start. Set the same fake spine + // CFI foliate would synthesize so positions round-trip across reopens. + cfi: CFI.fake.fromIndex(index), size: new TextEncoder().encode(str).length, linear: 'yes', load: () => {