ConvertContentH1 reads content as markdown on every path and converts it outside goldmark's code ranges only; a body opening with an HTML block keeps the h1 tags of its code samples, a raw h1 outside code is converted as in v0.3.1 (the 33-15 test pin for that case is reversed). quick-261104-xae, review WR-01.
141 lines
4.3 KiB
Go
141 lines
4.3 KiB
Go
package classes
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
|
|
xhtml "golang.org/x/net/html"
|
|
)
|
|
|
|
// ConvertH1 turns every h1 element of html into an h2 and returns the
|
|
// result with the number of headings it converted. It works on the
|
|
// x/net/html tokenizer's raw tokens: only the tag name of an h1 start, end
|
|
// or self-closing tag changes, and every other byte (attributes, text,
|
|
// comments, the content of script or style) is copied as it is. Text that
|
|
// merely looks like a tag inside raw-text elements or comments is not a
|
|
// tag and is not touched. html without an h1 comes back unchanged with 0,
|
|
// and the bytes of an unfinished tag at the end of html are kept.
|
|
func ConvertH1(html string) (string, int) {
|
|
if !containsFold(html, "<h1") && !containsFold(html, "</h1") {
|
|
return html, 0
|
|
}
|
|
z := xhtml.NewTokenizer(strings.NewReader(html))
|
|
var b bytes.Buffer
|
|
b.Grow(len(html))
|
|
count := 0
|
|
consumed := 0
|
|
for {
|
|
tt := z.Next()
|
|
if tt == xhtml.ErrorToken {
|
|
// The input may end inside something the tokenizer reads as a
|
|
// tag ("a <b"): those bytes are kept as they are.
|
|
b.WriteString(html[min(consumed, len(html)):])
|
|
break
|
|
}
|
|
raw := z.Raw()
|
|
consumed += len(raw)
|
|
switch tt {
|
|
case xhtml.StartTagToken, xhtml.SelfClosingTagToken:
|
|
if name, _ := z.TagName(); string(name) == "h1" && len(raw) >= 3 {
|
|
b.WriteString("<h2")
|
|
b.Write(raw[3:])
|
|
count++
|
|
continue
|
|
}
|
|
case xhtml.EndTagToken:
|
|
if name, _ := z.TagName(); string(name) == "h1" && len(raw) >= 4 {
|
|
b.WriteString("</h2")
|
|
b.Write(raw[4:])
|
|
continue
|
|
}
|
|
}
|
|
b.Write(raw)
|
|
}
|
|
if count == 0 && b.String() == html {
|
|
return html, 0
|
|
}
|
|
return b.String(), count
|
|
}
|
|
|
|
// CountH1 is the number of h1 start tags in html, as ConvertH1 counts them.
|
|
func CountH1(html string) int {
|
|
_, n := ConvertH1(html)
|
|
return n
|
|
}
|
|
|
|
func containsFold(s, sub string) bool {
|
|
return strings.Contains(strings.ToLower(s), sub)
|
|
}
|
|
|
|
// ConvertContentH1 applies ConvertH1 to a post body and its rendered HTML
|
|
// (content and content_html) and returns both with the number of headings
|
|
// converted: content_html's count, or content's when content_html had none.
|
|
// content is read as markdown, as every render path reads it (FormatHTML
|
|
// passes raw HTML through, so an HTML body is HTML blocks and is converted
|
|
// as before): an <h1> inside a code block or code span keeps its tag
|
|
// whatever the body's first character, so a markdown post that opens with
|
|
// an HTML block keeps its code samples (33-REVIEW-GAPFIX WR-01), and a raw
|
|
// <h1> outside code becomes <h2>, as in the rendered HTML.
|
|
func ConvertContentH1(content, contentHTML string) (string, string, int) {
|
|
contentHTML, nHTML := ConvertH1(contentHTML)
|
|
content, nContent := convertH1OutsideCode(content)
|
|
if nHTML > 0 {
|
|
return content, contentHTML, nHTML
|
|
}
|
|
return content, contentHTML, nContent
|
|
}
|
|
|
|
// convertH1OutsideCode applies ConvertH1 to the parts of markdown md
|
|
// outside its fenced and indented code blocks and code spans (goldmark's
|
|
// AST, the engine of FormatHTML): an h1 shown in a code sample keeps its
|
|
// tag. HTML blocks are not code, so an HTML body is converted whole. md
|
|
// without an h1 tag comes back unchanged with 0 without being parsed.
|
|
func convertH1OutsideCode(md string) (string, int) {
|
|
if !containsFold(md, "<h1") && !containsFold(md, "</h1") {
|
|
return md, 0
|
|
}
|
|
var b strings.Builder
|
|
b.Grow(len(md))
|
|
count := 0
|
|
last := 0
|
|
apply := func(part string) {
|
|
out, n := ConvertH1(part)
|
|
count += n
|
|
b.WriteString(out)
|
|
}
|
|
for _, r := range markdownCodeRanges(md) {
|
|
apply(md[last:r[0]])
|
|
b.WriteString(md[r[0]:r[1]])
|
|
last = r[1]
|
|
}
|
|
apply(md[last:])
|
|
if count == 0 && b.String() == md {
|
|
return md, 0
|
|
}
|
|
return b.String(), count
|
|
}
|
|
|
|
// ConvertTranslationH1 applies ConvertContentH1 to the string content and
|
|
// content_html of one locale's translated attributes, in place, and returns
|
|
// the number of headings converted. content is rewritten outside code
|
|
// only (ConvertContentH1). Other attributes and values that are not strings are
|
|
// left alone.
|
|
func ConvertTranslationH1(attrs map[string]any) int {
|
|
if attrs == nil {
|
|
return 0
|
|
}
|
|
content, hasContent := attrs["content"].(string)
|
|
html, hasHTML := attrs["content_html"].(string)
|
|
if !hasContent && !hasHTML {
|
|
return 0
|
|
}
|
|
content, html, n := ConvertContentH1(content, html)
|
|
if hasContent {
|
|
attrs["content"] = content
|
|
}
|
|
if hasHTML {
|
|
attrs["content_html"] = html
|
|
}
|
|
return n
|
|
}
|