fronde 0.3.4 → 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 +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
@@ -1,340 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
- require 'open-uri'
5
- require 'fronde/version'
6
-
7
- def fetch_org_version
8
- # Retrieve last org version from git repository tags page.
9
- tag_rx = Regexp.new(
10
- '<a href=\'/cgit/emacs/org-mode.git/tag/\?h=' \
11
- '(?<tag>release_(?<number>[^\']+))\'>\k<tag></a>'
12
- )
13
- versions = URI(
14
- 'https://git.savannah.gnu.org/cgit/emacs/org-mode.git/refs/'
15
- ).open.readlines.map do |line|
16
- line.match(tag_rx) { |matchdata| matchdata[:number] }
17
- end
18
- versions.compact.first
19
- end
20
-
21
- module Fronde
22
- # This module contains utilitary methods to ease ~org-config.el~
23
- # file generation
24
- module LispConfig
25
- # Fetch and return the last published version of Org.
26
- #
27
- # @return [String] the new x.x.x version string of Org
28
- def org_last_version
29
- return @org_version if @org_version
30
- if File.exist?('var/tmp/last_org_version')
31
- @org_version = File.read('var/tmp/last_org_version')
32
- return @org_version
33
- end
34
- @org_version = fetch_org_version
35
- FileUtils.mkdir_p 'var/tmp'
36
- File.write('var/tmp/last_org_version', @org_version)
37
- @org_version
38
- end
39
-
40
- # Generate emacs lisp configuration file for Org and write it.
41
- #
42
- # This method saves the generated configuration in the file
43
- # ~org-config.el~ at the root of your project, overwriting it if it
44
- # existed already.
45
- #
46
- # @return [Integer] the length written (as returned by the
47
- # underlying ~File.write~ method call)
48
- # rubocop:disable Metrics/MethodLength
49
- def write_org_lisp_config(with_tags: false)
50
- projects = org_generate_projects(with_tags: with_tags)
51
- workdir = Dir.pwd
52
- content = File.read(File.expand_path('./org-config.el', __dir__))
53
- .gsub('__VERSION__', Fronde::VERSION)
54
- .gsub('__WORK_DIR__', workdir)
55
- .gsub('__FRONDE_DIR__', __dir__)
56
- .gsub('__ORG_VER__', org_last_version)
57
- .gsub(
58
- '__ALL_PROJECTS__',
59
- projects.values.join("\n ")
60
- )
61
- .gsub('__THEME_CONFIG__', org_default_theme_config)
62
- .gsub('__ALL_PROJECTS_NAMES__', project_names(projects))
63
- .gsub('__LONG_DATE_FMT__', r18n_full_datetime_format)
64
- .gsub('__AUTHOR_EMAIL__', get('author_email', ''))
65
- .gsub('__AUTHOR_NAME__', get('author'))
66
- FileUtils.mkdir_p "#{workdir}/var/lib"
67
- File.write("#{workdir}/var/lib/org-config.el", content)
68
- end
69
- # rubocop:enable Metrics/MethodLength
70
-
71
- # Generate emacs directory variables file.
72
- #
73
- # This method generate the file ~.dir-locals.el~, which is
74
- # responsible to load fronde Org settings when visiting an Org file
75
- # of this fronde instance.
76
- #
77
- # @return [Integer] the length written (as returned by the
78
- # underlying ~File.write~ method call)
79
- def write_dir_locals
80
- workdir = Dir.pwd
81
- # rubocop:disable Layout/LineLength
82
- File.write(
83
- "#{workdir}/.dir-locals.el",
84
- "((org-mode . ((eval . (load-file \"#{workdir}/var/lib/org-config.el\")))))"
85
- )
86
- # rubocop:enable Layout/LineLength
87
- end
88
-
89
- private
90
-
91
- def r18n_full_datetime_format
92
- locale = R18n.get.locale
93
- date_fmt = R18n.t.fronde.index.full_date_format(
94
- date: locale.full_format
95
- )
96
- date_fmt = locale.year_format.sub('_', date_fmt)
97
- time_fmt = locale.time_format.delete('_').strip
98
- R18n.t.fronde.index.full_date_with_time_format(
99
- date: date_fmt, time: time_fmt
100
- )
101
- end
102
-
103
- def ruby_to_lisp_boolean(value)
104
- return 't' if value == true
105
- 'nil'
106
- end
107
-
108
- def project_names(projects)
109
- names = projects.keys.map do |p|
110
- ["\"#{p}\"", "\"#{p}-assets\""]
111
- end.flatten
112
- names << "\"theme-#{get('theme')}\"" unless get('theme') == 'default'
113
- sources.each do |s|
114
- # Default theme defined in settings is already included
115
- next unless s['theme'] && s['theme'] != get('theme')
116
- # Never include theme named 'default' as it does not rely on any
117
- # file to export.
118
- next if s['theme'] == 'default'
119
- theme = "\"theme-#{s['theme']}\""
120
- next if names.include? theme
121
- names << theme
122
- end
123
- names.join(' ')
124
- end
125
-
126
- # Return the full path to the publication path of a given project
127
- # configuration.
128
- #
129
- # @param project [Hash] a project configuration (as extracted from
130
- # the ~sources~ key)
131
- # @return [String] the full path to the target dir of this project
132
- def publication_path(project)
133
- publish_in = [Dir.pwd]
134
- if project['type'] == 'gemini'
135
- publish_in << get('gemini_public_folder', 'public_gmi')
136
- else
137
- publish_in << get('public_folder')
138
- end
139
- publish_in << project['target'] unless project['target'] == '.'
140
- publish_in.join('/')
141
- end
142
-
143
- # Return the publication function needed for a given project
144
- # configuration.
145
- #
146
- # @param project [Hash] a project configuration (as extracted from
147
- # the ~sources~ key)
148
- # @return [String] the org publication function name
149
- def publication_function(project)
150
- case project['type']
151
- when 'gemini'
152
- 'org-gmi-publish-to-gemini'
153
- else
154
- 'org-html-publish-to-html'
155
- end
156
- end
157
-
158
- def org_project(project_name, opts)
159
- shared_lines = org_project_shared_lines(opts)
160
- project_config = [
161
- org_project_config(project_name, opts, shared_lines),
162
- org_assets_config(project_name, shared_lines)
163
- ]
164
- themeconf = org_theme_config(opts['theme'])
165
- project_config << themeconf unless themeconf == ''
166
- project_config.join("\n ")
167
- end
168
-
169
- def org_default_html_postamble
170
- <<~POSTAMBLE
171
- <p><span class="author">#{R18n.t.fronde.org.postamble.written_by}</span>
172
- #{R18n.t.fronde.org.postamble.with_emacs_html}</p>
173
- <p class="date">#{R18n.t.fronde.org.postamble.last_modification}</p>
174
- <p class="validation">%v</p>
175
- POSTAMBLE
176
- end
177
-
178
- def org_default_gemini_postamble
179
- format(
180
- "📅 %<date>s\n📝 %<author>s %<creator>s",
181
- author: R18n.t.fronde.org.postamble.written_by,
182
- creator: R18n.t.fronde.org.postamble.with_emacs,
183
- date: R18n.t.fronde.org.postamble.last_modification
184
- )
185
- end
186
-
187
- def org_default_html_head
188
- <<~HTMLHEAD
189
- <link rel="stylesheet" type="text/css" media="screen"
190
- href="__DOMAIN__/assets/__THEME__/css/style.css">
191
- <link rel="stylesheet" type="text/css" media="screen"
192
- href="__DOMAIN__/assets/__THEME__/css/htmlize.css">
193
- __ATOM_FEED__
194
- HTMLHEAD
195
- end
196
-
197
- def org_default_html_options(project)
198
- defaults = {
199
- 'html-postamble' => org_default_html_postamble,
200
- 'html-head' => '__ATOM_FEED__',
201
- 'html-head-include-default-style' => 't',
202
- 'html-head-include-scripts' => 't'
203
- }
204
- curtheme = project['theme'] || get('theme')
205
- return defaults if curtheme.nil? || curtheme == 'default'
206
- defaults['html-head'] = org_default_html_head
207
- defaults['html-head-include-default-style'] = 'nil'
208
- defaults['html-head-include-scripts'] = 'nil'
209
- defaults
210
- end
211
-
212
- def org_publish_options(project)
213
- defaults = {
214
- 'section-numbers' => 'nil',
215
- 'with-toc' => 'nil'
216
- }
217
- if project['type'] == 'gemini'
218
- defaults['gemini-postamble'] = org_default_gemini_postamble
219
- else
220
- defaults.merge!(
221
- org_default_html_options(project),
222
- get('org-html', {}),
223
- project['org-html'] || {}
224
- )
225
- end
226
- defaults.merge(project['org-options'] || {})
227
- end
228
-
229
- def expand_vars_in_html_head(head, project)
230
- curtheme = project['theme'] || get('theme')
231
- # Head may be frozen when coming from settings
232
- head = head.gsub('__THEME__', curtheme)
233
- .gsub('__DOMAIN__', get('domain'))
234
- return head.gsub('__ATOM_FEED__', '') unless project['is_blog']
235
- atomfeed = <<~ATOMFEED
236
- <link rel="alternate" type="application/atom+xml" title="Atom 1.0"
237
- href="#{get('domain')}/feeds/index.xml" />
238
- ATOMFEED
239
- head.gsub('__ATOM_FEED__', atomfeed)
240
- end
241
-
242
- def cast_lisp_value(value)
243
- return 't' if value.is_a?(TrueClass)
244
- return 'nil' if value.nil? || value.is_a?(FalseClass)
245
- value.strip.gsub(/"/, '\"')
246
- end
247
-
248
- def build_project_org_headers(project)
249
- orgtplopts = org_publish_options(project)
250
- lisp_keywords = ['t', 'nil', '1', '-1', '0'].freeze
251
- orgtplopts.map do |k, v|
252
- v = expand_vars_in_html_head(v, project) if k == 'html-head'
253
- val = cast_lisp_value(v)
254
- if lisp_keywords.include? val
255
- ":#{k} #{val}"
256
- else
257
- ":#{k} \"#{val}\""
258
- end
259
- end
260
- end
261
-
262
- def org_generate_projects(with_tags: false)
263
- projects = {}
264
- projects_sources = sources
265
- if with_tags
266
- tags_conf = build_source('tags')
267
- tags_conf['recursive'] = false
268
- projects_sources << tags_conf
269
- end
270
- projects_sources.each do |opts|
271
- opts['org_headers'] = build_project_org_headers(opts)
272
- projects[opts['name']] = org_project(opts['name'], opts)
273
- end
274
- projects
275
- end
276
-
277
- def org_default_theme_config
278
- theme_config = org_theme_config(get('theme'))
279
- return theme_config if theme_config == ''
280
- format("\n %<conf>s", conf: theme_config)
281
- end
282
-
283
- def org_theme_config(theme)
284
- return '' if theme.nil? || theme == 'default'
285
- workdir = Dir.pwd
286
- [
287
- format('("theme-%<theme>s"', theme: theme),
288
- format(' :base-directory "%<wd>s/themes/%<theme>s"',
289
- wd: workdir, theme: theme),
290
- # rubocop:disable Layout/LineLength
291
- ' :base-extension "jpg\\\\\\|gif\\\\\\|png\\\\\\|js\\\\\\|css\\\\\\|otf\\\\\\|ttf\\\\\\|woff2?"',
292
- # rubocop:enable Layout/LineLength
293
- ' :recursive t',
294
- format(' :publishing-directory "%<wd>s/%<pub>s/assets/%<theme>s"',
295
- wd: workdir, pub: get('public_folder'), theme: theme),
296
- ' :publishing-function org-publish-attachment)'
297
- ].join("\n ").strip
298
- end
299
-
300
- def org_project_shared_lines(project)
301
- [
302
- format(':base-directory "%<path>s"', path: project['path']),
303
- format(
304
- ':publishing-directory "%<path>s"',
305
- path: publication_path(project)
306
- ),
307
- format(
308
- ':recursive %<rec>s',
309
- rec: ruby_to_lisp_boolean(project['recursive'])
310
- )
311
- ]
312
- end
313
-
314
- def org_project_config(project_name, project, shared_lines)
315
- project_lines = [
316
- format('"%<name>s"', name: project_name),
317
- ':base-extension "org"',
318
- format(
319
- ':publishing-function %<fun>s',
320
- fun: publication_function(project)
321
- )
322
- ] + shared_lines + project['org_headers']
323
- if project['exclude']
324
- project_lines << format(
325
- ':exclude "%<value>s"', value: project['exclude']
326
- )
327
- end
328
- format('(%<pr>s)', pr: project_lines.join("\n "))
329
- end
330
-
331
- def org_assets_config(project_name, shared_lines)
332
- assets_lines = [
333
- format('"%<name>s-assets"', name: project_name),
334
- ':base-extension "jpg\\\\\\|gif\\\\\\|png\\\\\\|svg\\\\\\|pdf"',
335
- ':publishing-function org-publish-attachment'
336
- ] + shared_lines
337
- format('(%<assets>s)', assets: assets_lines.join("\n "))
338
- end
339
- end
340
- end
@@ -1,19 +0,0 @@
1
- ;; Add org-mode to load path
2
- (add-to-list 'load-path (expand-file-name "org-__ORG_VER__/lisp" "__WORK_DIR__/lib"))
3
- ;; Load last version of htmlize.el
4
- (load-file (expand-file-name "htmlize.el" "__WORK_DIR__/lib"))
5
-
6
- ;; Current project options
7
- (setq fronde/version "__VERSION__"
8
- fronde/current-work-dir "__WORK_DIR__"
9
- user-mail-address "__AUTHOR_EMAIL__"
10
- user-full-name "__AUTHOR_NAME__"
11
- org-html-metadata-timestamp-format "__LONG_DATE_FMT__"
12
- org-gmi-timestamp-format "__LONG_DATE_FMT__"
13
- org-publish-project-alist
14
- `(__ALL_PROJECTS____THEME_CONFIG__
15
- ("website" :components (__ALL_PROJECTS_NAMES__))))
16
-
17
- ;; Load fronde lib
18
- (load-file (expand-file-name "ox-gmi.el" "__WORK_DIR__/lib"))
19
- (load-file (expand-file-name "ox-fronde.el" "__FRONDE_DIR__"))
@@ -1,72 +0,0 @@
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.get('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.get('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
@@ -1,72 +0,0 @@
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 = File.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.get('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.get('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
@@ -1,43 +0,0 @@
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.get('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