Add implementation for add, update and delete on Anki

This commit is contained in:
reuseman 2020-12-04 23:34:45 +01:00
parent ccd700833b
commit 9a0d3590d0
14 changed files with 740 additions and 97 deletions

16
.gitignore vendored
View File

@ -8,4 +8,18 @@ package-lock.json
# build # build
main.js main.js
*.js.map *.js.map
# scripts
move.sh
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/

View File

@ -1,45 +1,21 @@
## Obsidian Sample Plugin # Cards
This is a sample plugin for Obsidian (https://obsidian.md). An Anki plugin for Obsidian.
This project uses Typescript to provide type checking and documentation. ## How to install
The repo depends on the latest plugin API (obsidian.d.ts) in Typescript Definition format, which contains TSDoc comments describing what it does.
**Note:** The Obsidian API is still in early alpha and is subject to change at any time! ## How to start
This sample plugin demonstrates some of the basic functionality the plugin API can do. ## Anki-connect configuration
- Changes the default font color to red using `styles.css`.
- Adds a ribbon icon, which shows a Notice when clicked.
- Adds a command "Open Sample Modal" which opens a Modal.
- Adds a plugin setting tab to the settings page.
- Registers a global click event and output 'click' to the console.
- Registers a global interval which logs 'setInterval' to the console.
{
### Releasing new releases "apiKey": null,
"apiLogPath": null,
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release. "webBindAddress": "127.0.0.1",
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible. "webBindPort": 8765,
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases "webCorsOrigin": "http://localhost",
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. "webCorsOriginList": [
- Publish the release. "http://localhost",
"app://obsidian.md"
### Adding your plugin to the community plugin list ]
}
- Publish an initial version.
- Make sure you have a `README.md` file in the root of your repo.
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
### How to use
- Clone this repo.
- `npm i` or `yarn` to install dependencies
- `npm run dev` to start compilation in watch mode.
### Manually installing the plugin
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
### API Documentation
See https://github.com/obsidianmd/obsidian-api

106
main.ts
View File

@ -1,8 +1,36 @@
import { App, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; import { App, Modal, Notice, Plugin, PluginSettingTab, Setting, CachedMetadata, MetadataCache, parseFrontMatterTags, parseFrontMatterStringArray, SettingTab, parseFrontMatterEntry, TFile } from 'obsidian';
import { Flashcard } from "src/entities/flashcard"
import { Parser } from 'src/parser';
import { Settings } from 'settings';
import { SettingsTab } from 'src/gui/settings-tab';
import { Anki } from 'src/anki';
import { CardsService } from 'src/cards-service';
export default class ObsidianFlashcard extends Plugin {
private settings: Settings
private cardsService: CardsService
// EXTRA
// this gives you back the app:// of a resource
// this.app.vault.adapter.getResourcePath("name")
// IMAGES inside the file
// let temp = this.app.metadataCache.getFileCache(this.app.workspace.getActiveFile()).embeds[2].link
//
// this.app.vault.getAbstractFileByPath("resources/"+temp)
// Path
// this.app.vault.adapter.getBasePath()
// this.app.vault.adapter.getFullPath(attachmentFolder + attachment)
// this.app.vault.config.attachmentFolderPath in my case that ś reso
export default class MyPlugin extends Plugin {
onload() { onload() {
console.log('loading plugin'); // TODO test when file did not insert flashcards, but one of them is in Anki already
console.log('loading flashcard-plugin');
this.settings = new Settings()
this.cardsService = new CardsService(this.app, this.settings)
this.addRibbonIcon('dice', 'Sample Plugin', () => { this.addRibbonIcon('dice', 'Sample Plugin', () => {
new Notice('This is a notice!'); new Notice('This is a notice!');
@ -11,16 +39,17 @@ export default class MyPlugin extends Plugin {
this.addStatusBarItem().setText('Status Bar Text'); this.addStatusBarItem().setText('Status Bar Text');
this.addCommand({ this.addCommand({
id: 'open-sample-modal', id: 'generate-flashcard-this-file',
name: 'Open Sample Modal', name: 'Generate for this file',
// callback: () => {
// console.log('Simple Callback');
// },
checkCallback: (checking: boolean) => { checkCallback: (checking: boolean) => {
let leaf = this.app.workspace.activeLeaf; let activeFile = this.app.workspace.getActiveFile()
if (leaf) { if (activeFile) {
if (!checking) { if (!checking) {
new SampleModal(this.app).open(); this.cardsService.execute(activeFile).then(res => {
new Notice(res.join(" "))
}).catch(err => {
Error(err)
})
} }
return true; return true;
} }
@ -28,56 +57,51 @@ export default class MyPlugin extends Plugin {
} }
}); });
this.addSettingTab(new SampleSettingTab(this.app, this)); this.addCommand({
id: "anki-test", name: "Anki", callback: () => {
let anki: Anki = new Anki()
anki.getDeck().then(res => {
console.log(res)
})
}
})
this.addSettingTab(new SettingsTab(this.app, this));
this.registerEvent(this.app.on('codemirror', (cm: CodeMirror.Editor) => { this.registerEvent(this.app.on('codemirror', (cm: CodeMirror.Editor) => {
console.log('codemirror', cm); //console.log('codemirror', cm);
})); }));
this.registerDomEvent(document, 'click', (evt: MouseEvent) => { this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt); //console.log('click', evt);
}); });
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000)); this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
} }
onunload() { async onunload() {
console.log('unloading plugin'); console.log('Unloading flashcard-obsidian and saving data.');
await this.saveData(this.settings);
} }
} }
class SampleModal extends Modal { class SampleModal extends Modal {
constructor(app: App) { flashcards: Flashcard[];
constructor(app: App, flashcards: Flashcard[]) {
super(app); super(app);
this.flashcards = flashcards;
} }
onOpen() { onOpen() {
let {contentEl} = this; let { titleEl } = this;
contentEl.setText('Woah!'); let { contentEl } = this;
titleEl.setText('Generated flashcards!');
contentEl.setText(this.flashcards.join("\n\n"));
} }
onClose() { onClose() {
let {contentEl} = this; let { contentEl } = this;
contentEl.empty(); contentEl.empty();
} }
} }
class SampleSettingTab extends PluginSettingTab {
display(): void {
let {containerEl} = this;
containerEl.empty();
containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'});
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text.setPlaceholder('Enter your secret')
.setValue('')
.onChange((value) => {
console.log('Secret: ' + value);
}));
}
}

View File

@ -1,10 +1,10 @@
{ {
"id": "obsidian-sample-plugin", "id": "flashcards-obsidian",
"name": "Sample Plugin", "name": "Flashcards",
"version": "1.0.1", "version": "0.1.0",
"minAppVersion": "0.9.12", "minAppVersion": "0.9.17",
"description": "This is a sample plugin for Obsidian. This plugin demonstrates some of the capabilities of the Obsidian API.", "description": "An Anki integration.",
"author": "Obsidian", "author": "reuseman",
"authorUrl": "https://obsidian.md/about", "authorUrl": "https://github.com/reuseman",
"isDesktopOnly": false "isDesktopOnly": true
} }

View File

@ -17,7 +17,12 @@
"@types/node": "^14.14.2", "@types/node": "^14.14.2",
"obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master", "obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master",
"rollup": "^2.32.1", "rollup": "^2.32.1",
"ts-node": "^9.0.0",
"tslib": "^2.0.3", "tslib": "^2.0.3",
"typescript": "^4.0.3" "typescript": "^4.0.3"
},
"dependencies": {
"@types/showdown": "^1.9.3",
"showdown": "^1.9.1"
} }
} }

