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/table.md ADDED
@@ -0,0 +1,347 @@
1
+ # Tables
2
+
3
+ `table` draws rows of text as a framed table, sized to its content,
4
+ with numbers right-aligned.
5
+
6
+ ```ruby
7
+ Tessella.table(rows, header: true, style: nil, ascii: false,
8
+ align: nil, title: nil, width: nil, min_width: 4,
9
+ color: false) # => Block
10
+ Tessella::Table.parse(text, tsv: false) # => Array of rows
11
+ ```
12
+
13
+ ```text
14
+ tessella table [--tsv] [--no-header] [--style S] [--ascii]
15
+ [--align SPEC] [--title T] [--max-width N] [file]
16
+ ```
17
+
18
+ ## Input
19
+
20
+ `rows` is an Array of Arrays of Strings or numbers, the first row the
21
+ header unless `header: false`. A number is drawn as written (`12`,
22
+ `7.5`), and a column of them right-aligns; any other value is refused
23
+ by its position: "row 2, column 1: a cell must be a String or a
24
+ number: nil". `Table.parse` reads CSV with RFC 4180 quoting
25
+ (a quoted field may hold commas, doubled quotes and newlines), or
26
+ tab-separated text with `tsv: true`. The command line reads CSV by
27
+ default and TSV with `--tsv`.
28
+
29
+ ```text
30
+ $ cat kinds.csv
31
+ kind,canvas,notes
32
+ line,braille,"joins points, one series per column"
33
+ bar,none,"negative values split at zero"
34
+ stem,none,"back-to-back with two columns"
35
+ ```
36
+
37
+ ```text
38
+ $ tessella table kinds.csv
39
+ ┌──────┬─────────┬─────────────────────────────────────┐
40
+ │ kind │ canvas │ notes │
41
+ ├──────┼─────────┼─────────────────────────────────────┤
42
+ │ line │ braille │ joins points, one series per column │
43
+ │ bar │ none │ negative values split at zero │
44
+ │ stem │ none │ back-to-back with two columns │
45
+ └──────┴─────────┴─────────────────────────────────────┘
46
+ ```
47
+
48
+ A column whose every body cell is a number is right-aligned:
49
+
50
+ ```text
51
+ $ tessella table langs.csv
52
+ ┌──────┬─────┐
53
+ │ lang │ loc │
54
+ ├──────┼─────┤
55
+ │ ruby │ 12 │
56
+ │ c │ 30 │
57
+ │ rust │ 7.5 │
58
+ └──────┴─────┘
59
+ ```
60
+
61
+ `--no-header` draws every row as body:
62
+
63
+ ```text
64
+ $ tessella table --tsv --no-header sizes.tsv
65
+ ┌──────┬──────┐
66
+ │ name │ size │
67
+ │ lib │ 7876 │
68
+ │ test │ 5120 │
69
+ └──────┴──────┘
70
+ ```
71
+
72
+ ## Alignment and title
73
+
74
+ `--align` takes one letter per column, `l`, `r` or `c`; the library
75
+ also takes an Array of `:left`, `:right`, `:center`. `--title` adds a
76
+ row above the header, centred across the table.
77
+
78
+ ```text
79
+ $ tessella table --title Kinds --align lcr kinds.csv
80
+ ┌──────────────────────────────────────────────────────┐
81
+ │ Kinds │
82
+ ├──────┬─────────┬─────────────────────────────────────┤
83
+ │ kind │ canvas │ notes │
84
+ ├──────┼─────────┼─────────────────────────────────────┤
85
+ │ line │ braille │ joins points, one series per column │
86
+ │ bar │ none │ negative values split at zero │
87
+ │ stem │ none │ back-to-back with two columns │
88
+ └──────┴─────────┴─────────────────────────────────────┘
89
+ ```
90
+
91
+ ## Styles
92
+
93
+ `light` (the default), `heavy`, `double`, `rounded`, the four
94
+ dashed ones (`dashed`, `dashed_fine`, `heavy_dashed`,
95
+ `heavy_dashed_fine`), the two mixed doubles `double_h` and
96
+ `double_v`, `ascii`, `minimal` (dividers only, no outer frame) and
97
+ `markdown` (a GitHub pipe table, ready to paste). The mixed doubles
98
+ bring their own tees, `╤ ╧ ╞ ╪ ╡` and `╥ ╨ ╟ ╫ ╢`:
99
+
100
+ ```text
101
+ $ tessella table --style double_v langs.csv
102
+ ╓──────╥─────╖
103
+ ║ lang ║ loc ║
104
+ ╟──────╫─────╢
105
+ ║ ruby ║ 12 ║
106
+ ║ c ║ 30 ║
107
+ ║ rust ║ 7.5 ║
108
+ ╙──────╨─────╜
109
+ ```
110
+
111
+ ```text
112
+ $ tessella table --style minimal kinds.csv
113
+ ╷ ╷
114
+ kind │ canvas │ notes
115
+ ╶──────┼─────────┼─────────────────────────────────────╴
116
+ line │ braille │ joins points, one series per column
117
+ bar │ none │ negative values split at zero
118
+ stem │ none │ back-to-back with two columns
119
+ ╵ ╵
120
+ ```
121
+
122
+ ```text
123
+ $ tessella table --style markdown kinds.csv
124
+ | kind | canvas | notes |
125
+ |------|---------|-------------------------------------|
126
+ | line | braille | joins points, one series per column |
127
+ | bar | none | negative values split at zero |
128
+ | stem | none | back-to-back with two columns |
129
+ ```
130
+
131
+ ```text
132
+ $ tessella table --style ascii kinds.csv
133
+ +------+---------+-------------------------------------+
134
+ | kind | canvas | notes |
135
+ +------+---------+-------------------------------------+
136
+ | line | braille | joins points, one series per column |
137
+ | bar | none | negative values split at zero |
138
+ | stem | none | back-to-back with two columns |
139
+ +------+---------+-------------------------------------+
140
+ ```
141
+
142
+ ### Custom borders
143
+
144
+ `--style` (`style:`) also takes eleven glyphs: a frame's six (`tl tr
145
+ bl br h v`), then the five tees, in the order `top_t bot_t sep_l
146
+ sep_t sep_r` (the tee on the top edge, on the bottom edge, at the
147
+ left end of the header rule, its crossings, its right end):
148
+
149
+ ```text
150
+ $ tessella table --style '╒╕╘╛═│╤╧╞╪╡' langs.csv
151
+ ╒══════╤═════╕
152
+ │ lang │ loc │
153
+ ╞══════╪═════╡
154
+ │ ruby │ 12 │
155
+ │ c │ 30 │
156
+ │ rust │ 7.5 │
157
+ ╘══════╧═════╛
158
+ ```
159
+
160
+ Six glyphs are not enough for a table:
161
+
162
+ <!-- tessella: allow-mixed -->
163
+ ```text
164
+ $ tessella table --style '╓╖╙╜─║' langs.csv 2>&1 | \
165
+ fold -s -w 72 | sed 's/ $//'
166
+ tessella table: a table's border set needs 11 glyphs, its tees after
167
+ the six of a frame: "╓╖╙╜─║"
168
+ Usage: tessella table [options] [file] (stdin if none)
169
+ --tsv --no-header --align SPEC --style NAME|GLYPHS --[no-]ascii
170
+ --title TEXT --max-width N
171
+ ```
172
+
173
+ ### ASCII
174
+
175
+ Under `ascii: true` a named style draws as `ascii`, except `markdown`,
176
+ which is ASCII already and stays a pipe table; a custom set is drawn
177
+ if it is all ASCII and refused if not. A cell or title that is not
178
+ ASCII is refused. In a markdown table a `|` in a cell is written
179
+ `\|`, so it does not split the cell.
180
+
181
+ ```text
182
+ $ tessella table --ascii --style '****=!+++++' langs.csv
183
+ *======+=====*
184
+ ! lang ! loc !
185
+ +======+=====+
186
+ ! ruby ! 12 !
187
+ ! c ! 30 !
188
+ ! rust ! 7.5 !
189
+ *======+=====*
190
+ ```
191
+
192
+ ## Multi-line cells, and fitting a width
193
+
194
+ These two are library options, not command-line flags.
195
+
196
+ A cell holding `"\n"` draws on several lines within its row,
197
+ top-aligned. `width: N` fits the framed table to N columns. It
198
+ narrows the widest column one column at a time, then whichever is
199
+ widest by then, so wide columns come down together and a narrow one
200
+ is never cut first. The narrowed columns' cells are word-wrapped, a
201
+ word too long for its column breaking inside.
202
+
203
+ ```ruby
204
+ # frozen_string_literal: true
205
+
206
+ # examples/fit.rb: a table fitted to a width. At 40 columns only the
207
+ # widest column narrows and wraps; at 26 it has come down to the next
208
+ # widest, and both narrow together. A cell holding a newline keeps its
209
+ # own lines. Fitted to 20, the table is refused, naming the width it
210
+ # needs.
211
+ require "tessella"
212
+
213
+ rows = Tessella::Table.parse(File.read("kinds.csv"))
214
+ rows << ["box", "none", "quartiles\nand whiskers"]
215
+ [40, 26, 20].each do |width|
216
+ puts Tessella.table(rows, width: width)
217
+ rescue ArgumentError => e
218
+ puts e.message
219
+ end
220
+ ```
221
+
222
+ ```text
223
+ $ ruby fit.rb
224
+ ┌──────┬─────────┬─────────────────────┐
225
+ │ kind │ canvas │ notes │
226
+ ├──────┼─────────┼─────────────────────┤
227
+ │ line │ braille │ joins points, one │
228
+ │ │ │ series per column │
229
+ │ bar │ none │ negative values │
230
+ │ │ │ split at zero │
231
+ │ stem │ none │ back-to-back with │
232
+ │ │ │ two columns │
233
+ │ box │ none │ quartiles │
234
+ │ │ │ and whiskers │
235
+ └──────┴─────────┴─────────────────────┘
236
+ ┌──────┬────────┬────────┐
237
+ │ kind │ canvas │ notes │
238
+ ├──────┼────────┼────────┤
239
+ │ line │ braill │ joins │
240
+ │ │ e │ points │
241
+ │ │ │ , one │
242
+ │ │ │ series │
243
+ │ │ │ per │
244
+ │ │ │ column │
245
+ │ bar │ none │ negati │
246
+ │ │ │ ve │
247
+ │ │ │ values │
248
+ │ │ │ split │
249
+ │ │ │ at │
250
+ │ │ │ zero │
251
+ │ stem │ none │ back-t │
252
+ │ │ │ o-back │
253
+ │ │ │ with │
254
+ │ │ │ two │
255
+ │ │ │ column │
256
+ │ │ │ s │
257
+ │ box │ none │ quarti │
258
+ │ │ │ les │
259
+ │ │ │ and │
260
+ │ │ │ whiske │
261
+ │ │ │ rs │
262
+ └──────┴────────┴────────┘
263
+ table needs 22 columns at the least (min_width 4)
264
+ ```
265
+
266
+ No column narrows below `min_width:` (4 by default), or below its
267
+ own width when that is smaller. When every column is at that floor
268
+ and the table is still too wide, it is refused rather than drawn
269
+ wider than asked, and the message names the width it needs: the last
270
+ line above.
271
+
272
+ `min_width:` may also be an Array, one floor per column: a column
273
+ that must stay readable (a name, an identifier) keeps its width, and
274
+ the narrowing falls on the others. Each column's floor is its own
275
+ value, or its natural width when smaller, and the table is refused
276
+ only when every column is at its floor. The command line has no flag
277
+ for it.
278
+
279
+ ```ruby
280
+ # frozen_string_literal: true
281
+
282
+ # examples/floors.rb: one table fitted to 70 columns twice. With one
283
+ # floor for every column, the variable name is cut with the path; with
284
+ # a floor per column, the names keep their width and the path alone
285
+ # wraps. A list of the wrong length is refused.
286
+ require "tessella"
287
+
288
+ rows = [
289
+ %w[name variable path mode size age],
290
+ ["tado-pairing", "CLIMAT_PAIRING",
291
+ "/home/user/.config/climat-control/tado-pairing.json", "0600",
292
+ "1.2 kB", "3 d"]
293
+ ]
294
+ puts Tessella.table(rows, width: 70, min_width: 4)
295
+ puts Tessella.table(rows, width: 70, min_width: [12, 14, 12, 4, 6, 7])
296
+ begin
297
+ Tessella.table(rows, width: 70, min_width: [12, 14])
298
+ rescue ArgumentError => e
299
+ puts e.message
300
+ end
301
+ ```
302
+
303
+ ```text
304
+ $ ruby floors.rb
305
+ ┌──────────────┬───────────────┬───────────────┬──────┬────────┬─────┐
306
+ │ name │ variable │ path │ mode │ size │ age │
307
+ ├──────────────┼───────────────┼───────────────┼──────┼────────┼─────┤
308
+ │ tado-pairing │ CLIMAT_PAIRIN │ /home/user/.c │ 0600 │ 1.2 kB │ 3 d │
309
+ │ │ G │ onfig/climat- │ │ │ │
310
+ │ │ │ control/tado- │ │ │ │
311
+ │ │ │ pairing.json │ │ │ │
312
+ └──────────────┴───────────────┴───────────────┴──────┴────────┴─────┘
313
+ ┌──────────────┬────────────────┬──────────────┬──────┬────────┬─────┐
314
+ │ name │ variable │ path │ mode │ size │ age │
315
+ ├──────────────┼────────────────┼──────────────┼──────┼────────┼─────┤
316
+ │ tado-pairing │ CLIMAT_PAIRING │ /home/user/. │ 0600 │ 1.2 kB │ 3 d │
317
+ │ │ │ config/clima │ │ │ │
318
+ │ │ │ t-control/ta │ │ │ │
319
+ │ │ │ do-pairing.j │ │ │ │
320
+ │ │ │ son │ │ │ │
321
+ └──────────────┴────────────────┴──────────────┴──────┴────────┴─────┘
322
+ min_width must be a positive Integer or 6 of them: [12, 14]
323
+ ```
324
+
325
+ The `markdown` style refuses multi-line cells and `width:`: a pipe
326
+ table cannot hold either.
327
+
328
+ ## Colour
329
+
330
+ An ANSI colour code in a cell takes no column. It survives padding,
331
+ and wrapping too: it is closed at the end of each wrapped line and
332
+ reopened on the next, so every line stands on its own. `color: true`
333
+ sets the header in bold.
334
+
335
+ ## Errors
336
+
337
+ Bad input is refused naming the line (exit 1): an unterminated
338
+ quoted field, a row with a different number of fields from the
339
+ first, a non-ASCII cell or title under `--ascii`, a table wider than
340
+ `--max-width`.
341
+
342
+ ```text
343
+ $ printf 'a,b\nc\n' | tessella table
344
+ tessella table: line 2: expected 2 fields, got 1
345
+ ```
346
+
347
+ An unknown style or a malformed `--align` is a usage error (exit 2).
data/doc/targets.md ADDED
@@ -0,0 +1,222 @@
1
+ # Width targets
2
+
3
+ A figure lines up only if every glyph takes the columns tessella
4
+ counted, and that depends on where the figure is read. tessella can
5
+ draw for three targets:
6
+
7
+ | Target | Where | Widths |
8
+ |--------|------------------------------|-----------------------------------------------------------|
9
+ | `term` | a Western terminal (default) | ambiguous glyphs 1 column, wide ones (CJK, emoji) 2 |
10
+ | `cjk` | a CJK-configured terminal | ambiguous glyphs 2 columns: box drawing, blocks, `·`, `é` |
11
+ | `web` | a browser | as `term`, but some glyphs may turn into wide emoji |
12
+
13
+ ([Width classes](glyphs.md#width-classes) explains ambiguous and
14
+ wide.)
15
+
16
+ ```text
17
+ tessella --target term|cjk|web COMMAND ...
18
+ TESSELLA_TARGET=cjk tessella COMMAND ...
19
+ ```
20
+
21
+ ```ruby
22
+ Tessella.target = :cjk # for the rest of the fiber
23
+ Tessella::Target.with(:web) { Tessella.table(rows).to_s }
24
+ ```
25
+
26
+ `--target` wins over `TESSELLA_TARGET`, and an empty variable means
27
+ `term`. An unknown target, or `--target` with no name, is a usage
28
+ error, exit 2, even before `--help`:
29
+
30
+ ```text
31
+ $ tessella --target vt100 table langs.csv
32
+ tessella: unknown target: "vt100" (term, cjk, web)
33
+ ```
34
+
35
+ ## term
36
+
37
+ The default. `cities.csv` holds a CJK label, which takes two columns
38
+ everywhere, and an accented one:
39
+
40
+ ```text
41
+ $ tessella table cities.csv
42
+ ┌────────┬─────┐
43
+ │ city │ pop │
44
+ ├────────┼─────┤
45
+ │ 東京 │ 14 │
46
+ │ Paris │ 2 │
47
+ │ Zürich │ 0.4 │
48
+ └────────┴─────┘
49
+ ```
50
+
51
+ An emoji sequence, joined by ZWJ (👨‍👩‍👧) or skin-toned (👍🏽), is
52
+ one glyph two columns wide in every target, as terminals draw it.
53
+
54
+ ## cjk
55
+
56
+ In a CJK-configured terminal box drawing is two columns wide, so a
57
+ Unicode frame cannot line up there. Under `cjk` every module's
58
+ `ascii:` defaults to true: the structure is drawn in ASCII, while the
59
+ text stays as given and is measured with ambiguous glyphs as two
60
+ columns.
61
+
62
+ ```text
63
+ $ tessella --target cjk table cities.csv
64
+ +---------+-----+
65
+ | city | pop |
66
+ +---------+-----+
67
+ | 東京 | 14 |
68
+ | Paris | 2 |
69
+ | Zürich | 0.4 |
70
+ +---------+-----+
71
+ ```
72
+
73
+ Read here, in a Western font, the `Zürich` row looks one column
74
+ short: `ü` is ambiguous, and the table gave it the two columns a CJK
75
+ terminal will. There, the row lines up.
76
+
77
+ `ascii: false` still asks for Unicode structure. A frame and a table
78
+ then round every inner width up to a whole number of border glyphs
79
+ and count their border runs in columns, so each line has one width in
80
+ a CJK terminal and the box closes there. Read in a Western terminal,
81
+ as on this page, the box looks a column too wide here and there.
82
+ Other figures (trees, waves, charts) drawn in Unicode under `cjk` do
83
+ not line up in a CJK terminal: leave them to the ASCII default.
84
+
85
+ On the command line, `--no-ascii` asks for Unicode under `cjk`;
86
+ without it, a Unicode `--style` is refused rather than quietly drawn
87
+ in ASCII:
88
+
89
+ ```text
90
+ $ tessella --target cjk table --no-ascii --style light cities.csv
91
+ ┌─────┬───┐
92
+ │ city │ pop │
93
+ ├─────┼───┤
94
+ │ 東京 │ 14 │
95
+ │ Paris │ 2 │
96
+ │ Zürich │ 0.4 │
97
+ └─────┴───┘
98
+ ```
99
+
100
+ ```text
101
+ $ tessella --target cjk compose frame --style light note.txt 2>&1 | head -1
102
+ tessella compose: --style light needs --no-ascii under target cjk
103
+ ```
104
+
105
+ From Ruby, `ascii: false`:
106
+
107
+ ```ruby
108
+ # frozen_string_literal: true
109
+
110
+ # examples/cjk.rb: a table and a frame asked for in Unicode under the
111
+ # cjk target. Their inner widths are rounded up to whole border
112
+ # glyphs, so they close in a CJK terminal; read in a Western one, as
113
+ # here, the boxes look a column wide here and there.
114
+ require "tessella"
115
+
116
+ Tessella::Target.with(:cjk) do
117
+ rows = Tessella::Table.parse(File.read("cities.csv"))
118
+ puts Tessella.table(rows, ascii: false)
119
+ puts Tessella.frame(File.read("note.txt"), ascii: false)
120
+ end
121
+ ```
122
+
123
+ ```text
124
+ $ ruby cjk.rb
125
+ ┌─────┬───┐
126
+ │ city │ pop │
127
+ ├─────┼───┤
128
+ │ 東京 │ 14 │
129
+ │ Paris │ 2 │
130
+ │ Zürich │ 0.4 │
131
+ └─────┴───┘
132
+ ┌─────────┐
133
+ │ ruby is 12 kLOC │
134
+ │ c is the bulk │
135
+ └─────────┘
136
+ ```
137
+
138
+ ## web
139
+
140
+ A figure read in a browser (a README on GitHub, a page of docs) is at
141
+ risk where it draws an arrow head. The arrow glyphs fall in three
142
+ groups:
143
+
144
+ | Glyphs | On the web |
145
+ |--------------------|-------------------------------------------------------|
146
+ | `▸ ◂ ▴ ▾` | text glyphs most monospace fonts carry: they line up |
147
+ | `→ ← ↑ ↓` | text glyphs most monospace fonts carry: they line up |
148
+ | `► ◄` | not emoji-capable, bolder; some fonts lack them |
149
+ | `▶ ◀ ↔ ↕ ↗ ↘ ↙ ↖` | emoji-capable: may become a wide colour emoji |
150
+
151
+ So on the web, end a drawn line with a small head, `────▸`, or a
152
+ bolder `────►`, and keep `→ ← ↑ ↓` for arrows standing alone. `► ◄`
153
+ never turn into emoji, but a font without them still draws them at a
154
+ fallback width: they trade a little of `▸`'s safety for a head that
155
+ reads at a glance.
156
+
157
+ tessella's sequence diagrams use the bolder heads in every target,
158
+ terminal included: `►` and `◄` for calls and replies, `►►` and `◄◄`
159
+ for asynchronous messages ([sequence diagrams](seq.md#arrow-heads)).
160
+ `tessella glyphs head` lists the heads with the targets each suits
161
+ ([glyph sets](glyphs.md#arrow-heads)).
162
+
163
+ When a figure does carry an emoji-capable glyph, `--target web` puts
164
+ U+FE0E, the text-presentation selector, right after it. The selector
165
+ is invisible; here it is made visible as `[FE0E]`:
166
+
167
+ ```text
168
+ $ tessella --target web rule --width 24 --ornament '▶ next' | \
169
+ ruby -pe 'gsub("\u{FE0E}", "[FE0E]")'
170
+ ──────── ▶[FE0E] next ────────
171
+ ```
172
+
173
+ It stops the browser drawing the glyph as an emoji, and is the one
174
+ exception to tessella's rule of no invisible characters in its
175
+ output. It does not fix the glyph's width: a glyph the page's
176
+ monospace font lacks is drawn from a fallback font, at that font's
177
+ width. In Chrome on GitHub's code fonts `▶` takes two columns and
178
+ `↗` 1.4, with or without the selector. On a row that must line up,
179
+ the selector is not enough; `▸` is the arrow head that lines up.
180
+
181
+ The same holds beyond arrows. Measured in Chrome on GitHub's code
182
+ fonts, box drawing, quadrant and half blocks, eighths and shades are
183
+ exactly one column, but braille is about 1.2 and `◆` two. So under
184
+ `--target web` a chart defaults to quadrant blocks instead of braille
185
+ (`--canvas braille` still asks for it), the fill pattern that was `◆`
186
+ is `■`, and `tessella lint --target web` flags a row of braille.
187
+
188
+ `tessella lint --target web` (the same as `--web`) checks a figure
189
+ for this. It flags a bare emoji-capable glyph, and accepts one
190
+ followed by U+FE0E:
191
+
192
+ ```text
193
+ $ printf 'A ──▶ B\n' | tessella lint --target web
194
+ stdin:1: emoji-capable U+25B6 ▶ (web target)
195
+ ```
196
+
197
+ ```text
198
+ $ printf 'A ──▸ B\n' | tessella lint --target web && echo clean
199
+ clean
200
+ ```
201
+
202
+ To add the selector to a figure edited by hand, use a tool rather
203
+ than typing an invisible character:
204
+
205
+ ```sh
206
+ ruby -pi -e 'gsub(/([▶◀])(?!\u{FE0E})/, "\\1\u{FE0E}")' FIGURE.md
207
+ ```
208
+
209
+ ## Checking for a target
210
+
211
+ `tessella lint --target T` measures a figure with the target's widths
212
+ ([lint](lint.md#the-target)): under `cjk` it names an ambiguous glyph
213
+ on a row that must line up.
214
+
215
+ ```text
216
+ $ tessella table cities.csv | tessella lint --target cjk
217
+ stdin:6: ambiguous U+00FC ü on aligned row (cjk target: 2 columns)
218
+ ```
219
+
220
+ `tessella glyphs --target T` lists the glyph sets safe for a target,
221
+ and `--check` says which targets a set of one's own suits
222
+ ([glyph sets](glyphs.md)).
data/doc/text.md ADDED
@@ -0,0 +1,84 @@
1
+ # Text styles
2
+
3
+ `text` writes letters and digits in one of Unicode's styled
4
+ alphabets: 𝐛𝐨𝐥𝐝, 𝑖𝑡𝑎𝑙𝑖𝑐, 𝔣𝔯𝔞𝔨𝔱𝔲𝔯, ℍ𝕖𝕝𝕝𝕠, Sᴍᴀʟʟ ᴄᴀᴘꜱ, ⓒⓘⓡⓒⓛⓔⓓ and the
5
+ rest. They are characters, not formatting, so they survive where no
6
+ markup does: a terminal banner, a commit subject, a plain-text title.
7
+
8
+ ```ruby
9
+ Tessella.text("Hello", style: :double_struck) # => "ℍ𝕖𝕝𝕝𝕠"
10
+ Tessella.text("ℍ𝕖𝕝𝕝𝕠", style: :plain) # => "Hello"
11
+ ```
12
+
13
+ ```text
14
+ tessella text --style S [--strict] [TEXT ...]
15
+ tessella text --plain [TEXT ...]
16
+ ```
17
+
18
+ The command styles its arguments, joined by blanks, or stdin when
19
+ there are none:
20
+
21
+ ```text
22
+ $ tessella text --style bold Hello
23
+ 𝐇𝐞𝐥𝐥𝐨
24
+ ```
25
+
26
+ ```text
27
+ $ tessella text --style double-struck Hello 2024
28
+ ℍ𝕖𝕝𝕝𝕠 𝟚𝟘𝟚𝟜
29
+ ```
30
+
31
+ ```text
32
+ $ echo hi | tessella text --style fullwidth
33
+ hi
34
+ ```
35
+
36
+ ## The styles
37
+
38
+ `bold`, `italic`, `bold-italic`, `script`, `bold-script`, `fraktur`,
39
+ `bold-fraktur`, `double-struck`, `sans`, `sans-bold`, `sans-italic`,
40
+ `sans-bold-italic`, `monospace`, `small-caps`, `superscript`,
41
+ `subscript`, `circled`, `negative-circled`, `parenthesized`,
42
+ `squared`, `negative-squared` and `fullwidth`. In Ruby a style is a
43
+ Symbol with `_` for `-`: `:double_struck`.
44
+
45
+ The forms come from Unicode's own data; the gaps in the mathematical
46
+ alphabets are filled from Letterlike Symbols (`ℎ`, `ℬ`, `ℂ`, ...), as
47
+ Unicode intends. Capitals keep their case in small caps; `squared`
48
+ and the two negative styles have capitals only. `fullwidth` glyphs
49
+ are two columns wide.
50
+
51
+ `--plain` (`style: :plain`) turns any of them back into plain
52
+ letters and digits:
53
+
54
+ ```text
55
+ $ tessella text --plain 'ℍ𝕖𝕝𝕝𝕠 𝟚𝟘𝟚𝟜'
56
+ Hello 2024
57
+ ```
58
+
59
+ ## Characters without a form
60
+
61
+ Not every style has every character: digits exist only in `bold`,
62
+ `double-struck`, `sans`, `sans-bold` and `monospace`; small caps have
63
+ no `x`; most letters have no subscript. Such a character passes
64
+ through unchanged, and the command names it on stderr:
65
+
66
+ ```text
67
+ $ tessella text --style small-caps 'Hello x'
68
+ tessella text: no small-caps form for: x
69
+ Hᴇʟʟᴏ x
70
+ ```
71
+
72
+ `--strict` (`strict: true`) refuses the text instead: the command
73
+ prints nothing and exits 1.
74
+
75
+ ```text
76
+ $ tessella text --style small-caps --strict 'Hello x'
77
+ tessella text: no small-caps form for: x
78
+ ```
79
+
80
+ ## When not to
81
+
82
+ Every style is non-ASCII, so `--ascii` refuses them all. And they are
83
+ for a decorative line, not for prose: a screen reader spells them out
84
+ or skips them, and a search for "Hello" does not find "ℍ𝕖𝕝𝕝𝕠".