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,73 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module Bookbinder
|
4
|
+
class PdfGenerator
|
5
|
+
class MissingSource < StandardError
|
6
|
+
def initialize(required_file)
|
7
|
+
super "Could not find file #{required_file}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(logger)
|
12
|
+
@logger = logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate(sources, target, header, left_footer=nil)
|
16
|
+
sources.each { |s| check_destination_exists s }
|
17
|
+
check_destination_exists header
|
18
|
+
|
19
|
+
left_footer ||= " © Copyright 2013-#{Time.now.year}, Pivotal"
|
20
|
+
|
21
|
+
toc_xslt_path = File.expand_path('../../../toc.xslt', __FILE__)
|
22
|
+
|
23
|
+
command = <<-CMD
|
24
|
+
wkhtmltopdf \
|
25
|
+
--disable-external-links \
|
26
|
+
--disable-javascript \
|
27
|
+
--load-error-handling ignore \
|
28
|
+
--margin-top 26mm \
|
29
|
+
--margin-bottom 13mm \
|
30
|
+
--header-spacing 10 \
|
31
|
+
--header-html #{header} \
|
32
|
+
--footer-spacing 5 \
|
33
|
+
--footer-font-size 10 \
|
34
|
+
--footer-left '#{left_footer}' \
|
35
|
+
--footer-center '[page] of [toPage]' \
|
36
|
+
--print-media-type \
|
37
|
+
toc --xsl-style-sheet #{toc_xslt_path} \
|
38
|
+
#{sources.join(' ')} \
|
39
|
+
#{target}
|
40
|
+
CMD
|
41
|
+
|
42
|
+
`#{command}`
|
43
|
+
|
44
|
+
raise "'wkhtmltopdf' appears to have failed" unless $?.success? && File.exist?(target)
|
45
|
+
|
46
|
+
@logger.log "\nYour PDF file was generated to #{target.green}"
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def check_file_exists(required_file)
|
51
|
+
unless File.exist? required_file
|
52
|
+
@logger.error "\nPDF Generation failed (could not find file)!"
|
53
|
+
raise MissingSource, required_file
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def check_destination_exists(url)
|
58
|
+
uri = URI(url)
|
59
|
+
if uri.class == URI::Generic
|
60
|
+
check_file_exists url
|
61
|
+
elsif uri.class == URI::HTTP
|
62
|
+
check_url_exists url
|
63
|
+
else
|
64
|
+
@logger.error "Malformed destination provided for PDF generation source: #{url}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def check_url_exists(url)
|
69
|
+
res = Net::HTTP.get_response(URI(url))
|
70
|
+
raise MissingSource, url if res.code == '404'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'middleman-syntax'
|
2
|
+
require_relative 'bookbinder_logger'
|
3
|
+
require_relative 'directory_helpers'
|
4
|
+
require_relative 'section'
|
5
|
+
require_relative 'server_director'
|
6
|
+
|
7
|
+
module Bookbinder
|
8
|
+
class Publisher
|
9
|
+
include DirectoryHelperMethods
|
10
|
+
|
11
|
+
def initialize(logger, spider, static_site_generator)
|
12
|
+
@gem_root = File.expand_path('../../../', __FILE__)
|
13
|
+
@logger = logger
|
14
|
+
@spider = spider
|
15
|
+
@static_site_generator = static_site_generator
|
16
|
+
end
|
17
|
+
|
18
|
+
def publish(cli_options, output_paths, publish_config, git_accessor)
|
19
|
+
intermediate_directory = output_paths.fetch(:output_dir)
|
20
|
+
final_app_dir = output_paths.fetch(:final_app_dir)
|
21
|
+
master_middleman_dir = output_paths.fetch(:master_middleman_dir)
|
22
|
+
master_dir = File.join intermediate_directory, 'master_middleman'
|
23
|
+
workspace_dir = File.join master_dir, 'source'
|
24
|
+
build_directory = File.join master_dir, 'build/.'
|
25
|
+
public_directory = File.join final_app_dir, 'public'
|
26
|
+
|
27
|
+
@versions = publish_config.fetch(:versions, [])
|
28
|
+
@book_repo = publish_config[:book_repo]
|
29
|
+
prepare_directories final_app_dir, intermediate_directory, workspace_dir, master_middleman_dir, master_dir, git_accessor
|
30
|
+
FileUtils.cp 'redirects.rb', final_app_dir if File.exists?('redirects.rb')
|
31
|
+
|
32
|
+
target_tag = cli_options[:target_tag]
|
33
|
+
sections = gather_sections(workspace_dir, publish_config, output_paths, target_tag, git_accessor)
|
34
|
+
book = Book.new(logger: @logger,
|
35
|
+
full_name: @book_repo,
|
36
|
+
sections: publish_config.fetch(:sections))
|
37
|
+
host_for_sitemap = publish_config.fetch(:host_for_sitemap)
|
38
|
+
|
39
|
+
generate_site(cli_options, output_paths, publish_config, master_dir, book, sections, build_directory, public_directory, git_accessor)
|
40
|
+
generate_sitemap(final_app_dir, host_for_sitemap, @spider)
|
41
|
+
|
42
|
+
@logger.log "Bookbinder bound your book into #{final_app_dir.to_s.green}"
|
43
|
+
|
44
|
+
!@spider.has_broken_links?
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def generate_sitemap(final_app_dir, host_for_sitemap, spider)
|
50
|
+
server_director = ServerDirector.new(@logger, directory: final_app_dir)
|
51
|
+
raise "Your public host must be a single String." unless host_for_sitemap.is_a?(String)
|
52
|
+
|
53
|
+
server_director.use_server { |port| spider.generate_sitemap host_for_sitemap, port }
|
54
|
+
end
|
55
|
+
|
56
|
+
def generate_site(cli_options, output_paths, publish_config, middleman_dir, book, sections, build_dir, public_dir, git_accessor)
|
57
|
+
@static_site_generator.run(middleman_dir,
|
58
|
+
publish_config.fetch(:template_variables, {}),
|
59
|
+
output_paths[:local_repo_dir],
|
60
|
+
cli_options[:verbose],
|
61
|
+
book,
|
62
|
+
sections,
|
63
|
+
publish_config[:host_for_sitemap],
|
64
|
+
publish_config[:archive_menu],
|
65
|
+
git_accessor
|
66
|
+
)
|
67
|
+
FileUtils.cp_r build_dir, public_dir
|
68
|
+
end
|
69
|
+
|
70
|
+
def gather_sections(workspace, publish_config, output_paths, target_tag, git_accessor)
|
71
|
+
section_data = publish_config.fetch(:sections)
|
72
|
+
section_data.map do |attributes|
|
73
|
+
section = Section.get_instance(@logger,
|
74
|
+
section_hash: attributes,
|
75
|
+
destination_dir: workspace,
|
76
|
+
local_repo_dir: output_paths[:local_repo_dir],
|
77
|
+
target_tag: target_tag,
|
78
|
+
git_accessor: git_accessor)
|
79
|
+
section
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def prepare_directories(final_app, middleman_scratch_space, middleman_source, master_middleman_dir, middleman_dir, git_accessor)
|
84
|
+
forget_sections(middleman_scratch_space)
|
85
|
+
FileUtils.rm_rf File.join final_app, '.'
|
86
|
+
FileUtils.mkdir_p middleman_scratch_space
|
87
|
+
FileUtils.mkdir_p File.join final_app, 'public'
|
88
|
+
FileUtils.mkdir_p middleman_source
|
89
|
+
|
90
|
+
copy_directory_from_gem 'template_app', final_app
|
91
|
+
copy_directory_from_gem 'master_middleman', middleman_dir
|
92
|
+
FileUtils.cp_r File.join(master_middleman_dir, '.'), middleman_dir
|
93
|
+
|
94
|
+
copy_version_master_middleman(middleman_source, git_accessor)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Copy the index file from each version into the version's directory. Because version
|
98
|
+
# subdirectories are sections, this is the only way they get content from their master
|
99
|
+
# middleman directory.
|
100
|
+
def copy_version_master_middleman(dest_dir, git_accessor)
|
101
|
+
@versions.each do |version|
|
102
|
+
Dir.mktmpdir(version) do |tmpdir|
|
103
|
+
book = Book.from_remote(logger: @logger, full_name: @book_repo,
|
104
|
+
destination_dir: tmpdir, ref: version, git_accessor: git_accessor)
|
105
|
+
index_source_dir = File.join(tmpdir, book.directory, 'master_middleman', source_dir_name)
|
106
|
+
index_dest_dir = File.join(dest_dir, version)
|
107
|
+
FileUtils.mkdir_p(index_dest_dir)
|
108
|
+
|
109
|
+
Dir.glob(File.join(index_source_dir, 'index.*')) do |f|
|
110
|
+
FileUtils.cp(File.expand_path(f), index_dest_dir)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def forget_sections(middleman_scratch)
|
117
|
+
Section.store.clear
|
118
|
+
FileUtils.rm_rf File.join middleman_scratch, '.'
|
119
|
+
end
|
120
|
+
|
121
|
+
def copy_directory_from_gem(dir, output_dir)
|
122
|
+
FileUtils.cp_r File.join(@gem_root, "#{dir}/."), output_dir
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative 'app_fetcher'
|
2
|
+
|
3
|
+
module Bookbinder
|
4
|
+
class Pusher
|
5
|
+
def initialize(cf_cli, app_fetcher)
|
6
|
+
@cf_cli = cf_cli
|
7
|
+
@app_fetcher = app_fetcher
|
8
|
+
end
|
9
|
+
|
10
|
+
def push(app_dir)
|
11
|
+
Dir.chdir(app_dir) do
|
12
|
+
cf_cli.login
|
13
|
+
|
14
|
+
old_app = app_fetcher.fetch_current_app
|
15
|
+
|
16
|
+
if old_app
|
17
|
+
new_app = old_app.with_flipped_name
|
18
|
+
cf_cli.start(new_app)
|
19
|
+
cf_cli.push(new_app)
|
20
|
+
cf_cli.map_routes(new_app)
|
21
|
+
cf_cli.takedown_old_target_app(old_app)
|
22
|
+
else
|
23
|
+
new_app = cf_cli.new_app
|
24
|
+
cf_cli.push(new_app)
|
25
|
+
cf_cli.map_routes(new_app)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :cf_cli, :app_fetcher
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Bookbinder
|
5
|
+
class RemoteYamlCredentialProvider
|
6
|
+
def initialize(logger, repository, git_accessor = Git)
|
7
|
+
@logger = logger
|
8
|
+
@repository = repository
|
9
|
+
@git_accessor = git_accessor
|
10
|
+
end
|
11
|
+
|
12
|
+
def credentials
|
13
|
+
@logger.log "Processing #{@repository.full_name.cyan}"
|
14
|
+
Dir.mktmpdir do |destination_dir|
|
15
|
+
@repository.copy_from_remote(destination_dir, @git_accessor)
|
16
|
+
cred_file_yaml = File.join(destination_dir, @repository.short_name, 'credentials.yml')
|
17
|
+
YAML.load_file(cred_file_yaml)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'bookbinder/directory_helpers'
|
2
|
+
|
3
|
+
module Bookbinder
|
4
|
+
class Section
|
5
|
+
|
6
|
+
def self.store
|
7
|
+
@@store ||= {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.get_instance(logger,
|
11
|
+
section_hash: {},
|
12
|
+
local_repo_dir: nil,
|
13
|
+
destination_dir: Dir.mktmpdir,
|
14
|
+
target_tag: nil,
|
15
|
+
git_accessor: Git)
|
16
|
+
@git_accessor = git_accessor
|
17
|
+
store.fetch([section_hash, local_repo_dir]) { acquire(logger, section_hash, local_repo_dir, destination_dir, target_tag, git_accessor) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(logger, repository, subnav_template)
|
21
|
+
@logger = logger
|
22
|
+
@subnav_template = subnav_template
|
23
|
+
@repository = repository
|
24
|
+
@git_accessor = Git
|
25
|
+
end
|
26
|
+
|
27
|
+
def subnav_template
|
28
|
+
@subnav_template.gsub(/^_/, '').gsub(/\.erb$/, '') if @subnav_template
|
29
|
+
end
|
30
|
+
|
31
|
+
def directory
|
32
|
+
@repository.directory
|
33
|
+
end
|
34
|
+
|
35
|
+
def full_name
|
36
|
+
@repository.full_name
|
37
|
+
end
|
38
|
+
|
39
|
+
def copied?
|
40
|
+
@repository.copied?
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_modification_date_for(file: nil, full_path: nil)
|
44
|
+
unless @repository.has_git_object?
|
45
|
+
begin
|
46
|
+
git_base_object = @git_accessor.open(@repository.path_to_local_repo)
|
47
|
+
rescue => e
|
48
|
+
raise "Invalid git repository! Cannot get modification date for section: #{@repository.path_to_local_repo}."
|
49
|
+
end
|
50
|
+
end
|
51
|
+
@repository.get_modification_date_for(file: file, git: git_base_object)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def self.acquire(logger, section_hash, local_repo_dir, destination, target_tag, git_accessor)
|
57
|
+
repository = section_hash['repository']
|
58
|
+
raise "section repository '#{repository}' is not a hash" unless repository.is_a?(Hash)
|
59
|
+
raise "section repository '#{repository}' missing name key" unless repository['name']
|
60
|
+
logger.log "Gathering #{repository['name'].cyan}"
|
61
|
+
|
62
|
+
repository = build_repository(logger, destination, local_repo_dir, section_hash, target_tag, git_accessor)
|
63
|
+
section = new(logger, repository, section_hash['subnav_template'])
|
64
|
+
|
65
|
+
store[[section_hash, local_repo_dir]] = section
|
66
|
+
end
|
67
|
+
private_class_method :acquire
|
68
|
+
|
69
|
+
def self.build_repository(logger, destination, local_repo_dir, repo_hash, target_tag, git_accessor)
|
70
|
+
if local_repo_dir
|
71
|
+
GitHubRepository.build_from_local(logger, repo_hash, local_repo_dir, destination)
|
72
|
+
else
|
73
|
+
GitHubRepository.build_from_remote(logger, repo_hash, destination, target_tag, git_accessor)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
private_class_method :build_repository
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'popen4'
|
2
|
+
|
3
|
+
module Bookbinder
|
4
|
+
class ServerDirector
|
5
|
+
def initialize(logger, directory: nil, port: 41722)
|
6
|
+
@logger = logger
|
7
|
+
@directory = directory
|
8
|
+
@port = port
|
9
|
+
end
|
10
|
+
|
11
|
+
def use_server
|
12
|
+
Dir.chdir(@directory) do
|
13
|
+
POpen4::popen4("puma -p #{@port}") do |stdout, stderr, stdin, pid|
|
14
|
+
begin
|
15
|
+
wait_for_server(stdout)
|
16
|
+
consume_stream_in_separate_thread(stdout)
|
17
|
+
consume_stream_in_separate_thread(stderr)
|
18
|
+
yield @port
|
19
|
+
ensure
|
20
|
+
stop_server(pid)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def wait_for_server(io)
|
29
|
+
Kernel.sleep(1)
|
30
|
+
begin
|
31
|
+
line = io.gets
|
32
|
+
raise 'Puma could not start' if line.nil?
|
33
|
+
|
34
|
+
@logger.log "Vienna says, #{line}"
|
35
|
+
end until line.include?('Listening on')
|
36
|
+
|
37
|
+
@logger.log 'Vienna is lovely this time of year.'
|
38
|
+
end
|
39
|
+
|
40
|
+
def stop_server(pid)
|
41
|
+
Process.kill 'KILL', pid
|
42
|
+
end
|
43
|
+
|
44
|
+
# avoids deadlocks by ensuring rack doesn't hang waiting to write to stderr
|
45
|
+
def consume_stream_in_separate_thread(stream)
|
46
|
+
Thread.new do
|
47
|
+
s = nil
|
48
|
+
while stream.read(1024, s)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Bookbinder
|
4
|
+
module ShellOut
|
5
|
+
def shell_out(command, failure_okay = false)
|
6
|
+
Open3.popen3(command) do |input, stdout, stderr, wait_thr|
|
7
|
+
command_failed = (wait_thr.value != 0)
|
8
|
+
announce_failure(failure_okay, stderr, stdout) if command_failed
|
9
|
+
stdout.read
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def announce_failure(failure_okay, stderr, stdout)
|
14
|
+
contents_of_stderr = stderr.read
|
15
|
+
error_message = contents_of_stderr.empty? ? stdout.read : contents_of_stderr
|
16
|
+
raise "\n#{error_message}" unless failure_okay
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Bookbinder
|
2
|
+
class Sieve
|
3
|
+
def initialize(domain: ->(){ raise 'You must supply a domain parameter.' }.call)
|
4
|
+
@unverified_fragments_by_url = {}
|
5
|
+
@domain = domain
|
6
|
+
end
|
7
|
+
|
8
|
+
def links_from(page, is_first_pass)
|
9
|
+
if page.not_found?
|
10
|
+
working = []
|
11
|
+
broken = [Spider.prepend_location(page.referer, page.url)]
|
12
|
+
else
|
13
|
+
working = [page.url.to_s]
|
14
|
+
broken = broken_fragments_targeting(page, is_first_pass)
|
15
|
+
store_unverified_fragments_from(page) if is_first_pass
|
16
|
+
end
|
17
|
+
|
18
|
+
return broken, working
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def store_unverified_fragments_from(page)
|
24
|
+
@unverified_fragments_by_url.merge! fragments_targeting_other_pages_from page
|
25
|
+
end
|
26
|
+
|
27
|
+
def broken_fragments_targeting(page, first_pass)
|
28
|
+
first_pass ? local_fragments_missing_from(page) : remote_fragments_missing_from(page)
|
29
|
+
end
|
30
|
+
|
31
|
+
def local_fragments_missing_from(page)
|
32
|
+
local_fragments = page.fragment_identifiers targeting_locally: true
|
33
|
+
local_fragments.reject { |uri| page.has_target_for?(uri) }.map { |uri| Spider.prepend_location(page.url, uri) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def remote_fragments_missing_from(page)
|
37
|
+
@unverified_fragments_by_url.fetch(page.url, []).reject { |localized_identifier| page.has_target_for? URI(strip_location(localized_identifier)) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def fragments_targeting_other_pages_from(page)
|
41
|
+
uris_with_fragments = page.fragment_identifiers(targeting_locally: false)
|
42
|
+
uris_with_fragments.reduce({}) { |dict, uri| merge_uris_under_targets(dict, page, uri) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def merge_uris_under_targets(dict, page, uri)
|
46
|
+
target_url = URI::join @domain, uri.path
|
47
|
+
localized_identifier = Spider.prepend_location(page.url, "##{uri.fragment}")
|
48
|
+
|
49
|
+
if dict.has_key? target_url
|
50
|
+
dict[target_url] << localized_identifier
|
51
|
+
else
|
52
|
+
dict[target_url] = [localized_identifier]
|
53
|
+
end
|
54
|
+
|
55
|
+
dict
|
56
|
+
end
|
57
|
+
|
58
|
+
def strip_location(id)
|
59
|
+
id.split('=> ').last
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|