mirror of
https://github.com/kdnk/obsidian-automatic-linker.git
synced 2026-07-22 12:00:30 +00:00
chore: use update-editor from obsidian-linter for more reliable editor updates
This commit is contained in:
parent
62bae4a818
commit
a2fe71c829
5 changed files with 170 additions and 91 deletions
12
README.md
12
README.md
|
|
@ -58,10 +58,10 @@ Automatic Linker Plugin automatically converts plain text file references into O
|
|||
|
||||
- **Replace Bare URLs with Page Titles:**
|
||||
Finds bare URLs (like `https://example.com`) in your notes, fetches the title of the web page, and replaces the URL with a Markdown link `[Page Title](URL)`.
|
||||
- Triggered manually via a command.
|
||||
- Fetched titles are cached to minimize network requests.
|
||||
- Ignores URLs already in Markdown links, angle brackets, or code blocks.
|
||||
- Supports an internal list of domains to ignore (not yet configurable via UI).
|
||||
- Triggered manually via a command.
|
||||
- Fetched titles are cached to minimize network requests.
|
||||
- Ignores URLs already in Markdown links, angle brackets, or code blocks.
|
||||
- Supports an internal list of domains to ignore (not yet configurable via UI).
|
||||
|
||||
## Usage with Obsidian Linter
|
||||
|
||||
|
|
@ -70,3 +70,7 @@ To use this plugin with Obsidian Linter, ensure that the "Lint on Save" option i
|
|||
1. Disable "Lint on Save" in Obsidian Linter.
|
||||
2. Enable "Format on Save" in Automatic Linker settings.
|
||||
3. Enable "Run Obsidian Linter before formatting" in Automatic Linker settings.
|
||||
|
||||
## Thanks
|
||||
|
||||
- `updateEditor` function comes from https://github.com/platers/obsidian-linter.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"author": "",
|
||||
"license": "Appache-2.0",
|
||||
"devDependencies": {
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/node": "^22.12.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
|
|
@ -31,7 +32,9 @@
|
|||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@types/async-lock": "^1.4.2",
|
||||
"async-lock": "^1.4.1"
|
||||
"async-lock": "^1.4.1",
|
||||
"diff-match-patch": "^1.0.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,22 @@ importers:
|
|||
|
||||
.:
|
||||
dependencies:
|
||||
'@codemirror/state':
|
||||
specifier: ^6.5.2
|
||||
version: 6.5.2
|
||||
'@types/async-lock':
|
||||
specifier: ^1.4.2
|
||||
version: 1.4.2
|
||||
async-lock:
|
||||
specifier: ^1.4.1
|
||||
version: 1.4.1
|
||||
diff-match-patch:
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5
|
||||
devDependencies:
|
||||
'@types/diff-match-patch':
|
||||
specifier: ^1.0.36
|
||||
version: 1.0.36
|
||||
'@types/node':
|
||||
specifier: ^22.12.0
|
||||
version: 22.15.17
|
||||
|
|
@ -925,6 +934,9 @@ packages:
|
|||
'@types/d3@7.4.3':
|
||||
resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
|
||||
|
||||
'@types/diff-match-patch@1.0.36':
|
||||
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||
|
||||
'@types/earcut@2.1.4':
|
||||
resolution: {integrity: sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==}
|
||||
|
||||
|
|
@ -1397,6 +1409,9 @@ packages:
|
|||
detect-node@2.1.0:
|
||||
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
|
||||
|
||||
diff-match-patch@1.0.5:
|
||||
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -3057,6 +3072,8 @@ snapshots:
|
|||
'@types/d3-transition': 3.0.9
|
||||
'@types/d3-zoom': 3.0.8
|
||||
|
||||
'@types/diff-match-patch@1.0.36': {}
|
||||
|
||||
'@types/earcut@2.1.4': {}
|
||||
|
||||
'@types/estree@1.0.7': {}
|
||||
|
|
@ -3578,6 +3595,8 @@ snapshots:
|
|||
detect-node@2.1.0:
|
||||
optional: true
|
||||
|
||||
diff-match-patch@1.0.5: {}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
|
|
|
|||
167
src/main.ts
167
src/main.ts
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
App,
|
||||
Editor,
|
||||
getFrontMatterInfo,
|
||||
MarkdownView,
|
||||
Notice,
|
||||
parseFrontMatterAliases,
|
||||
Plugin,
|
||||
|
|
@ -23,6 +25,7 @@ import {
|
|||
DEFAULT_SETTINGS,
|
||||
} from "./settings/settings-info";
|
||||
import { buildCandidateTrie, CandidateData, TrieNode } from "./trie";
|
||||
import { updateEditor } from "./update-editor";
|
||||
|
||||
const sleep = (ms: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
|
@ -41,6 +44,12 @@ export default class AutomaticLinkerPlugin extends Plugin {
|
|||
super(app, pluginManifest);
|
||||
}
|
||||
|
||||
private getEditor(): Editor | null {
|
||||
const activeLeaf = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!activeLeaf) return null;
|
||||
return activeLeaf.editor;
|
||||
}
|
||||
|
||||
async modifyLinks() {
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (!activeFile) {
|
||||
|
|
@ -54,85 +63,89 @@ export default class AutomaticLinkerPlugin extends Plugin {
|
|||
return;
|
||||
}
|
||||
|
||||
await this.app.vault.process(activeFile, (fileContent) => {
|
||||
if (this.settings.formatGitHubURLs) {
|
||||
fileContent = replaceURLs(
|
||||
fileContent,
|
||||
this.settings,
|
||||
formatGitHubURL,
|
||||
);
|
||||
}
|
||||
const editor = this.getEditor();
|
||||
if (!editor) return;
|
||||
|
||||
if (this.settings.formatJiraURLs) {
|
||||
fileContent = replaceURLs(
|
||||
fileContent,
|
||||
this.settings,
|
||||
formatJiraURL,
|
||||
);
|
||||
}
|
||||
let fileContent = editor.getValue();
|
||||
const oldText = fileContent;
|
||||
if (this.settings.formatGitHubURLs) {
|
||||
fileContent = replaceURLs(
|
||||
fileContent,
|
||||
this.settings,
|
||||
formatGitHubURL,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.settings.formatLinearURLs) {
|
||||
fileContent = replaceURLs(
|
||||
fileContent,
|
||||
this.settings,
|
||||
formatLinearURL,
|
||||
);
|
||||
}
|
||||
if (this.settings.formatJiraURLs) {
|
||||
fileContent = replaceURLs(
|
||||
fileContent,
|
||||
this.settings,
|
||||
formatJiraURL,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.settings.replaceUrlWithTitle) {
|
||||
const { contentStart } = getFrontMatterInfo(fileContent);
|
||||
const frontmatter = fileContent.slice(0, contentStart);
|
||||
const body = fileContent.slice(contentStart);
|
||||
const updatedBody = replaceUrlWithTitle({
|
||||
body,
|
||||
urlTitleMap: this.urlTitleMap,
|
||||
});
|
||||
fileContent = frontmatter + updatedBody;
|
||||
}
|
||||
|
||||
if (!this.trie || !this.candidateMap) {
|
||||
return fileContent;
|
||||
}
|
||||
|
||||
if (this.settings.debug) {
|
||||
console.log("this.trie: ", this.trie);
|
||||
console.log("this.candidateMap: ", this.candidateMap);
|
||||
console.log(new Date().toISOString(), "modifyLinks started");
|
||||
new Notice(
|
||||
`Automatic Linker: ${new Date().toISOString()} modifyLinks started.`,
|
||||
);
|
||||
}
|
||||
if (this.settings.formatLinearURLs) {
|
||||
fileContent = replaceURLs(
|
||||
fileContent,
|
||||
this.settings,
|
||||
formatLinearURL,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.settings.replaceUrlWithTitle) {
|
||||
const { contentStart } = getFrontMatterInfo(fileContent);
|
||||
const frontmatter = fileContent.slice(0, contentStart);
|
||||
const updatedBody = replaceLinks({
|
||||
body: fileContent.slice(contentStart),
|
||||
linkResolverContext: {
|
||||
filePath: activeFile.path.replace(/\.md$/, ""),
|
||||
trie: this.trie,
|
||||
candidateMap: this.candidateMap,
|
||||
},
|
||||
settings: {
|
||||
minCharCount: this.settings.minCharCount,
|
||||
namespaceResolution: this.settings.namespaceResolution,
|
||||
baseDir: this.settings.baseDir,
|
||||
ignoreDateFormats: this.settings.ignoreDateFormats,
|
||||
ignoreCase: this.settings.ignoreCase,
|
||||
preventSelfLinking: this.settings.preventSelfLinking,
|
||||
removeAliasInDirs: this.settings.removeAliasInDirs,
|
||||
},
|
||||
const body = fileContent.slice(contentStart);
|
||||
const updatedBody = replaceUrlWithTitle({
|
||||
body,
|
||||
urlTitleMap: this.urlTitleMap,
|
||||
});
|
||||
fileContent = frontmatter + updatedBody;
|
||||
}
|
||||
|
||||
if (this.settings.debug) {
|
||||
console.log(new Date().toISOString(), "modifyLinks finished");
|
||||
new Notice(
|
||||
`Automatic Linker: ${new Date().toISOString()} modifyLinks finished.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.trie || !this.candidateMap) {
|
||||
return fileContent;
|
||||
}
|
||||
|
||||
if (this.settings.debug) {
|
||||
console.log("this.trie: ", this.trie);
|
||||
console.log("this.candidateMap: ", this.candidateMap);
|
||||
console.log(new Date().toISOString(), "modifyLinks started");
|
||||
new Notice(
|
||||
`Automatic Linker: ${new Date().toISOString()} modifyLinks started.`,
|
||||
);
|
||||
}
|
||||
|
||||
const { contentStart } = getFrontMatterInfo(fileContent);
|
||||
const frontmatter = fileContent.slice(0, contentStart);
|
||||
const updatedBody = replaceLinks({
|
||||
body: fileContent.slice(contentStart),
|
||||
linkResolverContext: {
|
||||
filePath: activeFile.path.replace(/\.md$/, ""),
|
||||
trie: this.trie,
|
||||
candidateMap: this.candidateMap,
|
||||
},
|
||||
settings: {
|
||||
minCharCount: this.settings.minCharCount,
|
||||
namespaceResolution: this.settings.namespaceResolution,
|
||||
baseDir: this.settings.baseDir,
|
||||
ignoreDateFormats: this.settings.ignoreDateFormats,
|
||||
ignoreCase: this.settings.ignoreCase,
|
||||
preventSelfLinking: this.settings.preventSelfLinking,
|
||||
removeAliasInDirs: this.settings.removeAliasInDirs,
|
||||
},
|
||||
});
|
||||
fileContent = frontmatter + updatedBody;
|
||||
|
||||
if (this.settings.debug) {
|
||||
console.log(new Date().toISOString(), "modifyLinks finished");
|
||||
new Notice(
|
||||
`Automatic Linker: ${new Date().toISOString()} modifyLinks finished.`,
|
||||
);
|
||||
}
|
||||
|
||||
const newText = fileContent;
|
||||
updateEditor(oldText, newText, editor);
|
||||
}
|
||||
|
||||
async buildUrlTitleMap() {
|
||||
|
|
@ -379,24 +392,6 @@ export default class AutomaticLinkerPlugin extends Plugin {
|
|||
this.originalSaveCallback = saveCallback;
|
||||
}
|
||||
|
||||
const getUrls = async () => {
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (!activeFile) return;
|
||||
const fileContent = await this.app.vault.read(activeFile);
|
||||
const { contentStart } = getFrontMatterInfo(fileContent);
|
||||
const body = fileContent.slice(contentStart);
|
||||
|
||||
const urls = listupAllUrls(
|
||||
body,
|
||||
this.settings.replaceUrlWithTitleIgnoreDomains,
|
||||
);
|
||||
for (const url of urls) {
|
||||
const response = await request(url);
|
||||
const title = getTitleFromHtml(response);
|
||||
this.urlTitleMap.set(url, title);
|
||||
}
|
||||
};
|
||||
|
||||
saveCommandDefinition.checkCallback = async (checking: boolean) => {
|
||||
if (checking) {
|
||||
return saveCallback?.(checking);
|
||||
|
|
|
|||
58
src/update-editor.ts
Normal file
58
src/update-editor.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { ChangeSpec } from "@codemirror/state";
|
||||
import { Editor } from "obsidian";
|
||||
import DiffMatchPatch from "diff-match-patch";
|
||||
|
||||
function endOfDocument(doc: string) {
|
||||
const lines = doc.split("\n");
|
||||
return { line: lines.length - 1, ch: lines[lines.length - 1].length };
|
||||
}
|
||||
|
||||
export function updateEditor(
|
||||
oldText: string,
|
||||
newText: string,
|
||||
editor: Editor,
|
||||
): DiffMatchPatch.Diff[] {
|
||||
const dmp = new DiffMatchPatch.diff_match_patch(); // eslint-disable-line new-cap
|
||||
const changes = dmp.diff_main(oldText, newText);
|
||||
let curText = "";
|
||||
changes.forEach((change) => {
|
||||
const [type, value] = change;
|
||||
|
||||
if (type == DiffMatchPatch.DIFF_INSERT) {
|
||||
// use codemirror dispatch in order to bypass the filter on transactions that causes editor.replaceRange not to not work in Live Preview
|
||||
//@ts-expect-error
|
||||
editor.cm.dispatch({
|
||||
changes: [
|
||||
{
|
||||
from: editor.posToOffset(endOfDocument(curText)),
|
||||
insert: value,
|
||||
} as ChangeSpec,
|
||||
],
|
||||
filter: false,
|
||||
});
|
||||
curText += value;
|
||||
} else if (type == DiffMatchPatch.DIFF_DELETE) {
|
||||
const start = endOfDocument(curText);
|
||||
let tempText = curText;
|
||||
tempText += value;
|
||||
const end = endOfDocument(tempText);
|
||||
|
||||
// use codemirror dispatch in order to bypass the filter on transactions that causes editor.replaceRange not to not work in Live Preview
|
||||
//@ts-expect-error
|
||||
editor.cm.dispatch({
|
||||
changes: [
|
||||
{
|
||||
from: editor.posToOffset(start),
|
||||
to: editor.posToOffset(end),
|
||||
insert: "",
|
||||
} as ChangeSpec,
|
||||
],
|
||||
filter: false,
|
||||
});
|
||||
} else {
|
||||
curText += value;
|
||||
}
|
||||
});
|
||||
|
||||
return changes;
|
||||
}
|
||||
Loading…
Reference in a new issue