Use glamour and termev to render/colorize content (#181)

Merge branch 'master' into use-glamour

select Glamour Theme based on BackgroundColor

Merge branch 'master' into use-glamour

Merge branch 'master' into use-glamour

update termev

update go.mod

label color colorate

use glamour for issue content

Vendor: Add glamour

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/181
Reviewed-by: techknowlogick <techknowlogick@gitea.io>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
6543
2020-09-19 16:00:50 +00:00
committed by Lunny Xiao
parent f8d983b523
commit 89e93d90b3
434 changed files with 68002 additions and 3 deletions

View File

@ -0,0 +1,83 @@
package ast
import (
gast "github.com/yuin/goldmark/ast"
)
// A DefinitionList struct represents a definition list of Markdown
// (PHPMarkdownExtra) text.
type DefinitionList struct {
gast.BaseBlock
Offset int
TemporaryParagraph *gast.Paragraph
}
// Dump implements Node.Dump.
func (n *DefinitionList) Dump(source []byte, level int) {
gast.DumpHelper(n, source, level, nil, nil)
}
// KindDefinitionList is a NodeKind of the DefinitionList node.
var KindDefinitionList = gast.NewNodeKind("DefinitionList")
// Kind implements Node.Kind.
func (n *DefinitionList) Kind() gast.NodeKind {
return KindDefinitionList
}
// NewDefinitionList returns a new DefinitionList node.
func NewDefinitionList(offset int, para *gast.Paragraph) *DefinitionList {
return &DefinitionList{
Offset: offset,
TemporaryParagraph: para,
}
}
// A DefinitionTerm struct represents a definition list term of Markdown
// (PHPMarkdownExtra) text.
type DefinitionTerm struct {
gast.BaseBlock
}
// Dump implements Node.Dump.
func (n *DefinitionTerm) Dump(source []byte, level int) {
gast.DumpHelper(n, source, level, nil, nil)
}
// KindDefinitionTerm is a NodeKind of the DefinitionTerm node.
var KindDefinitionTerm = gast.NewNodeKind("DefinitionTerm")
// Kind implements Node.Kind.
func (n *DefinitionTerm) Kind() gast.NodeKind {
return KindDefinitionTerm
}
// NewDefinitionTerm returns a new DefinitionTerm node.
func NewDefinitionTerm() *DefinitionTerm {
return &DefinitionTerm{}
}
// A DefinitionDescription struct represents a definition list description of Markdown
// (PHPMarkdownExtra) text.
type DefinitionDescription struct {
gast.BaseBlock
IsTight bool
}
// Dump implements Node.Dump.
func (n *DefinitionDescription) Dump(source []byte, level int) {
gast.DumpHelper(n, source, level, nil, nil)
}
// KindDefinitionDescription is a NodeKind of the DefinitionDescription node.
var KindDefinitionDescription = gast.NewNodeKind("DefinitionDescription")
// Kind implements Node.Kind.
func (n *DefinitionDescription) Kind() gast.NodeKind {
return KindDefinitionDescription
}
// NewDefinitionDescription returns a new DefinitionDescription node.
func NewDefinitionDescription() *DefinitionDescription {
return &DefinitionDescription{}
}

View File

@ -0,0 +1,125 @@
package ast
import (
"fmt"
gast "github.com/yuin/goldmark/ast"
)
// A FootnoteLink struct represents a link to a footnote of Markdown
// (PHP Markdown Extra) text.
type FootnoteLink struct {
gast.BaseInline
Index int
}
// Dump implements Node.Dump.
func (n *FootnoteLink) Dump(source []byte, level int) {
m := map[string]string{}
m["Index"] = fmt.Sprintf("%v", n.Index)
gast.DumpHelper(n, source, level, m, nil)
}
// KindFootnoteLink is a NodeKind of the FootnoteLink node.
var KindFootnoteLink = gast.NewNodeKind("FootnoteLink")
// Kind implements Node.Kind.
func (n *FootnoteLink) Kind() gast.NodeKind {
return KindFootnoteLink
}
// NewFootnoteLink returns a new FootnoteLink node.
func NewFootnoteLink(index int) *FootnoteLink {
return &FootnoteLink{
Index: index,
}
}
// A FootnoteBackLink struct represents a link to a footnote of Markdown
// (PHP Markdown Extra) text.
type FootnoteBackLink struct {
gast.BaseInline
Index int
}
// Dump implements Node.Dump.
func (n *FootnoteBackLink) Dump(source []byte, level int) {
m := map[string]string{}
m["Index"] = fmt.Sprintf("%v", n.Index)
gast.DumpHelper(n, source, level, m, nil)
}
// KindFootnoteBackLink is a NodeKind of the FootnoteBackLink node.
var KindFootnoteBackLink = gast.NewNodeKind("FootnoteBackLink")
// Kind implements Node.Kind.
func (n *FootnoteBackLink) Kind() gast.NodeKind {
return KindFootnoteBackLink
}
// NewFootnoteBackLink returns a new FootnoteBackLink node.
func NewFootnoteBackLink(index int) *FootnoteBackLink {
return &FootnoteBackLink{
Index: index,
}
}
// A Footnote struct represents a footnote of Markdown
// (PHP Markdown Extra) text.
type Footnote struct {
gast.BaseBlock
Ref []byte
Index int
}
// Dump implements Node.Dump.
func (n *Footnote) Dump(source []byte, level int) {
m := map[string]string{}
m["Index"] = fmt.Sprintf("%v", n.Index)
m["Ref"] = fmt.Sprintf("%s", n.Ref)
gast.DumpHelper(n, source, level, m, nil)
}
// KindFootnote is a NodeKind of the Footnote node.
var KindFootnote = gast.NewNodeKind("Footnote")
// Kind implements Node.Kind.
func (n *Footnote) Kind() gast.NodeKind {
return KindFootnote
}
// NewFootnote returns a new Footnote node.
func NewFootnote(ref []byte) *Footnote {
return &Footnote{
Ref: ref,
Index: -1,
}
}
// A FootnoteList struct represents footnotes of Markdown
// (PHP Markdown Extra) text.
type FootnoteList struct {
gast.BaseBlock
Count int
}
// Dump implements Node.Dump.
func (n *FootnoteList) Dump(source []byte, level int) {
m := map[string]string{}
m["Count"] = fmt.Sprintf("%v", n.Count)
gast.DumpHelper(n, source, level, m, nil)
}
// KindFootnoteList is a NodeKind of the FootnoteList node.
var KindFootnoteList = gast.NewNodeKind("FootnoteList")
// Kind implements Node.Kind.
func (n *FootnoteList) Kind() gast.NodeKind {
return KindFootnoteList
}
// NewFootnoteList returns a new FootnoteList node.
func NewFootnoteList() *FootnoteList {
return &FootnoteList{
Count: 0,
}
}

View File

@ -0,0 +1,29 @@
// Package ast defines AST nodes that represents extension's elements
package ast
import (
gast "github.com/yuin/goldmark/ast"
)
// A Strikethrough struct represents a strikethrough of GFM text.
type Strikethrough struct {
gast.BaseInline
}
// Dump implements Node.Dump.
func (n *Strikethrough) Dump(source []byte, level int) {
gast.DumpHelper(n, source, level, nil, nil)
}
// KindStrikethrough is a NodeKind of the Strikethrough node.
var KindStrikethrough = gast.NewNodeKind("Strikethrough")
// Kind implements Node.Kind.
func (n *Strikethrough) Kind() gast.NodeKind {
return KindStrikethrough
}
// NewStrikethrough returns a new Strikethrough node.
func NewStrikethrough() *Strikethrough {
return &Strikethrough{}
}

157
vendor/github.com/yuin/goldmark/extension/ast/table.go generated vendored Normal file
View File

@ -0,0 +1,157 @@
package ast
import (
"fmt"
gast "github.com/yuin/goldmark/ast"
"strings"
)
// Alignment is a text alignment of table cells.
type Alignment int
const (
// AlignLeft indicates text should be left justified.
AlignLeft Alignment = iota + 1
// AlignRight indicates text should be right justified.
AlignRight
// AlignCenter indicates text should be centered.
AlignCenter
// AlignNone indicates text should be aligned by default manner.
AlignNone
)
func (a Alignment) String() string {
switch a {
case AlignLeft:
return "left"
case AlignRight:
return "right"
case AlignCenter:
return "center"
case AlignNone:
return "none"
}
return ""
}
// A Table struct represents a table of Markdown(GFM) text.
type Table struct {
gast.BaseBlock
// Alignments returns alignments of the columns.
Alignments []Alignment
}
// Dump implements Node.Dump
func (n *Table) Dump(source []byte, level int) {
gast.DumpHelper(n, source, level, nil, func(level int) {
indent := strings.Repeat(" ", level)
fmt.Printf("%sAlignments {\n", indent)
for i, alignment := range n.Alignments {
indent2 := strings.Repeat(" ", level+1)
fmt.Printf("%s%s", indent2, alignment.String())
if i != len(n.Alignments)-1 {
fmt.Println("")
}
}
fmt.Printf("\n%s}\n", indent)
})
}
// KindTable is a NodeKind of the Table node.
var KindTable = gast.NewNodeKind("Table")
// Kind implements Node.Kind.
func (n *Table) Kind() gast.NodeKind {
return KindTable
}
// NewTable returns a new Table node.
func NewTable() *Table {
return &Table{
Alignments: []Alignment{},
}
}
// A TableRow struct represents a table row of Markdown(GFM) text.
type TableRow struct {
gast.BaseBlock
Alignments []Alignment
}
// Dump implements Node.Dump.
func (n *TableRow) Dump(source []byte, level int) {
gast.DumpHelper(n, source, level, nil, nil)
}
// KindTableRow is a NodeKind of the TableRow node.
var KindTableRow = gast.NewNodeKind("TableRow")
// Kind implements Node.Kind.
func (n *TableRow) Kind() gast.NodeKind {
return KindTableRow
}
// NewTableRow returns a new TableRow node.
func NewTableRow(alignments []Alignment) *TableRow {
return &TableRow{}
}
// A TableHeader struct represents a table header of Markdown(GFM) text.
type TableHeader struct {
gast.BaseBlock
Alignments []Alignment
}
// KindTableHeader is a NodeKind of the TableHeader node.
var KindTableHeader = gast.NewNodeKind("TableHeader")
// Kind implements Node.Kind.
func (n *TableHeader) Kind() gast.NodeKind {
return KindTableHeader
}
// Dump implements Node.Dump.
func (n *TableHeader) Dump(source []byte, level int) {
gast.DumpHelper(n, source, level, nil, nil)
}
// NewTableHeader returns a new TableHeader node.
func NewTableHeader(row *TableRow) *TableHeader {
n := &TableHeader{}
for c := row.FirstChild(); c != nil; {
next := c.NextSibling()
n.AppendChild(n, c)
c = next
}
return n
}
// A TableCell struct represents a table cell of a Markdown(GFM) text.
type TableCell struct {
gast.BaseBlock
Alignment Alignment
}
// Dump implements Node.Dump.
func (n *TableCell) Dump(source []byte, level int) {
gast.DumpHelper(n, source, level, nil, nil)
}
// KindTableCell is a NodeKind of the TableCell node.
var KindTableCell = gast.NewNodeKind("TableCell")
// Kind implements Node.Kind.
func (n *TableCell) Kind() gast.NodeKind {
return KindTableCell
}
// NewTableCell returns a new TableCell node.
func NewTableCell() *TableCell {
return &TableCell{
Alignment: AlignNone,
}
}

View File

@ -0,0 +1,35 @@
package ast
import (
"fmt"
gast "github.com/yuin/goldmark/ast"
)
// A TaskCheckBox struct represents a checkbox of a task list.
type TaskCheckBox struct {
gast.BaseInline
IsChecked bool
}
// Dump implements Node.Dump.
func (n *TaskCheckBox) Dump(source []byte, level int) {
m := map[string]string{
"Checked": fmt.Sprintf("%v", n.IsChecked),
}
gast.DumpHelper(n, source, level, m, nil)
}
// KindTaskCheckBox is a NodeKind of the TaskCheckBox node.
var KindTaskCheckBox = gast.NewNodeKind("TaskCheckBox")
// Kind implements Node.Kind.
func (n *TaskCheckBox) Kind() gast.NodeKind {
return KindTaskCheckBox
}
// NewTaskCheckBox returns a new TaskCheckBox node.
func NewTaskCheckBox(checked bool) *TaskCheckBox {
return &TaskCheckBox{
IsChecked: checked,
}
}

View File

@ -0,0 +1,270 @@
package extension
import (
"github.com/yuin/goldmark"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
type definitionListParser struct {
}
var defaultDefinitionListParser = &definitionListParser{}
// NewDefinitionListParser return a new parser.BlockParser that
// can parse PHP Markdown Extra Definition lists.
func NewDefinitionListParser() parser.BlockParser {
return defaultDefinitionListParser
}
func (b *definitionListParser) Trigger() []byte {
return []byte{':'}
}
func (b *definitionListParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
if _, ok := parent.(*ast.DefinitionList); ok {
return nil, parser.NoChildren
}
line, _ := reader.PeekLine()
pos := pc.BlockOffset()
indent := pc.BlockIndent()
if pos < 0 || line[pos] != ':' || indent != 0 {
return nil, parser.NoChildren
}
last := parent.LastChild()
// need 1 or more spaces after ':'
w, _ := util.IndentWidth(line[pos+1:], pos+1)
if w < 1 {
return nil, parser.NoChildren
}
if w >= 8 { // starts with indented code
w = 5
}
w += pos + 1 /* 1 = ':' */
para, lastIsParagraph := last.(*gast.Paragraph)
var list *ast.DefinitionList
status := parser.HasChildren
var ok bool
if lastIsParagraph {
list, ok = last.PreviousSibling().(*ast.DefinitionList)
if ok { // is not first item
list.Offset = w
list.TemporaryParagraph = para
} else { // is first item
list = ast.NewDefinitionList(w, para)
status |= parser.RequireParagraph
}
} else if list, ok = last.(*ast.DefinitionList); ok { // multiple description
list.Offset = w
list.TemporaryParagraph = nil
} else {
return nil, parser.NoChildren
}
return list, status
}
func (b *definitionListParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
line, _ := reader.PeekLine()
if util.IsBlank(line) {
return parser.Continue | parser.HasChildren
}
list, _ := node.(*ast.DefinitionList)
w, _ := util.IndentWidth(line, reader.LineOffset())
if w < list.Offset {
return parser.Close
}
pos, padding := util.IndentPosition(line, reader.LineOffset(), list.Offset)
reader.AdvanceAndSetPadding(pos, padding)
return parser.Continue | parser.HasChildren
}
func (b *definitionListParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
// nothing to do
}
func (b *definitionListParser) CanInterruptParagraph() bool {
return true
}
func (b *definitionListParser) CanAcceptIndentedLine() bool {
return false
}
type definitionDescriptionParser struct {
}
var defaultDefinitionDescriptionParser = &definitionDescriptionParser{}
// NewDefinitionDescriptionParser return a new parser.BlockParser that
// can parse definition description starts with ':'.
func NewDefinitionDescriptionParser() parser.BlockParser {
return defaultDefinitionDescriptionParser
}
func (b *definitionDescriptionParser) Trigger() []byte {
return []byte{':'}
}
func (b *definitionDescriptionParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
line, _ := reader.PeekLine()
pos := pc.BlockOffset()
indent := pc.BlockIndent()
if pos < 0 || line[pos] != ':' || indent != 0 {
return nil, parser.NoChildren
}
list, _ := parent.(*ast.DefinitionList)
if list == nil {
return nil, parser.NoChildren
}
para := list.TemporaryParagraph
list.TemporaryParagraph = nil
if para != nil {
lines := para.Lines()
l := lines.Len()
for i := 0; i < l; i++ {
term := ast.NewDefinitionTerm()
segment := lines.At(i)
term.Lines().Append(segment.TrimRightSpace(reader.Source()))
list.AppendChild(list, term)
}
para.Parent().RemoveChild(para.Parent(), para)
}
cpos, padding := util.IndentPosition(line[pos+1:], pos+1, list.Offset-pos-1)
reader.AdvanceAndSetPadding(cpos, padding)
return ast.NewDefinitionDescription(), parser.HasChildren
}
func (b *definitionDescriptionParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
// definitionListParser detects end of the description.
// so this method will never be called.
return parser.Continue | parser.HasChildren
}
func (b *definitionDescriptionParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
desc := node.(*ast.DefinitionDescription)
desc.IsTight = !desc.HasBlankPreviousLines()
if desc.IsTight {
for gc := desc.FirstChild(); gc != nil; gc = gc.NextSibling() {
paragraph, ok := gc.(*gast.Paragraph)
if ok {
textBlock := gast.NewTextBlock()
textBlock.SetLines(paragraph.Lines())
desc.ReplaceChild(desc, paragraph, textBlock)
}
}
}
}
func (b *definitionDescriptionParser) CanInterruptParagraph() bool {
return true
}
func (b *definitionDescriptionParser) CanAcceptIndentedLine() bool {
return false
}
// DefinitionListHTMLRenderer is a renderer.NodeRenderer implementation that
// renders DefinitionList nodes.
type DefinitionListHTMLRenderer struct {
html.Config
}
// NewDefinitionListHTMLRenderer returns a new DefinitionListHTMLRenderer.
func NewDefinitionListHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
r := &DefinitionListHTMLRenderer{
Config: html.NewConfig(),
}
for _, opt := range opts {
opt.SetHTMLOption(&r.Config)
}
return r
}
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
func (r *DefinitionListHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindDefinitionList, r.renderDefinitionList)
reg.Register(ast.KindDefinitionTerm, r.renderDefinitionTerm)
reg.Register(ast.KindDefinitionDescription, r.renderDefinitionDescription)
}
// DefinitionListAttributeFilter defines attribute names which dl elements can have.
var DefinitionListAttributeFilter = html.GlobalAttributeFilter
func (r *DefinitionListHTMLRenderer) renderDefinitionList(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
if n.Attributes() != nil {
_, _ = w.WriteString("<dl")
html.RenderAttributes(w, n, DefinitionListAttributeFilter)
_, _ = w.WriteString(">\n")
} else {
_, _ = w.WriteString("<dl>\n")
}
} else {
_, _ = w.WriteString("</dl>\n")
}
return gast.WalkContinue, nil
}
// DefinitionTermAttributeFilter defines attribute names which dd elements can have.
var DefinitionTermAttributeFilter = html.GlobalAttributeFilter
func (r *DefinitionListHTMLRenderer) renderDefinitionTerm(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
if n.Attributes() != nil {
_, _ = w.WriteString("<dt")
html.RenderAttributes(w, n, DefinitionTermAttributeFilter)
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("<dt>")
}
} else {
_, _ = w.WriteString("</dt>\n")
}
return gast.WalkContinue, nil
}
// DefinitionDescriptionAttributeFilter defines attribute names which dd elements can have.
var DefinitionDescriptionAttributeFilter = html.GlobalAttributeFilter
func (r *DefinitionListHTMLRenderer) renderDefinitionDescription(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
n := node.(*ast.DefinitionDescription)
_, _ = w.WriteString("<dd")
if n.Attributes() != nil {
html.RenderAttributes(w, n, DefinitionDescriptionAttributeFilter)
}
if n.IsTight {
_, _ = w.WriteString(">")
} else {
_, _ = w.WriteString(">\n")
}
} else {
_, _ = w.WriteString("</dd>\n")
}
return gast.WalkContinue, nil
}
type definitionList struct {
}
// DefinitionList is an extension that allow you to use PHP Markdown Extra Definition lists.
var DefinitionList = &definitionList{}
func (e *definitionList) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithBlockParsers(
util.Prioritized(NewDefinitionListParser(), 101),
util.Prioritized(NewDefinitionDescriptionParser(), 102),
))
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(NewDefinitionListHTMLRenderer(), 500),
))
}

