oddmuse-curl.el 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097
  1. ;;; oddmuse-curl.el -- edit pages on an Oddmuse wiki using curl
  2. ;;
  3. ;; Copyright (C) 2006–2015 Alex Schroeder <alex@gnu.org>
  4. ;; (C) 2007 rubikitch <rubikitch@ruby-lang.org>
  5. ;;
  6. ;; Latest version:
  7. ;; http://git.savannah.gnu.org/cgit/oddmuse.git/plain/contrib/oddmuse-curl.el
  8. ;; Discussion, feedback:
  9. ;; http://www.emacswiki.org/wiki/OddmuseCurl
  10. ;;
  11. ;; This program is free software: you can redistribute it and/or modify it
  12. ;; under the terms of the GNU General Public License as published by the Free
  13. ;; Software Foundation, either version 3 of the License, or (at your option)
  14. ;; any later version.
  15. ;;
  16. ;; This program is distributed in the hope that it will be useful, but WITHOUT
  17. ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  18. ;; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
  19. ;; more details.
  20. ;;
  21. ;; You should have received a copy of the GNU General Public License along
  22. ;; with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  23. ;;; Commentary:
  24. ;;
  25. ;; A mode to edit pages on Oddmuse wikis using Emacs and the
  26. ;; command-line HTTP client `curl'.
  27. ;;
  28. ;; Since text formatting rules depend on the wiki you're writing for,
  29. ;; the font-locking can only be an approximation.
  30. ;;
  31. ;; Put this file in a directory on your `load-path' and
  32. ;; add this to your init file:
  33. ;; (require 'oddmuse)
  34. ;; (oddmuse-mode-initialize)
  35. ;; And then use M-x oddmuse-edit to start editing.
  36. ;;; Code:
  37. (eval-when-compile
  38. '(progn
  39. (require 'cl)
  40. (require 'sgml-mode)
  41. (require 'skeleton)))
  42. (require 'goto-addr); URL regexp
  43. (require 'info); link face
  44. (require 'shr); preview
  45. (require 'xml); preview munging
  46. ;;; Options
  47. (defcustom oddmuse-directory "~/.emacs.d/oddmuse"
  48. "Directory to store oddmuse pages."
  49. :type '(string)
  50. :group 'oddmuse)
  51. (defcustom oddmuse-wikis
  52. '(("EmacsWiki" "http://www.emacswiki.org/cgi-bin/emacs"
  53. utf-8 "uihnscuskc" nil))
  54. "Alist mapping wiki names to URLs.
  55. The elements in this list are:
  56. NAME, the name of the wiki you provide when calling `oddmuse-edit'.
  57. URL, the base URL of the script used when posting. If the site
  58. uses URL rewriting, then you need to extract the URL from the
  59. edit page. Emacs Wiki, for example, usually shows an URL such as
  60. http://www.emacswiki.org/emacs/Foo, but when you edit the page
  61. and examine the page source, you'll find this:
  62. <form method=\"post\" action=\"http://www.emacswiki.org/cgi-bin/emacs\"
  63. enctype=\"multipart/form-data\" accept-charset=\"utf-8\"
  64. class=\"edit text\">...</form>
  65. Thus, the correct value for URL is
  66. http://www.emacswiki.org/cgi-bin/emacs.
  67. ENCODING, a symbol naming a coding-system.
  68. SECRET, the secret the wiki uses if it has the Question Asker
  69. extension enabled. If you're getting 403 responses (edit denied)
  70. eventhough you can do it from a browser, examine your cookie in
  71. the browser. For Emacs Wiki, for example, my cookie says:
  72. euihnscuskc%251e1%251eusername%251eAlexSchroeder
  73. Use `split-string' and split by \"%251e\" and you'll see that
  74. \"euihnscuskc\" is the odd one out. The parameter name is the
  75. relevant string (its value is always 1).
  76. USERNAME, your optional username to provide. It defaults to
  77. `oddmuse-username'."
  78. :type '(repeat (list (string :tag "Wiki")
  79. (string :tag "URL")
  80. (choice :tag "Coding System"
  81. (const :tag "default" utf-8)
  82. (symbol :tag "specify"
  83. :validate (lambda (widget)
  84. (unless (coding-system-p
  85. (widget-value widget))
  86. (widget-put widget :error
  87. "Not a valid coding system")))))
  88. (choice :tag "Secret"
  89. (const :tag "default" "question")
  90. (string :tag "specify"))
  91. (choice :tag "Username"
  92. (const :tag "default" nil)
  93. (string :tag "specify"))))
  94. :group 'oddmuse)
  95. ;;; Variables
  96. (defvar oddmuse-get-command
  97. "curl --silent %w --form action=browse --form raw=2 --form id='%t'"
  98. "Command to use for publishing pages.
  99. It must print the page to stdout.
  100. See `oddmuse-format-command' for the formatting options.")
  101. (defvar oddmuse-rc-command
  102. "curl --silent %w --form action=rc --form raw=1"
  103. "Command to use for Recent Changes.
  104. It must print the RSS 3.0 text format to stdout.
  105. See `oddmuse-format-command' for the formatting options.")
  106. (defvar oddmuse-search-command
  107. "curl --silent %w --form search='%r' --form raw=1"
  108. "Command to use for searching regular expression.
  109. It must print the RSS 3.0 text format to stdout.
  110. See `oddmuse-format-command' for the formatting options.")
  111. (defvar oddmuse-match-command
  112. "curl --silent %w --form action=index --form match='%r' --form raw=1"
  113. "Command to look for matching pages.
  114. It must print the page names to stdout.
  115. See `oddmuse-format-command' for the formatting options.")
  116. (defvar oddmuse-post-command
  117. (concat "curl --silent --write-out '%{http_code}'"
  118. " --form title='%t'"
  119. " --form summary='%s'"
  120. " --form username='%u'"
  121. " --form pwd='%p'"
  122. " --form %q=1"
  123. " --form recent_edit=%m"
  124. " --form oldtime=%o"
  125. " --form text='<-'"
  126. " '%w'")
  127. "Command to use for publishing pages.
  128. It must accept the page on stdin and print the HTTP status code
  129. on stdout.
  130. See `oddmuse-format-command' for the formatting options.")
  131. (defvar oddmuse-preview-command
  132. (concat "curl --silent"
  133. " --form title='%t'"
  134. " --form username='%u'"
  135. " --form pwd='%p'"
  136. " --form %q=1"
  137. " --form recent_edit=%m"
  138. " --form oldtime=%o"
  139. " --form Preview=Preview"; the only difference
  140. " --form text='<-'"
  141. " '%w'")
  142. "Command to use for previewing pages.
  143. It must accept the page on stdin and print the HTML on stdout.
  144. See `oddmuse-format-command' for the formatting options.")
  145. (defvar oddmuse-get-index-command
  146. "curl --silent %w --form action=index --form raw=1"
  147. "Command to use for publishing index pages.
  148. It must print the page to stdout.
  149. See `oddmuse-format-command' for the formatting options.")
  150. (defvar oddmuse-get-history-command
  151. "curl --silent %w --form action=history --form id=%t --form raw=1"
  152. "Command to use to get the history of a page.
  153. It must print the page to stdout.
  154. See `oddmuse-format-command' for the formatting options.")
  155. (defvar oddmuse-link-pattern
  156. "\\<[[:upper:]]+[[:lower:]]+\\([[:upper:]]+[[:lower:]]*\\)+\\>"
  157. "The pattern used for finding WikiName.")
  158. (defcustom oddmuse-username user-full-name
  159. "Username to use when posting.
  160. Setting a username is the polite thing to do. You can override
  161. this in `oddmuse-wikis'."
  162. :type '(string)
  163. :group 'oddmuse)
  164. (defcustom oddmuse-password ""
  165. "Password to use when posting.
  166. You only need this if you want to edit locked pages and you
  167. know an administrator password."
  168. :type '(string)
  169. :group 'oddmuse)
  170. (defcustom oddmuse-use-always-minor nil
  171. "If set, all edits will be minor edits by default.
  172. This is the default for `oddmuse-minor'."
  173. :type '(boolean)
  174. :group 'oddmuse)
  175. (defvar oddmuse-pages-hash (make-hash-table :test 'equal)
  176. "The wiki-name / pages pairs.
  177. Refresh using \\[oddmuse-reload].")
  178. ;;; Important buffer local variables
  179. (defvar oddmuse-wiki nil
  180. "The current wiki.
  181. Must match a key from `oddmuse-wikis'.")
  182. (defvar oddmuse-page-name nil
  183. "Pagename of the current buffer.")
  184. (defun oddmuse-set-missing-variables (&optional arg)
  185. "Set `oddmuse-wiki' and `oddmuse-page-name', if necessary.
  186. Force a binding of `oddmuse-wiki' if ARG is provided.
  187. Call this function when you're running a command in a buffer that
  188. was not previously connected to a wiki. One example would be
  189. calling `oddmuse-post' on an ordinary file that's not in Oddmuse
  190. Mode."
  191. (when (or (not oddmuse-wiki) arg)
  192. (set (make-local-variable 'oddmuse-wiki)
  193. (oddmuse-read-wiki)))
  194. (when (not oddmuse-page-name)
  195. (set (make-local-variable 'oddmuse-page-name)
  196. (oddmuse-read-pagename oddmuse-wiki t (buffer-name)))))
  197. (defvar oddmuse-minor nil
  198. "Is this edit a minor change?")
  199. (defvar oddmuse-ts nil
  200. "The timestamp of the current page's ancestor.
  201. This is used by Oddmuse to merge changes.")
  202. ;;; Remembering the latest revision of every page
  203. (defvar oddmuse-revisions nil
  204. "An alist to store the current revision we have per page.
  205. An alist wikis containing an alist of pages and revisions.
  206. Example:
  207. ((\"Alex\" ((\"Contact\" . \"58\"))))")
  208. (defvar oddmuse-revision nil
  209. "A variable to bind dynamically when calling `oddmuse-format-command'.")
  210. ;;; Helpers
  211. (defsubst oddmuse-page-name (file)
  212. "Return the page name based on FILE."
  213. (file-name-nondirectory file))
  214. (defsubst oddmuse-wiki (file)
  215. "Return the wiki name based on FILE."
  216. (file-name-nondirectory
  217. (directory-file-name
  218. (file-name-directory file))))
  219. (defmacro with-oddmuse-file (file &rest body)
  220. "Bind `wiki' and `pagename' based on FILE and execute BODY."
  221. (declare (debug (symbolp &rest form)))
  222. `(let ((pagename (oddmuse-page-name ,file))
  223. (wiki (oddmuse-wiki ,file)))
  224. ,@body))
  225. (put 'with-oddmuse-file 'lisp-indent-function 1)
  226. (font-lock-add-keywords 'emacs-lisp-mode '("\\<with-oddmuse-file\\>"))
  227. (defun oddmuse-url (wiki pagename)
  228. "Get the URL of oddmuse wiki."
  229. (condition-case v
  230. (concat (or (cadr (assoc wiki oddmuse-wikis)) (error "Wiki not found in `oddmuse-wikis'")) "/"
  231. (url-hexify-string pagename))
  232. (error nil)))
  233. (defvar oddmuse-pagename-history nil
  234. "History of Oddmuse pages edited.")
  235. (defun oddmuse-read-pagename (wiki &optional require default)
  236. "Read a pagename of WIKI with completion.
  237. Optional arguments REQUIRE and DEFAULT are passed on to `completing-read'.
  238. Typically you would use t and a `oddmuse-page-name', if that makes sense."
  239. (let ((completion-ignore-case t))
  240. (completing-read (if default
  241. (concat "Pagename [" default "]: ")
  242. "Pagename: ")
  243. (oddmuse-make-completion-table wiki)
  244. nil require nil
  245. 'oddmuse-pagename-history default)))
  246. (defvar oddmuse-wiki-history nil
  247. "History of Oddmuse Wikis edited.
  248. This is a list referring to `oddmuse-wikis'.")
  249. (defun oddmuse-read-wiki (&optional require default)
  250. "Read wiki name with completion.
  251. Optional arguments REQUIRE and DEFAULT are passed on to `completing-read'.
  252. Typically you would use t and the current wiki, `oddmuse-wiki'.
  253. If you want to use the current wiki unless the function was
  254. called with C-u. This is what you want for functions that end
  255. users call and that you might want to run on a different wiki
  256. such as searching.
  257. \(let* ((wiki (or (and (not current-prefix-arg) oddmuse-wiki)
  258. (oddmuse-read-wiki))))
  259. ...)
  260. ...)
  261. If you want to ask only when there is no current wiki:
  262. \(let* ((wiki (or oddmuse-wiki (oddmuse-read-wiki)))
  263. ...)
  264. ...)
  265. If you want to ask for a wiki and provide the current one as
  266. default:
  267. \(oddmuse-read-wiki t oddmuse-wiki)"
  268. (let ((completion-ignore-case t))
  269. (completing-read (if default
  270. (concat "Wiki [" default "]: ")
  271. "Wiki: ")
  272. oddmuse-wikis
  273. nil require nil
  274. 'oddmuse-wiki-history default)))
  275. (defun oddmuse-pagename (&optional arg)
  276. "Return the wiki and pagename the user wants to edit or follow.
  277. This cannot be the current pagename! If given the optional
  278. argument ARG, read it from the minibuffer. Otherwise, try to get
  279. a pagename at point. If this does not yield a pagename, ask the
  280. user for a page. Also, if no wiki has been give, ask for that,
  281. too. The pagename returned does not necessarily exist!
  282. Use this function when following links in regular wiki buffers,
  283. in Recent Changes, History Buffers, and also in text files and
  284. the like."
  285. (let* ((wiki (or (and (not arg) oddmuse-wiki)
  286. (oddmuse-read-wiki)))
  287. (pagename (or (and arg (oddmuse-read-pagename wiki))
  288. (oddmuse-pagename-at-point)
  289. (oddmuse-read-pagename wiki nil (word-at-point)))))
  290. (list wiki pagename)))
  291. (defun oddmuse-pagename-if-missing ()
  292. "Return the default wiki and page name or ask for one."
  293. (if (and oddmuse-wiki oddmuse-page-name)
  294. (list oddmuse-wiki oddmuse-page-name)
  295. (oddmuse-pagename)))
  296. (defun oddmuse-current-free-link-contents ()
  297. "The page name in a free link at point.
  298. This returns \"foo\" for [[foo]] and [[foo|bar]]."
  299. (save-excursion
  300. (let* ((pos (point))
  301. (start (when (search-backward "[[" nil t)
  302. (match-end 0)))
  303. (end (when (search-forward "]]" (line-end-position) t)
  304. (match-beginning 0))))
  305. (and start end (>= end pos)
  306. (replace-regexp-in-string
  307. " " "_"
  308. (car (split-string
  309. (buffer-substring-no-properties start end) "|")))))))
  310. (defun oddmuse-pagename-at-point ()
  311. "Page name at point.
  312. It's either a [[free link]] or a WikiWord based on
  313. `oddmuse-current-free-link-contents' or `oddmuse-wikiname-p'."
  314. (let ((pagename (word-at-point)))
  315. (or (oddmuse-current-free-link-contents)
  316. (oddmuse-wikiname-p pagename))))
  317. (defun oddmuse-wikiname-p (pagename)
  318. "Whether PAGENAME is WikiName or not."
  319. (when pagename
  320. (let (case-fold-search)
  321. (when (string-match (concat "^" oddmuse-link-pattern "$") pagename)
  322. pagename))))
  323. ;; (oddmuse-wikiname-p nil)
  324. ;; (oddmuse-wikiname-p "WikiName")
  325. ;; (oddmuse-wikiname-p "not-wikiname")
  326. ;; (oddmuse-wikiname-p "notWikiName")
  327. (defun oddmuse-render-rss3 ()
  328. "Parse current buffer as RSS 3.0 and display it correctly."
  329. (save-excursion
  330. (let (result)
  331. (dolist (item (cdr (split-string (buffer-string) "\n\n")));; skip first item
  332. (let ((data (mapcar (lambda (line)
  333. (when (string-match "^\\(.*?\\): \\(.*\\)" line)
  334. (cons (match-string 1 line)
  335. (match-string 2 line))))
  336. (split-string item "\n"))))
  337. (setq result (cons data result))))
  338. (erase-buffer)
  339. (dolist (item (nreverse result))
  340. (insert "title: " (cdr (assoc "title" item)) "\n"
  341. "version: " (cdr (assoc "revision" item)) "\n"
  342. "generator: " (cdr (assoc "generator" item)) "\n"
  343. "timestamp: " (cdr (assoc "last-modified" item)) "\n\n"
  344. " " (or (cdr (assoc "description" item)) ""))
  345. (fill-paragraph)
  346. (insert "\n\n"))
  347. (goto-char (point-min)))
  348. (view-mode)))
  349. ;;; processing the commands
  350. (defun oddmuse-format-command (command)
  351. "Format COMMAND, replacing placeholders with variables.
  352. %w `url' as provided by `oddmuse-wikis'
  353. %t `pagename'
  354. %s `summary' as provided by the user
  355. %u `username' as provided by `oddmuse-wikis' or `oddmuse-username' if not provided
  356. %m `oddmuse-minor'
  357. %p `oddmuse-password'
  358. %q `question' as provided by `oddmuse-wikis'
  359. %o `oddmuse-ts'
  360. %v `oddmuse-revision'
  361. %r `regexp' as provided by the user"
  362. (dolist (pair '(("%w" . url)
  363. ("%t" . pagename)
  364. ("%s" . summary)
  365. ("%u" . oddmuse-username)
  366. ("%m" . oddmuse-minor)
  367. ("%p" . oddmuse-password)
  368. ("%q" . question)
  369. ("%o" . oddmuse-ts)
  370. ("%v" . oddmuse-revision)
  371. ("%r" . regexp)))
  372. (let* ((key (car pair))
  373. (sym (cdr pair))
  374. value)
  375. (when (boundp sym)
  376. (setq value (symbol-value sym))
  377. (when (eq sym 'oddmuse-minor)
  378. (setq value (if value "on" "off")))
  379. (when (stringp value)
  380. (when (and (eq sym 'summary)
  381. (string-match "'" value))
  382. ;; form summary='A quote is '"'"' this!'
  383. (setq value (replace-regexp-in-string "'" "'\"'\"'" value t t)))
  384. (setq command (replace-regexp-in-string key value command t t))))))
  385. (replace-regexp-in-string "&" "%26" command t t))
  386. (defun oddmuse-run (mesg command wiki &optional pagename buf send-buffer expected-code)
  387. "Print MESG and run COMMAND on the current buffer.
  388. WIKI identifies the entry in `oddmuse-wiki' to be used and
  389. defaults to the variable `oddmuse-wiki'.
  390. PAGENAME is the optional page name to pass to
  391. `oddmuse-format-command' and defaults to the variable
  392. `oddmuse-page-name'.
  393. MESG should be appropriate for the following uses:
  394. \"MESG...\"
  395. \"MESG...done\"
  396. \"MESG failed: REASON\"
  397. Save output in BUF and report an appropriate error. If BUF is
  398. not provided, use the current buffer.
  399. SEND-BUFFER indicates whether the commands needs the content of
  400. the current buffer on STDIN---such as when posting---or whether
  401. it just runs by itself such as when loading a page.
  402. If SEND-BUFFER is not nil, the command output is compared to
  403. EXPECTED-CODE. The command is supposed to print the HTTP status
  404. code on stdout, so usually we want to provide either 302 or 200
  405. as EXPECTED-CODE.
  406. In addition to that, we check the HTML in the buffer for
  407. indications of an error. If we find any, that will get reported
  408. as well."
  409. (let* ((max-mini-window-height 1)
  410. (wiki (or wiki oddmuse-wiki))
  411. (pagename (or pagename oddmuse-page-name))
  412. (wiki-data (or (assoc wiki oddmuse-wikis)
  413. (error "Cannot find data for wiki %s" wiki)))
  414. (url (nth 1 wiki-data))
  415. (coding (nth 2 wiki-data))
  416. (coding-system-for-read coding)
  417. (coding-system-for-write coding)
  418. (question (nth 3 wiki-data))
  419. (oddmuse-username (or (nth 4 wiki-data) oddmuse-username)))
  420. (setq buf (or buf (current-buffer))
  421. command (oddmuse-format-command command))
  422. (message "%s using %s..." mesg command)
  423. (when (numberp expected-code)
  424. (setq expected-code (number-to-string expected-code)))
  425. (if send-buffer
  426. (shell-command-on-region (point-min) (point-max) command buf)
  427. (shell-command command buf))
  428. (let ((status (with-current-buffer buf (buffer-string))))
  429. (cond ((and send-buffer
  430. expected-code
  431. (not (string= expected-code status)))
  432. (error "Error %s: HTTP Status Code %s" mesg status))
  433. ((string-match "<title>Error</title>" status)
  434. (if (string-match "<h1>\\(.*\\)</h1>" status)
  435. (error "Error %s: %s" mesg (match-string 1 status))
  436. (error "Error %s: Cause unknown" status)))
  437. (t
  438. (message "%s...done" mesg))))))
  439. (defun oddmuse-make-completion-table (wiki)
  440. "Create pagename completion table for WIKI.
  441. If available, return precomputed one."
  442. (or (gethash wiki oddmuse-pages-hash)
  443. (oddmuse-reload wiki)))
  444. (defun oddmuse-reload (&optional wiki-arg)
  445. "Really fetch the list of pagenames from WIKI.
  446. This command is used to reflect new pages to `oddmuse-pages-hash'."
  447. (interactive)
  448. (let* ((wiki (or wiki-arg
  449. (oddmuse-read-wiki t oddmuse-wiki)))
  450. (url (cadr (assoc wiki oddmuse-wikis)))
  451. (command (oddmuse-format-command oddmuse-get-index-command))
  452. table)
  453. (message "Getting index of all pages...")
  454. (prog1
  455. (setq table (split-string (shell-command-to-string command)))
  456. (puthash wiki table oddmuse-pages-hash)
  457. (message "Getting index of all pages...done"))))
  458. ;;; Mode and font-locking
  459. (defun oddmuse-mode-initialize ()
  460. (add-to-list 'auto-mode-alist
  461. `(,(expand-file-name oddmuse-directory) . oddmuse-mode)))
  462. (defvar oddmuse-creole-markup
  463. '(("^=[^=\n]+"
  464. 0 '(face info-title-1
  465. help-echo "Creole H1")); = h1
  466. ("^==[^=\n]+"
  467. 0 '(face info-title-2
  468. help-echo "Creole H2")); == h2
  469. ("^===[^=\n]+"
  470. 0 '(face info-title-3
  471. help-echo "Creole H3")); === h3
  472. ("^====+[^=\n]+"
  473. 0 '(face info-title-4
  474. help-echo "Creole H4")); ====h4
  475. ("\\_<//\\(.*\n\\)*?.*?//"
  476. 0 '(face italic
  477. help-echo "Creole italic")); //italic//
  478. ("\\*\\*\\(.*\n\\)*?.*?\\*\\*"
  479. 0 '(face bold
  480. help-echo "Creole bold")); **bold**
  481. ("__\\(.*\n\\)*?.*?__"
  482. 0 '(face underline
  483. help-echo "Creole underline")); __underline__
  484. ("|+=?"
  485. 0 '(face font-lock-string-face
  486. help-echo "Creole table cell"))
  487. ("\\\\\\\\[ \t]+"
  488. 0 '(face font-lock-warning-face
  489. help-echo "Creole line break"))
  490. ("^#+ "
  491. 0 '(face font-lock-constant-face
  492. help-echo "Creole ordered list"))
  493. ("^- "
  494. 0 '(face font-lock-constant-face
  495. help-echo "Creole ordered list"))
  496. ("{{{.*?}}}"
  497. 0 '(face shadow
  498. help-echo "Creole code"))
  499. ("^{{{\\(.*\n\\)+?}}}\n"
  500. 0 '(face shadow
  501. help-echo "Creole multiline code")))
  502. "Implement markup rules for the Creole markup extension.
  503. The rule to identify multiline blocks of code doesn't really work.")
  504. (defvar oddmuse-bbcode-markup
  505. `(("\\[b\\]\\(.*\n\\)*?.*?\\[/b\\]"
  506. 0 '(face bold
  507. help-echo "BB code bold"))
  508. ("\\[i\\]\\(.*\n\\)*?.*?\\[/i\\]"
  509. 0 '(face italic
  510. help-echo "BB code italic"))
  511. ("\\[u\\]\\(.*\n\\)*?.*?\\[/u\\]"
  512. 0 '(face underline
  513. help-echo "BB code underline"))
  514. (,(concat "\\[url=" goto-address-url-regexp "\\]")
  515. 0 '(face font-lock-builtin-face
  516. help-echo "BB code url"))
  517. ("\\[/?\\(img\\|url\\)\\]"
  518. 0 '(face font-lock-builtin-face
  519. help-echo "BB code url or img"))
  520. ("\\[s\\(trike\\)?\\]\\(.*\n\\)*?.*?\\[/s\\(trike\\)?\\]"
  521. 0 '(face strike
  522. help-echo "BB code strike"))
  523. ("\\[/?\\(left\\|right\\|center\\)\\]"
  524. 0 '(face font-lock-constant-face
  525. help-echo "BB code alignment")))
  526. "Implement markup rules for the bbcode markup extension.")
  527. (defvar oddmuse-usemod-markup
  528. '(("^=[^=\n]+=$"
  529. 0 '(face info-title-1
  530. help-echo "Usemod H1"))
  531. ("^==[^=\n]+==$"
  532. 0 '(face info-title-2
  533. help-echo "Usemod H2"))
  534. ("^===[^=\n]+===$"
  535. 0 '(face info-title-3
  536. help-echo "Usemod H3"))
  537. ("^====+[^=\n]+====$"
  538. 0 '(face info-title-4
  539. help-echo "Usemod H4"))
  540. ("\n\n\\( .*\n\\)+"
  541. 0 '(face shadow
  542. font-lock-multiline t
  543. help-echo "Usemod block"))
  544. ("^[#]+ "
  545. 0 '(face font-lock-constant-face
  546. help-echo "Usemod ordered list")))
  547. "Implement markup rules for the Usemod markup extension.
  548. The rule to identify indented blocks of code doesn't really work.")
  549. (defvar oddmuse-usemod-html-markup
  550. '(("<\\(/?[a-z]+\\)"
  551. 1 '(face font-lock-function-name-face
  552. help-echo "Usemod HTML")))
  553. "Implement markup rules for the HTML option in the Usemod markup extension.")
  554. (defvar oddmuse-extended-markup
  555. '(("\\*\\w+[[:word:]-%.,:;\'\"!? ]*\\*"
  556. 0 '(face bold
  557. help-echo "Markup bold"
  558. nobreak t))
  559. ("\\_</\\w+[[:word:]-%.,:;\'\"!? ]*/"
  560. 0 '(face italic
  561. help-echo "Markup italic"
  562. nobreak t))
  563. ("_\\w+[[:word:]-%.,:;\'\"!? ]*_"
  564. 0 '(face underline
  565. help-echo "Markup underline"
  566. nobreak t)))
  567. "Implement markup rules for the Markup extension.")
  568. (defvar oddmuse-basic-markup
  569. `(("\\[\\[.*?\\]\\]"
  570. 0 '(face link
  571. help-echo "Basic free link"))
  572. (,(concat "\\[" goto-address-url-regexp "\\( .+?\\)?\\]")
  573. 0 '(face link
  574. help-echo "Basic external free link"))
  575. ("\\[[[:upper:]]\\S-*:\\S-+ [^]\n]*\\]"
  576. 0 '(face link
  577. help-echo "Basic external interlink with text"))
  578. ("[[:upper:]]\\S-*:\\S-+"
  579. 0 '(face link
  580. help-echo "Basic external interlink"))
  581. (,oddmuse-link-pattern
  582. 0 '(face link
  583. help-echo "Basic wiki name"))
  584. ("^\\([*] \\)"
  585. 0 '(face font-lock-constant-face
  586. help-echo "Basic bullet list")))
  587. "Implement markup rules for the basic Oddmuse setup without extensions.
  588. These rules should come come last because of such basic patterns
  589. as [.*] which are very generic.")
  590. (define-derived-mode oddmuse-mode text-mode "Odd"
  591. "Simple mode to edit wiki pages.
  592. Use \\[oddmuse-follow] to follow links. With prefix, allows you
  593. to specify the target page yourself.
  594. Use \\[oddmuse-post] to post changes. With prefix, allows you to
  595. post the page to a different wiki.
  596. Use \\[oddmuse-edit] to edit a different page. With prefix,
  597. forces a reload of the page instead of just popping to the buffer
  598. if you are already editing the page.
  599. Customize `oddmuse-wikis' to add more wikis to the list.
  600. Font-locking is controlled by `oddmuse-markup-functions'.
  601. \\{oddmuse-mode-map}"
  602. (set (make-local-variable 'oddmuse-minor)
  603. oddmuse-use-always-minor)
  604. (setq indent-tabs-mode nil)
  605. ;; font-locking (case sensitive)
  606. (goto-address)
  607. (setq font-lock-defaults
  608. (list (append oddmuse-basic-markup
  609. oddmuse-bbcode-markup
  610. oddmuse-creole-markup
  611. oddmuse-extended-markup
  612. oddmuse-usemod-markup
  613. oddmuse-usemod-html-markup)))
  614. (font-lock-mode 1)
  615. ;; HTML tags
  616. (set (make-local-variable 'sgml-tag-alist)
  617. `(("b") ("code") ("em") ("i") ("strong") ("nowiki")
  618. ("pre" \n) ("tt") ("u")))
  619. (set (make-local-variable 'skeleton-transformation-function) 'identity)
  620. (make-local-variable 'oddmuse-wiki)
  621. (make-local-variable 'oddmuse-page-name)
  622. (when buffer-file-name
  623. (setq oddmuse-wiki (oddmuse-wiki buffer-file-name)
  624. oddmuse-page-name (oddmuse-page-name buffer-file-name))
  625. ;; set buffer name
  626. (let ((name (concat oddmuse-wiki ":" oddmuse-page-name)))
  627. (unless (equal name (buffer-name)) (rename-buffer name))))
  628. ;; version control
  629. (set (make-local-variable 'oddmuse-ts)
  630. (save-excursion
  631. (goto-char (point-min))
  632. (if (looking-at
  633. "\\([0-9]+\\) # Do not delete this line when editing!\n")
  634. (prog1 (match-string 1)
  635. (replace-match "")
  636. (set-buffer-modified-p nil)))))
  637. ;; filling
  638. (set (make-local-variable 'fill-nobreak-predicate)
  639. '(oddmuse-nobreak-p))
  640. (set (make-local-variable 'font-lock-extra-managed-props)
  641. '(nobreak help-echo)))
  642. ;;; Key bindings
  643. (defun oddmuse-nobreak-p (&optional pos)
  644. "Prevent line break of links.
  645. This depends on the `link' face or the `nobreak' property: if
  646. both the character before and after point have it, don't break."
  647. (if pos
  648. (or (get-text-property pos 'nobreak)
  649. (let ((face (get-text-property pos 'face)))
  650. (if (listp face)
  651. (memq 'link face)
  652. (eq 'link face))))
  653. (and (oddmuse-nobreak-p (point))
  654. (oddmuse-nobreak-p (1- (point))))))
  655. (autoload 'sgml-tag "sgml-mode" t)
  656. (define-key oddmuse-mode-map (kbd "C-c C-b") 'oddmuse-browse-this-page)
  657. (define-key oddmuse-mode-map (kbd "C-c C-c") 'oddmuse-post)
  658. (define-key oddmuse-mode-map (kbd "C-c C-e") 'oddmuse-edit)
  659. (define-key oddmuse-mode-map (kbd "C-c C-f") 'oddmuse-follow)
  660. (define-key oddmuse-mode-map (kbd "C-c C-i") 'oddmuse-insert-pagename)
  661. (define-key oddmuse-mode-map (kbd "C-c C-l") 'oddmuse-match)
  662. (define-key oddmuse-mode-map (kbd "C-c C-m") 'oddmuse-toggle-minor)
  663. (define-key oddmuse-mode-map (kbd "C-c C-n") 'oddmuse-new)
  664. (define-key oddmuse-mode-map (kbd "C-c C-p") 'oddmuse-preview)
  665. (define-key oddmuse-mode-map (kbd "C-c C-r") 'oddmuse-rc)
  666. (define-key oddmuse-mode-map (kbd "C-c C-s") 'oddmuse-search)
  667. (define-key oddmuse-mode-map (kbd "C-c C-t") 'sgml-tag)
  668. ;; This has been stolen from simple-wiki-edit
  669. ;;;###autoload
  670. (defun oddmuse-toggle-minor (&optional arg)
  671. "Toggle minor mode state."
  672. (interactive)
  673. (let ((num (prefix-numeric-value arg)))
  674. (cond
  675. ((or (not arg) (equal num 0))
  676. (setq oddmuse-minor (not oddmuse-minor)))
  677. ((> num 0) (set 'oddmuse-minor t))
  678. ((< num 0) (set 'oddmuse-minor nil)))
  679. (message "Oddmuse Minor set to %S" oddmuse-minor)
  680. oddmuse-minor))
  681. (add-to-list 'minor-mode-alist
  682. '(oddmuse-minor " [MINOR]"))
  683. ;;;###autoload
  684. (defun oddmuse-insert-pagename (pagename)
  685. "Insert a PAGENAME of current wiki with completion.
  686. Replaces _ with spaces again."
  687. (interactive (list (oddmuse-read-pagename oddmuse-wiki)))
  688. (insert (replace-regexp-in-string "_" " " pagename)))
  689. ;;; Major functions
  690. (defun oddmuse-get-latest-revision (wiki pagename)
  691. "Return the latest revision as a string, eg. \"5\".
  692. Requires all the variables to be bound for
  693. `oddmuse-format-command'."
  694. ;; Since we don't know the most recent revision we have to fetch it
  695. ;; from the server every time.
  696. (with-temp-buffer
  697. (oddmuse-run "Determining latest revision" oddmuse-get-history-command wiki pagename)
  698. (if (re-search-forward "^revision: \\([0-9]+\\)$" nil t)
  699. (prog1 (match-string 1)
  700. (message "Determining latest revision...done"))
  701. (message "This is a new page")
  702. "new")))
  703. ;;;###autoload
  704. (defun oddmuse-edit (wiki pagename)
  705. "Edit a page on a wiki.
  706. WIKI is the name of the wiki as defined in `oddmuse-wikis',
  707. PAGENAME is the pagename of the page you want to edit. If the
  708. page is already in a buffer, pop to that buffer instead of
  709. loading the page Use a prefix argument to force a reload of the
  710. page. Use \\[oddmuse-reload] to reload the list of pages
  711. available if you changed the URL in `oddmuse-wikis' or if other
  712. people have been editing the wiki in the mean time."
  713. (interactive (oddmuse-pagename))
  714. (make-directory (concat oddmuse-directory "/" wiki) t)
  715. (let ((name (concat wiki ":" pagename)))
  716. (if (and (get-buffer name)
  717. (not current-prefix-arg))
  718. (pop-to-buffer (get-buffer name))
  719. ;; insert page content from the wiki
  720. (set-buffer (get-buffer-create name))
  721. (erase-buffer); in case of current-prefix-arg
  722. (oddmuse-run "Loading" oddmuse-get-command wiki pagename)
  723. (setq buffer-file-name (concat oddmuse-directory "/" wiki "/" pagename))
  724. (vc-working-revision buffer-file-name 'oddmuse)
  725. ;; check for a diff (this ends with display-buffer) and bury the
  726. ;; buffer if there are no hunks
  727. (when (file-exists-p buffer-file-name)
  728. (diff-buffer-with-file)
  729. (with-current-buffer (get-buffer "*Diff*")
  730. (unless (next-property-change (point-min))
  731. (kill-buffer))))
  732. ;; this also changes the buffer name
  733. (basic-save-buffer)
  734. ;; this makes sure that the buffer name is set correctly
  735. (oddmuse-mode)
  736. ;; fix mode-line for VC in the new buffer because this is not a vc-checkout
  737. (vc-mode-line buffer-file-name 'oddmuse))))
  738. (defalias 'oddmuse-go 'oddmuse-edit)
  739. ;;;###autoload
  740. (defun oddmuse-new (wiki pagename)
  741. "Create a new page on a wiki.
  742. WIKI is the name of the wiki as defined in `oddmuse-wikis'.
  743. The pagename begins with the current date."
  744. (interactive
  745. (list (or (and (not current-prefix-arg) oddmuse-wiki)
  746. (oddmuse-read-wiki))
  747. (replace-regexp-in-string
  748. " +" "_"
  749. (read-from-minibuffer "Pagename: "
  750. (format-time-string "%Y-%m-%d ")))))
  751. (oddmuse-edit wiki pagename))
  752. (autoload 'word-at-point "thingatpt")
  753. ;;;###autoload
  754. (defun oddmuse-follow (wiki pagename)
  755. "Figure out what page we need to visit
  756. and call `oddmuse-edit' on it."
  757. (interactive (oddmuse-pagename))
  758. (oddmuse-edit wiki pagename))
  759. ;;;###autoload
  760. (defun oddmuse-post (summary)
  761. "Post the current buffer to the current wiki.
  762. The current wiki is taken from `oddmuse-wiki'.
  763. Use a prefix argument to override this."
  764. (interactive "sSummary: ")
  765. (oddmuse-set-missing-variables current-prefix-arg)
  766. (let ((list (gethash oddmuse-wiki oddmuse-pages-hash)))
  767. (when (not (member oddmuse-page-name list))
  768. (puthash oddmuse-wiki (cons oddmuse-page-name list) oddmuse-pages-hash)))
  769. (and buffer-file-name (basic-save-buffer))
  770. (oddmuse-run "Posting" oddmuse-post-command nil nil
  771. (get-buffer-create " *oddmuse-response*") t 302)
  772. ;; force reload
  773. (vc-file-setprop buffer-file-name 'vc-working-revision
  774. (oddmuse-get-latest-revision oddmuse-wiki oddmuse-page-name))
  775. ;; fix mode-line for VC in the new buffer because this is not a vc-checkout
  776. (vc-mode-line buffer-file-name 'oddmuse))
  777. ;;;###autoload
  778. (defun oddmuse-preview (&optional arg)
  779. "Preview the current buffer for the current wiki.
  780. The current wiki is taken from `oddmuse-wiki'.
  781. Use a prefix argument to view the preview using an external
  782. browser."
  783. (interactive "P")
  784. (oddmuse-set-missing-variables)
  785. (let ((buf (get-buffer-create " *oddmuse-response*")))
  786. (and buffer-file-name (basic-save-buffer))
  787. (oddmuse-run "Previewing" oddmuse-preview-command nil nil buf t)
  788. (if arg
  789. (with-current-buffer buf
  790. (let ((file (make-temp-file "oddmuse-preview-" nil ".html")))
  791. (write-region (point-min) (point-max) file)
  792. (browse-url (browse-url-file-url file))))
  793. (message "Rendering...")
  794. (pop-to-buffer "*Preview*")
  795. (fundamental-mode)
  796. (erase-buffer)
  797. (shr-insert-document
  798. (with-current-buffer buf
  799. (let ((html (libxml-parse-html-region (point-min) (point-max))))
  800. (oddmuse-find-node
  801. (lambda (node)
  802. (and (eq (xml-node-name node) 'div)
  803. (string= (xml-get-attribute node 'class) "preview")))
  804. html))))
  805. (goto-char (point-min))
  806. (kill-buffer buf);; prevent it from showing up after q
  807. (view-mode)
  808. (message "Rendering...done"))))
  809. (defun oddmuse-find-node (test node)
  810. "Return the child of NODE that satisfies TEST.
  811. TEST is a function that takes a node as an argument. NODE is a
  812. node as returned by `libxml-parse-html-region' or
  813. `xml-parse-region'. The function recurses through the node tree."
  814. (if (funcall test node)
  815. node
  816. (dolist (child (xml-node-children node))
  817. (when (listp child)
  818. (let ((result (oddmuse-find-node test child)))
  819. (when result
  820. (return result)))))))
  821. ;;;###autoload
  822. (defun oddmuse-search (regexp)
  823. "Search the wiki for REGEXP.
  824. REGEXP must be a regular expression understood by the
  825. wiki (ie. it must use Perl syntax).
  826. Use a prefix argument to search a different wiki."
  827. (interactive "sSearch term: ")
  828. (let* ((wiki (or (and (not current-prefix-arg) oddmuse-wiki)
  829. (oddmuse-read-wiki)))
  830. (name (concat "*" wiki ": search for '" regexp "'*")))
  831. (if (and (get-buffer name)
  832. (not current-prefix-arg))
  833. (pop-to-buffer (get-buffer name))
  834. (set-buffer (get-buffer-create name))
  835. (erase-buffer)
  836. (oddmuse-run "Searching" oddmuse-search-command wiki)
  837. (oddmuse-rc-buffer)
  838. (dolist (re (split-string regexp))
  839. (highlight-regexp (hi-lock-process-phrase re)))
  840. (set (make-local-variable 'oddmuse-wiki) wiki))))
  841. ;;;###autoload
  842. (defun oddmuse-match (regexp)
  843. "Search the wiki for page names matching REGEXP.
  844. REGEXP must be a regular expression understood by the
  845. wiki (ie. it must use Perl syntax).
  846. Use a prefix argument to search a different wiki."
  847. (interactive "sPages matching: ")
  848. (let* ((wiki (or (and (not current-prefix-arg) oddmuse-wiki)
  849. (oddmuse-read-wiki)))
  850. (name (concat "*" wiki ": matches for '" regexp "'*")))
  851. (if (and (get-buffer name)
  852. (not current-prefix-arg))
  853. (pop-to-buffer (get-buffer name))
  854. (set-buffer (get-buffer-create name))
  855. (erase-buffer)
  856. (oddmuse-run "Searching" oddmuse-match-command wiki)
  857. (let ((lines (split-string (buffer-string) "\n" t)))
  858. (erase-buffer)
  859. (dolist (line lines)
  860. (insert "[[" (replace-regexp-in-string "_" " " line) "]]\n")))
  861. (oddmuse-mode)
  862. (set (make-local-variable 'oddmuse-wiki) wiki)
  863. (display-buffer (current-buffer)))))
  864. ;;;###autoload
  865. (defun oddmuse-rc (&optional include-minor-edits)
  866. "Show Recent Changes.
  867. With universal argument, reload."
  868. (interactive "P")
  869. (let* ((wiki (or (and (not current-prefix-arg) oddmuse-wiki)
  870. (oddmuse-read-wiki)))
  871. (name (concat "*" wiki ": recent changes*")))
  872. (if (and (get-buffer name) (not current-prefix-arg))
  873. (pop-to-buffer (get-buffer name))
  874. (set-buffer (get-buffer-create name))
  875. (erase-buffer)
  876. (oddmuse-run "Load recent changes" oddmuse-rc-command wiki)
  877. (oddmuse-rc-buffer)
  878. ;; set local variable after `oddmuse-mode' killed them
  879. (set (make-local-variable 'oddmuse-wiki) wiki))))
  880. (defun oddmuse-rc-buffer ()
  881. "Parse current buffer as RSS 3.0 and display it correctly."
  882. (let ((result nil)
  883. (fill-column (window-width))
  884. (fill-prefix " "))
  885. (dolist (item (cdr (split-string (buffer-string) "\n\n" t)));; skip first item
  886. (let ((data (mapcar (lambda (line)
  887. (when (string-match "^\\(.*?\\): \\(.*\\)" line)
  888. (cons (intern (match-string 1 line))
  889. (match-string 2 line))))
  890. (split-string item "\n" t))))
  891. (setq result (cons data result))))
  892. (erase-buffer)
  893. (dolist (item (nreverse result))
  894. (let ((title (cdr (assq 'title item)))
  895. (generator (cdr (assq 'generator item)))
  896. (description (cdr (assq 'description item)))
  897. (minor (cdr (assq 'minor item))))
  898. (insert "[[" title "]] – "
  899. (propertize generator 'font-lock-face 'shadow))
  900. (when minor
  901. (insert " [minor]"))
  902. (newline)
  903. (when description
  904. (save-restriction
  905. (narrow-to-region (point) (point))
  906. (insert fill-prefix description)
  907. (fill-paragraph))
  908. (newline))))
  909. (goto-char (point-min))
  910. (oddmuse-mode)))
  911. (defun oddmuse-history (wiki pagename)
  912. "Show the history for PAGENAME on WIKI.
  913. Compared to `vc-oddmuse-print-log' this only prints the revisions
  914. that can actually be retrieved (for diff and rollback)."
  915. (interactive (oddmuse-pagename-if-missing))
  916. (let ((name (concat "*" wiki ": history for " pagename "*")))
  917. (if (and (get-buffer name)
  918. (not current-prefix-arg))
  919. (pop-to-buffer (get-buffer name))
  920. (set-buffer (get-buffer-create name))
  921. (erase-buffer)
  922. (oddmuse-run "History" oddmuse-get-history-command wiki pagename)
  923. (oddmuse-mode)
  924. (set (make-local-variable 'oddmuse-wiki) wiki))))
  925. ;;;###autoload
  926. (defun emacswiki-post (&optional pagename summary)
  927. "Post the current buffer to the EmacsWiki.
  928. If this command is invoked interactively: with prefix argument,
  929. prompts for pagename, otherwise set pagename as basename of
  930. `buffer-file-name'.
  931. This command is intended to post current EmacsLisp program easily."
  932. (interactive)
  933. (let* ((oddmuse-wiki "EmacsWiki")
  934. (oddmuse-page-name (or pagename
  935. (and (not current-prefix-arg)
  936. buffer-file-name
  937. (file-name-nondirectory buffer-file-name))
  938. (oddmuse-read-pagename oddmuse-wiki)))
  939. (summary (or summary (read-string "Summary: "))))
  940. (oddmuse-post summary)))
  941. ;;;###autoload
  942. (defun oddmuse-browse-page (wiki pagename)
  943. "Ask a WWW browser to load an Oddmuse page.
  944. WIKI is the name of the wiki as defined in `oddmuse-wikis',
  945. PAGENAME is the pagename of the page you want to browse."
  946. (interactive (oddmuse-pagename))
  947. (browse-url (oddmuse-url wiki pagename)))
  948. ;;;###autoload
  949. (defun oddmuse-browse-this-page ()
  950. "Ask a WWW browser to load current oddmuse page."
  951. (interactive)
  952. (oddmuse-browse-page oddmuse-wiki oddmuse-page-name))
  953. ;;;###autoload
  954. (defun oddmuse-kill-url ()
  955. "Make the URL of current oddmuse page the latest kill in the kill ring."
  956. (interactive)
  957. (kill-new (oddmuse-url oddmuse-wiki oddmuse-page-name)))
  958. (provide 'oddmuse-curl)
  959. ;;; oddmuse-curl.el ends here