diff --git a/printingpress/agent_writer.go b/printingpress/agent_writer.go index 378e9db..73441e1 100644 --- a/printingpress/agent_writer.go +++ b/printingpress/agent_writer.go @@ -1397,6 +1397,10 @@ func renderOperationMD(ctx llmRenderContext, op *OperationPage) string { b.WriteString(curl) } + if codeSamples := renderCodeSamplesMD(op.CodeSamples); codeSamples != "" { + b.WriteString(codeSamples) + } + if idGuidance := renderOperationIDGuidanceMD(ctx, op); idGuidance != "" { b.WriteString(idGuidance) } @@ -1898,6 +1902,61 @@ func renderCurlMD(op *OperationPage) string { return b.String() } +func renderCodeSamplesMD(samples []*CodeSample) string { + if len(samples) == 0 { + return "" + } + var b strings.Builder + for i, sample := range samples { + if sample == nil || strings.TrimSpace(sample.Source) == "" { + continue + } + if b.Len() == 0 { + b.WriteString("#### Code Samples\n\n") + } + b.WriteString("**" + codeSampleMarkdownLabel(sample, i) + "**\n\n") + fence := markdownCodeFence(sample.Source) + language := markdownFenceLanguage(sample.Lang) + b.WriteString(fence + language + "\n") + b.WriteString(strings.TrimRight(sample.Source, "\n")) + b.WriteString("\n" + fence + "\n\n") + } + return b.String() +} + +func codeSampleMarkdownLabel(sample *CodeSample, index int) string { + // Markdown has no separate tab label and panel heading, so preserve both names when they differ. + if sample.Lang != "" && sample.Label != "" && sample.Label != sample.Lang { + return singleLine(sample.Lang + " - " + sample.Label) + } + return sample.DisplayLabel(index) +} + +func markdownCodeFence(source string) string { + fence := "```" + for strings.Contains(source, fence) { + fence += "`" + } + return fence +} + +func markdownFenceLanguage(language string) string { + language = strings.TrimSpace(language) + if language == "" { + return "" + } + var b strings.Builder + for _, r := range language { + if r >= 'a' && r <= 'z' || + r >= 'A' && r <= 'Z' || + r >= '0' && r <= '9' || + r == '-' || r == '_' || r == '+' || r == '#' || r == '.' { + b.WriteRune(r) + } + } + return b.String() +} + func uniqueCurlVariants(variants []*CurlVariant) []*CurlVariant { seen := make(map[string]bool) out := make([]*CurlVariant, 0, len(variants)) diff --git a/printingpress/agent_writer_test.go b/printingpress/agent_writer_test.go index f210d40..f23992a 100644 --- a/printingpress/agent_writer_test.go +++ b/printingpress/agent_writer_test.go @@ -1117,6 +1117,42 @@ func TestRenderOperationMD_CurlRelatedOpsGuidanceAndScopeDedup(t *testing.T) { assert.Equal(t, 1, strings.Count(md, "sp:config-object-mapping:manage")) } +func TestRenderOperationMD_CodeSamples(t *testing.T) { + curlVariants := []*CurlVariant{ + {Label: "Default", Command: "curl https://api.example.com/bookings"}, + } + curlJSON, err := json.Marshal(curlVariants) + require.NoError(t, err) + + op := &OperationPage{ + Method: "POST", + Path: "/bookings", + Summary: "Create booking", + CurlJSON: string(curlJSON), + CodeSamples: []*CodeSample{ + { + Lang: "typescript", + Label: "create-booking_json", + Source: "const client = new TrainTravelSDK();\nawait client.bookings.create({ tripId: \"abc\" });\n", + }, + { + Label: "python-sdk", + Source: "client.bookings.create({\"tripId\": \"abc\"})\n", + }, + }, + } + + md := renderOperationMD(rootLLMRenderContext(&Site{}), op) + + assert.Contains(t, md, "#### cURL") + assert.Contains(t, md, "#### Code Samples") + assert.Contains(t, md, "**typescript - create-booking_json**") + assert.Contains(t, md, "```typescript\nconst client = new TrainTravelSDK();") + assert.Contains(t, md, "**python-sdk**") + assert.Contains(t, md, "```\nclient.bookings.create") + assert.Less(t, strings.Index(md, "#### cURL"), strings.Index(md, "#### Code Samples")) +} + func TestRenderOperationMD_CurlSuppressesDuplicateVariants(t *testing.T) { curlVariants := []*CurlVariant{ {Label: "Default", Command: "curl https://api.example.com/items"}, diff --git a/printingpress/collector.go b/printingpress/collector.go index 2acfe97..e739422 100644 --- a/printingpress/collector.go +++ b/printingpress/collector.go @@ -9,8 +9,10 @@ import ( "encoding/json" "errors" "fmt" + "io" "math" "net/url" + "os" "path/filepath" "reflect" "sort" @@ -30,7 +32,12 @@ import ( "go.yaml.in/yaml/v4" ) -const typeSlugPathItems = "path-items" +const ( + typeSlugPathItems = "path-items" + codeSamplesExtensionKey = "x-codeSamples" + maxCodeSampleSourceBytes = 1 << 20 + codeSampleSourceLimitLabel = "1MiB" +) // refSegmentToTypeSlug maps OpenAPI $ref component segments to URL type slugs. var refSegmentToTypeSlug = map[string]string{ @@ -434,8 +441,13 @@ func (pp *PrintingPress) collectOperation(method, path string, op *v3.Operation, } } + operationContext := fmt.Sprintf("%s %s", strings.ToUpper(method), path) + + // Code samples + page.CodeSamples = pp.collectCodeSamples(val.Extensions, pp.operationSourceFile(piOrigin), operationContext) + // Extensions - page.Extensions = collectExtensions(val.Extensions) + page.Extensions = collectExtensionsExcept(val.Extensions, codeSamplesExtensionKey) if page.Extensions != nil { page.ExtensionsJSON = render.MustJSON(page.Extensions) } @@ -457,7 +469,7 @@ func (pp *PrintingPress) collectOperation(method, path string, op *v3.Operation, } // Serialize full operation to YAML + JSON for cowboy-components and raw viewer - pp.captureRawData(val, fmt.Sprintf("%s %s", method, path), + pp.captureRawData(val, operationContext, &page.RawYAML, &page.SchemaJSON, nil) // Use ValueNode line only for single-file specs; for multi-file bundled specs @@ -2000,18 +2012,283 @@ func (pp *PrintingPress) resolveSecurityRequirement(name string, scopes []string return req } +func (pp *PrintingPress) collectCodeSamples(extensions *orderedmap.Map[string, *yaml.Node], baseFile, context string) []*CodeSample { + if extensions == nil || extensions.Len() == 0 { + return nil + } + root := extensions.GetOrZero(codeSamplesExtensionKey) + if root == nil { + return nil + } + if root.Kind != yaml.SequenceNode { + pp.warn("x-codeSamples must be a sequence; skipping code samples", context, nil) + return nil + } + + samples := make([]*CodeSample, 0, len(root.Content)) + for i, item := range root.Content { + sampleContext := fmt.Sprintf("%s x-codeSamples[%d]", context, i) + sample := pp.collectCodeSample(item, baseFile, sampleContext) + if sample != nil { + samples = append(samples, sample) + } + } + if len(samples) == 0 { + return nil + } + return samples +} + +func (pp *PrintingPress) collectCodeSample(node *yaml.Node, baseFile, context string) *CodeSample { + if node == nil { + pp.warn("x-codeSamples entry must be an object or string; skipping code sample", context, nil) + return nil + } + + switch node.Kind { + case yaml.ScalarNode: + source, _, ok := pp.collectCodeSampleSource(node, baseFile, context) + if !ok { + return nil + } + return &CodeSample{Source: source} + case yaml.MappingNode: + source, sourceRef, ok := pp.collectCodeSampleSource(yamlMappingValue(node, "source"), baseFile, context) + if !ok { + return nil + } + sample := &CodeSample{ + Lang: yamlScalarString(yamlMappingValue(node, "lang")), + Label: yamlScalarString(yamlMappingValue(node, "label")), + Source: source, + SourceRef: sourceRef, + } + return sample + default: + pp.warn("x-codeSamples entry must be an object or string; skipping code sample", context, nil) + return nil + } +} + +func codeSampleHighlightLanguage(sample *CodeSample) string { + if sample == nil { + return "" + } + if sample.Lang != "" { + return sample.Lang + } + return sample.Label +} + +func (pp *PrintingPress) collectCodeSampleSource(sourceNode *yaml.Node, baseFile, context string) (string, string, bool) { + if sourceNode == nil { + pp.warn("x-codeSamples source is missing; skipping code sample", context, nil) + return "", "", false + } + + switch sourceNode.Kind { + case yaml.ScalarNode: + if sourceNode.Value == "" { + pp.warn("x-codeSamples source is empty; skipping code sample", context, nil) + return "", "", false + } + if len(sourceNode.Value) > maxCodeSampleSourceBytes { + pp.warn("x-codeSamples source exceeds "+codeSampleSourceLimitLabel+"; skipping code sample", context, nil) + return "", "", false + } + return sourceNode.Value, "", true + case yaml.MappingNode: + ref := yamlScalarString(yamlMappingValue(sourceNode, "$ref")) + if ref == "" { + pp.warn("x-codeSamples source $ref is missing; skipping code sample", context, nil) + return "", "", false + } + if refURL, err := url.Parse(ref); err == nil && refURL.Scheme != "" && refURL.Host != "" { + pp.warn("x-codeSamples source $ref is remote; skipping code sample", ref, nil) + return "", ref, false + } + resolvedPath, ok := pp.resolveCodeSampleRefPath(ref, baseFile) + if !ok { + pp.warn("x-codeSamples source $ref resolves outside the spec root; skipping code sample", ref, nil) + return "", ref, false + } + file, err := os.Open(resolvedPath) + if err != nil { + pp.warn("x-codeSamples source $ref could not be read; skipping code sample", ref, nil) + return "", ref, false + } + defer func() { + _ = file.Close() + }() + info, err := file.Stat() + if err != nil { + pp.warn("x-codeSamples source $ref could not be read; skipping code sample", ref, nil) + return "", ref, false + } + if !info.Mode().IsRegular() { + pp.warn("x-codeSamples source $ref is not a regular file; skipping code sample", ref, nil) + return "", ref, false + } + if info.Size() > maxCodeSampleSourceBytes { + pp.warn("x-codeSamples source $ref exceeds "+codeSampleSourceLimitLabel+"; skipping code sample", ref, nil) + return "", ref, false + } + data, err := io.ReadAll(io.LimitReader(file, maxCodeSampleSourceBytes+1)) + if err != nil { + pp.warn("x-codeSamples source $ref could not be read; skipping code sample", ref, nil) + return "", ref, false + } + if len(data) > maxCodeSampleSourceBytes { + pp.warn("x-codeSamples source $ref exceeds "+codeSampleSourceLimitLabel+"; skipping code sample", ref, nil) + return "", ref, false + } + if len(data) == 0 { + pp.warn("x-codeSamples source $ref is empty; skipping code sample", ref, nil) + return "", ref, false + } + return string(data), ref, true + default: + pp.warn("x-codeSamples source must be a string or $ref object; skipping code sample", context, nil) + return "", "", false + } +} + +func (pp *PrintingPress) operationSourceFile(origin *bundler.ComponentOrigin) string { + if origin != nil && origin.OriginalFile != "" { + if filepath.IsAbs(origin.OriginalFile) { + return origin.OriginalFile + } + if pp.engineConfig != nil && pp.engineConfig.SpecRoot != "" { + return filepath.Join(pp.engineConfig.SpecRoot, filepath.FromSlash(origin.OriginalFile)) + } + return origin.OriginalFile + } + if pp.engineConfig == nil { + return "" + } + if pp.engineConfig.SpecPath != "" { + return pp.engineConfig.SpecPath + } + if pp.engineConfig.SpecRoot != "" && pp.engineConfig.SpecLocation != "" { + return filepath.Join(pp.engineConfig.SpecRoot, filepath.FromSlash(pp.engineConfig.SpecLocation)) + } + return pp.engineConfig.SpecLocation +} + +func (pp *PrintingPress) resolveCodeSampleRefPath(ref, baseFile string) (string, bool) { + cleanRef := filepath.FromSlash(ref) + var candidate string + if baseFile != "" { + candidate = filepath.Join(filepath.Dir(baseFile), cleanRef) + } else if pp.engineConfig != nil && pp.engineConfig.SpecRoot != "" { + candidate = filepath.Join(pp.engineConfig.SpecRoot, cleanRef) + } else { + candidate = cleanRef + } + if filepath.IsAbs(cleanRef) { + candidate = cleanRef + } + absCandidate, err := filepath.Abs(filepath.Clean(candidate)) + if err != nil { + return "", false + } + allowedRoot, ok := pp.codeSampleAllowedRoot(baseFile) + if !ok || !pathWithinRoot(absCandidate, allowedRoot) { + return "", false + } + if evaluated, err := filepath.EvalSymlinks(absCandidate); err == nil { + evaluatedRoot := allowedRoot + if root, rootErr := filepath.EvalSymlinks(allowedRoot); rootErr == nil { + evaluatedRoot = root + } + if !pathWithinRoot(evaluated, evaluatedRoot) { + return "", false + } + } + return absCandidate, true +} + +func (pp *PrintingPress) codeSampleAllowedRoot(baseFile string) (string, bool) { + root := "" + if pp.engineConfig != nil { + if pp.engineConfig.SpecRoot != "" { + root = pp.engineConfig.SpecRoot + } else if pp.engineConfig.SpecPath != "" { + root = filepath.Dir(pp.engineConfig.SpecPath) + } + } + if root == "" && baseFile != "" { + root = filepath.Dir(baseFile) + } + if root == "" { + return "", false + } + absRoot, err := filepath.Abs(filepath.Clean(root)) + if err != nil { + return "", false + } + return absRoot, true +} + +func pathWithinRoot(path, root string) bool { + absPath, err := filepath.Abs(filepath.Clean(path)) + if err != nil { + return false + } + absRoot, err := filepath.Abs(filepath.Clean(root)) + if err != nil { + return false + } + rel, err := filepath.Rel(absRoot, absPath) + if err != nil { + return false + } + return rel == "." || rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) +} + +func yamlMappingValue(node *yaml.Node, key string) *yaml.Node { + if node == nil || node.Kind != yaml.MappingNode { + return nil + } + for i := 0; i+1 < len(node.Content); i += 2 { + k := node.Content[i] + if k != nil && k.Value == key { + return node.Content[i+1] + } + } + return nil +} + +func yamlScalarString(node *yaml.Node) string { + if node == nil || node.Kind != yaml.ScalarNode { + return "" + } + return strings.TrimSpace(node.Value) +} + // collectExtensions converts an orderedmap of extension yaml.Nodes to an ordered slice. // Returns nil when there are no extensions (so omitempty works). // The "x-" prefix is stripped from keys for cleaner documentation display. func collectExtensions(extensions *orderedmap.Map[string, *yaml.Node]) []*ExtensionEntry { + return collectExtensionsExcept(extensions) +} + +func collectExtensionsExcept(extensions *orderedmap.Map[string, *yaml.Node], excludedKeys ...string) []*ExtensionEntry { if extensions == nil || extensions.Len() == 0 { return nil } + excluded := make(map[string]struct{}, len(excludedKeys)) + for _, key := range excludedKeys { + excluded[key] = struct{}{} + } result := make([]*ExtensionEntry, 0, extensions.Len()) for pair := extensions.First(); pair != nil; pair = pair.Next() { if pair.Value() == nil { continue } + if _, skip := excluded[pair.Key()]; skip { + continue + } var decoded any if err := pair.Value().Decode(&decoded); err == nil { result = append(result, &ExtensionEntry{ @@ -2020,6 +2297,9 @@ func collectExtensions(extensions *orderedmap.Map[string, *yaml.Node]) []*Extens }) } } + if len(result) == 0 { + return nil + } return result } diff --git a/printingpress/collector_test.go b/printingpress/collector_test.go index af8bb06..2887e7b 100644 --- a/printingpress/collector_test.go +++ b/printingpress/collector_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "strings" "testing" @@ -15,7 +16,9 @@ import ( . "github.com/pb33f/doctor/printingpress/model" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/bundler" + "github.com/pb33f/libopenapi/datamodel" highbase "github.com/pb33f/libopenapi/datamodel/high/base" + "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v4" @@ -39,15 +42,21 @@ func pressFromSpec(t *testing.T, spec string) *Site { func pressSiteFromSpecWithConfig(t *testing.T, spec string, configure func(*pressEngineConfig)) *Site { t.Helper() - doc, err := libopenapi.NewDocument([]byte(spec)) - require.NoError(t, err) - v3, buildErr := doc.BuildV3Model() - require.NoError(t, buildErr) - drDoc := model.NewDrDocument(v3) - cfg := &pressEngineConfig{DrDoc: drDoc} + cfg := &pressEngineConfig{} if configure != nil { configure(cfg) } + docConfig := datamodel.NewDocumentConfiguration() + docConfig.ExcludeExtensionRefs = true + if cfg.SpecRoot != "" { + docConfig.AllowFileReferences = true + docConfig.BasePath = cfg.SpecRoot + } + doc, err := libopenapi.NewDocumentWithConfiguration([]byte(spec), docConfig) + require.NoError(t, err) + v3, buildErr := doc.BuildV3Model() + require.NoError(t, buildErr) + cfg.DrDoc = model.NewDrDocument(v3) pp := newPressEngine(cfg) site, err := pp.pressSite() require.NoError(t, err) @@ -1203,6 +1212,280 @@ func TestCollectExtensions_NilMap(t *testing.T) { assert.Nil(t, result) } +func TestCollectCodeSamples_FirstClassAndFiltersOperationExtensions(t *testing.T) { + spec := `openapi: "3.1.0" +info: + title: Test + version: "1.0" +paths: + /bookings: + post: + operationId: createBooking + summary: Create booking + x-beta: true + x-codeSamples: + - lang: typescript + label: create-booking_json + source: | + const client = new TrainTravelSDK(); + await client.bookings.create({ tripId: "abc" }); + - label: python-sdk + source: | + client.bookings.create({"tripId": "abc"}) + responses: + '201': + description: Created +` + site := pressFromSpec(t, spec) + require.Len(t, site.Operations, 1) + op := site.Operations[0] + + require.Len(t, op.CodeSamples, 2) + assert.Equal(t, "typescript", op.CodeSamples[0].Lang) + assert.Equal(t, "create-booking_json", op.CodeSamples[0].Label) + assert.Contains(t, op.CodeSamples[0].Source, "TrainTravelSDK") + assert.Empty(t, op.CodeSamples[0].HighlightedHTML) + assert.Empty(t, op.CodeSamples[1].Lang) + assert.Equal(t, "python-sdk", op.CodeSamples[1].Label) + + require.Len(t, op.Extensions, 1) + assert.Equal(t, "beta", op.Extensions[0].Key) + assert.NotContains(t, op.ExtensionsJSON, "codeSamples") + + jsonBytes, err := json.Marshal(op) + require.NoError(t, err) + jsonDoc := string(jsonBytes) + assert.Contains(t, jsonDoc, `"codeSamples"`) + assert.Contains(t, jsonDoc, `"source":"const client = new TrainTravelSDK()`) + assert.NotContains(t, jsonDoc, "HighlightedHTML") +} + +func TestCollectCodeSamples_AcceptsScalarBlockEntries(t *testing.T) { + spec := `openapi: "3.1.0" +info: + title: Test + version: "1.0" +paths: + /bookings: + post: + operationId: createBooking + x-codeSamples: + - | + const client = createClient(); + await client.bookings.create({ tripId: "abc" }); + - | + package main + + func main() { + client.Bookings.Create("abc") + } + responses: + '201': + description: Created +` + site := pressFromSpec(t, spec) + require.Len(t, site.Operations, 1) + op := site.Operations[0] + require.Len(t, op.CodeSamples, 2) + assert.Empty(t, op.CodeSamples[0].Lang) + assert.Empty(t, op.CodeSamples[0].Label) + assert.Empty(t, op.CodeSamples[0].SourceRef) + assert.Contains(t, op.CodeSamples[0].Source, "createClient") + assert.Contains(t, op.CodeSamples[1].Source, "package main") + assert.Empty(t, site.Warnings) +} + +func TestCollectCodeSamples_ResolvesSourceRefRelativeToSpecPath(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(dir, "code_samples"), 0o755)) + require.NoError(t, os.WriteFile( + filepath.Join(dir, "code_samples", "python.py"), + []byte("client.bookings.create({'tripId': 'abc'})\n"), + 0o644, + )) + spec := `openapi: "3.1.0" +info: + title: Test + version: "1.0" +paths: + /bookings: + post: + operationId: createBooking + x-codeSamples: + - lang: Python + source: + $ref: code_samples/python.py + responses: + '201': + description: Created +` + site := pressSiteFromSpecWithConfig(t, spec, func(cfg *pressEngineConfig) { + cfg.SpecRoot = dir + cfg.SpecPath = filepath.Join(dir, "openapi.yaml") + cfg.SpecLocation = "openapi.yaml" + }) + require.Len(t, site.Operations, 1) + op := site.Operations[0] + require.Len(t, op.CodeSamples, 1) + assert.Equal(t, "Python", op.CodeSamples[0].Lang) + assert.Equal(t, "code_samples/python.py", op.CodeSamples[0].SourceRef) + assert.Contains(t, op.CodeSamples[0].Source, "client.bookings.create") + assert.Empty(t, site.Warnings) +} + +func TestCollectCodeSamples_MissingSourceRefWarnsAndSkips(t *testing.T) { + dir := t.TempDir() + spec := `openapi: "3.1.0" +info: + title: Test + version: "1.0" +paths: + /bookings: + post: + operationId: createBooking + x-codeSamples: + - lang: Python + source: + $ref: code_samples/missing.py + responses: + '201': + description: Created +` + site := pressSiteFromSpecWithConfig(t, spec, func(cfg *pressEngineConfig) { + cfg.SpecRoot = dir + cfg.SpecPath = filepath.Join(dir, "openapi.yaml") + cfg.SpecLocation = "openapi.yaml" + }) + require.Len(t, site.Operations, 1) + assert.Nil(t, site.Operations[0].CodeSamples) + require.NotEmpty(t, site.Warnings) + assert.Equal(t, "x-codeSamples source $ref could not be read; skipping code sample", site.Warnings[0].Message) + assert.Equal(t, "code_samples/missing.py", site.Warnings[0].Context) + assert.NoError(t, site.Warnings[0].Err) +} + +func TestCollectCodeSamples_SourceRefOutsideSpecRootWarnsAndSkips(t *testing.T) { + baseDir := t.TempDir() + specRoot := filepath.Join(baseDir, "spec") + outsideDir := filepath.Join(baseDir, "outside") + require.NoError(t, os.MkdirAll(specRoot, 0o755)) + require.NoError(t, os.MkdirAll(outsideDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(outsideDir, "secret.py"), []byte("print('secret')\n"), 0o644)) + + spec := `openapi: "3.1.0" +info: + title: Test + version: "1.0" +paths: + /bookings: + post: + operationId: createBooking + x-codeSamples: + - lang: Python + source: + $ref: ../outside/secret.py + responses: + '201': + description: Created +` + site := pressSiteFromSpecWithConfig(t, spec, func(cfg *pressEngineConfig) { + cfg.SpecRoot = specRoot + cfg.SpecPath = filepath.Join(specRoot, "openapi.yaml") + cfg.SpecLocation = "openapi.yaml" + }) + require.Len(t, site.Operations, 1) + assert.Nil(t, site.Operations[0].CodeSamples) + require.NotEmpty(t, site.Warnings) + assert.Equal(t, "x-codeSamples source $ref resolves outside the spec root; skipping code sample", site.Warnings[0].Message) + assert.Equal(t, "../outside/secret.py", site.Warnings[0].Context) + assert.NoError(t, site.Warnings[0].Err) +} + +func TestCollectCodeSamples_SourceRefOverLimitWarnsAndSkips(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(dir, "code_samples"), 0o755)) + require.NoError(t, os.WriteFile( + filepath.Join(dir, "code_samples", "large.py"), + []byte(strings.Repeat("a", maxCodeSampleSourceBytes+1)), + 0o644, + )) + + spec := `openapi: "3.1.0" +info: + title: Test + version: "1.0" +paths: + /bookings: + post: + operationId: createBooking + x-codeSamples: + - lang: Python + source: + $ref: code_samples/large.py + responses: + '201': + description: Created +` + site := pressSiteFromSpecWithConfig(t, spec, func(cfg *pressEngineConfig) { + cfg.SpecRoot = dir + cfg.SpecPath = filepath.Join(dir, "openapi.yaml") + cfg.SpecLocation = "openapi.yaml" + }) + require.Len(t, site.Operations, 1) + assert.Nil(t, site.Operations[0].CodeSamples) + require.NotEmpty(t, site.Warnings) + assert.Equal(t, "x-codeSamples source $ref exceeds "+codeSampleSourceLimitLabel+"; skipping code sample", site.Warnings[0].Message) + assert.Equal(t, "code_samples/large.py", site.Warnings[0].Context) + assert.NoError(t, site.Warnings[0].Err) +} + +func TestCollectCodeSamples_ResolvesSourceRefRelativeToOperationOrigin(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(dir, "paths", "code_samples"), 0o755)) + require.NoError(t, os.WriteFile( + filepath.Join(dir, "paths", "code_samples", "go.go"), + []byte("client.Bookings.Create(\"abc\")\n"), + 0o644, + )) + + pp := &PrintingPress{engineConfig: &pressEngineConfig{ + SpecRoot: dir, + SpecPath: filepath.Join(dir, "openapi.yaml"), + }} + baseFile := pp.operationSourceFile(&bundler.ComponentOrigin{ + OriginalFile: filepath.Join(dir, "paths", "bookings.yaml"), + }) + samples := pp.collectCodeSamples(codeSampleExtensionsFromYAML(t, `x-codeSamples: + - lang: go + source: + $ref: code_samples/go.go +`), baseFile, "POST /bookings") + + require.Len(t, samples, 1) + assert.Equal(t, "go", samples[0].Lang) + assert.Equal(t, "code_samples/go.go", samples[0].SourceRef) + assert.Contains(t, samples[0].Source, "client.Bookings.Create") + assert.Empty(t, pp.warnings) +} + +func codeSampleExtensionsFromYAML(t *testing.T, source string) *orderedmap.Map[string, *yaml.Node] { + t.Helper() + var root yaml.Node + require.NoError(t, yaml.Unmarshal([]byte(source), &root)) + require.NotEmpty(t, root.Content) + mapping := root.Content[0] + require.Equal(t, yaml.MappingNode, mapping.Kind) + extensions := orderedmap.New[string, *yaml.Node]() + for i := 0; i+1 < len(mapping.Content); i += 2 { + key := mapping.Content[i] + value := mapping.Content[i+1] + if key != nil { + extensions.Set(key.Value, value) + } + } + return extensions +} + func TestModelExtensions(t *testing.T) { spec := `openapi: "3.1.0" info: diff --git a/printingpress/fixtures/testdata/ref-code-samples/openapi.yaml b/printingpress/fixtures/testdata/ref-code-samples/openapi.yaml new file mode 100644 index 0000000..3487a01 --- /dev/null +++ b/printingpress/fixtures/testdata/ref-code-samples/openapi.yaml @@ -0,0 +1,33 @@ +openapi: "3.1.0" +info: + title: Ref Code Samples + version: "1.0" +paths: + /bookings: + post: + operationId: createBooking + summary: Create booking + x-codeSamples: + - lang: typescript + label: create-booking_json + source: + $ref: snippets/create-booking-json.ts + - lang: typescript + label: create-booking_raw + source: + $ref: snippets/create-booking-raw.ts + - lang: go + label: create-booking_go + source: + $ref: snippets/create-booking.go + - lang: python + label: create-booking_python + source: + $ref: snippets/create-booking.py + - lang: c++ + label: create-booking_cpp + source: + $ref: snippets/create-booking.cpp + responses: + '201': + description: Created diff --git a/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking-json.ts b/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking-json.ts new file mode 100644 index 0000000..a825be5 --- /dev/null +++ b/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking-json.ts @@ -0,0 +1,16 @@ +import { TrainTravelSDK } from "train-travel-sdk"; + +const trainTravelSDK = new TrainTravelSDK({ + oAuth2: process.env["TRAINTRAVELSDK_O_AUTH2"] ?? "", +}); + +async function run() { + const result = await trainTravelSDK.bookings.createJson({ + tripId: "4f4e4e1-c824-4d63-b37a-d8d698862f1d", + passengerName: "John Doe", + }); + + console.log(result); +} + +run(); diff --git a/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking-raw.ts b/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking-raw.ts new file mode 100644 index 0000000..1bb9988 --- /dev/null +++ b/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking-raw.ts @@ -0,0 +1,16 @@ +import { TrainTravelSDK } from "train-travel-sdk"; + +const trainTravelSDK = new TrainTravelSDK({ + oAuth2: process.env["TRAINTRAVELSDK_O_AUTH2"] ?? "", +}); + +async function run() { + const payload = new TextEncoder().encode( + "{\"trip_id\":\"4f4e4e1-c824-4d63-b37a-d8d698862f1d\",\"passenger_name\":\"John Doe\"}", + ); + const result = await trainTravelSDK.bookings.createRaw(bytesToStream(payload)); + + console.log(result); +} + +run(); diff --git a/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking.cpp b/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking.cpp new file mode 100644 index 0000000..dc60cf4 --- /dev/null +++ b/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking.cpp @@ -0,0 +1,20 @@ +#include +#include +#include + +#include "train_travel.hpp" + +int main() { + train_travel::Client client(std::getenv("TRAIN_TRAVEL_TOKEN")); + + train_travel::CreateBookingRequest request{ + .trip_id = "4f4e4e1-c824-4d63-b37a-d8d698862f1d", + .passenger_name = "John Doe", + .has_bicycle = false, + .has_dog = true, + }; + + auto booking = client.bookings().create(request); + std::cout << "booking created: " << booking.id() << std::endl; + return 0; +} diff --git a/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking.go b/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking.go new file mode 100644 index 0000000..6c87e55 --- /dev/null +++ b/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking.go @@ -0,0 +1,22 @@ +package main + +import ( + "context" + "fmt" + "os" + + traintravel "github.com/example/train-travel-go" +) + +func main() { + client := traintravel.NewClient(os.Getenv("TRAIN_TRAVEL_TOKEN")) + booking, err := client.Bookings.Create(context.Background(), traintravel.CreateBookingRequest{ + TripID: "4f4e4e1-c824-4d63-b37a-d8d698862f1d", + PassengerName: "John Doe", + }) + if err != nil { + panic(err) + } + + fmt.Println(booking.ID) +} diff --git a/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking.py b/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking.py new file mode 100644 index 0000000..36fa061 --- /dev/null +++ b/printingpress/fixtures/testdata/ref-code-samples/snippets/create-booking.py @@ -0,0 +1,14 @@ +import os + +from train_travel import TrainTravelClient + + +client = TrainTravelClient(token=os.environ["TRAIN_TRAVEL_TOKEN"]) + +booking = client.bookings.create( + trip_id="4f4e4e1-c824-4d63-b37a-d8d698862f1d", + passenger_name="John Doe", + extras={"has_bicycle": False, "has_dog": True}, +) + +print(f"booking created: {booking.id}") diff --git a/printingpress/highlight.go b/printingpress/highlight.go index c08156a..4a3014c 100644 --- a/printingpress/highlight.go +++ b/printingpress/highlight.go @@ -7,6 +7,7 @@ package printingpress import ( "bytes" "html" + "strings" "github.com/alecthomas/chroma/v2" chromahtml "github.com/alecthomas/chroma/v2/formatters/html" @@ -17,7 +18,16 @@ import ( // highlightJSON renders JSON as syntax-highlighted HTML with chroma CSS classes. // Falls back to HTML-escaped plain text on error (never returns empty). func highlightJSON(code string) (string, bool) { - lexer := lexers.Get("json") + return highlightCode(code, "json") +} + +// highlightCode renders source code as syntax-highlighted HTML with chroma CSS classes. +// Falls back to HTML-escaped plain text on error (never returns empty). +func highlightCode(code, language string) (string, bool) { + lexer := lexers.Get(strings.TrimSpace(language)) + if lexer == nil { + lexer = lexers.Analyse(code) + } if lexer == nil { return html.EscapeString(code), false } diff --git a/printingpress/html_writer.go b/printingpress/html_writer.go index 42cad93..a5c7f49 100644 --- a/printingpress/html_writer.go +++ b/printingpress/html_writer.go @@ -288,6 +288,7 @@ func writeHTMLSiteDetailed(site *ppmodel.Site, outputDir, baseURL string, progre p := *params p.BaseURL = resolveBase(resolvedBaseURL, 1) p.ExtraCSS = []string{pppaths.StaticAsset(pppaths.FilePrintingPressOperationCSS)} + highlightCodeSamplesForHTML(op) opContent := render.OperationPageTempl(op, p.BaseURL) pageTitle := fmt.Sprintf("%s %s - %s", op.Method, op.Path, title) path := filepath.Join(resolvedOutputDir, filepath.FromSlash(pppaths.OperationHTML(op.Slug))) @@ -458,6 +459,7 @@ func writeHTMLSiteDetailed(site *ppmodel.Site, outputDir, baseURL string, progre p := *params p.BaseURL = resolveBase(resolvedBaseURL, 1) p.ExtraCSS = []string{pppaths.StaticAsset(pppaths.FilePrintingPressOperationCSS)} + highlightCodeSamplesForHTML(wh) whContent := render.OperationPageTempl(wh, p.BaseURL) pageTitle := fmt.Sprintf("Webhook: %s %s - %s", wh.Method, wh.Path, title) path := filepath.Join(resolvedOutputDir, filepath.FromSlash(pppaths.OperationHTML(wh.Slug))) @@ -697,6 +699,18 @@ func resolveHTMLAssetMode(site *ppmodel.Site) string { return site.AssetMode } +func highlightCodeSamplesForHTML(op *ppmodel.OperationPage) { + if op == nil { + return + } + for _, sample := range op.CodeSamples { + if sample == nil || sample.Source == "" || sample.HighlightedHTML != "" { + continue + } + sample.HighlightedHTML, _ = highlightCode(sample.Source, codeSampleHighlightLanguage(sample)) + } +} + // resolveBase returns the base href for a page at the given directory depth. // If a baseURL is explicitly set, it is used as-is. Otherwise, for file:// // compatibility, it computes the relative prefix (e.g. "../" for depth 1). diff --git a/printingpress/model/models.go b/printingpress/model/models.go index 9d63650..2e8a422 100644 --- a/printingpress/model/models.go +++ b/printingpress/model/models.go @@ -6,6 +6,7 @@ package model import ( "fmt" + "strings" v3 "github.com/pb33f/doctor/model/high/v3" "github.com/pb33f/libopenapi/bundler" @@ -273,6 +274,7 @@ type OperationPage struct { Servers []*ServerInfo ExternalDoc *ExternalDocInfo CurlJSON string `json:"curlJson,omitempty"` + CodeSamples []*CodeSample `json:"codeSamples,omitempty"` Extensions []*ExtensionEntry `json:"extensions,omitempty"` ExtensionsJSON string `json:"-"` // pre-serialized for Lit component PathExtensions []*ExtensionEntry `json:"pathExtensions,omitempty"` @@ -483,6 +485,34 @@ type CurlVariant struct { Command string `json:"command"` } +// CodeSample holds a first-class operation code sample from x-codeSamples. +type CodeSample struct { + Lang string `json:"lang,omitempty"` + Label string `json:"label,omitempty"` + Source string `json:"source"` + SourceRef string `json:"sourceRef,omitempty"` + HighlightedHTML string `json:"-"` +} + +// DisplayLabel returns the primary user-facing sample name, preferring lang over label. +func (c *CodeSample) DisplayLabel(index int) string { + if c != nil { + if lang := codeSampleInlineText(c.Lang); lang != "" { + return lang + } + if label := codeSampleInlineText(c.Label); label != "" { + return label + } + } + return fmt.Sprintf("Sample %d", index+1) +} + +func codeSampleInlineText(value string) string { + value = strings.ReplaceAll(value, "\r\n", " ") + value = strings.ReplaceAll(value, "\n", " ") + return strings.TrimSpace(value) +} + // SecurityRequirement holds a resolved security scheme with type info and model link. type SecurityRequirement struct { Name string `json:"name"` diff --git a/printingpress/press_test.go b/printingpress/press_test.go index 49adcb5..633404a 100644 --- a/printingpress/press_test.go +++ b/printingpress/press_test.go @@ -539,6 +539,113 @@ func TestPrintingPress_WriteHTMLSite(t *testing.T) { } } +func TestPrintingPress_WriteHTMLSite_RendersCodeSamplesSection(t *testing.T) { + spec := `openapi: "3.1.0" +info: + title: Code Samples + version: "1.0" +paths: + /bookings: + post: + operationId: createBooking + summary: Create booking + x-codeSamples: + - lang: typescript + label: create-booking_json + source: | + const client = new TrainTravelSDK(); + await client.bookings.create({ tripId: "abc" }); + - lang: Python + source: | + client.bookings.create({"tripId": "abc"}) + - | + const raw = await fetch("/bookings", { + method: "POST", + body: JSON.stringify({ tripId: "abc" }) + }); + responses: + '201': + description: Created +` + site := pressFromSpec(t, spec) + require.Len(t, site.Operations, 1) + + outputDir := t.TempDir() + require.NoError(t, WriteHTMLSite(site, outputDir, "")) + + op := site.Operations[0] + operationHTML, err := os.ReadFile(filepath.Join(outputDir, "operations", op.Slug+".html")) + require.NoError(t, err) + html := string(operationHTML) + assert.Contains(t, html, `id="section-code-samples" data-nav-label="Code Samples"`) + assert.Contains(t, html, ``) + assert.Contains(t, html, `typescript`) + assert.Contains(t, html, `Python`) + assert.Contains(t, html, `Sample 3`) + assert.Contains(t, html, ``) + assert.Contains(t, html, ``) + assert.Contains(t, html, ``) + assert.Contains(t, html, `

create-booking_json

`) + assert.Contains(t, html, `
`)
+	assert.Contains(t, html, "TrainTravelSDK")
+	assert.Contains(t, html, "bookings")
+	assert.Contains(t, html, "create")
+	assert.Contains(t, html, "fetch")
+}
+
+func TestPrintingPress_WriteHTMLSite_RendersCodeSamplesFromSourceRefs(t *testing.T) {
+	fixtureRoot := filepath.Join("fixtures", "testdata", "ref-code-samples")
+	specPath := filepath.Join(fixtureRoot, "openapi.yaml")
+	specBytes, err := os.ReadFile(specPath)
+	require.NoError(t, err)
+
+	outputDir := t.TempDir()
+	pp, err := CreatePrintingPressFromBytes(specBytes, &PrintingPressConfig{
+		BasePath:  fixtureRoot,
+		SpecPath:  specPath,
+		OutputDir: outputDir,
+	})
+	require.NoError(t, err)
+
+	site, err := pp.PressModel()
+	require.NoError(t, err)
+	require.Len(t, site.Operations, 1)
+	op := site.Operations[0]
+	require.Len(t, op.CodeSamples, 5)
+	assert.Equal(t, "snippets/create-booking-json.ts", op.CodeSamples[0].SourceRef)
+	assert.Equal(t, "snippets/create-booking-raw.ts", op.CodeSamples[1].SourceRef)
+	assert.Equal(t, "snippets/create-booking.go", op.CodeSamples[2].SourceRef)
+	assert.Equal(t, "snippets/create-booking.py", op.CodeSamples[3].SourceRef)
+	assert.Equal(t, "snippets/create-booking.cpp", op.CodeSamples[4].SourceRef)
+	assert.Contains(t, op.CodeSamples[0].Source, "createJson")
+	assert.Contains(t, op.CodeSamples[1].Source, "bytesToStream")
+	assert.Contains(t, op.CodeSamples[2].Source, "traintravel.NewClient")
+	assert.Contains(t, op.CodeSamples[3].Source, "TrainTravelClient")
+	assert.Contains(t, op.CodeSamples[4].Source, "train_travel::Client")
+	assert.Empty(t, op.CodeSamples[0].HighlightedHTML)
+
+	_, err = pp.PrintHTML()
+	require.NoError(t, err)
+	assert.NotEmpty(t, op.CodeSamples[0].HighlightedHTML)
+
+	operationHTML, err := os.ReadFile(filepath.Join(outputDir, "operations", op.Slug+".html"))
+	require.NoError(t, err)
+	html := string(operationHTML)
+	assert.Contains(t, html, `typescript`)
+	assert.Contains(t, html, `typescript`)
+	assert.Contains(t, html, `go`)
+	assert.Contains(t, html, `python`)
+	assert.Contains(t, html, `c++`)
+	assert.Contains(t, html, `

create-booking_go

`) + assert.Contains(t, html, `

create-booking_python

`) + assert.Contains(t, html, `

create-booking_cpp

`) + assert.Contains(t, html, "train-travel-go") + assert.Contains(t, html, "TrainTravelClient") + assert.Contains(t, html, "train_travel") + assert.Contains(t, html, "TrainTravelSDK") + assert.Contains(t, html, "bytesToStream") +} + func TestPrintingPress_WriteHTMLSiteProgressReportsPreparationAndWrites(t *testing.T) { specBytes, err := os.ReadFile("../test_specs/burgershop.openapi.yaml") require.NoError(t, err) diff --git a/printingpress/render/helpers.go b/printingpress/render/helpers.go index 6a5e04d..b63a7c4 100644 --- a/printingpress/render/helpers.go +++ b/printingpress/render/helpers.go @@ -5,6 +5,7 @@ package render import ( + "html" "net/url" "strings" "unicode" @@ -205,6 +206,9 @@ func operationNavSections(page *ppmodel.OperationPage) string { if page.CurlJSON != "" { sections = append(sections, navSection{"cURL", "section-curl"}) } + if len(page.CodeSamples) > 0 { + sections = append(sections, navSection{"Code Samples", "section-code-samples"}) + } if page.ExtensionsJSON != "" { sections = append(sections, navSection{"Extensions", "section-extensions"}) } @@ -220,6 +224,20 @@ func operationNavSections(page *ppmodel.OperationPage) string { return MustJSON(sections) } +func codeSampleTabLabel(sample *ppmodel.CodeSample, index int) string { + return sample.DisplayLabel(index) +} + +func codeSampleHighlightedHTML(sample *ppmodel.CodeSample) string { + if sample == nil { + return "" + } + if sample.HighlightedHTML != "" { + return sample.HighlightedHTML + } + return html.EscapeString(sample.Source) +} + func truncateDesc(desc string, maxLen int) string { desc = strings.TrimSpace(singleLine(desc)) if len(desc) <= maxLen { diff --git a/printingpress/render/templ_operation.templ b/printingpress/render/templ_operation.templ index d94736d..5920d33 100644 --- a/printingpress/render/templ_operation.templ +++ b/printingpress/render/templ_operation.templ @@ -162,6 +162,12 @@ templ OperationPageTempl(page *ppmodel.OperationPage, baseURL string) { } + if len(page.CodeSamples) > 0 { + +

Code Samples

+ @CodeSamplesSection(page.CodeSamples) +
+ } if page.ExtensionsJSON != "" {

Operation Extensions

@@ -201,6 +207,40 @@ templ OperationPageTempl(page *ppmodel.OperationPage, baseURL string) { } +templ CodeSamplesSection(samples []*ppmodel.CodeSample) { +
+ if len(samples) == 1 { + @CodeSamplePanel(samples[0]) + } else { + + for i, sample := range samples { + { codeSampleTabLabel(sample, i) } + } + for i, sample := range samples { + + @CodeSamplePanel(sample) + + } + + } +
+} + +templ CodeSamplePanel(sample *ppmodel.CodeSample) { + if sample != nil { +
+ if sample.Label != "" && sample.Label != sample.Lang { +

{ sample.Label }

+ } +
@templ.Raw(codeSampleHighlightedHTML(sample))
+
+ } +} + templ RequestBodySection(rb *ppmodel.RequestBodyInfo, baseURL string) {

Request Body diff --git a/printingpress/render/templ_operation_templ.go b/printingpress/render/templ_operation_templ.go index 05709d1..bc2bf81 100644 --- a/printingpress/render/templ_operation_templ.go +++ b/printingpress/render/templ_operation_templ.go @@ -641,52 +641,66 @@ func OperationPageTempl(page *ppmodel.OperationPage, baseURL string) templ.Compo return templ_7745c5c3_Err } } + if len(page.CodeSamples) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "

Code Samples

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = CodeSamplesSection(page.CodeSamples).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } if page.ExtensionsJSON != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "

Operation Extensions

Operation Extensions

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if page.PathExtensionsJSON != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "

Path Extensions

Path Extensions

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if page.CallbacksJSON != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "

Callbacks

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "

Callbacks

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if page.ExternalDoc != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "

EXTERNAL DOCS

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "

EXTERNAL DOCS

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -694,54 +708,54 @@ func OperationPageTempl(page *ppmodel.OperationPage, baseURL string) templ.Compo var templ_7745c5c3_Var31 string templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(page.ExternalDoc.Description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 189, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 195, Col: 37} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "Documentation ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "Documentation ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "\" target=\"_blank\" rel=\"noopener\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var33 string templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(page.ExternalDoc.URL) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 196, Col: 28} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 202, Col: 28} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -749,7 +763,7 @@ func OperationPageTempl(page *ppmodel.OperationPage, baseURL string) templ.Compo }) } -func RequestBodySection(rb *ppmodel.RequestBodyInfo, baseURL string) templ.Component { +func CodeSamplesSection(samples []*ppmodel.CodeSample) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -770,64 +784,253 @@ func RequestBodySection(rb *ppmodel.RequestBodyInfo, baseURL string) templ.Compo templ_7745c5c3_Var34 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "

Request Body ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if rb.RawYAML != "" || rb.RawJSON != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, " 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, " start-line=\"") + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for i, sample := range samples { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var36 string - templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(rb.Location) + templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(codeSampleTabLabel(sample, i)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 216, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 219, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + for i, sample := range samples { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = CodeSamplePanel(sample).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func CodeSamplePanel(sample *ppmodel.CodeSample) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var38 := templ.GetChildren(ctx) + if templ_7745c5c3_Var38 == nil { + templ_7745c5c3_Var38 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if sample != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if sample.Label != "" && sample.Label != sample.Lang { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var39 string + templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(sample.Label) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 237, Col: 51} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, ">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "
")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templ.Raw(codeSampleHighlightedHTML(sample)).Render(ctx, templ_7745c5c3_Buffer)
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "

") + return nil + }) +} + +func RequestBodySection(rb *ppmodel.RequestBodyInfo, baseURL string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var40 := templ.GetChildren(ctx) + if templ_7745c5c3_Var40 == nil { + templ_7745c5c3_Var40 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "

Request Body ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if rb.RawYAML != "" || rb.RawJSON != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, " 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, " start-line=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var41 string + templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", rb.SourceLine)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 253, Col: 50} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if rb.Location != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, " location=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var42 string + templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(rb.Location) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 256, Col: 27} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, ">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if rb.DescHTML != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -835,68 +1038,68 @@ func RequestBodySection(rb *ppmodel.RequestBodyInfo, baseURL string) templ.Compo if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if rb.ExtensionsJSON != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if rb.Ref != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "

➜ ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\">➜ ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var39 string - templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(rb.Ref.Name) + var templ_7745c5c3_Var45 string + templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(rb.Ref.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 232, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 272, Col: 27} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if len(rb.Content) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -920,55 +1123,55 @@ func InlineExamples(mt *ppmodel.MediaTypeInfo) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var40 := templ.GetChildren(ctx) - if templ_7745c5c3_Var40 == nil { - templ_7745c5c3_Var40 = templ.NopComponent + templ_7745c5c3_Var46 := templ.GetChildren(ctx) + if templ_7745c5c3_Var46 == nil { + templ_7745c5c3_Var46 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if len(mt.Examples) > 0 || mt.MockJSON != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, " 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, " examples-json=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, " examples-json=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var41 string - templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(MustJSON(mt.Examples)) + var templ_7745c5c3_Var47 string + templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(MustJSON(mt.Examples)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 245, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 285, Col: 41} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if mt.MockJSON != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, " mock-json=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, " mock-json=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var42 string - templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(mt.MockJSON) + var templ_7745c5c3_Var48 string + templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(mt.MockJSON) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 248, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 288, Col: 27} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, ">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, ">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -993,117 +1196,117 @@ func CommonHeadersSection(headers []*ppmodel.HeaderInfo, headersJSON string, bas }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var43 := templ.GetChildren(ctx) - if templ_7745c5c3_Var43 == nil { - templ_7745c5c3_Var43 = templ.NopComponent + templ_7745c5c3_Var49 := templ.GetChildren(ctx) + if templ_7745c5c3_Var49 == nil { + templ_7745c5c3_Var49 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, h := range headers { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if h.Ref != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "➜ ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "\">➜ ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var46 string - templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(h.Name) + var templ_7745c5c3_Var52 string + templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(h.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 260, Col: 23} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 300, Col: 23} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var47 string - templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(h.Name) + var templ_7745c5c3_Var53 string + templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(h.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 263, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 303, Col: 49} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if h.SchemaType != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var48 string - templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(h.SchemaType) + var templ_7745c5c3_Var54 string + templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(h.SchemaType) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 266, Col: 55} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 306, Col: 55} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if h.Description != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var49 string - templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(h.Description) + var templ_7745c5c3_Var55 string + templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(h.Description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 269, Col: 55} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 309, Col: 55} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1112,12 +1315,12 @@ func CommonHeadersSection(headers []*ppmodel.HeaderInfo, headersJSON string, bas if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1141,93 +1344,93 @@ func SecuritySchemeTypeBadge(sec *ppmodel.SecurityRequirement) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var50 := templ.GetChildren(ctx) - if templ_7745c5c3_Var50 == nil { - templ_7745c5c3_Var50 = templ.NopComponent + templ_7745c5c3_Var56 := templ.GetChildren(ctx) + if templ_7745c5c3_Var56 == nil { + templ_7745c5c3_Var56 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } switch sec.SchemeType { case "apiKey": if sec.In == "cookie" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "Cookie ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "Cookie ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if sec.In == "header" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "API Key ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "API Key ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if sec.In == "query" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "API Key ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "API Key ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "http": if sec.Scheme == "bearer" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "Bearer ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "Bearer ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if sec.Scheme == "basic" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "Basic Auth ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "Basic Auth ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "HTTP ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "HTTP ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var51 string - templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(sec.Scheme) + var templ_7745c5c3_Var57 string + templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(sec.Scheme) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 299, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 339, Col: 59} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "oauth2": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "OAuth 2.0 ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "OAuth 2.0 ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "openIdConnect": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "OpenID Connect ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "OpenID Connect ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var52 string - templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(sec.SchemeType) + var templ_7745c5c3_Var58 string + templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(sec.SchemeType) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 309, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `printingpress/render/templ_operation.templ`, Line: 349, Col: 57} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/printingpress/static/printing-press-operation.css b/printingpress/static/printing-press-operation.css index 1e5c3f2..97c2789 100644 --- a/printingpress/static/printing-press-operation.css +++ b/printingpress/static/printing-press-operation.css @@ -16,3 +16,53 @@ h1 { scroll-margin-top: var(--scroll-margin); font-weight: normal; } + +.pp-code-samples { + display: block; + width: 100%; +} + +.pp-code-samples-tabs::part(tabs) { + border-bottom: 1px dashed var(--hrcolor); +} + +.pp-code-samples-tabs::part(body) { + padding-top: var(--global-padding); +} + +.pp-code-samples-tabs sl-tab::part(base) { + font-family: var(--font-stack-bold), monospace; + letter-spacing: 0; + text-transform: uppercase; +} + +.pp-code-samples-tabs sl-tab[active]::part(base) { + color: var(--primary-color); +} + +.pp-code-sample { + min-width: 0; +} + +.pp-code-sample-label { + border-bottom: none; + color: var(--secondary-color); + font-family: var(--font-stack-bold), monospace; + font-weight: 700; + margin: 0; + padding-bottom: var(--global-padding); + word-break: break-word; +} + +pre.pp-code-sample-code { + margin-bottom: 0; + max-width: 100%; + overflow-x: hidden; + white-space: pre-wrap; + width: auto; +} + +.pp-code-sample-code code { + overflow-wrap: anywhere; + white-space: pre-wrap; +} diff --git a/terminal/printingpress_progress_test.go b/terminal/printingpress_progress_test.go index 4498d34..5cecb71 100644 --- a/terminal/printingpress_progress_test.go +++ b/terminal/printingpress_progress_test.go @@ -218,11 +218,17 @@ func TestPrintSummaryRendersStatsAndWarnings(t *testing.T) { "schemas": {{Name: "Burger", MermaidDiagram: "graph TD", GraphJSON: "{}"}}, }, Operations: []*ppmodel.OperationPage{{OperationID: "listBurgers"}}, - Warnings: []*ppmodel.BuildWarning{{ - Message: "source bundling failed; falling back to single-file parse, multi-file output may be incomplete", - Context: "/tmp/specs", - Err: errors.New("invalid model\ninfinite circular reference detected: payment_intent"), - }}, + Warnings: []*ppmodel.BuildWarning{ + { + Message: "x-codeSamples source $ref could not be read; skipping code sample", + Context: "snippets/missing.py", + }, + { + Message: "source bundling failed; falling back to single-file parse, multi-file output may be incomplete", + Context: "/tmp/specs", + Err: errors.New("invalid model\ninfinite circular reference detected: payment_intent"), + }, + }, } htmlStats := &printingpress.PressStatistics{ Pages: 2676, @@ -247,7 +253,12 @@ func TestPrintSummaryRendersStatsAndWarnings(t *testing.T) { require.Contains(t, output, "2,087") require.Contains(t, output, "13,708 files, 2.0 MiB") require.Contains(t, output, "warnings (1)") + require.Contains(t, output, "errors (1)") + require.Contains(t, output, "WRN") + require.Contains(t, output, "ERR") + require.Contains(t, output, "x-codeSamples source $ref could not be read; skipping code sample") require.Contains(t, output, "infinite circular reference detected: payment_intent") + require.Equal(t, 1, strings.Count(output, "source bundling failed; falling back to single-file parse, multi-file output may be incomplete")) require.Contains(t, output, "├─") require.Contains(t, output, "└─") } diff --git a/terminal/summary.go b/terminal/summary.go index e4aa9e6..83e9f48 100644 --- a/terminal/summary.go +++ b/terminal/summary.go @@ -43,7 +43,7 @@ func PrintSummary(writer io.Writer, palette Palette, site *ppmodel.Site, htmlSta } fmt.Fprintln(writer) - fmt.Fprintln(writer, withPrintingPressGutterBlock(renderWarningsSummary(palette, site.Warnings))) + fmt.Fprintln(writer, withPrintingPressGutterBlock(renderIssuesSummary(palette, site.Warnings))) } func PrintAggregateSummary(writer io.Writer, palette Palette, catalog *ppmodel.CatalogSite, htmlStats, jsonStats, llmStats *printingpress.AggregatePressStatistics, totalDuration time.Duration, fileCount int, totalBytes int64) { @@ -62,7 +62,7 @@ func PrintAggregateSummary(writer io.Writer, palette Palette, catalog *ppmodel.C } fmt.Fprintln(writer) - fmt.Fprintln(writer, withPrintingPressGutterBlock(renderWarningsSummary(palette, catalog.Warnings))) + fmt.Fprintln(writer, withPrintingPressGutterBlock(renderIssuesSummary(palette, catalog.Warnings))) } func buildSummaryRows(site *ppmodel.Site, htmlStats, llmStats *printingpress.PressStatistics, totalDuration time.Duration, fileCount int, totalBytes int64) []summaryRow { @@ -89,7 +89,7 @@ func buildSummaryRows(site *ppmodel.Site, htmlStats, llmStats *printingpress.Pre operationCount = len(site.Operations) + len(site.Webhooks) classDiagramCount = countClassDiagrams(site) dependencyDiagramCount = countDependencyGraphs(site) - warningCount = len(site.Warnings) + warningCount = countWarnings(site.Warnings) errorCount = countWarningErrors(site.Warnings) } if contentStats != nil { @@ -159,7 +159,7 @@ func buildAggregateSummaryRows(catalog *ppmodel.CatalogSite, htmlStats, jsonStat if catalog != nil { outputDir = catalog.OutputDir - warningCount = len(catalog.Warnings) + warningCount = countWarnings(catalog.Warnings) errorCount = countWarningErrors(catalog.Warnings) } if stats != nil { @@ -258,6 +258,27 @@ func countWarningErrors(warnings []*ppmodel.BuildWarning) int { return total } +func countWarnings(warnings []*ppmodel.BuildWarning) int { + total := 0 + for _, warning := range warnings { + if warning != nil && warning.Err == nil { + total++ + } + } + return total +} + +func renderIssuesSummary(palette Palette, warnings []*ppmodel.BuildWarning) string { + sections := make([]string, 0, 2) + if warningSection := renderWarningsSummary(palette, warnings); warningSection != "" { + sections = append(sections, warningSection) + } + if errorSection := renderErrorsSummary(palette, warnings); errorSection != "" { + sections = append(sections, errorSection) + } + return strings.Join(sections, "\n\n") +} + func renderWarningsSummary(palette Palette, warnings []*ppmodel.BuildWarning) string { titleStyle := styleWithForeground(palette.Modification).Bold(true) badgeStyle := lipgloss.NewStyle(). @@ -268,7 +289,7 @@ func renderWarningsSummary(palette Palette, warnings []*ppmodel.BuildWarning) st blocks := make([]string, 0, len(warnings)) for _, warning := range warnings { - if warning == nil { + if warning == nil || warning.Err != nil { continue } blocks = append(blocks, renderWarningBlock(palette, badgeStyle, warning)) @@ -282,25 +303,83 @@ func renderWarningsSummary(palette Palette, warnings []*ppmodel.BuildWarning) st func renderWarningBlock(palette Palette, badgeStyle lipgloss.Style, warning *ppmodel.BuildWarning) string { messageStyle := styleWithForeground(palette.Modification) - keyStyle := lipgloss.NewStyle().Bold(true) valueStyle := styleWithForeground(palette.Modification) treeStyle := styleWithForeground(palette.Muted) - var b strings.Builder - b.WriteString(fmt.Sprintf("%s %s\n", badgeStyle.Render("WRN"), messageStyle.Render(warning.Message))) + attrs := make([]issueBlockAttr, 0, 1) + if warning.Context != "" { + attrs = append(attrs, issueBlockAttr{key: "context", value: warning.Context, valueStyle: valueStyle}) + } + return renderIssueBlock(badgeStyle, "WRN", warning.Message, "unknown warning", messageStyle, treeStyle, attrs) +} + +func renderErrorsSummary(palette Palette, warnings []*ppmodel.BuildWarning) string { + titleStyle := styleWithForeground(palette.Breaking).Bold(true) + badgeStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("0")). + Background(summaryColorValue(palette.Breaking, "9")). + Padding(0, 1) - type attr struct { - key string - value string + blocks := make([]string, 0, len(warnings)) + for _, warning := range warnings { + if warning == nil || warning.Err == nil { + continue + } + blocks = append(blocks, renderErrorBlock(palette, badgeStyle, warning)) } - attrs := make([]attr, 0, 2) + if len(blocks) == 0 { + return "" + } + + return titleStyle.Render(fmt.Sprintf("errors (%d)", len(blocks))) + "\n\n" + strings.Join(blocks, "\n\n") +} + +func renderErrorBlock(palette Palette, badgeStyle lipgloss.Style, warning *ppmodel.BuildWarning) string { + messageStyle := styleWithForeground(palette.Breaking) + valueStyle := styleWithForeground(palette.Breaking) + warningValueStyle := styleWithForeground(palette.Modification) + treeStyle := styleWithForeground(palette.Muted) + + attrs := make([]issueBlockAttr, 0, 2) if warning.Context != "" { - attrs = append(attrs, attr{key: "context", value: warning.Context}) + attrs = append(attrs, issueBlockAttr{key: "context", value: warning.Context, valueStyle: valueStyle}) + } + if warning.Message != "" { + attrs = append(attrs, issueBlockAttr{key: "warning", value: warning.Message, valueStyle: warningValueStyle}) } - if warning.Err != nil { - attrs = append(attrs, attr{key: "error", value: warning.Err.Error()}) + return renderIssueBlock(badgeStyle, "ERR", warning.Err.Error(), "unknown error", messageStyle, treeStyle, attrs) +} + +type issueBlockAttr struct { + key string + value string + valueStyle lipgloss.Style +} + +func renderIssueBlock( + badgeStyle lipgloss.Style, + badge string, + headline string, + fallbackHeadline string, + headlineStyle lipgloss.Style, + treeStyle lipgloss.Style, + attrs []issueBlockAttr, +) string { + headlineLines := splitWarningLines(headline) + if len(headlineLines) == 0 { + headlineLines = []string{fallbackHeadline} } + keyStyle := lipgloss.NewStyle().Bold(true) + var b strings.Builder + b.WriteString(fmt.Sprintf("%s %s\n", badgeStyle.Render(badge), headlineStyle.Render(headlineLines[0]))) + for _, line := range headlineLines[1:] { + b.WriteString(summaryTreeSpace) + b.WriteString(" ") + b.WriteString(headlineStyle.Render(line)) + b.WriteByte('\n') + } for i, item := range attrs { lines := splitWarningLines(item.value) if len(lines) == 0 { @@ -317,12 +396,12 @@ func renderWarningBlock(palette Palette, badgeStyle lipgloss.Style, warning *ppm b.WriteString(" ") b.WriteString(keyStyle.Render(item.key)) b.WriteString(": ") - b.WriteString(valueStyle.Render(lines[0])) + b.WriteString(item.valueStyle.Render(lines[0])) b.WriteByte('\n') for _, line := range lines[1:] { b.WriteString(treeStyle.Render(childPrefix)) b.WriteString(" ") - b.WriteString(valueStyle.Render(line)) + b.WriteString(item.valueStyle.Render(line)) b.WriteByte('\n') } }