Editor: Find & Replace, read-aloud, backup, typography, phonetic, org niceties
Writer power-ups (Phase 11), plus the selection-bubble vs copy/paste fix. - Find & Replace (Ctrl/Cmd+F): SearchHighlight decoration extension + FindReplace bar (match-case, replace-all back-to-front, scroll without popping the selection bubble). - Read-aloud (Web Speech, offline) on the word card and selection bubble. - Keyboard/touch access to the ESL helpers: Ctrl/Cmd+D look up word at caret, Ctrl/Cmd+J rewrite selection, touch long-press lookup. Refactored the right-click handler into a shared openWordLookup(pos). - Whole-corpus backup: GET /api/docs/export-all zips every doc (md/docx), de-dupes filenames, dated name; sidebar download links. TestExportAll. - Smart typography input rules (curly quotes/em-dash/ellipsis), ASCII-only so CJK is untouched. - Duplicate doc, sidebar sort (Recent/Title/Longest), toolbar outline popover. - English phonetic (chosen over pinyin for an English learner): ECDICT-built phonetic.json.gz (46,579 words) + Result.Phonetic + WordCard IPA line; scripts/build_phonetic.py (full build + --seed fallback). - Selection bubble no longer blocks copy/paste: deferred to pointer-up and made click-through except on its buttons. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -61,6 +61,45 @@ func TestExportMarkdown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportAll(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
// Two docs, one with a duplicate title to exercise filename de-duplication.
|
||||
seedRichDoc(t, srv)
|
||||
id2 := newDoc(t, srv)
|
||||
if rec := do(t, srv, http.MethodPut, "/"+id2, `{"title":"日记 Diary","content":`+jsonString(richDocJSON)+`,"content_text":"x","word_count":1}`); rec.Code != http.StatusOK {
|
||||
t.Fatalf("seed second doc: %d %s", rec.Code, rec.Body)
|
||||
}
|
||||
|
||||
rec := do(t, srv, http.MethodGet, "/export-all?format=md", "")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("export-all: code=%d body=%s", rec.Code, rec.Body)
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); ct != "application/zip" {
|
||||
t.Fatalf("unexpected content-type: %q", ct)
|
||||
}
|
||||
zr, err := zip.NewReader(bytes.NewReader(rec.Body.Bytes()), int64(rec.Body.Len()))
|
||||
if err != nil {
|
||||
t.Fatalf("zip open: %v", err)
|
||||
}
|
||||
if len(zr.File) != 2 {
|
||||
t.Fatalf("expected 2 files in backup, got %d", len(zr.File))
|
||||
}
|
||||
names := map[string]bool{}
|
||||
for _, f := range zr.File {
|
||||
names[f.Name] = true
|
||||
rc, _ := f.Open()
|
||||
b, _ := io.ReadAll(rc)
|
||||
rc.Close()
|
||||
if !strings.Contains(string(b), "你好") {
|
||||
t.Fatalf("backup entry %q missing CJK body", f.Name)
|
||||
}
|
||||
}
|
||||
// The duplicate title must have been disambiguated, not overwritten.
|
||||
if !names["日记 Diary.md"] || !names["日记 Diary (1).md"] {
|
||||
t.Fatalf("expected de-duplicated filenames, got %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportHTML(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
id := seedRichDoc(t, srv)
|
||||
|
||||
Reference in New Issue
Block a user