Skip to content

feat: render Markdown in rich parameter descriptions #6098

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cli/cliui/parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ func ParameterSchema(cmd *cobra.Command, parameterSchema codersdk.ParameterSchem

func RichParameter(cmd *cobra.Command, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), Styles.Bold.Render(templateVersionParameter.Name))
if templateVersionParameter.Description != "" {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.Description, "\n"), "\n "))+"\n")
if templateVersionParameter.DescriptionPlaintext != "" {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
}

var err error
Expand Down
3 changes: 3 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 97 additions & 0 deletions coderd/parameter/plaintext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package parameter

import (
"strings"

"github.com/charmbracelet/glamour"
"github.com/charmbracelet/glamour/ansi"
"golang.org/x/xerrors"
)

var plaintextStyle = ansi.StyleConfig{
Document: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
BlockQuote: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
Paragraph: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
List: ansi.StyleList{
StyleBlock: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
LevelIndent: 4,
},
Heading: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
H1: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
H2: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
H3: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
H4: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
H5: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
H6: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
Strikethrough: ansi.StylePrimitive{},
Emph: ansi.StylePrimitive{},
Strong: ansi.StylePrimitive{},
HorizontalRule: ansi.StylePrimitive{},
Item: ansi.StylePrimitive{},
Enumeration: ansi.StylePrimitive{
BlockPrefix: ". ",
}, Task: ansi.StyleTask{},
Link: ansi.StylePrimitive{
Format: "({{.text}})",
},
LinkText: ansi.StylePrimitive{
Format: "{{.text}}",
},
ImageText: ansi.StylePrimitive{
Format: "{{.text}}",
},
Image: ansi.StylePrimitive{
Format: "({{.text}})",
},
Code: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
CodeBlock: ansi.StyleCodeBlock{
StyleBlock: ansi.StyleBlock{},
},
Table: ansi.StyleTable{},
DefinitionDescription: ansi.StylePrimitive{},
}

// Plaintext function converts the description with optional Markdown tags
// to the plaintext form.
func Plaintext(markdown string) (string, error) {
renderer, err := glamour.NewTermRenderer(
glamour.WithStandardStyle("ascii"),
glamour.WithWordWrap(0), // don't need to add spaces in the end of line
glamour.WithStyles(plaintextStyle),
)
if err != nil {
return "", xerrors.Errorf("can't initialize the Markdown renderer: %w", err)
}

output, err := renderer.Render(markdown)
if err != nil {
return "", xerrors.Errorf("can't render description to plaintext: %w", err)
}
defer renderer.Close()

return strings.TrimSpace(output), nil
}
49 changes: 49 additions & 0 deletions coderd/parameter/plaintext_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package parameter_test

import (
"testing"

"github.com/coder/coder/coderd/parameter"

"github.com/stretchr/testify/require"
)

func TestPlaintext(t *testing.T) {
t.Parallel()
t.Run("Simple", func(t *testing.T) {
t.Parallel()

mdDescription := `# Provide the machine image
See the [registry](https://container.registry.blah/namespace) for options.
![Minion](https://octodex.github.com/images/minion.png)
**This is bold text.**
__This is bold text.__
*This is italic text.*
> Blockquotes can also be nested.
~~Strikethrough.~~
1. Lorem ipsum dolor sit amet.
2. Consectetur adipiscing elit.
3. Integer molestie lorem at massa.
` + "`There are also code tags!`"

expected := "Provide the machine image\nSee the registry (https://container.registry.blah/namespace) for options.\n\nMinion (https://octodex.github.com/images/minion.png)\n\nThis is bold text.\nThis is bold text.\nThis is italic text.\n\nBlockquotes can also be nested.\nStrikethrough.\n\n1. Lorem ipsum dolor sit amet.\n2. Consectetur adipiscing elit.\n3. Integer molestie lorem at massa.\n\nThere are also code tags!"

stripped, err := parameter.Plaintext(mdDescription)
require.NoError(t, err)
require.Equal(t, expected, stripped)
})

t.Run("Nothing changes", func(t *testing.T) {
t.Parallel()

nothingChanges := "This is a simple description, so nothing changes."

stripped, err := parameter.Plaintext(nothingChanges)
require.NoError(t, err)
require.Equal(t, nothingChanges, stripped)
})
}
30 changes: 18 additions & 12 deletions coderd/templateversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -1458,19 +1458,25 @@ func convertTemplateVersionParameter(param database.TemplateVersionParameter) (c
Icon: option.Icon,
})
}

