class CiteProc::Ruby::Renderer
Constants
- PAGE_RANGE_PATTERN
Attributes
Public Class Methods
# File lib/citeproc/ruby/renderer.rb, line 10 def initialize(options_or_engine = nil) @state = State.new case options_or_engine when Engine @engine = options_or_engine when Hash locale, format = options_or_engine.values_at(:locale, :format) @locale = locale.is_a?(CSL::Locale) ? locale : CSL::Locale.load(locale) @format = Format.load(format) end end
Public Instance Methods
# File lib/citeproc/ruby/renderer.rb, line 23 def abbreviate(*arguments) return unless engine engine.abbreviate(*arguments) end
# File lib/citeproc/ruby/renderer.rb, line 29 def allow_locale_overrides? return false unless engine engine.options[:allow_locale_overrides] end
# File lib/citeproc/ruby/renderer/state.rb, line 9 def bibliography_mode? state.mode == 'bibliography' end
# File lib/citeproc/ruby/renderer/state.rb, line 5 def citation_mode? state.mode == 'citation' end
# File lib/citeproc/ruby/renderer/names.rb, line 117 def completely_substitute?(names) # Substitution applies only to the first names # node being rendered! return false if state.rendered_names? state.store_authors! names previous_names = state.previous_authors return false unless previous_names names == previous_names[0] end
Concatenates two strings, making sure that squeezable characters are not duplicated between string and suffix.
@param [String] string @param [String] suffix
@return [String] new string consisting of string
and suffix
# File lib/citeproc/ruby/renderer/format.rb, line 33 def concat(string, suffix) format.concat(string, suffix) end
# File lib/citeproc/ruby/renderer/names.rb, line 93 def count_names(names, node) names.reduce(0) do |count, (_, ns)| if node.truncate?(ns) count + node.truncate(ns).length else count + ns.length end end end
Evaluates the conditions of the passed-in Choose::Block against the passed-in CitationItem using the Block’s matcher.
@param item [CiteProc::CitationItem] @param node [CSL::Style::Choose::Block]
@return [Boolean] whether or not the node’s conditions
are true for the passed-in item
# File lib/citeproc/ruby/renderer/choose.rb, line 39 def evaluates?(item, node) # subtle: else-nodes have no conditions. since the default # matcher :all? returns true for an empty list we do not # need to check for an else node specifically. # return true if node.nodename == 'else' node.conditions.send(node.matcher) do |type, matcher, values| case type when :disambiguate false # TODO not implemented yet when :'is-numeric' evaluates_condition? matcher, values do |value| v = item.data.unobservable_read_attribute(value) v.respond_to?(:numeric?) && v.numeric? end when :'is-uncertain-date' evaluates_condition? matcher, values do |value| v = item.data.unobservable_read_attribute(value) v.respond_to?(:uncertain?) && v.uncertain? end when :locator locator = item.locator.to_s.tr(' ', '-') evaluates_condition? matcher, values do |value| value.to_s == locator end when :position false # TODO not implemented yet when :type type = item.data.unobservable_read_attribute(:type).to_s evaluates_condition? matcher, values do |value| value.to_s == type end when :variable evaluates_condition? matcher, values do |value| item.data.attribute?(value) end else fail "unknown condition type: #{type}" end end end
Evaluates the passed-in block for each value in values, negating the result if the value is prefixed with ‘not:’
# File lib/citeproc/ruby/renderer/choose.rb, line 95 def evaluates_condition?(matcher, values, &condition) values.send(matcher) do |value| value, negate = value.split(/^not:/, 2).reverse result = condition.call(value) negate ? !result : result end end
# File lib/citeproc/ruby/renderer/format.rb, line 7 def format @format ||= Format.load end
Applies the current format on the string using the node’s formatting options.
# File lib/citeproc/ruby/renderer/format.rb, line 17 def format!(string, node) format.apply(string, node, locale) end
# File lib/citeproc/ruby/renderer/format.rb, line 11 def format=(format) @format = Format.load(format) end
Formats
pages accoring to format. Valid formats are:
-
“chicago”: page ranges are abbreviated according to the Chicago Manual of Style rules.
-
“expanded”: Abbreviated page ranges are expanded to their non-abbreviated form: 42-45, 321-328, 2787-2816.
-
“minimal”: All digits repeated in the second number are left out: 42-45, 321-8, 2787-816.
@param [String] pages to be formatted @param [String] format to use for formatting
# File lib/citeproc/ruby/renderer/format.rb, line 55 def format_page_range(pages, format) return if pages.nil? format_page_range!(pages.dup, format) end
# File lib/citeproc/ruby/renderer/format.rb, line 60 def format_page_range!(pages, format) return if pages.nil? return pages if pages.empty? dash = translate('page-range-delimiter') || '–' # en-dash pages.gsub! PAGE_RANGE_PATTERN do affixes, f, t = [$1, $3, $4, $6], $2, $5 # When there are affixes or no format was # specified we skip this part. As a result, # only the delimiter will be replaced! if affixes.all?(&:empty?) && !format.nil? dim = f.length delta = dim - t.length if delta >= 0 t.prepend f[0, delta] unless delta.zero? if format == 'chicago' changes = dim - f.chars.zip(t.chars). take_while { |a,b| a == b }.length if dim == 4 format = case when dim < 3 'expanded' when dim == 4 && changes > 2 'expanded' when f[-2, 2] == '00' 'expanded' when f[-2] == '0' 'minimal' else 'minimal-two' end end case format when 'expanded' # nothing to do when 'minimal' t = t.each_char.drop_while.with_index { |c, i| c == f[i] }.join('') when 'minimal-two' if dim > 2 t = t.each_char.drop_while.with_index { |c, i| c == f[i] && dim - i > 2 }.join('') end else raise ArgumentError, "unknown page range format: #{format}" end end end affixes.zip([f, dash, t]).flatten.compact.join('') end pages end
# File lib/citeproc/ruby/renderer/names.rb, line 130 def individually_substitute!(names) end
# File lib/citeproc/ruby/renderer/format.rb, line 21 def join(list, delimiter = nil) format.join(list, delimiter) end
# File lib/citeproc/ruby/renderer/locale.rb, line 6 def locale @locale ||= CSL::Locale.load end
# File lib/citeproc/ruby/renderer/locale.rb, line 10 def locale=(locale) @locale = CSL::Locale.load(locale) end
@return [String] number as an ordinal
# File lib/citeproc/ruby/renderer/locale.rb, line 19 def ordinalize(number, options = {}) locale.ordinalize(number, options) end
@param item [CiteProc::CitationItem] @param node [CSL::Node] @return [String] the rendered and formatted string
# File lib/citeproc/ruby/renderer.rb, line 37 def render(item, node) raise ArgumentError, "no CSL node: #{node.inspect}" unless node.respond_to?(:nodename) specialize = "render_#{node.nodename.tr('-', '_')}" raise ArgumentError, "#{specialize} not implemented" unless respond_to?(specialize, true) format! send(specialize, item, node), node end
@param item [CiteProc::CitationItem] @param node [CSL::Style::Bibliography] @return [String] the rendered and formatted string
# File lib/citeproc/ruby/renderer.rb, line 77 def render_bibliography(item, node) state.store! item, node if allow_locale_overrides? && item.language != locale.language begin new_locale = CSL::Locale.load(item.language) unless new_locale.nil? original_locale, @locale = @locale, new_locale end rescue ParseError # locale not found end end result = render item, node.layout ensure unless original_locale.nil? @locale = original_locale end state.clear! result end
@param item [CiteProc::CitationItem] @param node [CSL::Style::Choose::Block] @return [String]
# File lib/citeproc/ruby/renderer/choose.rb, line 22 def render_block(item, node) return '' unless node.has_children? join node.each_child.map { |child| render item, child } end
@param item [CiteProc::CitationItem] @param node [CSL::Style::Choose] @return [String]
# File lib/citeproc/ruby/renderer/choose.rb, line 9 def render_choose(item, node) return '' unless node.has_children? node.each_child do |child| return render_block(item, child) if evaluates?(item, child) end '' # no block was rendered end
@param data [CiteProc::CitationData] @param node [CSL::Style::Citation] @return [String] the rendered and formatted string
# File lib/citeproc/ruby/renderer.rb, line 52 def render_citation(data, node) state.store! data, node citations = join data.map { |item| render_single_citation item, node.layout }, node.layout.delimiter || '' result = format! citations, node.layout ensure state.clear! result end
@param item [CiteProc::CitationItem] @param node [CSL::Node] @raise RenderingError @return [String]
# File lib/citeproc/ruby/renderer/date.rb, line 10 def render_date(item, node) return '' unless node.has_variable? date = item.data[node.variable] return '' if date.nil? || date.empty? return date.to_s if date.literal? # TODO date-ranges if node.localized? localized_node = locale.date.detect { |d| d.form == node.form } or raise RenderingError, "no localized date for form #{node.form} found" delimiter, filter = node.delimiter, node.parts_filter parts = localized_node.parts.select do |part| filter.include? part.name end else parts, delimiter = node.parts, node.delimiter end parts.map { |part| render date, part }.reject(&:empty?).join(delimiter) end
@param date [CiteProc::Date] @param node [CSL::Style::DatePart, CSL::Locale::DatePart] @return [String]
# File lib/citeproc/ruby/renderer/date.rb, line 41 def render_date_part(date, node) case when node.day? case when date.day.nil? '' when node.form == 'ordinal' if date.day > 1 && locale.limit_day_ordinals? date.day.to_s else ordinalize date.day end when node.form == 'numeric-leading-zeros' '%02d' % date.day else date.day.to_s end when node.month? case when date.season? translate(('season-%02d' % date.season), node.attributes_for(:form)) when date.month.nil? '' when node.numeric? date.month.to_s when node.numeric_leading_zeros? '%02d' % date.month else translate(('month-%02d' % date.month), node.attributes_for(:form)) end when node.year? year = date.year year = year % 100 if node.short? if date.ad? year = year.to_s year << translate(:ad) if date.ad? elsif date.bc? year = (-1*year).to_s year << translate(:bc) if date.bc? else year = year.to_s end year else '' end end
@param item [CiteProc::CitationItem] @param node [CSL::Style::Group] @return [String]
# File lib/citeproc/ruby/renderer/group.rb, line 9 def render_group(item, node) return '' unless node.has_children? observer = ItemObserver.new(item.data) observer.start begin rendition = node.each_child.map { |child| render item, child }.reject(&:empty?) rendition = join(rendition, node.delimiter) ensure observer.stop end return '' if observer.skip? rendition end
@param names [CiteProc::Name] @param node [CSL::Style::Name] @param position [Fixnum] @return [String]
# File lib/citeproc/ruby/renderer/names.rb, line 230 def render_individual_name(name, node, position = 1) if name.personal? name = name.dup # TODO move parts of the formatting logic here # because name parts may include particles etc. # Strip away some unusual characters to normalize # sort order for names. if sort_mode? name.family = name.family.to_s.gsub(/[\[\]]|^\W+/, '') end name.options.merge! node.name_options name.sort_order! node.name_as_sort_order_at?(position) name.initialize_without_hyphen! if node.initialize_without_hyphen? if style && style.demote_particle? name.options[:'demote-non-dropping-particle'] = style.demote_particle end # Strip away (hyphenated) particles in sort mode! if sort_mode? && name.demote_particle? name.family = name.family.to_s.sub(/^[[:lower:]]+[\s-]/, '') end node.name_part.each do |part| case part[:name] when 'family' if !name.particle? || name.demote_particle? name.family = format!(name.family, part) else name.family = format!("#{name.particle} #{name.family}", part) name.particle = nil end # Name suffix must be enclosed by family-part # suffix in display order! if name.has_suffix? && !name.sort_order? && part.attribute?(:suffix) comma = name.comma_suffix? ? name.comma : ' ' suffix = part[:suffix] name.family.chomp! suffix name.family.concat "#{comma}#{name.suffix}#{suffix}" name.suffix = nil end when 'given' if name.dropping_particle? name.given = format!("#{name.initials} #{name.dropping_particle}", part) name.dropping_particle = nil else name.given = format!(name.initials, part) end # Demoted particles must be enclosed by # given-part affixes in sort order! if name.particle? && name.demote_particle? && name.sort_order? && part.attribute?(:suffix) suffix = part[:suffix] name.given.chomp! suffix name.given.concat " #{name.particle}#{suffix}" name.particle = nil end end end end format! name.format, node end
@param item [CiteProc::CitationItem] @param node [CSL::Style::Label] @param variable [String]
@return [String]
# File lib/citeproc/ruby/renderer/label.rb, line 11 def render_label(item, node, variable = node.variable) return '' if variable.nil? || variable.empty? case when node.page? value, name = item.read_attribute(:page) || item.data.unobservable_read_attribute(:page).to_s, :page format_page_range!(value, node.page_range_format) when node.locator? # Subtle: when there is no locator we also look # in item.data; there should be no locator there # either but the read access will be noticed by # observers (if any). value, name = item.locator || item.data.unobservable_read_attribute(:locator), item.label || 'page' when node.names_label? # We handle the editortranslator special case # by fetching editors since we can assume # that both are present and identical! if variable == :editortranslator value, name = item.data.unobservable_read_attribute(:editor), variable.to_s else value, name = item.data.unobservable_read_attribute(variable), variable.to_s end else value, name = item.data.unobservable_read_attribute(variable), node.term end return '' if value.nil? || value.respond_to?(:empty?) && value.empty? options = node.attributes_for :form options[:plural] = case when node.always_pluralize? true when node.never_pluralize? false when node.number_of_pages?, node.number_of_volumes? value.to_i > 1 when value.respond_to?(:plural?) value.plural? else CiteProc::Number.pluralize?(value.to_s) end translate name, options end
Formats
one or more names according to the configuration of the passed-in node. Returns the formatted name(s) as a string.
@param names [CiteProc::Names] @param node [CSL::Style::Name] @return [String]
# File lib/citeproc/ruby/renderer/names.rb, line 140 def render_name(names, node) # TODO handle subsequent citation rules delimiter = node.delimiter connector = node.connector connector = translate('and') if connector == 'text' # Add spaces around connector connector = " #{connector} " unless connector.nil? rendered_names = case when node.truncate?(names) truncated = node.truncate(names) return '' if truncated.empty? if node.delimiter_precedes_last?(truncated) connector = join [delimiter, connector].compact end if node.ellipsis? && names.length - truncated.length > 1 join [ join(truncated.map.with_index { |name, idx| render_individual_name name, node, idx + 1 }, delimiter), render_individual_name(names[-1], node, truncated.length + 1) ], node.ellipsis else others = node.et_al ? format!(translate(node.et_al[:term]), node.et_al) : translate('et-al') connector = node.delimiter_precedes_et_al?(truncated) ? delimiter : ' ' join [ join(truncated.map.with_index { |name, idx| render_individual_name name, node, idx + 1 }, delimiter), others ], connector end when names.length < 3 if node.delimiter_precedes_last?(names) connector = [delimiter, connector].compact.join('').squeeze(' ') end join names.map.with_index { |name, idx| render_individual_name name, node, idx + 1 }, connector || delimiter else if node.delimiter_precedes_last?(names) connector = [delimiter, connector].compact.join('').squeeze(' ') end join [ join(names[0...-1].map.with_index { |name, idx| render_individual_name name, node, idx + 1 }, delimiter), render_individual_name(names[-1], node, names.length) ], connector || delimiter end if substitute_subsequent_authors_completely? && completely_substitute?(rendered_names) rendered_names = state.node.subsequent_author_substitute end format! rendered_names, node end
@param item [CiteProc::CitationItem] @param node [CSL::Style::Names] @return [String]
# File lib/citeproc/ruby/renderer/names.rb, line 9 def render_names(item, node) return '' unless node.has_variable? names = node.variable.split(/\s+/).map do |role| [role.to_sym, item.data[role]] end suppressed = names.reject! { |n| item.suppressed? n[0] } names.reject! { |n| n[1].nil? || n[1].empty? } if names.empty? # We also return when the list is empty because # of a suppression, because we do not want to # substitute suppressed items! return '' unless suppressed.nil? && node.has_substitute? rendered_names = render_substitute item, node.substitute if substitute_subsequent_authors_completely? && completely_substitute?(rendered_names) rendered_names = state.node.subsequent_author_substitute end rendered_names else resolve_editor_translator_exception! names # Pick the names node that will be used for # formatting; if we are currently in substiution # mode, the node that is being substituted for # will take precedence if the current node is # a descendant of it. # # This makes sure that nodes in macros do not # use the original names node. # # When the current node has children the names # will not be substituted either. if substitution_mode? && !node.has_children? && node.ancestors.include?(state.substitute) names_node = state.substitute else names_node = node end name = name_node_for(names_node) return count_names(names, name).to_s if name.count? names.map! do |role, ns| if names_node.has_label? label = render_label item, names_node.label, role label = format! label, names_node.label rendered_names = render_name(ns, name) if rendered_names.empty? rendered_names else if names_node.prefix_label? concat label, rendered_names else concat rendered_names, label end end else render_name ns, name end end join names, names_node.delimiter(state.node) end ensure state.rendered_names! end
@param item [CiteProc::CitationItem] @param node [CSL::Style::Number] @return [String]
# File lib/citeproc/ruby/renderer/number.rb, line 9 def render_number(item, node) return '' unless node.has_variable? variable = item.data[node.variable] return variable.to_s unless variable && variable.numeric? numbers = variable.tokenize case when node.ordinal? || node.long_ordinal? options = node.attributes_for :form # TODO lookup term of variable to check gender numbers.map! do |num| num =~ /^\d+$/ ? ordinalize(num, options) : num end when node.roman? numbers.map! do |num| num =~ /^\d+$/ ? romanize(num) : num end else # nothing end numbers.join('') end
@param data [CiteProc::CitationItem] @param node [CSL::Style::Layout] @return [String] the rendered and string
# File lib/citeproc/ruby/renderer.rb, line 67 def render_single_citation(item, node) # TODO author_only item.suppress! 'author' if item.suppress_author? join [item.prefix, render_layout(item, node), item.suffix].compact end
# File lib/citeproc/ruby/renderer.rb, line 102 def render_sort(a, b, node, key) state.store! nil, key original_format = @format @format = Formats::Sort.new if a.is_a?(CiteProc::Names) [render_name(a, node), render_name(b, node)] else # We need to clear any items that are suppressed # because they were used as substitutes during # rendering for sorting purposes! a_rendered = render a.cite, node a.suppressed.clear b_rendered = render b.cite, node b.suppressed.clear [a_rendered, b_rendered] end ensure @format = original_format state.clear! end
@param item [CiteProc::CitationItem] @param node [CSL::Style::Substitute] @return [String]
# File lib/citeproc/ruby/renderer/names.rb, line 311 def render_substitute(item, node) return '' unless node.has_children? if substitution_mode? saved_substitute = state.substitute end state.substitute! node.parent observer = ItemObserver.new(item.data) node.each_child do |child| observer.start begin string = render(item, child) unless string.empty? # Variables rendered as substitutes # must be suppressed during the remainder # of the rendering process! item.suppress!(*observer.accessed) # Report a read-access using the substitution string # for the name variable being substituted, or the first # name variable (if there are more than one). variable = node.parent.variable[/\w+/] item.data.simulate_read_attribute variable, string return string # break out of each loop! end ensure observer.stop observer.clear! end end '' # no substitute was rendered ensure state.clear_substitute! saved_substitute end
@param item [CiteProc::CitationItem] @param node [CSL::Style::Text] @return [String]
# File lib/citeproc/ruby/renderer/text.rb, line 9 def render_text(item, node) case when node.has_variable? if node.variable == 'locator' # Subtle: when there is no locator we also look # in item.data; there should be no locator there # either but the read access will be noticed by # observers (if any). text = item.locator || item.data[:locator].to_s else text = item.data.variable(node.variable, node.variable_options).to_s # Check for abbreviations or short-form fallbacks! context, was_short_form = node.variable.split(/-short$/, 2) if !was_short_form.nil? || node[:form] == 'short' if text.empty? && context != node.variable text = item.data.variable(context, node.variable_options).to_s end text = abbreviate(context, text) || text end end case when node.variable == 'page' format_page_range!(text, node.page_range_format) when node.variable == 'page-first' && text.empty? text = item.data[:'page'].to_s[/\d+/].to_s end text when node.has_macro? render item, node.macro when node.has_term? translate node[:term], node.attributes_for(:plural, :form) else node.value.to_s end end
@return [String] the roman numeral of number
# File lib/citeproc/ruby/renderer/format.rb, line 39 def romanize(number) CiteProc::Number.romanize(number) end
# File lib/citeproc/ruby/renderer/state.rb, line 13 def sort_mode? state.mode == 'key' end
# File lib/citeproc/ruby/renderer/state.rb, line 21 def style return unless state.node && !state.node.root? && state.node.root.is_a?(CSL::Style) state.node.root end
# File lib/citeproc/ruby/renderer/state.rb, line 17 def substitution_mode? !state.substitute.nil? end
# File lib/citeproc/ruby/renderer/locale.rb, line 14 def translate(name, options = {}) locale.translate(name, options) end
Private Instance Methods
# File lib/citeproc/ruby/renderer/names.rb, line 357 def name_node_for(names_node) # Make a copy of the name node and inherit # options from root and citation/bibliography # depending on current rendering mode. if names_node.has_name? name = names_node.name.deep_copy else name = CSL::Style::Name.new end # Inherit name options from style and the # current rendering node. We pass both in, # because this is now an unlinked node! options = name.inherited_name_options(state.node, names_node.root) name.reverse_merge! options name.et_al = names_node.et_al if names_node.has_et_al? # Override options if we are rendering a sort key! if sort_mode? name.merge! state.node.name_options name.all_names_as_sort_order! end name end
@param item [CiteProc::CitationItem] @param node [CSL::Style::Layout] @return [String]
# File lib/citeproc/ruby/renderer/layout.rb, line 11 def render_layout(item, node) join node.each_child.map { |child| render item, child }.reject(&:empty?), node.delimiter end
@param item [CiteProc::CitationItem] @param node [CSL::Style::Layout] @return [String]
# File lib/citeproc/ruby/renderer/macro.rb, line 11 def render_macro(item, node) node.each_child.map { |child| render item, child }.join('') end
# File lib/citeproc/ruby/renderer/names.rb, line 384 def resolve_editor_translator_exception!(names) i = names.index { |role, _| role == :translator } return names if i.nil? j = names.index { |role, _| role == :editor } return names if j.nil? return names unless names[i][1] == names[j][1] # rename the first instance and drop the second one i, j = j, i if j < i names[i][0] = :editortranslator names.slice!(j) names end