fix: H1 conversion leaves code alone in markdown that starts with HTML
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.
This commit is contained in:
@@ -70,27 +70,55 @@ func containsFold(s, sub string) bool {
|
||||
// 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 rewritten only when it is HTML (LooksLikeHTML, the rich
|
||||
// editor's body): a markdown body is never rewritten, so a code sample that
|
||||
// shows an <h1> keeps it, and only its rendered content_html is converted
|
||||
// (every render path derives content_html from the markdown again and
|
||||
// converts it again).
|
||||
// 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)
|
||||
if !LooksLikeHTML(content) {
|
||||
return content, contentHTML, nHTML
|
||||
}
|
||||
content, nContent := ConvertH1(content)
|
||||
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. A markdown content is never rewritten
|
||||
// (ConvertContentH1). Other attributes and values that are not strings are
|
||||
// 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 {
|
||||
|
||||
@@ -75,12 +75,41 @@ func TestConvertContentH1(t *testing.T) {
|
||||
if n != 1 || !strings.Contains(h, "<h2") || strings.Contains(h, "<h1") || !strings.Contains(h, "<h1>Hello</h1>") || !strings.Contains(h, "<code><h1></code>") {
|
||||
t.Fatalf("rendered HTML = %q n=%d", h, n)
|
||||
}
|
||||
// Markdown without rendered HTML: nothing changes, nothing counted.
|
||||
if c, h, n := ConvertContentH1("plain <h1>x</h1> text", ""); c != "plain <h1>x</h1> text" || h != "" || n != 0 {
|
||||
// A raw h1 outside code is a heading in the rendered page, so it is
|
||||
// converted in the markdown source too (as v0.3.1 did), and counted
|
||||
// when there is no rendered HTML.
|
||||
if c, h, n := ConvertContentH1("plain <h1>x</h1> text", ""); c != "plain <h2>x</h2> text" || h != "" || n != 1 {
|
||||
t.Fatalf("markdown without HTML = %q %q %d", c, h, n)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertContentH1HTMLLead pins markdown that starts with an HTML
|
||||
// block (33-REVIEW-GAPFIX WR-01): the first byte does not make it HTML,
|
||||
// so the h1 samples in its code keep their tags and only a raw h1 outside
|
||||
// code is converted.
|
||||
func TestConvertContentH1HTMLLead(t *testing.T) {
|
||||
probe := "<p class=\"lead\">Intro</p>\n\n```html\n<h1>Hello</h1>\n```"
|
||||
rendered, err := FormatHTML(probe)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, html := range []string{rendered, ""} {
|
||||
c, h, n := ConvertContentH1(probe, html)
|
||||
if c != probe || h != html || n != 0 {
|
||||
t.Fatalf("probe with content_html %q = %q %q n=%d", html, c, h, n)
|
||||
}
|
||||
}
|
||||
mixed := "<p class=\"lead\">Intro</p>\n\n<h1>Real</h1>\n\n```html\n<h1>Hello</h1>\n```"
|
||||
want := "<p class=\"lead\">Intro</p>\n\n<h2>Real</h2>\n\n```html\n<h1>Hello</h1>\n```"
|
||||
if c, h, n := ConvertContentH1(mixed, ""); c != want || h != "" || n != 1 {
|
||||
t.Fatalf("raw h1 outside code = %q %q n=%d", c, h, n)
|
||||
}
|
||||
span := "<p>x</p>\n\nUse `<h1>` here"
|
||||
if c, _, n := ConvertContentH1(span, ""); c != span || n != 0 {
|
||||
t.Fatalf("code span = %q n=%d", c, n)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertTranslationH1 pins a locale's translated attributes,
|
||||
// changed in place: content and content_html converted, other attributes
|
||||
// and values that are not strings left alone (a category's pillar text
|
||||
@@ -102,4 +131,13 @@ func TestConvertTranslationH1(t *testing.T) {
|
||||
if n := ConvertTranslationH1(tr); n != 0 || tr["content"] != md {
|
||||
t.Fatalf("a markdown translation was rewritten: %v n=%d", tr, n)
|
||||
}
|
||||
probe := "<p class=\"lead\">Intro</p>\n\n```html\n<h1>Hello</h1>\n```"
|
||||
rendered, err := FormatHTML(probe)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lead := map[string]any{"content": probe, "content_html": rendered}
|
||||
if n := ConvertTranslationH1(lead); n != 0 || lead["content"] != probe {
|
||||
t.Fatalf("markdown with an HTML lead was rewritten: %v n=%d", lead, n)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user