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" ;
1518import { getCustomSchema } from "@gramio/schema-parser" ;
1619import 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="..."> */
6062function 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 = / M a r k u p $ | ^ R e p l y K e y b o a r d R e m o v e $ | ^ F o r c e R e p l y $ / ;
86+
8187const TG_BASE = "https://core.telegram.org" ;
8288const 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) */
103109function escAttr ( s : string ) : string {
104- return fixTelegramLinks ( s )
105- . replace ( / & / g, "&" )
106- . replace ( / < / g, "<" )
107- . replace ( / > / g, ">" )
108- . replace ( / " / g, """ )
109- // Smart/curly double quotes from the schema — also escape them
110- . replace ( / \u201c | \u201d / g, """ )
111- . replace ( / \n / g, " " )
112- . trim ( ) ;
110+ return (
111+ fixTelegramLinks ( s )
112+ . replace ( / & / g, "&" )
113+ . replace ( / < / g, "<" )
114+ . replace ( / > / g, ">" )
115+ . replace ( / " / g, """ )
116+ // Smart/curly double quotes from the schema — also escape them
117+ . replace ( / \u201c | \u201d / g, """ )
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 {
195242const GEN_START = "<!-- GENERATED:START -->" ;
196243const 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+
198265function 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 ───────────────────────────────────────────────────────────────────
552650console . 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 ) ) ;
555657updateSidebar ( methodNames , typeNames ) ;
556658
557659// ── Skills index ──────────────────────────────────────────────────────────────
0 commit comments