Skip to content

Commit

Permalink
Enhance HTML functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
tansongchen committed Nov 2, 2022
1 parent 7da8ac0 commit ace53a9
Show file tree
Hide file tree
Showing 13 changed files with 131 additions and 69 deletions.
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ The plugin works in two modes:

## Installation

The plugin is currently in alpha stage and therefore not on the Obsidian marketplace. Therefore, manual installation is required.

Download the file `dist.zip` from [release page](https://github.com/tansongchen/obsidian-anki-synchronizer/releases), create a new folder `obsidian-anki-synchronizer` under the `.obsidian/plugins` directory in your vault, and put the three files extracted into this folder. Reload obsidian.

For example, the plugin path on my computer is `/Users/tansongchen/Library/Mobile Documents/iCloud~md~obsidian/Documents/卡片盒/.obsidian/plugins/obsidian-anki-synchronizer`.
Install via the Obsidian community plugin marketplace by searching "Note Synchronizer".

## Setup

Expand Down
6 changes: 1 addition & 5 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@

## 安装

本插件目前处于 Alpha 阶段,所以还没有在 Obsidian 插件市场发布。您需要手动安装此插件。

[发布页](https://github.com/tansongchen/obsidian-anki-synchronizer/releases)下载 `dist.zip` 文件,然后在您的知识库目录下面的 `.obsidian/plugins` 新建一个文件夹 `obsidian-anki-synchronizer`,然后把解压得到的三个文件放进去。重启 Obsidian。

例如,我这里的目录是 `/Users/tansongchen/Library/Mobile Documents/iCloud~md~obsidian/Documents/卡片盒/.obsidian/plugins/obsidian-anki-synchronizer`
在 Obsidian 插件市场中搜索「Note Synchronizer」并根据提示安装即可。

## 配置

Expand Down
7 changes: 4 additions & 3 deletions main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { normalizePath, Notice, Plugin } from 'obsidian';
import Anki, { AnkiError } from 'src/anki';
import Note from 'src/note';
import Note, { NoteManager } from 'src/note';
import locale from 'src/lang';
import { NoteDigest, NoteState, NoteTypeDigest, NoteTypeState } from 'src/state';
import AnkiSynchronizerSettingTab, { Settings, DEFAULT_SETTINGS } from 'src/setting';
Expand All @@ -16,6 +16,7 @@ interface Data {
export default class AnkiSynchronizer extends Plugin {
anki = new Anki();
settings = DEFAULT_SETTINGS;
noteManager = new NoteManager(this.settings);
noteState = new NoteState(this);
noteTypeState = new NoteTypeState(this);

Expand Down Expand Up @@ -119,7 +120,7 @@ export default class AnkiSynchronizer extends Plugin {
const content = await this.app.vault.cachedRead(file);
const frontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter;
if (!frontmatter) continue;
const note = Note.validateNote(file.path, frontmatter, content, this.noteTypeState);
const note = this.noteManager.validateNote(file.path, frontmatter, content, this.noteTypeState);
if (!note) continue;
if (note.nid === 0) { // new file
const nid = await this.noteState.handleAddNote(note);
Expand All @@ -128,7 +129,7 @@ export default class AnkiSynchronizer extends Plugin {
continue;
}
note.nid = nid;
this.app.vault.modify(file, note.dump());
this.app.vault.modify(file, this.noteManager.dump(note));
}
state.set(note.nid, [note.digest(), note]);
}
Expand Down
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"id": "note-synchronizer",
"name": "Note Synchronizer",
"version": "0.1.0",
"version": "0.1.1",
"minAppVersion": "0.14.0",
"description": "This is a plugin for synchornizing Obsidian notes to other note-based softwares like Anki, following more strictly the principles of Zettelkasten and treating each Obsidian file as a note.",
"author": "Songchen Tan",
"authorUrl": "https://tansongchen.com",
"isDesktopOnly": true
}
}
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "obsidian-note-synchronizer",
"version": "0.1.0",
"version": "0.1.1",
"description": "This is a plugin for synchornizing Obsidian notes to other note-based softwares like Anki, following more strictly the principles of Zettelkasten and treating each Obsidian file as a note.",
"main": "main.js",
"scripts": {
Expand All @@ -13,6 +13,7 @@
"author": "",
"license": "MIT",
"dependencies": {
"highlight.js": "^11.6.0",
"markdown-it": "^13.0.1",
"object-hash": "^3.0.0"
},
Expand Down
25 changes: 23 additions & 2 deletions src/format.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import { Settings } from "./setting";
import MarkdownIt from "markdown-it";
import hljs from "highlight.js";

export default class Formatter {
private settings: Settings;
private mdit = new MarkdownIt();
private mdit = new MarkdownIt({
html: true,
linkify: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang }).value;
} catch (__) {
return '';
}
}
return '';
}
});
private vaultName: string;

constructor(vaultName: string, settings: Settings) {
Expand All @@ -17,12 +31,19 @@ export default class Formatter {
}

markdown(markup: string) {
return markup.replace(/\[\[(.+)\]\]/, (match, p) => {
return markup.replace(/!?\[\[(.+?)\]\]/g, (match, p) => {
return this.renderBacklink(p);
});
}

convertMathDelimiter(markdown: string) {
markdown = markdown.replace(/\$(.+?)\$/g, '\\\\($1\\\\)');
markdown = markdown.replace(/\$\$(.+?)\$\$/gs, '\\\\[$1\\\\]');
return markdown;
}

html(markdown: string) {
markdown = this.convertMathDelimiter(markdown);
return this.mdit.render(markdown);
}

Expand Down
20 changes: 13 additions & 7 deletions src/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ interface Locale {
settingTabHeader: string,
settingRenderName: string,
settingRenderDescription: string,
settingHeadingLevelName: string,
settingHeadingLevelDescription: string,
}

const en: Locale = {
onLoad: 'Anki Synchronizer is successfully loaded!',
onUnload: 'Anki Synchronizer is successfully unloaded!',
onLoad: 'Note Synchronizer is successfully loaded!',
onUnload: 'Note Synchronizer is successfully unloaded!',
synchronizeCommandName: 'Synchronize',
templatesNotEnabledNotice: 'Core plugin Templates is not enabled!',
importCommandName: 'Import Note Types',
Expand All @@ -39,14 +41,16 @@ const en: Locale = {
synchronizeChangeDeckFailureNotice: (filename: string) => `Cannot change deck for ${filename}`,
synchronizeUpdateFieldsFailureNotice: (filename: string) => `Cannot update fields for ${filename}`,
synchronizeUpdateTagsFailureNotice: (filename: string) => `Cannot update tags for ${filename}`,
settingTabHeader: 'Anki Synchronizer Settings',
settingTabHeader: 'Note Synchronizer Settings',
settingRenderName: 'Render',
settingRenderDescription: 'Whether to render markdown before importing to Anki or not.',
settingHeadingLevelName: 'Field name heading level',
settingHeadingLevelDescription: 'Which level (h1, h2, h3, ...) to use for field names when generating the note template',
}

const zh_cn: Locale = {
onLoad: 'Anki 同步插件已成功启用!',
onUnload: 'Anki 同步插件已成功禁用!',
onLoad: '笔记同步插件已成功启用!',
onUnload: '笔记同步插件已成功禁用!',
synchronizeCommandName: '同步',
templatesNotEnabledNotice: '核心插件「模板」未启用,操作无法执行!',
importCommandName: '导入笔记类型',
Expand All @@ -61,9 +65,11 @@ const zh_cn: Locale = {
synchronizeChangeDeckFailureNotice: (filename: string) => `无法改变 ${filename} 的牌组`,
synchronizeUpdateFieldsFailureNotice: (filename: string) => `无法更新 ${filename} 的字段`,
synchronizeUpdateTagsFailureNotice: (filename: string) => `无法更新 ${filename} 的标签`,
settingTabHeader: 'Anki 同步设置',
settingTabHeader: '笔记同步设置',
settingRenderName: '渲染',
settingRenderDescription: '是否在导入时将 Markdown 渲染为 HTML'
settingRenderDescription: '是否在导入时将 Markdown 渲染为 HTML',
settingHeadingLevelName: '字段名称标题层级',
settingHeadingLevelDescription: '从 Anki 笔记类型生成模板时,将 Anki 的字段名称表示为几级标题(一级、二级、三级等)',
}

const locales: { [k: string]: Partial<Locale> } = {
Expand Down
88 changes: 50 additions & 38 deletions src/note.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { stringifyYaml, FrontMatterCache } from "obsidian";
import { NoteDigest, NoteTypeDigest } from "./state";
import { MD5 } from 'object-hash';
import { Settings } from "./setting";

export interface FrontMatter {
mid: number,
Expand All @@ -12,77 +13,88 @@ export default class Note {
nid: number;
tags: string[];
fields: Record<string, string>;
private path: string;
path: string;
typeName: string;
private mid: number;
private extras: object;
mid: number;
extras: object;

static validateNote(path: string, frontmatter: FrontMatterCache, content: string, noteTypes: Map<number, NoteTypeDigest>) {
constructor(path: string, typeName: string, frontMatter: FrontMatter, fields: Record<string, string>) {
this.path = path;
const { mid, nid, tags, ...extras } = frontMatter;
this.typeName = typeName;
this.mid = mid;
this.nid = nid;
this.tags = tags;
this.extras = extras;
this.fields = fields;
}

digest(): NoteDigest {
return { deck: this.renderDeckName(), hash: MD5(this.fields), tags: this.tags }
}

title() {
return Object.values(this.fields)[0];
}

renderDeckName() {
return this.path.split('/').slice(0, -1).join('::') || 'Obsidian';
}
}

export class NoteManager {
private settings: Settings;

constructor(settings: Settings) {
this.settings = settings;
}

validateNote(path: string, frontmatter: FrontMatterCache, content: string, noteTypes: Map<number, NoteTypeDigest>) {
if (!frontmatter.hasOwnProperty('mid') || !frontmatter.hasOwnProperty('nid') || !frontmatter.hasOwnProperty('tags')) return;
const frontMatter = Object.assign({}, frontmatter, { position: undefined }) as FrontMatter;
const lines = content.split('\n');
const yamlEndIndex = lines.indexOf('---', 1);
const body = lines.slice(yamlEndIndex + 1);
const noteType = noteTypes.get(frontMatter.mid);
if (!noteType) return;
const fields = Note.parseFields(path, noteType.fieldNames, body);
const fields = this.parseFields(path, noteType.fieldNames, body);
if (!fields) return;
// now it is a valid Note
return new Note(path, noteType.name, frontMatter, fields);
}

static parseFields(path: string, fieldNames: string[], body: string[]) {
parseFields(path: string, fieldNames: string[], body: string[]) {
const headingLevel = this.settings.headingLevel;
const pathList = path.split('/');
const baseName = pathList[pathList.length - 1];
const fieldContents: string[] = [baseName.split('.').slice(0, -1).join('')];
let buffer: string[] = [];
for (const line of body) {
if (line.slice(0, 2) === '# ') {
if (line.slice(0, headingLevel + 1) === ('#'.repeat(headingLevel) + ' ')) {
fieldContents.push(buffer.join('\n'));
buffer = [];
} else {
buffer.push(line)
}
}
fieldContents.push(buffer.join('\n'));
if (fieldNames.length !== fieldContents.length) return;
const fields: Record<string, string> = {};
fieldNames.map((v, i) => fields[v] = fieldContents[i]);
return fields;
}

constructor(path: string, typeName: string, frontMatter: FrontMatter, fields: Record<string, string>) {
this.path = path;
const { mid, nid, tags, ...extras } = frontMatter;
this.typeName = typeName;
this.mid = mid;
this.nid = nid;
this.tags = tags;
this.extras = extras;
this.fields = fields;
}

digest(): NoteDigest {
return { deck: this.renderDeckName(), hash: MD5(this.fields), tags: this.tags }
}

title() {
return Object.values(this.fields)[0];
}

dump() {
dump(note: Note) {
const frontMatter = stringifyYaml(Object.assign({
mid: this.mid,
nid: this.nid,
tags: this.tags
}, this.extras)).trim().replace(/"/g, ``);
const fieldNames = Object.keys(this.fields);
const lines = [`---`, frontMatter, `---`, this.fields[fieldNames[1]]];
mid: note.mid,
nid: note.nid,
tags: note.tags
}, note.extras)).trim().replace(/"/g, ``);
const fieldNames = Object.keys(note.fields);
const lines = [`---`, frontMatter, `---`, note.fields[fieldNames[1]]];
fieldNames.slice(2).map(s => {
lines.push(`# ${s}`, this.fields[s]);
lines.push(`${'#'.repeat(this.settings.headingLevel)} ${s}`, note.fields[s]);
});
return lines.join('\n');
}

renderDeckName() {
return this.path.split('/').slice(0, -1).join('::') || 'Obsidian';
}
}
16 changes: 15 additions & 1 deletion src/setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import AnkiSynchronizer from "main";
// Plugin Settings
export interface Settings {
render: boolean;
headingLevel: number;
}

export const DEFAULT_SETTINGS: Settings = {
render: false,
headingLevel: 1,
}

export default class AnkiSynchronizerSettingTab extends PluginSettingTab {
Expand All @@ -30,6 +32,18 @@ export default class AnkiSynchronizerSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.render)
.onChange(async (value) => {
this.plugin.settings.render = value;
}));
})
);

new Setting(this.containerEl)
.setName(locale.settingHeadingLevelName)
.setDesc(locale.settingHeadingLevelDescription)
.addDropdown(v => v
.addOptions({"1": "h1", "2": "h2", "3": "h3", "4": "h4", "5": "h5", "6": "h6"})
.setValue(this.plugin.settings.headingLevel.toString())
.onChange(async (value) => {
this.plugin.settings.headingLevel = parseInt(value);
})
);
}
}
Loading

0 comments on commit ace53a9

Please sign in to comment.