Class: RSyntaxTree::Element

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of Element.



17
18
19
20
21
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
# File 'lib/rsyntaxtree/element.rb', line 17

def initialize(id, parent, content, level, fontset, fontsize, global)
  @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
  @raw_content = content.sub(/\^?(?:\+-?>?<?\d+)+\^?\z/, '')

  parsed = Markup.parse(@global[:literal_hyphen] ? swap_hyphen_markup(content) : content)

  if parsed[:status] == :success
    results = parsed[:results]
  else
    error_text = +"Error: input text contains an invalid string"
    error_text += "\n > " + content
    raise RSTError, error_text
  end
  @content = results[:contents]
  @paths = results[:paths]
  @enclosure = results[:enclosure]
  @triangle = results[:triangle]
  @color = results[:color]
  @region = results[:region]
  @region_color = results[:region_color]

  @contains_phrase = false
  setup
end

Instance Attribute Details

#childrenObject

Returns the value of attribute children.



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

def children
  @children
end

#colorObject

Returns the value of attribute color.



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

def color
  @color
end

#contains_phraseObject

Returns the value of attribute contains_phrase.



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

def contains_phrase
  @contains_phrase
end

#contentObject

Returns the value of attribute content.



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

def content
  @content
end

#content_heightObject

Returns the value of attribute content_height.



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

def content_height
  @content_height
end

#content_widthObject

Returns the value of attribute content_width.



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

def content_width
  @content_width
end

#enclosureObject

Returns the value of attribute enclosure.



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

def enclosure
  @enclosure
end

#fontObject

Returns the value of attribute font.



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

def font
  @font
end

#fontsizeObject

Returns the value of attribute fontsize.



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

def fontsize
  @fontsize
end

#heightObject

Returns the value of attribute height.



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

def height
  @height
end

#horizontal_indentObject

Returns the value of attribute horizontal_indent.



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

def horizontal_indent
  @horizontal_indent
end

#idObject

Returns the value of attribute id.



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

def id
  @id
end

#levelObject

Returns the value of attribute level.



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

def level
  @level
end

#parentObject

Returns the value of attribute parent.



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

def parent
  @parent
end

#pathObject

Returns the value of attribute path.



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

def path
  @path
end

#raw_contentObject

Returns the value of attribute raw_content.



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

def raw_content
  @raw_content
end

#regionObject

Returns the value of attribute region.



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

def region
  @region
end

#region_colorObject

Returns the value of attribute region_color.



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

def region_color
  @region_color
end

#text_widthObject

Returns the value of attribute text_width.



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

def text_width
  @text_width
end

#triangleObject

Returns the value of attribute triangle.



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

def triangle
  @triangle
end

#typeObject

Returns the value of attribute type.



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

def type
  @type
end

#vertical_indentObject

Returns the value of attribute vertical_indent.



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

def vertical_indent
  @vertical_indent
end

#widthObject

Returns the value of attribute width.



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

def width
  @width
end

Instance Method Details

#add_child(child_id) ⇒ Object



308
309
310
# File 'lib/rsyntaxtree/element.rb', line 308

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.

Returns:

  • (Boolean)


66
67
68
69
70
71
72
73
# File 'lib/rsyntaxtree/element.rb', line 66

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

  @content.all? do |c|
    c[:type] == :text &&
      c[:elements].all? { |e| e[:text].gsub(WHITESPACE_BLOCK, "").strip.empty? }
  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.



114
115
116
117
118
# File 'lib/rsyntaxtree/element.rb', line 114

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

  @global[:width_half_x] * MATRIX_BRACKET_ROOM
end

#matrix_bracket_roomObject

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



314
315
316
# File 'lib/rsyntaxtree/element.rb', line 314

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.



320
321
322
# File 'lib/rsyntaxtree/element.rb', line 320

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.



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/rsyntaxtree/element.rb', line 127

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 << e[:height]
          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
        line_height = 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 = standard_metrics.ink_above / 2.0

          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.
        measured = [height, line_height].max

        if one_bvm_given
          elements_height << measured
        else
          one_bvm_given = true
          elements_height << measured + @global[:box_vertical_margin]
        end

        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

#setupObject



101
102
103
104
105
106
# File 'lib/rsyntaxtree/element.rb', line 101

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

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.



84
85
86
87
88
89
90
91
# File 'lib/rsyntaxtree/element.rb', line 84

def swap_hyphen_markup(text)
  path = text[/\^?(?:\+-?>?<?\d+)+\^?\z/]
  body = path ? text[0...-path.length] : text
  swapped = body.split('\n', -1).map do |line|
    /\A-{3,}\z/.match?(line) ? line : swap_hyphens(line)
  end.join('\n')
  swapped + path.to_s
end

#swap_hyphens(text) ⇒ Object



93
94
95
96
97
98
99
# File 'lib/rsyntaxtree/element.rb', line 93

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