fronde 0.4.0 → 0.6.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ext/nil_time.rb +3 -6
  3. data/lib/ext/time.rb +10 -17
  4. data/lib/ext/time_no_time.rb +27 -0
  5. data/lib/fronde/cli/commands.rb +18 -14
  6. data/lib/fronde/cli/data/fish_completion +20 -0
  7. data/lib/fronde/cli/data/gitignore +0 -1
  8. data/lib/fronde/cli/helpers.rb +0 -2
  9. data/lib/fronde/cli/opt_parse.rb +15 -18
  10. data/lib/fronde/cli/throbber.rb +35 -18
  11. data/lib/fronde/cli.rb +4 -3
  12. data/lib/fronde/config/data/org-config.el +3 -2
  13. data/lib/fronde/config/data/ox-fronde.el +91 -46
  14. data/lib/fronde/config/data/themes/umaneti/css/htmlize.css +364 -0
  15. data/lib/fronde/config/data/themes/umaneti/css/style.css +250 -0
  16. data/lib/fronde/config/data/themes/umaneti/img/bottom.png +0 -0
  17. data/lib/fronde/config/data/themes/umaneti/img/content.png +0 -0
  18. data/lib/fronde/config/data/themes/umaneti/img/tic.png +0 -0
  19. data/lib/fronde/config/data/themes/umaneti/img/top.png +0 -0
  20. data/lib/fronde/config/helpers.rb +1 -19
  21. data/lib/fronde/config/lisp.rb +14 -7
  22. data/lib/fronde/config.rb +47 -31
  23. data/lib/fronde/emacs.rb +23 -9
  24. data/lib/fronde/index/atom_generator.rb +1 -1
  25. data/lib/fronde/index/data/all_tags.org +6 -1
  26. data/lib/fronde/index/data/template.org +8 -4
  27. data/lib/fronde/index/org_generator.rb +10 -6
  28. data/lib/fronde/index.rb +19 -17
  29. data/lib/fronde/org/file.rb +71 -39
  30. data/lib/fronde/org/file_extracter.rb +23 -12
  31. data/lib/fronde/org.rb +14 -12
  32. data/lib/fronde/slug.rb +39 -12
  33. data/lib/fronde/source/gemini.rb +4 -9
  34. data/lib/fronde/source/html.rb +9 -9
  35. data/lib/fronde/source.rb +17 -12
  36. data/lib/fronde/sync/neocities.rb +220 -0
  37. data/lib/fronde/sync/rsync.rb +46 -0
  38. data/lib/fronde/sync.rb +32 -0
  39. data/lib/fronde/templater.rb +35 -51
  40. data/lib/fronde/version.rb +1 -1
  41. data/lib/tasks/cli.rake +45 -13
  42. data/lib/tasks/org.rake +30 -35
  43. data/lib/tasks/site.rake +63 -41
  44. data/lib/tasks/sync.rake +19 -50
  45. data/lib/tasks/tags.rake +2 -2
  46. data/locales/en.yml +143 -81
  47. data/locales/fr.yml +153 -89
  48. metadata +56 -17
  49. data/lib/ext/r18n.rb +0 -17
data/lib/fronde/config.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
- require 'r18n-core'
4
+ require 'i18n'
5
5
  require 'singleton'
6
6
 
7
7
  require_relative 'config/lisp'
@@ -39,16 +39,16 @@ module Fronde
39
39
 
40
40
  def initialize
41
41
  @default_settings = {
42
- 'author' => (ENV['USER'] || ''),
42
+ 'author' => ENV['USER'] || '',
43
43
  'domain' => '',
44
44
  'lang' => Fronde::Config::Helpers.extract_lang_from_env('en'),
45
45
  'html_public_folder' => 'public_html',
46
46
  'gemini_public_folder' => 'public_gmi',
47
47
  'templates' => [], 'theme' => 'default'
48
48
  }.freeze
49
- @org_version = @sources = nil
50
- @config = load_settings
51
49
  # Do not load sources now to avoid dependency loop on config
