diff --git a/internal/schemamd/render.go b/internal/schemamd/render.go index 8a72e636..b9b0a2eb 100644 --- a/internal/schemamd/render.go +++ b/internal/schemamd/render.go @@ -6,6 +6,7 @@ package schemamd import ( "fmt" "io" + "regexp" "slices" "sort" "strings" @@ -14,6 +15,11 @@ import ( "github.com/zclconf/go-cty/cty" ) +var ( + // multipleNewlinesRegexRender matches multiple consecutive newlines (2 or more) + multipleNewlinesRegexRender = regexp.MustCompile(`\n\n+`) +) + // Render writes a Markdown formatted Schema definition to the specified writer. // A Schema contains a Version and the root Block, for example: // @@ -154,6 +160,8 @@ func writeIdentityAttribute(w io.Writer, name string, attr *tfjson.IdentityAttri desc := strings.TrimSpace(attr.Description) if desc != "" { + // Collapse multiple newlines while keeping list formatting intact. + desc = multipleNewlinesRegexRender.ReplaceAllString(desc, " \n") _, err = io.WriteString(w, " "+desc) if err != nil { return err diff --git a/internal/schemamd/render_test.go b/internal/schemamd/render_test.go index 4c8f4374..a5ecda71 100644 --- a/internal/schemamd/render_test.go +++ b/internal/schemamd/render_test.go @@ -59,6 +59,12 @@ func TestRender(t *testing.T) { "testdata/deep_nested_write_only_attributes.schema.json", "testdata/deep_nested_write_only_attributes.md", }, + { + // Reference: https://github.com/hashicorp/terraform-plugin-docs/issues/531 + "multiline_descriptions", + "testdata/multiline_descriptions.schema.json", + "testdata/multiline_descriptions.md", + }, } { t.Run(c.name, func(t *testing.T) { t.Parallel() diff --git a/internal/schemamd/testdata/multiline_descriptions.md b/internal/schemamd/testdata/multiline_descriptions.md new file mode 100644 index 00000000..1f410435 --- /dev/null +++ b/internal/schemamd/testdata/multiline_descriptions.md @@ -0,0 +1,18 @@ +## Schema + +### Required + +- `long_description` (String) Line one. +Line two. +Line three. + +### Optional + +- `simple_description` (String) This is a simple single-line description. +- `two_line_description` (String) First paragraph. +Second paragraph with more details. + +### Read-Only + +- `id` (String) The ID of this resource. + diff --git a/internal/schemamd/testdata/multiline_descriptions.schema.json b/internal/schemamd/testdata/multiline_descriptions.schema.json new file mode 100644 index 00000000..e3789000 --- /dev/null +++ b/internal/schemamd/testdata/multiline_descriptions.schema.json @@ -0,0 +1,31 @@ +{ + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description_kind": "plain", + "computed": true + }, + "two_line_description": { + "type": "string", + "description": "First paragraph.\n\nSecond paragraph with more details.", + "description_kind": "markdown", + "optional": true + }, + "long_description": { + "type": "string", + "description": "Line one.\n\nLine two.\n\nLine three.", + "description_kind": "markdown", + "required": true + }, + "simple_description": { + "type": "string", + "description": "This is a simple single-line description.", + "description_kind": "markdown", + "optional": true + } + } + } +} + diff --git a/internal/schemamd/write_attribute_description.go b/internal/schemamd/write_attribute_description.go index 26db2a8c..acaeec6f 100644 --- a/internal/schemamd/write_attribute_description.go +++ b/internal/schemamd/write_attribute_description.go @@ -6,11 +6,17 @@ package schemamd import ( "fmt" "io" + "regexp" "strings" tfjson "github.com/hashicorp/terraform-json" ) +var ( + // multipleNewlinesRegex matches multiple consecutive newlines (2 or more) + multipleNewlinesRegex = regexp.MustCompile(`\n\n+`) +) + func WriteAttributeDescription(w io.Writer, att *tfjson.SchemaAttribute, includeRW bool) error { _, err := io.WriteString(w, "(") if err != nil { @@ -72,6 +78,8 @@ func WriteAttributeDescription(w io.Writer, att *tfjson.SchemaAttribute, include desc := strings.TrimSpace(att.Description) if desc != "" { + // Collapse multiple newlines while keeping list formatting intact. + desc = multipleNewlinesRegex.ReplaceAllString(desc, " \n") _, err = io.WriteString(w, " "+desc) if err != nil { return err diff --git a/internal/schemamd/write_attribute_description_test.go b/internal/schemamd/write_attribute_description_test.go index 34bade4e..00bb4be5 100644 --- a/internal/schemamd/write_attribute_description_test.go +++ b/internal/schemamd/write_attribute_description_test.go @@ -162,6 +162,32 @@ func TestWriteAttributeDescription(t *testing.T) { Description: "\n\t This is an attribute.\n\t ", }, }, + + // multiple consecutive newlines (issue #531) + { + "(String, Required) First paragraph. \nSecond paragraph with more details.", + &tfjson.SchemaAttribute{ + AttributeType: cty.String, + Required: true, + Description: "First paragraph.\n\nSecond paragraph with more details.", + }, + }, + { + "(String, Optional) Line one. \nLine two. \nLine three.", + &tfjson.SchemaAttribute{ + AttributeType: cty.String, + Optional: true, + Description: "Line one.\n\nLine two.\n\nLine three.", + }, + }, + { + "(String, Optional) Line one. \nLine two with more text.", + &tfjson.SchemaAttribute{ + AttributeType: cty.String, + Optional: true, + Description: "Line one.\n\n\nLine two with more text.", + }, + }, } { t.Run(c.expected, func(t *testing.T) { t.Parallel() diff --git a/internal/schemamd/write_block_type_description.go b/internal/schemamd/write_block_type_description.go index 31ca2782..f0673a91 100644 --- a/internal/schemamd/write_block_type_description.go +++ b/internal/schemamd/write_block_type_description.go @@ -6,11 +6,17 @@ package schemamd import ( "fmt" "io" + "regexp" "strings" tfjson "github.com/hashicorp/terraform-json" ) +var ( + // multipleNewlinesRegexBlock matches multiple consecutive newlines (2 or more) + multipleNewlinesRegexBlock = regexp.MustCompile(`\n\n+`) +) + func WriteBlockTypeDescription(w io.Writer, block *tfjson.SchemaBlockType) error { _, err := io.WriteString(w, "(Block") if err != nil { @@ -89,6 +95,8 @@ func WriteBlockTypeDescription(w io.Writer, block *tfjson.SchemaBlockType) error desc := strings.TrimSpace(block.Block.Description) if desc != "" { + // Collapse multiple newlines while keeping list formatting intact. + desc = multipleNewlinesRegexBlock.ReplaceAllString(desc, " \n") _, err = io.WriteString(w, " "+desc) if err != nil { return err diff --git a/internal/schemamd/write_block_type_description_test.go b/internal/schemamd/write_block_type_description_test.go index e6fa5023..bde76b0d 100644 --- a/internal/schemamd/write_block_type_description_test.go +++ b/internal/schemamd/write_block_type_description_test.go @@ -218,6 +218,26 @@ func TestWriteBlockTypeDescription(t *testing.T) { }, }, }, + + // multiple consecutive newlines (issue #531) + { + "(Block, Optional) First paragraph. \nSecond paragraph with more details.", + &tfjson.SchemaBlockType{ + NestingMode: tfjson.SchemaNestingModeSingle, + Block: &tfjson.SchemaBlock{ + Description: "First paragraph.\n\nSecond paragraph with more details.", + }, + }, + }, + { + "(Block List) Line one. \nLine two. \nLine three.", + &tfjson.SchemaBlockType{ + NestingMode: tfjson.SchemaNestingModeList, + Block: &tfjson.SchemaBlock{ + Description: "Line one.\n\nLine two.\n\nLine three.", + }, + }, + }, } { t.Run(c.expected, func(t *testing.T) { t.Parallel() diff --git a/internal/schemamd/write_nested_attribute_type_description.go b/internal/schemamd/write_nested_attribute_type_description.go index a45bdb1e..fea86925 100644 --- a/internal/schemamd/write_nested_attribute_type_description.go +++ b/internal/schemamd/write_nested_attribute_type_description.go @@ -6,11 +6,17 @@ package schemamd import ( "fmt" "io" + "regexp" "strings" tfjson "github.com/hashicorp/terraform-json" ) +var ( + // multipleNewlinesRegexNested matches multiple consecutive newlines (2 or more) + multipleNewlinesRegexNested = regexp.MustCompile(`\n\n+`) +) + func WriteNestedAttributeTypeDescription(w io.Writer, att *tfjson.SchemaAttribute, includeRW bool) error { nestedAttributeType := att.AttributeNestedType if nestedAttributeType == nil { @@ -111,6 +117,8 @@ func WriteNestedAttributeTypeDescription(w io.Writer, att *tfjson.SchemaAttribut desc := strings.TrimSpace(att.Description) if desc != "" { + // Collapse multiple newlines while keeping list formatting intact. + desc = multipleNewlinesRegexNested.ReplaceAllString(desc, " \n") _, err = io.WriteString(w, " "+desc) if err != nil { return err diff --git a/internal/schemamd/write_nested_attribute_type_description_test.go b/internal/schemamd/write_nested_attribute_type_description_test.go index 9b48ea10..3dae4901 100644 --- a/internal/schemamd/write_nested_attribute_type_description_test.go +++ b/internal/schemamd/write_nested_attribute_type_description_test.go @@ -108,6 +108,42 @@ func TestWriteNestedAttributeTypeDescription(t *testing.T) { }, }, }, + + // multiple consecutive newlines (issue #531) + { + "(Attributes, Optional) First paragraph. \nSecond paragraph with more details.", + &tfjson.SchemaAttribute{ + Description: "First paragraph.\n\nSecond paragraph with more details.", + AttributeNestedType: &tfjson.SchemaNestedAttributeType{ + NestingMode: tfjson.SchemaNestingModeSingle, + Attributes: map[string]*tfjson.SchemaAttribute{ + "foo": { + AttributeType: cty.String, + Required: true, + }, + }, + }, + Optional: true, + }, + }, + { + "(Attributes List, Min: 2, Max: 3) Line one. \nLine two. \nLine three.", + &tfjson.SchemaAttribute{ + Description: "Line one.\n\nLine two.\n\nLine three.", + AttributeNestedType: &tfjson.SchemaNestedAttributeType{ + NestingMode: tfjson.SchemaNestingModeList, + Attributes: map[string]*tfjson.SchemaAttribute{ + "foo": { + AttributeType: cty.String, + Required: true, + }, + }, + MinItems: 2, + MaxItems: 3, + }, + Required: true, + }, + }, } { t.Run(c.expected, func(t *testing.T) { t.Parallel()