9
settings.ts Normal file
View File

@ -0,0 +1,9 @@
export class Settings {
contextAwareMode: boolean = true
contextSeparator: string = ' > '
defaultDeck: string = "Default"
ankiProfile: string = "?"
ankiPath: string = "?" // TODO can i boot automagically anki?
}

89
src/anki.ts Normal file
View File

@ -0,0 +1,89 @@
import { Card } from 'src/entities/card';
export class Anki {
public async addCards(cards: Card[]): Promise<number[]> {
let notes: any = []
cards.forEach(card => notes.push(card.getCard(false)))
return this.invoke("addNotes", 6, {
"notes": notes
})
}
/**
* Given the new cards with an optional deck name, it updates all the cards on Anki.
*
* Be aware of https://github.com/FooSoft/anki-connect/issues/82. If the Browse pane is opened on Anki,
* the update does not change all the cards.
* @param cards the new cards.
* @param deckName the new deck name.
*/
public async updateCards(cards: Card[]): Promise<any> {
let updateActions: any[] = []
// TODO add possibility to edit even tags
// Unfortunately https://github.com/FooSoft/anki-connect/issues/183
// This means that the delta from the current tags on Anki and the generated one should be added/removed
for (let card of cards) {
updateActions.push({
"action": "updateNoteFields",
"params": {
"note": card.getCard(true)
}
})
}
return this.invoke("multi", 6, { "actions": updateActions })
}
public async getCards(ids: number[]) {
return await this.invoke("notesInfo", 6, { "notes": ids })
}
public async deleteCards(ids: number[]) {
return this.invoke("deleteNotes", 6, { "notes": ids })
}
public async getDeck() {
await this.invoke('createDeck', 6, { deck: 'test1' });
const result = await this.invoke('deckNames', 6);
console.log(`got list of decks: ${result}`);
return result
}
public async ping(): Promise<boolean> {
return await this.invoke('version', 6) === 6
}
private invoke(action: string, version: number, params = {}): any {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener('error', () => reject('failed to issue request'));
xhr.addEventListener('load', () => {
try {
const response = JSON.parse(xhr.responseText);
if (Object.getOwnPropertyNames(response).length != 2) {
throw 'response has an unexpected number of fields';
}
if (!response.hasOwnProperty('error')) {
throw 'response is missing required error field';
}
if (!response.hasOwnProperty('result')) {
throw 'response is missing required result field';
}
if (response.error) {
throw response.error;
}
resolve(response.result);
} catch (e) {
reject(e);
}
});
xhr.open('POST', 'http://127.0.0.1:8765');
xhr.send(JSON.stringify({ action, version, params }));
});
}
}