50
+ @sources = nil
51
+ @config = load_settings
52
52
  end
53
53
 
54
54
  include Fronde::Config::Lisp
@@ -93,7 +93,7 @@ module Fronde
93
93
  def reset
94
94
  # Reload config, taking default settings into account
95
95
  @config = load_settings
96
- @org_version = @sources = nil
96
+ @sources = nil
97
97
  @sources = load_sources
98
98
  end
99
99
 
@@ -109,7 +109,7 @@ module Fronde
109
109
  # @return [Fronde::Config::Store] self
110
110
  def load_test(config)
111
111
  @config = @default_settings.merge config
112
- @org_version = @sources = nil
112
+ @sources = nil
113
113
  @sources = load_sources
114
114
  self
115
115
  end
@@ -141,33 +141,57 @@ module Fronde
141
141
  get('sources', default_sources).filter_map do |source_conf|
142
142
  config = Source.canonical_config source_conf.dup
143
143
  unless config['path']
144
- warn R18n.t.fronde.error.source.no_path(source: config.inspect)
144
+ warn I18n.t('fronde.error.source.no_path', source: config.inspect)
145
145
  next
146
146
  end
147
147
  Source.new_from_config config
148
148
  end
149
149
  end
150
150
 
151
+ def check_duplicate_and_warn(collection, source, type)
152
+ path = source['path']
153
+ return path unless collection[type].has_key?(path)
154
+
155
+ warn(
156
+ I18n.t(
157
+ 'fronde.error.source.duplicate',
158
+ source: source['name'], type: type
159
+ )
160
+ )
161
+ end
162
+
151
163
  def remove_duplicate(sources)
152
164
  check_paths = {}
153
165
  sources.each do |source|
154
166
  type = source.type
155
167
  check_paths[type] ||= {}
156
- path = source['path']
168
+ path = check_duplicate_and_warn check_paths, source, type
157
169
  # Avoid duplicate
158
- if check_paths[type].has_key?(path)
159
- warn(
160
- R18n.t.fronde.error.source.duplicate(
161
- source: source['name'], type: type
162
- )
163
- )
164
- next
165
- end
170
+ next unless path
171
+
166
172
  check_paths[type][path] = source
167
173
  end
168
174
  check_paths
169
175
  end
170
176
 
177
+ def filter_possible_matchs(path, other_paths_list)
178
+ other_paths_list.select do |other_path|
179
+ path != other_path && other_path.start_with?(path)
180
+ end
181
+ end
182
+
183
+ def warn_on_existing_inclusion(type, other, possible_matchs, sources)
184
+ possible_matchs.each do |match|
185
+ warn(
186
+ I18n.t(
187
+ 'fronde.error.source.inclusion',
188
+ source: sources[match]['title'],
189
+ other_source: other, type: type
190
+ )
191
+ )
192
+ end
193
+ end
194
+
171
195
  def remove_inclusion(check_paths)
172
196
  check_paths.map do |type, sources_by_path|
173
197
  skip_paths = []
@@ -183,21 +207,13 @@ module Fronde
183
207
  next source unless source.recursive?
184
208
 
185
209
  # Ensure that the current source does not embed another one
186
- possible_matchs = sorted_paths.select do |other_path|
187
- path != other_path && other_path.start_with?(path)
188
- end
210
+ possible_matchs = filter_possible_matchs path, sorted_paths
189
211
  next source if possible_matchs.empty?
190
212
 
191
213
  skip_paths += possible_matchs
192
- possible_matchs.each do |match|
193
- other_source = sources_by_path[match]
194
- warn(
195
- R18n.t.fronde.error.source.inclusion(
196
- source: other_source['title'], type: type,
197
- other_source: source['title']
198
- )
199
- )
200
- end
214
+ warn_on_existing_inclusion(
215
+ type, source['title'], possible_matchs, sources_by_path
216
+ )
201
217
  end
202
218
  end.flatten
203
219
  end
