├── .prettierignore
├── icon.png
├── .gitattributes
├── src
├── types
│ ├── vite.type.ts
│ └── global.type.ts
├── templates
│ ├── _logseq_anki_sync_back.css
│ ├── _logseq_anki_sync_front.css
│ ├── _logseq_anki_sync_back.js
│ ├── _logseq_anki_sync.js
│ ├── AnkiCardTemplates.ts
│ ├── template.html
│ ├── compareAnswer.js
│ ├── _logseq_anki_sync_front.js
│ └── _logseq_anki_sync.scss
├── utils
│ ├── objectHashOptimized.ts
│ ├── waitForElement.ts
│ └── utils.ts
├── logseq
│ ├── getUUIDFromBlock.ts
│ ├── getLogseqContentDirectDependencies.ts
│ ├── blockAndPageHashCache.ts
│ └── BlockContentParser.ts
├── addons
│ ├── Addon.ts
│ ├── AddonRegistry.ts
│ ├── LogseqAnkiFeatureExplorer.ts
│ ├── HideOcclusionData.ts
│ └── PreviewInAnki.ts
├── ui
│ ├── ReactDOM.ts
│ ├── basic
│ │ ├── LogseqDropdownMenu.tsx
│ │ ├── LogseqCheckbox.tsx
│ │ └── LogseqButton.tsx
│ ├── React.ts
│ ├── customized
│ │ ├── ProgressNotification.ts
│ │ └── SyncResultDialog.tsx
│ └── general
│ │ ├── Notification.tsx
│ │ ├── Confirm.tsx
│ │ ├── Modal.tsx
│ │ ├── SelectionModal.tsx
│ │ ├── ActionNotification.tsx
│ │ └── ModelWithBtns.tsx
├── notes
│ ├── NoteUtils.ts
│ ├── NoteHashCalculator.ts
│ ├── SwiftArrowNote.ts
│ ├── Note.ts
│ ├── ClozeNote.ts
│ └── MultilineCardNote.ts
├── index.ts
├── settings.ts
└── anki-connect
│ └── AnkiConnect.ts
├── .babelrc
├── .idea
├── dictionaries
│ └── project.xml
├── .gitignore
├── vcs.xml
├── markdown.xml
├── modules.xml
└── logseq-anki-sync-dev.iml
├── .gitignore
├── index.html
├── vitest.config.ts
├── .prettierrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── FUNDING.yml
└── workflows
│ └── publish.yml
├── tsconfig.json
├── .eslintrc.js
├── package.json
├── tests
└── compareAnswer
│ └── compareAnswer.test.ts
├── vite.config.ts
└── README.md
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist/**
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/debanjandhar12/logseq-anki-sync/HEAD/icon.png
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/src/types/vite.type.ts:
--------------------------------------------------------------------------------
1 | declare module "*?string" {
2 | const value: string;
3 | export default value;
4 | }
5 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "test": {
4 | "presets": ["@babel/preset-env", "@babel/preset-typescript"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/templates/_logseq_anki_sync_back.css:
--------------------------------------------------------------------------------
1 | /***
2 | * This files contains the css for the back side of anki cards.
3 | */
4 | .extra {
5 | display: block;
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/objectHashOptimized.ts:
--------------------------------------------------------------------------------
1 | import hashIt from "hash-it";
2 |
3 | export default function objectHashOptimized(obj: any) {
4 | return hashIt(obj);
5 | }
6 |
--------------------------------------------------------------------------------
/.idea/dictionaries/project.xml:
--------------------------------------------------------------------------------
1 |
123')
12 | });
13 | test('completely incorrect input', async () => {
14 | const expected = '123'
15 | const provided = '456'
16 | const result = compareAnswer(expected, provided);
17 |
18 | expect(result).toEqual('456
↓
123')
19 | });
20 | test('partially correct input', async () => {
21 | const expected = '123'
22 | const provided = '1123'
23 | const result = compareAnswer(expected, provided);
24 |
25 | expect(result).toEqual('1123
↓
123')
26 | });
27 | test('empty input', async () => {
28 | const expected = '123'
29 | const provided = ''
30 | const result = compareAnswer(expected, provided);
31 |
32 | expect(result).toEqual('123')
33 | });
34 | test('empty expected', async () => {
35 | const expected = ''
36 | const provided = '123'
37 | const result = compareAnswer(expected, provided);
38 |
39 | expect(result).toEqual('123
↓
')
40 | });
41 | test('white space is trimmed', async () => {
42 | const expected = ' 123'
43 | const provided = '123 '
44 | const result = compareAnswer(expected, provided);
45 |
46 | expect(result).toEqual('123')
47 | });
48 | });
49 |
50 | describe("compareAnswer unicode tests", () => {
51 | test('tokenization test', async () => {
52 | const expected = "¿Y ahora qué vamos a hacer?";
53 | const provided = "y ahora qe vamosa hacer";
54 | const result = compareAnswer(expected, provided);
55 |
56 | expect(result).toEqual('y ahora qe vamos-a hacer-
↓
¿Y ahora qué vamos a hacer?');
57 | });
58 | test('tokenization test with Russian characters', async () => {
59 | const expected = "нос";
60 | const provided = "нс";
61 | const result = compareAnswer(expected, provided);
62 |
63 | expect(result).toEqual('н-с
↓
нос');
64 | });
65 | test('tokenization test with Korean characters', async () => {
66 | const expected = "쓰다듬다";
67 | const provided = "스다뜸다";
68 | const result = compareAnswer(expected, provided);
69 |
70 | expect(result).toEqual('스다뜸다
↓
쓰다듬다');
71 | });
72 | });
--------------------------------------------------------------------------------
/src/ui/general/Confirm.tsx:
--------------------------------------------------------------------------------
1 | import React from "../React";
2 | import {Modal} from "./Modal";
3 | import {LogseqButton} from "../basic/LogseqButton";
4 | import {UI} from "../UI";
5 |
6 | export async function Confirm(msg: string): Promise⏎
93 |
94 |
95 |
71 |
72 |
${escapeHtml(this.expected)}`;
80 | } else if (this.provided === this.expected) {
81 | return `${provided}`;
82 | } else {
83 | return `${provided}
↓
${expected}`;
84 | }
85 | }
86 | }
87 |
88 | class DiffToken {
89 | constructor(kind, text) {
90 | this.kind = kind;
91 | this.text = text;
92 | }
93 |
94 | static bad(text) {
95 | return new DiffToken("bad", text);
96 | }
97 |
98 | static good(text) {
99 | return new DiffToken("good", text);
100 | }
101 |
102 | static missing(text) {
103 | return new DiffToken("missing", text);
104 | }
105 | }
106 |
107 | class DiffOutput {
108 | constructor(provided, expected) {
109 | this.provided = provided;
110 | this.expected = expected;
111 | }
112 | }
113 |
114 | function renderTokens(tokens) {
115 | return tokens
116 | .map((token) => {
117 | const text = withIsolatedLeadingMark(token.text);
118 | const encoded = escapeHtml(text);
119 | const className =
120 | token.kind === "good"
121 | ? "typeGood"
122 | : token.kind === "bad"
123 | ? "typeBad"
124 | : "typeMissed";
125 | return `${encoded}`;
126 | })
127 | .join("");
128 | }
129 |
130 | function escapeHtml(text) {
131 | return text
132 | .replace(/&/g, "&")
133 | .replace(//g, ">")
135 | .replace(/"/g, """)
136 | .replace(/'/g, "'");
137 | }
138 |
139 | function withIsolatedLeadingMark(text) {
140 | const firstChar = text.charAt(0);
141 |
142 | const category = getUnicodeCategory(firstChar);
143 | if (category.startsWith("M")) {
144 | // If the first character is a mark, prepend a non-breaking space (U+00A0)
145 | return "\u00A0" + text;
146 | }
147 |
148 | return text;
149 | }
150 |
151 | function getUnicodeCategory(char) {
152 | const code = char.codePointAt(0);
153 | const category = String.fromCodePoint(code).normalize('NFD').charAt(0);
154 | return category;
155 | }
156 |
157 | function normalizeToNFC(text) {
158 | return text.normalize('NFC');
159 | }
--------------------------------------------------------------------------------
/src/notes/Note.ts:
--------------------------------------------------------------------------------
1 | import "@logseq/libs";
2 | import {LazyAnkiNoteManager} from "../anki-connect/LazyAnkiNoteManager";
3 | import {HTMLFile} from "../logseq/LogseqToHtmlConverter";
4 | import {DependencyEntity} from "../logseq/getLogseqContentDirectDependencies";
5 | import _ from "lodash";
6 | import {LogseqProxy} from "../logseq/LogseqProxy";
7 | import {NoteUtils} from "./NoteUtils";
8 | import {getLogseqBlockPropSafe} from "../utils/utils";
9 |
10 | export abstract class Note {
11 | public uuid: string;
12 | public content: string;
13 | public format: string;
14 | public properties: any;
15 | public page: any;
16 | public type: string;
17 | public ankiId: number;
18 | public tagIds: number[];
19 | static ankiNoteManager: LazyAnkiNoteManager;
20 |
21 | public constructor(
22 | uuid: string,
23 | content: string,
24 | format: string,
25 | properties: any,
26 | page: any,
27 | refs: number[],
28 | ) {
29 | this.uuid = uuid;
30 | this.content = content;
31 | this.format = format;
32 | this.properties = properties;
33 | this.page = page;
34 | this.page.originalName =
35 | _.get(this, "page.originalName", null) || _.get(this, "page.name", null); // Just in case the page doesn't have an originalName
36 | this.tagIds = refs;
37 | }
38 |
39 | public static setAnkiNoteManager(ankiNoteManager: LazyAnkiNoteManager) {
40 | Note.ankiNoteManager = ankiNoteManager;
41 | }
42 |
43 | public abstract getClozedContentHTML(): Promiseuse-namespace-as-default-deck is enabled, this will be used as the default deck only when page is not in any namespace.",
56 | default: "Default",
57 | },
58 | {
59 | key: "logseqSideSettingsHeading",
60 | title: "🐾 Logseq Menu & Display",
61 | description: "",
62 | type: "heading",
63 | default: null,
64 | },
65 | {
66 | key: "renderClozeMarcosInLogseq",
67 | type: "boolean",
68 | default: false,
69 | title: "Render cloze macros in Logseq? (Recommended: Disabled) [Experimental] [In Development]",
70 | description:
71 | "When enabled, markdown used inside ({{c1 Hello}}, {{c2 World}}, ...) clozes will be rendered.",
72 | },
73 | {
74 | key: "hideClozeMarcosUntilHoverInLogseq",
75 | type: "boolean",
76 | default: false,
77 | title: "Hide cloze macros in Logseq? (Recommended: Disabled) [Experimental]",
78 | description:
79 | "When enabled, ({{c1 Hello}}, {{c2 World}}, ...) clozes will be hidden by default and displayed only on hover.",
80 | },
81 | {
82 | key: "addonsList",
83 | type: "enum",
84 | default: AddonRegistry.getAll().map((addon) => addon.getName()),
85 | title: "Addons:",
86 | enumChoices: AddonRegistry.getAll().map((addon) => addon.getName()),
87 | enumPicker: "checkbox",
88 | description:
89 | "Select the addons to use. They add / modify gui elements to enhance plugin capabilities inside Logseq."
90 | },
91 | {
92 | key: "advancedSettingsHeading",
93 | title: "🎓 Advanced Settings",
94 | description: "",
95 | type: "heading",
96 | default: null,
97 | },
98 | {
99 | key: "ankiFieldOptions",
100 | type: "enum",
101 | default: [],
102 | title: "Select different field options to apply to Anki cards? (Recommended: None)",
103 | description: "This option allows you to add different filters and additional stuff to the Anki card templates. " +
104 | "Takes effect only after next sync.",
105 | enumChoices: [
106 | "furigana",
107 | "kana",
108 | "kanji",
109 | "tts",
110 | "tags",
111 | "rtl"
112 | ],
113 | enumPicker: "checkbox",
114 | },
115 | {
116 | key: "cacheLogseqAPIv1",
117 | type: "boolean",
118 | default: true,
119 | title: "Enable caching Logseq API for improved syncing speed? (Recommended: Enabled) [Experimental]",
120 | description:
121 | "Enable active cache for Logseq API. When enabled, syncing will be faster but the plugin may use more memory.