Skip to content

Commit

Permalink
Initial public release of tabular
Browse files Browse the repository at this point in the history
A 2-D grid model for holding "cells" geared for creating tables, for
various output models.  Current renderers for HTML, CSV and for
pretty-printed UTF-8 tables for fixed-character-cell displays (such as
the Unix visual-TTY terminal model).

Brief coverage in README.md, details of structure in Overview.md, there
are godocs, some tests (enough to catch major issues but not great) and
things work.  It's feature complete for a v1, this is not a barebones
MVP.  There's more that could be done for a v2, but want more real-world
feedback first.

Bumped API to 1.0.

There will be a new commit in a bit, setting up automated CI and adding
badges into the README.

Squashed history; original dev repo started 2016-10-03 and was worked on
intermittently between then and now.
  • Loading branch information
philpennock committed Nov 30, 2016
0 parents commit 6d68737
Show file tree
Hide file tree
Showing 38 changed files with 3,586 additions and 0 deletions.
43 changes: 43 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
language: go

# Be explicit about not needing sudo, so that Travis will use container-based
# infrastructure for our jobs, always, fewer heuristics.
sudo: false

#env:
# global:
# - secure: ""
# # Need: COVERALLS_TOKEN

matrix:
allow_failures:
- go: tip
fast_finish: true
include:
- go: 1.7.3
env: UPLOAD_COVERAGE=true
- go: 1.6
- go: tip

branches:
except:
- /^(?:exp|wip)(?:[/_-].*)?$/

install:
- go get -t -v -u ./...
- test "${UPLOAD_COVERAGE:-false}" != "true" || go get github.com/mattn/goveralls

script:
- go vet ./...
- go test -v ./...
- test "${UPLOAD_COVERAGE:-false}" != "true" || ./CoverTest.sh

# after_script:
# - test "${UPLOAD_COVERAGE:-false}" != "true" || goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN

#notifications:
# slack:
# on_success: always
# secure: #...

# vim: set sw=2 et :
12 changes: 12 additions & 0 deletions .version
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh
dirname="$(dirname "$0")"
if [ "_$dirname" != "_" ]; then
cd "${dirname:?}"
fi
branch="$(git symbolic-ref --short HEAD)"
if [ ".$branch" = ".master" ]; then
branch=""
else
branch=",$branch"
fi
printf "%s%s\n" "$(git describe --always --dirty --tags)" "$branch"
33 changes: 33 additions & 0 deletions CoverTest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/sh
#
# Relies upon: <https://github.com/wadey/gocovmerge>
#
# Based upon mmindenhall's solution in <https://github.com/golang/go/issues/6909>
#

TOP="github.com/PennockTech/tabular"

progname="$(basename "$0")"
trace() { printf >&2 "%s: %s\n" "$progname" "$*" ; }

trace "removing old c*.out files"
find . -name c\*.out -execdir rm -v {} \;

trace "generating new c.partial.out files"
for D in $(find . -name .git -prune -o -type d -print)
do
if [ $D = "." ]; then
go test -covermode=count -coverprofile=c.partial.out -coverpkg ./... .
continue
fi
( cd $D && \
go test -covermode=count -coverprofile=c.partial.out -coverpkg "$TOP,./..." .
)
done

trace "combining coverage files -> coverage.out"
gocovmerge $(find . -name .git -prune -o -name c.partial.out -print) > coverage.out

trace "suggestions:"
echo " go tool cover -func=coverage.out | less"
echo " go tool cover -html=coverage.out"
21 changes: 21 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright © 2016 Phil Pennock

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
176 changes: 176 additions & 0 deletions Overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
Tabular Overview
================

At the core, tabular knows nothing about rendering. The base-level library,
in the repo's top-level, can be imported and used to create tables and add
content. Rendering requires using another layer to convert the output for
display.

A `Table` is an interface. There is one core public type which implements the
interface. This allows the core type to be embedded in the wrapper/display
objects and for those to satisfy the table interface, thus being tables
themselves. Rows and cells are not interfaces. The core public type for a
table is, imaginatively, `*ATable`. Callers are advised to use the `Table`
interface, if they need to care about a non-inferred type.

A table consists of rows of cells and some metadata. The metadata includes
virtual columns, allowing for addressing by column too. Columns are
identified by the header name. There is only one (or zero) header row per
table.

Errors in adding data are usually not reported immediately, to let data stream
in. Instead, errors accumulate in an error holder. Rows hold errors, but
once a row is part of a table, its errors become the tables' errors (and the
error container is diverted to be the table's). An error container can be
interrogated for its list of current errors.

The errors are either a list of non-nil errors, or nil. An empty list should
never be returned. If a nil is returned in the list of errors then that is a
bug in tabular.

A row either contains cells or is a "special" row. The only type of special
row is a "separator" row. The tabular layer itself doesn't know what a
separator row is, beyond that it exists and a row can be one. The
`AddSeparator()` table method adds one, the `IsSeparator()` row method asks a
row if it is one. The `Cells()` method, which returns an array of cells, can
return nil if and only if the row is special (ie, at present, a separator). A
real row is always a splice of cells, even if that splice is empty.

A `Cell` contains "an object". That object can be a string, something which
satisfies `Stringer` or `GoStringer`, a rune, or another `Cell`. Cells can
contain cells and this is intended to allow for dynamic update, based upon
evaluation.

If a `Cell` contains an object then various rendering layers may make use of
other interfaces satisfied by that object to determine how to display it;
loosely, think of "width" and "height", but this will be covered in more
detail below.

Cells, Rows, Columns and Tables can have "properties" set upon them.
Properties are namespaced objects, very similar to Golang's net contexts.
Clients of the tabular package are free to decorate items with whatever
properties they want.