@@ -207,8 +223,8 @@ module Fronde
207
223
  CONFIG = Config::Store.instance
208
224
  end
209
225
 
210
- R18n.default_places = File.expand_path('../../locales', __dir__)
211
- R18n::Filters.on(:named_variables)
212
- R18n.set Fronde::CONFIG.get('lang')
226
+ i18n_glob = File.expand_path('../../locales', __dir__)
227
+ I18n.load_path = Dir.glob("#{i18n_glob}/*.yml")
228
+ I18n.default_locale = Fronde::CONFIG.get('lang')
213
229
 
214
230
  Fronde::CONFIG.load_sources
data/lib/fronde/emacs.rb CHANGED
@@ -10,8 +10,22 @@ module Fronde
10
10
  @command = nil
11
11
  end
12
12
 
13
- def publish(project = 'website')
14
- build_command("(org-publish \"#{project}\")")
13
+ def publish(project = 'website', force: false)
14
+ if force
15
+ build_command %[(org-publish "#{project}" t)]
16
+ else
17
+ build_command %[(org-publish "#{project}")]
18
+ end
19
+ run_command
20
+ end
21
+
22
+ def publish_file(file_path, force: false)
23
+ if force
24
+ build_command '(org-publish-current-file t)'
25
+ else
26
+ build_command '(org-publish-current-file)'
27
+ end
28
+ @command.insert(-2, %(--visit "#{file_path}"))
15
29
  run_command
16
30
  end
17
31
 
@@ -20,21 +34,21 @@ module Fronde
20
34
  def run_command
21
35
  cmd = @command.join(' ')
22
36
  if @verbose
23
- warn cmd
24
- return system(cmd, exception: true)
37
+ puts cmd
38
+ return system(cmd, err: $stdout, exception: true)
25
39
  end
26
- system cmd, out: '/dev/null', err: '/dev/null', exception: true
40
+ system cmd, out: File::NULL, err: File::NULL, exception: true
27
41
  end
28
42
 
29
43
  def build_command(org_action)
30
44
  default_emacs = Fronde::CONFIG.get('emacs')
