fronde 0.3.0

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