diff options
-rw-r--r-- | .agignore | 5 | ||||
-rw-r--r-- | .gitignore | 12 | ||||
-rw-r--r-- | .travis.yml | 4 | ||||
-rw-r--r-- | Makefile | 10 | ||||
-rw-r--r-- | bot/sx-bot.el | 82 | ||||
-rwxr-xr-x | bot/sx-bot.sh | 36 | ||||
-rw-r--r-- | sx-auth.el | 91 | ||||
-rw-r--r-- | sx-babel.el | 5 | ||||
-rw-r--r-- | sx-button.el | 6 | ||||
-rw-r--r-- | sx-cache.el | 16 | ||||
-rw-r--r-- | sx-compose.el | 39 | ||||
-rw-r--r-- | sx-encoding.el | 16 | ||||
-rw-r--r-- | sx-favorites.el | 17 | ||||
-rw-r--r-- | sx-filter.el | 72 | ||||
-rw-r--r-- | sx-inbox.el | 8 | ||||
-rw-r--r-- | sx-interaction.el | 56 | ||||
-rw-r--r-- | sx-load.el | 3 | ||||
-rw-r--r-- | sx-method.el | 58 | ||||
-rw-r--r-- | sx-networks.el | 47 | ||||
-rw-r--r-- | sx-notify.el | 4 | ||||
-rw-r--r-- | sx-question-list.el | 53 | ||||
-rw-r--r-- | sx-question-mode.el | 16 | ||||
-rw-r--r-- | sx-question-print.el | 6 | ||||
-rw-r--r-- | sx-question.el | 8 | ||||
-rw-r--r-- | sx-request.el | 66 | ||||
-rw-r--r-- | sx-search.el | 6 | ||||
-rw-r--r-- | sx-site.el | 32 | ||||
-rw-r--r-- | sx-switchto.el | 3 | ||||
-rw-r--r-- | sx-tab.el | 41 | ||||
-rw-r--r-- | sx-tag.el | 90 | ||||
-rw-r--r-- | sx-time.el | 11 | ||||
-rw-r--r-- | sx.el | 64 | ||||
-rw-r--r-- | test/test-api.el | 3 | ||||
-rw-r--r-- | test/test-macros.el | 22 | ||||
-rw-r--r-- | test/test-search.el | 4 | ||||
-rw-r--r-- | test/test-state.el | 22 | ||||
-rw-r--r-- | test/test-util.el | 14 | ||||
-rw-r--r-- | test/tests.el | 1 |
38 files changed, 825 insertions, 224 deletions
@@ -1,3 +1,5 @@ +# -*- gitignore -*- + # Backup files *~ \#*\# @@ -18,3 +20,6 @@ test/data-samples # Info files *.info + +# Data directory +data/ @@ -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 @@ -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"' @@ -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: @@ -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: @@ -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) )) @@ -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: @@ -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: @@ -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 "./"))) |