代码填空题技术实现:突破 highlight.js 安全限制的工程实践

文章目录

💻问题背景

在开发一个交互式代码教育平台时,我遇到了一个有趣的需求:需要在代码中插入可编辑的空白区域,让用户填写缺失的代码片段,类似于大学程序设计考试中的填空题。

具体来说,后端返回的代码类似如下,其中<<BLANK>>即为挖空的代码,然后前端标替换<<BLANK>>为标签元素input进行渲染。

python 复制代码
def fun1():
    a = 1
    <<BLANK>>
    print(a)

最初的想法很直观:直接将代码字符串中的<<BLANK>>替换为标签字符串 <Input>, 然后利用v-html属性插入到highlight所渲染的<code>标签即可

js 复制代码
  <pre>
    <code class="language-python" v-html="processedCode"></code>
  </pre>
  
	const processedCode = computed(() => {
	  return rawCode.replace(/<<BLANK>>/g, (match) => {
	    const currentIndex = index++
	    return `<input class="blank-input" >`
	  })
	})

然而,这个方法遇到了一个严重的问题:当代码通过 highlight.js 进行语法高亮时,所有 HTML 标签都被过滤掉了,只保留了纯文本。控制台出现了这样的警告:

大概原因是:为了防止XXS脚本攻击,highlight在高亮python代码前过滤了这些"脚本"代码。

查看了官方issue,发现也有人提出是否可以渲染混合代码(如python+一些html代码),从相关回复中可以看到,作者首先表示出了不太支持的态度,因为会存在一些安全风险,但后续说可以考虑增加一个属性 'allowUnsafeHTML',然而目前使用来看还是没有实现。

✨解决方案

既然无法在高亮前插入 HTML 标签,换一种思路:先让 highlight.js 完成代码高亮,然后再修改渲染后的 DOM,插入我们需要的输入框。

js 复制代码
    <pre>
      <code class="language-python">{{ streamedCode }}</code>
    </pre>
      
  // 处理结束事件
   eventSource.value.addEventListener('end', () => {
     console.log('代码生成结束')
     finalCode.value = streamedCode.value
     isStreaming.value = false
     if (eventSource.value) {
       eventSource.value.close()
       eventSource.value = null
     }
     nextTick(() => {
       document.querySelectorAll('.final-code-container code').forEach(block => {
         // 执行高亮渲染
         hljs.highlightElement(block)
         // 添加行号
         if (window.hljs && window.hljs.lineNumbersBlock) { 
           window.hljs.lineNumbersBlock(block)
         }
         
         // 为什么不再嵌套一个nextTick:
         // nextTick 只能保证Vue自身的DOM更新(只能保证Vue自身的虚拟DOM更新到真实DOM),不能保证所有衍生DOM操作(特别是异步的)都已完成。对于这种复杂场景, MutationObserver 是更专业的选择。
         // 使用MutationObserver监听高亮渲染变化,确保所有渲染完成后再操作
         const observer = new MutationObserver((mutations) => {
           const tables = block.querySelectorAll('table.hljs-ln')
           if (tables.length > 0) {
             observer.disconnect()
             processBlankInputs(block)
           }
         })
         observer.observe(block, { 
           childList: true, 
           subtree: true 
         })
       })
     })
   })

// 将<<BLANK>>替换为输入框
const processBlankInputs = (block: Element) => {
  let index = 0

  const tables = block.querySelectorAll('table.hljs-ln')
  tables.forEach(table => {
    const tds = table.querySelectorAll('td.hljs-ln-code')
    tds.forEach(td => {
      if (td.innerHTML.includes('&lt;&lt;BLANK&gt;&gt;')) {
        const currentIndex = index++
        // 创建一个span元素来包裹输入框
        const span = document.createElement('span')
        span.innerHTML = td.innerHTML.replace(
          '&lt;&lt;BLANK&gt;&gt;',
          `<input class="blank-input" data-index="${currentIndex}">`
        )
        td.innerHTML = ''
        td.appendChild(span)
        
        // 通过span获取到input元素
        const input = td.querySelector('input.blank-input') as HTMLInputElement
        input.value = blankValues.value[currentIndex] || ''
        
        // 使用类型安全的addEventListener代替oninput
        input.addEventListener('input', (event) => {
          blankValues.value[currentIndex] = (event.target as HTMLInputElement).value
          // console.log('Input changed:', currentIndex, blankValues.value[currentIndex])
        })
      }
    })
  })
}

实现细节与技术要点

  1. DOM 渲染时机的处理:
    1.1 使用 Vue 的nextTick确保 Vue 完成 DOM 更新
    1.2 使用MutationObserver监听 highlight.js 完成高亮和行号添加
    1.3 避免了多层嵌套nextTick的问题,提高了代码可靠性
  2. 安全处理 HTML 内容:
    2.1 先让 highlight.js 处理纯文本代码,确保安全
    2.2 再在渲染后的 DOM 中插入受控的 HTML 元素
  3. 用户交互优化:
    3.1 为每个输入框添加唯一索引,便于跟踪用户输入
    3.2 保存用户输入状态,支持恢复和提交
    实际编写时,还遇到了个小问题:当流式加载后端代码完成后(后端AI生成的代码,是一段段传回来的,所以有个加载过程),利用nextTick() 等dom加载后,再利用highlight进行高亮操作,很自然会继续嵌套一个nextTIck(),然而需要注意的是,nextTick 只能保证Vue自身的DOM更新(只能保证Vue自身的虚拟DOM更新到真实DOM),不能保证所有衍生DOM操作都已完成(特别是第三方库的异步操作),因此可以利用MutationObserver进行渲染监听,再进行替换BLANK为input标签的操作。
相关推荐
某公司摸鱼前端5 小时前
uniapp socket 封装 (可拿去直接用)
前端·javascript·websocket·uni-app
要加油哦~5 小时前
vue | 插件 | 移动文件的插件 —— move-file-cli 插件 的安装与使用
前端·javascript·vue.js
wen's7 小时前
React Native 0.79.4 中 [RCTView setColor:] 崩溃问题完整解决方案
javascript·react native·react.js
黄雪超7 小时前
JVM——函数式语法糖:如何使用Function、Stream来编写函数式程序?
java·开发语言·jvm
ThetaarSofVenice7 小时前
对象的finalization机制Test
java·开发语言·jvm
思则变7 小时前
[Pytest] [Part 2]增加 log功能
开发语言·python·pytest
vvilkim7 小时前
Electron 自动更新机制详解:实现无缝应用升级
前端·javascript·electron
vvilkim7 小时前
Electron 应用中的内容安全策略 (CSP) 全面指南
前端·javascript·electron
lijingguang7 小时前
在C#中根据URL下载文件并保存到本地,可以使用以下方法(推荐使用现代异步方式)
开发语言·c#
aha-凯心8 小时前
vben 之 axios 封装
前端·javascript·学习