citeproc-ruby 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. data/README.md +78 -0
  2. data/lib/citeproc.rb +100 -0
  3. data/lib/citeproc/bibliography.rb +57 -0
  4. data/lib/citeproc/data.rb +149 -0
  5. data/lib/citeproc/date.rb +133 -0
  6. data/lib/citeproc/formatter.rb +38 -0
  7. data/lib/citeproc/item.rb +53 -0
  8. data/lib/citeproc/name.rb +284 -0
  9. data/lib/citeproc/processor.rb +166 -0
  10. data/lib/citeproc/selector.rb +61 -0
  11. data/lib/citeproc/variable.rb +82 -0
  12. data/lib/citeproc/version.rb +3 -0
  13. data/lib/csl/locale.rb +223 -0
  14. data/lib/csl/node.rb +72 -0
  15. data/lib/csl/nodes.rb +1364 -0
  16. data/lib/csl/renderer.rb +88 -0
  17. data/lib/csl/sort.rb +53 -0
  18. data/lib/csl/style.rb +110 -0
  19. data/lib/csl/term.rb +124 -0
  20. data/lib/extensions/core.rb +43 -0
  21. data/lib/plugins/filters/bibtex.rb +12 -0
  22. data/lib/plugins/formats/default.rb +134 -0
  23. data/lib/plugins/formats/html.rb +67 -0
  24. data/lib/support/attributes.rb +99 -0
  25. data/lib/support/tree.rb +80 -0
  26. data/resource/locale/locales-af-ZA.xml +304 -0
  27. data/resource/locale/locales-ar-AR.xml +304 -0
  28. data/resource/locale/locales-bg-BG.xml +304 -0
  29. data/resource/locale/locales-ca-AD.xml +304 -0
  30. data/resource/locale/locales-cs-CZ.xml +304 -0
  31. data/resource/locale/locales-da-DK.xml +304 -0
  32. data/resource/locale/locales-de-AT.xml +304 -0
  33. data/resource/locale/locales-de-CH.xml +304 -0
  34. data/resource/locale/locales-de-DE.xml +332 -0
  35. data/resource/locale/locales-el-GR.xml +303 -0
  36. data/resource/locale/locales-en-US.xml +313 -0
  37. data/resource/locale/locales-es-ES.xml +304 -0
  38. data/resource/locale/locales-et-EE.xml +304 -0
  39. data/resource/locale/locales-fr-FR.xml +304 -0
  40. data/resource/locale/locales-he-IL.xml +304 -0
  41. data/resource/locale/locales-hu-HU.xml +304 -0
  42. data/resource/locale/locales-is-IS.xml +304 -0
  43. data/resource/locale/locales-it-IT.xml +304 -0
  44. data/resource/locale/locales-ja-JP.xml +304 -0
  45. data/resource/locale/locales-kh-KH.xml +303 -0
  46. data/resource/locale/locales-ko-KR.xml +304 -0
  47. data/resource/locale/locales-mn-MN.xml +304 -0
  48. data/resource/locale/locales-nb-NO.xml +304 -0
  49. data/resource/locale/locales-nl-NL.xml +304 -0
  50. data/resource/locale/locales-nn-NO.xml +304 -0
  51. data/resource/locale/locales-pl-PL.xml +304 -0
  52. data/resource/locale/locales-pt-BR.xml +304 -0
  53. data/resource/locale/locales-pt-PT.xml +304 -0
  54. data/resource/locale/locales-ro-RO.xml +304 -0
  55. data/resource/locale/locales-ru-RU.xml +304 -0
  56. data/resource/locale/locales-sk-SK.xml +304 -0
  57. data/resource/locale/locales-sl-SI.xml +304 -0
  58. data/resource/locale/locales-sr-RS.xml +304 -0
  59. data/resource/locale/locales-sv-SE.xml +304 -0
  60. data/resource/locale/locales-th-TH.xml +304 -0
  61. data/resource/locale/locales-tr-TR.xml +304 -0
  62. data/resource/locale/locales-uk-UA.xml +304 -0
  63. data/resource/locale/locales-vi-VN.xml +304 -0
  64. data/resource/locale/locales-zh-CN.xml +304 -0
  65. data/resource/locale/locales-zh-TW.xml +304 -0
  66. data/resource/schema/csl-categories.rnc +39 -0
  67. data/resource/schema/csl-data.rnc +98 -0
  68. data/resource/schema/csl-terms.rnc +106 -0
  69. data/resource/schema/csl-types.rnc +39 -0
  70. data/resource/schema/csl-variables.rnc +182 -0
  71. data/resource/schema/csl.rnc +941 -0
  72. data/resource/style/acta-materialia-x.csl +128 -0
  73. data/resource/style/advanced-engineering-materials-x.csl +121 -0
  74. data/resource/style/ama.csl +185 -0
  75. data/resource/style/ama2-x.csl +179 -0
  76. data/resource/style/apa-x.csl +324 -0
  77. data/resource/style/apa.csl +254 -0
  78. data/resource/style/apsa-x.csl +163 -0
  79. data/resource/style/apsa.csl +176 -0
  80. data/resource/style/asa-x.csl +203 -0
  81. data/resource/style/asa.csl +216 -0
  82. data/resource/style/asm-journals-x.csl +131 -0
  83. data/resource/style/bibtex-x2.csl +175 -0
  84. data/resource/style/bluebook-demo-x.csl +392 -0
  85. data/resource/style/bluebook-demo.csl +942 -0
  86. data/resource/style/chicago-author-date-listing.csl +434 -0
  87. data/resource/style/chicago-author-date.csl +369 -0
  88. data/resource/style/chicago-fullnote-bibliography-bb.csl +928 -0
  89. data/resource/style/chicago-fullnote-bibliography.csl +695 -0
  90. data/resource/style/chicago-note-bibliography.csl +446 -0
  91. data/resource/style/chicago-note.csl +388 -0
  92. data/resource/style/greek-chicago-x.csl +1182 -0
  93. data/resource/style/harvard1-institution-italic.csl +190 -0
  94. data/resource/style/harvard1.csl +181 -0
  95. data/resource/style/ieee.csl +129 -0
  96. data/resource/style/mhra-x.csl +312 -0
  97. data/resource/style/mhra.csl +390 -0
  98. data/resource/style/mhra_note_without_bibliography-x.csl +330 -0
  99. data/resource/style/mhra_note_without_bibliography.csl +338 -0
  100. data/resource/style/mla-x.csl +178 -0
  101. data/resource/style/mla.csl +189 -0
  102. data/resource/style/nature-x.csl +81 -0
  103. data/resource/style/nature.csl +88 -0
  104. data/resource/style/nlm.csl +117 -0
  105. data/spec/citeproc/bibliography_spec.rb +45 -0
  106. data/spec/citeproc/citeproc_spec.rb +76 -0
  107. data/spec/citeproc/date_spec.rb +85 -0
  108. data/spec/citeproc/formatter_spec.rb +101 -0
  109. data/spec/citeproc/item_spec.rb +71 -0
  110. data/spec/citeproc/name_spec.rb +30 -0
  111. data/spec/citeproc/processor_spec.rb +61 -0
  112. data/spec/citeproc/selector_spec.rb +82 -0
  113. data/spec/citeproc/variable_spec.rb +69 -0
  114. data/spec/csl/locale_spec.rb +208 -0
  115. data/spec/csl/node_spec.rb +25 -0
  116. data/spec/csl/nodes_spec.rb +140 -0
  117. data/spec/csl/style_spec.rb +62 -0
  118. data/spec/csl/term_spec.rb +56 -0
  119. data/spec/fixtures/dates.yaml +80 -0
  120. data/spec/fixtures/names.yaml +115 -0
  121. data/spec/fixtures/nodes.yaml +245 -0
  122. data/spec/spec_helper.rb +18 -0
  123. data/spec/support/attributes_spec.rb +39 -0
  124. data/spec/support/tree_spec.rb +163 -0
  125. metadata +264 -0
