├── .gitignore ├── .travis.yml ├── Dockerfile ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.adoc ├── Rakefile ├── ascii_binder.gemspec ├── bin ├── ascii_binder └── asciibinder ├── features ├── command_help.feature ├── command_version.feature ├── repo_build.feature ├── repo_clean.feature ├── repo_clone.feature ├── repo_create.feature ├── repo_package.feature ├── step_definitions │ └── steps.rb └── support │ ├── _clone_distro_map.yml │ ├── _invalid_alias_topic_map.yml │ ├── _invalid_distro_map.yml │ ├── env.rb │ └── test_distro │ ├── .gitignore │ ├── _distro_map.yml │ ├── _images │ ├── asciibinder-logo-horizontal.png │ ├── asciibinder_web_logo.svg │ ├── book_pages_bg.jpg │ ├── favicon.ico │ └── favicon32x32.png │ ├── _javascripts │ └── .gitkeep │ ├── _stylesheets │ └── asciibinder.css │ ├── _templates │ ├── _css.html.erb │ ├── _nav.html.erb │ └── page.html.erb │ ├── _topic_map.yml │ ├── index-main.html │ ├── index-test.html │ ├── main_only_topic_group │ └── index.adoc │ ├── test_only_topic_group │ └── index.adoc │ └── welcome │ ├── aliased.adoc │ ├── index.adoc │ └── subtopics │ ├── index.adoc │ ├── main_only_topic.adoc │ ├── test_only_topic.adoc │ └── wildcard_all.adoc ├── lib ├── ascii_binder.rb └── ascii_binder │ ├── distro.rb │ ├── distro_branch.rb │ ├── distro_map.rb │ ├── engine.rb │ ├── helpers.rb │ ├── site.rb │ ├── site_info.rb │ ├── site_map.rb │ ├── tasks │ ├── guards.rb │ └── tasks.rb │ ├── template_renderer.rb │ ├── topic_entity.rb │ ├── topic_map.rb │ └── version.rb └── templates ├── .gitignore ├── LICENSE.txt ├── README.adoc ├── _distro_map.yml ├── _images ├── asciibinder-logo-horizontal.png ├── asciibinder_web_logo.svg ├── book_pages_bg.jpg ├── favicon.ico └── favicon32x32.png ├── _javascripts ├── .gitkeep └── bootstrap-offcanvas.js ├── _stylesheets └── asciibinder.css ├── _templates ├── _breadcrumb.html.erb ├── _css.html.erb ├── _nav.html.erb ├── _title.html.erb └── page.html.erb ├── _topic_map.yml ├── index-main.html └── welcome └── index.adoc /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | *.gem 16 | *.swp 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | cache: bundler 4 | rvm: 5 | - 2.3.3 6 | after_install: gem list 7 | script: 8 | - git config --global user.name "Travis Test" 9 | - git config --global user.email "travis@mailinator.com" 10 | - bundle exec cucumber 11 | notifications: 12 | irc: 13 | channels: 14 | - "irc.freenode.org#asciibinder" 15 | on_success: change 16 | on_failure: always 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos/ruby-26-centos7 2 | 3 | RUN scl enable rh-ruby26 -- gem install listen -v 3.0.8 4 | RUN scl enable rh-ruby26 -- gem install ascii_binder 5 | USER root 6 | RUN yum install -y java-1.7.0-openjdk && \ 7 | yum clean all 8 | 9 | LABEL url="http://www.asciibinder.org" \ 10 | summary="a documentation system built on Asciidoctor" \ 11 | description="AsciiBinder is for documenting versioned, interrelated projects. Run this container image from the documentation repository, which is mounted into the container. Note: Generated files will be owned by root." \ 12 | RUN="docker run -it --rm \ 13 | -v `pwd`:/docs:z \ 14 | IMAGE" 15 | 16 | ENV JAVA_HOME=/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.261-2.6.22.2.el7_8.x86_64/jre/ 17 | ENV LANG=en_US.UTF-8 18 | WORKDIR /docs 19 | RUN git config --global --add safe.directory /docs 20 | CMD asciibinder package 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | require 'ascii_binder' 2 | gem_dir = Gem::Specification.find_by_name("ascii_binder").lib_dirs_glob 3 | instance_eval(File.read(File.join(gem_dir, 'ascii_binder/tasks/guards.rb'))) 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Red Hat, Inc. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = AsciiBinder 2 | 3 | image:https://badge.fury.io/rb/ascii_binder.svg["Gem Version", link="https://badge.fury.io/rb/ascii_binder"] 4 | 5 | AsciiBinder is an AsciiDoc-based system for authoring and publishing closely related documentation sets from a single source. 6 | 7 | This product is no longer actively maintained. 8 | 9 | == Learn More 10 | 11 | * Take the https://rubygems.org/gems/ascii_binder[ascii_binder Ruby Gem] for a spin. 12 | 13 | The AsciiBinder system was initially developed for https://github.com/openshift/openshift-docs[OpenShift documentation], but has been revised to work for documenting a wide variety of complex, multi-versioned software projects. 14 | 15 | == Contributing 16 | 17 | We are using the https://github.com/redhataccess/ascii_binder/issues[Issues] page to track bugs and feature ideas on the code, so have a look and feel free to ask questions there. 18 | 19 | == License 20 | 21 | The gem is available as open source under the terms of the http://opensource.org/licenses/MIT[MIT License]. 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /ascii_binder.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'ascii_binder/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "ascii_binder" 8 | spec.version = AsciiBinder::VERSION 9 | spec.authors = ["N. Harrison Ripps", "Jason Frey", "Carlos Munoz", "Brian Exelbierd", "Vikram Goyal"] 10 | spec.email = ["nhr@redhat.com", "jfrey@redhat.com", "chavo16@hotmail.com", "bex@pobox.com", "vigoyal@redhat.com"] 11 | spec.summary = %q{AsciiBinder is an AsciiDoc-based system for authoring and publishing closely related documentation sets from a single source.} 12 | spec.description = spec.summary 13 | spec.homepage = "https://github.com/redhataccess/ascii_binder" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler" 22 | spec.add_development_dependency "cucumber", "~> 2.3.3" 23 | spec.add_development_dependency "diff_dirs", "~> 0.1.2" 24 | spec.add_dependency "rake", "~> 12.3.3" 25 | 26 | spec.add_dependency 'asciidoctor', '~> 2.0.10' 27 | spec.add_dependency 'asciidoctor-diagram', '~> 2.0.2' 28 | spec.add_dependency 'rouge', '~> 3.18.0' 29 | spec.add_dependency 'git' 30 | spec.add_dependency 'guard' 31 | spec.add_dependency 'guard-shell' 32 | spec.add_dependency 'guard-livereload' 33 | spec.add_dependency 'haml' 34 | spec.add_dependency 'json' 35 | spec.add_dependency 'sitemap_generator', '~> 6.0.1' 36 | spec.add_dependency 'trollop', '~> 2.1.2' 37 | spec.add_dependency "yajl-ruby", ">= 1.4.2" 38 | spec.add_dependency 'tilt' 39 | 40 | end 41 | -------------------------------------------------------------------------------- /bin/ascii_binder: -------------------------------------------------------------------------------- 1 | asciibinder -------------------------------------------------------------------------------- /bin/asciibinder: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'ascii_binder/distro_map' 4 | require 'ascii_binder/engine' 5 | require 'ascii_binder/helpers' 6 | require 'ascii_binder/version' 7 | require 'pathname' 8 | require 'trollop' 9 | 10 | include AsciiBinder::Engine 11 | include AsciiBinder::Helpers 12 | 13 | def call_generate(branch_group, distro, page) 14 | if page == '' 15 | page = nil 16 | end 17 | begin 18 | generate_docs(branch_group, distro, page) 19 | rescue => e 20 | message = "#{e.class.name}: #{e.message} at\n #{e.backtrace.join("\n ")}" 21 | Trollop::die "Could not generate docs:\n#{message}" 22 | end 23 | end 24 | 25 | def repo_check(docs_basedir) 26 | missing_files = false 27 | # These must all be present 28 | ['_distro_map.yml','_templates'].each do |file| 29 | unless File.exist?(File.join(docs_basedir, file)) 30 | missing_files = true 31 | end 32 | end 33 | # Either of these must be present 34 | # unless File.exist?(File.join("#{docs_basedir}/#{TOPIC_MAP_FOLDER}", '_topic_map.yml')) 35 | # missing_files = true 36 | # end 37 | if missing_files or not in_git_repo(docs_basedir) 38 | Trollop::die "The specified docs base directory '#{docs_basedir}' does not appear to be part of an AsciiBinder-compatible repo." 39 | end 40 | end 41 | 42 | def in_git_repo(dir) 43 | git_path = File.join(dir,'.git') 44 | return true if File.exist?(git_path) and File.directory?(git_path) 45 | return false if dir == '/' 46 | in_git_repo(File.expand_path('..',dir)) 47 | end 48 | 49 | SUB_COMMANDS = %w{help version build watch package clean create clone} 50 | Trollop::options do 51 | version AsciiBinder::VERSION 52 | banner <<-EOF 53 | Usage: 54 | #$0 55 | 56 | Commands: 57 | build (default action) 58 | Builds the HTML docs within the indicated docs base directory 59 | create 60 | Generates a new AsciiBinder repo at the indicated dir 61 | clone 62 | Clones an existing AsciiBinder repo to the local filesystem 63 | watch 64 | Starts Guard, which automatically regenerates changed HTML 65 | files on the working branch in the docs base directory dir 66 | package 67 | Builds and packages the static HTML for all of the sites 68 | defined in the _distro_config.yml file 69 | clean 70 | Remove _preview, _publish and _package dirs created by 71 | other AsciiBinder operations. 72 | 73 | Options: 74 | EOF 75 | stop_on SUB_COMMANDS 76 | end 77 | 78 | cmd = ARGV.shift 79 | docs_basedir = nil 80 | 81 | if cmd.nil? 82 | cmd = "build" 83 | elsif !SUB_COMMANDS.include?(cmd) 84 | if ARGV.empty? 85 | docs_basedir = Pathname.new(cmd) 86 | cmd = "build" 87 | else 88 | Trollop::die "'#{cmd}' is not a valid asciibinder subcommand. Legal values are '#{SUB_COMMANDS.join('\', \'')}'." 89 | end 90 | end 91 | 92 | cmd_opts = case cmd 93 | when "build" 94 | Trollop::options do 95 | banner <<-EOF 96 | Usage: 97 | #$0 build 98 | 99 | Description: 100 | This is the default behavior for the asciibinder utility. When run, 101 | AsciiBinder reads the _distro_config.yml file out of the working 102 | branch of the indicated docs base directory and based on that, proceeds 103 | to build the working branch version of the documentation for each distro. 104 | 105 | If you use the --all_branches flag, AsciiBinder behaves as described 106 | above, and then once the working branch version is built, AsciiBinder 107 | cycles through the other branches named in the _distro_config.yml file 108 | until all of the permutations have been built. 109 | 110 | If you want to limit the scope of the build work for faster builds, 111 | you have two targeted options: 112 | 113 | --distro= - Only builds the specified distro and branches 114 | associated with this distro. 115 | 116 | --page= - Only builds the specified page for all distros. 117 | 118 | Note that the format for the "--page" option is: 119 | 120 | : 121 | 122 | or for subtopics: 123 | 124 | /: 125 | 126 | However, if you want to use the --page option extensively, then be 127 | aware of the `asciibinder watch` function, which does this for you 128 | automatically as you change any .adoc files in your working branch. 129 | 130 | Options: 131 | EOF 132 | opt :all_branches, "Instead of building only the current working branch, build all branches", :default => false 133 | opt :distro, "Instead of building all distros, build branches only for the specified distro.", :default => '' 134 | opt :page, "Build only the specified page for all distros and only the current working branch.", :default => '' 135 | opt :log_level, "Set the logging output level for this operation.", :default => 'warn' 136 | opt :toc_depth, "Maximum depth of topics allowed. Use 0 for infinite depth.", :default => 3 137 | conflicts :distro, :page 138 | end 139 | when "create" 140 | Trollop::options do 141 | banner <<-EOF 142 | Usage: 143 | #$0 create 144 | 145 | Description: 146 | Creates a new, bare AsciiBinder repo in the specified directory. 147 | EOF 148 | end 149 | when "clone" 150 | Trollop::options do 151 | banner <<-EOF 152 | Usage: 153 | #$0 clone 154 | 155 | Description: 156 | Clones an existing AsciiBinder repo to the current directory. 157 | Under the default behavior, AsciiBinder will attempt to set up 158 | tracking branches based on the contents of _distro_map.yml, 159 | but this can be suppressed (see Options). 160 | 161 | Options: 162 | EOF 163 | opt :branches, "Create tracking branches after cloning.", :default => true 164 | opt :dir, "Specify the pathname of the local directory for cloning.", :default => '' 165 | opt :log_level, "Set the logging output level for this operation.", :default => 'warn' 166 | end 167 | when "watch" 168 | Trollop::options do 169 | banner <<-EOF 170 | Usage: 171 | #$0 watch 172 | 173 | Description: 174 | In watch mode, AsciiBinder starts a Guard process in the foreground. 175 | This process watches the docs_basedir for changes to the AsciiDoc (.adoc) 176 | files. When a change occurs, AsciiBinder regenerates the specific 177 | HTML output of the file that was changed, for the working branch only. 178 | 179 | This is the equivalent of running: 180 | 181 | $ asciibinder build --page=':' 182 | 183 | ...except that the Guardfile automatically detects and runs this as 184 | you work. 185 | 186 | This is meant to be used in conjunction with a web browser that is viewing the 187 | output HTML page. Every time you save a new version of the .adoc file, you can 188 | manually refresh your page to view the newly-generated HTML. 189 | EOF 190 | opt :log_level, "Set the logging output level for this operation.", :default => 'warn' 191 | end 192 | when "package" 193 | Trollop::options do 194 | banner <<-EOF 195 | Usage: 196 | #$0 package 197 | 198 | Description: 199 | Publish mode is similar to 'build' mode, but once all of the branches' of 200 | HTML are generated, 'publish' goes on to organize the branch / distro 201 | combinations that are described in _distro_config.yml into their "site" 202 | layouts. As a final step, the site layouts are tarred and gzipped for 203 | easy placement onto a production web server. 204 | 205 | Options: 206 | EOF 207 | opt :site, "Instead of packaging every docs site, package the specified site only.", :default => '' 208 | opt :log_level, "Set the logging output level for this operation.", :default => 'warn' 209 | opt :toc_depth, "Maximum depth of topics allowed. Use 0 for infinite depth.", :default => 3 210 | end 211 | when "help" 212 | Trollop::educate 213 | when "version" 214 | puts AsciiBinder::VERSION 215 | exit 0 216 | end 217 | 218 | if (not docs_basedir.nil? and not ARGV.empty?) or (docs_basedir.nil? and ARGV.length > 1) 219 | Trollop::die "Too many arguments provided to ascii_binder: '#{ARGV.join(' ')}'. Exiting." 220 | elsif docs_basedir.nil? 221 | if ARGV.length == 1 222 | if cmd == 'clone' 223 | cmd_opts[:giturl] = ARGV.shift 224 | if cmd_opts[:dir] != '' 225 | docs_basedir = Pathname.new(cmd_opts[:dir]) 226 | else 227 | docs_basedir = Pathname.new(File.join(Pathname.pwd, cmd_opts[:giturl].split('/')[-1].split('.')[0])) 228 | end 229 | else 230 | docs_basedir = Pathname.new(ARGV.shift) 231 | end 232 | else 233 | if cmd != 'create' 234 | if cmd == 'clone' 235 | Trollop::die "Provide a git URL to clone from." 236 | else 237 | docs_basedir = Pathname.pwd 238 | end 239 | else 240 | Trollop::die "Specify a name for the new repo directory." 241 | end 242 | end 243 | end 244 | 245 | # Validate the docs_basedir path 246 | if cmd == 'create' or cmd == 'clone' 247 | if docs_basedir.exist? 248 | Trollop::die "The specified new repo directory '#{docs_basedir}' already exists." 249 | end 250 | else 251 | if !docs_basedir.exist? 252 | Trollop::die "The specified docs directory '#{docs_basedir}' does not exist." 253 | elsif !docs_basedir.directory? 254 | Trollop::die "The specified docs directory path '#{docs_basedir}' is not a directory." 255 | elsif !docs_basedir.readable? 256 | Trollop::die "The specified docs directory '#{docs_basedir}' is not readable." 257 | elsif !docs_basedir.writable? 258 | Trollop::die "The specified docs directory '#{docs_basedir}' cannot be written to." 259 | else 260 | repo_check(docs_basedir) 261 | end 262 | end 263 | 264 | # Set the repo root 265 | set_docs_root_dir(File.expand_path(docs_basedir)) 266 | 267 | # Set the log level 268 | user_log_level = :warn 269 | unless cmd_opts.nil? or cmd_opts[:log_level].nil? 270 | user_log_level = cmd_opts[:log_level].to_sym 271 | unless log_levels.has_key?(user_log_level) 272 | Trollop::die "log_level value '#{cmd_opts[:log_level]}' is not recognized. Legal values are " + log_levels.keys.map{ |lvl| "'#{lvl.to_s}'" }.join(', ') 273 | end 274 | end 275 | set_log_level(user_log_level) 276 | 277 | # Set the depth level 278 | user_depth = 3 279 | unless cmd_opts.nil? or cmd_opts[:toc_depth].nil? 280 | user_depth = cmd_opts[:toc_depth].to_i 281 | end 282 | set_depth(user_depth) 283 | 284 | 285 | # Cloning? Time to try it. 286 | if cmd == 'clone' 287 | puts "Cloning #{cmd_opts[:giturl]} to #{docs_basedir}" 288 | system("git clone #{cmd_opts[:giturl]} #{docs_basedir}") 289 | Trollop::die "The git URL could not be cloned: #{err}" if $?.exitstatus != 0 290 | 291 | # Make sure this cloned repo is legit. 292 | repo_check(docs_basedir) 293 | 294 | if cmd_opts[:branches] 295 | cloned_map = AsciiBinder::DistroMap.new(File.join(docs_basedir,DISTRO_MAP_FILENAME)) 296 | unless cloned_map.is_valid? 297 | error_info = cloned_map.errors.join("\n") 298 | Trollop::die "The distro map in the newly cloned repo is invalid, with the following errors:\n#{error_info}" 299 | end 300 | Dir.chdir(docs_basedir) 301 | puts "Tracking branch setup:" 302 | cloned_map.distro_branches.each do |doc_branch| 303 | next if doc_branch == 'main' 304 | puts "- #{doc_branch}" 305 | system("git branch #{doc_branch} origin/#{doc_branch}") 306 | end 307 | else 308 | puts "- Skipping tracking branch setup" 309 | end 310 | 311 | # Done and done. 312 | puts "Cloning complete." 313 | exit 314 | end 315 | 316 | # Change to the repo dir. This is necessary in order for 317 | # AsciiDoctor to work properly. 318 | if cmd != 'create' 319 | Dir.chdir docs_basedir 320 | end 321 | 322 | # Do the things with the stuff 323 | case cmd 324 | when "build" 325 | branch_group = cmd_opts[:all_branches] ? :all : :working_only 326 | build_distro = cmd_opts[:distro] || '' 327 | refresh_page = cmd_opts[:page] || nil 328 | call_generate(branch_group,build_distro,refresh_page) 329 | when "package" 330 | clean_up 331 | package_site = cmd_opts[:site] || '' 332 | branch_group = package_site == '' ? :publish : "publish_#{package_site}".to_sym 333 | call_generate(branch_group,'',nil) 334 | package_docs(package_site) 335 | when "watch" 336 | if !dir_empty?(preview_dir) 337 | guardfile_path = File.join(Gem::Specification.find_by_name("ascii_binder").full_gem_path, 'Guardfile') 338 | exec("guard -G #{guardfile_path}") 339 | else 340 | Trollop::die "Run 'asciibinder build' at least once before running 'asciibinder watch'." 341 | end 342 | when "clean" 343 | clean_up 344 | puts "Cleaned up #{docs_basedir}." 345 | when "create" 346 | create_new_repo 347 | puts "Created new repo in #{docs_basedir}." 348 | end 349 | 350 | exit 351 | -------------------------------------------------------------------------------- /features/command_help.feature: -------------------------------------------------------------------------------- 1 | Feature: asciibinder help 2 | 3 | Displays help information for the asciibinder utility 4 | 5 | Scenario: A user wants to see help information for the utility 6 | Given a nonexistent repo directory 7 | When the user runs `asciibinder help` on that repo directory 8 | Then the program displays help information 9 | -------------------------------------------------------------------------------- /features/command_version.feature: -------------------------------------------------------------------------------- 1 | Feature: asciibinder version 2 | 3 | This command returns the version of the installed utility 4 | 5 | Scenario: A user wants to display the version of the asciibinder utility 6 | Given a nonexistent repo directory 7 | When the user runs `asciibinder version` on that repo directory 8 | Then the program prints the current version of the utility 9 | -------------------------------------------------------------------------------- /features/repo_build.feature: -------------------------------------------------------------------------------- 1 | Feature: asciibinder build 2 | 3 | Causes the utility to process one or more distro/branch combinations, 4 | transforming AsciiDoc files to a unified docs set in HTML 5 | 6 | Scenario: A user wants to do any build in a repo with an invalid distro map 7 | Given an invalid AsciiBinder docs repo due to a malformed distro map 8 | When the user runs `asciibinder build` on that repo directory 9 | Then the program exits with a warning 10 | 11 | Scenario: A user wants to build all distros against the current repo branch 12 | Given a valid AsciiBinder docs repo with multiple distros 13 | When the user runs `asciibinder build` on that repo directory 14 | Then the program generates preview content for all distros in the current branch 15 | 16 | Scenario: A user wants to do a build in a repo with alias that points to a nonexistent topic 17 | Given a valid AsciiBinder docs repo with an invalid alias 18 | When the user runs `asciibinder build` on that repo directory 19 | Then the program exits with a warning 20 | 21 | Scenario: A user wants to build a single distro against the current repo branch 22 | Given a valid AsciiBinder docs repo with multiple distros 23 | When the user runs `asciibinder build --distro=distro_test` on that repo directory 24 | Then the program generates preview content for only the `distro_test` distro 25 | 26 | Scenario: A user wants to build all distros against all relevant branches 27 | Given a valid AsciiBinder docs repo with multiple distros 28 | When the user runs `asciibinder build --all-branches` on that repo directory 29 | Then the program generates preview content for all relevant distro/branch combos 30 | 31 | Scenario: A user wants to build a specific page in the current branch 32 | Given a valid AsciiBinder docs repo with multiple distros 33 | When the user runs `asciibinder build --page=welcome:index` on that repo directory 34 | Then the program generates preview content for the specified page in all distros 35 | 36 | Scenario: A user wants to build from a repo where the docs root is not the repo root 37 | Given a valid AsciiBinder docs repo where the docs root is not at the repo root 38 | When the user runs `asciibinder build` on that docs root directory 39 | Then the program generates preview content for all distros in the current branch 40 | -------------------------------------------------------------------------------- /features/repo_clean.feature: -------------------------------------------------------------------------------- 1 | Feature: asciibinder clean 2 | 3 | This command cleans up any files generated by AsciiBinder 4 | 5 | Scenario: A user wants to clean a docs repo of generated content 6 | Given a valid AsciiBinder docs repo 7 | And the docs repo contains generated content 8 | When the user runs `asciibinder clean` on that repo directory 9 | Then the generated content is removed 10 | 11 | Scenario: A user cleans a docs repo with no generated content 12 | Given a valid AsciiBinder docs repo 13 | And the docs repo contains no generated content 14 | When the user runs `asciibinder clean` on that repo directory 15 | Then the program exits without errors 16 | 17 | Scenario: A user cleans a directory that is not a docs repo 18 | Given an invalid AsciiBinder docs repo 19 | When the user runs `asciibinder clean` on that repo directory 20 | Then the program exits with a warning 21 | -------------------------------------------------------------------------------- /features/repo_clone.feature: -------------------------------------------------------------------------------- 1 | Feature: asciibinder clone 2 | 3 | This command clones a remote docs repo to a local directory and 4 | sets up tracking branches for each branch listed in the distro map 5 | 6 | Scenario: A user tries to clone a nonexistent remote repo 7 | Given a nonexistent remote repo 8 | And a nonexistent repo directory 9 | When the user runs `asciibinder clone` on that repo directory 10 | Then the program exits with a warning 11 | 12 | Scenario: A user tries to clone a remote repo into an existing directory 13 | Given an existing remote repo 14 | And an existing repo directory 15 | When the user runs `asciibinder clone` on that repo directory 16 | Then the program exits with a warning 17 | 18 | Scenario: A user tries to clone a remote repo into a nonexistent directory 19 | Given an existing remote repo 20 | And a nonexistent repo directory 21 | When the user runs `asciibinder clone` on that repo directory 22 | Then the program clones the remote repo into the local directory 23 | -------------------------------------------------------------------------------- /features/repo_create.feature: -------------------------------------------------------------------------------- 1 | Feature: asciibinder create 2 | 3 | This command creates a new base docs repo to be managed by AsciiBinder 4 | 5 | Scenario: A user tries to create a repo in an existing directory 6 | Given an existing repo directory 7 | When the user runs `asciibinder create` on that repo directory 8 | Then the program exits with a warning 9 | 10 | Scenario: A user tries to create a repo in a nonexistent directory 11 | Given a nonexistent repo directory 12 | When the user runs `asciibinder create` on that repo directory 13 | Then the program generates a new base docs repo 14 | -------------------------------------------------------------------------------- /features/repo_package.feature: -------------------------------------------------------------------------------- 1 | Feature: asciibinder package 2 | 3 | Causes the utility to run a `build` operation and then marshall 4 | together all of the distro / branch combinations that will be 5 | published into "sites". Each site has a distinct home page. 6 | 7 | Scenario: A user wants to package all of the sites contained in their docs repo 8 | Given a valid AsciiBinder docs repo with multiple distros 9 | When the user runs `asciibinder package` on that repo directory 10 | Then the program generates a site directory for each site in the distro map 11 | 12 | Scenario: A user wants to package one of the sites contained in their docs repo 13 | Given a valid AsciiBinder docs repo with multiple distros 14 | When the user runs `asciibinder package --site=test` on that repo directory 15 | Then the program generates a site directory for only the `test` site in the distro map 16 | -------------------------------------------------------------------------------- /features/step_definitions/steps.rb: -------------------------------------------------------------------------------- 1 | require 'ascii_binder/version' 2 | require 'cucumber' 3 | require 'fileutils' 4 | require 'open3' 5 | require 'diff_dirs' 6 | 7 | Given(/^an existing repo directory$/) do 8 | Dir.mkdir(working_dir) 9 | end 10 | 11 | Given(/^a nonexistent repo directory$/) do 12 | working_dir 13 | end 14 | 15 | Given(/^a valid AsciiBinder docs repo(.*)$/) do |repo_condition| 16 | multiple_distros = false 17 | offset_docs_root = false 18 | invalid_alias = false 19 | if repo_condition == ' with multiple distros' 20 | multiple_distros = true 21 | elsif repo_condition == ' where the docs root is not at the repo root' 22 | multiple_distros = true 23 | offset_docs_root = true 24 | elsif repo_condition == ' with an invalid alias' 25 | multiple_distros = true 26 | invalid_alias = true 27 | end 28 | initialize_test_repo(true,multiple_distros,offset_docs_root) 29 | if invalid_alias 30 | invalidate_topic_map 31 | end 32 | end 33 | 34 | Given(/^an invalid AsciiBinder docs repo(.*)$/) do |invalid_condition| 35 | if invalid_condition == ' due to a malformed distro map' 36 | initialize_test_repo(true,true) 37 | invalidate_distro_map 38 | else 39 | initialize_test_repo(false) 40 | end 41 | end 42 | 43 | Given(/^the docs repo contains generated content$/) do 44 | output = run_command('package') 45 | has_error = false 46 | [preview_dir,package_dir].each do |subdir| 47 | unless Dir.exist?(subdir) 48 | puts "ERROR: expected directory '#{subdir}' was not created." 49 | has_error = true 50 | end 51 | unless Dir.entries(subdir).select{ |item| not ['.','..'].include?(item) }.length > 0 52 | puts "ERROR: directory '#{subdir}' is empty." 53 | has_error = true 54 | end 55 | end 56 | if has_error 57 | print_output(output) 58 | exit 1 59 | end 60 | end 61 | 62 | Given(/^the docs repo contains no generated content$/) do 63 | [preview_dir,package_dir].each do |subdir| 64 | next unless Dir.exist?(subdir) 65 | FileUtils.rm_rf(subdir) 66 | end 67 | end 68 | 69 | Given(/^a nonexistent remote repo$/) do 70 | @remote_repo_url = 'http://example.com/repo.git' 71 | end 72 | 73 | Given(/^an existing remote repo$/) do 74 | # We're going to set up a local repo as a remote. 75 | @remote_repo_dir = initialize_remote_repo 76 | @remote_repo_url = "file://#{@remote_repo_dir}" 77 | end 78 | 79 | When(/^the user runs `asciibinder (.+)` on that (.+) directory$/) do |command_string,target_dir| 80 | @command_args = command_string.split(' ') 81 | run_dir = target_dir == 'docs root' ? File.join(working_dir,'docs') : working_dir 82 | command = @command_args.shift 83 | if command == 'clone' 84 | @step_output = run_command(command,["-d #{working_dir}"],@remote_repo_url) 85 | else 86 | @step_output = run_command(command,@command_args,run_dir) 87 | end 88 | end 89 | 90 | Then(/^the generated content is removed$/) do 91 | has_error = false 92 | [preview_dir,package_dir].each do |subdir| 93 | unless Dir.exist?(subdir) 94 | puts "ERROR: expected to find directory '#{subdir}' but didn't" 95 | has_error = true 96 | end 97 | unless Dir.entries(subdir).select{ |item| not ['.','..'].include?(item) }.length == 0 98 | puts "ERROR: expected directory '#{subdir}' to be empty" 99 | has_error 100 | end 101 | end 102 | if has_error 103 | print_output(@step_output) 104 | exit 1 105 | end 106 | end 107 | 108 | Then(/^the program exits without errors$/) do 109 | status_check(@step_output,'running `asciibinder clean`.') 110 | end 111 | 112 | Then(/^the program exits with a warning$/) do 113 | if @step_output[:status].exitstatus == 0 114 | puts "ERROR: testing `asciibinder clean`; expected an exit code other than 0." 115 | print_output(@step_output) 116 | exit 1 117 | end 118 | end 119 | 120 | Then(/^the program clones the remote repo into the local directory$/) do 121 | diffs = diff_dirs(@remote_repo_dir, working_dir) 122 | non_git_diffs = diffs.select{ |entry| not entry[1].start_with?('.git') } 123 | remote_branches = Git.open(@remote_repo_dir).branches.local.map{ |branch| branch.name }.sort 124 | local_branches = Git.open(working_dir).branches.local.map{ |branch| branch.name }.sort 125 | branch_matches = remote_branches & local_branches 126 | unless branch_matches.length == local_branches.length and non_git_diffs.length == 0 127 | puts "ERROR: cloned repo doesn't match remote repo." 128 | exit 1 129 | end 130 | end 131 | 132 | Then(/^the program generates a new base docs repo$/) do 133 | diffs = diff_dirs(repo_template_dir, working_dir) 134 | unless diffs.length == 1 and diffs[0][0] == :new and diffs[0][1] == '.git' 135 | puts "ERROR: template repo copy produced differences - #{diffs.inspect}" 136 | exit 1 137 | end 138 | end 139 | 140 | Then(/^the program displays help information$/) do 141 | status_check(@step_output,'`asciibinder help` command failed.') 142 | unless @step_output[:stderr] == '' and @step_output[:stdout].start_with?('Usage:') 143 | puts "ERROR: unexpected help output" 144 | print_output(@step_output) 145 | exit 1 146 | end 147 | end 148 | 149 | Then(/^the program prints the current version of the utility$/) do 150 | status_check(@step_output,'`asciibinder version` command failed.') 151 | unless @step_output[:stderr] == '' and @step_output[:stdout].chomp == AsciiBinder::VERSION 152 | puts "ERROR: unexpected help output" 153 | print_output(@step_output) 154 | exit 1 155 | end 156 | end 157 | 158 | Then(/^the program generates preview content for (.+)$/) do |build_target| 159 | status_check(@step_output,'`asciibinder build` command failed.') 160 | case build_target 161 | when 'all distros in the current branch' 162 | build_check(:default) 163 | when 'only the `distro_test` distro' 164 | distro = @command_args.select{ |arg| arg.starts_with?('--distro=') }.map{ |arg| arg.split('=')[1] }[0] 165 | build_check(:distro,distro) 166 | when 'all relevant distro/branch combos' 167 | build_check(:all_branches) 168 | when 'the specified page in all distros' 169 | page = @command_args.select{ |arg| arg.starts_with?('--page=') }.map{ |arg| arg.split('=')[1] }[0] 170 | build_check(:page,page) 171 | else 172 | puts "ERROR: unrecognized test case '#{build_target}'" 173 | exit 1 174 | end 175 | end 176 | 177 | Then(/^the program generates a site directory for (.+) in the distro map$/) do |package_target| 178 | status_check(@step_output,'`asciibinder package` command failed.') 179 | case package_target 180 | when 'each site' 181 | package_check() 182 | when 'only the `test` site' 183 | site = @command_args.select{ |arg| arg.starts_with?('--site=') }.map{ |arg| arg.split('=')[1] }[0] 184 | package_check(site) 185 | else 186 | puts "ERROR: unrecognized test case '#{build_target}'" 187 | exit 1 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /features/support/_clone_distro_map.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ascii_binder: 3 | name: AsciiBinder Doc Project 4 | author: AsciiBinder Team 5 | site: main 6 | site_name: Home 7 | site_url: http://asciibinder.org/ 8 | branches: 9 | master: 10 | name: Latest 11 | dir: latest 12 | branch1: 13 | name: Branch 1 14 | dir: branch1 15 | -------------------------------------------------------------------------------- /features/support/_invalid_alias_topic_map.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: AsciiBinder Doc Project 3 | Dir: welcome 4 | Topics: 5 | - Name: Welcome 6 | File: index 7 | - Name: Aliased Topic 8 | File: aliased 9 | - Name: Subtopics 10 | Dir: subtopics 11 | Topics: 12 | - Name: All Distros 13 | File: index 14 | - Name: MAIN_ONLY_TOPIC 15 | File: main_only_topic 16 | Distros: distro_main 17 | - Name: TEST_ONLY_TOPIC 18 | File: test_only_topic 19 | Distros: distro_test 20 | - Name: Wilcard All 21 | File: wildcard_all 22 | Distros: distro_* 23 | 24 | --- 25 | Name: MAIN_ONLY_TOPIC_GROUP 26 | Dir: main_only_topic_group 27 | Distros: distro_main 28 | Topics: 29 | - Name: MAIN_ONLY_WELCOME 30 | File: index 31 | 32 | --- 33 | Name: TEST_ONLY_TOPIC_GROUP 34 | Dir: test_only_topic_group 35 | Distros: distro_test 36 | Topics: 37 | - Name: TEST_ONLY_WELCOME 38 | File: index 39 | 40 | --- 41 | Name: ALIASES_GROUP 42 | Dir: aliases 43 | Topics: 44 | - Name: Alias to Nonexistant 45 | File: a_to_a 46 | Alias: welcome/nonexistent 47 | - Name: Alias to External 48 | File: a_to_e 49 | Alias: https://redhat.com/ 50 | 51 | -------------------------------------------------------------------------------- /features/support/_invalid_distro_map.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ascii_binder: 3 | name: " " 4 | author: " " 5 | site: " " 6 | site_name: " " 7 | site_url: " " 8 | branches: 9 | master: 10 | name: " " 11 | dir: " " 12 | branch1: 13 | name: " " 14 | dir: " " 15 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'git' 3 | require 'open3' 4 | require 'tmpdir' 5 | require 'yaml' 6 | 7 | module Helpers 8 | def gem_root 9 | File.expand_path '../../..', __FILE__ 10 | end 11 | 12 | def run_command(command,args=[],repo_dir=nil) 13 | if repo_dir.nil? 14 | repo_dir = working_dir 15 | end 16 | instructions = [File.join(gem_root,'bin','asciibinder'),command] 17 | instructions.concat(args) 18 | instructions << repo_dir 19 | stdout_str, stderr_str, status = Open3.capture3(instructions.join(' ')) 20 | return { :stdout => stdout_str, :stderr => stderr_str, :status => status } 21 | end 22 | 23 | def print_output(command_output) 24 | puts "STDOUT:\n#{command_output[:stdout]}\n\n" 25 | puts "STDERR:\n#{command_output[:stderr]}\n\n" 26 | puts "EXIT CODE: #{command_output[:status].exitstatus}\n\n" 27 | end 28 | 29 | def working_dir 30 | @working_dir ||= begin 31 | working_dir = Dir.mktmpdir('ascii_binder-cucumber') 32 | track_tmp_dir(working_dir) 33 | FileUtils.rm_rf(working_dir) 34 | working_dir 35 | end 36 | end 37 | 38 | def distro_map 39 | @distro_map ||= YAML.load_file(File.join(docs_root,'_distro_map.yml')) 40 | end 41 | 42 | def topic_map 43 | # Normally we want to read the topic map from each branch. In our test setup, 44 | # each branch has an identical topic map, so we can get away with this for now. 45 | @topic_map ||= YAML.load_stream(open(File.join(docs_root,'_topic_map.yml'))) 46 | end 47 | 48 | def alias_files 49 | @alias_files ||= ['aliases/a_to_a.html','aliases/a_to_e.html'] 50 | end 51 | 52 | def preview_dir 53 | @preview_dir ||= File.join(docs_root,'_preview') 54 | end 55 | 56 | def package_dir 57 | @package_dir ||= File.join(docs_root,'_package') 58 | end 59 | 60 | def repo_template_dir 61 | @repo_template_dir ||= File.join(gem_root,'templates') 62 | end 63 | 64 | def test_distro_dir 65 | @test_distro_dir ||= File.join(gem_root,'features','support','test_distro') 66 | end 67 | 68 | def track_tmp_dir(tmp_dir) 69 | if @tracked_tmp_dirs.nil? 70 | @tracked_tmp_dirs = [] 71 | end 72 | @tracked_tmp_dirs << tmp_dir unless @tracked_tmp_dirs.include?(tmp_dir) 73 | end 74 | 75 | def clean_tracked_dirs 76 | @tracked_tmp_dirs.each do |dir| 77 | FileUtils.rm_rf(dir) 78 | end 79 | end 80 | 81 | def find_html_files(dir) 82 | `cd #{dir} && find .`.split("\n").select{ |item| item.end_with?('.html') }.map{ |item| item[2..-1] } 83 | end 84 | 85 | def files_diff_explanation(gen_paths,cfg_paths) 86 | gen_extras = (gen_paths-cfg_paths) 87 | cfg_extras = (cfg_paths-gen_paths) 88 | explanation = '' 89 | if gen_extras.length > 0 90 | explanation = "Unexpected extra files were generated:\n\t* " + gen_extras.join("\n\t* ") 91 | end 92 | if cfg_extras.length > 0 93 | if explanation.length > 0 94 | explanation = explanation + "\n" 95 | end 96 | explanation = explanation + "Expected files were not generated:\n\t* " + cfg_extras.join("\n\t* ") 97 | end 98 | return explanation 99 | end 100 | 101 | def actual_preview_info 102 | all_preview_paths = find_html_files(preview_dir) 103 | 104 | map = {} 105 | dirmatch = {} 106 | distro_map.each do |distro,distro_info| 107 | map[distro] = {} 108 | distro_info['branches'].each do |branch,branch_info| 109 | map[distro][branch] = [] 110 | dirmatch["#{distro}/#{branch_info['dir']}"] = { :distro => distro, :branch => branch } 111 | end 112 | end 113 | 114 | populated_distros = [] 115 | populated_branches = [] 116 | populated_pages = [] 117 | all_preview_paths.each do |preview_path| 118 | found_dirmatch = false 119 | dirmatch.each do |branch_path,db_keys| 120 | next unless preview_path.start_with?(branch_path) 121 | found_dirmatch = true 122 | map[db_keys[:distro]][db_keys[:branch]] << preview_path 123 | populated_distros << db_keys[:distro] 124 | populated_branches << db_keys[:branch] 125 | populated_pages << preview_path.split('/')[2..-1].join('/') 126 | break 127 | end 128 | unless found_dirmatch 129 | puts "ERROR: unexpected output file '#{preview_path}'" 130 | exit 1 131 | end 132 | end 133 | 134 | map.keys.each do |distro| 135 | map[distro].keys.each do |branch| 136 | map[distro][branch].sort! 137 | end 138 | end 139 | 140 | return { 141 | :map => map, 142 | :distros => populated_distros.uniq, 143 | :branches => populated_branches.uniq, 144 | :pages => populated_pages.uniq, 145 | } 146 | end 147 | 148 | def actual_site_map 149 | all_site_paths = find_html_files(package_dir) 150 | 151 | map = {} 152 | dirmatch = {} 153 | distro_map.each do |distro,distro_info| 154 | site = distro_info['site'] 155 | unless map.has_key?(site) 156 | map[site] = {} 157 | end 158 | map[site][distro] = {} 159 | distro_info['branches'].each do |branch,branch_info| 160 | map[site][distro][branch] = [] 161 | dirmatch["#{distro_info['site']}/#{branch_info['dir']}"] = { 162 | :distro => distro, 163 | :branch => branch, 164 | :site => site, 165 | } 166 | end 167 | end 168 | 169 | all_site_paths.each do |site_path| 170 | # skip the top-level index.html file in each site. 171 | path_parts = site_path.split('/') 172 | next if path_parts.length == 2 and path_parts[1] == 'index.html' 173 | 174 | found_dirmatch = false 175 | dirmatch.each do |branch_path,db_keys| 176 | next unless site_path.start_with?(branch_path) 177 | found_dirmatch = true 178 | map[db_keys[:site]][db_keys[:distro]][db_keys[:branch]] << site_path 179 | break 180 | end 181 | unless found_dirmatch 182 | puts "ERROR: unexpected output file '#{site_path}'" 183 | exit 1 184 | end 185 | end 186 | 187 | map.keys.each do |site| 188 | map[site].keys.each do |distro| 189 | map[site][distro].keys.each do |branch| 190 | map[site][distro][branch].sort! 191 | end 192 | end 193 | end 194 | 195 | return map 196 | end 197 | 198 | def distro_preview_path_map 199 | map = {} 200 | distro_map.each do |distro,distro_info| 201 | map[distro] = {} 202 | distro_info['branches'].each do |branch,branch_info| 203 | map[distro][branch] = [] 204 | topic_map.each do |topic_node| 205 | map[distro][branch].concat(topic_paths(distro,topic_node).map{ |subpath| "#{distro}/#{branch_info['dir']}/#{subpath}" }) 206 | end 207 | map[distro][branch].sort! 208 | end 209 | end 210 | return map 211 | end 212 | 213 | def distro_site_path_map 214 | map = {} 215 | distro_map.each do |distro,distro_info| 216 | site = distro_info['site'] 217 | unless map.has_key?(site) 218 | map[site] = {} 219 | end 220 | map[site][distro] = {} 221 | distro_info['branches'].each do |branch,branch_info| 222 | map[site][distro][branch] = [] 223 | topic_map.each do |topic_node| 224 | map[site][distro][branch].concat(topic_paths(distro,topic_node).map{ |subpath| "#{site}/#{branch_info['dir']}/#{subpath}" }) 225 | end 226 | map[site][distro][branch].sort! 227 | end 228 | end 229 | return map 230 | end 231 | 232 | def topic_paths(distro,topic_node) 233 | # First, determine if this topic node should be included for this distro. 234 | if topic_node.has_key?('Distros') 235 | found_distro = false 236 | included_distros = topic_node['Distros'].split(',') 237 | included_distros.each do |check_distro| 238 | if check_distro.include?('*') and File.fnmatch(check_distro,distro) 239 | found_distro = true 240 | break 241 | elsif check_distro == distro 242 | found_distro = true 243 | break 244 | end 245 | end 246 | unless found_distro 247 | return [] 248 | end 249 | end 250 | 251 | if topic_node.has_key?('File') 252 | # This topic node is a topic "leaf"; return it with '.html' as the extension. 253 | filename = topic_node['File'].split('.')[0] 254 | return ["#{filename}.html"] 255 | elsif topic_node.has_key?('Dir') 256 | dirpath = topic_node['Dir'] 257 | subtopics = [] 258 | topic_node['Topics'].each do |subtopic_node| 259 | subtopics.concat(topic_paths(distro,subtopic_node)) 260 | end 261 | return subtopics.map{ |subpath| "#{dirpath}/#{subpath}" } 262 | else 263 | puts "ERROR: Malformed topic node. #{topic_node.inspect}" 264 | exit 1 265 | end 266 | end 267 | 268 | def set_initial_working_branch(branch) 269 | @initial_working_branch = branch 270 | end 271 | 272 | def initial_working_branch 273 | @initial_working_branch ||= nil 274 | end 275 | 276 | def using_offset_docs_root? 277 | @using_offset_docs_root 278 | end 279 | 280 | def docs_root 281 | using_offset_docs_root? ? File.join(working_dir,'docs') : working_dir 282 | end 283 | 284 | def initialize_test_repo(valid,multiple_distros=false,offset_docs_root=false) 285 | unless valid 286 | FileUtils.mkdir(working_dir) 287 | else 288 | status_check(run_command('create'),'Could not initialize test repo.') 289 | if multiple_distros 290 | FileUtils.cp_r(File.join(test_distro_dir,'.'),working_dir) 291 | end 292 | if offset_docs_root 293 | @using_offset_docs_root = true 294 | entries = Dir.entries(working_dir).select{ |item| not item.start_with?('.') } 295 | system("cd #{working_dir} && mkdir docs") 296 | entries.each do |entry| 297 | system("cd #{working_dir} && mv #{entry} docs") 298 | end 299 | end 300 | system("cd #{working_dir} && git add . > /dev/null && git commit -am 'test commit' > /dev/null") 301 | if multiple_distros 302 | system("cd #{working_dir} && git checkout -b branch1 > /dev/null 2>&1 && git checkout -b branch2 > /dev/null 2>&1 && git checkout main > /dev/null 2>&1") 303 | end 304 | set_initial_working_branch('main') 305 | end 306 | working_dir 307 | end 308 | 309 | def invalidate_distro_map 310 | invalid_map = File.join(gem_root,'features','support','_invalid_distro_map.yml') 311 | FileUtils.cp(invalid_map,File.join(docs_root,'_distro_map.yml')) 312 | system("cd #{working_dir} && git add . > /dev/null && git commit -am 'Commit invalid distro map' > /dev/null") 313 | end 314 | 315 | def invalidate_topic_map 316 | invalid_map = File.join(gem_root,'features','support','_invalid_alias_topic_map.yml') 317 | FileUtils.cp(invalid_map,File.join(docs_root,'_topic_map.yml')) 318 | system("cd #{working_dir} && git add . > /dev/null && git commit -am 'Commit invalid alias topic map' > /dev/null") 319 | end 320 | 321 | def initialize_remote_repo 322 | remote_dir = Dir.mktmpdir('ascii_binder-cucumber-remote') 323 | FileUtils.rm_rf(remote_dir) 324 | track_tmp_dir(remote_dir) 325 | if run_command('create',[],remote_dir)[:status].exitstatus == 0 326 | clone_map = File.join(gem_root,'features','support','_clone_distro_map.yml') 327 | FileUtils.cp(clone_map,File.join(remote_dir,'_distro_map.yml')) 328 | system("cd #{remote_dir} && git add . > /dev/null && git commit -am 'remote commit' > /dev/null && git checkout -b branch1 > /dev/null 2>&1 && git checkout main > /dev/null 2>&1") 329 | else 330 | puts "ERROR: Could not initialize remote repo" 331 | exit 1 332 | end 333 | remote_dir 334 | end 335 | 336 | def status_check(step_output,error_message) 337 | unless step_output[:status].exitstatus == 0 338 | puts "ERROR: #{error_message}" 339 | print_output(step_output) 340 | exit 1 341 | end 342 | end 343 | 344 | def build_check(scope,target='') 345 | # Initial state of check_map matches ':default' scope 346 | check_map = { 347 | :current_branch_only => true, 348 | :specified_distro_only => false, 349 | :specified_page_only => false, 350 | } 351 | case scope 352 | when :default 353 | # Change nothing 354 | when :distro 355 | check_map[:specified_distro_only] = true 356 | when :all_branches 357 | check_map[:current_branch_only] = false 358 | when :page 359 | check_map[:specified_page_only] = true 360 | else 361 | puts "ERROR: Build scope '#{scope}' not recognized." 362 | exit 1 363 | end 364 | 365 | # Make sure the build finished on the same branch where it started. 366 | git = Git.open(working_dir) 367 | current_working_branch = git.branch.name 368 | unless current_working_branch == initial_working_branch 369 | puts "ERROR: Build operation started on branch '#{initial_working_branch}' but ended on branch '#{current_working_branch}'" 370 | exit 1 371 | end 372 | 373 | # generate the expected preview paths for each full distro + branch combo 374 | all_paths_map = distro_preview_path_map 375 | 376 | # get all of the paths in the actual preview directory 377 | real_preview_info = actual_preview_info 378 | 379 | gen_paths_map = real_preview_info[:map] 380 | branch_count = real_preview_info[:branches].length 381 | distro_count = real_preview_info[:distros].length 382 | page_count = real_preview_info[:pages].length 383 | target_distro = real_preview_info[:distros][0] 384 | target_page = real_preview_info[:pages][0].split('/').join(':').split('.')[0] 385 | 386 | if distro_count == 0 or branch_count == 0 387 | puts "ERROR: A build operation should produce at least one distro / branch preview." 388 | exit 1 389 | end 390 | 391 | # Compare branches by count 392 | if branch_count > 1 and check_map[:current_branch_only] 393 | puts "ERROR: Expected behavior for '#{scope}' scope is to build current working branch only." 394 | exit 1 395 | elsif branch_count == 1 and not check_map[:current_branch_only] 396 | puts "ERROR: Expected behavior for '#{scope}' scope is to build all local branches." 397 | exit 1 398 | end 399 | 400 | # Compare distros by count 401 | if distro_count > 1 and check_map[:specified_distro_only] 402 | puts "ERROR: Expected behavior for '#{scope}' scope is to build specified branch ('#{target}') only." 403 | exit 1 404 | elsif distro_count == 1 405 | if not check_map[:specified_distro_only] 406 | puts "ERROR: Expected behavior for '#{scope}' scope is to build all distros." 407 | exit 1 408 | elsif not target_distro == target 409 | puts "ERROR: The build did not run for the expected target distro '#{target}' but instead for '#{target_distro}'" 410 | exit 1 411 | end 412 | end 413 | 414 | # Compare pages by count 415 | if page_count > 1 and check_map[:specified_page_only] 416 | puts "ERROR: Expected behavior for '#{scope}' is to build the specified page ('#{target}') only." 417 | exit 1 418 | elsif page_count == 1 419 | if not check_map[:specified_page_only] 420 | puts "ERROR: Expected behavior for '#{scope}' scope is to build all pages." 421 | exit 1 422 | elsif not target_page == target 423 | puts "ERROR: The build did not run for the expected target page '#{target}' but instead for '#{target_page}'" 424 | end 425 | end 426 | 427 | # Generated files vs expected files. 428 | if not check_map[:specified_page_only] 429 | all_paths_map.keys.each do |distro| 430 | next if check_map[:specified_distro_only] and not distro == target 431 | if not gen_paths_map.has_key?(distro) 432 | puts "ERROR: Expected distro '#{distro}' was not generated for preview." 433 | exit 1 434 | end 435 | all_paths_map[distro].keys.each do |branch| 436 | next if check_map[:current_branch_only] and not branch == current_working_branch 437 | if not gen_paths_map[distro].has_key?(branch) 438 | puts "ERROR: Expected distro / branch combo '#{distro}' / '#{branch}' was not generated for preview." 439 | exit 1 440 | end 441 | # Alias check 442 | alias_files.each do |afile| 443 | genmatches = gen_paths_map[distro][branch].select{ |i| i.end_with?(afile) } 444 | if genmatches.length == 0 445 | puts "ERROR: Alias file '#{afile}' was not generated for distro / branch combo '#{distro}' / '#{branch}'." 446 | exit 1 447 | elsif genmatches.length > 1 448 | puts "ERROR: Alias file '#{afile}' found more than once in generated output: #{genmatches.inspect}" 449 | exit 1 450 | end 451 | end 452 | if not gen_paths_map[distro][branch] == all_paths_map[distro][branch] 453 | explanation = files_diff_explanation(gen_paths_map[distro][branch],all_paths_map[distro][branch]) 454 | puts "ERROR: Mismatch between expected and actual preview file paths for distro / branch combo '#{distro}' / '#{branch}'.\n#{explanation}" 455 | exit 1 456 | end 457 | end 458 | end 459 | end 460 | end 461 | 462 | def package_check(target_site='') 463 | all_paths_map = distro_site_path_map 464 | real_site_map = actual_site_map 465 | 466 | real_site_map.keys.each do |site| 467 | real_site_map[site].keys.each do |distro| 468 | real_site_map[site][distro].keys.each do |branch| 469 | # If a target site was specified and any content was generated for a different site, raise an error. 470 | if not target_site == '' and not site == target_site and real_site_map[site][distro][branch].length > 0 471 | puts "ERROR: Content was generated for site '#{site}' even though it was only expected for site '#{target_site}'" 472 | exit 1 473 | end 474 | # Alias check 475 | if real_site_map[site][distro][branch].length > 0 and all_paths_map[site][distro][branch].length > 0 476 | alias_files.each do |afile| 477 | genmatches = real_site_map[site][distro][branch].select{ |i| i.end_with?(afile) } 478 | if genmatches.length == 0 479 | puts "ERROR: Alias file '#{afile}' was not generated for site / distro / branch combo '#{site}' / '#{distro}' / '#{branch}'." 480 | exit 1 481 | elsif genmatches.length > 1 482 | puts "ERROR: Alias file '#{afile}' found more than once in generated site output: #{genmatches.inspect}" 483 | exit 1 484 | end 485 | end 486 | end 487 | # Confirm that what was generated matches what was expected. 488 | if (target_site == '' or site == target_site) and not real_site_map[site][distro][branch] == all_paths_map[site][distro][branch] 489 | explanation = files_diff_explanation(real_site_map[site][distro][branch],all_paths_map[site][distro][branch]) 490 | puts "ERROR: Mismatch between expected and actual site file paths for site / distro / branch combo '#{site}' / '#{distro}' / '#{branch}'.\n#{explanation}" 491 | exit 1 492 | end 493 | end 494 | end 495 | 496 | # Skip the next check for sites that aren't being packaged. 497 | next unless target_site == '' or site == target_site 498 | 499 | # Finally, confirm that the expected site index page was copied to the site home directory. 500 | source_page = File.join(docs_root,"index-#{site}.html") 501 | target_page = File.join(package_dir,site,'index.html') 502 | unless FileUtils.compare_file(source_page,target_page) 503 | puts "ERROR: Incorrect site index file contents at '#{target_page}'; expected contents of '#{source_page}'." 504 | exit 1 505 | end 506 | end 507 | end 508 | end 509 | 510 | World(Helpers) 511 | 512 | Before do 513 | working_dir 514 | end 515 | 516 | After do 517 | clean_tracked_dirs 518 | end 519 | -------------------------------------------------------------------------------- /features/support/test_distro/.gitignore: -------------------------------------------------------------------------------- 1 | ## AsciiBinder-specific ignores 2 | _preview 3 | _package 4 | *.swp 5 | diag-*.png 6 | diag-*.png.cache 7 | 8 | ## Project-specific ignores 9 | 10 | -------------------------------------------------------------------------------- /features/support/test_distro/_distro_map.yml: -------------------------------------------------------------------------------- 1 | --- 2 | distro_main: 3 | name: AsciiBinder Doc Project 4 | author: AsciiBinder Team 5 | site: main 6 | site_name: Home 7 | site_url: http://asciibinder.org/ 8 | branches: 9 | master: 10 | name: Latest 11 | dir: latest 12 | branch1: 13 | name: Branch 1 14 | dir: branch1 15 | distro_test: 16 | name: TEST_NAME 17 | author: Test 18 | site: test 19 | site_name: TEST_SITE 20 | site_url: http://docs.test.example.com/ 21 | branches: 22 | master: 23 | name: TEST_BRANCH_LATEST 24 | dir: test_latest 25 | distro-overrides: 26 | name: TEST_BRANCH_NICEST 27 | branch2: 28 | name: TEST_BRANCH_2 29 | dir: 'test_branch/2' 30 | -------------------------------------------------------------------------------- /features/support/test_distro/_images/asciibinder-logo-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhataccess/ascii_binder/687f8a5fef233c9bb20a098505d32998f54ff4f3/features/support/test_distro/_images/asciibinder-logo-horizontal.png -------------------------------------------------------------------------------- /features/support/test_distro/_images/asciibinder_web_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 63 | 68 | 73 | 78 | 83 | 88 | 93 | 98 | 103 | 108 | 113 | 118 | 119 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /features/support/test_distro/_images/book_pages_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhataccess/ascii_binder/687f8a5fef233c9bb20a098505d32998f54ff4f3/features/support/test_distro/_images/book_pages_bg.jpg -------------------------------------------------------------------------------- /features/support/test_distro/_images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhataccess/ascii_binder/687f8a5fef233c9bb20a098505d32998f54ff4f3/features/support/test_distro/_images/favicon.ico -------------------------------------------------------------------------------- /features/support/test_distro/_images/favicon32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhataccess/ascii_binder/687f8a5fef233c9bb20a098505d32998f54ff4f3/features/support/test_distro/_images/favicon32x32.png -------------------------------------------------------------------------------- /features/support/test_distro/_javascripts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhataccess/ascii_binder/687f8a5fef233c9bb20a098505d32998f54ff4f3/features/support/test_distro/_javascripts/.gitkeep -------------------------------------------------------------------------------- /features/support/test_distro/_templates/_css.html.erb: -------------------------------------------------------------------------------- 1 | <%- Dir.glob("_stylesheets/*").sort.each do |sheet| -%> 2 | 3 | <%- end -%> 4 | -------------------------------------------------------------------------------- /features/support/test_distro/_templates/_nav.html.erb: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /features/support/test_distro/_templates/page.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= distro %> <%= version %> | <%= [group_title, subgroup_title, topic_title].compact.join(' | ') %> 8 | 9 | 10 | 11 | 12 | 13 | <%= render("_templates/_css.html.erb", :css_path => css_path) %> 14 | 15 | 16 | 17 | 21 | 22 | " rel="shortcut icon" type="text/css"> 23 | 24 | 25 | 26 | 27 | 34 |
35 |

