diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0293be9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,56 @@ +name: CI + +on: + pull_request: + push: + paths-ignore: + - LICENSE + - README.org + +jobs: + build: + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + emacs-version: + - 26.3 + - 27.2 + - 28.2 + - 29.4 + - 30.1 + experimental: [false] + include: + - os: ubuntu-latest + emacs-version: snapshot + experimental: true + - os: macos-latest + emacs-version: snapshot + experimental: true + - os: windows-latest + emacs-version: snapshot + experimental: true + exclude: + - os: macos-latest + emacs-version: 26.3 + - os: macos-latest + emacs-version: 27.2 + + steps: + - uses: jcs090218/setup-emacs@master + with: + version: ${{ matrix.emacs-version }} + + - uses: emacs-eask/setup-eask@master + with: + version: 'snapshot' + + - uses: actions/checkout@v4 + + - name: Run tests + run: | + eask package + eask install + eask compile diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47fd8d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.elc +/*-autoloads.el + +/.eask +/dist diff --git a/Eask b/Eask new file mode 100644 index 0000000..64855be --- /dev/null +++ b/Eask @@ -0,0 +1,21 @@ +;; -*- mode: eask; lexical-binding: t -*- + +(package "org-static-blog" + "1.7.0" + "a simple org-mode based static blog generator") + +(website-url "https://github.com/bastibe/org-static-blog") +(keywords "outlines") + +(package-file "org-static-blog.el") + +(script "test" "echo \"Error: no test specified\" && exit 1") + +(source 'gnu) + +(depends-on "emacs" "26.1") + +(add-hook 'eask-before-compile-hook + (lambda () + (setq byte-compile-error-on-warn t) + (setq byte-compile-docstring-max-column 1000))) diff --git a/org-static-blog.el b/org-static-blog.el index f974e36..3aaa3e7 100644 --- a/org-static-blog.el +++ b/org-static-blog.el @@ -1,4 +1,4 @@ -;;; org-static-blog.el --- a simple org-mode based static blog generator +;;; org-static-blog.el --- a simple org-mode based static blog generator -*- lexical-binding: t; -*- ;; Author: Bastian Bechtold ;; Contrib: Shmavon Gazanchyan, Rafał -rsm- Marek, neeasade, @@ -7,7 +7,8 @@ ;; K. Scarlet, zsxh ;; URL: https://github.com/bastibe/org-static-blog ;; Version: 1.7.0 -;; Package-Requires: ((emacs "24.3")) +;; Package-Requires: ((emacs "26.1")) +;; Keywords: outlines ;;; Commentary: @@ -39,6 +40,7 @@ ;;; Code: +(require 'autoinsert) (require 'cl-extra) (require 'org) (require 'ox-html) @@ -116,13 +118,13 @@ The tags page lists all posts as headlines." (defcustom org-static-blog-rss-excluded-tag nil "Posts with this tag won't be included in the RSS feeds." :type '(choice (const :tag "None" nil) - (string :tag "Tag name")) + (string :tag "Tag name")) :safe t) (defcustom org-static-blog-no-comments-tag nil "Posts with this tag won't include comments." :type '(choice (const :tag "None" nil) - (string :tag "Tag name")) + (string :tag "Tag name")) :safe t) (defcustom org-static-blog-rss-extra "" @@ -268,95 +270,95 @@ Only if og tags are enabled. It can be overridden with the ;; localization support (defconst org-static-blog-texts '((other-posts - ("en" . "Other posts") - ("pl" . "Pozostałe wpisy") - ("ru" . "Другие публикации") - ("by" . "Іншыя публікацыі") - ("it" . "Altri articoli") - ("es" . "Otros artículos") - ("fr" . "Autres articles") - ("zh" . "其他帖子") - ("ja" . "他の投稿")) - (date-format - ("en" . "%d %b %Y") - ("pl" . "%Y-%m-%d") - ("ru" . "%d.%m.%Y") - ("by" . "%d.%m.%Y") - ("it" . "%d/%m/%Y") - ("es" . "%d/%m/%Y") - ("fr" . "%d-%m-%Y") - ("zh" . "%Y-%m-%d") - ("ja" . "%Y/%m/%d")) - (tags - ("en" . "Tags") - ("pl" . "Tagi") - ("ru" . "Ярлыки") - ("by" . "Ярлыкі") - ("it" . "Categorie") - ("es" . "Categoría") - ("fr" . "Tags") - ("zh" . "标签") - ("ja" . "タグ")) - (archive - ("en" . "Archive") - ("pl" . "Archiwum") - ("ru" . "Архив") - ("by" . "Архіў") - ("it" . "Archivio") - ("es" . "Archivo") - ("fr" . "Archive") - ("zh" . "归档") - ("ja" . "アーカイブ")) - (posts-tagged - ("en" . "Posts tagged") - ("pl" . "Wpisy z tagiem") - ("ru" . "Публикации с ярлыками") - ("by" . "Публікацыі") - ("it" . "Articoli nella categoria") - ("es" . "Artículos de la categoría") - ("fr" . "Articles tagués") - ("zh" . "打标签的帖子") - ("ja" . "タグ付けされた投稿")) - (no-prev-post - ("en" . "There is no previous post") - ("pl" . "Poprzedni wpis nie istnieje") - ("ru" . "Нет предыдущей публикации") - ("by" . "Няма папярэдняй публікацыі") - ("it" . "Non c'è nessun articolo precedente") - ("es" . "No existe un artículo precedente") - ("fr" . "Il n'y a pas d'article précédent") - ("zh" . "无更旧的帖子") - ("ja" . "前の投稿はありません")) - (no-next-post - ("en" . "There is no next post") - ("pl" . "Następny wpis nie istnieje") - ("ru" . "Нет следующей публикации") - ("by" . "Няма наступнай публікацыі") - ("it" . "Non c'è nessun articolo successivo") - ("es" . "No hay artículo siguiente") - ("fr" . "Il n'y a pas d'article suivants") - ("zh" . "无更新的帖子") - ("ja" . "次の投稿はありません")) - (title - ("en" . "Title: ") - ("pl" . "Tytuł: ") - ("ru" . "Заголовок: ") - ("by" . "Загаловак: ") - ("it" . "Titolo: ") - ("es" . "Título: ") - ("fr" . "Titre : ") - ("zh" . "标题:") - ("ja" . "タイトル: ")) - (filename - ("en" . "Filename: ") - ("pl" . "Nazwa pliku: ") - ("ru" . "Имя файла: ") - ("by" . "Імя файла: ") - ("it" . "Nome del file: ") - ("es" . "Nombre del archivo: ") - ("fr" . "Nom du fichier :") - ("zh" . "文件名:") - ("ja" . "ファイル名: ")))) + ("en" . "Other posts") + ("pl" . "Pozostałe wpisy") + ("ru" . "Другие публикации") + ("by" . "Іншыя публікацыі") + ("it" . "Altri articoli") + ("es" . "Otros artículos") + ("fr" . "Autres articles") + ("zh" . "其他帖子") + ("ja" . "他の投稿")) + (date-format + ("en" . "%d %b %Y") + ("pl" . "%Y-%m-%d") + ("ru" . "%d.%m.%Y") + ("by" . "%d.%m.%Y") + ("it" . "%d/%m/%Y") + ("es" . "%d/%m/%Y") + ("fr" . "%d-%m-%Y") + ("zh" . "%Y-%m-%d") + ("ja" . "%Y/%m/%d")) + (tags + ("en" . "Tags") + ("pl" . "Tagi") + ("ru" . "Ярлыки") + ("by" . "Ярлыкі") + ("it" . "Categorie") + ("es" . "Categoría") + ("fr" . "Tags") + ("zh" . "标签") + ("ja" . "タグ")) + (archive + ("en" . "Archive") + ("pl" . "Archiwum") + ("ru" . "Архив") + ("by" . "Архіў") + ("it" . "Archivio") + ("es" . "Archivo") + ("fr" . "Archive") + ("zh" . "归档") + ("ja" . "アーカイブ")) + (posts-tagged + ("en" . "Posts tagged") + ("pl" . "Wpisy z tagiem") + ("ru" . "Публикации с ярлыками") + ("by" . "Публікацыі") + ("it" . "Articoli nella categoria") + ("es" . "Artículos de la categoría") + ("fr" . "Articles tagués") + ("zh" . "打标签的帖子") + ("ja" . "タグ付けされた投稿")) + (no-prev-post + ("en" . "There is no previous post") + ("pl" . "Poprzedni wpis nie istnieje") + ("ru" . "Нет предыдущей публикации") + ("by" . "Няма папярэдняй публікацыі") + ("it" . "Non c'è nessun articolo precedente") + ("es" . "No existe un artículo precedente") + ("fr" . "Il n'y a pas d'article précédent") + ("zh" . "无更旧的帖子") + ("ja" . "前の投稿はありません")) + (no-next-post + ("en" . "There is no next post") + ("pl" . "Następny wpis nie istnieje") + ("ru" . "Нет следующей публикации") + ("by" . "Няма наступнай публікацыі") + ("it" . "Non c'è nessun articolo successivo") + ("es" . "No hay artículo siguiente") + ("fr" . "Il n'y a pas d'article suivants") + ("zh" . "无更新的帖子") + ("ja" . "次の投稿はありません")) + (title + ("en" . "Title: ") + ("pl" . "Tytuł: ") + ("ru" . "Заголовок: ") + ("by" . "Загаловак: ") + ("it" . "Titolo: ") + ("es" . "Título: ") + ("fr" . "Titre : ") + ("zh" . "标题:") + ("ja" . "タイトル: ")) + (filename + ("en" . "Filename: ") + ("pl" . "Nazwa pliku: ") + ("ru" . "Имя файла: ") + ("by" . "Імя файла: ") + ("it" . "Nome del file: ") + ("es" . "Nombre del archivo: ") + ("fr" . "Nom du fichier :") + ("zh" . "文件名:") + ("ja" . "ファイル名: ")))) (defun concat-to-dir (dir filename) "Concat filename to another path interpreted as a directory." @@ -370,7 +372,7 @@ Only if og tags are enabled. It can be overridden with the "
\n" "\n" (when tDescription - (format "\n" tDescription)) + (format "\n" tDescription)) "" tTitle "\n" (when org-static-blog-enable-og-tags - (concat - "\n" - "\n" - (when tDescription - (format "\n" tDescription)) - (when tUrl - (format "\n" tUrl)) - (if tImage - (format "\n" - (org-static-blog-get-absolute-url tImage)) - (when (> (length org-static-blog-image) 0) - (format "\n" - (org-static-blog-get-absolute-url org-static-blog-image)))))) + (concat + "\n" + "\n" + (when tDescription + (format "\n" tDescription)) + (when tUrl + (format "\n" tUrl)) + (if tImage + (format "\n" + (org-static-blog-get-absolute-url tImage)) + (when (> (length org-static-blog-image) 0) + (format "\n" + (org-static-blog-get-absolute-url org-static-blog-image)))))) org-static-blog-page-header "\n" "\n" @@ -410,12 +412,12 @@ Only if og tags are enabled. It can be overridden with the "Return localized text. Depends on org-static-blog-langcode and org-static-blog-texts." (let* ((text-node (assoc text-id org-static-blog-texts)) - (text-lang-node (if text-node - (assoc org-static-blog-langcode text-node) - nil))) - (if text-lang-node - (cdr text-lang-node) - (concat "[" (symbol-name text-id) ":" org-static-blog-langcode "]")))) + (text-lang-node (if text-node + (assoc org-static-blog-langcode text-node) + nil))) + (if text-lang-node + (cdr text-lang-node) + (concat "[" (symbol-name text-id) ":" org-static-blog-langcode "]")))) ;;;###autoload @@ -428,28 +430,28 @@ With a prefix argument, all blog posts are re-rendered unconditionally." (interactive "P") (dolist (file (append (org-static-blog-get-post-filenames) - (org-static-blog-get-draft-filenames))) - (when (or force-render (org-static-blog-needs-publishing-p file)) - (org-static-blog-publish-file file))) + (org-static-blog-get-draft-filenames))) + (when (or force-render (org-static-blog-needs-publishing-p file)) + (org-static-blog-publish-file file))) ;; don't spam too many deprecation warnings: (let ((org-static-blog-enable-deprecation-warning nil)) - (org-static-blog-assemble-index) - (org-static-blog-assemble-rss) - (org-static-blog-assemble-archive) - (if org-static-blog-enable-tags - (org-static-blog-assemble-tags)))) + (org-static-blog-assemble-index) + (org-static-blog-assemble-rss) + (org-static-blog-assemble-archive) + (if org-static-blog-enable-tags + (org-static-blog-assemble-tags)))) (defun org-static-blog-needs-publishing-p (post-filename) "Check whether POST-FILENAME was changed since last render." (let ((pub-filename - (org-static-blog-matching-publish-filename post-filename))) - (not (and (file-exists-p pub-filename) - (file-newer-than-file-p pub-filename post-filename))))) + (org-static-blog-matching-publish-filename post-filename))) + (not (and (file-exists-p pub-filename) + (file-newer-than-file-p pub-filename post-filename))))) (defun org-static-blog-matching-publish-filename (post-filename) "Generate HTML file name for POST-FILENAME." (concat-to-dir org-static-blog-publish-directory - (org-static-blog-get-post-public-path post-filename))) + (org-static-blog-get-post-public-path post-filename))) (defun org-static-blog-get-post-filenames () "Returns a list of all posts." @@ -466,9 +468,9 @@ unconditionally." (require 'seq) (make-directory (file-name-directory file) t) (car (seq-filter - (lambda (buf) - (string= (with-current-buffer buf buffer-file-name) file)) - (buffer-list)))) + (lambda (buf) + (string= (with-current-buffer buf buffer-file-name) file)) + (buffer-list)))) ;; This macro is needed for many of the following functions. (defmacro org-static-blog-with-find-file (file contents &rest body) @@ -476,104 +478,103 @@ unconditionally." The buffer is disposed after the macro exits (unless it already existed before)." `(save-excursion - (let ((current-buffer (current-buffer)) - (buffer-exists (org-static-blog-file-buffer ,file)) - (result nil) - (auto-insert nil) - (contents ,contents)) - (if buffer-exists - (switch-to-buffer buffer-exists) - (find-file ,file)) - (erase-buffer) - (insert contents) - (setq result (progn ,@body)) - (basic-save-buffer) - (unless buffer-exists - (kill-buffer)) - (switch-to-buffer current-buffer) - result))) + (let ((current-buffer (current-buffer)) + (buffer-exists (org-static-blog-file-buffer ,file)) + (result nil) + (auto-insert nil) + (contents ,contents)) + (if buffer-exists + (switch-to-buffer buffer-exists) + (find-file ,file)) + (erase-buffer) + (insert contents) + (setq result (progn ,@body)) + (basic-save-buffer) + (unless buffer-exists + (kill-buffer)) + (switch-to-buffer current-buffer) + result))) (defun org-static-blog-get-date (post-filename) "Extract the `#+date:` from POST-FILENAME as date-time." (let ((case-fold-search t)) - (with-temp-buffer - (insert-file-contents post-filename) - (goto-char (point-min)) - (if (search-forward-regexp "^\\#\\+date:[ ]*[[<]?\\([^]>]+\\)[]>]?$" nil t) - (date-to-time (match-string 1)) - (time-since 0))))) + (with-temp-buffer + (insert-file-contents post-filename) + (goto-char (point-min)) + (if (search-forward-regexp "^\\#\\+date:[ ]*[[<]?\\([^]>]+\\)[]>]?$" nil t) + (date-to-time (match-string 1)) + (time-since 0))))) (defun org-static-blog-get-title (post-filename) "Extract the `#+title:` from POST-FILENAME." (let ((case-fold-search t)) - (with-temp-buffer - (insert-file-contents post-filename) - (goto-char (point-min)) - (if (search-forward-regexp "^\\#\\+title:[ ]*\\(.+\\)$" nil t) - (match-string 1) - (warn "%s file does not have a title, using %s as the title" post-filename post-filename) - post-filename)))) + (with-temp-buffer + (insert-file-contents post-filename) + (goto-char (point-min)) + (if (search-forward-regexp "^\\#\\+title:[ ]*\\(.+\\)$" nil t) + (match-string 1) + (warn "%s file does not have a title, using %s as the title" post-filename post-filename) + post-filename)))) (defun org-static-blog-get-description (post-filename) "Extract the `#+description:` from POST-FILENAME." (let ((case-fold-search t)) - (with-temp-buffer - (insert-file-contents post-filename) - (goto-char (point-min)) - (when (search-forward-regexp "^\\#\\+description:[ ]*\\(.+\\)$" nil t) - (let ((description (string-trim (match-string 1)))) - (unless (zerop (length description)) - description)))))) + (with-temp-buffer + (insert-file-contents post-filename) + (goto-char (point-min)) + (when (search-forward-regexp "^\\#\\+description:[ ]*\\(.+\\)$" nil t) + (let ((description (string-trim (match-string 1)))) + (unless (zerop (length description)) + description)))))) (defun org-static-blog-get-image (post-filename) "Extract the `#+image:` from POST-FILENAME." (let ((case-fold-search t)) - (with-temp-buffer - (insert-file-contents post-filename) - (goto-char (point-min)) - (when (search-forward-regexp "^\\#\\+image:[ ]*\\(.+\\)$" nil t) - (let ((image (string-trim (match-string 1)))) - (unless (zerop (length image)) - image)))))) + (with-temp-buffer + (insert-file-contents post-filename) + (goto-char (point-min)) + (when (search-forward-regexp "^\\#\\+image:[ ]*\\(.+\\)$" nil t) + (let ((image (string-trim (match-string 1)))) + (unless (zerop (length image)) + image)))))) (defun org-static-blog-get-tags (post-filename) "Extract the `#+filetags:` from POST-FILENAME as list of strings." (let ((case-fold-search t)) - (with-temp-buffer - (insert-file-contents post-filename) - (goto-char (point-min)) - (if (search-forward-regexp "^\\#\\+filetags:[ ]*:\\(.*\\):$" nil t) - (split-string (match-string 1) ":") - (if (search-forward-regexp "^\\#\\+filetags:[ ]*\\(.+\\)$" nil t) - (split-string (match-string 1)) - ))))) + (with-temp-buffer + (insert-file-contents post-filename) + (goto-char (point-min)) + (if (search-forward-regexp "^\\#\\+filetags:[ ]*:\\(.*\\):$" nil t) + (split-string (match-string 1) ":") + (if (search-forward-regexp "^\\#\\+filetags:[ ]*\\(.+\\)$" nil t) + (split-string (match-string 1))))))) (defun org-static-blog-get-tag-tree () "Return an association list of tags to filenames. -e.g. `(('foo' 'file1.org' 'file2.org') ('bar' 'file2.org'))`" +e.g. `((`foo' `file1.org' `file2.org') (`bar' `file2.org'))`" (let ((tag-tree '())) - (dolist (post-filename (org-static-blog-get-post-filenames)) - (let ((tags (org-static-blog-get-tags post-filename))) - (dolist (tag (remove org-static-blog-rss-excluded-tag tags)) - (if (assoc-string tag tag-tree t) - (push post-filename (cdr (assoc-string tag tag-tree t))) - (push (cons tag (list post-filename)) tag-tree))))) - tag-tree)) + (dolist (post-filename (org-static-blog-get-post-filenames)) + (let ((tags (org-static-blog-get-tags post-filename))) + (dolist (tag (remove org-static-blog-rss-excluded-tag tags)) + (if (assoc-string tag tag-tree t) + (push post-filename (cdr (assoc-string tag tag-tree t))) + (push (cons tag (list post-filename)) tag-tree))))) + tag-tree)) (defun org-static-blog--preview-region () "Find the start and end of the preview in the current buffer." (goto-char (point-min)) (if org-static-blog-preview-end - (when (or (search-forward (or org-static-blog-preview-start "") nil t) - (search-forward "
" nil t)) - (let ((start (match-beginning 0))) - (or (search-forward org-static-blog-preview-end nil t) - (search-forward "
" nil t)) - (buffer-substring-no-properties start (point)))) - (when (search-forward (or org-static-blog-preview-start "") nil t) - (let ((start (match-beginning 0))) - (search-forward "
") - (buffer-substring-no-properties start (point)))))) + (when (or (search-forward (or org-static-blog-preview-start "") nil t) + (search-forward "
" nil t)) + (let ((start (match-beginning 0))) + (or (search-forward org-static-blog-preview-end nil t) + (search-forward "
" nil t)) + (buffer-substring-no-properties start (point)))) + (when (search-forward (or org-static-blog-preview-start "") nil t) + (let ((start (match-beginning 0))) + (search-forward "
") + (buffer-substring-no-properties start (point)))))) (defun org-static-blog-get-preview (post-filename) "Get title, date, tags from POST-FILENAME and get the first paragraph from the rendered HTML. @@ -581,35 +582,35 @@ If the HTML body contains multiple paragraphs, include only the first paragraph, and display an ellipsis. Preamble and Postamble are excluded, too." (with-temp-buffer - (insert-file-contents (org-static-blog-matching-publish-filename post-filename)) - (let ((post-title (org-static-blog-get-title post-filename)) - (post-date (org-static-blog-get-date post-filename)) - (post-taglist (org-static-blog-post-taglist post-filename)) - (post-ellipsis "") - (preview-region (org-static-blog--preview-region))) - (when (and preview-region (search-forward "" nil t)) - (setq post-ellipsis - (concat (when org-static-blog-preview-link-p - (format "" - (org-static-blog-get-post-url post-filename))) - org-static-blog-preview-ellipsis - (when org-static-blog-preview-link-p "\n")))) - ;; Put the substrings together. - (let ((title-link - (format "
" nil t)) + (setq post-ellipsis + (concat (when org-static-blog-preview-link-p + (format "" + (org-static-blog-get-post-url post-filename))) + org-static-blog-preview-ellipsis + (when org-static-blog-preview-link-p "\n")))) + ;; Put the substrings together. + (let ((title-link + (format "