lib/liquidoc.rb in liquidoc-0.2.0 vs lib/liquidoc.rb in liquidoc-0.3.0
- old
+ new
@@ -6,19 +6,36 @@
require 'asciidoctor'
require 'logger'
require 'csv'
require 'crack/xml'
+# ===
+# Table of Contents
+# ===
+#
+# 1. dependencies stack
+# 2. default settings
+# 3. general methods
+# 4. object classes
+# 5. action-specific methods
+# 5a. parse methods
+# 5b. migrate methods
+# 5c. render methods
+# 6. text manipulation
+# 7. command/option parser
+# 8. executive method calls
+
+# ===
# Default settings
+# ===
+
@base_dir_def = Dir.pwd + '/'
@base_dir = @base_dir_def
@configs_dir = @base_dir + '_configs'
@templates_dir = @base_dir + '_templates/'
@data_dir = @base_dir + '_data/'
@output_dir = @base_dir + '_output/'
-@config_file_def = @base_dir + '_configs/cfg-sample.yml'
-@config_file = @config_file_def
@attributes_file_def = '_data/asciidoctor.yml'
@attributes_file = @attributes_file_def
@pdf_theme_file = 'theme/pdf-theme.yml'
@fonts_dir = 'theme/fonts/'
@output_filename = 'index'
@@ -29,120 +46,55 @@
@logger.formatter = proc do |severity, datetime, progname, msg|
"#{severity}: #{msg}\n"
end
# ===
-# General methods
+# Executive methods
# ===
-# Pull in a semi-structured data file, converting contents to a Ruby hash
-def get_data data
- # data must be a hash produced by data_hashify()
- if data['type']
- if data['type'].downcase == "yaml"
- data['type'] = "yml"
- end
- unless data['type'].downcase.match(/yml|json|xml|csv|regex/)
- @logger.error "Declared data type must be one of: yaml, json, xml, csv, or regex."
- raise "DataTypeUnrecognized"
- end
- else
- unless data['ext'].match(/\.yml|\.json|\.xml|\.csv/)
- @logger.error "Data file extension must be one of: .yml, .json, .xml, or .csv or else declared in config file."
- raise "FileExtensionUnknown (#{data[ext]})"
- end
- data['type'] = data['ext']
- data['type'].slice!(0) # removes leading dot char
- end
- case data['type']
- when "yml"
- begin
- return YAML.load_file(data['file'])
- rescue Exception => ex
- @logger.error "There was a problem with the data file. #{ex.message}"
- end
- when "json"
- begin
- return JSON.parse(File.read(data['file']))
- rescue Exception => ex
- @logger.error "There was a problem with the data file. #{ex.message}"
- end
- when "xml"
- begin
- data = Crack::XML.parse(File.read(data['file']))
- return data['root']
- rescue Exception => ex
- @logger.error "There was a problem with the data file. #{ex.message}"
- end
- when "csv"
- output = []
- i = 0
- begin
- CSV.foreach(data['file'], headers: true, skip_blanks: true) do |row|
- output[i] = row.to_hash
- i = i+1
- end
- output = {"data" => output}
- return output
- rescue
- @logger.error "The CSV format is invalid."
- end
- when "regex"
- if data['pattern']
- return parse_regex(data['file'], data['pattern'])
- else
- @logger.error "You must supply a regex pattern with your free-form data file."
- raise "MissingRegexPattern"
- end
- end
-end
-
# Establish source, template, index, etc details for build jobs from a config file
-# TODO This needs to be turned into a Class?
def config_build config_file
@logger.debug "Using config file #{config_file}."
validate_file_input(config_file, "config")
begin
config = YAML.load_file(config_file)
rescue
unless File.exists?(config_file)
- @logger.error "Config file not found."
+ @logger.error "Config file #{config_file} not found."
else
- @logger.error "Problem loading config file. Exiting."
+ @logger.error "Problem loading config file #{config_file}. Exiting."
end
- raise "Could not load #{config_file}"
+ raise "ConfigFileError"
end
- validate_config_structure(config)
- if config['compile']
- for src in config['compile']
- data = src['data']
- for cfgn in src['builds']
- template = @base_dir + cfgn['template']
- unless cfgn['output'].downcase == "stdout"
- output = @base_dir + cfgn['output']
- else
- output = "stdout"
- end
- liquify(data, template, output)
+ cfg = BuildConfig.new(config) # convert the config file to a new object called 'cfg'
+ iterate_build(cfg)
+end
+
+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
+ 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'
+ liquify(data, build.template, build.output) # perform the liquify operation
end
+ when "migrate"
+ @logger.warn "Migrate actions not yet implemented."
+ when "render"
+ @logger.warn "Render actions not yet implemented."
+ when "deploy"
+ @logger.warn "Deploy actions not yet implemented."
+ else
+ @logger.warn "The action `#{type}` is not valid."
end
end
- if config['publish']
- begin
- for pub in config['publish']
- for bld in pub['builds']
- if bld['publish']
- publish(pub, bld)
- else
- @logger.warn "Publish build for '#{index}' backend '#{backend}' disabled."
- end
- end
- end
- rescue Exception => ex
- @logger.error "Error during publish action. #{ex}"
- end
- end
end
# Verify files exist
def validate_file_input file, type
@logger.debug "Validating input file for #{type} file #{file}"
@@ -152,44 +104,264 @@
else
unless File.exists?(file)
error = "The #{type} file (#{file}) was not found."
end
end
- unless error
- @logger.debug "Input file validated for #{type} file #{file}."
- else
+ if error
@logger.error "Could not validate input file: #{error}"
raise "InvalidInput"
end
end
def validate_config_structure config
- unless config.is_a? Hash
- message = "The configuration file is not properly structured; it is not a hash"
+ unless config.is_a? Array
+ message = "The configuration file is not properly structured."
@logger.error message
- raise message
+ raise "ConfigStructError"
else
- unless config['publish'] or config['compile']
- raise "Config file must have at least one top-level section named 'publish:' or 'compile:'."
+ if (defined?(config['action'])).nil?
+ message = "Every listing in the configuration file needs an action type declaration."
+ @logger.error message
+ raise "ConfigStructError"
end
end
# TODO More validation needed
end
-def data_hashify data_var
- # TODO make datasource config a class
- if data_var.is_a?(String)
- data = {}
- data['file'] = data_var
- data['ext'] = File.extname(data_var)
- else # add ext to the hash
- data = data_var
- data['ext'] = File.extname(data['file'])
+# ===
+# Core classes
+# ===
+
+# For now BuildConfig is mostly to objectify the primary build 'action' steps
+class BuildConfig
+
+ def initialize config
+
+ if (defined?(config['compile'][0])) # The config is formatted for vesions < 0.3.0; convert it
+ config = deprecated_format(config)
+ end
+
+ # validations
+ unless config.is_a? Array
+ raise "ConfigStructError"
+ end
+
+ @@cfg = config
end
- return data
+
+ def steps
+ @@cfg
+ end
+
+ def deprecated_format config # for backward compatibility with 0.1.0 and 0.2.0
+ puts "You are using a deprecated configuration file structure. Update your config files; support for this structure will be dropped in version 1.0.0."
+ # There's only ever one item in the 'compile' array, and only one action type ("parse")
+ config['compile'].each do |n|
+ n.merge!("action" => "parse") # the action type was not previously declared
+ end
+ return config['compile']
+ end
+
+end #class BuildConfig
+
+class BuildConfigStep
+
+ def initialize step
+ @@step = step
+ @@logger = Logger.new(STDOUT)
+ if (defined?(@@step['action'])).nil?
+ @logger.error "Every step in the configuration file needs an 'action' type declared."
+ raise "ConfigStructError"
+ end
+ end
+
+ def type
+ return @@step['action']
+ end
+
+ def data
+ return @@step['data']
+ end
+
+ def builds
+ return @@step['builds']
+ end
+
+ def self.validate reqs
+ for req in reqs
+ if (defined?(@@step[req])).nil?
+ @@logger.error "Every #{@@step['action']}-type in the configuration file needs a '#{req}' declaration."
+ raise "ConfigStructError"
+ end
+ end
+ end
+
+end #class Action
+
+class Build
+
+ def initialize build, type
+ @@build = build
+ @@type = type
+ @@logger = Logger.new(STDOUT)
+ required = []
+ case type
+ when "parse"
+ required = ["template,output"]
+ when "render"
+ required = ["index,output"]
+ when "migrate"
+ required = ["source,target"]
+ end
+ for req in required
+ if (defined?(req)).nil?
+ raise ActionSettingMissing
+ end
+ end
+ end
+
+ def template
+ @@build['template']
+ end
+
+ def output
+ @@build['output']
+ end
+
+ def index
+ @@build['index']
+ end
+
+ def source
+ @@build['source']
+ end
+
+ def target
+ @@build['target']
+ end
+
+end #class Build
+
+class DataSrc
+ # initialization means establishing a proper hash for the 'data' param
+ def initialize datasrc
+ @@datasrc = {}
+ if datasrc.is_a? String # create a hash out of the filename
+ begin
+ @@datasrc['file'] = datasrc
+ @@datasrc['ext'] = File.extname(datasrc)
+ @@datasrc['type'] = false
+ @@datasrc['pattern'] = false
+ rescue
+ raise "InvalidDataFilename"
+ end
+ else
+ if datasrc.is_a? Hash # data var is a hash, so add 'ext' to it by extracting it from filename
+ @@datasrc['file'] = datasrc['file']
+ @@datasrc['ext'] = File.extname(datasrc['file'])
+ if (defined?(datasrc['pattern']))
+ @@datasrc['pattern'] = datasrc['pattern']
+ end
+ if (defined?(datasrc['type']))
+ @@datasrc['type'] = datasrc['type']
+ end
+ else # datasrc is neither String nor Hash
+ raise "InvalidDataSource"
+ end
+ end
+ end
+
+ def file
+ @@datasrc['file']
+ end
+
+ def ext
+ @@datasrc['ext']
+ end
+
+ def type
+ if @@datasrc['type'] # if we're carrying a 'type' setting for data, pass it along
+ datatype = @@datasrc['type']
+ if datatype.downcase == "yaml" # This is an expected common error, so let's do the user a solid
+ datatype = "yml"
+ end
+ else # If there's no 'type' defined, extract it from the filename and validate it
+ unless @@datasrc['ext'].downcase.match(/\.yml|\.json|\.xml|\.csv/)
+ # @logger.error "Data file extension must be one of: .yml, .json, .xml, or .csv or else declared in config file."
+ raise "FileExtensionUnknown"
+ end
+ datatype = @@datasrc['ext']
+ datatype = datatype[1..-1] # removes leading dot char
+ end
+ unless datatype.downcase.match(/yml|json|xml|csv|regex/) # 'type' must be one of these permitted vals
+ # @logger.error "Declared data type must be one of: yaml, json, xml, csv, or regex."
+ raise "DataTypeUnrecognized"
+ end
+ datatype
+ end
+
+ def pattern
+ @@datasrc['pattern']
+ end
end
+# ===
+# Action-specific methods
+#
+# PARSE-type build methods
+# ===
+
+# Pull in a semi-structured data file, converting contents to a Ruby hash
+def ingest_data datasrc
+# Must be passed a proper data object (there must be a better way to validate arg datatypes)
+ unless datasrc.is_a? Object
+ raise "InvalidDataObject"
+ end
+ # This method should really begin here, once the data object is in order
+ case datasrc.type
+ when "yml"
+ begin
+ return YAML.load_file(datasrc.file)
+ rescue Exception => ex
+ @logger.error "There was a problem with the data file. #{ex.message}"
+ end
+ when "json"
+ begin
+ return JSON.parse(File.read(datasrc.file))
+ rescue Exception => ex
+ @logger.error "There was a problem with the data file. #{ex.message}"
+ end
+ when "xml"
+ begin
+ data = Crack::XML.parse(File.read(datasrc.file))
+ return data['root']
+ rescue Exception => ex
+ @logger.error "There was a problem with the data file. #{ex.message}"
+ end
+ when "csv"
+ output = []
+ i = 0
+ begin
+ CSV.foreach(datasrc.file, headers: true, skip_blanks: true) do |row|
+ output[i] = row.to_hash
+ i = i+1
+ end
+ output = {"data" => output}
+ return output
+ rescue
+ @logger.error "The CSV format is invalid."
+ end
+ when "regex"
+ if datasrc.pattern
+ return parse_regex(datasrc.file, datasrc.pattern)
+ else
+ @logger.error "You must supply a regex pattern with your free-form data file."
+ raise "MissingRegexPattern"
+ end
+ end
+end
+
def parse_regex data_file, pattern
records = []
pattern_re = /#{pattern}/
@logger.debug "Using regular expression #{pattern} to parse data file."
groups = pattern_re.names
@@ -212,21 +384,19 @@
raise "Freeform parse error"
end
return output
end
-# ===
-# Liquify BUILD methods
-# ===
-
-# Parse given data using given template, saving to given filename
-def liquify data, template_file, output
+# Parse given data using given template, generating given output
+def liquify datasrc, template_file, output
@logger.debug "Executing liquify parsing operation."
- data = data_hashify(data)
- validate_file_input(data['file'], "data")
+ if datasrc.is_a? String
+ datasrc = DataSrc.new(datasrc)
+ end
+ validate_file_input(datasrc.file, "data")
validate_file_input(template_file, "template")
- data = get_data(data) # gathers the data
+ data = ingest_data(datasrc)
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
@@ -251,10 +421,14 @@
else # if stdout
puts "========\nOUTPUT: Rendered with template #{template_file}:\n\n#{rendered}\n"
end
end
+# ===
+# MIGRATE-type methods
+# ===
+
# Copy images and other assets into output dir for HTML operations
def copy_assets src, dest
if @recursive
dest = "#{dest}/#{src}"
recursively = "Recursively c"
@@ -271,11 +445,11 @@
end
@logger.debug "\s\s#{recursively}opied: #{src} --> #{dest}/#{src}"
end
# ===
-# PUBLISH methods
+# RENDER-type methods
# ===
# Gather attributes from a fixed attributes file
# Use _data/attributes.yml or designate as -a path/to/filename.yml
def get_attributes attributes_file
@@ -306,11 +480,11 @@
def publish pub, bld
@logger.warn "Publish actions not yet implemented."
end
# ===
-# Misc Classes, Modules, filters, etc
+# Text manipulation Classes, Modules, filters, etc
# ===
class String
# Adapted from Nikhil Gupta
# http://nikhgupta.com/code/wrapping-long-lines-in-ruby-for-display-in-source-files/
@@ -334,11 +508,11 @@
self.wrap(width: width).indent(spaces: spaces)
end
end
-# Liquid modules for text manipulation
+# Extending Liquid filters/text manipulation
module CustomFilters
def plainwrap input
input.wrap
end
def commentwrap input
@@ -380,15 +554,21 @@
replace(self.parameterize(sep))
end
end
+# register custom Liquid filters
Liquid::Template.register_filter(CustomFilters)
+
+# ===
+# Command/options parser
+# ===
+
# Define command-line option/argument parameters
# From the root directory of your project:
-# $ ./parse.rb --help
+# $ liquidoc --help
command_parser = OptionParser.new do|opts|
opts.banner = "Usage: liquidoc [options]"
opts.on("-a PATH", "--attributes-file=PATH", "For passing in a standard YAML AsciiDoc attributes file. Default: #{@attributes_file_def}") do |n|
@assets_path = n
@@ -440,21 +620,19 @@
exit
end
end
-# Parse options.
command_parser.parse!
# Upfront debug output
@logger.debug "Base dir: #{@base_dir}"
@logger.debug "Config file: #{@config_file}"
@logger.debug "Index file: #{@index_file}"
-# Parse data into docs!
-# liquify() takes the names of a Liquid template, a data file, and an output doc.
-# Input and output file extensions are non-determinant; your template
-# file establishes the structure.
+# ===
+# Execute
+# ===
unless @config_file
if @data_file
liquify(@data_file, @template_file, @output_file)
end