36 | 37 |

38 | 53 |
54 | 57 |
58 | 61 | <%= content %> 62 |
63 |
64 |
65 | 66 | 67 | 68 | 69 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /features/support/test_distro/_topic_map.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: AsciiBinder Doc Project 3 | Dir: welcome 4 | Topics: 5 | - Name: Welcome 6 | File: index 7 | - Name: Aliased Topic 8 | File: aliased 9 | - Name: Subtopics 10 | Dir: subtopics 11 | Topics: 12 | - Name: All Distros 13 | File: index 14 | - Name: MAIN_ONLY_TOPIC 15 | File: main_only_topic 16 | Distros: distro_main 17 | - Name: TEST_ONLY_TOPIC 18 | File: test_only_topic 19 | Distros: distro_test 20 | - Name: Wilcard All 21 | File: wildcard_all 22 | Distros: distro_* 23 | 24 | --- 25 | Name: MAIN_ONLY_TOPIC_GROUP 26 | Dir: main_only_topic_group 27 | Distros: distro_main 28 | Topics: 29 | - Name: MAIN_ONLY_WELCOME 30 | File: index 31 | 32 | --- 33 | Name: TEST_ONLY_TOPIC_GROUP 34 | Dir: test_only_topic_group 35 | Distros: distro_test 36 | Topics: 37 | - Name: TEST_ONLY_WELCOME 38 | File: index 39 | 40 | --- 41 | Name: ALIASES_GROUP 42 | Dir: aliases 43 | Topics: 44 | - Name: Alias to Aliased 45 | File: a_to_a 46 | Alias: welcome/aliased 47 | - Name: Alias to External 48 | File: a_to_e 49 | Alias: https://redhat.com/ 50 | 51 | -------------------------------------------------------------------------------- /features/support/test_distro/index-main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | INDEX MAIN 6 | 7 | 8 | INDEX MAIN 9 | 10 | 11 | -------------------------------------------------------------------------------- /features/support/test_distro/index-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | INDEX TEST 6 | 7 | 8 | INDEX TEST 9 | 10 | 11 | -------------------------------------------------------------------------------- /features/support/test_distro/main_only_topic_group/index.adoc: -------------------------------------------------------------------------------- 1 | = {product-title} {product-version} Documentation 2 | {product-author} 3 | {product-version} 4 | :data-uri: 5 | :icons: 6 | 7 | DIRECTIVE-TAGGED DOC 8 | 9 | Line 1: This should be visible in all distros 10 | 11 | ifdef::distro_main[] 12 | Line 2: This should only be visible in distro_main 13 | endif::[] 14 | 15 | ifdef::distro_test[] 16 | Line 3: This should only be visible in distro_test 17 | endif::[] 18 | -------------------------------------------------------------------------------- /features/support/test_distro/test_only_topic_group/index.adoc: -------------------------------------------------------------------------------- 1 | = {product-title} {product-version} Documentation 2 | {product-author} 3 | {product-version} 4 | :data-uri: 5 | :icons: 6 | 7 | DIRECTIVE-TAGGED DOC 8 | 9 | Line 1: This should be visible in all distros 10 | 11 | ifdef::distro_main[] 12 | Line 2: This should only be visible in distro_main 13 | endif::[] 14 | 15 | ifdef::distro_test[] 16 | Line 3: This should only be visible in distro_test 17 | endif::[] 18 | -------------------------------------------------------------------------------- /features/support/test_distro/welcome/aliased.adoc: -------------------------------------------------------------------------------- 1 | = Aliased Topic 2 | {product-author} 3 | {product-version} 4 | :data-uri: 5 | :icons: 6 | 7 | This topic exercises topic _aliasing_, which enables authors to manage page redirects without having to fiddle with web server settings. 8 | 9 | This behavior borrows directly from the same-named concept supported by the link:https://gohugo.io[hugo] blog generator. 10 | -------------------------------------------------------------------------------- /features/support/test_distro/welcome/index.adoc: -------------------------------------------------------------------------------- 1 | = {product-title} {product-version} Documentation 2 | {product-author} 3 | {product-version} 4 | :data-uri: 5 | :icons: 6 | 7 | DIRECTIVE-TAGGED DOC 8 | 9 | Line 1: This should be visible in all distros 10 | 11 | ifdef::distro_main[] 12 | Line 2: This should only be visible in distro_main 13 | endif::[] 14 | 15 | ifdef::distro_test[] 16 | Line 3: This should only be visible in distro_test 17 | endif::[] 18 | -------------------------------------------------------------------------------- /features/support/test_distro/welcome/subtopics/index.adoc: -------------------------------------------------------------------------------- 1 | = {product-title} {product-version} Documentation 2 | {product-author} 3 | {product-version} 4 | :data-uri: 5 | :icons: 6 | 7 | DIRECTIVE-TAGGED DOC 8 | 9 | Line 1: This should be visible in all distros 10 | 11 | ifdef::distro_main[] 12 | Line 2: This should only be visible in distro_main 13 | endif::[] 14 | 15 | ifdef::distro_test[] 16 | Line 3: This should only be visible in distro_test 17 | endif::[] 18 | -------------------------------------------------------------------------------- /features/support/test_distro/welcome/subtopics/main_only_topic.adoc: -------------------------------------------------------------------------------- 1 | = {product-title} {product-version} Documentation 2 | {product-author} 3 | {product-version} 4 | :data-uri: 5 | :icons: 6 | 7 | DIRECTIVE-TAGGED DOC 8 | 9 | Line 1: This should be visible in all distros 10 | 11 | ifdef::distro_main[] 12 | Line 2: This should only be visible in distro_main 13 | endif::[] 14 | 15 | ifdef::distro_test[] 16 | Line 3: This should only be visible in distro_test 17 | endif::[] 18 | -------------------------------------------------------------------------------- /features/support/test_distro/welcome/subtopics/test_only_topic.adoc: -------------------------------------------------------------------------------- 1 | = {product-title} {product-version} Documentation 2 | {product-author} 3 | {product-version} 4 | :data-uri: 5 | :icons: 6 | 7 | DIRECTIVE-TAGGED DOC 8 | 9 | Line 1: This should be visible in all distros 10 | 11 | ifdef::distro_main[] 12 | Line 2: This should only be visible in distro_main 13 | endif::[] 14 | 15 | ifdef::distro_test[] 16 | Line 3: This should only be visible in distro_test 17 | endif::[] 18 | -------------------------------------------------------------------------------- /features/support/test_distro/welcome/subtopics/wildcard_all.adoc: -------------------------------------------------------------------------------- 1 | = {product-title} {product-version} Documentation 2 | {product-author} 3 | {product-version} 4 | :data-uri: 5 | :icons: 6 | 7 | DIRECTIVE-TAGGED DOC 8 | 9 | Line 1: This should be visible in all distros 10 | 11 | ifdef::distro_main[] 12 | Line 2: This should only be visible in distro_main 13 | endif::[] 14 | 15 | ifdef::distro_test[] 16 | Line 3: This should only be visible in distro_test 17 | endif::[] 18 | -------------------------------------------------------------------------------- /lib/ascii_binder.rb: -------------------------------------------------------------------------------- 1 | require "ascii_binder/distro_map" 2 | require "ascii_binder/engine" 3 | require "ascii_binder/helpers" 4 | require "ascii_binder/template_renderer" 5 | require "ascii_binder/version" 6 | -------------------------------------------------------------------------------- /lib/ascii_binder/distro.rb: -------------------------------------------------------------------------------- 1 | require 'ascii_binder/distro_branch' 2 | require 'ascii_binder/helpers' 3 | require 'ascii_binder/site' 4 | require 'trollop' 5 | 6 | include AsciiBinder::Helpers 7 | 8 | module AsciiBinder 9 | class Distro 10 | attr_reader :id, :name, :author, :site 11 | 12 | def initialize(distro_map_filepath,distro_key,distro_config) 13 | @id = distro_key 14 | @name = distro_config['name'] 15 | @author = distro_config['author'] 16 | @site = AsciiBinder::Site.new(distro_config) 17 | @branch_map = {} 18 | distro_config['branches'].each do |branch_name,branch_config| 19 | if @branch_map.has_key?(branch_name) 20 | Trollop::die "Error parsing #{distro_map_filepath}: distro '#{distro_key}' lists git branch '#{branch_name}' multiple times." 21 | end 22 | @branch_map[branch_name] = AsciiBinder::DistroBranch.new(branch_name,branch_config,self) 23 | end 24 | end 25 | 26 | def is_valid? 27 | validate 28 | end 29 | 30 | def errors 31 | validate(true) 32 | end 33 | 34 | def branch(branch_name) 35 | unless @branch_map.has_key?(branch_name) 36 | Trollop::die "Distro '#{@id}' does not include branch '#{branch_name}' in the distro map." 37 | end 38 | @branch_map[branch_name] 39 | end 40 | 41 | def branch_ids 42 | @branch_map.keys 43 | end 44 | 45 | def branches 46 | @branch_map.values 47 | end 48 | 49 | private 50 | 51 | def validate(verbose=false) 52 | errors = [] 53 | unless valid_id?(@id) 54 | if verbose 55 | errors << "Distro ID '#{@id}' is not a valid string" 56 | else 57 | return false 58 | end 59 | end 60 | unless valid_string?(@name) 61 | if verbose 62 | errors << "Distro name '#{@name}' for distro '#{@id}' is not a valid string." 63 | else 64 | return false 65 | end 66 | end 67 | unless valid_string?(@author) 68 | if verbose 69 | errors << "Distro author '#{@author}' for distro '#{@id}' is not a valid string." 70 | else 71 | return false 72 | end 73 | end 74 | 75 | # Remaining checks are sub objects. Handle the verbose case first. 76 | if verbose 77 | site_errors = @site.errors 78 | unless site_errors.empty? 79 | error_txt = "The site info has errors:\n" 80 | site_errors.each do |error| 81 | error_txt << " * #{error}\n" 82 | end 83 | errors << error_txt 84 | end 85 | all_branch_errors = [] 86 | @branch_map.values.each do |branch| 87 | branch_errors = branch.errors 88 | unless branch_errors.empty? 89 | all_branch_errors << " * In branch #{branch.id}:\n" 90 | branch_errors.each do |error| 91 | all_branch_errors << " * #{error}\n" 92 | end 93 | end 94 | end 95 | unless all_branch_errors.empty? 96 | all_branch_errors.unshift("The branch info has errors:") 97 | errors.concat(all_branch_errors) 98 | end 99 | return errors 100 | end 101 | 102 | # Still here? Run the non-verbose checks instead. 103 | return false unless @site.is_valid? 104 | @branch_map.values.each do |branch| 105 | return false unless branch.is_valid? 106 | end 107 | 108 | return true 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/ascii_binder/distro_branch.rb: -------------------------------------------------------------------------------- 1 | require 'ascii_binder/helpers' 2 | 3 | include AsciiBinder::Helpers 4 | 5 | module AsciiBinder 6 | class DistroBranch 7 | attr_reader :id, :name, :dir, :distro, :distro_name, :distro_author 8 | 9 | def initialize(branch_name,branch_config,distro) 10 | @id = branch_name 11 | @name = branch_config['name'] 12 | @dir = branch_config['dir'] 13 | @distro = distro 14 | @distro_name = distro.name 15 | @distro_author = distro.author 16 | if branch_config.has_key?('distro-overrides') 17 | if branch_config['distro-overrides'].has_key?('name') 18 | @distro_name = branch_config['distro-overrides']['name'] 19 | end 20 | if branch_config['distro-overrides'].has_key?('author') 21 | @distro_author = branch_config['distro-overrides']['author'] 22 | end 23 | end 24 | end 25 | 26 | def branch_path 27 | @branch_path ||= File.join(preview_dir,@distro.id,@dir) 28 | end 29 | 30 | def branch_url_base 31 | @branch_url_base ||= File.join('/',@dir) 32 | end 33 | 34 | def branch_stylesheet_dir 35 | @branch_stylesheet_dir ||= File.join(branch_path,STYLESHEET_DIRNAME) 36 | end 37 | 38 | def branch_javascript_dir 39 | @branch_javascript_dir ||= File.join(branch_path,JAVASCRIPT_DIRNAME) 40 | end 41 | 42 | def branch_image_dir 43 | @branch_image_dir ||= File.join(branch_path,IMAGE_DIRNAME) 44 | end 45 | 46 | def is_valid? 47 | validate 48 | end 49 | 50 | def errors 51 | validate(true) 52 | end 53 | 54 | private 55 | 56 | def validate(verbose=true) 57 | errors = [] 58 | unless valid_string?(@id) 59 | if verbose 60 | errors << "Branch ID '#{@id}' is not a valid string." 61 | else 62 | return false 63 | end 64 | end 65 | unless valid_string?(@name) 66 | if verbose 67 | errors << "Branch name '#{@name}' for branch ID '#{@id}' is not a valid string." 68 | else 69 | return false 70 | end 71 | end 72 | unless valid_string?(@dir) 73 | if verbose 74 | errors << "Branch dir '#{@dir}' for branch ID '#{@id}' is not a valid string." 75 | else 76 | return false 77 | end 78 | end 79 | unless valid_string?(@distro_name) 80 | if verbose 81 | errors << "Branchwise distro name '#{@distro_name}' for branch ID '#{@id}' is not a valid string." 82 | else 83 | return false 84 | end 85 | end 86 | unless valid_string?(@distro_author) 87 | if verbose 88 | errors << "Branchwise distro author '#{@distro_author}' for branch ID '#{@id}' is not a valid string." 89 | else 90 | return false 91 | end 92 | end 93 | return errors if verbose 94 | return true 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/ascii_binder/distro_map.rb: -------------------------------------------------------------------------------- 1 | require 'ascii_binder/distro' 2 | require 'trollop' 3 | require 'yaml' 4 | 5 | module AsciiBinder 6 | class DistroMap 7 | def initialize(distro_map_filepath) 8 | @distro_yaml = YAML.load_file(distro_map_filepath) 9 | @distro_map = {} 10 | @distro_yaml.each do |distro_key,distro_config| 11 | if @distro_map.has_key?(distro_key) 12 | Trollop::die "Error parsing '#{distro_map_filepath}': distro key '#{distro_key}' is used more than once." 13 | end 14 | distro = AsciiBinder::Distro.new(distro_map_filepath,distro_key,distro_config) 15 | @distro_map[distro_key] = distro 16 | end 17 | end 18 | 19 | def get_distro(distro_key) 20 | unless @distro_map.has_key?(distro_key) 21 | Trollop::die "Distro key '#{distro_key}' does not exist" 22 | end 23 | @distro_map[distro_key] 24 | end 25 | 26 | def include_distro_key?(distro_key) 27 | @distro_map.has_key?(distro_key) 28 | end 29 | 30 | def distro_keys 31 | @distro_map.keys 32 | end 33 | 34 | def distros 35 | @distro_map.values 36 | end 37 | 38 | def distro_branches(distro_key='') 39 | if distro_key == '' 40 | branch_list = [] 41 | distros.each do |distro| 42 | branch_list.concat(distro.branch_ids) 43 | end 44 | return branch_list.uniq 45 | else 46 | return get_distro(distro_key).branch_ids 47 | end 48 | end 49 | 50 | def is_valid? 51 | @distro_map.values.each do |distro| 52 | next if distro.is_valid? 53 | return false 54 | end 55 | return true 56 | end 57 | 58 | def errors 59 | errors = [] 60 | @distro_map.values.each do |distro| 61 | next if distro.is_valid? 62 | errors << distro.errors 63 | end 64 | return errors 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/ascii_binder/engine.rb: -------------------------------------------------------------------------------- 1 | require 'ascii_binder/distro_branch' 2 | require 'ascii_binder/distro_map' 3 | require 'ascii_binder/helpers' 4 | require 'ascii_binder/site_map' 5 | require 'ascii_binder/template_renderer' 6 | require 'ascii_binder/topic_map' 7 | require 'asciidoctor' 8 | require 'asciidoctor/cli' 9 | require 'asciidoctor-diagram' 10 | require 'fileutils' 11 | require 'find' 12 | require 'git' 13 | require 'pathname' 14 | require 'sitemap_generator' 15 | require 'trollop' 16 | require 'yaml' 17 | 18 | include AsciiBinder::Helpers 19 | 20 | module AsciiBinder 21 | module Engine 22 | 23 | def build_date 24 | Time.now.utc 25 | end 26 | 27 | def git 28 | @git ||= Git.open(git_root_dir) 29 | end 30 | 31 | def git_checkout branch_name 32 | target_branch = git.branches.local.select{ |b| b.name == branch_name }[0] 33 | if not target_branch.nil? and not target_branch.current 34 | target_branch.checkout 35 | end 36 | end 37 | 38 | def git_stash_all 39 | # See if there are any changes in need of stashing 40 | @stash_needed = `cd #{git_root_dir} && git status --porcelain` !~ /^\s*$/ 41 | if @stash_needed 42 | log_unknown("Stashing uncommited changes and files in working branch.") 43 | `cd #{docs_root_dir} && git stash -u` 44 | end 45 | end 46 | 47 | def git_apply_and_drop 48 | return unless @stash_needed 49 | log_unknown("Re-applying uncommitted changes and files to working branch.") 50 | if system("cd #{docs_root_dir} && git stash pop") 51 | log_unknown("Stash application successful.") 52 | else 53 | log_error("Could not apply stashed code. Run `git stash apply` manually.") 54 | end 55 | @stash_needed = false 56 | end 57 | 58 | # Returns the local git branches; current branch is always first 59 | def local_branches 60 | @local_branches ||= begin 61 | branches = [] 62 | if not git.branches.local.empty? 63 | branches << git.branches.local.select{ |b| b.current }[0].name 64 | branches << git.branches.local.select{ |b| not b.current }.map{ |b| b.name } 65 | end 66 | branches.flatten 67 | end 68 | end 69 | 70 | def working_branch 71 | @working_branch ||= local_branches[0] 72 | end 73 | 74 | def dir_empty?(dir) 75 | Dir.entries(dir).select{ |f| not f.start_with?('.') }.empty? 76 | end 77 | 78 | 79 | # Protip: Don't cache these! The topic map needs to be reread every time we change branches. 80 | def topic_map_file 81 | 82 | # new stuff 1/Nov/2021 83 | # allow users to break up the topic map into multiple topic maps 84 | # load topic maps from a single folder called _topic_maps. 85 | # we assume that all files in this folder are correctly formatted topic maps. If not, you will get normal asciibinder errors 86 | topic_map_folder = TOPIC_MAP_FOLDER 87 | topic_file = TOPIC_MAP_FILENAME 88 | 89 | if !Dir.exist?(File.join(docs_root_dir, topic_map_folder)) 90 | # if the _topic_maps directory doesn't exist or is empty, see if we can find the topic map in the root folder to maintain backward compatibility 91 | 92 | if !File.exist?(File.join(docs_root_dir, topic_file)) 93 | # fall back to looking for a _topic_map in the root directory 94 | 95 | topic_file = BUILD_FILENAME # old folders use build_config.yml 96 | 97 | if !File.exist?(File.join(docs_root_dir, topic_file)) 98 | # Critical error - no topic map file at all. 99 | Trollop::die "Could not find a valid topic map file. There is no #{TOPIC_MAP_FOLDER} folder and the fall back files #{TOPIC_MAP_FILENAME} or #{BUILD_FILENAME} in branch '#{git.branch}' were also not found." 100 | else 101 | t = File.join(docs_root_dir, topic_file) # found build_config 102 | end 103 | else 104 | t = File.join(docs_root_dir, topic_file) # found topic_map in root 105 | end 106 | 107 | else 108 | 109 | # topic map files are in the _topic_maps folder 110 | 111 | # create a combined temp file with all topic maps 112 | tf = Tempfile.new("#{TOPIC_MAP_FILENAME}") 113 | 114 | Dir.glob("#{topic_map_folder}/*.yml").sort.each do |filename| 115 | lines = IO.read(filename) 116 | tf << lines 117 | tf.write "\n" 118 | end 119 | 120 | tf.rewind 121 | t = tf.path 122 | end 123 | 124 | # returns the path to the final file 125 | t 126 | 127 | end 128 | 129 | def topic_map 130 | topic_map = AsciiBinder::TopicMap.new(topic_map_file,distro_map.distro_keys) 131 | unless topic_map.is_valid? 132 | errors = topic_map.errors 133 | Trollop::die "The topic map file at '#{topic_map_file}' contains the following errors:\n- " + errors.join("\n- ") + "\n" 134 | end 135 | return topic_map 136 | end 137 | 138 | def create_new_repo 139 | gem_template_dir = File.join(gem_root_dir,"templates") 140 | 141 | # Create the new repo dir 142 | FileUtils.mkdir_p(docs_root_dir) 143 | 144 | # Copy the basic repo content into the new repo dir 145 | Find.find(gem_template_dir).each do |path| 146 | next if path == gem_template_dir 147 | src_path = Pathname.new(path) 148 | tgt_path = src_path.sub(gem_template_dir,docs_root_dir) 149 | if src_path.directory? 150 | FileUtils.mkdir_p(tgt_path.to_s) 151 | else 152 | FileUtils.cp src_path.to_s, tgt_path.to_s 153 | end 154 | end 155 | 156 | # Initialize the git repo 157 | Git.init(docs_root_dir) 158 | end 159 | 160 | def find_topic_files 161 | file_list = [] 162 | Find.find(docs_root_dir).each do |path| 163 | # Only consider .adoc files and ignore README, and anything in 164 | # directories whose names begin with 'old' or '_' (underscore) 165 | next if path.nil? or not path =~ /.*\.adoc/ or path =~ /README/ or path =~ /\/old\// or path =~ /\/_/ 166 | src_path = Pathname.new(path).sub(docs_root_dir,'').to_s 167 | next if src_path.split('/').length < 3 168 | file_list << src_path 169 | end 170 | file_list.map{ |path| File.join(File.dirname(path),File.basename(path,'.adoc'))[1..-1] } 171 | end 172 | 173 | def remove_found_topic_files(branch,branch_topic_map,branch_topic_files) 174 | nonexistent_topics = [] 175 | branch_topic_map.filepaths.each do |topic_map_filepath| 176 | result = branch_topic_files.delete(topic_map_filepath) 177 | if result.nil? 178 | nonexistent_topics << topic_map_filepath 179 | end 180 | end 181 | if nonexistent_topics.length > 0 182 | if AsciiBinder::LOG_LEVEL > log_levels[:debug] 183 | log_warn("The #{topic_map_file} file on branch '#{branch}' references #{nonexistent_topics.length} nonexistent topics. Set logging to 'debug' for details.") 184 | else 185 | log_warn("The #{topic_map_file} file on branch '#{branch}' references nonexistent topics:\n" + nonexistent_topics.map{ |topic| "- #{topic}" }.join("\n")) 186 | end 187 | end 188 | end 189 | 190 | def distro_map 191 | @distro_map ||= begin 192 | distro_map_file = File.join(docs_root_dir, DISTRO_MAP_FILENAME) 193 | distro_map = AsciiBinder::DistroMap.new(distro_map_file) 194 | unless distro_map.is_valid? 195 | errors = distro_map.errors 196 | Trollop::die "The distro map file at '#{distro_map_file}' contains the following errors:\n- " + errors.join("\n- ") + "\n" 197 | end 198 | distro_map 199 | end 200 | end 201 | 202 | def site_map 203 | @site_map ||= AsciiBinder::SiteMap.new(distro_map) 204 | end 205 | 206 | def branch_group_branches 207 | @branch_group_branches ||= begin 208 | group_branches = Hash.new 209 | group_branches[:working_only] = [local_branches[0]] 210 | group_branches[:publish] = distro_map.distro_branches 211 | site_map.sites.each do |site| 212 | group_branches["publish_#{site.id}".to_sym] = site.branches 213 | end 214 | group_branches[:all] = local_branches 215 | group_branches 216 | end 217 | end 218 | 219 | def page(args) 220 | # TODO: This process of rebuilding the entire nav for every page will not scale well. 221 | # As the doc set increases, we will need to think about refactoring this. 222 | args[:breadcrumb_root], args[:breadcrumb_group], args[:breadcrumb_subgroup], args[:breadcrumb_subsubgroup], args[:breadcrumb_topic] = extract_breadcrumbs(args) 223 | 224 | args[:breadcrumb_subgroup_block] = '' 225 | if args[:breadcrumb_subgroup] 226 | args[:breadcrumb_subgroup_block] = "
  • #{args[:breadcrumb_subgroup]}
  • " 227 | end 228 | 229 | args[:breadcrumb_subsubgroup_block] = '' 230 | if args[:breadcrumb_subsubgroup] 231 | args[:breadcrumb_subsubgroup_block] = "
  • #{args[:breadcrumb_subsubgroup]}
  • " 232 | end 233 | 234 | args[:subtopic_shim] = '../' * (args[:topic_id].split('::').length - 2) 235 | args[:subtopic_shim] = '' if args[:subtopic_shim].nil? 236 | 237 | template_path = File.expand_path("#{docs_root_dir}/_templates/page.html.erb") 238 | template_renderer.render(template_path, args) 239 | end 240 | 241 | def extract_breadcrumbs(args) 242 | breadcrumb_root = breadcrumb_group = breadcrumb_subgroup = breadcrumb_topic = nil 243 | selected_subgroup = selected_subsubgroup = nil 244 | 245 | root_group = args[:navigation].first 246 | selected_group = args[:navigation].detect { |group| group[:id] == args[:group_id] } 247 | selected_subgroup = selected_group[:topics].detect { |subgroup| subgroup[:id] == args[:subgroup_id] } 248 | if selected_subgroup 249 | selected_subsubgroup = selected_subgroup[:topics].detect { |subsubgroup| subsubgroup[:id] == args[:subsubgroup_id] } 250 | end 251 | 252 | offset = 0; 253 | if selected_subgroup 254 | offset = 1 255 | end 256 | if selected_subsubgroup 257 | offset = 2 258 | end 259 | 260 | if root_group 261 | root_topic = root_group[:topics].first 262 | breadcrumb_root = linkify_breadcrumb(root_topic[:path], "#{args[:distro]} #{args[:version]}", offset) if root_topic 263 | end 264 | 265 | if selected_group 266 | group_topic = selected_group[:topics].first 267 | breadcrumb_group = linkify_breadcrumb(group_topic[:path], selected_group[:name], offset) if group_topic 268 | selected_topic = selected_group[:topics].detect { |topic| topic[:id] == args[:topic_id] } 269 | breadcrumb_topic = linkify_breadcrumb(nil, selected_topic[:name], offset) if selected_topic 270 | end 271 | 272 | if selected_subgroup 273 | subgroup_topic = selected_subgroup[:topics].first 274 | breadcrumb_subgroup = linkify_breadcrumb(subgroup_topic[:path], selected_subgroup[:name], offset) if subgroup_topic 275 | 276 | selected_topic = selected_subgroup[:topics].detect { |topic| topic[:id] == args[:topic_id] } 277 | breadcrumb_topic = linkify_breadcrumb(nil, selected_topic[:name], offset) if selected_topic 278 | end 279 | 280 | if selected_subsubgroup 281 | subsubgroup_topic = selected_subsubgroup[:topics].first 282 | breadcrumb_subsubgroup = linkify_breadcrumb(subsubgroup_topic[:path], selected_subsubgroup[:name], offset) if subsubgroup_topic 283 | 284 | selected_topic = selected_subsubgroup[:topics].detect { |topic| topic[:id] == args[:topic_id] } 285 | breadcrumb_topic = linkify_breadcrumb(nil, selected_topic[:name], offset) if selected_topic 286 | end 287 | 288 | 289 | return breadcrumb_root, breadcrumb_group, breadcrumb_subgroup, breadcrumb_subsubgroup, breadcrumb_topic 290 | end 291 | 292 | def linkify_breadcrumb(href, text, offset) 293 | addl_level = '' 294 | if offset == 1 295 | addl_level = '../' 296 | end 297 | if offset == 2 298 | addl_level = '../../' 299 | end 300 | href ? "#{text}" : text 301 | end 302 | 303 | def asciidoctor_page_attrs(more_attrs=[]) 304 | [ 305 | 'source-highlighter=rouge', 306 | 'linkcss!', 307 | 'icons=font', 308 | 'idprefix=', 309 | 'idseparator=-', 310 | 'sectanchors', 311 | 'data-uri', 312 | ].concat(more_attrs) 313 | end 314 | 315 | def generate_docs(branch_group,build_distro,single_page) 316 | # First, test to see if the docs repo has any commits. If the user has just 317 | # run `asciibinder create`, there will be no commits to work from, yet. 318 | if local_branches.empty? 319 | raise "Before you can build the docs, you need at least one commit in your docs repo." 320 | end 321 | 322 | # Make a filepath in list form from the single_page argument 323 | single_page_path = [] 324 | if not single_page.nil? 325 | single_page_path = single_page.split(':')[0].split('/') 326 | single_page_path << single_page.split(':')[1] 327 | log_unknown("Rebuilding '#{single_page_path.join('/')}' on branch '#{working_branch}'.") 328 | end 329 | 330 | if not build_distro == '' 331 | if not distro_map.include_distro_key?(build_distro) 332 | exit 333 | else 334 | log_unknown("Building only the #{distro_map.get_distro(build_distro).name} distribution.") 335 | end 336 | elsif single_page.nil? 337 | log_unknown("Building all distributions.") 338 | end 339 | 340 | # Notify the user of missing local branches 341 | missing_branches = [] 342 | distro_map.distro_branches(build_distro).sort.each do |dbranch| 343 | next if local_branches.include?(dbranch) 344 | missing_branches << dbranch 345 | end 346 | if missing_branches.length > 0 and single_page.nil? 347 | message = "The following branches do not exist in your local git repo:\n" 348 | missing_branches.each do |mbranch| 349 | message << "- #{mbranch}\n" 350 | end 351 | message << "The build will proceed but these branches will not be generated." 352 | log_warn(message) 353 | end 354 | 355 | # Generate all distros for all branches in the indicated branch group 356 | branch_group_branches[branch_group].each do |local_branch| 357 | # Skip known missing branches; this will only come up for the :publish branch group 358 | next if missing_branches.include?(local_branch) 359 | 360 | # Single-page regen only occurs for the working branch 361 | if not local_branch == working_branch 362 | if single_page.nil? 363 | # Checkout the branch 364 | log_unknown("CHANGING TO BRANCH '#{local_branch}'") 365 | git_checkout(local_branch) 366 | else 367 | next 368 | end 369 | end 370 | 371 | # Note the image files checked in to this branch. 372 | branch_image_files = Find.find(docs_root_dir).select{ |path| not path.nil? and (path =~ /.*\.png$/ or path =~ /.*\.png\.cache$/) } 373 | 374 | first_branch = single_page.nil? 375 | 376 | if local_branch =~ /^\(detached from .*\)/ 377 | local_branch = 'detached' 378 | end 379 | 380 | # The branch_orphan_files list starts with the set of all 381 | # .adoc files found in the repo, and will be whittled 382 | # down from there. 383 | branch_orphan_files = find_topic_files 384 | branch_topic_map = topic_map 385 | remove_found_topic_files(local_branch,branch_topic_map,branch_orphan_files) 386 | 387 | if branch_orphan_files.length > 0 and single_page.nil? 388 | if AsciiBinder::LOG_LEVEL > log_levels[:debug] 389 | log_warn("Branch #{local_branch} includes #{branch_orphan_files.length} files that are not referenced in the #{topic_map_file} file. Set logging to 'debug' for details.") 390 | else 391 | log_warn("Branch '#{local_branch}' includes the following .adoc files that are not referenced in the #{topic_map_file} file:\n" + branch_orphan_files.map{ |file| "- #{file}" }.join("\n")) 392 | end 393 | end 394 | 395 | # Run all distros. 396 | distro_map.distros.each do |distro| 397 | if not build_distro == '' 398 | # Only building a single distro; build for all indicated branches, skip the others. 399 | next unless build_distro == distro.id 400 | else 401 | current_distro_branches = distro_map.distro_branches(distro.id) 402 | 403 | # In publish mode we only build "valid" distro-branch combos from the distro map 404 | if branch_group.to_s.start_with?("publish") and not current_distro_branches.include?(local_branch) 405 | next 406 | end 407 | 408 | # In "build all" mode we build every distro on the working branch plus the publish distro-branch combos 409 | if branch_group == :all and not local_branch == working_branch and not current_distro_branches.include?(local_branch) 410 | next 411 | end 412 | end 413 | 414 | # Get the current distro / branch object 415 | branch_config = AsciiBinder::DistroBranch.new('',{ "name" => "Branch Build", "dir" => local_branch },distro) 416 | dev_branch = true 417 | if distro.branch_ids.include?(local_branch) 418 | branch_config = distro.branch(local_branch) 419 | dev_branch = false 420 | end 421 | 422 | if first_branch 423 | log_unknown("Building #{distro.name} for branch '#{local_branch}'") 424 | first_branch = false 425 | end 426 | 427 | # Copy files into the preview area. 428 | [[stylesheet_dir, '*css', branch_config.branch_stylesheet_dir], 429 | [javascript_dir, '*js', branch_config.branch_javascript_dir], 430 | [image_dir, '*', branch_config.branch_image_dir]].each do |dgroup| 431 | src_dir = dgroup[0] 432 | glob = dgroup[1] 433 | tgt_dir = dgroup[2] 434 | if Dir.exist?(src_dir) and not dir_empty?(src_dir) 435 | FileUtils.mkdir_p tgt_dir 436 | FileUtils.cp_r Dir.glob(File.join(src_dir,glob)), tgt_dir 437 | end 438 | end 439 | 440 | # Build the navigation structure for this branch / distro 441 | navigation = branch_topic_map.nav_tree(distro.id) 442 | 443 | # Build the topic files for this branch & distro 444 | process_topic_entity_list(branch_config,single_page_path,navigation,branch_topic_map.list) 445 | end 446 | 447 | # In single-page context, we're done. 448 | if not single_page.nil? 449 | #exit 200 450 | return 451 | end 452 | 453 | # Remove DITAA-generated images 454 | ditaa_image_files = Find.find(docs_root_dir).select{ |path| not path.nil? and not (path =~ /_preview/ or path =~ /_package/) and (path =~ /.*\.png$/ or path =~ /.*\.png\.cache$/) and not branch_image_files.include?(path) } 455 | if not ditaa_image_files.empty? 456 | log_unknown("Removing ditaa-generated files from repo before changing branches.") 457 | ditaa_image_files.each do |dfile| 458 | File.unlink(dfile) 459 | end 460 | end 461 | 462 | if local_branch == working_branch 463 | # We're moving away from the working branch, so save off changed files 464 | git_stash_all 465 | end 466 | end 467 | 468 | # Return to the original branch 469 | git_checkout(working_branch) 470 | 471 | # If necessary, restore temporarily stashed files 472 | git_apply_and_drop 473 | 474 | log_unknown("All builds completed.") 475 | end 476 | 477 | def process_topic_entity_list(branch_config,single_page_path,navigation,topic_entity_list,preview_path='') 478 | # When called from a topic group entity, create the preview dir for that group 479 | Dir.mkdir(preview_path) unless preview_path == '' or File.exists?(preview_path) 480 | 481 | topic_entity_list.each do |topic_entity| 482 | # If this topic entity or any potential subentities are outside of the distro or single-page params, skip it. 483 | next unless topic_entity.include?(branch_config.distro.id,single_page_path) 484 | 485 | if topic_entity.is_group? 486 | preview_path = topic_entity.preview_path(branch_config.distro.id,branch_config.dir) 487 | process_topic_entity_list(branch_config,single_page_path,navigation,topic_entity.subitems,preview_path) 488 | elsif topic_entity.is_topic? 489 | if topic_entity.is_alias? 490 | configure_and_generate_alias(topic_entity,branch_config) 491 | else 492 | if File.exists?(topic_entity.source_path) 493 | if single_page_path.length == 0 494 | log_info(" - #{topic_entity.repo_path}") 495 | end 496 | configure_and_generate_page(topic_entity,branch_config,navigation) 497 | else 498 | log_warn(" - #{topic_entity.repo_path} <= Skipping nonexistent file") 499 | end 500 | end 501 | end 502 | end 503 | end 504 | 505 | def configure_and_generate_alias(topic,branch_config) 506 | distro = branch_config.distro 507 | topic_target = topic.topic_alias 508 | unless valid_url?(topic_target) 509 | topic_target = File.join(branch_config.branch_url_base,topic_target + ".html") 510 | end 511 | topic_text = alias_text(topic_target) 512 | preview_path = topic.preview_path(distro.id,branch_config.dir) 513 | File.write(preview_path,topic_text) 514 | end 515 | 516 | def configure_and_generate_page(topic,branch_config,navigation) 517 | distro = branch_config.distro 518 | # topic_adoc = File.open(topic.source_path,'r').read 519 | 520 | page_attrs = asciidoctor_page_attrs([ 521 | "imagesdir=#{File.join(topic.parent.source_path,'images')}", 522 | branch_config.distro.id, 523 | "product-title=#{branch_config.distro_name}", 524 | "product-version=#{branch_config.name}", 525 | "product-author=#{branch_config.distro_author}", 526 | "repo_path=#{topic.repo_path}", 527 | "allow-uri-read=" 528 | ]) 529 | 530 | File.open topic.source_path, 'r' do |topic_file| 531 | 532 | doc = without_warnings { Asciidoctor.load topic_file, :header_footer => false, :safe => :unsafe, :attributes => page_attrs, :base_dir => "." } 533 | article_title = doc.doctitle || topic.name 534 | 535 | topic_html = doc.render 536 | 537 | # This is logic bridges newer arbitrary-depth-tolerant code to 538 | # older depth-limited code. Truly removing depth limitations will 539 | # require changes to page templates in user docs repos. 540 | breadcrumb = topic.breadcrumb 541 | group_title = breadcrumb[0][:name] 542 | group_id = breadcrumb[0][:id] 543 | topic_title = breadcrumb[-1][:name] 544 | topic_id = breadcrumb[-1][:id] 545 | subgroup_title = nil 546 | subgroup_id = nil 547 | subsubgroup_title = nil 548 | subsubgroup_id = nil 549 | if breadcrumb.length == 3 550 | subgroup_title = breadcrumb[1][:name] 551 | subgroup_id = breadcrumb[1][:id] 552 | end 553 | 554 | if breadcrumb.length == 4 555 | topic_title = breadcrumb[-1][:name] 556 | topic_id = breadcrumb[-1][:id] 557 | subsubgroup_title = breadcrumb[-2][:name] 558 | subsubgroup_id = breadcrumb[-2][:id] 559 | subgroup_title = breadcrumb[-3][:name] 560 | subgroup_id = breadcrumb[-3][:id] 561 | group_title = breadcrumb[-4][:name] 562 | group_id = breadcrumb[-4][:id] 563 | end 564 | 565 | dir_depth = '../' * topic.breadcrumb[-1][:id].split('::').length 566 | dir_depth = '' if dir_depth.nil? 567 | 568 | preview_path = topic.preview_path(distro.id,branch_config.dir) 569 | topic_publish_url = topic.topic_publish_url(distro.site.url,branch_config.dir) 570 | 571 | page_args = { 572 | :distro_key => distro.id, 573 | :distro => branch_config.distro_name, 574 | :branch => branch_config.id, 575 | :site_name => distro.site.name, 576 | :site_url => distro.site.url, 577 | :topic_url => preview_path, 578 | :topic_publish_url => topic_publish_url, 579 | :version => branch_config.name, 580 | :group_title => group_title, 581 | :subgroup_title => subgroup_title, 582 | :subsubgroup_title => subsubgroup_title, 583 | :topic_title => topic_title, 584 | :article_title => article_title, 585 | :content => topic_html, 586 | :navigation => navigation, 587 | :group_id => group_id, 588 | :subgroup_id => subgroup_id, 589 | :subsubgroup_id => subsubgroup_id, 590 | :topic_id => topic_id, 591 | :css_path => "#{dir_depth}#{branch_config.dir}/#{STYLESHEET_DIRNAME}/", 592 | :javascripts_path => "#{dir_depth}#{branch_config.dir}/#{JAVASCRIPT_DIRNAME}/", 593 | :images_path => "#{dir_depth}#{branch_config.dir}/#{IMAGE_DIRNAME}/", 594 | :site_home_path => "#{dir_depth}index.html", 595 | :template_path => template_dir, 596 | :repo_path => topic.repo_path, 597 | } 598 | full_file_text = page(page_args) 599 | 600 | 601 | File.open(preview_path, 'w') { |file| file.write(full_file_text) } 602 | 603 | 604 | # File.write(preview_path,full_file_text) 605 | 606 | end 607 | end 608 | 609 | # package_docs 610 | # This method generates the docs and then organizes them the way they will be arranged 611 | # for the production websites. 612 | def package_docs(package_site) 613 | site_map.sites.each do |site| 614 | next if not package_site == '' and not package_site == site.id 615 | site.distros.each do |distro_id,branches| 616 | branches.each do |branch| 617 | src_dir = File.join(preview_dir,distro_id,branch.dir) 618 | tgt_tdir = branch.dir.split('/') 619 | tgt_tdir.pop 620 | tgt_dir = '' 621 | if tgt_tdir.length > 0 622 | tgt_dir = File.join(package_dir,site.id,tgt_tdir.join('/')) 623 | else 624 | tgt_dir = File.join(package_dir,site.id) 625 | end 626 | next if not File.directory?(src_dir) 627 | FileUtils.mkdir_p(tgt_dir) 628 | FileUtils.cp_r(src_dir,tgt_dir) 629 | end 630 | site_dir = File.join(package_dir,site.id) 631 | if File.directory?(site_dir) 632 | log_unknown("Packaging #{distro_id} for #{site.id} site.") 633 | 634 | # Any files in the root of the docs repo with names ending in: 635 | # *-#{site}.html 636 | # will get copied into the root dir of the packaged site with 637 | # the site name stripped out. 638 | # 639 | # Example: for site name 'commercial', the files: 640 | # * index-commercial.html would end up as #{site_root}/index.html 641 | # * search-commercial.html would end up as #{site_root}/search.html 642 | # * index-community.html would be ignored 643 | site_files = Dir.glob(File.join(docs_root_dir, '*-' + site.id + '.html')) 644 | unless site_files.empty? 645 | site_files.each do |fpath| 646 | target_basename = File.basename(fpath).gsub(/-#{site.id}\.html$/, '.html') 647 | FileUtils.cp(fpath,File.join(package_dir,site.id,target_basename)) 648 | end 649 | else 650 | FileUtils.cp(File.join(preview_dir,distro_id,'index.html'),File.join(package_dir,site.id,'index.html')) 651 | end 652 | ['_images','_stylesheets'].each do |support_dir| 653 | FileUtils.cp_r(File.join(docs_root_dir,support_dir),File.join(package_dir,site.id,support_dir)) 654 | end 655 | 656 | # Now build a sitemap 657 | site_dir_path = Pathname.new(site_dir) 658 | SitemapGenerator::Sitemap.create( 659 | :default_host => site.url, 660 | :public_path => site_dir_path, 661 | :compress => false, 662 | :filename => File.join(site_dir,'sitemap') 663 | ) do 664 | file_list = Find.find(site_dir).select{ |path| not path.nil? and path =~ /.*\.html$/ }.map{ |path| '/' + Pathname.new(path).relative_path_from(site_dir_path).to_s } 665 | file_list.each do |file| 666 | add(file, :changefreq => 'daily') 667 | end 668 | end 669 | end 670 | end 671 | end 672 | end 673 | 674 | def clean_up 675 | if not system("rm -rf #{docs_root_dir}/_preview/* #{docs_root_dir}/_package/*") 676 | log_unknown("Nothing to clean.") 677 | end 678 | end 679 | end 680 | end 681 | -------------------------------------------------------------------------------- /lib/ascii_binder/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'stringio' 3 | 4 | module AsciiBinder 5 | module Helpers 6 | BUILD_FILENAME = '_build_cfg.yml' 7 | TOPIC_MAP_FOLDER = '_topic_maps' 8 | TOPIC_MAP_FILENAME = '_topic_map.yml' 9 | DISTRO_MAP_FILENAME = '_distro_map.yml' 10 | PREVIEW_DIRNAME = '_preview' 11 | PACKAGE_DIRNAME = '_package' 12 | STYLESHEET_DIRNAME = '_stylesheets' 13 | JAVASCRIPT_DIRNAME = '_javascripts' 14 | IMAGE_DIRNAME = '_images' 15 | BLANK_STRING_RE = Regexp.new('^\s*$') 16 | ID_STRING_RE = Regexp.new('^[A-Za-z0-9\-\_]+$') 17 | URL_STRING_RE = Regexp.new('^https?:\/\/[\S]+$') 18 | 19 | def valid_id?(check_id) 20 | return false unless check_id.is_a?(String) 21 | return false unless check_id.match ID_STRING_RE 22 | return true 23 | end 24 | 25 | def valid_string?(check_string) 26 | return false unless check_string.is_a?(String) 27 | return false unless check_string.length > 0 28 | return false if check_string.match BLANK_STRING_RE 29 | return true 30 | end 31 | 32 | def valid_url?(check_string) 33 | return false unless valid_string?(check_string) 34 | return false unless check_string.match URL_STRING_RE 35 | return true 36 | end 37 | 38 | def camelize(text) 39 | text.gsub(/[^0-9a-zA-Z ]/i, '').split(' ').map{ |t| t.capitalize }.join 40 | end 41 | 42 | def git_root_dir 43 | @git_root_dir ||= `git rev-parse --show-toplevel`.chomp 44 | end 45 | 46 | def gem_root_dir 47 | @gem_root_dir ||= File.expand_path("../../../", __FILE__) 48 | end 49 | 50 | def set_docs_root_dir(docs_root_dir) 51 | AsciiBinder.const_set("DOCS_ROOT_DIR", docs_root_dir) 52 | end 53 | 54 | def docs_root_dir 55 | AsciiBinder::DOCS_ROOT_DIR 56 | end 57 | 58 | def set_depth(user_depth) 59 | AsciiBinder.const_set("DEPTH", user_depth) 60 | end 61 | 62 | def set_log_level(user_log_level) 63 | AsciiBinder.const_set("LOG_LEVEL", log_levels[user_log_level]) 64 | end 65 | 66 | def log_levels 67 | @log_levels ||= { 68 | :debug => Logger::DEBUG.to_i, 69 | :error => Logger::ERROR.to_i, 70 | :fatal => Logger::FATAL.to_i, 71 | :info => Logger::INFO.to_i, 72 | :warn => Logger::WARN.to_i, 73 | } 74 | end 75 | 76 | def logerr 77 | @logerr ||= begin 78 | logger = Logger.new(STDERR, level: AsciiBinder::LOG_LEVEL) 79 | logger.formatter = proc do |severity, datetime, progname, msg| 80 | "#{severity}: #{msg}\n" 81 | end 82 | logger 83 | end 84 | end 85 | 86 | def logstd 87 | @logstd ||= begin 88 | logger = Logger.new(STDOUT, level: AsciiBinder::LOG_LEVEL) 89 | logger.formatter = proc do |severity, datetime, progname, msg| 90 | severity == 'ANY' ? "#{msg}\n" : "#{severity}: #{msg}\n" 91 | end 92 | logger 93 | end 94 | end 95 | 96 | def log_info(text) 97 | logstd.info(text) 98 | end 99 | 100 | def log_warn(text) 101 | logstd.warn(text) 102 | end 103 | 104 | def log_error(text) 105 | logerr.error(text) 106 | end 107 | 108 | def log_fatal(text) 109 | logerr.fatal(text) 110 | end 111 | 112 | def log_debug(text) 113 | logstd.debug(text) 114 | end 115 | 116 | def log_unknown(text) 117 | logstd.unknown(text) 118 | end 119 | 120 | def without_warnings 121 | verboseness_level = $VERBOSE 122 | $VERBOSE = nil 123 | yield 124 | ensure 125 | $VERBOSE = verboseness_level 126 | end 127 | 128 | def template_renderer 129 | @template_renderer ||= TemplateRenderer.new(docs_root_dir, template_dir) 130 | end 131 | 132 | def template_dir 133 | @template_dir ||= File.join(docs_root_dir,'_templates') 134 | end 135 | 136 | def preview_dir 137 | @preview_dir ||= begin 138 | lpreview_dir = File.join(docs_root_dir,PREVIEW_DIRNAME) 139 | if not File.exists?(lpreview_dir) 140 | Dir.mkdir(lpreview_dir) 141 | end 142 | lpreview_dir 143 | end 144 | end 145 | 146 | def package_dir 147 | @package_dir ||= begin 148 | lpackage_dir = File.join(docs_root_dir,PACKAGE_DIRNAME) 149 | if not File.exists?(lpackage_dir) 150 | Dir.mkdir(lpackage_dir) 151 | end 152 | lpackage_dir 153 | end 154 | end 155 | 156 | def stylesheet_dir 157 | @stylesheet_dir ||= File.join(docs_root_dir,STYLESHEET_DIRNAME) 158 | end 159 | 160 | def javascript_dir 161 | @javascript_dir ||= File.join(docs_root_dir,JAVASCRIPT_DIRNAME) 162 | end 163 | 164 | def image_dir 165 | @image_dir ||= File.join(docs_root_dir,IMAGE_DIRNAME) 166 | end 167 | 168 | def alias_text(target) 169 | "#{target}" 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /lib/ascii_binder/site.rb: -------------------------------------------------------------------------------- 1 | require 'ascii_binder/helpers' 2 | 3 | include AsciiBinder::Helpers 4 | 5 | module AsciiBinder 6 | class Site 7 | attr_reader :id, :name, :url 8 | 9 | def initialize(distro_config) 10 | @id = distro_config['site'] 11 | @name = distro_config['site_name'] 12 | @url = distro_config['site_url'] 13 | end 14 | 15 | def is_valid? 16 | validate 17 | end 18 | 19 | def errors 20 | validate(true) 21 | end 22 | 23 | private 24 | 25 | def validate(verbose=false) 26 | errors = [] 27 | unless valid_id?(@id) 28 | if verbose 29 | errors << "Site ID '#{@id}' is not a valid ID." 30 | else 31 | return false 32 | end 33 | end 34 | unless valid_string?(@name) 35 | if verbose 36 | errors << "Site name '#{@name}' for site ID '#{@id}' is not a valid string." 37 | else 38 | return false 39 | end 40 | end 41 | unless valid_string?(@url) 42 | if verbose 43 | errors << "Site URL '#{@url}' for site ID '#{@id}' is not a valid string." 44 | else 45 | return false 46 | end 47 | end 48 | return errors if verbose 49 | return true 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/ascii_binder/site_info.rb: -------------------------------------------------------------------------------- 1 | module AsciiBinder 2 | class SiteInfo 3 | attr_reader :id, :name, :url, :distros, :branches 4 | 5 | def initialize(distro) 6 | @id = distro.site.id 7 | @name = distro.site.name 8 | @url = distro.site.url 9 | @distros = {} 10 | @branches = ['main'] 11 | add_distro(distro) 12 | end 13 | 14 | def add_distro(distro) 15 | @distros[distro.id] = distro.branches 16 | distro.branches.each do |branch| 17 | next if @branches.include?(branch.id) 18 | @branches << branch.id 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/ascii_binder/site_map.rb: -------------------------------------------------------------------------------- 1 | require 'ascii_binder/site_info' 2 | 3 | module AsciiBinder 4 | class SiteMap 5 | def initialize(distro_map) 6 | @site_map = {} 7 | distro_map.distros.each do |distro| 8 | unless @site_map.has_key?(distro.site.id) 9 | @site_map[distro.site.id] = AsciiBinder::SiteInfo.new(distro) 10 | else 11 | @site_map[distro.site.id].add_distro(distro) 12 | end 13 | end 14 | end 15 | 16 | def sites 17 | return @site_map.values 18 | end 19 | 20 | def ids 21 | return @site_map.keys 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/ascii_binder/tasks/guards.rb: -------------------------------------------------------------------------------- 1 | guard 'shell' do 2 | watch(/^.*\.adoc$/) { |m| 3 | if not m[0].start_with?('_preview') and not m[0].start_with?('_package') 4 | full_path = m[0].split('/') 5 | src_group_path = full_path.length == 1 ? '' : full_path[0..-2].join('/') 6 | filename = full_path[-1][0..-6] 7 | system("asciibinder build --page='#{src_group_path}:#{filename}'") 8 | end 9 | } 10 | end 11 | 12 | guard 'livereload' do 13 | watch(%r{^_preview/.+\.(css|js|html)$}) 14 | watch(%r{^_preview/.+\/.+\/.+\.(css|js|html)$}) 15 | end 16 | -------------------------------------------------------------------------------- /lib/ascii_binder/tasks/tasks.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'ascii_binder' 3 | 4 | include AsciiBinder::Engine 5 | include AsciiBinder::Helpers 6 | 7 | desc "Build the documentation" 8 | task :build, :build_distro do |task,args| 9 | # Figure out which distros we are building. 10 | # A blank value here == all distros 11 | set_docs_root_dir(git_root_dir) 12 | set_log_level(:warn) 13 | build_distro = args[:build_distro] || '' 14 | generate_docs(:all,build_distro,nil) 15 | end 16 | 17 | desc "Package the documentation" 18 | task :package, :package_site do |task,args| 19 | set_docs_root_dir(git_root_dir) 20 | set_log_level(:warn) 21 | package_site = args[:package_site] || '' 22 | Rake::Task["clean"].invoke 23 | Rake::Task["build"].invoke 24 | package_docs(package_site) 25 | end 26 | 27 | desc "Build the documentation and refresh the page" 28 | task :refresh_page, :single_page do |task,args| 29 | set_docs_root_dir(git_root_dir) 30 | set_log_level(:warn) 31 | generate_docs(:working_only,'',args[:single_page]) 32 | end 33 | 34 | desc "Clean all build artifacts" 35 | task :clean do 36 | sh "rm -rf _preview/* _package/*" do |ok,res| 37 | if ! ok 38 | puts "Nothing to clean." 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/ascii_binder/template_renderer.rb: -------------------------------------------------------------------------------- 1 | require 'tilt' 2 | 3 | module AsciiBinder 4 | class TemplateRenderer 5 | attr_reader :source_dir, :template_cache 6 | 7 | def initialize(source_dir,template_directory) 8 | @source_dir = source_dir 9 | @template_cache = {} 10 | Dir.glob(File.join(template_directory, "**/*")).each do |file| 11 | @template_cache[file] = Tilt.new(file, :trim => "-") 12 | end 13 | end 14 | 15 | def render(template, args = {}) 16 | # Inside erb files, template path is local to repo 17 | if not template.start_with?(source_dir) 18 | template = File.join(source_dir, template) 19 | end 20 | renderer_for(template).render(self, args).chomp 21 | end 22 | 23 | private 24 | 25 | def renderer_for(template) 26 | template_cache.fetch(File.expand_path(template)) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/ascii_binder/topic_entity.rb: -------------------------------------------------------------------------------- 1 | require 'ascii_binder/helpers' 2 | require 'trollop' 3 | 4 | include AsciiBinder::Helpers 5 | 6 | module AsciiBinder 7 | class TopicEntity 8 | attr_reader :name, :dir, :file, :topic_alias, :distro_keys, :subitems, :raw, :parent, :depth 9 | 10 | def initialize(topic_entity,actual_distro_keys,dir_path='',parent_group=nil,depth=0) 11 | @raw = topic_entity 12 | @parent = parent_group 13 | @dir_path = dir_path 14 | @name = topic_entity['Name'] 15 | @dir = topic_entity['Dir'] 16 | @file = topic_entity['File'] 17 | @topic_alias = topic_entity['Alias'] 18 | @depth = depth 19 | @actual_distro_keys = actual_distro_keys 20 | @distro_keys = topic_entity.has_key?('Distros') ? parse_distros(topic_entity['Distros']) : actual_distro_keys 21 | @nav_trees = {} 22 | @alias_lists = {} 23 | @path_lists = {} 24 | @subitems = [] 25 | if topic_entity.has_key?('Topics') 26 | entity_dir = @dir.nil? ? '' : @dir 27 | subdir_path = dir_path == '' ? entity_dir : File.join(dir_path,entity_dir) 28 | topic_entity['Topics'].each do |sub_entity| 29 | @subitems << AsciiBinder::TopicEntity.new(sub_entity,actual_distro_keys,subdir_path,self,depth+1) 30 | end 31 | end 32 | end 33 | 34 | def repo_path 35 | @repo_path ||= begin 36 | this_step = '' 37 | if is_group? 38 | this_step = dir 39 | elsif is_topic? 40 | this_step = file.end_with?('.adoc') ? file : "#{file}.adoc" 41 | end 42 | @dir_path == '' ? this_step : File.join(@dir_path,this_step) 43 | end 44 | end 45 | 46 | def basename_path 47 | @basename_path ||= File.join(File.dirname(repo_path),File.basename(repo_path,'.adoc')) 48 | end 49 | 50 | def repo_path_html 51 | @repo_path_html ||= is_topic? ? File.join(File.dirname(repo_path),File.basename(repo_path,'.adoc')) + ".html" : repo_path 52 | end 53 | 54 | def source_path 55 | @source_path ||= File.join(docs_root_dir,repo_path) 56 | end 57 | 58 | def preview_path(distro_key,branch_dir) 59 | File.join(preview_dir,distro_key,branch_dir,repo_path_html) 60 | end 61 | 62 | def topic_publish_url(distro_url,branch_dir) 63 | File.join(distro_url,branch_dir,repo_path_html) 64 | end 65 | 66 | def package_path(site_id,branch_dir) 67 | File.join(package_dir,site_id,branch_dir,repo_path_html) 68 | end 69 | 70 | def group_filepaths 71 | @group_filepaths ||= begin 72 | group_filepaths = [] 73 | if is_topic? and not is_alias? 74 | group_filepaths << File.join(File.dirname(repo_path),File.basename(repo_path,'.adoc')) 75 | elsif is_group? 76 | subitems.each do |subitem| 77 | group_filepaths.concat(subitem.group_filepaths) 78 | end 79 | group_filepaths.uniq! 80 | end 81 | group_filepaths 82 | end 83 | end 84 | 85 | def nav_tree(distro_key) 86 | @nav_trees[distro_key] ||= begin 87 | nav_tree = {} 88 | if distro_keys.include?(distro_key) and not is_alias? 89 | nav_tree[:id] = id 90 | nav_tree[:name] = name 91 | if is_topic? 92 | nav_tree[:path] = "../" + repo_path_html 93 | elsif is_group? 94 | sub_nav_items = [] 95 | subitems.each do |subitem| 96 | sub_nav = subitem.nav_tree(distro_key) 97 | next if sub_nav.empty? 98 | sub_nav_items << sub_nav 99 | end 100 | if sub_nav_items.empty? 101 | nav_tree = {} 102 | else 103 | nav_tree[:topics] = sub_nav_items 104 | end 105 | end 106 | end 107 | nav_tree 108 | end 109 | end 110 | 111 | def alias_list(distro_key) 112 | @alias_lists[distro_key] ||= begin 113 | sub_aliases = [] 114 | if distro_keys.include?(distro_key) 115 | if is_group? 116 | subitems.each do |subitem| 117 | sub_list = subitem.alias_list(distro_key) 118 | sub_list.each do |sub_list_alias| 119 | sub_aliases << sub_list_alias 120 | end 121 | end 122 | elsif is_alias? 123 | sub_aliases << { :alias_path => basename_path, :redirect_path => topic_alias } 124 | end 125 | end 126 | sub_aliases 127 | end 128 | end 129 | 130 | def path_list(distro_key) 131 | @path_lists[distro_key] ||= begin 132 | sub_paths = [] 133 | if distro_keys.include?(distro_key) 134 | if is_group? 135 | subitems.each do |subitem| 136 | sub_list = subitem.path_list(distro_key) 137 | sub_list.each do |sub_list_path| 138 | sub_paths << sub_list_path 139 | end 140 | end 141 | elsif is_topic? and not is_alias? 142 | sub_paths << basename_path 143 | end 144 | end 145 | sub_paths 146 | end 147 | end 148 | 149 | # Is this topic entity or any of its children used in 150 | # the specified distro / single page chain 151 | def include?(distro_key,single_page_path) 152 | # If this entity isn't for this distro, bail out 153 | return false unless distro_keys.include?(distro_key) 154 | 155 | # If we're building a single page, check if we're on the right track. 156 | if single_page_path.length > 0 and not single_page_path[depth].nil? 157 | if is_group? 158 | return false unless single_page_path[depth] == dir 159 | elsif is_topic? 160 | return false unless single_page_path[depth] == file 161 | else 162 | return false 163 | end 164 | elsif is_group? 165 | # If this is a topic group that -is- okay for this distro, but 166 | # none of its subitems are okay for this distro, then bail out. 167 | subitems_for_distro = false 168 | subitems.each do |subitem| 169 | if subitem.include?(distro_key,[]) 170 | subitems_for_distro = true 171 | break 172 | end 173 | end 174 | return false unless subitems_for_distro 175 | end 176 | 177 | return true 178 | end 179 | 180 | def breadcrumb 181 | @breadcrumb ||= hierarchy.map{ |entity| { :id => entity.id, :name => entity.name, :url => entity.repo_path_html } } 182 | end 183 | 184 | def id 185 | @id ||= hierarchy.map{ |entity| camelize(entity.name) }.join('::') 186 | end 187 | 188 | def is_group? 189 | @is_group ||= file.nil? and not name.nil? and not dir.nil? and subitems.length > 0 190 | end 191 | 192 | def is_topic? 193 | @is_topic ||= dir.nil? and not name.nil? and not file.nil? and subitems.length == 0 194 | end 195 | 196 | def is_alias? 197 | @is_alias ||= is_topic? and not topic_alias.nil? 198 | end 199 | 200 | def is_valid? 201 | validate 202 | end 203 | 204 | def errors 205 | validate(true) 206 | end 207 | 208 | private 209 | 210 | def parse_distros(entity_distros) 211 | values = entity_distros.split(',').map(&:strip) 212 | # Don't bother with glob expansion if 'all' is in the list. 213 | return @actual_distro_keys if values.include?('all') 214 | 215 | # Expand globs and return the list 216 | values.flat_map do |value| 217 | value_regex = Regexp.new("\\A#{value.gsub("*", ".*")}\\z") 218 | @actual_distro_keys.select { |k| value_regex.match(k) } 219 | end.uniq 220 | end 221 | 222 | def validate(verbose=false) 223 | errors = [] 224 | 225 | # Check common fields - Name and Distros 226 | if not valid_string?(name) 227 | if verbose 228 | errors << "Topic entity with missing or invalid 'Name' value: '#{raw.inspect}'" 229 | else 230 | return false 231 | end 232 | end 233 | distro_keys.each do |distro_key| 234 | next if @actual_distro_keys.include?(distro_key) 235 | if verbose 236 | errors << "#{entity_id} 'Distros' filter includes nonexistent distro key '#{distro_key}'" 237 | else 238 | return false 239 | end 240 | end 241 | 242 | # Check the depth. 243 | if (depth > AsciiBinder::DEPTH) and (AsciiBinder::DEPTH != 0) 244 | if verbose 245 | errors << "#{entity_id} exceeds the maximum nested depth." 246 | else 247 | return false 248 | end 249 | end 250 | 251 | # For groups, test the 'Dir' value and the sub-items. 252 | if is_group? 253 | if not valid_string?(dir) 254 | if verbose 255 | errors << "#{entity_id} has invalid 'Dir' value." 256 | else 257 | return false 258 | end 259 | end 260 | if not topic_alias.nil? 261 | if verbose 262 | errors << "#{entity_id} is a topic group with an Alias entry. Aliases are only supported for topic items." 263 | else 264 | return false 265 | end 266 | end 267 | subitems.each do |subitem| 268 | next if subitem.is_valid? 269 | if verbose 270 | errors = errors.concat(subitem.errors) 271 | else 272 | return false 273 | end 274 | end 275 | elsif is_topic? 276 | if not valid_string?(file) 277 | if verbose 278 | errors << "#{entity_id} has invalid 'File' value." 279 | else 280 | return false 281 | end 282 | end 283 | # We can do basic validation of the 'Alias' string here, but real validation has 284 | # to be done after the whole topic map is loaded. 285 | if not topic_alias.nil? and not valid_string?(topic_alias) 286 | if verbose 287 | errors << "#{entity_id} has invalid 'Alias' value." 288 | else 289 | return false 290 | end 291 | end 292 | else 293 | if verbose 294 | errors << "#{entity_id} is not parseable as a group or a topic: '#{raw.inspect}'" 295 | else 296 | return false 297 | end 298 | end 299 | return errors if verbose 300 | return true 301 | end 302 | 303 | def hierarchy 304 | @hierarchy ||= begin 305 | entity = self 306 | ancestry = [] 307 | loop do 308 | ancestry << entity 309 | break if entity.parent.nil? 310 | entity = entity.parent 311 | end 312 | ancestry.reverse 313 | end 314 | end 315 | 316 | def entity_id 317 | if hierarchy.length == 1 318 | return "Top level topic entity '#{name}'" 319 | else 320 | return "Topic entity at '#{breadcrumb.map{ |node| node[:name] }.join(' -> ')}'" 321 | end 322 | end 323 | end 324 | end 325 | -------------------------------------------------------------------------------- /lib/ascii_binder/topic_map.rb: -------------------------------------------------------------------------------- 1 | require 'ascii_binder/helpers' 2 | require 'ascii_binder/topic_entity' 3 | require 'trollop' 4 | require 'yaml' 5 | 6 | include AsciiBinder::Helpers 7 | 8 | module AsciiBinder 9 | class TopicMap 10 | attr_reader :list 11 | 12 | def initialize(topic_file,distro_keys) 13 | @topic_yaml = YAML.load_stream(open(topic_file)) 14 | @distro_keys = distro_keys 15 | @list = [] 16 | @topic_yaml.each do |topic_entity| 17 | @list << AsciiBinder::TopicEntity.new(topic_entity,distro_keys) 18 | end 19 | 20 | end 21 | 22 | def filepaths 23 | @filepaths ||= begin 24 | filepaths = [] 25 | @list.each do |topic_entity| 26 | filepaths.concat(topic_entity.group_filepaths) 27 | end 28 | filepaths 29 | end 30 | end 31 | 32 | def nav_tree(distro_key) 33 | nav_tree = [] 34 | @list.each do |topic_entity| 35 | entity_nav = topic_entity.nav_tree(distro_key) 36 | next if entity_nav.empty? 37 | nav_tree << entity_nav 38 | end 39 | return nav_tree 40 | end 41 | 42 | def alias_list(distro_key) 43 | alias_list = [] 44 | @list.each do |topic_entity| 45 | alias_sublist = topic_entity.alias_list(distro_key) 46 | next if alias_sublist.empty? 47 | alias_list.push(*alias_sublist) 48 | end 49 | return alias_list 50 | end 51 | 52 | def path_list(distro_key) 53 | path_list = [] 54 | @list.each do |topic_entity| 55 | path_sublist = topic_entity.path_list(distro_key) 56 | next if path_sublist.empty? 57 | path_list.push(*path_sublist) 58 | end 59 | return path_list 60 | end 61 | 62 | def is_valid? 63 | @list.each do |topic_entity| 64 | next if topic_entity.is_valid? and topic_entity.is_group? 65 | return false 66 | end 67 | # Test all aliases 68 | @distro_keys.each do |distro_key| 69 | distro_aliases = alias_list(distro_key) 70 | distro_paths = path_list(distro_key) 71 | distro_aliases.each do |alias_map| 72 | return false if distro_paths.include?(alias_map[:alias_path]) 73 | next if valid_url?(alias_map[:redirect_path]) 74 | return false unless distro_paths.include?(alias_map[:redirect_path]) 75 | end 76 | end 77 | return true 78 | end 79 | 80 | def errors 81 | errors = [] 82 | @list.each do |topic_entity| 83 | if not topic_entity.is_group? 84 | errors << "Top-level entries in the topic map must all be topic groups. Entity with name '#{topic_entity.name}' is not a group." 85 | next 86 | end 87 | next if topic_entity.is_valid? 88 | errors << topic_entity.errors 89 | end 90 | # Test all aliases 91 | @distro_keys.each do |distro_key| 92 | distro_aliases = alias_list(distro_key) 93 | distro_paths = path_list(distro_key) 94 | distro_aliases.each do |alias_map| 95 | if distro_paths.include?(alias_map[:alias_path]) 96 | errors << "An actual topic file and a topic alias both exist at the same path '#{alias_map[:alias_path]}' for distro '#{distro_key}'" 97 | end 98 | next if valid_url?(alias_map[:redirect_path]) 99 | if not distro_paths.include?(alias_map[:redirect_path]) 100 | errors << "Topic alias '#{alias_map[:alias_path]}' points to a nonexistent topic '#{alias_map[:redirect_path]}' for distro '#{distro_key}'" 101 | end 102 | end 103 | end 104 | return errors 105 | end 106 | 107 | private 108 | 109 | def validate_alias(topic_entity) 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/ascii_binder/version.rb: -------------------------------------------------------------------------------- 1 | module AsciiBinder 2 | VERSION = "1.2" 3 | end 4 | -------------------------------------------------------------------------------- /templates/.gitignore: -------------------------------------------------------------------------------- 1 | ## AsciiBinder-specific ignores 2 | _preview 3 | _package 4 | *.swp 5 | diag-*.png 6 | diag-*.png.cache 7 | 8 | ## Project-specific ignores 9 | 10 | -------------------------------------------------------------------------------- /templates/LICENSE.txt: -------------------------------------------------------------------------------- 1 | This work is licensed under the Creative Commons Attribution 4.0 International 2 | License. To view a copy of this license, visit 3 | http://creativecommons.org/licenses/by/4.0/ or send a letter to Creative 4 | Commons, PO Box 1866, Mountain View, CA 94042, USA. 5 | -------------------------------------------------------------------------------- /templates/README.adoc: -------------------------------------------------------------------------------- 1 | = AsciiBinder-Based Docs Repo 2 | 3 | This docs repository was created with http://asciibinder.org/[AsciiBinder]. For more information on how AsciiBinder works, check out the http://asciibinder.org/latest/[documentation]. 4 | 5 | == License 6 | 7 | image:http://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by.svg["Creative Commons License",height=31px,width=88px] 8 | 9 | This work is licensed under a http://creativecommons.org/licenses/by/4.0/[Creative Commons Attribution 4.0 International License]. 10 | 11 | See link:LICENSE.txt[LICENSE.txt]. 12 | -------------------------------------------------------------------------------- /templates/_distro_map.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ascii_binder: 3 | name: AsciiBinder Doc Project 4 | author: AsciiBinder Team 5 | site: main 6 | site_name: Home 7 | site_url: http://asciibinder.org/ 8 | branches: 9 | master: 10 | name: Latest 11 | dir: latest 12 | -------------------------------------------------------------------------------- /templates/_images/asciibinder-logo-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhataccess/ascii_binder/687f8a5fef233c9bb20a098505d32998f54ff4f3/templates/_images/asciibinder-logo-horizontal.png -------------------------------------------------------------------------------- /templates/_images/asciibinder_web_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 63 | 68 | 73 | 78 | 83 | 88 | 93 | 98 | 103 | 108 | 113 | 118 | 119 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /templates/_images/book_pages_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhataccess/ascii_binder/687f8a5fef233c9bb20a098505d32998f54ff4f3/templates/_images/book_pages_bg.jpg -------------------------------------------------------------------------------- /templates/_images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhataccess/ascii_binder/687f8a5fef233c9bb20a098505d32998f54ff4f3/templates/_images/favicon.ico -------------------------------------------------------------------------------- /templates/_images/favicon32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhataccess/ascii_binder/687f8a5fef233c9bb20a098505d32998f54ff4f3/templates/_images/favicon32x32.png -------------------------------------------------------------------------------- /templates/_javascripts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhataccess/ascii_binder/687f8a5fef233c9bb20a098505d32998f54ff4f3/templates/_javascripts/.gitkeep -------------------------------------------------------------------------------- /templates/_javascripts/bootstrap-offcanvas.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $('[data-toggle="offcanvas"]').click(function () { 3 | $('.sidebar').show(); 4 | $('.row-offcanvas').toggleClass('active'); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /templates/_stylesheets/asciibinder.css: -------------------------------------------------------------------------------- 1 | @import url(http://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css); 2 | /* ------------------------------------------------------------ 3 | Image: "Spin" https://www.flickr.com/photos/eflon/3655695161/ 4 | Author: eflon https://www.flickr.com/photos/eflon/ 5 | License: https://creativecommons.org/licenses/by/2.0/ 6 | ---------------------------------------------------------------*/ 7 | .attribution { 8 | text-align: center; 9 | position: relative; 10 | bottom: -20px; 11 | } 12 | .attribution .btn { 13 | color: #808080; 14 | color: rgba(175,175,175, .65); 15 | font-size: 11px; 16 | } 17 | .attribution .btn:hover { 18 | text-decoration: none; 19 | color: #aaa; 20 | } 21 | .popover-content { 22 | font-size: 12px; 23 | line-height: 1.3; 24 | font-weight: normal; 25 | } 26 | 27 | @media screen and (max-width: 980px) { 28 | body { 29 | margin-bottom: 200px; 30 | } 31 | footer { 32 | text-align: center; 33 | } 34 | footer .text-right { 35 | text-align: center !important; 36 | } 37 | #footer_social .first { 38 | margin-left: 0; 39 | } 40 | #footer_social > a { 41 | top: 24px; 42 | } 43 | } 44 | 45 | .fa-inverse:hover { 46 | color: #ccc; 47 | } 48 | 49 | .collapse a.active { 50 | background-color: #DEEAF4; 51 | color: #000; 52 | position: relative; 53 | } 54 | 55 | .collapse a.active:hover { 56 | text-decoration: none; 57 | } 58 | 59 | .collapse a.active:before { 60 | background-color: #A0C3E5; 61 | content: ""; 62 | display: inline-block; 63 | height: 100%; 64 | left: 0; 65 | position: absolute; 66 | top: 0; 67 | width: 3px; 68 | } 69 | 70 | .main h2, .main .h2 { 71 | border-top: 0px; 72 | padding-top: 10px; 73 | } 74 | 75 | .page-header { 76 | height: 100% !important; 77 | } 78 | 79 | .page-header h2{ 80 | font-size: 28px; 81 | } 82 | 83 | .navbar-brand { 84 | padding: initial; 85 | height: initial; 86 | } 87 | 88 | .nav > li > a.hover{ 89 | background-color: none; 90 | } 91 | 92 | h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { 93 | position: relative; 94 | } 95 | 96 | h2 > a.anchor, h3 > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { 97 | display: block; 98 | font-weight: normal; 99 | margin-left: -1.5ex; 100 | position: absolute; 101 | text-align: center; 102 | text-decoration: none !important; 103 | visibility: hidden; 104 | width: 1.5ex; 105 | z-index: 1001; 106 | } 107 | 108 | h2 > a.anchor:before, h3 > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { 109 | content: "\f0c1"; 110 | display: block; 111 | font-family: FontAwesome; 112 | font-size: 0.7em; 113 | -webkit-font-smoothing: antialiased; 114 | -moz-osx-font-smoothing: grayscale; 115 | padding-top: 0.2em; 116 | } 117 | 118 | h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { 119 | font-size: 1em; 120 | } 121 | 122 | h2:hover > a.anchor, 123 | h2 > a.anchor:hover, 124 | h3:hover > a.anchor, 125 | h3 > a.anchor:hover, 126 | h4:hover > a.anchor, 127 | h4 > a.anchor:hover, 128 | h5:hover > a.anchor, 129 | h5 > a.anchor:hover, 130 | h6:hover > a.anchor, 131 | h6 > a.anchor:hover { 132 | visibility: visible; 133 | } 134 | 135 | .main { 136 | border-left: 1px solid #e7e7e7; 137 | margin-left: -1px; 138 | padding-left: 25px; 139 | } 140 | 141 | 142 | @media (min-width: 768px) { 143 | .main { 144 | padding-left: 30px; 145 | } 146 | } 147 | 148 | /* 149 | * Sidebar 150 | */ 151 | 152 | .nav-header { 153 | font-size: 16px; 154 | } 155 | 156 | .nav-header ul { 157 | font-size: 14px; 158 | } 159 | 160 | .nav-header ul li a { 161 | display: block; 162 | padding: 5px 20px 5px 25px; 163 | font-size: 13px; 164 | font-weight: normal; 165 | } 166 | 167 | .nav-sidebar .fa { 168 | text-align: center; 169 | top: -1px; 170 | width: 14px; 171 | } 172 | 173 | .nav-sidebar li a { 174 | color: inherit; 175 | } 176 | 177 | .nav-sidebar li a:hover { 178 | color: #000; 179 | } 180 | 181 | .nav-sidebar ul li ul.nav-tertiary li a { 182 | padding-left: 50px; 183 | } 184 | 185 | .nav-sidebar > li > a { 186 | padding: 7px 0; 187 | } 188 | 189 | .nav-sidebar > li > a:focus, .nav-sidebar > li > a:hover { 190 | background: transparent; 191 | } 192 | 193 | .sidebar { 194 | font-weight: 300; 195 | display: none; 196 | padding-top: 13px; 197 | } 198 | 199 | @media screen and (max-width: 767px) { 200 | .sidebar { 201 | padding-left: 30px; 202 | padding-right: 0; 203 | } 204 | } 205 | 206 | @media screen and (min-width: 768px) { 207 | .sidebar { 208 | border-right: 1px solid #e7e7e7; 209 | display: block; 210 | } 211 | } 212 | 213 | /* 214 | * Off Canvas 215 | * -------------------------------------------------- 216 | */ 217 | 218 | body, html { 219 | overflow-x: hidden; /* Prevent scroll on narrow devices */ 220 | } 221 | 222 | .toggle-nav { 223 | margin-right: 20px; 224 | } 225 | 226 | @media screen and (max-width: 767px) { 227 | .row-offcanvas { 228 | position: relative; 229 | -webkit-transition: all .25s ease-out; 230 | -o-transition: all .25s ease-out; 231 | transition: all .25s ease-out; 232 | } 233 | 234 | .row-offcanvas-right { 235 | right: 0; 236 | } 237 | 238 | .row-offcanvas-left { 239 | left: 0; 240 | } 241 | 242 | .row-offcanvas-right 243 | .sidebar-offcanvas { 244 | right: -75%; /* 8 columns */ 245 | } 246 | 247 | .row-offcanvas-left 248 | .sidebar-offcanvas { 249 | left: -75%; /* 8 columns */ 250 | } 251 | 252 | .row-offcanvas-right.active { 253 | right: 75%; /* 8 columns */ 254 | } 255 | 256 | .row-offcanvas-left.active { 257 | left: 75%; /* 8 columns */ 258 | } 259 | 260 | .sidebar-offcanvas { 261 | overflow: hidden; 262 | position: absolute; 263 | top: 0; 264 | width: 75%; /* 8 columns */ 265 | } 266 | } 267 | 268 | p { 269 | margin: 0 0 1.6em; 270 | } 271 | 272 | /* Remnants of Asciidoctor default stylesheet - remove styles as needed */ 273 | 274 | #map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; } 275 | .left { float: left !important; } 276 | .right { float: right !important; } 277 | .text-left { text-align: left !important; } 278 | .text-right { text-align: right !important; } 279 | .text-center { text-align: center !important; } 280 | .text-justify { text-align: justify !important; } 281 | .hide { display: none; } 282 | .subheader, #content #toctitle, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #7a2518; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; } 283 | abbr, acronym { text-transform: uppercase; font-size: 90%; color: #333333; border-bottom: 1px dotted #dddddd; cursor: help; } 284 | abbr { text-transform: none; } 285 | blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 3px solid #487c58; } 286 | blockquote cite { display: block; font-size: inherit; color: #454545; } 287 | blockquote cite:before { content: "\2014 \0020"; } 288 | blockquote cite a, blockquote cite a:visited { color: #454545; } 289 | blockquote, blockquote p { line-height: 1.6; color: #6e6e6e; } 290 | @media only screen and (min-width: 768px) { 291 | #toctitle, .sidebarblock > .content > .title { line-height: 1.4; } 292 | #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; } 293 | } 294 | table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; } 295 | table thead, table tfoot { background: whitesmoke; font-weight: bold; } 296 | table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #333333; text-align: left; } 297 | table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #333333; } 298 | table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; } 299 | table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.6; } 300 | .clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; } 301 | .clearfix:after, .float-group:after { clear: both; } 302 | *:not(pre) > code { font-size: inherit; padding: 0; white-space: nowrap; background-color: inherit; border: 0 solid #dddddd; -webkit-border-radius: 4px; border-radius: 4px; text-shadow: none; line-height: 1; } 303 | .keyseq { color: #666666; } 304 | kbd:not(.keyseq) { display: inline-block; color: #333333; font-size: 0.75em; line-height: 1.4; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px white inset; margin: -0.15em 0.15em 0 0.15em; padding: 0.2em 0.6em 0.2em 0.5em; vertical-align: middle; white-space: nowrap; } 305 | .keyseq kbd:first-child { margin-left: 0; } 306 | .keyseq kbd:last-child { margin-right: 0; } 307 | .menuseq, .menu { color: #1a1a1a; } 308 | b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; } 309 | b.button:before { content: "["; padding: 0 3px 0 2px; } 310 | b.button:after { content: "]"; padding: 0 2px 0 3px; } 311 | p a > code:hover { color: #561309; } 312 | #header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; } 313 | #header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; } 314 | #header:after, #content:after, #footnotes:after, #footer:after { clear: both; } 315 | #content:before { content: none; } 316 | #header { margin-bottom: 2.5em; } 317 | #header > h1 { color: black; font-weight: 300; border-bottom: 1px solid #d8d8d8; margin-bottom: -28px; padding-bottom: 32px; } 318 | #header span { color: #6e6e6e; } 319 | #header #revnumber { text-transform: capitalize; } 320 | #header br { display: none; } 321 | #header br + span { padding-left: 3px; } 322 | #header br + span:before { content: "\2013 \0020"; } 323 | #header br + span.author { padding-left: 0; } 324 | #header br + span.author:before { content: ", "; } 325 | #toc { border-bottom: 3px double #e5e5e5; padding-top: 1em; padding-bottom: 1.25em; } 326 | #toc > ul { margin-left: 0.25em; } 327 | #toc ul.sectlevel0 > li > a { font-style: italic; } 328 | #toc ul.sectlevel0 ul.sectlevel1 { margin-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; } 329 | #toc ul { font-family: "Open Sans", "DejaVu Sans", "Sans", sans-serif; list-style-type: none; } 330 | #toc a { text-decoration: none; } 331 | #toc a:active { text-decoration: underline; } 332 | #toctitle { color: #7a2518; } 333 | @media only screen and (min-width: 768px) { body.toc2 { padding-left: 15em; padding-right: 0; } 334 | #toc.toc2 { background-color: #fafaf9; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #e5e5e5; border-bottom: 0; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; } 335 | #toc.toc2 #toctitle { margin-top: 0; font-size: 1.2em; } 336 | #toc.toc2 > ul { font-size: .90em; margin-bottom: 0; } 337 | #toc.toc2 ul ul { margin-left: 0; padding-left: 1em; } 338 | #toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; } 339 | body.toc2.toc-right { padding-left: 0; padding-right: 15em; } 340 | body.toc2.toc-right #toc.toc2 { border-right: 0; border-left: 1px solid #e5e5e5; left: auto; right: 0; } } 341 | @media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; } 342 | #toc.toc2 { width: 20em; } 343 | #toc.toc2 #toctitle { font-size: 1.375em; } 344 | #toc.toc2 > ul { font-size: 0.95em; } 345 | #toc.toc2 ul ul { padding-left: 1.25em; } 346 | body.toc2.toc-right { padding-left: 0; padding-right: 20em; } } 347 | #content #toc { border-style: solid; border-width: 1px; border-color: #e3e3dd; margin-bottom: 1.25em; padding: 1.25em; background: #fafaf9; border-width: 0; -webkit-border-radius: 4px; border-radius: 4px; } 348 | #content #toc > :first-child { margin-top: 0; } 349 | #content #toc > :last-child { margin-bottom: 0; } 350 | #content #toctitle { font-size: 1.375em; } 351 | #footer { max-width: 100%; background-color: #333333; padding: 1.25em; } 352 | #footer-text { color: #cccccc; line-height: 1.44; } 353 | .audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .verseblock, .videoblock { margin-bottom: 2.5em; } 354 | .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; font-family: "Noto Serif", "DejaVu Serif", "Serif", serif; font-weight: normal; font-style: italic; } 355 | table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; } 356 | table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; } 357 | .admonitionblock > table { border: 0; background: none; width: 100%; } 358 | .admonitionblock > table td.icon { text-align: center; width: 80px; } 359 | .admonitionblock > table td.icon img { max-width: none; } 360 | .admonitionblock > table td.icon .title { font-weight: 300; text-transform: uppercase; } 361 | .admonitionblock > table td.content { padding-left: 0; padding-right: 1.25em; color: #6e6e6e; } 362 | .admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; } 363 | .exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 4px; border-radius: 4px; } 364 | .exampleblock > .content > :first-child { margin-top: 0; } 365 | .exampleblock > .content > :last-child { margin-bottom: 0; } 366 | .exampleblock > .content h1, .exampleblock > .content h2, .exampleblock > .content h3, .exampleblock > .content #toctitle, .sidebarblock.exampleblock > .content > .title, .exampleblock > .content h4, .exampleblock > .content h5, .exampleblock > .content h6, .exampleblock > .content p { color: #333333; } 367 | .exampleblock > .content h1, .exampleblock > .content h2, .exampleblock > .content h3, .exampleblock > .content #toctitle, .sidebarblock.exampleblock > .content > .title, .exampleblock > .content h4, .exampleblock > .content h5, .exampleblock > .content h6 { line-height: 1; margin-bottom: 0.625em; } 368 | .exampleblock > .content h1.subheader, .exampleblock > .content h2.subheader, .exampleblock > .content h3.subheader, .exampleblock > .content .subheader#toctitle, .sidebarblock.exampleblock > .content > .subheader.title, .exampleblock > .content h4.subheader, .exampleblock > .content h5.subheader, .exampleblock > .content h6.subheader { line-height: 1.4; } 369 | .exampleblock.result > .content { -webkit-box-shadow: 0 1px 8px #e3e3dd; box-shadow: 0 1px 8px #e3e3dd; } 370 | .sidebarblock { border-style: solid; border-width: 1px; border-color: #e3e3dd; margin-top: -1.0em; margin-bottom: 1.6em; padding: .5em; background: #F1F3F5; -webkit-border-radius: 4px; border-radius: 4px; overflow-x: auto; } 371 | .sidebarblock > :first-child { margin-top: 0; } 372 | .sidebarblock > :last-child { margin-bottom: 0; } 373 | .sidebarblock h1, .sidebarblock h2, .sidebarblock h3, .sidebarblock #toctitle, .sidebarblock > .content > .title, .sidebarblock h4, .sidebarblock h5, .sidebarblock h6, .sidebarblock p { color: #333333; } 374 | .sidebarblock h1, .sidebarblock h2, .sidebarblock h3, .sidebarblock #toctitle, .sidebarblock > .content > .title, .sidebarblock h4, .sidebarblock h5, .sidebarblock h6 { line-height: 1; margin-bottom: 0.625em; } 375 | .sidebarblock h1.subheader, .sidebarblock h2.subheader, .sidebarblock h3.subheader, .sidebarblock .subheader#toctitle, .sidebarblock > .content > .subheader.title, .sidebarblock h4.subheader, .sidebarblock h5.subheader, .sidebarblock h6.subheader { line-height: 1.4; } 376 | .sidebarblock > .content > .title { color: #7a2518; margin-top: 0; line-height: 1.6; } 377 | .exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; } 378 | .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 0px; background-color: #F0F3F5; -webkit-border-radius: 5px; border-radius: 5px; padding: 1.5em 2.5em; word-wrap: break-word; } 379 | .literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; } 380 | .literalblock pre > code, .literalblock pre[class] > code, .listingblock pre > code, .listingblock pre[class] > code { display: block; } 381 | .listingblock > .content { position: relative; } 382 | .listingblock:hover code[class*=" language-"]:before { text-transform: uppercase; font-size: 0.9em; color: #999; position: absolute; top: 0.375em; right: 0.375em; } 383 | .listingblock:hover code.asciidoc:before { content: "asciidoc"; } 384 | .listingblock:hover code.clojure:before { content: "clojure"; } 385 | .listingblock:hover code.css:before { content: "css"; } 386 | .listingblock:hover code.go:before { content: "go"; } 387 | .listingblock:hover code.groovy:before { content: "groovy"; } 388 | .listingblock:hover code.html:before { content: "html"; } 389 | .listingblock:hover code.java:before { content: "java"; } 390 | .listingblock:hover code.javascript:before { content: "javascript"; } 391 | .listingblock:hover code.python:before { content: "python"; } 392 | .listingblock:hover code.ruby:before { content: "ruby"; } 393 | .listingblock:hover code.sass:before { content: "sass"; } 394 | .listingblock:hover code.scss:before { content: "scss"; } 395 | .listingblock:hover code.xml:before { content: "xml"; } 396 | .listingblock:hover code.yaml:before { content: "yaml"; } 397 | .listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; } 398 | .listingblock.terminal pre .command:not([data-prompt]):before { content: '$'; } 399 | table.pyhltable { border: 0; margin-bottom: 0; } 400 | table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; } 401 | table.pyhltable td.code { padding-left: .75em; padding-right: 0; } 402 | .highlight.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #d8d8d8; } 403 | .highlight.pygments .lineno { display: inline-block; margin-right: .25em; } 404 | table.pyhltable .linenodiv { background-color: transparent !important; padding-right: 0 !important; } 405 | .quoteblock { margin: 0 0 1.25em 0; padding: 0.5625em 1.25em 0 1.1875em; border-left: 3px solid #487c58; } 406 | .quoteblock blockquote { margin: 0 0 1.25em 0; padding: 0 0 0.625em 0; border: 0; } 407 | .quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; } 408 | .quoteblock .attribution { margin-top: -0.625em; padding-bottom: 0.625em; font-size: inherit; color: #454545; line-height: 1.6; } 409 | .quoteblock .attribution br { display: none; } 410 | .quoteblock .attribution cite { display: block; } 411 | table.tableblock { max-width: 100%; } 412 | table.tableblock td .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; } 413 | table.spread { width: 100%; } 414 | table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; } 415 | table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; } 416 | table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; } 417 | table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; } 418 | table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; } 419 | table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; } 420 | table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; } 421 | table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; } 422 | table.frame-all { border-width: 1px; } 423 | table.frame-sides { border-width: 0 1px; } 424 | table.frame-topbot { border-width: 1px 0; } 425 | th.halign-left, td.halign-left { text-align: left; } 426 | th.halign-right, td.halign-right { text-align: right; } 427 | th.halign-center, td.halign-center { text-align: center; } 428 | th.valign-top, td.valign-top { vertical-align: top; } 429 | th.valign-bottom, td.valign-bottom { vertical-align: bottom; } 430 | th.valign-middle, td.valign-middle { vertical-align: middle; } 431 | table thead th, table tfoot th { font-weight: bold; } 432 | tbody tr th { display: table-cell; line-height: 1.6; background: whitesmoke; } 433 | tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #333333; font-weight: bold; } 434 | td > div.verse { white-space: pre; } 435 | ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; } 436 | ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; } 437 | ul.checklist li > p:first-child > .fa-check-square-o:first-child, ul.checklist li > p:first-child > input[type="checkbox"]:first-child { margin-right: 0.25em; } 438 | ul.checklist li > p:first-child > input[type="checkbox"]:first-child { position: relative; top: 1px; } 439 | ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; } 440 | ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; } 441 | ul.inline > li > * { display: block; } 442 | .unstyled dl dt { font-weight: normal; font-style: normal; } 443 | ol.arabic { list-style-type: decimal; } 444 | ol.decimal { list-style-type: decimal-leading-zero; } 445 | ol.loweralpha { list-style-type: lower-alpha; } 446 | ol.upperalpha { list-style-type: upper-alpha; } 447 | ol.lowerroman { list-style-type: lower-roman; } 448 | ol.upperroman { list-style-type: upper-roman; } 449 | ol.lowergreek { list-style-type: lower-greek; } 450 | .hdlist > table, .colist > table { border: 0; background: none; } 451 | .hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; } 452 | td.hdlist1 { padding-right: .75em; font-weight: bold; } 453 | td.hdlist1, td.hdlist2 { vertical-align: top; } 454 | .literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; } 455 | .colist > table tr > td:first-of-type { padding: 0 .75em; line-height: 1; } 456 | .colist > table tr > td:last-of-type { padding: 0.25em 0; } 457 | .qanda > ol > li > p > em:only-child { color: #1d4b8f; } 458 | .thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; } 459 | .imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; } 460 | .imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; } 461 | .imageblock > .title { margin-bottom: 0; } 462 | .imageblock.thumb, .imageblock.th { border-width: 6px; } 463 | .imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; } 464 | .image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; } 465 | .image.left { margin-right: 0.625em; } 466 | .image.right { margin-left: 0.625em; } 467 | a.image { text-decoration: none; } 468 | span.footnote, span.footnoteref { vertical-align: super; font-size: 0.875em; } 469 | span.footnote a, span.footnoteref a { text-decoration: none; } 470 | span.footnote a:active, span.footnoteref a:active { text-decoration: underline; } 471 | #footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; } 472 | #footnotes hr { width: 20%; min-width: 6.25em; margin: -.25em 0 .75em 0; border-width: 1px 0 0 0; } 473 | #footnotes .footnote { padding: 0 0.375em; line-height: 1.3; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.2em; margin-bottom: .2em; } 474 | #footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; } 475 | #footnotes .footnote:last-of-type { margin-bottom: 0; } 476 | #content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; } 477 | .gist .file-data > table { border: none; background: #fff; width: 100%; margin-bottom: 0; } 478 | .gist .file-data > table td.line-data { width: 99%; } 479 | div.unbreakable { page-break-inside: avoid; } 480 | .replaceable { font-style: italic; font-color: inherit; font-family: inherit; } 481 | .parameter { font-style: italic; font-family: monospace; } 482 | .userinput { font-weight: bold; font-family: monospace; } 483 | .envar { font-weight: bold; font-family: monospace; font-size: 90%; } 484 | .sysitem { font-weight: bold; font-size: 90%; } 485 | .package { font-weight: bold; font-size: 90%; } 486 | .filename { font-weight: bold; font-style: italic; font-size: 90%; } 487 | .big { font-size: larger; } 488 | .small { font-size: smaller; } 489 | .underline { text-decoration: underline; } 490 | .overline { text-decoration: overline; } 491 | .line-through { text-decoration: line-through; } 492 | .aqua { color: #00bfbf; } 493 | .aqua-background { background-color: #00fafa; } 494 | .black { color: black; } 495 | .black-background { background-color: black; } 496 | .blue { color: #0000bf; } 497 | .blue-background { background-color: #0000fa; } 498 | .fuchsia { color: #bf00bf; } 499 | .fuchsia-background { background-color: #fa00fa; } 500 | .gray { color: #606060; } 501 | .gray-background { background-color: #7d7d7d; } 502 | .green { color: #006000; } 503 | .green-background { background-color: #007d00; } 504 | .lime { color: #00bf00; } 505 | .lime-background { background-color: #00fa00; } 506 | .maroon { color: #600000; } 507 | .maroon-background { background-color: #7d0000; } 508 | .navy { color: #000060; } 509 | .navy-background { background-color: #00007d; } 510 | .olive { color: #606000; } 511 | .olive-background { background-color: #7d7d00; } 512 | .purple { color: #600060; } 513 | .purple-background { background-color: #7d007d; } 514 | .red { color: #bf0000; } 515 | .red-background { background-color: #fa0000; } 516 | .silver { color: #909090; } 517 | .silver-background { background-color: #bcbcbc; } 518 | .teal { color: #006060; } 519 | .teal-background { background-color: #007d7d; } 520 | .white { color: #bfbfbf; } 521 | .white-background { background-color: #fafafa; } 522 | .yellow { color: #bfbf00; } 523 | .yellow-background { background-color: #fafa00; } 524 | span.icon > .fa { cursor: default; } 525 | .admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; cursor: default; } 526 | .admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #4E9FDD; } 527 | .admonitionblock td.icon .icon-tip:before { content: "\f0eb"; color: #2C8596; } 528 | .admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #ec7a08; } 529 | .admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #ec7a08; } 530 | .admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #c00; } 531 | .conum[data-value] { display: inline-block; color: white !important; background-color: #333333; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; width: 20px; height: 20px; font-size: 12px; line-height: 20px; font-family: "Open Sans", "Sans", sans-serif; font-style: normal; font-weight: bold; text-indent: -1px; } 532 | .conum[data-value] * { color: white !important; } 533 | .conum[data-value] + b { display: none; } 534 | .conum[data-value]:after { content: attr(data-value); } 535 | pre .conum[data-value] { position: relative; top: -2px; } 536 | b.conum * { color: inherit !important; } 537 | .conum:not([data-value]):empty { display: none; } 538 | .print-only { display: none !important; } 539 | @media print { @page { margin: 1.25cm 0.75cm; } 540 | * { -webkit-box-shadow: none !important; box-shadow: none !important; text-shadow: none !important; } 541 | a, a:visited { color: inherit !important; text-decoration: underline !important; } 542 | a[href^="http:"]:after, a[href^="https:"]:after { content: " (" attr(href) ")"; } 543 | a[href^="#"], a[href^="#"]:visited, a[href^="mailto:"], a[href^="mailto:"]:visited { text-decoration: none !important; } 544 | abbr[title]:after { content: " (" attr(title) ")"; } 545 | pre, blockquote { page-break-inside: avoid; } 546 | code { color: #191919; } 547 | thead { display: table-header-group; } 548 | tr, img { page-break-inside: avoid; } 549 | img { max-width: 100% !important; } 550 | p { orphans: 3; widows: 3; } 551 | h2, h3, #toctitle, .sidebarblock > .content > .title, #toctitle, .sidebarblock > .content > .title { page-break-after: avoid; } 552 | #toc, .sidebarblock { background: none !important; } 553 | #toc { border-bottom: 1px solid #d8d8d8 !important; padding-bottom: 0 !important; } 554 | .sect1 { padding-bottom: 0 !important; } 555 | .sect1 + .sect1 { border: none !important; } 556 | body.book #header { text-align: center; } 557 | body.book #header > h1 { border: none !important; margin: 2.5em 0 1em 0; padding: 0; } 558 | body.book #header span { line-height: 1.6; } 559 | body.book #header br { display: block; } 560 | body.book #header br + span { padding-left: 0; } 561 | body.book #header br + span:before { content: none !important; } 562 | body.book #toc { border: none !important; text-align: left !important; padding: 0 !important; } 563 | #footer { background: none !important; } 564 | #footer-text { color: #333333 !important; } 565 | .hide-on-print { display: none !important; } 566 | .print-only { display: block !important; } 567 | .hide-for-print { display: none !important; } 568 | .show-for-print { display: inherit !important; } } 569 | -------------------------------------------------------------------------------- /templates/_templates/_breadcrumb.html.erb: -------------------------------------------------------------------------------- 1 | <%- navigation.each do |topic_group| -%> 2 | <%- if topic_id.start_with?(topic_group[:id]) %> 3 | 10 | <%- end -%> 11 | <%- if topic_group.has_key?(:topics) -%> 12 | <%= render("_templates/_breadcrumb.html.erb", :navigation => topic_group[:topics], :topic_id => topic_id, :subtopic_shim => subtopic_shim) %> 13 | <%- end -%> 14 | <%- end -%> 15 | -------------------------------------------------------------------------------- /templates/_templates/_css.html.erb: -------------------------------------------------------------------------------- 1 | <%- Dir.glob("_stylesheets/*").sort.each do |sheet| -%> 2 | 3 | <%- end -%> 4 | -------------------------------------------------------------------------------- /templates/_templates/_nav.html.erb: -------------------------------------------------------------------------------- 1 | <%- navigation.each do |topic_group| -%> 2 | <%- if not topic_group.has_key?(:topics) -%> 3 |
  • <%= topic_group[:name] %>
  • 4 | <%- else -%> 5 | 13 | <%- end -%> 14 | <%- end -%> 15 | -------------------------------------------------------------------------------- /templates/_templates/_title.html.erb: -------------------------------------------------------------------------------- 1 | <%- navigation.each do |topic_group| -%> 2 | <%- if topic_id.start_with?(topic_group[:id]) %> 3 | | <%= topic_group[:name] %> 4 | <%- end -%> 5 | <%- if topic_group.has_key?(:topics) -%> 6 | <%= render("_templates/_title.html.erb", :navigation => topic_group[:topics], :topic_id => topic_id) %> 7 | <%- end -%> 8 | <%- end -%> 9 | -------------------------------------------------------------------------------- /templates/_templates/page.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= distro %> <%= version %> 8 | <%= render("_templates/_title.html.erb", :navigation => navigation, :topic_id => topic_id) %> 9 | 10 | 11 | 12 | 13 | 14 | 15 | <%= render("_templates/_css.html.erb", :css_path => css_path) %> 16 | 17 | 18 | 19 | 23 | 24 | " rel="shortcut icon" type="text/css"> 25 | 26 | 27 | 28 | 29 | 36 |
    37 |

    38 | 39 |

    40 | 46 |
    47 | 52 |
    53 | 56 | <%= content %> 57 |
    58 |
    59 |
    60 | 61 | 62 | 63 | 64 | 65 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /templates/_topic_map.yml: -------------------------------------------------------------------------------- 1 | # This configuration file dictates the organization of the topic groups and 2 | # topics on the main page of the doc site for this branch. Each record 3 | # consists of the following: 4 | # 5 | # --- <= Record delimiter 6 | # Name: Origin of the Species <= Display name of topic group 7 | # Dir: origin_of_the_species <= Directory name of topic group 8 | # Topics: 9 | # - Name: The Majestic Marmoset <= Topic name 10 | # File: the_majestic_marmoset <= Topic file under group dir +/- .adoc 11 | # - Name: The Curious Crocodile <= Topic 2 name 12 | # File: the_curious_crocodile <= Topic 2 file 13 | # - Name: The Numerous Nematodes <= Sub-topic group name 14 | # Dir: the_numerous_nematodes <= Sub-topic group dir 15 | # Topics: 16 | # - Name: The Wily Worm <= Sub-topic name 17 | # File: the_wily_worm <= Sub-topic file under / 18 | # - Name: The Acrobatic Ascarid <= Sub-topic 2 name 19 | # File: the_acrobatic_ascarid <= Sub-topic 2 file under / 20 | # 21 | # The ordering of the records in this document determines the ordering of the 22 | # topic groups and topics on the main page. 23 | --- 24 | Name: AsciiBinder Doc Project 25 | Dir: welcome 26 | Topics: 27 | - Name: Welcome 28 | File: index 29 | 30 | -------------------------------------------------------------------------------- /templates/index-main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | AsciiBinder Site Template 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 |
    24 |
    25 | 28 |
    29 |
    30 |
    31 |

    What is AsciiBinder?

    32 |

    AsciiBinder is a documentation system for people who have a lot of docs to maintain and republish on a regular basis. AsciiBinder was specifically developed to solve two problems at once:

    33 |
      34 |
    • Make it easier for developers and community members to contribute documentation.
    • 35 |
    • Make it easier for content managers to build and publish several variants of the same documentation.
    • 36 |
    37 |

     

    38 |

    AsciiBinder isn't for blogging.

    39 |

    If you are looking for a great tool for blogging where your articles are sourced in AsciiDoc, this isn't it. Seriously, go check out awestruct.github.io, which is awesome for that.

    40 |

     

    41 |

    AsciiBinder is for documenting versioned, interrelated projects.

    42 |

    On the other hand, if you are looking for a way to:

    43 |
      44 |
    • Source your docs in AsciiDoc
    • 45 |
    • Manage doc changes and doc versions with git
    • 46 |
    • Have the ability to conditionalize topics and produce different distributions of the docs based on those conditions
    • 47 |
    48 |

    ...then by jove, you've come to the right place.

    49 |

     

    50 |
    51 | 82 |
    83 |
    84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /templates/welcome/index.adoc: -------------------------------------------------------------------------------- 1 | = {product-title} {product-version} Documentation 2 | {product-author} 3 | {product-version} 4 | :data-uri: 5 | :icons: 6 | 7 | Welcome to the AsciiBinder Docs Management System. This welcome page is provided as a template for the topic pages that you will create for your software project. 8 | 9 | == Need Help? 10 | * Check out the http://www.asciibinder.org/latest/welcome/[AsciiBinder documentation] 11 | * Join our http://groups.google.com/group/asciibinder[mailing list] 12 | * Find us on IRC at FreeNode, http://webchat.freenode.net/?randomnick=1&channels=asciibinder&uio=d4[#asciibinder] channel 13 | * Open an https://github.com/redhataccess/ascii_binder/issues[issue on GitHub] 14 | 15 | --------------------------------------------------------------------------------