编者注:本文涉及到 Drafts 5、URL Schemes 和 Launch Center Pro 相关知识。如果你对以上三种工具尚不熟悉,可以通过以下文章有所了解:

本文基于 Drafts 5 所写,如果你在使用 Drafts 4,请跳转至这篇文章阅读。 


在生活中我们会有各种需要处理的碎片化文本信息。比如某句我很感同身受的语录,我想把它摘抄到笔记里,又或者是我今天看了某部很有意义的视频、电影,我想把它做个记录,以后我要引用这部视频、电影的时候,可以快速找到并分享。

在这些情况下我会向 Bear 笔记中追加这些内容,如何快速收集整理这些数据,就成了我需要去处理的问题。我制作了一个 Drafts 动作(点击安装)来高效地追加笔记。下面我们就去看看这个动作是如何⼀步步制作出来的。

注:同样的思路也可以用在其他支持 URL Scheme 的笔记软件中。例如同样对 URL Scheme 支持较全的印象笔记,只要把 URL Scheme 换成印象笔记的格式即可。

选择笔记

既然是要追加内容,显然使用 URL Scheme 会比手动选择笔记粘贴会快不少。最终实现的效果可以通过观看这个视频先对它先有个大致的了解。

 

观看完效果演示视频,现在要开始实现视频中的功能了。首先需要去查询一下 Bear 的 URL Scheme。我一般喜欢使用 Launch Center Pro(下面简称 LCP)去查询软件的 URL Scheme,如果你没有安装 LCP ,同样可以查阅 Bear 官方文档,了解到这些 URL Scheme 参数。

此处用 LCP 查询 Bear 的 URL Scheme,可以看到 Append to Note 可以传入三个参数实现功能,分别是 Title、ID、Text。其中 Title 和 ID 属于二选一的参数,Text 属于必要参数。在标题名字不冲突的情况下,选用 Title 作为标志一条笔记的方式会更方便,所以就得到了基础的追加内容的 URL Scheme:

bear://x-callback-url/add-text?title=笔记标题&text=文本内容&mode=prepend

但是可以看到这条 URL Scheme 中包含两个参数:「笔记标题」和「文本内容」,难不成每次要追加到某条笔记的时候,还要输入笔记标题名字才行吗?显示这并不是一种优雅的解决方案。好在 Drafts 也提供了列表选择功能,可以预先设定一系列的笔记标题,使用的时候直接选择即可追加内容到指定笔记。

下面开始在 Drafts 中新建一个带有列表选择功能的动作:打开 Drafts 在动作列表栏的右上角找到「+」选择「New Action」:

接着在 IDENTIFICATION 处填写动作名称,例如图中的「笔记追加内容」,接着点击「0 steps」滑到动作库底部选择 「Script」:

点击左上角的 Edit 进入 JavaScript 代码编辑界面:

E82CBB80-5A65-4E13-A8B6-BE5ECABA379B_1_105_c

在 Drafts 5 中可以使用 JavaScript 代码构建一个选择列表弹窗,这个 Action 可以使用这个弹窗列表选择不同的笔记进行追加内容。根据 Drafts 5 官方文档 中给出的实例,可以构建出如下所示的选择列表弹窗代码:

var p = Prompt.create();  // 创建一个列表弹窗
p.title = "请选择笔记";     // 设置列表弹窗标题
p.addButton("体重管理")  // 增加选择按钮
p.addButton("推特存档")  // 增加选择按钮
p.addButton("灵感清单")  // 增加选择按钮
p.addButton("生活日记")  // 增加选择按钮
p.addButton("生活随记")  // 增加选择按钮
p.addButton("人际 👬:关系日记")  // 增加选择按钮
p.addButton("分类 📰:时事概括")  // 增加选择按钮
p.addButton("分类 😴:梦境日记")  // 增加选择按钮
p.addButton("学习 💡:课外知识")  // 增加选择按钮
p.addButton("学习 💬:生财有术")  // 增加选择按钮
p.addButton("学习 💬:语录摘抄")  // 增加选择按钮
p.addButton("日记 ✅:成功日记")  // 增加选择按钮
p.addButton("人际 ❤️:恋爱日记")  // 增加选择按钮
p.addButton("分类 🎬:电影记录")  // 增加选择按钮
p.show();  // 显示列表选择弹窗

