From 0fc60bc54f9f1f562fe0a5cf0c20c56b9102738d Mon Sep 17 00:00:00 2001 From: jolheiser Date: Wed, 22 Dec 2021 23:48:19 -0600 Subject: [PATCH] Initial Commit Signed-off-by: jolheiser --- .gitignore | 3 ++ LICENSE | 19 +++++++ README.md | 110 +++++++++++++++++++++++++++++++++++++++++ cmd/emdbed/main.go | 39 +++++++++++++++ emdbed.go | 81 ++++++++++++++++++++++++++++++ emdbed_example_test.go | 27 ++++++++++ emdbed_test.go | 29 +++++++++++ go.mod | 5 ++ go.sum | 2 + io.go | 87 ++++++++++++++++++++++++++++++++ readme.go | 20 ++++++++ testdata/main.go | 7 +++ testdata/main.md | 2 + testdata/main.txt | 7 +++ 14 files changed, 438 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmd/emdbed/main.go create mode 100644 emdbed.go create mode 100644 emdbed_example_test.go create mode 100644 emdbed_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 io.go create mode 100644 readme.go create mode 100644 testdata/main.go create mode 100644 testdata/main.md create mode 100644 testdata/main.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53094d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +/emdbed +/emdbed.exe \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..433f7db --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e44853 --- /dev/null +++ b/README.md @@ -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: + + +```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: + // + //```go + //package main + // + //import "fmt" + // + //func main() { + // fmt.Println("Hello, world!") + //} + // ``` + // +} + +``` + + +Just the package: + + +```go +package emdbed +``` + + +First line until the end of imports: + + +```go +package emdbed + +import ( + "fmt" +) +``` + + +Only the example func + + +```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: + // + //```go + //package main + // + //import "fmt" + // + //func main() { + // fmt.Println("Hello, world!") + //} + // ``` + // +} +``` + + +A file in another directory (choosing/overriding the language) + + +```go +package main + +import "fmt" + +func main() { + fmt.Println("This file has no extension") +} +``` + + +## License + +[MIT](LICENSE) \ No newline at end of file diff --git a/cmd/emdbed/main.go b/cmd/emdbed/main.go new file mode 100644 index 0000000..f5f3b1a --- /dev/null +++ b/cmd/emdbed/main.go @@ -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) + } +} diff --git a/emdbed.go b/emdbed.go new file mode 100644 index 0000000..aa350b0 --- /dev/null +++ b/emdbed.go @@ -0,0 +1,81 @@ +package emdbed + +import ( + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" +) + +var ( + start = regexp.MustCompile(`(?m)^$`) + end = regexp.MustCompile(`(?m)^$`) +) + +// 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 +} diff --git a/emdbed_example_test.go b/emdbed_example_test.go new file mode 100644 index 0000000..492bb16 --- /dev/null +++ b/emdbed_example_test.go @@ -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: + // + //```go + //package main + // + //import "fmt" + // + //func main() { + // fmt.Println("Hello, world!") + //} + // ``` + // +} diff --git a/emdbed_test.go b/emdbed_test.go new file mode 100644 index 0000000..c220302 --- /dev/null +++ b/emdbed_test.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..22d40e3 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module go.jolheiser.com/emdbed + +go 1.17 + +require github.com/matryer/is v1.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ddd6bbf --- /dev/null +++ b/go.sum @@ -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= diff --git a/io.go b/io.go new file mode 100644 index 0000000..adc83f9 --- /dev/null +++ b/io.go @@ -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 +} diff --git a/readme.go b/readme.go new file mode 100644 index 0000000..0f5a333 --- /dev/null +++ b/readme.go @@ -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) + } +} diff --git a/testdata/main.go b/testdata/main.go new file mode 100644 index 0000000..f7b60bd --- /dev/null +++ b/testdata/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Hello, world!") +} diff --git a/testdata/main.md b/testdata/main.md new file mode 100644 index 0000000..b2f9f5a --- /dev/null +++ b/testdata/main.md @@ -0,0 +1,2 @@ + + diff --git a/testdata/main.txt b/testdata/main.txt new file mode 100644 index 0000000..be11d3c --- /dev/null +++ b/testdata/main.txt @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("This file has no extension") +} \ No newline at end of file