lib/liquidoc.rb in liquidoc-0.6.0 vs lib/liquidoc.rb in liquidoc-0.7.0
- old
+ new
@@ -1,6 +1,6 @@
-require "liquidoc"
+require 'liquidoc'
require 'yaml'
require 'json'
require 'optparse'
require 'liquid'
require 'asciidoctor'
@@ -82,32 +82,33 @@
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
+ if build.template
+ liquify(data, build.template, build.output) # 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"
- if defined?(step.data) # if we're passing attributes as a YAML file, let's ingest that up front
- attrs = ingest_attributes(step.data)
- else
- attrs = {}
- end
- validate_file_input(step.source, "source")
+ validate_file_input(step.source, "source") if step.source
doc = AsciiDocument.new(step.source)
- doc.add_attrs!(attrs)
+ 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
- build = Build.new(bld, type) # create an instance of the Build class; Build.new accepts a 'bld' hash & action 'type'
- asciidocify(doc, build) # perform the liquify operation
+ build = Build.new(bld, type) # create an instance of the Build class; Build.new accepts a 'bld' hash & action 'type' string
+ render_doc(doc, build) # perform the render operation
end
when "deploy"
- @logger.warn "Deploy actions not yet implemented."
+ @logger.warn "Deploy actions are limited and experimental experimental."
+ jekyll_serve(build)
else
@logger.warn "The action `#{type}` is not valid."
end
end
end
@@ -219,11 +220,11 @@
when "parse"
reqs = ["data,builds"]
when "migrate"
reqs = ["source,target"]
when "render"
- reqs = ["source,builds"]
+ reqs = ["builds"]
end
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"
@@ -234,10 +235,12 @@
end #class Action
class Build
def initialize build, type
+ build['attributes'] = Hash.new unless build['attributes']
+ build['props'] = build['properties'] if build['properties']
@build = build
@type = type
end
def template
@@ -258,18 +261,65 @@
def backend
@build['backend']
end
+ def props
+ @build['props']
+ end
+
+ def prop_files_array
+ if props
+ if props['files']
+ begin
+ props['files'].force_array if props['files']
+ rescue Exception => ex
+ raise "PropertiesFilesArrayError: #{ex}"
+ end
+ end
+ else
+ Array.new
+ end
+ end
+
+ # def prop_files_list # force the array back to a list of files (for CLI)
+ # props['files'].force_array if props['files']
+ # end
+
+ # NOTE this section repeats in Class.AsciiDocument
def attributes
@build['attributes']
end
+ def add_attrs! attrs
+ begin
+ attrs.to_h unless attrs.is_a? Hash
+ self.attributes.merge!attrs
+ rescue
+ raise "InvalidAttributesFormat"
+ end
+ end
+
def set key, val
@build[key] = val
end
+ def self.set key, val
+ @build[key] = val
+ end
+
+ def add_config_file config_file
+ @build['props'] = Hash.new unless @build['props']
+ @build['props']['files'] = Array.new unless @build['props']['files']
+ begin
+ files_array = @build['props']['files'].force_array
+ @build['props']['files'] = files_array.push(config_file)
+ rescue
+ raise "PropertiesFilesArrayError"
+ end
+ end
+
def validate
reqs = []
case self.type
when "parse"
reqs = ["template,output"]
@@ -288,11 +338,11 @@
class DataSrc
# initialization means establishing a proper hash for the 'data' param
def initialize datasrc
@datasrc = {}
@datasrc['file'] = datasrc
- @datasrc['ext'] = File.extname(datasrc)
+ @datasrc['ext'] = ''
@datasrc['type'] = false
@datasrc['pattern'] = false
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'])
@@ -300,12 +350,16 @@
@datasrc['pattern'] = datasrc['pattern']
end
if (defined?(datasrc['type']))
@datasrc['type'] = datasrc['type']
end
- else # datasrc is neither String nor Hash
- raise "InvalidDataSource"
+ else
+ if datasrc.is_a? String
+ @datasrc['ext'] = File.extname(datasrc)
+ else # datasrc is neither string nor hash
+ raise "InvalidDataSource"
+ end
end
end
def file
@datasrc['file']
@@ -340,20 +394,21 @@
@datasrc['pattern']
end
end
class AsciiDocument
- def initialize map, type='article'
- @index = map
- @attributes = {}
+ def initialize index, type='article'
+ @index = index
+ @attributes = {} # We start with clean attributes to delay setting those in the config > build step
@type = type
end
def index
@index
end
+ # NOTE this section repeats in Class.AsciiDocument
def add_attrs! attrs
raise "InvalidAttributesFormat" unless attrs.is_a?(Hash)
self.attributes.merge!attrs
end
@@ -376,10 +431,20 @@
# Action-specific procs
# ===
# PARSE-type build procs
# ===
+# Get data
+def get_data datasrc
+ @logger.debug "Executing liquify parsing operation."
+ if datasrc.is_a? String
+ datasrc = DataSrc.new(datasrc)
+ end
+ validate_file_input(datasrc.file, "data")
+ return ingest_data(datasrc)
+end
+
# 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"
@@ -404,19 +469,17 @@
data = data['root']
rescue Exception => ex
@logger.error "There was a problem with the data file. #{ex.message}"
end
when "csv"
- output = []
+ data = []
i = 0
begin
CSV.foreach(datasrc.file, headers: true, skip_blanks: true) do |row|
- output[i] = row.to_hash
+ data[i] = row.to_hash
i = i+1
end
- output = {"data" => output}
- data = output
rescue
@logger.error "The CSV format is invalid."
end
when "regex"
if datasrc.pattern
@@ -424,10 +487,14 @@
else
@logger.error "You must supply a regex pattern with your free-form data file."
raise "MissingRegexPattern"
end
end
+ if data.is_a? Array
+ data = {"data" => data}
+ end
+ return data
end
def parse_regex data_file, pattern
records = []
pattern_re = /#{pattern}/
@@ -454,17 +521,12 @@
return output
end
# Parse given data using given template, generating given output
def liquify datasrc, template_file, output
- @logger.debug "Executing liquify parsing operation."
- if datasrc.is_a? String
- datasrc = DataSrc.new(datasrc)
- end
- validate_file_input(datasrc.file, "data")
+ data = get_data(datasrc)
validate_file_input(template_file, "template")
- 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
@@ -492,10 +554,33 @@
else # if stdout
puts "========\nOUTPUT: Rendered with template #{template_file}:\n\n#{rendered}\n"
end
end
+def regurgidata datasrc, output
+ data = get_data(datasrc)
+ raise "UnrecognizedFileExtension" unless File.extname(output).match(/\.yml|\.json|\.xml|\.csv/)
+ case File.extname(output)
+ when ".yml"
+ new_data = data.to_yaml
+ when ".json"
+ new_data = data.to_json
+ when ".xml"
+ @logger.warn "XML output not yet implemented."
+ when ".csv"
+ @logger.warn "CSV output not yet implemented."
+ end
+ if new_data
+ begin
+ File.open(output, 'w') { |file| file.write(new_data) }
+ @logger.info "Data converted and saved to #{output}."
+ rescue
+ raise "FileWriteError"
+ end
+ end
+end
+
# ===
# MIGRATE-type procs
# ===
# Copy images and other files into target dir
@@ -525,13 +610,13 @@
# RENDER-type procs
# ===
# Gather attributes from one or more fixed attributes files
def ingest_attributes attr_file
- file_array = attr_file.split(",")
+ attr_files_array = attr_file.force_array
attrs = {}
- for f in file_array
+ attr_files_array.each do |f|
if f.include? ":"
file = f.split(":")
filename = file[0]
block_name = file[1]
else
@@ -569,62 +654,163 @@
backend = "html5"
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)
+ else
+ raise "UnrecognizedBackend"
+ end
+end
+
def asciidocify doc, build
@logger.debug "Executing Asciidoctor render operation for #{build.output}."
to_file = build.output
unless doc.type == build.doctype
- if build.doctype.nil?
+ if build.doctype.nil? # set a default doctype equal to our LiquiDoc action doc type
build.set("doctype", doc.type)
end
end
- back = derive_backend(doc.type, build.output)
- unless build.style.nil?
- case back
- when "pdf"
- doc.add_attrs!({"pdf-style"=>build.style})
- when "html5"
- doc.add_attrs!({"stylesheet"=>build.style})
+ # unfortunately we have to treat attributes accumilation differently for Jekyll vs Asciidoctor
+ attrs = doc.attributes # Start with attributes added at the action level; no more writing to doc obj
+ # Handle properties files array as attributes files and
+ # add the ingested attributes to local var
+ begin
+ if build.prop_files_array
+ ingested = ingest_attributes(build.prop_files_array)
+ attrs.merge!(ingested)
else
- raise "UnrecognizedBackend"
+ puts build.prop_files_array
end
+ rescue Exception => ex
+ @logger.warn "Attributes failed to merge. #{ex}" # Shd only trigger if build.props exists
+ raise
end
+ if build.backend == "html5" # Insert a stylesheet
+ attrs.merge!({"stylesheet"=>build.style}) if build.style
+ end
# Add attributes from config file build section
- doc.add_attrs!(build.attributes.to_h)
+ attrs.merge!(build.attributes) # Finally merge attributes from the build step
# Add attributes from command-line -a args
- doc.add_attrs!(@passed_attrs)
- @logger.debug "Final pre-parse attributes: #{doc.attributes}"
+ @logger.debug "Final pre-parse attributes: #{attrs.to_yaml}"
# Perform the aciidoctor convert
- unless back == "pdf"
+ unless build.backend == "pdf"
Asciidoctor.convert_file(
doc.index,
to_file: to_file,
- attributes: doc.attributes,
+ attributes: attrs,
require: "pdf",
- backend: back,
+ backend: build.backend,
doctype: build.doctype,
safe: "unsafe",
sourcemap: true,
verbose: @verbose,
mkdirs: true
)
else # For PDFs, we're calling the asciidoctor-pdf CLI, as the main dependency doesn't seem to perform the same way
- attributes = '-a ' + doc.attributes.map{|k,v| "#{k}='#{v}'"}.join(' -a ')
- command = "asciidoctor-pdf -o #{to_file} -b pdf -d #{build.doctype} -S unsafe #{attributes} -a no-header-footer --trace #{doc.index}"
+ attrs = '-a ' + attrs.map{|k,v| "#{k}='#{v}'"}.join(' -a ')
+ command = "asciidoctor-pdf -o #{to_file} -b pdf -d #{build.doctype} -S unsafe #{attrs} -a no-header-footer --trace #{doc.index}"
+ @logger.info "Generating PDF. This can take some time..."
@logger.debug "Running #{command}"
system command
end
+ @logger.debug "AsciiDoc attributes: #{doc.attributes}"
@logger.info "Rendered file #{to_file}."
end
+def generate_site doc, build
+ case build.backend
+ when "jekyll"
+ attrs = doc.attributes
+ build.add_config_file("_config.yml") unless build.prop_files_array
+ jekyll_config = YAML.load_file(build.prop_files_array[0]) # load the first Jekyll config file locally
+ 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")
+ config_list = build.prop_files_array.join(',') # flatten the Array back down for the CLI
+ opts_args = ""
+ if build.props['arguments']
+ opts_args = build.props['arguments'].to_opts_args
+ end
+ command = "bundle exec jekyll build --config #{config_list} #{opts_args}"
+ end
+ @logger.info "Running #{command}"
+ @logger.debug "AsciiDoc attributes: #{doc.attributes.to_yaml} "
+ system command
+ jekyll_serve(build) if @jekyll_serve
+end
+
# ===
-# Text manipulation Classes, Modules, filters, etc
+# DEPLOY procs
# ===
+def jekyll_serve build
+ # Locally serve Jekyll as per the primary Jekyll config file
+ config_file = build.props['files'][0]
+ if build.props['arguments']
+ opts_args = build.props['arguments'].to_opts_args
+ end
+ command = "bundle exec jekyll serve --config #{config_file} #{opts_args} --no-watch --skip-initial-build"
+ system command
+end
+
+# ===
+# Text manipulation Classes, Modules, procs, etc
+# ===
+
+module HashMash
+
+ def to_opts_args
+ out = ''
+ if self.is_a? Hash # TODO Should also be testing for flatness
+ self.each do |opt,arg|
+ out = out + " --#{opt} #{arg}"
+ end
+ end
+ return out
+ end
+
+end
+
+class Hash
+ include HashMash
+end
+
+module ForceArray
+ # So we can accept a list string ("item1.yml,item2.yml") or a single item ("item1.yml")
+ # and convert to array as needed
+ def force_array
+ obj = self
+ unless obj.class == Array
+ if obj.class == String
+ if obj.include? ","
+ obj = obj.split(",") # Will even force a string with no commas to a 1-item array
+ else
+ obj = Array.new.push(obj)
+ end
+ else
+ raise "ForceArrayFail"
+ end
+ end
+ return obj.to_ary
+ end
+
+end
+
class String
+ include ForceArray
# Adapted from Nikhil Gupta
# http://nikhgupta.com/code/wrapping-long-lines-in-ruby-for-display-in-source-files/
def wrap options = {}
width = options.fetch(:width, 76)
commentchar = options.fetch(:commentchar, '')
@@ -645,10 +831,14 @@
self.wrap(width: width).indent(spaces: spaces)
end
end
+class Array
+ include ForceArray
+end
+
# Extending Liquid filters/text manipulation
module CustomFilters
def plainwrap input
input.wrap
end
@@ -673,11 +863,10 @@
end
# register custom Liquid filters
Liquid::Template.register_filter(CustomFilters)
-
# ===
# Command/options parser
# ===
# Define command-line option/argument parameters
@@ -729,10 +918,18 @@
opts.on("--stdout", "Puts the output in STDOUT instead of writing to a file.") do
@output_type = "stdout"
end
+ opts.on("--clean PATH", "Force deletes the designated directory and all its contents WITHOUT WARNING.") do |n|
+ @clean_dir = n
+ 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
puts opts
exit
end
@@ -745,10 +942,12 @@
@logger.debug "Config file: #{@config_file}"
# ===
# Execute
# ===
-
+if @clean_dir
+ FileUtils.remove_dir(@clean_dir)
+end
unless @config_file
if @data_file
liquify(@data_file, @template_file, @output_file)
end
if @index_file