Repository: kovetskiy/mark
Pull request #318 (2023-05-19 17:08:39)

Move drop-h1 to the renderer
This is more reliable than a regular expression.


@kovetskiy do you know if we want to drop *ALL* h1 or just the first one with this?

mrueg: Testing it out, it looks like it'd only drop the first h1.

Patch:
diff --git a/README.md b/README.md
index 599b497..206fc0e 100644
--- a/README.md
+++ b/README.md
@@ -667,7 +667,7 @@ GLOBAL OPTIONS:
    --compile-only                                show resulting HTML and don't update Confluence page content. (default: false) [$MARK_COMPILE_ONLY]
    --dry-run                                     resolve page and ancestry, show resulting HTML and exit. (default: false) [$MARK_DRY_RUN]
    --edit-lock, -k                               lock page editing to current user only to prevent accidental manual edits over Confluence Web UI. (default: false) [$MARK_EDIT_LOCK]
-   --drop-h1, --h1_drop                          don't include H1 headings in Confluence output. (default: false) [$MARK_H1_DROP]
+   --drop-h1, --h1_drop                          don't include the first H1 heading in Confluence output. (default: false) [$MARK_H1_DROP]
    --title-from-h1, --h1_title                   extract page title from a leading H1 heading. If no H1 heading on a page exists, then title must be set in the page metadata. (default: false) [$MARK_H1_TITLE]
    --minor-edit                                  don't send notifications while updating Confluence page. (default: false) [$MARK_MINOR_EDIT]
    --color value                                 display logs in color. Possible values: auto, never. (default: "auto") [$MARK_COLOR]
