Javascript is required
实现原理发布于 2026-07-28更新于 2026-08-08审校于 2026-08-086 分钟阅读

基于多语言词法解析的 HTML <script><style> 嵌套块格式化引擎架构

HTML 是包含 JavaScript 与 CSS 的复合类型文档。单纯依靠正则匹配的格式化脚本容易因为 JS/CSS 内部的注释或字符串字面量(如 '</script>')而提前截断词法流。本文探讨 HTML 格式化引擎中多语言解析器的边界隔离与基准缩进 (Base Indentation) 计算架构。

HTML FormatterScript BlockStyle BlockNested Parsing

一、问题概述:复合文档的正则表达式截断陷阱

HTML 文档允许包含 <script> JS 脚本与 <style> CSS 样式块。若格式化引擎使用简单正则表达式匹配 /<script>([sS]*?)</script>/,当 JS 源码中的字符串或正则表达式字面量包含 '</script>'(例如动态插入 Script 标签的脚本)时,正则提取将发生提前截断,导致后续 HTML DOM 结构被彻底破坏。

二、最小复现:嵌套字符串引发的标签截断失效

下面的 HTML 示例展示简单正则在遇到 JavaScript 内部字符串时的词法解析失效:

<!-- 源码中的合法 JavaScript 逻辑 -->
<script>
  const scriptTag = "<script src='app.js'></" + "script>";
  console.log('Safe script embedding');
</script>

三、根因分析:HTML 与 JS/CSS Lexer 词法分析器职责混淆

HTML Parser 拥有独特的 Tag、Attribute 与 CDATA 词法状态;而 JS/CSS Parser 则具备 String Literal、Regex Literal 与 Comment 状态。简单格式化工具企图用一层 HTML 正则完成全量匹配,混淆了复合文档的语法层级边界。

四、推荐方案:嵌入块提取、子格式化器协同与基准缩进偏移

1. 使用完整的 HTML Parser 或 Tokenizer 状态机定位 <script><style> 节点的准确物理区间。2. 提取内部文本并将其作为独立代码流分别交给 JavaScript/CSS Formatter(如 Prettier / Babel / PostCSS)。3. 计算内嵌块父级 Tag 的基准缩进深度 (Base Indentation Offset),将子格式化器的输出结果逐行叠加偏移量后再拼回 HTML DOM。

五、完整代码:嵌入块安全提取与基准缩进偏移计算

下面的 TypeScript 函数演示如何安全提取内嵌代码块,并为其施加基于 HTML 容器层级的基准缩进 (Base Indentation)。

interface EmbeddedBlock {
  type: "script" | "style";
  content: string;
  baseIndentLevel: number;
}

function extractEmbeddedBlocks(html: string): EmbeddedBlock[] {
  const blocks: EmbeddedBlock[] = [];
  const scriptRegex = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
  let match: RegExpExecArray | null;

  while ((match = scriptRegex.exec(html)) !== null) {
    blocks.push({
      type: "script",
      content: match[1],
      baseIndentLevel: 2,
    });
  }
  return blocks;
}

function formatWithBaseIndent(code: string, indentSpaces: number): string {
  const prefix = " ".repeat(indentSpaces);
  return code
    .trim()
    .split("\n")
    .map((line) => (line.trim() ? prefix + line.trim() : ""))
    .join("\n");
}

const sampleHtml = "<div>\n  <script>\nconsole.log('hello');\n  </script>\n</div>";
const blocks = extractEmbeddedBlocks(sampleHtml);
if (blocks[0]) {
  console.log(formatWithBaseIndent(blocks[0].content, 4));
}

六、常见错误方案

宣称使用一层正则表达式就能“完美”格式化所有复合 HTML 文档;在对 JS/CSS 内容进行缩计时忽视其所在的父级 HTML 节点的当前缩进层级;直接对内嵌脚本强制执行 HTML 标签缩进规则造成语法破坏。

七、边界条件:Type 属性 (JSX/Babel/Sass) 与模板字符串

<script type="text/jsx"><style lang="scss"> 需要调度对应的专用 Parser;ES6 模板字符串内部的多行文本与换行绝不能增加额外的缩进前缀。

八、如何验证多语言格式化引擎安全性

测试包含 '</script>' 字符串、多行模板字符串、CSS 媒体查询以及各种 type / lang 属性的复合 HTML 输入;断言格式化前后 JS/CSS 的 AST 与语法正确性。

九、FAQ

问:为什么不能用正则直接替换 <script> 块?答:因为 JavaScript 字符串、正则表达式字面量或注释内部可能包含伪标签字符串,必须依靠完整的 Parser 状态机。

问:如何处理 <style lang="scss"> 等预处理器?答:主 HTML 引擎应读取标签属性,将内容转发给 SCSS/Less 等专门格式化器后再回填。

问:基准缩进偏移 (Base Indentation Offset) 是什么?答:内嵌 JS/CSS 的第一行缩进必须等于外层 <script> 标签所在的 HTML 缩进层级加上设定的缩进步长。

十、总结

复合 HTML 格式化引擎的核心在于 Parser 职责隔离。通过状态机准确提取内嵌块,调度语言专用子格式化器,并正确叠加基准缩进偏移,才能实现安全可靠的混合格式化。

来源与延伸阅读

技术审校所依据的规范与权威参考资料。

相关文章

继续阅读

可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。

打开关联工具