bookbindery 1.0.0

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