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,17 @@
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-publish-project-alist
13
+ `(__ALL_PROJECTS____THEME_CONFIG__
14
+ ("website" :components (__ALL_PROJECTS_NAMES__))))
15
+
16
+ ;; Load fronde lib
17
+ (load-file (expand-file-name "ox-fronde.el" "__FRONDE_DIR__"))
@@ -0,0 +1,114 @@
1
+ ;;; ox-fronde.el --- Fronde Gem specific helpers for Org Export Engine -*- lexical-binding: t; -*-
2
+
3
+ ;; Copyright (C) 2011-2019 Free Software Foundation, Inc.
4
+
5
+ ;; Author: Étienne Deparis <etienne at depar dot is>
6
+ ;; Keywords: org, export
7
+
8
+ ;; This file is not part of GNU Emacs.
9
+
10
+ ;; GNU Emacs is free software: you can redistribute it and/or modify
11
+ ;; it under the terms of the GNU General Public License as published by
12
+ ;; the Free Software Foundation, either version 3 of the License, or
13
+ ;; (at your option) any later version.
14
+
15
+ ;; GNU Emacs is distributed in the hope that it will be useful,
16
+ ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ ;; GNU General Public License for more details.
19
+
20
+ ;; You should have received a copy of the GNU General Public License
21
+ ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
22
+
23
+ ;;; Commentary:
24
+
25
+ ;; This library implements specific helpers function, needed by the Ruby
26
+ ;; Gem Fronde, which offers an easy way to publish a static website
27
+ ;; using Org files as sources.
28
+
29
+ ;;; Code:
30
+
31
+ (require 'org)
32
+ (require 'ox-html)
33
+
34
+ ;;; Function Declarations
35
+
36
+ (defvar fronde/version ""
37
+ "Version of the current fronde installation")
38
+
39
+ (defvar fronde/current-work-dir nil
40
+ "Location of the current fronde website base directory.")
41
+
42
+ (defvar fronde/org-temp-dir nil
43
+ "Location of the local Org temporary directory (where to place
44
+ org timestamps and id locations).")
45
+
46
+ (defun fronde/org-html-format-spec (upstream info)
47
+ "Return format specification for preamble and postamble.
48
+ INFO is a plist used as a communication channel."
49
+ (let ((output (funcall upstream info)))
50
+ (push `(?A . ,(format "<span class=\"author\">%s</span>"
51
+ (org-export-data (plist-get info :author) info)))
52
+ output)
53
+ (push `(?k . ,(org-export-data (plist-get info :keywords) info)) output)
54
+ (push `(?K . ,(format "<ul class=\"keywords-list\">%s</ul>"
55
+ (mapconcat
56
+ (lambda (k) (format "<li class=\"keyword\">%s</li>" k))
57
+ (split-string (or (plist-get info :keywords) "") ",+ *")
58
+ "\n")))
59
+ output)
60
+ (push `(?l . ,(org-export-data (plist-get info :language) info)) output)
61
+ (push `(?n . ,(format "Fronde %s" fronde/version)) output)
62
+ (push `(?N . ,(format "<a href=\"https://git.umaneti.net/fronde/about/\">Fronde</a> %s" fronde/version)) output)
63
+ (push `(?x . ,(org-export-data (plist-get info :description) info)) output)
64
+ (push `(?X . ,(format "<p>%s</p>"
65
+ (org-export-data (plist-get info :description) info)))
66
+ output)))
67
+
68
+ (defun fronde/org-i18n-export (link description format)
69
+ "Export a i18n link"
70
+ (let* ((splitted-link (split-string link "|"))
71
+ (path (car splitted-link))
72
+ (desc (or description path))
73
+ (lang (cadr splitted-link)))
74
+ (pcase format
75
+ (`html (if lang
76
+ (format "<a href=\"%s\" hreflang=\"%s\">%s</a>"
77
+ path lang desc)
78
+ (format "<a href=\"%s\">%s</a>" path desc)))
79
+ (`latex (format "\\href{%s}{%s}" path desc))
80
+ (`ascii (format "%s (%s)" desc path))
81
+ (_ path))))
82
+
83
+ (defun fronde/org-i18n-follow (link)
84
+ "Visit a i18n link"
85
+ (browse-url (car (split-string link "|"))))
86
+
87
+ (org-link-set-parameters "i18n"
88
+ :export #'fronde/org-i18n-export
89
+ :follow #'fronde/org-i18n-follow)
90
+
91
+
92
+ ;;; Set configuration options
93
+
94
+ (setq fronde/org-temp-dir (expand-file-name "var/tmp" fronde/current-work-dir)
95
+ org-publish-timestamp-directory (expand-file-name "timestamps/" fronde/org-temp-dir)
96
+ org-id-locations-file (expand-file-name "id-locations.el" fronde/org-temp-dir)
97
+ make-backup-files nil
98
+ enable-local-variables :all
99
+ org-confirm-babel-evaluate nil
100
+ org-export-with-broken-links t
101
+ org-html-doctype "html5"
102
+ org-html-html5-fancy t
103
+ org-html-htmlize-output-type 'css
104
+ org-html-text-markup-alist '((bold . "<strong>%s</strong>")
105
+ (code . "<code>%s</code>")
106
+ (italic . "<em>%s</em>")
107
+ (strike-through . "<del>%s</del>")
108
+ (underline . "<span class=\"underline\">%s</span>")
109
+ (verbatim . "<code>%s</code>")))
110
+ (advice-add 'org-html-format-spec :around #'fronde/org-html-format-spec)
111
+
112
+ (provide 'ox-fronde)
113
+
114
+ ;;; ox-fronde.el ends here
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fronde/config'
4
+
5
+ module Fronde
6
+ # Wraps Gnu/Emacs calls
7
+ class Emacs
8
+ def initialize(file_path: nil, verbose: false)
9
+ @file = file_path
10
+ @verbose = verbose
11
+ end
12
+
13
+ def publish
14
+ command = emacs_command(
15
+ '-l ./var/lib/org-config.el', '--eval \'(org-publish "website")\''
16
+ )
17
+ if @verbose
18
+ warn command
19
+ return system(command, exception: true)
20
+ end
21
+ system command, out: '/dev/null', err: '/dev/null', exception: true
22
+ end
23
+
24
+ private
25
+
26
+ def emacs_command(*arguments)
27
+ default_emacs = Fronde::Config.settings['emacs']
28
+ emacs_cmd = [
29
+ default_emacs || 'emacs -Q --batch -nw',
30
+ '--eval \'(setq enable-dir-local-variables nil)\''
31
+ ]
32
+ emacs_cmd << '--eval \'(setq inhibit-message t)\'' unless @verbose
33
+ emacs_cmd.concat(arguments)
34
+ emacs_cmd.join(' ')
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'digest/md5'
5
+ require 'fronde/config'
6
+ require 'fronde/org_file'
7
+ require 'fronde/index/atom_generator'
8
+ require 'fronde/index/org_generator'
9
+
10
+ module Fronde
11
+ # Generates website indexes and atom feeds for all the org documents
12
+ # keywords.
13
+ class Index
14
+ attr_reader :date
15
+
16
+ include Fronde::IndexAtomGenerator
17
+ include Fronde::IndexOrgGenerator
18
+
19
+ def initialize
20
+ @pubdir = Fronde::Config.settings['public_folder']
21
+ @index = { 'index' => [] }
22
+ @projects = {}
23
+ @tags_names = {}
24
+ @date = DateTime.now
25
+ feed
26
+ sort!
27
+ end
28
+
29
+ def entries
30
+ @index.keys.reject { |k| k == 'index' }
31
+ end
32
+
33
+ def empty?
34
+ @index['index'].empty?
35
+ end
36
+
37
+ def write_all(verbose: true)
38
+ @index.each_key do |k|
39
+ write_org(k)
40
+ warn "Generated index file for #{k}" if verbose
41
+ write_atom(k)
42
+ warn "Generated atom feed for #{k}" if verbose
43
+ end
44
+ write_all_blog_home(verbose)
45
+ end
46
+
47
+ def sort_by(kind)
48
+ if [:name, :weight].include?(kind)
49
+ tags_sorted = sort_tags_by_name_and_weight["by_#{kind}".to_sym]
50
+ # Reverse in order to have most important or A near next prompt
51
+ # and avoid to scroll to find the beginning of the list.
52
+ return tags_sorted.map do |k|
53
+ @tags_names[k] + " (#{@index[k].length})"
54
+ end.reverse
55
+ end
56
+ raise ArgumentError, "#{kind} not in [:name, :weight]"
57
+ end
58
+
59
+ private
60
+
61
+ def feed
62
+ Fronde::Config.sources.each do |project|
63
+ next unless project['is_blog']
64
+ if project['recursive']
65
+ file_pattern = '**/*.org'
66
+ else
67
+ file_pattern = '*.org'
68
+ end
69
+ Dir.glob(file_pattern, base: project['path']).map do |s|
70
+ org_file = File.join(project['path'], s)
71
+ next if exclude_file?(org_file, project)
72
+ add_to_indexes(
73
+ Fronde::OrgFile.new(org_file, project: project)
74
+ )
75
+ end
76
+ end
77
+ end
78
+
79
+ def add_to_project_index(article)
80
+ project = article.project
81
+ @projects[project['name']] ||= []
82
+ @projects[project['name']] << article
83
+ end
84
+
85
+ def add_to_indexes(article)
86
+ @index['index'] << article
87
+ add_to_project_index article
88
+ article.keywords.each do |k|
89
+ slug = Fronde::OrgFile.slug k
90
+ @tags_names[slug] = k # Overwrite is permitted
91
+ @index[slug] ||= []
92
+ @index[slug] << article
93
+ end
94
+ end
95
+
96
+ def sort!
97
+ @index.each do |k, i|
98
+ @index[k] = i.sort { |a, b| b.timekey <=> a.timekey }
99
+ end
100
+ @projects.each do |k, i|
101
+ @projects[k] = i.sort { |a, b| b.timekey <=> a.timekey }
102
+ end
103
+ end
104
+
105
+ def sort_tags_by_name_and_weight
106
+ tags_sorted = {}
107
+ all_keys = entries
108
+ tags_sorted[:by_name] = all_keys.sort
109
+ tags_sorted[:by_weight] = all_keys.sort do |a, b|
110
+ @index[b].length <=> @index[a].length
111
+ end
112
+ tags_sorted
113
+ end
114
+
115
+ def exclude_file?(file_path, project)
116
+ # Obviously excluding index itself for blogs
117
+ return true if file_path == File.join(project['path'], 'index.org')
118
+ return false unless project['exclude']
119
+ file_path.match? project['exclude']
120
+ end
121
+
122
+ def save?
123
+ return true unless empty?
124
+ Fronde::Config.sources.each do |project|
125
+ return true if project['is_blog'] && Dir.exist?(project['path'])
126
+ end
127
+ false
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+ require 'fronde/config'
5
+
6
+ module Fronde
7
+ # Embeds Atom feeds sepecific methods
8
+ module IndexAtomGenerator
9
+ def to_atom(index_name = 'index')
10
+ content = [atom_header(index_name)]
11
+ @index[index_name][0...10].each do |article|
12
+ content << atom_entry(article)
13
+ end
14
+ format '%<content>s</feed>', content: content.join("\n")
15
+ end
16
+
17
+ def write_atom(index_name)
18
+ return unless save?
19
+ slug = Fronde::OrgFile.slug index_name
20
+ FileUtils.mkdir_p "#{@pubdir}/feeds"
21
+ atomdest = "#{@pubdir}/feeds/#{slug}.xml"
22
+ IO.write(atomdest, to_atom(index_name))
23
+ end
24
+
25
+ private
26
+
27
+ # Render the Atom feed header.
28
+ #
29
+ # @param title [String] the title of the current atom feed
30
+ # @return [String] the Atom header as a String
31
+ def atom_header(title)
32
+ domain = Fronde::Config.settings['domain']
33
+ upddate = @date.rfc3339
34
+ if title == 'index'
35
+ slug = 'index'
36
+ tagurl = domain
37
+ title = Fronde::Config.settings['title'] || \
38
+ R18n.t.fronde.index.all_tags
39
+ else
40
+ slug = Fronde::OrgFile.slug(title)
41
+ tagurl = "#{domain}/tags/#{slug}.html"
42
+ title = @tags_names[title]
43
+ end
44
+ title = CGI.escapeHTML(title)
45
+ <<~ENDATOM
46
+ <?xml version="1.0" encoding="utf-8"?>
47
+ <feed xmlns="http://www.w3.org/2005/Atom"
48
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
49
+ xmlns:wfw="http://wellformedweb.org/CommentAPI/"
50
+ xml:lang="#{Fronde::Config.settings['lang']}">
51
+
52
+ <title>#{title}</title>
53
+ <link href="#{domain}/feeds/#{slug}.xml" rel="self" type="application/atom+xml"/>
54
+ <link href="#{tagurl}" rel="alternate" type="text/html" title="#{title}"/>
55
+ <updated>#{upddate}</updated>
56
+ <author><name>#{Fronde::Config.settings['author'] || ''}</name></author>
57
+ <id>urn:md5:#{Digest::MD5.hexdigest(domain)}</id>
58
+ <generator uri="https://git.umaneti.net/fronde/about/">Fronde</generator>
59
+ ENDATOM
60
+ end
61
+
62
+ # Render an Atom feed entry.
63
+ #
64
+ # @param article [Fronde::OrgFile] the related org document for this
65
+ # entry
66
+ # @return [String] the Atom entry as a String
67
+ def atom_entry(article)
68
+ keywords = article.keywords.map do |k|
69
+ "<dc:subject>#{CGI.escapeHTML(k)}</dc:subject>"
70
+ end.join
71
+ keywords += "\n " if keywords != ''
72
+ title = CGI.escapeHTML(article.title)
73
+ <<~ENDENTRY
74
+ <entry>
75
+ <title>#{title}</title>
76
+ <link href="#{article.url}" rel="alternate" type="text/html"
77
+ title="#{title}"/>
78
+ <id>urn:md5:#{Digest::MD5.hexdigest(article.timekey)}</id>
79
+ <published>#{article.datestring(:rfc3339)}</published>
80
+ <author><name>#{CGI.escapeHTML(article.author)}</name></author>
81
+ #{keywords}<content type="html">#{CGI.escapeHTML(article.excerpt)}</content>
82
+ </entry>
83
+ ENDENTRY
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fronde
4
+ # Embeds methods responsible for generating an org file for a given
5
+ # index.
6
+ module IndexOrgGenerator
7
+ def to_org(index_name = 'index', is_project: false)
8
+ return project_home_page(index_name) if is_project
9
+ return all_tags_index if index_name == 'index'
10
+ [org_header(index_name),
11
+ org_articles(@index[index_name])].join("\n")
12
+ end
13
+ alias_method :to_s, :to_org
14
+
15
+ def write_org(index_name)
16
+ return unless save?
17
+ slug = Fronde::OrgFile.slug index_name
18
+ FileUtils.mkdir 'tags' unless Dir.exist? 'tags'
19
+ content = to_org index_name
20
+ orgdest = "tags/#{slug}.org"
21
+ IO.write(orgdest, content)
22
+ end
23
+
24
+ private
25
+
26
+ def project_home_page(project_name)
27
+ content = [org_header(project_name, is_tag: false)]
28
+ if @projects[project_name]&.any?
29
+ content += org_articles(@projects[project_name])
30
+ end
31
+ content.join("\n")
32
+ end
33
+
34
+ def write_all_blog_home(verbose)
35
+ Fronde::Config.sources.each do |project|
36
+ next unless project['is_blog']
37
+ next unless Dir.exist?(project['path'])
38
+ warn "Generated blog home for #{project['name']}" if verbose
39
+ orgdest = format('%<root>s/index.org', root: project['path'])
40
+ IO.write(orgdest, to_org(project['name'], is_project: true))
41
+ end
42
+ end
43
+
44
+ def all_tags_index
45
+ content = [
46
+ org_header(R18n.t.fronde.index.all_tags, is_tag: false)
47
+ ]
48
+ sort_tags_by_name_and_weight.each do |t, tags|
49
+ content << ''
50
+ content << org_title(R18n.t.fronde.index.send(t), 'index-tags')
51
+ next if tags.empty?
52
+ tags.each do |k|
53
+ content << "- #{tag_published_url(k)} (#{@index[k].length})"
54
+ end
55
+ end
56
+ content.join("\n")
57
+ end
58
+
59
+ def tag_published_url(tag_name)
60
+ domain = Fronde::Config.settings['domain']
61
+ title = @tags_names[tag_name]
62
+ tag_link = "#{domain}/tags/#{tag_name}.html"
63
+ "[[#{tag_link}][#{title}]]"
64
+ end
65
+
66
+ def org_header(title = nil, is_tag: true)
67
+ if is_tag
68
+ title = @tags_names[title]
69
+ elsif title.nil? || title == 'index'
70
+ title = Fronde::Config.settings['title']
71
+ end
72
+ <<~HEADER.strip
73
+ #+title: #{title}
74
+ #+author: #{Fronde::Config.settings['author']}
75
+ #+language: #{Fronde::Config.settings['lang']}
76
+ HEADER
77
+ end
78
+
79
+ def org_articles(articles_list)
80
+ last_year = nil
81
+ articles_list.map do |article|
82
+ year_title = ''
83
+ year = article.timekey.slice(0, 4)
84
+ if year != last_year
85
+ year_title = format("\n%<title>s\n", title: org_title(year))
86
+ last_year = year
87
+ end
88
+ year_title + org_entry(article)
89
+ end
90
+ end
91
+
92
+ def org_entry(article)
93
+ line = "- *[[#{article.url}][#{article.title}]]*"
94
+ if article.date
95
+ art_date = article.datestring(:full, year: false)
96
+ published = R18n.t.fronde.index.published_on art_date
97
+ line += " / #{published}"
98
+ end
99
+ line += " \\\\\n #{article.excerpt}" if article.excerpt != ''
100
+ line
101
+ end
102
+
103
+ def org_title(year, html_class = 'index-year')
104
+ year = R18n.t.fronde.index.unsorted if year == '0000'
105
+ <<~ENDPROP
106
+ * #{year}
107
+ :PROPERTIES:
108
+ :HTML_CONTAINER_CLASS: #{html_class}
109
+ :UNNUMBERED: notoc
110
+ :END:
111
+ ENDPROP
112
+ end
113
+ end
114
+ end