nanaimo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ # frozen-string-literal: true
2
+ module Nanaimo
3
+ module Unicode
4
+ QUOTE_MAP = {
5
+ "\a" => "\\a",
6
+ "\b" => "\\b",
7
+ "\f" => "\\f",
8
+ "\r" => "\\r",
9
+ "\t" => "\\t",
10
+ "\v" => "\\v",
11
+ "\n" => "\\n",
12
+ "'" => "\\'",
13
+ "\"" => "\\\"",
14
+ "\x00" => "\\U0000",
15
+ "\x01" => "\\U0001",
16
+ "\x02" => "\\U0002",
17
+ "\x03" => "\\U0003",
18
+ "\x04" => "\\U0004",
19
+ "\x05" => "\\U0005",
20
+ "\x06" => "\\U0006",
21
+ "\x0E" => "\\U000e",
22
+ "\x0F" => "\\U000f",
23
+ "\x10" => "\\U0010",
24
+ "\x11" => "\\U0011",
25
+ "\x12" => "\\U0012",
26
+ "\x13" => "\\U0013",
27
+ "\x14" => "\\U0014",
28
+ "\x15" => "\\U0015",
29
+ "\x16" => "\\U0016",
30
+ "\x17" => "\\U0017",
31
+ "\x18" => "\\U0018",
32
+ "\x19" => "\\U0019",
33
+ "\x1A" => "\\U001a",
34
+ "\e" => "\\U001b",
35
+ "\x1C" => "\\U001c",
36
+ "\x1D" => "\\U001d",
37
+ "\x1E" => "\\U001e",
38
+ "\x1F" => "\\U001f",
39
+ }.freeze
40
+
41
+ UNQUOTE_MAP = {
42
+ "\n" => "\n",
43
+ "a" => "\a",
44
+ "b" => "\b",
45
+ "f" => "\f",
46
+ "r" => "\r",
47
+ "t" => "\t",
48
+ "v" => "\v",
49
+ "n" => "\n",
50
+ "'" => "'",
51
+ "\"" => "\"",
52
+ }.freeze
53
+
54
+ QUOTE_REGEXP = /\x07|\x08|\f|\r|\t|\v|\n|'|"|\x00|\x01|\x02|\x03|\x04|\x05|\x06|\x0E|\x0F|\x10|\x11|\x12|\x13|\x14|\x15|\x16|\x17|\x18|\x19|\x1A|\x1B|\x1C|\x1D|\x1E|\x1F/
55
+ end
56
+ end
@@ -0,0 +1,3 @@
1
+ module Nanaimo
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,174 @@
1
+ module Nanaimo
2
+ # Transforms native ruby objects or Plist objects into their ASCII Plist
3
+ # string representation.
4
+ #
5
+ class Writer
6
+ autoload :XMLWriter, 'nanaimo/writer/xml'
7
+
8
+ # The magic comment that denotes a UTF8-encoded plist.
9
+ #
10
+ UTF8 = "// !$*UTF8*$!\n".freeze
11
+
12
+ # @param plist [Plist,String,Hash,Array] The plist obejct to write
13
+ # @param pretty [Boolean] Whether to serialize annotations and add
14
+ # spaces and newlines to make output more legible
15
+ # @param output [#<<] The output stream to write the plist to
16
+ #
17
+ def initialize(plist, pretty = true, output = ::String.new)
18
+ @plist = plist
19
+ @pretty = pretty
20
+ @output = output
21
+ @indent = 0
22
+ @newlines = true
23
+ end
24
+
25
+ # Writes the plist to the given output.
26
+ #
27
+ def write
28
+ write_utf8
29
+ write_object(@plist.root_object)
30
+ write_newline
31
+ end
32
+
33
+ attr_reader :indent, :pretty, :output, :newlines
34
+ private :indent, :pretty, :output, :newlines
35
+
36
+ private
37
+
38
+ def write_utf8
39
+ output << UTF8
40
+ end
41
+
42
+ def write_newline
43
+ output << if newlines
44
+ "\n"
45
+ else
46
+ ' '
47
+ end
48
+ end
49
+
50
+ def write_object(object)
51
+ case object
52
+ when Array, ::Array
53
+ write_array(object)
54
+ when Dictionary, ::Hash
55
+ write_dictionary(object)
56
+ when %r{[^\w\./]}, QuotedString, ''
57
+ write_quoted_string(object)
58
+ when String, ::String, Symbol
59
+ write_string(object)
60
+ when Data
61
+ write_data(object)
62
+ else
63
+ raise "Cannot write #{object} to an ascii plist"
64
+ end
65
+ write_annotation(object) if pretty
66
+ output
67
+ end
68
+
69
+ def write_string(object)
70
+ output << value_for(object).to_s
71
+ end
72
+
73
+ def write_quoted_string(object)
74
+ output << '"' << Unicode.quotify_string(value_for(object)) << '"'
75
+ end
76
+
77
+ def write_data(object)
78
+ output << '<'
79
+ value_for(object).unpack('H*').first.chars.each_with_index do |c, i|
80
+ output << "\n" if i > 0 && (i % 16).zero?
81
+ output << ' ' if i > 0 && (i % 4).zero?
82
+ output << c
83
+ end
84
+ output << '>'
85
+ end
86
+
87
+ def write_array(object)
88
+ write_array_start
89
+ value = value_for(object)
90
+ value.each do |v|
91
+ write_array_element(v)
92
+ end
93
+ write_array_end
94
+ end
95
+
96
+ def write_array_start
97
+ output << '('
98
+ write_newline if newlines
99
+ push_indent!
100
+ end
101
+
102
+ def write_array_end
103
+ pop_indent!
104
+ write_indent
105
+ output << ')'
106
+ end
107
+
108
+ def write_array_element(object)
109
+ write_indent
110
+ write_object(object)
111
+ output << ','
112
+ write_newline
113
+ end
114
+
115
+ def write_dictionary(object)
116
+ write_dictionary_start
117
+ value = value_for(object)
118
+ value.each do |key, val|
119
+ write_dictionary_key_value_pair(key, val)
120
+ end
121
+ write_dictionary_end
122
+ end
123
+
124
+ def write_dictionary_start
125
+ output << '{'
126
+ write_newline if newlines
127
+ push_indent!
128
+ end
129
+
130
+ def write_dictionary_end
131
+ pop_indent!
132
+ write_indent
133
+ output << '}'
134
+ end
135
+
136
+ def write_dictionary_key_value_pair(key, value)
137
+ write_indent
138
+ write_object(key)
139
+ output << ' = '
140
+ write_object(value)
141
+ output << ';'
142
+ write_newline
143
+ end
144
+
145
+ def write_annotation(object)
146
+ return output unless object.is_a?(Nanaimo::Object)
147
+ annotation = object.annotation
148
+ return output unless annotation && !annotation.empty?
149
+ output << " /*#{annotation}*/"
150
+ end
151
+
152
+ def value_for(object)
153
+ if object.is_a?(Nanaimo::Object)
154
+ object.value
155
+ else
156
+ object
157
+ end
158
+ end
159
+
160
+ def push_indent!
161
+ @indent += 1
162
+ end
163
+
164
+ def pop_indent!
165
+ @indent -= 1
166
+ @indent = 0 if @indent < 0
167
+ end
168
+
169
+ def write_indent
170
+ output << "\t" * indent if newlines
171
+ output
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,131 @@
1
+ module Nanaimo
2
+ class Writer
3
+ # Transforms native ruby objects or Plist objects into their XML Plist
4
+ # string representation.
5
+ #
6
+ class XMLWriter < Writer
7
+ autoload :Base64, 'base64'
8
+
9
+ def write
10
+ write_xml_header
11
+ write_object(@plist.root_object)
12
+ write_newline
13
+ write_xml_footer
14
+ end
15
+
16
+ private
17
+
18
+ def write_object(object)
19
+ case object
20
+ when Float, Integer
21
+ write_number(object)
22
+ when Time, Date, DateTime
23
+ write_date(object)
24
+ when true, false
25
+ write_boolean(object)
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def write_xml_header
32
+ output << <<-EOS
33
+ <?xml version="1.0" encoding="UTF-8"?>
34
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
35
+ <plist version="1.0">
36
+ EOS
37
+ end
38
+
39
+ def write_xml_footer
40
+ output << <<-EOS
41
+ </plist>
42
+ EOS
43
+ end
44
+
45
+ def write_annotation(_)
46
+ end
47
+
48
+ def write_number(object)
49
+ type = object.integer? ? 'integer' : 'real'
50
+ output << "<#{type}>#{object}</#{type}>"
51
+ end
52
+
53
+ def write_boolean(object)
54
+ output << "<#{object}/>"
55
+ end
56
+
57
+ def write_date(object)
58
+ output << '<date>' << object.iso8601 << '</date>'
59
+ end
60
+
61
+ def write_string(object)
62
+ output << '<string>' << Unicode.xml_escape_string(value_for(object)) << '</string>'
63
+ end
64
+
65
+ def write_quoted_string(object)
66
+ write_string(object)
67
+ end
68
+
69
+ def write_data(object)
70
+ output << '<data>'
71
+ data = Base64.encode64(value_for(object)).delete("\n")
72
+ data = data.scan(/.{1,76}/).join("\n") if pretty
73
+ output << data << '</data>'
74
+ end
75
+
76
+ def write_array(object)
77
+ return output << '<array/>' if value_for(object).empty?
78
+ super
79
+ end
80
+
81
+ def write_array_start
82
+ output << '<array>'
83
+ write_newline if newlines
84
+ push_indent!
85
+ end
86
+
87
+ def write_array_end
88
+ pop_indent!
89
+ write_indent
90
+ output << '</array>'
91
+ end
92
+
93
+ def write_array_element(object)
94
+ write_indent
95
+ write_object(object)
96
+ write_newline
97
+ end
98
+
99
+ def write_dictionary(object)
100
+ object = value_for(object)
101
+ return output << '<dict/>' if object.empty?
102
+ keys = object.keys.sort_by(&:to_s)
103
+ object = keys.each_with_object({}) do |key, hash|
104
+ hash[key.to_s] = object[key]
105
+ end
106
+ super(object)
107
+ end
108
+
109
+ def write_dictionary_start
110
+ output << '<dict>'
111
+ write_newline if newlines
112
+ push_indent!
113
+ end
114
+
115
+ def write_dictionary_end
116
+ pop_indent!
117
+ write_indent
118
+ output << '</dict>'
119
+ end
120
+
121
+ def write_dictionary_key_value_pair(key, value)
122
+ write_indent
123
+ output << '<key>' << Unicode.xml_escape_string(value_for(key)) << '</key>'
124
+ write_newline
125
+ write_indent
126
+ write_object(value)
127
+ write_newline
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,76 @@
1
+ module Nanaimo
2
+ # Transforms native ruby objects or Plist objects into their ASCII Plist
3
+ # string representation, formatted as Xcode writes Xcode projects.
4
+ #
5
+ class XcodeProjectWriter < Writer
6
+ ISA = String.new('isa', '')
7
+ private_constant :ISA
8
+
9
+ def initialize(*)
10
+ super
11
+ @objects_section = false
12
+ end
13
+
14
+ private
15
+
16
+ def write_dictionary(object)
17
+ n = newlines
18
+ @newlines = false if flat_dictionary?(object)
19
+ return super(sort_dictionary(object)) unless @objects_section
20
+ @objects_section = false
21
+ write_dictionary_start
22
+ value = value_for(object)
23
+ objects_by_isa = value.group_by { |_k, v| isa_for(v) }
24
+ objects_by_isa.each do |isa, kvs|
25
+ write_newline
26
+ output << "/* Begin #{isa} section */"
27
+ write_newline
28
+ sort_dictionary(kvs).each do |k, v|
29
+ write_dictionary_key_value_pair(k, v)
30
+ end
31
+ output << "/* End #{isa} section */"
32
+ write_newline
33
+ end
34
+ write_dictionary_end
35
+ ensure
36
+ @newlines = n
37
+ end
38
+
39
+ def write_dictionary_key_value_pair(k, v)
40
+ @objects_section = true if value_for(k) == 'objects'
41
+ super
42
+ end
43
+
44
+ def sort_dictionary(dictionary)
45
+ hash = value_for(dictionary)
46
+ hash.to_a.sort do |(k1, v1), (k2, v2)|
47
+ v2_isa = isa_for(v2)
48
+ v1_isa = v2_isa && isa_for(v1)
49
+ comp = v1_isa <=> v2_isa
50
+ next comp if !comp.zero? && v1_isa
51
+
52
+ key1 = value_for(k1)
53
+ key2 = value_for(k2)
54
+ next -1 if key1 == 'isa'
55
+ next 1 if key2 == 'isa'
56
+ key1 <=> key2
57
+ end
58
+ end
59
+
60
+ def isa_for(dictionary)
61
+ dictionary = value_for(dictionary)
62
+ return unless dictionary.is_a?(Hash)
63
+ isa = dictionary.values_at('isa', ISA).map(&method(:value_for)).compact.first
64
+ isa && value_for(isa)
65
+ end
66
+
67
+ def flat_dictionary?(dictionary)
68
+ case isa_for(dictionary)
69
+ when 'PBXBuildFile', 'PBXFileReference'
70
+ true
71
+ else
72
+ false
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'nanaimo/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'nanaimo'
8
+ spec.version = Nanaimo::VERSION
9
+ spec.authors = ['Danielle Tomlinson', 'Samuel Giddins']
10
+ spec.email = ['dan@tomlinson.io', 'segiddins@segiddins.me']
11
+
12
+ spec.summary = 'A library for (de)serialization of ASCII Plists.'
13
+ spec.homepage = 'https://github.com/CocoaPods/Nanaimo'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.12'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency 'rspec', '~> 3.0'
24
+ end