Skip to content

Commit da3a4eb

Browse files
committed
docs: add formattable/markup semantic type badges to API reference with auto-linking to /formatting and /keyboards/overview, style Returns badge with dimmed label and 6px gap, add InputFile auto-linking to /files/media-upload, support enum values rendering, extend typeStr/returnBadge for ObjectFile type
1 parent 526b4db commit da3a4eb

File tree

2 files changed

+166
-30
lines changed

2 files changed

+166
-30
lines changed

docs/.vitepress/theme/style.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,12 @@ a.api-param-docs-link:hover {
279279
background: var(--vp-c-brand-soft);
280280
color: var(--vp-c-brand-1);
281281
border: 1px solid var(--vp-c-brand-2);
282+
gap: 6px;
283+
}
284+
285+
.api-badge.returns .returns-label {
286+
opacity: 0.65;
287+
font-weight: 400;
282288
}
283289

284290
.api-badge.returns a {
@@ -288,6 +294,34 @@ a.api-param-docs-link:hover {
288294
text-underline-offset: 2px;
289295
}
290296

297+
.api-badge.formattable {
298+
background: #14532d22;
299+
color: #22c55e;
300+
border: 1px solid #22c55e40;
301+
text-decoration: none !important;
302+
transition: opacity 0.2s;
303+
}
304+
305+
.api-badge.markup {
306+
background: #1e3a5f22;
307+
color: #60a5fa;
308+
border: 1px solid #60a5fa40;
309+
text-decoration: none !important;
310+
transition: opacity 0.2s;
311+
}
312+
313+
.dark .api-badge.formattable {
314+
background: #14532d33;
315+
color: #4ade80;
316+
border-color: #22c55e50;
317+
}
318+
319+
.dark .api-badge.markup {
320+
background: #1e3a5f33;
321+
color: #93c5fd;
322+
border-color: #60a5fa50;
323+
}
324+
291325
.api-badge.official {
292326
background: var(--vp-c-default-soft);
293327
color: var(--vp-c-text-2);

scripts/generate-api-docs.ts

Lines changed: 132 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
* bun run gen:api --dry-run — print stats without writing files
1313
*/
1414

15+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16+
import { join } from "node:path";
17+
import { fileURLToPath } from "node:url";
1518
import { getCustomSchema } from "@gramio/schema-parser";
1619
import type {
1720
Field,
@@ -23,10 +26,8 @@ import type {
2326
FieldReference,
2427
FieldString,
2528
Method,
29+
ObjectFile,
2630
} from "@gramio/schema-parser";
27-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
28-
import { join } from "node:path";
29-
import { fileURLToPath } from "node:url";
3031

3132
// ── Paths ────────────────────────────────────────────────────────────────────
3233

@@ -54,7 +55,8 @@ type FieldNoKey =
5455
| Omit<FieldBoolean, "key">
5556
| Omit<FieldArray, "key">
5657
| Omit<FieldReference, "key">
57-
| Omit<FieldOneOf, "key">;
58+
| Omit<FieldOneOf, "key">
59+
| Omit<ObjectFile, "key">;
5860

5961
/** Converts a schema field type to the display string used in <ApiParam type="..."> */
6062
function typeStr(f: FieldNoKey): string {
@@ -75,9 +77,13 @@ function typeStr(f: FieldNoKey): string {
7577
return `${typeStr(f.arrayOf)}[]`;
7678
case "one_of":
7779
return f.variants.map(typeStr).join(" | ");
80+
case "file":
81+
return "InputFile";
7882
}
7983
}
8084

85+
const MARKUP_PATTERN = /Markup$|^ReplyKeyboardRemove$|^ForceReply$/;
86+
8187
const TG_BASE = "https://core.telegram.org";
8288
const TG_API = `${TG_BASE}/bots/api`;
8389

@@ -101,15 +107,17 @@ function fixTelegramLinks(s: string): string {
101107

102108
/** Escapes a string for use in an HTML attribute value (double-quoted) */
103109
function escAttr(s: string): string {
104-
return fixTelegramLinks(s)
105-
.replace(/&/g, "&amp;")
106-
.replace(/</g, "&lt;")
107-
.replace(/>/g, "&gt;")
108-
.replace(/"/g, "&quot;")
109-
// Smart/curly double quotes from the schema — also escape them
110-
.replace(/\u201c|\u201d/g, "&quot;")
111-
.replace(/\n/g, " ")
112-
.trim();
110+
return (
111+
fixTelegramLinks(s)
112+
.replace(/&/g, "&amp;")
113+
.replace(/</g, "&lt;")
114+
.replace(/>/g, "&gt;")
115+
.replace(/"/g, "&quot;")
116+
// Smart/curly double quotes from the schema — also escape them
117+
.replace(/\u201c|\u201d/g, "&quot;")
118+
.replace(/\n/g, " ")
119+
.trim()
120+
);
113121
}
114122

115123
/** Converts a Field to an <ApiParam .../> component line */
@@ -118,23 +126,60 @@ function toApiParam(field: Field): string {
118126

119127
const attrs: string[] = [`name="${field.key}"`, `type="${type}"`];
120128
if (field.required) attrs.push("required");
121-
if (field.description) attrs.push(`description="${escAttr(field.description)}"`);
129+
if (field.description)
130+
attrs.push(`description="${escAttr(field.description)}"`);
122131

123132
if (field.type === "integer" || field.type === "float") {
124133
if (field.min !== undefined) attrs.push(`:min="${field.min}"`);
125134
if (field.max !== undefined) attrs.push(`:max="${field.max}"`);
126-
if (field.default !== undefined) attrs.push(`:defaultValue="${field.default}"`);
135+
if (field.default !== undefined)
136+
attrs.push(`:defaultValue="${field.default}"`);
127137
if (field.enum && field.enum.length > 0)
128138
attrs.push(`:enumValues='${JSON.stringify(field.enum)}'`);
129139
}
130140

131141
if (field.type === "string") {
132142
if (field.minLen !== undefined) attrs.push(`:minLen="${field.minLen}"`);
133143
if (field.maxLen !== undefined) attrs.push(`:maxLen="${field.maxLen}"`);
134-
if (field.default !== undefined) attrs.push(`defaultValue="${escAttr(field.default)}"`);
135-
if (field.const !== undefined) attrs.push(`constValue="${escAttr(field.const)}"`);
144+
if (field.default !== undefined)
145+
attrs.push(`defaultValue="${escAttr(field.default)}"`);
146+
if (field.const !== undefined)
147+
attrs.push(`constValue="${escAttr(field.const)}"`);
136148
if (field.enum && field.enum.length > 0)
137149
attrs.push(`:enumValues='${JSON.stringify(field.enum)}'`);
150+
const st = field.semanticType;
151+
if (st) {
152+
attrs.push(`semanticType="${st}"`);
153+
if (st === "formattable") attrs.push(`docsLink="/formatting"`);
154+
}
155+
}
156+
157+
if (
158+
(field.type === "reference" && field.reference.name === "InputFile") ||
159+
(field.type === "one_of" &&
160+
field.variants.some(
161+
(f) => f.type === "reference" && f.reference.name === "InputFile",
162+
))
163+
) {
164+
attrs.push(`docsLink="/files/media-upload"`);
165+
}
166+
167+
if (
168+
(field.type === "reference" && MARKUP_PATTERN.test(field.reference.name)) ||
169+
(field.type === "one_of" &&
170+
field.variants.some(
171+
(f) => f.type === "reference" && MARKUP_PATTERN.test(f.reference.name),
172+
))
173+
) {
174+
attrs.push(`docsLink="/keyboards/overview"`);
175+
}
176+
177+
if (field.type === "array" && field.arrayOf.type === "string") {
178+
const st = field.arrayOf.semanticType;
179+
if (st) {
180+
attrs.push(`semanticType="${st}"`);
181+
if (st === "formattable") attrs.push(`docsLink="/formatting"`);
182+
}
138183
}
139184

140185
return `<ApiParam ${attrs.join(" ")} />`;
@@ -167,6 +212,8 @@ function returnBadge(f: FieldNoKey): string {
167212
return f.variants.map(returnBadge).join(" | ");
168213
case "boolean":
169214
return "True";
215+
case "file":
216+
return "InputFile";
170217
default:
171218
return typeStr(f);
172219
}
@@ -195,6 +242,26 @@ function returnDesc(f: FieldNoKey): string {
195242
const GEN_START = "<!-- GENERATED:START -->";
196243
const GEN_END = "<!-- GENERATED:END -->";
197244

245+
function hasFormattableParam(params: Field[]): boolean {
246+
return params.some((f) => {
247+
if (f.type === "string") return (f as any).semanticType === "formattable";
248+
if (f.type === "array" && f.arrayOf.type === "string")
249+
return (f.arrayOf as any).semanticType === "formattable";
250+
return false;
251+
});
252+
}
253+
254+
function hasMarkupParam(params: Field[]): boolean {
255+
return params.some((f) => {
256+
if (f.type === "reference") return MARKUP_PATTERN.test(f.reference.name);
257+
if (f.type === "one_of")
258+
return f.variants.some(
259+
(v) => v.type === "reference" && MARKUP_PATTERN.test(v.reference.name),
260+
);
261+
return false;
262+
});
263+
}
264+
198265
function buildMethodBlock(method: Method): string {
199266
// The library types Method.returns as Omit<Field,"key"> (non-distributed),
200267
// cast to our distributed FieldNoKey so switch narrowing works inside helpers.
@@ -203,14 +270,22 @@ function buildMethodBlock(method: Method): string {
203270
const params = method.parameters.map(toApiParam).join("\n\n");
204271
const paramsSection =
205272
method.parameters.length > 0 ? `## Parameters\n\n${params}\n\n` : "";
206-
const description = method.description ? fixTelegramLinks(method.description) : "";
273+
const description = method.description
274+
? fixTelegramLinks(method.description)
275+
: "";
207276
const multipartBadge = method.hasMultipart
208-
? `\n <span class="api-badge multipart">📎 Accepts files</span>`
277+
? `\n <a class="api-badge multipart" href="/files/media-upload">📎 Accepts files</a>`
278+
: "";
279+
const formattableBadge = hasFormattableParam(method.parameters)
280+
? `\n <a class="api-badge formattable" href="/formatting">✏️ Formattable text</a>`
281+
: "";
282+
const markupBadge = hasMarkupParam(method.parameters)
283+
? `\n <a class="api-badge markup" href="/keyboards/overview">⌨️ Keyboards</a>`
209284
: "";
210285

211286
return `${GEN_START}
212287
<div class="api-badge-row">
213-
<span class="api-badge returns">Returns: ${returnBadge(returns)}</span>${multipartBadge}
288+
<span class="api-badge returns"><span class="returns-label">Returns:</span> ${returnBadge(returns)}</span>${multipartBadge}${formattableBadge}${markupBadge}
214289
<a class="api-badge official" href="${officialUrl}" target="_blank" rel="noopener">Official docs ↗</a>
215290
</div>
216291
@@ -219,6 +294,7 @@ ${description}
219294
${paramsSection}## Returns
220295
221296
${returnDesc(returns)}
297+
222298
${GEN_END}`;
223299
}
224300

@@ -228,12 +304,22 @@ function buildObjectBlock(obj: {
228304
description?: string;
229305
fields?: Field[];
230306
oneOf?: FieldNoKey[];
307+
type?: string;
308+
values?: string[];
309+
semanticType?: string;
231310
}): string {
232311
const officialUrl = `https://core.telegram.org/bots/api${obj.anchor}`;
233312
const description = obj.description ? fixTelegramLinks(obj.description) : "";
313+
const markupBadge =
314+
obj.semanticType === "markup"
315+
? `\n <a class="api-badge docs" href="/keyboards/overview">⌨️ Keyboard type</a>`
316+
: "";
234317

235318
let fieldsSection = "";
236-
if (obj.fields && obj.fields.length > 0) {
319+
if (obj.type === "enum" && obj.values && obj.values.length > 0) {
320+
const valueList = obj.values.map((v) => `- \`"${v}"\``).join("\n");
321+
fieldsSection = `## Values\n\n${valueList}\n`;
322+
} else if (obj.fields && obj.fields.length > 0) {
237323
fieldsSection = `## Fields\n\n${obj.fields.map(toApiParam).join("\n\n")}\n`;
238324
} else if (obj.oneOf && obj.oneOf.length > 0) {
239325
const variants = obj.oneOf
@@ -247,7 +333,7 @@ function buildObjectBlock(obj: {
247333

248334
return `${GEN_START}
249335
<div class="api-badge-row">
250-
<a class="api-badge official" href="${officialUrl}" target="_blank" rel="noopener">Official docs ↗</a>
336+
<a class="api-badge official" href="${officialUrl}" target="_blank" rel="noopener">Official docs ↗</a>${markupBadge}
251337
</div>
252338
253339
${description}
@@ -272,7 +358,9 @@ function applyGeneratedBlock(existing: string, newBlock: string): string {
272358
}
273359

274360
return (
275-
existing.slice(0, startIdx) + newBlock + existing.slice(endIdx + GEN_END.length)
361+
existing.slice(0, startIdx) +
362+
newBlock +
363+
existing.slice(endIdx + GEN_END.length)
276364
);
277365
}
278366

@@ -340,7 +428,12 @@ ${block}
340428

341429
// ── Skills index ─────────────────────────────────────────────────────────────
342430

343-
const SKILLS_INDEX = join(ROOT, "skills", "references", "telegram-api-index.md");
431+
const SKILLS_INDEX = join(
432+
ROOT,
433+
"skills",
434+
"references",
435+
"telegram-api-index.md",
436+
);
344437

345438
/**
346439
* Extracts the first sentence from a markdown description.
@@ -473,7 +566,8 @@ function updateSidebar(methods: string[], types: string[]): void {
473566
if (depth === 0) {
474567
let end = i + 1;
475568
if (content[end] === ",") end++; // include trailing comma
476-
content = content.slice(0, keyIdx) + newBlock + "," + content.slice(end);
569+
content =
570+
content.slice(0, keyIdx) + newBlock + "," + content.slice(end);
477571
break;
478572
}
479573
}
@@ -484,7 +578,9 @@ function updateSidebar(methods: string[], types: string[]): void {
484578
if (!DRY_RUN) {
485579
writeFileSync(EN_LOCALE, content, "utf-8");
486580
}
487-
console.log(`✅ ${DRY_RUN ? "[dry-run] Would update" : "Updated"} sidebar in en.locale.ts`);
581+
console.log(
582+
`✅ ${DRY_RUN ? "[dry-run] Would update" : "Updated"} sidebar in en.locale.ts`,
583+
);
488584
}
489585

490586
// ── Main ──────────────────────────────────────────────────────────────────────
@@ -519,7 +615,8 @@ for (const method of schema.methods) {
519615
skipped++;
520616
}
521617
} else {
522-
if (!DRY_RUN) writeFileSync(filePath, methodTemplate(method.name, block), "utf-8");
618+
if (!DRY_RUN)
619+
writeFileSync(filePath, methodTemplate(method.name, block), "utf-8");
523620
created++;
524621
console.log(` ✨ ${method.name}`);
525622
}
@@ -542,16 +639,21 @@ for (const obj of schema.objects) {
542639
skipped++;
543640
}
544641
} else {
545-
if (!DRY_RUN) writeFileSync(filePath, typeTemplate(obj.name, block), "utf-8");
642+
if (!DRY_RUN)
643+
writeFileSync(filePath, typeTemplate(obj.name, block), "utf-8");
546644
created++;
547645
console.log(` ✨ ${obj.name}`);
548646
}
549647
}
550648

551649
// ── Sidebar ───────────────────────────────────────────────────────────────────
552650
console.log("");
553-
const methodNames = schema.methods.map((m) => m.name).sort((a, b) => a.localeCompare(b));
554-
const typeNames = schema.objects.map((o) => o.name).sort((a, b) => a.localeCompare(b));
651+
const methodNames = schema.methods
652+
.map((m) => m.name)
653+
.sort((a, b) => a.localeCompare(b));
654+
const typeNames = schema.objects
655+
.map((o) => o.name)
656+
.sort((a, b) => a.localeCompare(b));
555657
updateSidebar(methodNames, typeNames);
556658

557659
// ── Skills index ──────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)