package schema import ( "bytes" "fmt" "io" "net/url" "github.com/pgavlin/goldmark/ast" "github.com/pgavlin/goldmark/renderer" "github.com/pgavlin/goldmark/renderer/markdown" "github.com/pgavlin/goldmark/util" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) // A RendererOption controls the behavior of a Renderer. type RendererOption func(*Renderer) // A ReferenceRenderer is responsible for rendering references to entities in a schema. type ReferenceRenderer func(r *Renderer, w io.Writer, source []byte, link *ast.Link, enter bool) (ast.WalkStatus, error) // WithReferenceRenderer sets the reference renderer for a renderer. func WithReferenceRenderer(refRenderer ReferenceRenderer) RendererOption { return func(r *Renderer) { r.refRenderer = refRenderer } } // A Renderer provides the ability to render parsed documentation back to Markdown source. type Renderer struct { md *markdown.Renderer refRenderer ReferenceRenderer } // MarkdownRenderer returns the underlying Markdown renderer used by the Renderer. func (r *Renderer) MarkdownRenderer() *markdown.Renderer { return r.md } func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { // blocks reg.Register(KindShortcode, r.renderShortcode) // inlines reg.Register(ast.KindLink, r.renderLink) } func (r *Renderer) renderShortcode(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { if enter { if err := r.md.OpenBlock(w, source, node); err != nil { return ast.WalkStop, err } if _, err := fmt.Fprintf(r.md.Writer(w), "{{%% %s %%}}\n", string(node.(*Shortcode).Name)); err != nil { return ast.WalkStop, err } } else { if _, err := fmt.Fprintf(r.md.Writer(w), "{{%% /%s %%}}\n", string(node.(*Shortcode).Name)); err != nil { return ast.WalkStop, err } if err := r.md.CloseBlock(w); err != nil { return ast.WalkStop, err } } return ast.WalkContinue, nil } func isEntityReference(dest []byte) bool { if len(dest) == 0 { return false } parsed, err := url.Parse(string(dest)) if err != nil { return false } if parsed.IsAbs() { return parsed.Scheme == "schema" } return parsed.Host == "" && parsed.Path == "" && parsed.RawQuery == "" && parsed.Fragment != "" } func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, enter bool) (ast.WalkStatus, error) { // If this is an entity reference, pass it off to the reference renderer (if any). link := node.(*ast.Link) if r.refRenderer != nil && isEntityReference(link.Destination) { return r.refRenderer(r, w, source, link, enter) } return r.md.RenderLink(w, source, node, enter) } // RenderDocs renders parsed documentation to the given Writer. The source that was used to parse the documentation // must be provided. func RenderDocs(w io.Writer, source []byte, node ast.Node, options ...RendererOption) error { md := &markdown.Renderer{} dr := &Renderer{md: md} for _, o := range options { o(dr) } nodeRenderers := []util.PrioritizedValue{ util.Prioritized(dr, 100), util.Prioritized(md, 200), } r := renderer.NewRenderer(renderer.WithNodeRenderers(nodeRenderers...)) return r.Render(w, source, node) } // RenderDocsToString is like RenderDocs, but renders to a string instead of a Writer. func RenderDocsToString(source []byte, node ast.Node, options ...RendererOption) string { var buf bytes.Buffer err := RenderDocs(&buf, source, node, options...) contract.AssertNoErrorf(err, "error rendering docs") return buf.String() }