bookbindery 1.0.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/bin/bookbinder +6 -0
- data/lib/bookbinder.rb +59 -0
- data/lib/bookbinder/app_fetcher.rb +40 -0
- data/lib/bookbinder/archive.rb +95 -0
- data/lib/bookbinder/artifact_namer.rb +22 -0
- data/lib/bookbinder/blue_green_app.rb +27 -0
- data/lib/bookbinder/book.rb +54 -0
- data/lib/bookbinder/bookbinder_logger.rb +33 -0
- data/lib/bookbinder/cf_command_runner.rb +114 -0
- data/lib/bookbinder/cf_routes.rb +19 -0
- data/lib/bookbinder/cli.rb +68 -0
- data/lib/bookbinder/cli_error.rb +6 -0
- data/lib/bookbinder/code_example.rb +40 -0
- data/lib/bookbinder/command_runner.rb +32 -0
- data/lib/bookbinder/command_validator.rb +24 -0
- data/lib/bookbinder/commands/bookbinder_command.rb +18 -0
- data/lib/bookbinder/commands/build_and_push_tarball.rb +31 -0
- data/lib/bookbinder/commands/generate_pdf.rb +140 -0
- data/lib/bookbinder/commands/help.rb +31 -0
- data/lib/bookbinder/commands/naming.rb +9 -0
- data/lib/bookbinder/commands/publish.rb +138 -0
- data/lib/bookbinder/commands/push_local_to_staging.rb +35 -0
- data/lib/bookbinder/commands/push_to_prod.rb +35 -0
- data/lib/bookbinder/commands/run_publish_ci.rb +42 -0
- data/lib/bookbinder/commands/tag.rb +31 -0
- data/lib/bookbinder/commands/update_local_doc_repos.rb +27 -0
- data/lib/bookbinder/commands/version.rb +25 -0
- data/lib/bookbinder/configuration.rb +163 -0
- data/lib/bookbinder/configuration_fetcher.rb +55 -0
- data/lib/bookbinder/configuration_validator.rb +162 -0
- data/lib/bookbinder/css_link_checker.rb +64 -0
- data/lib/bookbinder/directory_helpers.rb +15 -0
- data/lib/bookbinder/distributor.rb +69 -0
- data/lib/bookbinder/git_client.rb +63 -0
- data/lib/bookbinder/git_hub_repository.rb +151 -0
- data/lib/bookbinder/local_file_system_accessor.rb +9 -0
- data/lib/bookbinder/middleman_runner.rb +86 -0
- data/lib/bookbinder/pdf_generator.rb +73 -0
- data/lib/bookbinder/publisher.rb +125 -0
- data/lib/bookbinder/pusher.rb +34 -0
- data/lib/bookbinder/remote_yaml_credential_provider.rb +21 -0
- data/lib/bookbinder/section.rb +78 -0
- data/lib/bookbinder/server_director.rb +53 -0
- data/lib/bookbinder/shell_out.rb +19 -0
- data/lib/bookbinder/sieve.rb +62 -0
- data/lib/bookbinder/sitemap_generator.rb +19 -0
- data/lib/bookbinder/spider.rb +91 -0
- data/lib/bookbinder/stabilimentum.rb +59 -0
- data/lib/bookbinder/usage_messenger.rb +33 -0
- data/lib/bookbinder/yaml_loader.rb +22 -0
- data/master_middleman/bookbinder_helpers.rb +133 -0
- data/master_middleman/config.rb +23 -0
- data/master_middleman/quicklinks_renderer.rb +78 -0
- data/master_middleman/submodule_aware_assets.rb +45 -0
- data/template_app/Gemfile +7 -0
- data/template_app/Gemfile.lock +20 -0
- data/template_app/app.rb +3 -0
- data/template_app/config.ru +9 -0
- data/template_app/lib/rack_static.rb +19 -0
- data/template_app/lib/vienna_application.rb +26 -0
- metadata +462 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module Bookbinder
|
4
|
+
class SitemapGenerator
|
5
|
+
def generate(links, sitemap_file)
|
6
|
+
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
7
|
+
xml.urlset('xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9') {
|
8
|
+
links.each do |link|
|
9
|
+
xml.url {
|
10
|
+
xml.loc link
|
11
|
+
xml.changefreq 'daily'
|
12
|
+
}
|
13
|
+
end
|
14
|
+
}
|
15
|
+
end
|
16
|
+
File.write(sitemap_file, builder.to_xml)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'anemone'
|
2
|
+
require 'pty'
|
3
|
+
require_relative 'css_link_checker'
|
4
|
+
require_relative 'sieve'
|
5
|
+
require_relative 'stabilimentum'
|
6
|
+
require_relative 'sitemap_generator'
|
7
|
+
|
8
|
+
module Bookbinder
|
9
|
+
class Spider
|
10
|
+
def initialize(logger, app_dir: nil)
|
11
|
+
@logger = logger
|
12
|
+
@app_dir = app_dir || raise('Spiders must be initialized with an app directory.')
|
13
|
+
@broken_links = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_sitemap(target_host, port)
|
17
|
+
temp_host = "localhost:#{port}"
|
18
|
+
|
19
|
+
sieve = Sieve.new domain: "http://#{temp_host}"
|
20
|
+
links = crawl_from "http://#{temp_host}/index.html", sieve
|
21
|
+
@broken_links, working_links = links
|
22
|
+
|
23
|
+
announce_broken_links @broken_links
|
24
|
+
|
25
|
+
write_sitemap(target_host, temp_host, working_links)
|
26
|
+
working_links
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_broken_links?
|
30
|
+
@broken_links.any? { |link| !link.include?('#') } if @broken_links
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.prepend_location(location, url)
|
34
|
+
"#{URI(location).path} => #{url}"
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def write_sitemap(host, port, working_links)
|
40
|
+
sitemap_file = File.join(@app_dir, 'public', 'sitemap.xml')
|
41
|
+
sitemap_links = substitute_hostname(host, port, working_links)
|
42
|
+
SitemapGenerator.new.generate(sitemap_links, sitemap_file)
|
43
|
+
end
|
44
|
+
|
45
|
+
def announce_broken_links(broken_links)
|
46
|
+
if broken_links.any?
|
47
|
+
@logger.error "\nFound #{broken_links.count} broken links!"
|
48
|
+
|
49
|
+
broken_links.each do |link|
|
50
|
+
if link.include?('#')
|
51
|
+
@logger.warn(link)
|
52
|
+
else
|
53
|
+
@logger.notify(link)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
@logger.error "\nFound #{broken_links.count} broken links!"
|
58
|
+
else
|
59
|
+
@logger.success "\nNo broken links!"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def crawl_from(url, sieve)
|
64
|
+
broken_links = []
|
65
|
+
sitemap = [url]
|
66
|
+
2.times do |i|
|
67
|
+
is_first_pass = (i==0)
|
68
|
+
|
69
|
+
Anemone.crawl(url) do |anemone|
|
70
|
+
dont_visit_fragments(anemone)
|
71
|
+
anemone.on_every_page do |page|
|
72
|
+
broken, working = sieve.links_from Stabilimentum.new(page), is_first_pass
|
73
|
+
broken_links.concat broken
|
74
|
+
sitemap.concat working
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
broken_links.concat Dir.chdir(@app_dir) { CssLinkChecker.new.broken_links_in_all_stylesheets }
|
80
|
+
[broken_links.compact.uniq, sitemap.compact.uniq]
|
81
|
+
end
|
82
|
+
|
83
|
+
def dont_visit_fragments(anemone)
|
84
|
+
anemone.focus_crawl { |page| page.links.reject { |link| link.to_s.match(/%23/) } }
|
85
|
+
end
|
86
|
+
|
87
|
+
def substitute_hostname(target_host, temp_host, links)
|
88
|
+
links.map { |l| l.gsub(/#{Regexp.escape(temp_host)}/, target_host) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Bookbinder
|
2
|
+
class Spider
|
3
|
+
class Stabilimentum # Decorates a piece of the web.
|
4
|
+
FudgedUri = Struct.new(:path, :fragment, :to_s)
|
5
|
+
|
6
|
+
def initialize(page)
|
7
|
+
@page = page
|
8
|
+
end
|
9
|
+
|
10
|
+
def referer
|
11
|
+
@page.referer
|
12
|
+
end
|
13
|
+
|
14
|
+
def not_found?
|
15
|
+
@page.not_found?
|
16
|
+
end
|
17
|
+
|
18
|
+
def url
|
19
|
+
@page.url
|
20
|
+
end
|
21
|
+
|
22
|
+
def has_target_for?(uri)
|
23
|
+
id_selector = uri.fragment
|
24
|
+
name_selector = "[name=#{uri.fragment}]"
|
25
|
+
|
26
|
+
@page.doc.css("##{id_selector}").any? || @page.doc.css(name_selector).any?
|
27
|
+
rescue Nokogiri::CSS::SyntaxError
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def fragment_identifiers(targeting_locally: false)
|
32
|
+
if targeting_locally
|
33
|
+
fragment_anchor_uris.select { |uri| uri.path.empty? }
|
34
|
+
else
|
35
|
+
fragment_anchor_uris.reject { |uri| uri.path.empty? }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def fragment_anchor_uris
|
42
|
+
anchors = @page.doc ? @page.doc.css('a') : []
|
43
|
+
anchors.map { |a| convert_to_uri(a) }.select { |u| u.fragment }
|
44
|
+
end
|
45
|
+
|
46
|
+
def convert_to_uri(anchor)
|
47
|
+
URI anchor['href'].to_s
|
48
|
+
rescue URI::InvalidURIError
|
49
|
+
create_fudged_uri(anchor['href'])
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_fudged_uri(target)
|
53
|
+
path = target.split('#')[0]
|
54
|
+
fragment = target.include?('#') ? '#' + target.split('#')[1] : nil
|
55
|
+
FudgedUri.new(path, fragment, target)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Bookbinder
|
2
|
+
class UsageMessenger
|
3
|
+
|
4
|
+
def construct_for commands, flags
|
5
|
+
log_usage_header + "\n" + flag_usage_messages(flags) + command_usage_messages(commands)
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def flag_usage_messages(flags)
|
11
|
+
flag_usage_messages = ""
|
12
|
+
flags.each { |f| flag_usage_messages += " \t#{f.usage}\n" }
|
13
|
+
flag_usage_messages
|
14
|
+
end
|
15
|
+
|
16
|
+
def command_usage_messages(commands)
|
17
|
+
flag_command_messages = ""
|
18
|
+
commands.each do |command_class|
|
19
|
+
flag_command_messages += " \t#{command_class.usage}\n"
|
20
|
+
end
|
21
|
+
flag_command_messages
|
22
|
+
end
|
23
|
+
|
24
|
+
def log_usage_header
|
25
|
+
<<TEXT
|
26
|
+
|
27
|
+
\e[1;39;49mDocumentation\e[0m: https://github.com/pivotal-cf/docs-bookbinder
|
28
|
+
|
29
|
+
\e[1;39;49mUsage\e[0m: bookbinder <command|flag> [args]
|
30
|
+
TEXT
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Bookbinder
|
2
|
+
|
3
|
+
FileNotFoundError = Class.new(RuntimeError)
|
4
|
+
InvalidSyntaxError = Class.new(RuntimeError)
|
5
|
+
|
6
|
+
class YAMLLoader
|
7
|
+
def load(path)
|
8
|
+
unless File.exist? path
|
9
|
+
raise FileNotFoundError.new, "YAML"
|
10
|
+
end
|
11
|
+
|
12
|
+
begin
|
13
|
+
YAML.load_file(path)
|
14
|
+
rescue Psych::SyntaxError => e
|
15
|
+
raise InvalidSyntaxError.new e
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'date'
|
2
|
+
# mostly from https://github.com/multiscan/middleman-navigation but modified slightly
|
3
|
+
require_relative 'quicklinks_renderer'
|
4
|
+
|
5
|
+
I18n.enforce_available_locales = false
|
6
|
+
|
7
|
+
class ArchiveMenuTemplateNotFound < StandardError;
|
8
|
+
end
|
9
|
+
|
10
|
+
class ArchiveConfigFormatError < StandardError;
|
11
|
+
end
|
12
|
+
|
13
|
+
module Bookbinder
|
14
|
+
module Navigation
|
15
|
+
class << self
|
16
|
+
def registered(app)
|
17
|
+
app.helpers HelperMethods
|
18
|
+
end
|
19
|
+
|
20
|
+
alias :included :registered
|
21
|
+
end
|
22
|
+
|
23
|
+
module HelperMethods
|
24
|
+
|
25
|
+
def yield_for_code_snippet(from: nil, at: nil)
|
26
|
+
example = CodeExample.get_instance(bookbinder_logger, section_hash: {'repository' => {'name' => from}}, local_repo_dir: config[:local_repo_dir], git_accessor: config[:git_accessor])
|
27
|
+
snippet, language = example.get_snippet_and_language_at(at)
|
28
|
+
delimiter = '```'
|
29
|
+
|
30
|
+
snippet.prepend("#{delimiter}#{language}\n").concat("\n#{delimiter}")
|
31
|
+
end
|
32
|
+
|
33
|
+
def yield_for_subnav
|
34
|
+
if index_subnav
|
35
|
+
template = current_page.data.index_subnav
|
36
|
+
else
|
37
|
+
namespaces = decreasingly_specific_namespaces
|
38
|
+
template = namespaces.map do |namespace|
|
39
|
+
config[:subnav_templates][namespace]
|
40
|
+
end.compact.pop || 'default'
|
41
|
+
end
|
42
|
+
partial "subnavs/#{template}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def yield_for_archive_drop_down_menu
|
46
|
+
if config.respond_to?(:archive_menu)
|
47
|
+
title = config[:archive_menu].first
|
48
|
+
links = config[:archive_menu][1..-1]
|
49
|
+
|
50
|
+
new_links_based_from_root = links.map do |link|
|
51
|
+
link_from_root = link.dup
|
52
|
+
link_from_root.map do |k, v|
|
53
|
+
link_from_root[k] = "/#{v}"
|
54
|
+
end
|
55
|
+
link_from_root
|
56
|
+
end
|
57
|
+
|
58
|
+
partial 'archive_menus/default', locals: { menu_title: title, dropdown_links: new_links_based_from_root }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def breadcrumbs
|
63
|
+
page_chain = add_ancestors_of(current_page, [])
|
64
|
+
breadcrumbs = page_chain.map do |page|
|
65
|
+
make_breadcrumb(page, page == current_page)
|
66
|
+
end.compact
|
67
|
+
return if breadcrumbs.size < 2
|
68
|
+
return content_tag :ul, breadcrumbs.reverse.join(' '), class: 'breadcrumbs'
|
69
|
+
end
|
70
|
+
|
71
|
+
def vars
|
72
|
+
OpenStruct.new config[:template_variables]
|
73
|
+
end
|
74
|
+
|
75
|
+
def modified_date(format=nil)
|
76
|
+
current_file_in_repo = current_path.dup.gsub(File.basename(current_path), File.basename(current_page.source_file))
|
77
|
+
current_section = get_section_or_book_for(current_file_in_repo)
|
78
|
+
modified_time = current_section.get_modification_date_for(file: current_file_in_repo, full_path: current_page.source_file)
|
79
|
+
(format.nil? ? modified_time : modified_time.strftime(format))
|
80
|
+
end
|
81
|
+
|
82
|
+
def quick_links
|
83
|
+
page_src = File.read(current_page.source_file)
|
84
|
+
quicklinks_renderer = QuicklinksRenderer.new(vars)
|
85
|
+
Redcarpet::Markdown.new(quicklinks_renderer).render(page_src)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def get_section_or_book_for(path)
|
91
|
+
sections = config[:sections]
|
92
|
+
book = config[:book]
|
93
|
+
|
94
|
+
raise "Book or Selections are incorrectly specified for Middleman." if book.nil? || sections.nil?
|
95
|
+
|
96
|
+
current_section = nil
|
97
|
+
sections.each { |section| current_section = section if File.dirname(current_path).match(/^#{section.directory}/) }
|
98
|
+
|
99
|
+
return book if current_section.nil?
|
100
|
+
return current_section
|
101
|
+
end
|
102
|
+
|
103
|
+
def index_subnav
|
104
|
+
return true if current_page.data.index_subnav
|
105
|
+
end
|
106
|
+
|
107
|
+
def decreasingly_specific_namespaces
|
108
|
+
page_classes.split(' ')[0...-1].reverse
|
109
|
+
end
|
110
|
+
|
111
|
+
def add_ancestors_of(page, ancestors)
|
112
|
+
return ancestors if !page
|
113
|
+
add_ancestors_of(page.parent, ancestors << page)
|
114
|
+
end
|
115
|
+
|
116
|
+
def make_breadcrumb(page, is_current_page)
|
117
|
+
return nil unless (text = page.data.breadcrumb || page.data.title)
|
118
|
+
if is_current_page
|
119
|
+
css_class = 'active'
|
120
|
+
link = content_tag :span, text
|
121
|
+
else
|
122
|
+
link = link_to(text, '/' + page.path)
|
123
|
+
end
|
124
|
+
content_tag :li, link, :class => css_class
|
125
|
+
end
|
126
|
+
|
127
|
+
def bookbinder_logger
|
128
|
+
BookbinderLogger.new
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
::Middleman::Extensions.register(:navigation, Bookbinder::Navigation)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'bookbinder_helpers'
|
2
|
+
require 'submodule_aware_assets'
|
3
|
+
|
4
|
+
set :markdown_engine, :redcarpet
|
5
|
+
set :markdown, :layout_engine => :erb,
|
6
|
+
:tables => true,
|
7
|
+
:autolink => true,
|
8
|
+
:smartypants => true,
|
9
|
+
:fenced_code_blocks => true
|
10
|
+
|
11
|
+
set :css_dir, 'stylesheets'
|
12
|
+
|
13
|
+
set :js_dir, 'javascripts'
|
14
|
+
|
15
|
+
set :images_dir, 'images'
|
16
|
+
|
17
|
+
set :relative_links, true
|
18
|
+
|
19
|
+
activate :submodule_aware_assets
|
20
|
+
|
21
|
+
activate :navigation
|
22
|
+
|
23
|
+
activate :syntax
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'redcarpet'
|
2
|
+
|
3
|
+
class QuicklinksRenderer < Redcarpet::Render::Base
|
4
|
+
class BadHeadingLevelError < StandardError; end
|
5
|
+
|
6
|
+
attr_reader :vars
|
7
|
+
|
8
|
+
def initialize(template_variables)
|
9
|
+
super()
|
10
|
+
@vars = template_variables
|
11
|
+
end
|
12
|
+
|
13
|
+
def doc_header
|
14
|
+
@items = []
|
15
|
+
@items[1] = document.css('ul').first
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def doc_footer
|
20
|
+
document.css('.quick-links').to_html if any_headers?
|
21
|
+
end
|
22
|
+
|
23
|
+
def header(text, header_level)
|
24
|
+
return unless [2, 3].include?(header_level)
|
25
|
+
return unless anchor_for(text)
|
26
|
+
|
27
|
+
li = Nokogiri::XML::Node.new('li', document)
|
28
|
+
li.add_child anchor_for(text)
|
29
|
+
last_list_of_level(header_level-1).add_child(li)
|
30
|
+
@items[header_level] = li
|
31
|
+
nil
|
32
|
+
rescue BadHeadingLevelError => e
|
33
|
+
raise BadHeadingLevelError.new "The header \"#{text}\", which is at level #{e.message} has no higher-level headers occuring before it."
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def any_headers?
|
39
|
+
@items[2]
|
40
|
+
end
|
41
|
+
|
42
|
+
def anchor_for(text)
|
43
|
+
text = ERB.new(text).result(binding)
|
44
|
+
doc = Nokogiri::HTML(text)
|
45
|
+
target_anchor = doc.css('a').first
|
46
|
+
return unless target_anchor && target_anchor['id']
|
47
|
+
|
48
|
+
anchor = Nokogiri::XML::Node.new('a', document)
|
49
|
+
anchor['href'] = "##{target_anchor['id']}"
|
50
|
+
anchor.content = doc.text.strip
|
51
|
+
anchor
|
52
|
+
end
|
53
|
+
|
54
|
+
def last_list_of_level(n)
|
55
|
+
item = @items[n]
|
56
|
+
raise BadHeadingLevelError.new("#{n+1}") unless item
|
57
|
+
return item if item.name == 'ul'
|
58
|
+
|
59
|
+
item.add_child('<ul>') unless item.css('ul').any?
|
60
|
+
@items[n] = item.css('ul').first
|
61
|
+
end
|
62
|
+
|
63
|
+
def document
|
64
|
+
builder.doc
|
65
|
+
end
|
66
|
+
|
67
|
+
def builder
|
68
|
+
@builder ||= Nokogiri::HTML::Builder.new(&base_quicklinks_doc)
|
69
|
+
end
|
70
|
+
|
71
|
+
def base_quicklinks_doc
|
72
|
+
Proc.new do |html|
|
73
|
+
html.div(class: 'quick-links') {
|
74
|
+
html.ul
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|