bibsonomy 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,121 @@
1
+ # BibSonomy
2
+
3
+ BibSonomy client for Ruby
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'bibsonomy'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install bibsonomy
20
+
21
+ ## Usage
22
+
23
+ Getting posts from BibSonomy:
24
+
25
+ ```ruby
26
+ require 'bibsonomy'
27
+ api = BibSonomy::API.new('yourusername', 'yourapikey', 'ruby')
28
+ posts = api.get_posts_for_user('jaeschke', 'publication', ['myown'], 0, 20)
29
+ ```
30
+
31
+ Rendering posts with [CSL](http://citationstyles.org/):
32
+
33
+ ```ruby
34
+ require 'bibsonomy/csl'
35
+ csl = BibSonomy::CSL.new('yourusername', 'yourapikey')
36
+ html = csl.render('jaeschke', ['myown'], 100)
37
+ print html
38
+ ```
39
+
40
+ A command line wrapper to the CSL renderer:
41
+
42
+ ```ruby
43
+ #!/usr/bin/ruby
44
+ require 'bibsonomy/csl'
45
+ print BibSonomy::main(ARGV)
46
+ ```
47
+
48
+ ## Jekyll
49
+
50
+ A [Jekyll](http://jekyllrb.com/) plugin:
51
+
52
+ ```ruby
53
+ # coding: utf-8
54
+ require 'time'
55
+ require 'bibsonomy/csl'
56
+
57
+ module Jekyll
58
+
59
+ class BibSonomyPostList < Liquid::Tag
60
+ def initialize(tag_name, text, tokens)
61
+ super
62
+ parts = text.split(/\s+/)
63
+ @user = parts[0]
64
+ @tag = parts[1]
65
+ @count = Integer(parts[2])
66
+ end
67
+
68
+ def render(context)
69
+ site = context.registers[:site]
70
+
71
+ # user name and API key for BibSonomy
72
+ user_name = site.config['bibsonomy_user']
73
+ api_key = site.config['bibsonomy_apikey']
74
+ csl = BibSonomy::CSL.new(user_name, api_key)
75
+
76
+ # target directory for PDF documents
77
+ pdf_dir = site.config['bibsonomy_document_directory']
78
+ csl.pdf_dir = pdf_dir
79
+
80
+ # CSL style for rendering
81
+ style = site.config['bibsonomy_style']
82
+ csl.style = style
83
+
84
+ html = csl.render(@user, [@tag], @count)
85
+
86
+ # set date to now
87
+ context.registers[:page]["date"] = Time.new
88
+
89
+ return html
90
+ end
91
+ end
92
+
93
+ end
94
+
95
+ Liquid::Template.register_tag('bibsonomy', Jekyll::BibSonomyPostList)
96
+ ```
97
+
98
+ The plugin can be used inside Markdown files as follows:
99
+
100
+ ```
101
+ {% bibsonomy jaeschke myown 100 %}
102
+ ```
103
+
104
+ Add the following options to your `_config.yml`:
105
+
106
+ ```
107
+ bibsonomy_user: yourusername
108
+ bibsonomy_apikey: yourapikey
109
+ bibsonomy_document_directory: pdf
110
+ # other: apa, acm-siggraph
111
+ bibsonomy_style: springer-lecture-notes-in-computer-science
112
+ ```
113
+
114
+
115
+ ## Contributing
116
+
117
+ 1. Fork it ( https://github.com/rjoberon/bibsonomy-ruby/fork )
118
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
119
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
120
+ 4. Push to the branch (`git push origin my-new-feature`)
121
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bibsonomy/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "bibsonomy"
8
+ spec.version = BibSonomy::VERSION
9
+ spec.authors = ["Robert Jäschke"]
10
+ spec.email = ["jaeschke@l3s.de"]
11
+ spec.summary = %q{Wraps the BibSonomy REST API.}
12
+ spec.description = %q{Enables calls to the BibSonomy REST API with Ruby.}
13
+ spec.homepage = "https://github.com/rjoberon/bibsonomy-ruby"
14
+ spec.license = "LGPL 2.1"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "minitest", "~> 5.4"
24
+ spec.add_development_dependency "vcr", "~> 2.9"
25
+ spec.add_development_dependency "webmock", "~> 1.19"
26
+
27
+ spec.add_dependency "faraday", "~> 0.9"
28
+ spec.add_dependency "json", "~> 1.8"
29
+ spec.add_dependency "citeproc", "~> 1.0"
30
+ spec.add_dependency "csl-styles", "~> 1.0"
31
+ spec.add_dependency "bibtex-ruby", "~> 4.0"
32
+
33
+ end
@@ -0,0 +1,7 @@
1
+ require_relative "bibsonomy/version"
2
+ require_relative "bibsonomy/post"
3
+ require_relative "bibsonomy/api"
4
+
5
+ module BibSonomy
6
+ # Your code goes here...
7
+ end
@@ -0,0 +1,150 @@
1
+ # coding: utf-8
2
+ require 'faraday'
3
+ require 'json'
4
+
5
+ #
6
+ # TODO:
7
+ # - error handling
8
+ # - getting more than 1000 posts
9
+ #
10
+
11
+ # configuration options
12
+ $API_URL = "https://www.bibsonomy.org/"
13
+ $MAX_POSTS_PER_REQUEST = 20
14
+
15
+ #
16
+ # allowed shortcuts for resource types
17
+ #
18
+ $resource_types_bookmark = ['bookmark', 'bookmarks', 'book', 'link', 'links', 'url']
19
+ $resource_types_bibtex = ['bibtex', 'pub', 'publication', 'publications', 'publ']
20
+
21
+ #
22
+ # The BibSonomy REST client for Ruby.
23
+ #
24
+ module BibSonomy
25
+ class API
26
+
27
+ # Initializes the client with the given credentials.
28
+ #
29
+ # @param user_name [String] The name of the user account used for
30
+ # accessing the API
31
+ #
32
+ # @param api_key [String] The API key corresponding to the user
33
+ # account - can be obtained from
34
+ # http://www.bibsonomy.org/settings?selTab=1
35
+ #
36
+ # @param format [String] The requested return format. One of:
37
+ # 'xml', 'json', 'ruby', 'csl', 'bibtex'. The default is 'ruby'
38
+ # which returns Ruby objects defined by this library. Currently,
39
+ # 'csl' and 'bibtex' are only available for publications.
40
+ #
41
+ def initialize(user_name, api_key, format = 'ruby')
42
+
43
+ # configure output format
44
+ if format == 'ruby'
45
+ @format = 'json'
46
+ @parse = true
47
+ else
48
+ @format = format
49
+ @parse = false
50
+ end
51
+
52
+ @conn = Faraday.new(:url => $API_URL) do |faraday|
53
+ faraday.request :url_encoded # form-encode POST params
54
+ #faraday.response :logger
55
+ faraday.adapter Faraday.default_adapter # make requests with
56
+ # Net::HTTP
57
+ end
58
+
59
+ @conn.basic_auth(user_name, api_key)
60
+
61
+ end
62
+
63
+
64
+ #
65
+ # Get a single post
66
+ #
67
+ # @param user_name [String] The name of the post's owner.
68
+ # @param intra_hash [String] The intrag hash of the post.
69
+ # @return [BibSonomy::Post] the requested post
70
+ #
71
+ def get_post(user_name, intra_hash)
72
+ response = @conn.get "/api/users/" + CGI.escape(user_name) + "/posts/" + CGI.escape(intra_hash), { :format => @format }
73
+
74
+ if @parse
75
+ attributes = JSON.parse(response.body)
76
+ return Post.new(attributes["post"])
77
+ end
78
+ return response.body
79
+ end
80
+
81
+ #
82
+ # Get posts owned by a user, optionally filtered by tags.
83
+ #
84
+ # @param user_name [String] The name of the posts' owner.
85
+ # @resource_type [String] The type of the post. Currently
86
+ # supported are 'bookmark' and 'publication'.
87
+ #
88
+ def get_posts_for_user(user_name, resource_type, tags = nil, start = 0, endc = $MAX_POSTS_PER_REQUEST)
89
+ params = {
90
+ :format => @format,
91
+ :resourcetype => self.get_resource_type(resource_type),
92
+ :start => start,
93
+ :end => endc
94
+ }
95
+ # add tags, if requested
96
+ if tags != nil
97
+ params[:tags] = tags.join(" ")
98
+ end
99
+ response = @conn.get "/api/users/" + CGI.escape(user_name) + "/posts", params
100
+
101
+ if @parse
102
+ posts = JSON.parse(response.body)["posts"]["post"]
103
+ return posts.map { |attributes| Post.new(attributes) }
104
+ end
105
+ return response.body
106
+ end
107
+
108
+ def get_document_href(user_name, intra_hash, file_name)
109
+ return "/api/users/" + CGI.escape(user_name) + "/posts/" + CGI.escape(intra_hash) + "/documents/" + CGI.escape(file_name)
110
+ end
111
+
112
+ #
113
+ # get a document belonging to a post
114
+ #
115
+ def get_document(user_name, intra_hash, file_name)
116
+ response = @conn.get get_document_href(user_name, intra_hash, file_name)
117
+ if response.status == 200
118
+ return [response.body, response.headers['content-type']]
119
+ end
120
+ return nil, nil
121
+ end
122
+
123
+ def get_document_preview(user_name, intra_hash, file_name, size)
124
+ response = get_document_href(user_name, intra_hash, file_name), { :preview => size }
125
+ if response.status = 200
126
+ return [response.body, 'image/jpeg']
127
+ end
128
+ return nil, nil
129
+ end
130
+
131
+ #
132
+ # Convenience method to allow sloppy specification of the resource
133
+ # type.
134
+ #
135
+ # @private
136
+ #
137
+ def get_resource_type(resource_type)
138
+ if $resource_types_bookmark.include? resource_type.downcase()
139
+ return "bookmark"
140
+ end
141
+
142
+ if $resource_types_bibtex.include? resource_type.downcase()
143
+ return "bibtex"
144
+ end
145
+
146
+ raise ArgumentError.new("Unknown resource type: #{resource_type}. Supported resource types are ")
147
+ end
148
+
149
+ end
150
+ end
@@ -0,0 +1,395 @@
1
+ # coding: utf-8
2
+
3
+ require 'optparse'
4
+ require 'citeproc'
5
+ require 'csl/styles'
6
+ require 'bibtex'
7
+ require 'json'
8
+ require 'bibsonomy'
9
+
10
+ #
11
+ # Generates a list of publication posts from BibSonomy
12
+ #
13
+ # required parameters:
14
+ # - user name
15
+ # - api key
16
+ # optional parameters:
17
+ # - user name
18
+ # - tags
19
+ # - number of posts
20
+ # - style
21
+ # - directory
22
+ #
23
+ # Changes:
24
+ # 2015-02-24
25
+ # - initial version
26
+ #
27
+ # TODO:
28
+ # - escape data
29
+ # - make sorting, etc. configurable
30
+ # - add link to BibSonomy
31
+ # - automatically rename files (TODO: CSL lacks BibTeX key)
32
+ # - add intra_hash, user_name, DOI to CSL
33
+ # - integrate AJAX abstract
34
+ # - make all options available via command line
35
+
36
+ module BibSonomy
37
+ class CSL
38
+
39
+ #
40
+ # Create a new BibSonomy instance
41
+ # Params:
42
+ # +user_name+:: BibSonomy user name
43
+ # +api_key+:: API key of the given user (get at http://www.bibsonomy.org/settings?selTab=1)
44
+ def initialize(user_name, api_key)
45
+ super()
46
+ @bibsonomy = BibSonomy::API.new(user_name, api_key, 'csl')
47
+ # setting some defaults
48
+ @style = 'apa.csl'
49
+ @pdf_dir = nil
50
+ @css_class = 'publications'
51
+ @year_headings = true
52
+ @public_doc_postfix = '_oa.pdf'
53
+
54
+ # optional parts to be rendered (or not)
55
+ @doi_link = true
56
+ @url_link = true
57
+ @bibtex_link = true
58
+ @bibsonomy_link = true
59
+ @opt_sep = ' | '
60
+ end
61
+
62
+ #
63
+ # Download +count+ posts for the given +user+ and +tag(s)+ and render them with CSL.
64
+ # Params:
65
+ # +user+:: user name
66
+ # +tags+:: an array of tags
67
+ # +count+:: number of posts to download
68
+ def render(user, tags, count)
69
+ # get posts from BibSonomy
70
+ posts = JSON.parse(@bibsonomy.get_posts_for_user(user, 'publication', tags, 0, count))
71
+
72
+ # render them with citeproc
73
+ cp = CiteProc::Processor.new style: @style, format: 'html'
74
+ cp.import posts
75
+
76
+ # to check for duplicate file names
77
+ file_names = []
78
+
79
+ # sort posts by year
80
+ sorted_keys = posts.keys.sort { |a,b| get_sort_posts(posts[b], posts[a]) }
81
+
82
+ result = ""
83
+
84
+ # print first heading
85
+ last_year = 0
86
+
87
+ if @year_headings and sorted_keys.length > 0
88
+ last_year = get_year(posts[sorted_keys[0]])
89
+ result += "<h3>" + last_year + "</h3>"
90
+ end
91
+
92
+ result += "<ul class='#{@publications}'>\n"
93
+ for post_id in sorted_keys
94
+ post = posts[post_id]
95
+
96
+ # print heading
97
+ if @year_headings
98
+ year = get_year(post)
99
+ if year != last_year
100
+ last_year = year
101
+ result += "</ul>\n<h3>" + last_year + "</h3>\n<ul class='#{@publications}'>\n"
102
+ end
103
+ end
104
+
105
+ # render metadata
106
+ csl = cp.render(:bibliography, id: post_id)
107
+ result += "<li class='" + post["type"] + "'>#{csl[0]}"
108
+
109
+ # extract the post's id
110
+ intra_hash, user_name = get_intra_hash(post_id)
111
+
112
+ # optional parts
113
+ options = []
114
+ # attach documents
115
+ if @pdf_dir
116
+ for doc in get_public_docs(post["documents"])
117
+ # fileHash, fileName, md5hash, userName
118
+ file_path = get_document(@bibsonomy, intra_hash, user_name, doc, @pdf_dir, file_names)
119
+ options << "<a href='#{file_path}'>PDF</a>"
120
+ end
121
+ end
122
+ # attach DOI
123
+ doi = post["DOI"]
124
+ if @doi_link and doi != ""
125
+ options << "DOI:<a href='http://dx.doi.org/#{doi}'>#{doi}</a>"
126
+ end
127
+ # attach URL
128
+ url = post["URL"]
129
+ if @url_link and url != ""
130
+ options << "<a href='#{url}'>URL</a>"
131
+ end
132
+ # attach BibTeX
133
+ if @bibtex_link
134
+ options << "<a href='http://www.bibsonomy.org/bib/publication/#{intra_hash}/#{user_name}'>BibTeX</a>"
135
+ end
136
+ # attach link to BibSonomy
137
+ if @bibsonomy_link
138
+ options << "<a href='http://www.bibsonomy.org/publication/#{intra_hash}/#{user_name}'>BibSonomy</a>"
139
+ end
140
+
141
+ # attach options
142
+ if options.length > 0
143
+ result += " <span class='opt'>[" + options.join(@opt_sep) + "]</span>"
144
+ end
145
+
146
+ result += "</li>\n"
147
+ end
148
+ result += "</ul>\n"
149
+
150
+ return result
151
+ end
152
+
153
+ def get_year(post)
154
+ return post["issued"]["literal"]
155
+ end
156
+
157
+ def get_sort_posts(a, b)
158
+ person_a = a["author"]
159
+ if person_a.length == 0
160
+ person_a = a["editor"]
161
+ end
162
+ person_b = b["author"]
163
+ if person_b.length == 0
164
+ person_b = b["editor"]
165
+ end
166
+ return [get_year(a), a["type"], person_b[0]["family"]] <=> [get_year(b), b["type"], person_a[0]["family"]]
167
+ end
168
+
169
+ #
170
+ # only show PDF files and if
171
+ #
172
+ def get_public_docs(documents)
173
+ result = []
174
+ for doc in documents
175
+ file_name = doc["fileName"]
176
+ if file_name.end_with? ".pdf"
177
+ if documents.length < 2 or file_name.end_with? @public_doc_postfix
178
+ result << doc
179
+ end
180
+ end
181
+ end
182
+ return result
183
+ end
184
+
185
+ def warn(m)
186
+ print("WARN: " + m + "\n")
187
+ end
188
+
189
+ #
190
+ # downloads the documents for the posts (if necessary)
191
+ #
192
+ def get_document(bib, intra_hash, user_name, doc, dir, file_names)
193
+ # fileHash, fileName, md5hash, userName
194
+ file_name = doc["fileName"]
195
+ # strip doc prefix for public documents
196
+ if file_name.end_with? @public_doc_postfix
197
+ file_name = file_name[0, file_name.length - @public_doc_postfix.length] + ".pdf"
198
+ end
199
+ # check for possible duplicate file names
200
+ if file_names.include? file_name
201
+ warn "duplicate file name " + file_name + " for post " + intra_hash
202
+ end
203
+ # remember file name
204
+ file_names << file_name
205
+ # produce file path
206
+ file_path = dir + "/" + file_name
207
+ # download PDF if it not already exists
208
+ if not File.exists? file_path
209
+ pdf, mime = bib.get_document(user_name, intra_hash, doc["fileName"])
210
+ if pdf == nil
211
+ warn "could not download file " + intra_hash + "/" + user_name + "/" + file_name
212
+ else
213
+ File.binwrite(file_path, pdf)
214
+ end
215
+ end
216
+ return file_path
217
+ end
218
+
219
+ # format of the post ID for CSL: [0-9a-f]{32}USERNAME
220
+ def get_intra_hash(post_id)
221
+ return [post_id[0, 32], post_id[32, post_id.length]]
222
+ end
223
+
224
+ #
225
+ # setters
226
+ #
227
+
228
+ #
229
+ # Set the output directory for downloaded PDF files (default: +nil+)
230
+ # Params:
231
+ # +pdf_dir+:: directory for downloaded PDF files. If set to +nil+, no documents are downloaded.
232
+ def pdf_dir=(pdf_dir)
233
+ @pdf_dir = pdf_dir
234
+ end
235
+
236
+ #
237
+ # Set the CSL style used for rendering (default: +apa.csl+)
238
+ # Params:
239
+ # +style+:: CSL style used for rendering
240
+ def style=(style)
241
+ @style = style
242
+ end
243
+
244
+ #
245
+ # Enable/disable headings for years (default: enabled)
246
+ # Params:
247
+ # +year_headings+:: boolean indicating whether year headings shall be rendered
248
+ def year_headings=(year_headings)
249
+ @year_headings = year_headings
250
+ end
251
+
252
+ #
253
+ # The CSS class used to render the surrounding +<ul>+ list (default: 'publications')
254
+ # Params:
255
+ # +css_class+:: string indicating the CSS class for rendering the publication list
256
+ def css_class=(css_class)
257
+ @css_class = css_class
258
+ end
259
+
260
+ #
261
+ # Shall links for DOIs be rendered? (default: true)
262
+ # Params:
263
+ # +doi_link+:: render DOI link
264
+ def doi_link=(doi_link)
265
+ @doi_link = doi_link
266
+ end
267
+
268
+ #
269
+ # Shall links for URLs of posts be rendered? (default: true)
270
+ # Params:
271
+ # +url_link+:: render URL link
272
+ def url_link=(url_link)
273
+ @url_link = url_link
274
+ end
275
+
276
+ #
277
+ # Shall links to the BibTeX of a post (in BibSonomy) be rendered? (default: true)
278
+ # Params:
279
+ # +bibtex_link+:: render BibTeX link
280
+ def bibtex_link=(bibtex_link)
281
+ @bibtex_link = bibtex_link
282
+ end
283
+
284
+ #
285
+ # Shall links to BibSonomy be rendered? (default: true)
286
+ # Params:
287
+ # +bibsonomy_link+:: render BibSonomy link
288
+ def bibsonomy_link=(bibsonomy_link)
289
+ @bibsonomy_link = bibsonomy_link
290
+ end
291
+
292
+ #
293
+ # Separator between options (default: ' | ')
294
+ # Params:
295
+ # +opt_sep+:: option separator
296
+ def opt_sep=(opt_sep)
297
+ @opt_sep = opt_sep
298
+ end
299
+
300
+ #
301
+ # When a post has several documents and the filename of one of
302
+ # them ends with +public_doc_postfix+, only this document is
303
+ # downloaded and linked, all other are ignored. (default:
304
+ # '_oa.pdf')
305
+ # Params:
306
+ # +public_doc_postfix+:: postfix to check at document filenames
307
+ def public_doc_postfix=(public_doc_postfix)
308
+ @public_doc_postfix = public_doc_postfix
309
+ end
310
+
311
+ end
312
+
313
+
314
+ # parse command line options
315
+ def self.main(args)
316
+
317
+ # setting default options
318
+ options = OpenStruct.new
319
+ options.documents = false
320
+ options.directory = nil
321
+ options.tags = []
322
+ options.style = "apa.csl"
323
+ options.posts = 1000
324
+
325
+ opt_parser = OptionParser.new do |opts|
326
+ opts.banner = "Usage: csl.rb [options] user_name api_key"
327
+
328
+ opts.separator ""
329
+ opts.separator "Specific options:"
330
+
331
+ # mandatory arguments are handled separately
332
+
333
+ # optional arguments
334
+ opts.on('-u', '--user USER', 'return posts for USER instead of user') { |v| options[:user] = v }
335
+ opts.on('-t', '--tags TAG,TAG,...', Array, 'return posts with the given tags') { |v| options[:tags] = v }
336
+ opts.on('-s', '--style STYLE', 'use CSL style STYLE for rendering') { |v| options[:style] = v }
337
+ opts.on('-n', '--number-of-posts [COUNT]', Integer, 'number of posts to download') { |v| options[:posts] = v }
338
+ opts.on('-d', '--directory DIR', 'target directory', ' (if not given, no documents are downloaed)') { |v| options[:directory] = v }
339
+
340
+ opts.separator ""
341
+ opts.separator "Common options:"
342
+
343
+ opts.on('-h', '--help', 'show this help message and exit') do
344
+ puts opts
345
+ exit
346
+ end
347
+
348
+ opts.on_tail('-v', "--version", "show version") do
349
+ puts BibSonomy::VERSION
350
+ exit
351
+ end
352
+
353
+ end
354
+
355
+ opt_parser.parse!(args)
356
+
357
+ # handle mandatory arguments
358
+ begin
359
+ mandatory = [:user_name, :api_key]
360
+ missing = []
361
+
362
+ options[:api_key] = args.pop
363
+ missing << :api_key unless options[:api_key]
364
+
365
+ options[:user_name] = args.pop
366
+ missing << :user_name unless options[:user_name]
367
+
368
+ if not missing.empty?
369
+ puts "Missing options: #{missing.join(', ')}"
370
+ puts opt_parser
371
+ exit
372
+ end
373
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument
374
+ puts $!.to_s
375
+ puts opt_parser
376
+ exit
377
+ end
378
+
379
+ # set defaults for optional arguments
380
+ options[:user] = options[:user_name] unless options[:user]
381
+
382
+ #
383
+ # do the actual work
384
+ #
385
+ csl = BibSonomy::CSL.new(options[:user_name], options[:api_key])
386
+ csl.pdf_dir(options[:directory])
387
+ csl.style(options[:style])
388
+
389
+ html = csl.render(options[:user], options[:tags], options[:posts])
390
+
391
+ return html
392
+
393
+ end
394
+
395
+ end