renderer/asciidoctor.go (138 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 renderer import ( "fmt" "io/fs" "os" "strings" "text/template" "github.com/Masterminds/sprig" "github.com/elastic/crd-ref-docs/config" "github.com/elastic/crd-ref-docs/templates" "github.com/elastic/crd-ref-docs/types" ) const ( asciidocAnchorPrefix = "{anchor_prefix}-" ) type AsciidoctorRenderer struct { conf *config.Config *Functions } func NewAsciidoctorRenderer(conf *config.Config) (*AsciidoctorRenderer, error) { baseFuncs, err := NewFunctions(conf) if err != nil { return nil, err } return &AsciidoctorRenderer{conf: conf, Functions: baseFuncs}, nil } func (adr *AsciidoctorRenderer) Render(gvd []types.GroupVersionDetails) error { funcMap := combinedFuncMap(funcMap{prefix: "asciidoc", funcs: adr.ToFuncMap()}, funcMap{funcs: sprig.TxtFuncMap()}) var tpls fs.FS if adr.conf.TemplatesDir != "" { tpls = os.DirFS(adr.conf.TemplatesDir) } else { sub, err := fs.Sub(templates.Root, "asciidoctor") if err != nil { return err } tpls = sub } tmpl, err := loadTemplate(tpls, funcMap) if err != nil { return err } return renderTemplate(tmpl, adr.conf, "asciidoc", gvd) } func (adr *AsciidoctorRenderer) ToFuncMap() template.FuncMap { return template.FuncMap{ "GroupVersionID": adr.GroupVersionID, "RenderAnchorID": adr.RenderAnchorID, "RenderExternalLink": adr.RenderExternalLink, "RenderGVLink": adr.RenderGVLink, "RenderLocalLink": adr.RenderLocalLink, "RenderType": adr.RenderType, "RenderTypeLink": adr.RenderTypeLink, "SafeID": adr.SafeID, "ShouldRenderType": adr.ShouldRenderType, "TypeID": adr.TypeID, "RenderFieldDoc": adr.RenderFieldDoc, "RenderValidation": adr.RenderValidation, } } func (adr *AsciidoctorRenderer) ShouldRenderType(t *types.Type) bool { return t != nil && (t.GVK != nil || len(t.References) > 0) } func (adr *AsciidoctorRenderer) RenderType(t *types.Type) string { var sb strings.Builder switch t.Kind { case types.MapKind: sb.WriteString("object (") sb.WriteString("keys:") sb.WriteString(adr.RenderTypeLink(t.KeyType)) sb.WriteString(", values:") sb.WriteString(adr.RenderTypeLink(t.ValueType)) sb.WriteString(")") case types.SliceKind: sb.WriteString(adr.RenderTypeLink(t.UnderlyingType)) sb.WriteString(" array") default: sb.WriteString(adr.RenderTypeLink(t)) } return sb.String() } func (adr *AsciidoctorRenderer) RenderTypeLink(t *types.Type) string { text := adr.SimplifiedTypeName(t) link, local := adr.LinkForType(t) if link == "" { return text } if local { return adr.RenderLocalLink(asciidocAnchorPrefix, link, text) } else { return adr.RenderExternalLink(link, text) } } func (adr *AsciidoctorRenderer) RenderLocalLink(prefix, link, text string) string { return fmt.Sprintf("xref:%s%s[$$%s$$]", prefix, link, text) } func (adr *AsciidoctorRenderer) RenderExternalLink(link, text string) string { return fmt.Sprintf("link:%s[$$%s$$]", link, text) } func (adr *AsciidoctorRenderer) RenderGVLink(gv types.GroupVersionDetails) string { return adr.RenderLocalLink(asciidocAnchorPrefix, adr.GroupVersionID(gv), gv.GroupVersionString()) } func (adr *AsciidoctorRenderer) RenderAnchorID(id string) string { return fmt.Sprintf("%s%s", asciidocAnchorPrefix, adr.SafeID(id)) } func (adr *AsciidoctorRenderer) RenderFieldDoc(text string) string { // Escape the pipe character, which has special meaning for asciidoc as a way to format tables, // so that including | in a comment does not result in wonky tables. out := strings.ReplaceAll(text, "|", "\\|") // Trim any leading and trailing whitespace from each line. lines := strings.Split(out, "\n") for i := range lines { lines[i] = strings.TrimSpace(lines[i]) // Replace newlines with hard line breaks so that newlines are rendered as expected for non-empty lines. // See: https://docs.asciidoctor.org/asciidoc/latest/blocks/hard-line-breaks if lines[i] != "" { lines[i] = lines[i] + " +" } } return strings.Join(lines, "\n") } func (adr *AsciidoctorRenderer) RenderValidation(text string) string { renderedText := escapeFirstAsterixInEachPair(text) return escapeCurlyBraces(renderedText) } // escapeFirstAsterixInEachPair escapes the first asterix in each pair of // asterixes in text. E.g. "*a*b*c*" -> "\*a*b\*c*" and "*a*b*" -> "\*a*b*". func escapeFirstAsterixInEachPair(text string) string { index := -1 for i := 0; i < len(text); i++ { if text[i] == '*' { if index >= 0 { text = text[:index] + "\\" + text[index:] index = -1 i++ } else { index = i } } } return text } // escapeCurlyBraces ensures sufficient escapes are added to curly braces, so they are not mistaken // for asciidoctor id attributes. func escapeCurlyBraces(text string) string { // Per asciidoctor docs, only the leading curly brace needs to be escaped. return strings.Replace(text, "{", "\\{", -1) }