fronde 0.3.4 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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 +62 -42
  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 +53 -110
  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