fronde 0.3.3 → 0.4.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 (51) 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 +17 -0
  5. data/lib/ext/time.rb +49 -0
  6. data/lib/fronde/cli/commands.rb +92 -103
  7. data/lib/fronde/cli/data/Rakefile +8 -0
  8. data/lib/fronde/cli/data/config.yml +13 -0
  9. data/lib/fronde/cli/data/gitignore +7 -0
  10. data/lib/fronde/cli/data/zsh_completion +37 -0
  11. data/lib/fronde/cli/helpers.rb +55 -0
  12. data/lib/fronde/cli/opt_parse.rb +143 -0
  13. data/lib/fronde/cli/throbber.rb +99 -0
  14. data/lib/fronde/cli.rb +41 -42
  15. data/lib/fronde/config/data/org-config.el +24 -0
  16. data/lib/fronde/config/{ox-fronde.el → data/ox-fronde.el} +1 -1
  17. data/lib/fronde/config/helpers.rb +80 -0
  18. data/lib/fronde/config/lisp.rb +70 -0
  19. data/lib/fronde/config.rb +135 -99
  20. data/lib/fronde/emacs.rb +23 -20
  21. data/lib/fronde/index/atom_generator.rb +55 -66
  22. data/lib/fronde/index/data/all_tags.org +14 -0
  23. data/lib/fronde/index/data/template.org +22 -0
  24. data/lib/fronde/index/data/template.xml +37 -0
  25. data/lib/fronde/index/org_generator.rb +70 -88
  26. data/lib/fronde/index.rb +56 -82
  27. data/lib/fronde/org/file.rb +287 -0
  28. data/lib/fronde/org/file_extracter.rb +98 -0
  29. data/lib/fronde/org.rb +103 -0
  30. data/lib/fronde/preview.rb +43 -39
  31. data/lib/fronde/slug.rb +27 -0
  32. data/lib/fronde/source/gemini.rb +39 -0
  33. data/lib/fronde/source/html.rb +67 -0
  34. data/lib/fronde/source.rb +204 -0
  35. data/lib/fronde/templater.rb +94 -71
  36. data/lib/fronde/version.rb +1 -1
  37. data/lib/tasks/cli.rake +33 -0
  38. data/lib/tasks/org.rake +63 -43
  39. data/lib/tasks/site.rake +68 -30
  40. data/lib/tasks/sync.rake +41 -21
  41. data/lib/tasks/tags.rake +11 -7
  42. data/locales/en.yml +60 -14
  43. data/locales/fr.yml +68 -14
  44. metadata +57 -156
  45. data/lib/fronde/config/lisp_config.rb +0 -340
  46. data/lib/fronde/config/org-config.el +0 -19
  47. data/lib/fronde/org_file/class_methods.rb +0 -72
  48. data/lib/fronde/org_file/extracter.rb +0 -72
  49. data/lib/fronde/org_file/htmlizer.rb +0 -43
  50. data/lib/fronde/org_file.rb +0 -298
  51. data/lib/fronde/utils.rb +0 -229
@@ -0,0 +1,287 @@
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
+ # - %i :: the raw ~:short~ date and time
128
+ # - %I :: the raw ~:iso8601~ date and time
129
+ # - %k :: the keywords separated by a comma
130
+ # - %K :: the HTML list rendering of the keywords
131
+ # - %l :: the lang of the document
132
+ # - %L :: the license information, taken from the
133
+ # {Fronde::Config#settings}
134
+ # - %n :: the Fronde name and version
135
+ # - %N :: the Fronde name and version with a link to the project
136
+ # home on the name
137
+ # - %s :: the subtitle of the document
138
+ # - %t :: the title of the document
139
+ # - %u :: the URL to the related published HTML document
140
+ # - %x :: the raw description (eXcerpt)
141
+ # - %X :: the description, enclosed in an HTML ~p~ tag, equivalent
142
+ # to ~<p>%x</p>~
143
+ #
144
+ # @example
145
+ # org_file.format("Article written by %a the %d")
146
+ # => "Article written by Alice Smith the Wednesday 3rd July"
147
+ #
148
+ # @return [String] the given ~string~ after replacement occurs
149
+ # rubocop:disable Metrics/MethodLength
150
+ # rubocop:disable Layout/LineLength
151
+ def format(string)
152
+ string.gsub('%a', @data[:author])
153
+ .gsub('%A', "<span class=\"author\">#{@data[:author]}</span>")
154
+ .gsub('%d', @data[:date].l18n_short_date_html)
155
+ .gsub('%D', @data[:date].l18n_long_date_html)
156
+ .gsub('%i', @data[:date].l18n_short_date_string)
157
+ .gsub('%I', @data[:date].xmlschema)
158
+ .gsub('%k', @data[:keywords].join(', '))
159
+ .gsub('%K', keywords_to_html)
160
+ .gsub('%l', @data[:lang])
161
+ .gsub('%L', Fronde::CONFIG.get('license', '').gsub(/\s+/, ' ').strip)
162
+ .gsub('%n', "Fronde #{Fronde::VERSION}")
163
+ .gsub('%N', "<a href=\"https://git.umaneti.net/fronde/about/\">Fronde</a> #{Fronde::VERSION}")
164
+ .gsub('%s', @data[:subtitle])
165
+ .gsub('%t', @data[:title])
166
+ .gsub('%u', @data[:url] || '')
167
+ .gsub('%x', @data[:excerpt])
168
+ .gsub('%X', "<p>#{@data[:excerpt]}</p>")
169
+ end
170
+ # rubocop:enable Layout/LineLength
171
+ # rubocop:enable Metrics/MethodLength
172
+
173
+ # Writes the current Org::File content to the underlying file.
174
+ #
175
+ # The intermediate parent folders are created if necessary.
176
+ #
177
+ # @return [Integer] the length written (as returned by the
178
+ # underlying ~File.write~ method call)
179
+ def write
180
+ if ::File.directory? @file
181
+ if @data[:title] == ''
182
+ raise R18n.t.fronde.error.org_file.no_file_or_title
183
+ end
184
+
185
+ @file = ::File.join @file, "#{Slug.slug(@data[:title])}.org"
186
+ else
187
+ file_dir = ::File.dirname @file
188
+ FileUtils.mkdir_p file_dir
189
+ end
190
+ ::File.write @file, @data[:content]
191
+ end
192
+
193
+ def method_missing(method_name, *args, &block)
194
+ reader_method = method_name.to_s.delete_suffix('=').to_sym
195
+ if @data.has_key? reader_method
196
+ return @data[reader_method] if reader_method == method_name
197
+
198
+ return @data[reader_method] = args.first
199
+ end
200
+ super
201
+ end
202
+
203
+ def respond_to_missing?(method_name, include_private = false)
204
+ return true if @data.has_key? method_name
205
+
206
+ reader_method = method_name.to_s.delete_suffix('=').to_sym
207
+ return true if @data.has_key? reader_method
208
+
209
+ super
210
+ end
211
+
212
+ def to_h
213
+ fields = %w[author excerpt keywords timekey title url]
214
+ data = fields.to_h { |key| [key, send(key)] }
215
+ data['published_body'] = extract_published_body
216
+ pub_date = @data[:date]
217
+ data['published'] = pub_date.l18n_long_date_string(with_year: false)
218
+ data['published_xml'] = pub_date.xmlschema
219
+ data['updated_xml'] = @data[:updated]&.xmlschema
220
+ data
221
+ end
222
+
223
+ private
224
+
225
+ def find_source
226
+ if ::File.extname(@file) == '.org'
227
+ source = find_source_for_org_file
228
+ else
229
+ source = find_source_for_publication_file
230
+ end
231
+ warn R18n.t.fronde.error.org_file.no_project(file: @file) unless source
232
+ source
233
+ end
234
+
235
+ def find_source_for_org_file
236
+ Fronde::CONFIG.sources.find do |project|
237
+ project.source_for? @file
238
+ end
239
+ end
240
+
241
+ def find_source_for_publication_file
242
+ Fronde::CONFIG.sources.find do |project|
243
+ org_file = project.source_for @file
244
+ next unless org_file
245
+
246
+ @file = org_file
247
+ end
248
+ end
249
+
250
+ def init_empty_file
251
+ @data = {
252
+ title: @options[:title] || '', subtitle: '', excerpt: '',
253
+ date: Time.now,
254
+ author: @options[:author] || Fronde::CONFIG.get('author'),
255
+ keywords: [],
256
+ lang: @options[:lang] || Fronde::CONFIG.get('lang'),
257
+ pub_file: nil, url: nil
258
+ }
259
+ body = @options[:content] || ''
260
+ @data[:content] = @options[:raw_content] || <<~ORG
261
+ #+title: #{@data[:title]}
262
+ #+date: <#{@data[:date].strftime('%Y-%m-%d %a. %H:%M:%S')}>
263
+ #+author: #{@data[:author]}
264
+ #+language: #{@data[:lang]}
265
+
266
+ #{body}
267
+ ORG
268
+ end
269
+
270
+ # Format {Fronde::Org::File#keywords} list in an HTML listing.
271
+ #
272
+ # @return [String] the HTML keywords list
273
+ def keywords_to_html
274
+ domain = Fronde::CONFIG.get('domain')
275
+ # Allow a nil project, mainly for tests purpose. Should never
276
+ # happen in reality
277
+ pub_path = @project&.public_absolute_path || '/'
278
+ klist = @data[:keywords].map do |k|
279
+ %(<li class="keyword">
280
+ <a href="#{domain}#{pub_path}tags/#{Slug.slug(k)}.html">#{k}</a>
281
+ </li>)
282
+ end.join
283
+ %(<ul class="keywords-list">#{klist}</ul>)
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ using TimePatch
4
+
5
+ module Fronde
6
+ module Org
7
+ # This module holds extracter methods for the {Fronde::Org::File}
8
+ # class.
9
+ module FileExtracter
10
+ private
11
+
12
+ # Main method, which will call the other to initialize an
13
+ # {Fronde::Org::File} instance.
14
+ def extract_data
15
+ @data = { content: ::File.read(@file), pub_file: nil, url: nil }
16
+ %i[title subtitle date author keywords lang excerpt].each do |param|
17
+ @data[param] = send("extract_#{param}".to_sym)
18
+ end
19
+ return unless @project
20
+
21
+ @data[:updated] = ::File.mtime(@file)
22
+ @data[:pub_file] = @project.target_for @file
23
+ @data[:url] = Fronde::CONFIG.get('domain') + @data[:pub_file]
24
+ end
25
+
26
+ def extract_date
27
+ timerx = '([0-9:]{5})(?::([0-9]{2}))?'
28
+ daterx = /^#\+date: *<([0-9-]{10}) [\w.]+(?: #{timerx})?> *$/i
29
+ match = daterx.match(@data[:content])
30
+ return NilTime.new if match.nil?
31
+
32
+ notime = match[2].nil?
33
+ if notime
34
+ time = '00:00:00'
35
+ else
36
+ time = "#{match[2]}:#{match[3] || '00'}"
37
+ end
38
+ date = Time.strptime("#{match[1]} #{time}", '%Y-%m-%d %H:%M:%S')
39
+ date.no_time = notime
40
+ date
41
+ end
42
+
43
+ def extract_title
44
+ match = /^#\+title:(.+)$/i.match(@data[:content])
45
+ if match.nil?
46
+ # Avoid to leak absolute path
47
+ project_relative_path = @file.sub(/^#{Dir.pwd}\//, '')
48
+ return project_relative_path
49
+ end
50
+ match[1].strip
51
+ end
52
+
53
+ def extract_subtitle
54
+ match = /^#\+subtitle:(.+)$/i.match(@data[:content])
55
+ (match && match[1].strip) || ''
56
+ end
57
+
58
+ def extract_author
59
+ match = /^#\+author:(.+)$/i.match(@data[:content])
60
+ (match && match[1].strip) || Fronde::CONFIG.get('author')
61
+ end
62
+
63
+ def extract_keywords
64
+ match = /^#\+keywords:(.+)$/i.match(@data[:content])
65
+ (match && match[1].split(',').map(&:strip)) || []
66
+ end
67
+
68
+ def extract_lang
69
+ match = /^#\+language:(.+)$/i.match(@data[:content])
70
+ (match && match[1].strip) || Fronde::CONFIG.get('lang')
71
+ end
72
+
73
+ def extract_excerpt
74
+ @data[:content].scan(/^#\+description:(.+)$/i).map do |line|
75
+ line.first.strip
76
+ end.join(' ')
77
+ end
78
+
79
+ def extract_published_body
80
+ pub_file = @data[:pub_file]
81
+ # Always return something, even when not published yet
82
+ return @data[:excerpt] unless pub_file && @project
83
+
84
+ project_type = @project['type']
85
+ pub_folder = Fronde::CONFIG.get("#{project_type}_public_folder")
86
+ file_name = pub_folder + pub_file
87
+ return @data[:excerpt] unless ::File.exist? file_name
88
+
89
+ return ::File.read(file_name) if project_type == 'gemini'
90
+
91
+ dom = ::File.open(file_name, 'r') { |file| Nokogiri::HTML file }
92
+ body = dom.css('div#content')
93
+ body.css('header').unlink # Remove the main title
94
+ body.to_s
95
+ end
96
+ end
97
+ end
98
+ end
data/lib/fronde/org.rb ADDED
@@ -0,0 +1,103 @@
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
+ # Remove version number in dest file to allow easy rake file
59
+ # task naming
60
+ dest_file = ::File.expand_path('org.tar.gz', destination)
61
+ org_last_version = last_version(force: false, cookie_dir: destination)
62
+ tarball = "org-mode-release_#{org_last_version}.tar.gz"
63
+ uri = URI("https://git.savannah.gnu.org/cgit/emacs/org-mode.git/snapshot/#{tarball}")
64
+ # Will crash on purpose if anything goes wrong
65
+ Net::HTTP.start(uri.host) do |http|
66
+ request = Net::HTTP::Get.new uri
67
+
68
+ http.request request do |response|
69
+ ::File.open(dest_file, 'w') do |io|
70
+ response.read_body { |chunk| io.write chunk }
71
+ end
72
+ end
73
+ end
74
+ org_last_version
75
+ end
76
+
77
+ def make_org_cmd(org_dir, target, verbose: false)
78
+ make = ['make', '-C', org_dir, target]
79
+ return make.join(' ') if verbose
80
+
81
+ make.insert(3, '-s')
82
+ make << 'EMACSQ="emacs -Q --eval \'(setq inhibit-message t)\'"'
83
+ make.join(' ')
84
+ end
85
+
86
+ # Compile downloaded Org package
87
+ #
88
+ # @param source [String] path to the org-mode tarball to install
89
+ # @param version [String] version of the org package to install
90
+ # @param target [String] path to the final install directory
91
+ # @param verbose [Boolean] whether the process should be verbose
92
+ def compile(source, version, target, verbose: false)
93
+ untar_cmd = ['tar', '-xzf', source]
94
+ system(*untar_cmd)
95
+ FileUtils.mv "org-mode-release_#{version}", target
96
+ # Fix a weird unknown package version
97
+ ::File.write("#{target}/mk/version.mk", "ORGVERSION ?= #{version}")
98
+ system(*make_org_cmd(target, 'compile', verbose: verbose))
99
+ system(*make_org_cmd(target, 'autoloads', verbose: verbose))
100
+ end
101
+ end
102
+ end
103
+ 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,27 @@
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.tr(' ', '-')
9
+ .encode('ascii', fallback: ->(k) { translit(k) })
10
+ .gsub(/[^\w-]/, '').delete_suffix('-')
11
+ end
12
+
13
+ def translit(char)
14
+ return 'a' if %w[á à â ä ǎ ã å].include?(char)
15
+ return 'e' if %w[é è ê ë ě ẽ].include?(char)
16
+ return 'i' if %w[í ì î ï ǐ ĩ].include?(char)
17
+ return 'o' if %w[ó ò ô ö ǒ õ].include?(char)
18
+ return 'u' if %w[ú ù û ü ǔ ũ].include?(char)
19
+ return 'y' if %w[ý ỳ ŷ ÿ ỹ].include?(char)
20
+ return 'c' if char == 'ç'
21
+ return 'n' if char == 'ñ'
22
+
23
+ '-'
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fronde
4
+ class Source
5
+ # Specific settings for Gemini {Fronde::Source}
6
+ class Gemini < Source
7
+ def blog?
8
+ # TODO: See how to support blog/indexes with gemini
9
+ false
10
+ end
11
+
12
+ class << self
13
+ def org_default_postamble
14
+ format(
15
+ "📅 %<date>s\n📝 %<author>s %<creator>s",
16
+ author: R18n.t.fronde.org.postamble.written_by,
17
+ creator: R18n.t.fronde.org.postamble.with_emacs,
18
+ date: R18n.t.fronde.org.postamble.last_modification
19
+ )
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def fill_in_specific_config
26
+ @config.merge!(
27
+ 'type' => 'gemini', 'ext' => '.gmi', 'mime_type' => 'text/gemini',
28
+ 'folder' => CONFIG.get('gemini_public_folder')
29
+ )
30
+ end
31
+
32
+ def org_default_options
33
+ { 'publishing-function' => 'org-gmi-publish-to-gemini',
34
+ 'gemini-head' => '',
35
+ 'gemini-postamble' => Gemini.org_default_postamble }
36
+ end
37
+ end
38
+ end
39
+ end