guides_style_mbland 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +13 -0
  3. data/README.md +151 -0
  4. data/assets/css/google-fonts.css +58 -0
  5. data/assets/favicons/favicon.ico +0 -0
  6. data/assets/favicons/favicon16.png +0 -0
  7. data/assets/favicons/favicon32.png +0 -0
  8. data/assets/favicons/touch-icon-120.png +0 -0
  9. data/assets/favicons/touch-icon-152.png +0 -0
  10. data/assets/favicons/touch-icon-76.png +0 -0
  11. data/assets/fonts/opensans-1395a31469a458f6e2069017b504be065ff44897.ttf +0 -0
  12. data/assets/fonts/opensans-3e193feab52524db86cd1508693f2e5086102669.ttf +0 -0
  13. data/assets/fonts/opensans-d4d19ed3a763ce10e050662542bc0318bb620096.svg +1637 -0
  14. data/assets/fonts/opensans-dd44beeac9a044f2c478b70838e447f0af077825.ttf +0 -0
  15. data/assets/fonts/opensans-f0cc9c782f41b44a31392230103f5b4e101a944a.eot +0 -0
  16. data/assets/fonts/raleway-0dd0372e5ca423ab45f41c74cb0f8859d0527517.eot +0 -0
  17. data/assets/fonts/raleway-468d063b5293c0f76e63103d04cf547e7837cdd2.ttf +0 -0
  18. data/assets/fonts/raleway-6c44b90a1e166ce0b659df20e8e374b5e9d97329.svg +347 -0
  19. data/assets/fonts/raleway-c34d475f415db5bc71140225a0284159bd3d85f2.ttf +0 -0
  20. data/assets/js/accordion.js +47 -0
  21. data/assets/js/guide.js +5 -0
  22. data/assets/js/vendor/anchor.min.js +6 -0
  23. data/assets/js/vendor/jquery-1.11.2.min.js +4 -0
  24. data/lib/guides_style_mbland.rb +14 -0
  25. data/lib/guides_style_mbland/assets.rb +18 -0
  26. data/lib/guides_style_mbland/breadcrumbs.rb +28 -0
  27. data/lib/guides_style_mbland/generated_nodes.rb +63 -0
  28. data/lib/guides_style_mbland/generated_pages.rb +38 -0
  29. data/lib/guides_style_mbland/generator.rb +21 -0
  30. data/lib/guides_style_mbland/includes.rb +21 -0
  31. data/lib/guides_style_mbland/includes/analytics.html +16 -0
  32. data/lib/guides_style_mbland/includes/breadcrumbs.html +8 -0
  33. data/lib/guides_style_mbland/includes/footer.html +5 -0
  34. data/lib/guides_style_mbland/includes/header.html +29 -0
  35. data/lib/guides_style_mbland/includes/scripts.html +6 -0
  36. data/lib/guides_style_mbland/includes/sidebar-children.html +17 -0
  37. data/lib/guides_style_mbland/includes/sidebar.html +14 -0
  38. data/lib/guides_style_mbland/layouts.rb +51 -0
  39. data/lib/guides_style_mbland/layouts/default.html +45 -0
  40. data/lib/guides_style_mbland/layouts/generated/home-redirect.html +6 -0
  41. data/lib/guides_style_mbland/layouts/search-results.html +4 -0
  42. data/lib/guides_style_mbland/namespace_flattener.rb +39 -0
  43. data/lib/guides_style_mbland/navigation.rb +253 -0
  44. data/lib/guides_style_mbland/repository.rb +73 -0
  45. data/lib/guides_style_mbland/sass.rb +10 -0
  46. data/lib/guides_style_mbland/sass/_guides_style_mbland_custom.scss +50 -0
  47. data/lib/guides_style_mbland/sass/_guides_style_mbland_main.scss +643 -0
  48. data/lib/guides_style_mbland/sass/_guides_style_mbland_syntax.scss +60 -0
  49. data/lib/guides_style_mbland/sass/guides_style_mbland.scss +3 -0
  50. data/lib/guides_style_mbland/tags.rb +51 -0
  51. data/lib/guides_style_mbland/update.rb +6 -0
  52. data/lib/guides_style_mbland/version.rb +3 -0
  53. metadata +264 -0
