diff --git a/docs/qa-group-mapping-parser-decisions.md b/docs/qa-group-mapping-parser-decisions.md
new file mode 100644
index 0000000..350ad5f
--- /dev/null
+++ b/docs/qa-group-mapping-parser-decisions.md
@@ -0,0 +1,41 @@
+# QA Group Mapping / Parser Decisions
+
+## Mapping Authority
+
+- Concrete saved slots remain the runtime sync authority.
+- Derivation metadata is settings-time metadata, not a runtime replacement for saved slots.
+
+## First-Pair Derivation
+
+- QA Group mappings now optionally persist:
+ - `titleField`
+ - `derivation.mode = "first-pair"`
+ - `derivation.firstQuestionField`
+ - `derivation.firstAnswerField`
+- When both selected first fields contain a numeric segment:
+ - use the last numeric segment in each field name
+ - require both fields to start at the same index
+ - require the first selected pair to start at `1`
+ - preserve numeric width when generating later fields
+ - stop at the first missing complete pair
+- When one or both selected first fields have no numeric segment:
+ - keep exactly one configured pair
+ - do not try to auto-extend further pairs
+
+## Refresh Behavior
+
+- If a saved QA Group mapping has derivation metadata, reading fields from Anki regenerates slots from the stored first pair.
+- If a saved QA Group mapping does not have derivation metadata, reading fields from Anki only refreshes `loadedFieldNames` and preserves the saved explicit slots.
+
+## Parser Behavior
+
+- QA Group answers now use the entire child block under each first-level item.
+- The parser dedents the child block before storing it as answer markdown.
+- If the child block is exactly one direct child list item, the parser unwraps that outer bullet so existing simple answers stay stable.
+- If the child block contains multiple child bullets, paragraphs, nested lists, or fences, the parser keeps the markdown structure.
+
+## Cloze Cleanup
+
+- Removed `NoteModelDetails.isCloze` because sync no longer depends on Cloze-compatibility metadata.
+- Kept `CreateAnkiModelInput.isCloze` because model creation still needs the AnkiConnect `createModel` contract.
+- Removed the dead `clozeIncompatible` i18n message because no code path still emits it.
diff --git a/docs/qa-group-mapping-parser-gap-report.md b/docs/qa-group-mapping-parser-gap-report.md
new file mode 100644
index 0000000..e42e8fe
--- /dev/null
+++ b/docs/qa-group-mapping-parser-gap-report.md
@@ -0,0 +1,38 @@
+# QA Group Mapping / Parser Gap Report
+
+## Audit Baseline
+
+- Sync-side authority for QA Group was already correct before this change: sync used saved slots and only validated field existence.
+- Settings UI was still under-specified: users could only change the title field and see an auto-detected slot summary.
+- QA Group field detection still depended on fixed question/answer naming heuristics and could not persist a user-selected first pair.
+- QA Group parsing still truncated answers to the first direct second-level list item label.
+- Stale Cloze compatibility remnants still existed in `NoteModelDetails.isCloze` and the unused `clozeIncompatible` i18n copy.
+
+## Closed Gaps
+
+- Added optional QA Group derivation metadata so settings can persist the first selected question/answer pair while keeping concrete slots as sync authority.
+- Refactored QA Group field derivation to support “choose the first pair once, derive the rest automatically”.
+- Updated the QA Group settings UI to expose:
+ - title field
+ - first question field
+ - first answer field
+ - configured pair count with first/last example
+- Changed field refresh behavior so only mappings with derivation metadata are regenerated from the latest Anki fields.
+- Preserved old explicit QA Group slot mappings when no derivation metadata exists.
+- Updated the QA Group parser so answers now keep the full child block markdown, including multiline text, nested lists, and code fences.
+- Removed stale `NoteModelDetails.isCloze` metadata and deleted the dead `clozeIncompatible` copy.
+
+## Intentional Compatibility Rules
+
+- Old QA Group mappings with explicit slots and no derivation metadata continue to work unchanged.
+- New QA Group mappings no longer emit incomplete-tail warnings during suggestion; they derive continuous pairs from the selected first pair and stop at the first missing complete pair.
+- Simple QA Group answers that were previously represented as a single nested bullet still serialize to the same plain-text answer where practical.
+
+## Validation Status
+
+- Focused tests passed for:
+ - QA Group field derivation service
+ - plugin settings normalization / validation
+ - settings UI behavior
+ - QA Group parser behavior
+ - sync / gateway / batch executor slices touched by `NoteModelDetails`
diff --git a/main.js b/main.js
index 8fb9b80..98d0621 100644
--- a/main.js
+++ b/main.js
@@ -1,47 +1,49 @@
/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
*/
-"use strict";var Mn=Object.defineProperty;var mo=Object.getOwnPropertyDescriptor;var ho=Object.getOwnPropertyNames;var ko=Object.prototype.hasOwnProperty;var We=(e,t)=>{for(var n in t)Mn(e,n,{get:t[n],enumerable:!0})},bo=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of ho(t))!ko.call(e,i)&&i!==n&&Mn(e,i,{get:()=>t[i],enumerable:!(r=mo(t,i))||r.enumerable});return e};var yo=e=>bo(Mn({},"__esModule",{value:!0}),e);var fl={};We(fl,{default:()=>pl});module.exports=yo(fl);var fo=require("obsidian");function L(e,t){return`${e}:${t}`}function wn(e){return e?.cardType==="basic"||e?.cardType==="semantic-qa"}function jr(e){return e?.cardType==="cloze"}function W(e){return e?.cardType==="qa-group"}var Kr=require("obsidian");function ie(){return(0,Kr.getLanguage)()==="zh"?"zh":"en"}function ae(e,t){return t.join(e==="zh"?"\u3001":", ")}var Fn={commands:{syncCurrentFileToAnki:"Sync current file to Anki",syncVaultToAnki:"Sync vault to Anki",rebuildCardIndex:"Rebuild card index",clearCurrentFileSyncedCards:"Clear synced cards in current file",cleanupEmptyDecks:"Clean up empty decks"},settings:{pluginTitle:"Anki Heading Sync",ankiConnectUrl:{name:"AnkiConnect URL",desc:"Default is http://127.0.0.1:8765",placeholder:"http://127.0.0.1:8765"},qaHeadingLevel:{name:"QA heading level",desc:"Default is H4"},clozeHeadingLevel:{name:"Cloze heading level",desc:"Default is H5"},cardAnswerCutoffMode:{name:"Card answer cutoff mode",desc:"A valid sync marker always wins. Without a valid marker, either keep the whole heading block or stop before the first 2+ consecutive blank lines.",options:{headingBlock:"heading-block: whole heading block",doubleBlankLines:"double-blank-lines: stop before the first 2+ blank lines"}},qaGroup:{title:"QA Group 12",marker:{name:"QA Group marker",desc:"When a QA heading ends with this hashtag marker, the whole heading block syncs as one ObsiAnki QA Group 12 note.",placeholder:"#anki-list"},managedNoteType:"Managed note type: {{modelName}}. QA Group sync writes Stem / GroupId / Src / S01..S12 directly. The same note type can also appear in the field mapping panels below if you want to reuse it for other routes.",managedModelContract:"Managed model contract: {{fieldCount}} fields and {{templateCount}} templates are checked automatically during sync."},semanticQa:{title:"Semantic QA",marker:{name:"Semantic QA marker",desc:"When a QA heading ends with this hashtag marker, first-level list items with indented child content become separate cards.",placeholder:"#anki-list-qa"},previewTitle:"Semantic QA preview",triggerHeadingExample:"Trigger heading example: {{heading}}",previewUnavailable:"Preview unavailable. Use a trailing hashtag marker such as #anki-list-qa.",questionPreview:"Question preview: {{question}}",answerPreview:"Answer preview: {{answer}}"},syncOptions:{addObsidianBacklink:{name:"Add Obsidian backlink",desc:"Append a backlink to the source heading into synced cards."},obsidianBacklinkLabel:{name:"Obsidian backlink label",desc:"The text shown for the backlink. Blank input falls back to Open in Obsidian.",placeholder:"Open in Obsidian"},obsidianBacklinkPlacement:{name:"Obsidian backlink placement",desc:"Choose whether the backlink is appended to the question field or placed at the start or end of the answer body.",options:{questionLastLine:"Last line of question field",answerFirstLine:"First line of answer body",answerLastLine:"Last line of answer body"}},highlightsToCloze:{name:"Highlights to Cloze",desc:"Convert ==highlight== segments into cloze deletions for cloze cards."},syncObsidianTagsToAnki:{name:"Sync Obsidian tags to Anki",desc:"Sync Obsidian-recognized tags from the current note into Anki note tags, including nested tags. On each sync, these tags overwrite the managed tags in Anki based on the current Obsidian content."},keepPureTagLinesInCardBody:{name:"Keep pure tag lines in card body",desc:'When turned off, remove every line in the card body that contains only tags, such as "#ProjectA #\u91CD\u70B9/\u6848\u4F8B". Inline tags like "This is a #tag example" and lines with ordinary text like "\u6807\u7B7E\uFF1A#\u9879\u76EEA" are kept. Extra blank lines created by removal are cleaned up automatically.'}},mapping:{title:"Note type field mappings",refresh:{name:"Refresh note types from Anki",button:"Refresh note types"},status:{idle:"Refresh note types from Anki to load the available note types.",loadedCount:"Loaded {{count}} note types from Anki.",empty:"Anki returned no note types.",failedLoadNoteTypes:"Failed to load note types from Anki.",loadedSavedMapping:"Loaded saved mapping for {{modelName}}.",selectedModel:"Selected {{modelName}}. Read fields from Anki to create or refresh its mapping.",loadedFieldsForModel:"Loaded fields for {{modelName}}. Review the suggested mapping and save it.",failedLoadFields:"Failed to load fields for {{modelName}}.",noLoadedFields:"No loaded fields for {{modelName}}. Read fields from Anki first.",savedMapping:"Saved mapping for {{modelName}}.",failedSaveMapping:"Failed to save mapping for {{modelName}}.",savedMappingReady:"Saved mapping is ready for {{title}}.",noSavedMapping:"No saved mapping for {{title}}. Read fields from Anki first."},section:{basic:{title:"QA / Basic",description:"Choose the QA note type, read its fields from Anki, then confirm the title/body mapping."},cloze:{title:"Cloze",description:"Choose the cloze note type, read its fields from Anki, then confirm the main field mapping."},semanticQa:{title:"Semantic QA",description:"Choose the semantic QA note type, read its fields from Anki, then confirm the title/body mapping for child cards."}},noteTypeLabel:"{{title}} note type",noteTypeDesc:"Loaded from Anki note types. Refresh if the latest models are not shown.",fieldsLabel:"{{title}} fields",fieldsDesc:"Load the current note type fields from Anki, then review the suggested mapping.",readFieldsButton:"Read fields from Anki",loadedFields:"Loaded fields: {{fields}}",loadedFieldsNone:"Loaded fields: none. Read fields from Anki first.",mappingLabel:"{{title}} mapping",mappingDesc:"Save the current field mapping for this specific note type.",saveMappingButton:"Save mapping",titleFieldLabel:"{{title}} title field",titleFieldDesc:"Which Anki field should receive the heading/title fragment.",bodyFieldLabel:"{{title}} body field",bodyFieldDesc:"Which Anki field should receive the body fragment.",mainField:{name:"Cloze main field",desc:"The selected field receives title +
+ body during sync."},selectFieldPlaceholder:"-- Select field --"},deck:{defaultSectionTitle:"Default deck",defaultDeck:{name:"Default deck",desc:"Lowest priority: used when neither a file-level deck nor a folder mapping matches."},fileDeckSectionTitle:"File-level custom deck",fileDeckEnabled:{name:"Enable file-level custom deck",desc:"When enabled, a shared marker can define one deck for the whole note in YAML or body."},marker:{name:"Deck marker name",desc:"YAML key and body marker share the same identifier. The default value is TARGET DECK."},template:{name:"Default deck template",desc:"Only the filename variable is supported. It expands to the current file name without .md when inserted."},insertLocation:{name:"Template insert location",desc:"Choose whether the deck template is written into YAML frontmatter or the body header area.",options:{body:"Body header",yaml:"YAML frontmatter"}},insertTemplate:{name:"Insert deck template into the current file",desc:"Write the deck declaration into the current active Markdown file using the current marker, template, and insert location.",button:"Insert deck template"},folderMappingSectionTitle:"Advanced: folder mapping",folderDeckMode:{name:"Folder mapping mode",desc:"Off / parent folders only / parent folders plus current file name.",options:{off:"Off",folder:"Folder-based mapping",folderAndFile:"Folder-and-file mapping"}},examplesTitle:"Examples:",folderExample:"Folder-based mapping example: \u6570\u5B66/\u7B2C\u4E00\u7AE0/\u7B2C\u4E00\u8282.md -> \u6570\u5B66::\u7B2C\u4E00\u7AE0",folderAndFileExample:"Folder-and-file mapping example: \u6570\u5B66/\u7B2C\u4E00\u7AE0/\u7B2C\u4E00\u8282.md -> \u6570\u5B66::\u7B2C\u4E00\u7AE0::\u7B2C\u4E00\u8282",fallbackDesc:"If neither folder-and-file mapping nor file-level custom decks are used, the default deck name is used as the Anki deck.",priorityTitle:"Final priority",priorityDesc:"Final deck priority: file-level custom deck > folder-mapped deck > default deck."},scope:{name:"Plugin run scope",summary:{all:"Process Markdown files across the whole vault",include:"Only process Markdown files in the checked folders below",exclude:"Process the whole vault, but skip Markdown files in the checked folders below"},option:{all:"All files",include:"Only in selected folders",exclude:"Exclude selected folders"},treeDescription:{include:"Only in selected folders: only process Markdown files in the checked folders below",exclude:"Exclude selected folders: process the whole vault, but skip Markdown files in the checked folders below"},loading:"Loading folders from the current vault...",empty:"There are no selectable folders in the current vault.",failedLoad:"Failed to load folders from the current vault.",expandFolder:"Expand {{name}}",collapseFolder:"Collapse {{name}}"},cards:{cardTypes:{title:"Card types and basics",desc:"Edit recognition rules directly. Changes save automatically.",advancedHint:"Advanced: keep AnkiConnect URL here without promoting it to a separate status card.",markerPlaceholder:"/",fieldsUnavailable:"-- Read fields first --",autoManagedField:"Managed automatically",autoComposedAnswer:"Auto composed",qaGroupManagedNoteType:"{{modelName}} (managed automatically)",statusLabel:"Status: ",cacheEmpty:"There are no cached Anki note templates yet. Read the Anki template config manually.",cacheSummary:"Currently using {{noteTypeCount}} cached note templates with {{configuredCount}} selected card modes configured.",cacheChanged:"Anki currently reports {{liveCount}} note templates, which differs from the {{cachedCount}} cached templates on this page. The UI is still using the cache with {{configuredCount}} selected card modes configured; read the Anki template config manually to refresh.",cacheCheckFailed:"Still using {{noteTypeCount}} cached note templates with {{configuredCount}} selected card modes configured. The background check for the latest template list failed; read the Anki template config manually if you need a refresh.",loadedSummary:"Loaded {{noteTypeCount}} note templates and configured {{configuredCount}} selected card modes.",loadedFieldsStatus:"Refreshed fields for {{count}} selected note types.",failedLoad:"Failed to load note types or fields from Anki.",failedSave:"Failed to save card type settings.",loadAnki:{button:"Read Anki note types and fields manually",loading:"Loading Anki note types and fields..."},columns:{enabled:"Enabled",type:"Card type",headingLevel:"Heading marker",extraMarker:"Extra marker",noteType:"Anki note type",questionField:"Question field",answerField:"Answer field"},labels:{headingLevel:"Card marker:",extraMarker:"Extra marker:",noteType:"Anki note type:",questionField:"Question field:",answerField:"Answer field:",mainField:"Main field:",fields:"Fields:",qaGroupTitleField:"Title field:",qaGroupSlotFields:"Q/A fields:",qaGroupWarnings:"Warnings:"},qaGroup:{noModel:"Select an Anki note type first",unavailable:"Read fields from Anki first",detectedSlots:"Detected {{count}} pair(s)",acceptWarnings:"I confirm these warnings can be ignored and sync may continue",warning:{missingAnswer:"Slot {{index}} is missing its answer field, so {{questionField}} / {{answerField}} was ignored.",missingQuestion:"Slot {{index}} is missing its question field, so {{questionField}} / {{answerField}} was ignored."}},rows:{basic:"Q&A (regular paragraph)",qaGroup:"Q&A (nested list)",cloze:"Cloze",semanticQa:"Semantic Q&A"}},syncContent:{title:"Card body sync content",desc:"These options affect rendered content only. Updating them refreshes this card only.",sections:{bodyRange:"1. Choose the card body range",backlink:"2. Add backlinks for card-level jumps from Anki to Obsidian",tags:"3. Read tags",clozeSpecial:"4. Cloze-specific options"}},scope:{title:"Plugin run scope",desc:"Keep the existing scope modes and folder tree. The tree is loaded lazily and can be refreshed locally.",refreshFolders:"Refresh folder list"},deck:{title:"Card deck settings",desc:"Card deck settings are grouped by recommended workflow.",sections:{folderMapping:"1. Recommended: use folder-and-file mapping for Anki decks",fileDeck:"2. Optional: enable file-level custom decks",defaultDeck:"3. Fallback: use the default deck"}},commands:{title:"Command guide",desc:"These are the current commands exposed in the command palette.",items:{syncCurrentFile:"Sync only the active Markdown file to Anki.",syncVault:"Sync all in-scope Markdown files in the vault to Anki.",rebuildIndex:"Rebuild the local card index without creating or updating notes.",clearCurrentFile:"Clear synced card markers and tracked sync state for the active file.",cleanupDecks:"Delete selected empty decks from Anki."}}}},modal:{emptyDeck:{title:"Clean up empty decks",description:"Select the empty decks to delete. A deck is deleted only if it is still empty at deletion time.",selectAll:"Select all",clearAll:"Clear all",invert:"Invert selection",cancel:"Cancel",deleteSelected:"Delete selected empty decks",selectionCount:"Selected {{selected}} / {{total}} empty decks"}},notice:{invalidSettingsRestored:"Invalid plugin settings were detected. Default settings were restored in memory.",failedSavePluginSettings:"Failed to save plugin settings.",noActiveMarkdownForSync:"No active Markdown file is available for sync.",syncUseCaseNotInitialized:"Sync use case is not initialized.",currentFileSyncFailed:"Current file sync failed.",vaultSyncFailed:"Vault sync failed.",rebuildFailed:"Card index rebuild failed.",noActiveMarkdownForDeckTemplateInsertion:"No active Markdown file is available for deck template insertion.",vaultGatewayNotInitialized:"Vault gateway is not initialized.",markdownFileNotFound:"Markdown file not found: {{filePath}}",deckTemplateAlreadyCurrent:"The deck template in the current file is already up to date.",deckTemplateInserted:"Inserted deck template into the current file: {{deck}}",deckTemplateInsertionFailed:"Deck template insertion failed.",noActiveMarkdownForReset:"No active Markdown file is available for reset.",clearCurrentFileUseCaseNotInitialized:"Clear-current-file use case is not initialized.",noTrackedCardsInCurrentFile:"There are no synced cards in the current file.",clearCurrentFileFailed:"Clear current file synced cards failed.",cleanupEmptyDecksUseCaseNotInitialized:"Empty-deck cleanup use case is not initialized.",noEmptyDeckCandidates:"No empty decks were found for cleanup.",cleanupEmptyDecksCancelled:"Empty-deck cleanup was cancelled.",cleanupEmptyDecksFailed:"Cleanup empty decks failed.",summary:{currentFileSync:"Current file sync completed: files {{scannedFiles}}, cards {{scannedCards}}, created {{created}}, updated {{updated}}, migrated note types {{migratedNoteTypes}}, migrated decks {{migratedDecks}}, orphaned {{orphaned}}, media {{uploadedMedia}}, skipped {{skippedUnchangedCards}}.",vaultSync:"Vault sync completed: files {{scannedFiles}}, cards {{scannedCards}}, created {{created}}, updated {{updated}}, migrated note types {{migratedNoteTypes}}, migrated decks {{migratedDecks}}, orphaned {{orphaned}}, media {{uploadedMedia}}, skipped {{skippedUnchangedCards}}.",rebuild:"Card index rebuild completed: files {{scannedFiles}}, cards {{scannedCards}}, migrated decks {{migratedDecks}}, orphaned {{orphaned}}, rewritten markers {{rewrittenMarkers}}, skipped {{skippedUnchangedCards}}.",clearCurrentFile:"Current file synced cards cleared: tracked cards {{trackedCards}}, tracked groups {{trackedGroups}}, deleted notes {{deletedNotes}}, removed markers {{removedMarkers}}, removed ID markers {{removedCardMarkers}}, removed GI markers {{removedGroupMarkers}}, deleted local records {{deletedLocalRecords}}.",cleanupEmptyDecks:"Empty-deck cleanup completed: candidates {{candidateCount}}, selected {{selectedCount}}, deleted {{deletedCount}}, skipped {{skippedCount}}.",markerWriteConflicts:"Marker write conflicts: {{files}}.",warningsCount:"Warnings: {{count}}.",markerRemovalConflicts:"Marker removal conflicts: {{files}}.",failures:"Failures: {{items}}.",skippedDecks:"Skipped decks: {{decks}}."}},errors:{currentFileOutOfScope:"The current file is outside the plugin scope: {{filePath}}",deck:{emptyName:"Deck name cannot be empty. Check the file-level deck declaration or default deck setting."},settings:{headingLevelsRange:"Heading levels must be integers between 1 and 6.",headingLevelsDifferent:"QA and Cloze heading levels must be different.",cardTypeConfigsObject:"Card type configs must be an object.",cardTypeEnabledBoolean:"Each card type enabled flag must be a boolean.",cardTypeExtraMarkerString:"Each card type extra marker must be a string.",cardTypeDefaultConflict:"Only one enabled default card type is allowed at heading H{{headingLevel}}.",cardAnswerCutoffModeInvalid:"Card answer cutoff mode must be heading-block or double-blank-lines.",qaNoteTypeRequired:"QA note type is required.",qaGroupMarkerRequired:"QA Group marker is required.",qaGroupMarkerInvalid:"QA Group marker must be a hashtag-style token like #anki-list.",clozeNoteTypeRequired:"Cloze note type is required.",semanticQaMarkerRequired:"Semantic QA marker is required.",semanticQaMarkerInvalid:"Semantic QA marker must be a hashtag-style token like #anki-list-qa.",qaGroupMarkerConflict:"QA Group marker must be different from Semantic QA marker.",semanticQaNoteTypeRequired:"Semantic QA note type is required.",defaultDeckRequired:"Default deck is required.",fileDeckEnabledBoolean:"File deck enabled must be a boolean.",syncObsidianTagsToAnkiBoolean:"Sync Obsidian tags to Anki must be a boolean.",keepPureTagLinesInCardBodyBoolean:"Keep pure tag lines in card body must be a boolean.",fileDeckMarkerString:"File deck marker must be a string.",fileDeckTemplateString:"File deck template must be a string.",fileDeckInsertLocationInvalid:"File deck insert location must be yaml or body.",folderDeckModeInvalid:"Folder deck mode must be off, folder, or folder-and-file.",scopeModeInvalid:"Scope mode must be one of all, include, or exclude.",obsidianBacklinkPlacementInvalid:"Obsidian backlink placement must be question-last-line, answer-first-line, or answer-last-line.",includeFoldersArray:"Include folders must be an array.",includeFoldersStrings:"Include folders must only contain strings.",excludeFoldersArray:"Exclude folders must be an array.",excludeFoldersStrings:"Exclude folders must only contain strings.",ankiConnectUrlRequired:"AnkiConnect URL is required.",ankiNoteTypeCacheArray:"Anki note type cache must be an array.",ankiNoteTypeCacheStrings:"Anki note type cache can only contain strings.",ankiModelFieldCacheObject:"Anki model field cache must be an object.",ankiModelFieldCacheEntryObject:"Each Anki model field cache entry must be an object.",ankiModelFieldCacheFieldNamesArray:"Anki model field cache fieldNames must be an array.",ankiModelFieldCacheFieldNamesStrings:"Anki model field cache fieldNames can only contain strings.",ankiModelFieldCacheLoadedAt:"Anki model field cache loadedAt must be a finite number.",noteFieldMappingsObject:"Note field mappings must be an object.",noteFieldMappingsCardType:"Note field mappings must use a supported card type.",noteFieldMappingsModelName:"Note field mappings must include a model name.",noteFieldMappingsLoadedFieldNames:"Note field mappings must include loaded field names.",noteFieldMappingsLoadedAt:"Note field mappings must include a loaded timestamp.",noteFieldMappingsQaGroupSlotsArray:"QA Group field mapping slots must be an array.",noteFieldMappingsQaGroupSlotObject:"Each QA Group field mapping slot must be an object.",noteFieldMappingsQaGroupSlotIndex:"Each QA Group field mapping slot.index must be an integer greater than 0.",noteFieldMappingsQaGroupSlotQuestionField:"Each QA Group field mapping slot.questionField must be a string.",noteFieldMappingsQaGroupSlotAnswerField:"Each QA Group field mapping slot.answerField must be a string.",noteFieldMappingsQaGroupWarningsArray:"QA Group field mapping warnings must be an array.",noteFieldMappingsQaGroupWarningsStrings:"QA Group field mapping warnings can only contain strings.",noteFieldMappingsQaGroupAcceptedWarningsArray:"QA Group field mapping acceptedWarnings must be an array.",noteFieldMappingsQaGroupAcceptedWarningsStrings:"QA Group field mapping acceptedWarnings can only contain strings."},noteFieldMapping:{noteTypeNotSelected:{basic:"No Anki note type is selected for basic cards. Choose one in settings before syncing.",cloze:"No Anki note type is selected for cloze cards. Choose one in settings before syncing.",semanticQa:"No Anki note type is selected for semantic QA cards. Choose one in settings before syncing.",qaGroup:"No Anki note type is selected for QA Group cards. Choose one in settings before syncing."},missingSavedMapping:{basic:'No saved field mapping found for basic note type "{{modelName}}". Open plugin settings and read fields from Anki first.',cloze:'No saved field mapping found for cloze note type "{{modelName}}". Open plugin settings and read fields from Anki first.',semanticQa:'No saved field mapping found for semantic QA note type "{{modelName}}". Open plugin settings and read fields from Anki first.',qaGroup:'No saved field mapping found for QA Group note type "{{modelName}}". Open plugin settings, read fields from Anki, and save the mapping first.'},incompleteSavedMapping:{basic:'Saved field mapping for basic note type "{{modelName}}" is incomplete. Open plugin settings and save both title and body fields.',semanticQa:'Saved field mapping for semantic QA note type "{{modelName}}" is incomplete. Open plugin settings and save both title and body fields.',cloze:'Saved field mapping for cloze note type "{{modelName}}" is incomplete. Open plugin settings and save the main field.'},titleBodyMustDiffer:{basic:'Basic note type "{{modelName}}" must use different title and body fields.',semanticQa:'Semantic QA note type "{{modelName}}" must use different title and body fields.'},stale:'Saved field mapping for note type "{{modelName}}" is stale because these fields no longer exist in Anki: {{fields}}. Open plugin settings and read fields from Anki again.',clozeIncompatible:'Cloze note type "{{modelName}}" is not cloze-compatible in Anki.',qaGroupMissingTitle:'Saved field mapping for QA Group note type "{{modelName}}" is incomplete because the title field is missing. Save the mapping again in settings.',qaGroupNoCompleteSlots:'Saved field mapping for QA Group note type "{{modelName}}" is incomplete because it has no saved question/answer slot pairs. Save the mapping again in settings.',qaGroupNonContinuousSlots:'QA Group note type "{{modelName}}" must start its question/answer slots at pair {{firstIndex}} = 1. Renumber the first pair to 1.',qaGroupWarningsUnaccepted:'QA Group note type "{{modelName}}" still has unconfirmed field warnings. Confirm them in settings before syncing.',qaGroupSlotCapacityExceeded:'Saved field mapping for QA Group note type "{{modelName}}" only includes {{capacity}} question/answer pair(s), but the current block has {{itemCount}} item(s): {{filePath}}:{{blockStartLine}}.'},noteTypeMigration:{failed:'Failed to migrate Anki note {{noteId}} from "{{fromModel}}" to "{{toModel}}"{{location}}: {{reason}}',unsupported:'The current AnkiConnect does not support migrating Anki note {{noteId}} from "{{fromModel}}" to "{{toModel}}"{{location}}. Upgrade AnkiConnect, or clear the Anki ID and sync again. Reason: {{reason}}'},writeBack:{summary:"Markdown marker write-back failed for {{fileCount}} file(s).",markdownFileNotFound:"Markdown file was not found.",missingSourceContent:"Scanned source content is missing for marker write-back.",noteIdMissing:"Cannot write a marker because note ID is missing for the block at line {{blockStartLine}}."},markerRemoval:{markdownFileNotFound:"Markdown file was not found."}},warnings:{deck:{conflictYamlBody:"Conflicting {{marker}} declarations were found in YAML and body. This sync used the YAML value.",multipleBodyDeclarations:"Multiple {{marker}} declarations were found in the file. This sync used the first body declaration.",invalidFolderSegment:"A folder or file name contains ::, so folder-based deck mapping was skipped and the default deck was used.",fallbackDefault:"No explicit deck was found and a folder-based deck could not be generated, so the default deck was used."}}};var Zr={commands:{syncCurrentFileToAnki:"\u540C\u6B65\u5F53\u524D\u6587\u4EF6\u5230 Anki",syncVaultToAnki:"\u540C\u6B65\u5168\u5E93\u5230 Anki",rebuildCardIndex:"\u91CD\u5EFA\u5361\u7247\u7D22\u5F15",clearCurrentFileSyncedCards:"\u6E05\u7A7A\u5F53\u524D\u6587\u4EF6\u5DF2\u540C\u6B65\u5361\u7247",cleanupEmptyDecks:"\u6E05\u7406\u7A7A\u724C\u7EC4"},settings:{pluginTitle:"Anki Heading Sync",ankiConnectUrl:{name:"AnkiConnect URL",desc:"\u9ED8\u8BA4\u662F http://127.0.0.1:8765",placeholder:"http://127.0.0.1:8765"},qaHeadingLevel:{name:"QA \u6807\u9898\u5C42\u7EA7",desc:"\u9ED8\u8BA4\u662F H4"},clozeHeadingLevel:{name:"\u586B\u7A7A\u9898\u6807\u9898\u5C42\u7EA7",desc:"\u9ED8\u8BA4\u662F H5"},cardAnswerCutoffMode:{name:"\u5361\u7247\u6B63\u6587\u622A\u6B62\u6A21\u5F0F",desc:"\u6709\u6548\u540C\u6B65\u6807\u8BB0\u59CB\u7EC8\u4F18\u5148\u3002\u6CA1\u6709\u6709\u6548\u6807\u8BB0\u65F6\uFF0C\u9009\u62E9\u7EE7\u7EED\u5230\u6574\u4E2A\u6807\u9898\u5757\u672B\u5C3E\uFF0C\u6216\u5728\u9996\u4E2A 2+ \u8FDE\u7EED\u7A7A\u884C\u524D\u622A\u6B62\u3002",options:{headingBlock:"heading-block\uFF1A\u6574\u4E2A\u6807\u9898\u5757",doubleBlankLines:"double-blank-lines\uFF1A\u5728\u9996\u4E2A 2+ \u8FDE\u7EED\u7A7A\u884C\u524D\u622A\u6B62"}},qaGroup:{title:"QA Group 12",marker:{name:"QA Group \u6807\u8BB0",desc:"\u5F53 QA \u6807\u9898\u4EE5\u8FD9\u4E2A hashtag \u6807\u8BB0\u7ED3\u5C3E\u65F6\uFF0C\u6574\u4E2A\u6807\u9898\u5757\u4F1A\u4F5C\u4E3A\u4E00\u5F20 ObsiAnki QA Group 12 \u7B14\u8BB0\u540C\u6B65\u3002",placeholder:"#anki-list"},managedNoteType:"\u6258\u7BA1\u7B14\u8BB0\u7C7B\u578B\uFF1A{{modelName}}\u3002QA Group \u540C\u6B65\u4F1A\u76F4\u63A5\u5199\u5165 Stem / GroupId / Src / S01..S12\u3002\u5982\u679C\u4F60\u60F3\u5728\u5176\u4ED6\u540C\u6B65\u8DEF\u5F84\u91CC\u590D\u7528\u5B83\uFF0C\u5B83\u4E5F\u4F1A\u51FA\u73B0\u5728\u4E0B\u9762\u7684\u5B57\u6BB5\u6620\u5C04\u9762\u677F\u4E2D\u3002",managedModelContract:"\u6258\u7BA1\u6A21\u578B\u7EA6\u675F\uFF1A\u540C\u6B65\u65F6\u4F1A\u81EA\u52A8\u68C0\u67E5 {{fieldCount}} \u4E2A\u5B57\u6BB5\u548C {{templateCount}} \u4E2A\u6A21\u677F\u3002"},semanticQa:{title:"\u8BED\u4E49 QA",marker:{name:"\u8BED\u4E49 QA \u6807\u8BB0",desc:"\u5F53 QA \u6807\u9898\u4EE5\u8FD9\u4E2A hashtag \u6807\u8BB0\u7ED3\u5C3E\u65F6\uFF0C\u5E26\u7F29\u8FDB\u5B50\u5185\u5BB9\u7684\u4E00\u7EA7\u5217\u8868\u9879\u4F1A\u62C6\u5206\u6210\u72EC\u7ACB\u5361\u7247\u3002",placeholder:"#anki-list-qa"},previewTitle:"\u8BED\u4E49 QA \u9884\u89C8",triggerHeadingExample:"\u89E6\u53D1\u6807\u9898\u793A\u4F8B\uFF1A{{heading}}",previewUnavailable:"\u5F53\u524D\u65E0\u6CD5\u751F\u6210\u9884\u89C8\u3002\u8BF7\u4F7F\u7528\u7C7B\u4F3C #anki-list-qa \u7684\u5C3E\u968F hashtag \u6807\u8BB0\u3002",questionPreview:"\u95EE\u9898\u9884\u89C8\uFF1A{{question}}",answerPreview:"\u7B54\u6848\u9884\u89C8\uFF1A{{answer}}"},syncOptions:{addObsidianBacklink:{name:"\u6DFB\u52A0 Obsidian \u56DE\u94FE",desc:"\u628A\u6765\u6E90\u6807\u9898\u7684\u56DE\u94FE\u8FFD\u52A0\u5230\u540C\u6B65\u540E\u7684\u5361\u7247\u4E2D\u3002"},obsidianBacklinkLabel:{name:"Obsidian \u56DE\u94FE\u663E\u793A\u540D\u79F0",desc:"\u8BBE\u7F6E\u56DE\u94FE\u663E\u793A\u7684\u6587\u5B57\u3002\u7559\u7A7A\u6216\u53EA\u586B\u7A7A\u683C\u65F6\u4F1A\u56DE\u9000\u4E3A Open in Obsidian\u3002",placeholder:"Open in Obsidian"},obsidianBacklinkPlacement:{name:"Obsidian \u56DE\u94FE\u653E\u7F6E\u4F4D\u7F6E",desc:"\u9009\u62E9\u628A\u56DE\u94FE\u8FFD\u52A0\u5230\u95EE\u9898\u680F\u6700\u540E\u4E00\u884C\uFF0C\u6216\u653E\u5230\u7B54\u6848\u6B63\u6587\u7684\u7B2C\u4E00\u884C / \u6700\u540E\u4E00\u884C\u3002",options:{questionLastLine:"\u95EE\u9898\u680F\u7684\u6700\u540E\u4E00\u884C",answerFirstLine:"\u7B54\u6848\u6B63\u6587\u7684\u7B2C\u4E00\u884C",answerLastLine:"\u7B54\u6848\u6B63\u6587\u7684\u6700\u540E\u4E00\u884C"}},highlightsToCloze:{name:"\u9AD8\u4EAE\u8F6C\u586B\u7A7A\u9898",desc:"\u628A ==highlight== \u7247\u6BB5\u8F6C\u6362\u6210\u586B\u7A7A\u9898\u4F7F\u7528\u7684\u6316\u7A7A\u683C\u5F0F\u3002"},syncObsidianTagsToAnki:{name:"\u540C\u6B65 Obsidian \u6807\u7B7E\u5230 Anki",desc:"\u5C06\u5F53\u524D\u7B14\u8BB0\u4E2D Obsidian \u8BC6\u522B\u5230\u7684\u6807\u7B7E\u540C\u6B65\u4E3A Anki \u7B14\u8BB0\u6807\u7B7E\uFF0C\u652F\u6301\u5D4C\u5957\u6807\u7B7E\u3002\u6BCF\u6B21\u540C\u6B65\u65F6\uFF0C\u8FD9\u4E9B\u6807\u7B7E\u90FD\u4F1A\u4EE5\u5F53\u524D Obsidian \u5185\u5BB9\u4E3A\u51C6\u8986\u76D6 Anki\u3002"},keepPureTagLinesInCardBody:{name:"\u5728\u5361\u7247\u6B63\u6587\u4E2D\u4FDD\u7559\u7EAF\u6807\u7B7E\u884C",desc:"\u5173\u95ED\u540E\uFF0C\u4F1A\u5220\u9664\u6B63\u6587\u4E2D\u6240\u6709\u53EA\u5305\u542B\u6807\u7B7E\u7684\u6574\u884C\uFF0C\u4F8B\u5982 \u201C#\u9879\u76EEA #\u91CD\u70B9/\u6848\u4F8B\u201D\u3002\u50CF\u201C\u8FD9\u662F #\u6807\u7B7E \u7684\u6848\u4F8B\u201D\u8FD9\u7C7B\u884C\u5185\u6807\u7B7E\uFF0C\u6216\u201C\u6807\u7B7E\uFF1A#\u9879\u76EEA\u201D\u8FD9\u7C7B\u5E26\u666E\u901A\u6587\u5B57\u7684\u884C\uFF0C\u4E0D\u4F1A\u88AB\u5220\u9664\u3002\u5220\u9664\u540E\u4F1A\u81EA\u52A8\u6E05\u7406\u591A\u4F59\u7A7A\u884C\u3002"}},mapping:{title:"\u7B14\u8BB0\u7C7B\u578B\u5B57\u6BB5\u6620\u5C04",refresh:{name:"\u4ECE Anki \u5237\u65B0\u7B14\u8BB0\u7C7B\u578B",button:"\u5237\u65B0\u7B14\u8BB0\u7C7B\u578B"},status:{idle:"\u8BF7\u5148\u4ECE Anki \u5237\u65B0\u7B14\u8BB0\u7C7B\u578B\uFF0C\u4EE5\u52A0\u8F7D\u53EF\u7528\u7684\u7B14\u8BB0\u7C7B\u578B\u5217\u8868\u3002",loadedCount:"\u5DF2\u4ECE Anki \u52A0\u8F7D {{count}} \u4E2A\u7B14\u8BB0\u7C7B\u578B\u3002",empty:"Anki \u6CA1\u6709\u8FD4\u56DE\u4EFB\u4F55\u7B14\u8BB0\u7C7B\u578B\u3002",failedLoadNoteTypes:"\u4ECE Anki \u52A0\u8F7D\u7B14\u8BB0\u7C7B\u578B\u5931\u8D25\u3002",loadedSavedMapping:"\u5DF2\u52A0\u8F7D {{modelName}} \u7684\u5DF2\u4FDD\u5B58\u6620\u5C04\u3002",selectedModel:"\u5DF2\u9009\u62E9 {{modelName}}\u3002\u8BF7\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5\u4EE5\u521B\u5EFA\u6216\u5237\u65B0\u5B83\u7684\u6620\u5C04\u3002",loadedFieldsForModel:"\u5DF2\u52A0\u8F7D {{modelName}} \u7684\u5B57\u6BB5\u3002\u8BF7\u68C0\u67E5\u5EFA\u8BAE\u6620\u5C04\u540E\u4FDD\u5B58\u3002",failedLoadFields:"\u52A0\u8F7D {{modelName}} \u7684\u5B57\u6BB5\u5931\u8D25\u3002",noLoadedFields:"{{modelName}} \u8FD8\u6CA1\u6709\u5DF2\u52A0\u8F7D\u5B57\u6BB5\u3002\u8BF7\u5148\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5\u3002",savedMapping:"\u5DF2\u4FDD\u5B58 {{modelName}} \u7684\u6620\u5C04\u3002",failedSaveMapping:"\u4FDD\u5B58 {{modelName}} \u7684\u6620\u5C04\u5931\u8D25\u3002",savedMappingReady:"{{title}} \u7684\u5DF2\u4FDD\u5B58\u6620\u5C04\u5DF2\u5C31\u7EEA\u3002",noSavedMapping:"{{title}} \u8FD8\u6CA1\u6709\u5DF2\u4FDD\u5B58\u6620\u5C04\u3002\u8BF7\u5148\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5\u3002"},section:{basic:{title:"QA / \u57FA\u7840\u5361",description:"\u9009\u62E9 QA \u7B14\u8BB0\u7C7B\u578B\uFF0C\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5\uFF0C\u7136\u540E\u786E\u8BA4\u6807\u9898/\u6B63\u6587\u6620\u5C04\u3002"},cloze:{title:"\u586B\u7A7A\u9898",description:"\u9009\u62E9\u586B\u7A7A\u9898\u7B14\u8BB0\u7C7B\u578B\uFF0C\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5\uFF0C\u7136\u540E\u786E\u8BA4\u4E3B\u5B57\u6BB5\u6620\u5C04\u3002"},semanticQa:{title:"\u8BED\u4E49 QA",description:"\u9009\u62E9\u8BED\u4E49 QA \u7B14\u8BB0\u7C7B\u578B\uFF0C\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5\uFF0C\u7136\u540E\u786E\u8BA4\u5B50\u5361\u7247\u7684\u6807\u9898/\u6B63\u6587\u6620\u5C04\u3002"}},noteTypeLabel:"{{title}} \u7B14\u8BB0\u7C7B\u578B",noteTypeDesc:"\u4ECE Anki \u7684\u7B14\u8BB0\u7C7B\u578B\u5217\u8868\u52A0\u8F7D\u3002\u5982\u679C\u6700\u65B0\u6A21\u578B\u6CA1\u6709\u663E\u793A\uFF0C\u8BF7\u5148\u5237\u65B0\u3002",fieldsLabel:"{{title}} \u5B57\u6BB5",fieldsDesc:"\u4ECE Anki \u8BFB\u53D6\u5F53\u524D\u7B14\u8BB0\u7C7B\u578B\u7684\u5B57\u6BB5\uFF0C\u7136\u540E\u68C0\u67E5\u5EFA\u8BAE\u6620\u5C04\u3002",readFieldsButton:"\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5",loadedFields:"\u5DF2\u52A0\u8F7D\u5B57\u6BB5\uFF1A{{fields}}",loadedFieldsNone:"\u5DF2\u52A0\u8F7D\u5B57\u6BB5\uFF1A\u65E0\u3002\u8BF7\u5148\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5\u3002",mappingLabel:"{{title}} \u6620\u5C04",mappingDesc:"\u4FDD\u5B58\u5F53\u524D\u7B14\u8BB0\u7C7B\u578B\u7684\u5B57\u6BB5\u6620\u5C04\u3002",saveMappingButton:"\u4FDD\u5B58\u6620\u5C04",titleFieldLabel:"{{title}} \u6807\u9898\u5B57\u6BB5",titleFieldDesc:"\u9009\u62E9\u63A5\u6536\u6807\u9898\u7247\u6BB5\u7684 Anki \u5B57\u6BB5\u3002",bodyFieldLabel:"{{title}} \u6B63\u6587\u5B57\u6BB5",bodyFieldDesc:"\u9009\u62E9\u63A5\u6536\u6B63\u6587\u7247\u6BB5\u7684 Anki \u5B57\u6BB5\u3002",mainField:{name:"\u586B\u7A7A\u9898\u4E3B\u5B57\u6BB5",desc:"\u540C\u6B65\u65F6\uFF0C\u6240\u9009\u5B57\u6BB5\u4F1A\u63A5\u6536 \u6807\u9898 +
+ \u6B63\u6587\u3002"},selectFieldPlaceholder:"-- \u9009\u62E9\u5B57\u6BB5 --"},deck:{defaultSectionTitle:"\u9ED8\u8BA4\u724C\u7EC4",defaultDeck:{name:"\u9ED8\u8BA4\u724C\u7EC4",desc:"\u4F18\u5148\u7EA7\u6700\u4F4E\uFF1A\u5F53\u6587\u4EF6\u7EA7\u81EA\u5B9A\u4E49\u724C\u7EC4\u4E0E\u6587\u4EF6\u5939\u6620\u5C04\u724C\u7EC4\u90FD\u672A\u547D\u4E2D\u65F6\u4F7F\u7528\u3002"},fileDeckSectionTitle:"\u6587\u4EF6\u7EA7\u81EA\u5B9A\u4E49\u724C\u7EC4",fileDeckEnabled:{name:"\u5F00\u542F\u6587\u4EF6\u7EA7\u81EA\u5B9A\u4E49\u724C\u7EC4",desc:"\u5F00\u542F\u540E\uFF0C\u53EF\u5728 YAML \u6216\u6B63\u6587\u4E2D\u7528\u7EDF\u4E00\u8BC6\u522B\u540D\u4E3A\u6574\u7BC7\u7B14\u8BB0\u6307\u5B9A\u4E00\u4E2A\u724C\u7EC4\u3002"},marker:{name:"\u724C\u7EC4\u8BC6\u522B\u540D",desc:"YAML key \u4E0E\u6B63\u6587\u6807\u8BB0\u5171\u7528\u540C\u4E00\u4E2A\u8BC6\u522B\u540D\u3002\u9ED8\u8BA4\u503C\u662F TARGET DECK\u3002"},template:{name:"\u9ED8\u8BA4\u724C\u7EC4\u6A21\u677F",desc:"\u53EA\u652F\u6301 filename \u53D8\u91CF\u3002\u63D2\u5165\u65F6\u4F1A\u5C55\u5F00\u4E3A\u5F53\u524D\u6587\u4EF6\u540D\uFF08\u4E0D\u542B .md\uFF09\u3002"},insertLocation:{name:"\u6A21\u677F\u63D2\u5165\u4F4D\u7F6E",desc:"\u9009\u62E9\u5C06\u724C\u7EC4\u6A21\u677F\u5199\u5165 YAML frontmatter \u8FD8\u662F\u6B63\u6587\u524D\u90E8\u3002",options:{body:"\u6B63\u6587\u524D\u90E8",yaml:"YAML frontmatter"}},insertTemplate:{name:"\u5411\u5F53\u524D\u6587\u4EF6\u63D2\u5165\u724C\u7EC4\u6A21\u677F",desc:"\u6309\u5F53\u524D\u8BC6\u522B\u540D\u3001\u6A21\u677F\u548C\u63D2\u5165\u4F4D\u7F6E\uFF0C\u628A\u724C\u7EC4\u58F0\u660E\u5199\u5165\u5F53\u524D\u6D3B\u52A8 Markdown \u6587\u4EF6\u3002",button:"\u63D2\u5165\u724C\u7EC4\u6A21\u677F"},folderMappingSectionTitle:"\u9AD8\u7EA7\uFF1A\u6587\u4EF6\u5939\u6620\u5C04\u724C\u7EC4",folderDeckMode:{name:"\u6587\u4EF6\u5939\u6620\u5C04\u6A21\u5F0F",desc:"\u5173\u95ED / \u4EC5\u7236\u6587\u4EF6\u5939 / \u6587\u4EF6\u5939\u53CA\u6587\u4EF6\u540D\u3002",options:{off:"\u5173\u95ED",folder:"\u6587\u4EF6\u5939\u7EA7\u6620\u5C04",folderAndFile:"\u6587\u4EF6\u5939\u53CA\u6587\u4EF6\u540D\u7EA7\u6620\u5C04"}},examplesTitle:"\u793A\u4F8B\uFF1A",folderExample:"\u6587\u4EF6\u5939\u6620\u5C04\u724C\u7EC4\u793A\u4F8B\uFF1A\u6570\u5B66/\u7B2C\u4E00\u7AE0/\u7B2C\u4E00\u8282.md -> \u6570\u5B66::\u7B2C\u4E00\u7AE0",folderAndFileExample:"\u6587\u4EF6\u5939\u53CA\u6587\u4EF6\u540D\u6620\u5C04\u724C\u7EC4\u793A\u4F8B\uFF1A\u6570\u5B66/\u7B2C\u4E00\u7AE0/\u7B2C\u4E00\u8282.md -> \u6570\u5B66::\u7B2C\u4E00\u7AE0::\u7B2C\u4E00\u8282",fallbackDesc:"\u82E5\u5747\u4E0D\u6253\u5F00\u300C\u6587\u4EF6\u5939\u53CA\u6587\u4EF6\u540D\u300D\u53CA\u300C\u6587\u4EF6\u7EA7\u300D\u724C\u7EC4\uFF0C\u5219\u4EE5\u300C\u9ED8\u8BA4\u724C\u7EC4\u540D\u79F0\u300D\u4F5C\u4E3A Anki \u7684\u724C\u7EC4\u540D\u3002",priorityTitle:"\u6700\u7EC8\u724C\u7EC4\u4F18\u5148\u7EA7\u8BF4\u660E",priorityDesc:"\u6700\u7EC8\u724C\u7EC4\u4F18\u5148\u7EA7\uFF1A\u6587\u4EF6\u7EA7\u81EA\u5B9A\u4E49\u724C\u7EC4 > \u6587\u4EF6\u5939\u6620\u5C04\u724C\u7EC4 > \u9ED8\u8BA4\u724C\u7EC4\u3002"},scope:{name:"\u63D2\u4EF6\u8FD0\u884C\u8303\u56F4",summary:{all:"\u5904\u7406\u6574\u4E2A vault \u4E2D\u7684 Markdown \u6587\u4EF6",include:"\u4EC5\u5904\u7406\u4E0B\u65B9\u52FE\u9009\u6587\u4EF6\u5939\u4E2D\u7684 Markdown \u6587\u4EF6",exclude:"\u5904\u7406\u6574\u4E2A vault\uFF0C\u4F46\u8DF3\u8FC7\u4E0B\u65B9\u52FE\u9009\u6587\u4EF6\u5939\u4E2D\u7684 Markdown \u6587\u4EF6"},option:{all:"\u5168\u90E8\u6587\u4EF6",include:"\u4EC5\u5728\u6307\u5B9A\u6587\u4EF6\u5939",exclude:"\u6392\u9664\u6307\u5B9A\u6587\u4EF6\u5939"},treeDescription:{include:"\u4EC5\u5728\u6307\u5B9A\u6587\u4EF6\u5939\uFF1A\u53EA\u5904\u7406\u4E0B\u65B9\u52FE\u9009\u6587\u4EF6\u5939\u4E2D\u7684 Markdown \u6587\u4EF6",exclude:"\u6392\u9664\u6307\u5B9A\u6587\u4EF6\u5939\uFF1A\u5904\u7406\u6574\u4E2A vault\uFF0C\u4F46\u8DF3\u8FC7\u4E0B\u65B9\u52FE\u9009\u6587\u4EF6\u5939\u4E2D\u7684 Markdown \u6587\u4EF6"},loading:"\u6B63\u5728\u8BFB\u53D6\u5F53\u524D vault \u6587\u4EF6\u5939...",empty:"\u5F53\u524D vault \u4E2D\u6CA1\u6709\u53EF\u9009\u6587\u4EF6\u5939\u3002",failedLoad:"\u8BFB\u53D6\u5F53\u524D vault \u6587\u4EF6\u5939\u5931\u8D25\u3002",expandFolder:"\u5C55\u5F00 {{name}}",collapseFolder:"\u6536\u8D77 {{name}}"},cards:{cardTypes:{title:"\u5361\u7247\u7C7B\u578B\u53CA\u57FA\u7840\u8BBE\u7F6E",desc:"\u76F4\u63A5\u7F16\u8F91\u8BC6\u522B\u89C4\u5219\uFF0C\u4FEE\u6539\u540E\u81EA\u52A8\u4FDD\u5B58\u3002",advancedHint:"\u9AD8\u7EA7\u9879\uFF1A\u5C06 AnkiConnect URL \u4FDD\u7559\u5728\u8FD9\u91CC\uFF0C\u4E0D\u5355\u72EC\u505A\u8FDE\u63A5\u72B6\u6001\u5361\u7247\u3002",markerPlaceholder:"/",fieldsUnavailable:"-- \u8BF7\u5148\u8BFB\u53D6\u5B57\u6BB5 --",autoManagedField:"\u81EA\u52A8\u7EF4\u62A4",autoComposedAnswer:"\u81EA\u52A8\u62FC\u63A5",qaGroupManagedNoteType:"{{modelName}}\uFF08\u81EA\u52A8\u7EF4\u62A4\uFF09",statusLabel:"\u72B6\u6001\uFF1A",cacheEmpty:"\u5F53\u524D\u6CA1\u6709\u5DF2\u7F13\u5B58\u7684 Anki \u7B14\u8BB0\u6A21\u677F\u3002\u8BF7\u624B\u52A8\u8BFB\u53D6 Anki \u91CC\u7684\u6A21\u677F\u914D\u7F6E\u3002",cacheSummary:"\u5F53\u524D\u4F7F\u7528\u7F13\u5B58\u7684 {{noteTypeCount}} \u4E2A\u7B14\u8BB0\u6A21\u677F\uFF0C\u5DF2\u9009\u62E9\u5E76\u914D\u7F6E {{configuredCount}} \u4E2A\u5361\u7247\u6A21\u5F0F\u3002",cacheChanged:"\u68C0\u6D4B\u5230 Anki \u5F53\u524D\u6709 {{liveCount}} \u4E2A\u7B14\u8BB0\u6A21\u677F\uFF0C\u548C\u672C\u9875\u7F13\u5B58\u7684 {{cachedCount}} \u4E2A\u6A21\u677F\u4E0D\u4E00\u81F4\u3002\u5F53\u524D\u4ECD\u5728\u4F7F\u7528\u7F13\u5B58\uFF0C\u5DF2\u9009\u62E9\u5E76\u914D\u7F6E {{configuredCount}} \u4E2A\u5361\u7247\u6A21\u5F0F\uFF1B\u5982\u9700\u66F4\u65B0\u8BF7\u624B\u52A8\u8BFB\u53D6 Anki \u91CC\u7684\u6A21\u677F\u914D\u7F6E\u3002",cacheCheckFailed:"\u5F53\u524D\u7EE7\u7EED\u4F7F\u7528\u7F13\u5B58\u7684 {{noteTypeCount}} \u4E2A\u7B14\u8BB0\u6A21\u677F\uFF0C\u5DF2\u9009\u62E9\u5E76\u914D\u7F6E {{configuredCount}} \u4E2A\u5361\u7247\u6A21\u5F0F\uFF1B\u540E\u53F0\u68C0\u67E5\u6700\u65B0\u6A21\u677F\u5217\u8868\u5931\u8D25\uFF0C\u5982\u9700\u66F4\u65B0\u8BF7\u624B\u52A8\u8BFB\u53D6 Anki \u91CC\u7684\u6A21\u677F\u914D\u7F6E\u3002",loadedSummary:"\u5DF2\u83B7\u53D6 {{noteTypeCount}} \u4E2A\u7B14\u8BB0\u6A21\u677F\uFF0C\u5DF2\u9009\u62E9\u5E76\u914D\u7F6E {{configuredCount}} \u4E2A\u5361\u7247\u6A21\u5F0F\u3002",loadedFieldsStatus:"\u5DF2\u5237\u65B0 {{count}} \u4E2A\u5F53\u524D\u6240\u9009\u6A21\u677F\u7684\u5B57\u6BB5\u3002",failedLoad:"\u4ECE Anki \u8BFB\u53D6\u6A21\u677F\u6216\u5B57\u6BB5\u5931\u8D25\u3002",failedSave:"\u4FDD\u5B58\u5361\u7247\u7C7B\u578B\u8BBE\u7F6E\u5931\u8D25\u3002",loadAnki:{button:"\u624B\u52A8\u8BFB\u53D6 Anki \u91CC\u7684\u6A21\u677F\u914D\u7F6E",loading:"\u6B63\u5728\u8BFB\u53D6 Anki \u6A21\u677F\u548C\u5B57\u6BB5..."},columns:{enabled:"\u542F\u7528",type:"\u5361\u7247\u7C7B\u578B",headingLevel:"\u5361\u7247\u6807\u8BB0",extraMarker:"\u989D\u5916\u6807\u8BB0",noteType:"Anki \u7B14\u8BB0\u6A21\u677F",questionField:"\u95EE\u9898\u5B57\u6BB5",answerField:"\u7B54\u6848\u5B57\u6BB5"},labels:{headingLevel:"\u5361\u7247\u6807\u8BB0\uFF1A",extraMarker:"\u989D\u5916\u6807\u8BB0\uFF1A",noteType:"Anki \u7B14\u8BB0\u6A21\u677F\uFF1A",questionField:"\u95EE\u9898\u5B57\u6BB5\uFF1A",answerField:"\u7B54\u6848\u5B57\u6BB5\uFF1A",mainField:"\u4E3B\u5B57\u6BB5\uFF1A",fields:"\u5B57\u6BB5\uFF1A",qaGroupTitleField:"\u9898\u76EE\u5B57\u6BB5\uFF1A",qaGroupSlotFields:"\u95EE\u9898/\u7B54\u6848\u5B57\u6BB5\uFF1A",qaGroupWarnings:"\u8B66\u544A\uFF1A"},qaGroup:{noModel:"\u8BF7\u5148\u9009\u62E9 Anki \u7B14\u8BB0\u6A21\u677F",unavailable:"\u8BF7\u5148\u624B\u52A8\u8BFB\u53D6\u5B57\u6BB5",detectedSlots:"\u5DF2\u8BC6\u522B {{count}} \u7EC4",acceptWarnings:"\u6211\u5DF2\u786E\u8BA4\u5FFD\u7565\u5F53\u524D\u8B66\u544A\uFF0C\u5E76\u5141\u8BB8\u540C\u6B65",warning:{missingAnswer:"\u7B2C {{index}} \u7EC4\u7F3A\u5C11\u7B54\u6848\u5B57\u6BB5\uFF0C\u5DF2\u5FFD\u7565 {{questionField}} / {{answerField}}\u3002",missingQuestion:"\u7B2C {{index}} \u7EC4\u7F3A\u5C11\u95EE\u9898\u5B57\u6BB5\uFF0C\u5DF2\u5FFD\u7565 {{questionField}} / {{answerField}}\u3002"}},rows:{basic:"\u95EE\u7B54\u9898\uFF08\u5E38\u89C4\u6BB5\u843D\u5F62\u5F0F\uFF09",qaGroup:"\u95EE\u7B54\u9898\uFF08\u591A\u7EA7\u5217\u8868\u5F62\u5F0F\uFF09",cloze:"\u586B\u7A7A\u9898",semanticQa:"\u8BED\u4E49\u95EE\u7B54\u9898"}},syncContent:{title:"\u5361\u7247\u6B63\u6587\u540C\u6B65\u5185\u5BB9",desc:"\u8FD9\u4E9B\u9009\u9879\u53EA\u5F71\u54CD\u5361\u7247\u6E32\u67D3\u5185\u5BB9\uFF0C\u4FEE\u6539\u540E\u4EC5\u5237\u65B0\u8FD9\u4E00\u5F20\u5361\u7247\u3002",sections:{bodyRange:"1. \u786E\u5B9A\u300C\u5361\u7247\u6B63\u6587\u300D\u8303\u56F4",backlink:"2. \u786E\u5B9A\u662F\u5426\u589E\u52A0\u56DE\u94FE\uFF0C\u65B9\u4FBF\u4ECE Anki\u300C\u5361\u7247\u7EA7\u8DF3\u8F6C\u300D\u56DE Obsidian",tags:"3. \u786E\u5B9A\u662F\u5426\u8BFB\u53D6\u300C\u6807\u7B7E\u300D",clozeSpecial:"4. \u300C\u586B\u7A7A\u9898\u300D\u4E13\u9879"}},scope:{title:"\u63D2\u4EF6\u8FD0\u884C\u8303\u56F4",desc:"\u4FDD\u7559\u73B0\u6709\u4F5C\u7528\u8303\u56F4\u548C\u6587\u4EF6\u5939\u6811\u903B\u8F91\uFF1B\u6587\u4EF6\u5939\u6811\u6309\u9700\u52A0\u8F7D\uFF0C\u5E76\u652F\u6301\u5C40\u90E8\u5237\u65B0\u3002",refreshFolders:"\u5237\u65B0\u6587\u4EF6\u5939\u5217\u8868"},deck:{title:"\u5361\u7247\u724C\u7EC4\u8BBE\u7F6E",desc:"\u5361\u7247\u724C\u7EC4\u8BBE\u7F6E\u6309\u63A8\u8350\u8DEF\u5F84\u5206\u7EC4\u5C55\u793A\u3002",sections:{folderMapping:"1\uFF0C\u63A8\u8350\u7528\u300C\u6587\u4EF6\u5939\u53CA\u6587\u4EF6\u540D\u300D\u4F5C\u4E3A\u300CAnki\u724C\u7EC4\u300D",fileDeck:"2\uFF0C\u53EF\u6253\u5F00\u300C\u6587\u4EF6\u7EA7\u81EA\u5B9A\u4E49\u724C\u7EC4\u300D\uFF0C\u4F5C\u4E3A\u4E2A\u6027\u5316\u5B9A\u5236",defaultDeck:"3\uFF0C\u9ED8\u8BA4\u724C\u7EC4\u4F5C\u4E3A\u515C\u5E95"}},commands:{title:"\u547D\u4EE4\u8BF4\u660E",desc:"\u4E0B\u9762\u5C55\u793A\u5F53\u524D\u547D\u4EE4\u9762\u677F\u4E2D\u53EF\u7528\u7684\u63D2\u4EF6\u547D\u4EE4\u3002",items:{syncCurrentFile:"\u53EA\u628A\u5F53\u524D\u6D3B\u52A8 Markdown \u6587\u4EF6\u540C\u6B65\u5230 Anki\u3002",syncVault:"\u628A\u5168\u5E93\u4E2D\u6240\u6709\u5904\u4E8E\u4F5C\u7528\u8303\u56F4\u5185\u7684 Markdown \u6587\u4EF6\u540C\u6B65\u5230 Anki\u3002",rebuildIndex:"\u53EA\u91CD\u5EFA\u672C\u5730\u5361\u7247\u7D22\u5F15\uFF0C\u4E0D\u521B\u5EFA\u6216\u66F4\u65B0\u8FDC\u7AEF\u7B14\u8BB0\u3002",clearCurrentFile:"\u6E05\u7A7A\u5F53\u524D\u6587\u4EF6\u7684\u5DF2\u540C\u6B65\u6807\u8BB0\u548C\u672C\u5730\u8DDF\u8E2A\u72B6\u6001\u3002",cleanupDecks:"\u4ECE Anki \u5220\u9664\u6240\u9009\u7A7A\u724C\u7EC4\u3002"}}}},modal:{emptyDeck:{title:"\u6E05\u7406\u7A7A\u724C\u7EC4",description:"\u52FE\u9009\u8981\u5220\u9664\u7684\u7A7A\u724C\u7EC4\u3002\u53EA\u6709\u5220\u9664\u65F6\u4ECD\u4E3A\u7A7A\u7684\u724C\u7EC4\u4F1A\u88AB\u771F\u6B63\u5220\u9664\u3002",selectAll:"\u5168\u9009\u5168\u90E8",clearAll:"\u5168\u90E8\u4E0D\u9009",invert:"\u53CD\u9009",cancel:"\u53D6\u6D88",deleteSelected:"\u5220\u9664\u6240\u9009\u7A7A\u724C\u7EC4",selectionCount:"\u5DF2\u9009 {{selected}} / \u5171 {{total}} \u4E2A\u7A7A\u724C\u7EC4"}},notice:{invalidSettingsRestored:"\u68C0\u6D4B\u5230\u65E0\u6548\u7684\u63D2\u4EF6\u8BBE\u7F6E\uFF0C\u5DF2\u5728\u5185\u5B58\u4E2D\u6062\u590D\u9ED8\u8BA4\u8BBE\u7F6E\u3002",failedSavePluginSettings:"\u4FDD\u5B58\u63D2\u4EF6\u8BBE\u7F6E\u5931\u8D25\u3002",noActiveMarkdownForSync:"\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u4E8E\u540C\u6B65\u7684\u6D3B\u52A8 Markdown \u6587\u4EF6\u3002",syncUseCaseNotInitialized:"\u540C\u6B65\u7528\u4F8B\u5C1A\u672A\u521D\u59CB\u5316\u3002",currentFileSyncFailed:"\u5F53\u524D\u6587\u4EF6\u540C\u6B65\u5931\u8D25\u3002",vaultSyncFailed:"\u5168\u5E93\u540C\u6B65\u5931\u8D25\u3002",rebuildFailed:"\u5361\u7247\u7D22\u5F15\u91CD\u5EFA\u5931\u8D25\u3002",noActiveMarkdownForDeckTemplateInsertion:"\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u4E8E\u63D2\u5165\u724C\u7EC4\u6A21\u677F\u7684\u6D3B\u52A8 Markdown \u6587\u4EF6\u3002",vaultGatewayNotInitialized:"Vault \u7F51\u5173\u5C1A\u672A\u521D\u59CB\u5316\u3002",markdownFileNotFound:"\u672A\u627E\u5230 Markdown \u6587\u4EF6\uFF1A{{filePath}}",deckTemplateAlreadyCurrent:"\u5F53\u524D\u6587\u4EF6\u4E2D\u7684\u724C\u7EC4\u6A21\u677F\u5DF2\u662F\u6700\u65B0\uFF0C\u65E0\u9700\u91CD\u590D\u5199\u5165\u3002",deckTemplateInserted:"\u5DF2\u5411\u5F53\u524D\u6587\u4EF6\u5199\u5165\u724C\u7EC4\u6A21\u677F\uFF1A{{deck}}",deckTemplateInsertionFailed:"\u63D2\u5165\u724C\u7EC4\u6A21\u677F\u5931\u8D25\u3002",noActiveMarkdownForReset:"\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u4E8E\u91CD\u7F6E\u7684\u6D3B\u52A8 Markdown \u6587\u4EF6\u3002",clearCurrentFileUseCaseNotInitialized:"\u6E05\u7A7A\u5F53\u524D\u6587\u4EF6\u7528\u4F8B\u5C1A\u672A\u521D\u59CB\u5316\u3002",noTrackedCardsInCurrentFile:"\u5F53\u524D\u6587\u4EF6\u6CA1\u6709\u5DF2\u540C\u6B65\u5361\u7247\u3002",clearCurrentFileFailed:"\u6E05\u7A7A\u5F53\u524D\u6587\u4EF6\u5DF2\u540C\u6B65\u5361\u7247\u5931\u8D25\u3002",cleanupEmptyDecksUseCaseNotInitialized:"\u7A7A\u724C\u7EC4\u6E05\u7406\u7528\u4F8B\u5C1A\u672A\u521D\u59CB\u5316\u3002",noEmptyDeckCandidates:"\u672A\u53D1\u73B0\u53EF\u6E05\u7406\u7684\u7A7A\u724C\u7EC4\u3002",cleanupEmptyDecksCancelled:"\u5DF2\u53D6\u6D88\u7A7A\u724C\u7EC4\u6E05\u7406\u3002",cleanupEmptyDecksFailed:"\u6E05\u7406\u7A7A\u724C\u7EC4\u5931\u8D25\u3002",summary:{currentFileSync:"\u5F53\u524D\u6587\u4EF6\u540C\u6B65\u5B8C\u6210\uFF1A\u6587\u4EF6 {{scannedFiles}}\uFF0C\u5361\u7247 {{scannedCards}}\uFF0C\u65B0\u5EFA {{created}}\uFF0C\u66F4\u65B0 {{updated}}\uFF0C\u8FC1\u79FB\u6A21\u677F {{migratedNoteTypes}}\uFF0C\u8FC1\u79FB\u724C\u7EC4 {{migratedDecks}}\uFF0C\u5B64\u513F {{orphaned}}\uFF0C\u5A92\u4F53 {{uploadedMedia}}\uFF0C\u8DF3\u8FC7 {{skippedUnchangedCards}}\u3002",vaultSync:"\u5168\u5E93\u540C\u6B65\u5B8C\u6210\uFF1A\u6587\u4EF6 {{scannedFiles}}\uFF0C\u5361\u7247 {{scannedCards}}\uFF0C\u65B0\u5EFA {{created}}\uFF0C\u66F4\u65B0 {{updated}}\uFF0C\u8FC1\u79FB\u6A21\u677F {{migratedNoteTypes}}\uFF0C\u8FC1\u79FB\u724C\u7EC4 {{migratedDecks}}\uFF0C\u5B64\u513F {{orphaned}}\uFF0C\u5A92\u4F53 {{uploadedMedia}}\uFF0C\u8DF3\u8FC7 {{skippedUnchangedCards}}\u3002",rebuild:"\u5361\u7247\u7D22\u5F15\u91CD\u5EFA\u5B8C\u6210\uFF1A\u6587\u4EF6 {{scannedFiles}}\uFF0C\u5361\u7247 {{scannedCards}}\uFF0C\u8FC1\u79FB\u724C\u7EC4 {{migratedDecks}}\uFF0C\u5B64\u513F {{orphaned}}\uFF0C\u91CD\u5199\u6807\u8BB0 {{rewrittenMarkers}}\uFF0C\u8DF3\u8FC7 {{skippedUnchangedCards}}\u3002",clearCurrentFile:"\u5F53\u524D\u6587\u4EF6\u5DF2\u540C\u6B65\u5361\u7247\u6E05\u7A7A\u5B8C\u6210\uFF1A\u5DF2\u8DDF\u8E2A\u5361\u7247 {{trackedCards}}\uFF0C\u5DF2\u8DDF\u8E2A\u5206\u7EC4 {{trackedGroups}}\uFF0C\u5220\u9664\u7B14\u8BB0 {{deletedNotes}}\uFF0C\u79FB\u9664\u6807\u8BB0 {{removedMarkers}}\uFF0C\u79FB\u9664 ID \u6807\u8BB0 {{removedCardMarkers}}\uFF0C\u79FB\u9664 GI \u6807\u8BB0 {{removedGroupMarkers}}\uFF0C\u5220\u9664\u672C\u5730\u8BB0\u5F55 {{deletedLocalRecords}}\u3002",cleanupEmptyDecks:"\u7A7A\u724C\u7EC4\u6E05\u7406\u5B8C\u6210\uFF1A\u5019\u9009 {{candidateCount}}\uFF0C\u5DF2\u9009 {{selectedCount}}\uFF0C\u5DF2\u5220 {{deletedCount}}\uFF0C\u8DF3\u8FC7 {{skippedCount}}\u3002",markerWriteConflicts:"\u6807\u8BB0\u5199\u56DE\u51B2\u7A81\uFF1A{{files}}\u3002",warningsCount:"\u8B66\u544A\uFF1A{{count}}\u3002",markerRemovalConflicts:"\u6807\u8BB0\u79FB\u9664\u51B2\u7A81\uFF1A{{files}}\u3002",failures:"\u5931\u8D25\u9879\uFF1A{{items}}\u3002",skippedDecks:"\u8DF3\u8FC7\u7684\u724C\u7EC4\uFF1A{{decks}}\u3002"}},errors:{currentFileOutOfScope:"\u5F53\u524D\u6587\u4EF6\u4E0D\u5728\u63D2\u4EF6\u4F5C\u7528\u8303\u56F4\u5185\uFF1A{{filePath}}",deck:{emptyName:"\u724C\u7EC4\u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u7EA7\u724C\u7EC4\u58F0\u660E\u6216\u9ED8\u8BA4\u724C\u7EC4\u8BBE\u7F6E\u3002"},settings:{headingLevelsRange:"\u6807\u9898\u5C42\u7EA7\u5FC5\u987B\u662F 1 \u5230 6 \u4E4B\u95F4\u7684\u6574\u6570\u3002",headingLevelsDifferent:"QA \u548C\u586B\u7A7A\u9898\u7684\u6807\u9898\u5C42\u7EA7\u5FC5\u987B\u4E0D\u540C\u3002",cardTypeConfigsObject:"\u5361\u7247\u7C7B\u578B\u914D\u7F6E\u5FC5\u987B\u662F\u5BF9\u8C61\u3002",cardTypeEnabledBoolean:"\u6BCF\u79CD\u5361\u7247\u7C7B\u578B\u7684\u542F\u7528\u72B6\u6001\u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002",cardTypeExtraMarkerString:"\u6BCF\u79CD\u5361\u7247\u7C7B\u578B\u7684\u989D\u5916\u6807\u8BB0\u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002",cardTypeDefaultConflict:"\u540C\u4E00\u4E2A H{{headingLevel}} \u53EA\u80FD\u6709\u4E00\u4E2A\u542F\u7528\u7684\u9ED8\u8BA4\u5361\u7247\u7C7B\u578B\u3002",cardAnswerCutoffModeInvalid:"\u5361\u7247\u6B63\u6587\u622A\u6B62\u6A21\u5F0F\u53EA\u80FD\u662F heading-block \u6216 double-blank-lines\u3002",qaNoteTypeRequired:"QA \u7B14\u8BB0\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A\u3002",qaGroupMarkerRequired:"QA Group \u6807\u8BB0\u4E0D\u80FD\u4E3A\u7A7A\u3002",qaGroupMarkerInvalid:"QA Group \u6807\u8BB0\u5FC5\u987B\u662F\u7C7B\u4F3C #anki-list \u7684 hashtag \u6837\u5F0F token\u3002",clozeNoteTypeRequired:"\u586B\u7A7A\u9898\u7B14\u8BB0\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A\u3002",semanticQaMarkerRequired:"\u8BED\u4E49 QA \u6807\u8BB0\u4E0D\u80FD\u4E3A\u7A7A\u3002",semanticQaMarkerInvalid:"\u8BED\u4E49 QA \u6807\u8BB0\u5FC5\u987B\u662F\u7C7B\u4F3C #anki-list-qa \u7684 hashtag \u6837\u5F0F token\u3002",qaGroupMarkerConflict:"QA Group \u6807\u8BB0\u5FC5\u987B\u548C\u8BED\u4E49 QA \u6807\u8BB0\u4E0D\u540C\u3002",semanticQaNoteTypeRequired:"\u8BED\u4E49 QA \u7B14\u8BB0\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A\u3002",defaultDeckRequired:"\u9ED8\u8BA4\u724C\u7EC4\u4E0D\u80FD\u4E3A\u7A7A\u3002",fileDeckEnabledBoolean:"\u6587\u4EF6\u7EA7\u724C\u7EC4\u5F00\u5173\u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002",syncObsidianTagsToAnkiBoolean:"\u540C\u6B65 Obsidian \u6807\u7B7E\u5230 Anki \u5F00\u5173\u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002",keepPureTagLinesInCardBodyBoolean:"\u4FDD\u7559\u6B63\u6587\u7EAF\u6807\u7B7E\u884C\u5F00\u5173\u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002",fileDeckMarkerString:"\u6587\u4EF6\u7EA7\u724C\u7EC4\u8BC6\u522B\u540D\u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002",fileDeckTemplateString:"\u6587\u4EF6\u7EA7\u724C\u7EC4\u6A21\u677F\u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002",fileDeckInsertLocationInvalid:"\u6587\u4EF6\u7EA7\u724C\u7EC4\u6A21\u677F\u63D2\u5165\u4F4D\u7F6E\u53EA\u80FD\u662F yaml \u6216 body\u3002",folderDeckModeInvalid:"\u6587\u4EF6\u5939\u724C\u7EC4\u6A21\u5F0F\u53EA\u80FD\u662F off\u3001folder \u6216 folder-and-file\u3002",scopeModeInvalid:"\u8FD0\u884C\u8303\u56F4\u6A21\u5F0F\u53EA\u80FD\u662F all\u3001include \u6216 exclude\u3002",obsidianBacklinkPlacementInvalid:"Obsidian \u56DE\u94FE\u653E\u7F6E\u4F4D\u7F6E\u53EA\u80FD\u662F question-last-line\u3001answer-first-line \u6216 answer-last-line\u3002",includeFoldersArray:"\u5305\u542B\u6587\u4EF6\u5939\u5FC5\u987B\u662F\u6570\u7EC4\u3002",includeFoldersStrings:"\u5305\u542B\u6587\u4EF6\u5939\u5217\u8868\u4E2D\u53EA\u80FD\u5305\u542B\u5B57\u7B26\u4E32\u3002",excludeFoldersArray:"\u6392\u9664\u6587\u4EF6\u5939\u5FC5\u987B\u662F\u6570\u7EC4\u3002",excludeFoldersStrings:"\u6392\u9664\u6587\u4EF6\u5939\u5217\u8868\u4E2D\u53EA\u80FD\u5305\u542B\u5B57\u7B26\u4E32\u3002",ankiConnectUrlRequired:"AnkiConnect URL \u4E0D\u80FD\u4E3A\u7A7A\u3002",ankiNoteTypeCacheArray:"Anki \u7B14\u8BB0\u6A21\u677F\u7F13\u5B58\u5FC5\u987B\u662F\u6570\u7EC4\u3002",ankiNoteTypeCacheStrings:"Anki \u7B14\u8BB0\u6A21\u677F\u7F13\u5B58\u4E2D\u53EA\u80FD\u5305\u542B\u5B57\u7B26\u4E32\u3002",ankiModelFieldCacheObject:"Anki \u6A21\u677F\u5B57\u6BB5\u7F13\u5B58\u5FC5\u987B\u662F\u5BF9\u8C61\u3002",ankiModelFieldCacheEntryObject:"\u6BCF\u4E2A Anki \u6A21\u677F\u5B57\u6BB5\u7F13\u5B58\u9879\u90FD\u5FC5\u987B\u662F\u5BF9\u8C61\u3002",ankiModelFieldCacheFieldNamesArray:"Anki \u6A21\u677F\u5B57\u6BB5\u7F13\u5B58\u4E2D\u7684 fieldNames \u5FC5\u987B\u662F\u6570\u7EC4\u3002",ankiModelFieldCacheFieldNamesStrings:"Anki \u6A21\u677F\u5B57\u6BB5\u7F13\u5B58\u4E2D\u7684 fieldNames \u53EA\u80FD\u5305\u542B\u5B57\u7B26\u4E32\u3002",ankiModelFieldCacheLoadedAt:"Anki \u6A21\u677F\u5B57\u6BB5\u7F13\u5B58\u4E2D\u7684 loadedAt \u5FC5\u987B\u662F\u6709\u9650\u6570\u5B57\u3002",noteFieldMappingsObject:"\u5B57\u6BB5\u6620\u5C04\u5FC5\u987B\u662F\u5BF9\u8C61\u3002",noteFieldMappingsCardType:"\u5B57\u6BB5\u6620\u5C04\u5FC5\u987B\u4F7F\u7528\u53D7\u652F\u6301\u7684\u5361\u7247\u7C7B\u578B\u3002",noteFieldMappingsModelName:"\u5B57\u6BB5\u6620\u5C04\u5FC5\u987B\u5305\u542B\u6A21\u578B\u540D\u79F0\u3002",noteFieldMappingsLoadedFieldNames:"\u5B57\u6BB5\u6620\u5C04\u5FC5\u987B\u5305\u542B\u5DF2\u52A0\u8F7D\u5B57\u6BB5\u540D\u3002",noteFieldMappingsLoadedAt:"\u5B57\u6BB5\u6620\u5C04\u5FC5\u987B\u5305\u542B\u5DF2\u52A0\u8F7D\u65F6\u95F4\u6233\u3002",noteFieldMappingsQaGroupSlotsArray:"QA Group \u5B57\u6BB5\u6620\u5C04\u4E2D\u7684 slots \u5FC5\u987B\u662F\u6570\u7EC4\u3002",noteFieldMappingsQaGroupSlotObject:"QA Group \u5B57\u6BB5\u6620\u5C04\u4E2D\u7684\u6BCF\u4E2A slot \u90FD\u5FC5\u987B\u662F\u5BF9\u8C61\u3002",noteFieldMappingsQaGroupSlotIndex:"QA Group \u5B57\u6BB5\u6620\u5C04\u4E2D\u7684 slot.index \u5FC5\u987B\u662F\u5927\u4E8E 0 \u7684\u6574\u6570\u3002",noteFieldMappingsQaGroupSlotQuestionField:"QA Group \u5B57\u6BB5\u6620\u5C04\u4E2D\u7684 slot.questionField \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002",noteFieldMappingsQaGroupSlotAnswerField:"QA Group \u5B57\u6BB5\u6620\u5C04\u4E2D\u7684 slot.answerField \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002",noteFieldMappingsQaGroupWarningsArray:"QA Group \u5B57\u6BB5\u6620\u5C04\u4E2D\u7684 warnings \u5FC5\u987B\u662F\u6570\u7EC4\u3002",noteFieldMappingsQaGroupWarningsStrings:"QA Group \u5B57\u6BB5\u6620\u5C04\u4E2D\u7684 warnings \u53EA\u80FD\u5305\u542B\u5B57\u7B26\u4E32\u3002",noteFieldMappingsQaGroupAcceptedWarningsArray:"QA Group \u5B57\u6BB5\u6620\u5C04\u4E2D\u7684 acceptedWarnings \u5FC5\u987B\u662F\u6570\u7EC4\u3002",noteFieldMappingsQaGroupAcceptedWarningsStrings:"QA Group \u5B57\u6BB5\u6620\u5C04\u4E2D\u7684 acceptedWarnings \u53EA\u80FD\u5305\u542B\u5B57\u7B26\u4E32\u3002"},noteFieldMapping:{noteTypeNotSelected:{basic:"\u57FA\u7840\u5361\u5C1A\u672A\u9009\u62E9 Anki \u7B14\u8BB0\u6A21\u677F\u3002\u8BF7\u5148\u5728\u8BBE\u7F6E\u9875\u9009\u62E9\u6A21\u677F\u540E\u518D\u540C\u6B65\u3002",cloze:"\u586B\u7A7A\u9898\u5C1A\u672A\u9009\u62E9 Anki \u7B14\u8BB0\u6A21\u677F\u3002\u8BF7\u5148\u5728\u8BBE\u7F6E\u9875\u9009\u62E9\u6A21\u677F\u540E\u518D\u540C\u6B65\u3002",semanticQa:"\u8BED\u4E49 QA \u5C1A\u672A\u9009\u62E9 Anki \u7B14\u8BB0\u6A21\u677F\u3002\u8BF7\u5148\u5728\u8BBE\u7F6E\u9875\u9009\u62E9\u6A21\u677F\u540E\u518D\u540C\u6B65\u3002",qaGroup:"\u95EE\u7B54\u9898\uFF08\u591A\u7EA7\u5217\u8868\uFF09\u5C1A\u672A\u9009\u62E9 Anki \u7B14\u8BB0\u6A21\u677F\u3002\u8BF7\u5148\u5728\u8BBE\u7F6E\u9875\u9009\u62E9\u6A21\u677F\u540E\u518D\u540C\u6B65\u3002"},missingSavedMapping:{basic:'\u627E\u4E0D\u5230\u57FA\u7840\u5361\u7B14\u8BB0\u7C7B\u578B "{{modelName}}" \u7684\u5DF2\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u3002\u8BF7\u6253\u5F00\u63D2\u4EF6\u8BBE\u7F6E\u5E76\u5148\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5\u3002',cloze:'\u627E\u4E0D\u5230\u586B\u7A7A\u9898\u7B14\u8BB0\u7C7B\u578B "{{modelName}}" \u7684\u5DF2\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u3002\u8BF7\u6253\u5F00\u63D2\u4EF6\u8BBE\u7F6E\u5E76\u5148\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5\u3002',semanticQa:'\u627E\u4E0D\u5230\u8BED\u4E49 QA \u7B14\u8BB0\u7C7B\u578B "{{modelName}}" \u7684\u5DF2\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u3002\u8BF7\u6253\u5F00\u63D2\u4EF6\u8BBE\u7F6E\u5E76\u5148\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5\u3002',qaGroup:'\u627E\u4E0D\u5230\u95EE\u7B54\u9898\uFF08\u591A\u7EA7\u5217\u8868\uFF09\u7B14\u8BB0\u6A21\u677F "{{modelName}}" \u7684\u5DF2\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u3002\u8BF7\u5148\u5728\u8BBE\u7F6E\u9875\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5\u5E76\u4FDD\u5B58\u6620\u5C04\u3002'},incompleteSavedMapping:{basic:'\u57FA\u7840\u5361\u7B14\u8BB0\u7C7B\u578B "{{modelName}}" \u7684\u5DF2\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u4E0D\u5B8C\u6574\u3002\u8BF7\u6253\u5F00\u63D2\u4EF6\u8BBE\u7F6E\u5E76\u4FDD\u5B58\u6807\u9898\u5B57\u6BB5\u548C\u6B63\u6587\u5B57\u6BB5\u3002',semanticQa:'\u8BED\u4E49 QA \u7B14\u8BB0\u7C7B\u578B "{{modelName}}" \u7684\u5DF2\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u4E0D\u5B8C\u6574\u3002\u8BF7\u6253\u5F00\u63D2\u4EF6\u8BBE\u7F6E\u5E76\u4FDD\u5B58\u6807\u9898\u5B57\u6BB5\u548C\u6B63\u6587\u5B57\u6BB5\u3002',cloze:'\u586B\u7A7A\u9898\u7B14\u8BB0\u7C7B\u578B "{{modelName}}" \u7684\u5DF2\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u4E0D\u5B8C\u6574\u3002\u8BF7\u6253\u5F00\u63D2\u4EF6\u8BBE\u7F6E\u5E76\u4FDD\u5B58\u4E3B\u5B57\u6BB5\u3002'},titleBodyMustDiffer:{basic:'\u57FA\u7840\u5361\u7B14\u8BB0\u7C7B\u578B "{{modelName}}" \u7684\u6807\u9898\u5B57\u6BB5\u548C\u6B63\u6587\u5B57\u6BB5\u5FC5\u987B\u4E0D\u540C\u3002',semanticQa:'\u8BED\u4E49 QA \u7B14\u8BB0\u7C7B\u578B "{{modelName}}" \u7684\u6807\u9898\u5B57\u6BB5\u548C\u6B63\u6587\u5B57\u6BB5\u5FC5\u987B\u4E0D\u540C\u3002'},stale:'\u7B14\u8BB0\u7C7B\u578B "{{modelName}}" \u7684\u5DF2\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u5DF2\u8FC7\u671F\uFF0C\u56E0\u4E3A\u8FD9\u4E9B\u5B57\u6BB5\u5728 Anki \u4E2D\u5DF2\u4E0D\u5B58\u5728\uFF1A{{fields}}\u3002\u8BF7\u91CD\u65B0\u4ECE Anki \u8BFB\u53D6\u5B57\u6BB5\u3002',clozeIncompatible:'\u586B\u7A7A\u9898\u7B14\u8BB0\u7C7B\u578B "{{modelName}}" \u5728 Anki \u4E2D\u4E0D\u662F\u586B\u7A7A\u9898\u517C\u5BB9\u6A21\u578B\u3002',qaGroupMissingTitle:'\u95EE\u7B54\u9898\uFF08\u591A\u7EA7\u5217\u8868\uFF09\u7B14\u8BB0\u6A21\u677F "{{modelName}}" \u7684\u5DF2\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u4E0D\u5B8C\u6574\uFF0C\u7F3A\u5C11\u6807\u9898\u5B57\u6BB5\u3002\u8BF7\u5728\u8BBE\u7F6E\u9875\u91CD\u65B0\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u3002',qaGroupNoCompleteSlots:'\u95EE\u7B54\u9898\uFF08\u591A\u7EA7\u5217\u8868\uFF09\u7B14\u8BB0\u6A21\u677F "{{modelName}}" \u7684\u5DF2\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u4E0D\u5B8C\u6574\uFF0C\u81F3\u5C11\u9700\u8981\u4E00\u7EC4\u95EE\u9898/\u7B54\u6848\u5B57\u6BB5\u3002\u8BF7\u5728\u8BBE\u7F6E\u9875\u91CD\u65B0\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u3002',qaGroupNonContinuousSlots:'\u95EE\u7B54\u9898\uFF08\u591A\u7EA7\u5217\u8868\uFF09\u7B14\u8BB0\u6A21\u677F "{{modelName}}" \u7684\u7B2C\u4E00\u7EC4\u95EE\u9898/\u7B54\u6848\u5B57\u6BB5\u4E0D\u662F\u4ECE\u7B2C {{firstIndex}} \u7EC4\u5F00\u59CB\u8FDE\u7EED\u7F16\u53F7\u3002\u8BF7\u4ECE\u7B2C 1 \u7EC4\u5F00\u59CB\u3002',qaGroupWarningsUnaccepted:'\u95EE\u7B54\u9898\uFF08\u591A\u7EA7\u5217\u8868\uFF09\u7B14\u8BB0\u6A21\u677F "{{modelName}}" \u4ECD\u6709\u672A\u786E\u8BA4\u7684\u5B57\u6BB5\u8B66\u544A\u3002\u8BF7\u5148\u5728\u8BBE\u7F6E\u9875\u786E\u8BA4\u540E\u518D\u540C\u6B65\u3002',qaGroupSlotCapacityExceeded:'\u95EE\u7B54\u9898\uFF08\u591A\u7EA7\u5217\u8868\uFF09\u7B14\u8BB0\u6A21\u677F "{{modelName}}" \u7684\u5DF2\u4FDD\u5B58\u5B57\u6BB5\u6620\u5C04\u53EA\u5305\u542B {{capacity}} \u7EC4\u95EE\u9898/\u7B54\u6848\u5B57\u6BB5\uFF0C\u4F46\u5F53\u524D\u5757\u6709 {{itemCount}} \u9879\uFF1A{{filePath}}:{{blockStartLine}}\u3002'},noteTypeMigration:{failed:"\u65E0\u6CD5\u5C06 Anki \u7B14\u8BB0 {{noteId}} \u4ECE\u201C{{fromModel}}\u201D\u8FC1\u79FB\u5230\u201C{{toModel}}\u201D{{location}}\uFF1A{{reason}}",unsupported:"\u5F53\u524D AnkiConnect \u4E0D\u652F\u6301\u5C06 Anki \u7B14\u8BB0 {{noteId}} \u4ECE\u201C{{fromModel}}\u201D\u8FC1\u79FB\u5230\u201C{{toModel}}\u201D{{location}}\u3002\u8BF7\u5347\u7EA7 AnkiConnect\uFF0C\u6216\u6E05\u7406\u8BE5\u5361\u7247\u7684 Anki ID \u540E\u91CD\u65B0\u540C\u6B65\u3002\u539F\u56E0\uFF1A{{reason}}"},writeBack:{summary:"Markdown \u6807\u8BB0\u5199\u56DE\u5931\u8D25\uFF0C\u5171 {{fileCount}} \u4E2A\u6587\u4EF6\u3002",markdownFileNotFound:"Markdown \u6587\u4EF6\u4E0D\u5B58\u5728\u3002",missingSourceContent:"\u7F3A\u5C11\u7528\u4E8E\u5199\u56DE\u6807\u8BB0\u7684\u626B\u63CF\u6E90\u7801\u5185\u5BB9\u3002",noteIdMissing:"\u65E0\u6CD5\u4E3A\u7B2C {{blockStartLine}} \u884C\u6240\u5728\u5757\u5199\u5165\u6807\u8BB0\uFF0C\u56E0\u4E3A\u7F3A\u5C11 note ID\u3002"},markerRemoval:{markdownFileNotFound:"Markdown \u6587\u4EF6\u4E0D\u5B58\u5728\u3002"}},warnings:{deck:{conflictYamlBody:"\u68C0\u6D4B\u5230 YAML \u548C\u6B63\u6587\u4E2D\u7684 {{marker}} \u58F0\u660E\u4E92\u76F8\u51B2\u7A81\uFF0C\u672C\u6B21\u540C\u6B65\u5DF2\u4F7F\u7528 YAML \u7684\u503C\u3002",multipleBodyDeclarations:"\u68C0\u6D4B\u5230\u6587\u4EF6\u4E2D\u5B58\u5728\u591A\u4E2A {{marker}} \u58F0\u660E\uFF0C\u672C\u6B21\u540C\u6B65\u53EA\u4F7F\u7528\u7B2C\u4E00\u4E2A\u6B63\u6587\u58F0\u660E\u3002",invalidFolderSegment:"\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u540D\u6216\u6587\u4EF6\u540D\u5305\u542B ::\uFF0C\u5DF2\u8DF3\u8FC7\u6587\u4EF6\u5939\u6620\u5C04\u5E76\u56DE\u9000\u5230\u9ED8\u8BA4\u724C\u7EC4\u3002",fallbackDefault:"\u8BE5\u6587\u4EF6\u6CA1\u6709\u663E\u5F0F\u724C\u7EC4\uFF0C\u4E14\u65E0\u6CD5\u751F\u6210\u6587\u4EF6\u5939\u6620\u5C04\u724C\u7EC4\uFF0C\u5DF2\u56DE\u9000\u5230\u9ED8\u8BA4\u724C\u7EC4\u3002"}}};function Yr(e,t){let n=t.split("."),r=e;for(let i of n){if(!r||typeof r!="object"||!(i in r))return;r=r[i]}return typeof r=="string"?r:void 0}function xo(e,t,n){return t?e.replace(/\{\{\s*([\w.]+)\s*\}\}/g,(r,i)=>{let a=t[i];return a===void 0?"":Array.isArray(a)?ae(n,a.filter(o=>o!==void 0).map(o=>String(o))):String(a)}):e}function m(e,t){let n=ie(),i=Yr(n==="zh"?Zr:Fn,e)??Yr(Fn,e)??e;return xo(i,t,n)}var C=class extends Error{constructor(t,n,r){super(t),this.name="PluginUserError",this.userMessage={key:t,params:n},this.details=r}};function vn(e){return e instanceof C}function Te(e){return"rawMessage"in e?e.rawMessage:m(e.key,e.params)}function Xr(e){return"rawMessage"in e?e.rawMessage:m(e.key,e.params)}function Tn(e){let t=Te(e.userMessage);if(!e.details?.failures?.length)return t;let n=e.details.failures.map(r=>`${r.filePath}: ${Xr(r)}`).join(`
+"use strict";var Cn=Object.defineProperty;var So=Object.getOwnPropertyDescriptor;var Co=Object.getOwnPropertyNames;var Fo=Object.prototype.hasOwnProperty;var Qe=(e,t)=>{for(var n in t)Cn(e,n,{get:t[n],enumerable:!0})},Mo=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of Co(t))!Fo.call(e,i)&&i!==n&&Cn(e,i,{get:()=>t[i],enumerable:!(r=So(t,i))||r.enumerable});return e};var wo=e=>Mo(Cn({},"__esModule",{value:!0}),e);var bl={};Qe(bl,{default:()=>yl});module.exports=wo(bl);var bo=require("obsidian");function L(e,t){return`${e}:${t}`}function Fn(e){return e?.cardType==="basic"||e?.cardType==="semantic-qa"}function Kr(e){return e?.cardType==="cloze"}function G(e){return e?.cardType==="qa-group"}var Zr=require("obsidian");function ie(){return(0,Zr.getLanguage)()==="zh"?"zh":"en"}function ae(e,t){return t.join(e==="zh"?"\u3001":", ")}var Mn={commands:{syncCurrentFileToAnki:"Sync current file to Anki",syncVaultToAnki:"Sync vault to Anki",rebuildCardIndex:"Rebuild card index",clearCurrentFileSyncedCards:"Clear synced cards in current file",cleanupEmptyDecks:"Clean up empty decks"},settings:{pluginTitle:"Anki Heading Sync",ankiConnectUrl:{name:"AnkiConnect URL",desc:"Default is http://127.0.0.1:8765",placeholder:"http://127.0.0.1:8765"},qaHeadingLevel:{name:"QA heading level",desc:"Default is H4"},clozeHeadingLevel:{name:"Cloze heading level",desc:"Default is H5"},cardAnswerCutoffMode:{name:"Card answer cutoff mode",desc:"A valid sync marker always wins. Without a valid marker, either keep the whole heading block or stop before the first 2+ consecutive blank lines.",options:{headingBlock:"heading-block: whole heading block",doubleBlankLines:"double-blank-lines: stop before the first 2+ blank lines"}},qaGroup:{title:"QA Group 12",marker:{name:"QA Group marker",desc:"When a QA heading ends with this hashtag marker, the whole heading block syncs as one ObsiAnki QA Group 12 note.",placeholder:"#anki-list"},managedNoteType:"Managed note type: {{modelName}}. QA Group sync writes Stem / GroupId / Src / S01..S12 directly. The same note type can also appear in the field mapping panels below if you want to reuse it for other routes.",managedModelContract:"Managed model contract: {{fieldCount}} fields and {{templateCount}} templates are checked automatically during sync."},semanticQa:{title:"Semantic QA",marker:{name:"Semantic QA marker",desc:"When a QA heading ends with this hashtag marker, first-level list items with indented child content become separate cards.",placeholder:"#anki-list-qa"},previewTitle:"Semantic QA preview",triggerHeadingExample:"Trigger heading example: {{heading}}",previewUnavailable:"Preview unavailable. Use a trailing hashtag marker such as #anki-list-qa.",questionPreview:"Question preview: {{question}}",answerPreview:"Answer preview: {{answer}}"},syncOptions:{addObsidianBacklink:{name:"Add Obsidian backlink",desc:"Append a backlink to the source heading into synced cards."},obsidianBacklinkLabel:{name:"Obsidian backlink label",desc:"The text shown for the backlink. Blank input falls back to Open in Obsidian.",placeholder:"Open in Obsidian"},obsidianBacklinkPlacement:{name:"Obsidian backlink placement",desc:"Choose whether the backlink is appended to the question field or placed at the start or end of the answer body.",options:{questionLastLine:"Last line of question field",answerFirstLine:"First line of answer body",answerLastLine:"Last line of answer body"}},highlightsToCloze:{name:"Highlights to Cloze",desc:"Convert ==highlight== segments into cloze deletions for cloze cards."},syncObsidianTagsToAnki:{name:"Sync Obsidian tags to Anki",desc:"Sync Obsidian-recognized tags from the current note into Anki note tags, including nested tags. On each sync, these tags overwrite the managed tags in Anki based on the current Obsidian content."},keepPureTagLinesInCardBody:{name:"Keep pure tag lines in card body",desc:'When turned off, remove every line in the card body that contains only tags, such as "#ProjectA #\u91CD\u70B9/\u6848\u4F8B". Inline tags like "This is a #tag example" and lines with ordinary text like "\u6807\u7B7E\uFF1A#\u9879\u76EEA" are kept. Extra blank lines created by removal are cleaned up automatically.'}},mapping:{title:"Note type field mappings",refresh:{name:"Refresh note types from Anki",button:"Refresh note types"},status:{idle:"Refresh note types from Anki to load the available note types.",loadedCount:"Loaded {{count}} note types from Anki.",empty:"Anki returned no note types.",failedLoadNoteTypes:"Failed to load note types from Anki.",loadedSavedMapping:"Loaded saved mapping for {{modelName}}.",selectedModel:"Selected {{modelName}}. Read fields from Anki to create or refresh its mapping.",loadedFieldsForModel:"Loaded fields for {{modelName}}. Review the suggested mapping and save it.",failedLoadFields:"Failed to load fields for {{modelName}}.",noLoadedFields:"No loaded fields for {{modelName}}. Read fields from Anki first.",savedMapping:"Saved mapping for {{modelName}}.",failedSaveMapping:"Failed to save mapping for {{modelName}}.",savedMappingReady:"Saved mapping is ready for {{title}}.",noSavedMapping:"No saved mapping for {{title}}. Read fields from Anki first."},section:{basic:{title:"QA / Basic",description:"Choose the QA note type, read its fields from Anki, then confirm the title/body mapping."},cloze:{title:"Cloze",description:"Choose the cloze note type, read its fields from Anki, then confirm the main field mapping."},semanticQa:{title:"Semantic QA",description:"Choose the semantic QA note type, read its fields from Anki, then confirm the title/body mapping for child cards."}},noteTypeLabel:"{{title}} note type",noteTypeDesc:"Loaded from Anki note types. Refresh if the latest models are not shown.",fieldsLabel:"{{title}} fields",fieldsDesc:"Load the current note type fields from Anki, then review the suggested mapping.",readFieldsButton:"Read fields from Anki",loadedFields:"Loaded fields: {{fields}}",loadedFieldsNone:"Loaded fields: none. Read fields from Anki first.",mappingLabel:"{{title}} mapping",mappingDesc:"Save the current field mapping for this specific note type.",saveMappingButton:"Save mapping",titleFieldLabel:"{{title}} title field",titleFieldDesc:"Which Anki field should receive the heading/title fragment.",bodyFieldLabel:"{{title}} body field",bodyFieldDesc:"Which Anki field should receive the body fragment.",mainField:{name:"Cloze main field",desc:"The selected field receives title +
=p))){a=o===61?1:2;break}}if(e.sCount[u]<0)continue;let f=!1;for(let g=0,p=r.length;g
3||e.sCount[a]<0)continue;let c=!1;for(let d=0,l=r.length;d ${t} ]*>.*?<\/a><\/p>/g,"").replace(/ =p))){a=o===61?1:2;break}}if(e.sCount[u]<0)continue;let f=!1;for(let g=0,p=r.length;g 3||e.sCount[a]<0)continue;let c=!1;for(let l=0,d=r.length;l ${t} ]*>.*?<\/a><\/p>/g,"").replace(/ne((2147483647-i)/c)&&he("overflow"),i+=(s-r)*c,r=s;for(let d of e)if(d
${t}`:t,body:e.body};let r=`
]*>.*?<\/a>/g,"").trim()}function Zd(e){return e.includes("GroupId")&&e.includes("Src")&&e.some(n=>/^S\d+_Id$/i.test(n))&&e.some(n=>/^S\d+_Q$/i.test(n))&&e.some(n=>/^S\d+_A$/i.test(n))?e.filter(n=>n==="GroupId"||n==="Src"||/^S\d+_Id$/i.test(n)):[]}function Yd(e){let t=[];return{text:e.replace(Gd,r=>{let i=`@@QA_GROUP_INLINE_CODE_${t.length}@@`;return t.push(r),i}),restore:r=>t.reduce((i,a,o)=>i.split(`@@QA_GROUP_INLINE_CODE_${o}@@`).join(a),r)}}var Xd="manual-render-v3",we=class{constructor(t=new ze){this.deckResolutionService=t}resolve(t,n){let r=Jd(t.cardType,n),i=this.deckResolutionService.resolve(t,n.defaultDeck,n.folderDeckMode),a=i.resolvedDeck.value,o=n.noteFieldMappings[L(t.cardType,r)]??null,u={version:Xd,cardType:t.cardType,noteModel:r,mapping:o,addObsidianBacklink:n.addObsidianBacklink,obsidianBacklinkLabel:n.obsidianBacklinkLabel,obsidianBacklinkPlacement:n.obsidianBacklinkPlacement,convertHighlightsToCloze:n.convertHighlightsToCloze,keepPureTagLinesInCardBody:n.keepPureTagLinesInCardBody},s=H(JSON.stringify(u));return{deck:a,noteModel:r,renderConfigHash:s,warnings:i.warnings}}};function Jd(e,t){if(vi(e)){if(!t.clozeNoteType.trim())throw new C("errors.noteFieldMapping.noteTypeNotSelected.cloze");return t.clozeNoteType}if(e==="semantic-qa"){if(!t.semanticQaNoteType.trim())throw new C("errors.noteFieldMapping.noteTypeNotSelected.semanticQa");return t.semanticQaNoteType}if(!t.qaNoteType.trim())throw new C("errors.noteFieldMapping.noteTypeNotSelected.basic");return t.qaNoteType}var yn=class{constructor(t=new we){this.renderConfigService=t}plan(t,n,r,i){let a=new Map,o=new Set(n.pendingWriteBack.filter(k=>k.markerKind!=="group-gi").map(k=>Ee(k.filePath,k.blockStartLine,k.rawBlockHash))),u=new Set,s=[],c=[],d=[],l=[],f=[],g=new Map,p=0;for(let k of t){if(a.has(k.syncKey))throw new Error(`Duplicate syncKey detected in current scan: ${k.syncKey}`);if(a.set(k.syncKey,k),k.noteId!==void 0){let y=N(k.noteId);if(u.has(y))throw new Error(`Duplicate noteId detected in current scan: ${k.noteId}`);u.add(y)}let S=k.noteId!==void 0?n.cards[N(k.noteId)]:void 0,x=this.renderConfigService.resolve(k,i);for(let y of x.warnings)g.set(Oe(y),y);let h=k.noteId,w=Ee(k.filePath,k.blockStartLine,k.rawBlockHash),v={card:{...k,noteId:h},noteId:h,deck:x.deck,noteModel:x.noteModel,renderConfigHash:x.renderConfigHash};if(!h){s.push(v);continue}if(d.push(v),(k.idMarkerState!=="present-valid"||k.noteIdSource!=="marker"||o.has(w))&&f.push(v),!S){c.push(v);continue}let _=S.rawBlockHash!==k.rawBlockHash||S.renderConfigHash!==x.renderConfigHash||!Vi(S.tagsHint,k.tagsHint)||S.orphan,A=S.deck!==x.deck;_&&c.push(v),A&&l.push(v),!_&&!A&&(p+=1)}let M=new Set(r),b=Object.values(n.cards).filter(k=>M.has(k.filePath)&&!u.has(N(k.noteId)));return{toCreate:s,toUpdate:c,toVerifyDeck:d,toChangeDeck:l,toRewriteMarker:f,toOrphan:b,unchangedCards:p,warnings:Array.from(g.values())}}};var so=new tt({breaks:!0,html:!0,linkify:!0}),el=/`[^`\n]+`/g,tl=/```[\s\S]*?```|~~~[\s\S]*?~~~/g,nl=/(?`\\[${b.trim()}\\]`),c=c.replace(rl,(M,b)=>`\\(${b.trim()}\\)`),i&&r.convertHighlightsToCloze&&(c=c.replace(co,"{$1}")),i&&(c=c.replace(al,(M,b,k)=>`{{c${b?Number(b):l++}::${k}}}`)),c=c.replace(ol,(M,b)=>{let k=r.resourceResolver.resolveEmbed(b,n.card.filePath);return k?(d.push({kind:k.kind,fileName:k.fileName,absolutePath:k.absolutePath,altText:k.altText}),k.kind==="audio"?`[sound:${k.fileName}]`:``):M}),c=c.replace(ul,(M,b)=>{let k=r.resourceResolver.resolveWikiLink(b,n.card.filePath);return k?`${rt(k.displayText)}`:M}),c=c.replace(co,"$1");let f=Ur(c,il,"MATH");c=f.text,c=s.restore(c),c=u.restore(c);let g=(a?so.renderInline(c):so.render(c)).trim(),p=f.restore(g,rt);return{html:a?p:mn(p),media:d}}};function Ur(e,t,n){let r=[];return{text:e.replace(t,a=>{let o=`@@${n}_${r.length}@@`;return r.push(a),o}),restore:(a,o=u=>u)=>r.reduce((u,s,c)=>u.split(`@@${n}_${c}@@`).join(o(s)),a)}}function sl(e){let t=new Set;return e.filter(n=>{let r=`${n.kind}:${n.absolutePath}:${n.fileName}`;return t.has(r)?!1:(t.add(r),!0)})}function rt(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}var it=class extends C{constructor(t){super("errors.currentFileOutOfScope",{filePath:t}),this.name="CurrentFileOutOfScopeError"}},Sn=class{constructor(t,n,r,i=new Vt(t),a=new yn,o=new xn,u=new qt(r),s=new jt(t),c=new we,d=()=>Date.now()){this.vaultGateway=t;this.pluginStateRepository=n;this.ankiGateway=r;this.fileIndexerService=i;this.diffPlannerService=a;this.renderer=o;this.ankiBatchExecutor=u;this.markdownWriteBackService=s;this.scanScopeService=new Le;if(this.qaGroupSyncService=new bn(r,void 0,void 0,void 0,void 0,void 0,void 0,this.vaultGateway.createBacklink.bind(this.vaultGateway)),this.groupMarkerService=new V,cl(c)){this.renderConfigService=c,this.now=d;return}this.renderConfigService=new we,this.now=c}async syncVault(t){oe(t);let n=await this.pluginStateRepository.load(),r=await this.fileIndexerService.indexVault(t,n);return this.syncIndexedResult(r,n,t)}async syncFile(t,n){if(oe(n),!this.scanScopeService.isPathInScope(t,n.scopeMode,n.includeFolders,n.excludeFolders))throw new it(t);let r=await this.pluginStateRepository.load(),i=await this.fileIndexerService.indexFile(t,n,r);return this.syncIndexedResult(i,r,n)}async rebuildIndex(t){oe(t);let n=await this.pluginStateRepository.load(),r=await this.fileIndexerService.indexVault(t,n,!0),i=this.diffPlannerService.plan(r.cards,n,r.scopedFilePaths,t),a=new Map(r.indexedFiles.map(l=>[l.filePath,l])),o=i.toRewriteMarker.filter(l=>l.noteId!==void 0),u=this.buildReindexedGroupStates(r.groupBlocks,n),s=await this.markdownWriteBackService.write(o,a,u.markerWrites),c=lo(n,r.scopedFilePaths,u.syncedGroupBlocks),d=this.buildNextState(n,r.cards,r.indexedFiles,u.syncedGroupBlocks,t,new Set(s.writtenSyncKeys),new Map,i.toOrphan,c);if(d.pendingWriteBack=[...n.pendingWriteBack.filter(l=>!new Set(r.scopedFilePaths).has(l.filePath)),...s.pendingEntries],this.markFilesDirty(d,[...s.failureFiles.map(l=>l.filePath),...s.conflictFiles]),await this.pluginStateRepository.save(d),s.failureFiles.length>0)throw this.createWriteBackFailureError(s.failureFiles);return{scannedFiles:r.scannedFiles,scannedCards:r.cards.length+r.groupBlocks.length,created:0,updated:0,migratedNoteTypes:0,migratedDecks:0,orphaned:i.toOrphan.length+c.length,uploadedMedia:0,skippedUnchangedCards:r.skippedUnchangedCards,rewrittenMarkers:s.writtenSyncKeys.length,markerWriteConflictFiles:s.conflictFiles,warnings:po(i.warnings,u.warnings)}}async syncIndexedResult(t,n,r){let i=this.diffPlannerService.plan(t.cards,n,t.scopedFilePaths,r),a={addObsidianBacklink:r.addObsidianBacklink,obsidianBacklinkLabel:r.obsidianBacklinkLabel,obsidianBacklinkPlacement:r.obsidianBacklinkPlacement,convertHighlightsToCloze:r.convertHighlightsToCloze,keepPureTagLinesInCardBody:r.keepPureTagLinesInCardBody,resourceResolver:this.vaultGateway},o=new Map;for(let g of[...i.toCreate,...i.toUpdate])o.has(g.card.syncKey)||o.set(g.card.syncKey,this.renderer.render(g,a));let u=await this.ankiBatchExecutor.execute(i,o,async g=>this.renderer.render(g,a),r.noteFieldMappings),s=await this.qaGroupSyncService.sync(t.groupBlocks,n,r),c=new Map(t.indexedFiles.map(g=>[g.filePath,g])),d=await this.markdownWriteBackService.write(u.markerWrites,c,s.markerWrites),l=lo(n,t.scopedFilePaths,s.syncedGroupBlocks),f=this.buildNextState(n,t.cards,t.indexedFiles,s.syncedGroupBlocks,r,new Set([...u.touchedSyncKeys,...s.touchedSyncKeys,...d.writtenSyncKeys]),u.resolvedNoteIds,i.toOrphan,l);if(f.pendingWriteBack=[...n.pendingWriteBack.filter(g=>!new Set(t.scopedFilePaths).has(g.filePath)),...d.pendingEntries],this.markFilesDirty(f,[...d.failureFiles.map(g=>g.filePath),...d.conflictFiles]),await this.pluginStateRepository.save(f),d.failureFiles.length>0)throw this.createWriteBackFailureError(d.failureFiles);return{scannedFiles:t.scannedFiles,scannedCards:t.cards.length+t.groupBlocks.length,created:u.created+s.created,updated:u.updated+s.updated,migratedNoteTypes:u.migratedNoteTypes+s.migratedNoteTypes,migratedDecks:u.migratedDecks+s.migratedDecks,orphaned:i.toOrphan.length+l.length,uploadedMedia:u.uploadedMedia,skippedUnchangedCards:t.skippedUnchangedCards,rewrittenMarkers:d.writtenSyncKeys.length,markerWriteConflictFiles:d.conflictFiles,warnings:po(i.warnings,s.warnings)}}buildNextState(t,n,r,i,a,o,u,s,c){let d=this.now(),l=zn(a),f=new Set(r.filter(b=>b.content!==void 0).map(b=>b.filePath)),g=new Map;for(let b of i){let k=g.get(b.filePath);k?k.push(b):g.set(b.filePath,[b])}let p={files:{...t.files},cards:{...t.cards},groupBlocks:{...t.groupBlocks??{}},pendingWriteBack:[...t.pendingWriteBack]};for(let b of f)delete p.files[b];for(let[b,k]of Object.entries(p.cards))f.has(k.filePath)&&delete p.cards[b];for(let[b,k]of Object.entries(p.groupBlocks??{}))f.has(k.filePath)&&delete p.groupBlocks?.[b];let M=new Map;for(let b of n){let k=u.get(b.syncKey)??b.noteId;k!==void 0&&M.set(b.syncKey,k)}for(let b of r){if(b.content===void 0)continue;let k=Array.from(new Set(b.cards.map(S=>M.get(S.syncKey)).filter(S=>S!==void 0)));p.files[b.filePath]={filePath:b.filePath,fileHash:b.fileHash,fileStamp:b.fileStamp,deckRulesFingerprint:l,lastIndexedAt:d,noteIds:k,groupIds:(g.get(b.filePath)??[]).map(S=>S.groupId)}}for(let b of n){let k=M.get(b.syncKey);if(k===void 0)continue;let S=t.cards[N(k)]??(b.noteId!==void 0?t.cards[N(b.noteId)]:void 0),x=this.renderConfigService.resolve(b,a);p.cards[N(k)]={noteId:k,filePath:b.filePath,heading:b.heading,backlinkHeadingText:b.backlinkHeadingText,headingLevel:b.headingLevel,bodyMarkdown:b.bodyMarkdown,cardType:b.cardType,blockStartOffset:b.blockStartOffset,blockEndOffset:b.blockEndOffset,blockStartLine:b.blockStartLine,bodyStartLine:b.bodyStartLine,blockEndLine:b.blockEndLine,contentEndLine:b.contentEndLine,markerLine:b.markerLine,rawBlockText:b.rawBlockText,rawBlockHash:b.rawBlockHash,renderConfigHash:x.renderConfigHash,deck:x.deck,deckHint:b.deckHint,deckHintSource:b.deckHintSource,deckWarnings:[...b.deckWarnings],tagsHint:b.tagsHint,lastSyncedAt:o.has(b.syncKey)?d:S?.lastSyncedAt??0,orphan:!1}}for(let b of s)p.cards[N(b.noteId)]={...b,orphan:!0};for(let b of i)p.groupBlocks??(p.groupBlocks={}),p.groupBlocks[b.groupId]=b;for(let b of c)p.groupBlocks??(p.groupBlocks={}),p.groupBlocks[b.groupId]={...b,orphan:!0};return p}buildReindexedGroupStates(t,n){let r=[],i=[],a=[],o=new Map(Object.values(n.groupBlocks??{}).filter(s=>!s.orphan).map(s=>[s.noteId,s])),u=new Map;for(let s of Object.values(n.groupBlocks??{}).filter(c=>!c.orphan)){let c=u.get(s.src);c?c.push(s):u.set(s.src,[s])}for(let s of t){let c=(s.groupId?n.groupBlocks?.[s.groupId]:void 0)??(s.noteId!==void 0?o.get(s.noteId):void 0)??dl(u.get(s.src));if(!c)continue;let d=Object.fromEntries(c.items.filter(l=>l.itemId&&l.slot).map(l=>[l.itemId,l.slot]));r.push({...c,filePath:s.filePath,headingText:s.headingText,backlinkHeadingText:s.backlinkHeadingText,headingLevel:s.headingLevel,stem:s.stem,src:s.src,blockStartOffset:s.blockStartOffset,blockEndOffset:s.blockEndOffset,blockStartLine:s.blockStartLine,bodyStartLine:s.bodyStartLine,blockEndLine:s.blockEndLine,contentEndLine:s.contentEndLine,markerLine:s.markerLine,markerIndent:s.markerIndent,rawBlockText:s.rawBlockText,rawBlockHash:s.rawBlockHash,deckHint:s.deckHint,deckHintSource:s.deckHintSource,deckWarnings:[...s.deckWarnings],tagsHint:s.tagsHint?[...s.tagsHint]:[],orphan:!1}),a.push(...s.deckWarnings),s.sourceContent&&(s.markerState!=="present-valid"||!this.groupMarkerService.isEquivalent(s.groupMarker,c.noteId,d,c.freeSlots)||ll(s))&&i.push({syncKey:s.syncKey,filePath:s.filePath,blockStartLine:s.blockStartLine,contentEndLine:s.contentEndLine,blockEndLine:s.blockEndLine,markerLine:s.markerLine,markerIndent:s.markerIndent,noteId:c.noteId,groupId:c.groupId,itemToSlot:d,freeSlots:[...c.freeSlots],sourceContent:s.sourceContent})}return{syncedGroupBlocks:r,markerWrites:i,warnings:a}}markFilesDirty(t,n){for(let r of new Set(n))t.files[r]&&(t.files[r]={...t.files[r],fileHash:"",fileStamp:""})}createWriteBackFailureError(t){return new C("errors.writeBack.summary",{fileCount:t.length},{failures:t})}};function cl(e){return typeof e=="object"&&e!==null&&"resolve"in e}function lo(e,t,n){let r=new Set(t),i=new Set(n.map(a=>a.groupId));return Object.values(e.groupBlocks??{}).filter(a=>r.has(a.filePath)&&!i.has(a.groupId)).map(a=>({...a,orphan:!0}))}function po(...e){let t=new Map;for(let n of e)for(let r of n)t.set(Oe(r),r);return[...t.values()]}function dl(e){if(!(!e||e.length!==1))return e[0]}function ll(e){if(!e.sourceContent)return!1;let t=e.sourceContent.split(/\r?\n/);for(let n=e.blockStartLine;n
=4||e.src.charCodeAt(i)!==62)return!1;if(r)return!0;let u=[],s=[],c=[],l=[],d=e.md.block.ruler.getRules("blockquote"),f=e.parentType;e.parentType="blockquote";let g=!1,p;for(p=t;pne((2147483647-i)/c)&&ke("overflow"),i+=(s-r)*c,r=s;for(let l of e)if(l
${t}`:t,body:e.body};let r=`
]*>.*?<\/a>/g,"").trim()}function nl(e){return e.includes("GroupId")&&e.includes("Src")&&e.some(n=>/^S\d+_Id$/i.test(n))&&e.some(n=>/^S\d+_Q$/i.test(n))&&e.some(n=>/^S\d+_A$/i.test(n))?e.filter(n=>n==="GroupId"||n==="Src"||/^S\d+_Id$/i.test(n)):[]}function rl(e){let t=[];return{text:e.replace(Wd,r=>{let i=`@@QA_GROUP_INLINE_CODE_${t.length}@@`;return t.push(r),i}),restore:r=>t.reduce((i,a,o)=>i.split(`@@QA_GROUP_INLINE_CODE_${o}@@`).join(a),r)}}var il="manual-render-v3",ve=class{constructor(t=new Oe){this.deckResolutionService=t}resolve(t,n){let r=al(t.cardType,n),i=this.deckResolutionService.resolve(t,n.defaultDeck,n.folderDeckMode),a=i.resolvedDeck.value,o=n.noteFieldMappings[L(t.cardType,r)]??null,u={version:il,cardType:t.cardType,noteModel:r,mapping:o,addObsidianBacklink:n.addObsidianBacklink,obsidianBacklinkLabel:n.obsidianBacklinkLabel,obsidianBacklinkPlacement:n.obsidianBacklinkPlacement,convertHighlightsToCloze:n.convertHighlightsToCloze,keepPureTagLinesInCardBody:n.keepPureTagLinesInCardBody},s=O(JSON.stringify(u));return{deck:a,noteModel:r,renderConfigHash:s,warnings:i.warnings}}};function al(e,t){if(Ai(e)){if(!t.clozeNoteType.trim())throw new C("errors.noteFieldMapping.noteTypeNotSelected.cloze");return t.clozeNoteType}if(e==="semantic-qa"){if(!t.semanticQaNoteType.trim())throw new C("errors.noteFieldMapping.noteTypeNotSelected.semanticQa");return t.semanticQaNoteType}if(!t.qaNoteType.trim())throw new C("errors.noteFieldMapping.noteTypeNotSelected.basic");return t.qaNoteType}var yn=class{constructor(t=new ve){this.renderConfigService=t}plan(t,n,r,i){let a=new Map,o=new Set(n.pendingWriteBack.filter(y=>y.markerKind!=="group-gi").map(y=>Ee(y.filePath,y.blockStartLine,y.rawBlockHash))),u=new Set,s=[],c=[],l=[],d=[],f=[],g=new Map,p=0;for(let y of t){if(a.has(y.syncKey))throw new Error(`Duplicate syncKey detected in current scan: ${y.syncKey}`);if(a.set(y.syncKey,y),y.noteId!==void 0){let b=N(y.noteId);if(u.has(b))throw new Error(`Duplicate noteId detected in current scan: ${y.noteId}`);u.add(b)}let S=y.noteId!==void 0?n.cards[N(y.noteId)]:void 0,x=this.renderConfigService.resolve(y,i);for(let b of x.warnings)g.set(ze(b),b);let k=y.noteId,M=Ee(y.filePath,y.blockStartLine,y.rawBlockHash),v={card:{...y,noteId:k},noteId:k,deck:x.deck,noteModel:x.noteModel,renderConfigHash:x.renderConfigHash};if(!k){s.push(v);continue}if(l.push(v),(y.idMarkerState!=="present-valid"||y.noteIdSource!=="marker"||o.has(M))&&f.push(v),!S){c.push(v);continue}let _=S.rawBlockHash!==y.rawBlockHash||S.renderConfigHash!==x.renderConfigHash||!Ki(S.tagsHint,y.tagsHint)||S.orphan,A=S.deck!==x.deck;_&&c.push(v),A&&d.push(v),!_&&!A&&(p+=1)}let F=new Set(r),h=Object.values(n.cards).filter(y=>F.has(y.filePath)&&!u.has(N(y.noteId)));return{toCreate:s,toUpdate:c,toVerifyDeck:l,toChangeDeck:d,toRewriteMarker:f,toOrphan:h,unchangedCards:p,warnings:Array.from(g.values())}}};var mo=new tt({breaks:!0,html:!0,linkify:!0}),ol=/`[^`\n]+`/g,ul=/```[\s\S]*?```|~~~[\s\S]*?~~~/g,sl=/(?`\\[${h.trim()}\\]`),c=c.replace(cl,(F,h)=>`\\(${h.trim()}\\)`),i&&r.convertHighlightsToCloze&&(c=c.replace(ho,"{$1}")),i&&(c=c.replace(ll,(F,h,y)=>`{{c${h?Number(h):d++}::${y}}}`)),c=c.replace(pl,(F,h)=>{let y=r.resourceResolver.resolveEmbed(h,n.card.filePath);return y?(l.push({kind:y.kind,fileName:y.fileName,absolutePath:y.absolutePath,altText:y.altText}),y.kind==="audio"?`[sound:${y.fileName}]`:``):F}),c=c.replace(fl,(F,h)=>{let y=r.resourceResolver.resolveWikiLink(h,n.card.filePath);return y?`${rt(y.displayText)}`:F}),c=c.replace(ho,"$1");let f=jr(c,dl,"MATH");c=f.text,c=s.restore(c),c=u.restore(c);let g=(a?mo.renderInline(c):mo.render(c)).trim(),p=f.restore(g,rt);return{html:a?p:gn(p),media:l}}};function jr(e,t,n){let r=[];return{text:e.replace(t,a=>{let o=`@@${n}_${r.length}@@`;return r.push(a),o}),restore:(a,o=u=>u)=>r.reduce((u,s,c)=>u.split(`@@${n}_${c}@@`).join(o(s)),a)}}function gl(e){let t=new Set;return e.filter(n=>{let r=`${n.kind}:${n.absolutePath}:${n.fileName}`;return t.has(r)?!1:(t.add(r),!0)})}function rt(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}var it=class extends C{constructor(t){super("errors.currentFileOutOfScope",{filePath:t}),this.name="CurrentFileOutOfScopeError"}},xn=class{constructor(t,n,r,i=new Ut(t),a=new yn,o=new bn,u=new Bt(r),s=new jt(t),c=new ve,l=()=>Date.now()){this.vaultGateway=t;this.pluginStateRepository=n;this.ankiGateway=r;this.fileIndexerService=i;this.diffPlannerService=a;this.renderer=o;this.ankiBatchExecutor=u;this.markdownWriteBackService=s;this.scanScopeService=new Le;if(this.qaGroupSyncService=new kn(r,void 0,void 0,void 0,void 0,void 0,void 0,this.vaultGateway.createBacklink.bind(this.vaultGateway)),this.groupMarkerService=new j,ml(c)){this.renderConfigService=c,this.now=l;return}this.renderConfigService=new ve,this.now=c}async syncVault(t){oe(t);let n=await this.pluginStateRepository.load(),r=await this.fileIndexerService.indexVault(t,n);return this.syncIndexedResult(r,n,t)}async syncFile(t,n){if(oe(n),!this.scanScopeService.isPathInScope(t,n.scopeMode,n.includeFolders,n.excludeFolders))throw new it(t);let r=await this.pluginStateRepository.load(),i=await this.fileIndexerService.indexFile(t,n,r);return this.syncIndexedResult(i,r,n)}async rebuildIndex(t){oe(t);let n=await this.pluginStateRepository.load(),r=await this.fileIndexerService.indexVault(t,n,!0),i=this.diffPlannerService.plan(r.cards,n,r.scopedFilePaths,t),a=new Map(r.indexedFiles.map(d=>[d.filePath,d])),o=i.toRewriteMarker.filter(d=>d.noteId!==void 0),u=this.buildReindexedGroupStates(r.groupBlocks,n),s=await this.markdownWriteBackService.write(o,a,u.markerWrites),c=ko(n,r.scopedFilePaths,u.syncedGroupBlocks),l=this.buildNextState(n,r.cards,r.indexedFiles,u.syncedGroupBlocks,t,new Set(s.writtenSyncKeys),new Map,i.toOrphan,c);if(l.pendingWriteBack=[...n.pendingWriteBack.filter(d=>!new Set(r.scopedFilePaths).has(d.filePath)),...s.pendingEntries],this.markFilesDirty(l,[...s.failureFiles.map(d=>d.filePath),...s.conflictFiles]),await this.pluginStateRepository.save(l),s.failureFiles.length>0)throw this.createWriteBackFailureError(s.failureFiles);return{scannedFiles:r.scannedFiles,scannedCards:r.cards.length+r.groupBlocks.length,created:0,updated:0,migratedNoteTypes:0,migratedDecks:0,orphaned:i.toOrphan.length+c.length,uploadedMedia:0,skippedUnchangedCards:r.skippedUnchangedCards,rewrittenMarkers:s.writtenSyncKeys.length,markerWriteConflictFiles:s.conflictFiles,warnings:yo(i.warnings,u.warnings)}}async syncIndexedResult(t,n,r){let i=this.diffPlannerService.plan(t.cards,n,t.scopedFilePaths,r),a={addObsidianBacklink:r.addObsidianBacklink,obsidianBacklinkLabel:r.obsidianBacklinkLabel,obsidianBacklinkPlacement:r.obsidianBacklinkPlacement,convertHighlightsToCloze:r.convertHighlightsToCloze,keepPureTagLinesInCardBody:r.keepPureTagLinesInCardBody,resourceResolver:this.vaultGateway},o=new Map;for(let g of[...i.toCreate,...i.toUpdate])o.has(g.card.syncKey)||o.set(g.card.syncKey,this.renderer.render(g,a));let u=await this.ankiBatchExecutor.execute(i,o,async g=>this.renderer.render(g,a),r.noteFieldMappings),s=await this.qaGroupSyncService.sync(t.groupBlocks,n,r),c=new Map(t.indexedFiles.map(g=>[g.filePath,g])),l=await this.markdownWriteBackService.write(u.markerWrites,c,s.markerWrites),d=ko(n,t.scopedFilePaths,s.syncedGroupBlocks),f=this.buildNextState(n,t.cards,t.indexedFiles,s.syncedGroupBlocks,r,new Set([...u.touchedSyncKeys,...s.touchedSyncKeys,...l.writtenSyncKeys]),u.resolvedNoteIds,i.toOrphan,d);if(f.pendingWriteBack=[...n.pendingWriteBack.filter(g=>!new Set(t.scopedFilePaths).has(g.filePath)),...l.pendingEntries],this.markFilesDirty(f,[...l.failureFiles.map(g=>g.filePath),...l.conflictFiles]),await this.pluginStateRepository.save(f),l.failureFiles.length>0)throw this.createWriteBackFailureError(l.failureFiles);return{scannedFiles:t.scannedFiles,scannedCards:t.cards.length+t.groupBlocks.length,created:u.created+s.created,updated:u.updated+s.updated,migratedNoteTypes:u.migratedNoteTypes+s.migratedNoteTypes,migratedDecks:u.migratedDecks+s.migratedDecks,orphaned:i.toOrphan.length+d.length,uploadedMedia:u.uploadedMedia,skippedUnchangedCards:t.skippedUnchangedCards,rewrittenMarkers:l.writtenSyncKeys.length,markerWriteConflictFiles:l.conflictFiles,warnings:yo(i.warnings,s.warnings)}}buildNextState(t,n,r,i,a,o,u,s,c){let l=this.now(),d=zn(a),f=new Set(r.filter(h=>h.content!==void 0).map(h=>h.filePath)),g=new Map;for(let h of i){let y=g.get(h.filePath);y?y.push(h):g.set(h.filePath,[h])}let p={files:{...t.files},cards:{...t.cards},groupBlocks:{...t.groupBlocks??{}},pendingWriteBack:[...t.pendingWriteBack]};for(let h of f)delete p.files[h];for(let[h,y]of Object.entries(p.cards))f.has(y.filePath)&&delete p.cards[h];for(let[h,y]of Object.entries(p.groupBlocks??{}))f.has(y.filePath)&&delete p.groupBlocks?.[h];let F=new Map;for(let h of n){let y=u.get(h.syncKey)??h.noteId;y!==void 0&&F.set(h.syncKey,y)}for(let h of r){if(h.content===void 0)continue;let y=Array.from(new Set(h.cards.map(S=>F.get(S.syncKey)).filter(S=>S!==void 0)));p.files[h.filePath]={filePath:h.filePath,fileHash:h.fileHash,fileStamp:h.fileStamp,deckRulesFingerprint:d,lastIndexedAt:l,noteIds:y,groupIds:(g.get(h.filePath)??[]).map(S=>S.groupId)}}for(let h of n){let y=F.get(h.syncKey);if(y===void 0)continue;let S=t.cards[N(y)]??(h.noteId!==void 0?t.cards[N(h.noteId)]:void 0),x=this.renderConfigService.resolve(h,a);p.cards[N(y)]={noteId:y,filePath:h.filePath,heading:h.heading,backlinkHeadingText:h.backlinkHeadingText,headingLevel:h.headingLevel,bodyMarkdown:h.bodyMarkdown,cardType:h.cardType,blockStartOffset:h.blockStartOffset,blockEndOffset:h.blockEndOffset,blockStartLine:h.blockStartLine,bodyStartLine:h.bodyStartLine,blockEndLine:h.blockEndLine,contentEndLine:h.contentEndLine,markerLine:h.markerLine,rawBlockText:h.rawBlockText,rawBlockHash:h.rawBlockHash,renderConfigHash:x.renderConfigHash,deck:x.deck,deckHint:h.deckHint,deckHintSource:h.deckHintSource,deckWarnings:[...h.deckWarnings],tagsHint:h.tagsHint,lastSyncedAt:o.has(h.syncKey)?l:S?.lastSyncedAt??0,orphan:!1}}for(let h of s)p.cards[N(h.noteId)]={...h,orphan:!0};for(let h of i)p.groupBlocks??(p.groupBlocks={}),p.groupBlocks[h.groupId]=h;for(let h of c)p.groupBlocks??(p.groupBlocks={}),p.groupBlocks[h.groupId]={...h,orphan:!0};return p}buildReindexedGroupStates(t,n){let r=[],i=[],a=[],o=new Map(Object.values(n.groupBlocks??{}).filter(s=>!s.orphan).map(s=>[s.noteId,s])),u=new Map;for(let s of Object.values(n.groupBlocks??{}).filter(c=>!c.orphan)){let c=u.get(s.src);c?c.push(s):u.set(s.src,[s])}for(let s of t){let c=(s.groupId?n.groupBlocks?.[s.groupId]:void 0)??(s.noteId!==void 0?o.get(s.noteId):void 0)??hl(u.get(s.src));if(!c)continue;let l=Object.fromEntries(c.items.filter(d=>d.itemId&&d.slot).map(d=>[d.itemId,d.slot]));r.push({...c,filePath:s.filePath,headingText:s.headingText,backlinkHeadingText:s.backlinkHeadingText,headingLevel:s.headingLevel,stem:s.stem,src:s.src,blockStartOffset:s.blockStartOffset,blockEndOffset:s.blockEndOffset,blockStartLine:s.blockStartLine,bodyStartLine:s.bodyStartLine,blockEndLine:s.blockEndLine,contentEndLine:s.contentEndLine,markerLine:s.markerLine,markerIndent:s.markerIndent,rawBlockText:s.rawBlockText,rawBlockHash:s.rawBlockHash,deckHint:s.deckHint,deckHintSource:s.deckHintSource,deckWarnings:[...s.deckWarnings],tagsHint:s.tagsHint?[...s.tagsHint]:[],orphan:!1}),a.push(...s.deckWarnings),s.sourceContent&&(s.markerState!=="present-valid"||!this.groupMarkerService.isEquivalent(s.groupMarker,c.noteId,l,c.freeSlots)||kl(s))&&i.push({syncKey:s.syncKey,filePath:s.filePath,blockStartLine:s.blockStartLine,contentEndLine:s.contentEndLine,blockEndLine:s.blockEndLine,markerLine:s.markerLine,markerIndent:s.markerIndent,noteId:c.noteId,groupId:c.groupId,itemToSlot:l,freeSlots:[...c.freeSlots],sourceContent:s.sourceContent})}return{syncedGroupBlocks:r,markerWrites:i,warnings:a}}markFilesDirty(t,n){for(let r of new Set(n))t.files[r]&&(t.files[r]={...t.files[r],fileHash:"",fileStamp:""})}createWriteBackFailureError(t){return new C("errors.writeBack.summary",{fileCount:t.length},{failures:t})}};function ml(e){return typeof e=="object"&&e!==null&&"resolve"in e}function ko(e,t,n){let r=new Set(t),i=new Set(n.map(a=>a.groupId));return Object.values(e.groupBlocks??{}).filter(a=>r.has(a.filePath)&&!i.has(a.groupId)).map(a=>({...a,orphan:!0}))}function yo(...e){let t=new Map;for(let n of e)for(let r of n)t.set(ze(r),r);return[...t.values()]}function hl(e){if(!(!e||e.length!==1))return e[0]}function kl(e){if(!e.sourceContent)return!1;let t=e.sourceContent.split(/\r?\n/);for(let n=e.blockStartLine;n
Body sync-migrate-cloze",
+ Text: "Heading sync-migrate-cloze
Body sync-migrate-cloze",
},
},
]);
@@ -331,7 +331,7 @@ describe("AnkiBatchExecutor", () => {
it("surfaces the existing mapping error instead of migrating when the target mapping is incomplete", async () => {
const ankiGateway = new CountingAnkiGateway();
- ankiGateway.modelDetailsByName["New Basic"] = { fieldNames: ["Front", "Back"], isCloze: false };
+ ankiGateway.modelDetailsByName["New Basic"] = { fieldNames: ["Front", "Back"] };
ankiGateway.noteSummariesById.set(300, {
noteId: 300,
modelName: "Old Basic",
@@ -427,4 +427,4 @@ function createIndexedCard(syncKey: string, noteId?: number, cardType: IndexedCa
deckWarnings: [],
tagsHint: [],
};
-}
\ No newline at end of file
+}
diff --git a/src/application/services/NoteFieldMappingService.test.ts b/src/application/services/NoteFieldMappingService.test.ts
index 015d3c2..d4bc1f9 100644
--- a/src/application/services/NoteFieldMappingService.test.ts
+++ b/src/application/services/NoteFieldMappingService.test.ts
@@ -46,7 +46,6 @@ describe("NoteFieldMappingService", () => {
card,
{
fieldNames: ["Title", "Body"],
- isCloze: false,
},
{
[createNoteFieldMappingKey("basic", "Basic")]: {
@@ -76,7 +75,6 @@ describe("NoteFieldMappingService", () => {
}),
{
fieldNames: ["Text", "Extra"],
- isCloze: false,
},
{
[createNoteFieldMappingKey("cloze", "Cloze")]: {
@@ -89,7 +87,7 @@ describe("NoteFieldMappingService", () => {
},
);
- expect(fields).toEqual({ Text: "Context
{{c1::answer}}" });
+ expect(fields).toEqual({ Text: "Context
{{c1::answer}}" });
});
it("throws when a cloze main field is missing from the saved mapping", () => {
@@ -101,7 +99,7 @@ describe("NoteFieldMappingService", () => {
type: "cloze",
noteModel: "Cloze",
}),
- { fieldNames: ["Text", "Extra"], isCloze: false },
+ { fieldNames: ["Text", "Extra"] },
{
[createNoteFieldMappingKey("cloze", "Cloze")]: {
cardType: "cloze",
@@ -118,7 +116,7 @@ describe("NoteFieldMappingService", () => {
expect(error.userMessage.params).toEqual({ modelName: "Cloze" });
});
- it("throws when a cloze main field is stale even if isCloze is false", () => {
+ it("throws when a cloze main field is stale based on saved field existence only", () => {
const service = new NoteFieldMappingService();
const error = expectPluginUserError(() =>
@@ -127,7 +125,7 @@ describe("NoteFieldMappingService", () => {
type: "cloze",
noteModel: "Cloze",
}),
- { fieldNames: ["Body", "Extra"], isCloze: false },
+ { fieldNames: ["Body", "Extra"] },
{
[createNoteFieldMappingKey("cloze", "Cloze")]: {
cardType: "cloze",
@@ -151,7 +149,7 @@ describe("NoteFieldMappingService", () => {
const service = new NoteFieldMappingService();
const error = expectPluginUserError(() =>
- service.map(createCard({}), { fieldNames: ["Front", "Back"], isCloze: false }, {}),
+ service.map(createCard({}), { fieldNames: ["Front", "Back"] }, {}),
);
expect(error.userMessage.key).toBe("errors.noteFieldMapping.missingSavedMapping.basic");
@@ -164,7 +162,7 @@ describe("NoteFieldMappingService", () => {
const error = expectPluginUserError(() =>
service.map(
createCard({}),
- { fieldNames: ["Front", "Body"], isCloze: false },
+ { fieldNames: ["Front", "Body"] },
{
[createNoteFieldMappingKey("basic", "Basic")]: {
cardType: "basic",
@@ -191,7 +189,7 @@ describe("NoteFieldMappingService", () => {
const error = expectPluginUserError(() =>
service.map(
createCard({}),
- { fieldNames: ["Front", "Back"], isCloze: false },
+ { fieldNames: ["Front", "Back"] },
{
[createNoteFieldMappingKey("basic", "Basic")]: {
cardType: "basic",
@@ -208,4 +206,4 @@ describe("NoteFieldMappingService", () => {
expect(error.userMessage.key).toBe("errors.noteFieldMapping.titleBodyMustDiffer.basic");
expect(error.userMessage.params).toEqual({ modelName: "Basic" });
});
-});
\ No newline at end of file
+});
diff --git a/src/application/services/NoteFieldMappingService.ts b/src/application/services/NoteFieldMappingService.ts
index d5cb355..b3d1c5a 100644
--- a/src/application/services/NoteFieldMappingService.ts
+++ b/src/application/services/NoteFieldMappingService.ts
@@ -135,7 +135,7 @@ export class NoteFieldMappingService {
}
return {
- [mapping.mainField]: `${card.renderedFields.title}
${card.renderedFields.body}`,
+ [mapping.mainField]: `${card.renderedFields.title}
${card.renderedFields.body}`,
};
}
diff --git a/src/application/services/QaGroupFieldMappingService.test.ts b/src/application/services/QaGroupFieldMappingService.test.ts
index 0c82aae..4d9e9c5 100644
--- a/src/application/services/QaGroupFieldMappingService.test.ts
+++ b/src/application/services/QaGroupFieldMappingService.test.ts
@@ -3,8 +3,6 @@ import { describe, expect, it } from "vitest";
import { PluginUserError } from "@/application/errors/PluginUserError";
import {
- areQaGroupWarningsAccepted,
- parseQaGroupFieldWarning,
QaGroupFieldMappingService,
} from "./QaGroupFieldMappingService";
@@ -30,6 +28,11 @@ describe("QaGroupFieldMappingService", () => {
modelName: "问答题(多级列表)",
loadedFieldNames: ["题目", "问题01", "答案01", "问题02", "答案02"],
titleField: "题目",
+ derivation: {
+ mode: "first-pair",
+ firstQuestionField: "问题01",
+ firstAnswerField: "答案01",
+ },
slots: [
{
index: 1,
@@ -68,28 +71,48 @@ describe("QaGroupFieldMappingService", () => {
]);
});
- it("records warnings for incomplete tail slots and preserves accepted warnings only when they still match", () => {
+ it("derives slots from a selected first pair and stops at the first missing pair", () => {
const service = new QaGroupFieldMappingService();
- const mapping = service.suggest("问答题(多级列表)", ["题目", "问题01", "答案01", "问题02"], 1);
+ const mapping = service.createMappingFromSelection(
+ "问答题(多级列表)",
+ ["题目", "问题01", "答案01", "问题02"],
+ {
+ titleField: "题目",
+ firstQuestionField: "问题01",
+ firstAnswerField: "答案01",
+ },
+ 1,
+ );
expect(mapping.slots).toEqual([
{ index: 1, questionField: "问题01", answerField: "答案01" },
]);
- expect(mapping.warnings).toHaveLength(1);
- expect(parseQaGroupFieldWarning(mapping.warnings[0] ?? "")).toEqual({
- kind: "missing-answer",
- index: 2,
- questionField: "问题02",
- answerField: "答案02",
+ expect(mapping.derivation).toEqual({
+ mode: "first-pair",
+ firstQuestionField: "问题01",
+ firstAnswerField: "答案01",
});
+ expect(mapping.warnings).toEqual([]);
expect(mapping.acceptedWarnings).toBeUndefined();
+ });
- const accepted = service.suggest("问答题(多级列表)", ["题目", "问题01", "答案01", "问题02"], 1, mapping.warnings);
- expect(accepted.acceptedWarnings).toEqual(accepted.warnings);
- expect(areQaGroupWarningsAccepted(accepted.warnings, accepted.acceptedWarnings)).toBe(true);
+ it("keeps a single slot when the selected first pair does not contain numbers", () => {
+ const service = new QaGroupFieldMappingService();
- const reset = service.suggest("问答题(多级列表)", ["题目", "问题01", "答案01", "问题02", "答案03"], 1, mapping.warnings);
- expect(reset.acceptedWarnings).toBeUndefined();
+ const mapping = service.createMappingFromSelection(
+ "Custom QA Group",
+ ["题目", "Question", "Answer"],
+ {
+ titleField: "题目",
+ firstQuestionField: "Question",
+ firstAnswerField: "Answer",
+ },
+ 1,
+ );
+
+ expect(mapping.slots).toEqual([
+ { index: 1, questionField: "Question", answerField: "Answer" },
+ ]);
});
it("leaves the title field unset when no preferred title field exists", () => {
@@ -117,4 +140,22 @@ describe("QaGroupFieldMappingService", () => {
firstIndex: 2,
});
});
+
+ it("rejects selected first pairs whose numbers do not line up", () => {
+ const service = new QaGroupFieldMappingService();
+
+ const error = expectPluginUserError(() => {
+ service.createMappingFromSelection(
+ "问答题(多级列表)",
+ ["题目", "问题01", "答案02"],
+ {
+ titleField: "题目",
+ firstQuestionField: "问题01",
+ firstAnswerField: "答案02",
+ },
+ );
+ });
+
+ expect(error.userMessage.key).toBe("errors.noteFieldMapping.qaGroupFirstPairNumberMismatch");
+ });
});
diff --git a/src/application/services/QaGroupFieldMappingService.ts b/src/application/services/QaGroupFieldMappingService.ts
index 5b2e8cc..0801123 100644
--- a/src/application/services/QaGroupFieldMappingService.ts
+++ b/src/application/services/QaGroupFieldMappingService.ts
@@ -1,4 +1,4 @@
-import type { QaGroupFieldMapping, QaGroupSlotMapping } from "@/application/config/NoteModelFieldMapping";
+import type { QaGroupFieldDerivation, QaGroupFieldMapping, QaGroupSlotMapping } from "@/application/config/NoteModelFieldMapping";
import { PluginUserError } from "@/application/errors/PluginUserError";
type QaGroupFieldWarningKind = "missing-answer" | "missing-question";
@@ -10,9 +10,17 @@ interface QaGroupFieldWarningPayload {
answerField: string;
}
-interface QaGroupDetectedFields {
- slots: QaGroupSlotMapping[];
- warnings: string[];
+interface QaGroupFieldSelection {
+ titleField?: string;
+ firstQuestionField?: string;
+ firstAnswerField?: string;
+}
+
+interface IndexedFieldSegment {
+ prefix: string;
+ digits: string;
+ suffix: string;
+ index: number;
}
const TITLE_FIELD_PRIORITY = ["题目", "标题", "正面", "Stem", "Title"] as const;
@@ -27,21 +35,56 @@ export class QaGroupFieldMappingService {
acceptedWarnings?: string[],
preferredTitleField?: string,
): QaGroupFieldMapping {
+ void acceptedWarnings;
+
const titleField = preferredTitleField && fieldNames.includes(preferredTitleField)
? preferredTitleField
: findFieldName(fieldNames, TITLE_FIELD_PRIORITY);
- const detectedFields = detectSlots(fieldNames, modelName);
+ const firstPair = detectSuggestedFirstPair(fieldNames, modelName);
+
+ return this.createMappingFromSelection(
+ modelName,
+ fieldNames,
+ {
+ titleField,
+ firstQuestionField: firstPair.questionField,
+ firstAnswerField: firstPair.answerField,
+ },
+ loadedAt,
+ );
+ }
+
+ createMappingFromSelection(
+ modelName: string,
+ fieldNames: string[],
+ selection: QaGroupFieldSelection,
+ loadedAt = Date.now(),
+ ): QaGroupFieldMapping {
+ const loadedFieldNames = [...fieldNames];
+ const titleField = normalizeOptionalFieldName(selection.titleField);
+ const firstQuestionField = normalizeOptionalFieldName(selection.firstQuestionField);
+ const firstAnswerField = normalizeOptionalFieldName(selection.firstAnswerField);
+ const derivation = buildQaGroupDerivation(firstQuestionField, firstAnswerField);
+ const availableFields = new Set(loadedFieldNames);
+
+ for (const field of [titleField, firstQuestionField, firstAnswerField]) {
+ if (field && !availableFields.has(field)) {
+ throw new PluginUserError("errors.noteFieldMapping.qaGroupSelectedFieldMissing", {
+ modelName,
+ field,
+ });
+ }
+ }
return {
cardType: "qa-group",
modelName,
- loadedFieldNames: [...fieldNames],
+ loadedFieldNames,
titleField,
- slots: detectedFields.slots,
- warnings: detectedFields.warnings,
- acceptedWarnings: detectedFields.warnings.length > 0 && areQaGroupWarningsAccepted(detectedFields.warnings, acceptedWarnings)
- ? [...detectedFields.warnings]
- : undefined,
+ slots: deriveQaGroupSlots(modelName, loadedFieldNames, titleField, firstQuestionField, firstAnswerField),
+ warnings: [],
+ acceptedWarnings: undefined,
+ derivation,
loadedAt,
};
}
@@ -61,6 +104,8 @@ export class QaGroupFieldMappingService {
const availableFields = new Set(fieldNames);
const missingFields = new Set
+ body during sync.",
+ desc: "The selected field receives title +
+ body during sync.",
},
selectFieldPlaceholder: "-- Select field --",
},
@@ -255,13 +255,18 @@ export const en = {
mainField: "Main field:",
fields: "Fields:",
qaGroupTitleField: "Title field:",
+ qaGroupFirstQuestionField: "First question field:",
+ qaGroupFirstAnswerField: "First answer field:",
+ qaGroupConfiguredSlots: "Configured pairs:",
qaGroupSlotFields: "Q/A fields:",
qaGroupWarnings: "Warnings:",
},
qaGroup: {
noModel: "Select an Anki note type first",
unavailable: "Read fields from Anki first",
- detectedSlots: "Detected {{count}} pair(s)",
+ detectedSlots: "Configured {{count}} pair(s)",
+ configuredSlots: "Configured {{count}} pair(s)",
+ configuredSlotExample: "First {{firstQuestionField}} / {{firstAnswerField}}, last {{lastQuestionField}} / {{lastAnswerField}}",
acceptWarnings: "I confirm these warnings can be ignored and sync may continue",
warning: {
missingAnswer: "Slot {{index}} is missing its answer field, so {{questionField}} / {{answerField}} was ignored.",
@@ -416,6 +421,10 @@ export const en = {
noteFieldMappingsQaGroupWarningsStrings: "QA Group field mapping warnings can only contain strings.",
noteFieldMappingsQaGroupAcceptedWarningsArray: "QA Group field mapping acceptedWarnings must be an array.",
noteFieldMappingsQaGroupAcceptedWarningsStrings: "QA Group field mapping acceptedWarnings can only contain strings.",
+ noteFieldMappingsQaGroupDerivationObject: "QA Group field mapping derivation must be an object.",
+ noteFieldMappingsQaGroupDerivationMode: "QA Group field mapping derivation.mode must be first-pair.",
+ noteFieldMappingsQaGroupDerivationFirstQuestionField: "QA Group field mapping derivation.firstQuestionField must be a string.",
+ noteFieldMappingsQaGroupDerivationFirstAnswerField: "QA Group field mapping derivation.firstAnswerField must be a string.",
},
noteFieldMapping: {
noteTypeNotSelected: {
@@ -440,10 +449,13 @@ export const en = {
semanticQa: "Semantic QA note type \"{{modelName}}\" must use different title and body fields.",
},
stale: "Saved field mapping for note type \"{{modelName}}\" is stale because these fields no longer exist in Anki: {{fields}}. Open plugin settings and read fields from Anki again.",
- clozeIncompatible: "Cloze note type \"{{modelName}}\" is not cloze-compatible in Anki.",
qaGroupMissingTitle: "Saved field mapping for QA Group note type \"{{modelName}}\" is incomplete because the title field is missing. Save the mapping again in settings.",
qaGroupNoCompleteSlots: "Saved field mapping for QA Group note type \"{{modelName}}\" is incomplete because it has no saved question/answer slot pairs. Save the mapping again in settings.",
qaGroupNonContinuousSlots: "QA Group note type \"{{modelName}}\" must start its question/answer slots at pair {{firstIndex}} = 1. Renumber the first pair to 1.",
+ qaGroupSelectedFieldMissing: "The selected field \"{{field}}\" for QA Group note type \"{{modelName}}\" is no longer present in Anki. Read fields again and save the mapping again.",
+ qaGroupDuplicateFields: "QA Group note type \"{{modelName}}\" cannot reuse the same title, question, or answer field.",
+ qaGroupFirstPairNumberMismatch: "The first question field \"{{questionField}}\" and first answer field \"{{answerField}}\" for QA Group note type \"{{modelName}}\" do not use the same pair number.",
+ qaGroupFirstPairMustStartAtOne: "QA Group note type \"{{modelName}}\" must start its first selected pair at 1, but the current pair starts at {{firstIndex}}.",
qaGroupWarningsUnaccepted: "QA Group note type \"{{modelName}}\" still has unconfirmed field warnings. Confirm them in settings before syncing.",
qaGroupSlotCapacityExceeded: "Saved field mapping for QA Group note type \"{{modelName}}\" only includes {{capacity}} question/answer pair(s), but the current block has {{itemCount}} item(s): {{filePath}}:{{blockStartLine}}.",
},
diff --git a/src/presentation/i18n/messages/zh.ts b/src/presentation/i18n/messages/zh.ts
index 7a58ca8..26e8d50 100644
--- a/src/presentation/i18n/messages/zh.ts
+++ b/src/presentation/i18n/messages/zh.ts
@@ -137,7 +137,7 @@ export const zh = {
bodyFieldDesc: "选择接收正文片段的 Anki 字段。",
mainField: {
name: "填空题主字段",
- desc: "同步时,所选字段会接收 标题 +
+ 正文。",
+ desc: "同步时,所选字段会接收 标题 +
+ 正文。",
},
selectFieldPlaceholder: "-- 选择字段 --",
},
@@ -253,13 +253,18 @@ export const zh = {
mainField: "主字段:",
fields: "字段:",
qaGroupTitleField: "题目字段:",
+ qaGroupFirstQuestionField: "首个问题字段:",
+ qaGroupFirstAnswerField: "首个答案字段:",
+ qaGroupConfiguredSlots: "已配置组数:",
qaGroupSlotFields: "问题/答案字段:",
qaGroupWarnings: "警告:",
},
qaGroup: {
noModel: "请先选择 Anki 笔记模板",
unavailable: "请先手动读取字段",
- detectedSlots: "已识别 {{count}} 组",
+ detectedSlots: "已配置{{count}}组",
+ configuredSlots: "已配置{{count}}组",
+ configuredSlotExample: "首组 {{firstQuestionField}} / {{firstAnswerField}},末组 {{lastQuestionField}} / {{lastAnswerField}}",
acceptWarnings: "我已确认忽略当前警告,并允许同步",
warning: {
missingAnswer: "第 {{index}} 组缺少答案字段,已忽略 {{questionField}} / {{answerField}}。",
@@ -414,6 +419,10 @@ export const zh = {
noteFieldMappingsQaGroupWarningsStrings: "QA Group 字段映射中的 warnings 只能包含字符串。",
noteFieldMappingsQaGroupAcceptedWarningsArray: "QA Group 字段映射中的 acceptedWarnings 必须是数组。",
noteFieldMappingsQaGroupAcceptedWarningsStrings: "QA Group 字段映射中的 acceptedWarnings 只能包含字符串。",
+ noteFieldMappingsQaGroupDerivationObject: "QA Group 字段映射中的 derivation 必须是对象。",
+ noteFieldMappingsQaGroupDerivationMode: "QA Group 字段映射中的 derivation.mode 只能是 first-pair。",
+ noteFieldMappingsQaGroupDerivationFirstQuestionField: "QA Group 字段映射中的 derivation.firstQuestionField 必须是字符串。",
+ noteFieldMappingsQaGroupDerivationFirstAnswerField: "QA Group 字段映射中的 derivation.firstAnswerField 必须是字符串。",
},
noteFieldMapping: {
noteTypeNotSelected: {
@@ -438,10 +447,13 @@ export const zh = {
semanticQa: "语义 QA 笔记类型 \"{{modelName}}\" 的标题字段和正文字段必须不同。",
},
stale: "笔记类型 \"{{modelName}}\" 的已保存字段映射已过期,因为这些字段在 Anki 中已不存在:{{fields}}。请重新从 Anki 读取字段。",
- clozeIncompatible: "填空题笔记类型 \"{{modelName}}\" 在 Anki 中不是填空题兼容模型。",
qaGroupMissingTitle: "问答题(多级列表)笔记模板 \"{{modelName}}\" 的已保存字段映射不完整,缺少标题字段。请在设置页重新保存字段映射。",
qaGroupNoCompleteSlots: "问答题(多级列表)笔记模板 \"{{modelName}}\" 的已保存字段映射不完整,至少需要一组问题/答案字段。请在设置页重新保存字段映射。",
qaGroupNonContinuousSlots: "问答题(多级列表)笔记模板 \"{{modelName}}\" 的第一组问题/答案字段不是从第 {{firstIndex}} 组开始连续编号。请从第 1 组开始。",
+ qaGroupSelectedFieldMissing: "问答题(多级列表)笔记模板 \"{{modelName}}\" 中已选择字段 \"{{field}}\",但它当前不在 Anki 字段列表里。请重新读取字段后再保存。",
+ qaGroupDuplicateFields: "问答题(多级列表)笔记模板 \"{{modelName}}\" 的题目字段、问题字段和答案字段不能重复。",
+ qaGroupFirstPairNumberMismatch: "问答题(多级列表)笔记模板 \"{{modelName}}\" 的首个问题字段 \"{{questionField}}\" 与首个答案字段 \"{{answerField}}\" 编号不一致。请选择同一组字段。",
+ qaGroupFirstPairMustStartAtOne: "问答题(多级列表)笔记模板 \"{{modelName}}\" 的首组字段必须从第 1 组开始,当前是第 {{firstIndex}} 组。",
qaGroupWarningsUnaccepted: "问答题(多级列表)笔记模板 \"{{modelName}}\" 仍有未确认的字段警告。请先在设置页确认后再同步。",
qaGroupSlotCapacityExceeded: "问答题(多级列表)笔记模板 \"{{modelName}}\" 的已保存字段映射只包含 {{capacity}} 组问题/答案字段,但当前块有 {{itemCount}} 项:{{filePath}}:{{blockStartLine}}。",
},
diff --git a/src/presentation/settings/PluginSettingTab.test.ts b/src/presentation/settings/PluginSettingTab.test.ts
index e5978d8..df7d370 100644
--- a/src/presentation/settings/PluginSettingTab.test.ts
+++ b/src/presentation/settings/PluginSettingTab.test.ts
@@ -430,11 +430,10 @@ class FakePlugin {
}, {});
}
- async getNoteModelDetails(modelName: string): Promise<{ fieldNames: string[]; isCloze: boolean }> {
+ async getNoteModelDetails(modelName: string): Promise<{ fieldNames: string[] }> {
this.getNoteModelDetailsCalls += 1;
return {
fieldNames: this.getFieldNamesForModel(modelName),
- isCloze: modelName.includes("Cloze"),
};
}
@@ -840,11 +839,18 @@ describe("PluginSettingTab", () => {
const qaGroupTitleField = queryByDataset(tab.containerEl, "qaGroupTitleField", "qa-group");
expect(qaGroupTitleField.value).toBe("题目");
+ expect(queryByDataset(tab.containerEl, "qaGroupFirstQuestionField", "qa-group").value).toBe("问题01");
+ expect(queryByDataset(tab.containerEl, "qaGroupFirstAnswerField", "qa-group").value).toBe("答案01");
qaGroupTitleField.value = "标题";
await qaGroupTitleField.trigger("change");
- expect(queryByDataset(tab.containerEl, "qaGroupSlotSummary", "qa-group").textContent).toBe("已识别 2 组");
+ expect(queryByDataset(tab.containerEl, "qaGroupSlotSummary", "qa-group").textContent).toBe("已配置2组");
expect(plugin.settings.noteFieldMappings[createNoteFieldMappingKey("qa-group", plugin.settings.cardTypeConfigs["qa-group"].noteType)]).toEqual(expect.objectContaining({
titleField: "标题",
+ derivation: {
+ mode: "first-pair",
+ firstQuestionField: "问题01",
+ firstAnswerField: "答案01",
+ },
slots: [
{ index: 1, questionField: "问题01", answerField: "答案01" },
{ index: 2, questionField: "问题02", answerField: "答案02" },
@@ -1263,9 +1269,8 @@ describe("PluginSettingTab", () => {
expect(plugin.getNoteModelDetailsCalls).toBe(0);
});
- it("shows qa-group warnings, saves acceptance, and resets acceptance after field refresh changes warnings", async () => {
+ it("extends saved qa-group slots after field refresh only when derivation metadata exists", async () => {
const plugin = new FakePlugin();
- plugin.fieldNamesByModelName[QA_GROUP_USER_MODEL_NAME] = ["题目", "问题01", "答案01", "问题02"];
plugin.settings = normalizePluginSettings({
...plugin.settings,
ankiNoteTypeCache: [QA_GROUP_USER_MODEL_NAME],
@@ -1282,31 +1287,51 @@ describe("PluginSettingTab", () => {
noteType: QA_GROUP_USER_MODEL_NAME,
},
},
+ noteFieldMappings: {
+ ...plugin.settings.noteFieldMappings,
+ [createNoteFieldMappingKey("qa-group", QA_GROUP_USER_MODEL_NAME)]: {
+ cardType: "qa-group",
+ modelName: QA_GROUP_USER_MODEL_NAME,
+ loadedFieldNames: ["题目", "问题01", "答案01", "问题02"],
+ titleField: "题目",
+ derivation: {
+ mode: "first-pair",
+ firstQuestionField: "问题01",
+ firstAnswerField: "答案01",
+ },
+ slots: [
+ { index: 1, questionField: "问题01", answerField: "答案01" },
+ ],
+ warnings: [],
+ loadedAt: 1,
+ },
+ },
});
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
tab.display();
await expandCard(tab, "card-types");
- expect(queryByDataset(tab.containerEl, "qaGroupWarning", "qa-group").textContent).toContain("第 02 组缺少答案字段");
- const acceptWarning = queryByDataset(tab.containerEl, "qaGroupWarningAccept", "qa-group");
- acceptWarning.checked = true;
- await acceptWarning.trigger("change");
-
const mappingKey = createNoteFieldMappingKey("qa-group", QA_GROUP_USER_MODEL_NAME);
- expect(plugin.settings.noteFieldMappings[mappingKey]).toEqual(expect.objectContaining({
- acceptedWarnings: expect.any(Array),
- }));
+ expect(queryByDataset(tab.containerEl, "qaGroupSlotSummary", "qa-group").textContent).toBe("已配置1组");
- plugin.fieldNamesByModelName[QA_GROUP_USER_MODEL_NAME] = ["题目", "问题01", "答案01", "问题02", "答案02", "问题03"];
+ plugin.fieldNamesByModelName[QA_GROUP_USER_MODEL_NAME] = ["题目", "问题01", "答案01", "问题02", "答案02", "问题03", "答案03"];
await queryByDataset(tab.containerEl, "cardTypesRefresh", "true").trigger("click");
await flushPromises();
expect(plugin.settings.noteFieldMappings[mappingKey]).toEqual(expect.objectContaining({
- warnings: expect.any(Array),
- acceptedWarnings: expect.any(Array),
+ derivation: {
+ mode: "first-pair",
+ firstQuestionField: "问题01",
+ firstAnswerField: "答案01",
+ },
+ slots: [
+ { index: 1, questionField: "问题01", answerField: "答案01" },
+ { index: 2, questionField: "问题02", answerField: "答案02" },
+ { index: 3, questionField: "问题03", answerField: "答案03" },
+ ],
}));
- expect(queryByDataset(tab.containerEl, "qaGroupWarning", "qa-group").textContent).toContain("第 02 组缺少答案字段");
+ expect(queryByDataset(tab.containerEl, "qaGroupSlotSummary", "qa-group").textContent).toBe("已配置3组");
});
it("keeps the saved qa-group title field and slots when reading Anki fields again", async () => {
@@ -1359,7 +1384,9 @@ describe("PluginSettingTab", () => {
loadedFieldNames: ["题目", "问题01", "答案01", "Titre", "QPerso", "APerso"],
}));
expect(queryByDataset(tab.containerEl, "qaGroupTitleField", "qa-group").value).toBe("Titre");
- expect(queryByDataset(tab.containerEl, "qaGroupSlotSummary", "qa-group").textContent).toBe("已识别 1 组");
+ expect(queryByDataset(tab.containerEl, "qaGroupFirstQuestionField", "qa-group").value).toBe("QPerso");
+ expect(queryByDataset(tab.containerEl, "qaGroupFirstAnswerField", "qa-group").value).toBe("APerso");
+ expect(queryByDataset(tab.containerEl, "qaGroupSlotSummary", "qa-group").textContent).toBe("已配置1组");
});
it("renders the scope card with renamed copy and aligned folder tree rows", async () => {
diff --git a/src/presentation/settings/PluginSettingTab.ts b/src/presentation/settings/PluginSettingTab.ts
index 0d43562..2a74df4 100644
--- a/src/presentation/settings/PluginSettingTab.ts
+++ b/src/presentation/settings/PluginSettingTab.ts
@@ -433,6 +433,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
private renderQaGroupFieldControls(containerEl: HTMLElement, configId: Extract