tessella 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +900 -0
- data/doc/README.md +74 -0
- data/doc/bitfield.md +123 -0
- data/doc/charts.md +1370 -0
- data/doc/check.rb +75 -0
- data/doc/cli.md +483 -0
- data/doc/composition.md +599 -0
- data/doc/getting-started.md +121 -0
- data/doc/glyphs.md +134 -0
- data/doc/images.md +325 -0
- data/doc/library.md +239 -0
- data/doc/lint.md +270 -0
- data/doc/meter.md +210 -0
- data/doc/rule.md +51 -0
- data/doc/seq.md +169 -0
- data/doc/sparkline.md +63 -0
- data/doc/table.md +347 -0
- data/doc/targets.md +222 -0
- data/doc/text.md +84 -0
- data/doc/tree.md +108 -0
- data/doc/wave.md +122 -0
- data/examples/README.md +103 -0
- data/examples/ages.txt +9 -0
- data/examples/bad.csv +3 -0
- data/examples/boxes.txt +3 -0
- data/examples/broken.txt +4 -0
- data/examples/calibration.rb +12 -0
- data/examples/cities.csv +4 -0
- data/examples/cjk.rb +13 -0
- data/examples/comment.rb +9 -0
- data/examples/comment.sh +13 -0
- data/examples/cpu.txt +60 -0
- data/examples/dashboard.sh +25 -0
- data/examples/datasheet.rb +16 -0
- data/examples/disc.pgm +0 -0
- data/examples/disks.sh +18 -0
- data/examples/disks.txt +5 -0
- data/examples/dose.txt +7 -0
- data/examples/drift.txt +4 -0
- data/examples/ethernet.txt +3 -0
- data/examples/figure.c +5 -0
- data/examples/fit.rb +16 -0
- data/examples/floors.rb +21 -0
- data/examples/gallery.sh +20 -0
- data/examples/gauges.sh +31 -0
- data/examples/gradient.rb +16 -0
- data/examples/growth.txt +6 -0
- data/examples/hosts.csv +4 -0
- data/examples/icmp.txt +3 -0
- data/examples/ipv4.txt +12 -0
- data/examples/kinds.csv +4 -0
- data/examples/langs.csv +4 -0
- data/examples/lint.rb +16 -0
- data/examples/load.rb +14 -0
- data/examples/login.txt +10 -0
- data/examples/neg.csv +4 -0
- data/examples/normal.txt +200 -0
- data/examples/note.txt +2 -0
- data/examples/notes-fixed.md +17 -0
- data/examples/notes.md +17 -0
- data/examples/ohlc.txt +7 -0
- data/examples/os.csv +10 -0
- data/examples/outline.txt +8 -0
- data/examples/plot.txt +5 -0
- data/examples/refusals.rb +17 -0
- data/examples/report.sh +21 -0
- data/examples/rfc.sh +22 -0
- data/examples/samples.txt +7 -0
- data/examples/shapes.pgm +0 -0
- data/examples/sizes.tsv +3 -0
- data/examples/spi.txt +6 -0
- data/examples/status.sh +23 -0
- data/examples/stem.txt +12 -0
- data/examples/table-drift.txt +4 -0
- data/examples/temps.txt +31 -0
- data/examples/trig.sh +13 -0
- data/examples/trig.txt +102 -0
- data/examples/week.txt +8 -0
- data/examples/wide.txt +2 -0
- data/exe/tessella +6 -0
- data/lib/tessella/axis.rb +71 -0
- data/lib/tessella/bitfield.rb +194 -0
- data/lib/tessella/block.rb +56 -0
- data/lib/tessella/borders.rb +85 -0
- data/lib/tessella/canvas/area.rb +155 -0
- data/lib/tessella/canvas/ascii.rb +120 -0
- data/lib/tessella/canvas/block.rb +40 -0
- data/lib/tessella/canvas/braille.rb +30 -0
- data/lib/tessella/canvas/clip.rb +49 -0
- data/lib/tessella/canvas/density.rb +51 -0
- data/lib/tessella/canvas/dot.rb +27 -0
- data/lib/tessella/canvas/eighths.rb +43 -0
- data/lib/tessella/canvas/half.rb +51 -0
- data/lib/tessella/canvas/sextant.rb +36 -0
- data/lib/tessella/canvas.rb +168 -0
- data/lib/tessella/cell.rb +10 -0
- data/lib/tessella/chart.rb +211 -0
- data/lib/tessella/check.rb +52 -0
- data/lib/tessella/cli/bitfield.rb +121 -0
- data/lib/tessella/cli/chart.rb +615 -0
- data/lib/tessella/cli/chart_candle.rb +36 -0
- data/lib/tessella/cli/chart_heatmap.rb +21 -0
- data/lib/tessella/cli/chart_help.rb +85 -0
- data/lib/tessella/cli/chart_pie.rb +38 -0
- data/lib/tessella/cli/compose.rb +156 -0
- data/lib/tessella/cli/compose_options.rb +259 -0
- data/lib/tessella/cli/glyphs.rb +97 -0
- data/lib/tessella/cli/image.rb +250 -0
- data/lib/tessella/cli/input.rb +44 -0
- data/lib/tessella/cli/iso_time.rb +28 -0
- data/lib/tessella/cli/lint.rb +102 -0
- data/lib/tessella/cli/lint_options.rb +99 -0
- data/lib/tessella/cli/main.rb +121 -0
- data/lib/tessella/cli/meter.rb +80 -0
- data/lib/tessella/cli/rule.rb +57 -0
- data/lib/tessella/cli/seq.rb +117 -0
- data/lib/tessella/cli/sparkline.rb +72 -0
- data/lib/tessella/cli/table.rb +167 -0
- data/lib/tessella/cli/text.rb +78 -0
- data/lib/tessella/cli/tree.rb +100 -0
- data/lib/tessella/cli/wave.rb +148 -0
- data/lib/tessella/cli.rb +36 -0
- data/lib/tessella/color.rb +106 -0
- data/lib/tessella/compose.rb +243 -0
- data/lib/tessella/compose_fill.rb +176 -0
- data/lib/tessella/compose_grid.rb +36 -0
- data/lib/tessella/compose_shadow.rb +41 -0
- data/lib/tessella/error.rb +13 -0
- data/lib/tessella/frame.rb +170 -0
- data/lib/tessella/glyphs.rb +106 -0
- data/lib/tessella/image.rb +454 -0
- data/lib/tessella/image_octant.rb +46 -0
- data/lib/tessella/image_petscii.rb +116 -0
- data/lib/tessella/kinds/bar.rb +344 -0
- data/lib/tessella/kinds/box.rb +213 -0
- data/lib/tessella/kinds/box_upright.rb +110 -0
- data/lib/tessella/kinds/candlestick.rb +164 -0
- data/lib/tessella/kinds/columns.rb +259 -0
- data/lib/tessella/kinds/date_axis.rb +102 -0
- data/lib/tessella/kinds/error_bars.rb +71 -0
- data/lib/tessella/kinds/heatmap.rb +130 -0
- data/lib/tessella/kinds/line.rb +173 -0
- data/lib/tessella/kinds/multibar.rb +98 -0
- data/lib/tessella/kinds/multibar_columns.rb +130 -0
- data/lib/tessella/kinds/multibar_fine.rb +45 -0
- data/lib/tessella/kinds/multibar_rows.rb +93 -0
- data/lib/tessella/kinds/pie.rb +123 -0
- data/lib/tessella/kinds/pie_rim.rb +78 -0
- data/lib/tessella/kinds/sideways.rb +62 -0
- data/lib/tessella/kinds/stem.rb +134 -0
- data/lib/tessella/kinds/threshold.rb +73 -0
- data/lib/tessella/legend.rb +31 -0
- data/lib/tessella/lint/alphabet.rb +56 -0
- data/lib/tessella/lint/drift.rb +82 -0
- data/lib/tessella/lint/fences.rb +51 -0
- data/lib/tessella/lint/geometry.rb +120 -0
- data/lib/tessella/lint/geometry_checks.rb +224 -0
- data/lib/tessella/lint/lines.rb +115 -0
- data/lib/tessella/lint/mixing.rb +61 -0
- data/lib/tessella/lint/runner.rb +73 -0
- data/lib/tessella/lint/shadows.rb +120 -0
- data/lib/tessella/lint/target_rows.rb +66 -0
- data/lib/tessella/lint.rb +158 -0
- data/lib/tessella/meter.rb +133 -0
- data/lib/tessella/meter_upright.rb +43 -0
- data/lib/tessella/png.rb +456 -0
- data/lib/tessella/raster.rb +92 -0
- data/lib/tessella/rmagick_reader.rb +52 -0
- data/lib/tessella/rule.rb +59 -0
- data/lib/tessella/seq.rb +522 -0
- data/lib/tessella/sparkline.rb +57 -0
- data/lib/tessella/stats.rb +199 -0
- data/lib/tessella/table.rb +547 -0
- data/lib/tessella/table_cells.rb +225 -0
- data/lib/tessella/target.rb +105 -0
- data/lib/tessella/text.rb +76 -0
- data/lib/tessella/text_forms.rb +150 -0
- data/lib/tessella/tree.rb +221 -0
- data/lib/tessella/version.rb +5 -0
- data/lib/tessella/wave.rb +353 -0
- data/lib/tessella/width.rb +122 -0
- data/lib/tessella/width_class.rb +77 -0
- data/lib/tessella/width_invisible.rb +66 -0
- data/lib/tessella/width_invisible_high.rb +60 -0
- data/lib/tessella.rb +94 -0
- metadata +226 -0
data/doc/library.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# The library
|
|
2
|
+
|
|
3
|
+
```ruby
|
|
4
|
+
require "tessella"
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
loads the drawing library and nothing else: no command line, no
|
|
8
|
+
option parser, no gem. Every factory is a module function on
|
|
9
|
+
`Tessella`; its result prints with `puts`.
|
|
10
|
+
|
|
11
|
+
## Factories
|
|
12
|
+
|
|
13
|
+
### Charts
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
Tessella.line(x = nil, y, name: nil, mark: nil, **opts) # => Chart
|
|
17
|
+
Tessella.scatter(x = nil, y, name: nil, mark: nil, **opts)
|
|
18
|
+
Tessella.stairs(x = nil, y, name: nil, mark: nil, style: :post, **opts)
|
|
19
|
+
Tessella.density(x = nil, y, name: nil, mark: nil, **opts)
|
|
20
|
+
Tessella.bar(labels, values, **opts)
|
|
21
|
+
Tessella.histogram(values, bins: nil, closed: :left, **opts)
|
|
22
|
+
Tessella.box(names, samples, **opts) # samples: Array of Arrays
|
|
23
|
+
Tessella.stem(values, other = nil, scale: nil, **opts)
|
|
24
|
+
Tessella.heatmap(matrix, xlabels: nil, ylabels: nil, cell_width: 2,
|
|
25
|
+
colormap: nil, min: nil, max: nil, **opts)
|
|
26
|
+
Tessella.candlestick(x, open:, high:, low:, close:, **opts)
|
|
27
|
+
Tessella.pie(labels, values, donut: false, rim: :cells, **opts)
|
|
28
|
+
|
|
29
|
+
chart.add(x = nil, y, name: nil, color: nil, mark: nil) # => chart
|
|
30
|
+
chart.to_s # => String, framed, "\n"-terminated
|
|
31
|
+
chart.render(io = $stdout) # prints to_s
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`x` defaults to `1..y.length`. `add` takes another series on a line,
|
|
35
|
+
scatter, stairs or density chart. `stem`'s `other` is the second set
|
|
36
|
+
of a back-to-back plot. `opts`, their defaults, and the bar options
|
|
37
|
+
(`fill:`, `eighths:`, `track:`) are in [charts](charts.md#common-options).
|
|
38
|
+
|
|
39
|
+
### Composition
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
Tessella.frame(fig, title: nil, style: nil, pad: 1, fill: nil,
|
|
43
|
+
halo: true, ascii: false)
|
|
44
|
+
Tessella.beside(fig, fig, ..., gap: 1, align: :top, ascii: false)
|
|
45
|
+
Tessella.stack(fig, fig, ..., gap: 0, rule: nil, center: false,
|
|
46
|
+
ascii: false)
|
|
47
|
+
Tessella.grid(fig, fig, ..., columns: 2, gap: 2, row_gap: 1,
|
|
48
|
+
align: :top, ascii: false)
|
|
49
|
+
Tessella.indent(fig, prefix:, ascii: false)
|
|
50
|
+
Tessella.caption(fig, text:, above: false, ascii: false)
|
|
51
|
+
Tessella.shadow(fig, glyph: nil, ascii: false)
|
|
52
|
+
Tessella.fill(fig, pattern: nil, halo: true, ascii: false)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Each takes any figure (a `Block`, a `String`, a chart) and returns a
|
|
56
|
+
`Block`. See [composition](composition.md).
|
|
57
|
+
|
|
58
|
+
### Diagrams and pictures
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
Tessella.tree(outline, style: nil, ascii: false, root: nil)
|
|
62
|
+
Tessella.table(rows, header: true, style: nil, ascii: false,
|
|
63
|
+
align: nil, title: nil, width: nil, min_width: 4,
|
|
64
|
+
color: false) # min_width: N, or [N, ...]
|
|
65
|
+
Tessella::Table.parse(text, tsv: false) # => Array of rows
|
|
66
|
+
Tessella.bitfield(fields, word: 32, octets: false, ruler: true,
|
|
67
|
+
abbrev: false)
|
|
68
|
+
Tessella.wave(text, period: 4, ascii: false, ruler: false)
|
|
69
|
+
Tessella.seq(text, ascii: false, numbered: false, footer: false)
|
|
70
|
+
Tessella.image(source, style: :ramp, width: 78, ramp: nil, gamma: 1.0,
|
|
71
|
+
threshold: 0.5, dither: :fs, invert: false,
|
|
72
|
+
edge_threshold: 0.3, ascii: false)
|
|
73
|
+
Tessella.meter(fraction, style: :block, width: 20, title: nil,
|
|
74
|
+
title_width: nil, label: true, brackets: true,
|
|
75
|
+
orientation: :horizontal, height: 8, ascii: false)
|
|
76
|
+
Tessella.sparkline(values, width: nil, min: nil, max: nil, ascii: false)
|
|
77
|
+
Tessella.rule(width: 78, ornament: nil, style: :light, ascii: false)
|
|
78
|
+
Tessella.text(str, style:, strict: false, ascii: false) # => String
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The figures return a `Block`. Pages: [tree](tree.md), [table](table.md),
|
|
82
|
+
[bitfield](bitfield.md), [wave](wave.md), [seq](seq.md),
|
|
83
|
+
[images](images.md), [sparkline](sparkline.md), [meter](meter.md),
|
|
84
|
+
[rule](rule.md),
|
|
85
|
+
[text](text.md). `text` returns a String, not a Block.
|
|
86
|
+
|
|
87
|
+
### Lint
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
Tessella.lint(source, width: 78, prefix: "", web: false, forbid: [],
|
|
91
|
+
no_wide: false, ascii: false, allow_mixed: false,
|
|
92
|
+
fences: false, geometry: false, borders: [])
|
|
93
|
+
# => Tessella::Lint::Report
|
|
94
|
+
Tessella.alphabet(source) # => "ascii" or "utf-8 <classes>"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`source` is a String or an IO. A report has `findings` (each a
|
|
98
|
+
`Finding` with `line` and `message`), `clean?`, and `rulers` (each a
|
|
99
|
+
`Ruler` with `line`, `width` and `verticals`). See [lint](lint.md).
|
|
100
|
+
|
|
101
|
+
## Values
|
|
102
|
+
|
|
103
|
+
### `Tessella::Block`
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
block.lines # => Array of String
|
|
107
|
+
block.width # => Integer, display columns
|
|
108
|
+
block.height # => Integer
|
|
109
|
+
block.to_s # => String, "\n"-terminated
|
|
110
|
+
block.check_width!(n) # raises ArgumentError if wider than n
|
|
111
|
+
Tessella::Block.from(figure) # a Block from a Block, String or chart
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A block has no trailing blank on any line, no trailing blank line,
|
|
115
|
+
and no tab.
|
|
116
|
+
|
|
117
|
+
### `Tessella::Raster`
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
Tessella::Raster.new(width:, height:, luma:)
|
|
121
|
+
Tessella::Raster.decode(bytes) # PNM or PNG; others via rmagick
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A greyscale image: `luma` is `width * height` Floats in 0..1, row by
|
|
125
|
+
row from the top. See [images](images.md#sources).
|
|
126
|
+
|
|
127
|
+
### `Tessella::Borders::Set`
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
Tessella::Borders::Set.new(tl, tr, bl, br, h, v)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A custom border, for `frame`'s and `table`'s `style:` and `lint`'s
|
|
134
|
+
`borders:`. A six-glyph String in the same order does the same job;
|
|
135
|
+
a table needs eleven, its five tees after these six. See
|
|
136
|
+
[custom borders](composition.md#custom-borders).
|
|
137
|
+
|
|
138
|
+
### `Tessella::Width`
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
Tessella::Width.of("日本") # => 4
|
|
142
|
+
Tessella::Width.of("\e[1mab\e[0m") # => 2
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The display width of a string: the columns a terminal gives it, wide
|
|
146
|
+
characters two, combining marks and ANSI escapes none. Every figure
|
|
147
|
+
is measured with it, which is why CJK labels line up.
|
|
148
|
+
`Tessella::Width.class_of(char)` is a character's width class, `:N`,
|
|
149
|
+
`:A` or `:W`; see [glyph sets](glyphs.md). Width counts grapheme
|
|
150
|
+
clusters: an emoji sequence joined by ZWJ, or skin-toned, is one
|
|
151
|
+
glyph, two columns wide.
|
|
152
|
+
|
|
153
|
+
### Width targets
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
Tessella.target = :cjk # :term (default), :cjk or :web
|
|
157
|
+
Tessella::Target.with(:web) { ... }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The target the figures are drawn for, per fiber; see
|
|
161
|
+
[width targets](targets.md).
|
|
162
|
+
|
|
163
|
+
### `Tessella::Glyphs`
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
Tessella::Glyphs.fetch(kind, name) # => GlyphSet
|
|
167
|
+
Tessella::Glyphs.all # => every GlyphSet
|
|
168
|
+
glyph_set.glyphs; glyph_set.classes; glyph_set.mixed?
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The named glyph sets the library draws with, and their width classes.
|
|
172
|
+
See [glyph sets](glyphs.md).
|
|
173
|
+
|
|
174
|
+
## Errors
|
|
175
|
+
|
|
176
|
+
Every refusal is an `ArgumentError` with a message that names what is
|
|
177
|
+
wrong — the option, the value, the line of input — raised before
|
|
178
|
+
anything is drawn. A figure is either right or not returned. A value
|
|
179
|
+
that cannot be drawn (not finite, out of range, out of order) raises
|
|
180
|
+
`Tessella::ValueError`, a subclass, so a caller can tell bad data
|
|
181
|
+
from a bad call; the command line exits 1 for the first and 2 for
|
|
182
|
+
the second.
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
# frozen_string_literal: true
|
|
186
|
+
|
|
187
|
+
# examples/refusals.rb: what the library refuses, and how it says so.
|
|
188
|
+
require "tessella"
|
|
189
|
+
|
|
190
|
+
[
|
|
191
|
+
-> { Tessella.line([1, 2, 3], [1, 2]) },
|
|
192
|
+
-> { Tessella.bar(%w[a b], [1, Float::NAN]) },
|
|
193
|
+
-> { Tessella.line([1, 2], width: 0) },
|
|
194
|
+
-> { Tessella.line([1, 2], colour: true) },
|
|
195
|
+
-> { Tessella.tree("a\n b\n c") },
|
|
196
|
+
-> { Tessella.frame("é", ascii: true) }
|
|
197
|
+
].each do |call|
|
|
198
|
+
call.call
|
|
199
|
+
rescue ArgumentError => e
|
|
200
|
+
puts "#{e.class}: #{e.message}"
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
$ ruby refusals.rb
|
|
206
|
+
ArgumentError: x and y differ in length: 3 and 2
|
|
207
|
+
Tessella::ValueError: value must be a finite number: NaN
|
|
208
|
+
ArgumentError: width must be a positive Integer: 0
|
|
209
|
+
ArgumentError: unknown option: :colour
|
|
210
|
+
ArgumentError: line 3: indentation is not a multiple of 3
|
|
211
|
+
ArgumentError: block 1 is non-ASCII under ascii: true
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## The rules every figure keeps
|
|
215
|
+
|
|
216
|
+
- **No trailing blank** on any line, and no trailing blank line.
|
|
217
|
+
- **No escape code** unless `color: true`.
|
|
218
|
+
- **No tab, no invisible glyph.** A blank cell is a space (U+0020),
|
|
219
|
+
never the empty braille pattern U+2800.
|
|
220
|
+
- **`ascii: true` is ASCII from the first byte:** a figure is drawn
|
|
221
|
+
in ASCII glyphs, not drawn in Unicode and rewritten after, and any
|
|
222
|
+
text given to it that is not ASCII is refused. A bit field is
|
|
223
|
+
always ASCII.
|
|
224
|
+
- **Values as given:** `12` prints `12`, not `12.0`, and a Float
|
|
225
|
+
keeps its digits.
|
|
226
|
+
|
|
227
|
+
These are what make a figure safe to paste into a README, a commit
|
|
228
|
+
message, a source comment or a man page.
|
|
229
|
+
|
|
230
|
+
## Vendoring
|
|
231
|
+
|
|
232
|
+
tessella has no runtime dependency, so it can travel as one file:
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
make bundle # writes dist/tessella.rb: the library and the CLI
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`dist/tessella.rb` is the library followed by the command line, in
|
|
239
|
+
load order. Copy it into a project and `require_relative` it.
|
data/doc/lint.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Linting
|
|
2
|
+
|
|
3
|
+
A text figure that looks right in one editor can break on someone
|
|
4
|
+
else's screen: a tab that expands differently, a trailing blank a
|
|
5
|
+
formatter strips, a glyph that one font draws two cells wide, a box
|
|
6
|
+
side one column off. `lint` checks a figure for these before it ships,
|
|
7
|
+
whether tessella drew it or a person did.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
report = Tessella.lint(source, width: 78, prefix: "", suffix: "",
|
|
11
|
+
web: false, forbid: [], no_wide: false, ascii: false,
|
|
12
|
+
allow_mixed: false, fences: false,
|
|
13
|
+
geometry: false, borders: [])
|
|
14
|
+
report.findings # => Array of Finding (line, message)
|
|
15
|
+
report.clean? # => true when there is none
|
|
16
|
+
report.rulers # => Array of Ruler (line, width, verticals)
|
|
17
|
+
Tessella.alphabet(source) # => "ascii", or "utf-8 <classes>"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
tessella lint [--width N] [--prefix STR] [--suffix STR] [--ruler]
|
|
22
|
+
[--forbid SEQ]...
|
|
23
|
+
[--web] [--no-wide] [--ascii] [--allow-mixed]
|
|
24
|
+
[--fences] [--geometry] [--border GLYPHS]...
|
|
25
|
+
[--target T] [file ...]
|
|
26
|
+
tessella lint --alphabet [file ...]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`source` is a String or an IO. The command reads its files, or stdin
|
|
30
|
+
when none is given.
|
|
31
|
+
|
|
32
|
+
## Output and exit status
|
|
33
|
+
|
|
34
|
+
Each finding goes to stderr as `FILE:LINE: message` (`stdin` for
|
|
35
|
+
stdin), one per line. The exit status is 0 when every input is clean,
|
|
36
|
+
1 on any finding or a file that cannot be read (the other files are
|
|
37
|
+
still linted), and 2 on a usage mistake. A clean file prints nothing:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
$ tessella lint plot.txt && echo clean
|
|
41
|
+
clean
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`broken.txt` is a four-line figure with a tab on its first line, a
|
|
45
|
+
trailing blank on its second, and a box whose right side wanders:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
$ tessella lint broken.txt
|
|
49
|
+
broken.txt:1: tab character
|
|
50
|
+
broken.txt:2: trailing whitespace
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## What is always checked
|
|
54
|
+
|
|
55
|
+
- **Characters that have no fixed width:** tabs, invisible glyphs
|
|
56
|
+
(any character a terminal gives no column, such as a zero-width
|
|
57
|
+
space), invalid UTF-8.
|
|
58
|
+
- **Look-alike spaces:** a no-break, thin, ideographic or other space
|
|
59
|
+
that is not U+0020, drawn as a blank and told from a space by no
|
|
60
|
+
display.
|
|
61
|
+
- **Trailing blanks**, which an editor or a formatter may strip,
|
|
62
|
+
changing the figure behind the author's back.
|
|
63
|
+
- **The column budget:** a line wider than `--width` (78 by default)
|
|
64
|
+
once `--prefix` is drawn before it and `--suffix` after it. The
|
|
65
|
+
prefix is what the figure will sit behind: `// `, `# `, ` * `, four
|
|
66
|
+
spaces of Markdown code; the suffix is what closes a comment wrapped
|
|
67
|
+
around each row, ` -->` or ` */`.
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
$ tessella lint --width 16 --prefix '// ' note.txt
|
|
71
|
+
note.txt:1: 18 columns rendered (budget 16)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- **ASCII and Unicode structure mixed in one figure**, such as `+--`
|
|
75
|
+
corners on a `│` side, which the reader's font rarely lines up.
|
|
76
|
+
`--allow-mixed` turns this off; a line reading `tessella:
|
|
77
|
+
allow-mixed` (or `artlint: allow-mixed`), in any comment syntax,
|
|
78
|
+
exempts the figure after it.
|
|
79
|
+
- **Drift:** a vertical one column off its anchor on a few rows.
|
|
80
|
+
- **Shadows:** a `░` drop shadow missing from a row or misplaced.
|
|
81
|
+
|
|
82
|
+
## Geometry
|
|
83
|
+
|
|
84
|
+
`--geometry` (`geometry: true`) reads the boxes and connectors as
|
|
85
|
+
structure: a box's sides must stay in their columns down to its
|
|
86
|
+
bottom edge, and a vertical or an arrow head must hang from something
|
|
87
|
+
above it, not from blank space. It knows every box-drawing set (light,
|
|
88
|
+
heavy, double, rounded, dashed, mixed junctions) and ASCII `+-|`
|
|
89
|
+
boxes:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
$ tessella lint --geometry broken.txt
|
|
93
|
+
broken.txt:1: tab character
|
|
94
|
+
broken.txt:2: trailing whitespace
|
|
95
|
+
broken.txt:3: side of box opened at line 2 col 1 is off at col 4
|
|
96
|
+
broken.txt:3: connector '│' at col 5 hangs from ' '
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
It is off by default because it reads every `│` and `+` as structure,
|
|
100
|
+
so a Markdown table or a `|` in prose would be flagged.
|
|
101
|
+
|
|
102
|
+
A box drawn in a custom border set is not structure to the lint until
|
|
103
|
+
it is declared: `--border GLYPHS` (`borders:`) takes six glyphs in the
|
|
104
|
+
order `tl tr bl br h v`, as [frame's custom styles](composition.md#custom-borders)
|
|
105
|
+
do. `drift.txt` is a frame in `****=!` whose right side moved on its
|
|
106
|
+
third line:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
$ tessella lint --geometry drift.txt && \
|
|
110
|
+
echo 'clean: the custom box is not read'
|
|
111
|
+
clean: the custom box is not read
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
$ tessella lint --geometry --border '****=!' drift.txt
|
|
116
|
+
drift.txt:3: side of box opened at line 1 col 1 is off at col 19
|
|
117
|
+
drift.txt:3: connector '!' at col 20 hangs from ' '
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Rulers
|
|
121
|
+
|
|
122
|
+
`--ruler` prints, on stdout, each line's width and the columns of its
|
|
123
|
+
verticals. Down a figure the verticals must agree, so a ruler shows a
|
|
124
|
+
drift at a glance, even one `--geometry` is not asked to find:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
$ tessella lint --ruler table-drift.txt
|
|
128
|
+
table-drift.txt:1: width=7
|
|
129
|
+
table-drift.txt:2: width=7 verticals=1,4,7
|
|
130
|
+
table-drift.txt:3: width=8 verticals=1,5,8
|
|
131
|
+
table-drift.txt:4: width=7
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Markdown
|
|
135
|
+
|
|
136
|
+
Linted whole, a Markdown file's prose and tables would be read as
|
|
137
|
+
art. `--fences` (`fences: true`) lints each fenced block on its own,
|
|
138
|
+
keeping the file's line numbers; a `tessella: allow-mixed` marker just
|
|
139
|
+
before a fence exempts that block. `notes.md` holds a table, some
|
|
140
|
+
prose, and a hand-drawn diagram whose connector is one column off:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
$ tessella lint --fences --geometry notes.md
|
|
144
|
+
notes.md:13: connector '│' at col 20 hangs from '─'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
A Markdown file is usually read on the web, so lint it for that
|
|
148
|
+
target too. `--target web` also flags the `▶` at the end of the
|
|
149
|
+
diagram's arrow, which a browser may draw as a wide emoji:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
$ tessella lint --fences --geometry --target web notes.md
|
|
153
|
+
notes.md:11: emoji-capable U+25B6 ▶ (web target)
|
|
154
|
+
notes.md:13: connector '│' at col 20 hangs from '─'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Redrawn with a small arrow head, `▸`, and the connector back on its
|
|
158
|
+
`┬`, the figure in `notes-fixed.md` is clean for the web:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
$ sed -n 10,16p notes-fixed.md
|
|
162
|
+
┌───────┐ ┌───────┐
|
|
163
|
+
│ parse │────▸│ draw │
|
|
164
|
+
└───────┘ └───┬───┘
|
|
165
|
+
│
|
|
166
|
+
┌───┴───┐
|
|
167
|
+
│ print │
|
|
168
|
+
└───────┘
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
$ tessella lint --fences --geometry --target web notes-fixed.md && echo clean
|
|
173
|
+
clean
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## The target
|
|
177
|
+
|
|
178
|
+
The options describe where the figure is going:
|
|
179
|
+
|
|
180
|
+
- `--no-wide`: every wide (two-cell) glyph is a finding, for a target
|
|
181
|
+
whose font may not draw CJK at double width.
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
$ tessella lint --no-wide wide.txt
|
|
185
|
+
wide.txt:2: wide char U+65E5 日
|
|
186
|
+
wide.txt:2: wide char U+672C 本
|
|
187
|
+
wide.txt:2: wide char U+8A9E 語
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
$ printf '+---+\n|é |\n+---+\n' | tessella lint --target cjk
|
|
192
|
+
stdin:2: ambiguous U+00E9 é on aligned row (cjk target: 2 columns)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- `--target term|cjk|web`: measure with a [width target](targets.md)'s
|
|
196
|
+
widths. `web` is `--web`, and lets a glyph followed by U+FE0E pass;
|
|
197
|
+
`cjk` names an ambiguous glyph that is not box drawing on a row
|
|
198
|
+
that must line up, since it takes two columns there.
|
|
199
|
+
- `--web`: a page in a browser. Glyphs some web renderers draw as
|
|
200
|
+
emoji, or two cells wide, are flagged.
|
|
201
|
+
- `--ascii`: every non-ASCII glyph is a finding (it implies
|
|
202
|
+
`--no-wide`).
|
|
203
|
+
- `--forbid SEQ`: lines holding SEQ are findings. SEQ is a sequence,
|
|
204
|
+
or a preset naming the comment syntax the figure sits in, so a line
|
|
205
|
+
that would end the comment early is caught: `xml`, `html`,
|
|
206
|
+
`c-block`, `rust`, `c-line`, `haskell`, `ocaml`, `pascal`, `d`,
|
|
207
|
+
`lisp`, `lua`, `python-doc`, `cdata`, `markdown-fence`. The option
|
|
208
|
+
repeats.
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
$ tessella lint --forbid c-block figure.c
|
|
212
|
+
figure.c:1: forbidden sequence "/*"
|
|
213
|
+
figure.c:2: forbidden sequence "*/"
|
|
214
|
+
figure.c:5: forbidden sequence "*/"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Here the comment's own opener and closer are caught too: lint the
|
|
218
|
+
figure, not the comment around it.
|
|
219
|
+
|
|
220
|
+
## The alphabet
|
|
221
|
+
|
|
222
|
+
Before a figure goes into a file, `--alphabet` (`Tessella.alphabet`)
|
|
223
|
+
says what that file already holds. It lints nothing and exits 0. A
|
|
224
|
+
file that is pure ASCII should get an ASCII figure:
|
|
225
|
+
|
|
226
|
+
```text
|
|
227
|
+
$ tessella lint --alphabet figure.c langs.csv plot.txt notes.md
|
|
228
|
+
figure.c: ascii
|
|
229
|
+
langs.csv: ascii
|
|
230
|
+
plot.txt: utf-8 box-drawing(7) blocks(27) other(1: U+00B7 ·)
|
|
231
|
+
notes.md: utf-8 box-drawing(65) geometric(1)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## From Ruby
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
# frozen_string_literal: true
|
|
238
|
+
|
|
239
|
+
# examples/lint.rb: the lint from Ruby -- a figure tessella draws is
|
|
240
|
+
# clean by construction; a hand-drawn one is reported line by line,
|
|
241
|
+
# and the rulers show where its verticals stopped agreeing.
|
|
242
|
+
require "tessella"
|
|
243
|
+
|
|
244
|
+
chart = Tessella.bar(%w[ruby c rust], [12, 30, 7.5], width: 30)
|
|
245
|
+
puts "chart: #{Tessella.lint(chart.to_s, geometry: true).clean?}"
|
|
246
|
+
|
|
247
|
+
report = Tessella.lint(File.read("broken.txt"), geometry: true)
|
|
248
|
+
report.findings.each { puts "broken.txt:#{it.line}: #{it.message}" }
|
|
249
|
+
|
|
250
|
+
Tessella.lint(File.read("table-drift.txt")).rulers.each do |r|
|
|
251
|
+
puts "line #{r.line}: verticals #{r.verticals.inspect}"
|
|
252
|
+
end
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
```text
|
|
256
|
+
$ ruby lint.rb
|
|
257
|
+
chart: true
|
|
258
|
+
broken.txt:1: tab character
|
|
259
|
+
broken.txt:2: trailing whitespace
|
|
260
|
+
broken.txt:3: side of box opened at line 2 col 1 is off at col 4
|
|
261
|
+
broken.txt:3: connector '│' at col 5 hangs from ' '
|
|
262
|
+
line 1: verticals []
|
|
263
|
+
line 2: verticals [1, 4, 7]
|
|
264
|
+
line 3: verticals [1, 5, 8]
|
|
265
|
+
line 4: verticals []
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
tessella holds its own output to this lint: a figure it draws is
|
|
269
|
+
clean by construction, which is the point of the
|
|
270
|
+
[rules every figure keeps](library.md#the-rules-every-figure-keeps).
|