@autoanki/plugin-source-markdown
Version:
Extract Anki cards from Markdown files
122 lines (108 loc) • 3.65 kB
text/typescript
import { unified } from 'unified';
import { Code, Root } from 'mdast';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';
import { visit } from 'unist-util-visit';
import type {
AutoankiPlugin,
AutoankiPluginApi,
SourcePlugin,
SourcePluginParsingOutput,
} from '@autoanki/core';
import yamlPlugin from '@autoanki/plugin-source-yaml';
interface Metadata {
index: number;
yamlMetadata: unknown;
}
const mdProcessor = unified().use(remarkParse);
const mdStringifyProcessor = unified().use(remarkStringify);
const encoder = new TextEncoder();
const decoder = new TextDecoder('utf8');
export class MarkdownSourcePlugin implements SourcePlugin {
static pluginName = '@autoanki/plugin-source-markdown';
private markdownParseCache: Record<
string,
{ ast: Root; yamlBlocks: Code[] }
> = {};
private yamlPlugin;
constructor(api: AutoankiPluginApi) {
this.yamlPlugin = new yamlPlugin.source!(api);
}
async writeBackToInput(
inputKey: string,
originalInputContent: ArrayBufferLike,
notes: SourcePluginParsingOutput[]
): Promise<ArrayBufferLike> {
const thisFileCache = this.markdownParseCache[inputKey];
const notesPerYamlBlock = notes.reduce((group, note) => {
const metadata = note.metadata as Metadata;
(group[metadata.index] = group[metadata.index] || []).push(note);
return group;
}, {} as Record<number, SourcePluginParsingOutput[]>);
const modifiedMarkdownBlocks = await Promise.all(
Object.entries(notesPerYamlBlock).map(
async ([i, notesOfThisYamlBlock]) => {
const ithBlock = Number.parseInt(i, 10);
const newContent = await this.yamlPlugin.writeBackToInput(
`${inputKey}-${i}`,
encoder.encode(thisFileCache.yamlBlocks[ithBlock].value),
notesOfThisYamlBlock.map((note) => {
return {
note: note.note,
metadata: (note.metadata as Metadata).yamlMetadata,
} as SourcePluginParsingOutput;
})
);
return {
ithBlock,
newContent,
};
}
)
);
for (const block of modifiedMarkdownBlocks) {
thisFileCache.yamlBlocks[block.ithBlock].value = decoder.decode(
block.newContent
);
}
return encoder.encode(mdStringifyProcessor.stringify(thisFileCache.ast));
}
async parseFromInput(inputKey: string, inputContent: ArrayBufferLike) {
const enc = new TextDecoder('utf8');
const input = enc.decode(inputContent);
if (!this.markdownParseCache[inputKey]) {
this.markdownParseCache[inputKey] = {
ast: mdProcessor.parse(input),
yamlBlocks: [],
};
visit(this.markdownParseCache[inputKey].ast, 'code', (node, _) => {
if (node.lang === 'autoanki') {
this.markdownParseCache[inputKey].yamlBlocks.push(node);
}
});
const result = await Promise.all(
this.markdownParseCache[inputKey].yamlBlocks.map((yaml, i) => {
return this.yamlPlugin.parseFromInput(
`${inputKey}-${i}`,
encoder.encode(yaml.value)
);
})
);
return result.flatMap((outputs, ithYamlBlock) => {
return outputs.map((output) => {
return {
note: output.note,
metadata: {
index: ithYamlBlock,
yamlMetadata: output.metadata,
} as Metadata,
} as SourcePluginParsingOutput;
});
});
}
return [];
}
}
export default {
source: MarkdownSourcePlugin,
} as AutoankiPlugin;