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
93 94 95 |
# File 'lib/fontisan/ufo/plist.rb', line 93 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.
35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/fontisan/ufo/plist.rb', line 35 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
115 116 117 118 119 120 |
# File 'lib/fontisan/ufo/plist.rb', line 115 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
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/fontisan/ufo/plist.rb', line 97 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 30 31 |
# 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 do |c| c.element? || c.cdata? || c.text? && !c.text.strip.empty? end) end |
.parse_array(node) ⇒ Object
89 90 91 |
# File 'lib/fontisan/ufo/plist.rb', line 89 def self.parse_array(node) node.element_children.map { |child| parse_value(child) } end |
.parse_dict(node) ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/fontisan/ufo/plist.rb', line 72 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 unless key_node.name == "key" raise ParseError, "dict key is not <key>" end 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.
50 51 52 |
# File 'lib/fontisan/ufo/plist.rb', line 50 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) ----------
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/fontisan/ufo/plist.rb', line 56 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 |