aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sx-auth.el56
-rw-r--r--sx-cache.el66
-rw-r--r--sx-encoding.el83
-rw-r--r--sx-filter.el62
-rw-r--r--sx-lto.el43
-rw-r--r--sx-network.el16
-rw-r--r--sx-question-list.el241
-rw-r--r--sx-question.el72
-rw-r--r--sx-request.el194
-rw-r--r--sx-time.el54
-rw-r--r--sx.el359
11 files changed, 683 insertions, 563 deletions
diff --git a/sx-auth.el b/sx-auth.el
index 0da2f4c..59be452 100644
--- a/sx-auth.el
+++ b/sx-auth.el
@@ -1,4 +1,4 @@
-;;; stack-auth.el --- user authentication for stack-mode -*- lexical-binding: t; -*-
+;;; sx-auth.el --- user authentication -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,19 +19,21 @@
;;; Commentary:
-;;
+;;
;;; Code:
-(require 'stack-core)
+(require 'sx)
+(require 'sx-request)
+(require 'sx-cache)
-(defconst stack-auth-root
- "https://stackexchange.com/oauth/dialog")
-(defconst stack-auth--redirect-uri
+(defconst sx-auth-root
+ "https://stackexchange.com/oauth/")
+(defconst sx-auth-redirect-uri
"http://vermiculus.github.io/stack-mode/auth/auth.htm")
-(defconst stack-auth--client-id
+(defconst sx-auth-client-id
"3291")
-(defvar stack-auth-access-token
+(defvar sx-auth-access-token
nil
"Your access token.
@@ -39,7 +41,7 @@ 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 stack-authenticate ()
+(defun sx-auth-authenticate ()
"Authenticate this application.
Authentication is required to read your personal data (such as
@@ -47,23 +49,27 @@ notifications) and to write with the API (asking and answering
questions)."
(interactive)
(setq
- stack-auth-access-token
- (when (browse-url
- (let ((stack-core-api-root stack-auth-root)
- (stack-core-api-batch-request-separator ","))
- (stack-core-build-request
- nil
- `((client_id . ,stack-auth--client-id)
- (scope . (read_inbox
- no_expiry
- write_access))
- (redirect_uri . ,(url-hexify-string
- stack-auth--redirect-uri))))))
+ sx-auth-access-token
+ (let* ((sx-request-api-root sx-auth-root)
+ (url (sx-request--build
+ "dialog"
+ `((client_id . ,sx-auth-client-id)
+ (scope . (read_inbox
+ no_expiry
+ write_access))
+ (redirect_uri . ,(url-hexify-string
+ sx-auth-redirect-uri)))
+ ",")))
+ (browse-url url)
(read-string "Enter the access token displayed on the webpage: ")))
- (if (string-equal "" stack-auth-access-token)
- (progn (setq stack-auth-access-token nil)
+ (if (string-equal "" sx-auth-access-token)
+ (progn (setq sx-auth-access-token nil)
(error "You must enter this code to use this client fully"))
- (stack-cache-set "auth.el" `((access-token . ,stack-auth-access-token)))))
+ (sx-cache-set "auth.el" `((access-token . ,sx-auth-access-token)))))
-(provide 'stack-auth)
+(provide 'sx-auth)
;;; stack-auth.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-cache.el b/sx-cache.el
new file mode 100644
index 0000000..a090982
--- /dev/null
+++ b/sx-cache.el
@@ -0,0 +1,66 @@
+;;; sx-cache.el --- caching for stack-mode
+
+;; Copyright (C) 2014 Sean Allred
+
+;; Author: Sean Allred <sallred@calamity.tcs.com>
+;; Keywords: help
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+
+(defcustom sx-cache-directory
+ (expand-file-name ".stackmode" user-emacs-directory)
+ "Directory containined cached files and precompiled filters.")
+
+(defun sx-cache-get-file-name (filename)
+ "Expands FILENAME in the context of `sx-cache-directory'."
+ (expand-file-name filename sx-cache-directory))
+
+(defun sx-cache-get (cache)
+ "Return the data within CACHE.
+
+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))
+ (let ((file (sx-cache-get-file-name cache)))
+ (when (file-exists-p file)
+ (with-temp-buffer
+ (insert-file-contents (sx-cache-get-file-name cache))
+ (read (buffer-string))))))
+
+(defun sx-cache-set (cache data)
+ "Set the content of CACHE to DATA.
+
+As with `sx-cache-get', CACHE is a file name within the
+context of `sx-cache-directory'.
+
+DATA will be written as returned by `prin1'."
+ (unless (file-exists-p sx-cache-directory)
+ (mkdir sx-cache-directory))
+ (write-region (prin1-to-string data) nil
+ (sx-cache-get-file-name cache))
+ data)
+
+(provide 'sx-cache)
+;;; sx-cache.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-encoding.el b/sx-encoding.el
new file mode 100644
index 0000000..efb333e
--- /dev/null
+++ b/sx-encoding.el
@@ -0,0 +1,83 @@
+;;; sx-encoding.el --- encoding for stack-mode
+
+;; Copyright (C) 2014 Sean Allred
+
+;; Author: Sean Allred <sallred@calamity.tcs.com>
+;; Keywords: help
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+
+(defcustom sx-encoding-html-entities-plist
+ '(Aacute "Á" aacute "á" Acirc "Â" acirc "â" acute "´" AElig "Æ" aelig "æ"
+ Agrave "À" agrave "à" alefsym "ℵ" Alpha "Α" alpha "α" amp "&" and "∧"
+ ang "∠" apos "'" aring "å" Aring "Å" asymp "≈" atilde "ã" Atilde "Ã"
+ auml "ä" Auml "Ä" bdquo "„" Beta "Β" beta "β" brvbar "¦" bull "•"
+ cap "∩" ccedil "ç" Ccedil "Ç" cedil "¸" cent "¢" Chi "Χ" chi "χ"
+ circ "ˆ" clubs "♣" cong "≅" copy "©" crarr "↵" cup "∪" curren "¤"
+ Dagger "‡" dagger "†" darr "↓" dArr "⇓" deg "°" Delta "Δ" delta "δ"
+ diams "♦" divide "÷" eacute "é" Eacute "É" ecirc "ê" Ecirc "Ê" egrave "è"
+ Egrave "È" empty "∅" emsp " " ensp " " Epsilon "Ε" epsilon "ε" equiv "≡"
+ Eta "Η" eta "η" eth "ð" ETH "Ð" euml "ë" Euml "Ë" euro "€"
+ exist "∃" fnof "ƒ" forall "∀" frac12 "½" frac14 "¼" frac34 "¾" frasl "⁄"
+ Gamma "Γ" gamma "γ" ge "≥" gt ">" harr "↔" hArr "⇔" hearts "♥"
+ hellip "…" iacute "í" Iacute "Í" icirc "î" Icirc "Î" iexcl "¡" igrave "ì"
+ Igrave "Ì" image "ℑ" infin "∞" int "∫" Iota "Ι" iota "ι" iquest "¿"
+ isin "∈" iuml "ï" Iuml "Ï" Kappa "Κ" kappa "κ" Lambda "Λ" lambda "λ"
+ lang "〈" laquo "«" larr "←" lArr "⇐" lceil "⌈" ldquo "“" le "≤"
+ lfloor "⌊" lowast "∗" loz "◊" lrm "" lsaquo "‹" lsquo "‘" lt "<"
+ macr "¯" mdash "—" micro "µ" middot "·" minus "−" Mu "Μ" mu "μ"
+ nabla "∇" nbsp "" ndash "–" ne "≠" ni "∋" not "¬" notin "∉"
+ nsub "⊄" ntilde "ñ" Ntilde "Ñ" Nu "Ν" nu "ν" oacute "ó" Oacute "Ó"
+ ocirc "ô" Ocirc "Ô" OElig "Œ" oelig "œ" ograve "ò" Ograve "Ò" oline "‾"
+ omega "ω" Omega "Ω" Omicron "Ο" omicron "ο" oplus "⊕" or "∨" ordf "ª"
+ ordm "º" oslash "ø" Oslash "Ø" otilde "õ" Otilde "Õ" otimes "⊗" ouml "ö"
+ Ouml "Ö" para "¶" part "∂" permil "‰" perp "⊥" Phi "Φ" phi "φ"
+ Pi "Π" pi "π" piv "ϖ" plusmn "±" pound "£" Prime "″" prime "′"
+ prod "∏" prop "∝" Psi "Ψ" psi "ψ" quot "\"" radic "√" rang "〉"
+ raquo "»" rarr "→" rArr "⇒" rceil "⌉" rdquo "”" real "ℜ" reg "®"
+ rfloor "⌋" Rho "Ρ" rho "ρ" rlm "" rsaquo "›" rsquo "’" sbquo "‚"
+ scaron "š" Scaron "Š" sdot "⋅" sect "§" shy "" Sigma "Σ" sigma "σ"
+ sigmaf "ς" sim "∼" spades "♠" sub "⊂" sube "⊆" sum "∑" sup "⊃"
+ sup1 "¹" sup2 "²" sup3 "³" supe "⊇" szlig "ß" Tau "Τ" tau "τ"
+ there4 "∴" Theta "Θ" theta "θ" thetasym "ϑ" thinsp " " thorn "þ" THORN "Þ"
+ tilde "˜" times "×" trade "™" uacute "ú" Uacute "Ú" uarr "↑" uArr "⇑"
+ 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."
+ :type '(repeat (choice symbol string))
+ :group 'sx)
+
+(defun sx-encoding-decode-entities (string)
+ (let* ((plist sx-encoding-html-entities-plist)
+ (get-function (lambda (s) (let ((ss (substring s 1 -1)))
+ ;; Handle things like &quot;
+ (or (plist-get plist (intern ss))
+ ;; Handle things like &#39;
+ (format "%c" (string-to-number
+ (substring ss 1))))))))
+ (replace-regexp-in-string "&[^; ]*;" get-function string)))
+
+(provide 'sx-encoding)
+;;; sx-encoding.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-filter.el b/sx-filter.el
index 6b56001..7178259 100644
--- a/sx-filter.el
+++ b/sx-filter.el
@@ -1,4 +1,4 @@
-;;; stack-filter.el --- filters for stack-mode -*- lexical-binding: t; -*-
+;;; sx-filter.el --- filters -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,49 +19,45 @@
;;; Commentary:
-;;
+;;
;;; Code:
;;; Dependencies
-(require 'stack-core)
+(require 'sx)
+(require 'sx-cache)
;;; Customizations
-(defconst stack-filter-cache-file
+(defconst sx-filter-cache-file
"filters.el")
-(defvar stack-filter
+(defvar sx-filter
'default
"The current filter.
-To customize the filter for the next call
-to `stack-core-make-request', let-bind this variable to the
-output of a call to `stack-core-compile-filter'. Be careful! If
-you're going to be using this new filter a lot, create a variable
-for it. Creation requests count against
-`stack-core-remaining-api-requests'!")
+To customize the filter for the next call to `sx-request-make',
+let-bind this variable to the output of a call to
+`sx-filter-compile'. Be careful! If you're going to be using
+this new filter a lot, create a variable for it. Creation
+requests count against `sx-request-remaining-api-requests'!")
;;; Compilation
;;; TODO allow BASE to be a precompiled filter name
-(defun stack-filter-compile (&optional include exclude base)
+(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."
(let ((keyword-arguments
- `((include . ,(if include (mapconcat
- #'stack-core-thing-as-string
- include ";")))
- (exclude . ,(if exclude (mapconcat
- #'stack-core-thing-as-string
- exclude ";")))
+ `((include . ,(if include (sx--thing-as-string include)))
+ (exclude . ,(if exclude (sx--thing-as-string exclude)))
(base . ,(if base base)))))
- (let ((response (stack-core-make-request
+ (let ((response (sx-request-make
"filter/create"
keyword-arguments)))
(url-hexify-string
@@ -71,15 +67,15 @@ or string."
;;; Storage and Retrieval
-(defun stack-filter-get (filter)
- "Retrieve named FILTER from `stack-filter-cache-file'."
- (cdr (assoc filter (stack-cache-get stack-filter-cache-file))))
+(defun sx-filter-get (filter)
+ "Retrieve named FILTER from `sx-filter-cache-file'."
+ (cdr (assoc filter (sx-cache-get sx-filter-cache-file))))
-(defun stack-filter-store (name &optional filter)
- "Store NAME as FILTER in `stack-filter-cache-file'.
+(defun sx-filter-store (name &optional filter)
+ "Store NAME as FILTER in `sx-filter-cache-file'.
NAME should be a symbol and FILTER is a string as compiled by
-`stack-filter-compile'.
+`sx-filter-compile'.
If NAME is a cons cell, (car NAME) is taken to be the actual NAME
and (cdr NAME) is taken to be the actual FILTER. In this case,
@@ -88,15 +84,19 @@ the second argument is simply ignored."
(filter (if (consp name) (cdr name) filter)))
(unless (symbolp name)
(error "Name must be a symbol: %S" name))
- (let* ((dict (stack-cache-get stack-filter-cache-file))
+ (let* ((dict (sx-cache-get sx-filter-cache-file))
(entry (assoc name dict)))
(if entry (setcdr entry filter)
(setq dict (cons (cons name filter) dict)))
- (stack-cache-set stack-filter-cache-file dict))))
+ (sx-cache-set sx-filter-cache-file dict))))
-(defun stack-filter-store-all (name-filter-alist)
- (mapc #'stack-filter-store name-filter-alist))
+(defun sx-filter-store-all (name-filter-alist)
+ (mapc #'sx-filter-store name-filter-alist))
-(provide 'stack-filter)
-;;; stack-filter.el ends here
+(provide 'sx-filter)
+;;; sx-filter.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-lto.el b/sx-lto.el
index 65b57b2..6bdd5d0 100644
--- a/sx-lto.el
+++ b/sx-lto.el
@@ -1,4 +1,4 @@
-;;; stack-core.el --- lisp-to-org conversion functions for stack-mode -*- lexical-binding: t; -*-
+;;; sx-lto.el --- lisp-to-org conversion functions -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -23,47 +23,46 @@
;;; Requirements
-(require 'stack-core)
-(require 'json)
+(require 'sx)
(require 'org)
-(defun stack-lto--question (data)
+(defun sx-lto--question (data)
"Return question DATA in a format acceptable by `org-element-interpret-data'.
DATA is a list of cons cells representing a question, as received
by the API and read by `json-read'."
`(headline (:title ,(cdr (assoc 'title data))
:level 1
:tags ,(mapcar #'identity (cdr (assoc 'tags data))))
- ,(stack-lto--question-answer data)
- ,@(mapcar #'stack-lto--answer (cdr (assoc 'answers data)))))
+ ,(sx-lto--question-answer data)
+ ,@(mapcar #'sx-lto--answer (cdr (assoc 'answers data)))))
-(defun stack-lto--answer (data)
+(defun sx-lto--answer (data)
"Return answer DATA in a format acceptable by `org-element-interpret-data'.
DATA is a list of cons cells representing a question, as received
by the API and read by `json-read'."
;; Right now this doesn't do anything special. But it should check
;; whether the answer is accepted. How do we display that?
`(headline (:title "Answer" :level 2)
- ,(stack-lto--question-answer data)))
+ ,(sx-lto--question-answer data)))
-(defun stack-lto--question-answer (data)
+(defun sx-lto--question-answer (data)
"Process and return the elements of DATA which questions and answers have in common."
(let ((comments
- (mapcar #'stack-lto--comment (cdr (assoc 'comments data)))))
+ (mapcar #'sx-lto--comment (cdr (assoc 'comments data)))))
`(;; Body as a src block (really NOT nice).
- (src-block (:value ,(stack-lto--body data)
- . ,stack-lto--body-src-block))
+ (src-block (:value ,(sx-lto--body data)
+ . ,sx-lto--body-src-block))
;; Comments as descriptive lists. If there are no comments, an
;; empty list would throw an error.
,@(when comments `((plain-list (:type descriptive) ,comments))))))
;;; Body rendering
-(defvar stack-lto--body-src-block
+(defvar sx-lto--body-src-block
'(:language "markdown" :switches nil :parameters nil :hiddenp nil)
"Properties used on the markdown src-block which represents the body.")
-(defface stack-lto-body
+(defface sx-lto-body
'((((background light)) :background "Grey90")
(((background dark)) :background "Grey10"))
"Face used on the body content of questions and answers."
@@ -71,12 +70,12 @@ by the API and read by `json-read'."
;;; This is not used ATM since we got rid of HTML. But it can be used
;;; once we start extending markdown mode.
-(defcustom stack-lto-bullet (if (char-displayable-p ?•) " •" " -")
+(defcustom sx-lto-bullet (if (char-displayable-p ?•) " •" " -")
"Bullet used on the display of lists."
:type 'string
:group 'stack-mode)
-(defun stack-lto--body (data)
+(defun sx-lto--body (data)
"Get and cleanup `body_markdown' from DATA."
(concat
(replace-regexp-in-string
@@ -85,20 +84,20 @@ by the API and read by `json-read'."
;; We need to add padding in case the body contains a * at column 1
;; (which would break org-mode).
-(defvar stack-lto--padding
+(defvar sx-lto--padding
(propertize "  " 'display " ")
"Left-padding added to each line of a body.")
-(defvar stack-lto-comment-item
+(defvar sx-lto-comment-item
'(:bullet "- " :checkbox nil :counter nil :hiddenp nil)
"Properties used on the items which represent comments.")
-(defun stack-lto--comment (data)
+(defun sx-lto--comment (data)
""
(let* ((owner (cdr (assoc 'owner data)))
(owner-name (cdr (assoc 'display_name owner))))
- `(item (:tag ,owner-name . ,stack-lto-comment-item)
+ `(item (:tag ,owner-name . ,sx-lto-comment-item)
(paragraph () ,(cdr (assoc 'body_markdown data))))))
-(provide 'stack-lto)
-;;; stack-core.el ends here
+(provide 'sx-lto)
+;;; sx.el ends here
diff --git a/sx-network.el b/sx-network.el
index 1d3af0a..dcd2349 100644
--- a/sx-network.el
+++ b/sx-network.el
@@ -1,4 +1,4 @@
-;;; stack-network.el --- network functions for stack-mode -*- lexical-binding: t; -*-
+;;; sx-network.el --- browsing networks -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,14 +19,18 @@
;;; Commentary:
-;;
+;;
;;; Code:
-(require 'stack-core)
+(require 'sx-request)
-(defun stack-network-get-networks ()
- (stack-core-make-request "sites"))
+(defun sx-network-get-networks ()
+ (sx-request-make "sites"))
-(provide 'stack-network)
+(provide 'sx-network)
;;; stack-network.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-question-list.el b/sx-question-list.el
index 81d7cd5..59acbeb 100644
--- a/sx-question-list.el
+++ b/sx-question-list.el
@@ -1,4 +1,4 @@
-;;; stack-question-list.el --- Major-mode for navigating questions list. -*- lexical-binding: t; -*-
+;;; sx-question-list.el --- Major-mode for navigating questions list. -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -20,235 +20,236 @@
;;; Commentary:
;;; Code:
-(require 'stack-question)
+(require 'sx-question)
+(require 'sx-time)
(require 'tabulated-list)
(require 'cl-lib)
;;; Customization
-(defcustom stack-question-list-height 12
+(defcustom sx-question-list-height 12
"Height, in lines, of stack-mode's *question-list* buffer."
:type 'integer
- :group 'stack-question-list)
+ :group 'sx-question-list)
-(defface stack-question-list-parent
+(defface sx-question-list-parent
'((t :inherit default))
""
- :group 'stack-question-list-faces)
+ :group 'sx-question-list-faces)
-(defface stack-question-list-answers
+(defface sx-question-list-answers
'((((background light)) :foreground "SeaGreen4"
- :height 1.0 :inherit stack-question-list-parent)
+ :height 1.0 :inherit sx-question-list-parent)
(((background dark)) :foreground "#D1FA71"
- :height 1.0 :inherit stack-question-list-parent)
- (t :inherit stack-question-list-parent))
+ :height 1.0 :inherit sx-question-list-parent)
+ (t :inherit sx-question-list-parent))
""
- :group 'stack-question-list-faces)
+ :group 'sx-question-list-faces)
-(defface stack-question-list-answers-accepted
+(defface sx-question-list-answers-accepted
'((((background light)) :background "YellowGreen"
- :inherit stack-question-list-answers)
+ :inherit sx-question-list-answers)
(((background dark)) :background "DarkOliveGreen"
- :inherit stack-question-list-answers)
- (t :inherit stack-question-list-answers))
+ :inherit sx-question-list-answers)
+ (t :inherit sx-question-list-answers))
""
- :group 'stack-question-list-faces)
+ :group 'sx-question-list-faces)
-(defface stack-question-list-score
- '((t :height 1.0 :inherit stack-question-list-parent))
+(defface sx-question-list-score
+ '((t :height 1.0 :inherit sx-question-list-parent))
""
- :group 'stack-question-list-faces)
+ :group 'sx-question-list-faces)
-(defface stack-question-list-score-upvoted
+(defface sx-question-list-score-upvoted
'((t :weight bold
- :inherit stack-question-list-score))
+ :inherit sx-question-list-score))
""
- :group 'stack-question-list-faces)
+ :group 'sx-question-list-faces)
-(defface stack-question-list-tags
+(defface sx-question-list-tags
'((t :inherit font-lock-function-name-face))
""
- :group 'stack-question-list-faces)
+ :group 'sx-question-list-faces)
-(defface stack-question-list-date
+(defface sx-question-list-date
'((t :inherit font-lock-comment-face))
""
- :group 'stack-question-list-faces)
+ :group 'sx-question-list-faces)
-(defface stack-question-list-read-question
- '((t :height 1.0 :inherit stack-question-list-parent))
+(defface sx-question-list-read-question
+ '((t :height 1.0 :inherit sx-question-list-parent))
""
- :group 'stack-question-list-faces)
+ :group 'sx-question-list-faces)
-(defface stack-question-list-unread-question
- '((t :weight bold :inherit stack-question-list-read-question))
+(defface sx-question-list-unread-question
+ '((t :weight bold :inherit sx-question-list-read-question))
""
- :group 'stack-question-list-faces)
+ :group 'sx-question-list-faces)
;;; Mode Definition
-(define-derived-mode stack-question-list-mode tabulated-list-mode "Question List"
- "Major mode for browsing a list of questions from stack-exchange.
+(define-derived-mode sx-question-list-mode tabulated-list-mode "Question List"
+ "Major mode for browsing a list of questions from StackExchange.
Letters do not insert themselves; instead, they are commands.
-\\<stack-question-list>
-\\{stack-question-list}"
+\\<sx-question-list>
+\\{sx-question-list}"
(hl-line-mode 1)
- (stack-question-list--update-mode-line)
+ (sx-question-list--update-mode-line)
(setq tabulated-list-format
[(" V" 3 t :right-align t)
(" A" 3 t :right-align t)
- ("Title" 0 stack-question-list--date-more-recent-p)])
+ ("Title" 0 sx-question-list--date-more-recent-p)])
(setq tabulated-list-padding 1)
;; Sorting by title actually sorts by date. It's what we want, but
;; it's not terribly intuitive.
(setq tabulated-list-sort-key '("Title" . nil))
(add-hook 'tabulated-list-revert-hook
- #'stack-question-list-refresh nil t)
+ #'sx-question-list-refresh nil t)
(add-hook 'tabulated-list-revert-hook
- #'stack-question-list--update-mode-line nil t)
+ #'sx-question-list--update-mode-line nil t)
(tabulated-list-init-header))
-(defcustom stack-question-list-date-sort-method 'last_activity_date
+(defcustom sx-question-list-date-sort-method 'last_activity_date
"Parameter which controls date sorting."
;; This should be made into a (choice ...) of constants.
:type 'symbol
;; Add a setter to protect the value.
- :group 'stack-question-list)
+ :group 'sx-question-list)
-(defun stack-question-list--date-more-recent-p (x y)
+(defun sx-question-list--date-more-recent-p (x y)
"Non-nil if tabulated-entry X is newer than Y."
- (stack-question--<
- stack-question-list-date-sort-method
+ (sx-question--<
+ sx-question-list-date-sort-method
(car x) (car y) #'>))
(mapc
- (lambda (x) (define-key stack-question-list-mode-map
+ (lambda (x) (define-key sx-question-list-mode-map
(car x) (cadr x)))
- '(("n" stack-question-list-next)
- ("p" stack-question-list-previous)
- ("j" stack-question-list-view-next)
- ("k" stack-question-list-view-previous)
- ("g" stack-question-list-refresh)
- ([?\r] stack-question-list-display-question)))
-
-(defvar stack-question-list--current-page "Latest"
+ '(("n" sx-question-list-next)
+ ("p" sx-question-list-previous)
+ ("j" sx-question-list-view-next)
+ ("k" sx-question-list-view-previous)
+ ("g" sx-question-list-refresh)
+ ([?\r] sx-question-list-display-question)))
+
+(defvar sx-question-list--current-page "Latest"
;; Other values (once we implement them) are "Top Voted",
;; "Unanswered", etc.
"Variable describing current page being viewed.")
-(defvar stack-question-list--unread-count 0
+(defvar sx-question-list--unread-count 0
"Holds the number of unread questions in the current buffer.")
-(make-variable-buffer-local 'stack-question-list--unread-count)
+(make-variable-buffer-local 'sx-question-list--unread-count)
-(defvar stack-question-list--total-count 0
+(defvar sx-question-list--total-count 0
"Holds the total number of questions in the current buffer.")
-(make-variable-buffer-local 'stack-question-list--total-count)
+(make-variable-buffer-local 'sx-question-list--total-count)
-(defconst stack-question-list--mode-line-format
+(defconst sx-question-list--mode-line-format
'(" "
mode-name
" "
- (:propertize stack-question-list--current-page
+ (:propertize sx-question-list--current-page
face mode-line-buffer-id)
" ["
"Unread: "
(:propertize
- (:eval (int-to-string stack-question-list--unread-count))
+ (:eval (int-to-string sx-question-list--unread-count))
face mode-line-buffer-id)
", "
"Total: "
(:propertize
- (:eval (int-to-string stack-question-list--total-count))
+ (:eval (int-to-string sx-question-list--total-count))
face mode-line-buffer-id)
"] ")
"Mode-line construct to use in question-list buffers.")
-(defun stack-question-list--update-mode-line ()
+(defun sx-question-list--update-mode-line ()
"Fill the mode-line with useful information."
;; All the data we need is right in the buffer.
- (when (derived-mode-p 'stack-question-list-mode)
+ (when (derived-mode-p 'sx-question-list-mode)
(setq mode-line-format
- stack-question-list--mode-line-format)
- (setq stack-question-list--total-count
+ sx-question-list--mode-line-format)
+ (setq sx-question-list--total-count
(length tabulated-list-entries))))
-(defvar stack-question-list--current-site "emacs"
+(defvar sx-question-list--current-site "emacs"
"Site being displayed in the *question-list* buffer.")
-(defun stack-question-list-refresh (&optional redisplay no-update)
+(defun sx-question-list-refresh (&optional redisplay no-update)
"Update the list of questions.
If REDISPLAY is non-nil, also call `tabulated-list-print'.
-If the prefix argument NO-UPDATE is nil, query stack-exchange for
+If the prefix argument NO-UPDATE is nil, query StackExchange for
a new list before redisplaying."
(interactive "pP")
;; Reset the mode-line unread count (we rebuild it here).
- (setq stack-question-list--unread-count 0)
- (let ((question-list (stack-question-get-questions
- stack-question-list--current-site)))
+ (setq sx-question-list--unread-count 0)
+ (let ((question-list (sx-question-get-questions
+ sx-question-list--current-site)))
;; Print the result.
(setq tabulated-list-entries
- (mapcar #'stack-question-list--print-info question-list)))
+ (mapcar #'sx-question-list--print-info question-list)))
(when redisplay (tabulated-list-print 'remember)))
-(defcustom stack-question-list-ago-string " ago"
+(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 'stack-question-list)
+ :group 'sx-question-list)
-(defun stack-question-list--print-info (data)
+(defun sx-question-list--print-info (data)
"Convert `json-read' DATA into tabulated-list format."
(list
data
(vector
(list (int-to-string (cdr (assoc 'score data)))
'face
- (if (cdr (assoc 'upvoted data)) 'stack-question-list-score-upvoted
- 'stack-question-list-score))
+ (if (cdr (assoc 'upvoted data)) 'sx-question-list-score-upvoted
+ 'sx-question-list-score))
(list (int-to-string (cdr (assoc 'answer_count data)))
'face
- (if (stack-question--accepted-answer data)
- 'stack-question-list-answers-accepted
- 'stack-question-list-answers))
+ (if (sx-question--accepted-answer data)
+ 'sx-question-list-answers-accepted
+ 'sx-question-list-answers))
(concat
(propertize
(cdr (assoc 'title data))
'face
- (if (stack-question--read-p data)
- 'stack-question-list-read-question
- ;; Increment `stack-question-list--unread-count' for the mode-line.
- (cl-incf stack-question-list--unread-count)
- 'stack-question-list-unread-question))
+ (if (sx-question--read-p data)
+ 'sx-question-list-read-question
+ ;; 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 ")
- (propertize (concat (stack--time-since (cdr (assoc 'last_activity_date data)))
- stack-question-list-ago-string)
- 'face 'stack-question-list-date)
+ (propertize (concat (sx-time-since (cdr (assoc 'last_activity_date data)))
+ sx-question-list-ago-string)
+ 'face 'sx-question-list-date)
(propertize (concat " [" (mapconcat #'identity (cdr (assoc 'tags data)) "] [") "]")
- 'face 'stack-question-list-tags)
+ 'face 'sx-question-list-tags)
(propertize " " 'display "\n")))))
-(defun stack-question-list-view-previous (n)
+(defun sx-question-list-view-previous (n)
"Hide this question, move to previous one, display it."
(interactive "p")
- (stack-question-list-view-next (- n)))
+ (sx-question-list-view-next (- n)))
-(defun stack-question-list-view-next (n)
+(defun sx-question-list-view-next (n)
"Hide this question, move to next one, display it."
(interactive "p")
- (stack-question-list-next n)
- (stack-question-list-display-question))
+ (sx-question-list-next n)
+ (sx-question-list-display-question))
-(defun stack-question-list-next (n)
+(defun sx-question-list-next (n)
"Move to the next entry."
(interactive "p")
(forward-line n))
-(defun stack-question-list-previous (n)
+(defun sx-question-list-previous (n)
"Move to the previous entry."
(interactive "p")
- (stack-question-list-next (- n)))
+ (sx-question-list-next (- n)))
-(defun stack-question-list-display-question (&optional data focus)
+(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.
@@ -257,13 +258,13 @@ focus the relevant window."
(interactive '(nil t))
(unless data (setq data (tabulated-list-get-id)))
(unless data (error "No question here!"))
- (when (stack-question--read-p data)
- (cl-decf stack-question-list--unread-count)
- (stack-question--mark-read data))
- (unless (window-live-p stack-question--window)
- (setq stack-question--window
+ (when (sx-question--read-p data)
+ (cl-decf sx-question-list--unread-count)
+ (sx-question--mark-read data))
+ (unless (window-live-p sx-question--window)
+ (setq sx-question--window
(condition-case er
- (split-window-below stack-question-list-height)
+ (split-window-below sx-question-list-height)
(error
;; If the window is too small to split, use current one.
(if (string-match
@@ -271,27 +272,27 @@ focus the relevant window."
(car (cdr-safe er)))
nil
(error (cdr er)))))))
- (stack-question--display data stack-question--window)
+ (sx-question--display data sx-question--window)
(when focus
- (if stack-question--window
- (select-window stack-question--window)
- (switch-to-buffer stack-question--buffer))))
+ (if sx-question--window
+ (select-window sx-question--window)
+ (switch-to-buffer sx-question--buffer))))
-(defvar stack-question-list--buffer nil
+(defvar sx-question-list--buffer nil
"Buffer where the list of questions is displayed.")
(defun list-questions (no-update)
- "Display a list of stack-exchange questions."
+ "Display a list of StackExchange questions."
(interactive "P")
- (unless (buffer-live-p stack-question-list--buffer)
- (setq stack-question-list--buffer
+ (unless (buffer-live-p sx-question-list--buffer)
+ (setq sx-question-list--buffer
(generate-new-buffer "*question-list*")))
- (with-current-buffer stack-question-list--buffer
- (stack-question-list-mode)
- (stack-question-list-refresh 'redisplay no-update))
- (switch-to-buffer stack-question-list--buffer))
+ (with-current-buffer sx-question-list--buffer
+ (sx-question-list-mode)
+ (sx-question-list-refresh 'redisplay no-update))
+ (switch-to-buffer sx-question-list--buffer))
-(defalias 'stack-list-questions #'list-questions)
+(defalias 'sx-list-questions #'list-questions)
-(provide 'stack-question-list)
-;;; stack-question-list.el ends here
+(provide 'sx-question-list)
+;;; sx-question-list.el ends here
diff --git a/sx-question.el b/sx-question.el
index 80d772f..8a39493 100644
--- a/sx-question.el
+++ b/sx-question.el
@@ -1,4 +1,4 @@
-;;; stack-question.el --- question logic for stack-mode -*- lexical-binding: t; -*-
+;;; sx-question.el --- question logic for stack-mode -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,49 +19,49 @@
;;; Commentary:
-;;
+;;
;;; Code:
-(require 'stack-core)
-(require 'stack-filter)
-(require 'stack-lto)
+(require 'sx)
+(require 'sx-filter)
+(require 'sx-lto)
;; I don't know why this is here, but it was causing an API request on require.
-(defvar stack-question-browse-filter nil
+(defvar sx-question-browse-filter nil
;; (stack-filter-compile
;; nil
;; '(user.profile_image shallow_user.profile_image))
)
-;; (stack-filter-store 'question-browse stack-question-browse-filter)
+;; (stack-filter-store 'question-browse sx-question-browse-filter)
-(defun stack-question-get-questions (site &optional page)
+(defun sx-question-get-questions (site &optional page)
"Get the page PAGE of questions from SITE."
- (stack-core-make-request
+ (sx-request-make
"questions"
`((site . ,site)
(page . ,page))
- stack-question-browse-filter))
+ sx-question-browse-filter))
;;; Question Properties
-(defun stack-question--read-p (question)
+(defun sx-question--read-p (question)
"Non-nil if QUESTION has been read since last updated."
;; @TODO:
(cl-evenp (random)))
-(defun stack-question--accepted-answer (question)
+(defun sx-question--accepted-answer (question)
"Return accepted answer in QUESTION, or nil if none."
;; @TODO:
(cl-evenp (random)))
-(defun stack-question--mark-read (question)
+(defun sx-question--mark-read (question)
"Mark QUESTION as being read, until it is updated again."
nil)
-(defun stack-question--< (property x y &optional pred)
+(defun sx-question--< (property x y &optional pred)
"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 #'<)
@@ -69,52 +69,56 @@ With optional argument predicate, use it instead of `<'."
(cdr (assoc property y))))
;;; Displaying a question
-(defvar stack-question--window nil
+(defvar sx-question--window nil
"Window where the content of questions is displayed.")
-(defvar stack-question--buffer nil
+(defvar sx-question--buffer nil
"Buffer being used to display questions.")
-(defcustom stack-question-use-html t
+(defcustom sx-question-use-html t
"If nil, markdown is used for the body."
:type 'boolean
- :group 'stack-question)
+ :group 'sx-question)
-(defun stack-question--display (data &optional window)
+(defun sx-question--display (data &optional window)
"Display question given by DATA on WINDOW.
If WINDOW is nil, use selected one."
- (let ((stack-lto--body-src-block
- (if stack-question-use-html nil
- stack-lto--body-src-block))
+ (let ((sx-lto--body-src-block
+ (if sx-question-use-html nil
+ sx-lto--body-src-block))
(inhibit-read-only t))
(with-current-buffer
- (stack-question--display-buffer window)
+ (sx-question--display-buffer window)
(erase-buffer)
(insert
(org-element-interpret-data
- (stack-lto--question data)))
+ (sx-lto--question data)))
(org-mode)
(show-all)
(view-mode)
(current-buffer))))
-(defun stack-question--display-buffer (window)
+(defun sx-question--display-buffer (window)
"Display and return the buffer used for displaying a question.
Create the buffer if necessary.
If WINDOW is given, use that to display the buffer."
;; Create the buffer if necessary.
- (unless (buffer-live-p stack-question--buffer)
- (setq stack-question--buffer
- (generate-new-buffer "*stack-question*")))
+ (unless (buffer-live-p sx-question--buffer)
+ (setq sx-question--buffer
+ (generate-new-buffer "*sx-question*")))
(cond
;; Window was given, use it.
((window-live-p window)
- (set-window-buffer window stack-question--buffer))
+ (set-window-buffer window sx-question--buffer))
;; No window, but the buffer is already being displayed somewhere.
- ((get-buffer-window stack-question--buffer 'visible))
+ ((get-buffer-window sx-question--buffer 'visible))
;; Neither, so we create the window.
- (t (switch-to-buffer stack-question--buffer)))
- stack-question--buffer)
+ (t (switch-to-buffer sx-question--buffer)))
+ sx-question--buffer)
-(provide 'stack-question)
-;;; stack-question.el ends here
+(provide 'sx-question)
+;;; sx-question.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-request.el b/sx-request.el
new file mode 100644
index 0000000..a62ee0e
--- /dev/null
+++ b/sx-request.el
@@ -0,0 +1,194 @@
+;;; sx-request.el --- requests for stack-mode
+
+;; Copyright (C) 2014 Sean Allred
+
+;; Author: Sean Allred <sallred@calamity.tcs.com>
+;; Keywords:
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+(require 'json)
+(require 'url)
+(require 'sx)
+
+(defcustom sx-request-silent-p
+ t
+ "When `t', requests default to being silent.")
+
+(defcustom sx-request-cache-p
+ t
+ "Cache requests made to the StackExchange API.")
+
+(defcustom sx-request-unzip-program
+ "gunzip"
+ "program used to unzip the response")
+
+(defvar sx-request-remaining-api-requests
+ nil
+ "The number of API requests remaining according to the most
+recent call. Set by `sx-request-make'.")
+
+(defcustom sx-request-remaining-api-requests-message-threshold
+ 50
+ "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.")
+
+(defcustom sx-request-default-keyword-arguments-alist
+ '(("filters/create")
+ ("sites")
+ ("questions" (site . emacs))
+ (t nil))
+ "Keywords to use as the default for a given method.
+
+The first element of each list is the method call the keywords
+apply to. The remaining cons cells (and they must be conses) are
+the values for each keyword.
+
+For each list, if no keywords are provided, the method's
+arguments are forced to the default as determined by the API.
+
+For each cons cell, if the cdr is `nil', then the keyword will be
+forced to the default as determined by the API.
+
+See `sx-request-get-default-keyword-arguments' and
+`sx-request-build-keyword-arguments'.
+")
+
+(defconst sx-request-api-version
+ "2.2"
+ "The current version of the API.")
+
+(defconst sx-request-api-root
+ (format "http://api.stackexchange.com/%s/" sx-request-api-version)
+ "The base URL to make requests from.")
+
+(defconst sx-request-api-key
+ "0TE6s1tveCpP9K5r5JNDNQ(("
+ "When passed, this key provides a higher request quota.")
+
+(defun sx-request-make
+ (method &optional keyword-arguments filter silent)
+ "Make a request to the StackExchange API using METHOD and
+optional KEYWORD-ARGUMENTS. If no KEYWORD-ARGUMENTS are given,
+`sx-default-keyword-arguments-alist' is used. Return the
+entire response as a complex alist."
+ (let ((url-automatic-caching sx-request-cache-p)
+ (url-inhibit-uncompression t)
+ (silent (or silent sx-request-silent-p))
+ (call
+ (sx-request--build
+ method
+ (append `((filter . ,(cond (filter filter)
+ ((boundp 'stack-filter) stack-filter)))
+ (key . ,sx-request-api-key))
+ (if keyword-arguments keyword-arguments
+ (sx-request--get-default-keyword-arguments method))))))
+ ;; TODO: url-retrieve-synchronously can return nil if the call is
+ ;; unsuccessful should handle this case
+ (unless silent (sx-message "Request: %S" call))
+ (let ((response-buffer (cond
+ ((= emacs-minor-version 4)
+ (url-retrieve-synchronously call silent))
+ (t (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")
+ (delete-region (point-min) (point))
+ (buffer-string))))
+ (response (ignore-errors
+ (json-read-from-string data))))
+ ;; if response isn't nil, the response was in plain text
+ (unless response
+ ;; try to decompress the response
+ (setq response
+ (with-demoted-errors "JSON Error: %s"
+ (shell-command-on-region
+ (point-min) (point-max)
+ sx-request-unzip-program
+ nil t)
+ (json-read-from-string
+ (buffer-substring
+ (point-min) (point-max)))))
+ ;; If it still fails, error out
+ (unless response
+ (sx-message "Unable to parse response")
+ (sx-message "Printing response as message")
+ (message "%S" response)
+ (error "Response could not be read by json-read-string")))
+ ;; At this point, either response is a valid data structure
+ ;; or we have already thrown an error
+ (when (assoc 'error_id response)
+ (error "Request failed: (%s) [%i %s] %s"
+ method
+ (cdr (assoc 'error_id response))
+ (cdr (assoc 'error_name response))
+ (cdr (assoc 'error_message response))))
+ (when (< (setq sx-request-remaining-api-requests
+ (cdr (assoc 'quota_remaining response)))
+ sx-request-remaining-api-requests-message-threshold)
+ (sx-message "%d API requests remaining"
+ sx-request-remaining-api-requests))
+ (cdr (assoc 'items response))))))))
+
+(defun sx-request--build (method keyword-arguments &optional kv-value-sep)
+ "Build the request string that will be used to process REQUEST
+with the given KEYWORD-ARGUMENTS."
+ (let ((base (concat sx-request-api-root method))
+ (args (sx-request--build-keyword-arguments
+ keyword-arguments kv-value-sep)))
+ (if (string-equal "" args)
+ base
+ (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
+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'."
+ (mapconcat
+ (lambda (pair)
+ (concat
+ (sx--thing-as-string (car pair))
+ "="
+ (sx--thing-as-string (cdr pair) kv-value-sep)))
+ (delq nil (mapcar
+ (lambda (pair)
+ (when (cdr pair) pair))
+ alist))
+ "&"))
+
+(defun sx-request--get-default-keyword-arguments (method)
+ "Gets the correct keyword arguments for METHOD."
+ (let ((entry (assoc method sx-request-default-keyword-arguments-alist)))
+ (cdr (or entry (assoc t sx-request-default-keyword-arguments-alist)))))
+
+;;; @todo sx-request-change-default-keyword-arguments
+;;; (method new-keyword-arguments)
+;;; @todo sx-request-change-default-keyword-arguments-for-key
+;;; (method key new-value)
+
+
+(provide 'sx-request)
+;;; sx-request.el ends here
diff --git a/sx-time.el b/sx-time.el
new file mode 100644
index 0000000..1cea76f
--- /dev/null
+++ b/sx-time.el
@@ -0,0 +1,54 @@
+;;; sx-time.el --- time for stack-mode
+
+;; Copyright (C) 2014 Sean Allred
+
+;; Author: Sean Allred <sallred@calamity.tcs.com>
+;; Keywords: help
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;
+
+;;; Code:
+
+(require 'time-date)
+
+(defvar sx-time-seconds-to-string
+ ;; (LIMIT NAME VALUE)
+ ;; We use an entry if the number of seconds in question is less than
+ ;; LIMIT, but more than the previous entry's LIMIT.
+ '((100 "s" 1)
+ (6000 "m" 60.0)
+ (108000 "h" 3600.0)
+ (34560000 "d" 86400.0)
+ (nil "y" 31557600.0))
+ "Auxiliary variable used by `sx-time-since'.")
+
+(defun sx-time-since (time)
+ "Convert the time interval since TIME (in seconds) to a short string."
+ (let ((delay (- (time-to-seconds) time)))
+ (concat
+ (if (> 0 delay) "-" "")
+ (if (= 0 delay) "0s"
+ (setq delay (abs delay))
+ (let ((sts sx-time-seconds-to-string) here)
+ (while (and (car (setq here (pop sts)))
+ (<= (car here) delay)))
+ (concat (format "%.0f" (/ delay (car (cddr here))))
+ (cadr here)))))))
+
+(provide 'sx-time)
+;;; sx-time.el ends here
diff --git a/sx.el b/sx.el
index 05a50fb..9eb19bb 100644
--- a/sx.el
+++ b/sx.el
@@ -1,4 +1,4 @@
-;;; stack-core.el --- core functions for stack-mode -*- lexical-binding: t; -*-
+;;; sx.el --- core functions -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -20,99 +20,17 @@
;;; Commentary:
;; This file defines basic commands used by all other parts of
-;; StackMode. Currently, there are sections that are pretty wildly
-;; different from each other (e.g. `Filters' and `Questions'. These
-;; will eventually be migrated to their own files with related functions
-;; of their ilk -- for now, it is more convenient to keep them handy.
+;; StackMode.
;;; Code:
;;; Requirements
-(require 'json)
-(require 'url)
-(require 'time-date)
-
-
-;;; Package Logging
-
-(defun stack-message (format-string &rest args)
+(defun sx-message (format-string &rest args)
"Display a message"
(message "[stack] %s" (apply #'format format-string args)))
-
-;;; Constants and Customizable Options
-
-(defcustom stack-cache-directory
- (expand-file-name ".stackmode" user-emacs-directory)
- "Directory containined cached files and precompiled filters.")
-
-(defconst stack-core-api-version
- "2.2"
- "The current version of the API.")
-
-(defconst stack-core-api-root
- (format "http://api.stackexchange.com/%s/" stack-core-api-version)
- "The base URL to make requests from.")
-
-(defvar stack-core-api-batch-request-separator
- ";"
- "The separator character to use when making batch requests.
-
-Do not change this unless you know what you are doing!")
-
-(defconst stack-core-api-key
- "0TE6s1tveCpP9K5r5JNDNQ(("
- "When passed, this key provides a higher request quota.")
-
-(defcustom stack-core-default-keyword-arguments-alist
- '(("filters/create")
- ("sites")
- ("questions" (site . emacs))
- (t nil))
- "Keywords to use as the default for a given method.
-
-The first element of each list is the method call the keywords
-apply to. The remaining cons cells (and they must be conses) are
-the values for each keyword.
-
-For each list, if no keywords are provided, the method's
-arguments are forced to the default as determined by the API.
-
-For each cons cell, if the cdr is `nil', then the keyword will be
-forced to the default as determined by the API.
-
-See `stack-core-get-default-keyword-arguments' and
-`stack-core-build-keyword-arguments'.
-")
-
-(defcustom stack-core-cache-requests
- t
- "Cache requests made to the StackExchange API.")
-
-(defcustom stack-core-unzip-program
- "gunzip"
- "program used to unzip the response")
-
-(defvar stack-core-remaining-api-requests
- nil
- "The number of API requests remaining according to the most
-recent call. Set by `stack-core-make-request'.")
-
-(defcustom stack-core-remaining-api-requests-message-threshold
- 50
- "After `stack-core-remaining-api-requests' drops below this
-number, `stack-core-make-request' will begin printing out the
-number of requests left every time it finishes a call.")
-
-(defcustom stack-core-silent-requests
- t
- "When `t', requests default to being silent.")
-
-
-;;; Keyword Arguments
-
-(defun stack-core-thing-as-string (thing)
+(defun sx--thing-as-string (thing &optional sequence-sep)
"Return a string representation of THING. If THING is already
a string, just return it."
(cond
@@ -120,248 +38,39 @@ a string, just return it."
((symbolp thing) (symbol-name thing))
((numberp thing) (number-to-string thing))
((sequencep thing)
- (mapconcat #'stack-core-thing-as-string
- thing stack-core-api-batch-request-separator))))
-
-(defun stack-core-get-default-keyword-arguments (method)
- "Gets the correct keyword arguments for METHOD."
- (let ((entry (assoc method stack-core-default-keyword-arguments-alist)))
- (cdr (or entry (assoc t stack-core-default-keyword-arguments-alist)))))
-
-;;; @todo stack-core-change-default-keyword-arguments
-;;; (method new-keyword-arguments)
-;;; @todo stack-core-change-default-keyword-arguments-for-key
-;;; (method key new-value)
-
-(defun stack-core-build-keyword-arguments (alist)
- "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
-`stack-core-thing-as-string'."
- (mapconcat
- (lambda (pair)
- (concat
- (stack-core-thing-as-string (car pair))
- "="
- (stack-core-thing-as-string (cdr pair))))
- (delq nil (mapcar
- (lambda (pair)
- (when (cdr pair) pair))
- alist))
- "&"))
+ (mapconcat #'sx--thing-as-string
+ thing (if sequence-sep sequence-sep ";")))))
-
-;;; Making Requests of StackExchange
-
-(defun stack-core-build-request (method keyword-arguments)
- "Build the request string that will be used to process REQUEST
-with the given KEYWORD-ARGUMENTS."
- (let ((base (concat stack-core-api-root method))
- (args (stack-core-build-keyword-arguments keyword-arguments)))
- (if (string-equal "" args)
- base
- (concat base "?" args))))
-
-(defun stack-core-make-request
- (method &optional keyword-arguments filter silent)
- "Make a request to the StackExchange API using METHOD and
-optional KEYWORD-ARGUMENTS. If no KEYWORD-ARGUMENTS are given,
-`stack-core-default-keyword-arguments-alist' is used. Return the
-entire response as a complex alist."
- (let ((url-automatic-caching stack-core-cache-requests)
- (url-inhibit-uncompression t)
- (silent (or silent stack-core-silent-requests))
- (call
- (stack-core-build-request
- method
- (append `((filter . ,(cond (filter filter)
- ((boundp 'stack-filter) stack-filter)))
- (key . ,stack-core-api-key))
- (if keyword-arguments keyword-arguments
- (stack-core-get-default-keyword-arguments method))))))
- ;; TODO: url-retrieve-synchronously can return nil if the call is
- ;; unsuccessful should handle this case
- (unless silent (stack-message "Request: %S" call))
- (let ((response-buffer (cond
- ((= emacs-minor-version 4)
- (url-retrieve-synchronously call silent))
- (t (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")
- (delete-region (point-min) (point))
- (buffer-string))))
- (response (ignore-errors
- (json-read-from-string data))))
- ;; if response isn't nil, the response was in plain text
- (unless response
- ;; try to decompress the response
- (setq response
- (with-demoted-errors "JSON Error: %s"
- (shell-command-on-region
- (point-min) (point-max)
- stack-core-unzip-program
- nil t)
- (json-read-from-string
- (buffer-substring
- (point-min) (point-max)))))
- ;; If it still fails, error out
- (unless response
- (stack-message "Unable to parse response")
- (stack-message "Printing response as message")
- (message "%S" response)
- (error "Response could not be read by json-read-string")))
- ;; At this point, either response is a valid data structure
- ;; or we have already thrown an error
- (when (assoc 'error_id response)
- (error "Request failed: (%s) [%i %s] %s"
- method
- (cdr (assoc 'error_id response))
- (cdr (assoc 'error_name response))
- (cdr (assoc 'error_message response))))
- (when (< (setq stack-core-remaining-api-requests
- (cdr (assoc 'quota_remaining response)))
- stack-core-remaining-api-requests-message-threshold)
- (stack-message "%d API requests remaining"
- stack-core-remaining-api-requests))
- (cdr (assoc 'items response))))))))
-
-(defun stack-core-filter-data (data desired-tree)
+(defun sx--filter-data (data desired-tree)
"Filters DATA and returns the DESIRED-TREE"
(if (vectorp data)
- (apply #'vector
- (mapcar (lambda (entry)
- (stack-core-filter-data
- entry desired-tree))
- data))
+ (apply #'vector
+ (mapcar (lambda (entry)
+ (sx--filter-data
+ entry desired-tree))
+ data))
(delq
nil
(mapcar (lambda (cons-cell)
- ;; 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.
- ;; See edfab4443ec3d376c31a38bef12d305838d3fa2e.
- (let ((f (or (memq (car cons-cell) desired-tree)
- (assoc (car cons-cell) desired-tree))))
- (when f
- (if (and (sequencep (cdr cons-cell))
- (sequencep (elt (cdr cons-cell) 0)))
- (cons (car cons-cell)
- (stack-core-filter-data
- (cdr cons-cell) (cdr f)))
- cons-cell))))
- data))))
-
-(defun stack-cache-get-file-name (filename)
- "Expands FILENAME in the context of `stack-cache-directory'."
- (expand-file-name filename stack-cache-directory))
-
-(defun stack-cache-get (cache)
- "Return the data within CACHE.
-
-As with `stack-cache-set', CACHE is a file name within the
-context of `stack-cache-directory'."
- (unless (file-exists-p stack-cache-directory)
- (mkdir stack-cache-directory))
- (let ((file (stack-cache-get-file-name cache)))
- (when (file-exists-p file)
- (with-temp-buffer
- (insert-file-contents (stack-cache-get-file-name cache))
- (read (buffer-string))))))
-
-(defun stack-cache-set (cache data)
- "Set the content of CACHE to DATA.
-
-As with `stack-cache-get', CACHE is a file name within the
-context of `stack-cache-directory'.
-
-DATA will be written as returned by `prin1'."
- (unless (file-exists-p stack-cache-directory)
- (mkdir stack-cache-directory))
- (write-region (prin1-to-string data) nil
- (stack-cache-get-file-name cache))
- data)
-
-(defvar stack-core--seconds-to-string
- ;; (LIMIT NAME VALUE)
- ;; We use an entry if the number of seconds in question is less than
- ;; LIMIT, but more than the previous entry's LIMIT.
- '((100 "s" 1)
- (6000 "m" 60.0)
- (108000 "h" 3600.0)
- (34560000 "d" 86400.0)
- (nil "y" 31557600.0))
- "Auxiliary variable used by `stack--time-since'.")
-
-(defun stack--time-since (time)
- "Convert the time interval since TIME (in seconds) to a short string."
- (let ((delay (- (time-to-seconds) time)))
- (concat
- (if (> 0 delay) "-" "")
- (if (= 0 delay) "0s"
- (setq delay (abs delay))
- (let ((sts stack-core--seconds-to-string) here)
- (while (and (car (setq here (pop sts)))
- (<= (car here) delay)))
- (concat (format "%.0f" (/ delay (car (cddr here))))
- (cadr here)))))))
-
-(defcustom stack-core-html-entities-plist
- '(Aacute "Á" aacute "á" Acirc "Â" acirc "â" acute "´" AElig "Æ" aelig "æ"
- Agrave "À" agrave "à" alefsym "ℵ" Alpha "Α" alpha "α" amp "&" and "∧"
- ang "∠" apos "'" aring "å" Aring "Å" asymp "≈" atilde "ã" Atilde "Ã"
- auml "ä" Auml "Ä" bdquo "„" Beta "Β" beta "β" brvbar "¦" bull "•"
- cap "∩" ccedil "ç" Ccedil "Ç" cedil "¸" cent "¢" Chi "Χ" chi "χ"
- circ "ˆ" clubs "♣" cong "≅" copy "©" crarr "↵" cup "∪" curren "¤"
- Dagger "‡" dagger "†" darr "↓" dArr "⇓" deg "°" Delta "Δ" delta "δ"
- diams "♦" divide "÷" eacute "é" Eacute "É" ecirc "ê" Ecirc "Ê" egrave "è"
- Egrave "È" empty "∅" emsp " " ensp " " Epsilon "Ε" epsilon "ε" equiv "≡"
- Eta "Η" eta "η" eth "ð" ETH "Ð" euml "ë" Euml "Ë" euro "€"
- exist "∃" fnof "ƒ" forall "∀" frac12 "½" frac14 "¼" frac34 "¾" frasl "⁄"
- Gamma "Γ" gamma "γ" ge "≥" gt ">" harr "↔" hArr "⇔" hearts "♥"
- hellip "…" iacute "í" Iacute "Í" icirc "î" Icirc "Î" iexcl "¡" igrave "ì"
- Igrave "Ì" image "ℑ" infin "∞" int "∫" Iota "Ι" iota "ι" iquest "¿"
- isin "∈" iuml "ï" Iuml "Ï" Kappa "Κ" kappa "κ" Lambda "Λ" lambda "λ"
- lang "〈" laquo "«" larr "←" lArr "⇐" lceil "⌈" ldquo "“" le "≤"
- lfloor "⌊" lowast "∗" loz "◊" lrm "" lsaquo "‹" lsquo "‘" lt "<"
- macr "¯" mdash "—" micro "µ" middot "·" minus "−" Mu "Μ" mu "μ"
- nabla "∇" nbsp "" ndash "–" ne "≠" ni "∋" not "¬" notin "∉"
- nsub "⊄" ntilde "ñ" Ntilde "Ñ" Nu "Ν" nu "ν" oacute "ó" Oacute "Ó"
- ocirc "ô" Ocirc "Ô" OElig "Œ" oelig "œ" ograve "ò" Ograve "Ò" oline "‾"
- omega "ω" Omega "Ω" Omicron "Ο" omicron "ο" oplus "⊕" or "∨" ordf "ª"
- ordm "º" oslash "ø" Oslash "Ø" otilde "õ" Otilde "Õ" otimes "⊗" ouml "ö"
- Ouml "Ö" para "¶" part "∂" permil "‰" perp "⊥" Phi "Φ" phi "φ"
- Pi "Π" pi "π" piv "ϖ" plusmn "±" pound "£" Prime "″" prime "′"
- prod "∏" prop "∝" Psi "Ψ" psi "ψ" quot "\"" radic "√" rang "〉"
- raquo "»" rarr "→" rArr "⇒" rceil "⌉" rdquo "”" real "ℜ" reg "®"
- rfloor "⌋" Rho "Ρ" rho "ρ" rlm "" rsaquo "›" rsquo "’" sbquo "‚"
- scaron "š" Scaron "Š" sdot "⋅" sect "§" shy "" Sigma "Σ" sigma "σ"
- sigmaf "ς" sim "∼" spades "♠" sub "⊂" sube "⊆" sum "∑" sup "⊃"
- sup1 "¹" sup2 "²" sup3 "³" supe "⊇" szlig "ß" Tau "Τ" tau "τ"
- there4 "∴" Theta "Θ" theta "θ" thetasym "ϑ" thinsp " " thorn "þ" THORN "Þ"
- tilde "˜" times "×" trade "™" uacute "ú" Uacute "Ú" uarr "↑" uArr "⇑"
- 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."
- :type '(repeat (choice symbol string))
- :group 'stack-core)
-
-(defun stack-core--decode-entities (string)
- (let* ((plist stack-core-html-entities-plist)
- (get-function (lambda (s) (let ((ss (substring s 1 -1)))
- ;; Handle things like &quot;
- (or (plist-get plist (intern ss))
- ;; Handle things like &#39;
- (format "%c" (string-to-int
- (substring ss 1))))))))
- (replace-regexp-in-string "&[^; ]*;" get-function string)))
-
-(provide 'stack-core)
-;;; stack-core.el ends here
+ ;; 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.
+ ;; See edfab4443ec3d376c31a38bef12d305838d3fa2e.
+ (let ((f (or (memq (car cons-cell) desired-tree)
+ (assoc (car cons-cell) desired-tree))))
+ (when f
+ (if (and (sequencep (cdr cons-cell))
+ (sequencep (elt (cdr cons-cell) 0)))
+ (cons (car cons-cell)
+ (sx-filter-data
+ (cdr cons-cell) (cdr f)))
+ cons-cell))))
+ data))))
+
+(provide 'sx)
+;;; sx.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End: