aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorGravatar Étienne Deparis 2023-11-23 08:54:12 +0100
committerGravatar Étienne Deparis 2023-11-23 08:54:12 +0100
commitc0e61545cd6831c873d494a6151ce570ccb91597 (patch)
treee4a5ca8dfdb7b6eecc2ea3ac2d8f52b663ae43e6
parent8c7df0c80a240080479bd0324ee84c70580712fb (diff)
parent64797ecaa905e9f707e8c6b2f8739a333a6db7bf (diff)
downloadfronde-main.tar.gz
fronde-main.tar.xz
fronde-main.zip
Merge branch 'v0.4-dev'HEADv0.4main
-rw-r--r--.github/FUNDING.yml3
-rw-r--r--.gitignore7
-rw-r--r--.gitlab-ci.yml18
-rw-r--r--.pryrc7
-rw-r--r--.reek.yml15
-rw-r--r--.rspec4
-rw-r--r--.rubocop.yml30
-rw-r--r--CODE_OF_CONDUCT.org129
-rw-r--r--DOCUMENTATION.org188
-rw-r--r--DOCUMENTATION.pdfbin0 -> 73633 bytes
-rw-r--r--Gemfile10
-rw-r--r--Gemfile.lock108
-rw-r--r--README.org105
-rwxr-xr-xbin/fronde45
-rw-r--r--fronde.gemspec97
-rw-r--r--lib/ext/nil_time.rb25
-rw-r--r--lib/ext/r18n.rb17
-rw-r--r--lib/ext/time.rb49
-rw-r--r--lib/fronde/cli.rb83
-rw-r--r--lib/fronde/cli/commands.rb195
-rw-r--r--lib/fronde/cli/data/Rakefile8
-rw-r--r--lib/fronde/cli/data/config.yml13
-rw-r--r--lib/fronde/cli/data/gitignore7
-rw-r--r--lib/fronde/cli/data/zsh_completion37
-rw-r--r--lib/fronde/cli/helpers.rb55
-rw-r--r--lib/fronde/cli/opt_parse.rb143
-rw-r--r--lib/fronde/cli/throbber.rb99
-rw-r--r--lib/fronde/config.rb234
-rw-r--r--lib/fronde/config/data/org-config.el24
-rw-r--r--lib/fronde/config/data/ox-fronde.el (renamed from lib/fronde/config/ox-fronde.el)2
-rw-r--r--lib/fronde/config/helpers.rb80
-rw-r--r--lib/fronde/config/lisp.rb70
-rw-r--r--lib/fronde/config/lisp_config.rb340
-rw-r--r--lib/fronde/config/org-config.el19
-rw-r--r--lib/fronde/emacs.rb43
-rw-r--r--lib/fronde/index.rb138
-rw-r--r--lib/fronde/index/atom_generator.rb121
-rw-r--r--lib/fronde/index/data/all_tags.org14
-rw-r--r--lib/fronde/index/data/template.org22
-rw-r--r--lib/fronde/index/data/template.xml37
-rw-r--r--lib/fronde/index/org_generator.rb158
-rw-r--r--lib/fronde/org.rb103
-rw-r--r--lib/fronde/org/file.rb287
-rw-r--r--lib/fronde/org/file_extracter.rb98
-rw-r--r--lib/fronde/org_file.rb298
-rw-r--r--lib/fronde/org_file/class_methods.rb72
-rw-r--r--lib/fronde/org_file/extracter.rb72
-rw-r--r--lib/fronde/org_file/htmlizer.rb43
-rw-r--r--lib/fronde/preview.rb82
-rw-r--r--lib/fronde/slug.rb27
-rw-r--r--lib/fronde/source.rb204
-rw-r--r--lib/fronde/source/gemini.rb39
-rw-r--r--lib/fronde/source/html.rb67
-rw-r--r--lib/fronde/templater.rb165
-rw-r--r--lib/fronde/utils.rb229
-rw-r--r--lib/fronde/version.rb2
-rw-r--r--lib/tasks/cli.rake33
-rw-r--r--lib/tasks/org.rake104
-rw-r--r--lib/tasks/site.rake98
-rw-r--r--lib/tasks/sync.rake62
-rw-r--r--lib/tasks/tags.rake18
-rw-r--r--locales/en.yml74
-rw-r--r--locales/fr.yml82
-rw-r--r--spec/data/config_3_gemini_org_config.el57
-rw-r--r--spec/data/config_3_org_config.el61
-rw-r--r--spec/data/config_4_gemini_org_config.el91
-rw-r--r--spec/data/config_4_org_config.el95
-rw-r--r--spec/data/config_5_org_config.el69
-rw-r--r--spec/data/content.org3
-rw-r--r--spec/data/fronde.zsh48
-rw-r--r--spec/data/index_proof.html3
-rw-r--r--spec/data/org-config-blog-proof.el112
-rw-r--r--spec/data/org-config-proof.el94
-rw-r--r--spec/data/test4.org1
-rw-r--r--spec/fronde/cli/opt_parse_spec.rb108
-rw-r--r--spec/fronde/cli/throbber_spec.rb46
-rw-r--r--spec/fronde/config_spec.rb401
-rw-r--r--spec/fronde/index_spec.rb830
-rw-r--r--spec/fronde/org/file_spec.rb264
-rw-r--r--spec/fronde/org_file_spec.rb266
-rw-r--r--spec/fronde/slug_spec.rb15
-rw-r--r--spec/fronde/source_spec.rb97
-rw-r--r--spec/fronde/templater_spec.rb690
-rw-r--r--spec/fronde/utils_spec.rb107
-rw-r--r--spec/rake_cli_spec.rb17
-rw-r--r--spec/rake_org_spec.rb234
-rw-r--r--spec/rake_preview_spec.rb109
-rw-r--r--spec/rake_site_spec.rb317
-rw-r--r--spec/requirements.rb103
89 files changed, 5382 insertions, 3914 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..df5bb2b
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# github: [milouse]
+liberapay: milouse
+custom: ['https://paypal.me/milouse']
diff --git a/.gitignore b/.gitignore
index 8a84424..b1dc013 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
-.byebug_history
.ruby-*
+.yardoc
fronde-*.gem
doc
coverage
-.yardoc
+_minted-DOCUMENTATION
+DOCUMENTATION.*
+!DOCUMENTATION.org
+!DOCUMENTATION.pdf
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..38845aa
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,18 @@
+image: ruby:3.2.2-alpine
+
+default:
+ before_script:
+ - apk add -U build-base
+ - bundle install -j $(nproc)
+
+rubocop:
+ stage: test
+ script:
+ - rubocop --fail-level W
+
+rspec:
+ stage: test
+ script:
+ - apk add -U emacs-nox ncurses
+ - rspec
+ coverage: '/\(\d+\.\d+\%\) covered./'
diff --git a/.pryrc b/.pryrc
deleted file mode 100644
index 396b47d..0000000
--- a/.pryrc
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-require 'byebug'
-
-$LOAD_PATH.unshift('./lib')
-# The following requires other components automatically
-require 'fronde/org_file'
diff --git a/.reek.yml b/.reek.yml
new file mode 100644
index 0000000..f6abf1e
--- /dev/null
+++ b/.reek.yml
@@ -0,0 +1,15 @@
+---
+detectors:
+ DuplicateMethodCall:
+ allow_calls:
+ - R18n.t
+ NestedIterators:
+ max_allowed_nesting: 2
+# TooManyInstanceVariables:
+# max_instance_variables: 8
+ TooManyStatements:
+ max_statements: 10
+ UncommunicativeVariableName:
+ accept:
+ - _
+ - e
diff --git a/.rspec b/.rspec
index 73d7afd..04b2ced 100644
--- a/.rspec
+++ b/.rspec
@@ -1,2 +1,2 @@
--I ./lib
--r ./spec/requirements.rb
+--require ./spec/requirements.rb
+--order random
diff --git a/.rubocop.yml b/.rubocop.yml
index 6190f71..cabba1f 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -7,20 +7,11 @@ require:
- rubocop-performance
- rubocop-rspec
-Layout:
- Severity: warning
-
-Layout/EmptyLineAfterGuardClause:
- Enabled: no
-
Layout/LineLength:
Max: 80
Exclude:
- spec/**/*
-Metrics:
- Severity: warning
-
Metrics/AbcSize:
Enabled: no
@@ -32,13 +23,10 @@ Metrics/BlockLength:
- fronde.gemspec
Metrics/MethodLength:
- Max: 15
+ Max: 10
Metrics/ModuleLength:
Max: 120
- Exclude:
- - lib/fronde/config/lisp_config.rb
- - lib/fronde/utils.rb
Metrics/PerceivedComplexity:
Max: 10
@@ -46,14 +34,17 @@ Metrics/PerceivedComplexity:
Metrics/CyclomaticComplexity:
Max: 10
+RSpec:
+ Severity: warning
+
RSpec/ExampleLength:
Enabled: no
RSpec/MultipleExpectations:
Enabled: no
-RSpec/NestedGroups:
- Max: 4
+Style:
+ Severity: warning
Style/Alias:
EnforcedStyle: prefer_alias_method
@@ -61,18 +52,9 @@ Style/Alias:
Style/ConditionalAssignment:
EnforcedStyle: assign_inside_condition
-Style/HashSyntax:
- EnforcedStyle: no_mixed_keys
-
Style/PreferredHashMethods:
EnforcedStyle: verbose
Style/RegexpLiteral:
EnforcedStyle: slashes
AllowInnerSlashes: yes
-
-Style/SymbolArray:
- EnforcedStyle: brackets
-
-Style/WordArray:
- EnforcedStyle: brackets
diff --git a/CODE_OF_CONDUCT.org b/CODE_OF_CONDUCT.org
new file mode 100644
index 0000000..ad27925
--- /dev/null
+++ b/CODE_OF_CONDUCT.org
@@ -0,0 +1,129 @@
+#+title: Contributor Covenant Code of Conduct
+#+language: en
+#+options: ^:{} toc:3 num:3 H:4
+
+* Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in
+our community a harassment-free experience for everyone, regardless of
+age, body size, visible or invisible disability, ethnicity, sex
+characteristics, gender identity and expression, level of experience,
+education, socio-economic status, nationality, personal appearance,
+race, caste, color, religion, or sexual identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open,
+welcoming, diverse, inclusive, and healthy community.
+
+* Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our
+ mistakes, and learning from the experience
+- Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+- The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+- Trolling, insulting or derogatory comments, and personal or political
+ attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+* Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our
+standards of acceptable behavior and will take appropriate and fair
+corrective action in response to any behavior that they deem
+inappropriate, threatening, offensive, or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other
+contributions that are not aligned to this Code of Conduct, and will
+communicate reasons for moderation decisions when appropriate.
+
+* Scope
+
+This Code of Conduct applies within all community spaces, and also
+applies when an individual is officially representing the community in
+public spaces. Examples of representing our community include using an
+official e-mail address, posting via an official social media account,
+or acting as an appointed representative at an online or offline event.
+
+* Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may
+be reported to the community leaders responsible for enforcement at
+[[mailto:fronde_coc@depar.is?subject=Report%20of%20unacceptable%20behavior][fronde_coc@depar.is]].
+
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security
+of the reporter of any incident.
+
+* Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in
+determining the consequences for any action they deem in violation of
+this Code of Conduct:
+
+** Correction
+
+- Community Impact :: Use of inappropriate language or other behavior
+ deemed unprofessional or unwelcome in the community.
+- Consequence :: A private, written warning from community leaders,
+ providing clarity around the nature of the violation and an
+ explanation of why the behavior was inappropriate. A public apology
+ may be requested.
+
+** Warning
+
+- Community Impact :: A violation through a single incident or series of
+ actions.
+- Consequence :: A warning with consequences for continued behavior. No
+ interaction with the people involved, including unsolicited
+ interaction with those enforcing the Code of Conduct, for a specified
+ period of time. This includes avoiding interactions in community
+ spaces as well as external channels like social media. Violating these
+ terms may lead to a temporary or permanent ban.
+
+** Temporary Ban
+
+- Community Impact :: A serious violation of community standards,
+ including sustained inappropriate behavior.
+- Consequence :: A temporary ban from any sort of interaction or public
+ communication with the community for a specified period of time. No
+ public or private interaction with the people involved, including
+ unsolicited interaction with those enforcing the Code of Conduct, is
+ allowed during this period. Violating these terms may lead to a
+ permanent ban.
+
+** Permanent Ban
+
+- Community Impact :: Demonstrating a pattern of violation of community
+ standards, including sustained inappropriate behavior, harassment of
+ an individual, or aggression toward or disparagement of classes of
+ individuals.
+- Consequence :: A permanent ban from any sort of public interaction
+ within the community.
+
+* Attribution
+
+This Code of Conduct is adapted from the [[https://www.contributor-covenant.org][Contributor Covenant]],
+version 2.1, available at
+[[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html]].
+
+Community Impact Guidelines were inspired by
+[[https://github.com/mozilla/diversity][Mozilla's code of conduct enforcement ladder]].
+
+For answers to common questions about this code of conduct, see the
+[[https://www.contributor-covenant.org/faq][FAQ]]. [[https://www.contributor-covenant.org/translations][Translations are available too]].
diff --git a/DOCUMENTATION.org b/DOCUMENTATION.org
index 376748b..1adf628 100644
--- a/DOCUMENTATION.org
+++ b/DOCUMENTATION.org
@@ -1,8 +1,13 @@
#+title: Fronde Documentation
#+author: Étienne Deparis
-#+lang: en
+#+language: en
#+description: Glossary of all possible configuration options for Fronde
#+options: ^:{} toc:3 num:3 H:4
+#+latex_header: \usepackage{minted}
+#+latex_header: \usepackage{awesomebox}
+#+latex_header: \usepackage{libertinus-otf}
+#+latex_header: \defaultfontfeatures{Ligatures=TeX, Scale=MatchLowercase}
+#+latex_header: \setmonofont{InconsolataLGC}[Ligatures=TeXReset, Scale=0.75]
* Introduction
@@ -67,8 +72,8 @@ generator should be, and more specifically, what should it be for Org.
as convenient as Markdown's. That's why fronde will never support any
other markup language.
- However Org HTML export should be more easily expandable. That’s why
- fronde gives you the [[#templates_setting][templates feature]] and introduces [[#formatting_tags][new formatting
- tags]] for the preamble and postamble sections.
+ fronde gives you the [[#templates_setting][templates feature]] and introduces [[#formatting_tags][new formatting tags]]
+ for the preamble and postamble sections.
- Org writers should already be Emacs users, that’s why fronde will
directly call Emacs to convert your Org files to HTML. There is no
need to use Pandoc or org-ruby if you already have Emacs.
@@ -82,11 +87,11 @@ generator should be, and more specifically, what should it be for Org.
We are already aware of some limitations we would like to remove:
-**** TODO Allow theme to provide configuration options
+**** TODO Allow themes to provide configuration options
Themes should be able to declare a set of default ~templates~ or
specific Org export options
-**** TODO Better protect upgrade
+**** TODO Better protected upgrade
For example, Rakefile should only be overwritten after upgrade of the
gem.
@@ -94,14 +99,16 @@ gem.
** Source code
-You can access the source code at: https://git.umaneti.net/fronde
+The canonical repository is located at [[https://git.umaneti.net/fronde]].
+A mirror also exists on [[https://github.com/milouse/chwall][Github]].
* Getting started
** Installation
Fronde is a [[https://rubygems.org/][Ruby gem]] and thus you need a working Ruby environment to use
-it. Fronde requires at least ruby 2.6 to work.
+it. Fronde requires at least ruby 2.7 to work (though this version is no
+more maintained, you should really move to ruby 3.1 as soon as possible).
You may already have Ruby installed on your computer. You can check
inside a terminal emulator by typing: ~ruby -v~. You also need the
@@ -118,7 +125,7 @@ Some operating systems already package the right ruby version to use:
**** Archlinux-like
On Archlinux, Manjaro or Parabola, you just have to install the packages
-~ruby~ and ~rubygems~:
+~rubygems~ (which will install ~ruby~ as a dependency):
#+caption: Ruby installation procedure on Archlinux-like system
#+begin_src shell
@@ -127,18 +134,18 @@ On Archlinux, Manjaro or Parabola, you just have to install the packages
**** Debian-like
-Ruby 2.6 does not seem to be available at all. However, on Ubuntu at
-least, ruby 2.7 is available. Thus you should try the following command:
+On Debian, Ubuntu, Mint… you just have to install the package ~ruby~
+(which will install ~ruby-rubygems~ as a dependency):
#+caption: Ruby installation procedure on Debian-like system
#+begin_src shell
- sudo apt install ruby2.7
+ sudo apt install ruby
#+end_src
**** Others
-Ruby 2.7 seems to be already well packaged for Fedora, Red Hat or
-OpenMandriva, thus it should not be a problem for you to install it.
+Ruby seems to be well packaged for Fedora, Red Hat or OpenMandriva, thus
+it should not be a problem for you to install it.
*** Install ruby with RVM
@@ -151,7 +158,8 @@ polluting your other projects.
#+caption: Ruby installation procedure with RVM
#+begin_src shell
rvm get latest
- rvm use ruby-2.7.2@fronde --create
+ rvm install ruby-3.2.2
+ rvm use ruby-3.2.2@fronde --create
#+end_src
*** Install Fronde
@@ -187,7 +195,7 @@ number you just built):
#+caption: Fronde installation procedure from local gem file
#+begin_src shell
- gem install fronde-0.3.0.gem
+ gem install fronde-0.4.0.gem
#+end_src
** Creation of a website
@@ -215,13 +223,13 @@ Now, you can run the ~fronde init~ command with some arguments:
fronde init -t "My brand new website"
#+end_src
-#+begin_note
+#+begin_noteblock
The following arguments are allowed:
- -a (--author) :: Set up the default author name (see the [[#author_setting][~author~ config key]])
- -l (--lang) :: Set up the default lang of your website (see the [[#lang_setting][~lang~ config key]])
-- -t (--title) :: Set up the title of your website (see the [[#title_setting][~title~ config key]])
-#+end_note
+- -t (--title) :: Set up the title of your first page
+#+end_noteblock
At the end of the init process, fronde will automatically open Emacs on
the first page of your website. You can directly write in it or close it
@@ -243,10 +251,10 @@ yourproject/
└── var/ # variable files used during operation
#+end_example
-#+begin_caution
+#+begin_cautionblock
Fronde relies a lot on files stored in the ~lib~ and ~var~ folders. You
should never try to remove them by yourself.
-#+end_caution
+#+end_cautionblock
** Creation or edition of a page
@@ -311,7 +319,7 @@ HTML with the ~fronde build~ command.
fronde build
#+end_src
-#+begin_tip
+#+begin_tipblock
Because this building process makes a direct use of the org-mode
publishing feature, it will rebuild only files changed since the last
command invocation. If you want to force a full rebuild, you can pass
@@ -320,7 +328,7 @@ the ~--force~ arguments to the command.
#+begin_src shell
fronde build -f
#+end_src
-#+end_tip
+#+end_tipblock
To review what you just built, you can use the ~fronde preview~ command,
which will start a local webserver and open your default web browser on
@@ -366,10 +374,10 @@ atom feed.
author: Alice Doe
#+end_src
-#+begin_note
+#+begin_noteblock
The default ~author~ setting is your user name on your current computer
session.
-#+end_note
+#+end_noteblock
*** ~domain~
@@ -377,11 +385,11 @@ The domain name pointing to where your static website is expected to be
hosted. This value is used to generate absolute path to your files on
your expected host name (for example in the Atom feeds).
-#+begin_caution
+#+begin_cautionblock
Its value is expected to *not* end with a slash. That is to say
~https://example.com~ is *valid*, whereas ~https://example.com/~ is
*not*.
-#+end_caution
+#+end_cautionblock
By default, your static website is expected to be hosted at the root of
your domain name (like ~https://example.com/index.html~). If it is not
@@ -396,65 +404,69 @@ are *valid* values too.
domain: https://alice.doe.name
#+end_src
-#+begin_note
+#+begin_noteblock
The default ~domain~ value is an empty string.
-#+end_note
+#+end_noteblock
-*** ~lang~
-:PROPERTIES:
-:CUSTOM_ID: lang_setting
-:END:
+*** ~gemini_public_folder~
-The main locale your website will be written in. Its value must comply
-with the [[https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes][ISO 639-1]] standard.
+The ~gemini_public_folder~ option stores the path to the folder, which
+will contains the generated gemtext files, ready for Gemini publication.
-#+caption: Example of a lang setting
-#+begin_src yaml
- ---
- lang: en
-#+end_src
+#+begin_noteblock
+The default ~gemini_public_folder~ value is ~public_gmi~ (at the root of
+your project).
+#+end_noteblock
-#+begin_note
-The default ~lang~ setting is that of your current computer.
-#+end_note
+*** ~gemini_remote~
+
+The ~gemini_remote~ option contains an /rsync/ like destination string,
+which will be used to published your capsule when using the ~sync:push~
+/rake/ task. For example ~user@domain:/var/www/mycapsule/~
-*** ~public_folder~
+#+begin_noteblock
+There is no default value for ~gemini_remote~. Publication of your
+capsule will be skipped if there is no value for ~gemini_remote~.
+#+end_noteblock
-The ~public_folder~ option stores the path to the folder, which will
-contains the generated files, ready for publication.
+*** ~html_public_folder~
-#+begin_note
-The default ~public_folder~ value is ~public_html~ (at the root of your
-project).
-#+end_note
+The ~html_public_folder~ option stores the path to the folder, which
+will contains the HTML generated files, ready for Web publication.
-*** ~remote~
+#+begin_noteblock
+The default ~html_public_folder~ value is ~public_html~ (at the root of
+your project).
+#+end_noteblock
-The ~remote~ option contains an /rsync/ like destination string, which
-will be used to published your website when using the ~sync:push~ /rake/
-task. For example ~user@domain:/var/www/mywebsite/~
+*** ~html_remote~
-#+begin_note
-There is no default value for ~remote~. Publication of your website will
-be skipped if there is no value for ~remote~.
-#+end_note
+The ~html_remote~ option contains an /rsync/ like destination string,
+which will be used to published your website when using the ~sync:push~
+/rake/ task. For example ~user@domain:/var/www/mywebsite/~
-*** ~title~
+#+begin_noteblock
+There is no default value for ~html_remote~. Publication of your website
+will be skipped if there is no value for ~html_remote~.
+#+end_noteblock
+
+*** ~lang~
:PROPERTIES:
-:CUSTOM_ID: title_setting
+:CUSTOM_ID: lang_setting
:END:
-The ~title~ option stores the default title of your org files. This
-value is used when you create a new file with the ~fronde~ command.
+The main locale your website will be written in. Its value must comply
+with the [[https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes][ISO 639-1]] standard.
-#+begin_warning
-*This setting is not used by the regular org mode publishing process*.
-You must ensure that your org files contain a ~#+title:~ metadata field
-before publishing them.
-#+end_warning
+#+caption: Example of a lang setting
+#+begin_src yaml
+ ---
+ lang: en
+#+end_src
-This value is also used in the generation of the blog index page and the
-main atom feed as the website title.
+#+begin_noteblock
+The default ~lang~ setting is that of your current computer.
+#+end_noteblock
*** ~sources~
:PROPERTIES:
@@ -473,24 +485,28 @@ following keys:
- name :: [string, optional] key used to generate the [[https://orgmode.org/org.html#Project-alist][Org “project”
name]]. This name never appears in a generated file, it is more like an
internal id. Defaults to the last dirname of the path value.
+- title :: TODO
- recursive :: [boolean, optional] whether the path should be exported
recursively. Defaults to True
- exclude :: [string, optional] Regexp of files to not export for this
source. Default to nothing (no files to exclude).
- target :: [string, optional] Path where to put the exported files,
- relative to the ~public_folder~ folder. Defaults to the source name at
- the root of the ~public_folder~ folder.
+ relative to the ~html_public_folder~ or ~gemini_public_folder~
+ folder. Defaults to the source name at the root of the /public/
+ folder.
- is_blog :: [boolean, optional] Whether this source should be
considered as a blog and thus, serves to generate blog index, tags
indexes and atom feeds.
- theme :: [string, optional] Theme name to use for this source.
Defaults to “default”.
+- type :: [string, optional] Type of the exporter to use. Fronde
+ currently supports only “html” and “gemini”. Defaults to “html”.
-#+begin_tip
+#+begin_tipblock
If one of your sources is expected to use all default settings, instead
of an object, you can just provide the source path as a string. Look at
the ~src~ source in the example bellow.
-#+end_tip
+#+end_tipblock
#+caption: Example of a sources array setting
#+begin_src yaml
@@ -515,7 +531,7 @@ if the ~sources~ option has been:
recursive: yes
#+end_src
-#+begin_warning
+#+begin_warningblock
Not setting the ~sources~ option is different from giving it the
following exact value:
@@ -527,15 +543,15 @@ following exact value:
In the first case, as nothing as been given for the ~sources~ option,
its default value will use ~.~ as its target directory, which means to
-put generated HTML files directly at the root of the ~public_folder~
+put generated HTML files directly at the root of the ~html_public_folder~
(which defaults to ~public_html~). But in the later case, the generated
-target will be the ~src~ folder /inside/ the ~public_folder~, which will
-be, by default, ~public_html/src~.
+target will be the ~src~ folder /inside/ the ~html_public_folder~, which
+will be, by default, ~public_html/src~.
Thus, if you have a very simple website with only one source, you should
avoid setting the ~sources~ options, or be very precise in what you
specify.
-#+end_warning
+#+end_warningblock
*** ~templates~
:PROPERTIES:
@@ -573,14 +589,14 @@ object, which must use the following keys:
a CSS selector, any part of the HTML document, in order to move it
elsewhere. See example below.
-#+begin_warning
+#+begin_warningblock
The ~content~ and the ~source~ options are incompatible: either you have
a ~content~ or a ~source~, never both of them. This is because either
you want to /add/ a new content to your document (using ~content~) or
you want to /move/ an existing content of your document (using
~source~). In both case, the destination is taken from the ~selector~
option.
-#+end_warning
+#+end_warningblock
For example, if you want to hide social media ~meta~ tags in the /head/
of your generated HTML files, you can use the following ~templates~
@@ -628,7 +644,7 @@ option, this replacement occurs only for HTML documents under the
path: "/docs/*.html"
#+end_src
-** Preamble/Postamble/Templates formatting tags
+** Preamble, Postamble, and Templates formatting tags
:PROPERTIES:
:CUSTOM_ID: formatting_tags
:END:
@@ -647,7 +663,7 @@ evaluated Org file, or by default from the config of your project.
~<time datetime="%I">%i</time>~.
- %D :: the full date and time HTML representation.
- %i :: the raw short date and time.
-- %I :: the raw rfc3339 date and time.
+- %I :: the raw ISO 8601 date and time.
- %k :: the current Org file keywords separated by commas.
- %K :: the HTML list rendering of the keywords.
- %l :: the lang of the document, or the one given in the config file.
@@ -667,3 +683,13 @@ evaluated Org file, or by default from the config of your project.
Like a lot of ruby project, fronde exposes some [[https://ruby.github.io/rake/][Rake]] tasks. You can
discover them with the ~rake -T~ command.
+
+# Compile this file with:
+# latexmk -norc -xelatex -halt-on-error -shell-escape DOCUMENTATION
+# Clean folder with:
+# rm -rf _minted-DOCUMENTATION
+# latexmk -e "push @generated_exts, 'nav', 'snm', 'vrb';" -c DOCUMENTATION
+
+# Local Variables:
+# org-latex-src-block-backend: minted
+# End:
diff --git a/DOCUMENTATION.pdf b/DOCUMENTATION.pdf
new file mode 100644
index 0000000..61098bc
--- /dev/null
+++ b/DOCUMENTATION.pdf
Binary files differ
diff --git a/Gemfile b/Gemfile
index 5f10ba8..de66957 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,3 +2,13 @@
source 'https://rubygems.org'
gemspec
+
+group :development do
+ gem 'org-ruby', '~> 0.9'
+ gem 'rspec', '~> 3.12'
+ gem 'rubocop', '~> 1.56'
+ gem 'rubocop-performance', '~> 1.19'
+ gem 'rubocop-rspec', '~> 2.23'
+ gem 'simplecov', '~> 0.22'
+ gem 'yard', '~> 0.9'
+end
diff --git a/Gemfile.lock b/Gemfile.lock
index b14428c..4442e00 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,102 +1,100 @@
PATH
remote: .
specs:
- fronde (0.3.3)
- nokogiri (~> 1.13)
+ fronde (0.4.0)
+ liquid (~> 5.4)
+ nokogiri (~> 1.15)
r18n-core (~> 5.0)
rainbow (~> 3.1)
rake (~> 13.0)
- webrick (~> 1.7)
+ webrick (~> 1.8)
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
- byebug (11.1.3)
- coderay (1.1.3)
+ base64 (0.1.1)
diff-lcs (1.5.0)
docile (1.4.0)
- json (2.6.2)
- method_source (1.0.0)
- mini_portile2 (2.8.0)
- nokogiri (1.13.8)
- mini_portile2 (~> 2.8.0)
+ json (2.6.3)
+ language_server-protocol (3.17.0.3)
+ liquid (5.4.0)
+ nokogiri (1.15.2-x86_64-linux)
racc (~> 1.4)
org-ruby (0.9.12)
rubypants (~> 0.2)
- parallel (1.22.1)
- parser (3.1.2.1)
+ parallel (1.23.0)
+ parser (3.2.2.3)
ast (~> 2.4.1)
- pry (0.14.1)
- coderay (~> 1.1)
- method_source (~> 1.0)
- pry-doc (1.3.0)
- pry (~> 0.11)
- yard (~> 0.9.11)
+ racc
r18n-core (5.0.1)
- racc (1.6.0)
+ racc (1.7.1)
rainbow (3.1.1)
rake (13.0.6)
- regexp_parser (2.5.0)
- rexml (3.2.5)
- rspec (3.11.0)
- rspec-core (~> 3.11.0)
- rspec-expectations (~> 3.11.0)
- rspec-mocks (~> 3.11.0)
- rspec-core (3.11.0)
- rspec-support (~> 3.11.0)
- rspec-expectations (3.11.1)
+ regexp_parser (2.8.1)
+ rexml (3.2.6)
+ rspec (3.12.0)
+ rspec-core (~> 3.12.0)
+ rspec-expectations (~> 3.12.0)
+ rspec-mocks (~> 3.12.0)
+ rspec-core (3.12.2)
+ rspec-support (~> 3.12.0)
+ rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.11.0)
- rspec-mocks (3.11.1)
+ rspec-support (~> 3.12.0)
+ rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.11.0)
- rspec-support (3.11.1)
- rubocop (1.36.0)
+ rspec-support (~> 3.12.0)
+ rspec-support (3.12.1)
+ rubocop (1.56.2)
+ base64 (~> 0.1.1)
json (~> 2.3)
+ language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
- parser (>= 3.1.2.1)
+ parser (>= 3.2.2.3)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
- rubocop-ast (>= 1.20.1, < 2.0)
+ rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7)
- unicode-display_width (>= 1.4.0, < 3.0)
- rubocop-ast (1.21.0)
- parser (>= 3.1.1.0)
- rubocop-performance (1.15.0)
+ unicode-display_width (>= 2.4.0, < 3.0)
+ rubocop-ast (1.29.0)
+ parser (>= 3.2.1.0)
+ rubocop-capybara (2.18.0)
+ rubocop (~> 1.41)
+ rubocop-factory_bot (2.23.1)
+ rubocop (~> 1.33)
+ rubocop-performance (1.19.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
- rubocop-rspec (2.13.1)
+ rubocop-rspec (2.23.2)
rubocop (~> 1.33)
- ruby-progressbar (1.11.0)
+ rubocop-capybara (~> 2.17)
+ rubocop-factory_bot (~> 2.22)
+ ruby-progressbar (1.13.0)
rubypants (0.7.1)
- simplecov (0.21.2)
+ simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
- unicode-display_width (2.3.0)
- webrick (1.7.0)
- yard (0.9.28)
- webrick (~> 1.7.0)
+ unicode-display_width (2.4.2)
+ webrick (1.8.1)
+ yard (0.9.34)
PLATFORMS
ruby
DEPENDENCIES
- byebug (~> 11.1)
fronde!
org-ruby (~> 0.9)
- pry (~> 0.14)
- pry-doc (~> 1.3)
- rspec (~> 3.11)
- rubocop (~> 1.36)
- rubocop-performance (~> 1.15)
- rubocop-rspec (~> 2.13)
- simplecov (~> 0.21)
+ rspec (~> 3.12)
+ rubocop (~> 1.56)
+ rubocop-performance (~> 1.19)
+ rubocop-rspec (~> 2.23)
+ simplecov (~> 0.22)
yard (~> 0.9)
BUNDLED WITH
- 2.3.19
+ 2.4.13
diff --git a/README.org b/README.org
index ac276d7..835fd41 100644
--- a/README.org
+++ b/README.org
@@ -1,6 +1,6 @@
#+title: Fronde
#+author: Étienne Deparis
-#+lang: en
+#+language: en
#+keywords: static website generator, ruby, gem, org mode, emacs
#+description: Fronde is a ruby gem, which helps you to manage your
#+description: static website generated with Org mode
@@ -35,22 +35,107 @@ with a global ruby installation too.
#+caption: Installation procedure with RVM
#+begin_src shell
-rvm get latest
-rvm use ruby-2.7.2@fronde --create
-gem install fronde
+ rvm get latest
+ rvm install ruby-3.2.2
+ rvm use ruby-3.2.2@fronde --create
+ gem install fronde
#+end_src
Now you're ready to create your first fronde website.
#+begin_src shell
-mkdir mywebite
-cd mywebsite
-fronde init -t "My first fronde website"
-fronde build
-fronde preview
+ mkdir mywebite
+ cd mywebsite
+ fronde init -t "My first fronde website"
+ fronde build
+ fronde preview
#+end_src
-* Support
+* Announcements
+
+** Nov. 2023 - v0.4 release
+
+The version 0.4 of fronde is made available for all. This new release
+brings a lot of stability improvements and inner refactoring. Even if
+the visible changes list might look a bit poor, this release is a big
+step forward and will help a lot to develop new features in the
+future. Some parts of the software were poorly designed and we worked
+hard to improve the coding.
+
+#+begin_noteblock
+After the update, you will need to update your installations and force a
+full publication of your websites (because the way templates are
+inserted inside published files changed). Actually, this will become
+the new standard routine after any fronde upgrade.
+
+#+begin_src shell
+ fronde update
+ fronde build -f
+#+end_src
+#+end_noteblock
+
+Here is the list of the most visible changes:
+
+- Improve Gemini support
+- Translate all console messages
+- Introduce a new onboarding command (~new~ instead of ~init~) to follow
+ other static website generator habbits
+- Rework tags and Atom feeds publication
+ + Output full article body in Atom feeds
+ + Announce each Atom feed in its related tag page
+ + Move tag pages and Atom feeds /inside/ the related blog source. They
+ are not shared anymore but specific to their source.
+
+#+begin_warningblock
+As tags and feeds are now published under their related source public
+path and no more directly at the root of the publication folder, you
+might want to setup some redirections in your web server configuration.
+
+For the following example configuration:
+
+#+begin_src yaml
+ ---
+ sources:
+ - path: src/news
+ is_blog: true
+#+end_src
+
+Feeds and tags will now be published under ~public_html/news/feeds/~ and
+~/public_html/news/tags/~ instead of ~public_html/feeds/~ and
+~public_html/tags/~. Thus you might want to setup some redirections like
+the following for nginx:
+
+#+begin_src
+ location ~ ^/tags/(?<slug>.*)$ {
+ return 301 /news/tags/$slug;
+ }
+ location ~ ^/feeds/(?<slug>.*)$ {
+ return 301 /news/feeds/$slug;
+ }
+#+end_src
+#+end_warningblock
+
+* Contributing
+
+#+html: <a href="./CODE_OF_CONDUCT.html"><img alt="Contributor Covenant" src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg"/></a>
+
+Fronde is proudly following the [[./CODE_OF_CONDUCT.org][Contributor Covenant Code of Conduct]].
+
+** Translations
+
+You can fetch the yaml files directly from the [[./locales][source code tree]] and
+send your changes as a pull request (see bellow) or by email, as you
+prefer.
+
+** Code
+
+The canonical repository is located at [[https://git.umaneti.net/fronde]].
+A mirror also exists on [[https://framagit.org/milouse/fronde][Framagit]] and [[https://github.com/milouse/chwall][Github]].
+
+Merge requests and issues are accepted both on framagit and github, or
+by mail to the main author.
+
+** Support
#+html: <a href="https://liberapay.com/milouse/donate"><img alt="Support using Liberapay" src="https://img.shields.io/badge/Liberapay-Support_me-yellow?logo=liberapay"/></a>
#+html: <a href="https://paypal.me/milouse"><img alt="Support using Paypal" src="https://img.shields.io/badge/Paypal-Support_me-00457C?logo=paypal&labelColor=lightgray"/></a>
diff --git a/bin/fronde b/bin/fronde
index 3da72a7..8bf16d8 100755
--- a/bin/fronde
+++ b/bin/fronde
@@ -1,45 +1,30 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
-require 'optparse'
-require 'r18n-core'
-require 'fronde/utils'
-require 'fronde/version'
-require 'fronde/cli'
-
-R18n.default_places = File.expand_path('../locales', __dir__)
-R18n.set Fronde::Config.get('lang')
+require_relative '../lib/fronde/config'
+require_relative '../lib/fronde/version'
+require_relative '../lib/fronde/cli'
+require 'optparse'
optparser = OptionParser.new
optparser.version = Fronde::VERSION
-Fronde::Utils::FRONDE_OPTIONS.each do |k, opt|
- optparser.send(opt[:meth] || :on, *Fronde::Utils.decorate_option(k))
+Fronde::CLI::OptParse::FRONDE_OPTIONS.each do |k, opt|
+ optparser.send(opt[:method] || :on, *Fronde::CLI::OptParse.decorate_option(k))
end
-params = {}
-optparser.parse!(into: params)
+params = { verbose: false }
+begin
+ optparser.parse!(into: params)
+rescue OptionParser::InvalidArgument => e
+ warn e.message
+ params.merge!(help: true, recover_from_error: true)
+end
if params[:version]
warn optparser.ver
exit
end
-if ARGV[0] == 'help'
- params[:help] = true
- ARGV.shift
-end
-fronde = Fronde::CLI.new(params)
-command = "fronde_#{ARGV[0]}".to_sym
-cmd_err = !fronde.respond_to?(command)
-if params[:help] || cmd_err
- cmd_err = false if params[:help] && !ARGV[0]
- fronde.fronde_help(ARGV[0], error: cmd_err)
-end
-ARGV.shift
-
-init_cmds = [:fronde_init, :fronde_config, :fronde_update]
-unless File.exist?('config.yml') || init_cmds.include?(command)
- fronde.fronde_init
-end
-fronde.send command
+fronde = Fronde::CLI::App.new(params)
+exit fronde.run(ARGV)
diff --git a/fronde.gemspec b/fronde.gemspec
index 6ad65e0..581eb15 100644
--- a/fronde.gemspec
+++ b/fronde.gemspec
@@ -10,6 +10,17 @@ Gem::Specification.new do |s|
Fronde helps you to convert Org mode files into websites, giving you
full control over the publication process.
DESC
+ s.post_install_message = <<~POSTINST
+ Start your first fronde project with:
+
+ fronde new myproject
+
+ Update your existing projects with:
+
+ fronde update
+ fronde build -f
+
+ POSTINST
s.authors = ['Étienne Deparis']
s.email = 'etienne@depar.is'
s.metadata = {
@@ -18,50 +29,64 @@ Gem::Specification.new do |s|
'homepage_uri' => 'https://etienne.depar.is/fronde/',
'funding_uri' => 'https://liberapay.com/milouse'
}
- s.files = ['lib/fronde/cli.rb',
- 'lib/fronde/cli/commands.rb',
- 'lib/fronde/config.rb',
- 'lib/fronde/config/lisp_config.rb',
- 'lib/fronde/config/org-config.el',
- 'lib/fronde/config/ox-fronde.el',
- 'lib/fronde/emacs.rb',
- 'lib/fronde/index.rb',
- 'lib/fronde/index/atom_generator.rb',
- 'lib/fronde/index/org_generator.rb',
- 'lib/fronde/org_file.rb',
- 'lib/fronde/org_file/class_methods.rb',
- 'lib/fronde/org_file/extracter.rb',
- 'lib/fronde/org_file/htmlizer.rb',
- 'lib/fronde/templater.rb',
- 'lib/fronde/utils.rb',
- 'lib/fronde/preview.rb',
- 'lib/fronde/version.rb',
- # Rake tasks
- 'lib/tasks/org.rake',
- 'lib/tasks/site.rake',
- 'lib/tasks/sync.rake',
- 'lib/tasks/tags.rake',
- # Translations
- 'locales/en.yml',
- 'locales/fr.yml',
- # Others
- 'LICENSE']
+ s.files = [
+ # Monkey patches / extensions
+ 'lib/ext/nil_time.rb',
+ 'lib/ext/r18n.rb',
+ 'lib/ext/time.rb',
+ # Core files
+ 'lib/fronde/cli/commands.rb',
+ 'lib/fronde/cli/data/config.yml',
+ 'lib/fronde/cli/data/gitignore',
+ 'lib/fronde/cli/data/Rakefile',
+ 'lib/fronde/cli/data/zsh_completion',
+ 'lib/fronde/cli/helpers.rb',
+ 'lib/fronde/cli/opt_parse.rb',
+ 'lib/fronde/cli.rb',
+ 'lib/fronde/cli/throbber.rb',
+ 'lib/fronde/config/data/org-config.el',
+ 'lib/fronde/config/data/ox-fronde.el',
+ 'lib/fronde/config/helpers.rb',
+ 'lib/fronde/config/lisp.rb',
+ 'lib/fronde/config.rb',
+ 'lib/fronde/emacs.rb',
+ 'lib/fronde/index/atom_generator.rb',
+ 'lib/fronde/index/data/all_tags.org',
+ 'lib/fronde/index/data/template.org',
+ 'lib/fronde/index/data/template.xml',
+ 'lib/fronde/index/org_generator.rb',
+ 'lib/fronde/index.rb',
+ 'lib/fronde/org/file_extracter.rb',
+ 'lib/fronde/org/file.rb',
+ 'lib/fronde/org.rb',
+ 'lib/fronde/preview.rb',
+ 'lib/fronde/slug.rb',
+ 'lib/fronde/source/gemini.rb',
+ 'lib/fronde/source/html.rb',
+ 'lib/fronde/source.rb',
+ 'lib/fronde/templater.rb',
+ 'lib/fronde/version.rb',
+ # Rake tasks
+ 'lib/tasks/cli.rake',
+ 'lib/tasks/org.rake',
+ 'lib/tasks/site.rake',
+ 'lib/tasks/sync.rake',
+ 'lib/tasks/tags.rake',
+ # Translations
+ 'locales/en.yml',
+ 'locales/fr.yml',
+ # Others
+ 'LICENSE'
+ ]
s.executables = ['fronde']
s.homepage = 'https://git.umaneti.net/fronde/about/'
s.license = 'WTFPL'
s.required_ruby_version = '>= 2.7'
+ s.add_runtime_dependency 'liquid', '~> 5.4'
s.add_runtime_dependency 'nokogiri', '~> 1.15'
s.add_runtime_dependency 'r18n-core', '~> 5.0'
s.add_runtime_dependency 'rainbow', '~> 3.1'
s.add_runtime_dependency 'rake', '~> 13.0'
s.add_runtime_dependency 'webrick', '~> 1.8'
-
- s.add_development_dependency 'org-ruby', '~> 0.9'
- s.add_development_dependency 'rspec', '~> 3.12'
- s.add_development_dependency 'rubocop', '~> 1.56'
- s.add_development_dependency 'rubocop-performance', '~> 1.19'
- s.add_development_dependency 'rubocop-rspec', '~> 2.23'
- s.add_development_dependency 'simplecov', '~> 0.22'
- s.add_development_dependency 'yard', '~> 0.9'
end
diff --git a/lib/ext/nil_time.rb b/lib/ext/nil_time.rb
new file mode 100644
index 0000000..a36b4c1
--- /dev/null
+++ b/lib/ext/nil_time.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# A time emulator to handle cases where no time is available
+class NilTime
+ def strftime(*)
+ ''
+ end
+
+ def xmlschema(_ = 0)
+ ''
+ end
+
+ def l18n_short_date_string
+ ''
+ end
+
+ def l18n_long_date_string(*)
+ ''
+ end
+
+ def l18n_short_date_html
+ '<time></time>'
+ end
+ alias_method :l18n_long_date_html, :l18n_long_date_string
+end
diff --git a/lib/ext/r18n.rb b/lib/ext/r18n.rb
new file mode 100644
index 0000000..b80b0f6
--- /dev/null
+++ b/lib/ext/r18n.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+# Monkey patch to add a little helper
+module R18nPatch
+ refine R18n::Translation do
+ def full_datetime_format
+ date_fmt = fronde.index.full_date_format(
+ date: @locale.full_format
+ )
+ date_fmt = @locale.year_format.sub('_', date_fmt)
+ time_fmt = @locale.time_format.delete('_').strip
+ fronde.index.full_date_with_time_format(
+ date: date_fmt, time: time_fmt
+ )
+ end
+ end
+end
diff --git a/lib/ext/time.rb b/lib/ext/time.rb
new file mode 100644
index 0000000..21a391c
--- /dev/null
+++ b/lib/ext/time.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+# Monkey patch to add some helpers
+module TimePatch
+ refine Time do
+ # Returns the current Time instance as a localized short string.
+ #
+ # @return [String] the localized Time string representation
+ def l18n_short_date_string
+ R18n.l to_date
+ end
+
+ # Format the current Time as a HTML `time` tag showing a short date.
+ #
+ # @return [String] the HTML `time` tag
+ def l18n_short_date_html
+ "<time datetime=\"#{xmlschema}\">#{l18n_short_date_string}</time>"
+ end
+
+ def no_time=(value)
+ @no_time = value
+ end
+
+ # Returns the current Time instance as a localized long string.
+ #
+ # @param with_year [Boolean] wether or not the string must contain the
+ # year
+ # @return [String] the localized Time string representation
+ def l18n_long_date_string(with_year: true)
+ locale = R18n.get.locale
+ long_fmt = R18n.t.fronde.index.full_date_format(
+ date: locale.format_date_full(self, year: with_year)
+ )
+ unless @no_time
+ long_fmt = R18n.t.fronde.index.full_date_with_time_format(
+ date: long_fmt, time: locale.time_format.delete('_').strip
+ )
+ end
+ locale.strftime(self, long_fmt)
+ end
+
+ # Format the current Time as a HTML `time` tag showing a long date.
+ #
+ # @return [String] the HTML `time` tag
+ def l18n_long_date_html
+ "<time datetime=\"#{xmlschema}\">#{l18n_long_date_string}</time>"
+ end
+ end
+end
diff --git a/lib/fronde/cli.rb b/lib/fronde/cli.rb
index 93566ea..c9a3241 100644
--- a/lib/fronde/cli.rb
+++ b/lib/fronde/cli.rb
@@ -1,60 +1,59 @@
# frozen_string_literal: true
require 'rake'
-require 'fronde/cli/commands'
+require_relative 'cli/commands'
module Fronde
- # Fronde CLI app
- class CLI
- def initialize(opts = {})
- @options = { verbose: false }.merge(opts)
- init_required_files
- init_rake
- end
+ module CLI
+ # Fronde CLI app
+ class App
+ def initialize(opts = {})
+ @options = { verbose: false }.merge(opts)
+ @command = @rake = nil
+ @argv = []
+ end
- include Fronde::CLICommands
+ include Commands
- private
+ def run(argv)
+ @argv = argv
+ @command = OptParse.resolve_possible_alias(@argv.shift || 'basic')
- def init_required_files
- init_rakefile unless File.exist?('Rakefile')
- init_gitignore unless File.exist?('.gitignore')
- end
+ if help_param_given?
+ return 2 if @options[:recover_from_error]
+ return 0
+ end
- def init_rake
- @rake = Rake.application
- Rake.verbose(false) unless @options[:verbose]
- @rake.raw_load_rakefile
- end
+ init_rake if %w[build preview publish].include?(@command)
- def init_rakefile
- rakefile = <<~RAKE
- # frozen_string_literal: true
+ method = "fronde_#{@command}".to_sym
+ return 2 if method_unknown?(method)
- require 'fronde/config'
- require 'r18n-core'
+ send method
+ end
- fronde_spec = Gem::Specification.find_by_name 'fronde'
- R18n.default_places = "\#{fronde_spec.gem_dir}/locales"
- R18n.set(Fronde::Config.get('lang', 'en'))
- R18n::Filters.on(:named_variables)
+ private
- Dir.glob("\#{fronde_spec.gem_dir}/lib/tasks/*.rake").each { |r| import r }
+ def init_rake
+ @rake = Rake.application
+ Rake.verbose(false) unless @options[:verbose]
+ @rake.load_rakefile
+ end
- task default: 'site:build'
- RAKE
- File.write 'Rakefile', rakefile
- end
+ def help_param_given?
+ return false unless @options[:help]
+
+ fronde_help
+ true
+ end
+
+ def method_unknown?(method)
+ return false if respond_to?(method)
- def init_gitignore
- gitignore = <<~GITIGNORE
- .dir-locals.el
- Rakefile
- lib
- public_html
- var
- GITIGNORE
- File.write '.gitignore', gitignore
+ warn R18n.t.fronde.error.bin.no_command
+ fronde_help
+ true
+ end
end
end
end
diff --git a/lib/fronde/cli/commands.rb b/lib/fronde/cli/commands.rb
index 2b86f4d..784407d 100644
--- a/lib/fronde/cli/commands.rb
+++ b/lib/fronde/cli/commands.rb
@@ -1,125 +1,114 @@
# frozen_string_literal: true
+require_relative 'helpers'
+require_relative 'opt_parse'
+require_relative '../slug'
+require_relative '../org/file'
+
module Fronde
- # Fronde commands
- module CLICommands
- def fronde_update
- update_config
- @rake.options.build_all = true
- @rake.invoke_task('org:upgrade')
- end
- alias_method :fronde_config, :fronde_update
+ module CLI
+ # Fronde commands
+ module Commands
+ def fronde_new
+ new_dir = @argv.first || 'my_fronde_website'
+ FileUtils.mkdir new_dir
+ Dir.chdir new_dir
+ Helpers.init_config_file @options
+ Fronde::CONFIG.reset # Correctly compute various path
+ Helpers.init_rakefile
+ init_rake
+ @rake['org:install'].invoke
+ @argv = ['src/index.org']
+ fronde_open
+ end
- def fronde_init
- update_config
- @rake.options.build_all = true
- @rake.invoke_task('org:install')
- return if File.exist? 'src/index.org'
- Fronde::OrgFile.new('src/index.org', @options).write
- fronde_open 'src/index.org'
- end
+ def fronde_update
+ Helpers.init_rakefile
+ init_rake
+ @rake.options.build_all = true
+ @rake['org:upgrade'].invoke
+ 0
+ end
- def fronde_build
- @rake.options.build_all = true
- task = 'site:build'
- task = "#{task}[true]" if @options[:force]
- @rake.invoke_task task
- end
+ def fronde_build
+ @rake.options.build_all = true
+ @rake['site:build'].invoke @options[:force]
+ 0
+ end
- def fronde_preview
- Thread.new do
- sleep 1
- port = Fronde::Config.get(['preview', 'server_port'], 5000)
- uri = "http://127.0.0.1:#{port}/"
- current_os = Fronde::Utils.current_os
- case current_os
- when 'windows'
- system 'start', uri
- when 'apple'
- system 'open', uri
- else
- system 'gio', 'open', uri
+ def fronde_preview
+ Thread.new do
+ sleep 1
+ port = Fronde::CONFIG.get(%w[preview server_port], 5000)
+ Helpers.launch_app_for_uri "http://127.0.0.1:#{port}/"
end
+ @rake['site:preview'].invoke
+ 0
end
- @rake.invoke_task('site:preview')
- end
- def fronde_open(file_path = ARGV[0])
- editor = ENV['EDITOR'] || ENV['VISUAL'] || 'emacs'
- cmd = [editor]
- if file_path.nil? || !File.file?(file_path)
- # file_path may be updated with title given in options
- file_path = create_new_file(file_path)
- # Only move to the end of file for new file. Let the editor handle
- # the best position for already existing files
- cmd << '+6'
+ def fronde_open
+ editor = ENV['EDITOR'] || ENV['VISUAL'] || 'emacs'
+ cmd = [editor]
+ file_path = @argv.first || Dir.pwd
+ unless File.file?(file_path)
+ # file_path may be updated with title given in options
+ file_path = create_new_file(file_path)
+ # Only move to the end of file for new file. Let the editor handle
+ # the best position for already existing files
+ cmd << '+6'
+ end
+ cmd << file_path
+ (system(*cmd) && 0) || 1
end
- cmd << file_path
- system(*cmd)
- end
- alias_method :fronde_edit, :fronde_open
- def fronde_publish
- @rake.invoke_task('sync:push')
- end
+ def fronde_publish
+ @rake['sync:push'].invoke
+ 0
+ end
- def fronde_help(command = 'basic', error: false)
- warn R18n.t.fronde.bin.error.no_command if error
- cmd_opt = Fronde::Utils.command_options(command)
- label = cmd_opt[:label] || command
- warn format("%<label>s\n\n", label: R18n.t.fronde.bin.usage(label))
- cmd = cmd_opt[:name] || command
- if R18n.t.fronde.bin.commands[cmd].translated?
- warn format("%<label>s\n\n", label: R18n.t.fronde.bin.commands[cmd])
+ def fronde_help
+ # Try to find command in next argv, otherwise fallback again.
+ @command = @argv.shift || 'basic' if @command == 'help'
+ cmd_opt = OptParse.command_options(@command)
+ label = cmd_opt[:label] || @command
+ warn format("%<label>s\n\n", label: R18n.t.fronde.bin.usage(label))
+ cmd = cmd_opt[:name] || @command
+ if R18n.t.fronde.bin.commands[cmd].translated?
+ warn format("%<label>s\n\n", label: R18n.t.fronde.bin.commands[cmd])
+ end
+ body = OptParse.help_command_body(cmd)
+ warn body unless body == ''
+ 0
end
- warn help_command_body(cmd).join("\n")
- exit 1 if error
- exit
- end
- private
+ private
- def update_config
- cnf = @options.merge
- cnf.delete(:verbose)
- cnf.transform_keys!(&:to_s)
- Fronde::Config.save(Fronde::Config.settings.merge(cnf))
- end
+ def file_name_from_title
+ title = @options[:title] || R18n.t.fronde.bin.options.default_title
+ # No title, nor a reliable file_path? Better abort
+ raise R18n.t.fronde.error.bin.no_file if title == ''
- def new_file_name(file_path)
- file_path = File.expand_path(file_path || '')
- return file_path if file_path[-4..] == '.org'
- # file_path seems to be a dir path. Thus we have to create the new
- # filename from its title
- title = @options[:title]
- # No title, nor a reliable file_path? Better abort
- return nil if title.nil? || title == ''
- filename = "#{Fronde::OrgFile.slug(title)}.org"
- File.join file_path, filename
- end
+ "#{Fronde::Slug.slug(title)}.org"
+ end
- def create_new_file(file_path)
- filename = new_file_name(file_path)
- if filename.nil?
- warn R18n.t.fronde.bin.error.no_file
- exit 1
+ def new_file_name(file_path)
+ file_path = File.expand_path(file_path)
+
+ if file_path[-4..] == '.org' && !File.directory?(file_path)
+ return file_path
+ end
+
+ # file_path seems to be a dir path. Thus we have to create the new
+ # filename from its title
+ File.join file_path, file_name_from_title
end
- FileUtils.mkdir_p File.dirname(filename)
- Fronde::OrgFile.new(filename, @options).write
- filename
- end
- def help_command_body(command)
- body = [
- R18n.t.fronde.bin.options.cmd_title,
- Fronde::Utils.summarize_command(command)
- ]
- return body unless command == 'basic'
- body + [
- '',
- R18n.t.fronde.bin.commands.cmd_title,
- Fronde::Utils.list_commands
- ]
+ def create_new_file(file_path)
+ filename = new_file_name(file_path)
+ FileUtils.mkdir_p File.dirname(filename)
+ Fronde::Org::File.new(filename, @options).write
+ filename
+ end
end
end
end
diff --git a/lib/fronde/cli/data/Rakefile b/lib/fronde/cli/data/Rakefile
new file mode 100644
index 0000000..01091d4
--- /dev/null
+++ b/lib/fronde/cli/data/Rakefile
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require 'fronde/config'
+
+fronde_spec = Gem::Specification.find_by_name 'fronde'
+Dir.glob("#{fronde_spec.gem_dir}/lib/tasks/*.rake").each { |r| import r }
+
+task default: 'site:build'
diff --git a/lib/fronde/cli/data/config.yml b/lib/fronde/cli/data/config.yml
new file mode 100644
index 0000000..569a5b7
--- /dev/null
+++ b/lib/fronde/cli/data/config.yml
@@ -0,0 +1,13 @@
+---
+{%- if author %}
+author: {{ author }}
+{%- endif %}
+{%- if lang %}
+lang: {{ lang }}
+{%- endif %}
+sources:
+ - path: src
+ target: .
+{%- if output == 'gemini' %}
+ type: gemini
+{%- endif %}
diff --git a/lib/fronde/cli/data/gitignore b/lib/fronde/cli/data/gitignore
new file mode 100644
index 0000000..f94b452
--- /dev/null
+++ b/lib/fronde/cli/data/gitignore
@@ -0,0 +1,7 @@
+.dir-locals.el
+.ruby-version
+Rakefile
+lib/
+public_gmi/
+public_html/
+var/
diff --git a/lib/fronde/cli/data/zsh_completion b/lib/fronde/cli/data/zsh_completion
new file mode 100644
index 0000000..a49a4a0
--- /dev/null
+++ b/lib/fronde/cli/data/zsh_completion
@@ -0,0 +1,37 @@
+#compdef fronde
+#autoload
+
+{% for command in commands %}
+{%- for option in command.options %}
+{%- if forloop.first %}
+(( $+functions[__fronde_{{ command.name }}] )) ||
+__fronde_{{ command.name }}(){
+ _arguments \
+{%- endif %}
+ '({{ option.short }} {{ option.long }})'{{ '{' }}{{ option.short}},{{ option.long }}}
+ {%- if option.keyword %}':{{ option.keyword }}:'{% endif %}
+{%- if forloop.last %}
+{%- if command.name == 'open' %} \
+ '1:file:_files -g \*.org'
+{%- endif %}
+}
+{% else %} \{% endif %}
+{%- endfor %}{% endfor %}
+
+(( $+functions[__fronde_help] )) ||
+__fronde_help(){
+ _arguments \
+ "1:command:(({% for command in commands %}{{ command.name }}\:'{{ command.translation }}' {% endfor %}))"
+}
+
+local state
+
+_arguments -C \
+ '(-)-h[help]' \
+ '(-)-V[version]' \
+ "1:command:(({% for command in commands %}{{ command.name }}\:'{{ command.translation }}' {% endfor %}))" \
+ '*::arg:->args'
+
+if [ "$state" = args ]; then
+ _call_function ret __fronde_${words[1]}
+fi
diff --git a/lib/fronde/cli/helpers.rb b/lib/fronde/cli/helpers.rb
new file mode 100644
index 0000000..0214f5f
--- /dev/null
+++ b/lib/fronde/cli/helpers.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'uri'
+require 'net/http'
+require 'fileutils'
+
+module Fronde
+ module CLI
+ # Various utilitaries methods
+ module Helpers
+ def self.init_config_file(config)
+ return if File.exist? 'config.yml'
+
+ output = config[:output] || 'html'
+ output = 'gemini' if output == 'gmi'
+ data = { 'author' => config[:author],
+ 'lang' => config[:lang],
+ 'output' => output }
+ source = File.expand_path './data/config.yml', __dir__
+ template = Liquid::Template.parse(File.read(source))
+ File.write('config.yml', template.render(data))
+ end
+
+ def self.init_rakefile
+ FileUtils.cp(
+ File.expand_path('./data/Rakefile', __dir__),
+ 'Rakefile'
+ )
+ end
+
+ def self.launch_app_for_uri(uri)
+ case current_os
+ when 'windows'
+ system 'start', uri
+ when 'apple'
+ system 'open', uri
+ else
+ system 'gio', 'open', uri
+ end
+ end
+
+ # Try to discover the current host operating system.
+ #
+ # @return [String] either apple, windows or linux (default)
+ def self.current_os
+ if ENV['OS'] == 'Windows_NT' || RUBY_PLATFORM.include?('cygwin')
+ return 'windows'
+ end
+ return 'apple' if RUBY_PLATFORM.include?('darwin')
+
+ 'linux'
+ end
+ end
+ end
+end
diff --git a/lib/fronde/cli/opt_parse.rb b/lib/fronde/cli/opt_parse.rb
new file mode 100644
index 0000000..d11e465
--- /dev/null
+++ b/lib/fronde/cli/opt_parse.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+module Fronde
+ module CLI
+ # Helper code to help build the fronde option parser
+ module OptParse
+ # @return [Hash] the possible ~fronde~ options and their
+ # configuration
+ FRONDE_OPTIONS = {
+ '-a' => { long: 'author' },
+ '-f' => { long: 'force', boolean: true },
+ '-h' => { long: 'help', boolean: true, method: :on_tail,
+ help: R18n.t.fronde.bin.options.help },
+ '-l' => { long: 'lang', keyword: 'LOCALE' },
+ '-o' => { long: 'output', keyword: 'FORMAT', choices: %w[gemini html] },
+ '-t' => { long: 'title' },
+ '-v' => { long: 'verbose', boolean: true, method: :on_tail },
+ '-V' => { long: 'version', boolean: true, method: :on_tail,
+ help: R18n.t.fronde.bin.options.version }
+ }.freeze
+
+ # TODO: jekyll new [path] / jekyll build / jekyll clean / jekyll serve
+ # TODO: hugo new site [path] / hugo / hugo new content / hugo server
+ # TODO: zola init [path] / zola build --root path_to_project / zola serve
+
+ # @return [Hash] the possible ~fronde~ subcommands and their
+ # configuration
+ FRONDE_COMMANDS = {
+ 'new' => { opts: ['-a', '-l', '-o', '-t', '-v'], label: 'new <path>' },
+ 'init' => { alias: 'new' },
+ 'update' => {},
+ 'config' => { alias: 'update' },
+ 'preview' => {},
+ 'open' => { opts: ['-a', '-l', '-t', '-v'], label: 'open <path>' },
+ 'edit' => { alias: 'open' },
+ 'build' => { opts: ['-f'] },
+ 'publish' => {},
+ 'help' => {},
+ 'basic' => { opts: ['-h', '-V'], label: '<command>' }
+ }.freeze
+
+ class << self
+ # Returns the short and long options specification for a given
+ # short option.
+ #
+ # This method use the {Fronde::CLI::OptParse::FRONDE_OPTIONS}
+ # Hash to retrieve corresponding values.
+ #
+ # @example
+ # spec = Fronde::CLI::OptParse.decorate_option('-a')
+ # => ['-a AUTHOR', '--author AUTHOR']
+ #
+ # @param short [String] the short option to decorate
+ # @return [Array] the short and long specification for an option
+ def decorate_option(short)
+ opt = FRONDE_OPTIONS[short]
+ long = "--#{opt[:long]}"
+ if opt[:boolean]
+ config = [short, long]
+ else
+ key = opt[:keyword] || opt[:long].upcase
+ config = [short, format('%<long>s %<key>s', long: long, key: key)]
+ end
+ config << opt[:choices] if opt[:choices]
+ config << opt[:help] if opt[:help]
+ config
+ end
+
+ # Returns the ~fronde~ help summary for a given command.
+ #
+ # @param command [String] the command for which a summary
+ # should be given
+ # @return [String]
+ def summarize_command(command)
+ (FRONDE_COMMANDS[command][:opts] || []).map do |k|
+ short, long = decorate_option(k)
+ opt = FRONDE_OPTIONS[k]
+ label = [short, long].join(', ')
+ line = [format(' %<opt>s', opt: label).ljust(30)]
+ help = opt[:help]
+ line << help if help
+ choices = opt[:choices]
+ line << "(#{choices.join(', ')})" if choices
+ line.join(' ')
+ end.join("\n")
+ end
+
+ def help_command_body(command)
+ command_opts_doc = summarize_command(command)
+ return '' if command_opts_doc == ''
+
+ body = [R18n.t.fronde.bin.options.cmd_title, command_opts_doc]
+ if command == 'basic'
+ body += ['', R18n.t.fronde.bin.commands.cmd_title, list_commands]
+ end
+ body.join("\n")
+ end
+
+ # Returns a formatted list of available commands for ~fronde~.
+ #
+ # @return [String]
+ def list_commands
+ FRONDE_COMMANDS.filter_map do |cmd, opt|
+ next if cmd == 'basic'
+
+ line = [' ', cmd.ljust(10)]
+ if opt.has_key? :alias
+ line << R18n.t.fronde.bin.commands.alias(opt[:alias])
+ else
+ line << R18n.t.fronde.bin.commands[cmd]
+ end
+ line.join(' ')
+ end.join("\n")
+ end
+
+ # Returns the real command name for a given command, which may be
+ # an alias.
+ #
+ # @param command [String] the command to resolve
+ # @return [String]
+ def resolve_possible_alias(command)
+ return 'basic' unless FRONDE_COMMANDS.include?(command)
+
+ cmd_opt = FRONDE_COMMANDS[command]
+ return cmd_opt[:alias] if cmd_opt.has_key?(:alias)
+
+ command
+ end
+
+ # Returns the given command options.
+ #
+ # This method will first try to resolve command alias, if any.
+ #
+ # @param command [String] the command, which options should be returned
+ # @return [Hash] the command options
+ def command_options(command)
+ cmd = resolve_possible_alias command
+ FRONDE_COMMANDS[cmd].merge(name: cmd)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fronde/cli/throbber.rb b/lib/fronde/cli/throbber.rb
new file mode 100644
index 0000000..fa887da
--- /dev/null
+++ b/lib/fronde/cli/throbber.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'rainbow'
+
+module Fronde
+ module CLI
+ # Decorations for the command line
+ class Throbber
+ # @return [Hash] the possible throbber themes
+ THROBBER_FRAMES = {
+ 'basic' => '-\|/',
+ 'basicdots' => '⋯⋱⋮⋰',
+ 'moon' => '🌑🌒🌓🌔🌕🌖🌗🌘',
+ 'clock' => '🕛🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚',
+ 'bricks' => '⣷⣯⣟⡿⢿⣻⣽⣾',
+ 'points' => '·⁘⁛⁘',
+ 'quadrant' => '▙▛▜▟',
+ 'default' => ['⠁ ⠂ ⠄ ⡀ ⠄ ⠂ ⠁', '⠂ ⠁ ⠂ ⠄ ⡀ ⠄ ⠂', '⠄ ⠂ ⠁ ⠂ ⠄ ⡀ ⠄',
+ '⡀ ⠄ ⠂ ⠁ ⠂ ⠄ ⡀', '⠄ ⡀ ⠄ ⠂ ⠁ ⠂ ⠄', '⠂ ⠄ ⡀ ⠄ ⠂ ⠁ ⠂']
+ }.freeze
+
+ def initialize(thread, message)
+ @frames = select_frames
+ @thread = thread
+ @thread.abort_on_exception = false
+ @thread.report_on_exception = false
+ @message = message
+ end
+
+ def run
+ term_width = terminal_width
+ frames_len = @frames.length
+ current = 0
+ while @thread.alive?
+ sleep 0.1
+ frame = @frames[current % frames_len]
+ message = "#{@message} #{frame}"
+ print "#{message.ljust(term_width)}\r"
+ current += 1
+ end
+ @thread.join # Ensure any inner exception is re-raised
+ rescue RuntimeError => e
+ show_error
+ raise e
+ else
+ done = Rainbow(R18n.t.fronde.bin.done).green
+ puts "#{@message} #{done}".ljust(term_width)
+ end
+
+ class << self
+ # Animates strings in the user console to alert him that something
+ # is running in the background.
+ #
+ # The animation is chosen among a bunch of themes, with the
+ # configuration option ~throbber~ (retrieved via
+ # {Fronde::Config::Store#get}).
+ #
+ # @example
+ # long_stuff = Thread.new { very_long_operation }
+ # Fronde::CLI::Throbber.run(long_stuff, 'Computing hard stuff:')
+ #
+ # @param thread [Thread] the long-running operation to decorate
+ # @param message [String] the message to display before the throbber
+ # @return [void]
+ def run(thread, message)
+ throbber = new(thread, message)
+ throbber.run
+ end
+ end
+
+ private
+
+ def terminal_width
+ # Not a tty. Docker?
+ return 0 unless system('test -t 0')
+
+ `stty size`.strip.split[1].to_i - 1
+ end
+
+ def show_error
+ warn(
+ format(
+ "%<message>s %<label>s\n%<explanation>s",
+ message: @message,
+ label: Rainbow(R18n.t.fronde.error.bin.label).bold.red,
+ explanation: Rainbow(R18n.t.fronde.error.bin.explanation).bold
+ )
+ )
+ end
+
+ def select_frames
+ model = Fronde::CONFIG.get 'throbber', 'default'
+ model = 'default' unless THROBBER_FRAMES.has_key?(model)
+
+ THROBBER_FRAMES[model]
+ end
+ end
+ end
+end
diff --git a/lib/fronde/config.rb b/lib/fronde/config.rb
index f8dee65..e1cdf06 100644
--- a/lib/fronde/config.rb
+++ b/lib/fronde/config.rb
@@ -1,44 +1,62 @@
# frozen_string_literal: true
require 'yaml'
-require 'fronde/config/lisp_config'
+require 'r18n-core'
+require 'singleton'
+
+require_relative 'config/lisp'
+require_relative 'source'
module Fronde
- # Wrapper for configuration
- #
- # This class is a singleton interface, which share the static website
- # being build settings among different steps or tasks.
- #
- # It expects the website author to holds their custom settings in a
- # YAML file named ~config.yml~ available at the root of their
- # project.
- #
- # For example, with the given config file:
- #
- # #+begin_src
- # ---
- # title: My website
- # author: Alice Doe
- # #+end_src
- #
- # Settings will be available like this:
- #
- # #+begin_src
- # Fronde::Config.get('author')
- # => "Alice Doe"
- # #+end_src
- class Config
- extend Fronde::LispConfig
-
- class << self
+ module Config
+ # Wrapper for configuration
+ #
+ # This class is a singleton interface, which share the static website
+ # being build settings among different steps or tasks.
+ #
+ # It expects the website author to holds their custom settings in a
+ # YAML file named ~config.yml~ available at the root of their
+ # project.
+ #
+ # For example, with the given config file:
+ #
+ # #+begin_src
+ # ---
+ # title: My website
+ # author: Alice Doe
+ # #+end_src
+ #
+ # Settings will be available like this:
+ #
+ # #+begin_src
+ # Fronde::CONFIG.get('author')
+ # => "Alice Doe"
+ # #+end_src
+ class Store
+ include Singleton
+
+ attr_reader :sources
+
+ def initialize
+ @default_settings = {
+ 'author' => (ENV['USER'] || ''),
+ 'domain' => '',
+ 'lang' => Fronde::Config::Helpers.extract_lang_from_env('en'),
+ 'html_public_folder' => 'public_html',
+ 'gemini_public_folder' => 'public_gmi',
+ 'templates' => [], 'theme' => 'default'
+ }.freeze
+ @org_version = @sources = nil
+ @config = load_settings
+ # Do not load sources now to avoid dependency loop on config
+ end
+
+ include Fronde::Config::Lisp
+
# Access the current website settings
#
- # If necessary, this method will load settings from a config
- # file.
- #
# @return [Hash] the website settings
def settings
- load_settings
@config
end
@@ -56,7 +74,6 @@ module Fronde
# @param default the default value to use if ~setting~ is absent
# @return the setting value or nil
def get(setting, default = nil)
- load_settings
if setting.is_a? Array
value = @config.dig(*setting)
else
@@ -65,31 +82,6 @@ module Fronde
value || default
end
- # Save the settings given as a parameter to the ~config.yml~ file.
- #
- # Not only this method overwrite the old settings, but it replace
- # the current shared settings with the ones given in
- # parameter. Later call to
- # {file:Fronde/Config.html#settings-class_method settings}
- # will, obviously, use these new settings.
- #
- # @param new_config [Hash] the settings to save
- # @return [Hash] the new settings after save
- def save(new_config)
- # Do not save obvious default config values. We'll always try to
- # save author and lang as they default on system variables,
- # which may be different from a system to another. Thus it may
- # be confusing if one use fronde on two different computer and
- # these params always change.
- default_keys = default_settings.keys
- new_config.delete_if do |k, v|
- default_keys.include?(k) && v == default_settings[k]
- end
- File.write 'config.yml', new_config.to_yaml
- @config = @sources = nil
- load_settings # Reload config, taking default settings into account
- end
-
# Reset settings
#
# This method is handy for testing purpose. Next call to
@@ -99,7 +91,10 @@ module Fronde
#
# @return nil
def reset
- @sources = @config = nil
+ # Reload config, taking default settings into account
+ @config = load_settings
+ @org_version = @sources = nil
+ @sources = load_sources
end
# Load the given settings as if they comes from the ~config.yml~ file.
@@ -111,68 +106,109 @@ module Fronde
# use these new settings.
#
# @param config [Hash] the settings to artificially load
- # @return [Hash] the new settings
+ # @return [Fronde::Config::Store] self
def load_test(config)
- reset
- @config = default_settings.merge config
- sources
- @config
+ @config = @default_settings.merge config
+ @org_version = @sources = nil
+ @sources = load_sources
+ self
end
# Return the qualified projects sources list.
#
# @return [Array] the fully qualified projects sources list
- def sources
+ def load_sources
return @sources if @sources
- load_settings
- default_sources = [{ 'path' => 'src', 'target' => '.' }]
- @sources = get('sources', default_sources).map do |s|
- build_source(s)
- end.compact
+
+ @sources = remove_inclusion(remove_duplicate(build_sources))
end
private
def load_settings
- return @config if @config
conf_file = 'config.yml'
+ user_conf = {}
if File.exist? conf_file
- @config = default_settings.merge(YAML.load_file(conf_file)).freeze
- else
- @config = default_settings
+ user_conf = YAML.load_file(conf_file)
+ user_conf = Fronde::Config::Helpers.migrate(user_conf)
end
+ user_conf = @default_settings.merge(user_conf)
+ Fronde::Config::Helpers.ensure_expanded_paths(user_conf).freeze
end
- def extract_lang_from_env(default)
- (ENV['LANG'] || default).split('_', 2).first
+ def build_sources
+ default_sources = [{ 'path' => 'src', 'target' => '.' }]
+ get('sources', default_sources).filter_map do |source_conf|
+ config = Source.canonical_config source_conf.dup
+ unless config['path']
+ warn R18n.t.fronde.error.source.no_path(source: config.inspect)
+ next
+ end
+ Source.new_from_config config
+ end
end
- def default_settings
- return @default_settings if @default_settings
- @default_settings = {
- 'author' => (ENV['USER'] || ''),
- 'domain' => '',
- 'lang' => extract_lang_from_env('en'),
- 'public_folder' => 'public_html',
- 'templates' => [],
- 'theme' => 'default'
- }.freeze
+ def remove_duplicate(sources)
+ check_paths = {}
+ sources.each do |source|
+ type = source.type
+ check_paths[type] ||= {}
+ path = source['path']
+ # Avoid duplicate
+ if check_paths[type].has_key?(path)
+ warn(
+ R18n.t.fronde.error.source.duplicate(
+ source: source['name'], type: type
+ )
+ )
+ next
+ end
+ check_paths[type][path] = source
+ end
+ check_paths
end
- def build_source(seed)
- opts = { 'recursive' => true, 'is_blog' => false }
- case seed
- when String
- opts['path'] = seed
- when Hash
- opts.merge! seed
- end
- return nil unless opts.has_key?('path')
- opts['path'] = File.expand_path(opts['path'])
- opts['name'] ||= File.basename(opts['path']).sub(/^\./, '')
- opts['target'] ||= opts['name']
- opts
+ def remove_inclusion(check_paths)
+ check_paths.map do |type, sources_by_path|
+ skip_paths = []
+
+ # Check paths in the right order
+ sorted_paths = sources_by_path.keys.sort_by(&:length)
+ sorted_paths.filter_map do |path|
+ next if skip_paths.include?(path)
+
+ source = sources_by_path[path]
+ # If current source is not recursive, there is no possible
+ # issue
+ next source unless source.recursive?
+
+ # Ensure that the current source does not embed another one
+ possible_matchs = sorted_paths.select do |other_path|
+ path != other_path && other_path.start_with?(path)
+ end
+ next source if possible_matchs.empty?
+
+ skip_paths += possible_matchs
+ possible_matchs.each do |match|
+ other_source = sources_by_path[match]
+ warn(
+ R18n.t.fronde.error.source.inclusion(
+ source: other_source['title'], type: type,
+ other_source: source['title']
+ )
+ )
+ end
+ end
+ end.flatten
end
end
end
+
+ CONFIG = Config::Store.instance
end
+
+R18n.default_places = File.expand_path('../../locales', __dir__)
+R18n::Filters.on(:named_variables)
+R18n.set Fronde::CONFIG.get('lang')
+
+Fronde::CONFIG.load_sources
diff --git a/lib/fronde/config/data/org-config.el b/lib/fronde/config/data/org-config.el
new file mode 100644
index 0000000..36f3070
--- /dev/null
+++ b/lib/fronde/config/data/org-config.el
@@ -0,0 +1,24 @@
+;; Add org-mode to load path
+(add-to-list 'load-path (expand-file-name "org-{{ org_version }}/lisp" "{{ work_dir }}/lib"))
+;; Load last version of htmlize.el
+(load-file (expand-file-name "htmlize.el" "{{ work_dir }}/lib"))
+
+;; Current project options
+(setq fronde/version "{{ version }}"
+ fronde/current-work-dir "{{ work_dir }}"
+ user-mail-address "{{ author.email }}"
+ user-full-name "{{ author.name }}"
+ org-html-metadata-timestamp-format "{{ long_date_fmt }}"
+ org-gmi-timestamp-format "{{ long_date_fmt }}"
+ org-publish-project-alist
+ `({% for project in all_projects -%}
+ ("{{ project.name }}"
+ {%- for attribute in project.attributes %}
+ :{{ attribute[0] }} {{ attribute[1] | cast_lisp_value: attribute[0] }}
+ {%- endfor %})
+ {% endfor -%}
+ ("website" :components ("{{ all_projects | map: 'name' | join: '" "' | remove: '" "tags' }}"))))
+
+;; Load fronde lib
+(load-file (expand-file-name "ox-gmi.el" "{{ work_dir }}/lib"))
+(load-file (expand-file-name "ox-fronde.el" "{{ fronde_data_dir }}"))
diff --git a/lib/fronde/config/ox-fronde.el b/lib/fronde/config/data/ox-fronde.el
index dbb174a..6ffa6ba 100644
--- a/lib/fronde/config/ox-fronde.el
+++ b/lib/fronde/config/data/ox-fronde.el
@@ -60,7 +60,7 @@ INFO is a plist used as a communication channel."
output)
(push `(?l . ,(org-export-data (plist-get info :language) info)) output)
(push `(?n . ,(format "Fronde %s" fronde/version)) output)
- (push `(?N . ,(format "<a href=\"https://git.umaneti.net/fronde/about/\">Fronde</a> %s" fronde/version)) output)
+ (push `(?N . ,(format "<a href=\"https://etienne.depar.is/fronde/\">Fronde</a> %s" fronde/version)) output)
(push `(?x . ,(org-export-data (plist-get info :description) info)) output)
(push `(?X . ,(format "<p>%s</p>"
(org-export-data (plist-get info :description) info)))
diff --git a/lib/fronde/config/helpers.rb b/lib/fronde/config/helpers.rb
new file mode 100644
index 0000000..460844a
--- /dev/null
+++ b/lib/fronde/config/helpers.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'liquid'
+require 'digest/md5'
+
+module Fronde
+ module Config
+ # Various utilitaries methods
+ module Helpers
+ def self.extract_lang_from_env(default)
+ (ENV['LANG'] || default).split('_', 2).first
+ end
+
+ def self.migrate(config)
+ return config unless config.has_key?('public_folder')
+
+ warn R18n.t.fronde.error.config.deprecated_public_folder
+ old_pub_folder = config.delete('public_folder')
+ return config if config.has_key?('html_public_folder')
+
+ config['html_public_folder'] = old_pub_folder
+ config
+ end
+
+ def self.ensure_expanded_paths(config)
+ %w[html gemini].each do |what|
+ key = "#{what}_public_folder"
+ config[key] = File.expand_path config[key]
+ end
+ config
+ end
+
+ # Generate emacs directory variables file.
+ #
+ # This method generate the file ~.dir-locals.el~, which is
+ # responsible to load fronde Org settings when visiting an Org file
+ # of this fronde instance.
+ #
+ # @return [Integer] the length written (as returned by the
+ # underlying ~File.write~ method call)
+ def self.write_dir_locals
+ workdir = Dir.pwd
+ # rubocop:disable Layout/LineLength
+ File.write(
+ "#{workdir}/.dir-locals.el",
+ "((org-mode . ((eval . (load-file \"#{workdir}/var/lib/org-config.el\")))))"
+ )
+ # rubocop:enable Layout/LineLength
+ end
+
+ def self.render_liquid_template(content, data)
+ template = Liquid::Template.parse(content)
+ template.render(data)
+ end
+ end
+
+ # Filter for liquid templates
+ module Filters
+ def cast_lisp_value(value, key)
+ return 't' if value.is_a?(TrueClass)
+ return 'nil' if value.nil? || value.is_a?(FalseClass)
+
+ value = value.strip
+ lisp_keywords = ['t', 'nil', '1', '-1', '0']
+ if key.end_with?('-function') || lisp_keywords.include?(value)
+ return value
+ end
+
+ value.gsub!('"', '\"')
+ %("#{value}")
+ end
+
+ def md5(value)
+ Digest::MD5.hexdigest(value)
+ end
+ end
+ end
+end
+
+Liquid::Template.register_filter(Fronde::Config::Filters)
diff --git a/lib/fronde/config/lisp.rb b/lib/fronde/config/lisp.rb
new file mode 100644
index 0000000..338cc9e
--- /dev/null
+++ b/lib/fronde/config/lisp.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'json'
+require 'open-uri'
+require_relative '../version'
+require_relative '../org'
+require_relative 'helpers'
+
+require_relative '../../ext/r18n'
+using R18nPatch
+
+module Fronde
+ module Config
+ # This module contains utilitary methods to ease ~org-config.el~
+ # file generation
+ module Lisp
+ # Generate emacs lisp configuration file for Org and write it.
+ #
+ # This method saves the generated configuration in the file
+ # ~org-config.el~ at the root of your project, overwriting it if it
+ # existed already.
+ #
+ # @return [Integer] the length written (as returned by the
+ # underlying ~File.write~ method call)
+ # rubocop:disable Metrics/MethodLength
+ def write_org_lisp_config
+ workdir = Dir.pwd
+ all_projects = sources.map(&:org_config).flatten
+ all_themes = org_generate_themes(all_projects)
+ FileUtils.mkdir_p "#{workdir}/var/lib"
+ content = Helpers.render_liquid_template(
+ File.read(File.expand_path('./data/org-config.el', __dir__)),
+ 'version' => Fronde::VERSION,
+ 'work_dir' => workdir,
+ 'fronde_data_dir' => File.expand_path('data', __dir__),
+ 'org_version' => Fronde::Org.current_version,
+ 'long_date_fmt' => R18n.t.full_datetime_format.to_s,
+ 'author' => { 'email' => get('author_email', ''),
+ 'name' => get('author') },
+ 'all_projects' => all_projects + all_themes
+ )
+ File.write("#{workdir}/var/lib/org-config.el", content)
+ end
+ # rubocop:enable Metrics/MethodLength
+
+ private
+
+ def org_theme_config(theme)
+ { 'base-directory' => File.expand_path("themes/#{theme}"),
+ # rubocop:disable Layout/LineLength
+ 'base-extension' => %w[css js gif jpg png svg otf ttf woff2?].join('\\\\|'),
+ 'publishing-directory' => "#{get('html_public_folder')}/assets/#{theme}",
+ # rubocop:enable Layout/LineLength
+ 'publishing-function' => 'org-publish-attachment',
+ 'recursive' => true }
+ end
+
+ def org_generate_themes(projects)
+ all_themes = projects.filter_map { |project| project['theme'] }
+ all_themes << get('theme')
+ all_themes.uniq.compact.filter_map do |theme|
+ next if theme == 'default'
+
+ { 'name' => "theme-#{theme}",
+ 'attributes' => org_theme_config(theme) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/fronde/config/lisp_config.rb b/lib/fronde/config/lisp_config.rb
deleted file mode 100644
index 3862d7f..0000000
--- a/lib/fronde/config/lisp_config.rb
+++ /dev/null
@@ -1,340 +0,0 @@
-# frozen_string_literal: true
-
-require 'json'
-require 'open-uri'
-require 'fronde/version'
-
-def fetch_org_version
- # Retrieve last org version from git repository tags page.
- tag_rx = Regexp.new(
- '<a href=\'/cgit/emacs/org-mode.git/tag/\?h=' \
- '(?<tag>release_(?<number>[^\']+))\'>\k<tag></a>'
- )
- versions = URI(
- 'https://git.savannah.gnu.org/cgit/emacs/org-mode.git/refs/'
- ).open.readlines.map do |line|
- line.match(tag_rx) { |matchdata| matchdata[:number] }
- end
- versions.compact.first
-end
-
-module Fronde
- # This module contains utilitary methods to ease ~org-config.el~
- # file generation
- module LispConfig
- # Fetch and return the last published version of Org.
- #
- # @return [String] the new x.x.x version string of Org
- def org_last_version
- return @org_version if @org_version
- if File.exist?('var/tmp/last_org_version')
- @org_version = File.read('var/tmp/last_org_version')
- return @org_version
- end
- @org_version = fetch_org_version
- FileUtils.mkdir_p 'var/tmp'
- File.write('var/tmp/last_org_version', @org_version)
- @org_version
- end
-
- # Generate emacs lisp configuration file for Org and write it.
- #
- # This method saves the generated configuration in the file
- # ~org-config.el~ at the root of your project, overwriting it if it
- # existed already.
- #
- # @return [Integer] the length written (as returned by the
- # underlying ~File.write~ method call)
- # rubocop:disable Metrics/MethodLength
- def write_org_lisp_config(with_tags: false)
- projects = org_generate_projects(with_tags: with_tags)
- workdir = Dir.pwd
- content = File.read(File.expand_path('./org-config.el', __dir__))
- .gsub('__VERSION__', Fronde::VERSION)
- .gsub('__WORK_DIR__', workdir)
- .gsub('__FRONDE_DIR__', __dir__)
- .gsub('__ORG_VER__', org_last_version)
- .gsub(
- '__ALL_PROJECTS__',
- projects.values.join("\n ")
- )
- .gsub('__THEME_CONFIG__', org_default_theme_config)
- .gsub('__ALL_PROJECTS_NAMES__', project_names(projects))
- .gsub('__LONG_DATE_FMT__', r18n_full_datetime_format)
- .gsub('__AUTHOR_EMAIL__', get('author_email', ''))
- .gsub('__AUTHOR_NAME__', get('author'))
- FileUtils.mkdir_p "#{workdir}/var/lib"
- File.write("#{workdir}/var/lib/org-config.el", content)
- end
- # rubocop:enable Metrics/MethodLength
-
- # Generate emacs directory variables file.
- #
- # This method generate the file ~.dir-locals.el~, which is
- # responsible to load fronde Org settings when visiting an Org file
- # of this fronde instance.
- #
- # @return [Integer] the length written (as returned by the
- # underlying ~File.write~ method call)
- def write_dir_locals
- workdir = Dir.pwd
- # rubocop:disable Layout/LineLength
- File.write(
- "#{workdir}/.dir-locals.el",
- "((org-mode . ((eval . (load-file \"#{workdir}/var/lib/org-config.el\")))))"
- )
- # rubocop:enable Layout/LineLength
- end
-
- private
-
- def r18n_full_datetime_format
- locale = R18n.get.locale
- date_fmt = R18n.t.fronde.index.full_date_format(
- date: locale.full_format
- )
- date_fmt = locale.year_format.sub('_', date_fmt)
- time_fmt = locale.time_format.delete('_').strip
- R18n.t.fronde.index.full_date_with_time_format(
- date: date_fmt, time: time_fmt
- )
- end
-
- def ruby_to_lisp_boolean(value)
- return 't' if value == true
- 'nil'
- end
-
- def project_names(projects)
- names = projects.keys.map do |p|
- ["\"#{p}\"", "\"#{p}-assets\""]
- end.flatten
- names << "\"theme-#{get('theme')}\"" unless get('theme') == 'default'
- sources.each do |s|
- # Default theme defined in settings is already included
- next unless s['theme'] && s['theme'] != get('theme')
- # Never include theme named 'default' as it does not rely on any
- # file to export.
- next if s['theme'] == 'default'
- theme = "\"theme-#{s['theme']}\""
- next if names.include? theme
- names << theme
- end
- names.join(' ')
- end
-
- # Return the full path to the publication path of a given project
- # configuration.
- #
- # @param project [Hash] a project configuration (as extracted from
- # the ~sources~ key)
- # @return [String] the full path to the target dir of this project
- def publication_path(project)
- publish_in = [Dir.pwd]
- if project['type'] == 'gemini'
- publish_in << get('gemini_public_folder', 'public_gmi')
- else
- publish_in << get('public_folder')
- end
- publish_in << project['target'] unless project['target'] == '.'
- publish_in.join('/')
- end
-
- # Return the publication function needed for a given project
- # configuration.
- #
- # @param project [Hash] a project configuration (as extracted from
- # the ~sources~ key)
- # @return [String] the org publication function name
- def publication_function(project)
- case project['type']
- when 'gemini'
- 'org-gmi-publish-to-gemini'
- else
- 'org-html-publish-to-html'
- end
- end
-
- def org_project(project_name, opts)
- shared_lines = org_project_shared_lines(opts)
- project_config = [
- org_project_config(project_name, opts, shared_lines),
- org_assets_config(project_name, shared_lines)
- ]
- themeconf = org_theme_config(opts['theme'])
- project_config << themeconf unless themeconf == ''
- project_config.join("\n ")
- end
-
- def org_default_html_postamble
- <<~POSTAMBLE
- <p><span class="author">#{R18n.t.fronde.org.postamble.written_by}</span>
- #{R18n.t.fronde.org.postamble.with_emacs_html}</p>
- <p class="date">#{R18n.t.fronde.org.postamble.last_modification}</p>
- <p class="validation">%v</p>
- POSTAMBLE
- end
-
- def org_default_gemini_postamble
- format(
- "📅 %<date>s\n📝 %<author>s %<creator>s",
- author: R18n.t.fronde.org.postamble.written_by,
- creator: R18n.t.fronde.org.postamble.with_emacs,
- date: R18n.t.fronde.org.postamble.last_modification
- )
- end
-
- def org_default_html_head
- <<~HTMLHEAD
- <link rel="stylesheet" type="text/css" media="screen"
- href="__DOMAIN__/assets/__THEME__/css/style.css">
- <link rel="stylesheet" type="text/css" media="screen"
- href="__DOMAIN__/assets/__THEME__/css/htmlize.css">
- __ATOM_FEED__
- HTMLHEAD
- end
-
- def org_default_html_options(project)
- defaults = {
- 'html-postamble' => org_default_html_postamble,
- 'html-head' => '__ATOM_FEED__',
- 'html-head-include-default-style' => 't',
- 'html-head-include-scripts' => 't'
- }
- curtheme = project['theme'] || get('theme')
- return defaults if curtheme.nil? || curtheme == 'default'
- defaults['html-head'] = org_default_html_head
- defaults['html-head-include-default-style'] = 'nil'
- defaults['html-head-include-scripts'] = 'nil'
- defaults
- end
-
- def org_publish_options(project)
- defaults = {
- 'section-numbers' => 'nil',
- 'with-toc' => 'nil'
- }
- if project['type'] == 'gemini'
- defaults['gemini-postamble'] = org_default_gemini_postamble
- else
- defaults.merge!(
- org_default_html_options(project),
- get('org-html', {}),
- project['org-html'] || {}
- )
- end
- defaults.merge(project['org-options'] || {})
- end
-
- def expand_vars_in_html_head(head, project)
- curtheme = project['theme'] || get('theme')
- # Head may be frozen when coming from settings
- head = head.gsub('__THEME__', curtheme)
- .gsub('__DOMAIN__', get('domain'))
- return head.gsub('__ATOM_FEED__', '') unless project['is_blog']
- atomfeed = <<~ATOMFEED
- <link rel="alternate" type="application/atom+xml" title="Atom 1.0"
- href="#{get('domain')}/feeds/index.xml" />
- ATOMFEED
- head.gsub('__ATOM_FEED__', atomfeed)
- end
-
- def cast_lisp_value(value)
- return 't' if value.is_a?(TrueClass)
- return 'nil' if value.nil? || value.is_a?(FalseClass)
- value.strip.gsub(/"/, '\"')
- end
-
- def build_project_org_headers(project)
- orgtplopts = org_publish_options(project)
- lisp_keywords = ['t', 'nil', '1', '-1', '0'].freeze
- orgtplopts.map do |k, v|
- v = expand_vars_in_html_head(v, project) if k == 'html-head'
- val = cast_lisp_value(v)
- if lisp_keywords.include? val
- ":#{k} #{val}"
- else
- ":#{k} \"#{val}\""
- end
- end
- end
-
- def org_generate_projects(with_tags: false)
- projects = {}
- projects_sources = sources
- if with_tags
- tags_conf = build_source('tags')
- tags_conf['recursive'] = false
- projects_sources << tags_conf
- end
- projects_sources.each do |opts|
- opts['org_headers'] = build_project_org_headers(opts)
- projects[opts['name']] = org_project(opts['name'], opts)
- end
- projects
- end
-
- def org_default_theme_config
- theme_config = org_theme_config(get('theme'))
- return theme_config if theme_config == ''
- format("\n %<conf>s", conf: theme_config)
- end
-
- def org_theme_config(theme)
- return '' if theme.nil? || theme == 'default'
- workdir = Dir.pwd
- [
- format('("theme-%<theme>s"', theme: theme),
- format(' :base-directory "%<wd>s/themes/%<theme>s"',
- wd: workdir, theme: theme),
- # rubocop:disable Layout/LineLength
- ' :base-extension "jpg\\\\\\|gif\\\\\\|png\\\\\\|js\\\\\\|css\\\\\\|otf\\\\\\|ttf\\\\\\|woff2?"',
- # rubocop:enable Layout/LineLength
- ' :recursive t',
- format(' :publishing-directory "%<wd>s/%<pub>s/assets/%<theme>s"',
- wd: workdir, pub: get('public_folder'), theme: theme),
- ' :publishing-function org-publish-attachment)'
- ].join("\n ").strip
- end
-
- def org_project_shared_lines(project)
- [
- format(':base-directory "%<path>s"', path: project['path']),
- format(
- ':publishing-directory "%<path>s"',
- path: publication_path(project)
- ),
- format(
- ':recursive %<rec>s',
- rec: ruby_to_lisp_boolean(project['recursive'])
- )
- ]
- end
-
- def org_project_config(project_name, project, shared_lines)
- project_lines = [
- format('"%<name>s"', name: project_name),
- ':base-extension "org"',
- format(
- ':publishing-function %<fun>s',
- fun: publication_function(project)
- )
- ] + shared_lines + project['org_headers']
- if project['exclude']
- project_lines << format(
- ':exclude "%<value>s"', value: project['exclude']
- )
- end
- format('(%<pr>s)', pr: project_lines.join("\n "))
- end
-
- def org_assets_config(project_name, shared_lines)
- assets_lines = [
- format('"%<name>s-assets"', name: project_name),
- ':base-extension "jpg\\\\\\|gif\\\\\\|png\\\\\\|svg\\\\\\|pdf"',
- ':publishing-function org-publish-attachment'
- ] + shared_lines
- format('(%<assets>s)', assets: assets_lines.join("\n "))
- end
- end
-end
diff --git a/lib/fronde/config/org-config.el b/lib/fronde/config/org-config.el
deleted file mode 100644
index 2fbd491..0000000
--- a/lib/fronde/config/org-config.el
+++ /dev/null
@@ -1,19 +0,0 @@
-;; Add org-mode to load path
-(add-to-list 'load-path (expand-file-name "org-__ORG_VER__/lisp" "__WORK_DIR__/lib"))
-;; Load last version of htmlize.el
-(load-file (expand-file-name "htmlize.el" "__WORK_DIR__/lib"))
-
-;; Current project options
-(setq fronde/version "__VERSION__"
- fronde/current-work-dir "__WORK_DIR__"
- user-mail-address "__AUTHOR_EMAIL__"
- user-full-name "__AUTHOR_NAME__"
- org-html-metadata-timestamp-format "__LONG_DATE_FMT__"
- org-gmi-timestamp-format "__LONG_DATE_FMT__"
- org-publish-project-alist
- `(__ALL_PROJECTS____THEME_CONFIG__
- ("website" :components (__ALL_PROJECTS_NAMES__))))
-
-;; Load fronde lib
-(load-file (expand-file-name "ox-gmi.el" "__WORK_DIR__/lib"))
-(load-file (expand-file-name "ox-fronde.el" "__FRONDE_DIR__"))
diff --git a/lib/fronde/emacs.rb b/lib/fronde/emacs.rb
index 9295a07..0224e98 100644
--- a/lib/fronde/emacs.rb
+++ b/lib/fronde/emacs.rb
@@ -1,37 +1,40 @@
# frozen_string_literal: true
-require 'fronde/config'
+require_relative 'config'
module Fronde
# Wraps Gnu/Emacs calls
class Emacs
- def initialize(file_path: nil, verbose: false)
- @file = file_path
+ def initialize(verbose: false)
@verbose = verbose
+ @command = nil
end
- def publish
- command = emacs_command(
- '-l ./var/lib/org-config.el', '--eval \'(org-publish "website")\''
- )
- if @verbose
- warn command
- return system(command, exception: true)
- end
- system command, out: '/dev/null', err: '/dev/null', exception: true
+ def publish(project = 'website')
+ build_command("(org-publish \"#{project}\")")
+ run_command
end
private
- def emacs_command(*arguments)
- default_emacs = Fronde::Config.get('emacs')
- emacs_cmd = [
- default_emacs || 'emacs -Q --batch -nw',
- '--eval \'(setq enable-dir-local-variables nil)\''
+ def run_command
+ cmd = @command.join(' ')
+ if @verbose
+ warn cmd
+ return system(cmd, exception: true)
+ end
+ system cmd, out: '/dev/null', err: '/dev/null', exception: true
+ end
+
+ def build_command(org_action)
+ default_emacs = Fronde::CONFIG.get('emacs')
+ @command = [default_emacs || 'emacs -Q --batch -nw']
+ @command << '--eval \'(setq inhibit-message t)\'' unless @verbose
+ @command += [
+ '--eval \'(setq enable-dir-local-variables nil)\'',
+ '-l ./var/lib/org-config.el',
+ "--eval '#{org_action}'"
]
- emacs_cmd << '--eval \'(setq inhibit-message t)\'' unless @verbose
- emacs_cmd.concat(arguments)
- emacs_cmd.join(' ')
end
end
end
diff --git a/lib/fronde/index.rb b/lib/fronde/index.rb
index 06fa9cc..b4bc228 100644
--- a/lib/fronde/index.rb
+++ b/lib/fronde/index.rb
@@ -1,11 +1,8 @@
# frozen_string_literal: true
-require 'fileutils'
-require 'digest/md5'
-require 'fronde/config'
-require 'fronde/org_file'
-require 'fronde/index/atom_generator'
-require 'fronde/index/org_generator'
+require_relative 'slug'
+require_relative 'config'
+require_relative 'org/file'
module Fronde
# Generates website indexes and atom feeds for all the org documents
@@ -13,118 +10,95 @@ module Fronde
class Index
attr_reader :date
- include Fronde::IndexAtomGenerator
- include Fronde::IndexOrgGenerator
-
- def initialize
- @pubdir = Fronde::Config.get('public_folder')
+ def initialize(project)
+ @project = project
@index = { 'index' => [] }
- @projects = {}
+ @entries = []
@tags_names = {}
- @date = DateTime.now
- feed
- sort!
+ @date = Time.now
+ generate_feeds if @project.blog?
+ sort_feeds!
end
- def entries
- @index.keys.reject { |k| k == 'index' }
+ def all_tags
+ @index.keys.reject { |tag| tag == 'index' }
end
def empty?
@index['index'].empty?
end
- def write_all(verbose: true)
- @index.each_key do |k|
- write_org(k)
- warn "Generated index file for #{k}" if verbose
- write_atom(k)
- warn "Generated atom feed for #{k}" if verbose
- end
- write_all_blog_home(verbose)
- end
-
def sort_by(kind)
- if [:name, :weight].include?(kind)
+ accepted_values = %i[name weight]
+ if accepted_values.include?(kind)
tags_sorted = sort_tags_by_name_and_weight["by_#{kind}".to_sym]
# Reverse in order to have most important or A near next prompt
# and avoid to scroll to find the beginning of the list.
- return tags_sorted.map do |k|
- @tags_names[k] + " (#{@index[k].length})"
+ return tags_sorted.map do |tag|
+ @tags_names[tag] + " (#{@index[tag].length})"
end.reverse
end
- raise ArgumentError, "#{kind} not in [:name, :weight]"
+ error_msg = R18n.t.fronde.error.index.wrong_sort_kind(
+ kind: kind, accepted_values: accepted_values.inspect
+ )
+ raise ArgumentError, error_msg
end
- private
+ class << self
+ def all_html_blog_index(&block)
+ all_blogs = CONFIG.sources.filter_map do |project|
+ next unless project['type'] == 'html' && project.blog?
- def feed
- Fronde::Config.sources.each do |project|
- next unless project['is_blog']
- if project['recursive']
- file_pattern = '**/*.org'
- else
- file_pattern = '*.org'
- end
- Dir.glob(file_pattern, base: project['path']).map do |s|
- org_file = File.join(project['path'], s)
- next if exclude_file?(org_file, project)
- add_to_indexes(
- Fronde::OrgFile.new(org_file, project: project)
- )
+ Index.new(project)
end
+ return all_blogs unless block
+
+ all_blogs.each(&block)
end
end
- def add_to_project_index(article)
- project = article.project
- @projects[project['name']] ||= []
- @projects[project['name']] << article
+ private
+
+ def generate_feeds
+ file_pattern = '**/*.org'
+ Dir.glob(file_pattern, base: @project['path']).map do |index_file|
+ # Obviously don't parse tags
+ next if index_file.start_with?('tags/')
+
+ org_file = File.join(@project['path'], index_file)
+ next if @project.exclude_file? org_file
+
+ add_to_indexes(Org::File.new(org_file))
+ end
end
def add_to_indexes(article)
@index['index'] << article
- add_to_project_index article
- article.keywords.each do |k|
- slug = Fronde::OrgFile.slug k
- @tags_names[slug] = k # Overwrite is permitted
+ @entries << article
+ article.keywords.each do |tag|
+ slug = Slug.slug tag
+ @tags_names[slug] = tag # Overwrite is permitted
@index[slug] ||= []
@index[slug] << article
end
end
- def sort!
- @index.each do |k, i|
- @index[k] = i.sort { |a, b| b.timekey <=> a.timekey }
- end
- @projects.each do |k, i|
- @projects[k] = i.sort { |a, b| b.timekey <=> a.timekey }
+ def sort_feeds!
+ @index.transform_values! do |articles|
+ articles.sort_by(&:timekey).reverse
end
+ @entries = @entries.sort_by(&:timekey).reverse
end
def sort_tags_by_name_and_weight
- tags_sorted = {}
- all_keys = entries
- tags_sorted[:by_name] = all_keys.sort
- tags_sorted[:by_weight] = all_keys.sort do |a, b|
- @index[b].length <=> @index[a].length
- end
- tags_sorted
- end
-
- def exclude_file?(file_path, project)
- # Obviously excluding index itself for blogs
- return true if file_path == File.join(project['path'], 'index.org')
- return false unless project['exclude']
- file_path.match? project['exclude']
- end
-
- def save?
- return true unless empty?
- Fronde::Config.sources.each do |project|
- return true if project['is_blog'] && Dir.exist?(project['path'])
- end
- false
+ all_keys = all_tags
+ {
+ by_name: all_keys.sort,
+ by_weight: all_keys.sort_by { @index[_1].length }.reverse
+ }
end
end
end
+
+require_relative 'index/org_generator'
+require_relative 'index/atom_generator'
diff --git a/lib/fronde/index/atom_generator.rb b/lib/fronde/index/atom_generator.rb
index 028189b..f00e229 100644
--- a/lib/fronde/index/atom_generator.rb
+++ b/lib/fronde/index/atom_generator.rb
@@ -1,85 +1,74 @@
# frozen_string_literal: true
-require 'cgi'
-require 'fronde/config'
+require_relative '../config'
module Fronde
- # Embeds Atom feeds sepecific methods
- module IndexAtomGenerator
+ # Reopen Index class to embed Atom feeds sepecific methods
+ class Index
def to_atom(index_name = 'index')
- content = [atom_header(index_name)]
- @index[index_name][0...10].each do |article|
- content << atom_entry(article)
- end
- format '%<content>s</feed>', content: content.join("\n")
+ entries = @index[index_name][0...10].map(&:to_h)
+ return atom_index(entries) if index_name == 'index'
+
+ atom_file(index_name, entries)
end
def write_atom(index_name)
- return unless save?
- slug = Fronde::OrgFile.slug index_name
- FileUtils.mkdir_p "#{@pubdir}/feeds"
- atomdest = "#{@pubdir}/feeds/#{slug}.xml"
- File.write(atomdest, to_atom(index_name))
+ slug = Slug.slug index_name
+ atomdest = "#{@project.publication_path}/feeds/#{slug}.xml"
+ File.write atomdest, to_atom(index_name)
+ end
+
+ def write_all_feeds(verbose: true)
+ FileUtils.mkdir_p "#{@project.publication_path}/feeds"
+ @index.each_key do |tag|
+ write_atom(tag)
+ warn R18n.t.fronde.index.atom_generated(tag: tag) if verbose
+ end
end
private
- # Render the Atom feed header.
+ # Render an Atom feed file.
#
- # @param title [String] the title of the current atom feed
- # @return [String] the Atom header as a String
- def atom_header(title)
- domain = Fronde::Config.get('domain')
- upddate = @date.rfc3339
- if title == 'index'
- slug = 'index'
- tagurl = domain
- title = Fronde::Config.get('title', R18n.t.fronde.index.all_tags)
- else
- slug = Fronde::OrgFile.slug(title)
- tagurl = "#{domain}/tags/#{slug}.html"
- title = @tags_names[title]
- end
- title = CGI.escapeHTML(title)
- <<~ENDATOM
- <?xml version="1.0" encoding="utf-8"?>
- <feed xmlns="http://www.w3.org/2005/Atom"
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:wfw="http://wellformedweb.org/CommentAPI/"
- xml:lang="#{Fronde::Config.get('lang')}">
-
- <title>#{title}</title>
- <link href="#{domain}/feeds/#{slug}.xml" rel="self" type="application/atom+xml"/>
- <link href="#{tagurl}" rel="alternate" type="text/html" title="#{title}"/>
- <updated>#{upddate}</updated>
- <author><name>#{Fronde::Config.get('author', '')}</name></author>
- <id>urn:md5:#{Digest::MD5.hexdigest(domain)}</id>
- <generator uri="https://git.umaneti.net/fronde/about/">Fronde</generator>
- ENDATOM
+ # @param tag_name [String] the tag name of the current atom feed
+ # @param entries [Array] the article to list in this file
+ # @return [String] the Atom feed as a String
+ def atom_file(tag_name, entries)
+ domain = Fronde::CONFIG.get('domain')
+ slug = Slug.slug(tag_name)
+ tagurl = "#{domain}#{@project.public_absolute_path}tags/#{slug}.html"
+ Config::Helpers.render_liquid_template(
+ File.read(File.expand_path('./data/template.xml', __dir__)),
+ 'title' => @tags_names[tag_name],
+ 'lang' => Fronde::CONFIG.get('lang'),
+ 'domain' => domain,
+ 'slug' => slug,
+ 'tagurl' => tagurl,
+ 'upddate' => @date.xmlschema,
+ 'author' => Fronde::CONFIG.get('author'),
+ 'publication_format' => @project['mime_type'],
+ 'entries' => entries
+ )
end
- # Render an Atom feed entry.
+ # Render the main/index Atom feed.
#
- # @param article [Fronde::OrgFile] the related org document for this
- # entry
- # @return [String] the Atom entry as a String
- def atom_entry(article)
- keywords = article.keywords.map do |k|
- "<dc:subject>#{CGI.escapeHTML(k)}</dc:subject>"
- end.join
- keywords += "\n " if keywords != ''
- title = CGI.escapeHTML(article.title)
- <<~ENDENTRY
- <entry>
- <title>#{title}</title>
- <link href="#{article.url}" rel="alternate" type="text/html"
- title="#{title}"/>
- <id>urn:md5:#{Digest::MD5.hexdigest(article.timekey)}</id>
- <published>#{article.datestring(:rfc3339)}</published>
- <author><name>#{CGI.escapeHTML(article.author)}</name></author>
- #{keywords}<content type="html">#{CGI.escapeHTML(article.excerpt)}</content>
- </entry>
- ENDENTRY
+ # @param entries [Array] the article to list in this file
+ # @return [String] the Atom feed as a String
+ def atom_index(entries)
+ domain = Fronde::CONFIG.get('domain')
+ Config::Helpers.render_liquid_template(
+ File.read(File.expand_path('./data/template.xml', __dir__)),
+ 'title' => @project['title'],
+ 'lang' => Fronde::CONFIG.get('lang'),
+ 'domain' => domain,
+ 'slug' => 'index',
+ 'tagurl' => domain,
+ 'upddate' => @date.xmlschema,
+ 'author' => Fronde::CONFIG.get('author'),
+ 'publication_format' => @project['mime_type'],
+ 'entries' => entries
+ )
end
end
end
diff --git a/lib/fronde/index/data/all_tags.org b/lib/fronde/index/data/all_tags.org
new file mode 100644
index 0000000..91020c7
--- /dev/null
+++ b/lib/fronde/index/data/all_tags.org
@@ -0,0 +1,14 @@
+#+title: {{ title }}
+#+author: {{ author }}
+#+language: {{ lang }}
+{% for index in indexes %}
+* {{ index.title }}
+:PROPERTIES:
+:HTML_CONTAINER_CLASS: index-tags
+:UNNUMBERED: notoc
+:END:
+
+{% for tag in index.tags -%}
+- [[{{ domain }}{{ project_path }}tags/{{ tag.slug }}.html][{{ tag.title }}]] ({{ tag.weight }})
+{% endfor %}
+{%- endfor -%}
diff --git a/lib/fronde/index/data/template.org b/lib/fronde/index/data/template.org
new file mode 100644
index 0000000..a7891f6
--- /dev/null
+++ b/lib/fronde/index/data/template.org
@@ -0,0 +1,22 @@
+#+title: {{ title }}
+#+author: {{ author }}
+#+language: {{ lang }}
+{%- unless slug == '__HOME_PAGE__' %}
+#+html_head_extra: <link rel="alternate" type="application/atom+xml" title="{{ title }}" href="{{ domain }}{{ project_path }}feeds/{{ slug }}.xml" />
+{% endunless -%}
+{%- assign last_year = 0 -%}
+{% for article in entries %}
+{% assign cur_year = article.timekey | slice: 0, 4 %}
+{%- unless cur_year == last_year %}
+* {% if cur_year == "0000" %}{{ unsorted }}{% else %}{{ cur_year }}{% endif %}
+:PROPERTIES:
+:HTML_CONTAINER_CLASS: index-year
+:UNNUMBERED: notoc
+:END:
+{% assign last_year = cur_year %}
+{% endunless -%}
+- *[[{{ article.url }}][{{ article.title }}]]*
+{%- if article.published != '' %} / {{ article.published }}{% endif -%}
+{%- if article.excerpt != '' %} \\
+ {{ article.excerpt }}{% endif %}
+{%- endfor %}
diff --git a/lib/fronde/index/data/template.xml b/lib/fronde/index/data/template.xml
new file mode 100644
index 0000000..124568a
--- /dev/null
+++ b/lib/fronde/index/data/template.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xml:lang="{{ lang }}">
+
+ <title>{{ title | escape }}</title>
+ <link href="{{ domain }}/feeds/{{ slug }}.xml" rel="self" type="application/atom+xml"/>
+ <link href="{{ tagurl }}" rel="alternate" type="text/html" title="{{ title }}"/>
+ <updated>{{ upddate }}</updated>
+ <author><name>{{ author }}</name></author>
+ <id>urn:md5:{{ domain | md5 }}</id>
+ <generator uri="https://git.umaneti.net/fronde/about/">Fronde</generator>
+
+ {%- for article in entries %}
+
+ <entry>
+ <title>{{ article.title | escape }}</title>
+ <link href="{{ article.url }}" rel="alternate"
+ type="{{ publication_format }}"
+ title="{{ article.title }}"/>
+ <id>urn:md5:{{ article.timekey | md5 }}</id>
+ <published>{{ article.published_xml }}</published>
+ <updated>{{ article.updated_xml }}</updated>
+ <author><name>{{ article.author | escape }}</name></author>
+ {%- for keyword in article.keywords %}
+ <dc:subject>{{ keyword | escape }}</dc:subject>
+ {%- endfor %}
+ {%- if publication_format == 'text/html' %}
+ <content type="html">
+ {%- else %}
+ <content>
+ {%- endif %}
+ {{- article.published_body | escape -}}
+ </content>
+ </entry>
+ {%- endfor %}
+</feed>
diff --git a/lib/fronde/index/org_generator.rb b/lib/fronde/index/org_generator.rb
index 1d27a02..0fa325d 100644
--- a/lib/fronde/index/org_generator.rb
+++ b/lib/fronde/index/org_generator.rb
@@ -1,114 +1,96 @@
# frozen_string_literal: true
+using TimePatch
+
module Fronde
- # Embeds methods responsible for generating an org file for a given
- # index.
- module IndexOrgGenerator
- def to_org(index_name = 'index', is_project: false)
- return project_home_page(index_name) if is_project
+ # Reopen Index class to embed methods responsible for generating an
+ # org file for a given index.
+ class Index
+ def blog_home_page
+ org_index @project['title'], '__HOME_PAGE__', @entries.map(&:to_h)
+ end
+
+ def to_org(index_name = 'index')
return all_tags_index if index_name == 'index'
- [org_header(index_name),
- org_articles(@index[index_name])].join("\n")
+
+ org_index(
+ @tags_names[index_name], index_name,
+ (@index[index_name] || []).map(&:to_h)
+ )
end
alias_method :to_s, :to_org
def write_org(index_name)
- return unless save?
- slug = Fronde::OrgFile.slug index_name
- FileUtils.mkdir 'tags' unless Dir.exist? 'tags'
- content = to_org index_name
- orgdest = "tags/#{slug}.org"
- File.write(orgdest, content)
+ slug = Slug.slug index_name
+ orgdest = "#{@project['path']}/tags/#{slug}.org"
+ File.write orgdest, to_org(index_name)
end
- private
-
- def project_home_page(project_name)
- content = [org_header(project_name, is_tag: false)]
- if @projects[project_name]&.any?
- content += org_articles(@projects[project_name])
+ def write_all_org(verbose: true)
+ FileUtils.mkdir_p "#{@project['path']}/tags"
+ @index.each_key do |tag|
+ write_org(tag)
+ warn R18n.t.fronde.index.index_generated(tag: tag) if verbose
end
- content.join("\n")
+ write_blog_home_page(verbose)
end
- def write_all_blog_home(verbose)
- Fronde::Config.sources.each do |project|
- next unless project['is_blog']
- next unless Dir.exist?(project['path'])
- warn "Generated blog home for #{project['name']}" if verbose
- orgdest = format('%<root>s/index.org', root: project['path'])
- File.write(orgdest, to_org(project['name'], is_project: true))
- end
- end
+ private
- def all_tags_index
- content = [
- org_header(R18n.t.fronde.index.all_tags, is_tag: false)
- ]
- sort_tags_by_name_and_weight.each do |t, tags|
- content << ''
- content << org_title(R18n.t.fronde.index.send(t), 'index-tags')
- next if tags.empty?
- tags.each do |k|
- content << "- #{tag_published_url(k)} (#{@index[k].length})"
+ # Render an Org index file.
+ #
+ # @param title [String] the title of the current org index
+ # @param slug [String] the slug of the current org index
+ # @param entries [Array] the article to list in this file
+ # @return [String] the org file content as a String
+ def org_index(title, slug, entries)
+ entries.map! do |article|
+ published = article['published']
+ unless published == ''
+ article['published'] = R18n.t.fronde.index.published_on published
end
+ article
end
- content.join("\n")
- end
-
- def tag_published_url(tag_name)
- domain = Fronde::Config.get('domain')
- title = @tags_names[tag_name]
- tag_link = "#{domain}/tags/#{tag_name}.html"
- "[[#{tag_link}][#{title}]]"
+ Config::Helpers.render_liquid_template(
+ File.read(File.expand_path('./data/template.org', __dir__)),
+ 'title' => title,
+ 'slug' => slug,
+ 'project_path' => @project.public_absolute_path,
+ 'domain' => Fronde::CONFIG.get('domain'),
+ 'lang' => Fronde::CONFIG.get('lang'),
+ 'author' => Fronde::CONFIG.get('author'),
+ 'unsorted' => R18n.t.fronde.index.unsorted,
+ 'entries' => entries
+ )
end
- def org_header(title = nil, is_tag: true)
- if is_tag
- title = @tags_names[title]
- elsif title.nil? || title == 'index'
- title = Fronde::Config.get('title')
- end
- <<~HEADER.strip
- #+title: #{title}
- #+author: #{Fronde::Config.get('author')}
- #+language: #{Fronde::Config.get('lang')}
- HEADER
- end
-
- def org_articles(articles_list)
- last_year = nil
- articles_list.map do |article|
- year_title = ''
- year = article.timekey.slice(0, 4)
- if year != last_year
- year_title = format("\n%<title>s\n", title: org_title(year))
- last_year = year
+ def all_tags_index
+ indexes = sort_tags_by_name_and_weight.map do |title, tags|
+ all_tags = tags.map do |tag|
+ {
+ 'slug' => tag, 'title' => @tags_names[tag],
+ 'weight' => @index[tag].length
+ }
end
- year_title + org_entry(article)
+ { 'title' => R18n.t.fronde.index.send(title), 'tags' => all_tags }
end
+ Config::Helpers.render_liquid_template(
+ File.read(File.expand_path('./data/all_tags.org', __dir__)),
+ 'title' => R18n.t.fronde.index.all_tags,
+ 'lang' => Fronde::CONFIG.get('lang'),
+ 'author' => Fronde::CONFIG.get('author'),
+ 'domain' => Fronde::CONFIG.get('domain'),
+ 'project_path' => @project.public_absolute_path,
+ 'indexes' => indexes
+ )
end
- def org_entry(article)
- line = "- *[[#{article.url}][#{article.title}]]*"
- if article.date
- art_date = article.datestring(:full, year: false)
- published = R18n.t.fronde.index.published_on art_date
- line += " / #{published}"
+ def write_blog_home_page(verbose)
+ orgdest = format('%<root>s/index.org', root: @project['path'])
+ if verbose
+ warn R18n.t.fronde.org.generate_blog_index(name: @project['name'])
end
- line += " \\\\\n #{article.excerpt}" if article.excerpt != ''
- line
- end
-
- def org_title(year, html_class = 'index-year')
- year = R18n.t.fronde.index.unsorted if year == '0000'
- <<~ENDPROP
- * #{year}
- :PROPERTIES:
- :HTML_CONTAINER_CLASS: #{html_class}
- :UNNUMBERED: notoc
- :END:
- ENDPROP
+ File.write(orgdest, blog_home_page)
end
end
end
diff --git a/lib/fronde/org.rb b/lib/fronde/org.rb
new file mode 100644
index 0000000..819d284
--- /dev/null
+++ b/lib/fronde/org.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+module Fronde
+ # Everything related to Org mode
+ #
+ # The module itself wraps code necessary to download the last version
+ # of the Emacs package. It also serves as a namespace for the class
+ # responsible for handling Org files: {Fronde::Org::File}.
+ module Org
+ class << self
+ def current_version
+ # Do not crash if Org is not yet installed (and thus return nil)
+ Dir['lib/org-*'].first&.delete_prefix('lib/org-')
+ end
+
+ # Fetch and return the last published version of Org.
+ #
+ # To be nice with Org servers, this method will keep the fetched
+ # version number in a cache file. You can bypass it by using the
+ # force parameter.
+ #
+ # @param force [Boolean] Whether we should first remove the guard
+ # file if it exists
+ # @param destination [String] Where to store the cookie file to
+ # remember the last version number
+ # @return [String] the new x.x.x version string of Org
+ def last_version(force: false, cookie_dir: 'var/tmp')
+ cookie = "#{cookie_dir}/last_org_version"
+ return ::File.read cookie if !force && ::File.exist?(cookie)
+
+ org_version = fetch_version_number
+ raise 'No remote Org version found' unless org_version
+
+ FileUtils.mkdir_p cookie_dir
+ ::File.write cookie, org_version
+ org_version
+ end
+
+ def fetch_version_number
+ # Retrieve last org version from git repository tags page.
+ tag_rx = Regexp.new(
+ '<a href=\'/cgit/emacs/org-mode.git/tag/\?h=' \
+ '(?<tag>release_(?<number>[^\']+))\'>\k<tag></a>'
+ )
+ versions = URI(
+ 'https://git.savannah.gnu.org/cgit/emacs/org-mode.git/refs/'
+ ).open.readlines.map do |line|
+ line.match(tag_rx) { |matchdata| matchdata[:number] }
+ end
+ versions.compact.first
+ end
+
+ # Download latest org-mode tarball.
+ #
+ # @param destination [String] where to save the org-mode tarball
+ # @return [String] the downloaded org-mode version
+ def download(destination = 'var/tmp')
+ # Remove version number in dest file to allow easy rake file
+ # task naming
+ dest_file = ::File.expand_path('org.tar.gz', destination)
+ org_last_version = last_version(force: false, cookie_dir: destination)
+ tarball = "org-mode-release_#{org_last_version}.tar.gz"
+ uri = URI("https://git.savannah.gnu.org/cgit/emacs/org-mode.git/snapshot/#{tarball}")
+ # Will crash on purpose if anything goes wrong
+ Net::HTTP.start(uri.host) do |http|
+ request = Net::HTTP::Get.new uri
+
+ http.request request do |response|
+ ::File.open(dest_file, 'w') do |io|
+ response.read_body { |chunk| io.write chunk }
+ end
+ end
+ end
+ org_last_version
+ end
+
+ def make_org_cmd(org_dir, target, verbose: false)
+ make = ['make', '-C', org_dir, target]
+ return make.join(' ') if verbose
+
+ make.insert(3, '-s')
+ make << 'EMACSQ="emacs -Q --eval \'(setq inhibit-message t)\'"'
+ make.join(' ')
+ end
+
+ # Compile downloaded Org package
+ #
+ # @param source [String] path to the org-mode tarball to install
+ # @param version [String] version of the org package to install
+ # @param target [String] path to the final install directory
+ # @param verbose [Boolean] whether the process should be verbose
+ def compile(source, version, target, verbose: false)
+ untar_cmd = ['tar', '-xzf', source]
+ system(*untar_cmd)
+ FileUtils.mv "org-mode-release_#{version}", target
+ # Fix a weird unknown package version
+ ::File.write("#{target}/mk/version.mk", "ORGVERSION ?= #{version}")
+ system(*make_org_cmd(target, 'compile', verbose: verbose))
+ system(*make_org_cmd(target, 'autoloads', verbose: verbose))
+ end
+ end
+ end
+end
diff --git a/lib/fronde/org/file.rb b/lib/fronde/org/file.rb
new file mode 100644
index 0000000..081a9d3
--- /dev/null
+++ b/lib/fronde/org/file.rb
@@ -0,0 +1,287 @@
+# frozen_string_literal: true
+
+require 'time'
+require_relative '../../ext/nil_time'
+require_relative '../../ext/time'
+using TimePatch
+
+require 'nokogiri'
+require 'fileutils'
+
+require_relative '../config'
+require_relative '../version'
+require_relative '../slug'
+require_relative 'file_extracter'
+
+module Fronde
+ module Org
+ # Handles org files.
+ #
+ # This class is responsible for reading or writing existing or new
+ # org files, and formating their content to be used on the generated
+ # website.
+ class File
+ # @return [String] the relative path to the source of this
+ # document.
+ attr_reader :file
+
+ # @return [Hash] the project owning this document.
+ attr_reader :project
+
+ include FileExtracter
+
+ # Prepares the file named by ~file_name~ for read and write
+ # operations.
+ #
+ # If the file ~file_name~ does not exist, the new instance may be
+ # populated by data given in the ~opts~ parameter.
+ #
+ # @example
+ # File.exist? './test.org'
+ # => true
+ # o = Fronde::Org::File.new('./test.org')
+ # => #<Fronde::Org::File @file='./test.org'...>
+ # o.title
+ # => "This is an existing test file"
+ # File.exist? '/tmp/does_not_exist.org'
+ # => false
+ # o = Fronde::Org::File.new('/tmp/does_not_exist.org')
+ # => #<Fronde::Org::File @file='/tmp/does_not_exist.org'...>
+ # o.title
+ # => ""
+ # File.exist? '/tmp/other.org'
+ # => false
+ # o = Fronde::Org::File.new('/tmp/other.org', title: 'New file')
+ # => #<Fronde::Org::File @file='/tmp/other.org'...>
+ # o.title
+ # => "New file"
+ #
+ # @param file_name [String] path to the corresponding Org file
+ # @param opts [Hash] optional data to initialize new Org file
+ # @option opts [String] title ('') the title of the new Org file
+ # @option opts [String] author (system user or '') the author of
+ # the document
+ # @return [Fronde::Org::File] the new instance of
+ # Fronde::Org::File
+ def initialize(file_name, opts = {})
+ file_name ||= ''
+ @file = ::File.expand_path file_name
+ @options = opts
+ @project = find_source
+ @data = {}
+ if ::File.file?(@file)
+ extract_data
+ else
+ init_empty_file
+ end
+ end
+
+ # Returns a String representation of the document date, which aims
+ # to be used to sort several Org::Files.
+ #
+ # The format used for the key is ~%Y%m%d%H%M%S~. If the current
+ # Org::File instance does not have a date, this mehod return
+ # ~00000000000000~. If the current Org::File instance does not
+ # have time information, the date is padded with zeros.
+ #
+ # @example with the org header ~#+date: <2019-07-03 Wed 20:52:49>~
+ # org_file.date
+ # => #<Time: 2019-07-03T20:52:49+02:00...>
+ # org_file.timekey
+ # => "20190703205349"
+ #
+ # @example with the org header ~#+date: <2019-07-03 Wed>~
+ # org_file.date
+ # => #<Time: 2019-07-03T00:00:00+02:00...>
+ # org_file.timekey
+ # => "20190703000000"
+ #
+ # @example with no date header in the org file
+ # org_file.date
+ # => nil
+ # org_file.timekey
+ # => "00000000000000"
+ #
+ # @return [String] the document key
+ def timekey
+ return '00000000000000' if @data[:date].is_a? NilTime
+
+ @data[:date].strftime('%Y%m%d%H%M%S')
+ end
+
+ # Formats given ~string~ with values of the current Org::File.
+ #
+ # This method expects to find percent-tags in the given ~string~
+ # and replace them by their corresponding value.
+ #
+ # It reuses the same tags than the ~org-html-format-spec~ method.
+ #
+ # *** Format:
+ #
+ # - %a :: the raw author name
+ # - %A :: the HTML rendering of the author name, equivalent to
+ # ~<span class="author">%a</span>~
+ # - %d :: the ~:short~ date HTML representation, equivalent
+ # to ~<time datetime="%I">%i</time>~
+ # - %D :: the ~:full~ date and time HTML representation
+ # - %i :: the raw ~:short~ date and time
+ # - %I :: the raw ~:iso8601~ date and time
+ # - %k :: the keywords separated by a comma
+ # - %K :: the HTML list rendering of the keywords
+ # - %l :: the lang of the document
+ # - %L :: the license information, taken from the
+ # {Fronde::Config#settings}
+ # - %n :: the Fronde name and version
+ # - %N :: the Fronde name and version with a link to the project
+ # home on the name
+ # - %s :: the subtitle of the document
+ # - %t :: the title of the document
+ # - %u :: the URL to the related published HTML document
+ # - %x :: the raw description (eXcerpt)
+ # - %X :: the description, enclosed in an HTML ~p~ tag, equivalent
+ # to ~<p>%x</p>~
+ #
+ # @example
+ # org_file.format("Article written by %a the %d")
+ # => "Article written by Alice Smith the Wednesday 3rd July"
+ #
+ # @return [String] the given ~string~ after replacement occurs
+ # rubocop:disable Metrics/MethodLength
+ # rubocop:disable Layout/LineLength
+ def format(string)
+ string.gsub('%a', @data[:author])
+ .gsub('%A', "<span class=\"author\">#{@data[:author]}</span>")
+ .gsub('%d', @data[:date].l18n_short_date_html)
+ .gsub('%D', @data[:date].l18n_long_date_html)
+ .gsub('%i', @data[:date].l18n_short_date_string)
+ .gsub('%I', @data[:date].xmlschema)
+ .gsub('%k', @data[:keywords].join(', '))
+ .gsub('%K', keywords_to_html)
+ .gsub('%l', @data[:lang])
+ .gsub('%L', Fronde::CONFIG.get('license', '').gsub(/\s+/, ' ').strip)
+ .gsub('%n', "Fronde #{Fronde::VERSION}")
+ .gsub('%N', "<a href=\"https://git.umaneti.net/fronde/about/\">Fronde</a> #{Fronde::VERSION}")
+ .gsub('%s', @data[:subtitle])
+ .gsub('%t', @data[:title])
+ .gsub('%u', @data[:url] || '')
+ .gsub('%x', @data[:excerpt])
+ .gsub('%X', "<p>#{@data[:excerpt]}</p>")
+ end
+ # rubocop:enable Layout/LineLength
+ # rubocop:enable Metrics/MethodLength
+
+ # Writes the current Org::File content to the underlying file.
+ #
+ # The intermediate parent folders are created if necessary.
+ #
+ # @return [Integer] the length written (as returned by the
+ # underlying ~File.write~ method call)
+ def write
+ if ::File.directory? @file
+ if @data[:title] == ''
+ raise R18n.t.fronde.error.org_file.no_file_or_title
+ end
+
+ @file = ::File.join @file, "#{Slug.slug(@data[:title])}.org"
+ else
+ file_dir = ::File.dirname @file
+ FileUtils.mkdir_p file_dir
+ end
+ ::File.write @file, @data[:content]
+ end
+
+ def method_missing(method_name, *args, &block)
+ reader_method = method_name.to_s.delete_suffix('=').to_sym
+ if @data.has_key? reader_method
+ return @data[reader_method] if reader_method == method_name
+
+ return @data[reader_method] = args.first
+ end
+ super
+ end
+
+ def respond_to_missing?(method_name, include_private = false)
+ return true if @data.has_key? method_name
+
+ reader_method = method_name.to_s.delete_suffix('=').to_sym
+ return true if @data.has_key? reader_method
+
+ super
+ end
+
+ def to_h
+ fields = %w[author excerpt keywords timekey title url]
+ data = fields.to_h { |key| [key, send(key)] }
+ data['published_body'] = extract_published_body
+ pub_date = @data[:date]
+ data['published'] = pub_date.l18n_long_date_string(with_year: false)
+ data['published_xml'] = pub_date.xmlschema
+ data['updated_xml'] = @data[:updated]&.xmlschema
+ data
+ end
+
+ private
+
+ def find_source
+ if ::File.extname(@file) == '.org'
+ source = find_source_for_org_file
+ else
+ source = find_source_for_publication_file
+ end
+ warn R18n.t.fronde.error.org_file.no_project(file: @file) unless source
+ source
+ end
+
+ def find_source_for_org_file
+ Fronde::CONFIG.sources.find do |project|
+ project.source_for? @file
+ end
+ end
+
+ def find_source_for_publication_file
+ Fronde::CONFIG.sources.find do |project|
+ org_file = project.source_for @file
+ next unless org_file
+
+ @file = org_file
+ end
+ end
+
+ def init_empty_file
+ @data = {
+ title: @options[:title] || '', subtitle: '', excerpt: '',
+ date: Time.now,
+ author: @options[:author] || Fronde::CONFIG.get('author'),
+ keywords: [],
+ lang: @options[:lang] || Fronde::CONFIG.get('lang'),
+ pub_file: nil, url: nil
+ }
+ body = @options[:content] || ''
+ @data[:content] = @options[:raw_content] || <<~ORG
+ #+title: #{@data[:title]}
+ #+date: <#{@data[:date].strftime('%Y-%m-%d %a. %H:%M:%S')}>
+ #+author: #{@data[:author]}
+ #+language: #{@data[:lang]}
+
+ #{body}
+ ORG
+ end
+
+ # Format {Fronde::Org::File#keywords} list in an HTML listing.
+ #
+ # @return [String] the HTML keywords list
+ def keywords_to_html
+ domain = Fronde::CONFIG.get('domain')
+ # Allow a nil project, mainly for tests purpose. Should never
+ # happen in reality
+ pub_path = @project&.public_absolute_path || '/'
+ klist = @data[:keywords].map do |k|
+ %(<li class="keyword">
+ <a href="#{domain}#{pub_path}tags/#{Slug.slug(k)}.html">#{k}</a>
+ </li>)
+ end.join
+ %(<ul class="keywords-list">#{klist}</ul>)
+ end
+ end
+ end
+end
diff --git a/lib/fronde/org/file_extracter.rb b/lib/fronde/org/file_extracter.rb
new file mode 100644
index 0000000..3a320ad
--- /dev/null
+++ b/lib/fronde/org/file_extracter.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+using TimePatch
+
+module Fronde
+ module Org
+ # This module holds extracter methods for the {Fronde::Org::File}
+ # class.
+ module FileExtracter
+ private
+
+ # Main method, which will call the other to initialize an
+ # {Fronde::Org::File} instance.
+ def extract_data
+ @data = { content: ::File.read(@file), pub_file: nil, url: nil }
+ %i[title subtitle date author keywords lang excerpt].each do |param|
+ @data[param] = send("extract_#{param}".to_sym)
+ end
+ return unless @project
+
+ @data[:updated] = ::File.mtime(@file)
+ @data[:pub_file] = @project.target_for @file
+ @data[:url] = Fronde::CONFIG.get('domain') + @data[:pub_file]
+ end
+
+ def extract_date
+ timerx = '([0-9:]{5})(?::([0-9]{2}))?'
+ daterx = /^#\+date: *<([0-9-]{10}) [\w.]+(?: #{timerx})?> *$/i
+ match = daterx.match(@data[:content])
+ return NilTime.new if match.nil?
+
+ notime = match[2].nil?
+ if notime
+ time = '00:00:00'
+ else
+ time = "#{match[2]}:#{match[3] || '00'}"
+ end
+ date = Time.strptime("#{match[1]} #{time}", '%Y-%m-%d %H:%M:%S')
+ date.no_time = notime
+ date
+ end
+
+ def extract_title
+ match = /^#\+title:(.+)$/i.match(@data[:content])
+ if match.nil?
+ # Avoid to leak absolute path
+ project_relative_path = @file.sub(/^#{Dir.pwd}\//, '')
+ return project_relative_path
+ end
+ match[1].strip
+ end
+
+ def extract_subtitle
+ match = /^#\+subtitle:(.+)$/i.match(@data[:content])
+ (match && match[1].strip) || ''
+ end
+
+ def extract_author
+ match = /^#\+author:(.+)$/i.match(@data[:content])
+ (match && match[1].strip) || Fronde::CONFIG.get('author')
+ end
+
+ def extract_keywords
+ match = /^#\+keywords:(.+)$/i.match(@data[:content])
+ (match && match[1].split(',').map(&:strip)) || []
+ end
+
+ def extract_lang
+ match = /^#\+language:(.+)$/i.match(@data[:content])
+ (match && match[1].strip) || Fronde::CONFIG.get('lang')
+ end
+
+ def extract_excerpt
+ @data[:content].scan(/^#\+description:(.+)$/i).map do |line|
+ line.first.strip
+ end.join(' ')
+ end
+
+ def extract_published_body
+ pub_file = @data[:pub_file]
+ # Always return something, even when not published yet
+ return @data[:excerpt] unless pub_file && @project
+
+ project_type = @project['type']
+ pub_folder = Fronde::CONFIG.get("#{project_type}_public_folder")
+ file_name = pub_folder + pub_file
+ return @data[:excerpt] unless ::File.exist? file_name
+
+ return ::File.read(file_name) if project_type == 'gemini'
+
+ dom = ::File.open(file_name, 'r') { |file| Nokogiri::HTML file }
+ body = dom.css('div#content')
+ body.css('header').unlink # Remove the main title
+ body.to_s
+ end
+ end
+ end
+end
diff --git a/lib/fronde/org_file.rb b/lib/fronde/org_file.rb
deleted file mode 100644
index 23a7d46..0000000
--- a/lib/fronde/org_file.rb
+++ /dev/null
@@ -1,298 +0,0 @@
-# frozen_string_literal: true
-
-require 'time'
-require 'fileutils'
-# fronde/config is required by htmlizer
-require 'fronde/org_file/htmlizer'
-require 'fronde/org_file/extracter'
-require 'fronde/org_file/class_methods'
-require 'fronde/index'
-require 'fronde/version'
-
-module Fronde
- # Handles org files.
- #
- # This class is responsible for reading or writing existing or new org
- # files, and formating their content to be used on the generated
- # website.
- class OrgFile # rubocop:disable Metrics/ClassLength
- # @return [String] the title of the current org document, taken from
- # the ~#+title:~ header.
- attr_reader :title
-
- # @return [String] the subtitle of the current org document, taken
- # from the ~#+subtitle:~ header.
- attr_reader :subtitle
-
- # @return [DateTime] the date and time of the current org document,
- # taken from the ~#+date:~ header.
- attr_reader :date
-
- # @return [Boolean] wether a time has been extracted from the
- # current org document ~#+date:~ header.
- attr_reader :notime
-
- # The author of the current org document, taken from the ~#+author:~
- # header.
- #
- # If the current document doesn't have any authorship information,
- # the one from the ~config.yml~ file will be used instead
- #
- # @return [String] the author name
- attr_reader :author
-
- # @return [Array] the keywords list of the current org document,
- # taken from the ~#+keywords:~ header.
- attr_reader :keywords
-
- # @return [String] the description of this org document, taken from
- # the ~#+description:~ header.
- attr_reader :excerpt
-
- # The locale of the current org document, taken from the
- # ~#+language:~ header.
- #
- # If the current document doesn't have any language information, the
- # one from the ~config.yml~ file will be used instead, or "en" by
- # default.
- #
- # @return [String] the document lang
- attr_reader :lang
-
- # @return [String] the relative path to the source of this document.
- attr_reader :file
-
- # @return [String] the relative path to the generated html file of
- # this document.
- attr_reader :html_file
-
- # @return [String] the url of this document, build from the ~domain~
- # settings and the above {#html_file @html_file} attribute.
- attr_reader :url
-
- # @return [String] the project owning this document.
- attr_reader :project
-
- extend Fronde::OrgFileClassMethods
-
- include Fronde::OrgFileExtracter
- include Fronde::OrgFileHtmlizer
-
- # Prepares the file named by ~file_name~ for read and write
- # operations.
- #
- # If the file ~file_name~ does not exist, the new instance may be
- # populated by data given in the ~opts~ parameter.
- #
- # @example
- # File.exist? './test.org'
- # => true
- # o = Fronde::OrgFile.new('./test.org')
- # => #<Fronde::OrgFile @file='./test.org'...>
- # o.title
- # => "This is an existing test file"
- # File.exist? '/tmp/does_not_exist.org'
- # => false
- # o = Fronde::OrgFile.new('/tmp/does_not_exist.org')
- # => #<Fronde::OrgFile @file='/tmp/does_not_exist.org'...>
- # o.title
- # => ""
- # File.exist? '/tmp/other.org'
- # => false
- # o = Fronde::OrgFile.new('/tmp/other.org', title: 'New file')
- # => #<Fronde::OrgFile @file='/tmp/other.org'...>
- # o.title
- # => "New file"
- #
- # @param file_name [String] path to the corresponding Org file
- # @param opts [Hash] optional data to initialize new Org file
- # @option opts [String] title ('') the title of the new Org file
- # @option opts [String] author (system user or '') the author of the
- # document
- # @option opts [String] project the project owning this file
- # must be stored
- # @return [Fronde::OrgFile] the new instance of Fronde::OrgFile
- def initialize(file_name, opts = {})
- file_name = nil if file_name == ''
- @file = file_name
- @html_file = nil
- @url = nil
- @project = opts.delete :project
- @options = opts
- build_html_file_and_url
- if @file && File.exist?(@file)
- extract_data
- else
- init_empty_file
- end
- end
-
- # Returns a String representation of the document date, which aims
- # to be used to sort several OrgFiles.
- #
- # The format used for the key is ~%Y%m%d%H%M%S~. If the current
- # OrgFile instance does not have a date, this mehod return
- # ~00000000000000~. If the current OrgFile instance does not have
- # time information, the date is padded with zeros.
- #
- # @example with the org header ~#+date: <2019-07-03 Wed 20:52:49>~
- # org_file.date
- # => #<DateTime: 2019-07-03T20:52:49+02:00...>
- # org_file.timekey
- # => "20190703205349"
- #
- # @example with the org header ~#+date: <2019-07-03 Wed>~
- # org_file.date
- # => #<DateTime: 2019-07-03T00:00:00+02:00...>
- # org_file.timekey
- # => "20190703000000"
- #
- # @example with no date header in the org file
- # org_file.date
- # => nil
- # org_file.timekey
- # => "00000000000000"
- #
- # @return [String] the document key
- def timekey
- return '00000000000000' if @date.nil?
- @date.strftime('%Y%m%d%H%M%S')
- end
-
- # Returns the current OrgFile instance DateTime as a String.
- #
- # This method accepts three values for the ~dateformat~ parameter:
- #
- # - ~:full~ (or ~:long~) :: outputs a complete date and time
- # representation, localized through R18n;
- # - ~:short~ :: outputs a short date representation (without time),
- # localized with R18n;
- # - ~:rfc3339~ :: outputs the RFC 3339 date and time representation,
- # used in atom feed.
- #
- # @param dateformat [Symbol] the format to use to convert DateTime
- # into String
- # @param year [Boolean] wether or not the ~:full~ format must
- # contain the year
- # @return [String] the document DateTime string representation
- def datestring(dateformat = :full, year: true)
- return '' if @date.nil?
- return R18n.l @date.to_date if dateformat == :short
- return @date.rfc3339 if dateformat == :rfc3339
- locale = R18n.get.locale
- long_fmt = R18n.t.fronde.index.full_date_format(
- date: locale.format_date_full(@date, year: year)
- )
- unless @notime
- long_fmt = R18n.t.fronde.index.full_date_with_time_format(
- date: long_fmt, time: locale.time_format.delete('_').strip
- )
- end
- locale.strftime(@date, long_fmt)
- end
-
- # Formats given ~string~ with values of the current OrgFile.
- #
- # This method expects to find percent-tags in the given ~string~ and
- # replace them by their corresponding value.
- #
- # It reuses the same tags than the ~org-html-format-spec~ method.
- #
- # *** Format:
- #
- # - %a :: the raw author name
- # - %A :: the HTML rendering of the author name, equivalent to
- # ~<span class="author">%a</span>~
- # - %d :: the ~:short~ date HTML representation, equivalent
- # to ~<time datetime="%I">%i</time>~
- # - %D :: the ~:full~ date and time HTML representation
- # - %i :: the raw ~:short~ date and time
- # - %I :: the raw ~:rfc3339~ date and time
- # - %k :: the keywords separated by a comma
- # - %K :: the HTML list rendering of the keywords
- # - %l :: the lang of the document
- # - %L :: the license information, taken from the
- # {Fronde::Config#settings}
- # - %n :: the Fronde name and version
- # - %N :: the Fronde name and version with a link to the project
- # home on the name
- # - %s :: the subtitle of the document
- # - %t :: the title of the document
- # - %u :: the web path to the related published HTML document
- # - %x :: the raw description (eXcerpt)
- # - %X :: the description, enclosed in an HTML ~p~ tag, equivalent
- # to ~<p>%x</p>~
- #
- # @example
- # org_file.format("Article written by %a the %d")
- # => "Article written by Alice Smith the Wednesday 3rd July"
- #
- # @return [String] the given ~string~ after replacement occurs
- # rubocop:disable Metrics/MethodLength
- # rubocop:disable Layout/LineLength
- def format(string)
- string.gsub('%a', @author)
- .gsub('%A', author_to_html)
- .gsub('%d', date_to_html(:short))
- .gsub('%D', date_to_html)
- .gsub('%i', datestring(:short))
- .gsub('%I', datestring(:rfc3339))
- .gsub('%k', @keywords.join(', '))
- .gsub('%K', keywords_to_html)
- .gsub('%l', @lang)
- .gsub('%L', Fronde::Config.get('license', '').gsub(/\s+/, ' ').strip)
- .gsub('%n', "Fronde #{Fronde::VERSION}")
- .gsub('%N', "<a href=\"https://git.umaneti.net/fronde/about/\">Fronde</a> #{Fronde::VERSION}")
- .gsub('%s', @subtitle)
- .gsub('%t', @title)
- .gsub('%u', @html_file || '')
- .gsub('%x', @excerpt)
- .gsub('%X', "<p>#{@excerpt}</p>")
- end
- # rubocop:enable Layout/LineLength
- # rubocop:enable Metrics/MethodLength
-
- # Writes the current OrgFile content to the underlying file.
- #
- # The intermediate parent folders are created if necessary.
- #
- # @return [Integer] the length written (as returned by the
- # underlying ~File.write~ method call)
- def write
- raise TypeError, 'no conversion from nil file name to path.' if @file.nil?
- file_dir = File.dirname @file
- FileUtils.mkdir_p file_dir unless Dir.exist? file_dir
- File.write @file, @content
- end
-
- private
-
- def build_html_file_and_url
- return if @file.nil?
- @html_file = Fronde::OrgFile.target_for_source(
- @file, @project, with_public_folder: false
- )
- @url = "#{Fronde::Config.get('domain')}/#{@html_file}"
- end
-
- def init_empty_file
- @title = @options[:title] || ''
- @subtitle = ''
- @date = DateTime.now
- @notime = false
- @author = @options[:author] || Fronde::Config.get('author')
- @keywords = []
- @lang = @options[:lang] || Fronde::Config.get('lang')
- @excerpt = ''
- body = @options[:content] || ''
- @content = @options[:raw_content] || <<~ORG
- #+title: #{@title}
- #+date: <#{@date.strftime('%Y-%m-%d %a. %H:%M:%S')}>
- #+author: #{@author}
- #+language: #{@lang}
-
- #{body}
- ORG
- end
- end
-end
diff --git a/lib/fronde/org_file/class_methods.rb b/lib/fronde/org_file/class_methods.rb
deleted file mode 100644
index f62b734..0000000
--- a/lib/fronde/org_file/class_methods.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-module Fronde
- # This module holds class methods for the {Fronde::OrgFile} class.
- module OrgFileClassMethods
- def source_for_target(file_name)
- # file_name may be frozen...
- src = file_name.sub(/\.html\z/, '.org')
- pubfolder = Fronde::Config.get('public_folder')
- src.sub!(/^#{pubfolder}\//, '')
- # Look for match in each possible sources. The first found wins.
- Fronde::Config.sources.each do |project|
- if project['target'] == '.'
- origin = File.join(project['path'], src)
- else
- origin = File.join(
- project['path'], src.sub(/^#{project['target']}\//, '')
- )
- end
- return origin if File.exist?(origin)
- end
- nil
- end
-
- def target_for_source(file_name, project, with_public_folder: true)
- return nil if file_name.nil?
- # file_name may be frozen...
- target = file_name.sub(/\.org\z/, '.html').sub(/^#{Dir.pwd}\//, '')
- if project.nil?
- subfolder = File.basename(File.dirname(target))
- target = File.basename(target)
- target = "#{subfolder}/#{target}" if subfolder != '.'
- else
- project_relative_path = project['path'].sub(/^#{Dir.pwd}\//, '')
- target.sub!(/^#{project_relative_path}\//, '')
- target = "#{project['target']}/#{target}" if project['target'] != '.'
- end
- return target unless with_public_folder
- pubfolder = Fronde::Config.get('public_folder')
- "#{pubfolder}/#{target}"
- end
-
- def project_for_source(file_name)
- # Look for match in each possible sources. The first found wins.
- Fronde::Config.sources.each do |project|
- project_relative_path = project['path'].sub(/^#{Dir.pwd}\//, '')
- return project if file_name.match?(/^#{project_relative_path}\//)
- end
- nil
- end
-
- def slug(title)
- title.downcase.tr(' ', '-')
- .encode('ascii', fallback: ->(k) { translit(k) })
- .gsub(/[^\w-]/, '').delete_suffix('-')
- end
-
- private
-
- def translit(char)
- return 'a' if ['á', 'à', 'â', 'ä', 'ǎ', 'ã', 'å'].include?(char)
- return 'e' if ['é', 'è', 'ê', 'ë', 'ě', 'ẽ'].include?(char)
- return 'i' if ['í', 'ì', 'î', 'ï', 'ǐ', 'ĩ'].include?(char)
- return 'o' if ['ó', 'ò', 'ô', 'ö', 'ǒ', 'õ'].include?(char)
- return 'u' if ['ú', 'ù', 'û', 'ü', 'ǔ', 'ũ'].include?(char)
- return 'y' if ['ý', 'ỳ', 'ŷ', 'ÿ', 'ỹ'].include?(char)
- return 'c' if char == 'ç'
- return 'n' if char == 'ñ'
- '-'
- end
- end
-end
diff --git a/lib/fronde/org_file/extracter.rb b/lib/fronde/org_file/extracter.rb
deleted file mode 100644
index 7f7b595..0000000
--- a/lib/fronde/org_file/extracter.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-module Fronde
- # This module holds extracter methods for the {Fronde::OrgFile} class.
- module OrgFileExtracter
- private
-
- # Main method, which will call the other to initialize an
- # {Fronde::OrgFile} instance.
- def extract_data
- @content = File.read @file
- @title = extract_title
- @subtitle = extract_subtitle
- @date = extract_date
- @author = extract_author
- @keywords = extract_keywords
- @lang = extract_lang
- @excerpt = extract_excerpt
- end
-
- def extract_date
- timerx = '([0-9:]{5})(?::([0-9]{2}))?'
- m = /^#\+date: *<([0-9-]{10}) [\w.]+(?: #{timerx})?> *$/i.match(@content)
- return nil if m.nil?
- @notime = m[2].nil?
- if @notime
- time = '00:00:00'
- else
- time = "#{m[2]}:#{m[3] || '00'}"
- end
- DateTime.strptime("#{m[1]} #{time}", '%Y-%m-%d %H:%M:%S')
- end
-
- def extract_title
- m = /^#\+title:(.+)$/i.match(@content)
- if m.nil?
- # Avoid to leak absolute path
- project_relative_path = @file.sub(/^#{Dir.pwd}\//, '')
- return project_relative_path
- end
- m[1].strip
- end
-
- def extract_subtitle
- m = /^#\+subtitle:(.+)$/i.match(@content)
- return '' if m.nil?
- m[1].strip
- end
-
- def extract_author
- m = /^#\+author:(.+)$/i.match(@content)
- return Fronde::Config.get('author') if m.nil?
- m[1].strip
- end
-
- def extract_keywords
- m = /^#\+keywords:(.+)$/i.match(@content)
- return [] if m.nil?
- m[1].split(',').map(&:strip)
- end
-
- def extract_lang
- m = /^#\+language:(.+)$/i.match(@content)
- return Fronde::Config.get('lang') if m.nil?
- m[1].strip
- end
-
- def extract_excerpt
- @content.scan(/^#\+description:(.+)$/i).map { |l| l[0].strip }.join(' ')
- end
- end
-end
diff --git a/lib/fronde/org_file/htmlizer.rb b/lib/fronde/org_file/htmlizer.rb
deleted file mode 100644
index 296699e..0000000
--- a/lib/fronde/org_file/htmlizer.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'fronde/config'
-require 'fronde/emacs'
-
-module Fronde
- # This module holds HTML formatter methods for the {Fronde::OrgFile}
- # class.
- module OrgFileHtmlizer
- private
-
- # Format {Fronde::OrgFile#keywords} list in an HTML listing.
- #
- # @return [String] the HTML keywords list
- def keywords_to_html
- domain = Fronde::Config.get('domain')
- klist = @keywords.map do |k|
- <<~KEYWORDLINK
- <li class="keyword">
- <a href="#{domain}/tags/#{Fronde::OrgFile.slug(k)}.html">#{k}</a>
- </li>
- KEYWORDLINK
- end.join
- "<ul class=\"keywords-list\">#{klist}</ul>"
- end
-
- # Format {Fronde::OrgFile#date} as a HTML `time` tag.
- #
- # @return [String] the HTML `time` tag
- def date_to_html(dateformat = :full)
- return '<time></time>' if @date.nil?
- "<time datetime=\"#{@date.rfc3339}\">#{datestring(dateformat)}</time>"
- end
-
- # Format {Fronde::OrgFile#author} in a HTML `span` tag with a
- # specific class.
- #
- # @return [String] the author HTML `span`
- def author_to_html
- "<span class=\"author\">#{@author}</span>"
- end
- end
-end
diff --git a/lib/fronde/preview.rb b/lib/fronde/preview.rb
index 4a1595d..c5cd484 100644
--- a/lib/fronde/preview.rb
+++ b/lib/fronde/preview.rb
@@ -1,54 +1,58 @@
# frozen_string_literal: true
require 'webrick'
-require 'fronde/config'
-
-module Fronde # rubocop:disable Style/Documentation
- # A tiny preview server, which main goal is to replace references to
- # the target domain by localhost.
- class PreviewServlet < WEBrick::HTTPServlet::AbstractServlet
- include WEBrick::HTTPUtils
-
- def do_GET(request, response) # rubocop:disable Naming/MethodName
- file = local_path(request.path)
- response.body = parse_body(file, "http://#{request.host}:#{request.port}")
- response.status = 200
- response.content_type = mime_type(file, DefaultMimeTypes)
- end
+require_relative 'config'
- private
+module Fronde
+ module Preview # rubocop:disable Style/Documentation
+ # A tiny preview server, which main goal is to replace references to
+ # the target domain by localhost.
+ class Servlet < WEBrick::HTTPServlet::AbstractServlet
+ include WEBrick::HTTPUtils
- def local_path(requested_path)
- routes = Fronde::Config.get(['preview', 'routes'], {})
- return routes[requested_path] if routes.has_key? requested_path
- local_path = Fronde::Config.get('public_folder') + requested_path
- if File.directory? local_path
- local_path = format(
- '%<path>s/index.html', path: local_path.delete_suffix('/')
- )
+ def do_GET(request, response) # rubocop:disable Naming/MethodName
+ file = local_path(request.path)
+ response.body = parse_body(file, "http://#{request.host}:#{request.port}")
+ response.status = 200
+ response.content_type = mime_type(file, DefaultMimeTypes)
end
- return local_path if File.exist? local_path
- raise WEBrick::HTTPStatus::NotFound, 'Not found.'
- end
- def parse_body(local_path, local_host)
- body = File.read local_path
- return body unless local_path.match?(/\.(?:ht|x)ml\z/)
- domain = Fronde::Config.get('domain')
- return body if domain == ''
- body.gsub(/"file:\/\//, format('"%<host>s', host: local_host))
- .gsub(/"#{domain}/, format('"%<host>s', host: local_host))
+ private
+
+ def local_path(requested_path)
+ routes = Fronde::CONFIG.get(%w[preview routes], {})
+ return routes[requested_path] if routes.has_key? requested_path
+
+ local_path = Fronde::CONFIG.get('html_public_folder') + requested_path
+ if File.directory? local_path
+ local_path = format(
+ '%<path>s/index.html', path: local_path.delete_suffix('/')
+ )
+ end
+ return local_path if File.exist? local_path
+
+ raise WEBrick::HTTPStatus::NotFound, 'Not found.'
+ end
+
+ def parse_body(local_path, local_host)
+ body = File.read local_path
+ return body unless local_path.match?(/\.(?:ht|x)ml\z/)
+
+ domain = Fronde::CONFIG.get('domain')
+ return body if domain == ''
+
+ host_repl = %("#{local_host})
+ body.gsub('"file://', host_repl).gsub(%("#{domain}), host_repl)
+ end
end
- end
- class << self
- def start_preview
+ def self.start
# Inspired by ruby un.rb library, which allows normally to start a
# webrick server in one line: ruby -run -e httpd public_html -p 5000
- port = Fronde::Config.get(['preview', 'server_port'], 5000)
+ port = Fronde::CONFIG.get(%w[preview server_port], 5000)
s = WEBrick::HTTPServer.new(Port: port)
- s.mount '/', Fronde::PreviewServlet
- ['TERM', 'QUIT', 'INT'].each { |sig| trap(sig, proc { s.shutdown }) }
+ s.mount '/', Servlet
+ %w[TERM QUIT INT].each { |sig| trap(sig, proc { s.shutdown }) }
s.start
end
end
diff --git a/lib/fronde/slug.rb b/lib/fronde/slug.rb
new file mode 100644
index 0000000..101ed5c
--- /dev/null
+++ b/lib/fronde/slug.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Fronde
+ # Contains method to generate URL compatible strings
+ module Slug
+ class << self
+ def slug(title)
+ title.downcase.tr(' ', '-')
+ .encode('ascii', fallback: ->(k) { translit(k) })
+ .gsub(/[^\w-]/, '').delete_suffix('-')
+ end
+
+ def translit(char)
+ return 'a' if %w[á à â ä ǎ ã å].include?(char)
+ return 'e' if %w[é è ê ë ě ẽ].include?(char)
+ return 'i' if %w[í ì î ï ǐ ĩ].include?(char)
+ return 'o' if %w[ó ò ô ö ǒ õ].include?(char)
+ return 'u' if %w[ú ù û ü ǔ ũ].include?(char)
+ return 'y' if %w[ý ỳ ŷ ÿ ỹ].include?(char)
+ return 'c' if char == 'ç'
+ return 'n' if char == 'ñ'
+
+ '-'
+ end
+ end
+ end
+end
diff --git a/lib/fronde/source.rb b/lib/fronde/source.rb
new file mode 100644
index 0000000..e0c6f85
--- /dev/null
+++ b/lib/fronde/source.rb
@@ -0,0 +1,204 @@
+# frozen_string_literal: true
+
+module Fronde
+ # Wrapper for each possible project source.
+ # Must be subclassed by specific source type (like Gemini or HTML)
+ class Source
+ def initialize(source_config)
+ @config = {
+ 'recursive' => true, 'is_blog' => false,
+ 'domain' => CONFIG.get('domain'), 'atom_feed' => '',
+ 'org-options' => {
+ 'section-numbers' => 'nil', 'with-toc' => 'nil'
+ }
+ }.merge(source_config)
+ clean_config
+ org_publish_options
+ render_heading
+ end
+
+ def [](key)
+ @config[key]
+ end
+
+ def []=(key, value)
+ @config[key] = value
+ end
+
+ def type
+ @config['type']
+ end
+
+ def recursive?
+ !!@config['recursive']
+ end
+
+ def blog?
+ !!@config['is_blog']
+ end
+
+ def to_h
+ @config
+ end
+
+ def source_for?(file_name)
+ relative_file_path = file_name.delete_prefix "#{@config['path']}/"
+ # file_name does not begin with source path.
+ return false if relative_file_path == file_name
+
+ # Looks like a file at a deeper level, but current source is not
+ # recursive.
+ return false if relative_file_path.include?('/') && !recursive?
+
+ # We don’t check file if the file really exist as the current
+ # check may be done before the file is actually written.
+ true
+ end
+
+ def source_for(file_name)
+ relative_file_path = file_name.delete_prefix "#{publication_path}/"
+ # file_name does not begin with source path.
+ return nil if relative_file_path == file_name
+
+ # Looks like a file at a deeper level, but current source is not
+ # recursive.
+ return nil if relative_file_path.include?('/') && !recursive?
+
+ # Looks like a match. But does a source file for this one actually
+ # exists?
+ relative_source_path = relative_file_path.sub(
+ /#{@config['ext']}\z/, '.org'
+ )
+ source_path = File.join(@config['path'], relative_source_path)
+ return nil unless File.file?(source_path)
+
+ source_path
+ end
+
+ def target_for(file_name)
+ target = File.expand_path file_name
+ target.delete_prefix! "#{Dir.pwd}/"
+ target.sub!(/\.org\z/, @config['ext'])
+ project_relative_path = @config['path'].delete_prefix("#{Dir.pwd}/")
+ target.delete_prefix! "#{project_relative_path}/"
+ public_absolute_path + target
+ end
+
+ def exclude_file?(file_name)
+ # Obviously excluding index itself for blogs
+ return true if file_name == File.join(@config['path'], 'index.org')
+
+ exclusion_rules = @config['exclude']
+ return false unless exclusion_rules
+
+ file_name.match? exclusion_rules
+ end
+
+ def org_config
+ name = @config['name']
+ [{ 'name' => name, 'attributes' => org_project_config },
+ { 'name' => "#{name}-assets", 'attributes' => org_assets_config }]
+ end
+
+ # Return the publication absolute path on file system.
+ #
+ # The returned string never end with a slash (/).
+ #
+ # Use {Fronde::Source#public_absolute_path} to get the absolute path
+ # of this project, as seen from a web browser.
+ #
+ # @return [String] the absolute path to the target dir of this project
+ def publication_path
+ return @config['publication_path'] if @config['publication_path']
+
+ publish_in = [File.expand_path(@config['folder']), @config['target']]
+ @config['publication_path'] = publish_in.join('/').delete_suffix('/')
+ end
+
+ # Return the absolute path as seen in User Agent.
+ #
+ # The returned string always end with a slash (/).
+ #
+ # Use {Fronde::Source#publication_path} to locate published file on
+ # the file system.
+ #
+ # @return [String] the absolute path to this project
+ def public_absolute_path
+ return @config['public_absolute_path'] if @config['public_absolute_path']
+
+ @config['public_absolute_path'] = "/#{@config['target']}/".squeeze('/')
+ end
+
+ class << self
+ def canonical_config(config)
+ config = { 'path' => config } if config.is_a?(String)
+ config['type'] ||= 'html'
+ config
+ end
+
+ def new_from_config(config)
+ klass_name = config['type'].capitalize
+ klass = Kernel.const_get("::Fronde::Source::#{klass_name}")
+ klass.new(config)
+ end
+ end
+
+ private
+
+ def clean_config
+ fill_in_specific_config
+ @config['name'] ||= @config['path'].sub(/^[.~]*\//, '').tr('/.', '-')
+ @config['title'] ||= @config['path']
+ @config['target'] ||= File.basename(@config['path']).delete_prefix '.'
+ @config['target'] = '' if @config['target'] == '.'
+ @config['path'] = File.expand_path(@config['path'])
+ @config['theme'] ||= CONFIG.get('theme', 'default')
+ # Blog are necessarily recursive to allow publication of tags and feeds
+ @config['recursive'] = true if @config['is_blog']
+ end
+
+ def org_publish_options
+ type = @config['type']
+ options = org_default_options.merge(
+ @config['org-options'],
+ CONFIG.get("org-#{type}", {}),
+ @config["org-#{type}"] || {}
+ )
+ @config['org-options'] = options
+ end
+
+ def render_heading
+ heading_key = "#{@config['type']}-head"
+ heading = @config.dig 'org-options', heading_key
+ @config['org-options'][heading_key] = \
+ Config::Helpers.render_liquid_template(
+ heading, to_h
+ )
+ end
+
+ def org_project_config
+ attributes = {
+ 'base-directory' => @config['path'],
+ 'base-extension' => 'org',
+ 'publishing-directory' => publication_path,
+ 'recursive' => @config['recursive']
+ }.merge(@config['org-options'])
+ exclude = @config['exclude']
+ attributes['exclude'] = exclude if exclude
+ attributes.sort.to_h # Have lisp config sorted
+ end
+
+ def org_assets_config
+ {
+ 'base-directory' => @config['path'],
+ 'base-extension' => %w[gif jpg png svg pdf].join('\\\\|'),
+ 'publishing-directory' => publication_path,
+ 'publishing-function' => 'org-publish-attachment',
+ 'recursive' => @config['recursive']
+ }
+ end
+ end
+end
+
+require_relative 'source/gemini'
+require_relative 'source/html'
diff --git a/lib/fronde/source/gemini.rb b/lib/fronde/source/gemini.rb
new file mode 100644
index 0000000..2f44fc6
--- /dev/null
+++ b/lib/fronde/source/gemini.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Fronde
+ class Source
+ # Specific settings for Gemini {Fronde::Source}
+ class Gemini < Source
+ def blog?
+ # TODO: See how to support blog/indexes with gemini
+ false
+ end
+
+ class << self
+ def org_default_postamble
+ format(
+ "📅 %<date>s\n📝 %<author>s %<creator>s",
+ author: R18n.t.fronde.org.postamble.written_by,
+ creator: R18n.t.fronde.org.postamble.with_emacs,
+ date: R18n.t.fronde.org.postamble.last_modification
+ )
+ end
+ end
+
+ private
+
+ def fill_in_specific_config
+ @config.merge!(
+ 'type' => 'gemini', 'ext' => '.gmi', 'mime_type' => 'text/gemini',
+ 'folder' => CONFIG.get('gemini_public_folder')
+ )
+ end
+
+ def org_default_options
+ { 'publishing-function' => 'org-gmi-publish-to-gemini',
+ 'gemini-head' => '',
+ 'gemini-postamble' => Gemini.org_default_postamble }
+ end
+ end
+ end
+end
diff --git a/lib/fronde/source/html.rb b/lib/fronde/source/html.rb
new file mode 100644
index 0000000..018bc2d
--- /dev/null
+++ b/lib/fronde/source/html.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Fronde
+ class Source
+ # Specific settings for HTML {Fronde::Source}
+ class Html < Source
+ def org_config
+ config = super
+ config[0]['theme'] = @config['theme']
+ config
+ end
+
+ class << self
+ def org_default_postamble
+ <<~POSTAMBLE
+ <p><span class="author">#{R18n.t.fronde.org.postamble.written_by}</span>
+ #{R18n.t.fronde.org.postamble.with_emacs_html}</p>
+ <p class="date">#{R18n.t.fronde.org.postamble.last_modification}</p>
+ <p class="validation">%v</p>
+ POSTAMBLE
+ end
+ end
+
+ private
+
+ def fill_in_specific_config
+ @config.merge!(
+ 'type' => 'html', 'ext' => '.html', 'mime_type' => 'text/html',
+ 'folder' => CONFIG.get('html_public_folder')
+ )
+ end
+
+ def org_publish_options
+ if @config['is_blog']
+ @config['atom_feed'] = <<~ATOMFEED
+ <link rel="alternate" type="application/atom+xml" title="#{@config['title']}"
+ href="#{@config['domain']}#{public_absolute_path}feeds/index.xml" />
+ ATOMFEED
+ end
+ super
+ end
+
+ def org_default_options # rubocop:disable Metrics/MethodLength
+ defaults = {
+ 'publishing-function' => 'org-html-publish-to-html',
+ 'html-head-include-default-style' => 't',
+ 'html-head-include-scripts' => 't',
+ 'html-head' => '{{ atom_feed }}',
+ 'html-postamble' => Html.org_default_postamble
+ }
+ return defaults if @config['theme'] == 'default'
+
+ defaults.merge(
+ 'html-head-include-default-style' => 'nil',
+ 'html-head-include-scripts' => 'nil',
+ 'html-head' => <<~HTMLHEAD
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="{{ domain }}/assets/{{ theme }}/css/style.css">
+ <link rel="stylesheet" type="text/css" media="screen"
+ href="{{ domain }}/assets/{{ theme }}/css/htmlize.css">
+ {{ atom_feed }}
+ HTMLHEAD
+ )
+ end
+ end
+ end
+end
diff --git a/lib/fronde/templater.rb b/lib/fronde/templater.rb
index 17a7aea..bff94d9 100644
--- a/lib/fronde/templater.rb
+++ b/lib/fronde/templater.rb
@@ -2,117 +2,140 @@
require 'nokogiri'
require 'digest/md5'
-require 'fronde/org_file'
+require_relative 'org/file'
module Fronde
+ class NoHeadError < ::StandardError; end
+
# Insert custom part inside generated HTML files.
class Templater
- def initialize(source, dom, opts = {})
+ def initialize(source, dom, config = {})
@dom = dom
@org_file = source
- @position = opts['type'] || 'after'
- @content = extract_content opts
- @element = @dom.css(opts['selector'])
- digest = Digest::MD5.hexdigest(@content)
- @check_line = " Fronde Template: #{digest} "
+ @config = { 'type' => 'after' }.merge(config)
+ digest = Digest::MD5.hexdigest(config.to_s)
+ @config['check_line'] = " Fronde Template: #{digest} "
end
def apply
- flag_head
- content = @org_file.format(@content)
- @element.each do |e|
- insert_new_node_at e, content
+ # Flag the file for this template
+ head = @dom.xpath('//head').first
+ raise NoHeadError, self unless head
+
+ head.prepend_child("<!--#{@config['check_line']}-->")
+ content = @org_file.format extract_content
+ # Remove source element if necessary to avoid doubling it during
+ # the insert action
+ @config['source'].unlink if @config.has_key? 'source'
+ # Insert new content
+ @dom.css(@config['selector']).each do |element|
+ insert_new_node_at element, content
end
end
def in_head?
- @dom.xpath('//head').children.to_a.filter(&:comment?).each do |c|
- return true if c.text == @check_line
+ @dom.xpath('//head').children.any? do |child|
+ next false unless child.comment?
+
+ child.text == @config['check_line']
+ end
+ end
+
+ def valid?(file_name)
+ return false unless @config.has_key?('selector')
+
+ unless @config.has_key?('content') || @config.has_key?('source')
+ return false
end
- false
+
+ check_path(file_name)
end
class << self
- def customize_output(file_name, source = nil)
- templates_to_apply = filter_templates(file_name)
- return if templates_to_apply.empty?
- if source.nil?
- sourcepath = Fronde::OrgFile.source_for_target(file_name)
- source = Fronde::OrgFile.new(sourcepath)
- end
+ def customize_output(file_name)
+ source = Fronde::Org::File.new(file_name)
+ # Return if no org file found for this published file
+ return if source.file.end_with?(file_name)
+
dom = open_dom(file_name)
- templates_to_apply.each do |t|
- tpl = Fronde::Templater.new(source, dom, t)
- next if tpl.in_head?
- tpl.apply
- end
- write_dom(file_name, dom)
+ updated = apply_templates(source, dom, file_name)
+ write_dom(file_name, dom) if updated
end
private
- def filter_templates(file_name)
- templates = Fronde::Config.get('templates')
- return [] if templates.nil? || templates.empty?
- templates.filter { |t| check_required_keys(t, file_name) }
+ def apply_templates(source, dom, file_name)
+ Fronde::CONFIG.get('templates', []).map do |config|
+ template = Fronde::Templater.new(source, dom, config)
+ next if !template.valid?(file_name) || template.in_head?
+
+ template.apply
+ true
+ rescue NoHeadError
+ warn R18n.t.fronde.error.templater.no_head_element(file: file_name)
+ next
+ end.any?
end
def open_dom(file_name)
- file = File.new file_name, 'r'
- dom = Nokogiri::HTML file
- file.close
- dom
+ File.open(file_name, 'r') do |file|
+ Nokogiri::HTML file
+ end
end
def write_dom(file_name, dom)
- file = File.new file_name, 'w'
- dom.write_to file
- file.close
- end
-
- def check_path(file_name, pathes)
- pub_folder = Fronde::Config.get('public_folder')
- if pathes.is_a?(Array)
- pathes.each do |tp|
- return true if File.fnmatch?("#{pub_folder}#{tp}",
- file_name, File::FNM_DOTMATCH)
- end
- return false
+ File.open(file_name, 'w') do |file|
+ dom.write_to file
end
- File.fnmatch?("#{pub_folder}#{pathes}",
- file_name, File::FNM_DOTMATCH)
- end
-
- def check_required_keys(opts, file_name)
- return false unless opts.has_key?('selector')
- return false unless opts.has_key?('content') || opts.has_key?('source')
- return check_path(file_name, opts['path']) if opts.has_key?('path')
- true
end
end
private
- def flag_head
- @dom.xpath('//head').first.prepend_child("<!--#{@check_line}-->\n")
- end
-
- def insert_new_node_at(elem, content)
- case @position
+ def insert_new_node_at(element, content)
+ case @config['type']
when 'before'
- elem.add_previous_sibling content
+ element.add_previous_sibling content
when 'replace'
- elem.replace content
+ element.replace content
else
- elem.add_next_sibling content
+ element.add_next_sibling content
+ end
+ end
+
+ def extract_content
+ # We must either have a source or a content key
+ source = @config.delete 'source'
+ unless source.is_a?(String) && source != ''
+ return @config['content'] || ''
+ end
+
+ node = @dom.css(source)
+ # Do nothing if we don’t have a reliable content to work with
+ unless node.any?
+ warn(
+ R18n.t.fronde.error.templater.no_element_found(
+ source: source,
+ file: Fronde::CONFIG.get('html_public_folder') + @org_file.pub_file
+ )
+ )
+ return ''
end
+
+ @config['source'] = node
+ node.to_s
end
- def extract_content(opts)
- return opts['content'] if opts['content']
- # If we don't have a content option, then we must have a source
- # one.
- @dom.css(opts['source']).unlink.to_s
+ def check_path(file_name)
+ paths = @config['path']
+ return true unless paths
+
+ paths = [paths] unless paths.is_a? Array
+
+ pub_folder = Fronde::CONFIG.get('html_public_folder')
+ paths.any? do |template_path|
+ File.fnmatch?("#{pub_folder}#{template_path}", file_name)
+ end
end
end
end
diff --git a/lib/fronde/utils.rb b/lib/fronde/utils.rb
deleted file mode 100644
index d630a42..0000000
--- a/lib/fronde/utils.rb
+++ /dev/null
@@ -1,229 +0,0 @@
-# frozen_string_literal: true
-
-require 'uri'
-require 'rainbow'
-require 'net/http'
-require 'r18n-core'
-require 'fronde/config'
-
-module Fronde
- # Default Error, which may be raised by fronde code
- class Error < ::StandardError; end
-
- # Embeds usefull methods, mainly used in rake tasks.
- module Utils
- # @return [Hash] the possible throbber themes
- THROBBER_FRAMES = {
- 'basic' => '-\|/',
- 'basicdots' => '⋯⋱⋮⋰',
- 'moon' => '🌑🌒🌓🌔🌕🌖🌗🌘',
- 'clock' => '🕛🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚',
- 'bricks' => '⣷⣯⣟⡿⢿⣻⣽⣾',
- 'points' => '·⁘∷⁛∷⁘',
- 'quadrant2' => '▙▛▜▟',
- 'default' => ['⠁ ⠂ ⠄ ⡀ ⠄ ⠂ ⠁', '⠂ ⠁ ⠂ ⠄ ⡀ ⠄ ⠂', '⠄ ⠂ ⠁ ⠂ ⠄ ⡀ ⠄',
- '⡀ ⠄ ⠂ ⠁ ⠂ ⠄ ⡀', '⠄ ⡀ ⠄ ⠂ ⠁ ⠂ ⠄', '⠂ ⠄ ⡀ ⠄ ⠂ ⠁ ⠂']
- }.freeze
-
- # @return [Hash] the possible ~fronde~ options and their
- # configuration
- FRONDE_OPTIONS = {
- '-a' => { long: 'author' },
- '-f' => { long: 'force', boolean: true },
- '-h' => { long: 'help', boolean: true, meth: :on_tail },
- '-l' => { long: 'lang', keyword: 'LOCALE' },
- '-t' => { long: 'title' },
- '-v' => { long: 'verbose', boolean: true, meth: :on_tail },
- '-V' => { long: 'version', boolean: true, meth: :on_tail }
- }.freeze
-
- # @return [Hash] the possible ~fronde~ subcommands and their
- # configuration
- FRONDE_COMMANDS = {
- 'init' => { opts: ['-a', '-h', '-l', '-t', '-v'] },
- 'update' => { opts: ['-a', '-h', '-l', '-t', '-v'] },
- 'config' => { alias: 'update' },
- 'preview' => { opts: ['-h'] },
- 'open' => { opts: ['-a', '-h', '-l', '-t', '-v'] },
- 'edit' => { alias: 'open' },
- 'build' => { opts: ['-f', '-h'] },
- 'publish' => { opts: ['-h'] },
- 'help' => { opts: ['-h'] },
- 'basic' => { opts: ['-h', '-V'], label: '<command>' }
- }.freeze
-
- class << self
- # Animates strings in the user console to alert him that something
- # is running in the background.
- #
- # The animation is chosen among a bunch of themes, with the
- # configuration option ~throbber~ (retrieved via
- # {Fronde::Config#settings}).
- #
- # @example
- # long_stuff = Thread.new { very_long_operation }
- # Fronde::Utils.throbber(long_stuff, 'Computing hard stuff:')
- #
- # @param thread [Thread] the long-running operation to decorate
- # @param message [String] the message to display before the throbber
- # @return [void]
- def throbber(thread, message)
- frames = select_throbber_frames
- begin
- run_and_decorate_thread thread, message, frames
- rescue RuntimeError => e
- throbber_error message
- raise e
- else
- done = Rainbow('done'.ljust(frames[0].length)).green
- puts "#{message} #{done}"
- end
- end
-
- # Returns the short and long options specification for a given
- # short option.
- #
- # This method use the {Fronde::Utils::FRONDE_OPTIONS} Hash to
- # retrieve corresponding values.
- #
- # @example
- # spec = Fronde::Utils.decorate_option('-a')
- # => ['-a AUTHOR', '--author AUTHOR']
- #
- # @param short [String] the short option to decorate
- # @return [Array] the short and long specification for an option
- def decorate_option(short)
- opt = Fronde::Utils::FRONDE_OPTIONS[short]
- long = "--#{opt[:long]}"
- return [short, long] if opt[:boolean]
- key = opt[:keyword] || opt[:long].upcase
- [short + key, format('%<long>s %<key>s', long: long, key: key)]
- end
-
- # Returns the ~fronde~ help summary for a given command.
- #
- # @param command [String] the command for which a summary
- # should be given
- # @return [String]
- def summarize_command(command)
- Fronde::Utils::FRONDE_COMMANDS[command][:opts].map do |k|
- short, long = Fronde::Utils.decorate_option(k)
- opt = Fronde::Utils::FRONDE_OPTIONS[k]
- label = [short, long].join(', ')
- line = [format(' %<opt>s', opt: label).ljust(30)]
- if R18n.t.fronde.bin.options[opt[:long]].translated?
- line << R18n.t.fronde.bin.options[opt[:long]]
- end
- line.join(' ')
- end.join("\n")
- end
-
- # Returns a formatted list of available commands for ~fronde~.
- #
- # @return [String]
- def list_commands
- lines = []
- Fronde::Utils::FRONDE_COMMANDS.each do |cmd, opt|
- next if cmd == 'basic'
- line = [' ', cmd.ljust(10)]
- if opt.has_key? :alias
- line << R18n.t.fronde.bin.commands.alias(opt[:alias])
- else
- line << R18n.t.fronde.bin.commands[cmd]
- end
- lines << line.join(' ')
- end
- lines.join("\n")
- end
-
- # Returns the real command name for a given command, which may be
- # an alias.
- #
- # @param command [String] the command to resolve
- # @return [String]
- def resolve_possible_alias(command)
- return 'basic' unless Fronde::Utils::FRONDE_COMMANDS.include?(command)
- cmd_opt = Fronde::Utils::FRONDE_COMMANDS[command]
- return cmd_opt[:alias] if cmd_opt.has_key?(:alias)
- command
- end
-
- # Returns the given command options.
- #
- # This method will first try to resolve command alias, if any.
- #
- # @param command [String] the command, which options should be returned
- # @return [Hash] the command options
- def command_options(command)
- cmd = resolve_possible_alias command
- FRONDE_COMMANDS[cmd].merge(name: cmd)
- end
-
- # Try to discover the current host operating system.
- #
- # @return [String] either apple, windows or linux (default)
- # :nocov:
- def current_os
- if ENV['OS'] == 'Windows_NT' || RUBY_PLATFORM.include?('cygwin')
- return 'windows'
- end
- return 'apple' if RUBY_PLATFORM.include?('darwin')
- 'linux'
- end
- # :nocov:
-
- # Download latest org-mode tarball.
- #
- # @param destination [String] where to save the org-mode tarball
- # @return [String] the downloaded org-mode version
- def download_org(destination = 'var/tmp')
- # :nocov:
- return if Fronde::Config.org_last_version.nil?
- # :nocov:
- # Remove version number in dest file to allow easy rake file
- # task naming
- dest_file = File.expand_path('org.tar.gz', destination)
- return if File.exist?(dest_file)
- tarball = "org-mode-release_#{Fronde::Config.org_last_version}.tar.gz"
- uri = URI("https://git.savannah.gnu.org/cgit/emacs/org-mode.git/snapshot/#{tarball}")
- # Will crash on purpose if anything goes wrong
- Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http|
- http.request(Net::HTTP::Get.new(uri)) do |response|
- File.open(dest_file, 'w') do |io|
- response.read_body { |chunk| io.write chunk }
- end
- end
- end
- end
-
- private
-
- def throbber_error(message)
- warn(
- format(
- "%<message>s %<label>s\n%<explanation>s",
- message: message,
- label: Rainbow(R18n.t.fronde.error.label).bold.red,
- explanation: Rainbow(R18n.t.fronde.error.explanation).bold
- )
- )
- end
-
- def select_throbber_frames
- model = Fronde::Config.settings['throbber'] || 'default'
- model = 'default' unless Fronde::Utils::THROBBER_FRAMES.has_key?(model)
- Fronde::Utils::THROBBER_FRAMES[model]
- end
-
- def run_and_decorate_thread(thread, message, frames)
- thread.abort_on_exception = true
- current = 0
- while thread.alive?
- sleep 0.1
- print "#{message} #{frames[current % frames.length]}\r"
- current += 1
- end
- end
- end
- end
-end
diff --git a/lib/fronde/version.rb b/lib/fronde/version.rb
index 91ae6da..7deb88d 100644
--- a/lib/fronde/version.rb
+++ b/lib/fronde/version.rb
@@ -2,5 +2,5 @@
module Fronde
# @return [String] the version number of the current Fronde release.
- VERSION = '0.3.4'
+ VERSION = '0.4.0'
end
diff --git a/lib/tasks/cli.rake b/lib/tasks/cli.rake
new file mode 100644
index 0000000..2aa288f
--- /dev/null
+++ b/lib/tasks/cli.rake
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require_relative '../fronde/cli/opt_parse'
+
+namespace :cli do
+ desc 'Generate an autocomplete file for zsh'
+ task :zsh_complete do
+ data = {}
+ all_commands = Fronde::CLI::OptParse::FRONDE_COMMANDS
+ data['commands'] = all_commands.filter_map do |command, options|
+ next if options[:alias] || command == 'basic'
+
+ opts = (options[:opts] || []).map do |opt|
+ opt_config = Fronde::CLI::OptParse::FRONDE_OPTIONS[opt]
+ keyword = nil
+ unless opt_config[:boolean]
+ keyword = opt_config[:keyword] || opt_config[:long].upcase
+ end
+ { 'short' => opt,
+ 'long' => "--#{opt_config[:long]}",
+ 'keyword' => keyword }
+ end
+
+ translation = R18n.t.fronde.bin.commands[command].tr("'", '’')
+ { 'name' => command,
+ 'translation' => translation,
+ 'options' => opts }
+ end
+ source = File.expand_path '../fronde/cli/data/zsh_completion', __dir__
+ template = Liquid::Template.parse(File.read(source))
+ puts template.render(data)
+ end
+end
diff --git a/lib/tasks/org.rake b/lib/tasks/org.rake
index 7f1f252..f9eb848 100644
--- a/lib/tasks/org.rake
+++ b/lib/tasks/org.rake
@@ -2,61 +2,57 @@
require 'open-uri'
-# Fronde::Config is required by Fronde::Utils
-require 'fronde/utils'
+require_relative '../fronde/config'
+require_relative '../fronde/cli/throbber'
require 'rake/clean'
CLOBBER.push(
- 'var/tmp/org.tar.gz', 'var/tmp/last_org_version',
- 'var/lib/org-config.el', '.dir-locals.el', 'lib/htmlize.el'
+ 'var/lib/org-config.el', '.dir-locals.el',
+ 'lib/htmlize.el'
)
-def make_org_cmd(org_dir, target)
- make = ['make', '-C', org_dir, target]
- return make.join(' ') if verbose
- make.insert(3, '-s')
- make << 'EMACSQ="emacs -Q --eval \'(setq inhibit-message t)\'"'
- make.join(' ')
-end
-
namespace :org do
directory 'var/tmp'
desc 'Download last version of Org'
file 'var/tmp/org.tar.gz' => 'var/tmp' do
- download = Thread.new do
- Thread.current[:org_version] = Fronde::Config.org_last_version
- Fronde::Utils.download_org
- end
+ # Weird Rake issue, still executing the task even if the file exists
+ next if File.exist? 'var/tmp/org.tar.gz'
+
+ download = Thread.new { Fronde::Org.download }
if verbose
- download.join
- warn "Org version #{download[:org_version]} has been downloaded"
+ warn R18n.t.fronde.tasks.org.downloaded(version: download.value)
else
- Fronde::Utils.throbber(download, 'Downloading Org:')
+ Fronde::CLI::Throbber.run(download, R18n.t.fronde.tasks.org.downloading)
end
+ rescue RuntimeError
+ warn R18n.t.fronde.tasks.org.no_download if verbose
end
desc 'Compile Org'
- task compile: 'var/tmp/org.tar.gz' do |task|
- org_version = Fronde::Config.org_last_version
+ multitask compile: ['var/tmp/org.tar.gz', 'lib'] do |task|
+ begin
+ # No need to force fetch last version as it is only interesting as
+ # part of the upgrade task
+ org_version = Fronde::Org.last_version
+ rescue RuntimeError
+ next
+ end
org_dir = "lib/org-#{org_version}"
next if Dir.exist?("#{org_dir}/lisp")
+
build = Thread.new do
- sh "tar -C lib -xzf #{task.prerequisites[0]}"
- mv "lib/org-mode-release_#{org_version}", org_dir
- sh make_org_cmd(org_dir, 'compile')
- sh make_org_cmd(org_dir, 'autoloads')
- Dir.glob('lib/org-[0-9.]*').each do |ov|
- next if ov == org_dir
- rm_r ov
- end
+ Fronde::Org.compile(
+ task.prerequisites[0], org_version, org_dir, verbose: verbose
+ )
+ Dir.glob('lib/org-[0-9.]*').each { rm_r _1 unless _1 == org_dir }
end
if verbose
build.join
- warn "#{org_version} has been locally installed"
+ warn R18n.t.fronde.tasks.org.installed(version: org_version)
else
- Fronde::Utils.throbber(build, 'Installing Org:')
+ Fronde::CLI::Throbber.run(build, R18n.t.fronde.tasks.org.installing)
end
end
@@ -77,29 +73,53 @@ namespace :org do
end
file 'var/lib/org-config.el' => ['lib/htmlize.el', 'lib/ox-gmi.el'] do
- Fronde::Config.write_org_lisp_config
+ Fronde::CONFIG.write_org_lisp_config
end
file '.dir-locals.el' => 'var/lib/org-config.el' do
- Fronde::Config.write_dir_locals
+ Fronde::Config::Helpers.write_dir_locals
+ end
+
+ file '.gitignore' do
+ next if File.exist? '.gitignore'
+
+ upstream = File.expand_path(
+ '../fronde/cli/data/gitignore', __dir__
+ )
+ cp upstream, '.gitignore'
end
desc 'Install Org'
- multitask install: ['org:compile', '.dir-locals.el'] do
- mkdir_p "#{Fronde::Config.get('public_folder')}/assets"
- Fronde::Config.sources.each do |s|
- mkdir_p s['path'] unless Dir.exist? s['path']
+ multitask install: ['org:compile', '.gitignore'] do
+ # I need a fully installed org mode to correctly generate the lisp
+ # config
+ Rake::Task['.dir-locals.el'].invoke
+ sources = Fronde::CONFIG.sources
+ sources.each { mkdir_p _1['path'] }
+
+ outputs = sources.map { _1['type'] }.uniq
+ if outputs.include?('html')
+ mkdir_p "#{Fronde::CONFIG.get('html_public_folder')}/assets"
+ end
+ if outputs.include?('gemini')
+ mkdir_p Fronde::CONFIG.get('gemini_public_folder')
end
end
- # The following task only run the clobber task (not provided by us)
- # and the org:install one, which is already tested. Thus, we can
- # safely remove it from coverage.
- # :nocov:
desc 'Upgrade Org'
task :upgrade do
Rake::Task['clobber'].execute
+ if File.exist? 'var/tmp/org.tar.gz'
+ # Cleanup cached tarball only if a new version is available.
+ # Also cached the new remote org version in the same time.
+ org_version = Fronde::Org.current_version
+ begin
+ last_version = Fronde::Org.last_version(force: true)
+ rescue RuntimeError
+ last_version = org_version
+ end
+ File.unlink 'var/tmp/org.tar.gz' unless org_version == last_version
+ end
Rake::Task['org:install'].invoke
end
- # :nocov:
end
diff --git a/lib/tasks/site.rake b/lib/tasks/site.rake
index 6c30b39..3928029 100644
--- a/lib/tasks/site.rake
+++ b/lib/tasks/site.rake
@@ -1,54 +1,92 @@
# frozen_string_literal: true
-require 'fronde/emacs'
-require 'fronde/index'
-require 'fronde/utils'
-require 'fronde/org_file'
-require 'fronde/templater'
+require_relative '../fronde/emacs'
+require_relative '../fronde/index'
+require_relative '../fronde/templater'
+require_relative '../fronde/cli/throbber'
namespace :site do
- desc 'Generates all index files'
- task :index do
- index = Fronde::Index.new
- if verbose
- index.write_all
- next
+ desc 'Build all your projects'
+ task :build, [:force?] => ['var/lib/org-config.el'] do |_, args|
+ args.with_defaults(force?: false)
+ build_index = Thread.new do
+ all_index = Fronde::Index.all_html_blog_index
+ all_index.each do |index|
+ index.write_all_org(verbose: verbose)
+ end
+ Thread.current[:all_indexes] = all_index
end
- build = Thread.new do
- index.write_all(verbose: false)
+ if verbose
+ build_index.join
+ else
+ Fronde::CLI::Throbber.run(
+ build_index, R18n.t.fronde.tasks.site.generating_indexes
+ )
end
- Fronde::Utils.throbber(build, 'Generating indexes:')
- next if index.empty?
- Fronde::Config.write_org_lisp_config(with_tags: true)
- end
+ all_indexes = build_index[:all_indexes]
- desc 'Convert and customize all org files'
- task :build, [:force?] => ['var/lib/org-config.el', :index] do |_, args|
- args.with_defaults(:force? => false)
- build_html = Thread.new do
- rm_r 'var/tmp/timestamps', force: true if args[:force?]
- Fronde::Emacs.new(verbose: verbose).publish
- end
begin
- Fronde::Utils.throbber(build_html, 'Building:')
+ build_html = Thread.new do
+ rm_r 'var/tmp/timestamps', force: true if args[:force?]
+ Fronde::Emacs.new(verbose: verbose).publish
+ end
+ Fronde::CLI::Throbber.run(build_html, R18n.t.fronde.tasks.site.building)
+
# :nocov:
rescue RuntimeError
- warn 'Aborting'
+ warn R18n.t.fronde.tasks.site.aborting
next
end
# :nocov:
+
+ if all_indexes.any?
+ if verbose
+ all_indexes.each(&:write_all_feeds)
+ else
+ publish_feed = Thread.new do
+ all_indexes.each do |index|
+ index.write_all_feeds(verbose: false)
+ end
+ end
+ Fronde::CLI::Throbber.run(
+ publish_feed, R18n.t.fronde.tasks.site.publishing_feeds
+ )
+ end
+ end
+
+ next unless Fronde::CONFIG.sources.any? { |source| source.type == 'html' }
+
customize_html = Thread.new do
- pubfolder = Fronde::Config.get('public_folder')
+ pubfolder = Fronde::CONFIG.get('html_public_folder')
Dir["#{pubfolder}/**/*.html"].each do |f|
Fronde::Templater.customize_output(f)
end
end
- Fronde::Utils.throbber(customize_html, 'Customizing:')
+ Fronde::CLI::Throbber.run(
+ customize_html, R18n.t.fronde.tasks.site.customizing
+ )
+ end
+
+ desc 'Cleanup orphaned published files'
+ task :clean do
+ pubfolder = Fronde::CONFIG.get('html_public_folder')
+ Dir["#{pubfolder}/**/*.html"].each do |file_name|
+ source = Fronde::Org::File.new(file_name)
+
+ # Return if an org file has been found for this published file
+ next unless source.file == file_name
+
+ print R18n.t.fronde.tasks.site.remove_orphan_file
+ action = $stdin.gets.strip.downcase
+ next unless action == 'y'
+
+ rm file_name
+ end
end
desc 'Start a test server'
task :preview do
- require 'fronde/preview'
- Fronde.start_preview
+ require_relative '../fronde/preview'
+ Fronde::Preview.start
end
end
diff --git a/lib/tasks/sync.rake b/lib/tasks/sync.rake
index 74c3ce7..1654f1b 100644
--- a/lib/tasks/sync.rake
+++ b/lib/tasks/sync.rake
@@ -1,15 +1,16 @@
# frozen_string_literal: true
-require 'fronde/config'
-require 'fronde/utils'
+require_relative '../fronde/config'
+require_relative '../fronde/cli/throbber'
module Fronde
- class SyncError < Error; end
+ class SyncError < ::StandardError; end
end
def rsync_command(test = nil)
- rsync_command = Fronde::Config.get('rsync')
+ rsync_command = Fronde::CONFIG.get('rsync')
return rsync_command unless rsync_command.nil?
+
optstring = []
optstring << 'n' if test
if verbose
@@ -20,37 +21,56 @@ def rsync_command(test = nil)
"rsync -#{optstring.join}rlt --delete"
end
-def pull_or_push(direction, label, test)
- unless [:pull, :push].include? direction
- raise Fronde::SyncError, 'Not a valid direction'
- end
- remote_path = Fronde::Config.get('remote')
- raise Fronde::SyncError, 'No remote path set' if remote_path.nil?
- public_folder = Fronde::Config.get('public_folder')
+def pull_or_push(direction, type, test)
+ remote_path = Fronde::CONFIG.get("#{type}_remote")
+ raise Fronde::SyncError, "No #{type} remote path set" if remote_path.nil?
+
+ public_folder = Fronde::CONFIG.get("#{type}_public_folder")
# Default is to push
cmd = ["#{public_folder}/", remote_path]
cmd.reverse! if direction == :pull
rsync = rsync_command(test)
- publish_thread = Thread.new do
+ Thread.new do
sh "#{rsync} #{cmd.join(' ')}"
end
- Fronde::Utils.throbber(publish_thread, label)
+end
+
+def source_types
+ Fronde::CONFIG.sources.map(&:type).uniq
end
namespace :sync do
desc 'Push changes to server'
task :push, :test? do |_, args|
- pull_or_push(:push, 'Publishing:', args[:test?])
- rescue Fronde::SyncError => e
- warn e
- next
+ source_types.each do |type|
+ publish_thread = pull_or_push(:push, type, args[:test?])
+ if verbose
+ publish_thread.join
+ else
+ Fronde::CLI::Throbber.run(
+ publish_thread, format('Publishing %<fmt>s:', fmt: type)
+ )
+ end
+ rescue Fronde::SyncError => e
+ warn e
+ next
+ end
end
desc 'Pull changes from server'
task :pull, :test? do |_, args|
- pull_or_push(:pull, 'Pulling:', args[:test?])
- rescue Fronde::SyncError => e
- warn e
- next
+ source_types.each do |type|
+ pull_thread = pull_or_push(:pull, type, args[:test?])
+ if verbose
+ pull_thread.join
+ else
+ Fronde::CLI::Throbber.run(
+ pull_thread, format('Pulling %<fmt>s:', fmt: type)
+ )
+ end
+ rescue Fronde::SyncError => e
+ warn e
+ next
+ end
end
end
diff --git a/lib/tasks/tags.rake b/lib/tasks/tags.rake
index 9fc5dd9..e8bb303 100644
--- a/lib/tasks/tags.rake
+++ b/lib/tasks/tags.rake
@@ -1,19 +1,23 @@
# frozen_string_literal: true
-require 'fronde/index'
+require_relative '../fronde/index'
namespace :tags do
desc 'List all tags by name'
task :name do
- index = Fronde::Index.new
- next if index.empty?
- puts index.sort_by(:name).join("\n")
+ Fronde::Index.all_html_blog_index do |index|
+ next if index.empty?
+
+ puts index.sort_by(:name).join("\n")
+ end
end
desc 'List all tags by weight'
task :weight do
- index = Fronde::Index.new
- next if index.empty?
- puts index.sort_by(:weight).join("\n")
+ Fronde::Index.all_html_blog_index do |index|
+ next if index.empty?
+
+ puts index.sort_by(:weight).join("\n")
+ end
end
end
diff --git a/locales/en.yml b/locales/en.yml
index 6cb3310..2338720 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -2,27 +2,55 @@
fronde:
bin:
usage: 'Usage: fronde %1 [options]'
+ done: done
commands:
cmd_title: Commands
- alias: Alias for %1.
- init: Initialize your Fronde instance (you just need to do it once).
- update: Update Fronde dependency (to be run once in a while).
- preview: 'Start a test web server to preview your website on http://127.0.0.1:5000'
+ alias: Alias for ‘%1’.
+ new: Initialize a new Fronde instance.
+ update: >-
+ Update Fronde configuration and dependency (to be run after each
+ modification of the config.yml file and once in a while to stay
+ up-to-date with Org).
+ preview: Start a test web server to preview the generated website.
open: Open or create an org file.
- build: Compile your org files to HTML.
- publish: Push local changes to your public web server.
+ build: Compile all org files to HTML or gemtext.
+ publish: Push local changes to the public web server.
help: Alias for the -h switch.
options:
cmd_title: Options
- directory: Wrap the new org file in this named folder.
help: Display help for a command and exit.
version: Display Fronde version and exit.
- error:
- no_command: 'ERROR: no command or unknown command given.'
- no_file: 'ERROR: no file to open or edit.'
+ default_title: New article
error:
- label: An error occurred.
- explanation: To see the error, run your command again with more verbosity, for example, fronde build -v
+ bin:
+ label: An error occurred.
+ explanation: >-
+ To see the error, run the same command again with more
+ verbosity, for example, fronde build -v
+ no_command: 'ERROR: no command or unknown command given.'
+ no_file: >-
+ Warning: No file path given. Default file will be used.
+ config:
+ deprecated_public_folder: >-
+ ‘public_folder’ setting is deprecated. Please use either
+ ‘html_public_folder’ or ‘gemini_public_folder’.
+ source:
+ no_path: Skipping %{source} as its ‘path’ key is missing.
+ duplicate: >-
+ Skipping %{source} as it appears at least twice in the sources
+ of type %{type}.
+ inclusion: >-
+ Skipping %{source} as it might be already embedded into the
+ other source %{other_source} of type %{type}.
+ org_file:
+ no_file_or_title: No file or title given.
+ no_project: No project found for %{file}. Publication will fail.
+ index:
+ wrong_sort_kind: '%{kind} not in %{accepted_values}'
+ templater:
+ no_element_found: >-
+ No element found with the selector %{source} in %{file}.
+ no_head_element: No head tag found in file %{file}.
index:
unsorted: Unsorted
published_on: Published on %1
@@ -31,9 +59,27 @@ fronde:
by_weight: By publication number
full_date_format: '%A %{date}'
full_date_with_time_format: '%{date} at %{time}'
+ index_generated: Generated index file for %{tag}.
+ atom_generated: Generated Atom feed for %{tag}.
org:
+ generate_blog_index: Generating blog home page for %{name}
postamble:
written_by: Written by %a
last_modification: Last modification on %C
- with_emacs: 'with %c, and published with %n'
- with_emacs_html: 'with %c, and published with %N'
+ with_emacs: with %c, and published with %n
+ with_emacs_html: with %c, and published with %N
+ tasks:
+ site:
+ aborting: Aborting
+ generating_indexes: 'Generating index files:'
+ building_indexes: 'Building index files:'
+ publishing_feeds: 'Publishing Atom feeds:'
+ building: 'Building:'
+ customizing: 'Customizing:'
+ remove_orphan_file: 'Remove it? [y/N]: '
+ org:
+ downloaded: Org version %{version} has been downloaded.
+ downloading: 'Downloading Org:'
+ no_download: Impossible to download Org now. Please try again later.
+ installed: Org version %{version} has been locally installed.
+ installing: 'Installing Org:'
diff --git a/locales/fr.yml b/locales/fr.yml
index db6cd56..245dfbf 100644
--- a/locales/fr.yml
+++ b/locales/fr.yml
@@ -2,27 +2,61 @@
fronde:
bin:
usage: 'Usage : fronde %1 [options]'
+ done: fait
commands:
cmd_title: Commandes
- alias: Alias pour %1.
- init: Initialise votre instance de Fronde (vous ne devriez faire cela qu'une fois).
- update: Met à jour les dépendances de Fronde (à lancer une fois de temps à autre).
- preview: "Démarre un serveur web de test pour prévisualiser votre site à l'adresse http://127.0.0.1:5000"
+ alias: Alias pour ‘%1’.
+ new: Initialise une nouvelle instance de Fronde.
+ update: >-
+ Met à jour la configuration et les dépendances de Fronde (à
+ lancer après chaque modification du fichier config.yml et une
+ fois de temps à autre pour rester à jour avec Org).
+ preview: >-
+ Démarre un serveur web de test pour prévisualiser le site
+ généré.
open: Ouvre ou crée un fichier org.
- build: Compile vos fichiers org en HTML.
- publish: Pousse vos changements locaux vers votre serveur web public.
+ build: Compile les fichiers org en HTML ou gemtext.
+ publish: >-
+ Pousse les changements locaux vers le serveur web public.
help: Alias pour l'argument -h.
options:
cmd_title: Options
- directory: Place le nouveau fichier org dans un dossier à ce nom.
help: Affiche l'aide pour une commande et quitte.
version: Affiche la version de Fronde et quitte.
- error:
- no_command: 'ERREUR : Aucune commande ou commande inconnue donnée.'
- no_file: 'ERREUR : Aucun fichier à ouvrir ou éditer.'
+ default_title: Nouvel article
error:
- label: Une erreur est survenue
- explanation: Pour voir le détail, lancez de nouveau la commande avec plus de verbosité, par exemple fronde build -v
+ bin:
+ label: Une erreur est survenue
+ explanation: >-
+ Pour voir le détail, lancez de nouveau la commande avec plus de
+ verbosité, par exemple fronde build -v
+ no_command: 'ERREUR : Aucune commande ou commande inconnue donnée.'
+ no_file: >-
+ Attention : Aucun chemin de fichier donné. Utilisation du
+ fichier par défaut.
+ config:
+ deprecated_public_folder: >-
+ La clé de configuration ‘public_folder’ est dépréciée. Merci
+ d’utiliser ‘html_public_folder’ ou ‘gemini_public_folder’ à la
+ place.
+ source:
+ no_path: >-
+ Ignore %{source} comme sa clé ‘path’ est manquante.
+ duplicate: >-
+ Ignore %{source} comme elle apparaît au moins deux fois dans les
+ sources de type %{type}.
+ inclusion: >-
+ Ignore %{source} qui semble être déjà inclue dans l’autre source
+ %{other_source} de type %{type}.
+ org_file:
+ no_file_or_title: Aucun chemin de fichier ou titre donné.
+ no_project: Aucun projet trouvé pour %{file}. Sa publication va échouer.
+ index:
+ wrong_sort_kind: '%{kind} n’est pas dans %{accepted_values}'
+ templater:
+ no_element_found: >-
+ Aucun élément trouvé avec le sélecteur %{source} dans %{file}.
+ no_head_element: Aucun élément head trouvé dans le fichier %{file}.
index:
unsorted: Non triés
published_on: Publié le %1
@@ -31,9 +65,29 @@ fronde:
by_weight: Par nombre de publication
full_date_format: '%A %{date}'
full_date_with_time_format: '%{date} à %{time}'
+ index_generated: Fichier d’index généré pour %{tag}.
+ atom_generated: Flux Atom généré pour %{tag}.
org:
+ generate_blog_index: Génération de la page d’accueil du blog pour %{name}
postamble:
written_by: Écrit par %a
last_modification: dernière modification le %C
- with_emacs: 'avec %c et publié avec %n'
- with_emacs_html: 'avec %c et publié avec %N'
+ with_emacs: avec %c et publié avec %n
+ with_emacs_html: avec %c et publié avec %N
+ tasks:
+ site:
+ aborting: Annulation
+ generating_indexes: 'Génération des fichiers d’index :'
+ building_indexes: 'Compilation des fichiers d’index :'
+ publishing_feeds: 'Publication des flux Atom :'
+ building: 'Compilation :'
+ customizing: 'Décoration :'
+ remove_orphan_file: 'Le supprimer ? [y/N]: '
+ org:
+ downloaded: La version %{version} de Org a été téléchargée.
+ downloading: 'Téléchargement de Org :'
+ no_download: >-
+ Impossible de télécharger Org maintenant. Merci de réessayer
+ plus tard.
+ installed: La version %{version} de Org a été installé localement.
+ installing: 'Installation de Org :'
diff --git a/spec/data/config_3_gemini_org_config.el b/spec/data/config_3_gemini_org_config.el
new file mode 100644
index 0000000..70c87b9
--- /dev/null
+++ b/spec/data/config_3_gemini_org_config.el
@@ -0,0 +1,57 @@
+;; Add org-mode to load path
+(add-to-list 'load-path (expand-file-name "org-{{ org_version }}/lisp" "{{ test_dir }}/lib"))
+;; Load last version of htmlize.el
+(load-file (expand-file-name "htmlize.el" "{{ test_dir }}/lib"))
+
+;; Current project options
+(setq fronde/version "{{ version }}"
+ fronde/current-work-dir "{{ test_dir }}"
+ user-mail-address ""
+ user-full-name "Tata"
+ org-html-metadata-timestamp-format "%A %-d of %B, %Y at %R"
+ org-gmi-timestamp-format "%A %-d of %B, %Y at %R"
+ org-publish-project-alist
+ `(("src"
+ :base-directory "{{ test_dir }}/src"
+ :base-extension "org"
+ :gemini-head ""
+ :gemini-postamble "📅 Last modification on %C
+📝 Written by %a with %c, and published with %n"
+ :publishing-directory "{{ test_dir }}/public_gmi/src"
+ :publishing-function org-gmi-publish-to-gemini
+ :recursive nil
+ :section-numbers nil
+ :with-toc nil)
+ ("src-assets"
+ :base-directory "{{ test_dir }}/src"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_gmi/src"
+ :publishing-function org-publish-attachment
+ :recursive nil)
+ ("src-news"
+ :base-directory "{{ test_dir }}/src/news"
+ :base-extension "org"
+ :html-head "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"src/news\"
+ href=\"/news/feeds/index.xml\" />"
+ :html-head-include-default-style t
+ :html-head-include-scripts t
+ :html-postamble "<p><span class=\"author\">Written by %a</span>
+with %c, and published with %N</p>
+<p class=\"date\">Last modification on %C</p>
+<p class=\"validation\">%v</p>"
+ :publishing-directory "{{ test_dir }}/public_html/news"
+ :publishing-function org-html-publish-to-html
+ :recursive t
+ :section-numbers nil
+ :with-toc nil)
+ ("src-news-assets"
+ :base-directory "{{ test_dir }}/src/news"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_html/news"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("website" :components ("src" "src-assets" "src-news" "src-news-assets"))))
+
+;; Load fronde lib
+(load-file (expand-file-name "ox-gmi.el" "{{ test_dir }}/lib"))
+(load-file (expand-file-name "ox-fronde.el" "{{ base_dir }}/lib/fronde/config/data"))
diff --git a/spec/data/config_3_org_config.el b/spec/data/config_3_org_config.el
new file mode 100644
index 0000000..ba99935
--- /dev/null
+++ b/spec/data/config_3_org_config.el
@@ -0,0 +1,61 @@
+;; Add org-mode to load path
+(add-to-list 'load-path (expand-file-name "org-{{ org_version }}/lisp" "{{ test_dir }}/lib"))
+;; Load last version of htmlize.el
+(load-file (expand-file-name "htmlize.el" "{{ test_dir }}/lib"))
+
+;; Current project options
+(setq fronde/version "{{ version }}"
+ fronde/current-work-dir "{{ test_dir }}"
+ user-mail-address ""
+ user-full-name "Tata"
+ org-html-metadata-timestamp-format "%A %-d of %B, %Y at %R"
+ org-gmi-timestamp-format "%A %-d of %B, %Y at %R"
+ org-publish-project-alist
+ `(("src"
+ :base-directory "{{ test_dir }}/src"
+ :base-extension "org"
+ :html-head ""
+ :html-head-include-default-style t
+ :html-head-include-scripts t
+ :html-postamble "<p><span class=\"author\">Written by %a</span>
+with %c, and published with %N</p>
+<p class=\"date\">Last modification on %C</p>
+<p class=\"validation\">%v</p>"
+ :publishing-directory "{{ test_dir }}/public_html/src"
+ :publishing-function org-html-publish-to-html
+ :recursive nil
+ :section-numbers nil
+ :with-toc nil)
+ ("src-assets"
+ :base-directory "{{ test_dir }}/src"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_html/src"
+ :publishing-function org-publish-attachment
+ :recursive nil)
+ ("src-news"
+ :base-directory "{{ test_dir }}/src/news"
+ :base-extension "org"
+ :html-head "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"src/news\"
+ href=\"/news/feeds/index.xml\" />"
+ :html-head-include-default-style t
+ :html-head-include-scripts t
+ :html-postamble "<p><span class=\"author\">Written by %a</span>
+with %c, and published with %N</p>
+<p class=\"date\">Last modification on %C</p>
+<p class=\"validation\">%v</p>"
+ :publishing-directory "{{ test_dir }}/public_html/news"
+ :publishing-function org-html-publish-to-html
+ :recursive t
+ :section-numbers nil
+ :with-toc nil)
+ ("src-news-assets"
+ :base-directory "{{ test_dir }}/src/news"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_html/news"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("website" :components ("src" "src-assets" "src-news" "src-news-assets"))))
+
+;; Load fronde lib
+(load-file (expand-file-name "ox-gmi.el" "{{ test_dir }}/lib"))
+(load-file (expand-file-name "ox-fronde.el" "{{ base_dir }}/lib/fronde/config/data"))
diff --git a/spec/data/config_4_gemini_org_config.el b/spec/data/config_4_gemini_org_config.el
new file mode 100644
index 0000000..91013b7
--- /dev/null
+++ b/spec/data/config_4_gemini_org_config.el
@@ -0,0 +1,91 @@
+;; Add org-mode to load path
+(add-to-list 'load-path (expand-file-name "org-{{ org_version }}/lisp" "{{ test_dir }}/lib"))
+;; Load last version of htmlize.el
+(load-file (expand-file-name "htmlize.el" "{{ test_dir }}/lib"))
+
+;; Current project options
+(setq fronde/version "{{ version }}"
+ fronde/current-work-dir "{{ test_dir }}"
+ user-mail-address ""
+ user-full-name "Tata"
+ org-html-metadata-timestamp-format "%A %-d of %B, %Y at %R"
+ org-gmi-timestamp-format "%A %-d of %B, %Y at %R"
+ org-publish-project-alist
+ `(("src"
+ :base-directory "{{ test_dir }}/src"
+ :base-extension "org"
+ :gemini-head ""
+ :gemini-postamble "📅 Last modification on %C
+📝 Written by %a with %c, and published with %n"
+ :publishing-directory "{{ test_dir }}/public_gmi/src"
+ :publishing-function org-gmi-publish-to-gemini
+ :recursive t
+ :section-numbers nil
+ :with-toc nil)
+ ("src-assets"
+ :base-directory "{{ test_dir }}/src"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_gmi/src"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("news"
+ :base-directory "{{ test_dir }}/news"
+ :base-extension "org"
+ :html-head "<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
+ href=\"https://test.com/assets/my-theme/css/style.css\">
+<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
+ href=\"https://test.com/assets/my-theme/css/htmlize.css\">
+<link rel=\"alternate\" type=\"application/atom+xml\" title=\"news\"
+ href=\"https://test.com/news/feeds/index.xml\" />"
+ :html-head-include-default-style nil
+ :html-head-include-scripts nil
+ :html-postamble "<p><span class=\"author\">Written by %a</span>
+with %c, and published with %N</p>
+<p class=\"date\">Last modification on %C</p>
+<p class=\"validation\">%v</p>"
+ :publishing-directory "{{ test_dir }}/public_html/news"
+ :publishing-function org-html-publish-to-html
+ :recursive t
+ :section-numbers nil
+ :with-toc nil)
+ ("news-assets"
+ :base-directory "{{ test_dir }}/news"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_html/news"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("other"
+ :base-directory "{{ test_dir }}/other"
+ :base-extension "org"
+ :html-head "<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
+ href=\"https://test.com/assets/my-theme/css/style.css\">
+<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
+ href=\"https://test.com/assets/my-theme/css/htmlize.css\">"
+ :html-head-include-default-style nil
+ :html-head-include-scripts nil
+ :html-postamble "<p><span class=\"author\">Written by %a</span>
+with %c, and published with %N</p>
+<p class=\"date\">Last modification on %C</p>
+<p class=\"validation\">%v</p>"
+ :publishing-directory "{{ test_dir }}/public_html/other"
+ :publishing-function org-html-publish-to-html
+ :recursive t
+ :section-numbers nil
+ :with-toc nil)
+ ("other-assets"
+ :base-directory "{{ test_dir }}/other"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_html/other"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("theme-my-theme"
+ :base-directory "{{ test_dir }}/themes/my-theme"
+ :base-extension "css\\|js\\|gif\\|jpg\\|png\\|svg\\|otf\\|ttf\\|woff2?"
+ :publishing-directory "{{ test_dir }}/public_html/assets/my-theme"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("website" :components ("src" "src-assets" "news" "news-assets" "other" "other-assets" "theme-my-theme"))))
+
+;; Load fronde lib
+(load-file (expand-file-name "ox-gmi.el" "{{ test_dir }}/lib"))
+(load-file (expand-file-name "ox-fronde.el" "{{ base_dir }}/lib/fronde/config/data"))
diff --git a/spec/data/config_4_org_config.el b/spec/data/config_4_org_config.el
new file mode 100644
index 0000000..fd35b56
--- /dev/null
+++ b/spec/data/config_4_org_config.el
@@ -0,0 +1,95 @@
+;; Add org-mode to load path
+(add-to-list 'load-path (expand-file-name "org-{{ org_version }}/lisp" "{{ test_dir }}/lib"))
+;; Load last version of htmlize.el
+(load-file (expand-file-name "htmlize.el" "{{ test_dir }}/lib"))
+
+;; Current project options
+(setq fronde/version "{{ version }}"
+ fronde/current-work-dir "{{ test_dir }}"
+ user-mail-address ""
+ user-full-name "Tata"
+ org-html-metadata-timestamp-format "%A %-d of %B, %Y at %R"
+ org-gmi-timestamp-format "%A %-d of %B, %Y at %R"
+ org-publish-project-alist
+ `(("src"
+ :base-directory "{{ test_dir }}/src"
+ :base-extension "org"
+ :html-head ""
+ :html-head-include-default-style t
+ :html-head-include-scripts t
+ :html-postamble "<p><span class=\"author\">Written by %a</span>
+with %c, and published with %N</p>
+<p class=\"date\">Last modification on %C</p>
+<p class=\"validation\">%v</p>"
+ :publishing-directory "{{ test_dir }}/public_html/src"
+ :publishing-function org-html-publish-to-html
+ :recursive t
+ :section-numbers nil
+ :with-toc nil)
+ ("src-assets"
+ :base-directory "{{ test_dir }}/src"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_html/src"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("news"
+ :base-directory "{{ test_dir }}/news"
+ :base-extension "org"
+ :html-head "<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
+ href=\"https://test.com/assets/my-theme/css/style.css\">
+<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
+ href=\"https://test.com/assets/my-theme/css/htmlize.css\">
+<link rel=\"alternate\" type=\"application/atom+xml\" title=\"news\"
+ href=\"https://test.com/news/feeds/index.xml\" />"
+ :html-head-include-default-style nil
+ :html-head-include-scripts nil
+ :html-postamble "<p><span class=\"author\">Written by %a</span>
+with %c, and published with %N</p>
+<p class=\"date\">Last modification on %C</p>
+<p class=\"validation\">%v</p>"
+ :publishing-directory "{{ test_dir }}/public_html/news"
+ :publishing-function org-html-publish-to-html
+ :recursive t
+ :section-numbers nil
+ :with-toc nil)
+ ("news-assets"
+ :base-directory "{{ test_dir }}/news"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_html/news"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("other"
+ :base-directory "{{ test_dir }}/other"
+ :base-extension "org"
+ :html-head "<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
+ href=\"https://test.com/assets/my-theme/css/style.css\">
+<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
+ href=\"https://test.com/assets/my-theme/css/htmlize.css\">"
+ :html-head-include-default-style nil
+ :html-head-include-scripts nil
+ :html-postamble "<p><span class=\"author\">Written by %a</span>
+with %c, and published with %N</p>
+<p class=\"date\">Last modification on %C</p>
+<p class=\"validation\">%v</p>"
+ :publishing-directory "{{ test_dir }}/public_html/other"
+ :publishing-function org-html-publish-to-html
+ :recursive t
+ :section-numbers nil
+ :with-toc nil)
+ ("other-assets"
+ :base-directory "{{ test_dir }}/other"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_html/other"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("theme-my-theme"
+ :base-directory "{{ test_dir }}/themes/my-theme"
+ :base-extension "css\\|js\\|gif\\|jpg\\|png\\|svg\\|otf\\|ttf\\|woff2?"
+ :publishing-directory "{{ test_dir }}/public_html/assets/my-theme"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("website" :components ("src" "src-assets" "news" "news-assets" "other" "other-assets" "theme-my-theme"))))
+
+;; Load fronde lib
+(load-file (expand-file-name "ox-gmi.el" "{{ test_dir }}/lib"))
+(load-file (expand-file-name "ox-fronde.el" "{{ base_dir }}/lib/fronde/config/data"))
diff --git a/spec/data/config_5_org_config.el b/spec/data/config_5_org_config.el
new file mode 100644
index 0000000..d760504
--- /dev/null
+++ b/spec/data/config_5_org_config.el
@@ -0,0 +1,69 @@
+;; Add org-mode to load path
+(add-to-list 'load-path (expand-file-name "org-{{ org_version }}/lisp" "{{ test_dir }}/lib"))
+;; Load last version of htmlize.el
+(load-file (expand-file-name "htmlize.el" "{{ test_dir }}/lib"))
+
+;; Current project options
+(setq fronde/version "{{ version }}"
+ fronde/current-work-dir "{{ test_dir }}"
+ user-mail-address ""
+ user-full-name "Tata"
+ org-html-metadata-timestamp-format "%A %-d of %B, %Y at %R"
+ org-gmi-timestamp-format "%A %-d of %B, %Y at %R"
+ org-publish-project-alist
+ `(("src"
+ :base-directory "{{ test_dir }}/src"
+ :base-extension "org"
+ :html-head "<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
+ href=\"https://test.com/assets/my-theme/css/style.css\">
+<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
+ href=\"https://test.com/assets/my-theme/css/htmlize.css\">"
+ :html-head-include-default-style nil
+ :html-head-include-scripts nil
+ :html-postamble "<p><span class=\"author\">Written by %a</span>
+with %c, and published with %N</p>
+<p class=\"date\">Last modification on %C</p>
+<p class=\"validation\">%v</p>"
+ :publishing-directory "{{ test_dir }}/public_html/src"
+ :publishing-function org-html-publish-to-html
+ :recursive t
+ :section-numbers nil
+ :with-toc nil)
+ ("src-assets"
+ :base-directory "{{ test_dir }}/src"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_html/src"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("news"
+ :base-directory "{{ test_dir }}/news"
+ :base-extension "org"
+ :html-head ""
+ :html-head-include-default-style t
+ :html-head-include-scripts t
+ :html-postamble "<p><span class=\"author\">Written by %a</span>
+with %c, and published with %N</p>
+<p class=\"date\">Last modification on %C</p>
+<p class=\"validation\">%v</p>"
+ :publishing-directory "{{ test_dir }}/public_html/news"
+ :publishing-function org-html-publish-to-html
+ :recursive t
+ :section-numbers nil
+ :with-toc nil)
+ ("news-assets"
+ :base-directory "{{ test_dir }}/news"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_html/news"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("theme-my-theme"
+ :base-directory "{{ test_dir }}/themes/my-theme"
+ :base-extension "css\\|js\\|gif\\|jpg\\|png\\|svg\\|otf\\|ttf\\|woff2?"
+ :publishing-directory "{{ test_dir }}/public_html/assets/my-theme"
+ :publishing-function org-publish-attachment
+ :recursive t)
+ ("website" :components ("src" "src-assets" "news" "news-assets" "theme-my-theme"))))
+
+;; Load fronde lib
+(load-file (expand-file-name "ox-gmi.el" "{{ test_dir }}/lib"))
+(load-file (expand-file-name "ox-fronde.el" "{{ base_dir }}/lib/fronde/config/data"))
diff --git a/spec/data/content.org b/spec/data/content.org
index 03ba67e..3ff79ed 100644
--- a/spec/data/content.org
+++ b/spec/data/content.org
@@ -1,5 +1,8 @@
#+title: My first blog post?
#+subtitle: What to do with that
+#+description: An article stating whether this is
+#+description: my first blog post or not
+#+keywords: test, tata
#+date: <2019-06-13 jeu.>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec aliquet
diff --git a/spec/data/fronde.zsh b/spec/data/fronde.zsh
new file mode 100644
index 0000000..b458ff9
--- /dev/null
+++ b/spec/data/fronde.zsh
@@ -0,0 +1,48 @@
+#compdef fronde
+#autoload
+
+
+(( $+functions[__fronde_new] )) ||
+__fronde_new(){
+ _arguments \
+ '(-a --author)'{-a,--author}':AUTHOR:' \
+ '(-l --lang)'{-l,--lang}':LOCALE:' \
+ '(-o --output)'{-o,--output}':FORMAT:' \
+ '(-t --title)'{-t,--title}':TITLE:' \
+ '(-v --verbose)'{-v,--verbose}
+}
+
+(( $+functions[__fronde_open] )) ||
+__fronde_open(){
+ _arguments \
+ '(-a --author)'{-a,--author}':AUTHOR:' \
+ '(-l --lang)'{-l,--lang}':LOCALE:' \
+ '(-t --title)'{-t,--title}':TITLE:' \
+ '(-v --verbose)'{-v,--verbose} \
+ '1:file:_files -g \*.org'
+}
+
+(( $+functions[__fronde_build] )) ||
+__fronde_build(){
+ _arguments \
+ '(-f --force)'{-f,--force}
+}
+
+
+(( $+functions[__fronde_help] )) ||
+__fronde_help(){
+ _arguments \
+ "1:command:((new\:'Initialize a new Fronde instance.' update\:'Update Fronde configuration and dependency (to be run after each modification of the config.yml file and once in a while to stay up-to-date with Org).' preview\:'Start a test web server to preview the generated website.' open\:'Open or create an org file.' build\:'Compile all org files to HTML or gemtext.' publish\:'Push local changes to the public web server.' help\:'Alias for the -h switch.' ))"
+}
+
+local state
+
+_arguments -C \
+ '(-)-h[help]' \
+ '(-)-V[version]' \
+ "1:command:((new\:'Initialize a new Fronde instance.' update\:'Update Fronde configuration and dependency (to be run after each modification of the config.yml file and once in a while to stay up-to-date with Org).' preview\:'Start a test web server to preview the generated website.' open\:'Open or create an org file.' build\:'Compile all org files to HTML or gemtext.' publish\:'Push local changes to the public web server.' help\:'Alias for the -h switch.' ))" \
+ '*::arg:->args'
+
+if [ "$state" = args ]; then
+ _call_function ret __fronde_${words[1]}
+fi
diff --git a/spec/data/index_proof.html b/spec/data/index_proof.html
index af0afb1..655688f 100644
--- a/spec/data/index_proof.html
+++ b/spec/data/index_proof.html
@@ -2,8 +2,7 @@
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-<!-- Fronde Template: cb3b496c0cce07a7bd21c1ea1a94e8c7 -->
-
+<!-- Fronde Template: 24a41b7095a9f5a1d738932f672a628c -->
<!-- __PUB_DATE__ -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
diff --git a/spec/data/org-config-blog-proof.el b/spec/data/org-config-blog-proof.el
deleted file mode 100644
index 39e4288..0000000
--- a/spec/data/org-config-blog-proof.el
+++ /dev/null
@@ -1,112 +0,0 @@
-;; Add org-mode to load path
-(add-to-list 'load-path (expand-file-name "org-__ORG_VERSION__/lisp" "__TEST_DIR__/lib"))
-;; Load last version of htmlize.el
-(load-file (expand-file-name "htmlize.el" "__TEST_DIR__/lib"))
-
-;; Current project options
-(setq fronde/version "__VERSION__"
- fronde/current-work-dir "__TEST_DIR__"
- user-mail-address ""
- user-full-name "Tata"
- org-html-metadata-timestamp-format "%A %-d of %B, %Y at %R"
- org-gmi-timestamp-format "%A %-d of %B, %Y at %R"
- org-publish-project-alist
- `(("org"
- :base-extension "org"
- :publishing-function org-html-publish-to-html
- :base-directory "__TEST_DIR__/src"
- :publishing-directory "__TEST_DIR__/public_html"
- :recursive nil
- :section-numbers nil
- :with-toc nil
- :html-postamble "<footer>Published by Fronde.</footer>"
- :html-head "<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
- href=\"/assets/toto/css/style.css\">
-<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
- href=\"/assets/toto/css/htmlize.css\">"
- :html-head-include-default-style nil
- :html-head-include-scripts nil
- :exclude "tata\.org")
- ("org-assets"
- :base-extension "jpg\\|gif\\|png\\|svg\\|pdf"
- :publishing-function org-publish-attachment
- :base-directory "__TEST_DIR__/src"
- :publishing-directory "__TEST_DIR__/public_html"
- :recursive nil)
- ("news"
- :base-extension "org"
- :publishing-function org-html-publish-to-html
- :base-directory "__TEST_DIR__/src/news"
- :publishing-directory "__TEST_DIR__/public_html/news"
- :recursive t
- :section-numbers nil
- :with-toc nil
- :html-postamble "<footer>Published by Fronde.</footer>"
- :html-head "<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
- href=\"/assets/toto/css/style.css\">
-<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
- href=\"/assets/toto/css/htmlize.css\">
-<link rel=\"alternate\" type=\"application/atom+xml\" title=\"Atom 1.0\"
- href=\"/feeds/index.xml\" />"
- :html-head-include-default-style nil
- :html-head-include-scripts nil)
- ("news-assets"
- :base-extension "jpg\\|gif\\|png\\|svg\\|pdf"
- :publishing-function org-publish-attachment
- :base-directory "__TEST_DIR__/src/news"
- :publishing-directory "__TEST_DIR__/public_html/news"
- :recursive t)
- ("test"
- :base-extension "org"
- :publishing-function org-html-publish-to-html
- :base-directory "__TEST_DIR__/titi/test"
- :publishing-directory "__TEST_DIR__/public_html/test"
- :recursive nil
- :section-numbers nil
- :with-toc nil
- :html-postamble "<footer>Published by Fronde.</footer>"
- :html-head "<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
- href=\"/assets/toto/css/style.css\">
-<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
- href=\"/assets/toto/css/htmlize.css\">"
- :html-head-include-default-style nil
- :html-head-include-scripts nil
- :exclude "ugly\.org")
- ("test-assets"
- :base-extension "jpg\\|gif\\|png\\|svg\\|pdf"
- :publishing-function org-publish-attachment
- :base-directory "__TEST_DIR__/titi/test"
- :publishing-directory "__TEST_DIR__/public_html/test"
- :recursive nil)
- ("tata"
- :base-extension "org"
- :publishing-function org-html-publish-to-html
- :base-directory "__TEST_DIR__/titi/tutu/tata"
- :publishing-directory "__TEST_DIR__/public_html/tata"
- :recursive t
- :section-numbers nil
- :with-toc nil
- :html-postamble "<footer>Published by Fronde.</footer>"
- :html-head "<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
- href=\"/assets/toto/css/style.css\">
-<link rel=\"stylesheet\" type=\"text/css\" media=\"screen\"
- href=\"/assets/toto/css/htmlize.css\">"
- :html-head-include-default-style nil
- :html-head-include-scripts nil)
- ("tata-assets"
- :base-extension "jpg\\|gif\\|png\\|svg\\|pdf"
- :publishing-function org-publish-attachment
- :base-directory "__TEST_DIR__/titi/tutu/tata"
- :publishing-directory "__TEST_DIR__/public_html/tata"
- :recursive t)
- ("theme-toto"
- :base-directory "__TEST_DIR__/themes/toto"
- :base-extension "jpg\\|gif\\|png\\|js\\|css\\|otf\\|ttf\\|woff2?"
- :recursive t
- :publishing-directory "__TEST_DIR__/public_html/assets/toto"
- :publishing-function org-publish-attachment)
- ("website" :components ("org" "org-assets" "news" "news-assets" "test" "test-assets" "tata" "tata-assets" "theme-toto"))))
-
-;; Load fronde lib
-(load-file (expand-file-name "ox-gmi.el" "__TEST_DIR__/lib"))
-(load-file (expand-file-name "ox-fronde.el" "__BASE_DIR__/lib/fronde/config"))
diff --git a/spec/data/org-config-proof.el b/spec/data/org-config-proof.el
index c908721..1d5adfb 100644
--- a/spec/data/org-config-proof.el
+++ b/spec/data/org-config-proof.el
@@ -1,93 +1,39 @@
;; Add org-mode to load path
-(add-to-list 'load-path (expand-file-name "org-__ORG_VERSION__/lisp" "__TEST_DIR__/lib"))
+(add-to-list 'load-path (expand-file-name "org-{{ org_version }}/lisp" "{{ test_dir }}/lib"))
;; Load last version of htmlize.el
-(load-file (expand-file-name "htmlize.el" "__TEST_DIR__/lib"))
+(load-file (expand-file-name "htmlize.el" "{{ test_dir }}/lib"))
;; Current project options
-(setq fronde/version "__VERSION__"
- fronde/current-work-dir "__TEST_DIR__"
+(setq fronde/version "{{ version }}"
+ fronde/current-work-dir "{{ test_dir }}"
user-mail-address ""
- user-full-name "Tata"
+ user-full-name "alice"
org-html-metadata-timestamp-format "%A %-d of %B, %Y at %R"
org-gmi-timestamp-format "%A %-d of %B, %Y at %R"
org-publish-project-alist
- `(("org"
+ `(("src"
+ :base-directory "{{ test_dir }}/src"
:base-extension "org"
- :publishing-function org-html-publish-to-html
- :base-directory "__TEST_DIR__/src"
- :publishing-directory "__TEST_DIR__/public_html"
- :recursive nil
- :section-numbers nil
- :with-toc nil
- :html-postamble "<footer>Published by Fronde.</footer>"
- :html-head ""
- :html-head-include-default-style t
- :html-head-include-scripts t
- :exclude "tata\.org")
- ("org-assets"
- :base-extension "jpg\\|gif\\|png\\|svg\\|pdf"
- :publishing-function org-publish-attachment
- :base-directory "__TEST_DIR__/src"
- :publishing-directory "__TEST_DIR__/public_html"
- :recursive nil)
- ("news"
- :base-extension "org"
- :publishing-function org-html-publish-to-html
- :base-directory "__TEST_DIR__/src/news"
- :publishing-directory "__TEST_DIR__/public_html/news"
- :recursive t
- :section-numbers nil
- :with-toc nil
- :html-postamble "<footer>Published by Fronde.</footer>"
- :html-head "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"Atom 1.0\"
- href=\"/feeds/index.xml\" />"
- :html-head-include-default-style t
- :html-head-include-scripts t)
- ("news-assets"
- :base-extension "jpg\\|gif\\|png\\|svg\\|pdf"
- :publishing-function org-publish-attachment
- :base-directory "__TEST_DIR__/src/news"
- :publishing-directory "__TEST_DIR__/public_html/news"
- :recursive t)
- ("test"
- :base-extension "org"
- :publishing-function org-html-publish-to-html
- :base-directory "__TEST_DIR__/titi/test"
- :publishing-directory "__TEST_DIR__/public_html/test"
- :recursive nil
- :section-numbers nil
- :with-toc nil
- :html-postamble "<footer>Published by Fronde.</footer>"
:html-head ""
:html-head-include-default-style t
:html-head-include-scripts t
- :exclude "ugly\.org")
- ("test-assets"
- :base-extension "jpg\\|gif\\|png\\|svg\\|pdf"
- :publishing-function org-publish-attachment
- :base-directory "__TEST_DIR__/titi/test"
- :publishing-directory "__TEST_DIR__/public_html/test"
- :recursive nil)
- ("tata"
- :base-extension "org"
+ :html-postamble "<p><span class=\"author\">Written by %a</span>
+with %c, and published with %N</p>
+<p class=\"date\">Last modification on %C</p>
+<p class=\"validation\">%v</p>"
+ :publishing-directory "{{ test_dir }}/public_html"
:publishing-function org-html-publish-to-html
- :base-directory "__TEST_DIR__/titi/tutu/tata"
- :publishing-directory "__TEST_DIR__/public_html/tata"
:recursive t
:section-numbers nil
- :with-toc nil
- :html-postamble "<footer>Published by Fronde.</footer>"
- :html-head ""
- :html-head-include-default-style t
- :html-head-include-scripts t)
- ("tata-assets"
- :base-extension "jpg\\|gif\\|png\\|svg\\|pdf"
+ :with-toc nil)
+ ("src-assets"
+ :base-directory "{{ test_dir }}/src"
+ :base-extension "gif\\|jpg\\|png\\|svg\\|pdf"
+ :publishing-directory "{{ test_dir }}/public_html"
:publishing-function org-publish-attachment
- :base-directory "__TEST_DIR__/titi/tutu/tata"
- :publishing-directory "__TEST_DIR__/public_html/tata"
:recursive t)
- ("website" :components ("org" "org-assets" "news" "news-assets" "test" "test-assets" "tata" "tata-assets"))))
+ ("website" :components ("src" "src-assets"))))
;; Load fronde lib
-(load-file (expand-file-name "ox-gmi.el" "__TEST_DIR__/lib"))
-(load-file (expand-file-name "ox-fronde.el" "__BASE_DIR__/lib/fronde/config"))
+(load-file (expand-file-name "ox-gmi.el" "{{ test_dir }}/lib"))
+(load-file (expand-file-name "ox-fronde.el" "{{ base_dir }}/lib/fronde/config/data"))
diff --git a/spec/data/test4.org b/spec/data/test4.org
index 95ed2e8..208782e 100644
--- a/spec/data/test4.org
+++ b/spec/data/test4.org
@@ -1,5 +1,4 @@
#+title: Fourth test
-#+keywords: tutu
#+date: <2019-07-25 jeu. 20:45>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec aliquet
diff --git a/spec/fronde/cli/opt_parse_spec.rb b/spec/fronde/cli/opt_parse_spec.rb
new file mode 100644
index 0000000..d9a02f4
--- /dev/null
+++ b/spec/fronde/cli/opt_parse_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require_relative '../../../lib/fronde/cli/opt_parse'
+
+BASIC_CMDS = [
+ ' new Initialize a new Fronde instance.',
+ ' init Alias for ‘new’.',
+ ' update Update Fronde configuration and dependency (to be run after each modification of the config.yml file and once in a while to stay up-to-date with Org).',
+ ' config Alias for ‘update’.',
+ ' preview Start a test web server to preview the generated website.',
+ ' open Open or create an org file.',
+ ' edit Alias for ‘open’.',
+ ' build Compile all org files to HTML or gemtext.',
+ ' publish Push local changes to the public web server.',
+ ' help Alias for the -h switch.'
+].join("\n")
+
+BASIC_OPTS = [
+ " -h, --help Display help for a command and exit.\n",
+ ' -V, --version Display Fronde version and exit.'
+].join
+
+NEW_OPTS = [
+ ' -a, --author AUTHOR ',
+ ' -l, --lang LOCALE ',
+ ' -o, --output FORMAT (gemini, html)',
+ ' -t, --title TITLE ',
+ ' -v, --verbose '
+].join("\n")
+
+describe Fronde::CLI::OptParse do
+ context 'with theoritical fronde arguments' do
+ it 'resolves alias' do
+ expect(described_class.resolve_possible_alias('new')).to eq('new')
+ expect(described_class.resolve_possible_alias('init')).to eq('new')
+ expect(described_class.resolve_possible_alias('update')).to eq('update')
+ expect(described_class.resolve_possible_alias('config')).to eq('update')
+ expect(described_class.resolve_possible_alias('build')).to eq('build')
+ expect(described_class.resolve_possible_alias('edit')).to eq('open')
+ expect(described_class.resolve_possible_alias('wrong')).to eq('basic')
+ end
+
+ it 'returns possible options for a command' do
+ init_opts = described_class.command_options('new')
+ expect(init_opts).to have_key(:name)
+ expect(init_opts[:name]).to eq('new')
+ expect(init_opts).to have_key(:opts)
+ expect(init_opts[:opts]).to contain_exactly('-a', '-l', '-o', '-t', '-v')
+ expect(init_opts).not_to have_key(:alias)
+
+ config_opts = described_class.command_options('edit')
+ expect(config_opts).to have_key(:name)
+ expect(config_opts[:name]).to eq('open')
+ expect(config_opts).to have_key(:opts)
+ expect(config_opts[:opts]).to contain_exactly('-a', '-l', '-t', '-v')
+ expect(config_opts).not_to have_key(:alias)
+ end
+
+ it 'returns decorated options' do
+ expect(described_class.decorate_option('-a')).to(
+ eq(['-a', '--author AUTHOR'])
+ )
+ expect(described_class.decorate_option('-l')).to(
+ eq(['-l', '--lang LOCALE'])
+ )
+ expect(described_class.decorate_option('-v')).to(
+ eq(['-v', '--verbose'])
+ )
+ expect(described_class.decorate_option('-o')).to(
+ eq(['-o', '--output FORMAT', %w[gemini html]])
+ )
+ end
+
+ it 'lists possible commands' do
+ expect(described_class.list_commands).to eq(BASIC_CMDS)
+ end
+
+ it 'summarizes basic command' do
+ expect(described_class.summarize_command('basic')).to eq(BASIC_OPTS)
+ end
+
+ it 'summarizes new command' do
+ expect(described_class.summarize_command('new')).to eq(NEW_OPTS)
+ end
+
+ it 'summarizes open command' do
+ open_opts = [
+ ' -a, --author AUTHOR ',
+ ' -l, --lang LOCALE ',
+ ' -t, --title TITLE ',
+ ' -v, --verbose '
+ ].join("\n")
+ expect(described_class.summarize_command('open')).to eq(open_opts)
+ end
+
+ it 'displays help for basic command' do
+ help = ['Options', BASIC_OPTS, "\nCommands", BASIC_CMDS].join("\n")
+ expect(described_class.help_command_body('basic')).to eq help
+ end
+
+ it 'displays help for other commands' do
+ expect(described_class.help_command_body('new')).to(
+ eq "Options\n#{NEW_OPTS}"
+ )
+ expect(described_class.help_command_body('update')).to eq ''
+ end
+ end
+end
diff --git a/spec/fronde/cli/throbber_spec.rb b/spec/fronde/cli/throbber_spec.rb
new file mode 100644
index 0000000..8db2705
--- /dev/null
+++ b/spec/fronde/cli/throbber_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require_relative '../../../lib/fronde/cli/throbber'
+
+describe Fronde::CLI::Throbber do
+ let(:thread) { Thread.new { next } }
+
+ before { Fronde::CONFIG.reset }
+
+ it 'selects the right throbber' do
+ throbber = described_class.new(thread, 'Test')
+ expect(throbber.instance_variable_get(:@frames)).to(
+ eq(['⠁ ⠂ ⠄ ⡀ ⠄ ⠂ ⠁', '⠂ ⠁ ⠂ ⠄ ⡀ ⠄ ⠂', '⠄ ⠂ ⠁ ⠂ ⠄ ⡀ ⠄',
+ '⡀ ⠄ ⠂ ⠁ ⠂ ⠄ ⡀', '⠄ ⡀ ⠄ ⠂ ⠁ ⠂ ⠄', '⠂ ⠄ ⡀ ⠄ ⠂ ⠁ ⠂'])
+ )
+
+ Fronde::CONFIG.load_test('throbber' => 'default')
+ throbber = described_class.new(thread, 'Test')
+ expect(throbber.instance_variable_get(:@frames)).to(
+ eq(['⠁ ⠂ ⠄ ⡀ ⠄ ⠂ ⠁', '⠂ ⠁ ⠂ ⠄ ⡀ ⠄ ⠂', '⠄ ⠂ ⠁ ⠂ ⠄ ⡀ ⠄',
+ '⡀ ⠄ ⠂ ⠁ ⠂ ⠄ ⡀', '⠄ ⡀ ⠄ ⠂ ⠁ ⠂ ⠄', '⠂ ⠄ ⡀ ⠄ ⠂ ⠁ ⠂'])
+ )
+
+ Fronde::CONFIG.load_test('throbber' => 'wrong')
+ throbber = described_class.new(thread, 'Test')
+ expect(throbber.instance_variable_get(:@frames)).to(
+ eq(['⠁ ⠂ ⠄ ⡀ ⠄ ⠂ ⠁', '⠂ ⠁ ⠂ ⠄ ⡀ ⠄ ⠂', '⠄ ⠂ ⠁ ⠂ ⠄ ⡀ ⠄',
+ '⡀ ⠄ ⠂ ⠁ ⠂ ⠄ ⡀', '⠄ ⡀ ⠄ ⠂ ⠁ ⠂ ⠄', '⠂ ⠄ ⡀ ⠄ ⠂ ⠁ ⠂'])
+ )
+
+ Fronde::CONFIG.load_test('throbber' => 'basic')
+ throbber = described_class.new(thread, 'Test')
+ expect(throbber.instance_variable_get(:@frames)).to eq('-\|/')
+ end
+
+ it 'always compute a terminal size' do
+ tests = [[true, 79], [false, 0], [nil, 0]]
+ throbber = described_class.new(thread, 'Test')
+
+ allow(throbber).to receive(:`).and_return('24 80')
+ tests.each do |data|
+ allow(throbber).to receive(:system).with('test -t 0').and_return(data[0])
+ expect(throbber.send(:terminal_width)).to eq data[1]
+ end
+ end
+end
diff --git a/spec/fronde/config_spec.rb b/spec/fronde/config_spec.rb
index 98f4676..9bd12ef 100644
--- a/spec/fronde/config_spec.rb
+++ b/spec/fronde/config_spec.rb
@@ -3,22 +3,20 @@
SAMPLE_CONFIG = <<~CONF
---
author: Tata
- title: This is a website about test
CONF
SAMPLE_CONFIG_2 = <<~CONF
---
author: Titi
- title: Nevermind
domain: https://tutu.com
CONF
SAMPLE_CONFIG_3 = <<~CONF
---
author: Tata
- title: This is a website with a blog
sources:
- - src
+ - path: src
+ recursive: no
- path: src/news
is_blog: true
- wrong: source
@@ -27,7 +25,6 @@ CONF
SAMPLE_CONFIG_4 = <<~CONF
---
author: Tata
- title: This is a website with a blog
domain: https://test.com
sources:
- src
@@ -41,7 +38,6 @@ CONF
SAMPLE_CONFIG_5 = <<~CONF
---
author: Tata
- title: This is a website with a blog
domain: https://test.com
theme: my-theme
sources:
@@ -50,12 +46,70 @@ SAMPLE_CONFIG_5 = <<~CONF
theme: default
CONF
-describe Fronde::Config do
+describe Fronde::CONFIG do
+ context 'without a config file' do
+ before do
+ FileUtils.mkdir_p 'tmp/no_config'
+ Dir.chdir 'tmp/no_config'
+ described_class.reset
+ end
+
+ after do
+ tear_down 'tmp/no_config'
+ end
+
+ it 'uses default config' do
+ conf = described_class.settings
+ author = ENV['USER'] || ''
+ expect(conf['author']).to eq(author)
+ expect(conf['lang']).to eq('en') # Defined in ENV in requirements.rb
+ expect(conf['html_public_folder']).to eq(File.expand_path('public_html'))
+ expect(conf['domain']).to eq('')
+ end
+
+ it 'warns about duplicate sources' do
+ config = {
+ 'sources' => [
+ { 'path' => 'src', 'type' => 'html' },
+ { 'path' => 'src', 'type' => 'html' }
+ ]
+ }
+ expect { described_class.load_test(config) }.to(
+ output(/Skipping src as it appears at least twice/).to_stderr
+ )
+ end
+
+ it 'warns about embedded sources' do
+ config = {
+ 'sources' => [
+ { 'path' => 'src/test', 'type' => 'html' },
+ { 'path' => 'src', 'type' => 'html' },
+ { 'path' => 'src/other_test', 'type' => 'html' } # for coverage
+ ]
+ }
+ expect { described_class.load_test(config) }.to(
+ output(
+ /Skipping src\/test as it might be already embedded into the other source/
+ ).to_stderr
+ )
+ end
+
+ it 'does not warn about embedded sources when no recursive' do
+ config = {
+ 'sources' => [
+ { 'path' => 'src', 'type' => 'html', 'recursive' => false },
+ { 'path' => 'src/test', 'type' => 'html' }
+ ]
+ }
+ expect { described_class.load_test(config) }.to output('').to_stderr
+ end
+ end
+
context 'with a config file' do
before do
FileUtils.mkdir_p 'tmp/config'
- File.write('tmp/config/config.yml', SAMPLE_CONFIG)
Dir.chdir 'tmp/config'
+ File.write('config.yml', SAMPLE_CONFIG)
described_class.reset
end
@@ -64,33 +118,48 @@ describe Fronde::Config do
end
it 'parses it successfully' do
- # Be sure to unset @config to force parsing
- described_class.class_eval '@config = nil', __FILE__, __LINE__
conf = described_class.settings
expect(conf['author']).to eq('Tata')
- expect(conf['title']).to eq('This is a website about test')
expect(conf['lang']).to eq('en')
expect(conf['theme']).to eq('default')
expect(conf['domain']).to eq('')
end
- it 'saves it successfully' do
- described_class.save('author' => 'Titi', 'title' => 'Nevermind',
- 'domain' => 'https://tutu.com')
- expect(File.read('config.yml')).to eq(SAMPLE_CONFIG_2)
+ it 'runs migrations' do
+ config_with_migrations_to_do = <<~CONF
+ ---
+ public_folder: public
+ CONF
+ File.write('config.yml', config_with_migrations_to_do)
+ expect { described_class.reset }.to(
+ output(/‘public_folder’ setting is deprecated./).to_stderr
+ )
+ conf = described_class.settings
+ expect(conf['public_folder']).to be_nil
+ expect(conf['html_public_folder']).to eq File.expand_path('public')
+ end
+
+ it 'cleans config file if needed' do
+ config_with_migrations_to_do = <<~CONF
+ ---
+ public_folder: old_public
+ html_public_folder: new_public
+ CONF
+ File.write('config.yml', config_with_migrations_to_do)
+ expect { described_class.reset }.to(
+ output(/‘public_folder’ setting is deprecated./).to_stderr
+ )
conf = described_class.settings
- expect(conf['author']).to eq('Titi')
- expect(conf['title']).to eq('Nevermind')
- expect(conf['lang']).to eq('en')
- expect(conf['theme']).to eq('default')
- expect(conf['domain']).to eq('https://tutu.com')
+ expect(conf['public_folder']).to be_nil
+ expect(conf['html_public_folder']).to eq File.expand_path('new_public')
end
end
- context 'with a blog config file' do
+ context 'with a blog config file (config 3)' do
before do
FileUtils.mkdir_p 'tmp/config'
Dir.chdir 'tmp/config'
+ File.write('config.yml', SAMPLE_CONFIG_3)
described_class.reset
end
@@ -99,8 +168,9 @@ describe Fronde::Config do
end
it 'lists sources' do
- File.write('config.yml', SAMPLE_CONFIG_3)
- projects = described_class.sources
+ # for coverage testing, should not load sources twice and actually
+ # returns the already loaded sources
+ projects = described_class.load_sources
expect(projects.length).to eq(2)
expect(projects[0]['name']).to eq('src')
expect(projects[0]['path']).to(
@@ -108,276 +178,95 @@ describe Fronde::Config do
)
expect(projects[0]['target']).to eq('src')
expect(projects[0]['is_blog']).to be(false)
- expect(projects[0]['recursive']).to be(true)
- expect(projects[1]['name']).to eq('news')
+ expect(projects[0].blog?).to be(false)
+ expect(projects[0]['recursive']).to be(false)
+
+ expect(projects[1]['name']).to eq('src-news')
expect(projects[1]['path']).to(
eq(File.expand_path('src/news'))
)
expect(projects[1]['target']).to eq('news')
expect(projects[1]['is_blog']).to be(true)
+ expect(projects[1].blog?).to be(true)
expect(projects[1]['recursive']).to be(true)
end
- it 'generates projects hash' do
- File.write('config.yml', SAMPLE_CONFIG_3)
- described_class.reset
- projects = described_class.send(:org_generate_projects)
- expect(projects).to have_key('news')
- end
-
- it 'exposes correct head header' do
- File.write('config.yml', SAMPLE_CONFIG_3)
- described_class.reset
- projects = described_class.sources
- headers = described_class.send(:build_project_org_headers, projects[0])
- head = <<~HEAD.strip
- :section-numbers nil
- :with-toc nil
- :html-postamble "<p><span class=\\\"author\\\">Written by %a</span>
- with %c, and published with %N</p>
- <p class=\\\"date\\\">Last modification on %C</p>
- <p class=\\\"validation\\\">%v</p>"
- :html-head ""
- :html-head-include-default-style t
- :html-head-include-scripts t
- HEAD
- expect(headers.join("\n")).to eq(head)
+ it 'generates lisp-config.el' do
+ described_class.write_org_lisp_config
+ lisp_config = File.expand_path('var/lib/org-config.el')
+ expect(File.file?(lisp_config)).to be(true)
+ proof = proof_content('config_3_org_config.el')
+ expect(File.read(lisp_config)).to eq(proof)
end
- it 'exposes correct head header with boolean values in conf' do
- File.write('config.yml', SAMPLE_CONFIG_3)
- described_class.reset
- old_conf = described_class.settings.dup
+ it 'generates lisp-config.el for gemini projects' do
+ old_conf = described_class.settings.merge
old_conf['sources'][0] = {
'path' => 'src',
- 'org-html' => {
- 'html-head-include-default-style' => true,
- 'html-head-include-scripts' => false
- }
+ 'recursive' => false,
+ 'type' => 'gemini'
}
described_class.load_test(old_conf)
projects = described_class.sources
- headers = described_class.send(:build_project_org_headers, projects[0])
- head = <<~HEAD.strip
- :section-numbers nil
- :with-toc nil
- :html-postamble "<p><span class=\\\"author\\\">Written by %a</span>
- with %c, and published with %N</p>
- <p class=\\\"date\\\">Last modification on %C</p>
- <p class=\\\"validation\\\">%v</p>"
- :html-head ""
- :html-head-include-default-style t
- :html-head-include-scripts nil
- HEAD
- expect(headers.join("\n")).to eq(head)
+ expect(projects[0]['type']).to eq('gemini')
+ described_class.write_org_lisp_config
+ proof = proof_content('config_3_gemini_org_config.el')
+ lisp_config = File.expand_path('var/lib/org-config.el')
+ expect(File.read(lisp_config)).to eq(proof)
end
+ end
- it 'exposes correct head header with custom domain' do
+ context 'with a blog and a custom domain (config 4)' do
+ before do
+ FileUtils.mkdir_p 'tmp/config'
+ Dir.chdir 'tmp/config'
File.write('config.yml', SAMPLE_CONFIG_4)
described_class.reset
- projects = described_class.sources
- headers = described_class.send(:build_project_org_headers, projects[0])
- head = <<~HEAD.strip
- :section-numbers nil
- :with-toc nil
- :html-postamble "<p><span class=\\\"author\\\">Written by %a</span>
- with %c, and published with %N</p>
- <p class=\\\"date\\\">Last modification on %C</p>
- <p class=\\\"validation\\\">%v</p>"
- :html-head ""
- :html-head-include-default-style t
- :html-head-include-scripts t
- HEAD
- expect(headers.join("\n")).to eq(head)
- headers = described_class.send(:build_project_org_headers, projects[1])
- head = <<~HEAD.strip
- :section-numbers nil
- :with-toc nil
- :html-postamble "<p><span class=\\\"author\\\">Written by %a</span>
- with %c, and published with %N</p>
- <p class=\\\"date\\\">Last modification on %C</p>
- <p class=\\\"validation\\\">%v</p>"
- :html-head "<link rel=\\\"stylesheet\\\" type=\\\"text/css\\\" media=\\\"screen\\\"
- href=\\\"https://test.com/assets/my-theme/css/style.css\\\">
- <link rel=\\\"stylesheet\\\" type=\\\"text/css\\\" media=\\\"screen\\\"
- href=\\\"https://test.com/assets/my-theme/css/htmlize.css\\\">
- <link rel=\\\"alternate\\\" type=\\\"application/atom+xml\\\" title=\\\"Atom 1.0\\\"
- href=\\\"https://test.com/feeds/index.xml\\\" />"
- :html-head-include-default-style nil
- :html-head-include-scripts nil
- HEAD
- expect(headers.join("\n")).to eq(head)
end
- it 'exposes correct head header for gemini projects' do
- File.write('config.yml', SAMPLE_CONFIG_3)
- described_class.reset
- old_conf = described_class.settings.dup
- old_conf['sources'][0] = { 'path' => 'src', 'type' => 'gemini' }
- described_class.load_test(old_conf)
- projects = described_class.sources
- headers = described_class.send(:build_project_org_headers, projects[0])
- head = <<~HEAD.strip
- :section-numbers nil
- :with-toc nil
- :gemini-postamble "📅 Last modification on %C
- 📝 Written by %a with %c, and published with %n"
- HEAD
- expect(headers.join("\n")).to eq(head)
+ after do
+ tear_down 'tmp/config'
end
- it 'generates projects' do
- File.write('config.yml', SAMPLE_CONFIG_4)
- described_class.reset
- projects = described_class.send(:org_generate_projects)
- srcconf = <<~SRCCONF.strip
- ("src"
- :base-extension "org"
- :publishing-function org-html-publish-to-html
- :base-directory "#{Dir.pwd}/src"
- :publishing-directory "#{Dir.pwd}/public_html/src"
- :recursive t
- :section-numbers nil
- :with-toc nil
- :html-postamble "<p><span class=\\\"author\\\">Written by %a</span>
- with %c, and published with %N</p>
- <p class=\\\"date\\\">Last modification on %C</p>
- <p class=\\\"validation\\\">%v</p>"
- :html-head ""
- :html-head-include-default-style t
- :html-head-include-scripts t)
- ("src-assets"
- :base-extension "jpg\\\\\\|gif\\\\\\|png\\\\\\|svg\\\\\\|pdf"
- :publishing-function org-publish-attachment
- :base-directory "#{Dir.pwd}/src"
- :publishing-directory "#{Dir.pwd}/public_html/src"
- :recursive t)
- SRCCONF
- expect(projects).to have_key('src')
- expect(projects['src']).to eq(srcconf)
-
- blogconf = <<~BLOGCONF.strip
- ("news"
- :base-extension "org"
- :publishing-function org-html-publish-to-html
- :base-directory "#{Dir.pwd}/news"
- :publishing-directory "#{Dir.pwd}/public_html/news"
- :recursive t
- :section-numbers nil
- :with-toc nil
- :html-postamble "<p><span class=\\\"author\\\">Written by %a</span>
- with %c, and published with %N</p>
- <p class=\\\"date\\\">Last modification on %C</p>
- <p class=\\\"validation\\\">%v</p>"
- :html-head "<link rel=\\\"stylesheet\\\" type=\\\"text/css\\\" media=\\\"screen\\\"
- href=\\\"https://test.com/assets/my-theme/css/style.css\\\">
- <link rel=\\\"stylesheet\\\" type=\\\"text/css\\\" media=\\\"screen\\\"
- href=\\\"https://test.com/assets/my-theme/css/htmlize.css\\\">
- <link rel=\\\"alternate\\\" type=\\\"application/atom+xml\\\" title=\\\"Atom 1.0\\\"
- href=\\\"https://test.com/feeds/index.xml\\\" />"
- :html-head-include-default-style nil
- :html-head-include-scripts nil)
- ("news-assets"
- :base-extension "jpg\\\\\\|gif\\\\\\|png\\\\\\|svg\\\\\\|pdf"
- :publishing-function org-publish-attachment
- :base-directory "#{Dir.pwd}/news"
- :publishing-directory "#{Dir.pwd}/public_html/news"
- :recursive t)
- ("theme-my-theme"
- :base-directory "#{Dir.pwd}/themes/my-theme"
- :base-extension "jpg\\\\\\|gif\\\\\\|png\\\\\\|js\\\\\\|css\\\\\\|otf\\\\\\|ttf\\\\\\|woff2?"
- :recursive t
- :publishing-directory "#{Dir.pwd}/public_html/assets/my-theme"
- :publishing-function org-publish-attachment)
- BLOGCONF
- expect(projects).to have_key('news')
- expect(projects['news']).to eq(blogconf)
+ it 'generates lisp-config.el' do
+ described_class.write_org_lisp_config
+ lisp_config = File.expand_path('var/lib/org-config.el')
+ expect(File.file?(lisp_config)).to be(true)
+ proof = proof_content('config_4_org_config.el')
+ expect(File.read(lisp_config)).to eq(proof)
end
- it 'generates gemini projects' do
- File.write('config.yml', SAMPLE_CONFIG_4)
- described_class.reset
- old_conf = described_class.settings.dup
+ it 'generates lisp-config.el for gemini projects' do
+ old_conf = described_class.settings.merge
old_conf['sources'][0] = { 'path' => 'src', 'type' => 'gemini' }
described_class.load_test(old_conf)
- projects = described_class.send(:org_generate_projects)
- srcconf = <<~SRCCONF.strip
- ("src"
- :base-extension "org"
- :publishing-function org-gmi-publish-to-gemini
- :base-directory "#{Dir.pwd}/src"
- :publishing-directory "#{Dir.pwd}/public_gmi/src"
- :recursive t
- :section-numbers nil
- :with-toc nil
- :gemini-postamble "📅 Last modification on %C
- 📝 Written by %a with %c, and published with %n")
- ("src-assets"
- :base-extension "jpg\\\\\\|gif\\\\\\|png\\\\\\|svg\\\\\\|pdf"
- :publishing-function org-publish-attachment
- :base-directory "#{Dir.pwd}/src"
- :publishing-directory "#{Dir.pwd}/public_gmi/src"
- :recursive t)
- SRCCONF
- expect(projects).to have_key('src')
- expect(projects['src']).to eq(srcconf)
- end
-
- it 'generates projects names list' do
- File.write('config.yml', SAMPLE_CONFIG_4)
- described_class.reset
- projects = described_class.send(:org_generate_projects)
- expect(described_class.send(:project_names, projects)).to(
- eq('"src" "src-assets" "news" "news-assets" "other" "other-assets" "theme-my-theme"')
- )
- end
-
- it 'generates projects names list without default' do
- File.write('config.yml', SAMPLE_CONFIG_5)
- described_class.reset
- projects = described_class.send(:org_generate_projects)
- expect(described_class.send(:project_names, projects)).to(
- eq('"src" "src-assets" "news" "news-assets" "theme-my-theme"')
- )
+ projects = described_class.sources
+ expect(projects[0]['type']).to eq('gemini')
+ described_class.write_org_lisp_config
+ proof = proof_content('config_4_gemini_org_config.el')
+ lisp_config = File.expand_path('var/lib/org-config.el')
+ expect(File.read(lisp_config)).to eq(proof)
end
end
- context 'without a config file' do
+ context 'without a blog and with default theme (config 5)' do
before do
- FileUtils.mkdir_p 'tmp/config2'
- Dir.chdir 'tmp/config2'
+ FileUtils.mkdir_p 'tmp/config'
+ Dir.chdir 'tmp/config'
+ File.write('config.yml', SAMPLE_CONFIG_5)
described_class.reset
end
after do
- tear_down 'tmp/config2'
- end
-
- it 'uses default config' do
- conf = described_class.settings
- author = ENV['USER'] || ''
- expect(conf['author']).to eq(author)
- expect(conf['lang']).to eq('en') # Defined in ENV in requirements.rb
- expect(conf['public_folder']).to eq('public_html')
- expect(conf['domain']).to eq('')
- end
-
- it 'handles weird case' do
- expect(described_class.send(:build_source, 'name' => 'wrong')).to be(nil)
- expect(described_class.send(:build_source, :test)).to be(nil)
- end
- end
-
- context 'with config utils method' do
- after do
- FileUtils.rm 'var/tmp/last_org_version', force: true
- described_class.class_eval '@org_version = nil', __FILE__, __LINE__
+ tear_down 'tmp/config'
end
- it 'gives current org version' do
- described_class.class_eval '@org_version = nil', __FILE__, __LINE__
- File.write('var/tmp/last_org_version', 'test')
- expect(described_class.org_last_version).to eq('test')
+ it 'generates lisp-config.el' do
+ described_class.write_org_lisp_config
+ lisp_config = File.expand_path('var/lib/org-config.el')
+ expect(File.file?(lisp_config)).to be(true)
+ proof = proof_content('config_5_org_config.el')
+ expect(File.read(lisp_config)).to eq(proof)
end
end
end
diff --git a/spec/fronde/index_spec.rb b/spec/fronde/index_spec.rb
index 9d2547e..ccea502 100644
--- a/spec/fronde/index_spec.rb
+++ b/spec/fronde/index_spec.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
-SAMPLE_ALL_INDEX = <<~INDEX.strip
+require_relative '../../lib/fronde/index'
+
+SAMPLE_ALL_INDEX = <<~INDEX
#+title: All tags
#+author: Test
#+language: en
@@ -24,8 +26,8 @@ SAMPLE_ALL_INDEX = <<~INDEX.strip
- [[http://perdu.com/tags/toto.html][toto]] (1)
INDEX
-SAMPLE_PROJECT_INDEX = <<~BLOG_IDX.strip
- #+title: writings
+SAMPLE_PROJECT_INDEX = <<~BLOG_IDX
+ #+title: Blog
#+author: Test
#+language: en
@@ -48,9 +50,9 @@ SAMPLE_PROJECT_INDEX = <<~BLOG_IDX.strip
- *[[http://perdu.com/test1.html][My sweet article]]*
BLOG_IDX
-SAMPLE_NO_REC_INDEX = <<~INDEX.strip
+SAMPLE_EMPTY_INDEX = <<~INDEX
#+title: All tags
- #+author: Test
+ #+author: %<author>s
#+language: en
* By alphabetical order
@@ -59,7 +61,6 @@ SAMPLE_NO_REC_INDEX = <<~INDEX.strip
:UNNUMBERED: notoc
:END:
- - [[http://perdu.com/tags/tutu.html][tutu]] (1)
* By publication number
:PROPERTIES:
@@ -67,415 +68,323 @@ SAMPLE_NO_REC_INDEX = <<~INDEX.strip
:UNNUMBERED: notoc
:END:
- - [[http://perdu.com/tags/tutu.html][tutu]] (1)
INDEX
-SAMPLE_PROJECT_NO_REC_INDEX = <<~BLOG_IDX.strip
- #+title: writings
+SAMPLE_EMPTY_PROJECT_INDEX = <<~INDEX
+ #+title: Blog
#+author: Test
#+language: en
+INDEX
- * 2019
- :PROPERTIES:
- :HTML_CONTAINER_CLASS: index-year
- :UNNUMBERED: notoc
- :END:
-
- - *[[http://perdu.com/test2.html][My second article]]* / Published on Tuesday 11th of June \\\\
- Lorem ipsum
-
- * Unsorted
- :PROPERTIES:
- :HTML_CONTAINER_CLASS: index-year
- :UNNUMBERED: notoc
- :END:
+SAMPLE_ATOM = <<~ATOM
+ <?xml version="1.0" encoding="utf-8"?>
+ <feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xml:lang="en">
- - *[[http://perdu.com/test1.html][My sweet article]]*
-BLOG_IDX
+ <title>Blog</title>
+ <link href="http://perdu.com/feeds/index.xml" rel="self" type="application/atom+xml"/>
+ <link href="http://perdu.com" rel="alternate" type="text/html" title="Blog"/>
+ <updated>%<date>s</updated>
+ <author><name>Test</name></author>
+ <id>urn:md5:75d53866bcb20465b3287cf237234464</id>
+ <generator uri="https://git.umaneti.net/fronde/about/">Fronde</generator>
+
+ <entry>
+ <title>My third article</title>
+ <link href="http://perdu.com/other/test3.html" rel="alternate"
+ type="text/html"
+ title="My third article"/>
+ <id>urn:md5:8865383febd94ddf9df318267af5ae85</id>
+ <published>2019-06-11T23:42:10%<expected_tz>s</published>
+ <updated>%<mtime>s</updated>
+ <author><name>Test</name></author>
+ <dc:subject>toto</dc:subject>
+ <dc:subject>tutu</dc:subject>
+ <content type="html"></content>
+ </entry>
+
+ <entry>
+ <title>My second article</title>
+ <link href="http://perdu.com/test2.html" rel="alternate"
+ type="text/html"
+ title="My second article"/>
+ <id>urn:md5:123104bd8bb4c61e02a1e2a136e2fd6b</id>
+ <published>2019-06-11T00:00:00%<expected_tz>s</published>
+ <updated>%<mtime>s</updated>
+ <author><name>Titi</name></author>
+ <content type="html">Lorem ipsum</content>
+ </entry>
+
+ <entry>
+ <title>My sweet article</title>
+ <link href="http://perdu.com/test1.html" rel="alternate"
+ type="text/html"
+ title="My sweet article"/>
+ <id>urn:md5:c47532bbb1e2883c902071591ae1ec9b</id>
+ <published></published>
+ <updated>%<mtime>s</updated>
+ <author><name>Test</name></author>
+ <dc:subject>tutu</dc:subject>
+ <content type="html"></content>
+ </entry>
+ </feed>
+ATOM
-SAMPLE_ATOM = <<~ATOM.strip
+SAMPLE_EMPTY_ATOM = <<~ATOM
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xml:lang="en">
- <title>Blog</title>
- <link href="http://perdu.com/feeds/index.xml" rel="self" type="application/atom+xml"/>
- <link href="http://perdu.com" rel="alternate" type="text/html" title="Blog"/>
- <updated>%<date>s</updated>
- <author><name>Test</name></author>
- <id>urn:md5:75d53866bcb20465b3287cf237234464</id>
- <generator uri="https://git.umaneti.net/fronde/about/">Fronde</generator>
-
- <entry>
- <title>My third article</title>
- <link href="http://perdu.com/other/test3.html" rel="alternate" type="text/html"
- title="My third article"/>
- <id>urn:md5:8865383febd94ddf9df318267af5ae85</id>
- <published>2019-06-11T23:42:10+00:00</published>
- <author><name>Test</name></author>
- <dc:subject>toto</dc:subject><dc:subject>tutu</dc:subject>
- <content type="html"></content>
- </entry>
-
- <entry>
- <title>My second article</title>
- <link href="http://perdu.com/test2.html" rel="alternate" type="text/html"
- title="My second article"/>
- <id>urn:md5:123104bd8bb4c61e02a1e2a136e2fd6b</id>
- <published>2019-06-11T00:00:00+00:00</published>
- <author><name>Titi</name></author>
- <content type="html">Lorem ipsum</content>
- </entry>
-
- <entry>
- <title>My sweet article</title>
- <link href="http://perdu.com/test1.html" rel="alternate" type="text/html"
- title="My sweet article"/>
- <id>urn:md5:c47532bbb1e2883c902071591ae1ec9b</id>
- <published></published>
- <author><name>Test</name></author>
- <dc:subject>tutu</dc:subject>
- <content type="html"></content>
- </entry>
+ <title>%<title>s</title>
+ <link href="http://perdu.com/feeds/index.xml" rel="self" type="application/atom+xml"/>
+ <link href="http://perdu.com" rel="alternate" type="text/html" title="%<title>s"/>
+ <updated>%<date>s</updated>
+ <author><name>%<author>s</name></author>
+ <id>urn:md5:75d53866bcb20465b3287cf237234464</id>
+ <generator uri="https://git.umaneti.net/fronde/about/">Fronde</generator>
</feed>
ATOM
describe Fronde::Index do
- context 'with blog settings' do
- context 'with working org files' do
+ let(:index) { described_class.new Fronde::CONFIG.sources.first }
+ let(:tags_path) { "#{Fronde::CONFIG.sources.first['path']}/tags" }
+ let(:feeds_path) { "#{Fronde::CONFIG.sources.first.publication_path}/feeds" }
+
+ context 'with blog settings and working org files' do
+ before do
+ FileUtils.mkdir_p 'tmp/blog/writings/other'
+ FileUtils.mkdir_p 'tmp/blog/output'
+ FileUtils.cp(
+ ['data/test1.org',
+ 'data/test2.org'],
+ 'tmp/blog/writings'
+ )
+ FileUtils.cp 'data/test3.org', 'tmp/blog/writings/other'
+ Dir.chdir 'tmp/blog'
+ Fronde::CONFIG.load_test(
+ 'author' => 'Test',
+ 'html_public_folder' => 'output',
+ 'domain' => 'http://perdu.com',
+ 'sources' => [
+ { 'path' => 'writings',
+ 'title' => 'Blog',
+ 'target' => '.',
+ 'is_blog' => true }
+ ]
+ )
+ end
+
+ after do
+ tear_down 'tmp/blog'
+ end
+
+ it 'has generated two indexes' do
+ expect(index.all_tags.length).to eq(2)
+ expect(index.empty?).to be(false)
+ end
+
+ it 'excludes specified pattern from indexes' do
+ old_conf = Fronde::CONFIG.settings.merge
+ old_conf['sources'][0]['exclude'] = 'test3\.org$'
+ Fronde::CONFIG.load_test(old_conf)
+ expect(index.all_tags.length).to eq(1)
+ expect(index.all_tags.include?('toto')).to be(false)
+ end
+
+ it 'generates a main index' do
+ expect(index.to_s).to eq(SAMPLE_ALL_INDEX)
+ expect(index.blog_home_page).to eq(SAMPLE_PROJECT_INDEX)
+ end
+
+ it 'generates an atom feed' do
+ now_str = Time.now.strftime('%Y-%m-%d %H:%M')
+ index_date_str = index.date.strftime('%Y-%m-%d %H:%M')
+ expect(index_date_str).to eq(now_str)
+ expected_tz = Time.new(2019, 6, 13).strftime('%:z')
+ comp = format(
+ SAMPLE_ATOM,
+ date: index.date.xmlschema,
+ mtime: File.mtime('writings/test1.org').xmlschema,
+ expected_tz: expected_tz
+ )
+ expect(index.to_atom).to eq(comp)
+ end
+
+ it 'lists tags by name' do
+ expect(index.sort_by(:name)).to eql(['tutu (2)', 'toto (1)'])
+ end
+
+ it 'lists tags by weight' do
+ expect(index.sort_by(:weight)).to eql(['toto (1)', 'tutu (2)'])
+ end
+
+ it 'raises an error if sort_by is called with bad argument' do
+ expect { index.sort_by(:test) }.to raise_error(ArgumentError)
+ end
+
+ it 'correctly saves one index' do
+ FileUtils.mkdir_p tags_path
+ index.write_org('index')
+ expect(File.exist?("#{tags_path}/index.org")).to be(true)
+ expect(File.read("#{tags_path}/index.org")).to eq(SAMPLE_ALL_INDEX)
+ end
+
+ it 'correctly saves one blog index' do
+ index.send(:write_blog_home_page, false)
+ expect(File.exist?('writings/index.org')).to be(true)
+ expect(File.read('writings/index.org')).to eq(SAMPLE_PROJECT_INDEX)
+ end
+
+ it 'correctly saves one blog index, even verbosely' do
+ index.send(:write_blog_home_page, true)
+ expect(File.exist?('writings/index.org')).to be(true)
+ expect(File.read('writings/index.org')).to eq(SAMPLE_PROJECT_INDEX)
+ end
+
+ it 'correctly saves one atom feed' do
+ FileUtils.mkdir_p feeds_path
+ index.write_atom('index')
+ expect(File.exist?("#{feeds_path}/index.xml")).to be(true)
+ expected_tz = Time.new(2019, 6, 13).strftime('%:z')
+ comp = format(
+ SAMPLE_ATOM,
+ date: index.date.xmlschema,
+ mtime: File.mtime('writings/test1.org').xmlschema,
+ expected_tz: expected_tz
+ )
+ expect(File.read("#{feeds_path}/index.xml")).to eq(comp)
+ end
+
+ it 'writes them all' do
+ index.write_all_org
+ expect(File.exist?('writings/index.org')).to be(true)
+ expect(File.exist?("#{tags_path}/index.org")).to be(true)
+ expect(File.exist?("#{tags_path}/toto.org")).to be(true)
+ expect(File.exist?("#{tags_path}/tutu.org")).to be(true)
+
+ index.write_all_feeds
+ expect(File.exist?("#{feeds_path}/index.xml")).to be(true)
+ expect(File.exist?("#{feeds_path}/toto.xml")).to be(true)
+ expect(File.exist?("#{feeds_path}/tutu.xml")).to be(true)
+ end
+
+ it 'does not attempt to include tags into feeds' do
+ # Run it once to ensure index is instanciated a first time and
+ # index files are generated
+ index.write_all_org
+ entries = index.instance_variable_get(:@entries)
+ expect(entries.length).to eq(3)
+ # Run a second time to check that it does not parse previously
+ # generated tags
+ second_index = described_class.new Fronde::CONFIG.sources.first
+ entries = second_index.instance_variable_get(:@entries)
+ expect(entries.length).to eq(3)
+ end
+
+ context 'without recursive config' do
before do
- FileUtils.mkdir_p 'tmp/blog/writings/other'
- FileUtils.mkdir_p 'tmp/blog/output'
- FileUtils.cp(
- ['data/test1.org',
- 'data/test2.org'],
- 'tmp/blog/writings'
- )
- FileUtils.cp 'data/test3.org', 'tmp/blog/writings/other'
- Dir.chdir 'tmp/blog'
+ # This config will generate the same result as if it was
+ # recursive: true as is_blog necessarily requires the project
+ # to be recursive
+ old_conf = Fronde::CONFIG.settings
+ old_conf['sources'][0]['recursive'] = false
+ Fronde::CONFIG.load_test old_conf
end
- after do
- tear_down 'tmp/blog'
+ it 'has generated two index' do
+ expect(index.all_tags.length).to eq(2)
+ expect(index.empty?).to be(false)
end
- context 'with recursive config' do
- before do
- Fronde::Config.load_test(
- 'title' => 'Blog',
- 'author' => 'Test',
- 'public_folder' => 'output',
- 'domain' => 'http://perdu.com',
- 'sources' => [
- { 'path' => 'writings',
- 'target' => '.',
- 'is_blog' => true }
- ]
- )
- end
-
- it 'has generated two indexes', core: true do
- index = described_class.new
- expect(index.entries.length).to eq(2)
- expect(index.empty?).to be(false)
- end
-
- it 'excludes specified pattern from indexes', core: true do
- old_conf = Fronde::Config.settings.dup
- old_conf['sources'][0]['exclude'] = 'test3\.org$'
- Fronde::Config.load_test(old_conf)
- index = described_class.new
- expect(index.entries.length).to eq(1)
- expect(index.entries.include?('toto')).to be(false)
- end
-
- it 'generates a main index', core: true do
- index = described_class.new
- expect(index.to_s).to eq(SAMPLE_ALL_INDEX)
- expect(index.to_s('writings', is_project: true)).to(
- eq(SAMPLE_PROJECT_INDEX)
- )
- end
-
- it 'generates an atom feed', core: true do
- now_str = DateTime.now.strftime('%Y-%m-%d %H:%M')
- index = described_class.new
- index_date_str = index.date.strftime('%Y-%m-%d %H:%M')
- expect(index_date_str).to eq(now_str)
- expect(index.to_atom).to(
- eq(format(SAMPLE_ATOM, date: index.date.rfc3339))
- )
- end
-
- it 'generates the right org-header' do
- index = described_class.new
- header = ['#+title: ', '#+author: Test', '#+language: en'].join("\n").strip
- expect(index.send(:org_header)).to eq(header)
- expect(index.send(:org_header, 'index')).to eq(header)
- expect(index.send(:org_header, 'Test', is_tag: true)).to eq(header)
-
- header = ['#+title: Blog', '#+author: Test', '#+language: en'].join("\n").strip
- expect(index.send(:org_header, is_tag: false)).to eq(header)
- expect(index.send(:org_header, 'index', is_tag: false)).to eq(header)
-
- header = ['#+title: Test', '#+author: Test', '#+language: en'].join("\n")
- expect(index.send(:org_header, 'Test', is_tag: false)).to eq(header.strip)
-
- header = ['#+title: tutu', '#+author: Test', '#+language: en'].join("\n").strip
- expect(index.send(:org_header, 'tutu')).to eq(header)
- expect(index.send(:org_header, 'tutu', is_tag: true)).to eq(header)
- end
-
- it 'lists tags by name', core: true do
- list = described_class.new.sort_by :name
- expect(list).to eql(['tutu (2)', 'toto (1)'])
- end
-
- it 'lists tags by weight', core: true do
- list = described_class.new.sort_by :weight
- expect(list).to eql(['toto (1)', 'tutu (2)'])
- end
-
- it 'raises an error if sort_by is called with bad argument', core: true do
- index = described_class.new
- expect { index.sort_by(:test) }.to raise_error(ArgumentError)
- end
-
- it 'correctly saves one index', core: true do
- described_class.new.write_org('index')
- expect(File.exist?('tags/index.org')).to be(true)
- expect(File.read('tags/index.org')).to eq(SAMPLE_ALL_INDEX)
- end
-
- it 'correctly saves one blog index', core: true do
- described_class.new.send(:write_all_blog_home, false)
- expect(File.exist?('writings/index.org')).to be(true)
- expect(File.read('writings/index.org')).to eq(SAMPLE_PROJECT_INDEX)
- end
-
- it 'correctly saves one blog index, even verbosely', core: true do
- described_class.new.send(:write_all_blog_home, true)
- expect(File.exist?('writings/index.org')).to be(true)
- expect(File.read('writings/index.org')).to eq(SAMPLE_PROJECT_INDEX)
- end
-
- it 'correctly saves one atom feed', core: true do
- index = described_class.new
- index.write_atom('index')
- expect(File.exist?('output/feeds/index.xml')).to be(true)
- expect(File.read('output/feeds/index.xml')).to(
- eq(format(SAMPLE_ATOM, date: index.date.rfc3339))
- )
- end
-
- it 'writes them all', core: true do
- described_class.new.write_all
- expect(File.exist?('writings/index.org')).to be(true)
- expect(File.exist?('tags/index.org')).to be(true)
- expect(File.exist?('tags/toto.org')).to be(true)
- expect(File.exist?('tags/tutu.org')).to be(true)
- expect(File.exist?('output/feeds/index.xml')).to be(true)
- expect(File.exist?('output/feeds/toto.xml')).to be(true)
- expect(File.exist?('output/feeds/tutu.xml')).to be(true)
- end
+ it 'generates a main index' do
+ expect(index.to_s).to eq(SAMPLE_ALL_INDEX)
+ expect(index.blog_home_page).to eq(SAMPLE_PROJECT_INDEX)
end
+ end
+ end
- context 'without recursive config' do
- before do
- Fronde::Config.load_test(
+ context 'with blog settings and without org files' do
+ before do
+ FileUtils.mkdir_p 'tmp/blog/writings'
+ FileUtils.mkdir_p 'tmp/blog/output'
+ Dir.chdir 'tmp/blog'
+ Fronde::CONFIG.load_test(
+ 'author' => 'Test',
+ 'html_public_folder' => 'output',
+ 'domain' => 'http://perdu.com',
+ 'sources' => [
+ { 'path' => 'writings',
'title' => 'Blog',
- 'author' => 'Test',
- 'public_folder' => 'output',
- 'domain' => 'http://perdu.com',
- 'sources' => [
- { 'path' => 'writings',
- 'target' => '.',
- 'recursive' => false,
- 'is_blog' => true }
- ]
- )
- end
-
- it 'has generated one index', core: true do
- index = described_class.new
- expect(index.entries.length).to eq(1)
- expect(index.empty?).to be(false)
- end
-
- it 'generates a main index', core: true do
- index = described_class.new
- expect(index.to_s).to eq(SAMPLE_NO_REC_INDEX)
- expect(index.to_s('writings', is_project: true)).to(
- eq(SAMPLE_PROJECT_NO_REC_INDEX)
- )
- end
-
- it 'writes them all', core: true do
- described_class.new.write_all
- expect(File.exist?('writings/index.org')).to be(true)
- expect(File.exist?('tags/index.org')).to be(true)
- expect(File.exist?('tags/toto.org')).to be(false)
- expect(File.exist?('tags/tutu.org')).to be(true)
- expect(File.exist?('output/feeds/index.xml')).to be(true)
- expect(File.exist?('output/feeds/toto.xml')).to be(false)
- expect(File.exist?('output/feeds/tutu.xml')).to be(true)
- end
- end
+ 'target' => '.',
+ 'is_blog' => true }
+ ]
+ )
end
- context 'without org files' do
- before do
- FileUtils.mkdir_p 'tmp/blog/writings'
- FileUtils.mkdir_p 'tmp/blog/output'
- Dir.chdir 'tmp/blog'
- Fronde::Config.load_test(
- 'title' => 'Blog',
- 'author' => 'Test',
- 'public_folder' => 'output',
- 'domain' => 'http://perdu.com',
- 'sources' => [
- { 'path' => 'writings',
- 'target' => '.',
- 'is_blog' => true }
- ]
- )
- end
+ after do
+ tear_down 'tmp/blog'
+ end
- after do
- tear_down 'tmp/blog'
- end
+ it 'has generated no index' do
+ expect(index.all_tags.length).to eq(0)
+ expect(index.empty?).to be(true)
+ end
- it 'has generated no index', core: true do
- index = described_class.new
- expect(index.entries.length).to eq(0)
- expect(index.empty?).to be(true)
- end
+ it 'generates a main index' do
+ expect(index.to_s).to eq(format(SAMPLE_EMPTY_INDEX, author: 'Test'))
+ expect(index.blog_home_page).to eq(SAMPLE_EMPTY_PROJECT_INDEX)
+ end
- it 'generates a main index', core: true do
- index = described_class.new
- empty_index = <<~INDEX
- #+title: All tags
- #+author: Test
- #+language: en
-
- * By alphabetical order
- :PROPERTIES:
- :HTML_CONTAINER_CLASS: index-tags
- :UNNUMBERED: notoc
- :END:
-
-
- * By publication number
- :PROPERTIES:
- :HTML_CONTAINER_CLASS: index-tags
- :UNNUMBERED: notoc
- :END:
- INDEX
- expect(index.to_s).to eq(empty_index)
- empty_project_index = <<~INDEX.strip
- #+title: writings
- #+author: Test
- #+language: en
- INDEX
- expect(index.to_s('writings', is_project: true)).to(
- eq(empty_project_index)
- )
- end
+ it 'generates an atom feed' do
+ now_str = Time.now.strftime('%Y-%m-%d %H:%M')
+ index_date_str = index.date.strftime('%Y-%m-%d %H:%M')
+ expect(index_date_str).to eq(now_str)
+ comp = format(
+ SAMPLE_EMPTY_ATOM,
+ date: index.date.xmlschema,
+ author: 'Test', title: 'Blog'
+ )
+ expect(index.to_atom).to eq(comp)
+ end
- it 'generates an atom feed', core: true do
- now_str = DateTime.now.strftime('%Y-%m-%d %H:%M')
- index = described_class.new
- index_date_str = index.date.strftime('%Y-%m-%d %H:%M')
- expect(index_date_str).to eq(now_str)
- empty_atom = <<~ATOM.strip
- <?xml version="1.0" encoding="utf-8"?>
- <feed xmlns="http://www.w3.org/2005/Atom"
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:wfw="http://wellformedweb.org/CommentAPI/"
- xml:lang="en">
-
- <title>Blog</title>
- <link href="http://perdu.com/feeds/index.xml" rel="self" type="application/atom+xml"/>
- <link href="http://perdu.com" rel="alternate" type="text/html" title="Blog"/>
- <updated>%<date>s</updated>
- <author><name>Test</name></author>
- <id>urn:md5:75d53866bcb20465b3287cf237234464</id>
- <generator uri="https://git.umaneti.net/fronde/about/">Fronde</generator>
- </feed>
- ATOM
- expect(index.to_atom).to(
- eq(format(empty_atom, date: index.date.rfc3339))
- )
- end
+ it 'correctly saves one index' do
+ FileUtils.mkdir_p tags_path
+ index.write_org('index')
+ expect(File.exist?("#{tags_path}/index.org")).to be(true)
+ expect(File.read("#{tags_path}/index.org")).to(
+ eq(format(SAMPLE_EMPTY_INDEX, author: 'Test'))
+ )
+ end
- it 'correctly saves one index', core: true do
- described_class.new.write_org('index')
- expect(File.exist?('tags/index.org')).to be(true)
- empty_index = <<~INDEX
- #+title: All tags
- #+author: Test
- #+language: en
-
- * By alphabetical order
- :PROPERTIES:
- :HTML_CONTAINER_CLASS: index-tags
- :UNNUMBERED: notoc
- :END:
-
-
- * By publication number
- :PROPERTIES:
- :HTML_CONTAINER_CLASS: index-tags
- :UNNUMBERED: notoc
- :END:
- INDEX
- expect(File.read('tags/index.org')).to eq(empty_index)
- end
+ it 'correctly saves one blog index' do
+ index.send(:write_blog_home_page, false)
+ expect(File.exist?('writings/index.org')).to be(true)
+ expect(File.read('writings/index.org')).to(
+ eq(SAMPLE_EMPTY_PROJECT_INDEX)
+ )
+ end
- it 'correctly saves one blog index', core: true do
- described_class.new.send(:write_all_blog_home, false)
- expect(File.exist?('writings/index.org')).to be(true)
- empty_project_index = <<~INDEX.strip
- #+title: writings
- #+author: Test
- #+language: en
- INDEX
- expect(File.read('writings/index.org')).to eq(empty_project_index)
- end
+ it 'correctly saves one atom feed' do
+ FileUtils.mkdir_p feeds_path
+ index.write_atom('index')
+ expect(File.exist?("#{feeds_path}/index.xml")).to be(true)
+ comp = format(
+ SAMPLE_EMPTY_ATOM,
+ date: index.date.xmlschema,
+ author: 'Test', title: 'Blog'
+ )
+ expect(File.read("#{feeds_path}/index.xml")).to eq(comp)
+ end
- it 'correctly saves one atom feed', core: true do
- index = described_class.new
- index.write_atom('index')
- expect(File.exist?('output/feeds/index.xml')).to be(true)
- empty_atom = <<~ATOM.strip
- <?xml version="1.0" encoding="utf-8"?>
- <feed xmlns="http://www.w3.org/2005/Atom"
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:wfw="http://wellformedweb.org/CommentAPI/"
- xml:lang="en">
-
- <title>Blog</title>
- <link href="http://perdu.com/feeds/index.xml" rel="self" type="application/atom+xml"/>
- <link href="http://perdu.com" rel="alternate" type="text/html" title="Blog"/>
- <updated>%<date>s</updated>
- <author><name>Test</name></author>
- <id>urn:md5:75d53866bcb20465b3287cf237234464</id>
- <generator uri="https://git.umaneti.net/fronde/about/">Fronde</generator>
- </feed>
- ATOM
- expect(File.read('output/feeds/index.xml')).to(
- eq(format(empty_atom, date: index.date.rfc3339))
- )
- end
+ it 'writes them all' do
+ index.write_all_org
+ expect(File.exist?('writings/index.org')).to be(true)
+ expect(File.exist?("#{tags_path}/index.org")).to be(true)
- it 'writes them all', core: true do
- described_class.new.write_all
- expect(File.exist?('writings/index.org')).to be(true)
- expect(File.exist?('tags/index.org')).to be(true)
- expect(File.exist?('output/feeds/index.xml')).to be(true)
- end
+ index.write_all_feeds
+ expect(File.exist?("#{feeds_path}/index.xml")).to be(true)
end
end
@@ -490,12 +399,14 @@ describe Fronde::Index do
'tmp/txt/writings'
)
Dir.chdir 'tmp/txt'
- Fronde::Config.load_test(
- 'title' => 'My site',
+ Fronde::CONFIG.load_test(
'author' => 'Test',
- 'public_folder' => 'output',
+ 'html_public_folder' => 'output',
'domain' => 'http://perdu.com',
- 'sources' => [{ 'path' => 'writings', 'target' => '.' }]
+ 'sources' => [
+ { 'path' => 'writings',
+ 'target' => '.' }
+ ]
)
end
@@ -503,79 +414,28 @@ describe Fronde::Index do
tear_down 'tmp/txt'
end
- it 'does not have generated any indexes', core: true do
- index = described_class.new
- expect(index.entries.length).to eq(0)
+ it 'does not have generated any indexes' do
+ expect(index.all_tags.length).to eq(0)
expect(index.empty?).to be(true)
end
- it 'generates an empty main index', core: true do
- empty_index = <<~EMPTY_INDEX
- #+title: All tags
- #+author: Test
- #+language: en
-
- * By alphabetical order
- :PROPERTIES:
- :HTML_CONTAINER_CLASS: index-tags
- :UNNUMBERED: notoc
- :END:
-
-
- * By publication number
- :PROPERTIES:
- :HTML_CONTAINER_CLASS: index-tags
- :UNNUMBERED: notoc
- :END:
- EMPTY_INDEX
- expect(described_class.new.to_s).to eq(empty_index)
+ it 'generates an empty main index' do
+ expect(index.to_s).to(
+ eq(format(SAMPLE_EMPTY_INDEX, author: 'Test'))
+ )
end
- it 'generates an empty atom feed', core: true do
- now_str = DateTime.now.strftime('%Y-%m-%d %H:%M')
- index = described_class.new
+ it 'generates an empty atom feed' do
+ now_str = Time.now.strftime('%Y-%m-%d %H:%M')
index_date_str = index.date.strftime('%Y-%m-%d %H:%M')
expect(index_date_str).to eq(now_str)
- empty_atom = <<~EMPTY_ATOM.strip
- <?xml version="1.0" encoding="utf-8"?>
- <feed xmlns="http://www.w3.org/2005/Atom"
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:wfw="http://wellformedweb.org/CommentAPI/"
- xml:lang="en">
-
- <title>My site</title>
- <link href="http://perdu.com/feeds/index.xml" rel="self" type="application/atom+xml"/>
- <link href="http://perdu.com" rel="alternate" type="text/html" title="My site"/>
- <updated>%<date>s</updated>
- <author><name>Test</name></author>
- <id>urn:md5:75d53866bcb20465b3287cf237234464</id>
- <generator uri="https://git.umaneti.net/fronde/about/">Fronde</generator>
- </feed>
- EMPTY_ATOM
- expect(index.to_atom).to(
- eq(format(empty_atom, date: index.date.rfc3339))
+ comp = format(
+ SAMPLE_EMPTY_ATOM,
+ date: index.date.xmlschema,
+ author: 'Test',
+ title: 'writings'
)
- end
-
- it 'does not save one index', core: true do
- described_class.new.write_org('index')
- expect(File.exist?('tags/index.org')).to be(false)
- end
-
- it 'does not save one atom feed', core: true do
- described_class.new.write_atom('index')
- expect(File.exist?('output/feeds/index.xml')).to be(false)
- end
-
- it 'does not write it all', core: true do
- described_class.new.write_all
- expect(File.exist?('writings/index.org')).to be(false)
- expect(File.exist?('tags/index.org')).to be(false)
- expect(File.exist?('tags/toto.org')).to be(false)
- expect(File.exist?('tags/tutu.org')).to be(false)
- expect(File.exist?('output/feeds/index.xml')).to be(false)
- expect(File.exist?('output/feeds/toto.xml')).to be(false)
- expect(File.exist?('output/feeds/tutu.xml')).to be(false)
+ expect(index.to_atom).to eq(comp)
end
end
@@ -585,8 +445,8 @@ describe Fronde::Index do
FileUtils.mkdir_p 'tmp/txt/output'
FileUtils.cp 'data/test1.org', 'tmp/txt/writings'
Dir.chdir 'tmp/txt'
- Fronde::Config.load_test(
- 'public_folder' => 'output',
+ Fronde::CONFIG.load_test(
+ 'html_public_folder' => 'output',
'domain' => 'http://perdu.com',
'sources' => [{ 'path' => 'writings', 'target' => '.' }]
)
@@ -596,66 +456,28 @@ describe Fronde::Index do
tear_down 'tmp/txt'
end
- it 'does not have generated any indexes', core: true do
- index = described_class.new
- expect(index.entries.length).to eq(0)
+ it 'does not have generated any indexes' do
+ expect(index.all_tags.length).to eq(0)
expect(index.empty?).to be(true)
end
- it 'generates an empty main index', core: true do
- empty_index = <<~EMPTY_INDEX
- #+title: All tags
- #+author: alice
- #+language: en
-
- * By alphabetical order
- :PROPERTIES:
- :HTML_CONTAINER_CLASS: index-tags
- :UNNUMBERED: notoc
- :END:
-
-
- * By publication number
- :PROPERTIES:
- :HTML_CONTAINER_CLASS: index-tags
- :UNNUMBERED: notoc
- :END:
- EMPTY_INDEX
- expect(described_class.new.to_s).to eq(empty_index)
+ it 'generates an empty main index' do
+ expect(index.to_s).to(
+ eq(format(SAMPLE_EMPTY_INDEX, author: 'alice'))
+ )
end
- it 'generates an empty atom feed', core: true do
- now_str = DateTime.now.strftime('%Y-%m-%d %H:%M')
- index = described_class.new
+ it 'generates an empty atom feed' do
+ now_str = Time.now.strftime('%Y-%m-%d %H:%M')
index_date_str = index.date.strftime('%Y-%m-%d %H:%M')
expect(index_date_str).to eq(now_str)
- empty_atom = <<~EMPTY_ATOM.strip
- <?xml version="1.0" encoding="utf-8"?>
- <feed xmlns="http://www.w3.org/2005/Atom"
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:wfw="http://wellformedweb.org/CommentAPI/"
- xml:lang="en">
-
- <title>All tags</title>
- <link href="http://perdu.com/feeds/index.xml" rel="self" type="application/atom+xml"/>
- <link href="http://perdu.com" rel="alternate" type="text/html" title="All tags"/>
- <updated>%<date>s</updated>
- <author><name>alice</name></author>
- <id>urn:md5:75d53866bcb20465b3287cf237234464</id>
- <generator uri="https://git.umaneti.net/fronde/about/">Fronde</generator>
- </feed>
- EMPTY_ATOM
- expect(index.to_atom).to(
- eq(format(empty_atom, date: index.date.rfc3339))
+ comp = format(
+ SAMPLE_EMPTY_ATOM,
+ date: index.date.xmlschema,
+ author: 'alice',
+ title: 'writings'
)
- end
-
- it 'does not save any index', core: true do
- index = described_class.new
- index.write_org('index')
- expect(File.exist?('tags/index.org')).to be(false)
- index.write_atom('index')
- expect(File.exist?('output/feeds/index.xml')).to be(false)
+ expect(index.to_atom).to eq(comp)
end
end
end
diff --git a/spec/fronde/org/file_spec.rb b/spec/fronde/org/file_spec.rb
new file mode 100644
index 0000000..032d58b
--- /dev/null
+++ b/spec/fronde/org/file_spec.rb
@@ -0,0 +1,264 @@
+# frozen_string_literal: true
+
+require_relative '../../../lib/fronde/org/file'
+
+describe Fronde::Org::File do
+ before { Fronde::CONFIG.reset }
+
+ context 'with working org files' do
+ it 'parses without date' do
+ o = described_class.new('data/test1.org')
+ expect(o.title).to eq('My sweet article')
+ expect(o.date).to be_an_instance_of(NilTime)
+ expect(o.date.strftime('%Y-%m-%d')).to eq('')
+ expect(o.timekey).to eq('00000000000000')
+ expect(o.format('%i - (%t)')).to eq(' - (My sweet article)')
+ end
+
+ it 'parses with a partial date' do
+ o = described_class.new('data/test2.org')
+ expect(o.title).to eq('My second article')
+ expect(o.date).to eq(Time.strptime('2019-06-11 00:00:00', '%Y-%m-%d %H:%M:%S'))
+ expect(o.date.strftime('%Y-%m-%d')).to eq('2019-06-11')
+ expect(o.timekey).to eq('20190611000000')
+ expect(o.format('%i - (%t)')).to eq('2019-06-11 - (My second article)')
+ expect(o.lang).to eq('es')
+ end
+
+ it 'parses with a complete date' do
+ o = described_class.new('data/test3.org')
+ expect(o.title).to eq('My third article')
+ expect(o.date).to eq(Time.strptime('2019-06-11 23:42:10', '%Y-%m-%d %H:%M:%S'))
+ expect(o.timekey).to eq('20190611234210')
+ expect(o.format('%i - (%t)')).to eq('2019-06-11 - (My third article)')
+ end
+
+ it 'parses with a complete date, but partial time' do
+ o = described_class.new('data/test4.org')
+ expect(o.title).to eq('Fourth test')
+ expect(o.date).to eq(Time.strptime('2019-07-25 20:45:00', '%Y-%m-%d %H:%M:%S'))
+ expect(o.timekey).to eq('20190725204500')
+ expect(o.format('%i - (%t)')).to eq('2019-07-25 - (Fourth test)')
+ end
+
+ it 'finds a subtitle' do
+ o = described_class.new('data/content.org')
+ expect(o.title).to eq('My first blog post?')
+ expect(o.subtitle).to eq('What to do with that')
+ end
+
+ it 'can be cast to a Hash' do
+ o = described_class.new('data/content.org')
+ data = o.to_h
+ expect(data['author']).to eq 'alice'
+ expect(data['title']).to eq 'My first blog post?'
+ expect(data['excerpt']).to(
+ eq('An article stating whether this is my first blog post or not')
+ )
+ expect(data['keywords']).to eq %w[test tata]
+ expect(data['timekey']).to eq '20190613000000'
+ expect(data['published']).to eq 'Thursday 13th of June'
+ expected_tz = Time.new(2019, 6, 13).strftime('%:z')
+ expect(data['published_xml']).to eq "2019-06-13T00:00:00#{expected_tz}"
+ # Not published
+ expect(data['url']).to be_nil
+ expect(data['published_body']).to(
+ eq('An article stating whether this is my first blog post or not')
+ )
+ expect(data['updated_xml']).to be_nil
+ end
+
+ it 'accepts dynamic setters' do
+ o = described_class.new('data/content.org')
+ expect(o.title).to eq('My first blog post?')
+ o.title = 'Actually not my first one'
+ expect(o.title).to eq('Actually not my first one')
+ end
+
+ it 'raises NameError if it cannot resolves a method' do
+ o = described_class.new('data/content.org')
+ expect { o.not_a_key }.to raise_error(NameError)
+ expect { o.not_a_key = 'test' }.to raise_error(NameError)
+ end
+
+ it 'answers correctly to respond_to_missing?' do
+ o = described_class.new('data/content.org')
+ expect(o.respond_to?(:title)).to be true
+ expect(o.respond_to?(:title=)).to be true
+ expect(o.respond_to?(:not_a_key)).to be false
+ expect(o.respond_to?(:not_a_key=)).to be false
+ end
+ end
+
+ context 'without a working file' do
+ before do
+ FileUtils.mkdir_p 'tmp/org_test'
+ Dir.chdir 'tmp/org_test'
+ end
+
+ after do
+ tear_down 'tmp/org_test'
+ end
+
+ it 'raises if file_name is nil and try to write' do
+ expect { described_class.new(nil).write }.to raise_error(RuntimeError)
+ expect { described_class.new('').write }.to raise_error(RuntimeError)
+ end
+
+ it 'does not raise without file_name, but with a title' do
+ expect { described_class.new(nil, title: 'Test 1').write }.not_to(
+ raise_error
+ )
+ expect(File.exist?('test-1.org')).to be true
+ expect { described_class.new('', title: 'Test 2').write }.not_to(
+ raise_error
+ )
+ expect(File.exist?('test-2.org')).to be true
+ end
+
+ it 'returns a new org file structure' do
+ now_str = Time.now.strftime('%Y-%m-%d %H:%M')
+ o = described_class.new('__test__.org', title: 'test')
+ expect(o.title).to eq('test')
+ o_date_str = o.date.strftime('%Y-%m-%d %H:%M')
+ expect(o_date_str).to eq(now_str)
+ date = o.date.strftime('%Y-%m-%d %a. %H:%M:%S')
+ expect(o.author).not_to be_nil
+ expect(o.author).to be_an_instance_of(String)
+ o.write
+ empty_content = <<~CONTENT
+ #+title: test
+ #+date: <#{date}>
+ #+author: #{o.author}
+ #+language: en
+
+
+ CONTENT
+ expect(File.exist?('__test__.org')).to be(true)
+ expect(File.read('__test__.org')).to eq(empty_content)
+ end
+
+ it 'creates new Org file, even in a new folder' do
+ o = described_class.new(
+ 'not/existing/test.org',
+ title: 'My Test', content: 'Lorem ipsum'
+ )
+ o.write
+ expect(File.exist?('not/existing/test.org')).to be(true)
+ date = o.date.strftime('%Y-%m-%d %a. %H:%M:%S')
+ content = <<~CONTENT
+ #+title: My Test
+ #+date: <#{date}>
+ #+author: #{o.author}
+ #+language: en
+
+ Lorem ipsum
+ CONTENT
+ expect(File.read('not/existing/test.org')).to eq(content)
+ end
+
+ it 'uses file name as title when title is empty' do
+ FileUtils.mkdir 'src'
+ File.write 'src/no_title.org', 'Lorem ipsum.'
+ o = described_class.new('src/no_title.org')
+ expect(o.title).to eq('src/no_title.org')
+ end
+ end
+
+ context 'with configuration' do
+ it 'respects author name' do
+ Fronde::CONFIG.load_test('author' => 'Test')
+ o = described_class.new('data/test1.org')
+ expect(o.author).to eq('Test')
+ o = described_class.new('data/test2.org')
+ expect(o.author).to eq('Titi')
+ o = described_class.new('data/test3.org')
+ expect(o.author).to eq('Test')
+ end
+
+ it 'computes the right pub_file path for existing sources' do
+ o = described_class.new('data/test1.org')
+ expect(o.pub_file).to be_nil # No source match
+
+ config = { 'sources' => [{ 'path' => 'data' }] }
+ Fronde::CONFIG.load_test config
+ o = described_class.new('data/test1.org')
+ expect(o.pub_file).to eq('/data/test1.html')
+ o = described_class.new('data/content.org')
+ expect(o.pub_file).to eq('/data/content.html')
+
+ config['domain'] = 'http://perdu.com'
+ Fronde::CONFIG.load_test config
+ o = described_class.new('data/test1.org')
+ expect(o.pub_file).to eq('/data/test1.html')
+ o = described_class.new('data/content.org')
+ expect(o.pub_file).to eq('/data/content.html')
+ end
+
+ it 'computes the right url for existing sources' do
+ o = described_class.new('data/test1.org')
+ expect(o.url).to be_nil # No source match
+
+ config = { 'sources' => [{ 'path' => 'data' }] }
+ Fronde::CONFIG.load_test config
+ o = described_class.new('data/test1.org')
+ expect(o.url).to eq('/data/test1.html')
+ o = described_class.new('data/content.org')
+ expect(o.url).to eq('/data/content.html')
+
+ config['domain'] = 'http://perdu.com'
+ Fronde::CONFIG.load_test config
+ o = described_class.new('data/test1.org')
+ expect(o.url).to eq('http://perdu.com/data/test1.html')
+ o = described_class.new('data/content.org')
+ expect(o.url).to eq('http://perdu.com/data/content.html')
+ end
+ end
+
+ context 'with an html file as file name' do
+ before do
+ FileUtils.mkdir_p 'tmp/test_target/src/blog/toto'
+ FileUtils.mkdir_p 'tmp/test_target/writings'
+ Dir.chdir 'tmp/test_target'
+ File.write 'src/test.org', 'Lorem ipsum.'
+ FileUtils.touch(
+ ['src/blog/test.org', 'src/blog/toto/tata.org',
+ 'src/blog/toto/content.org', 'writings/notes.org']
+ )
+ Fronde::CONFIG.load_test(
+ 'sources' => [{ 'path' => 'src', 'target' => '.' }, 'writings']
+ )
+ end
+
+ after do
+ tear_down 'tmp/test_target'
+ end
+
+ it 'computes the right source path' do
+ o = described_class.new('public_html/test.html')
+ expect(o.file).to eq(File.expand_path('src/test.org'))
+ o = described_class.new('public_html/blog/test.html')
+ expect(o.file).to eq(File.expand_path('src/blog/test.org'))
+ o = described_class.new(
+ 'public_html/blog/toto/tata.html'
+ )
+ expect(o.file).to eq(File.expand_path('src/blog/toto/tata.org'))
+ o = described_class.new(
+ 'public_html/blog/toto/content.html'
+ )
+ expect(o.file).to eq(File.expand_path('src/blog/toto/content.org'))
+ end
+
+ it 'identifies a source for a given published file' do
+ o = described_class.new('public_html/writings/notes.html')
+ expect(o.file).to eq(File.expand_path('writings/notes.org'))
+ expect(o.project).not_to be_nil
+ o = described_class.new('public_html/test.html')
+ expect(o.file).to eq(File.expand_path('src/test.org'))
+ expect(o.project).not_to be_nil
+ o = described_class.new('public_html/not/known.html')
+ expect(o.file).to eq(File.expand_path('public_html/not/known.html'))
+ expect(o.project).to be_nil
+ end
+ end
+end
diff --git a/spec/fronde/org_file_spec.rb b/spec/fronde/org_file_spec.rb
deleted file mode 100644
index 1a2f0be..0000000
--- a/spec/fronde/org_file_spec.rb
+++ /dev/null
@@ -1,266 +0,0 @@
-# frozen_string_literal: true
-
-describe Fronde::OrgFile do
- context 'with working org files' do
- it 'parses without date', core: true do
- o = described_class.new('data/test1.org')
- expect(o.title).to eq('My sweet article')
- expect(o.date).to be_nil
- expect(o.timekey).to eq('00000000000000')
- expect(o.format('%i - (%t)')).to eq(' - (My sweet article)')
- end
-
- it 'parses with a partial date', core: true do
- o = described_class.new('data/test2.org')
- expect(o.title).to eq('My second article')
- expect(o.date).to eq(DateTime.strptime('2019-06-11 00:00:00', '%Y-%m-%d %H:%M:%S'))
- expect(o.timekey).to eq('20190611000000')
- expect(o.datestring(:short)).to eq('2019-06-11')
- expect(o.format('%i - (%t)')).to eq('2019-06-11 - (My second article)')
- expect(o.lang).to eq('es')
- end
-
- it 'parses with a complete date', core: true do
- o = described_class.new('data/test3.org')
- expect(o.title).to eq('My third article')
- expect(o.date).to eq(DateTime.strptime('2019-06-11 23:42:10', '%Y-%m-%d %H:%M:%S'))
- expect(o.timekey).to eq('20190611234210')
- expect(o.datestring(:short)).to eq('2019-06-11')
- expect(o.format('%i - (%t)')).to eq('2019-06-11 - (My third article)')
- end
-
- it 'parses with a complete date, but partial time', core: true do
- o = described_class.new('data/test4.org')
- expect(o.title).to eq('Fourth test')
- expect(o.date).to eq(DateTime.strptime('2019-07-25 20:45:00', '%Y-%m-%d %H:%M:%S'))
- expect(o.timekey).to eq('20190725204500')
- expect(o.datestring(:short)).to eq('2019-07-25')
- expect(o.format('%i - (%t)')).to eq('2019-07-25 - (Fourth test)')
- end
-
- it 'finds a subtitle', core: true do
- o = described_class.new('data/content.org')
- expect(o.title).to eq('My first blog post?')
- expect(o.subtitle).to eq('What to do with that')
- end
- end
-
- context 'with various titles' do
- it 'transliterates them into slugs', core: true do
- expect(described_class.slug('toto')).to eq('toto')
- expect(described_class.slug('TotO')).to eq('toto')
- expect(described_class.slug('Tôto')).to eq('toto')
- expect(described_class.slug('Tôto tata')).to eq('toto-tata')
- expect(described_class.slug('ÀùéïỸç/+*= truñlu°`')).to eq('aueiyc-trunlu')
- end
- end
-
- context 'without a working file' do
- before do
- FileUtils.mkdir_p 'tmp/org_test'
- Dir.chdir 'tmp/org_test'
- end
-
- after do
- tear_down 'tmp/org_test'
- end
-
- it 'raises if file_name is nil and try to write', core: true do
- expect { described_class.new(nil).write }.to raise_error(TypeError)
- expect { described_class.new('').write }.to raise_error(TypeError)
- end
-
- it 'returns a new org file structure', core: true do
- now_str = DateTime.now.strftime('%Y-%m-%d %H:%M')
- o = described_class.new('__test__.org', title: 'test')
- expect(o.title).to eq('test')
- o_date_str = o.date.strftime('%Y-%m-%d %H:%M')
- expect(o_date_str).to eq(now_str)
- date = o.date.strftime('%Y-%m-%d %a. %H:%M:%S')
- expect(o.author).not_to be_nil
- expect(o.author).to be_an_instance_of(String)
- o.write
- empty_content = <<~CONTENT
- #+title: test
- #+date: <#{date}>
- #+author: #{o.author}
- #+language: en
-
-
- CONTENT
- expect(File.exist?('__test__.org')).to be(true)
- expect(File.read('__test__.org')).to eq(empty_content)
- end
-
- it 'creates new Org file, even in a new folder', core: true do
- o = described_class.new(
- 'not/existing/test.org',
- title: 'My Test', content: 'Lorem ipsum'
- )
- o.write
- expect(File.exist?('not/existing/test.org')).to be(true)
- date = o.date.strftime('%Y-%m-%d %a. %H:%M:%S')
- content = <<~CONTENT
- #+title: My Test
- #+date: <#{date}>
- #+author: #{o.author}
- #+language: en
-
- Lorem ipsum
- CONTENT
- expect(File.read('not/existing/test.org')).to eq(content)
- end
- end
-
- context 'with configuration' do
- before do
- Fronde::Config.reset
- end
-
- it 'respects author name', core: true do
- Fronde::Config.load_test('author' => 'Test')
- o = described_class.new('data/test1.org')
- expect(o.author).to eq('Test')
- o = described_class.new('data/test2.org')
- expect(o.author).to eq('Titi')
- o = described_class.new('data/test3.org')
- expect(o.author).to eq('Test')
- end
-
- it 'computes the right html_file path for existing sources', core: true do
- o = described_class.new('data/test1.org')
- expect(o.html_file).to eq('data/test1.html')
- Fronde::Config.load_test('domain' => 'http://perdu.com')
- o = described_class.new('data/test1.org')
- expect(o.html_file).to eq('data/test1.html')
- o = described_class.new('data/content.org')
- expect(o.html_file).to eq('data/content.html')
- # The following are weird tests. We begin to test theoric stuff here
- Fronde::Config.load_test(
- 'domain' => 'http://perdu.com',
- 'sources' => [
- { 'path' => 'data' }
- ]
- )
- o = described_class.new('data/test1.org')
- expect(o.html_file).to eq('data/test1.html')
- o = described_class.new('data/content.org')
- expect(o.html_file).to eq('data/content.html')
- end
-
- it 'computes the right url for existing sources', core: true do
- o = described_class.new('data/test1.org')
- expect(o.url).to eq('/data/test1.html')
- Fronde::Config.load_test('domain' => 'http://perdu.com')
- o = described_class.new('data/test1.org')
- expect(o.url).to eq('http://perdu.com/data/test1.html')
- o = described_class.new('data/content.org')
- expect(o.url).to eq('http://perdu.com/data/content.html')
- # The following are weird tests. We begin to test theoric stuff here
- Fronde::Config.load_test(
- 'domain' => 'http://perdu.com',
- 'sources' => [
- { 'path' => 'data' }
- ]
- )
- o = described_class.new('data/test1.org')
- expect(o.url).to eq('http://perdu.com/data/test1.html')
- o = described_class.new('data/content.org')
- expect(o.url).to eq('http://perdu.com/data/content.html')
- end
-
- it 'computes the right html_file path for theoritical sources', core: true do
- expect(described_class.target_for_source(nil, nil)).to be(nil)
- target = described_class.target_for_source('src/test.org', nil)
- expect(target).to eq('public_html/src/test.html')
- target = described_class.target_for_source('not/known/at/all.org', nil)
- expect(target).to eq('public_html/at/all.html')
- target = described_class.target_for_source(
- 'not/known/at/all.org', nil, with_public_folder: false
- )
- expect(target).to eq('at/all.html')
- project = { 'path' => 'src', 'target' => '.' }
- target = described_class.target_for_source('src/test.org', project)
- expect(target).to eq('public_html/test.html')
- target = described_class.target_for_source(
- 'src/test.org', project, with_public_folder: false
- )
- expect(target).to eq('test.html')
- target = described_class.target_for_source(
- 'src/blog/test.org', project
- )
- expect(target).to eq('public_html/blog/test.html')
- target = described_class.target_for_source(
- 'src/blog/toto/tata.org', project
- )
- expect(target).to eq('public_html/blog/toto/tata.html')
- target = described_class.target_for_source(
- 'src/blog/toto/content.org', project
- )
- expect(target).to eq('public_html/blog/toto/content.html')
- project = { 'path' => '~/tata', 'target' => '.' }
- target = described_class.target_for_source(
- '~/tata/tutu/content.org', project
- )
- expect(target).to eq('public_html/tutu/content.html')
- target = described_class.target_for_source(
- '~/tata/blog/content.org', project
- )
- expect(target).to eq('public_html/blog/content.html')
- end
- end
-
- context 'with a fake sources structure' do
- before do
- FileUtils.mkdir_p 'tmp/test_target/src/blog/toto'
- FileUtils.mkdir_p 'tmp/test_target/writings'
- Dir.chdir 'tmp/test_target'
- File.write 'src/test.org', 'Lorem ipsum.'
- FileUtils.touch(
- ['src/blog/test.org', 'src/blog/toto/tata.org',
- 'src/blog/toto/content.org', 'writings/notes.org']
- )
- Fronde::Config.load_test(
- 'sources' => [{ 'path' => 'src', 'target' => '.' }, 'writings']
- )
- end
-
- after do
- tear_down 'tmp/test_target'
- end
-
- it 'uses file name as title when title is empty' do
- o = described_class.new('src/test.org')
- expect(o.title).to eq('src/test.org')
- end
-
- it 'computes the right source path', core: true do
- lp = described_class.source_for_target('public_html/test.html')
- expect(lp).to eq(File.expand_path('src/test.org'))
- lp = described_class.source_for_target('public_html/blog/test.html')
- expect(lp).to eq(File.expand_path('src/blog/test.org'))
- lp = described_class.source_for_target('public_html/blog/toto/tata.html')
- expect(lp).to eq(File.expand_path('src/blog/toto/tata.org'))
- lp = described_class.source_for_target(
- 'public_html/blog/toto/content.html'
- )
- expect(lp).to eq(File.expand_path('src/blog/toto/content.org'))
- end
-
- it 'identifies a project for a given file path' do
- ps = described_class.project_for_source('src/test.org')
- expect(ps).to eq(Fronde::Config.sources[0])
- ps = described_class.project_for_source('not/known.org')
- expect(ps).to be(nil)
- end
-
- it 'identifies a source for a given published file' do
- s = described_class.source_for_target('public_html/writings/notes.html')
- expect(s).to eq(File.expand_path('writings/notes.org'))
- s = described_class.source_for_target('public_html/test.html')
- expect(s).to eq(File.expand_path('src/test.org'))
- s = described_class.source_for_target('public_html/not/known.html')
- expect(s).to be(nil)
- end
- end
-end
diff --git a/spec/fronde/slug_spec.rb b/spec/fronde/slug_spec.rb
new file mode 100644
index 0000000..78872ea
--- /dev/null
+++ b/spec/fronde/slug_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative '../../lib/fronde/slug'
+
+describe Fronde::Slug do
+ context 'with various titles' do
+ it 'transliterates them into slugs' do
+ expect(described_class.slug('toto')).to eq('toto')
+ expect(described_class.slug('TotO')).to eq('toto')
+ expect(described_class.slug('Tôto')).to eq('toto')
+ expect(described_class.slug('Tôto tata')).to eq('toto-tata')
+ expect(described_class.slug('ÀùéïỸç/+*= truñlu°`')).to eq('aueiyc-trunlu')
+ end
+ end
+end
diff --git a/spec/fronde/source_spec.rb b/spec/fronde/source_spec.rb
new file mode 100644
index 0000000..7f48e38
--- /dev/null
+++ b/spec/fronde/source_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+describe Fronde::Source do
+ it 'can not be instantiated directly' do
+ expect { described_class.new({}) }.to raise_error(NameError)
+ expect { described_class.new({}) }.to raise_error(/`fill_in_specific_config'/)
+ end
+
+ it 'stores new settings' do
+ project = described_class.new_from_config(
+ 'path' => 'src', 'type' => 'gemini'
+ )
+ project['test'] = 'Lorem ipsum'
+ expect(project['test']).to eq('Lorem ipsum')
+ end
+
+ it 'always says no for gemini blog' do
+ project = described_class.new_from_config(
+ 'path' => 'src', 'type' => 'gemini', 'is_blog' => true
+ )
+ expect(project.blog?).to be(false)
+ end
+
+ it 'computes the right name' do
+ test_cases = {
+ 'src' => 'src',
+ 'src/test' => 'src-test',
+ './src' => 'src',
+ '.' => '-',
+ '~/test' => 'test',
+ '~/test/deeper' => 'test-deeper',
+ '~/.hidden_dir' => '-hidden_dir'
+ }
+ test_cases.each do |path, result|
+ source = described_class.new_from_config(
+ 'path' => path, 'type' => 'html'
+ )
+ expect(source['name']).to eq(result)
+ end
+ end
+
+ it 'computes the right title' do
+ test_cases = [
+ 'src',
+ 'src/test',
+ './src',
+ '.',
+ '~/test',
+ '~/test/deeper',
+ '~/.hidden_dir'
+ ]
+ test_cases.each do |path|
+ source = described_class.new_from_config(
+ 'path' => path, 'type' => 'html'
+ )
+ expect(source['title']).to eq(path)
+ end
+ end
+
+ it 'computes the right target' do
+ test_cases = {
+ 'src' => 'src',
+ 'src/test' => 'test',
+ './src' => 'src',
+ '.' => '',
+ '~/test' => 'test',
+ '~/test/deeper' => 'deeper',
+ '~/.hidden_dir' => 'hidden_dir'
+ }
+ test_cases.each do |path, result|
+ source = described_class.new_from_config(
+ 'path' => path, 'type' => 'html'
+ )
+ expect(source['target']).to eq(result)
+ end
+ end
+
+ it 'computes the right pub_file path for theoritical sources' do
+ config = described_class.canonical_config('path' => 'src', 'target' => '.')
+ project = described_class.new_from_config(config)
+ target = project.target_for 'src/test.org'
+ expect(target).to eq('/test.html')
+ target = project.target_for 'src/blog/test.org'
+ expect(target).to eq('/blog/test.html')
+ target = project.target_for 'src/blog/toto/tata.org'
+ expect(target).to eq('/blog/toto/tata.html')
+ target = project.target_for 'src/blog/toto/content.org'
+ expect(target).to eq('/blog/toto/content.html')
+
+ config = described_class.canonical_config('path' => '~/tata', 'target' => '.')
+ project = described_class.new_from_config(config)
+ target = project.target_for '~/tata/tutu/content.org'
+ expect(target).to eq('/tutu/content.html')
+ target = project.target_for '~/tata/blog/content.org'
+ expect(target).to eq('/blog/content.html')
+ end
+end
diff --git a/spec/fronde/templater_spec.rb b/spec/fronde/templater_spec.rb
index d3166c8..71fc5fb 100644
--- a/spec/fronde/templater_spec.rb
+++ b/spec/fronde/templater_spec.rb
@@ -1,6 +1,22 @@
# frozen_string_literal: true
-require 'fronde/templater'
+require_relative '../../lib/fronde/templater'
+
+def template_proof(digest)
+ <<~RESULT
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- Fronde Template: #{digest} -->
+ <meta property="test" content="TEST">
+ <title>My website</title>
+ </head>
+ <body>
+ <h1>My website</h1>
+ </body>
+ </html>
+ RESULT
+end
describe Fronde::Templater do
let(:html_base) do
@@ -17,336 +33,412 @@ describe Fronde::Templater do
HTML
end
- context 'with a testing website' do
- let(:metatag) { '<meta property="test" content="TEST">' }
- let(:metatag_digest) { Digest::MD5.hexdigest(metatag) }
- let(:result) do
- <<~RESULT
- <!DOCTYPE html>
- <html>
- <head>
- <!-- Fronde Template: #{metatag_digest} -->
+ let(:metatag) { '<meta property="test" content="TEST">' }
- <meta property="test" content="TEST">
- <title>My website</title>
- </head>
- <body>
- <h1>My website</h1>
- </body>
- </html>
- RESULT
- end
+ before { init_testing_environment }
+
+ after { tear_down 'tmp/website_testing' }
+ context 'with a simple customization process' do
before do
- init_testing_website
+ FileUtils.mkdir %w[src public_html]
+ FileUtils.touch 'src/customize_test.org'
+ File.write 'public_html/customize_test.html', html_base
end
after do
- tear_down 'tmp/website_testing'
+ FileUtils.rm_r %w[public_html src]
+ Fronde::CONFIG.reset
end
- context 'with a simple customization process' do
- before do
- FileUtils.mkdir 'public_html'
- File.write('public_html/customize_test.html', html_base)
- end
+ it 'skips file on broken html' do
+ FileUtils.touch 'src/broken.org'
+ File.write 'public_html/broken.html', 'No markup at all'
+ Fronde::CONFIG.load_test(
+ 'templates' => [
+ { 'selector' => 'title',
+ 'content' => metatag }
+ ]
+ )
+ expect do
+ described_class.customize_output('public_html/broken.html')
+ end.to(
+ output(
+ "No head tag found in file public_html/broken.html.\n"
+ ).to_stderr
+ )
+ end
- after do
- FileUtils.rm_r ['public_html', 'src'], force: true
- Fronde::Config.reset
- end
+ it 'skips file on unknown source' do
+ FileUtils.mkdir 'public_html/customize'
+ File.write 'public_html/customize/test.html', html_base
+ test_path = "#{Dir.pwd}/public_html/customize/test.html"
+ expect do
+ described_class.customize_output('public_html/customize/test.html')
+ end.to(
+ output(
+ "No project found for #{test_path}. Publication will fail.\n"
+ ).to_stderr
+ )
+ end
- it 'customizes a given html file with simple template' do
- Fronde::Config.load_test(
- 'templates' => [
- { 'selector' => 'title',
- 'content' => metatag }
- ]
- )
+ it 'warns when target node cannot be found' do
+ Fronde::CONFIG.load_test(
+ 'templates' => [{ 'selector' => 'title', 'source' => 'h2' }]
+ )
+ expect do
described_class.customize_output('public_html/customize_test.html')
- local_result = <<~RESULT
- <!DOCTYPE html>
- <html>
- <head>
- <!-- Fronde Template: #{metatag_digest} -->
+ end.to(
+ output(
+ "No element found with the selector h2 in public_html/customize_test.html.\n"
+ ).to_stderr
+ )
+ end
- <title>My website</title>
- <meta property="test" content="TEST">
- </head>
- <body>
- <h1>My website</h1>
- </body>
- </html>
- RESULT
- expect(File.read('public_html/customize_test.html')).to eq(local_result)
- end
+ it 'customizes a given html file with simple template' do
+ Fronde::CONFIG.load_test(
+ 'templates' => [
+ { 'selector' => 'title',
+ 'content' => metatag }
+ ]
+ )
+ described_class.customize_output('public_html/customize_test.html')
+ local_result = <<~RESULT
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- Fronde Template: e5a93c7cd8b3e75e4956a81dae18b7fe -->
+ <title>My website</title>
+ <meta property="test" content="TEST">
+ </head>
+ <body>
+ <h1>My website</h1>
+ </body>
+ </html>
+ RESULT
+ expect(File.read('public_html/customize_test.html')).to eq(local_result)
+ end
- it 'customizes a given html file with a given org object' do
- Fronde::Config.load_test(
- 'templates' => [
- { 'selector' => 'title',
- 'content' => metatag }
- ]
- )
- org_content = <<~ORG
- #+title: Index file
+ it 'customizes a given html file with a given org object' do
+ Fronde::CONFIG.load_test(
+ 'templates' => [
+ { 'selector' => 'title',
+ 'content' => metatag }
+ ]
+ )
+ org_content = <<~ORG
+ #+title: Index file
- My website
- ORG
- FileUtils.mkdir 'src'
- File.write('src/index.org', org_content)
- o = Fronde::OrgFile.new('src/index.org')
- described_class.customize_output('public_html/customize_test.html', o)
- local_result = <<~RESULT
- <!DOCTYPE html>
- <html>
- <head>
- <!-- Fronde Template: #{metatag_digest} -->
+ My website
+ ORG
+ File.write 'src/customize_test.org', org_content
+ described_class.customize_output('public_html/customize_test.html')
+ local_result = <<~RESULT
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- Fronde Template: e5a93c7cd8b3e75e4956a81dae18b7fe -->
+ <title>My website</title>
+ <meta property="test" content="TEST">
+ </head>
+ <body>
+ <h1>My website</h1>
+ </body>
+ </html>
+ RESULT
+ expect(File.read('public_html/customize_test.html')).to eq(local_result)
+ end
- <title>My website</title>
- <meta property="test" content="TEST">
- </head>
- <body>
- <h1>My website</h1>
- </body>
- </html>
- RESULT
- expect(File.read('public_html/customize_test.html')).to eq(local_result)
- end
+ it 'customizes a given html file with before' do
+ Fronde::CONFIG.load_test(
+ 'templates' => [
+ { 'selector' => 'title',
+ 'type' => 'before',
+ 'content' => metatag }
+ ]
+ )
+ described_class.customize_output('public_html/customize_test.html')
+ expect(File.read('public_html/customize_test.html')).to(
+ eq(template_proof('89a9811158ace2802f70a06146f96e46'))
+ )
+ end
- it 'customizes a given html file with before' do
- Fronde::Config.load_test(
- 'templates' => [
- { 'selector' => 'title',
- 'type' => 'before',
- 'content' => metatag }
- ]
- )
- described_class.customize_output('public_html/customize_test.html')
- expect(File.read('public_html/customize_test.html')).to eq(result)
- end
+ it 'customizes a given html file with after' do
+ Fronde::CONFIG.load_test(
+ 'templates' => [
+ { 'selector' => 'title',
+ 'type' => 'after',
+ 'content' => metatag }
+ ]
+ )
+ described_class.customize_output('public_html/customize_test.html')
+ local_result = <<~RESULT
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- Fronde Template: 04d9fa11efccbebe205507cceace0b3c -->
+ <title>My website</title>
+ <meta property="test" content="TEST">
+ </head>
+ <body>
+ <h1>My website</h1>
+ </body>
+ </html>
+ RESULT
+ expect(File.read('public_html/customize_test.html')).to eq(local_result)
+ end
- it 'customizes a given html file with after' do
- Fronde::Config.load_test(
- 'templates' => [
- { 'selector' => 'title',
- 'type' => 'after',
- 'content' => metatag }
- ]
- )
- described_class.customize_output('public_html/customize_test.html')
- local_result = <<~RESULT
- <!DOCTYPE html>
- <html>
- <head>
- <!-- Fronde Template: #{metatag_digest} -->
+ it 'customizes a given html file with replace content' do
+ Fronde::CONFIG.load_test(
+ 'templates' => [
+ { 'selector' => 'body>h1',
+ 'type' => 'replace',
+ 'content' => '<p>Toto tata</p>' }
+ ]
+ )
+ described_class.customize_output('public_html/customize_test.html')
+ local_result = <<~RESULT
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- Fronde Template: 1db4871b83a08057505303f8298c3060 -->
+ <title>My website</title>
+ </head>
+ <body>
+ <p>Toto tata</p>
+ </body>
+ </html>
+ RESULT
+ expect(File.read('public_html/customize_test.html')).to eq(local_result)
+ end
- <title>My website</title>
- <meta property="test" content="TEST">
- </head>
- <body>
- <h1>My website</h1>
- </body>
- </html>
- RESULT
- expect(File.read('public_html/customize_test.html')).to eq(local_result)
- end
+ it 'customizes a given html file with previous comments in head' do
+ Fronde::CONFIG.load_test(
+ 'templates' => [
+ { 'selector' => 'body>h1',
+ 'type' => 'replace',
+ 'content' => '<p>Toto tata</p>' }
+ ]
+ )
+ local_html_base = <<~HTML
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- This is a test comment -->
+ <title>My website</title>
+ </head>
+ <body>
+ <h1>My website</h1>
+ </body>
+ </html>
+ HTML
+ FileUtils.touch 'src/customize_test2.org'
+ File.write 'public_html/customize_test2.html', local_html_base
+ described_class.customize_output('public_html/customize_test2.html')
+ local_result = <<~RESULT
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- Fronde Template: 1db4871b83a08057505303f8298c3060 -->
+ <!-- This is a test comment -->
+ <title>My website</title>
+ </head>
+ <body>
+ <p>Toto tata</p>
+ </body>
+ </html>
+ RESULT
+ expect(File.read('public_html/customize_test2.html')).to eq(local_result)
+ end
- it 'customizes a given html file with replace content' do
- Fronde::Config.load_test(
- 'templates' => [
- { 'selector' => 'body>h1',
- 'type' => 'replace',
- 'content' => '<p>Toto tata</p>' }
- ]
- )
- described_class.customize_output('public_html/customize_test.html')
- local_result = <<~RESULT
- <!DOCTYPE html>
- <html>
- <head>
- <!-- Fronde Template: #{Digest::MD5.hexdigest('<p>Toto tata</p>')} -->
+ it 'does not customize a given html file with wrong templates' do
+ result = File.read('public_html/customize_test.html')
+ Fronde::CONFIG.load_test(
+ 'templates' => [
+ { 'type' => 'replace',
+ 'content' => '<p>Toto tata</p>' },
+ { 'selector' => 'body>h1',
+ 'type' => 'replace' }
+ ]
+ )
+ described_class.customize_output('public_html/customize_test.html')
+ expect(File.read('public_html/customize_test.html')).to eq(result)
+ end
- <title>My website</title>
- </head>
- <body>
- <p>Toto tata</p>
- </body>
- </html>
- RESULT
- expect(File.read('public_html/customize_test.html')).to eq(local_result)
- end
+ it 'does not customize a given html file with no templates' do
+ result = File.read 'public_html/customize_test.html'
+ Fronde::CONFIG.load_test({})
+ described_class.customize_output('public_html/customize_test.html')
+ expect(File.read('public_html/customize_test.html')).to eq(result)
+ end
- it 'customizes a given html file with previous comments in head' do
- Fronde::Config.load_test(
- 'templates' => [
- { 'selector' => 'body>h1',
- 'type' => 'replace',
- 'content' => '<p>Toto tata</p>' }
- ]
- )
- local_html_base = <<~HTML
- <!DOCTYPE html>
- <html>
- <head>
- <!-- This is a test comment -->
- <title>My website</title>
- </head>
- <body>
+ it 'moves elements around' do
+ other_html_base = <<~HTML
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>My website</title>
+ </head>
+ <body>
+ <div id="content">
<h1>My website</h1>
- </body>
- </html>
- HTML
- File.write('public_html/customize_test2.html', local_html_base)
- described_class.customize_output('public_html/customize_test2.html')
- local_result = <<~RESULT
- <!DOCTYPE html>
- <html>
- <head>
- <!-- Fronde Template: #{Digest::MD5.hexdigest('<p>Toto tata</p>')} -->
-
- <!-- This is a test comment -->
- <title>My website</title>
- </head>
- <body>
- <p>Toto tata</p>
- </body>
- </html>
- RESULT
- expect(File.read('public_html/customize_test2.html')).to eq(local_result)
- end
-
- it 'does not customize a given html file with wrong templates' do
- result = File.read('public_html/customize_test.html')
- Fronde::Config.load_test(
- 'templates' => [
- { 'type' => 'replace',
- 'content' => '<p>Toto tata</p>' },
- { 'selector' => 'body>h1',
- 'type' => 'replace' }
- ]
- )
- described_class.customize_output('public_html/customize_test.html')
- expect(File.read('public_html/customize_test.html')).to eq(result)
- end
-
- it 'does not customize a given html file with no templates' do
- result = File.read('public_html/customize_test.html')
- Fronde::Config.load_test({})
- described_class.customize_output('public_html/customize_test.html')
- expect(File.read('public_html/customize_test.html')).to eq(result)
- end
-
- it 'moves elements around' do
- other_html_base = <<~HTML
- <!DOCTYPE html>
- <html>
- <head>
- <title>My website</title>
- </head>
- <body>
- <div id="content">
- <h1>My website</h1>
- <nav>My menu</nav><p>Lorem ipsum...</p>
- </div>
- </body>
- </html>
- HTML
- File.write('public_html/customize_test.html', other_html_base)
- Fronde::Config.load_test(
- 'templates' => [
- { 'type' => 'before',
- 'selector' => 'div#content',
- 'source' => 'div#content>nav' }
- ]
- )
- described_class.customize_output('public_html/customize_test.html')
- result = <<~HTML
- <!DOCTYPE html>
- <html>
- <head>
- <!-- Fronde Template: #{Digest::MD5.hexdigest('<nav>My menu</nav>')} -->
+ <nav>My menu</nav><p>Lorem ipsum...</p>
+ </div>
+ </body>
+ </html>
+ HTML
+ File.write 'public_html/customize_test.html', other_html_base
+ Fronde::CONFIG.load_test(
+ 'templates' => [
+ { 'type' => 'before',
+ 'selector' => 'div#content',
+ 'source' => 'div#content>nav' }
+ ]
+ )
+ described_class.customize_output('public_html/customize_test.html')
+ result = <<~HTML
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- Fronde Template: 850906143b13df4a1d627f062fb667d0 -->
+ <title>My website</title>
+ </head>
+ <body>
+ <nav>My menu</nav><div id="content">
+ <h1>My website</h1>
+ <p>Lorem ipsum...</p>
+ </div>
+ </body>
+ </html>
+ HTML
+ expect(File.read('public_html/customize_test.html')).to eq(result)
+ end
+ end
- <title>My website</title>
- </head>
- <body>
- <nav>My menu</nav><div id="content">
- <h1>My website</h1>
- <p>Lorem ipsum...</p>
- </div>
- </body>
- </html>
- HTML
- expect(File.read('public_html/customize_test.html')).to eq(result)
- end
+ context 'with multiple customize call' do
+ before do
+ FileUtils.mkdir_p ['src/customize', 'public_html/customize']
+ FileUtils.touch 'src/customize_test.org'
+ File.write 'public_html/customize_test.html', html_base
+ FileUtils.touch 'src/customize/test.org'
+ File.write 'public_html/customize/test.html', html_base
+ Fronde::CONFIG.load_test(
+ 'templates' => [
+ { 'selector' => 'title',
+ 'path' => '/customize/*',
+ 'type' => 'before',
+ 'content' => metatag }
+ ]
+ )
end
- context 'with multiple customize call' do
- before do
- FileUtils.mkdir_p 'public_html/customize'
- File.write('public_html/customize_test.html', html_base)
- File.write('public_html/customize/test.html', html_base)
- Fronde::Config.load_test(
- 'templates' => [
- { 'selector' => 'title',
- 'path' => '/customize/*',
- 'type' => 'before',
- 'content' => metatag }
- ]
- )
- end
+ after { FileUtils.rm_r %w[public_html src] }
- after do
- FileUtils.rm_r 'public_html', force: true
- Fronde::Config.reset
- end
+ it 'customizes a file on a specific path' do
+ described_class.customize_output('public_html/customize_test.html')
+ expect(File.read('public_html/customize_test.html')).to eq(html_base)
+ # Run it a second time
+ described_class.customize_output('public_html/customize/test.html')
+ expect(File.read('public_html/customize/test.html')).to(
+ eq(template_proof('d310e02360415c4ddd995a0fcb104057'))
+ )
+ end
- it 'customizes a file on a specific path' do
- described_class.customize_output('public_html/customize_test.html')
- expect(File.read('public_html/customize_test.html')).to eq(html_base)
- described_class.customize_output('public_html/customize/test.html')
- expect(File.read('public_html/customize/test.html')).to eq(result)
- end
+ it 'does not customize twice a file' do
+ described_class.customize_output('public_html/customize/test.html')
+ expect(File.read('public_html/customize/test.html')).to(
+ eq(template_proof('d310e02360415c4ddd995a0fcb104057'))
+ )
+ # Run it a second time
+ described_class.customize_output('public_html/customize/test.html')
+ expect(File.read('public_html/customize/test.html')).to(
+ eq(template_proof('d310e02360415c4ddd995a0fcb104057'))
+ )
+ end
- it 'does not customize twice a file' do
- described_class.customize_output('public_html/customize/test.html')
- expect(File.read('public_html/customize/test.html')).to eq(result)
- described_class.customize_output('public_html/customize/test.html')
- expect(File.read('public_html/customize/test.html')).to eq(result)
- end
+ it 'does not remove moving elements' do
+ other_html_base = <<~HTML
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>My website</title>
+ </head>
+ <body>
+ <div id="content">
+ <h1>My website</h1>
+ <nav>My menu</nav><p>Lorem ipsum...</p>
+ </div>
+ </body>
+ </html>
+ HTML
+ File.write 'public_html/customize_test.html', other_html_base
+ Fronde::CONFIG.load_test(
+ 'templates' => [
+ { 'type' => 'before',
+ 'selector' => 'div#content',
+ 'source' => 'div#content>nav' }
+ ]
+ )
+ described_class.customize_output('public_html/customize_test.html')
+ result = <<~HTML
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- Fronde Template: 850906143b13df4a1d627f062fb667d0 -->
+ <title>My website</title>
+ </head>
+ <body>
+ <nav>My menu</nav><div id="content">
+ <h1>My website</h1>
+ <p>Lorem ipsum...</p>
+ </div>
+ </body>
+ </html>
+ HTML
+ expect(File.read('public_html/customize_test.html')).to eq(result)
+ # Run it a second time
+ described_class.customize_output('public_html/customize_test.html')
+ expect(File.read('public_html/customize_test.html')).to eq(result)
end
+ end
- context 'with multiple path to customize' do
- before do
- FileUtils.mkdir_p ['public_html/customize', 'public_html/other']
- File.write('public_html/customize_test.html', html_base)
- File.write('public_html/customize/test.html', html_base)
- File.write('public_html/other/file.html', html_base)
- Fronde::Config.load_test(
- 'templates' => [
- { 'selector' => 'title',
- 'path' => ['/customize/*', '/other/*'],
- 'type' => 'before',
- 'content' => metatag },
- { 'type' => 'replace',
- 'content' => 'Youpeee' }
- ]
- )
- end
+ context 'with multiple path to customize' do
+ before do
+ FileUtils.mkdir_p(
+ ['src/customize', 'src/other',
+ 'public_html/customize', 'public_html/other']
+ )
+ FileUtils.touch 'src/customize_test.org'
+ File.write 'public_html/customize_test.html', html_base
+ FileUtils.touch 'src/customize/test.org'
+ File.write 'public_html/customize/test.html', html_base
+ FileUtils.touch 'src/other/file.org'
+ File.write 'public_html/other/file.html', html_base
+ Fronde::CONFIG.load_test(
+ 'templates' => [
+ { 'selector' => 'title',
+ 'path' => ['/customize/*', '/other/*'],
+ 'type' => 'before',
+ 'content' => metatag },
+ { 'type' => 'replace',
+ 'content' => 'Youpeee' }
+ ]
+ )
+ end
- after do
- FileUtils.rm_r 'public_html', force: true
- Fronde::Config.reset
- end
+ after { FileUtils.rm_r %w[public_html src] }
- it 'customizes a file on a specific path' do
- described_class.customize_output('public_html/customize_test.html')
- expect(File.read('public_html/customize_test.html')).to eq(html_base)
- described_class.customize_output('public_html/customize/test.html')
- expect(File.read('public_html/customize/test.html')).to eq(result)
- described_class.customize_output('public_html/other/file.html')
- expect(File.read('public_html/other/file.html')).to eq(result)
- end
+ it 'customizes a file on a specific path' do
+ described_class.customize_output('public_html/customize_test.html')
+ expect(File.read('public_html/customize_test.html')).to eq(html_base)
+ described_class.customize_output('public_html/customize/test.html')
+ expect(File.read('public_html/customize/test.html')).to(
+ eq(template_proof('651709ceba80342164e93a462d4743ed'))
+ )
+ described_class.customize_output('public_html/other/file.html')
+ expect(File.read('public_html/other/file.html')).to(
+ eq(template_proof('651709ceba80342164e93a462d4743ed'))
+ )
end
end
end
diff --git a/spec/fronde/utils_spec.rb b/spec/fronde/utils_spec.rb
deleted file mode 100644
index 993d841..0000000
--- a/spec/fronde/utils_spec.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-require 'fronde/utils'
-
-describe Fronde::Utils do
- context 'with theoritical fronde arguments' do
- it 'returns decorated options' do
- expect(described_class.decorate_option('-a')).to(
- eq(['-aAUTHOR', '--author AUTHOR'])
- )
- expect(described_class.decorate_option('-l')).to(
- eq(['-lLOCALE', '--lang LOCALE'])
- )
- expect(described_class.decorate_option('-v')).to(
- eq(['-v', '--verbose'])
- )
- end
-
- it 'summarizes basic commands' do
- basic_cmd = [
- " -h, --help Display help for a command and exit.\n",
- ' -V, --version Display Fronde version and exit.'
- ].join
- expect(described_class.summarize_command('basic')).to eq(basic_cmd)
- end
-
- it 'summarizes open commands' do
- basic_cmd = [
- " -aAUTHOR, --author AUTHOR \n",
- " -h, --help Display help for a command and exit.\n",
- " -lLOCALE, --lang LOCALE \n",
- " -tTITLE, --title TITLE \n",
- ' -v, --verbose '
- ].join
- expect(described_class.summarize_command('open')).to eq(basic_cmd)
- end
-
- it 'lists possible commands' do
- basic_cmd = [
- " init Initialize your Fronde instance (you just need to do it once).\n",
- " update Update Fronde dependency (to be run once in a while).\n",
- " config Alias for update.\n",
- " preview Start a test web server to preview your website on http://127.0.0.1:5000\n",
- " open Open or create an org file.\n",
- " edit Alias for open.\n",
- " build Compile your org files to HTML.\n",
- " publish Push local changes to your public web server.\n",
- ' help Alias for the -h switch.'
- ].join
- expect(described_class.list_commands).to eq(basic_cmd)
- end
-
- it 'resolves alias' do
- expect(described_class.resolve_possible_alias('init')).to eq('init')
- expect(described_class.resolve_possible_alias('update')).to eq('update')
- expect(described_class.resolve_possible_alias('config')).to eq('update')
- expect(described_class.resolve_possible_alias('build')).to eq('build')
- expect(described_class.resolve_possible_alias('edit')).to eq('open')
- expect(described_class.resolve_possible_alias('wrong')).to eq('basic')
- end
-
- it 'returns possible options for a command' do
- init_opts = described_class.command_options('init')
- expect(init_opts).to have_key(:name)
- expect(init_opts[:name]).to eq('init')
- expect(init_opts).to have_key(:opts)
- expect(init_opts[:opts]).to contain_exactly('-a', '-h', '-l', '-t', '-v')
- expect(init_opts).not_to have_key(:alias)
-
- config_opts = described_class.command_options('config')
- expect(config_opts).to have_key(:name)
- expect(config_opts[:name]).to eq('update')
- expect(config_opts).to have_key(:opts)
- expect(config_opts[:opts]).to contain_exactly('-a', '-h', '-l', '-t', '-v')
- expect(config_opts).not_to have_key(:alias)
- end
-
- context 'with config' do
- before do
- Fronde::Config.reset
- end
-
- it 'selects the right throbber' do
- frames = described_class.send(:select_throbber_frames)
- expect(frames).to(
- eq(['⠁ ⠂ ⠄ ⡀ ⠄ ⠂ ⠁', '⠂ ⠁ ⠂ ⠄ ⡀ ⠄ ⠂', '⠄ ⠂ ⠁ ⠂ ⠄ ⡀ ⠄',
- '⡀ ⠄ ⠂ ⠁ ⠂ ⠄ ⡀', '⠄ ⡀ ⠄ ⠂ ⠁ ⠂ ⠄', '⠂ ⠄ ⡀ ⠄ ⠂ ⠁ ⠂'])
- )
- Fronde::Config.load_test('throbber' => 'default')
- frames = described_class.send(:select_throbber_frames)
- expect(frames).to(
- eq(['⠁ ⠂ ⠄ ⡀ ⠄ ⠂ ⠁', '⠂ ⠁ ⠂ ⠄ ⡀ ⠄ ⠂', '⠄ ⠂ ⠁ ⠂ ⠄ ⡀ ⠄',
- '⡀ ⠄ ⠂ ⠁ ⠂ ⠄ ⡀', '⠄ ⡀ ⠄ ⠂ ⠁ ⠂ ⠄', '⠂ ⠄ ⡀ ⠄ ⠂ ⠁ ⠂'])
- )
- Fronde::Config.load_test('throbber' => 'wrong')
- frames = described_class.send(:select_throbber_frames)
- expect(frames).to(
- eq(['⠁ ⠂ ⠄ ⡀ ⠄ ⠂ ⠁', '⠂ ⠁ ⠂ ⠄ ⡀ ⠄ ⠂', '⠄ ⠂ ⠁ ⠂ ⠄ ⡀ ⠄',
- '⡀ ⠄ ⠂ ⠁ ⠂ ⠄ ⡀', '⠄ ⡀ ⠄ ⠂ ⠁ ⠂ ⠄', '⠂ ⠄ ⡀ ⠄ ⠂ ⠁ ⠂'])
- )
- Fronde::Config.load_test('throbber' => 'basic')
- frames = described_class.send(:select_throbber_frames)
- expect(frames).to eq('-\|/')
- end
- end
- end
-end
diff --git a/spec/rake_cli_spec.rb b/spec/rake_cli_spec.rb
new file mode 100644
index 0000000..d4ccece
--- /dev/null
+++ b/spec/rake_cli_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'rake'
+
+context 'with an example project' do
+ before { init_testing_environment }
+
+ after { tear_down 'tmp/website_testing' }
+
+ it 'generates a zsh completion file' do
+ proof = File.expand_path 'data/fronde.zsh', __dir__
+ proof_content = File.read(proof)
+ expect { rake.invoke_task('cli:zsh_complete') }.to(
+ output(proof_content).to_stdout
+ )
+ end
+end
diff --git a/spec/rake_org_spec.rb b/spec/rake_org_spec.rb
index 3c682ef..498f939 100644
--- a/spec/rake_org_spec.rb
+++ b/spec/rake_org_spec.rb
@@ -2,12 +2,14 @@
require 'rake'
-context 'with working org files' do
- let(:org_dir) { "lib/org-#{Fronde::Config.org_last_version}" }
+context 'when managing Org installation' do
+ let(:org_dir) { "lib/org-#{Fronde::Org.last_version}" }
+
+ before(:all) { init_fake_org_install } # rubocop:disable RSpec/BeforeAfterAll
before do
- Fronde::Config.reset
- init_testing_website
+ init_testing_environment
+ Fronde::CONFIG.reset
end
after do
@@ -16,66 +18,202 @@ context 'with working org files' do
tear_down 'tmp/website_testing'
end
- it 'compiles org-config.el', rake: true do
- rake.invoke_task('var/lib/org-config.el')
- expect(File.exist?('var/lib/org-config.el')).to be(true)
- proof = File.expand_path('data/org-config-proof.el', __dir__)
- base_dir = File.expand_path('../', __dir__)
- proof_content = File.read(proof)
- .gsub(/__TEST_DIR__/, Dir.pwd)
- .gsub(/__BASE_DIR__/, base_dir)
- .gsub(/__VERSION__/, Fronde::VERSION)
- .gsub(/__ORG_VERSION__/, Fronde::Config.org_last_version)
- expect(File.read('var/lib/org-config.el')).to eq(proof_content)
- end
-
- it 'compiles org-config.el for blog', rake: true do
- old_conf = Fronde::Config.settings.dup
- old_conf['theme'] = 'toto'
- Fronde::Config.load_test(old_conf)
+ it 'compiles org-config.el' do
rake.invoke_task('var/lib/org-config.el')
- expect(File.exist?('var/lib/org-config.el')).to be(true)
- proof = File.expand_path('data/org-config-blog-proof.el', __dir__)
- base_dir = File.expand_path('../', __dir__)
- proof_content = File.read(proof)
- .gsub(/__TEST_DIR__/, Dir.pwd)
- .gsub(/__BASE_DIR__/, base_dir)
- .gsub(/__VERSION__/, Fronde::VERSION)
- .gsub(/__ORG_VERSION__/, Fronde::Config.org_last_version)
- expect(File.read('var/lib/org-config.el')).to eq(proof_content)
- end
-
- it 'creates .dir-locals.el', rake: true do
+ expect(File.exist?('var/lib/org-config.el')).to be true
+ proof = proof_content 'org-config-proof.el'
+ expect(File.read('var/lib/org-config.el')).to eq(proof)
+ end
+
+ it 'creates .dir-locals.el' do
rake.invoke_task('.dir-locals.el')
- expect(File.exist?('.dir-locals.el')).to be(true)
- proof = File.expand_path('var/lib/org-config.el', Dir.pwd)
+ expect(File.exist?('.dir-locals.el')).to be true
+ proof = File.expand_path('var/lib/org-config.el')
expect(File.read('.dir-locals.el')).to(
eq("((org-mode . ((eval . (load-file \"#{proof}\")))))")
)
end
- it 'installs Org', rake: true do
+ it 'tries to download Org verbosely' do
+ allow(Fronde::Org).to receive(:download).and_return('test')
+ expect do
+ rake(verbose: true).invoke_task('var/tmp/org.tar.gz')
+ end.to output("Org version test has been downloaded.\n").to_stderr
+ end
+
+ it 'does not download Org if a copy is already cached' do
+ copy_org_tarball_to_fake_tmp
+ expect(File.exist?('var/tmp/org.tar.gz')).to be true
+ expect do
+ rake(verbose: true).invoke_task('var/tmp/org.tar.gz')
+ end.to output('').to_stderr
+ end
+
+ it 'warns user if no last version can be found' do
+ allow(Fronde::Org).to receive(:fetch_version_number).and_return(nil)
+ expect { Fronde::Org.download }.to raise_error RuntimeError
+ expect { Fronde::Org.download }.to(
+ raise_error('No remote Org version found')
+ )
+ expect { rake(verbose: true).invoke_task('org:install') }.to(
+ output(
+ /Impossible to download Org now\. Please try again later\./
+ ).to_stderr
+ )
+ expect { rake(verbose: false).invoke_task('org:install') }.to(
+ output(/An error occurred\./).to_stderr
+ )
+ end
+
+ it 'installs Org' do
copy_org_tarball_to_fake_tmp
rake(verbose: false).invoke_task('org:install')
- expect(File.exist?('var/lib/org-config.el')).to be(true)
- expect(File.exist?('.dir-locals.el')).to be(true)
- expect(File.exist?("#{org_dir}/lisp/org-loaddefs.el")).to be(true)
+ expect(File.exist?('var/lib/org-config.el')).to be true
+ expect(File.exist?('.dir-locals.el')).to be true
+ expect(File.exist?("#{org_dir}/lisp/org-loaddefs.el")).to be true
end
- it 'installs Org in verbose mode', rake: true do
+ it 'installs Org in verbose mode' do
copy_org_tarball_to_fake_tmp
rake(verbose: true).invoke_task('org:install')
- expect(File.exist?('var/lib/org-config.el')).to be(true)
- expect(File.exist?('.dir-locals.el')).to be(true)
- expect(File.exist?("#{org_dir}/lisp/org-loaddefs.el")).to be(true)
+ expect(File.exist?('var/lib/org-config.el')).to be true
+ expect(File.exist?('.dir-locals.el')).to be true
+ expect(File.exist?("#{org_dir}/lisp/org-loaddefs.el")).to be true
end
- it 'removes old Org version while installing', rake: true do
+ it 'compiles Org successfully' do
+ copy_org_tarball_to_fake_tmp
+ expect { rake(verbose: true).invoke_task('org:compile') }.to(
+ output(/Org version [0-9.]+ has been locally installed./).to_stderr
+ )
+ end
+
+ it 'does not compile org twice if already there' do
+ copy_org_tarball_to_fake_tmp
+ copy_org_lisp_files_to_fake_tmp
+ # Second call should not output the same sentence, as the
+ # compilation will be skipped
+ expect { rake(verbose: true).invoke_task('org:compile') }.not_to(
+ output(/Org version [0-9.]+ has been locally installed./).to_stderr
+ )
+ allow(Fronde::Org).to receive(:compile)
+ rake.invoke_task('org:compile')
+ expect(Fronde::Org).not_to have_received(:compile)
+ end
+
+ it 'creates only a public_html folder with no gemini sources' do
+ copy_org_tarball_to_fake_tmp
+ # Speed up rake test
+ allow(Fronde::Org).to receive(:last_version).and_return('test')
+ FileUtils.mkdir_p 'lib/org-test/lisp'
+
+ config = { 'sources' => [{ 'path' => 'data', 'type' => 'html' }] }
+ Fronde::CONFIG.load_test config
+ rake(verbose: false).invoke_task('org:install')
+ expect(Dir.exist?('public_gmi')).to be false
+ expect(Dir.exist?('public_html')).to be true
+ end
+
+ it 'creates only a public_gmi folder with no html sources' do
+ copy_org_tarball_to_fake_tmp
+ # Speed up rake test
+ allow(Fronde::Org).to receive(:last_version).and_return('test')
+ FileUtils.mkdir_p 'lib/org-test/lisp'
+
+ config = { 'sources' => [{ 'path' => 'data', 'type' => 'gemini' }] }
+ Fronde::CONFIG.load_test config
+ rake(verbose: false).invoke_task('org:install')
+ expect(Dir.exist?('public_gmi')).to be true
+ expect(Dir.exist?('public_html')).to be false
+ end
+
+ it 'creates both public_gmi and public_html folders when needed' do
+ copy_org_tarball_to_fake_tmp
+ # Speed up rake test
+ allow(Fronde::Org).to receive(:last_version).and_return('test')
+ FileUtils.mkdir_p 'lib/org-test/lisp'
+
+ config = {
+ 'sources' => [{ 'path' => 'src', 'type' => 'html' },
+ { 'path' => 'data', 'type' => 'gemini' }]
+ }
+ Fronde::CONFIG.load_test config
+ rake(verbose: false).invoke_task('org:install')
+ expect(Dir.exist?('public_gmi')).to be true
+ expect(Dir.exist?('public_html')).to be true
+ end
+
+ it 'removes old Org version while installing' do
FileUtils.mkdir_p 'lib/org-2.3'
- expect(Dir.exist?('lib/org-2.3')).to be(true)
+ expect(Dir.exist?('lib/org-2.3')).to be true
copy_org_tarball_to_fake_tmp
- rake(verbose: true).invoke_task('org:install')
- rake(verbose: false).invoke_task('org:install') # To check coverage
- expect(Dir.exist?('lib/org-2.3')).to be(false)
+ allow(Fronde::Org).to receive(:compile)
+ rake.invoke_task('org:install')
+ expect(Dir.exist?('lib/org-2.3')).to be false
+ end
+
+ it 'does not touch org install files if no upgrade needed' do
+ copy_org_tarball_to_fake_tmp
+ copy_org_lisp_files_to_fake_tmp
+ rake.invoke_task 'org:install'
+ config_time = File.mtime 'var/lib/org-config.el'
+ locals_time = File.mtime '.dir-locals.el'
+ orgball_time = File.mtime 'var/tmp/org.tar.gz'
+ org_version = Fronde::Org.current_version
+ orglisp_time = File.mtime "lib/org-#{org_version}/lisp/org-loaddefs.el"
+ allow(Fronde::Org).to receive(:download).and_return org_version
+ allow(Fronde::Org).to receive(:compile)
+ sleep 1 # To change mtimes
+ rake.invoke_task 'org:upgrade'
+ expect(File.mtime('var/lib/org-config.el')).not_to eq config_time
+ expect(File.mtime('.dir-locals.el')).not_to eq locals_time
+ expect(Fronde::Org).not_to have_received(:download)
+ expect(File.mtime('var/tmp/org.tar.gz')).to eq orgball_time
+ expect(Fronde::Org).not_to have_received(:compile)
+ expect(File.mtime("lib/org-#{org_version}/lisp/org-loaddefs.el")).to(
+ eq(orglisp_time)
+ )
+ end
+
+ it 'changes org install files when an upgrade is needed' do
+ copy_org_tarball_to_fake_tmp
+ copy_org_lisp_files_to_fake_tmp
+ rake.invoke_task 'org:install'
+ config_time = File.mtime 'var/lib/org-config.el'
+ locals_time = File.mtime '.dir-locals.el'
+ org_version = Fronde::Org.current_version
+ FileUtils.mv "lib/org-#{org_version}", 'lib/org-2.3'
+ expect(Dir.exist?("lib/org-#{org_version}")).to be false
+ allow(Fronde::Org).to receive(:download).and_return org_version
+ allow(Fronde::Org).to receive(:compile)
+ sleep 1 # To change mtimes
+ rake.invoke_task('org:upgrade')
+ expect(File.mtime('var/lib/org-config.el')).not_to eq config_time
+ expect(File.mtime('.dir-locals.el')).not_to eq locals_time
+ # Removed by upgrade
+ expect(File.exist?('var/tmp/org.tar.gz')).to be false
+ expect(Dir.exist?('lib/org-2.3')).to be false
+ expect(Fronde::Org).to have_received(:download).once
+ expect(Fronde::Org).to have_received(:compile).once
+ end
+
+ it 'accepts to upgrade even when no previous install exists' do
+ allow(Fronde::Org).to receive(:download).and_return 'test'
+ allow(Fronde::Org).to receive(:compile)
+ rake.invoke_task('org:upgrade')
+ expect(Fronde::Org).to have_received(:download).once
+ expect(Fronde::Org).to have_received(:compile).once
+ end
+
+ it 'skips an upgrade when it fails to fetch org version' do
+ copy_org_tarball_to_fake_tmp
+ copy_org_lisp_files_to_fake_tmp
+ allow(Fronde::Org).to(
+ receive_messages(fetch_version_number: nil, download: 'test')
+ )
+ allow(Fronde::Org).to receive(:compile)
+ expect { rake.invoke_task('org:upgrade') }.not_to raise_error
+ expect(Fronde::Org).not_to have_received(:download)
+ expect(Fronde::Org).not_to have_received(:compile)
end
end
diff --git a/spec/rake_preview_spec.rb b/spec/rake_preview_spec.rb
index 8756646..f1715c2 100644
--- a/spec/rake_preview_spec.rb
+++ b/spec/rake_preview_spec.rb
@@ -4,15 +4,41 @@ require 'rake'
require 'open-uri'
require 'net/http'
-def init_preview
- copy_org_tarball_to_fake_tmp
- Fronde::Config.reset
- rake.invoke_task('org:install')
+def fetch_test_content(path)
+ page_content = URI("http://localhost:5000/#{path}").open.read
+ page_content.gsub(
+ /[FMSTW][a-z]+ \d{1,2} of [ADFJMNOS][a-z]+, \d{4} at \d{2}:\d{2}/,
+ '__PUB_DATE__'
+ )
+end
+
+def init_preview # rubocop:disable Metrics/MethodLength
+ config = <<~CONF
+ ---
+ author: Tata
+ title: This is a website about test
+ org-html:
+ html-postamble: '<footer>Published by Fronde.</footer>'
+ templates:
+ - type: before
+ selector: title
+ content: |
+ <meta property="fronde-test" content="test"/>
+ preview:
+ routes:
+ /test: public_html/index.html
+ CONF
+ File.write 'config.yml', config
+ # It is required to reload config now to have the right default author
+ # name
+ Fronde::CONFIG.reset
+ Fronde::CONFIG.write_org_lisp_config
+ FileUtils.mkdir_p 'src' # Might already exists in first example
FileUtils.cp(
File.expand_path('../tigre.png', __dir__),
'src/tigre.png'
)
- Fronde::OrgFile.new(
+ Fronde::Org::File.new(
'src/index.org',
title: 'My website',
content: <<~CONTENT
@@ -24,33 +50,43 @@ def init_preview
end
context 'when trying preview mode' do
- let(:now_str) { DateTime.now.strftime('%A %-d of %B, %Y at %R') }
+ before(:all) do # rubocop:disable RSpec/BeforeAfterAll
+ @webrick_app = nil # init variable
+ init_testing_environment
+ Fronde::CONFIG.reset # Take into account dir change
+ init_fake_org_install
+ copy_org_tarball_to_fake_tmp
+ copy_org_lisp_files_to_fake_tmp
+ rake.invoke_task('org:install')
+ end
after do
+ # rubocop:disable RSpec/InstanceVariable
+ @webrick_app.exit # Be sure to kill test server
+ @webrick_app.join # Be patient before quitting example
+ # rubocop:enable RSpec/InstanceVariable
+ clean_testing_environment
+ end
+
+ after(:all) do # rubocop:disable RSpec/BeforeAfterAll
tear_down 'tmp/website_testing'
end
context 'without a domain name' do
- around do |test|
- init_testing_website
+ before do
init_preview
-
rake.invoke_task('site:build')
- webrick_app = Thread.new do
+ @webrick_app = Thread.new do
rake.invoke_task('site:preview')
end
- sleep 1 # Necessary to let webrick start
- test.run # Actually run test case
- webrick_app.exit # Be sure to kill test server
- webrick_app.join # Be patient before quitting example
+ sleep 1 # Necessary to let webrick start
end
- it 'is viewable with preview', rake: true do
- home_page = URI('http://localhost:5000/index.html').open.read
+ it 'is viewable with preview' do
+ home_page = fetch_test_content 'index.html'
proof = File.expand_path('data/index_proof.html', __dir__)
- proof_content = File.read(proof).gsub(/__PUB_DATE__/, now_str)
- expect(home_page).to eq(proof_content)
+ expect(home_page).to eq(File.read(proof))
tigre = URI('http://localhost:5000/tigre.png')
res = Net::HTTP.get_response(tigre)
expect(res).to be_a(Net::HTTPOK)
@@ -58,21 +94,19 @@ context 'when trying preview mode' do
expect(res['content-length']).to eq(File.size('src/tigre.png').to_s)
end
- it 'serves index', rake: true do
- home_page = URI('http://localhost:5000/').open.read
+ it 'serves index' do
+ home_page = fetch_test_content ''
proof = File.expand_path('data/index_proof.html', __dir__)
- proof_content = File.read(proof).gsub(/__PUB_DATE__/, now_str)
- expect(home_page).to eq(proof_content)
+ expect(home_page).to eq(File.read(proof))
end
- it 'is viewable with routes testing', rake: true do
- home_page = URI('http://localhost:5000/test').open.read
+ it 'is viewable with routes testing' do
+ test_page = fetch_test_content 'test'
proof = File.expand_path('data/index_proof.html', __dir__)
- proof_content = File.read(proof).gsub(/__PUB_DATE__/, now_str)
- expect(home_page).to eq(proof_content)
+ expect(test_page).to eq(File.read(proof))
end
- it 'sends an error if a page is not found', rake: true do
+ it 'sends an error if a page is not found' do
expect { URI('http://localhost:5000/not_found.html').open.read }.to(
raise_error(/404 Not Found/)
)
@@ -80,27 +114,24 @@ context 'when trying preview mode' do
end
context 'with a domain name' do
- around do |test|
- init_testing_website
+ before do
init_preview
- old_conf = Fronde::Config.settings.dup
+ Fronde::CONFIG.reset
+ old_conf = Fronde::CONFIG.settings.merge
old_conf['domain'] = 'http://mydomain.local'
- Fronde::Config.load_test(old_conf)
+ Fronde::CONFIG.load_test(old_conf)
+ Fronde::CONFIG.write_org_lisp_config
rake.invoke_task('site:build')
- webrick_app = Thread.new do
+ @webrick_app = Thread.new do
rake.invoke_task('site:preview')
end
sleep 1
- test.run # Actually run test
- webrick_app.exit
- webrick_app.join
end
- it 'replaces domain occurence by localhost URIs', rake: true do
- home_page = URI('http://localhost:5000/index.html').open.read
+ it 'replaces domain occurence by localhost URIs' do
+ home_page = fetch_test_content 'index.html'
proof = File.expand_path('data/index_proof.html', __dir__)
- proof_content = File.read(proof).gsub(/__PUB_DATE__/, now_str)
- proof_content.gsub!(/mydomain\.local/, 'localhost:5000')
+ proof_content = File.read(proof).gsub('mydomain.local', 'localhost:5000')
expect(home_page).to eq(proof_content)
end
end
diff --git a/spec/rake_site_spec.rb b/spec/rake_site_spec.rb
index 2b15251..6c10c76 100644
--- a/spec/rake_site_spec.rb
+++ b/spec/rake_site_spec.rb
@@ -2,7 +2,25 @@
require 'rake'
-def write_base_files
+def write_base_files # rubocop:disable Metrics/MethodLength
+ config = <<~CONF
+ ---
+ sources:
+ - name: org
+ path: src
+ target: .
+ recursive: false
+ exclude: tata\\.org
+ - path: src/news
+ is_blog: true
+ - path: 'titi/test'
+ recursive: nil
+ exclude: ugly\\.org
+ - 'titi/tutu/tata'
+ CONF
+ File.write('config.yml', config)
+ Fronde::CONFIG.reset
+ Fronde::CONFIG.write_org_lisp_config
FileUtils.mkdir_p 'src/news'
org_content = <<~ORG
#+title: Index file
@@ -27,184 +45,287 @@ def write_base_files
end
context 'with a testing website' do
- before do
- Fronde::Config.reset
- init_testing_website
+ before(:all) do # rubocop:disable RSpec/BeforeAfterAll
+ init_testing_environment
+ Fronde::CONFIG.reset # Take into account dir change
+ init_fake_org_install
copy_org_tarball_to_fake_tmp
+ copy_org_lisp_files_to_fake_tmp
rake.invoke_task('org:install')
end
- after do
+ after { clean_testing_environment }
+
+ after(:all) do # rubocop:disable RSpec/BeforeAfterAll
tear_down 'tmp/website_testing'
end
context 'when building org files' do
before do
- o = Fronde::OrgFile.new('src/index.org',
- title: 'My website',
- content: 'Nice content.')
+ Fronde::CONFIG.reset
+ Fronde::CONFIG.write_org_lisp_config
+ o = Fronde::Org::File.new(
+ 'src/index.org',
+ title: 'My website',
+ content: "#+description: Nice description\n\nNice content."
+ )
o.write
end
- it 'builds something', rake: true do
+ it 'builds something' do
rake(verbose: false).invoke_task('site:build')
expect(File.exist?('public_html/index.html')).to be(true)
end
- it 'builds something even in verbose mode', rake: true do
+ it 'builds something even in verbose mode' do
+ # For coverage
rake(verbose: true).invoke_task('site:build')
expect(File.exist?('public_html/index.html')).to be(true)
end
- it 'does not build again with successive call', rake: true do
+ it 'build something for gemini projects' do
+ old_conf = Fronde::CONFIG.settings.merge
+ old_conf['sources'] = [
+ { 'name' => 'org', 'path' => 'src', 'target' => '.',
+ 'type' => 'gemini' }
+ ]
+ Fronde::CONFIG.load_test(old_conf)
+ Fronde::CONFIG.write_org_lisp_config
+ rake.invoke_task('site:build')
+ expect(File.exist?('public_gmi/index.gmi')).to be(true)
+ end
+
+ it 'extracts html body' do
+ # No source file
+ o = Fronde::Org::File.new(
+ 'test/test.org', title: 'No source', content: 'Test'
+ )
+ data = o.to_h
+ expect(data['published_body']).to eq('')
+
+ # Before build
+ o = Fronde::Org::File.new('src/index.org')
+ data = o.to_h
+ expect(data['published_body']).to eq('Nice description')
+
+ rake(verbose: false).invoke_task('site:build')
+ o = Fronde::Org::File.new('public_html/index.html')
+ data = o.to_h
+ content = <<~CONTENT.strip
+ <div id="content" class="content">
+ <p>
+ Nice content.
+ </p>
+ </div>
+ CONTENT
+ expect(data['published_body']).to eq(content)
+ end
+
+ it 'extracts gemini body' do
+ old_conf = Fronde::CONFIG.settings.merge
+ old_conf['sources'] = [
+ { 'name' => 'org', 'path' => 'src', 'target' => '.',
+ 'type' => 'gemini' }
+ ]
+ Fronde::CONFIG.load_test(old_conf)
+ Fronde::CONFIG.write_org_lisp_config
+
+ # Before build
+ o = Fronde::Org::File.new('src/index.org')
+ data = o.to_h
+ expect(data['published_body']).to eq('Nice description')
+
+ rake.invoke_task('site:build')
+ o = Fronde::Org::File.new('public_gmi/index.gmi')
+ data = o.to_h
+ content = <<~CONTENT
+ # My website
+
+ Nice content.
+
+
+ --
+ 📅 Last modification on __PUB_DATE__
+ 📝 Written by alice with __VERSION__
+
+ CONTENT
+ page_content = data['published_body'].gsub(
+ /[FMSTW][a-z]+ \d{1,2} of [ADFJMNOS][a-z]+, \d{4} at \d{2}:\d{2}/,
+ '__PUB_DATE__'
+ ).gsub(
+ /GNU\/Emacs [0-9.]+ \(Org mode [0-9.]+\), and published with Fronde [0-9.]+/,
+ '__VERSION__'
+ )
+ expect(page_content).to eq(content)
+ end
+
+ it 'does not build again with successive call' do
rake(verbose: false).invoke_task('site:build')
old_content = File.read('public_html/index.html')
- old_conf = Fronde::Config.settings.dup
- old_conf['org-html']['html-postamble'] = '<footer>Modified!</footer>'
- Fronde::Config.load_test(old_conf)
+ custom_footer = {
+ 'org-html' => { 'html-postamble' => '<footer>Modified!</footer>' }
+ }
+ new_conf = Fronde::CONFIG.settings.merge(custom_footer)
+ Fronde::CONFIG.load_test(new_conf)
+ Fronde::CONFIG.write_org_lisp_config
rake(verbose: false).invoke_task('site:build')
expect(File.read('public_html/index.html')).to eql(old_content)
end
- it 'builds again when call with force option', rake: true do
+ it 'builds again when call with force option' do
rake(verbose: false).invoke_task('site:build')
old_content = File.read('public_html/index.html')
- old_conf = Fronde::Config.settings.dup
- old_conf['org-html']['html-postamble'] = '<footer>Modified!</footer>'
- Fronde::Config.load_test(old_conf)
+ custom_footer = {
+ 'org-html' => { 'html-postamble' => '<footer>Modified!</footer>' }
+ }
+ new_conf = Fronde::CONFIG.settings.merge(custom_footer)
+ Fronde::CONFIG.load_test(new_conf)
+ Fronde::CONFIG.write_org_lisp_config
rake(verbose: false).invoke_task('site:build[true]')
expect(File.read('public_html/index.html')).not_to eql(old_content)
end
- it 'fails gracefully when something goes wrong', rake: true do
- old_conf = Fronde::Config.settings.dup
+ it 'fails gracefully when something goes wrong' do
+ old_conf = Fronde::CONFIG.settings.merge
old_conf['emacs'] = 'notemacsatall'
- Fronde::Config.load_test(old_conf)
+ Fronde::CONFIG.load_test(old_conf)
+ Fronde::CONFIG.write_org_lisp_config
expect { rake(verbose: false).invoke_task('site:build') }.to(
output(/Aborting/).to_stderr
)
end
+
+ it 'warns about orphaned files' do
+ FileUtils.mkdir_p 'public_html'
+ File.write('public_html/orphan.html', 'Orphan content')
+ expect { rake.invoke_task('site:build') }.to(
+ output(
+ /No project found for .+\/orphan.html. Publication will fail./
+ ).to_stderr
+ )
+ end
+
+ it 'cleans orphaned files' do
+ allow($stdin).to receive(:gets).and_return("y\n")
+ FileUtils.mkdir_p 'public_html'
+ File.write('public_html/orphan.html', 'Orphan content')
+ rake.invoke_task('site:build')
+ expect(File.exist?('public_html/orphan.html')).to be true
+ rake.invoke_task('site:clean')
+ expect(File.exist?('public_html/orphan.html')).to be false
+ end
+
+ it 'skips cleaning if answer is not y' do
+ allow($stdin).to receive(:gets).and_return("nope\n")
+ FileUtils.mkdir_p 'public_html'
+ File.write('public_html/orphan.html', 'Orphan content')
+ rake.invoke_task('site:build')
+ expect(File.exist?('public_html/orphan.html')).to be true
+ rake.invoke_task('site:clean')
+ expect(File.exist?('public_html/orphan.html')).to be true
+ end
end
context 'when generating indexes' do
before { write_base_files }
- context 'without blog setting', rake: true do
+ context 'without blog setting' do
before do
- old_conf = Fronde::Config.settings.dup
+ old_conf = Fronde::CONFIG.settings.merge
old_conf['sources'][1]['is_blog'] = false
- Fronde::Config.load_test(old_conf)
- end
-
- it 'does not generate index', rake: true do
- rake.invoke_task('site:index')
- expect(File.exist?('src/news/index.org')).to be(false)
- expect(File.exist?('tags/index.org')).to be(false)
- expect(File.exist?('tags/toto.org')).to be(false)
- expect(File.exist?('tags/titi.org')).to be(false)
- expect(File.exist?('public_html/feeds/index.xml')).to be(false)
- expect(File.exist?('public_html/feeds/toto.xml')).to be(false)
- expect(File.exist?('public_html/feeds/titi.xml')).to be(false)
+ Fronde::CONFIG.load_test(old_conf)
+ Fronde::CONFIG.write_org_lisp_config
end
- it 'does not generate index when calling build', rake: true do
+ it 'does not generate index' do
rake.invoke_task('site:build')
expect(File.exist?('src/news/index.org')).to be(false)
- expect(File.exist?('tags/index.org')).to be(false)
- expect(File.exist?('tags/toto.org')).to be(false)
- expect(File.exist?('tags/titi.org')).to be(false)
+ expect(File.exist?('src/news/tags/index.org')).to be(false)
+ expect(File.exist?('src/news/tags/toto.org')).to be(false)
+ expect(File.exist?('src/news/tags/titi.org')).to be(false)
expect(File.exist?('public_html/news/index.html')).to be(false)
- expect(File.exist?('public_html/tags/index.html')).to be(false)
- expect(File.exist?('public_html/tags/index.html')).to be(false)
- expect(File.exist?('public_html/tags/toto.html')).to be(false)
- expect(File.exist?('public_html/tags/titi.html')).to be(false)
- expect(File.exist?('public_html/feeds/index.xml')).to be(false)
- expect(File.exist?('public_html/feeds/toto.xml')).to be(false)
- expect(File.exist?('public_html/feeds/titi.xml')).to be(false)
+ expect(File.exist?('public_html/news/tags/index.html')).to be(false)
+ expect(File.exist?('public_html/news/tags/index.html')).to be(false)
+ expect(File.exist?('public_html/news/tags/toto.html')).to be(false)
+ expect(File.exist?('public_html/news/tags/titi.html')).to be(false)
+ expect(File.exist?('public_html/news/feeds/index.xml')).to be(false)
+ expect(File.exist?('public_html/news/feeds/toto.xml')).to be(false)
+ expect(File.exist?('public_html/news/feeds/titi.xml')).to be(false)
end
- it 'does not list tags', rake: true do
+ it 'does not list tags' do
expect { rake.invoke_task('tags:name') }.to output('').to_stdout
expect { rake.invoke_task('tags:weight') }.to output('').to_stdout
end
end
- context 'with wrong blog settings', rake: true do
+ context 'with wrong path' do
before do
- old_conf = Fronde::Config.settings.dup
+ old_conf = Fronde::CONFIG.settings.merge
old_conf['sources'][1]['path'] = 'src/test'
- Fronde::Config.load_test(old_conf)
+ Fronde::CONFIG.load_test(old_conf)
+ Fronde::CONFIG.write_org_lisp_config
end
- it 'does not generate index', rake: true do
- rake.invoke_task('site:index')
- expect(File.exist?('src/test/index.org')).to be(false)
- expect(File.exist?('public_html/feeds/index.xml')).to be(false)
- end
-
- it 'does not generate index when calling build', rake: true do
+ it 'still generates some index' do
rake.invoke_task('site:build')
- expect(File.exist?('src/test/index.org')).to be(false)
- expect(File.exist?('tags/index.org')).to be(false)
- expect(File.exist?('tags/toto.org')).to be(false)
- expect(File.exist?('tags/titi.org')).to be(false)
- expect(File.exist?('public_html/test/index.html')).to be(false)
- expect(File.exist?('public_html/tags/index.html')).to be(false)
- expect(File.exist?('public_html/feeds/index.xml')).to be(false)
+ expect(File.exist?('src/test/index.org')).to be(true)
+ expect(File.exist?('src/test/tags/index.org')).to be(true)
+ # The 2 following does not exist are there was nothing to parse
+ expect(File.exist?('src/test/tags/toto.org')).to be(false)
+ expect(File.exist?('src/test/tags/titi.org')).to be(false)
+ expect(File.exist?('public_html/test/index.html')).to be(true)
+ expect(File.exist?('public_html/test/tags/index.html')).to be(true)
+ expect(File.exist?('public_html/test/feeds/index.xml')).to be(true)
end
- it 'does not list tags', rake: true do
+ it 'does not list tags' do
expect { rake.invoke_task('tags:name') }.to output('').to_stdout
expect { rake.invoke_task('tags:weight') }.to output('').to_stdout
end
end
context 'with a correct blog path' do
- it 'generates indexes', rake: true do
- rake.invoke_task('site:index')
+ it 'generates indexes' do
+ rake(verbose: false).invoke_task('site:build')
expect(File.exist?('src/news/index.org')).to be(true)
- expect(File.exist?('tags/index.org')).to be(true)
- expect(File.exist?('tags/toto.org')).to be(true)
- expect(File.exist?('tags/titi.org')).to be(true)
- expect(File.exist?('public_html/feeds/index.xml')).to be(true)
- expect(File.exist?('public_html/feeds/toto.xml')).to be(true)
- expect(File.exist?('public_html/feeds/titi.xml')).to be(true)
- end
-
- it 'generates indexes, even verbosely', rake: true do
- rake(verbose: true).invoke_task('site:index')
- expect(File.exist?('src/news/index.org')).to be(true)
- expect(File.exist?('tags/index.org')).to be(true)
- expect(File.exist?('tags/toto.org')).to be(true)
- expect(File.exist?('tags/titi.org')).to be(true)
- expect(File.exist?('public_html/feeds/index.xml')).to be(true)
- expect(File.exist?('public_html/feeds/toto.xml')).to be(true)
- expect(File.exist?('public_html/feeds/titi.xml')).to be(true)
+ expect(File.exist?('src/news/tags/index.org')).to be(true)
+ expect(File.exist?('src/news/tags/toto.org')).to be(true)
+ expect(File.exist?('src/news/tags/titi.org')).to be(true)
+ expect(File.exist?('public_html/news/index.html')).to be(true)
+ expect(File.exist?('public_html/news/tags/index.html')).to be(true)
+ expect(File.exist?('public_html/news/tags/index.html')).to be(true)
+ expect(File.exist?('public_html/news/tags/toto.html')).to be(true)
+ expect(File.exist?('public_html/news/tags/titi.html')).to be(true)
+ expect(File.exist?('public_html/news/feeds/index.xml')).to be(true)
+ expect(File.exist?('public_html/news/feeds/toto.xml')).to be(true)
+ expect(File.exist?('public_html/news/feeds/titi.xml')).to be(true)
end
- it 'generates indexes, even with build', rake: true do
- rake.invoke_task('site:build')
+ it 'generates indexes in verbose mode' do
+ # For coverage
+ rake(verbose: true).invoke_task('site:build')
expect(File.exist?('src/news/index.org')).to be(true)
- expect(File.exist?('tags/index.org')).to be(true)
- expect(File.exist?('tags/toto.org')).to be(true)
- expect(File.exist?('tags/titi.org')).to be(true)
+ expect(File.exist?('src/news/tags/index.org')).to be(true)
+ expect(File.exist?('src/news/tags/toto.org')).to be(true)
+ expect(File.exist?('src/news/tags/titi.org')).to be(true)
expect(File.exist?('public_html/news/index.html')).to be(true)
- expect(File.exist?('public_html/tags/index.html')).to be(true)
- expect(File.exist?('public_html/tags/index.html')).to be(true)
- expect(File.exist?('public_html/tags/toto.html')).to be(true)
- expect(File.exist?('public_html/tags/titi.html')).to be(true)
- expect(File.exist?('public_html/feeds/index.xml')).to be(true)
- expect(File.exist?('public_html/feeds/toto.xml')).to be(true)
- expect(File.exist?('public_html/feeds/titi.xml')).to be(true)
+ expect(File.exist?('public_html/news/tags/index.html')).to be(true)
+ expect(File.exist?('public_html/news/tags/index.html')).to be(true)
+ expect(File.exist?('public_html/news/tags/toto.html')).to be(true)
+ expect(File.exist?('public_html/news/tags/titi.html')).to be(true)
+ expect(File.exist?('public_html/news/feeds/index.xml')).to be(true)
+ expect(File.exist?('public_html/news/feeds/toto.xml')).to be(true)
+ expect(File.exist?('public_html/news/feeds/titi.xml')).to be(true)
end
- it 'lists all tags by name', rake: true do
+ it 'lists all tags by name' do
expect { rake.invoke_task('tags:name') }.to(
output("toto (2)\ntiti (1)\n").to_stdout
)
end
- it 'lists all tags by weight', rake: true do
+ it 'lists all tags by weight' do
expect { rake.invoke_task('tags:weight') }.to(
output("titi (1)\ntoto (2)\n").to_stdout
)
diff --git a/spec/requirements.rb b/spec/requirements.rb
index 46ef06a..1f081e0 100644
--- a/spec/requirements.rb
+++ b/spec/requirements.rb
@@ -3,10 +3,7 @@
require 'fileutils'
require 'simplecov'
-require 'r18n-core'
-R18n.default_places = File.expand_path('../locales', __dir__)
-R18n.set 'en'
-R18n::Filters.on(:named_variables)
+require 'liquid'
SimpleCov.start do
# Activate new covering precision
@@ -15,20 +12,19 @@ SimpleCov.start do
# Remove dev only tools from coverage check
add_filter ['lib/tasks/sync.rake']
# Sort coverage results into usefull groups
- add_group 'Core libs', 'lib/fronde'
+ add_group 'Core libs', ['lib/fronde', 'lib/ext']
add_group 'Rake tasks', 'lib/tasks'
add_group 'Specs', 'spec'
end
-# The following requires other components automatically
-require 'fronde/org_file'
-require 'fronde/utils'
-
ENV['LANG'] = 'en'
ENV['USER'] = 'alice'
-# rubocop:disable Metrics/MethodLength
-def init_testing_website
+require_relative '../lib/fronde/config'
+require_relative '../lib/fronde/version'
+require_relative '../lib/fronde/org'
+
+def init_testing_environment
FileUtils.mkdir_p 'tmp/website_testing'
Dir.chdir 'tmp/website_testing'
# Full path is now spec/tmp/website_testing
@@ -36,11 +32,6 @@ def init_testing_website
rakefile = <<~RAKE
# frozen_string_literal: true
- require 'r18n-core'
- R18n.default_places = '../../../locales'
- R18n.set 'en'
- R18n::Filters.on(:named_variables)
-
$LOAD_PATH.unshift('../../../lib')
Dir.glob('../../../lib/tasks/*.rake').each { |r| import r }
@@ -48,43 +39,60 @@ def init_testing_website
task default: 'site:build'
RAKE
File.write('Rakefile', rakefile)
- config = <<~CONF
- ---
- author: Tata
- title: This is a website about test
- org-html:
- html-postamble: '<footer>Published by Fronde.</footer>'
- sources:
- - name: org
- path: src
- target: .
- recursive: false
- exclude: tata\\.org
- - path: src/news
- is_blog: true
- - path: 'titi/test'
- recursive: nil
- exclude: ugly\\.org
- - 'titi/tutu/tata'
- templates:
- - type: before
- selector: title
- content: |
- <meta property="fronde-test" content="test"/>
- preview:
- routes:
- /test: public_html/index.html
- CONF
- File.write('config.yml', config)
end
-# rubocop:enable Metrics/MethodLength
+
+def clean_testing_environment
+ safe_files = %w[lib var Rakefile].freeze
+ Dir['*'].each do |file|
+ next if safe_files.include? file
+
+ if File.directory? file
+ FileUtils.rm_r file
+ next
+ end
+ File.unlink file
+ end
+ FileUtils.rm_r 'var/tmp/timestamps', force: true # might be absent
+end
def copy_org_tarball_to_fake_tmp
- tarball = File.expand_path('tmp/org.tar.gz', __dir__)
+ tarball = File.expand_path 'tmp/org.tar.gz', __dir__
FileUtils.mkdir_p 'var/tmp'
FileUtils.cp tarball, 'var/tmp'
end
+def copy_org_lisp_files_to_fake_tmp
+ org_version_cookie = File.expand_path 'tmp/last_org_version', __dir__
+ org_version = File.read org_version_cookie
+ installed_files = File.expand_path "tmp/org-#{org_version}", __dir__
+ FileUtils.mkdir_p 'lib'
+ FileUtils.cp_r installed_files, 'lib/'
+end
+
+def init_fake_org_install
+ tmp_dir = File.expand_path 'tmp', __dir__
+ # It should be sufficient to have the tarball downloaded to assume it
+ # has also already compiled Org.
+ return if File.exist? "#{tmp_dir}/org.tar.gz"
+
+ last_version = Fronde::Org.download tmp_dir
+ Fronde::Org.compile(
+ "#{tmp_dir}/org.tar.gz", last_version,
+ "#{tmp_dir}/org-#{last_version}"
+ )
+end
+
+def proof_content(filename)
+ proof = File.expand_path "data/#{filename}", __dir__
+ template = Liquid::Template.parse(File.read(proof))
+ template.render(
+ 'test_dir' => Dir.pwd,
+ 'base_dir' => File.expand_path('../', __dir__),
+ 'version' => Fronde::VERSION,
+ 'org_version' => Fronde::Org.current_version
+ )
+end
+
def rake(verbose: false)
rake = Rake.application
rake.raw_load_rakefile
@@ -103,11 +111,10 @@ RSpec.configure do |config|
config.before(:suite) do
Dir.chdir __dir__
FileUtils.mkdir 'tmp'
- Fronde::Utils.download_org('tmp')
end
config.after(:suite) do
Dir.chdir __dir__
- FileUtils.rm_r ['tmp', 'var'], force: true
+ FileUtils.rm_r %w[tmp var], force: true
end
end