Class: StringDoc Private

Inherits:
Object
  • Object
show all
Includes:
Enumerable, Pakyow::Support::Silenceable
Defined in:
lib/string_doc.rb,
lib/string_doc/node.rb,
lib/string_doc/meta_node.rb,
lib/string_doc/attributes.rb,
lib/string_doc/meta_attributes.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

String-based XML document optimized for fast manipulation and rendering.

In Pakyow, we rarely care about every node in a document. Instead, only significant nodes and immediate children are available for manipulation. StringDoc provides “just enough” for our purposes. A StringDoc is represented as a multi- dimensional array of strings, making rendering essentially a flatten.join.

Because less work is performed during render, StringDoc is consistently faster than rendering a document using Nokigiri or Oga. One obvious tradeoff is that parsing is much slower (we use Oga to parse the XML, then convert it into a StringDoc). This is an acceptable tradeoff because we only pay the parsing cost once (when the Pakyow application boots).

All that to say, StringDoc is a tool that is very specialized to Pakyow’s use-case. Use it only when a longer parse time is acceptable and you only care about a handful of identifiable nodes in a document.

Defined Under Namespace

Classes: Attributes, MetaAttributes, MetaNode, Node

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(html) ⇒ StringDoc

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Creates a StringDoc from an html string.



143
144
145
146
# File 'lib/string_doc.rb', line 143

def initialize(html)
  @nodes = parse(Oga.parse_html(html))
  @collapsed = nil
end

Instance Attribute Details

#collapsedObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Array of Node objects.



139
140
141
# File 'lib/string_doc.rb', line 139

def collapsed
  @collapsed
end

#nodesObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Array of Node objects.



139
140
141
# File 'lib/string_doc.rb', line 139

def nodes
  @nodes
end

Class Method Details

.attributes(element) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns attributes for an oga element.



80
81
82
83
84
85
86
# File 'lib/string_doc.rb', line 80

def attributes(element)
  if element.is_a?(Oga::XML::Element)
    element.attributes
  else
    []
  end
end

.attributes_string(element) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Builds a string-based representation of attributes for an oga element.



90
91
92
93
94
# File 'lib/string_doc.rb', line 90

def attributes_string(element)
  attributes(element).each_with_object(String.new) do |attribute, string|
    string << " #{attribute.name}=\"#{attribute.value}\""
  end
end

.breadth_first(doc) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Yields nodes from an oga document, breadth-first.



64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/string_doc.rb', line 64

def breadth_first(doc)
  queue = [doc]

  until queue.empty?
    element = queue.shift

    if element == doc
      queue.concat(element.children.to_a); next
    end

    yield element
  end
end

.contains_significant_child?(element) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns true if the given Oga element contains a child node that is significant.

Returns:

  • (Boolean)


108
109
110
111
112
113
114
115
# File 'lib/string_doc.rb', line 108

def contains_significant_child?(element)
  element.children.each do |child|
    return true if find_significance(child).any?
    return true if contains_significant_child?(child)
  end

  false
end

.emptyObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Creates an empty doc.



36
37
38
39
40
41
# File 'lib/string_doc.rb', line 36

def empty
  allocate.tap do |doc|
    doc.instance_variable_set(:@nodes, [])
    doc.instance_variable_set(:@collapsed, nil)
  end
end

.find_significance(element) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Determines the significance of element.



98
99
100
101
102
103
104
# File 'lib/string_doc.rb', line 98

def find_significance(element)
  significant_types.each_with_object([]) do |(key, info), significance|
    if info[:object].significant?(element)
      significance << key
    end
  end
end

.from_nodes(nodes) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Creates a StringDoc from an array of Node objects.



51
52
53
54
55
56
57
58
59
60
# File 'lib/string_doc.rb', line 51

def from_nodes(nodes)
  allocate.tap do |instance|
    instance.instance_variable_set(:@nodes, nodes)
    instance.instance_variable_set(:@collapsed, nil)

    nodes.each do |node|
      node.parent = instance
    end
  end
end

.nodes_from_doc_or_string(doc_node_or_string) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



123
124
125
126
127
128
129
130
131
132
# File 'lib/string_doc.rb', line 123

def nodes_from_doc_or_string(doc_node_or_string)
  case doc_node_or_string
  when StringDoc
    doc_node_or_string.nodes
  when Node, MetaNode
    [doc_node_or_string]
  else
    StringDoc.new(doc_node_or_string.to_s).nodes
  end
end

.significant(name, object, descend: true) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Registers a significant node with a name and an object to handle parsing.



45
46
47
# File 'lib/string_doc.rb', line 45

def significant(name, object, descend: true)
  significant_types[name] = { object: object, descend: descend }
end

.significant_typesObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



118
119
120
# File 'lib/string_doc.rb', line 118

