├── README.md └── typo.el /README.md: -------------------------------------------------------------------------------- 1 | # Emacs mode for typographical editing 2 | 3 | typo.el includes two modes, `typo-mode` and `typo-global-mode`. 4 | 5 | `typo-mode` is a buffer-specific minor mode that will change a number 6 | of normal keys to make them insert typographically useful unicode 7 | characters. Some of those keys can be used repeatedly to cycle through 8 | variations. This includes in particular quotation marks and dashes. 9 | 10 | `typo-global-mode` introduces a global minor mode which adds the 11 | `C-c 8` prefix to complement Emacs’ default `C-x 8` prefix map. 12 | 13 | See the documentation of `typo-mode` and `typo-global-mode` for 14 | further details. 15 | 16 | ## Quotation Marks 17 | 18 | > “He said, ‘leave me alone,’ and closed the door.” 19 | 20 | All quotation marks in this sentence were added by hitting the " key 21 | exactly once each. typo.el guessed the correct glyphs to use from 22 | context. If it gets it wrong, you can just repeat hitting the " key 23 | until you get the quotation mark you wanted. 24 | 25 | `M-x typo-change-language` lets you change which quotation marks to 26 | use in a single buffer. To change globally, add 27 | `(setq-default typo-language )` to your initialization 28 | files. This is also configurable, in case you want to add your own. 29 | 30 | ## Dashes and Dots 31 | 32 | The hyphen key will insert a default hyphen-minus glyph. On repeated 33 | use, though, it will cycle through the en-dash, em-dash, and a number 34 | of other dash-like glyphs available in Unicode. This means that typing 35 | two dashes inserts an en-dash and typing three dashes inserts an 36 | em-dash, as would be expected. The name of the currently inserted dash 37 | is shown in the minibuffer. 38 | 39 | The full stop key will self-insert as usual. When three dots are 40 | inserted in a row, though, they are replaced by a horizontal ellipsis 41 | glyph. 42 | 43 | ## Other Keys 44 | 45 | Tick and backtick keys insert the appropriate quotation mark as well. 46 | The less-than and greater-than signs cycle insert the default glyphs 47 | on first use, but cycle through double and single guillemets on 48 | repeated use. 49 | 50 | ## Prefix Map 51 | 52 | In addition to the above, typo-global-mode also provides a 53 | globally-accessible key map under the `C-c 8` prefix (akin to Emacs’ 54 | default `C-x 8` prefix map) to insert various Unicode characters. 55 | 56 | In particular, `C-c 8 SPC` will insert a no-break space. Continued use 57 | of SPC after this will cycle through half a dozen different space 58 | types available in Unicode. 59 | 60 | Check the mode’s documentation for more details. 61 | 62 | ## Download and Installation 63 | 64 | Download `typo.el` and put it somewhere in your load-path. 65 | 66 | Add the following to your .emacs: 67 | 68 | ```Lisp 69 | (typo-global-mode 1) 70 | (add-hook 'text-mode-hook 'typo-mode) 71 | ``` 72 | 73 | ## Ligatures 74 | 75 | Unicode supports ligatures (ff, fi, fl, ffi, ffl). This is nice, but 76 | quite a lot of fonts lack support for this. Also, it could be argued 77 | that ligatures should happen as part of the display process, not in 78 | the document. Use ZERO WIDTH NON-JOINER (C-c 8 SPC SPC SPC) to prevent 79 | two characters from being merged like this. 80 | 81 | Until fonts widely support ligatures, typo.el will not support 82 | them. 83 | -------------------------------------------------------------------------------- /typo.el: -------------------------------------------------------------------------------- 1 | ;;; typo.el --- Minor mode for typographic editing 2 | 3 | ;; Copyright (C) 2012 Jorgen Schaefer 4 | 5 | ;; Version: 1.1 6 | ;; Author: Jorgen Schaefer 7 | ;; URL: https://github.com/jorgenschaefer/typoel 8 | ;; Created: 6 Feb 2012 9 | ;; Keywords: convenience, wp 10 | 11 | ;; This program is free software; you can redistribute it and/or 12 | ;; modify it under the terms of the GNU General Public License 13 | ;; as published by the Free Software Foundation; either version 3 14 | ;; of the License, or (at your option) any later version. 15 | 16 | ;; This program is distributed in the hope that it will be useful, 17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | ;; GNU General Public License for more details. 20 | 21 | ;; You should have received a copy of the GNU General Public License 22 | ;; along with this program; if not, write to the Free Software 23 | ;; Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 24 | ;; 02110-1301 USA 25 | 26 | ;;; Commentary: 27 | 28 | ;; typo.el includes two modes, `typo-mode` and `typo-global-mode`. 29 | ;; 30 | ;; `typo-mode` is a buffer-specific minor mode that will change a number 31 | ;; of normal keys to make them insert typographically useful unicode 32 | ;; characters. Some of those keys can be used repeatedly to cycle through 33 | ;; variations. This includes in particular quotation marks and dashes. 34 | ;; 35 | ;; `typo-global-mode` introduces a global minor mode which adds the 36 | ;; `C-c 8` prefix to complement Emacs’ default `C-x 8` prefix map. 37 | ;; 38 | ;; See the documentation of `typo-mode` and `typo-global-mode` for 39 | ;; further details. 40 | ;; 41 | ;; ## Quotation Marks 42 | ;; 43 | ;; > “He said, ‘leave me alone,’ and closed the door.” 44 | ;; 45 | ;; All quotation marks in this sentence were added by hitting the " key 46 | ;; exactly once each. typo.el guessed the correct glyphs to use from 47 | ;; context. If it gets it wrong, you can just repeat hitting the " key 48 | ;; until you get the quotation mark you wanted. 49 | ;; 50 | ;; `M-x typo-change-language` lets you change which quotation marks to 51 | ;; use. This is also configurable, in case you want to add your own. 52 | ;; 53 | ;; ## Dashes and Dots 54 | ;; 55 | ;; The hyphen key will insert a default hyphen-minus glyph. On repeated 56 | ;; use, though, it will cycle through the en-dash, em-dash, and a number 57 | ;; of other dash-like glyphs available in Unicode. This means that typing 58 | ;; two dashes inserts an en-dash and typing three dashes inserts an 59 | ;; em-dash, as would be expected. The name of the currently inserted dash 60 | ;; is shown in the minibuffer. 61 | ;; 62 | ;; The full stop key will self-insert as usual. When three dots are 63 | ;; inserted in a row, though, they are replaced by a horizontal ellipsis 64 | ;; glyph. 65 | ;; 66 | ;; ## Other Keys 67 | ;; 68 | ;; Tick and backtick keys insert the appropriate quotation mark as well. 69 | ;; The less-than and greater-than signs cycle insert the default glyphs 70 | ;; on first use, but cycle through double and single guillemets on 71 | ;; repeated use. 72 | ;; 73 | ;; ## Prefix Map 74 | ;; 75 | ;; In addition to the above, typo-global-mode also provides a 76 | ;; globally-accessible key map under the `C-c 8` prefix (akin to Emacs’ 77 | ;; default `C-x 8` prefix map) to insert various Unicode characters. 78 | ;; 79 | ;; In particular, `C-c 8 SPC` will insert a no-break space. Continued use 80 | ;; of SPC after this will cycle through half a dozen different space 81 | ;; types available in Unicode. 82 | ;; 83 | ;; Check the mode’s documentation for more details. 84 | 85 | ;;; Code: 86 | 87 | ;; For some reason, Emacs default has these as parentheses. This is 88 | ;; completely confusing when mixing this with normal parentheses, 89 | ;; and gets e.g. the following code wrong, even. Punctuation syntax 90 | ;; results in much more intuitive behavior. 91 | (modify-syntax-entry ?» ".") 92 | (modify-syntax-entry ?« ".") 93 | ;; Sorry for the intrusion. 94 | 95 | (defgroup typo nil 96 | "*Typography mode for Emacs" 97 | :prefix "typo-" 98 | :group 'convenience) 99 | 100 | (defcustom typo-quotation-marks 101 | '(("Czech" "„" "“" "‚" "‘") 102 | ("Czech (Guillemets)" "»" "«" "›" "‹") 103 | ("English" "“" "”" "‘" "’") 104 | ("German" "„" "“" "‚" "‘") 105 | ("German (Guillemets)" "»" "«" "›" "‹") 106 | ("French" "«" "»" "‹" "›") 107 | ("Finnish" "”" "”" "’" "’") 108 | ("Finnish (Guillemets)" "»" "»" "›" "›") 109 | ("Swedish" "”" "”" "’" "’") 110 | ("Russian" "«" "»" "„" "“") 111 | ("Italian" "«" "»" "“" "”") 112 | ("Polish" "„" "”" "‚" "’") 113 | ("Serbian" "„" "”" "’" "’") 114 | ("Ukrainian" "«" "»" "„" "“")) 115 | "*Quotation marks per language." 116 | :type '(repeat (list (string :tag "Language") 117 | (string :tag "Double Opening Quotation Mark") 118 | (string :tag "Double Closing Quotation Mark") 119 | (string :tag "Single Opening Quotation Mark") 120 | (string :tag "Single Closing Quotation Mark"))) 121 | :group 'typo) 122 | 123 | 124 | (defcustom typo-language "English" 125 | "*The default language typo-mode should use." 126 | :type '(string :tag "Default Language") 127 | :group 'typo) 128 | (make-variable-buffer-local 'typo-language) 129 | (put 'typo-language 'safe-local-variable 'stringp) 130 | 131 | (defcustom typo-disable-electricity-functions '(typo-in-xml-tag) 132 | "*A list of functions to call before an electric key binding is 133 | used. If one of the functions returns non-nil, the key 134 | self-inserts. 135 | 136 | This can be used to disable the electric keys in e.g. XML tags." 137 | :type 'hook 138 | :options '(typo-in-xml-tag) 139 | :group 'typo) 140 | 141 | (defvar typo-mode-map 142 | (let ((map (make-sparse-keymap))) 143 | (define-key map (kbd "\"") 'typo-insert-quotation-mark) 144 | (define-key map (kbd "'") 'typo-cycle-right-single-quotation-mark) 145 | (define-key map (kbd "`") 'typo-cycle-left-single-quotation-mark) 146 | (define-key map (kbd "-") 'typo-cycle-dashes) 147 | (define-key map (kbd ".") 'typo-cycle-ellipsis) 148 | (define-key map (kbd "<") 'typo-cycle-left-angle-brackets) 149 | (define-key map (kbd ">") 'typo-cycle-right-angle-brackets) 150 | map) 151 | "The keymap for `typo-mode'.") 152 | 153 | (defvar typo-global-mode-map 154 | (let ((gmap (make-sparse-keymap)) 155 | (map (make-sparse-keymap))) 156 | (define-key gmap (kbd "C-c 8") map) 157 | (define-key map (kbd "\"") 'typo-insert-quotation-mark) 158 | (define-key map (kbd "'") 'typo-cycle-right-single-quotation-mark) 159 | (define-key map (kbd "`") 'typo-cycle-left-single-quotation-mark) 160 | (define-key map (kbd "--") 'typo-cycle-dashes) 161 | (define-key map (kbd ".") 'typo-cycle-ellipsis) 162 | (define-key map (kbd "<<") 'typo-cycle-left-angle-brackets) 163 | (define-key map (kbd ">>") 'typo-cycle-right-angle-brackets) 164 | (define-key map (kbd "*") 'typo-cycle-multiplication-signs) 165 | (define-key map (kbd "SPC") 'typo-cycle-spaces) 166 | (define-key map (kbd "?") 'typo-cycle-question-mark) 167 | (define-key map (kbd "!") 'typo-cycle-exclamation-mark) 168 | (define-key map (kbd "/=") "≠") 169 | (define-key map (kbd "//") "÷") 170 | (define-key map (kbd ">=") "≥") 171 | (define-key map (kbd "<=") "≤") 172 | (define-key map (kbd "=<") "⇐") 173 | (define-key map (kbd "=>") "⇒") 174 | (define-key map (kbd "<-") "←") 175 | (define-key map (kbd "-<") "←") 176 | (define-key map (kbd "->") "→") 177 | (define-key map (kbd "-^") "↑") 178 | (define-key map (kbd "=^") "⇑") 179 | (define-key map (kbd "-v") "↓") 180 | (define-key map (kbd "=v") "⇓") 181 | (define-key map (kbd "T") "™") 182 | gmap) 183 | "The keymap for `typo-global-mode'.") 184 | 185 | ;;;###autoload 186 | (define-minor-mode typo-mode 187 | "Minor mode for typographic editing. 188 | 189 | This mode changes some default keybindings to enter typographic 190 | glyphs. In particular, this changes how quotation marks, the 191 | dash, the dot, and the angle brackets work. 192 | 193 | Most keys will cycle through various options when used 194 | repeatedly. 195 | 196 | \\{typo-mode-map}" 197 | :group 'typo 198 | :lighter " Typo" 199 | :keymap typo-mode-map) 200 | 201 | ;;;###autoload 202 | (define-minor-mode typo-global-mode 203 | "Minor mode for typographic editing. 204 | 205 | This mode provides a prefix map under C-c 8 which complements the 206 | default C-x 8 prefix map. 207 | 208 | \\{typo-global-mode-map}" 209 | :group 'typo 210 | :global t 211 | :keymap typo-global-mode-map) 212 | 213 | (defun typo-change-language (language) 214 | "Change the current language used for quotation marks." 215 | (interactive (list (completing-read 216 | "Quotation marks: " 217 | typo-quotation-marks 218 | ))) 219 | (when (not (assoc-string language typo-quotation-marks)) 220 | (error "Unknown language %s (see `typo-quotation-marks')" language)) 221 | (setq typo-language language)) 222 | 223 | (defun typo-open-double-quotation-mark () 224 | "Return the opening double quotation marks for the current language." 225 | (nth 1 (assoc-string typo-language typo-quotation-marks))) 226 | 227 | (defun typo-close-double-quotation-mark () 228 | "Return the closing double quotation marks for the current language." 229 | (nth 2 (assoc-string typo-language typo-quotation-marks))) 230 | 231 | (defun typo-open-single-quotation-mark () 232 | "Return the opening single quotation marks for the current language." 233 | (nth 3 (assoc-string typo-language typo-quotation-marks))) 234 | 235 | (defun typo-close-single-quotation-mark () 236 | "Return the closing single quotation marks for the current language." 237 | (nth 4 (assoc-string typo-language typo-quotation-marks))) 238 | 239 | (defun typo-in-xml-tag () 240 | "Return non-nil if point is inside an XML tag." 241 | (save-excursion 242 | (and (re-search-backward "[<>]" 243 | ;; If you have an XML tag that spans more 244 | ;; than 25 lines, you should be shot. 245 | (max (point-min) 246 | (- (point) 247 | (* 80 25))) 248 | t) 249 | ;; < without a word char is a math formula 250 | (looking-at "<\\w")))) 251 | 252 | (defun typo-electricity-disabled-p () 253 | "Return non-nil if electricity is disabled at point. 254 | 255 | See `typo-disable-electricity-functions'." 256 | ;; Only if this happened from a non-prefix variable 257 | (and (= (length (this-single-command-keys)) 1) 258 | (run-hook-with-args-until-success 'typo-disable-electricity-functions))) 259 | 260 | (defun typo-quotation-needs-closing (open close) 261 | "Return non-nil if the last occurrence of either OPEN and CLOSE 262 | in the current buffer is OPEN, i.e. if this pair still needs 263 | closing. 264 | 265 | This does not support nested, equal quotation marks." 266 | (save-excursion 267 | (if (re-search-backward (regexp-opt (list open close)) 268 | nil t) 269 | (equal open (match-string 0)) 270 | nil))) 271 | 272 | (defun typo-insert-quotation-mark (arg) 273 | "Insert quotation marks. 274 | 275 | This command tries to be intelligent. Opening quotation marks are 276 | closed. If you repeat the command after a quotation mark, that 277 | mark is cycled through various variants. 278 | 279 | After a closing double quotation mark, the next variant is an 280 | opening single quotation mark. So when this command is issued 281 | inside a quotation, it will first close the quotation. On the 282 | second time, it will open an inner quotation. 283 | 284 | After an opening double quotation mark, the next variant is the 285 | typewriter quotation mark, making it possible in the usual case 286 | to simple issue this command twice to get a typewriter quotation 287 | mark (use C-q \" or C-o \" to force inserting one). 288 | 289 | If used with a numeric prefix argument, only typewriter quotation 290 | marks will be inserted." 291 | (interactive "P") 292 | (if (or (typo-electricity-disabled-p) arg) 293 | (call-interactively 'self-insert-command) 294 | (let* ((double-open (typo-open-double-quotation-mark)) 295 | (double-close (typo-close-double-quotation-mark)) 296 | (double-needs-closing (typo-quotation-needs-closing 297 | double-open double-close)) 298 | 299 | (single-open (typo-open-single-quotation-mark)) 300 | (single-close (typo-close-single-quotation-mark)) 301 | (single-needs-closing (typo-quotation-needs-closing 302 | single-open single-close)) 303 | 304 | (after-any-opening (looking-back (regexp-opt (list double-open 305 | single-open))))) 306 | (cond 307 | ;; For languages that use the same symbol for opening and 308 | ;; closing (Finnish, Swedish...), the simplest thing to do is to 309 | ;; not try to be too smart and just cycle ” and " 310 | ((equal double-open double-close) 311 | (typo-insert-cycle (list double-open "\""))) 312 | ;; Inside a single quotation, if we're not directly at the 313 | ;; opening one, we close it. 314 | ((and single-needs-closing 315 | (not after-any-opening)) 316 | (insert single-close)) 317 | ;; Inside a double quotation, if we're not directly at the 318 | ;; opening one ... 319 | ((and double-needs-closing 320 | (not after-any-opening)) 321 | ;; ... if we are after a space, we open an inner quotation. 322 | ;; 323 | ;; (This misses the situation where we start a quotation with an 324 | ;; inner quotation, but that's indistinguishable from cycling 325 | ;; through keys, and the latter is more common.) 326 | (if (looking-back "\\s-") 327 | (insert single-open) 328 | ;; Otherwise, close the double one 329 | (insert double-close))) 330 | ;; Nothing is open, or we are directly at an opening quote. If 331 | ;; this is a repetition of a this command, start cycling. 332 | ((eq this-command last-command) 333 | (delete-char -1) 334 | (typo-insert-cycle (list "\"" 335 | double-open double-close 336 | single-open single-close))) 337 | ;; Otherwise, just open a double quotation mark. 338 | ;; 339 | ;; This can actually happen if we open a quotation, then move 340 | ;; point, then go back to directly after the quotation, and then 341 | ;; call this again. Opening another double quotation there is 342 | ;; weird, but I'm not sure what else to do then, either. 343 | (t 344 | (insert double-open)))))) 345 | 346 | (defun typo-cycle-ellipsis (arg) 347 | "Add periods. The third period will add an ellipsis. 348 | 349 | If used with a numeric prefix argument N, N periods will be inserted." 350 | (interactive "P") 351 | (if (or (typo-electricity-disabled-p) arg) 352 | (call-interactively 'self-insert-command) 353 | (if (looking-back "\\.\\.") 354 | (replace-match "…") 355 | (call-interactively 'self-insert-command)))) 356 | 357 | (defmacro define-typo-cycle (name docstring cycle) 358 | "Define a typo command that cycles through various options. 359 | 360 | If used with a numeric prefix argument N, N standard characters will be 361 | inserted instead of cycling. 362 | 363 | NAME is the name of the command to define. 364 | DOCSTRING is the docstring for that command. 365 | 366 | CYCLE is a list of strings to cycle through." 367 | (declare (indent 1) (doc-string 2)) 368 | `(defun ,name (arg) 369 | ,docstring 370 | (interactive "P") 371 | (if (or (typo-electricity-disabled-p) arg) 372 | (call-interactively 'self-insert-command) 373 | (typo-insert-cycle ',cycle)))) 374 | 375 | ;; This event cycling loop is from `kmacro-call-macro' 376 | (defun typo-insert-cycle (cycle) 377 | "Insert the strings in CYCLE" 378 | (let ((i 0) 379 | (repeat-key last-input-event) 380 | repeat-key-str) 381 | (insert (nth i cycle)) 382 | (setq repeat-key-str (format-kbd-macro (vector repeat-key) nil)) 383 | (while repeat-key 384 | (message "(Inserted %s; type %s for other options)" 385 | (typo-char-name (nth i cycle)) 386 | repeat-key-str) 387 | (if (equal repeat-key (read-event)) 388 | (progn 389 | (clear-this-command-keys t) 390 | (delete-char (- (length (nth i cycle)))) 391 | (setq i (% (+ i 1) 392 | (length cycle))) 393 | (insert (nth i cycle)) 394 | (setq last-input-event nil)) 395 | (setq repeat-key nil))) 396 | (when last-input-event 397 | (clear-this-command-keys t) 398 | (setq unread-command-events (list last-input-event))))) 399 | 400 | (defun typo-char-name (string) 401 | "Return the Unicode name of the first char in STRING." 402 | (let ((char-code (elt string 0)) 403 | name) 404 | (setq name (get-char-code-property char-code 'name)) 405 | (when (or (not name) 406 | (= ?< (elt name 0))) 407 | (setq name (get-char-code-property char-code 'old-name))) 408 | name)) 409 | 410 | (define-typo-cycle typo-cycle-right-single-quotation-mark 411 | "Cycle through the right quotation mark and the typewriter apostrophe. 412 | 413 | If used with a numeric prefix argument N, N typewriter apostrophes 414 | will be inserted." 415 | ("’" "'")) 416 | 417 | (define-typo-cycle typo-cycle-left-single-quotation-mark 418 | "Cycle through the left single quotation mark and the backtick. 419 | 420 | If used with a numeric prefix argument N, N backticks will be inserted." 421 | ("‘" "`")) 422 | 423 | (define-typo-cycle typo-cycle-dashes 424 | "Cycle through various dashes." 425 | ("-" ; HYPHEN-MINUS 426 | "–" ; EN DASH 427 | "—" ; EM DASH 428 | "−" ; MINUS SIGN 429 | "‐" ; HYPHEN 430 | "‑" ; NON-BREAKING HYPHEN 431 | )) 432 | 433 | (define-typo-cycle typo-cycle-left-angle-brackets 434 | "Cycle through the less-than sign and guillemet quotation marks. 435 | 436 | If used with a numeric prefix argument N, N less-than signs will be inserted." 437 | ("<" "«" "‹")) 438 | 439 | (define-typo-cycle typo-cycle-right-angle-brackets 440 | "Cycle through the greater-than sign and guillemet quotation marks. 441 | 442 | If used with a numeric prefix argument N, N greater-than signs will be inserted." 443 | (">" "»" "›")) 444 | 445 | (define-typo-cycle typo-cycle-multiplication-signs 446 | "Cycle through the asterisk and various multiplication signs" 447 | ("×" "·")) 448 | 449 | (define-typo-cycle typo-cycle-spaces 450 | "Cycle through various spaces" 451 | (" " ; NO-BREAK SPACE 452 | " " ; THIN SPACE 453 | "‌" ; ZERO WIDTH NON-JOINER 454 | "‍" ; ZERO WIDTH JOINER 455 | " " ; MEDIUM MATHEMATICAL SPACE 456 | " " ; HAIR SPACE 457 | ;; " " ; EM SPACE 458 | ;; " " ; EN SPACE 459 | " " ; SPACE 460 | )) 461 | 462 | (define-typo-cycle typo-cycle-question-mark 463 | "Cycle through various interrogatory marks." 464 | ("?" "¿" "‽" "⸘" "⸮")) 465 | 466 | (define-typo-cycle typo-cycle-exclamation-mark 467 | "Cycle through various exclamatory marks." 468 | ("!" "¡" "‽" "⸘")) 469 | 470 | (provide 'typo) 471 | ;;; typo.el ends here 472 | --------------------------------------------------------------------------------