citeproc-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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