yanki 2.0.10 → 2.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +160 -160
- package/dist/lib/index.d.ts +8 -6
- package/dist/lib/index.js +120 -58
- package/dist/standalone/index.d.ts +88 -89
- package/dist/standalone/index.js +91 -91
- package/package.json +17 -17
package/dist/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { YankiConnectOptions, YankiFetchAdapter, YankiParamsForAction } from "yanki-connect";
|
|
2
2
|
import { PartialDeep, Simplify } from "type-fest";
|
|
3
|
-
|
|
4
3
|
//#region src/lib/model/model.d.ts
|
|
5
4
|
declare const yankiModels: [{
|
|
6
5
|
readonly cardTemplates: [{
|
|
@@ -109,8 +108,10 @@ type GlobalOptions = {
|
|
|
109
108
|
* user-specified file names in other cases.
|
|
110
109
|
*/
|
|
111
110
|
maxFilenameLength: number;
|
|
112
|
-
namespace: string;
|
|
113
|
-
|
|
111
|
+
namespace: string;
|
|
112
|
+
/** Ensures that wiki-style links work correctly */
|
|
113
|
+
obsidianVault: string | undefined;
|
|
114
|
+
/** Exposed for testing only */
|
|
114
115
|
resolveUrls: boolean;
|
|
115
116
|
/**
|
|
116
117
|
* Whether to treat single newlines in Markdown as line breaks in the
|
|
@@ -122,7 +123,8 @@ type GlobalOptions = {
|
|
|
122
123
|
* be equivalent, don't match local notes with "orphaned" remote notes based
|
|
123
124
|
* on content
|
|
124
125
|
*/
|
|
125
|
-
strictMatching: boolean;
|
|
126
|
+
strictMatching: boolean;
|
|
127
|
+
/** Sync image, video, and audio assets to Anki's media storage system */
|
|
126
128
|
syncMediaAssets: SyncMediaAssets;
|
|
127
129
|
};
|
|
128
130
|
//#endregion
|
|
@@ -171,12 +173,12 @@ type LocalNote = {
|
|
|
171
173
|
};
|
|
172
174
|
//#endregion
|
|
173
175
|
//#region src/lib/actions/rename.d.ts
|
|
174
|
-
type RenameNotesOptions = Pick<GlobalOptions, 'dryRun' | 'fileAdapter' | 'manageFilenames' | 'maxFilenameLength'
|
|
176
|
+
type RenameNotesOptions = Pick<GlobalOptions, 'dryRun' | 'fileAdapter' | 'manageFilenames' | 'maxFilenameLength' |
|
|
175
177
|
/**
|
|
176
178
|
* Included because this can technically change the content of the "first
|
|
177
179
|
* line" of a card
|
|
178
180
|
*/
|
|
179
|
-
|
|
181
|
+
'strictLineBreaks'>;
|
|
180
182
|
type RenameFilesResult = {
|
|
181
183
|
dryRun: boolean;
|
|
182
184
|
notes: LocalNote[];
|
package/dist/lib/index.js
CHANGED
|
@@ -110,7 +110,7 @@ const NEWLINE_REGEX$1 = /* @__PURE__ */ new RegExp("\\r?\\n", "v");
|
|
|
110
110
|
const LEADING_WHITESPACE_REGEX = /* @__PURE__ */ new RegExp("^(\\s+)", "v");
|
|
111
111
|
function trimLeadingIndentation(strings, ...values) {
|
|
112
112
|
const lines = strings.reduce((result, text, i) => `${result}${text}${String(values[i] ?? "")}`, "").split(NEWLINE_REGEX$1).filter((line) => line.trim() !== "");
|
|
113
|
-
const leadingSpace = LEADING_WHITESPACE_REGEX.exec(lines[0])?.[0] ?? "";
|
|
113
|
+
const leadingSpace = LEADING_WHITESPACE_REGEX.exec(lines[0] ?? "")?.[0] ?? "";
|
|
114
114
|
const leadingSpaceRegex = new RegExp(`^${leadingSpace}`, "v");
|
|
115
115
|
return lines.map((line) => line.replace(leadingSpaceRegex, "").trimEnd()).join("\n");
|
|
116
116
|
}
|
|
@@ -502,14 +502,14 @@ function getFileExtensionForMimeType(mimeType) {
|
|
|
502
502
|
"video/x-matroska": "mkv",
|
|
503
503
|
"video/x-msvideo": "avi"
|
|
504
504
|
};
|
|
505
|
-
const normalizedMimeType = mimeType.split(";", 1)[0].trim().toLowerCase();
|
|
505
|
+
const normalizedMimeType = (mimeType.split(";", 1)[0] ?? "").trim().toLowerCase();
|
|
506
506
|
if (normalizedMimeType === "") return;
|
|
507
507
|
return mimeToExtension[normalizedMimeType];
|
|
508
508
|
}
|
|
509
509
|
//#endregion
|
|
510
510
|
//#region src/lib/utilities/path.ts
|
|
511
511
|
const WINDOWS_DRIVE_LETTER_REGEX = /* @__PURE__ */ new RegExp("^[A-Z]:", "iv");
|
|
512
|
-
const QUERY_FRAGMENT_START_REGEX = /* @__PURE__ */ new RegExp("[
|
|
512
|
+
const QUERY_FRAGMENT_START_REGEX = /* @__PURE__ */ new RegExp("[#^]", "v");
|
|
513
513
|
/**
|
|
514
514
|
* The browserify polyfill doesn't implement win32 absolute path detection...
|
|
515
515
|
*
|
|
@@ -575,26 +575,37 @@ function getBaseAndQueryParts(filePath) {
|
|
|
575
575
|
const [base, query] = splitAtFirstMatch(path.basename(filePath), QUERY_FRAGMENT_START_REGEX);
|
|
576
576
|
return [path.join(directoryPath, base), query];
|
|
577
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* Get every plausible base and anchor interpretation of a file path whose name
|
|
580
|
+
* may contain anchor delimiter characters (`#`, `^`), ordered from the longest
|
|
581
|
+
* literal file name (no anchor at all) to the shortest (anchor starts at the
|
|
582
|
+
* first delimiter in the file name, matching Obsidian's anchor syntax).
|
|
583
|
+
* Delimiters in directory names are never treated as anchor starts.
|
|
584
|
+
*
|
|
585
|
+
* File names may legitimately contain these characters, so callers should try
|
|
586
|
+
* candidates in order against a list of real files instead of assuming the
|
|
587
|
+
* first delimiter starts an anchor.
|
|
588
|
+
* https://github.com/kitschpatrol/yanki/issues/20
|
|
589
|
+
*/
|
|
590
|
+
function getBaseAndQueryCandidates(filePath) {
|
|
591
|
+
const directoryPath = path.dirname(filePath);
|
|
592
|
+
const fileName = path.basename(filePath);
|
|
593
|
+
const candidates = [{
|
|
594
|
+
base: path.join(directoryPath, fileName),
|
|
595
|
+
query: void 0
|
|
596
|
+
}];
|
|
597
|
+
for (let index = fileName.length - 1; index >= 0; index--) if (QUERY_FRAGMENT_START_REGEX.test(fileName.charAt(index))) candidates.push({
|
|
598
|
+
base: path.join(directoryPath, fileName.slice(0, index)),
|
|
599
|
+
query: fileName.slice(index)
|
|
600
|
+
});
|
|
601
|
+
return candidates;
|
|
602
|
+
}
|
|
578
603
|
function getBase(filePath) {
|
|
579
604
|
return getBaseAndQueryParts(filePath)[0];
|
|
580
605
|
}
|
|
581
606
|
function getQuery(filePath) {
|
|
582
607
|
return getBaseAndQueryParts(filePath).at(1) ?? "";
|
|
583
608
|
}
|
|
584
|
-
function hasExtension(filePath) {
|
|
585
|
-
return getExtension(filePath) !== "";
|
|
586
|
-
}
|
|
587
|
-
function getExtension(filePath) {
|
|
588
|
-
return path.extname(getBase(filePath));
|
|
589
|
-
}
|
|
590
|
-
function addExtensionIfMissing(filePath, extension) {
|
|
591
|
-
if (hasExtension(filePath)) return filePath;
|
|
592
|
-
return addExtension(filePath, extension);
|
|
593
|
-
}
|
|
594
|
-
function addExtension(filePath, extension) {
|
|
595
|
-
const [base, query] = getBaseAndQueryParts(filePath);
|
|
596
|
-
return `${base}.${extension}${query ?? ""}`;
|
|
597
|
-
}
|
|
598
609
|
//#endregion
|
|
599
610
|
//#region src/lib/utilities/url.ts
|
|
600
611
|
const DRIVE_LETTER_REGEX = /* @__PURE__ */ new RegExp("^[a-z]:", "iv");
|
|
@@ -718,11 +729,19 @@ async function getUrlContentHash(url, fetchAdapter, mode = MEDIA_DEFAULT_HASH_MO
|
|
|
718
729
|
} catch {}
|
|
719
730
|
return getHash(url, 16);
|
|
720
731
|
}
|
|
732
|
+
const DEFAULT_PORT_BY_PROTOCOL = {
|
|
733
|
+
"ftp:": 21,
|
|
734
|
+
"http:": 80,
|
|
735
|
+
"https:": 443,
|
|
736
|
+
"ws:": 80,
|
|
737
|
+
"wss:": 443
|
|
738
|
+
};
|
|
721
739
|
function urlToHostAndPort(url) {
|
|
722
740
|
const parsedUrl = safeParseUrl(url);
|
|
723
|
-
|
|
741
|
+
if (parsedUrl === void 0) return;
|
|
742
|
+
return {
|
|
724
743
|
host: `${parsedUrl.protocol}//${parsedUrl.hostname}`,
|
|
725
|
-
port: Number
|
|
744
|
+
port: parsedUrl.port === "" ? DEFAULT_PORT_BY_PROTOCOL[parsedUrl.protocol] ?? NaN : Number(parsedUrl.port)
|
|
726
745
|
};
|
|
727
746
|
}
|
|
728
747
|
function hostAndPortToUrl(host, port) {
|
|
@@ -786,7 +805,7 @@ const plugin$3 = function() {
|
|
|
786
805
|
visit(tree, (node, index, parent) => {
|
|
787
806
|
if (parent === void 0 || index === void 0 || node.type !== "element") return CONTINUE;
|
|
788
807
|
const [preChild] = node.children;
|
|
789
|
-
if (node.tagName === "pre" && node.children.length === 1 && preChild
|
|
808
|
+
if (node.tagName === "pre" && node.children.length === 1 && preChild?.type === "element" && preChild.tagName === "code" && Array.isArray(preChild.properties.className) && preChild.properties.className.includes("language-math")) {
|
|
790
809
|
fenced = true;
|
|
791
810
|
parent.children[index] = preChild;
|
|
792
811
|
}
|
|
@@ -807,6 +826,7 @@ const plugin$3 = function() {
|
|
|
807
826
|
}
|
|
808
827
|
];
|
|
809
828
|
}
|
|
829
|
+
return CONTINUE;
|
|
810
830
|
});
|
|
811
831
|
};
|
|
812
832
|
};
|
|
@@ -943,15 +963,17 @@ async function mdastToHtml(mdast, options) {
|
|
|
943
963
|
tagName: "span"
|
|
944
964
|
}, [u("text", `[sound:${finalSrc}]`)]);
|
|
945
965
|
});
|
|
966
|
+
return CONTINUE;
|
|
946
967
|
});
|
|
947
968
|
for (const mutationPromise of treeMutationPromises) await mutationPromise();
|
|
948
969
|
visit(hastWithClass, "element", (node, index, parent) => {
|
|
949
970
|
if (parent === void 0 || index === void 0 || node.tagName !== "img" || emptyIsUndefined(String(node.properties.alt)) === void 0) return CONTINUE;
|
|
950
|
-
const { alt, height, width } = parseDimensionsFromAltText(
|
|
971
|
+
const { alt, height, width } = parseDimensionsFromAltText(node.properties.alt ?? "");
|
|
951
972
|
if (alt === void 0) delete node.properties.alt;
|
|
952
973
|
else node.properties.alt = alt;
|
|
953
974
|
if (height !== void 0) node.properties.height = height;
|
|
954
975
|
if (width !== void 0) node.properties.width = width;
|
|
976
|
+
return CONTINUE;
|
|
955
977
|
});
|
|
956
978
|
const isEmpty = isVisuallyEmpty(hastWithClass);
|
|
957
979
|
if (isEmpty && !useEmptyPlaceholder) return "";
|
|
@@ -1034,7 +1056,7 @@ function parseDimensions(dimensions) {
|
|
|
1034
1056
|
function isVisuallyEmpty(tree) {
|
|
1035
1057
|
let hasVisualContent = false;
|
|
1036
1058
|
visit(tree, (node) => {
|
|
1037
|
-
if (hasVisualContent) return;
|
|
1059
|
+
if (hasVisualContent) return EXIT;
|
|
1038
1060
|
if (node.type === "element") {
|
|
1039
1061
|
const element = node;
|
|
1040
1062
|
const { tagName } = element;
|
|
@@ -1061,6 +1083,7 @@ function isVisuallyEmpty(tree) {
|
|
|
1061
1083
|
hasVisualContent = true;
|
|
1062
1084
|
return EXIT;
|
|
1063
1085
|
}
|
|
1086
|
+
return CONTINUE;
|
|
1064
1087
|
});
|
|
1065
1088
|
return !hasVisualContent;
|
|
1066
1089
|
}
|
|
@@ -1304,6 +1327,9 @@ function areTagsEqual(localTags, remoteTags) {
|
|
|
1304
1327
|
async function getRemoteNotes(client, namespace = defaultGlobalOptions.namespace) {
|
|
1305
1328
|
return (await getRemoteNotesById(client, await client.note.findNotes({ query: `"YankiNamespace:${namespace}"` }))).filter((note) => note !== void 0);
|
|
1306
1329
|
}
|
|
1330
|
+
function getFieldValue(field) {
|
|
1331
|
+
return field?.value ?? "";
|
|
1332
|
+
}
|
|
1307
1333
|
/**
|
|
1308
1334
|
* Get all data from Anki required to populate the YankiNote type.
|
|
1309
1335
|
*
|
|
@@ -1355,10 +1381,10 @@ async function getRemoteNotesById(client, noteIds) {
|
|
|
1355
1381
|
cards: ankiNote.cards,
|
|
1356
1382
|
deckName,
|
|
1357
1383
|
fields: {
|
|
1358
|
-
Back: ankiNote.fields.Back
|
|
1359
|
-
...ankiNote.fields.Extra !== void 0 && { Extra: ankiNote.fields.Extra
|
|
1360
|
-
Front: ankiNote.fields.Front
|
|
1361
|
-
YankiNamespace: ankiNote.fields.YankiNamespace
|
|
1384
|
+
Back: getFieldValue(ankiNote.fields.Back),
|
|
1385
|
+
...ankiNote.fields.Extra !== void 0 && { Extra: getFieldValue(ankiNote.fields.Extra) },
|
|
1386
|
+
Front: getFieldValue(ankiNote.fields.Front),
|
|
1387
|
+
YankiNamespace: getFieldValue(ankiNote.fields.YankiNamespace)
|
|
1362
1388
|
},
|
|
1363
1389
|
modelName: ankiNote.modelName,
|
|
1364
1390
|
noteId: ankiNote.noteId,
|
|
@@ -1777,7 +1803,9 @@ const defaultResolveLinkOptions = {
|
|
|
1777
1803
|
* @returns Resolved absolute path or URL One of:
|
|
1778
1804
|
*
|
|
1779
1805
|
* - Resolved absolute POSIX-style paths
|
|
1780
|
-
* - Removes any
|
|
1806
|
+
* - Removes any Obsidian-style heading and block anchors, unless the anchor
|
|
1807
|
+
* characters are a literal part of a matched file's name (`?` is always
|
|
1808
|
+
* treated as a literal file name character in local paths)
|
|
1781
1809
|
* - Not URI-encoded
|
|
1782
1810
|
* - Retains original case
|
|
1783
1811
|
* - HTTP protocol URL
|
|
@@ -1792,8 +1820,7 @@ function resolveLink(filePathOrUrl, options) {
|
|
|
1792
1820
|
const decodedUrl = safeDecodeURI(currentPathOrUrl) ?? currentPathOrUrl;
|
|
1793
1821
|
const sourceType = getSrcType(decodedUrl);
|
|
1794
1822
|
if (sourceType === "localFileName") {
|
|
1795
|
-
|
|
1796
|
-
resolvedUrl = resolveNameLink(resolvedUrl, cwd, allFilePaths ?? []) ?? resolveWithBasePath(decodedUrl, {
|
|
1823
|
+
const resolvedUrl = resolveNameLink(normalize(decodedUrl), cwd, allFilePaths ?? []) ?? resolveWithBasePath(decodedUrl, {
|
|
1797
1824
|
basePath,
|
|
1798
1825
|
cwd
|
|
1799
1826
|
});
|
|
@@ -1841,13 +1868,21 @@ function resolveLocalFilePath(decodedUrl, options) {
|
|
|
1841
1868
|
basePath,
|
|
1842
1869
|
cwd
|
|
1843
1870
|
});
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1871
|
+
let matchedPath;
|
|
1872
|
+
let matchedQuery;
|
|
1873
|
+
for (const { base, query } of getBaseAndQueryCandidates(resolvedUrl)) {
|
|
1874
|
+
matchedPath = (path.extname(base) === "" ? [`${base}.md`] : [base, `${base}.md`]).find((candidate) => pathExistsInAllFiles(candidate, allFilePaths ?? []));
|
|
1875
|
+
if (matchedPath !== void 0) {
|
|
1876
|
+
matchedQuery = query;
|
|
1877
|
+
break;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
if (matchedPath !== void 0) {
|
|
1881
|
+
if (convertFilePathsToProtocol !== "none" && (type === "link" || type === "embed" && [".md", ".pdf"].includes(path.extname(matchedPath)))) {
|
|
1882
|
+
if (convertFilePathsToProtocol === "obsidian" && obsidianVaultName !== void 0) return createObsidianVaultLink(`${matchedPath}${matchedQuery ?? ""}`, basePath ?? "", obsidianVaultName);
|
|
1883
|
+
if (convertFilePathsToProtocol === "file") return createFileLink(`${matchedPath}${matchedQuery ?? ""}`);
|
|
1849
1884
|
}
|
|
1850
|
-
return
|
|
1885
|
+
return matchedPath;
|
|
1851
1886
|
}
|
|
1852
1887
|
return getBase(resolvedUrl);
|
|
1853
1888
|
}
|
|
@@ -1860,9 +1895,10 @@ function resolveLocalFilePath(decodedUrl, options) {
|
|
|
1860
1895
|
* Obsidian seems to treat note links slightly differently from image / asset
|
|
1861
1896
|
* links.
|
|
1862
1897
|
*
|
|
1863
|
-
* @param name Non-URI-encoded name of the file,
|
|
1864
|
-
*
|
|
1865
|
-
*
|
|
1898
|
+
* @param name Non-URI-encoded, normalized name of the file, with or without a
|
|
1899
|
+
* file extension. Extension-less names are assumed to be .md files. May
|
|
1900
|
+
* include an anchor suffix, or anchor delimiter characters that are a literal
|
|
1901
|
+
* part of the file name. (POSIX-style paths.)
|
|
1866
1902
|
* @param cwd Absolute path to the current working directory of the file from
|
|
1867
1903
|
* which we're resolving the link. (POSIX-style paths)
|
|
1868
1904
|
* @param allFilePaths Array of absolute paths to all other files in the paths
|
|
@@ -1873,14 +1909,34 @@ function resolveLocalFilePath(decodedUrl, options) {
|
|
|
1873
1909
|
*/
|
|
1874
1910
|
function resolveNameLink(name, cwd, allFilePaths) {
|
|
1875
1911
|
if (allFilePaths.length === 0) return;
|
|
1876
|
-
const
|
|
1912
|
+
for (const { base, query } of getBaseAndQueryCandidates(name)) {
|
|
1913
|
+
const match = findBestNameMatch(path.extname(base) === "" ? `${base}.md` : base, cwd, allFilePaths);
|
|
1914
|
+
if (match !== void 0) return `${match}${query ?? ""}`;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Find the best matching file for a name in the list of all file paths.
|
|
1919
|
+
* Extracted from `resolveNameLink` so each base and anchor interpretation of
|
|
1920
|
+
* the name can be attempted in turn.
|
|
1921
|
+
*
|
|
1922
|
+
* @param base Non-URI-encoded name of the file with a file extension and no
|
|
1923
|
+
* anchor suffix. (POSIX-style paths.)
|
|
1924
|
+
* @param cwd Absolute path to the current working directory of the file from
|
|
1925
|
+
* which we're resolving the link. (POSIX-style paths)
|
|
1926
|
+
* @param allFilePaths Array of absolute paths to all other files in the paths
|
|
1927
|
+
* to be considered. (POSIX-style paths.)
|
|
1928
|
+
*
|
|
1929
|
+
* @returns Absolute path to the best matching file, or undefined if there's no
|
|
1930
|
+
* valid match. (POSIX-style paths.)
|
|
1931
|
+
*/
|
|
1932
|
+
function findBestNameMatch(base, cwd, allFilePaths) {
|
|
1877
1933
|
const baseWithoutMd = base.replace(MD_EXTENSION_REGEX, "").toLowerCase();
|
|
1878
1934
|
const pathsToName = allFilePaths.filter((filePath) => {
|
|
1879
1935
|
return filePath.replace(MD_EXTENSION_REGEX, "").toLowerCase().endsWith(baseWithoutMd);
|
|
1880
1936
|
});
|
|
1881
1937
|
if (pathsToName.length === 0) return;
|
|
1882
|
-
if (pathsToName.length === 1) return
|
|
1883
|
-
return
|
|
1938
|
+
if (pathsToName.length === 1) return pathsToName[0];
|
|
1939
|
+
return pathsToName.toSorted((a, b) => {
|
|
1884
1940
|
if (!base.endsWith(".md") || base.includes(path.sep)) {
|
|
1885
1941
|
const aHasCwd = a.startsWith(cwd);
|
|
1886
1942
|
if (aHasCwd !== b.startsWith(cwd)) return aHasCwd ? -1 : 1;
|
|
@@ -1889,22 +1945,22 @@ function resolveNameLink(name, cwd, allFilePaths) {
|
|
|
1889
1945
|
const bDepth = b.split(path.sep).length;
|
|
1890
1946
|
if (aDepth !== bDepth) return aDepth - bDepth;
|
|
1891
1947
|
return a.localeCompare(b);
|
|
1892
|
-
})[0]
|
|
1948
|
+
})[0];
|
|
1893
1949
|
}
|
|
1894
1950
|
/**
|
|
1895
|
-
* Check for presence of a path in a list in a case-
|
|
1896
|
-
*
|
|
1951
|
+
* Check for presence of a path in a list in a case-agnostic manner, since
|
|
1952
|
+
* Obsidian is not case sensitive.
|
|
1897
1953
|
*
|
|
1898
|
-
* @param filePath
|
|
1954
|
+
* @param filePath Literal file path with file extension and no anchor suffix.
|
|
1955
|
+
* (POSIX-style path.)
|
|
1899
1956
|
* @param allFilePaths Array of absolute file paths to check. (POSIX-style
|
|
1900
1957
|
* paths.)
|
|
1901
1958
|
*
|
|
1902
|
-
* @returns
|
|
1903
|
-
* undefined if it is not.
|
|
1959
|
+
* @returns Whether the file path is present in the list of all file paths.
|
|
1904
1960
|
*/
|
|
1905
1961
|
function pathExistsInAllFiles(filePath, allFilePaths) {
|
|
1906
|
-
const
|
|
1907
|
-
|
|
1962
|
+
const filePathLowerCase = filePath.toLowerCase();
|
|
1963
|
+
return allFilePaths.some((file) => file.toLowerCase().endsWith(filePathLowerCase));
|
|
1908
1964
|
}
|
|
1909
1965
|
function createFileLink(absolutePath) {
|
|
1910
1966
|
return `file://${absolutePath}`;
|
|
@@ -1933,7 +1989,7 @@ const plugin$1 = function(options) {
|
|
|
1933
1989
|
obsidianVaultName: obsidianVault,
|
|
1934
1990
|
type: "link"
|
|
1935
1991
|
});
|
|
1936
|
-
node.url = isUrl(resolvedLink) ? resolvedLink : encodeURI(resolvedLink);
|
|
1992
|
+
node.url = isUrl(resolvedLink) ? resolvedLink : encodeURI(resolvedLink).replaceAll("?", "%3F").replaceAll("#", "%23");
|
|
1937
1993
|
});
|
|
1938
1994
|
visit(tree, "image", (node) => {
|
|
1939
1995
|
node.data ??= {};
|
|
@@ -2323,7 +2379,7 @@ function replaceDeleteNodesWithClozeMarkup(ast) {
|
|
|
2323
2379
|
visit(ast, "delete", (node, index, parent) => {
|
|
2324
2380
|
if (parent === void 0 || index === void 0 || !("children" in node) || node.children.length === 0) return CONTINUE;
|
|
2325
2381
|
const [firstChild] = node.children;
|
|
2326
|
-
if (
|
|
2382
|
+
if (firstChild !== void 0 && isText(firstChild)) {
|
|
2327
2383
|
const result = CLOZE_NUMBER_PREFIX_REGEX.exec(firstChild.value);
|
|
2328
2384
|
if (result !== null && (node.children.length > 1 || (result.at(2) ?? "").length > 0)) {
|
|
2329
2385
|
const possibleClozeIndex = Number(result.at(1));
|
|
@@ -2347,6 +2403,7 @@ function replaceDeleteNodesWithClozeMarkup(ast) {
|
|
|
2347
2403
|
];
|
|
2348
2404
|
parent.children.splice(index, 1, ...clozeNodes);
|
|
2349
2405
|
clozeIndex += 1;
|
|
2406
|
+
return CONTINUE;
|
|
2350
2407
|
});
|
|
2351
2408
|
return ast;
|
|
2352
2409
|
}
|
|
@@ -2379,6 +2436,7 @@ function getYankiModelNameFromTree(ast) {
|
|
|
2379
2436
|
probableType = `Yanki - Cloze`;
|
|
2380
2437
|
return EXIT;
|
|
2381
2438
|
}
|
|
2439
|
+
return CONTINUE;
|
|
2382
2440
|
});
|
|
2383
2441
|
if (probableType !== void 0) return probableType;
|
|
2384
2442
|
if (!hasThematicBreak(ast) && isLastVisibleNodeEmphasisWithOthers(ast)) return `Yanki - Basic (type in the answer)`;
|
|
@@ -2393,6 +2451,7 @@ function getYankiModelNameFromTree(ast) {
|
|
|
2393
2451
|
}
|
|
2394
2452
|
}
|
|
2395
2453
|
lastNode = node;
|
|
2454
|
+
return CONTINUE;
|
|
2396
2455
|
});
|
|
2397
2456
|
return probableType ?? `Yanki - Basic`;
|
|
2398
2457
|
}
|
|
@@ -2428,6 +2487,7 @@ function isLastVisibleNodeEmphasisWithOthers(ast) {
|
|
|
2428
2487
|
visibleCount++;
|
|
2429
2488
|
return SKIP;
|
|
2430
2489
|
}
|
|
2490
|
+
return CONTINUE;
|
|
2431
2491
|
});
|
|
2432
2492
|
return lastVisibleNode?.type === "emphasis" && visibleCount > 1;
|
|
2433
2493
|
}
|
|
@@ -2440,6 +2500,7 @@ function removeLastEmphasis(ast) {
|
|
|
2440
2500
|
lastEmphasisNode = node;
|
|
2441
2501
|
lastEmphasisParent = parent;
|
|
2442
2502
|
lastEmphasisIndex = index;
|
|
2503
|
+
return CONTINUE;
|
|
2443
2504
|
});
|
|
2444
2505
|
if (lastEmphasisParent && lastEmphasisNode && typeof lastEmphasisIndex === "number") {
|
|
2445
2506
|
lastEmphasisParent.children.splice(lastEmphasisIndex, 1);
|
|
@@ -2644,7 +2705,7 @@ async function loadLocalNotes(allLocalFilePaths, options) {
|
|
|
2644
2705
|
strictLineBreaks,
|
|
2645
2706
|
syncMediaAssets
|
|
2646
2707
|
});
|
|
2647
|
-
if (note.deckName === "") note.deckName = deckNamesFromFilePaths[index];
|
|
2708
|
+
if (note.deckName === "") note.deckName = deckNamesFromFilePaths[index] ?? "";
|
|
2648
2709
|
return {
|
|
2649
2710
|
filePath,
|
|
2650
2711
|
filePathOriginal: filePath,
|
|
@@ -2771,9 +2832,10 @@ async function getStyle(options) {
|
|
|
2771
2832
|
const css = await getModelStyle(client, modelName);
|
|
2772
2833
|
cssSet.add(css);
|
|
2773
2834
|
}
|
|
2774
|
-
|
|
2835
|
+
const [firstCss] = cssSet;
|
|
2836
|
+
if (firstCss === void 0) throw new Error("No CSS found in any Yanki model.");
|
|
2775
2837
|
if (cssSet.size > 1) throw new Error("Expected all Yanki models to have identical CSS.");
|
|
2776
|
-
return
|
|
2838
|
+
return firstCss;
|
|
2777
2839
|
}
|
|
2778
2840
|
async function setStyle(options) {
|
|
2779
2841
|
const startTime = performance.now();
|
|
@@ -2849,7 +2911,7 @@ async function setNoteIdInFrontmatter(markdown, noteId) {
|
|
|
2849
2911
|
delete parsedFrontmatter.noteId;
|
|
2850
2912
|
if (Object.keys(parsedFrontmatter).length === 0) {
|
|
2851
2913
|
const markdownWithoutFrontmatter = lines.slice(frontmatterEnd + 1);
|
|
2852
|
-
if (markdownWithoutFrontmatter[0]
|
|
2914
|
+
if (markdownWithoutFrontmatter[0]?.trim() === "") return markdownWithoutFrontmatter.slice(1).join("\n");
|
|
2853
2915
|
return markdownWithoutFrontmatter.join("\n");
|
|
2854
2916
|
}
|
|
2855
2917
|
} else parsedFrontmatter.noteId = noteId;
|
|
@@ -2862,7 +2924,7 @@ async function setNoteIdInFrontmatter(markdown, noteId) {
|
|
|
2862
2924
|
}
|
|
2863
2925
|
function getFrontmatterRange(markdown) {
|
|
2864
2926
|
const lines = markdown.split(NEWLINE_REGEX);
|
|
2865
|
-
if (!lines.join("").
|
|
2927
|
+
if (!lines.join("").trimStart().startsWith("---")) return [void 0, void 0];
|
|
2866
2928
|
const frontmatterStart = lines.findIndex((line) => line.startsWith("---"));
|
|
2867
2929
|
const frontmatterEnd = lines.findIndex((line, index) => index > frontmatterStart && line.startsWith("---"));
|
|
2868
2930
|
if (frontmatterStart === -1 || frontmatterEnd === -1) return [void 0, void 0];
|
|
@@ -3035,7 +3097,7 @@ async function syncFiles(allLocalFilePaths, options) {
|
|
|
3035
3097
|
manageFilenames,
|
|
3036
3098
|
maxFilenameLength
|
|
3037
3099
|
});
|
|
3038
|
-
if (obsidianVault !== void 0) {
|
|
3100
|
+
if (obsidianVault !== void 0 && !dryRun) {
|
|
3039
3101
|
if (renamedLocalNotes.some((renamedNote) => renamedNote.filePath !== renamedNote.filePathOriginal)) {
|
|
3040
3102
|
const reloadedLocalNotes = await loadLocalNotes(renamedLocalNotes.map((note) => note.filePath), {
|
|
3041
3103
|
allFilePaths: allFilePaths.map((filePath) => {
|
|
@@ -3099,7 +3161,7 @@ function formatSyncFilesResult(result, verbose = false) {
|
|
|
3099
3161
|
}, {});
|
|
3100
3162
|
const totalSynced = synced.filter((note) => note.action !== "deleted").length;
|
|
3101
3163
|
const totalRenamed = synced.filter((note) => note.filePath !== note.filePathOriginal).length;
|
|
3102
|
-
const ankiUnreachable = actionCounts.ankiUnreachable > 0;
|
|
3164
|
+
const ankiUnreachable = (actionCounts.ankiUnreachable ?? 0) > 0;
|
|
3103
3165
|
lines.push(`${result.dryRun ? "Will sync" : ankiUnreachable ? "Failed to sync" : "Successfully synced"} ${totalSynced} ${plur("note", totalSynced)} to Anki${result.dryRun ? "" : ` in ${prettyMilliseconds(result.duration)}`}.`);
|
|
3104
3166
|
if (verbose) {
|
|
3105
3167
|
lines.push("", result.dryRun ? "Sync Plan Summary:" : "Sync Summary:");
|