puppet-forge-server 1.8.0 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/lib/puppet_forge_server.rb +4 -0
- data/lib/puppet_forge_server/api/v1/modules.rb +7 -3
- data/lib/puppet_forge_server/api/v3/modules.rb +7 -2
- data/lib/puppet_forge_server/api/v3/releases.rb +9 -4
- data/lib/puppet_forge_server/app/frontend.rb +19 -0
- data/lib/puppet_forge_server/app/public/css/puppetlabs.css +236 -0
- data/lib/puppet_forge_server/app/views/layout.haml +1 -1
- data/lib/puppet_forge_server/app/views/module.haml +71 -0
- data/lib/puppet_forge_server/app/views/modules.haml +1 -1
- data/lib/puppet_forge_server/backends/directory.rb +12 -5
- data/lib/puppet_forge_server/backends/proxy.rb +29 -4
- data/lib/puppet_forge_server/backends/proxy_v3.rb +20 -6
- data/lib/puppet_forge_server/http/http_client.rb +29 -2
- data/lib/puppet_forge_server/logger.rb +9 -2
- data/lib/puppet_forge_server/models/builder.rb +6 -1
- data/lib/puppet_forge_server/models/metadata.rb +1 -1
- data/lib/puppet_forge_server/models/module.rb +2 -2
- data/lib/puppet_forge_server/server.rb +2 -0
- data/lib/puppet_forge_server/utils/archiver.rb +3 -1
- data/lib/puppet_forge_server/utils/cache_provider.rb +42 -0
- data/lib/puppet_forge_server/utils/encoding.rb +46 -0
- data/lib/puppet_forge_server/utils/filtering_inspecter.rb +30 -0
- data/lib/puppet_forge_server/utils/md_renderer.rb +54 -0
- data/lib/puppet_forge_server/utils/option_parser.rb +23 -1
- data/lib/puppet_forge_server/version.rb +1 -1
- data/puppet-forge-server.gemspec +8 -0
- data/spec/spec_helper.rb +41 -1
- data/spec/unit/http/http_client_spec.rb +93 -0
- data/spec/unit/utils/encoding_spec.rb +35 -0
- metadata +110 -2
@@ -20,7 +20,7 @@
|
|
20
20
|
- modules.each do |element|
|
21
21
|
%li{:class => element['private'] ? 'clearfix private' : 'clearfix'}
|
22
22
|
.col
|
23
|
-
%h3= "#{element['owner']['username']}/#{element['name']}"
|
23
|
+
%a{:class => "h3", :href => "/module?name=#{element['current_release']['metadata']['name']}" }= "#{element['owner']['username']}/#{element['name']}"
|
24
24
|
%p= element['current_release']['metadata']['summary']
|
25
25
|
%span.release-info= "Version #{element['current_release']['metadata']['version']}"
|
26
26
|
- if element['current_release']['metadata']['issues_url']
|
@@ -56,9 +56,15 @@ module PuppetForgeServer::Backends
|
|
56
56
|
end
|
57
57
|
|
58
58
|
private
|
59
|
-
def
|
60
|
-
|
61
|
-
JSON.parse(
|
59
|
+
def read_module_data(archive_path)
|
60
|
+
file_contents = read_from_archive(archive_path, %r[^([^/]+/)?(metadata\.json|README\.md)$])
|
61
|
+
metadata = JSON.parse(file_contents.find {|key, value| key =~ /metadata\.json/}[1])
|
62
|
+
begin
|
63
|
+
readme = file_contents.find {|key, value| key =~ /README\.md/}[1]
|
64
|
+
rescue
|
65
|
+
readme = nil
|
66
|
+
end
|
67
|
+
[metadata, readme]
|
62
68
|
rescue => error
|
63
69
|
warn "Error reading from module archive #{archive_path}: #{error}"
|
64
70
|
return nil
|
@@ -80,14 +86,15 @@ module PuppetForgeServer::Backends
|
|
80
86
|
options = ({:with_checksum => true}).merge(options)
|
81
87
|
modules = []
|
82
88
|
Dir["#{@module_dir}/**/#{file_name}"].each do |path|
|
83
|
-
metadata_raw =
|
89
|
+
metadata_raw, readme = read_module_data(path)
|
84
90
|
if metadata_raw
|
85
91
|
modules <<
|
86
92
|
PuppetForgeServer::Models::Module.new({
|
87
93
|
:metadata => parse_dependencies(PuppetForgeServer::Models::Metadata.new(normalize_metadata(metadata_raw))),
|
88
94
|
:checksum => options[:with_checksum] == true ? Digest::MD5.file(path).hexdigest : nil,
|
89
95
|
:path => "/#{File.basename(path)}",
|
90
|
-
:private => ! @readonly
|
96
|
+
:private => ! @readonly,
|
97
|
+
:readme => readme
|
91
98
|
})
|
92
99
|
else
|
93
100
|
@log.error "Failed reading metadata from #{path}"
|
@@ -35,14 +35,18 @@ module PuppetForgeServer::Backends
|
|
35
35
|
|
36
36
|
def get_file_buffer(relative_path)
|
37
37
|
file_name = relative_path.split('/').last
|
38
|
-
File.join(@cache_dir, file_name[0].downcase, file_name)
|
38
|
+
target_file = File.join(@cache_dir, file_name[0].downcase, file_name)
|
39
39
|
path = Dir["#{@cache_dir}/**/#{file_name}"].first
|
40
40
|
unless File.exist?("#{path}")
|
41
41
|
buffer = download("#{@file_path.chomp('/')}/#{relative_path}")
|
42
|
-
File.open(
|
43
|
-
|
42
|
+
File.open(target_file, 'wb') do |file|
|
43
|
+
bytes = buffer.read
|
44
|
+
file.write(bytes)
|
45
|
+
@log.debug("Saved #{bytes.size} bytes in filesystem cache for path: #{relative_path}, target file: #{target_file}")
|
44
46
|
end
|
45
|
-
path =
|
47
|
+
path = target_file
|
48
|
+
else
|
49
|
+
@log.info("Filesystem cache HIT for path: #{relative_path}")
|
46
50
|
end
|
47
51
|
File.open(path, 'rb')
|
48
52
|
rescue => e
|
@@ -59,6 +63,27 @@ module PuppetForgeServer::Backends
|
|
59
63
|
protected
|
60
64
|
attr_reader :log
|
61
65
|
|
66
|
+
def get_non_mutable(relative_url)
|
67
|
+
file_name = relative_url.split('/').last
|
68
|
+
target_file = File.join(@cache_dir, file_name[0].downcase, file_name)
|
69
|
+
path = Dir["#{@cache_dir}/**/#{file_name}"].first
|
70
|
+
unless File.exist?("#{path}")
|
71
|
+
buffer = get(relative_url)
|
72
|
+
File.open(target_file, 'wb') do |file|
|
73
|
+
file.write(buffer)
|
74
|
+
@log.debug("Saved #{buffer} bytes in filesystem cache for url: #{relative_url}, target file: #{target_file}")
|
75
|
+
end
|
76
|
+
path = target_file
|
77
|
+
else
|
78
|
+
@log.info("Filesystem cache HIT for url: #{relative_url}")
|
79
|
+
end
|
80
|
+
File.binread(path)
|
81
|
+
rescue => e
|
82
|
+
@log.error("#{self.class.name} failed getting non-mutable url '#{relative_url}'")
|
83
|
+
@log.error("Error: #{e}")
|
84
|
+
return nil
|
85
|
+
end
|
86
|
+
|
62
87
|
def get(relative_url)
|
63
88
|
@http_client.get(url(relative_url))
|
64
89
|
end
|
@@ -30,11 +30,11 @@ module PuppetForgeServer::Backends
|
|
30
30
|
def get_metadata(author, name, options = {})
|
31
31
|
query ="#{author}-#{name}"
|
32
32
|
begin
|
33
|
-
releases =
|
33
|
+
releases = get_releases(query, options)
|
34
34
|
get_modules(releases)
|
35
35
|
rescue => e
|
36
|
-
@log.
|
37
|
-
@log.
|
36
|
+
@log.error("#{self.class.name} failed querying metadata for '#{query}' with options #{options}")
|
37
|
+
@log.error("Error: #{e}")
|
38
38
|
return nil
|
39
39
|
end
|
40
40
|
end
|
@@ -44,13 +44,26 @@ module PuppetForgeServer::Backends
|
|
44
44
|
releases = get_all_result_pages("/v3/modules?query=#{query}").map {|element| element['current_release']}
|
45
45
|
get_modules(releases)
|
46
46
|
rescue => e
|
47
|
-
@log.
|
48
|
-
@log.
|
47
|
+
@log.error("#{self.class.name} failed querying metadata for '#{query}' with options #{options}")
|
48
|
+
@log.error("Error: #{e}")
|
49
49
|
return nil
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
53
|
private
|
54
|
+
|
55
|
+
def get_releases(query, options = {})
|
56
|
+
version = options[:version]
|
57
|
+
unless version.nil?
|
58
|
+
url = "/v3/releases/#{query}-#{version}"
|
59
|
+
buffer = get_non_mutable(url)
|
60
|
+
release = JSON.parse(buffer)
|
61
|
+
[ release ]
|
62
|
+
else
|
63
|
+
get_all_result_pages("/v3/releases?module=#{query}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
54
67
|
def get_all_result_pages(next_page)
|
55
68
|
results = []
|
56
69
|
begin
|
@@ -80,7 +93,8 @@ module PuppetForgeServer::Backends
|
|
80
93
|
:checksum => element['file_md5'],
|
81
94
|
:path => element['file_uri'].gsub(/^#{@@FILE_PATH}/, ''),
|
82
95
|
:tags => (element['tags'] + (element['metadata']['tags'] ? element['metadata']['tags'] : [])).flatten.uniq,
|
83
|
-
:deleted_at => element['deleted_at']
|
96
|
+
:deleted_at => element['deleted_at'],
|
97
|
+
:readme => element['readme']
|
84
98
|
})
|
85
99
|
end
|
86
100
|
end
|
@@ -23,6 +23,15 @@ require 'net/http/post/multipart'
|
|
23
23
|
|
24
24
|
module PuppetForgeServer::Http
|
25
25
|
class HttpClient
|
26
|
+
include PuppetForgeServer::Utils::CacheProvider
|
27
|
+
include PuppetForgeServer::Utils::FilteringInspecter
|
28
|
+
|
29
|
+
def initialize(cache = nil)
|
30
|
+
cache = cache_instance if cache.nil?
|
31
|
+
cache.extend(PuppetForgeServer::Utils::FilteringInspecter)
|
32
|
+
@log = PuppetForgeServer::Logger.get
|
33
|
+
@cache = cache
|
34
|
+
end
|
26
35
|
|
27
36
|
def post_file(url, file_hash, options = {})
|
28
37
|
options = { :http => {}, :headers => {}}.merge(options)
|
@@ -45,11 +54,29 @@ module PuppetForgeServer::Http
|
|
45
54
|
open_uri(url)
|
46
55
|
end
|
47
56
|
|
57
|
+
def inspect
|
58
|
+
cache_inspected = @cache.inspect_without [ :@data ]
|
59
|
+
cache_inspected.gsub!(/>$/, ", @size=#{@cache.size}>")
|
60
|
+
inspected = inspect_without [ :@cache ]
|
61
|
+
inspected.gsub(/>$/, ", @cache=#{cache_inspected}>")
|
62
|
+
end
|
63
|
+
|
48
64
|
private
|
65
|
+
|
49
66
|
def open_uri(url)
|
50
|
-
|
51
|
-
|
67
|
+
hit_or_miss = @cache.include?(url) ? 'HIT' : 'MISS'
|
68
|
+
@log.info "Cache in RAM memory size: #{@cache.size}, #{hit_or_miss} for url: #{url}"
|
69
|
+
contents = @cache.fetch(url) do
|
70
|
+
tmpfile = ::Timeout.timeout(10) do
|
71
|
+
PuppetForgeServer::Logger.get.debug "Fetching data for url: #{url} from remote server"
|
72
|
+
open(url, 'User-Agent' => "Puppet-Forge-Server/#{PuppetForgeServer::VERSION}", :allow_redirections => :safe)
|
73
|
+
end
|
74
|
+
contents = tmpfile.read
|
75
|
+
tmpfile.close
|
76
|
+
contents
|
52
77
|
end
|
78
|
+
@log.debug "Data for url: #{url} fetched, #{contents.size} bytes"
|
79
|
+
StringIO.new(contents)
|
53
80
|
end
|
54
81
|
end
|
55
82
|
end
|
@@ -16,6 +16,7 @@
|
|
16
16
|
|
17
17
|
|
18
18
|
require 'logger'
|
19
|
+
require 'logger/colors'
|
19
20
|
|
20
21
|
module PuppetForgeServer
|
21
22
|
class Logger
|
@@ -45,7 +46,13 @@ module PuppetForgeServer
|
|
45
46
|
else
|
46
47
|
method_name
|
47
48
|
end
|
48
|
-
|
49
|
+
if args.size > 0
|
50
|
+
# setters
|
51
|
+
@loggers.each { |logger| logger.send(method_name, args.first) }
|
52
|
+
else
|
53
|
+
# getters
|
54
|
+
@loggers.collect { |logger| logger.send(method_name) }
|
55
|
+
end
|
49
56
|
end
|
50
57
|
|
51
58
|
def respond_to?(method_name, include_private = false)
|
@@ -70,4 +77,4 @@ module PuppetForgeServer
|
|
70
77
|
end
|
71
78
|
end
|
72
79
|
end
|
73
|
-
end
|
80
|
+
end
|
@@ -27,6 +27,11 @@ module PuppetForgeServer::Models
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def to_hash(obj = self)
|
30
|
+
# Quickly return if it's not one of ours
|
31
|
+
unless obj.kind_of? Builder
|
32
|
+
return obj
|
33
|
+
end
|
34
|
+
# Otherwise start building hash
|
30
35
|
hash = {}
|
31
36
|
obj.instance_variables.each do |var|
|
32
37
|
var_value = obj.instance_variable_get(var)
|
@@ -41,4 +46,4 @@ module PuppetForgeServer::Models
|
|
41
46
|
hash
|
42
47
|
end
|
43
48
|
end
|
44
|
-
end
|
49
|
+
end
|
@@ -18,7 +18,7 @@
|
|
18
18
|
module PuppetForgeServer::Models
|
19
19
|
class Module < Builder
|
20
20
|
|
21
|
-
attr_accessor :metadata, :checksum, :path, :private, :deleted_at, :tags
|
21
|
+
attr_accessor :metadata, :checksum, :path, :private, :deleted_at, :tags, :readme
|
22
22
|
|
23
23
|
def initialize(attributes)
|
24
24
|
super(attributes)
|
@@ -26,4 +26,4 @@ module PuppetForgeServer::Models
|
|
26
26
|
end
|
27
27
|
|
28
28
|
end
|
29
|
-
end
|
29
|
+
end
|
@@ -19,6 +19,7 @@ require 'rack/mount'
|
|
19
19
|
module PuppetForgeServer
|
20
20
|
class Server
|
21
21
|
include PuppetForgeServer::Utils::OptionParser
|
22
|
+
include PuppetForgeServer::Utils::CacheProvider
|
22
23
|
include PuppetForgeServer::Utils::Http
|
23
24
|
|
24
25
|
def go(args)
|
@@ -27,6 +28,7 @@ module PuppetForgeServer
|
|
27
28
|
begin
|
28
29
|
options = parse_options(args)
|
29
30
|
@log = logging(options)
|
31
|
+
configure_cache(options[:ram_cache_ttl], options[:ram_cache_size])
|
30
32
|
backends = backends(options)
|
31
33
|
server = build(backends, options[:webui_root])
|
32
34
|
announce(options, backends)
|
@@ -22,9 +22,11 @@ module PuppetForgeServer::Utils
|
|
22
22
|
def read_from_archive(archive, name_regex)
|
23
23
|
tar = Gem::Package::TarReader.new(Zlib::GzipReader.open(archive))
|
24
24
|
tar.rewind
|
25
|
+
files = {}
|
25
26
|
tar.each do |obj|
|
26
|
-
|
27
|
+
files[obj.full_name] = obj.read if obj.file? && obj.full_name =~ name_regex
|
27
28
|
end
|
29
|
+
return files unless files.empty?
|
28
30
|
raise "Given name #{name_regex} not found in #{archive}"
|
29
31
|
end
|
30
32
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright 2015 Centralny Osroder Informatyki (gov.pl)
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require 'lrucache'
|
18
|
+
|
19
|
+
module PuppetForgeServer::Utils
|
20
|
+
module CacheProvider
|
21
|
+
|
22
|
+
opts = PuppetForgeServer::Utils::OptionParser.DEFAULT_OPTIONS
|
23
|
+
@@CACHE = LRUCache.new(:ttl => opts[:ram_cache_ttl], :max_size => opts[:ram_cache_size])
|
24
|
+
|
25
|
+
# Method for fetching application wide cache for fetching HTTP requests
|
26
|
+
#
|
27
|
+
# @return [LRUCache] a instance of cache for application
|
28
|
+
def cache_instance
|
29
|
+
@@CACHE
|
30
|
+
end
|
31
|
+
|
32
|
+
# Configure a application wide cache using LSUCache implementation
|
33
|
+
#
|
34
|
+
# @param [int] ttl a time to live for elements
|
35
|
+
# @param [int] size a maximum size for cache
|
36
|
+
def configure_cache(ttl, size)
|
37
|
+
@@CACHE = LRUCache.new(:ttl => ttl, :max_size => size)
|
38
|
+
PuppetForgeServer::Logger.get.info("Using RAM memory LRUCache with time to live of #{ttl}sec and max size of #{size} elements")
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright 2015 Centralny Osroder Informatyki (gov.pl)
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require 'iconv'
|
18
|
+
|
19
|
+
module PuppetForgeServer::Utils
|
20
|
+
module Encoding
|
21
|
+
|
22
|
+
@@ic = Iconv.new('UTF-8//IGNORE', 'UTF-8')
|
23
|
+
|
24
|
+
# Converts give text to valid UTF-8
|
25
|
+
# @param [string] text given string, can be null
|
26
|
+
# @return [string] output string in utf-8
|
27
|
+
def to_utf8(text)
|
28
|
+
replaced = text
|
29
|
+
unless replaced.nil?
|
30
|
+
replaced = replaced.force_encoding("UTF-8") if is_ascii_8bit?(replaced)
|
31
|
+
replaced = cleanup_utf8(replaced)
|
32
|
+
end
|
33
|
+
replaced
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def is_ascii_8bit?(text)
|
39
|
+
text.encoding.name == 'ASCII-8BIT'
|
40
|
+
end
|
41
|
+
|
42
|
+
def cleanup_utf8(text)
|
43
|
+
@@ic.iconv(text)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright 2015 Centralny Osroder Informatyki (gov.pl)
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module PuppetForgeServer::Utils
|
18
|
+
module FilteringInspecter
|
19
|
+
def self.inspect_without(object, variables)
|
20
|
+
filtered = object.instance_variables.reject { |n| variables.include? n }
|
21
|
+
vars = filtered.map { |n| "#{n}=#{object.instance_variable_get(n).inspect}" }
|
22
|
+
oid = object.object_id << 1
|
23
|
+
"#<%s:0x%x %s>" % [ object.class, oid, vars.join(', ') ]
|
24
|
+
end
|
25
|
+
|
26
|
+
def inspect_without(variables)
|
27
|
+
PuppetForgeServer::Utils::FilteringInspecter.inspect_without(self, variables)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright 2015 North Development AB
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require 'tilt/redcarpet'
|
18
|
+
|
19
|
+
module PuppetForgeServer::Utils
|
20
|
+
module MarkdownRenderer
|
21
|
+
|
22
|
+
class CustomRenderer < Redcarpet::Render::HTML
|
23
|
+
def block_code(code, lang)
|
24
|
+
output = "<pre>"
|
25
|
+
output << "<code class=\"prettyprint lang-puppet\">#{code}</code>"
|
26
|
+
output << "</pre>"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def markdown(text)
|
31
|
+
options = {
|
32
|
+
filter_html: true,
|
33
|
+
with_toc_data: true,
|
34
|
+
hard_wrap: true,
|
35
|
+
prettify: true
|
36
|
+
}
|
37
|
+
|
38
|
+
extensions = {
|
39
|
+
autolink: true,
|
40
|
+
superscript: true,
|
41
|
+
disable_indented_code_blocks: false,
|
42
|
+
fenced_code_blocks: true,
|
43
|
+
strikethrough: true,
|
44
|
+
quote: true,
|
45
|
+
tables: true
|
46
|
+
}
|
47
|
+
|
48
|
+
renderer = CustomRenderer.new(options)
|
49
|
+
markdown = Redcarpet::Markdown.new(renderer, extensions)
|
50
|
+
markdown.render(text)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|