Class: RSyntaxTree::Element

Inherits:
Object
  • Object
show all
Defined in:
lib/rsyntaxtree/element.rb

Constant Summary collapse

MARKUP_REPAIRS =

One candidate repair per way of getting the notation wrong, tried in order. Each is a whole edit of the label, not a pattern to recognise: the diagnosis below applies one and asks the parser whether the label now parses, so a cause is only ever reported when its fix is known to work. That keeps the list from drifting away from the grammar the way a set of hand-written patterns would — the grammar is the judge.

[
  # A malformed color spec ('@' then a bad name or a bad hex) leaves a
  # bare '#' or '@' behind that the enclosure repair below would happily
  # blame itself for. Tried first, so a color mistake is named as one.
  [:invalid_color,
   ->(s) { s.sub(/\A%?@(?:#[0-9a-zA-Z]+|[a-zA-Z]+)?:/, "") },
   "A color is @name: with a CSS color name, or @#rgb: / @#rrggbb: with 3 or 6 hex digits (e.g. @blue:VP, @#3af:VP)."],
  [:angle_brackets,
   ->(s) { s.gsub(/(?<!\\)<([^<>]*[^<>\d][^<>]*)>/) { "#{$1}" } },
   "'<' and '>' mark whitespace here, not a list. Write the angle bracket characters themselves: ⟨NP⟩, 'hand⟨SUBJ,OBJ⟩'."],
  [:bare_hyphen,
   ->(s) { Element.escape_hyphens(s) },
   "A hyphen opens an underline. Escape it (e.g. f\\-structure, V\\-bar) or set the hyphen option to literal."],
  # Every other repair leaves a label it has nothing to do with exactly
  # as it was, and an unchanged label is not tried. Appending always
  # changes one, so this one asks first whether there is an unclosed
  # matrix at all: without that, a label of "^" parses once "#)" is
  # stuck on the end, and would be reported as a matrix left open.
  [:unclosed_matrix,
   ->(s) { s.scan("#(").size > s.scan("#)").size ? s + "#)" : s },
   "A matrix opened with '#(' is never closed with '#)'."],
  # A derivation writes the name of each step after a column break, and
  # the combinators it is written with are made of the same characters as
  # the whitespace marker. Read as an ordinary tree such a label is
  # unclosed markup, and the advice below is about spaces — the opposite of
  # what the writer needs. Asked here, before those, so that a derivation
  # written without the option on is told so.
  [:rule_name_without_derivation,
   ->(s) { s.include?('\\n') ? s : s.sub(/(?<!\\)\\t.*\z/m, "") },
   "This label names a rule after a column break, the way a derivation " \
   "does. Turn derivation on to draw it as one, or escape the break as \\\\t."],
  # Neutralising every occurrence of one character, rather than adding a
  # closing one, locates the culprit wherever it sits in the label — an
  # opener left unclosed halfway down a matrix is not fixed by appending.
  *{ "*" => "A '*' decoration (italic or bold) is never closed.",
     "|" => "A '|' box is never closed.",
     "_" => "A '_' subscript or superscript is never closed.",
     "{" => "A '{...}' circle is never closed.",
     "=" => "An '=' overline is never closed.",
     "~" => "A '~' strikethrough is never closed.",
     "#" => "A '#' enclosure is not one of #, ## or ###, or a matrix is left open.",
     "<" => "'<' and '>' mark whitespace: <> is one space, <3> is three." }
    .map { |ch, hint| [:unclosed_markup, ->(s) { s.gsub(/(?<!\\)#{Regexp.escape(ch)}/) { "\\#{ch}" } }, hint] },
  [:stray_triangle,
   ->(s) { s.sub(/\A\^+/, "^") }, "Only one '^' may prefix a label."],
  [:incomplete_path,
   ->(s) { s.sub(/(?<!\\)\+>?<?\z/, "") },
   "A path marker needs a number: write +1, or +>1 for the arrowhead."]
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(id, parent, content, level, fontset, fontsize, global, names_a_rule = false) ⇒ Element

names_a_rule says the content is a mother's label rather than a leaf's text. Only a mother has a step under it for a name to sit beside, and a leaf's a\tb is a row of two columns, so the name is read out of the one and left alone in the other.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/rsyntaxtree/element.rb', line 22

def initialize(id, parent, content, level, fontset, fontsize, global,
               names_a_rule = false)
  @global = global
  @type = ETYPE_LEAF
  @id = id                 # Unique element id
  @parent = parent         # Parent element id
  @children = []           # Child element ids
  @level = level           # Element level in the tree (0=top etc...)
  @width = 0               # Width of the part of the tree including itself and it governs
  @content_width = 0       # Width of the content
  @horizontal_indent = 0   # Drawing offset
  @vertical_indent = 0     # Drawing offset
  content = content.strip

  @path = if /.+?\^?((?:\+-?>?<?\d+)+)\^?\z/m =~ content
            $1.sub(/\A\+/, "").split("+")
          else
            []
          end

  @fontset = fontset
  @fontsize = fontsize
  # In a derivation the label may carry the name of the rule that produced
  # it, written after a column break: `S/NP\t>B`. The name is taken out
  # before the label is read as markup, so it is drawn exactly as written —
  # `>` and `<` need no escaping there, and escaping them puts the backslash
  # in the figure. (This example carried one, which is the opposite of what
  # the gallery's derivations do.) The name belongs beside
  # the rule rather than beside the result, so it comes out of the label
  # here, before the label is measured, and BaseGraph draws it at the end
  # of the rule.
  if global && global[:derivation] && names_a_rule
    at = rule_name_break(content)
    name = at && content[(at + 2)..].to_s.strip
    if name && !name.empty?
      @rule_name = name
      # Kept whole, because whether this node has daughters — and so
      # whether it names a rule at all — is not known until the tree
      # is built. StringParser puts the label back if it turns out
      # there was no step for the name to belong to.
      @label_with_rule_name = content
      content = content[0...at]
    end
  end

  @raw_content = content.sub(/\^?(?:\+-?>?<?\d+)+\^?\z/, '')

  parsed = Markup.parse(prepare_markup(content))

  if parsed[:status] == :success
    results = parsed[:results]
  else
    error_text = +"Error: input text contains an invalid string"
    error_text += "\n > " + content
    raise RSTError.new(error_text, **markup_failure_details(content, parsed[:charpos]))
  end
  @content = results[:contents]
  @enclosure = results[:enclosure]
  @triangle = results[:triangle]
  @color = results[:color]
  @region = results[:region]
  @region_color = results[:region_color]
  validate_color!(@color, content)
  validate_color!(@region_color, content)

  @contains_phrase = false
  setup
end

Instance Attribute Details

#childrenObject

Returns the value of attribute children.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def children
  @children
end

#colorObject

Returns the value of attribute color.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def color
  @color
end

#contains_phraseObject

Returns the value of attribute contains_phrase.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def contains_phrase
  @contains_phrase
end

#contentObject

Returns the value of attribute content.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def content
  @content
end

#content_heightObject

Returns the value of attribute content_height.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def content_height
  @content_height
end

#content_widthObject

Returns the value of attribute content_width.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def content_width
  @content_width
end

#enclosureObject

Returns the value of attribute enclosure.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def enclosure
  @enclosure
end

#fontObject

Returns the value of attribute font.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def font
  @font
end

#fontsizeObject

Returns the value of attribute fontsize.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def fontsize
  @fontsize
end

#heightObject

Returns the value of attribute height.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def height
  @height
end

#horizontal_indentObject

Returns the value of attribute horizontal_indent.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def horizontal_indent
  @horizontal_indent
end

#idObject

Returns the value of attribute id.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def id
  @id
end

#label_with_rule_nameObject

Returns the value of attribute label_with_rule_name.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def label_with_rule_name
  @label_with_rule_name
end

#levelObject

Returns the value of attribute level.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def level
  @level
end

#parentObject

Returns the value of attribute parent.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def parent
  @parent
end

#pathObject

Returns the value of attribute path.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def path
  @path
end

#raw_contentObject

Returns the value of attribute raw_content.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def raw_content
  @raw_content
end

#regionObject

Returns the value of attribute region.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def region
  @region
end

#region_colorObject

Returns the value of attribute region_color.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def region_color
  @region_color
end

#rule_nameObject

Returns the value of attribute rule_name.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def rule_name
  @rule_name
end

#text_widthObject

Returns the value of attribute text_width.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def text_width
  @text_width
end

#triangleObject

Returns the value of attribute triangle.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def triangle
  @triangle
end

#typeObject

Returns the value of attribute type.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def type
  @type
end

#vertical_indentObject

Returns the value of attribute vertical_indent.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def vertical_indent
  @vertical_indent
end

#widthObject

Returns the value of attribute width.



16
17
18
# File 'lib/rsyntaxtree/element.rb', line 16

def width
  @width
end

Class Method Details

.escape_hyphens(text) ⇒ Object

Escape the hyphens that open an underline. Shares its exemptions with swap_hyphen_markup through hyphen_safe_lines.



148
149
150
151
# File 'lib/rsyntaxtree/element.rb', line 148

def self.escape_hyphens(text)
  lines, path = hyphen_safe_lines(text)
  lines.map { |line| /\A-{3,}\z/.match?(line) ? line : line.gsub(/(?<!\\)-/, '\\-') }.join('\n') + path
end

.hyphen_safe_lines(text) ⇒ Object

With hyphen: literal, the two readings of '-' trade places: a bare one is a hyphen and an escaped one opens and closes an underline. Feature names in HPSG and its relatives are full of hyphens — HEAD-DTR, RELIED-ON — and escaping every one of them is a poor trade for a rule nobody there uses. Swapping the two before parsing leaves the grammar untouched. Two uses of the hyphen are structure rather than markup, and are left alone: a line of nothing but hyphens is the horizontal rule, and the dash in a path suffix (+-1, +->2) is what makes that path dashed. Swapping those turned a rule into the text "---" without a word of complaint. Split a label into lines with the trailing path markers detached, so hyphen handling can spare the two places a run of hyphens means something else: a --- rule line of its own, and the +-2 markers at the end. Shared by swap_hyphen_markup and escape_hyphens.



127
128
129
130
131
# File 'lib/rsyntaxtree/element.rb', line 127

def self.hyphen_safe_lines(text)
  path = text[/\^?(?:\+-?>?<?\d+)+\^?\z/]
  body = path ? text[0...-path.length] : text
  [body.split('\n', -1), path.to_s]
end

Instance Method Details

#add_child(child_id) ⇒ Object



471
472
473
# File 'lib/rsyntaxtree/element.rb', line 471

def add_child(child_id)
  @children << child_id
end

#empty_label?Boolean

True when the element renders no visible label — its text consists only of whitespace placeholders (from <>) and it carries no enclosure. Such nodes act as pass-through joints: connectors run continuously through them, which lets a <> chain push a leaf down to align with deeper leaves while the line stays unbroken.

A label that is one whole feature matrix keeps its text inside the matrix and leaves none of it here, and the matrix is a decoration on the run rather than the element's own enclosure. Counted as empty, such a node was joined as a pass-through joint and the connectors were run to the middle of it — through the matrix and the rows written in it.

Returns:

  • (Boolean)


102
103
104
105
106
107
108
109
110
111
112
# File 'lib/rsyntaxtree/element.rb', line 102

def empty_label?
  return false if @enclosure && @enclosure != :none

  @content.all? do |c|
    c[:type] == :text &&
      c[:elements].all? do |e|
        !e[:decoration].to_a.include?(:matrix) &&
          e[:text].gsub(WHITESPACE_BLOCK, "").strip.empty?
      end
  end
end

#label_enclosure_roomObject

Room on each side of a label for its own bracket or rectangle. It is part of the width the tree lays the node out at, so that whatever attaches to the node — a connector, a movement arrow, the neighbour beside it — meets the line actually drawn around it. A matrix nested in the label keeps the same room, so the outermost pair of brackets in a feature structure stands as far from its contents as every pair within.



277
278
279
280
281
# File 'lib/rsyntaxtree/element.rb', line 277

def label_enclosure_room
  return 0 unless [:brackets, :rectangle, :brectangle].include?(@enclosure)

  @global[:width_half_x] * MATRIX_BRACKET_ROOM
end

#markup_failure_details(label, charpos) ⇒ Object

Turn a Markup.parse failure into structured error attributes: a code for machines, the label and the offset inside it for people, a fix that has been checked to work, and whether rewriting could help.



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/rsyntaxtree/element.rb', line 218

def markup_failure_details(label, charpos)
  details = { label: label, position: charpos }

  # Nothing left to parse: the raw spaces around it took the whole label
  # away, one of them starting the split and the next ending it.
  if label.strip.empty?
    return details.merge(code: :label_split,
                         hint: "Raw spaces left this label empty. Write a space inside a label as <> (e.g. ⟨<>NP<>⟩).",
                         retryable: true)
  end

  MARKUP_REPAIRS.each do |code, repair, hint|
    repaired = repair.call(label)
    next if repaired == label
    # Through the same preprocessing the real parse uses, or a label
    # under hyphen: literal would be judged against a different string.
    next unless Markup.parse(prepare_markup(repaired))[:status] == :success

    return details.merge(code: code, hint: hint, retryable: true)
  end

  # No single repair worked. In practice this is where two mistakes in
  # one label land — each of them one this could have named alone — so
  # it is the most fixable bucket, not the least: still retryable, with
  # a hint that admits no one cause was found rather than naming a wrong
  # one. retryable: false is for what no rewriting reaches.
  details.merge(code: :invalid_markup,
                hint: "No single cause fits, which usually means more than one mistake in this label. Check that *, _, =, ~, |, { and #(...#) are paired, that hyphens are escaped, and that ⟨ ⟩ are the angle bracket characters.",
                retryable: true)
end

#matrix_bracket_roomObject

Horizontal room a nested matrix needs on each side for its bracket and the air around it.



477
478
479
# File 'lib/rsyntaxtree/element.rb', line 477

def matrix_bracket_room
  @global[:width_half_x] * MATRIX_BRACKET_ROOM
end

#matrix_vertical_roomObject

Vertical room a nested matrix keeps between itself and the rows above and below it.



525
526
527
# File 'lib/rsyntaxtree/element.rb', line 525

def matrix_vertical_room
  @global[:single_x_metrics].height * MATRIX_VERTICAL_ROOM
end

#measure_lines(lines, nested: false) ⇒ Object

Measures a list of label lines, filling in the width and height of every element and aligning the columns that \t marks. A nested matrix runs through here again, which is what lets a feature structure hold another. nested is set for a matrix inside a label. The first row of a label carries an extra margin that holds the text clear of the connector above it; a nested block sits inside that margin already, so counting it again would leave a bracket half a line taller than the rows it encloses.



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/rsyntaxtree/element.rb', line 290

def measure_lines(lines, nested: false)
  total_width = 0
  total_height = 0
  one_bvm_given = nested
  lines.each do |content|
    content_width = 0
    case content[:type]
    when :border, :bborder
      height = @global[:single_line_height] / 2
      content[:height] = height
      total_height += height
    when :text
      row_width = 0
      elements_height = []
      # A line of nothing but shapes is spaced by the shapes themselves, so
      # a grid of boxes closes up instead of showing a seam between its
      # rows. A shape sharing a line with text cannot be: the line keeps the
      # text's rhythm, and a box is nearly as tall as a line, so two of them
      # on consecutive rows would come out edge to edge.
      row_holds_text = content[:elements].any? do |e|
        (e[:decoration] & [:box, :circle, :bar]).empty? && !e[:text].to_s.strip.empty?
      end
      content[:elements].each do |e|
        # A nested matrix is measured by the same code one level down, and
        # reports the size of the block it will occupy in this row: its own
        # content plus the brackets drawn around it.
        if e[:decoration].include?(:matrix)
          inner = measure_lines(e[:matrix], nested: true)
          e[:matrix_width] = inner[:width]
          e[:matrix_height] = inner[:height]
          e[:width] = inner[:width] + matrix_bracket_room * 2
          # Two separate allowances. The block is padded inside its own
          # brackets, above and below, and that padding is part of the row.
          # The gap that keeps the block clear of the rows either side is
          # not: it widens the space the row is entered on, so the bracket
          # is not simply drawn over the line before it.
          e[:height] = inner[:height] + matrix_vertical_room * 2
          # Twice the padding: the bracket is drawn that far above the
          # baseline of its first row, so the first helping only pays for
          # the padding and the second is what actually separates the
          # bracket from the descenders of the line above it.
          content[:top_room] = matrix_vertical_room * 2
          elements_height << with_first_row_margin(e[:height], one_bvm_given)
          one_bvm_given = true
          row_width += e[:width]
          next
        end

        text = e[:text]
        # Handle escaped square brackets
        text = text.gsub('\\[', '[')
                  .gsub('\\]', ']')
        # Typographic apostrophe: render a straight ASCII apostrophe (U+0027)
        # as a curly apostrophe (U+2019) for smarter typography, e.g. the
        # X-bar prime in "T'". Applied before metrics so the measured glyph
        # matches the rendered one.
        text = text.gsub("'", "")
        e[:text] = text.gsub(" ", WHITESPACE_BLOCK)
                      .gsub(">", '&#62;')
                      .gsub("<", '&#60;')

        @contains_phrase = true if text.include?(" ")
        decoration = e[:decoration]
        fontsize = decoration.include?(:small) ? @fontsize * SUBSCRIPT_CONST : @fontsize
        fontsize = decoration.include?(:subscript) || decoration.include?(:superscript) ? fontsize * SUBSCRIPT_CONST : fontsize
        style    = decoration.include?(:italic) || decoration.include?(:bolditalic) ? :italic : :normal
        weight   = decoration.include?(:bold) || decoration.include?(:bolditalic) ? :bold : :normal
        # Bold/italic are expressed through Pango's style/weight parameters,
        # so a single family list is measured for every decoration.
        font = @fontset[:family]

        standard_metrics = FontMetrics.get_metrics('X', font, fontsize, :normal, :normal)

        height = standard_metrics.height
        # The rhythm a row advances by belongs to the label, not to the size
        # a run inside it happens to be set at. Taken from the run, a label
        # that is nothing but a subscript measured a subscript tall and came
        # out shorter than the text drawn in it, so its daughters were
        # placed up into it.
        line_height = @global[:single_x_metrics].height
        if /\A[<>]+\z/ =~ text
          width = standard_metrics.width * text.size / 2
        elsif text.contains_emoji?
          segments = text.split_by_emoji
          width = 0
          segments.each do |seg|
            ch = if /\s/ =~ seg[:char]
                   't'
                 else
                   seg[:char]
                 end
            # Emoji segments are measured with the same family list;
            # fontconfig/coretext falls back to an emoji font.
            metrics = FontMetrics.get_metrics(ch, font, fontsize, style, weight)
            width += metrics.width
          end
        else
          text.gsub!("\\\\", 'i')
          text.gsub!("\\", "")
          text.gsub!(" ", "x")
          text.gsub!("%", "X")
          metrics = FontMetrics.get_metrics(text, font, fontsize, style, weight)
          width = metrics.width
        end

        if e[:decoration].include?(:box) || e[:decoration].include?(:circle) || e[:decoration].include?(:bar)
          # One size and one height for every enclosure in a figure, so a
          # hatched circle, an empty box and a lettered tag line up and
          # share a diameter. The line's rhythm used to set the size, which
          # left a box standing a head taller than the numeral inside;
          # centring each shape on its own glyph instead made the box
          # around 's' sit lower than the one around 'G'.
          #
          # The shape is centred on a capital — the half-way point of the
          # letters it will usually hold — and drawn at ENCLOSURE_SIZE,
          # which is wide enough that a descender still clears the bottom.
          # Centring on the whole cap-to-descender band instead would sit
          # the shape low around the digits and capitals that fill most
          # tags, since those never reach below the baseline. It grows past
          # ENCLOSURE_SIZE only for content that will not fit.
          band = FontMetrics.get_metrics("Xg", font, fontsize, :normal, :normal)
          descender = band.ink_height - band.ink_above
          centre = FontMetrics.visual_centre(font, fontsize)

          ink = FontMetrics.get_metrics(text, font, fontsize, style, weight)
          ink_height = ink.ink_height.to_f
          # Deep enough for a descender, so the one letter in a hundred that
          # has one does not get a taller box than its neighbours.
          half = [fontsize * ENCLOSURE_SIZE / 2.0, centre + descender].max

          if ink_height.positive?
            reach = [ink.ink_above - centre, centre - (ink.ink_above - ink_height)].max
            half = reach + fontsize * ENCLOSURE_PADDING if reach > half
          end

          e[:enc_height] = half * 2
          e[:enc_above] = centre + half

          height = if row_holds_text
                     [height, e[:enc_height] + fontsize * ENCLOSURE_PADDING * 2].max
                   else
                     e[:enc_height]
                   end

          e[:content_width] = width
          width += if e[:text].size == 1
                    [e[:enc_height] - width, 0].max
                  else
                    @global[:width_half_x]
                  end
        end

        if e[:decoration].include?(:whitespace)
          width = @global[:width_half_x] / 2 * e[:text].size / 4
          e[:text] = ""
        end

        e[:height] = height

        # What the label measures is not what its rows advance by. A line
        # of nothing but shapes advances by the shapes, so a grid closes
        # up, but it still measures a full line: the tree places a level by
        # the height of the nodes above it, so a node measured short of the
        # rhythm pulls its own children up and off the row its cousins sit
        # on.
        elements_height << with_first_row_margin([height, line_height].max, one_bvm_given)
        one_bvm_given = true

        e[:width] = width
        row_width += width
      end

      content[:height] = elements_height.max
      total_height += elements_height.max + content[:top_room].to_f
      content_width += row_width
    end
    total_width = content_width if total_width < content_width
  end
  { width: align_columns(lines, total_width), height: total_height }
end

#prepare_markup(text) ⇒ Object

What the markup parser is actually handed: under hyphen: literal a hyphen and its escape swap roles before parsing.



155
156
157
# File 'lib/rsyntaxtree/element.rb', line 155

def prepare_markup(text)
  @global[:literal_hyphen] ? swap_hyphen_markup(text) : text
end

#rule_name_break(content) ⇒ Object

Where the break that names the rule is, or nil if the label does not name one. It is the break at the top level of the label: a feature matrix is written with breaks of its own and those belong to its columns, so a step whose category is a matrix can still name the rule that produced it.

Exactly one, and no line break outside a matrix: a rule has one name, and a label written over several lines is a matrix rather than a step. A break the writer escaped is theirs and is not a break here.



489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# File 'lib/rsyntaxtree/element.rb', line 489

def rule_name_break(content)
  depth = 0
  breaks = []
  lined = false
  index = 0
  while index < content.length
    case content[index, 2]
    when '\\t' then breaks << index if depth.zero?
    when '\\n' then lined = true if depth.zero?
    when "#(" then depth += 1
    when "#)" then depth -= 1
    else
      # A backslash takes the character after it, whatever that character
      # is. Only the two breaks above are read here; every other escape is
      # the notation's, and `\#` is a hash that opens no matrix — counted as
      # one it left the depth wrong and the break that names the rule was
      # looked for in the wrong place.
      index += content[index] == "\\" ? 2 : 1
      next
    end
    index += 2
  end
  breaks.first if !lined && breaks.size == 1
end

#setupObject



264
265
266
267
268
269
# File 'lib/rsyntaxtree/element.rb', line 264

def setup
  layout = measure_lines(@content)
  @text_width = layout[:width]
  @content_width = layout[:width] + label_enclosure_room * 2
  @content_height = layout[:height]
end

#swap_hyphen_markup(text) ⇒ Object



133
134
135
136
# File 'lib/rsyntaxtree/element.rb', line 133

def swap_hyphen_markup(text)
  lines, path = Element.hyphen_safe_lines(text)
  lines.map { |line| /\A-{3,}\z/.match?(line) ? line : swap_hyphens(line) }.join('\n') + path
end

#swap_hyphens(text) ⇒ Object



138
139
140
141
142
143
144
# File 'lib/rsyntaxtree/element.rb', line 138

def swap_hyphens(text)
  # A character no label can contain, so the two swaps cannot see each
  # other's output. Written as an escape: a literal NUL in the source
  # makes git and grep treat this file as binary.
  placeholder = "\u0000"
  text.gsub('\\-', placeholder).gsub("-", '\\-').gsub(placeholder, "-")
end

#validate_color!(color, content) ⇒ Object

A hex color is already constrained by the grammar; a named color is checked against the names librsvg can paint, because an unknown name does not fail downstream — it silently comes out black.

Raises:



252
253
254
255
256
257
258
259
260
261
262
# File 'lib/rsyntaxtree/element.rb', line 252

def validate_color!(color, content)
  return if color.nil? || color.start_with?("#")
  return if COLOR_NAMES.include?(color.downcase)

  raise RSTError.new(+"Error: input text contains an unknown color '#{color}'" \
                     "\n > #{content}",
                     code: :unknown_color,
                     label: content,
                     hint: "Use a CSS color name or a hex code: @blue:VP, @#3af:VP, @#33aaff:VP.",
                     retryable: true)
end

#with_first_row_margin(height, already_given) ⇒ Object

The first row of a label carries a margin above it that holds the text clear of the connector coming down to the node. Every row is entered through here so that the one holding a feature matrix is given it too: it used to be measured without, and a matrix node came out shorter than the rows drawn in it, which sat its daughters up inside the matrix.



519
520
521
# File 'lib/rsyntaxtree/element.rb', line 519

def with_first_row_margin(height, already_given)
  already_given ? height : height + @global[:box_vertical_margin]
end