xc_html_generator 0.0.1
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.
- checksums.yaml +7 -0
- data/lib/html/page.html.erb +285 -0
- data/lib/xc_html_generator.rb +125 -0
- metadata +44 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 25460904a9b2cb299009f1328895c4510cb5926e293e1cae9d47cbc569c9aa16
|
4
|
+
data.tar.gz: 25f2bc9ab1020212c535b02b7eb27d6e2c07fc9a1b4a4c0d4942169a346aadcd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 10f784b665bd07a0e374ada0727b3798c11238cb2ba7449c9050ca76621e3010deae41ddd83f1480d535755b7f9aa76f091246335ebe99e9d2d58f8c05e9f767
|
7
|
+
data.tar.gz: 4572bc001b4da3654baf30f318467472f42ae97436e0f0966645654d8f19c859a1655677c84615f347ff7034de1ef0956062a51d4055b357e91fa2111910a6cc
|
@@ -0,0 +1,285 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>fastlane/snapshot</title>
|
5
|
+
<meta charset="UTF-8">
|
6
|
+
<style type="text/css">
|
7
|
+
* {
|
8
|
+
font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
9
|
+
font-weight: 300;
|
10
|
+
}
|
11
|
+
#sortMenu {
|
12
|
+
overflow: hidden;
|
13
|
+
border: 1px solid #ccc;
|
14
|
+
background-color: #f1f1f1;
|
15
|
+
display: none;
|
16
|
+
}
|
17
|
+
#sortMenu button {
|
18
|
+
background-color: inherit;
|
19
|
+
float: left;
|
20
|
+
border: none;
|
21
|
+
outline: none;
|
22
|
+
cursor: pointer;
|
23
|
+
padding: 14px 16px;
|
24
|
+
font-size: 17px;
|
25
|
+
}
|
26
|
+
#sortMenu button:hover {
|
27
|
+
background-color: #ddd;
|
28
|
+
}
|
29
|
+
#sortMenu button.active {
|
30
|
+
background-color: #ccc;
|
31
|
+
}
|
32
|
+
.deviceName {
|
33
|
+
display: block;
|
34
|
+
font-size: 30px;
|
35
|
+
padding-bottom: 24px;
|
36
|
+
padding-top: 45px;
|
37
|
+
}
|
38
|
+
.screenshot {
|
39
|
+
cursor: pointer;
|
40
|
+
border: 1px #EEE solid;
|
41
|
+
z-index: 0;
|
42
|
+
}
|
43
|
+
.caption {
|
44
|
+
font-size: 24px;
|
45
|
+
padding-bottom: 24px;
|
46
|
+
padding-top: 30px;
|
47
|
+
}
|
48
|
+
h1, h2 {
|
49
|
+
font-weight: bold;
|
50
|
+
}
|
51
|
+
th {
|
52
|
+
text-align: left;
|
53
|
+
}
|
54
|
+
td {
|
55
|
+
text-align: center;
|
56
|
+
min-width: 200px;
|
57
|
+
}
|
58
|
+
#overlay {
|
59
|
+
position:fixed;
|
60
|
+
top:0;
|
61
|
+
left:0;
|
62
|
+
background:rgba(0,0,0,0.8);
|
63
|
+
z-index:5;
|
64
|
+
width:100%;
|
65
|
+
height:100%;
|
66
|
+
display:none;
|
67
|
+
cursor: zoom-out;
|
68
|
+
text-align: center;
|
69
|
+
}
|
70
|
+
#imageDisplay {
|
71
|
+
height: auto;
|
72
|
+
width: auto;
|
73
|
+
z-index: 10;
|
74
|
+
cursor: pointer;
|
75
|
+
}
|
76
|
+
#imageInfo {
|
77
|
+
background: none repeat scroll 0 0 rgba(0, 0, 0, 0.2);
|
78
|
+
border-radius: 5px;
|
79
|
+
color: white;
|
80
|
+
margin: 20px;
|
81
|
+
padding: 10px;
|
82
|
+
position: absolute;
|
83
|
+
right: 0;
|
84
|
+
top: 0;
|
85
|
+
width: 250px;
|
86
|
+
z-index: -1;
|
87
|
+
}
|
88
|
+
#imageInfo:hover {
|
89
|
+
z-index: 20;
|
90
|
+
}
|
91
|
+
</style>
|
92
|
+
</head>
|
93
|
+
<body>
|
94
|
+
<div id="sortMenu">
|
95
|
+
<button id="defaultTab" class="tabLink" onclick="openTab(event, 'byLanguage')">By Language</button>
|
96
|
+
<button class="tabLink" onclick="openTab(event, 'byScreen')">By Screen</button>
|
97
|
+
</div>
|
98
|
+
<div id="byLanguage" class="tabContent"><h1 class="tabTitle">By Language:</h1><% image_counter = 0 %><% @data_by_language.each do |language, content| %>
|
99
|
+
<h2 id="<%= language %>"><%= language %></h2>
|
100
|
+
<hr>
|
101
|
+
<table><% content.each do |device_name, screens| %>
|
102
|
+
<tr>
|
103
|
+
<th colspan="<%= screens.count %>">
|
104
|
+
<a id="<%= language %>-<%= device_name %>" class="deviceName" href="#<%= language %>-<%= device_name %>"><%= device_name %></a>
|
105
|
+
</th>
|
106
|
+
</tr>
|
107
|
+
<tr><% screens.each do |screen_path| %><% next if screen_path.include?"_framed.png" %>
|
108
|
+
<td><% image_counter += 1 %>
|
109
|
+
<a href="<%= screen_path %>" target="_blank" class="screenshotLink">
|
110
|
+
<img class="screenshot" src="<%= screen_path %>" style="width: 100%;" alt="<%= language %> <%= device_name %>" data-tab="1" data-counter="<%= image_counter %>">
|
111
|
+
</a>
|
112
|
+
</td><% end %>
|
113
|
+
</tr><% end %>
|
114
|
+
</table><% end %>
|
115
|
+
</div>
|
116
|
+
<div id="byScreen" class="tabContent"><h1 class="tabTitle">By Screen:</h1><% image_counter = 0 %><% @data_by_screen.each do |screen, content| %>
|
117
|
+
<h2 id="<%= screen %>" class="screen"><%= screen %></h2>
|
118
|
+
<hr>
|
119
|
+
<table><% content.each do |device_name, screens| %>
|
120
|
+
<tr>
|
121
|
+
<th colspan="<%= screens.count %>">
|
122
|
+
<a id="<%= screen %>-<%= device_name %>" class="deviceName" href="#<%= screen %>-<%= device_name %>"><%= device_name %></a>
|
123
|
+
</th>
|
124
|
+
</tr>
|
125
|
+
<tr><% screens.each do |language, screen_path| %><% next if screen_path.include?"_framed.png" %>
|
126
|
+
<td><% image_counter += 1 %>
|
127
|
+
<a href="<%= screen_path %>" target="_blank" class="screenshotLink">
|
128
|
+
<img class="screenshot" src="<%= screen_path %>" style="width: 100%;" alt="<%= language %> <%= device_name %>" data-tab="2" data-counter="<%= image_counter %>">
|
129
|
+
</a>
|
130
|
+
<div class="caption"><%= language %></div>
|
131
|
+
</td><% end %>
|
132
|
+
</tr><% end %>
|
133
|
+
</table><% end %>
|
134
|
+
</div>
|
135
|
+
<div id="overlay">
|
136
|
+
<img id="imageDisplay" src="" alt="" />
|
137
|
+
<div id="imageInfo"></div>
|
138
|
+
</div>
|
139
|
+
<script type="text/javascript">
|
140
|
+
var overlay = document.getElementById('overlay');
|
141
|
+
var imageDisplay = document.getElementById('imageDisplay');
|
142
|
+
var imageInfo = document.getElementById('imageInfo');
|
143
|
+
var screenshotLink = document.getElementsByClassName('screenshotLink');
|
144
|
+
|
145
|
+
window.onload = setup();
|
146
|
+
|
147
|
+
function setup() {
|
148
|
+
var i, menu, tabTitles;
|
149
|
+
|
150
|
+
// Since JS is enabled, show sort menu and hide tab titles
|
151
|
+
menu = document.getElementById("sortMenu");
|
152
|
+
menu.style.display = "block";
|
153
|
+
|
154
|
+
tabTitles = document.getElementsByClassName("tabTitle");
|
155
|
+
for (i = 0; i < tabTitles.length; i++) {
|
156
|
+
tabTitles[i].style.display = "none";
|
157
|
+
}
|
158
|
+
|
159
|
+
doClick(document.getElementById("defaultTab"));
|
160
|
+
}
|
161
|
+
|
162
|
+
function getCurrentTab() {
|
163
|
+
var i, tabs;
|
164
|
+
tabs = document.getElementsByClassName("tabContent");
|
165
|
+
for (i = 0; i < tabs.length; i++) {
|
166
|
+
if (tabs[i].style.display != "none") {
|
167
|
+
return i + 1;
|
168
|
+
}
|
169
|
+
}
|
170
|
+
return 1; // fallback
|
171
|
+
}
|
172
|
+
|
173
|
+
function openTab(evt, tabName) {
|
174
|
+
var i, tabContent, tabLinks;
|
175
|
+
tabs = document.getElementsByClassName("tabContent");
|
176
|
+
for (i = 0; i < tabs.length; i++) {
|
177
|
+
tabs[i].style.display = "none";
|
178
|
+
}
|
179
|
+
tabLinks = document.getElementsByClassName("tabLink");
|
180
|
+
for (i = 0; i < tabLinks.length; i++) {
|
181
|
+
tabLinks[i].className = tabLinks[i].className.replace(" active", "");
|
182
|
+
}
|
183
|
+
document.getElementById(tabName).style.display = "block";
|
184
|
+
evt.currentTarget.className += " active";
|
185
|
+
}
|
186
|
+
|
187
|
+
function doClick(el) {
|
188
|
+
if (document.createEvent) {
|
189
|
+
var evObj = document.createEvent('MouseEvents', true);
|
190
|
+
evObj.initMouseEvent("click", false, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
191
|
+
el.dispatchEvent(evObj);
|
192
|
+
} else if (document.createEventObject) { //IE
|
193
|
+
var evObj = document.createEventObject();
|
194
|
+
el.fireEvent('onclick', evObj);
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
198
|
+
for (index = 0; index < screenshotLink.length; ++index) {
|
199
|
+
screenshotLink[index].addEventListener('click', function(e) {
|
200
|
+
e.preventDefault();
|
201
|
+
|
202
|
+
var img = e.target;
|
203
|
+
if (e.target.tagName == 'A') {
|
204
|
+
img = e.target.children[0];
|
205
|
+
}
|
206
|
+
|
207
|
+
// beautify
|
208
|
+
var tmpImg = new Image();
|
209
|
+
tmpImg.src = img.src;
|
210
|
+
imageDisplay.style.height = 'auto';
|
211
|
+
imageDisplay.style.width = 'auto';
|
212
|
+
imageDisplay.style.paddingTop = 0;
|
213
|
+
if (window.innerHeight < tmpImg.height) {
|
214
|
+
imageDisplay.style.height = document.documentElement.clientHeight+'px';
|
215
|
+
} else if (window.innerWidth < tmpImg.width) {
|
216
|
+
imageDisplay.style.width = document.documentElement.clientWidth;+'px';
|
217
|
+
} else {
|
218
|
+
imageDisplay.style.paddingTop = parseInt((window.innerHeight - tmpImg.height) / 2)+'px';
|
219
|
+
}
|
220
|
+
|
221
|
+
imageDisplay.src = img.src;
|
222
|
+
imageDisplay.alt = img.alt;
|
223
|
+
imageDisplay.dataset.counter = img.dataset.counter;
|
224
|
+
|
225
|
+
imageInfo.innerHTML = '<h3>'+img.alt+'</h3>';
|
226
|
+
imageInfo.innerHTML += decodeURI(img.src.split("/").pop());
|
227
|
+
imageInfo.innerHTML += '<br />'+tmpImg.height+'×'+tmpImg.width+'px';
|
228
|
+
|
229
|
+
overlay.style.display = "block";
|
230
|
+
});
|
231
|
+
}
|
232
|
+
|
233
|
+
imageDisplay.addEventListener('click', function(e) {
|
234
|
+
e.stopPropagation(); // !
|
235
|
+
|
236
|
+
overlay.style.display = "none";
|
237
|
+
|
238
|
+
img_tab = parseInt(getCurrentTab());
|
239
|
+
img_counter = parseInt(e.target.dataset.counter) + 1;
|
240
|
+
try {
|
241
|
+
link = document.body.querySelector('img[data-tab="'+img_tab+'"][data-counter="'+img_counter+'"]').parentNode;
|
242
|
+
} catch (e) {
|
243
|
+
try {
|
244
|
+
link = document.body.querySelector('img[data-tab="'+img_tab+'"][data-counter="0"]').parentNode;
|
245
|
+
} catch (e) {
|
246
|
+
return false;
|
247
|
+
}
|
248
|
+
}
|
249
|
+
doClick(link);
|
250
|
+
});
|
251
|
+
|
252
|
+
overlay.addEventListener('click', function(e) {
|
253
|
+
overlay.style.display = "none";
|
254
|
+
})
|
255
|
+
|
256
|
+
function keyPressed(e) {
|
257
|
+
e = e || window.event;
|
258
|
+
var charCode = e.keyCode || e.which;
|
259
|
+
switch(charCode) {
|
260
|
+
case 27: // Esc
|
261
|
+
overlay.style.display = "none";
|
262
|
+
break;
|
263
|
+
case 34: // Page Down
|
264
|
+
case 39: // Right arrow
|
265
|
+
case 54: // Keypad right
|
266
|
+
case 76: // l
|
267
|
+
case 102: // Keypad right
|
268
|
+
e.preventDefault();
|
269
|
+
doClick(imageDisplay);
|
270
|
+
break;
|
271
|
+
case 33: // Page up
|
272
|
+
case 37: // Left arrow
|
273
|
+
case 52: // Keypad left
|
274
|
+
case 72: // h
|
275
|
+
case 100: // Keypad left
|
276
|
+
e.preventDefault();
|
277
|
+
document.getElementById('imageDisplay').dataset.counter -= 2; // hacky
|
278
|
+
doClick(imageDisplay);
|
279
|
+
break;
|
280
|
+
}
|
281
|
+
};
|
282
|
+
document.body.addEventListener('keydown', keyPressed);
|
283
|
+
</script>
|
284
|
+
</body>
|
285
|
+
</html>
|
@@ -0,0 +1,125 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class HtmlGenerator
|
4
|
+
require 'erb'
|
5
|
+
require 'fastimage'
|
6
|
+
require 'xcresult'
|
7
|
+
require 'fastlane'
|
8
|
+
|
9
|
+
attr_accessor :screenshots_path
|
10
|
+
|
11
|
+
def initialize(params = {})
|
12
|
+
@screenshots_path = params.fetch(:screenshots_path, path)
|
13
|
+
end
|
14
|
+
|
15
|
+
def path
|
16
|
+
Dir.pwd
|
17
|
+
end
|
18
|
+
|
19
|
+
def html_path
|
20
|
+
File.join(path, "lib", "html", "page.html.erb")
|
21
|
+
end
|
22
|
+
|
23
|
+
def result_path
|
24
|
+
path
|
25
|
+
end
|
26
|
+
# Tweet
|
27
|
+
# @return [String]
|
28
|
+
def generate
|
29
|
+
|
30
|
+
screens_path = screenshots_path
|
31
|
+
|
32
|
+
@data_by_language = {}
|
33
|
+
@data_by_screen = {}
|
34
|
+
Dir[File.join(screens_path, "*")].sort.each do |device_name_folders|
|
35
|
+
device_name = File.basename(device_name_folders)
|
36
|
+
Dir[File.join(device_name_folders, "*")].sort.each do |language_folder|
|
37
|
+
language = File.basename(language_folder)
|
38
|
+
Dir[File.join(language_folder, '*.png')].sort.each do |screenshot|
|
39
|
+
file_name = File.basename(screenshot)
|
40
|
+
available_devices.each do |key_name, output_name|
|
41
|
+
next unless device_name.include?(key_name)
|
42
|
+
# This screenshot is from this device
|
43
|
+
|
44
|
+
@data_by_language[language] ||= {}
|
45
|
+
@data_by_language[language][output_name] ||= []
|
46
|
+
|
47
|
+
screen_name = file_name.sub(key_name + '-', '').sub('.png', '')
|
48
|
+
@data_by_screen[screen_name] ||= {}
|
49
|
+
@data_by_screen[screen_name][output_name] ||= {}
|
50
|
+
|
51
|
+
resulting_path = screenshot
|
52
|
+
@data_by_language[language][output_name] << resulting_path
|
53
|
+
@data_by_screen[screen_name][output_name][language] = resulting_path
|
54
|
+
break # to not include iPhone 6 and 6 Plus (name is contained in the other name)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
html = ERB.new(File.read(html_path)).result(binding) # https://web.archive.org/web/20160430190141/www.rrn.dk/rubys-erb-templating-system
|
60
|
+
|
61
|
+
export_path = "#{screens_path}/screenshots.html"
|
62
|
+
|
63
|
+
File.write(export_path, html)
|
64
|
+
|
65
|
+
export_path = File.expand_path(export_path)
|
66
|
+
p "Successfully created HTML file with an overview of all the screenshots: '#{export_path}'"
|
67
|
+
#system("open '#{export_path}'") unless Snapshot.config[:skip_open_summary]
|
68
|
+
end
|
69
|
+
|
70
|
+
def xcode_9_and_above_device_name_mappings
|
71
|
+
{
|
72
|
+
# snapshot in Xcode 9 saves screenshots with the SIMULATOR_DEVICE_NAME
|
73
|
+
# which includes spaces
|
74
|
+
'iPhone 12 Pro Max' => "iPhone 12 Pro Max",
|
75
|
+
'iPhone 12 Pro' => "iPhone 12 Pro",
|
76
|
+
'iPhone 12 mini' => "iPhone 12 mini",
|
77
|
+
'iPhone 12' => "iPhone 12",
|
78
|
+
'iPhone 11 Pro Max' => "iPhone 11 Pro Max",
|
79
|
+
'iPhone 11 Pro' => "iPhone 11 Pro",
|
80
|
+
'iPhone 11' => "iPhone 11",
|
81
|
+
'iPhone XS Max' => "iPhone XS Max",
|
82
|
+
'iPhone XS' => "iPhone XS",
|
83
|
+
'iPhone XR' => "iPhone XR",
|
84
|
+
'iPhone 8 Plus' => "iPhone 8 Plus",
|
85
|
+
'iPhone 8' => "iPhone 8",
|
86
|
+
'iPhone X' => "iPhone X",
|
87
|
+
'iPhone 7 Plus' => "iPhone 7 Plus (5.5-Inch)",
|
88
|
+
'iPhone 7' => "iPhone 7 (4.7-Inch)",
|
89
|
+
'iPhone 6s Plus' => "iPhone 6s Plus (5.5-Inch)",
|
90
|
+
'iPhone 6 Plus' => "iPhone 6 Plus (5.5-Inch)",
|
91
|
+
'iPhone 6s' => "iPhone 6s (4.7-Inch)",
|
92
|
+
'iPhone 6' => "iPhone 6 (4.7-Inch)",
|
93
|
+
'iPhone 5s' => "iPhone 5s (4-Inch)",
|
94
|
+
'iPhone 5' => "iPhone 5 (4-Inch)",
|
95
|
+
'iPhone SE' => "iPhone SE",
|
96
|
+
'iPhone 4s' => "iPhone 4s (3.5-Inch)",
|
97
|
+
'iPad 2' => 'iPad 2',
|
98
|
+
'iPad Air (3rd generation)' => 'iPad Air (3rd generation)',
|
99
|
+
'iPad Air 2' => 'iPad Air 2',
|
100
|
+
'iPad Air' => 'iPad Air',
|
101
|
+
'iPad (5th generation)' => 'iPad (5th generation)',
|
102
|
+
'iPad (7th generation)' => 'iPad (7th generation)',
|
103
|
+
'iPad Pro (9.7-inch)' => 'iPad Pro (9.7-inch)',
|
104
|
+
'iPad Pro (9.7 inch)' => 'iPad Pro (9.7-inch)', # iOS 10.3.1 simulator
|
105
|
+
'iPad Pro (10.5-inch)' => 'iPad Pro (10.5-inch)',
|
106
|
+
'iPad Pro (11-inch) (2nd generation)' => 'iPad Pro (11-inch) (2nd generation)',
|
107
|
+
'iPad Pro (11-inch)' => 'iPad Pro (11-inch)',
|
108
|
+
'iPad Pro (12.9-inch) (4th generation)' => 'iPad Pro (12.9-inch) (4th generation)',
|
109
|
+
'iPad Pro (12.9-inch) (3rd generation)' => 'iPad Pro (12.9-inch) (3rd generation)',
|
110
|
+
'iPad Pro (12.9-inch) (2nd generation)' => 'iPad Pro (12.9-inch) (2nd generation)',
|
111
|
+
'iPad Pro (12.9-inch)' => 'iPad Pro (12.9-inch)',
|
112
|
+
'iPad Pro (12.9 inch)' => 'iPad Pro (12.9-inch)', # iOS 10.3.1 simulator
|
113
|
+
'iPad Pro' => 'iPad Pro (12.9-inch)', # iOS 9.3 simulator
|
114
|
+
'Apple TV 1080p' => 'Apple TV',
|
115
|
+
'Apple TV 4K (at 1080p)' => 'Apple TV 4K (at 1080p)',
|
116
|
+
'Apple TV 4K' => 'Apple TV 4K',
|
117
|
+
'Apple TV' => 'Apple TV',
|
118
|
+
'Mac' => 'Mac'
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
def available_devices
|
123
|
+
return xcode_9_and_above_device_name_mappings
|
124
|
+
end
|
125
|
+
end
|
metadata
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xc_html_generator
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kostyantin Ishchenko
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-05-13 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A simple gem that genarate html from xcparse result
|
14
|
+
email: ko7tyantin@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/html/page.html.erb
|
20
|
+
- lib/xc_html_generator.rb
|
21
|
+
homepage: https://rubygems.org/gems/xc_html_generator
|
22
|
+
licenses:
|
23
|
+
- MIT
|
24
|
+
metadata: {}
|
25
|
+
post_install_message:
|
26
|
+
rdoc_options: []
|
27
|
+
require_paths:
|
28
|
+
- lib
|
29
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
requirements: []
|
40
|
+
rubygems_version: 3.2.16
|
41
|
+
signing_key:
|
42
|
+
specification_version: 4
|
43
|
+
summary: Helper gem for xcparse
|
44
|
+
test_files: []
|