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