From 7872ed58316164d325458019aa71fbc9ee4452d4 Mon Sep 17 00:00:00 2001 From: Pawel Blazejewski Date: Sun, 1 Mar 2026 10:04:45 +0100 Subject: [PATCH 1/2] concurrent md execution --- internal/markdown/process.go | 97 ++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 38 deletions(-) diff --git a/internal/markdown/process.go b/internal/markdown/process.go index 71d5f0e..a9695f6 100644 --- a/internal/markdown/process.go +++ b/internal/markdown/process.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "strings" + "sync" "github.com/pblazh/tabula/internal/csv" ) @@ -18,60 +19,80 @@ func Process( return err } - var result []chunk + result := make([][]*chunk, len(chunks)) + var wg sync.WaitGroup for i := range chunks { ch := chunks[i] - write := csv.Write - wrap := wrapCodeBlock + result[i] = make([]*chunk, 0) + scriptChunk := getScriptChunk(chunks, i) - if ch.kind == csvKind && config.Align { - write = csv.WriteAligned + if chunkNeedsProcessing(&ch, scriptChunk) { + wg.Add(1) + go func() { + defer wg.Done() + output, err := processChunk(config, &ch, scriptChunk) + if err != nil { + fmt.Println(err) + } + result[i] = output + }() + continue } + result[i] = append(result[i], &ch) + } - if ch.kind == tableKind { - write = WriteAligned - wrap = wrapTable - } + wg.Wait() - scriptChunk := getScriptChunk(chunks, i) - if chunkNeedsProcessing(&ch, scriptChunk) { - data, comments, err := execute(config, &ch, scriptChunk) + for _, chs := range result { + for _, ch := range chs { + _, err = fmt.Fprintf(writer, "%s\n", ch) if err != nil { - result = append(result, ch) - result = append( - result, - chunk{ - kind: messageKind, - text: []string{toMessage(err.Error())}, - }, - ) - continue + return ErrWriteMD(err) } + } + } - // create a formatter depending on if align was requested - var sb strings.Builder - err = write(&sb, data, comments) - if err != nil { - return ErrWriteCSV(err) - } + return nil +} + +func processChunk(config *Config, data, script *chunk) ([]*chunk, error) { + var result []*chunk + lines, comments, err := execute(config, data, script) + if err != nil { + result = append(result, data) + result = append( + result, + &chunk{ + kind: messageKind, + text: []string{toMessage(err.Error())}, + }, + ) + return result, nil + } - lines := wrap(sb.String()) + write := csv.Write + wrap := wrapCodeBlock - result = append(result, chunk{kind: csvKind, text: lines}) - continue - } - result = append(result, ch) + if data.kind == csvKind && config.Align { + write = csv.WriteAligned } - for _, ch := range result { - _, err = fmt.Fprintf(writer, "%s\n", ch) - if err != nil { - return ErrWriteMD(err) - } + if data.kind == tableKind { + write = WriteAligned + wrap = wrapTable + } + // create a formatter depending on if align was requested + var sb strings.Builder + err = write(&sb, lines, comments) + if err != nil { + return nil, ErrWriteCSV(err) } - return nil + output := wrap(sb.String()) + + result = append(result, &chunk{kind: csvKind, text: output}) + return result, nil } func wrapCodeBlock(code string) []string { From 547ab03ef823613bed75c3d0528c85909389bc82 Mon Sep 17 00:00:00 2001 From: Pawel Blazejewski Date: Tue, 31 Mar 2026 22:38:02 +0200 Subject: [PATCH 2/2] combine errors --- .github/workflows/tabula.vscode.yaml | 2 +- internal/markdown/process.go | 15 +++- internal/markdown/processor_test.go | 102 +++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tabula.vscode.yaml b/.github/workflows/tabula.vscode.yaml index 974c7cc..d451617 100644 --- a/.github/workflows/tabula.vscode.yaml +++ b/.github/workflows/tabula.vscode.yaml @@ -10,7 +10,7 @@ on: workflow_dispatch: env: - NODE_VERSION: 20.x + NODE_VERSION: 24.x jobs: tabula_vscode: diff --git a/internal/markdown/process.go b/internal/markdown/process.go index a9695f6..be65b2f 100644 --- a/internal/markdown/process.go +++ b/internal/markdown/process.go @@ -1,6 +1,7 @@ package markdown import ( + "errors" "fmt" "io" "strings" @@ -21,9 +22,10 @@ func Process( result := make([][]*chunk, len(chunks)) var wg sync.WaitGroup + var mu sync.Mutex + var errs []error - for i := range chunks { - ch := chunks[i] + for i, ch := range chunks { result[i] = make([]*chunk, 0) scriptChunk := getScriptChunk(chunks, i) @@ -33,7 +35,10 @@ func Process( defer wg.Done() output, err := processChunk(config, &ch, scriptChunk) if err != nil { - fmt.Println(err) + mu.Lock() + errs = append(errs, err) + mu.Unlock() + return } result[i] = output }() @@ -44,6 +49,10 @@ func Process( wg.Wait() + if err := errors.Join(errs...); err != nil { + return err + } + for _, chs := range result { for _, ch := range chs { _, err = fmt.Fprintf(writer, "%s\n", ch) diff --git a/internal/markdown/processor_test.go b/internal/markdown/processor_test.go index 2ec3411..20d7cab 100644 --- a/internal/markdown/processor_test.go +++ b/internal/markdown/processor_test.go @@ -2,6 +2,7 @@ package markdown import ( "strings" + "sync" "testing" ) @@ -335,6 +336,45 @@ func TestProcess(t *testing.T) { "", }, "\n"), }, + { + name: "multiple CSV with code concurrent", + input: strings.Join([]string{ + "```csv", + "one,two,three", + "1,2,3", + "```", + "```tabula", + "let A2 = 10;", + "```", + "", + "```csv", + "one,two,three", + "4,5,6", + "```", + "```tabula", + "let A2 = 20;", + "```", + "", + }, "\n"), + output: strings.Join([]string{ + "```csv", + "one,two,three", + "10,2,3", + "```", + "```tabula", + "let A2 = 10;", + "```", + "", + "```csv", + "one,two,three", + "20,5,6", + "```", + "```tabula", + "let A2 = 20;", + "```", + "", + }, "\n"), + }, } for _, tc := range cases { @@ -357,3 +397,65 @@ func TestProcess(t *testing.T) { }) } } + +// TestProcessConcurrentChunks verifies that multiple processable chunks in a +// single document are each executed with their own correct script and data. +// This guards against closure variable capture bugs where goroutines share +// loop variables and all end up processing the last chunk's data. +func TestProcessConcurrentChunks(t *testing.T) { + input := strings.Join([]string{ + "```csv", + "one,two,three", + "1,2,3", + "```", + "```tabula", + "let A2 = 10;", + "```", + "", + "```csv", + "one,two,three", + "4,5,6", + "```", + "```tabula", + "let A2 = 20;", + "```", + "", + }, "\n") + + want := strings.Join([]string{ + "```csv", + "one,two,three", + "10,2,3", + "```", + "```tabula", + "let A2 = 10;", + "```", + "", + "```csv", + "one,two,three", + "20,5,6", + "```", + "```tabula", + "let A2 = 20;", + "```", + "", + }, "\n") + + const iterations = 100 + var wg sync.WaitGroup + wg.Add(iterations) + for range iterations { + go func() { + defer wg.Done() + var writer strings.Builder + if err := Process(&Config{}, strings.NewReader(input), &writer); err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if got := writer.String(); got != want { + t.Errorf("concurrent output mismatch:\nwant: %q\n got: %q", want, got) + } + }() + } + wg.Wait() +}