@@ -0,0 +1,28 @@
1
+ require 'jekyll'
2
+ require 'safe_yaml'
3
+
4
+ module GuidesStyleMbland
5
+ class Breadcrumbs
6
+ def self.generate(site, docs)
7
+ breadcrumbs = create_breadcrumbs(site)
8
+ docs.each do |page|
9
+ page.data['breadcrumbs'] = breadcrumbs[page.permalink || page.url]
10
+ end
11
+ end
12
+
13
+ def self.create_breadcrumbs(site)
14
+ (site.config['navigation'] || []).flat_map do |nav|
15
+ Breadcrumbs.generate_breadcrumbs(nav, '/', [])
16
+ end.to_h
17
+ end
18
+
19
+ def self.generate_breadcrumbs(nav, parent_url, parents)
20
+ url = parent_url + (nav['url'] || '')
21
+ crumbs = parents + [{ 'url' => url, 'text' => nav['text'] }]
22
+ child_crumbs = (nav['children'] || []).flat_map do |child|
23
+ generate_breadcrumbs(child, url, crumbs)
24
+ end
25
+ [[url, crumbs]] + child_crumbs
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,63 @@
1
+ module GuidesStyleMbland
2
+ class GeneratedNodes
3
+ # Params:
4
+ # url_to_nav: Mapping from original document URL to "nav item" objects,
5
+ # i.e. { 'text' => '...', 'url' => '...', 'internal' => true }
6
+ # nav_data: Array of nav item objects contained in `url_to_nav` after
7
+ # applying updates, possibly containing "orphan" items marked with an
8
+ # `:orphan_url` property
9
+ #
10
+ # Returns:
11
+ # nav_data with orphans properly nested within automatically-generated
12
+ # parent nodes marked with `'generated' => true`
13
+ def self.create_homes_for_orphans(url_to_nav, nav_data)
14
+ orphans = nav_data.select { |nav| nav[:orphan_url] }
15
+ orphans.each { |nav| create_home_for_orphan(nav, nav_data, url_to_nav) }
16
+ nav_data.reject! { |nav| nav[:orphan_url] }
17
+ prune_childless_parents(nav_data)
18
+ end
19
+
20
+ def self.create_home_for_orphan(nav, nav_data, url_to_nav)
21
+ parents = nav[:orphan_url].split('/')[1..-1]
22
+ nav['url'] = parents.pop + '/'
23
+ child_url = '/'
24
+ immediate_parent = parents.reduce(nil) do |parent, child|
25
+ child_url = child_url + child + '/'
26
+ find_or_create_node(nav_data, child_url, parent, child, url_to_nav)
27
+ end
28
+ assign_orphan_to_home(nav, immediate_parent, url_to_nav)
29
+ end
30
+
31
+ def self.find_or_create_node(nav_data, child_url, parent, child, url_to_nav)
32
+ child_nav = url_to_nav[child_url]
33
+ if child_nav.nil?
34
+ child_nav = generated_node(child)
35
+ url_to_nav[child_url] = child_nav
36
+ (parent.nil? ? nav_data : (parent['children'] ||= [])) << child_nav
37
+ end
38
+ child_nav
39
+ end
40
+
41
+ def self.generated_node(parent_slug)
42
+ { 'text' => parent_slug.split('-').join(' ').capitalize,
43
+ 'url' => parent_slug + '/',
44
+ 'internal' => true,
45
+ 'generated' => true,
46
+ }
47
+ end
48
+
49
+ def self.assign_orphan_to_home(nav, immediate_parent, url_to_nav)
50
+ nav_copy = {}.merge(nav)
51
+ url_to_nav[nav_copy.delete(:orphan_url)] = nav_copy
52
+ (immediate_parent['children'] ||= []) << nav_copy
53
+ end
54
+
55
+ def self.prune_childless_parents(nav_data)
56
+ (nav_data || []).reject! do |nav|
57
+ children = (nav['children'] || [])
58
+ prune_childless_parents(children)
59
+ nav['generated'] && children.empty?
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,38 @@
1
+ require_relative './layouts'
2
+
3
+ module GuidesStyleMbland
4
+ class GeneratedPages
5
+ DEFAULT_LAYOUT = 'guides_style_mbland_generated_home_redirect'
6
+
7
+ def self.generate_pages_from_navigation_data(site)
8
+ layout = site.config['generate_nodes']
9
+ return if layout.nil? || layout == false
10
+ layout = DEFAULT_LAYOUT if layout == true
11
+ nav_data = site.config['navigation']
12
+ generate_pages_from_generated_nodes(site, layout, nav_data, '/')
13
+ end
14
+
15
+ def self.generate_pages_from_generated_nodes(
16
+ site, layout, nav_data, parent_url)
17
+ (nav_data || []).select { |nav| nav['generated'] }.each do |nav|
18
+ site.pages << GeneratedPage.new(site, layout, nav, parent_url)
19
+ children = nav['children']
20
+ next_url = parent_url + nav['url']
21
+ generate_pages_from_generated_nodes(site, layout, children, next_url)
22
+ end
23
+ end
24
+ end
25
+
26
+ class GeneratedPage < ::Jekyll::Page
27
+ def initialize(site, layout, nav, parent_url)
28
+ @site = site
29
+ @name = 'index.html'
30
+
31
+ process(@name)
32
+ @data = {}
33
+ data['title'] = nav['text']
34
+ data['permalink'] = parent_url + nav['url']
35
+ data['layout'] = layout
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,21 @@
1
+ require_relative './assets'
2
+ require_relative './breadcrumbs'
3
+ require_relative './generated_pages'
4
+ require_relative './layouts'
5
+ require_relative './namespace_flattener'
6
+
7
+ require 'jekyll'
8
+
9
+ module GuidesStyleMbland
10
+ class Generator < ::Jekyll::Generator
11
+ def generate(site)
12
+ Layouts.register(site)
13
+ Assets.copy_to_site(site)
14
+ GeneratedPages.generate_pages_from_navigation_data(site)
15
+ pages = site.collections['pages']
16
+ docs = (pages.nil? ? [] : pages.docs) + site.pages
17
+ Breadcrumbs.generate(site, docs)
18
+ NamespaceFlattener.flatten_url_namespace(site, docs)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ require 'jekyll/tags/include'
2
+ require 'liquid'
3
+
4
+ module GuidesStyleMbland
5
+ class IncludeTag < ::Jekyll::Tags::IncludeTag
6
+ ::Liquid::Template.register_tag 'guides_style_mbland_include', self
7
+
8
+ def initialize(_tag_name, _name, _tokens)
9
+ super
10
+ @includes_dir = File.join File.dirname(__FILE__), 'includes'
11
+ end
12
+
13
+ def tag_includes_dir(*_context)
14
+ includes_dir
15
+ end
16
+
17
+ def resolved_includes_dir(_context)
18
+ includes_dir
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ {% if site.google_analytics_ua %}<!-- Google Analytics -->
2
+ <script>
3
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
4
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
5
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
6
+ })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
7
+ ga('create', '{{ site.google_analytics_ua }}', 'auto');
8
+
9
+ // anonymize user IPs (chops off the last IP triplet)
10
+ ga('set', 'anonymizeIp', true);
11
+ ga('set', 'forceSSL', true);
12
+ ga('send', 'pageview');
13
+ </script>
14
+
15
+ <!-- Digital Analytics Program roll-up, see https://analytics.usa.gov for data -->
16
+ <script id="_fed_an_ua_tag" src="https://dap.digitalgov.gov/Universal-Federated-Analytics-Min.js?agency=GSA"></script>{% endif %}
@@ -0,0 +1,8 @@
1
+ <nav class="nav-main">
2
+ <div class="wrapper">
3
+ <ol class="breadcrumbs">
4
+ {% for breadcrumb in page.breadcrumbs %}<li>{% if forloop.last %}{{ breadcrumb.text }}{% else %}<a href="{{ site.baseurl}}{{ breadcrumb.url }}">{{ breadcrumb.text }}</a>{% endif %}</li>
5
+ {% endfor %}
6
+ </div>
7
+ </div>
8
+ </nav>
@@ -0,0 +1,5 @@
1
+ <footer role="contentinfo">
2
+ <div class="wrap">{% assign repo_url = site.repos[0].url %}
3
+ <p>This project is maintained by <a href="{{ site.author.url }}">{{ site.author.name }}</a>. The source is available at <a href="{{ repo_url }}">{{ repo_url }}</a>.</p>
4
+ </div>
5
+ </footer>
@@ -0,0 +1,29 @@
1
+ <head>
2
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
3
+ <meta charset="utf-8">
4
+ <meta name="viewport" content="width=device-width">
5
+
6
+ {% if page.title %}
7
+ <title>{{ page.title }} - {{ site.name }}</title>
8
+ {% elsif site.title %}
9
+ <title>{{ site.title }} - {{ site.name }}</title>
10
+ {% endif %}
11
+
12
+ {% if page.description %}
13
+ <meta name="description" content="{{ page.description | xml_escape }}">
14
+ {% elsif site.description %}
15
+ <meta name="description" content="{{ site.description | xml_escape }}">
16
+ {% endif %}
17
+
18
+ <link rel="shortcut icon" type="image/ico" href="{{ site.baseurl }}/assets/favicons/favicon.ico" />
19
+ <link rel="icon" type="image/png" sizes="16x16" href="{{ site.baseurl }}/assets/favicons/favicon16.png" />
20
+ <link rel="icon" type="image/png" sizes="32x32" href="{{ site.baseurl }}/assets/favicons/favicon32.png" />
21
+ <link rel="apple-touch-icon" href="{{ site.baseurl }}/assets/favicons/18f-center-57.png" />
22
+ <link rel="apple-touch-icon" sizes="76x76" href="{{ site.baseurl }}/assets/favicons/touch-icon-76.png" />
23
+ <link rel="apple-touch-icon" sizes="120x120" href="{{ site.baseurl }}/assets/favicons/touch-icon-120.png" />
24
+ <link rel="apple-touch-icon" sizes="152x152" href="{{ site.baseurl }}/assets/favicons/touch-icon-152.png"/>
25
+ <link href='{{ site.baseurl }}/assets/css/google-fonts.css' rel='stylesheet' type='text/css' />
26
+ <link rel="stylesheet" href="{{ site.baseurl }}/assets/css/styles.css" type='text/css' />{% for style in site.styles %}
27
+ <link rel="stylesheet" href="{{ site.baseurl }}/{{ style }}" type='text/css' />{% endfor %}{% for style in page.styles %}
28
+ <link rel="stylesheet" href="{{ site.baseurl }}/{{ style }}" type='text/css' />{% endfor %}
29
+ </head>
@@ -0,0 +1,6 @@
1
+ <script src="{{ site.baseurl }}/assets/js/vendor/jquery-1.11.2.min.js"></script>
2
+ <script src="{{ site.baseurl }}/assets/js/accordion.js"></script>
3
+ <script src="{{ site.baseurl }}/assets/js/guide.js"></script>{% for script in site.scripts %}
4
+ <script src="{{ site.baseurl }}/{{ script }}"></script>{% endfor %}{% for script in page.scripts %}
5
+ <script src="{{ site.baseurl }}/{{ script }}"></script>{% endfor %}
6
+ {% jekyll_pages_api_search_load %}
@@ -0,0 +1,17 @@
1
+ {% if parent.children %}
2
+ {% capture expand_nav %}{% guides_style_mbland_should_expand_nav parent, parent_url %}{% endcapture %}
3
+ <button class="expand-subnav"
4
+ aria-expanded="{{ expand_nav }}"
5
+ aria-controls="nav-collapsible-{{ forloop.index }}">+</button>
6
+ <ul class="nav-children" id="nav-collapsible-{{ forloop.index }}"
7
+ aria-hidden="{% if expand_nav == 'true' %}false{% else %}true{% endif %}">
8
+ {% for child in parent.children %}
9
+ {% capture child_url %}{{ parent_url }}{{ child.url }}{% endcapture %}
10
+ <li class="{% if page.url == child_url %}sidebar-nav-active{% endif %}">
11
+ <a href="{% if child.internal == true %}{{ site.baseurl }}{{ child_url }}{% else %}{{ child.url }}{% endif %}"
12
+ title="{% if page.url == child_url %}Current Page{% else %}{{ child.text }}{% endif %}">{{ child.text }}</a>
13
+ {% assign parent = child %}{% assign parent_url = child_url %}
14
+ {% guides_style_mbland_include sidebar-children.html %}
15
+ {% capture parent_url %}{% guides_style_mbland_pop_last_url_component parent_url %}{% endcapture %}
16
+ </li>{% endfor %}
17
+ </ul>{% endif %}
@@ -0,0 +1,14 @@
1
+ <aside>
2
+ <p class="intro">{{ site.subtitle }}</p>
3
+ <nav class="sidebar-nav" role="navigation">
4
+ <ul>{% for link in site.navigation %}{% capture parent_url %}/{{ link.url }}{% endcapture %}
5
+ <li class="group {% if page.url == parent_url %}sidebar-nav-active{% endif %}">
6
+ <a href="{% if link.internal == true %}{{ site.baseurl }}/{% endif %}{{ link.url }}"
7
+ title="{% if page.url == parent_url %}Current Page
8
+ {% else %}{{ link.text }}{% endif %}">{{ link.text }}</a>
9
+ {% assign parent = link %}
10
+ {% guides_style_mbland_include sidebar-children.html %}
11
+ </li>{% endfor %}
12
+ </ul>
13
+ </nav>
14
+ </aside>
@@ -0,0 +1,51 @@
1
+ require 'jekyll/layout'
2
+ require 'safe_yaml'
3
+
4
+ module GuidesStyleMbland
5
+ # We have to essentially recreate the ::Jekyll::Layout constructor to loosen
6
+ # the default restriction that layouts be included in the site source.
7
+ class Layouts < ::Jekyll::Layout
8
+ LAYOUTS_DIR = File.join(File.dirname(__FILE__), 'layouts')
9
+ SEARCH_RESULTS_LAYOUT = 'search-results'
10
+
11
+ private_class_method :new
12
+
13
+ def initialize(site, subdir, layout_file)
14
+ @site = site
15
+ @base = File.join(LAYOUTS_DIR, subdir)
16
+ @name = "#{layout_file}.html"
17
+ @path = File.join @base, @name
18
+ parse_content_and_data File.join(@base, name)
19
+ process name
20
+ end
21
+
22
+ def parse_content_and_data(file_path)
23
+ self.data = {}
24
+ self.content = File.read(file_path)
25
+
26
+ front_matter_pattern = /^(---\n.*)---\n/m
27
+ front_matter_match = front_matter_pattern.match content
28
+ return unless front_matter_match
29
+
30
+ self.content = front_matter_match.post_match
31
+ self.data = SafeYAML.load front_matter_match[1], safe: true
32
+ end
33
+ private :parse_content_and_data
34
+
35
+ def self.register(site)
36
+ site.layouts['guides_style_mbland_default'] = new(site, '', 'default')
37
+ site.layouts['guides_style_mbland_generated_home_redirect'] = new(
38
+ site, 'generated', 'home-redirect')
39
+ register_search_results_layout(site)
40
+ end
41
+
42
+ def self.register_search_results_layout(site)
43
+ layouts_dir = File.join(site.source, site.config['layouts_dir'])
44
+ results_layout = File.join(layouts_dir, "#{SEARCH_RESULTS_LAYOUT}.html")
45
+ unless File.exist?(results_layout)
46
+ site.layouts[SEARCH_RESULTS_LAYOUT] = new(
47
+ site, '', SEARCH_RESULTS_LAYOUT)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ <!DOCTYPE html>
2
+ <html lang='en'>
3
+
4
+ {% guides_style_mbland_include header.html %}
5
+
6
+ <body>
7
+
8
+ <div class="container">
9
+ <a class="skip-link visuallyhidden focusable" href="#main">Skip to Main Content</a>
10
+
11
+ <header role="banner">
12
+
13
+ <div class="wrap">
14
+
15
+
16
+ <h1 class="site-title"><a class="title-link" href="{{ site.baseurl }}/">{{ site.name }}</a></h1>{% if site.back_link %}
17
+
18
+ {% jekyll_pages_api_search_interface %}
19
+
20
+ <div class="back-link"><a href="{{ site.back_link.url }}">&laquo; {{ site.back_link.text }}</a></div>{% endif %}
21
+
22
+
23
+ </div>
24
+
25
+ </header>
26
+
27
+ <div class="wrap content">
28
+
29
+ <section id="main" class="main-content" role="main">
30
+ <h2>{{ page.title }}</h2>
31
+ {{ content }}
32
+ </section>
33
+
34
+ {% guides_style_mbland_include sidebar.html %}
35
+
36
+ </div><!-- /.wrap content -->
37
+
38
+ {% guides_style_mbland_include footer.html %}
39
+
40
+ </div> <!-- /.container -->
41
+
42
+ </body>
43
+ {% guides_style_mbland_include analytics.html %}
44
+ {% guides_style_mbland_include scripts.html %}
45
+ </html>
@@ -0,0 +1,6 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head><meta http-equiv="refresh" content="0;URL='{{ site.baseurl }}/#{{ page.title | slugify }}'">
4
+ </head>
5
+ <body></body>
6
+ </html>
@@ -0,0 +1,4 @@
1
+ ---
2
+ layout: guides_style_mbland_default
3
+ ---
4
+ {% jekyll_pages_api_search_results %}
@@ -0,0 +1,39 @@
1
+ module GuidesStyleMbland
2
+ class NamespaceFlattener
3
+ def self.flatten_url_namespace(site, docs)
4
+ flatten_urls(docs) if site.config['flat_namespace']
5
+ end
6
+
7
+ def self.flatten_urls(docs)
8
+ flat_to_orig = {}
9
+ docs.each { |page| flatten_page_urls(page, flat_to_orig) }
10
+ check_for_collisions(flat_to_orig)
11
+ end
12
+
13
+ def self.flatten_page_urls(page, flat_to_orig)
14
+ orig_url = page.permalink || page.url
15
+ flattened_url = flat_url(orig_url)
16
+ (flat_to_orig[flattened_url] ||= []) << orig_url
17
+ page.data['permalink'] = flattened_url
18
+ (page.data['breadcrumbs'] || []).each do |crumb|
19
+ crumb['url'] = flat_url(crumb['url'])
20
+ end
21
+ end
22
+
23
+ def self.flat_url(url)
24
+ url == '/' ? url : "/#{url.split('/')[1..-1].last}/"
25
+ end
26
+
27
+ def self.check_for_collisions(flat_to_orig)
28
+ collisions = flat_to_orig.map do |flattened, orig|
29
+ [flattened, orig] if orig.size != 1
30
+ end.compact
31
+
32
+ return if collisions.empty?
33
+
34
+ messages = collisions.map { |flat, orig| "#{flat}: #{orig.join(', ')}" }
35
+ fail(StandardError, "collisions in flattened namespace between\n " +
36
+ messages.join("\n "))
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,253 @@
1
+ require_relative './generated_nodes'
2
+
3
+ require 'jekyll'
4
+ require 'safe_yaml'
5
+
6
+ module GuidesStyleMbland
7
+ module FrontMatter
8
+ EXTNAMES = %w(.md .html)
9
+
10
+ def self.load(basedir)
11
+ # init_file_to_front_matter_map is initializing the map with a nil value
12
+ # for every file that _should_ contain front matter as far as the
13
+ # navigation menu is concerned. Any nil values that remain after merging
14
+ # with the site_file_to_front_matter map will result in a validation
15
+ # error.
16
+ init_file_to_front_matter_map(basedir).merge(
17
+ site_file_to_front_matter(init_site(basedir)))
18
+ end
19
+
20
+ def self.validate_with_message_upon_error(front_matter)
21
+ files_with_errors = validate front_matter
22
+ return if files_with_errors.empty?
23
+ message = ['The following files have errors in their front matter:']
24
+ files_with_errors.each do |file, errors|
25
+ message << " #{file}:"
26
+ message.concat errors.map { |error| " #{error}" }
27
+ end
28
+ message.join "\n" unless message.size == 1
29
+ end
30
+
31
+ def self.validate(front_matter)
32
+ front_matter.map do |file, data|
33
+ next [file, ['no front matter defined']] if data.nil?
34
+ errors = missing_property_errors(data) + permalink_errors(data)
35
+ [file, errors] unless errors.empty?
36
+ end.compact.to_h
37
+ end
38
+
39
+ private
40
+
41
+ def self.init_site(basedir)
42
+ Dir.chdir(basedir) do
43
+ config = SafeYAML.load_file('_config.yml', safe: true)
44
+ adjust_config_paths(basedir, config)
45
+ site = Jekyll::Site.new(Jekyll.configuration(config))
46
+ site.reset
47
+ site.read
48
+ site
49
+ end
50
+ end
51
+
52
+ def self.adjust_config_paths(basedir, config)
53
+ source = config['source']
54
+ config['source'] = source.nil? ? basedir : File.join(basedir, source)
55
+ destination = config['destination']
56
+ destination = '_site' if destination.nil?
57
+ config['destination'] = File.join(basedir, destination)
58
+ end
59
+
60
+ def self.site_file_to_front_matter(site)
61
+ site_pages(site).map do |page|
62
+ [page.relative_path, page.data]
63
+ end.to_h
64
+ end
65
+
66
+ # We're supporting two possible configurations:
67
+ #
68
+ # - a `pages/` directory in which documents appear as part of the regular
69
+ # site.pages collection; we have to filter by page.relative_path, and we
70
+ # do not assign a permalink so that validation (in a later step) will
71
+ # ensure that each page has a permalink assigned
72
+ #
73
+ # - an actual `pages` collection, stored in a `_pages` directory; no
74
+ # filtering is necessary, and we can reliably set the permalink to
75
+ # page.url as a default
76
+ def self.site_pages(site)
77
+ pages = site.collections['pages']
78
+ if pages.nil?
79
+ site.pages.select do |page|
80
+ # Handle both with and without leading slash, as leading slash was
81
+ # removed in v3.2.0.pre.beta2:
82
+ # jekyll/jekyll/commit/4fbbeddae20fa52732f30ef001bb1f80258bc5d7
83
+ page.relative_path.start_with?('/pages/', 'pages/') || page.url == '/'
84
+ end
85
+ else
86
+ pages.docs.each { |page| page.data['permalink'] ||= page.url }
87
+ end
88
+ end
89
+
90
+ def self.init_file_to_front_matter_map(basedir)
91
+ file_to_front_matter = {}
92
+ Dir.chdir(basedir) do
93
+ pages_dir = Dir.exist?('_pages') ? '_pages' : 'pages'
94
+ Dir[File.join(pages_dir, '**', '*')].each do |file_name|
95
+ extname = File.extname(file_name)
96
+ next unless File.file?(file_name) && EXTNAMES.include?(extname)
97
+ file_to_front_matter[file_name] = nil
98
+ end
99
+ end
100
+ file_to_front_matter
101
+ end
102
+
103
+ def self.missing_property_errors(data)
104
+ properties = %w(title permalink)
105
+ properties.map { |p| "no `#{p}:` property" if data[p].nil? }.compact
106
+ end
107
+
108
+ def self.permalink_errors(data)
109
+ pl = data['permalink']
110
+ return [] if pl.nil?
111
+ errors = []
112
+ errors << "`permalink:` does not begin with '/'" unless pl.start_with? '/'
113
+ errors << "`permalink:` does not end with '/'" unless pl.end_with? '/'
114
+ errors
115
+ end
116
+ end
117
+
118
+ # Automatically updates the `navigation:` field in _config.yml.
119
+ #
120
+ # Does this by parsing the front matter from files in `pages/`. Preserves the
121
+ # existing order of items in `navigation:`, but new items may need to be
122
+ # reordered manually.
123
+ def self.update_navigation_configuration(basedir)
124
+ config_path = File.join basedir, '_config.yml'
125
+ config_data = SafeYAML.load_file config_path, safe: true
126
+ return unless config_data
127
+ nav_data = config_data['navigation'] || []
128
+ NavigationMenu.update_navigation_data(nav_data, basedir, config_data)
129
+ NavigationMenuWriter.write_navigation_data_to_config_file(
130
+ config_path, nav_data)
131
+ end
132
+
133
+ module NavigationMenu
134
+ def self.update_navigation_data(nav_data, basedir, config_data)
135
+ original = map_nav_items_by_url('/', nav_data).to_h
136
+ updated = updated_nav_data(basedir)
137
+ remove_stale_nav_entries(nav_data, original, updated)
138
+ updated.map { |url, nav| apply_nav_update(url, nav, nav_data, original) }
139
+ if config_data['generate_nodes']
140
+ GeneratedNodes.create_homes_for_orphans(original, nav_data)
141
+ else
142
+ check_for_orphaned_items(nav_data)
143
+ end
144
+ end
145
+
146
+ def self.map_nav_items_by_url(parent_url, nav_data)
147
+ nav_data.flat_map do |nav|
148
+ url = File.join('', parent_url, nav['url'] || '')
149
+ [[url, nav]].concat(map_nav_items_by_url(url, nav['children'] || []))
150
+ end
151
+ end
152
+
153
+ def self.updated_nav_data(basedir)
154
+ front_matter = FrontMatter.load basedir
155
+ errors = FrontMatter.validate_with_message_upon_error front_matter
156
+ abort errors + "\n_config.yml not updated" if errors
157
+ front_matter.values.sort_by { |fm| fm['permalink'] }
158
+ .map { |fm| [fm['permalink'], page_nav(fm)] }.to_h
159
+ end
160
+
161
+ def self.page_nav(front_matter)
162
+ url_components = front_matter['permalink'].split('/')[1..-1]
163
+ result = {
164
+ 'text' => front_matter['navtitle'] || front_matter['title'],
165
+ 'url' => "#{url_components.nil? ? '' : url_components.last}/",
166
+ 'internal' => true,
167
+ }
168
+ # Delete the root URL so we don't have an empty `url:` property laying
169
+ # around.
170
+ result.delete 'url' if result['url'] == '/'
171
+ result
172
+ end
173
+
174
+ def self.remove_stale_nav_entries(nav_data, original, updated)
175
+ # Remove old entries whose pages have been deleted
176
+ original.each do |url, nav|
177
+ if !updated.member?(url) && nav['internal'] && !nav['generated']
178
+ nav['delete'] = true
179
+ end
180
+ end
181
+ original.delete_if { |_url, nav| nav['delete'] }
182
+ nav_data.delete_if { |nav| nav['delete'] }
183
+ nav_data.each { |nav| remove_stale_children(nav) }
184
+ end
185
+
186
+ def self.remove_stale_children(parent)
187
+ children = (parent['children'] || [])
188
+ children.delete_if { |nav| nav['delete'] }
189
+ parent.delete 'children' if children.empty?
190
+ children.each { |child| remove_stale_children(child) }
191
+ end
192
+
193
+ def self.apply_nav_update(url, nav, nav_data, original)
194
+ orig = original[url]
195
+ if orig.nil?
196
+ apply_new_nav_item(url, nav, nav_data, original)
197
+ else
198
+ orig['text'] = nav['text']
199
+ orig.delete('generated')
200
+ end
201
+ end
202
+
203
+ def self.apply_new_nav_item(url, nav, nav_data, original)
204
+ parent_url = File.dirname(url || '/')
205
+ parent = original["#{parent_url}/"]
206
+ if parent_url == '/'
207
+ nav_data << (original[url] = nav)
208
+ elsif parent.nil?
209
+ nav_data << nav.merge(orphan_url: url)
210
+ else
211
+ (parent['children'] ||= []) << nav
212
+ end
213
+ end
214
+
215
+ def self.check_for_orphaned_items(nav_data)
216
+ orphan_urls = nav_data.map { |nav| nav[:orphan_url] }.compact
217
+ unless orphan_urls.empty?
218
+ fail(StandardError, "Parent pages missing for the following:\n " +
219
+ orphan_urls.join("\n "))
220
+ end
221
+ end
222
+ end
223
+
224
+ class NavigationMenuWriter
225
+ def self.write_navigation_data_to_config_file(config_path, nav_data)
226
+ lines = []
227
+ in_navigation = false
228
+ open(config_path).each_line do |line|
229
+ in_navigation = process_line line, lines, nav_data, in_navigation
230
+ end
231
+ File.write config_path, lines.join
232
+ end
233
+
234
+ def self.process_line(line, lines, nav_data, in_navigation = false)
235
+ if !in_navigation && line.start_with?('navigation:')
236
+ lines << line << format_navigation_section(nav_data)
237
+ in_navigation = true
238
+ elsif in_navigation
239
+ in_navigation = line.start_with?(' ') || line.start_with?('-')
240
+ lines << line unless in_navigation
241
+ else
242
+ lines << line
243
+ end
244
+ in_navigation
245
+ end
246
+
247
+ YAML_PREFIX = "---\n"
248
+
249
+ def self.format_navigation_section(nav_data)
250
+ nav_data.empty? ? '' : nav_data.to_yaml[YAML_PREFIX.size..-1]
251
+ end
252
+ end
253
+ end