bookbindery 8.0.0 → 8.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bookbinder.gemspec +3 -2
- data/install_bin/bookbinder +1 -3
- data/lib/bookbinder/cli.rb +86 -57
- data/lib/bookbinder/commands/bind.rb +7 -9
- data/lib/bookbinder/commands/collection.rb +2 -2
- data/lib/bookbinder/config/checkers/products_checker.rb +1 -16
- data/lib/bookbinder/config/configuration.rb +1 -2
- data/lib/bookbinder/config/product_config.rb +3 -15
- data/lib/bookbinder/config/validator.rb +1 -3
- data/lib/bookbinder/html_document_manipulator.rb +0 -7
- data/lib/bookbinder/legacy/cli.rb +71 -0
- data/lib/bookbinder/postprocessing/{sitemap_writer.rb → broken_links_checker.rb} +13 -12
- data/lib/bookbinder/spider.rb +18 -39
- data/lib/bookbinder/subnav/json_from_markdown_toc.rb +50 -43
- data/lib/bookbinder/subnav/template_creator.rb +1 -9
- data/master_middleman/bookbinder_helpers.rb +7 -0
- data/master_middleman/config.rb +2 -0
- data/template_app/Gemfile +1 -1
- data/template_app/lib/vienna_application.rb +4 -1
- metadata +21 -9
- data/lib/bookbinder/config/checkers/subnav_topics_checker.rb +0 -37
- data/lib/bookbinder/config/topic_config.rb +0 -29
- data/lib/bookbinder/sitemap_generator.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6cb870420c8d260fe02e262c3d8db416a668a3fa
|
4
|
+
data.tar.gz: c2b4a7842bcdbcba9006096c7f5b4e08c04eff33
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a45300da2301b2e37ea278f03d221d52592f9a6ad42429b349fe7169f75ae9b15aade134740f9c17a0effd101e807f00476f73970a8a4573ffea1a3ce0210e50
|
7
|
+
data.tar.gz: 1b3fa518acaa00628b2115d4c166df2272d08ca564416565a36e3d60aa69c82404d8dd97de84f2ed939eb2a04b608b11d8e4dfce8eadcb37f0d042f0a607c8e6
|
data/bookbinder.gemspec
CHANGED
@@ -2,7 +2,7 @@ require 'base64'
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = 'bookbindery'
|
5
|
-
s.version = '8.
|
5
|
+
s.version = '8.1.0'
|
6
6
|
s.summary = 'Markdown to Rackup application documentation generator'
|
7
7
|
s.description = 'A command line utility to be run in Book repositories to stitch together their constituent Markdown repos into a static-HTML-serving application'
|
8
8
|
s.authors = ['Mike Grafton', 'Lucas Marks', 'Gavin Morgan', 'Nikhil Gajwani', 'Dan Wendorf', 'Brenda Chan', 'Matthew Boedicker', 'Frank Kotsianas', 'Elena Sharma', 'Christa Hartsock', 'Michael Trestman']
|
@@ -14,7 +14,7 @@ Gem::Specification.new do |s|
|
|
14
14
|
s.bindir = 'install_bin'
|
15
15
|
s.executable = 'bookbinder'
|
16
16
|
|
17
|
-
s.required_ruby_version = '
|
17
|
+
s.required_ruby_version = '~> 2.3.0'
|
18
18
|
s.add_runtime_dependency 'fog-aws', ['~> 0.7.1']
|
19
19
|
s.add_runtime_dependency 'ansi', ['~> 1.4']
|
20
20
|
s.add_runtime_dependency 'unf', ['~> 0.1']
|
@@ -30,6 +30,7 @@ Gem::Specification.new do |s|
|
|
30
30
|
s.add_runtime_dependency 'therubyracer'
|
31
31
|
s.add_runtime_dependency 'git', '~> 1.2.8'
|
32
32
|
s.add_runtime_dependency 'nokogiri', ['1.6.7.2']
|
33
|
+
s.add_runtime_dependency 'thor'
|
33
34
|
|
34
35
|
s.add_development_dependency 'license_finder'
|
35
36
|
s.add_development_dependency 'pry-byebug'
|
data/install_bin/bookbinder
CHANGED
data/lib/bookbinder/cli.rb
CHANGED
@@ -1,69 +1,98 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require_relative '
|
4
|
-
require_relative '
|
5
|
-
require_relative 'streams/colorized_stream'
|
6
|
-
require_relative 'terminal'
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
require_relative 'ingest/git_accessor'
|
4
|
+
require_relative 'legacy/cli'
|
7
5
|
|
8
6
|
module Bookbinder
|
9
|
-
class
|
10
|
-
|
11
|
-
|
7
|
+
class CLI < Thor
|
8
|
+
map '--version' => :version
|
9
|
+
map '--help' => :help
|
10
|
+
|
11
|
+
desc '--version', 'Print the version of bookbinder'
|
12
|
+
def version
|
13
|
+
gemspec = File.expand_path('../../../bookbinder.gemspec', __FILE__)
|
14
|
+
say "bookbinder #{Gem::Specification::load(gemspec).version}"
|
15
|
+
end
|
16
|
+
|
17
|
+
desc '--help', 'Print this message'
|
18
|
+
def help
|
19
|
+
super
|
12
20
|
end
|
13
21
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
22
|
+
desc 'generate <book_name>', 'Generate a skeleton book that can be bound with "bookbinder bind"'
|
23
|
+
def generate(book_name)
|
24
|
+
run_legacy_cli('generate', book_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'build_and_push_tarball', 'Create a tarball from the final_app directory and push to the S3 bucket specified in your credentials.yml'
|
28
|
+
def build_and_push_tarball
|
29
|
+
run_legacy_cli('build_and_push_tarball')
|
30
|
+
end
|
31
|
+
|
32
|
+
desc 'bind <local|remote> [--verbose] [--dita-flags=\"<dita-option>=<value>\"]', 'Bind the sections specified in config.yml from <local> or <remote> into the final_app directory'
|
33
|
+
option :verbose, type: :boolean
|
34
|
+
option 'dita-flags'
|
35
|
+
def bind(source)
|
36
|
+
args = ['bind', source]
|
37
|
+
args << '--verbose' if options[:verbose]
|
38
|
+
args << "--dita-flags=\\\"#{options['dita-flags']}\\\""
|
39
|
+
run_legacy_cli(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'push_local_to <environment>', 'Push the contents of final_app to the specified environment using the credentials.yml'
|
43
|
+
def push_local_to(environment)
|
44
|
+
run_legacy_cli('push_local_to', environment)
|
45
|
+
end
|
46
|
+
|
47
|
+
desc 'push_to_prod [build_#]', 'Push latest or <build_#> from your S3 bucket to the production host specified in credentials.yml'
|
48
|
+
def push_to_prod(build_num=nil)
|
49
|
+
args = ['push_to_prod', build_num].compact
|
50
|
+
run_legacy_cli(*args)
|
51
|
+
end
|
52
|
+
|
53
|
+
desc 'run_publish_ci', 'Run publish, push_local_to staging, and build_and_push_tarball for CI purposes'
|
54
|
+
def run_publish_ci
|
55
|
+
run_legacy_cli('run_publish_ci')
|
56
|
+
end
|
57
|
+
|
58
|
+
desc 'punch <git tag>', 'Apply the specified <git tag> to your book, sections, and layout repo'
|
59
|
+
def punch(git_tag)
|
60
|
+
run_legacy_cli('punch', git_tag)
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'update_local_doc_repos', 'Run `git pull` on all sections that exist at the same directory level as your book directory'
|
64
|
+
def update_local_doc_repos
|
65
|
+
run_legacy_cli('update_local_doc_repos')
|
66
|
+
end
|
67
|
+
|
68
|
+
desc 'watch', 'Bind and serve a local book, watching for changes'
|
69
|
+
def watch
|
70
|
+
run_legacy_cli('watch')
|
71
|
+
end
|
72
|
+
|
73
|
+
desc 'imprint <local|remote> [--verbose] [--dita-flags=\"<dita-option>=<value>\"]', 'Generate a PDF for a given book'
|
74
|
+
option :verbose, type: :boolean
|
75
|
+
option 'dita-flags'
|
76
|
+
def imprint(source)
|
77
|
+
args = ['imprint', source]
|
78
|
+
args << '--verbose' if options[:verbose]
|
79
|
+
args << "--dita-flags=\\\"#{options['dita-flags']}\\\""
|
80
|
+
run_legacy_cli(*args)
|
54
81
|
end
|
55
82
|
|
56
83
|
private
|
57
84
|
|
58
|
-
attr_reader :
|
85
|
+
attr_reader :legacy_cli
|
86
|
+
|
87
|
+
def initialize(*)
|
88
|
+
super
|
89
|
+
|
90
|
+
@legacy_cli = Legacy::Cli.new(Ingest::GitAccessor.new)
|
91
|
+
end
|
59
92
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
out: $stdout,
|
64
|
-
success: Streams::ColorizedStream.new(Colorizer::Colors.green, $stdout),
|
65
|
-
warn: Streams::ColorizedStream.new(Colorizer::Colors.yellow, $stdout),
|
66
|
-
}
|
93
|
+
def run_legacy_cli(*args)
|
94
|
+
status = legacy_cli.run(args)
|
95
|
+
exit status unless status.zero?
|
67
96
|
end
|
68
97
|
end
|
69
98
|
end
|
@@ -14,7 +14,7 @@ module Bookbinder
|
|
14
14
|
config_decorator: nil,
|
15
15
|
file_system_accessor: nil,
|
16
16
|
middleman_runner: nil,
|
17
|
-
|
17
|
+
broken_links_checker: nil,
|
18
18
|
preprocessor: nil,
|
19
19
|
cloner_factory: nil,
|
20
20
|
section_repository: nil,
|
@@ -26,7 +26,7 @@ module Bookbinder
|
|
26
26
|
@config_decorator = config_decorator
|
27
27
|
@file_system_accessor = file_system_accessor
|
28
28
|
@middleman_runner = middleman_runner
|
29
|
-
@
|
29
|
+
@broken_links_checker = broken_links_checker
|
30
30
|
@preprocessor = preprocessor
|
31
31
|
@cloner_factory = cloner_factory
|
32
32
|
@section_repository = section_repository
|
@@ -85,15 +85,13 @@ module Bookbinder
|
|
85
85
|
)
|
86
86
|
if generation_result.success?
|
87
87
|
file_system_accessor.copy(output_locations.build_dir, output_locations.public_dir)
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
bind_config.broken_link_exclusions
|
92
|
-
)
|
88
|
+
|
89
|
+
broken_links_checker.check!(bind_config.broken_link_exclusions)
|
90
|
+
broken_links_checker.announce(bind_options.streams)
|
93
91
|
|
94
92
|
bind_options.streams[:success].puts "Bookbinder bound your book into #{output_locations.final_app_dir}"
|
95
93
|
|
96
|
-
|
94
|
+
broken_links_checker.has_broken_links? ? 1 : 0
|
97
95
|
else
|
98
96
|
bind_options.streams[:err].puts "Your bind failed. Rerun with --verbose to troubleshoot."
|
99
97
|
1
|
@@ -113,7 +111,7 @@ module Bookbinder
|
|
113
111
|
:output_locations,
|
114
112
|
:preprocessor,
|
115
113
|
:section_repository,
|
116
|
-
:
|
114
|
+
:broken_links_checker,
|
117
115
|
:middleman_runner,
|
118
116
|
)
|
119
117
|
|
@@ -18,7 +18,7 @@ require_relative '../ingest/cloner_factory'
|
|
18
18
|
require_relative '../ingest/section_repository'
|
19
19
|
require_relative '../local_filesystem_accessor'
|
20
20
|
require_relative '../middleman_runner'
|
21
|
-
require_relative '../postprocessing/
|
21
|
+
require_relative '../postprocessing/broken_links_checker'
|
22
22
|
require_relative '../preprocessing/dita_html_preprocessor'
|
23
23
|
require_relative '../preprocessing/dita_pdf_preprocessor'
|
24
24
|
require_relative '../preprocessing/link_to_site_gen_dir'
|
@@ -97,7 +97,7 @@ module Bookbinder
|
|
97
97
|
config_decorator: Config::ConfigurationDecorator.new(loader: config_loader, config_filename: 'bookbinder.yml'),
|
98
98
|
file_system_accessor: local_filesystem_accessor,
|
99
99
|
middleman_runner: runner,
|
100
|
-
|
100
|
+
broken_links_checker: Postprocessing::BrokenLinksChecker.build(final_app_directory, sitemap_port),
|
101
101
|
preprocessor: Preprocessing::Preprocessor.new(
|
102
102
|
Preprocessing::DitaHTMLPreprocessor.new(
|
103
103
|
local_filesystem_accessor,
|
@@ -2,7 +2,6 @@ module Bookbinder
|
|
2
2
|
module Config
|
3
3
|
module Checkers
|
4
4
|
class ProductsChecker
|
5
|
-
MissingRequiredKeyError = Class.new(RuntimeError)
|
6
5
|
MissingProductsKeyError = Class.new(RuntimeError)
|
7
6
|
MissingProductIdError = Class.new(RuntimeError)
|
8
7
|
|
@@ -11,11 +10,9 @@ module Bookbinder
|
|
11
10
|
|
12
11
|
if section_product_ids.count > 0
|
13
12
|
if config.products.empty?
|
14
|
-
|
13
|
+
MissingProductsKeyError.new('You must specify at least one product under the products key in config.yml')
|
15
14
|
elsif missing_products.count != 0
|
16
15
|
MissingProductIdError.new("Your config.yml is missing required product id under the products key. Required product ids are #{missing_products.join(", ")}.")
|
17
|
-
elsif invalid_products.any?
|
18
|
-
MissingRequiredKeyError.new("Your config.yml is missing required key(s) for products #{invalid_product_ids}. Required keys are #{required_product_keys.join(", ")}.")
|
19
16
|
end
|
20
17
|
end
|
21
18
|
end
|
@@ -24,22 +21,10 @@ module Bookbinder
|
|
24
21
|
|
25
22
|
private
|
26
23
|
|
27
|
-
def invalid_products
|
28
|
-
config.products.map {|product_config| product_config unless product_config.valid? }
|
29
|
-
end
|
30
|
-
|
31
|
-
def invalid_product_ids
|
32
|
-
invalid_products.map(&:id).join(', ')
|
33
|
-
end
|
34
|
-
|
35
24
|
def missing_products
|
36
25
|
section_product_ids - config.products.map(&:id)
|
37
26
|
end
|
38
27
|
|
39
|
-
def required_product_keys
|
40
|
-
Bookbinder::Config::ProductConfig::CONFIG_REQUIRED_KEYS
|
41
|
-
end
|
42
|
-
|
43
28
|
def section_product_ids
|
44
29
|
config.sections.map(&:product_id).compact.uniq
|
45
30
|
end
|
@@ -57,7 +57,7 @@ module Bookbinder
|
|
57
57
|
end
|
58
58
|
|
59
59
|
CONFIG_REQUIRED_KEYS = %w(book_repo public_host)
|
60
|
-
CONFIG_OPTIONAL_KEYS = %w(archive_menu book_repo_url cred_repo cred_repo_url repo_link_enabled repo_links feedback_enabled layout_repo layout_repo_ref layout_repo_url sections
|
60
|
+
CONFIG_OPTIONAL_KEYS = %w(archive_menu book_repo_url cred_repo cred_repo_url repo_link_enabled repo_links feedback_enabled layout_repo layout_repo_ref layout_repo_url sections)
|
61
61
|
|
62
62
|
CONFIG_REQUIRED_KEYS.each do |method_name|
|
63
63
|
define_method(method_name) do
|
@@ -104,7 +104,6 @@ module Bookbinder
|
|
104
104
|
def assemble_products
|
105
105
|
if config[:products]
|
106
106
|
config[:products].map do |product|
|
107
|
-
product.merge!({'subnav_exclusions' => subnav_exclusions})
|
108
107
|
Config::ProductConfig.new(product)
|
109
108
|
end
|
110
109
|
end
|
@@ -1,11 +1,8 @@
|
|
1
|
-
require_relative '../config/topic_config'
|
2
|
-
|
3
1
|
module Bookbinder
|
4
2
|
module Config
|
5
3
|
class ProductConfig
|
6
4
|
def initialize(config)
|
7
5
|
@config = config
|
8
|
-
@subnav_topics = assemble_topics || []
|
9
6
|
end
|
10
7
|
|
11
8
|
def id
|
@@ -16,30 +13,21 @@ module Bookbinder
|
|
16
13
|
config['pdf_config']
|
17
14
|
end
|
18
15
|
|
19
|
-
def
|
20
|
-
config['
|
21
|
-
end
|
22
|
-
|
23
|
-
def specifies_subnav?
|
24
|
-
!subnav_topics.empty?
|
16
|
+
def subnav_root
|
17
|
+
config['subnav_root']
|
25
18
|
end
|
26
19
|
|
27
20
|
def valid?
|
28
21
|
(CONFIG_REQUIRED_KEYS - config.keys).empty?
|
29
22
|
end
|
30
23
|
|
31
|
-
CONFIG_REQUIRED_KEYS = %w(id
|
24
|
+
CONFIG_REQUIRED_KEYS = %w(id)
|
32
25
|
|
33
|
-
attr_reader :subnav_topics
|
34
26
|
alias_method :subnav_name, :id
|
35
27
|
|
36
28
|
private
|
37
29
|
|
38
30
|
attr_reader :config
|
39
|
-
|
40
|
-
def assemble_topics
|
41
|
-
config['subnav_topics'].map{|topic| Config::TopicConfig.new(topic)} if config['subnav_topics']
|
42
|
-
end
|
43
31
|
end
|
44
32
|
end
|
45
33
|
end
|
@@ -5,7 +5,6 @@ require_relative 'checkers/repository_name_presence_checker'
|
|
5
5
|
require_relative 'checkers/ditamap_presence_checker'
|
6
6
|
require_relative 'checkers/section_presence_checker'
|
7
7
|
require_relative 'checkers/products_checker'
|
8
|
-
require_relative 'checkers/subnav_topics_checker'
|
9
8
|
|
10
9
|
module Bookbinder
|
11
10
|
module Config
|
@@ -22,8 +21,7 @@ module Bookbinder
|
|
22
21
|
Checkers::SectionPresenceChecker.new,
|
23
22
|
Checkers::DitamapPresenceChecker.new,
|
24
23
|
Checkers::ArchiveMenuChecker.new(@file_system_accessor),
|
25
|
-
Checkers::ProductsChecker.new
|
26
|
-
Checkers::SubnavTopicsChecker.new
|
24
|
+
Checkers::ProductsChecker.new
|
27
25
|
].map do |checker|
|
28
26
|
checker.check(config)
|
29
27
|
end
|
@@ -12,13 +12,6 @@ module Bookbinder
|
|
12
12
|
doc.to_html
|
13
13
|
end
|
14
14
|
|
15
|
-
def add_class(document: nil, selector: nil, classname: nil)
|
16
|
-
doc = Nokogiri::HTML.fragment(document)
|
17
|
-
node_set = doc.css(selector)
|
18
|
-
node_set.add_class(classname)
|
19
|
-
doc.to_html
|
20
|
-
end
|
21
|
-
|
22
15
|
def read_html_in_tag(document: nil, tag: nil)
|
23
16
|
doc = Nokogiri::HTML(document)
|
24
17
|
doc.css(tag).inner_html
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require_relative '../command_runner'
|
2
|
+
require_relative '../command_validator'
|
3
|
+
require_relative '../commands/collection'
|
4
|
+
require_relative '../config/cf_credentials'
|
5
|
+
require_relative '../streams/colorized_stream'
|
6
|
+
require_relative '../terminal'
|
7
|
+
|
8
|
+
module Bookbinder
|
9
|
+
module Legacy
|
10
|
+
class Cli
|
11
|
+
def initialize(version_control_system)
|
12
|
+
@version_control_system = version_control_system
|
13
|
+
end
|
14
|
+
|
15
|
+
def run(args)
|
16
|
+
command_name, *command_arguments = args
|
17
|
+
|
18
|
+
logger = DeprecatedLogger.new
|
19
|
+
commands = Commands::Collection.new(
|
20
|
+
logger,
|
21
|
+
colorized_streams,
|
22
|
+
version_control_system
|
23
|
+
)
|
24
|
+
|
25
|
+
command_validator = CommandValidator.new(commands, commands.help.usage_message)
|
26
|
+
command_runner = CommandRunner.new(logger, commands)
|
27
|
+
command_name = command_name ? command_name : '--help'
|
28
|
+
|
29
|
+
colorizer = Colorizer.new
|
30
|
+
terminal = Terminal.new(colorizer)
|
31
|
+
|
32
|
+
user_message = command_validator.validate(command_name)
|
33
|
+
terminal.update(user_message)
|
34
|
+
|
35
|
+
if user_message.error?
|
36
|
+
return 1
|
37
|
+
end
|
38
|
+
|
39
|
+
begin
|
40
|
+
command_runner.run(command_name, command_arguments)
|
41
|
+
|
42
|
+
rescue Config::CfCredentials::CredentialKeyError => e
|
43
|
+
colorized_streams[:err].puts "#{e.message}, in credentials.yml"
|
44
|
+
1
|
45
|
+
rescue KeyError => e
|
46
|
+
colorized_streams[:err].puts "#{e.message} from your configuration."
|
47
|
+
1
|
48
|
+
rescue CliError::UnknownCommand => e
|
49
|
+
colorized_streams[:out].puts e.message
|
50
|
+
1
|
51
|
+
rescue RuntimeError => e
|
52
|
+
colorized_streams[:err].puts e.message
|
53
|
+
1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :version_control_system
|
60
|
+
|
61
|
+
def colorized_streams
|
62
|
+
{
|
63
|
+
err: Streams::ColorizedStream.new(Colorizer::Colors.red, $stderr),
|
64
|
+
out: $stdout,
|
65
|
+
success: Streams::ColorizedStream.new(Colorizer::Colors.green, $stdout),
|
66
|
+
warn: Streams::ColorizedStream.new(Colorizer::Colors.yellow, $stdout),
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -4,8 +4,8 @@ require_relative '../../../template_app/rack_app'
|
|
4
4
|
|
5
5
|
module Bookbinder
|
6
6
|
module Postprocessing
|
7
|
-
class
|
8
|
-
def self.build(
|
7
|
+
class BrokenLinksChecker
|
8
|
+
def self.build(final_app_directory, port)
|
9
9
|
new(
|
10
10
|
Spider.new(app_dir: final_app_directory),
|
11
11
|
ServerDirector.new(
|
@@ -21,17 +21,18 @@ module Bookbinder
|
|
21
21
|
@server_director = server_director
|
22
22
|
end
|
23
23
|
|
24
|
-
def
|
24
|
+
def check!(broken_link_exclusions)
|
25
25
|
server_director.use_server { |port|
|
26
|
-
spider.
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
26
|
+
@result = spider.find_broken_links(port, broken_link_exclusions: broken_link_exclusions)
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def announce(streams)
|
31
|
+
@result.announce_broken_links(streams)
|
32
|
+
end
|
33
|
+
|
34
|
+
def has_broken_links?
|
35
|
+
@result.has_broken_links?
|
35
36
|
end
|
36
37
|
|
37
38
|
private
|
data/lib/bookbinder/spider.rb
CHANGED
@@ -3,27 +3,31 @@ require 'pty'
|
|
3
3
|
require_relative 'css_link_checker'
|
4
4
|
require_relative 'sieve'
|
5
5
|
require_relative 'stabilimentum'
|
6
|
-
require_relative 'sitemap_generator'
|
7
6
|
|
8
7
|
module Bookbinder
|
9
8
|
class Spider
|
10
9
|
class Result
|
11
|
-
def initialize(broken_links
|
10
|
+
def initialize(broken_links)
|
12
11
|
@broken_links = broken_links
|
13
|
-
@sitemap = sitemap
|
14
|
-
@app_dir = app_dir
|
15
12
|
end
|
16
13
|
|
17
14
|
def has_broken_links?
|
18
15
|
@broken_links.any?
|
19
16
|
end
|
20
17
|
|
21
|
-
def
|
22
|
-
@
|
23
|
-
|
18
|
+
def announce_broken_links(streams)
|
19
|
+
if @broken_links.none?
|
20
|
+
streams[:out].puts "\nNo broken links!"
|
21
|
+
else
|
22
|
+
streams[:err].puts(<<-MESSAGE)
|
23
|
+
|
24
|
+
Found #{@broken_links.count} broken links!
|
24
25
|
|
25
|
-
|
26
|
-
|
26
|
+
#{@broken_links.sort.join("\n")}
|
27
|
+
|
28
|
+
Found #{@broken_links.count} broken links!
|
29
|
+
MESSAGE
|
30
|
+
end
|
27
31
|
end
|
28
32
|
end
|
29
33
|
|
@@ -35,41 +39,20 @@ module Bookbinder
|
|
35
39
|
@app_dir = app_dir || raise('Spiders must be initialized with an app directory.')
|
36
40
|
end
|
37
41
|
|
38
|
-
def
|
39
|
-
broken_link_exclusions: /(?!.*)/)
|
42
|
+
def find_broken_links(port, broken_link_exclusions: /(?!.*)/)
|
40
43
|
temp_host = "localhost:#{port}"
|
41
|
-
|
42
44
|
sieve = Sieve.new domain: "http://#{temp_host}"
|
43
45
|
links = crawl_from "http://#{temp_host}/index.html", sieve
|
44
|
-
broken_links
|
45
|
-
sitemap_links = substitute_hostname(temp_host, target_host, working_links)
|
46
|
+
broken_links = links.first
|
46
47
|
public_broken_links = broken_links.reject {|l| l.match(broken_link_exclusions)}
|
47
|
-
|
48
|
-
Result.new(
|
49
|
-
public_broken_links,
|
50
|
-
SitemapGenerator.new.generate(sitemap_links), app_dir
|
51
|
-
)
|
48
|
+
|
49
|
+
Result.new(public_broken_links)
|
52
50
|
end
|
53
51
|
|
54
52
|
private
|
55
53
|
|
56
54
|
attr_reader :app_dir
|
57
55
|
|
58
|
-
def announce_broken_links(broken_links, streams)
|
59
|
-
if broken_links.none?
|
60
|
-
streams[:out].puts "\nNo broken links!"
|
61
|
-
else
|
62
|
-
streams[:err].puts(<<-MESSAGE)
|
63
|
-
|
64
|
-
Found #{broken_links.count} broken links!
|
65
|
-
|
66
|
-
#{broken_links.sort.join("\n")}
|
67
|
-
|
68
|
-
Found #{broken_links.count} broken links!
|
69
|
-
MESSAGE
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
56
|
def crawl_from(url, sieve)
|
74
57
|
broken_links = []
|
75
58
|
sitemap = [url]
|
@@ -79,7 +62,7 @@ Found #{broken_links.count} broken links!
|
|
79
62
|
Anemone.crawl(url) do |anemone|
|
80
63
|
dont_visit_fragments(anemone)
|
81
64
|
anemone.on_every_page do |page|
|
82
|
-
broken, working = sieve.links_from
|
65
|
+
broken, working = sieve.links_from(Stabilimentum.new(page), is_first_pass)
|
83
66
|
broken_links.concat broken
|
84
67
|
sitemap.concat working
|
85
68
|
end
|
@@ -93,9 +76,5 @@ Found #{broken_links.count} broken links!
|
|
93
76
|
def dont_visit_fragments(anemone)
|
94
77
|
anemone.focus_crawl { |page| page.links.reject { |link| link.to_s.match(/%23/) } }
|
95
78
|
end
|
96
|
-
|
97
|
-
def substitute_hostname(temp_host, target_host, links)
|
98
|
-
links.map { |l| l.gsub(/#{Regexp.escape(temp_host)}/, target_host) }
|
99
|
-
end
|
100
79
|
end
|
101
80
|
end
|
@@ -5,72 +5,79 @@ require 'redcarpet'
|
|
5
5
|
module Bookbinder
|
6
6
|
module Subnav
|
7
7
|
class JsonFromMarkdownToc
|
8
|
+
SubnavDuplicateLinkError = Class.new(RuntimeError)
|
9
|
+
SubnavBrokenLinkError = Class.new(RuntimeError)
|
10
|
+
SubnavRootMissingError = Class.new(RuntimeError)
|
11
|
+
|
8
12
|
def initialize(fs)
|
9
13
|
@fs = fs
|
10
14
|
@renderer = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new)
|
11
15
|
end
|
12
16
|
|
13
|
-
def get_links(
|
17
|
+
def get_links(product_config, output_locations)
|
14
18
|
@source_for_site_gen = output_locations.source_for_site_generator
|
15
|
-
@config =
|
19
|
+
@config = product_config
|
16
20
|
|
17
|
-
|
18
|
-
end
|
21
|
+
root = absolute_source_from_path(Pathname(config.subnav_root))
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
private
|
23
|
+
raise SubnavRootMissingError.new('Subnav root not found at: ' + config.subnav_root) if root.nil?
|
23
24
|
|
24
|
-
|
25
|
-
menu_items = []
|
25
|
+
@parsed_files = { Pathname(root) => '(root)'}
|
26
26
|
|
27
|
-
|
28
|
-
menu_items << { text: topic.title, title: true }
|
29
|
-
menu_items << { url: "/#{topic.toc_path}.html", text: topic.toc_nav_name }
|
30
|
-
|
31
|
-
links_from_toc_page = parse_toc_file(topic)
|
32
|
-
links_from_toc_page.each {|link| menu_items << link}
|
33
|
-
end
|
34
|
-
|
35
|
-
menu_items
|
27
|
+
{links: gather_urls_and_texts(root)}.to_json
|
36
28
|
end
|
37
29
|
|
38
|
-
|
39
|
-
toc_files = fs.find_files_extension_agnostically(topic.toc_path, source_for_site_gen)
|
40
|
-
toc_md = fs.read(toc_files.first)
|
41
|
-
|
42
|
-
toc_html = get_html(toc_md)
|
30
|
+
attr_reader :fs, :source_for_site_gen, :renderer, :config
|
43
31
|
|
44
|
-
|
45
|
-
end
|
32
|
+
private
|
46
33
|
|
47
34
|
def get_html(md)
|
48
35
|
Nokogiri::HTML(renderer.render(md))
|
49
36
|
end
|
50
37
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
{text: element.inner_html}
|
55
|
-
else
|
56
|
-
list_elements = element.css('li > a', 'li > p > a')
|
57
|
-
list_elements.map do |li|
|
58
|
-
{ url: "/#{topic.toc_path.dirname.join(li['href'])}", text: li.inner_text }
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end.flatten
|
38
|
+
def absolute_source_from_path(path)
|
39
|
+
full_sources = fs.find_files_extension_agnostically(path, source_for_site_gen)
|
40
|
+
full_sources.first
|
62
41
|
end
|
63
42
|
|
64
|
-
|
65
|
-
|
66
|
-
|
43
|
+
# href: ./cat/index.html
|
44
|
+
# expanded href: my/cat/index.html
|
45
|
+
# full source: my/cat/index.html.md.erb
|
46
|
+
def gather_urls_and_texts(source)
|
47
|
+
toc_md = fs.read(source)
|
48
|
+
base_node = get_html(toc_md).css('html')
|
67
49
|
|
68
|
-
|
69
|
-
|
50
|
+
nav_items(base_node).map do |element|
|
51
|
+
href = element['href']
|
52
|
+
expanded_href = (source.dirname + href).relative_path_from(source_for_site_gen)
|
53
|
+
next_source = absolute_source_from_path(expanded_href)
|
54
|
+
|
55
|
+
raise SubnavBrokenLinkError.new(<<-ERROR) unless next_source
|
56
|
+
Broken link found in subnav for product_id: #{config.id}
|
57
|
+
|
58
|
+
Link: #{expanded_href}
|
59
|
+
Source file: #{source}
|
60
|
+
ERROR
|
61
|
+
raise SubnavDuplicateLinkError.new(<<-ERROR) if @parsed_files.has_key?(next_source)
|
62
|
+
)
|
63
|
+
Duplicate link found in subnav for product_id: #{config.id}
|
64
|
+
|
65
|
+
Link: #{expanded_href}
|
66
|
+
Original file: #{@parsed_files[next_source]}
|
67
|
+
Current file: #{source}
|
68
|
+
ERROR
|
69
|
+
|
70
|
+
@parsed_files[next_source] = source
|
71
|
+
nested_urls_and_texts = gather_urls_and_texts(next_source)
|
72
|
+
nested_links = {}
|
73
|
+
nested_links.merge!(nestedLinks: nested_urls_and_texts) unless nested_urls_and_texts.empty?
|
74
|
+
|
75
|
+
{url: '/' + expanded_href.to_s, text: element.inner_text}.merge(nested_links)
|
76
|
+
end
|
70
77
|
end
|
71
78
|
|
72
|
-
def
|
73
|
-
|
79
|
+
def nav_items(base_node)
|
80
|
+
base_node.css('a.subnav')
|
74
81
|
end
|
75
82
|
end
|
76
83
|
end
|
@@ -18,21 +18,13 @@ module Bookbinder
|
|
18
18
|
attribute: 'data-props-location',
|
19
19
|
value: props_filename)
|
20
20
|
|
21
|
-
|
22
|
-
selector: 'div.nav-content',
|
23
|
-
classname: nav_type(subnav_spec))
|
24
|
-
|
25
|
-
fs.write(text: nav_content, to: subnav_destination(subnav_spec.subnav_name))
|
21
|
+
fs.write(text: nav_with_props, to: subnav_destination(subnav_spec.subnav_name))
|
26
22
|
end
|
27
23
|
|
28
24
|
attr_reader :fs, :output_locations, :html_doc_manipulator
|
29
25
|
|
30
26
|
private
|
31
27
|
|
32
|
-
def nav_type(subnav_spec)
|
33
|
-
subnav_spec.subnav_name.include?('dita') ? 'deepnav-content' : 'shallownav-content'
|
34
|
-
end
|
35
|
-
|
36
28
|
def subnavs_path
|
37
29
|
output_locations.subnavs_for_layout_dir
|
38
30
|
end
|
@@ -110,6 +110,13 @@ module Bookbinder
|
|
110
110
|
Redcarpet::Markdown.new(quicklinks_renderer).render(page_src)
|
111
111
|
end
|
112
112
|
|
113
|
+
def owners
|
114
|
+
html_resources = sitemap.resources.select { |r| r.path.end_with?('.html') }
|
115
|
+
html_resources.each.with_object({}) { |resource, owners|
|
116
|
+
owners[resource.path] = Array(resource.data['owner'])
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
113
120
|
private
|
114
121
|
|
115
122
|
def subnav_template_name
|
data/master_middleman/config.rb
CHANGED
data/template_app/Gemfile
CHANGED
@@ -15,7 +15,10 @@ module Bookbinder
|
|
15
15
|
urls: Dir.glob("#{root}/**/*").map { |fn| fn.gsub(/^#{root}/, '')},
|
16
16
|
root: root,
|
17
17
|
index: 'index.html',
|
18
|
-
header_rules: [[:all, {
|
18
|
+
header_rules: [[:all, {
|
19
|
+
'Cache-Control' => 'public, max-age=3600',
|
20
|
+
'Access-Control-Allow-Origin' => '*'
|
21
|
+
}]]
|
19
22
|
}
|
20
23
|
run NotFound.new("#{root}/404.html")
|
21
24
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bookbindery
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 8.
|
4
|
+
version: 8.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Grafton
|
@@ -18,7 +18,7 @@ authors:
|
|
18
18
|
autorequire:
|
19
19
|
bindir: install_bin
|
20
20
|
cert_chain: []
|
21
|
-
date: 2016-
|
21
|
+
date: 2016-03-08 00:00:00.000000000 Z
|
22
22
|
dependencies:
|
23
23
|
- !ruby/object:Gem::Dependency
|
24
24
|
name: fog-aws
|
@@ -230,6 +230,20 @@ dependencies:
|
|
230
230
|
- - '='
|
231
231
|
- !ruby/object:Gem::Version
|
232
232
|
version: 1.6.7.2
|
233
|
+
- !ruby/object:Gem::Dependency
|
234
|
+
name: thor
|
235
|
+
requirement: !ruby/object:Gem::Requirement
|
236
|
+
requirements:
|
237
|
+
- - ">="
|
238
|
+
- !ruby/object:Gem::Version
|
239
|
+
version: '0'
|
240
|
+
type: :runtime
|
241
|
+
prerelease: false
|
242
|
+
version_requirements: !ruby/object:Gem::Requirement
|
243
|
+
requirements:
|
244
|
+
- - ">="
|
245
|
+
- !ruby/object:Gem::Version
|
246
|
+
version: '0'
|
233
247
|
- !ruby/object:Gem::Dependency
|
234
248
|
name: license_finder
|
235
249
|
requirement: !ruby/object:Gem::Requirement
|
@@ -344,7 +358,6 @@ files:
|
|
344
358
|
- lib/bookbinder/config/checkers/repository_name_presence_checker.rb
|
345
359
|
- lib/bookbinder/config/checkers/required_keys_checker.rb
|
346
360
|
- lib/bookbinder/config/checkers/section_presence_checker.rb
|
347
|
-
- lib/bookbinder/config/checkers/subnav_topics_checker.rb
|
348
361
|
- lib/bookbinder/config/configuration.rb
|
349
362
|
- lib/bookbinder/config/configuration_decorator.rb
|
350
363
|
- lib/bookbinder/config/dita_config_generator.rb
|
@@ -353,7 +366,6 @@ files:
|
|
353
366
|
- lib/bookbinder/config/product_config.rb
|
354
367
|
- lib/bookbinder/config/remote_yaml_credential_provider.rb
|
355
368
|
- lib/bookbinder/config/section_config.rb
|
356
|
-
- lib/bookbinder/config/topic_config.rb
|
357
369
|
- lib/bookbinder/config/validator.rb
|
358
370
|
- lib/bookbinder/config/yaml_loader.rb
|
359
371
|
- lib/bookbinder/css_link_checker.rb
|
@@ -385,9 +397,10 @@ files:
|
|
385
397
|
- lib/bookbinder/ingest/update_failure.rb
|
386
398
|
- lib/bookbinder/ingest/update_success.rb
|
387
399
|
- lib/bookbinder/ingest/working_copy.rb
|
400
|
+
- lib/bookbinder/legacy/cli.rb
|
388
401
|
- lib/bookbinder/local_filesystem_accessor.rb
|
389
402
|
- lib/bookbinder/middleman_runner.rb
|
390
|
-
- lib/bookbinder/postprocessing/
|
403
|
+
- lib/bookbinder/postprocessing/broken_links_checker.rb
|
391
404
|
- lib/bookbinder/preprocessing/dita_html_preprocessor.rb
|
392
405
|
- lib/bookbinder/preprocessing/dita_pdf_preprocessor.rb
|
393
406
|
- lib/bookbinder/preprocessing/dita_preprocessor.rb
|
@@ -396,7 +409,6 @@ files:
|
|
396
409
|
- lib/bookbinder/server_director.rb
|
397
410
|
- lib/bookbinder/sheller.rb
|
398
411
|
- lib/bookbinder/sieve.rb
|
399
|
-
- lib/bookbinder/sitemap_generator.rb
|
400
412
|
- lib/bookbinder/spider.rb
|
401
413
|
- lib/bookbinder/stabilimentum.rb
|
402
414
|
- lib/bookbinder/streams/colorized_stream.rb
|
@@ -435,9 +447,9 @@ require_paths:
|
|
435
447
|
- lib
|
436
448
|
required_ruby_version: !ruby/object:Gem::Requirement
|
437
449
|
requirements:
|
438
|
-
- - "
|
450
|
+
- - "~>"
|
439
451
|
- !ruby/object:Gem::Version
|
440
|
-
version:
|
452
|
+
version: 2.3.0
|
441
453
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
442
454
|
requirements:
|
443
455
|
- - ">="
|
@@ -445,7 +457,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
445
457
|
version: '0'
|
446
458
|
requirements: []
|
447
459
|
rubyforge_project:
|
448
|
-
rubygems_version: 2.
|
460
|
+
rubygems_version: 2.5.1
|
449
461
|
signing_key:
|
450
462
|
specification_version: 4
|
451
463
|
summary: Markdown to Rackup application documentation generator
|
@@ -1,37 +0,0 @@
|
|
1
|
-
module Bookbinder
|
2
|
-
module Config
|
3
|
-
module Checkers
|
4
|
-
class SubnavTopicsChecker
|
5
|
-
MissingRequiredKeyError = Class.new(RuntimeError)
|
6
|
-
|
7
|
-
def check(config)
|
8
|
-
@config = config
|
9
|
-
|
10
|
-
if invalid_products.any?
|
11
|
-
MissingRequiredKeyError.new("Your config.yml is missing required key(s) for subnav_topics in product id(s) #{invalid_product_ids}. Required keys are #{required_topic_keys.join(", ")}.")
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
attr_reader :config
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def invalid_products
|
20
|
-
config.products.select { |product_config| invalid_topics(product_config.subnav_topics).any? }
|
21
|
-
end
|
22
|
-
|
23
|
-
def invalid_topics(topics)
|
24
|
-
topics.map {|topic| topic unless topic.valid? }
|
25
|
-
end
|
26
|
-
|
27
|
-
def invalid_product_ids
|
28
|
-
invalid_products.map(&:id).join(', ')
|
29
|
-
end
|
30
|
-
|
31
|
-
def required_topic_keys
|
32
|
-
Bookbinder::Config::TopicConfig::CONFIG_REQUIRED_KEYS
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
module Bookbinder
|
2
|
-
module Config
|
3
|
-
class TopicConfig
|
4
|
-
def initialize(config)
|
5
|
-
@config = config
|
6
|
-
end
|
7
|
-
|
8
|
-
def title
|
9
|
-
config['title']
|
10
|
-
end
|
11
|
-
|
12
|
-
def toc_path
|
13
|
-
Pathname(config['toc_path'])
|
14
|
-
end
|
15
|
-
|
16
|
-
def toc_nav_name
|
17
|
-
config['toc_nav_name'] || title
|
18
|
-
end
|
19
|
-
|
20
|
-
def valid?
|
21
|
-
(CONFIG_REQUIRED_KEYS - config.keys).empty?
|
22
|
-
end
|
23
|
-
|
24
|
-
CONFIG_REQUIRED_KEYS = %w(title toc_path)
|
25
|
-
|
26
|
-
attr_reader :config
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
require 'nokogiri'
|
2
|
-
|
3
|
-
module Bookbinder
|
4
|
-
class SitemapGenerator
|
5
|
-
def generate(links)
|
6
|
-
Nokogiri::XML::Builder.new(encoding: 'UTF-8') { |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
|
-
}.to_xml
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|