一种前端解析html字符串片段的方案求教

发布于 2022-09-12 23:16:40 字数 4604 浏览 32 评论 0

规则制定


<h1>目录A</h1><p>::content1</p><h2>常用操作</h2><p>::content2</p><p>内容目录2</p><p>content2::</p><h2>目录2级</h2><p>::content2</p><h3>目录三级 </h3><p>      ::content3</p><h4>        目录四级</h4><p>        ::content4</p><p>         四级内容段落</p><p>        content4::</p><p>    content3::</p><p>    content2::</p><h2>  目录2级2标题</h2><p>  ::content2</p><p>  目录二级内容</p><p>  content2::</p><p>content1::</p><h1>目录1级</h1><p>  ::content1</p><h2>  目录2级</h2><p>  ::content2</p><p>  目录内容</p><p>  content2::</p><h2>  目录2级</h2><p>::content2</p><p>制定风险管理计划</p><p>项目经理在项目策划阶段,制定“风险管理计划”,并将该计划内容记录到《项 目实施计划》:项目概述-项目风险中。</p><p>创建风险管理跟踪表</p><p>content2::</p><p>content1::</p><h1>目录A</h1><p>::content1</p><h2>常用操作</h2><p>::content2</p><p>内容目录2</p><p>content2::</p><h2>目录2级</h2><p>::content2</p><h3>目录三级 </h3><p>      ::content3</p><h4>        目录四级</h4><p>        ::content4</p><p>         四级内容段落</p><p>        content4::</p><p>    content3::</p><p>    content2::</p><h2>  目录2级2标题</h2><p>  ::content2</p><p>  目录二级内容</p><p>  content2::</p><p>content1::</p><h1>目录1级</h1><p>  ::content1</p><h2>  目录2级</h2><p>  ::content2</p><p>  目录内容</p><p>  content2::</p><h2>  目录2级</h2><p>::content2</p><p>制定风险管理计划</p><p>项目经理在项目策划阶段,制定“风险管理计划”,并将该计划内容记录到《项 目实施计划》:项目概述-项目风险中。</p><p>创建风险管理跟踪表</p><p>content2::</p><p>content1::</p>

规则如上, 我需要解析标签和内容的对应结构。
例如: H1对应::content1 结尾content1:: 中间的内容 依次类推。
从里层到外层或者从外层到里层的递归。
如果content1层级对应里没有H2-H6标签,则该content的内容全部为该H1标签对应的内容。以此形成对应键值对结构。如果content里没有下级H标签,则此内容为最内层内容。

请教大神有无算法方案。
测试片段1:

<h1>目录A</h1>::content1<h2>常用操作</h2>::content2<p>内容目录2</p><h2>目录2级</h2>::content2<h3>目录三级</h3>::content3<h4>目录四级</h4>::content4<p>四级内容段落</p><h2>目录2级2标题</h2>::content2<p>目录二级内容</p><h1>目录1级</h1>::content1<h2>目录2级</h2>::content2<p>目录内容</p><h2>目录2级</h2>::content2<p>制定风险管理计划</p><p>项目经理在项目策划阶段,制定“风险管理计划”,并将该计划内容记录到《项目实施计划》:项目概述-项目风险中。</p><p>创建风险管理跟踪表</p>content2:: content1::

image.png

测试片段2:


<h2><a id="_Toc69321823"></a>概述</h2>
<p> ::content2</p>
<p>您可以根据实际需要进行选择。</p>
<p>content2::</p>
<h2>概论</h2>
<p> ::content2</p>
<p>如下图所示。</p>
<h4>目录3</h4>
<p> ::content3</p>
<p>云主机管理方式比物理服务器更简单高效,您可根据实际需要对这些资源进行灵活的组合,您可通过控制台、OpenAPI或CLI随时创建指定数量的云主机,在使用过程中可以根据业务规模随时调整实例规格,对于过剩或闲置的资源也可以进行释放以便节约投入成本。京东智联云云主机为您快速部署应用提供稳定可靠的基础能力,使您更专注于核心业务创新。</p>
<p>content3::</p>
<p>content2::</p>
<h2><a id="_Toc69321824"></a>相关概念</h2>
<p> ::content2</p>
<p>了解常会涉及到的概念请参见</p>
<p>content2::</p>
<h2><a id="_Toc69321825"></a>相关服务</h2>
<p>::content2</p>
<ul>
  <li>您可以从云市场获取由第三方服务商提供的基础软件、企业软件、网站建设、代运维、云安全、数据及API、解决方案等相关的各类软件和服务。您也可以成为云市场服务商。详细信息请参见云市场文档。</li>
</ul>
<p>content2::</p>
<h2><a id="_Toc69321826"></a>使用云主机</h2>
<p>::content2</p>
<p>您可以使用京东智联云账号直接登录对您的云主机进行管理及操作。</p>
<p>content2::</p>
<h2><a id="_Toc69321827"></a>计费说明</h2>
<p>::content2</p>
<p>云主机支持包年包月和按配置两种计费模式:</p>
<ul>
  <li>包年包月:可选1至9个月的包月服务,或1至3年的包年服务,价格较按配置计费更低。采用预付费方式,请您购买后留意云主机到期时间并及时续费或开启自动续费功能。</li>
  <li>按配置:根据您的实例配置及对应实际使用时长,每小时扣费,精确至秒,您只需在购买前预先向账户充值保证余额充足即可。</li>
</ul>
<p>具体内容</p>
<p>content2::</p>

此方案最终使用DOM Parser() 解析,对dom节点进行拼接处理已完成。感谢帮助的大哥。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

狼性发作 2022-09-19 23:16:40

只测试或你给的这个字符串,其他的没测试过

const template1 = `
<h1>1111</h1>
::content1
  <h2>22222
  333</h2>
    ::content2
    content2::
  <h2>333333</h2> 
    ::content2
      <h3>sdfsadfda</h3> 
      ::content3
        <h4>666 
        777</h4>
        ::content4
        content4::
      content3::
    content2::
  <h2></h2>
  <h3></h3>
content1::
<h1></h1>
::content1
  <h2></h2>
  ::content2
  content2::
  <h2></h2>
  ::content2
  content2::
content1::`

type HeadingElementTagName = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
type TreeNode = {
  tagName: HeadingElementTagName
  content: string
  children: TreeNode[]
  textContent: string
}

class Parser {
  private index = 0
  private REBlank = /\s/
  private template: string = ''
  private RELiteral = /[a-zA-Z0-9_]/
  private skipBlank() {
    while (
      this.index < this.template.length &&
      this.REBlank.test(this.template[this.index])
    )
      ++this.index
  }
  private parents: TreeNode[] = []

  private throwError() {
    throw new Error(
      `Unexpected token ${this.template[this.index]} at index ${this.index}`
    )
  }

  private consume(chars: string): void {
    if (!this.template.startsWith(chars, this.index)) this.throwError()
    this.index += chars.length
  }

  private readTagName(): string {
    let tagName = ''

    while (
      this.index < this.template.length &&
      this.template[this.index] !== '>' &&
      this.template[this.index] !== '/'
    ) {
      tagName += this.template[this.index]
      ++this.index
    }

    if (!tagName) this.throwError()

    return tagName
  }

  private readHeadingElement(): TreeNode {
    //读取开始标签比如,<h1>
    this.consume('<')
    let tagNameStart = this.readTagName()
    this.consume('>')

    let textContent = ''

    while (
      this.index < this.template.length &&
      this.template[this.index] !== '<'
    ) {
      textContent += this.template[this.index]
      ++this.index
    }
    this.skipBlank()
    //读取结束标签比如</h1>
    this.consume('</')
    const indexEnd = this.index
    const tagNameEnd = this.readTagName()
    this.consume('>')

    if (tagNameStart !== tagNameEnd)
      throw new Error(
        `mismatching tag expect ${tagNameStart} but got ${tagNameEnd} at index ${indexEnd}`
      )

    const headingNode: TreeNode = {
      tagName: tagNameStart as HeadingElementTagName,
      children: [],
      content: '',
      textContent,
    }

    return headingNode
  }

  readStartContent(): string {
    this.consume('::')
    let content = ''

    while (
      this.index < this.template.length &&
      this.RELiteral.test(this.template[this.index])
    ) {
      content += this.template[this.index]
      ++this.index
    }

    if (!content) this.throwError()

    return content
  }

  readEndContent(): string {
    let content = ''
    while (
      this.index < this.template.length &&
      this.RELiteral.test(this.template[this.index])
    ) {
      content += this.template[this.index]
      ++this.index
    }

    if (!content) this.throwError()
    this.consume('::')
    return content
  }

  /**
   * 读取一个TreeNode节点
   * @returns 读取的TreeNode节点
   */
  generateTree(): TreeNode | null {
    let resolvedRoot: TreeNode | null = null
    const nodeContent: string[] = []
    while (this.index < this.template.length) {
      const headingNode = this.readHeadingElement()
      const parentNode = this.parents[this.parents.length - 1] ?? null
      if (!resolvedRoot) resolvedRoot = headingNode
      if (parentNode) parentNode.children.push(headingNode)
      this.parents.push(headingNode)
      this.skipBlank()

      //判断该节点是否有content有则读取,没有则返回
      if (this.template.startsWith(':', this.index)) {
        const content = (headingNode.content = this.readStartContent())
        nodeContent.push(content)
        this.skipBlank()
        //判断该content下是否有子节点,有的话递归读取
        if (this.template.startsWith('<', this.index)) {
          this.generateTree()
        }

        this.skipBlank()

        //没有到达contentEnd据徐读取子节点
        if (
          this.index >= this.template.length ||
          !this.RELiteral.test(this.template[this.index])
        )
          continue
      } else {
        /**
         * 该节点没有子节点直接返回 */
        this.parents.pop()
        this.skipBlank()
        if (!nodeContent.length) break
      }

      //循环读取contentEnd
      while (
        nodeContent.length &&
        this.index < this.template.length &&
        this.RELiteral.test(this.template[this.index])
      ) {
        const indexEnd = this.index
        const contentEnd = this.readEndContent()
        if (nodeContent[nodeContent.length - 1] !== contentEnd) {
          throw new Error(
            `Mismatching content expect ${headingNode.content} but got ${contentEnd} at ${indexEnd}`
          )
        }
        nodeContent.pop()
        this.parents.pop()
        this.skipBlank()
      }

      //如果nodeContent为空说明该节点已经处理完毕,结束循环,继续父级的逻辑
      if (!nodeContent.length) break

      this.skipBlank()
    }

    return resolvedRoot
  }

  parse(str: string): null | TreeNode {
    this.template = str

    this.skipBlank()
    if (this.index >= this.template.length) return null

    const root: TreeNode = {
      tagName: 'DummyRoot' as HeadingElementTagName,
      content: '',
      children: [],
      textContent: '',
    }
    while (this.index < this.template.length) {
      root.children.push(this.generateTree()!)
      this.skipBlank()
    }

    return root
  }
}

const parser = new Parser()

console.log(parser.parse(template1)?.children[0])
金橙橙 2022-09-19 23:16:40

你自定义的 ::content1content1::
换成 <mytag_content1></mytag_content1>
直接create 一个 div, innerHTML 写进去,剩下的就是 DOM 操作了

安人多梦 2022-09-19 23:16:40

你给的是字符串后面是闭合的么

<h1></h1>
content1::

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文