diff --git a/.mockery.yaml b/.mockery.yaml index ca80148..01d5365 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -10,4 +10,12 @@ packages: CellParser: config: dir: ./internal/ - pkgname: internal_test \ No newline at end of file + pkgname: internal_test + OutputParser: + config: + dir: ./internal/bash/ + pkgname: bash_test + Runner: + config: + dir: ./internal/bash/ + pkgname: bash_test \ No newline at end of file diff --git a/cmd/file.go b/cmd/file.go index 9c2de53..7af2913 100644 --- a/cmd/file.go +++ b/cmd/file.go @@ -21,8 +21,8 @@ var fileCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { path := args[0] parsers := map[string]internal.CellParser{ - "static": internal.CellParserFunc(static.ParseCell), - "bash": internal.CellParserFunc(bash.ParseCell), + "static": static.NewParser(), + "bash": bash.MakeParser(), } data, err := internal.ProcessFile(path, parsers) if err != nil { diff --git a/internal/bash/OutputParser_mock_test.go b/internal/bash/OutputParser_mock_test.go new file mode 100644 index 0000000..717340d --- /dev/null +++ b/internal/bash/OutputParser_mock_test.go @@ -0,0 +1,110 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package bash_test + +import ( + "litdoc/internal" + + mock "github.com/stretchr/testify/mock" +) + +// NewMockOutputParser creates a new instance of MockOutputParser. 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 NewMockOutputParser(t interface { + mock.TestingT + Cleanup(func()) +}) *MockOutputParser { + mock := &MockOutputParser{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockOutputParser is an autogenerated mock type for the OutputParser type +type MockOutputParser struct { + mock.Mock +} + +type MockOutputParser_Expecter struct { + mock *mock.Mock +} + +func (_m *MockOutputParser) EXPECT() *MockOutputParser_Expecter { + return &MockOutputParser_Expecter{mock: &_m.Mock} +} + +// Parse provides a mock function for the type MockOutputParser +func (_mock *MockOutputParser) Parse(litdoc internal.Block, following []internal.Block) (internal.Output, int, error) { + ret := _mock.Called(litdoc, following) + + if len(ret) == 0 { + panic("no return value specified for Parse") + } + + var r0 internal.Output + var r1 int + var r2 error + if returnFunc, ok := ret.Get(0).(func(internal.Block, []internal.Block) (internal.Output, int, error)); ok { + return returnFunc(litdoc, following) + } + if returnFunc, ok := ret.Get(0).(func(internal.Block, []internal.Block) internal.Output); ok { + r0 = returnFunc(litdoc, following) + } else { + r0 = ret.Get(0).(internal.Output) + } + if returnFunc, ok := ret.Get(1).(func(internal.Block, []internal.Block) int); ok { + r1 = returnFunc(litdoc, following) + } else { + r1 = ret.Get(1).(int) + } + if returnFunc, ok := ret.Get(2).(func(internal.Block, []internal.Block) error); ok { + r2 = returnFunc(litdoc, following) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// MockOutputParser_Parse_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Parse' +type MockOutputParser_Parse_Call struct { + *mock.Call +} + +// Parse is a helper method to define mock.On call +// - litdoc internal.Block +// - following []internal.Block +func (_e *MockOutputParser_Expecter) Parse(litdoc interface{}, following interface{}) *MockOutputParser_Parse_Call { + return &MockOutputParser_Parse_Call{Call: _e.mock.On("Parse", litdoc, following)} +} + +func (_c *MockOutputParser_Parse_Call) Run(run func(litdoc internal.Block, following []internal.Block)) *MockOutputParser_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 *MockOutputParser_Parse_Call) Return(output internal.Output, n int, err error) *MockOutputParser_Parse_Call { + _c.Call.Return(output, n, err) + return _c +} + +func (_c *MockOutputParser_Parse_Call) RunAndReturn(run func(litdoc internal.Block, following []internal.Block) (internal.Output, int, error)) *MockOutputParser_Parse_Call { + _c.Call.Return(run) + return _c +} diff --git a/internal/bash/Runner_mock_test.go b/internal/bash/Runner_mock_test.go new file mode 100644 index 0000000..6535a40 --- /dev/null +++ b/internal/bash/Runner_mock_test.go @@ -0,0 +1,108 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package bash_test + +import ( + mock "github.com/stretchr/testify/mock" +) + +// NewMockRunner creates a new instance of MockRunner. 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 NewMockRunner(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRunner { + mock := &MockRunner{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockRunner is an autogenerated mock type for the Runner type +type MockRunner struct { + mock.Mock +} + +type MockRunner_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRunner) EXPECT() *MockRunner_Expecter { + return &MockRunner_Expecter{mock: &_m.Mock} +} + +// Run provides a mock function for the type MockRunner +func (_mock *MockRunner) Run(script string) (string, string, int, error) { + ret := _mock.Called(script) + + if len(ret) == 0 { + panic("no return value specified for Run") + } + + var r0 string + var r1 string + var r2 int + var r3 error + if returnFunc, ok := ret.Get(0).(func(string) (string, string, int, error)); ok { + return returnFunc(script) + } + if returnFunc, ok := ret.Get(0).(func(string) string); ok { + r0 = returnFunc(script) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(string) string); ok { + r1 = returnFunc(script) + } else { + r1 = ret.Get(1).(string) + } + if returnFunc, ok := ret.Get(2).(func(string) int); ok { + r2 = returnFunc(script) + } else { + r2 = ret.Get(2).(int) + } + if returnFunc, ok := ret.Get(3).(func(string) error); ok { + r3 = returnFunc(script) + } else { + r3 = ret.Error(3) + } + return r0, r1, r2, r3 +} + +// MockRunner_Run_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Run' +type MockRunner_Run_Call struct { + *mock.Call +} + +// Run is a helper method to define mock.On call +// - script string +func (_e *MockRunner_Expecter) Run(script interface{}) *MockRunner_Run_Call { + return &MockRunner_Run_Call{Call: _e.mock.On("Run", script)} +} + +func (_c *MockRunner_Run_Call) Run(run func(script string)) *MockRunner_Run_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockRunner_Run_Call) Return(stdout string, stderr string, exitCode int, err error) *MockRunner_Run_Call { + _c.Call.Return(stdout, stderr, exitCode, err) + return _c +} + +func (_c *MockRunner_Run_Call) RunAndReturn(run func(script string) (string, string, int, error)) *MockRunner_Run_Call { + _c.Call.Return(run) + return _c +} diff --git a/internal/bash/cell.go b/internal/bash/cell.go index 0292b1c..055be1d 100644 --- a/internal/bash/cell.go +++ b/internal/bash/cell.go @@ -2,6 +2,7 @@ package bash import ( "fmt" + "strings" "litdoc/internal" ) @@ -9,46 +10,64 @@ import ( type Cell struct { block internal.Block output internal.Output + runner internal.Runner } -func MakeCellFromRaw(content, indent string, output internal.Output) Cell { +func makeCellFromRaw( + content, indent string, + output internal.Output, + runner internal.Runner, +) Cell { return Cell{ block: internal.MakeBlockFromRaw(internal.BlockKindFencedCode, content, indent, false), output: output, + runner: runner, } } -func ParseCell( - block internal.Block, - following []internal.Block, -) ( - internal.Cell, - int, - error, -) { - return parseCellWith(block, following, internal.OutputFromBlocks) +type Parser struct { + runner internal.Runner + outputParser internal.OutputParser } -func parseCellWith( +func MakeParser() Parser { + return makeParserFromRaw(Runner{}, internal.OutputParserFunc(internal.OutputFromBlocks)) +} + +func makeParserFromRaw( + runner internal.Runner, + outputParser internal.OutputParser, +) Parser { + return Parser{runner: runner, outputParser: outputParser} +} + +func (p Parser) Parse( 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) + output, consumed, err := p.outputParser.Parse(block, following) if err != nil { return nil, 0, fmt.Errorf("parsing output: %w", err) } - return Cell{block: block, output: output}, consumed, nil + return Cell{block: block, output: output, runner: p.runner}, consumed, nil } func (c Cell) Execute() (internal.Cell, error) { + stdout, stderr, exitCode, err := c.runner.Run(internal.CodeBody(c.block.Content())) + if err != nil { + return nil, fmt.Errorf("running cell: %w", err) + } + if exitCode != 0 { + return nil, fmt.Errorf("exit status %d: %s", exitCode, strings.TrimSpace(stderr)) + } return Cell{ block: c.block, - output: internal.MakeOutput("output", c.block.Indent()), + runner: c.runner, + output: internal.MakeOutput(stdout, c.block.Indent()), }, nil } diff --git a/internal/bash/cell_private_test.go b/internal/bash/cell_private_test.go new file mode 100644 index 0000000..2505066 --- /dev/null +++ b/internal/bash/cell_private_test.go @@ -0,0 +1,38 @@ +package bash + +import ( + "testing" + + "litdoc/internal" + + "github.com/stretchr/testify/assert" +) + +func TestMakeCellFromRaw(t *testing.T) { + // given + content := "```bash\necho hello\n```\n" + indent := " " + output := internal.MakeOutput("hello", "") + runner := Runner{} + block := internal.MakeBlockFromRaw(internal.BlockKindFencedCode, content, indent, false) + + // when + c := makeCellFromRaw(content, indent, output, runner) + + // then + assert.Equal(t, block, c.block) + assert.Equal(t, output, c.output) + assert.Equal(t, runner, c.runner) +} + +func TestMakeParserFromRaw(t *testing.T) { + // given + runner := Runner{} + + // when + p := makeParserFromRaw(runner, internal.OutputParserFunc(internal.OutputFromBlocks)) + + // then + assert.Equal(t, runner, p.runner) + assert.NotNil(t, p.outputParser) +} diff --git a/internal/bash/cell_test.go b/internal/bash/cell_test.go index b7ad715..340ec8b 100644 --- a/internal/bash/cell_test.go +++ b/internal/bash/cell_test.go @@ -15,26 +15,7 @@ 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) { +func TestParser_Parse(t *testing.T) { block := internal.MakeBlockFromRaw(internal.BlockKindFencedCode, joinLines( "```bash", "echo hello", @@ -45,12 +26,14 @@ func TestParseCellWith(t *testing.T) { 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 - } + outputParser := NewMockOutputParser(t) + outputParser.EXPECT(). + Parse(block, []internal.Block(nil)). + Return(output, 3, nil) + parser := bash.NewParserWith(nil, outputParser) // when - cell, consumed, err := bash.ParseCellWith(block, nil, parseOutput) + cell, consumed, err := parser.Parse(block, nil) // then require.NoError(t, err) @@ -62,12 +45,14 @@ func TestParseCellWith(t *testing.T) { 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 - } + outputParser := NewMockOutputParser(t) + outputParser.EXPECT(). + Parse(block, []internal.Block(nil)). + Return(internal.Output{}, 0, assert.AnError) + parser := bash.NewParserWith(nil, outputParser) // when - _, _, err := bash.ParseCellWith(block, nil, parseOutput) + _, _, err := parser.Parse(block, nil) // then require.ErrorContains(t, err, "parsing output") @@ -75,7 +60,66 @@ func TestParseCellWith(t *testing.T) { }) } -func TestRender(t *testing.T) { +func TestCell_Execute(t *testing.T) { + fencedCode := joinLines( + "```bash", + "echo hello", + "```", + "", + ) + + t.Run("success", func(t *testing.T) { + // given + runner := NewMockRunner(t) + runner.EXPECT(). + Run("echo hello\n"). + Return("hello\n", "", 0, nil) + cell := bash.MakeCellFromRaw(fencedCode, "", internal.MakeOutput("", ""), runner) + + // when + gotCell, err := cell.Execute() + + // then + require.NoError(t, err) + rendered, err := gotCell.Render() + require.NoError(t, err) + assert.Equal(t, fencedCode+internal.MakeOutput("hello\n", "").Render(), rendered) + }) + + t.Run("non-zero exit code", func(t *testing.T) { + // given + runner := NewMockRunner(t) + runner.EXPECT(). + Run("echo hello\n"). + Return("", "bash: command not found\n", 127, nil) + cell := bash.MakeCellFromRaw(fencedCode, "", internal.MakeOutput("", ""), runner) + + // when + _, err := cell.Execute() + + // then + require.ErrorContains(t, err, "exit status 127") + require.ErrorContains(t, err, "bash: command not found") + }) + + t.Run("exec error", func(t *testing.T) { + // given + runner := NewMockRunner(t) + runner.EXPECT(). + Run("echo hello\n"). + Return("", "", 0, assert.AnError) + cell := bash.MakeCellFromRaw(fencedCode, "", internal.MakeOutput("", ""), runner) + + // when + _, err := cell.Execute() + + // then + require.ErrorContains(t, err, "running cell") + require.ErrorIs(t, err, assert.AnError) + }) +} + +func TestCell_Render(t *testing.T) { t.Run("without output", func(t *testing.T) { // given code := joinLines( @@ -84,7 +128,7 @@ func TestRender(t *testing.T) { "```", "", ) - cell := bash.MakeCellFromRaw(code, "", internal.MakeOutput("", "")) + cell := bash.MakeCellFromRaw(code, "", internal.MakeOutput("", ""), nil) // when gotContent, err := cell.Render() @@ -103,7 +147,7 @@ func TestRender(t *testing.T) { "", ) output := internal.MakeOutput("hello", "") - cell := bash.MakeCellFromRaw(fencedCode, "", output) + cell := bash.MakeCellFromRaw(fencedCode, "", output, nil) // when gotContent, err := cell.Render() @@ -113,23 +157,3 @@ func TestRender(t *testing.T) { 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 index 4d90090..e70aa1e 100644 --- a/internal/bash/export_test.go +++ b/internal/bash/export_test.go @@ -1,3 +1,4 @@ package bash -var ParseCellWith = parseCellWith +var MakeCellFromRaw = makeCellFromRaw +var NewParserWith = makeParserFromRaw diff --git a/internal/bash/runner.go b/internal/bash/runner.go new file mode 100644 index 0000000..b3791b3 --- /dev/null +++ b/internal/bash/runner.go @@ -0,0 +1,9 @@ +package bash + +import "litdoc/internal" + +type Runner struct{} + +func (r Runner) Run(script string) (string, string, int, error) { + return internal.RunCmd("bash", "-c", script) +} diff --git a/internal/cell.go b/internal/cell.go index 0d71913..d23ed1c 100644 --- a/internal/cell.go +++ b/internal/cell.go @@ -14,28 +14,6 @@ type CellParser interface { Parse(block Block, following []Block) (Cell, int, error) } -type CellParserFunc func(block Block, following []Block) (Cell, int, error) - -func (f CellParserFunc) Parse(block Block, following []Block) (Cell, int, error) { - return f(block, following) -} - -type InfoString struct { - Lang string - Litdoc bool -} - -func InfoStringFromBlock(b Block) InfoString { - raw := b.headerLine() - if raw == "" { - return InfoString{} - } - parts := strings.SplitN(raw, " | ", 2) - lang := strings.TrimSpace(parts[0]) - litdoc := len(parts) > 1 && strings.HasPrefix(strings.TrimSpace(parts[1]), "litdoc") - return InfoString{Lang: lang, Litdoc: litdoc} -} - func Classify(blocks []Block, parsers map[string]CellParser) ([]Cell, error) { static, ok := parsers["static"] if !ok { diff --git a/internal/cell_test.go b/internal/cell_test.go index b1feced..70612ff 100644 --- a/internal/cell_test.go +++ b/internal/cell_test.go @@ -12,153 +12,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestInfoStringFromBlock(t *testing.T) { - tests := []struct { - name string - block internal.Block - want internal.InfoString - }{ - { - name: "text block", - block: text("", "hello", false), - want: internal.InfoString{}, - }, - { - name: "fenced code/backtick/without-litdoc", - block: code( - "", - joinLines( - "```bash", - "echo hello", - "```", - "", - ), - false, - ), - want: internal.InfoString{Lang: "bash"}, - }, - { - name: "fenced code/backtick/with-litdoc", - block: code( - "", - joinLines( - "```bash | litdoc", - "echo hello", - "```", - "", - ), - false, - ), - want: internal.InfoString{Lang: "bash", Litdoc: true}, - }, - { - name: "fenced code/tilde/with-litdoc", - block: code( - "", - joinLines( - "~~~sh | litdoc", - "echo hello", - "~~~", - "", - ), - false, - ), - want: internal.InfoString{Lang: "sh", Litdoc: true}, - }, - { - name: "fenced code/no-info-string", - block: code( - "", - joinLines( - "```", - "echo hello", - "```", - "", - ), - false, - ), - want: internal.InfoString{}, - }, - { - name: "fenced code/trims-language", - block: code( - "", - joinLines( - "``` bash | litdoc", - "echo hello", - "```", - "", - ), - false, - ), - want: internal.InfoString{Lang: "bash", Litdoc: true}, - }, - { - name: "fenced code/litdoc-prefix", - block: code( - "", - joinLines( - "```bash | litdoc-output", - "echo hello", - "```", - "", - ), - false, - ), - want: internal.InfoString{Lang: "bash", Litdoc: true}, - }, - { - name: "html comment/without-litdoc", - block: cmnt( - "", - joinLines( - "", - "", - ), - false, - ), - want: internal.InfoString{Lang: "bash"}, - }, - { - name: "html comment/with-litdoc", - block: cmnt( - "", - joinLines( - "", - "", - ), - false, - ), - want: internal.InfoString{Lang: "bash", Litdoc: true}, - }, - { - name: "html comment/unsupported-litdoc-language", - block: cmnt( - "", - joinLines( - "", - "", - ), - false, - ), - want: internal.InfoString{Lang: "go", Litdoc: true}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := internal.InfoStringFromBlock(tt.block) - assert.Equal(t, tt.want, got) - }) - } -} - func TestClassify(t *testing.T) { type wantCell struct { kind string @@ -440,8 +293,8 @@ func TestClassify(t *testing.T) { } parsers := map[string]internal.CellParser{ - "static": internal.CellParserFunc(static.ParseCell), - "bash": internal.CellParserFunc(bash.ParseCell), + "static": static.NewParser(), + "bash": bash.MakeParser(), } for _, tt := range tests { @@ -492,7 +345,7 @@ func TestClassify(t *testing.T) { // when _, err := internal.Classify(blocks, map[string]internal.CellParser{ - "static": internal.CellParserFunc(static.ParseCell), + "static": static.NewParser(), "bash": failingParser, }) @@ -515,7 +368,7 @@ func TestClassify(t *testing.T) { // when _, err := internal.Classify(blocks, map[string]internal.CellParser{ "static": failingStatic, - "bash": internal.CellParserFunc(bash.ParseCell), + "bash": bash.MakeParser(), }) // then @@ -534,7 +387,7 @@ func TestClassify(t *testing.T) { // when _, err := internal.Classify(blocks, map[string]internal.CellParser{ "static": failingStatic, - "bash": internal.CellParserFunc(bash.ParseCell), + "bash": bash.MakeParser(), }) // then @@ -560,7 +413,9 @@ func TestExecute(t *testing.T) { // given result := NewMockCell(t) cell := NewMockCell(t) - cell.EXPECT().Execute().Return(result, nil) + cell.EXPECT(). + Execute(). + Return(result, nil) cells := []internal.Cell{cell} // when @@ -575,7 +430,9 @@ func TestExecute(t *testing.T) { t.Run("cell.Execute fails", func(t *testing.T) { // given cell := NewMockCell(t) - cell.EXPECT().Execute().Return(nil, assert.AnError) + cell.EXPECT(). + Execute(). + Return(nil, assert.AnError) cells := []internal.Cell{cell} // when @@ -591,9 +448,13 @@ func TestCompose(t *testing.T) { t.Run("happy path", func(t *testing.T) { // given cell1 := NewMockCell(t) - cell1.EXPECT().Render().Return("hello", nil) + cell1.EXPECT(). + Render(). + Return("hello", nil) cell2 := NewMockCell(t) - cell2.EXPECT().Render().Return(" world", nil) + cell2.EXPECT(). + Render(). + Return(" world", nil) cells := []internal.Cell{cell1, cell2} // when @@ -607,7 +468,9 @@ func TestCompose(t *testing.T) { t.Run("cell.Render fails", func(t *testing.T) { // given cell := NewMockCell(t) - cell.EXPECT().Render().Return("", assert.AnError) + cell.EXPECT(). + Render(). + Return("", assert.AnError) cells := []internal.Cell{cell} // when diff --git a/internal/file_test.go b/internal/file_test.go index 9b8bf4d..40fbca0 100644 --- a/internal/file_test.go +++ b/internal/file_test.go @@ -42,8 +42,8 @@ func TestProcessFile(t *testing.T) { // when got, err := internal.ProcessFile( f.Name(), map[string]internal.CellParser{ - "static": internal.CellParserFunc(static.ParseCell), - "bash": internal.CellParserFunc(bash.ParseCell), + "static": static.NewParser(), + "bash": bash.MakeParser(), }) // then diff --git a/internal/input.go b/internal/input.go new file mode 100644 index 0000000..c73392c --- /dev/null +++ b/internal/input.go @@ -0,0 +1,35 @@ +package internal + +import ( + "regexp" + "strings" +) + +// codeBodyRe strips the opening and closing delimiter lines from any block +// type. (?s) lets .* span newlines; greedy backtracking anchors the closing +// delimiter to the last non-empty line before end-of-string. +var codeBodyRe = regexp.MustCompile("(?s)^[^\n]*\n(.*)\n[^\n]+\n?$") + +type InfoString struct { + Lang string + Litdoc bool +} + +func InfoStringFromBlock(b Block) InfoString { + raw := b.headerLine() + if raw == "" { + return InfoString{} + } + parts := strings.SplitN(raw, " | ", 2) + lang := strings.TrimSpace(parts[0]) + litdoc := len(parts) > 1 && strings.HasPrefix(strings.TrimSpace(parts[1]), "litdoc") + return InfoString{Lang: lang, Litdoc: litdoc} +} + +func CodeBody(content string) string { + m := codeBodyRe.FindStringSubmatch(content) + if m == nil { + return "" + } + return m[1] + "\n" +} diff --git a/internal/input_test.go b/internal/input_test.go new file mode 100644 index 0000000..89907ac --- /dev/null +++ b/internal/input_test.go @@ -0,0 +1,302 @@ +package internal_test + +import ( + "testing" + + "litdoc/internal" + + "github.com/stretchr/testify/assert" +) + +func TestInfoStringFromBlock(t *testing.T) { + tests := []struct { + name string + block internal.Block + want internal.InfoString + }{ + { + name: "text block", + block: text("", "hello", false), + want: internal.InfoString{}, + }, + { + name: "fenced code/backtick/without-litdoc", + block: code( + "", + joinLines( + "```bash", + "echo hello", + "```", + "", + ), + false, + ), + want: internal.InfoString{Lang: "bash"}, + }, + { + name: "fenced code/backtick/with-litdoc", + block: code( + "", + joinLines( + "```bash | litdoc", + "echo hello", + "```", + "", + ), + false, + ), + want: internal.InfoString{Lang: "bash", Litdoc: true}, + }, + { + name: "fenced code/tilde/with-litdoc", + block: code( + "", + joinLines( + "~~~sh | litdoc", + "echo hello", + "~~~", + "", + ), + false, + ), + want: internal.InfoString{Lang: "sh", Litdoc: true}, + }, + { + name: "fenced code/no-info-string", + block: code( + "", + joinLines( + "```", + "echo hello", + "```", + "", + ), + false, + ), + want: internal.InfoString{}, + }, + { + name: "fenced code/trims-language", + block: code( + "", + joinLines( + "``` bash | litdoc", + "echo hello", + "```", + "", + ), + false, + ), + want: internal.InfoString{Lang: "bash", Litdoc: true}, + }, + { + name: "fenced code/litdoc-prefix", + block: code( + "", + joinLines( + "```bash | litdoc-output", + "echo hello", + "```", + "", + ), + false, + ), + want: internal.InfoString{Lang: "bash", Litdoc: true}, + }, + { + name: "html comment/without-litdoc", + block: cmnt( + "", + joinLines( + "", + "", + ), + false, + ), + want: internal.InfoString{Lang: "bash"}, + }, + { + name: "html comment/with-litdoc", + block: cmnt( + "", + joinLines( + "", + "", + ), + false, + ), + want: internal.InfoString{Lang: "bash", Litdoc: true}, + }, + { + name: "html comment/unsupported-litdoc-language", + block: cmnt( + "", + joinLines( + "", + "", + ), + false, + ), + want: internal.InfoString{Lang: "go", Litdoc: true}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, internal.InfoStringFromBlock(tt.block)) + }) + } +} + +func TestCodeBody(t *testing.T) { + tests := []struct { + name string + content string + want string + }{ + { + name: "single line", + content: joinLines( + "```bash", + "echo hello", + "```", + "", + ), + want: "echo hello\n", + }, + { + name: "multi-line body", + content: joinLines( + "```bash", + "line1", + "line2", + "line3", + "```", + "", + ), + want: "line1\nline2\nline3\n", + }, + { + name: "no info string", + content: joinLines( + "```", + "echo hello", + "```", + "", + ), + want: "echo hello\n", + }, + { + name: "longer fence", + content: joinLines( + "````bash", + "echo hello", + "````", + "", + ), + want: "echo hello\n", + }, + { + name: "no trailing newline on content", + content: joinLines( + "```bash", + "echo hello", + "```", + ), + want: "echo hello\n", + }, + { + name: "body line containing backticks", + content: joinLines( + "```bash", + "echo 'hello ```'", + "```", + "", + ), + want: "echo 'hello ```'\n", + }, + { + name: "empty body", + content: joinLines( + "```bash", + "```", + "", + ), + want: "", + }, + { + name: "missing closing fence", + content: joinLines( + "```bash", + "echo hello", + "", + ), + want: "", + }, + { + name: "not a fenced block", + content: "echo hello", + want: "", + }, + { + name: "tilde fence", + content: joinLines( + "~~~bash", + "echo hello", + "~~~", + "", + ), + want: "echo hello\n", + }, + { + name: "tilde fence with info string", + content: joinLines( + "~~~bash | litdoc", + "echo hello", + "~~~", + "", + ), + want: "echo hello\n", + }, + { + name: "html comment", + content: joinLines( + "", + "", + ), + want: "echo hello\n", + }, + { + name: "html comment multi-line", + content: joinLines( + "", + "", + ), + want: "line1\nline2\n", + }, + { + name: "html comment no trailing newline", + content: joinLines( + "", + ), + want: "echo hello\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, internal.CodeBody(tt.content)) + }) + } +} diff --git a/internal/output.go b/internal/output.go index 967a2bc..fe50a8c 100644 --- a/internal/output.go +++ b/internal/output.go @@ -10,6 +10,16 @@ const ( OutputEndMarker = "" ) +type OutputParser interface { + Parse(litdoc Block, following []Block) (Output, int, error) +} + +type OutputParserFunc func(litdoc Block, following []Block) (Output, int, error) + +func (f OutputParserFunc) Parse(litdoc Block, following []Block) (Output, int, error) { + return f(litdoc, following) +} + type Output struct { block Block } diff --git a/internal/runner.go b/internal/runner.go new file mode 100644 index 0000000..a9a5441 --- /dev/null +++ b/internal/runner.go @@ -0,0 +1,34 @@ +package internal + +import ( + "bytes" + "errors" + "fmt" + "os/exec" +) + +type Runner interface { + Run(script string) (stdout, stderr string, exitCode int, err error) +} + +func RunCmd(name string, args ...string) (string, string, int, error) { + var outBuf, errBuf bytes.Buffer + var exitCode int + + cmd := exec.Command(name, args...) + cmd.Stdout = &outBuf + cmd.Stderr = &errBuf + + err := cmd.Run() + + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + exitCode = exitErr.ExitCode() + } else { + return "", "", 0, fmt.Errorf("running command: %w", err) + } + } + + return outBuf.String(), errBuf.String(), exitCode, nil +} diff --git a/internal/static/cell.go b/internal/static/cell.go index c374e05..83eac9a 100644 --- a/internal/static/cell.go +++ b/internal/static/cell.go @@ -6,11 +6,17 @@ type Cell struct { block internal.Block } -func MakeCellFromRaw(content string) Cell { +func makeCellFromRaw(content string) Cell { return Cell{block: internal.MakeBlockFromRaw(internal.BlockKindText, content, "", false)} } -func ParseCell( +type Parser struct{} + +func NewParser() Parser { + return Parser{} +} + +func (p Parser) Parse( block internal.Block, _ []internal.Block, ) ( diff --git a/internal/static/cell_private_test.go b/internal/static/cell_private_test.go new file mode 100644 index 0000000..ff29c1b --- /dev/null +++ b/internal/static/cell_private_test.go @@ -0,0 +1,21 @@ +package static + +import ( + "testing" + + "litdoc/internal" + + "github.com/stretchr/testify/assert" +) + +func TestMakeCellFromRaw(t *testing.T) { + // given + content := "hello" + block := internal.MakeBlockFromRaw(internal.BlockKindText, content, "", false) + + // when + c := makeCellFromRaw(content) + + // then + assert.Equal(t, block, c.block) +} diff --git a/internal/static/cell_test.go b/internal/static/cell_test.go index e594e04..92d422f 100644 --- a/internal/static/cell_test.go +++ b/internal/static/cell_test.go @@ -10,24 +10,12 @@ import ( "github.com/stretchr/testify/require" ) -func TestMakeCellFromRaw(t *testing.T) { - // given - cell := static.MakeCellFromRaw("hello") - - // when - got, err := cell.Render() - - // then - require.NoError(t, err) - assert.Equal(t, "hello", got) -} - -func TestParseCell(t *testing.T) { +func TestParser_Parse(t *testing.T) { // given block := internal.MakeBlockFromRaw(internal.BlockKindText, "hello", "", false) // when - cell, consumed, err := static.ParseCell(block, nil) + cell, consumed, err := static.NewParser().Parse(block, nil) // then require.NoError(t, err) @@ -37,7 +25,7 @@ func TestParseCell(t *testing.T) { assert.Equal(t, block.Render(), rendered) } -func TestRender(t *testing.T) { +func TestCell_Render(t *testing.T) { // given cell := static.MakeCellFromRaw("hello") @@ -49,7 +37,7 @@ func TestRender(t *testing.T) { assert.Equal(t, "hello", got) } -func TestExecute(t *testing.T) { +func TestCell_Execute(t *testing.T) { // given cell := static.MakeCellFromRaw("hello") diff --git a/internal/static/export_test.go b/internal/static/export_test.go new file mode 100644 index 0000000..8d59a41 --- /dev/null +++ b/internal/static/export_test.go @@ -0,0 +1,3 @@ +package static + +var MakeCellFromRaw = makeCellFromRaw diff --git a/internal/testdata/output.md b/internal/testdata/output.md index 5e08ea5..83bbaae 100644 --- a/internal/testdata/output.md +++ b/internal/testdata/output.md @@ -33,7 +33,7 @@ echo "hello, world" ``` -output +hello, world - HTML comment @@ -43,7 +43,7 @@ echo "something to run" --> -output +something to run - Fenced code block with previously generated output @@ -53,7 +53,7 @@ echo "hello, world" ``` -output +hello, world - Indented code block @@ -63,7 +63,7 @@ output ``` - output + hello, world - Indented code block with previously generated output @@ -73,7 +73,7 @@ output ``` - output + hello, world > Block quoted fenced code block with previously generated output @@ -83,7 +83,7 @@ output > ``` > > -> output +> hello, world > ### Fenced code block indentation cases @@ -95,7 +95,7 @@ Fenced code block indented one space: ``` - output + hello, world Fenced code block indented three spaces: @@ -105,7 +105,7 @@ Fenced code block indented three spaces: ``` - output + hello, world > Block quoted fenced code block @@ -115,7 +115,7 @@ Fenced code block indented three spaces: > ``` > > -> output +> hello, world > > Nested block quoted fenced code block @@ -125,7 +125,7 @@ Fenced code block indented three spaces: > > ``` > > > > -> > output +> > hello, world > > Fenced code block in an unordered list: @@ -135,7 +135,7 @@ Fenced code block in an unordered list: ``` - output + hello, world Fenced code block in a nested unordered list: @@ -145,7 +145,7 @@ Fenced code block in a nested unordered list: ``` - output + hello, world Fenced code block in a plus list with a tilde fence: @@ -155,7 +155,7 @@ Fenced code block in a plus list with a tilde fence: ~~~ - output + hello, world Fenced code block in an ordered list: @@ -165,7 +165,7 @@ Fenced code block in an ordered list: ``` - output + hello, world Fenced code block in a nested ordered list: @@ -175,7 +175,7 @@ Fenced code block in a nested ordered list: ``` - output + hello, world Fenced code block in a list blockquote: @@ -185,7 +185,7 @@ Fenced code block in a list blockquote: > ``` > > - > output + > hello, world > > Fenced code block in a blockquote nested list: @@ -195,7 +195,7 @@ Fenced code block in a list blockquote: > ``` > > -> output +> hello, world > > Fenced code block in a blockquote ordered list: @@ -205,7 +205,7 @@ Fenced code block in a list blockquote: > ``` > > -> output +> hello, world > ### HTML comment indentation cases @@ -217,7 +217,7 @@ Fenced code block in a list blockquote: > --> > > -> output +> hello, world > > Nested block quoted HTML comment @@ -227,7 +227,7 @@ Fenced code block in a list blockquote: > > --> > > > > -> > output +> > hello, world > > HTML comment in an unordered list: @@ -237,7 +237,7 @@ HTML comment in an unordered list: --> - output + hello, world HTML comment in a nested unordered list: @@ -247,7 +247,7 @@ HTML comment in a nested unordered list: --> - output + hello, world HTML comment in a star list: @@ -257,7 +257,7 @@ HTML comment in a star list: --> - output + hello, world HTML comment in an ordered list: @@ -267,7 +267,7 @@ HTML comment in an ordered list: --> - output + hello, world > HTML comment in a blockquote list: @@ -277,7 +277,7 @@ HTML comment in an ordered list: > --> > > -> output +> hello, world > > HTML comment in a nested blockquote list: @@ -287,7 +287,7 @@ HTML comment in an ordered list: > > --> > > > > -> > output +> > hello, world > > HTML comment in an ordered-list blockquote: @@ -297,5 +297,5 @@ HTML comment in an ordered-list blockquote: > --> > > - > output + > hello, world >