diff --git a/main.go b/main.go
index ab32b98..b6d8d0a 100644
--- a/main.go
+++ b/main.go
@@ -58,7 +58,7 @@ var flags = []cli.Flag{
 		Name:    "drop-h1",
 		Value:   false,
 		Aliases: []string{"h1_drop"},
-		Usage:   "don't include H1 headings in Confluence output.",
+		Usage:   "don't include the first H1 heading in Confluence output.",
 		EnvVars: []string{"MARK_H1_DROP"},
 	}),
 	altsrc.NewBoolFlag(&cli.BoolFlag{
@@ -361,10 +361,9 @@ func processFile(
 			log.Info(
 				"the leading H1 heading will be excluded from the Confluence output",
 			)
-			markdown = mark.DropDocumentLeadingH1(markdown)
 		}
 
-		html, _ := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"))
+		html, _ := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"), cCtx.Bool("drop-h1"))
 		fmt.Println(html)
 		os.Exit(0)
 	}
@@ -438,10 +437,9 @@ func processFile(
 		log.Info(
 			"the leading H1 heading will be excluded from the Confluence output",
 		)
-		markdown = mark.DropDocumentLeadingH1(markdown)
 	}
 
-	html, inlineAttachments := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"))
+	html, inlineAttachments := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"), cCtx.Bool("drop-h1"))
 
 	// Resolve attachements detected from markdown
 	_, err = mark.ResolveAttachments(
diff --git a/pkg/mark/markdown.go b/pkg/mark/markdown.go
index 7119396..298fd79 100644
--- a/pkg/mark/markdown.go
+++ b/pkg/mark/markdown.go
@@ -53,17 +53,19 @@ type ConfluenceRenderer struct {
 	Stdlib          *stdlib.Lib
 	Path            string
 	MermaidProvider string
+	DropFirstH1     bool
 	LevelMap        BlockQuoteLevelMap
 	Attachments     []Attachment
 }
 
 // NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
-func NewConfluenceRenderer(stdlib *stdlib.Lib, path string, mermaidProvider string, opts ...html.Option) renderer.NodeRenderer {
+func NewConfluenceRenderer(stdlib *stdlib.Lib, path string, mermaidProvider string, dropFirstH1 bool, opts ...html.Option) renderer.NodeRenderer {
 	return &ConfluenceRenderer{
 		Config:          html.NewConfig(),
 		Stdlib:          stdlib,
 		Path:            path,
 		MermaidProvider: mermaidProvider,
+		DropFirstH1:     dropFirstH1,
 		LevelMap:        nil,
 		Attachments:     []Attachment{},
 	}
@@ -73,7 +75,7 @@ func NewConfluenceRenderer(stdlib *stdlib.Lib, path string, mermaidProvider stri
 func (r *ConfluenceRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
 	// blocks
 	// reg.Register(ast.KindDocument, r.renderNode)
-	// reg.Register(ast.KindHeading, r.renderNode)
+	reg.Register(ast.KindHeading, r.renderHeading)
 	reg.Register(ast.KindBlockquote, r.renderBlockQuote)
 	reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
 	reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
@@ -95,6 +97,37 @@ func (r *ConfluenceRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegister
 	// reg.Register(ast.KindString, r.renderNode)
 }
 
+func (r *ConfluenceRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*ast.Heading)
+
+	// If this is the first h1 heading of the document and we want to drop it, let's not render it at all.
+	if n.Level == 1 && r.DropFirstH1 {
+		if !entering {
+			r.DropFirstH1 = false
+		}
+		return ast.WalkSkipChildren, nil
+	}
+
+	return r.goldmarkRenderHeading(w, source, node, entering)
+}
+
+func (r *ConfluenceRenderer) goldmarkRenderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	n := node.(*ast.Heading)
+	if entering {
+		_, _ = w.WriteString("<h")
+		_ = w.WriteByte("0123456"[n.Level])
+		if n.Attributes() != nil {
+			html.RenderAttributes(w, node, html.HeadingAttributeFilter)
+		}
+		_ = w.WriteByte('>')
+	} else {
+		_, _ = w.WriteString("</h")
+		_ = w.WriteByte("0123456"[n.Level])
+		_, _ = w.WriteString(">\n")
+	}
+	return ast.WalkContinue, nil
+}
+
 func ParseLanguage(lang string) string {
 	// lang takes the following form: language? "collapse"? ("title"? <any string>*)?
 	// let's split it by spaces
@@ -612,10 +645,10 @@ func (r *ConfluenceRenderer) goldmarkRenderHTMLBlock(w util.BufWriter, source []
 	return ast.WalkContinue, nil
 }
 
-func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, mermaidProvider string) (string, []Attachment) {
+func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, mermaidProvider string, dropFirstH1 bool) (string, []Attachment) {
 	log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
 
-	confluenceRenderer := NewConfluenceRenderer(stdlib, path, mermaidProvider)
+	confluenceRenderer := NewConfluenceRenderer(stdlib, path, mermaidProvider, dropFirstH1)
 
 	converter := goldmark.New(
 		goldmark.WithExtensions(
@@ -657,18 +690,6 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, mermaidPr
 
 }
 
-// DropDocumentLeadingH1 will drop leading H1 headings to prevent
-// duplication of or visual conflict with page titles.
-// NOTE: This is intended only to operate on the whole markdown document.
-// Operating on individual lines will clear them if the begin with `#`.
-func DropDocumentLeadingH1(
-	markdown []byte,
-) []byte {
-	h1 := regexp.MustCompile(`^#[^#].*\n`)
-	markdown = h1.ReplaceAll(markdown, []byte(""))
-	return markdown
-}
-
 // ExtractDocumentLeadingH1 will extract leading H1 heading
 func ExtractDocumentLeadingH1(markdown []byte) string {
 	h1 := regexp.MustCompile(`#[^#]\s*(.*)\s*\n`)


Test patch:
diff --git a/pkg/mark/markdown_test.go b/pkg/mark/markdown_test.go
index 269d27f..e833251 100644
--- a/pkg/mark/markdown_test.go
+++ b/pkg/mark/markdown_test.go
@@ -36,7 +36,7 @@ func TestCompileMarkdown(t *testing.T) {
 		if err != nil {
 			panic(err)
 		}
-		actual, _ := CompileMarkdown(markdown, lib, filename, "")
+		actual, _ := CompileMarkdown(markdown, lib, filename, "", false)
 		test.EqualValues(string(html), actual, filename+" vs "+htmlname)
 	}
 }