# frozen_string_literal: true ######### # # CollectionBuilder Page Generator, v1.3-csv # # Jekyll plugin to generate pages from _data/ files. # Designed to create Item pages from metadata CSV for digital collection sites. # (c) 2021 CollectionBuilder, evanwill, https://github.com/CollectionBuilder/ # Distributed under the conditions of the MIT license # # Originally inspired by jekyll-datapage_gen, https://github.com/avillafiorita/jekyll-datapage_gen # ######### module CollectionBuilderPageGenerator class ItemPageGenerator < Jekyll::Generator safe true # include jekyll utils so can use slugify include Jekyll::Utils # main function to read config, data, and generate pages def generate(site) ######### # # Default Settings # # These values are used if not configured in the 'page_gen' object in _config.yml # Defaults follow CollectionBuilder specific conventions. # data_file_default = site.config['metadata'] || 'metadata' # _data to use template_location_default = "item/" # folder in _layouts used to organize templates, ends with slash, empty if using root template_default = 'item' # layout to use for all pages by default display_template_default = 'display_template' # metadata column to use to assign layout name_default = 'objectid' # value to use for filename dir_default = 'items' # where to output pages extension_default = 'html' # extension, usually html filter_default = 'objectid' # value to filter records on, filters on objectid by default filter_condition_default = '!record["parentid"]' # expression to filter records on, default filters rows with a parentid # ###### # get optional configuration from _config.yml, or create a single default one from CB metadata setting configure_gen = site.config['page_gen'] || [{ 'data' => data_file_default }] # iterate over each instance in configuration # this allows to generate from multiple _data sources configure_gen.each do |data_config| data_file = data_config['data'] || data_file_default template_location = data_config['template_location'] || template_location_default template = template_location + (data_config['template'] || template_default) display_template = data_config['display_template'] || display_template_default name = data_config['name'] || name_default dir = data_config['dir'] || dir_default extension = data_config['extension'] || extension_default filter = data_config['filter'] || filter_default filter_condition = data_config['filter_condition'] || filter_condition_default # check if data value uses .csv extension, if so provide error message and skip. This avoids common CB error. if data_file.split('.')[1] == "csv" puts color_text("Error cb_page_gen: metadata value '#{data_file}' includes '.csv' extension. Please remove the extension from _config.yml 'metadata' or page_gen 'data' value. Pages are NOT being generated from '#{data_file}'!", :red) next end # check if data file exists, if not provide error message and skip. This supports nested key yml or json data sources if !site.data.key? data_file.split('.')[0] puts color_text("Error cb_page_gen: Data value '#{data_file}' does not match any site data. Please check _config.yml 'metadata' or page_gen 'data' value. Common issues are spelling errors. Pages are NOT being generated from '#{data_file}'!", :red) next end # Get the records to generate pages from # note: this splits on . to support a nested key in yml or json for page gen. # however, CB template and other plugins do not support nested yml or json for metadata items. # items described in unnested yml or json will work for both page gen and CB template pages. records = nil data_file.split('.').each do |level| if records.nil? records = site.data[level] else records = records[level] end end # Filter records if filter is configured (default is on objectid) if filter filtered_records = records.select { |r| r[filter] } filtered_number = records.size - filtered_records.size # provide notice if filter is applied puts color_text("Notice cb_page_gen: filter on '#{filter}' is applied. #{filtered_number} records are filtered because they do not have a value in '#{filter}'.", :green) if filtered_number != 0 records = filtered_records end # Filter records if filter_condition is configured if filter_condition filtered_records = records.select { |record| eval(filter_condition) } filtered_number = records.size - filtered_records.size records = filtered_records # provide notice if non-default filter is applied if filter_condition != filter_condition_default puts color_text("Notice cb_page_gen: filter_condition '#{filter_condition}' is applied. #{filtered_number} records are filtered.", :green) if filtered_number != 0 end end # Check for unique names, if not provide error message names_test = records.map { |x| x[name] } if names_test.size != names_test.uniq.size puts color_text("Error cb_page_gen: some values in '#{name}' are not unique! This means those pages will overwrite each other, so you will be missing some Item pages. Please check '#{name}' and make them all unique.", :red) end # Check for missing layouts template_test = records.map { |x| x[display_template] ? template_location + x[display_template].strip : template }.uniq all_layouts = site.layouts.keys missing_layouts = (template_test - all_layouts) if !missing_layouts.empty? # if there is missing layouts if all_layouts.include? template # if there is a valid default layout fallback, continue puts color_text("Notice cb_page_gen: could not find layout(s) #{missing_layouts.join(', ')} in '_layouts/'. Records with these display_template will fallback to the default layout '#{template}'. If this is unexpected, please add the missing layout(s) or check configuration of 'template' and 'display_template' field.", :yellow) else # if there is no valid fallback / template puts color_text("Notice cb_page_gen: could not find layout(s) #{missing_layouts.join(', ')} in '_layouts/'. This includes the default layout '#{template}'. Please add the layout(s) or check configuration of 'template' and 'display_template'. Item pages will not be generated for records using the missing layouts!", :yellow) #next end end # Generate pages for each record records.each_with_index do |record, index| # Check for valid name, skip page gen if none if record[name].nil? || record[name].strip.empty? puts color_text("Notice cb_page_gen: record '#{index}' in '#{data_file}' does not have a value in '#{name}'! This record will be skipped.", :yellow) next end # create clean filename with Jekyll Slugify pretty mode # this ensures safe filenames, but may cause unintended issues with links if objectid are not well formed record['base_filename'] = slugify(record[name], mode: "pretty").to_s puts color_text("Notice cb_page_gen: record '#{index}' in '#{data_file}', '#{record[name]}' is being sanitized to create a valid filename. This may cause issues with links generated on other pages. Please check the naming convention used in '#{name}' field.", :yellow) if record['base_filename'] != record[name] # Provide index number for page object record['index_number'] = index # Find next item if index == records.size - 1 next_item = records[0][name] else next_item = records[index + 1][name] end record['next_item'] = "/" + dir + "/" + slugify(next_item, mode: "pretty").to_s + "." + extension.to_s # Find previous item if index == 0 previous_item = records[records.size - 1][name] else previous_item = records[index -1][name] end record['previous_item'] = "/" + dir + "/" + slugify(previous_item, mode: "pretty").to_s + "." + extension.to_s # Add layout value from display_template or the default if record[display_template] record['layout'] = template_location + record[display_template].strip # if not valid layout, fall back to template default if !all_layouts.include? record['layout'] record['layout'] = template end else record['layout'] = template end # Check if layout exists, if not provide error message and skip if !all_layouts.include? record['layout'] puts color_text("Error cb_page_gen: Could not find layout '#{record['layout']}'. Please check configuration or add the layout. Item page NOT generated for record '#{index}' in '#{data_file}'!", :red) next end # Pass the page data to the ItemPage generator site.pages << ItemPage.new(site, record, dir, extension) end end end # Color helper, to add warning colors to message outputs # use like: puts color_text("example", :red) def text_colors @colors = { red: 31, yellow: 33, green: 32 } end def color_text(str, color_code) "\e[#{text_colors[color_code]}m#{str}\e[0m" end end # Subclass of `Jekyll::Page` with custom method definitions. class ItemPage < Jekyll::Page # function to generate each individual page def initialize(site, record, dir, extension) @site = site # the current site instance. @base = site.source # path to the source directory. @dir = dir # the directory the page will output in @basename = record['base_filename'] # filename without the extension. @ext = "." + extension.to_s # the extension. @name = record['base_filename'] + "." + extension.to_s # @basename + @ext. # add record data to the page # all record data will be available in page object @data = record end end end