Class: StringDoc Private
- Inherits:
-
Object
- Object
- StringDoc
- 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
-
#collapsed ⇒ Object
readonly
private
Array of
Node
objects. -
#nodes ⇒ Object
readonly
private
Array of
Node
objects.
Class Method Summary collapse
-
.attributes(element) ⇒ Object
private
Returns attributes for an oga element.
-
.attributes_string(element) ⇒ Object
private
Builds a string-based representation of attributes for an oga element.
-
.breadth_first(doc) ⇒ Object
private
Yields nodes from an oga document, breadth-first.
-
.contains_significant_child?(element) ⇒ Boolean
private
Returns true if the given Oga element contains a child node that is significant.
-
.empty ⇒ Object
private
Creates an empty doc.
-
.find_significance(element) ⇒ Object
private
Determines the significance of
element
. -
.from_nodes(nodes) ⇒ Object
private
Creates a
StringDoc
from an array ofNode
objects. - .nodes_from_doc_or_string(doc_node_or_string) ⇒ Object private
-
.significant(name, object, descend: true) ⇒ Object
private
Registers a significant node with a name and an object to handle parsing.
- .significant_types ⇒ Object private
Instance Method Summary collapse
- #==(other) ⇒ Object private
-
#append(doc_or_string) ⇒ Object
private
Appends to this document.
-
#append_html(html) ⇒ Object
private
Appends raw html to this document, without parsing.
-
#clear ⇒ Object
(also: #remove)
private
Clears all nodes.
- #collapse(*significance) ⇒ Object private
- #each(descend: false, &block) ⇒ Object private
-
#each_significant_node(type, descend: false) ⇒ Object
private
Yields each node matching the significant type.
-
#each_significant_node_with_name(type, name, descend: false) ⇒ Object
private
Yields each node matching the significant type and name.
-
#each_significant_node_without_descending_into_type(type, descend: false, &block) ⇒ Object
private
Yields each node matching the significant type, without descending into nodes that are of that type.
- #empty? ⇒ Boolean private
- #finalize_labels(keep: []) ⇒ Object private
-
#find_first_significant_node(type, descend: false) ⇒ Object
private
Returns the first node matching the significant type.
-
#find_significant_nodes(type, descend: false) ⇒ Object
private
Returns nodes matching the significant type.
-
#find_significant_nodes_with_name(type, name, descend: false) ⇒ Object
private
Returns nodes matching the significant type and name.
-
#initialize(html) ⇒ StringDoc
constructor
private
Creates a
StringDoc
from an html string. - #initialize_copy(_) ⇒ Object private
-
#insert_after(node_to_insert, after_node) ⇒ Object
private
Inserts a node after another node contained in this document.
-
#insert_before(node_to_insert, before_node) ⇒ Object
private
Inserts a node before another node contained in this document.
-
#prepend(doc_or_string) ⇒ Object
private
Prepends to this document.
- #remove_empty_nodes ⇒ Object private
-
#remove_node(node_to_delete) ⇒ Object
private
Removes a node from the document.
- #render(output = String.new, context: nil) ⇒ Object (also: #to_html, #to_xml) private
-
#replace(doc_or_string) ⇒ Object
private
Replaces the current document.
-
#replace_node(node_to_replace, replacement_node) ⇒ Object
private
Replaces a node from the document.
- #significance?(*significance) ⇒ Boolean private
- #soft_copy ⇒ Object private
-
#to_s ⇒ Object
private
Returns the node as an xml string, without transforming.
- #transforms? ⇒ Boolean private
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
#collapsed ⇒ Object (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 |
#nodes ⇒ Object (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.
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 |
.empty ⇒ 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 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_types ⇒ 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.
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 |
#clear ⇒ Object 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 || yield 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 || yield 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 || yield 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.
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_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.
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.
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_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.
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_s ⇒ 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 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.
493 494 495 |
# File 'lib/string_doc.rb', line 493 def transforms? @nodes.any?(&:transforms?) end |