Class: Sevgi::Derender::Document

Inherits:
Object
  • Object
show all
Defined in:
lib/sevgi/derender/document.rb

Overview

Parsed SVG/XML document wrapper used by the derender pipeline.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(content) ⇒ void

Builds a parsed document wrapper from SVG/XML content.

Parameters:

  • content (String)

    SVG/XML source content

Raises:

  • (Sevgi::ArgumentError)

    when content is malformed XML

  • (Sevgi::ArgumentError)

    when content has no root element



62
63
64
65
# File 'lib/sevgi/derender/document.rb', line 62

def initialize(content)
  @doc = self.class.parse(content)
  @decl = self.class.declaration(content)
end

Instance Attribute Details

#declString? (readonly)

Returns the source XML declaration when present.

Returns:

  • (String, nil)


55
56
57
# File 'lib/sevgi/derender/document.rb', line 55

def decl
  @decl
end

#docNokogiri::XML::Document (readonly)

Returns the parsed XML document.

Returns:

  • (Nokogiri::XML::Document)


51
52
53
# File 'lib/sevgi/derender/document.rb', line 51

def doc
  @doc
end

Class Method Details

.declaration(content) ⇒ String?

Extracts the XML declaration from SVG/XML content.

Parameters:

  • content (String)

    SVG/XML source content

Returns:

  • (String, nil)

    XML declaration line, if present



43
44
45
46
47
# File 'lib/sevgi/derender/document.rb', line 43

def self.declaration(content)
  return unless (content = content.to_s.lstrip).start_with?("<?xml ")

  content[/\A<\?xml\b.*?\?>/m]
end

.load_file(path) ⇒ Sevgi::Derender::Document

Loads and parses an SVG/XML file.

Each call reads the current file content and returns an isolated parsed document. Caller mutation of one loaded document does not affect later loads of the same path.

Parameters:

  • path (String)

    path to the source file, with or without .svg extension

Returns:

Raises:

  • (Sevgi::ArgumentError)

    when the file cannot be found or file content is malformed XML

  • (Errno::EACCES)

    when the file cannot be read



17
18
19
20
21
22
23
24
# File 'lib/sevgi/derender/document.rb', line 17

def self.load_file(path)
  entry = ::File.expand_path(F.qualify(path, "svg"))

  ArgumentError.("File not found: #{path}") unless ::File.exist?(entry)

  content = ::File.read(entry)
  new(content)
end

.parse(content) ⇒ Nokogiri::XML::Document

Parses SVG/XML content in strict XML mode.

Parameters:

  • content (String)

    SVG/XML source content

Returns:

  • (Nokogiri::XML::Document)

    parsed XML document

Raises:

  • (Sevgi::ArgumentError)

    when content is not well-formed XML

  • (Sevgi::ArgumentError)

    when content has no root element



31
32
33
34
35
36
37
38
# File 'lib/sevgi/derender/document.rb', line 31

def self.parse(content)
  Nokogiri::XML(content.to_s.lstrip, &:strict).tap do |doc|
    ArgumentError.("XML document has no root element") unless doc.root
  end

rescue Nokogiri::XML::SyntaxError => e
  raise ArgumentError, "Malformed XML: #{e.message.lines.first.strip}", cause: e
end

Instance Method Details

#decompile(id = nil) ⇒ Sevgi::Derender::Node

Converts the root or selected node into a derender node.

Parameters:

  • id (String, nil) (defaults to: nil)

    optional SVG id selecting a node inside the document

Returns:

Raises:

  • (Sevgi::ArgumentError)

    when the document has no root element or the id is absent



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/sevgi/derender/document.rb', line 71

def decompile(id = nil)
  if id
    if (found = doc.xpath("//*[@id=#{xpath_literal(id)}]") || []).empty?
      ArgumentError.("No such element with id '#{id}' in document")
    end

    found.first
  else
    doc.root
  end => element

  ArgumentError.("XML document has no root element") unless element

  Node.new(element, pres, namespaces: namespace_scope(element))
end

#presArray<String>

Returns XML declaration and pre-root nodes preserved for root decompilation. The result contains only String lines and omits the declaration when the source did not provide one.

Returns:

  • (Array<String>)

    preamble XML lines



90
91
92
93
94
95
96
# File 'lib/sevgi/derender/document.rb', line 90

def pres
  @pres ||= [].tap do |lines|
    lines.append(*doc.children.take_while { |node| node != doc.root }.map(&:to_xml))
    lines.unshift(decl) if decl && lines.first != decl
    lines.compact!
  end
end