The tabular package supports automatically updating properties at "addition"
time and at "render" time. This is done by setting callbacks. Callbacks can
be on a table or a row. Within the table, they can be registered for use on a
table or a column, for when a row is added, or when a cell is added.

The cell's location in the grid is not a property, but is available via a
method call upon the cell.

The callbacks and properties should not be exposed to end-users.

All child objects have links back to their containers. This is used, eg, to
be able to get column information for a given cell. This does mean that there
are ownership loops.


Sub-package Commonalities
-------------------------

In all cases:

* There is a `Wrap()` function which takes any `tabular.Table` and returns
a wrapper object for this sub-package.
* There is a `New()` function which generates an empty table for this
sub-package.
* The wrapper objects have `Render()` and `RenderTo()` methods, and the
packages have top-level functions which create a wrapper object, with
default options, and calls the object methods.
* The `Render()` method will return a string of the rendered text, together
with an error.
* The `RenderTo(io.Writer)` method uses a stream-based approach and only
returns an error.
* The wrapper object's type is named with a `FooTable` naming style, accepting
that this causes some stuttering. This is acceptable because most callers
should never need to specify the type, but instead be using the
`tabular.Table` interface if they care at all beyond letting the type be
inferred. With so many variants, I went for clarity over smoothness in
reading out the fully-qualified type name.

Because the sub-package `New()` returns an object which satisfies the
`tabular.Table` interface, it should be capable of being populated like any
other, and most callers with simple use-cases should be able to only import
the sub-package, not `tabular` itself.

It is possible to use table properties to store data, but that's more
complexity than is usually warranted. If you want static attributes, put them
in your wrapper object. If you want dynamic attributes, updated based upon
content, _then_ use properties, and consider how to hide this from your users.


Text Table Display
------------------

This is the `texttable` sub-package of `tabular`.

This system is designed to draw a pretty table on a cell-based display system,
such as a classic Unix terminal emulator. Every "display cell" (_not_ table
cell) is a fixed pixel width and height, so using box-drawing characters,
everything can be made to line up.

If a cell's object supports the `Height()` method then that overrides a
calculation based on "newlines count + 1". If a cell's object supports the
`TerminalCellWidth()` method then that overrides a calculation based on text,
figuring out the longest line (multi-line supported) where length is
Unicode-aware and display-width (wide char and combining char) aware.

Calculations upon cells are done at _render_ time, to examine the contents and
determine width and height for text-table purposes. These are stored as
properties of each cell. This is a complete table sweep before printing the
first line starts. Then the rendering uses the properties to size itself and
print the table.

TODO: The maximum widths should become column properties of the table and the
height become a row property.

There is a `decoration` sub-package of `texttable` which has decoration styles
for rendering tables, as ASCII or as a few varieties of Unicode box-drawing.
Decoration objects can be created by callers and set directly upon the table,
or can be set by name. The names are maintained as a registry within the
`decoration` package. Each name is a simple string, thus typos are a
potential source of errors. For the styles native to the `decoration`
package, package constants are exported with the names, permitting
compile-time checks to catch issues. Eg, use `decoration.D_UTF8_LIGHT_CURVED`
instead of `"utf8-light-curved"` if you are willing to import the `decoration`
package here. There's a trade-off between provable correctness and importing
more and clients get to choose the level they're happy with.


HTML Table Rendering
--------------------

This is the `html` sub-package of `tabular`.

It does not render "separator" rows.

Table `Id`, `Class` and `Caption` are top-level attributes.

The table is rendered using Golang's `html/template` to handle auto-escaping
of unsafe data. If the template name matters (you are using templates more
generally) then you can use `TemplateName` on the `HTMLTable` object.

There is an `SetRowClassGenerator()` method to let you register your own
function to be used to emit a class-name on each `<tr>` of the table's body.
See the package docs for more details (you get a row index and your own
context for passing state).


CSV Rendering
-------------

This is the `csv` sub-package of `tabular`.

The rendering is compliant to RFC4180. All fields are always quoted. Note in
particular that newlines within strings are not `\n` escaped and double-quotes
are doubled for escaping, thus `""`. Both of these attributes are
RFC-specified.

The `CSVTable` type is designed to be extensible to change separators,
escaping styles and more. The _default_ is RFC4180, and that's the only
_current_ style, but we should accept PRs for any sane options, and also
"family" sets, as long as well-specified. At present, only the
`fieldSeparator` is called out in the struct, and there are no mutators for
it. That's not a bug, just "not yet implemented, waiting for solid
use-cases".
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
tabular
=======

<!-- FIXME: add banner widgets here, once released to public -->

The `tabular` package provides a Golang library for storing data in a table
consisting of rows and columns. Sub-packages provide for rendering such a
table as a terminal box-table (line-drawing with UTF-8 box-drawing in various
styles, or ASCII), as HTML, or as CSV data.

The core data model is designed to be extensible and powerful, letting such
a table be embedded in various more sophisticated models. (Eg, core of a
spreadsheet). Cells in the table contain arbitrary data and possess metadata
in the form of "properties", modelled after the `context` package's `Context`.

A table can be created from the base package and then populated, before being
passed to any of the renderers, or a table can be directly created using a
sub-package, such that you _probably_ won't need to import the base package
directly.

An overview guide to the codebase can be found in
[the Overview.md](Overview.md)

The usage documentation is in Godoc format. A link will be added here when
this package is made publicly available. <!-- FIXME: add link to godoc here. -->

This package should be installable in the usual `go get` manner.
Loading

0 comments on commit 6d68737

Please sign in to comment.