citeproc-ruby 2.1.8 → 2.3.0
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.
- checksums.yaml +4 -4
- data/README.md +72 -70
- data/lib/citeproc/ruby/engine.rb +11 -15
- data/lib/citeproc/ruby/format.rb +35 -9
- data/lib/citeproc/ruby/formats/default.rb +3 -2
- data/lib/citeproc/ruby/formats/html.rb +44 -4
- data/lib/citeproc/ruby/formats/latex.rb +2 -2
- data/lib/citeproc/ruby/renderer/date.rb +100 -16
- data/lib/citeproc/ruby/renderer/format.rb +18 -83
- data/lib/citeproc/ruby/renderer/group.rb +18 -6
- data/lib/citeproc/ruby/renderer/label.rb +2 -2
- data/lib/citeproc/ruby/renderer/layout.rb +4 -1
- data/lib/citeproc/ruby/renderer/locale.rb +63 -2
- data/lib/citeproc/ruby/renderer/macro.rb +2 -4
- data/lib/citeproc/ruby/renderer/names.rb +48 -65
- data/lib/citeproc/ruby/renderer/number.rb +2 -2
- data/lib/citeproc/ruby/renderer/observer.rb +10 -4
- data/lib/citeproc/ruby/renderer/state.rb +5 -1
- data/lib/citeproc/ruby/renderer/text.rb +16 -3
- data/lib/citeproc/ruby/renderer.rb +42 -14
- data/lib/citeproc/ruby/sort.rb +79 -36
- data/lib/citeproc/ruby/version.rb +1 -1
- metadata +14 -25
- data/.codeclimate.yml +0 -17
- data/.document +0 -4
- data/.github/workflows/ci.yml +0 -33
- data/.yardopts +0 -2
- data/AGPL +0 -662
- data/Gemfile +0 -31
- data/Rakefile +0 -54
- data/cucumber.yml +0 -1
|
@@ -3,39 +3,84 @@ module CiteProc
|
|
|
3
3
|
|
|
4
4
|
class Renderer
|
|
5
5
|
|
|
6
|
+
# The date parts from largest to smallest
|
|
7
|
+
DATE_PARTS = %w{ year month day }.freeze
|
|
8
|
+
|
|
6
9
|
# @param item [CiteProc::CitationItem]
|
|
7
10
|
# @param node [CSL::Node]
|
|
8
|
-
# @raise
|
|
11
|
+
# @raise CSL::Error if the locale has no date in the form of the node
|
|
9
12
|
# @return [String]
|
|
10
13
|
def render_date(item, node)
|
|
11
14
|
return '' unless node.has_variable?
|
|
12
15
|
|
|
13
|
-
date
|
|
14
|
-
|
|
16
|
+
# Observers see the date as rendered:
|
|
17
|
+
# a date without the date parts used counts as empty
|
|
18
|
+
item.data.deferred_read_attribute(node.variable) do |date|
|
|
19
|
+
next '' if date.nil? || date.empty?
|
|
20
|
+
next date.to_s if date.literal?
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
parts, delimiter = node.parts_for(locale), node.delimiter_for(locale)
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
if date.range?
|
|
25
|
+
render_date_range date, parts, delimiter
|
|
26
|
+
else
|
|
27
|
+
render_date_parts date, parts, delimiter
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
19
31
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
# Renders a date range. The date parts which are the same for
|
|
33
|
+
# both dates are rendered only once; the date parts which differ
|
|
34
|
+
# are rendered for both dates and are joined by the range delimiter
|
|
35
|
+
# of the largest date part that differs.
|
|
36
|
+
#
|
|
37
|
+
# @param date [CiteProc::Date] the date range
|
|
38
|
+
# @param parts [Array<CSL::Style::DatePart, CSL::Locale::DatePart>]
|
|
39
|
+
# @param delimiter [String]
|
|
40
|
+
# @return [String]
|
|
41
|
+
def render_date_range(date, parts, delimiter)
|
|
42
|
+
from, to = date.parts
|
|
23
43
|
|
|
24
|
-
|
|
44
|
+
# Skip date parts without values (e.g., days in a month range)
|
|
45
|
+
parts = parts.select { |part| from[part.name] || to[part.name] }
|
|
25
46
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
# Open ranges are rendered with a trailing range delimiter
|
|
48
|
+
return [
|
|
49
|
+
render_date_parts(from, parts, delimiter),
|
|
50
|
+
range_delimiter_for(parts, 'year')
|
|
51
|
+
].join('') if date.open_range?
|
|
52
|
+
|
|
53
|
+
names = parts.map(&:name)
|
|
54
|
+
largest = DATE_PARTS.detect do |name|
|
|
55
|
+
names.include?(name) && from[name] != to[name]
|
|
31
56
|
end
|
|
32
57
|
|
|
58
|
+
return render_date_parts(from, parts, delimiter) if largest.nil?
|
|
59
|
+
|
|
60
|
+
# If one date is more precise than the other, all parts differ
|
|
61
|
+
differing = names.any? { |name| from[name].nil? != to[name].nil? } ?
|
|
62
|
+
DATE_PARTS :
|
|
63
|
+
DATE_PARTS.drop_while { |name| name != largest }
|
|
64
|
+
|
|
65
|
+
before, range, after = split_date_parts(parts, differing) || [[], parts, []]
|
|
66
|
+
|
|
67
|
+
[
|
|
68
|
+
render_date_parts(from, before, delimiter),
|
|
69
|
+
[
|
|
70
|
+
render_date_parts(from, without_suffix(range), delimiter),
|
|
71
|
+
render_date_parts(to, range, delimiter)
|
|
72
|
+
].join(range_delimiter_for(parts, largest)),
|
|
73
|
+
render_date_parts(from, after, delimiter)
|
|
74
|
+
].reject(&:empty?).join(delimiter)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render_date_parts(date, parts, delimiter)
|
|
33
78
|
parts.map { |part|
|
|
34
79
|
render date, part
|
|
35
80
|
}.reject(&:empty?).join(delimiter)
|
|
36
81
|
end
|
|
37
82
|
|
|
38
|
-
# @param date [CiteProc::Date]
|
|
83
|
+
# @param date [CiteProc::Date, CiteProc::Date::DateParts]
|
|
39
84
|
# @param node [CSL::Style::DatePart, CSL::Locale::DatePart]
|
|
40
85
|
# @return [String]
|
|
41
86
|
def render_date_part(date, node)
|
|
@@ -48,7 +93,8 @@ module CiteProc
|
|
|
48
93
|
if date.day > 1 && locale.limit_day_ordinals?
|
|
49
94
|
date.day.to_s
|
|
50
95
|
else
|
|
51
|
-
|
|
96
|
+
# The ordinal uses the gender of the month
|
|
97
|
+
ordinalize date.day, :noun => date.month && 'month-%02d' % date.month
|
|
52
98
|
end
|
|
53
99
|
when node.form == 'numeric-leading-zeros'
|
|
54
100
|
'%02d' % date.day
|
|
@@ -58,6 +104,8 @@ module CiteProc
|
|
|
58
104
|
|
|
59
105
|
when node.month?
|
|
60
106
|
case
|
|
107
|
+
when date.season.is_a?(String)
|
|
108
|
+
date.season
|
|
61
109
|
when date.season?
|
|
62
110
|
translate(('season-%02d' % date.season), node.attributes_for(:form))
|
|
63
111
|
when date.month.nil?
|
|
@@ -91,6 +139,42 @@ module CiteProc
|
|
|
91
139
|
end
|
|
92
140
|
end
|
|
93
141
|
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
# @return [String] the range delimiter set on the date part
|
|
145
|
+
# with the given name; defaults to an en-dash
|
|
146
|
+
def range_delimiter_for(parts, name)
|
|
147
|
+
part = parts.detect { |p| p.name == name }
|
|
148
|
+
part && part[:'range-delimiter'] || '–'
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Splits the date parts into the common parts before, the
|
|
152
|
+
# differing parts, and the common parts after them.
|
|
153
|
+
#
|
|
154
|
+
# @return [Array<Array>, nil] the split parts or nil if the
|
|
155
|
+
# differing parts are not adjacent
|
|
156
|
+
def split_date_parts(parts, differing)
|
|
157
|
+
first = parts.index { |part| differing.include?(part.name) }
|
|
158
|
+
last = parts.rindex { |part| differing.include?(part.name) }
|
|
159
|
+
range = parts[first..last]
|
|
160
|
+
|
|
161
|
+
return unless range.all? { |part| differing.include?(part.name) }
|
|
162
|
+
|
|
163
|
+
[parts[0...first], range, parts[(last + 1)..]]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @return [Array<CSL::Node>] the date parts with a copy
|
|
167
|
+
# of the last date part without its suffix
|
|
168
|
+
def without_suffix(parts)
|
|
169
|
+
last = parts[-1]
|
|
170
|
+
return parts unless last.attribute?(:suffix)
|
|
171
|
+
|
|
172
|
+
copy = last.deep_copy
|
|
173
|
+
copy[:suffix] = nil
|
|
174
|
+
|
|
175
|
+
[*parts[0...-1], copy]
|
|
176
|
+
end
|
|
177
|
+
|
|
94
178
|
end
|
|
95
179
|
|
|
96
180
|
end
|
|
@@ -16,7 +16,16 @@ module CiteProc
|
|
|
16
16
|
# Applies the current format on the string using the
|
|
17
17
|
# node's formatting options.
|
|
18
18
|
def format!(string, node)
|
|
19
|
-
format.apply(string, node, locale)
|
|
19
|
+
format.apply(string, node, locale, state.language)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Reads the item's language without notifying observers.
|
|
23
|
+
#
|
|
24
|
+
# @param item [CiteProc::Item, nil]
|
|
25
|
+
# @return [String, nil] the item's language
|
|
26
|
+
def language_of(item)
|
|
27
|
+
return unless item.respond_to?(:unobservable_read_attribute)
|
|
28
|
+
item.unobservable_read_attribute(:language).to_s
|
|
20
29
|
end
|
|
21
30
|
|
|
22
31
|
def join(list, delimiter = nil)
|
|
@@ -42,93 +51,19 @@ module CiteProc
|
|
|
42
51
|
end
|
|
43
52
|
|
|
44
53
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# the
|
|
49
|
-
# * "expanded": Abbreviated page ranges are expanded to
|
|
50
|
-
# their non-abbreviated form: 42-45, 321-328, 2787-2816.
|
|
51
|
-
# * "minimal": All digits repeated in the second number
|
|
52
|
-
# are left out: 42-45, 321-8, 2787-816.
|
|
53
|
-
#
|
|
54
|
-
# @param [String] pages to be formatted
|
|
55
|
-
# @param [String] format to use for formatting
|
|
54
|
+
# @param pages [String] the pages to format
|
|
55
|
+
# @param format [String, nil] the page range format
|
|
56
|
+
# @return [String, nil] the pages formatted according to format
|
|
57
|
+
# using the localized range delimiter
|
|
56
58
|
def format_page_range(pages, format)
|
|
57
|
-
|
|
58
|
-
format_page_range!(pages.dup, format)
|
|
59
|
+
CiteProc::Number.format_page_range(pages, format, range_delimiter)
|
|
59
60
|
end
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
dash = translate('page-range-delimiter') || '–' # en-dash
|
|
66
|
-
|
|
67
|
-
pages.gsub! PAGE_RANGE_PATTERN do
|
|
68
|
-
affixes, f, t = [$1, $3, $4, $6], $2, $5
|
|
69
|
-
|
|
70
|
-
# When there are affixes or no format was
|
|
71
|
-
# specified we skip this part. As a result,
|
|
72
|
-
# only the delimiter will be replaced!
|
|
73
|
-
|
|
74
|
-
if affixes.all?(&:empty?) && !format.nil?
|
|
75
|
-
|
|
76
|
-
dim = f.length
|
|
77
|
-
delta = dim - t.length
|
|
78
|
-
|
|
79
|
-
if delta >= 0
|
|
80
|
-
t.prepend f[0, delta] unless delta.zero?
|
|
81
|
-
|
|
82
|
-
# TODO
|
|
83
|
-
if format == 'chicago-16' || format == 'chicago-15'
|
|
84
|
-
format = 'chicago'
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
if format == 'chicago'
|
|
88
|
-
changes = dim - f.chars.zip(t.chars).
|
|
89
|
-
take_while { |a,b| a == b }.length if dim == 4
|
|
90
|
-
|
|
91
|
-
format = case
|
|
92
|
-
when dim < 3
|
|
93
|
-
'expanded'
|
|
94
|
-
when dim == 4 && changes > 2
|
|
95
|
-
'expanded'
|
|
96
|
-
when f[-2, 2] == '00'
|
|
97
|
-
'expanded'
|
|
98
|
-
when f[-2] == '0'
|
|
99
|
-
'minimal'
|
|
100
|
-
else
|
|
101
|
-
'minimal-two'
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
case format
|
|
106
|
-
when 'expanded'
|
|
107
|
-
# nothing to do
|
|
108
|
-
when 'minimal'
|
|
109
|
-
t = t.each_char.drop_while.with_index { |c, i| c == f[i] }.join('')
|
|
110
|
-
when 'minimal-two'
|
|
111
|
-
if dim > 2
|
|
112
|
-
t = t.each_char.drop_while.with_index { |c, i|
|
|
113
|
-
c == f[i] && dim - i > 2
|
|
114
|
-
}.join('')
|
|
115
|
-
end
|
|
116
|
-
else
|
|
117
|
-
raise ArgumentError, "unknown page range format: #{format}"
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
affixes.zip([f, dash, t]).flatten.compact.join('')
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
pages
|
|
62
|
+
# @return [String] the localized range delimiter; defaults to an en-dash
|
|
63
|
+
def range_delimiter
|
|
64
|
+
translate('page-range-delimiter') || '–'
|
|
126
65
|
end
|
|
127
66
|
|
|
128
|
-
PAGE_RANGE_PATTERN =
|
|
129
|
-
# ------------ -2- ------------ ------------ -5- ------------
|
|
130
|
-
/\b([[:alpha:]]*)(\d+)([[:alpha:]]*)\s*[–-]+\s*([[:alpha:]]*)(\d+)([[:alpha:]]*)\b/
|
|
131
|
-
|
|
132
67
|
end
|
|
133
68
|
|
|
134
69
|
end
|
|
@@ -7,24 +7,36 @@ module CiteProc
|
|
|
7
7
|
# @param node [CSL::Style::Group]
|
|
8
8
|
# @return [String]
|
|
9
9
|
def render_group(item, node)
|
|
10
|
-
|
|
10
|
+
render_as_group item, node, node.delimiter
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
11
14
|
|
|
15
|
+
# Renders the node's children with group suppression:
|
|
16
|
+
# the result is empty if the children call variables
|
|
17
|
+
# and ALL of them are empty; a non-empty result counts
|
|
18
|
+
# as a non-empty variable for any enclosing groups.
|
|
19
|
+
#
|
|
20
|
+
# @param item [CiteProc::CitationItem]
|
|
21
|
+
# @param node [CSL::Node]
|
|
22
|
+
# @param delimiter [String, nil]
|
|
23
|
+
# @return [String]
|
|
24
|
+
def render_as_group(item, node, delimiter = nil)
|
|
12
25
|
observer = ItemObserver.new(item.data)
|
|
13
26
|
observer.start
|
|
14
27
|
|
|
15
28
|
begin
|
|
16
|
-
rendition = node.each_child.map { |child|
|
|
29
|
+
rendition = join(node.each_child.map { |child|
|
|
17
30
|
render item, child
|
|
18
|
-
}.reject(&:empty?)
|
|
19
|
-
|
|
20
|
-
rendition = join(rendition, node.delimiter)
|
|
21
|
-
|
|
31
|
+
}.reject(&:empty?), delimiter)
|
|
22
32
|
ensure
|
|
23
33
|
observer.stop
|
|
24
34
|
end
|
|
25
35
|
|
|
26
36
|
return '' if observer.skip?
|
|
27
37
|
|
|
38
|
+
item.data.simulate_read_attribute :group, rendition unless rendition.empty?
|
|
39
|
+
|
|
28
40
|
rendition
|
|
29
41
|
end
|
|
30
42
|
|
|
@@ -15,8 +15,6 @@ module CiteProc
|
|
|
15
15
|
when node.page?
|
|
16
16
|
value, name = item.read_attribute(:page) || item.data.unobservable_read_attribute(:page).to_s, :page
|
|
17
17
|
|
|
18
|
-
format_page_range!(value, node.page_range_format)
|
|
19
|
-
|
|
20
18
|
when node.locator?
|
|
21
19
|
|
|
22
20
|
# Subtle: when there is no locator we also look
|
|
@@ -52,6 +50,8 @@ module CiteProc
|
|
|
52
50
|
false
|
|
53
51
|
when node.number_of_pages?, node.number_of_volumes?
|
|
54
52
|
value.to_i > 1
|
|
53
|
+
when node.locator?
|
|
54
|
+
item.plural_locator?(locator_abbreviations)
|
|
55
55
|
when value.respond_to?(:plural?)
|
|
56
56
|
value.plural?
|
|
57
57
|
else
|
|
@@ -5,13 +5,16 @@ module CiteProc
|
|
|
5
5
|
|
|
6
6
|
private
|
|
7
7
|
|
|
8
|
+
# Note that the layout's delimiter is used only between
|
|
9
|
+
# cites in a citation, not between the layout's children.
|
|
10
|
+
#
|
|
8
11
|
# @param item [CiteProc::CitationItem]
|
|
9
12
|
# @param node [CSL::Style::Layout]
|
|
10
13
|
# @return [String]
|
|
11
14
|
def render_layout(item, node)
|
|
12
15
|
join node.each_child.map { |child|
|
|
13
16
|
render item, child
|
|
14
|
-
}.reject(&:empty?)
|
|
17
|
+
}.reject(&:empty?)
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
end
|
|
@@ -3,12 +3,16 @@ module CiteProc
|
|
|
3
3
|
|
|
4
4
|
class Renderer
|
|
5
5
|
|
|
6
|
+
# @return [CSL::Locale] the locale including the in-style locale
|
|
7
|
+
# definitions of the style currently being rendered
|
|
6
8
|
def locale
|
|
7
|
-
|
|
9
|
+
localize(base_locale)
|
|
8
10
|
end
|
|
9
11
|
|
|
12
|
+
# @param locale [CSL::Locale, String] a locale or an IETF tag;
|
|
13
|
+
# locales are used as is and never changed by the renderer
|
|
10
14
|
def locale=(locale)
|
|
11
|
-
@locale = CSL::Locale.load(locale)
|
|
15
|
+
@locale = locale.is_a?(CSL::Locale) ? locale : CSL::Locale.load(locale)
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
def translate(name, options = {})
|
|
@@ -20,6 +24,63 @@ module CiteProc
|
|
|
20
24
|
locale.ordinalize(number, options)
|
|
21
25
|
end
|
|
22
26
|
|
|
27
|
+
# @return [Hash<String,String>] the abbreviations of locator labels
|
|
28
|
+
# including the localized short terms
|
|
29
|
+
def locator_abbreviations
|
|
30
|
+
@locator_abbreviations ||= {}.compare_by_identity
|
|
31
|
+
@locator_abbreviations[locale] ||= begin
|
|
32
|
+
terms = {}
|
|
33
|
+
|
|
34
|
+
CiteProc::CitationItem.labels.each do |label|
|
|
35
|
+
[false, true].each do |plural|
|
|
36
|
+
term = translate(label, :form => 'short', :plural => plural)
|
|
37
|
+
terms[term] = label.to_s unless term.to_s.empty?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
CiteProc::CitationItem.locator_abbreviations.merge(terms)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [String] the symbol form of the "and" term
|
|
46
|
+
# if the locale defines it; otherwise "&"
|
|
47
|
+
def ampersand
|
|
48
|
+
term = locale.terms.lookup('and', :form => 'symbol')
|
|
49
|
+
term&.form == 'symbol' ? term.to_s : '&'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def clear_locale_cache!
|
|
55
|
+
@localized, @locales, @locator_abbreviations = nil, nil, nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [CSL::Locale] the locale without in-style locale
|
|
59
|
+
# definitions; defaults to the default locale
|
|
60
|
+
def base_locale
|
|
61
|
+
@locale ||= CSL::Locale.load
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Localized copies are cached per style and locale; the
|
|
65
|
+
# style and the locale themselves are never changed.
|
|
66
|
+
#
|
|
67
|
+
# @param locale [CSL::Locale]
|
|
68
|
+
# @return [CSL::Locale] the locale localized for the current style
|
|
69
|
+
def localize(locale)
|
|
70
|
+
return locale if style.nil?
|
|
71
|
+
|
|
72
|
+
@localized ||= {}.compare_by_identity
|
|
73
|
+
localized = (@localized[style] ||= {}.compare_by_identity)
|
|
74
|
+
localized[locale] ||= style.localize(locale)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @param language [String] an IETF tag
|
|
78
|
+
# @return [CSL::Locale] the (cached) locale for the language
|
|
79
|
+
def locale_for(language)
|
|
80
|
+
@locales ||= {}
|
|
81
|
+
@locales[language] ||= CSL::Locale.load(language)
|
|
82
|
+
end
|
|
83
|
+
|
|
23
84
|
end
|
|
24
85
|
|
|
25
86
|
end
|
|
@@ -6,12 +6,10 @@ module CiteProc
|
|
|
6
6
|
private
|
|
7
7
|
|
|
8
8
|
# @param item [CiteProc::CitationItem]
|
|
9
|
-
# @param node [CSL::Style::
|
|
9
|
+
# @param node [CSL::Style::Macro]
|
|
10
10
|
# @return [String]
|
|
11
11
|
def render_macro(item, node)
|
|
12
|
-
|
|
13
|
-
render item, child
|
|
14
|
-
}.join('')
|
|
12
|
+
render_as_group item, node
|
|
15
13
|
end
|
|
16
14
|
|
|
17
15
|
end
|
|
@@ -25,7 +25,8 @@ module CiteProc
|
|
|
25
25
|
|
|
26
26
|
rendered_names = render_substitute item, node.substitute
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
# Substituted output is replaced as a whole for all rules
|
|
29
|
+
if substitute_subsequent_authors? &&
|
|
29
30
|
completely_substitute?(rendered_names)
|
|
30
31
|
|
|
31
32
|
rendered_names = state.node.subsequent_author_substitute
|
|
@@ -146,8 +147,11 @@ module CiteProc
|
|
|
146
147
|
connector = node.connector
|
|
147
148
|
connector = translate('and') if connector == 'text'
|
|
148
149
|
|
|
149
|
-
# Add spaces around connector
|
|
150
|
-
|
|
150
|
+
# Add spaces around connector unless it belongs to a
|
|
151
|
+
# script which does not separate words by spaces
|
|
152
|
+
if CiteProc.romanesque_start?(connector)
|
|
153
|
+
connector = " #{connector} "
|
|
154
|
+
end
|
|
151
155
|
|
|
152
156
|
rendered_names =
|
|
153
157
|
case
|
|
@@ -171,12 +175,14 @@ module CiteProc
|
|
|
171
175
|
], node.ellipsis
|
|
172
176
|
|
|
173
177
|
else
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
translate('et-al')
|
|
178
|
+
term = translate(node.et_al ? node.et_al[:term] : 'et-al')
|
|
179
|
+
others = node.et_al ? format!(term, node.et_al) : term
|
|
177
180
|
|
|
178
|
-
connector =
|
|
179
|
-
|
|
181
|
+
connector = case
|
|
182
|
+
when node.delimiter_precedes_et_al?(truncated) then delimiter
|
|
183
|
+
when CiteProc.romanesque_start?(term) then ' '
|
|
184
|
+
else ''
|
|
185
|
+
end
|
|
180
186
|
|
|
181
187
|
join [
|
|
182
188
|
join(truncated.map.with_index { |name, idx|
|
|
@@ -185,7 +191,7 @@ module CiteProc
|
|
|
185
191
|
|
|
186
192
|
others
|
|
187
193
|
|
|
188
|
-
], connector
|
|
194
|
+
].reject { |part| part.to_s.empty? }, connector
|
|
189
195
|
|
|
190
196
|
end
|
|
191
197
|
|
|
@@ -220,7 +226,7 @@ module CiteProc
|
|
|
220
226
|
rendered_names = state.node.subsequent_author_substitute
|
|
221
227
|
end
|
|
222
228
|
|
|
223
|
-
|
|
229
|
+
affix rendered_names, node
|
|
224
230
|
end
|
|
225
231
|
|
|
226
232
|
# @param names [CiteProc::Name]
|
|
@@ -231,78 +237,35 @@ module CiteProc
|
|
|
231
237
|
if name.personal?
|
|
232
238
|
name = name.dup
|
|
233
239
|
|
|
234
|
-
# TODO move parts of the formatting logic here
|
|
235
|
-
# because name parts may include particles etc.
|
|
236
|
-
|
|
237
|
-
|
|
238
240
|
# Strip away some unusual characters to normalize
|
|
239
241
|
# sort order for names.
|
|
240
242
|
if sort_mode?
|
|
241
|
-
name.family = name.family.to_s.gsub(/[\[\]]
|
|
243
|
+
name.family = name.family.to_s.gsub(/[\[\]]|^[^[:word:]]+/, '')
|
|
242
244
|
end
|
|
243
245
|
|
|
244
246
|
name.options.merge! node.name_options
|
|
245
247
|
name.sort_order! node.name_as_sort_order_at?(position)
|
|
246
248
|
|
|
247
|
-
name.initialize_without_hyphen! if
|
|
249
|
+
name.initialize_without_hyphen! if style && style.initialize_without_hyphen?
|
|
248
250
|
|
|
249
251
|
if style && style.demote_particle?
|
|
250
252
|
name.options[:'demote-non-dropping-particle'] = style.demote_particle
|
|
251
253
|
end
|
|
252
254
|
|
|
253
|
-
#
|
|
254
|
-
if sort_mode? && name.
|
|
255
|
-
name.family = name.family.to_s.sub(/^[[:lower:]]+[\s-]/, '')
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
node.name_part.each do |part|
|
|
259
|
-
case part[:name]
|
|
260
|
-
when 'family'
|
|
261
|
-
if !name.particle? || name.demote_particle?
|
|
262
|
-
name.family = format!(name.family, part)
|
|
263
|
-
else
|
|
264
|
-
name.family = format!("#{name.particle} #{name.family}", part)
|
|
265
|
-
name.particle = nil
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
# Name suffix must be enclosed by family-part
|
|
269
|
-
# suffix in display order!
|
|
270
|
-
if name.has_suffix? && !name.sort_order? && part.attribute?(:suffix)
|
|
271
|
-
comma = name.comma_suffix? ? name.comma : ' '
|
|
272
|
-
suffix = part[:suffix]
|
|
273
|
-
|
|
274
|
-
name.family.chomp! suffix
|
|
275
|
-
name.family.concat "#{comma}#{name.suffix}#{suffix}"
|
|
276
|
-
name.suffix = nil
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
when 'given'
|
|
280
|
-
if name.dropping_particle?
|
|
281
|
-
name.given = format!("#{name.initials} #{name.dropping_particle}", part)
|
|
282
|
-
name.dropping_particle = nil
|
|
283
|
-
else
|
|
284
|
-
name.given = format!(name.initials, part)
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Demoted particles must be enclosed by
|
|
288
|
-
# given-part affixes in sort order!
|
|
289
|
-
if name.particle? && name.demote_particle? &&
|
|
290
|
-
name.sort_order? && part.attribute?(:suffix)
|
|
255
|
+
# Particles are demoted in sort keys unless they are never demoted
|
|
256
|
+
name.always_demote_particle! if sort_mode? && !name.never_demote_particle?
|
|
291
257
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
name.given.chomp! suffix
|
|
295
|
-
name.given.concat " #{name.particle}#{suffix}"
|
|
258
|
+
end
|
|
296
259
|
|
|
297
|
-
|
|
298
|
-
end
|
|
260
|
+
parts = node.name_part.to_h { |part| [part[:name].to_sym, part] }
|
|
299
261
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
262
|
+
formatted = name.format(
|
|
263
|
+
->(part, text) { format_without_affixes(text, parts[part]) },
|
|
264
|
+
->(part, text) { affix(text, parts[part]) }
|
|
265
|
+
)
|
|
304
266
|
|
|
305
|
-
|
|
267
|
+
# The affixes of the name node enclose the list of names
|
|
268
|
+
format_without_affixes formatted, node
|
|
306
269
|
end
|
|
307
270
|
|
|
308
271
|
# @param item [CiteProc::CitationItem]
|
|
@@ -381,6 +344,23 @@ module CiteProc
|
|
|
381
344
|
name
|
|
382
345
|
end
|
|
383
346
|
|
|
347
|
+
# @return [String, nil] the text with the formatting, but
|
|
348
|
+
# without the affixes, of the node
|
|
349
|
+
def format_without_affixes(text, node)
|
|
350
|
+
return text if node.nil? || text.to_s.empty?
|
|
351
|
+
|
|
352
|
+
formatting = node.deep_copy
|
|
353
|
+
formatting[:prefix] = formatting[:suffix] = nil
|
|
354
|
+
|
|
355
|
+
format! text, formatting
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# @return [String, nil] the text enclosed by the affixes of the node
|
|
359
|
+
def affix(text, node)
|
|
360
|
+
return text if node.nil? || text.to_s.empty?
|
|
361
|
+
"#{node[:prefix]}#{text}#{node[:suffix]}"
|
|
362
|
+
end
|
|
363
|
+
|
|
384
364
|
def resolve_editor_translator_exception!(names)
|
|
385
365
|
|
|
386
366
|
i = names.index { |role, _| role == :translator }
|
|
@@ -391,6 +371,9 @@ module CiteProc
|
|
|
391
371
|
|
|
392
372
|
return names unless names[i][1] == names[j][1]
|
|
393
373
|
|
|
374
|
+
# An empty term disables the special case
|
|
375
|
+
return names if translate('editortranslator').to_s.empty?
|
|
376
|
+
|
|
394
377
|
# rename the first instance and drop the second one
|
|
395
378
|
i, j = j, i if j < i
|
|
396
379
|
|
|
@@ -16,8 +16,7 @@ module CiteProc
|
|
|
16
16
|
|
|
17
17
|
case
|
|
18
18
|
when node.ordinal? || node.long_ordinal?
|
|
19
|
-
options = node.attributes_for
|
|
20
|
-
# TODO lookup term of variable to check gender
|
|
19
|
+
options = node.attributes_for(:form).merge(:noun => node.variable)
|
|
21
20
|
|
|
22
21
|
numbers.map! do |num|
|
|
23
22
|
num =~ /^\d+$/ ? ordinalize(num, options) : num
|
|
@@ -32,6 +31,7 @@ module CiteProc
|
|
|
32
31
|
# nothing
|
|
33
32
|
end
|
|
34
33
|
|
|
34
|
+
numbers.map! { |num| num == '-' ? range_delimiter : num }
|
|
35
35
|
numbers.join('')
|
|
36
36
|
end
|
|
37
37
|
|