油猴脚本——掘金Markdown格式适配器知识点记录【油猴脚本、Markdown、浏览器文件读取、tooltip、SVG、、模拟用户输入、aria-xxxx属性、剪切板操作、】

油猴脚本——掘金Markdown格式适配器知识点记录

脚本更新日志

参考:掘金Markdown格式适配器更新日志 - 掘金

脚本地址:

更新:2021年9月3日19:57:35

参考:掘金Markdown格式适配器

前言

关于我为什么要写这个脚本可以看下面这篇我写的吐槽(或者说是问题描述)博客:

参考:掘金Markdown编辑器问题描述 - 掘金

而我在本地的Markdown编辑器(typora)的写作习惯就是:

  • ==xxx== 来高亮文本;
  • <center></center> 来居中文本;
  • 关于图片居中,typora中是自动居中的,不用我操心,而CSDN的Markdown编辑器中则是在图片后添加#pic_center 来居中的。
  • 设置文本的颜色和大小之类的都是通过 <font color="gray" size="2px">大小为2px的灰色文本</font> 或是 **<font color="red" size="2px">大小为2px的加粗红色文本</font>** 之类的方式;
  • 设置按键格式都是通过 <kbd>按键</kbd>
  • ……

我整理了一下掘金的Markdown编辑器、CSDN的Markdown编辑器和typora中对于某些Markdown语法和HTML标签或属性的支持程度:

Markdown语法、
HTML标签或属性
功能typoraCSDN掘金
==xxx==高亮文本支持支持不支持
<mark>xxx</mark>高亮文本支持支持支持
style 属性设置样式支持不支持不支持
<font color=gray size=2></font>设置文字
大小和颜色
支持支持不支持
<center>xxx</center>居中文本支持支持不支持
<div align="center">xxx</div>居中图文支持支持支持
<div color="gray">xxx</div>设置文字样式不支持支持支持
<strong>xxx</strong>加粗文本支持支持支持
<strong color="red">xxx</strong>加粗文本并
设置颜色
不支持支持支持
<span color="red">xxx</span>设置文字颜色不支持支持支持
<br>换行支持支持支持
<kbd>xxx</kbd>设置按键样式支持支持不支持
😂 :joy:插入emoji支持支持不支持
…………………………

以上就是我常用的几个用法,可以看到有些是通用的,但是有些却不能全部支持……尤其是掘金不支持用 ==xxx== 来高亮文本这点让我挺头疼的,因为文章里看到一堆的 == 让人一种这文章是不是从哪个犄角旮旯里匆匆复制过来没来得及调格式的错觉……

总之就是掘金的Markdown编辑器对于我的一些常用Markdown书写习惯不够支持,所以我就写了这个适配脚本,来将本地写好的Markdown文档中的一些个人习惯写法改为适配掘金Markdown编辑解析规则的写法。

我真的觉得 <font><center> 标签以及 style 属性在Markdown里很好用,很希望官方能支持这些方便写作的标签和属性😂

脚本的功能比较简陋,但是还是花了不少时间和精力,这几天都连续搞到两三点。里面的正则匹配规则可能会在某些文档里抽风,等遇到了再说吧。目前脚本的功能还是比较够用的,

一、脚本功能描述

其实说实话这些问题其实都是我在本地Markdown编辑器中的书写习惯不能适应掘金的Markdown编辑器所导致的,有些我觉得很好用的小技巧在本地Markdown编辑器(我主要是用typora)或者CSDN的Markdown编辑器中很好使,但是到了掘金编辑器里就不好用了……这就很无语了,只希望掘金官方能有相应的解决方法,毕竟像我这样的小脚本还是不够方便啊。

1、把 ==xxx== 替换为 <mark>xxx</mark>

前提是导入的Markdown文档中有 ==xxx== 。这是针对上面提到的问题的第二点,比如:

==想要高亮的文本==
变为
<mark>想要高亮的文本</mark>

2、把 <center>xxx</center> 替换为 <div align="center">xxx</center>

前提是导入的Markdown文档中有 <center></center> 。这是针对上面提到的问题的第一点,比如:

<center>想要居中的文本或元素</center>
变为
<div align="center">想要居中的文本或元素</center>

3、把 ![](图片链接) 替换为 <div align="center"><img src="图片链接"></center>

前提是导入的Markdown文档中有 ![](图片链接) 。这是针对上面提到的问题的第一点,比如:

![想要居中的图片](图片链接)
变为
<div align="center"><img src="图片链接"></div>

【更新:2021年9月1日01:01:37】

还可以把图片描述追加在图片下方,格式参照【2】。

![图片描述](图片链接)
变为
<div align="center"><img src="图片链接"></div>
<div align="center" color="gray">图片描述</div>

4、(伪)导入本地文件

点击相应的脚本图标,可以选取本地 .md 文档,并对文档做此前提到的三点处理,然后将处理后的文本内容写入剪贴板。用户需要手动按下 Ctrl + V 来将剪切板里的内容放到相应的编辑器输入框中,让掘金编辑器对其进行解析。

5、自动填充文章标题

自动获取导入的Markdown文档中的第一个一级标题并填入文章标题输入框。

更新:2021年9月3日02:45:31

这里出现了一个bug,虽然表面上有了自动填充标题的效果,但是实际上还是会出问题,具体的描述请看下方的【知识点记录-15、模拟用户输入】。

更新:2021年9月3日19:04:05