336
vendor/github.com/yuin/goldmark/extension/footnote.go generated vendored Normal file
View File

@ -0,0 +1,336 @@
package extension
import (
"bytes"
"github.com/yuin/goldmark"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"strconv"
)
var footnoteListKey = parser.NewContextKey()
type footnoteBlockParser struct {
}
var defaultFootnoteBlockParser = &footnoteBlockParser{}
// NewFootnoteBlockParser returns a new parser.BlockParser that can parse
// footnotes of the Markdown(PHP Markdown Extra) text.
func NewFootnoteBlockParser() parser.BlockParser {
return defaultFootnoteBlockParser
}
func (b *footnoteBlockParser) Trigger() []byte {
return []byte{'['}
}
func (b *footnoteBlockParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
line, segment := reader.PeekLine()
pos := pc.BlockOffset()
if pos < 0 || line[pos] != '[' {
return nil, parser.NoChildren
}
pos++
if pos > len(line)-1 || line[pos] != '^' {
return nil, parser.NoChildren
}
open := pos + 1
closes := 0
closure := util.FindClosure(line[pos+1:], '[', ']', false, false)
closes = pos + 1 + closure
next := closes + 1
if closure > -1 {
if next >= len(line) || line[next] != ':' {
return nil, parser.NoChildren
}
} else {
return nil, parser.NoChildren
}
padding := segment.Padding
label := reader.Value(text.NewSegment(segment.Start+open-padding, segment.Start+closes-padding))
if util.IsBlank(label) {
return nil, parser.NoChildren
}
item := ast.NewFootnote(label)
pos = next + 1 - padding
if pos >= len(line) {
reader.Advance(pos)
return item, parser.NoChildren
}
reader.AdvanceAndSetPadding(pos, padding)
return item, parser.HasChildren
}
func (b *footnoteBlockParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
line, _ := reader.PeekLine()
if util.IsBlank(line) {
return parser.Continue | parser.HasChildren
}
childpos, padding := util.IndentPosition(line, reader.LineOffset(), 4)
if childpos < 0 {
return parser.Close
}
reader.AdvanceAndSetPadding(childpos, padding)
return parser.Continue | parser.HasChildren
}
func (b *footnoteBlockParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
var list *ast.FootnoteList
if tlist := pc.Get(footnoteListKey); tlist != nil {
list = tlist.(*ast.FootnoteList)
} else {
list = ast.NewFootnoteList()
pc.Set(footnoteListKey, list)
node.Parent().InsertBefore(node.Parent(), node, list)
}
node.Parent().RemoveChild(node.Parent(), node)
list.AppendChild(list, node)
}
func (b *footnoteBlockParser) CanInterruptParagraph() bool {
return true
}
func (b *footnoteBlockParser) CanAcceptIndentedLine() bool {
return false
}
type footnoteParser struct {
}
var defaultFootnoteParser = &footnoteParser{}
// NewFootnoteParser returns a new parser.InlineParser that can parse
// footnote links of the Markdown(PHP Markdown Extra) text.
func NewFootnoteParser() parser.InlineParser {
return defaultFootnoteParser
}
func (s *footnoteParser) Trigger() []byte {
// footnote syntax probably conflict with the image syntax.
// So we need trigger this parser with '!'.
return []byte{'!', '['}
}
func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
line, segment := block.PeekLine()
pos := 1
if len(line) > 0 && line[0] == '!' {
pos++
}
if pos >= len(line) || line[pos] != '^' {
return nil
}
pos++
if pos >= len(line) {
return nil
}
open := pos
closure := util.FindClosure(line[pos:], '[', ']', false, false)
if closure < 0 {
return nil
}
closes := pos + closure
value := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes))
block.Advance(closes + 1)
var list *ast.FootnoteList
if tlist := pc.Get(footnoteListKey); tlist != nil {
list = tlist.(*ast.FootnoteList)
}
if list == nil {
return nil
}
index := 0
for def := list.FirstChild(); def != nil; def = def.NextSibling() {
d := def.(*ast.Footnote)
if bytes.Equal(d.Ref, value) {
if d.Index < 0 {
list.Count += 1
d.Index = list.Count
}
index = d.Index
break
}
}
if index == 0 {
return nil
}
return ast.NewFootnoteLink(index)
}
type footnoteASTTransformer struct {
}
var defaultFootnoteASTTransformer = &footnoteASTTransformer{}
// NewFootnoteASTTransformer returns a new parser.ASTTransformer that
// insert a footnote list to the last of the document.
func NewFootnoteASTTransformer() parser.ASTTransformer {
return defaultFootnoteASTTransformer
}
func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
var list *ast.FootnoteList
if tlist := pc.Get(footnoteListKey); tlist != nil {
list = tlist.(*ast.FootnoteList)
} else {
return
}
pc.Set(footnoteListKey, nil)
for footnote := list.FirstChild(); footnote != nil; {
var container gast.Node = footnote
next := footnote.NextSibling()
if fc := container.LastChild(); fc != nil && gast.IsParagraph(fc) {
container = fc
}
index := footnote.(*ast.Footnote).Index
if index < 0 {
list.RemoveChild(list, footnote)
} else {
container.AppendChild(container, ast.NewFootnoteBackLink(index))
}
footnote = next
}
list.SortChildren(func(n1, n2 gast.Node) int {
if n1.(*ast.Footnote).Index < n2.(*ast.Footnote).Index {
return -1
}
return 1
})
if list.Count <= 0 {
list.Parent().RemoveChild(list.Parent(), list)
return
}
node.AppendChild(node, list)
}
// FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that
// renders FootnoteLink nodes.
type FootnoteHTMLRenderer struct {
html.Config
}
// NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer.
func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
r := &FootnoteHTMLRenderer{
Config: html.NewConfig(),
}
for _, opt := range opts {
opt.SetHTMLOption(&r.Config)
}
return r
}
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindFootnoteLink, r.renderFootnoteLink)
reg.Register(ast.KindFootnoteBackLink, r.renderFootnoteBackLink)
reg.Register(ast.KindFootnote, r.renderFootnote)
reg.Register(ast.KindFootnoteList, r.renderFootnoteList)
}
func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
n := node.(*ast.FootnoteLink)
is := strconv.Itoa(n.Index)
_, _ = w.WriteString(`<sup id="fnref:`)
_, _ = w.WriteString(is)
_, _ = w.WriteString(`"><a href="#fn:`)
_, _ = w.WriteString(is)
_, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`)
_, _ = w.WriteString(is)
_, _ = w.WriteString(`</a></sup>`)
}
return gast.WalkContinue, nil
}
func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
n := node.(*ast.FootnoteBackLink)
is := strconv.Itoa(n.Index)
_, _ = w.WriteString(` <a href="#fnref:`)
_, _ = w.WriteString(is)
_, _ = w.WriteString(`" class="footnote-backref" role="doc-backlink">`)
_, _ = w.WriteString("&#x21a9;&#xfe0e;")
_, _ = w.WriteString(`</a>`)
}
return gast.WalkContinue, nil
}
func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
n := node.(*ast.Footnote)
is := strconv.Itoa(n.Index)
if entering {
_, _ = w.WriteString(`<li id="fn:`)
_, _ = w.WriteString(is)
_, _ = w.WriteString(`" role="doc-endnote"`)
if node.Attributes() != nil {
html.RenderAttributes(w, node, html.ListItemAttributeFilter)
}
_, _ = w.WriteString(">\n")
} else {
_, _ = w.WriteString("</li>\n")
}
return gast.WalkContinue, nil
}
func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
tag := "section"
if r.Config.XHTML {
tag = "div"
}
if entering {
_, _ = w.WriteString("<")
_, _ = w.WriteString(tag)
_, _ = w.WriteString(` class="footnotes" role="doc-endnotes"`)
if node.Attributes() != nil {
html.RenderAttributes(w, node, html.GlobalAttributeFilter)
}
_ = w.WriteByte('>')
if r.Config.XHTML {
_, _ = w.WriteString("\n<hr />\n")
} else {
_, _ = w.WriteString("\n<hr>\n")
}
_, _ = w.WriteString("<ol>\n")
} else {
_, _ = w.WriteString("</ol>\n")
_, _ = w.WriteString("</")
_, _ = w.WriteString(tag)
_, _ = w.WriteString(">\n")
}
return gast.WalkContinue, nil
}
type footnote struct {
}
// Footnote is an extension that allow you to use PHP Markdown Extra Footnotes.
var Footnote = &footnote{}
func (e *footnote) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithBlockParsers(
util.Prioritized(NewFootnoteBlockParser(), 999),
),
parser.WithInlineParsers(
util.Prioritized(NewFootnoteParser(), 101),
),
parser.WithASTTransformers(
util.Prioritized(NewFootnoteASTTransformer(), 999),
),
)
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(NewFootnoteHTMLRenderer(), 500),
))
}