31
- @command = [default_emacs || 'emacs -Q --batch -nw']
32
- @command << '--eval \'(setq inhibit-message t)\'' unless @verbose
33
- @command += [
34
- '--eval \'(setq enable-dir-local-variables nil)\'',
45
+ @command = [
46
+ default_emacs || 'emacs -Q --batch -nw',
47
+ '--eval \'(setq inhibit-message t)\'',
35
48
  '-l ./var/lib/org-config.el',
36
49
  "--eval '#{org_action}'"
37
50
  ]
51
+ @command.delete_at(1) if @verbose
38
52
  end
39
53
  end
40
54
  end
@@ -22,7 +22,7 @@ module Fronde
22
22
  FileUtils.mkdir_p "#{@project.publication_path}/feeds"
23
23
  @index.each_key do |tag|
24
24
  write_atom(tag)
25
- warn R18n.t.fronde.index.atom_generated(tag: tag) if verbose
25
+ puts I18n.t('fronde.index.atom_generated', tag:) if verbose
26
26
  end
27
27
  end
28
28
 
@@ -4,11 +4,16 @@
4
4
  {% for index in indexes %}
5
5
  * {{ index.title }}
6
6
  :PROPERTIES:
7
- :HTML_CONTAINER_CLASS: index-tags
7
+ {%- if project_type == 'html' %}
8
+ :HTML_CONTAINER_CLASS: index-tags{% endif %}
8
9
  :UNNUMBERED: notoc
9
10
  :END:
10
11
 
11
12
  {% for tag in index.tags -%}
13
+ {%- if project_type == 'gemini' -%}
14
+ [[{{ domain }}{{ project_path }}tags/{{ tag.slug }}.gmi][{{ tag.title }} ({{ tag.weight }})]]
15
+ {%- else -%}
12
16
  - [[{{ domain }}{{ project_path }}tags/{{ tag.slug }}.html][{{ tag.title }}]] ({{ tag.weight }})
17
+ {%- endif %}
13
18
  {% endfor %}
14
19
  {%- endfor -%}
@@ -1,22 +1,26 @@
1
1
  #+title: {{ title }}
2
2
  #+author: {{ author }}
3
3
  #+language: {{ lang }}
4
- {%- unless slug == '__HOME_PAGE__' %}
4
+ {%- if project_type == 'html' and slug != '__HOME_PAGE__' %}
5
5
  #+html_head_extra: <link rel="alternate" type="application/atom+xml" title="{{ title }}" href="{{ domain }}{{ project_path }}feeds/{{ slug }}.xml" />
6
- {% endunless -%}
6
+ {% endif -%}
7
7
  {%- assign last_year = 0 -%}
8
8
  {% for article in entries %}
9
9
  {% assign cur_year = article.timekey | slice: 0, 4 %}
10
10
  {%- unless cur_year == last_year %}
11
11
  * {% if cur_year == "0000" %}{{ unsorted }}{% else %}{{ cur_year }}{% endif %}
12
12
  :PROPERTIES:
13
- :HTML_CONTAINER_CLASS: index-year
13
+ {%- if project_type == 'html' %}
14
+ :HTML_CONTAINER_CLASS: index-year{% endif %}
14
15
  :UNNUMBERED: notoc
15
16
  :END:
16
17
  {% assign last_year = cur_year %}
17
18
  {% endunless -%}
19
+ {%- if project_type == 'gemini' -%}
20
+ [[{{ article.url }}][{% if article.published != '' %}{{ article.published_gemini_index }} {% endif %}{{ article.title }}]]
21
+ {%- else -%}
18
22
  - *[[{{ article.url }}][{{ article.title }}]]*
19
23
  {%- if article.published != '' %} / {{ article.published }}{% endif -%}
20
24
  {%- if article.excerpt != '' %} \\
21
- {{ article.excerpt }}{% endif %}
25
+ {{ article.excerpt }}{% endif %}{% endif %}
22
26
  {%- endfor %}
@@ -30,7 +30,7 @@ module Fronde
30
30
  FileUtils.mkdir_p "#{@project['path']}/tags"
31
31
  @index.each_key do |tag|
32
32
  write_org(tag)
33
- warn R18n.t.fronde.index.index_generated(tag: tag) if verbose
33
+ puts I18n.t('fronde.index.index_generated', tag:) if verbose
34
34
  end
35
35
  write_blog_home_page(verbose)
36
36
  end
@@ -47,7 +47,9 @@ module Fronde
47
47
  entries.map! do |article|
48
48
  published = article['published']
49
49
  unless published == ''
50
- article['published'] = R18n.t.fronde.index.published_on published
50
+ article['published'] = I18n.with_locale(article['lang']) do
51
+ I18n.t('fronde.index.published_on', date: published)
52
+ end
51
53
  end
52
54
  article
53
55
  end
@@ -56,10 +58,11 @@ module Fronde
56
58
  'title' => title,
57
59
  'slug' => slug,
58
60
  'project_path' => @project.public_absolute_path,
61
+ 'project_type' => @project.type,
59
62
  'domain' => Fronde::CONFIG.get('domain'),
60
63
  'lang' => Fronde::CONFIG.get('lang'),
61
64
  'author' => Fronde::CONFIG.get('author'),
62
- 'unsorted' => R18n.t.fronde.index.unsorted,
65
+ 'unsorted' => I18n.t('fronde.index.unsorted'),
63
66
  'entries' => entries
64
67
  )
65
68
  end
@@ -72,15 +75,16 @@ module Fronde
72
75
  'weight' => @index[tag].length
73
76
  }
74
77
  end
75
- { 'title' => R18n.t.fronde.index.send(title), 'tags' => all_tags }
78
+ { 'title' => I18n.t("fronde.index.#{title}"), 'tags' => all_tags }
76
79
  end
