Add support for Cloze #9

This commit is contained in:
alexcolucci@protonmail.com 2022-02-04 23:24:58 +01:00
parent 2092d4c1ad
commit 2cfdb79fff
6 changed files with 121 additions and 19 deletions

View File

@ -13,8 +13,10 @@ Anki integration for [Obsidian](https://obsidian.md/).
📅 Spaced-only cards with **#card-spaced** or **#card/spaced**
✍️ Inline style with **Question::Answer**
✍️ Inline style reversed with **Question:::Answer**
📃 Cloze with **==Highlight==** or **{Curly brackets}** or **{2:Cloze}**
🧠 **Context-aware** mode
🏷️ Global and local **tags**
🔢 Support for **LaTeX**
🖼️ Support for **images**
🎤 Support for **audios**

View File

@ -1,7 +1,7 @@
{
"id": "flashcards-obsidian",
"name": "Flashcards",
"version": "1.5.6",
"version": "1.6.0",
"minAppVersion": "0.9.17",
"description": "Anki integration",
"author": "Alex Colucci",

View File

@ -28,7 +28,7 @@ export class Clozecard extends Card {
mediaNames,
containsCode
);
this.modelName = `Obsidian-clozed`;
this.modelName = `Obsidian-cloze`;
if (fields["Source"]) {
this.modelName += sourceDeckExtension;
}
@ -69,6 +69,6 @@ export class Clozecard extends Card {
};
public getIdFormat(): string {
return "^" + this.id.toString() + "\n";
return "\n^" + this.id.toString();
}
}

View File

@ -80,13 +80,11 @@ export class Regex {
this.cardsSpacedStyle = new RegExp(str, flags);
// https://regex101.com/r/cgtnLf/1
// str = "(?:.*?(==(.*?)==).*)(?:\n?^(d{13}))?";
// this.clozeHighlight = /((==)(.*?)(==))/gm;
// str = "( {0,3}[#]{0,6})?(?:(?:[\t ]*)(?:\\d.|[-+*]|#{1,6}))?(.*?(==.+?==|{.+?}).*?)((?: *#[\\w-]+)+|$)(?:\n\\^(?:\\d{13}))?"
// this.cardsClozeWholeLine = new RegExp(str, flags);
str = "( {0,3}[#]{0,6})?(?:(?:[\\t ]*)(?:\\d.|[-+*]|#{1,6}))?(.*?(==.+?==|\\{.+?\\}).*?)((?: *#[\\w\\-\\/_]+)+|$)(?:\n\\^(\\d{13}))?"
this.cardsClozeWholeLine = new RegExp(str, flags);
// this.singleClozeCurly = /((?:{)(?:(\d):?)?(.+?)(?:}))/g;
// this.singleClozeHighlight = /((?:==)(.+?)(?:==))/g;
this.singleClozeCurly = /((?:{)(?:(\d):?)?(.+?)(?:}))/g;
this.singleClozeHighlight = /((?:==)(.+?)(?:==))/g;
}
}

View File

