From 62e60b0d8425d34681d53bf557309df76c0d7fe6 Mon Sep 17 00:00:00 2001 From: Abraham Williams <4braham@gmail.com> Date: Sun, 12 Apr 2026 17:54:08 -0500 Subject: [PATCH] Support profiles --- dist/schema.json | 942 ++++++++++++++++++--- src/__tests__/parsers/MethodParser.test.ts | 35 + src/parsers/MethodParser.ts | 29 +- 3 files changed, 903 insertions(+), 103 deletions(-) diff --git a/dist/schema.json b/dist/schema.json index 9c1cd1c..a645dac 100644 --- a/dist/schema.json +++ b/dist/schema.json @@ -17737,6 +17737,131 @@ ] } ] + }, + "patch": { + "operationId": "patchNotificationPolicyV2", + "summary": "Update the filtering policy for notifications", + "description": "Update the user's notifications filtering policy.\n\nVersion history:\n\n4.3.0 - added", + "tags": [ + "notifications" + ], + "responses": { + "200": { + "description": "[NotificationPolicy]", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationPolicy" + }, + "examples": { + "NotificationPolicy200Example": { + "$ref": "#/components/examples/NotificationPolicy200Example" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error401Example": { + "$ref": "#/components/examples/Error401Example" + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error404Example": { + "$ref": "#/components/examples/Error404Example" + } + } + } + } + }, + "410": { + "description": "Gone" + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + }, + "examples": { + "ValidationError422Example": { + "$ref": "#/components/examples/ValidationError422Example" + } + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error429Example": { + "$ref": "#/components/examples/Error429Example" + } + } + } + } + }, + "503": { + "description": "Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error503Example": { + "$ref": "#/components/examples/Error503Example" + } + } + } + } + } + }, + "externalDocs": { + "url": "https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications", + "description": "Official Mastodon API documentation" + }, + "security": [ + { + "OAuth2": [ + "write:notifications" + ] + } + ] } }, "/api/v2/notifications/unread_count": { @@ -18752,19 +18877,559 @@ } } ] - } - }, - "/api/v1/polls/{id}": { - "get": { - "operationId": "getPoll", - "summary": "View a poll", - "description": "View a poll attached to a status.\n\nVersion history:\n\n2.8.0 - added", + } + }, + "/api/v1/polls/{id}": { + "get": { + "operationId": "getPoll", + "summary": "View a poll", + "description": "View a poll attached to a status.\n\nVersion history:\n\n2.8.0 - added", + "tags": [ + "polls" + ], + "responses": { + "200": { + "description": "[Poll]", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Poll" + }, + "examples": { + "Poll200Example": { + "$ref": "#/components/examples/Poll200Example" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error401Example": { + "$ref": "#/components/examples/Error401Example" + } + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error404Example": { + "$ref": "#/components/examples/Error404Example" + } + } + } + } + }, + "410": { + "description": "Gone" + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + }, + "examples": { + "ValidationError422Example": { + "$ref": "#/components/examples/ValidationError422Example" + } + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error429Example": { + "$ref": "#/components/examples/Error429Example" + } + } + } + } + }, + "503": { + "description": "Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error503Example": { + "$ref": "#/components/examples/Error503Example" + } + } + } + } + } + }, + "externalDocs": { + "url": "https://docs.joinmastodon.org/methods/polls/#get", + "description": "Official Mastodon API documentation" + }, + "security": [ + { + "OAuth2": [ + "read:statuses" + ] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "id parameter", + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/v1/polls/{id}/votes": { + "post": { + "operationId": "postPollVotes", + "summary": "Vote on a poll", + "description": "Vote on a poll attached to a status.\n\nVersion history:\n\n2.8.0 - added", + "tags": [ + "polls" + ], + "responses": { + "200": { + "description": "[Poll]", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Poll" + }, + "examples": { + "Poll200Example": { + "$ref": "#/components/examples/Poll200Example" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error401Example": { + "$ref": "#/components/examples/Error401Example" + } + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error404Example": { + "$ref": "#/components/examples/Error404Example" + } + } + } + } + }, + "410": { + "description": "Gone" + }, + "422": { + "description": "Unprocessable entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error422Example": { + "$ref": "#/components/examples/Error422Example" + } + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error429Example": { + "$ref": "#/components/examples/Error429Example" + } + } + } + } + }, + "503": { + "description": "Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error503Example": { + "$ref": "#/components/examples/Error503Example" + } + } + } + } + } + }, + "externalDocs": { + "url": "https://docs.joinmastodon.org/methods/polls/#vote", + "description": "Official Mastodon API documentation" + }, + "security": [ + { + "OAuth2": [ + "write:statuses" + ] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "id parameter", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "JSON request body parameters", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "choices": { + "type": "array", + "description": "Provide your own votes as an index for each option (starting from 0).", + "items": { + "type": "integer" + } + } + }, + "required": [ + "choices" + ] + } + } + } + } + } + }, + "/api/v1/preferences": { + "get": { + "operationId": "getPreferences", + "summary": "View user preferences", + "description": "Preferences defined by the user in their account settings.\n\nVersion history:\n\n2.8.0 - added\\\n4.5.0 (`mastodon` [API version] 7) - added `posting:default:quoted_policy`", + "tags": [ + "preferences" + ], + "responses": { + "200": { + "description": "Preferences by key and value", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error401Example": { + "$ref": "#/components/examples/Error401Example" + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error404Example": { + "$ref": "#/components/examples/Error404Example" + } + } + } + } + }, + "410": { + "description": "Gone" + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + }, + "examples": { + "ValidationError422Example": { + "$ref": "#/components/examples/ValidationError422Example" + } + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error429Example": { + "$ref": "#/components/examples/Error429Example" + } + } + } + } + }, + "503": { + "description": "Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error503Example": { + "$ref": "#/components/examples/Error503Example" + } + } + } + } + } + }, + "externalDocs": { + "url": "https://docs.joinmastodon.org/methods/preferences/#get", + "description": "Official Mastodon API documentation" + }, + "security": [ + { + "OAuth2": [ + "read:accounts" + ] + } + ] + } + }, + "/api/v1/profile": { + "get": { + "operationId": "getProfile", + "summary": "Get current user profile", + "description": "Version history:\n\n4.6.0 (`mastodon` [API version] 8) - added\\\n4.6.0 (`mastodon` [API version] 9) - added `avatar_description` and `header_description`", + "tags": [ + "profile" + ], + "responses": { + "200": { + "description": "[Profile]", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Profile" + }, + "examples": { + "Profile200Example": { + "$ref": "#/components/examples/Profile200Example" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error401Example": { + "$ref": "#/components/examples/Error401Example" + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error404Example": { + "$ref": "#/components/examples/Error404Example" + } + } + } + } + }, + "410": { + "description": "Gone" + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + }, + "examples": { + "ValidationError422Example": { + "$ref": "#/components/examples/ValidationError422Example" + } + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error429Example": { + "$ref": "#/components/examples/Error429Example" + } + } + } + } + }, + "503": { + "description": "Unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "Error503Example": { + "$ref": "#/components/examples/Error503Example" + } + } + } + } + } + }, + "externalDocs": { + "url": "https://docs.joinmastodon.org/methods/profile/#get-current-user-profile", + "description": "Official Mastodon API documentation" + }, + "security": [ + { + "OAuth2": [ + "profile", + "read:accounts" + ] + } + ] + }, + "patch": { + "operationId": "patchProfile", + "summary": "Update current user profile", + "description": "Update the current user's profile.\n\nVersion history:\n\n4.6.0 (`mastodon` [API version] 8) - added", "tags": [ - "polls" + "profile" ], "responses": { "200": { - "description": "[Poll]", + "description": "[Profile]", "headers": { "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" @@ -18779,12 +19444,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Poll" - }, - "examples": { - "Poll200Example": { - "$ref": "#/components/examples/Poll200Example" - } + "$ref": "#/components/schemas/Profile" } } } @@ -18805,7 +19465,7 @@ } }, "404": { - "description": "Not found", + "description": "Not Found", "content": { "application/json": { "schema": { @@ -18823,15 +19483,15 @@ "description": "Gone" }, "422": { - "description": "Unprocessable Content", + "description": "Unprocessable entity", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationError" + "$ref": "#/components/schemas/Error" }, "examples": { - "ValidationError422Example": { - "$ref": "#/components/examples/ValidationError422Example" + "Error422Example": { + "$ref": "#/components/examples/Error422Example" } } } @@ -18869,40 +19529,110 @@ } }, "externalDocs": { - "url": "https://docs.joinmastodon.org/methods/polls/#get", + "url": "https://docs.joinmastodon.org/methods/profile/#update-current-user-profile", "description": "Official Mastodon API documentation" }, "security": [ { "OAuth2": [ - "read:statuses" + "write:accounts" ] } ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "id parameter", - "schema": { - "type": "string" + "parameters": [], + "requestBody": { + "description": "JSON request body parameters", + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "attribution_domains": { + "type": "array", + "description": "Domains of websites allowed to credit the account. Maximum of 10 domains.", + "items": { + "type": "string" + } + }, + "avatar": { + "type": "string", + "description": "Avatar image encoded using `multipart/form-data`" + }, + "avatar_description": { + "type": "string", + "description": "A plain-text description of the avatar, for accessibility purposes." + }, + "bot": { + "type": "boolean", + "description": "Whether the account has a bot flag." + }, + "discoverable": { + "type": "boolean", + "description": "Whether the account should be shown in the profile directory." + }, + "display_name": { + "type": "string", + "description": "The display name to use for the profile." + }, + "fields_attributes": { + "type": "object", + "description": "The profile fields to be set. Each hash includes `name` and `value`. By default, max 4 fields (specified in [Instance#max_profile_fields])." + }, + "header": { + "type": "string", + "description": "Header image encoded using `multipart/form-data`" + }, + "header_description": { + "type": "string", + "description": "A plain-text description of the header, for accessibility purposes." + }, + "hide_collections": { + "type": "boolean", + "description": "Whether to hide followers and followed accounts." + }, + "indexable": { + "type": "boolean", + "description": "Whether public posts should be searchable to anyone." + }, + "locked": { + "type": "boolean", + "description": "Whether manual approval of follow requests is required." + }, + "note": { + "type": "string", + "description": "The account bio." + }, + "show_featured": { + "type": "boolean", + "description": "Whether a “Featured” tab should be shown on this profile." + }, + "show_media": { + "type": "boolean", + "description": "Whether a “Media” tab with media attachments should be shown on this profile." + }, + "show_media_replies": { + "type": "boolean", + "description": "Whether media attachments in replies should be shown in the “Media” tab of this profile." + } + } + } } } - ] + } } }, - "/api/v1/polls/{id}/votes": { - "post": { - "operationId": "postPollVotes", - "summary": "Vote on a poll", - "description": "Vote on a poll attached to a status.\n\nVersion history:\n\n2.8.0 - added", + "/api/v1/profile/avatar": { + "delete": { + "operationId": "deleteAvatar", + "summary": "Delete profile avatar", + "description": "Version history:\n\n4.2.0 - added\n\nDeletes the avatar associated with the user's profile.", "tags": [ - "polls" + "profile" ], "responses": { "200": { - "description": "[Poll]", + "description": "[CredentialAccount]", "headers": { "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" @@ -18917,11 +19647,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Poll" + "$ref": "#/components/schemas/CredentialAccount" }, "examples": { - "Poll200Example": { - "$ref": "#/components/examples/Poll200Example" + "CredentialAccount200Example": { + "$ref": "#/components/examples/CredentialAccount200Example" } } } @@ -18943,7 +19673,7 @@ } }, "404": { - "description": "Not found", + "description": "Not Found", "content": { "application/json": { "schema": { @@ -18961,15 +19691,15 @@ "description": "Gone" }, "422": { - "description": "Unprocessable entity", + "description": "Unprocessable Content", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Error" + "$ref": "#/components/schemas/ValidationError" }, "examples": { - "Error422Example": { - "$ref": "#/components/examples/Error422Example" + "ValidationError422Example": { + "$ref": "#/components/examples/ValidationError422Example" } } } @@ -19007,63 +19737,29 @@ } }, "externalDocs": { - "url": "https://docs.joinmastodon.org/methods/polls/#vote", + "url": "https://docs.joinmastodon.org/methods/profile/#delete-profile-avatar", "description": "Official Mastodon API documentation" }, "security": [ { "OAuth2": [ - "write:statuses" + "write:accounts" ] } - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "id parameter", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "description": "JSON request body parameters", - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "choices": { - "type": "array", - "description": "Provide your own votes as an index for each option (starting from 0).", - "items": { - "type": "integer" - } - } - }, - "required": [ - "choices" - ] - } - } - } - } + ] } }, - "/api/v1/preferences": { - "get": { - "operationId": "getPreferences", - "summary": "View user preferences", - "description": "Preferences defined by the user in their account settings.\n\nVersion history:\n\n2.8.0 - added\\\n4.5.0 (`mastodon` [API version] 7) - added `posting:default:quoted_policy`", + "/api/v1/profile/header": { + "delete": { + "operationId": "deleteProfileHeader", + "summary": "Delete profile header", + "description": "Version history:\n\n4.2.0 - added\n\nDeletes the header image associated with the user's profile.", "tags": [ - "preferences" + "profile" ], "responses": { "200": { - "description": "Preferences by key and value", + "description": "[CredentialAccount]", "headers": { "X-RateLimit-Limit": { "$ref": "#/components/headers/X-RateLimit-Limit" @@ -19074,6 +19770,18 @@ "X-RateLimit-Reset": { "$ref": "#/components/headers/X-RateLimit-Reset" } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialAccount" + }, + "examples": { + "CredentialAccount200Example": { + "$ref": "#/components/examples/CredentialAccount200Example" + } + } + } } }, "401": { @@ -19156,13 +19864,13 @@ } }, "externalDocs": { - "url": "https://docs.joinmastodon.org/methods/preferences/#get", + "url": "https://docs.joinmastodon.org/methods/profile/#delete-profile-header", "description": "Official Mastodon API documentation" }, "security": [ { "OAuth2": [ - "read:accounts" + "write:accounts" ] } ] @@ -39014,10 +39722,11 @@ "NotificationPolicy200Example": { "summary": "Example for NotificationPolicy", "value": { - "filter_not_following": false, - "filter_not_followers": false, - "filter_new_accounts": false, - "filter_private_mentions": true, + "for_not_following": "accept", + "for_not_followers": "accept", + "for_new_accounts": "accept", + "for_private_mentions": "drop", + "for_limited_accounts": "filter", "summary": { "pending_requests_count": 0, "pending_notifications_count": 0 @@ -39082,6 +39791,47 @@ "emojis": [] } }, + "Profile200Example": { + "summary": "Example for Profile", + "value": { + "id": "116222600881276277", + "display_name": "Documentation user", + "note": "I'm only here as an example for documentation", + "fields": [ + { + "name": "pronouns", + "value": "it/its", + "verified_at": null + } + ], + "avatar": null, + "avatar_static": null, + "avatar_description": "", + "header": null, + "header_static": null, + "header_description": "", + "locked": false, + "bot": false, + "hide_collections": null, + "discoverable": true, + "indexable": true, + "show_media": true, + "show_media_replies": true, + "show_featured": true, + "attribution_domains": [ + "articles.example.com" + ], + "featured_tags": [ + { + "id": "1", + "name": "foo", + "url": "https://example.com/@darrel_metz0/tagged/foo", + "statuses_count": "0", + "last_status_at": null + } + ] + } + }, "WebPushSubscription200Example": { "summary": "Example for WebPushSubscription", "value": { diff --git a/src/__tests__/parsers/MethodParser.test.ts b/src/__tests__/parsers/MethodParser.test.ts index 1ced9b4..7916de3 100644 --- a/src/__tests__/parsers/MethodParser.test.ts +++ b/src/__tests__/parsers/MethodParser.test.ts @@ -164,6 +164,41 @@ describe('MethodParser', () => { expect(foundFormParam).toBe(true); }); + test('should parse methods from files without explicit heading anchors', () => { + const methodFiles = methodParser.parseAllMethods(); + + const profileMethodsFile = methodFiles.find((f) => f.name === 'profile'); + expect(profileMethodsFile).toBeDefined(); + + if (profileMethodsFile) { + const getProfileMethod = profileMethodsFile.methods.find( + (method) => + method.httpMethod === 'GET' && method.endpoint === '/api/v1/profile' + ); + + expect(getProfileMethod).toBeDefined(); + expect(getProfileMethod?.anchor).toBe('get-current-user-profile'); + } + }); + + test('should parse nested level-3 method headers in filters file', () => { + const methodFiles = methodParser.parseAllMethods(); + + const filtersMethodsFile = methodFiles.find((f) => f.name === 'filters'); + expect(filtersMethodsFile).toBeDefined(); + + if (filtersMethodsFile) { + const legacyListMethod = filtersMethodsFile.methods.find( + (method) => + method.httpMethod === 'GET' && method.endpoint === '/api/v1/filters' + ); + + expect(legacyListMethod).toBeDefined(); + expect(legacyListMethod?.deprecated).toBe(true); + expect(legacyListMethod?.anchor).toBe('get-v1'); + } + }); + test('should extract HTTP methods and endpoints correctly', () => { const methodFiles = methodParser.parseAllMethods(); diff --git a/src/parsers/MethodParser.ts b/src/parsers/MethodParser.ts index b384a4a..4c0176c 100644 --- a/src/parsers/MethodParser.ts +++ b/src/parsers/MethodParser.ts @@ -103,9 +103,9 @@ class MethodParser { private parseMethods(content: string): ApiMethod[] { const methods: ApiMethod[] = []; - // Match method sections: only ## Method Name {#anchor} (level 2 headers) - // Level 3+ headers (###, ####, etc.) should be treated as subsections of the method - const methodSections = content.split(/(?=^## [^{]*\{#[^}]+\})/m); + // Split on level 2 headers. Some docs include explicit anchors ({#...}) while + // others rely on implicit heading slugs generated by Hugo. + const methodSections = content.split(/(?=^##\s+)/m); for (const section of methodSections) { if (section.trim() === '') continue; @@ -128,13 +128,16 @@ class MethodParser { } private parseMethodSection(section: string): ApiMethod | null { - // Extract method name and anchor from header: ## Method Name {#anchor} or ### Method Name {#anchor} - // Handle headers that may contain {{%removed%}} or other Hugo shortcodes - const nameMatch = section.match(/^##+ (.+?)\s*\{#([^}]+)\}/m); + // Extract method name and optional anchor from the heading. + // Supports both explicit anchors (## Name {#anchor}) and implicit anchors + // where Hugo generates the slug from heading text. + const nameMatch = section.match(/^#{2,3}\s+(.+?)(?:\s*\{#([^}]+)\})?\s*$/m); if (!nameMatch) return null; const name = nameMatch[1].trim(); - const anchor = nameMatch[2].trim(); + const anchor = nameMatch[2] + ? nameMatch[2].trim() + : this.generateAnchorFromHeading(name); // Skip methods marked as removed if (name.includes('{{%removed%}}')) { @@ -230,6 +233,18 @@ class MethodParser { }; } + private generateAnchorFromHeading(heading: string): string { + return heading + .replace(/\{\{%[^%]+%\}\}/g, '') + .trim() + .toLowerCase() + .replace(/&/g, ' and ') + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); + } + /** * Parse response codes from method section * Looks for patterns like "##### 200: OK" or "##### 202: Accepted"