From 4d4fcd022fe45a9b51483df001c9e5f4e632d5a9 Mon Sep 17 00:00:00 2001 From: Georg Makowski Date: Mon, 17 Jun 2024 14:56:12 +0200 Subject: [PATCH] extras: Implement delete extension With Goldmark v1.7.1 and earlier, the Goldmark "strikethrough" extension was triggered by wrapping text within a pair of double-tilde characters. With Goldmark v1.7.2 and later, to provide full GFM compatibility, the Goldmark "strikethrough" extension is triggered by wrapping text within a pair of single- or double-tilde characters. This change created a conflict with the Hugo Goldmark Extras "subscript" extension. When enabling the Hugo Goldmark Extras "subscript" extension, if you want to render subscript and strikethrough text concurrently, you must: 1. Disable the Goldmark "strikethrough" extension 2. Enable the Hugo Goldmark Extras "delete" extension --- extras/_test/delete.txt | 18 +++++++++++ extras/_test/insert.txt | 35 +++++----------------- extras/_test/mark.txt | 23 +------------- extras/_test/subscript.txt | 25 ++++++++++------ extras/_test/superscript.txt | 21 +++++-------- extras/ast.go | 11 +++++++ extras/inline.go | 30 ++++++------------- extras/inline_test.go | 58 ++++++++++++++++++++++++++++++------ 8 files changed, 118 insertions(+), 103 deletions(-) create mode 100644 extras/_test/delete.txt diff --git a/extras/_test/delete.txt b/extras/_test/delete.txt new file mode 100644 index 0000000..dbb48f6 --- /dev/null +++ b/extras/_test/delete.txt @@ -0,0 +1,18 @@ +1 +//- - - - - - - - -// +~~Hi~~ Hello, world! +//- - - - - - - - -// +

Hi Hello, world!

+//= = = = = = = = = = = = = = = = = = = = = = = =// + + + +2 +//- - - - - - - - -// +This ~~has a + +new paragraph~~. +//- - - - - - - - -// +

This ~~has a

+

new paragraph~~.

+//= = = = = = = = = = = = = = = = = = = = = = = =// diff --git a/extras/_test/insert.txt b/extras/_test/insert.txt index c3d50ef..73b1937 100644 --- a/extras/_test/insert.txt +++ b/extras/_test/insert.txt @@ -1,43 +1,22 @@ 1 //- - - - - - - - -// -++Hi++ Hello, world! +Hello ++,++ world! //- - - - - - - - -// -

Hi Hello, world!

+

Hello , world!

//= = = = = = = = = = = = = = = = = = = = = = = =// 2 //- - - - - - - - -// -This ++has a +This ++expression is stretched over a -new paragraph++. +paragraph++. //- - - - - - - - -// -

This ++has a

-

new paragraph++.

+

This ++expression is stretched over a

+

paragraph++.

//= = = = = = = = = = = = = = = = = = = = = = = =// 3 //- - - - - - - - -// -x ++++foo++ bar++ -//- - - - - - - - -// -

x foo bar

-//= = = = = = = = = = = = = = = = = = = = = = = =// - -4 -//- - - - - - - - -// -x ++foo ++bar++++ -//- - - - - - - - -// -

x foo bar

-//= = = = = = = = = = = = = = = = = = = = = = = =// - -5 -//- - - - - - - - -// -x ++++foo++++ -//- - - - - - - - -// -

x foo

-//= = = = = = = = = = = = = = = = = = = = = = = =// - -6 -//- - - - - - - - -// **++test**++ ++**test++** @@ -46,7 +25,7 @@ x ++++foo++++

**test**

//= = = = = = = = = = = = = = = = = = = = = = = =// -7 +4 //- - - - - - - - -// [++link]()++ diff --git a/extras/_test/mark.txt b/extras/_test/mark.txt index a3261ad..f3e7954 100644 --- a/extras/_test/mark.txt +++ b/extras/_test/mark.txt @@ -17,27 +17,6 @@ new paragraph==. 3 //- - - - - - - - -// -x ====foo== bar== -//- - - - - - - - -// -

x foo bar

-//= = = = = = = = = = = = = = = = = = = = = = = =// - -4 -//- - - - - - - - -// -x ==foo ==bar==== -//- - - - - - - - -// -

x foo bar

-//= = = = = = = = = = = = = = = = = = = = = = = =// - -5 -//- - - - - - - - -// -x ====foo==== -//- - - - - - - - -// -

x foo

-//= = = = = = = = = = = = = = = = = = = = = = = =// - -6 -//- - - - - - - - -// **==test**== ==**test==** @@ -46,7 +25,7 @@ x ====foo====

**test**

//= = = = = = = = = = = = = = = = = = = = = = = =// -7 +4 //- - - - - - - - -// [==link]()== diff --git a/extras/_test/subscript.txt b/extras/_test/subscript.txt index 59f3ebb..ad83e94 100644 --- a/extras/_test/subscript.txt +++ b/extras/_test/subscript.txt @@ -40,23 +40,30 @@ x~i~ + x~j~

