feat: articles format option with markdown new default
This commit is contained in:
1 parent
68401d66e0
commit
7e9bd9ba30
61 files changed
+2269
-243
No files matched your search
@@ -0,0 +1,365 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
"golang.org/x/net/html/atom"
|
||||
)
|
||||
|
||||
const (
|
||||
markBold = "bold"
|
||||
markItalic = "italic"
|
||||
markLink = "link"
|
||||
markCode = "code"
|
||||
|
||||
codeLanguagePrefix = "language-"
|
||||
)
|
||||
|
||||
type mark struct {
|
||||
kind string
|
||||
href string
|
||||
}
|
||||
|
||||
type inline struct {
|
||||
text string
|
||||
marks []mark
|
||||
}
|
||||
|
||||
type blockKind int
|
||||
|
||||
const (
|
||||
kindHeading blockKind = iota
|
||||
kindParagraph
|
||||
kindList
|
||||
kindBlockquote
|
||||
kindCode
|
||||
kindRule
|
||||
kindImage
|
||||
)
|
||||
|
||||
type block struct {
|
||||
kind blockKind
|
||||
level int
|
||||
inlines []inline
|
||||
ordered bool
|
||||
items [][]block
|
||||
children []block
|
||||
language string
|
||||
text string
|
||||
src string
|
||||
alt string
|
||||
align string
|
||||
}
|
||||
|
||||
var transparentBlocks = map[string]bool{
|
||||
"div": true, "section": true, "article": true, "header": true, "footer": true,
|
||||
"main": true, "aside": true, "nav": true, "figure": true, "figcaption": true,
|
||||
"table": true, "thead": true, "tbody": true, "tfoot": true, "tr": true,
|
||||
"td": true, "th": true, "dl": true, "dt": true, "dd": true, "details": true,
|
||||
"summary": true, "li": true,
|
||||
}
|
||||
|
||||
var skippedElements = map[string]bool{
|
||||
"script": true, "style": true, "template": true, "head": true,
|
||||
"noscript": true, "iframe": true, "object": true, "embed": true,
|
||||
}
|
||||
|
||||
var alignments = map[string]bool{"center": true, "right": true, "justify": true}
|
||||
|
||||
var allowedLinkSchemes = map[string]bool{
|
||||
"": true, "http": true, "https": true, "mailto": true, "tel": true,
|
||||
}
|
||||
|
||||
func parseBlocks(source string) ([]block, error) {
|
||||
context := &html.Node{Type: html.ElementNode, DataAtom: atom.Body, Data: "body"}
|
||||
nodes, err := html.ParseFragment(strings.NewReader(source), context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
collector := newCollector(false, paragraphBlock)
|
||||
collector.walkAll(nodes, nil)
|
||||
return collector.finish(false), nil
|
||||
}
|
||||
|
||||
func paragraphBlock(inlines []inline) block {
|
||||
return block{kind: kindParagraph, inlines: inlines}
|
||||
}
|
||||
|
||||
func headingBlock(level int) func([]inline) block {
|
||||
return func(inlines []inline) block {
|
||||
return block{kind: kindHeading, level: level, inlines: inlines}
|
||||
}
|
||||
}
|
||||
|
||||
type collector struct {
|
||||
out []block
|
||||
current []inline
|
||||
keepBlank bool
|
||||
build func([]inline) block
|
||||
}
|
||||
|
||||
func newCollector(keepBlank bool, build func([]inline) block) *collector {
|
||||
return &collector{keepBlank: keepBlank, build: build}
|
||||
}
|
||||
|
||||
func (c *collector) walkAll(nodes []*html.Node, marks []mark) {
|
||||
for _, node := range nodes {
|
||||
c.walk(node, marks)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *collector) walkChildren(parent *html.Node, marks []mark) {
|
||||
for child := parent.FirstChild; child != nil; child = child.NextSibling {
|
||||
c.walk(child, marks)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *collector) addText(text string, marks []mark) {
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
c.current = appendInline(c.current, inline{text: text, marks: marks})
|
||||
}
|
||||
|
||||
func (c *collector) flush() {
|
||||
if len(c.current) == 0 {
|
||||
return
|
||||
}
|
||||
if c.keepBlank || hasVisibleText(c.current) {
|
||||
c.out = append(c.out, c.build(c.current))
|
||||
}
|
||||
c.current = nil
|
||||
}
|
||||
|
||||
func (c *collector) finish(emitEmpty bool) []block {
|
||||
hadBlocks := len(c.out) > 0
|
||||
c.flush()
|
||||
if emitEmpty && !hadBlocks && len(c.out) == 0 {
|
||||
c.out = append(c.out, c.build(nil))
|
||||
}
|
||||
return c.out
|
||||
}
|
||||
|
||||
func (c *collector) walk(node *html.Node, marks []mark) {
|
||||
switch node.Type {
|
||||
case html.TextNode:
|
||||
c.addText(node.Data, marks)
|
||||
return
|
||||
case html.ElementNode:
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
tag := node.Data
|
||||
if skippedElements[tag] {
|
||||
return
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case "br":
|
||||
c.addText("\n", marks)
|
||||
case "strong", "b":
|
||||
c.walkChildren(node, withMark(marks, mark{kind: markBold}))
|
||||
case "em", "i":
|
||||
c.walkChildren(node, withMark(marks, mark{kind: markItalic}))
|
||||
case "code":
|
||||
c.walkChildren(node, withMark(marks, mark{kind: markCode}))
|
||||
case "a":
|
||||
href := safeHref(attr(node, "href"))
|
||||
if href == "" {
|
||||
c.walkChildren(node, marks)
|
||||
return
|
||||
}
|
||||
c.walkChildren(node, withMark(marks, mark{kind: markLink, href: href}))
|
||||
case "img":
|
||||
c.flush()
|
||||
if src := attr(node, "src"); src != "" {
|
||||
c.out = append(c.out, block{kind: kindImage, src: src, alt: attr(node, "alt")})
|
||||
}
|
||||
case "hr":
|
||||
c.flush()
|
||||
c.out = append(c.out, block{kind: kindRule})
|
||||
case "p":
|
||||
c.flush()
|
||||
c.out = append(c.out, parseTextBlock(node, paragraphBlock)...)
|
||||
case "h1", "h2", "h3", "h4", "h5", "h6":
|
||||
c.flush()
|
||||
level, _ := strconv.Atoi(tag[1:])
|
||||
c.out = append(c.out, parseTextBlock(node, headingBlock(level))...)
|
||||
case "ul", "ol":
|
||||
c.flush()
|
||||
c.out = append(c.out, parseList(node))
|
||||
case "blockquote":
|
||||
c.flush()
|
||||
c.out = append(c.out, block{kind: kindBlockquote, children: parseContainer(node)})
|
||||
case "pre":
|
||||
c.flush()
|
||||
c.out = append(c.out, parseCodeBlock(node))
|
||||
default:
|
||||
if transparentBlocks[tag] {
|
||||
c.flush()
|
||||
c.walkChildren(node, marks)
|
||||
c.flush()
|
||||
return
|
||||
}
|
||||
c.walkChildren(node, marks)
|
||||
}
|
||||
}
|
||||
|
||||
func parseTextBlock(node *html.Node, build func([]inline) block) []block {
|
||||
inner := newCollector(true, build)
|
||||
inner.walkChildren(node, nil)
|
||||
blocks := inner.finish(true)
|
||||
if align := alignFromStyle(attr(node, "style")); align != "" {
|
||||
for index := range blocks {
|
||||
if blocks[index].kind == kindParagraph || blocks[index].kind == kindHeading {
|
||||
blocks[index].align = align
|
||||
}
|
||||
}
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
func alignFromStyle(style string) string {
|
||||
for _, declaration := range strings.Split(style, ";") {
|
||||
name, value, found := strings.Cut(declaration, ":")
|
||||
if !found || strings.ToLower(strings.TrimSpace(name)) != "text-align" {
|
||||
continue
|
||||
}
|
||||
if align := strings.ToLower(strings.TrimSpace(value)); alignments[align] {
|
||||
return align
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseContainer(node *html.Node) []block {
|
||||
inner := newCollector(false, paragraphBlock)
|
||||
inner.walkChildren(node, nil)
|
||||
return inner.finish(false)
|
||||
}
|
||||
|
||||
func parseList(node *html.Node) block {
|
||||
list := block{kind: kindList, ordered: node.Data == "ol"}
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if child.Type == html.ElementNode && child.Data == "li" {
|
||||
list.items = append(list.items, parseContainer(child))
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func parseCodeBlock(node *html.Node) block {
|
||||
code := block{kind: kindCode, text: strings.TrimSuffix(textContent(node), "\n")}
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if child.Type == html.ElementNode && child.Data == "code" {
|
||||
code.language = languageFromClass(attr(child, "class"))
|
||||
break
|
||||
}
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
func languageFromClass(class string) string {
|
||||
for _, name := range strings.Fields(class) {
|
||||
if strings.HasPrefix(name, codeLanguagePrefix) {
|
||||
return strings.TrimPrefix(name, codeLanguagePrefix)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func textContent(node *html.Node) string {
|
||||
var builder strings.Builder
|
||||
var visit func(*html.Node)
|
||||
visit = func(n *html.Node) {
|
||||
switch {
|
||||
case n.Type == html.TextNode:
|
||||
builder.WriteString(n.Data)
|
||||
case n.Type == html.ElementNode && skippedElements[n.Data]:
|
||||
return
|
||||
case n.Type == html.ElementNode && n.Data == "br":
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
for child := n.FirstChild; child != nil; child = child.NextSibling {
|
||||
visit(child)
|
||||
}
|
||||
}
|
||||
visit(node)
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func attr(node *html.Node, name string) string {
|
||||
for _, a := range node.Attr {
|
||||
if a.Key == name {
|
||||
return a.Val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func safeHref(href string) string {
|
||||
trimmed := strings.TrimSpace(href)
|
||||
parsed, err := url.Parse(trimmed)
|
||||
if err != nil || !allowedLinkSchemes[strings.ToLower(parsed.Scheme)] {
|
||||
return ""
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func withMark(marks []mark, added mark) []mark {
|
||||
for _, existing := range marks {
|
||||
if existing.kind == added.kind {
|
||||
return marks
|
||||
}
|
||||
}
|
||||
next := make([]mark, 0, len(marks)+1)
|
||||
next = append(next, marks...)
|
||||
return append(next, added)
|
||||
}
|
||||
|
||||
func hasVisibleText(inlines []inline) bool {
|
||||
for _, item := range inlines {
|
||||
if strings.TrimSpace(item.text) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sameMarks(a, b []mark) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for index := range a {
|
||||
if a[index] != b[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func appendInline(inlines []inline, next inline) []inline {
|
||||
if last := len(inlines) - 1; last >= 0 && sameMarks(inlines[last].marks, next.marks) {
|
||||
inlines[last].text += next.text
|
||||
return inlines
|
||||
}
|
||||
return append(inlines, next)
|
||||
}
|
||||
|
||||
func withoutCodeMarks(inlines []inline) []inline {
|
||||
merged := make([]inline, 0, len(inlines))
|
||||
for _, item := range inlines {
|
||||
kept := make([]mark, 0, len(item.marks))
|
||||
for _, m := range item.marks {
|
||||
if m.kind != markCode {
|
||||
kept = append(kept, m)
|
||||
}
|
||||
}
|
||||
merged = appendInline(merged, inline{text: item.text, marks: kept})
|
||||
}
|
||||
return merged
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/article"
|
||||
)
|
||||
|
||||
type Converter struct{}
|
||||
|
||||
var _ domain.BodyRenderer = &Converter{}
|
||||
|
||||
func NewConverter() *Converter {
|
||||
return &Converter{}
|
||||
}
|
||||
|
||||
func (c *Converter) Render(html string, format domain.BodyFormat) (string, error) {
|
||||
switch format {
|
||||
case domain.BodyFormatHTML:
|
||||
return html, nil
|
||||
case domain.BodyFormatMarkdown:
|
||||
blocks, err := parseBlocks(html)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return renderMarkdown(blocks), nil
|
||||
case domain.BodyFormatJSON:
|
||||
blocks, err := parseBlocks(html)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return renderJSON(blocks)
|
||||
}
|
||||
return "", domain.ErrUnsupportedBodyFormat
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type jsonMark struct {
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href,omitempty"`
|
||||
}
|
||||
|
||||
type jsonText struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
Marks []jsonMark `json:"marks"`
|
||||
}
|
||||
|
||||
type jsonHeading struct {
|
||||
Type string `json:"type"`
|
||||
Level int `json:"level"`
|
||||
Align string `json:"align,omitempty"`
|
||||
Children []jsonText `json:"children"`
|
||||
}
|
||||
|
||||
type jsonParagraph struct {
|
||||
Type string `json:"type"`
|
||||
Align string `json:"align,omitempty"`
|
||||
Children []jsonText `json:"children"`
|
||||
}
|
||||
|
||||
type jsonList struct {
|
||||
Type string `json:"type"`
|
||||
Ordered bool `json:"ordered"`
|
||||
Items [][]any `json:"items"`
|
||||
}
|
||||
|
||||
type jsonBlockquote struct {
|
||||
Type string `json:"type"`
|
||||
Children []any `json:"children"`
|
||||
}
|
||||
|
||||
type jsonCode struct {
|
||||
Type string `json:"type"`
|
||||
Language string `json:"language"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type jsonRule struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type jsonImage struct {
|
||||
Type string `json:"type"`
|
||||
Src string `json:"src"`
|
||||
Alt string `json:"alt"`
|
||||
}
|
||||
|
||||
func renderJSON(blocks []block) (string, error) {
|
||||
var buffer bytes.Buffer
|
||||
encoder := json.NewEncoder(&buffer)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if err := encoder.Encode(toJSONBlocks(blocks)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSuffix(buffer.String(), "\n"), nil
|
||||
}
|
||||
|
||||
func toJSONBlocks(blocks []block) []any {
|
||||
result := make([]any, 0, len(blocks))
|
||||
for _, b := range blocks {
|
||||
result = append(result, toJSONBlock(b))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toJSONBlock(b block) any {
|
||||
switch b.kind {
|
||||
case kindHeading:
|
||||
return jsonHeading{Type: "heading", Level: b.level, Align: b.align, Children: toJSONTexts(b.inlines)}
|
||||
case kindParagraph:
|
||||
return jsonParagraph{Type: "paragraph", Align: b.align, Children: toJSONTexts(b.inlines)}
|
||||
case kindList:
|
||||
items := make([][]any, 0, len(b.items))
|
||||
for _, item := range b.items {
|
||||
items = append(items, toJSONBlocks(item))
|
||||
}
|
||||
return jsonList{Type: "list", Ordered: b.ordered, Items: items}
|
||||
case kindBlockquote:
|
||||
return jsonBlockquote{Type: "blockquote", Children: toJSONBlocks(b.children)}
|
||||
case kindCode:
|
||||
return jsonCode{Type: "code", Language: b.language, Text: b.text}
|
||||
case kindImage:
|
||||
return jsonImage{Type: "image", Src: b.src, Alt: b.alt}
|
||||
}
|
||||
return jsonRule{Type: "hr"}
|
||||
}
|
||||
|
||||
func toJSONTexts(inlines []inline) []jsonText {
|
||||
plain := withoutCodeMarks(inlines)
|
||||
texts := make([]jsonText, 0, len(plain))
|
||||
for _, item := range plain {
|
||||
marks := make([]jsonMark, 0, len(item.marks))
|
||||
for _, m := range item.marks {
|
||||
marks = append(marks, jsonMark{Type: m.kind, Href: m.href})
|
||||
}
|
||||
texts = append(texts, jsonText{Type: "text", Text: item.text, Marks: marks})
|
||||
}
|
||||
return texts
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
blockSeparator = "\n\n"
|
||||
hardBreak = "\\\n"
|
||||
bulletMarker = "- "
|
||||
codeFence = "```"
|
||||
alignOpen = `<div style="text-align: `
|
||||
alignOpenEnd = `">`
|
||||
alignClose = "</div>"
|
||||
)
|
||||
|
||||
var textEscaper = strings.NewReplacer(
|
||||
"\\", "\\\\", "*", "\\*", "_", "\\_", "`", "\\`", "[", "\\[", "]", "\\]", "<", "\\<",
|
||||
)
|
||||
|
||||
var (
|
||||
numberedLineStart = regexp.MustCompile(`^(\d+)([.)])(\s|$)`)
|
||||
markerLineStart = regexp.MustCompile(`^(>|#{1,6}(\s|$)|[+-](\s|$|-)|=+\s*$)`)
|
||||
)
|
||||
|
||||
var hrefEscaper = strings.NewReplacer(" ", "%20", "(", "%28", ")", "%29", "<", "%3C", ">", "%3E")
|
||||
|
||||
func renderMarkdown(blocks []block) string {
|
||||
return renderBlocks(blocks, blockSeparator)
|
||||
}
|
||||
|
||||
func renderBlocks(blocks []block, separator string) string {
|
||||
parts := make([]string, 0, len(blocks))
|
||||
for _, b := range blocks {
|
||||
if rendered := renderBlock(b); rendered != "" {
|
||||
parts = append(parts, rendered)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, separator)
|
||||
}
|
||||
|
||||
func renderBlock(b block) string {
|
||||
rendered := renderPlainBlock(b)
|
||||
if b.align == "" || rendered == "" {
|
||||
return rendered
|
||||
}
|
||||
return alignOpen + b.align + alignOpenEnd + blockSeparator + rendered + blockSeparator + alignClose
|
||||
}
|
||||
|
||||
func renderPlainBlock(b block) string {
|
||||
switch b.kind {
|
||||
case kindHeading:
|
||||
return strings.Repeat("#", b.level) + " " + renderInlines(b.inlines)
|
||||
case kindParagraph:
|
||||
return escapeLineStarts(renderInlines(b.inlines))
|
||||
case kindList:
|
||||
return renderList(b)
|
||||
case kindBlockquote:
|
||||
return prefixLines(renderBlocks(b.children, blockSeparator), "> ", ">")
|
||||
case kindCode:
|
||||
return renderCode(b)
|
||||
case kindImage:
|
||||
return " + ")"
|
||||
}
|
||||
return "---"
|
||||
}
|
||||
|
||||
func escapeLineStarts(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
for index, line := range lines {
|
||||
line = numberedLineStart.ReplaceAllString(line, `$1\$2$3`)
|
||||
if markerLineStart.MatchString(line) {
|
||||
line = `\` + line
|
||||
}
|
||||
lines[index] = line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func renderCode(b block) string {
|
||||
fence := codeFence
|
||||
for strings.Contains(b.text, fence) {
|
||||
fence += "`"
|
||||
}
|
||||
return fence + b.language + "\n" + b.text + "\n" + fence
|
||||
}
|
||||
|
||||
func escapeAlt(alt string) string {
|
||||
return strings.NewReplacer("[", "\\[", "]", "\\]", "\n", " ").Replace(alt)
|
||||
}
|
||||
|
||||
func renderList(b block) string {
|
||||
lines := make([]string, 0, len(b.items))
|
||||
for index, item := range b.items {
|
||||
marker := bulletMarker
|
||||
if b.ordered {
|
||||
marker = strconv.Itoa(index+1) + ". "
|
||||
}
|
||||
content := renderBlocks(item, itemSeparator(item))
|
||||
lines = append(lines, marker+indentContinuation(content, len(marker)))
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func itemSeparator(item []block) string {
|
||||
textBlocks := 0
|
||||
for _, b := range item {
|
||||
if b.kind != kindList {
|
||||
textBlocks++
|
||||
}
|
||||
}
|
||||
if textBlocks > 1 {
|
||||
return blockSeparator
|
||||
}
|
||||
return "\n"
|
||||
}
|
||||
|
||||
func indentContinuation(content string, width int) string {
|
||||
indent := strings.Repeat(" ", width)
|
||||
lines := strings.Split(content, "\n")
|
||||
for index := 1; index < len(lines); index++ {
|
||||
if lines[index] != "" {
|
||||
lines[index] = indent + lines[index]
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func prefixLines(content, prefix, emptyPrefix string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
for index, line := range lines {
|
||||
if line == "" {
|
||||
lines[index] = emptyPrefix
|
||||
continue
|
||||
}
|
||||
lines[index] = prefix + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func renderInlines(inlines []inline) string {
|
||||
var builder strings.Builder
|
||||
for _, item := range inlines {
|
||||
builder.WriteString(renderInline(item))
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func renderInline(item inline) string {
|
||||
isCode := hasMarkKind(item.marks, markCode)
|
||||
lines := strings.Split(item.text, "\n")
|
||||
for index, line := range lines {
|
||||
lines[index] = wrapMarks(item.marks, line, isCode)
|
||||
}
|
||||
return strings.Join(lines, hardBreak)
|
||||
}
|
||||
|
||||
func hasMarkKind(marks []mark, kind string) bool {
|
||||
for _, m := range marks {
|
||||
if m.kind == kind {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func wrapMarks(marks []mark, text string, isCode bool) string {
|
||||
core := strings.TrimSpace(text)
|
||||
if core == "" {
|
||||
return text
|
||||
}
|
||||
start := strings.Index(text, core)
|
||||
leading, trailing := text[:start], text[start+len(core):]
|
||||
var wrapped string
|
||||
if isCode {
|
||||
wrapped = inlineCode(core)
|
||||
} else {
|
||||
wrapped = escapeText(core)
|
||||
}
|
||||
for index := len(marks) - 1; index >= 0; index-- {
|
||||
wrapped = applyMark(marks[index], wrapped)
|
||||
}
|
||||
return leading + wrapped + trailing
|
||||
}
|
||||
|
||||
func inlineCode(text string) string {
|
||||
fence := "`"
|
||||
for strings.Contains(text, fence) {
|
||||
fence += "`"
|
||||
}
|
||||
if strings.HasPrefix(text, "`") || strings.HasSuffix(text, "`") {
|
||||
return fence + " " + text + " " + fence
|
||||
}
|
||||
return fence + text + fence
|
||||
}
|
||||
|
||||
func applyMark(m mark, text string) string {
|
||||
switch m.kind {
|
||||
case markBold:
|
||||
return "**" + text + "**"
|
||||
case markItalic:
|
||||
return "*" + text + "*"
|
||||
case markLink:
|
||||
return "[" + text + "](" + hrefEscaper.Replace(m.href) + ")"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func escapeText(text string) string {
|
||||
return textEscaper.Replace(text)
|
||||
}
|
||||
Reference in new issue
Block a user