Compare commits

..

16 commits

Author SHA1 Message Date
Threeinone
1ff5d97afc
Merge pull request #267 from alythobani/alyt/4-fix-jump-to-link-in-checkbox
Fix: Jump to link in checkbox
2026-03-25 21:50:07 -05:00
Threeinone
ba2c8243c1
Merge pull request #282 from joaosa/feat/leader-key
feat: Add <leader> key support
2026-03-25 21:29:01 -05:00
Threeinone
b3beeae815
Note on <Space> is unnecessary 2026-03-25 21:26:58 -05:00
João Andrade
10c5a580f9 feat: Add <leader> key support
Parse `let mapleader` directives and substitute `<leader>` in mapping
commands before passing lines to CodeMirror's handleEx(). This is
pure text substitution at the plugin level — no CodeMirror changes needed.

- Default leader is `\` (backslash), matching Vim
- Case-insensitive: <leader>, <Leader>, <LEADER> all work
- Single-pass processing: each `let mapleader` affects subsequent mappings only
- Only substitutes in recognized mapping commands (no false positives)
- Backwards compatible: no change for users who don't use <leader>
- Works with `source` command (sourced files inherit/override leader)

Closes #117
2026-02-16 10:50:39 +00:00
Dmitry Surin
774de34205
Update README.md (#272)
Not enough examples in readme. Trying to fix that. Spend some time to find this ability to run command without exmap
2025-08-22 06:28:13 +03:00
Aly Thobani
923de9fe8a chore: fix ts error by replacing Any with any 2025-05-18 09:21:19 -07:00
Aly Thobani
2fe5ee588c test: add more unit tests to jumpToLink.test.ts 2025-05-18 09:20:21 -07:00
Aly Thobani
2f18ae8b56 fix: make sure the closing paren of a markdown link isnt escaped 2025-05-17 22:33:45 -07:00
Aly Thobani
3d3bf60c88 docs: update docstrings for jumpToLink regexes 2025-05-17 22:33:45 -07:00
Aly Thobani
e800087a72 fix: make jumpToLink work for links inside checkboxes 2025-05-17 22:33:45 -07:00
Aly Thobani
1dd8077282 chore: remove package-lock.json from gitignore and add it to repo 2025-05-17 22:33:45 -07:00
Aly Thobani
340931c975 chore: add words to workspace dictionary to resolve cspell warnings 2025-05-17 22:33:45 -07:00
Juan Luque
4aa4ca31f1
Fix pasteinto error when clipboard text has surrounding whitespace. (#262)
`pasteinto` is called by `surroundFunc` which a string into whitespace
delimited arguments. Surrounding whitespace in the link to be copied
thus errors due to an unhandled number of arguments.

The now fixed error can be recreated by using `yy` to yank a line and
then using <A-p> to paste the link over a word. Similarly, `yW` will
reproduce the error if the link has a trailing whitespace.

`pasteinto` will still error if the link contains internal whitespace,
but this is more reasonable behavior. Fixing this will require tweaking
the regex `surroundFunc` uses to split up arguments.
2025-04-09 08:47:08 +03:00
Juan Luque
47de579471
Update README.md (#261)
Fix typo:  surround with '[ ]' command  `s[` was listed twice.
2025-02-27 16:59:31 +02:00
firai
378753b0b4
Update vim links to point to codemirror-vim repo (#251) 2024-11-24 20:30:37 +02:00
ko-shin-ryo
d376bfd048
fix: prevent yank events registration issues in multi-window setup (#249)
When opening a vault with multiple windows, non-active windows could miss the timing to register yank events. Added a mechanism to manage windows that have registered yank events to ensure proper event handling across all windows.
2024-11-17 13:13:41 +02:00
9 changed files with 3373 additions and 27 deletions

1
.gitignore vendored
View file

@ -7,7 +7,6 @@
# npm
node_modules
package-lock.json
# build
main.js

19
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,19 @@
{
"cSpell.words": [
"exmap",
"iunmap",
"jscommand",
"jsfile",
"Keybinds",
"keychord",
"keymap",
"nmap",
"noremap",
"nunmap",
"obcommand",
"pasteinto",
"sendkeys",
"unnamedplus",
"vunmap"
]
}

View file

@ -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
View file

@ -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;

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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
View 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]);
});
});
});
});

View file

@ -10,13 +10,7 @@
"moduleResolution": "node",
"downlevelIteration": true,
"importHelpers": true,
"lib": [
"dom",
"scripthost",
"ES2020"
]
"lib": ["dom", "scripthost", "ES2020"]
},
"include": [
"**/*.ts"
]
"include": ["**/*.ts"]
}