bookbindery 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|