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
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
- .create_doc_internal_subset(xml) ⇒ Object
-
.emit(value) ⇒ String
The XML plist text.
- .emit_dict_body(xml, hash) ⇒ Object
- .emit_value(xml, value) ⇒ Object
-
.parse(source) ⇒ Object
The deserialized value.
- .parse_array(node) ⇒ Object
- .parse_dict(node) ⇒ Object
-
.parse_document(source) ⇒ Nokogiri::XML::Document
The typed root.
-
.parse_value(node) ⇒ Object
---------- private-ish (called via module_function) ----------.
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.
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.
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.
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 |