说在前面
个人感觉,关于网址,当光标放在 )
的后面之后,视觉上会从 text
拓展成为 [text](link)
,这一点是我觉得很烦躁的。尤其是当 text
中出现 \_
\|
或者是 link
中出现 &direct=...
&utm_campaign=...
的时候,这个展开会很长,很影响注意力。
关于这些文本如何消除,这是另一个话题,按下不表;但就算是 text
和 link
可以得到简化,我也觉得在视觉上从 text
编程 [text](link)
多少有点不必要。
因此一种解决方式是改用 [text](link)
,即在后面添加一个空格,因此相关的触发器都是在这个基础上实现的,具有一定个人喜好:
- 当检测到
(link) c
时,触发“复制链接”功能 - 当检测到
(link) v
时,触发“访问链接”功能
在编写文档的时候和“网络”相关的操作无非一下几个:
- 访问网址
- 复制网址
- 粘贴网址生成链接
[text](link)
- 搜索内容
- 清除链接
- 清除
link
,但保留text
- 直接清除(删除行即可)
- 清除
因此在 LaTeX Suite 中写了几个比较常用的触发器。
写这些快捷键的原因是:我不会 vim 的操作,只能左手放键盘上,右手放鼠标上操作。
访问网址 & 复制网址 & 粘贴网址生成链接
t
模式
- 对于
purelink
,后面添加c
或者v
可以复制或访问; - 对于
[text](link)
,后面添加c
或者是v
可以复制或访问;
// 复制粘贴 & 访问
// 对于 `[text](link) ` 形式的链接(注意右括号结尾有一个空格)
// 可以通过在结尾添加一个 `c` 触发 [复制网址] 操作
// 可以通过在结尾添加一个 `v` 触发 [打开网址] 操作
// 对于 `purelink` 形式的链接 -> 光标在链接结尾时可以通过 Obsidian 自定义快捷键打开链接
{trigger: /(?<link>(?:https?|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|])(?<bracket>\)?)\s(?<trigger>c|v|C|V)/g, replacement: (match) => {
const { clipboard } = require('electron');
switch(match.groups.trigger) {
case "c": case "C": { clipboard.writeText(match.groups.link); break; } // copy `link`
case "v": case "V": { window.open(match.groups.link); break; } // visit `link`
}
if (match.groups.bracket == ")")
return `${match.groups.link}${match.groups.bracket} `; // return `link) `
return `${match.groups.link}`; // return `link`
}, options: "rtA"},
vt
模式
- 选中的文本中如果包含以下的内容,都复制下来:
purelink
[text](link)
[[inline link]]
$inline latex formula$
inline code
才疏学浅,用的是最简单的 switch case 来写的代码。。。
// c = copy ( links & inline latex & inline codes )
{trigger: "c", replacement: (sel) => {
// let text_links = /(((?:📌|📜|🌐)(?:rel|ref|via)\: )?(?<!!)\[(.*?)\]\((.*?)\))/g;
if (sel = sel.replaceAll(/\$(\d.*?)\$/g, "\${{ }}\$$")) ; // sensitive_math
// regex
let pure_links = /(?<emoji>(?:📌|📜|🌐)(?:rel|ref|via)\: )?(?<target>(?<!\[(.*)\]\()(?:https?|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|])/g;
let text_links = /(?<target>(?<emoji>(?:📌|📜|🌐)(?:rel|ref|via)\: )?(?<!!)\[(?<text>.*?)\]\((?<link>.*?)\))/g;
let inli_links = /(?<target>(?<emoji>(?:📌|📜|🌐)(?:rel|ref|via)\: )?\[\[([^\]]*?)\]\])/g;
let inli_LaTeX = /(?<target>\$.*?\$)/g;
let inli_codes = /(?<target>`.*?`)/g;
// matches & len
let pl_matches = Array.from(sel.matchAll(pure_links)) || []; // 1️⃣pure links
let tl_matches = Array.from(sel.matchAll(text_links)) || []; // 2️⃣text links
let il_matches = Array.from(sel.matchAll(inli_links)) || []; // 3️⃣inline links
let iT_matches = Array.from(sel.matchAll(inli_LaTeX)) || []; // 4️⃣inline LaTeX
let ic_matches = Array.from(sel.matchAll(inli_codes)) || []; // 5️⃣inline codes
// condition & output
let condition = (
pl_matches.length * 10000 +
tl_matches.length * 1000 +
il_matches.length * 100 +
iT_matches.length * 10 +
ic_matches.length * 1
);
let output = ``;
// 分情况讨论
switch(condition) {
// 没有 target
case 0: { break; }
// 一个 target -> 直接复制
case 10000: { for(let match of pl_matches) output += match.groups.target; break; } // 1️⃣pure links
case 1000: { for(let match of tl_matches) output += match.groups.target + ` `; break; } // 2️⃣text links
case 100: { for(let match of il_matches) output += match.groups.target; break; } // 3️⃣inline links
case 10: { for(let match of iT_matches) output += match.groups.target + ` `; break; } // 4️⃣inline LaTeX
case 1: { for(let match of ic_matches) output += match.groups.target + ` `; break; } // 5️⃣inline codes
// 多个 targets -> 按照自定义的格式复制
default: {
// 1️⃣pure links
for(let match of pl_matches) output += (
`${match.groups.emoji?match.groups.emoji:``}` +
`${match.groups.target}\n`
);
if(pl_matches.length > 0) output += `\n`;
// 2️⃣text links
for(let match of tl_matches) output += (
`- ${match.groups.emoji?match.groups.emoji:``}` +
`${match.groups.text} | ${match.groups.link}\n`
);
if(tl_matches.length > 0) output += `\n`;
// 3️⃣inline links
for(let match of il_matches) output += (
`${match.groups.emoji?match.groups.emoji:``}` +
`${match.groups.target}\n`
);
if(il_matches.length > 0) output += `\n`;
// 4️⃣inline LaTeX
for(let match of iT_matches) output += `${match.groups.target} \n`;
if(iT_matches.length > 0) output += `\n`;
// 5️⃣inline codes
for(let match of ic_matches) output += `${match.groups.target} \n`;
if(ic_matches.length > 0) output += `\n`;
}
}
// 如果在 sel 中成功匹配内容,则 [粘贴内容到剪贴板]
const { clipboard } = require('electron');
if (output != "") { clipboard.writeText(output); }
// sel 不改动
return sel;
}, options: "vt"},
- 如果
sel
中存在网址,那么(批量)访问 - 如果剪贴板中第一项是切仅是网址,那么得到
[sel](cliplink)
- 如果不是,不改变
sel
// 1. v = visit urls
// 2. v = ctrl + v = paste url
{trigger: "v", replacement: (sel) => {
if (sel = sel.replaceAll(/\$(\d.*?)\$/g, "\${{ }}\$$")) ; // sensitive_math
// 1. 如果 sel 中存在链接,就在浏览器中打开这些链接
let purelink = /((https?|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|])/g;
let linklists = Array.from(sel.matchAll(purelink)) || [];
if (linklists.length > 0) { linklists.forEach(url => window.open(url[0])); return sel; }
// 2. 如果 sel 中没有链接,并且剪贴板的第一个内容就只有链接,执行 `[text](link) ` 操作
const { clipboard } = require('electron');
let cliplink = clipboard.readText();
let linkmatch = cliplink.match(purelink);
if (cliplink == linkmatch) { return `[${sel}](${cliplink})`; }
// 最终都不改动 sel
return sel;
}, options: "vt"},
搜索内容
t
模式
举个例子,你想在 Bilibili、知乎、抖音、豆瓣搜索「董小姐、协和」,你可以输入
bb zh dy db 董小姐、协和
注意 b 董
是两个空格,然后光标放在结尾,敲击 tab
,页面上就得到四个搜索页
再来个例子,你想要在 linuxdo github hackernews youtube google kagi 中去搜索 DeepClase
ln gt hn yt gg kk DeepClase
or ln gt hn yt gg kk DeepClase usage
or ln gt hn yt gg kk DeepClase tutorial
同样,结尾敲击 TAB
就得到搜索页面
我没有加上 v2ex.com,因为我的号被封了。。。
同样, v2ex 和 arxiv 在缩写上有点冲突,因此。。。
// 上网查询搜索
// 简写规则:
// 1. 去掉元音字母,保留最前两个辅音字母
// 2. 某些情况,更习惯重复敲击同一个键位两次
{trigger: /(\b(?<platforms>(?:\w{2,3} ?)+)( ?[\||\-|\+|\&| ] )(?<text>.+))/g, replacement: (match) => {
// search platforms
let searchurls = [];
let searchplatforms = match.groups.platforms.split(" ");
searchplatforms.forEach(platform => {
switch(platform) {
case "bb":
case "bl": { searchurls.push("https://search.bilibili.com/all?keyword="); break; }
case "bn": { searchurls.push("https://www.bing.com/search?q="); break; }
case "bs": { searchurls.push("https://bsky.app/search?q="); break; }
case "db": { searchurls.push("https://www.douban.com/search?q="); break; }
case "dd":
case "dc": { searchurls.push("https://duckduckgo.com/?q="); break; }
case "dy": { searchurls.push("https://www.douyin.com/search/"); break; }
case "gg": { searchurls.push("https://www.google.com/search?q=", "https://search.luxirty.com/search?q="); break; }
case "gh":
case "gt": { searchurls.push("https://github.com/search?q="); break; }
case "jd": { searchurls.push("https://search.jd.com/Search?keyword="); break; }
case "kk":
case "kg": { searchurls.push("https://kagi.com/search?q="); break; }
case "lc": { searchurls.push("https://leetcode.cn/search/?q="); break; }
case "hn": case "hs":
case "lg": { searchurls.push("https://hn.algolia.com/?query="); break; }
case "ln": { searchurls.push("https://linux.do/search?q="); break; }
case "qr": { searchurls.push("https://www.quora.com/search?q="); break; }
case "rd": { searchurls.push("https://www.reddit.com/search/?q="); break; }
case "tb": { searchurls.push("https://s.taobao.com/search?&q="); break; }
case "st": { searchurls.push("https://stackoverflow.com/search?q="); break; }
case "xx": case "tw":
case "tt": { searchurls.push("https://x.com/search?q="); break; }
case "wk": { searchurls.push("https://wikipedia.org/wiki/", "https://zh.wikipedia.org/wiki/"); break; }
case "xh":
case "xhs": { searchurls.push("https://www.xiaohongshu.com/search_result?keyword="); break; }
case "xy": { searchurls.push("https://www.goofish.com/search?q="); break; }
case "rx": // arxiv
case "xv": { searchurls.push("https://arxiv.org/search/?query=", "https://searchthearxiv.com/?q="); break; }
case "yt": { searchurls.push("https://www.youtube.com/results?search_query="); break; }
case "zh": { searchurls.push("https://www.zhihu.com/search?q="); break; }
default: { break; }
}
})
// search urls
let tail = match.groups.text.trim()
.replaceAll(/\s+/g, "%20")
.replaceAll(/(,|,|、)/g, "%20")
.replaceAll("%20%20", "%20");
searchurls.forEach(url => { window.open(`${url}${tail}`); }); // 上网搜索
let platforms = searchplatforms.join(" ");
return `\${platforms.trim()} \${match.groups.text.trim()}`;
}, options: "rt"},
// "https://websets.exa.ai/", 可以以后添加上 // Exa Websets - 后期需要 money wss
vt
模式
第一梯队 - 专业搜索引擎
// s = search (Level1)
{trigger: "s", replacement: (sel) => {
if (sel = sel.replaceAll(/\$(\d.*?)\$/g, "\${{ }}\$$")) ; // sensitive_math
// tail
let tail = sel
.replaceAll(/\s+/g, "%20")
.replaceAll(/(,|,|、)/g, "%20")
.replaceAll("%20%20", "%20");
// 1️⃣searchurls -> 生成搜索链接即可完成查询操作
// 第一梯队 - 专业搜索引擎
let searchurls = [
"https://www.bing.com/search?q=", // Bing
"https://duckduckgo.com/?q=", // DuckDuckGo
"https://www.google.com/search?q=", // Google
"https://search.luxirty.com/search?q=", // Luxirty-Search
"https://kagi.com/search?q=", // Kagi
];
searchurls.forEach(url => { window.open(`${url}${tail}`); })
// 2️⃣specialurls -> 打开网站之后需要手动复制 sel 进行查找
// Special - 一些很有价值的搜索和数据挖掘产品
const { clipboard } = require('electron');
clipboard.writeText(sel);
let specialurls = [
"https://websets.exa.ai/", // Exa Websets - 后期需要 money
];
specialurls.forEach(url => { window.open(`${url}`); })
// 最终都不改动 sel
return sel;
}, options: "vt"},
第二梯队 - 某些常用平台的内部搜索
// S = search (Level2)
{trigger: "S", replacement: (sel) => {
if (sel = sel.replaceAll(/\$(\d.*?)\$/g, "\${{ }}\$$")) ; // sensitive_math
// tail
let tail = sel
.replaceAll(/\s+/g, "%20")
.replaceAll(/(,|,|、)/g, "%20")
.replaceAll("%20%20", "%20");
// 1️⃣searchurls -> 生成搜索链接即可完成查询操作
// 第二梯队 - 某些常用平台的内部搜索
let searchurls = [
"https://linux.do/search?q=", // LINUX DO
"https://www.reddit.com/search/?q=", // reddit
"https://www.zhihu.com/search?q=", // 知乎
"https://search.bilibili.com/all?keyword=", // Bilibili
"https://zh.wikipedia.org/wiki/", // Wiki-CN
"https://wikipedia.org/wiki/", // Wiki-EN
];
searchurls.forEach(url => { window.open(`${url}${tail}`); })
// 2️⃣specialurls -> 打开网站之后需要手动复制 sel 进行查找
// Special - 一些很有价值的搜索和数据挖掘产品
const { clipboard } = require('electron');
clipboard.writeText(sel);
let specialurls = [
"https://websets.exa.ai/", // Exa Websets - 后期需要 money
];
specialurls.forEach(url => { window.open(`${url}`); })
// 最终都不改动 sel
return sel;
}, options: "vt"},
清除链接
“清除 link 保留 text” 的难度仅仅是在正则表达式上有点难度,其他都还好
我是经常使用
d
触发器,因此功能比较冗杂,将就看吧。
{trigger: "d", replacement: (sel) => {
if (sel = sel.replaceAll(/\$(\d.*?)\$/g, "\${{ }}\$$")) ; // sensitive_math
let count = 0;
// highlights
const prefix_highlight = /<mark style="background: #[0-9a-fA-F]{6,8};">(.*?)/g;
const suffix_highlight = /(.*?)<\/mark>/g;
if (sel.match(prefix_highlight)) { sel = sel.replaceAll(prefix_highlight, ""); count ++; }
if (sel.match(suffix_highlight)) { sel = sel.replaceAll(suffix_highlight, ""); count ++; }
// emojis
// s q f r n x color else
const regex_emoji = /(👉|🌟|🌞|😎|❔|❓|🌀|🌸|🌺|🍁|✨|🔥|💥|✏️|✍️|✒️|((✅|⛔))|🔴|🟠|🟡|🟢|🔵|🟣|🤔|🤗|😇|🥰|🍂|🍃)+/g;
if (sel.match(regex_emoji)) { sel = sel.replaceAll(regex_emoji, ""); count ++; }
// links
let pretext_4_link = /(?:📜|📌|🌐)(?:ref|rel|via)\: /g; // 还需要优化名称
const outlink = /\[([^\]]*?)\]\(([^\)]*?)\)/g;
const inlink_1 = /\[\[([^\]]*?)\|([^\]]*?)\]\]/g;
const inlink_2 = /\[\[([^\]]*?)\]\]/g;
if (sel.match(pretext_4_link)) { sel = sel.replaceAll(pretext_4_link, ""); count ++; } //
if (sel.match(outlink)) { sel = sel.replaceAll(outlink, ""); count ++; } // []()
if (sel.match(inlink_1)) { sel = sel.replaceAll(inlink_1, ""); count ++; } // [[|]]
if (sel.match(inlink_2)) { sel = sel.replaceAll(inlink_2, ""); count ++; } // [[]]
// formats
const format_b = /\*\*(.*?)\*\*/g;
const format_h = /==(.*?)==/g;
const format_i = /\*(.*?)\*/g;
const format_a = /\%\%\s*(.*?)\s*\%\%/g;
if (sel.match(format_b)) { sel = sel.replaceAll(format_b, ""); count ++; } // **bold**
if (sel.match(format_h)) { sel = sel.replaceAll(format_h, ""); count ++; } // ==highlight==
if (sel.match(format_i)) { sel = sel.replaceAll(format_i, ""); count ++; } // *italic*
if (sel.match(format_a)) { sel = sel.replaceAll(format_a, ""); count ++; } // %% annotation %%
// if (!count) return false;
return sel;
}, options: "vtA"},
差不多是这些。