From f1739dc9dc974b8539dfca1d34131e40175590b1 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Fri, 28 Aug 2026 09:59:34 +0100 Subject: [PATCH] fix(docs): put-txt uses code block for fixed-width extracts INE/pdftotext -layout txt (e.g. loan-ine) opens reliably in OO as monospace; prose txt still uses hard line breaks. Co-authored-by: Cursor --- internal/docpipe/docpipe.go | 38 ++++++++++++++++++++++++++++---- internal/docpipe/docpipe_test.go | 19 ++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/internal/docpipe/docpipe.go b/internal/docpipe/docpipe.go index d65135f..44b7aa3 100644 --- a/internal/docpipe/docpipe.go +++ b/internal/docpipe/docpipe.go @@ -101,11 +101,14 @@ func (t Tools) TXTToDOCX(txtPath, docxPath string) error { return t.MDToDOCX(tmpMD, docxPath) } -// TxtToMarkdown converts plain text to Markdown that preserves line breaks in DOCX output. -// Single newlines become hard breaks; blank lines stay paragraph separators. +// TxtToMarkdown converts plain text to Markdown for DOCX output. +// Prose text: each line is a hard break. Fixed-width extracts (INE, pdftotext -layout): +// wrapped in a fenced code block (monospace, columns preserved). func TxtToMarkdown(content string) string { - content = strings.ReplaceAll(content, "\r\n", "\n") - content = strings.ReplaceAll(content, "\r", "\n") + content = normalizeTxtNewlines(content) + if isFixedWidthTxt(content) { + return "```\n" + content + "\n```\n" + } var b strings.Builder for _, line := range strings.Split(content, "\n") { if strings.TrimSpace(line) == "" { @@ -118,6 +121,33 @@ func TxtToMarkdown(content string) string { return b.String() } +func normalizeTxtNewlines(content string) string { + content = strings.ReplaceAll(content, "\r\n", "\n") + return strings.ReplaceAll(content, "\r", "\n") +} + +// isFixedWidthTxt detects pdftotext -layout style extracts (many indented/spaced columns). +func isFixedWidthTxt(content string) bool { + lines := strings.Split(content, "\n") + if len(lines) < 8 { + return false + } + indented, long := 0, 0 + for _, line := range lines { + if strings.TrimSpace(line) == "" { + continue + } + if len(line) >= 72 { + long++ + } + if len(line) > 0 && (line[0] == ' ' || line[0] == '\t') { + indented++ + } + } + n := len(lines) + return indented*100/n >= 20 || (long >= 5 && indented*100/n >= 10) +} + // MDToDOCX writes a DOCX next to or at outPath from a Markdown file. func (t Tools) MDToDOCX(mdPath, docxPath string) error { if Ext(mdPath) != ".md" && Ext(mdPath) != ".markdown" { diff --git a/internal/docpipe/docpipe_test.go b/internal/docpipe/docpipe_test.go index 1d9726c..f4d25a9 100644 --- a/internal/docpipe/docpipe_test.go +++ b/internal/docpipe/docpipe_test.go @@ -34,6 +34,25 @@ func TestNeedsOCR_Image(t *testing.T) { } } +func TestTxtToMarkdown_FixedWidthUsesCodeBlock(t *testing.T) { + var lines []string + for i := 0; i < 12; i++ { + lines = append(lines, " column layout line "+strings.Repeat("x", 40)) + } + in := strings.Join(lines, "\n") + md := TxtToMarkdown(in) + if !strings.HasPrefix(md, "```\n") || !strings.Contains(md, "```") { + t.Fatalf("expected code block: %q", md[:min(80, len(md))]) + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + func TestTxtToMarkdownPreservesLines(t *testing.T) { in := "line1\nline2\n\nline4" md := TxtToMarkdown(in)