commit
0fc60bc54f
|
@ -0,0 +1,3 @@
|
|||
.idea/
|
||||
/emdbed
|
||||
/emdbed.exe
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2021 John Olheiser
|
||||
|
||||
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.
|
|
@ -0,0 +1,110 @@
|
|||
# emdbed
|
||||
|
||||
Embed "things" in your markdown files.
|
||||
|
||||
Given the same input, `emdbed` should give idempotent results.
|
||||
|
||||
## Examples
|
||||
|
||||
The following are generated using [readme.go](readme.go) and `go generate ./...`.
|
||||
|
||||
An entire file:
|
||||
|
||||
<!-- emdbed: emdbed_example_test.go -->
|
||||
```go
|
||||
package emdbed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
fi, err := testdata.Open("testdata/main.md")
|
||||
defer fi.Close()
|
||||
out, err := Convert("testdata", fi)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(out)
|
||||
// Output:
|
||||
//<!-- emdbed: main.go -->
|
||||
//```go
|
||||
//package main
|
||||
//
|
||||
//import "fmt"
|
||||
//
|
||||
//func main() {
|
||||
// fmt.Println("Hello, world!")
|
||||
//}
|
||||
// ```
|
||||
//<!-- /emdbed -->
|
||||
}
|
||||
|
||||
```
|
||||
<!-- /emdbed -->
|
||||
|
||||
Just the package:
|
||||
|
||||
<!-- emdbed: emdbed_example_test.go L1 L1 -->
|
||||
```go
|
||||
package emdbed
|
||||
```
|
||||
<!-- /emdbed -->
|
||||
|
||||
First line until the end of imports:
|
||||
|
||||
<!-- emdbed: emdbed_example_test.go L1 /\)/ -->
|
||||
```go
|
||||
package emdbed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
```
|
||||
<!-- /emdbed -->
|
||||
|
||||
Only the example func
|
||||
|
||||
<!-- emdbed: emdbed_example_test.go /func Example/ /(?m)^\}/ -->
|
||||
```go
|
||||
func Example() {
|
||||
fi, err := testdata.Open("testdata/main.md")
|
||||
defer fi.Close()
|
||||
out, err := Convert("testdata", fi)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(out)
|
||||
// Output:
|
||||
//<!-- emdbed: main.go -->
|
||||
//```go
|
||||
//package main
|
||||
//
|
||||
//import "fmt"
|
||||
//
|
||||
//func main() {
|
||||
// fmt.Println("Hello, world!")
|
||||
//}
|
||||
// ```
|
||||
//<!-- /emdbed -->
|
||||
}
|
||||
```
|
||||
<!-- /emdbed -->
|
||||
|
||||
A file in another directory (choosing/overriding the language)
|
||||
|
||||
<!-- emdbed: testdata/main.txt ~go -->
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("This file has no extension")
|
||||
}
|
||||
```
|
||||
<!-- /emdbed -->
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
|
@ -0,0 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"go.jolheiser.com/emdbed"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fs := flag.NewFlagSet("emdbed", flag.ExitOnError)
|
||||
writeFlag := fs.Bool("write", false, "Write output")
|
||||
writeShortFlag := fs.Bool("w", false, "Write output")
|
||||
if err := fs.Parse(os.Args[1:]); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("emdbed requires a file argument")
|
||||
return
|
||||
}
|
||||
|
||||
convert, err := emdbed.ConvertFile(fs.Arg(0))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
write := *writeFlag || *writeShortFlag
|
||||
if !write {
|
||||
fmt.Println(convert)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(fs.Arg(0), []byte(convert), os.ModePerm); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package emdbed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
start = regexp.MustCompile(`(?m)^<!--\s*emdbed:\s*([^\s]+)\s*(~[\w]+)?\s*(?:(/[^/]+/|L\d+)\s*(/[^/]+/|L\d+)?)?\s*-->$`)
|
||||
end = regexp.MustCompile(`(?m)^<!--\s*/emdbed\s*-->$`)
|
||||
)
|
||||
|
||||
// ConvertFile is a helper for converting a file on disk
|
||||
func ConvertFile(path string) (string, error) {
|
||||
fi, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer fi.Close()
|
||||
return Convert(filepath.Dir(path), fi)
|
||||
}
|
||||
|
||||
// Convert converts an io.Reader with respect to a base directory
|
||||
func Convert(dir string, file io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
contents := string(data)
|
||||
|
||||
var out strings.Builder
|
||||
starts := start.FindAllStringSubmatchIndex(contents, -1)
|
||||
ends := end.FindAllStringIndex(contents, -1)
|
||||
if len(starts) != len(ends) {
|
||||
return "", fmt.Errorf("starts (%d) != ends (%d)", len(starts), len(ends))
|
||||
}
|
||||
var last int
|
||||
for idx, start := range starts {
|
||||
out.WriteString(contents[last:start[1]])
|
||||
|
||||
relPath := contents[start[2]:start[3]]
|
||||
fileContents, err := os.ReadFile(filepath.Join(dir, relPath))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lang := strings.TrimPrefix(filepath.Ext(relPath), ".")
|
||||
if start[4] != -1 {
|
||||
lang = contents[start[4]+1 : start[5]]
|
||||
}
|
||||
|
||||
var startSel string
|
||||
if start[6] != -1 {
|
||||
startSel = contents[start[6]:start[7]]
|
||||
}
|
||||
var endSel string
|
||||
if start[8] != -1 {
|
||||
endSel = contents[start[8]:start[9]]
|
||||
}
|
||||
|
||||
e := emdbed{
|
||||
name: relPath,
|
||||
contents: string(fileContents),
|
||||
language: lang,
|
||||
start: startSel,
|
||||
end: endSel,
|
||||
}
|
||||
sel, err := e.Selection()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out.WriteString(fmt.Sprintf("\n```%s\n%s\n```\n", e.language, sel))
|
||||
last = ends[idx][0]
|
||||
}
|
||||
out.WriteString(contents[last:])
|
||||
return out.String(), nil
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package emdbed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
fi, err := testdata.Open("testdata/main.md")
|
||||
defer fi.Close()
|
||||
out, err := Convert("testdata", fi)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(out)
|
||||
// Output:
|
||||
//<!-- emdbed: main.go -->
|
||||
//```go
|
||||
//package main
|
||||
//
|
||||
//import "fmt"
|
||||
//
|
||||
//func main() {
|
||||
// fmt.Println("Hello, world!")
|
||||
//}
|
||||
// ```
|
||||
//<!-- /emdbed -->
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package emdbed
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
//go:embed testdata
|
||||
var testdata embed.FS
|
||||
|
||||
func TestIdempotent(t *testing.T) {
|
||||
assert := is.NewRelaxed(t)
|
||||
|
||||
fi, err := testdata.Open("testdata/main.md")
|
||||
defer fi.Close()
|
||||
out, err := Convert("testdata", fi)
|
||||
assert.NoErr(err) // Conversion should be successful
|
||||
|
||||
idem := out
|
||||
for idx := 0; idx < 100; idx++ {
|
||||
idem, err = Convert("testdata", strings.NewReader(idem))
|
||||
assert.NoErr(err) // Conversion should be successful
|
||||
}
|
||||
|
||||
assert.Equal(out, idem) // Original output should match after 100 tries
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module go.jolheiser.com/emdbed
|
||||
|
||||
go 1.17
|
||||
|
||||
require github.com/matryer/is v1.4.0
|
|
@ -0,0 +1,2 @@
|
|||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
|
@ -0,0 +1,87 @@
|
|||
package emdbed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var endline = regexp.MustCompile(`(?m)$`)
|
||||
|
||||
type emdbed struct {
|
||||
name string
|
||||
contents string
|
||||
language string
|
||||
start string
|
||||
end string
|
||||
}
|
||||
|
||||
func (e emdbed) line(line int, end bool) (int, error) {
|
||||
if line == 1 && !end {
|
||||
return 0, nil
|
||||
}
|
||||
indexes := endline.FindAllStringIndex(e.contents, -1)
|
||||
if len(indexes) < line {
|
||||
return 0, fmt.Errorf("file %q has no line %d", e.name, line)
|
||||
}
|
||||
return indexes[line-1][0], nil
|
||||
}
|
||||
|
||||
func (e emdbed) regex(pattern string, end bool) (int, error) {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
match := re.FindStringIndex(e.contents)
|
||||
if match == nil {
|
||||
return 0, fmt.Errorf("file %q has no match for pattern %q", e.name, pattern)
|
||||
}
|
||||
var inc int
|
||||
if end {
|
||||
inc++
|
||||
}
|
||||
return match[0] + inc, nil
|
||||
}
|
||||
|
||||
func (e emdbed) selector(selector string, end bool) (int, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(selector, "L"):
|
||||
line, err := strconv.Atoi(selector[1:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return e.line(line, end)
|
||||
case strings.HasPrefix(selector, "/"):
|
||||
pattern := strings.Trim(selector, "/")
|
||||
return e.regex(pattern, end)
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown selector %q", selector)
|
||||
}
|
||||
}
|
||||
|
||||
func (e emdbed) Start() (int, error) {
|
||||
if e.start == "" {
|
||||
return 0, nil
|
||||
}
|
||||
return e.selector(e.start, false)
|
||||
}
|
||||
|
||||
func (e emdbed) End() (int, error) {
|
||||
if e.end == "" {
|
||||
return len(e.contents), nil
|
||||
}
|
||||
return e.selector(e.end, true)
|
||||
}
|
||||
|
||||
func (e emdbed) Selection() (string, error) {
|
||||
start, err := e.Start()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
end, err := e.End()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return e.contents[start:end], nil
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
//go:build generate
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"go.jolheiser.com/emdbed"
|
||||
)
|
||||
|
||||
//go:generate go run readme.go
|
||||
func main() {
|
||||
convert, err := emdbed.ConvertFile("README.md")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := os.WriteFile("README.md", []byte(convert), os.ModePerm); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Hello, world!")
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<!-- emdbed: main.go -->
|
||||
<!-- /emdbed -->
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("This file has no extension")
|
||||
}
|
Loading…
Reference in New Issue