diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0a524b..abfef6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,5 +32,6 @@ jobs: - run: nix develop --ignore-environment --command make vendor - run: nix develop --ignore-environment --command make fmt-check + - run: nix develop --ignore-environment --command make lint - run: nix develop --ignore-environment --command make vet - - run: nix develop --ignore-environment --command make test \ No newline at end of file + - run: nix develop --ignore-environment --command make test diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..7a88653 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,13 @@ +version: "2" + +linters: + default: none + enable: + - godox + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + +run: + modules-download-mode: vendor diff --git a/.mockery.yaml b/.mockery.yaml index c1a8c3a..ca80148 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -4,6 +4,10 @@ packages: litdoc/internal: interfaces: Cell: + config: + dir: ./internal/ + pkgname: internal_test + CellParser: config: dir: ./internal/ pkgname: internal_test \ No newline at end of file diff --git a/Makefile b/Makefile index ecbc794..bdc198f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ GO ?= go .PHONY: pre-pr -pre-pr: clean mock fmt-check vet test +pre-pr: clean mock fmt-check lint vet test .PHONY: fmt fmt: @@ -20,6 +20,10 @@ fmt-check: vet: @$(GO) vet ./... +.PHONY: lint +lint: vendor + @golangci-lint run ./... + .PHONY: mock mock: @mockery diff --git a/cmd/file.go b/cmd/file.go index e146666..9c2de53 100644 --- a/cmd/file.go +++ b/cmd/file.go @@ -6,6 +6,8 @@ import ( "path/filepath" "litdoc/internal" + "litdoc/internal/bash" + "litdoc/internal/static" "github.com/spf13/cobra" ) @@ -18,7 +20,11 @@ var fileCmd = &cobra.Command{ Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { path := args[0] - data, err := internal.ProcessFile(path) + parsers := map[string]internal.CellParser{ + "static": internal.CellParserFunc(static.ParseCell), + "bash": internal.CellParserFunc(bash.ParseCell), + } + data, err := internal.ProcessFile(path, parsers) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) diff --git a/flake.nix b/flake.nix index ca92971..b81c8fb 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,7 @@ devShells.default = pkgs.mkShell { packages = with pkgs; [ go + golangci-lint go-mockery cacert ]; @@ -27,4 +28,4 @@ }; } ); -} \ No newline at end of file +} diff --git a/internal/CellParser_mock_test.go b/internal/CellParser_mock_test.go new file mode 100644 index 0000000..0e4317d --- /dev/null +++ b/internal/CellParser_mock_test.go @@ -0,0 +1,112 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package internal_test + +import ( + "litdoc/internal" + + mock "github.com/stretchr/testify/mock" +) + +// NewMockCellParser creates a new instance of MockCellParser. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockCellParser(t interface { + mock.TestingT + Cleanup(func()) +}) *MockCellParser { + mock := &MockCellParser{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockCellParser is an autogenerated mock type for the CellParser type +type MockCellParser struct { + mock.Mock +} + +type MockCellParser_Expecter struct { + mock *mock.Mock +} + +func (_m *MockCellParser) EXPECT() *MockCellParser_Expecter { + return &MockCellParser_Expecter{mock: &_m.Mock} +} + +// Parse provides a mock function for the type MockCellParser +func (_mock *MockCellParser) Parse(block internal.Block, following []internal.Block) (internal.Cell, int, error) { + ret := _mock.Called(block, following) + + if len(ret) == 0 { + panic("no return value specified for Parse") + } + + var r0 internal.Cell + var r1 int + var r2 error + if returnFunc, ok := ret.Get(0).(func(internal.Block, []internal.Block) (internal.Cell, int, error)); ok { + return returnFunc(block, following) + } + if returnFunc, ok := ret.Get(0).(func(internal.Block, []internal.Block) internal.Cell); ok { + r0 = returnFunc(block, following) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(internal.Cell) + } + } + if returnFunc, ok := ret.Get(1).(func(internal.Block, []internal.Block) int); ok { + r1 = returnFunc(block, following) + } else { + r1 = ret.Get(1).(int) + } + if returnFunc, ok := ret.Get(2).(func(internal.Block, []internal.Block) error); ok { + r2 = returnFunc(block, following) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// MockCellParser_Parse_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Parse' +type MockCellParser_Parse_Call struct { + *mock.Call +} + +// Parse is a helper method to define mock.On call +// - block internal.Block +// - following []internal.Block +func (_e *MockCellParser_Expecter) Parse(block interface{}, following interface{}) *MockCellParser_Parse_Call { + return &MockCellParser_Parse_Call{Call: _e.mock.On("Parse", block, following)} +} + +func (_c *MockCellParser_Parse_Call) Run(run func(block internal.Block, following []internal.Block)) *MockCellParser_Parse_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 internal.Block + if args[0] != nil { + arg0 = args[0].(internal.Block) + } + var arg1 []internal.Block + if args[1] != nil { + arg1 = args[1].([]internal.Block) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockCellParser_Parse_Call) Return(cell internal.Cell, n int, err error) *MockCellParser_Parse_Call { + _c.Call.Return(cell, n, err) + return _c +} + +func (_c *MockCellParser_Parse_Call) RunAndReturn(run func(block internal.Block, following []internal.Block) (internal.Cell, int, error)) *MockCellParser_Parse_Call { + _c.Call.Return(run) + return _c +} diff --git a/internal/bash/cell.go b/internal/bash/cell.go new file mode 100644 index 0000000..0292b1c --- /dev/null +++ b/internal/bash/cell.go @@ -0,0 +1,57 @@ +package bash + +import ( + "fmt" + + "litdoc/internal" +) + +type Cell struct { + block internal.Block + output internal.Output +} + +func MakeCellFromRaw(content, indent string, output internal.Output) Cell { + return Cell{ + block: internal.MakeBlockFromRaw(internal.BlockKindFencedCode, content, indent, false), + output: output, + } +} + +func ParseCell( + block internal.Block, + following []internal.Block, +) ( + internal.Cell, + int, + error, +) { + return parseCellWith(block, following, internal.OutputFromBlocks) +} + +func parseCellWith( + block internal.Block, + following []internal.Block, + parseOutput func(internal.Block, []internal.Block) (internal.Output, int, error), +) ( + internal.Cell, + int, + error, +) { + output, consumed, err := parseOutput(block, following) + if err != nil { + return nil, 0, fmt.Errorf("parsing output: %w", err) + } + return Cell{block: block, output: output}, consumed, nil +} + +func (c Cell) Execute() (internal.Cell, error) { + return Cell{ + block: c.block, + output: internal.MakeOutput("output", c.block.Indent()), + }, nil +} + +func (c Cell) Render() (string, error) { + return c.block.Render() + c.output.Render(), nil +} diff --git a/internal/bash/cell_test.go b/internal/bash/cell_test.go new file mode 100644 index 0000000..b7ad715 --- /dev/null +++ b/internal/bash/cell_test.go @@ -0,0 +1,135 @@ +package bash_test + +import ( + "strings" + "testing" + + "litdoc/internal" + "litdoc/internal/bash" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func joinLines(lines ...string) string { + return strings.Join(lines, "\n") +} + +func TestMakeCellFromRaw(t *testing.T) { + // given + code := joinLines( + "```bash", + "echo hello", + "```", + "", + ) + output := internal.MakeOutput("hello", "") + cell := bash.MakeCellFromRaw(code, "", output) + + // when + got, err := cell.Render() + + // then + require.NoError(t, err) + assert.Equal(t, "```bash\necho hello\n```\n"+output.Render(), got) +} + +func TestParseCellWith(t *testing.T) { + block := internal.MakeBlockFromRaw(internal.BlockKindFencedCode, joinLines( + "```bash", + "echo hello", + "```", + "", + ), "", false) + + t.Run("assembles cell from block and output", func(t *testing.T) { + // given + output := internal.MakeOutput("hello", "") + parseOutput := func(internal.Block, []internal.Block) (internal.Output, int, error) { + return output, 3, nil + } + + // when + cell, consumed, err := bash.ParseCellWith(block, nil, parseOutput) + + // then + require.NoError(t, err) + assert.Equal(t, 3, consumed) + rendered, err := cell.Render() + require.NoError(t, err) + assert.Equal(t, block.Render()+output.Render(), rendered) + }) + + t.Run("output parsing error is wrapped", func(t *testing.T) { + // given + parseOutput := func(internal.Block, []internal.Block) (internal.Output, int, error) { + return internal.Output{}, 0, assert.AnError + } + + // when + _, _, err := bash.ParseCellWith(block, nil, parseOutput) + + // then + require.ErrorContains(t, err, "parsing output") + require.ErrorIs(t, err, assert.AnError) + }) +} + +func TestRender(t *testing.T) { + t.Run("without output", func(t *testing.T) { + // given + code := joinLines( + "```bash", + "echo hello", + "```", + "", + ) + cell := bash.MakeCellFromRaw(code, "", internal.MakeOutput("", "")) + + // when + gotContent, err := cell.Render() + + // then + require.NoError(t, err) + assert.Equal(t, code, gotContent) + }) + + t.Run("with output", func(t *testing.T) { + // given + fencedCode := joinLines( + "```bash", + "echo hello", + "```", + "", + ) + output := internal.MakeOutput("hello", "") + cell := bash.MakeCellFromRaw(fencedCode, "", output) + + // when + gotContent, err := cell.Render() + + // then + require.NoError(t, err) + assert.Equal(t, fencedCode+output.Render(), gotContent) + }) +} + +func TestExecute(t *testing.T) { + // given + fencedCode := joinLines( + "```bash", + "echo hello", + "```", + "", + ) + cell := bash.MakeCellFromRaw(fencedCode, "", internal.MakeOutput("", "")) + + // when + gotCell, err := cell.Execute() + + // then + require.NoError(t, err) + rendered, err := gotCell.Render() + require.NoError(t, err) + assert.Equal(t, fencedCode+internal.MakeOutput("output", "").Render(), rendered) +} diff --git a/internal/bash/export_test.go b/internal/bash/export_test.go new file mode 100644 index 0000000..4d90090 --- /dev/null +++ b/internal/bash/export_test.go @@ -0,0 +1,3 @@ +package bash + +var ParseCellWith = parseCellWith diff --git a/internal/block.go b/internal/block.go index d6ef63f..75196a1 100644 --- a/internal/block.go +++ b/internal/block.go @@ -25,7 +25,7 @@ type Block struct { // indent characters potentially nested blockquotes and lists indent string content string - // continuation when preceding block is on the smame line + // continuation when a preceding block is on the same line continuation bool } @@ -51,8 +51,70 @@ func (b Block) Content() string { return b.content } func (b Block) Continuation() bool { return b.continuation } -func (b Block) String() string { - return fmt.Sprintf("{%s %q}", b.kind, b.content) +// headerLine returns the text on the block's opening line after stripping +// the syntactic delimiter (fence characters for fenced code blocks, +// "