Categories: geek » emacs » org

RSS - Atom - Subscribe via email

This is a test post from Org Mode to 11ty

| blogging, org, emacs

At the moment, my Org file needs to be in the proper content directory. I'm planning to copy the way ox-hugo allows me to export to a different directory and export filename. In the meantime, this is a start.

;;; ox-11ty.el --- Eleventy export for Emacs Org Mode  -*- lexical-binding: t -*-

;; Copyright (C) 2021 Sacha Chua

;; Author: Sacha Chua <sacha@sachachua.com>
;; Version: 2.17.0
;; Package-Requires: ((emacs "27"))
;; Keywords: org, eleventy, 11ty
;; Homepage: https://github.com/sachac/ox-11ty

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; A very rough starting point for exporting to 11ty from Org Mode.
;;

;;; Code:

(require 'ox-html)

(defun org-11ty-template (contents info)
  (let* ((date (org-export-data (plist-get info :date) info))
         (title (org-export-data (plist-get info :title) info))
         (permalink (org-export-data (plist-get info :permalink) info))
         (categories (org-export-data (plist-get info :categories) info))
         (collections (org-export-data (plist-get info :collections) info))
         (front-matter (json-encode
                        (list :permalink permalink
                              :date date
                              :title title
                              :categories (split-string categories)
                              :tags (split-string collections)))))
    (format
     "module.exports = class {
  data() {
    return %s;
  }
  render() {
    return %s;
  }
}"
     front-matter
     (json-encode-string contents))))

