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
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ log.Print(buf.String()) // # My Document Title
You can control the style of various markdown elements via functional options that are passed to
the renderer.

| Functional Option | Type | Description |
| ----------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
| WithIndentStyle | markdown.IndentStyle | Indent nested blocks with spaces or tabs. |
| WithHeadingStyle | markdown.HeadingStyle | Render markdown headings as ATX (`#`-based), Setext (underlined with `===` or `---`), or variants thereof. |
| WithThematicBreakStyle | markdown.ThematicBreakStyle | Render thematic breaks with `-`, `*`, or `_`. |
| WithThematicBreakLength | markdown.ThematicBreakLength | Number of characters to use in a thematic break (minimum 3). |
| WithNestedListLength | markdown.NestedListLength | Number of characters to use in a nested list indentation (minimum 1). |
| Functional Option | Type | Description |
| ---------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| WithIndentStyle | markdown.IndentStyle | Indent nested blocks with spaces or tabs. |
| WithHeadingStyle | markdown.HeadingStyle | Render markdown headings as ATX (`#`-based), Setext (underlined with `===` or `---`), or variants thereof. |
| WithThematicBreakStyle | markdown.ThematicBreakStyle | Render thematic breaks with `-`, `*`, or `_`. |
| WithThematicBreakLength | markdown.ThematicBreakLength | Number of characters to use in a thematic break (minimum 3). |
| WithNestedListLength | markdown.NestedListLength | Number of characters to use in a nested list indentation (minimum 1). |
| WithTypographerSubstitutions | markdown.TypographerSubstitutions | Whether characters should be substituted by the typographer extension. This setting has no effect unless the typographer extension is enabled. The renderer must be added as an extension (e.g. via `NewExtension`) for this to work. |

## As a markdown transformer

Expand Down
63 changes: 63 additions & 0 deletions extension.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package markdown

import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
)

type rendererExtension struct{ opts []Option }

// NewExtension returns a new goldmark.Markdown extension that uses the markdown renderer.
func NewExtension(opts ...Option) *rendererExtension {
return &rendererExtension{opts: opts}
}

// Extend implements goldmark.Extension.Extend
func (re *rendererExtension) Extend(md goldmark.Markdown) {
renderer := NewRenderer(re.opts...)
md.SetRenderer(renderer)
if renderer.config.TypographerSubstitutions {
enableTypographicSubstitutions(md)
} else {
disableTypographicSubstitutions(md)
}
}

// enableTypographicSubstitutions configures the typographer extension
// to substitute punctuations with the corresponding unicode character,
// instead of the default HTML escape sequence.
// These substitutions are only applied IF the typographer extension is enabled.
func enableTypographicSubstitutions(md goldmark.Markdown) {
subs := make(extension.TypographicSubstitutions)
subs[extension.LeftSingleQuote] = []byte("‘")
subs[extension.RightSingleQuote] = []byte("’")
subs[extension.LeftDoubleQuote] = []byte("“")
subs[extension.RightDoubleQuote] = []byte("”")
subs[extension.EnDash] = []byte("–")
subs[extension.EmDash] = []byte("—")
subs[extension.Ellipsis] = []byte("…")
subs[extension.LeftAngleQuote] = []byte("«")
subs[extension.RightAngleQuote] = []byte("»")
subs[extension.Apostrophe] = []byte("'")

md.Parser().AddOptions(extension.WithTypographicSubstitutions(subs))
}

// disableTypographicSubstitutions configures the typographer extension
// to substitute punctuations with their original values,
// effectively turning typographer into a no-op.
func disableTypographicSubstitutions(md goldmark.Markdown) {
subs := make(extension.TypographicSubstitutions)
subs[extension.LeftSingleQuote] = []byte("'")
subs[extension.RightSingleQuote] = []byte("'")
subs[extension.LeftDoubleQuote] = []byte("\"")
subs[extension.RightDoubleQuote] = []byte("\"")
subs[extension.EnDash] = []byte("--")
subs[extension.EmDash] = []byte("---")
subs[extension.Ellipsis] = []byte("...")
subs[extension.LeftAngleQuote] = []byte("<<")
subs[extension.RightAngleQuote] = []byte(">>")
subs[extension.Apostrophe] = []byte("'")

md.Parser().AddOptions(extension.WithTypographicSubstitutions(subs))
}
67 changes: 67 additions & 0 deletions extension_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package markdown

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
)

func TestTypographerExtensionDisabled(t *testing.T) {
assert := assert.New(t)
md := goldmark.New(
goldmark.WithExtensions(NewExtension(), extension.NewTypographer()),
)
buf := bytes.Buffer{}
source := `'LeftSingleQuote
RightSingleQuote'
"LeftDoubleQuote
RightDoubleQuote"
EnDash --
EmDash ---
Ellipsis ...
LeftAngleQuote <<
RightAngleQuote >>
Apostrophe 'twas
`

err := md.Convert([]byte(source), &buf)
assert.NoError(err)
assert.Equal(source, buf.String())
}

