fronde 0.3.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.
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'fileutils'
5
+ # fronde/config is required by htmlizer
6
+ require 'fronde/org_file/htmlizer'
7
+ require 'fronde/org_file/extracter'
8
+ require 'fronde/org_file/class_methods'
9
+ require 'fronde/index'
10
+ require 'fronde/version'
11
+
12
+ module Fronde
13
+ # Handles org files.
14
+ #
15
+ # This class is responsible for reading or writing existing or new org
16
+ # files, and formating their content to be used on the generated
17
+ # website.
18
+ class OrgFile
19
+ # @return [String] the title of the current org document, taken from
20
+ # the ~#+title:~ header.
21
+ attr_reader :title
22
+
23
+ # @return [String] the subtitle of the current org document, taken
24
+ # from the ~#+subtitle:~ header.
25
+ attr_reader :subtitle
26
+
27
+ # @return [DateTime] the date and time of the current org document,
28
+ # taken from the ~#+date:~ header.
29
+ attr_reader :date
30
+
31
+ # @return [Boolean] wether a time has been extracted from the
32
+ # current org document ~#+date:~ header.
33
+ attr_reader :notime
34
+
35
+ # The author of the current org document, taken from the ~#+author:~
36
+ # header.
37
+ #
38
+ # If the current document doesn't have any authorship information,
39
+ # the one from the ~config.yml~ file will be used instead
40
+ #
41
+ # @return [String] the author name
42
+ attr_reader :author
43
+
44
+ # @return [Array] the keywords list of the current org document,
45
+ # taken from the ~#+keywords:~ header.
46
+ attr_reader :keywords
47
+
48
+ # @return [String] the description of this org document, taken from
49
+ # the ~#+description:~ header.
50
+ attr_reader :excerpt
51
+
52
+ # The locale of the current org document, taken from the
53
+ # ~#+language:~ header.
54
+ #
55
+ # If the current document doesn't have any language information, the
56
+ # one from the ~config.yml~ file will be used instead, or "en" by
57
+ # default.
58
+ #
59
+ # @return [String] the document lang
60
+ attr_reader :lang
61
+
62
+ # @return [String] the relative path to the source of this document.
63
+ attr_reader :file
64
+
65
+ # @return [String] the relative path to the generated html file of
66
+ # this document.
67
+ attr_reader :html_file
68
+
69
+ # @return [String] the url of this document, build from the ~domain~
70
+ # settings and the above {#html_file @html_file} attribute.
71
+ attr_reader :url
72
+
73
+ # @return [String] the project owning this document.
74
+ attr_reader :project
75
+
76
+ extend Fronde::OrgFileClassMethods
77
+
78
+ include Fronde::OrgFileExtracter
79
+ include Fronde::OrgFileHtmlizer
80
+
81
+ # Prepares the file named by ~file_name~ for read and write
82
+ # operations.
83
+ #
84
+ # If the file ~file_name~ does not exist, the new instance may be
85
+ # populated by data given in the ~opts~ parameter.
86
+ #
87
+ # @example
88
+ # File.exist? './test.org'
89
+ # => true
90
+ # o = Fronde::OrgFile.new('./test.org')
91
+ # => #<Fronde::OrgFile @file='./test.org'...>
92
+ # o.title
93
+ # => "This is an existing test file"
94
+ # File.exist? '/tmp/does_not_exist.org'
95
+ # => false
96
+ # o = Fronde::OrgFile.new('/tmp/does_not_exist.org')
97
+ # => #<Fronde::OrgFile @file='/tmp/does_not_exist.org'...>
98
+ # o.title
99
+ # => ""
100
+ # File.exist? '/tmp/other.org'
101
+ # => false
102
+ # o = Fronde::OrgFile.new('/tmp/other.org', title: 'New file')
103
+ # => #<Fronde::OrgFile @file='/tmp/other.org'...>
104
+ # o.title
105
+ # => "New file"
106
+ #
107
+ # @param file_name [String] path to the corresponding Org file
108
+ # @param opts [Hash] optional data to initialize new Org file
109
+ # @option opts [String] title ('') the title of the new Org file
110
+ # @option opts [String] author (system user or '') the author of the
111
+ # document
112
+ # @option opts [Boolean] verbose (false) if the
113
+ # {Fronde::OrgFileHtmlizer#publish publish} method should output
114
+ # emacs process messages
115
+ # @option opts [String] project the project owning this file
116
+ # must be stored
117
+ # @return [Fronde::OrgFile] the new instance of Fronde::OrgFile
118
+ def initialize(file_name, opts = {})
119
+ file_name = nil if file_name == ''
120
+ @file = file_name
121
+ @html_file = nil
122
+ @url = nil
123
+ @project = opts.delete :project
124
+ @options = opts
125
+ build_html_file_and_url
126
+ if @file && File.exist?(@file)
127
+ extract_data
128
+ else
129
+ init_empty_file
130
+ end
131
+ end
132
+
133
+ # Returns a String representation of the document date, which aims
134
+ # to be used to sort several OrgFiles.
135
+ #
136
+ # The format used for the key is ~%Y%m%d%H%M%S~. If the current
137
+ # OrgFile instance does not have a date, this mehod return
138
+ # ~00000000000000~. If the current OrgFile instance does not have
139
+ # time information, the date is padded with zeros.
140
+ #
141
+ # @example with the org header ~#+date: <2019-07-03 Wed 20:52:49>~
142
+ # org_file.date
143
+ # => #<DateTime: 2019-07-03T20:52:49+02:00...>
144
+ # org_file.timekey
145
+ # => "20190703205349"
146
+ #
147
+ # @example with the org header ~#+date: <2019-07-03 Wed>~
148
+ # org_file.date
149
+ # => #<DateTime: 2019-07-03T00:00:00+02:00...>
150
+ # org_file.timekey
151
+ # => "20190703000000"
152
+ #
153
+ # @example with no date header in the org file
154
+ # org_file.date
155
+ # => nil
156
+ # org_file.timekey
157
+ # => "00000000000000"
158
+ #
159
+ # @return [String] the document key
160
+ def timekey
161
+ return '00000000000000' if @date.nil?
162
+ @date.strftime('%Y%m%d%H%M%S')
163
+ end
164
+
165
+ # Returns the current OrgFile instance DateTime as a String.
166
+ #
167
+ # This method accepts three values for the ~dateformat~ parameter:
168
+ #
169
+ # - ~:full~ (or ~:long~) :: outputs a complete date and time
170
+ # representation, localized through R18n;
171
+ # - ~:short~ :: outputs a short date representation (without time),
172
+ # localized with R18n;
173
+ # - ~:rfc3339~ :: outputs the RFC 3339 date and time representation,
174
+ # used in atom feed.
175
+ #
176
+ # @param dateformat [Symbol] the format to use to convert DateTime
177
+ # into String
178
+ # @param year [Boolean] wether or not the ~:full~ format must
179
+ # contain the year
180
+ # @return [String] the document DateTime string representation
181
+ def datestring(dateformat = :full, year: true)
182
+ return '' if @date.nil?
183
+ return R18n.l @date.to_date if dateformat == :short
184
+ return @date.rfc3339 if dateformat == :rfc3339
185
+ locale = R18n.get.locale
186
+ long_fmt = R18n.t.fronde.index.full_date_format(
187
+ date: locale.format_date_full(@date, year)
188
+ )
189
+ unless @notime
190
+ long_fmt = R18n.t.fronde.index.full_date_with_time_format(
191
+ date: long_fmt, time: locale.time_format.delete('_').strip
192
+ )
193
+ end
194
+ locale.strftime(@date, long_fmt)
195
+ end
196
+
197
+ # Formats given ~string~ with values of the current OrgFile.
198
+ #
199
+ # This method expects to find percent-tags in the given ~string~ and
200
+ # replace them by their corresponding value.
201
+ #
202
+ # It reuses the same tags than the ~org-html-format-spec~ method.
203
+ #
204
+ # *** Format:
205
+ #
206
+ # - %a :: the raw author name
207
+ # - %A :: the HTML rendering of the author name, equivalent to
208
+ # ~<span class="author">%a</span>~
209
+ # - %d :: the ~:short~ date HTML representation, equivalent
210
+ # to ~<time datetime="%I">%i</time>~
211
+ # - %D :: the ~:full~ date and time HTML representation
212
+ # - %i :: the raw ~:short~ date and time
213
+ # - %I :: the raw ~:rfc3339~ date and time
214
+ # - %k :: the keywords separated by a comma
215
+ # - %K :: the HTML list rendering of the keywords
216
+ # - %l :: the lang of the document
217
+ # - %L :: the license information, taken from the
218
+ # {Fronde::Config#settings}
219
+ # - %n :: the Fronde name and version
220
+ # - %N :: the Fronde name and version with a link to the project
221
+ # home on the name
222
+ # - %s :: the subtitle of the document
223
+ # - %t :: the title of the document
224
+ # - %u :: the web path to the related published HTML document
225
+ # - %x :: the raw description (eXcerpt)
226
+ # - %X :: the description, enclosed in an HTML ~p~ tag, equivalent
227
+ # to ~<p>%x</p>~
228
+ #
229
+ # @example
230
+ # org_file.format("Article written by %a the %d")
231
+ # => "Article written by Alice Smith the Wednesday 3rd July"
232
+ #
233
+ # @return [String] the given ~string~ after replacement occurs
234
+ # rubocop:disable Metrics/MethodLength
235
+ # rubocop:disable Layout/LineLength
236
+ def format(string)
237
+ string.gsub('%a', @author)
238
+ .gsub('%A', author_to_html)
239
+ .gsub('%d', date_to_html(:short))
240
+ .gsub('%D', date_to_html)
241
+ .gsub('%i', datestring(:short))
242
+ .gsub('%I', datestring(:rfc3339))
243
+ .gsub('%k', @keywords.join(', '))
244
+ .gsub('%K', keywords_to_html)
245
+ .gsub('%l', @lang)
246
+ .gsub('%L', (Fronde::Config.settings['license'] || '').gsub(/\s+/, ' ').strip)
247
+ .gsub('%n', "Fronde #{Fronde::VERSION}")
248
+ .gsub('%N', "<a href=\"https://git.umaneti.net/fronde/about/\">Fronde</a> #{Fronde::VERSION}")
249
+ .gsub('%s', @subtitle)
250
+ .gsub('%t', @title)
251
+ .gsub('%u', @html_file || '')
252
+ .gsub('%x', @excerpt)
253
+ .gsub('%X', "<p>#{@excerpt}</p>")
254
+ end
255
+ # rubocop:enable Layout/LineLength
256
+ # rubocop:enable Metrics/MethodLength
257
+
258
+ # Writes the current OrgFile content to the underlying file.
259
+ #
260
+ # The intermediate parent folders are created if necessary.
261
+ #
262
+ # @return [Integer] the length written (as returned by the
263
+ # underlying ~IO.write~ method call)
264
+ def write
265
+ raise TypeError, 'no conversion from nil file name to path.' if @file.nil?
266
+ file_dir = File.dirname @file
267
+ FileUtils.mkdir_p file_dir unless Dir.exist? file_dir
268
+ IO.write @file, @content
269
+ end
270
+
271
+ private
272
+
273
+ def build_html_file_and_url
274
+ return if @file.nil?
275
+ @html_file = Fronde::OrgFile.target_for_source(
276
+ @file, @project, with_public_folder: false
277
+ )
278
+ @url = "#{Fronde::Config.settings['domain']}/#{@html_file}"
279
+ end
280
+
281
+ def init_empty_file
282
+ @title = @options[:title] || ''
283
+ @subtitle = ''
284
+ @date = DateTime.now
285
+ @notime = false
286
+ @author = @options[:author] || Fronde::Config.settings['author']
287
+ @keywords = []
288
+ @lang = @options[:lang] || Fronde::Config.settings['lang']
289
+ @excerpt = ''
290
+ body = @options[:content] || ''
291
+ @content = @options[:raw_content] || <<~ORG
292
+ #+title: #{@title}
293
+ #+date: <#{@date.strftime('%Y-%m-%d %a. %H:%M:%S')}>
294
+ #+author: #{@author}
295
+ #+language: #{@lang}
296
+
297
+ #{body}
298
+ ORG
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fronde
4
+ # This module holds class methods for the {Fronde::OrgFile} class.
5
+ module OrgFileClassMethods
6
+ def source_for_target(file_name)
7
+ # file_name may be frozen...
8
+ src = file_name.sub(/\.html\z/, '.org')
9
+ pubfolder = Fronde::Config.settings['public_folder']
10
+ src.sub!(/^#{pubfolder}\//, '')
11
+ # Look for match in each possible sources. The first found wins.
12
+ Fronde::Config.sources.each do |project|
13
+ if project['target'] == '.'
14
+ origin = File.join(project['path'], src)
15
+ else
16
+ origin = File.join(
17
+ project['path'], src.sub(/^#{project['target']}\//, '')
18
+ )
19
+ end
20
+ return origin if File.exist?(origin)
21
+ end
22
+ nil
23
+ end
24
+
25
+ def target_for_source(file_name, project, with_public_folder: true)
26
+ return nil if file_name.nil?
27
+ # file_name may be frozen...
28
+ target = file_name.sub(/\.org\z/, '.html').sub(/^#{Dir.pwd}\//, '')
29
+ if project.nil?
30
+ subfolder = File.basename(File.dirname(target))
31
+ target = File.basename(target)
32
+ target = "#{subfolder}/#{target}" if subfolder != '.'
33
+ else
34
+ project_relative_path = project['path'].sub(/^#{Dir.pwd}\//, '')
35
+ target.sub!(/^#{project_relative_path}\//, '')
36
+ target = "#{project['target']}/#{target}" if project['target'] != '.'
37
+ end
38
+ return target unless with_public_folder
39
+ pubfolder = Fronde::Config.settings['public_folder']
40
+ "#{pubfolder}/#{target}"
41
+ end
42
+
43
+ def project_for_source(file_name)
44
+ # Look for match in each possible sources. The first found wins.
45
+ Fronde::Config.sources.each do |project|
46
+ project_relative_path = project['path'].sub(/^#{Dir.pwd}\//, '')
47
+ return project if file_name.match?(/^#{project_relative_path}\//)
48
+ end
49
+ nil
50
+ end
51
+
52
+ def slug(title)
53
+ title.downcase.tr(' ', '-')
54
+ .encode('ascii', fallback: ->(k) { translit(k) })
55
+ .gsub(/[^\w-]/, '').delete_suffix('-')
56
+ end
57
+
58
+ private
59
+
60
+ def translit(char)
61
+ return 'a' if ['á', 'à', 'â', 'ä', 'ǎ', 'ã', 'å'].include?(char)
62
+ return 'e' if ['é', 'è', 'ê', 'ë', 'ě', 'ẽ'].include?(char)
63
+ return 'i' if ['í', 'ì', 'î', 'ï', 'ǐ', 'ĩ'].include?(char)
64
+ return 'o' if ['ó', 'ò', 'ô', 'ö', 'ǒ', 'õ'].include?(char)
65
+ return 'u' if ['ú', 'ù', 'û', 'ü', 'ǔ', 'ũ'].include?(char)
66
+ return 'y' if ['ý', 'ỳ', 'ŷ', 'ÿ', 'ỹ'].include?(char)
67
+ return 'c' if char == 'ç'
68
+ return 'n' if char == 'ñ'
69
+ '-'
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fronde
4
+ # This module holds extracter methods for the {Fronde::OrgFile} class.
5
+ module OrgFileExtracter
6
+ private
7
+
8
+ # Main method, which will call the other to initialize an
9
+ # {Fronde::OrgFile} instance.
10
+ def extract_data
11
+ @content = IO.read @file
12
+ @title = extract_title
13
+ @subtitle = extract_subtitle
14
+ @date = extract_date
15
+ @author = extract_author
16
+ @keywords = extract_keywords
17
+ @lang = extract_lang
18
+ @excerpt = extract_excerpt
19
+ end
20
+
21
+ def extract_date
22
+ timerx = '([0-9:]{5})(?::([0-9]{2}))?'
23
+ m = /^#\+date: *<([0-9-]{10}) [\w.]+(?: #{timerx})?> *$/i.match(@content)
24
+ return nil if m.nil?
25
+ @notime = m[2].nil?
26
+ if @notime
27
+ time = '00:00:00'
28
+ else
29
+ time = "#{m[2]}:#{m[3] || '00'}"
30
+ end
31
+ DateTime.strptime("#{m[1]} #{time}", '%Y-%m-%d %H:%M:%S')
32
+ end
33
+
34
+ def extract_title
35
+ m = /^#\+title:(.+)$/i.match(@content)
36
+ if m.nil?
37
+ # Avoid to leak absolute path
38
+ project_relative_path = @file.sub(/^#{Dir.pwd}\//, '')
39
+ return project_relative_path
40
+ end
41
+ m[1].strip
42
+ end
43
+
44
+ def extract_subtitle
45
+ m = /^#\+subtitle:(.+)$/i.match(@content)
46
+ return '' if m.nil?
47
+ m[1].strip
48
+ end
49
+
50
+ def extract_author
51
+ m = /^#\+author:(.+)$/i.match(@content)
52
+ return Fronde::Config.settings['author'] if m.nil?
53
+ m[1].strip
54
+ end
55
+
56
+ def extract_keywords
57
+ m = /^#\+keywords:(.+)$/i.match(@content)
58
+ return [] if m.nil?
59
+ m[1].split(',').map(&:strip)
60
+ end
61
+
62
+ def extract_lang
63
+ m = /^#\+language:(.+)$/i.match(@content)
64
+ return Fronde::Config.settings['lang'] if m.nil?
65
+ m[1].strip
66
+ end
67
+
68
+ def extract_excerpt
69
+ @content.scan(/^#\+description:(.+)$/i).map { |l| l[0].strip }.join(' ')
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fronde/config'
4
+ require 'fronde/emacs'
5
+
6
+ module Fronde
7
+ # This module holds HTML formatter methods for the {Fronde::OrgFile}
8
+ # class.
9
+ module OrgFileHtmlizer
10
+ private
11
+
12
+ # Format {Fronde::OrgFile#keywords} list in an HTML listing.
13
+ #
14
+ # @return [String] the HTML keywords list
15
+ def keywords_to_html
16
+ domain = Fronde::Config.settings['domain']
17
+ klist = @keywords.map do |k|
18
+ <<~KEYWORDLINK
19
+ <li class="keyword">
20
+ <a href="#{domain}/tags/#{Fronde::OrgFile.slug(k)}.html">#{k}</a>
21
+ </li>
22
+ KEYWORDLINK
23
+ end.join
24
+ "<ul class=\"keywords-list\">#{klist}</ul>"
25
+ end
26
+
27
+ # Format {Fronde::OrgFile#date} as a HTML `time` tag.
28
+ #
29
+ # @return [String] the HTML `time` tag
30
+ def date_to_html(dateformat = :full)
31
+ return '<time></time>' if @date.nil?
32
+ "<time datetime=\"#{@date.rfc3339}\">#{datestring(dateformat)}</time>"
33
+ end
34
+
35
+ # Format {Fronde::OrgFile#author} in a HTML `span` tag with a
36
+ # specific class.
37
+ #
38
+ # @return [String] the author HTML `span`
39
+ def author_to_html
40
+ "<span class=\"author\">#{@author}</span>"
41
+ end
42
+ end
43
+ end