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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
markup/goldmark: Add goldmark-cjk-friendly extension
Add `goldmark-cjk-friendly` extension which enhances handling of CJK emphasis and strikethrough.

Fixes #14114
  • Loading branch information
Martin005 committed Dec 9, 2025
commit 48c8ec036398880a67c5ffeeb94df69771fefa88
2 changes: 2 additions & 0 deletions docs/content/en/configuration/markup.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ The extensions below, excluding Extras and Passthrough, are enabled by default.
Extension|Documentation|Enabled
:--|:--|:-:
`cjk`|[Goldmark Extensions: CJK]|:heavy_check_mark:
`cjkFriendly`|[Goldmark Extensions: CJK Friendly]| 
`definitionList`|[PHP Markdown Extra: Definition lists]|:heavy_check_mark:
`extras`|[Hugo Goldmark Extensions: Extras]| 
`footnote`|[PHP Markdown Extra: Footnotes]|:heavy_check_mark:
Expand Down Expand Up @@ -337,6 +338,7 @@ ordered
[GitHub Flavored Markdown: Task list items]: https://github.github.com/gfm/#task-list-items-extension-
[GitHub Flavored Markdown]: https://github.github.com/gfm/
[Goldmark Extensions: CJK]: https://github.com/yuin/goldmark?tab=readme-ov-file#cjk-extension
[Goldmark Extensions: CJK Friendly]: https://github.com/tats-u/goldmark-cjk-friendly
[Goldmark Extensions: Typographer]: https://github.com/yuin/goldmark?tab=readme-ov-file#typographer-extension
[Goldmark]: https://github.com/yuin/goldmark/
[Hugo Goldmark Extensions: Extras]: https://github.com/gohugoio/hugo-goldmark-extensions?tab=readme-ov-file#extras-extension
Expand Down
2 changes: 2 additions & 0 deletions docs/data/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,8 @@ config:
eastAsianLineBreaksStyle: simple
enable: false
escapedSpace: false
cjkFriendly:
emphasis: false
definitionList: true
extras:
delete:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ require (
github.com/spf13/cobra v1.10.2
github.com/spf13/fsync v0.10.1
github.com/spf13/pflag v1.0.9
github.com/tats-u/goldmark-cjk-friendly/v2 v2.0.2
github.com/tdewolff/minify/v2 v2.24.8
github.com/tdewolff/parse/v2 v2.8.5
github.com/tetratelabs/wazero v1.10.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tats-u/goldmark-cjk-friendly/v2 v2.0.2 h1:+N9clTED51Tt55ljeXYXufmqS8wUXpLJW771aIGIRxo=
github.com/tats-u/goldmark-cjk-friendly/v2 v2.0.2/go.mod h1:1Wm+kJwLMq/sr22CCei+eN593nanPDVn1eH91QdVPEI=
github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE=
github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw=
github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU=
Expand Down
11 changes: 10 additions & 1 deletion markup/goldmark/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
"github.com/gohugoio/hugo/markup/goldmark/passthrough"
"github.com/gohugoio/hugo/markup/goldmark/tables"
cjkFriendly "github.com/tats-u/goldmark-cjk-friendly/v2"
"github.com/yuin/goldmark/util"

"github.com/yuin/goldmark"
Expand Down Expand Up @@ -153,7 +154,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
extensions = append(extensions, tables.New())
}

if cfg.Extensions.Strikethrough {
if cfg.Extensions.Strikethrough && !cfg.Extensions.CJKFriendly.Emphasis {
extensions = append(extensions, extension.Strikethrough)
}

Expand Down Expand Up @@ -207,6 +208,14 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
extensions = append(extensions, c)
}

if cfg.Extensions.CJKFriendly.Emphasis {
if cfg.Extensions.Strikethrough {
extensions = append(extensions, cjkFriendly.CJKFriendlyEmphasisAndStrikethrough)
} else {
extensions = append(extensions, cjkFriendly.CJKFriendlyEmphasis)
}
}

if cfg.Extensions.Passthrough.Enable {
extensions = append(extensions, passthrough.New(cfg.Extensions.Passthrough))
}
Expand Down
134 changes: 134 additions & 0 deletions markup/goldmark/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,140 @@ escapedSpace=true
c.Assert(got, qt.Contains, "<p>私は太郎です。\nプログラミングが好きです。運動が苦手です。</p>\n")
}

func TestConvertCJKFriendlyWithExtensionEmphasisWithExtensionStrikethrough(t *testing.T) {
c := qt.New(t)

content := "Git **(ギット)**Hub\n~~(真美好)~~"

tests := []struct {
name string
strikethrough bool
cjkFriendlyEmphasis bool
expect string
}{
{"noFriendly_noStrike", false, false, "<p>Git **(ギット)**Hub\n~~(真美好)~~</p>\n"},
{"friendly_noStrike", false, true, "<p>Git <strong>(ギット)</strong>Hub\n~~(真美好)~~</p>\n"},
{"noFriendly_strike", true, false, "<p>Git **(ギット)**Hub\n<del>(真美好)</del></p>\n"},
{"friendly_strike", true, true, "<p>Git <strong>(ギット)</strong>Hub\n<del>(真美好)</del></p>\n"},
}

for _, tt := range tests {
c.Run(tt.name, func(c *qt.C) {
confStr := fmt.Sprintf(`
[markup]
[markup.goldmark]
[markup.goldmark.extensions]
strikethrough=%v
[markup.goldmark.extensions.CJKFriendly]
emphasis=%v
`, tt.strikethrough, tt.cjkFriendlyEmphasis)

cfg := config.FromTOMLConfigString(confStr)
conf := testconfig.GetTestConfig(nil, cfg)

b := convert(c, conf, content)
got := string(b.Bytes())

c.Assert(got, qt.Contains, tt.expect)
})
}
}

func TestConvertCJKFriendlyWithExtensionEmphasisWithExtensionStrikethroughWithCJKEscapedSpace(t *testing.T) {
c := qt.New(t)

content := "a\\ **()**\\ a𩸽**()**𩸽~~(真美好)~~"

tests := []struct {
name string
strikethrough bool
escapedSpace bool
cjkFriendlyEmphasis bool
expect string
}{
{
name: "noFriendly_noStrike_noEscaped",
strikethrough: false,
escapedSpace: false,
cjkFriendlyEmphasis: false,
expect: "<p>a\\ <strong>()</strong>\\ a𩸽**()**𩸽~~(真美好)~~</p>\n",
},
{
name: "noFriendly_noStrike_escaped",
strikethrough: false,
escapedSpace: true,
cjkFriendlyEmphasis: false,
expect: "<p>a<strong>()</strong>a𩸽**()**𩸽~~(真美好)~~</p>\n",
},
{
name: "noFriendly_strike_noEscaped",
strikethrough: true,
escapedSpace: false,
cjkFriendlyEmphasis: false,
expect: "<p>a\\ <strong>()</strong>\\ a𩸽**()**𩸽~~(真美好)~~</p>\n",
},
{
name: "noFriendly_strike_escaped",
strikethrough: true,
escapedSpace: true,
cjkFriendlyEmphasis: false,
expect: "<p>a<strong>()</strong>a𩸽**()**𩸽~~(真美好)~~</p>\n",
},
{
name: "friendly_noStrike_noEscaped",
strikethrough: false,
escapedSpace: false,
cjkFriendlyEmphasis: true,
expect: "<p>a\\ <strong>()</strong>\\ a𩸽<strong>()</strong>𩸽~~(真美好)~~</p>\n",
},
{
name: "friendly_noStrike_escaped",
strikethrough: false,
escapedSpace: true,
cjkFriendlyEmphasis: true,
expect: "<p>a<strong>()</strong>a𩸽<strong>()</strong>𩸽~~(真美好)~~</p>\n",
},
{
name: "friendly_strike_noEscaped",
strikethrough: true,
escapedSpace: false,
cjkFriendlyEmphasis: true,
expect: "<p>a\\ <strong>()</strong>\\ a𩸽<strong>()</strong>𩸽<del>(真美好)</del></p>\n",
},
{
name: "friendly_strike_escaped",
strikethrough: true,
escapedSpace: true,
cjkFriendlyEmphasis: true,
expect: "<p>a<strong>()</strong>a𩸽<strong>()</strong>𩸽<del>(真美好)</del></p>\n",
},
}

for _, tt := range tests {
c.Run(tt.name, func(c *qt.C) {
confStr := fmt.Sprintf(`
[markup]
[markup.goldmark]
[markup.goldmark.extensions]
strikethrough=%v
[markup.goldmark.extensions.CJK]
enable=true
escapedSpace=%v
[markup.goldmark.extensions.CJKFriendly]
emphasis=%v
`, tt.strikethrough, tt.escapedSpace, tt.cjkFriendlyEmphasis)

cfg := config.FromTOMLConfigString(confStr)
conf := testconfig.GetTestConfig(nil, cfg)

b := convert(c, conf, content)
got := string(b.Bytes())

c.Assert(got, qt.Contains, tt.expect)
})
}
}

type tableRenderer int

