fronde 0.3.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/bin/fronde +15 -30
  3. data/lib/ext/nil_time.rb +25 -0
  4. data/lib/ext/r18n.rb +37 -0
  5. data/lib/ext/time.rb +39 -0
  6. data/lib/ext/time_no_time.rb +23 -0
  7. data/lib/fronde/cli/commands.rb +97 -104
  8. data/lib/fronde/cli/data/Rakefile +8 -0
  9. data/lib/fronde/cli/data/config.yml +13 -0
  10. data/lib/fronde/cli/data/gitignore +6 -0
  11. data/lib/fronde/cli/data/zsh_completion +37 -0
  12. data/lib/fronde/cli/helpers.rb +55 -0
  13. data/lib/fronde/cli/opt_parse.rb +140 -0
  14. data/lib/fronde/cli/throbber.rb +110 -0
  15. data/lib/fronde/cli.rb +42 -42
  16. data/lib/fronde/config/data/org-config.el +25 -0
  17. data/lib/fronde/config/data/ox-fronde.el +158 -0
  18. data/lib/fronde/config/data/themes/umaneti/css/htmlize.css +364 -0
  19. data/lib/fronde/config/data/themes/umaneti/css/style.css +250 -0
  20. data/lib/fronde/config/data/themes/umaneti/img/bottom.png +0 -0
  21. data/lib/fronde/config/data/themes/umaneti/img/content.png +0 -0
  22. data/lib/fronde/config/data/themes/umaneti/img/tic.png +0 -0
  23. data/lib/fronde/config/data/themes/umaneti/img/top.png +0 -0
  24. data/lib/fronde/config/helpers.rb +62 -0
  25. data/lib/fronde/config/lisp.rb +80 -0
  26. data/lib/fronde/config.rb +148 -98
  27. data/lib/fronde/emacs.rb +23 -20
  28. data/lib/fronde/index/atom_generator.rb +55 -66
  29. data/lib/fronde/index/data/all_tags.org +19 -0
  30. data/lib/fronde/index/data/template.org +26 -0
  31. data/lib/fronde/index/data/template.xml +37 -0
  32. data/lib/fronde/index/org_generator.rb +72 -88
  33. data/lib/fronde/index.rb +57 -86
  34. data/lib/fronde/org/file.rb +299 -0
  35. data/lib/fronde/org/file_extracter.rb +101 -0
  36. data/lib/fronde/org.rb +105 -0
  37. data/lib/fronde/preview.rb +43 -39
  38. data/lib/fronde/slug.rb +54 -0
  39. data/lib/fronde/source/gemini.rb +34 -0
  40. data/lib/fronde/source/html.rb +67 -0
  41. data/lib/fronde/source.rb +209 -0
  42. data/lib/fronde/sync/neocities.rb +220 -0
  43. data/lib/fronde/sync/rsync.rb +46 -0
  44. data/lib/fronde/sync.rb +32 -0
  45. data/lib/fronde/templater.rb +101 -71
  46. data/lib/fronde/version.rb +1 -1
  47. data/lib/tasks/cli.rake +33 -0
  48. data/lib/tasks/org.rake +58 -43
  49. data/lib/tasks/site.rake +66 -31
  50. data/lib/tasks/sync.rake +37 -40
  51. data/lib/tasks/tags.rake +11 -7
  52. data/locales/en.yml +61 -14
  53. data/locales/fr.yml +69 -14
  54. metadata +77 -95
  55. data/lib/fronde/config/lisp_config.rb +0 -340
  56. data/lib/fronde/config/org-config.el +0 -19
  57. data/lib/fronde/config/ox-fronde.el +0 -121
  58. data/lib/fronde/org_file/class_methods.rb +0 -72
  59. data/lib/fronde/org_file/extracter.rb +0 -72
  60. data/lib/fronde/org_file/htmlizer.rb +0 -43
  61. data/lib/fronde/org_file.rb +0 -298
  62. data/lib/fronde/utils.rb +0 -229
