neruda 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,6 +7,7 @@ require 'neruda/org_file/htmlizer'
7
7
  require 'neruda/org_file/extracter'
8
8
  require 'neruda/org_file/class_methods'
9
9
  require 'neruda/index'
10
+ require 'neruda/version'
10
11
 
11
12
  module Neruda
12
13
  # Handles org files.
@@ -19,6 +20,10 @@ module Neruda
19
20
  # the ~#+title:~ header.
20
21
  attr_reader :title
21
22
 
23
+ # @return [String] the subtitle of the current org document, taken
24
+ # from the ~#+subtitle:~ header.
25
+ attr_reader :subtitle
26
+
22
27
  # @return [DateTime] the date and time of the current org document,
23
28
  # taken from the ~#+date:~ header.
24
29
  attr_reader :date
@@ -40,6 +45,10 @@ module Neruda
40
45
  # taken from the ~#+keywords:~ header.
41
46
  attr_reader :keywords
42
47
 
48
+ # @return [String] the description of this org document, taken from
49
+ # the ~#+description:~ header.
50
+ attr_reader :excerpt
51
+
43
52
  # The locale of the current org document, taken from the
44
53
  # ~#+language:~ header.
45
54
  #
@@ -61,9 +70,8 @@ module Neruda
61
70
  # settings and the above {#html_file @html_file} attribute.
62
71
  attr_reader :url
63
72
 
64
- # @return [String] the description of this org document, taken from
65
- # the ~#+description:~ header.
66
- attr_reader :excerpt
73
+ # @return [String] the project owning this document.
74
+ attr_reader :project
67
75
 
68
76
  extend Neruda::OrgFileClassMethods
69
77
 
@@ -96,21 +104,25 @@ module Neruda
96
104
  # o.title
97
105
  # => "New file"
98
106
  #
99
- # @param file_name [String] path to the corresponding org mode file
100
- # @param opts [Hash] optional data to initialize new org file
101
- # @option opts [String] title ('') the title of the new org file
107
+ # @param file_name [String] path to the corresponding Org file
108
+ # @param opts [Hash] optional data to initialize new Org file
109
+ # @option opts [String] title ('') the title of the new Org file
102
110
  # @option opts [String] author (system user or '') the author of the
103
111
  # document
104
112
  # @option opts [Boolean] verbose (false) if the
105
113
  # {Neruda::OrgFileHtmlizer#publish publish} method should output
106
114
  # emacs process messages
115
+ # @option opts [String] project the project owning this file
116
+ # must be stored
107
117
  # @return [Neruda::OrgFile] the new instance of Neruda::OrgFile
108
118
  def initialize(file_name, opts = {})
109
119
  file_name = nil if file_name == ''
110
120
  @file = file_name
111
- @html_file = Neruda::OrgFile.html_file @file
112
- @url = Neruda::OrgFile.html_file_with_domain @file
121
+ @html_file = nil
122
+ @url = nil
123
+ @project = opts.delete :project
113
124
  @options = opts
125
+ build_html_file_and_url
114
126
  if @file && File.exist?(@file)
115
127
  extract_data
116
128
  else
@@ -166,7 +178,7 @@ module Neruda
166
178
  # @param year [Boolean] wether or not the ~:full~ format must
167
179
  # contain the year
168
180
  # @return [String] the document DateTime string representation
169
- def datestring(dateformat = :full, year = true)
181
+ def datestring(dateformat = :full, year: true)
170
182
  return '' if @date.nil?
171
183
  return R18n.l @date.to_date if dateformat == :short
172
184
  return @date.rfc3339 if dateformat == :rfc3339
@@ -202,6 +214,10 @@ module Neruda
202
214
  # - %l :: the lang of the document
203
215
  # - %L :: the license information, taken from the
204
216
  # {Neruda::Config#settings}
217
+ # - %n :: the Neruda name and version
218
+ # - %N :: the Neruda name and version with a link to the project
219
+ # home on the name
220
+ # - %s :: the subtitle of the document
205
221
  # - %t :: the title of the document