def significant_types
  @significant_types ||= {}
end

Instance Method Details

#==(other) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



456
457
458
# File 'lib/string_doc.rb', line 456

def ==(other)
  other.is_a?(StringDoc) && @nodes == other.nodes
end

#append(doc_or_string) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Appends to this document.

Accepts a StringDoc or XML String.



326
327
328
329
330
331
332
333
334
335
336
# File 'lib/string_doc.rb', line 326

def append(doc_or_string)
  tap do
    nodes = self.class.nodes_from_doc_or_string(doc_or_string)

    nodes.each do |node|
      node.parent = self
    end

    @nodes.concat(nodes)
  end
end

#append_html(html) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Appends raw html to this document, without parsing.



340
341
342
343
344
345
346
# File 'lib/string_doc.rb', line 340

def append_html(html)
  tap do
    node = Node.new(html.to_s)
    node.parent = self
    @nodes << node
  end
end

#clearObject Also known as: remove

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Clears all nodes.



299
300
301
302
303
# File 'lib/string_doc.rb', line 299

def clear
  tap do
    @nodes.clear
  end
end

#collapse(*significance) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



460
461
462
463
464
465
466
467
468
469
470
471
# File 'lib/string_doc.rb', line 460

def collapse(*significance)
  if significance?(*significance)
    @nodes.each do |node|
      node.children.collapse(*significance)
    end
  else
    @collapsed = render
    @nodes = []
  end

  @collapsed
end

#each(descend: false, &block) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/string_doc.rb', line 182

def each(descend: false, &block)
  return enum_for(:each, descend: descend) unless block_given?

  @nodes.each do |node|
    case node
    when MetaNode
      node.each do |each_meta_node|
        yield each_meta_node
      end
    else
      yield node
    end

    if descend || node.label(:descend) != false
      if node.children.is_a?(StringDoc)
        node.children.each(descend: descend, &block)
      else
        yield node.children
      end
    end
  end
end

#each_significant_node(type, descend: false) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Yields each node matching the significant type.



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/string_doc.rb', line 207

def each_significant_node(type, descend: false)
  return enum_for(:each_significant_node, type, descend: descend) unless block_given?

  each(descend: descend) do |node|
    case node
    when MetaNode
      if node.significant?(type)
        node.each do |each_meta_node|
          yield each_meta_node
        end
      end
    when Node
      if node.significant?(type)
        yield node
      end
    end
  end
end

#each_significant_node_with_name(type, name, descend: false) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Yields each node matching the significant type and name.



259
260
261
262
263
264
265
# File 'lib/string_doc.rb', line 259

def each_significant_node_with_name(type, name, descend: false)
  return enum_for(:each_significant_node_with_name, type, name, descend: descend) unless block_given?

  each_significant_node(type, descend: descend) do |node|
    yield node if node.label(type) == name
  end
end

#each_significant_node_without_descending_into_type(type, descend: false, &block) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Yields each node matching the significant type, without descending into nodes that are of that type.



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
# File 'lib/string_doc.rb', line 228

def each_significant_node_without_descending_into_type(type, descend: false, &block)
  return enum_for(:each_significant_node_without_descending_into_type, type, descend: descend) unless block_given?

  @nodes.each do |node|
    if node.is_a?(Node) || node.is_a?(MetaNode)
      if node.significant?(type)
        case node
        when MetaNode
          node.each do |each_meta_node|
            yield each_meta_node
          end
        when Node
          yield node
        end
      else
        if descend || node.label(:descend) != false
          if node.children.is_a?(StringDoc)
            node.children.each_significant_node_without_descending_into_type(type, descend: descend, &block)
          else
            yield node.children
          end
        end
      end
    end
  end
end

#empty?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Boolean)


489
490
491
# File 'lib/string_doc.rb', line 489

def empty?
  @nodes.empty?
end

#finalize_labels(keep: []) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



174
175
176
177
178
# File 'lib/string_doc.rb', line 174

def finalize_labels(keep: [])
  @nodes.each do |node|
    node.finalize_labels(keep: keep)
  end
end

#find_first_significant_node(type, descend: false) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the first node matching the significant type.



269
270
271
272
273
# File 'lib/string_doc.rb', line 269

def find_first_significant_node(type, descend: false)
  each(descend: descend).find { |node|
    node.significant?(type)
  }
end

#find_significant_nodes(type, descend: false) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns nodes matching the significant type.



277
278
279
280
281
282
283
# File 'lib/string_doc.rb', line 277

def find_significant_nodes(type, descend: false)
  [].tap do |nodes|
    each_significant_node(type, descend: descend) do |node|
      nodes << node
    end
  end
end

#find_significant_nodes_with_name(type, name, descend: false) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns nodes matching the significant type and name.



289
290
291
292
293
294
295
# File 'lib/string_doc.rb', line 289

def find_significant_nodes_with_name(type, name, descend: false)
  [].tap do |nodes|
    each_significant_node_with_name(type, name, descend: descend) do |node|
      nodes << node
    end
  end
end

#initialize_copy(_) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



149
150
151
152
153
154
155
156
157
# File 'lib/string_doc.rb', line 149

def initialize_copy(_)
  super

  @nodes = @nodes.map { |node|
    node.dup.tap do |duped_node|
      duped_node.parent = self
    end
  }
end

#insert_after(node_to_insert, after_node) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Inserts a node after another node contained in this document.



366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/string_doc.rb', line 366

def insert_after(node_to_insert, after_node)
  tap do
    if after_node_index = @nodes.index(after_node)
      nodes = self.class.nodes_from_doc_or_string(node_to_insert)

      nodes.each do |node|
        node.parent = self
      end

      @nodes.insert(after_node_index + 1, *nodes)
    end
  end
end

#insert_before(node_to_insert, before_node) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Inserts a node before another node contained in this document.



382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/string_doc.rb', line 382

def insert_before(node_to_insert, before_node)
  tap do
    if before_node_index = @nodes.index(before_node)
      nodes = self.class.nodes_from_doc_or_string(node_to_insert)

      nodes.each do |node|
        node.parent = self
      end

      @nodes.insert(before_node_index, *nodes)
    end
  end
end

#prepend(doc_or_string) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Prepends to this document.

Accepts a StringDoc or XML String.



352
353
354
355
356
357
358
359
360
361
362
# File 'lib/string_doc.rb', line 352

def prepend(doc_or_string)
  tap do
    nodes = self.class.nodes_from_doc_or_string(doc_or_string)

    nodes.each do |node|
      node.parent = self
    end

    @nodes.unshift(*nodes)
  end
end

#remove_empty_nodesObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



479
480
481
482
483
484
485
486
487
# File 'lib/string_doc.rb', line 479

def remove_empty_nodes
  @nodes.each do |node|
    node.children.remove_empty_nodes
  end

  unless empty?
    @nodes.delete_if(&:empty?)
  end
end

#remove_node(node_to_delete) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Removes a node from the document.



398
399
400
401
402
403
404
# File 'lib/string_doc.rb', line 398

def remove_node(node_to_delete)
  tap do
    @nodes.delete_if { |node|
      node.equal?(node_to_delete)
    }
  end
end

#render(output = String.new, context: nil) ⇒ Object Also known as: to_html, to_xml

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'lib/string_doc.rb', line 423

def render(output = String.new, context: nil)
  if collapsed && empty?
    output << collapsed
  else
    nodes.each do |node|
      case node
      when MetaNode
        node.render(output, context: context)
      when Node
        node.render(output, context: context)
      else
        output << node.to_s
      end
    end

    output
  end
end

#replace(doc_or_string) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Replaces the current document.

Accepts a StringDoc or XML String.



310
311
312
313
314
315
316
317
318
319
320
# File 'lib/string_doc.rb', line 310

def replace(doc_or_string)
  tap do
    nodes = self.class.nodes_from_doc_or_string(doc_or_string)

    nodes.each do |node|
      node.parent = self
    end

    @nodes = nodes
  end
end

#replace_node(node_to_replace, replacement_node) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Replaces a node from the document.



408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/string_doc.rb', line 408

def replace_node(node_to_replace, replacement_node)
  tap do
    if replace_node_index = @nodes.index(node_to_replace)
      nodes_to_insert = self.class.nodes_from_doc_or_string(replacement_node)

      nodes_to_insert.each do |node|
        node.parent = self
      end

      @nodes.insert(replace_node_index + 1, *nodes_to_insert)
      @nodes.delete_at(replace_node_index)
    end
  end
end

#significance?(*significance) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Boolean)


473
474
475
476
477
# File 'lib/string_doc.rb', line 473

def significance?(*significance)
  @nodes.any? { |node|
    node.significance?(*significance) || node.children.significance?(*significance)
  }
end

#soft_copyObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/string_doc.rb', line 160

def soft_copy
  instance = self.class.allocate

  instance.instance_variable_set(:@nodes, @nodes.map { |node|
    duped_node = node.soft_copy
    duped_node.parent = instance
    duped_node
  })

  instance.instance_variable_set(:@collapsed, @collapsed)

  instance
end

#to_sObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the node as an xml string, without transforming.



446
447
448
449
450
451
452
453
454
# File 'lib/string_doc.rb', line 446

def to_s
  if collapsed && empty?
    collapsed
  else
    @nodes.each_with_object(String.new) do |node, string|
      string << node.to_s
    end
  end
end

#transforms?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Boolean)


493
494
495
# File 'lib/string_doc.rb', line 493

def transforms?
  @nodes.any?(&:transforms?)
end