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 math extension #700

Open
wants to merge 1 commit into
base: v2
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions html.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,8 @@ var (
h5CloseTag = []byte("</h5>")
h6Tag = []byte("<h6")
h6CloseTag = []byte("</h6>")
mathTag = []byte("<math>")
mathCloseTag = []byte("</math>")

footnotesDivBytes = []byte("\n<div class=\"footnotes\">\n\n")
footnotesCloseDivBytes = []byte("\n</div>\n")
Expand Down Expand Up @@ -827,6 +829,10 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt
r.out(w, trCloseTag)
r.cr(w)
}
case Math:
r.out(w, mathTag)
escapeAllHTML(w, node.Literal)
r.out(w, mathCloseTag)
default:
panic("Unknown node type " + node.Type.String())
}
Expand Down
26 changes: 25 additions & 1 deletion inline.go
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,9 @@ func linkEndsWithEntity(data []byte, linkEnd int) bool {
}

// hasPrefixCaseInsensitive is a custom implementation of
// strings.HasPrefix(strings.ToLower(s), prefix)
//
// strings.HasPrefix(strings.ToLower(s), prefix)
//
// we rolled our own because ToLower pulls in a huge machinery of lowercasing
// anything from Unicode and that's very slow. Since this func will only be
// used on ASCII protocol prefixes, we can take shortcuts.
Expand Down Expand Up @@ -1226,3 +1228,25 @@ func text(s []byte) *Node {
func normalizeURI(s []byte) []byte {
return s // TODO: implement
}

func math(p *Markdown, data []byte, offset int) (int, *Node) {
if offset > 0 && data[offset-1] == '\\' {
return 0, nil // Dollar sign has been escaped.
}
data = data[offset:]
isInline := len(data) > 2 && data[1] != '$'
if isInline {
data = data[1:]
nl := bytes.IndexByte(data, '\n')
dolla := bytes.IndexByte(data, '$')
if dolla < 0 || (nl >= 0 && nl < dolla) {
return 0, nil // No end to math node or newline before dollar.
}
math := NewNode(Math)
math.Literal = data[:dolla]
return dolla + 2, math
}
// Block math unsupported.
// isBlock := len(data) > 4 && data[1] == '$' && data[2] != '$'
return 0, nil
}
12 changes: 12 additions & 0 deletions inline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1214,3 +1214,15 @@ func BenchmarkSmartDoubleQuotes(b *testing.B) {
runMarkdown("this should be normal \"quoted\" text.\n", params)
}
}

func TestMath(t *testing.T) {
doTestsParam(t, []string{
"In class we saw $a$ is a length.",
"<p>In class we saw <math>a</math> is a length.</p>\n",

"What we see is that following $a+b <three> four$. Right?",
"<p>What we see is that following <math>a+b &lt;three&gt; four</math>. Right?</p>\n",
}, TestParams{
extensions: DollarMath,
})
}
48 changes: 28 additions & 20 deletions markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const (
AutoHeadingIDs // Create the heading ID from the text
BackslashLineBreak // Translate trailing backslashes into line breaks
DefinitionLists // Render definition lists
DollarMath // Parse inline and block math inside '$' as Math nodes.

CommonHTMLFlags HTMLFlags = UseXHTML | Smartypants |
SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes
Expand Down Expand Up @@ -308,6 +309,9 @@ func New(opts ...Option) *Markdown {
if p.extensions&Footnotes != 0 {
p.notes = make([]*reference, 0)
}
if p.extensions&DollarMath != 0 {
p.inlineCallback['$'] = math
}
return &p
}

Expand Down Expand Up @@ -345,8 +349,8 @@ func WithNoExtensions() Option {
// In Markdown, the link reference syntax can be made to resolve a link to
// a reference instead of an inline URL, in one of the following ways:
//
// * [link text][refid]
// * [refid][]
// - [link text][refid]
// - [refid][]
//
// Usually, the refid is defined at the bottom of the Markdown document. If
// this override function is provided, the refid is passed to the override
Expand All @@ -363,21 +367,25 @@ func WithRefOverride(o ReferenceOverrideFunc) Option {
// block of markdown-encoded text.
//
// The simplest invocation of Run takes one argument, input:
// output := Run(input)
//
// output := Run(input)
//
// This will parse the input with CommonExtensions enabled and render it with
// the default HTMLRenderer (with CommonHTMLFlags).
//
// Variadic arguments opts can customize the default behavior. Since Markdown
// type does not contain exported fields, you can not use it directly. Instead,
// use the With* functions. For example, this will call the most basic
// functionality, with no extensions:
// output := Run(input, WithNoExtensions())
//
// output := Run(input, WithNoExtensions())
//
// You can use any number of With* arguments, even contradicting ones. They
// will be applied in order of appearance and the latter will override the
// former:
// output := Run(input, WithNoExtensions(), WithExtensions(exts),
// WithRenderer(yourRenderer))
//
// output := Run(input, WithNoExtensions(), WithExtensions(exts),
// WithRenderer(yourRenderer))
func Run(input []byte, opts ...Option) []byte {
r := NewHTMLRenderer(HTMLRendererParameters{
Flags: CommonHTMLFlags,
Expand Down Expand Up @@ -491,35 +499,35 @@ func (p *Markdown) parseRefsToAST() {
//
// Consider this markdown with reference-style links:
//
// [link][ref]
// [link][ref]
//
// [ref]: /url/ "tooltip title"
// [ref]: /url/ "tooltip title"
//
// It will be ultimately converted to this HTML:
//
// <p><a href=\"/url/\" title=\"title\">link</a></p>
// <p><a href=\"/url/\" title=\"title\">link</a></p>
//
// And a reference structure will be populated as follows:
//
// p.refs["ref"] = &reference{
// link: "/url/",
// title: "tooltip title",
// }
// p.refs["ref"] = &reference{
// link: "/url/",
// title: "tooltip title",
// }
//
// Alternatively, reference can contain information about a footnote. Consider
// this markdown:
//
// Text needing a footnote.[^a]
// Text needing a footnote.[^a]
//
// [^a]: This is the note
// [^a]: This is the note
//
// A reference structure will be populated as follows:
//
// p.refs["a"] = &reference{
// link: "a",
// title: "This is the note",
// noteID: <some positive int>,
// }
// p.refs["a"] = &reference{
// link: "a",
// title: "This is the note",
// noteID: <some positive int>,
// }
//
// TODO: As you can see, it begs for splitting into two dedicated structures
// for refs and for footnotes.
Expand Down
2 changes: 2 additions & 0 deletions node.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const (
TableHead
TableBody
TableRow
Math
)

var nodeTypeNames = []string{
Expand Down Expand Up @@ -63,6 +64,7 @@ var nodeTypeNames = []string{
TableHead: "TableHead",
TableBody: "TableBody",
TableRow: "TableRow",
Math: "Math",
}

func (t NodeType) String() string {
Expand Down