Skip to content

Commit 4955609

Browse files
committed
feat(discorddocs): display route and first section of body, if available
1 parent 1490bce commit 4955609

File tree

3 files changed

+158
-12
lines changed

3 files changed

+158
-12
lines changed

src/functions/algoliaResponse.ts

+14-12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { hideLinkEmbed, hyperlink, userMention, italic, bold } from '@discordjs/builders';
1+
import { hideLinkEmbed, hyperlink, userMention, italic, bold, inlineCode } from '@discordjs/builders';
22
import pkg from 'he';
33
import type { Response } from 'polka';
44
import { fetch } from 'undici';
55
import type { AlgoliaHit } from '../types/algolia.js';
66
import { expandAlgoliaObjectId } from '../util/compactAlgoliaId.js';
77
import { API_BASE_ALGOLIA } from '../util/constants.js';
8+
import { fetchDocsBody } from '../util/discordDocs.js';
89
import { prepareResponse, prepareErrorResponse } from '../util/respond.js';
910
import { truncate } from '../util/truncate.js';
1011
import { resolveHitToNamestring } from './autocomplete/algoliaAutoComplete.js';
@@ -35,17 +36,18 @@ export async function algoliaResponse(
3536
},
3637
}).then(async (res) => res.json())) as AlgoliaHit;
3738

38-
prepareResponse(
39-
res,
40-
`${target ? `${italic(`Suggestion for ${userMention(target)}:`)}\n` : ''}<:${emojiName}:${emojiId}> ${bold(
41-
resolveHitToNamestring(hit),
42-
)}${hit.content?.length ? `\n${truncate(decode(hit.content), 300)}` : ''}\n${hyperlink(
43-
'read more',
44-
hideLinkEmbed(hit.url),
45-
)}`,
46-
ephemeral ?? false,
47-
target ? [target] : [],
48-
);
39+
const docsBody = hit.url.includes('discord.com') ? await fetchDocsBody(hit.url) : null;
40+
const headlineSuffix = docsBody?.heading ? inlineCode(`${docsBody.heading.verb} ${docsBody.heading.route}`) : null;
41+
42+
const contentParts = [
43+
target ? `${italic(`Suggestion for ${userMention(target)}:`)}` : null,
44+
`<:${emojiName}:${emojiId}> ${bold(resolveHitToNamestring(hit))}${headlineSuffix ? ` ${headlineSuffix}` : ''}`,
45+
hit.content?.length ? `${truncate(decode(hit.content), 300)}` : null,
46+
docsBody?.lines.length ? docsBody.lines.at(0) : null,
47+
`${hyperlink('read more', hideLinkEmbed(hit.url))}`,
48+
].filter(Boolean) as string[];
49+
50+
prepareResponse(res, contentParts.join('\n'), ephemeral ?? false, target ? [target] : []);
4951
} catch {
5052
prepareErrorResponse(res, 'Invalid result. Make sure to select an entry from the autocomplete.');
5153
}

src/util/discordDocs.ts

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { urlOption } from './url.js';
2+
3+
export function toMdFilename(name: string) {
4+
return name
5+
.split('-')
6+
.map((part) => `${part.at(0)?.toUpperCase()}${part.slice(1).toLowerCase()}`)
7+
.join('');
8+
}
9+
10+
export function resolveResourceFromDocsURL(link: string) {
11+
const url = urlOption(link);
12+
if (!url) {
13+
return null;
14+
}
15+
16+
const pathParts = url.pathname.split('/').slice(2);
17+
if (!pathParts.length) {
18+
return null;
19+
}
20+
21+
return {
22+
docsAnchor: url.hash,
23+
githubUrl: `https://raw.githubusercontent.com/discord/discord-api-docs/main/${pathParts
24+
.slice(0, -1)
25+
.join('/')}/${toMdFilename(pathParts.at(-1)!)}.md`,
26+
};
27+
}
28+
29+
type Heading = {
30+
docs_anchor: string;
31+
label: string;
32+
route: string;
33+
verb: string;
34+
};
35+
36+
function parseHeadline(text: string): Heading | null {
37+
const match = /#{1,7} (?<label>.*) % (?<verb>\w{3,6}) (?<route>.*)/g.exec(text);
38+
if (!match) {
39+
return null;
40+
}
41+
42+
const { groups } = match;
43+
return {
44+
docs_anchor: `#${groups!.label.replaceAll(' ', '-').toLowerCase()}`,
45+
label: groups!.label,
46+
verb: groups!.verb,
47+
route: groups!.route,
48+
};
49+
}
50+
51+
// https://raw.githubusercontent.com/discord/discord-api-docs/main/docs/resources/user/User.md
52+
// https://raw.githubusercontent.com/discord/discord-api-docs/main/docs/resources/User.md
53+
54+
type ParsedSection = {
55+
heading: Heading | null;
56+
headline: string;
57+
lines: string[];
58+
};
59+
60+
function cleanLine(line: string) {
61+
return line
62+
.replaceAll(/\[(.*?)]\(.*?\)/g, '$1')
63+
.replaceAll(/{(.*?)#.*?}/g, '$1')
64+
.trim();
65+
}
66+
67+
export function parseSections(content: string): ParsedSection[] {
68+
const res = [];
69+
const section: ParsedSection = {
70+
heading: null,
71+
lines: [],
72+
headline: '',
73+
};
74+
75+
for (const line of content.split('\n')) {
76+
const cleanedLine = cleanLine(line);
77+
78+
if (line.startsWith('>')) {
79+
continue;
80+
}
81+
82+
if (line.startsWith('#')) {
83+
if (section.headline.length) {
84+
res.push({ ...section });
85+
86+
section.lines = [];
87+
section.heading = null;
88+
section.headline = '';
89+
}
90+
91+
section.headline = cleanedLine;
92+
const parsedHeading = parseHeadline(cleanedLine);
93+
if (parsedHeading) {
94+
section.heading = parsedHeading;
95+
}
96+
97+
continue;
98+
}
99+
100+
if (cleanedLine.length) {
101+
section.lines.push(cleanedLine);
102+
}
103+
}
104+
105+
return res;
106+
}
107+
108+
export function findRelevantDocsSection(query: string, docsMd: string) {
109+
const sections = parseSections(docsMd);
110+
for (const section of sections) {
111+
if (section.heading?.docs_anchor === query) {
112+
return section;
113+
}
114+
}
115+
}
116+
117+
export async function fetchDocsBody(link: string) {
118+
const githubResource = resolveResourceFromDocsURL(link);
119+
if (!githubResource) {
120+
return null;
121+
}
122+
123+
const docsMd = await fetch(githubResource.githubUrl).then(async (res) => res.text());
124+
const section = findRelevantDocsSection(githubResource.docsAnchor, docsMd);
125+
126+
if (section) {
127+
return section;
128+
}
129+
}

src/util/url.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { URL } from 'node:url';
2+
3+
/**
4+
* Transform a link into an URL or null, if invalid
5+
*
6+
* @param url - The link to transform
7+
* @returns The URL instance, if valid
8+
*/
9+
export function urlOption(url: string) {
10+
try {
11+
return new URL(url);
12+
} catch {
13+
return null;
14+
}
15+
}

0 commit comments

Comments
 (0)