aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Allred <code@seanallred.com>2014-11-20 21:24:19 -0600
committerSean Allred <code@seanallred.com>2014-11-20 21:24:19 -0600
commit1dfd91e7373160854eeb85582598e6c8cc1b3561 (patch)
treec7050cf510f00ea005b76395ff07064ec04bbef0
parent681319aeb250a83d982d1e3e02264a7af0ae4120 (diff)
parentfd6b8111a13c042e5d0f2f3b689043c394c6e52d (diff)
Merge pull request #77 from vermiculus/documentation
Documentation
-rw-r--r--.agignore20
-rw-r--r--.gitignore2
-rw-r--r--sx-auth.el27
-rw-r--r--sx-cache.el41
-rw-r--r--sx-encoding.el87
-rw-r--r--sx-favorites.el5
-rw-r--r--sx-filter.el33
-rw-r--r--sx-method.el23
-rw-r--r--sx-networks.el6
-rw-r--r--sx-question-list.el59
-rw-r--r--sx-question-mode.el82
-rw-r--r--sx-question.el61
-rw-r--r--sx-request.el112
-rw-r--r--sx-site.el10
-rw-r--r--sx.el53
-rw-r--r--sx.org126
-rw-r--r--test/tests.el1
17 files changed, 522 insertions, 226 deletions
diff --git a/.agignore b/.agignore
new file mode 100644
index 0000000..e00db68
--- /dev/null
+++ b/.agignore
@@ -0,0 +1,20 @@
+# Backup files
+*~
+\#*\#
+
+# Compiled Elisp
+*.elc
+
+# Generated by tests
+/.cask/
+/.stackmode/
+/url/
+
+# User-local variables
+.dir-locals.el
+
+# Test files
+test/data-samples
+
+# Info files
+*.info
diff --git a/.gitignore b/.gitignore
index 2585bf5..cfaa152 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@
.dir-locals.el
/.stackmode/
/url/
+/sx.info
+/sx.texi
diff --git a/sx-auth.el b/sx-auth.el
index b470523..14453ac 100644
--- a/sx-auth.el
+++ b/sx-auth.el
@@ -19,8 +19,6 @@
;;; Commentary:
-;;
-
;;; Code:
(require 'sx)
@@ -36,17 +34,34 @@
(defvar sx-auth-access-token
nil
"Your access token.
-
This is needed to use your account to write questions, make
comments, and read your inbox. Do not alter this unless you know
what you are doing!")
(defun sx-auth-authenticate ()
"Authenticate this application.
-
Authentication is required to read your personal data (such as
notifications) and to write with the API (asking and answering
-questions)."
+questions).
+
+When this function is called, `browse-url' is used to send the
+user to an authorization page managed by StackExchange. The
+following privileges are requested:
+
+* read_inbox
+ use SX to manage and visit items in your inbox
+
+* write_acesss
+ write comments, ask questions, and post answers on your
+ behalf
+
+* no_expiry
+ do not pester you to reauthorize again
+
+After authorization with StackExchange, the user is then
+redirected to a website managed by SX. The access token required
+to use authenticated methods is included in the hash (which is
+parsed and displayed prominently on the page)."
(interactive)
(setq
sx-auth-access-token
@@ -68,6 +83,8 @@ questions)."
(error "You must enter this code to use this client fully"))
(sx-cache-set 'auth `((access_token . ,sx-auth-access-token)))))
+(defalias 'sx-authenticate #'sx-auth-authenticate)
+
(provide 'sx-auth)
;;; sx-auth.el ends here
diff --git a/sx-cache.el b/sx-cache.el
index a564a53..9f152e2 100644
--- a/sx-cache.el
+++ b/sx-cache.el
@@ -30,39 +30,44 @@
(defcustom sx-cache-directory
(expand-file-name ".stackmode" user-emacs-directory)
- "Directory containined cached files and precompiled filters.")
+ "Directory containing cached data."
+ :type 'directory
+ :group 'sx-cache)
+
+(defun sx-cache--ensure-sx-cache-directory-exists ()
+ "Ensure `sx-cache-directory' exists."
+ (unless (file-exists-p sx-cache-directory)
+ (mkdir sx-cache-directory)))
(defun sx-cache-get-file-name (filename)
- "Expands FILENAME in the context of `sx-cache-directory'."
+ "Expand FILENAME in the context of `sx-cache-directory'."
(expand-file-name
(concat (symbol-name filename) ".el")
sx-cache-directory))
(defun sx-cache-get (cache &optional form)
"Return the data within CACHE.
+If CACHE does not exist, use `sx-cache-set' to set CACHE to the
+result of evaluating FORM.
-If CACHE does not exist, evaluate FORM and set it to its return.
-
-As with `sx-cache-set', CACHE is a file name within the
-context of `sx-cache-directory'."
- (unless (file-exists-p sx-cache-directory)
- (mkdir sx-cache-directory))
+CACHE is resolved to a file name by `sx-cache-get-file-name'."
+ (sx-cache--ensure-sx-cache-directory-exists)
(let ((file (sx-cache-get-file-name cache)))
+ ;; If the file exists, return the data it contains
(if (file-exists-p file)
(with-temp-buffer
(insert-file-contents (sx-cache-get-file-name cache))
(read (buffer-string)))
+ ;; Otherwise, set CACHE to the evaluation of FORM.
+ ;; `sx-cache-set' returns the data that CACHE was set to.
(sx-cache-set cache (eval form)))))
(defun sx-cache-set (cache data)
- "Set the content of CACHE to DATA.
+ "Set the content of CACHE to DATA and save.
+DATA will be written as returned by `prin1'.
-As with `sx-cache-get', CACHE is a file name within the
-context of `sx-cache-directory'.
-
-DATA will be written as returned by `prin1'."
- (unless (file-exists-p sx-cache-directory)
- (mkdir sx-cache-directory))
+CACHE is resolved to a file name by `sx-cache-get-file-name'."
+ (sx-cache--ensure-sx-cache-directory-exists)
(write-region (prin1-to-string data) nil
(sx-cache-get-file-name cache))
data)
@@ -79,10 +84,8 @@ re-initialize the cache."
(defun sx-cache-invalidate-all (&optional save-auth)
"Invalidate all caches using `sx-cache--invalidate'.
-
-Afterwards reinitialize caches using `sx-initialize'.
-
-If SAVE-AUTH is non-nil, do not clear AUTH cache."
+Afterwards reinitialize caches using `sx-initialize'. If
+SAVE-AUTH is non-nil, do not clear AUTH cache."
(let ((caches (let ((default-directory sx-cache-directory))
(file-expand-wildcards "*.el"))))
(when save-auth
diff --git a/sx-encoding.el b/sx-encoding.el
index 9d48e60..f683615 100644
--- a/sx-encoding.el
+++ b/sx-encoding.el
@@ -19,8 +19,6 @@
;;; Commentary:
-;;
-
;;; Code:
(require 'cl-lib)
@@ -62,28 +60,45 @@
ucirc "û" Ucirc "Û" ugrave "ù" Ugrave "Ù" uml "¨" upsih "ϒ" Upsilon "Υ"
upsilon "υ" uuml "ü" Uuml "Ü" weierp "℘" Xi "Ξ" xi "ξ" yacute "ý"
Yacute "Ý" yen "¥" yuml "ÿ" Yuml "Ÿ" Zeta "Ζ" zeta "ζ" zwj "" zwnj "")
- "Plist of html entities to replace when displaying question titles and other text."
+ "Plist of HTML entities and their respective glyphs.
+See `sx-encoding-decode-entities'."
:type '(repeat (choice symbol string))
:group 'sx)
(defun sx-encoding-decode-entities (string)
+ "Decode HTML entities (e.g. \"&quot;\") in STRING.
+
+Done according to `sx-encoding-html-entities-plist'. If this
+list does not contain the entity, it is assumed to be a number
+and converted to a string (with `char-to-string').
+
+Return the decoded string."
(let* ((plist sx-encoding-html-entities-plist)
- (get-function (lambda (s) (let ((ss (substring s 1 -1)))
- ;; Handle things like &quot;
- (or (plist-get plist (intern ss))
- ;; Handle things like &#39;
- (format "%c" (string-to-number
- (substring ss 1))))))))
+ (get-function
+ (lambda (s)
+ (let ((ss (substring s 1 -1)))
+ ;; Handle things like &quot;
+ (or (plist-get plist (intern ss))
+ ;; Handle things like &#39;
+ (char-to-string
+ (string-to-number
+ ;; Skip the `#'
+ (substring ss 1))))))))
(replace-regexp-in-string "&[^; ]*;" get-function string)))
(defun sx-encoding-normalize-line-endings (string)
- "Normalize the line endings for STRING"
+ "Normalize the line endings for STRING.
+The API returns strings that use Windows-style line endings.
+These are largely useless in an Emacs environment. Windows uses
+\"\\r\\n\", Unix uses just \"\\n\". Deleting \"\\r\" is sufficient for
+conversion."
(delete ?\r string))
(defun sx-encoding-clean-content (string)
- "Cleans STRING for display.
+ "Clean STRING for display.
Applies `sx-encoding-normalize-line-endings' and
-`sx-encoding-decode-entities'."
+`sx-encoding-decode-entities' (in that order) to prepare STRING
+for sane display."
(sx-encoding-decode-entities
(sx-encoding-normalize-line-endings
string)))
@@ -91,17 +106,24 @@ Applies `sx-encoding-normalize-line-endings' and
(defun sx-encoding-clean-content-deep (data)
"Clean DATA recursively where necessary.
-See `sx-encoding-clean-content'."
+If DATA is a list or a vector, map this function over DATA and
+return as the the same type of structure.
+
+If DATA is a cons cell (but not a list), use
+`sx-encoding-clean-content-deep' on the `cdr' of DATA.
+
+If DATA is a string, return DATA after applying
+`sx-encoding-clean-content'.
+
+Otherwise, return DATA.
+
+This function is highly specialized for the data structures
+returned by `json-read' via `sx-request-make'. It may fail in
+some cases."
(if (consp data)
- ;; If we're looking at a cons cell, test to see if is a list. If
- ;; it is, map ourselves over the entire list. If it is not,
- ;; reconstruct the cons cell using a cleaned cdr.
(if (listp (cdr data))
(cl-map #'list #'sx-encoding-clean-content-deep data)
(cons (car data) (sx-encoding-clean-content-deep (cdr data))))
- ;; If we're looking at an atom, clean and return if we're looking
- ;; at a string, map if we're looking at a vector, and just return
- ;; if we aren't looking at either.
(cond
((stringp data)
(sx-encoding-clean-content data))
@@ -110,28 +132,25 @@ See `sx-encoding-clean-content'."
(t data))))
(defun sx-encoding-gzipped-p (data)
- "Checks for magic bytes in DATA.
-
-Check if the first two bytes of a string in DATA match magic
-numbers identifying the gzip file format. See [1] for the file
-format description.
-
-http://www.gzip.org/zlib/rfc-gzip.html
+ "Check for magic bytes in DATA.
+Check if the first two bytes of a string in DATA match the magic
+numbers identifying the gzip file format.
-http://emacs.stackexchange.com/a/2978"
+See URL `http://www.gzip.org/zlib/rfc-gzip.html'."
+ ;; Credit: http://emacs.stackexchange.com/a/2978
(equal (substring (string-as-unibyte data) 0 2)
(unibyte-string 31 139)))
-(defun sx-encoding-gzipped-buffer-p (filename)
- "Check if the BUFFER is gzip-compressed.
-
-See `gzip-check-magic' for details."
- (sx-encoding-gzip-check-magic (buffer-string)))
+(defun sx-encoding-gzipped-buffer-p (buffer)
+ "Check if BUFFER is gzip-compressed.
+See `sx-encoding-gzipped-p'."
+ (with-current-buffer buffer
+ (sx-encoding-gzip-check-magic
+ (buffer-string))))
(defun sx-encoding-gzipped-file-p (file)
"Check if the FILE is gzip-compressed.
-
-See `gzip-check-magic' for details."
+See `sx-encoding-gzipped-p'."
(let ((first-two-bytes (with-temp-buffer
(set-buffer-multibyte nil)
(insert-file-contents-literally file nil 0 2)
diff --git a/sx-favorites.el b/sx-favorites.el
index 3aa96dd..71079fb 100644
--- a/sx-favorites.el
+++ b/sx-favorites.el
@@ -19,8 +19,6 @@
;;; Commentary:
-;;
-
;;; Code:
(require 'sx-method)
@@ -39,13 +37,11 @@
(defvar sx-favorites--user-favorite-list nil
"Alist of questions favorited by the user.
-
Each element has the form (SITE FAVORITE-LIST). And each element
in FAVORITE-LIST is the numerical QUESTION_ID.")
(defun sx-favorites--initialize ()
"Ensure question-favorites cache is available.
-
Added as hook to initialization."
(or (setq sx-favorites--user-favorite-list
(sx-cache-get 'question-favorites))
@@ -62,7 +58,6 @@ Added as hook to initialization."
(defun sx-favorites--update-site-favorites (site)
"Update list of starred QUESTION_IDs for SITE.
-
Writes list to cache QUESTION-FAVORITES."
(let* ((favs (sx-favorites--retrieve-favorites site))
(site-cell (assoc site
diff --git a/sx-filter.el b/sx-filter.el
index 90681e8..38084b9 100644
--- a/sx-filter.el
+++ b/sx-filter.el
@@ -19,8 +19,6 @@
;;; Commentary:
-;;
-
;;; Code:
@@ -35,25 +33,30 @@
(defvar sx--filter-alist
(sx-cache-get 'filter)
- "")
+ "An alist of known filters. See `sx-filter-compile'.
+Structure:
+
+ (((INCLUDE EXCLUDE BASE ) . \"compiled filter \")
+ ((INCLUDE2 EXCLUDE2 BASE2) . \"compiled filter2\")
+ ...)")
;;; Compilation
-;;; TODO allow BASE to be a precompiled filter name
+;;; @TODO allow BASE to be a precompiled filter name
(defun sx-filter-compile (&optional include exclude base)
"Compile INCLUDE and EXCLUDE into a filter derived from BASE.
+INCLUDE and EXCLUDE must both be lists; BASE should be a string.
-INCLUDE and EXCLUDE must both be lists; BASE should be a symbol
-or string."
+Returns the compiled filter as a string."
(let ((keyword-arguments
`((include . ,(if include (sx--thing-as-string include)))
(exclude . ,(if exclude (sx--thing-as-string exclude)))
(base . ,(if base base)))))
- (let ((response (sx-request-make
- "filter/create"
- keyword-arguments)))
- (sx-assoc-let (elt response 0)
+ (let ((response (elt (sx-request-make
+ "filter/create"
+ keyword-arguments) 0)))
+ (sx-assoc-let response
.filter))))
@@ -64,10 +67,14 @@ or string."
(apply #'sx-filter-get filter-variable))
(defun sx-filter-get (&optional include exclude base)
- "Return the string representation of the given filter."
- ;; Maybe we alreay have this filter
+ "Return the string representation of the given filter.
+
+If the filter data exist in `sx--filter-alist', that value will
+be returned. Otherwise, compile INCLUDE, EXCLUDE, and BASE into
+a filter with `sx-filter-compile' and push the association onto
+`sx--filter-alist'. Re-cache the alist with `sx-cache-set' and
+return the compiled filter."
(or (cdr (assoc (list include exclude base) sx--filter-alist))
- ;; If we don't, build it, save it, and return it.
(let ((filter (sx-filter-compile include exclude base)))
(when filter
(push (cons (list include exclude base) filter) sx--filter-alist)
diff --git a/sx-method.el b/sx-method.el
index e9c4f60..8909a2b 100644
--- a/sx-method.el
+++ b/sx-method.el
@@ -19,7 +19,10 @@
;;; Commentary:
-;;
+;;; This file is effectively a common-use wrapper for
+;;; `sx-request-make'. It provides higher-level handling such as
+;;; (authentication, filters, ...) that `sx-request-make' doesn't need
+;;; to handle.
;;; Code:
(require 'json)
@@ -29,8 +32,9 @@
(require 'sx-filter)
(defun sx-method-call
- (method &optional keyword-arguments filter need-auth use-post silent)
+ (method &optional keyword-arguments filter need-auth use-post)
"Call METHOD with KEYWORD-ARGUMENTS using FILTER.
+This is a high-level wrapper for `sx-request-make'.
If NEED-AUTH is non-nil, an auth-token is required. If 'WARN,
warn the user `(user-error ...)' if they do not have an AUTH
@@ -39,18 +43,11 @@ token set.
If USE-POST is non-nil, use `POST' rather than `GET' for passing
arguments.
-If SILENT is non-nil, no messages will be printed.
-
-Return the entire response as a complex alist."
- (sx-request-make
- method
- (cons (cons 'filter
- (sx-filter-get-var
- (cond (filter filter)
- ((boundp 'stack-filter) stack-filter))))
+Return the response content as a complex alist."
+ (sx-request-make method
+ (cons (cons 'filter (sx-filter-get-var filter))
keyword-arguments)
- need-auth
- use-post))
+ need-auth use-post))
(provide 'sx-method)
;;; sx-method.el ends here
diff --git a/sx-networks.el b/sx-networks.el
index 755d62c..6820e11 100644
--- a/sx-networks.el
+++ b/sx-networks.el
@@ -19,8 +19,6 @@
;;; Commentary:
-;;
-
;;; Code:
(require 'sx-method)
@@ -54,7 +52,6 @@
(defun sx-network--get-associated ()
"Retrieve cached information for network user.
-
If cache is not available, retrieve current data."
(or (and (setq sx-network--user-information (sx-cache-get 'network-user)
sx-network--user-sites
@@ -63,7 +60,6 @@ If cache is not available, retrieve current data."
(defun sx-network--update ()
"Update user information.
-
Sets cache and then uses `sx-network--get-associated' to update
the variables."
(sx-cache-set 'network-user
@@ -75,7 +71,6 @@ the variables."
(defun sx-network--initialize ()
"Ensure network-user cache is available.
-
Added as hook to initialization."
;; Cache was not retrieved, retrieve it.
(sx-network--get-associated))
@@ -83,7 +78,6 @@ Added as hook to initialization."
(defun sx-network--map-site-url-to-site-api ()
"Convert `me/associations' to a set of `api_site_parameter's.
-
`me/associations' does not return `api_site_parameter' so cannot
be directly used to retrieve content per site. This creates a
list of sites the user is active on."
diff --git a/sx-question-list.el b/sx-question-list.el
index be088c8..9e94536 100644
--- a/sx-question-list.el
+++ b/sx-question-list.el
@@ -161,12 +161,12 @@ Non-interactively, DATA is a question alist."
(tabulated-list-get-id)
(user-error "Not in `sx-question-list-mode'"))))
(sx-question--mark-read data)
- (sx-question-list-next 1)
+ (sx-question-list-next 1)
(when (called-interactively-p 'any)
(sx-question-list-refresh 'redisplay 'noupdate)))
(defvar sx-question-list--current-page "Latest"
- ;; Other values (once we implement them) are "Top Voted",
+ ;; @TODO Other values (once we implement them) are "Top Voted",
;; "Unanswered", etc.
"Variable describing current page being viewed.")
@@ -210,7 +210,9 @@ Non-interactively, DATA is a question alist."
"Site being displayed in the *question-list* buffer.")
(defvar sx-question-list--current-dataset nil
- "")
+ "The logical data behind the displayed list of questions.
+This dataset contains even questions that are hidden by the user,
+and thus not displayed in the list of questions.")
(defun sx-question-list-refresh (&optional redisplay no-update)
"Update the list of questions.
@@ -244,33 +246,36 @@ a new list before redisplaying."
(defcustom sx-question-list-ago-string " ago"
"String appended to descriptions of the time since something happened.
-Used in the questions list to indicate a question was updated \"4d ago\"."
+Used in the questions list to indicate a question was updated
+\"4d ago\"."
:type 'string
:group 'sx-question-list)
-(defun sx-question-list--print-info (data)
- "Convert `json-read' DATA into tabulated-list format."
- (sx-assoc-let data
+(defun sx-question-list--print-info (question-data)
+ "Convert `json-read' QUESTION-DATA into tabulated-list format.
+See `sx-question-list-refresh'."
+ (sx-assoc-let question-data
(let ((favorite (if (member .question_id
(assoc .site
sx-favorites--user-favorite-list))
(if (char-displayable-p ?\x2b26) "\x2b26" "*") " ")))
(list
- data
+ question-data
(vector
(list (int-to-string .score)
'face (if .upvoted 'sx-question-list-score-upvoted
'sx-question-list-score))
(list (int-to-string .answer_count)
- 'face (if (sx-question--accepted-answer-id data)
+ 'face (if (sx-question--accepted-answer-id question-data)
'sx-question-list-answers-accepted
'sx-question-list-answers))
(concat
(propertize
.title
- 'face (if (sx-question--read-p data)
+ 'face (if (sx-question--read-p question-data)
'sx-question-list-read-question
- ;; Increment `sx-question-list--unread-count' for the mode-line.
+ ;; Increment `sx-question-list--unread-count' for
+ ;; the mode-line.
(cl-incf sx-question-list--unread-count)
'sx-question-list-unread-question))
(propertize " " 'display "\n ")
@@ -285,32 +290,37 @@ Used in the questions list to indicate a question was updated \"4d ago\"."
(propertize " " 'display "\n")))))))
(defun sx-question-list-view-previous (n)
- "Hide this question, move to previous one, display it."
+ "Move cursor up N questions up and display this question.
+Displayed in `sx-question-mode--window', replacing any question
+that may currently be there."
(interactive "p")
(sx-question-list-view-next (- n)))
(defun sx-question-list-view-next (n)
- "Hide this question, move to next one, display it."
+ "Move cursor down N questions and display this question.
+Displayed in `sx-question-mode--window', replacing any question
+that may currently be there."
(interactive "p")
(sx-question-list-next n)
(sx-question-list-display-question))
(defun sx-question-list-next (n)
- "Move to the next entry."
+ "Move cursor down N questions.
+This does not update `sx-question-mode--window'."
(interactive "p")
(forward-line n))
(defun sx-question-list-previous (n)
- "Move to the previous entry."
+ "Move cursor up N questions.
+This does not update `sx-question-mode--window'."
(interactive "p")
(sx-question-list-next (- n)))
(defun sx-question-list-display-question (&optional data focus)
"Display question given by DATA.
-If called interactively (or with DATA being nil), display
-question under point.
-Also when called interactively (or when FOCUS is non-nil), also
-focus the relevant window."
+When DATA is nil, display question under point. When FOCUS is
+non-nil (the default when called interactively), also focus the
+relevant window."
(interactive '(nil t))
(unless data (setq data (tabulated-list-get-id)))
(unless data (error "No question here!"))
@@ -337,7 +347,7 @@ focus the relevant window."
(set-window-parameter
sx-question-mode--window
'quit-restore
- ;; See https://www.gnu.org/software/emacs/manual/html_node/elisp/Window-Parameters.html#Window-Parameters
+ ;; See (info "(elisp) Window Parameters")
`(window window ,(selected-window) ,sx-question-mode--buffer))
(when focus
(if sx-question-mode--window
@@ -345,7 +355,11 @@ focus the relevant window."
(switch-to-buffer sx-question-mode--buffer))))
(defun sx-question-list-switch-site (site)
- "Switch the current site to SITE and display its questions"
+ "Switch the current site to SITE and display its questions.
+Uses `ido-completing-read' if variable `ido-mode' is active. Retrieves
+completions from `sx-site-get-api-tokens'. Sets
+`sx-question-list--current-site' and then
+`sx-question-list-refresh' with `redisplay'."
(interactive
(list (funcall (if ido-mode #'ido-completing-read #'completing-read)
"Switch to site: " (sx-site-get-api-tokens)
@@ -359,7 +373,8 @@ focus the relevant window."
"Buffer where the list of questions is displayed.")
(defun list-questions (no-update)
- "Display a list of StackExchange questions."
+ "Display a list of StackExchange questions.
+NO-UPDATE is passed to `sx-question-list-refresh'."
(interactive "P")
(sx-initialize)
(unless (buffer-live-p sx-question-list--buffer)
diff --git a/sx-question-mode.el b/sx-question-mode.el
index 089ee12..f8a0d1e 100644
--- a/sx-question-mode.el
+++ b/sx-question-mode.el
@@ -19,8 +19,6 @@
;;; Commentary:
-;;
-
;;; Code:
(require 'markdown-mode)
@@ -54,6 +52,7 @@
(defun sx-question-mode--display (data &optional window)
"Display question given by DATA on WINDOW.
If WINDOW is nil, use selected one.
+
Returns the question buffer."
(let ((inhibit-read-only t))
(with-current-buffer
@@ -65,7 +64,7 @@ Returns the question buffer."
(defun sx-question-mode--display-buffer (window)
"Display and return the buffer used for displaying a question.
-Create the buffer if necessary.
+Create `sx-question-mode--buffer' if necessary.
If WINDOW is given, use that to display the buffer."
;; Create the buffer if necessary.
(unless (buffer-live-p sx-question-mode--buffer)
@@ -84,8 +83,9 @@ If WINDOW is given, use that to display the buffer."
;;; Printing a question's content
;;;; Faces and Variables
+
(defvar sx-question-mode--overlays nil
- "")
+ "Question mode overlays.")
(make-variable-buffer-local 'sx-question-mode--overlays)
(defface sx-question-mode-header
@@ -147,14 +147,14 @@ If WINDOW is given, use that to display the buffer."
'((((background dark)) :background "#090909")
(((background light)) :background "#f4f4f4"))
"Face used on the question body in the question buffer.
-Shouldn't have a foreground, or this will interfere with
+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."
+First \"%s\" is replaced with the date and the second \"%s\" with
+the editor's name."
:type 'string
:group 'sx-question-mode)
@@ -176,8 +176,8 @@ editor's name."
(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."
+First \"%s\" is replaced with user name. Second \"%s\" is
+replaced with the comment."
:type 'string
:group 'sx-question-mode)
@@ -206,17 +206,17 @@ QUESTION must be a data structure returned by `json-read'."
(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))
- "")
+ (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)
- "")
+ follow-link t)
+ "Title properties.")
(defun sx-question-mode--print-section (data)
"Print a section corresponding to DATA.
@@ -293,9 +293,11 @@ DATA can represent a question or an answer."
(propertize .display_name
'face 'sx-question-mode-author)))
-(defun sx-question-mode--print-comment (data)
- "Print the comment described by alist DATA."
- (sx-assoc-let data
+(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-assoc-let comment-data
(insert
(format
sx-question-mode-comments-format
@@ -310,8 +312,10 @@ DATA can represent a question or an answer."
3)))))
(defmacro sx-question-mode--wrap-in-overlay (properties &rest body)
- "Execute BODY and wrap any inserted text in an overlay.
-Overlay is pushed on `sx-question-mode--overlays' and given PROPERTIES.
+ "Start a scope with overlay PROPERTIES and execute BODY.
+Overlay is pushed on `sx-question-mode--overlays' and given
+PROPERTIES.
+
Return the result of BODY."
(declare (indent 1)
(debug t))
@@ -325,7 +329,7 @@ Return the result of BODY."
result))
(defmacro sx-question-mode--wrap-in-text-property (properties &rest body)
- "Execute BODY and PROPERTIES to any inserted text.
+ "Start a scope with PROPERTIES and execute BODY.
Return the result of BODY."
(declare (indent 1)
(debug t))
@@ -335,9 +339,14 @@ Return the result of BODY."
result))
(defun sx-question-mode--insert-header (&rest args)
- "Insert HEADER and VALUE.
-HEADER is given `sx-question-mode-header' face, and value is given FACE.
-\(fn header value face [header value face] [header value face] ...)"
+ "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.
+
+Syntax:
+
+ \(fn HEADER VALUE FACE [HEADER VALUE FACE] [HEADER VALUE FACE] ...)"
(while args
(insert
(propertize (pop args) 'face 'sx-question-mode-header)
@@ -351,7 +360,7 @@ HEADER is given `sx-question-mode-header' face, and value is given FACE.
"String to be displayed as the bullet of markdown list items.")
(defun sx-question-mode--fill-and-fontify (text)
- "Fill TEXT according to `markdown-mode' and return it."
+ "Return TEXT filled according to `markdown-mode'."
(with-temp-buffer
(erase-buffer)
(insert text)
@@ -415,7 +424,7 @@ URL is used as 'help-echo and 'url properties."
;; Mouse-over
'help-echo (format
(propertize "URL: %s, %s to visit" 'face 'minibuffer-prompt)
- (propertize url 'face 'default)
+ (propertize url 'face 'default)
(propertize "RET" 'face 'font-lock-function-name-face))
;; In case we need it.
'url url
@@ -430,22 +439,22 @@ URL is used as 'help-echo and 'url properties."
'action #'sx-question-mode-follow-link))
(defun sx-question-mode-follow-link (&optional pos)
- "Follow link at POS or point"
+ "Follow link at POS. If POS is nil, use `point'."
(interactive)
(browse-url
(or (get-text-property (or pos (point)) 'url)
- (error "No url under point: %s" (or pos (point))))))
+ (user-error "No url under point: %s" (or pos (point))))))
-(defun sx-question-mode-find-reference (id &optional id2)
+(defun sx-question-mode-find-reference (id &optional fallback-id)
"Find url identified by reference ID in current buffer.
-If ID is nil, use ID2 instead."
+If ID is nil, use FALLBACK-ID instead."
(save-excursion
(save-match-data
(goto-char (point-min))
(when (search-forward-regexp
(format (rx line-start (0+ blank) "[%s]:" (0+ blank)
(group-n 1 (1+ (not blank))))
- (or id id2))
+ (or id fallback-id))
nil t)
(match-string-no-properties 1)))))
@@ -468,6 +477,7 @@ If ID is nil, use ID2 instead."
"Screen line to which we recenter after moving between sections.
This is used as an argument to `recenter', only used if the end
of section is outside the window.
+
If nil, no recentering is performed."
:type '(choice (const :tag "Don't recenter" nil)
integer)
@@ -497,13 +507,14 @@ Prefix argument N moves N sections down or up."
(defun sx-question-mode-previous-section (&optional n)
"Move down to previous section (question or answer) of this buffer.
-Prefix argument N moves N sections up or down."
+Prefix argument moves N sections up or down."
(interactive "p")
(sx-question-mode-next-section (- (or n 1))))
(defun sx-question-mode--goto-property-change (prop &optional direction)
- "Move forward until the value of text-property sx-question-mode--PROP changes.
+ "Move forward to the next change of text-property sx-question-mode--PROP.
Return the new value of PROP at point.
+
If DIRECTION is negative, move backwards instead."
(let ((prop (intern (format "sx-question-mode--%s" prop)))
(func (if (and (numberp direction)
@@ -516,9 +527,9 @@ If DIRECTION is negative, move backwards instead."
(goto-char (funcall func (point) prop nil limit))
(get-text-property (point) prop)))
-;;; Optional argument is for `push-button'.
(defun sx-question-mode-hide-show-section (&optional _)
- "Hide or show section under point."
+ "Hide or show section under point.
+Optional argument _ is for `push-button'."
(interactive)
(let ((ov (car (or (sx-question-mode--section-overlays-at (point))
(sx-question-mode--section-overlays-at
@@ -537,8 +548,9 @@ If DIRECTION is negative, move backwards instead."
;;; Major-mode
(define-derived-mode sx-question-mode markdown-mode "Question"
- "Major mode for a question and its answers.
+ "Major mode to display and navigate a question and its answers.
Letters do not insert themselves; instead, they are commands.
+
\\<sx-question-mode>
\\{sx-question-mode}"
;; Determine how to close this window.
diff --git a/sx-question.el b/sx-question.el
index 972e9d9..f6b7beb 100644
--- a/sx-question.el
+++ b/sx-question.el
@@ -19,8 +19,6 @@
;;; Commentary:
-;;
-
;;; Code:
@@ -44,10 +42,17 @@
answer.owner
answer.body_markdown
answer.comments)
- (user.profile_image shallow_user.profile_image)))
+ (user.profile_image shallow_user.profile_image))
+ "The filter applied when retrieving question data.
+See `sx-question-get-questions' and `sx-question-get-question'.")
(defun sx-question-get-questions (site &optional page)
- "Get the page PAGE of questions from SITE."
+ "Get SITE questions. Return page PAGE (the first if nil).
+Return a list of question. Each question is an alist of
+properties returned by the API with an added (site SITE)
+property.
+
+`sx-method-call' is used with `sx-question-browse-filter'."
(mapcar
(lambda (question) (cons (cons 'site site) question))
(sx-method-call
@@ -56,33 +61,43 @@
(page . ,page))
sx-question-browse-filter)))
-(defun sx-question-get-question (site id)
- "Get the question ID from SITE."
+(defun sx-question-get-question (site question-id)
+ "Query SITE for a QUESTION-ID and return it.
+If QUESTION-ID doesn't exist on SITE, raise an error."
(let ((res (sx-method-call
- (format "questions/%s" id)
+ (format "questions/%s" question-id)
`((site . ,site))
sx-question-browse-filter)))
(if (vectorp res)
(elt res 0)
- (error "Couldn't find question %S in %S" id site))))
+ (error "Couldn't find question %S in %S"
+ question-id site))))
;;; Question Properties
+
;;;; Read/unread
-(defvar sx-question--user-read-list nil
+(defvar sx-question--user-read-list nil
"Alist of questions read by the user.
-Each element has the form (SITE . QUESTION-LIST).
-And each element in QUESTION-LIST has the form (QUESTION_ID . LAST-VIEWED-DATE).")
+
+Each element has the form
+
+ (SITE . QUESTION-LIST)
+
+where each element in QUESTION-LIST has the form
+
+ (QUESTION_ID . LAST-VIEWED-DATE).")
(defun sx-question--ensure-read-list (site)
- "Ensure the `sx-question--user-read-list' has been read from cache.
+ "Ensure `sx-question--user-read-list' has been read from cache.
If no cache exists for it, initialize one with SITE."
(unless sx-question--user-read-list
(setq sx-question--user-read-list
(sx-cache-get 'read-questions `'((,site))))))
(defun sx-question--read-p (question)
- "Non-nil if QUESTION has been read since last updated."
+ "Non-nil if QUESTION has been read since last updated.
+See `sx-question--user-read-list'."
(sx-assoc-let question
(sx-question--ensure-read-list .site)
(let ((ql (cdr (assoc .site sx-question--user-read-list))))
@@ -91,7 +106,8 @@ If no cache exists for it, initialize one with SITE."
.last_activity_date)))))
(defun sx-question--mark-read (question)
- "Mark QUESTION as being read, until it is updated again."
+ "Mark QUESTION as being read until it is updated again.
+See `sx-question--user-read-list'."
(sx-assoc-let question
(sx-question--ensure-read-list .site)
(let ((site-cell (assoc .site sx-question--user-read-list))
@@ -108,20 +124,23 @@ If no cache exists for it, initialize one with SITE."
(t
(sx-sorted-insert-skip-first
q-cell site-cell (lambda (x y) (> (car x) (car y))))))))
- ;; This causes a small lag on `j' and `k' as the list gets large.
- ;; Should we do this on a timer?
;; Save the results.
+ ;; @TODO This causes a small lag on `j' and `k' as the list gets
+ ;; large. Should we do this on a timer?
(sx-cache-set 'read-questions sx-question--user-read-list))
;;;; Hidden
-(defvar sx-question--user-hidden-list nil
+(defvar sx-question--user-hidden-list nil
"Alist of questions hidden by the user.
-Each element has the form (SITE . QUESTION-LIST).
-And each element in QUESTION-LIST has the form (QUESTION_ID . LAST-VIEWED-DATE).")
+
+Each element has the form
+
+ (SITE QUESTION_ID QUESTION_ID ...)")
(defun sx-question--ensure-hidden-list (site)
"Ensure the `sx-question--user-hidden-list' has been read from cache.
+
If no cache exists for it, initialize one with SITE."
(unless sx-question--user-hidden-list
(setq sx-question--user-hidden-list
@@ -158,13 +177,13 @@ If no cache exists for it, initialize one with SITE."
;;;; Other data
(defun sx-question--accepted-answer-id (question)
- "Return accepted answer in QUESTION, or nil if none."
+ "Return accepted answer in QUESTION or nil if none exists."
(sx-assoc-let question
(and (integerp .accepted_answer_id)
.accepted_answer_id)))
(defun sx-question--tag-format (tag)
- "Formats TAG for display"
+ "Formats TAG for display."
(concat "[" tag "]"))
(provide 'sx-question)
diff --git a/sx-request.el b/sx-request.el
index d982057..906785b 100644
--- a/sx-request.el
+++ b/sx-request.el
@@ -19,7 +19,30 @@
;;; Commentary:
+;; API requests are handled on three separate tiers:
;;
+;; `sx-method-call':
+;;
+;; This is the function that should be used most often, since it
+;; runs necessary checks (authentication) and provides basic
+;; processing of the result for consistency.
+;;
+;; `sx-request-make':
+;;
+;; This is the fundamental function for interacting with the API.
+;; It makes no provisions for 'common' usage, but it does ensure
+;; data is retrieved successfully or an appropriate signal is
+;; thrown.
+;;
+;; `url.el' and `json.el':
+;;
+;; The whole solution is built upon `url-retrieve-synchronously'
+;; for making the request and `json-read-from-string' for parsing
+;; it into a properly symbolic data structure.
+;;
+;; When at all possible, use `sx-method-call'. There are specialized
+;; cases for the use of `sx-request-make' outside of sx-method.el, but
+;; these must be well-documented inline with the code.
;;; Code:
@@ -44,53 +67,72 @@
(format "https://api.stackexchange.com/%s/" sx-request-api-version)
"The base URL to make requests from.")
-(defcustom sx-request-silent-p
- t
- "When `t', requests default to being silent.")
-
-(defcustom sx-request-cache-p
- t
- "Cache requests made to the StackExchange API.")
-
(defcustom sx-request-unzip-program
"gunzip"
- "program used to unzip the response")
+ "Program used to unzip the response if it is compressed.
+This program must accept compressed data on standard input."
+ :group 'sx-request
+ :type 'string)
(defvar sx-request-remaining-api-requests
nil
- "The number of API requests remaining according to the most
-recent call. Set by `sx-request-make'.")
+ "The number of API requests remaining.
+Set by `sx-request-make'.")
(defcustom sx-request-remaining-api-requests-message-threshold
50
- "After `sx-request-remaining-api-requests' drops below this
+ "Lower bound for printed warnings of API usage limits.
+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.")
+number of requests left every time it finishes a call."
+ :group 'sx-request
+ :type 'integer)
;;; Making Requests
(defun sx-request-make
- (method &optional args need-auth use-post silent)
- (let ((url-automatic-caching sx-request-cache-p)
+ (method &optional args need-auth use-post)
+ "Make a request to the API, executing METHOD with ARGS.
+You should almost certainly be using `sx-method-call' instead of
+this function.
+
+If NEED-AUTH is non-nil, authentication will be provided. If
+USE-POST is non-nil, the request will use POST instead of GET.
+
+Returns cleaned response content.
+See (`sx-encoding-clean-content-deep').
+
+The full call is built with `sx-request-build', prepending
+`sx-request-api-key' to receive a higher quota. This call is
+then resolved with `url-retrieve-synchronously' to a temporary
+buffer that it returns. The headers are then stripped using a
+search a blank line (\"\\n\\n\"). The main body of the response
+is then tested with `sx-encoding-gzipped-buffer-p' for
+compression. If it is compressed, `sx-request-unzip-program' is
+called to uncompress the response. The uncompressed respons is
+then read with `json-read-from-string'.
+
+`sx-request-remaining-api-requests' is updated appropriately and
+the main content of the response is returned."
+ (let ((url-automatic-caching t)
(url-inhibit-uncompression t)
- (silent (or silent sx-request-silent-p))
(request-method (if use-post "POST" "GET"))
(request-args
(sx-request--build-keyword-arguments args nil need-auth))
(request-url (concat sx-request-api-root method)))
- (unless silent (sx-message "Request: %S" request-url))
+ (sx-message "Request: %S" request-url)
(let ((response-buffer (sx-request--request request-url
request-args
- request-method
- silent)))
+ request-method)))
(if (not response-buffer)
(error "Something went wrong in `url-retrieve-synchronously'")
(with-current-buffer response-buffer
(let* ((data (progn
+ ;; @TODO use url-http-end-of-headers
(goto-char (point-min))
(if (not (search-forward "\n\n" nil t))
- (error "Response headers missing; response corrupt")
+ (error "Headers missing; response corrupt")
(delete-region (point-min) (point))
(buffer-string))))
(response-zipped-p (sx-encoding-gzipped-p data))
@@ -100,6 +142,8 @@ number of requests left every time it finishes a call.")
sx-request-unzip-program
nil t)
(buffer-string)))
+ ;; @TODO should use `condition-case' here -- set
+ ;; RESPONSE to 'corrupt or something
(response (with-demoted-errors "`json' error: %S"
(json-read-from-string data))))
(when (and (not response) (string-equal data "{}"))
@@ -110,8 +154,7 @@ number of requests left every time it finishes a call.")
(when .error_id
(error "Request failed: (%s) [%i %s] %S"
.method .error_id .error_name .error_message))
- (when (< (setq sx-request-remaining-api-requests
- .quota_remaining)
+ (when (< (setq sx-request-remaining-api-requests .quota_remaining)
sx-request-remaining-api-requests-message-threshold)
(sx-message "%d API requests reamining"
sx-request-remaining-api-requests))
@@ -120,20 +163,25 @@ number of requests left every time it finishes a call.")
;;; Support Functions
-(defun sx-request--request (url args method silent)
+(defun sx-request--request (url args method)
+ "Return the response buffer for URL with ARGS using METHOD."
(let ((url-request-method method)
(url-request-extra-headers
'(("Content-Type" . "application/x-www-form-urlencoded")))
(url-request-data args))
- (cond
- ((equal '(24 . 4) (cons emacs-major-version emacs-minor-version))
- (url-retrieve-synchronously url silent))
- (t (url-retrieve-synchronously url)))))
+ (url-retrieve-synchronously url)))
+
(defun sx-request--build-keyword-arguments (alist &optional
- kv-value-sep need-auth)
- "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
+ kv-sep need-auth)
+ "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
false, use the symbol `false'. Each element is processed with
`sx--thing-as-string'."
@@ -148,7 +196,7 @@ false, use the symbol `false'. Each element is processed with
;; Pass user error when asking to warn
(warn
(user-error
- "This query requires authentication. Please run `M-x sx-auth-authenticate' and try again."))
+ "This query requires authentication; run `M-x sx-auth-authenticate' and try again"))
((not auth)
(lwarn "stack-mode" :debug
"This query requires authentication")
@@ -161,7 +209,7 @@ false, use the symbol `false'. Each element is processed with
(concat
(sx--thing-as-string (car pair))
"="
- (sx--thing-as-string (cdr pair) kv-value-sep)))
+ (sx--thing-as-string (cdr pair) kv-sep)))
(delq nil (mapcar
(lambda (pair)
(when (cdr pair) pair))
diff --git a/sx-site.el b/sx-site.el
index 6bef91f..66d78dc 100644
--- a/sx-site.el
+++ b/sx-site.el
@@ -19,8 +19,6 @@
;;; Commentary:
-;;
-
;;; Code:
(require 'sx-method)
@@ -43,9 +41,11 @@
related_site.api_site_parameter
related_site.relation)
nil
- none))
+ none)
+ "Filter for browsing sites.")
(defun sx-site--get-site-list ()
+ "Return all sites with `sx-site-browse-filter'."
(sx-cache-get
'site-list
'(sx-method-call
@@ -54,7 +54,9 @@
(defcustom sx-site-favorites
nil
- "Favorite sites."
+ "List of favorite sites.
+Each entry is a string corresponding to a single site's
+api_site_parameter."
:group 'sx-site)
(defun sx-site-get-api-tokens ()
diff --git a/sx.el b/sx.el
index eab1ead..061c85d 100644
--- a/sx.el
+++ b/sx.el
@@ -49,7 +49,7 @@
(defmacro sx-sorted-insert-skip-first (newelt list &optional predicate)
"Inserted NEWELT into LIST sorted by PREDICATE.
-This is designed for the (site id id ...) lists. So the first car
+This is designed for the (site id id ...) lists. So the first car
is intentionally skipped."
`(let ((tail ,list)
(x ,newelt))
@@ -61,7 +61,8 @@ is intentionally skipped."
(setcdr tail (cons x (cdr tail)))))
(defun sx-message (format-string &rest args)
- "Display a message."
+ "Display FORMAT-STRING as a message with ARGS.
+See `format'."
(message "[stack] %s" (apply #'format format-string args)))
(defun sx-message-help-echo ()
@@ -72,7 +73,9 @@ is intentionally skipped."
(defun sx--thing-as-string (thing &optional sequence-sep)
"Return a string representation of THING.
If THING is already a string, just return it.
-Optional argument SEQUENCE-SEP is the separator applied between elements of a sequence."
+
+Optional argument SEQUENCE-SEP is the separator applied between
+elements of a sequence."
(cond
((stringp thing) thing)
((symbolp thing) (symbol-name thing))
@@ -82,7 +85,24 @@ Optional argument SEQUENCE-SEP is the separator applied between elements of a se
thing (if sequence-sep sequence-sep ";")))))
(defun sx--filter-data (data desired-tree)
- "Filters DATA and return the DESIRED-TREE."
+ "Filter DATA and return the DESIRED-TREE.
+
+For example:
+
+ (sx--filter-data
+ '((prop1 . value1)
+ (prop2 . value2)
+ (prop3
+ (test1 . 1)
+ (test2 . 2))
+ (prop4 . t))
+ '(prop1 (prop3 test2)))
+
+would yield
+
+ ((prop1 . value1)
+ (prop3
+ (test2 . 2)))"
(if (vectorp data)
(apply #'vector
(mapcar (lambda (entry)
@@ -92,7 +112,7 @@ Optional argument SEQUENCE-SEP is the separator applied between elements of a se
(delq
nil
(mapcar (lambda (cons-cell)
- ;; TODO the resolution of `f' is O(2n) in the worst
+ ;; @TODO the resolution of `f' is O(2n) in the worst
;; case. It may be faster to implement the same
;; functionality as a `while' loop to stop looking the
;; list once it has found a match. Do speed tests.
@@ -112,7 +132,7 @@ Optional argument SEQUENCE-SEP is the separator applied between elements of a se
;;; Interpreting request data
(defun sx--deep-dot-search (data)
"Find symbols somewhere inside DATA which start with a `.'.
-Returns a list where each element is a cons cell. The car is the
+Returns a list where each element is a cons cell. The car is the
symbol, the cdr is the symbol without the `.'."
(cond
((symbolp data)
@@ -127,13 +147,13 @@ symbol, the cdr is the symbol without the `.'."
(remove nil (mapcar #'sx--deep-dot-search data))))))
(defmacro sx-assoc-let (alist &rest body)
- "Execute BODY while let-binding dotted symbols to their values in ALIST.
-Dotted symbol is any symbol starting with a `.'. Only those
+ "Use dotted symbols let-bound to their values in ALIST and execute BODY.
+Dotted symbol is any symbol starting with a `.'. Only those
present in BODY are letbound, which leads to optimal performance.
-For instance the following code
+For instance, the following code
- (stack-core-with-data alist
+ (sx-assoc-let alist
(list .title .body))
is equivalent to
@@ -150,19 +170,19 @@ is equivalent to
(defcustom sx-init-hook nil
"Hook run when stack-mode initializes.
-
-Run after `sx-init--internal-hook'.")
+Run after `sx-init--internal-hook'."
+ :group 'sx
+ :type 'hook)
(defvar sx-init--internal-hook nil
"Hook run when stack-mode initializes.
-
This is used internally to set initial values for variables such
as filters.")
-(defun sx--< (property x y &optional pred)
+(defun sx--< (property x y &optional predicate)
"Non-nil if PROPERTY attribute of alist X is less than that of Y.
-With optional argument PRED, use it instead of `<'."
- (funcall (or pred #'<)
+With optional argument PREDICATE, use it instead of `<'."
+ (funcall (or predicate #'<)
(cdr (assoc property x))
(cdr (assoc property y))))
@@ -184,6 +204,7 @@ If it has, holds the time at which initialization happened.")
(defun sx-initialize (&optional force)
"Run initialization hooks if they haven't been run yet.
These are `sx-init--internal-hook' and `sx-init-hook'.
+
If FORCE is non-nil, run them even if they've already been run."
(when (or force (not sx-initialized))
(prog1
diff --git a/sx.org b/sx.org
new file mode 100644
index 0000000..b646b2c
--- /dev/null
+++ b/sx.org
@@ -0,0 +1,126 @@
+#+MACRO: version 0.1
+#+MACRO: versiondate 16 November 2014
+#+MACRO: updated last updated {{{versiondate}}}
+
+#+TITLE: SX: A StackExchange Client (v{{{version}}})
+#+DATE: 16 November 2014
+#+AUTHOR: @@texinfo:@url{@@www.github.com/vermiculus/stack-mode@@texinfo:}@@
+#+LANGUAGE: en
+
+#+OPTIONS: ':t toc:t
+
+#+TEXINFO_FILENAME: sx.info
+#+TEXINFO_HEADER: @syncodeindex pg cp
+
+#+TEXINFO_DIR_CATEGORY: Texinfo documentation system
+#+TEXINFO_DIR_TITLE: SX: (StackExchange Client)
+#+TEXINFO_DIR_DESC: A StackExchange client for Emacs
+
+#+TEXINFO_PRINTED_TITLE: SX: A StackExchange Client
+#+SUBTITLE: for version {{{version}}}, last updated {{{versiondate}}}
+
+* Copying
+ :PROPERTIES:
+ :COPYING: t
+ :END:
+
+This manual is for SX (version {{{version}}}, {{{updated}}}), a
+StackExchange client for Emacs.
+
+Copyright © 2014 Free Software Foundation, Inc.
+
+#+BEGIN_QUOTE
+Permission is granted to copy, distribute and/or modify this
+document under the terms of the GNU Free Documentation License,
+Version 1.3 or any later version published by the Free Software
+Foundation; with no Invariant Sections, with no Front-Cover Texts,
+and with no Back-Cover Texts. A copy of the license is included in
+the section entitled "GNU Free Documentation License".
+#+END_QUOTE
+
+* Introduction
+SX is a StackExchange client for Emacs. This means that it supports
+many of the same features that the official web interface does, but in
+a way that is /specialized/ for Emacs:
+
+- question browsing for any site on the network
+- asking, answering, and commenting
+- advanced searching and filtering
+- offline archiving
+- inbox notifications
+- ...
+
+All of these features are implemented in a way that makes sense with
+Emacs conventions. Of course, the core convention of Emacs is
+arbitrary customizability -- [[#hooks][hack away]]!
+
+* Basic Usage
+** Authenticating
+Use ~sx-auth-authenticate~. Calling this function will open up a
+webpage on StackExchange that will prompt you to authorize this
+application. Once you do this, StackExchange will redirect you to our
+own authorization landing page. This landing page will prominently
+display your access token. (This is /your/ token -- keep this
+private!) Enter this token into Emacs. Emacs will set and save it to
+a cache file.
+
+** Browsing Questions
+To browse a list of questions retrieved from the site, use
+~list-questions~. This pulls the first page of questions from the
+current site and displays them in a list. Refresh the page with =g=,
+use =n= and =p= to navigate without viewing the question, =j= and =k=
+to do the same /with/ viewing the question, and =RET= to visit the
+question at point.
+
+* List of Hooks
+ :PROPERTIES:
+ :CUSTOM_ID: hooks
+ :END:
+
+# Do not list internal hooks. While they are useful, they should be
+# used only by contributors.
+
+- ~sx-init-hook~ :: Run when ~sx-initialize~ is called.
+
+* Contributing
+This document is maintained in Org format. Updates to the source code
+should be accompanied by updates to this document when user-facing
+functionality is changed.
+
+Note that some distinctions are made which may not be apparent when
+viewing the document with Info.
+
+** Markup Conventions
+Markup is used consistently as follows:
+
+- packages :: =package.el=
+- keybinding :: =C-x C-s= (use ~kbd~ format)
+- values :: =value=
+- symbols :: =symbol=
+- functions :: ~function~
+
+To make the Info export readable, lists and source code blocks are
+separated from body text with a blank line (as to start a new
+paragraph).
+
+** Document Attributes
+Attributes should be given in uppercase:
+
+#+BEGIN_SRC org
+ ,#+BEGIN_SRC elisp
+ (some elisp)
+ ,#+END_SRC
+#+END_SRC
+
+** Source Code Blocks
+The language for Emacs Lisp source code blocks should be given as
+=elisp= and its content should be indented by two spaces. See
+~org-edit-src-content-indentation~.
+
+* COMMENT Local Variables
+# LocalWords: StackExchange SX inbox sx API url json inline Org
+# LocalWords: Markup keybinding keybindings customizability webpage
+
+# Local Variables:
+# org-export-date-timestamp-format: "$B %e %Y"
+# End:
diff --git a/test/tests.el b/test/tests.el
index 6a48257..bb23310 100644
--- a/test/tests.el
+++ b/test/tests.el
@@ -31,7 +31,6 @@
(setq
sx-request-remaining-api-requests-message-threshold 50000
debug-on-error t
- sx-request-silent-p nil
user-emacs-directory "."
sx-test-data-questions