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,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