Add --omd-aligned flag for column-padded markdown output (#2057)

Adds a new Markdown-only flag --omd-aligned (alias --omarkdown-aligned)
that left-justifies cells and pads each column to a uniform width.
The rendered table is unaffected; the goal is readability of the raw
markdown source. The flag implies --omd, so users do not need to pass
both.

Implementation batches records by schema (like pprint), computes max
column widths, then emits header / separator / data rows with bars
vertically aligned. Default markdown writer behavior is unchanged.
This commit is contained in:
John Kerl 2026-05-16 11:41:01 -04:00 committed by GitHub
parent 1b69ee1588
commit e0cf596853
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 244 additions and 5 deletions

View file

@ -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:
<pre class="pre-highlight-in-pair">
<b>mlr --omd-aligned cat data/small</b>
</pre>
<pre class="pre-non-highlight-in-pair">
| 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 |
</pre>
## 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

View file

@ -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

View file

@ -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)
</pre>

View file

@ -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)

View file

@ -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.

View file

@ -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)

View file

@ -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

View file

@ -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() {

View file

@ -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

View file

@ -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))
}