typewriter

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 13, 2026 License: MIT Imports: 6 Imported by: 0

README

goldmark-typewriter

Go Reference Build Status

A goldmark extension that converts typographic ("smart") Unicode characters — and Unicode style variants like bold and italic — back to their plain ASCII equivalents.

Built on github.com/client9/typewriter.

See also github.com/client9/demoji and github.com/client9/goldmark-demoji for emoji conversion and normalization.

Install

go get github.com/client9/goldmark-typewriter

Quick start

import typewriter "github.com/client9/goldmark-typewriter"

md := goldmark.New(goldmark.WithExtensions(typewriter.New()))

With options:

md := goldmark.New(goldmark.WithExtensions(
    typewriter.New(
        typewriter.WithoutCategory(typewriter.Math),
        typewriter.WithBold("**", "**"),
        typewriter.WithItalic("_", "_"),
    ),
))

What it converts

Character substitutions (all active by default)
Category Examples Result
Quotes " " ' ' « » " ' << >>
Dashes em dash , en dash , minus --- -- -
Ellipsis ...
Fractions ½ ¼ ¾ 1/2 1/4 3/4 1/3 1/8
Symbols © ® (c) (r) (tm)
Math × ÷ x / != <= >= ->
Ligatures fi fl ff ffi
Bullets * * **
Spaces NBSP, thin, en, em, figure, hair, U+2028, U+2029 plain space
Unicode style variants (opt-in)

Runs of styled Unicode characters — common in copy-pasted LinkedIn posts, Twitter formatting tricks, and AI-generated content — are detected and wrapped.

Option Input Output
WithBold("**", "**") 𝗛𝗲𝗹𝗹𝗼 **Hello**
WithItalic("_", "_") 𝘸𝘰𝘳𝘭𝘥 _world_
WithBoldItalic("***", "***") 𝙃𝙚𝙡𝙡𝙤 ***Hello***
WithMonospace(" + "" + ", " + "" + ") 𝙷𝚎𝚕𝚕𝚘 `Hello`
WithSuperscript("^", "") mc² mc^2
WithSubscript("", "") H₂O H2O

Prose vs code content

Prose Code spans / fenced blocks
goldmark extension converted preserved
typewriter.ReplaceBytes (preprocessor) converted converted

The extension preserves code content because goldmark's HTML renderer reads code spans and fenced blocks directly from the original source bytes — AST-level replacement is not possible there. This is a constraint of goldmark's architecture, not a policy choice.

To also normalise code content (e.g., smart quotes inside a pasted shell command), use typewriter.ReplaceBytes on raw source before parsing:

import tw "github.com/client9/typewriter"

clean := tw.ReplaceBytes(src)
md.Convert(clean, &buf)

Configuration

Enable only specific categories
// Only convert dashes and ellipses.
typewriter.New(typewriter.WithCategory(typewriter.Dashes | typewriter.Ellipsis))
Disable specific categories
typewriter.New(typewriter.WithoutCategory(typewriter.Math))
Override or exclude individual characters
typewriter.New(
    typewriter.WithMapping("—", "--"),  // prefer -- over --- for em dash
    typewriter.WithMapping("×", ""),    // leave × unchanged (empty = pass through)
    typewriter.WithMapping("°", "deg"), // add a mapping not in builtins
)
Convert Unicode bold/italic to markdown
typewriter.New(
    typewriter.WithBold("**", "**"),
    typewriter.WithItalic("_", "_"),
)
Convert Unicode bold/italic to HTML
typewriter.New(
    typewriter.WithBold("<b>", "</b>"),
    typewriter.WithItalic("<i>", "</i>"),
)
Superscripts and subscripts
typewriter.New(
    typewriter.WithSuperscript("^", ""),  // E=mc² → E=mc^2
    typewriter.WithSubscript("", ""),     // H₂O  → H2O
)

Using with the typographer

goldmark's typographer and this extension cannot be meaningfully combined in a single goldmark instance. The typographer is an inline parser (priority 9999) that fires during tokenisation — before any AST transformer runs. It converts ASCII → typographic as ast.String nodes; the typewriter transformer walks only ast.KindText and cannot undo that.

For consistent smart-typography output from mixed-source input, use a two-pass approach:

import (
    tw "github.com/client9/typewriter"
    "github.com/yuin/goldmark/extension"
)

// Step 1: strip all typographic characters from the raw markdown source.
clean := tw.ReplaceBytes(src)

// Step 2: render with the typographer — consistent output regardless of input source.
md := goldmark.New(goldmark.WithExtensions(extension.Typographer))
md.Convert(clean, &buf)

License

MIT

Documentation

Overview

Package typewriter provides a goldmark extension that applies typewriter conversions to prose text as a post-parse AST transformer.

Typical usage:

md := goldmark.New(goldmark.WithExtensions(typewriter.New()))

All re-exported Category and UnicodeStyle constants, and all Option constructors, are defined here so callers need only one import. To preprocess raw markdown source before parsing (e.g. to normalise code spans), use github.com/client9/typewriter.ReplaceBytes directly.

Index

Examples

Constants

View Source
const (
	Quotes      = tw.Quotes
	Dashes      = tw.Dashes
	Ellipsis    = tw.Ellipsis
	Fractions   = tw.Fractions
	Symbols     = tw.Symbols
	Math        = tw.Math
	Ligatures   = tw.Ligatures
	Bullets     = tw.Bullets
	Spaces      = tw.Spaces
	Default     = tw.Default
	CategoryAll = tw.CategoryAll
)

