mirror of
https://github.com/coder/coder.git
synced 2025-07-18 14:17:22 +00:00
feat: render Markdown in rich parameter descriptions (#6098)
This commit is contained in:
3
coderd/apidoc/docs.go
generated
3
coderd/apidoc/docs.go
generated
@ -7555,6 +7555,9 @@ const docTemplate = `{
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"description_plaintext": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
|
3
coderd/apidoc/swagger.json
generated
3
coderd/apidoc/swagger.json
generated
@ -6793,6 +6793,9 @@
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"description_plaintext": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
|
97
coderd/parameter/plaintext.go
Normal file
97
coderd/parameter/plaintext.go
Normal file
@ -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
coderd/parameter/plaintext_test.go
Normal file
49
coderd/parameter/plaintext_test.go
Normal file
@ -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.
|
||||
|
||||

|
||||
|
||||
**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)
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
@ -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
|
||||
)
|
||||
@ -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},
|
||||
|
Reference in New Issue
Block a user