77
80
  Config::Helpers.render_liquid_template(
78
81
  File.read(File.expand_path('./data/all_tags.org', __dir__)),
79
- 'title' => R18n.t.fronde.index.all_tags,
82
+ 'title' => I18n.t('fronde.index.all_tags'),
80
83
  'lang' => Fronde::CONFIG.get('lang'),
81
84
  'author' => Fronde::CONFIG.get('author'),
82
85
  'domain' => Fronde::CONFIG.get('domain'),
83
86
  'project_path' => @project.public_absolute_path,
87
+ 'project_type' => @project.type,
84
88
  'indexes' => indexes
85
89
  )
86
90
  end
@@ -88,7 +92,7 @@ module Fronde
88
92
  def write_blog_home_page(verbose)
89
93
  orgdest = format('%<root>s/index.org', root: @project['path'])
90
94
  if verbose
91
- warn R18n.t.fronde.org.generate_blog_index(name: @project['name'])
95
+ puts I18n.t('fronde.org.generate_blog_index', name: @project['name'])
92
96
  end
93
97
  File.write(orgdest, blog_home_page)
94
98
  end
data/lib/fronde/index.rb CHANGED
@@ -8,7 +8,7 @@ module Fronde
8
8
  # Generates website indexes and atom feeds for all the org documents
9
9
  # keywords.
10
10
  class Index
11
- attr_reader :date
11
+ attr_reader :date, :project
12
12
 
13
13
  def initialize(project)
14
14
  @project = project
@@ -30,26 +30,28 @@ module Fronde
30
30
 
31
31
  def sort_by(kind)
32
32
  accepted_values = %i[name weight]
33
- if accepted_values.include?(kind)
34
- tags_sorted = sort_tags_by_name_and_weight["by_#{kind}".to_sym]
35
- # Reverse in order to have most important or A near next prompt
36
- # and avoid to scroll to find the beginning of the list.
37
- return tags_sorted.map do |tag|
38
- @tags_names[tag] + " (#{@index[tag].length})"
39
- end.reverse
33
+ unless accepted_values.include?(kind)
34
+ error_msg = I18n.t(
35
+ 'fronde.error.index.wrong_sort_kind',
36
+ kind: kind, accepted_values: accepted_values.inspect
37
+ )
38
+ raise ArgumentError, error_msg
40
39
  end
41
- error_msg = R18n.t.fronde.error.index.wrong_sort_kind(
42
- kind: kind, accepted_values: accepted_values.inspect
43
- )
44
- raise ArgumentError, error_msg
40
+ sort_tags_by_name_and_weight[:"by_#{kind}"].map do |tag|
41
+ @tags_names[tag] + " (#{@index[tag].length})"
42
+ end.reverse
43
+ # Reverse in order to have most important or A near next prompt
44
+ # and avoid to scroll to find the beginning of the list.
45
+ end
46
+
47
+ def emacs_keywords
48
+ @tags_names.map { |slug, title| "#{title}\x1f#{slug}" }.join("\x1e")
45
49
  end
46
50
 
47
51
  class << self
48
- def all_html_blog_index(&block)
52
+ def all_blog_index(&block)
49
53
  all_blogs = CONFIG.sources.filter_map do |project|
50
- next unless project['type'] == 'html' && project.blog?
51
-
52
- Index.new(project)
54
+ Index.new(project) if project.blog?
53
55
  end
54
56
  return all_blogs unless block
55
57
 
@@ -94,7 +96,7 @@ module Fronde
94
96
  all_keys = all_tags
95
97
  {
96
98
  by_name: all_keys.sort,
97
- by_weight: all_keys.sort_by { @index[_1].length }.reverse
99
+ by_weight: all_keys.sort_by { [-@index[_1].length, _1] }
98
100
  }
99
101
  end
100
102
  end
@@ -109,6 +109,21 @@ module Fronde
109
109
  @data[:date].strftime('%Y%m%d%H%M%S')
110
110
  end
111
111
 
