guides_style_mbland 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +13 -0
- data/README.md +151 -0
- data/assets/css/google-fonts.css +58 -0
- data/assets/favicons/favicon.ico +0 -0
- data/assets/favicons/favicon16.png +0 -0
- data/assets/favicons/favicon32.png +0 -0
- data/assets/favicons/touch-icon-120.png +0 -0
- data/assets/favicons/touch-icon-152.png +0 -0
- data/assets/favicons/touch-icon-76.png +0 -0
- data/assets/fonts/opensans-1395a31469a458f6e2069017b504be065ff44897.ttf +0 -0
- data/assets/fonts/opensans-3e193feab52524db86cd1508693f2e5086102669.ttf +0 -0
- data/assets/fonts/opensans-d4d19ed3a763ce10e050662542bc0318bb620096.svg +1637 -0
- data/assets/fonts/opensans-dd44beeac9a044f2c478b70838e447f0af077825.ttf +0 -0
- data/assets/fonts/opensans-f0cc9c782f41b44a31392230103f5b4e101a944a.eot +0 -0
- data/assets/fonts/raleway-0dd0372e5ca423ab45f41c74cb0f8859d0527517.eot +0 -0
- data/assets/fonts/raleway-468d063b5293c0f76e63103d04cf547e7837cdd2.ttf +0 -0
- data/assets/fonts/raleway-6c44b90a1e166ce0b659df20e8e374b5e9d97329.svg +347 -0
- data/assets/fonts/raleway-c34d475f415db5bc71140225a0284159bd3d85f2.ttf +0 -0
- data/assets/js/accordion.js +47 -0
- data/assets/js/guide.js +5 -0
- data/assets/js/vendor/anchor.min.js +6 -0
- data/assets/js/vendor/jquery-1.11.2.min.js +4 -0
- data/lib/guides_style_mbland.rb +14 -0
- data/lib/guides_style_mbland/assets.rb +18 -0
- data/lib/guides_style_mbland/breadcrumbs.rb +28 -0
- data/lib/guides_style_mbland/generated_nodes.rb +63 -0
- data/lib/guides_style_mbland/generated_pages.rb +38 -0
- data/lib/guides_style_mbland/generator.rb +21 -0
- data/lib/guides_style_mbland/includes.rb +21 -0
- data/lib/guides_style_mbland/includes/analytics.html +16 -0
- data/lib/guides_style_mbland/includes/breadcrumbs.html +8 -0
- data/lib/guides_style_mbland/includes/footer.html +5 -0
- data/lib/guides_style_mbland/includes/header.html +29 -0
- data/lib/guides_style_mbland/includes/scripts.html +6 -0
- data/lib/guides_style_mbland/includes/sidebar-children.html +17 -0
- data/lib/guides_style_mbland/includes/sidebar.html +14 -0
- data/lib/guides_style_mbland/layouts.rb +51 -0
- data/lib/guides_style_mbland/layouts/default.html +45 -0
- data/lib/guides_style_mbland/layouts/generated/home-redirect.html +6 -0
- data/lib/guides_style_mbland/layouts/search-results.html +4 -0
- data/lib/guides_style_mbland/namespace_flattener.rb +39 -0
- data/lib/guides_style_mbland/navigation.rb +253 -0
- data/lib/guides_style_mbland/repository.rb +73 -0
- data/lib/guides_style_mbland/sass.rb +10 -0
- data/lib/guides_style_mbland/sass/_guides_style_mbland_custom.scss +50 -0
- data/lib/guides_style_mbland/sass/_guides_style_mbland_main.scss +643 -0
- data/lib/guides_style_mbland/sass/_guides_style_mbland_syntax.scss +60 -0
- data/lib/guides_style_mbland/sass/guides_style_mbland.scss +3 -0
- data/lib/guides_style_mbland/tags.rb +51 -0
- data/lib/guides_style_mbland/update.rb +6 -0
- data/lib/guides_style_mbland/version.rb +3 -0
- 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 }}">« {{ 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,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
|