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.
- checksums.yaml +4 -4
- data/bin/fronde +15 -30
- data/lib/ext/nil_time.rb +25 -0
- data/lib/ext/r18n.rb +37 -0
- data/lib/ext/time.rb +39 -0
- data/lib/ext/time_no_time.rb +23 -0
- data/lib/fronde/cli/commands.rb +97 -104
- data/lib/fronde/cli/data/Rakefile +8 -0
- data/lib/fronde/cli/data/config.yml +13 -0
- data/lib/fronde/cli/data/gitignore +6 -0
- data/lib/fronde/cli/data/zsh_completion +37 -0
- data/lib/fronde/cli/helpers.rb +55 -0
- data/lib/fronde/cli/opt_parse.rb +140 -0
- data/lib/fronde/cli/throbber.rb +110 -0
- data/lib/fronde/cli.rb +42 -42
- data/lib/fronde/config/data/org-config.el +25 -0
- data/lib/fronde/config/data/ox-fronde.el +158 -0
- data/lib/fronde/config/data/themes/umaneti/css/htmlize.css +364 -0
- data/lib/fronde/config/data/themes/umaneti/css/style.css +250 -0
- data/lib/fronde/config/data/themes/umaneti/img/bottom.png +0 -0
- data/lib/fronde/config/data/themes/umaneti/img/content.png +0 -0
- data/lib/fronde/config/data/themes/umaneti/img/tic.png +0 -0
- data/lib/fronde/config/data/themes/umaneti/img/top.png +0 -0
- data/lib/fronde/config/helpers.rb +62 -0
- data/lib/fronde/config/lisp.rb +80 -0
- data/lib/fronde/config.rb +148 -98
- data/lib/fronde/emacs.rb +23 -20
- data/lib/fronde/index/atom_generator.rb +55 -66
- data/lib/fronde/index/data/all_tags.org +19 -0
- data/lib/fronde/index/data/template.org +26 -0
- data/lib/fronde/index/data/template.xml +37 -0
- data/lib/fronde/index/org_generator.rb +72 -88
- data/lib/fronde/index.rb +57 -86
- data/lib/fronde/org/file.rb +299 -0
- data/lib/fronde/org/file_extracter.rb +101 -0
- data/lib/fronde/org.rb +105 -0
- data/lib/fronde/preview.rb +43 -39
- data/lib/fronde/slug.rb +54 -0
- data/lib/fronde/source/gemini.rb +34 -0
- data/lib/fronde/source/html.rb +67 -0
- data/lib/fronde/source.rb +209 -0
- data/lib/fronde/sync/neocities.rb +220 -0
- data/lib/fronde/sync/rsync.rb +46 -0
- data/lib/fronde/sync.rb +32 -0
- data/lib/fronde/templater.rb +101 -71
- data/lib/fronde/version.rb +1 -1
- data/lib/tasks/cli.rake +33 -0
- data/lib/tasks/org.rake +58 -43
- data/lib/tasks/site.rake +66 -31
- data/lib/tasks/sync.rake +37 -40
- data/lib/tasks/tags.rake +11 -7
- data/locales/en.yml +61 -14
- data/locales/fr.yml +69 -14
- metadata +77 -95
- data/lib/fronde/config/lisp_config.rb +0 -340
- data/lib/fronde/config/org-config.el +0 -19
- data/lib/fronde/config/ox-fronde.el +0 -121
- data/lib/fronde/org_file/class_methods.rb +0 -72
- data/lib/fronde/org_file/extracter.rb +0 -72
- data/lib/fronde/org_file/htmlizer.rb +0 -43
- data/lib/fronde/org_file.rb +0 -298
- 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
|
data/lib/fronde/preview.rb
CHANGED
@@ -1,54 +1,58 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'webrick'
|
4
|
-
|
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
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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::
|
52
|
+
port = Fronde::CONFIG.get(%w[preview server_port], 5000)
|
49
53
|
s = WEBrick::HTTPServer.new(Port: port)
|
50
|
-
s.mount '/',
|
51
|
-
[
|
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
|
data/lib/fronde/slug.rb
ADDED
@@ -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
|