Skip to content

Commit 9c64201

Browse files
authored
Add components to make release changelogs easier (#2129)
1 parent b4eee60 commit 9c64201

6 files changed

Lines changed: 162 additions & 0 deletions

File tree

.vuepress/client.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@
66

77
import { defineClientConfig } from 'vuepress/client';
88
import BlogPosts from './components/BlogPosts.vue';
9+
import JumpToc from './components/JumpToc.vue';
10+
import PrBy from './components/PrBy.vue';
11+
import ReleaseToc from './components/ReleaseToc.vue';
912

1013
export default defineClientConfig({
1114
enhance({ app }) {
1215
app.component('BlogPosts', BlogPosts);
16+
app.component('JumpToc', JumpToc);
17+
app.component('PrBy', PrBy);
18+
app.component('ReleaseToc', ReleaseToc);
1319
},
1420
});

.vuepress/components/JumpToc.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<template>[<a href="#table-of-contents">toc</a>]</template>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<template>
2+
<ul>
3+
<li v-for="(item, i) in items" :key="i">
4+
<span v-html="item.label"></span>
5+
6+
<NestedList
7+
v-if="item.children && item.children.length"
8+
:items="item.children"
9+
/>
10+
</li>
11+
</ul>
12+
</template>
13+
14+
<script setup lang="ts">
15+
defineOptions({ name: 'NestedList' });
16+
17+
defineProps({
18+
items: {
19+
type: Array<Item>,
20+
required: true,
21+
},
22+
});
23+
24+
export interface Item {
25+
label: String;
26+
children: Item[];
27+
}
28+
</script>

.vuepress/components/PrBy.vue

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<template>
2+
<p style="font-size: 0.8rem">
3+
PR <a :href="prLink" target="_blank">#{{ pr }}</a> by
4+
<a :href="userLink" target="_blank">@{{ user }}</a>
5+
</p>
6+
</template>
7+
8+
<style scoped>
9+
p {
10+
font-size: 0.8rem;
11+
margin: 0;
12+
}
13+
</style>
14+
15+
<script setup lang="ts">
16+
const props = defineProps({
17+
pr: {
18+
type: Number,
19+
required: true,
20+
},
21+
user: {
22+
type: String,
23+
required: true,
24+
},
25+
});
26+
27+
const prLink = 'https://github.com/nushell/nushell/pull/' + props.pr;
28+
const userLink = 'https://github.com/' + props.user;
29+
</script>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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>

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,8 @@
2929
"engines": {
3030
"npm": ">=9.0.0",
3131
"node": ">=18.12.0"
32+
},
33+
"volta": {
34+
"node": "22.22.0"
3235
}
3336
}

0 commit comments

Comments
 (0)