func TestTypographerExtensionEnabled(t *testing.T) {
assert := assert.New(t)
md := goldmark.New(
goldmark.WithExtensions(NewExtension(WithTypographerSubstitutions(true)), extension.NewTypographer()),
)
buf := bytes.Buffer{}
source := `'LeftSingleQuote
RightSingleQuote'
"LeftDoubleQuote
RightDoubleQuote"
EnDash --
EmDash ---
Ellipsis ...
LeftAngleQuote <<
RightAngleQuote >>
Apostrophe 'twas
`
expected := `‘LeftSingleQuote
RightSingleQuote’
“LeftDoubleQuote
RightDoubleQuote”
EnDash –
EmDash —
Ellipsis …
LeftAngleQuote «
RightAngleQuote »
Apostrophe 'twas
`

err := md.Convert([]byte(source), &buf)
assert.NoError(err)
assert.Equal(expected, buf.String())
}
33 changes: 33 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Config struct {
ThematicBreakStyle
ThematicBreakLength
NestedListLength
TypographerSubstitutions
}

// NewConfig returns a new Config with defaults and the given options.
Expand Down Expand Up @@ -269,3 +270,35 @@ func WithNestedListLength(style NestedListLength) interface {
} {
return &withNestedListLength{style}
}

// ============================================================================
// TypographerSubstitutions Option
// ============================================================================

// optTypographerSubstitutions is an option name used in WithTypographerSubstitutions
const optTypographerSubstitutions renderer.OptionName = "TypographerSubstitutions"

// TypographerSubstitutions specifies whether characters should be substituted by the typographer extension.
type TypographerSubstitutions bool

type withTypographerSubstitutions struct {
value TypographerSubstitutions
}

func (o *withTypographerSubstitutions) SetConfig(c *renderer.Config) {
c.Options[optTypographerSubstitutions] = o.value
}

// SetMarkdownOption implements renderer.Option
func (o *withTypographerSubstitutions) SetMarkdownOption(c *Config) {
c.TypographerSubstitutions = o.value
}

// WithTypographerSubstitutions is a functional option that determines whether characters should be substituted by the typographer extension.
// This setting has no effect unless the typographer extension is added to the goldmark.Markdown object.
func WithTypographerSubstitutions(enabled TypographerSubstitutions) interface {
renderer.Option
Option
} {
return &withTypographerSubstitutions{enabled}
}
7 changes: 7 additions & 0 deletions options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer"
)

Expand All @@ -27,6 +28,7 @@ func TestRendererOptions(t *testing.T) {
WithThematicBreakStyle(ThematicBreakStyleDashed),
WithThematicBreakLength(ThematicBreakLengthMinimum),
WithNestedListLength(NestedListLengthMinimum),
WithTypographerSubstitutions(false),
},
NewConfig(),
},
Expand All @@ -50,6 +52,11 @@ func TestRendererOptions(t *testing.T) {
r := NewRenderer(tc.options...)
assert.Equal(tc.expected, r.config)

// Set options by passing them to the renderer extension
md := goldmark.New(goldmark.WithExtensions(NewExtension(tc.options...)))
r = md.Renderer().(*Renderer)
assert.Equal(tc.expected, r.config)

// Set options by name using AddOptions
r = NewRenderer()
// Convert markdown Option interface to renderer.Option interface
Expand Down
11 changes: 9 additions & 2 deletions renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@ func (r *Renderer) Render(w io.Writer, source []byte, n ast.Node) error {
r.nodeRendererFuncs[ast.KindLink] = r.renderLink
r.nodeRendererFuncs[ast.KindRawHTML] = r.renderRawHTML
r.nodeRendererFuncs[ast.KindText] = r.renderText
// TODO: add KindString
// r.nodeRendererFuncs[ast.KindString] = r.renderString
r.nodeRendererFuncs[ast.KindString] = r.renderString

for kind, fun := range r.nodeRendererFuncsTmp {
r.nodeRendererFuncs[kind] = r.transform(fun)
Expand Down Expand Up @@ -322,6 +321,14 @@ func (r *Renderer) renderText(node ast.Node, entering bool) ast.WalkStatus {
return ast.WalkContinue
}

func (r *Renderer) renderString(node ast.Node, entering bool) ast.WalkStatus {
n := node.(*ast.String)
if entering {
r.rc.writer.WriteBytes(n.Value)
}
return ast.WalkContinue
}

func (r *Renderer) renderSegments(segments *text.Segments, asLines bool) {
for i := 0; i < segments.Len(); i++ {
segment := segments.At(i)
Expand Down
Loading
Loading