diff options
-rw-r--r-- | CONTRIBUTING.org | 15 | ||||
-rw-r--r-- | README.org | 7 | ||||
-rw-r--r-- | resources/sx.svg | 232 | ||||
-rw-r--r-- | sx-cache.el | 5 | ||||
-rw-r--r-- | sx-compose.el | 13 | ||||
-rw-r--r-- | sx-filter.el | 16 | ||||
-rw-r--r-- | sx-interaction.el | 2 | ||||
-rw-r--r-- | sx-question-list.el | 50 | ||||
-rw-r--r-- | sx-question-print.el | 21 | ||||
-rw-r--r-- | sx-question.el | 18 | ||||
-rw-r--r-- | sx-request.el | 2 | ||||
-rw-r--r-- | sx-search.el | 24 | ||||
-rw-r--r-- | sx.el | 33 | ||||
-rw-r--r-- | sx.org | 2 | ||||
-rw-r--r-- | test/test-macros.el | 5 | ||||
-rw-r--r-- | test/test-printing.el | 24 | ||||
-rw-r--r-- | test/tests.el | 22 |
17 files changed, 432 insertions, 59 deletions
diff --git a/CONTRIBUTING.org b/CONTRIBUTING.org index 3fcf111..cc9f8ce 100644 --- a/CONTRIBUTING.org +++ b/CONTRIBUTING.org @@ -1,9 +1,24 @@ +** Found a Bug? +Open an issue! Please include: +- a description of what you are trying to do +- the behavior you actually see +- steps we can take to reproduce the bug +If the bug actually causes Emacs to issue an error, please +additionally include the backtrace that led up to the error. To do +this, use ~M-x toggle-debug-on-error~ to make sure that the setting is +/enabled/. Now, when the error occurs, a =*Backtrace*= window will +open with most of the information that we typically need. Copy and +paste this into your bug report. (To get out of the =*Backtrace*= +window and Emacs' subsequent 'debug mode', press =q=.) + +** Don't Know Your Way Around? If you need help, search the issue tracker to see if anyone has asked your question before. If it hasn't, a good place to ask first is our chat room on [[https://gitter.im/vermiculus/sx.el][Gitter]]. Opening an issue is welcome of course, but chat will likely be faster for you. If a code change needs to be made, an issue can be written up as necessary. +** Want to Help? Have a great idea for SX? Again, discuss it on [[https://gitter.im/vermiculus/sx.el][Gitter]] first! Don't limit ideas to mimicking the official website, either -- this is Emacs; we should take advantage of its abilities. @@ -1,6 +1,8 @@ #+Title: SX -- Stack Exchange for Emacs [[https://travis-ci.org/vermiculus/sx.el][https://travis-ci.org/vermiculus/sx.el.svg?branch=master]] +[[http://melpa.org/#/sx][file:http://melpa.org/packages/sx-badge.svg]] +[[http://stable.melpa.org/#/sx][file:http://stable.melpa.org/packages/sx-badge.svg]] [[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]] @@ -35,6 +37,9 @@ section. Add comments with =c=. As always, =C-h m= is the definitive resource for the functions of this mode. * Installation +SX is now available on MELPA! Install it via the usual method or run =M-x +package-install RET sx RET=. + To install the development version, follow the usual steps: - Clone this repository - Add this directory to your ~load-path~ @@ -46,8 +51,6 @@ If you are going to be doing any asking / answering / commenting / upvoting / downvoting / /etc./, you must use ~sx-authenticate~ to provide SX with an authentication token to act on your behalf. -Eventually, this package will be available on MELPA. - * Contributing Please help contribute! Doing any of the following will help us immensely: - [[https://github.com/vermiculus/sx.el/issues/new][Open an issue]] diff --git a/resources/sx.svg b/resources/sx.svg new file mode 100644 index 0000000..56d0129 --- /dev/null +++ b/resources/sx.svg @@ -0,0 +1,232 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="35.405388mm" + height="38.84499mm" + viewBox="0 0 125.45216 137.63973" + id="svg2" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="sx.svg" + inkscape:export-filename="/home/will/sx.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + <defs + id="defs4"> + <linearGradient + id="linearGradient4846"> + <stop + id="stop4848" + offset="0" + style="stop-color:#65baf3;stop-opacity:1;" /> + <stop + style="stop-color:#65baf3;stop-opacity:1;" + offset="0.75" + id="stop4850" /> + <stop + id="stop4852" + offset="0.75" + style="stop-color:#36a5ef;stop-opacity:1;" /> + <stop + style="stop-color:#36a5ef;stop-opacity:1;" + offset="1" + id="stop4854" /> + </linearGradient> + <linearGradient + id="linearGradient3686"> + <stop + style="stop-color:#5479ae;stop-opacity:1;" + offset="0" + id="stop3688" /> + <stop + id="stop3690" + offset="0.75" + style="stop-color:#5479ae;stop-opacity:1;" /> + <stop + style="stop-color:#205196;stop-opacity:1;" + offset="0.75" + id="stop3692" /> + <stop + id="stop3694" + offset="1" + style="stop-color:#205196;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3666"> + <stop + id="stop3668" + offset="0" + style="stop-color:#518fd9;stop-opacity:1;" /> + <stop + style="stop-color:#518fd9;stop-opacity:1;" + offset="0.75" + id="stop3670" /> + <stop + id="stop3672" + offset="0.75" + style="stop-color:#1d6dcd;stop-opacity:1;" /> + <stop + style="stop-color:#1d6dcd;stop-opacity:1;" + offset="1" + id="stop3674" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4846" + id="linearGradient4844" + x1="-79.081757" + y1="283.1138" + x2="-19.876425" + y2="344.00983" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(488.55313,131.15373)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3666" + id="linearGradient4862" + x1="-122.40535" + y1="400.3822" + x2="-99.634369" + y2="423.06076" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.95292128,0,0,0.95292128,464.95389,150.57487)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3686" + id="linearGradient4870" + x1="-212.95235" + y1="477.93634" + x2="-182.6236" + y2="507.84705" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(480.47191,124.08266)" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="1" + inkscape:cx="465.89524" + inkscape:cy="179.68499" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + fit-margin-top="5" + fit-margin-left="5" + fit-margin-right="5" + fit-margin-bottom="5" + inkscape:window-width="2880" + inkscape:window-height="1498" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" /> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-194.57597,-617.50518)"> + <rect + width="354.98172" + height="354.98172" + x="367.20773" + y="262.03415" + id="rect4772" + style="display:none;fill:none" /> + <g + id="g4788" + style="display:none" + transform="matrix(0.6933237,0,0,0.6933237,367.08916,261.89478)"> + <g + id="g4790" + style="display:inline" /> + </g> + <g + id="g4806" + style="display:none" + transform="matrix(0.6933237,0,0,0.6933237,367.08916,261.89478)"> + <g + id="g4808" + style="display:inline"> + <path + inkscape:connector-curvature="0" + d="m 349.098,256.651 c -0.265,-0.254 37.637,27.605 39.421,25.012 6.362,-9.252 82.046,-93.137 84.784,-116.236 0.242,-2.003 -0.516,-4.096 -0.516,-4.096 0,0 -1.19,-0.144 -6.325,-4.314 -2.692,-2.192 -5.483,-4.581 -5.483,-4.581 -16.054,0.998 -57.885,41.559 -111.062,103.568" + id="path4810" + style="display:none;fill:#050505" /> + </g> + </g> + <g + id="g4796" + style="stroke:none" + transform="matrix(0.15739948,0,0,0.15739948,247.43469,629.47196)"> + <g + id="g4800" + style="stroke:none" /> + </g> + <g + id="g4774" + transform="matrix(0.15739948,0,0,0.15739948,247.43469,629.47196)" /> + <g + id="g4952" + transform="matrix(0.22702163,0,0,0.22702163,164.09751,570.01618)"> + <path + sodipodi:nodetypes="ccsccscsscsc" + inkscape:connector-curvature="0" + id="path4806" + d="m 403.41449,388.7755 c -2.6741,-12.03361 7.6962,-11.399 17.7242,-12.06753 32.3125,-2.22844 76.43569,1.55992 115.65634,9.35948 19.71448,3.92049 23.17582,3.34267 23.17582,3.34267 28.07841,1.33707 49.91718,-13.37067 48.80295,-44.34608 -0.22285,-31.42106 -31.63829,-56.61619 -66.40765,-57.71673 -32.74253,-1.03637 -113.42786,4.67974 -113.42786,4.67974 94.04039,19.38747 109.41387,24.33504 114.31922,35.65513 2.89698,6.68534 -4.8392,13.33227 -30.75254,12.0336 -28.21151,-1.41388 -86.01798,-8.24524 -86.01798,-8.24524 -55.0426,-8.4681 -93.3719,-16.49051 -110.0852,5.34826 -10.919,14.26755 0.1313,18.0725 5.0339,28.32333" + style="fill:#84d2e9;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + sodipodi:nodetypes="cscccc" + inkscape:connector-curvature="0" + id="path4818" + d="m 499.36625,501.60118 c -19.95659,-15.28447 -55.33856,-42.75992 -61.50206,-47.615 -12.1801,-9.59442 -31.8078,-24.3181 -33.9177,-37.47548 l -76.6113,-17.55183 c 23.2443,40.55893 56.2956,60.5283 81.8022,80.13911 37.7758,5.9885 54.2997,9.12772 90.22886,22.5032 z" + style="fill:url(#linearGradient4844);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + sodipodi:nodetypes="ccccccc" + inkscape:connector-curvature="0" + id="path4814" + d="m 433.15079,512.22758 c -86.7917,-23.73213 -143.5795,-16.71037 -187.4274,6.34368 -21.9905,13.01681 -34.8165,22.85342 -33.3116,41.70349 l 93.6882,11.05699 c 9.3276,-11.26036 34.5389,-12.58769 68.2631,-20.03347 32.2743,-7.8539 154.7509,-6.26654 154.7509,-6.26654 -19.18051,-15.22821 -95.9632,-32.80415 -95.9632,-32.80415 z" + style="fill:url(#linearGradient4862);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + sodipodi:nodetypes="cccccccc" + inkscape:connector-curvature="0" + id="path4839-6" + d="m 212.96599,587.14537 c 1.8429,23.08351 11.6545,74.9329 68.3141,73.11653 l 133.0515,1.40072 -12.339,75.76574 73.75297,-75.45038 36.4933,-0.62941 c 149.2534,-82.78703 -174.65397,-24.80201 -200.54897,-44.62823 -4.9981,-6.16606 -12.7038,-7.60189 -4.8341,-17.10232" + style="fill:url(#linearGradient4870);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + <flowRoot + xml:space="preserve" + id="flowRoot4936" + style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + transform="matrix(0.22702163,0,0,0.22702163,164.09751,570.01618)"><flowRegion + id="flowRegion4938"><rect + id="rect4940" + width="1457.1428" + height="1300" + x="-1531.4286" + y="-64.780655" /></flowRegion><flowPara + id="flowPara4942" /></flowRoot> </g> +</svg> diff --git a/sx-cache.el b/sx-cache.el index 3e8e08f..b17149f 100644 --- a/sx-cache.el +++ b/sx-cache.el @@ -73,8 +73,9 @@ DATA will be written as returned by `prin1'. CACHE is resolved to a file name by `sx-cache-get-file-name'." (sx-cache--ensure-sx-cache-directory-exists) - (write-region (prin1-to-string data) nil - (sx-cache-get-file-name cache)) + (let (print-length print-level) + (write-region (prin1-to-string data) nil + (sx-cache-get-file-name cache))) data) (defun sx-cache--invalidate (cache &optional vars init-method) diff --git a/sx-compose.el b/sx-compose.el index 3047a97..eb5e2eb 100644 --- a/sx-compose.el +++ b/sx-compose.el @@ -140,10 +140,15 @@ contents to the API, then calls `sx-compose-after-send-functions'." (interactive) (when (run-hook-with-args-until-failure 'sx-compose-before-send-hook) - (let ((result (funcall sx-compose--send-function))) - (with-demoted-errors - (run-hook-with-args 'sx-compose-after-send-functions - (current-buffer) result))))) + (let ((result (funcall sx-compose--send-function)) + (buf (current-buffer))) + (run-hook-wrapped + 'sx-compose-after-send-functions + (lambda (func) + (with-demoted-errors + "[sx] Error encountered AFTER sending post, but the post was sent successfully: %s" + (funcall func buf result)) + nil))))) (defun sx-compose-insert-tags () "Prompt for a tag list for this draft and insert them." diff --git a/sx-filter.el b/sx-filter.el index af3717f..1ccf611 100644 --- a/sx-filter.el +++ b/sx-filter.el @@ -47,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 @@ -64,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)))) @@ -93,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 diff --git a/sx-interaction.el b/sx-interaction.el index 75b51ab..3d60cbe 100644 --- a/sx-interaction.el +++ b/sx-interaction.el @@ -138,7 +138,7 @@ Element can be a question, answer, or comment." (list (read-string (concat "Link (" def "): ") nil nil def)))) ;; For now, we have no chance of handling chat links, let's just ;; send them to the browser. - (if (string-match (rx string-start "http" (opt "s") "://chat.")) + (if (string-match (rx string-start "http" (opt "s") "://chat.") link) (sx-visit-externally link) (let ((data (sx--link-to-data link))) (sx-assoc-let data diff --git a/sx-question-list.el b/sx-question-list.el index 884f994..7757503 100644 --- a/sx-question-list.el +++ b/sx-question-list.el @@ -230,6 +230,24 @@ This is ignored if `sx-question-list--refresh-function' is set.") ": Quit") "Header-line used on the question list.") +(defconst sx-question-list--order-methods + '(("Recent Activity" . activity) + ("Creation Date" . creation) + ("Most Voted" . votes) + ("Score" . votes) + ("Hot" . hot)) + "Alist of possible values to be passed to the `sort' keyword.") +(make-variable-buffer-local 'sx-question-list--order-methods) + +(defun sx-question-list--interactive-order-prompt (&optional prompt) + "Interactively prompt for a sorting order. +PROMPT is displayed to the user. If it is omitted, a default one +is used." + (let ((order (sx-completing-read + (or prompt "Order questions by: ") + (mapcar #'car sx-question-list--order-methods)))) + (cdr-safe (assoc-string order sx-question-list--order-methods)))) + ;;; Mode Definition (define-derived-mode sx-question-list-mode @@ -259,6 +277,10 @@ The full list of variables which can be set is: 5. `sx-question-list--dataset' This is only used if both 3 and 4 are nil. It can be used to display a static list. + 6. `sx-question-list--order' + Set this to the `sort' method that should be used when + requesting the list, if that makes sense. If it doesn't + leave it as nil. \\<sx-question-list-mode-map> If none of these is configured, the behaviour is that of a \"Frontpage\", for the site given by @@ -282,7 +304,7 @@ Adding further questions to the bottom of the list is done by: display; otherwise, decrement `sx-question-list--pages-so-far'. If `sx-question-list--site' is given, items 3 and 4 should take it -into consideration. +into consideration. The same holds for `sx-question-list--order'. \\{sx-question-list-mode-map}" (hl-line-mode 1) @@ -309,11 +331,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 @@ -421,6 +442,10 @@ Non-interactively, DATA is a question alist." "Site being displayed in the *question-list* buffer.") (make-variable-buffer-local 'sx-question-list--site) +(defvar sx-question-list--order nil + "Order being displayed in the *question-list* buffer.") +(make-variable-buffer-local 'sx-question-list--order) + (defun sx-question-list-refresh (&optional redisplay no-update) "Update the list of questions. If REDISPLAY is non-nil (or if interactive), also call `tabulated-list-print'. @@ -593,6 +618,19 @@ Sets `sx-question-list--site' and then call (setq sx-question-list--site site) (sx-question-list-refresh 'redisplay))) +(defun sx-question-list-order-by (sort) + "Order questions in the current list by the method SORT. +Sets `sx-question-list--order' and then calls +`sx-question-list-refresh' with `redisplay'." + (interactive + (list (when sx-question-list--order + (sx-question-list--interactive-order-prompt)))) + (unless sx-question-list--order + (sx-user-error "This list can't be reordered")) + (when (and sort (symbolp sort)) + (setq sx-question-list--order sort) + (sx-question-list-refresh 'redisplay))) + (provide 'sx-question-list) ;;; sx-question-list.el ends here diff --git a/sx-question-print.el b/sx-question-print.el index 9a51efb..778b580 100644 --- a/sx-question-print.el +++ b/sx-question-print.el @@ -153,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 @@ -167,11 +176,7 @@ QUESTION must be a data structure returned by `json-read'." (sx-question-mode--print-section question) (sx-assoc-let question (mapc #'sx-question-mode--print-section - (cl-sort .answers - ;; Highest-voted first. @TODO: custom sorting - (lambda (a b) - (> (cdr (assoc 'score a)) - (cdr (assoc 'score b))))))) + (cl-sort .answers sx-question-mode-answer-sort-function))) (insert "\n\n ") (insert-text-button "Write an Answer" :type 'sx-button-answer) ;; Go up @@ -281,10 +286,10 @@ The comment is indented, filled, and then printed according to (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. (sx-question-mode--fill-and-fontify - (concat " " .body_markdown)) + ;; We fill with three spaces at the start, so the comment is + ;; slightly indented. + (concat " " (sx--squash-whitespace .body_markdown))) ;; Then we remove the spaces from the first line, since we'll ;; add the username there anyway. 3)))))) diff --git a/sx-question.el b/sx-question.el index 35eabc8..1fde1aa 100644 --- a/sx-question.el +++ b/sx-question.el @@ -148,7 +148,8 @@ See `sx-question--user-read-list'." ;; Question wasn't present. (t (sx-sorted-insert-skip-first - q-cell site-cell (lambda (x y) (> (car x) (car y)))))))) + q-cell site-cell + (lambda (x y) (> (or (car x) -1) (or (car y) -1)))))))) ;; Save the results. ;; @TODO This causes a small lag on `j' and `k' as the list gets ;; large. Should we do this on a timer? @@ -207,6 +208,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 2650c55..8f672ec 100644 --- a/sx-request.el +++ b/sx-request.el @@ -160,7 +160,7 @@ the main content of the response is returned." (url-inhibit-uncompression t) (url-request-data (sx-request--build-keyword-arguments args nil)) (request-url (concat sx-request-api-root method)) - (url-request-method (and request-method (symbol-name request-method))) + (url-request-method (and request-method (upcase (symbol-name request-method)))) (url-request-extra-headers '(("Content-Type" . "application/x-www-form-urlencoded"))) (response-buffer (url-retrieve-synchronously request-url))) diff --git a/sx-search.el b/sx-search.el index aefd12e..55964b9 100644 --- a/sx-search.el +++ b/sx-search.el @@ -39,7 +39,9 @@ ;;; Basic function -(defun sx-search-get-questions (site page query &optional tags excluded-tags keywords) +(defun sx-search-get-questions (site page query + &optional tags excluded-tags + &rest keywords) "Like `sx-question-get-questions', but restrict results by a search. Perform search on SITE. PAGE is an integer indicating which page @@ -52,7 +54,6 @@ fail. EXCLUDED-TAGS is only is used if TAGS is also provided. KEYWORDS is passed to `sx-method-call'." (sx-method-call 'search :keywords `((page . ,page) - (sort . activity) (intitle . ,query) (tagged . ,tags) (nottagged . ,excluded-tags) @@ -61,8 +62,18 @@ KEYWORDS is passed to `sx-method-call'." :auth t :filter sx-browse-filter)) +(defconst sx-search--order-methods + (cons '("Relevance" . relevance) + (cl-remove-if (lambda (x) (eq (cdr x) 'hot)) + (default-value 'sx-question-list--order-methods))) + "Alist of possible values to be passed to the `sort' keyword.") + +(defvar sx-search-default-order 'activity + "Default ordering method used on new searches. +Possible values are the cdrs of `sx-search--order-methods'.") + -;;; User command +;;;###autoload (defun sx-search (site query &optional tags excluded-tags) "Display search on SITE for question titles containing QUERY. When TAGS is given, it is a lists of tags, one of which must @@ -84,7 +95,7 @@ prefix argument, the user is asked for everything." (when current-prefix-arg (setq tags (sx-tag-multiple-read site (concat "Tags" (when query " (optional)")))) - (when (and (not query) (string= "" tags)) + (unless (or query tags) (sx-user-error "Must supply either QUERY or TAGS")) (setq excluded-tags (sx-tag-multiple-read site "Excluded tags (optional)"))) @@ -98,8 +109,11 @@ prefix argument, the user is asked for everything." (lambda (page) (sx-search-get-questions sx-question-list--site page - query tags excluded-tags))) + query tags excluded-tags + (cons 'sort sx-question-list--order)))) (setq sx-question-list--site site) + (setq sx-question-list--order sx-search-default-order) + (setq sx-question-list--order-methods sx-search--order-methods) (sx-question-list-refresh 'redisplay) (switch-to-buffer (current-buffer)))) @@ -4,7 +4,7 @@ ;; Author: Sean Allred <code@seanallred.com> ;; URL: https://github.com/vermiculus/sx.el/ -;; Version: 0.1 +;; Version: 0.2 ;; Keywords: help, hypermedia, tools ;; Package-Requires: ((emacs "24.1") (cl-lib "0.5") (json "1.3") (markdown-mode "2.0") (let-alist "1.0.3")) @@ -28,7 +28,7 @@ ;;; Code: (require 'tabulated-list) -(defconst sx-version "0.1" "Version of the `sx' package.") +(defconst sx-version "0.2" "Version of the `sx' package.") (defgroup sx nil "Customization group for the `sx' package." @@ -148,12 +148,7 @@ with a `link' property)." ;; From URL (string-match (rx "/questions/" ;; Question ID - (group-n 1 (+ digit)) "/" - ;; Optional question title - (optional (+ (not (any "/"))) "/") - ;; Garbage at the end - (optional (and (any "?#") (* any))) - string-end) + (group-n 1 (+ digit)) "/") link)) (push '(type . question) result))) (push (cons 'id (string-to-number (match-string-no-properties 1 link))) @@ -325,6 +320,21 @@ ID is an integer." (when (looking-at-p "$") (forward-char 1))))) +(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)) + `(defun ,name (a b) + ,doc + (funcall ,compare-func + (funcall ,get-func a) + (funcall ,get-func b)))) + +(defun sx--squash-whitespace (string) + "Return STRING with consecutive whitespace squashed together." + (replace-regexp-in-string "[ \r\n]+" " " string)) + ;;; Printing request data (defvar sx--overlays nil @@ -420,13 +430,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, @@ -1,4 +1,4 @@ -#+MACRO: version 0.1 +#+MACRO: version 0.2 #+MACRO: versiondate 16 November 2014 #+MACRO: updated last updated {{{versiondate}}} 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 7384829..bcc3dd9 100644 --- a/test/test-printing.el +++ b/test/test-printing.el @@ -27,6 +27,18 @@ after being run through `sx-question--tag-format'." ;;; Tests +(ert-deftest time-since () + (cl-letf (((symbol-function #'float-time) + (lambda () 1420148997.))) + (should + (string= + "67m" + (sx-time-since 1420145000.))) + (should + (string= + "12h" + (sx-time-since 1420105000.))))) + (ert-deftest question-list-tag () "Test `sx-question--tag-format'." (should @@ -134,6 +146,17 @@ after being run through `sx-question--tag-format'." (should (equal object '((answers . [something "answer"])))))) + +;;; question-mode +(ert-deftest sx-display-question () + ;; Check it doesn't error. + (sx-display-question (elt sx-test-data-questions 0)) + ;; Check it does error. + (should-error + (sx-display-question sx-test-data-questions)) + (should-error + (sx-display-question sx-test-data-questions nil 1))) + (ert-deftest sx-question-mode--fill-and-fontify () "Check complicated questions are filled correctly." (should @@ -190,3 +213,4 @@ if you used the Stack Exchange login method, you'd... [1]: http://i.stack.imgur.com/ktFTs.png [2]: http://i.stack.imgur.com/5l2AY.png [3]: http://i.stack.imgur.com/22myl.png"))) + diff --git a/test/tests.el b/test/tests.el index ce42a9f..be1552b 100644 --- a/test/tests.el +++ b/test/tests.el @@ -59,4 +59,24 @@ (apply #'message message args))) (mapc #'sx-load-test - '(api macros printing util search)) + '(api macros printing util search state)) + +(ert-deftest user-entry-functions () + "Ensures all entry functions are autoloaded." + (should + (cl-every + #'fboundp + '(sx-ask + sx-authenticate + sx-bug-report + sx-switchto-map + sx-tab-featured + sx-tab-frontpage + sx-tab-hot + sx-tab-month + sx-tab-newest + sx-tab-starred + sx-tab-topvoted + sx-tab-unanswered + sx-tab-week + sx-version)))) |