From 9df98091a366a3b1585ba52a21b0261b314ea8bd Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Mon, 17 Nov 2014 08:45:09 -0500 Subject: Documentation -- part one Pushing this change to continue work elsewhere. --- .agignore | 20 ++++++ .gitignore | 2 + sx-auth.el | 21 +++++- sx-cache.el | 31 +++++---- sx-encoding.el | 77 +++++++++++++-------- sx-filter.el | 34 ++++++---- sx-method.el | 8 +-- sx-question.el | 47 ++++++++----- sx-request.el | 80 ++++++++++++++++------ sx.org | 211 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/tests.el | 1 - 11 files changed, 436 insertions(+), 96 deletions(-) create mode 100644 .agignore create mode 100644 sx.org 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 f32e7aa..96523f6 100644 --- a/sx-auth.el +++ b/sx-auth.el @@ -46,7 +46,26 @@ what you are doing!") 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 diff --git a/sx-cache.el b/sx-cache.el index e3b356b..80b6ced 100644 --- a/sx-cache.el +++ b/sx-cache.el @@ -30,10 +30,15 @@ (defcustom sx-cache-directory (expand-file-name ".stackmode" user-emacs-directory) - "Directory containined cached files and precompiled filters.") + "Directory containining cached data.") + +(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)) @@ -41,28 +46,28 @@ (defun sx-cache-get (cache &optional form) "Return the data within CACHE. -If CACHE does not exist, evaluate FORM and set it to its return. +If CACHE does not exist, use `sx-cache-set' to set CACHE to the +result of evaluating FORM. -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 changes permanently. -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'. -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) diff --git a/sx-encoding.el b/sx-encoding.el index 9d48e60..91d2765 100644 --- a/sx-encoding.el +++ b/sx-encoding.el @@ -19,8 +19,6 @@ ;;; Commentary: -;; - ;;; Code: (require 'cl-lib) @@ -62,28 +60,48 @@ 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." :type '(repeat (choice symbol string)) :group 'sx) (defun sx-encoding-decode-entities (string) + "Decode HTML entities (e.g. \""\") 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." + ;; @TODO Why are we limiting ourselves to two digits, here? + ;; "Ɛ" is perfectly valid, corresponding to "Ɛ". (Note that + ;; "Ɛ" is not in our plist.) (let* ((plist sx-encoding-html-entities-plist) - (get-function (lambda (s) (let ((ss (substring s 1 -1))) - ;; Handle things like " - (or (plist-get plist (intern ss)) - ;; Handle things like ' - (format "%c" (string-to-number - (substring ss 1)))))))) + (get-function + (lambda (s) + (let ((ss (substring s 1 -1))) + ;; Handle things like " + (or (plist-get plist (intern ss)) + ;; Handle things like ' + (char-to-string + (string-to-number + (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 +109,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)) @@ -112,26 +137,24 @@ See `sx-encoding-clean-content'." (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 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." +See `sx-encoding-gzipped-p'." (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-filter.el b/sx-filter.el index 90681e8..1fb2861 100644 --- a/sx-filter.el +++ b/sx-filter.el @@ -19,8 +19,6 @@ ;;; Commentary: -;; - ;;; Code: @@ -35,25 +33,32 @@ (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 symbol -or string." +INCLUDE and EXCLUDE must both be lists; BASE should be a 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 +69,13 @@ 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 together 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'." (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 6f0a36b..8838134 100644 --- a/sx-method.el +++ b/sx-method.el @@ -19,8 +19,6 @@ ;;; Commentary: -;; - ;;; Code: (require 'json) (require 'url) @@ -29,12 +27,12 @@ (require 'sx-filter) (defun sx-method-call - (method &optional keyword-arguments filter silent) + (method &optional keyword-arguments filter) "Call METHOD with KEYWORD-ARGUMENTS using FILTER. -If SILENT is non-nil, no messages will be printed. +Return the entire response as a complex alist. -Return the entire response as a complex alist." +See `sx-request-make' and `sx-filter-get-var'." (sx-request-make method (cons (cons 'filter diff --git a/sx-question.el b/sx-question.el index fc44bd8..0342479 100644 --- a/sx-question.el +++ b/sx-question.el @@ -19,8 +19,6 @@ ;;; Commentary: -;; - ;;; Code: @@ -42,10 +40,16 @@ answer.owner answer.body_markdown answer.comments) - (user.profile_image shallow_user.profile_image))) + (user.profile_image shallow_user.profile_image)) + "The filter applied with `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 the page PAGE of questions from SITE. + +Return a list of questions, each consed with (site SITE). + +`sx-method-call' is used with `sx-question-browse-filter'." (mapcar (lambda (question) (cons (cons 'site site) question)) (sx-method-call @@ -55,7 +59,9 @@ sx-question-browse-filter))) (defun sx-question-get-question (site id) - "Get the question ID from SITE." + "Query SITE for a question ID and return it. + +If ID doesn't exist on SITE, raise an error." (let ((res (sx-method-call (format "questions/%s" id) `((site . ,site)) @@ -66,20 +72,30 @@ ;;; Question Properties + (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 `(list ,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)))) @@ -88,7 +104,9 @@ 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)) @@ -102,21 +120,18 @@ If no cache exists for it, initialize one with SITE." ((setq cell (assoc .question_id site-cell)) (setcdr cell .last_activity_date)) ;; Question wasn't present. - (t - (setcdr site-cell (cons q-cell (cdr site-cell))))))) - ;; This causes a small lag on `j' and `k' as the list gets large. - ;; Should we do this on a timer? + (t (setcdr site-cell (cons q-cell (cdr site-cell))))))) ;; Save the results. (sx-cache-set 'read-questions sx-question--user-read-list)) (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 6dc54e7..0434a7b 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,26 +67,28 @@ (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.") - +;;; @TODO Shouldn't this be made moot by our caching system? (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.") (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.") @@ -71,26 +96,38 @@ number of requests left every time it finishes a call.") ;;; Making Requests (defun sx-request-make - (method &optional args silent) + (method &optional args) + "Make a request to the API, executing METHOD with ARGS. + +You should almost certainly be using `sx-method-call' instead of +this function. + +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 sx-request-cache-p) (url-inhibit-uncompression t) - (silent (or silent sx-request-silent-p)) (call (sx-request-build method (cons (cons 'key sx-request-api-key) args)))) - (unless silent (sx-message "Request: %S" call)) - (let ((response-buffer (cond - ((equal '(24 . 4) (cons emacs-major-version emacs-minor-version)) - (url-retrieve-synchronously call silent)) - (t (url-retrieve-synchronously call))))) + (sx-message "Request: %S" call) + (let ((response-buffer (url-retrieve-synchronously call))) (if (not response-buffer) (error "Something went wrong in `url-retrieve-synchronously'") (with-current-buffer response-buffer (let* ((data (progn (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 +137,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 +149,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)) @@ -121,8 +159,10 @@ number of requests left every time it finishes a call.") ;;; Support Functions (defun sx-request-build (method keyword-arguments &optional kv-value-sep root) - "Build the request string that will be used to process REQUEST -with the given KEYWORD-ARGUMENTS." + "Construct METHOD to use KEYWORD-ARGUMENTS. + +The KEYWORD-ARGUMENTS are joined with KV-VALUE-SEP when it +contains a 'vector'. See `sx-request--build-keyword-arguments'." (let ((base (concat (or root sx-request-api-root) method)) (args (sx-request--build-keyword-arguments keyword-arguments kv-value-sep))) diff --git a/sx.org b/sx.org new file mode 100644 index 0000000..ec6c82b --- /dev/null +++ b/sx.org @@ -0,0 +1,211 @@ +#+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 prominately +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. + +- ~sx-init-hook~ :: Run when ~sx-initialize~ is called. + +* Developer's Reference +Creating a logically consistent piece of software requires that all +developers know the patterns established and resources made available +to them. These sections hope to do just that. +** Caching +All cached data is stored in =sx-cache-directory=. This defaults to +=~/.emacs.d/.stackmode= where =user-emacs-directory= is =~/.emacs.d=. +Each piece of cached data is stored in its own file. + +There are two main functions for interfacing with the cache system: + +- ~sx-cache-get~ :: Gets a cached variable's value, setting it if it + does not exist. This function will, as a + side-effect, ensure that the cache file exists. +- ~sx-cache-set~ :: Sets a cached variable's value. + +Each of these functions will ensure =sx-cache-directory= exists. + +** Making API Requests +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.el= + for making the request and =json.el= 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. + +*** ~sx-request-make~ +Requests are made by building a parameter string of the format + +#+BEGIN_EXAMPLE + key=value&key=value +#+END_EXAMPLE + +and appending it to a root and a method: + +#+BEGIN_EXAMPLE + /? +#+END_EXAMPLE + +This works fine for 'GET' requests of the API, but will not work for +'POST'. A revised request system is being created that will support +both of methods. + +** Working with Data +The API returns data in JSON format. When a method is called, the +response is enclosed in a 'wrapper' that includes various metadata: + +- remaining requests for your quota +- if the response has been truncated +- etc. + +When ~sx-request-make~ receives the response, it sets variables +related to the response (most notably the number of remaining +requests) and returns just the main content of the response. In order +to access this content, you could use a lengthy ~let~-binding or you +could use the ~sx-assoc-let~ macro. That is, + +#+BEGIN_SRC elisp + (sx-assoc-let data + (some fancy .function with a .property + in 'data)) +#+END_SRC + +expands to + +#+BEGIN_SRC elisp + (let + ((.function (cdr (assoc 'function data))) + (.property (cdr (assoc 'property data)))) + (some fancy .function with a .property + in 'data)) +#+END_SRC + +Use the following to highlight the special =.property= forms: + +#+BEGIN_SRC elisp + +#+END_SRC +* About this Document +This document is maintained in Org format. Updates to the source code +should almost always be accompanied by updates to this document. Soem +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 + +# 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 -- cgit v1.2.3 From 2d44aaa67c3d462debd35ce7b814f4d78e08fe29 Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Tue, 18 Nov 2014 21:53:44 -0500 Subject: Documentation -- part two Pushing this change to continue work elsewhere. --- sx-encoding.el | 12 +++++----- sx-filter.el | 9 ++++---- sx-method.el | 7 +++++- sx-question-list.el | 66 +++++++++++++++++++++++++++++++++++------------------ sx-question-mode.el | 9 +++++--- 5 files changed, 67 insertions(+), 36 deletions(-) diff --git a/sx-encoding.el b/sx-encoding.el index 91d2765..8af020e 100644 --- a/sx-encoding.el +++ b/sx-encoding.el @@ -60,7 +60,9 @@ 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 and their respective glyphs." + "Plist of HTML entities and their respective glyphs. + +See `sx-encoding-decode-entities'." :type '(repeat (choice symbol string)) :group 'sx) @@ -72,9 +74,6 @@ 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." - ;; @TODO Why are we limiting ourselves to two digits, here? - ;; "Ɛ" is perfectly valid, corresponding to "Ɛ". (Note that - ;; "Ɛ" is not in our plist.) (let* ((plist sx-encoding-html-entities-plist) (get-function (lambda (s) @@ -84,6 +83,7 @@ Return the decoded string." ;; Handle things like ' (char-to-string (string-to-number + ;; Skip the `#' (substring ss 1)))))))) (replace-regexp-in-string "&[^; ]*;" get-function string))) @@ -92,8 +92,8 @@ Return the decoded 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." +\"\\r\\n\", Unix uses just \"\\n\". Deleting \"\\r\" is sufficient for +conversion." (delete ?\r string)) (defun sx-encoding-clean-content (string) diff --git a/sx-filter.el b/sx-filter.el index 1fb2861..1241614 100644 --- a/sx-filter.el +++ b/sx-filter.el @@ -71,10 +71,11 @@ Returns the compiled filter as a string." (defun sx-filter-get (&optional include exclude base) "Return the string representation of the given filter. -If the filter data exist together 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'." +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 alise with `sx-cache-set' and +return the compiled filter." (or (cdr (assoc (list include exclude base) sx--filter-alist)) (let ((filter (sx-filter-compile include exclude base))) (when filter diff --git a/sx-method.el b/sx-method.el index 8838134..bd91e38 100644 --- a/sx-method.el +++ b/sx-method.el @@ -19,6 +19,11 @@ ;;; 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) (require 'url) @@ -30,7 +35,7 @@ (method &optional keyword-arguments filter) "Call METHOD with KEYWORD-ARGUMENTS using FILTER. -Return the entire response as a complex alist. +Return the response content as a complex alist. See `sx-request-make' and `sx-filter-get-var'." (sx-request-make diff --git a/sx-question-list.el b/sx-question-list.el index b220097..26b7c2f 100644 --- a/sx-question-list.el +++ b/sx-question-list.el @@ -135,7 +135,7 @@ Letters do not insert themselves; instead, they are commands. ([?\r] sx-question-list-display-question))) (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.") @@ -179,10 +179,11 @@ Letters do not insert themselves; instead, they are commands. "Site being displayed in the *question-list* buffer.") (defvar sx-question-list--current-dataset nil - "") + "The logical data behind the displayed list of questions.") (defun sx-question-list-refresh (&optional redisplay no-update) "Update the list of questions. + If REDISPLAY is non-nil (or if interactive), also call `tabulated-list-print'. If the prefix argument NO-UPDATE is nil, query StackExchange for a new list before redisplaying." @@ -212,29 +213,33 @@ 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\"." :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) + "Format QUESTION-DATA for display in the list. + +See `sx-question-list-refresh'." + (sx-assoc-let question-data (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 ") @@ -247,32 +252,42 @@ 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 to the previous question and display it. + +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 to the next question and display it. + +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 to the next entry. + +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 to the previous entry. + +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!")) @@ -299,7 +314,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 @@ -307,7 +322,12 @@ 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 `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) @@ -320,8 +340,10 @@ focus the relevant window." (defvar sx-question-list--buffer nil "Buffer where the list of questions is displayed.") -(defun list-questions (no-update) - "Display a list of StackExchange questions." +(defun sx-list-questions (no-update) + "Display a list of StackExchange questions. + +NO-UPDATE is passed to `sx-question-list-refresh'." (interactive "P") (unless (buffer-live-p sx-question-list--buffer) (setq sx-question-list--buffer @@ -331,7 +353,7 @@ focus the relevant window." (sx-question-list-refresh 'redisplay no-update)) (switch-to-buffer sx-question-list--buffer)) -(defalias 'sx-list-questions #'list-questions) +(defalias 'list-questions #'sx-list-questions) (provide 'sx-question-list) ;;; sx-question-list.el ends here diff --git a/sx-question-mode.el b/sx-question-mode.el index 32cd112..e436b40 100644 --- a/sx-question-mode.el +++ b/sx-question-mode.el @@ -19,8 +19,6 @@ ;;; Commentary: -;; - ;;; Code: (require 'markdown-mode) @@ -53,7 +51,9 @@ (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 +65,9 @@ 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,6 +86,7 @@ If WINDOW is given, use that to display the buffer." ;;; Printing a question's content ;;;; Faces and Variables + (defvar sx-question-mode--overlays nil "") (make-variable-buffer-local 'sx-question-mode--overlays) -- cgit v1.2.3 From 11b698a4c750c379a8c0d4d843744913e931d3fe Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Tue, 18 Nov 2014 21:54:34 -0500 Subject: Simplify `sx-method-call' We aren't using `stack-filter' anymore (in fact, we haven't since filters became functional). Somehow it survived refactoring; this commit fixes that oversight. --- sx-method.el | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/sx-method.el b/sx-method.el index bd91e38..9a6dcc5 100644 --- a/sx-method.el +++ b/sx-method.el @@ -38,12 +38,8 @@ Return the response content as a complex alist. See `sx-request-make' and `sx-filter-get-var'." - (sx-request-make - method - (cons (cons 'filter - (sx-filter-get-var - (cond (filter filter) - ((boundp 'stack-filter) stack-filter)))) + (sx-request-make method + (cons (cons 'filter (sx-filter-get-var filter)) keyword-arguments))) (provide 'sx-method) -- cgit v1.2.3 From 23d25719deb31694f12750a638872a175aae0419 Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Tue, 18 Nov 2014 23:15:55 -0500 Subject: Documentation -- part three Source code is documented to the best of my knowledge and understanding. --- sx-question-mode.el | 71 ++++++++++++++++++++++++++++++++++++----------------- sx-question.el | 11 +++++---- sx-request.el | 4 ++- sx-site.el | 10 +++++--- sx-time.el | 2 ++ sx.el | 45 +++++++++++++++++++++++++-------- 6 files changed, 101 insertions(+), 42 deletions(-) diff --git a/sx-question-mode.el b/sx-question-mode.el index e436b40..d971a49 100644 --- a/sx-question-mode.el +++ b/sx-question-mode.el @@ -150,14 +150,16 @@ 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) @@ -179,8 +181,9 @@ 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) @@ -194,6 +197,7 @@ Second \"%s\" is replaced with the comment." ;;;; Functions (defun sx-question-mode--print-question (question) "Print a buffer describing QUESTION. + QUESTION must be a data structure returned by `json-read'." ;; Clear the overlays (mapc #'delete-overlay sx-question-mode--overlays) @@ -220,10 +224,11 @@ QUESTION must be a data structure returned by `json-read'." follow-link t) "") -(defun sx-question-mode--print-section (data) - "Print a section corresponding to DATA. -DATA can represent a question or an answer." - (sx-assoc-let data +(defun sx-question-mode--print-section (question-data) + "Print a section corresponding to QUESTION-DATA. + +QUESTION-DATA can represent a question or an answer." + (sx-assoc-let question-data (insert sx-question-mode-header-title (apply #'propertize @@ -291,9 +296,12 @@ 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 printed according to +`sx-question-mode-comments-format'." + (sx-assoc-let comment-data (insert (format sx-question-mode-comments-format @@ -309,7 +317,10 @@ DATA can represent a question or an answer." (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. + +Overlay is pushed on `sx-question-mode--overlays' and given +PROPERTIES. + Return the result of BODY." (declare (indent 1) (debug t)) @@ -323,9 +334,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. + +Use as (fn header value face + [header value face] ...)" (while args (insert (propertize (pop args) 'face 'sx-question-mode-header) @@ -339,7 +355,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) @@ -397,6 +413,7 @@ HEADER is given `sx-question-mode-header' face, and value is given FACE. (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 @@ -418,22 +435,23 @@ 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)))))) -(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]:" (1+ blank) (group-n 1 (1+ (not blank)))) - (or id id2)) + (or id fallback-id)) nil t) (match-string-no-properties 1))))) @@ -454,8 +472,10 @@ If ID is nil, use ID2 instead." ;; for comments). (defcustom sx-question-mode-recenter-line 1 "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) @@ -463,6 +483,7 @@ If nil, no recentering is performed." (defun sx-question-mode-next-section (&optional n) "Move down to next section (question or answer) of this buffer. + Prefix argument N moves N sections down or up." (interactive "p") (let ((count (if n (abs n) 1))) @@ -485,13 +506,16 @@ 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." (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. + 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) @@ -525,8 +549,10 @@ 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}" ;; Determine how to close this window. @@ -559,6 +585,7 @@ Letters do not insert themselves; instead, they are commands. (defun sx-question-mode-refresh () "Refresh currently displayed question. + Queries the API for any changes to the question or its answers or comments, and redisplays it." (interactive) diff --git a/sx-question.el b/sx-question.el index 0342479..827b7c3 100644 --- a/sx-question.el +++ b/sx-question.el @@ -58,17 +58,18 @@ Return a list of questions, each consed with (site SITE). (page . ,page)) sx-question-browse-filter))) -(defun sx-question-get-question (site id) - "Query SITE for a question ID and return it. +(defun sx-question-get-question (site question-id) + "Query SITE for a QUESTION-ID and return it. -If ID doesn't exist on SITE, raise an error." +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 diff --git a/sx-request.el b/sx-request.el index 0434a7b..b3668fe 100644 --- a/sx-request.el +++ b/sx-request.el @@ -171,7 +171,9 @@ contains a 'vector'. See `sx-request--build-keyword-arguments'." (concat base "?" args)))) (defun sx-request--build-keyword-arguments (alist &optional kv-value-sep) - "Build a \"key=value&key=value&...\"-style string with the elements + "Format ALIST as a key-value joined with KV-VALUE-SEP. + +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 diff --git a/sx-site.el b/sx-site.el index 6bef91f..4e880b1 100644 --- a/sx-site.el +++ b/sx-site.el @@ -19,8 +19,6 @@ ;;; Commentary: -;; - ;;; Code: (require 'sx-method) @@ -43,7 +41,8 @@ related_site.api_site_parameter related_site.relation) nil - none)) + none) + "") (defun sx-site--get-site-list () (sx-cache-get @@ -54,7 +53,10 @@ (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-time.el b/sx-time.el index 9c4dfaa..1ce0886 100644 --- a/sx-time.el +++ b/sx-time.el @@ -51,12 +51,14 @@ (defcustom sx-time-date-format-year "%H:%M %e %b %Y" "Format used for dates on a past year. + See also `sx-time-date-format'." :type 'string :group 'sx-time) (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) diff --git a/sx.el b/sx.el index 53aae84..eea0749 100644 --- a/sx.el +++ b/sx.el @@ -28,7 +28,7 @@ ;;; Utility Functions (defun sx-message (format-string &rest args) - "Display a message" + "Display a message." (message "[stack] %s" (apply #'format format-string args))) (defun sx-message-help-echo () @@ -37,8 +37,9 @@ (when echo (message "%s" echo)))) (defun sx--thing-as-string (thing &optional sequence-sep) - "Return a string representation of THING. If THING is already -a string, just return it." + "Return a string representation of THING. + +If THING is already a string, just return it." (cond ((stringp thing) thing) ((symbolp thing) (symbol-name thing)) @@ -48,7 +49,24 @@ a string, just return it." thing (if sequence-sep sequence-sep ";"))))) (defun sx--filter-data (data desired-tree) - "Filters DATA and returns the DESIRED-TREE" + "Filters DATA and returns the DESIRED-TREE. + +For example: + + (sx--filter-data + '((prop1 . value1) + (prop2 . value2) + (prop3 + (test1 . 1) + (test2 . 2)) + (prop4 . t)) + '(prop1 (prop3 test2))) + +would yeild + + ((prop1 . value1) + (prop3 + (test2 . 2)))" (if (vectorp data) (apply #'vector (mapcar (lambda (entry) @@ -58,7 +76,7 @@ a string, just return it." (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. @@ -78,6 +96,7 @@ a string, just return it." ;;; 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 symbol, the cdr is the symbol without the `.'." (cond @@ -93,11 +112,12 @@ 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. + "Execute BODY with dotted symbols let-bound to their values in ALIST. + 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 (list .title .body)) @@ -125,15 +145,17 @@ Run after `sx-init--internal-hook'.") 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 question X is less than that of Y. -With optional argument predicate, 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)))) (defmacro sx-init-variable (variable value &optional setter) "Set VARIABLE to VALUE using SETTER. + SETTER should be a function of two arguments. If SETTER is nil, `set' is used." (eval @@ -144,6 +166,9 @@ SETTER should be a function of two arguments. If SETTER is nil, nil) (defun stack-initialize () + "Initialize SX. + +Runs `sx-init--internal-hook' and `sx-init-hook', in that order." (run-hooks 'sx-init--internal-hook 'sx-init-hook)) -- cgit v1.2.3 From 20dd7254da8e95bd01ce57f806733dee20005039 Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Tue, 18 Nov 2014 23:21:39 -0500 Subject: Update user manual Removed developer reference since this should be embedded in docstrings for the most part. --- sx.org | 97 ++++-------------------------------------------------------------- 1 file changed, 5 insertions(+), 92 deletions(-) diff --git a/sx.org b/sx.org index ec6c82b..10adf1c 100644 --- a/sx.org +++ b/sx.org @@ -59,7 +59,7 @@ arbitrary customizability -- [[#hooks][hack away]]! 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 prominately +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. @@ -77,101 +77,14 @@ question at point. :CUSTOM_ID: hooks :END: -# Do not list internal hooks. +# 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. -* Developer's Reference -Creating a logically consistent piece of software requires that all -developers know the patterns established and resources made available -to them. These sections hope to do just that. -** Caching -All cached data is stored in =sx-cache-directory=. This defaults to -=~/.emacs.d/.stackmode= where =user-emacs-directory= is =~/.emacs.d=. -Each piece of cached data is stored in its own file. - -There are two main functions for interfacing with the cache system: - -- ~sx-cache-get~ :: Gets a cached variable's value, setting it if it - does not exist. This function will, as a - side-effect, ensure that the cache file exists. -- ~sx-cache-set~ :: Sets a cached variable's value. - -Each of these functions will ensure =sx-cache-directory= exists. - -** Making API Requests -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.el= - for making the request and =json.el= 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. - -*** ~sx-request-make~ -Requests are made by building a parameter string of the format - -#+BEGIN_EXAMPLE - key=value&key=value -#+END_EXAMPLE - -and appending it to a root and a method: - -#+BEGIN_EXAMPLE - /? -#+END_EXAMPLE - -This works fine for 'GET' requests of the API, but will not work for -'POST'. A revised request system is being created that will support -both of methods. - -** Working with Data -The API returns data in JSON format. When a method is called, the -response is enclosed in a 'wrapper' that includes various metadata: - -- remaining requests for your quota -- if the response has been truncated -- etc. - -When ~sx-request-make~ receives the response, it sets variables -related to the response (most notably the number of remaining -requests) and returns just the main content of the response. In order -to access this content, you could use a lengthy ~let~-binding or you -could use the ~sx-assoc-let~ macro. That is, - -#+BEGIN_SRC elisp - (sx-assoc-let data - (some fancy .function with a .property - in 'data)) -#+END_SRC - -expands to - -#+BEGIN_SRC elisp - (let - ((.function (cdr (assoc 'function data))) - (.property (cdr (assoc 'property data)))) - (some fancy .function with a .property - in 'data)) -#+END_SRC - -Use the following to highlight the special =.property= forms: - -#+BEGIN_SRC elisp - -#+END_SRC * About this Document This document is maintained in Org format. Updates to the source code -should almost always be accompanied by updates to this document. Soem +should almost always be accompanied by updates to this document. Some distinctions are made which may not be apparent when viewing the document with Info. @@ -204,7 +117,7 @@ The language for Emacs Lisp source code blocks should be given as * COMMENT Local Variables # LocalWords: StackExchange SX inbox sx API url json inline Org -# LocalWords: Markup keybinding keybindings customizability +# LocalWords: Markup keybinding keybindings customizability webpage # Local Variables: # org-export-date-timestamp-format: "$B %e %Y" -- cgit v1.2.3 From 4399cbac0625a53c4a8d91e661e13fe11fdb8255 Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Wed, 19 Nov 2014 10:44:41 -0500 Subject: Add `customize' data --- sx-cache.el | 4 +++- sx-request.el | 8 ++++++-- sx.el | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/sx-cache.el b/sx-cache.el index 63025ea..049e171 100644 --- a/sx-cache.el +++ b/sx-cache.el @@ -30,7 +30,9 @@ (defcustom sx-cache-directory (expand-file-name ".stackmode" user-emacs-directory) - "Directory containining cached data.") + "Directory containing cached data." + :type 'directory + :group 'sx-cache) (defun sx-cache--ensure-sx-cache-directory-exists () "Ensure `sx-cache-directory' exists." diff --git a/sx-request.el b/sx-request.el index 89c9a59..fc54bb3 100644 --- a/sx-request.el +++ b/sx-request.el @@ -76,7 +76,9 @@ "gunzip" "Program used to unzip the response if it is compressed. -This program must accept compressed data on standard input.") +This program must accept compressed data on standard input." + :group 'sx-request + :type 'string) (defvar sx-request-remaining-api-requests nil @@ -90,7 +92,9 @@ 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.") +number of requests left every time it finishes a call." + :group 'sx-request + :type 'integer) ;;; Making Requests diff --git a/sx.el b/sx.el index 8b2456d..4a5a275 100644 --- a/sx.el +++ b/sx.el @@ -173,7 +173,9 @@ 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. -- cgit v1.2.3 From 5f4f1a0495b79e418b6df16d4642a33c9ccda007 Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Wed, 19 Nov 2014 10:56:10 -0500 Subject: Minor typos and incorrect information --- sx-filter.el | 2 +- sx-question-list.el | 2 +- sx-question-mode.el | 2 +- sx-question.el | 7 +------ sx-request.el | 4 ++-- sx.el | 2 +- 6 files changed, 7 insertions(+), 12 deletions(-) diff --git a/sx-filter.el b/sx-filter.el index 1241614..5c878c5 100644 --- a/sx-filter.el +++ b/sx-filter.el @@ -74,7 +74,7 @@ Returns the compiled filter as a string." 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 alise with `sx-cache-set' and +`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)) (let ((filter (sx-filter-compile include exclude base))) diff --git a/sx-question-list.el b/sx-question-list.el index 6a36f6f..c562679 100644 --- a/sx-question-list.el +++ b/sx-question-list.el @@ -251,7 +251,7 @@ Used in the questions list to indicate a question was updated \"4d ago\"." :group 'sx-question-list) (defun sx-question-list--print-info (question-data) - "Convert `json-read' DATA into tabulated-list format. + "Convert `json-read' QUESTION-DATA into tabulated-list format. See `sx-question-list-refresh'." (sx-assoc-let question-data diff --git a/sx-question-mode.el b/sx-question-mode.el index 627081b..db9cc8b 100644 --- a/sx-question-mode.el +++ b/sx-question-mode.el @@ -339,7 +339,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. + "Execute BODY and add PROPERTIES to any inserted text. Return the result of BODY." (declare (indent 1) diff --git a/sx-question.el b/sx-question.el index d576b73..344b54c 100644 --- a/sx-question.el +++ b/sx-question.el @@ -128,7 +128,6 @@ See `sx-question--user-read-list'." (sx-sorted-insert-skip-first q-cell site-cell (lambda (x y) (> (car x) (car y)))))))) ;; 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)) @@ -140,11 +139,7 @@ See `sx-question--user-read-list'." Each element has the form - (SITE . QUESTION-LIST). - -And each element in QUESTION-LIST has the form - - (QUESTION_ID . LAST-VIEWED-DATE).") + (SITE QUESTION_ID QUESTION_ID ...)") (defun sx-question--ensure-hidden-list (site) "Ensure the `sx-question--user-hidden-list' has been read from cache. diff --git a/sx-request.el b/sx-request.el index fc54bb3..aff6dc5 100644 --- a/sx-request.el +++ b/sx-request.el @@ -40,8 +40,8 @@ ;; 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 +;; 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: diff --git a/sx.el b/sx.el index 4a5a275..b1730c5 100644 --- a/sx.el +++ b/sx.el @@ -155,7 +155,7 @@ present in BODY are letbound, which leads to optimal performance. For instance, the following code - (stack-core-with-data alist + (sx-assoc-let alist (list .title .body)) is equivalent to -- cgit v1.2.3 From f694d5ed0bf7d29f83622c041ae30433a7223366 Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Wed, 19 Nov 2014 10:56:27 -0500 Subject: Remove -cache-p --- sx-request.el | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/sx-request.el b/sx-request.el index aff6dc5..aeb2931 100644 --- a/sx-request.el +++ b/sx-request.el @@ -67,11 +67,6 @@ (format "https://api.stackexchange.com/%s/" sx-request-api-version) "The base URL to make requests from.") -;;; @TODO Shouldn't this be made moot by our caching system? -(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 if it is compressed. @@ -121,7 +116,7 @@ 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 sx-request-cache-p) + (let ((url-automatic-caching t) (url-inhibit-uncompression t) (request-method (if use-post "POST" "GET")) (request-args -- cgit v1.2.3 From 56371314c1eb4fede7fbf56b049a65f093af0778 Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Wed, 19 Nov 2014 10:56:51 -0500 Subject: Enhance documentation --- sx-question-list.el | 5 ++++- sx-question-mode.el | 7 ++++--- sx-question.el | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/sx-question-list.el b/sx-question-list.el index c562679..bb60a35 100644 --- a/sx-question-list.el +++ b/sx-question-list.el @@ -210,7 +210,10 @@ 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.") + "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. diff --git a/sx-question-mode.el b/sx-question-mode.el index db9cc8b..4e40e2c 100644 --- a/sx-question-mode.el +++ b/sx-question-mode.el @@ -304,7 +304,7 @@ DATA can represent a question or an answer." (defun sx-question-mode--print-comment (comment-data) "Print the comment described by alist COMMENT-DATA. -The comment is printed according to +The comment is indented, filled, and then printed according to `sx-question-mode-comments-format'." (sx-assoc-let comment-data (insert @@ -356,8 +356,9 @@ 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. -Use as (fn header value face - [header value face] ...)" +Syntax: + + \(fn HEADER VALUE FACE [HEADER VALUE FACE] [HEADER VALUE FACE] ...)" (while args (insert (propertize (pop args) 'face 'sx-question-mode-header) diff --git a/sx-question.el b/sx-question.el index 344b54c..cccf525 100644 --- a/sx-question.el +++ b/sx-question.el @@ -49,7 +49,9 @@ (defun sx-question-get-questions (site &optional page) "Get the page PAGE of questions from SITE. -Return a list of questions, each consed with (site SITE). +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 -- cgit v1.2.3 From de538d650f1f0ae3b9c747a7630932ffef91e9d6 Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Wed, 19 Nov 2014 10:57:01 -0500 Subject: Provide alias for `sx-auth-authenticate' --- sx-auth.el | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sx-auth.el b/sx-auth.el index 4299f37..99344c0 100644 --- a/sx-auth.el +++ b/sx-auth.el @@ -87,6 +87,8 @@ parsed and displayed prominently on the page)." (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 -- cgit v1.2.3 From 54d19079879332b0ff034c290baf1fe79a697f10 Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Wed, 19 Nov 2014 10:57:16 -0500 Subject: Follow package.el convention --- sx-question-list.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sx-question-list.el b/sx-question-list.el index bb60a35..cad67a1 100644 --- a/sx-question-list.el +++ b/sx-question-list.el @@ -381,7 +381,7 @@ completions from `sx-site-get-api-tokens'. Sets (defvar sx-question-list--buffer nil "Buffer where the list of questions is displayed.") -(defun sx-list-questions (no-update) +(defun list-questions (no-update) "Display a list of StackExchange questions. NO-UPDATE is passed to `sx-question-list-refresh'." @@ -395,7 +395,7 @@ NO-UPDATE is passed to `sx-question-list-refresh'." (sx-question-list-refresh 'redisplay no-update)) (switch-to-buffer sx-question-list--buffer)) -(defalias 'list-questions #'sx-list-questions) +(defalias 'sx-list-questions #'list-questions) (provide 'sx-question-list) ;;; sx-question-list.el ends here -- cgit v1.2.3 From 48e3e7bfd485901e90d807e4470ce8d192459933 Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Wed, 19 Nov 2014 10:57:43 -0500 Subject: Use `user-error' when there's no question at point --- sx-question-mode.el | 2 +- sx-request.el | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/sx-question-mode.el b/sx-question-mode.el index 4e40e2c..37d50f9 100644 --- a/sx-question-mode.el +++ b/sx-question-mode.el @@ -456,7 +456,7 @@ URL is used as 'help-echo and 'url properties." (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 fallback-id) "Find url identified by reference ID in current buffer. diff --git a/sx-request.el b/sx-request.el index aeb2931..c49a62d 100644 --- a/sx-request.el +++ b/sx-request.el @@ -130,6 +130,7 @@ the main content of the response is returned." (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 "Headers missing; response corrupt") -- cgit v1.2.3 From b2e59bb1f230267ec5257582478117737cc10049 Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Thu, 20 Nov 2014 20:46:52 -0600 Subject: GitHub comments -- #77 --- sx-auth.el | 4 ---- sx-cache.el | 10 +++------- sx-encoding.el | 16 ++++++---------- sx-favorites.el | 5 ----- sx-filter.el | 2 -- sx-method.el | 5 ++--- sx-networks.el | 6 ------ sx-question-list.el | 26 ++++++++------------------ sx-question-mode.el | 46 +++++++++++++--------------------------------- sx-question.el | 15 +++++---------- sx-request.el | 16 ++++++++-------- sx-site.el | 4 ++-- sx-time.el | 2 -- sx.el | 21 +++++++-------------- 14 files changed, 54 insertions(+), 124 deletions(-) diff --git a/sx-auth.el b/sx-auth.el index 99344c0..14453ac 100644 --- a/sx-auth.el +++ b/sx-auth.el @@ -19,8 +19,6 @@ ;;; Commentary: -;; - ;;; Code: (require 'sx) @@ -36,14 +34,12 @@ (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). diff --git a/sx-cache.el b/sx-cache.el index 049e171..9f152e2 100644 --- a/sx-cache.el +++ b/sx-cache.el @@ -47,7 +47,6 @@ (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. @@ -64,8 +63,7 @@ CACHE is resolved to a file name by `sx-cache-get-file-name'." (sx-cache-set cache (eval form))))) (defun sx-cache-set (cache data) - "Set the content of CACHE to DATA and save changes permanently. - + "Set the content of CACHE to DATA and save. DATA will be written as returned by `prin1'. CACHE is resolved to a file name by `sx-cache-get-file-name'." @@ -86,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 8af020e..f683615 100644 --- a/sx-encoding.el +++ b/sx-encoding.el @@ -61,7 +61,6 @@ upsilon "υ" uuml "ü" Uuml "Ü" weierp "℘" Xi "Ξ" xi "ξ" yacute "ý" Yacute "Ý" yen "¥" yuml "ÿ" Yuml "Ÿ" Zeta "Ζ" zeta "ζ" zwj "" zwnj "") "Plist of HTML entities and their respective glyphs. - See `sx-encoding-decode-entities'." :type '(repeat (choice symbol string)) :group 'sx) @@ -89,7 +88,6 @@ Return the decoded string." (defun sx-encoding-normalize-line-endings (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 @@ -98,7 +96,6 @@ conversion." (defun sx-encoding-clean-content (string) "Clean STRING for display. - Applies `sx-encoding-normalize-line-endings' and `sx-encoding-decode-entities' (in that order) to prepare STRING for sane display." @@ -135,8 +132,7 @@ some cases." (t data)))) (defun sx-encoding-gzipped-p (data) - "Checks for magic bytes in DATA. - + "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. @@ -145,15 +141,15 @@ See URL `http://www.gzip.org/zlib/rfc-gzip.html'." (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. - +(defun sx-encoding-gzipped-buffer-p (buffer) + "Check if BUFFER is gzip-compressed. See `sx-encoding-gzipped-p'." - (sx-encoding-gzip-check-magic (buffer-string))) + (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 `sx-encoding-gzipped-p'." (let ((first-two-bytes (with-temp-buffer (set-buffer-multibyte nil) 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 5c878c5..38084b9 100644 --- a/sx-filter.el +++ b/sx-filter.el @@ -34,7 +34,6 @@ (defvar sx--filter-alist (sx-cache-get 'filter) "An alist of known filters. See `sx-filter-compile'. - Structure: (((INCLUDE EXCLUDE BASE ) . \"compiled filter \") @@ -47,7 +46,6 @@ Structure: ;;; @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. Returns the compiled filter as a string." diff --git a/sx-method.el b/sx-method.el index 2d8f9d2..8909a2b 100644 --- a/sx-method.el +++ b/sx-method.el @@ -34,6 +34,7 @@ (defun sx-method-call (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 @@ -42,9 +43,7 @@ token set. If USE-POST is non-nil, use `POST' rather than `GET' for passing arguments. -Return the response content as a complex alist. - -See `sx-request-make' and `sx-filter-get-var'." +Return the response content as a complex alist." (sx-request-make method (cons (cons 'filter (sx-filter-get-var filter)) keyword-arguments) 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 cad67a1..9e94536 100644 --- a/sx-question-list.el +++ b/sx-question-list.el @@ -161,7 +161,7 @@ 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))) @@ -211,13 +211,11 @@ Non-interactively, DATA is a question alist." (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. - If REDISPLAY is non-nil (or if interactive), also call `tabulated-list-print'. If the prefix argument NO-UPDATE is nil, query StackExchange for a new list before redisplaying." @@ -248,14 +246,13 @@ 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 (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 @@ -293,16 +290,14 @@ See `sx-question-list-refresh'." (propertize " " 'display "\n"))))))) (defun sx-question-list-view-previous (n) - "Move to the previous question and 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) - "Move to the next question and 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") @@ -310,22 +305,19 @@ that may currently be there." (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. - When DATA is nil, display question under point. When FOCUS is non-nil (the default when called interactively), also focus the relevant window." @@ -364,8 +356,7 @@ relevant window." (defun sx-question-list-switch-site (site) "Switch the current site to SITE and display its questions. - -Uses `ido-completing-read' if `ido-mode' is active. Retrieves +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'." @@ -383,7 +374,6 @@ completions from `sx-site-get-api-tokens'. Sets (defun list-questions (no-update) "Display a list of StackExchange questions. - NO-UPDATE is passed to `sx-question-list-refresh'." (interactive "P") (sx-initialize) diff --git a/sx-question-mode.el b/sx-question-mode.el index 37d50f9..f8a0d1e 100644 --- a/sx-question-mode.el +++ b/sx-question-mode.el @@ -51,7 +51,6 @@ (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." @@ -65,9 +64,7 @@ Returns the question buffer." (defun sx-question-mode--display-buffer (window) "Display and return the buffer used for displaying a question. - 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) @@ -88,7 +85,7 @@ If WINDOW is given, use that to display the buffer." ;;;; 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 @@ -150,14 +147,12 @@ 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. - 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 @@ -181,7 +176,6 @@ the 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." :type 'string @@ -197,7 +191,6 @@ replaced with the comment." ;;;; 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 @@ -213,21 +206,20 @@ 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. - DATA can represent a question or an answer." ;; This makes `data' accessible through ;; `(get-text-property (point) 'sx-question-mode--data-here)' @@ -303,7 +295,6 @@ DATA can represent a question or an answer." (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 @@ -321,8 +312,7 @@ The comment is indented, filled, and then printed according to 3))))) (defmacro sx-question-mode--wrap-in-overlay (properties &rest body) - "Execute BODY and wrap any inserted text in an overlay. - + "Start a scope with overlay PROPERTIES and execute BODY. Overlay is pushed on `sx-question-mode--overlays' and given PROPERTIES. @@ -339,8 +329,7 @@ Return the result of BODY." result)) (defmacro sx-question-mode--wrap-in-text-property (properties &rest body) - "Execute BODY and add PROPERTIES to any inserted text. - + "Start a scope with PROPERTIES and execute BODY. Return the result of BODY." (declare (indent 1) (debug t)) @@ -351,7 +340,6 @@ Return the result of BODY." (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. @@ -430,14 +418,13 @@ Syntax: (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 url 'face 'default) (propertize "RET" 'face 'font-lock-function-name-face)) ;; In case we need it. 'url url @@ -460,7 +447,6 @@ URL is used as 'help-echo and 'url properties." (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 @@ -489,7 +475,6 @@ If ID is nil, use FALLBACK-ID instead." ;; for comments). (defcustom sx-question-mode-recenter-line 1 "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. @@ -500,7 +485,6 @@ If nil, no recentering is performed." (defun sx-question-mode-next-section (&optional n) "Move down to next section (question or answer) of this buffer. - Prefix argument N moves N sections down or up." (interactive "p") (let ((count (if n (abs n) 1))) @@ -523,14 +507,12 @@ 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." @@ -545,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 @@ -567,7 +549,6 @@ If DIRECTION is negative, move backwards instead." ;;; Major-mode (define-derived-mode sx-question-mode markdown-mode "Question" "Major mode to display and navigate a question and its answers. - Letters do not insert themselves; instead, they are commands. \\ @@ -616,7 +597,6 @@ Letters do not insert themselves; instead, they are commands. (defun sx-question-mode-refresh () "Refresh currently displayed question. - Queries the API for any changes to the question or its answers or comments, and redisplays it." (interactive) diff --git a/sx-question.el b/sx-question.el index cccf525..f6b7beb 100644 --- a/sx-question.el +++ b/sx-question.el @@ -43,12 +43,11 @@ answer.body_markdown answer.comments) (user.profile_image shallow_user.profile_image)) - "The filter applied with `sx-question-get-questions' and - `sx-question-get-question'.") + "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. @@ -64,7 +63,6 @@ property. (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" question-id) @@ -79,7 +77,7 @@ If QUESTION-ID doesn't exist on SITE, raise an error." ;;; 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 @@ -92,7 +90,6 @@ where each element in QUESTION-LIST has the form (defun sx-question--ensure-read-list (site) "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 @@ -100,7 +97,6 @@ If no cache exists for it, initialize one with SITE." (defun sx-question--read-p (question) "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) @@ -111,7 +107,6 @@ See `sx-question--user-read-list'." (defun sx-question--mark-read (question) "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) @@ -136,7 +131,7 @@ See `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 diff --git a/sx-request.el b/sx-request.el index c49a62d..906785b 100644 --- a/sx-request.el +++ b/sx-request.el @@ -20,7 +20,7 @@ ;;; Commentary: ;; API requests are handled on three separate tiers: -;; +;; ;; `sx-method-call': ;; ;; This is the function that should be used most often, since it @@ -39,7 +39,7 @@ ;; 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. @@ -70,7 +70,6 @@ (defcustom sx-request-unzip-program "gunzip" "Program used to unzip the response if it is compressed. - This program must accept compressed data on standard input." :group 'sx-request :type 'string) @@ -78,13 +77,11 @@ This program must accept compressed data on standard input." (defvar sx-request-remaining-api-requests nil "The number of API requests remaining. - Set by `sx-request-make'.") (defcustom sx-request-remaining-api-requests-message-threshold 50 "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." @@ -97,10 +94,12 @@ number of requests left every time it finishes a call." (defun sx-request-make (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'). @@ -176,12 +175,13 @@ the main content of the response is returned." (defun sx-request--build-keyword-arguments (alist &optional 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 +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'." diff --git a/sx-site.el b/sx-site.el index 4e880b1..66d78dc 100644 --- a/sx-site.el +++ b/sx-site.el @@ -42,9 +42,10 @@ related_site.relation) nil 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 +55,6 @@ (defcustom sx-site-favorites nil "List of favorite sites. - Each entry is a string corresponding to a single site's api_site_parameter." :group 'sx-site) diff --git a/sx-time.el b/sx-time.el index 1ce0886..9c4dfaa 100644 --- a/sx-time.el +++ b/sx-time.el @@ -51,14 +51,12 @@ (defcustom sx-time-date-format-year "%H:%M %e %b %Y" "Format used for dates on a past year. - See also `sx-time-date-format'." :type 'string :group 'sx-time) (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) diff --git a/sx.el b/sx.el index b1730c5..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 () @@ -71,7 +72,6 @@ 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 @@ -98,7 +98,7 @@ For example: (prop4 . t)) '(prop1 (prop3 test2))) -would yeild +would yield ((prop1 . value1) (prop3 @@ -132,8 +132,7 @@ would yeild ;;; 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) @@ -148,9 +147,8 @@ 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 with dotted symbols let-bound 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 @@ -172,20 +170,17 @@ is equivalent to (defcustom sx-init-hook nil "Hook run when stack-mode initializes. - 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 predicate) "Non-nil if PROPERTY attribute of alist X is less than that of Y. - With optional argument PREDICATE, use it instead of `<'." (funcall (or predicate #'<) (cdr (assoc property x)) @@ -193,7 +188,6 @@ With optional argument PREDICATE, use it instead of `<'." (defmacro sx-init-variable (variable value &optional setter) "Set VARIABLE to VALUE using SETTER. - SETTER should be a function of two arguments. If SETTER is nil, `set' is used." (eval @@ -209,7 +203,6 @@ 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." -- cgit v1.2.3 From fd6b8111a13c042e5d0f2f3b689043c394c6e52d Mon Sep 17 00:00:00 2001 From: Sean Allred Date: Thu, 20 Nov 2014 21:00:39 -0600 Subject: Reflect new purpose of sx.org --- sx.org | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sx.org b/sx.org index 10adf1c..b646b2c 100644 --- a/sx.org +++ b/sx.org @@ -82,11 +82,13 @@ question at point. - ~sx-init-hook~ :: Run when ~sx-initialize~ is called. -* About this Document +* Contributing This document is maintained in Org format. Updates to the source code -should almost always be accompanied by updates to this document. Some -distinctions are made which may not be apparent when viewing the -document with Info. +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: -- cgit v1.2.3