28 |
29 |
32 | {% if page.name == 'index.html' %}
33 |
37 |
38 | {% else %}
39 |
48 |
49 |
50 |
51 |
52 | {{ content }}
53 |
54 |
69 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/_plugins/samples_generator.rb:
--------------------------------------------------------------------------------
1 | require 'net/http'
2 | require 'uri'
3 | require 'yaml'
4 |
5 | module CppSamples
6 | DEFAULT_SAMPLES_DIR = 'samples'
7 | COMMENT_REGEX = /^\/\/\s*(.+)?$/
8 | SPECS = ['c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'experimental']
9 |
10 | def self.sort_specs(specs)
11 | specs.sort {|spec1, spec2| SPECS.find_index(spec1) <=> SPECS.find_index(spec2)}
12 | end
13 |
14 | class SamplesGenerator < Jekyll::Generator
15 | def generate(site)
16 | index = site.pages.detect { |page| page.name == 'index.html' }
17 |
18 | samples_dir = site.config['samples_dir'] || DEFAULT_SAMPLES_DIR
19 | samples_tree = CppSamples::build_samples_tree(site, samples_dir)
20 |
21 | index.data['specs'] = SPECS
22 | index.data['sample_categories'] = samples_tree
23 | index.data['random_sample'] = CppSamples::get_random_sample(samples_tree)
24 |
25 | samples_tree.each do |category, samples|
26 | samples.each do |sample|
27 | sample.variations.keys.each do |spec|
28 | site.pages << SamplePage.new(site, sample, spec)
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
35 | GithubUser = Struct.new(:username, :avatar, :profile)
36 |
37 | class GithubUserCache
38 | def initialize
39 | @cache = {}
40 | @usernames_cache = {}
41 |
42 | @github_token = ENV['GH_TOKEN']
43 | end
44 |
45 | def get_user_by_email(email)
46 | if @cache.has_key?(email)
47 | return @cache[email]
48 | end
49 |
50 | attempts = 0
51 |
52 | while attempts < 3
53 | search_uri = URI.parse("https://api.github.com/search/users?q=#{email}+in:email&per_page=1")
54 |
55 | if @github_token.nil? or @github_token.empty?
56 | search_response = Net::HTTP.get_response(search_uri)
57 | else
58 | search_request = Net::HTTP::Get.new(search_uri)
59 | search_request.basic_auth(ENV['GH_TOKEN'], 'x-oauth-basic')
60 | search_response = Net::HTTP.start(search_uri.hostname,
61 | search_uri.port,
62 | :use_ssl => true) do |http|
63 | http.request(search_request)
64 | end
65 | end
66 |
67 | search_result = JSON.parse(search_response.body)
68 |
69 | break if search_result.has_key?('items')
70 |
71 | attempts += 1
72 |
73 | rate_limit_reset_timestamp = search_response['X-RateLimit-Reset'].to_i
74 | while Time.now.to_i < rate_limit_reset_timestamp
75 | sleep(30)
76 | end
77 | end
78 |
79 | if search_result['items'].empty?
80 | user = GithubUser.new(nil, '/images/unknown_user.png', nil)
81 | else
82 | user_item = search_result['items'][0]
83 | user = GithubUser.new(user_item['login'], user_item['avatar_url'] + '&size=36', user_item['html_url'])
84 | end
85 |
86 | @cache[email] = user
87 | if !user.username.nil?
88 | @usernames_cache[user.username] = user
89 | end
90 |
91 | user
92 | end
93 |
94 | def get_user_by_username(username)
95 | if @usernames_cache.has_key?(username)
96 | return @usernames_cache[username]
97 | end
98 |
99 | attempts = 0
100 |
101 | while attempts < 3
102 | user_uri = URI.parse("https://api.github.com/users/#{username}")
103 |
104 | if @github_token.nil? or @github_token.empty?
105 | user_response = Net::HTTP.get_response(user_uri)
106 | else
107 | user_request = Net::HTTP::Get.new(user_uri)
108 | user_request.basic_auth(ENV['GH_TOKEN'], 'x-oauth-basic')
109 | user_response = Net::HTTP.start(user_uri.hostname,
110 | user_uri.port,
111 | :use_ssl => true) do |http|
112 | http.request(user_request)
113 | end
114 | end
115 |
116 | user_result = JSON.parse(user_response.body)
117 |
118 | break if user_result.has_key?('login') or
119 | user_result['message'] == "Not Found"
120 |
121 | attempts += 1
122 |
123 | rate_limit_reset_timestamp = user_response['X-RateLimit-Reset'].to_i
124 | while Time.now.to_i < rate_limit_reset_timestamp
125 | sleep(30)
126 | end
127 | end
128 |
129 | if user_result.has_key?('login')
130 | user = GithubUser.new(username, user_result['avatar_url'] + '&size=36', user_result['html_url'])
131 | else
132 | user = GithubUser.new(username, '/images/unknown_user.png', nil)
133 | end
134 |
135 | @usernames_cache[username] = user
136 | if user_result.has_key?('email')
137 | @cache[user_result['email']] = user
138 | end
139 | end
140 | end
141 |
142 | class DummyUserCache
143 | def get_user_by_email(email)
144 | GithubUser.new(nil, '/images/unknown_user.png', nil)
145 | end
146 |
147 | def get_user_by_username(username)
148 | GithubUser.new(nil, '/images/unknown_user.png', nil)
149 | end
150 | end
151 |
152 | class SamplePage < Jekyll::Page
153 | def initialize(site, sample, spec)
154 | @site = site
155 | @base = site.source
156 | @dir = "patterns"
157 |
158 | is_primary = spec == sample.primary_spec
159 |
160 | if is_primary
161 | @name = "#{sample.name}.html"
162 | else
163 | @name = "#{sample.name}.#{spec}.html"
164 | end
165 |
166 | process(@name)
167 | read_yaml(File.join(@base, '_includes', ''), 'sample.html')
168 |
169 | variation = sample.variations[spec]
170 |
171 | self.data['sample'] = sample
172 | self.data['spec'] = spec
173 | self.data['title'] = variation.title
174 | self.data['description'] = variation.intent
175 | end
176 | end
177 |
178 | class Section
179 | attr_accessor :title
180 |
181 | def initialize(title)
182 | @title = title
183 | end
184 |
185 | def to_liquid
186 | return {'title' => @title}
187 | end
188 | end
189 |
190 | class Sample
191 | attr_accessor :name, :variations
192 |
193 | def initialize(samples_dir, sample_name, user_cache, contributors_list)
194 | @name = File.basename(sample_name)
195 | @user_cache = user_cache
196 |
197 | @variations = {}
198 |
199 | sample_paths = Dir.glob("#{samples_dir}/#{sample_name}*.cpp")
200 | sample_paths.each do |sample_path|
201 | spec_match = /^.+\.(?
.+)\.cpp$/.match(sample_path)
202 | has_spec = !spec_match.nil?
203 |
204 | variation = Variation.new(sample_path, user_cache, contributors_list)
205 |
206 | spec = if has_spec
207 | spec_match['spec']
208 | else
209 | variation.tags[0] || 'c++98'
210 | end
211 |
212 | @variations[spec] = variation
213 | end
214 | end
215 |
216 | def primary
217 | @variations[primary_spec]
218 | end
219 |
220 | def min_spec
221 | CppSamples::sort_specs(@variations.keys).first
222 | end
223 |
224 | def primary_spec
225 | specs = CppSamples::sort_specs(@variations.keys)
226 | i = specs.rindex { |spec| spec.start_with?('c++') }
227 | specs[i] if i else specs.last
228 | end
229 |
230 | def to_liquid
231 | {
232 | 'name' => @name,
233 | 'primary' => primary.to_liquid,
234 | 'min_spec' => min_spec,
235 | 'primary_spec' => primary_spec,
236 | 'variations' => @variations.each_with_object({}) do |(spec, variation), variations|
237 | variations[spec] = variation.to_liquid
238 | variations
239 | end
240 | }
241 | end
242 | end
243 |
244 | class Variation
245 | attr_accessor :file_name, :path, :code, :code_offset, :title, :intent, :tags, :description
246 |
247 | def initialize(sample_path, user_cache, contributors_list)
248 | @user_cache = user_cache
249 |
250 | sample_file = File.new(sample_path, 'r')
251 |
252 | @file_name = get_file_name(sample_path)
253 | @path = file_name_to_path(sample_path)
254 |
255 | sample_contents = strip_blank_lines(sample_file.readlines)
256 |
257 | @title = extract_title(sample_contents)
258 | @tags = extract_tags(sample_contents)
259 |
260 | code_start = 1
261 | code_start = 2 unless @tags.empty?
262 |
263 | body_lines, body_start = extract_body(sample_contents)
264 | @intent, @description = extract_body_parts(body_lines)
265 |
266 | code_lines = strip_blank_lines(sample_contents[code_start..body_start-1])
267 | @code = code_lines.join
268 | @code_offset = sample_contents.index(code_lines[0])
269 |
270 | @contributors = get_contributors(sample_path, contributors_list)
271 | @modified_date = get_modified_date(sample_path)
272 | end
273 |
274 | def to_liquid
275 | {
276 | 'title' => @title,
277 | 'tags' => @tags,
278 | 'code' => @code,
279 | 'code_offset' => @code_offset,
280 | 'description' => @description,
281 | 'intent' => @intent,
282 | 'contributors' => @contributors,
283 | 'modified_date' => @modified_date,
284 | 'path' => @path,
285 | 'file_name' => @file_name
286 | }
287 | end
288 |
289 | private def get_file_name(full_file_name)
290 | file_name_parts = full_file_name.split('/')[-3..-1]
291 | file_name_parts.join('/')
292 | end
293 |
294 | private def file_name_to_path(file_name)
295 | file_name_parts = file_name.split('/')[-3..-1]
296 | file_name_parts[2] = File.basename(file_name_parts[2], '.*')
297 | file_name_parts[0].slice!(/^\d+\-/)
298 | file_name_parts.delete_at(1)
299 | file_name_parts.join('/')
300 | end
301 |
302 | private def extract_title(lines)
303 | header = lines[0]
304 | header_match = COMMENT_REGEX.match(header)
305 |
306 | unless header_match
307 | raise "invalid header line in sample"
308 | end
309 |
310 | header_match[1]
311 | end
312 |
313 | private def extract_tags(lines)
314 | tags_line = lines[1]
315 | tags_line_match = COMMENT_REGEX.match(tags_line)
316 |
317 | unless tags_line_match
318 | return []
319 | end
320 |
321 | tags_text = tags_line_match[1]
322 | tags = tags_text.split(/\s*,\s*/)
323 | tags.collect! {|tag| tag.strip.downcase}
324 |
325 | return tags
326 | end
327 |
328 | private def extract_body(lines)
329 | body = []
330 | line_index = lines.length - 1
331 |
332 | while not COMMENT_REGEX.match(lines[line_index])
333 | line_index -= 1
334 | end
335 |
336 | while match = COMMENT_REGEX.match(lines[line_index])
337 | body.unshift("#{match[1]}\n")
338 | line_index -= 1
339 | end
340 |
341 | return body, line_index
342 | end
343 |
344 | private def extract_body_parts(body_lines)
345 | blank_line_index = body_lines.index {|line| /^[\t ]*\n$/ =~ line}
346 | intent = body_lines[0..blank_line_index].join()
347 | description = body_lines[blank_line_index+1..-1].join()
348 | return intent, description
349 | end
350 |
351 | private def strip_blank_lines(lines)
352 | lines.join("").strip.split("\n").map {|line| "#{line}\n" }
353 | end
354 |
355 | private def get_contributors(file_name, contributors_list)
356 | real_path = file_name.split('/')[1..-1].join('/')
357 |
358 | committers = nil
359 | Dir.chdir('samples') do
360 | gitlog_output = `git log --follow --simplify-merges --format="format:%ae %an" -- #{real_path}`
361 | committer_strings = gitlog_output.split("\n")
362 | committers = committer_strings.inject([]) do |committers, committer_string|
363 | split_committer_string = committer_string.split(/\s/,2)
364 | committers << {'email' => split_committer_string[0], 'name' => split_committer_string[1]}
365 | end
366 | end
367 |
368 | committers.uniq! {|committer| committer['email'] }
369 |
370 | contributors = []
371 |
372 | committers.each do |committer|
373 | contributors_item = contributors_list.detect do |contributors_item|
374 | contributors_item['name'] == committer['name'] or
375 | contributors_item['email'] == committer['email']
376 | end
377 |
378 | unless contributors_item.nil?
379 | user = @user_cache.get_user_by_username(contributors_item['github_username'])
380 | end
381 |
382 | if user.nil? or user.username.nil?
383 | user = @user_cache.get_user_by_email(committer['email'])
384 | end
385 |
386 | contributors << {
387 | 'name' => committer['name'],
388 | 'image' => user.avatar,
389 | 'url' => user.profile
390 | }
391 | end
392 |
393 | contributors
394 | end
395 |
396 | private def get_modified_date(file_name)
397 | real_path = file_name.split('/')[-3..-1].join('/')
398 | Dir.chdir('samples') do
399 | `git log -1 --format="format:%ad" -- #{real_path}`.strip
400 | end
401 | end
402 | end
403 |
404 | def self.build_samples_tree(site, samples_dir)
405 | if site.config['environment'] == "production"
406 | user_cache = GithubUserCache.new
407 | else
408 | user_cache = DummyUserCache.new
409 | end
410 |
411 | contributors_list = []
412 | File.open("#{samples_dir}/CONTRIBUTORS.txt", 'r').each_line do |line|
413 | match = /^\- ([^<>]*) (<(.*)> )?\((.*)\)\s*$/.match(line)
414 | contributors_list << {
415 | 'name' => match[1],
416 | 'email' => match[3],
417 | 'github_username' => match[4]
418 | }
419 | end
420 |
421 | contents = YAML.load_file("#{samples_dir}/contents.yml")
422 |
423 | contents['categories'].each_with_object({}) do |category, tree|
424 | tree[Section.new(category['title'])] = category['samples'].map do |sample_path|
425 | Sample.new(samples_dir, sample_path, user_cache, contributors_list)
426 | end
427 |
428 | tree
429 | end
430 | end
431 |
432 | def self.get_random_sample(samples_tree)
433 | all_samples = []
434 |
435 | samples_tree.each do |_, samples|
436 | all_samples.concat(samples)
437 | end
438 |
439 | seed = Time.now.strftime("%U%Y").to_i
440 | all_samples.sample(random: Random.new(seed))
441 | end
442 | end
443 |
--------------------------------------------------------------------------------