Make xkcd-mode work with evil + over-engineering
This commit is contained in:
parent
9a67a30783
commit
3531508bbd
479
config.org
479
config.org
|
@ -988,7 +988,279 @@ done perfectly acceptable, so let's make that happen.
|
|||
We wan't to set this up so it loads nicely in [[*Extra links][Extra links]].
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(use-package! xkcd
|
||||
:commands (xkcd-get-json xkcd-download))
|
||||
:commands (xkcd-get-json xkcd-download xkcd-get
|
||||
;; now for funcs from my extension of this pkg
|
||||
+xkcd-find-and-copy +xkcd-find-and-view
|
||||
+xkcd-fetch-info +xkcd-select)
|
||||
:config
|
||||
(add-to-list 'evil-snipe-disabled-modes 'xkcd-mode)
|
||||
:general (:states 'normal
|
||||
:keymaps 'xkcd-mode-map
|
||||
"<right>" #'xkcd-next
|
||||
"n" #'xkcd-next ; evil-ish
|
||||
"<left>" #'xkcd-prev
|
||||
"N" #'xkcd-prev ; evil-ish
|
||||
"r" #'xkcd-rand
|
||||
"a" #'xkcd-rand ; because image-rotate can interfere
|
||||
"t" #'xkcd-alt-text
|
||||
"q" #'xkcd-kill-buffer
|
||||
"o" #'xkcd-open-browser
|
||||
"e" #'xkcd-open-explanation-browser
|
||||
;; extras
|
||||
"s" #'+xkcd-find-and-view
|
||||
"/" #'+xkcd-find-and-view
|
||||
"y" #'+xkcd-copy))
|
||||
#+END_SRC
|
||||
|
||||
Let's also extend the functionality a whole bunch.
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(after! xkcd
|
||||
(require 'emacsql-sqlite)
|
||||
|
||||
(defun +xkcd-select ()
|
||||
"Prompt the user for an xkcd using `ivy-read' and `+xkcd-select-format'. Return the xkcd number or nil"
|
||||
(let* (prompt-lines
|
||||
(-dummy (maphash (lambda (key xkcd-info)
|
||||
(push (+xkcd-select-format xkcd-info) prompt-lines))
|
||||
+xkcd-stored-info))
|
||||
(num (ivy-read (format "xkcd (%s): " xkcd-latest) prompt-lines)))
|
||||
(if (equal "" num) xkcd-latest
|
||||
(string-to-number (replace-regexp-in-string "\\([0-9]+\\).*" "\\1" num)))))
|
||||
|
||||
(defun +xkcd-select-format (xkcd-info)
|
||||
"Creates each ivy-read line from an xkcd info plist. Must start with the xkcd number"
|
||||
(format "%-4s %-30s %s"
|
||||
(propertize (number-to-string (plist-get xkcd-info :num))
|
||||
'face 'counsel-key-binding)
|
||||
(plist-get xkcd-info :title)
|
||||
(propertize (plist-get xkcd-info :alt)
|
||||
'face '(variable-pitch font-lock-comment-face))))
|
||||
|
||||
(defun +xkcd-fetch-info (&optional num)
|
||||
"Fetch the parsed json info for comic NUM. Fetches latest when omitted or 0"
|
||||
(require 'xkcd)
|
||||
(when (or (not num) (= num 0))
|
||||
(+xkcd-check-latest)
|
||||
(setq num xkcd-latest))
|
||||
(let ((res (or (gethash num +xkcd-stored-info)
|
||||
(puthash num (+xkcd-db-read num) +xkcd-stored-info))))
|
||||
(unless res
|
||||
(+xkcd-db-write
|
||||
(let* ((url (format "http://xkcd.com/%d/info.0.json" num))
|
||||
(json-assoc
|
||||
(if (assoc num +xkcd-stored-info)
|
||||
(assoc num +xkcd-stored-info)
|
||||
(json-read-from-string (xkcd-get-json url num)))))
|
||||
json-assoc))
|
||||
(setq res (+xkcd-db-read num)))
|
||||
res))
|
||||
|
||||
;; since we've done this, we may as well go one little step further
|
||||
(defun +xkcd-find-and-copy ()
|
||||
"Prompt for an xkcd using `+xkcd-select' and copy url to clipboard"
|
||||
(interactive)
|
||||
(+xkcd-copy (+xkcd-select)))
|
||||
|
||||
(defun +xkcd-copy (&optional num)
|
||||
"Copy a url to xkcd NUM to the clipboard"
|
||||
(interactive "i")
|
||||
(let ((num (or num xkcd-cur)))
|
||||
(gui-select-text (format "https://xkcd.com/%d" num))
|
||||
(message "xkcd.com/%d copied to clipboard" num)))
|
||||
|
||||
(defun +xkcd-find-and-view ()
|
||||
"Prompt for an xkcd using `+xkcd-select' and view it"
|
||||
(interactive)
|
||||
(xkcd-get (+xkcd-select))
|
||||
(switch-to-buffer "*xkcd*"))
|
||||
|
||||
(defvar +xkcd-latest-max-age (* 60 60) ; 1 hour
|
||||
"Time after which xkcd-latest should be refreshed, in seconds")
|
||||
|
||||
;; initialise `xkcd-latest' and `+xkcd-stored-info' with latest xkcd
|
||||
(add-transient-hook! '+xkcd-select
|
||||
(require 'xkcd)
|
||||
(+xkcd-fetch-info xkcd-latest)
|
||||
(setq +xkcd-stored-info (+xkcd-db-read-all)))
|
||||
|
||||
(add-transient-hook! '+xkcd-fetch-info
|
||||
(xkcd-update-latest))
|
||||
|
||||
(defun +xkcd-check-latest ()
|
||||
"Use value in `xkcd-cache-latest' as long as it isn't older thabn `+xkcd-latest-max-age'"
|
||||
(unless (and (file-exists-p xkcd-cache-latest)
|
||||
(< (- (time-to-seconds (current-time))
|
||||
(time-to-seconds (file-attribute-modification-time (file-attributes xkcd-cache-latest))))
|
||||
+xkcd-latest-max-age))
|
||||
(let* ((out (xkcd-get-json "http://xkcd.com/info.0.json" 0))
|
||||
(json-assoc (json-read-from-string out))
|
||||
(latest (cdr (assoc 'num json-assoc))))
|
||||
(when (/= xkcd-latest latest)
|
||||
(+xkcd-db-write json-assoc)
|
||||
(with-current-buffer (find-file xkcd-cache-latest)
|
||||
(setq xkcd-latest latest)
|
||||
(erase-buffer)
|
||||
(insert (number-to-string latest))
|
||||
(save-buffer)
|
||||
(kill-buffer (current-buffer)))))
|
||||
(shell-command (format "touch %s" xkcd-cache-latest))))
|
||||
|
||||
(defvar +xkcd-stored-info (make-hash-table :test 'eql)
|
||||
"Basic info on downloaded xkcds, in the form of a hashtable")
|
||||
|
||||
(defadvice! xkcd-get-json--and-cache (url &optional num)
|
||||
"Fetch the Json coming from URL.
|
||||
If the file NUM.json exists, use it instead.
|
||||
If NUM is 0, always download from URL.
|
||||
The return value is a string."
|
||||
:override #'xkcd-get-json
|
||||
(let* ((file (format "%s%d.json" xkcd-cache-dir num))
|
||||
(cached (and (file-exists-p file) (not (eq num 0))))
|
||||
(out (with-current-buffer (if cached
|
||||
(find-file file)
|
||||
(url-retrieve-synchronously url))
|
||||
(goto-char (point-min))
|
||||
(unless cached (re-search-forward "^$"))
|
||||
(prog1
|
||||
(buffer-substring-no-properties (point) (point-max))
|
||||
(kill-buffer (current-buffer))))))
|
||||
(unless (or cached (eq num 0))
|
||||
(xkcd-cache-json num out))
|
||||
out))
|
||||
|
||||
(defadvice! +xkcd-get (num)
|
||||
"Get the xkcd number NUM."
|
||||
:override 'xkcd-get
|
||||
(interactive "nEnter comic number: ")
|
||||
(xkcd-update-latest)
|
||||
(get-buffer-create "*xkcd*")
|
||||
(switch-to-buffer "*xkcd*")
|
||||
(xkcd-mode)
|
||||
(let (buffer-read-only)
|
||||
(erase-buffer)
|
||||
(setq xkcd-cur num)
|
||||
(let* ((xkcd-data (+xkcd-fetch-info num))
|
||||
(num (plist-get xkcd-data :num))
|
||||
(img (plist-get xkcd-data :img))
|
||||
(safe-title (plist-get xkcd-data :safe-title))
|
||||
(alt (plist-get xkcd-data :alt))
|
||||
title file)
|
||||
(message "Getting comic...")
|
||||
(setq file (xkcd-download img num))
|
||||
(setq title (format "%d: %s" num safe-title))
|
||||
(insert (propertize title
|
||||
'face 'outline-1))
|
||||
(center-line)
|
||||
(insert "\n")
|
||||
(xkcd-insert-image file num)
|
||||
(if (eq xkcd-cur 0)
|
||||
(setq xkcd-cur num))
|
||||
(setq xkcd-alt alt)
|
||||
(message "%s" title))))
|
||||
|
||||
(defconst +xkcd-db--sqlite-available-p
|
||||
(with-demoted-errors "+org-xkcd initialization: %S"
|
||||
(emacsql-sqlite-ensure-binary)
|
||||
t))
|
||||
|
||||
(defvar +xkcd-db--connection (make-hash-table :test #'equal)
|
||||
"Database connection to +org-xkcd database.")
|
||||
|
||||
(defun +xkcd-db--get ()
|
||||
"Return the sqlite db file."
|
||||
(expand-file-name "xkcd.db" xkcd-cache-dir))
|
||||
|
||||
(defun +xkcd-db--get-connection ()
|
||||
"Return the database connection, if any."
|
||||
(gethash (file-truename xkcd-cache-dir)
|
||||
+xkcd-db--connection))
|
||||
|
||||
(defconst +xkcd-db--table-schema
|
||||
'((xkcds
|
||||
[(num integer :unique :primary-key)
|
||||
(year :not-null)
|
||||
(month :not-null)
|
||||
(link :not-null)
|
||||
(news :not-null)
|
||||
(safe_title :not-null)
|
||||
(title :not-null)
|
||||
(transcript :not-null)
|
||||
(alt :not-null)
|
||||
(img :not-null)])))
|
||||
|
||||
(defun +xkcd-db--init (db)
|
||||
"Initialize database DB with the correct schema and user version."
|
||||
(emacsql-with-transaction db
|
||||
(pcase-dolist (`(,table . ,schema) +xkcd-db--table-schema)
|
||||
(emacsql db [:create-table $i1 $S2] table schema))))
|
||||
|
||||
(defun +xkcd-db ()
|
||||
"Entrypoint to the +org-xkcd sqlite database.
|
||||
Initializes and stores the database, and the database connection.
|
||||
Performs a database upgrade when required."
|
||||
(unless (and (+xkcd-db--get-connection)
|
||||
(emacsql-live-p (+xkcd-db--get-connection)))
|
||||
(let* ((db-file (+xkcd-db--get))
|
||||
(init-db (not (file-exists-p db-file))))
|
||||
(make-directory (file-name-directory db-file) t)
|
||||
(let ((conn (emacsql-sqlite db-file)))
|
||||
(set-process-query-on-exit-flag (emacsql-process conn) nil)
|
||||
(puthash (file-truename xkcd-cache-dir)
|
||||
conn
|
||||
+xkcd-db--connection)
|
||||
(when init-db
|
||||
(+xkcd-db--init conn)))))
|
||||
(+xkcd-db--get-connection))
|
||||
|
||||
(defun +xkcd-db-query (sql &rest args)
|
||||
"Run SQL query on +org-xkcd database with ARGS.
|
||||
SQL can be either the emacsql vector representation, or a string."
|
||||
(if (stringp sql)
|
||||
(emacsql (+xkcd-db) (apply #'format sql args))
|
||||
(apply #'emacsql (+xkcd-db) sql args)))
|
||||
|
||||
(defun +xkcd-db-read (num)
|
||||
(when-let ((res
|
||||
(car (+xkcd-db-query [:select * :from xkcds
|
||||
:where (= num $s1)]
|
||||
num
|
||||
:limit 1))))
|
||||
(+xkcd-db-list-to-plist res)))
|
||||
|
||||
(defun +xkcd-db-read-all ()
|
||||
(let ((xkcd-table (make-hash-table :test 'eql :size 4000)))
|
||||
(mapcar (lambda (xkcd-info-list)
|
||||
(puthash (car xkcd-info-list) (+xkcd-db-list-to-plist xkcd-info-list) xkcd-table))
|
||||
(+xkcd-db-query [:select * :from xkcds]))
|
||||
xkcd-table))
|
||||
|
||||
(defun +xkcd-db-list-to-plist (xkcd-datalist)
|
||||
`(:num ,(nth 0 xkcd-datalist)
|
||||
:year ,(nth 1 xkcd-datalist)
|
||||
:month ,(nth 2 xkcd-datalist)
|
||||
:link ,(nth 3 xkcd-datalist)
|
||||
:news ,(nth 4 xkcd-datalist)
|
||||
:safe-title ,(nth 5 xkcd-datalist)
|
||||
:title ,(nth 6 xkcd-datalist)
|
||||
:transcript ,(nth 7 xkcd-datalist)
|
||||
:alt ,(nth 8 xkcd-datalist)
|
||||
:img ,(nth 9 xkcd-datalist)))
|
||||
|
||||
(defun +xkcd-db-write (data)
|
||||
(+xkcd-db-query [:insert-into xkcds
|
||||
:values $v1]
|
||||
(list (vector
|
||||
(cdr (assoc 'num data))
|
||||
(cdr (assoc 'year data))
|
||||
(cdr (assoc 'month data))
|
||||
(cdr (assoc 'link data))
|
||||
(cdr (assoc 'news data))
|
||||
(cdr (assoc 'safe_title data))
|
||||
(cdr (assoc 'title data))
|
||||
(cdr (assoc 'transcript data))
|
||||
(cdr (assoc 'alt data))
|
||||
(cdr (assoc 'img data))
|
||||
)))))
|
||||
#+END_SRC
|
||||
* Language configuration
|
||||
*** File Templates
|
||||
|
@ -1985,210 +2257,7 @@ Saving seconds adds up after all! (but only so much)
|
|||
|
||||
(defun +org-xkcd-complete (&optional arg)
|
||||
"Complete xkcd using `+xkcd-stored-info'"
|
||||
(format "xkcd:%d" (+xkcd-select)))
|
||||
|
||||
(defun +xkcd-select ()
|
||||
"Prompt the user for an xkcd using `ivy-read' and `+xkcd-select-format'. Return the xkcd number or nil"
|
||||
(let ((num
|
||||
(ivy-read (format "xkcd (%s): " xkcd-latest)
|
||||
(mapcar #'+xkcd-select-format
|
||||
+xkcd-stored-info))))
|
||||
(if (equal "" num) xkcd-latest
|
||||
(string-to-number (replace-regexp-in-string "\\([0-9]+\\).*" "\\1" num)))))
|
||||
|
||||
(defun +xkcd-select-format (xkcd-info)
|
||||
"Creates each ivy-read line from an xkcd info plist. Must start with the xkcd number"
|
||||
(format "%-4s %-30s %s"
|
||||
(propertize (number-to-string (plist-get xkcd-info :num))
|
||||
'face 'counsel-key-binding)
|
||||
(plist-get xkcd-info :title)
|
||||
(propertize (plist-get xkcd-info :alt)
|
||||
'face '(variable-pitch font-lock-comment-face))))
|
||||
|
||||
(defun +xkcd-fetch-info (num)
|
||||
"Fetch the parsed json info for comic NUM"
|
||||
(require 'xkcd)
|
||||
(let ((res (+xkcd-db-read num)))
|
||||
(unless res
|
||||
(+xkcd-db-write
|
||||
(let* ((url (format "http://xkcd.com/%d/info.0.json" num))
|
||||
(json-assoc
|
||||
(if (assoc num +xkcd-stored-info)
|
||||
(assoc num +xkcd-stored-info)
|
||||
(json-read-from-string (xkcd-get-json url num)))))
|
||||
json-assoc))
|
||||
(setq res (+xkcd-db-read num)))
|
||||
res))
|
||||
|
||||
;; since we've done this, we may as well go one little step further
|
||||
(defun +xkcd-find-and-copy ()
|
||||
"Prompt the user for an xkcd using `+xkcd-select' and copy url to clipboard"
|
||||
(interactive)
|
||||
(let ((num (+xkcd-select)))
|
||||
(gui-select-text (format "https://xkcd.com/%d" num))
|
||||
(message "xkcd.com/%d copied to clipboard" num)))
|
||||
|
||||
(defun +xkcd-find-and-view ()
|
||||
"Prompt the user for an xkcd using `+xkcd-select' and copy url to clipboard"
|
||||
(interactive)
|
||||
(xkcd-get (+xkcd-select))
|
||||
(switch-to-buffer "*xkcd*"))
|
||||
|
||||
(defvar +xkcd-latest-max-age (* 60 60 12)
|
||||
"Time after which xkcd-latest should be refreshed, in seconds")
|
||||
|
||||
;; initialise `xkcd-latest' and `+xkcd-stored-info' with latest xkcd
|
||||
(add-transient-hook! '+xkcd-select
|
||||
(require 'xkcd)
|
||||
(xkcd-update-latest)
|
||||
(unless (and (file-exists-p xkcd-cache-latest)
|
||||
(< (- (time-to-seconds (current-time))
|
||||
(time-to-seconds (file-attribute-modification-time (file-attributes xkcd-cache-latest))))
|
||||
+xkcd-latest-max-age))
|
||||
(let* ((out (xkcd-get-json "http://xkcd.com/info.0.json" 0))
|
||||
(json-assoc (json-read-from-string out))
|
||||
(latest (cdr (assoc 'num json-assoc))))
|
||||
(when (/= xkcd-latest latest)
|
||||
(+xkcd-db-write json-assoc)
|
||||
(with-current-buffer (find-file xkcd-cache-latest)
|
||||
(setq xkcd-latest latest)
|
||||
(erase-buffer)
|
||||
(insert (number-to-string latest))
|
||||
(save-buffer)
|
||||
(kill-buffer (current-buffer))))))
|
||||
|
||||
(setq +xkcd-stored-info `(,(+xkcd-fetch-info xkcd-latest)))
|
||||
(+xkcd-update-stored-info))
|
||||
|
||||
(defvar +xkcd-stored-info nil
|
||||
"Basic info on downloaded xkcds, in the form ((num . title) ...)")
|
||||
|
||||
(defun +xkcd-update-stored-info ()
|
||||
"Compare the json files in `xkcd-cache-dir' to the info in `+xkcd-stored-info'
|
||||
and ensure that `+xkcd-stored-info' has info for every file"
|
||||
(let* ((file-nums (mapcar (lambda (f) (string-to-number (replace-regexp-in-string "\\.json" "" f)))
|
||||
(directory-files xkcd-cache-dir nil "\\.json")))
|
||||
(stored-nums (mapcar #'car +xkcd-stored-info))
|
||||
(new-nums (set-difference file-nums stored-nums)))
|
||||
(dolist (num new-nums)
|
||||
(push (+xkcd-fetch-info num) +xkcd-stored-info))))
|
||||
|
||||
(defadvice! xkcd-get-json--and-cache (url &optional num)
|
||||
"Fetch the Json coming from URL.
|
||||
If the file NUM.json exists, use it instead.
|
||||
If NUM is 0, always download from URL.
|
||||
The return value is a string."
|
||||
:override #'xkcd-get-json
|
||||
(let* ((file (format "%s%d.json" xkcd-cache-dir num))
|
||||
(cached (and (file-exists-p file) (not (eq num 0))))
|
||||
(out (with-current-buffer (if cached
|
||||
(find-file file)
|
||||
(url-retrieve-synchronously url))
|
||||
(goto-char (point-min))
|
||||
(unless cached (re-search-forward "^$"))
|
||||
(prog1
|
||||
(buffer-substring-no-properties (point) (point-max))
|
||||
(kill-buffer (current-buffer))))))
|
||||
(unless (or cached (eq num 0))
|
||||
(xkcd-cache-json num out))
|
||||
out))
|
||||
|
||||
(defconst +xkcd-db--sqlite-available-p
|
||||
(with-demoted-errors "+org-xkcd initialization: %S"
|
||||
(emacsql-sqlite-ensure-binary)
|
||||
t))
|
||||
|
||||
(defvar +xkcd-db--connection (make-hash-table :test #'equal)
|
||||
"Database connection to +org-xkcd database.")
|
||||
|
||||
(defun +xkcd-db--get ()
|
||||
"Return the sqlite db file."
|
||||
(expand-file-name "xkcd.db" xkcd-cache-dir))
|
||||
|
||||
(defun +xkcd-db--get-connection ()
|
||||
"Return the database connection, if any."
|
||||
(gethash (file-truename xkcd-cache-dir)
|
||||
+xkcd-db--connection))
|
||||
|
||||
(defconst +xkcd-db--table-schema
|
||||
'((xkcds
|
||||
[(num integer :unique :primary-key)
|
||||
(year :not-null)
|
||||
(month :not-null)
|
||||
(link :not-null)
|
||||
(news :not-null)
|
||||
(safe_title :not-null)
|
||||
(title :not-null)
|
||||
(transcript :not-null)
|
||||
(alt :not-null)
|
||||
(img :not-null)])))
|
||||
|
||||
(defun +xkcd-db--init (db)
|
||||
"Initialize database DB with the correct schema and user version."
|
||||
(emacsql-with-transaction db
|
||||
(pcase-dolist (`(,table . ,schema) +xkcd-db--table-schema)
|
||||
(emacsql db [:create-table $i1 $S2] table schema))))
|
||||
|
||||
(defun +xkcd-db ()
|
||||
"Entrypoint to the +org-xkcd sqlite database.
|
||||
Initializes and stores the database, and the database connection.
|
||||
Performs a database upgrade when required."
|
||||
(unless (and (+xkcd-db--get-connection)
|
||||
(emacsql-live-p (+xkcd-db--get-connection)))
|
||||
(let* ((db-file (+xkcd-db--get))
|
||||
(init-db (not (file-exists-p db-file))))
|
||||
(make-directory (file-name-directory db-file) t)
|
||||
(let ((conn (emacsql-sqlite db-file)))
|
||||
(set-process-query-on-exit-flag (emacsql-process conn) nil)
|
||||
(puthash (file-truename xkcd-cache-dir)
|
||||
conn
|
||||
+xkcd-db--connection)
|
||||
(when init-db
|
||||
(+xkcd-db--init conn)))))
|
||||
(+xkcd-db--get-connection))
|
||||
|
||||
(defun +xkcd-db-query (sql &rest args)
|
||||
"Run SQL query on +org-xkcd database with ARGS.
|
||||
SQL can be either the emacsql vector representation, or a string."
|
||||
(if (stringp sql)
|
||||
(emacsql (+xkcd-db) (apply #'format sql args))
|
||||
(apply #'emacsql (+xkcd-db) sql args)))
|
||||
|
||||
(defun +xkcd-db-read (num)
|
||||
(when-let ((res
|
||||
(car (+xkcd-db-query [:select * :from xkcds
|
||||
:where (= num $s1)]
|
||||
num
|
||||
:limit 1))))
|
||||
(+xkcd-db-list-to-plist res)))
|
||||
|
||||
(defun +xkcd-db-list-to-plist (xkcd-datalist)
|
||||
`(:num ,(nth 0 xkcd-datalist)
|
||||
:year ,(nth 1 xkcd-datalist)
|
||||
:month ,(nth 2 xkcd-datalist)
|
||||
:link ,(nth 3 xkcd-datalist)
|
||||
:news ,(nth 4 xkcd-datalist)
|
||||
:safe-title ,(nth 5 xkcd-datalist)
|
||||
:title ,(nth 6 xkcd-datalist)
|
||||
:transcript ,(nth 7 xkcd-datalist)
|
||||
:alt ,(nth 8 xkcd-datalist)
|
||||
:img ,(nth 9 xkcd-datalist)))
|
||||
|
||||
(defun +xkcd-db-write (data)
|
||||
(+xkcd-db-query [:insert-into xkcds
|
||||
:values $v1]
|
||||
(list (vector
|
||||
(cdr (assoc 'num data))
|
||||
(cdr (assoc 'year data))
|
||||
(cdr (assoc 'month data))
|
||||
(cdr (assoc 'link data))
|
||||
(cdr (assoc 'news data))
|
||||
(cdr (assoc 'safe_title data))
|
||||
(cdr (assoc 'title data))
|
||||
(cdr (assoc 'transcript data))
|
||||
(cdr (assoc 'alt data))
|
||||
(cdr (assoc 'img data))
|
||||
)))))
|
||||
(format "xkcd:%d" (+xkcd-select))))
|
||||
#+END_SRC
|
||||
***** YouTube
|
||||
The ~[[yt:...]]~ links preview nicely, but don't export nicely. Thankfully, we can
|
||||
|
|
Loading…
Reference in a new issue