gemini-mode/gemini-mode.el
2020-06-09 14:29:01 -04:00

145 lines
5.1 KiB
EmacsLisp

;;; gemini-mode.el --- A simple highlighting package for text/gemini -*- lexical-binding: t; -*-
;; Copyright (C) 2020 Jason McBrayer
;; Author: Jason McBrayer <jmcbray@carcosa.net>, tastytea <tastytea@tastytea.de>
;; Created: 20 May 2020
;; Version: 0.4.0
;; Keywords: languages
;; Homepage: https://git.carcosa.net/jmcbray/gemini.el
;; Package-Requires: ((emacs "24.3"))
;;; Commentary:
;; This package provides a major mode for editing text/gemini files.
;; Currently, it only provides syntax-highlighting support.
;; This file is not part of GNU Emacs.
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero 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 Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Code:
(require 'cl-lib)
(defface gemini-heading-face-1
'((t :inherit bold :height 1.8))
"Face for Gemini headings level 1"
:group 'gemini-mode)
(defface gemini-heading-face-2
'((t :inherit bold :height 1.4))
"Face for Gemini headings level 2"
:group 'gemini-mode)
(defface gemini-heading-face-3
'((t :inherit bold :height 1.2))
"Face for Gemini headings level 3"
:group 'gemini-mode)
(defface gemini-heading-face-rest
'((t :inherit bold))
"Face for Gemini headings below level 3"
:group 'gemini-mode)
(defvar gemini-highlights
(let* ((gemini-heading-3-regexp "^###\s.*$")
(gemini-heading-2-regexp "^##\s.*$")
(gemini-heading-1-regexp "^#\s.*$")
(gemini-heading-rest-regexp "^###+\s.*$")
(gemini-link-regexp "^=>.*$")
(gemini-ulist-regexp "^\\*")
(gemini-preformatted-regexp "^```"))
`((,gemini-heading-rest-regexp . 'gemini-heading-face-rest)
(,gemini-heading-3-regexp . 'gemini-heading-face-3)
(,gemini-heading-2-regexp . 'gemini-heading-face-2)
(,gemini-heading-1-regexp . 'gemini-heading-face-1)
(,gemini-link-regexp . 'link)
(,gemini-ulist-regexp . 'font-lock-keyword-face)
(,gemini-preformatted-regexp . 'font-lock-builtin-face)))
"Font lock keywords for `gemini-mode'.")
(defvar gemini-mode-map
(let ((map (make-keymap)))
(define-key map (kbd "C-c C-l") #'gemini-insert-link)
(define-key map (kbd "C-c RET") #'gemini-insert-list-item)
map)
"Keymap for `gemini-mode'.")
;; See RFC 3986 (URI).
(defconst gemini-regex-uri
"\\([a-zA-z0-9+-.]+:[^]\t\n\r<>,;() ]+\\)"
"Regular expression for matching URIs.")
(defun gemini-get-used-uris ()
"Return a list of all used URIs in the buffer."
(save-excursion
(goto-char (point-min))
(let (uris)
(while (re-search-forward gemini-regex-uri nil t)
(push (match-string 1) uris))
uris)))
(defun gemini-insert-link ()
"Insert new link, with interactive prompts.
If there is an active region, use the text as the default URL, if
it seems to be a URL, or link text value otherwise."
(interactive)
(cl-multiple-value-bind (begin end text uri)
(if (use-region-p)
;; Use region as either link text or URL as appropriate.
(let ((region (buffer-substring-no-properties
(region-beginning) (region-end))))
(if (string-match gemini-regex-uri region)
;; Region contains a URL; use it as such.
(list (region-beginning) (region-end)
nil (match-string 1 region))
;; Region doesn't contain a URL, so use it as text.
(list (region-beginning) (region-end)
region nil))))
(let* ((used-uris (gemini-get-used-uris))
(uri (completing-read "URL: "
used-uris nil nil uri))
(text (completing-read "Link text (blank for plain URL): "
nil nil nil text)))
(when (and begin end)
(delete-region begin end))
(insert "=> " uri)
(unless (string= text "")
(insert " " text)))))
(defun gemini-insert-list-item ()
"Insert a new list item.
If at the beginning of a line, just insert it. Otherwise
go to the end of the current line, insert a newline, and
insert a list item."
(interactive)
(if (equal (line-beginning-position) (point))
(insert "* ")
(end-of-line)
(newline)
(insert "* ")))
;;;###autoload
(define-derived-mode gemini-mode text-mode "gemini"
"Major mode for editing text/gemini 'geminimap' documents"
(setq font-lock-defaults '(gemini-highlights))
(visual-line-mode 1))
;;;###autoload
(progn
(add-to-list 'auto-mode-alist '("\\.gmi\\'" . gemini-mode))
(add-to-list 'auto-mode-alist '("\\.gemini\\'" . gemini-mode))
(add-to-list 'auto-mode-alist '("\\.geminimap\\'" . gemini-mode)))
(provide 'gemini-mode)
;;; gemini-mode.el ends here