【问题标题】:Convert list of H1..6 to Hierarchy将 H1..6 列表转换为层次结构
【发布时间】:2022-09-27 22:27:55
【问题描述】:

我想将 Markdown 文件中的 H1 到 H6 标签列表转换为 Javascript 层次结构,以便在目录中使用。

目前该列表是由 AstroJS 以这种格式[{depth: 1, text: \'I am a H1\'}, {depth: 2: \'I am a H2}] 生成的。

注意事项

  • 降价由最终用户创建。
  • 此列表可能只有一个根标题 (H1 -> H2 -> H3),
  • 它可能有多个根标题 (H2 -> H3, H2 -> H3)或者
  • 它可能有一个非常规的标题列表 (H3, H2 -> H3)
  • 它可能会跳过嵌套级别 (H1 -> H3 -> H6)

寻找 Javascript 或 Typescript 示例。

以下三个场景基于 AstroJS 网站正在处理的一些 Markdown 内容。

单根标题

这个标准的 SEO 友好的标题集有一个 H1,后跟其他标题

作为降价

# Main heading
## Sub heading 1
### More info
## Sub heading 2
### Even more info
## Sub heading 3 (edge case)
##### Deep nesting

作为平面 javascript 数组

headings = [
  { depth: 1, text: \'Main heading\' },
  { depth: 2, text: \'Sub heading 1\' },
  { depth: 3, text: \'More info\' },
  { depth: 2, text: \'Sub heading 2\' },
  { depth: 3, text: \'Even more info\' },
  { depth: 2, text: \'Sub heading 3 (edge case)\' },
  { depth: 6, text: \'Deep nesting\' },
]

作为 javascript 层次结构

list_of_heading_heirachies = [
  { text: \'Main heading\', headings: [
    { text: \'Sub heading 1\', headings: [
      { text: \'More info\', headings: [] },
    ] },
    { text: \'Sub heading 2\', headings: [
      { text: \'Even more info\', headings: [] },
    ] },
    { text: \'Sub heading 3 (edge case)\', headings: [
      { text: \'Deep nesting\', headings: [] },
    ] }
  ]}
]

console.log(list_of_heading_heirachies.length);
// => 1

多个根标题

这个降价(常见于 listicle 页面)没有像上面那样的单个根节点,而是有多个 H2

作为降价

## Why is it done
### Why abc
### Why xyz
## How is it done
### How reason 1
### How reason 2
#### More info
## Conclusion

作为平面 javascript 数组

headings = [
  { depth: 2, \'Why is it done\' },
  { depth: 3, \'Why abc\' },
  { depth: 3, \'Why xyz\' },
  { depth: 2, \'How is it done\' },
  { depth: 3, \'How reason 1\' },
  { depth: 3, \'How reason 2\' },
  { depth: 4, \'More info\' },
  { depth: 2, \'Conclusion\' }
]

作为 javascript 层次结构

list_of_heading_heirachies = [
  { text: \'Why is it done\', headings: [
    { text: \'Why abc\', headings: [] },
    { text: \'Why xyz\', headings: [] },
  ] },
  { text: \'How is it done\', headings: [
    { text: \'How reason 1\', headings: [] },
    { text: \'How reason 2\', headings: [
      { text: \'More info\', headings: [] },
    ] },
  ] },
  { text: \'Conclusion\', headings: [] }
]

console.log(list_of_heading_heirachies.length);
// => 3

非常规标题列表

当一般内容标题之前有元数据或面包屑数据时,就会出现这种非常规的标题列表

#### Home -> Blog -> Some Articles
### By Ben Hurr
#### 24th, Sep, 2022
# Some cool Article
## Why abc
### info on why
### more info on why
## How
### How we did it
## Conclusion

作为平面 javascript 数组

headings = [
  { depth: 4, text: \'Home -> Blog -> Some Articles\' },
  { depth: 3, text: \'By Ben Hurr\' },
  { depth: 4, text: \'24th, Sep, 2022\' },
  { depth: 1, text: \'Some cool Article\' },
  { depth: 2, text: \'Why abc\' },
  { depth: 3, text: \'info on why\' },
  { depth: 3, text: \'more info on why\' },
  { depth: 2, text: \'How\' },
  { depth: 3, text: \'How we did it\' },
  { depth: 2, text: \'Conclusion\' },
]

作为 javascript 层次结构

list_of_heading_heirachies = [
  { text: \'Home -> Blog -> Some Articles\', headings: [] },
  { text: \'By Ben Hurr\', headings: [
    { text: \'24th, Sep, 2022\', headings: [] },
  ] },
  { text: \'Some cool Article\', headings: [
    { text: \'Why abc\', headings: [
      { text: \'info on why\', headings: [] },
      { text: \'more info on why\', headings: [] },
    ] },
    { text: \'How\', headings: [
      { text: \'How we did it\', headings: [] },
    ] },
    { text: \'Conclusion\', headings: [] },
  ] },
]

console.log(list_of_heading_heirachies.length);
// => 3
  • 你尝试了什么?为什么这个问题是关于 Javascript、C#、Ruby 和 TypeScript 的?你肯定不是在尝试用每种语言做同样的事情吗?
  • 无论如何,您可能可以使用堆栈来跟踪当前父级(当存在更深的标题时推送,弹出直到到达下一个标题深度等)然后您知道在哪里应用标题。
  • 我目前正在通过单元测试探索 ruby​​ 中的堆栈弹出/推送方法,但是如果有人使用另一种语言的算法解决了问题,那么我自己会适应 ruby​​。

标签: javascript typescript algorithm


【解决方案1】:

我能够在 Typescript 中解决这个问题。

我没有这门语言的经验,虽然它工作得很好,但有更多经验的人可能能够改进这段代码。

我的目标是获取任何 H1..6 列表并将其转换为层次结构,然后过滤到最终层次结构以生成有用的目录。

输出屏幕截图

标题.ts

export default class Heading {
  depth: number;
  sequence: number;
  text: string;
  opts: any;

  parent: Heading | null;
  headings: Heading[] | null;

  constructor(depth: number, sequence: number, text: string, opts: any = {}) {
    this.depth = depth;
    this.sequence = sequence;
    this.text = text;
    this.opts = opts;
    this.parent = null;
    this.headings = null;
  }

  add_heading(heading: Heading) {
    heading.parent = this;
    if (this.headings === null) {
      this.headings = [];
    }
    this.headings.push(heading);
  }

  data() {
    let result: any = {
      depth: this.depth,
      sequence: this.sequence,
      text: this.text,
      ...this.opts,
      headings: this.headings?.map((h) => h.data()),
    };
    return result;
  }
}

运行时使用代码

let filterHeadings = headings
  .filter((heading) => heading.depth === 2 || heading.depth === 3)
  .map((heading, index) => new Heading(heading.depth, index, heading.text, { slug: heading.slug } ));

const toc = new CreateToc(filterHeadings);
toc.process();
let tocHeadings = toc.data();

创建目录

import type Heading from './heading';

export default class CreateToc {
  headings: Heading[];
  hierarchies: Heading[];
  hierarchy_root: Heading | null;
  current_heading: Heading | null;

  constructor(headings: Heading[]) {
    this.headings = headings;
    this.hierarchies = [];
    this.hierarchy_root = null;
    this.current_heading = null;
  }

  process() {
    this.headings.forEach((heading) => {
      this.process_heading(heading);
    });
  }

  data() {
    return this.hierarchies.map((h) => h.data());
  }

  private process_heading(heading: Heading) {
    if (this.hierarchy_root === null) {
      return this.new_hierarchy(heading);
    }
    if (heading.depth > this.current_heading!.depth) {
      return this.add_child_heading(heading);
    }
    if (heading.depth === this.current_heading!.depth) {
      return this.add_sibling_heading(heading);
    }

    // When an up-stream heading is encountered, you have to navigate up to the existing
    // hierarchy or create a new hierarchy out side of the current hierarchy
    if (heading.depth <= this.hierarchy_root.depth) {
      return this.new_hierarchy(heading);
    }
    if (heading.depth < this.current_heading!.depth) {
      return this.add_upstream_heading(heading);
    }
  }

  private new_hierarchy(heading: Heading) {
    this.hierarchies.push(heading);
    this.hierarchy_root = heading;
    this.current_heading = heading;
  }

  private add_child_heading(heading: Heading) {
    this.current_heading!.add_heading(heading);
    this.current_heading = heading;
  }

  private add_sibling_heading(heading: Heading) {
    if (this.current_heading!.parent === null) {
      return;
    }
    this.current_heading = this.current_heading!.parent;
    this.add_child_heading(heading);
  }

  private add_upstream_heading(heading: Heading) {
    while (this.current_heading !== null && heading.depth < this.current_heading.depth) {
      if (this.current_heading.parent === null) {
        throw new Error("current_heading.parent is null");
      }
      this.current_heading = this.current_heading.parent;
    }
    this.add_sibling_heading(heading);
  }
}

【讨论】:

    【解决方案2】:

    这似乎有很多代码可以解决这个问题。

    我会将节点列表折叠成一个节点数组,这些节点仍然可以添加子节点,按深度排序。我们从深度为 0 的初始节点开始。最后我们可以简单地提取第一个节点的子节点。

    如果我们不介意在您的节点上保留 depth 属性,那么就可以了。如果我们真的不想要它们,那么可以将其包装在一个简单的函数中,该函数递归地删除这些属性。

    这是一个版本:

    const removeDepth = (xs) => xs .map (
      ({headings = [], depth, ...rest}) => ({...rest, headings: removeDepth(headings)})
    )
    const makeTree = (xs) => removeDepth (xs .reduce (
      (hs, {depth, text}) => {
        const idx = hs .findLastIndex (x => x .depth < depth)
        const node = {text, headings: [], depth}
        hs [idx] .headings .push (node)
        return [...hs.slice (0, idx + 1), node]
      },
      [{text: null, headings: [], depth: 0}]
    ) [0] .headings)
    
    
    const singleRoot = [{depth: 1, text: 'Main heading'}, {depth: 2, text: 'Sub heading 1'}, {depth: 3, text: 'More info'}, {depth: 2, text: 'Sub heading 2'}, {depth: 3, text: 'Even more info'}, {depth: 2, text: 'Sub heading 3 (edge case)'}, {depth: 6, text: 'Deep nesting'}]
    const multipleRoot = [{depth: 2, text: 'Why is it done'}, {depth: 3, text: 'Why abc'}, {depth: 3, text: 'Why xyz'}, {depth: 2, text: 'How is it done'}, {depth: 3, text: 'How reason 1'}, {depth: 3, text: 'How reason 2'}, {depth: 4, text: 'More info'}, {depth: 2, text: 'Conclusion'}]
    const nonCon = [{depth: 4, text: 'Home -> Blog -> Some Articles'}, {depth: 3, text: 'By Ben Hurr'}, {depth: 4, text: '24th, Sep, 2022'}, {depth: 1, text: 'Some cool Article'}, {depth: 2, text: 'Why abc'}, {depth: 3, text: 'info on why'}, {depth: 3, text: 'more info on why'}, {depth: 2, text: 'How'}, {depth: 3, text: 'How we did it'}, {depth: 2, text: 'Conclusion'}]
    
    console .log ('Single root: ', makeTree (singleRoot))
    console .log ('Multiple root: ', makeTree (multipleRoot))
    console .log ('Non-conventional headings list: ', makeTree (nonCon))
    .as-console-wrapper {max-height: 100% !important; top: 0}

    真正的诀窍是维护当前的开放数组列表。我们通过在列表中找到深度低于当前节点的最后一个节点来维护它,将当前节点添加为该节点的子节点,在该点切割列表,并将当前节点添加到其中。当我们从您的示例之一中添加一个又一个节点时,该数组的运行方式如下:

    //Initial list: 
    [
      {depth: 0, headings: [], text: null}
    ]
    
    // After breadcrumbs (i.e. "Home -> Blog -> Some Articles"): (4)
    [
      {depth: 0, headings: [<breadcrumbs>], text: null}
      {depth: 4, headings: [], text: "Home -> Blog -> Some Articles"}
    ]
    
    // After "By Ben Hurr": (3):
    [
      {depth: 0, headings: [<breadcrumbs>, <By Ben>], text: null}
      {depth: 3, headings: [], text: "By Ben Hurr"}
    ]
    
    
    // After "24th Sept" (4):
    [
      {depth: 0, headings: [<breadcrumbs>, <By Ben>], text: null}
      {depth: 3, headings: [<24th Sept>], text: "By Ben Hurr"}
      {depth: 4, headings: [], text: "24th, Sep, 2022"}
    ]
    
    
    // After "Some Cool Article" (1):
    [
      {depth: 0, headings: [<breadcrumbs>, <By Ben>, <Some Cool>], text: null}
      {depth: 1, headings: [], text: 'Some Cool Article'}
    ]
    
    // After "Why ABC" (2):
    [
      {depth: 0, headings: [<breadcrumbs>, <By Ben>, <Some Cool>], text: null}
      {depth: 1, headings: [<Why abc>], text: 'Some Cool Article'}
      {depth: 2, headings: [], text: 'Why abc'}
    ]
    
    // ...
    
    // Finally
    
    // After "Conclusion" (2):
    [
      {depth: 0, headings: [<breadcrumbs>, <By Ben>, <Some Cool>], text: null}
      {depth: 1, headings: [<Why abc>, <How>, <Conclusion>], text: 'Some Cool Article'}
      {depth: 2, headings: [], text: 'Conclusion'}
    ]
    

    我认为这比您的方法简单得多,但我必须承认没有仔细阅读您的方法;对于一个有趣但并不过分复杂的问题来说,这似乎代码太多了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-02-06
      • 1970-01-01
      • 1970-01-01
      • 2023-03-31
      • 1970-01-01
      相关资源
      最近更新 更多