pulumi/pkg/codegen/docs/examples.go

194 lines
6.0 KiB
Go

// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the
// goconst linter's warning.
//
//nolint:lll, goconst
package docs
import (
"strings"
"github.com/pgavlin/goldmark/ast"
"github.com/pulumi/pulumi/pkg/v3/codegen"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
const defaultMissingExampleSnippetPlaceholder = "Coming soon!"
type examplesSection struct {
// Examples is a list of exampleSections. Each exampleSection contains a title and code snippets
Examples []exampleSection
// LangChooserLanguages is a comma-separated list of languages to pass to the
// language chooser shortcode. Use this to customize the languages shown for a
// resource. By default, the language chooser will show all languages supported
// by Pulumi for all resources.
// Supported values are "typescript", "python", "go", "csharp", "java", "yaml"
LangChooserLanguages string
}
type exampleSection struct {
Title string
// Snippets is a map of language to its code snippet, if any.
Snippets map[string]string
}
type docInfo struct {
description string
examples []exampleSection
importDetails string
}
func (dctx *docGenContext) decomposeDocstring(docstring, supportedSnippetLanguages string) docInfo {
if docstring == "" {
return docInfo{}
}
if strings.Contains(docstring, beginCodeBlock) {
return dctx.processDescription(docstring, supportedSnippetLanguages)
}
languages := codegen.NewStringSet(dctx.snippetLanguages...)
source := []byte(docstring)
parsed := schema.ParseDocs(source)
var examplesShortcode *schema.Shortcode
var exampleShortcode *schema.Shortcode
var examples []exampleSection
currentSection := exampleSection{
Snippets: map[string]string{},
}
var nextTitle string
var nextInferredTitle string
// Push any examples we have found. Since `pushExamples` is called between sections,
// it needs to behave correctly when no examples were found.
pushExamples := func() {
if len(currentSection.Snippets) > 0 {
for _, l := range dctx.snippetLanguages {
if _, ok := currentSection.Snippets[l]; !ok {
currentSection.Snippets[l] = defaultMissingExampleSnippetPlaceholder
}
}
examples = append(examples, currentSection)
}
if nextTitle == "" {
nextTitle = nextInferredTitle
}
currentSection = exampleSection{
Snippets: map[string]string{},
Title: nextTitle,
}
nextTitle = ""
nextInferredTitle = ""
}
err := ast.Walk(parsed, func(n ast.Node, enter bool) (ast.WalkStatus, error) {
// ast.Walk visits each node twice. The first time descending and the second time
// ascending. We only want to view the nodes while descending, so we skip when
// `enter` is false.
if !enter {
return ast.WalkContinue, nil
}
if shortcode, ok := n.(*schema.Shortcode); ok {
name := string(shortcode.Name)
switch name {
case schema.ExamplesShortcode:
if examplesShortcode == nil {
examplesShortcode = shortcode
}
case schema.ExampleShortcode:
if exampleShortcode == nil {
exampleShortcode = shortcode
currentSection.Title, currentSection.Snippets = "", map[string]string{}
} else if !enter && shortcode == exampleShortcode {
pushExamples()
exampleShortcode = nil
}
}
return ast.WalkContinue, nil
}
// We check to make sure we are in an examples section.
if exampleShortcode == nil {
return ast.WalkContinue, nil
}
switch n := n.(type) {
case *ast.Heading:
if n.Level == 3 {
title := strings.TrimSpace(schema.RenderDocsToString(source, n))
if currentSection.Title == "" && len(currentSection.Snippets) == 0 {
currentSection.Title = title
} else {
nextTitle = title
}
}
return ast.WalkSkipChildren, nil
case *ast.FencedCodeBlock:
language := string(n.Language(source))
snippet := schema.RenderDocsToString(source, n)
if !languages.Has(language) || len(snippet) == 0 {
return ast.WalkContinue, nil
}
if _, ok := currentSection.Snippets[language]; ok {
// We have the same language appearing multiple times in a {{% examples
// %}} without an {{% example %}} to break them up. We are going to just
// pretend there was an {{% example %}}
pushExamples()
}
currentSection.Snippets[language] = snippet
case *ast.Text:
// We only want to change the title before we collect any snippets
title := strings.TrimSuffix(string(n.Text(source)), ":")
if currentSection.Title == "" && len(currentSection.Snippets) == 0 {
currentSection.Title = title
} else {
// Since we might find out we are done with the previous section only
// after we have consumed the next title, we store the title.
nextInferredTitle = title
}
}
return ast.WalkContinue, nil
})
contract.AssertNoErrorf(err, "error walking AST")
pushExamples()
if examplesShortcode != nil {
p := examplesShortcode.Parent()
p.RemoveChild(p, examplesShortcode)
}
description := schema.RenderDocsToString(source, parsed)
importDetails := ""
parts := strings.Split(description, "\n\n## Import")
if len(parts) > 1 { // we only care about the Import section details here!!
importDetails = parts[1]
}
// When we split the description above, the main part of the description is always part[0]
// the description must have a blank line after it to render the examples correctly
description = parts[0] + "\n"
return docInfo{
description: description,
examples: examples,
importDetails: importDetails,
}
}