@@ -0,0 +1,88 @@
1
+ module CSL
2
+
3
+ # Represents a cs:citation or cs:bibliography element.
4
+ class Renderer < Node
5
+
6
+ attr_fields Nodes.inheritable_name_attributes
7
+ attr_fields %w{ delimiter-precedes-et-al }
8
+
9
+ attr_reader :layout, :style
10
+
11
+ def initialize(*args, &block)
12
+ @style = args.detect { |argument| argument.is_a?(Style) }
13
+ args.delete(@style) unless @style.nil?
14
+ @parent = @style
15
+
16
+ args.each do |argument|
17
+ case
18
+ when argument.is_a?(String) && argument.match(/^\s*</)
19
+ parse(Nokogiri::XML.parse(argument) { |config| config.strict.noblanks }.root)
20
+
21
+ when argument.is_a?(Nokogiri::XML::Node)
22
+ parse(argument)
23
+
24
+ when argument.is_a?(Hash)
25
+ merge!(argument)
26
+
27
+ else
28
+ CiteProc.log.warn "failed to initialize Renderer from argument #{ argument.inspect }" unless argument.nil?
29
+ end
30
+ end
31
+
32
+ set_defaults
33
+
34
+ yield self if block_given?
35
+ end
36
+
37
+ def sort(data, processor)
38
+ sort = find_children_by_name('sort').first
39
+ sort.nil? ? data : sort.apply(data, processor)
40
+ end
41
+
42
+ def parse(node)
43
+ @layout = Nodes.parse(node.at_css('layout'), style)
44
+ add_children(Node.parse(node.at_css('sort')))
45
+ end
46
+
47
+ def render(data, processor=nil)
48
+ # TODO add support for one-off processor instance
49
+ processor.format(process(data, processor).join(delimiter), attributes)
50
+ rescue Exception => e
51
+ CiteProc.log :error, "failed to render data #{ data.inspect }", e
52
+ end
53
+
54
+ def process(data, processor)
55
+ sort(data, processor).map do |item|
56
+ [item['prefix'], @layout.process(item, processor), item['suffix']].compact.join(' ')
57
+ end
58
+ end
59
+
60
+ protected
61
+
62
+ def set_defaults
63
+ end
64
+
65
+ end
66
+
67
+ class Bibliography < Renderer
68
+ attr_fields %w{ hanging-indent second-field-align line-spacing
69
+ entry-spacing subsequent-author-substitute }
70
+
71
+ end
72
+
73
+ class Citation < Renderer
74
+ attr_fields %w{ collapse year-suffix-delimiter after-collapse-delimiter
75
+ near-note-distance disambiguate-add-names disambiguate-add-given-name
76
+ given-name-disambiguation-rule disambiguate-add-year-suffix }
77
+
78
+ attr_fields %w{ delimiter suffix prefix }
79
+
80
+ def initialize(*arguments, &block)
81
+ super
82
+ %w{ delimiter suffix prefix }.each do |attribute|
83
+ self[attribute] = @layout.attributes.delete(attribute)
84
+ end
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,53 @@
1
+ module CSL
2
+
3
+ class Sort < Node
4
+ attr_children 'key'
5
+
6
+ alias :keys :key
7
+
8
+ def sort(items, processor)
9
+ items.sort do |a,b|
10
+ comparison = 0
11
+ keys.each do |key|
12
+ this, that = key.convert(a, processor), key.convert(b, processor)
13
+
14
+ comparison = this <=> that
15
+ comparison = comparison * -1 if comparison && key.descending?
16
+
17
+ comparison = comparison ? comparison : that.nil? ? -1 : 1
18
+
19
+ break unless comparison.zero?
20
+ end
21
+
22
+ comparison
23
+ end
24
+ end
25
+
26
+ alias :apply :sort
27
+
28
+ end
29
+
30
+ class Key < Node
31
+ attr_fields %w{ variable macro sort names-min names-use-first names-use-last }
32
+
33
+ def convert(item, processor)
34
+ case
35
+ when has_variable?
36
+ item[variable]
37
+ when has_macro?
38
+ processor.style.macros[macro].process(item, processor)
39
+ else
40
+ CiteProc.log.warn "sort key #{ inspect } contains no variable or macro definition."
41
+ item
42
+ end
43
+ end
44
+
45
+ def ascending?; !descending?; end
46
+
47
+ def descending?
48
+ has_sort? && sort == 'descending'
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,110 @@
1
+ module CSL
2
+
3
+ # class StyleOptions < Node
4
+ # attr_fields %w{ punctuation-in-quote }
5
+ # end
6
+ #
7
+ # class Info < Node
8
+ # end
9
+
10
+
11
+ class Style < Node
12
+
13
+ @schema = File.expand_path('../../resource/schema/csl.rnc', __FILE__)
14
+ @path = File.expand_path('../../../resource/style', __FILE__)
15
+ @default = 'apa'
16
+
17
+ class << self; attr_accessor :path, :schema, :default; end
18
+
19
+ def initialize(style=nil)
20
+ open(style || Style.default)
21
+ end
22
+
23
+ # @param style A CSL stream, a file, the name of Style in the local repository, or an URI
24
+ def open(style)
25
+ @attributes = {}
26
+ doc = Nokogiri::XML(locate(style)) { |config| config.strict.noblanks }
27
+
28
+ [:citation, :bibliography].each do |element|
29
+ @attributes[element] ||= CSL.const_get(element.to_s.capitalize).new(doc.at_css("style > #{element}"), self)
30
+ end
31
+
32
+ @attributes[:locales] = doc.css('style > locale').map { |locale| Locale.new(locale) }
33
+
34
+ @attributes[:info] = Hash[*doc.at_css('style > info').children.map { |node| [node.name.downcase, node.content] }.flatten]
35
+ @attributes[:options] = Hash[doc.root.attributes.values.map { |a| [a.name, a.value] }]
36
+ @attributes[:macros] = Hash[doc.css('style > macro').map { |m| [m[:name], Nodes::Macro.new(m, self)] } ]
37
+
38
+ self
39
+ end
40
+
41
+ # Returns the CSL Relax NG schema defintion.
42
+ def schema
43
+ @attributes[:schema] ||= Nokogiri::XML::RelaxNG(File.open(Style.schema))
44
+ end
45
+
46
+
47
+ # Validates the current style's source document against the CSL defintion.
48
+ def validate
49
+ [] # schema.validate(@doc)
50
+ end
51
+
52
+ # Returns true if the current style's source document conforms to the CSL definition.
53
+ def valid?
54
+ validate.empty?
55
+ end
56
+
57
+ # Updates the current style using the URI returned by #link.
58
+ def update!
59
+ open(link)
60
+ end
61
+
62
+ def options
63
+ @attributes[:options] ||= {}
64
+ end
65
+
66
+ def [](key)
67
+ options[key.to_s]
68
+ end
69
+
70
+ def info
71
+ @attributes[:info]
72
+ end
73
+
74
+ [:title, :id].each do |method_id|
75
+ define_method method_id do
76
+ @attributes[method_id] ||= info[method_id.to_s]
77
+ end
78
+ end
79
+
80
+ [:info, :macros, :citation, :bibliography].each do |method_id|
81
+ define_method method_id do; @attributes[method_id]; end
82
+ end
83
+
84
+ alias :macro macros
85
+
86
+ # @returns the style's locales.
87
+ def locales(language = nil, region = nil)
88
+ @attributes[:locales].select { |lc| lc.language.nil? || language.nil? || lc.language == language }.sort(&Locale.sort(language, region))
89
+ end
90
+
91
+ def link
92
+ @attributes[:link] ||= info.at_css('link')['href']
93
+ end
94
+
95
+
96
+ private
97
+
98
+ def locate(resource)
99
+ resource = resource.to_s
100
+ return resource if resource.match(/^\s*<(\?xml|style)/)
101
+ return File.read(resource) if File.exists?(resource)
102
+
103
+ local = File.join(Style.path, "#{resource}.csl")
104
+ return File.read(local) if File.exists?(local)
105
+
106
+ Kernel.open(resource)
107
+ end
108
+ end
109
+
110
+ end
@@ -0,0 +1,124 @@
1
+ module CSL
2
+
3
+ # == Term
4
+ #
5
+ # Terms are localized strings. For example, if a style specifies that the
6
+ # term "and" should be used, the string that appears in the style output
7
+ # depends on the locale: "and" for English, "und" for German. Terms are
8
+ # defined using cs:term elements, child elements of cs:terms, itself a child
9
+ # element of cs:locale. Terms are identified by the value of the name
10
+ # attribute of cs:term. Two types of terms exist: simple terms, where the
11
+ # content of the cs:term is the localized string, and compound terms, where
12
+ # cs:term includes the two child elements cs:single and cs:multiple, which
13
+ # respectively contain the singular and plural variant of the term (e.g.
14
+ # "page" and "pages"). Some terms are defined for multiple forms. In these
15
+ # cases, multiple cs:term element share the same value of name, but differ
16
+ # in the value of the optional form attribute. The different forms are:
17
+ #
18
+ # * "long" - the default, e.g. "editor" and "editors" for the term "editor"
19
+ # * "short" - e.g. "ed" and "eds" for the term "editor"
20
+ # * "verb" - e.g. "edited by" for the term "editor"
21
+ # * "verb-short" - e.g. "ed" for the term "editor"
22
+ # * "symbol" - e.g. "§" for the term "section"
23
+ #
24
+ # The plural attribute can be set to choose either the singular (value
25
+ # "false", the default) or plural variant (value "true") of a term. In
26
+ # addition, the form attribute can be set to select the desired term form
27
+ # ("long" [default], "short", "verb", "verb-short" or "symbol"). If for a
28
+ # given term the desired form does not exist, another form may be used:
29
+ # "verb-short" reverts to "verb", "symbol" reverts to "short", and "verb"
30
+ # and "short" both revert to "long".
31
+ #
32
+ class Term
33
+ include Support::Attributes
34
+
35
+ attr_fields %w{ name long short verb verb-short symbol gender feminine
36
+ masculine neutral }
37
+
38
+ def initialize(argument=nil, &block)
39
+ case
40
+ when argument.nil?
41
+
42
+ when argument.is_a?(Hash)
43
+ merge!(argument)
44
+
45
+ when argument.is_a?(Nokogiri::XML::Node)
46
+ parse!(argument)
47
+
48
+ when argument.is_a?(String) && argument.match(/^<term/)
49
+ parse!(Nokogiri::XML.parse(argument).root)
50
+
51
+ when argument.is_a?(String) || argument.is_a?(Symbol)
52
+ attributes['name'] = argument.to_s
53
+
54
+ else
55
+ CiteProc.log.warn "failed to create new Term from #{ argument.inspect }"
56
+
57
+ end
58
+
59
+ yield self if block_given?
60
+ end
61
+
62
+
63
+ # @returns a hash containing all the terms in the given document
64
+ def self.build(doc=nil)
65
+ terms = Hash.new { |h,k| h[k] = Term.new(k) }
66
+ doc.css('terms term').each { |term| terms[term['name']].parse!(term) } unless doc.nil?
67
+
68
+ terms
69
+ end
70
+
71
+ def parse!(node)
72
+ raise(ArgumentError, "failed to parse node; expected <term>, was: #{ node.inspect }") unless node.name == 'term'
73
+
74
+ self['name'] = node['name']
75
+ self['gender'] = node['gender']
76
+ self[node['form'] || node['gender-form'] || 'long'] = Hash[%w{ singular plural }.zip(node.children.map(&:content))]
77
+
78
+ end
79
+
80
+ def singularize(options={})
81
+ options['plural'] = 'false'
82
+ to_s(options)
83
+ end
84
+
85
+ def pluralize(options={})
86
+ options['plural'] = 'true'
87
+ to_s(options)
88
+ end
89
+
90
+ def to_s(options={})
91
+ plural = ['', 'false', '1', 'never'].include?(options['plural'].to_s) ? false : true
92
+
93
+ term = case options['form']
94
+ when 'verb-short' then verb_short || verb || long
95
+ when 'symbol' then symbol || short || long
96
+ when 'verb' then verb || long
97
+ when 'short' then short || long
98
+ else
99
+ self[options['form']] || self[options['gender-form']] || long || masculine || feminine || neutral
100
+ end || {}
101
+
102
+ plural && !term['plural'].nil? ? term['plural'].to_s : term['singular'].to_s
103
+ rescue Exception => e
104
+ CiteProc.log.error "failed to convert Term to String: #{ e.message }"
105
+ ''
106
+ end
107
+
108
+ def empty?
109
+ long.nil? && short.nil? && verb.nil? && verb_short.nil? && symbol.nil?
110
+ end
111
+
112
+ def has_gender?
113
+ !gender.nil?
114
+ end
115
+
116
+ %w{ masculine feminine neutral }.each do |gender|
117
+ define_method "#{gender}?" do
118
+ self['gender'] == gender
119
+ end
120
+ end
121
+
122
+ end
123
+
124
+ end
@@ -0,0 +1,43 @@
1
+
2
+ # ---------- Open Class ----------
3
+
4
+ module Kernel
5
+ alias :is_an? :is_a? unless defined?(is_an?)
6
+ end
7
+
8
+
9
+ # ---------- Extensions ----------
10
+
11
+ module Extensions
12
+ module Core
13
+
14
+ module Numbers
15
+ MAX_ROMAN = 4999
16
+ FACTORS = [["M", 1000], ["CM", 900], ["D", 500], ["CD", 400],
17
+ ["C", 100], ["XC", 90], ["L", 50], ["XL", 40],
18
+ ["X", 10], ["IX", 9], ["V", 5], ["IV", 4],
19
+ ["I", 1]]
20
+
21
+ # Returns roman equivalent of the integer
22
+ # This function is featured in the pickaxe book
23
+ def romanize
24
+ num = self.to_i
25
+ roman = ""
26
+ unless num < 1 || num > MAX_ROMAN
27
+ for code, factor in FACTORS
28
+ count, num = num.divmod(factor)
29
+ roman << (code * count)
30
+ end
31
+ end
32
+ roman.downcase
33
+ end
34
+
35
+ end
36
+ end
37
+ end
38
+
39
+ # ---------- Include extensions ----------
40
+
41
+ class Fixnum
42
+ include Extensions::Core::Numbers
43
+ end
@@ -0,0 +1,12 @@
1
+ CiteProc::Variable.filters[:bibtex] = (Hash.new { |h, k| k }).merge(Hash[*%w{
2
+ date issued
3
+ isbn ISBN
4
+ booktitle container-title
5
+ journal container-title
6
+ series collection-title
7
+ address publisher-place
8
+ pages page
9
+ number issue
10
+ url URL
11
+ doi DOI
12
+ }])
@@ -0,0 +1,134 @@
1
+ module CiteProc
2
+
3
+ module Format
4
+
5
+ class Default
6
+
7
+ attr_reader :input
8
+
9
+ Token = Struct.new :content, :annotations, :styles
10
+ AffixFilter = /([\.,\s!?()])/
11
+
12
+ def initialize
13
+ reset
14
+ end
15
+
16
+ def name; "CiteProc default style (plain text)"; end
17
+
18
+ def reset
19
+ @styles = {}
20
+ @tokens = []
21
+ @affixes = [nil, nil]
22
+ end
23
+
24
+ def input=(input)
25
+ reset
26
+ @tokens = input.split(/(<span[^>]*>[^<]*<\/span>)/).map do |t|
27
+ token = Token.new
28
+
29
+ if t.match(/^<span(?:\s+class=['"]([\w\s]*)["'])?>([^<]*)<\/span>$/)
30
+ token.content = $2 || ''
31
+ token.annotations = $1.split(/\s+/)
32
+ else
33
+ token = Token.new
34
+ token.content = t
35
+ token.annotations = []
36
+ end
37
+
38
+ token
39
+ end
40
+ end
41
+
42
+ def finalize
43
+ [prefix, @tokens.map(&:content).join, suffix].compact.join
44
+ end
45
+
46
+ def prefix
47
+ return nil if @affixes[0].nil?
48
+ @affixes[0].match(/([\.;:!?\s])$/) && @tokens.first.content.start_with?($1) ? @affixes[0].sub(/\.$/, '') : @affixes[0]
49
+ end
50
+
51
+ def suffix
52
+ return nil if @affixes[1].nil?
53
+ @affixes[1].match(/^([\.;:!?\s])/) && @tokens.last.content.end_with?($1) ? @affixes[1].sub(/^\./, '') : @affixes[1]
54
+ end
55
+
56
+ def set_prefix(prefix)
57
+ @affixes[0] = prefix
58
+ end
59
+
60
+ def set_suffix(suffix)
61
+ @affixes[1] = suffix
62
+ end
63
+
64
+ # @param display 'block', 'left-margin', 'right-inline', 'inline'
65
+ def set_display(display)
66
+ @styles['display'] = display || 'inline'
67
+ end
68
+
69
+ def set_strip_periods(strip)
70
+ @tokens.each { |token| token.content = token.content.gsub(/\.+/, ' ').squeeze(' ').gsub(/^\s+|\s+$/, '') } if strip == 'true'
71
+ end
72
+
73
+ # @param style 'normal', 'italic', 'oblique'
74
+ def set_font_style(style)
75
+ @styles['font-style'] = style || 'normal'
76
+ end
77
+
78
+ # @param variant 'normal', 'small-caps'
79
+ def set_font_variant(variant)
80
+ @styles['font-variant'] = variant || 'normal'
81
+ end
82
+
83
+ # @param weight 'normal', 'bold', 'light'
84
+ def set_font_weight(weight)
85
+ @styles['font-weight'] = weight || 'normal'
86
+ end
87
+
88
+ # @param decoration 'none', 'underline'
89
+ def set_text_decoration(decoration)
90
+ @styles['text-decoration'] = decoration || 'none'
91
+ end
92
+
93
+ # @param align 'baseline', 'sub', 'sup'
94
+ def set_vertical_align(align)
95
+ @styles['vertical-align'] = align || 'baseline'
96
+ end
97
+
98
+ # @param case 'lowercase', 'uppercase', 'capitalize-first', 'capitalize-all', 'title', 'sentence'
99
+ def set_text_case(text_case)
100
+
101
+ # note: the nocase annotations does not override lowercase and uppercase
102
+
103
+ case text_case
104
+ when 'lowercase'
105
+ @tokens.each { |token| token.content = UnicodeUtils ? UnicodeUtils.downcase(token.content) : token.content.downcase }
106
+
107
+ when 'uppercase'
108
+ @tokens.each { |token| token.content = UnicodeUtils ? UnicodeUtils.upcase(token.content) : token.content.upcase }
109
+
110
+ when 'capitalize-first'
111
+ token = @tokens.detect { |token| !token.annotations.include?('nocase') }
112
+ token.content.sub!(/^./) { UnicodeUtils ? UnicodeUtils.upcase($&) : $&.upcase }
113
+
114
+ when 'capitalize-all'
115
+ # @tokens.each { |token| token.content.gsub!(/\b\w/) { $&.upcase } unless token.annotations.include?('nocase') }
116
+ @tokens.each { |token| token.content = token.content.split(/(\s+)/).map(&:capitalize).join unless token.annotations.include?('nocase') }
117
+
118
+ # TODO exact specification?
119
+ when 'title'
120
+ @tokens.each { |token| token.content = token.content.split(/(\s+)/).map { |w| w.match(/^(and|of|a|an|the)$/i) ? w : w.gsub(/\b\w/) { UnicodeUtils ? UnicodeUtils.upcase($&) : $&.upcase } }.join.sub(/^(\w)/) {$&.upcase} unless token.annotations.include?('nocase') }
121
+
122
+ # TODO exact specification?
123
+ when 'sentence'
124
+ @tokens.each { |token| token.content.capitalize! unless token.annotations.include?('nocase') }
125
+
126
+ else
127
+ # nothing
128
+ end
129
+ end
130
+
131
+ end
132
+
133
+ end
134
+ end