Category constants re-exported from github.com/client9/typewriter. Default is the set active when no WithCategory option is supplied.

View Source
const (
	Bold        = tw.Bold
	Italic      = tw.Italic
	BoldItalic  = tw.BoldItalic
	Monospace   = tw.Monospace
	Superscript = tw.Superscript
	Subscript   = tw.Subscript
)

UnicodeStyle constants re-exported from github.com/client9/typewriter.

Variables

This section is empty.

Functions

This section is empty.

Types

type Category

type Category = tw.Category

Category is an alias for the core Category type.

type Extension

type Extension struct {
	// contains filtered or unexported fields
}

Extension is a goldmark.Extender that applies typewriter conversions to prose text during AST transformation. Create with New.

func New

func New(opts ...Option) *Extension

New creates the Extension. With no options the Default category set is active and no Unicode style runs are converted.

Example

ExampleNew demonstrates the typical usage: register the extension with a goldmark instance and convert markdown containing typographic Unicode characters to their ASCII equivalents.

package main

import (
	"os"

	typewriter "github.com/client9/goldmark-typewriter"

	gm "github.com/yuin/goldmark"
)

func main() {
	md := gm.New(gm.WithExtensions(typewriter.New()))
	_ = md.Convert([]byte("mix ½ cup, cost © 2024, wait…"), os.Stdout)
}
Output:
<p>mix 1/2 cup, cost (c) 2024, wait...</p>
Example (ReplaceBytes)

ExampleNew_replaceBytes shows the two-pass approach for normalising content inside code spans, which the AST transformer cannot reach. Call tw.ReplaceBytes on the raw source first, then parse with goldmark.

package main

import (
	"os"

	typewriter "github.com/client9/goldmark-typewriter"
	tw "github.com/client9/typewriter"

	gm "github.com/yuin/goldmark"
)

func main() {
	src := []byte("outside … but `inside … code`")
	md := gm.New(gm.WithExtensions(typewriter.New()))

	// Extension form: prose converted, code span preserved.
	_ = md.Convert(src, os.Stdout)

	// ReplaceBytes: everything converted, including inside code spans.
	_ = md.Convert(tw.ReplaceBytes(src), os.Stdout)
}
Output:
<p>outside ... but <code>inside … code</code></p>
<p>outside ... but <code>inside ... code</code></p>
Example (WithoutCategory)

ExampleNew_withoutCategory shows how to disable a category. Here the Math category is removed so the multiplication sign passes through unchanged.

package main

import (
	"os"

	typewriter "github.com/client9/goldmark-typewriter"

	gm "github.com/yuin/goldmark"
)

func main() {
	ext := typewriter.New(typewriter.WithoutCategory(typewriter.Math))
	md := gm.New(gm.WithExtensions(ext))
	_ = md.Convert([]byte("10×"), os.Stdout)
}
Output:
<p>10×</p>

func (*Extension) Extend

func (e *Extension) Extend(m gm.Markdown)

Extend implements goldmark.Extender.

type Option

type Option func(*tw.Config)

Option is a functional option for configuring the Extension.

func WithBold

func WithBold(prefix, suffix string) Option

WithBold converts runs of Unicode bold characters, wrapping with prefix and suffix. Empty prefix and suffix strips to plain ASCII. Each style may be set at most once; a second WithBold call is silently ignored by the underlying replacer.

func WithBoldItalic

func WithBoldItalic(prefix, suffix string) Option

WithBoldItalic converts runs of Unicode bold-italic characters, wrapping with prefix and suffix. Each style may be set at most once.

func WithCategory

func WithCategory(c Category) Option

WithCategory sets the active categories to exactly c, replacing the default. Because it overwrites the entire mask, it should appear before any WithoutCategory calls in the same New() invocation.

func WithItalic

func WithItalic(prefix, suffix string) Option

WithItalic converts runs of Unicode italic characters, wrapping with prefix and suffix. Each style may be set at most once.

func WithMapping

func WithMapping(from, to string) Option

WithMapping adds or overrides a single character conversion. Set to to "" to leave the character unchanged.

Example

ExampleWithMapping shows how to override a single conversion. Mapping "…" to itself prevents the ellipsis from being expanded to three dots.

package main

import (
	"os"

	typewriter "github.com/client9/goldmark-typewriter"

	gm "github.com/yuin/goldmark"
)

func main() {
	ext := typewriter.New(typewriter.WithMapping("…", "…"))
	md := gm.New(gm.WithExtensions(ext))
	_ = md.Convert([]byte("wait…"), os.Stdout)
}
Output:
<p>wait…</p>

func WithMonospace

func WithMonospace(prefix, suffix string) Option

WithMonospace converts runs of Unicode monospace characters, wrapping with prefix and suffix. Each style may be set at most once.

func WithSubscript

func WithSubscript(prefix, suffix string) Option

WithSubscript converts runs of subscript characters, wrapping with prefix and suffix. Each style may be set at most once.

func WithSuperscript

func WithSuperscript(prefix, suffix string) Option

WithSuperscript converts runs of superscript characters, wrapping with prefix and suffix. A common convention is prefix "^" with empty suffix. Each style may be set at most once.

func WithoutCategory

func WithoutCategory(c Category) Option

WithoutCategory removes one or more categories from the active set.

type UnicodeStyle

type UnicodeStyle = tw.UnicodeStyle

UnicodeStyle is an alias for the core UnicodeStyle type.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL