Skip to content

Commit

Permalink
basic working state
Browse files Browse the repository at this point in the history
  • Loading branch information
statup-github committed Oct 12, 2022
1 parent bd54dbe commit affa836
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 0 deletions.
31 changes: 31 additions & 0 deletions ast.go
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{},
}
}
50 changes: 50 additions & 0 deletions example_test.go
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>
}
48 changes: 48 additions & 0 deletions extend.go
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),
),
)
}
5 changes: 5 additions & 0 deletions go.mod
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
2 changes: 2 additions & 0 deletions go.sum
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=
155 changes: 155 additions & 0 deletions parser.go
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
}
67 changes: 67 additions & 0 deletions renderer.go
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($| )`)

0 comments on commit affa836

Please sign in to comment.