diff --git a/pkg/tfgen/installation_docs_test.go b/pkg/tfgen/installation_docs_test.go index f255351ed..06ccfd2ea 100644 --- a/pkg/tfgen/installation_docs_test.go +++ b/pkg/tfgen/installation_docs_test.go @@ -78,6 +78,16 @@ func TestPlainDocsParser(t *testing.T) { }, ), }, + { + // Discovered while generating docs for SD-WAN. + // Tests whether the custom table renderer is used correctly in docsgen overall. + name: "Transforms table correctly", + docFile: DocFile{ + Content: []byte(readfile(t, "test_data/convert-index-file-with-table/input.md")), + }, + expected: []byte(readfile(t, "test_data/convert-index-file-with-table/expected.md")), + edits: defaultEditRules(), + }, } for _, tt := range tests { tt := tt diff --git a/pkg/tfgen/parse/extension.go b/pkg/tfgen/parse/extension.go index 64f4516b2..1852d4520 100644 --- a/pkg/tfgen/parse/extension.go +++ b/pkg/tfgen/parse/extension.go @@ -16,7 +16,6 @@ package parse import ( "bytes" - "fmt" "strings" "github.com/olekukonko/tablewriter" @@ -58,7 +57,11 @@ func (s tfRegistryExtension) Extend(md goldmark.Markdown) { // since the HTML content is shown as-is. // // [^1]: https://github.com/teekennedy/goldmark-markdown/issues/19 - util.Prioritized(tableRenderer{md.Renderer()}, 499), + util.Prioritized(tableRenderer{ + renderer: markdown.NewRenderer(), + rows: make([][]string, 0), + headerRow: make([]string, 0), + }, 499), // The markdown renderer we use does not support rendering raw // [ast.String] nodes.[^2] We just render them out as-is. // @@ -131,89 +134,102 @@ func (renderType renderType) RegisterFuncs(r renderer.NodeRendererFuncRegisterer r.Register(renderType.kind, renderType.f) } -func panicOnRender(writer util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { - contract.Failf("The renderer for %s should not have been called", n.Kind()) - return ast.WalkStop, nil -} - var _ renderer.NodeRenderer = (*tableRenderer)(nil) type tableRenderer struct { - renderer.Renderer + renderer *markdown.Renderer + headerRow []string + rows [][]string + tableWriter *tablewriter.Table + inHeader bool + tableWidth int } func (tableRenderer tableRenderer) RegisterFuncs(r renderer.NodeRendererFuncRegisterer) { - r.Register(extensionast.KindTable, tableRenderer.render) - r.Register(extensionast.KindTableHeader, panicOnRender) - r.Register(extensionast.KindTableRow, panicOnRender) - r.Register(extensionast.KindTableCell, - func(writer util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { - return ast.WalkContinue, nil - }) + r.Register(extensionast.KindTable, tableRenderer.renderTable) + r.Register(extensionast.KindTableHeader, tableRenderer.renderHeader) + r.Register(extensionast.KindTableRow, tableRenderer.renderRow) + r.Register(extensionast.KindTableCell, tableRenderer.renderCell) } -func (tableRenderer tableRenderer) render( - writer util.BufWriter, source []byte, n ast.Node, entering bool, +func (tableRenderer *tableRenderer) renderTable( + writer util.BufWriter, + source []byte, + n ast.Node, + entering bool, ) (ast.WalkStatus, error) { - if !entering { - return ast.WalkSkipChildren, nil + if entering { + tableRenderer.tableWidth = len(n.(*extensionast.Table).Alignments) + tableRenderer.headerRow = make([]string, 0, tableRenderer.tableWidth) + tableRenderer.rows = [][]string{} + tableRenderer.tableWriter = tablewriter.NewWriter(writer) + } else { + _, err := writer.WriteRune('\n') + contract.AssertNoErrorf(err, "impossible") + tableRenderer.tableWriter.SetHeader(tableRenderer.headerRow) + tableRenderer.tableWriter.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + tableRenderer.tableWriter.SetCenterSeparator("|") + tableRenderer.tableWriter.SetAutoFormatHeaders(false) + tableRenderer.tableWriter.SetAutoMergeCells(false) + tableRenderer.tableWriter.SetAutoWrapText(false) + tableRenderer.tableWriter.SetReflowDuringAutoWrap(false) + tableRenderer.tableWriter.AppendBulk(tableRenderer.rows) + tableRenderer.tableWriter.Render() } - _, err := writer.WriteRune('\n') // this is so that we have a newline between markdown elements. - contract.AssertNoErrorf(err, "impossible") - var inHeader bool - header := make([]string, 0, len(n.(*extensionast.Table).Alignments)) - var rows [][]string - err = ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) { - switch n := n.(type) { - case *extensionast.Table: - return ast.WalkContinue, nil - case *extensionast.TableHeader: - inHeader = entering - return ast.WalkContinue, nil - case *extensionast.TableRow: - if entering { - rows = append(rows, make([]string, 0, len(n.Alignments))) - } - return ast.WalkContinue, nil - case *extensionast.TableCell: - if entering { - var cell bytes.Buffer - textBlock := ast.NewTextBlock() - child := n.FirstChild() - for { - if child == nil { - break - } - next := child.NextSibling() - textBlock.AppendChild(textBlock, child) - child = next - } - err := tableRenderer.Render(&cell, source, textBlock) - if err != nil { - return ast.WalkStop, err - } - content := strings.TrimSpace(cell.String()) - if inHeader { - header = append(header, content) - } else { - rows[len(rows)-1] = append(rows[len(rows)-1], content) - } + return ast.WalkContinue, nil +} + +func (tableRenderer *tableRenderer) renderHeader( + writer util.BufWriter, + source []byte, + n ast.Node, + entering bool, +) (ast.WalkStatus, error) { + tableRenderer.inHeader = entering + return ast.WalkContinue, nil +} + +func (tableRenderer *tableRenderer) renderRow( + writer util.BufWriter, + source []byte, + n ast.Node, + entering bool, +) (ast.WalkStatus, error) { + if entering { + tableRenderer.rows = append(tableRenderer.rows, make([]string, 0, tableRenderer.tableWidth)) + } + + return ast.WalkContinue, nil +} + +func (tableRenderer *tableRenderer) renderCell( + writer util.BufWriter, + source []byte, + n ast.Node, + entering bool, +) (ast.WalkStatus, error) { + if entering { + var cell bytes.Buffer + textBlock := ast.NewTextBlock() + child := n.FirstChild() + for { + if child == nil { + break } - return ast.WalkSkipChildren, nil - default: - return ast.WalkStop, fmt.Errorf("unexpected node in a table: %s", n.Kind()) + next := child.NextSibling() + textBlock.AppendChild(textBlock, child) + child = next } - }) - table := tablewriter.NewWriter(writer) - table.SetHeader(header) - table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) - table.SetCenterSeparator("|") - table.SetAutoFormatHeaders(false) - table.SetAutoMergeCells(false) - table.SetAutoWrapText(false) - table.SetReflowDuringAutoWrap(false) - table.AppendBulk(rows) - table.Render() - - return ast.WalkSkipChildren, err + err := (*tableRenderer.renderer).Render(&cell, source, textBlock) + if err != nil { + return ast.WalkStop, err + } + content := strings.TrimSpace(cell.String()) + if tableRenderer.inHeader { + tableRenderer.headerRow = append(tableRenderer.headerRow, content) + } else { + tableRenderer.rows[len(tableRenderer.rows)-1] = append(tableRenderer.rows[len(tableRenderer.rows)-1], content) + } + } + return ast.WalkSkipChildren, nil } diff --git a/pkg/tfgen/parse/extension_test.go b/pkg/tfgen/parse/extension_test.go index a012ca2fd..599079ae0 100644 --- a/pkg/tfgen/parse/extension_test.go +++ b/pkg/tfgen/parse/extension_test.go @@ -83,6 +83,11 @@ func TestRenderTable(t *testing.T) { input: readfile(t, "../test_data/table-rendering/input.md"), expected: autogold.Expect(readfile(t, "../test_data/table-rendering/expected.md")), }, + { + name: "in-middle-of-document", + input: readfile(t, "../test_data/table-rendering/in-middle-of-doc-input.md"), + expected: autogold.Expect(readfile(t, "../test_data/table-rendering/in-middle-of-doc-expected.md")), + }, } for _, tt := range tests { diff --git a/pkg/tfgen/test_data/convert-index-file-with-table/expected.md b/pkg/tfgen/test_data/convert-index-file-with-table/expected.md new file mode 100644 index 000000000..72b0549ba --- /dev/null +++ b/pkg/tfgen/test_data/convert-index-file-with-table/expected.md @@ -0,0 +1,267 @@ +--- +# *** WARNING: This file was auto-generated. Do not edit by hand unless you're certain you know what you are doing! *** +title: Libvirt Provider +meta_desc: Provides an overview on how to configure the Pulumi Libvirt provider. +layout: package +--- +## Installation + +The Libvirt provider is available as a package in all Pulumi languages: + +* JavaScript/TypeScript: [`@pulumi/libvirt`](https://www.npmjs.com/package/@pulumi/libvirt) +* Python: [`pulumi-libvirt`](https://pypi.org/project/pulumi-libvirt/) +* Go: [`github.com/pulumi/pulumi-libvirt/sdk/go/libvirt`](https://github.com/pulumi/pulumi-libvirt) +* .NET: [`Pulumi.Libvirt`](https://www.nuget.org/packages/Pulumi.Libvirt) +* Java: [`com.pulumi/libvirt`](https://central.sonatype.com/artifact/com.pulumi/libvirt) +## Overview + +The Libvirt provider is used to interact with Linux +[libvirt](https://libvirt.org) hypervisors. + +The provider needs to be configured with the proper connection information +before it can be used. + +| t1 | *t2* | +|----------|------| +| **r1c1** | r1c2 | + +> **Note:** while libvirt can be used with several types of hypervisors, this +provider focuses on [KVM](http://libvirt.org/drvqemu.html). Other drivers may not be +working and haven't been tested. +## The connection URI + +The provider understands [connection URIs](https://libvirt.org/uri.html). The supported transports are: + +* `tcp` (non-encrypted connection) +* `unix` (UNIX domain socket) +* `tls` (See [here](https://libvirt.org/kbase/tlscerts.html) for information how to setup certificates) +* `ssh` (Secure shell) + +Unlike the original libvirt, the `ssh` transport is not implemented using the ssh command and therefore does not require `nc` (netcat) on the server side. + +Additionally, the `ssh` URI supports passwords using the `driver+ssh://[username:PASSWORD@][hostname][:port]/[path]?sshauth=ssh-password` syntax. + +As the provider does not use libvirt on the client side, not all connection URI options are supported or apply. +## Example Usage + +{{< chooser language "typescript,python,go,csharp,java,yaml" >}} +{{% choosable language typescript %}} +```yaml +# Pulumi.yaml provider configuration file +name: configuration-example +runtime: nodejs +config: + simple-provider:authUrl: + value: http://myauthurl:5000/v3 + simple-provider:password: + value: pwd + simple-provider:region: + value: RegionOne + simple-provider:tenantName: + value: admin + simple-provider:userName: + value: admin + +``` +```typescript +import * as pulumi from "@pulumi/pulumi"; +import * as simple from "@pulumi/simple"; + +//# Define a resource +const aResource = new simple.index.Resource("a_resource", { + inputOne: "hello", + inputTwo: true, +}); +``` +{{% /choosable %}} +{{% choosable language python %}} +```yaml +# Pulumi.yaml provider configuration file +name: configuration-example +runtime: python +config: + simple-provider:authUrl: + value: http://myauthurl:5000/v3 + simple-provider:password: + value: pwd + simple-provider:region: + value: RegionOne + simple-provider:tenantName: + value: admin + simple-provider:userName: + value: admin + +``` +```python +import pulumi +import pulumi_simple as simple + +## Define a resource +a_resource = simple.index.Resource("a_resource", + input_one=hello, + input_two=True) +``` +{{% /choosable %}} +{{% choosable language csharp %}} +```yaml +# Pulumi.yaml provider configuration file +name: configuration-example +runtime: dotnet +config: + simple-provider:authUrl: + value: http://myauthurl:5000/v3 + simple-provider:password: + value: pwd + simple-provider:region: + value: RegionOne + simple-provider:tenantName: + value: admin + simple-provider:userName: + value: admin + +``` +```csharp +using System.Collections.Generic; +using System.Linq; +using Pulumi; +using Simple = Pulumi.Simple; + +return await Deployment.RunAsync(() => +{ + //# Define a resource + var aResource = new Simple.Index.Resource("a_resource", new() + { + InputOne = "hello", + InputTwo = true, + }); + +}); + +``` +{{% /choosable %}} +{{% choosable language go %}} +```yaml +# Pulumi.yaml provider configuration file +name: configuration-example +runtime: go +config: + simple-provider:authUrl: + value: http://myauthurl:5000/v3 + simple-provider:password: + value: pwd + simple-provider:region: + value: RegionOne + simple-provider:tenantName: + value: admin + simple-provider:userName: + value: admin + +``` +```go +package main + +import ( + "github.com/pulumi/pulumi-simple/sdk/go/simple" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + // # Define a resource + _, err := simple.NewResource(ctx, "a_resource", &simple.ResourceArgs{ + InputOne: "hello", + InputTwo: true, + }) + if err != nil { + return err + } + return nil + }) +} +``` +{{% /choosable %}} +{{% choosable language yaml %}} +```yaml +# Pulumi.yaml provider configuration file +name: configuration-example +runtime: yaml +config: + simple-provider:authUrl: + value: http://myauthurl:5000/v3 + simple-provider:password: + value: pwd + simple-provider:region: + value: RegionOne + simple-provider:tenantName: + value: admin + simple-provider:userName: + value: admin + +``` +```yaml +resources: + ## Define a resource + aResource: + type: simple:resource + name: a_resource + properties: + inputOne: hello + inputTwo: true +``` +{{% /choosable %}} +{{% choosable language java %}} +```yaml +# Pulumi.yaml provider configuration file +name: configuration-example +runtime: java +config: + simple-provider:authUrl: + value: http://myauthurl:5000/v3 + simple-provider:password: + value: pwd + simple-provider:region: + value: RegionOne + simple-provider:tenantName: + value: admin + simple-provider:userName: + value: admin + +``` +```java +package generated_program; + +import com.pulumi.Context; +import com.pulumi.Pulumi; +import com.pulumi.core.Output; +import com.pulumi.simple.resource; +import com.pulumi.simple.ResourceArgs; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class App { + public static void main(String[] args) { + Pulumi.run(App::stack); + } + + public static void stack(Context ctx) { + //# Define a resource + var aResource = new Resource("aResource", ResourceArgs.builder() + .inputOne("hello") + .inputTwo(true) + .build()); + + } +} +``` +{{% /choosable %}} +{{< /chooser >}} +## Configuration Reference + +The following keys can be used to configure the provider. + +* `uri` - (Required) The [connection URI](https://libvirt.org/uri.html) used + to connect to the libvirt host. \ No newline at end of file diff --git a/pkg/tfgen/test_data/convert-index-file-with-table/input.md b/pkg/tfgen/test_data/convert-index-file-with-table/input.md new file mode 100644 index 000000000..4ad2f210e --- /dev/null +++ b/pkg/tfgen/test_data/convert-index-file-with-table/input.md @@ -0,0 +1,64 @@ +--- +layout: "libvirt" +page_title: "Provider: libvirt" +sidebar_current: "docs-libvirt-index" +description: |- + The Libvirt provider is used to interact with Linux KVM/libvirt hypervisors. The provider needs to be configured with the proper connection information before it can be used. +--- + +# Libvirt Provider + +The Libvirt provider is used to interact with Linux +[libvirt](https://libvirt.org) hypervisors. + +The provider needs to be configured with the proper connection information +before it can be used. + +|t1 | *t2* | +|------|------| +| **r1c1** | r1c2 | + +~> **Note:** while libvirt can be used with several types of hypervisors, this +provider focuses on [KVM](http://libvirt.org/drvqemu.html). Other drivers may not be +working and haven't been tested. + +## The connection URI + +The provider understands [connection URIs](https://libvirt.org/uri.html). The supported transports are: + +* `tcp` (non-encrypted connection) +* `unix` (UNIX domain socket) +* `tls` (See [here](https://libvirt.org/kbase/tlscerts.html) for information how to setup certificates) +* `ssh` (Secure shell) + +Unlike the original libvirt, the `ssh` transport is not implemented using the ssh command and therefore does not require `nc` (netcat) on the server side. + +Additionally, the `ssh` URI supports passwords using the `driver+ssh://[username:PASSWORD@][hostname][:port]/[path]?sshauth=ssh-password` syntax. + +As the provider does not use libvirt on the client side, not all connection URI options are supported or apply. + +## Example Usage + +```hcl +# Configure the OpenStack Provider +provider "simple-provider" { + user_name = "admin" + tenant_name = "admin" + password = "pwd" + auth_url = "http://myauthurl:5000/v3" + region = "RegionOne" +} +## Define a resource +resource "simple_resource" "a_resource" { + input_one = "hello" + input_two = true +} +``` + +## Configuration Reference + +The following keys can be used to configure the provider. + +* `uri` - (Required) The [connection URI](https://libvirt.org/uri.html) used + to connect to the libvirt host. + diff --git a/pkg/tfgen/test_data/remove-title/sdwan-expected.md b/pkg/tfgen/test_data/remove-title/sdwan-expected.md new file mode 100644 index 000000000..ef0fe69fc --- /dev/null +++ b/pkg/tfgen/test_data/remove-title/sdwan-expected.md @@ -0,0 +1,26 @@ +The SDWAN provider provides resources to interact with a Cisco Catalyst SD-WAN environment. It communicates with the SD-WAN Manager via the REST API. + +All resources and data sources have been tested with the following releases. + +| Platform | Version | +| --------------- | ------- | +| Catalyst SD-WAN | 20.9 | +| Catalyst SD-WAN | 20.12 | + +## Getting Started + +The following guides with examples exist to demonstrate the use of the provider: + +- [Getting Started](https://registry.terraform.io/providers/CiscoDevNet/sdwan/latest/docs/guides/getting_started) +- [Updating Templates](https://registry.terraform.io/providers/CiscoDevNet/sdwan/latest/docs/guides/updating_templates) +- [Configuration Groups](https://registry.terraform.io/providers/CiscoDevNet/sdwan/latest/docs/guides/configuration_groups) + +## Example Usage + +Here is some example + +## Schema + +### Optional + +Getting some stuff \ No newline at end of file diff --git a/pkg/tfgen/test_data/remove-title/sdwan-input.md b/pkg/tfgen/test_data/remove-title/sdwan-input.md new file mode 100644 index 000000000..76d2f25a2 --- /dev/null +++ b/pkg/tfgen/test_data/remove-title/sdwan-input.md @@ -0,0 +1,28 @@ +# SDWAN Provider + +The SDWAN provider provides resources to interact with a Cisco Catalyst SD-WAN environment. It communicates with the SD-WAN Manager via the REST API. + +All resources and data sources have been tested with the following releases. + +| Platform | Version | +| --------------- | ------- | +| Catalyst SD-WAN | 20.9 | +| Catalyst SD-WAN | 20.12 | + +## Getting Started + +The following guides with examples exist to demonstrate the use of the provider: + +- [Getting Started](https://registry.terraform.io/providers/CiscoDevNet/sdwan/latest/docs/guides/getting_started) +- [Updating Templates](https://registry.terraform.io/providers/CiscoDevNet/sdwan/latest/docs/guides/updating_templates) +- [Configuration Groups](https://registry.terraform.io/providers/CiscoDevNet/sdwan/latest/docs/guides/configuration_groups) + +## Example Usage + +Here is some example + +## Schema + +### Optional + +Getting some stuff \ No newline at end of file diff --git a/pkg/tfgen/test_data/table-rendering/in-middle-of-doc-expected.md b/pkg/tfgen/test_data/table-rendering/in-middle-of-doc-expected.md new file mode 100644 index 000000000..3ef8de224 --- /dev/null +++ b/pkg/tfgen/test_data/table-rendering/in-middle-of-doc-expected.md @@ -0,0 +1,10 @@ +## Here is a section + +| t1 | *t2* | +|----------|------| +| **r1c1** | r1c2 | + +this is also dropped +## Another Header + +With More Stuff! diff --git a/pkg/tfgen/test_data/table-rendering/in-middle-of-doc-input.md b/pkg/tfgen/test_data/table-rendering/in-middle-of-doc-input.md new file mode 100644 index 000000000..99497711e --- /dev/null +++ b/pkg/tfgen/test_data/table-rendering/in-middle-of-doc-input.md @@ -0,0 +1,11 @@ +## Here is a section + +|t1 | *t2* | +|----------|------| +| **r1c1** | r1c2 | + +this is also dropped + +## Another Header + +With More Stuff! \ No newline at end of file