Anki note clipboard marker (#780)

* Update fields reference

* Add support for adding clipboard images to anki notes

* Add handlebars templates

* Add markers

* Add markers to readme
This commit is contained in:
toasted-nutbread 2020-09-08 11:01:08 -04:00 committed by GitHub
parent 36fc5abae5
commit f7093b4c1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 71 additions and 7 deletions

View File

@ -154,6 +154,7 @@ Flashcard fields can be configured with the following steps:
Marker | Description Marker | Description
-------|------------ -------|------------
`{audio}` | Audio sample of a native speaker's pronunciation in MP3 format (if available). `{audio}` | Audio sample of a native speaker's pronunciation in MP3 format (if available).
`{clipboard-image}` | An image which is stored in the system clipboard, if present.
`{cloze-body}` | Raw, inflected term as it appeared before being reduced to dictionary form by Yomichan. `{cloze-body}` | Raw, inflected term as it appeared before being reduced to dictionary form by Yomichan.
`{cloze-prefix}` | Fragment of the containing `{sentence}` starting at the beginning of `{sentence}` until the beginning of `{cloze-body}`. `{cloze-prefix}` | Fragment of the containing `{sentence}` starting at the beginning of `{sentence}` until the beginning of `{cloze-body}`.
`{cloze-suffix}` | Fragment of the containing `{sentence}` starting at the end of `{cloze-body}` until the end of `{sentence}`. `{cloze-suffix}` | Fragment of the containing `{sentence}` starting at the end of `{cloze-body}` until the end of `{sentence}`.
@ -178,6 +179,7 @@ Flashcard fields can be configured with the following steps:
Marker | Description Marker | Description
-------|------------ -------|------------
`{character}` | Unicode glyph representing the current kanji. `{character}` | Unicode glyph representing the current kanji.
`{clipboard-image}` | An image which is stored in the system clipboard, if present.
`{cloze-body}` | Raw, inflected parent term as it appeared before being reduced to dictionary form by Yomichan. `{cloze-body}` | Raw, inflected parent term as it appeared before being reduced to dictionary form by Yomichan.
`{cloze-prefix}` | Fragment of the containing `{sentence}` starting at the beginning of `{sentence}` until the beginning of `{cloze-body}`. `{cloze-prefix}` | Fragment of the containing `{sentence}` starting at the beginning of `{sentence}` until the beginning of `{cloze-body}`.
`{cloze-suffix}` | Fragment of the containing `{sentence}` starting at the end of `{cloze-body}` until the end of `{sentence}`. `{cloze-suffix}` | Fragment of the containing `{sentence}` starting at the end of `{cloze-body}` until the end of `{sentence}`.

View File

@ -0,0 +1,5 @@
{{#*inline "clipboard-image"}}
{{~#if definition.clipboardImageFileName~}}
<img src="{{definition.clipboardImageFileName}}" />
{{~/if~}}
{{/inline}}

View File

@ -276,4 +276,10 @@
{{/inline}} {{/inline}}
{{! End Pitch Accents }} {{! End Pitch Accents }}
{{#*inline "clipboard-image"}}
{{~#if definition.clipboardImageFileName~}}
<img src="{{definition.clipboardImageFileName}}" />
{{~/if~}}
{{/inline}}
{{~> (lookup . "marker") ~}} {{~> (lookup . "marker") ~}}

View File

@ -20,10 +20,11 @@
*/ */
class AnkiNoteBuilder { class AnkiNoteBuilder {
constructor({anki, audioSystem, renderTemplate}) { constructor({anki, audioSystem, renderTemplate, getClipboardImage=null}) {
this._anki = anki; this._anki = anki;
this._audioSystem = audioSystem; this._audioSystem = audioSystem;
this._renderTemplate = renderTemplate; this._renderTemplate = renderTemplate;
this._getClipboardImage = getClipboardImage;
} }
async createNote(definition, mode, context, options, templates) { async createNote(definition, mode, context, options, templates) {
@ -138,6 +139,31 @@ class AnkiNoteBuilder {
definition.screenshotFileName = fileName; definition.screenshotFileName = fileName;
} }
async injectClipboardImage(definition, fields) {
if (!this._containsMarker(fields, 'clipboard-image')) { return; }
const reading = definition.reading;
const now = new Date(Date.now());
try {
const dataUrl = await this._getClipboardImage();
if (dataUrl === null) { return; }
const extension = this._getImageExtensionFromDataUrl(dataUrl);
if (extension === null) { return; }
let fileName = `yomichan_clipboard_image_${reading}_${this._dateToString(now)}.${extension}`;
fileName = AnkiNoteBuilder.replaceInvalidFileNameCharacters(fileName);
const data = dataUrl.replace(/^data:[\w\W]*?,/, '');
await this._anki.storeMediaFile(fileName, data);
definition.clipboardImageFileName = fileName;
} catch (e) {
// NOP
}
}
_createInjectedAudioFileName(definition) { _createInjectedAudioFileName(definition) {
const {reading, expression} = definition; const {reading, expression} = definition;
if (!reading && !expression) { return null; } if (!reading && !expression) { return null; }
@ -170,6 +196,16 @@ class AnkiNoteBuilder {
return false; return false;
} }
_getImageExtensionFromDataUrl(dataUrl) {
const match = /^data:([^;]*);/.exec(dataUrl);
if (match === null) { return null; }
switch (match[1].toLowerCase()) {
case 'image/png': return 'png';
case 'image/jpeg': return 'jpeg';
default: return null;
}
}
static replaceInvalidFileNameCharacters(fileName) { static replaceInvalidFileNameCharacters(fileName) {
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
return fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '-'); return fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '-');

View File

@ -60,7 +60,8 @@ class Backend {
this._ankiNoteBuilder = new AnkiNoteBuilder({ this._ankiNoteBuilder = new AnkiNoteBuilder({
anki: this._anki, anki: this._anki,
audioSystem: this._audioSystem, audioSystem: this._audioSystem,
renderTemplate: this._renderTemplate.bind(this) renderTemplate: this._renderTemplate.bind(this),
getClipboardImage: this._onApiClipboardImageGet.bind(this)
}); });
this._templateRenderer = new TemplateRenderer(); this._templateRenderer = new TemplateRenderer();
@ -444,21 +445,28 @@ class Backend {
async _onApiDefinitionAdd({definition, mode, context, details, optionsContext}) { async _onApiDefinitionAdd({definition, mode, context, details, optionsContext}) {
const options = this.getOptions(optionsContext); const options = this.getOptions(optionsContext);
const templates = this._getTemplates(options); const templates = this._getTemplates(options);
const fields = (
mode === 'kanji' ?
options.anki.kanji.fields :
options.anki.terms.fields
);
if (mode !== 'kanji') { if (mode !== 'kanji') {
const {customSourceUrl} = options.audio; const {customSourceUrl} = options.audio;
await this._ankiNoteBuilder.injectAudio( await this._ankiNoteBuilder.injectAudio(
definition, definition,
options.anki.terms.fields, fields,
options.audio.sources, options.audio.sources,
customSourceUrl customSourceUrl
); );
} }
await this._ankiNoteBuilder.injectClipboardImage(definition, fields);
if (details && details.screenshot) { if (details && details.screenshot) {
await this._ankiNoteBuilder.injectScreenshot( await this._ankiNoteBuilder.injectScreenshot(
definition, definition,
options.anki.terms.fields, fields,
details.screenshot details.screenshot
); );
} }

View File

@ -432,7 +432,7 @@ class OptionsUtil {
update: this._updateVersion3.bind(this) update: this._updateVersion3.bind(this)
}, },
{ {
async: false, async: true,
update: this._updateVersion4.bind(this) update: this._updateVersion4.bind(this)
} }
]; ];
@ -468,10 +468,11 @@ class OptionsUtil {
return options; return options;
} }
static _updateVersion4(options) { static async _updateVersion4(options) {
// Version 4 changes: // Version 4 changes:
// Options conditions converted to string representations. // Options conditions converted to string representations.
// Added usePopupWindow. // Added usePopupWindow.
// Updated handlebars templates to include "clipboard-image" definition.
for (const {conditionGroups} of options.profiles) { for (const {conditionGroups} of options.profiles) {
for (const {conditions} of conditionGroups) { for (const {conditions} of conditionGroups) {
for (const condition of conditions) { for (const condition of conditions) {
@ -487,6 +488,7 @@ class OptionsUtil {
for (const {options: profileOptions} of options.profiles) { for (const {options: profileOptions} of options.profiles) {
profileOptions.general.usePopupWindow = false; profileOptions.general.usePopupWindow = false;
} }
await this._addFieldTemplatesToOptions(options, '/bg/data/anki-field-templates-upgrade-v4.handlebars');
return options; return options;
} }
} }

View File

@ -143,7 +143,10 @@ class AnkiTemplatesController {
}; };
let templates = options.anki.fieldTemplates; let templates = options.anki.fieldTemplates;
if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; } if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; }
const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: api.templateRender.bind(api)}); const ankiNoteBuilder = new AnkiNoteBuilder({
renderTemplate: api.templateRender.bind(api),
getClipboardImage: api.clipboardGetImage.bind(api)
});
const data = ankiNoteBuilder.createNoteData(definition, mode, context, options); const data = ankiNoteBuilder.createNoteData(definition, mode, context, options);
result = await ankiNoteBuilder.formatField(field, data, templates, exceptions); result = await ankiNoteBuilder.formatField(field, data, templates, exceptions);
} }

View File

@ -44,6 +44,7 @@ class AnkiController {
case 'terms': case 'terms':
return [ return [
'audio', 'audio',
'clipboard-image',
'cloze-body', 'cloze-body',
'cloze-prefix', 'cloze-prefix',
'cloze-suffix', 'cloze-suffix',
@ -66,6 +67,7 @@ class AnkiController {
case 'kanji': case 'kanji':
return [ return [
'character', 'character',
'clipboard-image',
'cloze-body', 'cloze-body',
'cloze-prefix', 'cloze-prefix',
'cloze-suffix', 'cloze-suffix',