diff options
-rw-r--r-- | .gitignore | 12 | ||||
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r-- | Makefile | 11 | ||||
-rwxr-xr-x | bot/sx-bot.sh | 2 | ||||
-rw-r--r-- | sx-auth.el | 91 | ||||
-rw-r--r-- | sx-babel.el | 13 | ||||
-rw-r--r-- | sx-button.el | 37 | ||||
-rw-r--r-- | sx-cache.el | 19 | ||||
-rw-r--r-- | sx-compose.el | 81 | ||||
-rw-r--r-- | sx-encoding.el | 16 | ||||
-rw-r--r-- | sx-favorites.el | 13 | ||||
-rw-r--r-- | sx-filter.el | 36 | ||||
-rw-r--r-- | sx-inbox.el | 9 | ||||
-rw-r--r-- | sx-interaction.el | 27 | ||||
-rw-r--r-- | sx-load.el | 2 | ||||
-rw-r--r-- | sx-method.el | 6 | ||||
-rw-r--r-- | sx-networks.el | 7 | ||||
-rw-r--r-- | sx-notify.el | 4 | ||||
-rw-r--r-- | sx-question-list.el | 40 | ||||
-rw-r--r-- | sx-question-mode.el | 11 | ||||
-rw-r--r-- | sx-question-print.el | 92 | ||||
-rw-r--r-- | sx-question.el | 20 | ||||
-rw-r--r-- | sx-request.el | 40 | ||||
-rw-r--r-- | sx-search.el | 21 | ||||
-rw-r--r-- | sx-site.el | 6 | ||||
-rw-r--r-- | sx-switchto.el | 16 | ||||
-rw-r--r-- | sx-tab.el | 25 | ||||
-rw-r--r-- | sx-tag.el | 59 | ||||
-rw-r--r-- | sx-time.el | 11 | ||||
-rw-r--r-- | sx-user.el | 203 | ||||
-rw-r--r-- | sx.el | 126 | ||||
-rw-r--r-- | sx.org | 79 | ||||
-rw-r--r-- | test/test-macros.el | 5 | ||||
-rw-r--r-- | test/test-printing.el | 123 |
34 files changed, 933 insertions, 332 deletions
@@ -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 @@ -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 @@ -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: @@ -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: @@ -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)) @@ -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: @@ -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: @@ -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: @@ -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, @@ -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"))) |