Update markdown parsing library to github.com/gomarkdown/markdown (#944)
This commit is contained in:
parent
aba86855b5
commit
0917dc8766
|
@ -14,8 +14,9 @@ import (
|
|||
"golang.org/x/image/webp"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gitlab.com/golang-commonmark/markdown"
|
||||
)
|
||||
|
||||
// DownloadFile downloads the given non-authenticated URL.
|
||||
|
@ -176,9 +177,12 @@ func ClipMessage(text string, length int) string {
|
|||
return text
|
||||
}
|
||||
|
||||
// ParseMarkdown takes in an input string as markdown and parses it to html
|
||||
func ParseMarkdown(input string) string {
|
||||
md := markdown.New(markdown.XHTMLOutput(true), markdown.Breaks(true))
|
||||
res := md.RenderToString([]byte(input))
|
||||
extensions := parser.HardLineBreak
|
||||
markdownParser := parser.NewWithExtensions(extensions)
|
||||
parsedMarkdown := markdown.ToHTML([]byte(input), markdownParser, nil)
|
||||
res := string(parsedMarkdown)
|
||||
res = strings.TrimPrefix(res, "<p>")
|
||||
res = strings.TrimSuffix(res, "</p>\n")
|
||||
return res
|
||||
|
|
9
go.mod
9
go.mod
|
@ -12,6 +12,7 @@ require (
|
|||
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible
|
||||
github.com/gomarkdown/markdown v0.0.0-20190912180731-281270bc6d83
|
||||
github.com/google/gops v0.3.6
|
||||
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect
|
||||
|
@ -46,7 +47,6 @@ require (
|
|||
github.com/russross/blackfriday v1.5.2
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca
|
||||
github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
|
||||
|
@ -55,19 +55,12 @@ require (
|
|||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
|
||||
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2
|
||||
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a // indirect
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 // indirect
|
||||
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f
|
||||
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2 // indirect
|
||||
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe // indirect
|
||||
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 // indirect
|
||||
golang.org/x/image v0.0.0-20190902063713-cb417be4ba39
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/russross/blackfriday.v2 v2.0.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
)
|
||||
|
||||
|
|
19
go.sum
19
go.sum
|
@ -63,6 +63,8 @@ github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk
|
|||
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gomarkdown/markdown v0.0.0-20190912180731-281270bc6d83 h1:w5VNUHB0SP2tr1+boQJWKvnyn3P61UFErZ2e2ih6x0A=
|
||||
github.com/gomarkdown/markdown v0.0.0-20190912180731-281270bc6d83/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/gops v0.3.6 h1:6akvbMlpZrEYOuoebn2kR+ZJekbZqJ28fJXTs84+8to=
|
||||
|
@ -203,8 +205,6 @@ github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7 h1:80VN+vGkqM773Br
|
|||
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
|
@ -247,18 +247,6 @@ github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6Ut
|
|||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2 h1:UQwvu7FjUEdVYofx0U6bsc5odNE7wa5TSA0fl559GcA=
|
||||
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2/go.mod h1:0MsIttMJIF/8Y7x0XjonJP7K99t3sR6bjj4m5S4JmqU=
|
||||
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a h1:Ax7kdHNICZiIeFpmevmaEWb0Ae3BUj3zCTKhZHZ+zd0=
|
||||
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a/go.mod h1:JT4uoTz0tfPoyVH88GZoWDNm5NHJI2VbUW+eyPClueI=
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 h1:oYrL81N608MLZhma3ruL8qTM4xcpYECGut8KSxRY59g=
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8=
|
||||
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f h1:jwXy/CsM4xS2aoiF2fHAlukmInWhd2TlWB+HDCyvzKc=
|
||||
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f/go.mod h1:SIHlEr9462fpIfTrVWf3GqQDxnA65Vm3BMMsUtuA6W0=
|
||||
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2 h1:wD/sPUgx2QJFPTyXZpJnLaROolfeKuruh06U4pRV0WY=
|
||||
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2/go.mod h1:wQk4rLkWrdOPjUAtqJRJ10hIlseLSVYWP95PLrjDF9s=
|
||||
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe h1:5kUPFAF52umOUPH12MuNUmyVTseJRNBftDl/KfsvX3I=
|
||||
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe/go.mod h1:P9LSM1KVzrIstFgUaveuwiAm8PK5VTB3yJEU8kqlbrU=
|
||||
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 h1:uPZaMiz6Sz0PZs3IZJWpU5qHKGNy///1pacZC9txiUI=
|
||||
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638/go.mod h1:EGRJaqe2eO9XGmFtQCvV3Lm9NLico3UhFwUpCG/+mVU=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
|
@ -266,6 +254,7 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
|||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
|
@ -326,8 +315,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
|
|||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/russross/blackfriday.v2 v2.0.0 h1:+FlnIV8DSQnT7NZ43hcVKcdJdzZoeCmJj4Ql8gq5keA=
|
||||
gopkg.in/russross/blackfriday.v2 v2.0.0/go.mod h1:6sSBNz/GtOm/pJTuh5UmBK2ZHfmnxGbl2NZg1UliSOI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
*.out
|
||||
*.swp
|
||||
*.8
|
||||
*.6
|
||||
_obj
|
||||
_test*
|
||||
markdown
|
||||
tags
|
||||
fuzz-workdir/
|
||||
markdown-fuzz.zip
|
||||
coverage.txt
|
||||
testdata/*_got.md
|
||||
testdata/*_ast.txt
|
|
@ -0,0 +1,7 @@
|
|||
checkoutLocation: "src/github.com/gomarkdown/markdown"
|
||||
workspaceLocation: "."
|
||||
tasks:
|
||||
- command: >
|
||||
cd /workspace/src/github.com/gomarkdown/markdown &&
|
||||
go get -v ./... &&
|
||||
go test -c
|
|
@ -0,0 +1,17 @@
|
|||
dist: bionic
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "1.12.x"
|
||||
|
||||
install:
|
||||
- go build -v ./...
|
||||
|
||||
script:
|
||||
- go test -v ./...
|
||||
- go test -run=^$ -bench=BenchmarkReference -benchmem
|
||||
- ./s/test_with_codecoverage.sh
|
||||
- ./s/ci_fuzzit.sh
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
|
@ -0,0 +1,31 @@
|
|||
Markdown is distributed under the Simplified BSD License:
|
||||
|
||||
Copyright © 2011 Russ Ross
|
||||
Copyright © 2018 Krzysztof Kowalczyk
|
||||
Copyright © 2018 Authors
|
||||
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.
|
|
@ -0,0 +1,325 @@
|
|||
# Markdown Parser and HTML Renderer for Go
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/gomarkdown/markdown?status.svg)](https://godoc.org/github.com/gomarkdown/markdown) [![codecov](https://codecov.io/gh/gomarkdown/markdown/branch/master/graph/badge.svg)](https://codecov.io/gh/gomarkdown/markdown)
|
||||
|
||||
Package `github.com/gomarkdown/markdown` is a very fast Go library for parsing [Markdown](https://daringfireball.net/projects/markdown/) documents and rendering them to HTML.
|
||||
|
||||
It's fast and supports common extensions.
|
||||
|
||||
## Installation
|
||||
|
||||
go get -u github.com/gomarkdown/markdown
|
||||
|
||||
API Docs:
|
||||
|
||||
- https://godoc.org/github.com/gomarkdown/markdown : top level package
|
||||
- https://godoc.org/github.com/gomarkdown/markdown/ast : defines abstract syntax tree of parsed markdown document
|
||||
- https://godoc.org/github.com/gomarkdown/markdown/parser : parser
|
||||
- https://godoc.org/github.com/gomarkdown/markdown/html : html renderer
|
||||
|
||||
## Usage
|
||||
|
||||
To convert markdown text to HTML using reasonable defaults:
|
||||
|
||||
```go
|
||||
md := []byte("## markdown document")
|
||||
output := markdown.ToHTML(md, nil, nil)
|
||||
```
|
||||
|
||||
## Customizing markdown parser
|
||||
|
||||
Markdown format is loosely specified and there are multiple extensions invented after original specification was created.
|
||||
|
||||
The parser supports several [extensions](https://godoc.org/github.com/gomarkdown/markdown/parser#Extensions).
|
||||
|
||||
Default parser uses most common `parser.CommonExtensions` but you can easily use parser with custom extension:
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
)
|
||||
|
||||
extensions := parser.CommonExtensions | parser.AutoHeadingIDs
|
||||
parser := parser.NewWithExtensions(extensions)
|
||||
|
||||
md := []byte("markdown text")
|
||||
html := markdown.ToHTML(md, parser, nil)
|
||||
```
|
||||
|
||||
## Customizing HTML renderer
|
||||
|
||||
Similarly, HTML renderer can be configured with different [options](https://godoc.org/github.com/gomarkdown/markdown/html#RendererOptions)
|
||||
|
||||
Here's how to use a custom renderer:
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
)
|
||||
|
||||
htmlFlags := html.CommonFlags | html.HrefTargetBlank
|
||||
opts := html.RendererOptions{Flags: htmlFlags}
|
||||
renderer := html.NewRenderer(opts)
|
||||
|
||||
md := []byte("markdown text")
|
||||
html := markdown.ToHTML(md, nil, renderer)
|
||||
```
|
||||
|
||||
HTML renderer also supports reusing most of the logic and overriding rendering of only specifc nodes.
|
||||
|
||||
You can provide [RenderNodeFunc](https://godoc.org/github.com/gomarkdown/markdown/html#RenderNodeFunc) in [RendererOptions](https://godoc.org/github.com/gomarkdown/markdown/html#RendererOptions).
|
||||
|
||||
The function is called for each node in AST, you can implement custom rendering logic and tell HTML renderer to skip rendering this node.
|
||||
|
||||
Here's the simplest example that drops all code blocks from the output:
|
||||
|
||||
````go
|
||||
import (
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
)
|
||||
|
||||
// return (ast.GoToNext, true) to tell html renderer to skip rendering this node
|
||||
// (because you've rendered it)
|
||||
func renderHookDropCodeBlock(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
|
||||
// skip all nodes that are not CodeBlock nodes
|
||||
if _, ok := node.(*ast.CodeBlock); !ok {
|
||||
return ast.GoToNext, false
|
||||
}
|
||||
// custom rendering logic for ast.CodeBlock. By doing nothing it won't be
|
||||
// present in the output
|
||||
return ast.GoToNext, true
|
||||
}
|
||||
|
||||
opts := html.RendererOptions{
|
||||
Flags: html.CommonFlags,
|
||||
RenderNodeHook: renderHookDropCodeBlock,
|
||||
}
|
||||
renderer := html.NewRenderer(opts)
|
||||
md := "test\n```\nthis code block will be dropped from output\n```\ntext"
|
||||
html := markdown.ToHTML([]byte(s), nil, renderer)
|
||||
````
|
||||
|
||||
## Sanitize untrusted content
|
||||
|
||||
We don't protect against malicious content. When dealing with user-provided
|
||||
markdown, run renderer HTML through HTML sanitizer such as [Bluemonday](https://github.com/microcosm-cc/bluemonday).
|
||||
|
||||
Here's an example of simple usage with Bluemonday:
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/gomarkdown/markdown"
|
||||
)
|
||||
|
||||
// ...
|
||||
maybeUnsafeHTML := markdown.ToHTML(md, nil, nil)
|
||||
html := bluemonday.UGCPolicy().SanitizeBytes(maybeUnsafeHTML)
|
||||
```
|
||||
|
||||
## mdtohtml command-line tool
|
||||
|
||||
https://github.com/gomarkdown/mdtohtml is a command-line markdown to html
|
||||
converter built using this library.
|
||||
|
||||
You can also use it as an example of how to use the library.
|
||||
|
||||
You can install it with:
|
||||
|
||||
go get -u github.com/gomarkdown/mdtohtml
|
||||
|
||||
To run: `mdtohtml input-file [output-file]`
|
||||
|
||||
## Features
|
||||
|
||||
- **Compatibility**. The Markdown v1.0.3 test suite passes with
|
||||
the `--tidy` option. Without `--tidy`, the differences are
|
||||
mostly in whitespace and entity escaping, where this package is
|
||||
more consistent and cleaner.
|
||||
|
||||
- **Common extensions**, including table support, fenced code
|
||||
blocks, autolinks, strikethroughs, non-strict emphasis, etc.
|
||||
|
||||
- **Safety**. Markdown is paranoid when parsing, making it safe
|
||||
to feed untrusted user input without fear of bad things
|
||||
happening. The test suite stress tests this and there are no
|
||||
known inputs that make it crash. If you find one, please let me
|
||||
know and send me the input that does it.
|
||||
|
||||
NOTE: "safety" in this context means _runtime safety only_. In order to
|
||||
protect yourself against JavaScript injection in untrusted content, see
|
||||
[this example](https://github.com/gomarkdown/markdown#sanitize-untrusted-content).
|
||||
|
||||
- **Fast**. It is fast enough to render on-demand in
|
||||
most web applications without having to cache the output.
|
||||
|
||||
- **Thread safety**. You can run multiple parsers in different
|
||||
goroutines without ill effect. There is no dependence on global
|
||||
shared state.
|
||||
|
||||
- **Minimal dependencies**. Only depends on standard library packages in Go.
|
||||
|
||||
- **Standards compliant**. Output successfully validates using the
|
||||
W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional.
|
||||
|
||||
## Extensions
|
||||
|
||||
In addition to the standard markdown syntax, this package
|
||||
implements the following extensions:
|
||||
|
||||
- **Intra-word emphasis supression**. The `_` character is
|
||||
commonly used inside words when discussing code, so having
|
||||
markdown interpret it as an emphasis command is usually the
|
||||
wrong thing. We let you treat all emphasis markers as
|
||||
normal characters when they occur inside a word.
|
||||
|
||||
- **Tables**. Tables can be created by drawing them in the input
|
||||
using a simple syntax:
|
||||
|
||||
```
|
||||
Name | Age
|
||||
--------|------
|
||||
Bob | 27
|
||||
Alice | 23
|
||||
```
|
||||
|
||||
Table footers are supported as well and can be added with equal signs (`=`):
|
||||
|
||||
```
|
||||
Name | Age
|
||||
--------|------
|
||||
Bob | 27
|
||||
Alice | 23
|
||||
========|======
|
||||
Total | 50
|
||||
```
|
||||
|
||||
- **Fenced code blocks**. In addition to the normal 4-space
|
||||
indentation to mark code blocks, you can explicitly mark them
|
||||
and supply a language (to make syntax highlighting simple). Just
|
||||
mark it like this:
|
||||
|
||||
```go
|
||||
func getTrue() bool {
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
- **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
|
||||
|
||||
Terms must be separated from the previous definition by a blank line.
|
||||
|
||||
- **Footnotes**. A marker in the text that will become a superscript number;
|
||||
a footnote definition that will be placed in a list of footnotes at the
|
||||
end of the document. A footnote looks like this:
|
||||
|
||||
This is a footnote.[^1]
|
||||
|
||||
[^1]: the footnote text.
|
||||
|
||||
- **Autolinking**. We can find URLs that have not been
|
||||
explicitly marked as links and turn them into links.
|
||||
|
||||
- **Strikethrough**. Use two tildes (`~~`) to mark text that
|
||||
should be crossed out.
|
||||
|
||||
- **Hard line breaks**. With this extension enabled newlines in the input
|
||||
translate into line breaks in the output. This extension is off by default.
|
||||
|
||||
- **Non blocking space**. With this extension enabled spaces preceeded by an backslash n the input
|
||||
translate non-blocking spaces in the output. This extension is off by default.
|
||||
|
||||
- **Smart quotes**. Smartypants-style punctuation substitution is
|
||||
supported, turning normal double- and single-quote marks into
|
||||
curly quotes, etc.
|
||||
|
||||
- **LaTeX-style dash parsing** is an additional option, where `--`
|
||||
is translated into `–`, and `---` is translated into
|
||||
`—`. This differs from most smartypants processors, which
|
||||
turn a single hyphen into an ndash and a double hyphen into an
|
||||
mdash.
|
||||
|
||||
- **Smart fractions**, where anything that looks like a fraction
|
||||
is translated into suitable HTML (instead of just a few special
|
||||
cases like most smartypant processors). For example, `4/5`
|
||||
becomes `<sup>4</sup>⁄<sub>5</sub>`, which renders as
|
||||
<sup>4</sup>⁄<sub>5</sub>.
|
||||
|
||||
- **MathJaX Support** is an additional feature which is supported by
|
||||
many markdown editor. It translate inline math equation quoted by `$`
|
||||
and display math block quoted by `$$` into MathJax compatible format.
|
||||
hyphen `_` won't break LaTeX render within a math element any more.
|
||||
|
||||
```
|
||||
$$
|
||||
\left[ \begin{array}{a} a^l_1 \\ ⋮ \\ a^l_{d_l} \end{array}\right]
|
||||
= \sigma(
|
||||
\left[ \begin{matrix}
|
||||
w^l_{1,1} & ⋯ & w^l_{1,d_{l-1}} \\
|
||||
⋮ & ⋱ & ⋮ \\
|
||||
w^l_{d_l,1} & ⋯ & w^l_{d_l,d_{l-1}} \\
|
||||
\end{matrix}\right] ·
|
||||
\left[ \begin{array}{x} a^{l-1}_1 \\ ⋮ \\ ⋮ \\ a^{l-1}_{d_{l-1}} \end{array}\right] +
|
||||
\left[ \begin{array}{b} b^l_1 \\ ⋮ \\ b^l_{d_l} \end{array}\right])
|
||||
$$
|
||||
```
|
||||
|
||||
- **Ordered list start number**. With this extension enabled an ordered list will start with the
|
||||
the number that was used to start it.
|
||||
|
||||
- **Super and subscript**. With this extension enabled sequences between ^ will indicate
|
||||
superscript and ~ will become a subscript. For example: H~2~O is a liquid, 2^10^ is 1024.
|
||||
|
||||
- **Block level attributes**, allow setting attributes (ID, classes and key/value pairs) on block
|
||||
level elements. The attribute must be enclosed with braces and be put on a line before the
|
||||
element.
|
||||
|
||||
```
|
||||
{#id3 .myclass fontsize="tiny"}
|
||||
# Header 1
|
||||
```
|
||||
|
||||
Will convert into `<h1 id="id3" class="myclass" fontsize="tiny">Header 1</h1>`.
|
||||
|
||||
- **Mmark support**, see <https://mmark.nl/syntax> for all new syntax elements this adds.
|
||||
|
||||
## Todo
|
||||
|
||||
- port https://github.com/russross/blackfriday/issues/348
|
||||
- port [LaTeX output](https://github.com/Ambrevar/Blackfriday-LaTeX):
|
||||
renders output as LaTeX.
|
||||
- port https://github.com/shurcooL/github_flavored_markdown to markdown
|
||||
- port [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt,
|
||||
but for markdown.
|
||||
- More unit testing
|
||||
- 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.
|
||||
|
||||
## History
|
||||
|
||||
markdown is a fork of v2 of https://github.com/russross/blackfriday that is:
|
||||
|
||||
- actively maintained (sadly in Feb 2018 blackfriday was inactive for 5 months with many bugs and pull requests accumulated)
|
||||
- refactored API (split into ast/parser/html sub-packages)
|
||||
|
||||
Blackfriday itself was based on C implementation [sundown](https://github.com/vmg/sundown) which in turn was based on [libsoldout](http://fossil.instinctive.eu/libsoldout/home).
|
||||
|
||||
## License
|
||||
|
||||
[Simplified BSD License](LICENSE.txt)
|
|
@ -0,0 +1,10 @@
|
|||
package ast
|
||||
|
||||
// An attribute can be attached to block elements. They are specified as
|
||||
// {#id .classs key="value"} where quotes for values are mandatory, multiple
|
||||
// key/value pairs are separated by whitespace.
|
||||
type Attribute struct {
|
||||
ID []byte
|
||||
Classes [][]byte
|
||||
Attrs map[string][]byte
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/*
|
||||
Package ast defines tree representation of a parsed markdown document.
|
||||
*/
|
||||
package ast
|
|
@ -0,0 +1,559 @@
|
|||
package ast
|
||||
|
||||
// ListType contains bitwise or'ed flags for list and list item objects.
|
||||
type ListType int
|
||||
|
||||
// These are the possible flag values for the ListItem renderer.
|
||||
// Multiple flag values may be ORed together.
|
||||
// These are mostly of interest if you are writing a new output format.
|
||||
const (
|
||||
ListTypeOrdered ListType = 1 << iota
|
||||
ListTypeDefinition
|
||||
ListTypeTerm
|
||||
|
||||
ListItemContainsBlock
|
||||
ListItemBeginningOfList // TODO: figure out if this is of any use now
|
||||
ListItemEndOfList
|
||||
)
|
||||
|
||||
// CellAlignFlags holds a type of alignment in a table cell.
|
||||
type CellAlignFlags int
|
||||
|
||||
// These are the possible flag values for the table cell renderer.
|
||||
// Only a single one of these values will be used; they are not ORed together.
|
||||
// These are mostly of interest if you are writing a new output format.
|
||||
const (
|
||||
TableAlignmentLeft CellAlignFlags = 1 << iota
|
||||
TableAlignmentRight
|
||||
TableAlignmentCenter = (TableAlignmentLeft | TableAlignmentRight)
|
||||
)
|
||||
|
||||
func (a CellAlignFlags) String() string {
|
||||
switch a {
|
||||
case TableAlignmentLeft:
|
||||
return "left"
|
||||
case TableAlignmentRight:
|
||||
return "right"
|
||||
case TableAlignmentCenter:
|
||||
return "center"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// DocumentMatters holds the type of a {front,main,back}matter in the document
|
||||
type DocumentMatters int
|
||||
|
||||
// These are all possible Document divisions.
|
||||
const (
|
||||
DocumentMatterNone DocumentMatters = iota
|
||||
DocumentMatterFront
|
||||
DocumentMatterMain
|
||||
DocumentMatterBack
|
||||
)
|
||||
|
||||
// CitationTypes holds the type of a citation, informative, normative or suppressed
|
||||
type CitationTypes int
|
||||
|
||||
const (
|
||||
CitationTypeNone CitationTypes = iota
|
||||
CitationTypeSuppressed
|
||||
CitationTypeInformative
|
||||
CitationTypeNormative
|
||||
)
|
||||
|
||||
// Node defines an ast node
|
||||
type Node interface {
|
||||
AsContainer() *Container
|
||||
AsLeaf() *Leaf
|
||||
GetParent() Node
|
||||
SetParent(newParent Node)
|
||||
GetChildren() []Node
|
||||
SetChildren(newChildren []Node)
|
||||
}
|
||||
|
||||
// Container is a type of node that can contain children
|
||||
type Container struct {
|
||||
Parent Node
|
||||
Children []Node
|
||||
|
||||
Literal []byte // Text contents of the leaf nodes
|
||||
Content []byte // Markdown content of the block nodes
|
||||
|
||||
*Attribute // Block level attribute
|
||||
}
|
||||
|
||||
// AsContainer returns itself as *Container
|
||||
func (c *Container) AsContainer() *Container {
|
||||
return c
|
||||
}
|
||||
|
||||
// AsLeaf returns nil
|
||||
func (c *Container) AsLeaf() *Leaf {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetParent returns parent node
|
||||
func (c *Container) GetParent() Node {
|
||||
return c.Parent
|
||||
}
|
||||
|
||||
// SetParent sets the parent node
|
||||
func (c *Container) SetParent(newParent Node) {
|
||||
c.Parent = newParent
|
||||
}
|
||||
|
||||
// GetChildren returns children nodes
|
||||
func (c *Container) GetChildren() []Node {
|
||||
return c.Children
|
||||
}
|
||||
|
||||
// SetChildren sets children node
|
||||
func (c *Container) SetChildren(newChildren []Node) {
|
||||
c.Children = newChildren
|
||||
}
|
||||
|
||||
// Leaf is a type of node that cannot have children
|
||||
type Leaf struct {
|
||||
Parent Node
|
||||
|
||||
Literal []byte // Text contents of the leaf nodes
|
||||
Content []byte // Markdown content of the block nodes
|
||||
|
||||
*Attribute // Block level attribute
|
||||
}
|
||||
|
||||
// AsContainer returns nil
|
||||
func (l *Leaf) AsContainer() *Container {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AsLeaf returns itself as *Leaf
|
||||
func (l *Leaf) AsLeaf() *Leaf {
|
||||
return l
|
||||
}
|
||||
|
||||
// GetParent returns parent node
|
||||
func (l *Leaf) GetParent() Node {
|
||||
return l.Parent
|
||||
}
|
||||
|
||||
// SetParent sets the parent nodd
|
||||
func (l *Leaf) SetParent(newParent Node) {
|
||||
l.Parent = newParent
|
||||
}
|
||||
|
||||
// GetChildren returns nil because Leaf cannot have children
|
||||
func (l *Leaf) GetChildren() []Node {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetChildren will panic becuase Leaf cannot have children
|
||||
func (l *Leaf) SetChildren(newChildren []Node) {
|
||||
panic("leaf node cannot have children")
|
||||
}
|
||||
|
||||
// Document represents markdown document node, a root of ast
|
||||
type Document struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// DocumentMatter represents markdown node that signals a document
|
||||
// division: frontmatter, mainmatter or backmatter.
|
||||
type DocumentMatter struct {
|
||||
Container
|
||||
|
||||
Matter DocumentMatters
|
||||
}
|
||||
|
||||
// BlockQuote represents markdown block quote node
|
||||
type BlockQuote struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// Aside represents an markdown aside node.
|
||||
type Aside struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// List represents markdown list node
|
||||
type List struct {
|
||||
Container
|
||||
|
||||
ListFlags ListType
|
||||
Tight bool // Skip <p>s around list item data if true
|
||||
BulletChar byte // '*', '+' or '-' in bullet lists
|
||||
Delimiter byte // '.' or ')' after the number in ordered lists
|
||||
Start int // for ordered lists this indicates the starting number if > 0
|
||||
RefLink []byte // If not nil, turns this list item into a footnote item and triggers different rendering
|
||||
IsFootnotesList bool // This is a list of footnotes
|
||||
}
|
||||
|
||||
// ListItem represents markdown list item node
|
||||
type ListItem struct {
|
||||
Container
|
||||
|
||||
ListFlags ListType
|
||||
Tight bool // Skip <p>s around list item data if true
|
||||
BulletChar byte // '*', '+' or '-' in bullet lists
|
||||
Delimiter byte // '.' or ')' after the number in ordered lists
|
||||
RefLink []byte // If not nil, turns this list item into a footnote item and triggers different rendering
|
||||
IsFootnotesList bool // This is a list of footnotes
|
||||
}
|
||||
|
||||
// Paragraph represents markdown paragraph node
|
||||
type Paragraph struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// Math represents markdown MathAjax inline node
|
||||
type Math struct {
|
||||
Leaf
|
||||
}
|
||||
|
||||
// MathBlock represents markdown MathAjax block node
|
||||
type MathBlock struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// Heading represents markdown heading node
|
||||
type Heading struct {
|
||||
Container
|
||||
|
||||
Level int // This holds the heading level number
|
||||
HeadingID string // This might hold heading ID, if present
|
||||
IsTitleblock bool // Specifies whether it's a title block
|
||||
IsSpecial bool // We are a special heading (starts with .#)
|
||||
}
|
||||
|
||||
// HorizontalRule represents markdown horizontal rule node
|
||||
type HorizontalRule struct {
|
||||
Leaf
|
||||
}
|
||||
|
||||
// Emph represents markdown emphasis node
|
||||
type Emph struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// Strong represents markdown strong node
|
||||
type Strong struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// Del represents markdown del node
|
||||
type Del struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// Link represents markdown link node
|
||||
type Link struct {
|
||||
Container
|
||||
|
||||
Destination []byte // Destination is what goes into a href
|
||||
Title []byte // Title is the tooltip thing that goes in a title attribute
|
||||
NoteID int // NoteID contains a serial number of a footnote, zero if it's not a footnote
|
||||
Footnote Node // If it's a footnote, this is a direct link to the footnote Node. Otherwise nil.
|
||||
DeferredID []byte // If a deferred link this holds the original ID.
|
||||
}
|
||||
|
||||
// CrossReference is a reference node.
|
||||
type CrossReference struct {
|
||||
Container
|
||||
|
||||
Destination []byte // Destination is where the reference points to
|
||||
}
|
||||
|
||||
// Citation is a citation node.
|
||||
type Citation struct {
|
||||
Leaf
|
||||
|
||||
Destination [][]byte // Destination is where the citation points to. Multiple ones are allowed.
|
||||
Type []CitationTypes // 1:1 mapping of destination and citation type
|
||||
Suffix [][]byte // Potential citation suffix, i.e. [@!RFC1035, p. 144]
|
||||
}
|
||||
|
||||
// Image represents markdown image node
|
||||
type Image struct {
|
||||
Container
|
||||
|
||||
Destination []byte // Destination is what goes into a href
|
||||
Title []byte // Title is the tooltip thing that goes in a title attribute
|
||||
}
|
||||
|
||||
// Text represents markdown text node
|
||||
type Text struct {
|
||||
Leaf
|
||||
}
|
||||
|
||||
// HTMLBlock represents markdown html node
|
||||
type HTMLBlock struct {
|
||||
Leaf
|
||||
}
|
||||
|
||||
// CodeBlock represents markdown code block node
|
||||
type CodeBlock struct {
|
||||
Leaf
|
||||
|
||||
IsFenced bool // Specifies whether it's a fenced code block or an indented one
|
||||
Info []byte // This holds the info string
|
||||
FenceChar byte
|
||||
FenceLength int
|
||||
FenceOffset int
|
||||
}
|
||||
|
||||
// Softbreak represents markdown softbreak node
|
||||
// Note: not used currently
|
||||
type Softbreak struct {
|
||||
Leaf
|
||||
}
|
||||
|
||||
// Hardbreak represents markdown hard break node
|
||||
type Hardbreak struct {
|
||||
Leaf
|
||||
}
|
||||
|
||||
// NonBlockingSpace represents markdown non-blocking space node
|
||||
type NonBlockingSpace struct {
|
||||
Leaf
|
||||
}
|
||||
|
||||
// Code represents markdown code node
|
||||
type Code struct {
|
||||
Leaf
|
||||
}
|
||||
|
||||
// HTMLSpan represents markdown html span node
|
||||
type HTMLSpan struct {
|
||||
Leaf
|
||||
}
|
||||
|
||||
// Table represents markdown table node
|
||||
type Table struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// TableCell represents markdown table cell node
|
||||
type TableCell struct {
|
||||
Container
|
||||
|
||||
IsHeader bool // This tells if it's under the header row
|
||||
Align CellAlignFlags // This holds the value for align attribute
|
||||
}
|
||||
|
||||
// TableHeader represents markdown table head node
|
||||
type TableHeader struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// TableBody represents markdown table body node
|
||||
type TableBody struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// TableRow represents markdown table row node
|
||||
type TableRow struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// TableFooter represents markdown table foot node
|
||||
type TableFooter struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// Caption represents a figure, code or quote caption
|
||||
type Caption struct {
|
||||
Container
|
||||
}
|
||||
|
||||
// CaptionFigure is a node (blockquote or codeblock) that has a caption
|
||||
type CaptionFigure struct {
|
||||
Container
|
||||
|
||||
HeadingID string // This might hold heading ID, if present
|
||||
}
|
||||
|
||||
// Callout is a node that can exist both in text (where it is an actual node) and in a code block.
|
||||
type Callout struct {
|
||||
Leaf
|
||||
|
||||
ID []byte // number of this callout
|
||||
}
|
||||
|
||||
// Index is a node that contains an Index item and an optional, subitem.
|
||||
type Index struct {
|
||||
Leaf
|
||||
|
||||
Primary bool
|
||||
Item []byte
|
||||
Subitem []byte
|
||||
ID string // ID of the index
|
||||
}
|
||||
|
||||
// Subscript is a subscript node
|
||||
type Subscript struct {
|
||||
Leaf
|
||||
}
|
||||
|
||||
// Subscript is a superscript node
|
||||
type Superscript struct {
|
||||
Leaf
|
||||
}
|
||||
|
||||
// Footnotes is a node that contains all footnotes
|
||||
type Footnotes struct {
|
||||
Container
|
||||
}
|
||||
|
||||
func removeNodeFromArray(a []Node, node Node) []Node {
|
||||
n := len(a)
|
||||
for i := 0; i < n; i++ {
|
||||
if a[i] == node {
|
||||
return append(a[:i], a[i+1:]...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AppendChild appends child to children of parent
|
||||
// It panics if either node is nil.
|
||||
func AppendChild(parent Node, child Node) {
|
||||
RemoveFromTree(child)
|
||||
child.SetParent(parent)
|
||||
newChildren := append(parent.GetChildren(), child)
|
||||
parent.SetChildren(newChildren)
|
||||
}
|
||||
|
||||
// RemoveFromTree removes this node from tree
|
||||
func RemoveFromTree(n Node) {
|
||||
if n.GetParent() == nil {
|
||||
return
|
||||
}
|
||||
// important: don't clear n.Children if n has no parent
|
||||
// we're called from AppendChild and that might happen on a node
|
||||
// that accumulated Children but hasn't been inserted into the tree
|
||||
n.SetChildren(nil)
|
||||
p := n.GetParent()
|
||||
newChildren := removeNodeFromArray(p.GetChildren(), n)
|
||||
if newChildren != nil {
|
||||
p.SetChildren(newChildren)
|
||||
}
|
||||
}
|
||||
|
||||
// GetLastChild returns last child of node n
|
||||
// It's implemented as stand-alone function to keep Node interface small
|
||||
func GetLastChild(n Node) Node {
|
||||
a := n.GetChildren()
|
||||
if len(a) > 0 {
|
||||
return a[len(a)-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFirstChild returns first child of node n
|
||||
// It's implemented as stand-alone function to keep Node interface small
|
||||
func GetFirstChild(n Node) Node {
|
||||
a := n.GetChildren()
|
||||
if len(a) > 0 {
|
||||
return a[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextNode returns next sibling of node n (node after n)
|
||||
// We can't make it part of Container or Leaf because we loose Node identity
|
||||
func GetNextNode(n Node) Node {
|
||||
parent := n.GetParent()
|
||||
if parent == nil {
|
||||
return nil
|
||||
}
|
||||
a := parent.GetChildren()
|
||||
len := len(a) - 1
|
||||
for i := 0; i < len; i++ {
|
||||
if a[i] == n {
|
||||
return a[i+1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPrevNode returns previous sibling of node n (node before n)
|
||||
// We can't make it part of Container or Leaf because we loose Node identity
|
||||
func GetPrevNode(n Node) Node {
|
||||
parent := n.GetParent()
|
||||
if parent == nil {
|
||||
return nil
|
||||
}
|
||||
a := parent.GetChildren()
|
||||
len := len(a)
|
||||
for i := 1; i < len; i++ {
|
||||
if a[i] == n {
|
||||
return a[i-1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WalkStatus allows NodeVisitor to have some control over the tree traversal.
|
||||
// It is returned from NodeVisitor and different values allow Node.Walk to
|
||||
// decide which node to go to next.
|
||||
type WalkStatus int
|
||||
|
||||
const (
|
||||
// GoToNext is the default traversal of every node.
|
||||
GoToNext WalkStatus = iota
|
||||
// SkipChildren tells walker to skip all children of current node.
|
||||
SkipChildren
|
||||
// Terminate tells walker to terminate the traversal.
|
||||
Terminate
|
||||
)
|
||||
|
||||
// NodeVisitor is a callback to be called when traversing the syntax tree.
|
||||
// Called twice for every node: once with entering=true when the branch is
|
||||
// first visited, then with entering=false after all the children are done.
|
||||
type NodeVisitor interface {
|
||||
Visit(node Node, entering bool) WalkStatus
|
||||
}
|
||||
|
||||
// NodeVisitorFunc casts a function to match NodeVisitor interface
|
||||
type NodeVisitorFunc func(node Node, entering bool) WalkStatus
|
||||
|
||||
// Walk traverses tree recursively
|
||||
func Walk(n Node, visitor NodeVisitor) WalkStatus {
|
||||
isContainer := n.AsContainer() != nil
|
||||
status := visitor.Visit(n, true) // entering
|
||||
if status == Terminate {
|
||||
// even if terminating, close container node
|
||||
if isContainer {
|
||||
visitor.Visit(n, false)
|
||||
}
|
||||
return status
|
||||
}
|
||||
if isContainer && status != SkipChildren {
|
||||
children := n.GetChildren()
|
||||
for _, n := range children {
|
||||
status = Walk(n, visitor)
|
||||
if status == Terminate {
|
||||
return status
|
||||
}
|
||||
}
|
||||
}
|
||||
if isContainer {
|
||||
status = visitor.Visit(n, false) // exiting
|
||||
if status == Terminate {
|
||||
return status
|
||||
}
|
||||
}
|
||||
return GoToNext
|
||||
}
|
||||
|
||||
// Visit calls visitor function
|
||||
func (f NodeVisitorFunc) Visit(node Node, entering bool) WalkStatus {
|
||||
return f(node, entering)
|
||||
}
|
||||
|
||||
// WalkFunc is like Walk but accepts just a callback function
|
||||
func WalkFunc(n Node, f NodeVisitorFunc) {
|
||||
visitor := NodeVisitorFunc(f)
|
||||
Walk(n, visitor)
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
package ast
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Print is for debugging. It prints a string representation of parsed
|
||||
// markdown doc (result of parser.Parse()) to dst.
|
||||
//
|
||||
// To make output readable, it shortens text output.
|
||||
func Print(dst io.Writer, doc Node) {
|
||||
PrintWithPrefix(dst, doc, " ")
|
||||
}
|
||||
|
||||
// PrintWithPrefix is like Print but allows customizing prefix used for
|
||||
// indentation. By default it's 2 spaces. You can change it to e.g. tab
|
||||
// by passing "\t"
|
||||
func PrintWithPrefix(w io.Writer, doc Node, prefix string) {
|
||||
// for more compact output, don't print outer Document
|
||||
if _, ok := doc.(*Document); ok {
|
||||
for _, c := range doc.GetChildren() {
|
||||
printRecur(w, c, prefix, 0)
|
||||
}
|
||||
} else {
|
||||
printRecur(w, doc, prefix, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// ToString is like Dump but returns result as a string
|
||||
func ToString(doc Node) string {
|
||||
var buf bytes.Buffer
|
||||
Print(&buf, doc)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func contentToString(d1 []byte, d2 []byte) string {
|
||||
if d1 != nil {
|
||||
return string(d1)
|
||||
}
|
||||
if d2 != nil {
|
||||
return string(d2)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getContent(node Node) string {
|
||||
if c := node.AsContainer(); c != nil {
|
||||
return contentToString(c.Literal, c.Content)
|
||||
}
|
||||
leaf := node.AsLeaf()
|
||||
return contentToString(leaf.Literal, leaf.Content)
|
||||
}
|
||||
|
||||
func shortenString(s string, maxLen int) string {
|
||||
// for cleaner, one-line ouput, replace some white-space chars
|
||||
// with their escaped version
|
||||
s = strings.Replace(s, "\n", `\n`, -1)
|
||||
s = strings.Replace(s, "\r", `\r`, -1)
|
||||
s = strings.Replace(s, "\t", `\t`, -1)
|
||||
if maxLen < 0 {
|
||||
return s
|
||||
}
|
||||
if len(s) < maxLen {
|
||||
return s
|
||||
}
|
||||
// add "..." to indicate truncation
|
||||
return s[:maxLen-3] + "..."
|
||||
}
|
||||
|
||||
// get a short name of the type of v which excludes package name
|
||||
// and strips "()" from the end
|
||||
func getNodeType(node Node) string {
|
||||
s := fmt.Sprintf("%T", node)
|
||||
s = strings.TrimSuffix(s, "()")
|
||||
if idx := strings.Index(s, "."); idx != -1 {
|
||||
return s[idx+1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func printDefault(w io.Writer, indent string, typeName string, content string) {
|
||||
content = strings.TrimSpace(content)
|
||||
if len(content) > 0 {
|
||||
fmt.Fprintf(w, "%s%s '%s'\n", indent, typeName, content)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s%s\n", indent, typeName)
|
||||
}
|
||||
}
|
||||
|
||||
func getListFlags(f ListType) string {
|
||||
var s string
|
||||
if f&ListTypeOrdered != 0 {
|
||||
s += "ordered "
|
||||
}
|
||||
if f&ListTypeDefinition != 0 {
|
||||
s += "definition "
|
||||
}
|
||||
if f&ListTypeTerm != 0 {
|
||||
s += "term "
|
||||
}
|
||||
if f&ListItemContainsBlock != 0 {
|
||||
s += "has_block "
|
||||
}
|
||||
if f&ListItemBeginningOfList != 0 {
|
||||
s += "start "
|
||||
}
|
||||
if f&ListItemEndOfList != 0 {
|
||||
s += "end "
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func printRecur(w io.Writer, node Node, prefix string, depth int) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
indent := strings.Repeat(prefix, depth)
|
||||
|
||||
content := shortenString(getContent(node), 40)
|
||||
typeName := getNodeType(node)
|
||||
switch v := node.(type) {
|
||||
case *Link:
|
||||
content := "url=" + string(v.Destination)
|
||||
printDefault(w, indent, typeName, content)
|
||||
case *Image:
|
||||
content := "url=" + string(v.Destination)
|
||||
printDefault(w, indent, typeName, content)
|
||||
case *List:
|
||||
if v.Start > 1 {
|
||||
content += fmt.Sprintf("start=%d ", v.Start)
|
||||
}
|
||||
if v.Tight {
|
||||
content += "tight "
|
||||
}
|
||||
if v.IsFootnotesList {
|
||||
content += "footnotes "
|
||||
}
|
||||
flags := getListFlags(v.ListFlags)
|
||||
if len(flags) > 0 {
|
||||
content += "flags=" + flags + " "
|
||||
}
|
||||
printDefault(w, indent, typeName, content)
|
||||
case *ListItem:
|
||||
if v.Tight {
|
||||
content += "tight "
|
||||
}
|
||||
if v.IsFootnotesList {
|
||||
content += "footnotes "
|
||||
}
|
||||
flags := getListFlags(v.ListFlags)
|
||||
if len(flags) > 0 {
|
||||
content += "flags=" + flags + " "
|
||||
}
|
||||
printDefault(w, indent, typeName, content)
|
||||
default:
|
||||
printDefault(w, indent, typeName, content)
|
||||
}
|
||||
for _, child := range node.GetChildren() {
|
||||
printRecur(w, child, prefix, depth+1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
## Changes from blackfriday
|
||||
|
||||
This library is derived from blackfriday library. Here's a list of changes.
|
||||
|
||||
**Redesigned API**
|
||||
|
||||
- split into 3 separate packages: ast, parser and html (for html renderer). This makes the API more manageable. It also separates e.g. parser option from renderer options
|
||||
- changed how AST node is represented from union-like representation (manually keeping track of the type of the node) to using interface{} (which is a Go way to combine an arbitrary value with its type)
|
||||
|
||||
**Allow re-using most of html renderer logic**
|
||||
|
||||
You can implement your own renderer by implementing `Renderer` interface.
|
||||
|
||||
Implementing a full renderer is a lot of work and often you just want to tweak html rendering of few node typs.
|
||||
|
||||
I've added a way to hook `Renderer.Render` function in html renderer with a custom function that can take over rendering of specific nodes.
|
||||
|
||||
I use it myself to do syntax-highlighting of code snippets.
|
||||
|
||||
**Speed up go test**
|
||||
|
||||
Running `go test` was really slow (17 secs) because it did a poor man's version of fuzzing by feeding the parser all subsets of test strings in order to find panics
|
||||
due to incorrect parsing logic.
|
||||
|
||||
I've moved that logic to `cmd/crashtest`, so that it can be run on CI but not slow down regular development.
|
||||
|
||||
Now `go test` is blazing fast.
|
|
@ -0,0 +1,8 @@
|
|||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# basic
|
||||
target: 60%
|
||||
threshold: 2%
|
||||
base: auto
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Package markdown implements markdown parser and HTML renderer.
|
||||
|
||||
It parses markdown into AST format which can be serialized to HTML
|
||||
(using html.Renderer) or possibly other formats (using alternate renderers).
|
||||
|
||||
Convert markdown to HTML
|
||||
|
||||
The simplest way to convert markdown document to HTML
|
||||
|
||||
md := []byte("## markdown document")
|
||||
html := markdown.ToHTML(md, nil, nil)
|
||||
|
||||
Customizing parsing and HTML rendering
|
||||
|
||||
You can customize parser and HTML renderer:
|
||||
|
||||
import (
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
"github.com/gomarkdown/markdown/renderer"
|
||||
"github.com/gomarkdown/markdown"
|
||||
)
|
||||
extensions := parser.CommonExtensions | parser.AutoHeadingIDs
|
||||
p := parser.NewWithExensions(extensions)
|
||||
|
||||
htmlFlags := html.CommonFlags | html.HrefTargetBlank
|
||||
opts := html.RendererOptions{Flags: htmlFlags}
|
||||
renderer := html.NewRenderer(opts)
|
||||
|
||||
md := []byte("markdown text")
|
||||
html := markdown.ToHTML(md, p, renderer)
|
||||
|
||||
For a cmd-line tool see https://github.com/gomarkdown/mdtohtml
|
||||
*/
|
||||
package markdown
|
|
@ -0,0 +1,9 @@
|
|||
// +build gofuzz
|
||||
|
||||
package markdown
|
||||
|
||||
// Fuzz is to be used by https://github.com/dvyukov/go-fuzz
|
||||
func Fuzz(data []byte) int {
|
||||
Parse(data, nil)
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module github.com/gomarkdown/markdown
|
||||
|
||||
go 1.12
|
||||
|
||||
require golang.org/dl v0.0.0-20190829154251-82a15e2f2ead // indirect
|
|
@ -0,0 +1,42 @@
|
|||
package html
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
)
|
||||
|
||||
// EscapeHTMLCallouts writes html-escaped d to w. It escapes &, <, > and " characters, *but*
|
||||
// expands callouts <<N>> with the callout HTML, i.e. by calling r.callout() with a newly created
|
||||
// ast.Callout node.
|
||||
func (r *Renderer) EscapeHTMLCallouts(w io.Writer, d []byte) {
|
||||
ld := len(d)
|
||||
Parse:
|
||||
for i := 0; i < ld; i++ {
|
||||
for _, comment := range r.opts.Comments {
|
||||
if !bytes.HasPrefix(d[i:], comment) {
|
||||
break
|
||||
}
|
||||
|
||||
lc := len(comment)
|
||||
if i+lc < ld {
|
||||
if id, consumed := parser.IsCallout(d[i+lc:]); consumed > 0 {
|
||||
// We have seen a callout
|
||||
callout := &ast.Callout{ID: id}
|
||||
r.callout(w, callout)
|
||||
i += consumed + lc - 1
|
||||
continue Parse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
escSeq := Escaper[d[i]]
|
||||
if escSeq != nil {
|
||||
w.Write(escSeq)
|
||||
} else {
|
||||
w.Write([]byte{d[i]})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
Package html implements HTML renderer of parsed markdown document.
|
||||
|
||||
Configuring and customizing a renderer
|
||||
|
||||
A renderer can be configured with multiple options:
|
||||
|
||||
import "github.com/gomarkdown/markdown/html"
|
||||
|
||||
flags := html.CommonFlags | html.CompletePage | html.HrefTargetBlank
|
||||
opts := html.RenderOptions{
|
||||
TItle: "A custom title",
|
||||
Flags: flags,
|
||||
}
|
||||
renderer := html.NewRenderer(opts)
|
||||
|
||||
You can also re-use most of the logic and customize rendering of selected nodes
|
||||
by providing node render hook.
|
||||
This is most useful for rendering nodes that allow for design choices, like
|
||||
links or code blocks.
|
||||
|
||||
import (
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
)
|
||||
|
||||
// a very dummy render hook that will output "code_replacements" instead of
|
||||
// <code>${content}</code> emitted by html.Renderer
|
||||
func renderHookCodeBlock(w io.Writer, node *ast.Node, entering bool) (ast.WalkStatus, bool) {
|
||||
_, ok := node.Data.(*ast.CodeBlockData)
|
||||
if !ok {
|
||||
return ast.GoToNext, false
|
||||
}
|
||||
io.WriteString(w, "code_replacement")
|
||||
return ast.GoToNext, true
|
||||
}
|
||||
|
||||
opts := html.RendererOptions{
|
||||
RenderNodeHook: renderHookCodeBlock,
|
||||
}
|
||||
renderer := html.NewRenderer(opts)
|
||||
*/
|
||||
package html
|
|
@ -0,0 +1,50 @@
|
|||
package html
|
||||
|
||||
import (
|
||||
"html"
|
||||
"io"
|
||||
)
|
||||
|
||||
var Escaper = [256][]byte{
|
||||
'&': []byte("&"),
|
||||
'<': []byte("<"),
|
||||
'>': []byte(">"),
|
||||
'"': []byte("""),
|
||||
}
|
||||
|
||||
// EscapeHTML writes html-escaped d to w. It escapes &, <, > and " characters.
|
||||
func EscapeHTML(w io.Writer, d []byte) {
|
||||
var start, end int
|
||||
n := len(d)
|
||||
for end < n {
|
||||
escSeq := Escaper[d[end]]
|
||||
if escSeq != nil {
|
||||
w.Write(d[start:end])
|
||||
w.Write(escSeq)
|
||||
start = end + 1
|
||||
}
|
||||
end++
|
||||
}
|
||||
if start < n && end <= n {
|
||||
w.Write(d[start:end])
|
||||
}
|
||||
}
|
||||
|
||||
func escLink(w io.Writer, text []byte) {
|
||||
unesc := html.UnescapeString(string(text))
|
||||
EscapeHTML(w, []byte(unesc))
|
||||
}
|
||||
|
||||
// Escape writes the text to w, but skips the escape character.
|
||||
func Escape(w io.Writer, text []byte) {
|
||||
esc := false
|
||||
for i := 0; i < len(text); i++ {
|
||||
if text[i] == '\\' {
|
||||
esc = !esc
|
||||
}
|
||||
if esc && text[i] == '\\' {
|
||||
continue
|
||||
}
|
||||
w.Write([]byte{text[i]})
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,444 @@
|
|||
package html
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
// SmartyPants rendering
|
||||
|
||||
// SPRenderer is a struct containing state of a Smartypants renderer.
|
||||
type SPRenderer struct {
|
||||
inSingleQuote bool
|
||||
inDoubleQuote bool
|
||||
callbacks [256]smartCallback
|
||||
}
|
||||
|
||||
func wordBoundary(c byte) bool {
|
||||
return c == 0 || isSpace(c) || isPunctuation(c)
|
||||
}
|
||||
|
||||
func tolower(c byte) byte {
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
return c - 'A' + 'a'
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func isdigit(c byte) bool {
|
||||
return c >= '0' && c <= '9'
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// enumerate all sixteen possibilities for (previousChar, nextChar)
|
||||
// each can be one of {0, space, punct, other}
|
||||
switch {
|
||||
case previousChar == 0 && nextChar == 0:
|
||||
// context is not any help here, so toggle
|
||||
*isOpen = !*isOpen
|
||||
case isSpace(previousChar) && nextChar == 0:
|
||||
// [ "] might be [ "<code>foo...]
|
||||
*isOpen = true
|
||||
case isPunctuation(previousChar) && nextChar == 0:
|
||||
// [!"] hmm... could be [Run!"] or [("<code>...]
|
||||
*isOpen = false
|
||||
case /* isnormal(previousChar) && */ nextChar == 0:
|
||||
// [a"] is probably a close
|
||||
*isOpen = false
|
||||
case previousChar == 0 && isSpace(nextChar):
|
||||
// [" ] might be [...foo</code>" ]
|
||||
*isOpen = false
|
||||
case isSpace(previousChar) && isSpace(nextChar):
|
||||
// [ " ] context is not any help here, so toggle
|
||||
*isOpen = !*isOpen
|
||||
case isPunctuation(previousChar) && isSpace(nextChar):
|
||||
// [!" ] is probably a close
|
||||
*isOpen = false
|
||||
case /* isnormal(previousChar) && */ isSpace(nextChar):
|
||||
// [a" ] this is one of the easy cases
|
||||
*isOpen = false
|
||||
case previousChar == 0 && isPunctuation(nextChar):
|
||||
// ["!] hmm... could be ["$1.95] or [</code>"!...]
|
||||
*isOpen = false
|
||||
case isSpace(previousChar) && isPunctuation(nextChar):
|
||||
// [ "!] looks more like [ "$1.95]
|
||||
*isOpen = true
|
||||
case isPunctuation(previousChar) && isPunctuation(nextChar):
|
||||
// [!"!] context is not any help here, so toggle
|
||||
*isOpen = !*isOpen
|
||||
case /* isnormal(previousChar) && */ isPunctuation(nextChar):
|
||||
// [a"!] is probably a close
|
||||
*isOpen = false
|
||||
case previousChar == 0 /* && isnormal(nextChar) */ :
|
||||
// ["a] is probably an open
|
||||
*isOpen = true
|
||||
case isSpace(previousChar) /* && isnormal(nextChar) */ :
|
||||
// [ "a] this is one of the easy cases
|
||||
*isOpen = true
|
||||
case isPunctuation(previousChar) /* && isnormal(nextChar) */ :
|
||||
// [!"a] is probably an open
|
||||
*isOpen = true
|
||||
default:
|
||||
// [a'b] maybe a contraction?
|
||||
*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')
|
||||
} else {
|
||||
out.WriteByte('r')
|
||||
}
|
||||
out.WriteByte(quote)
|
||||
out.WriteString("quo;")
|
||||
|
||||
if addNBSP && *isOpen {
|
||||
out.WriteString(" ")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text []byte) int {
|
||||
if len(text) >= 2 {
|
||||
t1 := tolower(text[1])
|
||||
|
||||
if t1 == '\'' {
|
||||
nextChar := byte(0)
|
||||
if len(text) >= 3 {
|
||||
nextChar = text[2]
|
||||
}
|
||||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) {
|
||||
out.WriteString("’")
|
||||
return 0
|
||||
}
|
||||
|
||||
if len(text) >= 3 {
|
||||
t2 := tolower(text[2])
|
||||
|
||||
if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) &&
|
||||
(len(text) < 4 || wordBoundary(text[3])) {
|
||||
out.WriteString("’")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextChar := byte(0)
|
||||
if len(text) > 1 {
|
||||
nextChar = text[1]
|
||||
}
|
||||
if smartQuoteHelper(out, previousChar, nextChar, 's', &r.inSingleQuote, false) {
|
||||
return 0
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartParens(out *bytes.Buffer, previousChar byte, text []byte) int {
|
||||
if len(text) >= 3 {
|
||||
t1 := tolower(text[1])
|
||||
t2 := tolower(text[2])
|
||||
|
||||
if t1 == 'c' && t2 == ')' {
|
||||
out.WriteString("©")
|
||||
return 2
|
||||
}
|
||||
|
||||
if t1 == 'r' && t2 == ')' {
|
||||
out.WriteString("®")
|
||||
return 2
|
||||
}
|
||||
|
||||
if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' {
|
||||
out.WriteString("™")
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartDash(out *bytes.Buffer, previousChar byte, text []byte) int {
|
||||
if len(text) >= 2 {
|
||||
if text[1] == '-' {
|
||||
out.WriteString("—")
|
||||
return 1
|
||||
}
|
||||
|
||||
if wordBoundary(previousChar) && wordBoundary(text[1]) {
|
||||
out.WriteString("–")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartDashLatex(out *bytes.Buffer, previousChar byte, text []byte) int {
|
||||
if len(text) >= 3 && text[1] == '-' && text[2] == '-' {
|
||||
out.WriteString("—")
|
||||
return 2
|
||||
}
|
||||
if len(text) >= 2 && text[1] == '-' {
|
||||
out.WriteString("–")
|
||||
return 1
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, 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, &r.inDoubleQuote, addNBSP) {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(text, []byte("�")) {
|
||||
return 3
|
||||
}
|
||||
|
||||
out.WriteByte('&')
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartAmp(angledQuotes, addNBSP bool) func(*bytes.Buffer, byte, []byte) int {
|
||||
var quote byte = 'd'
|
||||
if angledQuotes {
|
||||
quote = 'a'
|
||||
}
|
||||
|
||||
return func(out *bytes.Buffer, previousChar byte, text []byte) int {
|
||||
return r.smartAmpVariant(out, previousChar, text, quote, addNBSP)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartPeriod(out *bytes.Buffer, previousChar byte, text []byte) int {
|
||||
if len(text) >= 3 && text[1] == '.' && text[2] == '.' {
|
||||
out.WriteString("…")
|
||||
return 2
|
||||
}
|
||||
|
||||
if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' {
|
||||
out.WriteString("…")
|
||||
return 4
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartBacktick(out *bytes.Buffer, previousChar byte, text []byte) int {
|
||||
if len(text) >= 2 && text[1] == '`' {
|
||||
nextChar := byte(0)
|
||||
if len(text) >= 3 {
|
||||
nextChar = text[2]
|
||||
}
|
||||
if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartNumberGeneric(out *bytes.Buffer, previousChar byte, text []byte) int {
|
||||
if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
|
||||
// is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b
|
||||
// note: check for regular slash (/) or fraction slash (⁄, 0x2044, or 0xe2 81 84 in utf-8)
|
||||
// and avoid changing dates like 1/23/2005 into fractions.
|
||||
numEnd := 0
|
||||
for len(text) > numEnd && isdigit(text[numEnd]) {
|
||||
numEnd++
|
||||
}
|
||||
if numEnd == 0 {
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
denStart := numEnd + 1
|
||||
if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 {
|
||||
denStart = numEnd + 3
|
||||
} else if len(text) < numEnd+2 || text[numEnd] != '/' {
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
denEnd := denStart
|
||||
for len(text) > denEnd && isdigit(text[denEnd]) {
|
||||
denEnd++
|
||||
}
|
||||
if denEnd == denStart {
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' {
|
||||
out.WriteString("<sup>")
|
||||
out.Write(text[:numEnd])
|
||||
out.WriteString("</sup>⁄<sub>")
|
||||
out.Write(text[denStart:denEnd])
|
||||
out.WriteString("</sub>")
|
||||
return denEnd - 1
|
||||
}
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartNumber(out *bytes.Buffer, previousChar byte, text []byte) int {
|
||||
if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
|
||||
if text[0] == '1' && text[1] == '/' && text[2] == '2' {
|
||||
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' {
|
||||
out.WriteString("½")
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
if text[0] == '1' && text[1] == '/' && text[2] == '4' {
|
||||
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') {
|
||||
out.WriteString("¼")
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
if text[0] == '3' && text[1] == '/' && text[2] == '4' {
|
||||
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') {
|
||||
out.WriteString("¾")
|
||||
return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.WriteByte(text[0])
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartDoubleQuoteVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte) int {
|
||||
nextChar := byte(0)
|
||||
if len(text) > 1 {
|
||||
nextChar = text[1]
|
||||
}
|
||||
if !smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, false) {
|
||||
out.WriteString(""")
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int {
|
||||
return r.smartDoubleQuoteVariant(out, previousChar, text, 'd')
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartAngledDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int {
|
||||
return r.smartDoubleQuoteVariant(out, previousChar, text, 'a')
|
||||
}
|
||||
|
||||
func (r *SPRenderer) smartLeftAngle(out *bytes.Buffer, previousChar byte, text []byte) int {
|
||||
i := 0
|
||||
|
||||
for i < len(text) && text[i] != '>' {
|
||||
i++
|
||||
}
|
||||
|
||||
out.Write(text[:i+1])
|
||||
return i
|
||||
}
|
||||
|
||||
type smartCallback func(out *bytes.Buffer, previousChar byte, text []byte) int
|
||||
|
||||
// NewSmartypantsRenderer constructs a Smartypants renderer object.
|
||||
func NewSmartypantsRenderer(flags Flags) *SPRenderer {
|
||||
var (
|
||||
r SPRenderer
|
||||
|
||||
smartAmpAngled = r.smartAmp(true, false)
|
||||
smartAmpAngledNBSP = r.smartAmp(true, true)
|
||||
smartAmpRegular = r.smartAmp(false, false)
|
||||
smartAmpRegularNBSP = r.smartAmp(false, true)
|
||||
|
||||
addNBSP = flags&SmartypantsQuotesNBSP != 0
|
||||
)
|
||||
|
||||
if flags&SmartypantsAngledQuotes == 0 {
|
||||
r.callbacks['"'] = r.smartDoubleQuote
|
||||
if !addNBSP {
|
||||
r.callbacks['&'] = smartAmpRegular
|
||||
} else {
|
||||
r.callbacks['&'] = smartAmpRegularNBSP
|
||||
}
|
||||
} else {
|
||||
r.callbacks['"'] = r.smartAngledDoubleQuote
|
||||
if !addNBSP {
|
||||
r.callbacks['&'] = smartAmpAngled
|
||||
} else {
|
||||
r.callbacks['&'] = smartAmpAngledNBSP
|
||||
}
|
||||
}
|
||||
r.callbacks['\''] = r.smartSingleQuote
|
||||
r.callbacks['('] = r.smartParens
|
||||
if flags&SmartypantsDashes != 0 {
|
||||
if flags&SmartypantsLatexDashes == 0 {
|
||||
r.callbacks['-'] = r.smartDash
|
||||
} else {
|
||||
r.callbacks['-'] = r.smartDashLatex
|
||||
}
|
||||
}
|
||||
r.callbacks['.'] = r.smartPeriod
|
||||
if flags&SmartypantsFractions == 0 {
|
||||
r.callbacks['1'] = r.smartNumber
|
||||
r.callbacks['3'] = r.smartNumber
|
||||
} else {
|
||||
for ch := '1'; ch <= '9'; ch++ {
|
||||
r.callbacks[ch] = r.smartNumberGeneric
|
||||
}
|
||||
}
|
||||
r.callbacks['<'] = r.smartLeftAngle
|
||||
r.callbacks['`'] = r.smartBacktick
|
||||
return &r
|
||||
}
|
||||
|
||||
// Process is the entry point of the Smartypants renderer.
|
||||
func (r *SPRenderer) Process(w io.Writer, text []byte) {
|
||||
mark := 0
|
||||
for i := 0; i < len(text); i++ {
|
||||
if action := r.callbacks[text[i]]; action != nil {
|
||||
if i > mark {
|
||||
w.Write(text[mark:i])
|
||||
}
|
||||
previousChar := byte(0)
|
||||
if i > 0 {
|
||||
previousChar = text[i-1]
|
||||
}
|
||||
var tmp bytes.Buffer
|
||||
i += action(&tmp, previousChar, text[i:])
|
||||
w.Write(tmp.Bytes())
|
||||
mark = i + 1
|
||||
}
|
||||
}
|
||||
if mark < len(text) {
|
||||
w.Write(text[mark:])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
)
|
||||
|
||||
// Renderer is an interface for implementing custom renderers.
|
||||
type Renderer interface {
|
||||
// RenderNode renders markdown node to w.
|
||||
// It's called once for a leaf node.
|
||||
// It's called twice for non-leaf nodes:
|
||||
// * first with entering=true
|
||||
// * then with entering=false
|
||||
//
|
||||
// Return value is a way to tell the calling walker to adjust its walk
|
||||
// pattern: e.g. it can terminate the traversal by returning Terminate. Or it
|
||||
// can ask the walker to skip a subtree of this node by returning SkipChildren.
|
||||
// The typical behavior is to return GoToNext, which asks for the usual
|
||||
// traversal to the next node.
|
||||
RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus
|
||||
|
||||
// RenderHeader is a method that allows the renderer to produce some
|
||||
// content preceding the main body of the output document. The header is
|
||||
// understood in the broad sense here. For example, the default HTML
|
||||
// renderer will write not only the HTML document preamble, but also the
|
||||
// table of contents if it was requested.
|
||||
//
|
||||
// The method will be passed an entire document tree, in case a particular
|
||||
// implementation needs to inspect it to produce output.
|
||||
//
|
||||
// The output should be written to the supplied writer w. If your
|
||||
// implementation has no header to write, supply an empty implementation.
|
||||
RenderHeader(w io.Writer, ast ast.Node)
|
||||
|
||||
// RenderFooter is a symmetric counterpart of RenderHeader.
|
||||
RenderFooter(w io.Writer, ast ast.Node)
|
||||
}
|
||||
|
||||
// Parse parsers a markdown document using provided parser. If parser is nil,
|
||||
// we use parser configured with parser.CommonExtensions.
|
||||
//
|
||||
// It returns AST (abstract syntax tree) that can be converted to another
|
||||
// format using Render function.
|
||||
func Parse(markdown []byte, p *parser.Parser) ast.Node {
|
||||
if p == nil {
|
||||
p = parser.New()
|
||||
}
|
||||
return p.Parse(markdown)
|
||||
}
|
||||
|
||||
// Render uses renderer to convert parsed markdown document into a different format.
|
||||
//
|
||||
// To convert to HTML, pass html.Renderer
|
||||
func Render(doc ast.Node, renderer Renderer) []byte {
|
||||
var buf bytes.Buffer
|
||||
renderer.RenderHeader(&buf, doc)
|
||||
ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
|
||||
return renderer.RenderNode(&buf, node, entering)
|
||||
})
|
||||
renderer.RenderFooter(&buf, doc)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// ToHTML converts markdownDoc to HTML.
|
||||
//
|
||||
// You can optionally pass a parser and renderer. This allows to customize
|
||||
// a parser, use a customized html render or use completely custom renderer.
|
||||
//
|
||||
// If you pass nil for both, we use parser configured with parser.CommonExtensions
|
||||
// and html.Renderer configured with html.CommonFlags.
|
||||
func ToHTML(markdown []byte, p *parser.Parser, renderer Renderer) []byte {
|
||||
doc := Parse(markdown, p)
|
||||
if renderer == nil {
|
||||
opts := html.RendererOptions{
|
||||
Flags: html.CommonFlags,
|
||||
}
|
||||
renderer = html.NewRenderer(opts)
|
||||
}
|
||||
return Render(doc, renderer)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
)
|
||||
|
||||
// returns aisde prefix length
|
||||
func (p *Parser) asidePrefix(data []byte) int {
|
||||
i := 0
|
||||
n := len(data)
|
||||
for i < 3 && i < n && data[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
if i+1 < n && data[i] == 'A' && data[i+1] == '>' {
|
||||
if i+2 < n && data[i+2] == ' ' {
|
||||
return i + 3
|
||||
}
|
||||
return i + 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// aside ends with at least one blank line
|
||||
// followed by something without a aside prefix
|
||||
func (p *Parser) terminateAside(data []byte, beg, end int) bool {
|
||||
if p.isEmpty(data[beg:]) <= 0 {
|
||||
return false
|
||||
}
|
||||
if end >= len(data) {
|
||||
return true
|
||||
}
|
||||
return p.asidePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0
|
||||
}
|
||||
|
||||
// parse a aside fragment
|
||||
func (p *Parser) aside(data []byte) int {
|
||||
var raw bytes.Buffer
|
||||
beg, end := 0, 0
|
||||
// identical to quote
|
||||
for beg < len(data) {
|
||||
end = beg
|
||||
// Step over whole lines, collecting them. While doing that, check for
|
||||
// fenced code and if one's found, incorporate it altogether,
|
||||
// irregardless of any contents inside it
|
||||
for end < len(data) && data[end] != '\n' {
|
||||
if p.extensions&FencedCode != 0 {
|
||||
if i := p.fencedCodeBlock(data[end:], false); i > 0 {
|
||||
// -1 to compensate for the extra end++ after the loop:
|
||||
end += i - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
end++
|
||||
}
|
||||
end = skipCharN(data, end, '\n', 1)
|
||||
if pre := p.asidePrefix(data[beg:]); pre > 0 {
|
||||
// skip the prefix
|
||||
beg += pre
|
||||
} else if p.terminateAside(data, beg, end) {
|
||||
break
|
||||
}
|
||||
// this line is part of the aside
|
||||
raw.Write(data[beg:end])
|
||||
beg = end
|
||||
}
|
||||
|
||||
block := p.addBlock(&ast.Aside{})
|
||||
p.block(raw.Bytes())
|
||||
p.finalize(block)
|
||||
return end
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
)
|
||||
|
||||
// attribute parses a (potential) block attribute and adds it to p.
|
||||
func (p *Parser) attribute(data []byte) []byte {
|
||||
if len(data) < 3 {
|
||||
return data
|
||||
}
|
||||
i := 0
|
||||
if data[i] != '{' {
|
||||
return data
|
||||
}
|
||||
i++
|
||||
|
||||
// last character must be a } otherwise it's not an attribute
|
||||
end := skipUntilChar(data, i, '\n')
|
||||
if data[end-1] != '}' {
|
||||
return data
|
||||
}
|
||||
|
||||
i = skipSpace(data, i)
|
||||
b := &ast.Attribute{Attrs: make(map[string][]byte)}
|
||||
|
||||
esc := false
|
||||
quote := false
|
||||
trail := 0
|
||||
Loop:
|
||||
for ; i < len(data); i++ {
|
||||
switch data[i] {
|
||||
case ' ', '\t', '\f', '\v':
|
||||
if quote {
|
||||
continue
|
||||
}
|
||||
chunk := data[trail+1 : i]
|
||||
if len(chunk) == 0 {
|
||||
trail = i
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case chunk[0] == '.':
|
||||
b.Classes = append(b.Classes, chunk[1:])
|
||||
case chunk[0] == '#':
|
||||
b.ID = chunk[1:]
|
||||
default:
|
||||
k, v := keyValue(chunk)
|
||||
if k != nil && v != nil {
|
||||
b.Attrs[string(k)] = v
|
||||
} else {
|
||||
// this is illegal in an attribute
|
||||
return data
|
||||
}
|
||||
}
|
||||
trail = i
|
||||
case '"':
|
||||
if esc {
|
||||
esc = !esc
|
||||
continue
|
||||
}
|
||||
quote = !quote
|
||||
case '\\':
|
||||
esc = !esc
|
||||
case '}':
|
||||
if esc {
|
||||
esc = !esc
|
||||
continue
|
||||
}
|
||||
chunk := data[trail+1 : i]
|
||||
if len(chunk) == 0 {
|
||||
return data
|
||||
}
|
||||
switch {
|
||||
case chunk[0] == '.':
|
||||
b.Classes = append(b.Classes, chunk[1:])
|
||||
case chunk[0] == '#':
|
||||
b.ID = chunk[1:]
|
||||
default:
|
||||
k, v := keyValue(chunk)
|
||||
if k != nil && v != nil {
|
||||
b.Attrs[string(k)] = v
|
||||
} else {
|
||||
return data
|
||||
}
|
||||
}
|
||||
i++
|
||||
break Loop
|
||||
default:
|
||||
esc = false
|
||||
}
|
||||
}
|
||||
|
||||
p.attr = b
|
||||
return data[i:]
|
||||
}
|
||||
|
||||
// key="value" quotes are mandatory.
|
||||
func keyValue(data []byte) ([]byte, []byte) {
|
||||
chunk := bytes.SplitN(data, []byte{'='}, 2)
|
||||
if len(chunk) != 2 {
|
||||
return nil, nil
|
||||
}
|
||||
key := chunk[0]
|
||||
value := chunk[1]
|
||||
|
||||
if len(value) < 3 || len(key) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if value[0] != '"' || value[len(value)-1] != '"' {
|
||||
return key, nil
|
||||
}
|
||||
return key, value[1 : len(value)-1]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,29 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// IsCallout detects a callout in the following format: <<N>> Where N is a integer > 0.
|
||||
func IsCallout(data []byte) (id []byte, consumed int) {
|
||||
if !bytes.HasPrefix(data, []byte("<<")) {
|
||||
return nil, 0
|
||||
}
|
||||
start := 2
|
||||
end := bytes.Index(data[start:], []byte(">>"))
|
||||
if end < 0 {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
b := data[start : start+end]
|
||||
b = bytes.TrimSpace(b)
|
||||
i, err := strconv.Atoi(string(b))
|
||||
if err != nil {
|
||||
return nil, 0
|
||||
}
|
||||
if i <= 0 {
|
||||
return nil, 0
|
||||
}
|
||||
return b, start + end + 2 // 2 for >>
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// caption checks for a caption, it returns the caption data and a potential "headingID".
|
||||
func (p *Parser) caption(data, caption []byte) ([]byte, string, int) {
|
||||
if !bytes.HasPrefix(data, caption) {
|
||||
return nil, "", 0
|
||||
}
|
||||
j := len(caption)
|
||||
data = data[j:]
|
||||
end := p.linesUntilEmpty(data)
|
||||
|
||||
data = data[:end]
|
||||
|
||||
id, start := captionID(data)
|
||||
if id != "" {
|
||||
return data[:start], id, end + j
|
||||
}
|
||||
|
||||
return data, "", end + j
|
||||
}
|
||||
|
||||
// linesUntilEmpty scans lines up to the first empty line.
|
||||
func (p *Parser) linesUntilEmpty(data []byte) int {
|
||||
line, i := 0, 0
|
||||
|
||||
for line < len(data) {
|
||||
i++
|
||||
|
||||
// find the end of this line
|
||||
for i < len(data) && data[i-1] != '\n' {
|
||||
i++
|
||||
}
|
||||
|
||||
if p.isEmpty(data[line:i]) == 0 {
|
||||
line = i
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// captionID checks if the caption *ends* in {#....}. If so the text after {# is taken to be
|
||||
// the ID/anchor of the entire figure block.
|
||||
func captionID(data []byte) (string, int) {
|
||||
end := len(data)
|
||||
|
||||
j, k := 0, 0
|
||||
// find start/end of heading id
|
||||
for j = 0; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ {
|
||||
}
|
||||
for k = j + 1; k < end && data[k] != '}'; k++ {
|
||||
}
|
||||
// remains must be whitespace.
|
||||
for l := k + 1; l < end; l++ {
|
||||
if !isSpace(data[l]) {
|
||||
return "", 0
|
||||
}
|
||||
}
|
||||
|
||||
if j > 0 && k > 0 && j+2 < k {
|
||||
return string(data[j+2 : k]), j
|
||||
}
|
||||
return "", 0
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
)
|
||||
|
||||
// citation parses a citation. In its most simple form [@ref], we allow multiple
|
||||
// being separated by semicolons and a sub reference inside ala pandoc: [@ref, p. 23].
|
||||
// Each citation can have a modifier: !, ? or - wich mean:
|
||||
//
|
||||
// ! - normative
|
||||
// ? - formative
|
||||
// - - suppressed
|
||||
//
|
||||
// The suffix starts after a comma, we strip any whitespace before and after. If the output
|
||||
// allows for it, this can be rendered.
|
||||
func citation(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
// look for the matching closing bracket
|
||||
i := offset + 1
|
||||
for level := 1; level > 0 && i < len(data); i++ {
|
||||
switch {
|
||||
case data[i] == '\n':
|
||||
// no newlines allowed.
|
||||
return 0, nil
|
||||
|
||||
case data[i-1] == '\\':
|
||||
continue
|
||||
|
||||
case data[i] == '[':
|
||||
level++
|
||||
|
||||
case data[i] == ']':
|
||||
level--
|
||||
if level <= 0 {
|
||||
i-- // compensate for extra i++ in for loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if i >= len(data) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
node := &ast.Citation{}
|
||||
|
||||
citations := bytes.Split(data[1:i], []byte(";"))
|
||||
for _, citation := range citations {
|
||||
var suffix []byte
|
||||
citation = bytes.TrimSpace(citation)
|
||||
j := 0
|
||||
if citation[j] != '@' {
|
||||
// not a citation, drop out entirely.
|
||||
return 0, nil
|
||||
}
|
||||
if c := bytes.Index(citation, []byte(",")); c > 0 {
|
||||
part := citation[:c]
|
||||
suff := citation[c+1:]
|
||||
part = bytes.TrimSpace(part)
|
||||
suff = bytes.TrimSpace(suff)
|
||||
|
||||
citation = part
|
||||
suffix = suff
|
||||
}
|
||||
|
||||
citeType := ast.CitationTypeInformative
|
||||
j = 1
|
||||
switch citation[j] {
|
||||
case '!':
|
||||
citeType = ast.CitationTypeNormative
|
||||
j++
|
||||
case '?':
|
||||
citeType = ast.CitationTypeInformative
|
||||
j++
|
||||
case '-':
|
||||
citeType = ast.CitationTypeSuppressed
|
||||
j++
|
||||
}
|
||||
node.Destination = append(node.Destination, citation[j:])
|
||||
node.Type = append(node.Type, citeType)
|
||||
node.Suffix = append(node.Suffix, suffix)
|
||||
}
|
||||
|
||||
return i + 1, node
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package parser
|
||||
|
||||
// isEscape returns true if byte i is prefixed by an odd number of backslahses.
|
||||
func isEscape(data []byte, i int) bool {
|
||||
if i == 0 {
|
||||
return false
|
||||
}
|
||||
if i == 1 {
|
||||
return data[0] == '\\'
|
||||
}
|
||||
j := i - 1
|
||||
for ; j >= 0; j-- {
|
||||
if data[j] != '\\' {
|
||||
break
|
||||
}
|
||||
}
|
||||
j++
|
||||
// odd number of backslahes means escape
|
||||
return (i-j)%2 != 0
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
)
|
||||
|
||||
// sFigureLine checks if there's a figure line (e.g., !--- ) at the beginning of data,
|
||||
// and returns the end index if so, or 0 otherwise.
|
||||
func sFigureLine(data []byte, oldmarker string) (end int, marker string) {
|
||||
i, size := 0, 0
|
||||
|
||||
n := len(data)
|
||||
// skip up to three spaces
|
||||
for i < n && i < 3 && data[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
|
||||
// check for the marker characters: !
|
||||
if i+1 >= n {
|
||||
return 0, ""
|
||||
}
|
||||
if data[i] != '!' || data[i+1] != '-' {
|
||||
return 0, ""
|
||||
}
|
||||
i++
|
||||
|
||||
c := data[i] // i.e. the -
|
||||
|
||||
// the whole line must be the same char or whitespace
|
||||
for i < n && data[i] == c {
|
||||
size++
|
||||
i++
|
||||
}
|
||||
|
||||
// the marker char must occur at least 3 times
|
||||
if size < 3 {
|
||||
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 0, ""
|
||||
}
|
||||
|
||||
// there is no syntax modifier although it might be an idea to re-use this space for something?
|
||||
|
||||
i = skipChar(data, i, ' ')
|
||||
if i >= n || data[i] != '\n' {
|
||||
if i == n {
|
||||
return i, marker
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
return i + 1, marker // Take newline into account.
|
||||
}
|
||||
|
||||
// figureBlock returns the end index if data contains a figure 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 figure block.
|
||||
func (p *Parser) figureBlock(data []byte, doRender bool) int {
|
||||
beg, marker := sFigureLine(data, "")
|
||||
if beg == 0 || beg >= len(data) {
|
||||
return 0
|
||||
}
|
||||
|
||||
var raw bytes.Buffer
|
||||
|
||||
for {
|
||||
// safe to assume beg < len(data)
|
||||
|
||||
// check for the end of the code block
|
||||
figEnd, _ := sFigureLine(data[beg:], marker)
|
||||
if figEnd != 0 {
|
||||
beg += figEnd
|
||||
break
|
||||
}
|
||||
|
||||
// copy the current line
|
||||
end := skipUntilChar(data, beg, '\n') + 1
|
||||
|
||||
// did we reach the end of the buffer without a closing marker?
|
||||
if end >= len(data) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// verbatim copy to the working buffer
|
||||
if doRender {
|
||||
raw.Write(data[beg:end])
|
||||
}
|
||||
beg = end
|
||||
}
|
||||
|
||||
if !doRender {
|
||||
return beg
|
||||
}
|
||||
|
||||
figure := &ast.CaptionFigure{}
|
||||
p.addBlock(figure)
|
||||
p.block(raw.Bytes())
|
||||
|
||||
defer p.finalize(figure)
|
||||
|
||||
if captionContent, id, consumed := p.caption(data[beg:], []byte("Figure: ")); consumed > 0 {
|
||||
caption := &ast.Caption{}
|
||||
p.Inline(caption, captionContent)
|
||||
|
||||
figure.HeadingID = id
|
||||
|
||||
p.addChild(caption)
|
||||
|
||||
beg += consumed
|
||||
}
|
||||
|
||||
p.finalize(figure)
|
||||
return beg
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// isInclude parses {{...}}[...], that contains a path between the {{, the [...] syntax contains
|
||||
// an address to select which lines to include. It is treated as an opaque string and just given
|
||||
// to readInclude.
|
||||
func (p *Parser) isInclude(data []byte) (filename string, address []byte, consumed int) {
|
||||
i := skipCharN(data, 0, ' ', 3) // start with up to 3 spaces
|
||||
if len(data[i:]) < 3 {
|
||||
return "", nil, 0
|
||||
}
|
||||
if data[i] != '{' || data[i+1] != '{' {
|
||||
return "", nil, 0
|
||||
}
|
||||
start := i + 2
|
||||
|
||||
// find the end delimiter
|
||||
i = skipUntilChar(data, i, '}')
|
||||
if i+1 >= len(data) {
|
||||
return "", nil, 0
|
||||
}
|
||||
end := i
|
||||
i++
|
||||
if data[i] != '}' {
|
||||
return "", nil, 0
|
||||
}
|
||||
filename = string(data[start:end])
|
||||
|
||||
if i+1 < len(data) && data[i+1] == '[' { // potential address specification
|
||||
start := i + 2
|
||||
|
||||
end = skipUntilChar(data, start, ']')
|
||||
if end >= len(data) {
|
||||
return "", nil, 0
|
||||
}
|
||||
address = data[start:end]
|
||||
return filename, address, end + 1
|
||||
}
|
||||
|
||||
return filename, address, i + 1
|
||||
}
|
||||
|
||||
func (p *Parser) readInclude(from, file string, address []byte) []byte {
|
||||
if p.Opts.ReadIncludeFn != nil {
|
||||
return p.Opts.ReadIncludeFn(from, file, address)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isCodeInclude parses <{{...}} which is similar to isInclude the returned bytes are, however wrapped in a code block.
|
||||
func (p *Parser) isCodeInclude(data []byte) (filename string, address []byte, consumed int) {
|
||||
i := skipCharN(data, 0, ' ', 3) // start with up to 3 spaces
|
||||
if len(data[i:]) < 3 {
|
||||
return "", nil, 0
|
||||
}
|
||||
if data[i] != '<' {
|
||||
return "", nil, 0
|
||||
}
|
||||
start := i
|
||||
|
||||
filename, address, consumed = p.isInclude(data[i+1:])
|
||||
if consumed == 0 {
|
||||
return "", nil, 0
|
||||
}
|
||||
return filename, address, start + consumed + 1
|
||||
}
|
||||
|
||||
// readCodeInclude acts like include except the returned bytes are wrapped in a fenced code block.
|
||||
func (p *Parser) readCodeInclude(from, file string, address []byte) []byte {
|
||||
data := p.readInclude(from, file, address)
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
ext := path.Ext(file)
|
||||
buf := &bytes.Buffer{}
|
||||
buf.Write([]byte("```"))
|
||||
if ext != "" { // starts with a dot
|
||||
buf.WriteString(" " + ext[1:] + "\n")
|
||||
} else {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
buf.Write(data)
|
||||
buf.WriteString("```\n")
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// incStack hold the current stack of chained includes. Each value is the containing
|
||||
// path of the file being parsed.
|
||||
type incStack struct {
|
||||
stack []string
|
||||
}
|
||||
|
||||
func newIncStack() *incStack {
|
||||
return &incStack{stack: []string{}}
|
||||
}
|
||||
|
||||
// Push updates i with new.
|
||||
func (i *incStack) Push(new string) {
|
||||
if path.IsAbs(new) {
|
||||
i.stack = append(i.stack, path.Dir(new))
|
||||
return
|
||||
}
|
||||
last := ""
|
||||
if len(i.stack) > 0 {
|
||||
last = i.stack[len(i.stack)-1]
|
||||
}
|
||||
i.stack = append(i.stack, path.Dir(filepath.Join(last, new)))
|
||||
}
|
||||
|
||||
// Pop pops the last value.
|
||||
func (i *incStack) Pop() {
|
||||
if len(i.stack) == 0 {
|
||||
return
|
||||
}
|
||||
i.stack = i.stack[:len(i.stack)-1]
|
||||
}
|
||||
|
||||
func (i *incStack) Last() string {
|
||||
if len(i.stack) == 0 {
|
||||
return ""
|
||||
}
|
||||
return i.stack[len(i.stack)-1]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,36 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
)
|
||||
|
||||
func (p *Parser) documentMatter(data []byte) int {
|
||||
if data[0] != '{' {
|
||||
return 0
|
||||
}
|
||||
|
||||
consumed := 0
|
||||
matter := ast.DocumentMatterNone
|
||||
if bytes.HasPrefix(data, []byte("{frontmatter}")) {
|
||||
consumed = len("{frontmatter}")
|
||||
matter = ast.DocumentMatterFront
|
||||
}
|
||||
if bytes.HasPrefix(data, []byte("{mainmatter}")) {
|
||||
consumed = len("{mainmatter}")
|
||||
matter = ast.DocumentMatterMain
|
||||
}
|
||||
if bytes.HasPrefix(data, []byte("{backmatter}")) {
|
||||
consumed = len("{backmatter}")
|
||||
matter = ast.DocumentMatterBack
|
||||
}
|
||||
if consumed == 0 {
|
||||
return 0
|
||||
}
|
||||
node := &ast.DocumentMatter{Matter: matter}
|
||||
p.addBlock(node)
|
||||
p.finalize(node)
|
||||
|
||||
return consumed
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
)
|
||||
|
||||
// Flags control optional behavior of parser.
|
||||
type Flags int
|
||||
|
||||
// Options is a collection of supplementary parameters tweaking the behavior of various parts of the parser.
|
||||
type Options struct {
|
||||
ParserHook BlockFunc
|
||||
ReadIncludeFn ReadIncludeFunc
|
||||
|
||||
Flags Flags // Flags allow customizing parser's behavior
|
||||
}
|
||||
|
||||
// Parser renderer configuration options.
|
||||
const (
|
||||
FlagsNone Flags = 0
|
||||
SkipFootnoteList Flags = 1 << iota // Skip adding the footnote list (regardless if they are parsed)
|
||||
)
|
||||
|
||||
// BlockFunc allows to registration of a parser function. If successful it
|
||||
// returns an ast.Node, a buffer that should be parsed as a block and the the number of bytes consumed.
|
||||
type BlockFunc func(data []byte) (ast.Node, []byte, int)
|
||||
|
||||
// ReadIncludeFunc should read the file under path and returns the read bytes,
|
||||
// from will be set to the name of the current file being parsed. Initially
|
||||
// this will be empty. address is the optional address specifier of which lines
|
||||
// of the file to return. If this function is not set no data will be read.
|
||||
type ReadIncludeFunc func(from, path string, address []byte) []byte
|
|
@ -0,0 +1,812 @@
|
|||
/*
|
||||
Package parser implements parser for markdown text that generates AST (abstract syntax tree).
|
||||
*/
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
)
|
||||
|
||||
// Extensions is a bitmask of enabled parser extensions.
|
||||
type Extensions int
|
||||
|
||||
// Bit flags representing markdown parsing extensions.
|
||||
// Use | (or) to specify multiple extensions.
|
||||
const (
|
||||
NoExtensions Extensions = 0
|
||||
NoIntraEmphasis Extensions = 1 << iota // Ignore emphasis markers inside words
|
||||
Tables // Parse tables
|
||||
FencedCode // Parse fenced code blocks
|
||||
Autolink // Detect embedded URLs that are not explicitly marked
|
||||
Strikethrough // Strikethrough text using ~~test~~
|
||||
LaxHTMLBlocks // Loosen up HTML block parsing rules
|
||||
SpaceHeadings // Be strict about prefix heading rules
|
||||
HardLineBreak // Translate newlines into line breaks
|
||||
NonBlockingSpace // Translate backspace spaces into line non-blocking spaces
|
||||
TabSizeEight // Expand tabs to eight spaces instead of four
|
||||
Footnotes // Pandoc-style footnotes
|
||||
NoEmptyLineBeforeBlock // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block
|
||||
HeadingIDs // specify heading IDs with {#id}
|
||||
Titleblock // Titleblock ala pandoc
|
||||
AutoHeadingIDs // Create the heading ID from the text
|
||||
BackslashLineBreak // Translate trailing backslashes into line breaks
|
||||
DefinitionLists // Parse definition lists
|
||||
MathJax // Parse MathJax
|
||||
OrderedListStart // Keep track of the first number used when starting an ordered list.
|
||||
Attributes // Block Attributes
|
||||
SuperSubscript // Super- and subscript support: 2^10^, H~2~O.
|
||||
EmptyLinesBreakList // 2 empty lines break out of list
|
||||
Includes // Support including other files.
|
||||
Mmark // Support Mmark syntax, see https://mmark.nl/syntax
|
||||
|
||||
CommonExtensions Extensions = NoIntraEmphasis | Tables | FencedCode |
|
||||
Autolink | Strikethrough | SpaceHeadings | HeadingIDs |
|
||||
BackslashLineBreak | DefinitionLists | MathJax
|
||||
)
|
||||
|
||||
// The size of a tab stop.
|
||||
const (
|
||||
tabSizeDefault = 4
|
||||
tabSizeDouble = 8
|
||||
)
|
||||
|
||||
// for each character that triggers a response when parsing inline data.
|
||||
type inlineParser func(p *Parser, data []byte, offset int) (int, ast.Node)
|
||||
|
||||
// ReferenceOverrideFunc is expected to be called with a reference string and
|
||||
// return either a valid Reference type that the reference string maps to or
|
||||
// nil. If overridden is false, the default reference logic will be executed.
|
||||
// See the documentation in Options for more details on use-case.
|
||||
type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool)
|
||||
|
||||
// Parser is a type that holds extensions and the runtime state used by
|
||||
// Parse, and the renderer. You can not use it directly, construct it with New.
|
||||
type Parser struct {
|
||||
|
||||
// ReferenceOverride is an optional function callback that is called every
|
||||
// time a reference is resolved. It can be set before starting parsing.
|
||||
//
|
||||
// In Markdown, the link reference syntax can be made to resolve a link to
|
||||
// a reference instead of an inline URL, in one of the following ways:
|
||||
//
|
||||
// * [link text][refid]
|
||||
// * [refid][]
|
||||
//
|
||||
// Usually, the refid is defined at the bottom of the Markdown document. If
|
||||
// this override function is provided, the refid is passed to the override
|
||||
// function first, before consulting the defined refids at the bottom. If
|
||||
// the override function indicates an override did not occur, the refids at
|
||||
// the bottom will be used to fill in the link details.
|
||||
ReferenceOverride ReferenceOverrideFunc
|
||||
|
||||
Opts Options
|
||||
|
||||
// after parsing, this is AST root of parsed markdown text
|
||||
Doc ast.Node
|
||||
|
||||
extensions Extensions
|
||||
|
||||
refs map[string]*reference
|
||||
refsRecord map[string]struct{}
|
||||
inlineCallback [256]inlineParser
|
||||
nesting int
|
||||
maxNesting int
|
||||
insideLink bool
|
||||
indexCnt int // incremented after every index
|
||||
|
||||
// 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
|
||||
|
||||
tip ast.Node // = doc
|
||||
oldTip ast.Node
|
||||
lastMatchedContainer ast.Node // = doc
|
||||
allClosed bool
|
||||
|
||||
// Attributes are attached to block level elements.
|
||||
attr *ast.Attribute
|
||||
|
||||
includeStack *incStack
|
||||
}
|
||||
|
||||
// New creates a markdown parser with CommonExtensions.
|
||||
//
|
||||
// You can then call `doc := p.Parse(markdown)` to parse markdown document
|
||||
// and `markdown.Render(doc, renderer)` to convert it to another format with
|
||||
// a renderer.
|
||||
func New() *Parser {
|
||||
return NewWithExtensions(CommonExtensions)
|
||||
}
|
||||
|
||||
// NewWithExtensions creates a markdown parser with given extensions.
|
||||
func NewWithExtensions(extension Extensions) *Parser {
|
||||
p := Parser{
|
||||
refs: make(map[string]*reference),
|
||||
refsRecord: make(map[string]struct{}),
|
||||
maxNesting: 16,
|
||||
insideLink: false,
|
||||
Doc: &ast.Document{},
|
||||
extensions: extension,
|
||||
allClosed: true,
|
||||
includeStack: newIncStack(),
|
||||
}
|
||||
p.tip = p.Doc
|
||||
p.oldTip = p.Doc
|
||||
p.lastMatchedContainer = p.Doc
|
||||
|
||||
p.inlineCallback[' '] = maybeLineBreak
|
||||
p.inlineCallback['*'] = emphasis
|
||||
p.inlineCallback['_'] = emphasis
|
||||
if p.extensions&Strikethrough != 0 {
|
||||
p.inlineCallback['~'] = emphasis
|
||||
}
|
||||
p.inlineCallback['`'] = codeSpan
|
||||
p.inlineCallback['\n'] = lineBreak
|
||||
p.inlineCallback['['] = link
|
||||
p.inlineCallback['<'] = leftAngle
|
||||
p.inlineCallback['\\'] = escape
|
||||
p.inlineCallback['&'] = entity
|
||||
p.inlineCallback['!'] = maybeImage
|
||||
if p.extensions&Mmark != 0 {
|
||||
p.inlineCallback['('] = maybeShortRefOrIndex
|
||||
}
|
||||
p.inlineCallback['^'] = maybeInlineFootnoteOrSuper
|
||||
if p.extensions&Autolink != 0 {
|
||||
p.inlineCallback['h'] = maybeAutoLink
|
||||
p.inlineCallback['m'] = maybeAutoLink
|
||||
p.inlineCallback['f'] = maybeAutoLink
|
||||
p.inlineCallback['H'] = maybeAutoLink
|
||||
p.inlineCallback['M'] = maybeAutoLink
|
||||
p.inlineCallback['F'] = maybeAutoLink
|
||||
}
|
||||
if p.extensions&MathJax != 0 {
|
||||
p.inlineCallback['$'] = math
|
||||
}
|
||||
|
||||
return &p
|
||||
}
|
||||
|
||||
func (p *Parser) getRef(refid string) (ref *reference, found bool) {
|
||||
if p.ReferenceOverride != nil {
|
||||
r, overridden := p.ReferenceOverride(refid)
|
||||
if overridden {
|
||||
if r == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &reference{
|
||||
link: []byte(r.Link),
|
||||
title: []byte(r.Title),
|
||||
noteID: 0,
|
||||
hasBlock: false,
|
||||
text: []byte(r.Text)}, true
|
||||
}
|
||||
}
|
||||
// refs are case insensitive
|
||||
ref, found = p.refs[strings.ToLower(refid)]
|
||||
return ref, found
|
||||
}
|
||||
|
||||
func (p *Parser) isFootnote(ref *reference) bool {
|
||||
_, ok := p.refsRecord[string(ref.link)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p *Parser) finalize(block ast.Node) {
|
||||
p.tip = block.GetParent()
|
||||
}
|
||||
|
||||
func (p *Parser) addChild(node ast.Node) ast.Node {
|
||||
for !canNodeContain(p.tip, node) {
|
||||
p.finalize(p.tip)
|
||||
}
|
||||
ast.AppendChild(p.tip, node)
|
||||
p.tip = node
|
||||
return node
|
||||
}
|
||||
|
||||
func canNodeContain(n ast.Node, v ast.Node) bool {
|
||||
switch n.(type) {
|
||||
case *ast.List:
|
||||
return isListItem(v)
|
||||
case *ast.Document, *ast.BlockQuote, *ast.Aside, *ast.ListItem, *ast.CaptionFigure:
|
||||
return !isListItem(v)
|
||||
case *ast.Table:
|
||||
switch v.(type) {
|
||||
case *ast.TableHeader, *ast.TableBody, *ast.TableFooter:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case *ast.TableHeader, *ast.TableBody, *ast.TableFooter:
|
||||
_, ok := v.(*ast.TableRow)
|
||||
return ok
|
||||
case *ast.TableRow:
|
||||
_, ok := v.(*ast.TableCell)
|
||||
return ok
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Parser) closeUnmatchedBlocks() {
|
||||
if p.allClosed {
|
||||
return
|
||||
}
|
||||
for p.oldTip != p.lastMatchedContainer {
|
||||
parent := p.oldTip.GetParent()
|
||||
p.finalize(p.oldTip)
|
||||
p.oldTip = parent
|
||||
}
|
||||
p.allClosed = true
|
||||
}
|
||||
|
||||
// Reference represents the details of a link.
|
||||
// See the documentation in Options for more details on use-case.
|
||||
type Reference struct {
|
||||
// Link is usually the URL the reference points to.
|
||||
Link string
|
||||
// Title is the alternate text describing the link in more detail.
|
||||
Title string
|
||||
// Text is the optional text to override the ref with if the syntax used was
|
||||
// [refid][]
|
||||
Text string
|
||||
}
|
||||
|
||||
// Parse generates AST (abstract syntax tree) representing markdown document.
|
||||
//
|
||||
// The result is a root of the tree whose underlying type is *ast.Document
|
||||
//
|
||||
// You can then convert AST to html using html.Renderer, to some other format
|
||||
// using a custom renderer or transform the tree.
|
||||
func (p *Parser) Parse(input []byte) ast.Node {
|
||||
p.block(input)
|
||||
// Walk the tree and finish up some of unfinished blocks
|
||||
for p.tip != nil {
|
||||
p.finalize(p.tip)
|
||||
}
|
||||
// Walk the tree again and process inline markdown in each block
|
||||
ast.WalkFunc(p.Doc, func(node ast.Node, entering bool) ast.WalkStatus {
|
||||
switch node.(type) {
|
||||
case *ast.Paragraph, *ast.Heading, *ast.TableCell:
|
||||
p.Inline(node, node.AsContainer().Content)
|
||||
node.AsContainer().Content = nil
|
||||
}
|
||||
return ast.GoToNext
|
||||
})
|
||||
|
||||
if p.Opts.Flags&SkipFootnoteList == 0 {
|
||||
p.parseRefsToAST()
|
||||
}
|
||||
return p.Doc
|
||||
}
|
||||
|
||||
func (p *Parser) parseRefsToAST() {
|
||||
if p.extensions&Footnotes == 0 || len(p.notes) == 0 {
|
||||
return
|
||||
}
|
||||
p.tip = p.Doc
|
||||
list := &ast.List{
|
||||
IsFootnotesList: true,
|
||||
ListFlags: ast.ListTypeOrdered,
|
||||
}
|
||||
p.addBlock(&ast.Footnotes{})
|
||||
block := p.addBlock(list)
|
||||
flags := ast.ListItemBeginningOfList
|
||||
// Note: this loop is intentionally explicit, not range-form. This is
|
||||
// because the body of the loop will append nested footnotes to p.notes and
|
||||
// we need to process those late additions. Range form would only walk over
|
||||
// the fixed initial set.
|
||||
for i := 0; i < len(p.notes); i++ {
|
||||
ref := p.notes[i]
|
||||
p.addChild(ref.footnote)
|
||||
block := ref.footnote
|
||||
listItem := block.(*ast.ListItem)
|
||||
listItem.ListFlags = flags | ast.ListTypeOrdered
|
||||
listItem.RefLink = ref.link
|
||||
if ref.hasBlock {
|
||||
flags |= ast.ListItemContainsBlock
|
||||
p.block(ref.title)
|
||||
} else {
|
||||
p.Inline(block, ref.title)
|
||||
}
|
||||
flags &^= ast.ListItemBeginningOfList | ast.ListItemContainsBlock
|
||||
}
|
||||
above := list.Parent
|
||||
finalizeList(list)
|
||||
p.tip = above
|
||||
|
||||
ast.WalkFunc(block, func(node ast.Node, entering bool) ast.WalkStatus {
|
||||
switch node.(type) {
|
||||
case *ast.Paragraph, *ast.Heading:
|
||||
p.Inline(node, node.AsContainer().Content)
|
||||
node.AsContainer().Content = nil
|
||||
}
|
||||
return ast.GoToNext
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Link references
|
||||
//
|
||||
// This section implements support for references that (usually) appear
|
||||
// as footnotes in a document, and can be referenced anywhere in the document.
|
||||
// The basic format is:
|
||||
//
|
||||
// [1]: http://www.google.com/ "Google"
|
||||
// [2]: http://www.github.com/ "Github"
|
||||
//
|
||||
// Anywhere in the document, the reference can be linked by referring to its
|
||||
// label, i.e., 1 and 2 in this example, as in:
|
||||
//
|
||||
// This library is hosted on [Github][2], a git hosting site.
|
||||
//
|
||||
// Actual footnotes as specified in Pandoc and supported by some other Markdown
|
||||
// libraries such as php-markdown are also taken care of. They look like this:
|
||||
//
|
||||
// This sentence needs a bit of further explanation.[^note]
|
||||
//
|
||||
// [^note]: This is the explanation.
|
||||
//
|
||||
// Footnotes should be placed at the end of the document in an ordered list.
|
||||
// Inline footnotes such as:
|
||||
//
|
||||
// Inline footnotes^[Not supported.] also exist.
|
||||
//
|
||||
// are not yet supported.
|
||||
|
||||
// reference holds all information necessary for a reference-style links or
|
||||
// footnotes.
|
||||
//
|
||||
// Consider this markdown with reference-style links:
|
||||
//
|
||||
// [link][ref]
|
||||
//
|
||||
// [ref]: /url/ "tooltip title"
|
||||
//
|
||||
// It will be ultimately converted to this HTML:
|
||||
//
|
||||
// <p><a href=\"/url/\" title=\"title\">link</a></p>
|
||||
//
|
||||
// And a reference structure will be populated as follows:
|
||||
//
|
||||
// p.refs["ref"] = &reference{
|
||||
// link: "/url/",
|
||||
// title: "tooltip title",
|
||||
// }
|
||||
//
|
||||
// Alternatively, reference can contain information about a footnote. Consider
|
||||
// this markdown:
|
||||
//
|
||||
// Text needing a footnote.[^a]
|
||||
//
|
||||
// [^a]: This is the note
|
||||
//
|
||||
// A reference structure will be populated as follows:
|
||||
//
|
||||
// p.refs["a"] = &reference{
|
||||
// link: "a",
|
||||
// title: "This is the note",
|
||||
// noteID: <some positive int>,
|
||||
// }
|
||||
//
|
||||
// TODO: As you can see, it begs for splitting into two dedicated structures
|
||||
// for refs and for footnotes.
|
||||
type reference struct {
|
||||
link []byte
|
||||
title []byte
|
||||
noteID int // 0 if not a footnote ref
|
||||
hasBlock bool
|
||||
footnote ast.Node // a link to the Item node within a list of footnotes
|
||||
|
||||
text []byte // only gets populated by refOverride feature with Reference.Text
|
||||
}
|
||||
|
||||
func (r *reference) String() string {
|
||||
return fmt.Sprintf("{link: %q, title: %q, text: %q, noteID: %d, hasBlock: %v}",
|
||||
r.link, r.title, r.text, r.noteID, r.hasBlock)
|
||||
}
|
||||
|
||||
// Check whether or not data starts with a reference link.
|
||||
// If so, it is parsed and stored in the list of references
|
||||
// (in the render struct).
|
||||
// Returns the number of bytes to skip to move past it,
|
||||
// or zero if the first line is not a reference.
|
||||
func isReference(p *Parser, data []byte, tabSize int) int {
|
||||
// up to 3 optional leading spaces
|
||||
if len(data) < 4 {
|
||||
return 0
|
||||
}
|
||||
i := 0
|
||||
for i < 3 && data[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
|
||||
noteID := 0
|
||||
|
||||
// id part: anything but a newline between brackets
|
||||
if data[i] != '[' {
|
||||
return 0
|
||||
}
|
||||
i++
|
||||
if p.extensions&Footnotes != 0 {
|
||||
if i < len(data) && data[i] == '^' {
|
||||
// we can set it to anything here because the proper noteIds will
|
||||
// be assigned later during the second pass. It just has to be != 0
|
||||
noteID = 1
|
||||
i++
|
||||
}
|
||||
}
|
||||
idOffset := i
|
||||
for i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != ']' {
|
||||
i++
|
||||
}
|
||||
if i >= len(data) || data[i] != ']' {
|
||||
return 0
|
||||
}
|
||||
idEnd := i
|
||||
// footnotes can have empty ID, like this: [^], but a reference can not be
|
||||
// empty like this: []. Break early if it's not a footnote and there's no ID
|
||||
if noteID == 0 && idOffset == idEnd {
|
||||
return 0
|
||||
}
|
||||
// spacer: colon (space | tab)* newline? (space | tab)*
|
||||
i++
|
||||
if i >= len(data) || data[i] != ':' {
|
||||
return 0
|
||||
}
|
||||
i++
|
||||
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
if i < len(data) && (data[i] == '\n' || data[i] == '\r') {
|
||||
i++
|
||||
if i < len(data) && data[i] == '\n' && data[i-1] == '\r' {
|
||||
i++
|
||||
}
|
||||
}
|
||||
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
if i >= len(data) {
|
||||
return 0
|
||||
}
|
||||
|
||||
var (
|
||||
linkOffset, linkEnd int
|
||||
titleOffset, titleEnd int
|
||||
lineEnd int
|
||||
raw []byte
|
||||
hasBlock bool
|
||||
)
|
||||
|
||||
if p.extensions&Footnotes != 0 && noteID != 0 {
|
||||
linkOffset, linkEnd, raw, hasBlock = scanFootnote(p, data, i, tabSize)
|
||||
lineEnd = linkEnd
|
||||
} else {
|
||||
linkOffset, linkEnd, titleOffset, titleEnd, lineEnd = scanLinkRef(p, data, i)
|
||||
}
|
||||
if lineEnd == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// a valid ref has been found
|
||||
|
||||
ref := &reference{
|
||||
noteID: noteID,
|
||||
hasBlock: hasBlock,
|
||||
}
|
||||
|
||||
if noteID > 0 {
|
||||
// reusing the link field for the id since footnotes don't have links
|
||||
ref.link = data[idOffset:idEnd]
|
||||
// if footnote, it's not really a title, it's the contained text
|
||||
ref.title = raw
|
||||
} else {
|
||||
ref.link = data[linkOffset:linkEnd]
|
||||
ref.title = data[titleOffset:titleEnd]
|
||||
}
|
||||
|
||||
// id matches are case-insensitive
|
||||
id := string(bytes.ToLower(data[idOffset:idEnd]))
|
||||
|
||||
p.refs[id] = ref
|
||||
|
||||
return lineEnd
|
||||
}
|
||||
|
||||
func scanLinkRef(p *Parser, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) {
|
||||
// link: whitespace-free sequence, optionally between angle brackets
|
||||
if data[i] == '<' {
|
||||
i++
|
||||
}
|
||||
linkOffset = i
|
||||
for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' {
|
||||
i++
|
||||
}
|
||||
linkEnd = i
|
||||
if linkEnd < len(data) && data[linkOffset] == '<' && data[linkEnd-1] == '>' {
|
||||
linkOffset++
|
||||
linkEnd--
|
||||
}
|
||||
|
||||
// optional spacer: (space | tab)* (newline | '\'' | '"' | '(' )
|
||||
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
if i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != '\'' && data[i] != '"' && data[i] != '(' {
|
||||
return
|
||||
}
|
||||
|
||||
// compute end-of-line
|
||||
if i >= len(data) || data[i] == '\r' || data[i] == '\n' {
|
||||
lineEnd = i
|
||||
}
|
||||
if i+1 < len(data) && data[i] == '\r' && data[i+1] == '\n' {
|
||||
lineEnd++
|
||||
}
|
||||
|
||||
// optional (space|tab)* spacer after a newline
|
||||
if lineEnd > 0 {
|
||||
i = lineEnd + 1
|
||||
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
// optional title: any non-newline sequence enclosed in '"() alone on its line
|
||||
if i+1 < len(data) && (data[i] == '\'' || data[i] == '"' || data[i] == '(') {
|
||||
i++
|
||||
titleOffset = i
|
||||
|
||||
// look for EOL
|
||||
for i < len(data) && data[i] != '\n' && data[i] != '\r' {
|
||||
i++
|
||||
}
|
||||
if i+1 < len(data) && data[i] == '\n' && data[i+1] == '\r' {
|
||||
titleEnd = i + 1
|
||||
} else {
|
||||
titleEnd = i
|
||||
}
|
||||
|
||||
// step back
|
||||
i--
|
||||
for i > titleOffset && (data[i] == ' ' || data[i] == '\t') {
|
||||
i--
|
||||
}
|
||||
if i > titleOffset && (data[i] == '\'' || data[i] == '"' || data[i] == ')') {
|
||||
lineEnd = titleEnd
|
||||
titleEnd = i
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The first bit of this logic is the same as Parser.listItem, but the rest
|
||||
// is much simpler. This function simply finds the entire block and shifts it
|
||||
// over by one tab if it is indeed a block (just returns the line if it's not).
|
||||
// blockEnd is the end of the section in the input buffer, and contents is the
|
||||
// extracted text that was shifted over one tab. It will need to be rendered at
|
||||
// the end of the document.
|
||||
func scanFootnote(p *Parser, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) {
|
||||
if i == 0 || len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// skip leading whitespace on first line
|
||||
for i < len(data) && data[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
|
||||
blockStart = i
|
||||
|
||||
// find the end of the line
|
||||
blockEnd = i
|
||||
for i < len(data) && data[i-1] != '\n' {
|
||||
i++
|
||||
}
|
||||
|
||||
// get working buffer
|
||||
var raw bytes.Buffer
|
||||
|
||||
// put the first line into the working buffer
|
||||
raw.Write(data[blockEnd:i])
|
||||
blockEnd = i
|
||||
|
||||
// process the following lines
|
||||
containsBlankLine := false
|
||||
|
||||
gatherLines:
|
||||
for blockEnd < len(data) {
|
||||
i++
|
||||
|
||||
// find the end of this line
|
||||
for i < len(data) && 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[blockEnd:i]) > 0 {
|
||||
containsBlankLine = true
|
||||
blockEnd = i
|
||||
continue
|
||||
}
|
||||
|
||||
n := 0
|
||||
if n = isIndented(data[blockEnd:i], indentSize); n == 0 {
|
||||
// this is the end of the block.
|
||||
// we don't want to include this last line in the index.
|
||||
break gatherLines
|
||||
}
|
||||
|
||||
// if there were blank lines before this one, insert a new one now
|
||||
if containsBlankLine {
|
||||
raw.WriteByte('\n')
|
||||
containsBlankLine = false
|
||||
}
|
||||
|
||||
// get rid of that first tab, write to buffer
|
||||
raw.Write(data[blockEnd+n : i])
|
||||
hasBlock = true
|
||||
|
||||
blockEnd = i
|
||||
}
|
||||
|
||||
if data[blockEnd-1] != '\n' {
|
||||
raw.WriteByte('\n')
|
||||
}
|
||||
|
||||
contents = raw.Bytes()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// isPunctuation returns true if c is a punctuation symbol.
|
||||
func isPunctuation(c byte) bool {
|
||||
for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") {
|
||||
if c == r {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isSpace returns true if c is a white-space charactr
|
||||
func isSpace(c byte) bool {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'
|
||||
}
|
||||
|
||||
// isLetter returns true if c is ascii letter
|
||||
func isLetter(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
}
|
||||
|
||||
// isAlnum returns true if c is a digit or letter
|
||||
// TODO: check when this is looking for ASCII alnum and when it should use unicode
|
||||
func isAlnum(c byte) bool {
|
||||
return (c >= '0' && c <= '9') || isLetter(c)
|
||||
}
|
||||
|
||||
// TODO: this is not used
|
||||
// Replace tab characters with spaces, aligning to the next TAB_SIZE column.
|
||||
// always ends output with a newline
|
||||
func expandTabs(out *bytes.Buffer, line []byte, tabSize int) {
|
||||
// first, check for common cases: no tabs, or only tabs at beginning of line
|
||||
i, prefix := 0, 0
|
||||
slowcase := false
|
||||
for i = 0; i < len(line); i++ {
|
||||
if line[i] == '\t' {
|
||||
if prefix == i {
|
||||
prefix++
|
||||
} else {
|
||||
slowcase = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no need to decode runes if all tabs are at the beginning of the line
|
||||
if !slowcase {
|
||||
for i = 0; i < prefix*tabSize; i++ {
|
||||
out.WriteByte(' ')
|
||||
}
|
||||
out.Write(line[prefix:])
|
||||
return
|
||||
}
|
||||
|
||||
// the slow case: we need to count runes to figure out how
|
||||
// many spaces to insert for each tab
|
||||
column := 0
|
||||
i = 0
|
||||
for i < len(line) {
|
||||
start := i
|
||||
for i < len(line) && line[i] != '\t' {
|
||||
_, size := utf8.DecodeRune(line[i:])
|
||||
i += size
|
||||
column++
|
||||
}
|
||||
|
||||
if i > start {
|
||||
out.Write(line[start:i])
|
||||
}
|
||||
|
||||
if i >= len(line) {
|
||||
break
|
||||
}
|
||||
|
||||
for {
|
||||
out.WriteByte(' ')
|
||||
column++
|
||||
if column%tabSize == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
// Find if a line counts as indented or not.
|
||||
// Returns number of characters the indent is (0 = not indented).
|
||||
func isIndented(data []byte, indentSize int) int {
|
||||
if len(data) == 0 {
|
||||
return 0
|
||||
}
|
||||
if data[0] == '\t' {
|
||||
return 1
|
||||
}
|
||||
if len(data) < indentSize {
|
||||
return 0
|
||||
}
|
||||
for i := 0; i < indentSize; i++ {
|
||||
if data[i] != ' ' {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return indentSize
|
||||
}
|
||||
|
||||
// Create a url-safe slug for fragments
|
||||
func slugify(in []byte) []byte {
|
||||
if len(in) == 0 {
|
||||
return in
|
||||
}
|
||||
out := make([]byte, 0, len(in))
|
||||
sym := false
|
||||
|
||||
for _, ch := range in {
|
||||
if isAlnum(ch) {
|
||||
sym = false
|
||||
out = append(out, ch)
|
||||
} else if sym {
|
||||
continue
|
||||
} else {
|
||||
out = append(out, '-')
|
||||
sym = true
|
||||
}
|
||||
}
|
||||
var a, b int
|
||||
var ch byte
|
||||
for a, ch = range out {
|
||||
if ch != '-' {
|
||||
break
|
||||
}
|
||||
}
|
||||
for b = len(out) - 1; b > 0; b-- {
|
||||
if out[b] != '-' {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out[a : b+1]
|
||||
}
|
||||
|
||||
func isListItem(d ast.Node) bool {
|
||||
_, ok := d.(*ast.ListItem)
|
||||
return ok
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
)
|
||||
|
||||
// parse '(#r)', where r does not contain spaces. Or.
|
||||
// (!item) (!item, subitem), for an index, (!!item) signals primary.
|
||||
func maybeShortRefOrIndex(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
if len(data[offset:]) < 4 {
|
||||
return 0, nil
|
||||
}
|
||||
// short ref first
|
||||
data = data[offset:]
|
||||
i := 1
|
||||
switch data[i] {
|
||||
case '#': // cross ref
|
||||
i++
|
||||
Loop:
|
||||
for i < len(data) {
|
||||
c := data[i]
|
||||
switch {
|
||||
case c == ')':
|
||||
break Loop
|
||||
case !isAlnum(c):
|
||||
if c == '_' || c == '-' || c == ':' {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
i = 0
|
||||
break Loop
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i >= len(data) {
|
||||
return 0, nil
|
||||
}
|
||||
if data[i] != ')' {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
id := data[2:i]
|
||||
node := &ast.CrossReference{}
|
||||
node.Destination = id
|
||||
|
||||
return i + 1, node
|
||||
|
||||
case '!': // index
|
||||
i++
|
||||
start := i
|
||||
i = skipUntilChar(data, start, ')')
|
||||
|
||||
// did we reach the end of the buffer without a closing marker?
|
||||
if i >= len(data) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if len(data[start:i]) < 1 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
idx := &ast.Index{}
|
||||
|
||||
idx.ID = fmt.Sprintf("idxref:%d", p.indexCnt)
|
||||
p.indexCnt++
|
||||
|
||||
idx.Primary = data[start] == '!'
|
||||
buf := data[start:i]
|
||||
|
||||
if idx.Primary {
|
||||
buf = buf[1:]
|
||||
}
|
||||
items := bytes.Split(buf, []byte(","))
|
||||
switch len(items) {
|
||||
case 1:
|
||||
idx.Item = bytes.TrimSpace(items[0])
|
||||
return i + 1, idx
|
||||
case 2:
|
||||
idx.Item = bytes.TrimSpace(items[0])
|
||||
idx.Subitem = bytes.TrimSpace(items[1])
|
||||
return i + 1, idx
|
||||
}
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
# Things to do
|
||||
|
||||
[ ] docs: add examples like https://godoc.org/github.com/dgrijalva/jwt-go (put in foo_example_test.go). Or see https://github.com/garyburd/redigo/blob/master/redis/zpop_example_test.go#L5 / https://godoc.org/github.com/garyburd/redigo/redis or https://godoc.org/github.com/go-redis/redis
|
||||
|
||||
[ ] figure out expandTabs and parser.TabSizeEight. Are those used?
|
||||
|
||||
[ ] SoftbreakData is not used
|
|
@ -0,0 +1,189 @@
|
|||
## Tracking perf changes
|
||||
|
||||
Initial performance:
|
||||
```
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
pkg: github.com/gomarkdown/markdown
|
||||
BenchmarkEscapeHTML-8 2000000 823 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkSmartDoubleQuotes-8 300000 5033 ns/op 9872 B/op 56 allocs/op
|
||||
BenchmarkReferenceAmps-8 100000 19538 ns/op 26776 B/op 150 allocs/op
|
||||
BenchmarkReferenceAutoLinks-8 100000 17574 ns/op 24544 B/op 132 allocs/op
|
||||
BenchmarkReferenceBackslashEscapes-8 30000 50977 ns/op 76752 B/op 243 allocs/op
|
||||
BenchmarkReferenceBlockquotesWithCodeBlocks-8 200000 8546 ns/op 12864 B/op 65 allocs/op
|
||||
BenchmarkReferenceCodeBlocks-8 200000 9000 ns/op 14912 B/op 70 allocs/op
|
||||
BenchmarkReferenceCodeSpans-8 200000 8856 ns/op 14992 B/op 69 allocs/op
|
||||
BenchmarkReferenceHardWrappedPara-8 200000 6599 ns/op 11312 B/op 57 allocs/op
|
||||
BenchmarkReferenceHorizontalRules-8 100000 15483 ns/op 23536 B/op 98 allocs/op
|
||||
BenchmarkReferenceInlineHTMLAdvances-8 200000 6839 ns/op 12150 B/op 62 allocs/op
|
||||
BenchmarkReferenceInlineHTMLSimple-8 100000 19940 ns/op 28488 B/op 117 allocs/op
|
||||
BenchmarkReferenceInlineHTMLComments-8 200000 7455 ns/op 13440 B/op 64 allocs/op
|
||||
BenchmarkReferenceLinksInline-8 100000 16425 ns/op 23664 B/op 147 allocs/op
|
||||
BenchmarkReferenceLinksReference-8 30000 54895 ns/op 66464 B/op 416 allocs/op
|
||||
BenchmarkReferenceLinksShortcut-8 100000 17647 ns/op 23776 B/op 158 allocs/op
|
||||
BenchmarkReferenceLiterQuotesInTitles-8 200000 9367 ns/op 14832 B/op 95 allocs/op
|
||||
BenchmarkReferenceMarkdownBasics-8 10000 129772 ns/op 130848 B/op 378 allocs/op
|
||||
BenchmarkReferenceMarkdownSyntax-8 3000 502365 ns/op 461411 B/op 1411 allocs/op
|
||||
BenchmarkReferenceNestedBlockquotes-8 200000 7028 ns/op 12688 B/op 64 allocs/op
|
||||
BenchmarkReferenceOrderedAndUnorderedLists-8 20000 79686 ns/op 107520 B/op 374 allocs/op
|
||||
BenchmarkReferenceStrongAndEm-8 200000 10020 ns/op 17792 B/op 78 allocs/op
|
||||
BenchmarkReferenceTabs-8 200000 12025 ns/op 18224 B/op 81 allocs/op
|
||||
BenchmarkReferenceTidyness-8 200000 8985 ns/op 14432 B/op 71 allocs/op
|
||||
PASS
|
||||
ok github.com/gomarkdown/markdown 45.375s
|
||||
```
|
||||
|
||||
After switching to using interface{} for Node.Data:
|
||||
```
|
||||
BenchmarkEscapeHTML-8 2000000 929 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkSmartDoubleQuotes-8 300000 5126 ns/op 9248 B/op 56 allocs/op
|
||||
BenchmarkReferenceAmps-8 100000 19927 ns/op 17880 B/op 154 allocs/op
|
||||
BenchmarkReferenceAutoLinks-8 100000 20732 ns/op 17360 B/op 141 allocs/op
|
||||
BenchmarkReferenceBackslashEscapes-8 30000 50267 ns/op 38128 B/op 244 allocs/op
|
||||
BenchmarkReferenceBlockquotesWithCodeBlocks-8 200000 8988 ns/op 10912 B/op 67 allocs/op
|
||||
BenchmarkReferenceCodeBlocks-8 200000 8611 ns/op 12256 B/op 74 allocs/op
|
||||
BenchmarkReferenceCodeSpans-8 200000 8256 ns/op 11248 B/op 69 allocs/op
|
||||
BenchmarkReferenceHardWrappedPara-8 200000 6739 ns/op 9856 B/op 57 allocs/op
|
||||
BenchmarkReferenceHorizontalRules-8 100000 15503 ns/op 15600 B/op 104 allocs/op
|
||||
BenchmarkReferenceInlineHTMLAdvances-8 200000 6874 ns/op 10278 B/op 62 allocs/op
|
||||
BenchmarkReferenceInlineHTMLSimple-8 100000 22271 ns/op 18552 B/op 121 allocs/op
|
||||
BenchmarkReferenceInlineHTMLComments-8 200000 8315 ns/op 10736 B/op 64 allocs/op
|
||||
BenchmarkReferenceLinksInline-8 100000 16155 ns/op 16912 B/op 152 allocs/op
|
||||
BenchmarkReferenceLinksReference-8 30000 52387 ns/op 38192 B/op 445 allocs/op
|
||||
BenchmarkReferenceLinksShortcut-8 100000 17111 ns/op 16592 B/op 167 allocs/op
|
||||
BenchmarkReferenceLiterQuotesInTitles-8 200000 9164 ns/op 12048 B/op 97 allocs/op
|
||||
BenchmarkReferenceMarkdownBasics-8 10000 129262 ns/op 87264 B/op 416 allocs/op
|
||||
BenchmarkReferenceMarkdownSyntax-8 3000 496873 ns/op 293906 B/op 1559 allocs/op
|
||||
BenchmarkReferenceNestedBlockquotes-8 200000 6854 ns/op 10192 B/op 64 allocs/op
|
||||
BenchmarkReferenceOrderedAndUnorderedLists-8 20000 79633 ns/op 55024 B/op 447 allocs/op
|
||||
BenchmarkReferenceStrongAndEm-8 200000 9637 ns/op 12176 B/op 78 allocs/op
|
||||
BenchmarkReferenceTabs-8 100000 12164 ns/op 13776 B/op 87 allocs/op
|
||||
BenchmarkReferenceTidyness-8 200000 8677 ns/op 11296 B/op 75 allocs/op
|
||||
```
|
||||
|
||||
Not necessarily faster, but uses less bytes per op (but sometimes more allocs).
|
||||
|
||||
After tweaking the API:
|
||||
```
|
||||
$ ./s/run-bench.sh
|
||||
|
||||
go test -bench=. -test.benchmem
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
pkg: github.com/gomarkdown/markdown
|
||||
BenchmarkEscapeHTML-8 2000000 834 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkSmartDoubleQuotes-8 300000 3486 ns/op 6160 B/op 27 allocs/op
|
||||
BenchmarkReferenceAmps-8 100000 18158 ns/op 14792 B/op 125 allocs/op
|
||||
BenchmarkReferenceAutoLinks-8 100000 16824 ns/op 14272 B/op 112 allocs/op
|
||||
BenchmarkReferenceBackslashEscapes-8 30000 44066 ns/op 35040 B/op 215 allocs/op
|
||||
BenchmarkReferenceBlockquotesWithCodeBlocks-8 200000 6868 ns/op 7824 B/op 38 allocs/op
|
||||
BenchmarkReferenceCodeBlocks-8 200000 7157 ns/op 9168 B/op 45 allocs/op
|
||||
BenchmarkReferenceCodeSpans-8 200000 6663 ns/op 8160 B/op 40 allocs/op
|
||||
BenchmarkReferenceHardWrappedPara-8 300000 4821 ns/op 6768 B/op 28 allocs/op
|
||||
BenchmarkReferenceHorizontalRules-8 100000 13033 ns/op 12512 B/op 75 allocs/op
|
||||
BenchmarkReferenceInlineHTMLAdvances-8 300000 4998 ns/op 7190 B/op 33 allocs/op
|
||||
BenchmarkReferenceInlineHTMLSimple-8 100000 17696 ns/op 15464 B/op 92 allocs/op
|
||||
BenchmarkReferenceInlineHTMLComments-8 300000 5506 ns/op 7648 B/op 35 allocs/op
|
||||
BenchmarkReferenceLinksInline-8 100000 14450 ns/op 13824 B/op 123 allocs/op
|
||||
BenchmarkReferenceLinksReference-8 30000 52561 ns/op 35104 B/op 416 allocs/op
|
||||
BenchmarkReferenceLinksShortcut-8 100000 15616 ns/op 13504 B/op 138 allocs/op
|
||||
BenchmarkReferenceLiterQuotesInTitles-8 200000 7772 ns/op 8960 B/op 68 allocs/op
|
||||
BenchmarkReferenceMarkdownBasics-8 10000 121436 ns/op 84176 B/op 387 allocs/op
|
||||
BenchmarkReferenceMarkdownSyntax-8 3000 487404 ns/op 290818 B/op 1530 allocs/op
|
||||
BenchmarkReferenceNestedBlockquotes-8 300000 5098 ns/op 7104 B/op 35 allocs/op
|
||||
BenchmarkReferenceOrderedAndUnorderedLists-8 20000 74422 ns/op 51936 B/op 418 allocs/op
|
||||
BenchmarkReferenceStrongAndEm-8 200000 7888 ns/op 9088 B/op 49 allocs/op
|
||||
BenchmarkReferenceTabs-8 200000 10061 ns/op 10688 B/op 58 allocs/op
|
||||
BenchmarkReferenceTidyness-8 200000 7152 ns/op 8208 B/op 46 allocs/op
|
||||
ok github.com/gomarkdown/markdown 40.809s
|
||||
```
|
||||
|
||||
After refactoring Renderer:
|
||||
```
|
||||
BenchmarkEscapeHTML-8 2000000 883 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkSmartDoubleQuotes-8 300000 3717 ns/op 6208 B/op 29 allocs/op
|
||||
BenchmarkReferenceAmps-8 100000 19135 ns/op 14680 B/op 123 allocs/op
|
||||
BenchmarkReferenceAutoLinks-8 100000 17142 ns/op 14176 B/op 110 allocs/op
|
||||
BenchmarkReferenceBackslashEscapes-8 30000 54616 ns/op 35088 B/op 217 allocs/op
|
||||
BenchmarkReferenceBlockquotesWithCodeBlocks-8 200000 7993 ns/op 7872 B/op 40 allocs/op
|
||||
BenchmarkReferenceCodeBlocks-8 200000 8285 ns/op 9216 B/op 47 allocs/op
|
||||
BenchmarkReferenceCodeSpans-8 200000 7684 ns/op 8208 B/op 42 allocs/op
|
||||
BenchmarkReferenceHardWrappedPara-8 200000 5595 ns/op 6816 B/op 30 allocs/op
|
||||
BenchmarkReferenceHorizontalRules-8 100000 16444 ns/op 12560 B/op 77 allocs/op
|
||||
BenchmarkReferenceInlineHTMLAdvances-8 200000 5415 ns/op 7238 B/op 35 allocs/op
|
||||
BenchmarkReferenceInlineHTMLSimple-8 100000 19867 ns/op 15512 B/op 94 allocs/op
|
||||
BenchmarkReferenceInlineHTMLComments-8 200000 6026 ns/op 7696 B/op 37 allocs/op
|
||||
BenchmarkReferenceLinksInline-8 100000 14864 ns/op 13664 B/op 120 allocs/op
|
||||
BenchmarkReferenceLinksReference-8 30000 52479 ns/op 34816 B/op 401 allocs/op
|
||||
BenchmarkReferenceLinksShortcut-8 100000 15812 ns/op 13472 B/op 135 allocs/op
|
||||
BenchmarkReferenceLiterQuotesInTitles-8 200000 7767 ns/op 8880 B/op 68 allocs/op
|
||||
BenchmarkReferenceMarkdownBasics-8 10000 131065 ns/op 84048 B/op 386 allocs/op
|
||||
BenchmarkReferenceMarkdownSyntax-8 2000 515604 ns/op 289953 B/op 1501 allocs/op
|
||||
BenchmarkReferenceNestedBlockquotes-8 200000 5655 ns/op 7152 B/op 37 allocs/op
|
||||
BenchmarkReferenceOrderedAndUnorderedLists-8 20000 84188 ns/op 51984 B/op 420 allocs/op
|
||||
BenchmarkReferenceStrongAndEm-8 200000 8664 ns/op 9136 B/op 51 allocs/op
|
||||
BenchmarkReferenceTabs-8 100000 11110 ns/op 10736 B/op 60 allocs/op
|
||||
BenchmarkReferenceTidyness-8 200000 7628 ns/op 8256 B/op 48 allocs/op
|
||||
ok github.com/gomarkdown/markdown 40.841s
|
||||
```
|
||||
|
||||
After Node refactor to have Children array:
|
||||
```
|
||||
BenchmarkEscapeHTML-8 2000000 901 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkSmartDoubleQuotes-8 300000 3905 ns/op 6224 B/op 31 allocs/op
|
||||
BenchmarkReferenceAmps-8 100000 22216 ns/op 15560 B/op 157 allocs/op
|
||||
BenchmarkReferenceAutoLinks-8 100000 20335 ns/op 14824 B/op 146 allocs/op
|
||||
BenchmarkReferenceBackslashEscapes-8 20000 69174 ns/op 37392 B/op 316 allocs/op
|
||||
BenchmarkReferenceBlockquotesWithCodeBlocks-8 200000 8443 ns/op 7968 B/op 48 allocs/op
|
||||
BenchmarkReferenceCodeBlocks-8 200000 9250 ns/op 9392 B/op 58 allocs/op
|
||||
BenchmarkReferenceCodeSpans-8 200000 8515 ns/op 8432 B/op 54 allocs/op
|
||||
BenchmarkReferenceHardWrappedPara-8 200000 5738 ns/op 6856 B/op 34 allocs/op
|
||||
BenchmarkReferenceHorizontalRules-8 100000 20864 ns/op 13648 B/op 93 allocs/op
|
||||
BenchmarkReferenceInlineHTMLAdvances-8 200000 6187 ns/op 7310 B/op 40 allocs/op
|
||||
BenchmarkReferenceInlineHTMLSimple-8 50000 23793 ns/op 16128 B/op 114 allocs/op
|
||||
BenchmarkReferenceInlineHTMLComments-8 200000 7060 ns/op 7840 B/op 44 allocs/op
|
||||
BenchmarkReferenceLinksInline-8 100000 18432 ns/op 14496 B/op 153 allocs/op
|
||||
BenchmarkReferenceLinksReference-8 20000 67666 ns/op 37136 B/op 502 allocs/op
|
||||
BenchmarkReferenceLinksShortcut-8 100000 19324 ns/op 13984 B/op 162 allocs/op
|
||||
BenchmarkReferenceLiterQuotesInTitles-8 200000 8998 ns/op 9320 B/op 83 allocs/op
|
||||
BenchmarkReferenceMarkdownBasics-8 10000 160908 ns/op 88152 B/op 518 allocs/op
|
||||
BenchmarkReferenceMarkdownSyntax-8 2000 707160 ns/op 303801 B/op 2044 allocs/op
|
||||
BenchmarkReferenceNestedBlockquotes-8 200000 6740 ns/op 7248 B/op 45 allocs/op
|
||||
BenchmarkReferenceOrderedAndUnorderedLists-8 10000 115808 ns/op 55052 B/op 626 allocs/op
|
||||
BenchmarkReferenceStrongAndEm-8 100000 10540 ns/op 9416 B/op 72 allocs/op
|
||||
BenchmarkReferenceTabs-8 100000 13171 ns/op 10968 B/op 77 allocs/op
|
||||
BenchmarkReferenceTidyness-8 200000 8903 ns/op 8404 B/op 62 allocs/op
|
||||
PASS
|
||||
ok github.com/gomarkdown/markdown 43.477s
|
||||
```
|
||||
It's slower (but opens up possibilities for further improvements).
|
||||
|
||||
After refactoring to make ast.Node a top-level thing.
|
||||
```
|
||||
BenchmarkEscapeHTML-8 2000000 829 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkSmartDoubleQuotes-8 300000 3998 ns/op 6192 B/op 31 allocs/op
|
||||
BenchmarkReferenceAmps-8 50000 27389 ns/op 15480 B/op 153 allocs/op
|
||||
BenchmarkReferenceAutoLinks-8 50000 23106 ns/op 14656 B/op 137 allocs/op
|
||||
BenchmarkReferenceBackslashEscapes-8 10000 112435 ns/op 36696 B/op 315 allocs/op
|
||||
BenchmarkReferenceBlockquotesWithCodeBlocks-8 200000 9227 ns/op 7856 B/op 46 allocs/op
|
||||
BenchmarkReferenceCodeBlocks-8 200000 10469 ns/op 9248 B/op 54 allocs/op
|
||||
BenchmarkReferenceCodeSpans-8 200000 10522 ns/op 8368 B/op 54 allocs/op
|
||||
BenchmarkReferenceHardWrappedPara-8 200000 6354 ns/op 6784 B/op 34 allocs/op
|
||||
BenchmarkReferenceHorizontalRules-8 50000 32393 ns/op 13952 B/op 87 allocs/op
|
||||
BenchmarkReferenceInlineHTMLAdvances-8 200000 6894 ns/op 7238 B/op 40 allocs/op
|
||||
BenchmarkReferenceInlineHTMLSimple-8 50000 32942 ns/op 15864 B/op 110 allocs/op
|
||||
BenchmarkReferenceInlineHTMLComments-8 200000 8181 ns/op 7776 B/op 44 allocs/op
|
||||
BenchmarkReferenceLinksInline-8 100000 21679 ns/op 14400 B/op 148 allocs/op
|
||||
BenchmarkReferenceLinksReference-8 20000 83928 ns/op 36688 B/op 473 allocs/op
|
||||
BenchmarkReferenceLinksShortcut-8 100000 22053 ns/op 13872 B/op 153 allocs/op
|
||||
BenchmarkReferenceLiterQuotesInTitles-8 100000 10784 ns/op 9296 B/op 81 allocs/op
|
||||
BenchmarkReferenceMarkdownBasics-8 5000 237097 ns/op 87760 B/op 480 allocs/op
|
||||
BenchmarkReferenceMarkdownSyntax-8 1000 1465402 ns/op 300769 B/op 1896 allocs/op
|
||||
BenchmarkReferenceNestedBlockquotes-8 200000 7461 ns/op 7152 B/op 45 allocs/op
|
||||
BenchmarkReferenceOrderedAndUnorderedLists-8 5000 212256 ns/op 53724 B/op 553 allocs/op
|
||||
BenchmarkReferenceStrongAndEm-8 100000 13018 ns/op 9264 B/op 72 allocs/op
|
||||
BenchmarkReferenceTabs-8 100000 15005 ns/op 10752 B/op 71 allocs/op
|
||||
BenchmarkReferenceTidyness-8 200000 10308 ns/op 8292 B/op 58 allocs/op
|
||||
PASS
|
||||
ok github.com/gomarkdown/markdown 42.176s
|
||||
```
|
|
@ -1,16 +0,0 @@
|
|||
image: golang:1.11
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- go build ./...
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- test -z "$(gofmt -l . | tee /dev/stderr)"
|
||||
- go test ./...
|
|
@ -1,10 +0,0 @@
|
|||
Copyright (c) 2015, The Authors
|
||||
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.
|
|
@ -1,8 +0,0 @@
|
|||
html [![License](https://img.shields.io/badge/licence-BSD--2--Clause-blue.svg)](https://opensource.org/licenses/BSD-2-Clause) [![GoDoc](http://godoc.org/gitlab.com/golang-commonmark/html?status.svg)](http://godoc.org/gitlab.com/golang-commonmark/html) [![Pipeline status](https://gitlab.com/golang-commonmark/html/badges/master/pipeline.svg)](https://gitlab.com/golang-commonmark/html/commits/master)
|
||||
====
|
||||
|
||||
Package html provides functions for escaping/unescaping HTML text and for parsing HTML entities.
|
||||
|
||||
## Install
|
||||
|
||||
go get -u gitlab.com/golang-commonmark/html
|
|
@ -1,211 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package html provides functions for escaping/unescaping HTML text and for parsing HTML entities.
|
||||
package html
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const BadEntity = string(utf8.RuneError)
|
||||
|
||||
var htmlEscapeReplacer = strings.NewReplacer(
|
||||
"&", "&",
|
||||
"<", "<",
|
||||
">", ">",
|
||||
`"`, """,
|
||||
)
|
||||
|
||||
func EscapeString(s string) string {
|
||||
return htmlEscapeReplacer.Replace(s)
|
||||
}
|
||||
|
||||
func WriteEscapedString(w io.Writer, s string) error {
|
||||
_, err := htmlEscapeReplacer.WriteString(w, s)
|
||||
return err
|
||||
}
|
||||
|
||||
func isValidEntityCode(c int64) bool {
|
||||
switch {
|
||||
case !utf8.ValidRune(rune(c)):
|
||||
return false
|
||||
|
||||
// never used
|
||||
case c >= 0xfdd0 && c <= 0xfdef:
|
||||
return false
|
||||
case c&0xffff == 0xffff || c&0xffff == 0xfffe:
|
||||
return false
|
||||
// control codes
|
||||
case c >= 0x00 && c <= 0x08:
|
||||
return false
|
||||
case c == 0x0b:
|
||||
return false
|
||||
case c >= 0x0e && c <= 0x1f:
|
||||
return false
|
||||
case c >= 0x7f && c <= 0x9f:
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func letter(b byte) bool { return b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' }
|
||||
|
||||
func digit(b byte) bool { return b >= '0' && b <= '9' }
|
||||
|
||||
func alphanum(b byte) bool { return letter(b) || digit(b) }
|
||||
|
||||
func hexDigit(b byte) bool {
|
||||
return digit(b) || b >= 'a' && b <= 'f' || b >= 'A' && b <= 'F'
|
||||
}
|
||||
|
||||
func ParseEntity(s string) (string, int) {
|
||||
st := 0
|
||||
var n int
|
||||
|
||||
for i := 1; i < len(s); i++ {
|
||||
b := s[i]
|
||||
|
||||
switch st {
|
||||
case 0: // initial state
|
||||
switch {
|
||||
case b == '#':
|
||||
st = 1
|
||||
case letter(b):
|
||||
n = 1
|
||||
st = 2
|
||||
default:
|
||||
return "", 0
|
||||
}
|
||||
|
||||
case 1: // &#
|
||||
switch {
|
||||
case b == 'x' || b == 'X':
|
||||
st = 3
|
||||
case digit(b):
|
||||
n = 1
|
||||
st = 4
|
||||
default:
|
||||
return "", 0
|
||||
}
|
||||
|
||||
case 2: // &q
|
||||
switch {
|
||||
case alphanum(b):
|
||||
n++
|
||||
if n > 31 {
|
||||
return "", 0
|
||||
}
|
||||
case b == ';':
|
||||
if e, ok := entities[s[i-n:i]]; ok {
|
||||
return e, i + 1
|
||||
}
|
||||
return "", 0
|
||||
default:
|
||||
return "", 0
|
||||
}
|
||||
|
||||
case 3: // &#x
|
||||
switch {
|
||||
case hexDigit(b):
|
||||
n = 1
|
||||
st = 5
|
||||
default:
|
||||
return "", 0
|
||||
}
|
||||
|
||||
case 4: // �
|
||||
switch {
|
||||
case digit(b):
|
||||
n++
|
||||
if n > 8 {
|
||||
return "", 0
|
||||
}
|
||||
case b == ';':
|
||||
c, _ := strconv.ParseInt(s[i-n:i], 10, 32)
|
||||
if !isValidEntityCode(c) {
|
||||
return BadEntity, i + 1
|
||||
}
|
||||
return string(rune(c)), i + 1
|
||||
default:
|
||||
return "", 0
|
||||
}
|
||||
|
||||
case 5: // �
|
||||
switch {
|
||||
case hexDigit(b):
|
||||
n++
|
||||
if n > 8 {
|
||||
return "", 0
|
||||
}
|
||||
case b == ';':
|
||||
c, err := strconv.ParseInt(s[i-n:i], 16, 32)
|
||||
if err != nil {
|
||||
return BadEntity, i + 1
|
||||
}
|
||||
if !isValidEntityCode(c) {
|
||||
return BadEntity, i + 1
|
||||
}
|
||||
return string(rune(c)), i + 1
|
||||
default:
|
||||
return "", 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", 0
|
||||
}
|
||||
|
||||
func UnescapeString(s string) string {
|
||||
i := strings.IndexByte(s, '&')
|
||||
if i < 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
anyChanges := false
|
||||
var entityStr string
|
||||
var entityLen int
|
||||
for i < len(s) {
|
||||
if s[i] == '&' {
|
||||
entityStr, entityLen = ParseEntity(s[i:])
|
||||
if entityLen > 0 {
|
||||
anyChanges = true
|
||||
break
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if !anyChanges {
|
||||
return s
|
||||
}
|
||||
|
||||
buf := make([]byte, len(s)-entityLen+len(entityStr))
|
||||
copy(buf[:i], s)
|
||||
n := copy(buf[i:], entityStr)
|
||||
j := i + n
|
||||
i += entityLen
|
||||
for i < len(s) {
|
||||
b := s[i]
|
||||
if b == '&' {
|
||||
entityStr, entityLen = ParseEntity(s[i:])
|
||||
if entityLen > 0 {
|
||||
n = copy(buf[j:], entityStr)
|
||||
j += n
|
||||
i += entityLen
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
buf[j] = b
|
||||
j++
|
||||
i++
|
||||
}
|
||||
|
||||
return string(buf[:j])
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,19 +0,0 @@
|
|||
image: golang:1.13
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
|
||||
before_script:
|
||||
- go get golang.org/x/text/unicode/rangetable
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- go build ./...
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- test -z "$(gofmt -l . | tee /dev/stderr)"
|
||||
- go test ./...
|
|
@ -1,10 +0,0 @@
|
|||
Copyright (c) 2015, The Authors
|
||||
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.
|
|
@ -1,12 +0,0 @@
|
|||
linkify [![License](https://img.shields.io/badge/licence-BSD--2--Clause-blue.svg)](https://opensource.org/licenses/BSD-2-Clause) [![GoDoc](http://godoc.org/gitlab.com/golang-commonmark/linkify?status.svg)](http://godoc.org/gitlab.com/golang-commonmark/linkify) [![Pipeline status](https://gitlab.com/golang-commonmark/linkify/badges/master/pipeline.svg)](https://gitlab.com/golang-commonmark/linkify/commits/master)
|
||||
=======
|
||||
|
||||
Package linkify provides a way to find what looks like links in plain text.
|
||||
|
||||
## Install
|
||||
|
||||
go get -u gitlab.com/golang-commonmark/linkify
|
||||
|
||||
## Use
|
||||
|
||||
See an [example](https://gitlab.com/golang-commonmark/linkify/blob/master/linkify_example_test.go).
|
|
@ -1,51 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package linkify
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/unicode/rangetable"
|
||||
)
|
||||
|
||||
var (
|
||||
unreserved = [256]bool{'-': true, '.': true, '_': true, '~': true}
|
||||
basicPunct = [256]bool{'.': true, ',': true, '?': true, '!': true, ';': true, ':': true}
|
||||
subdelims = [256]bool{'!': true, '$': true, '&': true, '\'': true, '(': true, ')': true,
|
||||
'*': true, '+': true, ',': true, ';': true, '=': true}
|
||||
emailcs = [256]bool{'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 'g': true,
|
||||
'h': true, 'i': true, 'j': true, 'k': true, 'l': true, 'm': true, 'n': true, 'o': true,
|
||||
'p': true, 'q': true, 'r': true, 's': true, 't': true, 'u': true, 'v': true, 'w': true,
|
||||
'x': true, 'y': true, 'z': true, 'A': true, 'B': true, 'C': true, 'D': true, 'E': true,
|
||||
'F': true, 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true, 'M': true,
|
||||
'N': true, 'O': true, 'P': true, 'Q': true, 'R': true, 'S': true, 'T': true, 'U': true,
|
||||
'V': true, 'W': true, 'X': true, 'Y': true, 'Z': true, '0': true, '1': true, '2': true,
|
||||
'3': true, '4': true, '5': true, '6': true, '7': true, '8': true, '9': true, '!': true,
|
||||
'#': true, '$': true, '%': true, '&': true, '\'': true, '*': true, '+': true, '/': true,
|
||||
'=': true, '?': true, '^': true, '_': true, '`': true, '{': true, '|': true, '}': true,
|
||||
'~': true, '-': true}
|
||||
letterOrDigit = rangetable.Merge(unicode.Letter, unicode.Digit)
|
||||
punctSpaceCc = rangetable.Merge(unicode.Punct, unicode.Space, unicode.Cc)
|
||||
)
|
||||
|
||||
func isAllowedInEmail(r rune) bool {
|
||||
return r < 0x7f && emailcs[r]
|
||||
}
|
||||
|
||||
func isLetterOrDigit(r rune) bool {
|
||||
return unicode.Is(letterOrDigit, r)
|
||||
}
|
||||
|
||||
func isPunctOrSpaceOrControl(r rune) bool {
|
||||
return r == '<' || r == '>' || r == '\uff5c' || unicode.Is(punctSpaceCc, r)
|
||||
}
|
||||
|
||||
func isUnreserved(r rune) bool {
|
||||
return (r < 0x7f && unreserved[r]) || unicode.Is(letterOrDigit, r)
|
||||
}
|
||||
|
||||
func isSubDelimiter(r rune) bool {
|
||||
return r < 0x7f && subdelims[r]
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package linkify
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func findEmailStart(s string, start int) (_ int, _ bool) {
|
||||
end := start
|
||||
allowDot := false
|
||||
for end >= 0 {
|
||||
b := s[end]
|
||||
switch {
|
||||
case emailcs[b]:
|
||||
allowDot = true
|
||||
case b == '.':
|
||||
if !allowDot {
|
||||
return
|
||||
}
|
||||
allowDot = false
|
||||
default:
|
||||
if end == start {
|
||||
return
|
||||
}
|
||||
if s[end+1] == '.' {
|
||||
return
|
||||
}
|
||||
r, _ := utf8.DecodeLastRuneInString(s[:end+1])
|
||||
if r == utf8.RuneError {
|
||||
return
|
||||
}
|
||||
if !unicode.IsSpace(r) {
|
||||
return
|
||||
}
|
||||
return end + 1, true
|
||||
}
|
||||
end--
|
||||
}
|
||||
if end < start && s[end+1] == '.' {
|
||||
return
|
||||
}
|
||||
return end + 1, true
|
||||
}
|
||||
|
||||
func findEmailEnd(s string, start int) (_ int, _ bool) {
|
||||
end := start
|
||||
allowDot := false
|
||||
loop:
|
||||
for end < len(s) {
|
||||
b := s[end]
|
||||
switch {
|
||||
case emailcs[b]:
|
||||
allowDot = true
|
||||
case b == '.':
|
||||
if !allowDot {
|
||||
return
|
||||
}
|
||||
allowDot = false
|
||||
case b == '@':
|
||||
break loop
|
||||
default:
|
||||
return
|
||||
}
|
||||
end++
|
||||
}
|
||||
if end >= len(s)-5 {
|
||||
return
|
||||
}
|
||||
if end > start && s[end-1] == '.' {
|
||||
return
|
||||
}
|
||||
|
||||
var dot int
|
||||
var ok bool
|
||||
end, dot, ok = findHostnameEnd(s, end+1)
|
||||
if !ok || dot == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
if dot+5 <= len(s) && s[dot+1:dot+5] == "xn--" {
|
||||
return end, true
|
||||
}
|
||||
|
||||
if length := match(s[dot+1:]); dot+length+1 != end {
|
||||
return
|
||||
}
|
||||
|
||||
return end, true
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,462 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package linkify provides a way to find links in plain text.
|
||||
package linkify
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Link represents a link found in a string with a schema and a position in the string.
|
||||
type Link struct {
|
||||
Scheme string
|
||||
Start, End int
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a >= b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Links returns links found in s.
|
||||
func Links(s string) (links []Link) {
|
||||
for i := 0; i < len(s)-2; i++ {
|
||||
switch s[i] {
|
||||
case '.': // IP address or domain name
|
||||
if i == 0 {
|
||||
continue // . at the start of a line
|
||||
}
|
||||
if length := match(s[i+1:]); length > 0 {
|
||||
pos := i + 1 + length
|
||||
switch s[pos-1] {
|
||||
case '.': // IP address
|
||||
if pos >= len(s) {
|
||||
continue // . at the end of line
|
||||
}
|
||||
if !digit(s[i-1]) {
|
||||
i = pos
|
||||
continue // . should be preceded by a digit
|
||||
}
|
||||
if !digit(s[pos]) {
|
||||
i = pos
|
||||
continue // . should be followed by a digit
|
||||
}
|
||||
|
||||
// find the start of the IP address
|
||||
j := i - 2
|
||||
m := max(0, j-3)
|
||||
for j >= m && digit(s[j]) {
|
||||
j--
|
||||
}
|
||||
if i-2-j > 2 {
|
||||
i = pos + 1
|
||||
continue // at most 3 digits
|
||||
}
|
||||
start := 0
|
||||
if j >= 0 {
|
||||
r, rlen := utf8.DecodeLastRuneInString(s[:j+1])
|
||||
if !isPunctOrSpaceOrControl(r) {
|
||||
i = pos + 1
|
||||
continue
|
||||
}
|
||||
switch r {
|
||||
case '.', ':', '/', '\\', '-', '_':
|
||||
i = pos + 1
|
||||
continue
|
||||
}
|
||||
start = j + 2 - rlen
|
||||
}
|
||||
|
||||
length, ok := skipIPv4(s[start:])
|
||||
if !ok {
|
||||
i = pos + 1
|
||||
continue
|
||||
}
|
||||
end := start + length
|
||||
if end == len(s) {
|
||||
links = append(links, Link{
|
||||
Scheme: "",
|
||||
Start: start,
|
||||
End: end,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
r, _ := utf8.DecodeRuneInString(s[end:])
|
||||
if !isPunctOrSpaceOrControl(r) {
|
||||
continue
|
||||
}
|
||||
|
||||
end = skipPort(s, end)
|
||||
end = skipPath(s, end)
|
||||
end = skipQuery(s, end)
|
||||
end = skipFragment(s, end)
|
||||
end = unskipPunct(s, end)
|
||||
|
||||
if end < len(s) {
|
||||
r, _ = utf8.DecodeRuneInString(s[end:])
|
||||
if !isPunctOrSpaceOrControl(r) || r == '%' {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
links = append(links, Link{
|
||||
Scheme: "",
|
||||
Start: start,
|
||||
End: end,
|
||||
})
|
||||
i = end
|
||||
|
||||
default: // domain name
|
||||
r, _ := utf8.DecodeLastRuneInString(s[:i])
|
||||
if isPunctOrSpaceOrControl(r) {
|
||||
continue
|
||||
}
|
||||
|
||||
if pos == len(s) {
|
||||
start, ok := findHostnameStart(s, i)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
links = append(links, Link{
|
||||
Scheme: "",
|
||||
Start: start,
|
||||
End: pos,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if s[i+1:pos] != "xn--" {
|
||||
r, _ = utf8.DecodeRuneInString(s[pos:])
|
||||
if isLetterOrDigit(r) {
|
||||
continue // should not be followed by a letter or a digit
|
||||
}
|
||||
}
|
||||
|
||||
end, dot, ok := findHostnameEnd(s, pos)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
dot = max(dot, i)
|
||||
|
||||
if !(dot+5 <= len(s) && s[dot+1:dot+5] == "xn--") {
|
||||
if length := match(s[dot+1:]); dot+length+1 != end {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
start, ok := findHostnameStart(s, i)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
end = skipPort(s, end)
|
||||
end = skipPath(s, end)
|
||||
end = skipQuery(s, end)
|
||||
end = skipFragment(s, end)
|
||||
end = unskipPunct(s, end)
|
||||
|
||||
if end < len(s) {
|
||||
r, _ = utf8.DecodeRuneInString(s[end:])
|
||||
if !isPunctOrSpaceOrControl(r) || r == '%' {
|
||||
continue // should be followed by punctuation or space
|
||||
}
|
||||
}
|
||||
|
||||
links = append(links, Link{
|
||||
Scheme: "",
|
||||
Start: start,
|
||||
End: end,
|
||||
})
|
||||
i = end
|
||||
}
|
||||
}
|
||||
|
||||
case '/': // schema-less link
|
||||
if s[i+1] != '/' {
|
||||
continue
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
if s[i-1] == ':' {
|
||||
i++
|
||||
continue // should not be preceded by a colon
|
||||
}
|
||||
r, _ := utf8.DecodeLastRuneInString(s[:i])
|
||||
if !isPunctOrSpaceOrControl(r) {
|
||||
i++
|
||||
continue // should be preceded by punctuation or space
|
||||
}
|
||||
}
|
||||
|
||||
r, _ := utf8.DecodeRuneInString(s[i+2:])
|
||||
if !isLetterOrDigit(r) {
|
||||
i++
|
||||
continue // should be followed by a letter or a digit
|
||||
}
|
||||
|
||||
start := i
|
||||
end, dot, ok := findHostnameEnd(s, i+2)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if s[i+2:end] != "localhost" {
|
||||
if dot == -1 {
|
||||
continue // no dot
|
||||
}
|
||||
if length, ok := skipIPv4(s[i+2:]); !ok || i+2+length != end {
|
||||
if length := match(s[dot+1:]); dot+length+1 != end {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
end = skipPort(s, end)
|
||||
end = skipPath(s, end)
|
||||
end = skipQuery(s, end)
|
||||
end = skipFragment(s, end)
|
||||
end = unskipPunct(s, end)
|
||||
|
||||
if end < len(s) {
|
||||
r, _ = utf8.DecodeRuneInString(s[end:])
|
||||
if !isPunctOrSpaceOrControl(r) || r == '%' {
|
||||
continue // should be followed by punctuation or space
|
||||
}
|
||||
}
|
||||
|
||||
links = append(links, Link{
|
||||
Scheme: "//",
|
||||
Start: start,
|
||||
End: end,
|
||||
})
|
||||
i = end
|
||||
|
||||
case ':': // http, https, ftp, mailto or localhost
|
||||
if i < 3 { // at least ftp:
|
||||
continue
|
||||
}
|
||||
|
||||
if i >= 9 && s[i-1] == 't' && s[i-9:i] == "localhost" {
|
||||
j := i - 9
|
||||
if !digit(s[j+10]) {
|
||||
continue
|
||||
}
|
||||
if j > 0 {
|
||||
r, _ := utf8.DecodeLastRuneInString(s[:j])
|
||||
if !isPunctOrSpaceOrControl(r) {
|
||||
i++
|
||||
continue // should be preceded by punctuation or space
|
||||
}
|
||||
}
|
||||
|
||||
start := j
|
||||
pos := j + 9
|
||||
end := skipPort(s, pos)
|
||||
if end == pos {
|
||||
continue // invalid port
|
||||
}
|
||||
end = skipPath(s, end)
|
||||
end = skipQuery(s, end)
|
||||
end = skipFragment(s, end)
|
||||
end = unskipPunct(s, end)
|
||||
|
||||
if end < len(s) {
|
||||
r, _ := utf8.DecodeRuneInString(s[end:])
|
||||
if !isPunctOrSpaceOrControl(r) || r == '%' {
|
||||
i++
|
||||
continue // should be followed by punctuation or space
|
||||
}
|
||||
}
|
||||
|
||||
links = append(links, Link{
|
||||
Scheme: "",
|
||||
Start: start,
|
||||
End: end,
|
||||
})
|
||||
i = end
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
j := i - 1
|
||||
var start int
|
||||
var schema string
|
||||
|
||||
switch byteToLower(s[j]) {
|
||||
case 'o': // mailto
|
||||
if j < 5 {
|
||||
continue // too short for mailto
|
||||
}
|
||||
if len(s)-j < 8 {
|
||||
continue // insufficient length after
|
||||
}
|
||||
if strings.ToLower(s[j-5:j+2]) != "mailto:" {
|
||||
continue
|
||||
}
|
||||
r, _ := utf8.DecodeLastRuneInString(s[:j-5])
|
||||
if isLetterOrDigit(r) {
|
||||
continue // should not be preceded by a letter or a digit
|
||||
}
|
||||
r, _ = utf8.DecodeRuneInString(s[j+2:])
|
||||
if !isAllowedInEmail(r) {
|
||||
continue // should be followed by a valid e-mail character
|
||||
}
|
||||
|
||||
start = j - 5
|
||||
end, ok := findEmailEnd(s, j+2)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
links = append(links, Link{
|
||||
Scheme: "mailto:",
|
||||
Start: start,
|
||||
End: end,
|
||||
})
|
||||
i = end
|
||||
continue // continue processing
|
||||
|
||||
case 'p': // http or ftp
|
||||
if len(s)-j < 8 {
|
||||
continue // insufficient length after
|
||||
}
|
||||
switch byteToLower(s[j-2]) {
|
||||
case 'f':
|
||||
if strings.ToLower(s[j-2:j+4]) != "ftp://" {
|
||||
continue
|
||||
}
|
||||
start = j - 2
|
||||
schema = "ftp:"
|
||||
case 't':
|
||||
if j < 3 {
|
||||
continue
|
||||
}
|
||||
if strings.ToLower(s[j-3:j+4]) != "http://" {
|
||||
continue
|
||||
}
|
||||
start = j - 3
|
||||
schema = "http:"
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
case 's': // https
|
||||
if j < 4 {
|
||||
continue // too short for https
|
||||
}
|
||||
if len(s)-j < 8 {
|
||||
continue // insufficient length after
|
||||
}
|
||||
start = j - 4
|
||||
if strings.ToLower(s[start:j+4]) != "https://" {
|
||||
continue
|
||||
}
|
||||
schema = "https:"
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// http, https or ftp
|
||||
|
||||
if start > 0 {
|
||||
r, _ := utf8.DecodeLastRuneInString(s[:start])
|
||||
if !isPunctOrSpaceOrControl(r) {
|
||||
continue // should be preceded by punctuation or space
|
||||
}
|
||||
}
|
||||
|
||||
r, _ := utf8.DecodeRuneInString(s[j+4:])
|
||||
if isPunctOrSpaceOrControl(r) {
|
||||
continue
|
||||
}
|
||||
|
||||
end, dot, ok := findHostnameEnd(s, j+4)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if s[j+4:end] != "localhost" {
|
||||
if dot == -1 {
|
||||
continue // no dot
|
||||
}
|
||||
if length, ok := skipIPv4(s[j+4:]); !ok || j+4+length != end {
|
||||
if !(dot+5 <= len(s) && s[dot+1:dot+5] == "xn--") {
|
||||
if length := match(s[dot+1:]); dot+length+1 != end {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
end = skipPort(s, end)
|
||||
end = skipPath(s, end)
|
||||
end = skipQuery(s, end)
|
||||
end = skipFragment(s, end)
|
||||
end = unskipPunct(s, end)
|
||||
|
||||
if end < len(s) {
|
||||
r, _ = utf8.DecodeRuneInString(s[end:])
|
||||
if !isPunctOrSpaceOrControl(r) || r == '%' {
|
||||
continue // should be followed by punctuation or space
|
||||
}
|
||||
}
|
||||
|
||||
links = append(links, Link{
|
||||
Scheme: schema,
|
||||
Start: start,
|
||||
End: end,
|
||||
})
|
||||
i = end
|
||||
|
||||
case '@': // schema-less e-mail
|
||||
if i == 0 {
|
||||
continue // @ at the start of a line
|
||||
}
|
||||
|
||||
if len(s)-i < 5 {
|
||||
continue // insufficient length after
|
||||
}
|
||||
|
||||
r, _ := utf8.DecodeLastRuneInString(s[:i])
|
||||
if !isAllowedInEmail(r) {
|
||||
continue // should be preceded by a valid e-mail character
|
||||
}
|
||||
|
||||
r, _ = utf8.DecodeRuneInString(s[i+1:])
|
||||
if !isLetterOrDigit(r) {
|
||||
continue // should be followed by a letter or a digit
|
||||
}
|
||||
|
||||
start, ok := findEmailStart(s, i-1)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
end, dot, ok := findHostnameEnd(s, i+1)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if dot == -1 {
|
||||
continue // no dot
|
||||
}
|
||||
if !(dot+5 <= len(s) && s[dot+1:dot+5] == "xn--") {
|
||||
if length := match(s[dot+1:]); dot+length+1 != end {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
links = append(links, Link{
|
||||
Scheme: "mailto:",
|
||||
Start: start,
|
||||
End: end,
|
||||
})
|
||||
i = end
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -1,404 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package linkify
|
||||
|
||||
import "unicode/utf8"
|
||||
|
||||
func atoi3(s string, start int) (int, bool) {
|
||||
n := 0
|
||||
var i int
|
||||
for i = start; i < len(s) && digit(s[i]); i++ {
|
||||
n = n*10 + int(s[i]-'0')
|
||||
if n > 255 {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
if i == start {
|
||||
return 0, false
|
||||
}
|
||||
return i, true
|
||||
}
|
||||
|
||||
func skipIPv4(s string) (_ int, _ bool) {
|
||||
j := 0
|
||||
for i := 0; i < 4; i++ {
|
||||
if j >= len(s) {
|
||||
return
|
||||
}
|
||||
if i > 0 {
|
||||
if s[j] != '.' {
|
||||
return
|
||||
}
|
||||
j++
|
||||
}
|
||||
n, ok := atoi3(s, j)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
j = n
|
||||
}
|
||||
return j, true
|
||||
}
|
||||
|
||||
func atoi5(s string, start int) (int, bool) {
|
||||
n := 0
|
||||
var i int
|
||||
for i = start; i < len(s) && digit(s[i]); i++ {
|
||||
n = n*10 + int(s[i]-'0')
|
||||
if n > 65535 {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
if i == start || n == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return i, true
|
||||
}
|
||||
|
||||
func skipPort(s string, start int) int {
|
||||
if start >= len(s) || s[start] != ':' {
|
||||
return start
|
||||
}
|
||||
end, ok := atoi5(s, start+1)
|
||||
if !ok {
|
||||
return start
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func skipPath(s string, start int) int {
|
||||
if start >= len(s) || s[start] != '/' {
|
||||
return start // skip empty path
|
||||
}
|
||||
var stack []rune
|
||||
var notClosedIndex int
|
||||
var nHyphen int
|
||||
end := start + 1
|
||||
loop:
|
||||
for end < len(s) {
|
||||
r, rlen := utf8.DecodeRuneInString(s[end:])
|
||||
if r == utf8.RuneError {
|
||||
nHyphen = 0
|
||||
break
|
||||
}
|
||||
|
||||
switch {
|
||||
case isUnreserved(r):
|
||||
if r == '-' {
|
||||
nHyphen++
|
||||
if nHyphen > 2 {
|
||||
break loop
|
||||
}
|
||||
} else {
|
||||
nHyphen = 0
|
||||
}
|
||||
case isSubDelimiter(r) || r == '[' || r == ']':
|
||||
nHyphen = 0
|
||||
switch r {
|
||||
case '[', '(':
|
||||
if len(stack) == 0 {
|
||||
notClosedIndex = end
|
||||
}
|
||||
stack = append(stack, r)
|
||||
case ']', ')':
|
||||
opening := '['
|
||||
if r == ')' {
|
||||
opening = '('
|
||||
}
|
||||
if len(stack) == 0 || stack[len(stack)-1] != opening {
|
||||
break loop
|
||||
}
|
||||
stack = stack[:len(stack)-1]
|
||||
}
|
||||
case r == '/' || r == ':' || r == '@':
|
||||
nHyphen = 0
|
||||
case r == '%':
|
||||
nHyphen = 0
|
||||
if end+2 >= len(s) {
|
||||
break loop
|
||||
}
|
||||
if !(hexDigit(s[end+1]) &&
|
||||
hexDigit(s[end+2])) {
|
||||
break loop
|
||||
}
|
||||
end += 2
|
||||
default:
|
||||
nHyphen = 0
|
||||
if r != ' ' || len(stack) == 0 {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
end += rlen
|
||||
}
|
||||
if len(stack) > 0 {
|
||||
return notClosedIndex
|
||||
}
|
||||
if nHyphen > 0 {
|
||||
return end - nHyphen + 1
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func skipQuery(s string, start int) int {
|
||||
if start >= len(s) || s[start] != '?' {
|
||||
return start
|
||||
}
|
||||
var stack []rune
|
||||
var notClosedIndex int
|
||||
var nHyphen int
|
||||
end := start + 1
|
||||
loop:
|
||||
for end < len(s) {
|
||||
r, rlen := utf8.DecodeRuneInString(s[end:])
|
||||
if r == utf8.RuneError {
|
||||
nHyphen = 0
|
||||
break
|
||||
}
|
||||
|
||||
switch {
|
||||
case isUnreserved(r):
|
||||
if r == '-' {
|
||||
nHyphen++
|
||||
if nHyphen > 1 {
|
||||
break loop
|
||||
}
|
||||
} else {
|
||||
nHyphen = 0
|
||||
}
|
||||
case isSubDelimiter(r) || r == '[' || r == ']':
|
||||
nHyphen = 0
|
||||
switch r {
|
||||
case '[', '(':
|
||||
if len(stack) == 0 {
|
||||
notClosedIndex = end
|
||||
}
|
||||
stack = append(stack, r)
|
||||
case ']', ')':
|
||||
opening := '['
|
||||
if r == ')' {
|
||||
opening = '('
|
||||
}
|
||||
if len(stack) == 0 || stack[len(stack)-1] != opening {
|
||||
break loop
|
||||
}
|
||||
stack = stack[:len(stack)-1]
|
||||
}
|
||||
case r == '?' || r == '/' || r == ':' || r == '@':
|
||||
nHyphen = 0
|
||||
case r == '%':
|
||||
nHyphen = 0
|
||||
if end+2 >= len(s) {
|
||||
break loop
|
||||
}
|
||||
if !(hexDigit(s[end+1]) &&
|
||||
hexDigit(s[end+2])) {
|
||||
break loop
|
||||
}
|
||||
end += 2
|
||||
default:
|
||||
nHyphen = 0
|
||||
if r != ' ' || len(stack) == 0 {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
end += rlen
|
||||
}
|
||||
if len(stack) > 0 {
|
||||
return notClosedIndex
|
||||
}
|
||||
if nHyphen > 0 {
|
||||
return end - nHyphen + 1
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func skipFragment(s string, start int) int {
|
||||
if start >= len(s) || s[start] != '#' {
|
||||
return start
|
||||
}
|
||||
var stack []rune
|
||||
var notClosedIndex int
|
||||
var nHyphen int
|
||||
end := start + 1
|
||||
loop:
|
||||
for end < len(s) {
|
||||
r, rlen := utf8.DecodeRuneInString(s[end:])
|
||||
if r == utf8.RuneError {
|
||||
nHyphen = 0
|
||||
break
|
||||
}
|
||||
|
||||
switch {
|
||||
case isUnreserved(r):
|
||||
if r == '-' {
|
||||
nHyphen++
|
||||
if nHyphen > 1 {
|
||||
break loop
|
||||
}
|
||||
} else {
|
||||
nHyphen = 0
|
||||
}
|
||||
case isSubDelimiter(r) || r == '[' || r == ']':
|
||||
nHyphen = 0
|
||||
switch r {
|
||||
case '[', '(':
|
||||
if len(stack) == 0 {
|
||||
notClosedIndex = end
|
||||
}
|
||||
stack = append(stack, r)
|
||||
case ']', ')':
|
||||
opening := '['
|
||||
if r == ')' {
|
||||
opening = '('
|
||||
}
|
||||
if len(stack) == 0 || stack[len(stack)-1] != opening {
|
||||
break loop
|
||||
}
|
||||
stack = stack[:len(stack)-1]
|
||||
}
|
||||
case r == '?' || r == '/' || r == ':' || r == '@':
|
||||
nHyphen = 0
|
||||
case r == '%':
|
||||
nHyphen = 0
|
||||
if end+2 >= len(s) {
|
||||
break loop
|
||||
}
|
||||
if !(hexDigit(s[end+1]) &&
|
||||
hexDigit(s[end+2])) {
|
||||
break loop
|
||||
}
|
||||
end += 2
|
||||
default:
|
||||
nHyphen = 0
|
||||
if r != ' ' || len(stack) == 0 {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
end += rlen
|
||||
}
|
||||
if len(stack) > 0 {
|
||||
return notClosedIndex
|
||||
}
|
||||
if nHyphen > 0 {
|
||||
return end - nHyphen + 1
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func unskipPunct(s string, start int) int {
|
||||
end := start - 1
|
||||
if end < 0 || end >= len(s) || !basicPunct[s[end]] {
|
||||
return start
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func findHostnameStart(s string, start int) (_ int, _ bool) {
|
||||
end := start
|
||||
lastDot := true
|
||||
nHyphen := 0
|
||||
loop:
|
||||
for end > 0 {
|
||||
r, rlen := utf8.DecodeLastRuneInString(s[:end])
|
||||
if r == utf8.RuneError {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case r == '.':
|
||||
if nHyphen > 0 {
|
||||
return
|
||||
}
|
||||
lastDot = true
|
||||
case r == '-':
|
||||
if end == start {
|
||||
return
|
||||
}
|
||||
if lastDot {
|
||||
return
|
||||
}
|
||||
nHyphen++
|
||||
if nHyphen == 3 {
|
||||
return
|
||||
}
|
||||
case r == ':' || r == '/' || r == '\\' || r == '_':
|
||||
return
|
||||
case isPunctOrSpaceOrControl(r):
|
||||
break loop
|
||||
default:
|
||||
lastDot = false
|
||||
nHyphen = 0
|
||||
}
|
||||
end -= rlen
|
||||
}
|
||||
if lastDot || nHyphen > 0 {
|
||||
return
|
||||
}
|
||||
return end, true
|
||||
}
|
||||
|
||||
func findHostnameEnd(s string, start int) (_ int, _ int, _ bool) {
|
||||
end := start
|
||||
lastDot := false
|
||||
lastDotPos := -1
|
||||
nHyphen := 0
|
||||
loop:
|
||||
for end < len(s) {
|
||||
r, rlen := utf8.DecodeRuneInString(s[end:])
|
||||
if r == utf8.RuneError {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case r == '.':
|
||||
if nHyphen > 0 {
|
||||
return
|
||||
}
|
||||
if lastDot {
|
||||
break loop
|
||||
}
|
||||
lastDot = true
|
||||
lastDotPos = end
|
||||
nHyphen = 0
|
||||
case r == '-':
|
||||
lastDot = false
|
||||
if end == start {
|
||||
return
|
||||
}
|
||||
if lastDot {
|
||||
return
|
||||
}
|
||||
nHyphen++
|
||||
if nHyphen == 3 {
|
||||
break loop
|
||||
}
|
||||
case r == '\\' || r == '_':
|
||||
return
|
||||
case isPunctOrSpaceOrControl(r):
|
||||
break loop
|
||||
default:
|
||||
lastDot = false
|
||||
nHyphen = 0
|
||||
}
|
||||
end += rlen
|
||||
}
|
||||
|
||||
if nHyphen > 0 {
|
||||
end -= nHyphen
|
||||
} else if lastDot {
|
||||
if s[end-1] == '.' {
|
||||
end--
|
||||
}
|
||||
lastDotPos = end - 1
|
||||
for lastDotPos >= start && s[lastDotPos] != '.' {
|
||||
lastDotPos--
|
||||
}
|
||||
if lastDotPos < start {
|
||||
lastDotPos = -1
|
||||
}
|
||||
}
|
||||
|
||||
return end, lastDotPos, true
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package linkify
|
||||
|
||||
func digit(b byte) bool {
|
||||
return b >= '0' && b <= '9'
|
||||
}
|
||||
|
||||
func hexDigit(b byte) bool {
|
||||
return digit(b) || b >= 'a' && b <= 'f' || b >= 'A' && b <= 'F'
|
||||
}
|
||||
|
||||
func byteToLower(b byte) byte {
|
||||
if b >= 'A' && b <= 'Z' {
|
||||
return b - 'A' + 'a'
|
||||
}
|
||||
return b
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
image: golang:1.11
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
|
||||
before_script:
|
||||
- go get github.com/russross/blackfriday
|
||||
- go get gitlab.com/golang-commonmark/html
|
||||
- go get gitlab.com/golang-commonmark/linkify
|
||||
- go get gitlab.com/golang-commonmark/mdurl
|
||||
- go get gitlab.com/golang-commonmark/puny
|
||||
- go get gitlab.com/opennota/wd
|
||||
- go get gopkg.in/russross/blackfriday.v2
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- go build ./...
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- test -z "$(gofmt -l . | tee /dev/stderr)"
|
||||
- go test -cover ./...
|
|
@ -1 +0,0 @@
|
|||
opennota@gmail.com
|
|
@ -1,10 +0,0 @@
|
|||
Copyright (c) 2015, The Authors
|
||||
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.
|
|
@ -1,64 +0,0 @@
|
|||
markdown [![GoDoc](http://godoc.org/gitlab.com/golang-commonmark/markdown?status.svg)](http://godoc.org/gitlab.com/golang-commonmark/markdown) [![License](https://img.shields.io/badge/licence-BSD--2--Clause-blue.svg)](https://opensource.org/licenses/BSD-2-Clause) [![Pipeline status](https://gitlab.com/golang-commonmark/markdown/badges/master/pipeline.svg)](https://gitlab.com/golang-commonmark/markdown/commits/master) [![Coverage report](https://gitlab.com/golang-commonmark/markdown/badges/master/coverage.svg)](https://gitlab.com/golang-commonmark/markdown/commits/master)
|
||||
========
|
||||
|
||||
Package golang-commonmark/markdown provides a CommonMark-compliant markdown parser and renderer, written in Go.
|
||||
|
||||
## Installation
|
||||
|
||||
go get -u gitlab.com/golang-commonmark/markdown
|
||||
|
||||
You can also go get [mdtool](https://gitlab.com/golang-commonmark/mdtool), an example command-line tool:
|
||||
|
||||
go get -u gitlab.com/golang-commonmark/mdtool
|
||||
|
||||
## Standards support
|
||||
|
||||
Currently supported CommonMark spec: [v0.28](http://spec.commonmark.org/0.28/).
|
||||
|
||||
## Extensions
|
||||
|
||||
Besides the features required by CommonMark, golang-commonmark/markdown supports:
|
||||
|
||||
* Tables (GFM)
|
||||
* Strikethrough (GFM)
|
||||
* Autoconverting plain-text URLs to links
|
||||
* Typographic replacements (smart quotes and other)
|
||||
|
||||
## Usage
|
||||
|
||||
``` go
|
||||
md := markdown.New(markdown.XHTMLOutput(true))
|
||||
fmt.Println(md.RenderToString([]byte("Header\n===\nText")))
|
||||
```
|
||||
|
||||
Check out [the source of mdtool](https://gitlab.com/golang-commonmark/mdtool/blob/master/main.go) for a more complete example.
|
||||
|
||||
The following options are currently supported:
|
||||
|
||||
Name | Type | Description | Default
|
||||
--------------- | --------- | ----------------------------------------------------------- | ---------
|
||||
HTML | bool | whether to enable raw HTML | false
|
||||
Tables | bool | whether to enable GFM tables | true
|
||||
Linkify | bool | whether to autoconvert plain-text URLs to links | true
|
||||
Typographer | bool | whether to enable typographic replacements | true
|
||||
Quotes | string / []string | double + single quote replacement pairs for the typographer | “”‘’
|
||||
MaxNesting | int | maximum nesting level | 20
|
||||
LangPrefix | string | CSS language prefix for fenced blocks | language-
|
||||
Breaks | bool | whether to convert newlines inside paragraphs into `<br>` | false
|
||||
XHTMLOutput | bool | whether to output XHTML instead of HTML | false
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Rendering spec/spec-0.28.txt on a Intel(R) Core(TM) i5-2400 CPU @ 3.10GHz
|
||||
|
||||
BenchmarkRenderSpecNoHTML 100 10254720 ns/op 2998037 B/op 18225 allocs/op
|
||||
BenchmarkRenderSpec 100 10180241 ns/op 2997307 B/op 18214 allocs/op
|
||||
BenchmarkRenderSpecBlackFriday 200 7241749 ns/op 2834340 B/op 17101 allocs/op
|
||||
BenchmarkRenderSpecBlackFriday2 200 7448256 ns/op 2991202 B/op 16705 allocs/op
|
||||
|
||||
## See also
|
||||
|
||||
https://github.com/jgm/CommonMark — the reference CommonMark implementations in C and JavaScript,
|
||||
also contains the latest spec and an online demo.
|
||||
|
||||
http://talk.commonmark.org — the CommonMark forum, a good place to join together the efforts of the developers.
|
|
@ -1,26 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
type Align byte
|
||||
|
||||
const (
|
||||
AlignNone = iota
|
||||
AlignLeft
|
||||
AlignCenter
|
||||
AlignRight
|
||||
)
|
||||
|
||||
func (a Align) String() string {
|
||||
switch a {
|
||||
case AlignLeft:
|
||||
return "left"
|
||||
case AlignCenter:
|
||||
return "center"
|
||||
case AlignRight:
|
||||
return "right"
|
||||
}
|
||||
return ""
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
rAutolink = regexp.MustCompile(`^<([a-zA-Z][a-zA-Z0-9+.\-]{1,31}):([^<>\x00-\x20]*)>`)
|
||||
rEmail = regexp.MustCompile(`^<([a-zA-Z0-9.!#$%&'*+/=?^_{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>`)
|
||||
)
|
||||
|
||||
func ruleAutolink(s *StateInline, silent bool) bool {
|
||||
pos := s.Pos
|
||||
src := s.Src
|
||||
|
||||
if src[pos] != '<' {
|
||||
return false
|
||||
}
|
||||
|
||||
tail := src[pos:]
|
||||
|
||||
if strings.IndexByte(tail, '>') < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
link := rAutolink.FindString(tail)
|
||||
if link != "" {
|
||||
link = link[1 : len(link)-1]
|
||||
href := normalizeLink(link)
|
||||
if !validateLink(href) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !silent {
|
||||
s.PushOpeningToken(&LinkOpen{Href: href})
|
||||
s.PushToken(&Text{Content: normalizeLinkText(link)})
|
||||
s.PushClosingToken(&LinkClose{})
|
||||
}
|
||||
|
||||
s.Pos += len(link) + 2
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
email := rEmail.FindString(tail)
|
||||
if email != "" {
|
||||
email = email[1 : len(email)-1]
|
||||
href := normalizeLink("mailto:" + email)
|
||||
if !validateLink(href) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !silent {
|
||||
s.PushOpeningToken(&LinkOpen{Href: href})
|
||||
s.PushToken(&Text{Content: normalizeLinkText(email)})
|
||||
s.PushClosingToken(&LinkClose{})
|
||||
}
|
||||
|
||||
s.Pos += len(email) + 2
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "strings"
|
||||
|
||||
func ruleBackticks(s *StateInline, silent bool) bool {
|
||||
pos := s.Pos
|
||||
src := s.Src
|
||||
|
||||
if src[pos] != '`' {
|
||||
return false
|
||||
}
|
||||
|
||||
start := pos
|
||||
pos++
|
||||
max := s.PosMax
|
||||
|
||||
for pos < max && src[pos] == '`' {
|
||||
pos++
|
||||
}
|
||||
|
||||
marker := src[start:pos]
|
||||
|
||||
matchStart := pos
|
||||
matchEnd := pos
|
||||
for {
|
||||
matchStart = strings.IndexByte(src[matchEnd:], '`')
|
||||
if matchStart == -1 {
|
||||
break
|
||||
}
|
||||
matchStart += matchEnd
|
||||
|
||||
matchEnd = matchStart + 1
|
||||
|
||||
for matchEnd < max && src[matchEnd] == '`' {
|
||||
matchEnd++
|
||||
}
|
||||
|
||||
if matchEnd-matchStart == len(marker) {
|
||||
if !silent {
|
||||
s.PushToken(&CodeInline{
|
||||
Content: normalizeInlineCode(src[pos:matchStart]),
|
||||
})
|
||||
}
|
||||
s.Pos = matchEnd
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if !silent {
|
||||
s.Pending.WriteString(marker)
|
||||
}
|
||||
|
||||
s.Pos += len(marker)
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
func ruleBalancePairs(s *StateInline) {
|
||||
delimiters := s.Delimiters
|
||||
max := len(delimiters)
|
||||
|
||||
for i := 0; i < max; i++ {
|
||||
lastDelim := delimiters[i]
|
||||
|
||||
if !lastDelim.Close {
|
||||
continue
|
||||
}
|
||||
|
||||
j := i - lastDelim.Jump - 1
|
||||
|
||||
for j >= 0 {
|
||||
currDelim := delimiters[j]
|
||||
|
||||
if currDelim.Open &&
|
||||
currDelim.Marker == lastDelim.Marker &&
|
||||
currDelim.End < 0 &&
|
||||
currDelim.Level == lastDelim.Level {
|
||||
oddMatch := (currDelim.Close || lastDelim.Open) &&
|
||||
currDelim.Length != -1 &&
|
||||
lastDelim.Length != -1 &&
|
||||
(currDelim.Length+lastDelim.Length)%3 == 0
|
||||
if !oddMatch {
|
||||
delimiters[i].Jump = i - j
|
||||
delimiters[i].Open = false
|
||||
delimiters[j].End = i
|
||||
delimiters[j].Jump = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
j -= currDelim.Jump + 1
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,233 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "unicode/utf8"
|
||||
|
||||
var blockquoteTerminatedBy []BlockRule
|
||||
|
||||
func ruleBlockQuote(s *StateBlock, startLine, endLine int, silent bool) bool {
|
||||
if s.SCount[startLine]-s.BlkIndent >= 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
pos := s.BMarks[startLine] + s.TShift[startLine]
|
||||
max := s.EMarks[startLine]
|
||||
src := s.Src
|
||||
|
||||
if pos >= max {
|
||||
return false
|
||||
}
|
||||
if src[pos] != '>' {
|
||||
return false
|
||||
}
|
||||
pos++
|
||||
|
||||
if silent {
|
||||
return true
|
||||
}
|
||||
|
||||
initial := s.SCount[startLine] + pos - (s.BMarks[startLine] + s.TShift[startLine])
|
||||
offset := initial
|
||||
|
||||
spaceAfterMarker := false
|
||||
adjustTab := false
|
||||
if pos < max {
|
||||
if src[pos] == ' ' {
|
||||
pos++
|
||||
initial++
|
||||
offset++
|
||||
spaceAfterMarker = true
|
||||
} else if src[pos] == '\t' {
|
||||
spaceAfterMarker = true
|
||||
if (s.BSCount[startLine]+offset)%4 == 3 {
|
||||
pos++
|
||||
initial++
|
||||
offset++
|
||||
} else {
|
||||
adjustTab = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oldBMarks := []int{s.BMarks[startLine]}
|
||||
s.BMarks[startLine] = pos
|
||||
|
||||
for pos < max {
|
||||
r, size := utf8.DecodeRuneInString(src[pos:])
|
||||
if runeIsSpace(r) {
|
||||
if r == '\t' {
|
||||
d := 0
|
||||
if adjustTab {
|
||||
d = 1
|
||||
}
|
||||
offset += 4 - (offset+s.BSCount[startLine]+d)%4
|
||||
} else {
|
||||
offset++
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
pos += size
|
||||
}
|
||||
|
||||
oldBSCount := []int{s.BSCount[startLine]}
|
||||
d := 0
|
||||
if spaceAfterMarker {
|
||||
d = 1
|
||||
}
|
||||
s.BSCount[startLine] = s.SCount[startLine] + 1 + d
|
||||
|
||||
lastLineEmpty := pos >= max
|
||||
|
||||
oldSCount := []int{s.SCount[startLine]}
|
||||
s.SCount[startLine] = offset - initial
|
||||
|
||||
oldTShift := []int{s.TShift[startLine]}
|
||||
s.TShift[startLine] = pos - s.BMarks[startLine]
|
||||
|
||||
oldParentType := s.ParentType
|
||||
s.ParentType = ptBlockQuote
|
||||
wasOutdented := false
|
||||
|
||||
oldLineMax := s.LineMax
|
||||
|
||||
nextLine := startLine + 1
|
||||
for ; nextLine < endLine; nextLine++ {
|
||||
if s.SCount[nextLine] < s.BlkIndent {
|
||||
wasOutdented = true
|
||||
}
|
||||
pos = s.BMarks[nextLine] + s.TShift[nextLine]
|
||||
max = s.EMarks[nextLine]
|
||||
|
||||
if pos >= max {
|
||||
break
|
||||
}
|
||||
|
||||
pos++
|
||||
if src[pos-1] == '>' && !wasOutdented {
|
||||
initial = s.SCount[nextLine] + pos + (s.BMarks[nextLine] + s.TShift[nextLine])
|
||||
offset = initial
|
||||
|
||||
if pos >= len(src) || src[pos] != ' ' && src[pos] != '\t' {
|
||||
spaceAfterMarker = true
|
||||
} else if src[pos] == ' ' {
|
||||
pos++
|
||||
initial++
|
||||
offset++
|
||||
adjustTab = false
|
||||
spaceAfterMarker = true
|
||||
} else if src[pos] == '\t' {
|
||||
spaceAfterMarker = true
|
||||
|
||||
if (s.BSCount[nextLine]+offset)%4 == 3 {
|
||||
pos++
|
||||
initial++
|
||||
offset++
|
||||
adjustTab = false
|
||||
} else {
|
||||
adjustTab = true
|
||||
}
|
||||
}
|
||||
|
||||
oldBMarks = append(oldBMarks, s.BMarks[nextLine])
|
||||
s.BMarks[nextLine] = pos
|
||||
|
||||
for pos < max {
|
||||
r, size := utf8.DecodeRuneInString(src[pos:])
|
||||
if runeIsSpace(r) {
|
||||
if r == '\t' {
|
||||
d := 0
|
||||
if adjustTab {
|
||||
d = 1
|
||||
}
|
||||
offset += 4 - (offset+s.BSCount[startLine]+d)%4
|
||||
} else {
|
||||
offset++
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
pos += size
|
||||
}
|
||||
|
||||
lastLineEmpty = pos >= max
|
||||
|
||||
oldBSCount = append(oldBSCount, s.BSCount[nextLine])
|
||||
d := 0
|
||||
if spaceAfterMarker {
|
||||
d = 1
|
||||
}
|
||||
s.BSCount[nextLine] = s.SCount[nextLine] + 1 + d
|
||||
|
||||
oldSCount = append(oldSCount, s.SCount[nextLine])
|
||||
s.SCount[nextLine] = offset - initial
|
||||
|
||||
oldTShift = append(oldTShift, s.TShift[nextLine])
|
||||
s.TShift[nextLine] = pos - s.BMarks[nextLine]
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if lastLineEmpty {
|
||||
break
|
||||
}
|
||||
|
||||
terminate := false
|
||||
for _, r := range blockquoteTerminatedBy {
|
||||
if r(s, nextLine, endLine, true) {
|
||||
terminate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if terminate {
|
||||
s.LineMax = nextLine
|
||||
|
||||
if s.BlkIndent != 0 {
|
||||
oldBMarks = append(oldBMarks, s.BMarks[nextLine])
|
||||
oldBSCount = append(oldBSCount, s.BSCount[nextLine])
|
||||
oldTShift = append(oldTShift, s.TShift[nextLine])
|
||||
oldSCount = append(oldSCount, s.SCount[nextLine])
|
||||
s.SCount[nextLine] -= s.BlkIndent
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
oldBMarks = append(oldBMarks, s.BMarks[nextLine])
|
||||
oldBSCount = append(oldBSCount, s.BSCount[nextLine])
|
||||
oldTShift = append(oldTShift, s.TShift[nextLine])
|
||||
oldSCount = append(oldSCount, s.SCount[nextLine])
|
||||
|
||||
s.SCount[nextLine] = -1
|
||||
}
|
||||
|
||||
oldIndent := s.BlkIndent
|
||||
s.BlkIndent = 0
|
||||
|
||||
tok := &BlockquoteOpen{
|
||||
Map: [2]int{startLine, 0},
|
||||
}
|
||||
s.PushOpeningToken(tok)
|
||||
|
||||
s.Md.Block.Tokenize(s, startLine, nextLine)
|
||||
|
||||
s.PushClosingToken(&BlockquoteClose{})
|
||||
|
||||
s.LineMax = oldLineMax
|
||||
s.ParentType = oldParentType
|
||||
tok.Map[1] = s.Line
|
||||
|
||||
for i := 0; i < len(oldTShift); i++ {
|
||||
s.BMarks[startLine+i] = oldBMarks[i]
|
||||
s.TShift[startLine+i] = oldTShift[i]
|
||||
s.SCount[startLine+i] = oldSCount[i]
|
||||
s.BSCount[startLine+i] = oldBSCount[i]
|
||||
}
|
||||
s.BlkIndent = oldIndent
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
func ruleCode(s *StateBlock, startLine, endLine int, _ bool) bool {
|
||||
if s.SCount[startLine]-s.BlkIndent < 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
nextLine := startLine + 1
|
||||
last := nextLine
|
||||
|
||||
for nextLine < endLine {
|
||||
if s.IsLineEmpty(nextLine) {
|
||||
nextLine++
|
||||
continue
|
||||
}
|
||||
|
||||
if s.SCount[nextLine]-s.BlkIndent >= 4 {
|
||||
nextLine++
|
||||
last = nextLine
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
s.Line = last
|
||||
s.PushToken(&CodeBlock{
|
||||
Content: s.Lines(startLine, last, 4+s.BlkIndent, true),
|
||||
Map: [2]int{startLine, s.Line},
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
type Delimiter struct {
|
||||
Length int
|
||||
Jump int
|
||||
Token int
|
||||
Level int
|
||||
End int
|
||||
Open bool
|
||||
Close bool
|
||||
Marker byte
|
||||
}
|
||||
|
||||
func ruleEmphasis(s *StateInline, silent bool) bool {
|
||||
src := s.Src
|
||||
start := s.Pos
|
||||
marker := src[start]
|
||||
|
||||
if silent {
|
||||
return false
|
||||
}
|
||||
|
||||
if marker != '_' && marker != '*' {
|
||||
return false
|
||||
}
|
||||
|
||||
canOpen, canClose, length := s.scanDelims(s.Pos, marker == '*')
|
||||
for i := 0; i < length; i++ {
|
||||
s.PushToken(&Text{Content: string(marker)})
|
||||
|
||||
s.Delimiters = append(s.Delimiters, Delimiter{
|
||||
Marker: marker,
|
||||
Length: length,
|
||||
Jump: i,
|
||||
Token: len(s.Tokens) - 1,
|
||||
Level: s.Level,
|
||||
End: -1,
|
||||
Open: canOpen,
|
||||
Close: canClose,
|
||||
})
|
||||
}
|
||||
|
||||
s.Pos += length
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func ruleEmphasisPostprocess(s *StateInline) {
|
||||
delimiters := s.Delimiters
|
||||
max := len(delimiters)
|
||||
for i := max - 1; i >= 0; i-- {
|
||||
startDelim := delimiters[i]
|
||||
if startDelim.Marker != '_' && startDelim.Marker != '*' {
|
||||
continue
|
||||
}
|
||||
|
||||
if startDelim.End == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
endDelim := delimiters[startDelim.End]
|
||||
|
||||
isStrong := i > 0 &&
|
||||
delimiters[i-1].End == startDelim.End+1 &&
|
||||
delimiters[i-1].Token == startDelim.Token-1 &&
|
||||
delimiters[startDelim.End+1].Token == endDelim.Token+1 &&
|
||||
delimiters[i-1].Marker == startDelim.Marker
|
||||
|
||||
if isStrong {
|
||||
s.Tokens[startDelim.Token] = &StrongOpen{}
|
||||
s.Tokens[endDelim.Token] = &StrongClose{}
|
||||
|
||||
if text, ok := s.Tokens[delimiters[i-1].Token].(*Text); ok {
|
||||
text.Content = ""
|
||||
}
|
||||
if text, ok := s.Tokens[delimiters[startDelim.End+1].Token].(*Text); ok {
|
||||
text.Content = ""
|
||||
}
|
||||
i--
|
||||
} else {
|
||||
s.Tokens[startDelim.Token] = &EmphasisOpen{}
|
||||
s.Tokens[endDelim.Token] = &EmphasisClose{}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "gitlab.com/golang-commonmark/html"
|
||||
|
||||
func ruleEntity(s *StateInline, silent bool) bool {
|
||||
pos := s.Pos
|
||||
src := s.Src
|
||||
|
||||
if src[pos] != '&' {
|
||||
return false
|
||||
}
|
||||
|
||||
max := s.PosMax
|
||||
|
||||
if pos+1 < max {
|
||||
if e, n := html.ParseEntity(src[pos:]); n > 0 {
|
||||
if !silent {
|
||||
s.Pending.WriteString(e)
|
||||
}
|
||||
s.Pos += n
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if !silent {
|
||||
s.Pending.WriteByte('&')
|
||||
}
|
||||
s.Pos++
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "strings"
|
||||
|
||||
func escaped(b byte) bool {
|
||||
return strings.IndexByte("\\!\"#$%&'()*+,./:;<=>?@[]^_`{|}~-", b) != -1
|
||||
}
|
||||
|
||||
func ruleEscape(s *StateInline, silent bool) bool {
|
||||
pos := s.Pos
|
||||
src := s.Src
|
||||
|
||||
if src[pos] != '\\' {
|
||||
return false
|
||||
}
|
||||
|
||||
pos++
|
||||
max := s.PosMax
|
||||
|
||||
if pos < max {
|
||||
b := src[pos]
|
||||
|
||||
if b < 0x7f && escaped(b) {
|
||||
if !silent {
|
||||
s.Pending.WriteByte(b)
|
||||
}
|
||||
s.Pos += 2
|
||||
return true
|
||||
}
|
||||
|
||||
if b == '\n' {
|
||||
if !silent {
|
||||
s.PushToken(&Hardbreak{})
|
||||
}
|
||||
|
||||
pos++
|
||||
|
||||
for pos < max {
|
||||
b := src[pos]
|
||||
if !byteIsSpace(b) {
|
||||
break
|
||||
}
|
||||
pos++
|
||||
}
|
||||
|
||||
s.Pos = pos
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if !silent {
|
||||
s.Pending.WriteByte('\\')
|
||||
}
|
||||
s.Pos++
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "strings"
|
||||
|
||||
func ruleFence(s *StateBlock, startLine, endLine int, silent bool) bool {
|
||||
haveEndMarker := false
|
||||
pos := s.BMarks[startLine] + s.TShift[startLine]
|
||||
max := s.EMarks[startLine]
|
||||
|
||||
if s.SCount[startLine]-s.BlkIndent >= 4 {
|
||||
return false
|
||||
}
|
||||
if pos+3 > max {
|
||||
return false
|
||||
}
|
||||
|
||||
src := s.Src
|
||||
|
||||
marker := src[pos]
|
||||
if marker != '~' && marker != '`' {
|
||||
return false
|
||||
}
|
||||
|
||||
mem := pos
|
||||
pos = s.SkipBytes(pos, marker)
|
||||
|
||||
len := pos - mem
|
||||
|
||||
if len < 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
params := strings.TrimSpace(src[pos:max])
|
||||
|
||||
if strings.IndexByte(params, marker) >= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if silent {
|
||||
return true
|
||||
}
|
||||
|
||||
nextLine := startLine
|
||||
|
||||
for {
|
||||
nextLine++
|
||||
if nextLine >= endLine {
|
||||
break
|
||||
}
|
||||
|
||||
mem = s.BMarks[nextLine] + s.TShift[nextLine]
|
||||
pos = mem
|
||||
max = s.EMarks[nextLine]
|
||||
|
||||
if pos < max && s.SCount[nextLine] < s.BlkIndent {
|
||||
break
|
||||
}
|
||||
|
||||
if pos >= max || src[pos] != marker {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.SCount[nextLine]-s.BlkIndent >= 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
pos = s.SkipBytes(pos, marker)
|
||||
|
||||
if pos-mem < len {
|
||||
continue
|
||||
}
|
||||
|
||||
pos = s.SkipSpaces(pos)
|
||||
|
||||
if pos < max {
|
||||
continue
|
||||
}
|
||||
|
||||
haveEndMarker = true
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
len = s.SCount[startLine]
|
||||
|
||||
s.Line = nextLine
|
||||
if haveEndMarker {
|
||||
s.Line++
|
||||
}
|
||||
|
||||
s.PushToken(&Fence{
|
||||
Params: params,
|
||||
Content: s.Lines(startLine+1, nextLine, len, true),
|
||||
Map: [2]int{startLine, s.Line},
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
//+build gofuzz
|
||||
|
||||
package markdown
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
md := New(HTML(true))
|
||||
md.Parse(data)
|
||||
return 1
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "strings"
|
||||
|
||||
func ruleHeading(s *StateBlock, startLine, _ int, silent bool) bool {
|
||||
pos := s.BMarks[startLine] + s.TShift[startLine]
|
||||
max := s.EMarks[startLine]
|
||||
src := s.Src
|
||||
|
||||
if s.SCount[startLine]-s.BlkIndent >= 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
if pos >= max || src[pos] != '#' {
|
||||
return false
|
||||
}
|
||||
|
||||
level := 1
|
||||
pos++
|
||||
for pos < max && src[pos] == '#' && level <= 6 {
|
||||
level++
|
||||
pos++
|
||||
}
|
||||
|
||||
if level > 6 || (pos < max && !byteIsSpace(src[pos])) {
|
||||
return false
|
||||
}
|
||||
|
||||
if silent {
|
||||
return true
|
||||
}
|
||||
|
||||
max = s.SkipSpacesBack(max, pos)
|
||||
tmp := s.SkipBytesBack(max, '#', pos)
|
||||
if tmp > pos && byteIsSpace(src[tmp-1]) {
|
||||
max = tmp
|
||||
}
|
||||
|
||||
s.Line = startLine + 1
|
||||
|
||||
s.PushOpeningToken(&HeadingOpen{
|
||||
HLevel: level,
|
||||
Map: [2]int{startLine, s.Line},
|
||||
})
|
||||
|
||||
if pos < max {
|
||||
s.PushToken(&Inline{
|
||||
Content: strings.TrimSpace(src[pos:max]),
|
||||
Map: [2]int{startLine, s.Line},
|
||||
})
|
||||
}
|
||||
s.PushClosingToken(&HeadingClose{HLevel: level})
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
func parseLinkLabel(s *StateInline, start int, disableNested bool) int {
|
||||
src := s.Src
|
||||
labelEnd := -1
|
||||
max := s.PosMax
|
||||
oldPos := s.Pos
|
||||
|
||||
s.Pos = start + 1
|
||||
level := 1
|
||||
found := false
|
||||
|
||||
for s.Pos < max {
|
||||
marker := src[s.Pos]
|
||||
|
||||
if marker == ']' {
|
||||
level--
|
||||
if level == 0 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
prevPos := s.Pos
|
||||
|
||||
s.Md.Inline.SkipToken(s)
|
||||
|
||||
if marker == '[' {
|
||||
if prevPos == s.Pos-1 {
|
||||
level++
|
||||
} else if disableNested {
|
||||
s.Pos = oldPos
|
||||
return -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
labelEnd = s.Pos
|
||||
}
|
||||
|
||||
s.Pos = oldPos
|
||||
|
||||
return labelEnd
|
||||
}
|
||||
|
||||
func parseLinkDestination(s string, pos, max int) (url string, lines, endpos int, ok bool) {
|
||||
start := pos
|
||||
if pos < max && s[pos] == '<' {
|
||||
pos++
|
||||
for pos < max {
|
||||
b := s[pos]
|
||||
if b == '\n' || byteIsSpace(b) {
|
||||
return
|
||||
}
|
||||
if b == '>' {
|
||||
endpos = pos + 1
|
||||
url = unescapeAll(s[start+1 : pos])
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
if b == '\\' && pos+1 < max {
|
||||
pos += 2
|
||||
continue
|
||||
}
|
||||
|
||||
pos++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
level := 0
|
||||
for pos < max {
|
||||
b := s[pos]
|
||||
|
||||
if b == ' ' {
|
||||
break
|
||||
}
|
||||
|
||||
if b < 0x20 || b == 0x7f {
|
||||
break
|
||||
}
|
||||
|
||||
if b == '\\' && pos+1 < max {
|
||||
pos += 2
|
||||
continue
|
||||
}
|
||||
|
||||
if b == '(' {
|
||||
level++
|
||||
}
|
||||
|
||||
if b == ')' {
|
||||
if level == 0 {
|
||||
break
|
||||
}
|
||||
level--
|
||||
}
|
||||
|
||||
pos++
|
||||
}
|
||||
|
||||
if start == pos {
|
||||
return
|
||||
}
|
||||
if level != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
url = unescapeAll(s[start:pos])
|
||||
endpos = pos
|
||||
ok = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func parseLinkTitle(s string, pos, max int) (title string, nlines, endpos int, ok bool) {
|
||||
lines := 0
|
||||
start := pos
|
||||
|
||||
if pos >= max {
|
||||
return
|
||||
}
|
||||
|
||||
marker := s[pos]
|
||||
|
||||
if marker != '"' && marker != '\'' && marker != '(' {
|
||||
return
|
||||
}
|
||||
|
||||
pos++
|
||||
|
||||
if marker == '(' {
|
||||
marker = ')'
|
||||
}
|
||||
|
||||
for pos < max {
|
||||
switch s[pos] {
|
||||
case marker:
|
||||
endpos = pos + 1
|
||||
nlines = lines
|
||||
title = unescapeAll(s[start+1 : pos])
|
||||
ok = true
|
||||
return
|
||||
case '\n':
|
||||
lines++
|
||||
case '\\':
|
||||
if pos+1 < max {
|
||||
pos++
|
||||
if s[pos] == '\n' {
|
||||
lines++
|
||||
}
|
||||
}
|
||||
}
|
||||
pos++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
func ruleHR(s *StateBlock, startLine, endLine int, silent bool) bool {
|
||||
pos := s.BMarks[startLine] + s.TShift[startLine]
|
||||
max := s.EMarks[startLine]
|
||||
|
||||
if pos >= max {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.SCount[startLine]-s.BlkIndent >= 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
src := s.Src
|
||||
marker := src[pos]
|
||||
pos++
|
||||
|
||||
if marker != '*' && marker != '-' && marker != '_' {
|
||||
return false
|
||||
}
|
||||
|
||||
cnt := 1
|
||||
for pos < max {
|
||||
ch := src[pos]
|
||||
pos++
|
||||
if ch != marker && !byteIsSpace(ch) {
|
||||
return false
|
||||
}
|
||||
if ch == marker {
|
||||
cnt++
|
||||
}
|
||||
}
|
||||
|
||||
if cnt < 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
if silent {
|
||||
return true
|
||||
}
|
||||
|
||||
s.Line = startLine + 1
|
||||
|
||||
s.PushToken(&Hr{
|
||||
Map: [2]int{startLine, s.Line},
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,222 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
htmlBlocks = []string{
|
||||
"address",
|
||||
"article",
|
||||
"aside",
|
||||
"base",
|
||||
"basefont",
|
||||
"blockquote",
|
||||
"body",
|
||||
"caption",
|
||||
"center",
|
||||
"col",
|
||||
"colgroup",
|
||||
"dd",
|
||||
"details",
|
||||
"dialog",
|
||||
"dir",
|
||||
"div",
|
||||
"dl",
|
||||
"dt",
|
||||
"fieldset",
|
||||
"figcaption",
|
||||
"figure",
|
||||
"footer",
|
||||
"form",
|
||||
"frame",
|
||||
"frameset",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"head",
|
||||
"header",
|
||||
"hr",
|
||||
"html",
|
||||
"iframe",
|
||||
"legend",
|
||||
"li",
|
||||
"link",
|
||||
"main",
|
||||
"menu",
|
||||
"menuitem",
|
||||
"meta",
|
||||
"nav",
|
||||
"noframes",
|
||||
"ol",
|
||||
"optgroup",
|
||||
"option",
|
||||
"p",
|
||||
"param",
|
||||
"section",
|
||||
"source",
|
||||
"summary",
|
||||
"table",
|
||||
"tbody",
|
||||
"td",
|
||||
"tfoot",
|
||||
"th",
|
||||
"thead",
|
||||
"title",
|
||||
"tr",
|
||||
"track",
|
||||
"ul",
|
||||
}
|
||||
|
||||
htmlBlocksSet = make(map[string]bool)
|
||||
|
||||
rStartCond1 = regexp.MustCompile(`(?i)^(pre|script|style)([\n\t >]|$)`)
|
||||
rEndCond1 = regexp.MustCompile(`(?i)</(pre|script|style)>`)
|
||||
rStartCond6 = regexp.MustCompile(`(?i)^/?(` + strings.Join(htmlBlocks, "|") + `)(\s|$|>|/>)`)
|
||||
rStartCond7 = regexp.MustCompile(`(?i)^(/[a-z][a-z0-9-]*|[a-z][a-z0-9-]*(\s+[a-z_:][a-z0-9_.:-]*\s*=\s*("[^"]*"|'[^']*'|[ "'=<>\x60]))*\s*/?)>\s*$`)
|
||||
)
|
||||
|
||||
func init() {
|
||||
for _, tag := range htmlBlocks {
|
||||
htmlBlocksSet[tag] = true
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func matchTagName(s string) string {
|
||||
if len(s) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
i := 0
|
||||
if s[0] == '/' {
|
||||
i++
|
||||
}
|
||||
start := i
|
||||
max := min(15+i, len(s))
|
||||
for i < max && isLetter(s[i]) {
|
||||
i++
|
||||
}
|
||||
if i >= len(s) {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch s[i] {
|
||||
case ' ', '\n', '/', '>':
|
||||
return strings.ToLower(s[start:i])
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func ruleHTMLBlock(s *StateBlock, startLine, endLine int, silent bool) bool {
|
||||
if !s.Md.HTML {
|
||||
return false
|
||||
}
|
||||
|
||||
pos := s.BMarks[startLine] + s.TShift[startLine]
|
||||
max := s.EMarks[startLine]
|
||||
|
||||
if pos+1 >= max {
|
||||
return false
|
||||
}
|
||||
|
||||
src := s.Src
|
||||
|
||||
if src[pos] != '<' {
|
||||
return false
|
||||
}
|
||||
|
||||
pos++
|
||||
b := src[pos]
|
||||
if !htmlSecond(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
nextLine := startLine + 1
|
||||
|
||||
var endCond func(string) bool
|
||||
|
||||
if pos+2 < max && isLetter(b) && rStartCond1.MatchString(src[pos:]) {
|
||||
endCond = func(s string) bool {
|
||||
return rEndCond1.MatchString(s)
|
||||
}
|
||||
} else if strings.HasPrefix(src[pos:], "!--") {
|
||||
endCond = func(s string) bool {
|
||||
return strings.Contains(s, "-->")
|
||||
}
|
||||
} else if b == '?' {
|
||||
endCond = func(s string) bool {
|
||||
return strings.Contains(s, "?>")
|
||||
}
|
||||
} else if b == '!' && pos+1 < max && isUppercaseLetter(src[pos+1]) {
|
||||
endCond = func(s string) bool {
|
||||
return strings.Contains(s, ">")
|
||||
}
|
||||
} else if strings.HasPrefix(src[pos:], "![CDATA[") {
|
||||
endCond = func(s string) bool {
|
||||
return strings.Contains(s, "]]>")
|
||||
}
|
||||
} else if pos+2 < max && (isLetter(b) || b == '/' && isLetter(src[pos+1])) {
|
||||
terminator := true
|
||||
if rStartCond6.MatchString(src[pos:max]) {
|
||||
} else if rStartCond7.MatchString(src[pos:max]) {
|
||||
terminator = false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
if silent {
|
||||
return terminator
|
||||
}
|
||||
endCond = func(s string) bool {
|
||||
return s == ""
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
if silent {
|
||||
return true
|
||||
}
|
||||
|
||||
if !endCond(src[pos:max]) {
|
||||
for nextLine < endLine {
|
||||
if s.SCount[nextLine] < s.BlkIndent {
|
||||
break
|
||||
}
|
||||
|
||||
pos := s.BMarks[nextLine] + s.TShift[nextLine]
|
||||
max := s.EMarks[nextLine]
|
||||
lineText := src[pos:max]
|
||||
if endCond(lineText) {
|
||||
if pos != max {
|
||||
nextLine++
|
||||
}
|
||||
break
|
||||
}
|
||||
nextLine++
|
||||
}
|
||||
}
|
||||
|
||||
s.Line = nextLine
|
||||
s.PushToken(&HTMLBlock{
|
||||
Content: s.Lines(startLine, nextLine, s.BlkIndent, true),
|
||||
Map: [2]int{startLine, nextLine},
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "regexp"
|
||||
|
||||
var (
|
||||
attrName = `[a-zA-Z_:][a-zA-Z0-9:._-]*`
|
||||
unquoted = "[^\"'=<>`\\x00-\\x20]+"
|
||||
singleQuoted = `'[^']*'`
|
||||
doubleQuoted = `"[^"]*"`
|
||||
attrValue = `(?:` + unquoted + `|` + singleQuoted + `|` + doubleQuoted + `)`
|
||||
attribute = `(?:\s+` + attrName + `(?:\s*=\s*` + attrValue + `)?)`
|
||||
openTag = `<[A-Za-z][A-Za-z0-9-]*` + attribute + `*\s*/?>`
|
||||
closeTag = `</[A-Za-z][A-Za-z0-9-]*\s*>`
|
||||
comment = `<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->`
|
||||
processing = `<[?].*?[?]>`
|
||||
declaration = `<![A-Z]+\s+[^>]*>`
|
||||
cdata = `<!\[CDATA\[[\s\S]*?\]\]>`
|
||||
rHTMLTag = regexp.MustCompile(`^(?:` + openTag + `|` + closeTag + `|` + comment +
|
||||
`|` + processing + `|` + declaration + `|` + cdata + `)`)
|
||||
)
|
||||
|
||||
func htmlSecond(b byte) bool {
|
||||
return b == '!' || b == '/' || b == '?' || isLetter(b)
|
||||
}
|
||||
|
||||
func ruleHTMLInline(s *StateInline, silent bool) bool {
|
||||
if !s.Md.HTML {
|
||||
return false
|
||||
}
|
||||
|
||||
pos := s.Pos
|
||||
src := s.Src
|
||||
if pos+2 >= s.PosMax || src[pos] != '<' {
|
||||
return false
|
||||
}
|
||||
|
||||
if !htmlSecond(src[pos+1]) {
|
||||
return false
|
||||
}
|
||||
|
||||
match := rHTMLTag.FindString(src[pos:])
|
||||
if match == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if !silent {
|
||||
s.PushToken(&HTMLInline{Content: match})
|
||||
}
|
||||
|
||||
s.Pos += len(match)
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
func ruleImage(s *StateInline, silent bool) bool {
|
||||
pos := s.Pos
|
||||
max := s.PosMax
|
||||
|
||||
if pos+2 >= max {
|
||||
return false
|
||||
}
|
||||
|
||||
src := s.Src
|
||||
if src[pos] != '!' || src[pos+1] != '[' {
|
||||
return false
|
||||
}
|
||||
|
||||
labelStart := pos + 2
|
||||
labelEnd := parseLinkLabel(s, pos+1, false)
|
||||
if labelEnd < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
var href, title, label string
|
||||
oldPos := pos
|
||||
pos = labelEnd + 1
|
||||
if pos < max && src[pos] == '(' {
|
||||
pos++
|
||||
for pos < max {
|
||||
b := src[pos]
|
||||
if !byteIsSpace(b) && b != '\n' {
|
||||
break
|
||||
}
|
||||
pos++
|
||||
}
|
||||
if pos >= max {
|
||||
return false
|
||||
}
|
||||
|
||||
start := pos
|
||||
url, _, endpos, ok := parseLinkDestination(src, pos, s.PosMax)
|
||||
if ok {
|
||||
url = normalizeLink(url)
|
||||
if validateLink(url) {
|
||||
href = url
|
||||
pos = endpos
|
||||
}
|
||||
}
|
||||
|
||||
start = pos
|
||||
for pos < max {
|
||||
b := src[pos]
|
||||
if !byteIsSpace(b) && b != '\n' {
|
||||
break
|
||||
}
|
||||
pos++
|
||||
}
|
||||
if pos >= max {
|
||||
return false
|
||||
}
|
||||
|
||||
title, _, endpos, ok = parseLinkTitle(src, pos, s.PosMax)
|
||||
if pos < max && start != pos && ok {
|
||||
pos = endpos
|
||||
for pos < max {
|
||||
b := src[pos]
|
||||
if !byteIsSpace(b) && b != '\n' {
|
||||
break
|
||||
}
|
||||
pos++
|
||||
}
|
||||
}
|
||||
|
||||
if pos >= max || src[pos] != ')' {
|
||||
s.Pos = oldPos
|
||||
return false
|
||||
}
|
||||
|
||||
pos++
|
||||
|
||||
} else {
|
||||
if s.Env.References == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if pos < max && src[pos] == '[' {
|
||||
start := pos + 1
|
||||
pos = parseLinkLabel(s, pos, false)
|
||||
if pos >= 0 {
|
||||
label = src[start:pos]
|
||||
pos++
|
||||
} else {
|
||||
pos = labelEnd + 1
|
||||
}
|
||||
} else {
|
||||
pos = labelEnd + 1
|
||||
}
|
||||
|
||||
if label == "" {
|
||||
label = src[labelStart:labelEnd]
|
||||
}
|
||||
|
||||
ref, ok := s.Env.References[normalizeReference(label)]
|
||||
if !ok {
|
||||
s.Pos = oldPos
|
||||
return false
|
||||
}
|
||||
|
||||
href = ref["href"]
|
||||
title = ref["title"]
|
||||
}
|
||||
|
||||
if !silent {
|
||||
content := src[labelStart:labelEnd]
|
||||
|
||||
tokens := s.Md.Inline.Parse(content, s.Md, s.Env)
|
||||
|
||||
s.PushToken(&Image{
|
||||
Src: href,
|
||||
Title: title,
|
||||
Tokens: tokens,
|
||||
})
|
||||
}
|
||||
|
||||
s.Pos = pos
|
||||
s.PosMax = max
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
func ruleInline(s *StateCore) {
|
||||
for _, tok := range s.Tokens {
|
||||
if tok, ok := tok.(*Inline); ok {
|
||||
tok.Children = s.Md.Inline.Parse(tok.Content, s.Md, s.Env)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "strings"
|
||||
|
||||
func ruleLHeading(s *StateBlock, startLine, endLine int, silent bool) bool {
|
||||
nextLine := startLine + 1
|
||||
|
||||
if s.SCount[startLine]-s.BlkIndent >= 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
oldParentType := s.ParentType
|
||||
s.ParentType = ptParagraph
|
||||
src := s.Src
|
||||
|
||||
var pos int
|
||||
var hLevel int
|
||||
outer:
|
||||
for ; nextLine < endLine && !s.IsLineEmpty(nextLine); nextLine++ {
|
||||
if s.SCount[nextLine]-s.BlkIndent > 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.SCount[nextLine] >= s.BlkIndent {
|
||||
pos = s.BMarks[nextLine] + s.TShift[nextLine]
|
||||
max := s.EMarks[nextLine]
|
||||
|
||||
if pos < max {
|
||||
marker := src[pos]
|
||||
|
||||
if marker == '-' || marker == '=' {
|
||||
pos = s.SkipBytes(pos, marker)
|
||||
pos = s.SkipSpaces(pos)
|
||||
|
||||
if pos >= max {
|
||||
hLevel = 1
|
||||
if marker == '-' {
|
||||
hLevel++
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.SCount[nextLine] < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, r := range paragraphTerminatedBy {
|
||||
if r(s, nextLine, endLine, true) {
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hLevel == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
s.Line = nextLine + 1
|
||||
|
||||
s.PushOpeningToken(&HeadingOpen{
|
||||
HLevel: hLevel,
|
||||
Map: [2]int{startLine, s.Line},
|
||||
})
|
||||
s.PushToken(&Inline{
|
||||
Content: strings.TrimSpace(s.Lines(startLine, nextLine, s.BlkIndent, false)),
|
||||
Map: [2]int{startLine, s.Line - 1},
|
||||
})
|
||||
s.PushClosingToken(&HeadingClose{HLevel: hLevel})
|
||||
|
||||
s.ParentType = oldParentType
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
func ruleLink(s *StateInline, silent bool) bool {
|
||||
pos := s.Pos
|
||||
oldPos := s.Pos
|
||||
max := s.PosMax
|
||||
start := s.Pos
|
||||
parseReference := true
|
||||
src := s.Src
|
||||
|
||||
if src[pos] != '[' {
|
||||
return false
|
||||
}
|
||||
|
||||
labelStart := pos + 1
|
||||
labelEnd := parseLinkLabel(s, pos, true)
|
||||
|
||||
if labelEnd < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
pos = labelEnd + 1
|
||||
|
||||
var title, href, label string
|
||||
if pos < max && src[pos] == '(' {
|
||||
parseReference = false
|
||||
|
||||
pos++
|
||||
for pos < max {
|
||||
code := src[pos]
|
||||
if !byteIsSpace(code) && code != '\n' {
|
||||
break
|
||||
}
|
||||
pos++
|
||||
}
|
||||
if pos >= max {
|
||||
return false
|
||||
}
|
||||
|
||||
start = pos
|
||||
url, _, endpos, ok := parseLinkDestination(src, pos, s.PosMax)
|
||||
if ok {
|
||||
url = normalizeLink(url)
|
||||
if validateLink(url) {
|
||||
pos = endpos
|
||||
href = url
|
||||
}
|
||||
}
|
||||
|
||||
start = pos
|
||||
for pos < max {
|
||||
code := src[pos]
|
||||
if !byteIsSpace(code) && code != '\n' {
|
||||
break
|
||||
}
|
||||
pos++
|
||||
}
|
||||
|
||||
title, _, endpos, ok = parseLinkTitle(src, pos, s.PosMax)
|
||||
if pos < max && start != pos && ok {
|
||||
pos = endpos
|
||||
for pos < max {
|
||||
code := src[pos]
|
||||
if !byteIsSpace(code) && code != '\n' {
|
||||
break
|
||||
}
|
||||
pos++
|
||||
}
|
||||
}
|
||||
|
||||
if pos >= max || src[pos] != ')' {
|
||||
parseReference = true
|
||||
}
|
||||
|
||||
pos++
|
||||
}
|
||||
|
||||
if parseReference {
|
||||
if s.Env.References == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if pos < max && src[pos] == '[' {
|
||||
start := pos + 1
|
||||
pos = parseLinkLabel(s, pos, false)
|
||||
if pos >= 0 {
|
||||
label = src[start:pos]
|
||||
pos++
|
||||
} else {
|
||||
pos = labelEnd + 1
|
||||
}
|
||||
} else {
|
||||
pos = labelEnd + 1
|
||||
}
|
||||
|
||||
if label == "" {
|
||||
label = src[labelStart:labelEnd]
|
||||
}
|
||||
|
||||
ref, ok := s.Env.References[normalizeReference(label)]
|
||||
if !ok {
|
||||
s.Pos = oldPos
|
||||
return false
|
||||
}
|
||||
|
||||
href = ref["href"]
|
||||
title = ref["title"]
|
||||
}
|
||||
|
||||
if !silent {
|
||||
s.Pos = labelStart
|
||||
s.PosMax = labelEnd
|
||||
|
||||
s.PushOpeningToken(&LinkOpen{
|
||||
Href: href,
|
||||
Title: title,
|
||||
})
|
||||
|
||||
s.Md.Inline.Tokenize(s)
|
||||
|
||||
s.PushClosingToken(&LinkClose{})
|
||||
}
|
||||
|
||||
s.Pos = pos
|
||||
s.PosMax = max
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gitlab.com/golang-commonmark/linkify"
|
||||
)
|
||||
|
||||
func isLinkOpen(s string) bool { return isLetter(s[1]) }
|
||||
|
||||
func isLinkClose(s string) bool { return s[1] == '/' }
|
||||
|
||||
func ruleLinkify(s *StateCore) {
|
||||
blockTokens := s.Tokens
|
||||
|
||||
if !s.Md.Linkify {
|
||||
return
|
||||
}
|
||||
|
||||
for _, tok := range blockTokens {
|
||||
if tok, ok := tok.(*Inline); ok {
|
||||
tokens := tok.Children
|
||||
|
||||
htmlLinkLevel := 0
|
||||
|
||||
for i := len(tokens) - 1; i >= 0; i-- {
|
||||
currentTok := tokens[i]
|
||||
|
||||
if _, ok := currentTok.(*LinkClose); ok {
|
||||
i--
|
||||
for tokens[i].Level() != currentTok.Level() {
|
||||
if _, ok := tokens[i].(*LinkOpen); ok {
|
||||
break
|
||||
}
|
||||
i--
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if currentTok, ok := currentTok.(*HTMLInline); ok {
|
||||
if isLinkOpen(currentTok.Content) && htmlLinkLevel > 0 {
|
||||
htmlLinkLevel--
|
||||
}
|
||||
if isLinkClose(currentTok.Content) {
|
||||
htmlLinkLevel++
|
||||
}
|
||||
}
|
||||
if htmlLinkLevel > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if currentTok, ok := currentTok.(*Text); ok {
|
||||
text := currentTok.Content
|
||||
links := linkify.Links(text)
|
||||
if len(links) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var nodes []Token
|
||||
level := currentTok.Lvl
|
||||
lastPos := 0
|
||||
|
||||
for _, ln := range links {
|
||||
urlText := text[ln.Start:ln.End]
|
||||
url := urlText
|
||||
if ln.Scheme == "" {
|
||||
url = "http://" + url
|
||||
} else if ln.Scheme == "mailto:" && !strings.HasPrefix(url, "mailto:") {
|
||||
url = "mailto:" + url
|
||||
}
|
||||
url = normalizeLink(url)
|
||||
if !validateLink(url) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ln.Scheme == "" {
|
||||
urlText = strings.TrimPrefix(normalizeLinkText("http://"+urlText), "http://")
|
||||
} else if ln.Scheme == "mailto:" && !strings.HasPrefix(urlText, "mailto:") {
|
||||
urlText = strings.TrimPrefix(normalizeLinkText("mailto:"+urlText), "mailto:")
|
||||
} else {
|
||||
urlText = normalizeLinkText(urlText)
|
||||
}
|
||||
|
||||
pos := ln.Start
|
||||
|
||||
if pos > lastPos {
|
||||
tok := Text{
|
||||
Content: text[lastPos:pos],
|
||||
Lvl: level,
|
||||
}
|
||||
nodes = append(nodes, &tok)
|
||||
}
|
||||
|
||||
nodes = append(nodes, &LinkOpen{
|
||||
Href: url,
|
||||
Lvl: level,
|
||||
})
|
||||
nodes = append(nodes, &Text{
|
||||
Content: urlText,
|
||||
Lvl: level + 1,
|
||||
})
|
||||
nodes = append(nodes, &LinkClose{
|
||||
Lvl: level,
|
||||
})
|
||||
|
||||
lastPos = ln.End
|
||||
}
|
||||
|
||||
if lastPos < len(text) {
|
||||
tok := Text{
|
||||
Content: text[lastPos:],
|
||||
Lvl: level,
|
||||
}
|
||||
nodes = append(nodes, &tok)
|
||||
}
|
||||
|
||||
children := make([]Token, len(tokens)+len(nodes)-1)
|
||||
copy(children, tokens[:i])
|
||||
copy(children[i:], nodes)
|
||||
copy(children[i+len(nodes):], tokens[i+1:])
|
||||
tok.Children = children
|
||||
tokens = children
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,285 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "strconv"
|
||||
|
||||
var listTerminatedBy []BlockRule
|
||||
|
||||
func skipBulletListMarker(s *StateBlock, startLine int) int {
|
||||
pos := s.BMarks[startLine] + s.TShift[startLine]
|
||||
max := s.EMarks[startLine]
|
||||
src := s.Src
|
||||
|
||||
if pos >= max {
|
||||
return -1
|
||||
}
|
||||
|
||||
marker := src[pos]
|
||||
if marker != '*' && marker != '-' && marker != '+' {
|
||||
return -1
|
||||
}
|
||||
pos++
|
||||
|
||||
if pos < max && !byteIsSpace(src[pos]) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func skipOrderedListMarker(s *StateBlock, startLine int) int {
|
||||
start := s.BMarks[startLine] + s.TShift[startLine]
|
||||
pos := start
|
||||
max := s.EMarks[startLine]
|
||||
|
||||
if pos+1 >= max {
|
||||
return -1
|
||||
}
|
||||
|
||||
src := s.Src
|
||||
ch := src[pos]
|
||||
if ch < '0' || ch > '9' {
|
||||
return -1
|
||||
}
|
||||
pos++
|
||||
|
||||
for {
|
||||
if pos >= max {
|
||||
return -1
|
||||
}
|
||||
|
||||
ch = src[pos]
|
||||
pos++
|
||||
|
||||
if ch >= '0' && ch <= '9' {
|
||||
if pos-start >= 10 {
|
||||
return -1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == ')' || ch == '.' {
|
||||
break
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
if pos < max && !byteIsSpace(src[pos]) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func markParagraphsTight(s *StateBlock, idx int) {
|
||||
level := s.Level + 2
|
||||
tokens := s.Tokens
|
||||
|
||||
for i := idx + 2; i < len(tokens)-2; i++ {
|
||||
if tokens[i].Level() == level {
|
||||
if tok, ok := tokens[i].(*ParagraphOpen); ok {
|
||||
tok.Hidden = true
|
||||
i += 2
|
||||
tokens[i].(*ParagraphClose).Hidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ruleList(s *StateBlock, startLine, endLine int, silent bool) bool {
|
||||
isTerminatingParagraph := false
|
||||
tight := true
|
||||
|
||||
if s.SCount[startLine]-s.BlkIndent >= 4 {
|
||||
return false
|
||||
}
|
||||
src := s.Src
|
||||
|
||||
if silent && s.ParentType == ptParagraph {
|
||||
if s.TShift[startLine] >= s.BlkIndent {
|
||||
isTerminatingParagraph = true
|
||||
}
|
||||
}
|
||||
|
||||
var start int
|
||||
var markerValue int
|
||||
|
||||
isOrdered := false
|
||||
posAfterMarker := skipOrderedListMarker(s, startLine)
|
||||
if posAfterMarker > 0 {
|
||||
isOrdered = true
|
||||
start = s.BMarks[startLine] + s.TShift[startLine]
|
||||
markerValue, _ = strconv.Atoi(src[start : posAfterMarker-1])
|
||||
|
||||
if isTerminatingParagraph && markerValue != 1 {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
posAfterMarker = skipBulletListMarker(s, startLine)
|
||||
if posAfterMarker < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if isTerminatingParagraph {
|
||||
if s.SkipSpaces(posAfterMarker) >= s.EMarks[startLine] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
markerChar := src[posAfterMarker-1]
|
||||
|
||||
if silent {
|
||||
return true
|
||||
}
|
||||
|
||||
tokenIdx := len(s.Tokens)
|
||||
|
||||
var listMap *[2]int
|
||||
if isOrdered {
|
||||
tok := &OrderedListOpen{
|
||||
Order: markerValue,
|
||||
Map: [2]int{startLine, 0},
|
||||
}
|
||||
s.PushOpeningToken(tok)
|
||||
listMap = &tok.Map
|
||||
} else {
|
||||
tok := &BulletListOpen{
|
||||
Map: [2]int{startLine, 0},
|
||||
}
|
||||
s.PushOpeningToken(tok)
|
||||
listMap = &tok.Map
|
||||
}
|
||||
|
||||
nextLine := startLine
|
||||
prevEmptyEnd := false
|
||||
|
||||
oldParentType := s.ParentType
|
||||
s.ParentType = ptList
|
||||
|
||||
var pos int
|
||||
var contentStart int
|
||||
|
||||
outer:
|
||||
for nextLine < endLine {
|
||||
pos = posAfterMarker
|
||||
max := s.EMarks[nextLine]
|
||||
|
||||
initial := s.SCount[nextLine] + posAfterMarker - (s.BMarks[startLine] + s.TShift[startLine])
|
||||
offset := initial
|
||||
|
||||
loop:
|
||||
for pos < max {
|
||||
switch src[pos] {
|
||||
case '\t':
|
||||
offset += 4 - (offset+s.BSCount[nextLine])%4
|
||||
case ' ':
|
||||
offset++
|
||||
default:
|
||||
break loop
|
||||
}
|
||||
pos++
|
||||
}
|
||||
|
||||
contentStart = pos
|
||||
|
||||
indentAfterMarker := 1
|
||||
if contentStart < max {
|
||||
if iam := offset - initial; iam <= 4 {
|
||||
indentAfterMarker = iam
|
||||
}
|
||||
}
|
||||
|
||||
indent := initial + indentAfterMarker
|
||||
|
||||
tok := &ListItemOpen{
|
||||
Map: [2]int{startLine, 0},
|
||||
}
|
||||
s.PushOpeningToken(tok)
|
||||
itemMap := &tok.Map
|
||||
|
||||
oldIndent := s.BlkIndent
|
||||
oldTight := s.Tight
|
||||
oldTShift := s.TShift[startLine]
|
||||
oldLIndent := s.SCount[startLine]
|
||||
s.BlkIndent = indent
|
||||
s.Tight = true
|
||||
s.TShift[startLine] = contentStart - s.BMarks[startLine]
|
||||
s.SCount[startLine] = offset
|
||||
|
||||
if contentStart >= max && s.IsLineEmpty(startLine+1) {
|
||||
s.Line = min(s.Line+2, endLine)
|
||||
} else {
|
||||
s.Md.Block.Tokenize(s, startLine, endLine)
|
||||
}
|
||||
|
||||
if !s.Tight || prevEmptyEnd {
|
||||
tight = false
|
||||
}
|
||||
|
||||
prevEmptyEnd = s.Line-startLine > 1 && s.IsLineEmpty(s.Line-1)
|
||||
|
||||
s.BlkIndent = oldIndent
|
||||
s.TShift[startLine] = oldTShift
|
||||
s.SCount[startLine] = oldLIndent
|
||||
s.Tight = oldTight
|
||||
|
||||
s.PushClosingToken(&ListItemClose{})
|
||||
|
||||
startLine = s.Line
|
||||
nextLine = startLine
|
||||
(*itemMap)[1] = nextLine
|
||||
|
||||
if nextLine >= endLine {
|
||||
break
|
||||
}
|
||||
|
||||
contentStart = s.BMarks[startLine]
|
||||
|
||||
if s.SCount[nextLine] < s.BlkIndent {
|
||||
break
|
||||
}
|
||||
|
||||
for _, r := range listTerminatedBy {
|
||||
if r(s, nextLine, endLine, true) {
|
||||
break outer
|
||||
}
|
||||
}
|
||||
|
||||
if isOrdered {
|
||||
posAfterMarker = skipOrderedListMarker(s, nextLine)
|
||||
if posAfterMarker < 0 {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
posAfterMarker = skipBulletListMarker(s, nextLine)
|
||||
if posAfterMarker < 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if markerChar != src[posAfterMarker-1] {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isOrdered {
|
||||
s.PushClosingToken(&OrderedListClose{})
|
||||
} else {
|
||||
s.PushClosingToken(&BulletListClose{})
|
||||
}
|
||||
|
||||
(*listMap)[1] = nextLine
|
||||
s.Line = nextLine
|
||||
s.ParentType = oldParentType
|
||||
|
||||
if tight {
|
||||
markParagraphsTight(s, tokenIdx)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package markdown provides CommonMark-compliant markdown parser and renderer.
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Markdown struct {
|
||||
options
|
||||
Block ParserBlock
|
||||
Inline ParserInline
|
||||
renderOptions RenderOptions
|
||||
}
|
||||
|
||||
type RenderOptions struct {
|
||||
LangPrefix string // CSS language class prefix for fenced blocks
|
||||
XHTML bool // render as XHTML instead of HTML
|
||||
Breaks bool // convert \n in paragraphs into <br>
|
||||
Nofollow bool // add rel="nofollow" to the links
|
||||
}
|
||||
|
||||
type options struct {
|
||||
HTML bool // allow raw HTML in the markup
|
||||
Tables bool // GFM tables
|
||||
Linkify bool // autoconvert URL-like text to links
|
||||
Typographer bool // enable some typographic replacements
|
||||
Quotes [4]string // double/single quotes replacement pairs
|
||||
MaxNesting int // maximum nesting level
|
||||
}
|
||||
|
||||
type Environment struct {
|
||||
References map[string]map[string]string
|
||||
}
|
||||
|
||||
type CoreRule func(*StateCore)
|
||||
|
||||
var coreRules []CoreRule
|
||||
|
||||
func New(opts ...option) *Markdown {
|
||||
m := &Markdown{
|
||||
options: options{
|
||||
Tables: true,
|
||||
Linkify: true,
|
||||
Typographer: true,
|
||||
Quotes: [4]string{"“", "”", "‘", "’"},
|
||||
MaxNesting: 20,
|
||||
},
|
||||
renderOptions: RenderOptions{LangPrefix: "language-"},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Markdown) Parse(src []byte) []Token {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := &StateCore{
|
||||
Md: m,
|
||||
Env: &Environment{},
|
||||
}
|
||||
s.Tokens = m.Block.Parse(src, m, s.Env)
|
||||
|
||||
for _, r := range coreRules {
|
||||
r(s)
|
||||
}
|
||||
return s.Tokens
|
||||
}
|
||||
|
||||
func (m *Markdown) Render(w io.Writer, src []byte) error {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NewRenderer(w).Render(m.Parse(src), m.renderOptions)
|
||||
}
|
||||
|
||||
func (m *Markdown) RenderTokens(w io.Writer, tokens []Token) error {
|
||||
if len(tokens) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NewRenderer(w).Render(tokens, m.renderOptions)
|
||||
}
|
||||
|
||||
func (m *Markdown) RenderToString(src []byte) string {
|
||||
if len(src) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
NewRenderer(&buf).Render(m.Parse(src), m.renderOptions)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (m *Markdown) RenderTokensToString(tokens []Token) string {
|
||||
if len(tokens) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
NewRenderer(&buf).Render(tokens, m.renderOptions)
|
||||
return buf.String()
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
func ruleNewline(s *StateInline, silent bool) bool {
|
||||
pos := s.Pos
|
||||
src := s.Src
|
||||
|
||||
if src[pos] != '\n' {
|
||||
return false
|
||||
}
|
||||
|
||||
pending := s.Pending.Bytes()
|
||||
pmax := len(pending) - 1
|
||||
max := s.PosMax
|
||||
|
||||
if !silent {
|
||||
if pmax >= 0 && pending[pmax] == ' ' {
|
||||
if pmax >= 1 && pending[pmax-1] == ' ' {
|
||||
pmax -= 2
|
||||
for pmax >= 0 && pending[pmax] == ' ' {
|
||||
pmax--
|
||||
}
|
||||
s.Pending.Truncate(pmax + 1)
|
||||
s.PushToken(&Hardbreak{})
|
||||
} else {
|
||||
s.Pending.Truncate(pmax)
|
||||
s.PushToken(&Softbreak{})
|
||||
}
|
||||
} else {
|
||||
s.PushToken(&Softbreak{})
|
||||
}
|
||||
}
|
||||
|
||||
pos++
|
||||
|
||||
for pos < max && byteIsSpace(src[pos]) {
|
||||
pos++
|
||||
}
|
||||
|
||||
s.Pos = pos
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
type option func(m *Markdown)
|
||||
|
||||
func HTML(b bool) option {
|
||||
return func(m *Markdown) {
|
||||
m.HTML = b
|
||||
}
|
||||
}
|
||||
|
||||
func Linkify(b bool) option {
|
||||
return func(m *Markdown) {
|
||||
m.Linkify = b
|
||||
}
|
||||
}
|
||||
|
||||
func Typographer(b bool) option {
|
||||
return func(m *Markdown) {
|
||||
m.Typographer = b
|
||||
}
|
||||
}
|
||||
|
||||
func Quotes(stringOrArray interface{}) option {
|
||||
if s, ok := stringOrArray.(string); ok {
|
||||
return func(m *Markdown) {
|
||||
for i, r := range []rune(s) {
|
||||
m.Quotes[i] = string(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
a := stringOrArray.([]string)
|
||||
return func(m *Markdown) {
|
||||
for i, s := range a {
|
||||
m.Quotes[i] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MaxNesting(n int) option {
|
||||
return func(m *Markdown) {
|
||||
m.MaxNesting = n
|
||||
}
|
||||
}
|
||||
|
||||
func XHTMLOutput(b bool) option {
|
||||
return func(m *Markdown) {
|
||||
m.renderOptions.XHTML = b
|
||||
}
|
||||
}
|
||||
|
||||
func Breaks(b bool) option {
|
||||
return func(m *Markdown) {
|
||||
m.renderOptions.Breaks = b
|
||||
}
|
||||
}
|
||||
|
||||
func LangPrefix(p string) option {
|
||||
return func(m *Markdown) {
|
||||
m.renderOptions.LangPrefix = p
|
||||
}
|
||||
}
|
||||
|
||||
func Nofollow(b bool) option {
|
||||
return func(m *Markdown) {
|
||||
m.renderOptions.Nofollow = b
|
||||
}
|
||||
}
|
||||
|
||||
func Tables(b bool) option {
|
||||
return func(m *Markdown) {
|
||||
m.Tables = b
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "strings"
|
||||
|
||||
var paragraphTerminatedBy []BlockRule
|
||||
|
||||
func ruleParagraph(s *StateBlock, startLine, _ int, _ bool) bool {
|
||||
nextLine := startLine + 1
|
||||
endLine := s.LineMax
|
||||
|
||||
oldParentType := s.ParentType
|
||||
s.ParentType = ptParagraph
|
||||
|
||||
outer:
|
||||
for ; nextLine < endLine && !s.IsLineEmpty(nextLine); nextLine++ {
|
||||
if s.SCount[nextLine]-s.BlkIndent > 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.SCount[nextLine] < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, r := range paragraphTerminatedBy {
|
||||
if r(s, nextLine, endLine, true) {
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(s.Lines(startLine, nextLine, s.BlkIndent, false))
|
||||
|
||||
s.Line = nextLine
|
||||
|
||||
s.PushOpeningToken(&ParagraphOpen{
|
||||
Map: [2]int{startLine, s.Line},
|
||||
})
|
||||
s.PushToken(&Inline{
|
||||
Content: content,
|
||||
Map: [2]int{startLine, s.Line},
|
||||
})
|
||||
s.PushClosingToken(&ParagraphClose{})
|
||||
|
||||
s.ParentType = oldParentType
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type ParserBlock struct{}
|
||||
|
||||
type BlockRule func(*StateBlock, int, int, bool) bool
|
||||
|
||||
var blockRules []BlockRule
|
||||
|
||||
var nl = []byte{'\n'}
|
||||
|
||||
func normalizeNewlines(src []byte) ([]byte, int) {
|
||||
if bytes.IndexByte(src, '\r') == -1 {
|
||||
return src, bytes.Count(src, nl)
|
||||
}
|
||||
n := 0
|
||||
buf := make([]byte, 0, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
switch ch := src[i]; ch {
|
||||
case '\n':
|
||||
n++
|
||||
buf = append(buf, '\n')
|
||||
case '\r':
|
||||
buf = append(buf, '\n')
|
||||
n++
|
||||
if i < len(src)-1 && src[i+1] == '\n' {
|
||||
i++
|
||||
}
|
||||
default:
|
||||
buf = append(buf, ch)
|
||||
}
|
||||
}
|
||||
return buf, n
|
||||
}
|
||||
|
||||
func (b ParserBlock) Parse(src []byte, md *Markdown, env *Environment) []Token {
|
||||
src, n := normalizeNewlines(src)
|
||||
if len(src) == 0 || src[len(src)-1] != '\n' {
|
||||
n++
|
||||
}
|
||||
n++
|
||||
|
||||
indentFound := false
|
||||
start := 0
|
||||
indent := 0
|
||||
offset := 0
|
||||
|
||||
mem := make([]int, 0, n*5)
|
||||
bMarks := mem[0:0:n]
|
||||
eMarks := mem[n : n : n*2]
|
||||
tShift := mem[n*2 : n*2 : n*3]
|
||||
sCount := mem[n*3 : n*3 : n*4]
|
||||
bsCount := mem[n*4 : n*4 : n*5]
|
||||
|
||||
_, lastRuneLen := utf8.DecodeLastRune(src)
|
||||
lastRunePos := len(src) - lastRuneLen
|
||||
for pos, r := range string(src) {
|
||||
if !indentFound {
|
||||
if runeIsSpace(r) {
|
||||
indent++
|
||||
if r == '\t' {
|
||||
offset += 4 - offset%4
|
||||
} else {
|
||||
offset++
|
||||
}
|
||||
continue
|
||||
}
|
||||
indentFound = true
|
||||
}
|
||||
|
||||
if r == '\n' || pos == lastRunePos {
|
||||
if r != '\n' {
|
||||
pos = len(src)
|
||||
}
|
||||
bMarks = append(bMarks, start)
|
||||
eMarks = append(eMarks, pos)
|
||||
tShift = append(tShift, indent)
|
||||
sCount = append(sCount, offset)
|
||||
bsCount = append(bsCount, 0)
|
||||
|
||||
indentFound = false
|
||||
indent = 0
|
||||
offset = 0
|
||||
start = pos + 1
|
||||
}
|
||||
}
|
||||
|
||||
bMarks = append(bMarks, len(src))
|
||||
eMarks = append(eMarks, len(src))
|
||||
tShift = append(tShift, 0)
|
||||
sCount = append(sCount, 0)
|
||||
bsCount = append(bsCount, 0)
|
||||
|
||||
var s StateBlock
|
||||
s.BMarks = bMarks
|
||||
s.EMarks = eMarks
|
||||
s.TShift = tShift
|
||||
s.SCount = sCount
|
||||
s.BSCount = bsCount
|
||||
s.LineMax = n - 1
|
||||
s.Src = string(src)
|
||||
s.Md = md
|
||||
s.Env = env
|
||||
|
||||
b.Tokenize(&s, s.Line, s.LineMax)
|
||||
|
||||
return s.Tokens
|
||||
}
|
||||
|
||||
func (ParserBlock) Tokenize(s *StateBlock, startLine, endLine int) {
|
||||
line := startLine
|
||||
hasEmptyLines := false
|
||||
maxNesting := s.Md.MaxNesting
|
||||
|
||||
for line < endLine {
|
||||
line = s.SkipEmptyLines(line)
|
||||
s.Line = line
|
||||
if line >= endLine {
|
||||
break
|
||||
}
|
||||
|
||||
if s.SCount[line] < s.BlkIndent {
|
||||
break
|
||||
}
|
||||
|
||||
if s.Level >= maxNesting {
|
||||
s.Line = endLine
|
||||
break
|
||||
}
|
||||
|
||||
for _, r := range blockRules {
|
||||
if r(s, line, endLine, false) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
s.Tight = !hasEmptyLines
|
||||
|
||||
if s.IsLineEmpty(s.Line - 1) {
|
||||
hasEmptyLines = true
|
||||
}
|
||||
|
||||
line = s.Line
|
||||
|
||||
if line < endLine && s.IsLineEmpty(line) {
|
||||
hasEmptyLines = true
|
||||
line++
|
||||
s.Line = line
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "unicode/utf8"
|
||||
|
||||
type ParserInline struct {
|
||||
}
|
||||
|
||||
type (
|
||||
InlineRule func(*StateInline, bool) bool
|
||||
PostprocessRule func(*StateInline)
|
||||
)
|
||||
|
||||
var (
|
||||
inlineRules []InlineRule
|
||||
postprocessRules []PostprocessRule
|
||||
)
|
||||
|
||||
func (i ParserInline) Parse(src string, md *Markdown, env *Environment) []Token {
|
||||
if src == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var s StateInline
|
||||
s.Src = src
|
||||
s.Md = md
|
||||
s.Env = env
|
||||
s.PosMax = len(src)
|
||||
s.Tokens = s.bootstrap[:0]
|
||||
|
||||
i.Tokenize(&s)
|
||||
|
||||
for _, r := range postprocessRules {
|
||||
r(&s)
|
||||
}
|
||||
|
||||
return s.Tokens
|
||||
}
|
||||
|
||||
func (ParserInline) Tokenize(s *StateInline) {
|
||||
end := s.PosMax
|
||||
src := s.Src
|
||||
maxNesting := s.Md.MaxNesting
|
||||
ok := false
|
||||
|
||||
for s.Pos < end {
|
||||
if s.Level < maxNesting {
|
||||
for _, rule := range inlineRules {
|
||||
ok = rule(s, false)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ok {
|
||||
if s.Pos >= end {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
r, size := utf8.DecodeRuneInString(src[s.Pos:])
|
||||
s.Pending.WriteRune(r)
|
||||
s.Pos += size
|
||||
}
|
||||
|
||||
if s.Pending.Len() > 0 {
|
||||
s.PushPending()
|
||||
}
|
||||
}
|
||||
|
||||
func (ParserInline) SkipToken(s *StateInline) {
|
||||
pos := s.Pos
|
||||
if s.Cache != nil {
|
||||
if newPos, ok := s.Cache[pos]; ok {
|
||||
s.Pos = newPos
|
||||
return
|
||||
}
|
||||
} else {
|
||||
s.Cache = make(map[int]int)
|
||||
}
|
||||
|
||||
ok := false
|
||||
if s.Level < s.Md.MaxNesting {
|
||||
for _, r := range inlineRules {
|
||||
s.Level++
|
||||
ok = r(s, true)
|
||||
s.Level--
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.Pos = s.PosMax
|
||||
}
|
||||
|
||||
if !ok {
|
||||
_, size := utf8.DecodeRuneInString(s.Src[s.Pos:])
|
||||
s.Pos += size
|
||||
}
|
||||
s.Cache[pos] = s.Pos
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "sort"
|
||||
|
||||
type registeredCoreRule struct {
|
||||
id int
|
||||
rule CoreRule
|
||||
}
|
||||
|
||||
var registeredCoreRules []registeredCoreRule
|
||||
|
||||
type registeredBlockRule struct {
|
||||
id int
|
||||
rule BlockRule
|
||||
terminates []int
|
||||
}
|
||||
|
||||
var registeredBlockRules []registeredBlockRule
|
||||
|
||||
type registeredInlineRule struct {
|
||||
id int
|
||||
rule InlineRule
|
||||
}
|
||||
|
||||
var registeredInlineRules []registeredInlineRule
|
||||
|
||||
type registeredPostprocessRule struct {
|
||||
id int
|
||||
rule PostprocessRule
|
||||
}
|
||||
|
||||
var registeredPostprocessRules []registeredPostprocessRule
|
||||
|
||||
func indexInt(a []int, n int) int {
|
||||
for i, m := range a {
|
||||
if m == n {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func RegisterCoreRule(id int, rule CoreRule) {
|
||||
registeredCoreRules = append(registeredCoreRules, registeredCoreRule{
|
||||
id: id,
|
||||
rule: rule,
|
||||
})
|
||||
sort.Slice(registeredCoreRules, func(i, j int) bool {
|
||||
return registeredCoreRules[i].id < registeredCoreRules[j].id
|
||||
})
|
||||
|
||||
coreRules = coreRules[:0]
|
||||
for _, r := range registeredCoreRules {
|
||||
coreRules = append(coreRules, r.rule)
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterBlockRule(id int, rule BlockRule, terminates []int) {
|
||||
registeredBlockRules = append(registeredBlockRules, registeredBlockRule{
|
||||
id: id,
|
||||
rule: rule,
|
||||
terminates: terminates,
|
||||
})
|
||||
sort.Slice(registeredBlockRules, func(i, j int) bool {
|
||||
return registeredBlockRules[i].id < registeredBlockRules[j].id
|
||||
})
|
||||
|
||||
blockRules = blockRules[:0]
|
||||
blockquoteTerminatedBy = blockquoteTerminatedBy[:0]
|
||||
listTerminatedBy = listTerminatedBy[:0]
|
||||
referenceTerminatedBy = referenceTerminatedBy[:0]
|
||||
paragraphTerminatedBy = paragraphTerminatedBy[:0]
|
||||
for _, r := range registeredBlockRules {
|
||||
blockRules = append(blockRules, r.rule)
|
||||
if indexInt(r.terminates, 400) != -1 {
|
||||
blockquoteTerminatedBy = append(blockquoteTerminatedBy, r.rule)
|
||||
}
|
||||
if indexInt(r.terminates, 600) != -1 {
|
||||
listTerminatedBy = append(listTerminatedBy, r.rule)
|
||||
}
|
||||
if indexInt(r.terminates, 700) != -1 {
|
||||
referenceTerminatedBy = append(referenceTerminatedBy, r.rule)
|
||||
}
|
||||
if indexInt(r.terminates, 1100) != -1 {
|
||||
paragraphTerminatedBy = append(paragraphTerminatedBy, r.rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterInlineRule(id int, rule InlineRule) {
|
||||
registeredInlineRules = append(registeredInlineRules, registeredInlineRule{
|
||||
id: id,
|
||||
rule: rule,
|
||||
})
|
||||
sort.Slice(registeredInlineRules, func(i, j int) bool {
|
||||
return registeredInlineRules[i].id < registeredInlineRules[j].id
|
||||
})
|
||||
|
||||
inlineRules = inlineRules[:0]
|
||||
for _, r := range registeredInlineRules {
|
||||
inlineRules = append(inlineRules, r.rule)
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterPostprocessRule(id int, rule PostprocessRule) {
|
||||
registeredPostprocessRules = append(registeredPostprocessRules, registeredPostprocessRule{
|
||||
id: id,
|
||||
rule: rule,
|
||||
})
|
||||
sort.Slice(registeredPostprocessRules, func(i, j int) bool {
|
||||
return registeredPostprocessRules[i].id < registeredPostprocessRules[j].id
|
||||
})
|
||||
|
||||
postprocessRules = postprocessRules[:0]
|
||||
for _, r := range registeredPostprocessRules {
|
||||
postprocessRules = append(postprocessRules, r.rule)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterCoreRule(100, ruleInline)
|
||||
RegisterCoreRule(200, ruleLinkify)
|
||||
RegisterCoreRule(300, ruleReplacements)
|
||||
RegisterCoreRule(400, ruleSmartQuotes)
|
||||
|
||||
RegisterBlockRule(100, ruleTable, []int{1100, 700})
|
||||
RegisterBlockRule(200, ruleCode, nil)
|
||||
RegisterBlockRule(300, ruleFence, []int{1100, 700, 400, 600})
|
||||
RegisterBlockRule(400, ruleBlockQuote, []int{1100, 700, 400, 600})
|
||||
RegisterBlockRule(500, ruleHR, []int{1100, 700, 400, 600})
|
||||
RegisterBlockRule(600, ruleList, []int{1100, 700, 400})
|
||||
RegisterBlockRule(700, ruleReference, nil)
|
||||
RegisterBlockRule(800, ruleHeading, []int{1100, 700, 400})
|
||||
RegisterBlockRule(900, ruleLHeading, nil)
|
||||
RegisterBlockRule(1000, ruleHTMLBlock, []int{1100, 700, 400})
|
||||
RegisterBlockRule(1100, ruleParagraph, nil)
|
||||
|
||||
RegisterInlineRule(100, ruleText)
|
||||
RegisterInlineRule(200, ruleNewline)
|
||||
RegisterInlineRule(300, ruleEscape)
|
||||
RegisterInlineRule(400, ruleBackticks)
|
||||
RegisterInlineRule(500, ruleStrikeThrough)
|
||||
RegisterInlineRule(600, ruleEmphasis)
|
||||
RegisterInlineRule(700, ruleLink)
|
||||
RegisterInlineRule(800, ruleImage)
|
||||
RegisterInlineRule(900, ruleAutolink)
|
||||
RegisterInlineRule(1000, ruleHTMLInline)
|
||||
RegisterInlineRule(1100, ruleEntity)
|
||||
|
||||
RegisterPostprocessRule(100, ruleBalancePairs)
|
||||
RegisterPostprocessRule(200, ruleStrikethroughPostprocess)
|
||||
RegisterPostprocessRule(300, ruleEmphasisPostprocess)
|
||||
RegisterPostprocessRule(400, ruleTextCollapse)
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "strings"
|
||||
|
||||
var referenceTerminatedBy []BlockRule
|
||||
|
||||
func ruleReference(s *StateBlock, startLine, _ int, silent bool) bool {
|
||||
lines := 0
|
||||
pos := s.BMarks[startLine] + s.TShift[startLine]
|
||||
max := s.EMarks[startLine]
|
||||
nextLine := startLine + 1
|
||||
|
||||
if s.SCount[startLine]-s.BlkIndent >= 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
src := s.Src
|
||||
|
||||
if src[pos] != '[' {
|
||||
return false
|
||||
}
|
||||
|
||||
pos++
|
||||
for pos < max {
|
||||
if src[pos] == ']' && src[pos-1] != '\\' {
|
||||
if pos+1 == max {
|
||||
return false
|
||||
}
|
||||
if src[pos+1] != ':' {
|
||||
return false
|
||||
}
|
||||
break
|
||||
}
|
||||
pos++
|
||||
}
|
||||
|
||||
endLine := s.LineMax
|
||||
|
||||
oldParentType := s.ParentType
|
||||
s.ParentType = ptReference
|
||||
outer:
|
||||
for ; nextLine < endLine && !s.IsLineEmpty(nextLine); nextLine++ {
|
||||
if s.SCount[nextLine]-s.BlkIndent > 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.SCount[nextLine] < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, r := range referenceTerminatedBy {
|
||||
if r(s, nextLine, endLine, true) {
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
str := strings.TrimSpace(s.Lines(startLine, nextLine, s.BlkIndent, false))
|
||||
max = len(str)
|
||||
|
||||
var labelEnd int
|
||||
for pos = 1; pos < max; pos++ {
|
||||
b := str[pos]
|
||||
if b == '[' {
|
||||
return false
|
||||
} else if b == ']' {
|
||||
labelEnd = pos
|
||||
break
|
||||
} else if b == '\n' {
|
||||
lines++
|
||||
} else if b == '\\' {
|
||||
pos++
|
||||
if pos < max && str[pos] == '\n' {
|
||||
lines++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if labelEnd <= 0 || labelEnd+1 >= max || str[labelEnd+1] != ':' {
|
||||
return false
|
||||
}
|
||||
|
||||
for pos = labelEnd + 2; pos < max; pos++ {
|
||||
b := str[pos]
|
||||
if b == '\n' {
|
||||
lines++
|
||||
} else if !byteIsSpace(b) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
href, nlines, endpos, ok := parseLinkDestination(str, pos, max)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
href = normalizeLink(href)
|
||||
if !validateLink(href) {
|
||||
return false
|
||||
}
|
||||
|
||||
pos = endpos
|
||||
lines += nlines
|
||||
|
||||
destEndPos := pos
|
||||
destEndLineNo := lines
|
||||
|
||||
start := pos
|
||||
for ; pos < max; pos++ {
|
||||
b := str[pos]
|
||||
if b == '\n' {
|
||||
lines++
|
||||
} else if !byteIsSpace(b) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
title, nlines, endpos, ok := parseLinkTitle(str, pos, max)
|
||||
if pos < max && start != pos && ok {
|
||||
pos = endpos
|
||||
lines += nlines
|
||||
} else {
|
||||
pos = destEndPos
|
||||
lines = destEndLineNo
|
||||
}
|
||||
|
||||
for pos < max && byteIsSpace(str[pos]) {
|
||||
pos++
|
||||
}
|
||||
|
||||
if pos < max && str[pos] != '\n' {
|
||||
if title != "" {
|
||||
title = ""
|
||||
pos = destEndPos
|
||||
lines = destEndLineNo
|
||||
for pos < max && byteIsSpace(src[pos]) {
|
||||
pos++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pos < max && str[pos] != '\n' {
|
||||
return false
|
||||
}
|
||||
|
||||
label := normalizeReference(str[1:labelEnd])
|
||||
if label == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if silent {
|
||||
return true
|
||||
}
|
||||
|
||||
if s.Env.References == nil {
|
||||
s.Env.References = make(map[string]map[string]string)
|
||||
}
|
||||
if _, ok := s.Env.References[label]; !ok {
|
||||
s.Env.References[label] = map[string]string{
|
||||
"title": title,
|
||||
"href": href,
|
||||
}
|
||||
}
|
||||
|
||||
s.ParentType = oldParentType
|
||||
|
||||
s.Line = startLine + lines + 1
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,333 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/golang-commonmark/html"
|
||||
)
|
||||
|
||||
type Renderer struct {
|
||||
w *monadicWriter
|
||||
}
|
||||
|
||||
func NewRenderer(w io.Writer) *Renderer {
|
||||
return &Renderer{newMonadicWriter(w)}
|
||||
}
|
||||
|
||||
func (r *Renderer) Render(tokens []Token, options RenderOptions) error {
|
||||
for i, tok := range tokens {
|
||||
if tok, ok := tok.(*Inline); ok {
|
||||
r.renderInline(tok.Children, options)
|
||||
} else {
|
||||
r.renderToken(tokens, i, options)
|
||||
}
|
||||
}
|
||||
|
||||
r.w.Flush()
|
||||
|
||||
return r.w.err
|
||||
}
|
||||
|
||||
func (r *Renderer) renderInline(tokens []Token, o RenderOptions) {
|
||||
for i := range tokens {
|
||||
r.renderToken(tokens, i, o)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) renderInlineAsText(tokens []Token) {
|
||||
for _, tok := range tokens {
|
||||
if text, ok := tok.(*Text); ok {
|
||||
html.WriteEscapedString(r.w, text.Content)
|
||||
} else if img, ok := tok.(*Image); ok {
|
||||
r.renderInlineAsText(img.Tokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var rNotSpace = regexp.MustCompile(`^\S+`)
|
||||
|
||||
func (r *Renderer) renderToken(tokens []Token, idx int, options RenderOptions) {
|
||||
tok := tokens[idx]
|
||||
|
||||
if idx > 0 && tok.Block() && !tok.Closing() {
|
||||
switch t := tokens[idx-1].(type) {
|
||||
case *ParagraphOpen:
|
||||
if t.Hidden {
|
||||
r.w.WriteByte('\n')
|
||||
}
|
||||
case *ParagraphClose:
|
||||
if t.Hidden {
|
||||
r.w.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch tok := tok.(type) {
|
||||
case *BlockquoteClose:
|
||||
r.w.WriteString("</blockquote>")
|
||||
|
||||
case *BlockquoteOpen:
|
||||
r.w.WriteString("<blockquote>")
|
||||
|
||||
case *BulletListClose:
|
||||
r.w.WriteString("</ul>")
|
||||
|
||||
case *BulletListOpen:
|
||||
r.w.WriteString("<ul>")
|
||||
|
||||
case *CodeBlock:
|
||||
r.w.WriteString("<pre><code>")
|
||||
html.WriteEscapedString(r.w, tok.Content)
|
||||
r.w.WriteString("</code></pre>")
|
||||
|
||||
case *CodeInline:
|
||||
r.w.WriteString("<code>")
|
||||
html.WriteEscapedString(r.w, tok.Content)
|
||||
r.w.WriteString("</code>")
|
||||
|
||||
case *EmphasisClose:
|
||||
r.w.WriteString("</em>")
|
||||
|
||||
case *EmphasisOpen:
|
||||
r.w.WriteString("<em>")
|
||||
|
||||
case *Fence:
|
||||
r.w.WriteString("<pre><code")
|
||||
if tok.Params != "" {
|
||||
langName := strings.SplitN(unescapeAll(tok.Params), " ", 2)[0]
|
||||
langName = rNotSpace.FindString(langName)
|
||||
if langName != "" {
|
||||
r.w.WriteString(` class="`)
|
||||
r.w.WriteString(options.LangPrefix)
|
||||
html.WriteEscapedString(r.w, langName)
|
||||
r.w.WriteByte('"')
|
||||
}
|
||||
}
|
||||
r.w.WriteByte('>')
|
||||
html.WriteEscapedString(r.w, tok.Content)
|
||||
r.w.WriteString("</code></pre>")
|
||||
|
||||
case *Hardbreak:
|
||||
if options.XHTML {
|
||||
r.w.WriteString("<br />\n")
|
||||
} else {
|
||||
r.w.WriteString("<br>\n")
|
||||
}
|
||||
|
||||
case *HeadingClose:
|
||||
r.w.WriteString("</h")
|
||||
r.w.WriteByte("0123456789"[tok.HLevel])
|
||||
r.w.WriteString(">")
|
||||
|
||||
case *HeadingOpen:
|
||||
r.w.WriteString("<h")
|
||||
r.w.WriteByte("0123456789"[tok.HLevel])
|
||||
r.w.WriteByte('>')
|
||||
|
||||
case *Hr:
|
||||
if options.XHTML {
|
||||
r.w.WriteString("<hr />")
|
||||
} else {
|
||||
r.w.WriteString("<hr>")
|
||||
}
|
||||
|
||||
case *HTMLBlock:
|
||||
r.w.WriteString(tok.Content)
|
||||
return // no newline
|
||||
|
||||
case *HTMLInline:
|
||||
r.w.WriteString(tok.Content)
|
||||
|
||||
case *Image:
|
||||
r.w.WriteString(`<img src="`)
|
||||
html.WriteEscapedString(r.w, tok.Src)
|
||||
r.w.WriteString(`" alt="`)
|
||||
r.renderInlineAsText(tok.Tokens)
|
||||
r.w.WriteByte('"')
|
||||
|
||||
if tok.Title != "" {
|
||||
r.w.WriteString(` title="`)
|
||||
html.WriteEscapedString(r.w, tok.Title)
|
||||
r.w.WriteByte('"')
|
||||
}
|
||||
if options.XHTML {
|
||||
r.w.WriteString(" />")
|
||||
} else {
|
||||
r.w.WriteByte('>')
|
||||
}
|
||||
|
||||
case *LinkClose:
|
||||
r.w.WriteString("</a>")
|
||||
|
||||
case *LinkOpen:
|
||||
r.w.WriteString(`<a href="`)
|
||||
html.WriteEscapedString(r.w, tok.Href)
|
||||
r.w.WriteByte('"')
|
||||
if tok.Title != "" {
|
||||
r.w.WriteString(` title="`)
|
||||
html.WriteEscapedString(r.w, (tok.Title))
|
||||
r.w.WriteByte('"')
|
||||
}
|
||||
if tok.Target != "" {
|
||||
r.w.WriteString(` target="`)
|
||||
html.WriteEscapedString(r.w, tok.Target)
|
||||
r.w.WriteByte('"')
|
||||
}
|
||||
if options.Nofollow {
|
||||
r.w.WriteString(` rel="nofollow"`)
|
||||
}
|
||||
r.w.WriteByte('>')
|
||||
|
||||
case *ListItemClose:
|
||||
r.w.WriteString("</li>")
|
||||
|
||||
case *ListItemOpen:
|
||||
r.w.WriteString("<li>")
|
||||
|
||||
case *OrderedListClose:
|
||||
r.w.WriteString("</ol>")
|
||||
|
||||
case *OrderedListOpen:
|
||||
if tok.Order != 1 {
|
||||
r.w.WriteString(`<ol start="`)
|
||||
r.w.WriteString(strconv.Itoa(tok.Order))
|
||||
r.w.WriteString(`">`)
|
||||
} else {
|
||||
r.w.WriteString("<ol>")
|
||||
}
|
||||
|
||||
case *ParagraphClose:
|
||||
if tok.Hidden {
|
||||
return
|
||||
}
|
||||
if !tok.Tight {
|
||||
r.w.WriteString("</p>")
|
||||
} else if tokens[idx+1].Closing() {
|
||||
return // no newline
|
||||
}
|
||||
|
||||
case *ParagraphOpen:
|
||||
if tok.Hidden {
|
||||
return
|
||||
}
|
||||
if !tok.Tight {
|
||||
r.w.WriteString("<p>")
|
||||
}
|
||||
|
||||
case *Softbreak:
|
||||
if options.Breaks {
|
||||
if options.XHTML {
|
||||
r.w.WriteString("<br />\n")
|
||||
} else {
|
||||
r.w.WriteString("<br>\n")
|
||||
}
|
||||
} else {
|
||||
r.w.WriteByte('\n')
|
||||
}
|
||||
return
|
||||
|
||||
case *StrongClose:
|
||||
r.w.WriteString("</strong>")
|
||||
|
||||
case *StrongOpen:
|
||||
r.w.WriteString("<strong>")
|
||||
|
||||
case *StrikethroughClose:
|
||||
r.w.WriteString("</s>")
|
||||
|
||||
case *StrikethroughOpen:
|
||||
r.w.WriteString("<s>")
|
||||
|
||||
case *TableClose:
|
||||
r.w.WriteString("</table>")
|
||||
|
||||
case *TableOpen:
|
||||
r.w.WriteString("<table>")
|
||||
|
||||
case *TbodyClose:
|
||||
r.w.WriteString("</tbody>")
|
||||
|
||||
case *TbodyOpen:
|
||||
r.w.WriteString("<tbody>")
|
||||
|
||||
case *TdClose:
|
||||
r.w.WriteString("</td>")
|
||||
|
||||
case *TdOpen:
|
||||
if tok.Align != AlignNone {
|
||||
r.w.WriteString(`<td style="text-align:`)
|
||||
r.w.WriteString(tok.Align.String())
|
||||
r.w.WriteString(`">`)
|
||||
} else {
|
||||
r.w.WriteString("<td>")
|
||||
}
|
||||
|
||||
case *Text:
|
||||
html.WriteEscapedString(r.w, tok.Content)
|
||||
|
||||
case *TheadClose:
|
||||
r.w.WriteString("</thead>")
|
||||
|
||||
case *TheadOpen:
|
||||
r.w.WriteString("<thead>")
|
||||
|
||||
case *ThClose:
|
||||
r.w.WriteString("</th>")
|
||||
|
||||
case *ThOpen:
|
||||
if align := tok.Align; align != AlignNone {
|
||||
r.w.WriteString(`<th style="text-align:`)
|
||||
r.w.WriteString(align.String())
|
||||
r.w.WriteString(`">`)
|
||||
} else {
|
||||
r.w.WriteString("<th>")
|
||||
}
|
||||
|
||||
case *TrClose:
|
||||
r.w.WriteString("</tr>")
|
||||
|
||||
case *TrOpen:
|
||||
r.w.WriteString("<tr>")
|
||||
|
||||
default:
|
||||
panic("unknown token type")
|
||||
}
|
||||
|
||||
needLf := false
|
||||
if tok.Block() {
|
||||
needLf = true
|
||||
|
||||
if tok.Opening() {
|
||||
nextTok := tokens[idx+1]
|
||||
blockquote := false
|
||||
switch nextTok := nextTok.(type) {
|
||||
case *Inline:
|
||||
needLf = false
|
||||
case *ParagraphOpen:
|
||||
if nextTok.Tight || nextTok.Hidden {
|
||||
needLf = false
|
||||
}
|
||||
case *ParagraphClose:
|
||||
if nextTok.Tight || nextTok.Hidden {
|
||||
needLf = false
|
||||
}
|
||||
case *BlockquoteClose:
|
||||
blockquote = true
|
||||
}
|
||||
if !blockquote && needLf && nextTok.Closing() && nextTok.Tag() == tok.Tag() {
|
||||
needLf = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needLf {
|
||||
r.w.WriteByte('\n')
|
||||
}
|
||||
}
|
|
@ -1,229 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "strings"
|
||||
|
||||
func exclquest(b byte) bool {
|
||||
return b == '!' || b == '?'
|
||||
}
|
||||
|
||||
func byteToLower(b byte) byte {
|
||||
if b >= 'A' && b <= 'Z' {
|
||||
return b - 'A' + 'a'
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
var replChar = [256]bool{
|
||||
'(': true,
|
||||
'!': true,
|
||||
'+': true,
|
||||
',': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'?': true,
|
||||
}
|
||||
|
||||
func performReplacements(s string) string {
|
||||
var ss []string
|
||||
|
||||
start := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
b := s[i]
|
||||
|
||||
if replChar[b] {
|
||||
|
||||
outer:
|
||||
switch b {
|
||||
case '(':
|
||||
if i+2 >= len(s) {
|
||||
break
|
||||
}
|
||||
|
||||
b2 := s[i+1]
|
||||
|
||||
b2 = byteToLower(b2)
|
||||
switch b2 {
|
||||
case 'c', 'r', 'p':
|
||||
if s[i+2] != ')' {
|
||||
break outer
|
||||
}
|
||||
switch b2 {
|
||||
case 'c':
|
||||
if start < i {
|
||||
ss = append(ss, s[start:i])
|
||||
}
|
||||
ss = append(ss, "©")
|
||||
case 'r':
|
||||
if start < i {
|
||||
ss = append(ss, s[start:i])
|
||||
}
|
||||
ss = append(ss, "®")
|
||||
case 'p':
|
||||
if start < i {
|
||||
ss = append(ss, s[start:i])
|
||||
}
|
||||
ss = append(ss, "§")
|
||||
}
|
||||
i += 2
|
||||
start = i + 1
|
||||
continue
|
||||
|
||||
case 't':
|
||||
if i+3 >= len(s) {
|
||||
break outer
|
||||
}
|
||||
if s[i+3] != ')' || byteToLower(s[i+2]) != 'm' {
|
||||
break outer
|
||||
}
|
||||
if start < i {
|
||||
ss = append(ss, s[start:i])
|
||||
}
|
||||
ss = append(ss, "™")
|
||||
i += 3
|
||||
start = i + 1
|
||||
continue
|
||||
default:
|
||||
break outer
|
||||
}
|
||||
|
||||
case '+':
|
||||
if i+1 >= len(s) || s[i+1] != '-' {
|
||||
break
|
||||
}
|
||||
if start < i {
|
||||
ss = append(ss, s[start:i])
|
||||
}
|
||||
ss = append(ss, "±")
|
||||
i++
|
||||
start = i + 1
|
||||
continue
|
||||
|
||||
case '.':
|
||||
if i+1 >= len(s) || s[i+1] != '.' {
|
||||
break
|
||||
}
|
||||
|
||||
j := i + 2
|
||||
for j < len(s) && s[j] == '.' {
|
||||
j++
|
||||
}
|
||||
if start < i {
|
||||
ss = append(ss, s[start:i])
|
||||
}
|
||||
if i == 0 || !(s[i-1] == '?' || s[i-1] == '!') {
|
||||
ss = append(ss, "…")
|
||||
} else {
|
||||
ss = append(ss, "..")
|
||||
}
|
||||
i = j - 1
|
||||
start = i + 1
|
||||
continue
|
||||
|
||||
case '?', '!':
|
||||
if i+3 >= len(s) {
|
||||
break
|
||||
}
|
||||
if !(exclquest(s[i+1]) && exclquest(s[i+2]) && exclquest(s[i+3])) {
|
||||
break
|
||||
}
|
||||
if start < i {
|
||||
ss = append(ss, s[start:i])
|
||||
}
|
||||
ss = append(ss, s[i:i+3])
|
||||
j := i + 3
|
||||
for j < len(s) && exclquest(s[j]) {
|
||||
j++
|
||||
}
|
||||
i = j - 1
|
||||
start = i + 1
|
||||
continue
|
||||
|
||||
case ',':
|
||||
if i+1 >= len(s) || s[i+1] != ',' {
|
||||
break
|
||||
}
|
||||
if start < i {
|
||||
ss = append(ss, s[start:i])
|
||||
}
|
||||
ss = append(ss, ",")
|
||||
j := i + 2
|
||||
for j < len(s) && s[j] == ',' {
|
||||
j++
|
||||
}
|
||||
i = j - 1
|
||||
start = i + 1
|
||||
continue
|
||||
|
||||
case '-':
|
||||
if i+1 >= len(s) || s[i+1] != '-' {
|
||||
break
|
||||
}
|
||||
if i+2 >= len(s) || s[i+2] != '-' {
|
||||
if start < i {
|
||||
ss = append(ss, s[start:i])
|
||||
}
|
||||
ss = append(ss, "–")
|
||||
i++
|
||||
start = i + 1
|
||||
continue
|
||||
}
|
||||
if i+3 >= len(s) || s[i+3] != '-' {
|
||||
if start < i {
|
||||
ss = append(ss, s[start:i])
|
||||
}
|
||||
ss = append(ss, "—")
|
||||
i += 2
|
||||
start = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
j := i + 3
|
||||
for j < len(s) && s[j] == '-' {
|
||||
j++
|
||||
}
|
||||
if start < i {
|
||||
ss = append(ss, s[start:i])
|
||||
}
|
||||
ss = append(ss, s[i:j])
|
||||
i = j - 1
|
||||
start = i + 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if ss == nil {
|
||||
return s
|
||||
}
|
||||
if start < len(s) {
|
||||
ss = append(ss, s[start:])
|
||||
}
|
||||
return strings.Join(ss, "")
|
||||
}
|
||||
|
||||
func ruleReplacements(s *StateCore) {
|
||||
if !s.Md.Typographer {
|
||||
return
|
||||
}
|
||||
|
||||
insideLink := false
|
||||
for _, tok := range s.Tokens {
|
||||
if tok, ok := tok.(*Inline); ok {
|
||||
for _, itok := range tok.Children {
|
||||
switch itok := itok.(type) {
|
||||
case *LinkOpen:
|
||||
insideLink = true
|
||||
case *LinkClose:
|
||||
insideLink = false
|
||||
case *Text:
|
||||
if !insideLink {
|
||||
itok.Content = performReplacements(itok.Content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,256 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func nextQuoteIndex(s []rune, from int) int {
|
||||
for i := from; i < len(s); i++ {
|
||||
r := s[i]
|
||||
if r == '\'' || r == '"' {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func firstRune(s string) rune {
|
||||
for _, r := range s {
|
||||
return r
|
||||
}
|
||||
return utf8.RuneError
|
||||
}
|
||||
|
||||
func replaceQuotes(tokens []Token, s *StateCore) {
|
||||
type stackItem struct {
|
||||
token int
|
||||
text []rune
|
||||
pos int
|
||||
single bool
|
||||
level int
|
||||
}
|
||||
var stack []stackItem
|
||||
var changed map[int][]rune
|
||||
|
||||
for i, tok := range tokens {
|
||||
thisLevel := tok.Level()
|
||||
|
||||
j := len(stack) - 1
|
||||
for j >= 0 {
|
||||
if stack[j].level <= thisLevel {
|
||||
break
|
||||
}
|
||||
j--
|
||||
}
|
||||
stack = stack[:j+1]
|
||||
|
||||
tok, ok := tok.(*Text)
|
||||
if !ok || !strings.ContainsAny(tok.Content, `"'`) {
|
||||
continue
|
||||
}
|
||||
|
||||
text := []rune(tok.Content)
|
||||
pos := 0
|
||||
max := len(text)
|
||||
|
||||
loop:
|
||||
for pos < max {
|
||||
index := nextQuoteIndex(text, pos)
|
||||
if index < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
canOpen := true
|
||||
canClose := true
|
||||
pos = index + 1
|
||||
isSingle := text[index] == '\''
|
||||
|
||||
lastChar := ' '
|
||||
if index-1 > 0 {
|
||||
lastChar = text[index-1]
|
||||
} else {
|
||||
loop1:
|
||||
for j := i - 1; j >= 0; j-- {
|
||||
switch tok := tokens[j].(type) {
|
||||
case *Softbreak:
|
||||
break loop1
|
||||
case *Hardbreak:
|
||||
break loop1
|
||||
case *Text:
|
||||
lastChar, _ = utf8.DecodeLastRuneInString(tok.Content)
|
||||
break loop1
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextChar := ' '
|
||||
if pos < max {
|
||||
nextChar = text[pos]
|
||||
} else {
|
||||
loop2:
|
||||
for j := i + 1; j < len(tokens); j++ {
|
||||
switch tok := tokens[j].(type) {
|
||||
case *Softbreak:
|
||||
break loop2
|
||||
case *Hardbreak:
|
||||
break loop2
|
||||
case *Text:
|
||||
nextChar, _ = utf8.DecodeRuneInString(tok.Content)
|
||||
break loop2
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isLastPunct := isMdAsciiPunct(lastChar) || unicode.IsPunct(lastChar)
|
||||
isNextPunct := isMdAsciiPunct(nextChar) || unicode.IsPunct(nextChar)
|
||||
isLastWhiteSpace := unicode.IsSpace(lastChar)
|
||||
isNextWhiteSpace := unicode.IsSpace(nextChar)
|
||||
|
||||
if isNextWhiteSpace {
|
||||
canOpen = false
|
||||
} else if isNextPunct {
|
||||
if !(isLastWhiteSpace || isLastPunct) {
|
||||
canOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
if isLastWhiteSpace {
|
||||
canClose = false
|
||||
} else if isLastPunct {
|
||||
if !(isNextWhiteSpace || isNextPunct) {
|
||||
canClose = false
|
||||
}
|
||||
}
|
||||
|
||||
if nextChar == '"' && text[index] == '"' {
|
||||
if lastChar >= '0' && lastChar <= '9' {
|
||||
canClose = false
|
||||
canOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
if canOpen && canClose {
|
||||
canOpen = false
|
||||
canClose = isNextPunct
|
||||
}
|
||||
|
||||
if !canOpen && !canClose {
|
||||
if isSingle {
|
||||
text[index] = '’'
|
||||
if changed == nil {
|
||||
changed = make(map[int][]rune)
|
||||
}
|
||||
changed[i] = text
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if canClose {
|
||||
for j := len(stack) - 1; j >= 0; j-- {
|
||||
item := stack[j]
|
||||
if item.level < thisLevel {
|
||||
break
|
||||
}
|
||||
if item.single == isSingle && item.level == thisLevel {
|
||||
if changed == nil {
|
||||
changed = make(map[int][]rune)
|
||||
}
|
||||
|
||||
var q1, q2 string
|
||||
if isSingle {
|
||||
q1 = s.Md.options.Quotes[2]
|
||||
q2 = s.Md.options.Quotes[3]
|
||||
} else {
|
||||
q1 = s.Md.options.Quotes[0]
|
||||
q2 = s.Md.options.Quotes[1]
|
||||
}
|
||||
|
||||
if utf8.RuneCountInString(q1) == 1 && utf8.RuneCountInString(q2) == 1 {
|
||||
item.text[item.pos] = firstRune(q1)
|
||||
text[index] = firstRune(q2)
|
||||
} else if tok == tokens[item.token] {
|
||||
newText := make([]rune, 0, len(text)-2+len(q1)+len(q2))
|
||||
newText = append(newText, text[:item.pos]...)
|
||||
newText = append(newText, []rune(q1)...)
|
||||
newText = append(newText, text[item.pos+1:index]...)
|
||||
newText = append(newText, []rune(q2)...)
|
||||
newText = append(newText, text[index+1:]...)
|
||||
|
||||
text = newText
|
||||
item.text = newText
|
||||
} else {
|
||||
newText := make([]rune, 0, len(item.text)-1+len(q1))
|
||||
newText = append(newText, item.text[:item.pos]...)
|
||||
newText = append(newText, []rune(q1)...)
|
||||
newText = append(newText, item.text[item.pos+1:]...)
|
||||
item.text = newText
|
||||
|
||||
newText = make([]rune, 0, len(text)-1+len(q2))
|
||||
newText = append(newText, text[:index]...)
|
||||
newText = append(newText, []rune(q2)...)
|
||||
newText = append(newText, text[index+1:]...)
|
||||
|
||||
text = newText
|
||||
}
|
||||
|
||||
max = len(text)
|
||||
|
||||
if changed == nil {
|
||||
changed = make(map[int][]rune)
|
||||
}
|
||||
changed[i] = text
|
||||
changed[item.token] = item.text
|
||||
stack = stack[:j]
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if canOpen {
|
||||
stack = append(stack, stackItem{
|
||||
token: i,
|
||||
text: text,
|
||||
pos: index,
|
||||
single: isSingle,
|
||||
level: thisLevel,
|
||||
})
|
||||
} else if canClose && isSingle {
|
||||
text[index] = '’'
|
||||
if changed == nil {
|
||||
changed = make(map[int][]rune)
|
||||
}
|
||||
changed[i] = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changed != nil {
|
||||
for i, text := range changed {
|
||||
tokens[i].(*Text).Content = string(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ruleSmartQuotes(s *StateCore) {
|
||||
if !s.Md.Typographer {
|
||||
return
|
||||
}
|
||||
|
||||
tokens := s.Tokens
|
||||
for i := len(tokens) - 1; i >= 0; i-- {
|
||||
tok := tokens[i]
|
||||
if tok, ok := tok.(*Inline); ok {
|
||||
replaceQuotes(tok.Children, s)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
ptRoot = iota
|
||||
ptList
|
||||
ptBlockQuote
|
||||
ptParagraph
|
||||
ptReference
|
||||
)
|
||||
|
||||
type StateBlock struct {
|
||||
StateCore
|
||||
|
||||
BMarks []int // offsets of the line beginnings
|
||||
EMarks []int // offsets of the line endings
|
||||
TShift []int // indents for each line
|
||||
SCount []int
|
||||
BSCount []int
|
||||
BlkIndent int // required block content indent (in a list etc.)
|
||||
Line int // line index in the source string
|
||||
LineMax int // number of lines
|
||||
Tight bool // loose or tight mode for lists
|
||||
ParentType byte // parent block type
|
||||
Level int
|
||||
}
|
||||
|
||||
func (s *StateBlock) IsLineEmpty(n int) bool {
|
||||
return s.BMarks[n]+s.TShift[n] >= s.EMarks[n]
|
||||
}
|
||||
|
||||
func (s *StateBlock) SkipEmptyLines(from int) int {
|
||||
for from < s.LineMax && s.IsLineEmpty(from) {
|
||||
from++
|
||||
}
|
||||
return from
|
||||
}
|
||||
|
||||
func (s *StateBlock) SkipSpaces(pos int) int {
|
||||
src := s.Src
|
||||
for pos < len(src) && byteIsSpace(src[pos]) {
|
||||
pos++
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
func (s *StateBlock) SkipBytes(pos int, b byte) int {
|
||||
src := s.Src
|
||||
for pos < len(src) && src[pos] == b {
|
||||
pos++
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
func (s *StateBlock) SkipBytesBack(pos int, b byte, min int) int {
|
||||
for pos > min {
|
||||
pos--
|
||||
if s.Src[pos] != b {
|
||||
return pos + 1
|
||||
}
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
func (s *StateBlock) SkipSpacesBack(pos int, min int) int {
|
||||
for pos > min {
|
||||
pos--
|
||||
if !byteIsSpace(s.Src[pos]) {
|
||||
return pos + 1
|
||||
}
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
func (s *StateBlock) Lines(begin, end, indent int, keepLastLf bool) string {
|
||||
if begin == end {
|
||||
return ""
|
||||
}
|
||||
|
||||
src := s.Src
|
||||
|
||||
queue := make([]string, end-begin)
|
||||
|
||||
for i, line := 0, begin; line < end; i, line = i+1, line+1 {
|
||||
lineIndent := 0
|
||||
lineStart := s.BMarks[line]
|
||||
first := lineStart
|
||||
last := s.EMarks[line]
|
||||
if (line+1 < end || keepLastLf) && last < len(src) {
|
||||
last++
|
||||
}
|
||||
|
||||
for first < last && lineIndent < indent {
|
||||
ch := src[first]
|
||||
|
||||
if byteIsSpace(ch) {
|
||||
if ch == '\t' {
|
||||
lineIndent += 4 - (lineIndent+s.BSCount[line])%4
|
||||
} else {
|
||||
lineIndent++
|
||||
}
|
||||
} else if first-lineStart < s.TShift[line] {
|
||||
lineIndent++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
first++
|
||||
}
|
||||
|
||||
if lineIndent > indent {
|
||||
queue[i] = strings.Repeat(" ", lineIndent-indent) + src[first:last]
|
||||
} else {
|
||||
queue[i] = src[first:last]
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(queue, "")
|
||||
}
|
||||
|
||||
func (s *StateBlock) PushToken(tok Token) {
|
||||
tok.SetLevel(s.Level)
|
||||
s.Tokens = append(s.Tokens, tok)
|
||||
}
|
||||
|
||||
func (s *StateBlock) PushOpeningToken(tok Token) {
|
||||
tok.SetLevel(s.Level)
|
||||
s.Level++
|
||||
s.Tokens = append(s.Tokens, tok)
|
||||
}
|
||||
|
||||
func (s *StateBlock) PushClosingToken(tok Token) {
|
||||
s.Level--
|
||||
tok.SetLevel(s.Level)
|
||||
s.Tokens = append(s.Tokens, tok)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
type StateCore struct {
|
||||
Src string
|
||||
Tokens []Token
|
||||
bootstrap [3]Token
|
||||
Md *Markdown
|
||||
Env *Environment
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type StateInline struct {
|
||||
StateCore
|
||||
|
||||
Pos int
|
||||
PosMax int
|
||||
Level int
|
||||
Pending bytes.Buffer
|
||||
PendingLevel int
|
||||
Delimiters []Delimiter
|
||||
|
||||
Cache map[int]int
|
||||
}
|
||||
|
||||
func (s *StateInline) PushToken(tok Token) {
|
||||
if s.Pending.Len() > 0 {
|
||||
s.PushPending()
|
||||
}
|
||||
tok.SetLevel(s.Level)
|
||||
s.PendingLevel = s.Level
|
||||
s.Tokens = append(s.Tokens, tok)
|
||||
}
|
||||
|
||||
func (s *StateInline) PushOpeningToken(tok Token) {
|
||||
if s.Pending.Len() > 0 {
|
||||
s.PushPending()
|
||||
}
|
||||
tok.SetLevel(s.Level)
|
||||
s.Level++
|
||||
s.PendingLevel = s.Level
|
||||
s.Tokens = append(s.Tokens, tok)
|
||||
}
|
||||
|
||||
func (s *StateInline) PushClosingToken(tok Token) {
|
||||
if s.Pending.Len() > 0 {
|
||||
s.PushPending()
|
||||
}
|
||||
s.Level--
|
||||
tok.SetLevel(s.Level)
|
||||
s.PendingLevel = s.Level
|
||||
s.Tokens = append(s.Tokens, tok)
|
||||
}
|
||||
|
||||
func (s *StateInline) PushPending() {
|
||||
s.Tokens = append(s.Tokens, &Text{
|
||||
Content: s.Pending.String(),
|
||||
Lvl: s.PendingLevel,
|
||||
})
|
||||
s.Pending.Reset()
|
||||
}
|
||||
|
||||
func (s *StateInline) scanDelims(start int, canSplitWord bool) (canOpen bool, canClose bool, length int) {
|
||||
pos := start
|
||||
max := s.PosMax
|
||||
src := s.Src
|
||||
marker := src[start]
|
||||
leftFlanking, rightFlanking := true, true
|
||||
|
||||
lastChar := ' '
|
||||
if start > 0 {
|
||||
lastChar, _ = utf8.DecodeLastRuneInString(src[:start])
|
||||
}
|
||||
|
||||
for pos < max && src[pos] == marker {
|
||||
pos++
|
||||
}
|
||||
length = pos - start
|
||||
|
||||
nextChar := ' '
|
||||
if pos < max {
|
||||
nextChar, _ = utf8.DecodeRuneInString(src[pos:])
|
||||
}
|
||||
|
||||
isLastPunct := isMdAsciiPunct(lastChar) || unicode.IsPunct(lastChar)
|
||||
isNextPunct := isMdAsciiPunct(nextChar) || unicode.IsPunct(nextChar)
|
||||
|
||||
isLastWhiteSpace := unicode.IsSpace(lastChar)
|
||||
isNextWhiteSpace := unicode.IsSpace(nextChar)
|
||||
|
||||
if isNextWhiteSpace {
|
||||
leftFlanking = false
|
||||
} else if isNextPunct {
|
||||
if !(isLastWhiteSpace || isLastPunct) {
|
||||
leftFlanking = false
|
||||
}
|
||||
}
|
||||
|
||||
if isLastWhiteSpace {
|
||||
rightFlanking = false
|
||||
} else if isLastPunct {
|
||||
if !(isNextWhiteSpace || isNextPunct) {
|
||||
rightFlanking = false
|
||||
}
|
||||
}
|
||||
|
||||
if !canSplitWord {
|
||||
canOpen = leftFlanking && (!rightFlanking || isLastPunct)
|
||||
canClose = rightFlanking && (!leftFlanking || isNextPunct)
|
||||
} else {
|
||||
canOpen = leftFlanking
|
||||
canClose = rightFlanking
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
func ruleStrikeThrough(s *StateInline, silent bool) bool {
|
||||
src := s.Src
|
||||
start := s.Pos
|
||||
marker := src[start]
|
||||
|
||||
if silent {
|
||||
return false
|
||||
}
|
||||
|
||||
if src[start] != '~' {
|
||||
return false
|
||||
}
|
||||
|
||||
canOpen, canClose, length := s.scanDelims(start, true)
|
||||
origLength := length
|
||||
ch := string(marker)
|
||||
if length < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
if length%2 != 0 {
|
||||
s.PushToken(&Text{
|
||||
Content: ch,
|
||||
})
|
||||
length--
|
||||
}
|
||||
|
||||
for i := 0; i < length; i += 2 {
|
||||
s.PushToken(&Text{
|
||||
Content: ch + ch,
|
||||
})
|
||||
|
||||
s.Delimiters = append(s.Delimiters, Delimiter{
|
||||
Marker: marker,
|
||||
Length: -1,
|
||||
Jump: i,
|
||||
Token: len(s.Tokens) - 1,
|
||||
Level: s.Level,
|
||||
End: -1,
|
||||
Open: canOpen,
|
||||
Close: canClose,
|
||||
})
|
||||
}
|
||||
|
||||
s.Pos += origLength
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
func ruleStrikethroughPostprocess(s *StateInline) {
|
||||
var loneMarkers []int
|
||||
delimiters := s.Delimiters
|
||||
max := len(delimiters)
|
||||
|
||||
for i := 0; i < max; i++ {
|
||||
startDelim := delimiters[i]
|
||||
|
||||
if startDelim.Marker != '~' {
|
||||
continue
|
||||
}
|
||||
|
||||
if startDelim.End == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
endDelim := delimiters[startDelim.End]
|
||||
|
||||
s.Tokens[startDelim.Token] = &StrikethroughOpen{}
|
||||
s.Tokens[endDelim.Token] = &StrikethroughClose{}
|
||||
|
||||
if text, ok := s.Tokens[endDelim.Token-1].(*Text); ok && text.Content == "~" {
|
||||
loneMarkers = append(loneMarkers, endDelim.Token-1)
|
||||
}
|
||||
}
|
||||
|
||||
for len(loneMarkers) > 0 {
|
||||
i := loneMarkers[len(loneMarkers)-1]
|
||||
loneMarkers = loneMarkers[:len(loneMarkers)-1]
|
||||
j := i + 1
|
||||
|
||||
for j < len(s.Tokens) {
|
||||
if _, ok := s.Tokens[j].(*StrikethroughClose); !ok {
|
||||
break
|
||||
}
|
||||
j++
|
||||
}
|
||||
|
||||
j--
|
||||
|
||||
if i != j {
|
||||
s.Tokens[i], s.Tokens[j] = s.Tokens[j], s.Tokens[i]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,229 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getLine(s *StateBlock, line int) string {
|
||||
pos := s.BMarks[line] + s.BlkIndent
|
||||
max := s.EMarks[line]
|
||||
if pos >= max {
|
||||
return ""
|
||||
}
|
||||
return s.Src[pos:max]
|
||||
}
|
||||
|
||||
func escapedSplit(s string) (result []string) {
|
||||
pos := 0
|
||||
escapes := 0
|
||||
lastPos := 0
|
||||
backTicked := false
|
||||
lastBackTick := 0
|
||||
|
||||
for pos < len(s) {
|
||||
ch := s[pos]
|
||||
if ch == '`' {
|
||||
if backTicked {
|
||||
backTicked = false
|
||||
lastBackTick = pos
|
||||
} else if escapes%2 == 0 {
|
||||
backTicked = true
|
||||
lastBackTick = pos
|
||||
}
|
||||
} else if ch == '|' && (escapes%2 == 0) && !backTicked {
|
||||
result = append(result, s[lastPos:pos])
|
||||
lastPos = pos + 1
|
||||
}
|
||||
|
||||
if ch == '\\' {
|
||||
escapes++
|
||||
} else {
|
||||
escapes = 0
|
||||
}
|
||||
|
||||
pos++
|
||||
|
||||
if pos == len(s) && backTicked {
|
||||
backTicked = false
|
||||
pos = lastBackTick + 1
|
||||
}
|
||||
}
|
||||
|
||||
return append(result, s[lastPos:])
|
||||
}
|
||||
|
||||
var rColumn = regexp.MustCompile("^:?-+:?$")
|
||||
|
||||
func ruleTable(s *StateBlock, startLine, endLine int, silent bool) bool {
|
||||
if !s.Md.Tables {
|
||||
return false
|
||||
}
|
||||
|
||||
if startLine+2 > endLine {
|
||||
return false
|
||||
}
|
||||
|
||||
nextLine := startLine + 1
|
||||
|
||||
if s.SCount[nextLine] < s.BlkIndent {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.SCount[nextLine]-s.BlkIndent >= 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
pos := s.BMarks[nextLine] + s.TShift[nextLine]
|
||||
if pos >= s.EMarks[nextLine] {
|
||||
return false
|
||||
}
|
||||
|
||||
src := s.Src
|
||||
ch := src[pos]
|
||||
pos++
|
||||
|
||||
if ch != '|' && ch != '-' && ch != ':' {
|
||||
return false
|
||||
}
|
||||
|
||||
for pos < s.EMarks[nextLine] {
|
||||
ch = src[pos]
|
||||
if ch != '|' && ch != '-' && ch != ':' && !byteIsSpace(ch) {
|
||||
return false
|
||||
}
|
||||
pos++
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
lineText := getLine(s, startLine+1)
|
||||
|
||||
columns := strings.Split(lineText, "|")
|
||||
var aligns []Align
|
||||
for i := 0; i < len(columns); i++ {
|
||||
t := strings.TrimSpace(columns[i])
|
||||
if t == "" {
|
||||
if i == 0 || i == len(columns)-1 {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if !rColumn.MatchString(t) {
|
||||
return false
|
||||
}
|
||||
|
||||
if t[len(t)-1] == ':' {
|
||||
if t[0] == ':' {
|
||||
aligns = append(aligns, AlignCenter)
|
||||
} else {
|
||||
aligns = append(aligns, AlignRight)
|
||||
}
|
||||
} else if t[0] == ':' {
|
||||
aligns = append(aligns, AlignLeft)
|
||||
} else {
|
||||
aligns = append(aligns, AlignNone)
|
||||
}
|
||||
}
|
||||
|
||||
lineText = strings.TrimSpace(getLine(s, startLine))
|
||||
if strings.IndexByte(lineText, '|') == -1 {
|
||||
return false
|
||||
}
|
||||
if s.SCount[startLine]-s.BlkIndent >= 4 {
|
||||
return false
|
||||
}
|
||||
columns = escapedSplit(strings.TrimSuffix(strings.TrimPrefix(lineText, "|"), "|"))
|
||||
columnCount := len(columns)
|
||||
if columnCount > len(aligns) {
|
||||
return false
|
||||
}
|
||||
|
||||
if silent {
|
||||
return true
|
||||
}
|
||||
|
||||
tableTok := &TableOpen{
|
||||
Map: [2]int{startLine, 0},
|
||||
}
|
||||
s.PushOpeningToken(tableTok)
|
||||
s.PushOpeningToken(&TheadOpen{
|
||||
Map: [2]int{startLine, startLine + 1},
|
||||
})
|
||||
s.PushOpeningToken(&TrOpen{
|
||||
Map: [2]int{startLine, startLine + 1},
|
||||
})
|
||||
|
||||
for i := 0; i < len(columns); i++ {
|
||||
s.PushOpeningToken(&ThOpen{
|
||||
Align: aligns[i],
|
||||
Map: [2]int{startLine, startLine + 1},
|
||||
})
|
||||
s.PushToken(&Inline{
|
||||
Content: strings.TrimSpace(columns[i]),
|
||||
Map: [2]int{startLine, startLine + 1},
|
||||
})
|
||||
s.PushClosingToken(&ThClose{})
|
||||
}
|
||||
|
||||
s.PushClosingToken(&TrClose{})
|
||||
s.PushClosingToken(&TheadClose{})
|
||||
|
||||
tbodyTok := &TbodyOpen{
|
||||
Map: [2]int{startLine + 2, 0},
|
||||
}
|
||||
s.PushOpeningToken(tbodyTok)
|
||||
|
||||
for nextLine = startLine + 2; nextLine < endLine; nextLine++ {
|
||||
if s.SCount[nextLine] < s.BlkIndent {
|
||||
break
|
||||
}
|
||||
|
||||
lineText = strings.TrimSpace(getLine(s, nextLine))
|
||||
if strings.IndexByte(lineText, '|') == -1 {
|
||||
break
|
||||
}
|
||||
if s.SCount[nextLine]-s.BlkIndent >= 4 {
|
||||
break
|
||||
}
|
||||
columns = escapedSplit(strings.TrimPrefix(strings.TrimSuffix(lineText, "|"), "|"))
|
||||
|
||||
if len(columns) < len(aligns) {
|
||||
columns = append(columns, make([]string, len(aligns)-len(columns))...)
|
||||
} else if len(columns) > len(aligns) {
|
||||
columns = columns[:len(aligns)]
|
||||
}
|
||||
|
||||
s.PushOpeningToken(&TrOpen{})
|
||||
for i := 0; i < columnCount; i++ {
|
||||
tdOpen := TdOpen{}
|
||||
if i < len(aligns) {
|
||||
tdOpen.Align = aligns[i]
|
||||
}
|
||||
s.PushOpeningToken(&tdOpen)
|
||||
|
||||
inline := Inline{}
|
||||
if i < len(columns) {
|
||||
inline.Content = strings.TrimSpace(columns[i])
|
||||
}
|
||||
s.PushToken(&inline)
|
||||
|
||||
s.PushClosingToken(&TdClose{})
|
||||
}
|
||||
s.PushClosingToken(&TrClose{})
|
||||
}
|
||||
|
||||
s.PushClosingToken(&TbodyClose{})
|
||||
s.PushClosingToken(&TableClose{})
|
||||
|
||||
tableTok.Map[1] = nextLine
|
||||
tbodyTok.Map[1] = nextLine
|
||||
s.Line = nextLine
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
func isHeaderLine(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
st := 0
|
||||
n := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch st {
|
||||
case 0: // initial state
|
||||
switch b {
|
||||
case '|':
|
||||
st = 1
|
||||
case ':':
|
||||
st = 2
|
||||
case '-':
|
||||
st = 3
|
||||
n++
|
||||
case ' ':
|
||||
break
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
case 1: // |
|
||||
switch b {
|
||||
case ' ':
|
||||
break
|
||||
case ':':
|
||||
st = 2
|
||||
case '-':
|
||||
st = 3
|
||||
n++
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
case 2: // |:
|
||||
switch b {
|
||||
case ' ':
|
||||
break
|
||||
case '-':
|
||||
st = 3
|
||||
n++
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
case 3: // |:-
|
||||
switch b {
|
||||
case '-':
|
||||
break
|
||||
case ':':
|
||||
st = 4
|
||||
case '|':
|
||||
st = 5
|
||||
case ' ':
|
||||
st = 6
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
case 4: // |:---:
|
||||
switch b {
|
||||
case ' ':
|
||||
break
|
||||
case '|':
|
||||
st = 5
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
case 5: // |:---:|
|
||||
switch b {
|
||||
case ' ':
|
||||
break
|
||||
case ':':
|
||||
st = 2
|
||||
case '-':
|
||||
st = 3
|
||||
n++
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
case 6: // |:--- SPACE
|
||||
switch b {
|
||||
case ' ':
|
||||
break
|
||||
case ':':
|
||||
st = 4
|
||||
case '|':
|
||||
st = 5
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return n >= 1
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
var terminatorCharTable = [256]bool{
|
||||
'\n': true,
|
||||
'!': true,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
'-': true,
|
||||
':': true,
|
||||
'<': true,
|
||||
'=': true,
|
||||
'>': true,
|
||||
'@': true,
|
||||
'[': true,
|
||||
'\\': true,
|
||||
']': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'{': true,
|
||||
'}': true,
|
||||
'~': true,
|
||||
}
|
||||
|
||||
func ruleText(s *StateInline, silent bool) bool {
|
||||
pos := s.Pos
|
||||
max := s.PosMax
|
||||
src := s.Src
|
||||
|
||||
for pos < max && !terminatorCharTable[src[pos]] {
|
||||
pos++
|
||||
}
|
||||
if pos == s.Pos {
|
||||
return false
|
||||
}
|
||||
|
||||
if !silent {
|
||||
s.Pending.WriteString(src[s.Pos:pos])
|
||||
}
|
||||
|
||||
s.Pos = pos
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
func ruleTextCollapse(s *StateInline) {
|
||||
level := 0
|
||||
tokens := s.Tokens
|
||||
max := len(tokens)
|
||||
|
||||
curr := 0
|
||||
last := 0
|
||||
for ; curr < max; curr++ {
|
||||
tok := tokens[curr]
|
||||
if tok.Opening() {
|
||||
level++
|
||||
} else if tok.Closing() {
|
||||
level--
|
||||
}
|
||||
|
||||
tok.SetLevel(level)
|
||||
|
||||
if text, ok := tok.(*Text); ok && curr+1 < max {
|
||||
if text2, ok := tokens[curr+1].(*Text); ok {
|
||||
text2.Content = text.Content + text2.Content
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if curr != last {
|
||||
tokens[last] = tokens[curr]
|
||||
}
|
||||
last++
|
||||
}
|
||||
|
||||
if curr != last {
|
||||
tokens = tokens[:last]
|
||||
}
|
||||
|
||||
s.Tokens = tokens
|
||||
}
|
|
@ -1,753 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
type Token interface {
|
||||
Tag() string
|
||||
Opening() bool
|
||||
Closing() bool
|
||||
Block() bool
|
||||
Level() int
|
||||
SetLevel(lvl int)
|
||||
}
|
||||
|
||||
type BlockquoteOpen struct {
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type BlockquoteClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type BulletListOpen struct {
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type BulletListClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type OrderedListOpen struct {
|
||||
Order int
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type OrderedListClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type ListItemOpen struct {
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type ListItemClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type CodeBlock struct {
|
||||
Content string
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type CodeInline struct {
|
||||
Content string
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type EmphasisOpen struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type EmphasisClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type StrongOpen struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type StrongClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type StrikethroughOpen struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type StrikethroughClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type Fence struct {
|
||||
Params string
|
||||
Content string
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type Softbreak struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type Hardbreak struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type HeadingOpen struct {
|
||||
HLevel int
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type HeadingClose struct {
|
||||
HLevel int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type HTMLBlock struct {
|
||||
Content string
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type HTMLInline struct {
|
||||
Content string
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type Hr struct {
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Src string
|
||||
Title string
|
||||
Tokens []Token
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type Inline struct {
|
||||
Content string
|
||||
Map [2]int
|
||||
Children []Token
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type LinkOpen struct {
|
||||
Href string
|
||||
Title string
|
||||
Target string
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type LinkClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type ParagraphOpen struct {
|
||||
Tight bool
|
||||
Hidden bool
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type ParagraphClose struct {
|
||||
Tight bool
|
||||
Hidden bool
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type TableOpen struct {
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type TableClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type TheadOpen struct {
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type TheadClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type TrOpen struct {
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type TrClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type ThOpen struct {
|
||||
Align Align
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type ThClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type TbodyOpen struct {
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type TbodyClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type TdOpen struct {
|
||||
Align Align
|
||||
Map [2]int
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type TdClose struct {
|
||||
Lvl int
|
||||
}
|
||||
|
||||
type Text struct {
|
||||
Content string
|
||||
Lvl int
|
||||
}
|
||||
|
||||
var htags = []string{
|
||||
"",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
}
|
||||
|
||||
func (t *BlockquoteOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *BlockquoteClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *BulletListOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *BulletListClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *OrderedListOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *OrderedListClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *ListItemOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *ListItemClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *CodeBlock) Level() int { return t.Lvl }
|
||||
|
||||
func (t *CodeInline) Level() int { return t.Lvl }
|
||||
|
||||
func (t *EmphasisOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *EmphasisClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *StrongOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *StrongClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *StrikethroughOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *StrikethroughClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *Fence) Level() int { return t.Lvl }
|
||||
|
||||
func (t *Softbreak) Level() int { return t.Lvl }
|
||||
|
||||
func (t *Hardbreak) Level() int { return t.Lvl }
|
||||
|
||||
func (t *HeadingOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *HeadingClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *HTMLBlock) Level() int { return t.Lvl }
|
||||
|
||||
func (t *HTMLInline) Level() int { return t.Lvl }
|
||||
|
||||
func (t *Hr) Level() int { return t.Lvl }
|
||||
|
||||
func (t *Image) Level() int { return t.Lvl }
|
||||
|
||||
func (t *Inline) Level() int { return t.Lvl }
|
||||
|
||||
func (t *LinkOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *LinkClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *ParagraphOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *ParagraphClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *TableOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *TableClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *TheadOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *TheadClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *TrOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *TrClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *ThOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *ThClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *TbodyOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *TbodyClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *TdOpen) Level() int { return t.Lvl }
|
||||
|
||||
func (t *TdClose) Level() int { return t.Lvl }
|
||||
|
||||
func (t *Text) Level() int { return t.Lvl }
|
||||
|
||||
func (t *BlockquoteOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *BlockquoteClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *BulletListOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *BulletListClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *OrderedListOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *OrderedListClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *ListItemOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *ListItemClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *CodeBlock) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *CodeInline) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *EmphasisOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *EmphasisClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *StrongOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *StrongClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *StrikethroughOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *StrikethroughClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *Fence) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *Softbreak) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *Hardbreak) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *HeadingOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *HeadingClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *HTMLBlock) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *HTMLInline) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *Hr) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *Image) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *Inline) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *LinkOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *LinkClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *ParagraphOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *ParagraphClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *TableOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *TableClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *TheadOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *TheadClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *TrOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *TrClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *ThOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *ThClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *TbodyOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *TbodyClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *TdOpen) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *TdClose) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *Text) SetLevel(lvl int) { t.Lvl = lvl }
|
||||
|
||||
func (t *BlockquoteOpen) Opening() bool { return true }
|
||||
|
||||
func (t *BlockquoteClose) Opening() bool { return false }
|
||||
|
||||
func (t *BulletListOpen) Opening() bool { return true }
|
||||
|
||||
func (t *BulletListClose) Opening() bool { return false }
|
||||
|
||||
func (t *OrderedListOpen) Opening() bool { return true }
|
||||
|
||||
func (t *OrderedListClose) Opening() bool { return false }
|
||||
|
||||
func (t *ListItemOpen) Opening() bool { return true }
|
||||
|
||||
func (t *ListItemClose) Opening() bool { return false }
|
||||
|
||||
func (t *CodeBlock) Opening() bool { return false }
|
||||
|
||||
func (t *CodeInline) Opening() bool { return false }
|
||||
|
||||
func (t *EmphasisOpen) Opening() bool { return true }
|
||||
|
||||
func (t *EmphasisClose) Opening() bool { return false }
|
||||
|
||||
func (t *StrongOpen) Opening() bool { return true }
|
||||
|
||||
func (t *StrongClose) Opening() bool { return false }
|
||||
|
||||
func (t *StrikethroughOpen) Opening() bool { return true }
|
||||
|
||||
func (t *StrikethroughClose) Opening() bool { return false }
|
||||
|
||||
func (t *Fence) Opening() bool { return false }
|
||||
|
||||
func (t *Softbreak) Opening() bool { return false }
|
||||
|
||||
func (t *Hardbreak) Opening() bool { return false }
|
||||
|
||||
func (t *HeadingOpen) Opening() bool { return true }
|
||||
|
||||
func (t *HeadingClose) Opening() bool { return false }
|
||||
|
||||
func (t *HTMLBlock) Opening() bool { return false }
|
||||
|
||||
func (t *HTMLInline) Opening() bool { return false }
|
||||
|
||||
func (t *Hr) Opening() bool { return false }
|
||||
|
||||
func (t *Image) Opening() bool { return false }
|
||||
|
||||
func (t *Inline) Opening() bool { return false }
|
||||
|
||||
func (t *LinkOpen) Opening() bool { return true }
|
||||
|
||||
func (t *LinkClose) Opening() bool { return false }
|
||||
|
||||
func (t *ParagraphOpen) Opening() bool { return true }
|
||||
|
||||
func (t *ParagraphClose) Opening() bool { return false }
|
||||
|
||||
func (t *TableOpen) Opening() bool { return true }
|
||||
|
||||
func (t *TableClose) Opening() bool { return false }
|
||||
|
||||
func (t *TheadOpen) Opening() bool { return true }
|
||||
|
||||
func (t *TheadClose) Opening() bool { return false }
|
||||
|
||||
func (t *TrOpen) Opening() bool { return true }
|
||||
|
||||
func (t *TrClose) Opening() bool { return false }
|
||||
|
||||
func (t *ThOpen) Opening() bool { return true }
|
||||
|
||||
func (t *ThClose) Opening() bool { return false }
|
||||
|
||||
func (t *TbodyOpen) Opening() bool { return true }
|
||||
|
||||
func (t *TbodyClose) Opening() bool { return false }
|
||||
|
||||
func (t *TdOpen) Opening() bool { return true }
|
||||
|
||||
func (t *TdClose) Opening() bool { return false }
|
||||
|
||||
func (t *Text) Opening() bool { return false }
|
||||
|
||||
func (t *BlockquoteOpen) Closing() bool { return false }
|
||||
|
||||
func (t *BlockquoteClose) Closing() bool { return true }
|
||||
|
||||
func (t *BulletListOpen) Closing() bool { return false }
|
||||
|
||||
func (t *BulletListClose) Closing() bool { return true }
|
||||
|
||||
func (t *OrderedListOpen) Closing() bool { return false }
|
||||
|
||||
func (t *OrderedListClose) Closing() bool { return true }
|
||||
|
||||
func (t *ListItemOpen) Closing() bool { return false }
|
||||
|
||||
func (t *ListItemClose) Closing() bool { return true }
|
||||
|
||||
func (t *CodeBlock) Closing() bool { return false }
|
||||
|
||||
func (t *CodeInline) Closing() bool { return false }
|
||||
|
||||
func (t *EmphasisOpen) Closing() bool { return false }
|
||||
|
||||
func (t *EmphasisClose) Closing() bool { return true }
|
||||
|
||||
func (t *StrongOpen) Closing() bool { return false }
|
||||
|
||||
func (t *StrongClose) Closing() bool { return true }
|
||||
|
||||
func (t *StrikethroughOpen) Closing() bool { return false }
|
||||
|
||||
func (t *StrikethroughClose) Closing() bool { return true }
|
||||
|
||||
func (t *Fence) Closing() bool { return false }
|
||||
|
||||
func (t *Softbreak) Closing() bool { return false }
|
||||
|
||||
func (t *Hardbreak) Closing() bool { return false }
|
||||
|
||||
func (t *HeadingOpen) Closing() bool { return false }
|
||||
|
||||
func (t *HeadingClose) Closing() bool { return true }
|
||||
|
||||
func (t *HTMLBlock) Closing() bool { return false }
|
||||
|
||||
func (t *HTMLInline) Closing() bool { return false }
|
||||
|
||||
func (t *Hr) Closing() bool { return false }
|
||||
|
||||
func (t *Image) Closing() bool { return false }
|
||||
|
||||
func (t *Inline) Closing() bool { return false }
|
||||
|
||||
func (t *LinkOpen) Closing() bool { return false }
|
||||
|
||||
func (t *LinkClose) Closing() bool { return true }
|
||||
|
||||
func (t *ParagraphOpen) Closing() bool { return false }
|
||||
|
||||
func (t *ParagraphClose) Closing() bool { return true }
|
||||
|
||||
func (t *TableOpen) Closing() bool { return false }
|
||||
|
||||
func (t *TableClose) Closing() bool { return true }
|
||||
|
||||
func (t *TheadOpen) Closing() bool { return false }
|
||||
|
||||
func (t *TheadClose) Closing() bool { return true }
|
||||
|
||||
func (t *TrOpen) Closing() bool { return false }
|
||||
|
||||
func (t *TrClose) Closing() bool { return true }
|
||||
|
||||
func (t *ThOpen) Closing() bool { return false }
|
||||
|
||||
func (t *ThClose) Closing() bool { return true }
|
||||
|
||||
func (t *TbodyOpen) Closing() bool { return false }
|
||||
|
||||
func (t *TbodyClose) Closing() bool { return true }
|
||||
|
||||
func (t *TdOpen) Closing() bool { return false }
|
||||
|
||||
func (t *TdClose) Closing() bool { return true }
|
||||
|
||||
func (t *Text) Closing() bool { return false }
|
||||
|
||||
func (t *BlockquoteOpen) Block() bool { return true }
|
||||
|
||||
func (t *BlockquoteClose) Block() bool { return true }
|
||||
|
||||
func (t *BulletListOpen) Block() bool { return true }
|
||||
|
||||
func (t *BulletListClose) Block() bool { return true }
|
||||
|
||||
func (t *OrderedListOpen) Block() bool { return true }
|
||||
|
||||
func (t *OrderedListClose) Block() bool { return true }
|
||||
|
||||
func (t *ListItemOpen) Block() bool { return true }
|
||||
|
||||
func (t *ListItemClose) Block() bool { return true }
|
||||
|
||||
func (t *CodeBlock) Block() bool { return true }
|
||||
|
||||
func (t *CodeInline) Block() bool { return false }
|
||||
|
||||
func (t *EmphasisOpen) Block() bool { return false }
|
||||
|
||||
func (t *EmphasisClose) Block() bool { return false }
|
||||
|
||||
func (t *StrongOpen) Block() bool { return false }
|
||||
|
||||
func (t *StrongClose) Block() bool { return false }
|
||||
|
||||
func (t *StrikethroughOpen) Block() bool { return false }
|
||||
|
||||
func (t *StrikethroughClose) Block() bool { return false }
|
||||
|
||||
func (t *Fence) Block() bool { return true }
|
||||
|
||||
func (t *Softbreak) Block() bool { return false }
|
||||
|
||||
func (t *Hardbreak) Block() bool { return false }
|
||||
|
||||
func (t *HeadingOpen) Block() bool { return true }
|
||||
|
||||
func (t *HeadingClose) Block() bool { return true }
|
||||
|
||||
func (t *HTMLBlock) Block() bool { return true }
|
||||
|
||||
func (t *HTMLInline) Block() bool { return false }
|
||||
|
||||
func (t *Hr) Block() bool { return true }
|
||||
|
||||
func (t *Image) Block() bool { return false }
|
||||
|
||||
func (t *Inline) Block() bool { return false }
|
||||
|
||||
func (t *LinkOpen) Block() bool { return false }
|
||||
|
||||
func (t *LinkClose) Block() bool { return false }
|
||||
|
||||
func (t *ParagraphOpen) Block() bool { return true }
|
||||
|
||||
func (t *ParagraphClose) Block() bool { return true }
|
||||
|
||||
func (t *TableOpen) Block() bool { return true }
|
||||
|
||||
func (t *TableClose) Block() bool { return true }
|
||||
|
||||
func (t *TheadOpen) Block() bool { return true }
|
||||
|
||||
func (t *TheadClose) Block() bool { return true }
|
||||
|
||||
func (t *TrOpen) Block() bool { return true }
|
||||
|
||||
func (t *TrClose) Block() bool { return true }
|
||||
|
||||
func (t *ThOpen) Block() bool { return true }
|
||||
|
||||
func (t *ThClose) Block() bool { return true }
|
||||
|
||||
func (t *TbodyOpen) Block() bool { return true }
|
||||
|
||||
func (t *TbodyClose) Block() bool { return true }
|
||||
|
||||
func (t *TdOpen) Block() bool { return true }
|
||||
|
||||
func (t *TdClose) Block() bool { return true }
|
||||
|
||||
func (t *Text) Block() bool { return false }
|
||||
|
||||
func (t *BlockquoteOpen) Tag() string { return "blockquote" }
|
||||
|
||||
func (t *BlockquoteClose) Tag() string { return "blockquote" }
|
||||
|
||||
func (t *BulletListOpen) Tag() string { return "ul" }
|
||||
|
||||
func (t *BulletListClose) Tag() string { return "ul" }
|
||||
|
||||
func (t *OrderedListOpen) Tag() string { return "ol" }
|
||||
|
||||
func (t *OrderedListClose) Tag() string { return "ol" }
|
||||
|
||||
func (t *ListItemOpen) Tag() string { return "li" }
|
||||
|
||||
func (t *ListItemClose) Tag() string { return "li" }
|
||||
|
||||
func (t *CodeBlock) Tag() string { return "code" }
|
||||
|
||||
func (t *CodeInline) Tag() string { return "code" }
|
||||
|
||||
func (t *EmphasisOpen) Tag() string { return "em" }
|
||||
|
||||
func (t *EmphasisClose) Tag() string { return "em" }
|
||||
|
||||
func (t *StrongOpen) Tag() string { return "strong" }
|
||||
|
||||
func (t *StrongClose) Tag() string { return "strong" }
|
||||
|
||||
func (t *StrikethroughOpen) Tag() string { return "s" }
|
||||
|
||||
func (t *StrikethroughClose) Tag() string { return "s" }
|
||||
|
||||
func (t *Fence) Tag() string { return "code" }
|
||||
|
||||
func (t *Softbreak) Tag() string { return "br" }
|
||||
|
||||
func (t *Hardbreak) Tag() string { return "br" }
|
||||
|
||||
func (t *HeadingOpen) Tag() string { return htags[t.HLevel] }
|
||||
|
||||
func (t *HeadingClose) Tag() string { return htags[t.HLevel] }
|
||||
|
||||
func (t *HTMLBlock) Tag() string { return "" }
|
||||
|
||||
func (t *HTMLInline) Tag() string { return "" }
|
||||
|
||||
func (t *Hr) Tag() string { return "hr" }
|
||||
|
||||
func (t *Image) Tag() string { return "img" }
|
||||
|
||||
func (t *Inline) Tag() string { return "" }
|
||||
|
||||
func (t *LinkOpen) Tag() string { return "a" }
|
||||
|
||||
func (t *LinkClose) Tag() string { return "a" }
|
||||
|
||||
func (t *ParagraphOpen) Tag() string { return "p" }
|
||||
|
||||
func (t *ParagraphClose) Tag() string { return "p" }
|
||||
|
||||
func (t *TableOpen) Tag() string { return "table" }
|
||||
|
||||
func (t *TableClose) Tag() string { return "table" }
|
||||
|
||||
func (t *TheadOpen) Tag() string { return "thead" }
|
||||
|
||||
func (t *TheadClose) Tag() string { return "thead" }
|
||||
|
||||
func (t *TrOpen) Tag() string { return "tr" }
|
||||
|
||||
func (t *TrClose) Tag() string { return "tr" }
|
||||
|
||||
func (t *ThOpen) Tag() string { return "th" }
|
||||
|
||||
func (t *ThClose) Tag() string { return "th" }
|
||||
|
||||
func (t *TbodyOpen) Tag() string { return "tbody" }
|
||||
|
||||
func (t *TbodyClose) Tag() string { return "tbody" }
|
||||
|
||||
func (t *TdOpen) Tag() string { return "td" }
|
||||
|
||||
func (t *TdClose) Tag() string { return "td" }
|
||||
|
||||
func (t *Text) Tag() string { return "" }
|
|
@ -1,185 +0,0 @@
|
|||
// Copyright 2015 The Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package markdown
|
||||
|
||||
var urlSchemas = []string{
|
||||
"aaa",
|
||||
"aaas",
|
||||
"about",
|
||||
"acap",
|
||||
"adiumxtra",
|
||||
"afp",
|
||||
"afs",
|
||||
"aim",
|
||||
"apt",
|
||||
"attachment",
|
||||
"aw",
|
||||
"beshare",
|
||||
"bitcoin",
|
||||
"bolo",
|
||||
"callto",
|
||||
"cap",
|
||||
"chrome",
|
||||
"chrome-extension",
|
||||
"cid",
|
||||
"coap",
|
||||
"com-eventbrite-attendee",
|
||||
"content",
|
||||
"crid",
|
||||
"cvs",
|
||||
"data",
|
||||
"dav",
|
||||
"dict",
|
||||
"dlna-playcontainer",
|
||||
"dlna-playsingle",
|
||||
"dns",
|
||||
"doi",
|
||||
"dtn",
|
||||
"dvb",
|
||||
"ed2k",
|
||||
"facetime",
|
||||
"feed",
|
||||
"file",
|
||||
"finger",
|
||||
"fish",
|
||||
"ftp",
|
||||
"geo",
|
||||
"gg",
|
||||
"git",
|
||||
"gizmoproject",
|
||||
"go",
|
||||
"gopher",
|
||||
"gtalk",
|
||||
"h323",
|
||||
"hcp",
|
||||
"http",
|
||||
"https",
|
||||
"iax",
|
||||
"icap",
|
||||
"icon",
|
||||
"im",
|
||||
"imap",
|
||||
"info",
|
||||
"ipn",
|
||||
"ipp",
|
||||
"irc",
|
||||
"irc6",
|
||||
"ircs",
|
||||
"iris",
|
||||
"iris.beep",
|
||||
"iris.lwz",
|
||||
"iris.xpc",
|
||||
"iris.xpcs",
|
||||
"itms",
|
||||
"jar",
|
||||
"javascript",
|
||||
"jms",
|
||||
"keyparc",
|
||||
"lastfm",
|
||||
"ldap",
|
||||
"ldaps",
|
||||
"magnet",
|
||||
"mailto",
|
||||
"maps",
|
||||
"market",
|
||||
"message",
|
||||
"mid",
|
||||
"mms",
|
||||
"ms-help",
|
||||
"msnim",
|
||||
"msrp",
|
||||
"msrps",
|
||||
"mtqp",
|
||||
"mumble",
|
||||
"mupdate",
|
||||
"mvn",
|
||||
"news",
|
||||
"nfs",
|
||||
"ni",
|
||||
"nih",
|
||||
"nntp",
|
||||
"notes",
|
||||
"oid",
|
||||
"opaquelocktoken",
|
||||
"palm",
|
||||
"paparazzi",
|
||||
"platform",
|
||||
"pop",
|
||||
"pres",
|
||||
"proxy",
|
||||
"psyc",
|
||||
"query",
|
||||
"res",
|
||||
"resource",
|
||||
"rmi",
|
||||
"rsync",
|
||||
"rtmp",
|
||||
"rtsp",
|
||||
"secondlife",
|
||||
"service",
|
||||
"session",
|
||||
"sftp",
|
||||
"sgn",
|
||||
"shttp",
|
||||
"sieve",
|
||||
"sip",
|
||||
"sips",
|
||||
"skype",
|
||||
"smb",
|
||||
"sms",
|
||||
"snmp",
|
||||
"soap.beep",
|
||||
"soap.beeps",
|
||||
"soldat",
|
||||
"spotify",
|
||||
"ssh",
|
||||
"steam",
|
||||
"svn",
|
||||
"tag",
|
||||
"teamspeak",
|
||||
"tel",
|
||||
"telnet",
|
||||
"tftp",
|
||||
"things",
|
||||
"thismessage",
|
||||
"tip",
|
||||
"tn3270",
|
||||
"tv",
|
||||
"udp",
|
||||
"unreal",
|
||||
"urn",
|
||||
"ut2004",
|
||||
"vemmi",
|
||||
"ventrilo",
|
||||
"view-source",
|
||||
"webcal",
|
||||
"ws",
|
||||
"wss",
|
||||
"wtai",
|
||||
"wyciwyg",
|
||||
"xcon",
|
||||
"xcon-userid",
|
||||
"xfire",
|
||||
"xmlrpc.beep",
|
||||
"xmlrpc.beeps",
|
||||
"xmpp",
|
||||
"xri",
|
||||
"ymsgr",
|
||||
"z39.50r",
|
||||
"z39.50s",
|
||||
}
|
||||
|
||||
var urlSchemasSet = make(map[string]bool)
|
||||
|
||||
func init() {
|
||||
for _, s := range urlSchemas {
|
||||
urlSchemasSet[s] = true
|
||||
}
|
||||
}
|
||||
|
||||
func matchSchema(s string) bool {
|
||||
_, ok := urlSchemasSet[s]
|
||||
return ok
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue