From 89d9e9973d1ec65fdd23d69664bce6fd4b164f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20SZKIBA?= Date: Mon, 8 Jan 2024 21:48:26 +0100 Subject: [PATCH] First version --- .github/dependabot.yml | 6 + .github/workflows/lint.yml | 32 ++ .github/workflows/release.yml | 28 + .github/workflows/test.yml | 31 ++ .gitignore | 3 + .golangci.yml | 44 ++ .goreleaser.yaml | 37 ++ .vscode/settings.json | 11 + LICENSE | 21 + README.md | 579 +++++++++++++++++++++ README.test.js | 21 + README_test.go | 26 + examples/factorial/README.md | 102 ++++ examples/factorial/factorial.test.js | 21 + examples/factorial/factorial_test.go | 26 + examples/fibonacci/README.md | 150 ++++++ examples/fibonacci/fibonacci.go | 21 + examples/fibonacci/fibonacci.js | 20 + examples/fibonacci/fibonacci.test.js | 11 + examples/fibonacci/fibonacci_test.go | 15 + examples/hello/README.md | 108 ++++ examples/hello/hello.go | 7 + examples/hello/hello.js | 1 + examples/hello/hello.test.js | 9 + examples/hello/hello_test.go | 37 ++ go.mod | 24 + go.sum | 41 ++ internal/cmd/dump.go | 136 +++++ internal/cmd/extract.go | 114 ++++ internal/cmd/filter.go | 72 +++ internal/cmd/help.go | 62 +++ internal/cmd/help/dump.md | 7 + internal/cmd/help/extract.md | 7 + internal/cmd/help/filtering.md | 45 ++ internal/cmd/help/invisible.md | 19 + internal/cmd/help/metadata.md | 27 + internal/cmd/help/outline.md | 5 + internal/cmd/help/regions.md | 25 + internal/cmd/help/root.md | 3 + internal/cmd/help/update.md | 7 + internal/cmd/list.go | 135 +++++ internal/cmd/options.go | 66 +++ internal/cmd/root.go | 161 ++++++ internal/cmd/update.go | 122 +++++ internal/cmd/walk.go | 13 + internal/mdcode/block.go | 9 + internal/mdcode/meta.go | 71 +++ internal/mdcode/meta_internal_test.go | 70 +++ internal/mdcode/testdata/entire-comment.js | 3 + internal/mdcode/testdata/entire-script.js | 3 + internal/mdcode/testdata/entire.js | 3 + internal/mdcode/testdata/partial.go | 8 + internal/mdcode/testdata/testdoc.md | 44 ++ internal/mdcode/testdata/testdocmod.md | 50 ++ internal/mdcode/unfence.go | 16 + internal/mdcode/walk.go | 231 ++++++++ internal/mdcode/walk_internal_test.go | 117 +++++ internal/region/region.go | 128 +++++ internal/region/region_test.go | 94 ++++ internal/region/testdata/block.js | 3 + internal/region/testdata/nonempty.js | 3 + internal/region/testdata/testdoc.js | 22 + internal/region/testdata/testdocmod.js | 28 + internal/region/testdata/testdocoutline.js | 16 + main.go | 13 + main_test.go | 6 + releases/v0.1.0.md | 3 + tools/gendoc/main.go | 96 ++++ 68 files changed, 3495 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .goreleaser.yaml create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.test.js create mode 100644 README_test.go create mode 100644 examples/factorial/README.md create mode 100644 examples/factorial/factorial.test.js create mode 100644 examples/factorial/factorial_test.go create mode 100644 examples/fibonacci/README.md create mode 100644 examples/fibonacci/fibonacci.go create mode 100644 examples/fibonacci/fibonacci.js create mode 100644 examples/fibonacci/fibonacci.test.js create mode 100644 examples/fibonacci/fibonacci_test.go create mode 100644 examples/hello/README.md create mode 100644 examples/hello/hello.go create mode 100644 examples/hello/hello.js create mode 100644 examples/hello/hello.test.js create mode 100644 examples/hello/hello_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/cmd/dump.go create mode 100644 internal/cmd/extract.go create mode 100644 internal/cmd/filter.go create mode 100644 internal/cmd/help.go create mode 100644 internal/cmd/help/dump.md create mode 100644 internal/cmd/help/extract.md create mode 100644 internal/cmd/help/filtering.md create mode 100644 internal/cmd/help/invisible.md create mode 100644 internal/cmd/help/metadata.md create mode 100644 internal/cmd/help/outline.md create mode 100644 internal/cmd/help/regions.md create mode 100644 internal/cmd/help/root.md create mode 100644 internal/cmd/help/update.md create mode 100644 internal/cmd/list.go create mode 100644 internal/cmd/options.go create mode 100644 internal/cmd/root.go create mode 100644 internal/cmd/update.go create mode 100644 internal/cmd/walk.go create mode 100644 internal/mdcode/block.go create mode 100644 internal/mdcode/meta.go create mode 100644 internal/mdcode/meta_internal_test.go create mode 100644 internal/mdcode/testdata/entire-comment.js create mode 100644 internal/mdcode/testdata/entire-script.js create mode 100644 internal/mdcode/testdata/entire.js create mode 100644 internal/mdcode/testdata/partial.go create mode 100644 internal/mdcode/testdata/testdoc.md create mode 100644 internal/mdcode/testdata/testdocmod.md create mode 100644 internal/mdcode/unfence.go create mode 100644 internal/mdcode/walk.go create mode 100644 internal/mdcode/walk_internal_test.go create mode 100644 internal/region/region.go create mode 100644 internal/region/region_test.go create mode 100644 internal/region/testdata/block.js create mode 100644 internal/region/testdata/nonempty.js create mode 100644 internal/region/testdata/testdoc.js create mode 100644 internal/region/testdata/testdocmod.js create mode 100644 internal/region/testdata/testdocoutline.js create mode 100644 main.go create mode 100644 main_test.go create mode 100644 releases/v0.1.0.md create mode 100644 tools/gendoc/main.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..cdaf002 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,32 @@ +name: lint + +on: + pull_request: + workflow_dispatch: + push: + paths-ignore: + - "docs/**" + - README.md + - "releases/**" + +permissions: + contents: read + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version: "1.21" + cache: false + - name: Go linter + uses: golangci/golangci-lint-action@v3 + with: + version: v1.55 + args: --timeout=30m + install-mode: binary diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..88048aa --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: release +on: + push: + tags: + - "v*" +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.21" + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser + version: "1.22.0" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..73f1c81 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: test + +on: + pull_request: + workflow_dispatch: + push: + paths-ignore: + - "docs/**" + - README.md + - "releases/**" + +jobs: + test: + name: Test + strategy: + matrix: + platform: + - ubuntu-latest + - macos-latest + - windows-latest + runs-on: ${{matrix.platform}} + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.21" + - name: Checkout code + uses: actions/checkout@v4 + + - name: Test + run: go test ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c3620c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/mdcode +/mdcode.exe +/build diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..29fba2b --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,44 @@ +linters: + enable-all: true + fast: false + disable: + # deprecated + - scopelint + - varcheck + - exhaustivestruct + - ifshort + - structcheck + - deadcode + - nosnakecase + - maligned + - golint + - interfacer + # disabled + - wrapcheck + + +linters-settings: + depguard: + rules: + prevent_unmaintained_packages: + files: + - $all + - "!$test" + allow: + - $gostd + - github.com/google/shlex + - github.com/yuin/goldmark + - github.com/yuin/goldmark/ast + - github.com/yuin/goldmark/text + - github.com/rodaine/table + - github.com/spf13/cobra + - github.com/gobwas/glob + - github.com/liamg/memoryfs + - github.com/szkiba/mdcode/internal + deny: + - pkg: io/ioutil + desc: "replaced by io and os packages since Go 1.16: https://tip.golang.org/doc/go1.16#ioutil" + +run: + skip-dirs: + - "examples" \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..a3d1a3d --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,37 @@ +project_name: mdcode +before: + hooks: + - go mod tidy +dist: build/dist +builds: + - env: + - CGO_ENABLED=0 + goos: ["darwin", "linux", "windows"] + goarch: ["amd64", "arm64"] + ldflags: + - '-s -w -X {{.ModulePath}}/internal/cmd.version={{.Version}} -X {{.ModulePath}}/internal/cmd.appname={{.ProjectName}}' +source: + enabled: true + name_template: "{{ .ProjectName }}_{{ .Version }}_source" + +archives: + - id: bundle + format: tar.gz + format_overrides: + - goos: windows + format: zip + +checksum: + name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" + +snapshot: + name_template: "{{ incpatch .Version }}-next+{{.ShortCommit}}{{if .IsGitDirty}}.dirty{{else}}{{end}}" + +changelog: + sort: asc + abbrev: -1 + filters: + exclude: + - "^chore:" + - "^docs:" + - "^test:" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..519dcff --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "go.lintTool": "golangci-lint", + "go.lintFlags": ["--fast"], + "[javascript]": { + "editor.formatOnSave": true, + "editor.insertSpaces": true, + "editor.tabSize": 4, + "editor.detectIndentation": false, + "editor.defaultFormatter": "vscode.typescript-language-features" + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d8e0a10 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Iván Szkiba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..041f89c --- /dev/null +++ b/README.md @@ -0,0 +1,579 @@ +# mdcode + +**Markdown code block authoring tool** + +The `mdcode` command-line tool allows code blocks embedded in a markdown document to be developed in the usual way. During the development of the code blocks, the usual tools and methods can be used. This makes the embedded codes testable, which is especially important for example codes. There is no worse developer experience than a faulty sample code. + +Here is a simple example code for a factorial calculation: + +**go** + + +```go file=README_test.go region=function +func factorial(n uint64) uint64 { + if n > 1 { + return n * factorial(n-1) + } + + return 1 +} + +``` + +**JavaScript** + + + +```js file=README.test.js region=function +function factorial(n) { + if (n > 1) { + return n * factorial(n - 1) + } + + return 1 +} + +``` + +At first glance, there is nothing special about this. *However, these code blocks are testable!* + +This document also contains the code required for testing in invisible code blocks. If you look at the [source of this document](https://github.com/szkiba/mdcode/blob/master/README.md?plain=1), you can see how easy it is to embed code blocks (even invisibly) using `mdcode`. + +Code blocks embedded in this document can be saved to files using the [`mdcode extract`](#mdcode-extract) command. A `README_test.go` and a `README.test.js` file will be created in the current directory. After modification, the code blocks can be updated from these files to the document using the [`mdcode update`](#mdcode-update) command. + +More examples can be found in the [examples](examples/) directory. + +### Features + +- include source files as code blocks in the markdown document +- update the code blocks in the markdown document +- save markdown code blocks to source files +- supports source file fragments using `#region` comments +- supports invisible (not rendered) code blocks +- allows you to add metadata to code blocks +- supports any programming language +- dump code blocks as tar archive + +### Use Cases + +**Develop the example/tutorial codes as you would any other code** + - use your favorite IDE, toolchain + - use any test framework for testing + - integrate example code testing into the build process + - use [`mdcode update`](#mdcode-update) to update example code in markdown documents + +**Write example code directly in the markdown documemts** + - use [`mdcode extract`](#mdcode-extract) to extract code blocks and save them to files + - use any test framework for testing + - integrate example code testing into the build process + +**Create a self-contained markdown tutorial document** + - use [`mdcode update`](#mdcode-update) to embed source fragments + - use [`mdcode update`](#mdcode-update) to embed additional files (package.json, go.mod, etc.) as invisible code blocks + - use [`mdcode extract`](#mdcode-extract) to extract working examples from the markdown documemt + +**Save all examples for later use** + - use [`mdcode dump`](#mdcode-dump) to create tar archive from code blocks + +### Install + +Precompiled binaries can be downloaded and installed from the [Releases](https://github.com/szkiba/mdcode/releases) page. + +If you have a go development environment, the installation can also be done with the following command: + +``` +go install github.com/szkiba/mdcode@latest +``` + +It can even be run without installation using the following command: + +``` +go run github.com/szkiba/mdcode@latest +``` + +### Usage + +Check [CLI Reference](#cli-reference) section for detailed command line usage. + +## Concepts + +### Metadata + + +Metadata can be specified for code blocks. These metadata can be used to modify the operation of the subcommands (for example, they can be used for filtering). + +The [CommonMark specification](https://spec.commonmark.org/current/) allows the use of a so-called [info-string](https://spec.commonmark.org/current/#info-string) in the fenced code block. The first word of the *info-string* typically indicates the programming language, the meaning of the remaining part is not defined by the specification. + +`mdcode` uses the part after the first word of the *info-string* to specify metadata. The metadata can be entered in JSON format and in a simple, space-separated `name="value"` format list (where the use of quotation marks is only necessary for values containing spaces). The latter form is more readable, but the JSON format is more portable. + +Example name="value" list metadata: + + ```js file=sample.js region=factorial + + ``` + +Example JSON metadata: + + ```js {"file":"sample.js","region":"factorial"} + + ``` + +Metadata used by `mdcode`: + +name | description +----------|------------------------------------------------- +`file` | name of the file assigned to the code block +`region` | name of region within file (if any) +`outline` | true if the code block is an outline of the file + +The only mandatory metadata is `file`. + + +### Filtering + + +By default, `mdcode` work with all code blocks in a markdown document. It is possible to filter code blocks based on programming language or metadata. In this case, `mdcode` ignore code blocks that do not meet the filter criteria. + +A language filter pattern can be specified using the `--lang` flag. Then only code blocks with a language matching the pattern will be processed. For example, filtering for code blocks containing JavaScript code: + + mdcode --lang js + +A file name filtering pattern can be specified using the `--file` flag. Then only code blocks with `file` metadata matching the pattern will be processed. For example, filtering for code blocks containing the file named `examples/foo.js` (or parts of it): + + mdcode --file examples/foo.js + +The `--meta` flag can be used to specify an arbitrary metadata filtering pattern. Then only code blocks with metadata matching the pattern are processed. For example, filtering for code blocks that have metadata named `name` and its value is `simple`: + + mdcode --meta name=simple + +Specifying several different filter criteria (e.g. language and metadata, or two different metadata) each criteria must be met (and relation). + +Standard glob patterns can be used in programming language and metadata filter criteria. + +pattern | match +-----------------|-------------------------------------------------------------- +`*` | matches any sequence of non-separator characters +`**` | matches any sequence of characters +`?` | matches any single non-separator character +`[` range `]` | character in range +`[` `!` range `]`| character not in range +`{` list `}` | matches any of comma-separated (without spaces) patterns +c | matches character c (c != `*`, `**`, `?`, `\`, `[`, `{`, `}`) +`\` c | matches character c + +range | match +----------|----------------------------------------- +c | matches character c (c != `\`, `-`, `]`) +`\` c | matches character c +lo `-` hi | matches character c for lo <= c <= hi + +Examples of filter pattern use: + + mdcode extract --meta file='examples/**/*.go' + mdcode extract --lang '{go,js}' + +Filtering with frequently used metadata can also be done using dedicated flags. + +flag | shorthand | equivalent +-----------------|--------------|---------------------- +`--file pattern` | `-f pattern` | `--meta file=pattern` + + +### Regions + + +In addition to embedding entire files, `mdcode` supports the use of file regions. Named regions can be used in the source code of any programming language. The beginning of the region is marked by a comment line with the content `#region name` and the end by a comment line with the content `#endregion`. + +For example, in the case of programming languages using C-style line comments (C, C++, Java, JavaScript, go, etc.): + + // #region common + + // #endregion + +In the case of programming languages using shell-style line comments (Python, sh, bash, etc.): + + # #region common + + # #endregion + +Or if only block comments can be used (CSS): + + /* #region common */ + + /* #endregion */ + +Regions marked in this way are used by IDEs to collapse parts of the source code. + +In the case of `mdcode`, regions can be referenced with the `region` metadata. If a region is specified for a code block, the subcommand (update or extract) applies only to the specified region of the file. That is, the update command only embeds the specified region from the file to the markdown document, and the extract command overwrites only the specified region in the file. + +`mdcode` can handle regions in any programming language, the only requirement is that the comment indicating the beginning and end of the region is placed in a separate line containing only the given comment. + + +### Invisible + + +It is possible to use invisible code blocks. This is useful, for embedding test code or additional files in the markdown document. The invisible code block is also useful if you want to embed the entire file, but only want to display certain parts of it. + +A markdown document can contain HTML elements. Unknown or unsupported HTML elements are usually not rendered by markdown renderers. Taking advantage of this, `mdcode` supports hiding code blocks using the standard ` + +Unfortunately, the GitHub markdown renderer renders the content of unsupported HTML elements as text. Therefore, `mdcode` also supports the use of a ` + +Unfortunately, the GitHub markdown renderer renders the content of unsupported HTML elements as text. Therefore, `mdcode` also supports the use of a ` + + + + +## file region + + + +```go file=partial.go region=function +func add(a, b uint64) uint64 { + return a + b +} + +``` diff --git a/internal/mdcode/testdata/testdocmod.md b/internal/mdcode/testdata/testdocmod.md new file mode 100644 index 0000000..e7ece85 --- /dev/null +++ b/internal/mdcode/testdata/testdocmod.md @@ -0,0 +1,50 @@ +# Test document + +## entire file + +```js file=entire.js +/* +function add(a, b) { + return a + b +} +*/ +``` + + + + + + +## file region + + + +```go file=partial.go region=function +func add(a, b uint64) uint64 { + return a + b +} + +``` diff --git a/internal/mdcode/unfence.go b/internal/mdcode/unfence.go new file mode 100644 index 0000000..827868d --- /dev/null +++ b/internal/mdcode/unfence.go @@ -0,0 +1,16 @@ +package mdcode + +func Unfence(source []byte) (Blocks, error) { + var blocks Blocks + + _, _, err := Walk(source, func(block *Block) error { + blocks = append(blocks, block) + + return nil + }) + if err != nil { + return nil, err + } + + return blocks, nil +} diff --git a/internal/mdcode/walk.go b/internal/mdcode/walk.go new file mode 100644 index 0000000..f0df553 --- /dev/null +++ b/internal/mdcode/walk.go @@ -0,0 +1,231 @@ +package mdcode + +import ( + "bytes" + "regexp" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/text" +) + +var reInfo = regexp.MustCompile(`\s*(\w+)\s*(.*)\s*`) + +type Walker func(block *Block) error + +type change struct { + fcb *ast.FencedCodeBlock + block *Block +} + +func (c *change) bounds() (int, int) { + lines := c.fcb.Lines() + if lines.Len() == 0 { + return c.fcb.Info.Segment.Stop + 1, c.fcb.Info.Segment.Stop + 1 + } + + return lines.At(0).Start, lines.At(lines.Len() - 1).Stop +} + +func (c *change) sizeIncrement() int { + start, stop := c.bounds() + + return len(c.block.Code) - (stop - start) +} + +func Walk(source []byte, walker Walker) (bool, []byte, error) { + parser := goldmark.DefaultParser() + reader := text.NewReader(source) + root := parser.Parse(reader).OwnerDocument() + + var changes []*change + + err := ast.Walk(root, func(node ast.Node, entering bool) (ast.WalkStatus, error) { + node = transformCommentedCodeBlock(node, entering, source) + + fcb := asFencedCodeBlock(node, entering) + if fcb == nil { + return ast.WalkContinue, nil + } + + block, berr := extractBlock(fcb, source) + if berr != nil { + return ast.WalkContinue, berr + } + + code := block.Code + + berr = walker(block) + if berr != nil { + return ast.WalkContinue, berr + } + + if !bytes.Equal(code, block.Code) { + changes = append(changes, &change{fcb: fcb, block: block}) + } + + return ast.WalkContinue, nil + }) + if err != nil { + return false, nil, err + } + + if len(changes) == 0 { + return false, nil, nil + } + + return true, applyChanges(changes, source), nil +} + +func asFencedCodeBlock(node ast.Node, entering bool) *ast.FencedCodeBlock { + if entering || node.Kind() != ast.KindFencedCodeBlock { + return nil + } + + if fcb, ok := node.(*ast.FencedCodeBlock); ok { + return fcb + } + + return nil +} + +func extractBlock(fcb *ast.FencedCodeBlock, source []byte) (*Block, error) { + lang, meta, err := extractInfo(fcb, source) + if err != nil { + return nil, err + } + + return &Block{Lang: lang, Meta: meta, Code: extractCode(fcb, source)}, nil +} + +func extractCode(fcb *ast.FencedCodeBlock, source []byte) []byte { + var buff bytes.Buffer + + lines := fcb.Lines() + for i := 0; i < lines.Len(); i++ { + seg := lines.At(i) + + buff.Write(seg.Value(source)) + } + + return buff.Bytes() +} + +func extractInfo(fcb *ast.FencedCodeBlock, source []byte) (string, Meta, error) { + if fcb.Info == nil { + return "", nil, nil + } + + return parseInfo(fcb.Info.Text(source)) +} + +func parseInfo(text []byte) (string, Meta, error) { + all := reInfo.FindSubmatch(text) + if all == nil { + return "", nil, nil + } + + var ( + lang string + meta Meta + err error + ) + + if len(all) > 1 { + lang = string(all[1]) + } + + if len(all) <= 2 { //nolint:gomnd + return lang, meta, nil + } + + meta, err = parseMeta(all[2]) + + return lang, meta, err +} + +func applyChanges(changes []*change, source []byte) []byte { + resSize := len(source) + + for _, change := range changes { + resSize += change.sizeIncrement() + } + + result := make([]byte, resSize) + + var srcIdx, resIdx int + + for _, change := range changes { + start, stop := change.bounds() + + copy(result[resIdx:], source[srcIdx:start]) + resIdx += (start - srcIdx) + + copy(result[resIdx:], change.block.Code) + resIdx += len(change.block.Code) + + srcIdx = stop + } + + copy(result[resIdx:], source[srcIdx:]) + + return result +} + +var ( + reCommentedCodeBlock = regexp.MustCompile(`^\s*(