Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add superscript and subscript extensions #13

Closed
wants to merge 6 commits into from
Closed
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
5 changes: 5 additions & 0 deletions subscript/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/gohugoio/hugo-goldmark-extensions/subscript

go 1.21.6

require github.com/yuin/goldmark v1.7.0
2 changes: 2 additions & 0 deletions subscript/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
146 changes: 146 additions & 0 deletions subscript/subscript.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package subscript

import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)

// A Subscript struct represents a subscript text segment.
type Subscript struct {
ast.BaseInline
}

// Dump implements Node.Dump.
func (n *Subscript) Dump(source []byte, level int) {
ast.DumpHelper(n, source, level, nil, nil)
}

// Kind is a NodeKind of the Subscript node.
var Kind = ast.NewNodeKind("Subscript")

// Kind implements Node.Kind.
func (n *Subscript) Kind() ast.NodeKind {
return Kind
}

// New returns a new Subscript node.
func New() *Subscript {
return &Subscript{}
}

type delimiterProcessor struct {
}

func (p *delimiterProcessor) IsDelimiter(b byte) bool {
return b == '~'
}

func (p *delimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool {
return opener.Char == closer.Char
}

func (p *delimiterProcessor) OnMatch(int) ast.Node {
return New()
}

var defaultDelimiterProcessor = &delimiterProcessor{}

type Parser struct {
}

var defaultParser = &Parser{}

// NewParser returns a new InlineParser that parses subscript expressions.
func NewParser() parser.InlineParser {
return defaultParser
}

func (s *Parser) Trigger() []byte {
return []byte{'~'}
}

func (s *Parser) Parse(_ ast.Node, block text.Reader, pc parser.Context) ast.Node {
before := block.PrecendingCharacter()
line, segment := block.PeekLine()
node := parser.ScanDelimiter(line, before, 1, defaultDelimiterProcessor)
if node == nil {
return nil
}
if node.CanOpen {
for i := 1; i < len(line); i++ {
c := line[i]
if c == line[0] { // Found closing match
break
}
if util.IsSpace(c) {
return nil
} // No ordinary whitespaces allowed
}
}
node.Segment = segment.WithStop(segment.Start + node.OriginalLength)
block.Advance(node.OriginalLength)
pc.PushDelimiter(node)
return node
}

func (s *Parser) CloseBlock() {
// nothing to do
}

// HTMLRenderer is a renderer.NodeRenderer implementation that renders
// Subscript nodes.
type HTMLRenderer struct {
html.Config
}

// NewHTMLRenderer returns a new HTMLRenderer.
func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
r := &HTMLRenderer{
Config: html.NewConfig(),
}
for _, opt := range opts {
opt.SetHTMLOption(&r.Config)
}
return r
}

// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(Kind, r.render)
}

// AttributeFilter defines attribute names which dd elements can have.
var AttributeFilter = html.GlobalAttributeFilter

func (r *HTMLRenderer) render(
w util.BufWriter, _ []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
if n.Attributes() != nil {
_, _ = w.WriteString("<sub")
html.RenderAttributes(w, n, AttributeFilter)
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("<sub>")
}
} else {
_, _ = w.WriteString("</sub>")
}
return ast.WalkContinue, nil
}

// The Extension allows you to use a subscript expression like 'H~2~O'.
var Extension = &Subscript{}

func (n *Subscript) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithInlineParsers(
util.Prioritized(NewParser(), 501),
))
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(NewHTMLRenderer(), 501),
))
}
66 changes: 66 additions & 0 deletions subscript/subscript_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package subscript

import (
"bytes"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/text"
"testing"

"github.com/yuin/goldmark"
"github.com/yuin/goldmark/testutil"
)

func buildTestParser() goldmark.Markdown {
markdown := goldmark.New(
goldmark.WithExtensions(
Extension, extension.Strikethrough,
),
)
return markdown
}

func Test(t *testing.T) {
markdown := buildTestParser()
testutil.DoTestCaseFile(markdown, "testCases.txt", t, testutil.ParseCliCaseArg()...)
}

func TestDump(t *testing.T) {
input := "**H~2~O** or simply water."
markdown := goldmark.New(
goldmark.WithExtensions(
Extension,
),
)
root := markdown.Parser().Parse(text.NewReader([]byte(input)))
root.Dump([]byte(input), 0)
// Prints to stdout, so just test that it doesn't crash
}

func BenchmarkWithAndWithoutOneSubscript(b *testing.B) {
const input = `
## Water formula

The chemical formula for water H~2~O contains one subscript.`

b.Run("without subscript", func(b *testing.B) {
markdown := goldmark.New()
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 subscript", func(b *testing.B) {
markdown := buildTestParser()
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)
}
}
})
}
56 changes: 56 additions & 0 deletions subscript/testCases.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
1
//- - - - - - - - -//
~foo~
//- - - - - - - - -//
<p><sub>foo</sub></p>
//= = = = = = = = = = = = = = = = = = = = = = = =//

2
//- - - - - - - - -//
H~2~O
//- - - - - - - - -//
<p>H<sub>2</sub>O</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//

3
//- - - - - - - - -//
x~i~ + x~j~
//- - - - - - - - -//
<p>x<sub>i</sub> + x<sub>j</sub></p>
//= = = = = = = = = = = = = = = = = = = = = = = =//

4
//- - - - - - - - -//
a~4 + a~5
//- - - - - - - - -//
<p>a~4 + a~5</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//

5
//- - - - - - - - -//
~foo\~
//- - - - - - - - -//
<p>~foo~</p>
//= = = = = = = = = = = = = = = = = = = = = = = =//

6
//- - - - - - - - -//
~foo&nbsp;bar~
//- - - - - - - - -//
<p><sub>foo bar</sub></p>
//= = = = = = = = = = = = = = = = = = = = = = = =//

7
//- - - - - - - - -//
~foo bar~
//- - - - - - - - -//
<p><sub>foo bar</sub></p>
//= = = = = = = = = = = = = = = = = = = = = = = =//


8
//- - - - - - - - -//
~~x~foobar~~~
//- - - - - - - - -//
<p><del>x<sub>foobar</sub></del></p>
//= = = = = = = = = = = = = = = = = = = = = = = =//
5 changes: 5 additions & 0 deletions superscript/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/gohugoio/hugo-goldmark-extensions/superscript

go 1.21.6

require github.com/yuin/goldmark v1.7.0
2 changes: 2 additions & 0 deletions superscript/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
Loading