-
Notifications
You must be signed in to change notification settings - Fork 40
/
sx-compose.el
355 lines (310 loc) · 12.8 KB
/
sx-compose.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
;;; sx-compose.el --- major-mode for composing questions and answers -*- lexical-binding: t; -*-
;; Copyright (C) 2014-2018 Artur Malabarba
;; Author: Artur Malabarba <[email protected]>
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; This file defines `sx-compose-mode' and its auxiliary functions and
;; variables. In order to use `sx-compose-mode', it is vital that the
;; variable `sx-compose--send-function' be set. Otherwise it's just a
;; regular markdown buffer.
;;
;; In order to help avoid mistakes, there is the function
;; `sx-compose-create'. This is the preferred way of activating the
;; mode. It creates a buffer, activates the major mode, and sets the
;; `send-function' variable according to the arguments it is given.
;;; Code:
(require 'markdown-mode)
(require 'sx)
(require 'sx-tag)
(defgroup sx-compose-mode nil
"Customization group for sx-compose-mode."
:prefix "sx-compose-mode-"
:tag "SX compose Mode"
:group 'sx)
;;; Faces and Variables
(defvar sx-compose-before-send-hook nil
"Hook run before POSTing to the API.
Functions are called without arguments and should return non-nil.
Returning nil indicates something went wrong and the sending will
be aborted. In this case, the function is responsible for
notifying the user.
Current buffer is the compose-mode buffer whose content is about
to be POSTed.")
(defvar sx-compose-after-send-functions nil
"Hook run after POSTing to the API.
Functions on this hook should take two arguments, the
`sx-compose-mode' buffer (which not be live) and the data
returned by `sx-compose--send-function' (usually the object
created by the API). They are only called if the transaction
succeeds.")
(defvar sx-compose--send-function nil
"Function used by `sx-compose-send' to send the data.
Is invoked between `sx-compose-before-send-hook' and
`sx-compose-after-send-functions'.")
(defconst sx-compose--question-headers
(concat
#("Title: " 0 7 (intangible t read-only t rear-nonsticky t))
"%s"
#("\n" 0 1 (read-only t))
#("Tags : " 0 7 (read-only t intangible t rear-nonsticky t))
"%s"
#("\n" 0 1 (read-only t rear-nonsticky t))
#("________________________________________\n"
0 41 (read-only t rear-nonsticky t intangible t
sx-compose-separator t))
"\n")
"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 current compose buffer belongs to.")
(make-variable-buffer-local 'sx-compose--site)
;;; Major-mode
(define-derived-mode sx-compose-mode markdown-mode "Compose"
"Major mode for coposing questions and answers.
Most of the functionality comes from `markdown-mode'. This mode
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.
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
#'sx-compose--copy-as-kill nil t))
(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.
Calls `sx-compose-before-send-hook', POSTs the the current buffer
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))
(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."
(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 _)
"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)
(ignore-errors (delete-window w))))
(kill-buffer buffer)))
(defun sx-compose--copy-as-kill (buffer _)
"Copy BUFFER contents to the kill-ring."
(when (buffer-live-p buffer)
(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
(sx--split-string (match-string 1) (rx (any space ",;")))))
(defun sx-compose--check-tags ()
"Check if tags in current compose buffer are valid."
(save-excursion
(let ((invalid-tags
(sx-tag--invalid-name-p
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 (format "Following tags don't exist. Create them? %s " invalid-tags))
t))))
;;; Functions to help preparing buffers
(defun sx-compose-create (site parent &optional before-functions after-functions)
"Create an `sx-compose-mode' buffer.
SITE is the site where it will be posted.
If composing questions, PARENT is nil.
If composing answers, it is the `question_id'.
If editing answers or questions, it should be the alist data
related to that object.
Each element of BEFORE-FUNCTIONS and AFTER-FUNCTIONS are
respectively added locally to `sx-compose-before-send-hook' and
`sx-compose-after-send-functions'."
(or (integerp parent) (listp parent)
(error "Invalid PARENT"))
(let ((is-question
(and (listp parent)
(or (null parent)
(cdr (assoc 'title parent))))))
(with-current-buffer (sx-compose--get-buffer-create site parent)
(sx-compose-mode)
(setq sx-compose--site site)
(setq sx-compose--is-question-p is-question)
(setq sx-compose--send-function
(if (consp parent)
(sx-assoc-let parent
(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 .comment_id .answer_id .question_id)
:submethod 'edit)))
(lambda () (sx-method-call 'questions
:auth 'warn
:url-method 'post
:filter sx-browse-filter
:site site
:keywords (sx-compose--generate-keywords is-question)
:id parent
:submethod (if parent 'answers/add 'add)))))
;; Reverse so they're left in the same order.
(dolist (it (reverse before-functions))
(add-hook 'sx-compose-before-send-hook it nil t))
(dolist (it (reverse after-functions))
(add-hook 'sx-compose-after-send-functions it nil t))
(when is-question
(add-hook 'sx-compose-before-send-hook #'sx-compose--check-tags nil t))
;; If the buffer is empty, the draft didn't exist. So prepare the
;; question.
(when (or (string= (buffer-string) "")
(y-or-n-p "Draft buffer exists. Reset it? "))
(let ((inhibit-point-motion-hooks t)
(inhibit-read-only t))
(erase-buffer)
(when (consp parent)
(insert (cdr (assoc 'body_markdown parent))))
(when is-question
(sx-compose--print-question-headers
(when (consp parent) parent))
(unless (consp parent)
(goto-char (point-min))
(goto-char (line-end-position))))))
;; Return the buffer
(current-buffer))))
(defun sx-compose--print-question-headers (question)
"Print question headers for the compose buffer.
If QUESTION is non-nil, fill the headers with the data from
QUESTION."
(sx-assoc-let question
(goto-char (point-min))
(insert
(format sx-compose--question-headers
(or .title "") (mapconcat #'identity .tags " ")))))
(defun sx-compose--generate-keywords (is-question)
"Reading current buffer, generate a keywords alist.
Keywords meant to be used in `sx-method-call'.
`body' is read as the `buffer-string'. If IS-QUESTION is non-nil,
other keywords are read from the header "
(goto-char (point-min))
`(,@(when is-question
(let ((inhibit-point-motion-hooks t)
(header-end
(next-single-property-change
(point-min) 'sx-compose-separator))
keywords)
;; Read the Title.
(unless (search-forward-regexp
"^Title: *\\(.*\\) *$" header-end 'noerror)
(error "No Title header found"))
(push (cons 'title (match-string 1)) keywords)
;; And the tags
(goto-char (point-min))
(unless (search-forward-regexp "^Tags : *\\([^[:space:]].*\\) *$"
header-end 'noerror)
(error "No Tags header found"))
(push (cons 'tags (sx--split-string (match-string 1) "[[:space:],;]"))
keywords)
;; And move past the header so it doesn't get sent.
(goto-char (next-single-property-change
header-end 'sx-compose-separator))
keywords))
(body . ,(buffer-substring-no-properties (point) (point-max)))))
(defun sx-compose--get-buffer-create (site data)
"Get or create a buffer for use with `sx-compose-mode'.
SITE is the site for which composing is aimed (just used to
uniquely identify the buffers).
If DATA is nil, get a fresh compose buffer.
If DATA is an integer, try to find an existing buffer
corresponding to that integer, otherwise create one.
If DATA is an alist (question or answer data), like above but use
the id property."
(cond
((null data)
(generate-new-buffer
(format "*sx draft question %s*" site)))
((integerp data)
(get-buffer-create
(format "*sx draft answer %s %s*"
site data)))
(t
(get-buffer-create
(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
;; Local Variables:
;; indent-tabs-mode: nil
;; End: