Module: Fontisan::Ufo::Plist

Defined in:
lib/fontisan/ufo/plist.rb

Overview

Minimal XML plist parser/serializer for the files that UFO v3 uses. UFO uses XML plists exclusively; binary plists are not used in the spec.

The parser is typed at the value level: strings stay as String, integers stay as Integer, booleans become true/false, dicts become Hash<String, Object>, arrays become Array. Dates, data blobs, and nested structures are supported.

Defined Under Namespace

Classes: ParseError

Constant Summary collapse

PLIST_DTD =
"-//Apple//DTD PLIST 1.0//EN"
PLIST_NS =
"http://www.apple.com/DTDs/PropertyList-1.0.dtd"

Class Method Summary collapse

Class Method Details

.create_doc_internal_subset(xml) ⇒ Object



88
89
90
# File 'lib/fontisan/ufo/plist.rb', line 88

def self.create_doc_internal_subset(xml)
  xml.doc.create_internal_subset("plist", PLIST_DTD, PLIST_NS)
end

.emit(value) ⇒ String

Returns the XML plist text.

Parameters:

  • value (Object)

    the value to serialize

Returns:

  • (String)

    the XML plist text



33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/fontisan/ufo/plist.rb', line 33

def self.emit(value)
  builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
    create_doc_internal_subset(xml)
    xml.plist(version: "1.0") do
      emit_value(xml, value)
    end
  end
  builder.to_xml(
    save_with:
      Nokogiri::XML::Node::SaveOptions::AS_XML |
      Nokogiri::XML::Node::SaveOptions::NO_DECLARATION,
  )
end

.emit_dict_body(xml, hash) ⇒ Object



110
111
112
113
114
115
# File 'lib/fontisan/ufo/plist.rb', line 110

def self.emit_dict_body(xml, hash)
  hash.each do |k, v|
    xml.key(k.to_s)
    emit_value(xml, v)
  end
end

.emit_value(xml, value) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/fontisan/ufo/plist.rb', line 92

def self.emit_value(xml, value)
  case value
  when nil
    # No <nil/> in Apple's plist DTD; use a sentinel string.
    xml.string("")
  when true then xml.__send__(true)
  when false then xml.__send__(false)
  when Integer then xml.integer(value)
  when Float then xml.real(value)
  when String then xml.string(value)
  when Symbol then xml.string(value.to_s)
  when Array then xml.array { value.each { |v| emit_value(xml, v) } }
  when Hash  then xml.dict { emit_dict_body(xml, value) }
  else
    raise ArgumentError, "cannot plist-encode #{value.class}"
  end
end

.parse(source) ⇒ Object

Returns the deserialized value.

Parameters:

  • source (String, Nokogiri::XML::Document)

    the XML plist

Returns:

  • (Object)

    the deserialized value

Raises:



23
24
25
26
27
28
29
# File 'lib/fontisan/ufo/plist.rb', line 23

def self.parse(source)
  doc = source.is_a?(Nokogiri::XML::Document) ? source : Nokogiri::XML(source)
  root = doc.root
  raise ParseError, "no <plist> root element" unless root&.name == "plist"

  parse_value(root.children.find { |c| c.element? || c.cdata? || c.text? && !c.text.strip.empty? })
end

.parse_array(node) ⇒ Object



84
85
86
# File 'lib/fontisan/ufo/plist.rb', line 84

def self.parse_array(node)
  node.element_children.map { |child| parse_value(child) }
end

.parse_dict(node) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/fontisan/ufo/plist.rb', line 70

def self.parse_dict(node)
  result = {}
  children = node.element_children.to_a
  # dict children are key/value pairs (key first, then value)
  while children.any?
    key_node = children.shift
    raise ParseError, "dict key is not <key>" unless key_node.name == "key"

    value_node = children.shift
    result[key_node.text] = parse_value(value_node)
  end
  result
end

.parse_document(source) ⇒ Nokogiri::XML::Document

Returns the typed root.

Returns:

  • (Nokogiri::XML::Document)

    the typed root



48
49
50
# File 'lib/fontisan/ufo/plist.rb', line 48

def self.parse_document(source)
  source.is_a?(Nokogiri::XML::Document) ? source : Nokogiri::XML(source)
end

.parse_value(node) ⇒ Object

---------- private-ish (called via module_function) ----------



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/fontisan/ufo/plist.rb', line 54

def self.parse_value(node)
  return nil if node.nil?

  case node.name
  when "string" then node.text
  when "integer" then node.text.to_i
  when "real" then node.text.to_f
  when "true" then true
  when "false" then false
  when "dict" then parse_dict(node)
  when "array" then parse_array(node)
  else
    raise ParseError, "unsupported plist element: <#{node.name}>"
  end
end