112
+ # Returns the path to the published version of this document.
113
+ #
114
+ # By default, this method returns the relative path to the published
115
+ # file. If the ~absolute~ argument is true, it will return the absolute
116
+ # path to the published file.
117
+ #
118
+ # @param absolute [Boolean] whether to display absolute or relative
119
+ # published file path (default false)
120
+ # @return [String] the document key
121
+ def pub_file(absolute: false)
122
+ return @data[:pub_file] unless absolute
123
+
124
+ "#{@project['folder']}#{@data[:pub_file]}"
125
+ end
126
+
112
127
  # Formats given ~string~ with values of the current Org::File.
113
128
  #
114
129
  # This method expects to find percent-tags in the given ~string~
@@ -118,42 +133,60 @@ module Fronde
118
133
  #
119
134
  # *** Format:
120
135
  #
121
- # - %a :: the raw author name
136
+ # - %a :: the raw author name.
122
137
  # - %A :: the HTML rendering of the author name, equivalent to
123
- # ~<span class="author">%a</span>~
138
+ # ~<span class="author">%a</span>~.
124
139
  # - %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
140
+ # to ~<time datetime="%I">%i</time>~.
141
+ # - %D :: the ~:full~ date and time HTML representation.
142
+ # - %F :: the ~link~ HTML tag for the main Atom feed of the
143
+ # current file source.
144
+ # - %h :: the declared host/domain name, taken from the
145
+ # {Fronde::Config#settings}.
146
+ # - %i :: the raw ~:short~ date and time.
147
+ # - %I :: the raw ~:iso8601~ date and time.
148
+ # - %k :: the document keywords separated by commas.
149
+ # - %K :: the HTML list rendering of the keywords.
150
+ # - %l :: the lang of the document.
132
151
  # - %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)
152
+ # {Fronde::Config#settings}.
153
+ # - %n :: the fronde name and version.
154
+ # - %N :: the fronde name and version with a link to the project
155
+ # home on the name.
156
+ # - %o :: the theme name (~o~ as in Outfit) of the current file source.
157
+ # - %s :: the subtitle of the document (from ~#+subtitle:~).
158
+ # - %t :: the title of the document (from ~#+title:~).
159
+ # - %u :: the URL to the related published HTML document.
160
+ # - %x :: the raw description (~x~ as in eXcerpt) of the document
161
+ # (from ~#+description:~).
141
162
  # - %X :: the description, enclosed in an HTML ~p~ tag, equivalent
142
- # to ~<p>%x</p>~
163
+ # to ~<p>%x</p>~.
143
164
  #
144
165
  # @example
145
166
  # org_file.format("Article written by %a the %d")
146
167
  # => "Article written by Alice Smith the Wednesday 3rd July"
147
168
  #
169
+ # @param string [String] the template text to edit
148
170
  # @return [String] the given ~string~ after replacement occurs
149
- # rubocop:disable Metrics/MethodLength
150
171
  # rubocop:disable Layout/LineLength
151
172
  def format(string)
173
+ project_data = @project.to_h
174
+ # NOTE: The following keycode are reserved by Org itself:
175
+ # %a (author), %c (creator), %C (input-file), %d (date),
176
+ # %e (email), %s (subtitle), %t (title), %T (timestamp),
177
+ # %v (html validation link)
178
+ localized_dates = I18n.with_locale(@data[:lang]) do
179
+ { short: @data[:date].l18n_short_date_string,
180
+ short_html: @data[:date].l18n_short_date_html,
181
+ long_html: @data[:date].l18n_long_date_html }
182
+ end
152
183
  string.gsub('%a', @data[:author])
153
184
  .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)
185
+ .gsub('%d', localized_dates[:short_html])
186
+ .gsub('%D', localized_dates[:long_html])
187
+ .gsub('%F', project_data['atom_feed'] || '')
188
+ .gsub('%h', project_data['domain'] || '')
189
+ .gsub('%i', localized_dates[:short])
157
190
  .gsub('%I', @data[:date].xmlschema)
158
191
  .gsub('%k', @data[:keywords].join(', '))
