├── .gitignore ├── LICENSE ├── README.md ├── changelog.org ├── img ├── browser.png └── reader.png └── rfc-mode.el /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | /rfc-mode-pkg.el 3 | /rfc-mode-autoloads.el 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2023 Nicolas Martyanoff . 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 9 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![MELPA Stable](https://stable.melpa.org/packages/rfc-mode-badge.svg)](https://stable.melpa.org/#/rfc-mode) 3 | [![MELPA](https://melpa.org/packages/rfc-mode-badge.svg)](https://melpa.org/#/rfc-mode) 4 | 5 | # rfc-mode 6 | 7 | ## Introduction 8 | 9 | The rfc-mode Emacs major mode is a browser and reader for RFC documents. 10 | 11 | ## Installation 12 | 13 | The package should be installed from MELPA. 14 | 15 | Start by loading the mode: 16 | 17 | ```elisp 18 | (require 'rfc-mode) 19 | ``` 20 | 21 | Then set the location containing all RFC documents (the default value is the 22 | `rfc` directory in the home directory): 23 | 24 | ```elisp 25 | (setq rfc-mode-directory (expand-file-name "~/rfc/")) 26 | ``` 27 | 28 | RFC documents and their index will be directly downloaded from 29 | https://www.rfc-editor.org when required. Alternatively, the entire RFC 30 | collection can be downloadeded from https://www.rfc-editor.org/retrieve/bulk 31 | to ensure full access without the need for an internet connection. 32 | 33 | Call `rfc-mode-browse` to choose a RFC document to read, or `rfc-mode-read` to 34 | enter the reference of the RFC document yourself. 35 | 36 | ## Screenshots 37 | ### Browser 38 | ![Helm-based browser](img/browser.png) 39 | 40 | ### Reader 41 | ![Reader](img/reader.png) 42 | 43 | ## Contact 44 | If you have an idea or a question, email me at . 45 | -------------------------------------------------------------------------------- /changelog.org: -------------------------------------------------------------------------------- 1 | #+TITLE: rfc-mode changelog 2 | 3 | * Next Version 4 | /Work in progress./ 5 | 6 | * 1.4.2 7 | ** Bugs 8 | - Fix footer highlighting. 9 | 10 | * 1.4.1 11 | ** Bugs 12 | - Fix page-based navigation (thanks to Matthew Woodcraft for reporting the 13 | issue) on Emacs 29. 14 | - Fix major-mode auto-detection for rfc-mode buffers. 15 | 16 | * 1.4.0 17 | I should have released minor versions for bug fixes, but it seems everyone is 18 | using the =master= branch from MELPA, and so am I. So major only it is. I will 19 | release a minor version if a severe issue is found of course. 20 | 21 | ** Features 22 | - ~rfc-mode-read~ now supports a numeric prefix argument, so you can simply 23 | type ~C-u 1 2 3 4 M-x rfc-mode-read~ to read RFC number 1234. Also, if you 24 | call ~rfc-mode-read~ when point is on a number, you will be offered that 25 | number as default. Thanks to Daniel Martín. 26 | - ~rfc-mode-goto-section~ now makes sure the title of the section is at the 27 | top of the page, avoiding annoying edge cases where a section is at the end 28 | of a page (thanks to Štěpán Němec). 29 | 30 | ** Misc 31 | - Make the dependency on Helm optional (thanks to Jonas Bernoulli). 32 | - Code cleaning and simplifications (thanks to Stefan Monnier, Jonas Bernoulli 33 | and Basil L. Contovounesios). 34 | 35 | * 1.3.0 36 | This release improves navigation and introduce section detection, thanks to 37 | Daniel Martín. 38 | 39 | ** Features 40 | - ~imenu~ integration. If you use a graphical user interface, the menu 41 | bar will show a new menu, "RFC Contents", with links to the 42 | different parts of an RFC document. 43 | - RFC links can now be navigated using the mouse, or by pressing 44 | ~~/~~. 45 | - Pressing ~g~ in a ~rfc-mode~ buffer lets you navigate to an RFC 46 | section by name. 47 | - You can navigate to previous and next RFC sections by pressing ~p~ and 48 | ~n~, respectively. 49 | 50 | ** Misc 51 | - Derive ~rfc-mode~ from ~special-mode~. 52 | - Make ~rfc-mode-read~ display the document in a separate window, 53 | without switching buffers. This follows the typical Emacs convention 54 | for displaying help buffers, like ~help-mode~ or ~man-mode~ follow. 55 | 56 | * 1.2.0 57 | This new release is driven by suggestions from Stefan Monnier and some issues 58 | which were open on Github. Thanks everyone! 59 | 60 | ** Features 61 | - Let the module load without Helm since some features can be used without it. 62 | - Auto load ~rfc-mode-read~. 63 | - Improved accuracy for section title detection. 64 | - Offer the possibility to keep original buffer names with 65 | ~rfc-mode-use-original-buffer-names~. 66 | 67 | ** Fixes 68 | - Compute the index path dynamically so that ~rfc-mode-directory~ can be 69 | modified after the module has been loaded. 70 | 71 | ** Misc 72 | - Follow Emacs conventions in docstrings. 73 | - Derive ~rfc-mode~ from ~text-mode~. 74 | - Use ~expand-file-name~ instead of ~concat~. 75 | 76 | * 1.1.1 77 | ** Features 78 | - Automatically download missing files. 79 | 80 | * 1.1.0 81 | ** Fixes 82 | - Require missing modules. 83 | - Fix invalid escape sequence. 84 | 85 | ** Misc 86 | - Make some functions private. 87 | 88 | * 1.0.1 89 | *** Fixes 90 | - Various fixes for checkdoc. 91 | 92 | * 1.0.0 93 | First public version. 94 | 95 | ** Features 96 | - Helm-based RFC browser. 97 | - RFC document reader with highlighting. 98 | -------------------------------------------------------------------------------- /img/browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galdor/rfc-mode/ab09db78d9d1baa4da4f926930833598e1e978ce/img/browser.png -------------------------------------------------------------------------------- /img/reader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galdor/rfc-mode/ab09db78d9d1baa4da4f926930833598e1e978ce/img/reader.png -------------------------------------------------------------------------------- /rfc-mode.el: -------------------------------------------------------------------------------- 1 | ;;; rfc-mode.el --- RFC document browser and viewer -*- lexical-binding: t -*- 2 | 3 | ;; Author: Nicolas Martyanoff 4 | ;; SPDX-License-Identifier: ISC 5 | ;; URL: https://github.com/galdor/rfc-mode 6 | ;; Version: 1.4.2 7 | ;; Package-Requires: ((emacs "25.1")) 8 | 9 | ;;; Commentary: 10 | 11 | ;; This package makes it easy to browse and read RFC documents. 12 | 13 | ;; It offers a Helm-based browser of the list of RFCs as well as 14 | ;; some highlighting of hyperlinks when reading the actual RFCs. 15 | ;; If you want to browse the list without Helm, you might prefer 16 | ;; [rfcview](http://github.com/zeph1e/rfcview.el). 17 | 18 | ;; Todo: 19 | ;; - Use font-lock 20 | ;; - Add hyperlinks from the `Table of Contents' 21 | 22 | ;;; Code: 23 | 24 | (require 'helm nil t) 25 | (require 'seq) 26 | 27 | 28 | (declare-function helm-build-sync-source "helm-source") 29 | (declare-function helm-make-actions "helm-lib") 30 | 31 | ;;; Configuration: 32 | 33 | (defgroup rfc-mode-group nil 34 | "Tools to browse and read RFC documents." 35 | :prefix "rfc-mode-" 36 | :link '(url-link :tag "GitHub" "https://github.com/galdor/rfc-mode") 37 | :group 'external) 38 | 39 | (defface rfc-mode-document-header-face 40 | '((t :inherit font-lock-comment-face)) 41 | "Face used for RFC document page headers.") 42 | 43 | (defface rfc-mode-document-footer-face 44 | '((t :inherit font-lock-comment-face)) 45 | "Face used for RFC document page footers.") 46 | 47 | (defface rfc-mode-document-section-title-face 48 | '((t :inherit font-lock-keyword-face)) 49 | "Face used for RFC document section titles.") 50 | 51 | (defface rfc-mode-browser-ref-face 52 | '((t :inherit font-lock-preprocessor-face)) 53 | "Face used to highlight RFC references in the RFC browser.") 54 | 55 | (defface rfc-mode-browser-title-face 56 | '((t :inherit default)) 57 | "Face used to highlight the title of RFC documents in the RFC browser.") 58 | 59 | (defface rfc-mode-browser-title-obsolete-face 60 | '((t :inherit font-lock-comment-face)) 61 | "Face used to highlight the title of obsolete RFC documents in the RFC browser.") 62 | 63 | (defface rfc-mode-browser-status-face 64 | '((t :inherit font-lock-keyword-face)) 65 | "Face used to highlight RFC document statuses in the RFC browser.") 66 | 67 | (defcustom rfc-mode-directory (expand-file-name "~/rfc/") 68 | "The directory where RFC documents are stored." 69 | :type 'directory) 70 | 71 | (defcustom rfc-mode-document-url 72 | "https://www.rfc-editor.org/rfc/rfc%s.txt" 73 | "A `format'able URL for fetching arbitrary RFC documents. 74 | Assume RFC documents are named as e.g. rfc21.txt, rfc-index.txt." 75 | :type 'string) 76 | 77 | (defcustom rfc-mode-browse-input-function 78 | (if (featurep 'helm) 'helm 'completing-read) 79 | "Function used by `rfc-mode-browse' to read user input. 80 | 81 | Only `read-number', `completing-read' and `helm' are explicitly 82 | supported. Any other function is called with no arguments and 83 | must return an integer. 84 | 85 | Here `completion-read' works best if you use some completion 86 | mode that displays candidates \"vertically\" like `helm' does. 87 | `ivy-mode' is a popular choice. `fido-mode' in combination 88 | with `icomplete-vertical-mode' should also work well." 89 | :type '(choice (const read-number) 90 | (const completing-read) 91 | (const helm) 92 | function)) 93 | 94 | (defcustom rfc-mode-use-original-buffer-names nil 95 | "Whether RFC document buffers should have the name of the document file. 96 | If nil (the default) then use e.g. *rfc21*, otherwise use e.g. rfc21.txt." 97 | :type 'boolean) 98 | 99 | (defcustom rfc-mode-browser-entry-title-width 60 100 | "The width of the column containing RFC titles in the browser." 101 | :type 'integer) 102 | 103 | (defcustom rfc-mode-imenu-title "RFC Contents" 104 | "The title to use if `rfc-mode' adds a RFC Contents menu to the menubar." 105 | :type 'string) 106 | 107 | ;;; Misc variables: 108 | 109 | (defvar rfc-mode-index-entries nil 110 | "The list of entries in the RFC index.") 111 | 112 | (defconst rfc-mode-title-regexp "^\\(?:[0-9]+\\.\\)+\\(?:[0-9]+\\)? .*$" 113 | "Regular expression to model section titles in RFC documents.") 114 | 115 | (defvar-local rfc-mode--titles nil 116 | "Buffer-local variable that keeps a list of section titles in this RFC.") 117 | 118 | (defvar rfc-mode--last-title nil 119 | "Last section title that the user visited.") 120 | 121 | ;;; Keys: 122 | 123 | (defvar rfc-mode-map 124 | (let ((map (make-keymap))) 125 | (set-keymap-parent map special-mode-map) 126 | (define-key map (kbd "") #'forward-button) 127 | (define-key map (kbd "") #'backward-button) 128 | (define-key map (kbd "") #'rfc-mode-backward-page) 129 | (define-key map (kbd "") #'rfc-mode-forward-page) 130 | (define-key map (kbd "g") #'rfc-mode-goto-section) 131 | (define-key map (kbd "n") #'rfc-mode-next-section) 132 | (define-key map (kbd "p") #'rfc-mode-previous-section) 133 | map) 134 | "The keymap for `rfc-mode'.") 135 | 136 | ;;; Main: 137 | 138 | (defun rfc-mode-init () 139 | "Initialize the current buffer for `rfc-mode'." 140 | (setq-local page-delimiter "^.*?\n ") 141 | (rfc-mode-highlight) 142 | (setq imenu-generic-expression (list (list nil rfc-mode-title-regexp 0))) 143 | (imenu-add-to-menubar rfc-mode-imenu-title)) 144 | 145 | (define-obsolete-function-alias 'rfc-mode-quit #'quit-window "rfc-mode-1.4") 146 | 147 | (defun rfc-mode-recenter () 148 | "Do the same as `recenter-top-bottom' would for the `top' position." 149 | (rfc-mode-header-start) 150 | (let ((recenter-positions '(top))) 151 | (recenter-top-bottom))) 152 | 153 | (defun rfc-mode-backward-page () 154 | "Scroll to the previous page of the current buffer." 155 | (interactive) 156 | (beginning-of-line) 157 | (unless (looking-at " ") 158 | (backward-page)) 159 | (backward-page) 160 | (beginning-of-line 1) 161 | (rfc-mode-recenter)) 162 | 163 | (defun rfc-mode-forward-page () 164 | "Scroll to the next page of the current buffer." 165 | (interactive) 166 | (forward-page) 167 | (beginning-of-line 1) 168 | (rfc-mode-recenter)) 169 | 170 | (defun rfc-mode-goto-section (section) ;FIXME: Why not use imenu for that? 171 | "Move point to SECTION." 172 | (interactive 173 | (let* ((default (if (member rfc-mode--last-title rfc-mode--titles) 174 | rfc-mode--last-title 175 | (car rfc-mode--titles))) 176 | (completion-ignore-case t) 177 | (prompt (concat "Go to section (default " default "): ")) 178 | (chosen (completing-read prompt rfc-mode--titles 179 | nil nil nil nil default))) 180 | (list chosen))) 181 | (setq rfc-mode--last-title section) 182 | (unless (rfc-mode--goto-section section) 183 | (error "Section %s not found" section))) 184 | 185 | (defun rfc-mode--goto-section (section) 186 | "Move point to SECTION if it exists, otherwise don't move point. 187 | Returns t if section is found, nil otherwise." 188 | (let ((curpos (point)) 189 | (case-fold-search nil)) 190 | (goto-char (point-min)) 191 | (if (re-search-forward (concat "^" section) (point-max) t) 192 | (progn 193 | (beginning-of-line) 194 | (rfc-mode-recenter) 195 | t) 196 | (goto-char curpos) 197 | nil))) 198 | 199 | (defun rfc-mode-next-section (n) 200 | "Move point to Nth next section (default 1)." 201 | (interactive "p") 202 | (let ((case-fold-search nil) 203 | (start (point))) 204 | (if (looking-at rfc-mode-title-regexp) 205 | (forward-line 1)) 206 | (if (re-search-forward rfc-mode-title-regexp (point-max) t n) 207 | (progn 208 | (beginning-of-line) 209 | (rfc-mode-recenter)) 210 | (goto-char (point-max)) 211 | ;; The last line doesn't belong to any section. 212 | (forward-line -1)) 213 | ;; Ensure we never move back from the starting point. 214 | (if (< (point) start) (goto-char start)))) 215 | 216 | (defun rfc-mode-previous-section (n) 217 | "Move point to Nth previous section (default 1)." 218 | (interactive "p") 219 | (let ((case-fold-search nil)) 220 | (if (looking-at rfc-mode-title-regexp) 221 | (forward-line -1)) 222 | (if (re-search-backward rfc-mode-title-regexp (point-min) t n) 223 | (progn 224 | (beginning-of-line) 225 | (rfc-mode-recenter)) 226 | (goto-char (point-min))))) 227 | 228 | ;;;###autoload 229 | (defun rfc-mode-read (number) 230 | "Read the RFC document NUMBER. 231 | Offer the number at point as default." 232 | (interactive 233 | (if (and current-prefix-arg (not (consp current-prefix-arg))) 234 | (list (prefix-numeric-value current-prefix-arg)) 235 | (list (read-number "RFC number: " (rfc-mode--integer-at-point))))) 236 | (display-buffer (rfc-mode--document-buffer number))) 237 | 238 | (defun rfc-mode-reload-index () 239 | "Reload the RFC document index from its original file." 240 | (interactive) 241 | (setq rfc-mode-index-entries nil)) 242 | 243 | (defun rfc-mode--index-entries () 244 | (or rfc-mode-index-entries 245 | (let ((file (rfc-mode--document-file "-index"))) 246 | (setq rfc-mode-index-entries 247 | (rfc-mode-read-index-file file))))) 248 | 249 | ;;;###autoload 250 | (defun rfc-mode-browse () 251 | "Browse through all RFC documents referenced in the index." 252 | (interactive) 253 | (pcase rfc-mode-browse-input-function 254 | ('read-number 255 | (display-buffer (rfc-mode--document-buffer 256 | (read-number "View RFC document: " 257 | (rfc-mode--integer-at-point))))) 258 | ('helm 259 | (if (fboundp 'helm) 260 | (helm :buffer "*helm rfc browser*" 261 | :sources (rfc-mode-browser-helm-sources 262 | (rfc-mode--index-entries))) 263 | (user-error "Helm has to be installed explicitly"))) 264 | ('completing-read 265 | (let* ((default (rfc-mode--integer-at-point)) 266 | (cands (mapcar (lambda (entry) 267 | (let ((cand 268 | (rfc-mode-browser-format-candidate entry))) 269 | (and (numberp default) 270 | (= (plist-get entry :number) default) 271 | (setq default (car cand))) 272 | cand)) 273 | (rfc-mode--index-entries))) 274 | (choice (completing-read "View RFC document: " 275 | cands nil nil nil nil default)) 276 | (number (or (and (string-match "\\`RFC\\([0-9]+\\)" choice) 277 | (string-to-number (match-string 1 choice))) 278 | (ignore-errors (string-to-number choice))))) 279 | (unless number 280 | (user-error 281 | "%s doesn't match a completion candidate and is not a number" 282 | choice)) 283 | (display-buffer (rfc-mode--document-buffer number)))) 284 | (_ (display-buffer (rfc-mode--document-buffer 285 | (funcall rfc-mode-browse-input-function)))))) 286 | 287 | ;;;###autoload 288 | (define-derived-mode rfc-mode special-mode "rfc-mode" 289 | "Major mode to browse and read RFC documents." 290 | (rfc-mode-init)) 291 | 292 | ;;;###autoload 293 | (add-to-list 'auto-mode-alist '("/rfc[0-9]+\\.txt\\'" . rfc-mode)) 294 | (add-to-list 'auto-mode-alist '("\\*rfc[0-9]+\\*\\'" . rfc-mode)) 295 | 296 | ;;; Syntax utils: 297 | 298 | (defun rfc-mode-highlight () 299 | "Highlight the current buffer." 300 | (setq rfc-mode--titles nil) 301 | ;; FIXME: Use font-lock! 302 | (with-silent-modifications 303 | (let ((inhibit-read-only t)) 304 | ;; Headers and footers 305 | (save-excursion 306 | (goto-char (point-min)) 307 | (while (search-forward " " nil t) 308 | (beginning-of-line) 309 | (let ((form-feed (point))) 310 | (let* ((footer-end (rfc-mode-previous-footer-start)) 311 | (footer-start (point))) 312 | (put-text-property 313 | footer-start footer-end 314 | 'face 'rfc-mode-document-footer-face)) 315 | (goto-char form-feed) 316 | (let* ((header-end (rfc-mode-header-start)) 317 | (header-start (point))) 318 | (put-text-property 319 | header-start header-end 320 | 'face 'rfc-mode-document-header-face) 321 | (goto-char header-end))))) 322 | ;; Section titles 323 | (save-excursion 324 | (goto-char (point-min)) 325 | (while (search-forward-regexp rfc-mode-title-regexp nil t) 326 | (let ((start (match-beginning 0)) 327 | (end (match-end 0))) 328 | (put-text-property start end 329 | 'face 'rfc-mode-document-section-title-face) 330 | (push (match-string 0) rfc-mode--titles) 331 | (goto-char end)))) 332 | ;; Keep titles in expected top to bottom order. 333 | (setq rfc-mode--titles (nreverse rfc-mode--titles)) 334 | ;; RFC references 335 | (save-excursion 336 | (goto-char (point-min)) 337 | (while (search-forward-regexp "RFC *\\([0-9]+\\)" nil t) 338 | (let ((start (match-beginning 0)) 339 | (end (match-end 0)) 340 | (number (string-to-number (match-string 1)))) 341 | (unless (= start (line-beginning-position)) 342 | (make-text-button start end 343 | 'action (lambda (_button) 344 | (rfc-mode-read number)) 345 | 'help-echo (format "Read RFC %d" number) 346 | 'follow-link t)) 347 | (goto-char end))))))) 348 | 349 | (defun rfc-mode-header-start () 350 | "Move to the start of the current header. 351 | 352 | When the point is on a form feed character, move it to the start 353 | of the current page header and return the position of the end of 354 | the header." 355 | (when (looking-at " ") 356 | (forward-line 1) 357 | (move-end-of-line 1) 358 | (prog1 (point) 359 | (move-beginning-of-line 1)))) 360 | 361 | (defun rfc-mode-previous-footer-start () 362 | "Move to the start of the previous footer. 363 | 364 | When the point is on a form feed character, move it to the start 365 | of the previous page footer and return the position of the end of 366 | the footer." 367 | (when (looking-at " ") 368 | (forward-line -1) 369 | (move-end-of-line 1) 370 | (prog1 (point) 371 | (move-beginning-of-line 1)))) 372 | 373 | ;;; Browser utils: 374 | 375 | (defun rfc-mode-browser-helm-sources (entries) 376 | "Create a Helm source for ENTRIES. 377 | 378 | ENTRIES is a list of RFC index entries in the browser." 379 | (helm-build-sync-source "RFC documents" 380 | :candidates (mapcar #'rfc-mode-browser-format-candidate entries) 381 | :action (helm-make-actions 382 | "Read" #'rfc-mode-browser-helm-entry-read))) 383 | 384 | (defun rfc-mode-browser-format-candidate (entry) 385 | "Create a Helm candidate for ENTRY. 386 | 387 | ENTRY is a RFC index entry in the browser." 388 | (let* ((ref (rfc-mode--pad-string 389 | (format "RFC%d" (plist-get entry :number)) 7)) 390 | (title (rfc-mode--pad-string 391 | (plist-get entry :title) 392 | rfc-mode-browser-entry-title-width)) 393 | (status (or (plist-get entry :status) "")) 394 | (obsoleted-by (plist-get entry :obsoleted-by)) 395 | (obsoletep (> (length obsoleted-by) 0)) 396 | (string (format "%s %s %s" 397 | (rfc-mode--highlight-string 398 | ref 'rfc-mode-browser-ref-face) 399 | (rfc-mode--highlight-string 400 | title (if obsoletep 401 | 'rfc-mode-browser-title-obsolete-face 402 | 'rfc-mode-browser-title-face)) 403 | (rfc-mode--highlight-string 404 | status 'rfc-mode-browser-status-face)))) 405 | (cons string entry))) 406 | 407 | (defun rfc-mode-browser-helm-entry-read (entry) 408 | "The read action the Helm candidate ENTRY in the browser." 409 | (let ((number (plist-get entry :number))) 410 | (rfc-mode-read number))) 411 | 412 | ;;; Index utils: 413 | 414 | (defun rfc-mode-read-index-file (filename) 415 | "Read an RFC index file at FILENAME and return a list of entries." 416 | (with-temp-buffer 417 | (insert-file-contents filename) 418 | (rfc-mode-read-index (current-buffer)))) 419 | 420 | (defun rfc-mode-read-index (buffer) 421 | "Read an RFC index file from BUFFER and return a list of entries." 422 | (with-current-buffer buffer 423 | (goto-char (point-min)) 424 | (let ((entries nil)) 425 | (while (search-forward-regexp "^[0-9]+ " nil t) 426 | (let ((start (match-beginning 0))) 427 | (search-forward-regexp " $") 428 | (let* ((end (match-beginning 0)) 429 | (lines (buffer-substring start end)) 430 | (entry-string (replace-regexp-in-string "[ \n]+" " " lines)) 431 | (entry (rfc-mode-parse-index-entry entry-string))) 432 | (unless (string= (plist-get entry :title) "Not Issued") 433 | (push entry entries))))) 434 | (nreverse entries)))) 435 | 436 | (defun rfc-mode-parse-index-entry (string) 437 | "Parse the RFC document index entry STRING and return it as a plist." 438 | (unless (string-match "\\(^[0-9]+\\) *\\(.*?\\)\\.\\(?: \\|$\\)" string) 439 | (error "Invalid index entry format: %S" string)) 440 | (let* ((number-string (match-string 1 string)) 441 | (number (string-to-number number-string)) 442 | (title (match-string 2 string))) 443 | (when (zerop number) 444 | (error "Invalid index entry number: %S" number-string)) 445 | (let ((entry (list :number number :title title))) 446 | (when (string-match "(Status: \\([^)]+\\))" string) 447 | (plist-put entry :status (downcase (match-string 1 string)))) 448 | (when (string-match "(Obsoletes \\([^)]+\\))" string) 449 | (plist-put entry :obsoletes 450 | (rfc-mode--parse-rfc-refs (match-string 1 string)))) 451 | (when (string-match "(Obsoleted by \\([^)]+\\))" string) 452 | (plist-put entry :obsoleted-by 453 | (rfc-mode--parse-rfc-refs (match-string 1 string)))) 454 | (when (string-match "(Updates \\([^)]+\\))" string) 455 | (plist-put entry :updates 456 | (rfc-mode--parse-rfc-refs (match-string 1 string)))) 457 | (when (string-match "(Updated by \\([^)]+\\))" string) 458 | (plist-put entry :updated-by 459 | (rfc-mode--parse-rfc-refs (match-string 1 string)))) 460 | entry))) 461 | 462 | ;;; Document utils: 463 | 464 | (defun rfc-mode--document-buffer-name (number) 465 | "Return the buffer name for the RFC document NUMBER." 466 | (concat "*rfc" (number-to-string number) "*")) 467 | 468 | (defun rfc-mode--document-file (number) 469 | "Return the absolute file name of the RFC document NUMBER." 470 | (let ((file 471 | (expand-file-name (format "rfc%s.txt" number) rfc-mode-directory))) 472 | (rfc-mode--ensure-directory-exists) 473 | (unless (file-exists-p file) 474 | (url-copy-file (format rfc-mode-document-url number) file)) 475 | file)) 476 | 477 | (defun rfc-mode--document-buffer (number) 478 | "Return a buffer visiting the RFC document NUMBER. 479 | 480 | The buffer is created if it does not exist." 481 | (let* ((buffer-name (rfc-mode--document-buffer-name number)) 482 | (document-path (rfc-mode--document-file number))) 483 | (with-current-buffer (find-file-noselect document-path) 484 | (unless rfc-mode-use-original-buffer-names 485 | (rename-buffer buffer-name)) 486 | (rfc-mode) 487 | (current-buffer)))) 488 | 489 | ;;; Misc utils: 490 | 491 | (defun rfc-mode--integer-at-point () 492 | ;; Note that we don't use `number-at-point' as it will match 493 | ;; number formats that make no sense as RFC numbers (floating 494 | ;; point, hexadecimal, etc.). 495 | (save-excursion 496 | (skip-chars-backward "0-9") 497 | (and (looking-at "[0-9]") 498 | (string-to-number 499 | (buffer-substring-no-properties 500 | (point) 501 | (progn (skip-chars-forward "0-9") 502 | (point))))))) 503 | 504 | (defun rfc-mode--ensure-directory-exists () 505 | "Check that `rfc-mode-directory' exists, creating it if it does not." 506 | (when (and (not (file-exists-p rfc-mode-directory)) 507 | (y-or-n-p (format "Create directory %s? " rfc-mode-directory))) 508 | (make-directory rfc-mode-directory t))) 509 | 510 | (defun rfc-mode--parse-rfc-ref (string) 511 | "Parse a reference to a RFC document from STRING. 512 | 513 | For example: \"RFC 2822\"." 514 | (when (string-match "^RFC *\\([0-9]+\\)" string) 515 | (string-to-number (match-string 1 string)))) 516 | 517 | (defun rfc-mode--parse-rfc-refs (string) 518 | "Parse a list of references to RFC documents from STRING. 519 | 520 | For example: \"RFC3401, RFC3402 ,RFC 3403\"." 521 | (seq-remove #'null (mapcar #'rfc-mode--parse-rfc-ref 522 | (split-string string "," t " +")))) 523 | 524 | (defun rfc-mode--pad-string (string width) 525 | "Pad STRING with spaces to WIDTH characters." 526 | (truncate-string-to-width string width 0 ?\s)) 527 | 528 | (defun rfc-mode--highlight-string (string face) 529 | "Highlight STRING using FACE." 530 | (put-text-property 0 (length string) 'face face string) 531 | string) 532 | 533 | (provide 'rfc-mode) 534 | 535 | ;;; rfc-mode.el ends here 536 | --------------------------------------------------------------------------------