lib/liquidoc.rb in liquidoc-0.7.0 vs lib/liquidoc.rb in liquidoc-0.8.0
- old
+ new
@@ -7,10 +7,11 @@
require 'asciidoctor-pdf'
require 'logger'
require 'csv'
require 'crack/xml'
require 'fileutils'
+require 'jekyll'
# ===
# Table of Contents
# ===
#
@@ -30,10 +31,12 @@
# Default settings
# ===
@base_dir_def = Dir.pwd + '/'
@base_dir = @base_dir_def
+@build_dir_def = @base_dir + '_build'
+@build_dir = @build_dir_def
@configs_dir = @base_dir + '_configs'
@templates_dir = @base_dir + '_templates/'
@data_dir = @base_dir + '_data/'
@attributes_file_def = '_data/asciidoctor.yml'
@attributes_file = @attributes_file_def
@@ -41,17 +44,25 @@
@fonts_dir = 'theme/fonts/'
@output_filename = 'index'
@attributes = {}
@passed_attrs = {}
@verbose = false
+@quiet = false
+@explicit = false
+# Instantiate the main Logger object, which is always running
@logger = Logger.new(STDOUT)
-@logger.level = Logger::INFO
@logger.formatter = proc do |severity, datetime, progname, msg|
"#{severity}: #{msg}\n"
end
+@logger.level = Logger::INFO # suppresses DEBUG-level messages
+
+FileUtils::mkdir_p("#{@build_dir}") unless File.exists?("#{@build_dir}")
+FileUtils::mkdir_p("#{@build_dir}/pre") unless File.exists?("#{@build_dir}/pre")
+
+
# ===
# Executive procs
# ===
# Establish source, template, index, etc details for build jobs from a config file
@@ -75,35 +86,39 @@
def iterate_build cfg
stepcount = 0
for step in cfg.steps # iterate through each node in the 'config' object, which should start with an 'action' parameter
stepcount = stepcount + 1
step = BuildConfigStep.new(step) # create an instance of the Action class, validating the top-level step hash (now called 'step') in the process
+ @explainer.info step.message
type = step.type
case type # a switch to evaluate the 'action' parameter for each step in the iteration...
when "parse"
data = DataSrc.new(step.data)
builds = step.builds
for bld in builds
build = Build.new(bld, type) # create an instance of the Build class; Build.new accepts a 'bld' hash & action 'type'
if build.template
- liquify(data, build.template, build.output) # perform the liquify operation
+ @explainer.info build.message
+ liquify(data, build.template, build.output, build.variables) # perform the liquify operation
else
regurgidata(data, build.output)
end
end
when "migrate"
inclusive = true
inclusive = step.options['inclusive'] if defined?(step.options['inclusive'])
copy_assets(step.source, step.target, inclusive)
when "render"
validate_file_input(step.source, "source") if step.source
- doc = AsciiDocument.new(step.source)
- attrs = ingest_attributes(step.data) if step.data # Set attributes in from YAML files
- doc.add_attrs!(attrs) # Set attributes from the action-level data file
builds = step.builds
for bld in builds
+ doc = AsciiDocument.new(step.source)
+ attrs = ingest_attributes(step.data) if step.data # Set attributes from from YAML files
+ doc.add_attrs!(attrs) # Set attributes from the action-level data file
build = Build.new(bld, type) # create an instance of the Build class; Build.new accepts a 'bld' hash & action 'type' string
+ build.set("backend", derive_backend(doc.type, build.output) ) unless build.backend
+ @explainer.info build.message
render_doc(doc, build) # perform the render operation
end
when "deploy"
@logger.warn "Deploy actions are limited and experimental experimental."
jekyll_serve(build)
@@ -143,10 +158,31 @@
end
end
# TODO More validation needed
end
+def explainer_init out=nil
+ unless @explainer
+ if out == "STDOUT"
+ @explainer = Logger.new(STDOUT)
+ else
+ out = "#{@build_dir}/pre/config-explainer.adoc" if out.nil?
+ File.open(out, 'w') unless File.exists?(out)
+ file = File.open(out, File::WRONLY)
+ begin
+ @explainer = Logger.new(file)
+ rescue Exception => ex
+ @logger.error ex
+ raise "ExplainerCreateError"
+ end
+ end
+ @explainer.formatter = proc do |severity, datetime, progname, msg|
+ "#{msg}\n"
+ end
+ end
+end
+
# ===
# Core classes
# ===
# For now BuildConfig is mostly to objectify the primary build 'action' steps
@@ -209,14 +245,63 @@
def options
return @step['options']
end
+ def stage
+ return @step['stage']
+ end
+
def builds
return @step['builds']
end
+ def message
+ # dynamically build a human-friendly log message, possibly appending a reason
+ unless @step['message']
+ reason = ", #{@step['reason']}" if @step['reason']
+ noninclusively = ", without carrying the parent directory" if self.options.is_a?(Hash) && self.options['inclusive'] == false && File.directory?(self.source)
+ stage = "" ; stage = "[#{self.stage}] " if self.stage
+ case self.type
+ when "migrate"
+ text = ". #{stage}Copies `#{self.source}` to `#{self.target}`#{noninclusively}#{reason}."
+ when "parse"
+ if self.data.is_a? Array
+ if self.data.count > 1
+ text = ". Draws data from the following files:"
+ self.data.each do |file|
+ text.concat("\n * `#{file}`.")
+ end
+ text.concat("\n")
+ else
+ text = ". #{stage}Draws data from `#{self.data[0]}`"
+ end
+ else
+ text = ". #{stage}Draws data from `#{self.data['file']}`"
+ end
+ text.concat("#{reason},") if reason
+ text.concat(" and parses it as follows:")
+ return text
+ when "render"
+ if self.source
+ text = ". #{stage}Using the index file `#{self.source}` as a map#{reason}, and ingesting AsciiDoc attributes from "
+ if self.data.is_a? Array
+ text.concat("the following data files:")
+ self.data.each do |file|
+ text.concat("\n * `#{file}`.")
+ end
+ else
+ text.concat("`#{self.data}`")
+ end
+ return text
+ end
+ end
+ else
+ return @step['message']
+ end
+ end
+
def validate
case self.type
when "parse"
reqs = ["data,builds"]
when "migrate"
@@ -265,10 +350,62 @@
def props
@build['props']
end
+ def variables
+ @build['variables']
+ end
+
+ def message
+ # dynamically build a message, possibly appending a reason
+ unless @build['message']
+ reason = ", #{@build['reason']}" if @build['reason']
+ case @type
+ when "parse"
+ text = ".. Builds `#{self.output}` pressed with the template `#{self.template}`#{reason}."
+ when "render"
+ case self.backend
+ when "pdf"
+ text = ".. Uses Asciidoctor/Prawn to generate a PDF file `#{self.output}`"
+ text.concat("#{reason}") if reason
+ text.concat(".")
+ when "html5"
+ text = ".. Compiles a standard Asciidoctor HTML5 file, `#{self.output}`"
+ text.concat("#{reason}") if reason
+ text.concat(".")
+ when "jekyll"
+ text = ".. Uses Jekyll config files:\n+\n--"
+ files = self.props['files']
+ if files.is_a? String
+ if files.include? ","
+ files = files.split(",")
+ else
+ files = files.split
+ end
+ else
+ unless files.is_a? Array
+ @logger.error "The Jekyll configuration file must be a single filename, a comma-separated list of filenames, or an array of filenames."
+ end
+ end
+ files.each do |file|
+ text.concat("\n * `#{file}`")
+ end
+ text.concat("\n\nto generate a static site")
+ if self.props && self.props['arguments']
+ text.concat(" at `#{self.props['arguments']['destination']}`")
+ end
+ text.concat("#{reason}") if reason
+ text.concat(".\n--\n")
+ end
+ return text
+ end
+ else
+ @build['message']
+ end
+ end
+
def prop_files_array
if props
if props['files']
begin
props['files'].force_array if props['files']
@@ -331,11 +468,11 @@
raise "ActionSettingMissing"
end
end
end
-end #class Build
+end # class Build
class DataSrc
# initialization means establishing a proper hash for the 'data' param
def initialize datasrc
@datasrc = {}
@@ -353,12 +490,16 @@
@datasrc['type'] = datasrc['type']
end
else
if datasrc.is_a? String
@datasrc['ext'] = File.extname(datasrc)
- else # datasrc is neither string nor hash
- raise "InvalidDataSource"
+ else
+ if datasrc.is_a? Array
+
+ else
+ raise "InvalidDataSource"
+ end
end
end
end
def file
@@ -520,13 +661,17 @@
end
return output
end
# Parse given data using given template, generating given output
-def liquify datasrc, template_file, output
+def liquify datasrc, template_file, output, variables=nil
data = get_data(datasrc)
validate_file_input(template_file, "template")
+ if variables
+ vars = { "vars" => variables }
+ data.merge!vars
+ end
begin
template = File.read(template_file) # reads the template file
template = Liquid::Template.parse(template) # compiles template
rendered = template.render(data) # renders the output
rescue Exception => ex
@@ -544,11 +689,11 @@
rescue Exception => ex
@logger.error "Failed to save output.\n#{ex.class} #{ex.message}"
raise "FileNotBuilt"
end
if File.exists?(output_file)
- @logger.info "File built: #{File.basename(output_file)}"
+ @logger.info "File built: #{output_file}"
else
@logger.error "Hrmp! File not built."
raise "FileNotBuilt"
end
else # if stdout
@@ -636,11 +781,15 @@
rescue Exception => ex
@logger.error "Attributes block invalid. #{ex.class}: #{ex.message}"
raise "AttributeBlockError"
end
begin
- attrs.merge!new_attrs
+ if new_attrs.is_a? Hash
+ attrs.merge!new_attrs
+ else
+ @logger.warn "The AsciiDoc attributes file #{filename} is not formatted as a hash, so its data was not ingested."
+ end
rescue Exception => ex
raise "AttributesMergeError #{ex.message}"
end
end
return attrs
@@ -655,11 +804,10 @@
end
return backend
end
def render_doc doc, build
- build.set("backend", derive_backend(doc.type, build.output) ) unless build.backend
case build.backend
when "html5", "pdf"
asciidocify(doc, build)
when "jekyll"
generate_site(doc, build)
@@ -732,19 +880,19 @@
attrs.merge! ({"base_dir" => jekyll_config['source']}) # Sets default Asciidoctor base_dir to == Jekyll root
# write all AsciiDoc attributes to a config file for Jekyll to ingest
attrs.merge!(build.attributes) if build.attributes
attrs = {"asciidoctor" => {"attributes" => attrs} }
attrs_yaml = attrs.to_yaml # Convert it all back to Yaml, as we're going to write a file to feed back to Jekyll
- FileUtils::mkdir_p("build/pre") unless File.exists?("build/pre")
- File.open("build/pre/_attributes.yml", 'w') { |file| file.write(attrs_yaml) }
- build.add_config_file("build/pre/_attributes.yml")
+ File.open("#{@build_dir}/pre/_attributes.yml", 'w') { |file| file.write(attrs_yaml) }
+ build.add_config_file("#{@build_dir}/pre/_attributes.yml")
config_list = build.prop_files_array.join(',') # flatten the Array back down for the CLI
opts_args = ""
+ quiet = "--quiet" if @quiet || @explicit
if build.props['arguments']
opts_args = build.props['arguments'].to_opts_args
end
- command = "bundle exec jekyll build --config #{config_list} #{opts_args}"
+ command = "bundle exec jekyll build --config #{config_list} #{opts_args} #{quiet}"
end
@logger.info "Running #{command}"
@logger.debug "AsciiDoc attributes: #{doc.attributes.to_yaml} "
system command
jekyll_serve(build) if @jekyll_serve
@@ -837,10 +985,12 @@
include ForceArray
end
# Extending Liquid filters/text manipulation
module CustomFilters
+ include Jekyll::Filters
+
def plainwrap input
input.wrap
end
def commentwrap input
input.wrap commentchar: "# "
@@ -873,22 +1023,26 @@
# From the root directory of your project:
# $ liquidoc --help
command_parser = OptionParser.new do|opts|
opts.banner = "Usage: liquidoc [options]"
- opts.on("-a KEY=VALUE", "For passing an AsciiDoc attribute parameter to Asciidoctor. Ex: -a basedir=some/path -a custom_var='my value'") do |n|
+ opts.on("-a KEY=VALUE", "For passing an AsciiDoc attribute parameter to Asciidoctor. Ex: -a imagesdir=some/path -a custom_var='my value'") do |n|
pair = {}
k,v = n.split('=')
pair[k] = v
@passed_attrs.merge!pair
end
# Global Options
opts.on("-b PATH", "--base=PATH", "The base directory, relative to this script. Defaults to `.`, or pwd." ) do |n|
- @data_file = @base_dir + n
+ @base_dir = n
end
+ opts.on("-B PATH", "--build=PATH", "The directory under which LiquiDoc should save automatically preprocessed files. Defaults to #{@base_dir}_build. Can be absolute or relative to the base path (-b/--base=). Do NOT append '/' to the build path." ) do |n|
+ @build_dir = n
+ end
+
opts.on("-c", "--config=PATH", "Configuration file, enables preset source, template, and output.") do |n|
@config_file = @base_dir + n
end
opts.on("-d PATH", "--data=PATH", "Semi-structured data source (input) path. Ex. path/to/data.yml. Required unless --config is called." ) do |n|
@@ -909,23 +1063,31 @@
opts.on("-t PATH", "--template=PATH", "Path to liquid template. Required unless --configuration is called." ) do |n|
@template_file = @base_dir + n
end
- opts.on("--verbose", "Run verbose") do |n|
+ opts.on("--verbose", "Run verbose debug logging.") do |n|
@logger.level = Logger::DEBUG
@verbose = true
end
- opts.on("--stdout", "Puts the output in STDOUT instead of writing to a file.") do
- @output_type = "stdout"
+ opts.on("--quiet", "Run with only WARN- and error-level logs written to console.") do |n|
+ @logger.level = Logger::WARN
+ @quiet = true
end
- opts.on("--clean PATH", "Force deletes the designated directory and all its contents WITHOUT WARNING.") do |n|
- @clean_dir = n
+ opts.on("--explicit", "Log explicit step descriptions to console as build progresses. (Otherwise writes to file at #{@build_dir}/pre/config-explainer.adoc .)") do |n|
+ explainer_init("STDOUT")
+ @explainer.level = Logger::INFO
+ @logger.level = Logger::WARN # Suppress all those INFO-level messages
+ @explicit = true
end
+ opts.on("--stdout", "Puts the output in STDOUT instead of writing to a file.") do
+ @output_type = "stdout"
+ end
+
opts.on("--deploy", "EXPERIMENTAL: Trigger a jekyll serve operation against the destination dir of a Jekyll render step.") do
@jekyll_serve = true
end
opts.on("-h", "--help", "Returns help.") do
@@ -936,24 +1098,24 @@
end
command_parser.parse!
# Upfront debug output
-@logger.debug "Base dir: #{@base_dir}"
-@logger.debug "Config file: #{@config_file}"
+@logger.debug "Base dir: #{@base_dir} (The path from which LiquiDoc CLI commands are relative.)"
+explainer_init
+
# ===
# Execute
# ===
-if @clean_dir
- FileUtils.remove_dir(@clean_dir)
-end
+
unless @config_file
+ @logger.debug "Executing config-free build based on API/CLI arguments alone."
if @data_file
liquify(@data_file, @template_file, @output_file)
end
if @index_file
- @logger.warn "Publishing via command line arguments not yet implemented. Use a config file."
+ @logger.warn "Rendering via command line arguments is not yet implemented. Use a config file."
end
else
@logger.debug "Executing... config_build"
config_build(@config_file)
end