|
| 1 | +<template> |
| 2 | + <NestedList :items="items"></NestedList> |
| 3 | +</template> |
| 4 | + |
| 5 | +<script setup lang="ts"> |
| 6 | +import NestedList, { Item } from './NestedList.vue'; |
| 7 | +import { ref, onMounted, nextTick } from 'vue'; |
| 8 | +import { onContentUpdated } from 'vuepress/client'; |
| 9 | +
|
| 10 | +const items = ref([] as Item[]); |
| 11 | +
|
| 12 | +function getHeaders(): NodeListOf<HTMLHeadingElement> { |
| 13 | + return document.querySelectorAll('h1, h2, h3'); |
| 14 | +} |
| 15 | +
|
| 16 | +type OrganizedHeader = { |
| 17 | + element?: HTMLHeadingElement; |
| 18 | + children: OrganizedHeader[]; |
| 19 | +}; |
| 20 | +function organizeHeaders( |
| 21 | + headers: HTMLHeadingElement[], |
| 22 | + index: [number] = [0], |
| 23 | + level: number = 1, |
| 24 | +): OrganizedHeader { |
| 25 | + const node: OrganizedHeader = { children: [] }; |
| 26 | +
|
| 27 | + while (index[0] < headers.length) { |
| 28 | + const header = headers[index[0]]; |
| 29 | + const headerLevel = Number(header.tagName.slice(1)); // "H2" -> 2 |
| 30 | +
|
| 31 | + // if we hit a header above our current level, we are done here |
| 32 | + if (headerLevel < level) break; |
| 33 | +
|
| 34 | + // if we hit a header deeper than expected, let the caller handle it as children |
| 35 | + if (headerLevel > level) break; |
| 36 | +
|
| 37 | + // headerLevel === level: consume this header and attach its children. |
| 38 | + index[0]++; |
| 39 | +
|
| 40 | + const entry: OrganizedHeader = { element: header, children: [] }; |
| 41 | +
|
| 42 | + // children are the following headers with level+1 (and their descendants) |
| 43 | + const childrenTree = organizeHeaders(headers, index, level + 1); |
| 44 | + entry.children = childrenTree.children; |
| 45 | +
|
| 46 | + node.children.push(entry); |
| 47 | + } |
| 48 | +
|
| 49 | + return node; |
| 50 | +} |
| 51 | +
|
| 52 | +function filterHeaders(root: OrganizedHeader): OrganizedHeader { |
| 53 | + const wanted = [ |
| 54 | + 'Highlights and themes of this release', |
| 55 | + 'Changes', |
| 56 | + 'Notes for plugin developers', |
| 57 | + 'Hall of fame', |
| 58 | + 'Full changelog', |
| 59 | + ].map((section) => section.toLowerCase()); |
| 60 | +
|
| 61 | + return { |
| 62 | + ...root, |
| 63 | + children: root.children.filter((section) => |
| 64 | + wanted.some((wanted) => |
| 65 | + section.element!.innerText.toLowerCase().startsWith(wanted), |
| 66 | + ), |
| 67 | + ), |
| 68 | + }; |
| 69 | +} |
| 70 | +
|
| 71 | +function generateItem(header: OrganizedHeader): Item { |
| 72 | + let slug = '#' + header.element?.id; |
| 73 | + let content = header.element?.querySelector('span')?.innerHTML; |
| 74 | +
|
| 75 | + const i = content?.lastIndexOf('['); |
| 76 | + if (i !== -1) content = content?.slice(0, i).trim(); |
| 77 | +
|
| 78 | + return { |
| 79 | + label: `<em><a href="${slug}">${content}</a></em>`, |
| 80 | + children: header.children.map(generateItem), |
| 81 | + }; |
| 82 | +} |
| 83 | +
|
| 84 | +async function refresh() { |
| 85 | + await nextTick(); |
| 86 | + const headers = Array.from(getHeaders()); |
| 87 | + const organized = organizeHeaders(headers); |
| 88 | + const filtered = filterHeaders(organized); |
| 89 | + const item = generateItem(filtered); |
| 90 | + items.value = item.children; |
| 91 | +} |
| 92 | +
|
| 93 | +onMounted(refresh); |
| 94 | +onContentUpdated(refresh); |
| 95 | +</script> |
0 commit comments