mirror of https://github.com/docker/cli.git
Add missing dependencies to vendor, and fix generation imports
Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
parent
d2cb97e76d
commit
875daf0130
|
@ -8,8 +8,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/docker/cli/command/commands"
|
"github.com/docker/cli/cli/command/commands"
|
||||||
"github.com/docker/docker/pkg/term"
|
"github.com/docker/docker/pkg/term"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/docker/docker/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/docker/cli/command/commands"
|
"github.com/docker/cli/cli/command/commands"
|
||||||
"github.com/docker/docker/pkg/term"
|
"github.com/docker/docker/pkg/term"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/cobra/doc"
|
"github.com/spf13/cobra/doc"
|
||||||
|
|
|
@ -4,6 +4,7 @@ github.com/Nvveen/Gotty a8b993ba6abdb0e0c12b0125c603323a71c7790c https://github.
|
||||||
github.com/Sirupsen/logrus v0.11.0
|
github.com/Sirupsen/logrus v0.11.0
|
||||||
github.com/agl/ed25519 d2b94fd789ea21d12fac1a4443dd3a3f79cda72c
|
github.com/agl/ed25519 d2b94fd789ea21d12fac1a4443dd3a3f79cda72c
|
||||||
github.com/coreos/etcd 824277cb3a577a0e8c829ca9ec557b973fe06d20
|
github.com/coreos/etcd 824277cb3a577a0e8c829ca9ec557b973fe06d20
|
||||||
|
github.com/cpuguy83/go-md2man a65d4d2de4d5f7c74868dfa9b202a3c8be315aaa
|
||||||
github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
|
github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
|
||||||
github.com/docker/distribution b38e5838b7b2f2ad48e06ec4b500011976080621
|
github.com/docker/distribution b38e5838b7b2f2ad48e06ec4b500011976080621
|
||||||
github.com/docker/docker 45c6f4262a865a03adaac291a9ce33c0f2190d77
|
github.com/docker/docker 45c6f4262a865a03adaac291a9ce33c0f2190d77
|
||||||
|
@ -31,6 +32,8 @@ github.com/opencontainers/runc 9c2d8d184e5da67c95d601382adf14862e4f2228 https://
|
||||||
github.com/opencontainers/selinux ba1aefe8057f1d0cfb8e88d0ec1dc85925ef987d
|
github.com/opencontainers/selinux ba1aefe8057f1d0cfb8e88d0ec1dc85925ef987d
|
||||||
github.com/pkg/errors 839d9e913e063e28dfd0e6c7b7512793e0a48be9
|
github.com/pkg/errors 839d9e913e063e28dfd0e6c7b7512793e0a48be9
|
||||||
github.com/pmezard/go-difflib 792786c7400a136282c1664665ae0a8db921c6c2
|
github.com/pmezard/go-difflib 792786c7400a136282c1664665ae0a8db921c6c2
|
||||||
|
github.com/russross/blackfriday 1d6b8e9301e720b08a8938b8c25c018285885438
|
||||||
|
github.com/shurcooL/sanitized_anchor_name 10ef21a441db47d8b13ebcc5fd2310f636973c77
|
||||||
github.com/spf13/cobra v1.5.1 https://github.com/dnephin/cobra.git
|
github.com/spf13/cobra v1.5.1 https://github.com/dnephin/cobra.git
|
||||||
github.com/spf13/pflag 9ff6c6923cfffbcd502984b8e0c80539a94968b7
|
github.com/spf13/pflag 9ff6c6923cfffbcd502984b8e0c80539a94968b7
|
||||||
github.com/stretchr/testify 4d4bfba8f1d1027c4fdbe371823030df51419987
|
github.com/stretchr/testify 4d4bfba8f1d1027c4fdbe371823030df51419987
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Brian Goff
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,18 @@
|
||||||
|
go-md2man
|
||||||
|
=========
|
||||||
|
|
||||||
|
** Work in Progress **
|
||||||
|
This still needs a lot of help to be complete, or even usable!
|
||||||
|
|
||||||
|
Uses blackfriday to process markdown into man pages.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
./md2man -in /path/to/markdownfile.md -out /manfile/output/path
|
||||||
|
|
||||||
|
### How to contribute
|
||||||
|
|
||||||
|
We use [govend](https://github.com/govend/govend) for vendoring Go packages.
|
||||||
|
|
||||||
|
How to update dependencies: `govend -v -u --prune`
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package md2man
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/russross/blackfriday"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Render(doc []byte) []byte {
|
||||||
|
renderer := RoffRenderer(0)
|
||||||
|
extensions := 0
|
||||||
|
extensions |= blackfriday.EXTENSION_NO_INTRA_EMPHASIS
|
||||||
|
extensions |= blackfriday.EXTENSION_TABLES
|
||||||
|
extensions |= blackfriday.EXTENSION_FENCED_CODE
|
||||||
|
extensions |= blackfriday.EXTENSION_AUTOLINK
|
||||||
|
extensions |= blackfriday.EXTENSION_SPACE_HEADERS
|
||||||
|
extensions |= blackfriday.EXTENSION_FOOTNOTES
|
||||||
|
extensions |= blackfriday.EXTENSION_TITLEBLOCK
|
||||||
|
|
||||||
|
return blackfriday.Markdown(doc, renderer, extensions)
|
||||||
|
}
|
|
@ -0,0 +1,282 @@
|
||||||
|
package md2man
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/russross/blackfriday"
|
||||||
|
)
|
||||||
|
|
||||||
|
type roffRenderer struct{}
|
||||||
|
|
||||||
|
var listCounter int
|
||||||
|
|
||||||
|
func RoffRenderer(flags int) blackfriday.Renderer {
|
||||||
|
return &roffRenderer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) GetFlags() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) TitleBlock(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString(".TH ")
|
||||||
|
|
||||||
|
splitText := bytes.Split(text, []byte("\n"))
|
||||||
|
for i, line := range splitText {
|
||||||
|
line = bytes.TrimPrefix(line, []byte("% "))
|
||||||
|
if i == 0 {
|
||||||
|
line = bytes.Replace(line, []byte("("), []byte("\" \""), 1)
|
||||||
|
line = bytes.Replace(line, []byte(")"), []byte("\" \""), 1)
|
||||||
|
}
|
||||||
|
line = append([]byte("\""), line...)
|
||||||
|
line = append(line, []byte("\" ")...)
|
||||||
|
out.Write(line)
|
||||||
|
}
|
||||||
|
out.WriteString("\n")
|
||||||
|
|
||||||
|
// disable hyphenation
|
||||||
|
out.WriteString(".nh\n")
|
||||||
|
// disable justification (adjust text to left margin only)
|
||||||
|
out.WriteString(".ad l\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) {
|
||||||
|
out.WriteString("\n.PP\n.RS\n\n.nf\n")
|
||||||
|
escapeSpecialChars(out, text)
|
||||||
|
out.WriteString("\n.fi\n.RE\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) BlockQuote(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("\n.PP\n.RS\n")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("\n.RE\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) BlockHtml(out *bytes.Buffer, text []byte) {
|
||||||
|
out.Write(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) Header(out *bytes.Buffer, text func() bool, level int, id string) {
|
||||||
|
marker := out.Len()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case marker == 0:
|
||||||
|
// This is the doc header
|
||||||
|
out.WriteString(".TH ")
|
||||||
|
case level == 1:
|
||||||
|
out.WriteString("\n\n.SH ")
|
||||||
|
case level == 2:
|
||||||
|
out.WriteString("\n.SH ")
|
||||||
|
default:
|
||||||
|
out.WriteString("\n.SS ")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !text() {
|
||||||
|
out.Truncate(marker)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) HRule(out *bytes.Buffer) {
|
||||||
|
out.WriteString("\n.ti 0\n\\l'\\n(.lu'\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||||
|
marker := out.Len()
|
||||||
|
if flags&blackfriday.LIST_TYPE_ORDERED != 0 {
|
||||||
|
listCounter = 1
|
||||||
|
}
|
||||||
|
if !text() {
|
||||||
|
out.Truncate(marker)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) ListItem(out *bytes.Buffer, text []byte, flags int) {
|
||||||
|
if flags&blackfriday.LIST_TYPE_ORDERED != 0 {
|
||||||
|
out.WriteString(fmt.Sprintf(".IP \"%3d.\" 5\n", listCounter))
|
||||||
|
listCounter += 1
|
||||||
|
} else {
|
||||||
|
out.WriteString(".IP \\(bu 2\n")
|
||||||
|
}
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) Paragraph(out *bytes.Buffer, text func() bool) {
|
||||||
|
marker := out.Len()
|
||||||
|
out.WriteString("\n.PP\n")
|
||||||
|
if !text() {
|
||||||
|
out.Truncate(marker)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if marker != 0 {
|
||||||
|
out.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This might now work
|
||||||
|
func (r *roffRenderer) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
|
||||||
|
out.WriteString(".TS\nallbox;\n")
|
||||||
|
|
||||||
|
out.Write(header)
|
||||||
|
out.Write(body)
|
||||||
|
out.WriteString("\n.TE\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) TableRow(out *bytes.Buffer, text []byte) {
|
||||||
|
if out.Len() > 0 {
|
||||||
|
out.WriteString("\n")
|
||||||
|
}
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
|
||||||
|
if out.Len() > 0 {
|
||||||
|
out.WriteString(" ")
|
||||||
|
}
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This is probably broken
|
||||||
|
func (r *roffRenderer) TableCell(out *bytes.Buffer, text []byte, align int) {
|
||||||
|
if out.Len() > 0 {
|
||||||
|
out.WriteString("\t")
|
||||||
|
}
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("\t")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) Footnotes(out *bytes.Buffer, text func() bool) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) AutoLink(out *bytes.Buffer, link []byte, kind int) {
|
||||||
|
out.WriteString("\n\\[la]")
|
||||||
|
out.Write(link)
|
||||||
|
out.WriteString("\\[ra]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) CodeSpan(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("\\fB\\fC")
|
||||||
|
escapeSpecialChars(out, text)
|
||||||
|
out.WriteString("\\fR")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) DoubleEmphasis(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("\\fB")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("\\fP")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) Emphasis(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("\\fI")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("\\fP")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) LineBreak(out *bytes.Buffer) {
|
||||||
|
out.WriteString("\n.br\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
|
||||||
|
out.Write(content)
|
||||||
|
r.AutoLink(out, link, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) RawHtmlTag(out *bytes.Buffer, tag []byte) {
|
||||||
|
out.Write(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) TripleEmphasis(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("\\s+2")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("\\s-2")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) StrikeThrough(out *bytes.Buffer, text []byte) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) Entity(out *bytes.Buffer, entity []byte) {
|
||||||
|
out.WriteString(html.UnescapeString(string(entity)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func processFooterText(text []byte) []byte {
|
||||||
|
text = bytes.TrimPrefix(text, []byte("% "))
|
||||||
|
newText := []byte{}
|
||||||
|
textArr := strings.Split(string(text), ") ")
|
||||||
|
|
||||||
|
for i, w := range textArr {
|
||||||
|
if i == 0 {
|
||||||
|
w = strings.Replace(w, "(", "\" \"", 1)
|
||||||
|
w = fmt.Sprintf("\"%s\"", w)
|
||||||
|
} else {
|
||||||
|
w = fmt.Sprintf(" \"%s\"", w)
|
||||||
|
}
|
||||||
|
newText = append(newText, []byte(w)...)
|
||||||
|
}
|
||||||
|
newText = append(newText, []byte(" \"\"")...)
|
||||||
|
|
||||||
|
return newText
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) NormalText(out *bytes.Buffer, text []byte) {
|
||||||
|
escapeSpecialChars(out, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) DocumentHeader(out *bytes.Buffer) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *roffRenderer) DocumentFooter(out *bytes.Buffer) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func needsBackslash(c byte) bool {
|
||||||
|
for _, r := range []byte("-_&\\~") {
|
||||||
|
if c == r {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeSpecialChars(out *bytes.Buffer, text []byte) {
|
||||||
|
for i := 0; i < len(text); i++ {
|
||||||
|
// escape initial apostrophe or period
|
||||||
|
if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') {
|
||||||
|
out.WriteString("\\&")
|
||||||
|
}
|
||||||
|
|
||||||
|
// directly copy normal characters
|
||||||
|
org := i
|
||||||
|
|
||||||
|
for i < len(text) && !needsBackslash(text[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i > org {
|
||||||
|
out.Write(text[org:i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// escape a character
|
||||||
|
if i >= len(text) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
out.WriteByte('\\')
|
||||||
|
out.WriteByte(text[i])
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
Blackfriday is distributed under the Simplified BSD License:
|
||||||
|
|
||||||
|
> Copyright © 2011 Russ Ross
|
||||||
|
> All rights reserved.
|
||||||
|
>
|
||||||
|
> Redistribution and use in source and binary forms, with or without
|
||||||
|
> modification, are permitted provided that the following conditions
|
||||||
|
> are met:
|
||||||
|
>
|
||||||
|
> 1. Redistributions of source code must retain the above copyright
|
||||||
|
> notice, this list of conditions and the following disclaimer.
|
||||||
|
>
|
||||||
|
> 2. Redistributions in binary form must reproduce the above
|
||||||
|
> copyright notice, this list of conditions and the following
|
||||||
|
> disclaimer in the documentation and/or other materials provided with
|
||||||
|
> the distribution.
|
||||||
|
>
|
||||||
|
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||||
|
> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||||
|
> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||||
|
> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||||
|
> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
> POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,267 @@
|
||||||
|
Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday) [![GoDoc](https://godoc.org/github.com/russross/blackfriday?status.svg)](https://godoc.org/github.com/russross/blackfriday)
|
||||||
|
===========
|
||||||
|
|
||||||
|
Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It
|
||||||
|
is paranoid about its input (so you can safely feed it user-supplied
|
||||||
|
data), it is fast, it supports common extensions (tables, smart
|
||||||
|
punctuation substitutions, etc.), and it is safe for all utf-8
|
||||||
|
(unicode) input.
|
||||||
|
|
||||||
|
HTML output is currently supported, along with Smartypants
|
||||||
|
extensions. An experimental LaTeX output engine is also included.
|
||||||
|
|
||||||
|
It started as a translation from C of [Sundown][3].
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Blackfriday is compatible with Go 1. If you are using an older
|
||||||
|
release of Go, consider using v1.1 of blackfriday, which was based
|
||||||
|
on the last stable release of Go prior to Go 1. You can find it as a
|
||||||
|
tagged commit on github.
|
||||||
|
|
||||||
|
With Go 1 and git installed:
|
||||||
|
|
||||||
|
go get github.com/russross/blackfriday
|
||||||
|
|
||||||
|
will download, compile, and install the package into your `$GOPATH`
|
||||||
|
directory hierarchy. Alternatively, you can achieve the same if you
|
||||||
|
import it into a project:
|
||||||
|
|
||||||
|
import "github.com/russross/blackfriday"
|
||||||
|
|
||||||
|
and `go get` without parameters.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
For basic usage, it is as simple as getting your input into a byte
|
||||||
|
slice and calling:
|
||||||
|
|
||||||
|
output := blackfriday.MarkdownBasic(input)
|
||||||
|
|
||||||
|
This renders it with no extensions enabled. To get a more useful
|
||||||
|
feature set, use this instead:
|
||||||
|
|
||||||
|
output := blackfriday.MarkdownCommon(input)
|
||||||
|
|
||||||
|
### Sanitize untrusted content
|
||||||
|
|
||||||
|
Blackfriday itself does nothing to protect against malicious content. If you are
|
||||||
|
dealing with user-supplied markdown, we recommend running blackfriday's output
|
||||||
|
through HTML sanitizer such as
|
||||||
|
[Bluemonday](https://github.com/microcosm-cc/bluemonday).
|
||||||
|
|
||||||
|
Here's an example of simple usage of blackfriday together with bluemonday:
|
||||||
|
|
||||||
|
``` go
|
||||||
|
import (
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
"github.com/russross/blackfriday"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ...
|
||||||
|
unsafe := blackfriday.MarkdownCommon(input)
|
||||||
|
html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom options
|
||||||
|
|
||||||
|
If you want to customize the set of options, first get a renderer
|
||||||
|
(currently either the HTML or LaTeX output engines), then use it to
|
||||||
|
call the more general `Markdown` function. For examples, see the
|
||||||
|
implementations of `MarkdownBasic` and `MarkdownCommon` in
|
||||||
|
`markdown.go`.
|
||||||
|
|
||||||
|
You can also check out `blackfriday-tool` for a more complete example
|
||||||
|
of how to use it. Download and install it using:
|
||||||
|
|
||||||
|
go get github.com/russross/blackfriday-tool
|
||||||
|
|
||||||
|
This is a simple command-line tool that allows you to process a
|
||||||
|
markdown file using a standalone program. You can also browse the
|
||||||
|
source directly on github if you are just looking for some example
|
||||||
|
code:
|
||||||
|
|
||||||
|
* <http://github.com/russross/blackfriday-tool>
|
||||||
|
|
||||||
|
Note that if you have not already done so, installing
|
||||||
|
`blackfriday-tool` will be sufficient to download and install
|
||||||
|
blackfriday in addition to the tool itself. The tool binary will be
|
||||||
|
installed in `$GOPATH/bin`. This is a statically-linked binary that
|
||||||
|
can be copied to wherever you need it without worrying about
|
||||||
|
dependencies and library versions.
|
||||||
|
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
All features of Sundown are supported, including:
|
||||||
|
|
||||||
|
* **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 blackfriday is
|
||||||
|
more consistent and cleaner.
|
||||||
|
|
||||||
|
* **Common extensions**, including table support, fenced code
|
||||||
|
blocks, autolinks, strikethroughs, non-strict emphasis, etc.
|
||||||
|
|
||||||
|
* **Safety**. Blackfriday 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/russross/blackfriday#sanitize-untrusted-content).
|
||||||
|
|
||||||
|
* **Fast processing**. 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**. Blackfriday only depends on standard
|
||||||
|
library packages in Go. The source code is pretty
|
||||||
|
self-contained, so it is easy to add to any project, including
|
||||||
|
Google App Engine projects.
|
||||||
|
|
||||||
|
* **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. Blackfriday lets 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
|
||||||
|
```
|
||||||
|
|
||||||
|
* **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**. Blackfriday 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 (it is off by
|
||||||
|
default in the `MarkdownBasic` and `MarkdownCommon` convenience
|
||||||
|
functions), newlines in the input translate into line breaks in
|
||||||
|
the output.
|
||||||
|
|
||||||
|
* **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>.
|
||||||
|
|
||||||
|
|
||||||
|
Other renderers
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Blackfriday is structured to allow alternative rendering engines. Here
|
||||||
|
are a few of note:
|
||||||
|
|
||||||
|
* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown):
|
||||||
|
provides a GitHub Flavored Markdown renderer with fenced code block
|
||||||
|
highlighting, clickable header anchor links.
|
||||||
|
|
||||||
|
It's not customizable, and its goal is to produce HTML output
|
||||||
|
equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode),
|
||||||
|
except the rendering is performed locally.
|
||||||
|
|
||||||
|
* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt,
|
||||||
|
but for markdown.
|
||||||
|
|
||||||
|
* LaTeX output: renders output as LaTeX. This is currently part of the
|
||||||
|
main Blackfriday repository, but may be split into its own project
|
||||||
|
in the future. If you are interested in owning and maintaining the
|
||||||
|
LaTeX output component, please be in touch.
|
||||||
|
|
||||||
|
It renders some basic documents, but is only experimental at this
|
||||||
|
point. In particular, it does not do any inline escaping, so input
|
||||||
|
that happens to look like LaTeX code will be passed through without
|
||||||
|
modification.
|
||||||
|
|
||||||
|
* [Md2Vim](https://github.com/FooSoft/md2vim): transforms markdown files into vimdoc format.
|
||||||
|
|
||||||
|
|
||||||
|
Todo
|
||||||
|
----
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt)
|
||||||
|
|
||||||
|
|
||||||
|
[1]: http://daringfireball.net/projects/markdown/ "Markdown"
|
||||||
|
[2]: http://golang.org/ "Go Language"
|
||||||
|
[3]: https://github.com/vmg/sundown "Sundown"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,949 @@
|
||||||
|
//
|
||||||
|
// Blackfriday Markdown Processor
|
||||||
|
// Available at http://github.com/russross/blackfriday
|
||||||
|
//
|
||||||
|
// Copyright © 2011 Russ Ross <russ@russross.com>.
|
||||||
|
// Distributed under the Simplified BSD License.
|
||||||
|
// See README.md for details.
|
||||||
|
//
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// HTML rendering backend
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
package blackfriday
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Html renderer configuration options.
|
||||||
|
const (
|
||||||
|
HTML_SKIP_HTML = 1 << iota // skip preformatted HTML blocks
|
||||||
|
HTML_SKIP_STYLE // skip embedded <style> elements
|
||||||
|
HTML_SKIP_IMAGES // skip embedded images
|
||||||
|
HTML_SKIP_LINKS // skip all links
|
||||||
|
HTML_SAFELINK // only link to trusted protocols
|
||||||
|
HTML_NOFOLLOW_LINKS // only link with rel="nofollow"
|
||||||
|
HTML_NOREFERRER_LINKS // only link with rel="noreferrer"
|
||||||
|
HTML_HREF_TARGET_BLANK // add a blank target
|
||||||
|
HTML_TOC // generate a table of contents
|
||||||
|
HTML_OMIT_CONTENTS // skip the main contents (for a standalone table of contents)
|
||||||
|
HTML_COMPLETE_PAGE // generate a complete HTML page
|
||||||
|
HTML_USE_XHTML // generate XHTML output instead of HTML
|
||||||
|
HTML_USE_SMARTYPANTS // enable smart punctuation substitutions
|
||||||
|
HTML_SMARTYPANTS_FRACTIONS // enable smart fractions (with HTML_USE_SMARTYPANTS)
|
||||||
|
HTML_SMARTYPANTS_DASHES // enable smart dashes (with HTML_USE_SMARTYPANTS)
|
||||||
|
HTML_SMARTYPANTS_LATEX_DASHES // enable LaTeX-style dashes (with HTML_USE_SMARTYPANTS and HTML_SMARTYPANTS_DASHES)
|
||||||
|
HTML_SMARTYPANTS_ANGLED_QUOTES // enable angled double quotes (with HTML_USE_SMARTYPANTS) for double quotes rendering
|
||||||
|
HTML_FOOTNOTE_RETURN_LINKS // generate a link at the end of a footnote to return to the source
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
alignments = []string{
|
||||||
|
"left",
|
||||||
|
"right",
|
||||||
|
"center",
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: improve this regexp to catch all possible entities:
|
||||||
|
htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type HtmlRendererParameters struct {
|
||||||
|
// Prepend this text to each relative URL.
|
||||||
|
AbsolutePrefix string
|
||||||
|
// Add this text to each footnote anchor, to ensure uniqueness.
|
||||||
|
FootnoteAnchorPrefix string
|
||||||
|
// Show this text inside the <a> tag for a footnote return link, if the
|
||||||
|
// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
|
||||||
|
// <sup>[return]</sup> is used.
|
||||||
|
FootnoteReturnLinkContents string
|
||||||
|
// If set, add this text to the front of each Header ID, to ensure
|
||||||
|
// uniqueness.
|
||||||
|
HeaderIDPrefix string
|
||||||
|
// If set, add this text to the back of each Header ID, to ensure uniqueness.
|
||||||
|
HeaderIDSuffix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Html is a type that implements the Renderer interface for HTML output.
|
||||||
|
//
|
||||||
|
// Do not create this directly, instead use the HtmlRenderer function.
|
||||||
|
type Html struct {
|
||||||
|
flags int // HTML_* options
|
||||||
|
closeTag string // how to end singleton tags: either " />" or ">"
|
||||||
|
title string // document title
|
||||||
|
css string // optional css file url (used with HTML_COMPLETE_PAGE)
|
||||||
|
|
||||||
|
parameters HtmlRendererParameters
|
||||||
|
|
||||||
|
// table of contents data
|
||||||
|
tocMarker int
|
||||||
|
headerCount int
|
||||||
|
currentLevel int
|
||||||
|
toc *bytes.Buffer
|
||||||
|
|
||||||
|
// Track header IDs to prevent ID collision in a single generation.
|
||||||
|
headerIDs map[string]int
|
||||||
|
|
||||||
|
smartypants *smartypantsRenderer
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
xhtmlClose = " />"
|
||||||
|
htmlClose = ">"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HtmlRenderer creates and configures an Html object, which
|
||||||
|
// satisfies the Renderer interface.
|
||||||
|
//
|
||||||
|
// flags is a set of HTML_* options ORed together.
|
||||||
|
// title is the title of the document, and css is a URL for the document's
|
||||||
|
// stylesheet.
|
||||||
|
// title and css are only used when HTML_COMPLETE_PAGE is selected.
|
||||||
|
func HtmlRenderer(flags int, title string, css string) Renderer {
|
||||||
|
return HtmlRendererWithParameters(flags, title, css, HtmlRendererParameters{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func HtmlRendererWithParameters(flags int, title string,
|
||||||
|
css string, renderParameters HtmlRendererParameters) Renderer {
|
||||||
|
// configure the rendering engine
|
||||||
|
closeTag := htmlClose
|
||||||
|
if flags&HTML_USE_XHTML != 0 {
|
||||||
|
closeTag = xhtmlClose
|
||||||
|
}
|
||||||
|
|
||||||
|
if renderParameters.FootnoteReturnLinkContents == "" {
|
||||||
|
renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Html{
|
||||||
|
flags: flags,
|
||||||
|
closeTag: closeTag,
|
||||||
|
title: title,
|
||||||
|
css: css,
|
||||||
|
parameters: renderParameters,
|
||||||
|
|
||||||
|
headerCount: 0,
|
||||||
|
currentLevel: 0,
|
||||||
|
toc: new(bytes.Buffer),
|
||||||
|
|
||||||
|
headerIDs: make(map[string]int),
|
||||||
|
|
||||||
|
smartypants: smartypants(flags),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using if statements is a bit faster than a switch statement. As the compiler
|
||||||
|
// improves, this should be unnecessary this is only worthwhile because
|
||||||
|
// attrEscape is the single largest CPU user in normal use.
|
||||||
|
// Also tried using map, but that gave a ~3x slowdown.
|
||||||
|
func escapeSingleChar(char byte) (string, bool) {
|
||||||
|
if char == '"' {
|
||||||
|
return """, true
|
||||||
|
}
|
||||||
|
if char == '&' {
|
||||||
|
return "&", true
|
||||||
|
}
|
||||||
|
if char == '<' {
|
||||||
|
return "<", true
|
||||||
|
}
|
||||||
|
if char == '>' {
|
||||||
|
return ">", true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func attrEscape(out *bytes.Buffer, src []byte) {
|
||||||
|
org := 0
|
||||||
|
for i, ch := range src {
|
||||||
|
if entity, ok := escapeSingleChar(ch); ok {
|
||||||
|
if i > org {
|
||||||
|
// copy all the normal characters since the last escape
|
||||||
|
out.Write(src[org:i])
|
||||||
|
}
|
||||||
|
org = i + 1
|
||||||
|
out.WriteString(entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if org < len(src) {
|
||||||
|
out.Write(src[org:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func entityEscapeWithSkip(out *bytes.Buffer, src []byte, skipRanges [][]int) {
|
||||||
|
end := 0
|
||||||
|
for _, rang := range skipRanges {
|
||||||
|
attrEscape(out, src[end:rang[0]])
|
||||||
|
out.Write(src[rang[0]:rang[1]])
|
||||||
|
end = rang[1]
|
||||||
|
}
|
||||||
|
attrEscape(out, src[end:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) GetFlags() int {
|
||||||
|
return options.flags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) TitleBlock(out *bytes.Buffer, text []byte) {
|
||||||
|
text = bytes.TrimPrefix(text, []byte("% "))
|
||||||
|
text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1)
|
||||||
|
out.WriteString("<h1 class=\"title\">")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("\n</h1>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) Header(out *bytes.Buffer, text func() bool, level int, id string) {
|
||||||
|
marker := out.Len()
|
||||||
|
doubleSpace(out)
|
||||||
|
|
||||||
|
if id == "" && options.flags&HTML_TOC != 0 {
|
||||||
|
id = fmt.Sprintf("toc_%d", options.headerCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id != "" {
|
||||||
|
id = options.ensureUniqueHeaderID(id)
|
||||||
|
|
||||||
|
if options.parameters.HeaderIDPrefix != "" {
|
||||||
|
id = options.parameters.HeaderIDPrefix + id
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.parameters.HeaderIDSuffix != "" {
|
||||||
|
id = id + options.parameters.HeaderIDSuffix
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id))
|
||||||
|
} else {
|
||||||
|
out.WriteString(fmt.Sprintf("<h%d>", level))
|
||||||
|
}
|
||||||
|
|
||||||
|
tocMarker := out.Len()
|
||||||
|
if !text() {
|
||||||
|
out.Truncate(marker)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// are we building a table of contents?
|
||||||
|
if options.flags&HTML_TOC != 0 {
|
||||||
|
options.TocHeaderWithAnchor(out.Bytes()[tocMarker:], level, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString(fmt.Sprintf("</h%d>\n", level))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) BlockHtml(out *bytes.Buffer, text []byte) {
|
||||||
|
if options.flags&HTML_SKIP_HTML != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
doubleSpace(out)
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) HRule(out *bytes.Buffer) {
|
||||||
|
doubleSpace(out)
|
||||||
|
out.WriteString("<hr")
|
||||||
|
out.WriteString(options.closeTag)
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) BlockCode(out *bytes.Buffer, text []byte, lang string) {
|
||||||
|
doubleSpace(out)
|
||||||
|
|
||||||
|
// parse out the language names/classes
|
||||||
|
count := 0
|
||||||
|
for _, elt := range strings.Fields(lang) {
|
||||||
|
if elt[0] == '.' {
|
||||||
|
elt = elt[1:]
|
||||||
|
}
|
||||||
|
if len(elt) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
out.WriteString("<pre><code class=\"language-")
|
||||||
|
} else {
|
||||||
|
out.WriteByte(' ')
|
||||||
|
}
|
||||||
|
attrEscape(out, []byte(elt))
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
out.WriteString("<pre><code>")
|
||||||
|
} else {
|
||||||
|
out.WriteString("\">")
|
||||||
|
}
|
||||||
|
|
||||||
|
attrEscape(out, text)
|
||||||
|
out.WriteString("</code></pre>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) BlockQuote(out *bytes.Buffer, text []byte) {
|
||||||
|
doubleSpace(out)
|
||||||
|
out.WriteString("<blockquote>\n")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("</blockquote>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
|
||||||
|
doubleSpace(out)
|
||||||
|
out.WriteString("<table>\n<thead>\n")
|
||||||
|
out.Write(header)
|
||||||
|
out.WriteString("</thead>\n\n<tbody>\n")
|
||||||
|
out.Write(body)
|
||||||
|
out.WriteString("</tbody>\n</table>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) TableRow(out *bytes.Buffer, text []byte) {
|
||||||
|
doubleSpace(out)
|
||||||
|
out.WriteString("<tr>\n")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("\n</tr>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
|
||||||
|
doubleSpace(out)
|
||||||
|
switch align {
|
||||||
|
case TABLE_ALIGNMENT_LEFT:
|
||||||
|
out.WriteString("<th align=\"left\">")
|
||||||
|
case TABLE_ALIGNMENT_RIGHT:
|
||||||
|
out.WriteString("<th align=\"right\">")
|
||||||
|
case TABLE_ALIGNMENT_CENTER:
|
||||||
|
out.WriteString("<th align=\"center\">")
|
||||||
|
default:
|
||||||
|
out.WriteString("<th>")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("</th>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) TableCell(out *bytes.Buffer, text []byte, align int) {
|
||||||
|
doubleSpace(out)
|
||||||
|
switch align {
|
||||||
|
case TABLE_ALIGNMENT_LEFT:
|
||||||
|
out.WriteString("<td align=\"left\">")
|
||||||
|
case TABLE_ALIGNMENT_RIGHT:
|
||||||
|
out.WriteString("<td align=\"right\">")
|
||||||
|
case TABLE_ALIGNMENT_CENTER:
|
||||||
|
out.WriteString("<td align=\"center\">")
|
||||||
|
default:
|
||||||
|
out.WriteString("<td>")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("</td>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) Footnotes(out *bytes.Buffer, text func() bool) {
|
||||||
|
out.WriteString("<div class=\"footnotes\">\n")
|
||||||
|
options.HRule(out)
|
||||||
|
options.List(out, text, LIST_TYPE_ORDERED)
|
||||||
|
out.WriteString("</div>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
|
||||||
|
if flags&LIST_ITEM_CONTAINS_BLOCK != 0 || flags&LIST_ITEM_BEGINNING_OF_LIST != 0 {
|
||||||
|
doubleSpace(out)
|
||||||
|
}
|
||||||
|
slug := slugify(name)
|
||||||
|
out.WriteString(`<li id="`)
|
||||||
|
out.WriteString(`fn:`)
|
||||||
|
out.WriteString(options.parameters.FootnoteAnchorPrefix)
|
||||||
|
out.Write(slug)
|
||||||
|
out.WriteString(`">`)
|
||||||
|
out.Write(text)
|
||||||
|
if options.flags&HTML_FOOTNOTE_RETURN_LINKS != 0 {
|
||||||
|
out.WriteString(` <a class="footnote-return" href="#`)
|
||||||
|
out.WriteString(`fnref:`)
|
||||||
|
out.WriteString(options.parameters.FootnoteAnchorPrefix)
|
||||||
|
out.Write(slug)
|
||||||
|
out.WriteString(`">`)
|
||||||
|
out.WriteString(options.parameters.FootnoteReturnLinkContents)
|
||||||
|
out.WriteString(`</a>`)
|
||||||
|
}
|
||||||
|
out.WriteString("</li>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||||
|
marker := out.Len()
|
||||||
|
doubleSpace(out)
|
||||||
|
|
||||||
|
if flags&LIST_TYPE_DEFINITION != 0 {
|
||||||
|
out.WriteString("<dl>")
|
||||||
|
} else if flags&LIST_TYPE_ORDERED != 0 {
|
||||||
|
out.WriteString("<ol>")
|
||||||
|
} else {
|
||||||
|
out.WriteString("<ul>")
|
||||||
|
}
|
||||||
|
if !text() {
|
||||||
|
out.Truncate(marker)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if flags&LIST_TYPE_DEFINITION != 0 {
|
||||||
|
out.WriteString("</dl>\n")
|
||||||
|
} else if flags&LIST_TYPE_ORDERED != 0 {
|
||||||
|
out.WriteString("</ol>\n")
|
||||||
|
} else {
|
||||||
|
out.WriteString("</ul>\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) ListItem(out *bytes.Buffer, text []byte, flags int) {
|
||||||
|
if (flags&LIST_ITEM_CONTAINS_BLOCK != 0 && flags&LIST_TYPE_DEFINITION == 0) ||
|
||||||
|
flags&LIST_ITEM_BEGINNING_OF_LIST != 0 {
|
||||||
|
doubleSpace(out)
|
||||||
|
}
|
||||||
|
if flags&LIST_TYPE_TERM != 0 {
|
||||||
|
out.WriteString("<dt>")
|
||||||
|
} else if flags&LIST_TYPE_DEFINITION != 0 {
|
||||||
|
out.WriteString("<dd>")
|
||||||
|
} else {
|
||||||
|
out.WriteString("<li>")
|
||||||
|
}
|
||||||
|
out.Write(text)
|
||||||
|
if flags&LIST_TYPE_TERM != 0 {
|
||||||
|
out.WriteString("</dt>\n")
|
||||||
|
} else if flags&LIST_TYPE_DEFINITION != 0 {
|
||||||
|
out.WriteString("</dd>\n")
|
||||||
|
} else {
|
||||||
|
out.WriteString("</li>\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) Paragraph(out *bytes.Buffer, text func() bool) {
|
||||||
|
marker := out.Len()
|
||||||
|
doubleSpace(out)
|
||||||
|
|
||||||
|
out.WriteString("<p>")
|
||||||
|
if !text() {
|
||||||
|
out.Truncate(marker)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.WriteString("</p>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) AutoLink(out *bytes.Buffer, link []byte, kind int) {
|
||||||
|
skipRanges := htmlEntity.FindAllIndex(link, -1)
|
||||||
|
if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) && kind != LINK_TYPE_EMAIL {
|
||||||
|
// mark it but don't link it if it is not a safe link: no smartypants
|
||||||
|
out.WriteString("<tt>")
|
||||||
|
entityEscapeWithSkip(out, link, skipRanges)
|
||||||
|
out.WriteString("</tt>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString("<a href=\"")
|
||||||
|
if kind == LINK_TYPE_EMAIL {
|
||||||
|
out.WriteString("mailto:")
|
||||||
|
} else {
|
||||||
|
options.maybeWriteAbsolutePrefix(out, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
entityEscapeWithSkip(out, link, skipRanges)
|
||||||
|
|
||||||
|
var relAttrs []string
|
||||||
|
if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) {
|
||||||
|
relAttrs = append(relAttrs, "nofollow")
|
||||||
|
}
|
||||||
|
if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) {
|
||||||
|
relAttrs = append(relAttrs, "noreferrer")
|
||||||
|
}
|
||||||
|
if len(relAttrs) > 0 {
|
||||||
|
out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// blank target only add to external link
|
||||||
|
if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) {
|
||||||
|
out.WriteString("\" target=\"_blank")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString("\">")
|
||||||
|
|
||||||
|
// Pretty print: if we get an email address as
|
||||||
|
// an actual URI, e.g. `mailto:foo@bar.com`, we don't
|
||||||
|
// want to print the `mailto:` prefix
|
||||||
|
switch {
|
||||||
|
case bytes.HasPrefix(link, []byte("mailto://")):
|
||||||
|
attrEscape(out, link[len("mailto://"):])
|
||||||
|
case bytes.HasPrefix(link, []byte("mailto:")):
|
||||||
|
attrEscape(out, link[len("mailto:"):])
|
||||||
|
default:
|
||||||
|
entityEscapeWithSkip(out, link, skipRanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString("</a>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) CodeSpan(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("<code>")
|
||||||
|
attrEscape(out, text)
|
||||||
|
out.WriteString("</code>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) DoubleEmphasis(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("<strong>")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("</strong>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) Emphasis(out *bytes.Buffer, text []byte) {
|
||||||
|
if len(text) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.WriteString("<em>")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("</em>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) maybeWriteAbsolutePrefix(out *bytes.Buffer, link []byte) {
|
||||||
|
if options.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
|
||||||
|
out.WriteString(options.parameters.AbsolutePrefix)
|
||||||
|
if link[0] != '/' {
|
||||||
|
out.WriteByte('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
|
||||||
|
if options.flags&HTML_SKIP_IMAGES != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString("<img src=\"")
|
||||||
|
options.maybeWriteAbsolutePrefix(out, link)
|
||||||
|
attrEscape(out, link)
|
||||||
|
out.WriteString("\" alt=\"")
|
||||||
|
if len(alt) > 0 {
|
||||||
|
attrEscape(out, alt)
|
||||||
|
}
|
||||||
|
if len(title) > 0 {
|
||||||
|
out.WriteString("\" title=\"")
|
||||||
|
attrEscape(out, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteByte('"')
|
||||||
|
out.WriteString(options.closeTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) LineBreak(out *bytes.Buffer) {
|
||||||
|
out.WriteString("<br")
|
||||||
|
out.WriteString(options.closeTag)
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
|
||||||
|
if options.flags&HTML_SKIP_LINKS != 0 {
|
||||||
|
// write the link text out but don't link it, just mark it with typewriter font
|
||||||
|
out.WriteString("<tt>")
|
||||||
|
attrEscape(out, content)
|
||||||
|
out.WriteString("</tt>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) {
|
||||||
|
// write the link text out but don't link it, just mark it with typewriter font
|
||||||
|
out.WriteString("<tt>")
|
||||||
|
attrEscape(out, content)
|
||||||
|
out.WriteString("</tt>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString("<a href=\"")
|
||||||
|
options.maybeWriteAbsolutePrefix(out, link)
|
||||||
|
attrEscape(out, link)
|
||||||
|
if len(title) > 0 {
|
||||||
|
out.WriteString("\" title=\"")
|
||||||
|
attrEscape(out, title)
|
||||||
|
}
|
||||||
|
var relAttrs []string
|
||||||
|
if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) {
|
||||||
|
relAttrs = append(relAttrs, "nofollow")
|
||||||
|
}
|
||||||
|
if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) {
|
||||||
|
relAttrs = append(relAttrs, "noreferrer")
|
||||||
|
}
|
||||||
|
if len(relAttrs) > 0 {
|
||||||
|
out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// blank target only add to external link
|
||||||
|
if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) {
|
||||||
|
out.WriteString("\" target=\"_blank")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteString("\">")
|
||||||
|
out.Write(content)
|
||||||
|
out.WriteString("</a>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) RawHtmlTag(out *bytes.Buffer, text []byte) {
|
||||||
|
if options.flags&HTML_SKIP_HTML != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if options.flags&HTML_SKIP_STYLE != 0 && isHtmlTag(text, "style") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if options.flags&HTML_SKIP_LINKS != 0 && isHtmlTag(text, "a") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if options.flags&HTML_SKIP_IMAGES != 0 && isHtmlTag(text, "img") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.Write(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) TripleEmphasis(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("<strong><em>")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("</em></strong>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) StrikeThrough(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("<del>")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("</del>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
|
||||||
|
slug := slugify(ref)
|
||||||
|
out.WriteString(`<sup class="footnote-ref" id="`)
|
||||||
|
out.WriteString(`fnref:`)
|
||||||
|
out.WriteString(options.parameters.FootnoteAnchorPrefix)
|
||||||
|
out.Write(slug)
|
||||||
|
out.WriteString(`"><a rel="footnote" href="#`)
|
||||||
|
out.WriteString(`fn:`)
|
||||||
|
out.WriteString(options.parameters.FootnoteAnchorPrefix)
|
||||||
|
out.Write(slug)
|
||||||
|
out.WriteString(`">`)
|
||||||
|
out.WriteString(strconv.Itoa(id))
|
||||||
|
out.WriteString(`</a></sup>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) Entity(out *bytes.Buffer, entity []byte) {
|
||||||
|
out.Write(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) NormalText(out *bytes.Buffer, text []byte) {
|
||||||
|
if options.flags&HTML_USE_SMARTYPANTS != 0 {
|
||||||
|
options.Smartypants(out, text)
|
||||||
|
} else {
|
||||||
|
attrEscape(out, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) Smartypants(out *bytes.Buffer, text []byte) {
|
||||||
|
smrt := smartypantsData{false, false}
|
||||||
|
|
||||||
|
// first do normal entity escaping
|
||||||
|
var escaped bytes.Buffer
|
||||||
|
attrEscape(&escaped, text)
|
||||||
|
text = escaped.Bytes()
|
||||||
|
|
||||||
|
mark := 0
|
||||||
|
for i := 0; i < len(text); i++ {
|
||||||
|
if action := options.smartypants[text[i]]; action != nil {
|
||||||
|
if i > mark {
|
||||||
|
out.Write(text[mark:i])
|
||||||
|
}
|
||||||
|
|
||||||
|
previousChar := byte(0)
|
||||||
|
if i > 0 {
|
||||||
|
previousChar = text[i-1]
|
||||||
|
}
|
||||||
|
i += action(out, &smrt, previousChar, text[i:])
|
||||||
|
mark = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mark < len(text) {
|
||||||
|
out.Write(text[mark:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) DocumentHeader(out *bytes.Buffer) {
|
||||||
|
if options.flags&HTML_COMPLETE_PAGE == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ending := ""
|
||||||
|
if options.flags&HTML_USE_XHTML != 0 {
|
||||||
|
out.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
|
||||||
|
out.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
|
||||||
|
out.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
|
||||||
|
ending = " /"
|
||||||
|
} else {
|
||||||
|
out.WriteString("<!DOCTYPE html>\n")
|
||||||
|
out.WriteString("<html>\n")
|
||||||
|
}
|
||||||
|
out.WriteString("<head>\n")
|
||||||
|
out.WriteString(" <title>")
|
||||||
|
options.NormalText(out, []byte(options.title))
|
||||||
|
out.WriteString("</title>\n")
|
||||||
|
out.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
|
||||||
|
out.WriteString(VERSION)
|
||||||
|
out.WriteString("\"")
|
||||||
|
out.WriteString(ending)
|
||||||
|
out.WriteString(">\n")
|
||||||
|
out.WriteString(" <meta charset=\"utf-8\"")
|
||||||
|
out.WriteString(ending)
|
||||||
|
out.WriteString(">\n")
|
||||||
|
if options.css != "" {
|
||||||
|
out.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"")
|
||||||
|
attrEscape(out, []byte(options.css))
|
||||||
|
out.WriteString("\"")
|
||||||
|
out.WriteString(ending)
|
||||||
|
out.WriteString(">\n")
|
||||||
|
}
|
||||||
|
out.WriteString("</head>\n")
|
||||||
|
out.WriteString("<body>\n")
|
||||||
|
|
||||||
|
options.tocMarker = out.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) DocumentFooter(out *bytes.Buffer) {
|
||||||
|
// finalize and insert the table of contents
|
||||||
|
if options.flags&HTML_TOC != 0 {
|
||||||
|
options.TocFinalize()
|
||||||
|
|
||||||
|
// now we have to insert the table of contents into the document
|
||||||
|
var temp bytes.Buffer
|
||||||
|
|
||||||
|
// start by making a copy of everything after the document header
|
||||||
|
temp.Write(out.Bytes()[options.tocMarker:])
|
||||||
|
|
||||||
|
// now clear the copied material from the main output buffer
|
||||||
|
out.Truncate(options.tocMarker)
|
||||||
|
|
||||||
|
// corner case spacing issue
|
||||||
|
if options.flags&HTML_COMPLETE_PAGE != 0 {
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert the table of contents
|
||||||
|
out.WriteString("<nav>\n")
|
||||||
|
out.Write(options.toc.Bytes())
|
||||||
|
out.WriteString("</nav>\n")
|
||||||
|
|
||||||
|
// corner case spacing issue
|
||||||
|
if options.flags&HTML_COMPLETE_PAGE == 0 && options.flags&HTML_OMIT_CONTENTS == 0 {
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// write out everything that came after it
|
||||||
|
if options.flags&HTML_OMIT_CONTENTS == 0 {
|
||||||
|
out.Write(temp.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.flags&HTML_COMPLETE_PAGE != 0 {
|
||||||
|
out.WriteString("\n</body>\n")
|
||||||
|
out.WriteString("</html>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) TocHeaderWithAnchor(text []byte, level int, anchor string) {
|
||||||
|
for level > options.currentLevel {
|
||||||
|
switch {
|
||||||
|
case bytes.HasSuffix(options.toc.Bytes(), []byte("</li>\n")):
|
||||||
|
// this sublist can nest underneath a header
|
||||||
|
size := options.toc.Len()
|
||||||
|
options.toc.Truncate(size - len("</li>\n"))
|
||||||
|
|
||||||
|
case options.currentLevel > 0:
|
||||||
|
options.toc.WriteString("<li>")
|
||||||
|
}
|
||||||
|
if options.toc.Len() > 0 {
|
||||||
|
options.toc.WriteByte('\n')
|
||||||
|
}
|
||||||
|
options.toc.WriteString("<ul>\n")
|
||||||
|
options.currentLevel++
|
||||||
|
}
|
||||||
|
|
||||||
|
for level < options.currentLevel {
|
||||||
|
options.toc.WriteString("</ul>")
|
||||||
|
if options.currentLevel > 1 {
|
||||||
|
options.toc.WriteString("</li>\n")
|
||||||
|
}
|
||||||
|
options.currentLevel--
|
||||||
|
}
|
||||||
|
|
||||||
|
options.toc.WriteString("<li><a href=\"#")
|
||||||
|
if anchor != "" {
|
||||||
|
options.toc.WriteString(anchor)
|
||||||
|
} else {
|
||||||
|
options.toc.WriteString("toc_")
|
||||||
|
options.toc.WriteString(strconv.Itoa(options.headerCount))
|
||||||
|
}
|
||||||
|
options.toc.WriteString("\">")
|
||||||
|
options.headerCount++
|
||||||
|
|
||||||
|
options.toc.Write(text)
|
||||||
|
|
||||||
|
options.toc.WriteString("</a></li>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) TocHeader(text []byte, level int) {
|
||||||
|
options.TocHeaderWithAnchor(text, level, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) TocFinalize() {
|
||||||
|
for options.currentLevel > 1 {
|
||||||
|
options.toc.WriteString("</ul></li>\n")
|
||||||
|
options.currentLevel--
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.currentLevel > 0 {
|
||||||
|
options.toc.WriteString("</ul>\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHtmlTag(tag []byte, tagname string) bool {
|
||||||
|
found, _ := findHtmlTagPos(tag, tagname)
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for a character, but ignore it when it's in any kind of quotes, it
|
||||||
|
// might be JavaScript
|
||||||
|
func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
|
||||||
|
inSingleQuote := false
|
||||||
|
inDoubleQuote := false
|
||||||
|
inGraveQuote := false
|
||||||
|
i := start
|
||||||
|
for i < len(html) {
|
||||||
|
switch {
|
||||||
|
case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
|
||||||
|
return i
|
||||||
|
case html[i] == '\'':
|
||||||
|
inSingleQuote = !inSingleQuote
|
||||||
|
case html[i] == '"':
|
||||||
|
inDoubleQuote = !inDoubleQuote
|
||||||
|
case html[i] == '`':
|
||||||
|
inGraveQuote = !inGraveQuote
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return start
|
||||||
|
}
|
||||||
|
|
||||||
|
func findHtmlTagPos(tag []byte, tagname string) (bool, int) {
|
||||||
|
i := 0
|
||||||
|
if i < len(tag) && tag[0] != '<' {
|
||||||
|
return false, -1
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
i = skipSpace(tag, i)
|
||||||
|
|
||||||
|
if i < len(tag) && tag[i] == '/' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
i = skipSpace(tag, i)
|
||||||
|
j := 0
|
||||||
|
for ; i < len(tag); i, j = i+1, j+1 {
|
||||||
|
if j >= len(tagname) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ToLower(string(tag[i]))[0] != tagname[j] {
|
||||||
|
return false, -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(tag) {
|
||||||
|
return false, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
|
||||||
|
if rightAngle > i {
|
||||||
|
return true, rightAngle
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipUntilChar(text []byte, start int, char byte) int {
|
||||||
|
i := start
|
||||||
|
for i < len(text) && text[i] != char {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipSpace(tag []byte, i int) int {
|
||||||
|
for i < len(tag) && isspace(tag[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipChar(data []byte, start int, char byte) int {
|
||||||
|
i := start
|
||||||
|
for i < len(data) && data[i] == char {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func doubleSpace(out *bytes.Buffer) {
|
||||||
|
if out.Len() > 0 {
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRelativeLink(link []byte) (yes bool) {
|
||||||
|
// a tag begin with '#'
|
||||||
|
if link[0] == '#' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// link begin with '/' but not '//', the second maybe a protocol relative link
|
||||||
|
if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// only the root '/'
|
||||||
|
if len(link) == 1 && link[0] == '/' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// current directory : begin with "./"
|
||||||
|
if bytes.HasPrefix(link, []byte("./")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// parent directory : begin with "../"
|
||||||
|
if bytes.HasPrefix(link, []byte("../")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Html) ensureUniqueHeaderID(id string) string {
|
||||||
|
for count, found := options.headerIDs[id]; found; count, found = options.headerIDs[id] {
|
||||||
|
tmp := fmt.Sprintf("%s-%d", id, count+1)
|
||||||
|
|
||||||
|
if _, tmpFound := options.headerIDs[tmp]; !tmpFound {
|
||||||
|
options.headerIDs[id] = count + 1
|
||||||
|
id = tmp
|
||||||
|
} else {
|
||||||
|
id = id + "-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := options.headerIDs[id]; !found {
|
||||||
|
options.headerIDs[id] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,332 @@
|
||||||
|
//
|
||||||
|
// Blackfriday Markdown Processor
|
||||||
|
// Available at http://github.com/russross/blackfriday
|
||||||
|
//
|
||||||
|
// Copyright © 2011 Russ Ross <russ@russross.com>.
|
||||||
|
// Distributed under the Simplified BSD License.
|
||||||
|
// See README.md for details.
|
||||||
|
//
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// LaTeX rendering backend
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
package blackfriday
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Latex is a type that implements the Renderer interface for LaTeX output.
|
||||||
|
//
|
||||||
|
// Do not create this directly, instead use the LatexRenderer function.
|
||||||
|
type Latex struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// LatexRenderer creates and configures a Latex object, which
|
||||||
|
// satisfies the Renderer interface.
|
||||||
|
//
|
||||||
|
// flags is a set of LATEX_* options ORed together (currently no such options
|
||||||
|
// are defined).
|
||||||
|
func LatexRenderer(flags int) Renderer {
|
||||||
|
return &Latex{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) GetFlags() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// render code chunks using verbatim, or listings if we have a language
|
||||||
|
func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, lang string) {
|
||||||
|
if lang == "" {
|
||||||
|
out.WriteString("\n\\begin{verbatim}\n")
|
||||||
|
} else {
|
||||||
|
out.WriteString("\n\\begin{lstlisting}[language=")
|
||||||
|
out.WriteString(lang)
|
||||||
|
out.WriteString("]\n")
|
||||||
|
}
|
||||||
|
out.Write(text)
|
||||||
|
if lang == "" {
|
||||||
|
out.WriteString("\n\\end{verbatim}\n")
|
||||||
|
} else {
|
||||||
|
out.WriteString("\n\\end{lstlisting}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) TitleBlock(out *bytes.Buffer, text []byte) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) BlockQuote(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("\n\\begin{quotation}\n")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("\n\\end{quotation}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) BlockHtml(out *bytes.Buffer, text []byte) {
|
||||||
|
// a pretty lame thing to do...
|
||||||
|
out.WriteString("\n\\begin{verbatim}\n")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("\n\\end{verbatim}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) Header(out *bytes.Buffer, text func() bool, level int, id string) {
|
||||||
|
marker := out.Len()
|
||||||
|
|
||||||
|
switch level {
|
||||||
|
case 1:
|
||||||
|
out.WriteString("\n\\section{")
|
||||||
|
case 2:
|
||||||
|
out.WriteString("\n\\subsection{")
|
||||||
|
case 3:
|
||||||
|
out.WriteString("\n\\subsubsection{")
|
||||||
|
case 4:
|
||||||
|
out.WriteString("\n\\paragraph{")
|
||||||
|
case 5:
|
||||||
|
out.WriteString("\n\\subparagraph{")
|
||||||
|
case 6:
|
||||||
|
out.WriteString("\n\\textbf{")
|
||||||
|
}
|
||||||
|
if !text() {
|
||||||
|
out.Truncate(marker)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.WriteString("}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) HRule(out *bytes.Buffer) {
|
||||||
|
out.WriteString("\n\\HRule\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||||
|
marker := out.Len()
|
||||||
|
if flags&LIST_TYPE_ORDERED != 0 {
|
||||||
|
out.WriteString("\n\\begin{enumerate}\n")
|
||||||
|
} else {
|
||||||
|
out.WriteString("\n\\begin{itemize}\n")
|
||||||
|
}
|
||||||
|
if !text() {
|
||||||
|
out.Truncate(marker)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if flags&LIST_TYPE_ORDERED != 0 {
|
||||||
|
out.WriteString("\n\\end{enumerate}\n")
|
||||||
|
} else {
|
||||||
|
out.WriteString("\n\\end{itemize}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) ListItem(out *bytes.Buffer, text []byte, flags int) {
|
||||||
|
out.WriteString("\n\\item ")
|
||||||
|
out.Write(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) Paragraph(out *bytes.Buffer, text func() bool) {
|
||||||
|
marker := out.Len()
|
||||||
|
out.WriteString("\n")
|
||||||
|
if !text() {
|
||||||
|
out.Truncate(marker)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
|
||||||
|
out.WriteString("\n\\begin{tabular}{")
|
||||||
|
for _, elt := range columnData {
|
||||||
|
switch elt {
|
||||||
|
case TABLE_ALIGNMENT_LEFT:
|
||||||
|
out.WriteByte('l')
|
||||||
|
case TABLE_ALIGNMENT_RIGHT:
|
||||||
|
out.WriteByte('r')
|
||||||
|
default:
|
||||||
|
out.WriteByte('c')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.WriteString("}\n")
|
||||||
|
out.Write(header)
|
||||||
|
out.WriteString(" \\\\\n\\hline\n")
|
||||||
|
out.Write(body)
|
||||||
|
out.WriteString("\n\\end{tabular}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) TableRow(out *bytes.Buffer, text []byte) {
|
||||||
|
if out.Len() > 0 {
|
||||||
|
out.WriteString(" \\\\\n")
|
||||||
|
}
|
||||||
|
out.Write(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
|
||||||
|
if out.Len() > 0 {
|
||||||
|
out.WriteString(" & ")
|
||||||
|
}
|
||||||
|
out.Write(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) TableCell(out *bytes.Buffer, text []byte, align int) {
|
||||||
|
if out.Len() > 0 {
|
||||||
|
out.WriteString(" & ")
|
||||||
|
}
|
||||||
|
out.Write(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this
|
||||||
|
func (options *Latex) Footnotes(out *bytes.Buffer, text func() bool) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) AutoLink(out *bytes.Buffer, link []byte, kind int) {
|
||||||
|
out.WriteString("\\href{")
|
||||||
|
if kind == LINK_TYPE_EMAIL {
|
||||||
|
out.WriteString("mailto:")
|
||||||
|
}
|
||||||
|
out.Write(link)
|
||||||
|
out.WriteString("}{")
|
||||||
|
out.Write(link)
|
||||||
|
out.WriteString("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) CodeSpan(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("\\texttt{")
|
||||||
|
escapeSpecialChars(out, text)
|
||||||
|
out.WriteString("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) DoubleEmphasis(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("\\textbf{")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) Emphasis(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("\\textit{")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
|
||||||
|
if bytes.HasPrefix(link, []byte("http://")) || bytes.HasPrefix(link, []byte("https://")) {
|
||||||
|
// treat it like a link
|
||||||
|
out.WriteString("\\href{")
|
||||||
|
out.Write(link)
|
||||||
|
out.WriteString("}{")
|
||||||
|
out.Write(alt)
|
||||||
|
out.WriteString("}")
|
||||||
|
} else {
|
||||||
|
out.WriteString("\\includegraphics{")
|
||||||
|
out.Write(link)
|
||||||
|
out.WriteString("}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) LineBreak(out *bytes.Buffer) {
|
||||||
|
out.WriteString(" \\\\\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
|
||||||
|
out.WriteString("\\href{")
|
||||||
|
out.Write(link)
|
||||||
|
out.WriteString("}{")
|
||||||
|
out.Write(content)
|
||||||
|
out.WriteString("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) RawHtmlTag(out *bytes.Buffer, tag []byte) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) TripleEmphasis(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("\\textbf{\\textit{")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("}}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) StrikeThrough(out *bytes.Buffer, text []byte) {
|
||||||
|
out.WriteString("\\sout{")
|
||||||
|
out.Write(text)
|
||||||
|
out.WriteString("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this
|
||||||
|
func (options *Latex) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func needsBackslash(c byte) bool {
|
||||||
|
for _, r := range []byte("_{}%$&\\~#") {
|
||||||
|
if c == r {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeSpecialChars(out *bytes.Buffer, text []byte) {
|
||||||
|
for i := 0; i < len(text); i++ {
|
||||||
|
// directly copy normal characters
|
||||||
|
org := i
|
||||||
|
|
||||||
|
for i < len(text) && !needsBackslash(text[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i > org {
|
||||||
|
out.Write(text[org:i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// escape a character
|
||||||
|
if i >= len(text) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
out.WriteByte('\\')
|
||||||
|
out.WriteByte(text[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) Entity(out *bytes.Buffer, entity []byte) {
|
||||||
|
// TODO: convert this into a unicode character or something
|
||||||
|
out.Write(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) NormalText(out *bytes.Buffer, text []byte) {
|
||||||
|
escapeSpecialChars(out, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// header and footer
|
||||||
|
func (options *Latex) DocumentHeader(out *bytes.Buffer) {
|
||||||
|
out.WriteString("\\documentclass{article}\n")
|
||||||
|
out.WriteString("\n")
|
||||||
|
out.WriteString("\\usepackage{graphicx}\n")
|
||||||
|
out.WriteString("\\usepackage{listings}\n")
|
||||||
|
out.WriteString("\\usepackage[margin=1in]{geometry}\n")
|
||||||
|
out.WriteString("\\usepackage[utf8]{inputenc}\n")
|
||||||
|
out.WriteString("\\usepackage{verbatim}\n")
|
||||||
|
out.WriteString("\\usepackage[normalem]{ulem}\n")
|
||||||
|
out.WriteString("\\usepackage{hyperref}\n")
|
||||||
|
out.WriteString("\n")
|
||||||
|
out.WriteString("\\hypersetup{colorlinks,%\n")
|
||||||
|
out.WriteString(" citecolor=black,%\n")
|
||||||
|
out.WriteString(" filecolor=black,%\n")
|
||||||
|
out.WriteString(" linkcolor=black,%\n")
|
||||||
|
out.WriteString(" urlcolor=black,%\n")
|
||||||
|
out.WriteString(" pdfstartview=FitH,%\n")
|
||||||
|
out.WriteString(" breaklinks=true,%\n")
|
||||||
|
out.WriteString(" pdfauthor={Blackfriday Markdown Processor v")
|
||||||
|
out.WriteString(VERSION)
|
||||||
|
out.WriteString("}}\n")
|
||||||
|
out.WriteString("\n")
|
||||||
|
out.WriteString("\\newcommand{\\HRule}{\\rule{\\linewidth}{0.5mm}}\n")
|
||||||
|
out.WriteString("\\addtolength{\\parskip}{0.5\\baselineskip}\n")
|
||||||
|
out.WriteString("\\parindent=0pt\n")
|
||||||
|
out.WriteString("\n")
|
||||||
|
out.WriteString("\\begin{document}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Latex) DocumentFooter(out *bytes.Buffer) {
|
||||||
|
out.WriteString("\n\\end{document}\n")
|
||||||
|
}
|
|
@ -0,0 +1,926 @@
|
||||||
|
//
|
||||||
|
// Blackfriday Markdown Processor
|
||||||
|
// Available at http://github.com/russross/blackfriday
|
||||||
|
//
|
||||||
|
// Copyright © 2011 Russ Ross <russ@russross.com>.
|
||||||
|
// Distributed under the Simplified BSD License.
|
||||||
|
// See README.md for details.
|
||||||
|
//
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Markdown parsing and processing
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
// Blackfriday markdown processor.
|
||||||
|
//
|
||||||
|
// Translates plain text with simple formatting rules into HTML or LaTeX.
|
||||||
|
package blackfriday
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
const VERSION = "1.5"
|
||||||
|
|
||||||
|
// These are the supported markdown parsing extensions.
|
||||||
|
// OR these values together to select multiple extensions.
|
||||||
|
const (
|
||||||
|
EXTENSION_NO_INTRA_EMPHASIS = 1 << iota // ignore emphasis markers inside words
|
||||||
|
EXTENSION_TABLES // render tables
|
||||||
|
EXTENSION_FENCED_CODE // render fenced code blocks
|
||||||
|
EXTENSION_AUTOLINK // detect embedded URLs that are not explicitly marked
|
||||||
|
EXTENSION_STRIKETHROUGH // strikethrough text using ~~test~~
|
||||||
|
EXTENSION_LAX_HTML_BLOCKS // loosen up HTML block parsing rules
|
||||||
|
EXTENSION_SPACE_HEADERS // be strict about prefix header rules
|
||||||
|
EXTENSION_HARD_LINE_BREAK // translate newlines into line breaks
|
||||||
|
EXTENSION_TAB_SIZE_EIGHT // expand tabs to eight spaces instead of four
|
||||||
|
EXTENSION_FOOTNOTES // Pandoc-style footnotes
|
||||||
|
EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block
|
||||||
|
EXTENSION_HEADER_IDS // specify header IDs with {#id}
|
||||||
|
EXTENSION_TITLEBLOCK // Titleblock ala pandoc
|
||||||
|
EXTENSION_AUTO_HEADER_IDS // Create the header ID from the text
|
||||||
|
EXTENSION_BACKSLASH_LINE_BREAK // translate trailing backslashes into line breaks
|
||||||
|
EXTENSION_DEFINITION_LISTS // render definition lists
|
||||||
|
|
||||||
|
commonHtmlFlags = 0 |
|
||||||
|
HTML_USE_XHTML |
|
||||||
|
HTML_USE_SMARTYPANTS |
|
||||||
|
HTML_SMARTYPANTS_FRACTIONS |
|
||||||
|
HTML_SMARTYPANTS_DASHES |
|
||||||
|
HTML_SMARTYPANTS_LATEX_DASHES
|
||||||
|
|
||||||
|
commonExtensions = 0 |
|
||||||
|
EXTENSION_NO_INTRA_EMPHASIS |
|
||||||
|
EXTENSION_TABLES |
|
||||||
|
EXTENSION_FENCED_CODE |
|
||||||
|
EXTENSION_AUTOLINK |
|
||||||
|
EXTENSION_STRIKETHROUGH |
|
||||||
|
EXTENSION_SPACE_HEADERS |
|
||||||
|
EXTENSION_HEADER_IDS |
|
||||||
|
EXTENSION_BACKSLASH_LINE_BREAK |
|
||||||
|
EXTENSION_DEFINITION_LISTS
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are the possible flag values for the link 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 (
|
||||||
|
LINK_TYPE_NOT_AUTOLINK = iota
|
||||||
|
LINK_TYPE_NORMAL
|
||||||
|
LINK_TYPE_EMAIL
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
LIST_TYPE_ORDERED = 1 << iota
|
||||||
|
LIST_TYPE_DEFINITION
|
||||||
|
LIST_TYPE_TERM
|
||||||
|
LIST_ITEM_CONTAINS_BLOCK
|
||||||
|
LIST_ITEM_BEGINNING_OF_LIST
|
||||||
|
LIST_ITEM_END_OF_LIST
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
TABLE_ALIGNMENT_LEFT = 1 << iota
|
||||||
|
TABLE_ALIGNMENT_RIGHT
|
||||||
|
TABLE_ALIGNMENT_CENTER = (TABLE_ALIGNMENT_LEFT | TABLE_ALIGNMENT_RIGHT)
|
||||||
|
)
|
||||||
|
|
||||||
|
// The size of a tab stop.
|
||||||
|
const (
|
||||||
|
TAB_SIZE_DEFAULT = 4
|
||||||
|
TAB_SIZE_EIGHT = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
// blockTags is a set of tags that are recognized as HTML block tags.
|
||||||
|
// Any of these can be included in markdown text without special escaping.
|
||||||
|
var blockTags = map[string]struct{}{
|
||||||
|
"blockquote": {},
|
||||||
|
"del": {},
|
||||||
|
"div": {},
|
||||||
|
"dl": {},
|
||||||
|
"fieldset": {},
|
||||||
|
"form": {},
|
||||||
|
"h1": {},
|
||||||
|
"h2": {},
|
||||||
|
"h3": {},
|
||||||
|
"h4": {},
|
||||||
|
"h5": {},
|
||||||
|
"h6": {},
|
||||||
|
"iframe": {},
|
||||||
|
"ins": {},
|
||||||
|
"math": {},
|
||||||
|
"noscript": {},
|
||||||
|
"ol": {},
|
||||||
|
"pre": {},
|
||||||
|
"p": {},
|
||||||
|
"script": {},
|
||||||
|
"style": {},
|
||||||
|
"table": {},
|
||||||
|
"ul": {},
|
||||||
|
|
||||||
|
// HTML5
|
||||||
|
"address": {},
|
||||||
|
"article": {},
|
||||||
|
"aside": {},
|
||||||
|
"canvas": {},
|
||||||
|
"figcaption": {},
|
||||||
|
"figure": {},
|
||||||
|
"footer": {},
|
||||||
|
"header": {},
|
||||||
|
"hgroup": {},
|
||||||
|
"main": {},
|
||||||
|
"nav": {},
|
||||||
|
"output": {},
|
||||||
|
"progress": {},
|
||||||
|
"section": {},
|
||||||
|
"video": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderer is the rendering interface.
|
||||||
|
// This is mostly of interest if you are implementing a new rendering format.
|
||||||
|
//
|
||||||
|
// When a byte slice is provided, it contains the (rendered) contents of the
|
||||||
|
// element.
|
||||||
|
//
|
||||||
|
// When a callback is provided instead, it will write the contents of the
|
||||||
|
// respective element directly to the output buffer and return true on success.
|
||||||
|
// If the callback returns false, the rendering function should reset the
|
||||||
|
// output buffer as though it had never been called.
|
||||||
|
//
|
||||||
|
// Currently Html and Latex implementations are provided
|
||||||
|
type Renderer interface {
|
||||||
|
// block-level callbacks
|
||||||
|
BlockCode(out *bytes.Buffer, text []byte, lang string)
|
||||||
|
BlockQuote(out *bytes.Buffer, text []byte)
|
||||||
|
BlockHtml(out *bytes.Buffer, text []byte)
|
||||||
|
Header(out *bytes.Buffer, text func() bool, level int, id string)
|
||||||
|
HRule(out *bytes.Buffer)
|
||||||
|
List(out *bytes.Buffer, text func() bool, flags int)
|
||||||
|
ListItem(out *bytes.Buffer, text []byte, flags int)
|
||||||
|
Paragraph(out *bytes.Buffer, text func() bool)
|
||||||
|
Table(out *bytes.Buffer, header []byte, body []byte, columnData []int)
|
||||||
|
TableRow(out *bytes.Buffer, text []byte)
|
||||||
|
TableHeaderCell(out *bytes.Buffer, text []byte, flags int)
|
||||||
|
TableCell(out *bytes.Buffer, text []byte, flags int)
|
||||||
|
Footnotes(out *bytes.Buffer, text func() bool)
|
||||||
|
FootnoteItem(out *bytes.Buffer, name, text []byte, flags int)
|
||||||
|
TitleBlock(out *bytes.Buffer, text []byte)
|
||||||
|
|
||||||
|
// Span-level callbacks
|
||||||
|
AutoLink(out *bytes.Buffer, link []byte, kind int)
|
||||||
|
CodeSpan(out *bytes.Buffer, text []byte)
|
||||||
|
DoubleEmphasis(out *bytes.Buffer, text []byte)
|
||||||
|
Emphasis(out *bytes.Buffer, text []byte)
|
||||||
|
Image(out *bytes.Buffer, link []byte, title []byte, alt []byte)
|
||||||
|
LineBreak(out *bytes.Buffer)
|
||||||
|
Link(out *bytes.Buffer, link []byte, title []byte, content []byte)
|
||||||
|
RawHtmlTag(out *bytes.Buffer, tag []byte)
|
||||||
|
TripleEmphasis(out *bytes.Buffer, text []byte)
|
||||||
|
StrikeThrough(out *bytes.Buffer, text []byte)
|
||||||
|
FootnoteRef(out *bytes.Buffer, ref []byte, id int)
|
||||||
|
|
||||||
|
// Low-level callbacks
|
||||||
|
Entity(out *bytes.Buffer, entity []byte)
|
||||||
|
NormalText(out *bytes.Buffer, text []byte)
|
||||||
|
|
||||||
|
// Header and footer
|
||||||
|
DocumentHeader(out *bytes.Buffer)
|
||||||
|
DocumentFooter(out *bytes.Buffer)
|
||||||
|
|
||||||
|
GetFlags() int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback functions for inline parsing. One such function is defined
|
||||||
|
// for each character that triggers a response when parsing inline data.
|
||||||
|
type inlineParser func(p *parser, out *bytes.Buffer, data []byte, offset int) int
|
||||||
|
|
||||||
|
// Parser holds runtime state used by the parser.
|
||||||
|
// This is constructed by the Markdown function.
|
||||||
|
type parser struct {
|
||||||
|
r Renderer
|
||||||
|
refOverride ReferenceOverrideFunc
|
||||||
|
refs map[string]*reference
|
||||||
|
inlineCallback [256]inlineParser
|
||||||
|
flags int
|
||||||
|
nesting int
|
||||||
|
maxNesting int
|
||||||
|
insideLink bool
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) getRef(refid string) (ref *reference, found bool) {
|
||||||
|
if p.refOverride != nil {
|
||||||
|
r, overridden := p.refOverride(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
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Public interface
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Options represents configurable overrides and callbacks (in addition to the
|
||||||
|
// extension flag set) for configuring a Markdown parse.
|
||||||
|
type Options struct {
|
||||||
|
// Extensions is a flag set of bit-wise ORed extension bits. See the
|
||||||
|
// EXTENSION_* flags defined in this package.
|
||||||
|
Extensions int
|
||||||
|
|
||||||
|
// ReferenceOverride is an optional function callback that is called every
|
||||||
|
// time a reference is resolved.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkdownBasic is a convenience function for simple rendering.
|
||||||
|
// It processes markdown input with no extensions enabled.
|
||||||
|
func MarkdownBasic(input []byte) []byte {
|
||||||
|
// set up the HTML renderer
|
||||||
|
htmlFlags := HTML_USE_XHTML
|
||||||
|
renderer := HtmlRenderer(htmlFlags, "", "")
|
||||||
|
|
||||||
|
// set up the parser
|
||||||
|
return MarkdownOptions(input, renderer, Options{Extensions: 0})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Markdown with most useful extensions enabled
|
||||||
|
// MarkdownCommon is a convenience function for simple rendering.
|
||||||
|
// It processes markdown input with common extensions enabled, including:
|
||||||
|
//
|
||||||
|
// * Smartypants processing with smart fractions and LaTeX dashes
|
||||||
|
//
|
||||||
|
// * Intra-word emphasis suppression
|
||||||
|
//
|
||||||
|
// * Tables
|
||||||
|
//
|
||||||
|
// * Fenced code blocks
|
||||||
|
//
|
||||||
|
// * Autolinking
|
||||||
|
//
|
||||||
|
// * Strikethrough support
|
||||||
|
//
|
||||||
|
// * Strict header parsing
|
||||||
|
//
|
||||||
|
// * Custom Header IDs
|
||||||
|
func MarkdownCommon(input []byte) []byte {
|
||||||
|
// set up the HTML renderer
|
||||||
|
renderer := HtmlRenderer(commonHtmlFlags, "", "")
|
||||||
|
return MarkdownOptions(input, renderer, Options{
|
||||||
|
Extensions: commonExtensions})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markdown is the main rendering function.
|
||||||
|
// It parses and renders a block of markdown-encoded text.
|
||||||
|
// The supplied Renderer is used to format the output, and extensions dictates
|
||||||
|
// which non-standard extensions are enabled.
|
||||||
|
//
|
||||||
|
// To use the supplied Html or LaTeX renderers, see HtmlRenderer and
|
||||||
|
// LatexRenderer, respectively.
|
||||||
|
func Markdown(input []byte, renderer Renderer, extensions int) []byte {
|
||||||
|
return MarkdownOptions(input, renderer, Options{
|
||||||
|
Extensions: extensions})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkdownOptions is just like Markdown but takes additional options through
|
||||||
|
// the Options struct.
|
||||||
|
func MarkdownOptions(input []byte, renderer Renderer, opts Options) []byte {
|
||||||
|
// no point in parsing if we can't render
|
||||||
|
if renderer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions := opts.Extensions
|
||||||
|
|
||||||
|
// fill in the render structure
|
||||||
|
p := new(parser)
|
||||||
|
p.r = renderer
|
||||||
|
p.flags = extensions
|
||||||
|
p.refOverride = opts.ReferenceOverride
|
||||||
|
p.refs = make(map[string]*reference)
|
||||||
|
p.maxNesting = 16
|
||||||
|
p.insideLink = false
|
||||||
|
|
||||||
|
// register inline parsers
|
||||||
|
p.inlineCallback['*'] = emphasis
|
||||||
|
p.inlineCallback['_'] = emphasis
|
||||||
|
if extensions&EXTENSION_STRIKETHROUGH != 0 {
|
||||||
|
p.inlineCallback['~'] = emphasis
|
||||||
|
}
|
||||||
|
p.inlineCallback['`'] = codeSpan
|
||||||
|
p.inlineCallback['\n'] = lineBreak
|
||||||
|
p.inlineCallback['['] = link
|
||||||
|
p.inlineCallback['<'] = leftAngle
|
||||||
|
p.inlineCallback['\\'] = escape
|
||||||
|
p.inlineCallback['&'] = entity
|
||||||
|
|
||||||
|
if extensions&EXTENSION_AUTOLINK != 0 {
|
||||||
|
p.inlineCallback[':'] = autoLink
|
||||||
|
}
|
||||||
|
|
||||||
|
if extensions&EXTENSION_FOOTNOTES != 0 {
|
||||||
|
p.notes = make([]*reference, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
first := firstPass(p, input)
|
||||||
|
second := secondPass(p, first)
|
||||||
|
return second
|
||||||
|
}
|
||||||
|
|
||||||
|
// first pass:
|
||||||
|
// - extract references
|
||||||
|
// - expand tabs
|
||||||
|
// - normalize newlines
|
||||||
|
// - copy everything else
|
||||||
|
func firstPass(p *parser, input []byte) []byte {
|
||||||
|
var out bytes.Buffer
|
||||||
|
tabSize := TAB_SIZE_DEFAULT
|
||||||
|
if p.flags&EXTENSION_TAB_SIZE_EIGHT != 0 {
|
||||||
|
tabSize = TAB_SIZE_EIGHT
|
||||||
|
}
|
||||||
|
beg, end := 0, 0
|
||||||
|
lastFencedCodeBlockEnd := 0
|
||||||
|
for beg < len(input) { // iterate over lines
|
||||||
|
if end = isReference(p, input[beg:], tabSize); end > 0 {
|
||||||
|
beg += end
|
||||||
|
} else { // skip to the next line
|
||||||
|
end = beg
|
||||||
|
for end < len(input) && input[end] != '\n' && input[end] != '\r' {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.flags&EXTENSION_FENCED_CODE != 0 {
|
||||||
|
// track fenced code block boundaries to suppress tab expansion
|
||||||
|
// inside them:
|
||||||
|
if beg >= lastFencedCodeBlockEnd {
|
||||||
|
if i := p.fencedCode(&out, input[beg:], false); i > 0 {
|
||||||
|
lastFencedCodeBlockEnd = beg + i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the line body if present
|
||||||
|
if end > beg {
|
||||||
|
if end < lastFencedCodeBlockEnd { // Do not expand tabs while inside fenced code blocks.
|
||||||
|
out.Write(input[beg:end])
|
||||||
|
} else {
|
||||||
|
expandTabs(&out, input[beg:end], tabSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.WriteByte('\n')
|
||||||
|
|
||||||
|
if end < len(input) && input[end] == '\r' {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
if end < len(input) && input[end] == '\n' {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
|
||||||
|
beg = end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty input?
|
||||||
|
if out.Len() == 0 {
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// second pass: actual rendering
|
||||||
|
func secondPass(p *parser, input []byte) []byte {
|
||||||
|
var output bytes.Buffer
|
||||||
|
|
||||||
|
p.r.DocumentHeader(&output)
|
||||||
|
p.block(&output, input)
|
||||||
|
|
||||||
|
if p.flags&EXTENSION_FOOTNOTES != 0 && len(p.notes) > 0 {
|
||||||
|
p.r.Footnotes(&output, func() bool {
|
||||||
|
flags := LIST_ITEM_BEGINNING_OF_LIST
|
||||||
|
for i := 0; i < len(p.notes); i += 1 {
|
||||||
|
ref := p.notes[i]
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if ref.hasBlock {
|
||||||
|
flags |= LIST_ITEM_CONTAINS_BLOCK
|
||||||
|
p.block(&buf, ref.title)
|
||||||
|
} else {
|
||||||
|
p.inline(&buf, ref.title)
|
||||||
|
}
|
||||||
|
p.r.FootnoteItem(&output, ref.link, buf.Bytes(), flags)
|
||||||
|
flags &^= LIST_ITEM_BEGINNING_OF_LIST | LIST_ITEM_CONTAINS_BLOCK
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
p.r.DocumentFooter(&output)
|
||||||
|
|
||||||
|
if p.nesting != 0 {
|
||||||
|
panic("Nesting level did not end at zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// References are parsed and stored in this struct.
|
||||||
|
type reference struct {
|
||||||
|
link []byte
|
||||||
|
title []byte
|
||||||
|
noteId int // 0 if not a footnote ref
|
||||||
|
hasBlock bool
|
||||||
|
text []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
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.flags&EXTENSION_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
|
||||||
|
|
||||||
|
// 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.flags&EXTENSION_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
|
||||||
|
if i == len(data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
linkEnd = i
|
||||||
|
if data[linkOffset] == '<' && data[linkEnd-1] == '>' {
|
||||||
|
linkOffset++
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Miscellaneous helper functions
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
// Test if a character is a punctuation symbol.
|
||||||
|
// Taken from a private function in regexp in the stdlib.
|
||||||
|
func ispunct(c byte) bool {
|
||||||
|
for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") {
|
||||||
|
if c == r {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test if a character is a whitespace character.
|
||||||
|
func isspace(c byte) bool {
|
||||||
|
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test if a character is letter.
|
||||||
|
func isletter(c byte) bool {
|
||||||
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test if a character is a letter or a digit.
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
}
|
|
@ -0,0 +1,400 @@
|
||||||
|
//
|
||||||
|
// Blackfriday Markdown Processor
|
||||||
|
// Available at http://github.com/russross/blackfriday
|
||||||
|
//
|
||||||
|
// Copyright © 2011 Russ Ross <russ@russross.com>.
|
||||||
|
// Distributed under the Simplified BSD License.
|
||||||
|
// See README.md for details.
|
||||||
|
//
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// SmartyPants rendering
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
package blackfriday
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type smartypantsData struct {
|
||||||
|
inSingleQuote bool
|
||||||
|
inDoubleQuote bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func wordBoundary(c byte) bool {
|
||||||
|
return c == 0 || isspace(c) || ispunct(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) 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 ispunct(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 ispunct(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 && ispunct(nextChar):
|
||||||
|
// ["!] hmm... could be ["$1.95] or [</code>"!...]
|
||||||
|
*isOpen = false
|
||||||
|
case isspace(previousChar) && ispunct(nextChar):
|
||||||
|
// [ "!] looks more like [ "$1.95]
|
||||||
|
*isOpen = true
|
||||||
|
case ispunct(previousChar) && ispunct(nextChar):
|
||||||
|
// [!"!] context is not any help here, so toggle
|
||||||
|
*isOpen = !*isOpen
|
||||||
|
case /* isnormal(previousChar) && */ ispunct(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 ispunct(previousChar) /* && isnormal(nextChar) */ :
|
||||||
|
// [!"a] is probably an open
|
||||||
|
*isOpen = true
|
||||||
|
default:
|
||||||
|
// [a'b] maybe a contraction?
|
||||||
|
*isOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteByte('&')
|
||||||
|
if *isOpen {
|
||||||
|
out.WriteByte('l')
|
||||||
|
} else {
|
||||||
|
out.WriteByte('r')
|
||||||
|
}
|
||||||
|
out.WriteByte(quote)
|
||||||
|
out.WriteString("quo;")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func smartSingleQuote(out *bytes.Buffer, smrt *smartypantsData, 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', &smrt.inDoubleQuote) {
|
||||||
|
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', &smrt.inSingleQuote) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteByte(text[0])
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func smartParens(out *bytes.Buffer, smrt *smartypantsData, 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 smartDash(out *bytes.Buffer, smrt *smartypantsData, 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 smartDashLatex(out *bytes.Buffer, smrt *smartypantsData, 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 smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte) int {
|
||||||
|
if bytes.HasPrefix(text, []byte(""")) {
|
||||||
|
nextChar := byte(0)
|
||||||
|
if len(text) >= 7 {
|
||||||
|
nextChar = text[6]
|
||||||
|
}
|
||||||
|
if smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote) {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.HasPrefix(text, []byte("�")) {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteByte('&')
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func smartAmp(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||||
|
return smartAmpVariant(out, smrt, previousChar, text, 'd')
|
||||||
|
}
|
||||||
|
|
||||||
|
func smartAmpAngledQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||||
|
return smartAmpVariant(out, smrt, previousChar, text, 'a')
|
||||||
|
}
|
||||||
|
|
||||||
|
func smartPeriod(out *bytes.Buffer, smrt *smartypantsData, 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 smartBacktick(out *bytes.Buffer, smrt *smartypantsData, 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', &smrt.inDoubleQuote) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.WriteByte(text[0])
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func smartNumberGeneric(out *bytes.Buffer, smrt *smartypantsData, 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 smartNumber(out *bytes.Buffer, smrt *smartypantsData, 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 smartDoubleQuoteVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte) int {
|
||||||
|
nextChar := byte(0)
|
||||||
|
if len(text) > 1 {
|
||||||
|
nextChar = text[1]
|
||||||
|
}
|
||||||
|
if !smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote) {
|
||||||
|
out.WriteString(""")
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func smartDoubleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||||
|
return smartDoubleQuoteVariant(out, smrt, previousChar, text, 'd')
|
||||||
|
}
|
||||||
|
|
||||||
|
func smartAngledDoubleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
|
||||||
|
return smartDoubleQuoteVariant(out, smrt, previousChar, text, 'a')
|
||||||
|
}
|
||||||
|
|
||||||
|
func smartLeftAngle(out *bytes.Buffer, smrt *smartypantsData, 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, smrt *smartypantsData, previousChar byte, text []byte) int
|
||||||
|
|
||||||
|
type smartypantsRenderer [256]smartCallback
|
||||||
|
|
||||||
|
func smartypants(flags int) *smartypantsRenderer {
|
||||||
|
r := new(smartypantsRenderer)
|
||||||
|
if flags&HTML_SMARTYPANTS_ANGLED_QUOTES == 0 {
|
||||||
|
r['"'] = smartDoubleQuote
|
||||||
|
r['&'] = smartAmp
|
||||||
|
} else {
|
||||||
|
r['"'] = smartAngledDoubleQuote
|
||||||
|
r['&'] = smartAmpAngledQuote
|
||||||
|
}
|
||||||
|
r['\''] = smartSingleQuote
|
||||||
|
r['('] = smartParens
|
||||||
|
if flags&HTML_SMARTYPANTS_DASHES != 0 {
|
||||||
|
if flags&HTML_SMARTYPANTS_LATEX_DASHES == 0 {
|
||||||
|
r['-'] = smartDash
|
||||||
|
} else {
|
||||||
|
r['-'] = smartDashLatex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r['.'] = smartPeriod
|
||||||
|
if flags&HTML_SMARTYPANTS_FRACTIONS == 0 {
|
||||||
|
r['1'] = smartNumber
|
||||||
|
r['3'] = smartNumber
|
||||||
|
} else {
|
||||||
|
for ch := '1'; ch <= '9'; ch++ {
|
||||||
|
r[ch] = smartNumberGeneric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r['<'] = smartLeftAngle
|
||||||
|
r['`'] = smartBacktick
|
||||||
|
return r
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2015 Dmitri Shuralyov
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,31 @@
|
||||||
|
# sanitized_anchor_name [![Build Status](https://travis-ci.org/shurcooL/sanitized_anchor_name.svg?branch=master)](https://travis-ci.org/shurcooL/sanitized_anchor_name) [![GoDoc](https://godoc.org/github.com/shurcooL/sanitized_anchor_name?status.svg)](https://godoc.org/github.com/shurcooL/sanitized_anchor_name)
|
||||||
|
|
||||||
|
Package sanitized_anchor_name provides a func to create sanitized anchor names.
|
||||||
|
|
||||||
|
Its logic can be reused by multiple packages to create interoperable anchor names and links to those anchors.
|
||||||
|
|
||||||
|
At this time, it does not try to ensure that generated anchor names are unique, that responsibility falls on the caller.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get -u github.com/shurcooL/sanitized_anchor_name
|
||||||
|
```
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
```Go
|
||||||
|
anchorName := sanitized_anchor_name.Create("This is a header")
|
||||||
|
|
||||||
|
fmt.Println(anchorName)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// this-is-a-header
|
||||||
|
```
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
- [MIT License](LICENSE)
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Package sanitized_anchor_name provides a func to create sanitized anchor names.
|
||||||
|
//
|
||||||
|
// Its logic can be reused by multiple packages to create interoperable anchor names
|
||||||
|
// and links to those anchors.
|
||||||
|
//
|
||||||
|
// At this time, it does not try to ensure that generated anchor names
|
||||||
|
// are unique, that responsibility falls on the caller.
|
||||||
|
package sanitized_anchor_name // import "github.com/shurcooL/sanitized_anchor_name"
|
||||||
|
|
||||||
|
import "unicode"
|
||||||
|
|
||||||
|
// Create returns a sanitized anchor name for the given text.
|
||||||
|
func Create(text string) string {
|
||||||
|
var anchorName []rune
|
||||||
|
var futureDash = false
|
||||||
|
for _, r := range []rune(text) {
|
||||||
|
switch {
|
||||||
|
case unicode.IsLetter(r) || unicode.IsNumber(r):
|
||||||
|
if futureDash && len(anchorName) > 0 {
|
||||||
|
anchorName = append(anchorName, '-')
|
||||||
|
}
|
||||||
|
futureDash = false
|
||||||
|
anchorName = append(anchorName, unicode.ToLower(r))
|
||||||
|
default:
|
||||||
|
futureDash = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(anchorName)
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
// Copyright 2015 Red Hat Inc. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package doc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mangen "github.com/cpuguy83/go-md2man/md2man"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenManTree will generate a man page for this command and all descendants
|
||||||
|
// in the directory given. The header may be nil. This function may not work
|
||||||
|
// correctly if your command names have - in them. If you have `cmd` with two
|
||||||
|
// subcmds, `sub` and `sub-third`. And `sub` has a subcommand called `third`
|
||||||
|
// it is undefined which help output will be in the file `cmd-sub-third.1`.
|
||||||
|
func GenManTree(cmd *cobra.Command, header *GenManHeader, dir string) error {
|
||||||
|
return GenManTreeFromOpts(cmd, GenManTreeOptions{
|
||||||
|
Header: header,
|
||||||
|
Path: dir,
|
||||||
|
CommandSeparator: "_",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenManTreeFromOpts generates a man page for the command and all descendants.
|
||||||
|
// The pages are written to the opts.Path directory.
|
||||||
|
func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error {
|
||||||
|
header := opts.Header
|
||||||
|
if header == nil {
|
||||||
|
header = &GenManHeader{}
|
||||||
|
}
|
||||||
|
for _, c := range cmd.Commands() {
|
||||||
|
if !c.IsAvailableCommand() || c.IsHelpCommand() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := GenManTreeFromOpts(c, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section := "1"
|
||||||
|
if header.Section != "" {
|
||||||
|
section = header.Section
|
||||||
|
}
|
||||||
|
|
||||||
|
separator := "_"
|
||||||
|
if opts.CommandSeparator != "" {
|
||||||
|
separator = opts.CommandSeparator
|
||||||
|
}
|
||||||
|
basename := strings.Replace(cmd.CommandPath(), " ", separator, -1)
|
||||||
|
filename := filepath.Join(opts.Path, basename+"."+section)
|
||||||
|
f, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
headerCopy := *header
|
||||||
|
return GenMan(cmd, &headerCopy, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenManTreeOptions struct {
|
||||||
|
Header *GenManHeader
|
||||||
|
Path string
|
||||||
|
CommandSeparator string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenManHeader is a lot like the .TH header at the start of man pages. These
|
||||||
|
// include the title, section, date, source, and manual. We will use the
|
||||||
|
// current time if Date if unset and will use "Auto generated by spf13/cobra"
|
||||||
|
// if the Source is unset.
|
||||||
|
type GenManHeader struct {
|
||||||
|
Title string
|
||||||
|
Section string
|
||||||
|
Date *time.Time
|
||||||
|
date string
|
||||||
|
Source string
|
||||||
|
Manual string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenMan will generate a man page for the given command and write it to
|
||||||
|
// w. The header argument may be nil, however obviously w may not.
|
||||||
|
func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error {
|
||||||
|
if header == nil {
|
||||||
|
header = &GenManHeader{}
|
||||||
|
}
|
||||||
|
fillHeader(header, cmd.CommandPath())
|
||||||
|
|
||||||
|
b := genMan(cmd, header)
|
||||||
|
_, err := w.Write(mangen.Render(b))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillHeader(header *GenManHeader, name string) {
|
||||||
|
if header.Title == "" {
|
||||||
|
header.Title = strings.ToUpper(strings.Replace(name, " ", "\\-", -1))
|
||||||
|
}
|
||||||
|
if header.Section == "" {
|
||||||
|
header.Section = "1"
|
||||||
|
}
|
||||||
|
if header.Date == nil {
|
||||||
|
now := time.Now()
|
||||||
|
header.Date = &now
|
||||||
|
}
|
||||||
|
header.date = (*header.Date).Format("Jan 2006")
|
||||||
|
if header.Source == "" {
|
||||||
|
header.Source = "Auto generated by spf13/cobra"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func manPreamble(out io.Writer, header *GenManHeader, cmd *cobra.Command, dashedName string) {
|
||||||
|
description := cmd.Long
|
||||||
|
if len(description) == 0 {
|
||||||
|
description = cmd.Short
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(out, `%% %s(%s)%s
|
||||||
|
%% %s
|
||||||
|
%% %s
|
||||||
|
# NAME
|
||||||
|
`, header.Title, header.Section, header.date, header.Source, header.Manual)
|
||||||
|
fmt.Fprintf(out, "%s \\- %s\n\n", dashedName, cmd.Short)
|
||||||
|
fmt.Fprintf(out, "# SYNOPSIS\n")
|
||||||
|
fmt.Fprintf(out, "**%s**\n\n", cmd.UseLine())
|
||||||
|
fmt.Fprintf(out, "# DESCRIPTION\n")
|
||||||
|
fmt.Fprintf(out, "%s\n\n", description)
|
||||||
|
}
|
||||||
|
|
||||||
|
func manPrintFlags(out io.Writer, flags *pflag.FlagSet) {
|
||||||
|
flags.VisitAll(func(flag *pflag.Flag) {
|
||||||
|
if len(flag.Deprecated) > 0 || flag.Hidden {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
format := ""
|
||||||
|
if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 {
|
||||||
|
format = fmt.Sprintf("**-%s**, **--%s**", flag.Shorthand, flag.Name)
|
||||||
|
} else {
|
||||||
|
format = fmt.Sprintf("**--%s**", flag.Name)
|
||||||
|
}
|
||||||
|
if len(flag.NoOptDefVal) > 0 {
|
||||||
|
format = format + "["
|
||||||
|
}
|
||||||
|
if flag.Value.Type() == "string" {
|
||||||
|
// put quotes on the value
|
||||||
|
format = format + "=%q"
|
||||||
|
} else {
|
||||||
|
format = format + "=%s"
|
||||||
|
}
|
||||||
|
if len(flag.NoOptDefVal) > 0 {
|
||||||
|
format = format + "]"
|
||||||
|
}
|
||||||
|
format = format + "\n\t%s\n\n"
|
||||||
|
fmt.Fprintf(out, format, flag.DefValue, flag.Usage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func manPrintOptions(out io.Writer, command *cobra.Command) {
|
||||||
|
flags := command.NonInheritedFlags()
|
||||||
|
if flags.HasFlags() {
|
||||||
|
fmt.Fprintf(out, "# OPTIONS\n")
|
||||||
|
manPrintFlags(out, flags)
|
||||||
|
fmt.Fprintf(out, "\n")
|
||||||
|
}
|
||||||
|
flags = command.InheritedFlags()
|
||||||
|
if flags.HasFlags() {
|
||||||
|
fmt.Fprintf(out, "# OPTIONS INHERITED FROM PARENT COMMANDS\n")
|
||||||
|
manPrintFlags(out, flags)
|
||||||
|
fmt.Fprintf(out, "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
|
||||||
|
// something like `rootcmd-subcmd1-subcmd2`
|
||||||
|
dashCommandName := strings.Replace(cmd.CommandPath(), " ", "-", -1)
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
manPreamble(buf, header, cmd, dashCommandName)
|
||||||
|
manPrintOptions(buf, cmd)
|
||||||
|
if len(cmd.Example) > 0 {
|
||||||
|
fmt.Fprintf(buf, "# EXAMPLE\n")
|
||||||
|
fmt.Fprintf(buf, "\n%s\n\n", cmd.Example)
|
||||||
|
}
|
||||||
|
if hasSeeAlso(cmd) {
|
||||||
|
fmt.Fprintf(buf, "# SEE ALSO\n")
|
||||||
|
seealsos := make([]string, 0)
|
||||||
|
if cmd.HasParent() {
|
||||||
|
parentPath := cmd.Parent().CommandPath()
|
||||||
|
dashParentPath := strings.Replace(parentPath, " ", "-", -1)
|
||||||
|
seealso := fmt.Sprintf("**%s(%s)**", dashParentPath, header.Section)
|
||||||
|
seealsos = append(seealsos, seealso)
|
||||||
|
cmd.VisitParents(func(c *cobra.Command) {
|
||||||
|
if c.DisableAutoGenTag {
|
||||||
|
cmd.DisableAutoGenTag = c.DisableAutoGenTag
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
children := cmd.Commands()
|
||||||
|
sort.Sort(byName(children))
|
||||||
|
for _, c := range children {
|
||||||
|
if !c.IsAvailableCommand() || c.IsHelpCommand() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seealso := fmt.Sprintf("**%s-%s(%s)**", dashCommandName, c.Name(), header.Section)
|
||||||
|
seealsos = append(seealsos, seealso)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(buf, "%s\n", strings.Join(seealsos, ", "))
|
||||||
|
}
|
||||||
|
if !cmd.DisableAutoGenTag {
|
||||||
|
fmt.Fprintf(buf, "# HISTORY\n%s Auto generated by spf13/cobra\n", header.Date.Format("2-Jan-2006"))
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
//Copyright 2015 Red Hat Inc. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package doc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func printOptions(w io.Writer, cmd *cobra.Command, name string) error {
|
||||||
|
flags := cmd.NonInheritedFlags()
|
||||||
|
flags.SetOutput(w)
|
||||||
|
if flags.HasFlags() {
|
||||||
|
if _, err := fmt.Fprintf(w, "### Options\n\n```\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flags.PrintDefaults()
|
||||||
|
if _, err := fmt.Fprintf(w, "```\n\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parentFlags := cmd.InheritedFlags()
|
||||||
|
parentFlags.SetOutput(w)
|
||||||
|
if parentFlags.HasFlags() {
|
||||||
|
if _, err := fmt.Fprintf(w, "### Options inherited from parent commands\n\n```\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parentFlags.PrintDefaults()
|
||||||
|
if _, err := fmt.Fprintf(w, "```\n\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenMarkdown(cmd *cobra.Command, w io.Writer) error {
|
||||||
|
return GenMarkdownCustom(cmd, w, func(s string) string { return s })
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error {
|
||||||
|
name := cmd.CommandPath()
|
||||||
|
|
||||||
|
short := cmd.Short
|
||||||
|
long := cmd.Long
|
||||||
|
if len(long) == 0 {
|
||||||
|
long = short
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := fmt.Fprintf(w, "## %s\n\n", name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(w, "%s\n\n", short); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(w, "### Synopsis\n\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(w, "\n%s\n\n", long); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Runnable() {
|
||||||
|
if _, err := fmt.Fprintf(w, "```\n%s\n```\n\n", cmd.UseLine()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cmd.Example) > 0 {
|
||||||
|
if _, err := fmt.Fprintf(w, "### Examples\n\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(w, "```\n%s\n```\n\n", cmd.Example); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := printOptions(w, cmd, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if hasSeeAlso(cmd) {
|
||||||
|
if _, err := fmt.Fprintf(w, "### SEE ALSO\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cmd.HasParent() {
|
||||||
|
parent := cmd.Parent()
|
||||||
|
pname := parent.CommandPath()
|
||||||
|
link := pname + ".md"
|
||||||
|
link = strings.Replace(link, " ", "_", -1)
|
||||||
|
if _, err := fmt.Fprintf(w, "* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.VisitParents(func(c *cobra.Command) {
|
||||||
|
if c.DisableAutoGenTag {
|
||||||
|
cmd.DisableAutoGenTag = c.DisableAutoGenTag
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
children := cmd.Commands()
|
||||||
|
sort.Sort(byName(children))
|
||||||
|
|
||||||
|
for _, child := range children {
|
||||||
|
if !child.IsAvailableCommand() || child.IsHelpCommand() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cname := name + " " + child.Name()
|
||||||
|
link := cname + ".md"
|
||||||
|
link = strings.Replace(link, " ", "_", -1)
|
||||||
|
if _, err := fmt.Fprintf(w, "* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(w, "\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !cmd.DisableAutoGenTag {
|
||||||
|
if _, err := fmt.Fprintf(w, "###### Auto generated by spf13/cobra on %s\n", time.Now().Format("2-Jan-2006")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenMarkdownTree(cmd *cobra.Command, dir string) error {
|
||||||
|
identity := func(s string) string { return s }
|
||||||
|
emptyStr := func(s string) string { return "" }
|
||||||
|
return GenMarkdownTreeCustom(cmd, dir, emptyStr, identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error {
|
||||||
|
for _, c := range cmd.Commands() {
|
||||||
|
if !c.IsAvailableCommand() || c.IsHelpCommand() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := GenMarkdownTreeCustom(c, dir, filePrepender, linkHandler); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".md"
|
||||||
|
filename := filepath.Join(dir, basename)
|
||||||
|
f, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err := io.WriteString(f, filePrepender(filename)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := GenMarkdownCustom(cmd, f, linkHandler); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright 2015 Red Hat Inc. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package doc
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
// Test to see if we have a reason to print See Also information in docs
|
||||||
|
// Basically this is a test for a parent commend or a subcommand which is
|
||||||
|
// both not deprecated and not the autogenerated help command.
|
||||||
|
func hasSeeAlso(cmd *cobra.Command) bool {
|
||||||
|
if cmd.HasParent() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, c := range cmd.Commands() {
|
||||||
|
if !c.IsAvailableCommand() || c.IsHelpCommand() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type byName []*cobra.Command
|
||||||
|
|
||||||
|
func (s byName) Len() int { return len(s) }
|
||||||
|
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
|
Loading…
Reference in New Issue