func (hr tableRenderer) RenderTable(cctx context.Context, w hugio.FlexiWriter, ctx hooks.TableContext) error {
Expand Down
9 changes: 9 additions & 0 deletions markup/goldmark/goldmark_config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ var Default = Config{
EastAsianLineBreaksStyle: "simple",
EscapedSpace: false,
},
CJKFriendly: CJKFriendly{
Emphasis: false,
},
Extras: Extras{
Delete: Delete{
Enable: false,
Expand Down Expand Up @@ -168,6 +171,7 @@ type Extensions struct {
LinkifyProtocol string
TaskList bool
CJK CJK
CJKFriendly CJKFriendly `json:"cjkFriendly"`
}

// Footnote holds footnote configuration.
Expand Down Expand Up @@ -270,6 +274,11 @@ type CJK struct {
EscapedSpace bool
}

type CJKFriendly struct {
// Emphasis adds support for CJK-friendly 'emphasis'. If "strikethrough" goldmark extension is enabled as well, CJK-friendly 'emphasis and strikethrough' will be used.
Emphasis bool
}

type Renderer struct {
// Whether softline breaks should be rendered as '<br>'
HardWraps bool
Expand Down
130 changes: 130 additions & 0 deletions markup/goldmark/goldmark_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,136 @@ H~2~0
)
}

func TestExtrasExtensionWithCJKFriendlyEmphasis(t *testing.T) {
t.Parallel()

files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
[markup.goldmark.extensions]
strikethrough = false
[markup.goldmark.extensions.extras.delete]
enable = false
[markup.goldmark.extensions.extras.insert]
enable = false
[markup.goldmark.extensions.extras.mark]
enable = false
[markup.goldmark.extensions.extras.subscript]
enable = false
[markup.goldmark.extensions.extras.superscript]
enable = false
[markup.goldmark.extensions.CJKFriendly]
emphasis = false
-- layouts/index.html --
{{ .Content }}
-- content/_index.md --
---
title: home
---
~~削除~~

++挿入++

==マーク==

(H~2~O)

面積は1^乗^です

(~~削除~~)

~~(挿入)~~

混合 **(強)**~~削除~~

空 ~~ ~~

++CJK**「ハロー」**Test++

Mark**==マーク==**Test

MarkParen**(==マーク==)**Test
`

b := hugolib.Test(t, files)

// CJKFriendlyEmphasis disabled, Extras disabled.
b.AssertFileContent("public/index.html",
"<p>~~削除~~</p>",
"<p>++挿入++</p>",
"<p>==マーク==</p>",
"<p>(H~2~O)</p>",
"<p>面積は1^乗^です</p>",
"<p>(~~削除~~)</p>",
"<p>~~(挿入)~~</p>",
"<p>混合 <strong>(強)</strong>~~削除~~</p>",
"<p>空 ~~ ~~</p>",
"<p>++CJK**「ハロー」**Test++</p>",
"<p>Mark**==マーク==**Test</p>",
"<p>MarkParen**(==マーク==)**Test</p>",
)

files = strings.ReplaceAll(files, "enable = false", "enable = true")

b = hugolib.Test(t, files)

// CJKFriendlyEmphasis disabled, Extras enabled.
b.AssertFileContent("public/index.html",
"<p><del>削除</del></p>",
"<p><ins>挿入</ins></p>",
"<p><mark>マーク</mark></p>",
"<p>(H<sub>2</sub>O)</p>",
"<p>面積は1<sup>乗</sup>です</p>",
"<p>(<del>削除</del>)</p>",
"<p><del>(挿入)</del></p>",
"<p>混合 <strong>(強)</strong><del>削除</del></p>",
"<p>空 ~~ ~~</p>",
"<p><ins>CJK**「ハロー」**Test</ins></p>",
"<p>Mark**<mark>マーク</mark>**Test</p>",
"<p>MarkParen**(<mark>マーク</mark>)**Test</p>",
)

filesCJKEnabled := strings.Replace(files, "emphasis = false", "emphasis = true", 1)

b = hugolib.Test(t, filesCJKEnabled)

// CJKFriendlyEmphasis enabled, Extras enabled.
b.AssertFileContent("public/index.html",
"<p><del>削除</del></p>",
"<p><ins>挿入</ins></p>",
"<p><mark>マーク</mark></p>",
"<p>(H<sub>2</sub>O)</p>",
"<p>面積は1<sup>乗</sup>です</p>",
"<p>(<del>削除</del>)</p>",
"<p><del>(挿入)</del></p>",
"<p>混合 <strong>(強)</strong><del>削除</del></p>",
"<p>空 ~~ ~~</p>",
"<p><ins>CJK<strong>「ハロー」</strong>Test</ins></p>",
"<p>Mark**<mark>マーク</mark>**Test</p>",
"<p>MarkParen<strong>(<mark>マーク</mark>)</strong>Test</p>",
)

filesCJKEnabledExtrasDisabled := strings.ReplaceAll(filesCJKEnabled, "enable = true", "enable = false")

b = hugolib.Test(t, filesCJKEnabledExtrasDisabled)

// CJKFriendlyEmphasis enabled, Extras disabled.
b.AssertFileContent("public/index.html",
"<p>~~削除~~</p>",
"<p>++挿入++</p>",
"<p>==マーク==</p>",
"<p>(H~2~O)</p>",
"<p>面積は1^乗^です</p>",
"<p>(~~削除~~)</p>",
"<p>~~(挿入)~~</p>",
"<p>混合 <strong>(強)</strong>~~削除~~</p>",
"<p>空 ~~ ~~</p>",
"<p>++CJK<strong>「ハロー」</strong>Test++</p>",
"<p>Mark**==マーク==**Test</p>",
"<p>MarkParen<strong>(==マーク==)</strong>Test</p>",
)
}

// Issue 12997.
func TestGoldmarkRawHTMLWarningBlocks(t *testing.T) {
files := `
Expand Down