@ -233,19 +233,23 @@ export class Anki {
}
const css =
'.card {\r\n font-family: arial;\r\n font-size: 20px;\r\n text-align: center;\r\n color: black;\r\n background-color: white;\r\n}\r\n\r\n.tag::before {\r\n\tcontent: "#";\r\n}\r\n\r\n.tag {\r\n color: white;\r\n background-color: #9F2BFF;\r\n border: none;\r\n font-size: 11px;\r\n font-weight: bold;\r\n padding: 1px 8px;\r\n margin: 0px 3px;\r\n text-align: center;\r\n text-decoration: none;\r\n cursor: pointer;\r\n border-radius: 14px;\r\n display: inline;\r\n vertical-align: middle;\r\n}\r\n';
'.card {\r\n font-family: arial;\r\n font-size: 20px;\r\n text-align: center;\r\n color: black;\r\n background-color: white;\r\n}\r\n\r\n.tag::before {\r\n\tcontent: "#";\r\n}\r\n\r\n.tag {\r\n color: white;\r\n background-color: #9F2BFF;\r\n border: none;\r\n font-size: 11px;\r\n font-weight: bold;\r\n padding: 1px 8px;\r\n margin: 0px 3px;\r\n text-align: center;\r\n text-decoration: none;\r\n cursor: pointer;\r\n border-radius: 14px;\r\n display: inline;\r\n vertical-align: middle;\r\n}\r\n .cloze { font-weight: bold; color: blue;}.nightMode .cloze { color: lightblue;}';
const front = `{{Front}}\r\n<p class=\"tags\">{{Tags}}<\/p>\r\n\r\n<script>\r\n var tagEl = document.querySelector(\'.tags\');\r\n var tags = tagEl.innerHTML.split(\' \');\r\n var html = \'\';\r\n tags.forEach(function(tag) {\r\n\tif (tag) {\r\n\t var newTag = \'<span class=\"tag\">\' + tag + \'<\/span>\';\r\n html += newTag;\r\n \t tagEl.innerHTML = html;\r\n\t}\r\n });\r\n \r\n<\/script>${codeScriptContent}`;
const back = `{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}${sourceFieldContent}`;
const frontReversed = `{{Back}}\r\n<p class=\"tags\">{{Tags}}<\/p>\r\n\r\n<script>\r\n var tagEl = document.querySelector(\'.tags\');\r\n var tags = tagEl.innerHTML.split(\' \');\r\n var html = \'\';\r\n tags.forEach(function(tag) {\r\n\tif (tag) {\r\n\t var newTag = \'<span class=\"tag\">\' + tag + \'<\/span>\';\r\n html += newTag;\r\n \t tagEl.innerHTML = html;\r\n\t}\r\n });\r\n \r\n<\/script>${codeScriptContent}`;
const backReversed = `{{FrontSide}}\n\n<hr id=answer>\n\n{{Front}}${sourceFieldContent}`;
const prompt = `{{Prompt}}\r\n<p class=\"tags\">🧠spaced {{Tags}}<\/p>\r\n\r\n<script>\r\n var tagEl = document.querySelector(\'.tags\');\r\n var tags = tagEl.innerHTML.split(\' \');\r\n var html = \'\';\r\n tags.forEach(function(tag) {\r\n\tif (tag) {\r\n\t var newTag = \'<span class=\"tag\">\' + tag + \'<\/span>\';\r\n html += newTag;\r\n \t tagEl.innerHTML = html;\r\n\t}\r\n });\r\n \r\n<\/script>${codeScriptContent}`;
const prompt = `{{Prompt}}\r\n<p class="tags\">🧠spaced {{Tags}}<\/p>\r\n\r\n<script>\r\n var tagEl = document.querySelector(\'.tags\');\r\n var tags = tagEl.innerHTML.split(\' \');\r\n var html = \'\';\r\n tags.forEach(function(tag) {\r\n\tif (tag) {\r\n\t var newTag = \'<span class=\"tag\">\' + tag + \'<\/span>\';\r\n html += newTag;\r\n \t tagEl.innerHTML = html;\r\n\t}\r\n });\r\n \r\n<\/script>${codeScriptContent}`;
const promptBack = `{{FrontSide}}\n\n<hr id=answer>🧠 Review done.${sourceFieldContent}`;
const clozeFront = `{{cloze:Text}}\n\n<script>\r\n var tagEl = document.querySelector(\'.tags\');\r\n var tags = tagEl.innerHTML.split(\' \');\r\n var html = \'\';\r\n tags.forEach(function(tag) {\r\n\tif (tag) {\r\n\t var newTag = \'<span class=\"tag\">\' + tag + \'<\/span>\';\r\n html += newTag;\r\n \t tagEl.innerHTML = html;\r\n\t}\r\n });\r\n \r\n<\/script>${codeScriptContent}`;
const clozeBack = `{{cloze:Text}}\n\n<br>{{Extra}}${sourceFieldContent}<script>\r\n var tagEl = document.querySelector(\'.tags\');\r\n var tags = tagEl.innerHTML.split(\' \');\r\n var html = \'\';\r\n tags.forEach(function(tag) {\r\n\tif (tag) {\r\n\t var newTag = \'<span class=\"tag\">\' + tag + \'<\/span>\';\r\n html += newTag;\r\n \t tagEl.innerHTML = html;\r\n\t}\r\n });\r\n \r\n<\/script>${codeScriptContent}`;
let classicFields = ["Front", "Back"];
let promptFields = ["Prompt"];
let clozeFields = ["Text", "Extra"];
if (sourceSupport) {
classicFields = classicFields.concat("Source");
promptFields = promptFields.concat("Source");
clozeFields = clozeFields.concat("Source");
}
const obsidianBasic = {
@ -285,6 +289,24 @@ export class Anki {
},
};
const obsidianCloze = {
action: "createModel",
params: {
modelName: `Obsidian-cloze${sourceExtension}${codeExtension}`,
inOrderFields: clozeFields,
css: css,
isCloze: true,
cardTemplates: [
{
Name: "Cloze",
Front: clozeFront,
Back: clozeBack,
},
],
},
}
const obsidianSpaced = {
action: "createModel",
params: {
@ -301,10 +323,10 @@ export class Anki {
},
};
return [obsidianBasic, obsidianBasicReversed, obsidianSpaced];
return [obsidianBasic, obsidianBasicReversed, obsidianCloze, obsidianSpaced];
}
public async requestPermission() {
return await this.invoke("requestPermission", 6);
return this.invoke("requestPermission", 6);
}
}