240
src/cards-service.ts Normal file
View File

@ -0,0 +1,240 @@
import { Anki } from 'src/anki'
import { App, FrontMatterCache, Notice, parseFrontMatterEntry, parseFrontMatterTags, TFile } from 'obsidian'
import { Parser } from 'src/parser'
import { Settings } from 'settings'
import { Card } from 'src/entities/card'
import { Flashcard } from 'src/entities/flashcard'
export class CardsService {
// TODO right now you do not check for cards that when inserted/updated gives back null as ID
// TODO check the deletion for the reversed notes that have 2 cards bind
private app: App
private parser: Parser
private anki: Anki
private settings: Settings
private updateFile: boolean
private totalOffset: number
private file: string
private notifications: string[]
constructor(app: App, settings: Settings) {
this.app = app
this.anki = new Anki()
this.parser = new Parser(settings)
this.settings = settings
}
public async execute(activeFile: TFile): Promise<string[]> {
// TODO add note-type to Anki
try {
await this.anki.ping()
} catch (err) {
console.error(err)
return ["Error: Anki must be open with AnkiConnect installed."]
}
// Init for the execute phase
this.updateFile = false
this.totalOffset = 0
this.notifications = []
let fileCachedMetadata = this.app.metadataCache.getFileCache(activeFile)
let globalTags: string[] = undefined
// Parse frontmatter
let frontmatter = fileCachedMetadata.frontmatter
let deckName = this.settings.defaultDeck
if (frontmatter) {
deckName = parseFrontMatterEntry(frontmatter, "cards-deck")
globalTags = parseFrontMatterTags(frontmatter).map(tag => tag.substr(1))
}
try {
this.file = await this.app.vault.read(activeFile)
// TODO with empty check that does not call ankiCards line
let ankiBlocks = this.parser.getAnkiIDsBlocks(this.file)
let ankiCards = ankiBlocks ? await this.anki.getCards(this.getAnkiIDs(ankiBlocks)) : undefined
let cards: Flashcard[] = this.parser.generateFlashcards(this.file, globalTags, deckName)
let [cardsToCreate, cardsToUpdate] = this.filterByUpdate(ankiCards, cards)
let cardsToDelete: number[] = this.parser.getCardsToDelete(this.file)
this.printCards(cardsToCreate, cardsToUpdate, cardsToDelete) // TODO delete
await this.deleteCardsOnAnki(cardsToDelete, ankiBlocks)
await this.updateCardsOnAnki(cardsToUpdate)
await this.insertCardsOnAnki(cardsToCreate, frontmatter, deckName)
// Update file
if (this.updateFile) {
try {
this.app.vault.modify(activeFile, this.file)
} catch (err) {
Error("Could not update the file.")
return ["Error: Could not update the file."]
}
}
if (!this.notifications.length) {
this.notifications.push("Nothing to do. Everything is up to date")
}
return this.notifications
} catch (err) {
console.error(err)
}
}
private async insertCardsOnAnki(cardsToCreate: Card[], frontmatter: FrontMatterCache, deckName: string): Promise<number> {
if (cardsToCreate.length) {
let insertedCards = 0
// TODO before adding create deck if not exists
// TODO check if cardsToCreate is not empty?
try {
let ids = await this.anki.addCards(cardsToCreate)
// Add IDs from response to Flashcard[]
ids.map((id: number, index: number) => {
cardsToCreate[index].id = id
})
cardsToCreate.forEach(card => {
if (card.id === null) {
new Notice(`Error, could not add: '${card.initialContent}'`)
} else {
insertedCards++
}
});
this.updateFrontmatter(frontmatter, deckName)
this.writeAnkiBlocks(cardsToCreate)
this.notifications.push(`Inserted successfully ${insertedCards}/${cardsToCreate.length} cards.`)
return insertedCards
} catch (err) {
console.error(err)
Error("Error: Could not write cards on Anki")
}
}
}
private updateFrontmatter(frontmatter: FrontMatterCache, deckName: string) {
// TODO evaluate https://regex101.com/r/bJySNf/1
let newFrontmatter: string = ""
let cardsDeckLine: string = `cards-deck: Default\n`
if (frontmatter) {
let oldFrontmatter: string = this.file.substring(frontmatter.position.start.offset, frontmatter.position.end.offset)
if (!deckName) {
newFrontmatter = oldFrontmatter.substring(0, oldFrontmatter.length - 3) + cardsDeckLine + "---"
this.totalOffset += cardsDeckLine.length
this.file = newFrontmatter + this.file.substring(frontmatter.position.end.offset, this.file.length + 1)
}
} else {
newFrontmatter = `---\n${cardsDeckLine}---\n`
this.totalOffset += newFrontmatter.length
this.file = newFrontmatter + this.file
}
}
private writeAnkiBlocks(cardsToCreate: Card[]) {
for (let card of cardsToCreate) {
// Card.id cannot be null, because if written already previously it has an ID,
// if it has been inserted it has an ID too
if (card.id !== null && !card.inserted) {
let id = "^" + card.id.toString() + "\n"
card.endOffset += this.totalOffset
let offset = card.endOffset
this.updateFile = true
this.file = this.file.substring(0, offset) + id + this.file.substring(offset, this.file.length + 1)
this.totalOffset += id.length
}
}
}
public printCards(flashcardsToCreate: Card[], flashcardsToUpdate: Card[], flashcardsToDelete: number[]) {
console.info("Cards to create")
console.info(flashcardsToCreate)
console.info("Cards to update")
console.info(flashcardsToUpdate)
console.info("IDs of cards to delete")
console.info(flashcardsToDelete)
}
private async updateCardsOnAnki(cards: Card[]): Promise<number> {
if (cards.length) {
this.anki.updateCards(cards).then(res => {
this.notifications.push(`Updated successfully ${cards.length}/${cards.length} cards.`)
}).catch(err => {
console.error(err)
Error("Error: Could not update cards on Anki")
})
return cards.length
}
}
public async deleteCardsOnAnki(cards: number[], ankiBlocks: RegExpMatchArray[]): Promise<number> {
if (cards.length) {
let deletedCards = 0
for (const block of ankiBlocks) {
let id = Number(block[1])
// Deletion of cards that need to be deleted (i.e. blocks ID that don't have content)
if (cards.includes(id)) {
try {
this.anki.deleteCards(cards)
deletedCards++
this.updateFile = true
this.file = this.file.substring(0, block["index"]) + this.file.substring(block["index"] + block[0].length, this.file.length)
this.totalOffset -= block[0].length
this.notifications.push(`Deleted successfully ${deletedCards}/${cards.length} cards.`)
} catch (err) {
console.error(err)
Error("Error, could not delete the card from Anki")
}
}
}
return deletedCards
}
}
private getAnkiIDs(blocks: RegExpMatchArray[]): number[] {
let IDs: number[] = []
for (let b of blocks) {
IDs.push(Number(b[1]))
}
return IDs
}
public filterByUpdate(ankiCards: any, generatedCards: Card[]) {
let cardsToCreate: Card[] = []
let cardsToUpdate: Card[] = []
if (ankiCards) {
for (let flashcard of generatedCards) {
// Inserted means that anki blocks are available, that means that the card should
// (the user can always delete it) be in Anki
let ankiCard = undefined
if (flashcard.inserted) {
ankiCard = ankiCards.filter((card: any) => Number(card.noteId) === flashcard.id)[0]
if (!flashcard.match(ankiCard)) {
cardsToUpdate.push(flashcard)
}
} else {
cardsToCreate.push(flashcard)
}
}
} else {
console.log("No cards in Anki, I am going to create all of them")
cardsToCreate = [...generatedCards]
}
return [cardsToCreate, cardsToUpdate]
}
}

38
src/entities/card.ts Normal file
View File

@ -0,0 +1,38 @@
export abstract class Card {
id: number
deckName: string
initialContent: string
fields: Record<string, string>
reversed: boolean
endOffset: number
tags: string[]
inserted: boolean
// TODO set "obsidian as optional in the settings", this means that the tag should be outside
constructor(id: number, deckName: string, initialContent: string, fields: Record<string, string>, reversed: boolean, endOffset: number, tags: string[], inserted: boolean) {
this.id = id
this.deckName = deckName
this.initialContent = initialContent
this.fields = fields
this.reversed = reversed
this.endOffset = endOffset
this.tags = tags
this.tags.unshift("obsidian")
this.inserted = inserted
}
abstract toString(): string
abstract getCard(update: boolean): object
match(card: any): boolean {
let fields = Object.entries(card.fields)
for (let field of fields) {
let fieldName = field[0]
if (field[1].value !== this.fields[fieldName]) {
return false
}
}
return true
}
}

31
src/entities/flashcard.ts Normal file
View File

@ -0,0 +1,31 @@
import { Card } from "src/entities/card";
export class Flashcard extends Card {
constructor(id: number = -1, deckName: string, initialContent: string, fields: Record<string, string>, reversed: boolean, endOffset: number, tags: string[] = [], inserted: boolean = false) {
super(id, deckName, initialContent, fields, reversed, endOffset, tags, inserted)
}
public getCard(update: boolean = false): object {
let modelName = this.reversed ? "Obsidian-basic-reversed" : "Obsidian-basic"
let card: any = {
"deckName": this.deckName,
"modelName": modelName,
"fields": {
"Front": this.fields["Front"],
"Back": this.fields["Back"]
},
"tags": this.tags,
}
if (update) {
card["id"] = this.id
}
return card
}
public toString = (): string => {
return `Q: ${this.fields[0]}\nA: ${this.fields[1]}`
}
}

