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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bookbinder +6 -0
  3. data/lib/bookbinder.rb +59 -0
  4. data/lib/bookbinder/app_fetcher.rb +40 -0
  5. data/lib/bookbinder/archive.rb +95 -0
  6. data/lib/bookbinder/artifact_namer.rb +22 -0
  7. data/lib/bookbinder/blue_green_app.rb +27 -0
  8. data/lib/bookbinder/book.rb +54 -0
  9. data/lib/bookbinder/bookbinder_logger.rb +33 -0
  10. data/lib/bookbinder/cf_command_runner.rb +114 -0
  11. data/lib/bookbinder/cf_routes.rb +19 -0
  12. data/lib/bookbinder/cli.rb +68 -0
  13. data/lib/bookbinder/cli_error.rb +6 -0
  14. data/lib/bookbinder/code_example.rb +40 -0
  15. data/lib/bookbinder/command_runner.rb +32 -0
  16. data/lib/bookbinder/command_validator.rb +24 -0
  17. data/lib/bookbinder/commands/bookbinder_command.rb +18 -0
  18. data/lib/bookbinder/commands/build_and_push_tarball.rb +31 -0
  19. data/lib/bookbinder/commands/generate_pdf.rb +140 -0
  20. data/lib/bookbinder/commands/help.rb +31 -0
  21. data/lib/bookbinder/commands/naming.rb +9 -0
  22. data/lib/bookbinder/commands/publish.rb +138 -0
  23. data/lib/bookbinder/commands/push_local_to_staging.rb +35 -0
  24. data/lib/bookbinder/commands/push_to_prod.rb +35 -0
  25. data/lib/bookbinder/commands/run_publish_ci.rb +42 -0
  26. data/lib/bookbinder/commands/tag.rb +31 -0
  27. data/lib/bookbinder/commands/update_local_doc_repos.rb +27 -0
  28. data/lib/bookbinder/commands/version.rb +25 -0
  29. data/lib/bookbinder/configuration.rb +163 -0
  30. data/lib/bookbinder/configuration_fetcher.rb +55 -0
  31. data/lib/bookbinder/configuration_validator.rb +162 -0
  32. data/lib/bookbinder/css_link_checker.rb +64 -0
  33. data/lib/bookbinder/directory_helpers.rb +15 -0
  34. data/lib/bookbinder/distributor.rb +69 -0
  35. data/lib/bookbinder/git_client.rb +63 -0
  36. data/lib/bookbinder/git_hub_repository.rb +151 -0
  37. data/lib/bookbinder/local_file_system_accessor.rb +9 -0
  38. data/lib/bookbinder/middleman_runner.rb +86 -0
  39. data/lib/bookbinder/pdf_generator.rb +73 -0
  40. data/lib/bookbinder/publisher.rb +125 -0
  41. data/lib/bookbinder/pusher.rb +34 -0
  42. data/lib/bookbinder/remote_yaml_credential_provider.rb +21 -0
  43. data/lib/bookbinder/section.rb +78 -0
  44. data/lib/bookbinder/server_director.rb +53 -0
  45. data/lib/bookbinder/shell_out.rb +19 -0
  46. data/lib/bookbinder/sieve.rb +62 -0
  47. data/lib/bookbinder/sitemap_generator.rb +19 -0
  48. data/lib/bookbinder/spider.rb +91 -0
  49. data/lib/bookbinder/stabilimentum.rb +59 -0
  50. data/lib/bookbinder/usage_messenger.rb +33 -0
  51. data/lib/bookbinder/yaml_loader.rb +22 -0
  52. data/master_middleman/bookbinder_helpers.rb +133 -0
  53. data/master_middleman/config.rb +23 -0
  54. data/master_middleman/quicklinks_renderer.rb +78 -0
  55. data/master_middleman/submodule_aware_assets.rb +45 -0
  56. data/template_app/Gemfile +7 -0
  57. data/template_app/Gemfile.lock +20 -0
  58. data/template_app/app.rb +3 -0
  59. data/template_app/config.ru +9 -0
  60. data/template_app/lib/rack_static.rb +19 -0
  61. data/template_app/lib/vienna_application.rb +26 -0
  62. 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