diff --git a/NEWS.md b/NEWS.md index b2df7c977f..2f2a6af8e6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -65,3 +65,5 @@ * Documentation is now available also in *Dark* mode ([#2315](https://github.com/JuliaData/DataFrames.jl/pull/2315)) +* add rich display support for Markdown cell entries in HTML and LaTeX + ([#2346](https://github.com/JuliaData/DataFrames.jl/pull/2346)) diff --git a/Project.toml b/Project.toml index 4d8bc8321c..80cb062e3c 100644 --- a/Project.toml +++ b/Project.toml @@ -10,6 +10,7 @@ Future = "9fa8497b-333b-5362-9e8d-4d0656e87820" InvertedIndices = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" IteratorInterfaceExtensions = "82899510-4779-5014-852e-03e436cf321d" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Missings = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" PooledArrays = "2dfb63ee-cc39-5dd5-95bd-886bf059d720" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" diff --git a/src/DataFrames.jl b/src/DataFrames.jl index f8ed23da02..9d45d575bb 100644 --- a/src/DataFrames.jl +++ b/src/DataFrames.jl @@ -6,6 +6,7 @@ using Reexport, SortingAlgorithms, Compat, Unicode, PooledArrays using Base.Sort, Base.Order, Base.Iterators using TableTraits, IteratorInterfaceExtensions import LinearAlgebra: norm +using Markdown import DataAPI, DataAPI.All, diff --git a/src/abstractdataframe/io.jl b/src/abstractdataframe/io.jl index 8c999fd8d7..eda9a60200 100644 --- a/src/abstractdataframe/io.jl +++ b/src/abstractdataframe/io.jl @@ -142,6 +142,10 @@ function _show(io::IO, ::MIME"text/html", df::AbstractDataFrame; cell_val = df[row, column_name] if ismissing(cell_val) write(io, "missing") + elseif cell_val isa Markdown.MD + write(io, "") + show(io, "text/html", cell_val) + write(io, "") elseif cell_val isa SHOW_TABULAR_TYPES write(io, "") cell = sprint(ourshow, cell_val) @@ -302,6 +306,8 @@ function _show(io::IO, ::MIME"text/latex", df::AbstractDataFrame; cell = df[row,col] if ismissing(cell) print(io, "\\emph{missing}") + elseif cell isa Markdown.MD + print(io, strip(repr(MIME("text/latex"), cell))) elseif cell isa SHOW_TABULAR_TYPES print(io, "\\emph{") print(io, latex_escape(sprint(ourshow, cell, context=io))) @@ -410,17 +416,23 @@ function printtable(io::IO, quotestr = string(quotemark) for i in 1:n for j in 1:p - if ismissing(df[i, j]) + cell = df[i, j] + if ismissing(cell) print(io, missingstring) - elseif isnothing(df[i, j]) + elseif isnothing(cell) print(io, nothingstring) else - if ! (etypes[j] <: Real) + if cell isa Markdown.MD print(io, quotemark) - escapedprint(io, df[i, j], quotestr) + r = repr(cell) + escapedprint(io, chomp(r), quotestr) + print(io, quotemark) + elseif ! (etypes[j] <: Real) + print(io, quotemark) + escapedprint(io, cell, quotestr) print(io, quotemark) else - print(io, df[i, j]) + print(io, cell) end end if j < p diff --git a/src/abstractdataframe/show.jl b/src/abstractdataframe/show.jl index 85ba621c30..4bbc203943 100644 --- a/src/abstractdataframe/show.jl +++ b/src/abstractdataframe/show.jl @@ -54,6 +54,11 @@ ourshow(io::IO, x::Symbol) = ourshow(io, string(x)) ourshow(io::IO, x::Nothing; styled::Bool=false) = ourshow(io, "", styled=styled) ourshow(io::IO, x::SHOW_TABULAR_TYPES; styled::Bool=false) = ourshow(io, summary(x), styled=styled) +function ourshow(io::IO, x::Markdown.MD) + r = repr(x) + len = min(length(r, 1, something(findfirst(==('\n'), r), lastindex(r)+1)-1), 32) + return print(io, len < length(r) - 1 ? first(r, len)*'…' : first(r, len)) +end # AbstractChar: https://github.com/JuliaLang/julia/pull/34730 (1.5.0-DEV.261) # Irrational: https://github.com/JuliaLang/julia/pull/34741 (1.5.0-DEV.266) diff --git a/test/io.jl b/test/io.jl index 0e9ea5bb64..6df042e064 100644 --- a/test/io.jl +++ b/test/io.jl @@ -1,28 +1,30 @@ module TestIO -using Test, DataFrames, CategoricalArrays, Dates +using Test, DataFrames, CategoricalArrays, Dates, Markdown # Test LaTeX export @testset "LaTeX export" begin - df = DataFrame(A = 1:4, + df = DataFrame(A = Int64.( 1:4 ), B = ["\$10.0", "M&F", "A~B", "\\alpha"], C = ["A", "B", "C", "S"], D = [1.0, 2.0, missing, 3.0], E = CategoricalArray(["a", missing, "c", "d"]), - F = Vector{String}(undef, 4) + F = Vector{String}(undef, 4), + G = [ md"[DataFrames.jl](http://juliadata.github.io/DataFrames.jl)", md"###A", md"``\frac{A}{B}``", md"*A*b**A**"] ) str = """ - \\begin{tabular}{r|cccccc} - \t& A & B & C & D & E & F\\\\ + \\begin{tabular}{r|ccccccc} + \t& A & B & C & D & E & F & G\\\\ \t\\hline - \t& $(Int) & String & String & Float64? & Cat…? & String\\\\ + \t& Int64 & String & String & Float64? & Cat…? & String & MD…\\\\ \t\\hline - \t1 & 1 & \\\$10.0 & A & 1.0 & a & \\emph{\\#undef} \\\\ - \t2 & 2 & M\\&F & B & 2.0 & \\emph{missing} & \\emph{\\#undef} \\\\ - \t3 & 3 & A\\textasciitilde{}B & C & \\emph{missing} & c & \\emph{\\#undef} \\\\ - \t4 & 4 & \\textbackslash{}\\textbackslash{}alpha & S & 3.0 & d & \\emph{\\#undef} \\\\ + \t1 & 1 & \\\$10.0 & A & 1.0 & a & \\emph{\\#undef} & \\href{http://juliadata.github.io/DataFrames.jl}{DataFrames.jl} \\\\ + \t2 & 2 & M\\&F & B & 2.0 & \\emph{missing} & \\emph{\\#undef} & \\#\\#\\#A \\\\ + \t3 & 3 & A\\textasciitilde{}B & C & \\emph{missing} & c & \\emph{\\#undef} & \$\\frac{A}{B}\$ \\\\ + \t4 & 4 & \\textbackslash{}\\textbackslash{}alpha & S & 3.0 & d & \\emph{\\#undef} & \\emph{A}b\\textbf{A} \\\\ \\end{tabular} """ + @test repr(MIME("text/latex"), df) == str @test repr(MIME("text/latex"), eachcol(df)) == str @test repr(MIME("text/latex"), eachrow(df)) == str @@ -130,6 +132,27 @@ end @test_throws ArgumentError DataFrames._show(stdout, MIME("text/html"), DataFrame(ones(2,2)), rowid=10) + + df = DataFrame( + A=Int64[1,4,9,16], + B = [ + md"[DataFrames.jl](http://juliadata.github.io/DataFrames.jl)", + md"###A", + md"``\frac{A}{B}``", + md"*A*b**A**" ] + ) + + @test repr(MIME("text/html"), df) == + "" * + "

4 rows × 2 columns

" * + "" * + "
AB
Int64MD…
11
" * + "

DataFrames.jl" * + "

\n
24
" * + "

###A

\n
39
" * + "

$\\frac{A}{B}$

\n
416

AbA

"* + "\n
" + end # test limit attribute of IOContext is used @@ -180,6 +203,101 @@ end end end +@testset "Markdown as text/plain and as text/csv" begin + df = DataFrame( + A=Int64[1,4,9,16,25,36,49,64], + B = [ + md"[DataFrames.jl](http://juliadata.github.io/DataFrames.jl)", + md"``\frac{x^2}{x^2+y^2}``", + md"# Header", + md"This is *very*, **very**, very, very, very, very, very, very, very long line" , + md"", + Markdown.parse("∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0∫αγ∞1∫αγ∞2∫αγ∞3"), + Markdown.parse("∫αγ∞1∫αγ∞\n"* + " * 2∫αγ∞3∫αγ∞4\n"* + " * ∫αγ∞5∫αγ\n"* + " * ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0"), + Markdown.parse("∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0∫α\n"* + " * γ∞1∫α\n"* + " * γ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0"), + ] + ) + @test sprint(show, "text/plain", df) == """ + 8×2 DataFrame + │ Row │ A │ B │ + │ │ Int64 │ Markdown.MD │ + ├─────┼───────┼───────────────────────────────────┤ + │ 1 │ 1 │ [DataFrames.jl](http://juliadata… │ + │ 2 │ 4 │ \$\\frac{x^2}{x^2+y^2}\$ │ + │ 3 │ 9 │ # Header │ + │ 4 │ 16 │ This is *very*, **very**, very, … │ + │ 5 │ 25 │ │ + │ 6 │ 36 │ ∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫α… │ + │ 7 │ 49 │ ∫αγ∞1∫αγ∞… │ + │ 8 │ 64 │ ∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫α… │""" + + @test sprint(show, "text/csv", df) == + """ + \"A\",\"B\" + 1,\"[DataFrames.jl](http://juliadata.github.io/DataFrames.jl)\" + 4,\"\$\\\\frac{x^2}{x^2+y^2}\$\" + 9,\"# Header\" + 16,\"This is *very*, **very**, very, very, very, very, very, very, very long line\" + 25,\"\" + 36,\"∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0∫αγ∞1∫αγ∞2∫αγ∞3\" + 49,\"∫αγ∞1∫αγ∞\\n\\n * 2∫αγ∞3∫αγ∞4\\n * ∫αγ∞5∫αγ\\n * ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0\" + 64,\"∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0∫α\\n\\n * γ∞1∫α\\n * γ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0\" + """ +end + +@testset "Markdown as HTML" begin + df = DataFrame( + A=Int64[1,4,9,16,25,36,49,64], + B = [ + md"[DataFrames.jl](http://juliadata.github.io/DataFrames.jl)", + md"``\frac{x^2}{x^2+y^2}``", + md"# Header", + md"This is *very*, **very**, very, very, very, very, very, very, very long line" , + md"", + Markdown.parse("∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0" * + "∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0"), + Markdown.parse("∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ\n"* + " * ∞7∫αγ\n"* + " * ∞8∫αγ\n"* + " * ∞9∫αγ∞0∫α\nγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0"), + Markdown.parse("∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0∫α\n"* + " * γ∞1∫α\n"* + " * γ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0"), + ] + ) + @test sprint(show,"text/html",df) == + "" * + "" * + "" * + "" * + "" * "

8 rows × 2 columns

" * + "" * + "" * + "" * + "" * + "" * + "" * + "" * + "
AB
Int64MD…
11
" * + "

DataFrames.jl

\n
24

$\\frac{x^2}{x^2+y^2}$

\n
39

Header

\n
416
" * + "

This is very, very, very, very, very, very, very, very, very long line

\n" * + "
525
636
" * + "

∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0

\n" * + "
749
" * + "

∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ

\n
    \n
  • ∞7∫αγ

    \n
  • \n
  • ∞8∫αγ

    \n
  • \n
  • ∞9∫αγ∞0∫α

    \n
  • \n
\n

γ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0

\n" * + "
864
" * + "

∫αγ∞1∫αγ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0∫α

" * + "\n
    \n" * + "
  • γ∞1∫α

    \n
  • \n" * + "
  • γ∞2∫αγ∞3∫αγ∞4∫αγ∞5∫αγ∞6∫αγ∞7∫αγ∞8∫αγ∞9∫αγ∞0

    \n
  • \n" * + "
\n" * "
" +end + @testset "empty data frame and DataFrameRow" begin df = DataFrame(a = [1,2], b = [1.0, 2.0])