距离上一条想法才没几天,又有新的问题出现了。
在 markdown 的列表语法中,如果有一个 listItem 仅仅包含一个 html 标签,并且不是那种常见的 inline 格式(比如 span),那么它会被认为是一个块级 html,同时该层级 listItem 内的子节点(即下一层级列表)将全部被当做 html 的一部分。
同样又是查了一圈,这也是 CommonMark 标准里面的,属于是让人很无语。
距离上一条想法才没几天,又有新的问题出现了。
在 markdown 的列表语法中,如果有一个 listItem 仅仅包含一个 html 标签,并且不是那种常见的 inline 格式(比如 span),那么它会被认为是一个块级 html,同时该层级 listItem 内的子节点(即下一层级列表)将全部被当做 html 的一部分。
同样又是查了一圈,这也是 CommonMark 标准里面的,属于是让人很无语。
今天又发现了一个 markdown 渲染上的 bug。
加粗的语法是前后各两个星号,如果结尾的两个星号前有一个中文标点符号,比如。\*\*」,且后面紧跟着其他文字,那么就渲染不出来加粗格式了。
查了一圈的结论是:在 CommonMark 标准中,中文标点符号不是合法的 word character,当它和星号放在一起时,将不能作为加粗格式的闭合标签。
前端很常用的两个工具 remarkjs 和 markdown-it,都遵循这个标准。所以几乎所有大模型对话产品都能复现这个问题,比如豆包和 ChatGPT。
解决办法有两种:
第一种是自行实现 remarkjs/mdit 的插件去做预处理/后处理,或者干脆选择一种不那么遵守标准的工具,如 marked.js。
第二种方法是我从腾讯元宝学到的。你还别说,腾讯元宝作为国内对话产品中的「后辈」,居然对这种魔鬼细节都有处理到,给他们的测试团队点个赞。
它是这么做的:由前端处理,将大模型输出的 markdown 数据中所有的星号星号前后各添加一个零宽空格,既不影响展示,又满足了 CommonMark 的闭合要求,非常完美。
不得不说,markdown 转 html 的坑实在是太多了,尽管本身语法并不多,但依然存在大量的细节,你不踩坑就不知道。
比如一个段落结尾处有两个空格,它表示的意思是换行,在 mdast 中对应了一个 break 节点。
很多时候只能 case by case 去解了。
Quill 编辑器的实例很重要,对内容的各种操作、事件监听都需要直接调用实例上的方法,它是一个层级很深、非常复杂的 js 对象(下面都以 quill 称)。
当在 Vue3 项目中使用 quill 时,可能随手就将它放进了 ref 里,默认会被深层代理。这里就会出大问题。
如果直接通过代理的方式访问 quill 上的任何方法时,浏览器可能会报错(找不到 blot 上的 offset),不仅操作不会生效,而且后续会一直报错,出现键盘操作无响应、注册的事件不触发、显示内容与 delta 数据不一致等等各种问题,几乎就是彻底崩溃了。
猜测原因是 Vue Proxy 数据代理机制会对这种第三方的非响应式的对象产生影响。
解决办法就是不对它进行代理:
// 1、不使用 vue ref
let quill = null
onMounted(() => {
quill = new Quill({})
})
// 2、用 markRaw 标记 quill 实例
const quill = ref(null)
onMounted(() => {
quill.value = markRaw(new Quill({}))
})
Vue 文档中有关于 markRaw 的详细介绍,里面专门提到了「有些值不应该是响应式的,例如复杂的第三方类实例或 Vue 组件对象」,可参考:markRaw。
浏览器默认有这样一个行为:当用户双击一段文本时,会选中单词(中文有分词处理),当三击时,会选中整段文本。
这个「选中整段文本」的行为所产生的浏览器选区 Selection 对象上,extendNode、focusNode 是当前块级文本的下一行的节点。
而如果用户通过手动拖拽行为,从段落开头框选到段落结尾,这时产生的选区对象上,上述的两个字段是当前块级文本的末尾节点。
对比这两种操作,用户看到的选区是一样的,实际生成的选区对象却是不同的,从编辑器框架的角度看,前者的 range 似乎比后者多了 1。
这就会造成很多问题,如果在此时进行插入带格式文本的操作,就会有意料之外的效果。
那么如何解决这个问题?
unhang 的格式化处理,在其中重新计算了选区结尾节点,参考 Slate 源码。关于它,也有一些比较有趣的社区讨论。Chrome 130 版本。
在用户的交互回调中触发 requestFullscreen 方法后,如果点击浏览器左上角的退出全屏幕按钮,能退出全屏状态。
但是此时不会触发 fullscreenchange 事件,并且通过浏览器的控制台可以看到目标 dom(即 document.fullscreenElement 元素)依然处于全屏激活(Layer 1)的状态。
此时需要稍微调整一下浏览器窗口(resize),就会触发 fullscreenchange 事件,目标元素退出全屏激活态。
Safari、Firefox、Arc 都不能复现这个问题。
有这样一个样式:
@for $i from 1 through 24 {
.offset-#{$i} {
width: calc((100 / 24) * #{$i} * 1%);
}
}
编译结果中的第一个样式是:width: calc(4.1666666667 * 1 * 1%);。
因为目标环境(React Native) 并不支持 calc,所以需要做改造。
第一个方案:width: percentage($i / 24);
它的编译结果中第一个样式是 width: 4.1666666667%;,满足要求,不过编译过程会报警告,原因见:breaking-changes/slash-div。这样做很好,理由也很让我信服,所以试试它的替代方案。
第二个方案:width: percentage(math.div($i, 24))
math 是 sass 内置的一个模块,需要通过 @use 'sass:math' 的方式声明后才可以使用,但是 @use 又有用法上的限制,比如不能在任何已有样式规则之后使用,这就导致我没办法做完美的替换。由于样式文件结构和引用的关系,我没找到合适的声明位置。
第三个方案:width: percentage(round($i * 0.04166666666, 0.000000001));
用除法各种问题,咱还是直接用乘法曲线救国吧,叠加一个精确位数的四舍五入方法,结果很完美。