Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions cmd/account/account_producer_extension_info_push.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package account

import (
"bytes"
"context"
"fmt"
"os"
Expand All @@ -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"
)

Expand Down Expand Up @@ -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 {
Expand Down
25 changes: 3 additions & 22 deletions internal/extension/changelog.go
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
),
)
}
42 changes: 42 additions & 0 deletions internal/markdown/heading_renderer.go
Original file line number Diff line number Diff line change
@@ -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 <span class="hN"> elements
// instead of real <h1>-<h6> 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 <h1> 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(`<span class="h`)
_ = w.WriteByte("0123456"[n.Level])
_ = w.WriteByte('"')
if n.Attributes() != nil {
html.RenderAttributes(w, node, html.HeadingAttributeFilter)
}
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("</span>\n")
}

return ast.WalkContinue, nil
}
45 changes: 45 additions & 0 deletions internal/markdown/heading_renderer_test.go
Original file line number Diff line number Diff line change
@@ -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 <h1>-<h6> tags so they do not compete with
// the store page heading hierarchy.
assert.NotContains(t, html, "<h1")
assert.NotContains(t, html, "<h2")
assert.Contains(t, html, `<span class="h1"`)
assert.Contains(t, html, `<span class="h2"`)
assert.Contains(t, html, "Title</span>")
assert.Contains(t, html, "Subtitle</span>")
}

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, `<span class="`+level+`"`)
assert.NotContains(t, html, "<"+level)
}
}

func TestMarkdownNonHeadingContentUnchanged(t *testing.T) {
html := renderMarkdown(t, "Some **bold** text with a [link](https://example.com).")

assert.Contains(t, html, "<strong>bold</strong>")
assert.Contains(t, html, `<a href="https://example.com">link</a>`)
}
48 changes: 48 additions & 0 deletions internal/markdown/markdown.go
Original file line number Diff line number Diff line change
@@ -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
// <span class="hN"> instead of competing <h1>-<h6> 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
}
Loading