Repository: AlgebraicJulia/Catlab.jl (julia)

Issue:
Show C-set as HTML tables
Implement `show(::IO, ::MIME"text/html", ::ACSet)` to display a C-set as a collection of HTML tables, one for each object in C.

For inspiration, see how this is implemented in [DataFrames.j](https://github.com/JuliaData/DataFrames.jl/blob/f5b531f0ca32b1de9db37a9b77942c97e39bdb13/src/abstractdataframe/io.jl#L82).

Pull request description:
Uses [PrettyTables.jl](https://github.com/ronisbr/PrettyTables.jl) for both ASCII and HTML printing. Closes #262 and closes #263.

Patch:
diff --git a/Project.toml b/Project.toml
index e8a657b81..2b4ee5f76 100644
--- a/Project.toml
+++ b/Project.toml
@@ -20,6 +20,7 @@ MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078"
 MetaGraphs = "626554b9-1ddb-594c-aa3c-2596fe9399a5"
 Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a"
 Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
+PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
 Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
 Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
 Requires = "ae029012-a4dd-5104-9daa-d747884805df"
@@ -41,6 +42,7 @@ LightXML = "0.8, 0.9"
 MLStyle = "0.4"
 MetaGraphs = "^0.6"
 Parameters = "0.11, 0.12"
+PrettyTables = "0.9"
 Reexport = "0.2"
 Requires = "^1"
 StaticArrays = "0.12"
diff --git a/src/categorical_algebra/CSetDataStructures.jl b/src/categorical_algebra/CSetDataStructures.jl
index 3409c42ef..26f6b1d11 100644
--- a/src/categorical_algebra/CSetDataStructures.jl
+++ b/src/categorical_algebra/CSetDataStructures.jl
@@ -3,10 +3,13 @@
 module CSetDataStructures
 export AbstractACSet, ACSet, AbstractCSet, CSet, Schema, FreeSchema,
   AbstractACSetType, ACSetType, AbstractCSetType, CSetType,
-  nparts, has_part, subpart, has_subpart, incident, add_part!, add_parts!,
-  copy_parts!, set_subpart!, set_subparts!, disjoint_union
+  tables, nparts, has_part, subpart, has_subpart, incident,
+  add_part!, add_parts!, copy_parts!, set_subpart!, set_subparts!,
+  disjoint_union
 
 using Compat: isnothing, only
+
+using PrettyTables: pretty_table
 using StructArrays
 
 using ...Theories: Schema, FreeSchema, dom, codom,
@@ -193,8 +196,9 @@ function Base.copy(acs::T) where T <: ACSet
   T(map(copy, acs.tables), map(copy, acs.indices))
 end
 
-function Base.show(io::IO, acs::AbstractACSet{CD,AD,Ts}) where {CD,AD,Ts}
-  println(io, "ACSet(")
+function Base.show(io::IO, acs::T) where {CD,AD,Ts,T<:AbstractACSet{CD,AD,Ts}}
+  print(io, T <: AbstractCSet ? "CSet" : "ACSet")
+  println(io, "(")
   join(io, vcat(
     [ "  $ob = 1:$(nparts(acs,ob))" for ob in CD.ob ],
     [ "  $data = $(Ts.parameters[i])" for (i,data) in enumerate(AD.data) ],
@@ -206,21 +210,48 @@ function Base.show(io::IO, acs::AbstractACSet{CD,AD,Ts}) where {CD,AD,Ts}
   print(io, ")")
 end
 
-function Base.show(io::IO, ::MIME"text/plain", acs::ACSet)
-  println(io, "ACSet:")
-  for (name, table) in pairs(acs.tables)
-    print(io, "  $name table with $(length(table)) elements")
+function Base.show(io::IO, ::MIME"text/plain", acs::T) where {T<:AbstractACSet}
+  print(io, T <: AbstractCSet ? "CSet" : "ACSet")
+  print(io, " with elements ")
+  join(io, ["$ob = 1:$(nparts(acs,ob))" for ob in keys(tables(acs))], ", ")
+  println(io)
+  for (ob, table) in pairs(tables(acs))
     if !(eltype(table) <: EmptyTuple)
-      print(io, ":\n    ")
-      join(io, map(string, table), "\n    ")
+      # TODO: Set option `row_number_column_title=name` when next version of
+      # PrettyTables is released, instead of making new table.
+      table = StructArray((; ob => 1:nparts(acs,ob), fieldarrays(table)...))
+      pretty_table(io, table, nosubheader=true)
     end
-    println(io)
   end
 end
 
+function Base.show(io::IO, ::MIME"text/html", acs::T) where {T<:AbstractACSet}
+  println(io, "<div class=\"c-set\">")
+  print(io, "<span class=\"c-set-summary\">")
+  print(io, T <: AbstractCSet ? "CSet" : "ACSet")
+  print(io, " with elements ")
+  join(io, ["$ob = 1:$(nparts(acs,ob))" for ob in keys(tables(acs))], ", ")
+  println(io, "</span>")
+  for (ob, table) in pairs(tables(acs))
+    if !(eltype(table) <: EmptyTuple)
+      # TODO: Set option `row_number_column_title`. See above.
+      table = StructArray((; ob => 1:nparts(acs,ob), fieldarrays(table)...))
+      pretty_table(io, table, backend=:html, standalone=false, nosubheader=true)
+    end
+  end
+  println(io, "</div>")
+end
+
 # Imperative interface
 ######################
 
+""" Tables defining a C-set.
+
+A named tuple with a table for each part type. To ensure consistency, do not
+directly mutate these tables, especially when indexing is enabled!
+"""
+tables(acs::ACSet) = acs.tables
+
 """ Number of parts of given type in a C-set.
 """
 nparts(acs::ACSet, type) = length(acs.tables[type])
diff --git a/src/categorical_algebra/CSets.jl b/src/categorical_algebra/CSets.jl
index ca9205d5b..0a6cd4b04 100644
--- a/src/categorical_algebra/CSets.jl
+++ b/src/categorical_algebra/CSets.jl
@@ -109,7 +109,7 @@ force(α::ACSetTransformation) =
   end
 end
 
-finsets(X::ACSet) = map(table -> FinSet(length(table)), X.tables)
+finsets(X::ACSet) = map(table -> FinSet(length(table)), tables(X))
 
 # Limits and colimits
 #####################


Test patch:
diff --git a/test/categorical_algebra/CSetDataStructures.jl b/test/categorical_algebra/CSetDataStructures.jl
index 454777858..30c1f75fa 100644
--- a/test/categorical_algebra/CSetDataStructures.jl
+++ b/test/categorical_algebra/CSetDataStructures.jl
@@ -18,6 +18,7 @@ const DDS = CSetType(TheoryDDS, index=[:Φ])
 @test DDS <: CSet
 
 dds = DDS()
+@test keys(tables(dds)) == (:X,)
 @test keys(dds.indices) == (:Φ,)
 @test nparts(dds, :X) == 0
 @test add_part!(dds, :X) == 1
@@ -47,12 +48,18 @@ set_subpart!(dds, 1, :Φ, 1)
 
 # Pretty printing.
 s = sprint(show, dds)
+@test startswith(s, "CSet")
 @test occursin("X = 1:3", s)
 @test occursin("Φ : X → X = ", s)
 
 s = sprint(show, MIME"text/plain"(), dds)
-@test occursin("X table with 3 elements", s)
-@test occursin("(Φ = 1,)", s)
+@test startswith(s, "CSet")
+@test occursin("X = 1:3", s)
+
+s = sprint(show, MIME"text/html"(), dds)
+@test startswith(s, "<div class=\"c-set\">")
+@test occursin("<table>", s)
+@test endswith(rstrip(s), "</div>")
 
 # Error handling.
 @test_throws AssertionError add_part!(dds, :X, Φ=5)
@@ -109,11 +116,12 @@ du = disjoint_union(d, d2)
 
 # Pretty printing of data attributes.
 s = sprint(show, d)
+@test startswith(s, "ACSet")
 @test occursin("R = Int64", s)
 @test occursin("height : X → R = ", s)
 
 s = sprint(show, MIME"text/plain"(), d)
-@test occursin("(parent = 4, height = 0)", s)
+@test startswith(s, "ACSet")
 
 # Allow type inheritance for data attributes.
 d = Dendrogram{Number}()