18
vendor/github.com/yuin/goldmark/extension/gfm.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
package extension
import (
"github.com/yuin/goldmark"
)
type gfm struct {
}
// GFM is an extension that provides Github Flavored markdown functionalities.
var GFM = &gfm{}
func (e *gfm) Extend(m goldmark.Markdown) {
Linkify.Extend(m)
Table.Extend(m)
Strikethrough.Extend(m)
TaskList.Extend(m)
}

303
vendor/github.com/yuin/goldmark/extension/linkify.go generated vendored Normal file
View File

@ -0,0 +1,303 @@
package extension
import (
"bytes"
"regexp"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]+(?:(?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`)
var urlRegexp = regexp.MustCompile(`^(?:http|https|ftp):\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]+(?:(?:/|[#?])[-a-zA-Z0-9@:%_+.~#$!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`)
// An LinkifyConfig struct is a data structure that holds configuration of the
// Linkify extension.
type LinkifyConfig struct {
AllowedProtocols [][]byte
URLRegexp *regexp.Regexp
WWWRegexp *regexp.Regexp
EmailRegexp *regexp.Regexp
}
const optLinkifyAllowedProtocols parser.OptionName = "LinkifyAllowedProtocols"
const optLinkifyURLRegexp parser.OptionName = "LinkifyURLRegexp"
const optLinkifyWWWRegexp parser.OptionName = "LinkifyWWWRegexp"
const optLinkifyEmailRegexp parser.OptionName = "LinkifyEmailRegexp"
// SetOption implements SetOptioner.
func (c *LinkifyConfig) SetOption(name parser.OptionName, value interface{}) {
switch name {
case optLinkifyAllowedProtocols:
c.AllowedProtocols = value.([][]byte)
case optLinkifyURLRegexp:
c.URLRegexp = value.(*regexp.Regexp)
case optLinkifyWWWRegexp:
c.WWWRegexp = value.(*regexp.Regexp)
case optLinkifyEmailRegexp:
c.EmailRegexp = value.(*regexp.Regexp)
}
}
// A LinkifyOption interface sets options for the LinkifyOption.
type LinkifyOption interface {
parser.Option
SetLinkifyOption(*LinkifyConfig)
}
type withLinkifyAllowedProtocols struct {
value [][]byte
}
func (o *withLinkifyAllowedProtocols) SetParserOption(c *parser.Config) {
c.Options[optLinkifyAllowedProtocols] = o.value
}
func (o *withLinkifyAllowedProtocols) SetLinkifyOption(p *LinkifyConfig) {
p.AllowedProtocols = o.value
}
// WithLinkifyAllowedProtocols is a functional option that specify allowed
// protocols in autolinks. Each protocol must end with ':' like
// 'http:' .
func WithLinkifyAllowedProtocols(value [][]byte) LinkifyOption {
return &withLinkifyAllowedProtocols{
value: value,
}
}
type withLinkifyURLRegexp struct {
value *regexp.Regexp
}
func (o *withLinkifyURLRegexp) SetParserOption(c *parser.Config) {
c.Options[optLinkifyURLRegexp] = o.value
}
func (o *withLinkifyURLRegexp) SetLinkifyOption(p *LinkifyConfig) {
p.URLRegexp = o.value
}
// WithLinkifyURLRegexp is a functional option that specify
// a pattern of the URL including a protocol.
func WithLinkifyURLRegexp(value *regexp.Regexp) LinkifyOption {
return &withLinkifyURLRegexp{
value: value,
}
}
// WithLinkifyWWWRegexp is a functional option that specify
// a pattern of the URL without a protocol.
// This pattern must start with 'www.' .
type withLinkifyWWWRegexp struct {
value *regexp.Regexp
}
func (o *withLinkifyWWWRegexp) SetParserOption(c *parser.Config) {
c.Options[optLinkifyWWWRegexp] = o.value
}
func (o *withLinkifyWWWRegexp) SetLinkifyOption(p *LinkifyConfig) {
p.WWWRegexp = o.value
}
func WithLinkifyWWWRegexp(value *regexp.Regexp) LinkifyOption {
return &withLinkifyWWWRegexp{
value: value,
}
}
// WithLinkifyWWWRegexp is a functional otpion that specify
// a pattern of the email address.
type withLinkifyEmailRegexp struct {
value *regexp.Regexp
}
func (o *withLinkifyEmailRegexp) SetParserOption(c *parser.Config) {
c.Options[optLinkifyEmailRegexp] = o.value
}
func (o *withLinkifyEmailRegexp) SetLinkifyOption(p *LinkifyConfig) {
p.EmailRegexp = o.value
}
func WithLinkifyEmailRegexp(value *regexp.Regexp) LinkifyOption {
return &withLinkifyEmailRegexp{
value: value,
}
}
type linkifyParser struct {
LinkifyConfig
}
// NewLinkifyParser return a new InlineParser can parse
// text that seems like a URL.
func NewLinkifyParser(opts ...LinkifyOption) parser.InlineParser {
p := &linkifyParser{
LinkifyConfig: LinkifyConfig{
AllowedProtocols: nil,
URLRegexp: urlRegexp,
WWWRegexp: wwwURLRegxp,
},
}
for _, o := range opts {
o.SetLinkifyOption(&p.LinkifyConfig)
}
return p
}
func (s *linkifyParser) Trigger() []byte {
// ' ' indicates any white spaces and a line head
return []byte{' ', '*', '_', '~', '('}
}
var protoHTTP = []byte("http:")
var protoHTTPS = []byte("https:")
var protoFTP = []byte("ftp:")
var domainWWW = []byte("www.")
func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
if pc.IsInLinkLabel() {
return nil
}
line, segment := block.PeekLine()
consumes := 0
start := segment.Start
c := line[0]
// advance if current position is not a line head.
if c == ' ' || c == '*' || c == '_' || c == '~' || c == '(' {
consumes++
start++
line = line[1:]
}
var m []int
var protocol []byte
var typ ast.AutoLinkType = ast.AutoLinkURL
if s.LinkifyConfig.AllowedProtocols == nil {
if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) {
m = s.LinkifyConfig.URLRegexp.FindSubmatchIndex(line)
}
} else {
for _, prefix := range s.LinkifyConfig.AllowedProtocols {
if bytes.HasPrefix(line, prefix) {
m = s.LinkifyConfig.URLRegexp.FindSubmatchIndex(line)
break
}
}
}
if m == nil && bytes.HasPrefix(line, domainWWW) {
m = s.LinkifyConfig.WWWRegexp.FindSubmatchIndex(line)
protocol = []byte("http")
}
if m != nil && m[0] != 0 {
m = nil
}
if m != nil && m[0] == 0 {
lastChar := line[m[1]-1]
if lastChar == '.' {
m[1]--
} else if lastChar == ')' {
closing := 0
for i := m[1] - 1; i >= m[0]; i-- {
if line[i] == ')' {
closing++
} else if line[i] == '(' {
closing--
}
}
if closing > 0 {
m[1] -= closing
}
} else if lastChar == ';' {
i := m[1] - 2
for ; i >= m[0]; i-- {
if util.IsAlphaNumeric(line[i]) {
continue
}
break
}
if i != m[1]-2 {
if line[i] == '&' {
m[1] -= m[1] - i
}
}
}
}
if m == nil {
if len(line) > 0 && util.IsPunct(line[0]) {
return nil
}
typ = ast.AutoLinkEmail
stop := -1
if s.LinkifyConfig.EmailRegexp == nil {
stop = util.FindEmailIndex(line)
} else {
m := s.LinkifyConfig.EmailRegexp.FindSubmatchIndex(line)
if m != nil && m[0] == 0 {
stop = m[1]
}
}
if stop < 0 {
return nil
}
at := bytes.IndexByte(line, '@')
m = []int{0, stop, at, stop - 1}
if m == nil || bytes.IndexByte(line[m[2]:m[3]], '.') < 0 {
return nil
}
lastChar := line[m[1]-1]
if lastChar == '.' {
m[1]--
}
if m[1] < len(line) {
nextChar := line[m[1]]
if nextChar == '-' || nextChar == '_' {
return nil
}
}
}
if m == nil {
return nil
}
if consumes != 0 {
s := segment.WithStop(segment.Start + 1)
ast.MergeOrAppendTextSegment(parent, s)
}
consumes += m[1]
block.Advance(consumes)
n := ast.NewTextSegment(text.NewSegment(start, start+m[1]))
link := ast.NewAutoLink(typ, n)
link.Protocol = protocol
return link
}
func (s *linkifyParser) CloseBlock(parent ast.Node, pc parser.Context) {
// nothing to do
}
type linkify struct {
options []LinkifyOption
}
// Linkify is an extension that allow you to parse text that seems like a URL.
var Linkify = &linkify{}
func NewLinkify(opts ...LinkifyOption) goldmark.Extender {
return &linkify{
options: opts,
}
}
func (e *linkify) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithInlineParsers(
util.Prioritized(NewLinkifyParser(e.options...), 999),
),
)
}

