| | import cheerio from 'cheerio' |
| | import { range } from 'lodash-es' |
| |
|
| | import { renderContent } from '@/content-render/index' |
| | import type { Context } from '@/types' |
| |
|
| | interface MiniTocContents { |
| | href: string |
| | title: string |
| | } |
| |
|
| | export interface MiniTocItem { |
| | contents: MiniTocContents |
| | items?: MiniTocItem[] |
| | platform?: string |
| | } |
| |
|
| | interface FlatTocItem { |
| | contents: MiniTocContents |
| | headingLevel: number |
| | platform: string |
| | indentationLevel: number |
| | items?: FlatTocItem[] |
| | } |
| |
|
| | |
| | export default function getMiniTocItems( |
| | html: string, |
| | maxHeadingLevel = 2, |
| | headingScope = '', |
| | ): MiniTocItem[] { |
| | const $ = cheerio.load(html, { xmlMode: true }) |
| |
|
| | |
| | const selector = range(2, maxHeadingLevel + 1) |
| | .map((num) => `${headingScope} h${num}`) |
| | .join(', ') |
| | const headings = $(selector) |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | let mostImportantHeadingLevel: number | undefined |
| | const flatToc = headings |
| | .get() |
| | .filter((item) => { |
| | if (!item.parent || !item.parent.attribs) return true |
| | |
| | const { attribs } = item.parent |
| | return !('hidden' in attribs) |
| | }) |
| | .map((item) => { |
| | |
| | $('span', item).remove() |
| |
|
| | |
| | const anchor = $('a.heading-link', item) |
| | const href = anchor.attr('href') |
| | if (!href) { |
| | |
| | |
| | |
| | |
| | |
| | return null |
| | } |
| |
|
| | |
| | $('strong', item).map((i, el) => $(el).replaceWith($(el).contents())) |
| |
|
| | const contents: MiniTocContents = { href, title: $(item).text().trim() } |
| | const element = $(item)[0] as cheerio.TagElement |
| | const headingLevel = parseInt(element.name.match(/\d+/)![0], 10) || 0 |
| |
|
| | const platform = $(item).parent('.ghd-tool').attr('class') || '' |
| |
|
| | |
| | if (headingLevel < mostImportantHeadingLevel! || mostImportantHeadingLevel === undefined) { |
| | mostImportantHeadingLevel = headingLevel |
| | } |
| |
|
| | return { contents, headingLevel, platform } |
| | }) |
| | .filter(Boolean) |
| | .map((item) => { |
| | |
| | |
| | return { |
| | ...item!, |
| | indentationLevel: item!.headingLevel - mostImportantHeadingLevel!, |
| | } |
| | }) |
| |
|
| | |
| | const nestedToc = buildNestedToc(flatToc) |
| |
|
| | return minimalMiniToc(nestedToc) |
| | } |
| |
|
| | |
| | function buildNestedToc(allItems: FlatTocItem[], startIndex = 0): FlatTocItem[] { |
| | const startItem = allItems[startIndex] |
| | if (!startItem) { |
| | return [] |
| | } |
| | let curLevelIndentation = startItem.indentationLevel |
| | const currentLevel: FlatTocItem[] = [] |
| |
|
| | for (let cursor = startIndex; cursor < allItems.length; cursor++) { |
| | const cursorItem = allItems[cursor] |
| | const nextItem = allItems[cursor + 1] |
| | const nextItemIsNested = nextItem && nextItem.indentationLevel! > cursorItem.indentationLevel! |
| |
|
| | |
| | if (curLevelIndentation === cursorItem.indentationLevel) { |
| | currentLevel.push({ |
| | ...cursorItem, |
| | items: nextItemIsNested ? buildNestedToc(allItems, cursor + 1) : [], |
| | }) |
| | continue |
| | } |
| |
|
| | |
| | if (curLevelIndentation < cursorItem.indentationLevel) { |
| | continue |
| | } |
| |
|
| | |
| | if (curLevelIndentation > cursorItem.indentationLevel) { |
| | |
| | |
| | if (startIndex === 0) { |
| | curLevelIndentation = cursorItem.indentationLevel |
| | currentLevel.push({ |
| | ...cursorItem, |
| | items: nextItemIsNested ? buildNestedToc(allItems, cursor + 1) : [], |
| | }) |
| | continue |
| | } |
| | break |
| | } |
| | } |
| |
|
| | return currentLevel |
| | } |
| |
|
| | |
| | |
| | function minimalMiniToc(toc: FlatTocItem[]): MiniTocItem[] { |
| | return toc.map(({ platform, contents, items }) => { |
| | const minimal: MiniTocItem = { contents } |
| | const subItems = minimalMiniToc(items || []) |
| | if (subItems.length) minimal.items = subItems |
| | if (platform) minimal.platform = platform |
| | return minimal |
| | }) |
| | } |
| |
|
| | export async function getAutomatedPageMiniTocItems( |
| | items: string[], |
| | context: Context, |
| | depth = 2, |
| | markdownHeading = '', |
| | ): Promise<MiniTocItem[]> { |
| | const titles = |
| | markdownHeading + |
| | items |
| | .map((item) => { |
| | let title = '' |
| | for (let i = 0; i < depth; i++) { |
| | title += '#' |
| | } |
| | return `${title} ${item}\n` |
| | }) |
| | .join('') |
| |
|
| | const toc = await renderContent(titles, context) |
| | return getMiniTocItems(toc, depth, '') |
| | } |
| |
|