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.
Files changed (188) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +900 -0
  4. data/doc/README.md +74 -0
  5. data/doc/bitfield.md +123 -0
  6. data/doc/charts.md +1370 -0
  7. data/doc/check.rb +75 -0
  8. data/doc/cli.md +483 -0
  9. data/doc/composition.md +599 -0
  10. data/doc/getting-started.md +121 -0
  11. data/doc/glyphs.md +134 -0
  12. data/doc/images.md +325 -0
  13. data/doc/library.md +239 -0
  14. data/doc/lint.md +270 -0
  15. data/doc/meter.md +210 -0
  16. data/doc/rule.md +51 -0
  17. data/doc/seq.md +169 -0
  18. data/doc/sparkline.md +63 -0
  19. data/doc/table.md +347 -0
  20. data/doc/targets.md +222 -0
  21. data/doc/text.md +84 -0
  22. data/doc/tree.md +108 -0
  23. data/doc/wave.md +122 -0
  24. data/examples/README.md +103 -0
  25. data/examples/ages.txt +9 -0
  26. data/examples/bad.csv +3 -0
  27. data/examples/boxes.txt +3 -0
  28. data/examples/broken.txt +4 -0
  29. data/examples/calibration.rb +12 -0
  30. data/examples/cities.csv +4 -0
  31. data/examples/cjk.rb +13 -0
  32. data/examples/comment.rb +9 -0
  33. data/examples/comment.sh +13 -0
  34. data/examples/cpu.txt +60 -0
  35. data/examples/dashboard.sh +25 -0
  36. data/examples/datasheet.rb +16 -0
  37. data/examples/disc.pgm +0 -0
  38. data/examples/disks.sh +18 -0
  39. data/examples/disks.txt +5 -0
  40. data/examples/dose.txt +7 -0
  41. data/examples/drift.txt +4 -0
  42. data/examples/ethernet.txt +3 -0
  43. data/examples/figure.c +5 -0
  44. data/examples/fit.rb +16 -0
  45. data/examples/floors.rb +21 -0
  46. data/examples/gallery.sh +20 -0
  47. data/examples/gauges.sh +31 -0
  48. data/examples/gradient.rb +16 -0
  49. data/examples/growth.txt +6 -0
  50. data/examples/hosts.csv +4 -0
  51. data/examples/icmp.txt +3 -0
  52. data/examples/ipv4.txt +12 -0
  53. data/examples/kinds.csv +4 -0
  54. data/examples/langs.csv +4 -0
  55. data/examples/lint.rb +16 -0
  56. data/examples/load.rb +14 -0
  57. data/examples/login.txt +10 -0
  58. data/examples/neg.csv +4 -0
  59. data/examples/normal.txt +200 -0
  60. data/examples/note.txt +2 -0
  61. data/examples/notes-fixed.md +17 -0
  62. data/examples/notes.md +17 -0
  63. data/examples/ohlc.txt +7 -0
  64. data/examples/os.csv +10 -0
  65. data/examples/outline.txt +8 -0
  66. data/examples/plot.txt +5 -0
  67. data/examples/refusals.rb +17 -0
  68. data/examples/report.sh +21 -0
  69. data/examples/rfc.sh +22 -0
  70. data/examples/samples.txt +7 -0
  71. data/examples/shapes.pgm +0 -0
  72. data/examples/sizes.tsv +3 -0
  73. data/examples/spi.txt +6 -0
  74. data/examples/status.sh +23 -0
  75. data/examples/stem.txt +12 -0
  76. data/examples/table-drift.txt +4 -0
  77. data/examples/temps.txt +31 -0
  78. data/examples/trig.sh +13 -0
  79. data/examples/trig.txt +102 -0
  80. data/examples/week.txt +8 -0
  81. data/examples/wide.txt +2 -0
  82. data/exe/tessella +6 -0
  83. data/lib/tessella/axis.rb +71 -0
  84. data/lib/tessella/bitfield.rb +194 -0
  85. data/lib/tessella/block.rb +56 -0
  86. data/lib/tessella/borders.rb +85 -0
  87. data/lib/tessella/canvas/area.rb +155 -0
  88. data/lib/tessella/canvas/ascii.rb +120 -0
  89. data/lib/tessella/canvas/block.rb +40 -0
  90. data/lib/tessella/canvas/braille.rb +30 -0
  91. data/lib/tessella/canvas/clip.rb +49 -0
  92. data/lib/tessella/canvas/density.rb +51 -0
  93. data/lib/tessella/canvas/dot.rb +27 -0
  94. data/lib/tessella/canvas/eighths.rb +43 -0
  95. data/lib/tessella/canvas/half.rb +51 -0
  96. data/lib/tessella/canvas/sextant.rb +36 -0
  97. data/lib/tessella/canvas.rb +168 -0
  98. data/lib/tessella/cell.rb +10 -0
  99. data/lib/tessella/chart.rb +211 -0
  100. data/lib/tessella/check.rb +52 -0
  101. data/lib/tessella/cli/bitfield.rb +121 -0
  102. data/lib/tessella/cli/chart.rb +615 -0
  103. data/lib/tessella/cli/chart_candle.rb +36 -0
  104. data/lib/tessella/cli/chart_heatmap.rb +21 -0
  105. data/lib/tessella/cli/chart_help.rb +85 -0
  106. data/lib/tessella/cli/chart_pie.rb +38 -0
  107. data/lib/tessella/cli/compose.rb +156 -0
  108. data/lib/tessella/cli/compose_options.rb +259 -0
  109. data/lib/tessella/cli/glyphs.rb +97 -0
  110. data/lib/tessella/cli/image.rb +250 -0
  111. data/lib/tessella/cli/input.rb +44 -0
  112. data/lib/tessella/cli/iso_time.rb +28 -0
  113. data/lib/tessella/cli/lint.rb +102 -0
  114. data/lib/tessella/cli/lint_options.rb +99 -0
  115. data/lib/tessella/cli/main.rb +121 -0
  116. data/lib/tessella/cli/meter.rb +80 -0
  117. data/lib/tessella/cli/rule.rb +57 -0
  118. data/lib/tessella/cli/seq.rb +117 -0
  119. data/lib/tessella/cli/sparkline.rb +72 -0
  120. data/lib/tessella/cli/table.rb +167 -0
  121. data/lib/tessella/cli/text.rb +78 -0
  122. data/lib/tessella/cli/tree.rb +100 -0
  123. data/lib/tessella/cli/wave.rb +148 -0
  124. data/lib/tessella/cli.rb +36 -0
  125. data/lib/tessella/color.rb +106 -0
  126. data/lib/tessella/compose.rb +243 -0
  127. data/lib/tessella/compose_fill.rb +176 -0
  128. data/lib/tessella/compose_grid.rb +36 -0
  129. data/lib/tessella/compose_shadow.rb +41 -0
  130. data/lib/tessella/error.rb +13 -0
  131. data/lib/tessella/frame.rb +170 -0
  132. data/lib/tessella/glyphs.rb +106 -0
  133. data/lib/tessella/image.rb +454 -0
  134. data/lib/tessella/image_octant.rb +46 -0
  135. data/lib/tessella/image_petscii.rb +116 -0
  136. data/lib/tessella/kinds/bar.rb +344 -0
  137. data/lib/tessella/kinds/box.rb +213 -0
  138. data/lib/tessella/kinds/box_upright.rb +110 -0
  139. data/lib/tessella/kinds/candlestick.rb +164 -0
  140. data/lib/tessella/kinds/columns.rb +259 -0
  141. data/lib/tessella/kinds/date_axis.rb +102 -0
  142. data/lib/tessella/kinds/error_bars.rb +71 -0
  143. data/lib/tessella/kinds/heatmap.rb +130 -0
  144. data/lib/tessella/kinds/line.rb +173 -0
  145. data/lib/tessella/kinds/multibar.rb +98 -0
  146. data/lib/tessella/kinds/multibar_columns.rb +130 -0
  147. data/lib/tessella/kinds/multibar_fine.rb +45 -0
  148. data/lib/tessella/kinds/multibar_rows.rb +93 -0
  149. data/lib/tessella/kinds/pie.rb +123 -0
  150. data/lib/tessella/kinds/pie_rim.rb +78 -0
  151. data/lib/tessella/kinds/sideways.rb +62 -0
  152. data/lib/tessella/kinds/stem.rb +134 -0
  153. data/lib/tessella/kinds/threshold.rb +73 -0
  154. data/lib/tessella/legend.rb +31 -0
  155. data/lib/tessella/lint/alphabet.rb +56 -0
  156. data/lib/tessella/lint/drift.rb +82 -0
  157. data/lib/tessella/lint/fences.rb +51 -0
  158. data/lib/tessella/lint/geometry.rb +120 -0
  159. data/lib/tessella/lint/geometry_checks.rb +224 -0
  160. data/lib/tessella/lint/lines.rb +115 -0
  161. data/lib/tessella/lint/mixing.rb +61 -0
  162. data/lib/tessella/lint/runner.rb +73 -0
  163. data/lib/tessella/lint/shadows.rb +120 -0
  164. data/lib/tessella/lint/target_rows.rb +66 -0
  165. data/lib/tessella/lint.rb +158 -0
  166. data/lib/tessella/meter.rb +133 -0
  167. data/lib/tessella/meter_upright.rb +43 -0
  168. data/lib/tessella/png.rb +456 -0
  169. data/lib/tessella/raster.rb +92 -0
  170. data/lib/tessella/rmagick_reader.rb +52 -0
  171. data/lib/tessella/rule.rb +59 -0
  172. data/lib/tessella/seq.rb +522 -0
  173. data/lib/tessella/sparkline.rb +57 -0
  174. data/lib/tessella/stats.rb +199 -0
  175. data/lib/tessella/table.rb +547 -0
  176. data/lib/tessella/table_cells.rb +225 -0
  177. data/lib/tessella/target.rb +105 -0
  178. data/lib/tessella/text.rb +76 -0
  179. data/lib/tessella/text_forms.rb +150 -0
  180. data/lib/tessella/tree.rb +221 -0
  181. data/lib/tessella/version.rb +5 -0
  182. data/lib/tessella/wave.rb +353 -0
  183. data/lib/tessella/width.rb +122 -0
  184. data/lib/tessella/width_class.rb +77 -0
  185. data/lib/tessella/width_invisible.rb +66 -0
  186. data/lib/tessella/width_invisible_high.rb +60 -0
  187. data/lib/tessella.rb +94 -0
  188. 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).