guides_style_mbland 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.
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