diff --git a/docs/src/file-formats.md b/docs/src/file-formats.md index 6d0c62915..59cb6d80c 100644 --- a/docs/src/file-formats.md +++ b/docs/src/file-formats.md @@ -551,6 +551,24 @@ which renders like this when dropped into various web tools (e.g. github.comment As of Miller 4.3.0, markdown format is supported only for output, not input; as of Miller 6.11.0, markdown format is supported for input as well. +By default, markdown cells are not padded -- which renders identically in a Markdown viewer +but can be awkward to read or maintain as raw text. Use `--omd-aligned` to pad each column +to a uniform width so the raw markdown source lines up. This flag implies `--omd`, so you +do not need to pass `--omd` in addition: + +
+mlr --omd-aligned cat data/small
+
+
+| a   | b   | i   | x        | y        |
+| --- | --- | --- | ---      | ---      |
+| pan | pan | 1   | 0.346791 | 0.726802 |
+| eks | pan | 2   | 0.758679 | 0.522151 |
+| wye | wye | 3   | 0.204603 | 0.338318 |
+| eks | wye | 4   | 0.381399 | 0.134188 |
+| wye | pan | 5   | 0.573288 | 0.863624 |
+
+ ## XTAB: Vertical tabular This is perhaps most useful for looking a very wide and/or multi-column data which causes line-wraps on the screen (but see also diff --git a/docs/src/file-formats.md.in b/docs/src/file-formats.md.in index 068b04767..bbb748981 100644 --- a/docs/src/file-formats.md.in +++ b/docs/src/file-formats.md.in @@ -223,6 +223,15 @@ which renders like this when dropped into various web tools (e.g. github.comment As of Miller 4.3.0, markdown format is supported only for output, not input; as of Miller 6.11.0, markdown format is supported for input as well. +By default, markdown cells are not padded -- which renders identically in a Markdown viewer +but can be awkward to read or maintain as raw text. Use `--omd-aligned` to pad each column +to a uniform width so the raw markdown source lines up. This flag implies `--omd`, so you +do not need to pass `--omd` in addition: + +GENMD-RUN-COMMAND +mlr --omd-aligned cat data/small +GENMD-EOF + ## XTAB: Vertical tabular This is perhaps most useful for looking a very wide and/or multi-column data which causes line-wraps on the screen (but see also diff --git a/docs/src/manpage.md b/docs/src/manpage.md index 044756af7..8a4de9a56 100644 --- a/docs/src/manpage.md +++ b/docs/src/manpage.md @@ -178,6 +178,7 @@ This is simply a copy of what you should see on running `man mlr` at a command p mlr help format-conversion-keystroke-saver-flags mlr help json-only-flags mlr help legacy-flags + mlr help markdown-only-flags mlr help miscellaneous-flags mlr help output-colorization-flags mlr help pprint-only-flags @@ -560,6 +561,16 @@ This is simply a copy of what you should see on running `man mlr` at a command p --vflatsep Ignored as of version 6. This functionality is subsumed into JSON formatting. +1mMARKDOWN-ONLY FLAGS0m + These are flags which are applicable to markdown-tabular format. + + --omd-aligned or --omarkdown-aligned + For markdown-tabular output, left-justify cells and + pad each column to a uniform width, making the raw + markdown source easier to read and maintain. (The + rendered table is unaffected.) Implies --omd, so you + do not need to also pass --omd. + 1mMISCELLANEOUS FLAGS0m These are flags which don't fit into any other category. --fflush Force buffered output to be written after every @@ -3835,5 +3846,5 @@ This is simply a copy of what you should see on running `man mlr` at a command p MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite https://miller.readthedocs.io - 2026-04-19 4mMILLER24m(1) + 2026-05-16 4mMILLER24m(1) diff --git a/docs/src/manpage.txt b/docs/src/manpage.txt index 830feb0f0..c505c8ee4 100644 --- a/docs/src/manpage.txt +++ b/docs/src/manpage.txt @@ -157,6 +157,7 @@ mlr help format-conversion-keystroke-saver-flags mlr help json-only-flags mlr help legacy-flags + mlr help markdown-only-flags mlr help miscellaneous-flags mlr help output-colorization-flags mlr help pprint-only-flags @@ -539,6 +540,16 @@ --vflatsep Ignored as of version 6. This functionality is subsumed into JSON formatting. +1mMARKDOWN-ONLY FLAGS0m + These are flags which are applicable to markdown-tabular format. + + --omd-aligned or --omarkdown-aligned + For markdown-tabular output, left-justify cells and + pad each column to a uniform width, making the raw + markdown source easier to read and maintain. (The + rendered table is unaffected.) Implies --omd, so you + do not need to also pass --omd. + 1mMISCELLANEOUS FLAGS0m These are flags which don't fit into any other category. --fflush Force buffered output to be written after every @@ -3814,4 +3825,4 @@ MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite https://miller.readthedocs.io - 2026-04-19 4mMILLER24m(1) + 2026-05-16 4mMILLER24m(1) diff --git a/docs/src/reference-main-flag-list.md b/docs/src/reference-main-flag-list.md index 3447f14d1..73d44f64c 100644 --- a/docs/src/reference-main-flag-list.md +++ b/docs/src/reference-main-flag-list.md @@ -282,6 +282,15 @@ They are accepted as no-op flags in order to keep old scripts from breaking. * `--quote-original`: Ignored as of version 6. Types are inferred/retained through the processing flow now. * `--vflatsep`: Ignored as of version 6. This functionality is subsumed into JSON formatting. +## Markdown-only flags + +These are flags which are applicable to markdown-tabular format. + + +**Flags:** + +* `--omd-aligned or --omarkdown-aligned`: For markdown-tabular output, left-justify cells and pad each column to a uniform width, making the raw markdown source easier to read and maintain. (The rendered table is unaffected.) Implies --omd, so you do not need to also pass --omd. + ## Miscellaneous flags These are flags which don't fit into any other category. diff --git a/man/manpage.txt b/man/manpage.txt index 830feb0f0..c505c8ee4 100644 --- a/man/manpage.txt +++ b/man/manpage.txt @@ -157,6 +157,7 @@ mlr help format-conversion-keystroke-saver-flags mlr help json-only-flags mlr help legacy-flags + mlr help markdown-only-flags mlr help miscellaneous-flags mlr help output-colorization-flags mlr help pprint-only-flags @@ -539,6 +540,16 @@ --vflatsep Ignored as of version 6. This functionality is subsumed into JSON formatting. +1mMARKDOWN-ONLY FLAGS0m + These are flags which are applicable to markdown-tabular format. + + --omd-aligned or --omarkdown-aligned + For markdown-tabular output, left-justify cells and + pad each column to a uniform width, making the raw + markdown source easier to read and maintain. (The + rendered table is unaffected.) Implies --omd, so you + do not need to also pass --omd. + 1mMISCELLANEOUS FLAGS0m These are flags which don't fit into any other category. --fflush Force buffered output to be written after every @@ -3814,4 +3825,4 @@ MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite https://miller.readthedocs.io - 2026-04-19 4mMILLER24m(1) + 2026-05-16 4mMILLER24m(1) diff --git a/man/mlr.1 b/man/mlr.1 index e9c69ad29..7108bfde8 100644 --- a/man/mlr.1 +++ b/man/mlr.1 @@ -2,12 +2,12 @@ .\" Title: mlr .\" Author: [see the "AUTHOR" section] .\" Generator: ./mkman.rb -.\" Date: 2026-04-19 +.\" Date: 2026-05-16 .\" Manual: \ \& .\" Source: \ \& .\" Language: English .\" -.TH "MILLER" "1" "2026-04-19" "\ \&" "\ \&" +.TH "MILLER" "1" "2026-05-16" "\ \&" "\ \&" .\" ----------------------------------------------------------------- .\" * Portability definitions .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -194,6 +194,7 @@ Flags: mlr help format-conversion-keystroke-saver-flags mlr help json-only-flags mlr help legacy-flags + mlr help markdown-only-flags mlr help miscellaneous-flags mlr help output-colorization-flags mlr help pprint-only-flags @@ -662,6 +663,24 @@ They are accepted as no-op flags in order to keep old scripts from breaking. .fi .if n \{\ .RE +.SH "MARKDOWN-ONLY FLAGS" +.sp + +.if n \{\ +.RS 0 +.\} +.nf +These are flags which are applicable to markdown-tabular format. + +--omd-aligned or --omarkdown-aligned + For markdown-tabular output, left-justify cells and + pad each column to a uniform width, making the raw + markdown source easier to read and maintain. (The + rendered table is unaffected.) Implies --omd, so you + do not need to also pass --omd. +.fi +.if n \{\ +.RE .SH "MISCELLANEOUS FLAGS" .sp diff --git a/pkg/cli/option_parse.go b/pkg/cli/option_parse.go index dc1b27dda..0c994578f 100644 --- a/pkg/cli/option_parse.go +++ b/pkg/cli/option_parse.go @@ -117,6 +117,7 @@ var FLAG_TABLE = FlagTable{ &CSVTSVOnlyFlagSection, &JSONOnlyFlagSection, &PPRINTOnlyFlagSection, + &MarkdownOnlyFlagSection, &DKVPOnlyFlagSection, &CompressedDataFlagSection, &CommentsInDataFlagSection, @@ -582,6 +583,35 @@ var PPRINTOnlyFlagSection = FlagSection{ }, } +// MARKDOWN-ONLY FLAGS + +func MarkdownOnlyPrintInfo() { + fmt.Println("These are flags which are applicable to markdown-tabular format.") +} + +func init() { MarkdownOnlyFlagSection.Sort() } + +var MarkdownOnlyFlagSection = FlagSection{ + name: "Markdown-only flags", + infoPrinter: MarkdownOnlyPrintInfo, + flags: []Flag{ + + { + name: "--omd-aligned", + altNames: []string{"--omarkdown-aligned"}, + help: "For markdown-tabular output, left-justify cells and pad each " + + "column to a uniform width, making the raw markdown source easier " + + "to read and maintain. (The rendered table is unaffected.) Implies " + + "--omd, so you do not need to also pass --omd.", + parser: func(args []string, argc int, pargi *int, options *TOptions) { + options.WriterOptions.OutputFileFormat = "markdown" + options.WriterOptions.MarkdownAlignedOutput = true + *pargi += 1 + }, + }, + }, +} + // DKVP-ONLY FLAGS func DKVPOnlyPrintInfo() { diff --git a/pkg/cli/option_types.go b/pkg/cli/option_types.go index f8a023813..451fc142b 100644 --- a/pkg/cli/option_types.go +++ b/pkg/cli/option_types.go @@ -101,6 +101,7 @@ type TWriterOptions struct { BarredUseUnicode bool RightAlignedPPRINTOutput bool RightAlignedXTABOutput bool + MarkdownAlignedOutput bool // JSON output: --jlistwrap on, --jvstack on // JSON Lines output: --jlistwrap off, --jvstack off diff --git a/pkg/output/record_writer_markdown.go b/pkg/output/record_writer_markdown.go index 97a84dc4d..97738e239 100644 --- a/pkg/output/record_writer_markdown.go +++ b/pkg/output/record_writer_markdown.go @@ -3,6 +3,7 @@ package output import ( "bufio" "strings" + "unicode/utf8" "github.com/johnkerl/miller/v6/pkg/cli" "github.com/johnkerl/miller/v6/pkg/colorizer" @@ -13,8 +14,14 @@ import ( type RecordWriterMarkdown struct { writerOptions *cli.TWriterOptions + // Streaming-mode state (when MarkdownAlignedOutput is false) numHeaderLinesOutput int lastJoinedHeader string + + // Aligned-mode state (when MarkdownAlignedOutput is true) + batch []*mlrval.Mlrmap + batchJoinedHeader *string + numBatchesOutput int } func NewRecordWriterMarkdown(writerOptions *cli.TWriterOptions) (*RecordWriterMarkdown, error) { @@ -23,6 +30,10 @@ func NewRecordWriterMarkdown(writerOptions *cli.TWriterOptions) (*RecordWriterMa numHeaderLinesOutput: 0, lastJoinedHeader: "", + + batch: []*mlrval.Mlrmap{}, + batchJoinedHeader: nil, + numBatchesOutput: 0, }, nil } @@ -31,6 +42,17 @@ func (writer *RecordWriterMarkdown) Write( _ *types.Context, bufferedOutputStream *bufio.Writer, outputIsStdout bool, +) error { + if writer.writerOptions.MarkdownAlignedOutput { + return writer.writeAligned(outrec, bufferedOutputStream, outputIsStdout) + } + return writer.writeStreaming(outrec, bufferedOutputStream, outputIsStdout) +} + +func (writer *RecordWriterMarkdown) writeStreaming( + outrec *mlrval.Mlrmap, + bufferedOutputStream *bufio.Writer, + outputIsStdout bool, ) error { if outrec == nil { // end of record stream return nil @@ -76,3 +98,101 @@ func (writer *RecordWriterMarkdown) Write( return nil } + +// writeAligned accumulates records into a same-schema batch and flushes when +// the schema changes or the stream ends. We need the whole batch in hand +// before emitting any row, since column widths depend on every value. +func (writer *RecordWriterMarkdown) writeAligned( + outrec *mlrval.Mlrmap, + bufferedOutputStream *bufio.Writer, + outputIsStdout bool, +) error { + if outrec == nil { + if len(writer.batch) > 0 { + writer.flushBatch(bufferedOutputStream, outputIsStdout) + } + return nil + } + + joinedHeader := outrec.GetKeysJoined() + if writer.batchJoinedHeader == nil { + writer.batch = append(writer.batch, outrec) + writer.batchJoinedHeader = &joinedHeader + } else if *writer.batchJoinedHeader != joinedHeader { + writer.flushBatch(bufferedOutputStream, outputIsStdout) + writer.batch = []*mlrval.Mlrmap{outrec} + writer.batchJoinedHeader = &joinedHeader + } else { + writer.batch = append(writer.batch, outrec) + } + return nil +} + +func (writer *RecordWriterMarkdown) flushBatch( + bufferedOutputStream *bufio.Writer, + outputIsStdout bool, +) { + if writer.numBatchesOutput > 0 { + bufferedOutputStream.WriteString(writer.writerOptions.ORS) + } + + first := writer.batch[0] + + // Floor of 3 so "---" never overflows the column. + maxWidths := make(map[string]int) + for pe := first.Head; pe != nil; pe = pe.Next { + maxWidths[pe.Key] = max(utf8.RuneCountInString(pe.Key), 3) + } + for _, rec := range writer.batch { + for pe := rec.Head; pe != nil; pe = pe.Next { + value := strings.ReplaceAll(pe.Value.String(), "|", "\\|") + width := utf8.RuneCountInString(value) + if width > maxWidths[pe.Key] { + maxWidths[pe.Key] = width + } + } + } + + // Header + bufferedOutputStream.WriteString("|") + for pe := first.Head; pe != nil; pe = pe.Next { + bufferedOutputStream.WriteString(" ") + bufferedOutputStream.WriteString(colorizer.MaybeColorizeKey(pe.Key, outputIsStdout)) + writePadding(bufferedOutputStream, maxWidths[pe.Key]-utf8.RuneCountInString(pe.Key)) + bufferedOutputStream.WriteString(" |") + } + bufferedOutputStream.WriteString(writer.writerOptions.ORS) + + // Separator + bufferedOutputStream.WriteString("|") + for pe := first.Head; pe != nil; pe = pe.Next { + bufferedOutputStream.WriteString(" ---") + writePadding(bufferedOutputStream, maxWidths[pe.Key]-3) + bufferedOutputStream.WriteString(" |") + } + bufferedOutputStream.WriteString(writer.writerOptions.ORS) + + // Data + for _, rec := range writer.batch { + bufferedOutputStream.WriteString("|") + for pe := rec.Head; pe != nil; pe = pe.Next { + value := strings.ReplaceAll(pe.Value.String(), "|", "\\|") + bufferedOutputStream.WriteString(" ") + bufferedOutputStream.WriteString(colorizer.MaybeColorizeValue(value, outputIsStdout)) + writePadding(bufferedOutputStream, maxWidths[pe.Key]-utf8.RuneCountInString(value)) + bufferedOutputStream.WriteString(" |") + } + bufferedOutputStream.WriteString(writer.writerOptions.ORS) + } + + writer.batch = nil + writer.batchJoinedHeader = nil + writer.numBatchesOutput++ +} + +func writePadding(bufferedOutputStream *bufio.Writer, n int) { + if n <= 0 { + return + } + bufferedOutputStream.WriteString(strings.Repeat(" ", n)) +}