citeproc-ruby 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +78 -0
- data/lib/citeproc.rb +100 -0
- data/lib/citeproc/bibliography.rb +57 -0
- data/lib/citeproc/data.rb +149 -0
- data/lib/citeproc/date.rb +133 -0
- data/lib/citeproc/formatter.rb +38 -0
- data/lib/citeproc/item.rb +53 -0
- data/lib/citeproc/name.rb +284 -0
- data/lib/citeproc/processor.rb +166 -0
- data/lib/citeproc/selector.rb +61 -0
- data/lib/citeproc/variable.rb +82 -0
- data/lib/citeproc/version.rb +3 -0
- data/lib/csl/locale.rb +223 -0
- data/lib/csl/node.rb +72 -0
- data/lib/csl/nodes.rb +1364 -0
- data/lib/csl/renderer.rb +88 -0
- data/lib/csl/sort.rb +53 -0
- data/lib/csl/style.rb +110 -0
- data/lib/csl/term.rb +124 -0
- data/lib/extensions/core.rb +43 -0
- data/lib/plugins/filters/bibtex.rb +12 -0
- data/lib/plugins/formats/default.rb +134 -0
- data/lib/plugins/formats/html.rb +67 -0
- data/lib/support/attributes.rb +99 -0
- data/lib/support/tree.rb +80 -0
- data/resource/locale/locales-af-ZA.xml +304 -0
- data/resource/locale/locales-ar-AR.xml +304 -0
- data/resource/locale/locales-bg-BG.xml +304 -0
- data/resource/locale/locales-ca-AD.xml +304 -0
- data/resource/locale/locales-cs-CZ.xml +304 -0
- data/resource/locale/locales-da-DK.xml +304 -0
- data/resource/locale/locales-de-AT.xml +304 -0
- data/resource/locale/locales-de-CH.xml +304 -0
- data/resource/locale/locales-de-DE.xml +332 -0
- data/resource/locale/locales-el-GR.xml +303 -0
- data/resource/locale/locales-en-US.xml +313 -0
- data/resource/locale/locales-es-ES.xml +304 -0
- data/resource/locale/locales-et-EE.xml +304 -0
- data/resource/locale/locales-fr-FR.xml +304 -0
- data/resource/locale/locales-he-IL.xml +304 -0
- data/resource/locale/locales-hu-HU.xml +304 -0
- data/resource/locale/locales-is-IS.xml +304 -0
- data/resource/locale/locales-it-IT.xml +304 -0
- data/resource/locale/locales-ja-JP.xml +304 -0
- data/resource/locale/locales-kh-KH.xml +303 -0
- data/resource/locale/locales-ko-KR.xml +304 -0
- data/resource/locale/locales-mn-MN.xml +304 -0
- data/resource/locale/locales-nb-NO.xml +304 -0
- data/resource/locale/locales-nl-NL.xml +304 -0
- data/resource/locale/locales-nn-NO.xml +304 -0
- data/resource/locale/locales-pl-PL.xml +304 -0
- data/resource/locale/locales-pt-BR.xml +304 -0
- data/resource/locale/locales-pt-PT.xml +304 -0
- data/resource/locale/locales-ro-RO.xml +304 -0
- data/resource/locale/locales-ru-RU.xml +304 -0
- data/resource/locale/locales-sk-SK.xml +304 -0
- data/resource/locale/locales-sl-SI.xml +304 -0
- data/resource/locale/locales-sr-RS.xml +304 -0
- data/resource/locale/locales-sv-SE.xml +304 -0
- data/resource/locale/locales-th-TH.xml +304 -0
- data/resource/locale/locales-tr-TR.xml +304 -0
- data/resource/locale/locales-uk-UA.xml +304 -0
- data/resource/locale/locales-vi-VN.xml +304 -0
- data/resource/locale/locales-zh-CN.xml +304 -0
- data/resource/locale/locales-zh-TW.xml +304 -0
- data/resource/schema/csl-categories.rnc +39 -0
- data/resource/schema/csl-data.rnc +98 -0
- data/resource/schema/csl-terms.rnc +106 -0
- data/resource/schema/csl-types.rnc +39 -0
- data/resource/schema/csl-variables.rnc +182 -0
- data/resource/schema/csl.rnc +941 -0
- data/resource/style/acta-materialia-x.csl +128 -0
- data/resource/style/advanced-engineering-materials-x.csl +121 -0
- data/resource/style/ama.csl +185 -0
- data/resource/style/ama2-x.csl +179 -0
- data/resource/style/apa-x.csl +324 -0
- data/resource/style/apa.csl +254 -0
- data/resource/style/apsa-x.csl +163 -0
- data/resource/style/apsa.csl +176 -0
- data/resource/style/asa-x.csl +203 -0
- data/resource/style/asa.csl +216 -0
- data/resource/style/asm-journals-x.csl +131 -0
- data/resource/style/bibtex-x2.csl +175 -0
- data/resource/style/bluebook-demo-x.csl +392 -0
- data/resource/style/bluebook-demo.csl +942 -0
- data/resource/style/chicago-author-date-listing.csl +434 -0
- data/resource/style/chicago-author-date.csl +369 -0
- data/resource/style/chicago-fullnote-bibliography-bb.csl +928 -0
- data/resource/style/chicago-fullnote-bibliography.csl +695 -0
- data/resource/style/chicago-note-bibliography.csl +446 -0
- data/resource/style/chicago-note.csl +388 -0
- data/resource/style/greek-chicago-x.csl +1182 -0
- data/resource/style/harvard1-institution-italic.csl +190 -0
- data/resource/style/harvard1.csl +181 -0
- data/resource/style/ieee.csl +129 -0
- data/resource/style/mhra-x.csl +312 -0
- data/resource/style/mhra.csl +390 -0
- data/resource/style/mhra_note_without_bibliography-x.csl +330 -0
- data/resource/style/mhra_note_without_bibliography.csl +338 -0
- data/resource/style/mla-x.csl +178 -0
- data/resource/style/mla.csl +189 -0
- data/resource/style/nature-x.csl +81 -0
- data/resource/style/nature.csl +88 -0
- data/resource/style/nlm.csl +117 -0
- data/spec/citeproc/bibliography_spec.rb +45 -0
- data/spec/citeproc/citeproc_spec.rb +76 -0
- data/spec/citeproc/date_spec.rb +85 -0
- data/spec/citeproc/formatter_spec.rb +101 -0
- data/spec/citeproc/item_spec.rb +71 -0
- data/spec/citeproc/name_spec.rb +30 -0
- data/spec/citeproc/processor_spec.rb +61 -0
- data/spec/citeproc/selector_spec.rb +82 -0
- data/spec/citeproc/variable_spec.rb +69 -0
- data/spec/csl/locale_spec.rb +208 -0
- data/spec/csl/node_spec.rb +25 -0
- data/spec/csl/nodes_spec.rb +140 -0
- data/spec/csl/style_spec.rb +62 -0
- data/spec/csl/term_spec.rb +56 -0
- data/spec/fixtures/dates.yaml +80 -0
- data/spec/fixtures/names.yaml +115 -0
- data/spec/fixtures/nodes.yaml +245 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/attributes_spec.rb +39 -0
- data/spec/support/tree_spec.rb +163 -0
- metadata +264 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module CiteProc
|
|
2
|
+
|
|
3
|
+
class Selector
|
|
4
|
+
include Support::Attributes
|
|
5
|
+
|
|
6
|
+
attr_fields :select, :include, :exclude, :quash
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def initialize(argument = {})
|
|
10
|
+
key_filter['all'] = 'select'
|
|
11
|
+
key_filter['any'] = 'include'
|
|
12
|
+
key_filter['none'] = 'exclude'
|
|
13
|
+
key_filter['skip'] = 'quash'
|
|
14
|
+
|
|
15
|
+
merge(normalize(argument))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def type
|
|
19
|
+
attributes.keys.detect { |k| [:select, :include, :exclude].include?(k.to_sym) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @returns one of :all?, :any?, :none?
|
|
23
|
+
def matcher
|
|
24
|
+
type == 'include' ? :any? : type == 'exclude' ? :none? : :all?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def conditions
|
|
28
|
+
attributes[type] || []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def matches?(item)
|
|
32
|
+
conditions.send(matcher) { |condition| match(item, condition) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def skip?(item)
|
|
36
|
+
has_quash? && quash.all? { |condition| match(item, condition) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_proc
|
|
40
|
+
Proc.new { |item| matches?(item) && !skip?(item) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
protected
|
|
44
|
+
|
|
45
|
+
def match(item, condition)
|
|
46
|
+
values, expected = [item[condition['field']]].flatten.map(&:to_s), [condition['value']].flatten
|
|
47
|
+
expected & values != []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def normalize(argument)
|
|
51
|
+
case
|
|
52
|
+
when [String, Symbol].include?(argument.class) && !(argument.to_s =~ /^\s*\{/)
|
|
53
|
+
{ argument.to_s => [] }
|
|
54
|
+
else
|
|
55
|
+
argument
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
|
|
3
|
+
module CiteProc
|
|
4
|
+
|
|
5
|
+
class Variable
|
|
6
|
+
extend Forwardable
|
|
7
|
+
|
|
8
|
+
include Support::Attributes
|
|
9
|
+
include Comparable
|
|
10
|
+
|
|
11
|
+
@date_fields = %w{ accessed container event-date issued original-date }
|
|
12
|
+
|
|
13
|
+
@name_fields = %w{
|
|
14
|
+
author editor translator recipient interviewer publisher composer
|
|
15
|
+
original-publisher original-author container-author collection-editor }
|
|
16
|
+
|
|
17
|
+
@text_fields = %w{
|
|
18
|
+
id abstract annote archive archive-location archive-place authority
|
|
19
|
+
call-number chapter-number citation-label citation-number collection-title
|
|
20
|
+
container-title DOI edition event event-place first-reference-note-number
|
|
21
|
+
genre ISBN issue jurisdiction keyword locator medium note number
|
|
22
|
+
number-of-pages number-of-volumes original-publisher original-publisher-place
|
|
23
|
+
original-title page page-first publisher publisher-place references
|
|
24
|
+
section status title URL version volume year-suffix }
|
|
25
|
+
|
|
26
|
+
@filters = Hash.new
|
|
27
|
+
|
|
28
|
+
@types = Hash.new(Variable)
|
|
29
|
+
|
|
30
|
+
attr_fields :value
|
|
31
|
+
|
|
32
|
+
def_delegators :value, :empty?, :to_s, :match
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
attr_reader :date_fields, :name_fields, :text_fields, :filters, :types
|
|
36
|
+
|
|
37
|
+
def fields
|
|
38
|
+
date_fields + name_fields + text_fields
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def filter(id, key)
|
|
42
|
+
Variable.filters[id][key]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse(values, name=nil)
|
|
46
|
+
values.is_a?(Array) ? values.map { |value| Variable.types[name].new(value) } :
|
|
47
|
+
Variable.types[name].new(values)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize(attributes={}, &block)
|
|
52
|
+
parse!(attributes)
|
|
53
|
+
yield self if block_given?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse!(argument)
|
|
57
|
+
argument = argument.to_hash if argument.is_a?(Variable)
|
|
58
|
+
argument.is_a?(Hash) ? self.merge!(argument) : self.value = argument.to_s
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def numeric?
|
|
62
|
+
to_s =~ /\d/
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @returns (first) numeric data contained in the variable's value
|
|
66
|
+
def to_i
|
|
67
|
+
to_s =~ /(-?\d[\d,\.]*)/ && $1.to_i || 0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def <=>(other)
|
|
72
|
+
strip_markup(to_s) <=> strip_markup(other.to_s)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
protected
|
|
76
|
+
|
|
77
|
+
def strip_markup(string)
|
|
78
|
+
string.gsub(/<[^>]*>/, '')
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
end
|
data/lib/csl/locale.rb
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
module CSL
|
|
2
|
+
|
|
3
|
+
class Locale
|
|
4
|
+
include Support::Tree
|
|
5
|
+
include Comparable
|
|
6
|
+
|
|
7
|
+
# Class Instance Variables
|
|
8
|
+
@path = File.expand_path('../../../resource/locale/', __FILE__)
|
|
9
|
+
@default = 'en-US'
|
|
10
|
+
|
|
11
|
+
# Language and region defaults
|
|
12
|
+
@regions = Hash.new { |hash, key| hash[key] = Locale.match_region(key) }
|
|
13
|
+
@regions['en'] = 'US'
|
|
14
|
+
@regions['de'] = 'DE'
|
|
15
|
+
@regions['pt'] = 'PT'
|
|
16
|
+
|
|
17
|
+
@languages = Hash.new { |hash, key| hash[key] = Locale.match_language(key) }
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
attr_accessor :path, :default, :regions, :languages
|
|
21
|
+
|
|
22
|
+
# @returns first available region for current language.
|
|
23
|
+
def match_region(language)
|
|
24
|
+
Dir.entries(Locale.path).detect { |l| l.match(%r/^[\w]+-#{language}-([A-Z]{2})\.xml$/) }
|
|
25
|
+
$1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @returns first available language for current region.
|
|
29
|
+
def match_language(region)
|
|
30
|
+
Dir.entries(Locale.path).detect { |l| l.match(%r/^[\w]+-([a-z]{2})-#{region}\.xml$/) }
|
|
31
|
+
$1
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
attr_reader :language, :region
|
|
37
|
+
|
|
38
|
+
alias :style :parent
|
|
39
|
+
|
|
40
|
+
# @param argument a language tag; or an XML node
|
|
41
|
+
def initialize(argument=nil, style=nil, &block)
|
|
42
|
+
case
|
|
43
|
+
when argument.is_a?(Nokogiri::XML::Node)
|
|
44
|
+
@language, @region = argument['lang'].split(/-/) if argument['lang']
|
|
45
|
+
parse!(argument)
|
|
46
|
+
|
|
47
|
+
when argument.is_a?(String) && argument.match(/^\s*<locale/)
|
|
48
|
+
argument = Nokogiri::XML.parse(argument) { |config| config.strict.noblanks }.root
|
|
49
|
+
@language, @region = argument['lang'].split(/-/) if argument['lang']
|
|
50
|
+
parse!(argument)
|
|
51
|
+
|
|
52
|
+
when argument.is_a?(String) || argument.is_a?(Symbol)
|
|
53
|
+
set(argument)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@parent = style
|
|
57
|
+
|
|
58
|
+
yield self if block_given?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def language=(language)
|
|
62
|
+
@language = language
|
|
63
|
+
@region = Locale.regions[language]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def region=(region)
|
|
67
|
+
@region = region
|
|
68
|
+
@language = Locale.languages[region]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse!(node)
|
|
72
|
+
raise(ArgumentError, "expected XML node, was: #{ node.inspect }") unless node.is_a?(Nokogiri::XML::Node)
|
|
73
|
+
|
|
74
|
+
@terms = Term.build(node)
|
|
75
|
+
|
|
76
|
+
@options = Hash[*node.css('style-options').map(&:attributes).map { |a| a.map { |name, a| [name, a.value] } }.flatten]
|
|
77
|
+
|
|
78
|
+
@date = Hash.new([])
|
|
79
|
+
['text', 'numeric'].each do |form|
|
|
80
|
+
@date[form] = node.css("date[form='#{form}'] > date-part").map do |part|
|
|
81
|
+
Nodes::DatePart.new(Hash[*part.attributes.values.map { |a| [a.name, a.value] }.flatten])
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def set(tag)
|
|
89
|
+
@language, @region = tag.to_s.split(/-/)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def tag
|
|
93
|
+
[@language, @region].compact.join('-')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def terms
|
|
97
|
+
@terms ||= Term.build
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
alias :term :terms
|
|
101
|
+
|
|
102
|
+
def has_term?(term)
|
|
103
|
+
terms.has_key?(term.to_s)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def [](term)
|
|
107
|
+
terms[term.to_s]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @example
|
|
111
|
+
# #options['punctuation-in-quotes'] => 'true'
|
|
112
|
+
def options
|
|
113
|
+
@options ||= {}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
alias :style_options :options
|
|
117
|
+
|
|
118
|
+
# @example
|
|
119
|
+
# #date['numeric']['month']['suffix'] => '/'
|
|
120
|
+
def date
|
|
121
|
+
@date ||= Hash.new([])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Around Alias Chains to call reload whenver locale changes
|
|
125
|
+
[:language=, :region=, :set].each do |method|
|
|
126
|
+
original = [:original, method].join('_')
|
|
127
|
+
alias_method original, method
|
|
128
|
+
define_method method do |*args|
|
|
129
|
+
self.send(original, *args)
|
|
130
|
+
reload!()
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Reloads the XML file. Called automatically whenever language or region changes.
|
|
135
|
+
def reload!
|
|
136
|
+
@region = Locale.regions[@language] if @region.nil?
|
|
137
|
+
@language = Locale.languages[@region] if @language.nil?
|
|
138
|
+
|
|
139
|
+
parse!(Nokogiri::XML(File.open(document_path)) { |config| config.strict.noblanks }.root)
|
|
140
|
+
rescue Exception => e
|
|
141
|
+
CiteProc.log.error "Failed to open locale file, falling back to default: #{e.message}"
|
|
142
|
+
CiteProc.log.debug e.backtrace[0..10].join("\n")
|
|
143
|
+
|
|
144
|
+
unless tag == Locale.default
|
|
145
|
+
@language, @region = Locale.default.split(/-/)
|
|
146
|
+
retry
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Locales are sorted first by language, then by region; sort order is
|
|
151
|
+
# alphabetical with the following exceptions: en_US (the default locale)
|
|
152
|
+
# is prioritised; in case of a language-match the default region of that
|
|
153
|
+
# language will be prioritised (thus, de_DE will comes before de_AT even
|
|
154
|
+
# though the alphabetical order would be different).
|
|
155
|
+
def <=>(other)
|
|
156
|
+
Locale.sort.call(self, other)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def self.sort(language = nil, region = nil)
|
|
160
|
+
Proc.new do |a,b|
|
|
161
|
+
if a.language != b.language
|
|
162
|
+
case
|
|
163
|
+
when a.language == language.to_s || b.language.nil? then -1
|
|
164
|
+
when b.language == language.to_s || a.language.nil? then 1
|
|
165
|
+
when a.language == 'en' then -1
|
|
166
|
+
when b.language == 'en' then 1
|
|
167
|
+
else
|
|
168
|
+
a.language <=> b.language
|
|
169
|
+
end
|
|
170
|
+
else
|
|
171
|
+
case
|
|
172
|
+
when a.region == b.region then 0
|
|
173
|
+
when a.region == region.to_s || b.region.nil? then -1
|
|
174
|
+
when b.region == region.to_s || a.region.nil? then 1
|
|
175
|
+
when a.region == Locale.regions[a.language] then -1
|
|
176
|
+
when b.region == Locale.regions[a.language] then 1
|
|
177
|
+
else
|
|
178
|
+
a.region <=> b.region
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Returns an ordinalized number according to the rules specified in the
|
|
185
|
+
# given locale. Does not conform to CSL 1.0 in order to work around some
|
|
186
|
+
# shortcomings in the schema: this version tries a useful fallback if
|
|
187
|
+
# there is no direct hit in the locale (e.g., if 21 is not specified, we
|
|
188
|
+
# will try with 21 and then with 1). The fallback of the long-ordinal form
|
|
189
|
+
# is ordinal.
|
|
190
|
+
#
|
|
191
|
+
# TODO: long-ordinals may be influenced by gender in some locales
|
|
192
|
+
#
|
|
193
|
+
# @param number a Fixnum
|
|
194
|
+
# @param options the options hash; should contain a 'form' attribute
|
|
195
|
+
# @returns a string, e.g., '1st'
|
|
196
|
+
#
|
|
197
|
+
def ordinalize(number, options={})
|
|
198
|
+
number = number.to_i
|
|
199
|
+
|
|
200
|
+
options['form'] ||= 'ordinal'
|
|
201
|
+
key = [options['form'], '%02d'].join('-')
|
|
202
|
+
|
|
203
|
+
ordinal = self[key % number].to_s(options)
|
|
204
|
+
mod = 100
|
|
205
|
+
|
|
206
|
+
while ordinal.empty? && mod > 1
|
|
207
|
+
key = 'ordinal-%02d'
|
|
208
|
+
ordinal = self[key % (number % mod)].to_s(options)
|
|
209
|
+
mod = mod / 10
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
key.match(/^ordinal/) ? [number, ordinal].join : ordinal
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
def document_path
|
|
219
|
+
File.expand_path("./locales-#{@language}-#{@region}.xml", Locale.path)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
end
|
|
223
|
+
end
|
data/lib/csl/node.rb
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module CSL
|
|
2
|
+
|
|
3
|
+
def Node(*args, &block)
|
|
4
|
+
Node.parse(*args, &block)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module_function :Node
|
|
8
|
+
|
|
9
|
+
class Node
|
|
10
|
+
include Support::Attributes
|
|
11
|
+
include Support::Tree
|
|
12
|
+
|
|
13
|
+
def self.parse(*args, &block)
|
|
14
|
+
return if args.compact.empty?
|
|
15
|
+
|
|
16
|
+
node = args.detect { |argument| argument.is_a?(Nokogiri::XML::Node) } ||
|
|
17
|
+
raise(ArgumentError, "arguments must contain an XML node; was #{ args.map(&:class).inspect }")
|
|
18
|
+
|
|
19
|
+
name = node.name.split(/[\s-]+/).map(&:capitalize).join
|
|
20
|
+
klass = CSL.const_defined?(name) ? CSL.const_get(name) : self
|
|
21
|
+
|
|
22
|
+
klass.new({ :node => node }, &block)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(arguments = {})
|
|
26
|
+
parse(normalize(arguments[:node])) if arguments.has_key?(:node)
|
|
27
|
+
|
|
28
|
+
merge!(arguments[:attributes])
|
|
29
|
+
|
|
30
|
+
@node_name = arguments[:name].to_s if arguments.has_key?(:name)
|
|
31
|
+
|
|
32
|
+
yield self if block_given?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def name
|
|
36
|
+
node_name || self.class.name.split(/::/).last.gsub(/([[:lower:]])([[:upper:]])/) { [$1, $2].join('-') }.downcase
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
alias :name= :node_name=
|
|
40
|
+
|
|
41
|
+
def style!
|
|
42
|
+
@style = root!.is_a?(Style) ? nil : root
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def style; @style || style!; end
|
|
46
|
+
|
|
47
|
+
def parse(node)
|
|
48
|
+
@node_name = node.name
|
|
49
|
+
|
|
50
|
+
node.attributes.values.each { |a| attributes[a.name] = a.value }
|
|
51
|
+
add_children(node.children.map { |child| Node.parse(child) })
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_xml
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
protected
|
|
58
|
+
|
|
59
|
+
def normalize(node)
|
|
60
|
+
case node
|
|
61
|
+
when Nokogiri::XML::Node
|
|
62
|
+
node
|
|
63
|
+
when String
|
|
64
|
+
# TODO file path (e.g. locale or style)
|
|
65
|
+
Nokogiri::XML.parse(node) { |config| config.strict.noblanks }.root
|
|
66
|
+
else
|
|
67
|
+
raise(ArgumentError, "failed to parse #{ node.inspect }")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/csl/nodes.rb
ADDED
|
@@ -0,0 +1,1364 @@
|
|
|
1
|
+
module CSL
|
|
2
|
+
|
|
3
|
+
class Nodes
|
|
4
|
+
|
|
5
|
+
@formatting_attributes = %w{ text-case font-style font-variant font-weight
|
|
6
|
+
text-decoration vertical-align prefix suffix display strip-periods }
|
|
7
|
+
|
|
8
|
+
@inheritable_name_attributes = %w{ and delimiter-precedes-last et-al-min
|
|
9
|
+
et-al-use-first et-al-subsequent-min et-al-subsequent-use-first
|
|
10
|
+
initialize-with name-as-sort-order sort-separator }
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
attr_reader :formatting_attributes, :inheritable_name_attributes
|
|
14
|
+
|
|
15
|
+
# Parses the given node an returns a new instance of Node or a suitable
|
|
16
|
+
# subclass corresponding to the node's name.
|
|
17
|
+
def parse(*args, &block)
|
|
18
|
+
node = args.detect { |argument| argument.is_a?(Nokogiri::XML::Node) }
|
|
19
|
+
raise(ArgumentError, "arguments must contain an XML node; was #{ args.map(&:class).inspect }") if node.nil?
|
|
20
|
+
|
|
21
|
+
name = node.name.split(/[\s-]+/).map(&:capitalize).join
|
|
22
|
+
klass = Nodes.const_defined?(name) ? Nodes.const_get(name) : Node
|
|
23
|
+
|
|
24
|
+
klass.new(*args, &block)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# == Node
|
|
30
|
+
#
|
|
31
|
+
# A Node represents a CSL rendering element.
|
|
32
|
+
# Rendering elements are used to specify which, and in what order,
|
|
33
|
+
# bibliographic data should be included in citations and bibliographies.
|
|
34
|
+
# Rendering elements also partly control the formatting of this data.
|
|
35
|
+
#
|
|
36
|
+
# Each Node is bound to a node in a CSL Style document. Furthermore,
|
|
37
|
+
# before any processiong can be done, the Node must be linked with a
|
|
38
|
+
# processor, in order to be able to access items, format, or locale
|
|
39
|
+
# information.
|
|
40
|
+
#
|
|
41
|
+
class Node
|
|
42
|
+
include Support::Attributes
|
|
43
|
+
include Support::Tree
|
|
44
|
+
|
|
45
|
+
attr_reader :style
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
# Chains the format method to the given methods
|
|
49
|
+
def format_on(*args)
|
|
50
|
+
args.flatten.each do |method_id|
|
|
51
|
+
|
|
52
|
+
# Set up Around Alias Chain
|
|
53
|
+
original_method = [method_id, 'without_formatting'].join('_')
|
|
54
|
+
alias_method original_method, method_id
|
|
55
|
+
|
|
56
|
+
define_method method_id do |*args, &block|
|
|
57
|
+
begin
|
|
58
|
+
string = send(original_method, *args, &block)
|
|
59
|
+
processor = args.detect { |argument| argument.is_a?(CiteProc::Processor) }
|
|
60
|
+
|
|
61
|
+
processor.nil? ? string : processor.format(string, attributes)
|
|
62
|
+
rescue Exception => e
|
|
63
|
+
CiteProc.log :error, "failed to format string #{ string.inspect }", e
|
|
64
|
+
args[0]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def initialize(*args)
|
|
72
|
+
@style = args.detect { |argument| argument.is_a?(Style) }
|
|
73
|
+
args.delete(@style) unless @style.nil?
|
|
74
|
+
|
|
75
|
+
args.each do |argument|
|
|
76
|
+
case
|
|
77
|
+
when argument.is_a?(Node)
|
|
78
|
+
@parent = argument
|
|
79
|
+
@style = @parent.style || @style
|
|
80
|
+
|
|
81
|
+
when argument.is_a?(String) && argument.match(/^\s*</)
|
|
82
|
+
parse(Nokogiri::XML.parse(argument) { |config| config.strict.noblanks }.root)
|
|
83
|
+
|
|
84
|
+
when argument.is_a?(Nokogiri::XML::Node)
|
|
85
|
+
parse(argument)
|
|
86
|
+
|
|
87
|
+
when argument.is_a?(Hash)
|
|
88
|
+
merge!(argument)
|
|
89
|
+
|
|
90
|
+
else
|
|
91
|
+
CiteProc.log.warn "cannot initialize Node from argument #{ argument.inspect }" unless argument.nil?
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
set_defaults
|
|
96
|
+
|
|
97
|
+
yield self if block_given?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Parses the given XML node.
|
|
101
|
+
def parse(node)
|
|
102
|
+
return if node.nil?
|
|
103
|
+
|
|
104
|
+
node.attributes.values.each { |a| attributes[a.name] = a.value }
|
|
105
|
+
|
|
106
|
+
@children = node.children.map do |child|
|
|
107
|
+
Nodes.parse(self, child)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
inherit_attributes(node)
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @returns a new Node with the attributes and style of self and other
|
|
115
|
+
# merged.
|
|
116
|
+
def merge(other)
|
|
117
|
+
return self.copy if other.nil?
|
|
118
|
+
self.class.new(attributes.merge(other.attributes), other.style || style)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @returns a new Node that contains the same attributes and style as self.
|
|
122
|
+
def copy; self.class.new(attributes, style); end
|
|
123
|
+
|
|
124
|
+
# @returns a new Node with the attributes of self and other merged;
|
|
125
|
+
# attributes in other take precedence.
|
|
126
|
+
def reverse_merge(other)
|
|
127
|
+
other.merge(self)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @returns the localized term with the given key.
|
|
131
|
+
def localized_terms(key, processor=nil)
|
|
132
|
+
localize(:term, key, processor) do |hash|
|
|
133
|
+
return hash[key] if hash.has_key?(key) && !hash[key].empty?
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @returns the localized date parts.
|
|
138
|
+
def localized_date_parts(key, processor=nil)
|
|
139
|
+
localize(:date, key, processor) do |hash|
|
|
140
|
+
return hash[key] if hash.has_key?(key) && !hash[key].empty?
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def localized_options(key, processor=nil)
|
|
145
|
+
localize(:options, key, processor) do |hash|
|
|
146
|
+
return hash[key] if hash.has_key?(key)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Processes the supplied data. @returns a formatted string.
|
|
151
|
+
def process(data, processor)
|
|
152
|
+
''
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def to_s
|
|
156
|
+
attributes.merge('node' => self.class.name).inspect
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
protected
|
|
160
|
+
|
|
161
|
+
# Empty method; nodes may override this method.
|
|
162
|
+
def set_defaults
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# TODO Refactor: move all processor-dependencies to citeproc
|
|
166
|
+
|
|
167
|
+
# Prioritized locale look-up.
|
|
168
|
+
def localize(type, key, processor, &block)
|
|
169
|
+
unless @style.nil?
|
|
170
|
+
style.locales(processor && processor.language || nil).each do |locale|
|
|
171
|
+
yield locale.send(type)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
unless processor.nil?
|
|
176
|
+
yield processor.locale.send(type)
|
|
177
|
+
yield CSL::Locale.new(processor.language)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
CSL.default_locale.send(type)[key]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# @returns the locale with the highest priority
|
|
184
|
+
def locale(processor = nil)
|
|
185
|
+
locales(processor).first
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def locales(processor = nil)
|
|
189
|
+
if processor.nil?
|
|
190
|
+
style && style.locales || []
|
|
191
|
+
else
|
|
192
|
+
(style && style.locales(processor.language, processor.region) || []) + [Locale.new(processor.language)]
|
|
193
|
+
end + [Locale.default]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# Empty method; nodes may override this method.
|
|
198
|
+
def inherit_attributes(node)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Attributes for the cs:names and cs:name elements may also be set on
|
|
202
|
+
# cs:style, cs:citation and cs:bibliography. This eliminates the need to
|
|
203
|
+
# repeat the same attributes and attribute values for every occurrence
|
|
204
|
+
# of the cs:names and cs:name elements.
|
|
205
|
+
#
|
|
206
|
+
# The available inheritable attributes for cs:name are and,
|
|
207
|
+
# delimiter-precedes-last, et-al-min, et-al-use-first,
|
|
208
|
+
# et-al-subsequent-min, et-al-subsequent-use-first, initialize-with,
|
|
209
|
+
# name-as-sort-order and sort-separator. The attributes name-form and
|
|
210
|
+
# name-delimiter accompany the form and delimiter attributes on cs:name.
|
|
211
|
+
# Similarly, names-delimiter, the only inheritable attribute available
|
|
212
|
+
# for cs:names, accompanies the delimiter attribute on cs:names.
|
|
213
|
+
#
|
|
214
|
+
# When an inheritable name attribute is set on cs:style, cs:citation or
|
|
215
|
+
# cs:bibliography, its value is used for all cs:names elements within
|
|
216
|
+
# the element carrying the attribute. When an element lower in the
|
|
217
|
+
# hierarchy includes the same attribute with a different value, this
|
|
218
|
+
# latter value will override the value(s) specified higher in the
|
|
219
|
+
# hierarchy.
|
|
220
|
+
#
|
|
221
|
+
# This mehtod traverses a nodes ancestor chain and inherits all
|
|
222
|
+
# specified attributes from each parent that matches a name in names.
|
|
223
|
+
#
|
|
224
|
+
def inherit_attributes_from(node, nodes=[], attributes=[], prefix='')
|
|
225
|
+
return unless node
|
|
226
|
+
|
|
227
|
+
# TODO refactor so that node is not required anymore
|
|
228
|
+
parent = node.parent
|
|
229
|
+
until parent.name == 'document' do
|
|
230
|
+
attributes.each { |attribute| self[attribute] ||= parent[[prefix, attribute].join] } if nodes.include?(parent.name)
|
|
231
|
+
parent = parent.parent
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def handle_processing_error(e, data, processor)
|
|
236
|
+
CiteProc.log :error, "failed to process item #{ data.inspect }", e
|
|
237
|
+
''
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# All the rendering elements that should appear in the citations and
|
|
243
|
+
# bibliography should be nested inside the cs:layout element. Itself a
|
|
244
|
+
# rendering element, cs:layout accepts both affixes and formatting
|
|
245
|
+
# attributes. When used in the cs:citation element, a delimiter can be set
|
|
246
|
+
# to separate multiple bibliographic items in a single citation.
|
|
247
|
+
class Layout < Node
|
|
248
|
+
attr_fields Nodes.formatting_attributes
|
|
249
|
+
attr_fields %w{ delimiter }
|
|
250
|
+
|
|
251
|
+
def process(data, processor)
|
|
252
|
+
children.map { |child| child.process(data, processor) }.join
|
|
253
|
+
rescue Exception => e
|
|
254
|
+
handle_processing_error(e, data, processor)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
format_on :process
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
class Macro < Layout
|
|
261
|
+
attr_fields :name
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The cs:text element is used to output text, which can originate from
|
|
265
|
+
# different sources. The source-type is indicated with an attribute, and
|
|
266
|
+
# the attribute value acts as an identifier within the source-type. For
|
|
267
|
+
# example,
|
|
268
|
+
#
|
|
269
|
+
# <text variable="title" form="short" font-style="italic"/>
|
|
270
|
+
#
|
|
271
|
+
# indicates that the source-type is a variable, and that the variable that
|
|
272
|
+
# should be displayed is the italicized short form of "title". The
|
|
273
|
+
# different source-types are:
|
|
274
|
+
#
|
|
275
|
+
# * variable - the text contents of a variable (see Standard Variables). The
|
|
276
|
+
# optional form attribute can be set to either "long" (the default) or
|
|
277
|
+
# "short" to select the long or short forms of variables, e.g. the full
|
|
278
|
+
# and short title.
|
|
279
|
+
# * macro - the text generated by a macro. The value of macro should
|
|
280
|
+
# correspond to the value of the name attribute of the desired cs:macro
|
|
281
|
+
# element.
|
|
282
|
+
# * term - the text of a localized term (see Appendix III - Terms and
|
|
283
|
+
# Locale). The plural attribute can be set to choose either the singular
|
|
284
|
+
# (value "false", the default) or plural variant (value "true") of a term.
|
|
285
|
+
# In addition, the form attribute can be set to select the desired term
|
|
286
|
+
# form ("long" [default], "short", "verb", "verb-short" or "symbol"). If
|
|
287
|
+
# for a given term the desired form does not exist, another form may be
|
|
288
|
+
# used: "verb-short" reverts to "verb", "symbol" reverts to "short", and
|
|
289
|
+
# "verb" and "short" both revert to "long".
|
|
290
|
+
# * value - used to output verbatim text, which is set via the value of
|
|
291
|
+
# value (e.g. value="some text")
|
|
292
|
+
#
|
|
293
|
+
# In all cases the attributes for affixes, display, formatting, quotes,
|
|
294
|
+
# strip-periods and text-case may be applied to cs:text.
|
|
295
|
+
class Text < Node
|
|
296
|
+
attr_fields Nodes.formatting_attributes
|
|
297
|
+
attr_fields %w{ variable form macro term plural value quotes }
|
|
298
|
+
|
|
299
|
+
def process(data, processor)
|
|
300
|
+
case
|
|
301
|
+
when has_value?
|
|
302
|
+
text = value
|
|
303
|
+
|
|
304
|
+
when has_macro?
|
|
305
|
+
text = @style.macros[macro].process(data, processor)
|
|
306
|
+
|
|
307
|
+
when has_term?
|
|
308
|
+
text = localized_terms(term).to_s(attributes)
|
|
309
|
+
|
|
310
|
+
when has_variable?
|
|
311
|
+
text = (data["short#{variable.capitalize}"] || data[['short', variable].join('-')] || data[variable]).to_s
|
|
312
|
+
|
|
313
|
+
if form == 'short'
|
|
314
|
+
text = processor.abbreviate(variable, text)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
if variable == 'page' && @style.options.has_key?('page-range-format')
|
|
318
|
+
text = format_page_range(text, @style.options['page-range-format'])
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
if variable == 'page-first' && text.empty?
|
|
322
|
+
text = data['page'].to_s.scan(/\d+/).first.to_s
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
else
|
|
326
|
+
text = ''
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Add localized quotes
|
|
330
|
+
if has_quotes? && !text.empty?
|
|
331
|
+
prefix = [self['prefix'], localized_terms('open-quote')].compact.join
|
|
332
|
+
|
|
333
|
+
if localized_options('punctuation-in-quote', processor) == 'true'
|
|
334
|
+
suffix = self['suffix'].to_s.sub(/^([\.,!?;:]+)/, '')
|
|
335
|
+
suffix = [$1, localized_terms('close-quote'), suffix].compact.join
|
|
336
|
+
else
|
|
337
|
+
text = text.sub(/([\.,!?;:]+)$/, '')
|
|
338
|
+
suffix = [localized_terms('close-quote'), $1, self['suffix']].compact.join
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
self['prefix'] = prefix
|
|
342
|
+
self['suffix'] = suffix
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
text
|
|
346
|
+
rescue Exception => e
|
|
347
|
+
handle_processing_error(e, data, processor)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
format_on :process
|
|
351
|
+
|
|
352
|
+
protected
|
|
353
|
+
|
|
354
|
+
# The page abbreviation rules for the different values of the
|
|
355
|
+
# page-range-format attribute on cs:style are:
|
|
356
|
+
#
|
|
357
|
+
# * "minimum": All digits repeated in the second number are left out:
|
|
358
|
+
# 42-5, 321-8, 2787-816
|
|
359
|
+
# * "expanded": Abbreviated page ranges are expanded to their
|
|
360
|
+
# non-abbreviated form: 42-45, 321-328, 2787-2816
|
|
361
|
+
# * "chicago": Page ranges are abbreviated according to the link Chicago
|
|
362
|
+
# Manual of Style-rules
|
|
363
|
+
#
|
|
364
|
+
def format_page_range(value, format)
|
|
365
|
+
return value unless value.match(/([a-z]*)(\d+)\s*\D\s*([a-z]*)(\d+)/i)
|
|
366
|
+
|
|
367
|
+
tokens = [$1, $2, "\u2013", $3, $4]
|
|
368
|
+
|
|
369
|
+
# normalize page range to expanded form
|
|
370
|
+
f, t = tokens[1].chars.to_a, tokens[4].chars.to_a
|
|
371
|
+
d = t.length - f.length
|
|
372
|
+
d > 0 ? d.times { f.unshift('0') } : t = f.take(d.abs) + t
|
|
373
|
+
|
|
374
|
+
# TODO handle prefixes correctly
|
|
375
|
+
# TODO handle multiple ranges
|
|
376
|
+
|
|
377
|
+
case format
|
|
378
|
+
when /mini/
|
|
379
|
+
tokens[4] = f.zip(t).map { |f, t| f == t ? nil : t }.reject(&:nil?)
|
|
380
|
+
tokens[3] = nil if tokens[3] == tokens[0]
|
|
381
|
+
when 'expanded'
|
|
382
|
+
tokens[4] = t
|
|
383
|
+
tokens[3] = tokens[0] if tokens[3].nil? || tokens[3].empty?
|
|
384
|
+
when 'chicago'
|
|
385
|
+
case
|
|
386
|
+
when f.length < 3 || f.join.to_i % 100 == 0
|
|
387
|
+
# use all digits
|
|
388
|
+
tokens[4] = t
|
|
389
|
+
tokens[3] = tokens[0] if tokens[3].nil? || tokens[3].empty?
|
|
390
|
+
when f.join.to_i % 100 < 10
|
|
391
|
+
# use changed part only
|
|
392
|
+
tokens[4] = f.zip(t).map { |f, t| f == t ? nil : t }.reject(&:nil?)
|
|
393
|
+
tokens[3] = nil if tokens[3] == tokens[0]
|
|
394
|
+
when f.length == 4
|
|
395
|
+
# use at least two digits, and all if three or more change
|
|
396
|
+
match = f[0..-3].zip(t[0..-3]).map { |f, t| f == t ? nil : t }.reject(&:nil?) + t[-2..-1]
|
|
397
|
+
tokens[4] = match.length > 2 ? t : match
|
|
398
|
+
tokens[3] = tokens[0] if tokens[3].nil? || tokens[3].empty?
|
|
399
|
+
else
|
|
400
|
+
# use at least two digits in second number
|
|
401
|
+
tokens[4] = f[0..-3].zip(t[0..-3]).map { |f, t| f == t ? nil : t }.reject(&:nil?) + t[-2..-1]
|
|
402
|
+
tokens[3] = nil if tokens[3] == tokens[0]
|
|
403
|
+
end
|
|
404
|
+
else
|
|
405
|
+
value
|
|
406
|
+
end
|
|
407
|
+
tokens.join
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
#
|
|
414
|
+
# The Date element is used to output dates, in either a localized or a
|
|
415
|
+
# non-localized format. The desired date variable (@see CiteProc::Date) is
|
|
416
|
+
# selected with the variable attribute.
|
|
417
|
+
#
|
|
418
|
+
# Localized date formats are selected with the form attribute. This
|
|
419
|
+
# attribute can be set to "numeric" (for numeric date formats, e.g.
|
|
420
|
+
# "12-15-2005"), or to "text" (for date formats with a non-numeric month,
|
|
421
|
+
# e.g. "December 15, 2005"). Localized dates can be customized in two
|
|
422
|
+
# ways. First, the date-parts attribute may be used to specify which
|
|
423
|
+
# cs:date-part elements are shown. The possible values are:
|
|
424
|
+
#
|
|
425
|
+
# * "year-month-day" - default, displays year, month and day
|
|
426
|
+
# * "year-month" - displays year and month
|
|
427
|
+
# * "year" - displays year only
|
|
428
|
+
#
|
|
429
|
+
# Secondly, cs:date may include one or more cs:date-part elements (see
|
|
430
|
+
# Date-part). The attributes set on these elements override those
|
|
431
|
+
# originally specified for the localized date formats (e.g. the form
|
|
432
|
+
# attribute of the month-cs:date-part element can be set to "short" to get
|
|
433
|
+
# abbreviated month names in all locales.). Note that the use of
|
|
434
|
+
# cs:date-part elements for localized dates does not affect which, and in
|
|
435
|
+
# what order, the cs:date-part elements are included in the rendered date.
|
|
436
|
+
# Also, the cs:date-part elements may not carry the attributes for
|
|
437
|
+
# affixes, as these are considered to be locale-specific.
|
|
438
|
+
#
|
|
439
|
+
# Non-localized date formats are self-contained: the date format is
|
|
440
|
+
# entirely controlled by cs:date and its cs:date-part children. In
|
|
441
|
+
# contrast to localized dates, cs:date is used without the form and
|
|
442
|
+
# date-parts attributes. Only the included cs:date-part elements will be
|
|
443
|
+
# rendered, in the order in which they are specified. The cs:date-part
|
|
444
|
+
# elements may carry attributes for both affixes and formatting, while
|
|
445
|
+
# cs:date may carry a delimiter (delimiting the various cs:date-part
|
|
446
|
+
# elements).
|
|
447
|
+
#
|
|
448
|
+
# For both localized and non-localized dates, affixes, display and
|
|
449
|
+
# formatting attributes may be specified for the cs:date element.
|
|
450
|
+
#
|
|
451
|
+
class Date < Node
|
|
452
|
+
attr_fields Nodes.formatting_attributes
|
|
453
|
+
attr_fields %w{ variable form date-parts delimiter }
|
|
454
|
+
|
|
455
|
+
def process(data, processor)
|
|
456
|
+
date = data[variable]
|
|
457
|
+
|
|
458
|
+
case
|
|
459
|
+
when date.nil?
|
|
460
|
+
''
|
|
461
|
+
when date.literal?
|
|
462
|
+
date.literal
|
|
463
|
+
when date.range?
|
|
464
|
+
process_range(date, processor)
|
|
465
|
+
else
|
|
466
|
+
parts(processor).map { |part| part.process(date, processor) }.join(delimiter)
|
|
467
|
+
end
|
|
468
|
+
rescue Exception => e
|
|
469
|
+
handle_processing_error(e, data, processor)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
format_on :process
|
|
473
|
+
|
|
474
|
+
# By default, date ranges are delimited by an en-dash (e.g. "May-July
|
|
475
|
+
# 2008"). The range-delimiter attribute can be used to specify custom
|
|
476
|
+
# date range delimiters. The attribute value set on the largest
|
|
477
|
+
# date-part ("day", "month" or "year") that differs between the two
|
|
478
|
+
# dates of the date range will then be used instead of the en-dash. For
|
|
479
|
+
# example,
|
|
480
|
+
#
|
|
481
|
+
# <style>
|
|
482
|
+
# <citation>
|
|
483
|
+
# <layout>
|
|
484
|
+
# <date variable="issued">
|
|
485
|
+
# <date-part name="month" suffix=" "/>
|
|
486
|
+
# <date-part name="year" range-delimiter="/"/>
|
|
487
|
+
# </date>
|
|
488
|
+
# </layout>
|
|
489
|
+
# </citation>
|
|
490
|
+
# </style>
|
|
491
|
+
#
|
|
492
|
+
# would result in "May-July 2008" and "May 2008/June 2009".
|
|
493
|
+
#
|
|
494
|
+
def process_range(date, processor)
|
|
495
|
+
order = parts(processor)
|
|
496
|
+
|
|
497
|
+
parts = [order, order].zip(date.display_parts).map do |order, parts|
|
|
498
|
+
order.map { |part| parts.include?(part['name']) ? part : nil }.compact
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
result = parts.zip([date, date.to]).map { |parts, date| parts.map { |part| part.process(date, processor) }.join(delimiter) }.compact
|
|
502
|
+
result[0].gsub!(/\s+$/, '')
|
|
503
|
+
result.join(parts[0].last.range_delimiter)
|
|
504
|
+
|
|
505
|
+
# case
|
|
506
|
+
# when date.open_range?
|
|
507
|
+
# result << parts.map { |part| part.process(date, processor) }.join(delimiter)
|
|
508
|
+
# result << parts.last.range_delimiter
|
|
509
|
+
#
|
|
510
|
+
# when discriminator == 'month'
|
|
511
|
+
# month_parts = parts.reject { |part| part['name'] == 'year' }
|
|
512
|
+
#
|
|
513
|
+
# result << month_parts.map { |part| part.process(date, processor) }.join(delimiter)
|
|
514
|
+
# result << month_parts.last.range_delimiter
|
|
515
|
+
# result << parts.map { |part| part.process(date.to, processor) }.join(delimiter)
|
|
516
|
+
#
|
|
517
|
+
# when discriminator == 'day'
|
|
518
|
+
# day_parts = parts.select { |part| part['name'] == 'day' }
|
|
519
|
+
#
|
|
520
|
+
# result << day_parts.map { |part| part.process(date, processor) }.join(delimiter)
|
|
521
|
+
# result << day_parts.last.range_delimiter
|
|
522
|
+
# result << parts.map { |part| part.process(date.to, processor) }.join(delimiter)
|
|
523
|
+
#
|
|
524
|
+
# else # year
|
|
525
|
+
# year_parts = parts.select { |part| part['name'] == 'year' }
|
|
526
|
+
#
|
|
527
|
+
# result << parts.map { |part| part.process(date, processor) }.join(delimiter)
|
|
528
|
+
# result << year_parts.last.range_delimiter
|
|
529
|
+
# result << year_parts.map { |part| part.process(date.to, processor) }.join(delimiter)
|
|
530
|
+
#
|
|
531
|
+
# end
|
|
532
|
+
#
|
|
533
|
+
# result.join
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def parts(processor)
|
|
537
|
+
has_form? ? merge_parts(localized_date_parts(form, processor), children) : children
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# Combines two lists of date-part elements; includes only the parts set
|
|
542
|
+
# in the 'date-parts' attribute and retains the order of elements in the
|
|
543
|
+
# first list.
|
|
544
|
+
def merge_parts(p1, p2)
|
|
545
|
+
merged = p1.map do |part|
|
|
546
|
+
DatePart.new(part.attributes, style).merge(p2.detect { |p| p['name'] == part['name'] })
|
|
547
|
+
end
|
|
548
|
+
merged.reject { |part| !date_parts.match(Regexp.new(part['name'])) }
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def date_parts
|
|
552
|
+
self['date-parts'] || 'year-month-day'
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
class DatePart < Node
|
|
557
|
+
attr_fields Nodes.formatting_attributes
|
|
558
|
+
attr_fields %w{ name form range-delimiter strip-periods }
|
|
559
|
+
|
|
560
|
+
def process(date, processor)
|
|
561
|
+
send(['process', self['name']].join('_'), date, processor)
|
|
562
|
+
rescue Exception => e
|
|
563
|
+
handle_processing_error(e, data, processor)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
format_on :process
|
|
567
|
+
|
|
568
|
+
def process_year(date, processor)
|
|
569
|
+
return '' if date.year.nil?
|
|
570
|
+
|
|
571
|
+
year = date.year.abs.to_s
|
|
572
|
+
year = year[-2..-1] if form == 'short'
|
|
573
|
+
year = [year, localized_terms('ad')].join if date.ad?
|
|
574
|
+
year = [year, localized_terms('bc')].join if date.bc?
|
|
575
|
+
year
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def process_month(date, processor)
|
|
579
|
+
return process_season(date, processor) if date.has_season?
|
|
580
|
+
return '' if date.month.nil?
|
|
581
|
+
|
|
582
|
+
case
|
|
583
|
+
when form == 'numeric'
|
|
584
|
+
date.month.to_s
|
|
585
|
+
when form == 'numeric-leading-zeros'
|
|
586
|
+
"%02d" % date.month
|
|
587
|
+
else
|
|
588
|
+
localized_terms("month-%02d" % date.month).to_s(attributes)
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def process_season(date, processor)
|
|
593
|
+
season = date.season.to_s
|
|
594
|
+
season = date.month.to_s if season.match(/true|always|yes/i)
|
|
595
|
+
season = localized_terms('season-%02d' % season.to_i).to_s if season.match(/0?[1-4]/)
|
|
596
|
+
season
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def process_day(date, processor)
|
|
600
|
+
return '' if date.day.nil?
|
|
601
|
+
|
|
602
|
+
case
|
|
603
|
+
when form == 'ordinal'
|
|
604
|
+
locale(processor).ordinalize(date.day)
|
|
605
|
+
when form == 'numeric-leading-zeros'
|
|
606
|
+
"%02d" % date.day
|
|
607
|
+
else # 'numeric'
|
|
608
|
+
date.day.to_s
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
protected
|
|
613
|
+
|
|
614
|
+
def set_defaults
|
|
615
|
+
self['range-delimiter'] ||= "\u2013"
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
# The cs:number element can be used to output any of the following
|
|
621
|
+
# variables (selected with the variable attribute):
|
|
622
|
+
#
|
|
623
|
+
# "edition"
|
|
624
|
+
# "volume"
|
|
625
|
+
# "issue"
|
|
626
|
+
# "number"
|
|
627
|
+
# "number-of-volumes"
|
|
628
|
+
#
|
|
629
|
+
# Although these variables can also be rendered with cs:text, cs:number
|
|
630
|
+
# has the benefit of offering number-specific formatting via the form
|
|
631
|
+
# attribute, with values:
|
|
632
|
+
#
|
|
633
|
+
# * "numeric" (default) - e.g. "1", "2", "3"
|
|
634
|
+
# * "ordinal" - e.g. "1st", "2nd", "3rd"
|
|
635
|
+
# * "long-ordinal" - e.g. "first", "second", "third"
|
|
636
|
+
# * "roman" - e.g. "i", "ii", "iii"
|
|
637
|
+
#
|
|
638
|
+
# If a variable displayed with cs:number contains a mixture of numeric and
|
|
639
|
+
# non-numeric text, only the first number encountered is used for
|
|
640
|
+
# rendering (e.g. "12" when the entire string is "12th edition"). If a
|
|
641
|
+
# variable only contains non-numeric text (e.g. "special edition"), the
|
|
642
|
+
# entire string is rendered, as if cs:text were used instead. Fields can
|
|
643
|
+
# be tested for containing numeric content with the is-numeric
|
|
644
|
+
# conditional, e.g. "12th edition" would test "true" while "third edition"
|
|
645
|
+
# would test "false" (@see Choose).
|
|
646
|
+
#
|
|
647
|
+
# The cs:number element may carry any of the affixes, display, formatting
|
|
648
|
+
# and text-case attributes.
|
|
649
|
+
#
|
|
650
|
+
class Number < Node
|
|
651
|
+
attr_fields Nodes.formatting_attributes
|
|
652
|
+
attr_fields %w{ variable form }
|
|
653
|
+
|
|
654
|
+
def process(data, processor)
|
|
655
|
+
number = data[variable]
|
|
656
|
+
|
|
657
|
+
case
|
|
658
|
+
when number.nil? || number.empty? || !number.numeric?
|
|
659
|
+
number.to_s
|
|
660
|
+
when form == 'roman'
|
|
661
|
+
number.to_i.romanize
|
|
662
|
+
when form == 'ordinal'
|
|
663
|
+
locale(processor).ordinalize(number.to_i, attributes)
|
|
664
|
+
when form == 'long-ordinal'
|
|
665
|
+
locale(processor).ordinalize(number.to_i, attributes)
|
|
666
|
+
else
|
|
667
|
+
number.to_i.to_s
|
|
668
|
+
end
|
|
669
|
+
rescue Exception => e
|
|
670
|
+
handle_processing_error(e, data, processor)
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
format_on :process
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# The cs:name element is a required child element of cs:names, and describes
|
|
677
|
+
# both how individual names are formatted, and how names within a name
|
|
678
|
+
# variable are separated from each other. The attributes that may be used on
|
|
679
|
+
# cs:name are:
|
|
680
|
+
#
|
|
681
|
+
# 'and'
|
|
682
|
+
# This attribute specifies the delimiter between the second to last
|
|
683
|
+
# and the last name of the names in a name variable. The value of the
|
|
684
|
+
# attribute may be either "text", which selects the "and" term, or "symbol",
|
|
685
|
+
# which selects the ampersand (&).
|
|
686
|
+
#
|
|
687
|
+
# 'delimiter'
|
|
688
|
+
# Specifies the text string to separate names of a name variable. The
|
|
689
|
+
# default value is ", " ("J. Doe, S. Smith").
|
|
690
|
+
#
|
|
691
|
+
# 'delimiter-precedes-last'
|
|
692
|
+
# Determines in which cases the delimiter used to delimit names is also used
|
|
693
|
+
# to separate the second to last and the last name in name lists. The
|
|
694
|
+
# possible values are:
|
|
695
|
+
#
|
|
696
|
+
# "contextual" (default): the delimiter is only included for name lists with three or more names
|
|
697
|
+
# 2 names: "J. Doe and T. Williams,"
|
|
698
|
+
# 3 names: "J. Doe, S. Smith, and T. Williams"
|
|
699
|
+
# "always": the delimiter is always included
|
|
700
|
+
# 2 names: "J. Doe, and T. Williams"
|
|
701
|
+
# 3 names: "J. Doe, S. Smith, and T. Williams"
|
|
702
|
+
# "never": the delimiter is never included
|
|
703
|
+
# 2 names: "J. Doe and T. Williams,"
|
|
704
|
+
# 3 names: "J. Doe, S. Smith and T. Williams"
|
|
705
|
+
#
|
|
706
|
+
# 'et-al-min / et-al-use-first'
|
|
707
|
+
# Together, these attributes control et-al abbreviation. When the number of
|
|
708
|
+
# names in a name variable matches or exceeds the number set on et-al-min,
|
|
709
|
+
# the rendered name list is truncated at the number of names set on
|
|
710
|
+
# et-al-use-first. If truncation occurs, the "et-al" term is appended to the
|
|
711
|
+
# names rendered (see also Et-al). With a single name (et-al-use-first="1"),
|
|
712
|
+
# the "et-al" term is preceded by a space (e.g. "Doe et al."). With multiple
|
|
713
|
+
# names, the "et-al" term is preceded by the name delimiter (e.g. "Doe,
|
|
714
|
+
# Smith, et al.").
|
|
715
|
+
#
|
|
716
|
+
# 'et-al-subsequent-min / et-al-subsequent-use-first'
|
|
717
|
+
# The (optional) et-al-min and et-al-use-first attributes take effect for
|
|
718
|
+
# all cites and bibliographic entries. With the et-al-subsequent-min and
|
|
719
|
+
# et-al-subsequent-use-first attributes divergent et-al abbreviation rules
|
|
720
|
+
# can be specified for subsequent cites (cites referencing earlier cited
|
|
721
|
+
# items).
|
|
722
|
+
#
|
|
723
|
+
# The remaining attributes, discussed below, only affect personal names.
|
|
724
|
+
# Personal names require a "family" name-part, and may also contain "given",
|
|
725
|
+
# "suffix", "non-dropping-particle" and "dropping-particle" name-parts. The
|
|
726
|
+
# roles of these name-parts, which are delimited by single spaces in
|
|
727
|
+
# rendered names, are:
|
|
728
|
+
#
|
|
729
|
+
# * "family": the surname minus any particles and suffixes
|
|
730
|
+
# * "given": the given names, which may be either full ("John Edward") or
|
|
731
|
+
# initialized ("J. E.")
|
|
732
|
+
# * "suffix": name suffix, e.g. "Jr." in "John Smith Jr." and "III" in "Bill
|
|
733
|
+
# Gates III"
|
|
734
|
+
# * "non-dropping-particle": name particles that are not dropped when only the
|
|
735
|
+
# last name is shown ("de" in the Dutch surname "de Koning") but which may
|
|
736
|
+
# be treated as a separate object from the family name (e.g. for sorting)
|
|
737
|
+
# * "dropping-particle": name particles that are dropped when only the
|
|
738
|
+
# surname is shown ("van" in "Ludwig van Beethoven", which becomes
|
|
739
|
+
# "Beethoven")
|
|
740
|
+
#
|
|
741
|
+
# 'form'
|
|
742
|
+
# Specifies whether all the name-parts of personal names should be displayed
|
|
743
|
+
# (value "long"), or only the family name and the non-dropping-particle
|
|
744
|
+
# (value "short"). A third value, "count", returns the total number of names
|
|
745
|
+
# that would be otherwise displayed by the use of the cs:names element
|
|
746
|
+
# (taking into account the effects of et-al abbreviation and
|
|
747
|
+
# editor/translator collapsing), and may be used for advanced sorting.
|
|
748
|
+
#
|
|
749
|
+
# 'initialize-with'
|
|
750
|
+
# If this attribute is set, given names are converted to initials. The
|
|
751
|
+
# attribute value specifies the suffix that is included after each initial
|
|
752
|
+
# ("." results in "J.J. Doe"). Note that the global initialize-with-hyphen
|
|
753
|
+
# option controls how compound given names (e.g. "Jean-Luc") are hyphenated
|
|
754
|
+
# when initialized (see Hyphenation of Initialized Names).
|
|
755
|
+
#
|
|
756
|
+
# 'name-as-sort-order'
|
|
757
|
+
# Specifies that names should be displayed with the given name following the
|
|
758
|
+
# family name (e.g. "John Doe" becomes "Doe, John"). The attribute may have
|
|
759
|
+
# one of the two values:
|
|
760
|
+
#
|
|
761
|
+
# "first": name-as-sort-order applies to the first name in each name variable
|
|
762
|
+
# "all": name-as-sort-order applies to all names
|
|
763
|
+
#
|
|
764
|
+
# Note that the sort order of names may differ from the display order for
|
|
765
|
+
# names containing particles and suffixes (see Name-part order). Also, this
|
|
766
|
+
# attribute only affects names written in the latin or Cyrillic alphabet.
|
|
767
|
+
# Names written in other alphabets (e.g. Asian scripts) are always shown
|
|
768
|
+
# with the family name preceding the given name.
|
|
769
|
+
#
|
|
770
|
+
# 'sort-separator'
|
|
771
|
+
# Sets the delimiter for name-parts that have switched positions as a result
|
|
772
|
+
# of name-as-sort-order. The default value is ", " ("Doe, John"). As is the
|
|
773
|
+
# case for name-as-sort-order, this attribute only affects names written in
|
|
774
|
+
# the latin or Cyrillic alphabet.
|
|
775
|
+
#
|
|
776
|
+
# The cs:name element may also carry any of the attributes for affixes and formatting.
|
|
777
|
+
#
|
|
778
|
+
class Name < Node
|
|
779
|
+
attr_fields Nodes.formatting_attributes
|
|
780
|
+
attr_fields Nodes.inheritable_name_attributes
|
|
781
|
+
attr_fields %w{ form delimiter delimiter-precedes-et-al }
|
|
782
|
+
|
|
783
|
+
def parts
|
|
784
|
+
@parts ||= Hash.new { |h, k| k.match(/(non-)?dropping-particle/) ? h['family'] : {} }
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def process_names(role, names, processor)
|
|
788
|
+
|
|
789
|
+
# set display options
|
|
790
|
+
names = names.each { |name| name.merge_options(attributes) }
|
|
791
|
+
names.first.options['name-as-sort-order'] = 'true' if name_as_sort_order == 'first'
|
|
792
|
+
|
|
793
|
+
# name-part formatting
|
|
794
|
+
names.map! do |name|
|
|
795
|
+
name.normalize(name.display_order.map do |token|
|
|
796
|
+
processor.format(name.send(token.tr('-', '_')), parts[token])
|
|
797
|
+
end.compact.join(name.delimiter))
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# join names
|
|
801
|
+
if names.length > 2
|
|
802
|
+
names = [names[0..-2].join(delimiter), names.last]
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
names.join(ampersand(processor))
|
|
806
|
+
rescue Exception => e
|
|
807
|
+
CiteProc.log :error, "failed to process names #{ names.inspect }", e
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
format_on :process_names
|
|
811
|
+
|
|
812
|
+
def truncate(names)
|
|
813
|
+
# TODO subsequent
|
|
814
|
+
et_al_min? && et_al_min.to_i <= names.length ? names[0, et_al_use_first.to_i] : names
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
protected
|
|
818
|
+
|
|
819
|
+
def set_defaults
|
|
820
|
+
attributes['delimiter'] ||= ', '
|
|
821
|
+
attributes['delimiter-precedes-last'] ||= 'false'
|
|
822
|
+
attributes['et-al-use-first'] ||= '1'
|
|
823
|
+
|
|
824
|
+
children.each { |child| parts[child['name']] = child.attributes }
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
# @returns the delimiter to be used between the penultimate and last
|
|
828
|
+
# name in the list.
|
|
829
|
+
def ampersand(processor)
|
|
830
|
+
if self.and?
|
|
831
|
+
ampersand = self.and == 'symbol' ? '&' : localized_terms(self.and == 'text' ? 'and' : self.and).to_s(attributes)
|
|
832
|
+
delimiter_precedes_last? ? [delimiter, ampersand, ' '].join : ampersand.center(ampersand.length + 2)
|
|
833
|
+
else
|
|
834
|
+
delimiter
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
def inherit_attributes(node)
|
|
839
|
+
inherit_attributes_from(node, ['citation', 'bibliography', 'style'], Nodes.inheritable_name_attributes)
|
|
840
|
+
inherit_attributes_from(node, ['citation', 'bibliography', 'style'], ['et-al-use-first', 'delimiter-precedes-et-al'])
|
|
841
|
+
inherit_attributes_from(node, ['citation', 'bibliography', 'style'], ['form', 'delimiter'], 'name-')
|
|
842
|
+
inherit_attributes_from(node, ['style'], ['demote-non-dropping-particle', 'initialize-with-hyphen'])
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
# The cs:name element may include one or two cs:name-part child elements.
|
|
848
|
+
# These child elements accept the formatting and text-case attributes, which
|
|
849
|
+
# allows for separate formatting of the different name parts (e.g. "Jane
|
|
850
|
+
# DOE", see example below). The required name attribute on cs:name-part
|
|
851
|
+
# specifies which name-parts are affected: when set to "given", the
|
|
852
|
+
# formatting only acts on the "given" name-part. When set to "family", the
|
|
853
|
+
# formatting acts on the "family", "dropping-particle" and
|
|
854
|
+
# "non-dropping-particle" name-parts (the "suffix" name-part is not subject
|
|
855
|
+
# to any name-part formatting). The order of the cs:name-part elements does
|
|
856
|
+
# not affect which, and in what order, the name-parts are rendered.
|
|
857
|
+
#
|
|
858
|
+
# <names variable="author">
|
|
859
|
+
# <name>
|
|
860
|
+
# <name-part name="family" text-case="uppercase">
|
|
861
|
+
# </name>
|
|
862
|
+
# </names>
|
|
863
|
+
#
|
|
864
|
+
class NamePart < Node
|
|
865
|
+
attr_fields Nodes.formatting_attributes
|
|
866
|
+
attr_fields %w{ name }
|
|
867
|
+
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
# Et-al abbreviation, controlled via the et-al attributes on cs:name (see
|
|
871
|
+
# Name), can be further customized with the optional cs:et-al element, which
|
|
872
|
+
# should be included directly after the cs:name element. The term attribute
|
|
873
|
+
# of this element can be set to either "et-al" (default) or to "and others"
|
|
874
|
+
# to use either term (with this different et-al terms can be used for
|
|
875
|
+
# citations and the bibliography). In addition, attributes for affixes and
|
|
876
|
+
# formatting can be used, for example to italicize the et-al term:
|
|
877
|
+
#
|
|
878
|
+
# <names variable="author">
|
|
879
|
+
# <name/>
|
|
880
|
+
# <et-al term="and others" font-style="italic"/>
|
|
881
|
+
# </names>
|
|
882
|
+
#
|
|
883
|
+
class EtAl < Node
|
|
884
|
+
attr_fields Nodes.formatting_attributes
|
|
885
|
+
attr_fields %w{ term }
|
|
886
|
+
|
|
887
|
+
attr_accessor :parent
|
|
888
|
+
|
|
889
|
+
def process(data, processor)
|
|
890
|
+
super
|
|
891
|
+
localized_terms(term || 'et-al').to_s(attributes)
|
|
892
|
+
rescue Exception => e
|
|
893
|
+
handle_processing_error(e, data, processor)
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
format_on :process
|
|
897
|
+
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
# The cs:label element, used to output text terms whose pluralization
|
|
902
|
+
# depends on the contents of another variable (e.g. "(editors)" in "Doe and
|
|
903
|
+
# Smith (editors)"), is discussed in detail in the label section. It should
|
|
904
|
+
# be included after the cs:name and cs:et-al elements, but before the
|
|
905
|
+
# cs:substitute element. When used within cs:names, the variable attribute
|
|
906
|
+
# should be omitted, as the value set on the parent cs:names element is
|
|
907
|
+
# used.
|
|
908
|
+
#
|
|
909
|
+
# The Citation Style Language includes several variables that have
|
|
910
|
+
# matching terms. The cs:label element can be used to render one of these
|
|
911
|
+
# terms, while matching the term plurality with that of the corresponding
|
|
912
|
+
# variable. The variable/term combination is selected with the variable
|
|
913
|
+
# attribute, which can be set to either "page" or "locator". When cs:label
|
|
914
|
+
# is used as a child element of cs:names, the value of the variable
|
|
915
|
+
# attribute is automatically inherited from the parent cs:names element.
|
|
916
|
+
# The example below displays the "page" variable, using the singular form
|
|
917
|
+
# of the "page" term for a single page ("page 5"), or the plural form for
|
|
918
|
+
# a page range ("pages 5-7").
|
|
919
|
+
#
|
|
920
|
+
class Label < Node
|
|
921
|
+
attr_fields Nodes.formatting_attributes
|
|
922
|
+
attr_fields %w{ variable plural form }
|
|
923
|
+
|
|
924
|
+
def process(data, processor)
|
|
925
|
+
localized_terms(data['label'].to_s).to_s(attributes.merge({ 'plural' => plural?(data, 0).to_s }))
|
|
926
|
+
rescue Exception => e
|
|
927
|
+
handle_processing_error(e, data, processor)
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
def process_names(role, number, processor)
|
|
931
|
+
localized_terms(role).to_s(attributes.merge({ 'plural' => plural?(nil, number).to_s }))
|
|
932
|
+
rescue Exception => e
|
|
933
|
+
handle_processing_error(e, data, processor)
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
format_on :process
|
|
937
|
+
format_on :process_names
|
|
938
|
+
|
|
939
|
+
def plural?(data, number)
|
|
940
|
+
case
|
|
941
|
+
when plural == 'always'
|
|
942
|
+
true
|
|
943
|
+
when plural == 'never'
|
|
944
|
+
false
|
|
945
|
+
when number > 1
|
|
946
|
+
true
|
|
947
|
+
when ['locator'].include?(variable)
|
|
948
|
+
data[variable].to_s.match(/\d+f|\d+\-\d+/)
|
|
949
|
+
else
|
|
950
|
+
false
|
|
951
|
+
end
|
|
952
|
+
end
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
# The optional cs:substitute element, which should be included as the last
|
|
957
|
+
# child element of cs:names, controls substitution in case the name
|
|
958
|
+
# variables specified in the parent cs:names element are empty. The
|
|
959
|
+
# substitutions are specified as child elements of cs:substitute, and can
|
|
960
|
+
# consist of any of the standard rendering elements (with the exception of
|
|
961
|
+
# cs:layout). It is also possible to use a shorthand version of cs:names,
|
|
962
|
+
# which doesn't allow for any child elements, and uses the attributes values
|
|
963
|
+
# set on the cs:name and cs:et-al child elements of the original cs:names
|
|
964
|
+
# element. If cs:substitute contains multiple child elements, the first
|
|
965
|
+
# element to return a non-empty result is used for substitution. Substituted
|
|
966
|
+
# variables are repressed in the rest of the output to prevent duplication.
|
|
967
|
+
# An example, where an empty "author" name variable is substituted by the
|
|
968
|
+
# "editor" name variable, or, when no editors exist, by the "title" macro:
|
|
969
|
+
#
|
|
970
|
+
# <macro name="author">
|
|
971
|
+
# <names variable="author">
|
|
972
|
+
# <name/>
|
|
973
|
+
# <substitute>
|
|
974
|
+
# <names variable="editor"/>
|
|
975
|
+
# <text macro="title"/>
|
|
976
|
+
# </substitute>
|
|
977
|
+
# </names>
|
|
978
|
+
# </macro>
|
|
979
|
+
#
|
|
980
|
+
class Substitute < Node
|
|
981
|
+
|
|
982
|
+
def process(data, processor)
|
|
983
|
+
super
|
|
984
|
+
children.each do |child|
|
|
985
|
+
processed = child.process(data, processor)
|
|
986
|
+
return processed unless processed.empty?
|
|
987
|
+
end
|
|
988
|
+
''
|
|
989
|
+
rescue Exception => e
|
|
990
|
+
handle_processing_error(e, data, processor)
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
# The cs:names element can be used to display the contents of one or more
|
|
996
|
+
# name variables, each of which can contain multiple names (e.g. the
|
|
997
|
+
# "author" variable will contain all the cited item's author names). The
|
|
998
|
+
# variables to be displayed are set with the variable attribute. If multiple
|
|
999
|
+
# variables are selected (separated by single spaces, see example below),
|
|
1000
|
+
# each variable is independently rendered in the order specified, with one
|
|
1001
|
+
# exception: if the value of variable consists of "editor" and "translator"
|
|
1002
|
+
# (in either order), and if the contents of the two name variables is
|
|
1003
|
+
# identical, then the contents of only one name variable is rendered. In
|
|
1004
|
+
# addition, the "editor-translator" term is used if the cs:names element
|
|
1005
|
+
# contains a cs:label element, replacing the default "editor" and
|
|
1006
|
+
# "translator" terms (e.g., this might result in "Doe (editor &
|
|
1007
|
+
# translator)". The delimiter attribute may be set on cs:names to delimit
|
|
1008
|
+
# the names of the different name variables (e.g. the semicolon in "Doe
|
|
1009
|
+
# (editor); Johnson (translator)").
|
|
1010
|
+
#
|
|
1011
|
+
# <names variable="editor translator" delimiter="; ">
|
|
1012
|
+
# <name/>
|
|
1013
|
+
# <label prefix=" (" suffix=")"/>
|
|
1014
|
+
# </names>
|
|
1015
|
+
#
|
|
1016
|
+
# There are four child elements associated with the cs:names element:
|
|
1017
|
+
# cs:name, cs:et-al, cs:substitute and cs:label (all discussed below). In
|
|
1018
|
+
# addition, the cs:names element may carry the attributes for affixes,
|
|
1019
|
+
# display and formatting.
|
|
1020
|
+
#
|
|
1021
|
+
class Names < Node
|
|
1022
|
+
attr_fields Nodes.formatting_attributes
|
|
1023
|
+
attr_fields %w{ variable delimiter }
|
|
1024
|
+
|
|
1025
|
+
[:name, :et_al, :label, :substitute].each do |method_id|
|
|
1026
|
+
klass = CSL::Nodes.const_get(method_id.to_s.split(/_/).map(&:capitalize).join)
|
|
1027
|
+
define_method method_id do
|
|
1028
|
+
elements = children.empty? && parent.is_a?(Substitute) && klass != Substitute ? parent.parent.children : children
|
|
1029
|
+
elements.detect { |child| child.class == klass }
|
|
1030
|
+
end
|
|
1031
|
+
end
|
|
1032
|
+
|
|
1033
|
+
def prefix_label?
|
|
1034
|
+
children.map {|c| [Label, Name].include?(c.class) ? c.class : nil }.compact == [Label, Name]
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
def process(data, processor)
|
|
1038
|
+
names = collect_names(data)
|
|
1039
|
+
|
|
1040
|
+
count_only = self.name.form == 'count'
|
|
1041
|
+
|
|
1042
|
+
unless names.empty? || names.map(&:last).flatten.empty?
|
|
1043
|
+
|
|
1044
|
+
# handle the editor-translator special case
|
|
1045
|
+
if names.map(&:first).sort.join.match(/editortranslator/)
|
|
1046
|
+
editors = names.detect { |name| name.first == 'editor' }
|
|
1047
|
+
translators = names.detect { |name| name.first == 'translator' }
|
|
1048
|
+
|
|
1049
|
+
if editors.last.sort == translators.last.sort
|
|
1050
|
+
editors[0] = 'editortranslator'
|
|
1051
|
+
names.delete(translators)
|
|
1052
|
+
end
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
names = names.map do |role, names|
|
|
1056
|
+
processed = []
|
|
1057
|
+
|
|
1058
|
+
truncated = name.truncate(names)
|
|
1059
|
+
|
|
1060
|
+
unless count_only
|
|
1061
|
+
processed << name.process_names(role, truncated, processor)
|
|
1062
|
+
|
|
1063
|
+
if names.length > truncated.length
|
|
1064
|
+
# use delimiter before et al. if there is more than a single name; squeeze whitespace
|
|
1065
|
+
others = (et_al.nil? ? localized_terms('et-al').to_s : et_al.process(data, processor))
|
|
1066
|
+
link = (name.et_al_use_first.to_i > 1 || name.delimiter_precedes_et_al? ? name.delimiter : ' ')
|
|
1067
|
+
|
|
1068
|
+
processed << [link, others].join.squeeze(' ')
|
|
1069
|
+
end
|
|
1070
|
+
|
|
1071
|
+
processed.send(prefix_label? ? :unshift : :push, label.process_names(role, names.length, processor)) unless label.nil?
|
|
1072
|
+
else
|
|
1073
|
+
processed << truncated.length
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
processed.join
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
count_only ? names.inject(0) { |a, b| a.to_i + b.to_i }.to_s : names.join(delimiter)
|
|
1080
|
+
else
|
|
1081
|
+
count_only ? '0' : substitute.nil? ? '' : substitute.process(data, processor)
|
|
1082
|
+
end
|
|
1083
|
+
rescue Exception => e
|
|
1084
|
+
handle_processing_error(e, data, processor)
|
|
1085
|
+
end
|
|
1086
|
+
|
|
1087
|
+
format_on :process
|
|
1088
|
+
|
|
1089
|
+
protected
|
|
1090
|
+
|
|
1091
|
+
# @returns a list of all name variables covered by this node; each list
|
|
1092
|
+
# is wrapped in a list containing the lists role (e.g., 'editor')
|
|
1093
|
+
# followed by the list proper.
|
|
1094
|
+
def collect_names(item)
|
|
1095
|
+
return [] unless self.variable?
|
|
1096
|
+
self.variable.split(/\s+/).map { |variable| [variable, (item[variable] || []).map(&:clone)] }
|
|
1097
|
+
end
|
|
1098
|
+
|
|
1099
|
+
def inherit_attributes(node)
|
|
1100
|
+
inherit_attributes_from(node, ['citation', 'bibliography', 'style'], ['delimiter'], 'names-')
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
# The cs:group element may contain one or more rendering elements (not
|
|
1107
|
+
# cs:layout). cs:group itself may carry the delimiter attribute (to
|
|
1108
|
+
# delimit the enclosed elements) and the attributes for affixes (applied
|
|
1109
|
+
# to the group output as a whole), display and formatting (formatting
|
|
1110
|
+
# settings are transmitted to the enclosed elements). Note that cs:group
|
|
1111
|
+
# implicitly acts as a conditional: cs:group and its child elements are
|
|
1112
|
+
# suppressed if a) at least one rendering element in cs:group calls a
|
|
1113
|
+
# variable (either directly or via a macro), and b) all variables that are
|
|
1114
|
+
# called are empty. This behavior exists to accommodate descriptive
|
|
1115
|
+
# cs:text elements. For example
|
|
1116
|
+
#
|
|
1117
|
+
# <layout>
|
|
1118
|
+
# <group prefix="(" suffix=")">
|
|
1119
|
+
# <text value="Published by: "/>
|
|
1120
|
+
# <text variable="publisher"/>
|
|
1121
|
+
# </group>
|
|
1122
|
+
# </layout>
|
|
1123
|
+
#
|
|
1124
|
+
# results in "(Published by: Company A)" when the "publisher" variable is
|
|
1125
|
+
# set to "Company A", but doesn't generate output when the "publisher"
|
|
1126
|
+
# variable is empty.
|
|
1127
|
+
#
|
|
1128
|
+
class Group < Node
|
|
1129
|
+
attr_fields Nodes.formatting_attributes
|
|
1130
|
+
attr_fields %w{ delimiter }
|
|
1131
|
+
|
|
1132
|
+
def process(data, processor)
|
|
1133
|
+
start_observing(data)
|
|
1134
|
+
|
|
1135
|
+
processed = children.map { |child| child.process(data, processor) }.reject(&:empty?).join(delimiter)
|
|
1136
|
+
|
|
1137
|
+
stop_observing(data)
|
|
1138
|
+
|
|
1139
|
+
# if any variable returned nil, skip the entire group
|
|
1140
|
+
skip? ? '' : processor.format(processed, attributes)
|
|
1141
|
+
rescue Exception => e
|
|
1142
|
+
handle_processing_error(e, data, processor)
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
def start_observing(item)
|
|
1146
|
+
@variables = []
|
|
1147
|
+
item.add_observer(self)
|
|
1148
|
+
end
|
|
1149
|
+
|
|
1150
|
+
def stop_observing(item)
|
|
1151
|
+
item.delete_observer(self)
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1154
|
+
def update(key, value)
|
|
1155
|
+
@variables << [key, value]
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
def skip?
|
|
1159
|
+
@variables && @variables.map(&:last).all?(&:nil?)
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
protected
|
|
1164
|
+
|
|
1165
|
+
def set_defaults
|
|
1166
|
+
formatting_attributes = collect_formatting_attributes(%w{ delimiter suffix prefix })
|
|
1167
|
+
|
|
1168
|
+
children.each do |child|
|
|
1169
|
+
formatting_attributes.each_pair do |key, value|
|
|
1170
|
+
child[key] ||= value
|
|
1171
|
+
end
|
|
1172
|
+
end
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
def collect_formatting_attributes(exceptions=[])
|
|
1176
|
+
formatting_attributes = {}
|
|
1177
|
+
(Nodes.formatting_attributes - exceptions).each do |key|
|
|
1178
|
+
formatting_attributes[key] = self[key]
|
|
1179
|
+
self.attributes.delete(key)
|
|
1180
|
+
end
|
|
1181
|
+
formatting_attributes
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
# Similarly to the conditional statements encountered in programming
|
|
1187
|
+
# languages, the cs:choose element allows for the conditional rendering of
|
|
1188
|
+
# rendering elements. An example is shown below:
|
|
1189
|
+
#
|
|
1190
|
+
# <choose>
|
|
1191
|
+
# <if type="book thesis" match="any">
|
|
1192
|
+
# <text variable="title" font-style="italic">
|
|
1193
|
+
# </if>
|
|
1194
|
+
# <else-if type="chapter">
|
|
1195
|
+
# <text variable="title" quotes="true">
|
|
1196
|
+
# </else-if>
|
|
1197
|
+
# <else>
|
|
1198
|
+
# <text variable="title">
|
|
1199
|
+
# </else>
|
|
1200
|
+
# </choose>
|
|
1201
|
+
#
|
|
1202
|
+
# cs:choose requires a cs:if child element, which may be followed by one or
|
|
1203
|
+
# more cs:else-if child elements, and an optional closing cs:else child
|
|
1204
|
+
# element. The cs:if and cs:else-if elements may contain any number of
|
|
1205
|
+
# rendering elements (except for cs:layout). As an empty cs:else element
|
|
1206
|
+
# would be superfluous, cs:else must contain at least one rendering element.
|
|
1207
|
+
# cs:if and cs:else-if elements must each hold at least one condition, which
|
|
1208
|
+
# are expressed as attributes. The different types of conditions available
|
|
1209
|
+
# are:
|
|
1210
|
+
#
|
|
1211
|
+
# disambiguate
|
|
1212
|
+
# The contents of an <if disambiguate="true"> block is only rendered if it
|
|
1213
|
+
# disambiguates two otherwise identical citations. This attempt at
|
|
1214
|
+
# disambiguation will only be made when all other disambiguation methods
|
|
1215
|
+
# have failed to uniquely identify the target source.
|
|
1216
|
+
#
|
|
1217
|
+
# is-numeric
|
|
1218
|
+
# Tests whether the given variables (Appendix I - Variables) contain numeric
|
|
1219
|
+
# data.
|
|
1220
|
+
#
|
|
1221
|
+
# is-uncertain-date
|
|
1222
|
+
# Tests whether the given date variables contain uncertain dates.
|
|
1223
|
+
#
|
|
1224
|
+
# locator
|
|
1225
|
+
# Tests whether the locator matches the given locator variable subtype (see
|
|
1226
|
+
# Locators).
|
|
1227
|
+
#
|
|
1228
|
+
# position
|
|
1229
|
+
# Tests whether the position of the item cite matches the given positions
|
|
1230
|
+
# (when called within cs:bibliography, this condition will always test
|
|
1231
|
+
# "false"). The different positions are (note on terminology: a citation
|
|
1232
|
+
# refers to a citation group, which contains one or more cites to individual
|
|
1233
|
+
# items):
|
|
1234
|
+
#
|
|
1235
|
+
# * "first": the position of a cite that is the first to reference an item
|
|
1236
|
+
# * "ibid"/"ibid-with-locator"/"subsequent": a cite that references an
|
|
1237
|
+
# earlier cited item always has the "subsequent" position. In special
|
|
1238
|
+
# cases cites may have the "ibid" or "ibid-with-locator" position. These
|
|
1239
|
+
# positions are only assigned when:
|
|
1240
|
+
#
|
|
1241
|
+
# 1. the current cite immediately follows on another cite, within the same
|
|
1242
|
+
# citation, that references the same item
|
|
1243
|
+
#
|
|
1244
|
+
# or
|
|
1245
|
+
#
|
|
1246
|
+
# 2. the current cite is the first cite in the citation, and the previous
|
|
1247
|
+
# citation includes a single cite that references the same item
|
|
1248
|
+
#
|
|
1249
|
+
#
|
|
1250
|
+
# If either requirement is met, the presence of locators determines which
|
|
1251
|
+
# position is assigned:
|
|
1252
|
+
#
|
|
1253
|
+
# 1. Preceding cite does not have a locator: if the current cite has a
|
|
1254
|
+
# locator, the position of the current cite is "ibid-with-locator".
|
|
1255
|
+
# Otherwise the position is "ibid".
|
|
1256
|
+
#
|
|
1257
|
+
# 2. Preceding cite does have a locator: if the current cite has the same
|
|
1258
|
+
# locator, the position of the current cite is "ibid". If the locator
|
|
1259
|
+
# differs the position is "ibid-with-locator". If the current cite
|
|
1260
|
+
# lacks a locator the position is "subsequent".
|
|
1261
|
+
#
|
|
1262
|
+
# * "near-note": the position of a cite following another cite that
|
|
1263
|
+
# references the same item. Both cites have to be located in foot or
|
|
1264
|
+
# endnotes, and the distance between both cites may not exceed the maximum
|
|
1265
|
+
# distance (measured in number of foot or endnotes) set with the
|
|
1266
|
+
# near-note-distance option (see Note Distance).
|
|
1267
|
+
#
|
|
1268
|
+
# Note that each cite can have multiple position values. Whenever
|
|
1269
|
+
# position="ibid-with-locator" is true, position="ibid" is also true.
|
|
1270
|
+
# And whenever position="ibid" or position="near-note" is true,
|
|
1271
|
+
# position="subsequent" is also true.
|
|
1272
|
+
#
|
|
1273
|
+
# type
|
|
1274
|
+
# Tests whether the item matches the given types (Appendix II - Types).
|
|
1275
|
+
#
|
|
1276
|
+
# variable
|
|
1277
|
+
# Tests whether the given variables (Appendix I - Variables) contain
|
|
1278
|
+
# non-empty values.
|
|
1279
|
+
#
|
|
1280
|
+
# With the exception of disambiguate, all conditions allow for multiple test
|
|
1281
|
+
# values (separated with spaces, e.g. "book thesis").
|
|
1282
|
+
#
|
|
1283
|
+
# The cs:if and cs:else-if elements may include the match attribute to
|
|
1284
|
+
# control the testing logic, with possible values:
|
|
1285
|
+
#
|
|
1286
|
+
# * "all" (default): the element only tests "true" when all conditions test "true" for all given test values
|
|
1287
|
+
# * "any": the element tests "true" when any condition tests "true" for any given test value
|
|
1288
|
+
# * "none": the element only tests "true" when none of the conditions test "true" for any given test value
|
|
1289
|
+
#
|
|
1290
|
+
class Choose < Node
|
|
1291
|
+
|
|
1292
|
+
def process(data, processor)
|
|
1293
|
+
children.each do |child|
|
|
1294
|
+
return child.process(data, processor) if child.evaluate(data, processor)
|
|
1295
|
+
end
|
|
1296
|
+
''
|
|
1297
|
+
rescue Exception => e
|
|
1298
|
+
handle_processing_error(e, data, processor)
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
end
|
|
1302
|
+
|
|
1303
|
+
class ConditionalBlock < Node
|
|
1304
|
+
attr_fields %w{ disambiguate is-numeric is-uncertain-date locator
|
|
1305
|
+
position type variable match }
|
|
1306
|
+
|
|
1307
|
+
def process(data, processor)
|
|
1308
|
+
children.map { |child| child.process(data, processor) }.join
|
|
1309
|
+
rescue Exception => e
|
|
1310
|
+
handle_processing_error(e, data, processor)
|
|
1311
|
+
end
|
|
1312
|
+
|
|
1313
|
+
def evaluate(data, processor)
|
|
1314
|
+
case
|
|
1315
|
+
when disambiguate?
|
|
1316
|
+
# CiteProc.log.warn "Choose disambiguate not implemented yet"
|
|
1317
|
+
false
|
|
1318
|
+
|
|
1319
|
+
when is_numeric?
|
|
1320
|
+
data[is_numeric] && data[is_numeric].numeric?
|
|
1321
|
+
|
|
1322
|
+
when is_uncertain_date?
|
|
1323
|
+
data[is_uncertain_date] && data[is_uncertain_date].uncertain?
|
|
1324
|
+
|
|
1325
|
+
when has_locator?
|
|
1326
|
+
locator == data['locator'].to_s
|
|
1327
|
+
|
|
1328
|
+
when has_position?
|
|
1329
|
+
# CiteProc.log.warn "Choose position not implemented yet"
|
|
1330
|
+
false
|
|
1331
|
+
|
|
1332
|
+
when has_type?
|
|
1333
|
+
matches?(type.split(/\s+/)) { |type| type == data['type'].to_s }
|
|
1334
|
+
|
|
1335
|
+
when has_variable?
|
|
1336
|
+
matches?(variable.split(/\s+/)) { |variable| !data[variable].nil? }
|
|
1337
|
+
|
|
1338
|
+
when self.is_a?(Else)
|
|
1339
|
+
true
|
|
1340
|
+
|
|
1341
|
+
else
|
|
1342
|
+
CiteProc.log :warn, "conditional block #{ inspect } could not be evaluated"
|
|
1343
|
+
false
|
|
1344
|
+
|
|
1345
|
+
end
|
|
1346
|
+
rescue Exception => e
|
|
1347
|
+
CiteProc.log.error "failed to evaluate item #{data.inspect}: #{ e.message }; returning false."
|
|
1348
|
+
false
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
# @returns true if &condition is true for any/all/none elements in the list
|
|
1352
|
+
def matches?(list, &condition)
|
|
1353
|
+
list.send([self['match'] || 'all', '?'].join, &condition)
|
|
1354
|
+
end
|
|
1355
|
+
|
|
1356
|
+
end
|
|
1357
|
+
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1360
|
+
%w{ If ElseIf Else }.each do |node_name|
|
|
1361
|
+
CSL::Nodes.const_set(node_name.to_sym, Class.new(CSL::Nodes::ConditionalBlock))
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
end
|