├── README.org
├── metaweblog-pkg.el
├── metaweblog.el
└── test-metaweblog.el
/README.org:
--------------------------------------------------------------------------------
1 | #+TITLE: MetaWeblog
2 |
3 | [[https://github.com/org2blog/org2blog/blob/master/metaweblog.el][MetaWeblog]] is now stored in the [[https://github.com/org2blog/org2blog][Org2Blog repo.]]
4 |
--------------------------------------------------------------------------------
/metaweblog-pkg.el:
--------------------------------------------------------------------------------
1 | (define-package "metaweblog" "1.0.1"
2 | "An emacs library to access metaweblog based weblogs"
3 | '((xml-rpc "1.6.8")))
4 |
--------------------------------------------------------------------------------
/metaweblog.el:
--------------------------------------------------------------------------------
1 | ;;; metaweblog.el --- an emacs library to access metaweblog based weblogs
2 | ;; Copyright (C) 2008 Ashish Shukla
3 | ;; Copyright (C) 2010 Puneeth Chaganti
4 |
5 | ;; This program is free software: you can redistribute it and/or modify
6 | ;; it under the terms of the GNU General Public License as published by
7 | ;; the Free Software Foundation, either version 3 of the License, or
8 | ;; (at your option) any later version.
9 |
10 | ;; This program is distributed in the hope that it will be useful,
11 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | ;; GNU General Public License for more details.
14 |
15 | ;; You should have received a copy of the GNU General Public License
16 | ;; along with this program. If not, see .
17 |
18 | (require 'xml-rpc)
19 |
20 | (defconst metaweblog-version "1.0.1"
21 | "Current version of metaweblog.el")
22 |
23 | (defun metaweblog-get-categories (blog-xmlrpc user-name password blog-id)
24 | "Retrieves list of categories from the weblog system"
25 | (xml-rpc-method-call blog-xmlrpc
26 | "metaWeblog.getCategories"
27 | blog-id
28 | user-name
29 | password))
30 |
31 | (defun wp-new-category (blog-xmlrpc user-name password blog-id category)
32 | "Create new category on the weblog"
33 | (xml-rpc-method-call blog-xmlrpc
34 | "wp.newCategory"
35 | blog-id
36 | user-name
37 | password
38 | `(("name" . ,category))))
39 |
40 | (defun wp-get-tags (blog-xmlrpc user-name password blog-id)
41 | "Retrieves list of tags from the weblog system. Uses wp.getTags"
42 | (xml-rpc-method-call blog-xmlrpc
43 | "wp.getTags"
44 | blog-id
45 | user-name
46 | password))
47 |
48 | (defun wp-get-pages (blog-xmlrpc user-name password blog-id)
49 | "Retrieves list of pages from the weblog system. Uses wp.getPages."
50 | (xml-rpc-method-call blog-xmlrpc
51 | "wp.getPages"
52 | blog-id
53 | user-name
54 | password))
55 |
56 | (defun wp-get-pagelist (blog-xmlrpc user-name password blog-id)
57 | "Retrieves list of pages (minimal information) from the weblog
58 | system. Uses wp.getPageList."
59 | (xml-rpc-method-call blog-xmlrpc
60 | "wp.getPageList"
61 | blog-id
62 | user-name
63 | password))
64 |
65 | (defun metaweblog-new-post
66 | (blog-xmlrpc user-name password blog-id content publish)
67 | "Sends a new post to the blog. If PUBLISH is non-nil, the post is
68 | published, otherwise it is saved as draft. CONTENT will be an alist
69 | title, description, categories, and date as keys (string-ified) mapped to the
70 | title of the post, post contents, list of categories, and date respectively."
71 | (let ((post-title (cdr (assoc "title" content)))
72 | (post-description (cdr (assoc "description" content)))
73 | (post-categories (cdr (assoc "categories" content)))
74 | (post-tags (cdr (assoc "tags" content)))
75 | (post-excerpt (cdr (assoc "excerpt" content)))
76 | (post-permalink (cdr (assoc "permalink" content)))
77 | (post-date (cdr (assoc "date" content))))
78 | ;;; since xml-rpc-method-call entitifies the HTML text in the post
79 | ;;; we've to use raw
80 | (xml-rpc-xml-to-response (xml-rpc-request
81 | blog-xmlrpc
82 | `((methodCall
83 | nil
84 | (methodName nil "metaWeblog.newPost")
85 | (params nil
86 | (param nil (value nil (string nil ,blog-id)))
87 | (param nil (value nil (string nil ,user-name)))
88 | (param nil (value nil (string nil ,password)))
89 | (param nil (value nil
90 | (struct
91 | nil
92 | (member nil
93 | (name nil "title")
94 | (value nil ,post-title))
95 | (member nil
96 | (name nil "description")
97 | (value nil ,post-description))
98 | (member nil
99 | (name nil "mt_excerpt")
100 | (value nil ,post-excerpt))
101 | (member nil
102 | (name nil "wp_slug")
103 | (value nil ,post-permalink))
104 | (member nil
105 | (name nil "dateCreated")
106 | (dateTime.iso8601 nil ,post-date))
107 | ,(when post-tags
108 | `(member nil
109 | (name nil "mt_keywords")
110 | (value nil
111 | (array
112 | nil
113 | ,(append
114 | '(data nil)
115 | (mapcar
116 | (lambda(f)
117 | `(value nil (string nil ,f)))
118 | post-tags))))))
119 | ,(when post-categories
120 | `(member nil
121 | (name nil "categories")
122 | (value nil
123 | (array
124 | nil
125 | ,(append
126 | '(data nil)
127 | (mapcar
128 | (lambda(f)
129 | `(value nil (string nil ,f)))
130 | post-categories)))))))))
131 | (param nil (value nil (boolean nil ,(if publish "1" "0")))))))))))
132 |
133 | (defun wp-new-page
134 | (blog-xmlrpc user-name password blog-id content publish)
135 | "Sends a new page to the blog. If PUBLISH is non-nil, the post is
136 | published, otherwise it is saved as draft. CONTENT will be an alist
137 | title, description, categories, and date as keys (string-ified) mapped to the
138 | title of the post, post contents, list of categories, and date respectively."
139 | (let ((post-title (cdr (assoc "title" content)))
140 | (post-description (cdr (assoc "description" content)))
141 | (post-categories (cdr (assoc "categories" content)))
142 | (post-tags (cdr (assoc "tags" content)))
143 | (post-excerpt (cdr (assoc "excerpt" content)))
144 | (post-permalink (cdr (assoc "permalink" content)))
145 | (post-parent (cdr (assoc "parent" content)))
146 | (post-date (cdr (assoc "date" content))))
147 | ;;; since xml-rpc-method-call entitifies the HTML text in the post
148 | ;;; we've to use raw
149 | (xml-rpc-xml-to-response (xml-rpc-request
150 | blog-xmlrpc
151 | `((methodCall
152 | nil
153 | (methodName nil "wp.newPage")
154 | (params nil
155 | (param nil (value nil (string nil ,blog-id)))
156 | (param nil (value nil (string nil ,user-name)))
157 | (param nil (value nil (string nil ,password)))
158 | (param nil (value nil
159 | (struct
160 | nil
161 | (member nil
162 | (name nil "title")
163 | (value nil ,post-title))
164 | (member nil
165 | (name nil "description")
166 | (value nil ,post-description))
167 | (member nil
168 | (name nil "mt_excerpt")
169 | (value nil ,post-excerpt))
170 | (member nil
171 | (name nil "wp_slug")
172 | (value nil ,post-permalink))
173 | (member nil
174 | (name nil "wp_page_parent_id")
175 | (value nil ,post-parent))
176 | (member nil
177 | (name nil "dateCreated")
178 | (dateTime.iso8601 nil ,post-date))
179 | ,(when post-tags
180 | `(member nil
181 | (name nil "mt_keywords")
182 | (value nil
183 | (array
184 | nil
185 | ,(append
186 | '(data nil)
187 | (mapcar
188 | (lambda(f)
189 | `(value nil (string nil ,f)))
190 | post-tags))))))
191 | ,(when post-categories
192 | `(member nil
193 | (name nil "categories")
194 | (value nil
195 | (array
196 | nil
197 | ,(append
198 | '(data nil)
199 | (mapcar
200 | (lambda(f)
201 | `(value nil (string nil ,f)))
202 | post-categories)))))))))
203 | (param nil (value nil (boolean nil ,(if publish "1" "0")))))))))))
204 |
205 | (defun wp-edit-page
206 | (blog-xmlrpc user-name password blog-id post-id content publish)
207 | "Edits an existing page on the blog. If PUBLISH is non-nil, the
208 | post is published, otherwise it is saved as draft. CONTENT will
209 | be an alist title, description, categories, and date as
210 | keys (string-ified) mapped to the title of the post, post
211 | contents, list of categories, and date respectively."
212 | (let ((post-title (cdr (assoc "title" content)))
213 | (post-description (cdr (assoc "description" content)))
214 | (post-categories (cdr (assoc "categories" content)))
215 | (post-tags (cdr (assoc "tags" content)))
216 | (post-excerpt (cdr (assoc "excerpt" content)))
217 | (post-permalink (cdr (assoc "permalink" content)))
218 | (post-parent (cdr (assoc "parent" content)))
219 | (post-date (cdr (assoc "date" content))))
220 | (message post-date)
221 | ;;; since xml-rpc-method-call entitifies the HTML text in the post
222 | ;;; we've to use raw
223 | (xml-rpc-xml-to-response
224 | (xml-rpc-request
225 | blog-xmlrpc
226 | `((methodCall
227 | nil
228 | (methodName nil "wp.editPage")
229 | (params nil
230 | (param nil (value nil (string nil ,blog-id)))
231 | (param nil (value nil (string nil ,post-id)))
232 | (param nil (value nil (string nil ,user-name)))
233 | (param nil (value nil (string nil ,password)))
234 | (param nil (value nil
235 | (struct
236 | nil
237 | (member nil
238 | (name nil "title")
239 | (value nil ,post-title))
240 | (member nil
241 | (name nil "description")
242 | (value nil ,post-description))
243 | (member nil
244 | (name nil "mt_excerpt")
245 | (value nil ,post-excerpt))
246 | (member nil
247 | (name nil "wp_slug")
248 | (value nil ,post-permalink))
249 | (member nil
250 | (name nil "wp_page_parent_id")
251 | (value nil ,post-parent))
252 | (member nil
253 | (name nil "dateCreated")
254 | (dateTime.iso8601 nil ,post-date))
255 | ,(when post-tags
256 | `(member nil
257 | (name nil "mt_keywords")
258 | (value nil
259 | (array
260 | nil
261 | ,(append
262 | '(data nil)
263 | (mapcar
264 | (lambda(f)
265 | `(value nil (string nil ,f)))
266 | post-tags))))))
267 | ,(when post-categories
268 | `(member nil
269 | (name nil "categories")
270 | (value nil
271 | (array
272 | nil
273 | ,(append
274 | '(data nil)
275 | (mapcar
276 | (lambda(f)
277 | `(value nil (string nil ,f)))
278 | post-categories)))))))))
279 | (param nil (value nil (boolean nil ,(if publish "1" "0")))))))))))
280 |
281 | (defun metaweblog-edit-post
282 | (blog-xmlrpc user-name password post-id content publish)
283 | "Edits an exiting post, if post-id is given. If PUBLISH is non-nil, the
284 | post is published, otherwise it is saved as draft. CONTENT will be an alist
285 | title, description, categories, and date as keys (string-ified) mapped to the
286 | title of the post, post contents, list of categories, and date respectively."
287 | (let ((post-title (cdr (assoc "title" content)))
288 | (post-description (cdr (assoc "description" content)))
289 | (post-categories (cdr (assoc "categories" content)))
290 | (post-tags (cdr (assoc "tags" content)))
291 | (post-excerpt (cdr (assoc "excerpt" content)))
292 | (post-permalink (cdr (assoc "permalink" content)))
293 | (post-date (cdr (assoc "date" content))))
294 | (message post-date)
295 | ;;; since xml-rpc-method-call entitifies the HTML text in the post
296 | ;;; we've to use raw
297 | (xml-rpc-xml-to-response (xml-rpc-request
298 | blog-xmlrpc
299 | `((methodCall
300 | nil
301 | (methodName nil "metaWeblog.editPost")
302 | (params nil
303 | (param nil (value nil (string nil ,post-id)))
304 | (param nil (value nil (string nil ,user-name)))
305 | (param nil (value nil (string nil ,password)))
306 | (param nil (value nil
307 | (struct
308 | nil
309 | (member nil
310 | (name nil "title")
311 | (value nil ,post-title))
312 | (member nil
313 | (name nil "description")
314 | (value nil ,post-description))
315 | (member nil
316 | (name nil "mt_excerpt")
317 | (value nil ,post-excerpt))
318 | (member nil
319 | (name nil "wp_slug")
320 | (value nil ,post-permalink))
321 | (member nil
322 | (name nil "dateCreated")
323 | (dateTime.iso8601 nil ,post-date))
324 | ,(when post-tags
325 | `(member nil
326 | (name nil "mt_keywords")
327 | (value nil
328 | (array
329 | nil
330 | ,(append
331 | '(data nil)
332 | (mapcar
333 | (lambda(f)
334 | `(value nil (string nil ,f)))
335 | post-tags))))))
336 | ,(when post-categories
337 | `(member nil
338 | (name nil "categories")
339 | (value nil
340 | (array
341 | nil
342 | ,(append
343 | '(data nil)
344 | (mapcar
345 | (lambda(f)
346 | `(value nil (string nil ,f)))
347 | post-categories)))))))))
348 | (param nil (value nil (boolean nil ,(if publish "1" "0")))))))))))
349 |
350 | (defun metaweblog-get-post (blog-xmlrpc user-name password post-id)
351 | "Retrieves a post from the weblog. POST-ID is the id of the post
352 | which is to be returned. Can be used with pages as well."
353 | (xml-rpc-method-call blog-xmlrpc
354 | "metaWeblog.getPost"
355 | post-id
356 | user-name
357 | password))
358 |
359 | (defun metaweblog-delete-post (blog-xmlrpc user-name password post-id)
360 | "Delete an entry from the weblog system."
361 | (xml-rpc-method-call blog-xmlrpc
362 | "blogger.deletePost"
363 | nil
364 | post-id
365 | user-name
366 | password
367 | t))
368 |
369 | (defun wp-delete-page (blog-xmlrpc blog-id user-name password page-id)
370 | "Delete a page from the weblog system."
371 | (xml-rpc-method-call blog-xmlrpc
372 | "wp.deletePage"
373 | blog-id
374 | user-name
375 | password
376 | page-id))
377 |
378 | (defun metaweblog-get-recent-posts(blog-xmlrpc blog-id user-name password number-of-posts)
379 | "Fetches the recent posts from the weblog. NUMBER-OF-POSTS is the
380 | no. of posts that should be returned."
381 | (xml-rpc-method-call blog-xmlrpc
382 | "metaWeblog.getRecentPosts"
383 | blog-id
384 | user-name
385 | password
386 | number-of-posts))
387 |
388 | (defun get-file-properties (file)
389 | "Gets the properties of a file. Returns an assoc list with
390 | name - file name
391 | bits - data of the file as a base64 encoded string
392 | type - mimetype of file deduced from extension."
393 | (let* (base64-str type name)
394 | (save-excursion
395 | (save-restriction
396 | (with-current-buffer (find-file-noselect file nil t)
397 | (fundamental-mode)
398 | (setq name (file-name-nondirectory file))
399 | (setq base64-str (base64-encode-string (encode-coding-string (buffer-string) 'binary)))
400 | (setq type (mailcap-extension-to-mime (or (file-name-extension file) "")))
401 | (kill-buffer)
402 | (setq file-props `(("name" . ,name)
403 | ("bits" . ,base64-str)
404 | ("type" . ,type))))))
405 | file-props))
406 |
407 | (defun metaweblog-upload-file (blog-xmlrpc user-name password blog-id file)
408 | "Uploads file to the blog. FILE will be an alist name, type,
409 | bits, as keys mapped to name of the file, mime type and the
410 | data."
411 | (let ((file-name (cdr (assoc "name" file)))
412 | (file-type (cdr (assoc "type" file)))
413 | (file-bits (cdr (assoc "bits" file))))
414 |
415 | (xml-rpc-xml-to-response
416 | (xml-rpc-request
417 | blog-xmlrpc
418 | `((methodCall
419 | nil
420 | (methodName nil "metaWeblog.newMediaObject")
421 | (params nil
422 | (param nil (value nil (string nil ,blog-id)))
423 | (param nil (value nil (string nil ,user-name)))
424 | (param nil (value nil (string nil ,password)))
425 | (param nil (value nil
426 | (struct
427 | nil
428 | (member nil
429 | (name nil "name")
430 | (value nil ,file-name))
431 | (member nil
432 | (name nil "bits")
433 | (base64 nil ,file-bits))
434 | (member nil
435 | (name nil "type")
436 | (value nil ,file-type))
437 | (member nil
438 | (name nil "overwrite")
439 | (value nil "t")))))
440 | )))))))
441 |
442 |
443 | (provide 'metaweblog)
444 |
--------------------------------------------------------------------------------
/test-metaweblog.el:
--------------------------------------------------------------------------------
1 | ;;; test-metaweblog.el --- tests for metaweblog.el
2 | ;; Copyright (C) 2012 Puneeth Chaganti
3 |
4 | (require 'metaweblog)
5 |
6 | ;;;;; Test env-setup ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
7 |
8 | (setq blog-xmlrpc "http://localhost/xmlrpc.php"
9 | blog-user "admin"
10 | blog-pass "test123"
11 | blog-id "1")
12 |
13 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
14 | ; Util functions
15 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
16 |
17 | (defun random-string (length)
18 | "Return a random string of given length"
19 | (let ((alpha "abcdefghijklmnopqrstuvwxyz")
20 | (char-list))
21 | (dotimes (char length)
22 | (setq char-list (cons (string (elt "abcdefghijklmnopqrstuvwxyz" (random 25))) char-list)))
23 | (mapconcat 'identity char-list "")))
24 |
25 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
26 | ; Tests
27 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
28 |
29 | (ert-deftest test-new-category ()
30 | "Test if creating a new category works."
31 | (let* ((category (random-string 8))
32 | ;; Add new category
33 | (category-id (wp-new-category blog-xmlrpc blog-user blog-pass blog-id category)))
34 | ;; List all categories and check if our category is present that list
35 | (setq categories (metaweblog-get-categories blog-xmlrpc blog-user blog-pass blog-id))
36 | (dolist (cat categories)
37 | (if (equal (cdr (assoc "categoryId" cat)) category-id)
38 | (should (equal (cdr (assoc "categoryName" cat)) category))))))
39 |
40 | (ert-deftest test-new-tag ()
41 | "Test if getting tags works. This test does nothing, just
42 | checks if Wordpress version has the API.
43 | FIXME: Make this a real test ..."
44 | (let* ()
45 | (setq tag-list (wp-get-tags blog-xmlrpc blog-user blog-pass blog-id))))
46 |
47 | (ert-deftest test-pages ()
48 | "Test if creating, listing, fetching content and deleting pages works."
49 | (let* ((content '(("date" . "20120817T18:30:00+0000")
50 | ("title" . "Hello World")
51 | ("tags" "org2blog" "emacs")
52 | ("categories" "org2blog" "emacs")
53 | ("post-id")
54 | ("parent" . "0")
55 | ("excerpt" . "")
56 | ("permalink" . "")
57 | ("description" . "
Test content.
")))
58 |
59 | ;; Post a new page
60 | (page-id (wp-new-page blog-xmlrpc blog-user blog-pass blog-id content nil))
61 | (fetched-page (metaweblog-get-post blog-xmlrpc blog-user blog-pass page-id))
62 | (new-content "New Content
"))
63 |
64 | ;; Check if fetched-page's content and posted content match
65 | (should (equal (cdr (assoc "description" fetched-page)) (cdr (assoc "description" content))))
66 |
67 | ;; Change content and post again...
68 | (setcdr (assoc "description" content) new-content)
69 | (wp-edit-page blog-xmlrpc blog-user blog-pass blog-id page-id content nil)
70 | ;; Fetch page again and check if content changed
71 | (setq fetched-page (metaweblog-get-post blog-xmlrpc blog-user blog-pass page-id))
72 | (should (equal (cdr (assoc "description" fetched-page)) (cdr (assoc "description" content))))
73 |
74 | ;; List pages
75 | ;; Check if our page is listed in minimal listing
76 | (dolist (page (wp-get-pagelist blog-xmlrpc blog-user blog-pass blog-id))
77 | (if (equal (cdr (assoc "post_id" page)) page-id)
78 | (should (equal (cdr (assoc "page_title" page)) (cdr (assoc "title" content))))))
79 |
80 | ;; Check if our page is listed in full listing
81 | (dolist (page (wp-get-pages blog-xmlrpc blog-user blog-pass blog-id))
82 | (if (equal (cdr (assoc "post_id" page)) page-id)
83 | (should (equal (cdr (assoc "page_title" page)) (cdr (assoc "title" content))))))
84 |
85 | ;; Delete page
86 | (wp-delete-page blog-xmlrpc blog-id blog-user blog-pass page-id)))
87 |
88 | (ert-deftest test-posts ()
89 | "Test if creating, listing, fetching content and deleting posts works."
90 | (let* ((content '(("date" . "20120817T18:30:00+0000")
91 | ("title" . "Hello World")
92 | ("tags" "org2blog" "emacs")
93 | ("categories" "org2blog" "emacs")
94 | ("post-id")
95 | ("parent" . "0")
96 | ("excerpt" . "")
97 | ("permalink" . "")
98 | ("description" . "Test content.
")))
99 |
100 | ;; Make a new post
101 | (post-id (metaweblog-new-post blog-xmlrpc blog-user blog-pass blog-id content nil))
102 | (fetched-post (metaweblog-get-post blog-xmlrpc blog-user blog-pass post-id))
103 | (new-content "New Content
"))
104 |
105 | ;; Check if fetched-post's content and posted content match
106 | (should (equal (cdr (assoc "description" fetched-post)) (cdr (assoc "description" content))))
107 |
108 | ;; Change content and post again...
109 | (setcdr (assoc "description" content) new-content)
110 | (metaweblog-edit-post blog-xmlrpc blog-user blog-pass post-id content nil)
111 | ;; Fetch post again and check if content changed
112 | (setq fetched-post (metaweblog-get-post blog-xmlrpc blog-user blog-pass post-id))
113 | (should (equal (cdr (assoc "description" fetched-post)) (cdr (assoc "description" content))))
114 |
115 | ;; List recent posts and check if our post is listed
116 | (dolist (post (metaweblog-get-recent-posts blog-xmlrpc blog-id blog-user blog-pass 1000))
117 | (when (equal (cdr (assoc "postid" post)) post-id)
118 | (should (equal (cdr (assoc "title" post)) (cdr (assoc "title" content))))
119 | (should (equal (cdr (assoc "description" post)) (cdr (assoc "description" content))))))
120 |
121 | ;; Delete post
122 | (metaweblog-delete-post blog-xmlrpc blog-user blog-pass post-id)))
123 |
124 |
125 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
126 | ;; FIXME: Write tests for these functions ...
127 | ;; (defun get-file-properties (file)
128 | ;; "Gets the properties of a file. Returns an assoc list with
129 | ;; name - file name
130 | ;; bits - data of the file as a base64 encoded string
131 | ;; type - mimetype of file deduced from extension.")
132 |
133 | ;; (defun metaweblog-upload-file (blog-xmlrpc user-name password blog-id file)
134 | ;; "Uploads file to the blog. FILE will be an alist name, type,
135 | ;; bits, as keys mapped to name of the file, mime type and the
136 | ;; data.")
137 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
138 |
--------------------------------------------------------------------------------