├── .gitignore ├── go.mod ├── go.sum ├── example_test.go ├── README.md ├── LICENSE ├── attr_test.go └── attr.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mdigger/goldmark-attributes 2 | 3 | go 1.22 4 | 5 | toolchain go1.24.5 6 | 7 | require github.com/yuin/goldmark v1.7.13 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= 2 | github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 3 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package attributes_test 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | attributes "github.com/mdigger/goldmark-attributes" 8 | "github.com/yuin/goldmark" 9 | ) 10 | 11 | func Example() { 12 | var source = []byte(` 13 | text 14 | {#id .class} 15 | `) 16 | var md = goldmark.New(attributes.Enable) 17 | err := md.Convert(source, os.Stdout) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | // Output:

text

22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goldmark-attributes 2 | [![GoDoc](https://godoc.org/github.com/mdigger/goldmark-attributes?status.svg)](https://godoc.org/github.com/mdigger/goldmark-attributes) 3 | 4 | [GoldMark](https://github.com/yuin/goldmark/) block attributes extension. 5 | 6 | ```markdown 7 | # Document {#main} 8 | 9 | > Why, you may take the most gallant sailor, the most intrepid airman or the 10 | > most audacious soldier, put them at a table together – what do you get? The 11 | > sum of their fears. 12 | {.epigraph} 13 | ``` 14 | 15 | ```html 16 |

Document

17 |

Why, you may take the most gallant sailor, the 18 | most intrepid airman or the most audacious soldier, put them at a table 19 | together – what do you get? The sum of their fears.

20 |
21 | ``` 22 | 23 | ```go 24 | var md = goldmark.New(attributes.Enable) 25 | var source = []byte("{#id .class1}\ntext") 26 | err := md.Convert(source, os.Stdout) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dmitry Sedykh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /attr_test.go: -------------------------------------------------------------------------------- 1 | package attributes 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "testing" 7 | 8 | "github.com/yuin/goldmark" 9 | ) 10 | 11 | func TestAttributes(t *testing.T) { 12 | source := []byte(` 13 | Paragraph with attributes. 14 | {.myPar1} 15 | 16 | > Paragraph with attributes inside a block quote. 17 | > {.myPar2} 18 | 19 | > Blockquote with attributes. 20 | {.myBlockquote1} 21 | 22 | - list with 23 | - attributes 24 | {.myList1} 25 | 26 | and now: 27 | 28 | - a loose list 29 | 30 | - with attributes 31 | 32 | {.myList2} 33 | 34 | another: 35 | 36 | - loose list 37 | 38 | - with attributes 39 | {.myList3} 40 | 41 | and now: 42 | 43 | - a loose list where 44 | 45 | - the last paragraph has attributes. 46 | Note that the indentation of the attribute block is significant. 47 | {.myPar3} 48 | 49 | and finally: 50 | 51 | - > a list where each 52 | > item is a blockquote 53 | > {.myPar4} 54 | 55 | - > to see that everything is possible 56 | > {.myPar5} 57 | {.myBlockquote2} 58 | {.myList4} 59 | `) 60 | 61 | var md = goldmark.New(Enable) 62 | err := md.Convert(source, os.Stdout) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /attr.go: -------------------------------------------------------------------------------- 1 | // Package attributes is a extension for the goldmark 2 | // (http://github.com/yuin/goldmark). 3 | // 4 | // This extension adds support for block attributes in markdowns. 5 | // paragraph text with attributes 6 | // {#id .class option="value"} 7 | package attributes 8 | 9 | import ( 10 | "github.com/yuin/goldmark" 11 | "github.com/yuin/goldmark/ast" 12 | "github.com/yuin/goldmark/parser" 13 | "github.com/yuin/goldmark/renderer" 14 | "github.com/yuin/goldmark/text" 15 | "github.com/yuin/goldmark/util" 16 | ) 17 | 18 | // block are parsed attributes block. 19 | type block struct { 20 | ast.BaseBlock 21 | } 22 | 23 | // Dump implements Node.Dump. 24 | func (a *block) Dump(source []byte, level int) { 25 | attrs := a.Attributes() 26 | list := make(map[string]string, len(attrs)) 27 | for _, attr := range attrs { 28 | name := util.BytesToReadOnlyString(attr.Name) 29 | value := util.BytesToReadOnlyString(util.EscapeHTML(attr.Value.([]byte))) 30 | list[name] = value 31 | } 32 | 33 | ast.DumpHelper(a, source, level, list, nil) 34 | } 35 | 36 | // KindAttributes is a NodeKind of the attributes block node. 37 | var KindAttributes = ast.NewNodeKind("BlockAttributes") 38 | 39 | // Kind implements Node.Kind. 40 | func (a *block) Kind() ast.NodeKind { 41 | return KindAttributes 42 | } 43 | 44 | type attrParser struct{} 45 | 46 | // Trigger implement parser.BlockParser interface. 47 | func (a *attrParser) Trigger() []byte { 48 | return []byte{'{'} 49 | } 50 | 51 | // Open implement parser.BlockParser interface. 52 | func (a *attrParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { 53 | // add attributes if defined 54 | if attrs, ok := parser.ParseAttributes(reader); ok { 55 | node := &block{BaseBlock: ast.BaseBlock{}} 56 | for _, attr := range attrs { 57 | node.SetAttribute(attr.Name, attr.Value) 58 | } 59 | 60 | return node, parser.NoChildren 61 | } 62 | 63 | return nil, parser.RequireParagraph 64 | } 65 | 66 | // Continue implement parser.BlockParser interface. 67 | func (a *attrParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { 68 | return parser.Close 69 | } 70 | 71 | // Close implement parser.BlockParser interface. 72 | func (a *attrParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { 73 | // nothing to do 74 | } 75 | 76 | // CanInterruptParagraph implement parser.BlockParser interface. 77 | func (a *attrParser) CanInterruptParagraph() bool { 78 | return true 79 | } 80 | 81 | // CanAcceptIndentedLine implement parser.BlockParser interface. 82 | func (a *attrParser) CanAcceptIndentedLine() bool { 83 | return false 84 | } 85 | 86 | type transformer struct{} 87 | 88 | // Transform implement parser.Transformer interface. 89 | func (a *transformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { 90 | // collect all attributes block 91 | var attributes = make([]ast.Node, 0, 1000) 92 | _ = ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) { 93 | if entering && node.Kind() == KindAttributes { 94 | attributes = append(attributes, node) 95 | return ast.WalkSkipChildren, nil 96 | } 97 | 98 | return ast.WalkContinue, nil 99 | }) 100 | 101 | // set attributes to next block sibling 102 | for _, attr := range attributes { 103 | prev := attr.PreviousSibling() 104 | if prev != nil && prev.Type() == ast.TypeBlock && 105 | !attr.HasBlankPreviousLines() { 106 | for _, attr := range attr.Attributes() { 107 | if _, exist := prev.Attribute(attr.Name); !exist { 108 | prev.SetAttribute(attr.Name, attr.Value) 109 | } 110 | } 111 | } 112 | 113 | // remove attributes node 114 | attr.Parent().RemoveChild(attr.Parent(), attr) 115 | } 116 | } 117 | 118 | type attrRender struct{} 119 | 120 | // RegisterFuncs implement renderer.NodeRenderer interface. 121 | func (a *attrRender) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 122 | // not render 123 | reg.Register(KindAttributes, 124 | func(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 125 | return ast.WalkSkipChildren, nil 126 | }) 127 | } 128 | 129 | // extension defines a goldmark.Extender for markdown block attributes. 130 | type extension struct{} 131 | 132 | var ( 133 | defaultParser = new(attrParser) 134 | defaultTransformer = new(transformer) 135 | defaultRenderer = new(attrRender) 136 | ) 137 | 138 | // Extend implement goldmark.Extender interface. 139 | func (a *extension) Extend(m goldmark.Markdown) { 140 | m.Parser().AddOptions( 141 | parser.WithBlockParsers( 142 | util.Prioritized(defaultParser, 100)), 143 | parser.WithASTTransformers( 144 | util.Prioritized(defaultTransformer, 100), 145 | ), 146 | ) 147 | m.Renderer().AddOptions( 148 | renderer.WithNodeRenderers( 149 | util.Prioritized(defaultRenderer, 100), 150 | ), 151 | ) 152 | } 153 | 154 | // Extension is a goldmark.Extender with markdown block attributes support. 155 | var Extension goldmark.Extender = new(extension) 156 | 157 | // Enable is a goldmark.Option with block attributes support. 158 | var Enable = goldmark.WithExtensions(Extension) 159 | --------------------------------------------------------------------------------