24
src/gui/settings-tab.ts Normal file
View File

@ -0,0 +1,24 @@
import { PluginSettingTab, Setting } from "obsidian"
export class SettingsTab extends PluginSettingTab {
display(): void {
let { containerEl } = this
const plugin = (this as any).plugin
containerEl.empty()
containerEl.createEl("h1", { text: "Flashcards" })
containerEl.createEl("h2", { text: "General Settings" })
new Setting(containerEl)
.setName("Context-aware mode")
.setDesc("Add the ancestor headings to the question of the flashcard.")
.addToggle((toggle) =>
toggle
.setValue(plugin.settings.contextAwareMode)
.onChange((value) => {
plugin.settings.contextAwareMode = value
plugin.saveData(plugin.settings)
})
)
}
}

162
src/parser.ts Normal file
View File

@ -0,0 +1,162 @@
import { Flashcard } from './entities/flashcard';
import { Spaced } from "./entities/spaced"
import { Cloze } from './entities/cloze';
import { Settings } from 'settings';
import * as showdown from 'showdown';
export class Parser {
private settings: Settings
constructor(settings: Settings) {
this.settings = settings
}
/**
* Gives back the ancestor headings of a line.
* @param headings The list of all the headings available in a file.
* @param line The line whose ancestors need to be calculated.
* @param headingLevel The level of the first ancestor heading, i.e. the number of #.
*/
private getContext(headings: any, index: number, headingLevel: number): string[] {
let context: string[] = []
let currentIndex: number = index
let goalLevel: number = 6
let i = headings.length - 1
// Get the level of the first heading upon the line
if (headingLevel !== -1) {
// This is the case of a #flashcard in a heading
goalLevel = headingLevel - 1
} else {
// Find first heading and its level
// This is the case of a #flashcard in a paragraph
for (i; i >= 0; i--) {
if (headings[i].index < currentIndex) {
currentIndex = headings[i].index
goalLevel = headings[i][1].length - 1
context.unshift(headings[i][2].trim())
break
}
}
}
// Search for the other headings
for (i; i >= 0; i--) {
let currentLevel = headings[i][1].length
if (currentLevel == goalLevel && headings[i].index < currentIndex) {
currentIndex = headings[i].index
goalLevel = currentLevel - 1
context.unshift(headings[i][2].trim())
}
}
return context
}
public getCardsToDelete(file: string): number[] {
// Find block IDs with no content above it
let regex: RegExp = /^\s*(?:\n)(?:\^(\d{13}))(?:\n\s*?)?/gm
return [...file.matchAll(regex)].map((match) => { return Number(match[1]) })
}
public generateFlashcards(file: string, globalTags: string[] = [], deckName: string): Flashcard[] {
let htmlConverter = new showdown.Converter()
htmlConverter.setOption("simplifiedAutoLink", true)
htmlConverter.setOption("tables", true)
htmlConverter.setOption("tasks", true)
let contextAware = this.settings.contextAwareMode
let flashcards: Flashcard[] = []
let regex: RegExp = /( {0,3}[#]*)((?:[^\n]\n?)+?)(#flashcard(?:-reverse)?)((?: *#\w+)*) *?\n+((?:[^\n]\n?)*?(?=\^\d{13}|$))(?:\^(\d{13}))?/gim
let matches = [...file.matchAll(regex)]
let headings: any = []
if (contextAware) {
// https://regex101.com/r/agSp9X/4
headings = [...file.matchAll(/^ {0,3}(#{1,6}) +([^\n]+?) ?((?: *#\S+)*) *$/gim)]
}
for (let match of matches) {
let reversed: boolean = match[3].trim().toLowerCase() === "#flashcard-reverse"
let headingLevel = match[1].trim().length !== 0 ? match[1].length : -1
// Match.index - 1 because otherwise in the context there will be even match[1], i.e. the question
let context = contextAware ? this.getContext(headings, match.index - 1, headingLevel) : ""
// todo image
// todo code
let originalQuestion = match[2].trim()
let question = contextAware ? [...context, match[2].trim()].join(`${this.settings.contextSeparator}`) : match[2].trim()
question = this.mathToAnki(htmlConverter.makeHtml(question))
let answer = this.mathToAnki(htmlConverter.makeHtml(match[5].trim()))
let endingLine = match.index + match[0].length
let tags: string[] = this.parseTags(match[4], globalTags)
let id: number = match[6] ? Number(match[6]) : -1
let inserted: boolean = match[6] ? true : false
let fields = { "Front": question, "Back": answer }
let flashcard = new Flashcard(id, deckName, originalQuestion, fields, reversed, endingLine, tags, inserted)
flashcards.push(flashcard)
}
return flashcards
}
private generateSpacedCards(file: string): Spaced[] {
let spaced: Spaced[] = []
let regex: RegExp = new RegExp("[# ]*((?:[^\n]\n?)+) *(#spaced) ?", "gim")
let matches = file.matchAll(regex)
for (let match of matches) {
let spacedCard = new Spaced(match[1].trim(), match.index + match.length)
spaced.push(spacedCard)
}
return spaced
}
private generateClozeCards(): Cloze[] {
let clozeCards: Cloze[]
return clozeCards
}
// TODO maybe move it in anki
private mathToAnki(str: string) {
let mathBlockRegex = /(\$\$)(.*)(\$\$)/gi
str = str.replace(mathBlockRegex, '\\($2\\)')
let mathInlineRegex = /(\$)(.*)(\$)/gi
str = str.replace(mathInlineRegex, '\\($2\\)')
return str
}
private parseTags(str: string, globalTags: string[]): string[] {
let tags: string[] = [...globalTags]
if (str) {
for (let tag of str.split("#")) {
tag = tag.trim()
if (tag) {
tags.push(tag)
}
}
}
return tags
}
public getAnkiIDsBlocks(file: string): RegExpMatchArray[] {
return Array.from(file.matchAll(/\^(\d{13})\s*/gm))
}
private convertImagesToHtml(str: string) {
}
}

View File

@ -1,4 +1,32 @@
/* Sets all the text color to red! */ /* Sets all the text color to red! */
body { body {
color: red; color: green;
}
.tag {
color: var(--text-normal);
background-color: var(--text-accent);
border: none;
font-size: 11px;
padding: 1px 8px;
text-align: center;
text-decoration: none;
margin: 0px 0px;
cursor: pointer;
border-radius: 14px;
display: inline;
vertical-align: middle;
}
.tag:hover {
color: var(--text-normal);
background-color: var(--text-accent-hover);
}
.tag[href="#flashcard"] {
background-color: #821515;
}
.tag[href="#flashcard-reverse"] {
background-color: #821515;
} }

View File

@ -1,4 +1,4 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"inlineSourceMap": true, "inlineSourceMap": true,
@ -9,14 +9,17 @@
"noImplicitAny": true, "noImplicitAny": true,
"moduleResolution": "node", "moduleResolution": "node",
"importHelpers": true, "importHelpers": true,
"downlevelIteration": true,
"lib": [ "lib": [
"dom", "dom",
"es5", "es5",
"scripthost", "scripthost",
"es2015" "es2015",
] "ES2020.String"
],
}, },
"include": [ "include": [
"**/*.ts" "**/*.ts",
] ]
} }