Skip to content

Commit

Permalink
Add a goldmark parser extension for first class sections
Browse files Browse the repository at this point in the history
  • Loading branch information
iwahbe committed Aug 12, 2024
1 parent c33c861 commit 32e8e2f
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 9 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ require (
github.com/spf13/afero v1.9.5
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0
github.com/teekennedy/goldmark-markdown v0.3.0
github.com/yuin/goldmark v1.7.4
github.com/zclconf/go-cty v1.14.2
golang.org/x/crypto v0.24.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1920,6 +1920,8 @@ github.com/pulumi/terraform-plugin-sdk/v2 v2.0.0-20240520223432-0c0bf0d65f10 h1:
github.com/pulumi/terraform-plugin-sdk/v2 v2.0.0-20240520223432-0c0bf0d65f10/go.mod h1:H+8tjs9TjV2w57QFVSMBQacf8k/E1XwLXGCARgViC6A=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rhysd/go-fakeio v1.0.0 h1:+TjiKCOs32dONY7DaoVz/VPOdvRkPfBkEyUDIpM8FQY=
github.com/rhysd/go-fakeio v1.0.0/go.mod h1:joYxF906trVwp2JLrE4jlN7A0z6wrz8O6o1UjarbFzE=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
Expand Down Expand Up @@ -1997,6 +1999,8 @@ github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/teekennedy/goldmark-markdown v0.3.0 h1:ik9/biVGCwGWFg8dQ3KVm2pQ/wiiG0whYiUcz9xH0W8=
github.com/teekennedy/goldmark-markdown v0.3.0/go.mod h1:kMhDz8La77A9UHvJGsxejd0QUflN9sS+QXCqnhmxmNo=
github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U=
github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=
github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 h1:X9dsIWPuuEJlPX//UmRKophhOKCGXc46RVIGuttks68=
Expand Down
12 changes: 3 additions & 9 deletions pkg/tfgen/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,19 +388,13 @@ func trimFrontMatter(text []byte) []byte {
return body[idx+3:]
}

func gmWalkNodes(node gmast.Node, f func(gmast.Node)) {
f(node)
for child := node.FirstChild(); child != nil; child = child.NextSibling() {
gmWalkNodes(child, f)
}
}

func gmWalkNode[T gmast.Node](node gmast.Node, f func(T)) {
gmWalkNodes(node, func(node gmast.Node) {
gmast.Walk(node, func(node gmast.Node, entering bool) (gmast.WalkStatus, error) {

Check failure on line 392 in pkg/tfgen/docs.go

View workflow job for this annotation

GitHub Actions / Test and Lint / lint

Error return value of `gmast.Walk` is not checked (errcheck)
n, ok := node.(T)
if ok {
if ok && entering {
f(n)
}
return gmast.WalkContinue, nil
})
}

Expand Down
82 changes: 82 additions & 0 deletions pkg/tfgen/parse/section/section.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2016-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package section

import (
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"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 _ goldmark.Extender = section{}

func Extension(level, priority int) goldmark.Extender {
return section{level, priority}
}

var Kind = ast.NewNodeKind("Section")

type section struct{ level, priority int }

func (s section) Extend(md goldmark.Markdown) {
md.Parser().AddOptions(parser.WithASTTransformers(
util.Prioritized(sectionParser{s.level}, s.priority),
))
}

type Section struct{ ast.BaseBlock }

func (s *Section) Heading() *ast.Heading {
return s.FirstChild().(*ast.Heading)
}

func (s *Section) Dump(source []byte, level int) {
ast.DumpHelper(s, source, level, nil, nil)
}

func (s *Section) Kind() ast.NodeKind { return Kind }

func (s sectionParser) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
err := ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
heading, ok := node.(*ast.Heading)
if !ok || heading.Level != s.level {
return ast.WalkContinue, nil
}

parent := heading.Parent()
section := &Section{}
c := heading.NextSibling()
section.AppendChild(section, heading)
parent.ReplaceChild(parent, heading, section)
for c != nil {
if child, ok := c.(*ast.Heading); ok && child.Level >= heading.Level {
break
}
child := c
// We are going to add c to section
c = c.NextSibling()
section.AppendChild(section, child)
}

return ast.WalkContinue, nil
})

contract.AssertNoErrorf(err, "The walker cannot error, so the walk cannot error")
}

type sectionParser struct{ level int }
104 changes: 104 additions & 0 deletions pkg/tfgen/parse/section/section_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2016-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package section_test

import (
"bytes"
"testing"

"github.com/hexops/autogold/v2"
"github.com/stretchr/testify/require"
markdown "github.com/teekennedy/goldmark-markdown"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"

"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfgen/parse/section"
)

type walkTransformer func(node ast.Node, entering bool) (ast.WalkStatus, error)

func (w walkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
ast.Walk(node, (func(node ast.Node, entering bool) (ast.WalkStatus, error))(w))

Check failure on line 36 in pkg/tfgen/parse/section/section_test.go

View workflow job for this annotation

GitHub Actions / Test and Lint / lint

Error return value of `ast.Walk` is not checked (errcheck)
}

func TestSection(t *testing.T) {
t.Parallel()

tests := []struct {
input string
walk func(src []byte, node ast.Node, entering bool) (ast.WalkStatus, error)
expected autogold.Value
}{
{
input: `
Hi
## 1
content *foo*
content
## 2
content (again)
`,
walk: func(src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
s, ok := node.(*section.Section)
if !ok || !entering {
return ast.WalkContinue, nil
}
s.Dump(src, 0)
if string(s.FirstChild().(*ast.Heading).Text(src)) == "1" {
s.Parent().RemoveChild(s.Parent(), s)
}
return ast.WalkContinue, nil
},
expected: autogold.Expect(`Hi
## 2
content (again)
`),
},
}

for _, tt := range tests {
tt := tt
t.Run("", func(t *testing.T) {
t.Parallel()
src := []byte(tt.input)
walk := func(node ast.Node, entering bool) (ast.WalkStatus, error) {
return tt.walk(src, node, entering)
}
var b bytes.Buffer
gm := goldmark.New(
goldmark.WithExtensions(section.Extension(2, 100)),
goldmark.WithParserOptions(
parser.WithASTTransformers(
util.Prioritized(walkTransformer(walk), 2000),
),
),
goldmark.WithRenderer(markdown.NewRenderer()),
)
require.NoError(t, gm.Convert(src, &b))
tt.expected.Equal(t, b.String())
})
}
}

0 comments on commit 32e8e2f

Please sign in to comment.