fronde 0.4.0 → 0.6.0

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