rsyntaxtree 1.13.0 → 1.13.2
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 +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +3 -0
- data/lib/rsyntaxtree/base_graph.rb +26 -4
- data/lib/rsyntaxtree/lsif_graph.rb +4 -3
- data/lib/rsyntaxtree/notation_examples.md +41 -5
- data/lib/rsyntaxtree/svg_graph.rb +87 -28
- data/lib/rsyntaxtree/utils.rb +19 -11
- data/lib/rsyntaxtree/version.rb +1 -1
- data/lib/rsyntaxtree.rb +12 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa35ea4c535165dcf70b1a1b6375ff32666ce42294219a9b3d59ba6cb614ee85
|
|
4
|
+
data.tar.gz: 23d80a1781dd85a41d349b670b7610981bba30a87fa90cd0847d91dc65356fe4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: baa9e98752ce86da774ededc1228b6f588e69da5b381bc94112190f7198f8a2128d64760cc49114b9e3a8815d24e0984714cf562ebdb0e86572dc4c3790e0bab
|
|
7
|
+
data.tar.gz: ada76c5d30433a0652be01242c8da0225f63e84c3717accb0eeb3628567df9744b67c408405acc94e52dbd38288fe5352f3ceb2c789b41974184dd2443322f6b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.13.2] - 2026-08
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- A movement path is drawn as one stroke with its corners eased, in place of
|
|
7
|
+
three lines meeting at right angles. The dash of a non-directional path now
|
|
8
|
+
runs round each turn instead of restarting at it, and the arrowhead is the end
|
|
9
|
+
of the stroke rather than a marker on whichever line finished last. The radius
|
|
10
|
+
is held down to half of each run it joins, so a short leg cannot be swallowed
|
|
11
|
+
by its own turn, and to the length of an arrowhead where there is one, so the
|
|
12
|
+
head is never drawn onto the curve. Three gallery figures carry paths and
|
|
13
|
+
change with it.
|
|
14
|
+
- Digits are set in the text face again on machines that have a colour emoji
|
|
15
|
+
font. The font chains named emoji families by name, and asking fontconfig for
|
|
16
|
+
one is asking for the generic `emoji` family, whose preference list fontconfig
|
|
17
|
+
then prepends to the whole pattern with a strong binding — so the emoji face
|
|
18
|
+
arrived ahead of the Noto text faces rather than behind them. A colour emoji
|
|
19
|
+
font carries the ASCII digits, for keycap sequences, so on an ordinary Linux
|
|
20
|
+
desktop `[N 20]` came out in the emoji face at more than twice the width.
|
|
21
|
+
Measurement and drawing read the same chain, so the figure held together; it
|
|
22
|
+
was simply not the figure the same input gives elsewhere. No emoji family is
|
|
23
|
+
named now: a codepoint no named family covers reaches fontconfig's own
|
|
24
|
+
fallback, which is the path this was duplicating. Emoji draw as before, and
|
|
25
|
+
the gallery is unchanged to the pixel — every raster figure is byte-identical
|
|
26
|
+
and the SVGs differ only in the family list they carry.
|
|
27
|
+
|
|
28
|
+
## [1.13.1] - 2026-08
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- `^` at the head of a leaf draws the triangle it asks for. The mark may be
|
|
32
|
+
written on the node's label or on the leaf's own text — `[^NP cats]` and
|
|
33
|
+
`[NP ^cats]` — and the documentation, the notation reference and the gallery
|
|
34
|
+
all use both, but only the first was drawn: the second lost its caret to the
|
|
35
|
+
parser and then a bar was drawn under it. The two now produce the same figure
|
|
36
|
+
in every connector style, and LSIF records the edge as a triangle. Three
|
|
37
|
+
gallery figures change accordingly, each toward what it was written to show.
|
|
38
|
+
- An option given as an empty string is read as an option not given, rather
|
|
39
|
+
than as a value no list contains. An HTML form posts a field for every
|
|
40
|
+
control it carries, and a control with nothing selected posts the empty
|
|
41
|
+
string, so a form that has outlived one of its own controls sends that field
|
|
42
|
+
empty alongside the rest. Since option validation arrived in 1.11.0 that
|
|
43
|
+
failed the whole request: the web UI's Download buttons answered 500 for
|
|
44
|
+
every input, because the form still read a `format` select the page no longer
|
|
45
|
+
had. A value that is actually wrong is still rejected.
|
|
46
|
+
|
|
3
47
|
## [1.13.0] - 2026-08
|
|
4
48
|
|
|
5
49
|
### Added
|
data/README.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
**RSyntaxTree** is a graphical syntax tree generator for linguistic research.
|
|
4
4
|
|
|
5
|
+
[](https://github.com/yohasebe/rsyntaxtree/actions/workflows/test.yml)
|
|
6
|
+
[](https://rubygems.org/gems/rsyntaxtree)
|
|
7
|
+
|
|
5
8
|
<p>
|
|
6
9
|
<a href="https://yohasebe.com/rsyntaxtree"><strong>Web App</strong></a> ·
|
|
7
10
|
<a href="https://yohasebe.github.io/rsyntaxtree/examples"><strong>Example Gallery</strong></a> ·
|
|
@@ -221,7 +221,12 @@ module RSyntaxTree
|
|
|
221
221
|
else
|
|
222
222
|
parent = @element_list.get_id(target.parent)
|
|
223
223
|
|
|
224
|
-
|
|
224
|
+
# A leaf with nothing drawn over it is pulled up against its node; one
|
|
225
|
+
# with a triangle over it needs the room the triangle takes. Which is
|
|
226
|
+
# which is the same question the connector asks, so it is asked the same
|
|
227
|
+
# way — reading only the leaf's own mark left `[^NP cats]` a triangle
|
|
228
|
+
# squashed into a gap that had been closed.
|
|
229
|
+
vertical_indent = if !triangle_asked_for?(parent, target) &&
|
|
225
230
|
(@leafstyle == "nothing" || @leafstyle == "none") &&
|
|
226
231
|
ETYPE_LEAF == target.type && parent.children.size == 1
|
|
227
232
|
if @direction == "ltr"
|
|
@@ -395,6 +400,22 @@ module RSyntaxTree
|
|
|
395
400
|
end
|
|
396
401
|
end
|
|
397
402
|
|
|
403
|
+
# Whether a `^` has asked for a triangle over this leaf. It may be written
|
|
404
|
+
# at the head of the node's label or at the head of the leaf's own text —
|
|
405
|
+
# `[^NP cats]` and `[NP ^cats]` — and both forms are in the gallery. Only
|
|
406
|
+
# the first was honoured here, and the second was dropped without a word:
|
|
407
|
+
# the caret vanished from the text and a bar was drawn, so an example named
|
|
408
|
+
# for its triangles had none. The rest of the layout already reads the
|
|
409
|
+
# leaf's own flag — a leaf marked this way keeps the gap a triangle needs
|
|
410
|
+
# even when connectors are off — which is what left the two sides of one
|
|
411
|
+
# rule disagreeing.
|
|
412
|
+
#
|
|
413
|
+
# Asked of a leaf only. A `^` on an internal node is that node's own mark,
|
|
414
|
+
# and it is honoured when that node is in turn the parent here.
|
|
415
|
+
def triangle_asked_for?(parent, child)
|
|
416
|
+
parent.triangle || (ETYPE_LEAF == child.type && child.triangle)
|
|
417
|
+
end
|
|
418
|
+
|
|
398
419
|
def draw_connector(id = 1)
|
|
399
420
|
parent = @element_list.get_id(id)
|
|
400
421
|
children = parent.children.map { |c| @element_list.get_id(c) }
|
|
@@ -410,21 +431,22 @@ module RSyntaxTree
|
|
|
410
431
|
|
|
411
432
|
if children.size == 1
|
|
412
433
|
child = children[0]
|
|
434
|
+
forced = triangle_asked_for?(parent, child)
|
|
413
435
|
case @leafstyle
|
|
414
436
|
when "auto"
|
|
415
|
-
if
|
|
437
|
+
if forced || child.contains_phrase
|
|
416
438
|
triangle_to_parent(parent, child)
|
|
417
439
|
else
|
|
418
440
|
line_to_parent(parent, child)
|
|
419
441
|
end
|
|
420
442
|
when "bar"
|
|
421
|
-
if
|
|
443
|
+
if forced
|
|
422
444
|
triangle_to_parent(parent, child)
|
|
423
445
|
else
|
|
424
446
|
line_to_parent(parent, child)
|
|
425
447
|
end
|
|
426
448
|
when "nothing", "none"
|
|
427
|
-
if
|
|
449
|
+
if forced
|
|
428
450
|
triangle_to_parent(parent, child)
|
|
429
451
|
elsif ETYPE_LEAF != child.type
|
|
430
452
|
line_to_parent(parent, child)
|
|
@@ -82,21 +82,22 @@ module RSyntaxTree
|
|
|
82
82
|
|
|
83
83
|
if children.size == 1
|
|
84
84
|
child = children[0]
|
|
85
|
+
forced = triangle_asked_for?(parent, child)
|
|
85
86
|
case @leafstyle
|
|
86
87
|
when "auto"
|
|
87
|
-
if
|
|
88
|
+
if forced || child.contains_phrase
|
|
88
89
|
triangle_to_parent(parent, child)
|
|
89
90
|
else
|
|
90
91
|
line_to_parent(parent, child)
|
|
91
92
|
end
|
|
92
93
|
when "bar"
|
|
93
|
-
if
|
|
94
|
+
if forced
|
|
94
95
|
triangle_to_parent(parent, child)
|
|
95
96
|
else
|
|
96
97
|
line_to_parent(parent, child)
|
|
97
98
|
end
|
|
98
99
|
when "nothing", "none"
|
|
99
|
-
if
|
|
100
|
+
if forced
|
|
100
101
|
triangle_to_parent(parent, child)
|
|
101
102
|
elsif ETYPE_LEAF != child.type
|
|
102
103
|
line_to_parent(parent, child)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
RSyntaxTree examples:
|
|
1
|
+
RSyntaxTree examples: 76 trees, every one verified to draw.
|
|
2
2
|
|
|
3
3
|
Each is the input behind a figure in the gallery at https://yohasebe.github.io/rsyntaxtree/examples.
|
|
4
4
|
The settings line names the options the gallery records for that figure;
|
|
@@ -279,21 +279,36 @@ Source: Radford 2004
|
|
|
279
279
|
## 011 — CCG derivation: forward and backward application
|
|
280
280
|
|
|
281
281
|
Category: Combinatory Categorial Grammar
|
|
282
|
-
Settings: color=none derivation=on direction=btt fontstyle=noto-serif leafstyle=nothing tidy=low vheight=0.
|
|
282
|
+
Settings: color=none derivation=on direction=btt fontstyle=noto-serif hspacing=2.0 leafstyle=nothing tidy=low vheight=0.5
|
|
283
283
|
Source: Steedman 2000
|
|
284
284
|
|
|
285
285
|
```
|
|
286
|
-
[S\t<
|
|
286
|
+
[S\t<
|
|
287
|
+
[NP\t>
|
|
288
|
+
[NP/N the]
|
|
289
|
+
[N dog]]
|
|
290
|
+
[S\\NP\t>
|
|
291
|
+
[(S\\NP)/NP bit]
|
|
292
|
+
[NP John]]]
|
|
287
293
|
```
|
|
288
294
|
|
|
289
295
|
## 012 — CCG derivation: a relative clause
|
|
290
296
|
|
|
291
297
|
Category: Combinatory Categorial Grammar
|
|
292
|
-
Settings: color=none derivation=on direction=btt fontstyle=noto-serif leafstyle=nothing tidy=low vheight=0.
|
|
298
|
+
Settings: color=none derivation=on direction=btt fontstyle=noto-serif hspacing=2.0 leafstyle=nothing tidy=low vheight=0.5
|
|
293
299
|
Source: Steedman 2000
|
|
294
300
|
|
|
295
301
|
```
|
|
296
|
-
[NP\t>
|
|
302
|
+
[NP\t>
|
|
303
|
+
[NP/N the]
|
|
304
|
+
[N\t<
|
|
305
|
+
[N man]
|
|
306
|
+
[N\\N\t>
|
|
307
|
+
[(N\\N)/(S/NP) that]
|
|
308
|
+
[S/NP\t>B
|
|
309
|
+
[S/(S\\NP)\t>T
|
|
310
|
+
[NP Mary]]
|
|
311
|
+
[(S\\NP)/NP saw]]]]]
|
|
297
312
|
```
|
|
298
313
|
|
|
299
314
|
## 013 — HPSG sample
|
|
@@ -2485,3 +2500,24 @@ Settings: fontstyle=noto-serif tidy=low vheight=1.0
|
|
|
2485
2500
|
[V [<> [<> sat]]]
|
|
2486
2501
|
[PP [P [<> on]] [NP [D the] [N mat]]]]]
|
|
2487
2502
|
```
|
|
2503
|
+
|
|
2504
|
+
## 081 — CCG derivation: right node raising
|
|
2505
|
+
|
|
2506
|
+
Category: Combinatory Categorial Grammar
|
|
2507
|
+
Settings: color=none derivation=on direction=btt fontstyle=noto-serif hspacing=2.0 leafstyle=nothing tidy=low vheight=0.5
|
|
2508
|
+
Source: Steedman 2000
|
|
2509
|
+
|
|
2510
|
+
```
|
|
2511
|
+
[S\t>
|
|
2512
|
+
[S/NP\t<Φ>
|
|
2513
|
+
[S/NP\t>B
|
|
2514
|
+
[S/(S\\NP)\t>T
|
|
2515
|
+
[NP Mary]]
|
|
2516
|
+
[(S\\NP)/NP likes]]
|
|
2517
|
+
[conj and]
|
|
2518
|
+
[S/NP\t>B
|
|
2519
|
+
[S/(S\\NP)\t>T
|
|
2520
|
+
[NP Bill]]
|
|
2521
|
+
[(S\\NP)/NP hates]]]
|
|
2522
|
+
[NP London]]
|
|
2523
|
+
```
|
|
@@ -108,6 +108,9 @@ module RSyntaxTree
|
|
|
108
108
|
<marker id="arrow" markerUnits="userSpaceOnUse" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="#{as2}" markerHeight="#{as2}" orient="auto">
|
|
109
109
|
<path d="M 0 0 L 10 5 L 0 10" fill="#{@col_extra}"/>
|
|
110
110
|
</marker>
|
|
111
|
+
<marker id="arrowStart" markerUnits="userSpaceOnUse" viewBox="0 0 10 10" refX="0" refY="5" markerWidth="#{as2}" markerHeight="#{as2}" orient="auto">
|
|
112
|
+
<path d="M 10 0 L 0 5 L 10 10" fill="#{@col_extra}"/>
|
|
113
|
+
</marker>
|
|
111
114
|
<marker id="arrowBackward" markerUnits="userSpaceOnUse" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="#{as2}" markerHeight="#{as2}" orient="auto">
|
|
112
115
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#{@col_extra}"/>
|
|
113
116
|
</marker>
|
|
@@ -259,9 +262,83 @@ module RSyntaxTree
|
|
|
259
262
|
b
|
|
260
263
|
end
|
|
261
264
|
|
|
265
|
+
# A point the given distance from `from` along the line towards `to`.
|
|
266
|
+
def along(from, to, distance)
|
|
267
|
+
dx = to[0] - from[0]
|
|
268
|
+
dy = to[1] - from[1]
|
|
269
|
+
length = Math.sqrt((dx * dx) + (dy * dy))
|
|
270
|
+
return from.dup if length.zero?
|
|
271
|
+
|
|
272
|
+
[from[0] + (dx / length * distance), from[1] + (dy / length * distance)]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# The `d` of a polyline whose corners are eased into quarter turns.
|
|
276
|
+
#
|
|
277
|
+
# The radius is clamped to half of each of the two runs meeting at a corner,
|
|
278
|
+
# so a short leg can never be swallowed by its own turn and two adjacent
|
|
279
|
+
# corners can never overlap — which is the whole of the arithmetic that
|
|
280
|
+
# rounding a right angle needs, and why it can be a constant everywhere
|
|
281
|
+
# else.
|
|
282
|
+
def rounded_polyline_d(points, radius)
|
|
283
|
+
return +"" if points.size < 2
|
|
284
|
+
|
|
285
|
+
at = ->(p) { "#{p[0].round(3)},#{p[1].round(3)}" }
|
|
286
|
+
d = +"M#{at.call(points[0])}"
|
|
287
|
+
points.each_cons(3) do |before, corner, after|
|
|
288
|
+
r = [radius,
|
|
289
|
+
Math.sqrt(((corner[0] - before[0])**2) + ((corner[1] - before[1])**2)) / 2.0,
|
|
290
|
+
Math.sqrt(((after[0] - corner[0])**2) + ((after[1] - corner[1])**2)) / 2.0].min
|
|
291
|
+
if r < 0.01
|
|
292
|
+
d << " L#{at.call(corner)}"
|
|
293
|
+
else
|
|
294
|
+
d << " L#{at.call(along(corner, before, r))}" \
|
|
295
|
+
" Q#{at.call(corner)} #{at.call(along(corner, after, r))}"
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
d << " L#{at.call(points.last)}"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# One stroke rather than three lines meeting at right angles. Drawn this way
|
|
302
|
+
# the dash pattern runs round each turn instead of restarting at it, and an
|
|
303
|
+
# arrowhead is the end of the stroke rather than a marker on whichever line
|
|
304
|
+
# happens to finish last.
|
|
305
|
+
def generate_path(points, col, dashed: false, radius: 0, start_arrow: false, end_arrow: false)
|
|
306
|
+
# An arrowhead is drawn back along the run it ends, so the corner before
|
|
307
|
+
# it has to leave that run its full length. Without this the shorter of
|
|
308
|
+
# the two end runs — which is only as long as the path's bulge — lost half
|
|
309
|
+
# of itself to the turn and the head came out sitting on the curve.
|
|
310
|
+
arrow_length = @global[:h_gap_between_nodes]
|
|
311
|
+
leg = ->(a, b) { Math.sqrt(((a[0] - b[0])**2) + ((a[1] - b[1])**2)) }
|
|
312
|
+
radius = [radius, leg.call(points[0], points[1]) - arrow_length].min if start_arrow
|
|
313
|
+
radius = [radius, leg.call(points[-2], points[-1]) - arrow_length].min if end_arrow
|
|
314
|
+
radius = [radius, 0].max
|
|
315
|
+
|
|
316
|
+
dash = @fontsize / 4.0
|
|
317
|
+
dasharray = dashed ? "stroke-dasharray='#{dash} #{dash}' " : ""
|
|
318
|
+
markers = +""
|
|
319
|
+
markers << "marker-start='url(#arrowStart)' " if start_arrow
|
|
320
|
+
markers << "marker-end='url(#arrow)' " if end_arrow
|
|
321
|
+
|
|
322
|
+
# A round cap is half the stroke's width of ink past the point the stroke
|
|
323
|
+
# ends at, and an arrowhead has its tip at exactly that point — so the
|
|
324
|
+
# line came through the tip: 13% ink on the pixel beyond the head of a
|
|
325
|
+
# path, and 81% beyond its tail, where the marker's apex sits on the
|
|
326
|
+
# anchor rather than behind it. Squared off, the stroke stops where the
|
|
327
|
+
# head begins. Only where there is a head: a path without one is the
|
|
328
|
+
# dashed form, and every dash of it wants its round ends.
|
|
329
|
+
linecap = start_arrow || end_arrow ? "butt" : "round"
|
|
330
|
+
|
|
331
|
+
"<path d='#{rounded_polyline_d(points, radius)}' style='fill: none; stroke: #{col}; " \
|
|
332
|
+
"stroke-width:#{@global[:stroke_normal]}; stroke-linecap:#{linecap}; stroke-linejoin:round;' " \
|
|
333
|
+
"#{dasharray}#{markers}/>"
|
|
334
|
+
end
|
|
335
|
+
|
|
262
336
|
def draw_a_path(s_x, s_y, t_x, t_y, target_arrow = :none)
|
|
263
337
|
spacing = @global[:h_gap_between_nodes] * 1.25
|
|
264
338
|
min_bulge = @global[:height_connector_to_text]
|
|
339
|
+
# Small enough that the turn reads as an eased corner rather than as an
|
|
340
|
+
# arc, and taken from a layout measure so it holds at every font size.
|
|
341
|
+
corner_radius = min_bulge / 2.0
|
|
265
342
|
|
|
266
343
|
# Centered offset for multiple lines at the same endpoint
|
|
267
344
|
s_key = "#{s_x.round}"
|
|
@@ -288,20 +365,11 @@ module RSyntaxTree
|
|
|
288
365
|
new_s_y = s_y + s_offset
|
|
289
366
|
new_t_y = t_y + t_offset
|
|
290
367
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
when :double
|
|
297
|
-
@extra_lines << generate_line(new_x, new_s_y, s_x, new_s_y, @col_path, dashed, true)
|
|
298
|
-
@extra_lines << generate_line(new_x, new_s_y, new_x, new_t_y, @col_path, dashed)
|
|
299
|
-
@extra_lines << generate_line(new_x, new_t_y, t_x, new_t_y, @col_path, dashed, true)
|
|
300
|
-
else
|
|
301
|
-
@extra_lines << generate_line(s_x, new_s_y, new_x, new_s_y, @col_path, dashed)
|
|
302
|
-
@extra_lines << generate_line(new_x, new_s_y, new_x, new_t_y, @col_path, dashed)
|
|
303
|
-
@extra_lines << generate_line(new_x, new_t_y, t_x, new_t_y, @col_path, dashed)
|
|
304
|
-
end
|
|
368
|
+
@extra_lines << generate_path([[s_x, new_s_y], [new_x, new_s_y],
|
|
369
|
+
[new_x, new_t_y], [t_x, new_t_y]],
|
|
370
|
+
@col_path, dashed: dashed, radius: corner_radius,
|
|
371
|
+
start_arrow: target_arrow == :double,
|
|
372
|
+
end_arrow: target_arrow != :none)
|
|
305
373
|
@width = new_x if new_x > @width
|
|
306
374
|
else
|
|
307
375
|
# TTB: route BELOW the tree (U shape)
|
|
@@ -315,20 +383,11 @@ module RSyntaxTree
|
|
|
315
383
|
new_s_x = s_x - s_offset
|
|
316
384
|
new_t_x = t_x - t_offset
|
|
317
385
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
when :double
|
|
324
|
-
@extra_lines << generate_line(new_s_x, new_y, new_s_x, s_y, @col_path, dashed, true)
|
|
325
|
-
@extra_lines << generate_line(new_s_x, new_y, new_t_x, new_y, @col_path, dashed)
|
|
326
|
-
@extra_lines << generate_line(new_t_x, new_y, new_t_x, t_y, @col_path, dashed, true)
|
|
327
|
-
else
|
|
328
|
-
@extra_lines << generate_line(new_s_x, s_y, new_s_x, new_y, @col_path, dashed)
|
|
329
|
-
@extra_lines << generate_line(new_s_x, new_y, new_t_x, new_y, @col_path, dashed)
|
|
330
|
-
@extra_lines << generate_line(new_t_x, new_y, new_t_x, t_y, @col_path, dashed)
|
|
331
|
-
end
|
|
386
|
+
@extra_lines << generate_path([[new_s_x, s_y], [new_s_x, new_y],
|
|
387
|
+
[new_t_x, new_y], [new_t_x, t_y]],
|
|
388
|
+
@col_path, dashed: dashed, radius: corner_radius,
|
|
389
|
+
start_arrow: target_arrow == :double,
|
|
390
|
+
end_arrow: target_arrow != :none)
|
|
332
391
|
@height = new_y if new_y > @height
|
|
333
392
|
end
|
|
334
393
|
end
|
data/lib/rsyntaxtree/utils.rb
CHANGED
|
@@ -28,14 +28,22 @@ SCRIPT_FAMILIES_SANS = ["Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans Devan
|
|
|
28
28
|
# Arabic behind it for systems that ship one but not the other.
|
|
29
29
|
SCRIPT_FAMILIES_SERIF = ["Noto Naskh Arabic", "Noto Sans Arabic", "Noto Serif Hebrew", "Noto Serif Devanagari", "Noto Serif Thai", "Noto Serif Khmer"].freeze
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
# or COLR tables, which Pango happily measures but librsvg does not draw, so a
|
|
33
|
-
# colour font in this list would give a figure whose emoji are blank or blobbed.
|
|
31
|
+
# No emoji family is named in the chains below, and naming one was a mistake.
|
|
34
32
|
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
|
|
33
|
+
# Asking for an emoji face by name is asking fontconfig for it, and fontconfig
|
|
34
|
+
# aliases the name to the generic `emoji` family, whose preference list it then
|
|
35
|
+
# prepends to the whole pattern with a strong binding. The requested face
|
|
36
|
+
# arrives at the head of the chain rather than at the tail, ahead of the Noto
|
|
37
|
+
# text faces — and a colour emoji font carries ASCII digits, for the keycap
|
|
38
|
+
# sequences. So on any machine with one installed, which is every ordinary
|
|
39
|
+
# Linux desktop, `[N 20]` was set in the emoji face at more than twice the
|
|
40
|
+
# width. Measurement and drawing read the same chain, so the figure held
|
|
41
|
+
# together; it was simply not the figure the same input gives anywhere else.
|
|
42
|
+
#
|
|
43
|
+
# Emoji still draw. A codepoint no named family covers reaches fontconfig's own
|
|
44
|
+
# fallback, which finds the emoji font — that is the path this list was
|
|
45
|
+
# duplicating, and it was already the path taken wherever these families were
|
|
46
|
+
# not installed.
|
|
39
47
|
|
|
40
48
|
# Mathematical alphanumerics (U+1D400–) label heads such as the little v of vP.
|
|
41
49
|
# Neither Noto Sans nor Noto Serif covers the block, so without these entries
|
|
@@ -49,15 +57,15 @@ MATH_FAMILIES_SANS = ["Noto Sans Math"].freeze
|
|
|
49
57
|
MATH_FAMILIES_SERIF = ["DejaVu Serif", "Noto Sans Math"].freeze
|
|
50
58
|
|
|
51
59
|
FONT_FAMILIES = {
|
|
52
|
-
sans: (["Noto Sans", "Noto Sans JP", "Noto Sans CJK JP"] + SCRIPT_FAMILIES_SANS + MATH_FAMILIES_SANS +
|
|
53
|
-
serif: (["Noto Serif", "Noto Serif JP", "Noto Serif CJK JP"] + SCRIPT_FAMILIES_SERIF + MATH_FAMILIES_SERIF +
|
|
60
|
+
sans: (["Noto Sans", "Noto Sans JP", "Noto Sans CJK JP"] + SCRIPT_FAMILIES_SANS + MATH_FAMILIES_SANS + ["sans-serif"]).freeze,
|
|
61
|
+
serif: (["Noto Serif", "Noto Serif JP", "Noto Serif CJK JP"] + SCRIPT_FAMILIES_SERIF + MATH_FAMILIES_SERIF + ["serif"]).freeze,
|
|
54
62
|
# Noto ships no monospaced faces for these scripts, so the mono style borrows
|
|
55
63
|
# the proportional ones rather than dropping to the generic fallback.
|
|
56
|
-
mono: (["Noto Sans Mono", "Noto Sans JP", "Noto Sans Mono CJK JP"] + SCRIPT_FAMILIES_SANS + MATH_FAMILIES_SANS +
|
|
64
|
+
mono: (["Noto Sans Mono", "Noto Sans JP", "Noto Sans Mono CJK JP"] + SCRIPT_FAMILIES_SANS + MATH_FAMILIES_SANS + ["monospace"]).freeze,
|
|
57
65
|
# The cjk style puts a full-coverage CJK family first, for text that mixes
|
|
58
66
|
# Han, Hangul and kana. Latin falls back to the same Noto faces the sans
|
|
59
67
|
# style uses. (Before 1.8.0 this was WQY Zen Hei.)
|
|
60
|
-
cjk: (["Noto Sans CJK JP", "Noto Sans", "Noto Sans JP"] + SCRIPT_FAMILIES_SANS + MATH_FAMILIES_SANS +
|
|
68
|
+
cjk: (["Noto Sans CJK JP", "Noto Sans", "Noto Sans JP"] + SCRIPT_FAMILIES_SANS + MATH_FAMILIES_SANS + ["sans-serif"]).freeze
|
|
61
69
|
}.freeze
|
|
62
70
|
|
|
63
71
|
module FontFamily
|
data/lib/rsyntaxtree/version.rb
CHANGED
data/lib/rsyntaxtree.rb
CHANGED
|
@@ -151,6 +151,18 @@ module RSyntaxTree
|
|
|
151
151
|
fontset = {}
|
|
152
152
|
params.each do |keystr, value|
|
|
153
153
|
key = keystr.to_sym
|
|
154
|
+
# An option given as an empty string is an option not given. An HTML
|
|
155
|
+
# form posts a field for every control it carries, and a control with
|
|
156
|
+
# nothing selected posts the empty string — so a form that has outlived
|
|
157
|
+
# one of its own controls sends `format=` and every other option along
|
|
158
|
+
# with it. Read as a value, that is a choice nobody can have made, and
|
|
159
|
+
# it failed the whole request: the web UI's three Download buttons
|
|
160
|
+
# returned 500 for every input, with the reason nowhere the user could
|
|
161
|
+
# see it. Read as silence, the default stands, which is what the sender
|
|
162
|
+
# meant.
|
|
163
|
+
next if (OPTION_VALUES.key?(key) || NUMERIC_RANGES.key?(key)) &&
|
|
164
|
+
(value.nil? || value.to_s.strip.empty?)
|
|
165
|
+
|
|
154
166
|
if OPTION_VALUES.key?(key) && !OPTION_VALUES[key].include?(value.to_s)
|
|
155
167
|
raise RSTError.new(+"Error: invalid value for option '#{key}': #{value.inspect}",
|
|
156
168
|
code: :invalid_option,
|