突然想到,要是文档里没有一级标题,那岂不是又会犯之前碰到的匹配项为空的错误。所以应该加上对一级标题的存在性判断,当文档中不存在一级标题时,就截取文档的前十个字符作为标题,不满十个的话就直接把全部内容当做标题(话说不满十个字还发啥博客呀……)。

6、最终效果演示

脚本最终效果演示

二、知识点记录

1、脚本中用到的正则表达式

这个脚本的核心功能就是靠正则表达式来实现的。以下是我在脚本中用到的几个正则:

需求正则
a. 匹配由成对 == 包裹的文本/==[^==]*==/g
/==(?:(?!(==)).)*==/g
/==.*?==/g(我最后采用的)
b. 匹配由 <center>xxx</center> 包裹的文本/<center>.*<\/center>/g
c. 匹配Markdown图片链接 ![]()/!\[.*\]\(.*\)/g
d. 匹配文档中的一级标题 #/#\s.*/g
1.1、实现非贪婪或最小匹配

在需求【a】中,我一开始使用的是 /==.*==/g,结果发现有些匹配结果和预期的不一样,比如下面这段:

并且把 `interceptors.request` 中保存的请求拦截器回调函数==成对==地压入 `chain` 数组的==头部==(通过数组对象的 `unshift()` 方法);把 `interceptors.response` 中保存的响应拦截器回调函数==成对==地压入 `chain` 数组的==尾部==(通过数组对象的 `push()` 方法)。

在这段文字中,按预期应该是有四个匹配结果:==成对====头部====成对====尾部==

但是上面的正则只会匹配一个结果:==成对==地压入chain数组的==头部==(通过数组对象的unshift()方法);把interceptors.response中保存的响应拦截器回调函数==成对==地压入chain数组的==尾部==

也就是说,/==.*==/g 这个正则会把 ==成对== 的前面那个 ====尾部== 的后面那个 == 当成一对,而忽略了中间的 == ,显然这不是我们想要的结果。所以,我把 /==.*==/g 改成了 ① 的写法 /==[^==]*==/g。这个正则的意思就是找到由 == 包裹的文本,且该文本中不能再包含 ==。这样一来就算是满足了我的需求。

后来,我又在一篇博客里看到了一个更加通用的办法:

更新:2021年8月29日21:37:33

参考:正则表达式:不包含某些指定的单词(超级难的正则式,前无古人哦)_邢晓宁专栏-CSDN博客

于是顺着这个博主的思路,我写出了②/==(?:(?!(==)).)*==/g,也能满足我的需求。

我一开始在这里走了很多弯路,然后在菜鸟教程的正则教程里看到了这么一段:

更新:2021年9月1日14:36:46

参考:正则表达式 – 语法 | 菜鸟教程

菜鸟正则教程-最小匹配-QQ截图20210902172007

于是顺着这个思路,我又写出了③/==.*?==/g,感觉这个才是在逻辑上最直接地满足了我的需求。

另外,顺便推荐一下菜鸟教程的在线正则工具,感觉挺好用的:

参考:正则表达式在线测试 | 菜鸟工具

正则这一块还是花了我很多精力的,一番探索下来,深感其中的不易啊……(此处纪念一下我牺牲的头发😂)

1.2、正则表达式构造函数

创建一个正则表达式有字面量和构造函数两种方式。因为一开始我没有封装函数,所以是直接用的字面量。到了封装函数的时候,字面量就显得不够灵活了。于是就用上了 RegExp() 这个构造函数。详细用法可以参看下方官方文档:

更新:2021年9月1日22:39:47

参考:RegExp(正则表达式) - JavaScript | MDN

这里我只提我遇到的一个坑:

image-20210902174605536

比如,对于图片链接的匹配,如果我想通过 RegExp() 构造函数的方式来创建正则表达式,那么就要像下面这样传入参数:

let reg = '!\\[.*\\]\\(.*\\)'
let replacement = target.match(new RegExp(reg, 'g'));
1.3、匹配正则开头和结尾以及换行符

在需求【d. 匹配文档中的一级标题 #】中,我一开始写的正则是 /^#\s.*\n$/g,想表达的是:匹配的文本必须以 # 开头,且 # 后紧跟着一个空格,且必须以一个换行符结尾。

本以为是合理的,但是却一直无法通过测试,而且我在那个菜鸟在线正则工具中的匹配结果和我在vscode里用正则匹配的结果还有差别,最后放到代码里执行也报错,原因是文本中无法匹配到符合要求的文本……大半夜的,我以为自己的脚本功能马上就要完成了,就硬生生卡在这里一个小时左右……人都麻了。

后来我通过搜索和排查,发现似乎是 \n^$ 这三个地方出了问题。最后在一番调调改改之下,总算是让代码实现了我想要的功能(虽然到最后也没怎么整明白为啥会这样……再次感叹,正则真的是个大坑啊……)。

1.4、正则在匹配的原内容上添加(拼接)文本

这个发现和脚本没有什么大关系,但是我感觉非常的有意思,也挺实用的,所以也在此做一些记录。

比如我要在下面这组数据的每一项后面都添加 ,100

#chara 2,cgm_kt13
#chara 2,cgm_fd22
#chara 2,cgm_gy75

那么只要让正则表达式为:(#chara 2,cgm_\w+)。再让替换的表达式为:$1,100即可:

#chara 2,cgm_kt13,100
#chara 2,cgm_fd22,100
#chara 2,cgm_gy75,100

在正则表达式中,放在圆括号中的是分组,按括号出现顺序可用\1,\2…\9(或$1,$2…$9)引用 整个正则用\0或$​0引用因此,替换中\1引用了括号中匹配的内容,然后加上要添加的字符。

①正则表达式中在多个匹配的内容中间加字符(在浏览器中测试过一遍后感觉有点搞不明白):

let selectedText = selectedText.replace(/(a)(b)(c)(d)(e)(f)(g)(h)(i)/g,'$1- $2- $3- $4- $5- $6- $7');

②正则表达式在行首添加指定内容:

​ 匹配字符:^(.+)$ ——代表匹配任意行首

​ 替换字符:a$1 ——代表在上面的匹配结果前加一个字符a

给CSDN中的图片加上 #pic_center 来让图片居中(亲测有效,需要在CSDN编辑器中按下Ctrl + G开启搜索替换):

​ 匹配字符:(.*\.png|.*\.jpg|.*\.gif|.*\.jpeg|.*\.webp|.*\.svg|.*\.bmp|.*\.ico)——匹配所有的图片链接

​ 替换字符:$1#pic_center——在所有图片链接后添加 #pic_center 后缀

// 待匹配字符串
![插件效果1](https://img-blog.csdnimg.cn/79f13197e56845a08c2e3e8536cece19.png)

// 匹配结果
![插件效果1](https://img-blog.csdnimg.cn/79f13197e56845a08c2e3e8536cece19.png

// 替换结果
![插件效果1](https://img-blog.csdnimg.cn/79f13197e56845a08c2e3e8536cece19.png#pic_center)

更新:2021年9月1日21:09:41

参考:正则表达式中,如何在任意匹配字符后面加上原字符和特定内容_百度知道

2、Node.js 中的 util.promisify()

我最开始并没有在浏览器环境中测试脚本功能,而是在Node.js环境中进行功能的实现和测试。此间,我用到了Node.js内置文件操作模块 fs 读写文档的相关功能。后来又使用Node.js中的 util.promisify() 封装了读写文件的相关异步操作。

util模块是Node.js中的内置模块,util.promisify()函数可以将一个采用错误优先风格的回调函数包装成另一个函数,且包装后的函数的返回值为Promise类型。

官方文档中的描述:

采用遵循常见的错误优先的回调风格的函数(也就是将 (err, value) => ... 回调作为最后一个参数),并返回一个返回 promise 的版本。

参考:util.promisify() 实用功能

比如Node.js中fs模块的 readFile(path, (error, data) => {}) 函数就是典型的错误优先的异步操作。那么就可以通过下面的操作将 readFile() 函数包装为一个返回值为Promise对象的函数。writeFile() 函数也是同理。

const fs = require('fs');
const util = require('util');
const myReadFile = util.promisify(fs.readFile);
const myWriteFile = util.promisify(fs.writeFile);

myReadFile('./测试文档.md')
  .then(data => {
    let content = data.toString();
    ......	// 正则处理逻辑
    // console.log(content);
    myWriteFile('./processed.md', content);
  }, error => {
    console.warn(error);
  })

有关Promise的知识可以参看我之前写的学习笔记:

参考:Promise学习笔记(万字长文)【手写Promise;异步编程;Promise的API;异常穿透;链式调用;async和await】 - 掘金

3、window.getSelection() 获取鼠标选中的文字

其实在决定使用读取本地文档的方案前,我是想通过鼠标选中编辑区中想要修改的一小部分文本,然后按下自定义的快捷键后,该选中的文本内容就能按照规则进行替换。所以稍稍的研究了一下 Selection 这个对象。

更新:2021年9月2日19:11:27

参考:Selection - Web API 接口参考 | MDN

参考:关于window.getSelection_xiaoxu的博客-CSDN博客

参考:【JavaScript】获取到选中的文字、复制选中的文字_汪小穆的博客-CSDN博客

参考:【Javascript】获取选中的文字 - SegmentFault 思否

参考:【JavaScript】获取到选中的文字、复制选中的文字_汪小穆的博客-CSDN博客

参考:JS 之 文本操作 copy复制和选中文本_前端看世界-CSDN博客

参考:javascript中如何获得选中文字所在的节点-CSDN社区

但是,很快这个方案就被我放弃了,毕竟要自己一个个找要修改的文本的话太费眼睛了,一顿操作下来反而是给自己找麻烦。

4、文件读取接口及文件对象(input 标签和 FileReader 对象)

JavaScript为浏览器读取本地文件提供了一系列接口。

当我们想要通过浏览器从本地读取文件时,往往是要用到 <input type="file"> 这个标签,注意其 type 值为 file。当我们点击相应的按钮时,浏览器将会调用一个和我们当前操作系统内置的文件选择窗口的UI风格一致的窗口以让用户选择相应的本地文件。于此同时,input 标签的 change 事件将被触发,且我们可以通过 event.target.files 来获取所选择的文件列表,其类型为 FileList,其中的每一项都是一个 File 类型的文件对象。

我们还可以通过设置 input 标签的 accept 属性值来限制所接受的文件类型。

<input type="file" id="input" accept=".md, .txt, .docx">

在获取了 File 对象之后,我们就可以通过 FileReader 对象来操作我们获取的文件内容。

if (window.FileList && window.File && window.FileReader) {
  document.getElementById('input-file').addEventListener('change', event => {
    const file = event.target.files[0];
    const reader = new FileReader();
    reader.addEventListener('load', event => {
      // 这个 event.target 就是我们创建的那个FileReader对象
      let content = event.target.result;	
			.....
    });
    // reader.readAsDataURL(file);
    reader.readAsText(file);
  });
}

以下是 MDN文档:FileReader - Web API 接口参考 | MDNFileReader 的相关API的描述:

接口功能
FileReader.result(只读)文件的内容。该属性仅在读取操作完成后才有效,数据的格式取决于使用哪个方法来启动读取操作。
FileReader.readAsDataURL(file)开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个data: URL格式的Base64字符串以表示所读取文件的内容。
FileReader.readAsText(file)开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个字符串以表示所读取的文件内容。

现在我们已经可以获取指定的文档了,并且是以字符串的形式。

更新:2021年8月30日01:06:11

参考:Read files in JavaScript

参考::输入(表单输入)元素 - HTML(超文本标记语言) | MDN

参考:在web应用程序中使用文件 - Web API 接口参考 | MDN

参考:FileReader - Web API 接口参考 | MDN

参考:关于input type=“file”的及其files对象的深层探究_lianzhang861的博客-CSDN博客

参考:JS学习–用JS读取本地文件_殷硕的专栏-CSDN博客

参考:不使用file类型input也能触发文件上传 « 张鑫旭-鑫空间-鑫生活

参考:input[type=“file”]上传文件原理详解_懒虫翻身的博客-CSDN博客_input type=file

参考:前端文件上传原理_weixin_42600496的博客-CSDN博客

5、input标签样式的自定义

但是原生的 input 标签的样式不够美观,所以需要对其进行一番乔装打扮。但是 input 标签可供的样式设置比较受限,还是不能满足我的需求。所以就有了下面的做法。

总体思路就是给 input 标签配一个对应的 label 标签。通过设置 labelfor 属性和 inputid 属性一致来达到绑定两者的效果。

<label for="file-input">label</label>
<input type="file" id="file-input"  style="display: none;"/>

当然,为了美观,还应当设置 input 标签的 display: none 。这样页面中就只能看到 label 标签,但是当我们点击 label 标签时,却能触发 input 标签的相关事件,相当于就是点击了 input。随后就可以按自己的需求来设置 label 的样式了。

当然,还可以通过给 input 标签覆盖一个 div 或是 img 之类的元素,并且把 input 的透明度降为 0,但是要注意让 input 标签的层级在其覆盖元素之上(设置 z-index 值),这样才能保证点击相应位置时能触发读取文件的操作。其主要思想是:把 input(position:absolute)使用一个 div(position:relative)包装起来,然后再将input 给隐藏起来,那就实现了修改样式的效果,其实并非真正修改了 input 的样式,而是利用 wrap 的样式遮盖了input

更新:2021年8月30日02:01:33

参考:怎么覆盖默认样式_巧妙利用label标签实现input file上传文件自定义样式_weixin_39761645的博客-CSDN博客

参考:如何给file类型的input标签,自定义样式 - Shmily-HJT - 博客园

参考:【前端】input file label自定义上传样式_聪明努力的积极向上的博客-CSDN博客

参考:Web前端自定义input按钮样式_lsy_u56的博客-CSDN博客_input按钮样式

参考:自定义input文件上传样式 - huanzi-qch - 博客园

参考:css修改input type=“file” 文件的默认上传upload样式_jo_an_na的博客-CSDN博客

一开始我是根据工具栏中的其他图标样式对 label 的样式进行自定义的,但是总归是有差别的,虽然是一些细节,但是还是能感受到不一样,但是没办法啊,毕竟没法儿直接获取其它按钮的全部样式,而且有些样式是直接继承来的,想要一层层找的话还是很难的……

后来,我通过开发者工具观察其他按钮的样式时发现每个按钮都有三个共同的 class 属性值。

image-20210902222411352

经过一番试验,我发现只要给脚本生成的 label 标签添加 class="byte-toolbar-icon" 就可让脚本按钮和原本的其他几个按钮样式完全一致。亏我之前还费心费力调样式,到头来直接一个 class 属性就搞定了……当然,这个样式要配合下面提到的SVG图标才能有一样的效果。

6、cursor: pointer 和 background-image不起作用

①在设置样式时,我设置了当鼠标悬浮在元素上方时指针样式变为 pointer,但是实际却没有其作用,后来发现其实是元素的层级没有设置好,导致 hover 时没有触发相应的样式。

②通过 background-image 可以设置元素的背景图,可以通过 url() 来设置目标背景。支持传入本地的图片文件路径(绝对路径和相对路径)和网络图片,也支持 base64 格式的图片。

属性作用
background-image设置背景图片
background-size设置背景图片的大小
background-color设置元素的背景色
background-position设置背景图像的起始位置

更新:2021年9月2日21:05:03

参考:cursor - CSS(层叠样式表) | MDN

参考:background-size - CSS(层叠样式表) | MDN

参考:CSS background-size 属性

参考:关于background-image调整大小和位置的方法笔记 - 林丶 - 博客园

推荐一个字节跳动出品的图表库,感觉挺方便的,可以下载无背景图片,也能直接复制对应的SVG或直接导入到项目中。

参考:ByteDance IconPark

7、插入或创建SVG

一开始我是通过设置背景图片的方式来让我的导入按钮看起来的风格和原本的UI风格差不多。但是因为图片还是无法动态地根据网页背景色来改变其背景色,所以当网页处于黑夜模式时,脚本生成的按钮就会显得非常突兀。所以我决定将原本的图片背景改用SVG来代替,这样脚本按钮就能比较好地融入原本的其他按钮了。

一开始我是在上面提到的那个字节在线图标库里复制了相应的SVG代码,在本地测试用的网页中直接粘贴后也能正确显示,且完美适应暗黑模式。但是当我通过JavaScript尝试将这一整段的SVG代码插入 label 标签中时,却发现图标无法显示。

importLabel.innerHTML = `<?xml version="1.0" encoding="UTF-8"?>
    <svg width="12" height="12" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
      <rect width="48" height="48" fill="white" fill-opacity="0.01"/>
      <path d="M6 24.0083V42H42V24" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
      <path d="M33 23L24 32L15 23" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
      <path d="M23.9917 6V32" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
    </svg>`;

我预期中的效果:

image-20210902213520828

实际上的效果:

image-20210902214416068

嵌套关系自然无法正常显示图标了,经过一番排查,我发现从那个在线网站中复制过来的SVG代码中的 <rect><path> 标签都是自结束标签,但是在本地网页测试的时候又会自动变成成对的闭合标签。所以我就尝试将复制过来的SVG代码由原来的自闭合改成成对的闭合标签,发现成功显示图标。

importLabel.innerHTML = `<?xml version="1.0" encoding="UTF-8"?>
    <svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
      <rect width="48" height="48" fill="white" fill-opacity="0.01"></rect>
      <path d="M6 24.0083V42H42V24" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path>
      <path d="M33 23L24 32L15 23" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path>
      <path d="M23.9917 6V32" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path>
    </svg>`;

其实一开始我是想通过 document.createElement() 函数来创建 <svg></svg> 标签的,但是发现这样创建出来的标签不起作用,后来才知道SVG是基于xml格式的,创建标签节点需要有命名空间,createElementNS()是创建SVG和path元素的必需方法。所以需要把 createElement() 改成 createElementNS()

由于把SVG代码插入 label 内部的做法已经能够满足我的需求了,所以就没有再特意使用JavaScript代码来创建SVG。

更新:2021年9月2日22:00:35

参考:使用JavaScript创建SVG标签_慕课猿问

参考:JS创建SVG的问题 - .why - 博客园

参考:js创建svg元素的方法 - lovollll - 博客园

参考:XML DOM setAttributeNS() 方法

参考:SVG 路径 | 菜鸟教程

8、实现tooltip

在掘金原本的Markdown编辑器工具条中,当鼠标移入时,会有一个提示效果:

掘金Markdown编辑器中的tooltip效果

为了视觉上的和谐统一,我觉得我的脚本按钮也应该搞一个提示效果。于是便开始了tooltip的实现。

限于篇幅,详情可以参看我的另一篇博客:

更新:2021年9月3日18:43:50

参考:tooltip使用经验 - 掘金

9、aria-xxxx属性

在研究上面的tooltip效果时,我反复观察了掘金的tooltip是如何实现的,我发现每次将鼠标移到按钮上时,相应的元素都会动态添加一个 aria-describedby 属性,且它的值都是随着鼠标移入次数的增加而呈现有规律的变化。

掘金Markdown编辑器工具栏tooltip效果

于是我就怀疑掘金的tooltip实现是不是和这个属性有关系。于是我尝试写出了下面的测试代码:

<ul>
  <li aria-describedby="desc1">按钮</li>
</ul>
<p id="desc1">提示</p>
<script type="text/javascript">
  const target = document.querySelector('[aria-describedby="desc1"]');
  const tip = document.getElementById('desc1');
  target.onmouseover = function () {
    tip.style.display = 'block';
  }
  target.onmouseleave = function () {
    tip.style.display = 'none';
  }
</script>

看起来好像是可以实现tooltip的效果,但是总感觉不是这么用的……于是我决定先弄明白这个 aria-describedby 属性到底是干嘛的。后来查到了如下定义:

该属性值为字符串,可以是用空格分隔的多个 id 属性值列表。

该属性定义了文档结构表现不出来的的元素间的相互关联性。该属性旨在通过标签提供更多用户可能需要的信息。如果指定了不只一个id, 所有元素会合并在一起共同创建一条单独的描述。

感觉好像有点关联,但是为什么非得加这么一个属性来实现tooltip呢?我有点搞不懂。因为后来我用其他方式实现了tooltip,所以就没有往这方面再深究了。但是这个 aria-xxxx 属性还是需要留意的。

更新:2021年8月30日21:18:43

参考:ARIA - 无障碍 | MDN

参考:Using the aria-describedby attribute - Accessibility | MDN

参考:无障碍开发(三)之ARIA aria-***属性值_weixin_30510153的博客-CSDN博客

参考:data-container=“body” 是什么意思?-慕课网

10、几个函数的封装

10.1、创建DOM节点对象
/**
 * @description: 该函数用于创建一个<eleName k="attrs[k]">text</eleName>样式的页面元素
 * @param {string} eleName DOM元素标签类型
 * @param {string} text  DOM元素内部文本
 * @param {object} attrs DOM元素属性配置
 * @return {HTMLElement} 返回一个DOM节点
 */
function createEle(eleName, text, attrs) {
  let ele = document.createElement(eleName);
  // innerText 也就是 <p>text会被添加到这里</p>
  ele.innerText = text;
  // attrs 的类型是一个 map
  for (let k in attrs) {
    // 遍历 attrs, 给节点 ele 添加我们想要的属性
    ele.setAttribute(k, attrs[k]);
  }
  // 返回节点
  return ele;
}
10.2、截取两个指定字符串之间的字符串
/**
 * @description: 该函数用于截取目标字符串 target 中在指定的开始子串 startStr 和 结束子串 endStr 之间的字符串
 * @param {string} target  目标字符串
 * @param {string} startStr  开始子串
 * @param {string} endStr 结束子串
 * @return {string} 所截取的子串
 */
function getSubStr(target, startStr, endStr) {
  let startPos = target.indexOf(startStr) + startStr.length;
  let endPos;
  if (startStr === endStr) {
    endPos = target.lastIndexOf(endStr);
  } else {
    endPos = target.indexOf(endStr);
  }
  return target.substring(startPos, endPos);
}
10.3、正则替换文本

脚本中有几处用正则匹配并替换文本的代码有重复性,所以我就对其进行了一些封装。

/**
 * @description: 该函数封装了替换原Markdown文档中相关文本的功能
 * @param {string} target  待处理的目标文本内容
 * @param {string} reg 正则表达式
 * @param {string} wrapperHead 预期的包装头
 * @param {string} wrapperTail 预期的包装尾
 * @param {string} startStr  截取内容时的开始子串
 * @param {string} endStr  截取内容时的结束子串
 * @return {string} 返回处理过的文档内容
 */
function replaceText(target, reg, wrapperHead, wrapperTail, startStr, endStr) {
  let replacement = target.match(new RegExp(reg, 'g'));
  // 一定要对匹配项做存在性判断,否则后面会报错
  if (!replacement) {
    return;
  }
  for (let i = 0; i < replacement.length; i++) {
    let content = getSubStr(replacement[i], startStr, endStr);
    let imgDescription = '';
    // 判断当前匹配的是否为Markdown中的图片,如果是的话,则在其后追加图片描述
    if (reg === '!\\[.*\\]\\(.*\\)') {
      imgDescription = `<div align="center" color="gray">${getSubStr(replacement[i], '[', ']')}</div><br>`;
    }
    replacement[i] = wrapperHead + content + wrapperTail + imgDescription;
  }
  for (let i = 0; i < replacement.length; i++) {
    target = target.replace(new RegExp(reg), replacement[i]);
  }
  // 【更新:2021年9月2日00:53:08】一定要返回处理结果!!!!
  return target;
}

更新:2021年9月1日00:57:37

在测试一篇文档时,控制台突然报出了下面的错:

image-20210901005753231

因为有些文档里可能没有用到某些匹配项,那么相应的正则所匹配到的结果就为 null,读取 null 值的 length 自然会报错。所以一定要事先对匹配项做存在性判断。也就是在替换之前加上这个判断:

if (!replacement) {
  return;
}

更新:2021年9月3日02:50:45

在我尝试测试更多文档时,我发现某个文档突然报了下面的错:

image-20210903025300744
原因很简单,因为我的脚本里第一个正则替换就是针对 == 的,如果文档中没有使用 == 高亮的文本,那么就会在第上面显示的第 65 行返回一个 undefined。那么当匹配下一个正则时,replaceText() 函数里的 target 参数就会是 undefined,所以就报错了。有两种改法:

// 第一种
let replacement = target.match(new RegExp(reg, 'g'));
if (!replacement) {
  return target;
}
......	// 处理文本的逻辑
return target;
// 第二种
let replacement = target.match(new RegExp(reg, 'g'));
if (replacement) {
  ......	// 处理文本的逻辑
}
return target;

11、函数的值传递和引用传递

在封装上面的【10.3】时,我一开始没有将处理后的结果返回,结果导致导入的文档内容完全没有经过处理。经过控制台的逐步调试观察我才发现原来是犯了一个低级错误:没有搞清楚函数的值传递和引用传递。

当我将要处理的文档内容(字符串形式)以参数(也就是上面的 target 参数)的形式传入这个函数,在函数内部确实可以观察到 target 按预期地发生了变化,但是一旦函数结束,函数的处理结果也只是停留在函数内部而已,所以传进去的文档内容相当于没有经过处理,最后的结果自然就是一段没有经过处理的文本内容被写进了用户的剪切板。

所以一定要通过一个返回值来将处理结果暴露给函数外部,并安排一个合适的变量来接收它。这样才能让函数起作用。

函数带出结果的三种方式:

  • 全局变量
  • 函数返回值
  • 地址参数

12、JavaScript剪切板操作

有关剪切板的问题在我的上一个脚本中就有提到过。

参考:复制标题和地址(myFirstScript)

参考:【油猴脚本编写初体验】一键复制网页标题和地址(copy-title-and-location)_赖念安的博客-CSDN博客

有几个经常提到的与剪贴板相关的API、对象或JS库:

  • navigator.clipboard.writeText()navigator.clipboard.write()navigator.clipboard.readText()navigator.clipboard.read()
  • document.execCommand('copy')document.execCommand('paste')
  • copy 事件、cut 事件、paste 事件
  • event.clipboardData 对象和 Clipboard 对象
  • clipboard-write(写权限)和 clipboard-read(读权限)
  • clipboard.js

而对于这个脚本,在上面的功能描述【4】中就提到脚本提供的文件导入功能其实不是真正意义上的“导入”,因为处理过后的内容还需要通过用户自己在编辑区按下 Ctrl + V 才能放到编辑区中。我本来是想获取编辑区中光标所在元素,然后再将文本内容通过 HTMLElement.innerText() 的方式写入编辑区的,但是事情却没有我想的那么简单。因为掘金的Markdown编辑中是根据用户的输入实时渲染的,所以一块块的文本内容都是分布在一个个不同的 <pre></pre> 中的……如果脚本通过 document.querySelector() 的方式选择所谓的编辑区,那么只能往某一个特定的 <pre></pre> 中放入文本内容,也就没有解析效果了……

image-20210903015336941

所以,迫于无奈,只能靠 Ctrl + V 来曲线救国了。

更新:2021年9月3日24:57:04

参考:剪贴板操作 Clipboard API 教程 - 阮一峰的网络日志

参考:Event - Web API 接口参考 | MDN

参考:Element: paste事件 - Web API 接口参考 | MDN

参考:JS:复制内容到剪贴板(无插件,兼容所有浏览器)_小白一个-CSDN博客_js复制到剪贴板

参考:js复制文本到粘贴板(Clipboard.writeText())_★【World Of Moshow 郑锴】★-CSDN博客

参考:Chrome 66 新增异步剪贴板API - 知乎

参考:js获取剪切板内容,js控制图片粘贴。 - SegmentFault 思否

13、触发自定义事件

利用 document.createEvent(eventType)event.initEvent("xxx", false, true)document.body.dispatchEvent(event) 可以触发自定义事件。

更新:2021年9月3日01:34:41

参考:js自动触发事件&&自定义事件 - 简书

参考:Document.createEvent() - Web APIs | MDN

参考:html - How to create copy-paste event in JavaScript? - Stack Overflow

更新:2021年9月3日13:31:07

参考:javascript - How do I programmatically trigger an “input” event without jQuery? - Stack Overflow

参考:input - Web API 接口参考 | MDN

参考:KeyboardEvent - Web API 接口参考 | MDN

参考:KeyboardEvent.key - Web API 接口参考 | MDN

更新:2021年9月3日18:34:24

如果在 addEventListener() 中直接调用了 dispatchEvent() ,那么可能会报错:【Fail to execute ‘dispathEvent’ on ‘EventTarget’: The event is already being dispatched.】,我猜测可能是绑定事件时, addEventListener() 内部的代码执行过一遍了,那就意味着 dispatchEvent() 已经被调用过一次了,而当事件被触发时,又会调用一次 dispatchEvent() ,所以就出现了错误,我用一个定时包裹 dispatchEvent() 之后就不会报错了,但是其逻辑发生了改变,其中原理目前不做研究。

14、substr() 和 substring()

substr()substring() 的区别:

  • str.substr(start [, length] ):返回一个从指定位置开始的指定长度的子字符串。

  • str.substring(start,end ):返回位于String对象中指定位置的子字符串。

更新:2021年9月2日02:49:35

参考:JavaScript substr() 方法

更新:2021年9月2日19:37:06

参考:js实现截取或查找字符串中的子字符串 - icon_sunny - 博客园

参考:去除字符串开头结尾的指定字符串_pannijingling的技术博客_51CTO博客

15、模拟用户输入

在上面的【脚本功能描述-5、自动填充文章标题】中,出现了一个bug。

我以为只要简单地将 inputvalue 值设置为所获得的一级标题就可以了,可没想到我还是太年轻了……

如果用户只让脚本自动填充标题而没有再对标题输入框里的内容做任何更改,那么当用户想要发布文章时,在线编辑器将发出如下警告:【标题不能为空】

image-20210903160537461-标题不能为空

可是脚本明明已经在标题输入框填充了内容啊,为什么还会警告我说标题不能为空呢?

顺着控制台的报错提示,可以看到是运行下面这段代码是报的错:

image-20210903161327750

在同一JS代码文件下搜索【title】,发现网页标签页的内容好像是由下面这段代码控制的:

image-20210903165622971

于是我开始观察当用户手动输入标题时的情况:

用户手动输入标题时的情况

而当用户想通过脚本来改变 inputvalue 值时,上面提到的两处都不会变化——也就是掘金编辑器没有监测到 input 框值的变化。于是我手动给输入框加上了 input 事件监听:

let titleInput = document.querySelector('#juejin-web-editor > div.edit-draft > div > header > input');
titleInput.addEventListener('input', function () {
  console.log('The value is now ' + titleInput.value);
});

这样,每当用户对输入值进行修改后,就会触发 input 事件,并在控制台输出当前 input 框的 value 值。当我手动在文章标题输入框输入文字时,控制台确实如预期中输出了相关的信息。但是当我在控制台通过JavaScript代码改变 value 值时,却没有相应的输出信息:

titleInput.value = 'change';

image-20210903164002363

看来是需要用户手动输入标题才管用,于是我开始探索该如何模拟用户输入的事件。

更新:2021年9月3日16:50:36

参考:input - Web API 接口参考 | MDN

参考:javascript - How do I programmatically trigger an “input” event without jQuery? - Stack Overflow

参考:事件:change,input,cut,copy,paste

参考:模拟用户输入_Areom个人博客-CSDN博客

参考:如何使用事件模拟完整的用户输入_慕课猿问

参考:【前端】js代码模拟用户键盘鼠标输入 - 赵康 - 博客园

参考:JavaScript_js模拟键盘输入 - GisClub - 博客园

参考:oninput 事件 | 菜鸟教程

这些说法我没有都去尝试,但是我隐隐觉得似乎都行不通。因为我发现每次用户输入内容时,掘金在线编辑器都会向服务器发送一个POST 类型的 update 请求。我猜这也是为什么掘金编辑器能够实现实时保存草稿的原因吧。

其发送的请求总体信息为:

// General
Request URL: https://api.juejin.cn/content_api/v1/article_draft/update
Request Method: POST
Status Code: 200 
Remote Address: 124.225.193.233:443
Referrer Policy: no-referrer-when-downgrade

其发送的数据格式为:

// Request Payload
{
  "id":"7003406308921573389",
  "category_id":"6809637767543259144",
  "tag_ids":["6809640507191328782"],
  "link_url":"",
  "cover_image":"",
  "is_gfw":0,
  "title":"测试标题2",
  "brief_content":"测试编辑摘要测试......编辑摘要测试编辑摘要测试编辑摘要",
	"is_english":0,
  "is_original":1,
  "edit_type":10,
  "html_content":"deprecated",
  "mark_content":""
}

其中 "id":"7003406308921573389" 即为当前草稿的唯一标识,也就是当前的网址中的最后一段:

https://juejin.cn/editor/drafts/7003406308921573389

而我当前关心的就是 "title":"测试标题2" 这个信息,可以看到,这就是我想要的那个文章标题。于是我有了一个大胆的想法:能不能由脚本主动向服务器发送一个 POST 请求,然后在请求体中携带我们想要给服务器的信息呢?于是我甚至又不闲事多地在脚本里引入了 axios 来手动发送 POST 请求,但是结果还是无法达到我想要的效果。始终有个【must bind phone】的报错信息:

{
  data: null
  err_msg: "must bind phone"
  err_no: 403
}

弄到这里,我已经没有再探索下去的热情了。感觉好像为了这么一个小功能而这么大费周章实在是有点不值。所以我就想了一个折中的办法:每次处理完本地文档内容之后还是通过 titleInput.value = xxx 的方式填写标题,但是没次都在标题后加一个空格,并且通过 titleInput.focus() 的方式让标题输入框获取焦点,然后用户点击了提示【文档处理完毕,删除标题尾部空格,然后在编辑区按下 Ctrl + V 粘贴内容】的弹窗后,可以直接按下退格键 Backspace 来删除那个添加的空格,多次一举的目的就是触发掘金编辑器自动向服务器发送更新信息,又算是一个曲线救国的歪门邪道了……没办法,目前只有这个实力😂……

参考:JS实现使用POST方式发送请求 - 远洪 - 博客园

参考:tampermonkey 如何引用Jquery+CSDN阅读模式案例_LFSenior-CSDN博客

没想到这个小功能把我搞到凌晨两三点,淦……看来提升空间还是很大呀……

16、剪切板是否有容量限制

写到最后,我突然想到一个问题:剪切板是否有容量限制,如果我将一篇非常长的文档放进剪切板会不会出问题?百度搜索似乎没有找到我想要的解释。后来在谷歌上发现了相关的讨论。

The Windows clipboard does not impose any other size limits. Unless you have a surprisingly small amount of virtual memory available (or you where trying to copy and paste the human genome), then it’s the fault of the programmers of the application.

总结过来就是:不用你操心这个问题……😳

更新:2021年9月3日19:29:10

参考:What is the max amount of characters you can copy at once, and why is there a limit? : askscience

参考:windows - Clipboard size limit - Stack Overflow

参考:What is the maximum amount of text characters you can copy on a computer at once, and why is there a limit? - Quora

有关参考

【正则表达式】

更新:2021年8月29日21:37:33

参考:正则表达式:不包含某些指定的单词(超级难的正则式,前无古人哦)_邢晓宁专栏-CSDN博客

更新:2021年9月1日22:39:47

参考:RegExp(正则表达式) - JavaScript | MDN

参考:正则表达式在线测试 | 菜鸟工具

更新:2021年9月1日21:09:41

参考:正则表达式中,如何在任意匹配字符后面加上原字符和特定内容_百度知道

更新:2021年9月2日19:16:31

参考:VSCode查找和替换正则表达式转义字符整理 - 至尊王者 - 博客园

参考:正则表达式匹配各种特殊字符_正则表达式_脚本之家

【window.Selection】

详细参考内容请看上方相关内容:二-3(按住Ctrl单击跳转)。

【JS读取本地文件】

详细参考内容请看上方相关内容:二-4(按住Ctrl单击跳转)。

【自定义 input 标签的样式】

详细参考内容请看上方相关内容:二-5(按住Ctrl单击跳转)。

【JavaScript插入或创建SVG】

详细参考内容请看上方相关内容:二-7(按住Ctrl单击跳转)。

【aria-xxxx属性】

详细参考内容请看上方相关内容:二-9(按住Ctrl单击跳转)。

【JavaScript剪切板操作】

详细参考内容请看上方相关内容:二-12(按住Ctrl单击跳转)。

【触发自定义事件】

详细参考内容请看上方相关内容:二-13(按住Ctrl单击跳转)。

【模拟用户操作】

详细参考内容请看上方相关内容:二-15(按住Ctrl单击跳转)。

【其他】

更新:2021年8月29日21:52:19

参考:String.prototype.replace() - JavaScript | MDN

更新:2021年8月30日01:55:21

参考:在dom最前面插入_DOM操作填坑(一)_甄公子的博客-CSDN博客

参考:Dom中获取元素的第一个和最后一个子节点与元素节点_Always-Learning-CSDN博客_获取元素的第一个子节点

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值