View File

@ -0,0 +1,116 @@
package extension
import (
"github.com/yuin/goldmark"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
type strikethroughDelimiterProcessor struct {
}
func (p *strikethroughDelimiterProcessor) IsDelimiter(b byte) bool {
return b == '~'
}
func (p *strikethroughDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool {
return opener.Char == closer.Char
}
func (p *strikethroughDelimiterProcessor) OnMatch(consumes int) gast.Node {
return ast.NewStrikethrough()
}
var defaultStrikethroughDelimiterProcessor = &strikethroughDelimiterProcessor{}
type strikethroughParser struct {
}
var defaultStrikethroughParser = &strikethroughParser{}
// NewStrikethroughParser return a new InlineParser that parses
// strikethrough expressions.
func NewStrikethroughParser() parser.InlineParser {
return defaultStrikethroughParser
}
func (s *strikethroughParser) Trigger() []byte {
return []byte{'~'}
}
func (s *strikethroughParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
before := block.PrecendingCharacter()
line, segment := block.PeekLine()
node := parser.ScanDelimiter(line, before, 2, defaultStrikethroughDelimiterProcessor)
if node == nil {
return nil
}
node.Segment = segment.WithStop(segment.Start + node.OriginalLength)
block.Advance(node.OriginalLength)
pc.PushDelimiter(node)
return node
}
func (s *strikethroughParser) CloseBlock(parent gast.Node, pc parser.Context) {
// nothing to do
}
// StrikethroughHTMLRenderer is a renderer.NodeRenderer implementation that
// renders Strikethrough nodes.
type StrikethroughHTMLRenderer struct {
html.Config
}
// NewStrikethroughHTMLRenderer returns a new StrikethroughHTMLRenderer.
func NewStrikethroughHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
r := &StrikethroughHTMLRenderer{
Config: html.NewConfig(),
}
for _, opt := range opts {
opt.SetHTMLOption(&r.Config)
}
return r
}
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
func (r *StrikethroughHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindStrikethrough, r.renderStrikethrough)
}
// StrikethroughAttributeFilter defines attribute names which dd elements can have.
var StrikethroughAttributeFilter = html.GlobalAttributeFilter
func (r *StrikethroughHTMLRenderer) renderStrikethrough(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
if n.Attributes() != nil {
_, _ = w.WriteString("<del")
html.RenderAttributes(w, n, StrikethroughAttributeFilter)
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("<del>")
}
} else {
_, _ = w.WriteString("</del>")
}
return gast.WalkContinue, nil
}
type strikethrough struct {
}
// Strikethrough is an extension that allow you to use strikethrough expression like '~~text~~' .
var Strikethrough = &strikethrough{}
func (e *strikethrough) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithInlineParsers(
util.Prioritized(NewStrikethroughParser(), 500),
))
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(NewStrikethroughHTMLRenderer(), 500),
))
}

