diff options
author | Sean Allred <code@seanallred.com> | 2014-11-29 09:36:59 -0500 |
---|---|---|
committer | Sean Allred <code@seanallred.com> | 2014-11-29 09:36:59 -0500 |
commit | f5ca9c6d7629817569c3bd58e5569fee88cd5f2b (patch) | |
tree | bcbbdcb2e2fdc91f9431d3b3ad605aa0a7fa0bde | |
parent | a8e882d99a037075595260dc74fad0fb67c69d81 (diff) | |
parent | 00a187f5bb7dc08117965eae05df51d0eedac90e (diff) |
Merge branch 'master' into issue-100
Conflicts:
sx-question-mode.el
-rw-r--r-- | README.org | 50 | ||||
-rw-r--r-- | sx-auth.el | 18 | ||||
-rw-r--r-- | sx-cache.el | 8 | ||||
-rw-r--r-- | sx-encoding.el | 4 | ||||
-rw-r--r-- | sx-interaction.el | 3 | ||||
-rw-r--r-- | sx-method.el | 3 | ||||
-rw-r--r-- | sx-question-list.el | 14 | ||||
-rw-r--r-- | sx-question-mode.el | 435 | ||||
-rw-r--r-- | sx-question-print.el | 440 | ||||
-rw-r--r-- | sx-question.el | 32 | ||||
-rw-r--r-- | sx-request.el | 20 | ||||
-rw-r--r-- | sx-site.el | 2 | ||||
-rw-r--r-- | sx-tab.el | 11 | ||||
-rw-r--r-- | sx-time.el | 6 | ||||
-rw-r--r-- | sx.el | 23 | ||||
-rw-r--r-- | sx.org | 2 |
16 files changed, 560 insertions, 511 deletions
@@ -1,21 +1,25 @@ -#+Title: Stack-Mode +#+Title: SX -- Stack Exchange for Emacs -[[https://travis-ci.org/vermiculus/stack-mode][https://travis-ci.org/vermiculus/stack-mode.svg?branch=master]] -[[https://gitter.im/vermiculus/stack-mode?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge][https://badges.gitter.im/Join Chat.svg]] -[[https://www.waffle.io/vermiculus/stack-mode][https://badge.waffle.io/vermiculus/stack-mode.svg]] +[[https://travis-ci.org/vermiculus/sx.el][https://travis-ci.org/vermiculus/sx.el.svg?branch=master]] +[[https://gitter.im/vermiculus/sx.el?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge][https://badges.gitter.im/Join Chat.svg]] +[[https://www.waffle.io/vermiculus/sx.el][https://badge.waffle.io/vermiculus/sx.el.svg]] -StackMode will be a full featured Stack Exchange mode for GNU Emacs 24 -and up. Using the official API, we aim to create a more versatile -experience for the Stack Exchange network within Emacs itself. +SX will be a full featured Stack Exchange mode for GNU Emacs 24+. +Using the official API, we aim to create a more versatile experience +for the Stack Exchange network within Emacs itself. * Features -- ~list-questions~ :: +- ~sx-tab-frontpage~ :: List questions on a StackExchange site. - Viewing Posts :: - Use =jknp= to open questions from within ~list-questions~; use =RET= to move focus. - - Use =v= to open the site in your browser. + - Use =v= to open the object at point in your browser. - Use =TAB= to fold questions and answers. + - Use =RET= to open a link at point. + - Use =:= to switch sites. + - Vote up and down with =u= and =d=. + ** Planned - Archiving questions for offline access - Browsing and favoriting networks @@ -30,37 +34,37 @@ Have a feature in mind that isn't on the list? Submit a pull request to add it to the list! If you want to discuss it first, pop in our Gitter chatroom (badge above) -- someone will be around shortly to talk about it. + * Installation To install the development version, follow the usual steps: - Clone this repository - Add this directory to your ~load-path~ -- Issue ~(require 'sx-question-list)~ +- Issue ~(require 'sx)~ This should give you access to the only entry point function at the -moment, ~list-questions~. +moment, ~sx-tab-frontpage~. + +Eventually, this package will be available on MELPA. -Eventually, this package will at least be available on MELPA. -Depending on community involvement, it may even be submitted to the -official GNU ELPA. * Contributing Please help contribute! Doing any of the following will help us immensely: - - [[https://github.com/vermiculus/stack-mode/issues/new][Open an issue]] - - [[https://github.com/vermiculus/stack-mode/pulls][Submit a pull request]] - - [[https://gitter.im/vermiculus/stack-mode][Suggest a package or library in our Chat on Gitter]] + - [[https://github.com/vermiculus/sx.el/issues/new][Open an issue]] + - [[https://github.com/vermiculus/sx.el/pulls][Submit a pull request]] + - [[https://gitter.im/vermiculus/sx.el][Suggest a package or library in our Chat on Gitter]] (or just hang out =:)=) - Spread the word! For a better view of all of the open issues, take a look at our lovely -[[http://www.waffle.io/vermiculus/stack-mode][Waffle board]]. Feel free to take the torch on anything in =backlog= or +[[http://www.waffle.io/vermiculus/sx.el][Waffle board]]. Feel free to take the torch on anything in =backlog= or =ready=. If you have thoughts on any other issues, don't hesitate to chime in! + * Resources - [[http://www.gnu.org/software/emacs/][GNU Emacs]] - [[https://api.stackexchange.com/docs][Stack Exchange API v2.2]] -- [[http://stackapps.com/apps/oauth/register][StackApps Registration Page]] -- [[http://www.emacswiki.org/emacs/ModeTutorial][Creating Major Modes for Emacs]] + ** Icons -Stack Exchange Mode for Emacs has no explicit use for an icon, -although standard SVG files have been gathered in =resources/= if -anyone would fancy a crack at it. +SX has no explicit /need/ for an icon, although standard SVG files +have been gathered in =resources/= if anyone would fancy a crack at +it. - [[file:resources/emacs.svg][Emacs icon]] - [[file:resources/stackexchange.svg][Stack Exchange icon]] @@ -28,7 +28,7 @@ (defconst sx-auth-root "https://stackexchange.com/oauth/dialog") (defconst sx-auth-redirect-uri - "http://vermiculus.github.io/stack-mode/auth/auth.htm") + "http://vermiculus.github.io/sx.el/auth/auth.htm") (defconst sx-auth-client-id "3291") (defvar sx-auth-access-token @@ -154,14 +154,14 @@ If it has `auth-required' properties, return a filter that has removed those properties." (let* ((incl-filter (if (listp filter) (car filter))) (rest-filter (if incl-filter (cdr filter))) - (auth-filters (cl-remove-if #'nil - ;; Only retrieve the elements that - ;; are issues. - (mapcar (lambda (prop) - (car - (member prop - sx-auth-filter-auth))) - (or incl-filter filter)))) + (auth-filters (remove nil + ;; Only retrieve the elements that + ;; are issues. + (mapcar (lambda (prop) + (car + (member prop + sx-auth-filter-auth))) + (or incl-filter filter)))) clean-filter out-filter) (lwarn "sx-auth filter" :debug "Filter: %S" filter) ;; Auth-filters is the filters that are issues diff --git a/sx-cache.el b/sx-cache.el index 07352d0..51c2267 100644 --- a/sx-cache.el +++ b/sx-cache.el @@ -1,4 +1,4 @@ -;;; sx-cache.el --- caching for stack-mode +;;; sx-cache.el --- caching ;; Copyright (C) 2014 Sean Allred @@ -29,10 +29,10 @@ ;;; Code: (defcustom sx-cache-directory - (expand-file-name ".stackmode" user-emacs-directory) + (expand-file-name ".sx" user-emacs-directory) "Directory containing cached data." :type 'directory - :group 'sx-cache) + :group 'sx) (defun sx-cache--ensure-sx-cache-directory-exists () "Ensure `sx-cache-directory' exists." @@ -99,7 +99,7 @@ as delete the list of hidden questions." (when save-auth (setq caches (cl-remove-if (lambda (x) (string= x "auth.el")) caches))) - (lwarn 'stack-mode :debug "Invalidating: %S" caches) + (lwarn 'sx :debug "Invalidating: %S" caches) (mapc #'delete-file caches) (sx-initialize 'force))) diff --git a/sx-encoding.el b/sx-encoding.el index f683615..0e66677 100644 --- a/sx-encoding.el +++ b/sx-encoding.el @@ -1,4 +1,4 @@ -;;; sx-encoding.el --- encoding for stack-mode +;;; sx-encoding.el --- encoding ;; Copyright (C) 2014 Sean Allred @@ -145,7 +145,7 @@ See URL `http://www.gzip.org/zlib/rfc-gzip.html'." "Check if BUFFER is gzip-compressed. See `sx-encoding-gzipped-p'." (with-current-buffer buffer - (sx-encoding-gzip-check-magic + (sx-encoding-gzipped-p (buffer-string)))) (defun sx-encoding-gzipped-file-p (file) diff --git a/sx-interaction.el b/sx-interaction.el index e4234b0..5f3ece6 100644 --- a/sx-interaction.el +++ b/sx-interaction.el @@ -180,8 +180,7 @@ ID is an integer." (defun sx--add-comment-to-object (comment object) "Add COMMENT to OBJECT's `comments' property. OBJECT can be a question or an answer." - (let ((com-cell (assoc 'comments object)) - (count-cell (assoc 'comment_count object))) + (let ((com-cell (assoc 'comments object))) (if com-cell (progn (setcdr diff --git a/sx-method.el b/sx-method.el index 1b20cbf..83455b8 100644 --- a/sx-method.el +++ b/sx-method.el @@ -82,7 +82,8 @@ Return the entire response as a complex alist." (prog1 (format "?site=%s" site) (setq site nil))))) - (call #'sx-request-make)) + (call #'sx-request-make) + parameters) (lwarn "sx-call-method" :debug "A: %S T: %S. M: %S,%s. F: %S" (equal 'warn auth) access-token method-auth full-method filter-auth) (unless access-token diff --git a/sx-question-list.el b/sx-question-list.el index fbed4ea..9709b99 100644 --- a/sx-question-list.el +++ b/sx-question-list.el @@ -30,10 +30,22 @@ (require 'sx-question-mode) (require 'sx-favorites) +(defgroup sx-question-list nil + "Customization group for sx-question-list." + :prefix "sx-question-list-" + :tag "SX Question List" + :group 'sx) + +(defgroup sx-question-list-faces nil + "Customization group for the faces of `sx-question-list'." + :prefix "sx-question-list-" + :tag "SX Question List Faces" + :group 'sx-question-list) + ;;; Customization (defcustom sx-question-list-height 12 - "Height, in lines, of stack-mode's *question-list* buffer." + "Height, in lines, of SX's *question-list* buffer." :type 'integer :group 'sx-question-list) diff --git a/sx-question-mode.el b/sx-question-mode.el index db3bb95..01a980a 100644 --- a/sx-question-mode.el +++ b/sx-question-mode.el @@ -1,4 +1,4 @@ -;;; sx-question-mode.el --- Creating the buffer that displays questions -*- lexical-binding: t; -*- +;;; sx-question-mode.el --- Creating the buffer that displays questions ;; Copyright (C) 2014 Artur Malabarba @@ -21,22 +21,12 @@ ;;; Code: -(require 'markdown-mode) (eval-when-compile (require 'rx)) (require 'sx) (require 'sx-question) - -(defgroup sx-question-mode nil - "Customization group for sx-question-mode." - :prefix "sx-question-mode-" - :group 'sx) - -(defgroup sx-question-mode-faces nil - "Customization group for the faces of `sx-question-mode'." - :prefix "sx-question-mode-" - :group 'sx-question-mode) +(require 'sx-question-print) ;;; Displaying a question @@ -73,7 +63,7 @@ If WINDOW is given, use that to display the buffer." ;; Create the buffer if necessary. (unless (buffer-live-p sx-question-mode--buffer) (setq sx-question-mode--buffer - (generate-new-buffer "*stack-question*"))) + (generate-new-buffer "*sx-question*"))) (cond ;; Window was given, use it. ((window-live-p window) @@ -85,415 +75,6 @@ If WINDOW is given, use that to display the buffer." sx-question-mode--buffer) -;;; Printing a question's content -;;;; Faces and Variables - -(defface sx-question-mode-header - '((t :inherit font-lock-variable-name-face)) - "Face used on the question headers in the question buffer." - :group 'sx-question-mode-faces) - -(defface sx-question-mode-title - '((t :height 1.3 :weight bold :inherit default)) - "Face used on the question title in the question buffer." - :group 'sx-question-mode-faces) - -(defface sx-question-mode-title-comments - '((t :height 1.1 :inherit sx-question-mode-title)) - "Face used on the question title in the question buffer." - :group 'sx-question-mode-faces) - -(defcustom sx-question-mode-deleted-user - '((display_name . "(deleted user)")) - "The structure used to represent a deleted account." - :type '(alist :options ((display_name string))) - :group 'sx-question-mode) - -(defcustom sx-question-mode-header-title "\n" - "String used before the question title at the header." - :type 'string - :group 'sx-question-mode) - -(defface sx-question-mode-author - '((t :inherit font-lock-string-face)) - "Face used on the question author in the question buffer." - :group 'sx-question-mode-faces) - -(defcustom sx-question-mode-header-author "\nAuthor: " - "String used before the question author at the header." - :type 'string - :group 'sx-question-mode) - -(defface sx-question-mode-date - '((t :inherit font-lock-string-face)) - "Face used on the question date in the question buffer." - :group 'sx-question-mode-faces) - -(defcustom sx-question-mode-header-date "\nAsked on: " - "String used before the question date at the header." - :type 'string - :group 'sx-question-mode) - -(defface sx-question-mode-tags - '((t :underline nil :inherit font-lock-function-name-face)) - "Face used on the question tags in the question buffer." - :group 'sx-question-mode-faces) - -(defface sx-question-mode-author - '((t :inherit font-lock-variable-name-face)) - "Face used for author names in the question buffer." - :group 'sx-question-mode-faces) - -(defface sx-question-mode-score - '((t)) - "Face used for the score in the question buffer." - :group 'sx-question-mode-faces) - -(defface sx-question-mode-score-downvoted - '((t :inherit (font-lock-warning-face sx-question-mode-score))) - "Face used for downvoted score in the question buffer." - :group 'sx-question-mode-faces) - -(defface sx-question-mode-score-upvoted - '((t :weight bold - :inherit (font-lock-function-name-face sx-question-mode-score))) - "Face used for downvoted score in the question buffer." - :group 'sx-question-mode-faces) - -(defcustom sx-question-mode-header-tags "\nTags: " - "String used before the question tags at the header." - :type 'string - :group 'sx-question-mode) - -(defcustom sx-question-mode-header-score "\nScore: " - "String used before the question score at the header." - :type 'string - :group 'sx-question-mode) - -(defface sx-question-mode-content-face - '((((background dark)) :background "#090909") - (((background light)) :background "#f4f4f4")) - "Face used on the question body in the question buffer. -This shouldn't have a foreground, or this will interfere with -font-locking." - :group 'sx-question-mode-faces) - -(defcustom sx-question-mode-last-edit-format " (edited %s ago by %s)" - "Format used to describe last edit date in the header. -First \"%s\" is replaced with the date and the second \"%s\" with -the editor's name." - :type 'string - :group 'sx-question-mode) - -(defcustom sx-question-mode-separator - (concat (make-string 80 ?_) "\n") - "Separator used between header and body." - :type 'string - :group 'sx-question-mode) - -(defcustom sx-question-mode-answer-title "Answer" - "Title used at the start of \"Answer\" sections." - :type 'string - :group 'sx-question-mode) - -(defcustom sx-question-mode-comments-title " Comments" - "Title used at the start of \"Comments\" sections." - :type 'string - :group 'sx-question-mode) - -(defcustom sx-question-mode-comments-format "%s: %s\n" - "Format used to display comments. -First \"%s\" is replaced with user name. Second \"%s\" is -replaced with the comment." - :type 'string - :group 'sx-question-mode) - -(defcustom sx-question-mode-pretty-links t - "If non-nil, markdown links are displayed in a compact form." - :type 'boolean - :group 'sx-question-mode) - - -;;; Printing a question's content -;;;; Functions -(defun sx-question-mode--print-question (question) - "Print a buffer describing QUESTION. -QUESTION must be a data structure returned by `json-read'." - (setq sx-question-mode--data question) - ;; Clear the overlays - (mapc #'delete-overlay sx--overlays) - (setq sx--overlays nil) - ;; Print everything - (sx-question-mode--print-section question) - (sx-assoc-let question - (mapc #'sx-question-mode--print-section .answers)) - (goto-char (point-min)) - (sx-question-mode-next-section)) - -(defvar sx-question-mode--section-help-echo - (format - (propertize "%s to hide/display content" 'face 'minibuffer-prompt) - (propertize "RET" 'face 'font-lock-function-name-face)) - "Help echoed in the minibuffer when point is on a section.") - -(defvar sx-question-mode--title-properties - `(face sx-question-mode-title - action sx-question-mode-hide-show-section - help-echo ,sx-question-mode--section-help-echo - button t - follow-link t) - "Title properties.") - -(defun sx-question-mode--print-section (data) - "Print a section corresponding to DATA. -DATA can represent a question or an answer." - ;; This makes `data' accessible through `sx--data-here'. - (sx-assoc-let data - (sx--wrap-in-text-property - (list 'sx--data-here data) - (insert sx-question-mode-header-title - (apply - #'propertize - ;; Questions have title - (or .title - ;; Answers don't - sx-question-mode-answer-title) - ;; Section level - 'sx-question-mode--section (if .title 1 2) - ;; face, action and help-echo - sx-question-mode--title-properties)) - ;; Sections can be hidden with overlays - (sx--wrap-in-overlay - '(sx-question-mode--section-content t) - (sx-question-mode--insert-header - ;; Author - sx-question-mode-header-author - (sx-question-mode--propertize-display-name .owner) - 'sx-question-mode-author - ;; Date - sx-question-mode-header-date - (concat - (sx-time-seconds-to-date .creation_date) - (when .last_edit_date - (format sx-question-mode-last-edit-format - (sx-time-since .last_edit_date) - (sx-question-mode--propertize-display-name - (or .last_editor sx-question-mode-deleted-user))))) - 'sx-question-mode-date) - (sx-question-mode--insert-header - sx-question-mode-header-score - (format "%s" .score) - (cond - ((eq .upvoted t) 'sx-question-mode-score-upvoted) - ((eq .downvoted t) 'sx-question-mode-score-downvoted) - (t 'sx-question-mode-score))) - (when .title - ;; Tags - (sx-question-mode--insert-header - sx-question-mode-header-tags - (mapconcat #'sx-question--tag-format .tags " ") - 'sx-question-mode-tags)) - ;; Body - (insert "\n" - (propertize sx-question-mode-separator - 'face 'sx-question-mode-header - 'sx-question-mode--section 4)) - (sx--wrap-in-overlay - '(face sx-question-mode-content-face) - (insert "\n" - (sx-question-mode--fill-and-fontify - .body_markdown) - "\n" - (propertize sx-question-mode-separator - 'face 'sx-question-mode-header))))) - ;; Comments have their own `sx--data-here' property (so they can - ;; be upvoted too). - (when .comments - (insert "\n" - (apply #'propertize - sx-question-mode-comments-title - 'face 'sx-question-mode-title-comments - 'sx-question-mode--section 3 - sx-question-mode--title-properties)) - (sx--wrap-in-overlay - '(sx-question-mode--section-content t) - (insert "\n") - (sx--wrap-in-overlay - '(face sx-question-mode-content-face) - (mapc #'sx-question-mode--print-comment .comments)))))) - -(defun sx-question-mode--propertize-display-name (author) - "Return display_name of AUTHOR with `sx-question-mode-author' face." - (sx-assoc-let author - (propertize .display_name - 'face 'sx-question-mode-author))) - -(defun sx-question-mode--print-comment (comment-data) - "Print the comment described by alist COMMENT-DATA. -The comment is indented, filled, and then printed according to -`sx-question-mode-comments-format'." - (sx--wrap-in-text-property - (list 'sx--data-here comment-data) - (sx-assoc-let comment-data - (insert - (format - sx-question-mode-comments-format - (sx-question-mode--propertize-display-name .owner) - (substring - ;; We fill with three spaces at the start, so the comment is - ;; slightly indented. - (sx-question-mode--fill-and-fontify - (concat " " .body_markdown)) - ;; Then we remove the spaces from the first line, since we'll - ;; add the username there anyway. - 3)))))) - -(defun sx-question-mode--insert-header (&rest args) - "Insert propertized ARGS. -ARGS is a list of repeating values -- `header', `value', and -`face'. `header' is given `sx-question-mode-header' as a face, -where `value' is given `face' as its face. - -\(fn HEADER VALUE FACE [HEADER VALUE FACE] [HEADER VALUE FACE] ...)" - (while args - (insert - (propertize (pop args) 'face 'sx-question-mode-header) - (propertize (pop args) 'face (pop args))))) - - -;;;;; Font-locking the content -(defvar sx-question-mode-bullet-appearance - (propertize (if (char-displayable-p ?•) " •" " *") - 'face 'markdown-list-face) - "String to be displayed as the bullet of markdown list items.") - -(defun sx-question-mode--fill-and-fontify (text) - "Return TEXT filled according to `markdown-mode'." - (with-temp-buffer - (insert text) - (markdown-mode) - (font-lock-mode -1) - (when sx-question-mode-bullet-appearance - (font-lock-add-keywords ;; Bullet items. - nil - `((,(rx line-start (0+ blank) (group-n 1 (any "*+-")) blank) - 1 '(face nil display ,sx-question-mode-bullet-appearance) prepend)))) - (font-lock-add-keywords ;; Highlight usernames. - nil - `((,(rx (or blank line-start) - (group-n 1 (and "@" (1+ (or (syntax word) (syntax symbol))))) - symbol-end) - 1 font-lock-builtin-face))) - ;; Everything. - (font-lock-fontify-region (point-min) (point-max)) - ;; Compact links. - (sx-question-mode--process-links-in-buffer) - ;; And now the filling - (goto-char (point-min)) - (while (null (eobp)) - ;; Don't fill pre blocks. - (unless (sx-question-mode--dont-fill-here) - (skip-chars-forward "\r\n[:blank:]") - (fill-paragraph) - (forward-paragraph))) - (buffer-string))) - -(defvar sx-question-mode--reference-regexp - (rx line-start (0+ blank) "[%s]:" (0+ blank) - (group-n 1 (1+ (not blank)))) - "Regexp used to find the url of labeled links. -E.g.: - [1]: https://...") - -(defun sx-question-mode--dont-fill-here () - "If text shouldn't be filled here, return t and skip over it." - (or (sx-question-mode--move-over-pre) - ;; Skip headers and references - (let ((pos (point))) - (skip-chars-forward "\r\n[:blank:]") - (goto-char (line-beginning-position)) - (if (or (looking-at-p (format sx-question-mode--reference-regexp ".+")) - (looking-at-p "^#")) - ;; Returns non-nil - (forward-paragraph) - ;; Go back and return nil - (goto-char pos) - nil)))) - -(defvar sx-question-mode--link-regexp - ;; Done at compile time. - (rx "[" (group-n 1 (1+ (not (any "]")))) "]" - (or (and "(" (group-n 2 (1+ (not (any ")")))) ")") - (and "[" (group-n 3 (1+ (not (any "]")))) "]"))) - "Regexp matching markdown links.") - -(defun sx-question-mode--process-links-in-buffer () - "Turn all markdown links in this buffer into compact format." - (save-excursion - (goto-char (point-min)) - (while (search-forward-regexp sx-question-mode--link-regexp nil t) - (let* ((text (match-string-no-properties 1)) - (url (or (match-string-no-properties 2) - (sx-question-mode-find-reference - (match-string-no-properties 3) - text)))) - (replace-match - (sx-question-mode--propertize-link - (if sx-question-mode-pretty-links - text - (match-string-no-properties 0)) - url) - :fixedcase :literal nil 0))))) - -(defun sx-question-mode--propertize-link (text url) - "Return a link propertized version of string TEXT. -URL is used as 'help-echo and 'url properties." - (propertize - text - ;; Mouse-over - 'help-echo (format - (propertize "URL: %s, %s to visit" 'face 'minibuffer-prompt) - (propertize url 'face 'default) - (propertize "RET" 'face 'font-lock-function-name-face)) - ;; In case we need it. - 'url url - ;; Decoration - 'face 'link - 'mouse-face 'highlight - ;; So RET works - 'button t - ;; So mouse works - 'follow-link t - ;; What RET calls - 'action #'sx-question-mode-follow-link)) - -(defun sx-question-mode-follow-link (&optional pos) - "Follow link at POS. If POS is nil, use `point'." - (interactive) - (browse-url - (or (get-text-property (or pos (point)) 'url) - (user-error "No url under point: %s" (or pos (point)))))) - -(defun sx-question-mode-find-reference (id &optional fallback-id) - "Find url identified by reference ID in current buffer. -If ID is nil, use FALLBACK-ID instead." - (save-excursion - (save-match-data - (goto-char (point-min)) - (when (search-forward-regexp - (format sx-question-mode--reference-regexp - (or id fallback-id)) - nil t) - (match-string-no-properties 1))))) - -(defun sx-question-mode--move-over-pre () - "Non-nil if paragraph at point can be filled." - (markdown-match-pre-blocks - (save-excursion - (skip-chars-forward "\r\n[:blank:]") - (point)))) - - ;;; Movement commands ;; Sections are headers placed above a question's content or an ;; answer's content, or above the list of comments. They are @@ -591,8 +172,7 @@ Letters do not insert themselves; instead, they are commands. (font-lock-mode -1) (remove-hook 'after-change-functions 'markdown-check-change-for-wiki-link t) (remove-hook 'window-configuration-change-hook - 'markdown-fontify-buffer-wiki-links t) - (read-only-mode)) + 'markdown-fontify-buffer-wiki-links t)) (mapc (lambda (x) (define-key sx-question-mode-map @@ -611,8 +191,7 @@ Letters do not insert themselves; instead, they are commands. ([tab] forward-button) (,(kbd "<S-iso-lefttab>") backward-button) (,(kbd "<S-tab>") backward-button) - (,(kbd "<backtab>") backward-button) - ([return] push-button))) + (,(kbd "<backtab>") backward-button))) (defun sx-question-mode-refresh (&optional no-update) "Refresh currently displayed question. @@ -644,3 +223,7 @@ query the api." (provide 'sx-question-mode) ;;; sx-question-mode.el ends here + +;; Local Variables: +;; lexical-binding: t +;; End: diff --git a/sx-question-print.el b/sx-question-print.el new file mode 100644 index 0000000..0959f36 --- /dev/null +++ b/sx-question-print.el @@ -0,0 +1,440 @@ +;;; sx-question-print.el --- Populating the question-mode buffer with content. + +;; Copyright (C) 2014 Artur Malabarba + +;; Author: Artur Malabarba <bruce.connor.am@gmail.com> + +;; 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + + +;;; Code: +(require 'markdown-mode) +(require 'button) +(eval-when-compile + (require 'rx)) + +(require 'sx) +(require 'sx-question) + +(defgroup sx-question-mode nil + "Customization group for sx-question-mode." + :prefix "sx-question-mode-" + :tag "SX Question Mode" + :group 'sx) + +(defgroup sx-question-mode-faces nil + "Customization group for the faces of `sx-question-mode'." + :prefix "sx-question-mode-" + :tag "SX Question Mode Faces" + :group 'sx-question-mode) + + +;;; Faces and Variables + +(defface sx-question-mode-header + '((t :inherit font-lock-variable-name-face)) + "Face used on the question headers in the question buffer." + :group 'sx-question-mode-faces) + +(defface sx-question-mode-title + '((t :weight bold :inherit default)) + "Face used on the question title in the question buffer." + :group 'sx-question-mode-faces) + +(defface sx-question-mode-title-comments + '((t :inherit sx-question-mode-title)) + "Face used on the question title in the question buffer." + :group 'sx-question-mode-faces) + +(defcustom sx-question-mode-header-title "\n" + "String used before the question title at the header." + :type 'string + :group 'sx-question-mode) + +(defface sx-question-mode-author + '((t :inherit font-lock-string-face)) + "Face used on the question author in the question buffer." + :group 'sx-question-mode-faces) + +(defcustom sx-question-mode-header-author "\nAuthor: " + "String used before the question author at the header." + :type 'string + :group 'sx-question-mode) + +(defface sx-question-mode-date + '((t :inherit font-lock-string-face)) + "Face used on the question date in the question buffer." + :group 'sx-question-mode-faces) + +(defcustom sx-question-mode-header-date "\nAsked on: " + "String used before the question date at the header." + :type 'string + :group 'sx-question-mode) + +(defface sx-question-mode-tags + '((t :underline nil :inherit font-lock-function-name-face)) + "Face used on the question tags in the question buffer." + :group 'sx-question-mode-faces) + +(defface sx-question-mode-author + '((t :inherit font-lock-variable-name-face)) + "Face used for author names in the question buffer." + :group 'sx-question-mode-faces) + +(defface sx-question-mode-score + '((t)) + "Face used for the score in the question buffer." + :group 'sx-question-mode-faces) + +(defface sx-question-mode-score-downvoted + '((t :inherit (font-lock-warning-face sx-question-mode-score))) + "Face used for downvoted score in the question buffer." + :group 'sx-question-mode-faces) + +(defface sx-question-mode-score-upvoted + '((t :weight bold + :inherit (font-lock-function-name-face sx-question-mode-score))) + "Face used for downvoted score in the question buffer." + :group 'sx-question-mode-faces) + +(defcustom sx-question-mode-header-tags "\nTags: " + "String used before the question tags at the header." + :type 'string + :group 'sx-question-mode) + +(defcustom sx-question-mode-header-score "\nScore: " + "String used before the question score at the header." + :type 'string + :group 'sx-question-mode) + +(defface sx-question-mode-content-face + '((((background dark)) :background "#090909") + (((background light)) :background "#f4f4f4")) + "Face used on the question body in the question buffer. +This shouldn't have a foreground, or this will interfere with +font-locking." + :group 'sx-question-mode-faces) + +(defcustom sx-question-mode-last-edit-format " (edited %s ago by %s)" + "Format used to describe last edit date in the header. +First \"%s\" is replaced with the date and the second \"%s\" with +the editor's name." + :type 'string + :group 'sx-question-mode) + +(defcustom sx-question-mode-separator + (concat (make-string 80 ?_) "\n") + "Separator used between header and body." + :type 'string + :group 'sx-question-mode) + +(defcustom sx-question-mode-answer-title "Answer" + "Title used at the start of \"Answer\" sections." + :type 'string + :group 'sx-question-mode) + +(defcustom sx-question-mode-comments-title " Comments" + "Title used at the start of \"Comments\" sections." + :type 'string + :group 'sx-question-mode) + +(defcustom sx-question-mode-comments-format "%s: %s\n" + "Format used to display comments. +First \"%s\" is replaced with user name. Second \"%s\" is +replaced with the comment." + :type 'string + :group 'sx-question-mode) + +(defcustom sx-question-mode-pretty-links t + "If non-nil, markdown links are displayed in a compact form." + :type 'boolean + :group 'sx-question-mode) + + +;;; Buttons +(define-button-type 'sx-question-mode-title + 'face 'sx-question-mode-title + 'action #'sx-question-mode-hide-show-section + 'help-echo 'sx-question-mode--section-help-echo + 'follow-link t) + +(define-button-type 'sx-question-mode-link + 'follow-link t + 'action #'sx-question-mode-follow-link) + + +;;; Functions +;;;; Printing the general structure +(defvar sx-question-mode--section-help-echo + (format + (propertize "%s to hide/display content" 'face 'minibuffer-prompt) + (propertize "RET" 'face 'font-lock-function-name-face)) + "Help echoed in the minibuffer when point is on a section.") + +(defun sx-question-mode--print-question (question) + "Print a buffer describing QUESTION. +QUESTION must be a data structure returned by `json-read'." + (setq sx-question-mode--data question) + ;; Clear the overlays + (mapc #'delete-overlay sx--overlays) + (setq sx--overlays nil) + ;; Print everything + (sx-question-mode--print-section question) + (sx-assoc-let question + (mapc #'sx-question-mode--print-section .answers)) + (goto-char (point-min)) + (sx-question-mode-next-section)) + +(defun sx-question-mode--print-section (data) + "Print a section corresponding to DATA. +DATA can represent a question or an answer." + ;; This makes `data' accessible through `sx--data-here'. + (sx-assoc-let data + (sx--wrap-in-text-property + (list 'sx--data-here data) + (insert sx-question-mode-header-title) + (insert-text-button + ;; Questions have title, Answers don't + (or .title sx-question-mode-answer-title) + ;; Section level + 'sx-question-mode--section (if .title 1 2) + :type 'sx-question-mode-title) + ;; Sections can be hidden with overlays + (sx--wrap-in-overlay + '(sx-question-mode--section-content t) + (sx-question-mode--insert-header + ;; Author + sx-question-mode-header-author + (sx-question-mode--propertize-display-name .owner) + 'sx-question-mode-author + ;; Date + sx-question-mode-header-date + (concat + (sx-time-seconds-to-date .creation_date) + (when .last_edit_date + (format sx-question-mode-last-edit-format + (sx-time-since .last_edit_date) + (sx-question-mode--propertize-display-name .last_editor)))) + 'sx-question-mode-date) + (sx-question-mode--insert-header + sx-question-mode-header-score + (format "%s" .score) + (cond + ((eq .upvoted t) 'sx-question-mode-score-upvoted) + ((eq .downvoted t) 'sx-question-mode-score-downvoted) + (t 'sx-question-mode-score))) + (when .title + ;; Tags + (sx-question-mode--insert-header + sx-question-mode-header-tags + (mapconcat #'sx-question--tag-format .tags " ") + 'sx-question-mode-tags)) + ;; Body + (insert "\n" + (propertize sx-question-mode-separator + 'face 'sx-question-mode-header + 'sx-question-mode--section 4)) + (sx--wrap-in-overlay + '(face sx-question-mode-content-face) + (insert "\n" + (sx-question-mode--fill-and-fontify + .body_markdown) + "\n" + (propertize sx-question-mode-separator + 'face 'sx-question-mode-header))))) + ;; Comments have their own `sx--data-here' property (so they can + ;; be upvoted too). + (when .comments + (insert "\n") + (insert-text-button + sx-question-mode-comments-title + 'face 'sx-question-mode-title-comments + 'sx-question-mode--section 3 + :type 'sx-question-mode-title) + (sx--wrap-in-overlay + '(sx-question-mode--section-content t) + (insert "\n") + (sx--wrap-in-overlay + '(face sx-question-mode-content-face) + (mapc #'sx-question-mode--print-comment .comments)))))) + +(defun sx-question-mode--propertize-display-name (author) + "Return display_name of AUTHOR with `sx-question-mode-author' face." + (sx-assoc-let author + (propertize .display_name + 'face 'sx-question-mode-author))) + +(defun sx-question-mode--print-comment (comment-data) + "Print the comment described by alist COMMENT-DATA. +The comment is indented, filled, and then printed according to +`sx-question-mode-comments-format'." + (sx--wrap-in-text-property + (list 'sx--data-here comment-data) + (sx-assoc-let comment-data + (insert + (format + sx-question-mode-comments-format + (sx-question-mode--propertize-display-name .owner) + (substring + ;; We fill with three spaces at the start, so the comment is + ;; slightly indented. + (sx-question-mode--fill-and-fontify + (concat " " .body_markdown)) + ;; Then we remove the spaces from the first line, since we'll + ;; add the username there anyway. + 3)))))) + +(defun sx-question-mode--insert-header (&rest args) + "Insert propertized ARGS. +ARGS is a list of repeating values -- `header', `value', and +`face'. `header' is given `sx-question-mode-header' as a face, +where `value' is given `face' as its face. + +\(fn HEADER VALUE FACE [HEADER VALUE FACE] [HEADER VALUE FACE] ...)" + (while args + (insert + (propertize (pop args) 'face 'sx-question-mode-header) + (propertize (pop args) 'face (pop args))))) + + +;;;; Printing and Font-locking the content (body) +(defvar sx-question-mode-bullet-appearance + (propertize (if (char-displayable-p ?•) " •" " *") + 'face 'markdown-list-face) + "String to be displayed as the bullet of markdown list items.") + +(defvar sx-question-mode--reference-regexp + (rx line-start (0+ blank) "[%s]:" (0+ blank) + (group-n 1 (1+ (not blank)))) + "Regexp used to find the url of labeled links. +E.g.: + [1]: https://...") + +(defvar sx-question-mode--link-regexp + ;; Done at compile time. + (rx "[" (group-n 1 (1+ (not (any "]")))) "]" + (or (and "(" (group-n 2 (1+ (not (any ")")))) ")") + (and "[" (group-n 3 (1+ (not (any "]")))) "]"))) + "Regexp matching markdown links.") + +(defun sx-question-mode--fill-and-fontify (text) + "Return TEXT filled according to `markdown-mode'." + (with-temp-buffer + (insert text) + (markdown-mode) + (font-lock-mode -1) + (when sx-question-mode-bullet-appearance + (font-lock-add-keywords ;; Bullet items. + nil + `((,(rx line-start (0+ blank) (group-n 1 (any "*+-")) blank) + 1 '(face nil display ,sx-question-mode-bullet-appearance) prepend)))) + (font-lock-add-keywords ;; Highlight usernames. + nil + `((,(rx (or blank line-start) + (group-n 1 (and "@" (1+ (or (syntax word) (syntax symbol))))) + symbol-end) + 1 font-lock-builtin-face))) + ;; Everything. + (font-lock-fontify-region (point-min) (point-max)) + ;; Compact links. + (sx-question-mode--process-links-in-buffer) + ;; And now the filling + (goto-char (point-min)) + (while (null (eobp)) + ;; Don't fill pre blocks. + (unless (sx-question-mode--dont-fill-here) + (skip-chars-forward "\r\n[:blank:]") + (fill-paragraph) + (forward-paragraph))) + (buffer-string))) + +(defun sx-question-mode--dont-fill-here () + "If text shouldn't be filled here, return t and skip over it." + (or (sx-question-mode--move-over-pre) + ;; Skip headers and references + (let ((pos (point))) + (skip-chars-forward "\r\n[:blank:]") + (goto-char (line-beginning-position)) + (if (or (looking-at-p (format sx-question-mode--reference-regexp ".+")) + (looking-at-p "^#")) + ;; Returns non-nil + (forward-paragraph) + ;; Go back and return nil + (goto-char pos) + nil)))) + +(defun sx-question-mode--process-links-in-buffer () + "Turn all markdown links in this buffer into compact format." + (save-excursion + (goto-char (point-min)) + (while (search-forward-regexp sx-question-mode--link-regexp nil t) + (let* ((text (match-string-no-properties 1)) + (url (or (match-string-no-properties 2) + (sx-question-mode-find-reference + (match-string-no-properties 3) + text))) + (full-text (match-string-no-properties 0))) + (replace-match "") + (sx-question-mode--insert-link + (if sx-question-mode-pretty-links text full-text) + url))))) + +(defun sx-question-mode--insert-link (text url) + "Return a link propertized version of string TEXT. +URL is used as 'help-echo and 'url properties." + (insert-text-button + text + ;; Mouse-over + 'help-echo + (format (propertize "URL: %s, %s to visit" 'face 'minibuffer-prompt) + (propertize url 'face 'default) + (propertize "RET" 'face 'font-lock-function-name-face)) + ;; For visiting and stuff. + 'url url + :type 'sx-question-mode-link)) + +(defun sx-question-mode-follow-link (&optional pos) + "Follow link at POS. If POS is nil, use `point'." + (interactive) + (browse-url + (or (get-text-property (or pos (point)) 'url) + (user-error "No url under point: %s" (or pos (point)))))) + +(defun sx-question-mode-find-reference (id &optional fallback-id) + "Find url identified by reference ID in current buffer. +If ID is nil, use FALLBACK-ID instead." + (save-excursion + (save-match-data + (goto-char (point-min)) + (when (search-forward-regexp + (format sx-question-mode--reference-regexp + (or id fallback-id)) + nil t) + (match-string-no-properties 1))))) + +(defun sx-question-mode--move-over-pre () + "Non-nil if paragraph at point can be filled." + (markdown-match-pre-blocks + (save-excursion + (skip-chars-forward "\r\n[:blank:]") + (point)))) + +(provide 'sx-question-print) +;;; sx-question-print.el ends here + +;; Local Variables: +;; lexical-binding: t +;; End: diff --git a/sx-question.el b/sx-question.el index a7aadb2..f80a9bd 100644 --- a/sx-question.el +++ b/sx-question.el @@ -1,4 +1,4 @@ -;;; sx-question.el --- question logic for stack-mode -*- lexical-binding: t; -*- +;;; sx-question.el --- question logic ;; Copyright (C) 2014 Sean Allred @@ -134,20 +134,21 @@ If no cache exists for it, initialize one with SITE." (defun sx-question--mark-hidden (question) "Mark QUESTION as being hidden." - (let ((site-cell (assoc .site sx-question--user-hidden-list)) - cell) - ;; If question already hidden, do nothing. - (unless (memq .question_id site-cell) - ;; First question from this site. - (push (list .site .question_id) sx-question--user-hidden-list) - ;; Question wasn't present. - ;; Add it in, but make sure it's sorted (just in case we need - ;; it later). - (sx-sorted-insert-skip-first .question_id site-cell >) - ;; This causes a small lag on `j' and `k' as the list gets large. - ;; Should we do this on a timer? - ;; Save the results. - (sx-cache-set 'hidden-questions sx-question--user-hidden-list)))) + (sx-assoc-let question + (let ((site-cell (assoc .site sx-question--user-hidden-list)) + cell) + ;; If question already hidden, do nothing. + (unless (memq .question_id site-cell) + ;; First question from this site. + (push (list .site .question_id) sx-question--user-hidden-list) + ;; Question wasn't present. + ;; Add it in, but make sure it's sorted (just in case we need + ;; it later). + (sx-sorted-insert-skip-first .question_id site-cell >) + ;; This causes a small lag on `j' and `k' as the list gets large. + ;; Should we do this on a timer? + ;; Save the results. + (sx-cache-set 'hidden-questions sx-question--user-hidden-list))))) ;;;; Other data @@ -167,4 +168,5 @@ If no cache exists for it, initialize one with SITE." ;; Local Variables: ;; indent-tabs-mode: nil +;; lexical-binding: t ;; End: diff --git a/sx-request.el b/sx-request.el index 6b09988..6be363d 100644 --- a/sx-request.el +++ b/sx-request.el @@ -71,7 +71,7 @@ "gunzip" "Program used to unzip the response if it is compressed. This program must accept compressed data on standard input." - :group 'sx-request + :group 'sx :type 'string) (defvar sx-request-remaining-api-requests @@ -85,15 +85,14 @@ Set by `sx-request-make'.") After `sx-request-remaining-api-requests' drops below this number, `sx-request-make' will begin printing out the number of requests left every time it finishes a call." - :group 'sx-request + :group 'sx :type 'integer) ;;; Making Requests -(defun sx-request-make - (method &optional args request-method) - "Make a request to the API, executing METHOD with ARGS. +(defun sx-request-make (method &optional args request-method) + "Make a request to the API, executing METHOD with ARGS. You should almost certainly be using `sx-method-call' instead of this function. REQUEST-METHOD is one of `GET' (default) or `POST'. @@ -116,8 +115,7 @@ then read with `json-read-from-string'. the main content of the response is returned." (let* ((url-automatic-caching t) (url-inhibit-uncompression t) - (url-request-data (sx-request--build-keyword-arguments args - nil)) + (url-request-data (sx-request--build-keyword-arguments args nil)) (request-url (concat sx-request-api-root method)) (url-request-method request-method) (url-request-extra-headers @@ -168,15 +166,11 @@ Currently returns nil." ;;; Support Functions - -(defun sx-request--build-keyword-arguments (alist &optional - kv-sep need-auth) +(defun sx-request--build-keyword-arguments (alist &optional kv-sep) "Format ALIST as a key-value list joined with KV-SEP. If authentication is needed, include it also or error if it is not available. -If NEED-AUTH is non-nil, authentication is required. - Build a \"key=value&key=value&...\"-style string with the elements of ALIST. If any value in the alist is nil, that pair will not be included in the return. If you wish to pass a notion of @@ -185,7 +179,7 @@ false, use the symbol `false'. Each element is processed with ;; Add API key to list of arguments, this allows for increased quota ;; automatically. (let ((api-key (cons "key" sx-request-api-key)) - (auth (car (sx-cache-get 'auth)))) + (auth (car (sx-cache-get 'auth)))) (push api-key alist) (when auth (push auth alist)) @@ -57,7 +57,7 @@ "List of favorite sites. Each entry is a string corresponding to a single site's api_site_parameter." - :group 'sx-site) + :group 'sx) (defun sx-site-get-api-tokens () "Return a list of all known site tokens." @@ -30,11 +30,11 @@ (defcustom sx-tab-default-site "emacs" "Name of the site to use by default when listing questions." :type 'string - :group 'stack-exchange) + :group 'sx) (defmacro sx-tab--define (tab pager &optional printer refresher &rest body) - "Define a stack-exchange tab called TAB. + "Define a StackExchange tab called TAB. TAB is a capitalized string. This defines a command `sx-tab-TAB' for displaying the tab, @@ -97,6 +97,13 @@ If SITE is nil, use `sx-tab-default-site'." (lambda (page) (sx-question-get-questions sx-question-list--site page))) +;;;###autoload +(autoload 'sx-tab-frontpage + (expand-file-name + "sx-tab" + (when load-file-name + (file-name-directory load-file-name))) + nil t) (provide 'sx-tab) ;;; sx-tab.el ends here @@ -1,4 +1,4 @@ -;;; sx-time.el --- time for stack-mode +;;; sx-time.el --- time ;; Copyright (C) 2014 Sean Allred @@ -57,13 +57,13 @@ "Format used for dates on a past year. See also `sx-time-date-format'." :type 'string - :group 'sx-time) + :group 'sx) (defcustom sx-time-date-format "%H:%M - %d %b" "Format used for dates on this year. See also `sx-time-date-format-year'." :type 'string - :group 'sx-time) + :group 'sx) (defun sx-time-seconds-to-date (seconds) "Return the integer SECONDS as a date string." @@ -1,9 +1,9 @@ -;;; sx.el --- Core functions of the sx package. -*- lexical-binding: t; -*- +;;; sx.el --- core functions of the sx package. ;; Copyright (C) 2014 Sean Allred ;; Author: Sean Allred <code@seanallred.com> -;; URL: https://github.com/vermiculus/stack-mode/ +;; URL: https://github.com/vermiculus/sx.el/ ;; Version: 0.1 ;; Keywords: help, hypermedia, tools ;; Package-Requires: ((emacs "24.1") (cl-lib "0.5") (json "1.3") (markdown-mode "2.0")) @@ -23,14 +23,20 @@ ;;; Commentary: -;; This file defines basic commands used by all other parts of -;; StackMode. +;; This file defines basic commands used by all other parts of SX. ;;; Code: (require 'tabulated-list) (defconst sx-version "0.1" "Version of the `sx' package.") +(defgroup sx nil + "Customization group for sx-question-mode." + :prefix "sx-" + :tag "SX" + :group 'applications) + + ;;; User commands (defun sx-version () @@ -43,7 +49,7 @@ (defun sx-bug-report () "File a bug report about the `sx' package." (interactive) - (browse-url "https://github.com/vermiculus/stack-mode/issues/new")) + (browse-url "https://github.com/vermiculus/sx.el/issues/new")) ;;; Browsing filter @@ -100,7 +106,7 @@ is intentionally skipped." (defun sx-message (format-string &rest args) "Display FORMAT-STRING as a message with ARGS. See `format'." - (message "[stack] %s" (apply #'format format-string args))) + (message "[sx] %s" (apply #'format format-string args))) (defun sx-message-help-echo () "If there's a 'help-echo property under point, message it." @@ -265,13 +271,13 @@ is equivalent to ,@body))) (defcustom sx-init-hook nil - "Hook run when stack-mode initializes. + "Hook run when SX initializes. Run after `sx-init--internal-hook'." :group 'sx :type 'hook) (defvar sx-init--internal-hook nil - "Hook run when stack-mode initializes. + "Hook run when SX initializes. This is used internally to set initial values for variables such as filters.") @@ -313,4 +319,5 @@ If FORCE is non-nil, run them even if they've already been run." ;; Local Variables: ;; indent-tabs-mode: nil +;; lexical-binding: t ;; End: @@ -4,7 +4,7 @@ #+TITLE: SX: A StackExchange Client (v{{{version}}}) #+DATE: 16 November 2014 -#+AUTHOR: @@texinfo:@url{@@www.github.com/vermiculus/stack-mode@@texinfo:}@@ +#+AUTHOR: @@texinfo:@url{@@www.github.com/vermiculus/sx.el@@texinfo:}@@ #+LANGUAGE: en #+OPTIONS: ':t toc:t |