neruda 0.0.9 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/bin/pablo +135 -238
  3. data/lib/neruda/config.rb +137 -0
  4. data/lib/neruda/config/lisp_config.rb +254 -0
  5. data/lib/neruda/config/org-config.el +18 -0
  6. data/lib/neruda/config/ox-neruda.el +114 -0
  7. data/lib/neruda/emacs.rb +44 -0
  8. data/lib/neruda/index.rb +122 -0
  9. data/lib/neruda/index/atom_generator.rb +86 -0
  10. data/lib/neruda/index/org_generator.rb +115 -0
  11. data/lib/neruda/org_file.rb +299 -0
  12. data/lib/neruda/org_file/class_methods.rb +72 -0
  13. data/lib/neruda/org_file/extracter.rb +72 -0
  14. data/lib/neruda/org_file/htmlizer.rb +53 -0
  15. data/lib/neruda/preview.rb +55 -0
  16. data/lib/neruda/templater.rb +112 -0
  17. data/lib/neruda/utils.rb +212 -0
  18. data/lib/neruda/version.rb +6 -0
  19. data/lib/tasks/org.rake +84 -0
  20. data/lib/tasks/site.rake +86 -0
  21. data/lib/tasks/sync.rake +34 -0
  22. data/lib/tasks/tags.rake +19 -0
  23. data/locales/en.yml +37 -0
  24. data/locales/fr.yml +37 -0
  25. data/themes/default/css/htmlize.css +346 -0
  26. data/themes/default/css/style.css +153 -0
  27. data/themes/default/img/bottom.png +0 -0
  28. data/themes/default/img/tic.png +0 -0
  29. data/themes/default/img/top.png +0 -0
  30. metadata +153 -43
  31. data/README.md +0 -98
  32. data/docs/Rakefile.example +0 -4
  33. data/docs/config.yml.example +0 -17
  34. data/lib/assets/chapter.slim +0 -14
  35. data/lib/assets/index.slim +0 -13
  36. data/lib/assets/layout.slim +0 -17
  37. data/lib/assets/style.css +0 -199
  38. data/lib/neruda.rb +0 -106
  39. data/lib/neruda/chapter.rb +0 -26
  40. data/lib/neruda/url.rb +0 -14
  41. data/lib/tasks/book.rake +0 -60
  42. data/lib/tasks/capistrano/chapters.rake +0 -60
  43. data/lib/tasks/capistrano/sinatra.rake +0 -18
  44. data/lib/tasks/chapters.rake +0 -132
  45. data/lib/tasks/sinatra.rake +0 -36
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Neruda
4
+ # This module holds class methods for the {Neruda::OrgFile} class.
5
+ module OrgFileClassMethods
6
+ def source_for_target(file_name)
7
+ # file_name may be frozen...
8
+ src = file_name.sub(/\.html$/, '.org')
9
+ pubfolder = Neruda::Config.settings['public_folder']
10
+ src.sub!(/^#{pubfolder}\//, '')
11
+ # Look for match in each possible sources. The first found wins.
12
+ Neruda::Config.sources.each do |project|
13
+ if project['target'] == '.'
14
+ origin = File.join(project['path'], src)
15
+ else
16
+ origin = File.join(
17
+ project['path'], src.sub(/^#{project['target']}\//, '')
18
+ )
19
+ end
20
+ return origin if File.exist?(origin)
21
+ end
22
+ nil
23
+ end
24
+
25
+ def target_for_source(file_name, project, with_public_folder: true)
26
+ return nil if file_name.nil?
27
+ # file_name may be frozen...
28
+ target = file_name.sub(/\.org$/, '.html').sub(/^#{Dir.pwd}\//, '')
29
+ if project.nil?
30
+ subfolder = File.basename(File.dirname(target))
31
+ target = File.basename(target)
32
+ target = "#{subfolder}/#{target}" if subfolder != '.'
33
+ else
34
+ project_relative_path = project['path'].sub(/^#{Dir.pwd}\//, '')
35
+ target.sub!(/^#{project_relative_path}\//, '')
36
+ target = "#{project['target']}/#{target}" if project['target'] != '.'
37
+ end
38
+ return target unless with_public_folder
39
+ pubfolder = Neruda::Config.settings['public_folder']
40
+ "#{pubfolder}/#{target}"
41
+ end
42
+
43
+ def project_for_source(file_name)
44
+ # Look for match in each possible sources. The first found wins.
45
+ Neruda::Config.sources.each do |project|
46
+ project_relative_path = project['path'].sub(/^#{Dir.pwd}\//, '')
47
+ return project if file_name =~ /^#{project_relative_path}\//
48
+ end
49
+ nil
50
+ end
51
+
52
+ def slug(title)
53
+ title.downcase.gsub(' ', '-')
54
+ .encode('ascii', fallback: ->(k) { translit(k) })
55
+ .gsub(/[^\w-]/, '').gsub(/-$/, '')
56
+ end
57
+
58
+ private
59
+
60
+ def translit(char)
61
+ return 'a' if ['á', 'à', 'â', 'ä', 'ǎ', 'ã', 'å'].include?(char)
62
+ return 'e' if ['é', 'è', 'ê', 'ë', 'ě', 'ẽ'].include?(char)
63
+ return 'i' if ['í', 'ì', 'î', 'ï', 'ǐ', 'ĩ'].include?(char)
64
+ return 'o' if ['ó', 'ò', 'ô', 'ö', 'ǒ', 'õ'].include?(char)
65
+ return 'u' if ['ú', 'ù', 'û', 'ü', 'ǔ', 'ũ'].include?(char)
66
+ return 'y' if ['ý', 'ỳ', 'ŷ', 'ÿ', 'ỹ'].include?(char)
67
+ return 'c' if char == 'ç'
68
+ return 'n' if char == 'ñ'
69
+ '-'
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Neruda
4
+ # This module holds extracter methods for the {Neruda::OrgFile} class.
5
+ module OrgFileExtracter
6
+ private
7
+
8
+ # Main method, which will call the other to initialize an
9
+ # {Neruda::OrgFile} instance.
10
+ def extract_data
11
+ @content = IO.read @file
12
+ @title = extract_title
13
+ @subtitle = extract_subtitle
14
+ @date = extract_date
15
+ @author = extract_author
16
+ @keywords = extract_keywords
17
+ @lang = extract_lang
18
+ @excerpt = extract_excerpt
19
+ end
20
+
21
+ def extract_date
22
+ timerx = '([0-9:]{5})(?::([0-9]{2}))?'
23
+ m = /^#\+date: *<([0-9-]{10}) [\w.]+(?: #{timerx})?> *$/i.match(@content)
24
+ return nil if m.nil?
25
+ @notime = m[2].nil?
26
+ if @notime
27
+ time = '00:00:00'
28
+ else
29
+ time = "#{m[2]}:#{m[3] || '00'}"
30
+ end
31
+ DateTime.strptime("#{m[1]} #{time}", '%Y-%m-%d %H:%M:%S')
32
+ end
33
+
34
+ def extract_title
35
+ m = /^#\+title:(.+)$/i.match(@content)
36
+ if m.nil?
37
+ # Avoid to leak absolute path
38
+ project_relative_path = @file.sub(/^#{Dir.pwd}\//, '')
39
+ return project_relative_path
40
+ end
41
+ m[1].strip
42
+ end
43
+
44
+ def extract_subtitle
45
+ m = /^#\+subtitle:(.+)$/i.match(@content)
46
+ return '' if m.nil?
47
+ m[1].strip
48
+ end
49
+
50
+ def extract_author
51
+ m = /^#\+author:(.+)$/i.match(@content)
52
+ return Neruda::Config.settings['author'] if m.nil?
53
+ m[1].strip
54
+ end
55
+
56
+ def extract_keywords
57
+ m = /^#\+keywords:(.+)$/i.match(@content)
58
+ return [] if m.nil?
59
+ m[1].split(',').map(&:strip)
60
+ end
61
+
62
+ def extract_lang
63
+ m = /^#\+language:(.+)$/i.match(@content)
64
+ return Neruda::Config.settings['lang'] if m.nil?
65
+ m[1].strip
66
+ end
67
+
68
+ def extract_excerpt
69
+ @content.scan(/^#\+description:(.+)$/i).map { |l| l[0].strip }.join(' ')
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'neruda/config'
4
+ require 'neruda/emacs'
5
+
6
+ module Neruda
7
+ # This module holds HTML formatter methods for the {Neruda::OrgFile}
8
+ # class.
9
+ module OrgFileHtmlizer
10
+ # Publish the current file or the entire project if
11
+ # {Neruda::OrgFile#file @file} is ~nil~.
12
+ #
13
+ # @return [Boolean, nil] the underlying ~system~ method return value
14
+ def publish
15
+ Neruda::Emacs.new(
16
+ file_path: @file, verbose: @options[:verbose]
17
+ ).publish
18
+ end
19
+
20
+ private
21
+
22
+ # Format {Neruda::OrgFile#keywords} list in an HTML listing.
23
+ #
24
+ # @return [String] the HTML keywords list
25
+ def keywords_to_html
26
+ domain = Neruda::Config.settings['domain']
27
+ klist = @keywords.map do |k|
28
+ <<~KEYWORDLINK
29
+ <li class="keyword">
30
+ <a href="#{domain}/tags/#{Neruda::OrgFile.slug(k)}.html">#{k}</a>
31
+ </li>
32
+ KEYWORDLINK
33
+ end.join
34
+ "<ul class=\"keywords-list\">#{klist}</ul>"
35
+ end
36
+
37
+ # Format {Neruda::OrgFile#date} as a HTML `time` tag.
38
+ #
39
+ # @return [String] the HTML `time` tag
40
+ def date_to_html(dateformat = :full)
41
+ return '<time></time>' if @date.nil?
42
+ "<time datetime=\"#{@date.rfc3339}\">#{datestring(dateformat)}</time>"
43
+ end
44
+
45
+ # Format {Neruda::OrgFile#author} in a HTML `span` tag with a
46
+ # specific class.
47
+ #
48
+ # @return [String] the author HTML `span`
49
+ def author_to_html
50
+ "<span class=\"author\">#{@author}</span>"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'webrick'
4
+ require 'neruda/config'
5
+
6
+ module Neruda # rubocop:disable Style/Documentation
7
+ # A tiny preview server, which main goal is to replace references to
8
+ # the target domain by localhost.
9
+ class PreviewServlet < WEBrick::HTTPServlet::AbstractServlet
10
+ include WEBrick::HTTPUtils
11
+
12
+ def do_GET(request, response) # rubocop:disable Naming/MethodName
13
+ file = local_path(request.path)
14
+ response.body = parse_body(file, "http://#{request.host}:#{request.port}")
15
+ response.status = 200
16
+ response.content_type = mime_type(file, DefaultMimeTypes)
17
+ end
18
+
19
+ private
20
+
21
+ def local_path(requested_path)
22
+ routes = Neruda::Config.settings.dig('preview', 'routes') || {}
23
+ return routes[requested_path] if routes.keys.include? requested_path
24
+ local_path = Neruda::Config.settings['public_folder'] + requested_path
25
+ if File.directory? local_path
26
+ local_path = format(
27
+ '%<path>s/index.html', path: local_path.delete_suffix('/')
28
+ )
29
+ end
30
+ return local_path if File.exist? local_path
31
+ raise WEBrick::HTTPStatus::NotFound, 'Not found.'
32
+ end
33
+
34
+ def parse_body(local_path, local_host)
35
+ body = IO.read local_path
36
+ return body unless local_path.match?(/\.(?:ht|x)ml$/)
37
+ domain = Neruda::Config.settings['domain']
38
+ return body if domain == ''
39
+ body.gsub(/"file:\/\//, format('"%<host>s', host: local_host))
40
+ .gsub(/"#{domain}/, format('"%<host>s', host: local_host))
41
+ end
42
+ end
43
+
44
+ class << self
45
+ def start_preview
46
+ # Inspired by ruby un.rb library, which allows normally to start a
47
+ # webrick server in one line: ruby -run -e httpd public_html -p 5000
48
+ port = Neruda::Config.settings.dig('preview', 'server_port') || 5000
49
+ s = WEBrick::HTTPServer.new(Port: port)
50
+ s.mount '/', Neruda::PreviewServlet
51
+ ['TERM', 'QUIT', 'INT'].each { |sig| trap(sig, proc { s.shutdown }) }
52
+ s.start
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'digest/md5'
5
+ require 'neruda/org_file'
6
+
7
+ module Neruda
8
+ # Insert custom part inside generated HTML files.
9
+ class Templater
10
+ def initialize(source, dom, opts = {})
11
+ @dom = dom
12
+ @org_file = source
13
+ @position = opts['type'] || 'after'
14
+ @content = opts['content']
15
+ @element = @dom.css(opts['selector'])
16
+ digest = Digest::MD5.hexdigest(@content)
17
+ @check_line = " Neruda Template: #{digest} "
18
+ end
19
+
20
+ def apply
21
+ flag_head
22
+ content = @org_file.format(@content)
23
+ @element.each do |e|
24
+ insert_new_node_at e, content
25
+ end
26
+ end
27
+
28
+ def in_head?
29
+ @dom.xpath('//head').children.to_a.filter(&:comment?).each do |c|
30
+ return true if c.text == @check_line
31
+ end
32
+ false
33
+ end
34
+
35
+ class << self
36
+ def customize_output(file_name, source = nil)
37
+ templates_to_apply = filter_templates(file_name)
38
+ return if templates_to_apply.empty?
39
+ if source.nil?
40
+ sourcepath = Neruda::OrgFile.source_for_target(file_name)
41
+ source = Neruda::OrgFile.new(sourcepath)
42
+ end
43
+ dom = open_dom(file_name)
44
+ templates_to_apply.each do |t|
45
+ tpl = Neruda::Templater.new(source, dom, t)
46
+ next if tpl.in_head?
47
+ tpl.apply
48
+ end
49
+ write_dom(file_name, dom)
50
+ end
51
+
52
+ private
53
+
54
+ def filter_templates(file_name)
55
+ templates = Neruda::Config.settings['templates']
56
+ return [] if templates.nil? || templates.empty?
57
+ templates.filter do |t|
58
+ if !t.has_key?('selector') || !t.has_key?('content')
59
+ false
60
+ elsif t.has_key?('path') && !check_path(file_name, t['path'])
61
+ false
62
+ else
63
+ true
64
+ end
65
+ end
66
+ end
67
+
68
+ def open_dom(file_name)
69
+ file = File.new file_name, 'r'
70
+ dom = Nokogiri::HTML file
71
+ file.close
72
+ dom
73
+ end
74
+
75
+ def write_dom(file_name, dom)
76
+ file = File.new file_name, 'w'
77
+ dom.write_to file
78
+ file.close
79
+ end
80
+
81
+ def check_path(file_name, pathes)
82
+ pub_folder = Neruda::Config.settings['public_folder']
83
+ if pathes.is_a?(Array)
84
+ pathes.each do |tp|
85
+ return true if File.fnmatch?("#{pub_folder}#{tp}",
86
+ file_name, File::FNM_DOTMATCH)
87
+ end
88
+ return false
89
+ end
90
+ File.fnmatch?("#{pub_folder}#{pathes}",
91
+ file_name, File::FNM_DOTMATCH)
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def flag_head
98
+ @dom.xpath('//head').first.prepend_child("<!--#{@check_line}-->\n")
99
+ end
100
+
101
+ def insert_new_node_at(elem, content)
102
+ case @position
103
+ when 'before'
104
+ elem.add_previous_sibling content
105
+ when 'replace'
106
+ elem.replace content
107
+ else
108
+ elem.add_next_sibling content
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'rainbow'
5
+ require 'net/http'
6
+ require 'r18n-core'
7
+ require 'neruda/config'
8
+
9
+ module Neruda
10
+ # Embeds usefull methods, mainly used in rake tasks.
11
+ module Utils
12
+ # @return [Hash] the possible throbber themes
13
+ THROBBER_FRAMES = {
14
+ 'basic' => '-\|/',
15
+ 'basicdots' => '⋯⋱⋮⋰',
16
+ 'moon' => '🌑🌒🌓🌔🌕🌖🌗🌘',
17
+ 'clock' => '🕛🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚',
18
+ 'bricks' => '⣷⣯⣟⡿⢿⣻⣽⣾',
19
+ 'points' => '·⁘∷⁛∷⁘',
20
+ 'quadrant2' => '▙▛▜▟',
21
+ 'default' => ['⠁ ⠂ ⠄ ⡀ ⠄ ⠂ ⠁', '⠂ ⠁ ⠂ ⠄ ⡀ ⠄ ⠂', '⠄ ⠂ ⠁ ⠂ ⠄ ⡀ ⠄',
22
+ '⡀ ⠄ ⠂ ⠁ ⠂ ⠄ ⡀', '⠄ ⡀ ⠄ ⠂ ⠁ ⠂ ⠄', '⠂ ⠄ ⡀ ⠄ ⠂ ⠁ ⠂']
23
+ }.freeze
24
+
25
+ # @return [Hash] the possible ~pablo~ options and their
26
+ # configuration
27
+ PABLO_OPTIONS = {
28
+ '-a' => { long: 'author' },
29
+ '-l' => { long: 'lang', keyword: 'LOCALE' },
30
+ '-t' => { long: 'title' },
31
+ '-p' => { long: 'path' },
32
+ '-d' => { long: 'directory', boolean: true },
33
+ '-v' => { long: 'verbose', boolean: true, meth: :on_tail },
34
+ '-h' => { long: 'help', boolean: true, meth: :on_tail },
35
+ '-V' => { long: 'version', boolean: true, meth: :on_tail }
36
+ }.freeze
37
+
38
+ # @return [Hash] the possible ~pablo~ subcommands and their
39
+ # configuration
40
+ PABLO_COMMANDS = {
41
+ 'init' => { opts: ['-a', '-l', '-t', '-v', '-h'] },
42
+ 'config' => { alias: 'init' },
43
+ 'preview' => { opts: ['-h'] },
44
+ 'open' => { opts: ['-a', '-l', '-t', '-d', '-p', '-v', '-h'] },
45
+ 'edit' => { alias: 'open' },
46
+ 'build' => { opts: ['-h'] },
47
+ 'publish' => { opts: ['-h'] },
48
+ 'help' => { opts: ['-h'] },
49
+ 'basic' => { opts: ['-h', '-V'], label: '<command>' }
50
+ }.freeze
51
+
52
+ class << self
53
+ # Animates strings in the user console to alert him that something
54
+ # is running in the background.
55
+ #
56
+ # The animation is chosen among a bunch of themes, with the
57
+ # configuration option ~throbber~ (retrieved via
58
+ # {Neruda::Config#settings}).
59
+ #
60
+ # @example
61
+ # long_stuff = Thread.new { very_long_operation }
62
+ # Neruda::Utils.throbber(long_stuff, 'Computing hard stuff:')
63
+ #
64
+ # @param thread [Thread] the long-running operation to decorate
65
+ # @param message [String] the message to display before the throbber
66
+ # @return [void]
67
+ def throbber(thread, message)
68
+ frames = select_throbber_frames
69
+ begin
70
+ run_and_decorate_thread thread, message, frames
71
+ rescue RuntimeError => e
72
+ throbber_error message
73
+ raise e
74
+ else
75
+ done = Rainbow('done'.ljust(frames[0].length)).green
76
+ puts "#{message} #{done}"
77
+ end
78
+ end
79
+
80
+ # Returns the short and long options specification for a given
81
+ # short option.
82
+ #
83
+ # This method use the {Neruda::Utils::PABLO_OPTIONS} Hash to
84
+ # retrieve corresponding values.
85
+ #
86
+ # @example
87
+ # spec = Neruda::Utils.decorate_option('-a')
88
+ # => ['-a AUTHOR', '--author AUTHOR']
89
+ #
90
+ # @param short [String] the short option to decorate
91
+ # @return [Array] the short and long specification for an option
92
+ def decorate_option(short)
93
+ opt = Neruda::Utils::PABLO_OPTIONS[short]
94
+ long = "--#{opt[:long]}"
95
+ return [short, long] if opt[:boolean]
96
+ key = opt[:keyword] || opt[:long].upcase
97
+ [short + key, format('%<long>s %<key>s', long: long, key: key)]
98
+ end
99
+
100
+ # Returns the ~pablo~ help summary for a given command.
101
+ #
102
+ # @param command [String] the command for which a summary
103
+ # should be given
104
+ # @return [String]
105
+ def summarize_command(command)
106
+ Neruda::Utils::PABLO_COMMANDS[command][:opts].map do |k|
107
+ short, long = Neruda::Utils.decorate_option(k)
108
+ opt = Neruda::Utils::PABLO_OPTIONS[k]
109
+ label = [short, long].join(', ')
110
+ line = [format(' %<opt>s', opt: label).ljust(30)]
111
+ if R18n.t.pablo.options[opt[:long]].translated?
112
+ line << R18n.t.pablo.options[opt[:long]]
113
+ end
114
+ line.join(' ')
115
+ end.join("\n")
116
+ end
117
+
118
+ # Returns a formatted list of available commands for ~pablo~.
119
+ #
120
+ # @return [String]
121
+ def list_commands
122
+ lines = []
123
+ Neruda::Utils::PABLO_COMMANDS.each do |cmd, opt|
124
+ next if cmd == 'basic'
125
+ line = [' ', cmd.ljust(10)]
126
+ if opt.has_key? :alias
127
+ line << R18n.t.pablo.commands.alias(opt[:alias])
128
+ else
129
+ line << R18n.t.pablo.commands[cmd]
130
+ end
131
+ lines << line.join(' ')
132
+ end
133
+ lines.join("\n")
134
+ end
135
+
136
+ # Returns the real command name for a given command, which may be
137
+ # an alias.
138
+ #
139
+ # @param command [String] the command to resolve
140
+ # @return [String]
141
+ def resolve_possible_alias(command)
142
+ return 'basic' unless Neruda::Utils::PABLO_COMMANDS.include?(command)
143
+ cmd_opt = Neruda::Utils::PABLO_COMMANDS[command]
144
+ return cmd_opt[:alias] if cmd_opt.has_key?(:alias)
145
+ command
146
+ end
147
+
148
+ # Try to discover the current host operating system.
149
+ #
150
+ # @return [String] either apple, windows or linux (default)
151
+ # :nocov:
152
+ def current_os
153
+ if ENV['OS'] == 'Windows_NT' || RUBY_PLATFORM =~ /cygwin/
154
+ return 'windows'
155
+ end
156
+ return 'apple' if RUBY_PLATFORM =~ /darwin/
157
+ 'linux'
158
+ end
159
+ # :nocov:
160
+
161
+ # Download latest org-mode tarball.
162
+ #
163
+ # @return [String] the downloaded org-mode version
164
+ def download_org
165
+ # :nocov:
166
+ return if Neruda::Config.org_last_version.nil?
167
+ # :nocov:
168
+ tarball = "org-#{Neruda::Config.org_last_version}.tar.gz"
169
+ dest_file = "tmp/#{tarball}"
170
+ return if File.exist?(dest_file)
171
+ uri = URI("https://orgmode.org/#{tarball}")
172
+ # Will crash on purpose if anything goes wrong
173
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http|
174
+ http.request(Net::HTTP::Get.new(uri)) do |response|
175
+ File.open(dest_file, 'w') do |io|
176
+ response.read_body { |chunk| io.write chunk }
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ private
183
+
184
+ def throbber_error(message)
185
+ warn(
186
+ format(
187
+ "%<message>s %<label>s\n%<explanation>s",
188
+ message: message,
189
+ label: Rainbow(R18n.t.neruda.error.label).bold.red,
190
+ explanation: Rainbow(R18n.t.neruda.error.explanation).bold
191
+ )
192
+ )
193
+ end
194
+
195
+ def select_throbber_frames
196
+ model = Neruda::Config.settings['throbber'] || 'default'
197
+ model = 'default' unless Neruda::Utils::THROBBER_FRAMES.has_key?(model)
198
+ Neruda::Utils::THROBBER_FRAMES[model]
199
+ end
200
+
201
+ def run_and_decorate_thread(thread, message, frames)
202
+ thread.abort_on_exception = true
203
+ current = 0
204
+ while thread.alive?
205
+ sleep 0.1
206
+ print "#{message} #{frames[current % frames.length]}\r"
207
+ current += 1
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end