159
192
  .gsub('%K', keywords_to_html)
@@ -161,6 +194,7 @@ module Fronde
161
194
  .gsub('%L', Fronde::CONFIG.get('license', '').gsub(/\s+/, ' ').strip)
162
195
  .gsub('%n', "Fronde #{Fronde::VERSION}")
163
196
  .gsub('%N', "<a href=\"https://git.umaneti.net/fronde/about/\">Fronde</a> #{Fronde::VERSION}")
197
+ .gsub('%o', project_data['theme'] || '')
164
198
  .gsub('%s', @data[:subtitle])
165
199
  .gsub('%t', @data[:title])
166
200
  .gsub('%u', @data[:url] || '')
@@ -168,7 +202,6 @@ module Fronde
168
202
  .gsub('%X', "<p>#{@data[:excerpt]}</p>")
169
203
  end
170
204
  # rubocop:enable Layout/LineLength
171
- # rubocop:enable Metrics/MethodLength
172
205
 
173
206
  # Writes the current Org::File content to the underlying file.
174
207
  #
@@ -179,18 +212,17 @@ module Fronde
179
212
  def write
180
213
  if ::File.directory? @file
181
214
  if @data[:title] == ''
182
- raise R18n.t.fronde.error.org_file.no_file_or_title
215
+ raise I18n.t('fronde.error.org_file.no_file_or_title')
183
216
  end
184
217
 
185
218
  @file = ::File.join @file, "#{Slug.slug(@data[:title])}.org"
186
219
  else
187
- file_dir = ::File.dirname @file
188
- FileUtils.mkdir_p file_dir
220
+ FileUtils.mkdir_p ::File.dirname(@file)
189
221
  end
190
222
  ::File.write @file, @data[:content]
191
223
  end
192
224
 
193
- def method_missing(method_name, *args, &block)
225
+ def method_missing(method_name, *args, &)
194
226
  reader_method = method_name.to_s.delete_suffix('=').to_sym
195
227
  if @data.has_key? reader_method
196
228
  return @data[reader_method] if reader_method == method_name
@@ -210,11 +242,14 @@ module Fronde
210
242
  end
211
243
 
212
244
  def to_h
213
- fields = %w[author excerpt keywords timekey title url]
245
+ fields = %w[author excerpt keywords lang timekey title url]
214
246
  data = fields.to_h { |key| [key, send(key)] }
215
247
  data['published_body'] = extract_published_body
216
248
  pub_date = @data[:date]
217
- data['published'] = pub_date.l18n_long_date_string(with_year: false)
249
+ data['published'] = I18n.with_locale(@data[:lang]) do
250
+ pub_date.l18n_long_date_no_year_string
251
+ end
252
+ data['published_gemini_index'] = pub_date.strftime('%Y-%m-%d')
218
253
  data['published_xml'] = pub_date.xmlschema
219
254
  data['updated_xml'] = @data[:updated]&.xmlschema
220
255
  data
@@ -228,14 +263,14 @@ module Fronde
228
263
  else
229
264
  source = find_source_for_publication_file
230
265
  end
