pulumi/pkg/codegen/schema/docs_parser.go

193 lines
5.0 KiB
Go

// Copyright 2020-2024, 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.
package schema
import (
"bytes"
"io"
"unicode"
"unicode/utf8"
"github.com/pgavlin/goldmark"
"github.com/pgavlin/goldmark/ast"
"github.com/pgavlin/goldmark/parser"
"github.com/pgavlin/goldmark/text"
"github.com/pgavlin/goldmark/util"
)
const (
// ExamplesShortcode is the name for the `{{% examples %}}` shortcode, which demarcates a set of example sections.
ExamplesShortcode = "examples"
// ExampleShortcode is the name for the `{{% example %}}` shortcode, which demarcates the content for a single
// example.
ExampleShortcode = "example"
)
// Shortcode represents a shortcode element and its contents, e.g. `{{% examples %}}`.
type Shortcode struct {
ast.BaseBlock
// Name is the name of the shortcode.
Name []byte
}
func (s *Shortcode) Dump(w io.Writer, source []byte, level int) {
m := map[string]string{
"Name": string(s.Name),
}
ast.DumpHelper(w, s, source, level, m, nil)
}
// KindShortcode is an ast.NodeKind for the Shortcode node.
var KindShortcode = ast.NewNodeKind("Shortcode")
// Kind implements ast.Node.Kind.
func (*Shortcode) Kind() ast.NodeKind {
return KindShortcode
}
// NewShortcode creates a new shortcode with the given name.
func NewShortcode(name []byte) *Shortcode {
return &Shortcode{Name: name}
}
type shortcodeParser int
// NewShortcodeParser returns a BlockParser that parses shortcode (e.g. `{{% examples %}}`).
func NewShortcodeParser() parser.BlockParser {
return shortcodeParser(0)
}
func (shortcodeParser) Trigger() []byte {
return []byte{'{'}
}
func (shortcodeParser) parseShortcode(line []byte, pos int) (int, int, int, bool, bool) {
// Look for `{{%` to open the shortcode.
text := line[pos:]
if len(text) < 3 || text[0] != '{' || text[1] != '{' || text[2] != '%' {
return 0, 0, 0, false, false
}
text, pos = text[3:], pos+3
// Scan through whitespace.
for {
if len(text) == 0 {
return 0, 0, 0, false, false
}
r, sz := utf8.DecodeRune(text)
if !unicode.IsSpace(r) {
break
}
text, pos = text[sz:], pos+sz
}
// Check for a '/' to indicate that this is a closing shortcode.
isClose := false
if text[0] == '/' {
isClose = true
text, pos = text[1:], pos+1
}
// Find the end of the name and the closing delimiter (`%}}`) for this shortcode.
nameStart, nameEnd, inName := pos, pos, true
for {
if len(text) == 0 {
return 0, 0, 0, false, false
}
if len(text) >= 3 && text[0] == '%' && text[1] == '}' && text[2] == '}' {
if inName {
nameEnd = pos
}
pos = pos + 3
// We don't need to update text
// because we return after this break.
break
}
r, sz := utf8.DecodeRune(text)
if inName && unicode.IsSpace(r) {
nameEnd, inName = pos, false
}
text, pos = text[sz:], pos+sz
}
return nameStart, nameEnd, pos, isClose, true
}
func (p shortcodeParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) {
line, _ := reader.PeekLine()
pos := pc.BlockOffset()
if pos < 0 {
return nil, parser.NoChildren
}
nameStart, nameEnd, shortcodeEnd, isClose, ok := p.parseShortcode(line, pos)
if !ok || isClose {
return nil, parser.NoChildren
}
name := line[nameStart:nameEnd]
reader.Advance(shortcodeEnd)
return NewShortcode(name), parser.HasChildren
}
func (p shortcodeParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State {
line, seg := reader.PeekLine()
pos := pc.BlockOffset()
if pos < 0 {
return parser.Continue | parser.HasChildren
} else if pos > seg.Len() {
return parser.Continue | parser.HasChildren
}
nameStart, nameEnd, shortcodeEnd, isClose, ok := p.parseShortcode(line, pos)
if !ok || !isClose {
return parser.Continue | parser.HasChildren
}
shortcode := node.(*Shortcode)
if !bytes.Equal(line[nameStart:nameEnd], shortcode.Name) {
return parser.Continue | parser.HasChildren
}
reader.Advance(shortcodeEnd)
return parser.Close
}
func (shortcodeParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {
}
// CanInterruptParagraph returns true for shortcodes.
func (shortcodeParser) CanInterruptParagraph() bool {
return true
}
// CanAcceptIndentedLine returns false for shortcodes; all shortcodes must start at the first column.
func (shortcodeParser) CanAcceptIndentedLine() bool {
return false
}
// ParseDocs parses the given documentation text as Markdown with shortcodes and returns the AST.
func ParseDocs(docs []byte) ast.Node {
p := goldmark.DefaultParser()
p.AddOptions(parser.WithBlockParsers(util.Prioritized(shortcodeParser(0), 50)))
return p.Parse(text.NewReader(docs))
}