【安利】个人在 Obsidian 中关于「网址」相关的 LaTeX Suite 触发器

说在前面

个人感觉,关于网址,当光标放在 ) 的后面之后,视觉上会从 text 拓展成为 [text](link),这一点是我觉得很烦躁的。尤其是当 text 中出现 \_ \| 或者是 link 中出现 &direct=... &utm_campaign=... 的时候,这个展开会很长,很影响注意力。

关于这些文本如何消除,这是另一个话题,按下不表;但就算是 textlink 可以得到简化,我也觉得在视觉上从 text 编程 [text](link) 多少有点不必要。

因此一种解决方式是改用 [text](link) ,即在后面添加一个空格,因此相关的触发器都是在这个基础上实现的,具有一定个人喜好:

  • 当检测到 (link) c 时,触发“复制链接”功能
  • 当检测到 (link) v 时,触发“访问链接”功能

在编写文档的时候和“网络”相关的操作无非一下几个:

  1. 访问网址
  2. 复制网址
  3. 粘贴网址生成链接 [text](link)
  4. 搜索内容
  5. 清除链接
    1. 清除 link,但保留 text
    2. 直接清除(删除行即可)

因此在 LaTeX Suite 中写了几个比较常用的触发器。

写这些快捷键的原因是:我不会 vim 的操作,只能左手放键盘上,右手放鼠标上操作。

访问网址 & 复制网址 & 粘贴网址生成链接

t 模式

  1. 对于 purelink,后面添加 c 或者 v 可以复制或访问;
  2. 对于 [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 模式

  • 选中的文本中如果包含以下的内容,都复制下来:
    1. purelink
    2. [text](link)
    3. [[inline link]]
    4. $inline latex formula$
    5. 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"}, 

差不多是这些。