206
222
  # - %u :: the web path to the related published HTML document
207
223
  # - %x :: the raw description (eXcerpt)
@@ -213,8 +229,9 @@ module Neruda
213
229
  # => "Article written by Alice Smith the Wednesday 3rd July"
214
230
  #
215
231
  # @return [String] the given ~string~ after replacement occurs
232
+ # rubocop:disable Metrics/MethodLength
233
+ # rubocop:disable Layout/LineLength
216
234
  def format(string)
217
- license = Neruda::Config.settings['license'] || ''
218
235
  string.gsub('%a', @author)
219
236
  .gsub('%A', author_to_html)
220
237
  .gsub('%d', date_to_html(:short))
@@ -224,12 +241,17 @@ module Neruda
224
241
  .gsub('%k', @keywords.join(', '))
225
242
  .gsub('%K', keywords_to_html)
226
243
  .gsub('%l', @lang)
227
- .gsub('%L', license.gsub(/\s+/, ' ').strip)
244
+ .gsub('%L', (Neruda::Config.settings['license'] || '').gsub(/\s+/, ' ').strip)
245
+ .gsub('%n', "Neruda #{Neruda::VERSION}")
246
+ .gsub('%N', "<a href=\"https://git.umaneti.net/neruda/about/\">Neruda</a> #{Neruda::VERSION}")
247
+ .gsub('%s', @subtitle)
228
248
  .gsub('%t', @title)
229
- .gsub('%u', @html_file)
249
+ .gsub('%u', @html_file || '')
230
250
  .gsub('%x', @excerpt)
231
251
  .gsub('%X', "<p>#{@excerpt}</p>")
232
252
  end
253
+ # rubocop:enable Layout/LineLength
254
+ # rubocop:enable Metrics/MethodLength
233
255
 
234
256
  # Writes the current OrgFile content to the underlying file.
235
257
  #
@@ -246,20 +268,31 @@ module Neruda
246
268
 
247
269
  private
248
270
 
271
+ def build_html_file_and_url
272
+ return if @file.nil?
273
+ @html_file = Neruda::OrgFile.target_for_source(
274
+ @file, @project, with_public_folder: false
275
+ )
276
+ @url = "#{Neruda::Config.settings['domain']}/#{@html_file}"
277
+ end
278
+
249
279
  def init_empty_file
250
280
  @title = @options[:title] || ''
281
+ @subtitle = ''
251
282
  @date = DateTime.now
252
283
  @notime = false
253
284
  @author = @options[:author] || Neruda::Config.settings['author']
254
285
  @keywords = []
255
286
  @lang = @options[:lang] || Neruda::Config.settings['lang']
256
287
  @excerpt = ''
257
- @content = @options[:content] || <<~ORG
288
+ body = @options[:content] || ''
289
+ @content = @options[:raw_content] || <<~ORG
258
290
  #+title: #{@title}
259
291
  #+date: <#{@date.strftime('%Y-%m-%d %a. %H:%M:%S')}>
260
292
  #+author: #{@author}
261
293
  #+language: #{@lang}
262
294
 
295
+ #{body}
263
296
  ORG
264
297
  end
265
298
  end
@@ -3,33 +3,50 @@
3
3
  module Neruda
4
4
  # This module holds class methods for the {Neruda::OrgFile} class.
5
5
  module OrgFileClassMethods
6
- def html_file(file_name)
7
- return nil if file_name.nil?
8
- path = Neruda::OrgFile.target_for_source(file_name)
9
- pubfolder = Neruda::Config.settings['public_folder']
10
- path.sub(/^#{pubfolder}\//, '/')
11
- end
12
-
13
- def html_file_with_domain(file_name)
14
- return nil if file_name.nil?
15
- Neruda::Config.settings['domain'] + html_file(file_name)
16
- end
17
-
18
6
  def source_for_target(file_name)
19
7
  # file_name may be frozen...
20
8
  src = file_name.sub(/\.html$/, '.org')
21
9
  pubfolder = Neruda::Config.settings['public_folder']
22
- src.sub(/^#{pubfolder}\//, 'src/')
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
23
  end
24
24
 
25
- def target_for_source(file_name)
25
+ def target_for_source(file_name, project, with_public_folder: true)
26
+ return nil if file_name.nil?
26
27
  # file_name may be frozen...
27
- target = file_name.sub(/\.org$/, '.html')
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
28
39
  pubfolder = Neruda::Config.settings['public_folder']
29
- return target.sub(/^src\//, "#{pubfolder}/") if /^src\//.match?(target)
30
- subfolder = File.basename(File.dirname(target))
31
- leaf = File.basename(target)
32
- "#{pubfolder}/#{subfolder}/#{leaf}"
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
33
50
  end
34
51
 
35
52
  def slug(title)
@@ -10,6 +10,7 @@ module Neruda
10
10
  def extract_data
11
11
  @content = IO.read @file
12
12
  @title = extract_title
13
+ @subtitle = extract_subtitle
13
14
  @date = extract_date
14
15
  @author = extract_author
15
16
  @keywords = extract_keywords
@@ -32,7 +33,17 @@ module Neruda
32
33
 
33
34
  def extract_title
34
35
  m = /^#\+title:(.+)$/i.match(@content)
35
- return @file if m.nil?
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?
36
47
  m[1].strip
37
48
  end
38
49
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'neruda/config'
4
+ require 'neruda/emacs'
4
5
 
5
6
  module Neruda
6
7
  # This module holds HTML formatter methods for the {Neruda::OrgFile}
@@ -11,12 +12,9 @@ module Neruda
11
12
  #
12
13
  # @return [Boolean, nil] the underlying ~system~ method return value
13
14
  def publish
14
- if @file.nil?
15
- emacs_args = ['--eval \'(org-publish "website")\'']
16
- else
17
- emacs_args = ['-f org-publish-current-file']
18
- end
19
- call_emacs emacs_args
15
+ Neruda::Emacs.new(
16
+ file_path: @file, verbose: @options[:verbose]
17
+ ).publish
20
18
  end
21
19
 
22
20
  private
@@ -40,7 +38,7 @@ module Neruda
40
38
  #
41
39
  # @return [String] the HTML `time` tag
42
40
  def date_to_html(dateformat = :full)
43
- return '' if @date.nil?
41
+ return '<time></time>' if @date.nil?
44
42
  "<time datetime=\"#{@date.rfc3339}\">#{datestring(dateformat)}</time>"
45
43
  end
46
44
 
@@ -49,30 +47,7 @@ module Neruda
49
47
  #
50
48
  # @return [String] the author HTML `span`
51
49
  def author_to_html
52
- return '' if @author == ''
53
50
  "<span class=\"author\">#{@author}</span>"
54
51
  end
55
-
56
- def emacs_command(arguments = [])
57
- default_emacs = Neruda::Config.settings['emacs']
58
- emacs_cmd = [default_emacs || 'emacs -Q --batch -nw']
59
- emacs_cmd << '--eval \'(setq enable-dir-local-variables nil)\''
60
- unless @options[:verbose]
61
- emacs_cmd << '--eval \'(setq inhibit-message t)\''
62
- end
63
- emacs_cmd << '-l ./org-config.el'
64
- emacs_cmd << "--eval '(find-file \"#{@file}\")'" unless @file.nil?
65
- emacs_cmd.concat(arguments)
66
- emacs_cmd.join(' ')
67
- end
68
-
69
- def call_emacs(arguments = [])
70
- command = emacs_command arguments
71
- if @options[:verbose]
72
- warn command
73
- return system(command, exception: true)
74
- end
75
- system command, out: '/dev/null', err: '/dev/null', exception: true
76
- end
77
52
  end
78
53
  end
@@ -19,11 +19,13 @@ module Neruda # rubocop:disable Style/Documentation
19
19
  private
20
20
 
21
21
  def local_path(requested_path)
22
- routes = Neruda::Config.settings['routes'] || {}
22
+ routes = Neruda::Config.settings.dig('preview', 'routes') || {}
23
23
  return routes[requested_path] if routes.keys.include? requested_path
24
24
  local_path = Neruda::Config.settings['public_folder'] + requested_path
25
25
  if File.directory? local_path
26
- local_path = local_path.delete_suffix('/') + '/index.html'
26
+ local_path = format(
27
+ '%<path>s/index.html', path: local_path.delete_suffix('/')
28
+ )
27
29
  end
28
30
  return local_path if File.exist? local_path
29
31
  raise WEBrick::HTTPStatus::NotFound, 'Not found.'
@@ -34,8 +36,8 @@ module Neruda # rubocop:disable Style/Documentation
34
36
  return body unless local_path.match?(/\.(?:ht|x)ml$/)
35
37
  domain = Neruda::Config.settings['domain']
36
38
  return body if domain == ''
37
- body.gsub(/"file:\/\//, '"' + local_host)
38
- .gsub(/"#{domain}/, '"' + local_host)
39
+ body.gsub(/"file:\/\//, format('"%<host>s', host: local_host))
40
+ .gsub(/"#{domain}/, format('"%<host>s', host: local_host))
39
41
  end
40
42
  end
41
43
 
@@ -43,7 +45,7 @@ module Neruda # rubocop:disable Style/Documentation
43
45
  def start_preview
44
46
  # Inspired by ruby un.rb library, which allows normally to start a
45
47
  # webrick server in one line: ruby -run -e httpd public_html -p 5000
46
- port = Neruda::Config.settings['server_port'] || 5000
48
+ port = Neruda::Config.settings.dig('preview', 'server_port') || 5000
47
49
  s = WEBrick::HTTPServer.new(Port: port)
48
50
  s.mount '/', Neruda::PreviewServlet
49
51
  ['TERM', 'QUIT', 'INT'].each { |sig| trap(sig, proc { s.shutdown }) }
@@ -99,9 +99,10 @@ module Neruda
99
99
  end
100
100
 
101
101
  def insert_new_node_at(elem, content)
102
- if @position == 'before'
102
+ case @position
103
+ when 'before'
103
104
  elem.add_previous_sibling content
104
- elsif @position == 'replace'
105
+ when 'replace'
105
106
  elem.replace content
106
107
  else
107
108
  elem.add_next_sibling content
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uri'
3
4
  require 'rainbow'
5
+ require 'net/http'
6
+ require 'r18n-core'
4
7
  require 'neruda/config'
5
8
 
6
9
  module Neruda
@@ -25,33 +28,24 @@ module Neruda
25
28
  '-a' => { long: 'author' },
26
29
  '-l' => { long: 'lang', keyword: 'LOCALE' },
27
30
  '-t' => { long: 'title' },
28
- '-p' => { long: 'path', desc: 'Path to the new file' },
29
- '-d' => { long: 'directory', boolean: true,
30
- desc: 'Wrap the new org file in a named folder' },
31
+ '-p' => { long: 'path' },
32
+ '-d' => { long: 'directory', boolean: true },
31
33
  '-v' => { long: 'verbose', boolean: true, meth: :on_tail },
32
- '-h' => { long: 'help', boolean: true, meth: :on_tail,
33
- desc: 'Display help for a command and exit' },
34
- '-V' => { long: 'version', boolean: true, meth: :on_tail,
35
- desc: 'Display Neruda version and exit' }
34
+ '-h' => { long: 'help', boolean: true, meth: :on_tail },
35
+ '-V' => { long: 'version', boolean: true, meth: :on_tail }
36
36
  }.freeze
37
37
 
38
38
  # @return [Hash] the possible ~pablo~ subcommands and their
39
39
  # configuration
40
40
  PABLO_COMMANDS = {
41
- 'init' => { opts: ['-a', '-l', '-t', '-v', '-h'],
42
- desc: 'Initialize your Neruda instance ' \
43
- '(you just need to do it once).' },
41
+ 'init' => { opts: ['-a', '-l', '-t', '-v', '-h'] },
44
42
  'config' => { alias: 'init' },
45
- 'preview' => { opts: ['-h'],
46
- desc: 'Start a test webserver to preview ' \
47
- 'your website on http://127.0.0.1:5000' },
48
- 'open' => { opts: ['-a', '-l', '-t', '-d', '-p', '-v', '-h'],
49
- desc: 'Open or create an org file for edition.' },
43
+ 'preview' => { opts: ['-h'] },
44
+ 'open' => { opts: ['-a', '-l', '-t', '-d', '-p', '-v', '-h'] },
50
45
  'edit' => { alias: 'open' },
51
- 'publish' => { opts: ['-h'],
52
- desc: 'Push local changes to your public ' \
53
- 'server online.' },
54
- 'help' => { opts: ['-h'], desc: 'Alias for the -h switch.' },
46
+ 'build' => { opts: ['-h'] },
47
+ 'publish' => { opts: ['-h'] },
48
+ 'help' => { opts: ['-h'] },
55
49
  'basic' => { opts: ['-h', '-V'], label: '<command>' }
56
50
  }.freeze
57
51
 
@@ -75,10 +69,7 @@ module Neruda
75
69
  begin
76
70
  run_and_decorate_thread thread, message, frames
77
71
  rescue RuntimeError => e
78
- done = Rainbow('An error occured.').bold.red + "\n"
79
- done += Rainbow('To see it, run again your command with more ' \
80
- 'verbosity, i.e. pablo build -v').bold
81
- warn "#{message} #{done}"
72
+ throbber_error message
82
73
  raise e
83
74
  else
84
75
  done = Rainbow('done'.ljust(frames[0].length)).green
@@ -103,7 +94,7 @@ module Neruda
103
94
  long = "--#{opt[:long]}"
104
95
  return [short, long] if opt[:boolean]
105
96
  key = opt[:keyword] || opt[:long].upcase
106
- [short + key, long + ' ' + key]
97
+ [short + key, format('%<long>s %<key>s', long: long, key: key)]
107
98
  end
108
99
 
109
100
  # Returns the ~pablo~ help summary for a given command.
@@ -115,22 +106,31 @@ module Neruda
115
106
  Neruda::Utils::PABLO_COMMANDS[command][:opts].map do |k|
116
107
  short, long = Neruda::Utils.decorate_option(k)
117
108
  opt = Neruda::Utils::PABLO_OPTIONS[k]
118
- line = ' ' + [short, long].join(', ')
119
- line = line.ljust(34) + " #{opt[:desc]}" if opt.has_key?(:desc)
120
- line + "\n"
121
- end.join
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")
122
116
  end
123
117
 
124
118
  # Returns a formatted list of available commands for ~pablo~.
125
119
  #
126
120
  # @return [String]
127
121
  def list_commands
128
- lines = ''
122
+ lines = []
129
123
  Neruda::Utils::PABLO_COMMANDS.each do |cmd, opt|
130
124
  next if cmd == 'basic'
131
- lines += " #{cmd.ljust(10)} #{opt[:desc]}\n"
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
132
  end
133
- lines
133
+ lines.join("\n")
134
134
  end
135
135
 
136
136
  # Returns the real command name for a given command, which may be
@@ -148,6 +148,7 @@ module Neruda
148
148
  # Try to discover the current host operating system.
149
149
  #
150
150
  # @return [String] either apple, windows or linux (default)
151
+ # :nocov:
151
152
  def current_os
152
153
  if ENV['OS'] == 'Windows_NT' || RUBY_PLATFORM =~ /cygwin/
153
154
  return 'windows'
@@ -155,9 +156,42 @@ module Neruda
155
156
  return 'apple' if RUBY_PLATFORM =~ /darwin/
156
157
  'linux'
157
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
158
181
 
159
182
  private
160
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
+
161
195
  def select_throbber_frames
162
196
  model = Neruda::Config.settings['throbber'] || 'default'
163
197
  model = 'default' unless Neruda::Utils::THROBBER_FRAMES.has_key?(model)