aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.agignore5
-rw-r--r--.gitignore12
-rw-r--r--.travis.yml4
-rw-r--r--Makefile10
-rw-r--r--bot/sx-bot.el82
-rwxr-xr-xbot/sx-bot.sh36
-rw-r--r--sx-auth.el91
-rw-r--r--sx-babel.el5
-rw-r--r--sx-button.el6
-rw-r--r--sx-cache.el16
-rw-r--r--sx-compose.el39
-rw-r--r--sx-encoding.el16
-rw-r--r--sx-favorites.el17
-rw-r--r--sx-filter.el72
-rw-r--r--sx-inbox.el8
-rw-r--r--sx-interaction.el56
-rw-r--r--sx-load.el3
-rw-r--r--sx-method.el58
-rw-r--r--sx-networks.el47
-rw-r--r--sx-notify.el4
-rw-r--r--sx-question-list.el53
-rw-r--r--sx-question-mode.el16
-rw-r--r--sx-question-print.el6
-rw-r--r--sx-question.el8
-rw-r--r--sx-request.el66
-rw-r--r--sx-search.el6
-rw-r--r--sx-site.el32
-rw-r--r--sx-switchto.el3
-rw-r--r--sx-tab.el41
-rw-r--r--sx-tag.el90
-rw-r--r--sx-time.el11
-rw-r--r--sx.el64
-rw-r--r--test/test-api.el3
-rw-r--r--test/test-macros.el22
-rw-r--r--test/test-search.el4
-rw-r--r--test/test-state.el22
-rw-r--r--test/test-util.el14
-rw-r--r--test/tests.el1
38 files changed, 825 insertions, 224 deletions
diff --git a/.agignore b/.agignore
index e00db68..3f11419 100644
--- a/.agignore
+++ b/.agignore
@@ -1,3 +1,5 @@
+# -*- gitignore -*-
+
# Backup files
*~
\#*\#
@@ -18,3 +20,6 @@ test/data-samples
# Info files
*.info
+
+# Data directory
+data/
diff --git a/.gitignore b/.gitignore
index cfaa152..59d35bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,18 @@
-# Emacs backup files
+# Personal Development
+.dir-locals.el
+
+# Backup Files
*~
\#*\#
# Compiled Elisp
*.elc
+
+# Package Artifacts
/.cask/
-.dir-locals.el
-/.stackmode/
/url/
+/.sx/
+
+# Generated Files
/sx.info
/sx.texi
diff --git a/.travis.yml b/.travis.yml
index ae882b2..d00ab46 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,10 @@
# Stolen from capitaomorte/yasnippet
language: emacs-lisp
+branches:
+ except:
+ - data
+
env:
- EVM_EMACS=emacs-24.1-bin
- EVM_EMACS=emacs-24.2-bin
diff --git a/Makefile b/Makefile
index 7b0b698..e04c1b0 100644
--- a/Makefile
+++ b/Makefile
@@ -20,19 +20,19 @@ VERSIONS = 1 2 3 4
all :: $(VERSIONS)
-$(VERSIONS) ::
+$(VERSIONS) :: clean
evm install emacs-24.$@-bin --skip || true
evm use emacs-24.$@-bin
emacs --version
cask install
emacs --batch -L . -l ert -l test/tests.el -f ert-run-tests-batch-and-exit
+clean:
+ rm -rf .sx/
+ cask clean-elc
+
install_cask:
curl -fsSkL https://raw.github.com/cask/cask/master/go | python
install_evm:
curl -fsSkL https://raw.github.com/rejeep/evm/master/go | bash
-
-# Local Variables:
-# indent-tabs-mode: t
-# End:
diff --git a/bot/sx-bot.el b/bot/sx-bot.el
new file mode 100644
index 0000000..b32a69c
--- /dev/null
+++ b/bot/sx-bot.el
@@ -0,0 +1,82 @@
+;;; sx-bot.el --- Functions for automated maintanence -*- 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:
+
+;; This file defines the behavior of a bot. To allow completion for
+;; tags, this bot runs through all sites in the network and retrieves
+;; all of their tags. This data is then written to a directory which
+;; is tracked by the git repository.
+
+
+;;; Code:
+
+(require 'package)
+(package-initialize)
+
+(require 'sx-load)
+
+(setq sx-request-remaining-api-requests-message-threshold 50000)
+
+(defcustom sx-bot-out-dir "./data/tags/"
+ "Directory where output tag files are saved."
+ :type 'directory
+ :group 'sx)
+
+
+;;; Printing
+(defun sx-bot-write-to-file (data)
+ "Write (cdr DATA) to file named (car DATA).
+File is savedd in `sx-bot-out-dir'."
+ (let ((file-name (expand-file-name (car data) sx-bot-out-dir)))
+ (with-temp-file file-name
+ (let* (print-length
+ (repr (prin1-to-string
+ (sort (cdr data)
+ #'string-lessp))))
+ (insert repr "\n")
+ (goto-char (point-min))
+ (while (search-forward "\" \"" nil t)
+ (replace-match "\"\n \"" nil t))))
+ (message "Wrote %S" file-name)
+ file-name))
+
+(defun sx-bot-fetch-and-write-tags ()
+ "Get a list of all tags of all sites and save to disk."
+ (make-directory sx-bot-out-dir t)
+ (let* ((url-show-status nil)
+ (site-tokens (sx-site-get-api-tokens))
+ (number-of-sites (length site-tokens))
+ (current-site-number 0)
+ (sx-request-all-items-delay 0.25))
+ (mapcar
+ (lambda (site)
+ (message "[%d/%d] Working on %S"
+ (cl-incf current-site-number)
+ number-of-sites
+ site)
+ (sx-bot-write-to-file
+ (cons (concat site ".el")
+ (sx-tag--get-all site))))
+ site-tokens)))
+
+
+;;; Newest
+(provide 'sx-bot)
+;;; sx-bot.el ends here
diff --git a/bot/sx-bot.sh b/bot/sx-bot.sh
new file mode 100755
index 0000000..6a5df17
--- /dev/null
+++ b/bot/sx-bot.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/bash
+
+DESTINATION_BRANCH=gh-pages
+
+function notify-done {
+ local title
+ local message
+ title="SX Tag Bot"
+ message="Finished retrieving tag lists"
+ case $(uname | tr '[[:upper:]]' '[[:lower:]]') in
+ darwin)
+ terminal-notifier \
+ -message ${message} \
+ -title ${title} \
+ -sound default
+ ;;
+ *)
+ echo ${message}
+ esac
+}
+
+function generate-tags {
+ emacs -Q --batch \
+ -L "./" -L "./bot/" -l sx-bot \
+ -f sx-bot-fetch-and-write-tags
+ ret = $?
+ notify-done
+ return ${ret}
+}
+
+git branch ${DESTINATION_BRANCH} &&
+ git pull &&
+ generate-tags &&
+ git stage data/ &&
+ git commit -m "Update tag data" &&
+ echo 'Ready for "git push"'
diff --git a/sx-auth.el b/sx-auth.el
index fca5392..cba310d 100644
--- a/sx-auth.el
+++ b/sx-auth.el
@@ -1,4 +1,4 @@
-;;; sx-auth.el --- user authentication -*- lexical-binding: t; -*-
+;;; sx-auth.el --- user authentication -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,6 +19,13 @@
;;; Commentary:
+;; This file handles logic related to authentication. This includes
+;; determining if a certain filter requires authentication (via the
+;; variable `sx-auth-filter-auth' and function `sx-auth--filter-p'),
+;; determining if a method requires authentication (via the variable
+;; `sx-auth-method-auth' and function `sx-auth--method-p'), and
+;; actually authenticating the user (with `sx-auth-authenticate').
+
;;; Code:
(require 'sx)
@@ -36,49 +43,53 @@
"Your access token.
This is needed to use your account to write questions, make
comments, and read your inbox. Do not alter this unless you know
-what you are doing!")
-
-(defvar sx-auth-method-auth '((me . t)
- (inbox . t)
- (notifications . t)
- (events . t)
- (posts (comments add))
- (comments delete
- edit
- flags
- upvote)
- (answers accept
- delete
- downvote
- edit
- flags
- upvote)
- (questions answers
- add
- close
- delete
- downvote
- edit
- favorite
- flags
- render
- upvote
- (unanswered my-tags)))
+what you are doing!
+
+This variable is set with `sx-auth-authenticate'.")
+
+(defconst sx-auth-method-auth
+ '((me . t)
+ (inbox . t)
+ (notifications . t)
+ (events . t)
+ (posts (comments add))
+ (comments delete
+ edit
+ flags
+ upvote)
+ (answers accept
+ delete
+ downvote
+ edit
+ flags
+ upvote)
+ (questions answers
+ add
+ close
+ delete
+ downvote
+ edit
+ favorite
+ flags
+ render
+ upvote
+ (unanswered my-tags)))
"List of methods that require auth.
-Methods are of form (METHOD SUBMETHODS) where SUBMETHODS
- is (METHOD METHOD METHOD ...).
+Methods are of the form \(METHOD . SUBMETHODS) where SUBMETHODS
+ is \(METHOD METHOD METHOD ...).
If all SUBMETHODS require auth or there are no submethods, form
-will be (METHOD . t)")
-
-(defvar sx-auth-filter-auth '(question.upvoted
- question.downvoted
- answer.upvoted
- answer.downvoted
- comment.upvoted)
+will be \(METHOD . t)")
+
+(defconst sx-auth-filter-auth
+ '(question.upvoted
+ question.downvoted
+ answer.upvoted
+ answer.downvoted
+ comment.upvoted)
"List of filter types that require auth.
-Keywords are of form (OBJECT TYPES) where TYPES is (FILTER FILTER
-FILTER).")
+Keywords are of the form \(OBJECT TYPES) where TYPES is \(FILTER
+FILTER FILTER).")
;;;###autoload
(defun sx-authenticate ()
diff --git a/sx-babel.el b/sx-babel.el
index b30a044..4386172 100644
--- a/sx-babel.el
+++ b/sx-babel.el
@@ -1,4 +1,4 @@
-;;; sx-babel.el --- Font-locking pre blocks according to language. -*- lexical-binding: t; -*-
+;;; sx-babel.el --- font-locking pre blocks according to language -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -122,3 +122,6 @@ Returns the amount of indentation removed."
(provide 'sx-babel)
;;; sx-babel.el ends here
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-button.el b/sx-button.el
index f166164..4c0666b 100644
--- a/sx-button.el
+++ b/sx-button.el
@@ -1,4 +1,4 @@
-;;; sx-button.el --- Defining buttons used throughout SX. -*- lexical-binding: t; -*-
+;;; sx-button.el --- defining buttons -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -163,3 +163,7 @@ usually part of a code-block."
(provide 'sx-button)
;;; sx-button.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-cache.el b/sx-cache.el
index 51c2267..3a5bd3b 100644
--- a/sx-cache.el
+++ b/sx-cache.el
@@ -1,4 +1,4 @@
-;;; sx-cache.el --- caching
+;;; sx-cache.el --- caching -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,12 +19,18 @@
;;; Commentary:
-;; All caches are retrieved and set using symbols. The symbol should
-;; be the sub-subpackage that is using the cache. For example,
-;; `sx-pkg' would use `(sx-cache-get 'pkg)'.
+;; This file handles the cache system. All caches are retrieved and
+;; set using symbols. The symbol should be the sub-package that is
+;; using the cache. For example, `sx-pkg' would use
+;;
+;; `(sx-cache-get 'pkg)'
;;
;; This symbol is then converted into a filename within
-;; `sx-cache-directory'.
+;; `sx-cache-directory' using `sx-cache-get-file-name'.
+;;
+;; Currently, the cache is written at every `sx-cache-set', but this
+;; write will eventually be done by some write-all function which will
+;; be set on an idle timer.
;;; Code:
diff --git a/sx-compose.el b/sx-compose.el
index ab4a58d..f4fcd0a 100644
--- a/sx-compose.el
+++ b/sx-compose.el
@@ -1,4 +1,4 @@
-;;; sx-compose.el --- Major-mode for coposing questions and answers. -*- lexical-binding: t; -*-
+;;; sx-compose.el --- major-mode for composing questions and answers -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -82,6 +82,10 @@ Is invoked between `sx-compose-before-send-hook' and
"Headers inserted when composing a new question.
Used by `sx-compose-create'.")
+(defvar sx-compose--site nil
+ "Site which the curent compose buffer belongs to.")
+(make-variable-buffer-local 'sx-compose--site)
+
;;; Major-mode
(define-derived-mode sx-compose-mode markdown-mode "Compose"
@@ -116,6 +120,8 @@ contents to the API, then calls `sx-compose-after-send-functions'."
(run-hook-with-args 'sx-compose-after-send-functions
(current-buffer) result)))))
+
+;;; Functions for use in hooks
(defun sx-compose-quit (buffer _)
"Close BUFFER's window and kill it."
(interactive (list (current-buffer) nil))
@@ -131,6 +137,26 @@ contents to the API, then calls `sx-compose-after-send-functions'."
(with-current-buffer buffer
(kill-new (buffer-string)))))
+(defun sx-compose--check-tags ()
+ "Check if tags in current compose buffer are valid."
+ (save-excursion
+ (goto-char (point-min))
+ (unless (search-forward-regexp
+ "^Tags : *\\([^[:space:]].*\\) *$"
+ (next-single-property-change (point-min) 'sx-compose-separator)
+ 'noerror)
+ (error "No Tags header found"))
+ (let ((invalid-tags
+ (sx-tag--invalid-name-p
+ (split-string (match-string 1) "[[:space:],;]"
+ 'omit-nulls "[[:space:]]")
+ sx-compose--site)))
+ (if invalid-tags
+ ;; If the user doesn't want to create the tags, we return
+ ;; nil and sending is aborted.
+ (y-or-n-p "Following tags don't exist. Create them? %s " invalid-tags)
+ t))))
+
;;; Functions to help preparing buffers
(defun sx-compose-create (site parent &optional before-functions after-functions)
@@ -153,6 +179,7 @@ respectively added locally to `sx-compose-before-send-hook' and
(cdr (assoc 'title parent))))))
(with-current-buffer (sx-compose--get-buffer-create site parent)
(sx-compose-mode)
+ (setq sx-compose--site site)
(setq sx-compose--send-function
(if (consp parent)
(sx-assoc-let parent
@@ -161,7 +188,7 @@ respectively added locally to `sx-compose-before-send-hook' and
(.comment_id 'comments)
(t 'answers))
:auth 'warn
- :url-method "POST"
+ :url-method 'post
:filter sx-browse-filter
:site site
:keywords (sx-compose--generate-keywords is-question)
@@ -169,7 +196,7 @@ respectively added locally to `sx-compose-before-send-hook' and
:submethod 'edit)))
(lambda () (sx-method-call 'questions
:auth 'warn
- :url-method "POST"
+ :url-method 'post
:filter sx-browse-filter
:site site
:keywords (sx-compose--generate-keywords is-question)
@@ -180,6 +207,8 @@ respectively added locally to `sx-compose-before-send-hook' and
(add-hook 'sx-compose-before-send-hook it nil t))
(dolist (it (reverse after-functions))
(add-hook 'sx-compose-after-send-functions it nil t))
+ (when is-question
+ (add-hook 'sx-compose-before-send-hook #'sx-compose--check-tags nil t))
;; If the buffer is empty, the draft didn't exist. So prepare the
;; question.
(when (or (string= (buffer-string) "")
@@ -273,3 +302,7 @@ the id property."
(provide 'sx-compose)
;;; sx-compose.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-encoding.el b/sx-encoding.el
index 0e66677..d8ad2ba 100644
--- a/sx-encoding.el
+++ b/sx-encoding.el
@@ -1,4 +1,4 @@
-;;; sx-encoding.el --- encoding
+;;; sx-encoding.el --- encoding -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,10 +19,18 @@
;;; Commentary:
+;; This file handles decoding the responses we get from the API. They
+;; are received either as plain-text or as a `gzip' compressed archive.
+;; For this, `sx-encoding-gzipped-p' is used to determine if content
+;; has been compressed under `gzip'.
+
;;; Code:
(require 'cl-lib)
+
+;;;; HTML Encoding
+
(defcustom sx-encoding-html-entities-plist
'(Aacute "Á" aacute "á" Acirc "Â" acirc "â" acute "´" AElig "Æ" aelig "æ"
Agrave "À" agrave "à" alefsym "ℵ" Alpha "Α" alpha "α" amp "&" and "∧"
@@ -86,6 +94,9 @@ Return the decoded string."
(substring ss 1))))))))
(replace-regexp-in-string "&[^; ]*;" get-function string)))
+
+;;;; Convenience Functions
+
(defun sx-encoding-normalize-line-endings (string)
"Normalize the line endings for STRING.
The API returns strings that use Windows-style line endings.
@@ -131,6 +142,9 @@ some cases."
(cl-map #'vector #'sx-encoding-clean-content-deep data))
(t data))))
+
+;;;; GZIP
+
(defun sx-encoding-gzipped-p (data)
"Check for magic bytes in DATA.
Check if the first two bytes of a string in DATA match the magic
diff --git a/sx-favorites.el b/sx-favorites.el
index d957167..d98b4c2 100644
--- a/sx-favorites.el
+++ b/sx-favorites.el
@@ -1,4 +1,4 @@
-;;; sx-favorites.el --- Starred questions -*- lexical-binding: t; -*-
+;;; sx-favorites.el --- starred questions -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,21 +19,20 @@
;;; Commentary:
+;; This file provides logic for retrieving and managing a user's
+;; starred questions.
+
;;; Code:
(require 'sx-method)
(require 'sx-cache)
(require 'sx-site)
(require 'sx-networks)
+(require 'sx-filter)
-(defvar sx-favorite-list-filter
- '((.backoff
- .items
- .quota_max
- .quota_remaining
- question.question_id)
- nil
- none))
+(defconst sx-favorite-list-filter
+ (sx-filter-from-nil
+ (question.question_id)))
(defvar sx-favorites--user-favorite-list nil
"Alist of questions favorited by the user.
diff --git a/sx-filter.el b/sx-filter.el
index 8c00c12..57c491d 100644
--- a/sx-filter.el
+++ b/sx-filter.el
@@ -1,4 +1,4 @@
-;;; sx-filter.el --- Handles retrieval of filters. -*- lexical-binding: t; -*-
+;;; sx-filter.el --- handles retrieval of filters -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,6 +19,10 @@
;;; Commentary:
+;; This file manages filters and provides an API to compile filters
+;; and retrieve them from the cache. See `sx-filter-compile' and
+;; `sx-filter-get-var', respectively.
+
;;; Code:
@@ -41,7 +45,27 @@ Structure:
...)")
-;;; Compilation
+;;; Creation
+(defmacro sx-filter-from-nil (included)
+ "Creates a filter data structure with INCLUDED fields.
+All wrapper fields are included by default."
+ `(quote
+ ((,@(sx--tree-expand
+ (lambda (path)
+ (intern (mapconcat #'symbol-name path ".")))
+ included)
+ .backoff
+ .error_id
+ .error_message
+ .error_name
+ .has_more
+ .items
+ .page
+ .page_size
+ .quota_max
+ .quota_remaining
+ .total)
+ nil none)))
;;; @TODO allow BASE to be a precompiled filter name
(defun sx-filter-compile (&optional include exclude base)
@@ -81,6 +105,50 @@ return the compiled filter."
(sx-cache-set 'filter sx--filter-alist)
filter))))
+
+;;; Browsing filter
+(defconst sx-browse-filter
+ (sx-filter-from-nil
+ ((question body_markdown
+ bounty_amount
+ comments
+ answers
+ last_editor
+ last_activity_date
+ accepted_answer_id
+ link
+ upvoted
+ downvoted
+ question_id
+ share_link)
+ (user display_name
+ reputation)
+ (shallow_user display_name
+ reputation)
+ (comment owner
+ body_markdown
+ body
+ link
+ edited
+ creation_date
+ upvoted
+ score
+ post_type
+ post_id
+ comment_id)
+ (answer answer_id
+ last_editor
+ last_activity_date
+ link
+ share_link
+ owner
+ body_markdown
+ upvoted
+ downvoted
+ comments)))
+ "The filter applied when retrieving question data.
+See `sx-question-get-questions' and `sx-question-get-question'.")
+
(provide 'sx-filter)
;;; sx-filter.el ends here
diff --git a/sx-inbox.el b/sx-inbox.el
index d0be379..1efceb1 100644
--- a/sx-inbox.el
+++ b/sx-inbox.el
@@ -1,4 +1,4 @@
-;;; sx-inbox.el --- Base inbox logic. -*- lexical-binding: t; -*-
+;;; sx-inbox.el --- base inbox logic -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -28,7 +28,7 @@
;;; API
-(defvar sx-inbox-filter
+(defconst sx-inbox-filter
'((inbox_item.answer_id
inbox_item.body
inbox_item.comment_id
@@ -91,7 +91,7 @@ These are identified by their links.")
"List of notification items which are read.
These are identified by their links.")
-(defvar sx-inbox--header-line
+(defconst sx-inbox--header-line
'(" "
(:propertize "n p j k" face mode-line-buffer-id)
": Navigate"
@@ -106,7 +106,7 @@ These are identified by their links.")
": Quit")
"Header-line used on the inbox list.")
-(defvar sx-inbox--mode-line
+(defconst sx-inbox--mode-line
'(" "
(:propertize
(sx-inbox--notification-p
diff --git a/sx-interaction.el b/sx-interaction.el
index 3877035..4d71c17 100644
--- a/sx-interaction.el
+++ b/sx-interaction.el
@@ -1,4 +1,4 @@
-;;; sx-interaction.el --- Voting, commenting, and otherwise interacting with questions. -*- lexical-binding: t; -*-
+;;; sx-interaction.el --- voting, commenting, and other interaction -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -189,22 +189,42 @@ If WINDOW nil, the window is decided by
(switch-to-buffer sx-question-mode--buffer))))
-;;; Voting
-(defun sx-toggle-upvote (data)
- "Apply or remove upvote from DATA.
-DATA can be a question, answer, or comment. Interactively, it is
-guessed from context at point."
- (interactive (list (sx--error-if-unread (sx--data-here))))
+;;; Favoriting
+(defun sx-favorite (data &optional undo)
+ "Favorite question given by DATA.
+Interactively, it is guessed from context at point.
+With the UNDO prefix argument, unfavorite the question instead."
+ (interactive (list (sx--error-if-unread (sx--data-here 'question))
+ current-prefix-arg))
(sx-assoc-let data
- (sx-set-vote data "upvote" (null (eq .upvoted t)))))
+ (sx-method-call 'questions
+ :id .question_id
+ :submethod (if undo 'favorite/undo 'favorite)
+ :auth 'warn
+ :site .site_par
+ :url-method 'post
+ :filter sx-browse-filter)))
+(defalias 'sx-star #'sx-favorite)
-(defun sx-toggle-downvote (data)
- "Apply or remove downvote from DATA.
+
+;;; Voting
+(defun sx-upvote (data &optional undo)
+ "Upvote an object given by DATA.
+DATA can be a question, answer, or comment. Interactively, it is
+guessed from context at point.
+With UNDO prefix argument, remove upvote instead of applying it."
+ (interactive (list (sx--error-if-unread (sx--data-here))
+ current-prefix-arg))
+ (sx-set-vote data "upvote" (not undo)))
+
+(defun sx-downvote (data &optional undo)
+ "Downvote an object given by DATA.
DATA can be a question or an answer. Interactively, it is guessed
-from context at point."
- (interactive (list (sx--error-if-unread (sx--data-here))))
- (sx-assoc-let data
- (sx-set-vote data "downvote" (null (eq .downvoted t)))))
+from context at point.
+With UNDO prefix argument, remove downvote instead of applying it."
+ (interactive (list (sx--error-if-unread (sx--data-here))
+ current-prefix-arg))
+ (sx-set-vote data "downvote" (not undo)))
(defun sx-set-vote (data type status)
"Set the DATA's vote TYPE to STATUS.
@@ -223,7 +243,7 @@ changes."
:id (or .comment_id .answer_id .question_id)
:submethod (concat type (unless status "/undo"))
:auth 'warn
- :url-method "POST"
+ :url-method 'post
:filter sx-browse-filter
:site .site_par))))
;; The api returns the new DATA.
@@ -264,7 +284,7 @@ TEXT is a string. Interactively, it is read from the minibufer."
:id (or .post_id .answer_id .question_id)
:submethod "comments/add"
:auth 'warn
- :url-method "POST"
+ :url-method 'post
:filter sx-browse-filter
:site .site_par
:keywords `((body . ,text)))))
@@ -423,3 +443,7 @@ context at point. "
(provide 'sx-interaction)
;;; sx-interaction.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-load.el b/sx-load.el
index 8de4374..003f965 100644
--- a/sx-load.el
+++ b/sx-load.el
@@ -1,4 +1,4 @@
-;;; sx-load.el --- Load all files of the sx package.
+;;; sx-load.el --- load all files of the SX package -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -45,6 +45,7 @@
sx-site
sx-switchto
sx-tab
+ sx-tag
))
(provide 'sx-load)
diff --git a/sx-method.el b/sx-method.el
index 1078014..9d61e60 100644
--- a/sx-method.el
+++ b/sx-method.el
@@ -1,4 +1,4 @@
-;;; sx-method.el --- Main interface for API method calls. -*- lexical-binding: t; -*-
+;;; sx-method.el --- method calls -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -35,9 +35,14 @@
(cl-defun sx-method-call (method &key id
submethod
keywords
+ page
+ (pagesize 100)
(filter '(()))
auth
- (url-method "GET")
+ (url-method 'get)
+ get-all
+ (process-function
+ #'sx-request-response-get-items)
site)
"Call METHOD with additional keys.
@@ -48,8 +53,15 @@ user.
:FILTER is the set of filters to control the returned information
:AUTH defines how to act if the method or filters require
authentication.
-:URL-METHOD is either \"POST\" or \"GET\"
+:URL-METHOD is either `post' or `get'
:SITE is the api parameter specifying the site.
+:GET-ALL is nil or non-nil
+:PROCESS-FUNCTION is a response-processing function
+:PAGE is the page number which will be requested
+:PAGESIZE is the number of items to retrieve per request, default 100
+
+Any conflicting information in :KEYWORDS overrides the :PAGE
+and :PAGESIZE settings.
When AUTH is nil, it is assumed that no auth-requiring filters or
methods will be used. If they are an error will be signaled. This is
@@ -66,6 +78,18 @@ for interactive commands that absolutely require authentication
\(submitting questions/answers, reading inbox, etc). Filters will
treat 'warn as equivalent to t.
+If GET-ALL is nil, this method will only return the first (or
+specified) page available from this method call. If t, all pages
+will be retrieved (`sx-request-all-stop-when-no-more') .
+Otherwise, it is a function STOP-WHEN for `sx-request-all-items'.
+
+If PROCESS-FUNCTION is nil, only the items of the response will
+be returned (`sx-request-response-get-items'). Otherwise, it is
+a function that processes the entire response (as returned by
+`json-read').
+
+See `sx-request-make' and `sx-request-all-items'.
+
Return the entire response as a complex alist."
(declare (indent 1))
(let ((access-token (sx-cache-get 'auth))
@@ -78,12 +102,15 @@ Return the entire response as a complex alist."
(format "/%s" submethod))
;; On GET methods site is buggy, so we
;; need to provide it as a url argument.
- (when (and site (string= url-method "GET"))
+ (when (and site (eq url-method 'get))
(prog1
(format "?site=%s" site)
(setq site nil)))))
- (call #'sx-request-make)
- parameters)
+ (call (if get-all #'sx-request-all-items #'sx-request-make))
+ (get-all
+ (cond
+ ((eq get-all t) #'sx-request-all-stop-when-no-more)
+ (t get-all))))
(lwarn "sx-call-method" :debug "A: %S T: %S. M: %S,%s. F: %S" (equal 'warn auth)
access-token method-auth full-method filter-auth)
(unless access-token
@@ -102,15 +129,22 @@ Return the entire response as a complex alist."
((and (or filter-auth method-auth) (not auth))
(error "This request requires authentication."))))
;; Concatenate all parameters now that filter is ensured.
- (setq parameters
- (cons (cons 'filter (sx-filter-get-var filter))
- keywords))
+ (push `(filter . ,(sx-filter-get-var filter)) keywords)
+ (unless (assq 'page keywords)
+ (push `(page . ,page) keywords))
+ (unless (assq 'pagesize keywords)
+ (push `(pagesize . ,pagesize) keywords))
(when site
- (setq parameters (cons (cons 'site site) parameters)))
+ (push `(site . ,site) keywords))
(funcall call
full-method
- parameters
- url-method)))
+ keywords
+ url-method
+ (or get-all process-function))))
(provide 'sx-method)
;;; sx-method.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-networks.el b/sx-networks.el
index e4660af..45eaf05 100644
--- a/sx-networks.el
+++ b/sx-networks.el
@@ -1,4 +1,4 @@
-;;; sx-networks.el --- user network information -*- lexical-binding: t; -*-
+;;; sx-networks.el --- user network information -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,36 +19,31 @@
;;; Commentary:
+;; This file provides logic for retrieving information about the user
+;; across the entire network, e.g. their registered sites.
+
;;; Code:
(require 'sx-method)
(require 'sx-cache)
(require 'sx-site)
-
-(defvar sx-network--user-filter
- '((.backoff
- .error_id
- .error_message
- .error_name
- .has_more
- .items
- .quota_max
- .quota_remaining
- badge_count.bronze
- badge_count.silver
- badge_count.gold
- network_user.account_id
- network_user.answer_count
- network_user.badge_counts
- network_user.creation_date
- network_user.last_access_date
- network_user.reputation
- network_user.site_name
- network_user.site_url
- network_user.user_id
- network_user.user_type)
- nil
- none))
+(require 'sx-filter)
+
+(defconst sx-network--user-filter
+ (sx-filter-from-nil
+ ((badge_count bronze
+ silver
+ gold)
+ (network_user account_id
+ answer_count
+ badge_counts
+ creation_date
+ last_access_date
+ reputation
+ site_name
+ site_url
+ user_id
+ user_type))))
(defun sx-network--get-associated ()
"Retrieve cached information for network user.
diff --git a/sx-notify.el b/sx-notify.el
index c335427..0c9a5b8 100644
--- a/sx-notify.el
+++ b/sx-notify.el
@@ -1,4 +1,4 @@
-;;; sx-notify.el --- Mode-line notifications. -*- lexical-binding: t; -*-
+;;; sx-notify.el --- mode-line notifications -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -27,7 +27,7 @@
;;; mode-line notification
-(defvar sx-notify--mode-line
+(defconst sx-notify--mode-line
'((sx-inbox--unread-inbox (sx-inbox--unread-notifications " ["))
(sx-inbox--unread-inbox
(:propertize
diff --git a/sx-question-list.el b/sx-question-list.el
index 9e08787..c72dc0d 100644
--- a/sx-question-list.el
+++ b/sx-question-list.el
@@ -1,4 +1,4 @@
-;;; sx-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
@@ -19,6 +19,8 @@
;;; Commentary:
+;; Provides question list logic (as used in e.g. `sx-tab-frontpage').
+
;;; Code:
(require 'tabulated-list)
(require 'cl-lib)
@@ -104,6 +106,21 @@
""
:group 'sx-question-list-faces)
+(defface sx-question-list-bounty
+ '((t :inherit font-lock-warning-face))
+ ""
+ :group 'sx-question-list-faces)
+
+(defface sx-question-list-reputation
+ '((t :inherit sx-question-list-date))
+ ""
+ :group 'sx-question-list-faces)
+
+(defface sx-question-list-user
+ '((t :inherit font-lock-builtin-face))
+ ""
+ :group 'sx-question-list-faces)
+
;;; Backend variables
(defvar sx-question-list--print-function #'sx-question-list--print-info
@@ -141,20 +158,35 @@ Also see `sx-question-list-refresh'."
'sx-question-list-answers-accepted
'sx-question-list-answers))
(concat
+ ;; First line
(propertize
.title
'face (if (sx-question--read-p question-data)
'sx-question-list-read-question
'sx-question-list-unread-question))
(propertize " " 'display "\n ")
+ ;; Second line
(propertize favorite 'face 'sx-question-list-favorite)
- " "
- (propertize (concat (sx-time-since .last_activity_date)
- sx-question-list-ago-string)
+ (if (and (numberp .bounty_amount) (> .bounty_amount 0))
+ (propertize (format "%4d" .bounty_amount)
+ 'face 'sx-question-list-bounty)
+ " ")
+ " "
+ (propertize (format "%3s%s"
+ (sx-time-since .last_activity_date)
+ sx-question-list-ago-string)
'face 'sx-question-list-date)
" "
- (propertize (mapconcat #'sx-question--tag-format .tags " ")
+ ;; @TODO: Make this width customizable. (Or maybe just make
+ ;; the whole thing customizable)
+ (propertize (format "%-40s" (mapconcat #'sx-question--tag-format .tags " "))
'face 'sx-question-list-tags)
+ " "
+ (let-alist .owner
+ (format "%15s %5s"
+ (propertize .display_name 'face 'sx-question-list-user)
+ (propertize (number-to-string .reputation)
+ 'face 'sx-question-list-reputation)))
(propertize " " 'display "\n")))))))
(defvar sx-question-list--pages-so-far 0
@@ -196,7 +228,7 @@ and thus not displayed in the list of questions.
This is ignored if `sx-question-list--refresh-function' is set.")
(make-variable-buffer-local 'sx-question-list--dataset)
-(defvar sx-question-list--header-line
+(defconst sx-question-list--header-line
'(" "
(:propertize "n p j k" face mode-line-buffer-id)
": Navigate"
@@ -319,10 +351,11 @@ into consideration.
("S" sx-search)
("s" sx-switchto-map)
("v" sx-visit-externally)
- ("u" sx-toggle-upvote)
- ("d" sx-toggle-downvote)
+ ("u" sx-upvote)
+ ("d" sx-downvote)
("h" sx-question-list-hide)
("m" sx-question-list-mark-read)
+ ("*" sx-favorite)
([?\r] sx-display)
))
@@ -575,3 +608,7 @@ Sets `sx-question-list--site' and then call
(provide 'sx-question-list)
;;; sx-question-list.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-question-mode.el b/sx-question-mode.el
index c618c96..5303ebb 100644
--- a/sx-question-mode.el
+++ b/sx-question-mode.el
@@ -1,4 +1,4 @@
-;;; sx-question-mode.el --- Major-mode for displaying a question. -*- lexical-binding: t; -*-
+;;; sx-question-mode.el --- major-mode for displaying questions -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -19,6 +19,9 @@
;;; Commentary:
+;; This file provides a means to print questions with their answers
+;; and all comments. See the customizable group `sx-question-mode'.
+
;;; Code:
(eval-when-compile
@@ -175,7 +178,7 @@ property."
;;; Major-mode
-(defvar sx-question-mode--header-line
+(defconst sx-question-mode--header-line
'(" "
(:propertize "n p TAB" face mode-line-buffer-id)
": Navigate"
@@ -225,14 +228,15 @@ Letters do not insert themselves; instead, they are commands.
("g" sx-question-mode-refresh)
("c" sx-comment)
("v" sx-visit-externally)
- ("u" sx-toggle-upvote)
- ("d" sx-toggle-downvote)
+ ("u" sx-upvote)
+ ("d" sx-downvote)
("q" quit-window)
(" " scroll-up-command)
("a" sx-answer)
("e" sx-edit)
("S" sx-search)
("s" sx-switchto-map)
+ ("*" sx-favorite)
(,(kbd "S-SPC") scroll-down-command)
([backspace] scroll-down-command)
([tab] forward-button)
@@ -270,3 +274,7 @@ query the api."
(provide 'sx-question-mode)
;;; sx-question-mode.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-question-print.el b/sx-question-print.el
index 07378e8..e148d5f 100644
--- a/sx-question-print.el
+++ b/sx-question-print.el
@@ -1,4 +1,4 @@
-;;; sx-question-print.el --- Populating the question-mode buffer with content. -*- lexical-binding: t; -*-
+;;; sx-question-print.el --- populating the question-mode buffer with content -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -468,3 +468,7 @@ font-locking."
(provide 'sx-question-print)
;;; sx-question-print.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-question.el b/sx-question.el
index 1df67cd..b9fc78a 100644
--- a/sx-question.el
+++ b/sx-question.el
@@ -1,4 +1,4 @@
-;;; sx-question.el --- Base question logic. -*- lexical-binding: t; -*-
+;;; sx-question.el --- question logic -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,6 +19,9 @@
;;; Commentary:
+;; Thie file provides an API for retrieving questions and defines
+;; additional logic for marking questions as read or hidden.
+
;;; Code:
@@ -119,7 +122,8 @@ See `sx-question--user-read-list'."
;; Question already present.
((setq cell (assoc .question_id site-cell))
;; Current version is newer than cached version.
- (when (> .last_activity_date (cdr cell))
+ (when (or (not (numberp (cdr cell)))
+ (> .last_activity_date (cdr cell)))
(setcdr cell .last_activity_date)))
;; Question wasn't present.
(t
diff --git a/sx-request.el b/sx-request.el
index bc34f9c..ebc16d2 100644
--- a/sx-request.el
+++ b/sx-request.el
@@ -1,4 +1,4 @@
-;;; sx-request.el --- Requests and url manipulation. -*- lexical-binding: t; -*-
+;;; sx-request.el --- requests and url manipulation -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -92,16 +92,52 @@ number of requests left every time it finishes a call."
:group 'sx
:type 'integer)
+(defvar sx-request-all-items-delay
+ 1
+ "Delay in seconds with each `sx-request-all-items' iteration.
+It is good to use a reasonable delay to avoid rate-limiting.")
+
;;; Making Requests
+(defun sx-request-all-items (method &optional args request-method
+ stop-when)
+ "Call METHOD with ARGS until there are no more items.
+STOP-WHEN is a function that takes the entire response and
+returns non-nil if the process should stop.
+
+All other arguments are identical to `sx-request-make', but
+PROCESS-FUNCTION is given the default value of `identity' (rather
+than `sx-request-response-get-items') to allow STOP-WHEN to
+access the response wrapper."
+ ;; @TODO: Refactor. This is the product of a late-night jam
+ ;; session... it is not intended to be model code.
+ (declare (indent 1))
+ (let* ((return-value [])
+ (current-page 1)
+ (stop-when (or stop-when #'sx-request-all-stop-when-no-more))
+ (process-function #'identity)
+ (response
+ (sx-request-make method `((page . ,current-page) ,@args)
+ request-method process-function)))
+ (while (not (funcall stop-when response))
+ (setq current-page (1+ current-page)
+ return-value
+ (vconcat return-value
+ (cdr (assoc 'items response))))
+ (sleep-for sx-request-all-items-delay)
+ (setq response
+ (sx-request-make method `((page . ,current-page) ,@args)
+ request-method process-function)))
+ (vconcat return-value
+ (cdr (assoc 'items response)))))
-(defun sx-request-make (method &optional args request-method)
+(defun sx-request-make (method &optional args request-method process-function)
"Make a request to the API, executing METHOD with ARGS.
You should almost certainly be using `sx-method-call' instead of
-this function. REQUEST-METHOD is one of `GET' (default) or `POST'.
+this function. REQUEST-METHOD is one of `get' (default) or `post'.
-Returns cleaned response content.
-See (`sx-encoding-clean-content-deep').
+Returns the entire response as processed by PROCESS-FUNCTION.
+This defaults to `sx-request-response-get-items'.
The full set of arguments is built with
`sx-request--build-keyword-arguments', prepending
@@ -117,11 +153,12 @@ then read with `json-read-from-string'.
`sx-request-remaining-api-requests' is updated appropriately and
the main content of the response is returned."
+ (declare (indent 1))
(let* ((url-automatic-caching t)
(url-inhibit-uncompression t)
(url-request-data (sx-request--build-keyword-arguments args nil))
(request-url (concat sx-request-api-root method))
- (url-request-method request-method)
+ (url-request-method (and request-method (symbol-name request-method)))
(url-request-extra-headers
'(("Content-Type" . "application/x-www-form-urlencoded")))
(response-buffer (url-retrieve-synchronously request-url)))
@@ -164,7 +201,8 @@ the main content of the response is returned."
sx-request-remaining-api-requests-message-threshold)
(sx-message "%d API requests remaining"
sx-request-remaining-api-requests))
- (sx-encoding-clean-content-deep .items)))))))
+ (funcall (or process-function #'sx-request-response-get-items)
+ response)))))))
(defun sx-request-fallback (_method &optional _args _request-method)
"Fallback method when authentication is not available.
@@ -205,6 +243,20 @@ false, use the symbol `false'. Each element is processed with
alist))
"&")))
+
+;;; Response Processors
+(defun sx-request-response-get-items (response)
+ "Returns the items from RESPONSE."
+ (sx-assoc-let response
+ (sx-encoding-clean-content-deep .items)))
+
+(defun sx-request-all-stop-when-no-more (response)
+ (or (not response)
+ (equal :json-false (cdr (assoc 'has_more response)))))
(provide 'sx-request)
;;; sx-request.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-search.el b/sx-search.el
index 2633da9..d47905e 100644
--- a/sx-search.el
+++ b/sx-search.el
@@ -1,4 +1,4 @@
-;;; sx-search.el --- Searching for questions. -*- lexical-binding: t; -*-
+;;; sx-search.el --- searching for questions -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -110,3 +110,7 @@ prefix argument, the user is asked for everything."
(provide 'sx-search)
;;; sx-search.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-site.el b/sx-site.el
index 8bd4fc0..9b2ea34 100644
--- a/sx-site.el
+++ b/sx-site.el
@@ -19,29 +19,23 @@
;;; Commentary:
+;; This file provides various pieces of site logic, such as retrieving
+;; the list of sites and the list of a user's favorited questions.
+
;;; Code:
(require 'sx-method)
(require 'sx-cache)
+(require 'sx-filter)
-(defvar sx-site-browse-filter
- '((.backoff
- .error_id
- .error_message
- .error_name
- .has_more
- .items
- .quota_max
- .quota_remaining
- site.site_type
- site.name
- site.site_url
- site.api_site_parameter
- site.related_sites
- related_site.api_site_parameter
- related_site.relation)
- nil
- none)
+(defconst sx-site-browse-filter
+ (sx-filter-from-nil
+ ((site site_type
+ name
+ api_site_parameter
+ related_sites)
+ (related_site api_site_parameter
+ relation)))
"Filter for browsing sites.")
(defun sx-site--get-site-list ()
@@ -49,7 +43,7 @@
(sx-cache-get
'site-list
'(sx-method-call 'sites
- :keywords '((pagesize . 999))
+ :pagesize 999
:filter sx-site-browse-filter)))
(defcustom sx-site-favorites
diff --git a/sx-switchto.el b/sx-switchto.el
index 1a2c3a0..458586a 100644
--- a/sx-switchto.el
+++ b/sx-switchto.el
@@ -1,4 +1,4 @@
-;;; sx-switchto.el --- Keymap for navigating between pages. -*- lexical-binding: t; -*-
+;;; sx-switchto.el --- keymap for navigating between pages -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -47,6 +47,7 @@
("U" sx-tab-unanswered-my-tags)
("v" sx-tab-topvoted)
("w" sx-tab-week)
+ ("*" sx-tab-starred)
))
diff --git a/sx-tab.el b/sx-tab.el
index 92e5921..2d605a5 100644
--- a/sx-tab.el
+++ b/sx-tab.el
@@ -1,4 +1,4 @@
-;;; sx-tab.el --- Functions for viewing different tabs. -*- lexical-binding: t; -*-
+;;; sx-tab.el --- functions for viewing different tabs -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
@@ -19,9 +19,22 @@
;;; Commentary:
-;;
+;; This file provides a single macro to define 'tabs' to view lists of
+;; questions.
+
+;;; Tabs:
+
+;; - FrontPage :: The standard front page
+;; - Newest :: Newest questions
+;; - TopVoted :: Top-voted questions
+;; - Hot :: Hot questions recently
+;; - Week :: Hot questions for the week
+;; - Month :: Hot questions for the month
+;; - Unanswered :: Unanswered questions
+;; - Unanswered My-tags :: Unanswered questions (subscribed tags)
+;; - Featured :: Featured questions
+;; - Starred :: Favorite questions
-
;;; Code:
(require 'sx)
@@ -232,6 +245,24 @@ If SITE is nil, use `sx-default-site'."
nil t)
+;;; Starred
+(sx-tab--define "Starred"
+ (lambda (page)
+ (sx-method-call 'me
+ :page page
+ :site sx-question-list--site
+ :auth t
+ :submethod 'favorites
+ :filter sx-browse-filter)))
+;;;###autoload
+(autoload 'sx-tab-featured
+ (expand-file-name
+ "sx-tab"
+ (when load-file-name
+ (file-name-directory load-file-name)))
+ nil t)
+
+
;;; Inter-modes navigation
(defun sx-tab-meta-or-main ()
"Switch to the meta version of a main site, or vice-versa.
@@ -248,3 +279,7 @@ belongs to."
(provide 'sx-tab)
;;; sx-tab.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-tag.el b/sx-tag.el
new file mode 100644
index 0000000..5e75890
--- /dev/null
+++ b/sx-tag.el
@@ -0,0 +1,90 @@
+;;; sx-tag.el --- retrieving list of tags and handling tags -*- lexical-binding: t; -*-
+
+;; 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:
+(eval-when-compile
+ '(require 'cl-lib))
+
+(require 'sx)
+(require 'sx-method)
+
+
+;;; Getting the list from a site
+(defconst sx-tag-filter
+ (sx-filter-from-nil
+ (tag.name
+ tag.synonyms))
+ "Filter used when querying tags.")
+
+(defun sx-tag--get-all (site &optional no-synonyms)
+ "Retrieve all tags for SITE.
+If NO-SYNONYMS is non-nil, don't return synonyms."
+ (cl-reduce
+ (lambda (so-far tag)
+ (let-alist tag
+ (cons .name
+ (if no-synonyms so-far
+ (append .synonyms so-far)))))
+ (sx-method-call 'tags
+ :get-all t
+ :filter sx-tag-filter
+ :site site)
+ :initial-value nil))
+
+(defun sx-tag--get-some-tags-containing (site string)
+ "Return at most 100 tags for SITE containing STRING.
+Returns an array."
+ (sx-method-call 'tags
+ :auth nil
+ :filter sx-tag-filter
+ :site site
+ :keywords `((inname . ,string))))
+
+(defun sx-tag--get-some-tag-names-containing (site string)
+ "Return at most 100 tag names for SITE containing STRING.
+Returns a list."
+ (mapcar (lambda (x) (cdr (assoc 'name x)))
+ (sx-tag--get-some-tags-containing site string)))
+
+
+;;; Check tag validity
+(defun sx-tag--invalid-name-p (site tags)
+ "Nil if TAGS exist in SITE.
+TAGS can be a string (the tag name) or a list of strings.
+Fails if TAGS is a list with more than 100 items.
+Return the list of invalid tags in TAGS."
+ (and (listp tags) (> (length tags) 100)
+ (error "Invalid argument. TAG has more than 100 items"))
+ (let ((result
+ (mapcar
+ (lambda (x) (cdr (assoc 'name x)))
+ (sx-method-call 'tags
+ :id (sx--thing-as-string tags)
+ :submethod 'info
+ :auth nil
+ :filter sx-tag-filter
+ :site site))))
+ (cl-remove-if (lambda (x) (member x result)) tags)))
+
+(provide 'sx-tag)
+;;; sx-tag.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx-time.el b/sx-time.el
index 3de124d..9fa0037 100644
--- a/sx-time.el
+++ b/sx-time.el
@@ -1,4 +1,4 @@
-;;; sx-time.el --- time
+;;; sx-time.el --- time -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,13 +19,14 @@
;;; Commentary:
-;;
+;; This file provides functions for manipulating and displaying
+;; timestamps.
;;; Code:
(require 'time-date)
-(defvar sx-time-seconds-to-string
+(defconst 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.
@@ -77,3 +78,7 @@ See also `sx-time-date-format-year'."
(provide 'sx-time)
;;; sx-time.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx.el b/sx.el
index 62484b7..3271755 100644
--- a/sx.el
+++ b/sx.el
@@ -1,4 +1,4 @@
-;;; sx.el --- StackExchange client. Ask and answer questions on Stack Overflow, Super User, and the likes. -*- lexical-binding: t; -*-
+;;; sx.el --- StackExchange client. Ask and answer questions on Stack Overflow, Super User, and the likes -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -136,6 +136,26 @@ with a `link' property)."
result))
result))
+(defun sx--tree-paths (tree)
+ "Return a list of all paths in TREE.
+Adapted from http://stackoverflow.com/q/3019250."
+ (if (atom tree)
+ (list (list tree))
+ (apply #'append
+ (mapcar (lambda (node)
+ (mapcar (lambda (path)
+ (cons (car tree) path))
+ (sx--tree-paths node)))
+ (cdr tree)))))
+
+(defun sx--tree-expand (path-func tree)
+ "Apply PATH-FUNC to every path in TREE.
+Return the result. See `sx--tree-paths'."
+ (mapcar path-func
+ (apply #'append
+ (mapcar #'sx--tree-paths
+ tree))))
+
(defmacro sx-assoc-let (alist &rest body)
"Use ALIST with `let-alist' to execute BODY.
`.site_par' has a special meaning, thanks to `sx--ensure-site'.
@@ -149,46 +169,6 @@ If ALIST doesn't have a `site' property, one is created using the
`(let-alist ,alist ,@body))))
-;;; Browsing filter
-(defvar sx-browse-filter
- '((question.body_markdown
- question.comments
- question.answers
- question.last_editor
- question.last_activity_date
- question.accepted_answer_id
- question.link
- question.upvoted
- question.downvoted
- question.question_id
- question.share_link
- user.display_name
- comment.owner
- comment.body_markdown
- comment.body
- comment.link
- comment.edited
- comment.creation_date
- comment.upvoted
- comment.score
- comment.post_type
- comment.post_id
- comment.comment_id
- answer.answer_id
- answer.last_editor
- answer.last_activity_date
- answer.link
- answer.share_link
- answer.owner
- answer.body_markdown
- answer.upvoted
- answer.downvoted
- answer.comments)
- (user.profile_image shallow_user.profile_image))
- "The filter applied when retrieving question data.
-See `sx-question-get-questions' and `sx-question-get-question'.")
-
-
;;; Utility Functions
(defun sx-completing-read (&rest args)
"Like `completing-read', but possibly use ido.
@@ -320,7 +300,7 @@ Return the result of BODY."
(push ov sx--overlays))
result))
-(defvar sx--ascii-replacement-list
+(defconst sx--ascii-replacement-list
'(("[:space:]" . "")
("àåáâäãåą" . "a")
("èéêëę" . "e")
diff --git a/test/test-api.el b/test/test-api.el
index ca775ff..b7d5dbb 100644
--- a/test/test-api.el
+++ b/test/test-api.el
@@ -11,3 +11,6 @@
(should-error
(sx-request-make "questions" '(()))))
+(ert-deftest test-method-get-all ()
+ "Tests sx-method interface to `sx-request-all-items'"
+ (should (< 250 (length (sx-method-call 'sites :get-all t)))))
diff --git a/test/test-macros.el b/test/test-macros.el
index b6bf20b..1634603 100644
--- a/test/test-macros.el
+++ b/test/test-macros.el
@@ -20,3 +20,25 @@
(should
(equal (sx-assoc-let data (cons .test-one .test-two))
'(1 . 2)))))
+
+(ert-deftest macro-test--sx-filter-from-nil ()
+ "Test `sx-filter-from-nil'"
+ (should
+ (equal
+ (sx-filter-from-nil
+ (one two (three four five) (six seven)
+ (a b c d e)))
+ '((one two three.four three.five six.seven
+ a.b a.c a.d a.e
+ .backoff
+ .error_id
+ .error_message
+ .error_name
+ .has_more
+ .items
+ .page
+ .page_size
+ .quota_max
+ .quota_remaining
+ .total)
+ nil none))))
diff --git a/test/test-search.el b/test/test-search.el
index 72dbcdc..72f0846 100644
--- a/test/test-search.el
+++ b/test/test-search.el
@@ -29,8 +29,8 @@
(ert-deftest test-search-full-page ()
"Test retrieval of the full search page"
(should
- (= 30 (length (sx-search-get-questions
- "stackoverflow" 1 "jquery")))))
+ (= 100 (length (sx-search-get-questions
+ "stackoverflow" 1 "jquery")))))
(ert-deftest test-search-exclude-tags ()
"Test excluding tags from a search"
diff --git a/test/test-state.el b/test/test-state.el
new file mode 100644
index 0000000..7af4a64
--- /dev/null
+++ b/test/test-state.el
@@ -0,0 +1,22 @@
+(defmacro with-question-data (cell id &rest body)
+ (declare (indent 2))
+ `(let ((,cell '((question_id . ,id)
+ (site_par . "emacs")
+ (last_activity_date . 1234123456))))
+ ,@body))
+
+(ert-deftest test-question-mark-read ()
+ "00ccd139248e782cd8316eff65c26aed838c7e46"
+ (with-question-data q 10
+ ;; Check basic logic.
+ (should (sx-question--mark-read q))
+ (should (sx-question--read-p q))
+ (should (not (setcdr (assq 10 (cdr (assoc "emacs" sx-question--user-read-list))) nil)))
+ ;; Don't freak out because the cdr was nil.
+ (should (not (sx-question--read-p q)))
+ (should (sx-question--mark-read q)))
+ (should
+ (with-question-data q nil
+ ;; Don't freak out because question_id was nil.
+ (sx-question--mark-read q))))
+
diff --git a/test/test-util.el b/test/test-util.el
index 5db1691..1e3dc2b 100644
--- a/test/test-util.el
+++ b/test/test-util.el
@@ -29,3 +29,17 @@
(string= (sx--thing-as-string
'test& nil t)
"test%26")))
+
+(ert-deftest tree ()
+ (should
+ (equal
+ (sx--tree-expand
+ (lambda (path) (mapconcat #'symbol-name path "."))
+ '(a b (c d (e f g) h i (j k) l) m (n o) p))
+ '("a" "b" "c.d" "c.e.f" "c.e.g" "c.h" "c.i" "c.j.k" "c.l" "m" "n.o" "p")))
+ (should
+ (equal
+ (sx--tree-expand
+ (lambda (path) (intern (mapconcat #'symbol-name path "/")))
+ '(a b (c d (e f g) h i (j k) l) m (n o) p))
+ '(a b c/d c/e/f c/e/g c/h c/i c/j/k c/l m n/o p))))
diff --git a/test/tests.el b/test/tests.el
index d06c0ff..ce42a9f 100644
--- a/test/tests.el
+++ b/test/tests.el
@@ -11,6 +11,7 @@
sx-initialized t
sx-request-remaining-api-requests-message-threshold 50000
debug-on-error t
+ url-show-status nil
user-emacs-directory "."
sx-test-base-dir (file-name-directory (or load-file-name "./")))