aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Allred <code@seanallred.com>2014-11-07 13:38:23 -0500
committerSean Allred <code@seanallred.com>2014-11-07 13:38:23 -0500
commit87fecb1707510885b8974c8fe2d061f4b1c927d7 (patch)
tree5659c9b2cb0cdda9393107779f4075c1233a18a4
parent0d59cd54c84b1245d0dd0ea25ff49d7abd5e60b7 (diff)
parent70e96f5aa97eca789a307fb29302fca20c13686f (diff)
Merge pull request #31 from vermiculus/rename
Rename files to sx prefix (and variables accordingly)
-rw-r--r--Cask2
-rw-r--r--stack-core.el369
-rw-r--r--stack-question-list.el299
-rw-r--r--sx-auth.el (renamed from stack-auth.el)57
-rw-r--r--sx-cache.el66
-rw-r--r--sx-encoding.el83
-rw-r--r--sx-filter.el (renamed from stack-filter.el)63
-rw-r--r--sx-lto.el (renamed from stack-lto.el)45
-rw-r--r--sx-network.el (renamed from stack-network.el)17
-rw-r--r--sx-question-list.el298
-rw-r--r--sx-question.el (renamed from stack-question.el)74
-rw-r--r--sx-request.el194
-rw-r--r--sx-time.el54
-rw-r--r--sx.el76
-rw-r--r--test/tests.el64
15 files changed, 936 insertions, 825 deletions
diff --git a/Cask b/Cask
index 4633bb6..3e939ef 100644
--- a/Cask
+++ b/Cask
@@ -3,7 +3,7 @@
(source gnu)
(source melpa-stable)
-(files "stack-*.el")
+(files "sx*.el")
(depends-on "json" "1.4")
(depends-on "url")
diff --git a/stack-core.el b/stack-core.el
deleted file mode 100644
index 496533e..0000000
--- a/stack-core.el
+++ /dev/null
@@ -1,369 +0,0 @@
-;;; stack-core.el --- core functions for stack-mode -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2014 Sean Allred
-
-;; Author: Sean Allred <code@seanallred.com>
-;; Keywords: help, hypermedia, mail, news, tools
-
-
-;; 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:
-
-;; 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.
-
-;;; Code:
-
-
-;;; Requirements
-(require 'json)
-(require 'url)
-(require 'time-date)
-
-
-;;; Package Logging
-
-(defun stack-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)
- "Return a string representation of THING. If THING is already
-a string, just return it."
- (cond
- ((stringp thing) thing)
- ((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))
- "&"))
-
-
-;;; 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)
- "Filters DATA and returns the DESIRED-TREE"
- (if (vectorp data)
- (apply #'vector
- (mapcar (lambda (entry)
- (stack-core-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
diff --git a/stack-question-list.el b/stack-question-list.el
deleted file mode 100644
index 924e90e..0000000
--- a/stack-question-list.el
+++ /dev/null
@@ -1,299 +0,0 @@
-;;; stack-question-list.el --- Major-mode for navigating questions list. -*- lexical-binding: t; -*-
-
-;; Copyright (C) 2014 Artur Malabarba
-
-;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
-;; Keywords: help, hypermedia, mail, news, tools
-
-
-;; 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 'stack-question)
-(require 'tabulated-list)
-(require 'cl-lib)
-
-
-;;; Customization
-(defcustom stack-question-list-height 12
- "Height, in lines, of stack-mode's *question-list* buffer."
- :type 'integer
- :group 'stack-question-list)
-
-(defface stack-question-list-parent
- '((t :inherit default))
- ""
- :group 'stack-question-list-faces)
-
-(defface stack-question-list-answers
- '((((background light)) :foreground "SeaGreen4"
- :height 1.0 :inherit stack-question-list-parent)
- (((background dark)) :foreground "#D1FA71"
- :height 1.0 :inherit stack-question-list-parent)
- (t :inherit stack-question-list-parent))
- ""
- :group 'stack-question-list-faces)
-
-(defface stack-question-list-answers-accepted
- '((((background light)) :background "YellowGreen"
- :inherit stack-question-list-answers)
- (((background dark)) :background "DarkOliveGreen"
- :inherit stack-question-list-answers)
- (t :inherit stack-question-list-answers))
- ""
- :group 'stack-question-list-faces)
-
-(defface stack-question-list-score
- '((t :height 1.0 :inherit stack-question-list-parent))
- ""
- :group 'stack-question-list-faces)
-
-(defface stack-question-list-score-upvoted
- '((t :weight bold
- :inherit stack-question-list-score))
- ""
- :group 'stack-question-list-faces)
-
-(defface stack-question-list-tags
- '((t :inherit font-lock-function-name-face))
- ""
- :group 'stack-question-list-faces)
-
-(defface stack-question-list-date
- '((t :inherit font-lock-comment-face))
- ""
- :group 'stack-question-list-faces)
-
-(defface stack-question-list-read-question
- '((t :height 1.0 :inherit stack-question-list-parent))
- ""
- :group 'stack-question-list-faces)
-
-(defface stack-question-list-unread-question
- '((t :weight bold :inherit stack-question-list-read-question))
- ""
- :group 'stack-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.
-Letters do not insert themselves; instead, they are commands.
-\\<stack-question-list>
-\\{stack-question-list}"
- (hl-line-mode 1)
- (stack-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)])
- (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)
- (add-hook 'tabulated-list-revert-hook
- #'stack-question-list--update-mode-line nil t)
- (tabulated-list-init-header))
-
-(defcustom stack-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)
-
-(defun stack-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
- (car x) (car y) #'>))
-
-(mapc
- (lambda (x) (define-key stack-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"
- ;; Other values (once we implement them) are "Top Voted",
- ;; "Unanswered", etc.
- "Variable describing current page being viewed.")
-
-(defvar stack-question-list--unread-count 0
- "Holds the number of unread questions in the current buffer.")
-(make-variable-buffer-local 'stack-question-list--unread-count)
-
-(defvar stack-question-list--total-count 0
- "Holds the total number of questions in the current buffer.")
-(make-variable-buffer-local 'stack-question-list--total-count)
-
-(defconst stack-question-list--mode-line-format
- '(" "
- mode-name
- " "
- (:propertize stack-question-list--current-page
- face mode-line-buffer-id)
- " ["
- "Unread: "
- (:propertize
- (:eval (int-to-string stack-question-list--unread-count))
- face mode-line-buffer-id)
- ", "
- "Total: "
- (:propertize
- (:eval (int-to-string stack-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 ()
- "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)
- (setq mode-line-format
- stack-question-list--mode-line-format)
- (setq stack-question-list--total-count
- (length tabulated-list-entries))))
-
-(defvar stack-question-list--current-site "emacs"
- "Site being displayed in the *question-list* buffer.")
-
-(defun stack-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
-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)))
- ;; Print the result.
- (setq tabulated-list-entries
- (mapcar #'stack-question-list--print-info question-list)))
- (when redisplay (tabulated-list-print 'remember)))
-
-(defcustom stack-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)
-
-(defun stack-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))
- (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))
- (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))
- (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 " [" (mapconcat #'identity (cdr (assoc 'tags data)) "] [") "]")
- 'face 'stack-question-list-tags)
- (propertize " " 'display "\n")))))
-
-(defun stack-question-list-view-previous (n)
- "Hide this question, move to previous one, display it."
- (interactive "p")
- (stack-question-list-view-next (- n)))
-
-(defun stack-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))
-
-(defun stack-question-list-next (n)
- "Move to the next entry."
- (interactive "p")
- (forward-line n))
-
-(defun stack-question-list-previous (n)
- "Move to the previous entry."
- (interactive "p")
- (stack-question-list-next (- n)))
-
-(defun stack-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."
- (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
- (condition-case er
- (split-window-below stack-question-list-height)
- (error
- ;; If the window is too small to split, use current one.
- (if (string-match
- "Window #<window .*> too small for splitting"
- (car (cdr-safe er)))
- nil
- (error (cdr er)))))))
- (stack-question--display data stack-question--window)
- (when focus
- (if stack-question--window
- (select-window stack-question--window)
- (switch-to-buffer stack-question--buffer))))
-
-(defvar stack-question-list--buffer nil
- "Buffer where the list of questions is displayed.")
-
-(defun list-questions (no-update)
- "Display a list of stack-exchange questions."
- (interactive "P")
- (unless (buffer-live-p stack-question-list--buffer)
- (setq stack-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))
-
-(defalias 'stack-list-questions #'list-questions)
-
-(provide 'stack-question-list)
-;;; stack-question-list.el ends here
diff --git a/stack-auth.el b/sx-auth.el
index e55fae1..59be452 100644
--- a/stack-auth.el
+++ b/sx-auth.el
@@ -1,9 +1,8 @@
-;;; stack-auth.el --- user authentication for stack-mode -*- lexical-binding: t; -*-
+;;; sx-auth.el --- user authentication -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
;; Author: Sean Allred <code@seanallred.com>
-;; Keywords: help, hypermedia, mail, news, tools
;; 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
@@ -20,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.
@@ -40,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
@@ -48,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/stack-filter.el b/sx-filter.el
index 4210549..7178259 100644
--- a/stack-filter.el
+++ b/sx-filter.el
@@ -1,9 +1,8 @@
-;;; stack-filter.el --- filters for stack-mode -*- lexical-binding: t; -*-
+;;; sx-filter.el --- filters -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
;; Author: Sean Allred <code@seanallred.com>
-;; Keywords: help, hypermedia, mail, news, tools
;; 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
@@ -20,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
@@ -72,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,
@@ -89,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/stack-lto.el b/sx-lto.el
index af1be9f..6bdd5d0 100644
--- a/stack-lto.el
+++ b/sx-lto.el
@@ -1,10 +1,8 @@
-;;; 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
;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
-;; Keywords: help, hypermedia, mail, news, tools
-
;; 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
@@ -25,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."
@@ -73,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
@@ -87,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/stack-network.el b/sx-network.el
index e7e333c..dcd2349 100644
--- a/stack-network.el
+++ b/sx-network.el
@@ -1,9 +1,8 @@
-;;; stack-network.el --- network functions for stack-mode -*- lexical-binding: t; -*-
+;;; sx-network.el --- browsing networks -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
;; Author: Sean Allred <code@seanallred.com>
-;; Keywords: help, hypermedia, mail, news, tools
;; 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
@@ -20,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
new file mode 100644
index 0000000..59acbeb
--- /dev/null
+++ b/sx-question-list.el
@@ -0,0 +1,298 @@
+;;; sx-question-list.el --- Major-mode for navigating questions list. -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2014 Artur Malabarba
+
+;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+(require 'sx-question)
+(require 'sx-time)
+(require 'tabulated-list)
+(require 'cl-lib)
+
+
+;;; Customization
+(defcustom sx-question-list-height 12
+ "Height, in lines, of stack-mode's *question-list* buffer."
+ :type 'integer
+ :group 'sx-question-list)
+
+(defface sx-question-list-parent
+ '((t :inherit default))
+ ""
+ :group 'sx-question-list-faces)
+
+(defface sx-question-list-answers
+ '((((background light)) :foreground "SeaGreen4"
+ :height 1.0 :inherit sx-question-list-parent)
+ (((background dark)) :foreground "#D1FA71"
+ :height 1.0 :inherit sx-question-list-parent)
+ (t :inherit sx-question-list-parent))
+ ""
+ :group 'sx-question-list-faces)
+
+(defface sx-question-list-answers-accepted
+ '((((background light)) :background "YellowGreen"
+ :inherit sx-question-list-answers)
+ (((background dark)) :background "DarkOliveGreen"
+ :inherit sx-question-list-answers)
+ (t :inherit sx-question-list-answers))
+ ""
+ :group 'sx-question-list-faces)
+
+(defface sx-question-list-score
+ '((t :height 1.0 :inherit sx-question-list-parent))
+ ""
+ :group 'sx-question-list-faces)
+
+(defface sx-question-list-score-upvoted
+ '((t :weight bold
+ :inherit sx-question-list-score))
+ ""
+ :group 'sx-question-list-faces)
+
+(defface sx-question-list-tags
+ '((t :inherit font-lock-function-name-face))
+ ""
+ :group 'sx-question-list-faces)
+
+(defface sx-question-list-date
+ '((t :inherit font-lock-comment-face))
+ ""
+ :group 'sx-question-list-faces)
+
+(defface sx-question-list-read-question
+ '((t :height 1.0 :inherit sx-question-list-parent))
+ ""
+ :group 'sx-question-list-faces)
+
+(defface sx-question-list-unread-question
+ '((t :weight bold :inherit sx-question-list-read-question))
+ ""
+ :group 'sx-question-list-faces)
+
+
+;;; Mode Definition
+(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.
+\\<sx-question-list>
+\\{sx-question-list}"
+ (hl-line-mode 1)
+ (sx-question-list--update-mode-line)
+ (setq tabulated-list-format
+ [(" V" 3 t :right-align t)
+ (" A" 3 t :right-align t)
+ ("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
+ #'sx-question-list-refresh nil t)
+ (add-hook 'tabulated-list-revert-hook
+ #'sx-question-list--update-mode-line nil t)
+ (tabulated-list-init-header))
+
+(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 'sx-question-list)
+
+(defun sx-question-list--date-more-recent-p (x y)
+ "Non-nil if tabulated-entry X is newer than Y."
+ (sx-question--<
+ sx-question-list-date-sort-method
+ (car x) (car y) #'>))
+
+(mapc
+ (lambda (x) (define-key sx-question-list-mode-map
+ (car x) (cadr x)))
+ '(("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 sx-question-list--unread-count 0
+ "Holds the number of unread questions in the current buffer.")
+(make-variable-buffer-local 'sx-question-list--unread-count)
+
+(defvar sx-question-list--total-count 0
+ "Holds the total number of questions in the current buffer.")
+(make-variable-buffer-local 'sx-question-list--total-count)
+
+(defconst sx-question-list--mode-line-format
+ '(" "
+ mode-name
+ " "
+ (:propertize sx-question-list--current-page
+ face mode-line-buffer-id)
+ " ["
+ "Unread: "
+ (:propertize
+ (:eval (int-to-string sx-question-list--unread-count))
+ face mode-line-buffer-id)
+ ", "
+ "Total: "
+ (:propertize
+ (:eval (int-to-string sx-question-list--total-count))
+ face mode-line-buffer-id)
+ "] ")
+ "Mode-line construct to use in question-list buffers.")
+
+(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 'sx-question-list-mode)
+ (setq mode-line-format
+ sx-question-list--mode-line-format)
+ (setq sx-question-list--total-count
+ (length tabulated-list-entries))))
+
+(defvar sx-question-list--current-site "emacs"
+ "Site being displayed in the *question-list* buffer.")
+
+(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 StackExchange for
+a new list before redisplaying."
+ (interactive "pP")
+ ;; Reset the mode-line unread count (we rebuild it here).
+ (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 #'sx-question-list--print-info question-list)))
+ (when redisplay (tabulated-list-print 'remember)))
+
+(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."
+ (list
+ data
+ (vector
+ (list (int-to-string (cdr (assoc 'score data)))
+ 'face
+ (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 (sx-question--accepted-answer data)
+ 'sx-question-list-answers-accepted
+ 'sx-question-list-answers))
+ (concat
+ (propertize
+ (cdr (assoc 'title data))
+ 'face
+ (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 (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 'sx-question-list-tags)
+ (propertize " " 'display "\n")))))
+
+(defun sx-question-list-view-previous (n)
+ "Hide this question, move to previous one, display it."
+ (interactive "p")
+ (sx-question-list-view-next (- n)))
+
+(defun sx-question-list-view-next (n)
+ "Hide this question, move to next one, display it."
+ (interactive "p")
+ (sx-question-list-next n)
+ (sx-question-list-display-question))
+
+(defun sx-question-list-next (n)
+ "Move to the next entry."
+ (interactive "p")
+ (forward-line n))
+
+(defun sx-question-list-previous (n)
+ "Move to the previous entry."
+ (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."
+ (interactive '(nil t))
+ (unless data (setq data (tabulated-list-get-id)))
+ (unless data (error "No question here!"))
+ (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 sx-question-list-height)
+ (error
+ ;; If the window is too small to split, use current one.
+ (if (string-match
+ "Window #<window .*> too small for splitting"
+ (car (cdr-safe er)))
+ nil
+ (error (cdr er)))))))
+ (sx-question--display data sx-question--window)
+ (when focus
+ (if sx-question--window
+ (select-window sx-question--window)
+ (switch-to-buffer sx-question--buffer))))
+
+(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."
+ (interactive "P")
+ (unless (buffer-live-p sx-question-list--buffer)
+ (setq sx-question-list--buffer
+ (generate-new-buffer "*question-list*")))
+ (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 'sx-list-questions #'list-questions)
+
+(provide 'sx-question-list)
+;;; sx-question-list.el ends here
diff --git a/stack-question.el b/sx-question.el
index 9fd5fcc..e9634f7 100644
--- a/stack-question.el
+++ b/sx-question.el
@@ -1,9 +1,8 @@
-;;; 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
;; Author: Sean Allred <code@seanallred.com>
-;; Keywords: help, hypermedia, mail, news, tools
;; 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
@@ -20,49 +19,50 @@
;;; Commentary:
-;;
+;;
;;; Code:
-(require 'stack-core)
-(require 'stack-filter)
-(require 'stack-lto)
+(require 'sx)
+(require 'sx-filter)
+(require 'sx-lto)
+(require 'sx-request)
;; 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 #'<)
@@ -70,52 +70,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
new file mode 100644
index 0000000..cd8af95
--- /dev/null
+++ b/sx.el
@@ -0,0 +1,76 @@
+;;; sx.el --- core functions -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2014 Sean Allred
+
+;; Author: Sean Allred <code@seanallred.com>
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file defines basic commands used by all other parts of
+;; StackMode.
+
+;;; Code:
+
+
+;;; Requirements
+(defun sx-message (format-string &rest args)
+ "Display a message"
+ (message "[stack] %s" (apply #'format format-string args)))
+
+(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
+ ((stringp thing) thing)
+ ((symbolp thing) (symbol-name thing))
+ ((numberp thing) (number-to-string thing))
+ ((sequencep thing)
+ (mapconcat #'sx--thing-as-string
+ thing (if sequence-sep sequence-sep ";")))))
+
+(defun sx--filter-data (data desired-tree)
+ "Filters DATA and returns the DESIRED-TREE"
+ (if (vectorp 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)
+ (sx--filter-data
+ (cdr cons-cell) (cdr f)))
+ cons-cell))))
+ data))))
+
+(provide 'sx)
+;;; sx.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/test/tests.el b/test/tests.el
index 74ef158..2864428 100644
--- a/test/tests.el
+++ b/test/tests.el
@@ -22,9 +22,9 @@
(read (buffer-string))))))
(setq
- stack-core-remaining-api-requests-message-threshold 50000
+ sx-request-remaining-api-requests-message-threshold 50000
debug-on-error t
- stack-core-silent-requests nil
+ sx-request-silent-p nil
user-emacs-directory "."
stack-test-data-questions
@@ -37,22 +37,22 @@
stack-test-data-dir))
(package-initialize)
(require 'cl-lib)
-(require 'stack-core)
-(require 'stack-question)
-(require 'stack-question-list)
+(require 'sx)
+(require 'sx-question)
+(require 'sx-question-list)
(ert-deftest test-basic-request ()
"Test basic request functionality"
- (should (stack-core-make-request "sites")))
+ (should (sx-request-make "sites")))
(ert-deftest test-question-retrieve ()
"Test the ability to receive a list of questions."
- (should (stack-question-get-questions 'emacs)))
+ (should (sx-question-get-questions 'emacs)))
(ert-deftest test-bad-request ()
"Test a method given a bad set of keywords"
(should-error
- (stack-core-make-request "questions" '(()))))
+ (sx-request-make "questions" '(()))))
(ert-deftest test-tree-filter ()
"`stack-core-filter-data'"
@@ -60,9 +60,9 @@
(should
(equal
'((1 . t) (2 . [1 2]) (3))
- (stack-core-filter-data '((0 . 3) (1 . t) (a . five) (2 . [1 2])
- ("5" . bop) (3) (p . 4))
- '(1 2 3))))
+ (sx--filter-data '((0 . 3) (1 . t) (a . five) (2 . [1 2])
+ ("5" . bop) (3) (p . 4))
+ '(1 2 3))))
;; complex
(should
(equal
@@ -70,12 +70,12 @@
(2 . [((a . 1) (c . 3))
((a . 4) (c . 6))])
(3 . peach))
- (stack-core-filter-data '((1 . [a b c])
- (2 . [((a . 1) (b . 2) (c . 3))
- ((a . 4) (b . 5) (c . 6))])
- (3 . peach)
- (4 . banana))
- '(1 (2 a c) 3))))
+ (sx--filter-data '((1 . [a b c])
+ (2 . [((a . 1) (b . 2) (c . 3))
+ ((a . 4) (b . 5) (c . 6))])
+ (3 . peach)
+ (4 . banana))
+ '(1 (2 a c) 3))))
;; vector
(should
@@ -83,29 +83,29 @@
[((1 . 2) (2 . 3) (3 . 4))
((1 . a) (2 . b) (3 . c))
nil ((1 . alpha) (2 . beta))]
- (stack-core-filter-data [((1 . 2) (2 . 3) (3 . 4))
- ((1 . a) (2 . b) (3 . c) (5 . seven))
- ((should-not-go))
- ((1 . alpha) (2 . beta))]
- '(1 2 3)))))
+ (sx--filter-data [((1 . 2) (2 . 3) (3 . 4))
+ ((1 . a) (2 . b) (3 . c) (5 . seven))
+ ((should-not-go))
+ ((1 . alpha) (2 . beta))]
+ '(1 2 3)))))
(ert-deftest test-filters ()
(let ((stack-cache-directory (make-temp-file "stack-test" t)))
- (should-error (stack-filter-store "names must be symbols"
- "this is a filter"))
+ (should-error (sx-filter-store "names must be symbols"
+ "this is a filter"))
;; basic use
(should (equal '((test . "filter"))
- (stack-filter-store 'test "filter")))
+ (sx-filter-store 'test "filter")))
;; aggregation
(should (equal '((test2 . "filter2") (test . "filter"))
- (stack-filter-store 'test2 "filter2")))
+ (sx-filter-store 'test2 "filter2")))
;; mutation
(should (equal '((test2 . "filter2") (test . "filter-test"))
- (stack-filter-store 'test "filter-test")))
+ (sx-filter-store 'test "filter-test")))
;; clean up (note: the file should exist)
(delete-file
- (stack-cache-get-file-name
- stack-filter-cache-file))))
+ (sx-cache-get-file-name
+ sx-filter-cache-file))))
(defmacro line-should-match (regexp)
""
@@ -116,7 +116,7 @@
(should (string-match ,regexp line))))
(ert-deftest question-list-display ()
- (cl-letf (((symbol-function #'stack-core-make-request)
+ (cl-letf (((symbol-function #'sx-request-make)
(lambda (&rest _) stack-test-data-questions)))
(list-questions nil)
(switch-to-buffer "*question-list*")
@@ -124,13 +124,13 @@
(should (equal (buffer-name) "*question-list*"))
(line-should-match
"^\\s-+1\\s-+0\\s-+Focus-hook: attenuate colours when losing focus [ 0-9]+[ydhms] ago\\s-+\\[frames\\] \\[hooks\\] \\[focus\\]")
- (stack-question-list-next 5)
+ (sx-question-list-next 5)
(line-should-match
"^\\s-+0\\s-+1\\s-+Babel doesn&#39;t wrap results in verbatim [ 0-9]+[ydhms] ago\\s-+\\[org-mode\\]")
;; ;; Use this when we have a real stack-question buffer.
;; (call-interactively 'stack-question-list-display-question)
;; (should (equal (buffer-name) "*stack-question*"))
(switch-to-buffer "*question-list*")
- (stack-question-list-previous 4)
+ (sx-question-list-previous 4)
(line-should-match
"^\\s-+2\\s-+1\\s-+&quot;Making tag completion table&quot; Freezes/Blocks -- how to disable [ 0-9]+[ydhms] ago\\s-+\\[autocomplete\\]")))