diff --git a/Project.toml b/Project.toml index 7a6df28b..fcd8841f 100644 --- a/Project.toml +++ b/Project.toml @@ -39,9 +39,10 @@ julia = "1.10" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Dates", "DelimitedFiles", "JET", "OffsetArrays", "StableRNGs", "Test"] +test = ["Dates", "DelimitedFiles", "JET", "Markdown", "OffsetArrays", "StableRNGs", "Test"] diff --git a/src/statmodels.jl b/src/statmodels.jl index b487a30e..28c2a477 100644 --- a/src/statmodels.jl +++ b/src/statmodels.jl @@ -147,8 +147,18 @@ function show(io::IO, ::MIME"text/plain", ct::CoefTable) nothing end +# An unescaped `|` ends the cell it appears in, so a row containing one no longer +# has the same number of cells as the alignment row and the table stops parsing as +# a table at all. Callers hit this with the R-style `Pr(>|z|)` p-value header, and +# with any string cell or row name that happens to contain a pipe. Same rule the +# Markdown stdlib applies in `plain(::IO, ::Markdown.Table)`; applying it before +# the column widths are measured keeps them consistent with what is printed. +escape_markdown_pipes(s::AbstractString) = replace(s, '|' => "\\|") + function show(io::IO, ::MIME"text/markdown", ct::CoefTable) - cols = ct.cols; rownms = ct.rownms; colnms = ct.colnms; + cols = ct.cols + rownms = escape_markdown_pipes.(ct.rownms) + colnms = escape_markdown_pipes.(ct.colnms) nc = length(cols) nr = length(cols[1]) if length(rownms) == 0 @@ -157,7 +167,7 @@ function show(io::IO, ::MIME"text/markdown", ct::CoefTable) mat = [j == 1 ? NoQuote(rownms[i]) : j-1 == ct.pvalcol ? NoQuote(sprint(show, PValue(cols[j-1][i]))) : j-1 in ct.teststatcol ? TestStat(cols[j-1][i]) : - cols[j-1][i] isa AbstractString ? NoQuote(cols[j-1][i]) : cols[j-1][i] + cols[j-1][i] isa AbstractString ? NoQuote(escape_markdown_pipes(cols[j-1][i])) : cols[j-1][i] for i in 1:nr, j in 1:nc+1] # Code inspired by print_matrix in Base io = IOContext(io, :compact=>true, :limit=>false) diff --git a/test/statmodels.jl b/test/statmodels.jl index 70d864be..6f2da68c 100644 --- a/test/statmodels.jl +++ b/test/statmodels.jl @@ -1,6 +1,6 @@ using StatsBase using StatsBase: PValue, TestStat -using Test, Random, StatsAPI, LinearAlgebra +using Test, Random, StatsAPI, LinearAlgebra, Markdown v1 = [1.45666, -23.14, 1.56734e-13] v2 = ["Good", "Great", "Bad"] @@ -46,6 +46,24 @@ x3 1.56734e-13 Bad 2 0.00 <1e-15 | [2] | -23.14 | Great | 56 | 0.13 | 0.3467 | | [3] | 1.56734e-13 | Bad | 2 | 0.00 | <1e-15 |""" +# Pipes in a header, a row name or a string cell must be escaped: an unescaped one +# ends its cell, so the row presents more cells than the alignment row and the +# whole thing stops parsing as a table. Downstream packages hit this with the +# R-style `Pr(>|z|)` p-value header. +ct_pipes = CoefTable(Any[v1, ["a|b", "Great", "Bad"], v3, v4, v5], + ["Estimate", "Comments", "df", "t", "Pr(>|z|)"], + ["x|1", "x2", "x3"], 5, 4) +md_pipes = sprint(show, MIME"text/markdown"(), ct_pipes) +@test occursin(raw"Pr(>\|z\|)", md_pipes) +@test occursin(raw"x\|1", md_pipes) +@test occursin(raw"a\|b", md_pipes) +# Every row must present as many cells as the alignment row. +let counts = [length(collect(eachmatch(r"(?