Compare commits
84 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
e96880f42b | |
|
|
abb995c466 | |
|
|
665c4def65 | |
|
|
41c5fccfd6 | |
|
|
75d2440b2e | |
|
|
89e6d96148 | |
|
|
c04ab11830 | |
|
|
41eb1fe8a7 | |
|
|
e2f6cd9f54 | |
|
|
5c6bc5df8a | |
|
|
a925a152c1 | |
|
|
f3ccc8fc06 | |
|
|
a477dd1646 | |
|
|
13768b07fa | |
|
|
8c3eacd7a5 | |
|
|
abafa45cd8 | |
|
|
05f3235734 | |
|
|
f1f45ab762 | |
|
|
46c73eb196 | |
|
|
34d6fae961 | |
|
|
11635eb403 | |
|
|
16ac584625 | |
|
|
55d61fa8aa | |
|
|
338ca359c1 | |
|
|
8db9055e38 | |
|
|
e32caebbc7 | |
|
|
6d1ef893fc | |
|
|
8249792ba8 | |
|
|
8ad7e40db0 | |
|
|
a03b7ee643 | |
|
|
52dd06192f | |
|
|
ad4c953876 | |
|
|
74be510331 | |
|
|
4048872b16 | |
|
|
4ca8c28b21 | |
|
|
067529f716 | |
|
|
5b2fb1b893 | |
|
|
8c89af6200 | |
|
|
a20399916c | |
|
|
0ba0f2b6ed | |
|
|
b253417e1c | |
|
|
f0bb45f44c | |
|
|
8098dab4eb | |
|
|
3ffe8c7f6b | |
|
|
5f33e7b787 | |
|
|
35eb537633 | |
|
|
93622da34e | |
|
|
4e6f303e8d | |
|
|
a5812bb8f2 | |
|
|
0049676599 | |
|
|
96537c6eaa | |
|
|
1d6b8e9301 | |
|
|
b88a9bd458 | |
|
|
2004188462 | |
|
|
acc07e6144 | |
|
|
43529be397 | |
|
|
0529888f55 | |
|
|
fc997ac2ba | |
|
|
151efb040f | |
|
|
69f51afe42 | |
|
|
232d06cf99 | |
|
|
b43df972fb | |
|
|
1d94959fea | |
|
|
006144af03 | |
|
|
2ee51ae2a0 | |
|
|
c8875c0ed4 | |
|
|
fc6236fc55 | |
|
|
b8031576aa | |
|
|
ee63ffd3e2 | |
|
|
f5ac2ddf7a | |
|
|
594d923645 | |
|
|
05a79218ed | |
|
|
d18b67ae0a | |
|
|
3c4a488ada | |
|
|
fc740701cf | |
|
|
bcd6dd8711 | |
|
|
e3cbadc5c9 | |
|
|
ce3ffa70a0 | |
|
|
1bb1fa9e95 | |
|
|
c1d4a77100 | |
|
|
45f5800b75 | |
|
|
1622f8f312 | |
|
|
300106c228 | |
|
|
7d22880556 |
28
.travis.yml
28
.travis.yml
|
|
@ -1,18 +1,18 @@
|
|||
# Travis CI (http://travis-ci.org/) is a continuous integration service for
|
||||
# open source projects. This file configures it to run unit tests for
|
||||
# blackfriday.
|
||||
|
||||
sudo: false
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
|
||||
- "1.9.x"
|
||||
- "1.10.x"
|
||||
- "1.11.x"
|
||||
- tip
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- go: tip
|
||||
install:
|
||||
- go get -d -t -v ./...
|
||||
- go build -v ./...
|
||||
|
||||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
|
||||
script:
|
||||
- go test -v ./...
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
||||
- go tool vet .
|
||||
- go test -v -race ./...
|
||||
|
|
|
|||
53
LICENSE.txt
53
LICENSE.txt
|
|
@ -1,29 +1,28 @@
|
|||
Blackfriday is distributed under the Simplified BSD License:
|
||||
|
||||
> Copyright © 2011 Russ Ross
|
||||
> All rights reserved.
|
||||
>
|
||||
> Redistribution and use in source and binary forms, with or without
|
||||
> modification, are permitted provided that the following conditions
|
||||
> are met:
|
||||
>
|
||||
> 1. Redistributions of source code must retain the above copyright
|
||||
> notice, this list of conditions and the following disclaimer.
|
||||
>
|
||||
> 2. Redistributions in binary form must reproduce the above
|
||||
> copyright notice, this list of conditions and the following
|
||||
> disclaimer in the documentation and/or other materials provided with
|
||||
> the distribution.
|
||||
>
|
||||
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
> POSSIBILITY OF SUCH DAMAGE.
|
||||
Copyright © 2011 Russ Ross
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided with
|
||||
the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
|
|||
181
README.md
181
README.md
|
|
@ -1,4 +1,6 @@
|
|||
Blackfriday [](https://travis-ci.org/russross/blackfriday)
|
||||
Blackfriday
|
||||
[![Build Status][BuildV2SVG]][BuildV2URL]
|
||||
[![PkgGoDev][PkgGoDevV2SVG]][PkgGoDevV2URL]
|
||||
===========
|
||||
|
||||
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.
|
||||
|
||||
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].
|
||||
|
||||
|
|
@ -16,64 +18,126 @@ It started as a translation from C of [Sundown][3].
|
|||
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.
|
||||
|
||||
With Go 1 and git installed:
|
||||
Blackfriday is compatible with modern Go releases in module mode.
|
||||
With Go installed:
|
||||
|
||||
go get github.com/russross/blackfriday
|
||||
|
||||
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:
|
||||
will resolve and add the package to the current development module,
|
||||
then build and install it. Alternatively, you can achieve the same
|
||||
if you import it in a package:
|
||||
|
||||
import "github.com/russross/blackfriday"
|
||||
|
||||
and `go get` without parameters.
|
||||
|
||||
Old versions of Go and legacy GOPATH mode might work,
|
||||
but no effort is made to keep them working.
|
||||
|
||||
|
||||
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://pkg.go.dev/github.com/russross/blackfriday/v2.
|
||||
|
||||
It is `go get`-able in module mode at `github.com/russross/blackfriday/v2`.
|
||||
|
||||
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://pkg.go.dev/github.com/russross/blackfriday.
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
### v1
|
||||
|
||||
For basic usage, it is as simple as getting your input into a byte
|
||||
slice and calling:
|
||||
|
||||
output := blackfriday.MarkdownBasic(input)
|
||||
```go
|
||||
output := blackfriday.MarkdownBasic(input)
|
||||
```
|
||||
|
||||
This renders it with no extensions enabled. To get a more useful
|
||||
feature set, use this instead:
|
||||
|
||||
output := blackfriday.MarkdownCommon(input)
|
||||
```go
|
||||
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
|
||||
|
||||
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 (
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
// ...
|
||||
unsafe := blackfriday.MarkdownCommon(input)
|
||||
unsafe := blackfriday.Run(input)
|
||||
html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||
```
|
||||
|
||||
### Custom options
|
||||
### Custom options, v1
|
||||
|
||||
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
|
||||
implementations of `MarkdownBasic` and `MarkdownCommon` in
|
||||
`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
|
||||
of how to use it. Download and install it using:
|
||||
|
||||
|
|
@ -84,7 +148,7 @@ markdown file using a standalone program. You can also browse the
|
|||
source directly on github if you are just looking for some example
|
||||
code:
|
||||
|
||||
* <http://github.com/russross/blackfriday-tool>
|
||||
* <https://github.com/russross/blackfriday-tool>
|
||||
|
||||
Note that if you have not already done so, installing
|
||||
`blackfriday-tool` will be sufficient to download and install
|
||||
|
|
@ -93,6 +157,22 @@ installed in `$GOPATH/bin`. This is a statically-linked binary that
|
|||
can be copied to wherever you need it without worrying about
|
||||
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://pkg.go.dev/github.com/russross/blackfriday#hdr-Sanitized_Anchor_Names.
|
||||
|
||||
[`SanitizedAnchorName`](https://pkg.go.dev/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://pkg.go.dev/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
|
||||
--------
|
||||
|
|
@ -114,7 +194,7 @@ All features of Sundown are supported, including:
|
|||
know and send me the input that does it.
|
||||
|
||||
NOTE: "safety" in this context means *runtime safety only*. In order to
|
||||
protect yourself agains JavaScript injection in untrusted content, see
|
||||
protect yourself against JavaScript injection in untrusted content, see
|
||||
[this example](https://github.com/russross/blackfriday#sanitize-untrusted-content).
|
||||
|
||||
* **Fast processing**. It is fast enough to render on-demand in
|
||||
|
|
@ -160,7 +240,7 @@ implements the following extensions:
|
|||
and supply a language (to make syntax highlighting simple). Just
|
||||
mark it like this:
|
||||
|
||||
``` go
|
||||
```go
|
||||
func getTrue() bool {
|
||||
return true
|
||||
}
|
||||
|
|
@ -169,12 +249,21 @@ implements the following extensions:
|
|||
You can use 3 or more backticks to mark the beginning of the
|
||||
block, and the same number to mark the end of the block.
|
||||
|
||||
To preserve classes of fenced code blocks while using the bluemonday
|
||||
HTML sanitizer, use the following policy:
|
||||
|
||||
```go
|
||||
p := bluemonday.UGCPolicy()
|
||||
p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code")
|
||||
html := p.SanitizeBytes(unsafe)
|
||||
```
|
||||
|
||||
* **Definition lists**. A simple definition list is made of a single-line
|
||||
term followed by a colon and the definition for that term.
|
||||
|
||||
Cat
|
||||
: Fluffy animal everyone likes
|
||||
|
||||
|
||||
Internet
|
||||
: Vector of transmission for pictures of cats
|
||||
|
||||
|
|
@ -185,7 +274,7 @@ implements the following extensions:
|
|||
end of the document. A footnote looks like this:
|
||||
|
||||
This is a footnote.[^1]
|
||||
|
||||
|
||||
[^1]: the footnote text.
|
||||
|
||||
* **Autolinking**. Blackfriday can find URLs that have not been
|
||||
|
|
@ -222,9 +311,9 @@ Other renderers
|
|||
Blackfriday is structured to allow alternative rendering engines. Here
|
||||
are a few of note:
|
||||
|
||||
* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown):
|
||||
* [github_flavored_markdown](https://pkg.go.dev/github.com/shurcooL/github_flavored_markdown):
|
||||
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
|
||||
equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode),
|
||||
|
|
@ -233,25 +322,28 @@ are a few of note:
|
|||
* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt,
|
||||
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.
|
||||
* [LaTeX output](https://gitlab.com/ambrevar/blackfriday-latex):
|
||||
renders output as LaTeX.
|
||||
|
||||
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.
|
||||
* [bfchroma](https://github.com/Depado/bfchroma/): provides convenience
|
||||
integration with the [Chroma](https://github.com/alecthomas/chroma) code
|
||||
highlighting library. bfchroma is only compatible with v2 of Blackfriday and
|
||||
provides a drop-in renderer ready to use with Blackfriday, as well as
|
||||
options and means for further customization.
|
||||
|
||||
* [Blackfriday-Confluence](https://github.com/kentaro-m/blackfriday-confluence): provides a [Confluence Wiki Markup](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html) renderer.
|
||||
|
||||
* [Blackfriday-Slack](https://github.com/karriereat/blackfriday-slack): converts markdown to slack message style
|
||||
|
||||
|
||||
Todo
|
||||
TODO
|
||||
----
|
||||
|
||||
* 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,
|
||||
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
|
||||
|
|
@ -260,6 +352,13 @@ License
|
|||
[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"
|
||||
[4]: https://pkg.go.dev/github.com/russross/blackfriday/v2#Parse "Parse func"
|
||||
[5]: https://github.com/microcosm-cc/bluemonday "Bluemonday"
|
||||
|
||||
[BuildV2SVG]: https://travis-ci.org/russross/blackfriday.svg?branch=v2
|
||||
[BuildV2URL]: https://travis-ci.org/russross/blackfriday
|
||||
[PkgGoDevV2SVG]: https://pkg.go.dev/badge/github.com/russross/blackfriday/v2
|
||||
[PkgGoDevV2URL]: https://pkg.go.dev/github.com/russross/blackfriday/v2
|
||||
|
|
|
|||
250
block.go
250
block.go
|
|
@ -15,8 +15,8 @@ package blackfriday
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/shurcooL/sanitized_anchor_name"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Parse block-level data.
|
||||
|
|
@ -93,7 +93,7 @@ func (p *parser) block(out *bytes.Buffer, data []byte) {
|
|||
|
||||
// fenced code block:
|
||||
//
|
||||
// ``` go
|
||||
// ``` go info string here
|
||||
// func fact(n int) int {
|
||||
// if n <= 1 {
|
||||
// return n
|
||||
|
|
@ -102,7 +102,7 @@ func (p *parser) block(out *bytes.Buffer, data []byte) {
|
|||
// }
|
||||
// ```
|
||||
if p.flags&EXTENSION_FENCED_CODE != 0 {
|
||||
if i := p.fencedCode(out, data, true); i > 0 {
|
||||
if i := p.fencedCodeBlock(out, data, true); i > 0 {
|
||||
data = data[i:]
|
||||
continue
|
||||
}
|
||||
|
|
@ -243,7 +243,7 @@ func (p *parser) prefixHeader(out *bytes.Buffer, data []byte) int {
|
|||
}
|
||||
if end > i {
|
||||
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 {
|
||||
p.inline(out, data[i:end])
|
||||
|
|
@ -320,6 +320,11 @@ func (p *parser) html(out *bytes.Buffer, data []byte, doRender bool) int {
|
|||
return size
|
||||
}
|
||||
|
||||
// check for HTML CDATA
|
||||
if size := p.htmlCDATA(out, data, doRender); size > 0 {
|
||||
return size
|
||||
}
|
||||
|
||||
// no special case recognized
|
||||
return 0
|
||||
}
|
||||
|
|
@ -397,12 +402,10 @@ func (p *parser) html(out *bytes.Buffer, data []byte, doRender bool) int {
|
|||
return i
|
||||
}
|
||||
|
||||
// HTML comment, lax form
|
||||
func (p *parser) htmlComment(out *bytes.Buffer, data []byte, doRender bool) int {
|
||||
i := p.inlineHtmlComment(out, data)
|
||||
// needs to end with a blank line
|
||||
if j := p.isEmpty(data[i:]); j > 0 {
|
||||
size := i + j
|
||||
func (p *parser) renderHTMLBlock(out *bytes.Buffer, data []byte, start int, doRender bool) int {
|
||||
// html block needs to end with a blank line
|
||||
if i := p.isEmpty(data[start:]); i > 0 {
|
||||
size := start + i
|
||||
if doRender {
|
||||
// trim trailing newlines
|
||||
end := size
|
||||
|
|
@ -416,6 +419,35 @@ func (p *parser) htmlComment(out *bytes.Buffer, data []byte, doRender bool) int
|
|||
return 0
|
||||
}
|
||||
|
||||
// HTML comment, lax form
|
||||
func (p *parser) htmlComment(out *bytes.Buffer, data []byte, doRender bool) int {
|
||||
i := p.inlineHTMLComment(out, data)
|
||||
return p.renderHTMLBlock(out, data, i, doRender)
|
||||
}
|
||||
|
||||
// HTML CDATA section
|
||||
func (p *parser) htmlCDATA(out *bytes.Buffer, data []byte, doRender bool) int {
|
||||
const cdataTag = "<![cdata["
|
||||
const cdataTagLen = len(cdataTag)
|
||||
if len(data) < cdataTagLen+1 {
|
||||
return 0
|
||||
}
|
||||
if !bytes.Equal(bytes.ToLower(data[:cdataTagLen]), []byte(cdataTag)) {
|
||||
return 0
|
||||
}
|
||||
i := cdataTagLen
|
||||
// scan for an end-of-comment marker, across lines if necessary
|
||||
for i < len(data) && !(data[i-2] == ']' && data[i-1] == ']' && data[i] == '>') {
|
||||
i++
|
||||
}
|
||||
i++
|
||||
// no end-of-comment marker
|
||||
if i >= len(data) {
|
||||
return 0
|
||||
}
|
||||
return p.renderHTMLBlock(out, data, i, doRender)
|
||||
}
|
||||
|
||||
// HR, which is the only self-closing block tag considered
|
||||
func (p *parser) htmlHr(out *bytes.Buffer, data []byte, doRender bool) int {
|
||||
if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') {
|
||||
|
|
@ -432,19 +464,7 @@ func (p *parser) htmlHr(out *bytes.Buffer, data []byte, doRender bool) int {
|
|||
}
|
||||
|
||||
if data[i] == '>' {
|
||||
i++
|
||||
if j := p.isEmpty(data[i:]); j > 0 {
|
||||
size := i + j
|
||||
if doRender {
|
||||
// trim newlines
|
||||
end := size
|
||||
for end > 0 && data[end-1] == '\n' {
|
||||
end--
|
||||
}
|
||||
p.r.BlockHtml(out, data[:end])
|
||||
}
|
||||
return size
|
||||
}
|
||||
return p.renderHTMLBlock(out, data, i+1, doRender)
|
||||
}
|
||||
|
||||
return 0
|
||||
|
|
@ -495,7 +515,7 @@ func (p *parser) htmlFindEnd(tag string, data []byte) int {
|
|||
return i + skip
|
||||
}
|
||||
|
||||
func (p *parser) isEmpty(data []byte) int {
|
||||
func (*parser) isEmpty(data []byte) int {
|
||||
// it is okay to call isEmpty on an empty buffer
|
||||
if len(data) == 0 {
|
||||
return 0
|
||||
|
|
@ -510,7 +530,7 @@ func (p *parser) isEmpty(data []byte) int {
|
|||
return i + 1
|
||||
}
|
||||
|
||||
func (p *parser) isHRule(data []byte) bool {
|
||||
func (*parser) isHRule(data []byte) bool {
|
||||
i := 0
|
||||
|
||||
// skip up to three spaces
|
||||
|
|
@ -539,21 +559,24 @@ func (p *parser) isHRule(data []byte) bool {
|
|||
return n >= 3
|
||||
}
|
||||
|
||||
func (p *parser) isFencedCode(data []byte, syntax **string, oldmarker string) (skip int, marker string) {
|
||||
// isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data,
|
||||
// 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.
|
||||
// A final newline is mandatory to recognize the fence line, unless newlineOptional is true.
|
||||
func isFenceLine(data []byte, info *string, oldmarker string, newlineOptional bool) (end int, marker string) {
|
||||
i, size := 0, 0
|
||||
skip = 0
|
||||
|
||||
// skip up to three spaces
|
||||
for i < len(data) && i < 3 && data[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
if i >= len(data) {
|
||||
return
|
||||
}
|
||||
|
||||
// check for the marker characters: ~ or `
|
||||
if i >= len(data) {
|
||||
return 0, ""
|
||||
}
|
||||
if data[i] != '~' && data[i] != '`' {
|
||||
return
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
c := data[i]
|
||||
|
|
@ -564,79 +587,87 @@ func (p *parser) isFencedCode(data []byte, syntax **string, oldmarker string) (s
|
|||
i++
|
||||
}
|
||||
|
||||
if i >= len(data) {
|
||||
return
|
||||
}
|
||||
|
||||
// the marker char must occur at least 3 times
|
||||
if size < 3 {
|
||||
return
|
||||
return 0, ""
|
||||
}
|
||||
marker = string(data[i-size : i])
|
||||
|
||||
// if this is the end marker, it must match the beginning marker
|
||||
if oldmarker != "" && marker != oldmarker {
|
||||
return
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
if syntax != nil {
|
||||
syn := 0
|
||||
// TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here
|
||||
// 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, ' ')
|
||||
|
||||
if i >= len(data) {
|
||||
return
|
||||
if newlineOptional && i == len(data) {
|
||||
return i, marker
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
syntaxStart := i
|
||||
infoStart := i
|
||||
|
||||
if data[i] == '{' {
|
||||
i++
|
||||
syntaxStart++
|
||||
infoStart++
|
||||
|
||||
for i < len(data) && data[i] != '}' && data[i] != '\n' {
|
||||
syn++
|
||||
infoLength++
|
||||
i++
|
||||
}
|
||||
|
||||
if i >= len(data) || data[i] != '}' {
|
||||
return
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
// strip all whitespace at the beginning and the end
|
||||
// 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++
|
||||
} else {
|
||||
for i < len(data) && !isspace(data[i]) {
|
||||
syn++
|
||||
for i < len(data) && !isverticalspace(data[i]) {
|
||||
infoLength++
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
language := string(data[syntaxStart : syntaxStart+syn])
|
||||
*syntax = &language
|
||||
*info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength]))
|
||||
}
|
||||
|
||||
i = skipChar(data, i, ' ')
|
||||
if i >= len(data) || data[i] != '\n' {
|
||||
return
|
||||
if i >= len(data) {
|
||||
if newlineOptional {
|
||||
return i, marker
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
if data[i] == '\n' {
|
||||
i++ // Take newline into account
|
||||
}
|
||||
|
||||
skip = i + 1
|
||||
return
|
||||
return i, marker
|
||||
}
|
||||
|
||||
func (p *parser) fencedCode(out *bytes.Buffer, data []byte, doRender bool) int {
|
||||
var lang *string
|
||||
beg, marker := p.isFencedCode(data, &lang, "")
|
||||
// fencedCodeBlock returns the end index if data contains a fenced code block at the beginning,
|
||||
// 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.
|
||||
func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) int {
|
||||
var infoString string
|
||||
beg, marker := isFenceLine(data, &infoString, "", false)
|
||||
if beg == 0 || beg >= len(data) {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -647,7 +678,8 @@ func (p *parser) fencedCode(out *bytes.Buffer, data []byte, doRender bool) int {
|
|||
// safe to assume beg < len(data)
|
||||
|
||||
// check for the end of the code block
|
||||
fenceEnd, _ := p.isFencedCode(data[beg:], nil, marker)
|
||||
newlineOptional := !doRender
|
||||
fenceEnd, _ := isFenceLine(data[beg:], nil, marker, newlineOptional)
|
||||
if fenceEnd != 0 {
|
||||
beg += fenceEnd
|
||||
break
|
||||
|
|
@ -668,13 +700,8 @@ func (p *parser) fencedCode(out *bytes.Buffer, data []byte, doRender bool) int {
|
|||
beg = end
|
||||
}
|
||||
|
||||
syntax := ""
|
||||
if lang != nil {
|
||||
syntax = *lang
|
||||
}
|
||||
|
||||
if doRender {
|
||||
p.r.BlockCode(out, work.Bytes(), syntax)
|
||||
p.r.BlockCode(out, work.Bytes(), infoString)
|
||||
}
|
||||
|
||||
return beg
|
||||
|
|
@ -914,7 +941,7 @@ func (p *parser) quote(out *bytes.Buffer, data []byte) int {
|
|||
// irregardless of any contents inside it
|
||||
for data[end] != '\n' {
|
||||
if p.flags&EXTENSION_FENCED_CODE != 0 {
|
||||
if i := p.fencedCode(out, data[end:], false); i > 0 {
|
||||
if i := p.fencedCodeBlock(out, data[end:], false); i > 0 {
|
||||
// -1 to compensate for the extra end++ after the loop:
|
||||
end += i - 1
|
||||
break
|
||||
|
|
@ -1109,6 +1136,15 @@ func (p *parser) listItem(out *bytes.Buffer, data []byte, flags *int) int {
|
|||
i++
|
||||
}
|
||||
|
||||
// process the following lines
|
||||
containsBlankLine := false
|
||||
sublist := 0
|
||||
codeBlockMarker := ""
|
||||
if p.flags&EXTENSION_FENCED_CODE != 0 && i > line {
|
||||
// determine if codeblock starts on the first line
|
||||
_, codeBlockMarker = isFenceLine(data[line:i], nil, "", false)
|
||||
}
|
||||
|
||||
// get working buffer
|
||||
var raw bytes.Buffer
|
||||
|
||||
|
|
@ -1116,10 +1152,6 @@ func (p *parser) listItem(out *bytes.Buffer, data []byte, flags *int) int {
|
|||
raw.Write(data[line:i])
|
||||
line = i
|
||||
|
||||
// process the following lines
|
||||
containsBlankLine := false
|
||||
sublist := 0
|
||||
|
||||
gatherlines:
|
||||
for line < len(data) {
|
||||
i++
|
||||
|
|
@ -1128,11 +1160,11 @@ gatherlines:
|
|||
for data[i-1] != '\n' {
|
||||
i++
|
||||
}
|
||||
|
||||
// if it is an empty line, guess that it is part of this item
|
||||
// and move on to the next line
|
||||
if p.isEmpty(data[line:i]) > 0 {
|
||||
containsBlankLine = true
|
||||
raw.Write(data[line:i])
|
||||
line = i
|
||||
continue
|
||||
}
|
||||
|
|
@ -1145,6 +1177,28 @@ gatherlines:
|
|||
|
||||
chunk := data[line+indent : i]
|
||||
|
||||
if p.flags&EXTENSION_FENCED_CODE != 0 {
|
||||
// determine if in or out of codeblock
|
||||
// if in codeblock, ignore normal list processing
|
||||
_, marker := isFenceLine(chunk, nil, codeBlockMarker, false)
|
||||
if marker != "" {
|
||||
if codeBlockMarker == "" {
|
||||
// start of codeblock
|
||||
codeBlockMarker = marker
|
||||
} else {
|
||||
// end of codeblock.
|
||||
*flags |= LIST_ITEM_CONTAINS_BLOCK
|
||||
codeBlockMarker = ""
|
||||
}
|
||||
}
|
||||
// we are in a codeblock, write line, and continue
|
||||
if codeBlockMarker != "" || marker != "" {
|
||||
raw.Write(data[line+indent : i])
|
||||
line = i
|
||||
continue gatherlines
|
||||
}
|
||||
}
|
||||
|
||||
// evaluate how this line fits in
|
||||
switch {
|
||||
// is this a nested list item?
|
||||
|
|
@ -1153,6 +1207,14 @@ gatherlines:
|
|||
p.dliPrefix(chunk) > 0:
|
||||
|
||||
if containsBlankLine {
|
||||
// end the list if the type changed after a blank line
|
||||
if indent <= itemIndent &&
|
||||
((*flags&LIST_TYPE_ORDERED != 0 && p.uliPrefix(chunk) > 0) ||
|
||||
(*flags&LIST_TYPE_ORDERED == 0 && p.oliPrefix(chunk) > 0)) {
|
||||
|
||||
*flags |= LIST_ITEM_END_OF_LIST
|
||||
break gatherlines
|
||||
}
|
||||
*flags |= LIST_ITEM_CONTAINS_BLOCK
|
||||
}
|
||||
|
||||
|
|
@ -1200,17 +1262,10 @@ gatherlines:
|
|||
|
||||
// a blank line means this should be parsed as a block
|
||||
case containsBlankLine:
|
||||
raw.WriteByte('\n')
|
||||
*flags |= LIST_ITEM_CONTAINS_BLOCK
|
||||
}
|
||||
|
||||
// if this line was preceeded by one or more blanks,
|
||||
// re-introduce the blank into the buffer
|
||||
if containsBlankLine {
|
||||
containsBlankLine = false
|
||||
raw.WriteByte('\n')
|
||||
|
||||
}
|
||||
containsBlankLine = false
|
||||
|
||||
// add the line into the working buffer without prefix
|
||||
raw.Write(data[line+indent : i])
|
||||
|
|
@ -1218,6 +1273,12 @@ gatherlines:
|
|||
line = i
|
||||
}
|
||||
|
||||
// If reached end of data, the Renderer.ListItem call we're going to make below
|
||||
// is definitely the last in the list.
|
||||
if line >= len(data) {
|
||||
*flags |= LIST_ITEM_END_OF_LIST
|
||||
}
|
||||
|
||||
rawBytes := raw.Bytes()
|
||||
|
||||
// render the contents of the list item
|
||||
|
|
@ -1332,7 +1393,7 @@ func (p *parser) paragraph(out *bytes.Buffer, data []byte) int {
|
|||
|
||||
id := ""
|
||||
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)
|
||||
|
|
@ -1362,7 +1423,7 @@ func (p *parser) paragraph(out *bytes.Buffer, data []byte) int {
|
|||
|
||||
// if there's a fenced code block, paragraph is over
|
||||
if p.flags&EXTENSION_FENCED_CODE != 0 {
|
||||
if p.fencedCode(out, current, false) > 0 {
|
||||
if p.fencedCodeBlock(out, current, false) > 0 {
|
||||
p.renderParagraph(out, data[:i])
|
||||
return i
|
||||
}
|
||||
|
|
@ -1396,3 +1457,24 @@ func (p *parser) paragraph(out *bytes.Buffer, data []byte) int {
|
|||
p.renderParagraph(out, data[: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)
|
||||
}
|
||||
|
|
|
|||
332
block_test.go
332
block_test.go
|
|
@ -657,6 +657,9 @@ func TestUnorderedList(t *testing.T) {
|
|||
"Paragraph\n\n* Linebreak\n",
|
||||
"<p>Paragraph</p>\n\n<ul>\n<li>Linebreak</li>\n</ul>\n",
|
||||
|
||||
"* List\n\n1. Spacer Mixed listing\n",
|
||||
"<ul>\n<li>List</li>\n</ul>\n\n<ol>\n<li>Spacer Mixed listing</li>\n</ol>\n",
|
||||
|
||||
"* List\n * Nested list\n",
|
||||
"<ul>\n<li>List\n\n<ul>\n<li>Nested list</li>\n</ul></li>\n</ul>\n",
|
||||
|
||||
|
|
@ -694,18 +697,49 @@ func TestUnorderedList(t *testing.T) {
|
|||
"* List\n extra indent, same paragraph\n",
|
||||
"<ul>\n<li>List\n extra indent, same paragraph</li>\n</ul>\n",
|
||||
|
||||
"* List\n\n code block\n",
|
||||
"<ul>\n<li><p>List</p>\n\n<pre><code>code block\n</code></pre></li>\n</ul>\n",
|
||||
"* List\n\n code block\n\n* List continues",
|
||||
"<ul>\n<li><p>List</p>\n\n<pre><code>code block\n</code></pre></li>\n\n<li><p>List continues</p></li>\n</ul>\n",
|
||||
|
||||
"* List\n\n code block with spaces\n",
|
||||
"<ul>\n<li><p>List</p>\n\n<pre><code> code block with spaces\n</code></pre></li>\n</ul>\n",
|
||||
|
||||
"* List\n\n * sublist\n\n normal text\n\n * another sublist\n",
|
||||
"<ul>\n<li><p>List</p>\n\n<ul>\n<li>sublist</li>\n</ul>\n\n<p>normal text</p>\n\n<ul>\n<li>another sublist</li>\n</ul></li>\n</ul>\n",
|
||||
|
||||
`* Foo
|
||||
|
||||
bar
|
||||
|
||||
qux
|
||||
`,
|
||||
`<ul>
|
||||
<li><p>Foo</p>
|
||||
|
||||
<pre><code>bar
|
||||
|
||||
qux
|
||||
</code></pre></li>
|
||||
</ul>
|
||||
`,
|
||||
}
|
||||
doTestsBlock(t, tests, 0)
|
||||
}
|
||||
|
||||
func TestFencedCodeBlockWithinList(t *testing.T) {
|
||||
doTestsBlock(t, []string{
|
||||
"* Foo\n\n ```\n bar\n\n qux\n ```\n",
|
||||
`<ul>
|
||||
<li><p>Foo</p>
|
||||
|
||||
<pre><code>bar
|
||||
|
||||
qux
|
||||
</code></pre></li>
|
||||
</ul>
|
||||
`,
|
||||
}, EXTENSION_FENCED_CODE)
|
||||
}
|
||||
|
||||
func TestOrderedList(t *testing.T) {
|
||||
var tests = []string{
|
||||
"1. Hello\n",
|
||||
|
|
@ -784,6 +818,12 @@ func TestOrderedList(t *testing.T) {
|
|||
"1. List\n\n code block with spaces\n",
|
||||
"<ol>\n<li><p>List</p>\n\n<pre><code> code block with spaces\n</code></pre></li>\n</ol>\n",
|
||||
|
||||
"1. List\n\n* Spacer Mixed listing\n",
|
||||
"<ol>\n<li>List</li>\n</ol>\n\n<ul>\n<li>Spacer Mixed listing</li>\n</ul>\n",
|
||||
|
||||
"1. List\n* Mixed listing\n",
|
||||
"<ol>\n<li>List</li>\n<li>Mixed listing</li>\n</ol>\n",
|
||||
|
||||
"1. List\n * Mixted list\n",
|
||||
"<ol>\n<li>List\n\n<ul>\n<li>Mixted list</li>\n</ul></li>\n</ol>\n",
|
||||
|
||||
|
|
@ -798,6 +838,26 @@ func TestOrderedList(t *testing.T) {
|
|||
|
||||
"1. numbers\n1. are ignored\n",
|
||||
"<ol>\n<li>numbers</li>\n<li>are ignored</li>\n</ol>\n",
|
||||
|
||||
`1. Foo
|
||||
|
||||
bar
|
||||
|
||||
|
||||
|
||||
qux
|
||||
`,
|
||||
`<ol>
|
||||
<li><p>Foo</p>
|
||||
|
||||
<pre><code>bar
|
||||
|
||||
|
||||
|
||||
qux
|
||||
</code></pre></li>
|
||||
</ol>
|
||||
`,
|
||||
}
|
||||
doTestsBlock(t, tests, 0)
|
||||
}
|
||||
|
|
@ -900,6 +960,14 @@ func TestDefinitionList(t *testing.T) {
|
|||
"<dd><p>Definition b</p></dd>\n" +
|
||||
"</dl>\n" +
|
||||
"\n<p>Text 2</p>\n",
|
||||
|
||||
"Term 1\n: Definition a\n\n Text 1\n\n 1. First\n 2. Second",
|
||||
"<dl>\n" +
|
||||
"<dt>Term 1</dt>\n" +
|
||||
"<dd><p>Definition a</p>\n\n" +
|
||||
"<p>Text 1</p>\n\n" +
|
||||
"<ol>\n<li>First</li>\n<li>Second</li>\n</ol></dd>\n" +
|
||||
"</dl>\n",
|
||||
}
|
||||
doTestsBlock(t, tests, EXTENSION_DEFINITION_LISTS)
|
||||
}
|
||||
|
|
@ -985,6 +1053,9 @@ func TestFencedCodeBlock(t *testing.T) {
|
|||
"``` go\nfunc foo() bool {\n\treturn true;\n}\n```\n",
|
||||
"<pre><code class=\"language-go\">func foo() bool {\n\treturn true;\n}\n</code></pre>\n",
|
||||
|
||||
"``` go foo bar\nfunc foo() bool {\n\treturn true;\n}\n```\n",
|
||||
"<pre><code class=\"language-go\">func foo() bool {\n\treturn true;\n}\n</code></pre>\n",
|
||||
|
||||
"``` c\n/* special & char < > \" escaping */\n```\n",
|
||||
"<pre><code class=\"language-c\">/* special & char < > " escaping */\n</code></pre>\n",
|
||||
|
||||
|
|
@ -1025,7 +1096,7 @@ func TestFencedCodeBlock(t *testing.T) {
|
|||
"<p>``` lisp\nno ending</p>\n",
|
||||
|
||||
"~~~ lisp\nend with language\n~~~ lisp\n",
|
||||
"<p>~~~ lisp\nend with language\n~~~ lisp</p>\n",
|
||||
"<pre><code class=\"language-lisp\">end with language\n</code></pre>\n\n<p>lisp</p>\n",
|
||||
|
||||
"```\nmismatched begin and end\n~~~\n",
|
||||
"<p>```\nmismatched begin and end\n~~~</p>\n",
|
||||
|
|
@ -1062,6 +1133,27 @@ func TestFencedCodeBlock(t *testing.T) {
|
|||
|
||||
"Some text before a fenced code block\n``` oz\ncode blocks breakup paragraphs\n```\nSome text in between\n``` oz\nmultiple code blocks work okay\n```\nAnd some text after a fenced code block",
|
||||
"<p>Some text before a fenced code block</p>\n\n<pre><code class=\"language-oz\">code blocks breakup paragraphs\n</code></pre>\n\n<p>Some text in between</p>\n\n<pre><code class=\"language-oz\">multiple code blocks work okay\n</code></pre>\n\n<p>And some text after a fenced code block</p>\n",
|
||||
|
||||
"```\n[]:()\n```\n",
|
||||
"<pre><code>[]:()\n</code></pre>\n",
|
||||
|
||||
"```\n[]:()\n[]:)\n[]:(\n[]:x\n[]:testing\n[:testing\n\n[]:\nlinebreak\n[]()\n\n[]:\n[]()\n```",
|
||||
"<pre><code>[]:()\n[]:)\n[]:(\n[]:x\n[]:testing\n[:testing\n\n[]:\nlinebreak\n[]()\n\n[]:\n[]()\n</code></pre>\n",
|
||||
|
||||
"- test\n\n```\n codeblock\n ```\ntest\n",
|
||||
"<ul>\n<li><p>test</p>\n\n<pre><code>codeblock\n</code></pre></li>\n</ul>\n\n<p>test</p>\n",
|
||||
|
||||
"- ```\n codeblock\n ```\n\n- test\n",
|
||||
"<ul>\n<li><pre><code>codeblock\n</code></pre></li>\n\n<li><p>test</p></li>\n</ul>\n",
|
||||
|
||||
"- test\n- ```\n codeblock\n ```\n",
|
||||
"<ul>\n<li>test</li>\n\n<li><pre><code>codeblock\n</code></pre></li>\n</ul>\n",
|
||||
|
||||
"- test\n```\ncodeblock\n```\n\n- test\n",
|
||||
"<ul>\n<li><p>test</p>\n\n<pre><code>codeblock\n</code></pre></li>\n\n<li><p>test</p></li>\n</ul>\n",
|
||||
|
||||
"- test\n```go\nfunc foo() bool {\n\treturn true;\n}\n```\n\n- test\n",
|
||||
"<ul>\n<li><p>test</p>\n\n<pre><code class=\"language-go\">func foo() bool {\n\treturn true;\n}\n</code></pre></li>\n\n<li><p>test</p></li>\n</ul>\n",
|
||||
}
|
||||
doTestsBlock(t, tests, EXTENSION_FENCED_CODE)
|
||||
}
|
||||
|
|
@ -1437,6 +1529,9 @@ func TestFencedCodeBlock_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) {
|
|||
"``` go\nfunc foo() bool {\n\treturn true;\n}\n```\n",
|
||||
"<pre><code class=\"language-go\">func foo() bool {\n\treturn true;\n}\n</code></pre>\n",
|
||||
|
||||
"``` go foo bar\nfunc foo() bool {\n\treturn true;\n}\n```\n",
|
||||
"<pre><code class=\"language-go\">func foo() bool {\n\treturn true;\n}\n</code></pre>\n",
|
||||
|
||||
"``` c\n/* special & char < > \" escaping */\n```\n",
|
||||
"<pre><code class=\"language-c\">/* special & char < > " escaping */\n</code></pre>\n",
|
||||
|
||||
|
|
@ -1477,7 +1572,7 @@ func TestFencedCodeBlock_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) {
|
|||
"<p>``` lisp\nno ending</p>\n",
|
||||
|
||||
"~~~ lisp\nend with language\n~~~ lisp\n",
|
||||
"<p>~~~ lisp\nend with language\n~~~ lisp</p>\n",
|
||||
"<pre><code class=\"language-lisp\">end with language\n</code></pre>\n\n<p>lisp</p>\n",
|
||||
|
||||
"```\nmismatched begin and end\n~~~\n",
|
||||
"<p>```\nmismatched begin and end\n~~~</p>\n",
|
||||
|
|
@ -1503,6 +1598,44 @@ func TestFencedCodeBlock_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) {
|
|||
doTestsBlock(t, tests, EXTENSION_FENCED_CODE|EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK)
|
||||
}
|
||||
|
||||
func TestListWithFencedCodeBlock(t *testing.T) {
|
||||
var tests = []string{
|
||||
"1. one\n\n ```\n code\n ```\n\n2. two\n",
|
||||
"<ol>\n<li><p>one</p>\n\n<pre><code>code\n</code></pre></li>\n\n<li><p>two</p></li>\n</ol>\n",
|
||||
// https://github.com/russross/blackfriday/issues/239
|
||||
"1. one\n\n ```\n - code\n ```\n\n2. two\n",
|
||||
"<ol>\n<li><p>one</p>\n\n<pre><code>- code\n</code></pre></li>\n\n<li><p>two</p></li>\n</ol>\n",
|
||||
}
|
||||
doTestsBlock(t, tests, EXTENSION_FENCED_CODE)
|
||||
}
|
||||
|
||||
func TestListWithMalformedFencedCodeBlock(t *testing.T) {
|
||||
// Ensure that in the case of an unclosed fenced code block in a list,
|
||||
// no source gets ommitted (even if it is malformed).
|
||||
// See russross/blackfriday#372 for context.
|
||||
var tests = []string{
|
||||
"1. one\n\n ```\n code\n\n2. two\n",
|
||||
"<ol>\n<li>one\n\n```\ncode\n\n2. two</li>\n</ol>\n",
|
||||
|
||||
"1. one\n\n ```\n - code\n\n2. two\n",
|
||||
"<ol>\n<li>one\n\n```\n- code\n\n2. two</li>\n</ol>\n",
|
||||
}
|
||||
doTestsBlock(t, tests, EXTENSION_FENCED_CODE)
|
||||
}
|
||||
|
||||
func TestListWithFencedCodeBlockNoExtensions(t *testing.T) {
|
||||
// If there is a fenced code block in a list, and FencedCode is not set,
|
||||
// lists should be processed normally.
|
||||
var tests = []string{
|
||||
"1. one\n\n ```\n code\n ```\n\n2. two\n",
|
||||
"<ol>\n<li><p>one</p>\n\n<p><code>\ncode\n</code></p></li>\n\n<li><p>two</p></li>\n</ol>\n",
|
||||
|
||||
"1. one\n\n ```\n - code\n ```\n\n2. two\n",
|
||||
"<ol>\n<li><p>one</p>\n\n<p>```</p>\n\n<ul>\n<li>code\n```</li>\n</ul></li>\n\n<li><p>two</p></li>\n</ol>\n",
|
||||
}
|
||||
doTestsBlock(t, tests, 0)
|
||||
}
|
||||
|
||||
func TestTitleBlock_EXTENSION_TITLEBLOCK(t *testing.T) {
|
||||
var tests = []string{
|
||||
"% Some title\n" +
|
||||
|
|
@ -1530,3 +1663,194 @@ func TestBlockComments(t *testing.T) {
|
|||
}
|
||||
doTestsBlock(t, tests, 0)
|
||||
}
|
||||
|
||||
func TestCDATA(t *testing.T) {
|
||||
var tests = []string{
|
||||
"Some text\n\n<![CDATA[foo]]>\n",
|
||||
"<p>Some text</p>\n\n<![CDATA[foo]]>\n",
|
||||
|
||||
"CDATA ]]\n\n<![CDATA[]]]]>\n",
|
||||
"<p>CDATA ]]</p>\n\n<![CDATA[]]]]>\n",
|
||||
|
||||
"CDATA >\n\n<![CDATA[>]]>\n",
|
||||
"<p>CDATA ></p>\n\n<![CDATA[>]]>\n",
|
||||
|
||||
"Lots of text\n\n<![CDATA[lots of te><t\non\nseveral\nlines]]>\n",
|
||||
"<p>Lots of text</p>\n\n<![CDATA[lots of te><t\non\nseveral\nlines]]>\n",
|
||||
|
||||
"<![CDATA[>]]>\n",
|
||||
"<![CDATA[>]]>\n",
|
||||
}
|
||||
doTestsBlock(t, tests, 0)
|
||||
doTestsBlock(t, []string{
|
||||
"``` html\n<![CDATA[foo]]>\n```\n",
|
||||
"<pre><code class=\"language-html\"><![CDATA[foo]]>\n</code></pre>\n",
|
||||
|
||||
"<![CDATA[\n``` python\ndef func():\n pass\n```\n]]>\n",
|
||||
"<![CDATA[\n``` python\ndef func():\n pass\n```\n]]>\n",
|
||||
|
||||
`<![CDATA[
|
||||
> def func():
|
||||
> pass
|
||||
]]>
|
||||
`,
|
||||
`<![CDATA[
|
||||
> def func():
|
||||
> pass
|
||||
]]>
|
||||
`,
|
||||
}, EXTENSION_FENCED_CODE)
|
||||
}
|
||||
|
||||
func TestIsFenceLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
data []byte
|
||||
infoRequested bool
|
||||
newlineOptional bool
|
||||
wantEnd int
|
||||
wantMarker string
|
||||
wantInfo string
|
||||
}{
|
||||
{
|
||||
data: []byte("```"),
|
||||
wantEnd: 0,
|
||||
},
|
||||
{
|
||||
data: []byte("```\nstuff here\n"),
|
||||
wantEnd: 4,
|
||||
wantMarker: "```",
|
||||
},
|
||||
{
|
||||
data: []byte("```\nstuff here\n"),
|
||||
infoRequested: true,
|
||||
wantEnd: 4,
|
||||
wantMarker: "```",
|
||||
},
|
||||
{
|
||||
data: []byte("stuff here\n```\n"),
|
||||
wantEnd: 0,
|
||||
},
|
||||
{
|
||||
data: []byte("```"),
|
||||
newlineOptional: true,
|
||||
wantEnd: 3,
|
||||
wantMarker: "```",
|
||||
},
|
||||
{
|
||||
data: []byte("```"),
|
||||
infoRequested: true,
|
||||
newlineOptional: true,
|
||||
wantEnd: 3,
|
||||
wantMarker: "```",
|
||||
},
|
||||
{
|
||||
data: []byte("``` go"),
|
||||
infoRequested: true,
|
||||
newlineOptional: true,
|
||||
wantEnd: 6,
|
||||
wantMarker: "```",
|
||||
wantInfo: "go",
|
||||
},
|
||||
{
|
||||
data: []byte("``` go foo bar"),
|
||||
infoRequested: true,
|
||||
newlineOptional: true,
|
||||
wantEnd: 14,
|
||||
wantMarker: "```",
|
||||
wantInfo: "go foo bar",
|
||||
},
|
||||
{
|
||||
data: []byte("``` go foo bar "),
|
||||
infoRequested: true,
|
||||
newlineOptional: true,
|
||||
wantEnd: 16,
|
||||
wantMarker: "```",
|
||||
wantInfo: "go foo bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
var info *string
|
||||
if test.infoRequested {
|
||||
info = new(string)
|
||||
}
|
||||
end, marker := isFenceLine(test.data, info, "```", test.newlineOptional)
|
||||
if got, want := end, test.wantEnd; got != want {
|
||||
t.Errorf("got end %v, want %v", got, want)
|
||||
}
|
||||
if got, want := marker, test.wantMarker; got != want {
|
||||
t.Errorf("got marker %q, want %q", got, want)
|
||||
}
|
||||
if test.infoRequested {
|
||||
if got, want := *info, test.wantInfo; got != want {
|
||||
t.Errorf("got info %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinLines(t *testing.T) {
|
||||
input := `# 标题
|
||||
|
||||
第一
|
||||
行文字。
|
||||
|
||||
第
|
||||
二
|
||||
行文字。
|
||||
`
|
||||
result := `<h1>标题</h1>
|
||||
|
||||
<p>第一行文字。</p>
|
||||
|
||||
<p>第二行文字。</p>
|
||||
`
|
||||
opt := Options{Extensions: commonExtensions | EXTENSION_JOIN_LINES}
|
||||
renderer := HtmlRenderer(commonHtmlFlags, "", "")
|
||||
output := MarkdownOptions([]byte(input), renderer, opt)
|
||||
|
||||
if string(output) != result {
|
||||
t.Error("output dose not match.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizedAnchorName(t *testing.T) {
|
||||
tests := []struct {
|
||||
text string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
text: "This is a header",
|
||||
want: "this-is-a-header",
|
||||
},
|
||||
{
|
||||
text: "This is also a header",
|
||||
want: "this-is-also-a-header",
|
||||
},
|
||||
{
|
||||
text: "main.go",
|
||||
want: "main-go",
|
||||
},
|
||||
{
|
||||
text: "Article 123",
|
||||
want: "article-123",
|
||||
},
|
||||
{
|
||||
text: "<- Let's try this, shall we?",
|
||||
want: "let-s-try-this-shall-we",
|
||||
},
|
||||
{
|
||||
text: " ",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
text: "Hello, 世界",
|
||||
want: "hello-世界",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := SanitizedAnchorName(test.text); got != test.want {
|
||||
t.Errorf("SanitizedAnchorName(%q):\ngot %q\nwant %q", test.text, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
38
html.go
38
html.go
|
|
@ -32,6 +32,7 @@ const (
|
|||
HTML_SAFELINK // only link to trusted protocols
|
||||
HTML_NOFOLLOW_LINKS // only link with rel="nofollow"
|
||||
HTML_NOREFERRER_LINKS // only link with rel="noreferrer"
|
||||
HTML_NOOPENER_LINKS // only link with rel="noopener"
|
||||
HTML_HREF_TARGET_BLANK // add a blank target
|
||||
HTML_TOC // generate a table of contents
|
||||
HTML_OMIT_CONTENTS // skip the main contents (for a standalone table of contents)
|
||||
|
|
@ -42,6 +43,7 @@ const (
|
|||
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_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
|
||||
)
|
||||
|
||||
|
|
@ -254,33 +256,21 @@ func (options *Html) HRule(out *bytes.Buffer) {
|
|||
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)
|
||||
|
||||
// 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>")
|
||||
} else {
|
||||
out.WriteString("<pre><code class=\"language-")
|
||||
attrEscape(out, []byte(lang))
|
||||
out.WriteString("\">")
|
||||
}
|
||||
|
||||
attrEscape(out, text)
|
||||
out.WriteString("</code></pre>\n")
|
||||
}
|
||||
|
|
@ -456,6 +446,9 @@ func (options *Html) AutoLink(out *bytes.Buffer, link []byte, kind int) {
|
|||
if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) {
|
||||
relAttrs = append(relAttrs, "noreferrer")
|
||||
}
|
||||
if options.flags&HTML_NOOPENER_LINKS != 0 && !isRelativeLink(link) {
|
||||
relAttrs = append(relAttrs, "noopener")
|
||||
}
|
||||
if len(relAttrs) > 0 {
|
||||
out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
|
||||
}
|
||||
|
|
@ -570,6 +563,9 @@ func (options *Html) Link(out *bytes.Buffer, link []byte, title []byte, content
|
|||
if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) {
|
||||
relAttrs = append(relAttrs, "noreferrer")
|
||||
}
|
||||
if options.flags&HTML_NOOPENER_LINKS != 0 && !isRelativeLink(link) {
|
||||
relAttrs = append(relAttrs, "noopener")
|
||||
}
|
||||
if len(relAttrs) > 0 {
|
||||
out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
|
||||
}
|
||||
|
|
@ -619,7 +615,7 @@ func (options *Html) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
|
|||
out.WriteString(`fnref:`)
|
||||
out.WriteString(options.parameters.FootnoteAnchorPrefix)
|
||||
out.Write(slug)
|
||||
out.WriteString(`"><a rel="footnote" href="#`)
|
||||
out.WriteString(`"><a href="#`)
|
||||
out.WriteString(`fn:`)
|
||||
out.WriteString(options.parameters.FootnoteAnchorPrefix)
|
||||
out.Write(slug)
|
||||
|
|
|
|||
37
inline.go
37
inline.go
|
|
@ -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 = 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?
|
||||
if p.flags&EXTENSION_HARD_LINE_BREAK == 0 && !precededByTwoSpaces && !precededByBackslash {
|
||||
return 0
|
||||
|
|
@ -240,13 +244,15 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int {
|
|||
i++
|
||||
}
|
||||
|
||||
brace := 0
|
||||
|
||||
// look for the matching closing bracket
|
||||
for level := 1; level > 0 && i < len(data); i++ {
|
||||
switch {
|
||||
case data[i] == '\n':
|
||||
textHasNl = true
|
||||
|
||||
case data[i-1] == '\\':
|
||||
case isBackslashEscaped(data, i):
|
||||
continue
|
||||
|
||||
case data[i] == '[':
|
||||
|
|
@ -273,8 +279,8 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int {
|
|||
i++
|
||||
}
|
||||
|
||||
// inline style link
|
||||
switch {
|
||||
// inline style link
|
||||
case i < len(data) && data[i] == '(':
|
||||
// skip initial whitespace
|
||||
i++
|
||||
|
|
@ -285,14 +291,27 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int {
|
|||
|
||||
linkB := i
|
||||
|
||||
// look for link end: ' " )
|
||||
// look for link end: ' " ), check for new opening braces and take this
|
||||
// into account, this may lead for overshooting and probably will require
|
||||
// some fine-tuning.
|
||||
findlinkend:
|
||||
for i < len(data) {
|
||||
switch {
|
||||
case data[i] == '\\':
|
||||
i += 2
|
||||
|
||||
case data[i] == ')' || data[i] == '\'' || data[i] == '"':
|
||||
case data[i] == '(':
|
||||
brace++
|
||||
i++
|
||||
|
||||
case data[i] == ')':
|
||||
if brace <= 0 {
|
||||
break findlinkend
|
||||
}
|
||||
brace--
|
||||
i++
|
||||
|
||||
case data[i] == '\'' || data[i] == '"':
|
||||
break findlinkend
|
||||
|
||||
default:
|
||||
|
|
@ -469,6 +488,7 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int {
|
|||
}
|
||||
|
||||
p.notes = append(p.notes, ref)
|
||||
p.notesRecord[string(ref.link)] = struct{}{}
|
||||
|
||||
link = ref.link
|
||||
title = ref.title
|
||||
|
|
@ -479,9 +499,10 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int {
|
|||
return 0
|
||||
}
|
||||
|
||||
if t == linkDeferredFootnote {
|
||||
if t == linkDeferredFootnote && !p.isFootnote(lr) {
|
||||
lr.noteId = len(p.notes) + 1
|
||||
p.notes = append(p.notes, lr)
|
||||
p.notesRecord[string(lr.link)] = struct{}{}
|
||||
}
|
||||
|
||||
// keep link and title from reference
|
||||
|
|
@ -560,7 +581,7 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int {
|
|||
return i
|
||||
}
|
||||
|
||||
func (p *parser) inlineHtmlComment(out *bytes.Buffer, data []byte) int {
|
||||
func (p *parser) inlineHTMLComment(out *bytes.Buffer, data []byte) int {
|
||||
if len(data) < 5 {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -584,7 +605,7 @@ func leftAngle(p *parser, out *bytes.Buffer, data []byte, offset int) int {
|
|||
data = data[offset:]
|
||||
altype := LINK_TYPE_NOT_AUTOLINK
|
||||
end := tagLength(data, &altype)
|
||||
if size := p.inlineHtmlComment(out, data); size > 0 {
|
||||
if size := p.inlineHTMLComment(out, data); size > 0 {
|
||||
end = size
|
||||
}
|
||||
if end > 2 {
|
||||
|
|
@ -923,7 +944,7 @@ func isMailtoAutoLink(data []byte) int {
|
|||
nb++
|
||||
|
||||
case '-', '.', '_':
|
||||
break
|
||||
// Do nothing.
|
||||
|
||||
case '>':
|
||||
if nb == 1 {
|
||||
|
|
|
|||
182
inline_test.go
182
inline_test.go
|
|
@ -15,9 +15,8 @@ package blackfriday
|
|||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func runMarkdownInline(input string, opts Options, htmlFlags int, params HtmlRendererParameters) string {
|
||||
|
|
@ -60,13 +59,11 @@ func doTestsInlineParam(t *testing.T, tests []string, opts Options, htmlFlags in
|
|||
params HtmlRendererParameters) {
|
||||
// catch and report panics
|
||||
var candidate string
|
||||
/*
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
t.Errorf("\npanic while processing [%#v] (%v)\n", candidate, err)
|
||||
}
|
||||
}()
|
||||
*/
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
t.Errorf("\npanic while processing [%#v]: %s\n", candidate, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i+1 < len(tests); i += 2 {
|
||||
input := tests[i]
|
||||
|
|
@ -156,10 +153,35 @@ func TestEmphasis(t *testing.T) {
|
|||
|
||||
"*What is A\\* algorithm?*\n",
|
||||
"<p><em>What is A* algorithm?</em></p>\n",
|
||||
|
||||
"some para_graph with _emphasised_ text.\n",
|
||||
"<p>some para_graph with <em>emphasised</em> text.</p>\n",
|
||||
|
||||
"some paragraph with _emphasised_ te_xt.\n",
|
||||
"<p>some paragraph with <em>emphasised</em> te_xt.</p>\n",
|
||||
|
||||
"some paragraph with t_wo bi_ts of _emphasised_ text.\n",
|
||||
"<p>some paragraph with t<em>wo bi</em>ts of <em>emphasised</em> text.</p>\n",
|
||||
|
||||
"un*frigging*believable\n",
|
||||
"<p>un<em>frigging</em>believable</p>\n",
|
||||
}
|
||||
doTestsInline(t, tests)
|
||||
}
|
||||
|
||||
func TestNoIntraEmphasis(t *testing.T) {
|
||||
tests := []string{
|
||||
"some para_graph with _emphasised_ text.\n",
|
||||
"<p>some para_graph with <em>emphasised</em> text.</p>\n",
|
||||
|
||||
"un*frigging*believable\n",
|
||||
"<p>un*frigging*believable</p>\n",
|
||||
}
|
||||
doTestsInlineParam(t, tests, Options{
|
||||
Extensions: EXTENSION_NO_INTRA_EMPHASIS},
|
||||
0, HtmlRendererParameters{})
|
||||
}
|
||||
|
||||
func TestReferenceOverride(t *testing.T) {
|
||||
var tests = []string{
|
||||
"test [ref1][]\n",
|
||||
|
|
@ -541,6 +563,20 @@ func TestInlineLink(t *testing.T) {
|
|||
|
||||
"[link](<../>)\n",
|
||||
"<p><a href=\"../\">link</a></p>\n",
|
||||
|
||||
// Issue 116 in blackfriday
|
||||
".jpg)",
|
||||
"<p><img src=\"http://www.broadgate.co.uk/Content/Upload/DetailImages/Cyclus700(1).jpg\" alt=\"\" /></p>\n",
|
||||
|
||||
// no closing ), autolinking detects the url next
|
||||
"[disambiguation](http://en.wikipedia.org/wiki/Disambiguation_(disambiguation) is the",
|
||||
"<p>[disambiguation](<a href=\"http://en.wikipedia.org/wiki/Disambiguation_(disambiguation\">http://en.wikipedia.org/wiki/Disambiguation_(disambiguation</a>) is the</p>\n",
|
||||
|
||||
"[disambiguation](http://en.wikipedia.org/wiki/Disambiguation_(disambiguation)) is the",
|
||||
"<p><a href=\"http://en.wikipedia.org/wiki/Disambiguation_(disambiguation)\">disambiguation</a> is the</p>\n",
|
||||
|
||||
"[disambiguation](http://en.wikipedia.org/wiki/Disambiguation_(disambiguation))",
|
||||
"<p><a href=\"http://en.wikipedia.org/wiki/Disambiguation_(disambiguation)\">disambiguation</a></p>\n",
|
||||
}
|
||||
doLinkTestsInline(t, tests)
|
||||
|
||||
|
|
@ -588,6 +624,25 @@ func TestRelAttrLink(t *testing.T) {
|
|||
}
|
||||
doTestsInlineParam(t, nofollownoreferrerTests, Options{}, HTML_SAFELINK|HTML_NOFOLLOW_LINKS|HTML_NOREFERRER_LINKS,
|
||||
HtmlRendererParameters{})
|
||||
|
||||
var noopenerTests = []string{
|
||||
"[foo](http://bar.com/foo/)\n",
|
||||
"<p><a href=\"http://bar.com/foo/\" rel=\"noopener\">foo</a></p>\n",
|
||||
|
||||
"[foo](/bar/)\n",
|
||||
"<p><a href=\"/bar/\">foo</a></p>\n",
|
||||
}
|
||||
doTestsInlineParam(t, noopenerTests, Options{}, HTML_SAFELINK|HTML_NOOPENER_LINKS, HtmlRendererParameters{})
|
||||
|
||||
var nofollownoreferrernoopenerTests = []string{
|
||||
"[foo](http://bar.com/foo/)\n",
|
||||
"<p><a href=\"http://bar.com/foo/\" rel=\"nofollow noreferrer noopener\">foo</a></p>\n",
|
||||
|
||||
"[foo](/bar/)\n",
|
||||
"<p><a href=\"/bar/\">foo</a></p>\n",
|
||||
}
|
||||
doTestsInlineParam(t, nofollownoreferrernoopenerTests, Options{},
|
||||
HTML_SAFELINK|HTML_NOOPENER_LINKS|HTML_NOFOLLOW_LINKS|HTML_NOREFERRER_LINKS, HtmlRendererParameters{})
|
||||
}
|
||||
|
||||
func TestHrefTargetBlank(t *testing.T) {
|
||||
|
|
@ -681,6 +736,9 @@ func TestReferenceLink(t *testing.T) {
|
|||
|
||||
"[ref]\n [ref]: ../url/ \"title\"\n",
|
||||
"<p><a href=\"../url/\" title=\"title\">ref</a></p>\n",
|
||||
|
||||
"[link][ref]\n [ref]: /url/",
|
||||
"<p><a href=\"/url/\">link</a></p>\n",
|
||||
}
|
||||
doLinkTestsInline(t, tests)
|
||||
}
|
||||
|
|
@ -814,7 +872,7 @@ func TestAutoLink(t *testing.T) {
|
|||
|
||||
var footnoteTests = []string{
|
||||
"testing footnotes.[^a]\n\n[^a]: This is the note\n",
|
||||
`<p>testing footnotes.<sup class="footnote-ref" id="fnref:a"><a rel="footnote" href="#fn:a">1</a></sup></p>
|
||||
`<p>testing footnotes.<sup class="footnote-ref" id="fnref:a"><a href="#fn:a">1</a></sup></p>
|
||||
<div class="footnotes">
|
||||
|
||||
<hr />
|
||||
|
|
@ -838,7 +896,7 @@ var footnoteTests = []string{
|
|||
|
||||
No longer in the footnote
|
||||
`,
|
||||
`<p>testing long<sup class="footnote-ref" id="fnref:b"><a rel="footnote" href="#fn:b">1</a></sup> notes.</p>
|
||||
`<p>testing long<sup class="footnote-ref" id="fnref:b"><a href="#fn:b">1</a></sup> notes.</p>
|
||||
|
||||
<p>No longer in the footnote</p>
|
||||
<div class="footnotes">
|
||||
|
|
@ -874,7 +932,7 @@ what happens here
|
|||
[note]: /link/c
|
||||
|
||||
`,
|
||||
`<p>testing<sup class="footnote-ref" id="fnref:c"><a rel="footnote" href="#fn:c">1</a></sup> multiple<sup class="footnote-ref" id="fnref:d"><a rel="footnote" href="#fn:d">2</a></sup> notes.</p>
|
||||
`<p>testing<sup class="footnote-ref" id="fnref:c"><a href="#fn:c">1</a></sup> multiple<sup class="footnote-ref" id="fnref:d"><a href="#fn:d">2</a></sup> notes.</p>
|
||||
|
||||
<p>omg</p>
|
||||
|
||||
|
|
@ -893,7 +951,7 @@ what happens here
|
|||
`,
|
||||
|
||||
"testing inline^[this is the note] notes.\n",
|
||||
`<p>testing inline<sup class="footnote-ref" id="fnref:this-is-the-note"><a rel="footnote" href="#fn:this-is-the-note">1</a></sup> notes.</p>
|
||||
`<p>testing inline<sup class="footnote-ref" id="fnref:this-is-the-note"><a href="#fn:this-is-the-note">1</a></sup> notes.</p>
|
||||
<div class="footnotes">
|
||||
|
||||
<hr />
|
||||
|
|
@ -905,7 +963,7 @@ what happens here
|
|||
`,
|
||||
|
||||
"testing multiple[^1] types^[inline note] of notes[^2]\n\n[^2]: the second deferred note\n[^1]: the first deferred note\n\n\twhich happens to be a block\n",
|
||||
`<p>testing multiple<sup class="footnote-ref" id="fnref:1"><a rel="footnote" href="#fn:1">1</a></sup> types<sup class="footnote-ref" id="fnref:inline-note"><a rel="footnote" href="#fn:inline-note">2</a></sup> of notes<sup class="footnote-ref" id="fnref:2"><a rel="footnote" href="#fn:2">3</a></sup></p>
|
||||
`<p>testing multiple<sup class="footnote-ref" id="fnref:1"><a href="#fn:1">1</a></sup> types<sup class="footnote-ref" id="fnref:inline-note"><a href="#fn:inline-note">2</a></sup> of notes<sup class="footnote-ref" id="fnref:2"><a href="#fn:2">3</a></sup></p>
|
||||
<div class="footnotes">
|
||||
|
||||
<hr />
|
||||
|
|
@ -928,7 +986,7 @@ what happens here
|
|||
|
||||
may be multiple paragraphs.
|
||||
`,
|
||||
`<p>This is a footnote<sup class="footnote-ref" id="fnref:1"><a rel="footnote" href="#fn:1">1</a></sup><sup class="footnote-ref" id="fnref:and-this-is-an-i"><a rel="footnote" href="#fn:and-this-is-an-i">2</a></sup></p>
|
||||
`<p>This is a footnote<sup class="footnote-ref" id="fnref:1"><a href="#fn:1">1</a></sup><sup class="footnote-ref" id="fnref:and-this-is-an-i"><a href="#fn:and-this-is-an-i">2</a></sup></p>
|
||||
<div class="footnotes">
|
||||
|
||||
<hr />
|
||||
|
|
@ -944,13 +1002,13 @@ what happens here
|
|||
`,
|
||||
|
||||
"empty footnote[^]\n\n[^]: fn text",
|
||||
"<p>empty footnote<sup class=\"footnote-ref\" id=\"fnref:\"><a rel=\"footnote\" href=\"#fn:\">1</a></sup></p>\n<div class=\"footnotes\">\n\n<hr />\n\n<ol>\n<li id=\"fn:\">fn text\n</li>\n</ol>\n</div>\n",
|
||||
"<p>empty footnote<sup class=\"footnote-ref\" id=\"fnref:\"><a href=\"#fn:\">1</a></sup></p>\n<div class=\"footnotes\">\n\n<hr />\n\n<ol>\n<li id=\"fn:\">fn text\n</li>\n</ol>\n</div>\n",
|
||||
|
||||
"Some text.[^note1]\n\n[^note1]: fn1",
|
||||
"<p>Some text.<sup class=\"footnote-ref\" id=\"fnref:note1\"><a rel=\"footnote\" href=\"#fn:note1\">1</a></sup></p>\n<div class=\"footnotes\">\n\n<hr />\n\n<ol>\n<li id=\"fn:note1\">fn1\n</li>\n</ol>\n</div>\n",
|
||||
"<p>Some text.<sup class=\"footnote-ref\" id=\"fnref:note1\"><a href=\"#fn:note1\">1</a></sup></p>\n<div class=\"footnotes\">\n\n<hr />\n\n<ol>\n<li id=\"fn:note1\">fn1\n</li>\n</ol>\n</div>\n",
|
||||
|
||||
"Some text.[^note1][^note2]\n\n[^note1]: fn1\n[^note2]: fn2\n",
|
||||
"<p>Some text.<sup class=\"footnote-ref\" id=\"fnref:note1\"><a rel=\"footnote\" href=\"#fn:note1\">1</a></sup><sup class=\"footnote-ref\" id=\"fnref:note2\"><a rel=\"footnote\" href=\"#fn:note2\">2</a></sup></p>\n<div class=\"footnotes\">\n\n<hr />\n\n<ol>\n<li id=\"fn:note1\">fn1\n</li>\n<li id=\"fn:note2\">fn2\n</li>\n</ol>\n</div>\n",
|
||||
"<p>Some text.<sup class=\"footnote-ref\" id=\"fnref:note1\"><a href=\"#fn:note1\">1</a></sup><sup class=\"footnote-ref\" id=\"fnref:note2\"><a href=\"#fn:note2\">2</a></sup></p>\n<div class=\"footnotes\">\n\n<hr />\n\n<ol>\n<li id=\"fn:note1\">fn1\n</li>\n<li id=\"fn:note2\">fn2\n</li>\n</ol>\n</div>\n",
|
||||
|
||||
`Bla bla [^1] [WWW][w3]
|
||||
|
||||
|
|
@ -958,7 +1016,7 @@ what happens here
|
|||
|
||||
[w3]: http://www.w3.org/
|
||||
`,
|
||||
`<p>Bla bla <sup class="footnote-ref" id="fnref:1"><a rel="footnote" href="#fn:1">1</a></sup> <a href="http://www.w3.org/">WWW</a></p>
|
||||
`<p>Bla bla <sup class="footnote-ref" id="fnref:1"><a href="#fn:1">1</a></sup> <a href="http://www.w3.org/">WWW</a></p>
|
||||
<div class="footnotes">
|
||||
|
||||
<hr />
|
||||
|
|
@ -974,7 +1032,7 @@ what happens here
|
|||
|
||||
[^fn1]: Fine print
|
||||
`,
|
||||
`<p>This is exciting!<sup class="footnote-ref" id="fnref:fn1"><a rel="footnote" href="#fn:fn1">1</a></sup></p>
|
||||
`<p>This is exciting!<sup class="footnote-ref" id="fnref:fn1"><a href="#fn:fn1">1</a></sup></p>
|
||||
<div class="footnotes">
|
||||
|
||||
<hr />
|
||||
|
|
@ -984,6 +1042,28 @@ what happens here
|
|||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
`,
|
||||
`testing footnotes.[^a]
|
||||
|
||||
test footnotes the second.[^b]
|
||||
|
||||
[^a]: This is the first note[^a].
|
||||
[^b]: this is the second note.[^a]
|
||||
`,
|
||||
`<p>testing footnotes.<sup class="footnote-ref" id="fnref:a"><a href="#fn:a">1</a></sup></p>
|
||||
|
||||
<p>test footnotes the second.<sup class="footnote-ref" id="fnref:b"><a href="#fn:b">2</a></sup></p>
|
||||
<div class="footnotes">
|
||||
|
||||
<hr />
|
||||
|
||||
<ol>
|
||||
<li id="fn:a">This is the first note<sup class="footnote-ref" id="fnref:a"><a href="#fn:a">1</a></sup>.
|
||||
</li>
|
||||
<li id="fn:b">this is the second note.<sup class="footnote-ref" id="fnref:a"><a href="#fn:a">1</a></sup>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
|
||||
|
|
@ -1025,18 +1105,46 @@ func TestNestedFootnotes(t *testing.T) {
|
|||
|
||||
[^fn2]:
|
||||
Obelisk`,
|
||||
`<p>Paragraph.<sup class="footnote-ref" id="fnref:fn1"><a rel="footnote" href="#fn:fn1">1</a></sup></p>
|
||||
`<p>Paragraph.<sup class="footnote-ref" id="fnref:fn1"><a href="#fn:fn1">1</a></sup></p>
|
||||
<div class="footnotes">
|
||||
|
||||
<hr />
|
||||
|
||||
<ol>
|
||||
<li id="fn:fn1">Asterisk<sup class="footnote-ref" id="fnref:fn2"><a rel="footnote" href="#fn:fn2">2</a></sup>
|
||||
<li id="fn:fn1">Asterisk<sup class="footnote-ref" id="fnref:fn2"><a href="#fn:fn2">2</a></sup>
|
||||
</li>
|
||||
<li id="fn:fn2">Obelisk
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
`,
|
||||
`This uses footnote A.[^A]
|
||||
|
||||
This uses footnote C.[^C]
|
||||
|
||||
[^A]:
|
||||
A note. use itself.[^A]
|
||||
[^B]:
|
||||
B note, uses A to test duplicate.[^A]
|
||||
[^C]:
|
||||
C note, uses B.[^B]
|
||||
`,
|
||||
`<p>This uses footnote A.<sup class="footnote-ref" id="fnref:A"><a href="#fn:A">1</a></sup></p>
|
||||
|
||||
<p>This uses footnote C.<sup class="footnote-ref" id="fnref:C"><a href="#fn:C">2</a></sup></p>
|
||||
<div class="footnotes">
|
||||
|
||||
<hr />
|
||||
|
||||
<ol>
|
||||
<li id="fn:A">A note. use itself.<sup class="footnote-ref" id="fnref:A"><a href="#fn:A">1</a></sup>
|
||||
</li>
|
||||
<li id="fn:C">C note, uses B.<sup class="footnote-ref" id="fnref:B"><a href="#fn:B">3</a></sup>
|
||||
</li>
|
||||
<li id="fn:B">B note, uses A to test duplicate.<sup class="footnote-ref" id="fnref:A"><a href="#fn:A">1</a></sup>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
doTestsInlineParam(t, tests, Options{Extensions: EXTENSION_FOOTNOTES}, 0,
|
||||
|
|
@ -1084,6 +1192,18 @@ func TestSmartDoubleQuotes(t *testing.T) {
|
|||
doTestsInlineParam(t, tests, Options{}, HTML_USE_SMARTYPANTS, HtmlRendererParameters{})
|
||||
}
|
||||
|
||||
func TestSmartDoubleQuotesNbsp(t *testing.T) {
|
||||
var tests = []string{
|
||||
"this should be normal \"quoted\" text.\n",
|
||||
"<p>this should be normal “ quoted ” text.</p>\n",
|
||||
"this \" single double\n",
|
||||
"<p>this “ single double</p>\n",
|
||||
"two pair of \"some\" quoted \"text\".\n",
|
||||
"<p>two pair of “ some ” quoted “ text ”.</p>\n"}
|
||||
|
||||
doTestsInlineParam(t, tests, Options{}, HTML_USE_SMARTYPANTS|HTML_SMARTYPANTS_QUOTES_NBSP, HtmlRendererParameters{})
|
||||
}
|
||||
|
||||
func TestSmartAngledDoubleQuotes(t *testing.T) {
|
||||
var tests = []string{
|
||||
"this should be angled \"quoted\" text.\n",
|
||||
|
|
@ -1096,6 +1216,18 @@ func TestSmartAngledDoubleQuotes(t *testing.T) {
|
|||
doTestsInlineParam(t, tests, Options{}, HTML_USE_SMARTYPANTS|HTML_SMARTYPANTS_ANGLED_QUOTES, HtmlRendererParameters{})
|
||||
}
|
||||
|
||||
func TestSmartAngledDoubleQuotesNbsp(t *testing.T) {
|
||||
var tests = []string{
|
||||
"this should be angled \"quoted\" text.\n",
|
||||
"<p>this should be angled « quoted » text.</p>\n",
|
||||
"this \" single double\n",
|
||||
"<p>this « single double</p>\n",
|
||||
"two pair of \"some\" quoted \"text\".\n",
|
||||
"<p>two pair of « some » quoted « text ».</p>\n"}
|
||||
|
||||
doTestsInlineParam(t, tests, Options{}, HTML_USE_SMARTYPANTS|HTML_SMARTYPANTS_ANGLED_QUOTES|HTML_SMARTYPANTS_QUOTES_NBSP, HtmlRendererParameters{})
|
||||
}
|
||||
|
||||
func TestSmartFractions(t *testing.T) {
|
||||
var tests = []string{
|
||||
"1/2, 1/4 and 3/4; 1/4th and 3/4ths\n",
|
||||
|
|
@ -1151,3 +1283,9 @@ func TestDisableSmartDashes(t *testing.T) {
|
|||
HTML_USE_SMARTYPANTS|HTML_SMARTYPANTS_LATEX_DASHES,
|
||||
HtmlRendererParameters{})
|
||||
}
|
||||
|
||||
func BenchmarkSmartDoubleQuotes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
runMarkdownInline("this should be normal \"quoted\" text.\n", Options{}, HTML_USE_SMARTYPANTS, HtmlRendererParameters{})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
latex.go
8
latex.go
|
|
@ -17,6 +17,7 @@ package blackfriday
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
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")
|
||||
} else {
|
||||
lang := strings.Fields(info)[0]
|
||||
out.WriteString("\n\\begin{lstlisting}[language=")
|
||||
out.WriteString(lang)
|
||||
out.WriteString("]\n")
|
||||
}
|
||||
out.Write(text)
|
||||
if lang == "" {
|
||||
if info == "" {
|
||||
out.WriteString("\n\\end{verbatim}\n")
|
||||
} else {
|
||||
out.WriteString("\n\\end{lstlisting}\n")
|
||||
|
|
|
|||
193
markdown.go
193
markdown.go
|
|
@ -13,9 +13,6 @@
|
|||
//
|
||||
//
|
||||
|
||||
// Blackfriday markdown processor.
|
||||
//
|
||||
// Translates plain text with simple formatting rules into HTML or LaTeX.
|
||||
package blackfriday
|
||||
|
||||
import (
|
||||
|
|
@ -25,7 +22,7 @@ import (
|
|||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const VERSION = "1.4"
|
||||
const VERSION = "1.5"
|
||||
|
||||
// These are the supported markdown parsing extensions.
|
||||
// OR these values together to select multiple extensions.
|
||||
|
|
@ -46,6 +43,7 @@ const (
|
|||
EXTENSION_AUTO_HEADER_IDS // Create the header ID from the text
|
||||
EXTENSION_BACKSLASH_LINE_BREAK // translate trailing backslashes into line breaks
|
||||
EXTENSION_DEFINITION_LISTS // render definition lists
|
||||
EXTENSION_JOIN_LINES // delete newline and join lines
|
||||
|
||||
commonHtmlFlags = 0 |
|
||||
HTML_USE_XHTML |
|
||||
|
|
@ -105,46 +103,48 @@ const (
|
|||
// blockTags is a set of tags that are recognized as HTML block tags.
|
||||
// Any of these can be included in markdown text without special escaping.
|
||||
var blockTags = map[string]struct{}{
|
||||
"blockquote": struct{}{},
|
||||
"del": struct{}{},
|
||||
"div": struct{}{},
|
||||
"dl": struct{}{},
|
||||
"fieldset": struct{}{},
|
||||
"form": struct{}{},
|
||||
"h1": struct{}{},
|
||||
"h2": struct{}{},
|
||||
"h3": struct{}{},
|
||||
"h4": struct{}{},
|
||||
"h5": struct{}{},
|
||||
"h6": struct{}{},
|
||||
"iframe": struct{}{},
|
||||
"ins": struct{}{},
|
||||
"math": struct{}{},
|
||||
"noscript": struct{}{},
|
||||
"ol": struct{}{},
|
||||
"pre": struct{}{},
|
||||
"p": struct{}{},
|
||||
"script": struct{}{},
|
||||
"style": struct{}{},
|
||||
"table": struct{}{},
|
||||
"ul": struct{}{},
|
||||
"blockquote": {},
|
||||
"del": {},
|
||||
"div": {},
|
||||
"dl": {},
|
||||
"fieldset": {},
|
||||
"form": {},
|
||||
"h1": {},
|
||||
"h2": {},
|
||||
"h3": {},
|
||||
"h4": {},
|
||||
"h5": {},
|
||||
"h6": {},
|
||||
"iframe": {},
|
||||
"ins": {},
|
||||
"math": {},
|
||||
"noscript": {},
|
||||
"ol": {},
|
||||
"pre": {},
|
||||
"p": {},
|
||||
"script": {},
|
||||
"style": {},
|
||||
"table": {},
|
||||
"ul": {},
|
||||
|
||||
// HTML5
|
||||
"address": struct{}{},
|
||||
"article": struct{}{},
|
||||
"aside": struct{}{},
|
||||
"canvas": struct{}{},
|
||||
"figcaption": struct{}{},
|
||||
"figure": struct{}{},
|
||||
"footer": struct{}{},
|
||||
"header": struct{}{},
|
||||
"hgroup": struct{}{},
|
||||
"main": struct{}{},
|
||||
"nav": struct{}{},
|
||||
"output": struct{}{},
|
||||
"progress": struct{}{},
|
||||
"section": struct{}{},
|
||||
"video": struct{}{},
|
||||
"address": {},
|
||||
"article": {},
|
||||
"aside": {},
|
||||
"canvas": {},
|
||||
"details": {},
|
||||
"figcaption": {},
|
||||
"figure": {},
|
||||
"footer": {},
|
||||
"header": {},
|
||||
"hgroup": {},
|
||||
"main": {},
|
||||
"nav": {},
|
||||
"output": {},
|
||||
"progress": {},
|
||||
"section": {},
|
||||
"summary": {},
|
||||
"video": {},
|
||||
}
|
||||
|
||||
// Renderer is the rendering interface.
|
||||
|
|
@ -161,7 +161,7 @@ var blockTags = map[string]struct{}{
|
|||
// Currently Html and Latex implementations are provided
|
||||
type Renderer interface {
|
||||
// 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)
|
||||
BlockHtml(out *bytes.Buffer, text []byte)
|
||||
Header(out *bytes.Buffer, text func() bool, level int, id string)
|
||||
|
|
@ -220,7 +220,8 @@ type parser struct {
|
|||
// 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
|
||||
// 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) {
|
||||
|
|
@ -243,6 +244,11 @@ func (p *parser) getRef(refid string) (ref *reference, found bool) {
|
|||
return ref, found
|
||||
}
|
||||
|
||||
func (p *parser) isFootnote(ref *reference) bool {
|
||||
_, ok := p.notesRecord[string(ref.link)]
|
||||
return ok
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// Public interface
|
||||
|
|
@ -378,6 +384,7 @@ func MarkdownOptions(input []byte, renderer Renderer, opts Options) []byte {
|
|||
|
||||
if extensions&EXTENSION_FOOTNOTES != 0 {
|
||||
p.notes = make([]*reference, 0)
|
||||
p.notesRecord = make(map[string]struct{})
|
||||
}
|
||||
|
||||
first := firstPass(p, input)
|
||||
|
|
@ -386,9 +393,9 @@ func MarkdownOptions(input []byte, renderer Renderer, opts Options) []byte {
|
|||
}
|
||||
|
||||
// first pass:
|
||||
// - extract references
|
||||
// - expand tabs
|
||||
// - normalize newlines
|
||||
// - extract references (outside of fenced code blocks)
|
||||
// - expand tabs (outside of fenced code blocks)
|
||||
// - copy everything else
|
||||
func firstPass(p *parser, input []byte) []byte {
|
||||
var out bytes.Buffer
|
||||
|
|
@ -396,46 +403,46 @@ func firstPass(p *parser, input []byte) []byte {
|
|||
if p.flags&EXTENSION_TAB_SIZE_EIGHT != 0 {
|
||||
tabSize = TAB_SIZE_EIGHT
|
||||
}
|
||||
beg, end := 0, 0
|
||||
beg := 0
|
||||
lastFencedCodeBlockEnd := 0
|
||||
for beg < len(input) { // iterate over lines
|
||||
if end = isReference(p, input[beg:], tabSize); end > 0 {
|
||||
beg += end
|
||||
} else { // skip to the next line
|
||||
end = beg
|
||||
for end < len(input) && input[end] != '\n' && input[end] != '\r' {
|
||||
end++
|
||||
}
|
||||
|
||||
if p.flags&EXTENSION_FENCED_CODE != 0 {
|
||||
// track fenced code block boundaries to suppress tab expansion
|
||||
// inside them:
|
||||
if beg >= lastFencedCodeBlockEnd {
|
||||
if i := p.fencedCode(&out, input[beg:], false); i > 0 {
|
||||
lastFencedCodeBlockEnd = beg + i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add the line body if present
|
||||
if end > beg {
|
||||
if end < lastFencedCodeBlockEnd { // Do not expand tabs while inside fenced code blocks.
|
||||
out.Write(input[beg:end])
|
||||
} else {
|
||||
expandTabs(&out, input[beg:end], tabSize)
|
||||
}
|
||||
}
|
||||
out.WriteByte('\n')
|
||||
|
||||
if end < len(input) && input[end] == '\r' {
|
||||
end++
|
||||
}
|
||||
if end < len(input) && input[end] == '\n' {
|
||||
end++
|
||||
}
|
||||
|
||||
beg = end
|
||||
for beg < len(input) {
|
||||
// Find end of this line, then process the line.
|
||||
end := beg
|
||||
for end < len(input) && input[end] != '\n' && input[end] != '\r' {
|
||||
end++
|
||||
}
|
||||
|
||||
if p.flags&EXTENSION_FENCED_CODE != 0 {
|
||||
// track fenced code block boundaries to suppress tab expansion
|
||||
// and reference extraction inside them:
|
||||
if beg >= lastFencedCodeBlockEnd {
|
||||
if i := p.fencedCodeBlock(&out, input[beg:], false); i > 0 {
|
||||
lastFencedCodeBlockEnd = beg + i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add the line body if present
|
||||
if end > beg {
|
||||
if end < lastFencedCodeBlockEnd { // Do not expand tabs while inside fenced code blocks.
|
||||
out.Write(input[beg:end])
|
||||
} else if refEnd := isReference(p, input[beg:], tabSize); refEnd > 0 {
|
||||
beg += refEnd
|
||||
continue
|
||||
} else {
|
||||
expandTabs(&out, input[beg:end], tabSize)
|
||||
}
|
||||
}
|
||||
|
||||
if end < len(input) && input[end] == '\r' {
|
||||
end++
|
||||
}
|
||||
if end < len(input) && input[end] == '\n' {
|
||||
end++
|
||||
}
|
||||
out.WriteByte('\n')
|
||||
|
||||
beg = end
|
||||
}
|
||||
|
||||
// empty input?
|
||||
|
|
@ -635,12 +642,12 @@ func scanLinkRef(p *parser, data []byte, i int) (linkOffset, linkEnd, titleOffse
|
|||
i++
|
||||
}
|
||||
linkOffset = i
|
||||
for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' {
|
||||
i++
|
||||
}
|
||||
if i == len(data) {
|
||||
return
|
||||
}
|
||||
for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' {
|
||||
i++
|
||||
}
|
||||
linkEnd = i
|
||||
if data[linkOffset] == '<' && data[linkEnd-1] == '>' {
|
||||
linkOffset++
|
||||
|
|
@ -799,7 +806,17 @@ func ispunct(c byte) bool {
|
|||
|
||||
// Test if a character is a whitespace character.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// Blackfriday Markdown Processor
|
||||
// Available at http://github.com/russross/blackfriday
|
||||
//
|
||||
// Copyright © 2011 Russ Ross <russ@russross.com>.
|
||||
// Distributed under the Simplified BSD License.
|
||||
// See README.md for details.
|
||||
//
|
||||
|
||||
//
|
||||
// Unit tests for full document parsing and rendering
|
||||
//
|
||||
|
||||
package blackfriday
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func runMarkdown(input string) string {
|
||||
return string(MarkdownCommon([]byte(input)))
|
||||
}
|
||||
|
||||
func doTests(t *testing.T, tests []string) {
|
||||
// catch and report panics
|
||||
var candidate string
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
t.Errorf("\npanic while processing [%#v]: %s\n", candidate, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i+1 < len(tests); i += 2 {
|
||||
input := tests[i]
|
||||
candidate = input
|
||||
expected := tests[i+1]
|
||||
actual := runMarkdown(candidate)
|
||||
if actual != expected {
|
||||
t.Errorf("\nInput [%#v]\nExpected[%#v]\nActual [%#v]",
|
||||
candidate, expected, actual)
|
||||
}
|
||||
|
||||
// now test every substring to stress test bounds checking
|
||||
if !testing.Short() {
|
||||
for start := 0; start < len(input); start++ {
|
||||
for end := start + 1; end <= len(input); end++ {
|
||||
candidate = input[start:end]
|
||||
_ = runMarkdown(candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocument(t *testing.T) {
|
||||
var tests = []string{
|
||||
// Empty document.
|
||||
"",
|
||||
"",
|
||||
|
||||
" ",
|
||||
"",
|
||||
|
||||
// This shouldn't panic.
|
||||
// https://github.com/russross/blackfriday/issues/172
|
||||
"[]:<",
|
||||
"<p>[]:<</p>\n",
|
||||
|
||||
// This shouldn't panic.
|
||||
// https://github.com/russross/blackfriday/issues/173
|
||||
" [",
|
||||
"<p>[</p>\n",
|
||||
}
|
||||
doTests(t, tests)
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ func doTestsReference(t *testing.T, files []string, flag int) {
|
|||
var candidate string
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
t.Errorf("\npanic while processing [%#v]\n", candidate)
|
||||
t.Errorf("\npanic while processing [%#v]: %s\n", candidate, err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func isdigit(c byte) bool {
|
|||
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,
|
||||
// so we treat it like text sometimes
|
||||
|
||||
|
|
@ -96,6 +96,12 @@ func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote
|
|||
*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('&')
|
||||
if *isOpen {
|
||||
out.WriteByte('l')
|
||||
|
|
@ -104,6 +110,11 @@ func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote
|
|||
}
|
||||
out.WriteByte(quote)
|
||||
out.WriteString("quo;")
|
||||
|
||||
if addNBSP && *isOpen {
|
||||
out.WriteString(" ")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +127,7 @@ func smartSingleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byt
|
|||
if len(text) >= 3 {
|
||||
nextChar = text[2]
|
||||
}
|
||||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
|
||||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote, false) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
|
@ -141,7 +152,7 @@ func smartSingleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byt
|
|||
if len(text) > 1 {
|
||||
nextChar = text[1]
|
||||
}
|
||||
if smartQuoteHelper(out, previousChar, nextChar, 's', &smrt.inSingleQuote) {
|
||||
if smartQuoteHelper(out, previousChar, nextChar, 's', &smrt.inSingleQuote, false) {
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -205,13 +216,13 @@ func smartDashLatex(out *bytes.Buffer, smrt *smartypantsData, previousChar byte,
|
|||
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(""")) {
|
||||
nextChar := byte(0)
|
||||
if len(text) >= 7 {
|
||||
nextChar = text[6]
|
||||
}
|
||||
if smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote) {
|
||||
if smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote, addNBSP) {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
|
@ -224,12 +235,15 @@ func smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte
|
|||
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 {
|
||||
|
|
@ -253,7 +267,7 @@ func smartBacktick(out *bytes.Buffer, smrt *smartypantsData, previousChar byte,
|
|||
if len(text) >= 3 {
|
||||
nextChar = text[2]
|
||||
}
|
||||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
|
||||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote, false) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
|
@ -337,7 +351,7 @@ func smartDoubleQuoteVariant(out *bytes.Buffer, smrt *smartypantsData, previousC
|
|||
if len(text) > 1 {
|
||||
nextChar = text[1]
|
||||
}
|
||||
if !smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote) {
|
||||
if !smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote, false) {
|
||||
out.WriteString(""")
|
||||
}
|
||||
|
||||
|
|
@ -367,14 +381,30 @@ type smartCallback func(out *bytes.Buffer, smrt *smartypantsData, previousChar b
|
|||
|
||||
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 {
|
||||
r := new(smartypantsRenderer)
|
||||
addNBSP := flags&HTML_SMARTYPANTS_QUOTES_NBSP != 0
|
||||
if flags&HTML_SMARTYPANTS_ANGLED_QUOTES == 0 {
|
||||
r['"'] = smartDoubleQuote
|
||||
r['&'] = smartAmp
|
||||
if !addNBSP {
|
||||
r['&'] = smartAmpRegular
|
||||
} else {
|
||||
r['&'] = smartAmpRegularNBSP
|
||||
}
|
||||
} else {
|
||||
r['"'] = smartAngledDoubleQuote
|
||||
r['&'] = smartAmpAngledQuote
|
||||
if !addNBSP {
|
||||
r['&'] = smartAmpAngled
|
||||
} else {
|
||||
r['&'] = smartAmpAngledNBSP
|
||||
}
|
||||
}
|
||||
r['\''] = smartSingleQuote
|
||||
r['('] = smartParens
|
||||
|
|
|
|||
|
|
@ -8,4 +8,6 @@
|
|||
|
||||
<p><a href="/url/" title="title has spaces afterward">URL and title</a>.</p>
|
||||
|
||||
<p><a href="/url/">URL with backslashes\</a>.</p>
|
||||
|
||||
<p>[Empty]().</p>
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@ Just a [URL](/url/).
|
|||
|
||||
[URL and title](/url/ "title has spaces afterward" ).
|
||||
|
||||
[URL with backslashes\\](/url/).
|
||||
|
||||
[Empty]().
|
||||
|
|
|
|||
Loading…
Reference in New Issue