
基于WangEditor实现SSML编辑器
WangEditor简介
WangEditor 是一款流行的富文本编辑器,它提供了许多功能和特性,使得在网页中实现富文本编辑变得更加简单和便捷。以下是一些关于 WangEditor 的评价:
易于集成:WangEditor 提供了简单的 API 和文档,方便开发者快速集成到他们的项目中。
功能丰富:WangEditor 提供了丰富的编辑功能,包括文字样式设置、插入图片、插入视频、表格编辑等,满足了大部分富文本编辑的需求。
定制化:WangEditor 支持自定义配置和样式,开发者可以根据自己的需求定制编辑器的外观和功能。
支持多平台:WangEditor 支持在 PC 和移动端进行使用,具有良好的跨平台兼容性。
持续更新:WangEditor 团队持续更新和维护编辑器,修复 bug 并增加新功能,保持了编辑器的稳定性和可靠性。
SSML简介
SSML(Speech Synthesis Markup Language)是一种用于控制文本到语音(TTS)转换过程的标记语言。它允许开发者控制文本的发音、语速、语调、音量等方面,以提高生成的语音的自然度和表现力。以下是关于 SSML 的一些简要介绍:
控制音频输出:SSML 允许开发者控制文本转语音的各个方面,如语速、音调、音量、语气等,以使合成语音更加自然和生动。
语音合成引擎支持:许多语音合成引擎(如 Amazon Polly、Google Text-to-Speech 等)支持 SSML,开发者可以使用 SSML 标记来增强生成的语音。
标记语言:SSML 是一种标记语言,类似于 HTML,通过在文本中插入标记来控制语音合成引擎的行为。
功能丰富:SSML 提供了丰富的标记和选项,可以控制音频输出的各个方面,如音调、语速、静音、发音等。
应用领域:SSML 主要用于语音合成应用中,如语音助手、语音导航、语音广播等领域,可以提高生成语音的质量和表现力。
总的来说,SSML 是一种用于控制文本到语音转换过程的标记语言,通过使用 SSML,开发者可以更精细地控制生成的语音,使之更加自然、生动和符合特定的语境需求。
需求简介
我们需要开发一款SSML富文本编辑器,来控制TTS转换的效果,SSML和HTML极其相似,我们引入普通HTML富文本编辑器的思路,来开发一款SSML编辑器。
为什么选择WangEditor
WangEditor有一个很重要的特性:自定义元素(官方文档),编辑器默认只有基本的标题、列表、文字、图片、表格等元素,如果你想让编辑器渲染一个新元素,如 附件 数学公式 链接卡片 等,就可以通过这个功能自定义实现。
有哪些坑?
文档示例过少
开发过程略微复杂,细节很多
公开的实践案例较少,无法提供很好的参照
报错信息不明确,很多异常根本无法得到有效的提示
参考资料
Github大神基于WangEditor实现的SSML编辑器:https://github.com/mekumiao/ssml-editor?tab=readme-ov-file
实现过程
技术栈
Vue3+、vite
技术路线
根据实际的需求和技术实现难度,实现路径不完全与官网的例子相同,并不完全实现编辑器对新标签啊的支持,仅仅实现编辑器对新标签的显示,设置和获取通过单独的函数处理。
具体代码逻辑(以定义注音phoneme功能为例)
1、将phoneme标签定义为inline和void,且禁用掉换行和tab操作,简化编辑器操作逻辑
function registerPlugin(editor) {
const { isInline, isVoid } = editor
const newEditor = editor
newEditor.isInline = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'phoneme') return true
return isInline(elem)
}
newEditor.isVoid = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'phoneme') return true
return isVoid(elem)
}
// 重写 insertBreak 换行 禁止换行
newEditor.insertBreak = () => {
return
}
// 重写 tab 禁止tab
newEditor.handleTab = () => {
return
}
return newEditor // 返回 newEditor ,重要!!!
}
// 把插件 registerPlugin 注册到 wangEditor
Boot.registerPlugin(registerPlugin)
2、渲染函数逻辑,在编辑器中渲染出节点。技术要点如下:
自定义class类名的配置在props中的className字段,后期使用此class定义css
删除功能,要拿到元素的path,通过path删除,然后将节点的文本以普通文本的方式插入进编辑器
注册元素的type很重要,要与插入时的保持一致
元素css必须添加左右padding,否则光标无法选中元素
// 在编辑器中渲染新元素
const phonemeRenderAttachment = (elem) => {
const { ph = '', nodeText = '' } = elem
// 文字node
const textVnode = h(
// HTML tag
'span',
// HTML 属性、样式、事件
{
props: { contentEditable: false, className: 'phoneme-text-elem' } // HTML 属性,驼峰式写法
},
`${nodeText}-${convert(ph)}`
)
// 删除按钮节点
const delVnode = h(
// HTML tag
'span',
// HTML 属性、样式、事件
{
props: { contentEditable: false, className: 'phoneme-del-elem iconfont icon-guanbi' }, // HTML 属性,驼峰式写法
on: {
click(event) {
event.preventDefault()
// 找到路径
const path = DomEditor.findPath(editor, elem)
// 选中路径
editor.select(path)
// 删除节点
SlateTransforms.removeNodes(editor, {
at: path
})
// 插入字符节点
editor.insertNode({
type: 'text',
text: nodeText
})
}
}
}
)
// 附件元素 vnode
const attachVnode = h(
// HTML tag
'span',
// HTML 属性、样式、事件
{
props: { contentEditable: false, className: 'phoneme-elem' } // HTML 属性,驼峰式写法
},
// 子节点
[textVnode, delVnode]
)
return attachVnode
}
// 把 phonemeRenderElemConf 注册到 wangEditor
Boot.registerRenderElem({
type: 'phonemeElement', // 新元素 type ,重要!!!
renderElem: phonemeRenderAttachment
})
3、创建编辑器实例,技术要点如下:
autoFocus要设置为false,否则会有无法清除聚焦的问题。
自定义粘贴功能,将粘贴内容的文本提取出来,放弃掉样式信息。
mode设置为simple,禁用掉操作栏,禁止操作样式
editor = createEditor({
selector: '#editor-container',
html: '',
config: {
placeholder: '请输入',
autoFocus: false,
maxLength: 100,
// 自定义粘贴
customPaste: (editor, event) => {
const text = event.clipboardData.getData('text/plain')
if (text) {
editor.insertText(text)
}
event.preventDefault()
return false
}
},
mode: 'simple'
})
4、解析函数HTML-->SSML
此函数的应用场景是:从编辑器提取SSML
import { DomEditor, SlateElement, SlateText } from '@wangeditor/editor'
const escapeText = (text) => {
const result = text
.replaceAll(/[&]/gi, '&')
.replaceAll(/[<]/gi, '<')
.replaceAll(/[>]/gi, '>')
return result
}
const serializeSpeak = (node, children) => {
return `<speak>${children}</speak>`
}
const serializePhoneme = (node, text) => {
return `<phoneme alphabet="py" ph="${node.ph}">${text}</phoneme>`
}
const serializeNode = (node) => {
if (SlateText.isText(node)) {
return escapeText(node.text.trim())
} else if (SlateElement.isElement(node)) {
const children = node.children.map((n) => serializeNode(n)).join('')
const type = DomEditor.getNodeType(node)
switch (type) {
case 'paragraph':
return children
case 'speakElement':
return serializeSpeak(node, children)
case 'phonemeElement':
return serializePhoneme(node, node.nodeText)
default:
return children
}
}
return ''
}
const HTMLToSSML = (nodes) => {
return `<speak>${serializeNode(nodes[0])}</speak>`
}
技术要点如下:
HTMLToSSML函数的传参是editor.children,通过循环解析节点来转换
节点类型只区分text和其他
5、解析函数SSML-->HTML
函数的应用场景是:将SSML渲染到编辑器
import { DomEditor, SlateElement, SlateText } from '@wangeditor/editor'
const SSMLToHTML = (text) => {
const formatNodes = (nodes) => {
const nodeDatas = []
for (let index = 0; index < nodes.length; index++) {
const element = nodes[index]
switch (element.nodeName) {
case 'SPEAK':
return formatNodes(element.childNodes)
case '#text':
nodeDatas.push({
text: element.nodeValue,
})
break
case 'PHONEME':
nodeDatas.push({
type: 'phonemeElement',
ph: element.getAttribute('ph') || '',
nodeText: element.innerText || '',
children: [{ text: element.innerText || '' }],
})
break
}
}
return nodeDatas
}
const parentDom = document.createElement('div')
parentDom.innerHTML = text
return formatNodes(parentDom.childNodes)
}
技术要点如下:
因为HTML5支持完全自定义便签,所以SSML文本在解析上完全可以当作HTML文本处理。
使用时,使用SlateTransforms.insertNodes(editor, SSMLToHTML(ssmlText))方法,将节点注入到编辑器