foo bar

//= = = = = = = = = = = = = = = = = = = = = = = =// -7: Mixed subscript and strikethrough -//- - - - - - - - -// -~~x~foobar~~~ -//- - - - - - - - -// -

xfoobar

-//= = = = = = = = = = = = = = = = = = = = = = = =// - -8: Tildes in the middle and text before +7: Tildes in the middle and text before //- - - - - - - - -// text H~2~O //- - - - - - - - -//

text H2O

//= = = = = = = = = = = = = = = = = = = = = = = =// -9: Tildes in the middle and text after +8: Tildes in the middle and text after //- - - - - - - - -// H~2~O text //- - - - - - - - -//

H2O text

//= = = = = = = = = = = = = = = = = = = = = = = =// + +9: Check for a direct conflict with strikethrough +//- - - - - - - - -// +~~foobar~~ +//- - - - - - - - -// +

foobar

+//= = = = = = = = = = = = = = = = = = = = = = = =// + +10: Mixed subscript and strikethrough will not work as expected! +//- - - - - - - - -// +~~x~foobar~~~ +//- - - - - - - - -// +

~~x~foobar~~~

+//= = = = = = = = = = = = = = = = = = = = = = = =// diff --git a/extras/_test/superscript.txt b/extras/_test/superscript.txt index 7bf06bf..764cfc2 100644 --- a/extras/_test/superscript.txt +++ b/extras/_test/superscript.txt @@ -1,4 +1,4 @@ -1: Surrounded by cares +1: Surrounded by carets //- - - - - - - - -// ^foo^ //- - - - - - - - -// @@ -19,49 +19,42 @@ x^2^ + x^5^

x2 + x5

//= = = = = = = = = = = = = = = = = = = = = = = =// -4: Escaped care +4: Escaped caret //- - - - - - - - -// ^foo\^ //- - - - - - - - -//

^foo^

//= = = = = = = = = = = = = = = = = = = = = = = =// -5: : Cares surround text with a non-breaking space entity +5: Carets surround text with a non-breaking space entity //- - - - - - - - -// ^foo bar^ //- - - - - - - - -//

foo bar

//= = = = = = = = = = = = = = = = = = = = = = = =// -6: Cares surround text Surround with a non-breaking space (UTF-8) +6: Carets surround text Surround with a non-breaking space (UTF-8) //- - - - - - - - -// ^foo bar^ //- - - - - - - - -//

foo bar

//= = = = = = = = = = = = = = = = = = = = = = = =// -7: Cares in the middle and text before +7: Carets in the middle and text before //- - - - - - - - -// text C^foo^C //- - - - - - - - -//

text CfooC

//= = = = = = = = = = = = = = = = = = = = = = = =// -8: Cares in the middle and text after +8: Carets in the middle and text after //- - - - - - - - -// C^foo^C text //- - - - - - - - -//

CfooC text

//= = = = = = = = = = = = = = = = = = = = = = = =// -9: Wrong markers in LaTeX style notation should be left untouched -//- - - - - - - - -// -x^2 + x^3 -//- - - - - - - - -// -

x^2 + x^3