@@ -0,0 +1,299 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require_relative '../../ext/nil_time'
5
+ require_relative '../../ext/time'
6
+ using TimePatch
7
+
8
+ require 'nokogiri'
9
+ require 'fileutils'
10
+
11
+ require_relative '../config'
12
+ require_relative '../version'
13
+ require_relative '../slug'
14
+ require_relative 'file_extracter'
15
+
16
+ module Fronde
17
+ module Org
18
+ # Handles org files.
19
+ #
20
+ # This class is responsible for reading or writing existing or new
21
+ # org files, and formating their content to be used on the generated
22
+ # website.
23
+ class File
24
+ # @return [String] the relative path to the source of this
25
+ # document.
26
+ attr_reader :file
27
+
28
+ # @return [Hash] the project owning this document.
29
+ attr_reader :project
30
+
31
+ include FileExtracter
32
+
33
+ # Prepares the file named by ~file_name~ for read and write
34
+ # operations.
35
+ #
36
+ # If the file ~file_name~ does not exist, the new instance may be
37
+ # populated by data given in the ~opts~ parameter.
38
+ #
39
+ # @example
40
+ # File.exist? './test.org'
41
+ # => true
42
+ # o = Fronde::Org::File.new('./test.org')
43
+ # => #<Fronde::Org::File @file='./test.org'...>
44
+ # o.title
45
+ # => "This is an existing test file"
46
+ # File.exist? '/tmp/does_not_exist.org'
47
+ # => false
48
+ # o = Fronde::Org::File.new('/tmp/does_not_exist.org')
49
+ # => #<Fronde::Org::File @file='/tmp/does_not_exist.org'...>
50
+ # o.title
51
+ # => ""
52
+ # File.exist? '/tmp/other.org'
53
+ # => false
54
+ # o = Fronde::Org::File.new('/tmp/other.org', title: 'New file')
55
+ # => #<Fronde::Org::File @file='/tmp/other.org'...>
56
+ # o.title
57
+ # => "New file"
58
+ #
59
+ # @param file_name [String] path to the corresponding Org file
60
+ # @param opts [Hash] optional data to initialize new Org file
61
+ # @option opts [String] title ('') the title of the new Org file
62
+ # @option opts [String] author (system user or '') the author of
63
+ # the document
64
+ # @return [Fronde::Org::File] the new instance of
65
+ # Fronde::Org::File
66
+ def initialize(file_name, opts = {})
67
+ file_name ||= ''
68
+ @file = ::File.expand_path file_name
69
+ @options = opts
70
+ @project = find_source
71
+ @data = {}
72
+ if ::File.file?(@file)
73
+ extract_data
74
+ else
75
+ init_empty_file
76
+ end
77
+ end
78
+
79
+ # Returns a String representation of the document date, which aims
80
+ # to be used to sort several Org::Files.
81
+ #
82
+ # The format used for the key is ~%Y%m%d%H%M%S~. If the current
83
+ # Org::File instance does not have a date, this mehod return
84
+ # ~00000000000000~. If the current Org::File instance does not
85
+ # have time information, the date is padded with zeros.
86
+ #
87
+ # @example with the org header ~#+date: <2019-07-03 Wed 20:52:49>~
88
+ # org_file.date
89
+ # => #<Time: 2019-07-03T20:52:49+02:00...>
90
+ # org_file.timekey
91
+ # => "20190703205349"
92
+ #
93
+ # @example with the org header ~#+date: <2019-07-03 Wed>~
94
+ # org_file.date
95
+ # => #<Time: 2019-07-03T00:00:00+02:00...>
96
+ # org_file.timekey
97
+ # => "20190703000000"
98
+ #
99
+ # @example with no date header in the org file
100
+ # org_file.date
101
+ # => nil
102
+ # org_file.timekey
103
+ # => "00000000000000"
104
+ #
105
+ # @return [String] the document key
106
+ def timekey
107
+ return '00000000000000' if @data[:date].is_a? NilTime
108
+
109
+ @data[:date].strftime('%Y%m%d%H%M%S')
110
+ end
111
+
112
+ # Formats given ~string~ with values of the current Org::File.
113
+ #
114
+ # This method expects to find percent-tags in the given ~string~
115
+ # and replace them by their corresponding value.
116
+ #
117
+ # It reuses the same tags than the ~org-html-format-spec~ method.
118
+ #
119
+ # *** Format:
120
+ #
121
+ # - %a :: the raw author name.
122
+ # - %A :: the HTML rendering of the author name, equivalent to
123
+ # ~<span class="author">%a</span>~.
124
+ # - %d :: the ~:short~ date HTML representation, equivalent
125
+ # to ~<time datetime="%I">%i</time>~.
126
+ # - %D :: the ~:full~ date and time HTML representation.
127
+ # - %F :: the ~link~ HTML tag for the main Atom feed of the
128
+ # current file source.
129
+ # - %h :: the declared host/domain name, taken from the
130
+ # {Fronde::Config#settings}.
131
+ # - %i :: the raw ~:short~ date and time.
132
+ # - %I :: the raw ~:iso8601~ date and time.
133
+ # - %k :: the document keywords separated by commas.
134
+ # - %K :: the HTML list rendering of the keywords.
135
+ # - %l :: the lang of the document.
136
+ # - %L :: the license information, taken from the
137
+ # {Fronde::Config#settings}.
138
+ # - %n :: the fronde name and version.
139
+ # - %N :: the fronde name and version with a link to the project
140
+ # home on the name.
141
+ # - %o :: the theme name (~o~ as in Outfit) of the current file source.
142
+ # - %s :: the subtitle of the document (from ~#+subtitle:~).
143
+ # - %t :: the title of the document (from ~#+title:~).
144
+ # - %u :: the URL to the related published HTML document.
145
+ # - %x :: the raw description (~x~ as in eXcerpt) of the document
146
+ # (from ~#+description:~).
147
+ # - %X :: the description, enclosed in an HTML ~p~ tag, equivalent
148
+ # to ~<p>%x</p>~.
149
+ #
150
+ # @example
151
+ # org_file.format("Article written by %a the %d")
152
+ # => "Article written by Alice Smith the Wednesday 3rd July"
153
+ #
154
+ # @return [String] the given ~string~ after replacement occurs
155
+ # rubocop:disable Layout/LineLength
156
+ def format(string)
157
+ project_data = @project.to_h
158
+ # NOTE: The following keycode are reserved by Org itself:
159
+ # %a (author), %c (creator), %C (input-file), %d (date),
160
+ # %e (email), %s (subtitle), %t (title), %T (timestamp),
161
+ # %v (html validation link)
162
+ string.gsub('%a', @data[:author])
163
+ .gsub('%A', "<span class=\"author\">#{@data[:author]}</span>")
164
+ .gsub('%d', @data[:date].l18n_short_date_html)
165
+ .gsub('%D', @data[:date].l18n_long_date_html)
166
+ .gsub('%F', project_data['atom_feed'] || '')
167
+ .gsub('%h', project_data['domain'] || '')
168
+ .gsub('%i', @data[:date].l18n_short_date_string)
169
+ .gsub('%I', @data[:date].xmlschema)
170
+ .gsub('%k', @data[:keywords].join(', '))
171
+ .gsub('%K', keywords_to_html)
172
+ .gsub('%l', @data[:lang])
173
+ .gsub('%L', Fronde::CONFIG.get('license', '').gsub(/\s+/, ' ').strip)
174
+ .gsub('%n', "Fronde #{Fronde::VERSION}")
175
+ .gsub('%N', "<a href=\"https://git.umaneti.net/fronde/about/\">Fronde</a> #{Fronde::VERSION}")
176
+ .gsub('%o', project_data['theme'] || '')
177
+ .gsub('%s', @data[:subtitle])
178
+ .gsub('%t', @data[:title])
179
+ .gsub('%u', @data[:url] || '')
180
+ .gsub('%x', @data[:excerpt])
181
+ .gsub('%X', "<p>#{@data[:excerpt]}</p>")
182
+ end
183
+ # rubocop:enable Layout/LineLength
184
+
185
+ # Writes the current Org::File content to the underlying file.
186
+ #
187
+ # The intermediate parent folders are created if necessary.
188
+ #
189
+ # @return [Integer] the length written (as returned by the
190
+ # underlying ~File.write~ method call)
191
+ def write
192
+ if ::File.directory? @file
193
+ if @data[:title] == ''
194
+ raise R18n.t.fronde.error.org_file.no_file_or_title
195
+ end
196
+
197
+ @file = ::File.join @file, "#{Slug.slug(@data[:title])}.org"
198
+ else
199
+ file_dir = ::File.dirname @file
200
+ FileUtils.mkdir_p file_dir
201
+ end
202
+ ::File.write @file, @data[:content]
203
+ end
204
+
205
+ def method_missing(method_name, *args, &block)
206
+ reader_method = method_name.to_s.delete_suffix('=').to_sym
207
+ if @data.has_key? reader_method
208
+ return @data[reader_method] if reader_method == method_name
209
+
210
+ return @data[reader_method] = args.first
211
+ end
212
+ super
213
+ end
214
+
215
+ def respond_to_missing?(method_name, include_private = false)
216
+ return true if @data.has_key? method_name
217
+
218
+ reader_method = method_name.to_s.delete_suffix('=').to_sym
219
+ return true if @data.has_key? reader_method
220
+
221
+ super
222
+ end
223
+
224
+ def to_h
225
+ fields = %w[author excerpt keywords timekey title url]
226
+ data = fields.to_h { |key| [key, send(key)] }
227
+ data['published_body'] = extract_published_body
228
+ pub_date = @data[:date]
229
+ data['published'] = pub_date.l18n_long_date_string(with_year: false)
230
+ data['published_gemini_index'] = pub_date.strftime('%Y-%m-%d')
231
+ data['published_xml'] = pub_date.xmlschema
232
+ data['updated_xml'] = @data[:updated]&.xmlschema
233
+ data
234
+ end
235
+
236
+ private
237
+
238
+ def find_source
239
+ if ::File.extname(@file) == '.org'
240
+ source = find_source_for_org_file
241
+ else
242
+ source = find_source_for_publication_file
243
+ end
244
+ return source if source
245
+
246
+ short_file = @file.sub(/^#{Dir.pwd}/, '.')
247
+ warn R18n.t.fronde.error.org_file.no_project(file: short_file)
248
+ end
249
+
250
+ def find_source_for_org_file
251
+ Fronde::CONFIG.sources.find do |project|
252
+ project.source_for? @file
253
+ end
254
+ end
255
+
256
+ def find_source_for_publication_file
257
+ Fronde::CONFIG.sources.find do |project|
258
+ org_file = project.source_for @file
259
+ next unless org_file
260
+
261
+ @file = org_file
262
+ end
263
+ end
264
+
265
+ def init_empty_file
266
+ @data = {
267
+ title: @options[:title] || '', subtitle: '', excerpt: '',
268
+ author: @options[:author] || Fronde::CONFIG.get('author'),
269
+ lang: @options[:lang] || Fronde::CONFIG.get('lang'),
270
+ date: Time.now, keywords: [], pub_file: nil, url: nil
271
+ }
272
+ @data[:content] = @options[:raw_content] || <<~ORG
273
+ #+title: #{@data[:title]}
274
+ #+date: <#{@data[:date].strftime('%Y-%m-%d %a. %H:%M:%S')}>
275
+ #+author: #{@data[:author]}
276
+ #+language: #{@data[:lang]}
277
+
278
+ #{@options[:content]}
279
+ ORG
280
+ end
281
+
282
+ # Format {Fronde::Org::File#keywords} list in an HTML listing.
283
+ #
284
+ # @return [String] the HTML keywords list
285
+ def keywords_to_html
286
+ domain = Fronde::CONFIG.get('domain')
287
+ # Allow a nil project, mainly for tests purpose. Should never
288
+ # happen in reality
289
+ pub_path = @project&.public_absolute_path || '/'
290
+ klist = @data[:keywords].map do |k|
291
+ %(<li class="keyword">
292
+ <a href="#{domain}#{pub_path}tags/#{Slug.slug(k)}.html">#{k}</a>
293
+ </li>)
294
+ end.join
295
+ %(<ul class="keywords-list">#{klist}</ul>)
296
+ end
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ using TimePatch
4
+
5
+ require_relative '../../ext/time_no_time'
6
+
7
+ module Fronde
8
+ module Org
9
+ # This module holds extracter methods for the {Fronde::Org::File}
10
+ # class.
11
+ module FileExtracter
12
+ private
13
+
14
+ # Main method, which will call the other to initialize an
15
+ # {Fronde::Org::File} instance.
16
+ def extract_data
17
+ @data = { content: ::File.read(@file), pub_file: nil, url: nil }
18
+ %i[title subtitle date author keywords lang excerpt].each do |param|
19
+ @data[param] = send(:"extract_#{param}")
20
+ end
21
+ return unless @project
22
+
23
+ @data[:updated] = ::File.mtime(@file)
24
+ @data[:pub_file] = @project.target_for @file
25
+ @data[:url] = Fronde::CONFIG.get('domain') + @data[:pub_file]
26
+ end
27
+
28
+ def extract_date
29
+ timerx = '([0-9:]{5})(?::([0-9]{2}))?'
30
+ daterx = /^#\+date: *<([0-9-]{10}) [\w.]+(?: #{timerx})?> *$/i
31
+ match = daterx.match(@data[:content])
32
+ return NilTime.new if match.nil?
33
+
34
+ return TimeNoTime.parse_no_time(match[1]) if match[2].nil?
35
+
36
+ Time.strptime(
37
+ "#{match[1]} #{match[2]}:#{match[3] || '00'}",
38
+ '%Y-%m-%d %H:%M:%S'
39
+ )
40
+ end
41
+
42
+ def extract_title
43
+ match = /^#\+title:(.+)$/i.match(@data[:content])
44
+ if match.nil?
45
+ # Avoid to leak absolute path
46
+ project_relative_path = @file.sub %r{^#{Dir.pwd}/}, ''
47
+ return project_relative_path
48
+ end
49
+ match[1].strip
50
+ end
51
+
52
+ def extract_subtitle
53
+ match = /^#\+subtitle:(.+)$/i.match(@data[:content])
54
+ (match && match[1].strip) || ''
55
+ end
56
+
57
+ def extract_author
58
+ match = /^#\+author:(.+)$/i.match(@data[:content])
59
+ (match && match[1].strip) || Fronde::CONFIG.get('author')
60
+ end
61
+
62
+ def extract_keywords
63
+ match = /^#\+keywords:(.+)$/i.match(@data[:content])
64
+ (match && match[1].split(',').map(&:strip)) || []
65
+ end
66
+
67
+ def extract_lang
68
+ match = /^#\+language:(.+)$/i.match(@data[:content])
69
+ (match && match[1].strip) || Fronde::CONFIG.get('lang')
70
+ end
71
+
72
+ def extract_excerpt
73
+ @data[:content].scan(/^#\+description:(.+)$/i).map do |line|
74
+ line.first.strip
75
+ end.join(' ')
76
+ end
77
+
78
+ def extract_published_body
79
+ pub_file = @data[:pub_file]
80
+ # Always return something, even when not published yet
81
+ return @data[:excerpt] unless pub_file && @project
82
+
83
+ project_type = @project.type
84
+ pub_folder = Fronde::CONFIG.get("#{project_type}_public_folder")
85
+ file_name = pub_folder + pub_file
86
+ return @data[:excerpt] unless ::File.exist? file_name
87
+
88
+ return ::File.read(file_name) if project_type == 'gemini'
89
+
90
+ read_html_body file_name
91
+ end
92
+
93
+ def read_html_body(file_name)
94
+ dom = ::File.open(file_name, 'r') { |file| Nokogiri::HTML file }
95
+ body = dom.css('div#content')
96
+ body.css('header').unlink # Remove the main title
97
+ body.to_s
98
+ end
99
+ end
100
+ end
101
+ end
data/lib/fronde/org.rb ADDED
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fronde
4
+ # Everything related to Org mode
5
+ #
6
+ # The module itself wraps code necessary to download the last version
7
+ # of the Emacs package. It also serves as a namespace for the class
8
+ # responsible for handling Org files: {Fronde::Org::File}.
9
+ module Org
10
+ class << self
11
+ def current_version
12
+ # Do not crash if Org is not yet installed (and thus return nil)
13
+ Dir['lib/org-*'].first&.delete_prefix('lib/org-')
14
+ end
15
+
16
+ # Fetch and return the last published version of Org.
17
+ #
18
+ # To be nice with Org servers, this method will keep the fetched
19
+ # version number in a cache file. You can bypass it by using the
20
+ # force parameter.
21
+ #
22
+ # @param force [Boolean] Whether we should first remove the guard
23
+ # file if it exists
24
+ # @param destination [String] Where to store the cookie file to
25
+ # remember the last version number
26
+ # @return [String] the new x.x.x version string of Org
27
+ def last_version(force: false, cookie_dir: 'var/tmp')
28
+ cookie = "#{cookie_dir}/last_org_version"
29
+ return ::File.read cookie if !force && ::File.exist?(cookie)
30
+
31
+ org_version = fetch_version_number
32
+ raise 'No remote Org version found' unless org_version
33
+
34
+ FileUtils.mkdir_p cookie_dir
35
+ ::File.write cookie, org_version
36
+ org_version
37
+ end
38
+
39
+ def fetch_version_number
40
+ # Retrieve last org version from git repository tags page.
41
+ tag_rx = Regexp.new(
42
+ '<a href=\'/cgit/emacs/org-mode.git/tag/\?h=' \
43
+ '(?<tag>release_(?<number>[^\']+))\'>\k<tag></a>'
44
+ )
45
+ versions = URI(
46
+ 'https://git.savannah.gnu.org/cgit/emacs/org-mode.git/refs/'
47
+ ).open.readlines.map do |line|
48
+ line.match(tag_rx) { |matchdata| matchdata[:number] }
49
+ end
50
+ versions.compact.first
51
+ end
52
+
53
+ # Download latest org-mode tarball.
54
+ #
55
+ # @param destination [String] where to save the org-mode tarball
56
+ # @return [String] the downloaded org-mode version
57
+ def download(destination = 'var/tmp')
58
+ org_last_version = last_version(force: false, cookie_dir: destination)
59
+ tarball = "org-mode-release_#{org_last_version}.tar.gz"
60
+ uri = URI("https://git.savannah.gnu.org/cgit/emacs/org-mode.git/snapshot/#{tarball}")
61
+ # Will crash on purpose if anything goes wrong
62
+ Net::HTTP.start(uri.host) do |http|
63
+ fetch_org_tarball http, Net::HTTP::Get.new(uri), destination
64
+ end
65
+ org_last_version
66
+ end
67
+
68
+ def fetch_org_tarball(http, request, destination)
69
+ # Remove version number in dest file to allow easy rake file
70
+ # task naming
71
+ dest_file = ::File.expand_path('org.tar.gz', destination)
72
+ http.request request do |response|
73
+ ::File.open(dest_file, 'w') do |io|
74
+ response.read_body { |chunk| io.write chunk }
75
+ end
76
+ end
77
+ end
78
+
79
+ def make_org_cmd(org_dir, target, verbose: false)
80
+ make = ['make', '-C', org_dir, target]
81
+ return make.join(' ') if verbose
82
+
83
+ make.insert(3, '-s')
84
+ make << 'EMACSQ="emacs -Q --eval \'(setq inhibit-message t)\'"'
85
+ make.join(' ')
86
+ end
87
+
88
+ # Compile downloaded Org package
89
+ #
90
+ # @param source [String] path to the org-mode tarball to install
91
+ # @param version [String] version of the org package to install
92
+ # @param target [String] path to the final install directory
93
+ # @param verbose [Boolean] whether the process should be verbose
94
+ def compile(source, version, target, verbose: false)
95
+ untar_cmd = ['tar', '-xzf', source]
96
+ system(*untar_cmd)
97
+ FileUtils.mv "org-mode-release_#{version}", target
98
+ # Fix a weird unknown package version
99
+ ::File.write("#{target}/mk/version.mk", "ORGVERSION ?= #{version}")
100
+ system(*make_org_cmd(target, 'compile', verbose: verbose))
101
+ system(*make_org_cmd(target, 'autoloads', verbose: verbose))
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,54 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'webrick'
4
- require 'fronde/config'
5
-
6
- module Fronde # 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
4
+ require_relative 'config'
18
5
 
19
- private
6
+ module Fronde
7
+ module Preview # rubocop:disable Style/Documentation
8
+ # A tiny preview server, which main goal is to replace references to
9
+ # the target domain by localhost.
10
+ class Servlet < WEBrick::HTTPServlet::AbstractServlet
11
+ include WEBrick::HTTPUtils
20
12
 
21
- def local_path(requested_path)
22
- routes = Fronde::Config.get(['preview', 'routes'], {})
23
- return routes[requested_path] if routes.has_key? requested_path
24
- local_path = Fronde::Config.get('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
- )
13
+ def do_GET(request, response) # rubocop:disable Naming/MethodName
14
+ file = local_path(request.path)
15
+ response.body = parse_body(file, "http://#{request.host}:#{request.port}")
16
+ response.status = 200
17
+ response.content_type = mime_type(file, DefaultMimeTypes)
29
18
  end
30
- return local_path if File.exist? local_path
31
- raise WEBrick::HTTPStatus::NotFound, 'Not found.'
32
- end
33
19
 
34
- def parse_body(local_path, local_host)
35
- body = File.read local_path
36
- return body unless local_path.match?(/\.(?:ht|x)ml\z/)
37
- domain = Fronde::Config.get('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))
20
+ private
21
+
22
+ def local_path(requested_path)
23
+ routes = Fronde::CONFIG.get(%w[preview routes], {})
24
+ return routes[requested_path] if routes.has_key? requested_path
25
+
26
+ local_path = Fronde::CONFIG.get('html_public_folder') + requested_path
27
+ if File.directory? local_path
28
+ local_path = format(
29
+ '%<path>s/index.html', path: local_path.delete_suffix('/')
30
+ )
31
+ end
32
+ return local_path if File.exist? local_path
33
+
34
+ raise WEBrick::HTTPStatus::NotFound, 'Not found.'
35
+ end
36
+
37
+ def parse_body(local_path, local_host)
38
+ body = File.read local_path
39
+ return body unless local_path.match?(/\.(?:ht|x)ml\z/)
40
+
41
+ domain = Fronde::CONFIG.get('domain')
42
+ return body if domain == ''
43
+
44
+ host_repl = %("#{local_host})
45
+ body.gsub('"file://', host_repl).gsub(%("#{domain}), host_repl)
46
+ end
41
47
  end
42
- end
43
48
 
44
- class << self
45
- def start_preview
49
+ def self.start
46
50
  # Inspired by ruby un.rb library, which allows normally to start a
47
51
  # webrick server in one line: ruby -run -e httpd public_html -p 5000
48
- port = Fronde::Config.get(['preview', 'server_port'], 5000)
52
+ port = Fronde::CONFIG.get(%w[preview server_port], 5000)
49
53
  s = WEBrick::HTTPServer.new(Port: port)
50
- s.mount '/', Fronde::PreviewServlet
51
- ['TERM', 'QUIT', 'INT'].each { |sig| trap(sig, proc { s.shutdown }) }
54
+ s.mount '/', Servlet
55
+ %w[TERM QUIT INT].each { |sig| trap(sig, proc { s.shutdown }) }
52
56
  s.start
53
57
  end
54
58
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fronde
4
+ # Contains method to generate URL compatible strings
5
+ module Slug
6
+ class << self
7
+ def slug(title)
8
+ title.downcase
9
+ .encode('ascii', fallback: ->(k) { translit(k) })
10
+ .encode('utf-8') # Convert back to utf-8 string
11
+ .gsub(/[^\w-]/, '-')
12
+ .squeeze('-')
13
+ .delete_suffix('-')
14
+ end
15
+
16
+ # rubocop:disable Metrics/CyclomaticComplexity
17
+ # rubocop:disable Metrics/MethodLength
18
+ def translit(char)
19
+ case char
20
+ when 'á', 'à', 'â', 'ä', 'ǎ', 'ã', 'å'
21
+ 'a'
22
+ when 'é', 'è', 'ê', 'ë', 'ě', 'ẽ', '€'
23
+ 'e'
24
+ when 'í', 'ì', 'î', 'ï', 'ǐ', 'ĩ'
25
+ 'i'
26
+ when 'ó', 'ò', 'ô', 'ö', 'ǒ', 'õ', 'ø'
27
+ 'o'
28
+ when 'ú', 'ù', 'û', 'ü', 'ǔ', 'ũ'
29
+ 'u'
30
+ when 'ý', 'ỳ', 'ŷ', 'ÿ', 'ỹ'
31
+ 'y'
32
+ when 'ç', '©', '🄯'
33
+ 'c'
34
+ when 'ñ'
35
+ 'n'
36
+ when 'ß'
37
+ 'ss'
38
+ when 'œ'
39
+ 'oe'
40
+ when 'æ'
41
+ 'ae'
42
+ when '®'
43
+ 'r'
44
+ when '™'
45
+ 'tm'
46
+ else
47
+ '-'
48
+ end
49
+ end
50
+ # rubocop:enable Metrics/CyclomaticComplexity
51
+ # rubocop:enable Metrics/MethodLength
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fronde
4
+ class Source
5
+ # Specific settings for Gemini {Fronde::Source}
6
+ class Gemini < Source
7
+ class << self
8
+ def org_default_postamble
9
+ format(
10
+ "📅 %<date>s\n📝 %<author>s %<creator>s",
11
+ author: R18n.t.fronde.org.postamble.written_by,
12
+ creator: R18n.t.fronde.org.postamble.with_emacs,
13
+ date: R18n.t.fronde.org.postamble.last_modification
14
+ )
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def fill_in_specific_config
21
+ @config.merge!(
22
+ 'type' => 'gemini', 'ext' => '.gmi', 'mime_type' => 'text/gemini',
23
+ 'folder' => CONFIG.get('gemini_public_folder')
24
+ )
25
+ end
26
+
27
+ def org_default_options
28
+ { 'publishing-function' => 'org-gmi-publish-to-gemini',
29
+ 'gemini-head' => '',
30
+ 'gemini-postamble' => Gemini.org_default_postamble }
31
+ end
32
+ end
33
+ end
34
+ end