(defun org-11ty-export-as-11ty (&optional async subtreep visible-only body-only ext-plist)
  "Export current buffer as 11ty file."
  (interactive)
  (org-export-to-buffer '11ty "*org 11ty export*" async subtreep visible-only body-only ext-plist))

(defun org-11ty-export-to-11ty (&optional async subtreep visible-only body-only ext-plist)
  (interactive)
  (let* ((info
          (org-combine-plists
           (org-export--get-export-attributes '11ty subtreep visible-only)
           (org-export--get-buffer-attributes)
           (org-export-get-environment '11ty subtreep)))
         (base-file-name (concat (or
                                  (and (plist-get info :file-name)
                                       (if (string= (file-name-base (plist-get info :file-name)) "")
                                           (concat (plist-get info :file-name) "index")
                                         (plist-get info :file-name)))
                                  (org-export-output-file-name "" subtreep))
                                 ".11ty.js"))
         (file
          (if (plist-get info :base-dir)
              (expand-file-name base-file-name (plist-get info :base-dir))
            base-file-name)))
    (when (file-name-directory file)
      (make-directory (file-name-directory file) :parents))
    (org-export-to-file '11ty file
      async subtreep visible-only body-only ext-plist)))

(org-export-define-derived-backend '11ty 'html
  :menu-entry
  '(?1 "Export to 11ty JS"
       ((?b "As buffer" org-11ty-export-as-11ty) 
        (?1 "To file" org-11ty-export-to-11ty)))
  :translate-alist
  '((template . org-11ty-template))
  :options-alist
  '((:permalink "PERMALINK" nil nil)
    (:categories "CATEGORIES" nil 'split)
    (:base-dir "ELEVENTY_BASE_DIR" nil nil)
    (:file-name "FILE_NAME" nil nil)
    (:collections "ELEVENTY_COLLECTIONS" nil 'split)))

;;; ox-11ty.el ends here

https://github.com/sachac/ox-11ty/

View or add comments

Add a note to the bottom of blog posts exported from my config file

Posted: - Modified: | emacs, org

Update: 2021-04-18: Tweaked the code so that I could add it to the main org-export-filter-body-functions list now that I'm using Eleventy and ox-11ty.el instead of Wordpress and org2blog.

I occasionally post snippets from my Emacs configuration file, drafting the notes directly in my literate config and posting them via org2blog. I figured it might be a good idea to include a link to my config at the end of the posts, but I didn't want to scatter redundant links in my config file itself. Wouldn't it be cool if the link could be automatically added whenever I use org2blog to post a subtree from my config file? I think the code below accomplishes that.

(defun my/org-export-filter-body-add-emacs-configuration-link (string backend info)
  (when (and (plist-get info :input-file) (string-match "\\.emacs\\.d/Sacha\\.org" (plist-get info :input-file)))
    (concat string
            (let ((id (org-entry-get-with-inheritance "CUSTOM_ID")))
              (format
               "\n<div class=\"note\">This is part of my <a href=\"https://sachachua.com/dotemacs%s\">Emacs configuration.</a></div>"
               (if id (concat "#" id) ""))))))

(use-package org
  :config
  (add-to-list 'org-export-filter-body-functions #'my/org-export-filter-body-add-emacs-configuration-link))
This is part of my Emacs configuration.
View or add comments

Org Mode: Create a quick timestamped note and capture a screenshot

| emacs, org

I wanted to be able to quickly create timestamped notes and possibly capture a screenshot. Prompting for a value inside an org-capture-template disrupts my screen a little, so maybe this will make it as easy as possible. I could probably do this without going through org-capture-templates, but I wanted to take advantage of the fact that Org Mode will deal with the date tree and finding the right position itself.

(use-package org
  :config
  (add-to-list 'org-capture-templates
               '("p" "Podcast log - timestamped" item
                 (file+olp+datetree "~/orgzly/timestamped.org")
                 "%<%H:%M:%S,%3N> %^{Note}"
                 :immediate-finish t)))
  (defun my/org-capture-prefill-template (template &rest values)
    "Pre-fill TEMPLATE with VALUES."
    (setq template (or template (org-capture-get :template)))
    (with-temp-buffer
      (insert template)
      (goto-char (point-min))
      (while (re-search-forward
              (concat "%\\("
                      "\\[\\(.+\\)\\]\\|"
                      "<\\([^>\n]+\\)>\\|"
                      "\\([tTuUaliAcxkKInfF]\\)\\|"
                      "\\(:[-a-zA-Z]+\\)\\|"
                      "\\^\\({\\([^}]*\\)}\\)"
                      "?\\([gGtTuUCLp]\\)?\\|"
                      "%\\\\\\([1-9][0-9]*\\)"
                      "\\)") nil t)
        (if (car values)
            (replace-match (car values) nil t))
        (setq values (cdr values)))
      (buffer-string)))
(defun my/capture-screenshot (time &optional note)
  "Capture screenshot and save it to a file labeled with TIME and NOTE.
Return the filename."
  (interactive (list (current-time) (read-string "Note: ")))
  (let* ((filename (expand-file-name
                        (concat "Screenshot_"
                                (format-time-string "%Y%0m%d_%H%M%S" time)
                                (if note (concat " " note) "")
                                ".png")
                        "~/Pictures"))
         (cmd (concat "spectacle -b -o "
                      (shell-quote-argument filename))))
    (shell-command cmd)
    filename))
(defun my/capture-timestamped-note (time note)
  "Disable Helm and capture a quick timestamped note."
  (interactive (list (current-time) (read-string "Note: ")))
  (let ((helm-completing-read-handlers-alist '((org-capture . nil)))
        (entry (org-capture-select-template "p")))
    (org-capture-set-plist entry)
    (org-capture-get-template)
    (org-capture-set-target-location)
    (org-capture-put
     :template (org-capture-fill-template
                (my/org-capture-prefill-template (org-capture-get :template)
                                                 (format-time-string "%H:%M:%S,%3N")
                                                 note)))
    (org-capture-place-template)
    (org-capture-finalize)))
(defun my/capture-timestamped-note-with-screenshot (time note)
  (interactive (list (current-time) (read-string "Note: ")))
  (kill-new (my/capture-screenshot time note))
  (my/capture-timestamped-note time note))

Then I can call it with h h n for my/capture-timestamped-note or h h i for my/capture-timestamped-note-with-screenshot via keyboard shortcuts defined elsewhere in my config (see my/key-chord-commands).

View or add comments

#org-mode answers: task creation time, subtree at end, Emacs Lisp variables in TBLFM, logbook and refile

Posted: - Modified: | emacs, org

In the interest of getting more tips out there so that they can be searchable, here are a few things I helped people out with on the #org-mode channel on freenode.net and through e-mail.

How can I log task creation times in Org Mode?

You can use an Org capture template.

How can I create a subtree at the end of the current entry?

C-u C-u C-RET M-right gets you the behaviour without configuration, or you can use:

(defun my/org-insert-subheading-after () (interactive) (org-insert-subheading '(16)))

and bind it to a speed command or a shortcut.

How can I refer to Emacs Lisp variables in #+TBLFM?

#+TBLFM: @1$2='(+ @1$1 my-var1);L

How can I write a command that adds a logbook entry and refiles a subtree?

Here was the source that someone asked me for help on:

#+TODO: TODO(t!) | DONE(d!)
#+NAME: startup
#+BEGIN_SRC emacs-lisp
(setq org-log-into-drawer t)
(setq org-use-speed-commands t)
(defun my/refiletree (file headline &optional arg)
               (let ((pos (save-excursion
                      (find-file file)
                      (org-find-exact-headline-in-buffer headline))))
               (org-refile arg nil (list headline file nil pos)))
               (switch-to-buffer (current-buffer)))

;;(setq org-use-speed-commands 'my/org-use-speed-commands-for-headings-and-lists)

(add-to-list 'org-speed-commands-user '("t" (lambda ()
                                               (org-todo "TODO")
                                               (my/refiletree buffer-file-name "Next"))))
(add-to-list 'org-speed-commands-user '("d" (lambda ()
                                               (org-todo "DONE")
                                               (my/refiletree buffer-file-name "Done"))))
#+END_SRC

* Inbox
** Task 1
** Task 2
** Task 3
* Next
* Done

The problem was that the logbook entry was getting added to the wrong heading, since the subtree had already been refiled. It’s because logging is done in post-command-hook (example code from org-add-log-setup: (add-hook 'post-command-hook 'org-add-log-note 'append)). That’s why it gets confused. Try this. It defines a function to add to org-after-refile-insert-hook.

(setq org-log-into-drawer t)
(setq org-use-speed-commands t)
(defmacro my/def-state-and-refile-shortcut (key state heading)
  `(progn
     (defun ,(intern (concat "my/change-state-to-" state)) ()
       (org-todo ,state)
       (remove-hook 'org-after-refile-insert-hook (quote ,(intern (concat "my/change-state-to-" state)))))
     (add-to-list 'org-speed-commands-user
                  '(,key
                    (lambda ()
                      (add-hook 'org-after-refile-insert-hook (quote ,(intern (concat "my/change-state-to-" state))))
                      (my/refiletree buffer-file-name ,heading))))))
(my/def-state-and-refile-shortcut "t" "TODO" "Next")
(my/def-state-and-refile-shortcut "d" "DONE" "Done")
(defun my/refiletree (file headline &optional arg)
  (let ((pos (with-current-buffer (or (find-buffer-visiting file)
                                      (find-file-noselect file))
               (save-excursion
                 (org-find-exact-headline-in-buffer headline)))))
    (org-refile nil nil (list headline file nil pos))))
View or add comments

2020-09-07 Emacs news

Posted: - Modified: | emacs, org

Almost forgot to say: EmacsConf 2020 Call for Proposals is open until Sept 30, 2020. Please encourage someone you’d like to hear from! =)

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Hacker News, planet.emacslife.com, YouTube, the Emacs NEWS file and emacs-devel.

View or add comments

Updated my blog index using Org Mode

| emacs, org

I just spent 40 minutes updating my blog index to include all the non-Emacs News and non-weekly/monthly-review posts since April 2017. I had kept a blog index as a way to quickly organize my posts into finer-grained categories without mucking around too much with WordPress. Updating it was pretty easy since I had built an Org Mode list view into my theme eight years ago. A URL like https://sachachua.com/blog/2020/?org=1 gets me a list like:

- [[https://sachachua.com/blog/2020/01/2020-01-06-emacs-news/][2020-01-06 Emacs news]] 
- [[https://sachachua.com/blog/2020/01/weekly-review-week-ending-december-13-2019/][Weekly review: Week ending December 13, 2019]] 
- [[https://sachachua.com/blog/2020/01/weekly-review-week-ending-december-20-2019/][Weekly review: Week ending December 20, 2019]] 
- [[https://sachachua.com/blog/2020/01/weekly-review-week-ending-january-3-2020/][Weekly review: Week ending January  3, 2020]] 
- [[https://sachachua.com/blog/2020/01/weekly-review-week-ending-december-27-2019/][Weekly review: Week ending December 27, 2019]] 
- [[https://sachachua.com/blog/2020/01/2020-01-13-emacs-news/][2020-01-13 Emacs news]] 
- [[https://sachachua.com/blog/2020/01/2020-01-20-emacs-news/][2020-01-20 Emacs news]]
...

which is easy to narrow to in Emacs with narrow-to-region (C-x n n) and filter with flush-lines to get rid of all the fairly routine weekly reviews and Emacs news posts. Then I could use my/org-file-blog-index-entries from my Emacs config to file things to the high-level trees, and (while t (my/org-move-current-item-to-category (completing-read "Category: " (my/org-get-list-categories)))) to file things within a list.

Yay Emacs!

View or add comments

Having fun kerning using Org Mode and FontForge

| emacs, org

It turns out that working with font bearings and kerning tables using Org Mode makes lots of things so much easier.

Bearings in the top left, kerning matrix in the top right

While trying to figure out kerning, I came across this issue that described how you sometimes need a character-pair kern table instead of just class-based kerning. Since I had figured out character-based kerning before I figured out class-based kerning, it was easy to restore my Python code that takes the same kerning matrix and generates character pairs. Here’s what that code looks like.

def kern_by_char(font, kerning_matrix):
  # Add kerning by character as backup
  font.addLookupSubtable("kern", "kern-2")
  offsets = np.asarray(kerning_matrix)
  classes_right = [None if (x == "" or x == "None") else x.split(",") for x in offsets[0,1:]]
  classes_left = [None if (x == "" or x == "None") else x.split(',') for x in offsets[1:,0]]
  for r, row in enumerate(classes_left):
    if row is None: continue
    for first_letter in row:
      g = font.createMappedChar(first_letter)
      for c, column in enumerate(classes_right):
        if column is None: continue
        for second_letter in column:
          if kerning_matrix[r + 1][c + 1]:
            g.addPosSub("kern-2", second_letter, 0, 0, kerning_matrix[r + 1][c + 1], 0, 0, 0, 0, 0)
  return font

I wanted to be able to easily compare different versions of my font: my original glyphs versus my tweaked glyphs, simple spacing versus kerned. This was a hassle with FontForge, since I had to open different font files in different Metrics windows. If I execute a little bit of source code in my Org Mode, though, I can use my test web page to view all the different versions. By arranging my Emacs windows a certain way and adding :eval no to the Org Babel blocks I’m not currently using, I can easily change the relevant table entries and evaluate the whole buffer to regenerate the font versions, including exports to OTF and WOFF. Here’s the code for that:

<<params>>
<<def_import_glyphs>>
<<def_set_bearings>>
<<def_kern_classes>>
<<def_kern_by_char>>
font = fontforge.font()
font = import_glyphs(font, params)
font = set_bearings(font, bearings)
save_font(font, {**params, "new_otf": "sachacHandRaw.otf"})
font = kern_classes(font, kerning_matrix)
font = kern_by_char(font, kerning_matrix)
save_font(font, {**params, "new_otf": "sachacHandRawKerned.otf"})
font = load_font('SachaHandEdited.sfd')
font = set_bearings(font, bearings)
font.removeLookup('kern')
save_font(font, {**params, "new_otf": "sachacHandEdited.otf"})
font = kern_classes(font, kerning_matrix)
font = kern_by_char(font, kerning_matrix)
save_font(font, {**params, "new_otf": "sachacHand.otf"})

I also like the way it’s pretty easy to update multiple kerning values without clicking around. I sometimes use FontForge to get the number to set it to and then copy that into my table, but I also sometimes just tweak the number in Org Mode directly.

To see the results, I can generate a test HTML that shows me text with different versions of my font. I can also look at lots of kerning pairs at the same time. Here are the components of that test page:

def test_css(fonts):
  doc, tag, text, line = Doc().ttl()
  with tag('style'):
    for f in fonts:
      text("@font-face { font-family: '%s'; src: url('%s'); }" % (f[0], f[1]))
      text(".%s { font-family: '%s'; }" % (f[0], f[0]))
    text("table { font-size: inherit; font-weight: inherit }")
    text("td { text-align: left }")
    text(".blog-heading { font-weight: bold; font-size: 32px }")
    text(".default { color: gray }")
    text("body { font-family: woff, Arial, sans-serif; font-size: 32px; padding: 10px }")
  return doc.getvalue()
def test_html(strings):
  doc, tag, text, line = Doc().ttl()
  with doc.tag('table', style='border-bottom: 1px solid gray; width: 100%; border-collapse: collapse'):
    for s in strings:
      for i, f in enumerate(fonts):
        style = 'border-top: 1px solid gray' if (i == 0) else ""
        with tag('tr', klass=f[0], style=style):
          line('td', f[0])
          line('td', s)
  return doc.getvalue()
def test_kerning_matrix(kerning_matrix):
  doc, tag, text, line = Doc().ttl()
  with tag('table'):
    for r, row in enumerate(classes_left):
      if row is None: continue
      for first_letter in row:
        with tag('tr'):
          line('td', first_letter)
          for c, column in enumerate(classes_right):
            if column is None: continue
            for second_letter in column:
              klass = "kerned" if kerning_matrix[r + 1][c + 1] else "default"
              line('td', aglfn.to_glyph(first_letter) + aglfn.to_glyph(second_letter), klass=klass)
  return doc.getvalue()

This code actually generates the test file:

from yattag import Doc
import numpy as np
import aglfn
<<def_test_html>>
doc, tag, text, line = Doc().ttl()
fonts = [['raw', 'sachacHandRaw.otf'],
         ['raw-kerned', 'sachacHandRawKerned.otf'],
         ['edited', 'sachacHandEdited.otf'],
         ['woff', 'sachacHand.woff']]
strings = ["Python+FontForge+Org: I made a font based on my handwriting!",
           "Monthly review: May 2020",
           "Emacs News 2020-06-01"]
offsets = np.asarray(kerning_matrix)
classes_left = [None if (x == "" or x == "None") else x.split(',') for x in offsets[1:,0]]
classes_right = [None if (x == "" or x == "None") else x.split(",") for x in offsets[0,1:]]
with tag('html'):
  with tag('head'): 
    doc.asis(test_css(fonts))
  with tag('body'):
    line('h1', 'Test headings')
    with tag('div', klass="blog-heading"):
      doc.asis(test_html(strings))
    line('h1', 'Kerning matrix')
    doc.asis(test_kerning_matrix(kerning_matrix))
return doc.getvalue()

And here’s what that test.html looks like:

Testing the font with a few blog headings
Kerning pairs: black for specified pairs, gray for defaults

Not bad… Now Emacs is my font editor! The code is at https://github.com/sachac/sachac-hand .

View or add comments