446
vendor/github.com/yuin/goldmark/extension/table.go generated vendored Normal file
View File

@ -0,0 +1,446 @@
package extension
import (
"bytes"
"fmt"
"regexp"
"github.com/yuin/goldmark"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
// TableCellAlignMethod indicates how are table cells aligned in HTML format.indicates how are table cells aligned in HTML format.
type TableCellAlignMethod int
const (
// TableCellAlignDefault renders alignments by default method.
// With XHTML, alignments are rendered as an align attribute.
// With HTML5, alignments are rendered as a style attribute.
TableCellAlignDefault TableCellAlignMethod = iota
// TableCellAlignAttribute renders alignments as an align attribute.
TableCellAlignAttribute
// TableCellAlignStyle renders alignments as a style attribute.
TableCellAlignStyle
// TableCellAlignNone does not care about alignments.
// If you using classes or other styles, you can add these attributes
// in an ASTTransformer.
TableCellAlignNone
)
// TableConfig struct holds options for the extension.
type TableConfig struct {
html.Config
// TableCellAlignMethod indicates how are table celss aligned.
TableCellAlignMethod TableCellAlignMethod
}
// TableOption interface is a functional option interface for the extension.
type TableOption interface {
renderer.Option
// SetTableOption sets given option to the extension.
SetTableOption(*TableConfig)
}
// NewTableConfig returns a new Config with defaults.
func NewTableConfig() TableConfig {
return TableConfig{
Config: html.NewConfig(),
TableCellAlignMethod: TableCellAlignDefault,
}
}
// SetOption implements renderer.SetOptioner.
func (c *TableConfig) SetOption(name renderer.OptionName, value interface{}) {
switch name {
case optTableCellAlignMethod:
c.TableCellAlignMethod = value.(TableCellAlignMethod)
default:
c.Config.SetOption(name, value)
}
}
type withTableHTMLOptions struct {
value []html.Option
}
func (o *withTableHTMLOptions) SetConfig(c *renderer.Config) {
if o.value != nil {
for _, v := range o.value {
v.(renderer.Option).SetConfig(c)
}
}
}
func (o *withTableHTMLOptions) SetTableOption(c *TableConfig) {
if o.value != nil {
for _, v := range o.value {
v.SetHTMLOption(&c.Config)
}
}
}
// WithTableHTMLOptions is functional option that wraps goldmark HTMLRenderer options.
func WithTableHTMLOptions(opts ...html.Option) TableOption {
return &withTableHTMLOptions{opts}
}
const optTableCellAlignMethod renderer.OptionName = "TableTableCellAlignMethod"
type withTableCellAlignMethod struct {
value TableCellAlignMethod
}
func (o *withTableCellAlignMethod) SetConfig(c *renderer.Config) {
c.Options[optTableCellAlignMethod] = o.value
}
func (o *withTableCellAlignMethod) SetTableOption(c *TableConfig) {
c.TableCellAlignMethod = o.value
}
// WithTableCellAlignMethod is a functional option that indicates how are table cells aligned in HTML format.
func WithTableCellAlignMethod(a TableCellAlignMethod) TableOption {
return &withTableCellAlignMethod{a}
}
var tableDelimRegexp = regexp.MustCompile(`^[\s\-\|\:]+$`)
var tableDelimLeft = regexp.MustCompile(`^\s*\:\-+\s*$`)
var tableDelimRight = regexp.MustCompile(`^\s*\-+\:\s*$`)
var tableDelimCenter = regexp.MustCompile(`^\s*\:\-+\:\s*$`)
var tableDelimNone = regexp.MustCompile(`^\s*\-+\s*$`)
type tableParagraphTransformer struct {
}
var defaultTableParagraphTransformer = &tableParagraphTransformer{}
// NewTableParagraphTransformer returns a new ParagraphTransformer
// that can transform paragraphs into tables.
func NewTableParagraphTransformer() parser.ParagraphTransformer {
return defaultTableParagraphTransformer
}
func (b *tableParagraphTransformer) Transform(node *gast.Paragraph, reader text.Reader, pc parser.Context) {
lines := node.Lines()
if lines.Len() < 2 {
return
}
alignments := b.parseDelimiter(lines.At(1), reader)
if alignments == nil {
return
}
header := b.parseRow(lines.At(0), alignments, true, reader)
if header == nil || len(alignments) != header.ChildCount() {
return
}
table := ast.NewTable()
table.Alignments = alignments
table.AppendChild(table, ast.NewTableHeader(header))
for i := 2; i < lines.Len(); i++ {
table.AppendChild(table, b.parseRow(lines.At(i), alignments, false, reader))
}
node.Parent().InsertBefore(node.Parent(), node, table)
node.Parent().RemoveChild(node.Parent(), node)
}
func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments []ast.Alignment, isHeader bool, reader text.Reader) *ast.TableRow {
source := reader.Source()
line := segment.Value(source)
pos := 0
pos += util.TrimLeftSpaceLength(line)
limit := len(line)
limit -= util.TrimRightSpaceLength(line)
row := ast.NewTableRow(alignments)
if len(line) > 0 && line[pos] == '|' {
pos++
}
if len(line) > 0 && line[limit-1] == '|' {
limit--
}
i := 0
for ; pos < limit; i++ {
alignment := ast.AlignNone
if i >= len(alignments) {
if !isHeader {
return row
}
} else {
alignment = alignments[i]
}
closure := util.FindClosure(line[pos:], byte(0), '|', true, false)
if closure < 0 {
closure = len(line[pos:])
}
node := ast.NewTableCell()
seg := text.NewSegment(segment.Start+pos, segment.Start+pos+closure)
seg = seg.TrimLeftSpace(source)
seg = seg.TrimRightSpace(source)
node.Lines().Append(seg)
node.Alignment = alignment
row.AppendChild(row, node)
pos += closure + 1
}
for ; i < len(alignments); i++ {
row.AppendChild(row, ast.NewTableCell())
}
return row
}
func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader text.Reader) []ast.Alignment {
line := segment.Value(reader.Source())
if !tableDelimRegexp.Match(line) {
return nil
}
cols := bytes.Split(line, []byte{'|'})
if util.IsBlank(cols[0]) {
cols = cols[1:]
}
if len(cols) > 0 && util.IsBlank(cols[len(cols)-1]) {
cols = cols[:len(cols)-1]
}
var alignments []ast.Alignment
for _, col := range cols {
if tableDelimLeft.Match(col) {
alignments = append(alignments, ast.AlignLeft)
} else if tableDelimRight.Match(col) {
alignments = append(alignments, ast.AlignRight)
} else if tableDelimCenter.Match(col) {
alignments = append(alignments, ast.AlignCenter)
} else if tableDelimNone.Match(col) {
alignments = append(alignments, ast.AlignNone)
} else {
return nil
}
}
return alignments
}
// TableHTMLRenderer is a renderer.NodeRenderer implementation that
// renders Table nodes.
type TableHTMLRenderer struct {
TableConfig
}
// NewTableHTMLRenderer returns a new TableHTMLRenderer.
func NewTableHTMLRenderer(opts ...TableOption) renderer.NodeRenderer {
r := &TableHTMLRenderer{
TableConfig: NewTableConfig(),
}
for _, opt := range opts {
opt.SetTableOption(&r.TableConfig)
}
return r
}
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
func (r *TableHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindTable, r.renderTable)
reg.Register(ast.KindTableHeader, r.renderTableHeader)
reg.Register(ast.KindTableRow, r.renderTableRow)
reg.Register(ast.KindTableCell, r.renderTableCell)
}
// TableAttributeFilter defines attribute names which table elements can have.
var TableAttributeFilter = html.GlobalAttributeFilter.Extend(
[]byte("align"), // [Deprecated]
[]byte("bgcolor"), // [Deprecated]
[]byte("border"), // [Deprecated]
[]byte("cellpadding"), // [Deprecated]
[]byte("cellspacing"), // [Deprecated]
[]byte("frame"), // [Deprecated]
[]byte("rules"), // [Deprecated]
[]byte("summary"), // [Deprecated]
[]byte("width"), // [Deprecated]
)
func (r *TableHTMLRenderer) renderTable(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
_, _ = w.WriteString("<table")
if n.Attributes() != nil {
html.RenderAttributes(w, n, TableAttributeFilter)
}
_, _ = w.WriteString(">\n")
} else {
_, _ = w.WriteString("</table>\n")
}
return gast.WalkContinue, nil
}
// TableHeaderAttributeFilter defines attribute names which <thead> elements can have.
var TableHeaderAttributeFilter = html.GlobalAttributeFilter.Extend(
[]byte("align"), // [Deprecated since HTML4] [Obsolete since HTML5]
[]byte("bgcolor"), // [Not Standardized]
[]byte("char"), // [Deprecated since HTML4] [Obsolete since HTML5]
[]byte("charoff"), // [Deprecated since HTML4] [Obsolete since HTML5]
[]byte("valign"), // [Deprecated since HTML4] [Obsolete since HTML5]
)
func (r *TableHTMLRenderer) renderTableHeader(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
_, _ = w.WriteString("<thead")
if n.Attributes() != nil {
html.RenderAttributes(w, n, TableHeaderAttributeFilter)
}
_, _ = w.WriteString(">\n")
_, _ = w.WriteString("<tr>\n") // Header <tr> has no separate handle
} else {
_, _ = w.WriteString("</tr>\n")
_, _ = w.WriteString("</thead>\n")
if n.NextSibling() != nil {
_, _ = w.WriteString("<tbody>\n")
}
}
return gast.WalkContinue, nil
}
// TableRowAttributeFilter defines attribute names which <tr> elements can have.
var TableRowAttributeFilter = html.GlobalAttributeFilter.Extend(
[]byte("align"), // [Obsolete since HTML5]
[]byte("bgcolor"), // [Obsolete since HTML5]
[]byte("char"), // [Obsolete since HTML5]
[]byte("charoff"), // [Obsolete since HTML5]
[]byte("valign"), // [Obsolete since HTML5]
)
func (r *TableHTMLRenderer) renderTableRow(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
_, _ = w.WriteString("<tr")
if n.Attributes() != nil {
html.RenderAttributes(w, n, TableRowAttributeFilter)
}
_, _ = w.WriteString(">\n")
} else {
_, _ = w.WriteString("</tr>\n")
if n.Parent().LastChild() == n {
_, _ = w.WriteString("</tbody>\n")
}
}
return gast.WalkContinue, nil
}
// TableThCellAttributeFilter defines attribute names which table <th> cells can have.
var TableThCellAttributeFilter = html.GlobalAttributeFilter.Extend(
[]byte("abbr"), // [OK] Contains a short abbreviated description of the cell's content [NOT OK in <td>]
[]byte("align"), // [Obsolete since HTML5]
[]byte("axis"), // [Obsolete since HTML5]
[]byte("bgcolor"), // [Not Standardized]
[]byte("char"), // [Obsolete since HTML5]
[]byte("charoff"), // [Obsolete since HTML5]
[]byte("colspan"), // [OK] Number of columns that the cell is to span
[]byte("headers"), // [OK] This attribute contains a list of space-separated strings, each corresponding to the id attribute of the <th> elements that apply to this element
[]byte("height"), // [Deprecated since HTML4] [Obsolete since HTML5]
[]byte("rowspan"), // [OK] Number of rows that the cell is to span
[]byte("scope"), // [OK] This enumerated attribute defines the cells that the header (defined in the <th>) element relates to [NOT OK in <td>]
[]byte("valign"), // [Obsolete since HTML5]
[]byte("width"), // [Deprecated since HTML4] [Obsolete since HTML5]
)
// TableTdCellAttributeFilter defines attribute names which table <td> cells can have.
var TableTdCellAttributeFilter = html.GlobalAttributeFilter.Extend(
[]byte("abbr"), // [Obsolete since HTML5] [OK in <th>]
[]byte("align"), // [Obsolete since HTML5]
[]byte("axis"), // [Obsolete since HTML5]
[]byte("bgcolor"), // [Not Standardized]
[]byte("char"), // [Obsolete since HTML5]
[]byte("charoff"), // [Obsolete since HTML5]
[]byte("colspan"), // [OK] Number of columns that the cell is to span
[]byte("headers"), // [OK] This attribute contains a list of space-separated strings, each corresponding to the id attribute of the <th> elements that apply to this element
[]byte("height"), // [Deprecated since HTML4] [Obsolete since HTML5]
[]byte("rowspan"), // [OK] Number of rows that the cell is to span
[]byte("scope"), // [Obsolete since HTML5] [OK in <th>]
[]byte("valign"), // [Obsolete since HTML5]
[]byte("width"), // [Deprecated since HTML4] [Obsolete since HTML5]
)
func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
n := node.(*ast.TableCell)
tag := "td"
if n.Parent().Kind() == ast.KindTableHeader {
tag = "th"
}
if entering {
fmt.Fprintf(w, "<%s", tag)
if n.Alignment != ast.AlignNone {
amethod := r.TableConfig.TableCellAlignMethod
if amethod == TableCellAlignDefault {
if r.Config.XHTML {
amethod = TableCellAlignAttribute
} else {
amethod = TableCellAlignStyle
}
}
switch amethod {
case TableCellAlignAttribute:
if _, ok := n.AttributeString("align"); !ok { // Skip align render if overridden
fmt.Fprintf(w, ` align="%s"`, n.Alignment.String())
}
case TableCellAlignStyle:
v, ok := n.AttributeString("style")
var cob util.CopyOnWriteBuffer
if ok {
cob = util.NewCopyOnWriteBuffer(v.([]byte))
cob.AppendByte(';')
}
style := fmt.Sprintf("text-align:%s", n.Alignment.String())
cob.Append(util.StringToReadOnlyBytes(style))
n.SetAttributeString("style", cob.Bytes())
}
}
if n.Attributes() != nil {
if tag == "td" {
html.RenderAttributes(w, n, TableTdCellAttributeFilter) // <td>
} else {
html.RenderAttributes(w, n, TableThCellAttributeFilter) // <th>
}
}
_ = w.WriteByte('>')
} else {
fmt.Fprintf(w, "</%s>\n", tag)
}
return gast.WalkContinue, nil
}
type table struct {
options []TableOption
}
// Table is an extension that allow you to use GFM tables .
var Table = &table{
options: []TableOption{},
}
// NewTable returns a new extension with given options.
func NewTable(opts ...TableOption) goldmark.Extender {
return &table{
options: opts,
}
}
func (e *table) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithParagraphTransformers(
util.Prioritized(NewTableParagraphTransformer(), 200),
))
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(NewTableHTMLRenderer(e.options...), 500),
))
}

