fronde 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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