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