A handy module to publish org headlines to the lektor blog.
A handy module to publish org headlines to the lektor blog.
Purpose
This file tackles the problem of publishing the org files to lektor blogging platform. It convert the current heading into a lektor compatible markdown file and copies to the lektor blog directory with associated media if any.
Requirements
This module assumes the following:
- a working lektor installation.
- org-mode is installed.
- ox-gfm module required to generate git flavoured markdown exists.
Usage
Usage is a very simple 2 step process:
- Make sure heading you want to publish is selected.
- Run lektor-publish-post-to-blog function.
;; (lektor-publish-post-to-blog)
Before using the conversion function, ensure that the necessary custom variables are set appropriately.
Main Module
This module provides functions to convert Org files to Lektor format for publishing as blog posts.
Export module start
;; org-to-lektor.el --- Convert Org files to Lektor format ;; pre-requisites start ;;; ensure that org-mode exists (use-package org) ;;; ensure git flavoured markdown module is installed. (defun lektor-ensure-gfm-is-installed() "this function will ensure that github flavoured markdown gfm or normal markdown backend is installed." (use-package ox-gfm :diminish t)) ;; pre-requisites ends
The `lektor-ensure-gfm-is-installed` function checks if the `ox-gfm` package is installed and loaded. If not, it ensures that the package is installed and loaded.
Custom Variables
Custom variables are used to configure the Lektor blog directory, post directory, assets directory, and other options.
;; custom variable required by Lektor (defcustom lektor-blog-directory "~/blog" "Directory where the Lektor blog is located." :group 'org-to-lektor :type 'directory) (defcustom lektor-post-directory (concat lektor-blog-directory "/content/blog") "Directory where Lektor blog posts are stored." :group 'org-to-lektor :type 'directory) (defcustom lektor-assets-directory (concat lektor-blog-directory "/assets/static") "Directory where Lektor assets (static files) are stored." :group 'org-to-lektor :type 'directory) (defcustom org-export-with-toc nil "Whether to include table of contents in Org exports." :group 'org-to-lektor :type 'boolean) (defcustom org-export-with-title nil "Whether to include title in Org exports." :group 'org-to-lektor :type 'boolean) (defcustom lektor-post-properties '("lektor_title" "lektor_pub_date" "lektor_author" "lektor_categories" "lektor_tags" "lektor_model") "List of lektor post properties." :group 'org-to-lektor :type '(repeat string)) ;; custom variable ends
The following custom variables are defined:
- `lektor-blog-directory`: The directory where the Lektor blog is located.
- `lektor-post-directory`: The directory where Lektor blog posts are stored. It is set as a subdirectory of the blog directory.
- `lektor-assets-directory`: The directory where Lektor assets (static files) are stored. It is set as a subdirectory of the blog directory.
- `org-export-with-toc`: Whether to include a table of contents in Org exports.
- `org-export-with-title`: Whether to include the title in Org exports.
- `lektor-post-properties`: A list of lektor post properties.
Set Post Properties
-
The `lektor-set-post-properties` function:
- Sets properties of a post.
- Calls the `lektor-set-title` function to set the post title property (`LEKTORTITLE`) based on the headline of the current entry.
- Iterates over a list of post properties (`lektor-post-properties`).
- For each property:
- If it is `lektorpubdate`, calls `org-set-property` with the property and the result of `org-read-date` to set the publication date of the post.
- If it is `lektortitle`, calls `org-set-property` with the property and the value obtained from `org-entry-get` using the "item" argument.
- Otherwise, calls `org-set-property` with the property and the value obtained from `org-read-property-value` using the property as the argument.
-
The `lektor-set-title` function:
- Sets the post title property (`LEKTORTITLE`).
- Calls `org-entry-get` with the `nil` argument and "LEKTORTITLE" as the property name to check if the property is already set.
- If the property is not set or is an empty string:
- Sets the property by calling `org-set-property` with "LEKTORTITLE" and the value obtained from `org-entry-get` using the "ITEM" argument.
- If the property is already set:
- Displays a message indicating that the property is already set and shows the current value.
-
The `lektor-get-post-title` function:
- Retrieves the post title property (`LEKTORTITLE`).
- Calls `org-entry-get` with the `nil` argument and "LEKTORTITLE" as the property name to obtain the value.
These functions provide functionality for managing post properties, specifically the title, in an org-mode document for use in a Lektor-based system.
```emacs-lisp ;; START_function to set the properties of the post (defun lektor-set-post-properties () "Set PUB_DATE by calling org-read-date function. Set rest of the properties by calling org-set-property function" (interactive) (lektor-set-title) (dolist (prop lektor-post-properties) (cond ((equal prop "lektor_pub_date") (org-set-property prop (org-read-date))) ((equal prop "lektor_title") (org-set-property "lektor_title" (org-entry-get nil "item"))) (t (org-set-property prop (org-read-property-value prop))))))
;;; functions to return the post-title (defun lektor-get-post-title () "read and returns post title property." (org-entry-get nil "LEKTOR_TITLE"))
(defun lektor-set-title () "This function reads the headline of the current entry and sets it as the LEKTOR_TITLE property." (interactive) (let ((lektor-title (org-entry-get nil "LEKTOR_TITLE"))) (if (or (equal lektor-title nil) (equal lektor-title "")) (progn (message "Setting LEKTOR_TITLE property to: %s" (org-entry-get nil "ITEM")) (org-set-property "LEKTOR_TITLE" (org-entry-get nil "ITEM"))) (message "LEKTOR_TITLE property already set: %s" lektor-title)))) ;; END_functions to set properties of the post ```
AST Tree Manipulation
The following functions are used to manipulate the Abstract Syntax Tree (AST) of the org-heading.
- The `lektor-make-ast-tree` function is used to create an Abstract Syntax Tree (AST) representation of an Org heading.
- When called, it checks if the `ast-tree` argument is valid. If not provided or nil, it creates an AST tree for the current headline.
- The function operates within the current buffer and saves the current position to be restored later.
- It also saves the buffer restriction to the current subtree using `org-narrow-to-subtree` to focus solely on the heading's content.
- If `ast-tree` is not provided, the function uses `org-element-parse-buffer` to parse the narrowed subtree and generate the AST tree.
- Once the AST tree is created, the buffer restriction is widened again using `widen` to restore the full buffer view.
- Finally, a message is displayed indicating the successful creation of the AST tree for the current headline.
- The resulting AST tree is returned as the value of the function.
This function is useful for manipulating and analyzing the structure of Org headings programmatically by providing a convenient way to obtain their AST representation.
Please let me know if there is anything else I can assist you with.
;;; functions to make an AST tree of the org-heading (defun lektor-make-ast-tree (&optional ast-tree) "Check if AST-TREE is valid, otherwise create an AST tree for the current headline." (save-excursion (save-restriction (unless ast-tree (org-narrow-to-subtree) (setq ast-tree (org-element-parse-buffer)) (widen) (message "Created AST tree for current headline")))) ast-tree)
Transformations
-
Front Matter Transformation
This function facilitates the conversion of Lektor properties within an Org heading into a format that can be easily consumed by Lektor, a static website generator.
- The `lektor-transform-properties-to-front-matter` function is designed to extract Lektor properties from an Org heading and transform them into the Lektor-compatible front matter format.
- When called interactively, it operates on the current Org heading.
- The function begins by retrieving all the properties of the current heading using `org-entry-properties` with the 'standard' parameter, which includes inherited properties.
- Next, it filters the properties list to extract only those that start with the prefix "lektor_" using `seq-filter` and a lambda function.
- The resulting filtered properties are stored in the `lektor-post-properties` variable.
- The function then proceeds to construct the front matter string by iterating over the `lektor-post-properties` using `mapconcat`.
- For each property, the lambda function extracts the property name by removing the "lektor_" prefix and converts it to lowercase.
- It then formats the property and its value using `format` with the format string "%s: %s", resulting in a key-value pair.
- The key-value pairs are concatenated using "\n" as the separator.
- Finally, the front matter string is returned, with "—" used as the separator between the front matter and the content of the Org heading.
emacs-lisp ;;; functions to tranform the front matter to lektor compatible format (defun lektor-transform-properties-to-front-matter () "Find all Lektor properties and return them in Lektor post front matter format." (interactive) (let* ((props (org-entry-properties nil 'standard)) (lektor-post-properties (seq-filter (lambda (prop) (string-prefix-p "lektor_" (car prop) t)) props)) (front-matter (mapconcat (lambda (prop) (format "%s: %s" (downcase (substring (car prop) 7)) (cdr prop))) lektor-post-properties "\n---\n"))) (concat front-matter "\n---\n")))
-
Transform the local media links
-
`lektor-get-post-media-paths`:
- The `lektor-get-post-media-paths` function scans the entire post represented by an Abstract Syntax Tree (AST) and returns the paths of all file links present.
- When called interactively, it operates on the current post's AST.
- The function begins by saving the current position and buffer restriction using `save-excursion` and `save-restriction`, respectively.
- If an `ast-tree` argument is not provided, it creates an AST tree for the current post by calling the `lektor-make-ast-tree` function.
- It then uses `org-element-map` to iterate over the AST tree, specifically targeting elements of type 'link'.
- For each link element, it checks if the link type is "file" using `org-element-property` and compares it with the string "file".
- If the link type is "file", it retrieves the path of the file link using `org-element-property` and adds it to the `file-links` list.
- The resulting `file-links` list may contain `nil` values, so `seq-remove` is used to filter out those `nil` values.
- Finally, the function returns the filtered `file-links` list.
-
`lektor-transform-local-file-links`:
- The `lektor-transform-local-file-links` function is responsible for modifying the links pointing to the local file system within the post's AST.
- When called interactively, it operates on the current post's AST.
- The function begins by saving the current position using `save-excursion`.
- If an `ast-tree` argument is not provided, it creates an AST tree for the current post by calling the `lektor-make-ast-tree` function.
- It then uses `org-element-map` to iterate over the AST tree, targeting elements of type 'link'.
- For each link element, it checks if the link type is either "file" or "fuzzy" using `org-element-property`.
- If the link type matches either of these, it retrieves the file path using `org-element-property`.
- The function then extracts the file name from the file path using `file-name-nondirectory`.
- It prepends "file:" to the file name and updates the `:raw-link` and `:path` properties of the link element using `org-element-put-property`.
- This effectively transforms the local file links within the AST to include the "file:" protocol prefix.
- Finally, the modified AST tree is returned as the value of the function.
These functions can be used to extract media paths from an Org post's AST and transform local file links to include the "file:" protocol prefix.
```emacs-lisp ;;; scan the post's AST tree and find all the media links. (defun lektor-get-post-media-paths (&optional ast-tree) "Scan the entire post and return the paths of file links." (interactive) (save-excursion (save-restriction (let* ((ast-tree (or ast-tree (lektor-make-ast-tree))) (file-links (org-element-map ast-tree 'link (lambda (link) (when (string= (org-element-property :type link) "file") (org-element-property :path link)))))) (seq-remove #'null file-links)))))
(defun lektor-transform-local-file-links (&optional ast-atree) "This function prepends 'file:' to all links pointing to the local file system." (interactive) (let ((ast-tree (or ast-atree (lektor-make-ast-tree)))) (org-element-map ast-tree 'link (lambda (link) (when (or (string= (org-element-property :type link) "file") (string= (org-element-property :type link) "fuzzy")) (let ((file-path (file-name-nondirectory (org-element-property :path link)))) (org-element-put-property link :raw-link (concat "file:" file-path)) (org-element-put-property link :path file-path)))))) ast-tree) ```
-
-
Transform the post to markdown format.
Certainly! Here is the literate description of the `lektor-transform-org-markdown` function:
- The `lektor-transform-org-markdown` function is used to export the content of the current Org heading into Markdown format.
- When called interactively, it operates on the current heading.
- The function first ensures that the necessary Markdown exporter is installed by calling the `lektor-ensure-gfm-is-installed` function. This ensures that GitHub Flavored Markdown (GFM) is available for export.
- The variable `ast-tree` represents the Abstract Syntax Tree (AST) of the current heading, or it can be provided as an optional argument.
- The variable `backend` is set to `'gfm`, indicating the use of GitHub Flavored Markdown for export.
- The variables `body` and `md-body` are initialized to store the intermediate and final results, respectively.
- The function creates a temporary buffer using `with-temp-buffer`.
- It interprets the data in the `ast-tree` using `org-element-interpret-data` and assigns the result to the `body` variable.
- The `body` is inserted into the temporary buffer.
- The function then exports the content of the buffer as Markdown using `org-export-as` with the `backend` specified.
- The resulting Markdown content is assigned to the `md-body` variable.
- The temporary buffer is cleared using `erase-buffer`.
- Finally, the `md-body` containing the Markdown representation of the heading's content is returned as the value of the function.
This function provides a convenient way to export the content of an Org heading to Markdown format, specifically using GitHub Flavored Markdown (GFM).
If you have any further questions, feel free to ask!
emacs-lisp (defun lektor-transform-org-markdown (&optional ast-tree) "Exports the content of the current heading into markdown format." (interactive) (lektor-ensure-gfm-is-installed) (let* ((ast-tree (or ast-tree (lektor-make-ast-tree))) (backend 'gfm) (body) (md-body)) (with-temp-buffer (setq body (org-element-interpret-data ast-tree)) (insert body) (setq md-body (org-export-as backend)) (erase-buffer)) md-body))
Output functions
-
Write post to the content directory
The `lektor-write-post-to-content-directory` function is responsible for writing a Markdown-formatted post body, including front matter, to a specified file path within a content directory.
- The function takes two arguments: `md-post`, which represents the Markdown-formatted post body, and `post-file-path`, which specifies the path to the post file within the content directory.
- The function creates a temporary buffer using `with-temp-buffer`.
- It inserts the `md-post` content into the temporary buffer.
- To ensure consistent indentation, the function calls `untabify` to convert all tabs to spaces within the buffer.
- Next, the function creates the directory specified by `post-file-path` using `make-directory`. The `t` argument ensures that any necessary parent directories are also created.
- It then uses `write-region` to write the contents of the temporary buffer to a file named "contents.lr" within the `post-file-path` directory.
- The file is created or overwritten if it already exists.
- Finally, a message is displayed indicating the successful export to `post-file-path`.
This function facilitates the process of writing a Markdown-formatted post, including its front matter, to a specific file path within a content directory.
emacs-lisp (defun lektor-write-post-to-content-directory (md-post post-file-path) "Writes the front-matter markdown-formatted post-body to the specified post-file-path." (with-temp-buffer (insert md-post) (untabify (point-min) (point-max)) (make-directory post-file-path t) (write-region (point-min) (point-max) (concat post-file-path "/contents.lr")) (message "Exported to %s" post-file-path)))
-
Copy the media to the content directory
The `lektor-copy-post-media-to-content-directory` function is responsible for copying media files associated with a blog post to the post's directory within the content directory.
- The function takes two arguments: `media-paths`, which represents a list of media file paths, and `lektor-post-file-path`, which specifies the path to the post's directory within the content directory.
- The function begins by displaying informational messages to indicate the start of the process.
- It also displays the list of `media-paths` to provide visibility into the assets being copied.
- Next, the function iterates over each media path in the `media-paths` list using `dolist`.
- For each media path, it creates the post's directory specified by `lektor-post-file-path` using `make-directory`. The `t` argument ensures that any necessary parent directories are also created.
- The function then displays a message indicating the asset being copied.
- It uses `copy-file` to copy the media file to the post's directory. The destination is specified as `(concat lektor-post-file-path "/")`, ensuring that the file is copied to the correct directory. The `t` argument indicates that if a file with the same name already exists, it should be overwritten.
- This process is repeated for each media path in the list.
- Finally, a message is displayed indicating the completion of the entire process.
This function facilitates the task of copying media files associated with a blog post to the appropriate directory within the content directory.
emacs-lisp (defun lektor-copy-post-media-to-content-directory (media-paths lektor-post-file-path) "this functions copies the media files to the post directory." (message "saved blog post...") (message "copying assets into the right path...") (message "asset list is:%s" media-paths) ;; Finally copy the asset files to the exported folder. ;; If folder by the post name doesn't exist create it. (dolist (media media-paths) (make-directory lektor-post-file-path t) (message "copying asset: %s" media) (copy-file media (concat lektor-post-file-path "/") t)) (message "Entire process complete...."))
Main function to publish post to the lektor blog in markdown format
The `lektor-publish-post-to-blog` function is the main function responsible for publishing the current headline as a blog post in Lektor. The `lektor-publish-post-to-blog` function is an autoloaded function that serves as the entry point for publishing a blog post in Lektor.
- When called interactively, it executes a series of steps to publish the current headline.
- The function first calls `lektor-set-post-properties` to set the necessary properties for the Lektor blog post.
- It then saves the current position and buffer restriction using `save-excursion` and `save-restriction`, respectively.
- The variable `lektor-post-title` is assigned the post's title obtained from `lektor-get-post-title`.
- The variable `lektor-front-matter` is assigned the front matter properties in Lektor format, generated using `lektor-transform-properties-to-front-matter`.
- The variable `ast-tree` is assigned the AST tree of the current headline obtained by calling `lektor-make-ast-tree`.
- The variable `lektor-post-media-paths` is assigned the media file paths associated with the post, obtained using `lektor-get-post-media-paths` on `ast-tree`.
- The `lektor-transform-local-file-links` function is called with `ast-tree` to transform local file links within the AST tree.
- The resulting transformed AST tree is assigned to `transformed-ast-tree`.
- The variable `lektor-post-body` is assigned the Markdown representation of the post's content by calling `lektor-transform-org-markdown` with `transformed-ast-tree`.
- The variable `lektor-post-file-path` is set to the path of the post's directory within the Lektor content directory.
- The variable `lektor-post-assets-path` is set to the path of the assets directory associated with the post within the Lektor assets directory.
- The variable `lektor-full-post` is constructed by concatenating the front matter, post body, and relevant markers.
- Several messages are displayed to provide visibility into the intermediate values during the process.
- The `lektor-write-post-to-content-directory` function is called to write the full post content to the specified post file path.
- The `lektor-copy-post-media-to-content-directory` function is called to copy the media files associated with the post to the post's directory.
- A final message is displayed to indicate the successful export of the post.
This function serves as the main workflow for publishing a blog post in Lektor, including generating the necessary front matter, transforming the content, writing it to the appropriate files, and copying media assets to the corresponding directory.
;;;###autoload (defun lektor-publish-post-to-blog () "Main function to publish current headline to Lektor blog." (interactive) (lektor-set-post-properties) (save-excursion (save-restriction (let* ((lektor-post-title (lektor-get-post-title)) (lektor-front-matter (lektor-transform-properties-to-front-matter)) (ast-tree (lektor-make-ast-tree)) (lektor-post-media-paths (lektor-get-post-media-paths ast-tree)) (transformed-ast-tree (lektor-transform-local-file-links ast-tree)) (lektor-post-body (lektor-transform-org-markdown transformed-ast-tree)) (lektor-post-file-path (concat lektor-post-directory "/" lektor-post-title)) (lektor-post-assets-path (concat lektor-assets-directory "/" lektor-post-title)) (lektor-full-post (concat lektor-front-matter "\n---\n" "body:" "\n" lektor-post-body))) (message "modified ast tree is: %s" transformed-ast-tree) (message "lektor post body is: %s" lektor-post-body) (message "md converted post body is: %s" lektor-post-body) (message " %s" lektor-full-post) (lektor-write-post-to-content-directory lektor-full-post lektor-post-file-path) (lektor-copy-post-media-to-content-directory lektor-post-media-paths lektor-post-file-path) (message "exported the post successfully")))))
Export module ends
(provide 'org-to-lektor) ;;; org-to-lektor.el ends here