Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _extensions/quarto-openapi/_extension.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
title: quarto-openapi
author: Posit Software, PBC
version: 0.2.1
version: 0.2.2
quarto-required: ">=1.6.0"
contributes:
metadata:
Expand Down
11 changes: 6 additions & 5 deletions _extensions/quarto-openapi/lib/sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ function renderEndpoint(spec: OpenAPISpec, endpoint: Endpoint): string[] {
// Parameters
const paramLines = renderParameters(spec, operation, path);
if (paramLines.length > 0) {
lines.push(heading(4, "Parameters"));
lines.push(heading(4, "Parameters", `${anchor}-parameters`));
lines.push("");
lines.push(...paramLines);
lines.push("");
Expand All @@ -258,16 +258,16 @@ function renderEndpoint(spec: OpenAPISpec, endpoint: Endpoint): string[] {
// Request body
const bodyLines = renderRequestBody(spec, operation);
if (bodyLines.length > 0) {
lines.push(heading(4, "Request body"));
lines.push(heading(4, "Request body", `${anchor}-request-body`));
lines.push("");
lines.push(...bodyLines);
lines.push("");
}

// Responses
const responseLines = renderResponses(spec, operation);
const responseLines = renderResponses(spec, operation, anchor);
if (responseLines.length > 0) {
lines.push(heading(4, "Responses"));
lines.push(heading(4, "Responses", `${anchor}-responses`));
lines.push("");
lines.push(...responseLines);
lines.push("");
Expand Down Expand Up @@ -371,6 +371,7 @@ function renderRequestBody(
function renderResponses(
spec: OpenAPISpec,
operation: Operation,
anchor: string,
): string[] {
const entries = Object.entries(operation.responses).sort(
([a], [b]) => a.localeCompare(b),
Expand Down Expand Up @@ -402,7 +403,7 @@ function renderResponses(
"::: {.panel-tabset}",
"",
...tabs.flatMap((tab) => [
`##### ${tab.label}`,
heading(5, tab.label, `${anchor}-${tab.label}`),
"",
...tab.content,
"",
Expand Down
2 changes: 1 addition & 1 deletion example/_extensions/quarto-openapi/_extension.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
title: quarto-openapi
author: Posit Software, PBC
version: 0.2.1
version: 0.2.2
quarto-required: ">=1.6.0"
contributes:
metadata:
Expand Down
118 changes: 118 additions & 0 deletions tests/render_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,29 @@ Deno.test("renderSection: parameters rendered as grid table", () => {
assertStringIncludes(output, "Max items");
});

Deno.test("renderSection: parameters heading has scoped anchor ID", () => {
const spec = minimalSpec({
"/v1/pets": {
get: {
operationId: "listPets",
summary: "List pets",
parameters: [
{
name: "limit",
in: "query",
schema: { type: "integer" },
},
],
responses: { "200": { description: "OK" } },
},
},
});

const output = renderedSection(spec);

assertStringIncludes(output, '#### Parameters {id="listPets-parameters"}');
});

Deno.test("renderSection: request body schema rendered", () => {
const spec = minimalSpec({
"/v1/pets": {
Expand Down Expand Up @@ -129,6 +152,50 @@ Deno.test("renderSection: request body schema rendered", () => {
assertStringIncludes(output, "Pet name");
});

Deno.test("renderSection: request body heading has scoped anchor ID", () => {
const spec = minimalSpec({
"/v1/pets": {
post: {
operationId: "createPet",
summary: "Create a pet",
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: { type: "string" },
},
},
},
},
},
responses: { "201": { description: "Created" } },
},
},
});

const output = renderedSection(spec);

assertStringIncludes(output, '#### Request body {id="createPet-request-body"}');
});

Deno.test("renderSection: responses heading has scoped anchor ID", () => {
const spec = minimalSpec({
"/v1/pets": {
get: {
operationId: "listPets",
summary: "List pets",
responses: { "200": { description: "OK" } },
},
},
});

const output = renderedSection(spec);

assertStringIncludes(output, '#### Responses {id="listPets-responses"}');
});

Deno.test("renderSection: multiple responses render as tabset", () => {
const spec = minimalSpec({
"/v1/pets/{id}": {
Expand All @@ -150,6 +217,57 @@ Deno.test("renderSection: multiple responses render as tabset", () => {
assertStringIncludes(output, "**404**: Not found");
});

Deno.test("renderSection: response code tab headings have scoped anchor IDs", () => {
const spec = minimalSpec({
"/v1/pets/{id}": {
get: {
operationId: "getPet",
summary: "Get a pet",
responses: {
"200": { description: "OK" },
"404": { description: "Not found" },
},
},
},
});

const output = renderedSection(spec);

assertStringIncludes(output, '##### 200 {id="getPet-200"}');
assertStringIncludes(output, '##### 404 {id="getPet-404"}');
});

Deno.test("renderSection: sub-heading anchors fall back to method+path slug without operationId", () => {
const spec = minimalSpec({
"/v1/pets": {
get: {
summary: "List pets",
parameters: [
{
name: "limit",
in: "query",
schema: { type: "integer" },
},
],
responses: {
"200": { description: "OK" },
"400": { description: "Bad request" },
},
},
},
});

const output = renderedSection(spec);

// Endpoint heading uses path-based anchor
assertStringIncludes(output, '{id="get-/v1/pets"}');
// Sub-headings use the same slug as prefix
assertStringIncludes(output, '#### Parameters {id="get-/v1/pets-parameters"}');
assertStringIncludes(output, '#### Responses {id="get-/v1/pets-responses"}');
assertStringIncludes(output, '##### 200 {id="get-/v1/pets-200"}');
assertStringIncludes(output, '##### 400 {id="get-/v1/pets-400"}');
});

Deno.test("renderSection: path-level parameters merged into operations", () => {
const spec: OpenAPISpec = {
openapi: "3.0.3",
Expand Down