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