// 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 ( "fmt" "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 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 string) docInfo { if docstring == "" { return docInfo{} } 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 = fmt.Sprintf("%s\n", parts[0]) return docInfo{ description: description, examples: examples, importDetails: importDetails, } }