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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
module Table
|
|
5
|
+
# Cells as lines (DESIGN.md, "Diagram modules"): a cell's "\n"
|
|
6
|
+
# splits it into lines within its row, and every cell of a row
|
|
7
|
+
# is padded with empty lines to the row's tallest, top-aligned.
|
|
8
|
+
module Cells
|
|
9
|
+
def self.lines(cell)
|
|
10
|
+
lines = Table.sanitize(cell.to_s).split("\n", -1)
|
|
11
|
+
lines.empty? ? [""] : lines
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.grid(rows) = rows.map { |row| row.map { lines(it) } }
|
|
15
|
+
|
|
16
|
+
def self.multiline?(grid) = grid.any? { |r| r.any? { it.size > 1 } }
|
|
17
|
+
|
|
18
|
+
def self.width(cell_lines) = cell_lines.map { Width.of(it) }.max
|
|
19
|
+
|
|
20
|
+
# A title: one line, tabs as blanks.
|
|
21
|
+
def self.title_text(title)
|
|
22
|
+
title && Table.sanitize(Compose.text!("title", title, false))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A markdown cell: tabs as blanks, a | escaped, as a GFM pipe
|
|
26
|
+
# table needs.
|
|
27
|
+
def self.md_cell(text) = Table.sanitize(text).gsub("|", "\\|")
|
|
28
|
+
|
|
29
|
+
# One row of cells (each an Array of lines) as Arrays of
|
|
30
|
+
# line strings, one Array per output line.
|
|
31
|
+
def self.rows_of(row)
|
|
32
|
+
height = row.map(&:size).max
|
|
33
|
+
(0...height).map { |i| row.map { it[i] || "" } }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
BOLD = "\e[1m"
|
|
37
|
+
RESET = "\e[0m"
|
|
38
|
+
|
|
39
|
+
def self.bold(cell_lines)
|
|
40
|
+
cell_lines.map { it.empty? ? it : "#{BOLD}#{it}#{RESET}" }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The header row in bold when colour is on.
|
|
44
|
+
def self.header(row, color)
|
|
45
|
+
color && row ? row.map { bold(it) } : row
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.column_widths(grid)
|
|
49
|
+
grid.transpose.map { |col| col.map { width(it) }.max + 2 }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.check_markdown!(head, body, fit)
|
|
53
|
+
return unless fit[:width] ||
|
|
54
|
+
multiline?(grid([head, *body].compact))
|
|
55
|
+
|
|
56
|
+
raise ArgumentError,
|
|
57
|
+
"markdown has no multi-line cells and no width: fitting"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A framed table's glyphs and widths under the width target:
|
|
62
|
+
# under :cjk a Unicode glyph takes two columns, so widths go in
|
|
63
|
+
# whole glyphs and edges are padded to the widest glyph.
|
|
64
|
+
module Edges
|
|
65
|
+
# width: as Fit counts it, one column a border glyph: less, under
|
|
66
|
+
# :cjk, the columns its two-column glyphs and the rounding of
|
|
67
|
+
# each column to whole glyphs may add.
|
|
68
|
+
def self.fit_width(fit, g, widths)
|
|
69
|
+
unit = unit(g)
|
|
70
|
+
fit[:width] &&
|
|
71
|
+
(fit[:width] - (((2 * widths.size) + 1) * (unit - 1)))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The style's glyphs, an edge or joint narrower than the widest
|
|
75
|
+
# glyph padded to its columns, so a minimal table's blanks and
|
|
76
|
+
# its narrow โถ โท keep up with its two-column rules under :cjk
|
|
77
|
+
# (the runs, h and sep_h, are repeated to the column instead).
|
|
78
|
+
def self.glyphs(style)
|
|
79
|
+
g = Glyphs.fetch(style)
|
|
80
|
+
unit = unit(g)
|
|
81
|
+
g.to_h do |role, glyph|
|
|
82
|
+
runs = %i[h sep_h].include?(role)
|
|
83
|
+
[role, runs ? glyph : Width.ljust(glyph, unit)]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.unit(g) = g.values.map { Width.of(it) }.max
|
|
88
|
+
|
|
89
|
+
# The widths, the last column widened when the title would not
|
|
90
|
+
# fit the span with a blank either side.
|
|
91
|
+
def self.titled(widths, title, g)
|
|
92
|
+
return widths unless title
|
|
93
|
+
|
|
94
|
+
short = Width.of(title) + 2 - Render.span_width(widths, g[:cs])
|
|
95
|
+
short.positive? ? [*widths[..-2], widths[-1] + short] : widths
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Fitting a width (width:, min_width:): when the framed table is
|
|
100
|
+
# wider than width, the widest column narrows by the overflow
|
|
101
|
+
# and its cells wrap to it -- min_width: one floor for every
|
|
102
|
+
# column, or an Array of one a column; below them it
|
|
103
|
+
# is refused, naming the width the table needs at the least.
|
|
104
|
+
module Fit
|
|
105
|
+
def self.apply(grid, widths, width, min_width)
|
|
106
|
+
floors = min_widths!(min_width, widths.size)
|
|
107
|
+
total = Render.span_width(widths) + 2
|
|
108
|
+
return [grid, widths] if width.nil? || total <= width
|
|
109
|
+
|
|
110
|
+
narrow(grid, widths, total - width, [floors, min_width])
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# min_width: one floor for every column, or an Array of one a
|
|
114
|
+
# column; each a positive Integer.
|
|
115
|
+
def self.min_widths!(min_width, ncols)
|
|
116
|
+
floors = Array(min_width)
|
|
117
|
+
floors *= ncols unless min_width.is_a?(Array)
|
|
118
|
+
good = floors.all? { it.is_a?(Integer) && it.positive? }
|
|
119
|
+
return floors if good && floors.size == ncols
|
|
120
|
+
|
|
121
|
+
raise ArgumentError, "min_width must be a positive Integer " \
|
|
122
|
+
"or #{ncols} of them: #{min_width.inspect}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Over columns come off the widest column still above its
|
|
126
|
+
# floor, one at a time, so widths converge downwards and a
|
|
127
|
+
# narrow column is never narrowed first. A column's floor is
|
|
128
|
+
# min_width, or its natural width when that is smaller; the
|
|
129
|
+
# table is refused only when every column is at its floor.
|
|
130
|
+
def self.narrow(grid, widths, over, mins)
|
|
131
|
+
per_column, min_width = mins
|
|
132
|
+
texts = widths.map { it - 2 }
|
|
133
|
+
floors = texts.zip(per_column).map(&:min)
|
|
134
|
+
target = texts.sum - over
|
|
135
|
+
check_room!(floors, target, min_width)
|
|
136
|
+
fitted = level(texts, floors, target)
|
|
137
|
+
[grid.map { wrap_columns(it, texts, fitted) },
|
|
138
|
+
fitted.map { it + 2 }]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.level(texts, floors, target)
|
|
142
|
+
fitted = texts.dup
|
|
143
|
+
while fitted.sum > target
|
|
144
|
+
i = fitted.each_index.select { fitted[it] > floors[it] }
|
|
145
|
+
.max_by { fitted[it] }
|
|
146
|
+
fitted[i] -= 1
|
|
147
|
+
end
|
|
148
|
+
fitted
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def self.check_room!(floors, target, min_width)
|
|
152
|
+
return if floors.sum <= target
|
|
153
|
+
|
|
154
|
+
least = Render.span_width(floors.map { it + 2 }) + 2
|
|
155
|
+
raise ArgumentError, "table needs #{least} columns at the " \
|
|
156
|
+
"least (min_width #{min_width})"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Every narrowed column's cells wrapped to its new width.
|
|
160
|
+
def self.wrap_columns(row, texts, fitted)
|
|
161
|
+
row.each_with_index.map do |cell, i|
|
|
162
|
+
next cell if fitted[i] == texts[i]
|
|
163
|
+
|
|
164
|
+
cell.flat_map { Wrap.line(it, fitted[i]) }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Word wrap by display width, ANSI colour kept intact: a word
|
|
170
|
+
# longer than the column breaks inside, and a colour still open
|
|
171
|
+
# at a line's end is closed there and reopened on the next line,
|
|
172
|
+
# so every line stands alone.
|
|
173
|
+
module Wrap
|
|
174
|
+
SGR = /\e\[[0-9;]*m/
|
|
175
|
+
RESETS = ["\e[0m", "\e[m"].freeze
|
|
176
|
+
|
|
177
|
+
def self.line(text, cols)
|
|
178
|
+
return [text] if Width.of(text) <= cols
|
|
179
|
+
|
|
180
|
+
recolour(pack(words(text, cols), cols))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def self.words(text, cols)
|
|
184
|
+
text.split.flat_map do |word|
|
|
185
|
+
Width.of(word) > cols ? chop(word, cols) : [word]
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# A word too long for the column, in pieces of cols columns;
|
|
190
|
+
# an SGR sequence stays whole, attached to the next glyph.
|
|
191
|
+
def self.chop(word, cols)
|
|
192
|
+
glyphs = word.scan(/(?:#{SGR})*(?:.(?:#{SGR})*)/o)
|
|
193
|
+
glyphs.each_with_object([+""]) do |g, out|
|
|
194
|
+
Width.of(out.last + g) > cols ? out << g.dup : out.last << g
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.pack(words, cols)
|
|
199
|
+
words.each_with_object([]) do |w, out|
|
|
200
|
+
if out.any? && Width.of("#{out.last} #{w}") <= cols
|
|
201
|
+
out.last << " " << w
|
|
202
|
+
else
|
|
203
|
+
out << w.dup
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def self.recolour(lines)
|
|
209
|
+
open = []
|
|
210
|
+
lines.map do |l|
|
|
211
|
+
text = open.join + l
|
|
212
|
+
open = active(text)
|
|
213
|
+
open.empty? ? text : text + Cells::RESET
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# The SGR sequences still in effect at the end of text.
|
|
218
|
+
def self.active(text)
|
|
219
|
+
text.scan(SGR).each_with_object([]) do |seq, on|
|
|
220
|
+
RESETS.include?(seq) ? on.clear : on << seq
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
# Where a figure will be read (DESIGN.md, "Width targets"), which
|
|
5
|
+
# decides what a glyph's width is and which glyphs are safe:
|
|
6
|
+
#
|
|
7
|
+
# :term a terminal (the default): ambiguous glyphs -- box
|
|
8
|
+
# drawing, blocks, ยท โ โ
, Greek, accented Latin -- one
|
|
9
|
+
# column, wide ones two.
|
|
10
|
+
# :cjk a CJK-configured terminal: ambiguous glyphs two columns
|
|
11
|
+
# too, so figures draw their structure in ASCII (ascii:
|
|
12
|
+
# defaults to true, text stays as given) and measure text
|
|
13
|
+
# with ambiguous glyphs as two.
|
|
14
|
+
# :web a browser: as :term, but emoji-capable glyphs (โถ โ โ ...)
|
|
15
|
+
# may turn into wide emoji, so a figure's text carries
|
|
16
|
+
# U+FE0E, the text-presentation selector, after each one.
|
|
17
|
+
# That keeps it a text glyph, not its width: a glyph the
|
|
18
|
+
# viewer's monospace font lacks takes its fallback font's
|
|
19
|
+
# advance (โถ two columns, โ 1.4 in Chrome on GitHub's font
|
|
20
|
+
# stack), so only glyphs that font carries line up.
|
|
21
|
+
#
|
|
22
|
+
# The target is set for a block of code (Target.with) or for the
|
|
23
|
+
# rest of the fiber (Tessella.target=); the command line takes
|
|
24
|
+
# `tessella --target T COMMAND` or TESSELLA_TARGET.
|
|
25
|
+
module Target
|
|
26
|
+
TARGETS = %i[term cjk web].freeze
|
|
27
|
+
TEXT_STYLE = "\u{FE0E}"
|
|
28
|
+
|
|
29
|
+
# Emoji-capable with text-default presentation (Emoji=Yes minus
|
|
30
|
+
# Emoji_Presentation=Yes, BMP; digits/#/* excluded): one cell in
|
|
31
|
+
# terminals, but web viewers may substitute the 2-cell emoji
|
|
32
|
+
# glyph. Generated from Unicode 15.1 emoji-data.txt.
|
|
33
|
+
EMOJI_RISK = [
|
|
34
|
+
0x00A9..0x00A9, 0x00AE..0x00AE, 0x203C..0x203C, 0x2049..0x2049,
|
|
35
|
+
0x2122..0x2122, 0x2139..0x2139, 0x2194..0x2199, 0x21A9..0x21AA,
|
|
36
|
+
0x2328..0x2328, 0x23CF..0x23CF, 0x23ED..0x23EF, 0x23F1..0x23F2,
|
|
37
|
+
0x23F8..0x23FA, 0x24C2..0x24C2, 0x25AA..0x25AB, 0x25B6..0x25B6,
|
|
38
|
+
0x25C0..0x25C0, 0x25FB..0x25FC, 0x2600..0x2604, 0x260E..0x260E,
|
|
39
|
+
0x2611..0x2611, 0x2618..0x2618, 0x261D..0x261D, 0x2620..0x2620,
|
|
40
|
+
0x2622..0x2623, 0x2626..0x2626, 0x262A..0x262A, 0x262E..0x262F,
|
|
41
|
+
0x2638..0x263A, 0x2640..0x2640, 0x2642..0x2642, 0x265F..0x2660,
|
|
42
|
+
0x2663..0x2663, 0x2665..0x2666, 0x2668..0x2668, 0x267B..0x267B,
|
|
43
|
+
0x267E..0x267E, 0x2692..0x2692, 0x2694..0x2697, 0x2699..0x2699,
|
|
44
|
+
0x269B..0x269C, 0x26A0..0x26A0, 0x26A7..0x26A7, 0x26B0..0x26B1,
|
|
45
|
+
0x26C8..0x26C8, 0x26CF..0x26CF, 0x26D1..0x26D1, 0x26D3..0x26D3,
|
|
46
|
+
0x26E9..0x26E9, 0x26F0..0x26F1, 0x26F4..0x26F4, 0x26F7..0x26F9,
|
|
47
|
+
0x2702..0x2702, 0x2708..0x2709, 0x270C..0x270D, 0x270F..0x270F,
|
|
48
|
+
0x2712..0x2712, 0x2714..0x2714, 0x2716..0x2716, 0x271D..0x271D,
|
|
49
|
+
0x2721..0x2721, 0x2733..0x2734, 0x2744..0x2744, 0x2747..0x2747,
|
|
50
|
+
0x2763..0x2764, 0x27A1..0x27A1, 0x2934..0x2935, 0x2B05..0x2B07,
|
|
51
|
+
0x3030..0x3030, 0x303D..0x303D, 0x3297..0x3297, 0x3299..0x3299
|
|
52
|
+
].freeze
|
|
53
|
+
def self.current = Fiber[:tessella_target] || :term
|
|
54
|
+
def self.cjk? = current == :cjk
|
|
55
|
+
def self.web? = current == :web
|
|
56
|
+
|
|
57
|
+
# ascii:'s default: true under :cjk, whose terminals give box
|
|
58
|
+
# drawing two columns.
|
|
59
|
+
def self.ascii? = cjk?
|
|
60
|
+
|
|
61
|
+
# A chart's canvas by default: braille, but quadrant blocks
|
|
62
|
+
# under :web, where a browser draws braille from a fallback
|
|
63
|
+
# font a fifth wider than a column (measured in Chrome on
|
|
64
|
+
# GitHub's code fonts), so a braille chart's rows run unequal.
|
|
65
|
+
def self.canvas = web? ? :block : :braille
|
|
66
|
+
|
|
67
|
+
# Text under ascii: true must be ASCII, except under :cjk,
|
|
68
|
+
# where ascii: only means the structure.
|
|
69
|
+
def self.text_free? = cjk?
|
|
70
|
+
|
|
71
|
+
def self.check!(target)
|
|
72
|
+
sym = target.to_s.to_sym
|
|
73
|
+
return sym if TARGETS.include?(sym)
|
|
74
|
+
|
|
75
|
+
raise ArgumentError, "unknown target: #{target.inspect} " \
|
|
76
|
+
"(#{TARGETS.join(", ")})"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.with(target)
|
|
80
|
+
before = Fiber[:tessella_target]
|
|
81
|
+
Fiber[:tessella_target] = check!(target)
|
|
82
|
+
yield
|
|
83
|
+
ensure
|
|
84
|
+
Fiber[:tessella_target] = before
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.emoji_risk?(code) = EMOJI_RISK.any? { it.cover?(code) }
|
|
88
|
+
|
|
89
|
+
# Under :web, U+FE0E after every emoji-capable glyph not
|
|
90
|
+
# already followed by a presentation selector; else as is.
|
|
91
|
+
def self.adapt(text)
|
|
92
|
+
return text unless web?
|
|
93
|
+
|
|
94
|
+
text.gsub(/(.)(?![\u{FE0E}\u{FE0F}])/) do |ch|
|
|
95
|
+
emoji_risk?(ch.ord) ? ch + TEXT_STYLE : ch
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.target = Target.current
|
|
101
|
+
|
|
102
|
+
def self.target=(target)
|
|
103
|
+
Fiber[:tessella_target] = Target.check!(target)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
# Text styles (DESIGN.md, "Text styles"): plain letters and digits
|
|
5
|
+
# to the Unicode forms of a style -- bold, italic, script, fraktur,
|
|
6
|
+
# double-struck, sans, monospace, small caps, superscript,
|
|
7
|
+
# subscript, circled, parenthesized, squared, fullwidth -- and back
|
|
8
|
+
# (style: :plain). A character the style has no form for passes
|
|
9
|
+
# through unchanged; strict: true refuses the text instead, naming
|
|
10
|
+
# them. Only letters and digits count as missing: punctuation and
|
|
11
|
+
# spaces are expected to pass. Small caps keep capitals as they
|
|
12
|
+
# are; the styles with capitals only use them for small letters
|
|
13
|
+
# too. Every style is non-ASCII, so ascii: true refuses them all.
|
|
14
|
+
# The output is text, measured by Width like any other: fullwidth
|
|
15
|
+
# is two columns a glyph.
|
|
16
|
+
module Text
|
|
17
|
+
CAPITALS_ONLY = %i[squared negative_circled negative_squared].freeze
|
|
18
|
+
STYLES = (FORMS.keys + %i[plain]).freeze
|
|
19
|
+
|
|
20
|
+
def self.convert(text, style:, strict: false, ascii: false)
|
|
21
|
+
name = style!(style)
|
|
22
|
+
raise ArgumentError, "text styles are not ASCII" if ascii
|
|
23
|
+
|
|
24
|
+
missing = unmapped(text, name)
|
|
25
|
+
if strict && !missing.empty?
|
|
26
|
+
raise ArgumentError, "no #{dashed(name)} form for: " \
|
|
27
|
+
"#{missing.join(" ")}"
|
|
28
|
+
end
|
|
29
|
+
text.to_s.each_char.map { form(it, name) }.join
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The letters and digits of text the style has no form for.
|
|
33
|
+
def self.unmapped(text, style)
|
|
34
|
+
name = style!(style)
|
|
35
|
+
return [] if name == :plain
|
|
36
|
+
|
|
37
|
+
text.to_s.each_char.select do |ch|
|
|
38
|
+
ch.match?(/[A-Za-z0-9]/) && form(ch, name) == ch &&
|
|
39
|
+
!(name == :small_caps && ch.match?(/[A-Z]/))
|
|
40
|
+
end.uniq
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.dashed(name) = name.to_s.tr("_", "-")
|
|
44
|
+
|
|
45
|
+
def self.style!(style)
|
|
46
|
+
name = Borders.name(style)
|
|
47
|
+
return name if STYLES.include?(name)
|
|
48
|
+
|
|
49
|
+
raise ArgumentError, "unknown text style: #{style.inspect} " \
|
|
50
|
+
"(#{STYLES.join(", ").tr("_", "-")})"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.form(char, name)
|
|
54
|
+
return plain_map.fetch(char, char) if name == :plain
|
|
55
|
+
|
|
56
|
+
key = CAPITALS_ONLY.include?(name) ? char.upcase : char
|
|
57
|
+
map(name).fetch(key, char)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.map(name)
|
|
61
|
+
(@maps ||= {})[name] ||= FORMS.fetch(name).then do |form|
|
|
62
|
+
form[:from].chars.zip(form[:to].chars).to_h
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Every style's forms back to their plain characters.
|
|
67
|
+
def self.plain_map
|
|
68
|
+
@plain_map ||= FORMS.keys.each_with_object({}) do |name, back|
|
|
69
|
+
map(name).each { |plain, styled| back[styled] ||= plain }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# (text, style:, strict: false, ascii: false) => String.
|
|
75
|
+
def self.text(text, **) = Text.convert(text, **)
|
|
76
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tessella
|
|
4
|
+
# Each text style as { from:, to: }: the plain characters it has a
|
|
5
|
+
# form for, and those forms, in the same order -- generated from
|
|
6
|
+
# Unicode 16's names and decompositions (<font>, <super>, <sub>,
|
|
7
|
+
# <circle>, <wide>, <square>), never by code point offsets. The
|
|
8
|
+
# Mathematical Alphanumeric holes are filled from Letterlike
|
|
9
|
+
# Symbols (โ โฌ โฐ โฑ โ โ โ โณ โ โฏ โ โด โญ โ โ โ โจ โ โ โ โ โ โ โค); the
|
|
10
|
+
# ordinal indicators (ยช ยบ) are not superscripts.
|
|
11
|
+
module Text
|
|
12
|
+
# The Mathematical Alphanumeric Symbols styles.
|
|
13
|
+
module MathForms
|
|
14
|
+
FORMS = {
|
|
15
|
+
bold: {
|
|
16
|
+
from: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcd" \
|
|
17
|
+
"efghijklmnopqrstuvwxyz",
|
|
18
|
+
to: "๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐
๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐" \
|
|
19
|
+
"๐๐๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ"
|
|
20
|
+
},
|
|
21
|
+
italic: {
|
|
22
|
+
from: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn" \
|
|
23
|
+
"opqrstuvwxyz",
|
|
24
|
+
to: "๐ด๐ต๐ถ๐ท๐ธ๐น๐บ๐ป๐ผ๐ฝ๐พ๐ฟ๐๐๐๐๐๐
๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐โ๐๐๐๐๐๐" \
|
|
25
|
+
"๐๐๐๐๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง"
|
|
26
|
+
},
|
|
27
|
+
bold_italic: {
|
|
28
|
+
from: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn" \
|
|
29
|
+
"opqrstuvwxyz",
|
|
30
|
+
to: "๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐น๐บ๐ป๐ผ๐ฝ๐พ๐ฟ๐๐๐๐๐๐
๐๐๐๐๐๐๐๐๐๐" \
|
|
31
|
+
"๐๐๐๐๐๐๐๐๐๐๐๐"
|
|
32
|
+
},
|
|
33
|
+
script: {
|
|
34
|
+
from: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn" \
|
|
35
|
+
"opqrstuvwxyz",
|
|
36
|
+
to: "๐โฌ๐๐โฐโฑ๐ขโโ๐ฅ๐ฆโโณ๐ฉ๐ช๐ซ๐ฌโ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐นโฏ๐ปโ๐ฝ๐พ๐ฟ๐๐๐๐" \
|
|
37
|
+
"โด๐
๐๐๐๐๐๐๐๐๐๐"
|
|
38
|
+
},
|
|
39
|
+
bold_script: {
|
|
40
|
+
from: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn" \
|
|
41
|
+
"opqrstuvwxyz",
|
|
42
|
+
to: "๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท" \
|
|
43
|
+
"๐ธ๐น๐บ๐ป๐ผ๐ฝ๐พ๐ฟ๐๐๐๐"
|
|
44
|
+
},
|
|
45
|
+
fraktur: {
|
|
46
|
+
from: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn" \
|
|
47
|
+
"opqrstuvwxyz",
|
|
48
|
+
to: "๐๐
โญ๐๐๐๐โโ๐๐๐๐๐๐๐๐โ๐๐๐๐๐๐๐โจ๐๐๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ" \
|
|
49
|
+
"๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท"
|
|
50
|
+
},
|
|
51
|
+
bold_fraktur: {
|
|
52
|
+
from: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn" \
|
|
53
|
+
"opqrstuvwxyz",
|
|
54
|
+
to: "๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐น๐บ๐ป๐ผ๐ฝ๐พ๐ฟ๐๐๐๐๐๐
๐๐๐๐๐๐๐๐๐๐๐๐๐๐" \
|
|
55
|
+
"๐๐๐๐๐๐๐๐๐๐๐๐"
|
|
56
|
+
},
|
|
57
|
+
double_struck: {
|
|
58
|
+
from: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcd" \
|
|
59
|
+
"efghijklmnopqrstuvwxyz",
|
|
60
|
+
to: "๐๐๐๐๐๐๐๐๐ ๐ก๐ธ๐นโ๐ป๐ผ๐ฝ๐พโ๐๐๐๐๐โ๐โโโ๐๐๐๐๐๐๐โค๐๐๐๐" \
|
|
61
|
+
"๐๐๐๐๐๐๐๐๐๐๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ"
|
|
62
|
+
},
|
|
63
|
+
sans: {
|
|
64
|
+
from: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcd" \
|
|
65
|
+
"efghijklmnopqrstuvwxyz",
|
|
66
|
+
to: "๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐น๐บ๐ป๐ผ๐ฝ" \
|
|
67
|
+
"๐พ๐ฟ๐๐๐๐๐๐
๐๐๐๐๐๐๐๐๐๐๐๐๐๐"
|
|
68
|
+
},
|
|
69
|
+
sans_bold: {
|
|
70
|
+
from: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcd" \
|
|
71
|
+
"efghijklmnopqrstuvwxyz",
|
|
72
|
+
to: "๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐๐๐๐๐๐๐๐๐๐๐๐๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ" \
|
|
73
|
+
"๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐น๐บ๐ป๐ผ๐ฝ๐พ๐ฟ๐๐๐๐๐๐
๐๐"
|
|
74
|
+
},
|
|
75
|
+
sans_italic: {
|
|
76
|
+
from: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn" \
|
|
77
|
+
"opqrstuvwxyz",
|
|
78
|
+
to: "๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ" \
|
|
79
|
+
"๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐น๐บ๐ป"
|
|
80
|
+
},
|
|
81
|
+
sans_bold_italic: {
|
|
82
|
+
from: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn" \
|
|
83
|
+
"opqrstuvwxyz",
|
|
84
|
+
to: "๐ผ๐ฝ๐พ๐ฟ๐๐๐๐๐๐
๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐ ๐ก๐ข๐ฃ" \
|
|
85
|
+
"๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ"
|
|
86
|
+
},
|
|
87
|
+
monospace: {
|
|
88
|
+
from: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcd" \
|
|
89
|
+
"efghijklmnopqrstuvwxyz",
|
|
90
|
+
to: "๐ถ๐ท๐ธ๐น๐บ๐ป๐ผ๐ฝ๐พ๐ฟ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐น๐บ๐ป๐ผ๐ฝ๐พ๐ฟ๐๐๐๐๐๐
๐๐๐๐๐๐๐๐" \
|
|
91
|
+
"๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐ ๐ก๐ข๐ฃ"
|
|
92
|
+
}
|
|
93
|
+
}.freeze
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Small caps, super/subscripts, enclosed and fullwidth forms.
|
|
97
|
+
module OtherForms
|
|
98
|
+
FORMS = {
|
|
99
|
+
small_caps: {
|
|
100
|
+
from: "abcdefghijklmnopqrstuvwyz",
|
|
101
|
+
to: "แดสแดแด
แด๊ฐษขสษชแดแดสแดษดแดแด๊ฏส๊ฑแดแดแด แดกสแดข"
|
|
102
|
+
},
|
|
103
|
+
superscript: {
|
|
104
|
+
from: "0123456789ABCDEFGHIJKLMNOPQRTUVWabcdefgh" \
|
|
105
|
+
"ijklmnopqrstuvwxyz",
|
|
106
|
+
to: "โฐยนยฒยณโดโตโถโทโธโนแดฌแดฎ๊ฒแดฐแดฑ๊ณแดณแดดแดตแดถแดทแดธแดนแดบแดผแดพ๊ดแดฟแตแตโฑฝแตแตแตแถแตแตแถ แตสฐ" \
|
|
107
|
+
"โฑสฒแตหกแตโฟแตแต๐ฅสณหขแตแตแตสทหฃสธแถป"
|
|
108
|
+
},
|
|
109
|
+
subscript: {
|
|
110
|
+
from: "0123456789aehijklmnoprstuvx",
|
|
111
|
+
to: "โโโโโโ
โโโโโโโแตขโฑผโโโโโโแตฃโโแตคแตฅโ"
|
|
112
|
+
},
|
|
113
|
+
circled: {
|
|
114
|
+
from: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcd" \
|
|
115
|
+
"efghijklmnopqrstuvwxyz",
|
|
116
|
+
to: "โชโ โกโขโฃโคโฅโฆโงโจโถโทโธโนโบโปโผโฝโพโฟโโโโโโ
โโโโโโโโโโโโโโ" \
|
|
117
|
+
"โโโโโโโโโโโโโ โกโขโฃโคโฅโฆโงโจโฉ"
|
|
118
|
+
},
|
|
119
|
+
negative_circled: {
|
|
120
|
+
from: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
121
|
+
to: "โฟโถโทโธโนโบโปโผโฝโพ๐
๐
๐
๐
๐
๐
๐
๐
๐
๐
๐
๐
๐
๐
๐
๐
๐
๐
ก๐
ข๐
ฃ๐
ค๐
ฅ๐
ฆ๐
ง๐
จ๐
ฉ"
|
|
122
|
+
},
|
|
123
|
+
parenthesized: {
|
|
124
|
+
from: "123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcde" \
|
|
125
|
+
"fghijklmnopqrstuvwxyz",
|
|
126
|
+
to: "โดโตโถโทโธโนโบโปโผ๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉโโโโโ " \
|
|
127
|
+
"โกโขโฃโคโฅโฆโงโจโฉโชโซโฌโญโฎโฏโฐโฑโฒโณโดโต"
|
|
128
|
+
},
|
|
129
|
+
squared: {
|
|
130
|
+
from: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
131
|
+
to: "๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐น๐บ๐ป๐ผ๐ฝ๐พ๐ฟ๐
๐
๐
๐
๐
๐
๐
๐
๐
๐
"
|
|
132
|
+
},
|
|
133
|
+
negative_squared: {
|
|
134
|
+
from: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
135
|
+
to: "๐
ฐ๐
ฑ๐
ฒ๐
ณ๐
ด๐
ต๐
ถ๐
ท๐
ธ๐
น๐
บ๐
ป๐
ผ๐
ฝ๐
พ๐
ฟ๐๐๐๐๐๐
๐๐๐๐"
|
|
136
|
+
},
|
|
137
|
+
fullwidth: {
|
|
138
|
+
from: "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGH" \
|
|
139
|
+
"IJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnop" \
|
|
140
|
+
"qrstuvwxyz{|}~ ",
|
|
141
|
+
to: "๏ผ๏ผ๏ผ๏ผ๏ผ
๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ๏ผ ๏ผก๏ผข๏ผฃ๏ผค๏ผฅ๏ผฆ๏ผง๏ผจ" \
|
|
142
|
+
"๏ผฉ๏ผช๏ผซ๏ผฌ๏ผญ๏ผฎ๏ผฏ๏ผฐ๏ผฑ๏ผฒ๏ผณ๏ผด๏ผต๏ผถ๏ผท๏ผธ๏ผน๏ผบ๏ผป๏ผผ๏ผฝ๏ผพ๏ผฟ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ
๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ" \
|
|
143
|
+
"๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝใ"
|
|
144
|
+
}
|
|
145
|
+
}.freeze
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
FORMS = MathForms::FORMS.merge(OtherForms::FORMS).freeze
|
|
149
|
+
end
|
|
150
|
+
end
|