Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,77 @@ files, since `.go` matches `go`). However, this will fail with other files like
[embedmd]:# (file.ext)
```

## Options

Options can be added after the regular expressions in an `embedmd` command.

### Excluding delimiter lines

Prefix a regexp with `!` to use it as a boundary but exclude the matching line
from the output. This is useful when you use comments in your code to mark
snippet boundaries:

```go
// snippet-start
fmt.Println("hello")
// snippet-end
```

```Markdown
[embedmd]:# (example.go go !/\/\/ snippet-start/ !/\/\/ snippet-end/)
```

This embeds only `fmt.Println("hello")`, excluding the marker comments.

You can also exclude just the start or just the end:

```Markdown
[embedmd]:# (example.go go !/\/\/ start/ /end/)
```

### Stripping indentation

Code inside functions is typically indented. The `dedent` option removes the
common leading whitespace from all lines, so your Markdown code blocks are not
unnecessarily indented:

```Markdown
[embedmd]:# (example.go go !/\/\/ snippet-start/ !/\/\/ snippet-end/ dedent)
```

### Trimming trailing blank lines

Go's `gofmt` inserts a blank line between code and a following comment. The
`trim` option removes trailing blank lines from the extracted content:

```Markdown
[embedmd]:# (example.go go !/\/\/ snippet-start/ !/\/\/ snippet-end/ trim)
```

### Text substitution

The `s/old/new/` option replaces all occurrences of `old` with `new` in the
extracted content. This lets you use placeholder tokens in compilable code:

```Markdown
[embedmd]:# (example.go go /func demo/ /}/ s/ELLIPSIS/.../)
```

Multiple substitutions can be chained:

```Markdown
[embedmd]:# (example.go go /func/ /}/ s/ELLIPSIS/.../ s/_ = ELLIPSIS/.../)
```

### Combining options

Options can be combined. They are applied in this order: line exclusion, trailing
blank line trimming, dedentation, then text substitution.

```Markdown
[embedmd]:# (example.go go !/\/\/ snippet-start/ !/\/\/ snippet-end/ dedent trim s/ELLIPSIS/.../)
```

## Installation

> You can install Go by following [these instructions](https://golang.org/doc/install).
Expand Down
126 changes: 112 additions & 14 deletions embedmd/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,23 @@ package embedmd

import (
"errors"
"fmt"
"path/filepath"
"strings"
)

type substitution struct {
old, new string
}

type command struct {
path, lang string
start, end *string
path, lang string
start, end *string
excludeStart bool
excludeEnd bool
dedent bool
trim bool
substitutions []substitution
}

func parseCommand(s string) (*command, error) {
Expand All @@ -40,7 +50,7 @@ func parseCommand(s string) (*command, error) {

cmd := &command{path: args[0]}
args = args[1:]
if len(args) > 0 && args[0][0] != '/' {
if len(args) > 0 && !isRegexpOrOption(args[0]) {
cmd.lang, args = args[0], args[1:]
} else {
ext := filepath.Ext(cmd.path[1:])
Expand All @@ -50,31 +60,119 @@ func parseCommand(s string) (*command, error) {
cmd.lang = ext[1:]
}

switch {
case len(args) == 1:
cmd.start = &args[0]
case len(args) == 2:
cmd.start, cmd.end = &args[0], &args[1]
case len(args) > 2:
return nil, errors.New("too many arguments")
// Consume regexp arguments (starting with / or !/ or bare $).
for len(args) > 0 {
arg := args[0]
isRegexp := strings.HasPrefix(arg, "/")
isExclude := strings.HasPrefix(arg, "!/")
isDollar := arg == "$"
isExcludeDollar := arg == "!$"

if isExcludeDollar {
return nil, errors.New("exclude (!) cannot be used with $")
}

if !isRegexp && !isExclude && !isDollar {
break
}

if cmd.start == nil && !isDollar {
re := arg
if isExclude {
re = arg[1:]
cmd.excludeStart = true
}
cmd.start = &re
} else if cmd.end == nil {
re := arg
if isExclude {
re = arg[1:]
cmd.excludeEnd = true
}
cmd.end = &re
} else {
return nil, errors.New("too many arguments")
}
args = args[1:]
}

// Single regexp with exclude is useless (would produce empty output).
if cmd.start != nil && cmd.end == nil && cmd.excludeStart {
return nil, errors.New("exclude (!) cannot be used with a single regexp")
}

// Consume options: dedent, trim, s/old/new/.
for _, arg := range args {
switch {
case arg == "dedent":
cmd.dedent = true
case arg == "trim":
cmd.trim = true
case strings.HasPrefix(arg, "s/"):
sub, err := parseSubstitution(arg)
if err != nil {
return nil, err
}
cmd.substitutions = append(cmd.substitutions, sub)
default:
return nil, fmt.Errorf("unknown option %q", arg)
}
}

return cmd, nil
}

func parseSubstitution(s string) (substitution, error) {
// s is already validated as a complete s/old/new/ token by fields().
inner := s[2 : len(s)-1] // strip "s/" and trailing "/"
idx := strings.Index(inner, "/")
if idx < 0 {
return substitution{}, fmt.Errorf("invalid substitution %q", s)
}
return substitution{old: inner[:idx], new: inner[idx+1:]}, nil
}

// isRegexpOrOption reports whether arg looks like a regexp (/.../, !/.../, $)
// or an option (dedent, trim, s/.../) rather than a language identifier.
func isRegexpOrOption(arg string) bool {
return arg[0] == '/' || arg[0] == '!' || arg == "$" ||
strings.HasPrefix(arg, "s/") || arg == "dedent" || arg == "trim"
}

// fields returns a list of the groups of text separated by blanks,
// keeping all text surrounded by / as a group.
// It also handles !/regexp/ (exclude) and s/old/new/ (substitution) tokens.
func fields(s string) ([]string, error) {
var args []string

for s = strings.TrimSpace(s); len(s) > 0; s = strings.TrimSpace(s) {
if s[0] == '/' {
sep := nextSlash(s[1:])
regexpStart := s[0] == '/'
excludeRegexp := len(s) > 1 && s[0] == '!' && s[1] == '/'
substStart := len(s) > 1 && s[0] == 's' && s[1] == '/'

switch {
case regexpStart, excludeRegexp:
offset := 0
if excludeRegexp {
offset = 1
}
sep := nextSlash(s[offset+1:])
if sep < 0 {
return nil, errors.New("unbalanced /")
}
args, s = append(args, s[:sep+2]), s[sep+2:]
} else {
args, s = append(args, s[:offset+sep+2]), s[offset+sep+2:]
case substStart:
sep1 := nextSlash(s[2:])
if sep1 < 0 {
return nil, errors.New("unbalanced / in substitution")
}
sep2 := nextSlash(s[2+sep1+1:])
if sep2 < 0 {
return nil, errors.New("unbalanced / in substitution")
}
end := 2 + sep1 + 1 + sep2 + 1
args, s = append(args, s[:end]), s[end:]
default:
sep := strings.IndexByte(s[1:], ' ')
if sep < 0 {
return append(args, s), nil
Expand Down
Loading