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.
Files changed (125) hide show
  1. data/README.md +78 -0
  2. data/lib/citeproc.rb +100 -0
  3. data/lib/citeproc/bibliography.rb +57 -0
  4. data/lib/citeproc/data.rb +149 -0
  5. data/lib/citeproc/date.rb +133 -0
  6. data/lib/citeproc/formatter.rb +38 -0
  7. data/lib/citeproc/item.rb +53 -0
  8. data/lib/citeproc/name.rb +284 -0
  9. data/lib/citeproc/processor.rb +166 -0
  10. data/lib/citeproc/selector.rb +61 -0
  11. data/lib/citeproc/variable.rb +82 -0
  12. data/lib/citeproc/version.rb +3 -0
  13. data/lib/csl/locale.rb +223 -0
  14. data/lib/csl/node.rb +72 -0
  15. data/lib/csl/nodes.rb +1364 -0
  16. data/lib/csl/renderer.rb +88 -0
  17. data/lib/csl/sort.rb +53 -0
  18. data/lib/csl/style.rb +110 -0
  19. data/lib/csl/term.rb +124 -0
  20. data/lib/extensions/core.rb +43 -0
  21. data/lib/plugins/filters/bibtex.rb +12 -0
  22. data/lib/plugins/formats/default.rb +134 -0
  23. data/lib/plugins/formats/html.rb +67 -0
  24. data/lib/support/attributes.rb +99 -0
  25. data/lib/support/tree.rb +80 -0
  26. data/resource/locale/locales-af-ZA.xml +304 -0
  27. data/resource/locale/locales-ar-AR.xml +304 -0
  28. data/resource/locale/locales-bg-BG.xml +304 -0
  29. data/resource/locale/locales-ca-AD.xml +304 -0
  30. data/resource/locale/locales-cs-CZ.xml +304 -0
  31. data/resource/locale/locales-da-DK.xml +304 -0
  32. data/resource/locale/locales-de-AT.xml +304 -0
  33. data/resource/locale/locales-de-CH.xml +304 -0
  34. data/resource/locale/locales-de-DE.xml +332 -0
  35. data/resource/locale/locales-el-GR.xml +303 -0
  36. data/resource/locale/locales-en-US.xml +313 -0
  37. data/resource/locale/locales-es-ES.xml +304 -0
  38. data/resource/locale/locales-et-EE.xml +304 -0
  39. data/resource/locale/locales-fr-FR.xml +304 -0
  40. data/resource/locale/locales-he-IL.xml +304 -0
  41. data/resource/locale/locales-hu-HU.xml +304 -0
  42. data/resource/locale/locales-is-IS.xml +304 -0
  43. data/resource/locale/locales-it-IT.xml +304 -0
  44. data/resource/locale/locales-ja-JP.xml +304 -0
  45. data/resource/locale/locales-kh-KH.xml +303 -0
  46. data/resource/locale/locales-ko-KR.xml +304 -0
  47. data/resource/locale/locales-mn-MN.xml +304 -0
  48. data/resource/locale/locales-nb-NO.xml +304 -0
  49. data/resource/locale/locales-nl-NL.xml +304 -0
  50. data/resource/locale/locales-nn-NO.xml +304 -0
  51. data/resource/locale/locales-pl-PL.xml +304 -0
  52. data/resource/locale/locales-pt-BR.xml +304 -0
  53. data/resource/locale/locales-pt-PT.xml +304 -0
  54. data/resource/locale/locales-ro-RO.xml +304 -0
  55. data/resource/locale/locales-ru-RU.xml +304 -0
  56. data/resource/locale/locales-sk-SK.xml +304 -0
  57. data/resource/locale/locales-sl-SI.xml +304 -0
  58. data/resource/locale/locales-sr-RS.xml +304 -0
  59. data/resource/locale/locales-sv-SE.xml +304 -0
  60. data/resource/locale/locales-th-TH.xml +304 -0
  61. data/resource/locale/locales-tr-TR.xml +304 -0
  62. data/resource/locale/locales-uk-UA.xml +304 -0
  63. data/resource/locale/locales-vi-VN.xml +304 -0
  64. data/resource/locale/locales-zh-CN.xml +304 -0
  65. data/resource/locale/locales-zh-TW.xml +304 -0
  66. data/resource/schema/csl-categories.rnc +39 -0
  67. data/resource/schema/csl-data.rnc +98 -0
  68. data/resource/schema/csl-terms.rnc +106 -0
  69. data/resource/schema/csl-types.rnc +39 -0
  70. data/resource/schema/csl-variables.rnc +182 -0
  71. data/resource/schema/csl.rnc +941 -0
  72. data/resource/style/acta-materialia-x.csl +128 -0
  73. data/resource/style/advanced-engineering-materials-x.csl +121 -0
  74. data/resource/style/ama.csl +185 -0
  75. data/resource/style/ama2-x.csl +179 -0
  76. data/resource/style/apa-x.csl +324 -0
  77. data/resource/style/apa.csl +254 -0
  78. data/resource/style/apsa-x.csl +163 -0
  79. data/resource/style/apsa.csl +176 -0
  80. data/resource/style/asa-x.csl +203 -0
  81. data/resource/style/asa.csl +216 -0
  82. data/resource/style/asm-journals-x.csl +131 -0
  83. data/resource/style/bibtex-x2.csl +175 -0
  84. data/resource/style/bluebook-demo-x.csl +392 -0
  85. data/resource/style/bluebook-demo.csl +942 -0
  86. data/resource/style/chicago-author-date-listing.csl +434 -0
  87. data/resource/style/chicago-author-date.csl +369 -0
  88. data/resource/style/chicago-fullnote-bibliography-bb.csl +928 -0
  89. data/resource/style/chicago-fullnote-bibliography.csl +695 -0
  90. data/resource/style/chicago-note-bibliography.csl +446 -0
  91. data/resource/style/chicago-note.csl +388 -0
  92. data/resource/style/greek-chicago-x.csl +1182 -0
  93. data/resource/style/harvard1-institution-italic.csl +190 -0
  94. data/resource/style/harvard1.csl +181 -0
  95. data/resource/style/ieee.csl +129 -0
  96. data/resource/style/mhra-x.csl +312 -0
  97. data/resource/style/mhra.csl +390 -0
  98. data/resource/style/mhra_note_without_bibliography-x.csl +330 -0
  99. data/resource/style/mhra_note_without_bibliography.csl +338 -0
  100. data/resource/style/mla-x.csl +178 -0
  101. data/resource/style/mla.csl +189 -0
  102. data/resource/style/nature-x.csl +81 -0
  103. data/resource/style/nature.csl +88 -0
  104. data/resource/style/nlm.csl +117 -0
  105. data/spec/citeproc/bibliography_spec.rb +45 -0
  106. data/spec/citeproc/citeproc_spec.rb +76 -0
  107. data/spec/citeproc/date_spec.rb +85 -0
  108. data/spec/citeproc/formatter_spec.rb +101 -0
  109. data/spec/citeproc/item_spec.rb +71 -0
  110. data/spec/citeproc/name_spec.rb +30 -0
  111. data/spec/citeproc/processor_spec.rb +61 -0
  112. data/spec/citeproc/selector_spec.rb +82 -0
  113. data/spec/citeproc/variable_spec.rb +69 -0
  114. data/spec/csl/locale_spec.rb +208 -0
  115. data/spec/csl/node_spec.rb +25 -0
  116. data/spec/csl/nodes_spec.rb +140 -0
  117. data/spec/csl/style_spec.rb +62 -0
  118. data/spec/csl/term_spec.rb +56 -0
  119. data/spec/fixtures/dates.yaml +80 -0
  120. data/spec/fixtures/names.yaml +115 -0
  121. data/spec/fixtures/nodes.yaml +245 -0
  122. data/spec/spec_helper.rb +18 -0
  123. data/spec/support/attributes_spec.rb +39 -0
  124. data/spec/support/tree_spec.rb +163 -0
  125. 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
@@ -0,0 +1,3 @@
1
+ module CiteProc
2
+ VERSION = '0.0.1'.freeze
3
+ end
@@ -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
@@ -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
@@ -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