diff --git a/cmd/account/account_producer_extension_info_push.go b/cmd/account/account_producer_extension_info_push.go index 82566d96..f8b80c4d 100644 --- a/cmd/account/account_producer_extension_info_push.go +++ b/cmd/account/account_producer_extension_info_push.go @@ -1,7 +1,6 @@ package account import ( - "bytes" "context" "fmt" "os" @@ -15,6 +14,7 @@ import ( accountApi "github.com/shopware/shopware-cli/internal/account-api" "github.com/shopware/shopware-cli/internal/extension" + "github.com/shopware/shopware-cli/internal/markdown" "github.com/shopware/shopware-cli/logging" ) @@ -312,15 +312,12 @@ func parseInlineablePath(path, extensionDir string) (string, error) { return string(content), nil } - md := extension.GetConfiguredGoldMark() - - var buf bytes.Buffer - err = md.Convert(content, &buf) + html, err := markdown.ToHTML(content) if err != nil { return "", fmt.Errorf("cannot convert file at path %s from markdown to html with error: %v", filePath, err) } - return buf.String(), nil + return html, nil } func uploadImagesByDirectory(ctx context.Context, extensionId int, directory string, index int, p *accountApi.ProducerEndpoint) error { diff --git a/internal/extension/changelog.go b/internal/extension/changelog.go index b04551d0..1186e0aa 100644 --- a/internal/extension/changelog.go +++ b/internal/extension/changelog.go @@ -1,16 +1,12 @@ package extension import ( - "bytes" "fmt" "os" "path/filepath" "strings" - "github.com/yuin/goldmark" - goldmarkExtension "github.com/yuin/goldmark/extension" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/renderer/html" + "github.com/shopware/shopware-cli/internal/markdown" ) func parseMarkdownChangelogInPath(path string) (map[string]map[string]string, error) { @@ -64,14 +60,12 @@ func parseMarkdownChangelog(content string) (map[string]string, error) { versions[currentVersion] = versionText for key, changelog := range versions { - var buf bytes.Buffer - - err := GetConfiguredGoldMark().Convert([]byte(changelog), &buf) + html, err := markdown.ToHTML([]byte(changelog)) if err != nil { return nil, err } - versions[key] = buf.String() + versions[key] = html } return versions, nil @@ -121,16 +115,3 @@ func parseExtensionMarkdownChangelog(ext Extension) (*ExtensionChangelog, error) return &ExtensionChangelog{German: changelogDeVersion, English: changelogEnVersion, Changelogs: allChangelogsInVersion}, nil } - -func GetConfiguredGoldMark() goldmark.Markdown { - return goldmark.New( - goldmark.WithExtensions(goldmarkExtension.GFM), - goldmark.WithParserOptions( - parser.WithAutoHeadingID(), - ), - goldmark.WithRendererOptions( - html.WithHardWraps(), - html.WithXHTML(), - ), - ) -} diff --git a/internal/markdown/heading_renderer.go b/internal/markdown/heading_renderer.go new file mode 100644 index 00000000..e18df0e2 --- /dev/null +++ b/internal/markdown/heading_renderer.go @@ -0,0 +1,42 @@ +package markdown + +import ( + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/util" +) + +// spanHeadingRenderer renders Markdown headings as elements +// instead of real

-

tags. +// +// The HTML produced by this package is pushed to the Shopware Store +// (extension description, installation manual and changelog) where it is +// embedded inside a store page that already owns its heading hierarchy +// (the

is the product title). Emitting real headings for this description +// chrome would inject competing headings and harm the page's SEO structure, so +// we keep the visual hierarchy via CSS classes (class="h1" ... "h6") without the +// heading semantics, matching the Shopware Storefront SEO guideline. +type spanHeadingRenderer struct{} + +func (r *spanHeadingRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(ast.KindHeading, r.renderHeading) +} + +func (r *spanHeadingRenderer) renderHeading(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + n := node.(*ast.Heading) + + if entering { + _, _ = w.WriteString(`') + } else { + _, _ = w.WriteString("\n") + } + + return ast.WalkContinue, nil +} diff --git a/internal/markdown/heading_renderer_test.go b/internal/markdown/heading_renderer_test.go new file mode 100644 index 00000000..11db7339 --- /dev/null +++ b/internal/markdown/heading_renderer_test.go @@ -0,0 +1,45 @@ +package markdown + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func renderMarkdown(t *testing.T, source string) string { + t.Helper() + + html, err := ToHTML([]byte(source)) + assert.NoError(t, err) + + return html +} + +func TestMarkdownHeadingsRenderedAsSpans(t *testing.T) { + html := renderMarkdown(t, "# Title\n\n## Subtitle\n\nText") + + // Headings must not emit real

-

tags so they do not compete with + // the store page heading hierarchy. + assert.NotContains(t, html, "") + assert.Contains(t, html, "Subtitle") +} + +func TestMarkdownAllHeadingLevelsRenderedAsSpans(t *testing.T) { + html := renderMarkdown(t, "# A\n\n## B\n\n### C\n\n#### D\n\n##### E\n\n###### F") + + for _, level := range []string{"h1", "h2", "h3", "h4", "h5", "h6"} { + assert.Contains(t, html, `bold") + assert.Contains(t, html, `link`) +} diff --git a/internal/markdown/markdown.go b/internal/markdown/markdown.go new file mode 100644 index 00000000..3e2902ea --- /dev/null +++ b/internal/markdown/markdown.go @@ -0,0 +1,48 @@ +// Package markdown renders Markdown to HTML for content that is pushed to the +// Shopware Store (extension description, installation manual and changelog). +// +// The generated HTML is embedded inside a store page that already owns its +// heading hierarchy, so the renderer is configured to follow the Shopware +// Storefront SEO guidelines (see heading_renderer.go). +package markdown + +import ( + "bytes" + + "github.com/yuin/goldmark" + goldmarkExtension "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/util" +) + +// New returns a goldmark instance configured for rendering Markdown that is +// pushed to the Shopware Store. +func New() goldmark.Markdown { + return goldmark.New( + goldmark.WithExtensions(goldmarkExtension.GFM), + goldmark.WithParserOptions( + parser.WithAutoHeadingID(), + ), + goldmark.WithRendererOptions( + html.WithHardWraps(), + html.WithXHTML(), + // Override heading rendering so Markdown headings become + // instead of competing

-

tags in the + // store page. Priority < 1000 makes it win over the default + // HTML renderer. + renderer.WithNodeRenderers(util.Prioritized(&spanHeadingRenderer{}, 100)), + ), + ) +} + +// ToHTML converts Markdown content to HTML using the store-configured renderer. +func ToHTML(content []byte) (string, error) { + var buf bytes.Buffer + if err := New().Convert(content, &buf); err != nil { + return "", err + } + + return buf.String(), nil +}