UNPKG

@autoanki/plugin-source-yaml

Version:

Extract Anki cards from Markdown fiels

155 lines (140 loc) 4.31 kB
import yaml from 'yaml'; import { z } from 'zod'; import type { AutoankiPlugin, SourcePlugin, ParsedNote, SourcePluginParsingOutput, AutoankiPluginApi, } from '@autoanki/core'; interface Metadata { /** * The index of the current note in the deserialized YAML array. * Will be 0 if the original YAML input was a single item and not an array. */ index: number; } const yamlAnkiNoteSchema = z .object({ id: z.string().optional(), deleted: z.boolean().optional(), deck: z.string().optional(), note_type: z.string(), fields: z.record(z.string().min(1), z.string()), tags: z.string().array().optional(), }) .strict(); type YamlAnkiNote = z.infer<typeof yamlAnkiNoteSchema>; function yamlAnkiNoteToParsedNote(yamlNote: YamlAnkiNote): ParsedNote { return { tags: yamlNote.tags ?? [], id: yamlNote.id, fields: yamlNote.fields, deckName: yamlNote.deck, modelName: yamlNote.note_type, deleted: yamlNote.deleted, }; } function parsedNoteToYamlAnkiNote(parsedNote: ParsedNote): YamlAnkiNote { return { id: parsedNote.id, fields: parsedNote.fields, note_type: parsedNote.modelName, deck: parsedNote.deckName, tags: parsedNote.tags, deleted: parsedNote.deleted, }; } export class YamlSourcePlugin implements SourcePlugin { static pluginName = '@autoanki/plugin-source-yaml'; constructor(private coreApi: AutoankiPluginApi) {} private yamlParseCache: Record< string, { /** * Whether the YAML input was an array */ originallyArray: boolean; parsed: YamlAnkiNote[]; } > = {}; async writeBackToInput( inputKey: string, originalInputContent: ArrayBufferLike, notes: SourcePluginParsingOutput[] ): Promise<ArrayBufferLike> { const cache = this.yamlParseCache[inputKey]; for (const note of notes) { const metadata: Metadata = note.metadata! as Metadata; cache.parsed[metadata.index] = parsedNoteToYamlAnkiNote(note.note); } return new TextEncoder().encode( yaml.stringify(cache.originallyArray ? cache.parsed : cache.parsed[0], { /* * Disable folded block string (which I don't like. I use literal block * string). * TODO: implement YAML stringfication with minimal changes. * Using something like: * * * https://github.com/eemeli/yaml/issues/308 * * https://github.com/eemeli/yaml/pull/309 * * https://azimi.me/2015/10/16/yawn-yaml.html * * https://github.com/yarnpkg/berry/issues/1463 * * https://github.com/paul-soporan/enhanced-yaml */ lineWidth: 0, }) ); } async parseFromInput( inputKey: string, inputContent: ArrayBufferLike ): Promise<SourcePluginParsingOutput[]> { const enc = new TextDecoder('utf8'); const input = enc.decode(inputContent); let parsedYaml; try { parsedYaml = yaml.parse(input); } catch (error) { this.coreApi.logger.warn(`Unable to parse YAML in ${inputKey}. ${error}`); return []; } const isParsedYamlArray = Array.isArray(parsedYaml); if (!isParsedYamlArray) { parsedYaml = [parsedYaml]; } this.yamlParseCache[inputKey] = { parsed: [], originallyArray: isParsedYamlArray, }; const currentInputCache = this.yamlParseCache[inputKey]; const outputs: SourcePluginParsingOutput[] = []; for (const [i, yamlItem] of (parsedYaml as unknown[]).entries()) { if (yamlItem === null) { /* * Some inputs may generated a parsed item that is `null`. * E.g. a file that contains only YAML comments. * Filter them out. */ continue; } const parseResult = yamlAnkiNoteSchema.safeParse(yamlItem); if (parseResult.success) { currentInputCache.parsed.push(parseResult.data); const note: ParsedNote = yamlAnkiNoteToParsedNote(parseResult.data); outputs.push({ note, metadata: { index: i } as Metadata, }); } else { this.coreApi.logger.warn( `Item ${i} in "${inputKey}" is invalid: ${parseResult.error}` ); } } return outputs; } } export default { source: YamlSourcePlugin, } as AutoankiPlugin;