fronde 0.3.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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