aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore12
-rw-r--r--.travis.yml2
-rw-r--r--Makefile11
-rwxr-xr-xbot/sx-bot.sh2
-rw-r--r--sx-auth.el91
-rw-r--r--sx-babel.el13
-rw-r--r--sx-button.el37
-rw-r--r--sx-cache.el19
-rw-r--r--sx-compose.el81
-rw-r--r--sx-encoding.el16
-rw-r--r--sx-favorites.el13
-rw-r--r--sx-filter.el36
-rw-r--r--sx-inbox.el9
-rw-r--r--sx-interaction.el27
-rw-r--r--sx-load.el2
-rw-r--r--sx-method.el6
-rw-r--r--sx-networks.el7
-rw-r--r--sx-notify.el4
-rw-r--r--sx-question-list.el40
-rw-r--r--sx-question-mode.el11
-rw-r--r--sx-question-print.el92
-rw-r--r--sx-question.el20
-rw-r--r--sx-request.el40
-rw-r--r--sx-search.el21
-rw-r--r--sx-site.el6
-rw-r--r--sx-switchto.el16
-rw-r--r--sx-tab.el25
-rw-r--r--sx-tag.el59
-rw-r--r--sx-time.el11
-rw-r--r--sx-user.el203
-rw-r--r--sx.el126
-rw-r--r--sx.org79
-rw-r--r--test/test-macros.el5
-rw-r--r--test/test-printing.el123
34 files changed, 933 insertions, 332 deletions
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 d00ab46..067fa62 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -31,7 +31,7 @@ script:
notifications:
webhooks:
urls:
- - https://webhooks.gitter.im/e/07063bd143e35f54b1e8
+ - https://webhooks.gitter.im/e/77b562dfc62ea5cd545a
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: false # default: false
diff --git a/Makefile b/Makefile
index ae11f84..e04c1b0 100644
--- a/Makefile
+++ b/Makefile
@@ -20,20 +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
- rm -rf .sx/
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.sh b/bot/sx-bot.sh
index 6a5df17..22c7284 100755
--- a/bot/sx-bot.sh
+++ b/bot/sx-bot.sh
@@ -1,6 +1,6 @@
#!/usr/bin/bash
-DESTINATION_BRANCH=gh-pages
+DESTINATION_BRANCH=data
function notify-done {
local title
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..7f84fe0 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
@@ -22,7 +22,7 @@
;; This file contains functions and a variable for font-locking the
;; content of markdown pre blocks according to their language. The
;; main configuration point, for both the user and the developer is
-;; the varuable `sx-babel-major-mode-alist', which see.
+;; the variable `sx-babel-major-mode-alist', which see.
;;; Code:
@@ -34,6 +34,12 @@
;; @TODO: Make shell-mode work here. Currently errors because it
;; needs a process. `sh-mode' isn't as nice.
(,(rx (or "$ " "# ")) sh-mode)
+ ;; Not sure if leaving out "[{" might lead to false positives.
+ (,(rx "\\" (+ alnum) (any "[{")) latex-mode)
+ ;; Right now, this will match a lot of stuff. Once we are capable
+ ;; of determining major-mode from tags, site, and comments, this
+ ;; will work as a last case fallback.
+ (,(rx (or (and "int" (+ space) "main" (* space) "("))) c-mode)
)
"List of cons cells determining which major-mode to use when.
Each car is a rule and each cdr is a major-mode. The first rule
@@ -122,3 +128,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..5a2f052 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
@@ -23,7 +23,7 @@
;; buttons, see:
;; http://www.gnu.org/software/emacs/manual/html_node/elisp/Buttons.html
;;
-;; Most interactible parts of the SX buffers are buttons. Wherever you
+;; Most interactive parts of the SX buffers are buttons. Wherever you
;; are, you can always cycle through all buttons by hitting `TAB',
;; that should help identify what's a button in each buffer.
;;
@@ -34,7 +34,7 @@
;;
;; Buttons can then be inserted in their respective files using
;; `insert-text-button'. Give it the string, the `:type' you defined,
-;; and any aditional properties that can only be determined at
+;; and any additional properties that can only be determined at
;; creation. Existing text can be transformed into a button with
;; `make-text-button' instead.
@@ -104,23 +104,29 @@ usually part of a code-block."
;;; Help-echo definitions
-(defvar sx-button--help-echo
+(defconst sx-button--help-echo
(concat "mouse-1, RET"
(propertize ": %s -- " 'face 'minibuffer-prompt)
"w"
(propertize ": copy %s" 'face 'minibuffer-prompt))
"Base help-echo on which others can be written.")
-(defvar sx-button--question-title-help-echo
+(defconst sx-button--user-help-echo
(format sx-button--help-echo
- (propertize "hide content" 'face 'minibuffer-prompt)
- (propertize "link" 'face 'minibuffer-prompt))
+ "visit user page"
+ "link")
+ "Help echoed in the minibuffer when point is on a user.")
+
+(defconst sx-button--question-title-help-echo
+ (format sx-button--help-echo
+ "hide content"
+ "link")
"Help echoed in the minibuffer when point is on a section.")
-(defvar sx-button--link-help-echo
+(defconst sx-button--link-help-echo
(format sx-button--help-echo
- (propertize "visit %s" 'face 'minibuffer-prompt)
- (propertize "URL" 'face 'minibuffer-prompt))
+ "visit %s"
+ "URL")
"Help echoed in the minibuffer when point is on a section.")
@@ -145,6 +151,13 @@ usually part of a code-block."
'action #'sx-button-follow-link
:supertype 'sx-button)
+(define-button-type 'sx-button-user
+ 'action #'sx-button-follow-link
+ 'help-echo sx-button--user-help-echo
+ ;; We use different faces on different parts of the user button.
+ 'face 'sx-user-name
+ :supertype 'sx-button)
+
(define-button-type 'sx-button-comment
'help-echo (concat "mouse-1, RET"
(propertize ": write a comment"
@@ -163,3 +176,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 e68397d..3e8e08f 100644
--- a/sx-cache.el
+++ b/sx-cache.el
@@ -1,4 +1,4 @@
-;;; sx-cache.el --- caching -*- lexical-binding: t; -*-
+;;; sx-cache.el --- caching -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
@@ -19,17 +19,22 @@
;;; 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:
-(defcustom sx-cache-directory
- (expand-file-name ".sx" user-emacs-directory)
+(defcustom sx-cache-directory (locate-user-emacs-file ".sx")
"Directory containing cached data."
:type 'directory
:group 'sx)
diff --git a/sx-compose.el b/sx-compose.el
index 8a8637b..3047a97 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
@@ -34,6 +34,7 @@
(require 'markdown-mode)
(require 'sx)
+(require 'sx-tag)
(defgroup sx-compose-mode nil
"Customization group for sx-compose-mode."
@@ -67,7 +68,7 @@ succeeds.")
Is invoked between `sx-compose-before-send-hook' and
`sx-compose-after-send-functions'.")
-(defvar sx-compose--question-headers
+(defconst sx-compose--question-headers
(concat
#("Title: " 0 7 (intangible t read-only t rear-nonsticky t))
"%s"
@@ -82,6 +83,23 @@ Is invoked between `sx-compose-before-send-hook' and
"Headers inserted when composing a new question.
Used by `sx-compose-create'.")
+(defconst sx-compose--header-line
+ '(" "
+ (:propertize "C-c C-c" face mode-line-buffer-id)
+ ": Finish and Send"
+ (sx-compose--is-question-p
+ (" "
+ (:propertize "C-c C-q" face mode-line-buffer-id)
+ ": Insert tags"))
+ " "
+ (:propertize "C-c C-k" face mode-line-buffer-id)
+ ": Discard Draft")
+ "Header-line used on `sx-compose-mode' drafts.")
+
+(defvar sx-compose--is-question-p nil
+ "Non-nil if this `sx-compose-mode' buffer is a question.")
+(make-variable-buffer-local 'sx-compose--is-question-p)
+
(defvar sx-compose--site nil
"Site which the curent compose buffer belongs to.")
(make-variable-buffer-local 'sx-compose--site)
@@ -95,11 +113,15 @@ just implements some extra features related to posting to the
API.
This mode won't function if `sx-compose--send-function' isn't
-set. To make sure you set it correctly, you can create the buffer
-with the `sx-compose-create' function.
+set. To make sure you set it correctly, you can create the
+buffer with the `sx-compose-create' function.
+
+If creating a question draft, the `sx-compose--is-question-p'
+variable should also be set to enable more functionality.
\\<sx-compose-mode>
\\{sx-compose-mode}"
+ (setq header-line-format sx-compose--header-line)
(add-hook 'sx-compose-after-send-functions
#'sx-compose-quit nil t)
(add-hook 'sx-compose-after-send-functions
@@ -107,6 +129,9 @@ with the `sx-compose-create' function.
(define-key sx-compose-mode-map "\C-c\C-c" #'sx-compose-send)
(define-key sx-compose-mode-map "\C-c\C-k" #'sx-compose-quit)
+(sx--define-conditional-key
+ sx-compose-mode-map "\C-c\C-q" #'sx-compose-insert-tags
+ sx-compose--is-question-p)
(defun sx-compose-send ()
"Finish composing current buffer and send it.
@@ -120,6 +145,21 @@ contents to the API, then calls `sx-compose-after-send-functions'."
(run-hook-with-args 'sx-compose-after-send-functions
(current-buffer) result)))))
+(defun sx-compose-insert-tags ()
+ "Prompt for a tag list for this draft and insert them."
+ (interactive)
+ (save-excursion
+ (let* ((old (sx-compose--goto-tag-header))
+ (new
+ (save-match-data
+ (mapconcat
+ #'identity
+ (sx-tag-multiple-read sx-compose--site "Tags" old)
+ " "))))
+ (if (match-string 1)
+ (replace-match new :fixedcase nil nil 1)
+ (insert new)))))
+
;;; Functions for use in hooks
(defun sx-compose-quit (buffer _)
@@ -128,7 +168,7 @@ contents to the API, then calls `sx-compose-after-send-functions'."
(when (buffer-live-p buffer)
(let ((w (get-buffer-window buffer)))
(when (window-live-p w)
- (delete-window w)))
+ (ignore-errors (delete-window w))))
(kill-buffer buffer)))
(defun sx-compose--copy-as-kill (buffer _)
@@ -137,24 +177,30 @@ contents to the API, then calls `sx-compose-after-send-functions'."
(with-current-buffer buffer
(kill-new (buffer-string)))))
+(defun sx-compose--goto-tag-header ()
+ "Move to the \"Tags:\" header.
+Match data is set so group 1 encompasses any already inserted
+tags. Return a list of already inserted tags."
+ (goto-char (point-min))
+ (unless (search-forward-regexp
+ (rx bol "Tags : " (group-n 1 (* not-newline)) eol)
+ (next-single-property-change (point-min) 'sx-compose-separator)
+ 'noerror)
+ (error "No Tags header found"))
+ (save-match-data
+ (split-string (match-string 1) (rx (any space ",;"))
+ 'omit-nulls (rx space))))
+
(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)))
+ sx-compose--site (sx-compose--goto-tag-header))))
(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)
+ (y-or-n-p (format "Following tags don't exist. Create them? %s " invalid-tags))
t))))
@@ -180,6 +226,7 @@ respectively added locally to `sx-compose-before-send-hook' and
(with-current-buffer (sx-compose--get-buffer-create site parent)
(sx-compose-mode)
(setq sx-compose--site site)
+ (setq sx-compose--is-question-p is-question)
(setq sx-compose--send-function
(if (consp parent)
(sx-assoc-let parent
@@ -302,3 +349,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 795f175..d8ad2ba 100644
--- a/sx-encoding.el
+++ b/sx-encoding.el
@@ -1,4 +1,4 @@
-;;; sx-encoding.el --- encoding -*- lexical-binding: t; -*-
+;;; 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 e86e521..444df29 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,6 +19,9 @@
;;; Commentary:
+;; This file provides logic for retrieving and managing a user's
+;; starred questions.
+
;;; Code:
(require 'sx-method)
@@ -27,7 +30,7 @@
(require 'sx-networks)
(require 'sx-filter)
-(defvar sx-favorite-list-filter
+(defconst sx-favorite-list-filter
(sx-filter-from-nil
(question.question_id)))
@@ -42,8 +45,10 @@ Added as hook to initialization."
(or (setq sx-favorites--user-favorite-list
(sx-cache-get 'question-favorites))
(sx-favorites-update)))
-;; Append to ensure `sx-network--initialize is run before it.
-(add-hook 'sx-init--internal-hook #'sx-favorites--initialize 'append)
+;; ;; Append to ensure `sx-network--initialize' is run before it.
+;; This is removed for now because it performs a lot of API calls and
+;; was never used.
+;; (add-hook 'sx-init--internal-hook #'sx-favorites--initialize 'append)
(defun sx-favorites--retrieve-favorites (site)
"Obtain list of starred QUESTION_IDs for SITE."
diff --git a/sx-filter.el b/sx-filter.el
index 15bd8a1..1ccf611 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:
@@ -43,7 +47,7 @@ Structure:
;;; Creation
(defmacro sx-filter-from-nil (included)
- "Creates a filter data structure with INCLUDED fields.
+ "Create a filter data structure with INCLUDED fields.
All wrapper fields are included by default."
`(quote
((,@(sx--tree-expand
@@ -60,23 +64,21 @@ All wrapper fields are included by default."
.page_size
.quota_max
.quota_remaining
- .total)
- nil none)))
+ )
+ nil nil)))
;;; @TODO allow BASE to be a precompiled filter name
(defun sx-filter-compile (&optional include exclude base)
"Compile INCLUDE and EXCLUDE into a filter derived from BASE.
-INCLUDE and EXCLUDE must both be lists; BASE should be a string.
+INCLUDE and EXCLUDE must both be lists; BASE should be a symbol.
Returns the compiled filter as a string."
(let ((keyword-arguments
`((include . ,(if include (sx--thing-as-string include)))
(exclude . ,(if exclude (sx--thing-as-string exclude)))
(base . ,(if base base)))))
- (let ((response (elt (sx-request-make
- "filter/create"
- keyword-arguments) 0)))
- (sx-assoc-let response
+ (let ((result (elt (sx-request-make "filter/create" keyword-arguments) 0)))
+ (sx-assoc-let result
.filter))))
@@ -89,7 +91,7 @@ Returns the compiled filter as a string."
(defun sx-filter-get (&optional include exclude base)
"Return the string representation of the given filter.
-If the filter data exist in `sx--filter-alist', that value will
+If the filter data exists in `sx--filter-alist', that value will
be returned. Otherwise, compile INCLUDE, EXCLUDE, and BASE into
a filter with `sx-filter-compile' and push the association onto
`sx--filter-alist'. Re-cache the alist with `sx-cache-set' and
@@ -103,12 +105,18 @@ return the compiled filter."
;;; Browsing filter
-(defvar sx-browse-filter
+(defconst sx-browse-filter
(sx-filter-from-nil
((question body_markdown
bounty_amount
comments
+ creation_date
answers
+ answer_count
+ score
+ title
+ owner
+ tags
last_editor
last_activity_date
accepted_answer_id
@@ -118,8 +126,12 @@ return the compiled filter."
question_id
share_link)
(user display_name
+ link
+ accept_rate
reputation)
(shallow_user display_name
+ link
+ accept_rate
reputation)
(comment owner
body_markdown
@@ -133,10 +145,12 @@ return the compiled filter."
post_id
comment_id)
(answer answer_id
+ creation_date
last_editor
last_activity_date
link
share_link
+ score
owner
body_markdown
upvoted
diff --git a/sx-inbox.el b/sx-inbox.el
index d0be379..21589fb 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
@@ -69,6 +69,7 @@ KEYWORDS are added to the method call along with PAGE.
`sx-method-call' is used with `sx-inbox-filter'."
(sx-method-call (if notifications 'notifications 'inbox)
:keywords keywords
+ :page page
:filter sx-inbox-filter))
@@ -91,7 +92,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 +107,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 dc4398e..97c68b6 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
@@ -272,7 +272,7 @@ TEXT is a string. Interactively, it is read from the minibufer."
(setq text (read-string
"Comment text: "
(when .comment_id
- (concat (sx--user-@name .owner) " "))))
+ (substring-no-properties (sx-user--format "%@ " .owner)))))
(while (not (sx--comment-valid-p text 'silent))
(setq text (read-string "Comment text (between 16 and 600 characters): " text))))
;; If non-interactive, `text' could be anything.
@@ -291,10 +291,8 @@ TEXT is a string. Interactively, it is read from the minibufer."
;; The api returns the new DATA.
(when (> (length result) 0)
(sx--add-comment-to-object
- (elt result 0)
- (if .post_id
- (sx--get-post .post_type .site_par .post_id)
- data))
+ (sx--ensure-owner-in-object (list (cons 'display_name "(You)")) (elt result 0))
+ (if .post_id (sx--get-post .post_type .site_par .post_id) data))
;; Display the changes in `data'.
(sx--maybe-update-display)))))
@@ -344,7 +342,15 @@ OBJECT can be a question or an answer."
(list comment)))))
;; No previous comments, add it manually.
(setcdr object (cons (car object) (cdr object)))
- (setcar object `(comments . [,comment])))))
+ (setcar object `(comments . [,comment]))))
+ object)
+
+(defun sx--ensure-owner-in-object (owner object)
+ "Add `owner' property with value OWNER to OBJECT."
+ (unless (cdr-safe (assq 'owner object))
+ (setcdr object (cons (car object) (cdr object)))
+ (setcar object `(owner . ,owner)))
+ object)
;;; Editing
@@ -439,7 +445,12 @@ context at point. "
(append (cdr cell) (list answer))))
;; No previous comments, add it manually.
(setcdr question (cons (car question) (cdr question)))
- (setcar question `(answers . [,answer])))))
+ (setcar question `(answers . [,answer])))
+ question))
(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 f1ec7c3..003f965 100644
--- a/sx-load.el
+++ b/sx-load.el
@@ -1,4 +1,4 @@
-;;; sx-load.el --- Load all files of the sx package. -*- lexical-binding: t; -*-
+;;; sx-load.el --- load all files of the SX package -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Artur Malabarba
diff --git a/sx-method.el b/sx-method.el
index bff6d30..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
@@ -144,3 +144,7 @@ Return the entire response as a complex alist."
(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 58ebff5..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,6 +19,9 @@
;;; Commentary:
+;; This file provides logic for retrieving information about the user
+;; across the entire network, e.g. their registered sites.
+
;;; Code:
(require 'sx-method)
@@ -26,7 +29,7 @@
(require 'sx-site)
(require 'sx-filter)
-(defvar sx-network--user-filter
+(defconst sx-network--user-filter
(sx-filter-from-nil
((badge_count bronze
silver
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 3354052..92b4c07 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)
@@ -109,16 +111,6 @@
""
: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
@@ -136,8 +128,9 @@ change `tabulated-list-format' accordingly.")
This is the default printer used by `sx-question-list'. It
assumes QUESTION-DATA is an alist containing (at least) the
elements:
- `site', `score', `upvoted', `answer_count', `title',
- `last_activity_date', `tags', `uestion_id'.
+ `question_id', `site_par', `score', `upvoted', `answer_count',
+ `title', `bounty_amount', `bounty_amount', `bounty_amount',
+ `last_activity_date', `tags', `owner'.
Also see `sx-question-list-refresh'."
(sx-assoc-let question-data
@@ -180,11 +173,7 @@ Also see `sx-question-list-refresh'."
(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)))
+ (sx-user--format "%15d %4r" .owner)
(propertize " " 'display "\n")))))))
(defvar sx-question-list--pages-so-far 0
@@ -226,7 +215,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"
@@ -320,11 +309,10 @@ into consideration.
;; Add a setter to protect the value.
:group 'sx-question-list)
-(defun sx-question-list--date-more-recent-p (x y)
- "Non-nil if tabulated-entry X is newer than Y."
- (sx--<
- sx-question-list-date-sort-method
- (car x) (car y) #'>))
+(sx--create-comparator sx-question-list--date-more-recent-p
+ "Non-nil if tabulated-entry A is newer than B."
+ #'> (lambda (x)
+ (cdr (assq sx-question-list-date-sort-method (car x)))))
;;; Keybinds
@@ -606,3 +594,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 b13caf3..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"
@@ -271,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..f9ecfab 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
@@ -26,6 +26,7 @@
(require 'sx)
(require 'sx-question)
(require 'sx-babel)
+(require 'sx-user)
(defgroup sx-question-mode nil
"Customization group for sx-question-mode."
@@ -33,20 +34,15 @@
:tag "SX Question Mode"
:group 'sx)
-(defgroup sx-question-mode-faces nil
- "Customization group for the faces of `sx-question-mode'."
+(defgroup sx-question-mode-faces '((sx-user custom-group))
+ "Customization group for the faces of `sx-question-mode'.
+Some faces of this mode might be defined in the `sx-user' group."
:prefix "sx-question-mode-"
:tag "SX Question Mode Faces"
:group 'sx-question-mode)
;;; Faces and Variables
-(defcustom sx-question-mode-deleted-user
- '((display_name . "(deleted user)"))
- "The structure used to represent a deleted account."
- :type '(alist :options ((display_name string)))
- :group 'sx-question-mode)
-
(defface sx-question-mode-header
'((t :inherit font-lock-variable-name-face))
"Face used on the question headers in the question buffer."
@@ -67,13 +63,9 @@
:type 'string
:group 'sx-question-mode)
-(defface sx-question-mode-author
- '((t :inherit font-lock-string-face))
- "Face used on the question author in the question buffer."
- :group 'sx-question-mode-faces)
-
-(defcustom sx-question-mode-header-author "\nAuthor: "
- "String used before the question author at the header."
+(defcustom sx-question-mode-header-author-format "\nAuthor: %d %r"
+ "String used to display the question author at the header.
+% constructs have special meaning here. See `sx-user--format'."
:type 'string
:group 'sx-question-mode)
@@ -92,11 +84,6 @@
"Face used on the question tags in the question buffer."
:group 'sx-question-mode-faces)
-(defface sx-question-mode-author
- '((t :inherit font-lock-variable-name-face))
- "Face used for author names in the question buffer."
- :group 'sx-question-mode-faces)
-
(defface sx-question-mode-score
'((t))
"Face used for the score in the question buffer."
@@ -166,6 +153,15 @@ replaced with the comment."
:type 'boolean
:group 'sx-question-mode)
+(defcustom sx-question-mode-answer-sort-function
+ #'sx-answer-higher-score-p
+ "Function used to sort answers in the question buffer."
+ :type '(choice
+ (const :tag "Higher-scoring first" sx-answer-higher-score-p)
+ (const :tag "Newer first" sx-answer-newer-p)
+ (const :tag "More active first" sx-answer-more-active-p))
+ :group 'sx-question-mode)
+
;;; Functions
;;;; Printing the general structure
@@ -179,7 +175,8 @@ QUESTION must be a data structure returned by `json-read'."
;; Print everything
(sx-question-mode--print-section question)
(sx-assoc-let question
- (mapc #'sx-question-mode--print-section .answers))
+ (mapc #'sx-question-mode--print-section
+ (cl-sort .answers sx-question-mode-answer-sort-function)))
(insert "\n\n ")
(insert-text-button "Write an Answer" :type 'sx-button-answer)
;; Go up
@@ -204,11 +201,13 @@ DATA can represent a question or an answer."
;; Sections can be hidden with overlays
(sx--wrap-in-overlay
'(sx-question-mode--section-content t)
+ ;; Author
+ (insert
+ (sx-user--format
+ (propertize sx-question-mode-header-author-format
+ 'face 'sx-question-mode-header)
+ .owner))
(sx-question-mode--insert-header
- ;; Author
- sx-question-mode-header-author
- (sx-question-mode--propertize-display-name .owner)
- 'sx-question-mode-author
;; Date
sx-question-mode-header-date
(concat
@@ -216,8 +215,7 @@ DATA can represent a question or an answer."
(when .last_edit_date
(format sx-question-mode-last-edit-format
(sx-time-since .last_edit_date)
- (sx-question-mode--propertize-display-name
- (or .last_editor sx-question-mode-deleted-user)))))
+ (sx-user--format "%d" .last_editor))))
'sx-question-mode-date)
(sx-question-mode--insert-header
sx-question-mode-header-score
@@ -273,12 +271,6 @@ DATA can represent a question or an answer."
:type 'sx-button-comment)
(insert "\n")))))
-(defun sx-question-mode--propertize-display-name (author)
- "Return display_name of AUTHOR with `sx-question-mode-author' face."
- (sx-assoc-let author
- (propertize .display_name
- 'face 'sx-question-mode-author)))
-
(defun sx-question-mode--print-comment (comment-data)
"Print the comment described by alist COMMENT-DATA.
The comment is indented, filled, and then printed according to
@@ -291,9 +283,8 @@ The comment is indented, filled, and then printed according to
(if (eq .upvoted t) "^" "")
" "))
(insert
- (format
- sx-question-mode-comments-format
- (sx-question-mode--propertize-display-name .owner)
+ (format sx-question-mode-comments-format
+ (sx-user--format "%d" .owner)
(substring
;; We fill with three spaces at the start, so the comment is
;; slightly indented.
@@ -322,18 +313,22 @@ where `value' is given `face' as its face.
'face 'markdown-list-face)
"String to be displayed as the bullet of markdown list items.")
-(defvar sx-question-mode--reference-regexp
+(defconst sx-question-mode--reference-regexp
(rx line-start (0+ blank) "[%s]:" (0+ blank)
(group-n 1 (1+ (not blank))))
"Regexp used to find the url of labeled links.
E.g.:
[1]: https://...")
-(defvar sx-question-mode--link-regexp
+(defconst sx-question-mode--link-regexp
;; Done at compile time.
- (rx "[" (group-n 1 (1+ (not (any "]")))) "]"
- (or (and "(" (group-n 2 (1+ (not (any ")")))) ")")
- (and "[" (group-n 3 (1+ (not (any "]")))) "]")))
+ (rx (or (and "[" (group-n 1 (1+ (not (any "]")))) "]"
+ (or (and "(" (group-n 2 (1+ (not (any ")")))) ")")
+ (and "[" (group-n 3 (1+ (not (any "]")))) "]")))
+ (group-n 4 (and (and "http" (opt "s") "://") ""
+ (>= 2 (any lower numeric "_%"))
+ "."
+ (>= 2 (any lower numeric "/._%&#?=;"))))))
"Regexp matching markdown links.")
(defun sx-question-mode--fill-and-fontify (text)
@@ -377,6 +372,7 @@ E.g.:
(while (search-forward-regexp sx-question-mode--link-regexp nil t)
(let* ((text (match-string-no-properties 1))
(url (or (match-string-no-properties 2)
+ (match-string-no-properties 4)
(sx-question-mode-find-reference
(match-string-no-properties 3)
text)))
@@ -384,7 +380,7 @@ E.g.:
(when (stringp url)
(replace-match "")
(sx-question-mode--insert-link
- (if sx-question-mode-pretty-links text full-text)
+ (or (if sx-question-mode-pretty-links text full-text) url)
url))))))
(defun sx-question-mode--insert-link (text url)
@@ -462,9 +458,15 @@ font-locking."
(defun sx-question-mode--skip-references ()
"If there's a reference ahead, skip it and return non-nil."
- (while (looking-at-p (format sx-question-mode--reference-regexp ".+"))
+ (forward-line 0)
+ (when (looking-at-p (format sx-question-mode--reference-regexp ".+"))
;; Returns non-nil
- (forward-line 1)))
+ (forward-paragraph 1)
+ t))
(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 0e830a6..1e3a02c 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:
+;; This file provides an API for retrieving questions and defines
+;; additional logic for marking questions as read or hidden.
+
;;; Code:
@@ -184,6 +187,21 @@ If no cache exists for it, initialize one with SITE."
"Formats TAG for display."
(concat "[" tag "]"))
+
+;;; Question Mode Answer-Sorting Functions
+
+(sx--create-comparator sx-answer-higher-score-p
+ "Return t if answer A has a higher score than answer B."
+ #'> (lambda (x) (cdr (assq 'score x))))
+
+(sx--create-comparator sx-answer-newer-p
+ "Return t if answer A was posted later than answer B."
+ #'> (lambda (x) (cdr (assq 'creation_date x))))
+
+(sx--create-comparator sx-answer-more-active-p
+ "Return t if answer A was updated after answer B."
+ #'> (lambda (x) (cdr (assq 'last_activity_date x))))
+
(provide 'sx-question)
;;; sx-question.el ends here
diff --git a/sx-request.el b/sx-request.el
index bab53ec..2650c55 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
@@ -131,6 +131,8 @@ access the response wrapper."
(vconcat return-value
(cdr (assoc 'items response)))))
+;;; NOTE: Whenever this is arglist changes, `sx-request-fallback' must
+;;; also change.
(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
@@ -189,6 +191,7 @@ the main content of the response is returned."
;; RESPONSE to 'corrupt or something
(response (with-demoted-errors "`json' error: %S"
(json-read-from-string data))))
+ (kill-buffer response-buffer)
(when (and (not response) (string-equal data "{}"))
(sx-message "Unable to parse response: %S" response)
(error "Response could not be read by `json-read-from-string'"))
@@ -204,7 +207,7 @@ the main content of the response is returned."
(funcall (or process-function #'sx-request-response-get-items)
response)))))))
-(defun sx-request-fallback (_method &optional _args _request-method)
+(defun sx-request-fallback (_method &optional _args _request-method _process-function)
"Fallback method when authentication is not available.
This is for UI generation when the associated API call would
require authentication.
@@ -213,6 +216,35 @@ Currently returns nil."
'(()))
+;;; Our own generated data
+(defconst sx-request--data-url-format
+ "https://raw.githubusercontent.com/vermiculus/sx.el/data/data/%s.el"
+ "Url of the \"data\" directory inside the SX `data' branch.")
+
+(defun sx-request-get-data (file)
+ "Fetch and return data stored online by SX.
+FILE is a string or symbol, the name of the file which holds the
+desired data, relative to `sx-request--data-url-format'. For
+instance, `tags/emacs' returns the list of tags on Emacs.SE."
+ (let* ((url-automatic-caching t)
+ (url-inhibit-uncompression t)
+ (request-url (format sx-request--data-url-format file))
+ (url-request-method "GET")
+ (url-request-extra-headers
+ '(("Content-Type" . "application/x-www-form-urlencoded")))
+ (response-buffer (url-retrieve-synchronously request-url)))
+ (if (not response-buffer)
+ (error "Something went wrong in `url-retrieve-synchronously'")
+ (with-current-buffer response-buffer
+ (progn
+ (goto-char (point-min))
+ (if (not (search-forward "\n\n" nil t))
+ (error "Headers missing; response corrupt")
+ (when (looking-at-p "Not Found") (error "Page not found."))
+ (prog1 (read (current-buffer))
+ (kill-buffer (current-buffer)))))))))
+
+
;;; Support Functions
(defun sx-request--build-keyword-arguments (alist &optional kv-sep)
"Format ALIST as a key-value list joined with KV-SEP.
@@ -256,3 +288,7 @@ false, use the symbol `false'. Each element is processed with
(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..aefd12e 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
@@ -19,7 +19,7 @@
;;; Commentary:
-;; Implements sarch functionality. The basic function is
+;; Implements search functionality. The basic function is
;; `sx-search-get-questions', which returns an array of questions
;; according to a search term.
;;
@@ -32,13 +32,11 @@
(require 'sx)
(require 'sx-question-list)
+(require 'sx-tag)
(defvar sx-search--query-history nil
"Query history for interactive prompts.")
-(defvar sx-search--tag-history nil
- "Tags history for interactive prompts.")
-
;;; Basic function
(defun sx-search-get-questions (site page query &optional tags excluded-tags keywords)
@@ -84,15 +82,12 @@ prefix argument, the user is asked for everything."
(when (string= query "")
(setq query nil))
(when current-prefix-arg
- (setq tags (sx--multiple-read
- (format "Tags (%s)"
- (if query "optional" "mandatory"))
- 'sx-search--tag-history))
+ (setq tags (sx-tag-multiple-read
+ site (concat "Tags" (when query " (optional)"))))
(when (and (not query) (string= "" tags))
(sx-user-error "Must supply either QUERY or TAGS"))
(setq excluded-tags
- (sx--multiple-read
- "Excluded tags (optional)" 'sx-search--tag-history)))
+ (sx-tag-multiple-read site "Excluded tags (optional)")))
(list site query tags excluded-tags)))
;; Here starts the actual function
@@ -110,3 +105,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 2f0a31d..4dac8e6 100644
--- a/sx-site.el
+++ b/sx-site.el
@@ -19,17 +19,21 @@
;;; 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
+(defconst sx-site-browse-filter
(sx-filter-from-nil
((site site_type
name
api_site_parameter
+ site_url
related_sites)
(related_site api_site_parameter
relation)))
diff --git a/sx-switchto.el b/sx-switchto.el
index 76804e4..6a195e0 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
@@ -33,7 +33,7 @@
(mapc (lambda (x) (define-key sx-switchto-map (car x) (cadr x)))
'(
- ;; These immitate the site's G hotkey.
+ ;; These imitate the site's G hotkey.
("a" sx-ask)
("h" sx-tab-frontpage)
("m" sx-tab-meta-or-main)
@@ -54,18 +54,6 @@
;;; These are keys which depend on context.
;;;; For instance, it makes no sense to have `switch-site' bound to a
;;;; key on a buffer with no `sx-question-list--site' variable.
-(defmacro sx--define-conditional-key (keymap key def &rest body)
- "In KEYMAP, define key sequence KEY as DEF conditionally.
-This is like `define-key', except the definition \"disapears\"
-whenever BODY evaluates to nil."
- (declare (indent 3)
- (debug (form form form &rest sexp)))
- `(define-key ,keymap ,key
- '(menu-item
- ,(format "maybe-%s" (or (car (cdr-safe def)) def)) ignore
- :filter (lambda (&optional _)
- (when (progn ,@body) ,def)))))
-
(sx--define-conditional-key sx-switchto-map "s" #'sx-question-list-switch-site
(and (boundp 'sx-question-list--site) sx-question-list--site))
diff --git a/sx-tab.el b/sx-tab.el
index f97119a..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)
@@ -210,7 +223,7 @@ If SITE is nil, use `sx-default-site'."
(sx-question-get-questions
sx-question-list--site page nil 'unanswered/my-tags)))
;;;###autoload
-(autoload 'sx-tab-unanswered
+(autoload 'sx-tab-unanswered-my-tags
(expand-file-name
"sx-tab"
(when load-file-name
@@ -266,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
index 8c468a6..b2ad375 100644
--- a/sx-tag.el
+++ b/sx-tag.el
@@ -1,4 +1,4 @@
-;;; sx-tag.el --- Retrieving list of tags and handling tags. -*- lexical-binding: t; -*-
+;;; 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
@@ -26,7 +26,7 @@
;;; Getting the list from a site
-(defvar sx-tag-filter
+(defconst sx-tag-filter
(sx-filter-from-nil
(tag.name
tag.synonyms))
@@ -63,6 +63,23 @@ Returns a list."
(sx-tag--get-some-tags-containing site string)))
+;;; Getting tags from our data branch. Without the API.
+;;;; @TODO: Once the cache is finished, this can probably be made into
+;;;; a cache variasble with 1 day expiration time.
+(defvar sx-tag-list-alist nil
+ "Alist where the tag list for each site is stored.
+Elements are of the type (SITE . TAG-LIST).")
+
+(defun sx-tag-list--get (site)
+ "Retrieve all tags from SITE in a single request.
+This does not access the API. Instead, it uses
+`sx-request-get-data', which accesses SX's tag cache."
+ (or (cdr (assoc site sx-tag-list-alist))
+ (let ((list (sx-request-get-data (concat "tags/" site))))
+ (push (cons site list) sx-tag-list-alist)
+ list)))
+
+
;;; Check tag validity
(defun sx-tag--invalid-name-p (site tags)
"Nil if TAGS exist in SITE.
@@ -82,5 +99,43 @@ Return the list of invalid tags in TAGS."
:site site))))
(cl-remove-if (lambda (x) (member x result)) tags)))
+
+;;; Prompt the user for tags.
+(defvar sx-tag-history nil
+ "Tags history for interactive prompts.")
+
+;;; @TODO: Make it so that hitting BACKSPACE with an empty input
+;;; deletes a previously submitted tag.
+(defun sx-tag-multiple-read (site prompt &optional initial-value)
+ "Interactively read a list of tags for SITE.
+Call `sx-completing-read' multiple times, until input is empty,
+with completion options given by the tag list of SITE.
+Return a list of tags given by the user.
+
+PROMPT is a string displayed to the user and should not end with
+a space nor a colon. INITIAL-VALUE is a list of already-selected
+tags."
+ (let ((completion-list (sx-tag-list--get site))
+ (list (reverse initial-value))
+ (empty-string
+ (propertize "--\x000-some-string-representing-empty-\x000--"
+ 'display "DONE"))
+ input)
+ (while (not (string=
+ empty-string
+ (setq input (sx-completing-read
+ (concat prompt " ["
+ (mapconcat #'identity (reverse list) ",")
+ "]: ")
+ completion-list
+ nil 'require-match nil 'sx-tag-history
+ empty-string))))
+ (push input list))
+ (reverse list)))
+
(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 e05d95a..9fa0037 100644
--- a/sx-time.el
+++ b/sx-time.el
@@ -1,4 +1,4 @@
-;;; sx-time.el --- time -*- lexical-binding: t; -*-
+;;; 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-user.el b/sx-user.el
new file mode 100644
index 0000000..c0f3a78
--- /dev/null
+++ b/sx-user.el
@@ -0,0 +1,203 @@
+;;; sx-user.el --- handling and printing user information -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2014 Artur Malabarba
+
+;; Author: Artur Malabarba <bruce.connor.am@gmail.com>
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+
+;;; Code:
+(require 'sx)
+(require 'sx-button)
+
+(defgroup sx-user nil
+ "How users are displayed by SX."
+ :prefix "sx-user-"
+ :tag "SX User"
+ :group 'sx)
+
+(defcustom sx-question-mode-fallback-user
+ '(
+ (about_me . "")
+ (accept_rate . -1)
+ (account_id . -1)
+ (age . -1)
+ (answer_count . -1)
+ (badge_counts . ((bronze . -1) (silver . -1) (gold . -1)))
+ (creation_date . -1)
+ (display_name . "(unknown user)")
+ (down_vote_count . -1)
+ (is_employee . :json-false)
+ (last_access_date . -1)
+ (last_modified_date . -1)
+ (link . "")
+ (location . "")
+ (profile_image . ":(")
+ (question_count . -1)
+ (reputation . -1)
+ (reputation_change_day . -1)
+ (reputation_change_month . -1)
+ (reputation_change_quarter . -1)
+ (reputation_change_week . -1)
+ (reputation_change_year . -1)
+ (timed_penalty_date . -1)
+ (up_vote_count . -1)
+ (user_id . -1)
+ (user_type . does_not_exist)
+ (view_count . -1)
+ (website_url . "")
+ )
+ "The structure used to represent missing user information.
+NOOTE: SX relies on this variable containing all necessary user
+information. You may edit any of its fields, but you'll run into
+errors if you remove them."
+ :type '(alist :options ((about_me string)
+ (accept_rate integer)
+ (account_id integer)
+ (age integer)
+ (answer_count integer)
+ (badge_counts alist)
+ (creation_date integer)
+ (display_name string)
+ (down_vote_count integer)
+ (is_employee boolean)
+ (last_access_date integer)
+ (last_modified_date integer)
+ (link string)
+ (location string)
+ (profile_image string)
+ (question_count integer)
+ (reputation integer)
+ (reputation_change_day integer)
+ (reputation_change_month integer)
+ (reputation_change_quarter integer)
+ (reputation_change_week integer)
+ (reputation_change_year integer)
+ (timed_penalty_date integer)
+ (up_vote_count integer)
+ (user_id integer)
+ (user_type symbol)
+ (view_count integer)
+ (website_url string)))
+ :group 'sx-user)
+
+
+;;; Text properties
+(defface sx-user-name
+ '((t :inherit font-lock-builtin-face))
+ "Face used for user names."
+ :group 'sx-user)
+
+(defface sx-user-reputation
+ '((t :inherit font-lock-comment-face))
+ "Face used for user reputations."
+ :group 'sx-user)
+
+(defface sx-user-accept-rate
+ '((t))
+ "Face used for user accept-rates."
+ :group 'sx-user)
+
+(defvar sx-user--format-property-alist
+ `((?d button ,(list t) category ,(button-category-symbol 'sx-button-user))
+ (?n button ,(list t) category ,(button-category-symbol 'sx-button-user))
+ (?@ button ,(list t) category ,(button-category-symbol 'sx-button-user))
+ (?r face sx-user-reputation)
+ (?a face sx-user-accept-rate))
+ "Alist relating % constructs with text properties.
+See `sx-user--format'.")
+
+
+;;; Formatting function
+(defun sx-user--format (format-string user)
+ "Use FORMAT-STRING to format the user object USER.
+The value is a copy of FORMAT-STRING, but with certain constructs
+replaced by text that describes the specified USER:
+
+%d is the display name.
+%@ is the display name in a format suitable for @mentions.
+%l is the link to the profile.
+%r is the reputation.
+%a is the accept rate.
+
+The string replaced in each of these construct is also given the
+text-properties specified in `sx-user--format-property-alist'.
+Specially, %d and %@ are turned into buttons with the
+`sx-button-user' category."
+ (sx-assoc-let (append user sx-question-mode-fallback-user)
+ (let* ((text (sx-format-replacements
+ format-string
+ `((?d . ,\.display_name)
+ (?n . ,\.display_name)
+ (?l . ,\.link)
+ (?r . ,\.reputation)
+ (?a . ,\.accept_rate)
+ (?@ . ,(when (string-match "%@" format-string)
+ (sx-user--@name .display_name)))
+ )
+ sx-user--format-property-alist)))
+ (if (< 0 (string-width .link))
+ (propertize text
+ ;; For visiting and stuff.
+ 'sx-button-url .link
+ 'sx-button-copy .link)
+ text))))
+
+
+;;; @name conversion
+(defconst sx-user--ascii-replacement-list
+ '(("[:space:]" . "")
+ ("àåáâäãåą" . "a")
+ ("èéêëę" . "e")
+ ("ìíîïı" . "i")
+ ("òóôõöøőð" . "o")
+ ("ùúûüŭů" . "u")
+ ("çćčĉ" . "c")
+ ("żźž" . "z")
+ ("śşšŝ" . "s")
+ ("ñń" . "n")
+ ("ýÿ" . "y")
+ ("ğĝ" . "g")
+ ("ř" . "r")
+ ("ł" . "l")
+ ("đ" . "d")
+ ("ß" . "ss")
+ ("Þ" . "th")
+ ("ĥ" . "h")
+ ("ĵ" . "j")
+ ("^[:ascii:]" . ""))
+ "List of replacements to use for non-ascii characters.
+Used to convert user names into @mentions.")
+
+(defun sx-user--@name (display-name)
+ "Convert DISPLAY-NAME into an @mention.
+In order to correctly @mention the user, all whitespace is
+removed from DISPLAY-NAME and a series of unicode conversions are
+performed before it is returned.
+See `sx-user--ascii-replacement-list'.
+
+If all you need is the @name, this is very slightly faster than
+using `sx-user--format', but it doesn't do any sanity checking."
+ (concat "@" (sx--recursive-replace
+ sx-user--ascii-replacement-list display-name)))
+
+(provide 'sx-user)
+;;; sx-user.el ends here
+
+;; Local Variables:
+;; indent-tabs-mode: nil
+;; End:
diff --git a/sx.el b/sx.el
index f77b313..a385a84 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
@@ -90,7 +90,7 @@ with a `link' property)."
(defun sx--link-to-data (link)
"Convert string LINK into data that can be displayed."
- (let ((result (list (cons 'site (sx--site link)))))
+ (let ((result (list (cons 'site_par (sx--site link)))))
;; Try to strip a question or answer ID
(when (or
;; Answer
@@ -176,24 +176,6 @@ All ARGS are passed to `completing-read' or `ido-completing-read'."
(apply (if ido-mode #'ido-completing-read #'completing-read)
args))
-(defun sx--multiple-read (prompt hist-var)
- "Interactively query the user for a list of strings.
-Call `read-string' multiple times, until the input is empty.
-
-PROMPT is a string displayed to the user and should not end with
-a space nor a colon. HIST-VAR is a quoted symbol, indicating a
-list in which to store input history."
- (let (list input)
- (while (not (string=
- ""
- (setq input (read-string
- (concat prompt " ["
- (mapconcat #'identity list ",")
- "]: ")
- "" hist-var))))
- (push input list))
- list))
-
(defmacro sx-sorted-insert-skip-first (newelt list &optional predicate)
"Inserted NEWELT into LIST sorted by PREDICATE.
This is designed for the (site id id ...) lists. So the first car
@@ -265,6 +247,32 @@ Anything before the (sub)domain is removed."
(rx string-start (or (and (0+ word) (optional ":") "//")))
"" url)))
+(defmacro sx--define-conditional-key (keymap key def &rest body)
+ "In KEYMAP, define key sequence KEY as DEF conditionally.
+This is like `define-key', except the definition \"disappears\"
+whenever BODY evaluates to nil."
+ (declare (indent 3)
+ (debug (form form form &rest sexp)))
+ `(define-key ,keymap ,key
+ '(menu-item
+ ,(format "maybe-%s" (or (car (cdr-safe def)) def)) ignore
+ :filter (lambda (&optional _)
+ (when (progn ,@body) ,def)))))
+
+(defmacro sx--create-comparator (name doc compare-func get-func)
+ "Define a new comparator called NAME with documentation DOC.
+COMPARE-FUNC is a function that takes the return value of
+GET-FUNC and performs the actual comparison."
+ (declare (indent 1) (doc-string 2))
+ `(progn
+ ;; In using `defalias', the macro supports both function
+ ;; symbols and lambda expressions.
+ (defun ,name (a b)
+ ,doc
+ (funcall ,compare-func
+ (funcall ,get-func a)
+ (funcall ,get-func b)))))
+
;;; Printing request data
(defvar sx--overlays nil
@@ -300,39 +308,6 @@ Return the result of BODY."
(push ov sx--overlays))
result))
-(defvar sx--ascii-replacement-list
- '(("[:space:]" . "")
- ("àåáâäãåą" . "a")
- ("èéêëę" . "e")
- ("ìíîïı" . "i")
- ("òóôõöøőð" . "o")
- ("ùúûüŭů" . "u")
- ("çćčĉ" . "c")
- ("żźž" . "z")
- ("śşšŝ" . "s")
- ("ñń" . "n")
- ("ýÿ" . "y")
- ("ğĝ" . "g")
- ("ř" . "r")
- ("ł" . "l")
- ("đ" . "d")
- ("ß" . "ss")
- ("Þ" . "th")
- ("ĥ" . "h")
- ("ĵ" . "j")
- ("^[:ascii:]" . ""))
- "List of replacements to use for non-ascii characters.
-Used to convert user names into @mentions.")
-
-(defun sx--user-@name (user)
- "Get the `display_name' of USER prepended with @.
-In order to correctly @mention the user, all whitespace is
-removed from the display name before it is returned."
- (sx-assoc-let user
- (when (stringp .display_name)
- (concat "@" (sx--recursive-replace
- sx--ascii-replacement-list .display_name)))))
-
(defun sx--recursive-replace (alist string)
"Replace each car of ALIST with its cdr in STRING."
(if alist
@@ -343,6 +318,44 @@ removed from the display name before it is returned."
(format "[%s]" (car kar)) (cdr kar) string)))
string))
+(defun sx-format-replacements (format alist &optional property-alist)
+ "Use FORMAT-STRING to format the values in ALIST.
+ALIST is a list with elements of the form (CHAR . STRING).
+The value is a copy of FORMAT-STRING, but with certain constructs
+replaced by text as given by ALIST.
+
+The construct is a `%' character followed by any other character.
+The replacement is the STRING corresponding to CHAR in ALIST. In
+addition, if CHAR is also the car of an element in
+PROPERTY-ALIST, the cdr of that element should be a list of text
+properties which will be applied on the replacement.
+
+The %% construct is special, it is replaced with a single %, even
+if ALIST contains a different string at the ?% entry."
+ (let ((alist (cons '(?% . "%") alist)))
+ (with-temp-buffer
+ (insert format)
+ (goto-char (point-min))
+ (while (search-forward-regexp
+ (rx "%" (group-n 1 (* (any "-+ #0-9.")))) nil 'noerror)
+ (let* ((char (char-after))
+ ;; Understand flags
+ (flag (match-string 1))
+ (val (cdr-safe (assq char alist))))
+ (unless val
+ (error "Invalid format character: `%%%c'" char))
+ ;; Insert first, to preserve text properties.
+ (insert-and-inherit (format (concat "%" flag "s") val))
+ (when property-alist
+ (add-text-properties (match-end 0) (point)
+ (cdr-safe (assq char property-alist))))
+ ;; Delete the specifier body.
+ (delete-region (match-beginning 0)
+ (match-end 0))
+ ;; Delete `char-after'.
+ (delete-char 1)))
+ (buffer-string))))
+
(defcustom sx-init-hook nil
"Hook run when SX initializes.
@@ -355,13 +368,6 @@ Run after `sx-init--internal-hook'."
This is used internally to set initial values for variables such
as filters.")
-(defun sx--< (property x y &optional predicate)
- "Non-nil if PROPERTY attribute of alist X is less than that of Y.
-With optional argument PREDICATE, use it instead of `<'."
- (funcall (or predicate #'<)
- (cdr (assoc property x))
- (cdr (assoc property y))))
-
(defmacro sx-init-variable (variable value &optional setter)
"Set VARIABLE to VALUE using SETTER.
SETTER should be a function of two arguments. If SETTER is nil,
diff --git a/sx.org b/sx.org
index 7ccb51b..e206cc2 100644
--- a/sx.org
+++ b/sx.org
@@ -97,7 +97,7 @@ Scrolling past the bottom of the list fetches more questions.
- ~sx-init-hook~ :: Run when ~sx-initialize~ is called.
- ~sx-compose-before-send-hook~ :: Run before POSTing to the API from
a buffer in ~sx-compose-mode~. If any of the functions in this
- hook, return nil, the transaction is cancelled.
+ hook, return nil, the transaction is canceled.
- ~sx-compose-after-send-functions~ :: Run after POSTing to the API
from a buffer in ~sx-compose-mode~, if the transaction was
successful.
@@ -156,44 +156,45 @@ has a descriptive header explaining its purpose. Still, to help you
find your way around, we describe below the current project
structure. This list is very loosely ordered form low to high-level.
-- ~sx.el~ - Utility functions used throughout the package. Essentially
- every file indirectly requires this one. If you're adding a function
- that's used by different parts of the package, add it to this file.
-- ~sx-time.el~ - Similar to ~sx.el~, but only contains a few
- time-related functions.
-- ~sx-filter.el~ - Handles retrieval of filters.
-- ~sx-cache.el~ - Saves and restores persistent data between sessions.
-- ~sx-button.el~ - Defines all button types used throughout the
- package. Currently used only by ~sx-question-print.el~.
-
-- ~sx-request.el~ - Requests and url manipulation. Backend used by
- ~sx-method.el~. It shouldn't be necessary to use the functions in
- this file outside ~sx-method.el~.
-- ~sx-method.el~ - Main interface for API method calls.
-
-- ~sx-favorites.el~ - Starred questions.
-- ~sx-networks.el~ - User network information.
-- ~sx-site.el~ - Browsing sites.
-- ~sx-auth.el~ - Handles user authentication.
-
-- ~sx-question.el~ - Base question logic. Holds several functions for
- retrieving questions and for processing retrieved questions. Doesn't
- do any sort of user interface, that is left for
- ~sx-question-list.el~ and ~sx-question-mode.el~.
-- ~sx-question-list.el~ - Major-mode for navigating questions list.
-- ~sx-question-mode.el~ - User interface for displaying a
- question. Creates the buffer and defines the major-mode.
-- ~sx-question-print.el~ - Populating the question buffer with
- content. Used by ~sx-question-mode.el~ to actually print the content
- of a question.
-- ~sx-babel.el~ - Font-locking code blocks printed by
- ~sx-question-print.el~ according to the language.
-
-- ~sx-compose.el~ - Major-mode for composing questions and answers.
-- ~sx-interaction.el~ - Voting, commenting, and otherwise interacting with questions.
-- ~sx-tab.el~ - Functions for viewing different tabs.
-
-- ~sx-load.el~ - Load all files of the sx package. Designed as an easy way in for users who install the package manually (since they don't have autoloads).
+- ~sx.el~ :: Utility functions used throughout the
+ package. Essentially every file indirectly requires this
+ one. If you're adding a function that's used by different
+ parts of the package, add it to this file.
+- ~sx-time.el~ :: Similar to ~sx.el~, but only contains a few
+ time-related functions.
+- ~sx-filter.el~ :: Handles retrieval of filters.
+- ~sx-cache.el~ :: Saves and restores persistent data between
+ sessions.
+- ~sx-button.el~ :: Defines all button types used throughout the
+ package. Currently used only by
+ ~sx-question-print.el~.
+- ~sx-request.el~ :: Requests and url manipulation. Back-end used by
+ ~sx-method.el~. It shouldn't be necessary to use the functions in
+ this file outside ~sx-method.el~.
+- ~sx-method.el~ :: Main interface for API method calls.
+- ~sx-favorites.el~ :: Starred questions.
+- ~sx-networks.el~ :: User network information.
+- ~sx-site.el~ :: Browsing sites.
+- ~sx-auth.el~ :: Handles user authentication.
+- ~sx-question.el~ :: Base question logic. Holds several functions for
+ retrieving questions and for processing retrieved
+ questions. Doesn't do any sort of user interface, that is left
+ for ~sx-question-list.el~ and ~sx-question-mode.el~.
+- ~sx-question-list.el~ :: Major-mode for navigating questions list.
+- ~sx-question-mode.el~ :: User interface for displaying a
+ question. Creates the buffer and defines the major-mode.
+- ~sx-question-print.el~ :: Populating the question buffer with
+ content. Used by ~sx-question-mode.el~ to actually print the
+ content of a question.
+- ~sx-babel.el~ :: Font-locking code blocks printed by
+ ~sx-question-print.el~ according to the language.
+- ~sx-compose.el~ :: Major-mode for composing questions and answers.
+- ~sx-interaction.el~ :: Voting, commenting, and otherwise interacting
+ with questions.
+- ~sx-tab.el~ :: Functions for viewing different tabs.
+- ~sx-load.el~ :: Load all files of the SX package. Designed as an
+ easy way in for users who install the package
+ manually (since they don't have autoloads).
* COMMENT Local Variables
# LocalWords: StackExchange SX inbox sx API url json inline Org
diff --git a/test/test-macros.el b/test/test-macros.el
index 1634603..5e0eac9 100644
--- a/test/test-macros.el
+++ b/test/test-macros.el
@@ -39,6 +39,5 @@
.page
.page_size
.quota_max
- .quota_remaining
- .total)
- nil none))))
+ .quota_remaining)
+ nil nil))))
diff --git a/test/test-printing.el b/test/test-printing.el
index 29c209d..677dca3 100644
--- a/test/test-printing.el
+++ b/test/test-printing.el
@@ -76,10 +76,129 @@ after being run through `sx-question--tag-format'."
"Test `sx--user-@name' character substitution"
(should
(string=
- (sx--user-@name '((display_name . "ĥÞßđłřğĝýÿñńśşšŝżźžçćčĉùúûüŭůòóôõöøőðìíîïıèéêëęàåáâäãåąĵ★")))
+ (sx-user--@name "ĥÞßđłřğĝýÿñńśşšŝżźžçćčĉùúûüŭůòóôõöøőðìíîïıèéêëęàåáâäãåąĵ★")
"@hTHssdlrggyynnsssszzzccccuuuuuuooooooooiiiiieeeeeaaaaaaaaj"))
(should
(string=
- (sx--user-@name '((display_name . "ĤÞßĐŁŘĞĜÝŸÑŃŚŞŠŜŻŹŽÇĆČĈÙÚÛÜŬŮÒÓÔÕÖØŐÐÌÍÎÏıÈÉÊËĘÀÅÁÂÄÃÅĄĴ")))
+ (sx-user--@name "ĤÞßĐŁŘĞĜÝŸÑŃŚŞŠŜŻŹŽÇĆČĈÙÚÛÜŬŮÒÓÔÕÖØŐÐÌÍÎÏıÈÉÊËĘÀÅÁÂÄÃÅĄĴ")
+ "@HTHssDLRGGYYNNSSSSZZZCCCCUUUUUUOOOOOOOOIIIIiEEEEEAAAAAAAAJ"))
+ (should-error
+ (sx-user--@name 2)))
+
+(ert-deftest sx-user--format ()
+ "Test various `sx-user--format' features."
+ (let ((user
+ '((display_name . "ĥÞßđłřğĝýÿñńśşšŝżźžçćčĉùúûüŭůòóôõöøőðìíîïıèéêëęàåáâäãåąĵ★")
+ (accept_rate . 90)
+ (reputation . 10)
+ (link . "link"))))
+ (should
+ (equal (sx-user--format "%l" user) "link"))
+ (should
+ (equal
+ (sx-user--format "%@" user)
+ "@hTHssdlrggyynnsssszzzccccuuuuuuooooooooiiiiieeeeeaaaaaaaaj"))
+ (should
+ (equal
+ (sx-user--format "%@%%d%%%-30d %9r%l" user)
+ "@hTHssdlrggyynnsssszzzccccuuuuuuooooooooiiiiieeeeeaaaaaaaaj%d%ĥÞßđłřğĝýÿñńśşšŝżźžçćčĉùúûüŭůòóôõöøőðìíîïıèéêëęàåáâäãåąĵ★ 10link")))
+ (should
+ (string=
+ (sx-user--format "%@" '((display_name . "ĤÞßĐŁŘĞĜÝŸÑŃŚŞŠŜŻŹŽÇĆČĈÙÚÛÜŬŮÒÓÔÕÖØŐÐÌÍÎÏıÈÉÊËĘÀÅÁÂÄÃÅĄĴ")))
"@HTHssDLRGGYYNNSSSSZZZCCCCUUUUUUOOOOOOOOIIIIiEEEEEAAAAAAAAJ")))
+(ert-deftest sx-object-modification ()
+ "Test adding things to objects"
+ (let ((object (list (cons 'owner "me"))))
+ (should
+ (equal (sx--ensure-owner-in-object 1 object)
+ '((owner . "me"))))
+ (should
+ (equal object '((owner . "me")))))
+ (let ((object (list (cons 'not-owner "me"))))
+ (should
+ (equal (sx--ensure-owner-in-object 1 object)
+ '((owner . 1) (not-owner . "me"))))
+ (should
+ (equal object '((owner . 1) (not-owner . "me")))))
+ (let ((object (list (cons 'comments [something]))))
+ (should
+ (equal (sx--add-comment-to-object "comment" object)
+ '((comments . [something "comment"]))))
+ (should
+ (equal object '((comments . [something "comment"])))))
+ (let ((object (list (cons 'not-comments [something]))))
+ (should
+ (equal (sx--add-comment-to-object "comment" object)
+ '((comments . ["comment"]) (not-comments . [something]))))
+ (should
+ (equal object '((comments . ["comment"]) (not-comments . [something])))))
+ (let ((object (list (cons 'not-answers [something]))))
+ (should
+ (equal (sx--add-answer-to-question-object "answer" object)
+ '((answers . ["answer"]) (not-answers . [something]))))
+ (should
+ (equal object '((answers . ["answer"]) (not-answers . [something])))))
+ (let ((object (list (cons 'answers [something]))))
+ (should
+ (equal (sx--add-answer-to-question-object "answer" object)
+ '((answers . [something "answer"]))))
+ (should
+ (equal object '((answers . [something "answer"]))))))
+
+(ert-deftest sx-question-mode--fill-and-fontify ()
+ "Check complicated questions are filled correctly."
+ (should
+ (equal
+ (sx-question-mode--fill-and-fontify
+ "Creating an account on a new site requires you to log into that site using *the same credentials you used on existing sites.* For instance, if you used the Stack Exchange login method, you'd...
+
+1. Click the \"Log in using Stack Exchange\" button:
+
+ ![][1]
+
+2. Enter your username and password (yes, even if you *just did this* to log into, say, Stack Overflow) and press the \"Log In\" button:
+
+ ![][2]
+
+3. Confirm the creation of the new account:
+
+ ![][3]
+
+ some code block
+ some code block
+ some code block
+ some code block
+ some code block
+ some code block
+
+ [1]: http://i.stack.imgur.com/ktFTs.png
+ [2]: http://i.stack.imgur.com/5l2AY.png
+ [3]: http://i.stack.imgur.com/22myl.png")
+ "Creating an account on a new site requires you to log into that site
+using *the same credentials you used on existing sites.* For instance,
+if you used the Stack Exchange login method, you'd...
+
+1. Click the \"Log in using Stack Exchange\" button:
+
+ ![][1]
+
+2. Enter your username and password (yes, even if you *just did this*
+ to log into, say, Stack Overflow) and press the \"Log In\" button:
+
+ ![][2]
+
+3. Confirm the creation of the new account:
+
+ ![][3]
+
+ some code block
+ some code block
+ some code block
+ some code block
+ some code block
+ some code block
+
+ [1]: http://i.stack.imgur.com/ktFTs.png
+ [2]: http://i.stack.imgur.com/5l2AY.png
+ [3]: http://i.stack.imgur.com/22myl.png")))