-//= = = = = = = = = = = = = = = = = = = = = = = =// - -10: Footnote markers should be left untouched +9: Footnote markers should be left untouched //- - - - - - - - -// text[^1] text[^2] //- - - - - - - - -// diff --git a/extras/ast.go b/extras/ast.go index c790bde..9ddaeed 100644 --- a/extras/ast.go +++ b/extras/ast.go @@ -54,6 +54,16 @@ var markTag = inlineTag{ RenderPriority: 550, } +var deleteTag = inlineTag{ + TagKind: kindDelete, + Char: '~', + Number: 2, + Html: "del", + WhitespaceAllowed: false, + ParsePriority: 400, + RenderPriority: 400, +} + type inlineTagNode struct { ast.BaseInline @@ -73,6 +83,7 @@ var ( kindSubscript = ast.NewNodeKind("Subscript") kindInsert = ast.NewNodeKind("Insert") kindMark = ast.NewNodeKind("Mark") + kindDelete = ast.NewNodeKind("Delete") ) func (n *inlineTagNode) Kind() ast.NodeKind { diff --git a/extras/inline.go b/extras/inline.go index 094121a..60f347e 100644 --- a/extras/inline.go +++ b/extras/inline.go @@ -48,35 +48,15 @@ func (s *inlineTagParser) Parse(_ ast.Node, block text.Reader, pc parser.Context before := block.PrecendingCharacter() line, segment := block.PeekLine() node := parser.ScanDelimiter(line, before, s.Number, newInlineTagDelimiterProcessor(s.inlineTag)) - if node == nil { + if node == nil || node.OriginalLength > 2 || before == rune(s.Char) { return nil } - if !s.WhitespaceAllowed && node.CanOpen && hasSpace(line) { - if !(node.CanClose && pc.LastDelimiter() != nil && pc.LastDelimiter().Char == node.Char) { - return nil - } - } node.Segment = segment.WithStop(segment.Start + node.OriginalLength) block.Advance(node.OriginalLength) pc.PushDelimiter(node) return node } -// Check if there is an ordinary white space in the line before the next marker -func hasSpace(line []byte) bool { - marker := line[0] - for i := 1; i < len(line); i++ { - c := line[i] - if c == marker { - break - } - if util.IsSpace(c) { - return true - } - } - return false -} - type inlineTagHTMLRenderer struct { htmlTag string tagKind ast.NodeKind @@ -133,6 +113,7 @@ type Config struct { Subscript SubscriptConfig Insert InsertConfig Mark MarkConfig + Delete DeleteConfig } // SuperscriptConfig configures the superscript extension. @@ -155,6 +136,10 @@ type MarkConfig struct { Enable bool } +type DeleteConfig struct { + Enable bool +} + // New returns a new inline tag extension. func New(config Config) goldmark.Extender { @@ -185,4 +170,7 @@ func (tag *inlineExtension) Extend(md goldmark.Markdown) { if tag.conf.Mark.Enable { addTag(markTag) } + if tag.conf.Delete.Enable { + addTag(deleteTag) + } } diff --git a/extras/inline_test.go b/extras/inline_test.go index 9823d3d..41fa958 100644 --- a/extras/inline_test.go +++ b/extras/inline_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/gohugoio/hugo-goldmark-extensions/extras" - "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark" @@ -17,11 +16,17 @@ func buildGoldmarkWithInlineTag(conf extras.Config) goldmark.Markdown { } var ( - markdown = goldmark.New() - markdownWithSuperscript = buildGoldmarkWithInlineTag(extras.Config{Superscript: extras.SuperscriptConfig{Enable: true}}) - markdownWithSubscript = buildGoldmarkWithInlineTag(extras.Config{Subscript: extras.SubscriptConfig{Enable: true}}) - markdownWithInsert = buildGoldmarkWithInlineTag(extras.Config{Insert: extras.InsertConfig{Enable: true}}) - markdownWithMark = buildGoldmarkWithInlineTag(extras.Config{Mark: extras.MarkConfig{Enable: true}}) + markdown = goldmark.New() + markdownWithSuperscript = buildGoldmarkWithInlineTag(extras.Config{Superscript: extras.SuperscriptConfig{Enable: true}}) + markdownWithSubscript = buildGoldmarkWithInlineTag(extras.Config{Subscript: extras.SubscriptConfig{Enable: true}}) + markdownWithInsert = buildGoldmarkWithInlineTag(extras.Config{Insert: extras.InsertConfig{Enable: true}}) + markdownWithMark = buildGoldmarkWithInlineTag(extras.Config{Mark: extras.MarkConfig{Enable: true}}) + markdownWithDelete = buildGoldmarkWithInlineTag(extras.Config{Delete: extras.DeleteConfig{Enable: true}}) + markdownWithDeleteAndSubscript = goldmark.New( + goldmark.WithExtensions( + extras.New(extras.Config{Subscript: extras.SubscriptConfig{Enable: true}}), + extras.New(extras.Config{Delete: extras.DeleteConfig{Enable: true}}), + )) ) func TestSuperscript(t *testing.T) { @@ -62,9 +67,7 @@ This formula contains one superscript: f(x) = x^2^ .` } func TestSubscript(t *testing.T) { - markdown := goldmark.New( - goldmark.WithExtensions(extras.New(extras.Config{Subscript: extras.SubscriptConfig{Enable: true}}), extension.Strikethrough)) - testutil.DoTestCaseFile(markdown, "_test/subscript.txt", t, testutil.ParseCliCaseArg()...) + testutil.DoTestCaseFile(markdownWithDeleteAndSubscript, "_test/subscript.txt", t, testutil.ParseCliCaseArg()...) } func TestSubscriptDump(t *testing.T) { @@ -175,3 +178,40 @@ Add some marked text: ==marked==. Amazing.` } }) } + +func TestDelete(t *testing.T) { + testutil.DoTestCaseFile(markdownWithDelete, "_test/delete.txt", t, testutil.ParseCliCaseArg()...) +} + +func TestDeleteDump(t *testing.T) { + input := "Delete some text: ~~deleted~~. Amazing." + root := markdownWithDelete.Parser().Parse(text.NewReader([]byte(input))) + root.Dump([]byte(input), 0) +} + +func BenchmarkWithAndWithoutDelete(b *testing.B) { + const input = ` +## Delete text + +Delete some text: ~~deleted~~. Amazing.` + + b.Run("without delete extension", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + if err := markdown.Convert([]byte(input), &buf); err != nil { + b.Fatal(err) + } + } + }) + + b.Run("with delete extension", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + var buf bytes.Buffer + if err := markdownWithMark.Convert([]byte(input), &buf); err != nil { + b.Fatal(err) + } + } + }) +}