mirror of
https://github.com/panatgithub/AnkiHeadingSync.git
synced 2026-07-22 06:51:43 +00:00
- 将 marker、state、writeback 的 durable identity 改为 noteId - Switch marker, state, and writeback durable identity to noteId - 支持满足标题规则的旧 ID 卡片重接进并按当前 renderer 重渲染 - Reconnect legacy ID cards that match heading rules and rerender them with the current renderer - 补充 schema migration、测试、构建验证与插件同步 - Add schema migration, tests, build verification, and plugin sync
501 lines
No EOL
22 KiB
TypeScript
501 lines
No EOL
22 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import type { PluginSettings } from "@/application/config/PluginSettings";
|
|
import { createDeckRulesFingerprint } from "@/application/services/FileIndexerService";
|
|
import { RenderConfigService } from "@/application/services/RenderConfigService";
|
|
import type { CardState } from "@/domain/manual-sync/entities/PluginState";
|
|
import { hashString } from "@/domain/shared/hash";
|
|
|
|
import { CurrentFileOutOfScopeError, ManualSyncService } from "./ManualSyncService";
|
|
import { createModule3Settings, FakeManualSyncAnkiGateway, FakeManualSyncVaultGateway, InMemoryPluginStateRepository } from "@/test-support/manualSyncFakes";
|
|
|
|
describe("ManualSyncService", () => {
|
|
it("syncs one file, creates Anki notes, writes the new marker format, and saves plugin state", async () => {
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"notes/example.md": ["#### Prompt", "Answer"].join("\n"),
|
|
});
|
|
const stateRepository = new InMemoryPluginStateRepository();
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
const result = await service.syncFile("notes/example.md", createModule3Settings());
|
|
|
|
expect(result.created).toBe(1);
|
|
expect(result.updated).toBe(0);
|
|
expect(result.migratedDecks).toBe(0);
|
|
expect(result.warnings).toEqual([]);
|
|
expect(ankiGateway.addedNotes[0]?.deckName).toBe("notes");
|
|
expect(ankiGateway.ensuredDecks).toEqual([["notes"]]);
|
|
expect(ankiGateway.deletedNotes).toEqual([]);
|
|
expect(ankiGateway.deletedDecks).toEqual([]);
|
|
expect(vaultGateway.getFileContent("notes/example.md")).toContain("<!--ID: 9001-->");
|
|
expect(stateRepository.savedState?.pendingWriteBack).toEqual([]);
|
|
expect(Object.values(stateRepository.savedState?.cards ?? {})).toHaveLength(1);
|
|
});
|
|
|
|
it("records pending write-back and reports conflict files when markdown changed before write-back", async () => {
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"notes/example.md": ["#### Prompt", "Answer"].join("\n"),
|
|
});
|
|
vaultGateway.conflictPaths.add("notes/example.md");
|
|
const stateRepository = new InMemoryPluginStateRepository();
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
const result = await service.syncFile("notes/example.md", createModule3Settings());
|
|
|
|
expect(result.markerWriteConflictFiles).toEqual(["notes/example.md"]);
|
|
expect(stateRepository.savedState?.pendingWriteBack).toHaveLength(1);
|
|
expect(stateRepository.savedState?.pendingWriteBack[0]).toMatchObject({ filePath: "notes/example.md", targetNoteId: 9001 });
|
|
});
|
|
|
|
it("rebuilds the card index without writing unresolved ID markers or calling Anki", async () => {
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"notes/example.md": ["#### Prompt", "Answer"].join("\n"),
|
|
});
|
|
const stateRepository = new InMemoryPluginStateRepository();
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
const result = await service.rebuildIndex(createModule3Settings());
|
|
|
|
expect(result.created).toBe(0);
|
|
expect(result.rewrittenMarkers).toBe(0);
|
|
expect(result.migratedDecks).toBe(0);
|
|
expect(ankiGateway.addedNotes).toHaveLength(0);
|
|
expect(ankiGateway.changedDecks).toEqual([]);
|
|
expect(ankiGateway.deletedNotes).toEqual([]);
|
|
expect(ankiGateway.deletedDecks).toEqual([]);
|
|
expect(vaultGateway.getFileContent("notes/example.md")).toBe(["#### Prompt", "Answer"].join("\n"));
|
|
});
|
|
|
|
it("rebuildIndex does not migrate decks even when deck rules changed", async () => {
|
|
const oldSettings = createModule3Settings({ defaultDeck: "Old::Deck", folderDeckMode: "off" });
|
|
const newSettings = createModule3Settings({ defaultDeck: "New::Deck", folderDeckMode: "folder" });
|
|
const content = ["#### Prompt", "Answer"].join("\n");
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"课程/数学/第一章/导数.md": content,
|
|
});
|
|
const stateRepository = new InMemoryPluginStateRepository({
|
|
files: {
|
|
"课程/数学/第一章/导数.md": {
|
|
filePath: "课程/数学/第一章/导数.md",
|
|
fileHash: "hash-a",
|
|
fileStamp: `1:${content.length}`,
|
|
deckRulesFingerprint: createDeckRulesFingerprint(oldSettings),
|
|
lastIndexedAt: 1,
|
|
noteIds: [42],
|
|
},
|
|
},
|
|
cards: {
|
|
"42": createStoredSyncedCard(oldSettings, {
|
|
filePath: "课程/数学/第一章/导数.md",
|
|
deck: "Old::Deck",
|
|
}),
|
|
},
|
|
pendingWriteBack: [],
|
|
});
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
const result = await service.rebuildIndex(newSettings);
|
|
|
|
expect(result.migratedDecks).toBe(0);
|
|
expect(ankiGateway.changedDecks).toEqual([]);
|
|
});
|
|
|
|
it("restores the original marker without creating a new Anki note when marker was deleted but content is unchanged", async () => {
|
|
const settings = createModule3Settings();
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"notes/example.md": ["#### Prompt", "Answer"].join("\n"),
|
|
});
|
|
const storedCard = createStoredSyncedCard(settings);
|
|
const stateRepository = new InMemoryPluginStateRepository({
|
|
files: {},
|
|
cards: {
|
|
"42": storedCard,
|
|
},
|
|
pendingWriteBack: [],
|
|
});
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
ankiGateway.noteSummariesById.set(42, {
|
|
noteId: 42,
|
|
modelName: "Basic",
|
|
cardIds: [7001],
|
|
});
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
const result = await service.syncFile("notes/example.md", settings);
|
|
|
|
expect(result.created).toBe(0);
|
|
expect(result.updated).toBe(0);
|
|
expect(result.rewrittenMarkers).toBe(1);
|
|
expect(ankiGateway.addedNotes).toHaveLength(0);
|
|
expect(ankiGateway.updatedNotes).toHaveLength(0);
|
|
expect(vaultGateway.getFileContent("notes/example.md")).toContain("<!--ID: 42-->");
|
|
});
|
|
|
|
it("creates a new Anki note when marker was deleted and the card content changed", async () => {
|
|
const settings = createModule3Settings();
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"notes/example.md": ["#### Prompt", "Updated Answer"].join("\n"),
|
|
});
|
|
const stateRepository = new InMemoryPluginStateRepository({
|
|
files: {},
|
|
cards: {
|
|
"42": createStoredSyncedCard(settings),
|
|
},
|
|
pendingWriteBack: [],
|
|
});
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
const result = await service.syncFile("notes/example.md", settings);
|
|
|
|
expect(result.created).toBe(1);
|
|
expect(ankiGateway.addedNotes).toHaveLength(1);
|
|
expect(vaultGateway.getFileContent("notes/example.md")).toContain("<!--ID: 9001-->");
|
|
expect(vaultGateway.getFileContent("notes/example.md")).not.toContain("<!--ID: 42-->");
|
|
expect(stateRepository.savedState?.cards["42"]?.orphan).toBe(true);
|
|
expect(stateRepository.savedState?.cards["9001"]?.orphan).toBe(false);
|
|
});
|
|
|
|
it("uses YAML deck, emits conflict warning, and continues syncing when YAML and body deck declarations differ", async () => {
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"notes/example.md": [
|
|
"---",
|
|
"TARGET DECK: YAML/Deck",
|
|
"---",
|
|
"",
|
|
"TARGET DECK: Body::Deck",
|
|
"",
|
|
"#### Prompt",
|
|
"Answer",
|
|
].join("\n"),
|
|
});
|
|
const stateRepository = new InMemoryPluginStateRepository();
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
const result = await service.syncFile("notes/example.md", createModule3Settings());
|
|
|
|
expect(result.created).toBe(1);
|
|
expect(result.warnings.map((warning) => warning.code)).toEqual(["deck_conflict_yaml_body"]);
|
|
expect(ankiGateway.addedNotes[0]?.deckName).toBe("YAML::Deck");
|
|
});
|
|
|
|
it("maps folder hierarchy to nested Anki decks for new notes", async () => {
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"课程/数学/第一章/导数.md": ["#### Prompt", "Answer"].join("\n"),
|
|
});
|
|
const stateRepository = new InMemoryPluginStateRepository();
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
await service.syncFile("课程/数学/第一章/导数.md", createModule3Settings());
|
|
|
|
expect(ankiGateway.addedNotes[0]?.deckName).toBe("课程::数学::第一章");
|
|
expect(ankiGateway.ensuredDecks).toEqual([["课程::数学::第一章"]]);
|
|
});
|
|
|
|
it("migrates an existing note when only the resolved deck changes", async () => {
|
|
const settings = createModule3Settings({ defaultDeck: "New::Deck" });
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"example.md": ["#### Prompt", "Answer", "<!--ID: 42-->"] .join("\n"),
|
|
});
|
|
const indexedCard = {
|
|
noteId: 42,
|
|
syncKey: "example.md\u00001\u0000hash-card",
|
|
idMarkerState: "present-valid" as const,
|
|
noteIdSource: "marker" as const,
|
|
filePath: "example.md",
|
|
cardType: "basic" as const,
|
|
heading: "Prompt",
|
|
headingLevel: 4,
|
|
bodyMarkdown: "Answer",
|
|
blockStartOffset: 0,
|
|
blockEndOffset: 16,
|
|
blockStartLine: 1,
|
|
bodyStartLine: 2,
|
|
blockEndLine: 3,
|
|
contentEndLine: 2,
|
|
markerLine: 3,
|
|
rawBlockText: ["#### Prompt", "Answer"].join("\n"),
|
|
rawBlockHash: hashString(["#### Prompt", "Answer"].join("\n")),
|
|
deckWarnings: [],
|
|
tagsHint: [],
|
|
};
|
|
const oldSettings = createModule3Settings({ defaultDeck: "Old::Deck" });
|
|
const renderPlan = new RenderConfigService().resolve(indexedCard, oldSettings);
|
|
const stateRepository = new InMemoryPluginStateRepository({
|
|
files: {},
|
|
cards: {
|
|
"42": createStoredSyncedCard(settings, {
|
|
filePath: "example.md",
|
|
deck: "Old::Deck",
|
|
renderConfigHash: renderPlan.renderConfigHash,
|
|
}),
|
|
},
|
|
pendingWriteBack: [],
|
|
});
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
ankiGateway.noteSummariesById.set(42, {
|
|
noteId: 42,
|
|
modelName: "Basic",
|
|
cardIds: [7001],
|
|
});
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
const result = await service.syncFile("example.md", settings);
|
|
|
|
expect(result.updated).toBe(0);
|
|
expect(result.migratedDecks).toBe(1);
|
|
expect(ankiGateway.updatedNotes).toHaveLength(0);
|
|
expect(ankiGateway.changedDecks).toEqual([{ deckName: "New::Deck", cardIds: [7001] }]);
|
|
});
|
|
|
|
it("ordinary sync re-evaluates unchanged files when folder deck rules changed and migrates old notes", async () => {
|
|
const oldSettings = createModule3Settings({ folderDeckMode: "off", defaultDeck: "Default::Deck" });
|
|
const newSettings = createModule3Settings({ folderDeckMode: "folder", defaultDeck: "Default::Deck" });
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"课程/数学/第一章/导数.md": ["#### Prompt", "Answer", "<!--ID: 42-->"] .join("\n"),
|
|
});
|
|
const stateRepository = new InMemoryPluginStateRepository({
|
|
files: {
|
|
"课程/数学/第一章/导数.md": {
|
|
filePath: "课程/数学/第一章/导数.md",
|
|
fileHash: "hash-a",
|
|
fileStamp: `1:${["#### Prompt", "Answer", "<!--ID: 42-->"] .join("\n").length}`,
|
|
deckRulesFingerprint: createDeckRulesFingerprint(oldSettings),
|
|
lastIndexedAt: 1,
|
|
noteIds: [42],
|
|
},
|
|
},
|
|
cards: {
|
|
"42": createStoredSyncedCard(oldSettings, {
|
|
filePath: "课程/数学/第一章/导数.md",
|
|
deck: "Default::Deck",
|
|
}),
|
|
},
|
|
pendingWriteBack: [],
|
|
});
|
|
stateRepository.savedState = await stateRepository.load();
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
ankiGateway.noteSummariesById.set(42, {
|
|
noteId: 42,
|
|
modelName: "Basic",
|
|
cardIds: [7001],
|
|
});
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
const result = await service.syncVault(newSettings);
|
|
|
|
expect(result.updated).toBe(0);
|
|
expect(result.migratedDecks).toBe(1);
|
|
expect(vaultGateway.readCalls).toEqual(["课程/数学/第一章/导数.md"]);
|
|
expect(ankiGateway.changedDecks).toEqual([{ deckName: "课程::数学::第一章", cardIds: [7001] }]);
|
|
expect(stateRepository.savedState?.cards["42"]?.deck).toBe("课程::数学::第一章");
|
|
expect(stateRepository.savedState?.files["课程/数学/第一章/导数.md"]?.deckRulesFingerprint).toBe(createDeckRulesFingerprint(newSettings));
|
|
});
|
|
|
|
it("ordinary sync re-evaluates unchanged files when default deck changed and migrates root-level notes", async () => {
|
|
const content = ["#### Prompt", "Answer", "<!--ID: 42-->"] .join("\n");
|
|
const oldSettings = createModule3Settings({ defaultDeck: "Old::Deck", folderDeckMode: "off" });
|
|
const newSettings = createModule3Settings({ defaultDeck: "New::Deck", folderDeckMode: "off" });
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"example.md": content,
|
|
});
|
|
const stateRepository = new InMemoryPluginStateRepository({
|
|
files: {
|
|
"example.md": {
|
|
filePath: "example.md",
|
|
fileHash: "hash-a",
|
|
fileStamp: `1:${content.length}`,
|
|
deckRulesFingerprint: createDeckRulesFingerprint(oldSettings),
|
|
lastIndexedAt: 1,
|
|
noteIds: [42],
|
|
},
|
|
},
|
|
cards: {
|
|
"42": createStoredSyncedCard(oldSettings, {
|
|
filePath: "example.md",
|
|
deck: "Old::Deck",
|
|
}),
|
|
},
|
|
pendingWriteBack: [],
|
|
});
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
ankiGateway.noteSummariesById.set(42, {
|
|
noteId: 42,
|
|
modelName: "Basic",
|
|
cardIds: [7001],
|
|
});
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
const result = await service.syncVault(newSettings);
|
|
|
|
expect(result.migratedDecks).toBe(1);
|
|
expect(ankiGateway.changedDecks).toEqual([{ deckName: "New::Deck", cardIds: [7001] }]);
|
|
});
|
|
|
|
it("ordinary sync re-evaluates unchanged files when file deck marker changes and migrates old notes", async () => {
|
|
const content = ["MY DECK: Scoped::Deck", "", "#### Prompt", "Answer", "<!--ID: 42-->"] .join("\n");
|
|
const oldSettings = createModule3Settings({ fileDeckMarker: "TARGET DECK", defaultDeck: "Default::Deck" });
|
|
const newSettings = createModule3Settings({ fileDeckMarker: "MY DECK", defaultDeck: "Default::Deck" });
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"notes/example.md": content,
|
|
});
|
|
const stateRepository = new InMemoryPluginStateRepository({
|
|
files: {
|
|
"notes/example.md": {
|
|
filePath: "notes/example.md",
|
|
fileHash: "hash-a",
|
|
fileStamp: `1:${content.length}`,
|
|
deckRulesFingerprint: createDeckRulesFingerprint(oldSettings),
|
|
lastIndexedAt: 1,
|
|
noteIds: [42],
|
|
},
|
|
},
|
|
cards: {
|
|
"42": createStoredSyncedCard(oldSettings, {
|
|
deck: "notes",
|
|
}),
|
|
},
|
|
pendingWriteBack: [],
|
|
});
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
ankiGateway.noteSummariesById.set(42, {
|
|
noteId: 42,
|
|
modelName: "Basic",
|
|
cardIds: [7001],
|
|
});
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
const result = await service.syncVault(newSettings);
|
|
|
|
expect(result.migratedDecks).toBe(1);
|
|
expect(ankiGateway.changedDecks).toEqual([{ deckName: "Scoped::Deck", cardIds: [7001] }]);
|
|
expect(ankiGateway.deletedNotes).toEqual([]);
|
|
expect(ankiGateway.deletedDecks).toEqual([]);
|
|
});
|
|
|
|
it("ordinary sync re-evaluates unchanged files when file-level deck is enabled and migrates old notes", async () => {
|
|
const content = ["TARGET DECK: Scoped::Deck", "", "#### Prompt", "Answer", "<!--ID: 42-->"] .join("\n");
|
|
const oldSettings = createModule3Settings({ fileDeckEnabled: false, defaultDeck: "Default::Deck" });
|
|
const newSettings = createModule3Settings({ fileDeckEnabled: true, defaultDeck: "Default::Deck" });
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"notes/example.md": content,
|
|
});
|
|
const stateRepository = new InMemoryPluginStateRepository({
|
|
files: {
|
|
"notes/example.md": {
|
|
filePath: "notes/example.md",
|
|
fileHash: "hash-a",
|
|
fileStamp: `1:${content.length}`,
|
|
deckRulesFingerprint: createDeckRulesFingerprint(oldSettings),
|
|
lastIndexedAt: 1,
|
|
noteIds: [42],
|
|
},
|
|
},
|
|
cards: {
|
|
"42": createStoredSyncedCard(oldSettings, {
|
|
deck: "notes",
|
|
}),
|
|
},
|
|
pendingWriteBack: [],
|
|
});
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
ankiGateway.noteSummariesById.set(42, {
|
|
noteId: 42,
|
|
modelName: "Basic",
|
|
cardIds: [7001],
|
|
});
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
const result = await service.syncVault(newSettings);
|
|
|
|
expect(result.migratedDecks).toBe(1);
|
|
expect(ankiGateway.changedDecks).toEqual([{ deckName: "Scoped::Deck", cardIds: [7001] }]);
|
|
});
|
|
|
|
it("skips current file sync when the file is outside the configured scope", async () => {
|
|
const vaultGateway = new FakeManualSyncVaultGateway({
|
|
"outside/example.md": ["#### Prompt", "Answer"].join("\n"),
|
|
});
|
|
const stateRepository = new InMemoryPluginStateRepository();
|
|
const ankiGateway = new FakeManualSyncAnkiGateway();
|
|
const service = new ManualSyncService(vaultGateway, stateRepository, ankiGateway, undefined, undefined, undefined, undefined, undefined, undefined, () => 1234);
|
|
|
|
await expect(
|
|
service.syncFile(
|
|
"outside/example.md",
|
|
createModule3Settings({
|
|
scopeMode: "include",
|
|
includeFolders: ["notes"],
|
|
}),
|
|
),
|
|
).rejects.toBeInstanceOf(CurrentFileOutOfScopeError);
|
|
|
|
expect(vaultGateway.readCalls).toEqual([]);
|
|
expect(ankiGateway.addedNotes).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
function createStoredSyncedCard(settings: PluginSettings, overrides: Partial<CardState> = {}): CardState {
|
|
const heading = overrides.heading ?? "Prompt";
|
|
const bodyMarkdown = overrides.bodyMarkdown ?? "Answer";
|
|
const rawBlockText = overrides.rawBlockText ?? [`#### ${heading}`, bodyMarkdown].join("\n");
|
|
const rawBlockHash = overrides.rawBlockHash ?? hashString(rawBlockText);
|
|
const noteId = overrides.noteId ?? 42;
|
|
const indexedCard = {
|
|
noteId,
|
|
syncKey: `${overrides.filePath ?? "notes/example.md"}\u0000${overrides.blockStartLine ?? 1}\u0000${rawBlockHash}`,
|
|
idMarkerState: "present-valid" as const,
|
|
noteIdSource: "marker" as const,
|
|
filePath: overrides.filePath ?? "notes/example.md",
|
|
cardType: overrides.cardType ?? "basic",
|
|
heading,
|
|
headingLevel: overrides.headingLevel ?? 4,
|
|
bodyMarkdown,
|
|
blockStartOffset: overrides.blockStartOffset ?? 0,
|
|
blockEndOffset: overrides.blockEndOffset ?? rawBlockText.length,
|
|
blockStartLine: overrides.blockStartLine ?? 1,
|
|
bodyStartLine: overrides.bodyStartLine ?? 2,
|
|
blockEndLine: overrides.blockEndLine ?? 2,
|
|
contentEndLine: overrides.contentEndLine ?? 2,
|
|
markerLine: overrides.markerLine,
|
|
rawBlockText,
|
|
rawBlockHash,
|
|
deckHint: overrides.deckHint,
|
|
deckHintSource: overrides.deckHintSource,
|
|
deckWarnings: overrides.deckWarnings ?? [],
|
|
tagsHint: overrides.tagsHint ?? [],
|
|
};
|
|
const renderPlan = new RenderConfigService().resolve(indexedCard, settings);
|
|
|
|
return {
|
|
noteId: indexedCard.noteId,
|
|
filePath: indexedCard.filePath,
|
|
heading: indexedCard.heading,
|
|
headingLevel: indexedCard.headingLevel,
|
|
bodyMarkdown: indexedCard.bodyMarkdown,
|
|
cardType: indexedCard.cardType,
|
|
blockStartOffset: indexedCard.blockStartOffset,
|
|
blockEndOffset: indexedCard.blockEndOffset,
|
|
blockStartLine: indexedCard.blockStartLine,
|
|
bodyStartLine: indexedCard.bodyStartLine,
|
|
blockEndLine: indexedCard.blockEndLine,
|
|
contentEndLine: indexedCard.contentEndLine,
|
|
markerLine: indexedCard.markerLine,
|
|
rawBlockText: indexedCard.rawBlockText,
|
|
rawBlockHash: indexedCard.rawBlockHash,
|
|
renderConfigHash: overrides.renderConfigHash ?? renderPlan.renderConfigHash,
|
|
deck: overrides.deck ?? renderPlan.deck,
|
|
deckHint: indexedCard.deckHint,
|
|
deckHintSource: indexedCard.deckHintSource,
|
|
deckWarnings: [...indexedCard.deckWarnings],
|
|
tagsHint: indexedCard.tagsHint,
|
|
lastSyncedAt: overrides.lastSyncedAt ?? 1,
|
|
orphan: overrides.orphan ?? false,
|
|
};
|
|
} |