View File

@ -52,12 +52,12 @@ export class Parser {
cards = cards.concat(
this.generateSpacedCards(file, headings, deck, vault, note, globalTags)
);
// cards = cards.concat(
// this.generateClozeCards(file, headings, deck, vault, note, globalTags)
// );
cards = cards.concat(
this.generateClozeCards(file, headings, deck, vault, note, globalTags)
);
// Filter out cards that are fully inside a code block
// Filter out cards that are fully inside a code block (that's needed for the )
const codeBlocks = [...file.matchAll(this.regex.obsidianCodeBlock)];
const rangesToDiscard = codeBlocks.map(x=>([x.index, x.index+x[0].length]))
cards = cards.filter(card => {
@ -194,6 +194,87 @@ export class Parser {
return cards;
}
private generateClozeCards(
file: string,
headings: any,
deck: string,
vault: string,
note: string,
globalTags: string[] = []
) {
const contextAware = this.settings.contextAwareMode;
const cards: Clozecard[] = [];
const matches = [...file.matchAll(this.regex.cardsClozeWholeLine)];
for (const match of matches) {
const reversed = false;
let headingLevel = -1;
if (match[1]) {
headingLevel =
match[1].trim().length !== 0 ? match[1].trim().length : -1;
}
// Match.index - 1 because otherwise in the context there will be even match[1], i.e. the question itself
const context = contextAware
? this.getContext(headings, match.index - 1, headingLevel)
: "";
// Replace the curly in the line with Anki syntax
let clozeText = match[2].replace(this.regex.singleClozeCurly, (match, g1, g2, g3) => {
console.log("group1 :" + g1);
console.log("group2 :" + g2);
console.log("group3 :" + g3);
if (g2) {
return `{{c${g2}::${g3}}}`;
} else {
return `{{c1::${g3}}}`;
}
} );
// Replace the highlight clozes in the line with Anki syntax
clozeText = clozeText.replace(this.regex.singleClozeHighlight, "{{c1::$2}}");
const originalLine = match[2].trim();
// Add context
clozeText = contextAware
? [...context, clozeText.trim()].join(
`${this.settings.contextSeparator}`
)
: clozeText.trim();
let medias: string[] = this.getImageLinks(clozeText);
medias = medias.concat(this.getAudioLinks(clozeText));
clozeText = this.parseLine(clozeText, vault);
const initialOffset = match.index;
const endingLine = match.index + match[0].length;
const tags: string[] = this.parseTags(match[4], globalTags);
const id: number = match[5] ? Number(match[5]) : -1;
const inserted: boolean = match[5] ? true : false;
const fields: any = { Text: clozeText, Extra: "" };
if (this.settings.sourceSupport) {
fields["Source"] = note;
}
const containsCode = this.containsCode([clozeText]);
const card = new Clozecard(
id,
deck,
originalLine,
fields,
reversed,
initialOffset,
endingLine,
tags,
inserted,
medias,
containsCode
);
cards.push(card);
}
return cards;
}
private generateInlineCards(
file: string,
headings: any,
@ -398,8 +479,7 @@ export class Parser {
filename
)}.md`;
const fileRename = rename ? rename : filename;
const link = `<a href="${href}">[[${fileRename}]]</a>`;
return link;
return `<a href="${href}">[[${fileRename}]]</a>`;
});
}