Add support for code syntax highlight

This commit is contained in:
reuseman 2020-12-12 12:07:01 +01:00
parent e5de6b32aa
commit 7dc7915c1f
13 changed files with 127 additions and 32 deletions

View File

@ -14,7 +14,8 @@ Anki integration for [Obsidian](https://obsidian.md/).
🏷️ Global and local **tags**
🔢 Support for **LaTeX**
🖼️ Support for **images**
🔗 **Obsidian URI** support
🔗 Support for **Obsidian URI**
📟 Support for **Code syntax highlight**
## How it works?

View File

@ -56,7 +56,7 @@ export default class ObsidianFlashcard extends Plugin {
}
private getDefaultSettings(): ISettings {
return { contextAwareMode: true, contextSeparator: " > ", deck: "Default", flashcardsTag: "card" }
return { contextAwareMode: true, codeHighlightSupport: true, contextSeparator: " > ", deck: "Default", flashcardsTag: "card" }
}
private generateCards(activeFile: TFile) {

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,4 @@
import { codeDeckExtension } from 'src/constants'
import { arraysEqual } from 'src/utils'
export abstract class Card {
@ -12,9 +13,11 @@ export abstract class Card {
mediaNames: string[]
mediaBase64Encoded: string[]
oldTags: string[]
containsCode: boolean
modelName: string
// 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, mediaNames: string[]) {
constructor(id: number, deckName: string, initialContent: string, fields: Record<string, string>, reversed: boolean, endOffset: number, tags: string[], inserted: boolean, mediaNames: string[], containsCode: boolean = false) {
this.id = id
this.deckName = deckName
this.initialContent = initialContent
@ -27,6 +30,8 @@ export abstract class Card {
this.mediaNames = mediaNames
this.mediaBase64Encoded = []
this.oldTags = []
this.containsCode = containsCode
this.modelName = ""
}
abstract toString(): string
@ -35,6 +40,11 @@ export abstract class Card {
abstract getIdFormat(): string
match(card: any): boolean {
// TODO not supported currently
// if (this.modelName !== card.modelName) {
// return false
// }
let fields = Object.entries(card.fields)
for (let field of fields) {
let fieldName = field[0]
@ -45,4 +55,8 @@ export abstract class Card {
return arraysEqual(card.tags, this.tags)
}
getCodeDeckNameExtension() {
return this.containsCode ? codeDeckExtension : ""
}
}

View File

@ -1,15 +1,16 @@
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, mediaNames: string[]) {
super(id, deckName, initialContent, fields, reversed, endOffset, tags, inserted, mediaNames)
constructor(id: number = -1, deckName: string, initialContent: string, fields: Record<string, string>, reversed: boolean, endOffset: number, tags: string[] = [], inserted: boolean = false, mediaNames: string[], containsCode: boolean) {
super(id, deckName, initialContent, fields, reversed, endOffset, tags, inserted, mediaNames, containsCode)
let codeExtension = this.getCodeDeckNameExtension()
this.modelName = this.reversed ? `Obsidian-basic-reversed${codeExtension}` : `Obsidian-basic${codeExtension}`
}
public getCard(update: boolean = false): object {
let modelName = this.reversed ? "Obsidian-basic-reversed" : "Obsidian-basic"
let card: any = {
"deckName": this.deckName,
"modelName": modelName,
"modelName": this.modelName,
"fields": {
"Front": this.fields["Front"],
"Back": this.fields["Back"]

View File

@ -1,15 +1,16 @@
import { Card } from "src/entities/card";
export class Inlinecard extends Card {
constructor(id: number = -1, deckName: string, initialContent: string, fields: Record<string, string>, reversed: boolean, endOffset: number, tags: string[] = [], inserted: boolean = false, mediaNames: string[]) {
super(id, deckName, initialContent, fields, reversed, endOffset, tags, inserted, mediaNames)
constructor(id: number = -1, deckName: string, initialContent: string, fields: Record<string, string>, reversed: boolean, endOffset: number, tags: string[] = [], inserted: boolean = false, mediaNames: string[], containsCode: boolean) {
super(id, deckName, initialContent, fields, reversed, endOffset, tags, inserted, mediaNames, containsCode)
let codeExtension = this.getCodeDeckNameExtension()
this.modelName = `Obsidian-basic${codeExtension}`
}
public getCard(update: boolean = false): object {
let modelName = "Obsidian-basic"
let card: any = {
"deckName": this.deckName,
"modelName": modelName,
"modelName": this.modelName,
"fields": {
"Front": this.fields["Front"],
"Back": this.fields["Back"]

View File

@ -1,15 +1,17 @@
import { Card } from "src/entities/card";
export class Spacedcard extends Card {
constructor(id: number = -1, deckName: string, initialContent: string, fields: Record<string, string>, reversed: boolean, endOffset: number, tags: string[] = [], inserted: boolean = false, mediaNames: string[]) {
super(id, deckName, initialContent, fields, reversed, endOffset, tags, inserted, mediaNames)
constructor(id: number = -1, deckName: string, initialContent: string, fields: Record<string, string>, reversed: boolean, endOffset: number, tags: string[] = [], inserted: boolean = false, mediaNames: string[], containsCode: boolean) {
super(id, deckName, initialContent, fields, reversed, endOffset, tags, inserted, mediaNames, containsCode)
let codeExtension = this.getCodeDeckNameExtension()
this.modelName = `Obsidian-spaced${codeExtension}`
}
public getCard(update: boolean = false): object {
let modelName = "Obsidian-spaced"
let card: any = {
"deckName": this.deckName,
"modelName": modelName,
"modelName": this.modelName,
"fields": {
"Prompt": this.fields["Prompt"],
},

View File

@ -32,6 +32,18 @@ export class SettingsTab extends PluginSettingTab {
})
)
new Setting(containerEl)
.setName("Code highlight support")
.setDesc("Add highlight of the code in Anki.")
.addToggle((toggle) =>
toggle
.setValue(plugin.settings.codeHighlightSupport)
.onChange((value) => {
plugin.settings.codeHighlightSupport = value
plugin.saveData(plugin.settings)
})
)
new Setting(containerEl)
.setName("Default deck")
.setDesc("The name of the default deck where the cards will be added when not specified.")
@ -64,5 +76,6 @@ export class SettingsTab extends PluginSettingTab {
})
})
}
}

View File

@ -4,6 +4,7 @@ export class Regex {
headingsRegex: RegExp
wikiImageLinks: RegExp
markdownImageLinks: RegExp
codeBlock: RegExp
cardsDeckLine: RegExp
cardsToDelete: RegExp
flashscardsWithTag: RegExp
@ -21,6 +22,7 @@ export class Regex {
// Supported images https://publish.obsidian.md/help/How+to/Embed+files
this.wikiImageLinks = /!\[\[(.*\.(?:png|jpg|jpeg|gif|bmp|svg|tiff))\]\]/gim
this.markdownImageLinks = /!\[\]\((.*\.(?:png|jpg|jpeg|gif|bmp|svg|tiff))\)/gim
this.codeBlock = /<code\b[^>]*>(.*?)<\/code>/gims
this.cardsDeckLine = /cards-deck: [\w\d]+/gi
this.cardsToDelete = /^\s*(?:\n)(?:\^(\d{13}))(?:\n\s*?)?/gm

View File

@ -1,13 +1,24 @@
import { Card } from 'src/entities/card';
import { codeScript, highlightjsBase64, hihglightjsInitBase64, highlightCssBase64, codeDeckExtension } from 'src/constants'
export class Anki {
public async createModels() {
public async createModels(codeHighlightSupport: boolean) {
let 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"
let 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>"
let 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>"
let 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>"
let 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>`
let 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>`
let 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>`
let models = this.getModels(front, frontReversed, prompt, css)
if (codeHighlightSupport) {
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"
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>\r\n${codeScript}\r\n`
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>\r\n${codeScript}\r\n`
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>\r\n${codeScript}\r\n`
models = models.concat(this.getModels(front, frontReversed, prompt, css, codeDeckExtension))
}
return this.invoke("multi", 6, { "actions": models })
}
@ -34,6 +45,37 @@ export class Anki {
}
}
public async storeCodeHighlightMedias() {
let fileExists = await this.invoke(
"retrieveMediaFile",
6,
{
"filename": "_highlightInit.js"
})
if (!fileExists) {
let highlightjs = {
"action": "storeMediaFile", "params": {
"filename": "_highlight.js",
"data": highlightjsBase64
}
}
let highlightjsInit = {
"action": "storeMediaFile", "params": {
"filename": "_highlightInit.js",
"data": hihglightjsInitBase64
}
}
let highlightjcss = {
"action": "storeMediaFile", "params": {
"filename": "_highlight.css",
"data": highlightCssBase64
}
}
return this.invoke("multi", 6, { "actions": [highlightjs, highlightjsInit, highlightjcss] })
}
}
public async addCards(cards: Card[]): Promise<number[]> {
let notes: any = []
@ -121,7 +163,7 @@ export class Anki {
return actions
}
private invoke(action: string, version: number, params = {}): any {
private invoke(action: string, version: number = 6, params = {}): any {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener('error', () => reject('failed to issue request'));
@ -151,11 +193,11 @@ export class Anki {
});
}
private getModels(front: string, frontReversed: string, prompt: string, css: string): object[] {
private getModels(front: string, frontReversed: string, prompt: string, css: string, extension: string = ""): object[] {
let obsidianBasic = {
"action": "createModel",
"params": {
"modelName": "Obsidian-basic",
"modelName": `Obsidian-basic${extension}`,
"inOrderFields": ["Front", "Back"],
"css": css,
"cardTemplates": [
@ -171,7 +213,7 @@ export class Anki {
let obsidianBasicReversed = {
"action": "createModel",
"params": {
"modelName": "Obsidian-basic-reversed",
"modelName": `Obsidian-basic-reversed${extension}`,
"inOrderFields": ["Front", "Back"],
"css": css,
"cardTemplates": [
@ -192,7 +234,7 @@ export class Anki {
let obsidianSpaced = {
"action": "createModel",
"params": {
"modelName": "Obsidian-spaced",
"modelName": `Obsidian-spaced${extension}`,
"inOrderFields": ["Prompt"],
"css": css,
"cardTemplates": [

View File

@ -3,7 +3,6 @@ import { App, FileSystemAdapter, FrontMatterCache, Notice, parseFrontMatterEntry
import { Parser } from 'src/services/parser'
import { ISettings } from 'src/settings'
import { Card } from 'src/entities/card'
import { Flashcard } from 'src/entities/flashcard'
import { arrayBufferToBase64 } from "src/utils"
import { Regex } from 'src/regex'
import { noticeTimeout } from 'src/constants'
@ -57,7 +56,8 @@ export class CardsService {
}
try {
await this.anki.createModels()
await this.anki.storeCodeHighlightMedias()
await this.anki.createModels(this.settings.codeHighlightSupport)
await this.anki.createDeck(deckName)
this.file = await this.app.vault.read(activeFile)
// TODO with empty check that does not call ankiCards line

View File

@ -17,6 +17,7 @@ export class Parser {
this.htmlConverter.setOption("simplifiedAutoLink", true)
this.htmlConverter.setOption("tables", true)
this.htmlConverter.setOption("tasks", true)
this.htmlConverter.setOption("ghCodeBlocks", true)
}
public generateFlashcards(file: string, deck: string, vault: string, globalTags: string[] = []): Flashcard[] {
@ -106,8 +107,9 @@ export class Parser {
let id: number = match[5] ? Number(match[5]) : -1
let inserted: boolean = match[5] ? true : false
let fields = { "Prompt": prompt }
let containsCode = this.containsCode([prompt])
let card = new Spacedcard(id, deck, originalPrompt, fields, reversed, endingLine, tags, inserted, imagesMedia)
let card = new Spacedcard(id, deck, originalPrompt, fields, reversed, endingLine, tags, inserted, imagesMedia, containsCode)
cards.push(card)
}
@ -141,8 +143,9 @@ export class Parser {
let id: number = match[5] ? Number(match[5]) : -1
let inserted: boolean = match[5] ? true : false
let fields = { "Front": question, "Back": answer }
let containsCode = this.containsCode([question, answer])
let card = new Inlinecard(id, deck, originalQuestion, fields, reversed, endingLine, tags, inserted, imagesMedia)
let card = new Inlinecard(id, deck, originalQuestion, fields, reversed, endingLine, tags, inserted, imagesMedia, containsCode)
cards.push(card)
}
@ -173,21 +176,31 @@ export class Parser {
let id: number = match[6] ? Number(match[6]) : -1
let inserted: boolean = match[6] ? true : false
let fields = { "Front": question, "Back": answer }
let containsCode = this.containsCode([question, answer])
let card = new Flashcard(id, deck, originalQuestion, fields, reversed, endingLine, tags, inserted, imagesMedia)
let card = new Flashcard(id, deck, originalQuestion, fields, reversed, endingLine, tags, inserted, imagesMedia, containsCode)
cards.push(card)
}
return cards
}
public containsCode(str: string[]): boolean {
for (let s of str) {
if (s.match(this.regex.codeBlock)) {
return true
}
}
return false
}
public getCardsToDelete(file: string): number[] {
// Find block IDs with no content above it
return [...file.matchAll(this.regex.cardsToDelete)].map((match) => { return Number(match[1]) })
}
private parseLine(str: string, vaultName: string) {
return this.mathToAnki(this.substituteObsidianLinks(this.substituteImageLinks(str), vaultName))
return this.mathToAnki(this.htmlConverter.makeHtml(this.substituteObsidianLinks(this.substituteImageLinks(str), vaultName)))
}
private getImageLinks(str: string) {

View File

@ -1,5 +1,6 @@
export interface ISettings {
contextAwareMode: boolean
codeHighlightSupport: boolean
contextSeparator: string
deck: string
flashcardsTag: string