231
- warn R18n.t.fronde.error.org_file.no_project(file: @file) unless source
232
- source
266
+ return source if source
267
+
268
+ short_file = @file.sub(/^#{Dir.pwd}/, '.')
269
+ warn I18n.t('fronde.error.org_file.no_project', file: short_file)
233
270
  end
234
271
 
235
272
  def find_source_for_org_file
236
- Fronde::CONFIG.sources.find do |project|
237
- project.source_for? @file
238
- end
273
+ Fronde::CONFIG.sources.find { _1.source_for? @file }
239
274
  end
240
275
 
241
276
  def find_source_for_publication_file
@@ -250,20 +285,17 @@ module Fronde
250
285
  def init_empty_file
251
286
  @data = {
252
287
  title: @options[:title] || '', subtitle: '', excerpt: '',
253
- date: Time.now,
254
288
  author: @options[:author] || Fronde::CONFIG.get('author'),
255
- keywords: [],
256
289
  lang: @options[:lang] || Fronde::CONFIG.get('lang'),
257
- pub_file: nil, url: nil
290
+ date: Time.now, keywords: [], pub_file: nil, url: nil
258
291
  }
259
- body = @options[:content] || ''
260
292
  @data[:content] = @options[:raw_content] || <<~ORG
261
293
  #+title: #{@data[:title]}
262
294
  #+date: <#{@data[:date].strftime('%Y-%m-%d %a. %H:%M:%S')}>
263
295
  #+author: #{@data[:author]}
264
296
  #+language: #{@data[:lang]}
265
297
 
266
- #{body}
298
+ #{@options[:content]}
267
299
  ORG
268
300
  end
269
301
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  using TimePatch
4
4
 
5
+ require_relative '../../ext/time_no_time'
6
+
5
7
  module Fronde
6
8
  module Org
7
9
  # This module holds extracter methods for the {Fronde::Org::File}
@@ -14,37 +16,42 @@ module Fronde
14
16
  def extract_data
15
17
  @data = { content: ::File.read(@file), pub_file: nil, url: nil }
16
18
  %i[title subtitle date author keywords lang excerpt].each do |param|
17
- @data[param] = send("extract_#{param}".to_sym)
19
+ @data[param] = send(:"extract_#{param}")
18
20
  end
19
21
  return unless @project
20
22
 
23
+ warn_if_dangerous_code_block
21
24
  @data[:updated] = ::File.mtime(@file)
22
25
  @data[:pub_file] = @project.target_for @file
23
26
  @data[:url] = Fronde::CONFIG.get('domain') + @data[:pub_file]
24
27
  end
25
28
 
29
+ def warn_if_dangerous_code_block
30
+ code_block_rx = /^#\+begin_src.+:exports (?:results|both).*$/i
31
+ return unless code_block_rx.match?(@data[:content])
32
+
33
+ warn I18n.t('fronde.error.org_file.dangerous_code_block', file: @file)
34
+ end
35
+
26
36
  def extract_date
27
37
  timerx = '([0-9:]{5})(?::([0-9]{2}))?'
28
38
  daterx = /^#\+date: *<([0-9-]{10}) [\w.]+(?: #{timerx})?> *$/i
29
39
  match = daterx.match(@data[:content])
30
40
  return NilTime.new if match.nil?
31
41
 
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
42
+ return TimeNoTime.parse_no_time(match[1]) if match[2].nil?
43
+
44
+ Time.strptime(
45
+ "#{match[1]} #{match[2]}:#{match[3] || '00'}",
46
+ '%Y-%m-%d %H:%M:%S'
47
+ )
41
48
  end
42
49
 
43
50
  def extract_title
44
51
  match = /^#\+title:(.+)$/i.match(@data[:content])
45
52
  if match.nil?
46
53
  # Avoid to leak absolute path
47
- project_relative_path = @file.sub(/^#{Dir.pwd}\//, '')
54
+ project_relative_path = @file.sub %r{^#{Dir.pwd}/}, ''
48
55
  return project_relative_path
49
56
  end
50
57
  match[1].strip
@@ -81,13 +88,17 @@ module Fronde
81
88
  # Always return something, even when not published yet
82
89
  return @data[:excerpt] unless pub_file && @project
83
90
 
84
- project_type = @project['type']
91
+ project_type = @project.type
85
92
  pub_folder = Fronde::CONFIG.get("#{project_type}_public_folder")
86
93
  file_name = pub_folder + pub_file
87
94
  return @data[:excerpt] unless ::File.exist? file_name
88
95
 
89
96
  return ::File.read(file_name) if project_type == 'gemini'
90
97
 
98
+ read_html_body file_name
99
+ end
100
+
101
+ def read_html_body(file_name)
91
102
  dom = ::File.open(file_name, 'r') { |file| Nokogiri::HTML file }
92
103
  body = dom.css('div#content')
93
104
  body.css('header').unlink # Remove the main title