保存编辑好的代码,到此第一步的列表选择弹窗代码就构建好了,可以看出只要使用 create title addButtonshow 这四个函数就可以实现这个简单的选择功能。

返回动作列表,点击一下这个刚刚创建好的动作,就会弹出如下所示的选择列表。只要选择了任意一个笔记之后,选择的内容就会被赋值给 p.buttonPressed 这个变量中,并在接下来使用 JavaScript 进行格式化处理。

FBFACDA3-7D8D-4CD3-87BB-739925B6A80A_1_105_c

用 JavaScript 代码统一文本样式

为什么需要使用到 JavaScript 代码来处理文本?因为这些追加的笔记内容部分来自我的个人生活,另一部分来源于网络,而网络上不同人写的文字排版风格不尽相同,以某篇文章对断舍离定义为例,文章中是这么写的:

“断舍离”这个词,是由日本女作家山下英子提出的。
“断”是指断绝不需要的东西,“舍”是指舍弃多余的废物,“离”是指脱离对物品的执念。

但是为了统一 Bear 笔记中的文章排版,可以使用直角引号来替换所有的半角引号,那么在 Bear 中看到的内容就应该是这样的:

「断舍离」这个词,是由日本女作家山下英子提出的。
「断」是指断绝不需要的东西,「舍」是指舍弃多余的废物,「离」是指脱离对物品的执念。

我们就可以使⽤ JavaScript 来实现这个格式化⽂本的功能。

代码整体设计思路:

  1. 获取文稿内容
  2. 格式化文本
  3. 格式化笔记标题
  4. 定义标签

获取文稿内容

// 默认内容来自文稿 否则从剪切板读取
if (draft.content) {
  text = draft.content;
} else {
  text = app.getClipboard();
}

首先使用了 Drafts 自带的 draft.content 函数,它的作用是就是获取当前文稿的内容。而 if (draft.content) 有编程基础的读者就可以看出来这是判断 draft.content 是否为空的语句,等价于 if (draft.content != null)。如果 draft.content 有内容,就把内容赋值给变量 text,如果文稿没有任何内容就同样使用 Drafts 自带函数 getClipboard() 把剪切板内容赋值给变量 text。
至此,实现了一个文本来源的复用,让这个动作既能接收来自文稿的内容也可以接收来自剪切板的内容并将内容赋值给 text 为后续文本格式化做准备。

格式化文本

格式化文本可以分为两种情况:首尾空格删除、符号替换删除。由于摘抄的文本多数都是单句,而有一些情况下行首或者行尾会出现一些多余的空格,这时候就可以使用 JavaScript 自带的 trim 函数进行删除。

text = text.trim();

 

trim 前:     这是一段文本
trim 后:这是一段文本

接下来就是常规的符号替换删除,例如将 # 删除防止 Bear 将它错误地识别为标签记号,这常见于公众号文章开头的文章分类,比如:

收录于话题
#职场 #沟通

# 删除完了之后就是紧接着删除直引号、替换全角引号为直角引号,格式化文本模块代码如下所示,大家可以根据自己的实际需求添加其他的删除或者修改点。

