citeproc-rb 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +93 -0
- data/Rakefile +73 -0
- data/TODO +25 -0
- data/VERSION +1 -0
- data/data/locales/locales_en.xml +89 -0
- data/data/locales/locales_nl.xml +96 -0
- data/data/styles/ama.csl +184 -0
- data/data/styles/apa.csl +230 -0
- data/data/styles/apsa.csl +175 -0
- data/data/styles/asa.csl +215 -0
- data/data/styles/chicago-author-date.csl +361 -0
- data/data/styles/chicago-fullnote-bibliography.csl +665 -0
- data/data/styles/chicago-note-bibliography.csl +437 -0
- data/data/styles/chicago-note.csl +383 -0
- data/data/styles/harvard.csl +185 -0
- data/data/styles/ieee.csl +111 -0
- data/data/styles/mhra.csl +317 -0
- data/data/styles/mla.csl +173 -0
- data/data/styles/nature.csl +87 -0
- data/data/styles/nlm.csl +118 -0
- data/examples/citeproc_driver.rb +174 -0
- data/lib/citeproc.rb +28 -0
- data/lib/citeproc/csl.rb +647 -0
- data/lib/citeproc/csl_parser.rb +479 -0
- data/lib/citeproc/csl_processor.rb +485 -0
- data/lib/citeproc/csl_reference.rb +228 -0
- data/lib/citeproc/filters/bibo/bibo.rb +635 -0
- data/lib/citeproc/filters/bibo/bibo_input_filter.rb +242 -0
- data/lib/citeproc/filters/bibo/bibo_utils.rb +75 -0
- data/lib/citeproc/filters/csl_citation/csl_citation.rb +137 -0
- data/lib/citeproc/filters/csl_citation/csl_citation_input_filter.rb +274 -0
- data/lib/citeproc/formatters/base_formatter.rb +115 -0
- data/lib/citeproc/formatters/xhtml_formatter.rb +144 -0
- data/lib/citeproc/input_filter.rb +97 -0
- data/test/fixtures/bibo_test_data.n3 +191 -0
- data/test/fixtures/bibo_test_data.yaml +96 -0
- data/test/fixtures/bibo_test_how_to_cook.n3 +63 -0
- data/test/fixtures/bibo_test_zotero.rdf +129 -0
- data/test/fixtures/csl_test_data.json +62 -0
- data/test/fixtures/csl_test_data.yaml +8 -0
- data/test/fixtures/locales/test_locale_en.xml +89 -0
- data/test/fixtures/styles/test_csl_parse.csl +230 -0
- data/test/fixtures/styles/test_groups.csl +32 -0
- data/test/fixtures/styles/test_names.csl +46 -0
- data/test/fixtures/styles/test_sort.csl +125 -0
- data/test/fixtures/styles/test_text_terms.csl +40 -0
- data/test/test_all.rb +14 -0
- data/test/test_bibo.rb +20 -0
- data/test/test_csl.rb +136 -0
- data/test/test_csl_parser.rb +222 -0
- data/test/test_sample_bibo.rb +82 -0
- metadata +101 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# == Synopsis
|
|
2
|
+
#
|
|
3
|
+
# Processes a series of document references according to a given CSL style.
|
|
4
|
+
# Builds a directed graph of ProcessedNodes, which can be used by a Formatter.
|
|
5
|
+
#
|
|
6
|
+
# == Author
|
|
7
|
+
#
|
|
8
|
+
# Liam Magee
|
|
9
|
+
#
|
|
10
|
+
# == Copyright
|
|
11
|
+
#
|
|
12
|
+
# Copyright (c) 2008, Liam Magee.
|
|
13
|
+
# Licensed under the same terms as Ruby - see http://www.ruby-lang.org/en/LICENSE.txt.
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
module Citeproc
|
|
17
|
+
|
|
18
|
+
# Represents a processed part of the CSL tree with results
|
|
19
|
+
class ProcessedNode
|
|
20
|
+
attr_reader :element, :results, :children
|
|
21
|
+
|
|
22
|
+
def initialize(element, results, children)
|
|
23
|
+
@element = element
|
|
24
|
+
@results = results
|
|
25
|
+
@children = children
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def empty?
|
|
29
|
+
results.empty?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class CslProcessor
|
|
34
|
+
|
|
35
|
+
def init_process(filter, style, locale)
|
|
36
|
+
@substitutions = []
|
|
37
|
+
@filter = filter
|
|
38
|
+
@style = style
|
|
39
|
+
@locale = locale
|
|
40
|
+
|
|
41
|
+
# Citation number & label
|
|
42
|
+
@citation_number = 1
|
|
43
|
+
@citation_label = "#"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def process_citation(filter, style, locale)
|
|
48
|
+
process_top_node(style.citation, filter, style, locale)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def process_bibliography(filter, style, locale)
|
|
53
|
+
process_top_node(style.bibliography, filter, style, locale)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def process_top_node(node, filter, style, locale)
|
|
58
|
+
init_process(filter, style, locale)
|
|
59
|
+
nodes = []
|
|
60
|
+
if node
|
|
61
|
+
@filter.each_sorted_citation(generate_sort_keys(node)) do |citation|
|
|
62
|
+
nodes << process_layout(node.layout) if node.layout
|
|
63
|
+
@citation_number += 1
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
nodes
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def process_all(filter, style, locale)
|
|
70
|
+
results = []
|
|
71
|
+
results << process_citation(filter, style, locale)
|
|
72
|
+
results << process_bibliography(filter, style, locale)
|
|
73
|
+
results
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def generate_sort_keys(node)
|
|
78
|
+
key_sets = []
|
|
79
|
+
if node.sort
|
|
80
|
+
@filter.each_citation do |citation|
|
|
81
|
+
keys = []
|
|
82
|
+
keys << citation
|
|
83
|
+
node.sort.keys.each do |key|
|
|
84
|
+
tmp = find_keys(process_element(key))
|
|
85
|
+
keys << tmp.flatten if tmp
|
|
86
|
+
end
|
|
87
|
+
key_sets << keys
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
key_sets
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def find_keys(node)
|
|
95
|
+
results = []
|
|
96
|
+
node.children.compact.each do |n|
|
|
97
|
+
results << n.results
|
|
98
|
+
results << find_keys(n)
|
|
99
|
+
end
|
|
100
|
+
results.flatten
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def process_layout(layout)
|
|
106
|
+
elements = process_elements(layout.elements)
|
|
107
|
+
process(layout, [], elements)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def process_elements(elements, context = nil)
|
|
112
|
+
results = []
|
|
113
|
+
elements.each do |element|
|
|
114
|
+
results << process_element(element, context) unless substituted?(element)
|
|
115
|
+
end
|
|
116
|
+
results
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Less than ideal... Checks if an element has already been substituted.
|
|
120
|
+
# In the case of variables, this is complicated due to the possibility
|
|
121
|
+
# of multiple variable names in the one attribute.
|
|
122
|
+
def substituted?(element)
|
|
123
|
+
return true if @substitutions.include?(element)
|
|
124
|
+
@substitutions.each do |sub|
|
|
125
|
+
if sub.kind_of?(ShortNames) and element.kind_of?(ShortNames)
|
|
126
|
+
element.variable.split.each do |e|
|
|
127
|
+
return true if e == sub.variable
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
false
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def process_element(element, context = nil)
|
|
136
|
+
case
|
|
137
|
+
when element.kind_of?(Names)
|
|
138
|
+
process_names(element)
|
|
139
|
+
# This is a special case for names declared within the substitute node of another names context.
|
|
140
|
+
when (element.kind_of?(ShortNames) and context)
|
|
141
|
+
process_name_series(element.variable, context)
|
|
142
|
+
when element.kind_of?(Date)
|
|
143
|
+
process_date(element)
|
|
144
|
+
when element.kind_of?(Text)
|
|
145
|
+
process_text(element)
|
|
146
|
+
when element.kind_of?(Label)
|
|
147
|
+
process_label(element, context)
|
|
148
|
+
when element.kind_of?(Group)
|
|
149
|
+
process_group(element)
|
|
150
|
+
when element.kind_of?(ConditionGroup)
|
|
151
|
+
process_group_conditions(element)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def process_names(names)
|
|
157
|
+
result = process_name_series(names.variable, names)
|
|
158
|
+
|
|
159
|
+
# Process the rest of the substitute node, if the result is blank
|
|
160
|
+
if !result or result.empty?
|
|
161
|
+
names.substitute.elements.each do |sub|
|
|
162
|
+
tmp = process_element(sub, names)
|
|
163
|
+
if tmp and !tmp.empty?
|
|
164
|
+
# Put this node on the substitutions stack
|
|
165
|
+
@substitutions << sub
|
|
166
|
+
result = tmp
|
|
167
|
+
break
|
|
168
|
+
end
|
|
169
|
+
end if names.substitute
|
|
170
|
+
end
|
|
171
|
+
result
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def process_name_series(variable, name_context)
|
|
176
|
+
name_def = name_context.name
|
|
177
|
+
|
|
178
|
+
# Format the names
|
|
179
|
+
name_groups = []
|
|
180
|
+
|
|
181
|
+
process_name(name_context, variable) do |name_strs, role|
|
|
182
|
+
labels = []
|
|
183
|
+
name_context.labels.each do |label|
|
|
184
|
+
labels << process_label(label, role)
|
|
185
|
+
end
|
|
186
|
+
tmp = process(name_def, name_strs, labels)
|
|
187
|
+
name_groups << tmp
|
|
188
|
+
end
|
|
189
|
+
result = process(name_context, [], name_groups)
|
|
190
|
+
result
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def process_name(name_context, roles)
|
|
194
|
+
name_str = ''
|
|
195
|
+
name_def = name_context.name
|
|
196
|
+
roles.split.each do |r|
|
|
197
|
+
names = []
|
|
198
|
+
et_al_counter = 0
|
|
199
|
+
@filter.extract_contributor(r, name_def.name_as_sort_order) do |n|
|
|
200
|
+
given_names = n.given_name
|
|
201
|
+
if name_def.form == "short"
|
|
202
|
+
name_str = "#{n.family_name}"
|
|
203
|
+
elsif name_def.name_as_sort_order
|
|
204
|
+
sep = name_def.sort_separator ? name_def.sort_separator : " "
|
|
205
|
+
name_str = n.family_name + sep + process_given_name(name_def, given_names)
|
|
206
|
+
else
|
|
207
|
+
name_str = process_given_name(name_def, given_names) + n.family_name
|
|
208
|
+
end
|
|
209
|
+
names << name_str.strip
|
|
210
|
+
end
|
|
211
|
+
# TODO: place et-al, collapse and disambiguate logic here
|
|
212
|
+
yield names, r if !names.empty?
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Need to add sort
|
|
217
|
+
def process_given_name(name_def, given_names)
|
|
218
|
+
if given_names
|
|
219
|
+
if name_def.initialize_with
|
|
220
|
+
given_names.split.collect{|n| n[0..0].upcase + name_def.initialize_with }.join
|
|
221
|
+
else
|
|
222
|
+
given_names
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def process_label(label, variable = nil)
|
|
229
|
+
results = []
|
|
230
|
+
if label.kind_of?(Label) and label.variable
|
|
231
|
+
var, singular = @filter.extract_label(label)
|
|
232
|
+
results << @locale.term_text(var, label.form, singular) if var and @locale
|
|
233
|
+
elsif variable
|
|
234
|
+
tmp = @locale.term_text(variable, label.form, singular) if @locale
|
|
235
|
+
results << tmp if tmp
|
|
236
|
+
end
|
|
237
|
+
results << '.' if label.include_period and !results.empty?
|
|
238
|
+
process(label, results, [])
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def process_date(date)
|
|
243
|
+
d = @filter.extract_date(date.variable)
|
|
244
|
+
elements = []
|
|
245
|
+
date.dateparts.each do |datepart|
|
|
246
|
+
elements << process_datepart(datepart, d)
|
|
247
|
+
end
|
|
248
|
+
#puts elements
|
|
249
|
+
process(date, [], elements)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def process_datepart(datepart, date)
|
|
254
|
+
process(datepart, [get_datepart(datepart, date)], []) if date
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_datepart(datepart, date)
|
|
259
|
+
#puts date.class
|
|
260
|
+
#puts date
|
|
261
|
+
if date.is_a?(String)
|
|
262
|
+
# Can't do much here for now...
|
|
263
|
+
date
|
|
264
|
+
else
|
|
265
|
+
case datepart.name
|
|
266
|
+
when "month"
|
|
267
|
+
case datepart.form
|
|
268
|
+
when "short"
|
|
269
|
+
date.strftime("%b") + (datepart.include_period ? '.' : '')
|
|
270
|
+
when "numeric"
|
|
271
|
+
date.month.to_s
|
|
272
|
+
when "numeric-leading-zeros"
|
|
273
|
+
date.strftime("%d")
|
|
274
|
+
else
|
|
275
|
+
date.strftime("%B")
|
|
276
|
+
end
|
|
277
|
+
when "day"
|
|
278
|
+
if datepart.form == "numeric"
|
|
279
|
+
date.day.to_s
|
|
280
|
+
elsif datepart.form == "numeric-leading-zeros" # Leading zeros
|
|
281
|
+
date.strftime("%d")
|
|
282
|
+
else # Ordinal - NB: not internationalised
|
|
283
|
+
if date.day % 10 == 1 and date.day / 10 != 1
|
|
284
|
+
date.day.to_s + "st"
|
|
285
|
+
elsif date.day % 10 == 2 and date.day / 10 != 1
|
|
286
|
+
date.day.to_s + "nd"
|
|
287
|
+
elsif date.day % 10 == 3 and date.day / 10 != 1
|
|
288
|
+
date.day.to_s + "rd"
|
|
289
|
+
else
|
|
290
|
+
date.day.to_s + "th"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
when "year"
|
|
294
|
+
if datepart.form == "short"
|
|
295
|
+
date.strftime("%y")
|
|
296
|
+
else
|
|
297
|
+
date.strftime("%Y")
|
|
298
|
+
end
|
|
299
|
+
else
|
|
300
|
+
date.to_s
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def process_text(text)
|
|
307
|
+
node = nil
|
|
308
|
+
case
|
|
309
|
+
when text.kind_of?(VariableText)
|
|
310
|
+
tmp = text.variable.split.collect { |e| process_variable(e) }
|
|
311
|
+
node = process(text, tmp, [])
|
|
312
|
+
when text.kind_of?(MacroText)
|
|
313
|
+
node = expand_macro(text)
|
|
314
|
+
when text.kind_of?(TermText)
|
|
315
|
+
# Fix
|
|
316
|
+
node = process(text, [text.value], [])
|
|
317
|
+
when text.kind_of?(ValueText)
|
|
318
|
+
node = process(text, [text.value], [])
|
|
319
|
+
end
|
|
320
|
+
node
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def process_variable(variable)
|
|
325
|
+
case variable
|
|
326
|
+
when "citation-number"
|
|
327
|
+
@citation_number
|
|
328
|
+
when "citation-label"
|
|
329
|
+
@citation_label
|
|
330
|
+
else
|
|
331
|
+
@filter.extract_variable(variable)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def process_group(group)
|
|
339
|
+
process(group, [], process_elements(group.elements)) if eval_group_variable(group)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# Test whether there is a non-blank variable in this group
|
|
344
|
+
# TODO: Very poor perfomance (forces 2-time evaluation of all rules) and
|
|
345
|
+
# incorrect treatment of conditions - evaluates ALL for variable resolution.
|
|
346
|
+
def eval_group_variable(group)
|
|
347
|
+
group.elements.flatten.each do |element|
|
|
348
|
+
if element.kind_of? VariableText
|
|
349
|
+
var = element.variable
|
|
350
|
+
return true if eval_variable(var)
|
|
351
|
+
elsif element.kind_of? Date
|
|
352
|
+
var = element.variable
|
|
353
|
+
return true if eval_variable(var)
|
|
354
|
+
elsif element.kind_of? Names
|
|
355
|
+
var = element.variable
|
|
356
|
+
return true if eval_variable(var)
|
|
357
|
+
element.substitute.names.each do |name|
|
|
358
|
+
var = name.variable
|
|
359
|
+
return true if eval_variable(var)
|
|
360
|
+
end if element.substitute
|
|
361
|
+
elsif element.kind_of? MacroText
|
|
362
|
+
macro = get_macro(element)
|
|
363
|
+
if macro
|
|
364
|
+
return true if eval_group_variable(macro)
|
|
365
|
+
else
|
|
366
|
+
raise "Could not find macro with the name of: #{element.value}."
|
|
367
|
+
end
|
|
368
|
+
elsif element.kind_of? Group
|
|
369
|
+
return true if eval_group_variable(element)
|
|
370
|
+
elsif element.kind_of? ConditionGroup
|
|
371
|
+
element.conditions.each do |c|
|
|
372
|
+
return true if eval_group_variable(c)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end if group.elements
|
|
376
|
+
false
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def eval_variable(var)
|
|
380
|
+
var.split.each do |v|
|
|
381
|
+
tmp = @filter.extract_variable(v)
|
|
382
|
+
if tmp and !tmp.to_s.empty?
|
|
383
|
+
return true
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
false
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def process_group_conditions(conditions)
|
|
391
|
+
node = nil
|
|
392
|
+
conditions.each do |condition|
|
|
393
|
+
if met?(condition)
|
|
394
|
+
node = process(condition, [], process_elements(condition.elements))
|
|
395
|
+
break
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
node
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def met?(condition)
|
|
402
|
+
if condition.type
|
|
403
|
+
met_type?(condition)
|
|
404
|
+
elsif condition.variable
|
|
405
|
+
met_variable?(condition)
|
|
406
|
+
elsif condition.position
|
|
407
|
+
met_position?(condition)
|
|
408
|
+
elsif condition.disambiguate
|
|
409
|
+
met_disambiguate?(condition)
|
|
410
|
+
elsif condition.locator
|
|
411
|
+
met_locator?(condition)
|
|
412
|
+
else
|
|
413
|
+
true
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def met_type?(condition)
|
|
418
|
+
conditions = condition.type.split
|
|
419
|
+
if condition.match == "none"
|
|
420
|
+
!conditions.include?(@filter.resolve_type)
|
|
421
|
+
else
|
|
422
|
+
conditions.include?(@filter.resolve_type)
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def met_variable?(condition)
|
|
427
|
+
conditions = condition.variable.split
|
|
428
|
+
if condition.match == "none"
|
|
429
|
+
conditions.each do |c|
|
|
430
|
+
return false if @filter.extract_variable(c)
|
|
431
|
+
end
|
|
432
|
+
return true
|
|
433
|
+
else
|
|
434
|
+
conditions.each do |c|
|
|
435
|
+
tmp = @filter.extract_variable(c)
|
|
436
|
+
return false if !tmp
|
|
437
|
+
case tmp.class
|
|
438
|
+
when Array
|
|
439
|
+
return !tmp.empty?
|
|
440
|
+
when String
|
|
441
|
+
return !tmp.strip.empty?
|
|
442
|
+
else
|
|
443
|
+
return false
|
|
444
|
+
end
|
|
445
|
+
#return true if (tmp != nil and (tmp.is_a?(Array) ? !tmp.empty? : true))
|
|
446
|
+
end
|
|
447
|
+
return false
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def met_position?(condition)
|
|
453
|
+
false # TODO
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def met_disambiguate?(condition)
|
|
457
|
+
false # TODO
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def met_locator?(condition)
|
|
461
|
+
false # TODO
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def get_macro(macro)
|
|
466
|
+
@style.macros.each do |m|
|
|
467
|
+
return m if m.name == macro.macro
|
|
468
|
+
end
|
|
469
|
+
nil
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def expand_macro(macro)
|
|
473
|
+
results = []
|
|
474
|
+
m = get_macro(macro)
|
|
475
|
+
results = process_elements(m.elements) if m
|
|
476
|
+
process(macro, [], results)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def process(element, results, children)
|
|
481
|
+
ProcessedNode.new(element, results, children)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
end
|
|
485
|
+
end
|