115
vendor/github.com/yuin/goldmark/extension/tasklist.go generated vendored Normal file
View File

@ -0,0 +1,115 @@
package extension
import (
"github.com/yuin/goldmark"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"regexp"
)
var taskListRegexp = regexp.MustCompile(`^\[([\sxX])\]\s*`)
type taskCheckBoxParser struct {
}
var defaultTaskCheckBoxParser = &taskCheckBoxParser{}
// NewTaskCheckBoxParser returns a new InlineParser that can parse
// checkboxes in list items.
// This parser must take precedence over the parser.LinkParser.
func NewTaskCheckBoxParser() parser.InlineParser {
return defaultTaskCheckBoxParser
}
func (s *taskCheckBoxParser) Trigger() []byte {
return []byte{'['}
}
func (s *taskCheckBoxParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
// Given AST structure must be like
// - List
// - ListItem : parent.Parent
// - TextBlock : parent
// (current line)
if parent.Parent() == nil || parent.Parent().FirstChild() != parent {
return nil
}
if _, ok := parent.Parent().(*gast.ListItem); !ok {
return nil
}
line, _ := block.PeekLine()
m := taskListRegexp.FindSubmatchIndex(line)
if m == nil {
return nil
}
value := line[m[2]:m[3]][0]
block.Advance(m[1])
checked := value == 'x' || value == 'X'
return ast.NewTaskCheckBox(checked)
}
func (s *taskCheckBoxParser) CloseBlock(parent gast.Node, pc parser.Context) {
// nothing to do
}
// TaskCheckBoxHTMLRenderer is a renderer.NodeRenderer implementation that
// renders checkboxes in list items.
type TaskCheckBoxHTMLRenderer struct {
html.Config
}
// NewTaskCheckBoxHTMLRenderer returns a new TaskCheckBoxHTMLRenderer.
func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
r := &TaskCheckBoxHTMLRenderer{
Config: html.NewConfig(),
}
for _, opt := range opts {
opt.SetHTMLOption(&r.Config)
}
return r
}
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
func (r *TaskCheckBoxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindTaskCheckBox, r.renderTaskCheckBox)
}
func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
if !entering {
return gast.WalkContinue, nil
}
n := node.(*ast.TaskCheckBox)
if n.IsChecked {
w.WriteString(`<input checked="" disabled="" type="checkbox"`)
} else {
w.WriteString(`<input disabled="" type="checkbox"`)
}
if r.XHTML {
w.WriteString(" /> ")
} else {
w.WriteString("> ")
}
return gast.WalkContinue, nil
}
type taskList struct {
}
// TaskList is an extension that allow you to use GFM task lists.
var TaskList = &taskList{}
func (e *taskList) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithInlineParsers(
util.Prioritized(NewTaskCheckBoxParser(), 0),
))
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(NewTaskCheckBoxHTMLRenderer(), 500),
))
}

View File

@ -0,0 +1,323 @@
package extension
import (
"unicode"
"github.com/yuin/goldmark"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
var uncloseCounterKey = parser.NewContextKey()
type unclosedCounter struct {
Single int
Double int
}
func (u *unclosedCounter) Reset() {
u.Single = 0
u.Double = 0
}
func getUnclosedCounter(pc parser.Context) *unclosedCounter {
v := pc.Get(uncloseCounterKey)
if v == nil {
v = &unclosedCounter{}
pc.Set(uncloseCounterKey, v)
}
return v.(*unclosedCounter)
}
// TypographicPunctuation is a key of the punctuations that can be replaced with
// typographic entities.
type TypographicPunctuation int
const (
// LeftSingleQuote is '
LeftSingleQuote TypographicPunctuation = iota + 1
// RightSingleQuote is '
RightSingleQuote
// LeftDoubleQuote is "
LeftDoubleQuote
// RightDoubleQuote is "
RightDoubleQuote
// EnDash is --
EnDash
// EmDash is ---
EmDash
// Ellipsis is ...
Ellipsis
// LeftAngleQuote is <<
LeftAngleQuote
// RightAngleQuote is >>
RightAngleQuote
// Apostrophe is '
Apostrophe
typographicPunctuationMax
)
// An TypographerConfig struct is a data structure that holds configuration of the
// Typographer extension.
type TypographerConfig struct {
Substitutions [][]byte
}
func newDefaultSubstitutions() [][]byte {
replacements := make([][]byte, typographicPunctuationMax)
replacements[LeftSingleQuote] = []byte("&lsquo;")
replacements[RightSingleQuote] = []byte("&rsquo;")
replacements[LeftDoubleQuote] = []byte("&ldquo;")
replacements[RightDoubleQuote] = []byte("&rdquo;")
replacements[EnDash] = []byte("&ndash;")
replacements[EmDash] = []byte("&mdash;")
replacements[Ellipsis] = []byte("&hellip;")
replacements[LeftAngleQuote] = []byte("&laquo;")
replacements[RightAngleQuote] = []byte("&raquo;")
replacements[Apostrophe] = []byte("&rsquo;")
return replacements
}
// SetOption implements SetOptioner.
func (b *TypographerConfig) SetOption(name parser.OptionName, value interface{}) {
switch name {
case optTypographicSubstitutions:
b.Substitutions = value.([][]byte)
}
}
// A TypographerOption interface sets options for the TypographerParser.
type TypographerOption interface {
parser.Option
SetTypographerOption(*TypographerConfig)
}
const optTypographicSubstitutions parser.OptionName = "TypographicSubstitutions"
// TypographicSubstitutions is a list of the substitutions for the Typographer extension.
type TypographicSubstitutions map[TypographicPunctuation][]byte
type withTypographicSubstitutions struct {
value [][]byte
}
func (o *withTypographicSubstitutions) SetParserOption(c *parser.Config) {
c.Options[optTypographicSubstitutions] = o.value
}
func (o *withTypographicSubstitutions) SetTypographerOption(p *TypographerConfig) {
p.Substitutions = o.value
}
// WithTypographicSubstitutions is a functional otpion that specify replacement text
// for punctuations.
func WithTypographicSubstitutions(values map[TypographicPunctuation][]byte) TypographerOption {
replacements := newDefaultSubstitutions()
for k, v := range values {
replacements[k] = v
}
return &withTypographicSubstitutions{replacements}
}
type typographerDelimiterProcessor struct {
}
func (p *typographerDelimiterProcessor) IsDelimiter(b byte) bool {
return b == '\'' || b == '"'
}
func (p *typographerDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool {
return opener.Char == closer.Char
}
func (p *typographerDelimiterProcessor) OnMatch(consumes int) gast.Node {
return nil
}
var defaultTypographerDelimiterProcessor = &typographerDelimiterProcessor{}
type typographerParser struct {
TypographerConfig
}
// NewTypographerParser return a new InlineParser that parses
// typographer expressions.
func NewTypographerParser(opts ...TypographerOption) parser.InlineParser {
p := &typographerParser{
TypographerConfig: TypographerConfig{
Substitutions: newDefaultSubstitutions(),
},
}
for _, o := range opts {
o.SetTypographerOption(&p.TypographerConfig)
}
return p
}
func (s *typographerParser) Trigger() []byte {
return []byte{'\'', '"', '-', '.', '<', '>'}
}
func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
line, _ := block.PeekLine()
c := line[0]
if len(line) > 2 {
if c == '-' {
if s.Substitutions[EmDash] != nil && line[1] == '-' && line[2] == '-' { // ---
node := gast.NewString(s.Substitutions[EmDash])
node.SetCode(true)
block.Advance(3)
return node
}
} else if c == '.' {
if s.Substitutions[Ellipsis] != nil && line[1] == '.' && line[2] == '.' { // ...
node := gast.NewString(s.Substitutions[Ellipsis])
node.SetCode(true)
block.Advance(3)
return node
}
return nil
}
}
if len(line) > 1 {
if c == '<' {
if s.Substitutions[LeftAngleQuote] != nil && line[1] == '<' { // <<
node := gast.NewString(s.Substitutions[LeftAngleQuote])
node.SetCode(true)
block.Advance(2)
return node
}
return nil
} else if c == '>' {
if s.Substitutions[RightAngleQuote] != nil && line[1] == '>' { // >>
node := gast.NewString(s.Substitutions[RightAngleQuote])
node.SetCode(true)
block.Advance(2)
return node
}
return nil
} else if s.Substitutions[EnDash] != nil && c == '-' && line[1] == '-' { // --
node := gast.NewString(s.Substitutions[EnDash])
node.SetCode(true)
block.Advance(2)
return node
}
}
if c == '\'' || c == '"' {
before := block.PrecendingCharacter()
d := parser.ScanDelimiter(line, before, 1, defaultTypographerDelimiterProcessor)
if d == nil {
return nil
}
counter := getUnclosedCounter(pc)
if c == '\'' {
if s.Substitutions[Apostrophe] != nil {
// Handle decade abbrevations such as '90s
if d.CanOpen && !d.CanClose && len(line) > 3 && util.IsNumeric(line[1]) && util.IsNumeric(line[2]) && line[3] == 's' {
after := rune(' ')
if len(line) > 4 {
after = util.ToRune(line, 4)
}
if len(line) == 3 || util.IsSpaceRune(after) || util.IsPunctRune(after) {
node := gast.NewString(s.Substitutions[Apostrophe])
node.SetCode(true)
block.Advance(1)
return node
}
}
// Convert normal apostrophes. This is probably more flexible than necessary but
// converts any apostrophe in between two alphanumerics.
if len(line) > 1 && (unicode.IsDigit(before) || unicode.IsLetter(before)) && (unicode.IsLetter(util.ToRune(line, 1))) {
node := gast.NewString(s.Substitutions[Apostrophe])
node.SetCode(true)
block.Advance(1)
return node
}
}
if s.Substitutions[LeftSingleQuote] != nil && d.CanOpen && !d.CanClose {
nt := LeftSingleQuote
// special cases: Alice's, I'm ,Don't, You'd
if len(line) > 1 && (line[1] == 's' || line[1] == 'm' || line[1] == 't' || line[1] == 'd') && (len(line) < 3 || util.IsPunct(line[2]) || util.IsSpace(line[2])) {
nt = RightSingleQuote
}
// special cases: I've, I'll, You're
if len(line) > 2 && ((line[1] == 'v' && line[2] == 'e') || (line[1] == 'l' && line[2] == 'l') || (line[1] == 'r' && line[2] == 'e')) && (len(line) < 4 || util.IsPunct(line[3]) || util.IsSpace(line[3])) {
nt = RightSingleQuote
}
if nt == LeftSingleQuote {
counter.Single++
}
node := gast.NewString(s.Substitutions[nt])
node.SetCode(true)
block.Advance(1)
return node
}
if s.Substitutions[RightSingleQuote] != nil && counter.Single > 0 {
isClose := d.CanClose && !d.CanOpen
maybeClose := d.CanClose && d.CanOpen && len(line) > 1 && (line[1] == ',' || line[1] == '.' || line[1] == '!' || line[1] == '?') && (len(line) == 2 || (len(line) > 2 && util.IsPunct(line[2]) || util.IsSpace(line[2])))
if isClose || maybeClose {
node := gast.NewString(s.Substitutions[RightSingleQuote])
node.SetCode(true)
block.Advance(1)
counter.Single--
return node
}
}
}
if c == '"' {
if s.Substitutions[LeftDoubleQuote] != nil && d.CanOpen && !d.CanClose {
node := gast.NewString(s.Substitutions[LeftDoubleQuote])
node.SetCode(true)
block.Advance(1)
counter.Double++
return node
}
if s.Substitutions[RightDoubleQuote] != nil && counter.Double > 0 {
isClose := d.CanClose && !d.CanOpen
maybeClose := d.CanClose && d.CanOpen && len(line) > 1 && (line[1] == ',' || line[1] == '.' || line[1] == '!' || line[1] == '?') && (len(line) == 2 || (len(line) > 2 && util.IsPunct(line[2]) || util.IsSpace(line[2])))
if isClose || maybeClose {
// special case: "Monitor 21""
if len(line) > 1 && line[1] == '"' && unicode.IsDigit(before) {
return nil
}
node := gast.NewString(s.Substitutions[RightDoubleQuote])
node.SetCode(true)
block.Advance(1)
counter.Double--
return node
}
}
}
}
return nil
}
func (s *typographerParser) CloseBlock(parent gast.Node, pc parser.Context) {
getUnclosedCounter(pc).Reset()
}
type typographer struct {
options []TypographerOption
}
// Typographer is an extension that replaces punctuations with typographic entities.
var Typographer = &typographer{}
// NewTypographer returns a new Extender that replaces punctuations with typographic entities.
func NewTypographer(opts ...TypographerOption) goldmark.Extender {
return &typographer{
options: opts,
}
}
func (e *typographer) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithInlineParsers(
util.Prioritized(NewTypographerParser(e.options...), 9999),
))
}