// 替换中文引号 删除井号空格
text = text.replace(/#/g,'');
text = text.replace(/"/g,'');
text = text.replace(/“/g,'「');
text = text.replace(/”/g,'」');

在摘抄某些文章的内容时,可能会有一种特殊的红色横线空格出现,如下图所示:

这个红色一横的空格本质上是 U00A0 不间断空格,这与键盘上的空格 (U0020) 看起来一模一样,但却不是同一个字符,是无法通过键盘直接打出来的。因此在这里使用了 JavaScript 的 Unicode 替换方法,在前面加上 \ 之后再用方括号括起来,让 JavaScript 将它当做 Unicode 进行替换处理,所以得到代码: 

text = text.replace(/[\u00A0]/g,'');

格式化笔记标题

在前面的添加 Buttons 内容的时候,你可能会注意到我在各个笔记标题签名加了分类和 emoji:

p.addButton("人际 👬:关系日记")  // 增加选择按钮
p.addButton("分类 📰:时事概括")  // 增加选择按钮
p.addButton("分类 😴:梦境日记")  // 增加选择按钮
p.addButton("学习 💡:课外知识")  // 增加选择按钮
p.addButton("学习 💬:生财有术")  // 增加选择按钮
p.addButton("学习 💬:语录摘抄")  // 增加选择按钮
p.addButton("日记 ✅:成功日记")  // 增加选择按钮
p.addButton("人际 ❤️:恋爱日记")  // 增加选择按钮
p.addButton("分类 🎬:电影记录")  // 增加选择按钮

那么为什么我要这么做?因为凭我个人经验来看,人眼对图片比文字有更快的识别速度,在这 10 个笔记里找到一个笔记,直接对着 emoji 找比对着文本找会稍微快一些。

但是我在 Bear 中存储的笔记标题里可没有 emoji,所以你可以看到我用 将 emoji 和标题做了间隔,这样我就可以很方便地使用 JavaScript 删掉 之前的分类和 emoji 了,代码如下:

var note = p.buttonPressed.replace(/^.*:/,'');

此处需要解释一下 p.buttonPressed 的来源,它就是刚才我们在列表选择弹窗选择之后赋值的变量。在它的后面使用 replace 函数替换从开头到冒号的内容为空即可得到 之后的正确标题。

这是一句正则表达式,如果你不了解正则表达式可以通过王树义老师的这篇文章快速入门:《如何用 Python 和正则表达式抽取文本结构化信息》

定义标签

到了编写代码的最后一步:定义标签。

在刚刚的处理过程中定义了变量:text 和 note。但是在 Drafts 的 URL 模块中它不认识谁是 text,谁是 note。所以在这里要定义一下这两个变量,让下一步能够顺利地传递参数。定义参数的方法也很简单,使用 draft.setTemplateTag('标签', 变量) 即可,代码如下:

draft.setTemplateTag("text", text);
draft.setTemplateTag("title", note);

完整代码

var p = Prompt.create();  // 创建一个列表弹窗
p.title = "请选择笔记";     // 设置列表弹窗标题
p.addButton("体重管理");  // 增加选择按钮
p.addButton("推特存档");  // 增加选择按钮
p.addButton("灵感清单");  // 增加选择按钮
p.addButton("生活日记");  // 增加选择按钮
p.addButton("生活随记");  // 增加选择按钮
p.addButton("人际 👬:关系日记");  // 增加选择按钮
p.addButton("分类 📰:时事概括");  // 增加选择按钮
p.addButton("分类 😴:梦境日记");  // 增加选择按钮
p.addButton("学习 💡:课外知识");  // 增加选择按钮
p.addButton("学习 💬:生财有术");  // 增加选择按钮
p.addButton("学习 💬:语录摘抄");  // 增加选择按钮
p.addButton("日记 ✅:成功日记");  // 增加选择按钮
p.addButton("人际 ❤️:恋爱日记");  // 增加选择按钮
p.addButton("分类 🎬:电影记录");  // 增加选择按钮
p.show();  // 显示列表选择弹窗
var note = p.buttonPressed.replace(/^.*:/,'');

// 默认内容来自文稿 否则从剪切板读取
if (draft.content) {
  text = draft.content;
} else {
  text = app.getClipboard();
}

// 首尾空格删除
text = text.trim();

// 替换中文引号 删除井号空格
text = text.replace(/#/g,'');
text = text.replace(/"/g,'');
text = text.replace(/“/g,'「');
text = text.replace(/”/g,'」');
text = text.replace(/[\u00A0]/g,'');

// 定义标签
draft.setTemplateTag("text", text);
draft.setTemplateTag("title", note);

构造 URL Scheme

通过前面两步的选择笔记和格式化处理,已经定义完了 title 和 text 这两个变量。接下来就要结合查询到的 URL Scheme 进行构造,只要将两个定义好的标签使用规定的 [[]] 括起来即可。

原始:bear://x-callback-url/add-text?title=笔记标题&text=文本内容&mode=prepend

构造:bear://x-callback-url/add-text?title=[[title]]&text=[[text]]&mode=prepend

在动作里再添加 URL 模块,将构造好的 URL Scheme 粘贴进去即可,到此为止就完成了笔记追加内容动作雏形的构建。

 

如上图所示我依次追加了三条内容到灵感清单里,但是可以看到它们之间是没有空行进行分隔的,也没有添加的时间戳。那么接下来就要在 URL Scheme 上做点文章了。有两个功能需要实现:

  1. 两次追加内容之间添加换行间隔
  2. 每次追加内容的开头添加时间戳

URL Scheme 参数添加

回看刚才构建好的 URL Scheme,它长这样:

bear://x-callback-url/add-text?title=[[title]]&text=[[text]]&mode=prepend

首先为了实现两次追加内容之间添加换行间隔,需要在 [[text]] 参数之后添加一个换行,当然这可不是键盘上 return 的那个换行,在 URL Scheme 里添加换行就要按照 URL 的规定对换行符进行编码之后,将编码好的字符添加进去才行。换行符在 URL encode 之后的结果是 %0A,将其添加到 URL Scheme 里之后得到:

bear://x-callback-url/add-text?title=[[title]]&text=[[text]]%0A&mode=prepend

接下来实现每次追加内容的开头添加时间戳,由于 Drafts 自带 [[time]] 标签(下图红框处),可以很方便地调用这个标签来添加时间戳,新增的参数如下图淡蓝色高亮所示:

除了 [[time]] 之外还在后面添加了 %0A,用途就是添加了时间戳之后换行接上正文。而前面的 -%20 就是标准的 Markdown 列表记号,%20 是空格进行 URL 编码之后得到的结果,用直观一点的表达形式就是:

- 时间
正文内容

到此,URL Scheme 参数初步升级,再试试看新的内容追加效果如何:
 

可以看到,这次追加的效果就比前三条追加的内容好多了,阅读起来也更加方便,还能清楚地知道是在何年何月何日追加的这条内容。但是也可以看到时间戳之间的日期是用 - 进行分隔的,这样看实在是有点不那么漂亮。所以接下来还要对 [[time]] 参数对显示效果进行稍微的调整。

具体的配置方法1就是在 [[time]] 里面用半角竖线间隔开之后,在后面添加你自己想要的格式化符号。举个例子:[[time|%Y-%m-%d %-H:%M %A]] 对应着:年-月-日 时-分 星期,请注意在这里的空格是不需要使用 %20 来表示的,构造后完整的 URL Scheme 如下所示:

bear://x-callback-url/add-text?title=[[title]]&text=-%20[[time|%Y-%m-%d %-H:%M %A]]%0A[[text]]%0A&mode=prepend

另外,如果你想把 - 直接写成「年月日」来表示,我已经帮你尝试过行不通的,目前这种格式就可以满足使用需求了。

自动返回

上面的功能都设计完成后还有一个地方可以优化一下,就是在 Bear 追加完笔记之后自动返回 Drafts。熟悉 URL Scheme 的读者们都能想到这只要在 URL Scheme 后面加一个 x-success 参数即可实现这个功能。最终可以得到下面这个 URL Scheme:

bear://x-callback-url/add-text?title=[[title]]&text=-%20[[time|%Y-%m-%d %-H:%M %A]]%0A[[text]]%0A&mode=prepend&x-success=drafts5://

 


可以看到现在的笔记内容时间戳都是比较易读的,而且也实现了追加完笔记内容之后自动返回 Drafts,至此 Bear 笔记追加内容的插件基本功能实现完成。

小结

我们使用 Drafts 自带的 prompt 功能实现了选择追加笔记的功能,使用 JavaScript 格式化了输入的文本,最后构造了一个稍微有点复杂的 URL Scheme 一共三个步骤实现了笔记追加内容的功能。技术点涉及 JavaScript、NSDateFormatter 和 URL Scheme。
在下一篇的文章中,我们将讨论在 JavaScript 代码中玩一些花样,实现更多的功能,敬请期待。

本文中的所有代码都可以在 Apple-Automation/Drafts 中找到,如果方便希望能帮我 star 一下,如果有任何需求或者 bug 也可以通过 issue 报给我。