descriptionPlaintext, err := parameter.Plaintext(param.Description)
if err != nil {
return codersdk.TemplateVersionParameter{}, err
}
return codersdk.TemplateVersionParameter{
Name: param.Name,
Description: param.Description,
Type: param.Type,
Mutable: param.Mutable,
DefaultValue: param.DefaultValue,
Icon: param.Icon,
Options: options,
ValidationRegex: param.ValidationRegex,
ValidationMin: param.ValidationMin,
ValidationMax: param.ValidationMax,
ValidationError: param.ValidationError,
ValidationMonotonic: codersdk.ValidationMonotonicOrder(param.ValidationMonotonic),
Name: param.Name,
Description: param.Description,
DescriptionPlaintext: descriptionPlaintext,
Type: param.Type,
Mutable: param.Mutable,
DefaultValue: param.DefaultValue,
Icon: param.Icon,
Options: options,
ValidationRegex: param.ValidationRegex,
ValidationMin: param.ValidationMin,
ValidationMax: param.ValidationMax,
ValidationError: param.ValidationError,
ValidationMonotonic: codersdk.ValidationMonotonicOrder(param.ValidationMonotonic),
}, nil
}

Expand Down
27 changes: 18 additions & 9 deletions coderd/workspaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/coder/coder/coderd/autobuild/schedule"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/parameter"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
Expand Down Expand Up @@ -1788,12 +1789,12 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
const (
firstParameterName = "first_parameter"
firstParameterType = "string"
firstParameterDescription = "This is first parameter"
firstParameterDescription = "This is _first_ *parameter*"
firstParameterValue = "1"

secondParameterName = "second_parameter"
secondParameterType = "number"
secondParameterDescription = "This is second parameter"
secondParameterDescription = "_This_ is second *parameter*"
secondParameterValue = "2"
secondParameterValidationMonotonic = codersdk.MonotonicOrderIncreasing
)
Expand Down Expand Up @@ -1835,16 +1836,24 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

firstParameterDescriptionPlaintext, err := parameter.Plaintext(firstParameterDescription)
require.NoError(t, err)
secondParameterDescriptionPlaintext, err := parameter.Plaintext(secondParameterDescription)
require.NoError(t, err)

templateRichParameters, err := client.TemplateVersionRichParameters(ctx, version.ID)
require.NoError(t, err)
require.Len(t, templateRichParameters, 2)
require.Equal(t, templateRichParameters[0].Name, firstParameterName)
require.Equal(t, templateRichParameters[0].Type, firstParameterType)
require.Equal(t, templateRichParameters[0].ValidationMonotonic, codersdk.ValidationMonotonicOrder("")) // no validation for string

require.Equal(t, templateRichParameters[1].Name, secondParameterName)
require.Equal(t, templateRichParameters[1].Type, secondParameterType)
require.Equal(t, templateRichParameters[1].ValidationMonotonic, secondParameterValidationMonotonic)
require.Equal(t, firstParameterName, templateRichParameters[0].Name)
require.Equal(t, firstParameterType, templateRichParameters[0].Type)
require.Equal(t, firstParameterDescription, templateRichParameters[0].Description)
require.Equal(t, firstParameterDescriptionPlaintext, templateRichParameters[0].DescriptionPlaintext)
require.Equal(t, codersdk.ValidationMonotonicOrder(""), templateRichParameters[0].ValidationMonotonic) // no validation for string
require.Equal(t, secondParameterName, templateRichParameters[1].Name)
require.Equal(t, secondParameterType, templateRichParameters[1].Type)
require.Equal(t, secondParameterDescription, templateRichParameters[1].Description)
require.Equal(t, secondParameterDescriptionPlaintext, templateRichParameters[1].DescriptionPlaintext)
require.Equal(t, secondParameterValidationMonotonic, templateRichParameters[1].ValidationMonotonic)

expectedBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: firstParameterName, Value: firstParameterValue},
Expand Down
25 changes: 13 additions & 12 deletions codersdk/templateversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,19 @@ const (

// TemplateVersionParameter represents a parameter for a template version.
type TemplateVersionParameter struct {
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type" enums:"string,number,bool"`
Mutable bool `json:"mutable"`
DefaultValue string `json:"default_value"`
Icon string `json:"icon"`
Options []TemplateVersionParameterOption `json:"options"`
ValidationError string `json:"validation_error,omitempty"`
ValidationRegex string `json:"validation_regex,omitempty"`
ValidationMin int32 `json:"validation_min,omitempty"`
ValidationMax int32 `json:"validation_max,omitempty"`
ValidationMonotonic ValidationMonotonicOrder `json:"validation_monotonic,omitempty" enums:"increasing,decreasing"`
Name string `json:"name"`
Description string `json:"description"`
DescriptionPlaintext string `json:"description_plaintext"`
Type string `json:"type" enums:"string,number,bool"`
Mutable bool `json:"mutable"`
DefaultValue string `json:"default_value"`
Icon string `json:"icon"`
Options []TemplateVersionParameterOption `json:"options"`
ValidationError string `json:"validation_error,omitempty"`
ValidationRegex string `json:"validation_regex,omitempty"`
ValidationMin int32 `json:"validation_min,omitempty"`
ValidationMax int32 `json:"validation_max,omitempty"`
ValidationMonotonic ValidationMonotonicOrder `json:"validation_monotonic,omitempty" enums:"increasing,decreasing"`
}

// TemplateVersionParameterOption represents a selectable option for a template parameter.
Expand Down
30 changes: 16 additions & 14 deletions docs/api/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -4505,6 +4505,7 @@ Parameter represents a set value for the scope.
{
"default_value": "string",
"description": "string",
"description_plaintext": "string",
"icon": "string",
"mutable": true,
"name": "string",
Expand All @@ -4527,20 +4528,21 @@ Parameter represents a set value for the scope.

### Properties

| Name | Type | Required | Restrictions | Description |
| ---------------------- | ------------------------------------------------------------------------------------------- | -------- | ------------ | ----------- |
| `default_value` | string | false | | |
| `description` | string | false | | |
| `icon` | string | false | | |
| `mutable` | boolean | false | | |
| `name` | string | false | | |
| `options` | array of [codersdk.TemplateVersionParameterOption](#codersdktemplateversionparameteroption) | false | | |
| `type` | string | false | | |
| `validation_error` | string | false | | |
| `validation_max` | integer | false | | |
| `validation_min` | integer | false | | |
| `validation_monotonic` | [codersdk.ValidationMonotonicOrder](#codersdkvalidationmonotonicorder) | false | | |
| `validation_regex` | string | false | | |
| Name | Type | Required | Restrictions | Description |
| ----------------------- | ------------------------------------------------------------------------------------------- | -------- | ------------ | ----------- |
| `default_value` | string | false | | |
| `description` | string | false | | |
| `description_plaintext` | string | false | | |
| `icon` | string | false | | |
| `mutable` | boolean | false | | |
| `name` | string | false | | |
| `options` | array of [codersdk.TemplateVersionParameterOption](#codersdktemplateversionparameteroption) | false | | |
| `type` | string | false | | |
| `validation_error` | string | false | | |
| `validation_max` | integer | false | | |
| `validation_min` | integer | false | | |
| `validation_monotonic` | [codersdk.ValidationMonotonicOrder](#codersdkvalidationmonotonicorder) | false | | |
| `validation_regex` | string | false | | |

#### Enumerated Values

Expand Down
Loading