anystyle 1.0.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.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.md +78 -0
  3. data/LICENSE +27 -0
  4. data/README.md +103 -0
  5. data/lib/anystyle.rb +71 -0
  6. data/lib/anystyle/dictionary.rb +132 -0
  7. data/lib/anystyle/dictionary/gdbm.rb +52 -0
  8. data/lib/anystyle/dictionary/lmdb.rb +67 -0
  9. data/lib/anystyle/dictionary/marshal.rb +27 -0
  10. data/lib/anystyle/dictionary/redis.rb +55 -0
  11. data/lib/anystyle/document.rb +264 -0
  12. data/lib/anystyle/errors.rb +14 -0
  13. data/lib/anystyle/feature.rb +27 -0
  14. data/lib/anystyle/feature/affix.rb +43 -0
  15. data/lib/anystyle/feature/brackets.rb +32 -0
  16. data/lib/anystyle/feature/canonical.rb +13 -0
  17. data/lib/anystyle/feature/caps.rb +20 -0
  18. data/lib/anystyle/feature/category.rb +70 -0
  19. data/lib/anystyle/feature/dictionary.rb +16 -0
  20. data/lib/anystyle/feature/indent.rb +16 -0
  21. data/lib/anystyle/feature/keyword.rb +52 -0
  22. data/lib/anystyle/feature/line.rb +39 -0
  23. data/lib/anystyle/feature/locator.rb +18 -0
  24. data/lib/anystyle/feature/number.rb +39 -0
  25. data/lib/anystyle/feature/position.rb +28 -0
  26. data/lib/anystyle/feature/punctuation.rb +22 -0
  27. data/lib/anystyle/feature/quotes.rb +20 -0
  28. data/lib/anystyle/feature/ref.rb +21 -0
  29. data/lib/anystyle/feature/terminal.rb +19 -0
  30. data/lib/anystyle/feature/words.rb +74 -0
  31. data/lib/anystyle/finder.rb +94 -0
  32. data/lib/anystyle/format/bibtex.rb +63 -0
  33. data/lib/anystyle/format/csl.rb +28 -0
  34. data/lib/anystyle/normalizer.rb +65 -0
  35. data/lib/anystyle/normalizer/brackets.rb +13 -0
  36. data/lib/anystyle/normalizer/container.rb +13 -0
  37. data/lib/anystyle/normalizer/date.rb +109 -0
  38. data/lib/anystyle/normalizer/edition.rb +16 -0
  39. data/lib/anystyle/normalizer/journal.rb +14 -0
  40. data/lib/anystyle/normalizer/locale.rb +30 -0
  41. data/lib/anystyle/normalizer/location.rb +24 -0
  42. data/lib/anystyle/normalizer/locator.rb +22 -0
  43. data/lib/anystyle/normalizer/names.rb +88 -0
  44. data/lib/anystyle/normalizer/page.rb +29 -0
  45. data/lib/anystyle/normalizer/publisher.rb +18 -0
  46. data/lib/anystyle/normalizer/pubmed.rb +18 -0
  47. data/lib/anystyle/normalizer/punctuation.rb +23 -0
  48. data/lib/anystyle/normalizer/quotes.rb +14 -0
  49. data/lib/anystyle/normalizer/type.rb +54 -0
  50. data/lib/anystyle/normalizer/volume.rb +26 -0
  51. data/lib/anystyle/parser.rb +199 -0
  52. data/lib/anystyle/support.rb +4 -0
  53. data/lib/anystyle/support/finder.mod +3234 -0
  54. data/lib/anystyle/support/finder.txt +75 -0
  55. data/lib/anystyle/support/parser.mod +15025 -0
  56. data/lib/anystyle/support/parser.txt +75 -0
  57. data/lib/anystyle/utils.rb +70 -0
  58. data/lib/anystyle/version.rb +3 -0
  59. data/res/finder/bb132pr2055.ttx +6803 -0
  60. data/res/finder/bb550sh8053.ttx +18660 -0
  61. data/res/finder/bb599nz4341.ttx +2957 -0
  62. data/res/finder/bb725rt6501.ttx +15276 -0
  63. data/res/finder/bc605xz1554.ttx +18815 -0
  64. data/res/finder/bd040gx5718.ttx +4271 -0
  65. data/res/finder/bd413nt2715.ttx +4956 -0
  66. data/res/finder/bd466fq0394.ttx +6100 -0
  67. data/res/finder/bf668vw2021.ttx +3578 -0
  68. data/res/finder/bg495cx0468.ttx +7267 -0
  69. data/res/finder/bg599vt3743.ttx +6752 -0
  70. data/res/finder/bg608dx2253.ttx +4094 -0
  71. data/res/finder/bh410qk3771.ttx +8785 -0
  72. data/res/finder/bh989ww6442.ttx +17204 -0
  73. data/res/finder/bj581pc8202.ttx +2719 -0
  74. data/res/parser/bad.xml +5199 -0
  75. data/res/parser/core.xml +7924 -0
  76. data/res/parser/gold.xml +2707 -0
  77. data/res/parser/good.xml +34281 -0
  78. data/res/parser/stanford-books.xml +2280 -0
  79. data/res/parser/stanford-diss.xml +726 -0
  80. data/res/parser/stanford-theses.xml +4684 -0
  81. data/res/parser/ugly.xml +33246 -0
  82. metadata +195 -0
