mirror of
https://github.com/esm7/obsidian-vimrc-support.git
synced 2026-07-22 05:00:25 +00:00
Compare commits
16 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ff5d97afc | ||
|
|
ba2c8243c1 | ||
|
|
b3beeae815 | ||
|
|
10c5a580f9 | ||
|
|
774de34205 | ||
|
|
923de9fe8a | ||
|
|
2fe5ee588c | ||
|
|
2f18ae8b56 | ||
|
|
3d3bf60c88 | ||
|
|
e800087a72 | ||
|
|
1dd8077282 | ||
|
|
340931c975 | ||
|
|
4aa4ca31f1 | ||
|
|
47de579471 | ||
|
|
378753b0b4 | ||
|
|
d376bfd048 |
9 changed files with 3373 additions and 27 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
# npm
|
||||
node_modules
|
||||
package-lock.json
|
||||
|
||||
# build
|
||||
main.js
|
||||
|
|
|
|||
19
.vscode/settings.json
vendored
Normal file
19
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"exmap",
|
||||
"iunmap",
|
||||
"jscommand",
|
||||
"jsfile",
|
||||
"Keybinds",
|
||||
"keychord",
|
||||
"keymap",
|
||||
"nmap",
|
||||
"noremap",
|
||||
"nunmap",
|
||||
"obcommand",
|
||||
"pasteinto",
|
||||
"sendkeys",
|
||||
"unnamedplus",
|
||||
"vunmap"
|
||||
]
|
||||
}
|
||||
26
README.md
26
README.md
|
|
@ -50,6 +50,9 @@ exmap back obcommand app:go-back
|
|||
nmap <C-o> :back<CR>
|
||||
exmap forward obcommand app:go-forward
|
||||
nmap <C-i> :forward<CR>
|
||||
|
||||
" Also works
|
||||
nmap <C-w>h :obcommand<space>workspace:split-horizontal<CR>
|
||||
```
|
||||
|
||||
## Supported Commands
|
||||
|
|
@ -100,9 +103,9 @@ If you want to support the development of this plugin, please consider to [buy m
|
|||
|
||||
## "Please implement \[some Vim feature here\]..."
|
||||
|
||||
I'd like to emphasize again that this plugin is a tweak to Obsidian's built-in Vim mode, which is in turn mostly the [Vim mode of CodeMirror](https://codemirror.net/demo/vim.html). And while I am personally very fond of helping everybody make use of Vim modes everywhere, this plugin is often not the best place to implement some types of features.
|
||||
I'd like to emphasize again that this plugin is a tweak to Obsidian's built-in Vim mode, which is in turn mostly the [`codemirror-vim` extension for CodeMirror](https://raw.githack.com/replit/codemirror-vim/master/dev/web-demo.html). And while I am personally very fond of helping everybody make use of Vim modes everywhere, this plugin is often not the best place to implement some types of features.
|
||||
|
||||
1. Vim editor features (e.g. new motions) would best be implemented in CodeMirror, so other editors using this component would enjoy them too! Please consider submitting issues or pull requests [there](https://github.com/codemirror/CodeMirror/) first.
|
||||
1. Vim editor features (e.g. new motions) would best be implemented in `codemirror-vim`, so other editors using this component would enjoy them too! Please consider submitting issues or pull requests [there](https://github.com/replit/codemirror-vim) first.
|
||||
2. Features that are already implemented by other Obsidian plugins are best to stay in these plugins. Please consider asking these plugin authors to add Vim support for their features (using the CodeMirror API), or even better -- help them out :)
|
||||
|
||||
Having said that, adding features here in this plugin is often very easy thanks to the CodeMirror [API for extending its Vim mode](https://codemirror.net/doc/manual.html#vimapi_extending), so as the path of least resistance I will occassionally implement some requested Vim features and be happy to accept PRs.
|
||||
|
|
@ -186,7 +189,7 @@ map sb :surround_brackets<CR>
|
|||
map s( :surround_brackets<CR>
|
||||
map s) :surround_brackets<CR>
|
||||
map s[ :surround_square_brackets<CR>
|
||||
map s[ :surround_square_brackets<CR>
|
||||
map s] :surround_square_brackets<CR>
|
||||
map s{ :surround_curly_brackets<CR>
|
||||
map s} :surround_curly_brackets<CR>
|
||||
```
|
||||
|
|
@ -214,6 +217,22 @@ But first `<Space>` must be unbound with `unmap <Space>`.
|
|||
Afterwards `<Space>` can be mapped normally as any other key.
|
||||
|
||||
|
||||
## Leader Key Support
|
||||
|
||||
You can use the standard Vim `<leader>` key in your mappings. Set your leader key with `let mapleader` and then use `<leader>` in any mapping command:
|
||||
|
||||
```vim
|
||||
let mapleader = ","
|
||||
nmap <leader>f :nohl<CR>
|
||||
nmap <leader>w :obcommand editor:save-file<CR>
|
||||
```
|
||||
|
||||
The default leader key is `\` (backslash), matching Vim's default.
|
||||
|
||||
`<leader>` works in all mapping commands (`map`, `nmap`, `noremap`, `imap`, `vmap`, etc.) and is case-insensitive — `<Leader>`, `<LEADER>`, and `<leader>` all work.
|
||||
|
||||
You can change the leader key mid-file — each `let mapleader` only affects mappings that come after it, matching Vim's behavior.
|
||||
|
||||
## Emulate Common Vim Commands via Obsidian commands
|
||||
|
||||
Using `obcommand`, it is possible to emulate some additional Vim commands that aren't included in Obsidian's Vim mode, like for example `gt` and `zo`.
|
||||
|
|
@ -577,3 +596,4 @@ Fixed [an issue](https://github.com/esm7/obsidian-vimrc-support/issues/2) caused
|
|||
The plugin now injects the Vimrc just once for the CodeMirror class (for the class -- not object instance, because that's where CodeMirror keeps the Vim settings.)
|
||||
|
||||
This seems to work well, but in theory there could be Vimrc settings that are CodeMirror-object bound and not class-bound, and in that case we'll be in trouble (these settings will be lost when Obsidian replaces CodeMirror objects).
|
||||
|
||||
|
|
|
|||
54
main.ts
54
main.ts
|
|
@ -58,6 +58,14 @@ const mappingCommands: String[] = [
|
|||
"vunmap",
|
||||
]
|
||||
|
||||
// All Ex commands that take key-sequence arguments where <leader> should be expanded.
|
||||
// This is broader than mappingCommands because CodeMirror supports more mapping variants.
|
||||
const leaderMapCommands: string[] = [
|
||||
"map", "nmap", "imap", "vmap", "omap",
|
||||
"noremap", "nnoremap", "vnoremap", "inoremap", "onoremap",
|
||||
"unmap", "iunmap", "nunmap", "vunmap",
|
||||
];
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
|
@ -71,6 +79,7 @@ export default class VimrcPlugin extends Plugin {
|
|||
private lastYankBuffer: string[] = [""];
|
||||
private lastSystemClipboard = "";
|
||||
private yankToSystemClipboard: boolean = false;
|
||||
private registeredYankEventsWindows: Set<Window> = new Set();
|
||||
private currentKeyChord: any = [];
|
||||
private vimChordStatusBar: HTMLElement = null;
|
||||
private vimStatusBar: HTMLElement = null;
|
||||
|
|
@ -78,6 +87,7 @@ export default class VimrcPlugin extends Plugin {
|
|||
private customVimKeybinds: { [name: string]: boolean } = {};
|
||||
private currentSelection: [EditorSelection] = null;
|
||||
private isInsertMode: boolean = false;
|
||||
private leaderKey: string = "\\";
|
||||
|
||||
updateVimStatusBar() {
|
||||
this.vimStatusBar.setText(
|
||||
|
|
@ -115,6 +125,9 @@ export default class VimrcPlugin extends Plugin {
|
|||
this.app.workspace.on("window-open", (workspaceWindow, w) => {
|
||||
this.registerYankEvents(w);
|
||||
})
|
||||
this.app.workspace.on("window-close", (workspaceWindow, w) => {
|
||||
this.registeredYankEventsWindows.delete(w);
|
||||
})
|
||||
|
||||
this.prepareChordDisplay();
|
||||
this.prepareVimModeDisplay();
|
||||
|
|
@ -123,13 +136,12 @@ export default class VimrcPlugin extends Plugin {
|
|||
// this don't trigger on loading/reloading obsidian with note opened
|
||||
this.app.workspace.on("active-leaf-change", async () => {
|
||||
this.updateSelectionEvent();
|
||||
|
||||
this.updateVimEvents();
|
||||
this.registerYankEvents(activeWindow);
|
||||
});
|
||||
// and this don't trigger on opening same file in new pane
|
||||
this.app.workspace.on("file-open", async () => {
|
||||
this.updateSelectionEvent();
|
||||
|
||||
this.updateVimEvents();
|
||||
});
|
||||
|
||||
|
|
@ -137,6 +149,8 @@ export default class VimrcPlugin extends Plugin {
|
|||
}
|
||||
|
||||
registerYankEvents(win: Window) {
|
||||
if (this.registeredYankEventsWindows.has(win))
|
||||
return;
|
||||
this.registerDomEvent(win.document, 'click', () => {
|
||||
this.captureYankBuffer(win);
|
||||
});
|
||||
|
|
@ -146,6 +160,7 @@ export default class VimrcPlugin extends Plugin {
|
|||
this.registerDomEvent(win.document, 'focusin', () => {
|
||||
this.captureYankBuffer(win);
|
||||
})
|
||||
this.registeredYankEventsWindows.add(win)
|
||||
}
|
||||
|
||||
async updateSelectionEvent() {
|
||||
|
|
@ -170,7 +185,7 @@ export default class VimrcPlugin extends Plugin {
|
|||
}
|
||||
|
||||
async updateVimEvents() {
|
||||
if (!(this.app as Any).isVimEnabled())
|
||||
if (!(this.app as any).isVimEnabled())
|
||||
return;
|
||||
let view = this.getActiveView();
|
||||
if (view) {
|
||||
|
|
@ -309,18 +324,45 @@ export default class VimrcPlugin extends Plugin {
|
|||
vimCommands.split("\n").forEach(
|
||||
function (line: string, index: number, arr: [string]) {
|
||||
if (line.trim().length > 0 && line.trim()[0] != '"') {
|
||||
let split = line.split(" ")
|
||||
// Parse "let mapleader" directives (consumed, not forwarded to CodeMirror).
|
||||
const leader = this.parseLeaderDirective(line.trim());
|
||||
if (leader !== null) {
|
||||
this.leaderKey = leader;
|
||||
return;
|
||||
}
|
||||
|
||||
// Substitute <leader> in mapping commands before handing to CodeMirror.
|
||||
const processedLine = this.substituteLeader(line, this.leaderKey);
|
||||
|
||||
let split = processedLine.split(" ")
|
||||
if (mappingCommands.includes(split[0])) {
|
||||
// Have to do this because "vim-command-done" event doesn't actually work properly, or something.
|
||||
this.customVimKeybinds[split[1]] = true
|
||||
}
|
||||
this.codeMirrorVimObject.handleEx(cmEditor, line);
|
||||
this.codeMirrorVimObject.handleEx(cmEditor, processedLine);
|
||||
}
|
||||
}.bind(this) // Faster than an arrow function. https://stackoverflow.com/questions/50375440/binding-vs-arrow-function-for-react-onclick-event
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private parseLeaderDirective(line: string): string | null {
|
||||
// Match: let mapleader = "x" or let mapleader = 'x'
|
||||
// Case-insensitive on "let mapleader" to be forgiving, matching Vim behavior.
|
||||
const match = line.match(/^\s*let\s+mapleader\s*=\s*["'](.+?)["']\s*$/i);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
private substituteLeader(line: string, leaderKey: string): string {
|
||||
const trimmed = line.trim();
|
||||
const firstSpace = trimmed.indexOf(" ");
|
||||
if (firstSpace === -1) return line;
|
||||
const command = trimmed.substring(0, firstSpace);
|
||||
if (!leaderMapCommands.includes(command)) return line;
|
||||
// Replace all occurrences of <leader> (case-insensitive) with the leader key.
|
||||
return line.replace(/<leader>/gi, leaderKey);
|
||||
}
|
||||
|
||||
defineBasicCommands(vimObject: any) {
|
||||
vimObject.defineOption('clipboard', '', 'string', ['clip'], (value: string, cm: any) => {
|
||||
if (value) {
|
||||
|
|
@ -515,7 +557,7 @@ export default class VimrcPlugin extends Plugin {
|
|||
// Using the register for when this.yankToSystemClipboard == false
|
||||
surroundFunc(
|
||||
['[',
|
||||
'](' + vimObject.getRegisterController().getRegister('yank').keyBuffer + ")"]);
|
||||
'](' + vimObject.getRegisterController().getRegister('yank').keyBuffer[0].trim() + ")"]);
|
||||
})
|
||||
|
||||
var editor = this.getActiveView().editor;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,71 @@
|
|||
import { jumpToPattern } from "../utils/jumpToPattern";
|
||||
import { MotionFn } from "../utils/vimApi";
|
||||
|
||||
const WIKILINK_REGEX_STRING = "\\[\\[.*?\\]\\]";
|
||||
const MARKDOWN_LINK_REGEX_STRING = "\\[.*?\\]\\(.*?\\)";
|
||||
const URL_REGEX_STRING = "\\w+://\\S+";
|
||||
/** Regex for a wikilink. Starts off with a negative lookbehind for a backslash, to make sure the
|
||||
* opening square bracket isn't escaped.
|
||||
*
|
||||
* Note that although Obsidian doesn't allow most special characters inside a file name (e.g. `[`,
|
||||
* `]`, `:`, `#`, `|`, `^`, `\`, or `/`), its Markdown parser is lenient enough to result in
|
||||
* basically anything inside non-escaped double square brackets being rendered as a wikilink. This
|
||||
* even includes a trailing backslash, which you'd think would escape the first closing square
|
||||
* bracket! So we follow suit and allow any character inside the double square brackets, except for
|
||||
* a newline (which `.` won't match as long as we don't use the `s` flag).
|
||||
*/
|
||||
const WIKILINK_REGEX = /(?<!\\)\[\[.*?\]\]/g;
|
||||
|
||||
/** Regex for a markdown link of the form `[display text](url)`.
|
||||
*
|
||||
* Obsidian's parsing of markdown links is inconsistent between Live Preview and Reading mode. We
|
||||
* align more with Reading mode, differing mainly in our handling of square brackets in the display
|
||||
* text.
|
||||
*
|
||||
* Our markdown link regex matches a string with the following structure:
|
||||
* - An opening square bracket `[` that is not escaped (i.e. not preceded by a backslash)
|
||||
* - A sequence of display text characters:
|
||||
* - Any character that is not a backslash, square bracket, or newline; or
|
||||
* - A backslash followed by another character. This simultaneously allows escaped square
|
||||
* brackets within the display text, and ensures that the backslash is not escaping the closing
|
||||
* square bracket of the display text.
|
||||
* - A closing square bracket `]`
|
||||
* - A URL in non-escaped parentheses `( ... )`
|
||||
*
|
||||
* **Note**: This regex does not allow for unescaped square brackets within the display text, even
|
||||
* though Obsidian's Markdown parser may render such links in Reading mode. In particular, Obsidian
|
||||
* allows unescaped brackets as long as they come in pairs (e.g. `[Label [2]](url)`).
|
||||
*
|
||||
* Our stricter behavior is intentional:
|
||||
* - It provides a simple way to prevent things like checkboxes being treated as part of the link
|
||||
* (e.g. `- [ ] [display text](url)`)
|
||||
* - Fully aligning with Obsidian's parsing would require a more complex regex that would be less
|
||||
* performant (e.g. nested lookbehinds to only match pairs of brackets) and harder to maintain
|
||||
* - Obsidian's markdown link parsing is not super consistent or coherent anyway
|
||||
* - Users can still use square brackets in display text if they escape them, which is arguably
|
||||
* better practice anyway
|
||||
*/
|
||||
const MARKDOWN_LINK_REGEX = /(?<!\\)\[(?:[^\\\[\]\n]|\\.)*?\]\(.*?(?<!\\)\)/g;
|
||||
|
||||
/** Regex for a standalone URL. This is a naive regex that matches any string that starts with a
|
||||
* protocol-looking prefix (any lowercase letters followed by `://`) followed by one or more
|
||||
* non-whitespace characters (from domain name to URL end).
|
||||
*
|
||||
* Note that Obsidian's markdown parser again, just like for wikilinks and markdown links, is
|
||||
* inconsistent between Live Preview and Reading modes, but seems to be more accurate in Reading
|
||||
* mode. Our regex is a bit more lenient than both (e.g. we don't restrict to a subset of known
|
||||
* protocols), but it should be good enough for most cases, and it's better to be lenient than to be
|
||||
* too strict.
|
||||
*/
|
||||
const URL_REGEX = /[a-z]+:\/\/\S+/g;
|
||||
|
||||
/**
|
||||
* Regex for a link (which can be a wikilink, a markdown link, or a standalone URL).
|
||||
*
|
||||
* Ordered to match on wikilinks first, then markdown links, and finally standalone URLs. This
|
||||
* matches how Obsidian's markdown parser works. E.g. Obsidian treats the following as:
|
||||
* - `[[[display text](https://example.com)]]]` - a wikilink
|
||||
* - `[[2]](https://example.com)` - a wikilink plus a standalone URL
|
||||
*/
|
||||
const LINK_REGEX_STRING = `${WIKILINK_REGEX_STRING}|${MARKDOWN_LINK_REGEX_STRING}|${URL_REGEX_STRING}`;
|
||||
const LINK_REGEX = new RegExp(LINK_REGEX_STRING, "g");
|
||||
const LINK_REGEX_STRING = `${WIKILINK_REGEX.source}|${MARKDOWN_LINK_REGEX.source}|${URL_REGEX.source}`;
|
||||
export const LINK_REGEX = new RegExp(LINK_REGEX_STRING, "g");
|
||||
|
||||
/**
|
||||
* Jumps to the repeat-th next link.
|
||||
|
|
@ -18,7 +74,7 @@ const LINK_REGEX = new RegExp(LINK_REGEX_STRING, "g");
|
|||
* match; and that `LINK_REGEX` matches wikilinks / markdown links first. So, this won't catch
|
||||
* non-standalone URLs (e.g. the URL in a markdown link). This should be a good thing in most cases;
|
||||
* otherwise it could be tedious (as a user) for each markdown link to contain two jumpable spots.
|
||||
*/
|
||||
*/
|
||||
export const jumpToNextLink: MotionFn = (cm, cursorPosition, { repeat }) => {
|
||||
return jumpToPattern({
|
||||
cm,
|
||||
|
|
|
|||
2889
package-lock.json
generated
Normal file
2889
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,9 @@
|
|||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "rollup --config rollup.config.js -w",
|
||||
"build": "rollup --config rollup.config.js"
|
||||
"build": "rollup --config rollup.config.js",
|
||||
"test": "vitest",
|
||||
"test:watch": "vitest --watch"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
|
@ -14,14 +16,14 @@
|
|||
"@rollup/plugin-commonjs": "^15.1.0",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"@rollup/plugin-typescript": "^11.0.0",
|
||||
"@types/node": "^14.14.6",
|
||||
"@types/string.prototype.matchall": "^4.0.4",
|
||||
"codemirror": "^5.62.2",
|
||||
"keyboardevent-from-electron-accelerator": "*",
|
||||
"obsidian": "^1.1.1",
|
||||
"rollup": "^2.33.0",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.3"
|
||||
"typescript": "^5.5.3",
|
||||
"vitest": "^3.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"string.prototype.matchall": "^4.0.11"
|
||||
|
|
|
|||
325
tests/jumpToLink.test.ts
Normal file
325
tests/jumpToLink.test.ts
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
import { LINK_REGEX } from "motions/jumpToLink";
|
||||
import { describe, test, expect } from "vitest";
|
||||
|
||||
function expectMatchesForLines(contentLines: string[], expectedMatches: string[]): void {
|
||||
expect(getMatches(contentLines)).toEqual(expectedMatches);
|
||||
}
|
||||
|
||||
function getMatches(contentLines: string[]): string[] {
|
||||
const content = contentLines.join("\n");
|
||||
return [...content.matchAll(LINK_REGEX)].map((match) => match[0]);
|
||||
}
|
||||
|
||||
function testSingleLinkLine(link: string): void {
|
||||
const lineWithLink = makeLineWithLink(link);
|
||||
expectMatchesForLines([lineWithLink], [link]);
|
||||
}
|
||||
|
||||
function testSingleMarkdownLinkLine(displayText: string): void {
|
||||
const markdownLink = makeExampleHttpsMarkdownLink(displayText);
|
||||
testSingleLinkLine(markdownLink);
|
||||
}
|
||||
|
||||
function makeMarkdownLink(displayText: string, link: string): string {
|
||||
return `[${displayText}](${link})`;
|
||||
}
|
||||
|
||||
function makeExampleHttpMarkdownLink(displayText: string): string {
|
||||
return makeMarkdownLink(displayText, EXAMPLE_HTTP_URL);
|
||||
}
|
||||
|
||||
function makeExampleHttpsMarkdownLink(displayText: string): string {
|
||||
return makeMarkdownLink(displayText, EXAMPLE_HTTPS_URL);
|
||||
}
|
||||
|
||||
function makeLineWithLink(link: string): string {
|
||||
return `Check out this link: ${link} - click it!`;
|
||||
}
|
||||
|
||||
function makeLineWithMultipleLinks(links: string[]): string {
|
||||
return `Check out these links: ${links.join(" | ")} - click any of them!`;
|
||||
}
|
||||
|
||||
const WIKILINK_TEXT = "[[Some internal note]]";
|
||||
|
||||
const EXAMPLE_HTTP_URL = "http://example.com";
|
||||
const EXAMPLE_HTTPS_URL = "https://example.com";
|
||||
|
||||
const MARKDOWN_HTTP_LINK = makeExampleHttpMarkdownLink("example");
|
||||
const MARKDOWN_HTTPS_LINK = makeExampleHttpsMarkdownLink("example");
|
||||
|
||||
describe("regex matching links", () => {
|
||||
describe("wikilink", () => {
|
||||
test("simple wikilink", () => testSingleLinkLine(WIKILINK_TEXT));
|
||||
test("escaped open bracket makes wikilink invalid", () => {
|
||||
expectMatchesForLines([`\\${WIKILINK_TEXT}`], []);
|
||||
});
|
||||
describe("shouldn't be matched over multiple lines", () => {
|
||||
test("split after first open bracket", () => {
|
||||
const firstLine = "Check out this link: [";
|
||||
const secondLine = `[Some internal note]] - click it!`;
|
||||
expectMatchesForLines([firstLine, secondLine], []);
|
||||
});
|
||||
test("split after second open bracket", () => {
|
||||
const firstLine = "Check out this link: [[";
|
||||
const secondLine = `Some internal note]] - click it!`;
|
||||
expectMatchesForLines([firstLine, secondLine], []);
|
||||
});
|
||||
test("split in middle of text", () => {
|
||||
const firstLine = "Check out this link: [[Some internal";
|
||||
const secondLine = ` note]] - click it!`;
|
||||
expectMatchesForLines([firstLine, secondLine], []);
|
||||
});
|
||||
test("split before first close bracket", () => {
|
||||
const firstLine = "Check out this link: [[Some internal note";
|
||||
const secondLine = `]] - click it!`;
|
||||
expectMatchesForLines([firstLine, secondLine], []);
|
||||
});
|
||||
test("split before second close bracket", () => {
|
||||
const firstLine = "Check out this link: [[Some internal note]";
|
||||
const secondLine = `] - click it!`;
|
||||
expectMatchesForLines([firstLine, secondLine], []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("markdown link", () => {
|
||||
describe("simple display text", () => {
|
||||
test("http", () => testSingleLinkLine(MARKDOWN_HTTP_LINK));
|
||||
test("https", () => testSingleLinkLine(MARKDOWN_HTTPS_LINK));
|
||||
});
|
||||
describe("square brackets inside display text", () => {
|
||||
describe("escaped square brackets", () => {
|
||||
describe("escaped open bracket", () => {
|
||||
test("at start", () => testSingleMarkdownLinkLine("\\[some display text"));
|
||||
test("in middle", () => testSingleMarkdownLinkLine("some display \\[ text"));
|
||||
test("at end", () => testSingleMarkdownLinkLine("some display text \\["));
|
||||
});
|
||||
describe("escaped close bracket", () => {
|
||||
test("at start", () => testSingleMarkdownLinkLine("\\] some display text"));
|
||||
test("in middle", () => testSingleMarkdownLinkLine("some display \\] text"));
|
||||
test("at end", () => testSingleMarkdownLinkLine("some display text \\]"));
|
||||
});
|
||||
describe("escaped open and close bracket", () => {
|
||||
test("at start", () => testSingleMarkdownLinkLine("\\[ some display \\] text"));
|
||||
test("in middle", () => testSingleMarkdownLinkLine("some \\[ display \\] text"));
|
||||
test("at end", () => testSingleMarkdownLinkLine("some display \\[ text \\]"));
|
||||
test("footnote link", () => testSingleMarkdownLinkLine("\\[2\\]"));
|
||||
});
|
||||
});
|
||||
describe("non-escaped square brackets", () => {
|
||||
test("non-escaped open bracket should become the start of the markdown link", () => {
|
||||
const markdownLink = `[some display [ text](${EXAMPLE_HTTPS_URL})`;
|
||||
expectMatchesForLines(
|
||||
[makeLineWithLink(markdownLink)],
|
||||
[`[ text](${EXAMPLE_HTTPS_URL})`]
|
||||
);
|
||||
});
|
||||
test("non-escaped close bracket should invalidate the markdown link", () => {
|
||||
const markdownLink = `[some display ] text](${EXAMPLE_HTTPS_URL})`;
|
||||
// Markdown link isn't valid, so the regex just matches the url as a standalone url
|
||||
expectMatchesForLines([makeLineWithLink(markdownLink)], [`${EXAMPLE_HTTPS_URL})`]);
|
||||
});
|
||||
test("non-escaped footnote attempt should end up being a wikilink plus standalone url", () => {
|
||||
const markdownLink = makeExampleHttpsMarkdownLink("[2]");
|
||||
const lineWithLink = makeLineWithLink(markdownLink);
|
||||
expectMatchesForLines([lineWithLink], ["[[2]]", `${EXAMPLE_HTTPS_URL})`]);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("square brackets before markdown link", () => {
|
||||
test("single open bracket before markdown link shouldn't affect link match", () => {
|
||||
const baseLineWithLink = makeLineWithLink(MARKDOWN_HTTP_LINK);
|
||||
const fullLineWithLink = `some text [ ${baseLineWithLink}`;
|
||||
expectMatchesForLines([fullLineWithLink], [MARKDOWN_HTTP_LINK]);
|
||||
});
|
||||
test("single close bracket before markdown link shouldn't affect link match", () => {
|
||||
const baseLineWithLink = makeLineWithLink(MARKDOWN_HTTP_LINK);
|
||||
const fullLineWithLink = `some text ] ${baseLineWithLink}`;
|
||||
expectMatchesForLines([fullLineWithLink], [MARKDOWN_HTTP_LINK]);
|
||||
});
|
||||
test("open and closed bracket before markdown link shouldn't affect link match", () => {
|
||||
const baseLineWithLink = makeLineWithLink(MARKDOWN_HTTP_LINK);
|
||||
const fullLineWithLink = `[some] text ${baseLineWithLink}`;
|
||||
expectMatchesForLines([fullLineWithLink], [MARKDOWN_HTTP_LINK]);
|
||||
});
|
||||
});
|
||||
describe("markdown link across lines shouldn't be matched", () => {
|
||||
test("split before ] should just match the url as a standalone url", () => {
|
||||
const firstLine = "Check out this link: [example";
|
||||
const secondLine = `](${EXAMPLE_HTTPS_URL}) - click it!`;
|
||||
const expectedMatch = `${EXAMPLE_HTTPS_URL})`; // it'll include the closing parenthesis
|
||||
expectMatchesForLines([firstLine, secondLine], [expectedMatch]);
|
||||
});
|
||||
test("split after ] should just match the url as a standalone url", () => {
|
||||
const firstLine = "Check out this link: [example]";
|
||||
const secondLine = `(${EXAMPLE_HTTPS_URL}) - click it!`;
|
||||
const expectedMatch = `${EXAMPLE_HTTPS_URL})`; // it'll include the opening parenthesis
|
||||
expectMatchesForLines([firstLine, secondLine], [expectedMatch]);
|
||||
});
|
||||
test("split after ( should just match the url as a standalone url", () => {
|
||||
const firstLine = "Check out this link: [example](";
|
||||
const secondLine = `${EXAMPLE_HTTPS_URL}) - click it!`;
|
||||
const expectedMatch = `${EXAMPLE_HTTPS_URL})`; // it'll include the closing parenthesis
|
||||
expectMatchesForLines([firstLine, secondLine], [expectedMatch]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("standalone URL", () => {
|
||||
describe("various protocols", () => {
|
||||
test("http", () => testSingleLinkLine(EXAMPLE_HTTP_URL));
|
||||
test("https", () => testSingleLinkLine(EXAMPLE_HTTPS_URL));
|
||||
test("ftp", () => testSingleLinkLine("ftp://example.com"));
|
||||
test("file", () => testSingleLinkLine("file://example/path/to/file"));
|
||||
test("chrome extensions", () => testSingleLinkLine("chrome://extensions"));
|
||||
test("chrome settings", () => testSingleLinkLine("chrome://settings"));
|
||||
test("chrome bookmarks", () => testSingleLinkLine("chrome://bookmarks"));
|
||||
test("chrome history", () => testSingleLinkLine("chrome://history"));
|
||||
test("chrome downloads", () => testSingleLinkLine("chrome://downloads"));
|
||||
test("chrome flags", () => testSingleLinkLine("chrome://flags"));
|
||||
});
|
||||
describe("shouldn't be matched over multiple lines", () => {
|
||||
test("split before colon", () => {
|
||||
const firstLine = "Check out this link: http";
|
||||
const secondLine = `://example.com - click it!`;
|
||||
expectMatchesForLines([firstLine, secondLine], []);
|
||||
});
|
||||
test("split after colon", () => {
|
||||
const firstLine = "Check out this link: http:";
|
||||
const secondLine = `//example.com - click it!`;
|
||||
expectMatchesForLines([firstLine, secondLine], []);
|
||||
});
|
||||
test("split between slashes", () => {
|
||||
const firstLine = "Check out this link: http:/";
|
||||
const secondLine = `/example.com - click it!`;
|
||||
expectMatchesForLines([firstLine, secondLine], []);
|
||||
});
|
||||
test("split after slashes", () => {
|
||||
const firstLine = "Check out this link: http://";
|
||||
const secondLine = `example.com - click it!`;
|
||||
expectMatchesForLines([firstLine, secondLine], []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("multiple link types", () => {
|
||||
const linkOrderings = [
|
||||
[WIKILINK_TEXT, MARKDOWN_HTTP_LINK, EXAMPLE_HTTP_URL],
|
||||
[WIKILINK_TEXT, MARKDOWN_HTTPS_LINK, EXAMPLE_HTTPS_URL],
|
||||
[WIKILINK_TEXT, EXAMPLE_HTTP_URL, MARKDOWN_HTTP_LINK],
|
||||
[WIKILINK_TEXT, EXAMPLE_HTTPS_URL, MARKDOWN_HTTPS_LINK],
|
||||
[MARKDOWN_HTTP_LINK, WIKILINK_TEXT, EXAMPLE_HTTP_URL],
|
||||
[MARKDOWN_HTTPS_LINK, WIKILINK_TEXT, EXAMPLE_HTTPS_URL],
|
||||
[MARKDOWN_HTTP_LINK, EXAMPLE_HTTP_URL, WIKILINK_TEXT],
|
||||
[MARKDOWN_HTTPS_LINK, EXAMPLE_HTTPS_URL, WIKILINK_TEXT],
|
||||
[EXAMPLE_HTTP_URL, WIKILINK_TEXT, MARKDOWN_HTTP_LINK],
|
||||
[EXAMPLE_HTTPS_URL, WIKILINK_TEXT, MARKDOWN_HTTPS_LINK],
|
||||
[EXAMPLE_HTTP_URL, MARKDOWN_HTTP_LINK, WIKILINK_TEXT],
|
||||
[EXAMPLE_HTTPS_URL, MARKDOWN_HTTPS_LINK, WIKILINK_TEXT],
|
||||
];
|
||||
test("various orderings of multiple link types within a line", () => {
|
||||
for (const links of linkOrderings) {
|
||||
const multiLinkLine = makeLineWithMultipleLinks(links);
|
||||
expectMatchesForLines([multiLinkLine], links);
|
||||
}
|
||||
});
|
||||
test("various orderings of multiple link types across lines", () => {
|
||||
for (const links of linkOrderings) {
|
||||
const separateLines = links.map((link) => makeLineWithLink(link));
|
||||
expectMatchesForLines(separateLines, links);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("interaction with other markdown", () => {
|
||||
function makeBulletPoint(text: string): string {
|
||||
return `- ${text}`;
|
||||
}
|
||||
describe("bullet point", () => {
|
||||
test("wikilink alone", () => {
|
||||
const wikilinkBulletPoint = makeBulletPoint(WIKILINK_TEXT);
|
||||
expectMatchesForLines([wikilinkBulletPoint], [WIKILINK_TEXT]);
|
||||
});
|
||||
test("wikilink with other text", () => {
|
||||
const wikilinkBulletPoint = makeBulletPoint(makeLineWithLink(WIKILINK_TEXT));
|
||||
expectMatchesForLines([wikilinkBulletPoint], [WIKILINK_TEXT]);
|
||||
});
|
||||
test("markdown link alone", () => {
|
||||
const markdownLinkBulletPoint = makeBulletPoint(MARKDOWN_HTTP_LINK);
|
||||
expectMatchesForLines([markdownLinkBulletPoint], [MARKDOWN_HTTP_LINK]);
|
||||
});
|
||||
test("markdown link with other text", () => {
|
||||
const markdownLinkBulletPoint = makeBulletPoint(makeLineWithLink(MARKDOWN_HTTP_LINK));
|
||||
expectMatchesForLines([markdownLinkBulletPoint], [MARKDOWN_HTTP_LINK]);
|
||||
});
|
||||
test("standalone URL alone", () => {
|
||||
const standaloneUrlBulletPoint = makeBulletPoint(EXAMPLE_HTTP_URL);
|
||||
expectMatchesForLines([standaloneUrlBulletPoint], [EXAMPLE_HTTP_URL]);
|
||||
});
|
||||
test("standalone URL with other text", () => {
|
||||
const standaloneUrlBulletPoint = makeBulletPoint(makeLineWithLink(EXAMPLE_HTTP_URL));
|
||||
expectMatchesForLines([standaloneUrlBulletPoint], [EXAMPLE_HTTP_URL]);
|
||||
});
|
||||
});
|
||||
|
||||
function makeUncheckedCheckbox(text: string): string {
|
||||
return `- [ ] ${text}`;
|
||||
}
|
||||
function makeCheckedCheckbox(text: string): string {
|
||||
return `- [x] ${text}`;
|
||||
}
|
||||
describe("unchecked checkbox", () => {
|
||||
test("wikilink alone", () => {
|
||||
const wikilinkCheckbox = makeUncheckedCheckbox(WIKILINK_TEXT);
|
||||
expectMatchesForLines([wikilinkCheckbox], [WIKILINK_TEXT]);
|
||||
});
|
||||
test("wikilink with other text", () => {
|
||||
const wikilinkCheckbox = makeUncheckedCheckbox(makeLineWithLink(WIKILINK_TEXT));
|
||||
expectMatchesForLines([wikilinkCheckbox], [WIKILINK_TEXT]);
|
||||
});
|
||||
test("markdown link alone", () => {
|
||||
const markdownLinkCheckbox = makeUncheckedCheckbox(MARKDOWN_HTTP_LINK);
|
||||
expectMatchesForLines([markdownLinkCheckbox], [MARKDOWN_HTTP_LINK]);
|
||||
});
|
||||
test("markdown link with other text", () => {
|
||||
const markdownLinkCheckbox = makeUncheckedCheckbox(makeLineWithLink(MARKDOWN_HTTP_LINK));
|
||||
expectMatchesForLines([markdownLinkCheckbox], [MARKDOWN_HTTP_LINK]);
|
||||
});
|
||||
test("standalone URL alone", () => {
|
||||
const standaloneUrlCheckbox = makeUncheckedCheckbox(EXAMPLE_HTTP_URL);
|
||||
expectMatchesForLines([standaloneUrlCheckbox], [EXAMPLE_HTTP_URL]);
|
||||
});
|
||||
test("standalone URL with other text", () => {
|
||||
const standaloneUrlCheckbox = makeUncheckedCheckbox(makeLineWithLink(EXAMPLE_HTTP_URL));
|
||||
expectMatchesForLines([standaloneUrlCheckbox], [EXAMPLE_HTTP_URL]);
|
||||
});
|
||||
});
|
||||
describe("checked checkbox", () => {
|
||||
test("wikilink alone", () => {
|
||||
const wikilinkCheckbox = makeCheckedCheckbox(WIKILINK_TEXT);
|
||||
expectMatchesForLines([wikilinkCheckbox], [WIKILINK_TEXT]);
|
||||
});
|
||||
test("wikilink with other text", () => {
|
||||
const wikilinkCheckbox = makeCheckedCheckbox(makeLineWithLink(WIKILINK_TEXT));
|
||||
expectMatchesForLines([wikilinkCheckbox], [WIKILINK_TEXT]);
|
||||
});
|
||||
test("markdown link alone", () => {
|
||||
const markdownLinkCheckbox = makeCheckedCheckbox(MARKDOWN_HTTP_LINK);
|
||||
expectMatchesForLines([markdownLinkCheckbox], [MARKDOWN_HTTP_LINK]);
|
||||
});
|
||||
test("markdown link with other text", () => {
|
||||
const markdownLinkCheckbox = makeCheckedCheckbox(makeLineWithLink(MARKDOWN_HTTP_LINK));
|
||||
expectMatchesForLines([markdownLinkCheckbox], [MARKDOWN_HTTP_LINK]);
|
||||
});
|
||||
test("standalone URL alone", () => {
|
||||
const standaloneUrlCheckbox = makeCheckedCheckbox(EXAMPLE_HTTP_URL);
|
||||
expectMatchesForLines([standaloneUrlCheckbox], [EXAMPLE_HTTP_URL]);
|
||||
});
|
||||
test("standalone URL with other text", () => {
|
||||
const standaloneUrlCheckbox = makeCheckedCheckbox(makeLineWithLink(EXAMPLE_HTTP_URL));
|
||||
expectMatchesForLines([standaloneUrlCheckbox], [EXAMPLE_HTTP_URL]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -10,13 +10,7 @@
|
|||
"moduleResolution": "node",
|
||||
"downlevelIteration": true,
|
||||
"importHelpers": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"scripthost",
|
||||
"ES2020"
|
||||
]
|
||||
"lib": ["dom", "scripthost", "ES2020"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue