-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
bd54dbe
commit affa836
Showing
7 changed files
with
358 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package fences | ||
|
||
import ( | ||
"github.com/yuin/goldmark/ast" | ||
) | ||
|
||
// A FencedContainer struct represents a fenced code block of Markdown text. | ||
type FencedContainer struct { | ||
ast.BaseBlock | ||
element string | ||
} | ||
|
||
// Dump implements Node.Dump . | ||
func (n *FencedContainer) Dump(source []byte, level int) { | ||
ast.DumpHelper(n, source, level, nil, nil) | ||
} | ||
|
||
// KindFencedContainer is a NodeKind of the FencedContainer node. | ||
var KindFencedContainer = ast.NewNodeKind("FencedContainer") | ||
|
||
// Kind implements Node.Kind. | ||
func (n *FencedContainer) Kind() ast.NodeKind { | ||
return KindFencedContainer | ||
} | ||
|
||
// NewFencedContainer return a new FencedContainer node. | ||
func NewFencedContainer() *FencedContainer { | ||
return &FencedContainer{ | ||
BaseBlock: ast.BaseBlock{}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package fences_test | ||
|
||
import ( | ||
"os" | ||
|
||
fences "github.com/stefanfritsch/goldmark-fences" | ||
"github.com/yuin/goldmark" | ||
"github.com/yuin/goldmark/text" | ||
) | ||
|
||
func Example() { | ||
src := []byte(` | ||
## Hello | ||
The following contains an id and a class | ||
:::{#big-div .add-border} | ||
And the next fence contains two classes. | ||
:::{.background-green .font-big} | ||
## This is nested within nested fences | ||
here we close the inner fence: | ||
::: | ||
and finally the outer one: | ||
:::`) | ||
|
||
markdown := goldmark.New( | ||
goldmark.WithExtensions( | ||
&fences.Extender{}, | ||
), | ||
) | ||
|
||
doc := markdown.Parser().Parse(text.NewReader(src)) | ||
markdown.Renderer().Render(os.Stdout, src, doc) | ||
|
||
// Output: | ||
// <h2>Hello</h2> | ||
// <p>The following contains an id and a class</p> | ||
// <div id="big-div" class="add-border"> | ||
// <p>And the next fence contains two classes.</p> | ||
// <div class="background-green font-big"> | ||
// <h2>This is nested within nested fences</h2> | ||
// <p>here we close the inner fence:</p> | ||
// </div> | ||
// <p>and finally the outer one:</p> | ||
// </div> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package fences | ||
|
||
import ( | ||
"github.com/yuin/goldmark" | ||
"github.com/yuin/goldmark/parser" | ||
"github.com/yuin/goldmark/renderer" | ||
"github.com/yuin/goldmark/util" | ||
) | ||
|
||
// Extender allows you to use fenced divs / fenced containers / fences in markdown | ||
// | ||
// Fences are a way to wrap other elements in divs and giving those divs | ||
// attributes using the same syntax as with headings. | ||
// | ||
// :::{#big-div .add-border} | ||
// this is some text | ||
// | ||
// ## with a header | ||
// | ||
// :::{.background-green .font-big} | ||
// ```R | ||
// X <- as.data.table(iris) | ||
// X[Species != "virginica", mean(Sepal.Length), Species] | ||
// ``` | ||
// ::: | ||
// ::: | ||
type Extender struct { | ||
priority int // optional int != 0. the priority value for parser and renderer. Defaults to 100. | ||
} | ||
|
||
// This implements the Extend method for goldmark-fences.Extender | ||
func (e *Extender) Extend(md goldmark.Markdown) { | ||
priority := 100 | ||
|
||
if e.priority != 0 { | ||
priority = e.priority | ||
} | ||
md.Parser().AddOptions( | ||
parser.WithBlockParsers( | ||
util.Prioritized(&fencedContainerParser{}, priority), | ||
), | ||
) | ||
md.Renderer().AddOptions( | ||
renderer.WithNodeRenderers( | ||
util.Prioritized(&Renderer{}, priority), | ||
), | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module github.com/stefanfritsch/goldmark-fences | ||
|
||
go 1.19 | ||
|
||
require github.com/yuin/goldmark v1.5.2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= | ||
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package fences | ||
|
||
import ( | ||
"github.com/yuin/goldmark/ast" | ||
"github.com/yuin/goldmark/parser" | ||
"github.com/yuin/goldmark/text" | ||
"github.com/yuin/goldmark/util" | ||
) | ||
|
||
type fencedContainerParser struct { | ||
} | ||
|
||
var defaultFencedContainerParser = &fencedContainerParser{} | ||
|
||
// NewFencedContainerParser returns a new BlockParser that | ||
// parses fenced code blocks. | ||
func NewFencedContainerParser() parser.BlockParser { | ||
return defaultFencedContainerParser | ||
} | ||
|
||
type fenceData struct { | ||
char byte | ||
indent int | ||
length int | ||
node ast.Node | ||
} | ||
|
||
var fencedContainerInfoKey = parser.NewContextKey() | ||
|
||
func (b *fencedContainerParser) Trigger() []byte { | ||
return []byte{':'} | ||
} | ||
|
||
func (b *fencedContainerParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { | ||
line, segment := reader.PeekLine() | ||
pos := pc.BlockOffset() | ||
if pos < 0 || line[pos] != ':' { | ||
return nil, parser.NoChildren | ||
} | ||
findent := pos | ||
fenceChar := line[pos] | ||
i := pos | ||
for ; i < len(line) && line[i] == fenceChar; i++ { | ||
} | ||
oFenceLength := i - pos | ||
if oFenceLength < 3 { | ||
return nil, parser.NoChildren | ||
} | ||
|
||
node := NewFencedContainer() | ||
if i < len(line)-1 { | ||
rest := line[i:] | ||
left := util.TrimLeftSpaceLength(rest) | ||
right := util.TrimRightSpaceLength(rest) | ||
|
||
if left < len(rest)-right { | ||
reader.Advance(i + left) | ||
attrs, ok := parser.ParseAttributes(reader) | ||
|
||
if ok { | ||
for _, attr := range attrs { | ||
node.SetAttribute(attr.Name, attr.Value) | ||
} | ||
} | ||
} | ||
} | ||
|
||
fdata := &fenceData{fenceChar, findent, oFenceLength, node} | ||
var fdataMap []*fenceData | ||
|
||
if oldData := pc.Get(fencedContainerInfoKey); oldData != nil { | ||
fdataMap = oldData.([]*fenceData) | ||
fdataMap = append(fdataMap, fdata) | ||
} else { | ||
fdataMap = []*fenceData{fdata} | ||
} | ||
pc.Set(fencedContainerInfoKey, fdataMap) | ||
|
||
// check if it's an empty block | ||
line, _ = reader.PeekLine() | ||
w, pos := util.IndentWidth(line, reader.LineOffset()) | ||
|
||
if close, _ := b.closes(line, segment, w, pos, node, fdata); close { | ||
return node, parser.NoChildren | ||
} | ||
|
||
return node, parser.HasChildren | ||
} | ||
|
||
func (b *fencedContainerParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { | ||
rawdata := pc.Get(fencedContainerInfoKey) | ||
fdataMap := rawdata.([]*fenceData) | ||
fdata := fdataMap[len(fdataMap)-1] | ||
|
||
line, segment := reader.PeekLine() | ||
w, pos := util.IndentWidth(line, reader.LineOffset()) | ||
|
||
if close, newline := b.closes(line, segment, w, pos, node, fdata); close { | ||
reader.Advance(segment.Stop - segment.Start - newline + segment.Padding) | ||
fdataMap = fdataMap[:len(fdataMap)-1] | ||
|
||
if len(fdataMap) == 0 { | ||
return parser.Close | ||
} else { | ||
pc.Set(fencedContainerInfoKey, fdataMap) | ||
return parser.Close | ||
} | ||
} | ||
|
||
return parser.Continue | parser.HasChildren | ||
} | ||
|
||
func (b *fencedContainerParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { | ||
} | ||
|
||
func (b *fencedContainerParser) CanInterruptParagraph() bool { | ||
return true | ||
} | ||
|
||
func (b *fencedContainerParser) CanAcceptIndentedLine() bool { | ||
return false | ||
} | ||
|
||
func (b *fencedContainerParser) closes(line []byte, segment text.Segment, w int, pos int, node ast.Node, fdata *fenceData) (bool, int) { | ||
|
||
// don't close anything but the last node | ||
if node != fdata.node { | ||
return false, 1 | ||
} | ||
|
||
// If the indentation is lower, we assume the user forgot to close the block | ||
if w < fdata.indent { | ||
return true, 1 | ||
} | ||
|
||
// else, check for the correct number of closing chars and provide the info | ||
// necessary to advance the reader | ||
if w == fdata.indent { | ||
i := pos | ||
for ; i < len(line) && line[i] == fdata.char; i++ { | ||
} | ||
length := i - pos | ||
|
||
if length >= fdata.length && util.IsBlank(line[i:]) { | ||
newline := 1 | ||
if line[len(line)-1] != '\n' { | ||
newline = 0 | ||
} | ||
|
||
return true, newline | ||
} | ||
} | ||
|
||
return false, 0 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package fences | ||
|
||
import ( | ||
"regexp" | ||
|
||
"github.com/yuin/goldmark/ast" | ||
"github.com/yuin/goldmark/renderer" | ||
"github.com/yuin/goldmark/renderer/html" | ||
"github.com/yuin/goldmark/util" | ||
) | ||
|
||
// A Config struct has configurations for the HTML based renderers. | ||
type Config struct { | ||
Writer html.Writer | ||
HardWraps bool | ||
XHTML bool | ||
Unsafe bool | ||
} | ||
|
||
// HeadingAttributeFilter defines attribute names which heading elements can have | ||
var FencedContainerAttributeFilter = html.GlobalAttributeFilter | ||
|
||
// A Renderer struct is an implementation of renderer.NodeRenderer that renders | ||
// nodes as (X)HTML. | ||
type Renderer struct { | ||
Config | ||
} | ||
|
||
// RegisterFuncs implements NodeRenderer.RegisterFuncs . | ||
func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { | ||
reg.Register(KindFencedContainer, r.renderFencedContainer) | ||
} | ||
|
||
func (r *Renderer) renderFencedContainer(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { | ||
n := node.(*FencedContainer) | ||
if entering { | ||
if n.Attributes() != nil { | ||
n.element = "div" | ||
if isNav(node) { | ||
n.element = "nav" | ||
} | ||
_, _ = w.WriteString("<" + n.element) | ||
html.RenderAttributes(w, n, FencedContainerAttributeFilter) | ||
_, _ = w.WriteString(">\n") | ||
} else { | ||
_, _ = w.WriteString("<div>\n") | ||
} | ||
} else { | ||
_, _ = w.WriteString("</" + n.element + ">\n") | ||
} | ||
return ast.WalkContinue, nil | ||
} | ||
|
||
func isNav(node ast.Node) bool { | ||
class, ok := node.AttributeString("class") | ||
if !ok { | ||
return false | ||
} | ||
if navChk.Match(class.([]byte)) { | ||
return true | ||
} | ||
|
||
return false | ||
} | ||
|
||
// check for the .nav class | ||
var navChk = regexp.MustCompile(`(^| |\.)elem-nav($| )`) |