html.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. package md
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. )
  7. // There are different ways to escape HTML and URLs. The CommonMark spec does
  8. // not specify any particular way, but the spec tests do assume a certain one.
  9. // The schemes below are chosen to match the spec tests.
  10. var (
  11. escapeHTML = strings.NewReplacer(
  12. "&", "&amp;", `"`, "&quot;", "<", "&lt;", ">", "&gt;",
  13. // No need to escape single quotes, since attributes in the output
  14. // always use double quotes.
  15. ).Replace
  16. escapeURL = strings.NewReplacer(
  17. `"`, "%22", `\`, "%5C", " ", "%20", "`", "%60",
  18. "[", "%5B", "]", "%5D", "<", "%3C", ">", "%3E",
  19. "ö", "%C3%B6",
  20. "ä", "%C3%A4", " ", "%C2%A0").Replace
  21. )
  22. // HTMLCodec converts markdown to HTML.
  23. type HTMLCodec struct {
  24. strings.Builder
  25. }
  26. var tags = []string{
  27. OpThematicBreak: "<hr />\n",
  28. OpBlockquoteStart: "<blockquote>\n", OpBlockquoteEnd: "</blockquote>\n",
  29. OpListItemStart: "<li>\n", OpListItemEnd: "</li>\n",
  30. OpBulletListStart: "<ul>\n", OpBulletListEnd: "</ul>\n",
  31. OpOrderedListEnd: "</ol>\n",
  32. }
  33. func (c *HTMLCodec) Do(op Op) {
  34. switch op.Type {
  35. case OpHeading:
  36. var attrs attrBuilder
  37. if op.Info != "" {
  38. // Only support #id since that's the only thing used in Elvish's
  39. // Markdown right now. More can be added if needed.
  40. if op.Info[0] == '#' {
  41. attrs.set("id", op.Info[1:])
  42. }
  43. }
  44. fmt.Fprintf(c, "<h%d%s>", op.Number, &attrs)
  45. RenderInlineContentToHTML(&c.Builder, op.Content)
  46. fmt.Fprintf(c, "</h%d>\n", op.Number)
  47. case OpCodeBlock:
  48. var attrs attrBuilder
  49. if op.Info != "" {
  50. language, _, _ := strings.Cut(op.Info, " ")
  51. attrs.set("class", "language-"+language)
  52. }
  53. fmt.Fprintf(c, "<pre><code%s>", &attrs)
  54. for _, line := range op.Lines {
  55. c.WriteString(escapeHTML(line))
  56. c.WriteByte('\n')
  57. }
  58. c.WriteString("</code></pre>\n")
  59. case OpHTMLBlock:
  60. for _, line := range op.Lines {
  61. c.WriteString(line)
  62. c.WriteByte('\n')
  63. }
  64. case OpParagraph:
  65. c.WriteString("<p>")
  66. RenderInlineContentToHTML(&c.Builder, op.Content)
  67. c.WriteString("</p>\n")
  68. case OpOrderedListStart:
  69. var attrs attrBuilder
  70. if op.Number != 1 {
  71. attrs.set("start", strconv.Itoa(op.Number))
  72. }
  73. fmt.Fprintf(c, "<ol%s>\n", &attrs)
  74. default:
  75. c.WriteString(tags[op.Type])
  76. }
  77. }
  78. var inlineTags = []string{
  79. OpNewLine: "\n",
  80. OpEmphasisStart: "<em>", OpEmphasisEnd: "</em>",
  81. OpStrongEmphasisStart: "<strong>", OpStrongEmphasisEnd: "</strong>",
  82. OpLinkEnd: "</a>",
  83. OpHardLineBreak: "<br />",
  84. }
  85. // RenderInlineContentToHTML renders inline content to HTML, writing to a
  86. // [strings.Builder].
  87. func RenderInlineContentToHTML(sb *strings.Builder, ops []InlineOp) {
  88. for _, op := range ops {
  89. doInline(sb, op)
  90. }
  91. }
  92. func doInline(sb *strings.Builder, op InlineOp) {
  93. switch op.Type {
  94. case OpText:
  95. sb.WriteString(escapeHTML(op.Text))
  96. case OpCodeSpan:
  97. sb.WriteString("<code>")
  98. sb.WriteString(escapeHTML(op.Text))
  99. sb.WriteString("</code>")
  100. case OpRawHTML:
  101. sb.WriteString(op.Text)
  102. case OpLinkStart:
  103. var attrs attrBuilder
  104. attrs.set("href", escapeURL(op.Dest))
  105. if op.Text != "" {
  106. attrs.set("title", op.Text)
  107. }
  108. fmt.Fprintf(sb, "<a%s>", &attrs)
  109. case OpImage:
  110. var attrs attrBuilder
  111. attrs.set("src", escapeURL(op.Dest))
  112. attrs.set("alt", op.Alt)
  113. if op.Text != "" {
  114. attrs.set("title", op.Text)
  115. }
  116. fmt.Fprintf(sb, "<img%s />", &attrs)
  117. case OpAutolink:
  118. var attrs attrBuilder
  119. attrs.set("href", escapeURL(op.Dest))
  120. fmt.Fprintf(sb, "<a%s>%s</a>", &attrs, escapeHTML(op.Text))
  121. default:
  122. sb.WriteString(inlineTags[op.Type])
  123. }
  124. }
  125. type attrBuilder struct{ strings.Builder }
  126. func (a *attrBuilder) set(k, v string) { fmt.Fprintf(a, ` %s="%s"`, k, escapeHTML(v)) }