github.com/phsym/gomarkdoc@v0.5.4/format/formatcore/base.go (about) 1 package formatcore 2 3 import ( 4 "errors" 5 "fmt" 6 "regexp" 7 "strings" 8 9 "github.com/russross/blackfriday/v2" 10 "mvdan.cc/xurls/v2" 11 ) 12 13 // Bold converts the provided text to bold 14 func Bold(text string) string { 15 if text == "" { 16 return "" 17 } 18 19 return fmt.Sprintf("**%s**", Escape(text)) 20 } 21 22 // CodeBlock wraps the provided code as a code block. Language syntax 23 // highlighting is not supported. 24 func CodeBlock(code string) string { 25 var builder strings.Builder 26 27 lines := strings.Split(code, "\n") 28 for i, line := range lines { 29 if i != 0 { 30 builder.WriteRune('\n') 31 } 32 33 builder.WriteRune('\t') 34 builder.WriteString(line) 35 } 36 37 builder.WriteString("\n\n") 38 39 return builder.String() 40 } 41 42 // GFMCodeBlock wraps the provided code as a code block and tags it with the 43 // provided language (or no language if the empty string is provided), using 44 // the triple backtick format from GitHub Flavored Markdown. 45 func GFMCodeBlock(language, code string) string { 46 return fmt.Sprintf("```%s\n%s\n```\n\n", language, strings.Trim(code, "\n")) 47 } 48 49 // Header converts the provided text into a header of the provided level. The 50 // level is expected to be at least 1. 51 func Header(level int, text string) (string, error) { 52 if level < 1 { 53 return "", errors.New("format: header level cannot be less than 1") 54 } 55 56 switch level { 57 case 1: 58 return fmt.Sprintf("# %s\n\n", text), nil 59 case 2: 60 return fmt.Sprintf("## %s\n\n", text), nil 61 case 3: 62 return fmt.Sprintf("### %s\n\n", text), nil 63 case 4: 64 return fmt.Sprintf("#### %s\n\n", text), nil 65 case 5: 66 return fmt.Sprintf("##### %s\n\n", text), nil 67 default: 68 // Only go up to 6 levels. Anything higher is also level 6 69 return fmt.Sprintf("###### %s\n\n", text), nil 70 } 71 } 72 73 // Link generates a link with the given text and href values. 74 func Link(text, href string) string { 75 if text == "" { 76 return "" 77 } 78 79 if href == "" { 80 return text 81 } 82 83 return fmt.Sprintf("[%s](<%s>)", text, href) 84 } 85 86 // ListEntry generates an unordered list entry with the provided text at the 87 // provided zero-indexed depth. A depth of 0 is considered the topmost level of 88 // list. 89 func ListEntry(depth int, text string) string { 90 // TODO: this is a weird special case 91 if text == "" { 92 return "" 93 } 94 95 prefix := strings.Repeat(" ", depth) 96 return fmt.Sprintf("%s- %s\n", prefix, text) 97 } 98 99 // GFMAccordion generates a collapsible content. The accordion's visible title 100 // while collapsed is the provided title and the expanded content is the body. 101 func GFMAccordion(title, body string) string { 102 return fmt.Sprintf("<details><summary>%s</summary>\n<p>\n\n%s</p>\n</details>\n\n", title, Escape(body)) 103 } 104 105 // GFMAccordionHeader generates the header visible when an accordion is 106 // collapsed. 107 // 108 // The GFMAccordionHeader is expected to be used in conjunction with 109 // GFMAccordionTerminator() when the demands of the body's rendering requires 110 // it to be generated independently. The result looks conceptually like the 111 // following: 112 // 113 // accordion := GFMAccordionHeader("Accordion Title") + "Accordion Body" + GFMAccordionTerminator() 114 func GFMAccordionHeader(title string) string { 115 return fmt.Sprintf("<details><summary>%s</summary>\n<p>\n\n", title) 116 } 117 118 // GFMAccordionTerminator generates the code necessary to terminate an 119 // accordion after the body. It is expected to be used in conjunction with 120 // GFMAccordionHeader(). See GFMAccordionHeader for a full description. 121 func GFMAccordionTerminator() string { 122 return "</p>\n</details>\n\n" 123 } 124 125 // Paragraph formats a paragraph with the provided text as the contents. 126 func Paragraph(text string) string { 127 return fmt.Sprintf("%s\n\n", text) 128 } 129 130 var ( 131 specialCharacterRegex = regexp.MustCompile("([\\\\`*_{}\\[\\]()<>#+\\-!~])") 132 urlRegex = xurls.Strict() // Require a scheme in URLs 133 ) 134 135 // Escape escapes the special characters in the provided text, but leaves URLs 136 // found intact. Note that the URLs included must begin with a scheme to skip 137 // the escaping. 138 func Escape(text string) string { 139 b := []byte(text) 140 141 var ( 142 cursor int 143 builder strings.Builder 144 ) 145 146 for _, urlLoc := range urlRegex.FindAllIndex(b, -1) { 147 // Walk through each found URL, escaping the text before the URL and 148 // leaving the text in the URL unchanged. 149 if urlLoc[0] > cursor { 150 // Escape the previous section if its length is nonzero 151 builder.Write(escapeRaw(b[cursor:urlLoc[0]])) 152 } 153 154 // Add the unescaped URL to the end of it 155 builder.Write(b[urlLoc[0]:urlLoc[1]]) 156 157 // Move the cursor forward for the next iteration 158 cursor = urlLoc[1] 159 } 160 161 // Escape the end of the string after the last URL if there's anything left 162 if len(b) > cursor { 163 builder.Write(escapeRaw(b[cursor:])) 164 } 165 166 return builder.String() 167 } 168 169 func escapeRaw(segment []byte) []byte { 170 return specialCharacterRegex.ReplaceAll(segment, []byte("\\$1")) 171 } 172 173 // PlainText converts a markdown string to the plain text that appears in the 174 // rendered output. 175 func PlainText(text string) string { 176 md := blackfriday.New(blackfriday.WithExtensions(blackfriday.CommonExtensions)) 177 node := md.Parse([]byte(text)) 178 179 var builder strings.Builder 180 plainTextInner(node, &builder) 181 182 return builder.String() 183 } 184 185 func plainTextInner(node *blackfriday.Node, builder *strings.Builder) { 186 // Only text nodes produce output 187 if node.Type == blackfriday.Text { 188 builder.Write(node.Literal) 189 } 190 191 // Run the children first 192 if node.FirstChild != nil { 193 plainTextInner(node.FirstChild, builder) 194 } 195 196 // Then run any other siblings 197 if node.Next != nil { 198 // Add extra space if necessary between nodes 199 if node.Type == blackfriday.Paragraph || 200 node.Type == blackfriday.CodeBlock || 201 node.Type == blackfriday.Heading { 202 builder.WriteRune(' ') 203 } 204 205 plainTextInner(node.Next, builder) 206 } 207 }