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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 32cc6cec6f19c5fe62dc9a6de16493d6db35680f72e40b6bbd36f0b1e98add96
4
- data.tar.gz: b6f928fd6919bb289765fac684ae3e98f7988103985deebcb6f73e7f8836e939
3
+ metadata.gz: aa35ea4c535165dcf70b1a1b6375ff32666ce42294219a9b3d59ba6cb614ee85
4
+ data.tar.gz: 23d80a1781dd85a41d349b670b7610981bba30a87fa90cd0847d91dc65356fe4
5
5
  SHA512:
6
- metadata.gz: 50d8b1e91e3c71286406bf45e5e5cdeb2d8170f98a513a6662f0b593a7e077b7af2650057e6d9670469ed937b6ddc7c0073868df6833721c2ba3c81a8f691c46
7
- data.tar.gz: 92df8d83ddbabcdd3cbeaa30f26868da4a13c168d5f97600475f552a197f7ebba70a80cc6ab272f98e2a68c1b8bd9d9a9094d3b01235ec556790f4a944337fee
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
+ [![Test](https://github.com/yohasebe/rsyntaxtree/actions/workflows/test.yml/badge.svg)](https://github.com/yohasebe/rsyntaxtree/actions/workflows/test.yml)
6
+ [![Gem Version](https://img.shields.io/gem/v/rsyntaxtree)](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
- vertical_indent = if !target.triangle &&
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 parent.triangle || child.contains_phrase
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 parent.triangle
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 parent.triangle
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 parent.triangle || child.contains_phrase
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 parent.triangle
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 parent.triangle
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: 75 trees, every one verified to draw.
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.7
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< [NP\t> [NP/N the] [N dog]] [S\\NP\t> [(S\\NP)/NP bit] [NP John]]]
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.7
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> [NP/N the] [N\t< [N man] [N\\N\t> [(N\\N)/(S/NP) that] [S/NP\t>B [S/(S\\NP)\t>T [NP Mary]] [(S\\NP)/NP saw]]]]]
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
- case target_arrow
292
- when :single
293
- @extra_lines << generate_line(s_x, new_s_y, new_x, new_s_y, @col_path, dashed)
294
- @extra_lines << generate_line(new_x, new_s_y, new_x, new_t_y, @col_path, dashed)
295
- @extra_lines << generate_line(new_x, new_t_y, t_x, new_t_y, @col_path, dashed, true)
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
- case target_arrow
319
- when :single
320
- @extra_lines << generate_line(new_s_x, s_y, new_s_x, new_y, @col_path, dashed)
321
- @extra_lines << generate_line(new_s_x, new_y, new_t_x, new_y, @col_path, dashed)
322
- @extra_lines << generate_line(new_t_x, new_y, new_t_x, t_y, @col_path, dashed, true)
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
@@ -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
- # Monochrome emoji faces only. Colour emoji fonts carry their glyphs as bitmap
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
- # This holds only where Pango resolves through fontconfig. On macOS it goes
36
- # through CoreText, which answers every emoji codepoint with Apple Color Emoji
37
- # whatever the chain asks for; emoji figures have to be generated elsewhere.
38
- EMOJI_FAMILIES = ["OpenMoji", "OpenMoji Color", "OpenMoji Black", "Noto Emoji"].freeze
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 + EMOJI_FAMILIES + ["sans-serif"]).freeze,
53
- serif: (["Noto Serif", "Noto Serif JP", "Noto Serif CJK JP"] + SCRIPT_FAMILIES_SERIF + MATH_FAMILIES_SERIF + EMOJI_FAMILIES + ["serif"]).freeze,
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 + EMOJI_FAMILIES + ["monospace"]).freeze,
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 + EMOJI_FAMILIES + ["sans-serif"]).freeze
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSyntaxTree
4
- VERSION = "1.13.0"
4
+ VERSION = "1.13.2"
5
5
  end
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,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsyntaxtree
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.0
4
+ version: 1.13.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoichiro Hasebe