Signed-off-by: PJ Eby <pje@telecommunity.com>tags/v1.5.0-dev
@@ -1,4 +1,6 @@ | |||||
Blackfriday [](https://travis-ci.org/russross/blackfriday) [](https://godoc.org/github.com/russross/blackfriday) | |||||
Blackfriday | |||||
[![Build Status][BuildSVG]][BuildURL] | |||||
[![Godoc][GodocV2SVG]][GodocV2URL] | |||||
=========== | =========== | ||||
Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It | Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It | ||||
@@ -8,7 +10,7 @@ punctuation substitutions, etc.), and it is safe for all utf-8 | |||||
(unicode) input. | (unicode) input. | ||||
HTML output is currently supported, along with Smartypants | HTML output is currently supported, along with Smartypants | ||||
extensions. An experimental LaTeX output engine is also included. | |||||
extensions. | |||||
It started as a translation from C of [Sundown][3]. | It started as a translation from C of [Sundown][3]. | ||||
@@ -16,26 +18,71 @@ It started as a translation from C of [Sundown][3]. | |||||
Installation | Installation | ||||
------------ | ------------ | ||||
Blackfriday is compatible with Go 1. If you are using an older | |||||
release of Go, consider using v1.1 of blackfriday, which was based | |||||
on the last stable release of Go prior to Go 1. You can find it as a | |||||
tagged commit on github. | |||||
Blackfriday is compatible with any modern Go release. With Go and git installed: | |||||
With Go 1 and git installed: | |||||
go get -u gopkg.in/russross/blackfriday.v2 | |||||
go get github.com/russross/blackfriday | |||||
will download, compile, and install the package into your `$GOPATH` directory | |||||
hierarchy. | |||||
will download, compile, and install the package into your `$GOPATH` | |||||
directory hierarchy. Alternatively, you can achieve the same if you | |||||
import it into a project: | |||||
import "github.com/russross/blackfriday" | |||||
Versions | |||||
-------- | |||||
Currently maintained and recommended version of Blackfriday is `v2`. It's being | |||||
developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the | |||||
documentation is available at | |||||
https://godoc.org/gopkg.in/russross/blackfriday.v2. | |||||
It is `go get`-able via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`, | |||||
but we highly recommend using package management tool like [dep][7] or | |||||
[Glide][8] and make use of semantic versioning. With package management you | |||||
should import `github.com/russross/blackfriday` and specify that you're using | |||||
version 2.0.0. | |||||
Version 2 offers a number of improvements over v1: | |||||
* Cleaned up API | |||||
* A separate call to [`Parse`][4], which produces an abstract syntax tree for | |||||
the document | |||||
* Latest bug fixes | |||||
* Flexibility to easily add your own rendering extensions | |||||
Potential drawbacks: | |||||
* Our benchmarks show v2 to be slightly slower than v1. Currently in the | |||||
ballpark of around 15%. | |||||
* API breakage. If you can't afford modifying your code to adhere to the new API | |||||
and don't care too much about the new features, v2 is probably not for you. | |||||
* Several bug fixes are trailing behind and still need to be forward-ported to | |||||
v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for | |||||
tracking. | |||||
If you are still interested in the legacy `v1`, you can import it from | |||||
`github.com/russross/blackfriday`. Documentation for the legacy v1 can be found | |||||
here: https://godoc.org/github.com/russross/blackfriday | |||||
### Known issue with `dep` | |||||
There is a known problem with using Blackfriday v1 _transitively_ and `dep`. | |||||
Currently `dep` prioritizes semver versions over anything else, and picks the | |||||
latest one, plus it does not apply a `[[constraint]]` specifier to transitively | |||||
pulled in packages. So if you're using something that uses Blackfriday v1, but | |||||
that something does not use `dep` yet, you will get Blackfriday v2 pulled in and | |||||
your first dependency will fail to build. | |||||
There are couple of fixes for it, documented here: | |||||
https://github.com/golang/dep/blob/master/docs/FAQ.md#how-do-i-constrain-a-transitive-dependencys-version | |||||
Meanwhile, `dep` team is working on a more general solution to the constraints | |||||
on transitive dependencies problem: https://github.com/golang/dep/issues/1124. | |||||
and `go get` without parameters. | |||||
Usage | Usage | ||||
----- | ----- | ||||
### v1 | |||||
For basic usage, it is as simple as getting your input into a byte | For basic usage, it is as simple as getting your input into a byte | ||||
slice and calling: | slice and calling: | ||||
@@ -46,34 +93,57 @@ feature set, use this instead: | |||||
output := blackfriday.MarkdownCommon(input) | output := blackfriday.MarkdownCommon(input) | ||||
### v2 | |||||
For the most sensible markdown processing, it is as simple as getting your input | |||||
into a byte slice and calling: | |||||
```go | |||||
output := blackfriday.Run(input) | |||||
``` | |||||
Your input will be parsed and the output rendered with a set of most popular | |||||
extensions enabled. If you want the most basic feature set, corresponding with | |||||
the bare Markdown specification, use: | |||||
```go | |||||
output := blackfriday.Run(input, blackfriday.WithNoExtensions()) | |||||
``` | |||||
### Sanitize untrusted content | ### Sanitize untrusted content | ||||
Blackfriday itself does nothing to protect against malicious content. If you are | Blackfriday itself does nothing to protect against malicious content. If you are | ||||
dealing with user-supplied markdown, we recommend running blackfriday's output | |||||
through HTML sanitizer such as | |||||
[Bluemonday](https://github.com/microcosm-cc/bluemonday). | |||||
dealing with user-supplied markdown, we recommend running Blackfriday's output | |||||
through HTML sanitizer such as [Bluemonday][5]. | |||||
Here's an example of simple usage of blackfriday together with bluemonday: | |||||
Here's an example of simple usage of Blackfriday together with Bluemonday: | |||||
``` go | |||||
```go | |||||
import ( | import ( | ||||
"github.com/microcosm-cc/bluemonday" | "github.com/microcosm-cc/bluemonday" | ||||
"github.com/russross/blackfriday" | |||||
"gopkg.in/russross/blackfriday.v2" | |||||
) | ) | ||||
// ... | // ... | ||||
unsafe := blackfriday.MarkdownCommon(input) | |||||
unsafe := blackfriday.Run(input) | |||||
html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) | html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) | ||||
``` | ``` | ||||
### Custom options | |||||
### Custom options, v1 | |||||
If you want to customize the set of options, first get a renderer | If you want to customize the set of options, first get a renderer | ||||
(currently either the HTML or LaTeX output engines), then use it to | |||||
(currently only the HTML output engine), then use it to | |||||
call the more general `Markdown` function. For examples, see the | call the more general `Markdown` function. For examples, see the | ||||
implementations of `MarkdownBasic` and `MarkdownCommon` in | implementations of `MarkdownBasic` and `MarkdownCommon` in | ||||
`markdown.go`. | `markdown.go`. | ||||
### Custom options, v2 | |||||
If you want to customize the set of options, use `blackfriday.WithExtensions`, | |||||
`blackfriday.WithRenderer` and `blackfriday.WithRefOverride`. | |||||
### `blackfriday-tool` | |||||
You can also check out `blackfriday-tool` for a more complete example | You can also check out `blackfriday-tool` for a more complete example | ||||
of how to use it. Download and install it using: | of how to use it. Download and install it using: | ||||
@@ -93,6 +163,22 @@ installed in `$GOPATH/bin`. This is a statically-linked binary that | |||||
can be copied to wherever you need it without worrying about | can be copied to wherever you need it without worrying about | ||||
dependencies and library versions. | dependencies and library versions. | ||||
### Sanitized anchor names | |||||
Blackfriday includes an algorithm for creating sanitized anchor names | |||||
corresponding to a given input text. This algorithm is used to create | |||||
anchors for headings when `EXTENSION_AUTO_HEADER_IDS` is enabled. The | |||||
algorithm has a specification, so that other packages can create | |||||
compatible anchor names and links to those anchors. | |||||
The specification is located at https://godoc.org/github.com/russross/blackfriday#hdr-Sanitized_Anchor_Names. | |||||
[`SanitizedAnchorName`](https://godoc.org/github.com/russross/blackfriday#SanitizedAnchorName) exposes this functionality, and can be used to | |||||
create compatible links to the anchor names generated by blackfriday. | |||||
This algorithm is also implemented in a small standalone package at | |||||
[`github.com/shurcooL/sanitized_anchor_name`](https://godoc.org/github.com/shurcooL/sanitized_anchor_name). It can be useful for clients | |||||
that want a small package and don't need full functionality of blackfriday. | |||||
Features | Features | ||||
-------- | -------- | ||||
@@ -233,7 +319,7 @@ are a few of note: | |||||
* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown): | * [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown): | ||||
provides a GitHub Flavored Markdown renderer with fenced code block | provides a GitHub Flavored Markdown renderer with fenced code block | ||||
highlighting, clickable header anchor links. | |||||
highlighting, clickable heading anchor links. | |||||
It's not customizable, and its goal is to produce HTML output | It's not customizable, and its goal is to produce HTML output | ||||
equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), | equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), | ||||
@@ -242,27 +328,18 @@ are a few of note: | |||||
* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, | * [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, | ||||
but for markdown. | but for markdown. | ||||
* LaTeX output: renders output as LaTeX. This is currently part of the | |||||
main Blackfriday repository, but may be split into its own project | |||||
in the future. If you are interested in owning and maintaining the | |||||
LaTeX output component, please be in touch. | |||||
It renders some basic documents, but is only experimental at this | |||||
point. In particular, it does not do any inline escaping, so input | |||||
that happens to look like LaTeX code will be passed through without | |||||
modification. | |||||
* [Md2Vim](https://github.com/FooSoft/md2vim): transforms markdown files into vimdoc format. | |||||
* [LaTeX output](https://bitbucket.org/ambrevar/blackfriday-latex): | |||||
renders output as LaTeX. | |||||
Todo | |||||
TODO | |||||
---- | ---- | ||||
* More unit testing | * More unit testing | ||||
* Improve unicode support. It does not understand all unicode | |||||
* Improve Unicode support. It does not understand all Unicode | |||||
rules (about what constitutes a letter, a punctuation symbol, | rules (about what constitutes a letter, a punctuation symbol, | ||||
etc.), so it may fail to detect word boundaries correctly in | etc.), so it may fail to detect word boundaries correctly in | ||||
some instances. It is safe on all utf-8 input. | |||||
some instances. It is safe on all UTF-8 input. | |||||
License | License | ||||
@@ -271,6 +348,16 @@ License | |||||
[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) | [Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) | ||||
[1]: http://daringfireball.net/projects/markdown/ "Markdown" | |||||
[2]: http://golang.org/ "Go Language" | |||||
[1]: https://daringfireball.net/projects/markdown/ "Markdown" | |||||
[2]: https://golang.org/ "Go Language" | |||||
[3]: https://github.com/vmg/sundown "Sundown" | [3]: https://github.com/vmg/sundown "Sundown" | ||||
[4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func" | |||||
[5]: https://github.com/microcosm-cc/bluemonday "Bluemonday" | |||||
[6]: https://labix.org/gopkg.in "gopkg.in" | |||||
[7]: https://github.com/golang/dep/ "dep" | |||||
[8]: https://github.com/Masterminds/glide "Glide" | |||||
[BuildSVG]: https://travis-ci.org/russross/blackfriday.svg?branch=master | |||||
[BuildURL]: https://travis-ci.org/russross/blackfriday | |||||
[GodocV2SVG]: https://godoc.org/gopkg.in/russross/blackfriday.v2?status.svg | |||||
[GodocV2URL]: https://godoc.org/gopkg.in/russross/blackfriday.v2 |
@@ -15,8 +15,8 @@ package blackfriday | |||||
import ( | import ( | ||||
"bytes" | "bytes" | ||||
"github.com/shurcooL/sanitized_anchor_name" | |||||
"strings" | |||||
"unicode" | |||||
) | ) | ||||
// Parse block-level data. | // Parse block-level data. | ||||
@@ -93,7 +93,7 @@ func (p *parser) block(out *bytes.Buffer, data []byte) { | |||||
// fenced code block: | // fenced code block: | ||||
// | // | ||||
// ``` go | |||||
// ``` go info string here | |||||
// func fact(n int) int { | // func fact(n int) int { | ||||
// if n <= 1 { | // if n <= 1 { | ||||
// return n | // return n | ||||
@@ -243,7 +243,7 @@ func (p *parser) prefixHeader(out *bytes.Buffer, data []byte) int { | |||||
} | } | ||||
if end > i { | if end > i { | ||||
if id == "" && p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { | if id == "" && p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { | ||||
id = sanitized_anchor_name.Create(string(data[i:end])) | |||||
id = SanitizedAnchorName(string(data[i:end])) | |||||
} | } | ||||
work := func() bool { | work := func() bool { | ||||
p.inline(out, data[i:end]) | p.inline(out, data[i:end]) | ||||
@@ -563,7 +563,7 @@ func (*parser) isHRule(data []byte) bool { | |||||
// and returns the end index if so, or 0 otherwise. It also returns the marker found. | // and returns the end index if so, or 0 otherwise. It also returns the marker found. | ||||
// If syntax is not nil, it gets set to the syntax specified in the fence line. | // If syntax is not nil, it gets set to the syntax specified in the fence line. | ||||
// A final newline is mandatory to recognize the fence line, unless newlineOptional is true. | // A final newline is mandatory to recognize the fence line, unless newlineOptional is true. | ||||
func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional bool) (end int, marker string) { | |||||
func isFenceLine(data []byte, info *string, oldmarker string, newlineOptional bool) (end int, marker string) { | |||||
i, size := 0, 0 | i, size := 0, 0 | ||||
// skip up to three spaces | // skip up to three spaces | ||||
@@ -599,9 +599,9 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional | |||||
} | } | ||||
// TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here | // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here | ||||
// into one, always get the syntax, and discard it if the caller doesn't care. | |||||
if syntax != nil { | |||||
syn := 0 | |||||
// into one, always get the info string, and discard it if the caller doesn't care. | |||||
if info != nil { | |||||
infoLength := 0 | |||||
i = skipChar(data, i, ' ') | i = skipChar(data, i, ' ') | ||||
if i >= len(data) { | if i >= len(data) { | ||||
@@ -611,14 +611,14 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional | |||||
return 0, "" | return 0, "" | ||||
} | } | ||||
syntaxStart := i | |||||
infoStart := i | |||||
if data[i] == '{' { | if data[i] == '{' { | ||||
i++ | i++ | ||||
syntaxStart++ | |||||
infoStart++ | |||||
for i < len(data) && data[i] != '}' && data[i] != '\n' { | for i < len(data) && data[i] != '}' && data[i] != '\n' { | ||||
syn++ | |||||
infoLength++ | |||||
i++ | i++ | ||||
} | } | ||||
@@ -628,24 +628,24 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional | |||||
// strip all whitespace at the beginning and the end | // strip all whitespace at the beginning and the end | ||||
// of the {} block | // of the {} block | ||||
for syn > 0 && isspace(data[syntaxStart]) { | |||||
syntaxStart++ | |||||
syn-- | |||||
for infoLength > 0 && isspace(data[infoStart]) { | |||||
infoStart++ | |||||
infoLength-- | |||||
} | } | ||||
for syn > 0 && isspace(data[syntaxStart+syn-1]) { | |||||
syn-- | |||||
for infoLength > 0 && isspace(data[infoStart+infoLength-1]) { | |||||
infoLength-- | |||||
} | } | ||||
i++ | i++ | ||||
} else { | } else { | ||||
for i < len(data) && !isspace(data[i]) { | |||||
syn++ | |||||
for i < len(data) && !isverticalspace(data[i]) { | |||||
infoLength++ | |||||
i++ | i++ | ||||
} | } | ||||
} | } | ||||
*syntax = string(data[syntaxStart : syntaxStart+syn]) | |||||
*info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength])) | |||||
} | } | ||||
i = skipChar(data, i, ' ') | i = skipChar(data, i, ' ') | ||||
@@ -663,8 +663,8 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional | |||||
// or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. | // or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. | ||||
// If doRender is true, a final newline is mandatory to recognize the fenced code block. | // If doRender is true, a final newline is mandatory to recognize the fenced code block. | ||||
func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) int { | func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) int { | ||||
var syntax string | |||||
beg, marker := isFenceLine(data, &syntax, "", false) | |||||
var infoString string | |||||
beg, marker := isFenceLine(data, &infoString, "", false) | |||||
if beg == 0 || beg >= len(data) { | if beg == 0 || beg >= len(data) { | ||||
return 0 | return 0 | ||||
} | } | ||||
@@ -698,7 +698,7 @@ func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) | |||||
} | } | ||||
if doRender { | if doRender { | ||||
p.r.BlockCode(out, work.Bytes(), syntax) | |||||
p.r.BlockCode(out, work.Bytes(), infoString) | |||||
} | } | ||||
return beg | return beg | ||||
@@ -1364,7 +1364,7 @@ func (p *parser) paragraph(out *bytes.Buffer, data []byte) int { | |||||
id := "" | id := "" | ||||
if p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { | if p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { | ||||
id = sanitized_anchor_name.Create(string(data[prev:eol])) | |||||
id = SanitizedAnchorName(string(data[prev:eol])) | |||||
} | } | ||||
p.r.Header(out, work, level, id) | p.r.Header(out, work, level, id) | ||||
@@ -1428,3 +1428,24 @@ func (p *parser) paragraph(out *bytes.Buffer, data []byte) int { | |||||
p.renderParagraph(out, data[:i]) | p.renderParagraph(out, data[:i]) | ||||
return i | return i | ||||
} | } | ||||
// SanitizedAnchorName returns a sanitized anchor name for the given text. | |||||
// | |||||
// It implements the algorithm specified in the package comment. | |||||
func SanitizedAnchorName(text string) string { | |||||
var anchorName []rune | |||||
futureDash := false | |||||
for _, r := range text { | |||||
switch { | |||||
case unicode.IsLetter(r) || unicode.IsNumber(r): | |||||
if futureDash && len(anchorName) > 0 { | |||||
anchorName = append(anchorName, '-') | |||||
} | |||||
futureDash = false | |||||
anchorName = append(anchorName, unicode.ToLower(r)) | |||||
default: | |||||
futureDash = true | |||||
} | |||||
} | |||||
return string(anchorName) | |||||
} |
@@ -0,0 +1,32 @@ | |||||
// Package blackfriday is a Markdown processor. | |||||
// | |||||
// It translates plain text with simple formatting rules into HTML or LaTeX. | |||||
// | |||||
// Sanitized Anchor Names | |||||
// | |||||
// Blackfriday includes an algorithm for creating sanitized anchor names | |||||
// corresponding to a given input text. This algorithm is used to create | |||||
// anchors for headings when EXTENSION_AUTO_HEADER_IDS is enabled. The | |||||
// algorithm is specified below, so that other packages can create | |||||
// compatible anchor names and links to those anchors. | |||||
// | |||||
// The algorithm iterates over the input text, interpreted as UTF-8, | |||||
// one Unicode code point (rune) at a time. All runes that are letters (category L) | |||||
// or numbers (category N) are considered valid characters. They are mapped to | |||||
// lower case, and included in the output. All other runes are considered | |||||
// invalid characters. Invalid characters that preceed the first valid character, | |||||
// as well as invalid character that follow the last valid character | |||||
// are dropped completely. All other sequences of invalid characters | |||||
// between two valid characters are replaced with a single dash character '-'. | |||||
// | |||||
// SanitizedAnchorName exposes this functionality, and can be used to | |||||
// create compatible links to the anchor names generated by blackfriday. | |||||
// This algorithm is also implemented in a small standalone package at | |||||
// github.com/shurcooL/sanitized_anchor_name. It can be useful for clients | |||||
// that want a small package and don't need full functionality of blackfriday. | |||||
package blackfriday | |||||
// NOTE: Keep Sanitized Anchor Name algorithm in sync with package | |||||
// github.com/shurcooL/sanitized_anchor_name. | |||||
// Otherwise, users of sanitized_anchor_name will get anchor names | |||||
// that are incompatible with those generated by blackfriday. |
@@ -42,6 +42,7 @@ const ( | |||||
HTML_SMARTYPANTS_DASHES // enable smart dashes (with HTML_USE_SMARTYPANTS) | HTML_SMARTYPANTS_DASHES // enable smart dashes (with HTML_USE_SMARTYPANTS) | ||||
HTML_SMARTYPANTS_LATEX_DASHES // enable LaTeX-style dashes (with HTML_USE_SMARTYPANTS and HTML_SMARTYPANTS_DASHES) | HTML_SMARTYPANTS_LATEX_DASHES // enable LaTeX-style dashes (with HTML_USE_SMARTYPANTS and HTML_SMARTYPANTS_DASHES) | ||||
HTML_SMARTYPANTS_ANGLED_QUOTES // enable angled double quotes (with HTML_USE_SMARTYPANTS) for double quotes rendering | HTML_SMARTYPANTS_ANGLED_QUOTES // enable angled double quotes (with HTML_USE_SMARTYPANTS) for double quotes rendering | ||||
HTML_SMARTYPANTS_QUOTES_NBSP // enable "French guillemets" (with HTML_USE_SMARTYPANTS) | |||||
HTML_FOOTNOTE_RETURN_LINKS // generate a link at the end of a footnote to return to the source | HTML_FOOTNOTE_RETURN_LINKS // generate a link at the end of a footnote to return to the source | ||||
) | ) | ||||
@@ -254,33 +255,21 @@ func (options *Html) HRule(out *bytes.Buffer) { | |||||
out.WriteByte('\n') | out.WriteByte('\n') | ||||
} | } | ||||
func (options *Html) BlockCode(out *bytes.Buffer, text []byte, lang string) { | |||||
func (options *Html) BlockCode(out *bytes.Buffer, text []byte, info string) { | |||||
doubleSpace(out) | doubleSpace(out) | ||||
// parse out the language names/classes | |||||
count := 0 | |||||
for _, elt := range strings.Fields(lang) { | |||||
if elt[0] == '.' { | |||||
elt = elt[1:] | |||||
} | |||||
if len(elt) == 0 { | |||||
continue | |||||
} | |||||
if count == 0 { | |||||
out.WriteString("<pre><code class=\"language-") | |||||
} else { | |||||
out.WriteByte(' ') | |||||
} | |||||
attrEscape(out, []byte(elt)) | |||||
count++ | |||||
endOfLang := strings.IndexAny(info, "\t ") | |||||
if endOfLang < 0 { | |||||
endOfLang = len(info) | |||||
} | } | ||||
if count == 0 { | |||||
lang := info[:endOfLang] | |||||
if len(lang) == 0 || lang == "." { | |||||
out.WriteString("<pre><code>") | out.WriteString("<pre><code>") | ||||
} else { | } else { | ||||
out.WriteString("<pre><code class=\"language-") | |||||
attrEscape(out, []byte(lang)) | |||||
out.WriteString("\">") | out.WriteString("\">") | ||||
} | } | ||||
attrEscape(out, text) | attrEscape(out, text) | ||||
out.WriteString("</code></pre>\n") | out.WriteString("</code></pre>\n") | ||||
} | } | ||||
@@ -619,7 +608,7 @@ func (options *Html) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { | |||||
out.WriteString(`fnref:`) | out.WriteString(`fnref:`) | ||||
out.WriteString(options.parameters.FootnoteAnchorPrefix) | out.WriteString(options.parameters.FootnoteAnchorPrefix) | ||||
out.Write(slug) | out.Write(slug) | ||||
out.WriteString(`"><a rel="footnote" href="#`) | |||||
out.WriteString(`"><a href="#`) | |||||
out.WriteString(`fn:`) | out.WriteString(`fn:`) | ||||
out.WriteString(options.parameters.FootnoteAnchorPrefix) | out.WriteString(options.parameters.FootnoteAnchorPrefix) | ||||
out.Write(slug) | out.Write(slug) | ||||
@@ -170,6 +170,10 @@ func lineBreak(p *parser, out *bytes.Buffer, data []byte, offset int) int { | |||||
precededByBackslash := offset >= 1 && data[offset-1] == '\\' // see http://spec.commonmark.org/0.18/#example-527 | precededByBackslash := offset >= 1 && data[offset-1] == '\\' // see http://spec.commonmark.org/0.18/#example-527 | ||||
precededByBackslash = precededByBackslash && p.flags&EXTENSION_BACKSLASH_LINE_BREAK != 0 | precededByBackslash = precededByBackslash && p.flags&EXTENSION_BACKSLASH_LINE_BREAK != 0 | ||||
if p.flags&EXTENSION_JOIN_LINES != 0 { | |||||
return 1 | |||||
} | |||||
// should there be a hard line break here? | // should there be a hard line break here? | ||||
if p.flags&EXTENSION_HARD_LINE_BREAK == 0 && !precededByTwoSpaces && !precededByBackslash { | if p.flags&EXTENSION_HARD_LINE_BREAK == 0 && !precededByTwoSpaces && !precededByBackslash { | ||||
return 0 | return 0 | ||||
@@ -484,6 +488,7 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { | |||||
} | } | ||||
p.notes = append(p.notes, ref) | p.notes = append(p.notes, ref) | ||||
p.notesRecord[string(ref.link)] = struct{}{} | |||||
link = ref.link | link = ref.link | ||||
title = ref.title | title = ref.title | ||||
@@ -494,9 +499,10 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int { | |||||
return 0 | return 0 | ||||
} | } | ||||
if t == linkDeferredFootnote { | |||||
if t == linkDeferredFootnote && !p.isFootnote(lr) { | |||||
lr.noteId = len(p.notes) + 1 | lr.noteId = len(p.notes) + 1 | ||||
p.notes = append(p.notes, lr) | p.notes = append(p.notes, lr) | ||||
p.notesRecord[string(lr.link)] = struct{}{} | |||||
} | } | ||||
// keep link and title from reference | // keep link and title from reference | ||||
@@ -17,6 +17,7 @@ package blackfriday | |||||
import ( | import ( | ||||
"bytes" | "bytes" | ||||
"strings" | |||||
) | ) | ||||
// Latex is a type that implements the Renderer interface for LaTeX output. | // Latex is a type that implements the Renderer interface for LaTeX output. | ||||
@@ -39,16 +40,17 @@ func (options *Latex) GetFlags() int { | |||||
} | } | ||||
// render code chunks using verbatim, or listings if we have a language | // render code chunks using verbatim, or listings if we have a language | ||||
func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, lang string) { | |||||
if lang == "" { | |||||
func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, info string) { | |||||
if info == "" { | |||||
out.WriteString("\n\\begin{verbatim}\n") | out.WriteString("\n\\begin{verbatim}\n") | ||||
} else { | } else { | ||||
lang := strings.Fields(info)[0] | |||||
out.WriteString("\n\\begin{lstlisting}[language=") | out.WriteString("\n\\begin{lstlisting}[language=") | ||||
out.WriteString(lang) | out.WriteString(lang) | ||||
out.WriteString("]\n") | out.WriteString("]\n") | ||||
} | } | ||||
out.Write(text) | out.Write(text) | ||||
if lang == "" { | |||||
if info == "" { | |||||
out.WriteString("\n\\end{verbatim}\n") | out.WriteString("\n\\end{verbatim}\n") | ||||
} else { | } else { | ||||
out.WriteString("\n\\end{lstlisting}\n") | out.WriteString("\n\\end{lstlisting}\n") | ||||
@@ -13,9 +13,6 @@ | |||||
// | // | ||||
// | // | ||||
// Blackfriday markdown processor. | |||||
// | |||||
// Translates plain text with simple formatting rules into HTML or LaTeX. | |||||
package blackfriday | package blackfriday | ||||
import ( | import ( | ||||
@@ -46,6 +43,7 @@ const ( | |||||
EXTENSION_AUTO_HEADER_IDS // Create the header ID from the text | EXTENSION_AUTO_HEADER_IDS // Create the header ID from the text | ||||
EXTENSION_BACKSLASH_LINE_BREAK // translate trailing backslashes into line breaks | EXTENSION_BACKSLASH_LINE_BREAK // translate trailing backslashes into line breaks | ||||
EXTENSION_DEFINITION_LISTS // render definition lists | EXTENSION_DEFINITION_LISTS // render definition lists | ||||
EXTENSION_JOIN_LINES // delete newline and join lines | |||||
commonHtmlFlags = 0 | | commonHtmlFlags = 0 | | ||||
HTML_USE_XHTML | | HTML_USE_XHTML | | ||||
@@ -161,7 +159,7 @@ var blockTags = map[string]struct{}{ | |||||
// Currently Html and Latex implementations are provided | // Currently Html and Latex implementations are provided | ||||
type Renderer interface { | type Renderer interface { | ||||
// block-level callbacks | // block-level callbacks | ||||
BlockCode(out *bytes.Buffer, text []byte, lang string) | |||||
BlockCode(out *bytes.Buffer, text []byte, infoString string) | |||||
BlockQuote(out *bytes.Buffer, text []byte) | BlockQuote(out *bytes.Buffer, text []byte) | ||||
BlockHtml(out *bytes.Buffer, text []byte) | BlockHtml(out *bytes.Buffer, text []byte) | ||||
Header(out *bytes.Buffer, text func() bool, level int, id string) | Header(out *bytes.Buffer, text func() bool, level int, id string) | ||||
@@ -220,7 +218,8 @@ type parser struct { | |||||
// Footnotes need to be ordered as well as available to quickly check for | // Footnotes need to be ordered as well as available to quickly check for | ||||
// presence. If a ref is also a footnote, it's stored both in refs and here | // presence. If a ref is also a footnote, it's stored both in refs and here | ||||
// in notes. Slice is nil if footnotes not enabled. | // in notes. Slice is nil if footnotes not enabled. | ||||
notes []*reference | |||||
notes []*reference | |||||
notesRecord map[string]struct{} | |||||
} | } | ||||
func (p *parser) getRef(refid string) (ref *reference, found bool) { | func (p *parser) getRef(refid string) (ref *reference, found bool) { | ||||
@@ -243,6 +242,11 @@ func (p *parser) getRef(refid string) (ref *reference, found bool) { | |||||
return ref, found | return ref, found | ||||
} | } | ||||
func (p *parser) isFootnote(ref *reference) bool { | |||||
_, ok := p.notesRecord[string(ref.link)] | |||||
return ok | |||||
} | |||||
// | // | ||||
// | // | ||||
// Public interface | // Public interface | ||||
@@ -378,6 +382,7 @@ func MarkdownOptions(input []byte, renderer Renderer, opts Options) []byte { | |||||
if extensions&EXTENSION_FOOTNOTES != 0 { | if extensions&EXTENSION_FOOTNOTES != 0 { | ||||
p.notes = make([]*reference, 0) | p.notes = make([]*reference, 0) | ||||
p.notesRecord = make(map[string]struct{}) | |||||
} | } | ||||
first := firstPass(p, input) | first := firstPass(p, input) | ||||
@@ -799,7 +804,17 @@ func ispunct(c byte) bool { | |||||
// Test if a character is a whitespace character. | // Test if a character is a whitespace character. | ||||
func isspace(c byte) bool { | func isspace(c byte) bool { | ||||
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' | |||||
return ishorizontalspace(c) || isverticalspace(c) | |||||
} | |||||
// Test if a character is a horizontal whitespace character. | |||||
func ishorizontalspace(c byte) bool { | |||||
return c == ' ' || c == '\t' | |||||
} | |||||
// Test if a character is a vertical whitespace character. | |||||
func isverticalspace(c byte) bool { | |||||
return c == '\n' || c == '\r' || c == '\f' || c == '\v' | |||||
} | } | ||||
// Test if a character is letter. | // Test if a character is letter. | ||||
@@ -39,7 +39,7 @@ func isdigit(c byte) bool { | |||||
return c >= '0' && c <= '9' | return c >= '0' && c <= '9' | ||||
} | } | ||||
func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool) bool { | |||||
func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool, addNBSP bool) bool { | |||||
// edge of the buffer is likely to be a tag that we don't get to see, | // edge of the buffer is likely to be a tag that we don't get to see, | ||||
// so we treat it like text sometimes | // so we treat it like text sometimes | ||||
@@ -96,6 +96,12 @@ func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote | |||||
*isOpen = false | *isOpen = false | ||||
} | } | ||||
// Note that with the limited lookahead, this non-breaking | |||||
// space will also be appended to single double quotes. | |||||
if addNBSP && !*isOpen { | |||||
out.WriteString(" ") | |||||
} | |||||
out.WriteByte('&') | out.WriteByte('&') | ||||
if *isOpen { | if *isOpen { | ||||
out.WriteByte('l') | out.WriteByte('l') | ||||
@@ -104,6 +110,11 @@ func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote | |||||
} | } | ||||
out.WriteByte(quote) | out.WriteByte(quote) | ||||
out.WriteString("quo;") | out.WriteString("quo;") | ||||
if addNBSP && *isOpen { | |||||
out.WriteString(" ") | |||||
} | |||||
return true | return true | ||||
} | } | ||||
@@ -116,7 +127,7 @@ func smartSingleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byt | |||||
if len(text) >= 3 { | if len(text) >= 3 { | ||||
nextChar = text[2] | nextChar = text[2] | ||||
} | } | ||||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote) { | |||||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote, false) { | |||||
return 1 | return 1 | ||||
} | } | ||||
} | } | ||||
@@ -141,7 +152,7 @@ func smartSingleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byt | |||||
if len(text) > 1 { | if len(text) > 1 { | ||||
nextChar = text[1] | nextChar = text[1] | ||||
} | } | ||||
if smartQuoteHelper(out, previousChar, nextChar, 's', &smrt.inSingleQuote) { | |||||
if smartQuoteHelper(out, previousChar, nextChar, 's', &smrt.inSingleQuote, false) { | |||||
return 0 | return 0 | ||||
} | } | ||||
@@ -205,13 +216,13 @@ func smartDashLatex(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, | |||||
return 0 | return 0 | ||||
} | } | ||||
func smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte) int { | |||||
func smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte, addNBSP bool) int { | |||||
if bytes.HasPrefix(text, []byte(""")) { | if bytes.HasPrefix(text, []byte(""")) { | ||||
nextChar := byte(0) | nextChar := byte(0) | ||||
if len(text) >= 7 { | if len(text) >= 7 { | ||||
nextChar = text[6] | nextChar = text[6] | ||||
} | } | ||||
if smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote) { | |||||
if smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote, addNBSP) { | |||||
return 5 | return 5 | ||||
} | } | ||||
} | } | ||||
@@ -224,12 +235,15 @@ func smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte | |||||
return 0 | return 0 | ||||
} | } | ||||
func smartAmp(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||||
return smartAmpVariant(out, smrt, previousChar, text, 'd') | |||||
} | |||||
func smartAmp(angledQuotes, addNBSP bool) func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||||
var quote byte = 'd' | |||||
if angledQuotes { | |||||
quote = 'a' | |||||
} | |||||
func smartAmpAngledQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||||
return smartAmpVariant(out, smrt, previousChar, text, 'a') | |||||
return func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | |||||
return smartAmpVariant(out, smrt, previousChar, text, quote, addNBSP) | |||||
} | |||||
} | } | ||||
func smartPeriod(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | func smartPeriod(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int { | ||||
@@ -253,7 +267,7 @@ func smartBacktick(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, | |||||
if len(text) >= 3 { | if len(text) >= 3 { | ||||
nextChar = text[2] | nextChar = text[2] | ||||
} | } | ||||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote) { | |||||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote, false) { | |||||
return 1 | return 1 | ||||
} | } | ||||
} | } | ||||
@@ -337,7 +351,7 @@ func smartDoubleQuoteVariant(out *bytes.Buffer, smrt *smartypantsData, previousC | |||||
if len(text) > 1 { | if len(text) > 1 { | ||||
nextChar = text[1] | nextChar = text[1] | ||||
} | } | ||||
if !smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote) { | |||||
if !smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote, false) { | |||||
out.WriteString(""") | out.WriteString(""") | ||||
} | } | ||||
@@ -367,14 +381,30 @@ type smartCallback func(out *bytes.Buffer, smrt *smartypantsData, previousChar b | |||||
type smartypantsRenderer [256]smartCallback | type smartypantsRenderer [256]smartCallback | ||||
var ( | |||||
smartAmpAngled = smartAmp(true, false) | |||||
smartAmpAngledNBSP = smartAmp(true, true) | |||||
smartAmpRegular = smartAmp(false, false) | |||||
smartAmpRegularNBSP = smartAmp(false, true) | |||||
) | |||||
func smartypants(flags int) *smartypantsRenderer { | func smartypants(flags int) *smartypantsRenderer { | ||||
r := new(smartypantsRenderer) | r := new(smartypantsRenderer) | ||||
addNBSP := flags&HTML_SMARTYPANTS_QUOTES_NBSP != 0 | |||||
if flags&HTML_SMARTYPANTS_ANGLED_QUOTES == 0 { | if flags&HTML_SMARTYPANTS_ANGLED_QUOTES == 0 { | ||||
r['"'] = smartDoubleQuote | r['"'] = smartDoubleQuote | ||||
r['&'] = smartAmp | |||||
if !addNBSP { | |||||
r['&'] = smartAmpRegular | |||||
} else { | |||||
r['&'] = smartAmpRegularNBSP | |||||
} | |||||
} else { | } else { | ||||
r['"'] = smartAngledDoubleQuote | r['"'] = smartAngledDoubleQuote | ||||
r['&'] = smartAmpAngledQuote | |||||
if !addNBSP { | |||||
r['&'] = smartAmpAngled | |||||
} else { | |||||
r['&'] = smartAmpAngledNBSP | |||||
} | |||||
} | } | ||||
r['\''] = smartSingleQuote | r['\''] = smartSingleQuote | ||||
r['('] = smartParens | r['('] = smartParens | ||||
@@ -1174,10 +1174,10 @@ | |||||
"revisionTime": "2016-09-12T16:18:15Z" | "revisionTime": "2016-09-12T16:18:15Z" | ||||
}, | }, | ||||
{ | { | ||||
"checksumSHA1": "c7jHQZk5ZEsFR9EXsWJXkszPBZA=", | |||||
"checksumSHA1": "Ne3D+KJs1TU2trnDy1UCSwlXbAE=", | |||||
"path": "github.com/russross/blackfriday", | "path": "github.com/russross/blackfriday", | ||||
"revision": "5f33e7b7878355cd2b7e6b8eefc48a5472c69f70", | |||||
"revisionTime": "2016-10-03T16:27:22Z" | |||||
"revision": "11635eb403ff09dbc3a6b5a007ab5ab09151c229", | |||||
"revisionTime": "2018-04-28T10:25:19Z" | |||||
}, | }, | ||||
{ | { | ||||
"checksumSHA1": "zmC8/3V4ls53DJlNTKDZwPSC/dA=", | "checksumSHA1": "zmC8/3V4ls53DJlNTKDZwPSC/dA=", | ||||