@@ -0,0 +1,16 @@
1
+ module AnyStyle
2
+ class Normalizer
3
+ class Edition < Normalizer
4
+ @keys = [:edition]
5
+
6
+ def normalize(item, **opts)
7
+ map_values(item) do |_, value|
8
+ value
9
+ .gsub(/rev\./, 'revised')
10
+ .gsub(/([eé]d(\.|ition)?|ausg(\.|abe)?)$/i, '')
11
+ .strip
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ module AnyStyle
2
+ class Normalizer
3
+ class Journal < Normalizer
4
+ def normalize(item, **opts)
5
+ if item.key?(:journal)
6
+ item[:type] = 'article-journal'
7
+ item[:journal].each { |journal| append item, :'container-title', journal }
8
+ item.delete(:journal)
9
+ end
10
+ item
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ module AnyStyle
2
+ maybe_require 'language_detector'
3
+
4
+ class Normalizer
5
+ class Locale < Normalizer
6
+ def initialize
7
+ @ld = LanguageDetector.new if defined?(LanguageDetector)
8
+ end
9
+
10
+ def normalize(item, **opts)
11
+ return item if @ld.nil? || item.key?(:language)
12
+
13
+ sample = item.values_at(
14
+ :title,
15
+ :'container-title',
16
+ # :'collection-title',
17
+ :location,
18
+ :journal,
19
+ :publisher
20
+ # :note
21
+ ).flatten.compact.join(' ')
22
+
23
+ return item if sample.empty?
24
+
25
+ item[:language] = @ld.detect(sample)
26
+ item
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ module AnyStyle
2
+ class Normalizer
3
+ class Location < Normalizer
4
+ @keys = [:location]
5
+
6
+ def normalize(item, **opts)
7
+ map_values(item) do |_, value|
8
+ location = strip value
9
+
10
+ if !item.key?(:publisher) && location.include?(':')
11
+ location, publisher = location.split(/\s*:\s*/)
12
+ item[:publisher] = publisher
13
+ end
14
+
15
+ location
16
+ end
17
+ end
18
+
19
+ def strip(string)
20
+ string.gsub(/^\p{^Alnum}+|\p{^Alnum}+$/, '')
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module AnyStyle
2
+ require 'uri'
3
+
4
+ class Normalizer
5
+ class Locator < Normalizer
6
+ @keys = [:isbn, :url]
7
+
8
+ def normalize(item, **opts)
9
+ map_values(item) do |key, value|
10
+ case key
11
+ when :isbn
12
+ value[/[\d-]+/]
13
+ when :url
14
+ URI.extract(value)
15
+ else
16
+ value
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,88 @@
1
+ module AnyStyle
2
+ require 'namae'
3
+
4
+ class Normalizer
5
+ class Names < Normalizer
6
+ @keys = [
7
+ :author, :editor, :translator, :director, :producer
8
+ ]
9
+
10
+ attr_accessor :namae
11
+
12
+ def initialize(**opts)
13
+ super(**opts)
14
+
15
+ @namae = Namae::Parser.new({
16
+ prefer_comma_as_separator: true,
17
+ separator: /\A(and|AND|&|;|und|UND|y|e)\s+/,
18
+ appellation: /\A(?!x)x/,
19
+ title: /\A(?!x)x/
20
+ })
21
+ end
22
+
23
+ def normalize(item, prev: [], **opts)
24
+ map_values(item) do |key, value|
25
+ value.gsub!(/(^[\(\[]|[,;:\)\]]+$)/, '')
26
+ case
27
+ when repeater?(value) && prev.length > 0
28
+ prev[-1][key][0] || prev[-1][:author][0]
29
+ else
30
+ begin
31
+ parse(strip(value))
32
+ rescue
33
+ [{ literal: value }]
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def repeater?(value)
40
+ value =~ /^[\p{P}\s]+$/
41
+ end
42
+
43
+ def strip(value)
44
+ value
45
+ .gsub(/^[Ii]n:?\s+/, '')
46
+ .gsub(/\b[EÉeé]d(s?\.|itors?\.?|ited|iteurs?|ité)(\s+(by|par)\s+|\b|$)/, '')
47
+ .gsub(/\b([Hh](rsg|gg?)\.|Herausgeber)\s+/, '')
48
+ .gsub(/\b[Hh]erausgegeben von\s+/, '')
49
+ .gsub(/\b((d|ein)er )?[Üü]ber(s\.|setzt|setzung|tragen|tragung) v(\.|on)\s+/, '')
50
+ .gsub(/\b[Tt]rans(l?\.|lated|lation)(\s+by\b)?\s*/, '')
51
+ .gsub(/\b[Tt]rad(ucteurs?|(uit|\.)(\s+par\b)?)\s*/, '')
52
+ .gsub(/\b([Dd]ir(\.|ected))(\s+by)?\s+/, '')
53
+ .gsub(/\b([Pp]rod(\.|uce[rd]))(\s+by)?\s+/, '')
54
+ .gsub(/\b([Pp]erf(\.|orme[rd]))(\s+by)?\s+/, '')
55
+ .gsub(/\*/, '')
56
+ .gsub(/\([^\)]*\)?/, '')
57
+ .gsub(/\[[^\]]*\)?/, '')
58
+ .gsub(/[;:]/, ',')
59
+ .gsub(/^\p{^L}+|\s+\p{^L}+$/, '')
60
+ .gsub(/[\s,\.]+$/, '')
61
+ .gsub(/,{2,}/, ',')
62
+ .gsub(/\s+\./, '.')
63
+ end
64
+
65
+ def parse(value)
66
+ raise ArgumentError if value.empty?
67
+
68
+ others = value.sub!(
69
+ /(,\s+)?((\&\s+)?\bet\s+(al|coll)\b|\bu\.\s*a\b|(\band|\&)\s+others).*$/, ''
70
+ ) || value.sub!(/\.\.\.|…/, '')
71
+
72
+ # Add surname/initial punctuation separator for Vancouver-style names
73
+ # E.g. Rang HP, Dale MM, Ritter JM, Moore PK
74
+ if value.match(/^(\p{Lu}[^\s,.]+)\s+([\p{Lu}][\p{Lu}\-]{0,3})(,|[.]?$)/)
75
+ value.gsub!(/\b(\p{Lu}[^\s,.]+)\s+([\p{Lu}][\p{Lu}\-]{0,3})(,|[.]?$)/, '\1, \2\3')
76
+ end
77
+
78
+ names = namae.parse!(value).map { |name|
79
+ name.normalize_initials
80
+ name.to_h.reject { |_, v| v.nil? }
81
+ }
82
+
83
+ names << { others: true } unless others.nil?
84
+ names
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,29 @@
1
+ module AnyStyle
2
+ class Normalizer
3
+ class Page < Normalizer
4
+ @keys = [:pages]
5
+
6
+ def normalize(item, **opts)
7
+ map_values(item) do |_, value|
8
+ pages = case value
9
+ when /(\d+)(?:\.(\d+))?(?:\((\d{4})\))?:(\d.*)/
10
+ # "volume.issue(year):pp"
11
+ append(item, :volume, $1.to_i)
12
+ append(item, :issue, $2.to_i) unless $2.nil?
13
+ append(item, :year, $3.to_i) unless $3.nil?
14
+ $4
15
+ else
16
+ value
17
+ end
18
+
19
+ # TODO chap. 5, pp. 195-234.
20
+
21
+ pages
22
+ .gsub(/\p{Pd}+/, '–')
23
+ .gsub(/[^\d,–]+/, ' ')
24
+ .strip
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ module AnyStyle
2
+ class Normalizer
3
+ class Publisher < Normalizer
4
+ @keys = [:publisher]
5
+
6
+ def normalize(item, **opts)
7
+ replace_author(item) if item.key?(:author)
8
+ item
9
+ end
10
+
11
+ def replace_author(item)
12
+ each_value(item) do |_, value|
13
+ value.gsub!(/^Author$/, item[:author][0])
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module AnyStyle
2
+ class Normalizer
3
+ class PubMed < Normalizer
4
+ @keys = [:note]
5
+
6
+ def normalize(item, **opts)
7
+ each_value(item) do |_, value|
8
+ if (value =~ /PMID:?\s*(\d+)/)
9
+ append item, :pmid, $1
10
+ end
11
+ if (value =~ /PMC(\d+)/)
12
+ append item, :pmcid, $1
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ module AnyStyle
2
+ class Normalizer
3
+ class Punctuation < Normalizer
4
+ @keys = [
5
+ :'container-title',
6
+ :'collection-title',
7
+ :date,
8
+ :edition,
9
+ :journal,
10
+ :location,
11
+ :publisher,
12
+ :title
13
+ ]
14
+
15
+ def normalize(item, **opts)
16
+ each_value(item) do |_, value|
17
+ value.gsub!(/[\)\]\.,:;\p{Pd}\p{Z}\p{C}]+$/, '')
18
+ value.gsub!(/^[\(\[]/, '')
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ module AnyStyle
2
+ class Normalizer
3
+ class Quotes < Normalizer
4
+ QUOTES = /^[«‹»›„‚“‟‘‛”’"❛❜❟❝❞⹂〝〞〟\[]|[«‹»›„‚“‟‘‛”’"❛❜❟❝❞⹂〝〞〟\]]$/
5
+ @keys = [:title, :'citation-number', :medium]
6
+
7
+ def normalize(item, **opts)
8
+ each_value(item) do |_, value|
9
+ value.gsub! QUOTES, ''
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,54 @@
1
+ module AnyStyle
2
+ class Normalizer
3
+ class Type < Normalizer
4
+ def normalize(item, **opts)
5
+ item[:type] = classify item unless item.key?(:type)
6
+ item
7
+ end
8
+
9
+ def classify(item)
10
+ keys = item.keys
11
+
12
+ case
13
+ when keys.include?(:'container-title')
14
+ case
15
+ when keys.include?(:issue)
16
+ 'article-journal'
17
+ when item[:'container-title'].to_s =~ /proceedings|proc\.|conference|meeting|symposi(on|um)/i
18
+ 'paper-conference'
19
+ when item[:'container-title'].to_s =~ /journal|zeitschrift|quarterly|review|revue/i
20
+ 'article-journal'
21
+ else
22
+ 'chapter'
23
+ end
24
+ when keys.include?(:genre)
25
+ case item[:genre].to_s
26
+ when /ph(\.\s*)?d|diss(\.|ertation)|thesis/i
27
+ 'thesis'
28
+ when /rep(\.|ort)/i
29
+ 'report'
30
+ when /unpublished|manuscript/i
31
+ 'manuscript'
32
+ when /patent/i
33
+ 'patent'
34
+ when /personal communication/i
35
+ 'personal_communication'
36
+ when /interview/i
37
+ 'interview'
38
+ when /web|online|en ligne/
39
+ 'webpage'
40
+ end
41
+ when keys.include?(:medium)
42
+ case item[:medium].to_s
43
+ when /dvd|video|vhs|motion/i
44
+ 'motion_picture'
45
+ when /television/i
46
+ 'broadcast'
47
+ end
48
+ when keys.include?(:publisher)
49
+ 'book'
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,26 @@
1
+ module AnyStyle
2
+ class Normalizer
3
+ class Volume < Normalizer
4
+ @keys = [:volume, :pages, :date]
5
+
6
+ def normalize(item, **opts)
7
+ map_values(item, [:volume]) do |_, volume|
8
+ case volume
9
+ when /(\p{Lu}?\d+)\s?\(([^)]+)\)/
10
+ append item, :issue, $2
11
+ $1
12
+ when /(?:(\p{Lu}?\d+)[\p{P}\s]+)?(?:nos?|nr|n°|nº|iss?)\.?\s?(.+)$/i
13
+ volume = $1
14
+ append item, :issue, $2.sub(/\p{P}$/, '')
15
+ volume
16
+ else
17
+ volume
18
+ .sub(/^[\p{P}\s]+/, '')
19
+ .sub(/.*vol(ume)?[\p{P}\s]+/i, '')
20
+ .sub(/\p{P}$/, '')
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,199 @@
1
+ module AnyStyle
2
+ class ParserCore
3
+ include StringUtils
4
+
5
+ class << self
6
+ attr_reader :defaults, :formats
7
+
8
+ def load(path)
9
+ new :model => path
10
+ end
11
+
12
+ # Returns a default parser instance
13
+ def instance
14
+ Thread.current["anystyle_#{name.downcase}"] ||= new
15
+ end
16
+ end
17
+
18
+ attr_reader :model, :options, :features, :normalizers
19
+
20
+ def initialize(options = {})
21
+ @options = self.class.defaults.merge(options)
22
+ load_model
23
+ end
24
+
25
+ def load_model(file = options[:model])
26
+ unless file.nil?
27
+ @model = Wapiti.load(file)
28
+ @model.options.update_attributes options
29
+ else
30
+ @model = Wapiti::Model.new(options.reject { |k,_| k == :model })
31
+ @model.path = options[:model]
32
+ end
33
+
34
+ self
35
+ end
36
+
37
+ def label(input, **opts)
38
+ model.label prepare(input, **opts)
39
+ end
40
+
41
+ def check(input)
42
+ model.check prepare(input, tagged: true)
43
+ end
44
+
45
+ def train(input = options[:training_data], truncate: true)
46
+ load_model(nil) if truncate
47
+ unless input.nil? || input.empty?
48
+ model.train prepare(input, tagged: true)
49
+ end
50
+ model
51
+ end
52
+
53
+ def learn(input)
54
+ train(input, truncate: false)
55
+ end
56
+
57
+ def normalize(hash, **opts)
58
+ normalizers.each do |n|
59
+ begin
60
+ hash = n.normalize(hash, **opts) unless n.skip?
61
+ rescue => e
62
+ warn "Error in #{n.name} normalizer: #{e.message}"
63
+ end
64
+ end
65
+ hash
66
+ end
67
+
68
+ def expand(dataset)
69
+ raise NotImplementedError
70
+ end
71
+
72
+ def prepare(input, **opts)
73
+ case input
74
+ when Wapiti::Dataset
75
+ expand input
76
+ when Wapiti::Sequence
77
+ expand Wapiti::Dataset.new([input])
78
+ when String
79
+ if !input.tainted? && input.length < 1024 && File.exists?(input)
80
+ expand Wapiti::Dataset.open(input, opts)
81
+ else
82
+ expand Wapiti::Dataset.parse(input, opts)
83
+ end
84
+ else
85
+ expand Wapiti::Dataset.parse(input, opts)
86
+ end
87
+ end
88
+ end
89
+
90
+
91
+ class Parser < ParserCore
92
+ include Format::BibTeX
93
+ include Format::CSL
94
+
95
+ @formats = [:bibtex, :citeproc, :csl, :hash, :wapiti]
96
+
97
+ @defaults = {
98
+ model: File.join(SUPPORT, 'parser.mod'),
99
+ pattern: File.join(SUPPORT, 'parser.txt'),
100
+ compact: true,
101
+ threads: 4,
102
+ separator: /(?:\r?\n)+/,
103
+ delimiter: /\s+/,
104
+ format: :hash,
105
+ training_data: File.join(RES, 'parser', 'core.xml')
106
+ }
107
+
108
+ def initialize(options = {})
109
+ super(options)
110
+
111
+ @features = [
112
+ Feature::Canonical.new,
113
+ Feature::Category.new,
114
+ Feature::Affix.new(size: 2),
115
+ Feature::Affix.new(size: 2, suffix: true),
116
+ Feature::Caps.new,
117
+ Feature::Number.new,
118
+ Feature::Dictionary.new(dictionary: options[:dictionary] || Dictionary.instance),
119
+ Feature::Keyword.new,
120
+ Feature::Position.new,
121
+ Feature::Punctuation.new,
122
+ Feature::Brackets.new,
123
+ Feature::Terminal.new,
124
+ Feature::Locator.new
125
+ ]
126
+
127
+ @normalizers = [
128
+ Normalizer::Quotes.new,
129
+ Normalizer::Brackets.new,
130
+ Normalizer::Punctuation.new,
131
+ Normalizer::Journal.new,
132
+ Normalizer::Container.new,
133
+ Normalizer::Edition.new,
134
+ Normalizer::Volume.new,
135
+ Normalizer::Page.new,
136
+ Normalizer::Date.new,
137
+ Normalizer::Location.new,
138
+ Normalizer::Locator.new,
139
+ Normalizer::Publisher.new,
140
+ Normalizer::PubMed.new,
141
+ Normalizer::Names.new,
142
+ Normalizer::Locale.new,
143
+ Normalizer::Type.new
144
+ ]
145
+ end
146
+
147
+ def expand(dataset)
148
+ dataset.each do |seq|
149
+ seq.tokens.each_with_index do |tok, idx|
150
+ alpha = scrub tok.value
151
+ tok.observations = features.map { |f|
152
+ f.observe tok.value, alpha: alpha, idx: idx, seq: seq
153
+ }
154
+ end
155
+ end
156
+ end
157
+
158
+ def format_hash(dataset, symbolize_keys: true)
159
+ dataset.inject([]) { |out, seq|
160
+ out << normalize(seq.to_h(symbolize_keys: symbolize_keys), prev: out)
161
+ }
162
+ end
163
+
164
+ def flatten_values(hash, skip: [], spacer: ' ')
165
+ hash.each_pair do |key, value|
166
+ unless !value.is_a?(Array) || skip.include?(key)
167
+ if value.length > 1 && value[0].respond_to?(:join)
168
+ hash[key] = value.join(spacer)
169
+ else
170
+ hash[key] = value[0]
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ def rename_value(hash, name, new_name)
177
+ hash[new_name] = hash.delete name if hash.key?(name)
178
+ end
179
+
180
+ def parse(input, format: options[:format], **opts)
181
+ case format.to_sym
182
+ when :wapiti
183
+ label(input, **opts)
184
+ when :hash, :bibtex, :citeproc, :csl
185
+ formatter = "format_#{format}".to_sym
186
+ send(formatter, label(input, **opts), **opts)
187
+ else
188
+ raise ArgumentError, "format not supported: #{format}"
189
+ end
190
+ end
191
+
192
+ def prepare(input, **opts)
193
+ opts[:separator] ||= options[:separator]
194
+ opts[:delimiter] ||= options[:delimiter]
195
+ input = input.join("\n") if input.is_a?(Array) && input[0].is_a?(String)
196
+ super(input, opts)
197
+ end
198
+ end
199
+ end