aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Allred <code@seanallred.com>2014-12-26 17:49:11 -0500
committerSean Allred <code@seanallred.com>2014-12-26 17:50:19 -0500
commit0db8321f1dbb827666ef79bdae19b4864cb524ac (patch)
treeed8eeb818cd78d18b597839fcf25dd843b2b4677
parent0354bf2c974b13967558187936918db4af125571 (diff)
parenta919c72f2b58d889bf3fbdde100f9912a90c64ab (diff)
Merge branch 'master' into visit-question-from-link
Conflicts: sx.el Conflict arose from 6eb53ee0f12dd9f7d444e6749f6cc55c6db62078
-rw-r--r--README.org8
-rw-r--r--list-and-question.pngbin0 -> 450796 bytes
-rw-r--r--sx-babel.el32
-rw-r--r--sx-button.el13
-rw-r--r--sx-compose.el23
-rw-r--r--sx-interaction.el30
-rw-r--r--sx-question-list.el2
-rw-r--r--sx-question-mode.el12
-rw-r--r--sx-question-print.el122
-rw-r--r--sx-request.el80
-rw-r--r--sx.el182
-rw-r--r--test/tests.el16
12 files changed, 291 insertions, 229 deletions
diff --git a/README.org b/README.org
index db47904..b9888a7 100644
--- a/README.org
+++ b/README.org
@@ -4,9 +4,11 @@
[[https://gitter.im/vermiculus/sx.el?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge][https://badges.gitter.im/Join Chat.svg]]
[[https://www.waffle.io/vermiculus/sx.el][https://badge.waffle.io/vermiculus/sx.el.svg]]
-SX will be a full featured Stack Exchange mode for GNU Emacs 24+. Using the
-official API, we aim to create a more versatile experience for the Stack
-Exchange network within Emacs itself.
+SX is a full featured Stack Exchange mode for GNU Emacs 24+. Using the official
+API, it provides a versatile experience for the Stack Exchange network within
+Emacs itself.
+
+[[file:list-and-question.png]]
* Features
** Viewing Questions
diff --git a/list-and-question.png b/list-and-question.png
new file mode 100644
index 0000000..9e89fec
--- /dev/null
+++ b/list-and-question.png
Binary files differ
diff --git a/sx-babel.el b/sx-babel.el
index 5544642..b30a044 100644
--- a/sx-babel.el
+++ b/sx-babel.el
@@ -51,38 +51,44 @@ on a match.")
(defun sx-babel--make-pre-button (beg end)
"Turn the region between BEG and END into a button."
(let ((text (buffer-substring-no-properties beg end))
- indent)
+ indent mode copy)
(with-temp-buffer
(insert text)
(setq indent (sx-babel--unindent-buffer))
(goto-char (point-min))
- (make-text-button
- (point-min) (point-max)
- 'sx-button-copy (buffer-string)
- :type 'sx-question-mode-code-block)
- (sx-babel--determine-and-activate-major-mode)
+ (setq mode (sx-babel--determine-major-mode))
+ (setq copy (replace-regexp-in-string "[[:space:]]+\\'" "" (buffer-string)))
+ (when mode
+ (delay-mode-hooks (funcall mode)))
(font-lock-fontify-region (point-min) (point-max))
(goto-char (point-min))
(let ((space (make-string indent ?\s)))
(while (not (eobp))
- (insert space)
+ (insert-and-inherit space)
(forward-line 1)))
(setq text (buffer-string)))
(goto-char beg)
(delete-region beg end)
- (insert text)))
-
-(defun sx-babel--determine-and-activate-major-mode ()
- "Activate the major-mode most suitable for the current buffer."
+ (insert-text-button
+ text
+ 'sx-button-copy copy
+ ;; We store the mode here so it can be used if the user wants
+ ;; to edit the code block.
+ 'sx-mode mode
+ :type 'sx-question-mode-code-block)))
+
+(defun sx-babel--determine-major-mode ()
+ "Return the major-mode most suitable for the current buffer."
(let ((alist sx-babel-major-mode-alist)
- cell)
+ cell out)
(while (setq cell (pop alist))
(goto-char (point-min))
(skip-chars-forward "\r\n[:blank:]")
(let ((kar (car cell)))
(when (if (stringp kar) (looking-at kar) (funcall kar))
(setq alist nil)
- (funcall (cadr cell)))))))
+ (setq out (cadr cell)))))
+ out))
(defun sx-babel--unindent-buffer ()
"Remove absolute indentation in current buffer.
diff --git a/sx-button.el b/sx-button.el
index 283fe0d..f166164 100644
--- a/sx-button.el
+++ b/sx-button.el
@@ -77,20 +77,23 @@ This is usually a link's URL, or the content of a code block."
(point) 'sx-button-copy-type)
content)))))
-(defun sx-button-edit-this (text-or-marker)
- "Open a temp buffer populated with the string TEXT-OR-MARKER.
+(defun sx-button-edit-this (text-or-marker &optional major-mode)
+ "Open a temp buffer populated with the string TEXT-OR-MARKER using MAJOR-MODE.
When given a marker (or interactively), use the 'sx-button-copy
-text-property under the marker. This is usually the content of a
-code-block."
+and the 'sx-mode text-properties under the marker. These are
+usually part of a code-block."
(interactive (list (point-marker)))
;; Buttons receive markers.
(when (markerp text-or-marker)
+ (setq major-mode (get-text-property text-or-marker 'sx-mode))
(unless (setq text-or-marker
(get-text-property text-or-marker 'sx-button-copy))
(sx-message "Nothing of interest here.")))
(with-current-buffer (pop-to-buffer (generate-new-buffer
"*sx temp buffer*"))
- (insert text-or-marker)))
+ (insert text-or-marker)
+ (when major-mode
+ (funcall major-mode))))
(defun sx-button-follow-link (&optional pos)
"Follow link at POS. If POS is nil, use `point'."
diff --git a/sx-compose.el b/sx-compose.el
index 96f47f3..5201435 100644
--- a/sx-compose.el
+++ b/sx-compose.el
@@ -117,9 +117,12 @@ contents to the API, then calls `sx-compose-after-send-functions'."
(current-buffer) result)))))
(defun sx-compose-quit (buffer _)
- "Kill BUFFER."
+ "Close BUFFER's window and kill it."
(interactive (list (current-buffer) nil))
(when (buffer-live-p buffer)
+ (let ((w (get-buffer-window buffer)))
+ (when (window-live-p w)
+ (delete-window w)))
(kill-buffer buffer)))
(defun sx-compose--copy-as-kill (buffer _)
@@ -146,19 +149,22 @@ respectively added locally to `sx-compose-before-send-hook' and
(error "Invalid PARENT"))
(let ((is-question
(and (listp parent)
- (null (cdr (assoc 'answer_id parent))))))
+ (cdr (assoc 'title parent)))))
(with-current-buffer (sx-compose--get-buffer-create site parent)
(sx-compose-mode)
(setq sx-compose--send-function
(if (consp parent)
(sx-assoc-let parent
- (lambda () (sx-method-call (if .title 'questions 'answers)
+ (lambda () (sx-method-call (cond
+ (.title 'questions)
+ (.comment_id 'comments)
+ (t 'answers))
:auth 'warn
:url-method "POST"
:filter sx-browse-filter
:site site
:keywords (sx-compose--generate-keywords is-question)
- :id (or .answer_id .question_id)
+ :id (or .comment_id .answer_id .question_id)
:submethod 'edit)))
(lambda () (sx-method-call 'questions
:auth 'warn
@@ -256,8 +262,13 @@ the id property."
site data)))
(t
(get-buffer-create
- (format "*sx draft edit %s %s*"
- site (sx-assoc-let data (or .answer_id .question_id)))))))
+ (sx-assoc-let data
+ (format "*sx draft edit %s %s %s*"
+ site
+ (cond (.title "question")
+ (.comment_id "comment")
+ (t "answer"))
+ (or .comment_id .answer_id .question_id)))))))
(provide 'sx-compose)
;;; sx-compose.el ends here
diff --git a/sx-interaction.el b/sx-interaction.el
index ea494eb..2768c8d 100644
--- a/sx-interaction.el
+++ b/sx-interaction.el
@@ -93,10 +93,11 @@ If it's not a question, or if it is read, return DATA."
If BUFFER is not live, nothing is done."
(setq buffer (or buffer (current-buffer)))
(when (buffer-live-p buffer)
- (cond ((derived-mode-p 'sx-question-list-mode)
- (sx-question-list-refresh 'redisplay 'no-update))
- ((derived-mode-p 'sx-question-mode)
- (sx-question-mode-refresh 'no-update)))))
+ (with-current-buffer buffer
+ (cond ((derived-mode-p 'sx-question-list-mode)
+ (sx-question-list-refresh 'redisplay 'no-update))
+ ((derived-mode-p 'sx-question-mode)
+ (sx-question-mode-refresh 'no-update))))))
(defun sx--copy-data (from to)
"Copy all fields of alist FORM onto TO.
@@ -233,8 +234,8 @@ TEXT is a string. Interactively, it is read from the minibufer."
"Comment text: "
(when .comment_id
(concat (sx--user-@name .owner) " "))))
- (while (< (string-width text) 15)
- (setq text (read-string "Comment text (at least 15 characters): " text))))
+ (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.
(unless (stringp text)
(error "Comment body must be a string"))
@@ -258,6 +259,18 @@ TEXT is a string. Interactively, it is read from the minibufer."
;; Display the changes in `data'.
(sx--maybe-update-display)))))
+(defun sx--comment-valid-p (&optional text silent)
+ "Non-nil if TEXT fits stack exchange comment length limits.
+If TEXT is nil, use `buffer-string'. Must have more than 15 and
+less than 601 characters.
+If SILENT is nil, message the user about this limit."
+ (let ((w (string-width (or text (buffer-string)))))
+ (if (and (< 15 w) (< w 601))
+ t
+ (unless silent
+ (message "Comments must be within 16 and 600 characters."))
+ nil)))
+
(defun sx--get-post (type site id)
"Find in the database a post identified by TYPE, SITE and ID.
TYPE is `question' or `answer'.
@@ -304,11 +317,12 @@ from context at point."
;; If we ever make an "Edit" button, first arg is a marker.
(when (markerp data) (setq data (sx--data-here)))
(sx-assoc-let data
- (when .comment_id (sx-user-error "Editing comments is not supported yet"))
(let ((buffer (current-buffer)))
(pop-to-buffer
(sx-compose-create
- .site data nil
+ .site data
+ ;; Before send hook
+ (when .comment_id (list #'sx--comment-valid-p))
;; After send functions
(list (lambda (_ res)
(sx--copy-data (elt res 0) data)
diff --git a/sx-question-list.el b/sx-question-list.el
index f6a82e2..62ce032 100644
--- a/sx-question-list.el
+++ b/sx-question-list.el
@@ -315,7 +315,7 @@ into consideration.
("K" sx-question-list-previous-far)
("g" sx-question-list-refresh)
(":" sx-question-list-switch-site)
- ("t" sx-question-list-switch-tab)
+ ("t" sx-tab-switch)
("a" sx-ask)
("v" sx-visit-externally)
("u" sx-toggle-upvote)
diff --git a/sx-question-mode.el b/sx-question-mode.el
index 807eeea..a60cf3a 100644
--- a/sx-question-mode.el
+++ b/sx-question-mode.el
@@ -30,11 +30,13 @@
;;; Displaying a question
-(defcustom sx-question-mode-display-buffer-function #'switch-to-buffer
+(defcustom sx-question-mode-display-buffer-function #'pop-to-buffer
"Function used to display the question buffer.
Called, for instance, when hitting \\<sx-question-list-mode-map>`\\[sx-question-list-display-question]' on an entry in the
question list.
-This is not used when navigating the question list with `\\[sx-question-list-view-next]."
+This is not used when navigating the question list with `\\[sx-question-list-view-next].
+
+Common values for this variable are `pop-to-buffer' and `switch-to-buffer'."
:type 'function
:group 'sx-question-mode)
@@ -120,10 +122,8 @@ Prefix argument N moves N sections down or up."
;; If all we did was move out the current one, then move again
;; and we're guaranteed to reach the next section.
(sx-question-mode--goto-property-change 'section n))
- (let ((ov (car-safe (sx-question-mode--section-overlays-at (point)))))
- (unless (and (overlayp ov)
- (overlay-get ov 'invisible))
- (cl-decf count)))))
+ (unless (get-char-property (point) 'invisible)
+ (cl-decf count))))
(when (equal (selected-window) (get-buffer-window))
(when sx-question-mode-recenter-line
(let ((ov (sx-question-mode--section-overlays-at (line-end-position))))
diff --git a/sx-question-print.el b/sx-question-print.el
index 223049a..07378e8 100644
--- a/sx-question-print.el
+++ b/sx-question-print.el
@@ -182,9 +182,6 @@ QUESTION must be a data structure returned by `json-read'."
(mapc #'sx-question-mode--print-section .answers))
(insert "\n\n ")
(insert-text-button "Write an Answer" :type 'sx-button-answer)
- ;; Display weird chars correctly
- (set-buffer-multibyte nil)
- (set-buffer-multibyte t)
;; Go up
(goto-char (point-min))
(sx-question-mode-next-section))
@@ -238,8 +235,7 @@ DATA can represent a question or an answer."
;; Body
(insert "\n"
(propertize sx-question-mode-separator
- 'face 'sx-question-mode-header
- 'sx-question-mode--section 4))
+ 'face 'sx-question-mode-header))
(sx--wrap-in-overlay
'(face sx-question-mode-content-face)
(insert "\n"
@@ -290,18 +286,22 @@ The comment is indented, filled, and then printed according to
(sx--wrap-in-overlay
(list 'sx--data-here comment-data)
(sx-assoc-let comment-data
+ (when (and (numberp .score) (> .score 0))
+ (insert (number-to-string .score)
+ (if (eq .upvoted t) "^" "")
+ " "))
(insert
(format
- sx-question-mode-comments-format
- (sx-question-mode--propertize-display-name .owner)
- (substring
- ;; We fill with three spaces at the start, so the comment is
- ;; slightly indented.
- (sx-question-mode--fill-and-fontify
- (concat " " .body_markdown))
- ;; Then we remove the spaces from the first line, since we'll
- ;; add the username there anyway.
- 3))))))
+ sx-question-mode-comments-format
+ (sx-question-mode--propertize-display-name .owner)
+ (substring
+ ;; We fill with three spaces at the start, so the comment is
+ ;; slightly indented.
+ (sx-question-mode--fill-and-fontify
+ (concat " " .body_markdown))
+ ;; Then we remove the spaces from the first line, since we'll
+ ;; add the username there anyway.
+ 3))))))
(defun sx-question-mode--insert-header (&rest args)
"Insert propertized ARGS.
@@ -340,7 +340,7 @@ E.g.:
"Return TEXT filled according to `markdown-mode'."
(with-temp-buffer
(insert text)
- (markdown-mode)
+ (delay-mode-hooks (markdown-mode))
(font-lock-mode -1)
(when sx-question-mode-bullet-appearance
(font-lock-add-keywords ;; Bullet items.
@@ -366,23 +366,10 @@ E.g.:
(skip-chars-forward "\r\n[:blank:]")
(forward-paragraph)
(fill-region beg (point)))))
- (string-trim-right (buffer-string))))
-
-(defun sx-question-mode--dont-fill-here ()
- "If text shouldn't be filled here, return t and skip over it."
- (or (sx-question-mode--skip-and-fontify-pre)
- ;; Skip headers and references
- (let ((pos (point)))
- (skip-chars-forward "\r\n[:blank:]")
- (goto-char (line-beginning-position))
- (if (or (looking-at-p (format sx-question-mode--reference-regexp ".+"))
- (looking-at-p "^#"))
- ;; Returns non-nil
- (forward-paragraph)
- ;; Go back and return nil
- (goto-char pos)
- nil))))
+ (replace-regexp-in-string "[[:blank:]]+\\'" "" (buffer-string))))
+
+;;; Handling links
(defun sx-question-mode--process-links-in-buffer ()
"Turn all markdown links in this buffer into compact format."
(save-excursion
@@ -394,10 +381,11 @@ E.g.:
(match-string-no-properties 3)
text)))
(full-text (match-string-no-properties 0)))
- (replace-match "")
- (sx-question-mode--insert-link
- (if sx-question-mode-pretty-links text full-text)
- url)))))
+ (when (stringp url)
+ (replace-match "")
+ (sx-question-mode--insert-link
+ (if sx-question-mode-pretty-links text full-text)
+ url))))))
(defun sx-question-mode--insert-link (text url)
"Return a link propertized version of string TEXT.
@@ -426,25 +414,57 @@ If ID is nil, use FALLBACK-ID instead."
nil t)
(match-string-no-properties 1)))))
+
+;;; Things we don't fill
+(defun sx-question-mode--dont-fill-here ()
+ "If text shouldn't be filled here, return t and skip over it."
+ (catch 'sx-question-mode-done
+ (let ((before (point)))
+ (skip-chars-forward "\r\n[:blank:]")
+ (let ((first-non-blank (point)))
+ (dolist (it '(sx-question-mode--skip-and-fontify-pre
+ sx-question-mode--skip-headline
+ sx-question-mode--skip-references
+ sx-question-mode--skip-comments))
+ ;; If something worked, keep point where it is and return t.
+ (if (funcall it) (throw 'sx-question-mode-done t)
+ ;; Before calling each new function. Go back to the first
+ ;; non-blank char.
+ (goto-char first-non-blank)))
+ ;; If nothing matched, go back to the very beginning.
+ (goto-char before)
+ ;; And return nil
+ nil))))
+
(defun sx-question-mode--skip-and-fontify-pre ()
"If there's a pre block ahead, handle it, skip it and return t.
Handling means to turn it into a button and remove erroneous
font-locking."
- (let ((before (point))
- beg end)
- (if (markdown-match-pre-blocks
- (save-excursion
- (skip-chars-forward "\r\n[:blank:]")
- (setq beg (point))))
- (progn
- (setq end (point))
- (sx-babel--make-pre-button
- (save-excursion
- (goto-char beg)
- (line-beginning-position))
- end))
- (goto-char before)
- nil)))
+ (let ((beg (line-beginning-position)))
+ ;; To identify code-blocks we need to be at start of line.
+ (goto-char beg)
+ (when (markdown-match-pre-blocks (line-end-position))
+ (sx-babel--make-pre-button beg (point))
+ t)))
+
+(defun sx-question-mode--skip-comments ()
+ "If there's an html comment ahead, skip it and return t."
+ ;; @TODO: Handle the comment.
+ ;; "Handling means to store any relevant metadata it might be holding."
+ (markdown-match-comments (line-end-position)))
+
+(defun sx-question-mode--skip-headline ()
+ "If there's a headline ahead, skip it and return non-nil."
+ (when (or (looking-at-p "^#+ ")
+ (progn (forward-line 1) (looking-at-p "===\\|---")))
+ ;; Returns non-nil.
+ (forward-line 1)))
+
+(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 ".+"))
+ ;; Returns non-nil
+ (forward-line 1)))
(provide 'sx-question-print)
;;; sx-question-print.el ends here
diff --git a/sx-request.el b/sx-request.el
index 2d894f0..1031ea7 100644
--- a/sx-request.el
+++ b/sx-request.el
@@ -70,7 +70,11 @@
(defcustom sx-request-unzip-program
"gunzip"
"Program used to unzip the response if it is compressed.
-This program must accept compressed data on standard input."
+This program must accept compressed data on standard input.
+
+This is only used (and necessary) if the function
+`zlib-decompress-region' is not defined, which is the case for
+Emacs versions < 24.4."
:group 'sx
:type 'string)
@@ -121,40 +125,46 @@ the main content of the response is returned."
(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
- (let* ((data (progn
- ;; @TODO use url-http-end-of-headers
- (goto-char (point-min))
- (if (not (search-forward "\n\n" nil t))
- (error "Headers missing; response corrupt")
- (delete-region (point-min) (point))
- (buffer-string))))
- (response-zipped-p (sx-encoding-gzipped-p data))
- (data (if (not response-zipped-p) data
- (shell-command-on-region
- (point-min) (point-max)
- sx-request-unzip-program
- nil t)
- (buffer-string)))
- ;; @TODO should use `condition-case' here -- set
- ;; RESPONSE to 'corrupt or something
- (response (with-demoted-errors "`json' error: %S"
- (json-read-from-string data))))
- (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'"))
- ;; If we get here, the response is a valid data structure
- (sx-assoc-let response
- (when .error_id
- (error "Request failed: (%s) [%i %s] %S"
- .method .error_id .error_name .error_message))
- (when (< (setq sx-request-remaining-api-requests .quota_remaining)
- sx-request-remaining-api-requests-message-threshold)
- (sx-message "%d API requests reamining"
- sx-request-remaining-api-requests))
- (sx-encoding-clean-content-deep .items)))))))
+ (if (not response-buffer)
+ (error "Something went wrong in `url-retrieve-synchronously'")
+ (with-current-buffer response-buffer
+ (let* ((data (progn
+ ;; @TODO use url-http-end-of-headers
+ (goto-char (point-min))
+ (if (not (search-forward "\n\n" nil t))
+ (error "Headers missing; response corrupt")
+ (delete-region (point-min) (point))
+ (buffer-string))))
+ (response-zipped-p (sx-encoding-gzipped-p data))
+ (data
+ ;; Turn string of bytes into string of characters. See
+ ;; http://emacs.stackexchange.com/q/4100/50
+ (decode-coding-string
+ (if (not response-zipped-p) data
+ (if (fboundp 'zlib-decompress-region)
+ (zlib-decompress-region (point-min) (point-max))
+ (shell-command-on-region
+ (point-min) (point-max)
+ sx-request-unzip-program nil t))
+ (buffer-string))
+ 'utf-8 'nocopy))
+ ;; @TODO should use `condition-case' here -- set
+ ;; RESPONSE to 'corrupt or something
+ (response (with-demoted-errors "`json' error: %S"
+ (json-read-from-string data))))
+ (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'"))
+ ;; If we get here, the response is a valid data structure
+ (sx-assoc-let response
+ (when .error_id
+ (error "Request failed: (%s) [%i %s] %S"
+ .method .error_id .error_name .error_message))
+ (when (< (setq sx-request-remaining-api-requests .quota_remaining)
+ sx-request-remaining-api-requests-message-threshold)
+ (sx-message "%d API requests reamining"
+ sx-request-remaining-api-requests))
+ (sx-encoding-clean-content-deep .items)))))))
(defun sx-request-fallback (_method &optional _args _request-method)
"Fallback method when authentication is not available.
diff --git a/sx.el b/sx.el
index c9b5d76..c9fbf75 100644
--- a/sx.el
+++ b/sx.el
@@ -51,6 +51,96 @@
(browse-url "https://github.com/vermiculus/sx.el/issues/new"))
+;;; Site
+(defun sx--site (data)
+ "Get the site in which DATA belongs.
+DATA can be a question, answer, comment, or user (or any object
+with a `link' property).
+DATA can also be the link itself."
+ (let ((link (if (stringp data) data
+ (cdr (assoc 'link data)))))
+ (when (stringp link)
+ (replace-regexp-in-string
+ (rx line-start "http" (optional "s") "://"
+ (or
+ (sequence
+ (group-n 1 (+ (not (any "/"))))
+ ".stackexchange")
+ (group-n 2 (+ (not (any "/")))))
+ "." (+ (not (any ".")))
+ "/" (* any)
+ line-end)
+ "\\1\\2" link))))
+
+(defun sx--ensure-site (data)
+ "Add a `site' property to DATA if it doesn't have one. Return DATA.
+DATA can be a question, answer, comment, or user (or any object
+with a `link' property)."
+ (when data
+ (unless (assq 'site data)
+ (setcdr data (cons (cons 'site (sx--site data))
+ (cdr data))))
+ data))
+
+(defun sx--link-to-data (link)
+ "Convert string LINK into data that can be displayed."
+ (let ((result (list (cons 'site (sx--site link)))))
+ ;; Try to strip a question or answer ID
+ (when (or
+ ;; Answer
+ (and (or (string-match
+ ;; From 'Share' button
+ (rx "/a/"
+ ;; Question ID
+ (group (+ digit))
+ ;; User ID
+ "/" (+ digit)
+ ;; Answer ID
+ (group (or (sequence "#" (* any)) ""))
+ string-end) link)
+ (string-match
+ ;; From URL
+ (rx "/questions/" (+ digit) "/"
+ (+ (not (any "/"))) "/"
+ ;; User ID
+ (optional (group (+ digit)))
+ (optional "/")
+ (group (or (sequence "#" (* any)) ""))
+ string-end) link))
+ (push '(type . answer) result))
+ ;; Question
+ (and (or (string-match
+ ;; From 'Share' button
+ (rx "/q/"
+ ;; Question ID
+ (group (+ digit))
+ ;; User ID
+ (optional "/" (+ digit))
+ ;; Answer or Comment ID
+ (group (or (sequence "#" (* any)) ""))
+ string-end) link)
+ (string-match
+ ;; From URL
+ (rx "/questions/"
+ ;; Question ID
+ (group (+ digit))
+ "/") link))
+ (push '(type . question) result)))
+ (push (cons 'id (string-to-number (match-string-no-properties 1 link)))
+ result))
+ result))
+
+(defmacro sx-assoc-let (alist &rest body)
+ "Identical to `let-alist', except `.site' has a special meaning.
+If ALIST doesn't have a `site' property, one is created using the
+`link' property."
+ (declare (indent 1) (debug t))
+ `(progn
+ (require 'let-alist)
+ (sx--ensure-site ,alist)
+ (let-alist ,alist ,@body)))
+
+
;;; Browsing filter
(defvar sx-browse-filter
'((question.body_markdown
@@ -107,7 +197,8 @@ is intentionally skipped."
(defun sx-user-error (format-string &rest args)
"Like `user-error', but prepend FORMAT-STRING with \"[sx]\".
See `format'."
- (signal 'user-error (list (apply #'format (concat "[sx] " format) args))))
+ (signal 'user-error
+ (list (apply #'format (concat "[sx] " format-string) args))))
(defun sx-message (format-string &rest args)
"Display FORMAT-STRING as a message with ARGS.
@@ -284,95 +375,6 @@ removed from the display name before it is returned."
string))
-;;; Site
-(defun sx--site (data)
- "Get the site in which DATA belongs.
-DATA can be a question, answer, comment, or user (or any object
-with a `link' property).
-DATA can also be the link itself."
- (let ((link (if (stringp data) data
- (cdr (assoc 'link data)))))
- (when (stringp link)
- (replace-regexp-in-string
- (rx line-start "http" (optional "s") "://"
- (or
- (sequence
- (group-n 1 (+ (not (any "/"))))
- ".stackexchange")
- (group-n 2 (+ (not (any "/")))))
- "." (+ (not (any ".")))
- "/" (* any)
- line-end)
- "\\1\\2" link))))
-
-(defun sx--ensure-site (data)
- "Add a `site' property to DATA if it doesn't have one. Return DATA.
-DATA can be a question, answer, comment, or user (or any object
-with a `link' property)."
- (when data
- (unless (assq 'site data)
- (setcdr data (cons (cons 'site (sx--site data))
- (cdr data))))
- data))
-
-(defmacro sx-assoc-let (alist &rest body)
- "Identical to `let-alist', except `.site' has a special meaning.
-If ALIST doesn't have a `site' property, one is created using the
-`link' property."
- (declare (indent 1) (debug t))
- `(progn
- (require 'let-alist)
- (sx--ensure-site ,alist)
- (let-alist ,alist ,@body)))
-
-(defun sx--link-to-data (link)
- "Convert string LINK into data that can be displayed."
- (let ((result (list (cons 'site (sx--site link)))))
- ;; Try to strip a question or answer ID
- (when (or
- ;; Answer
- (and (or (string-match
- ;; From 'Share' button
- (rx "/a/"
- ;; Question ID
- (group (+ digit))
- ;; User ID
- "/" (+ digit)
- ;; Answer ID
- (group (or (sequence "#" (* any)) ""))
- string-end) link)
- (string-match
- ;; From URL
- (rx "/questions/" (+ digit) "/"
- (+ (not (any "/"))) "/"
- ;; User ID
- (optional (group (+ digit)))
- (optional "/")
- (group (or (sequence "#" (* any)) ""))
- string-end) link))
- (push '(type . answer) result))
- ;; Question
- (and (or (string-match
- ;; From 'Share' button
- (rx "/q/"
- ;; Question ID
- (group (+ digit))
- ;; User ID
- (optional "/" (+ digit))
- ;; Answer or Comment ID
- (group (or (sequence "#" (* any)) ""))
- string-end) link)
- (string-match
- ;; From URL
- (rx "/questions/"
- ;; Question ID
- (group (+ digit))
- "/") link))
- (push '(type . question) result)))
- (push (cons 'id (string-to-number (match-string-no-properties 1 link)))
- result))
- result))
-
(defcustom sx-init-hook nil
"Hook run when SX initializes.
Run after `sx-init--internal-hook'."
diff --git a/test/tests.el b/test/tests.el
index 8969c37..66d8d88 100644
--- a/test/tests.el
+++ b/test/tests.el
@@ -123,20 +123,14 @@
(should
(equal '(progn (require 'let-alist)
(sx--ensure-site data)
- (let ((.test (cdr (assq 'test data))))
- .test))
- (macroexpand-all
- '(sx-assoc-let data
- .test))))
+ (let-alist data .test))
+ (macroexpand '(sx-assoc-let data .test))))
(should
(equal '(progn (require 'let-alist)
(sx--ensure-site data)
- (let ((.test-one (cdr (assq 'test-one data)))
- (.test-two (cdr (assq 'test-two data))))
- (cons .test-one .test-two)))
- (macroexpand-all
- '(sx-assoc-let data
- (cons .test-one .test-two))))))
+ (let-alist data (cons .test-one .test-two)))
+ (macroexpand
+ '(sx-assoc-let data (cons .test-one .test-two))))))
(ert-deftest sx--user-@name ()
"Tests macro expansion for `sx-assoc-let'"