diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6bece32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +doc/tags +.hg +.svn +.hg* diff --git a/README b/README deleted file mode 100644 index 649c28e..0000000 --- a/README +++ /dev/null @@ -1,41 +0,0 @@ -This is a mirror of http://www.vim.org/scripts/script.php?script_id=3510 - -VimRepress is a plugin for managing wordpress blog from Vim, a rewritten of vimscript #1953 , which is broken for years. - -Now VimRepress lives with more powerful again. - -*REQUIREMENT* - -- Vim 7.3+ with python 2.6/2.7 support -- Python Environment matched wtih Vim's support -- python-markdown/python-markdown2 installed -- wordpress 3.0.0 + - - - -COMMAND EXAMPLES - -Some commands list above contain special usage, example below may clearify them for you. - - - :BlogList - List 30 recent posts. - :BlogList page - List 30 recent pages. - :BlogList post 100 - List 100 recent posts. - - :BlogNew post - Write an new post. - :BlogNew page - Write an new page. - - :BlogSave - Save (defautely published.) - :BlogSave draft - Save as draft. - - :BlogPreview local - Preview page/post locally in your browser. - :BlogPreview publish - Same as `:BlogSave publish' with brower opened. - - :BlogOpen 679 - :BlogOpen http://your-first-blog.com/archives/679 - :BlogOpen http://your-second-blog.com/?p=679 - :BlogOpen http://your-third-blog.com/with-your-custom-permalink - -More detailed about this commands, type :help vimpress while you have vimrepress installed. - -Developing Repository: https://bitbucket.org/pentie/vimrepress diff --git a/README.md b/README.md new file mode 100644 index 0000000..d12b241 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +#Welcome +VimRepress is a plugin for managing WordPress blog from Vim, using Markdown syntax. + +##Features + * NEW/EDIT/DELETE WordPress Posts/Pages. + * In both Markdown / HTML format. + * Markdown text can be configured to be stored in the custom fields of WordPress. + * Upload attachments. + * Insert code highlight section. + * Preview a posts in local compiled version, or remote draft. + * WordPress.com account supported. + * Multiple account supported. + +##Commands Reference + * BlogList [post|page] + * BlogNew [post|page] + * BlogSave [publish|draft] + * BlogPreview [local|publish|draft] + * BlogUpload *[path/to/your/local/file] + * BlogOpen *[post id or full article URL] + * BlogSwitch [0,1,2 ... N, number of account in your config] + * BlogCode [type of lang for the \ element] + + (Commands with a `*`, argument must be present.) + + +##CONFIGURE + +Create file `~/.vimpressrc` in the following format: + + [Blog0] + blog_url = http://a-blog.com/ + username = admin + password = 123456 + + [Blog1] + blog_url = https://blog1.wordpress.com/ + username = someone + password = + store_markdown = n + + [BlogWhatEver] + blog_url = https://someone.wordpress.com/ + username = someone + password = + +Hardcoding the password is optional. If a password is not provided the plugin will prompt for one the first time it's needed. + +`store_markdown` is also optional. If not specified then Markdown text will be stored in custom fields of WordPress. If set to `n` then the Markdown text will not be stored. + +###For Upgraded Users + +Defining account info in `.vimrc` is now obsolesced, if you have correspond defination in `.vimrc` (for older version vimpress), they will automaticly copied into `~/.vimpressrc`, now you're safe to remove the VIMPRESS defination in `.vimrc`. + +Users from the 2.x.x versions of vimrepress, need to run the `markdown_posts_upgrade.py` to upgrade the their posts data to be compatible with the 3.x.x version of vimrepress, or their Markdown source can not be used to re-edit by a newer vimrepress. + + diff --git a/doc/vimpress.txt b/doc/vimpress.txt index da86696..88c6725 100644 --- a/doc/vimpress.txt +++ b/doc/vimpress.txt @@ -17,27 +17,33 @@ License: Same terms as Vim itself (see |license|) *INSTALL* -Download vimpress_2.x.x.zip, extract it in your .vim directory: +Download vimpress_x.x.x.zip, extract it in your .vim directory: cd ~/.vim - unzip /path/to/vimpress_2.x.x.zip + unzip /path/to/vimpress_x.x.x.zip *CONFIGURE* -Edit ~/.vimrc , add a variable named VIMPRESS. Multiple blog configurations are supported. +Create file `~/.vimpressrc' in the following format: -Example: + [Blog0] + blog_url = http://a-blog.com/ + username = admin + password = 123456 -let VIMPRESS = [{'username':'user', - \'password':'pass', - \'blog_url':'http://your-first-blog.com/' - \}, - \{'username':'user', - \'blog_url':'http://your-second-blog.com/' - \}] + [Blog1] + blog_url = https://someone.wordpress.com/ + username = someone + password = -Hardcoding the password is optional. If a password is not provided the plugin -will prompt for one the first time it's needed. +> +For Upgraded Users: + + Defining Account info in `.vimrc` is now obsolesced, if you have correspond defination in `.vimrc', they will automaticly copied into `~/.vimpressrc', now you're safe to remove the VIMPRESS defination in `.vimrc'. +< + +Hardcoding the password is optional. If a password is not provided, +the plugin will prompt for one the first time it's needed. If you need Markdown support, simply run `sudo apt-get install python-markdown' in Ubuntu. @@ -125,61 +131,3 @@ Some commands list above contain special usage, example below may clearify them :BlogOpen http://your-third-blog.com/with-your-custom-permalink < -*CHANGE_LOG* - - 2011 May. 15 [by Preston] - Upgrade to 2.0 beta - Different command structure - Markdown file uploaded when post is saved and interpreted - when opened. - The open command takes many types of parameters - - [by Conner] - Add: autocompletion for tags and categories with ^X^U - Add: prompts for password when not hardcoded - Add: BlogSwitch now accepts an index parameter - Add: Delete function in BlogList view. - *Add: HTML highlighting for blog syntax - *Add: Delete command - *(not yet included in Preston's release version) - - 2011 Mar. 24 [by Lenin Lee] - Fix: use setl instead of set to set option value; - Add: Detect current buffer content before switch to vimpress - views, open a split buffer to avoid conflicts. - Add: Commands to manage wordpress pages. - - [by Preston] - Add: Auto charset convert for non-utf8 environment.(Win) - Add: Use python markdown module. Both markdown and markdown2 - are supported. - - 2011 Mar. 15 [by Preston] - Fix: MarkdownNewPost may override original mkd source file. - Add: MarkdownNewPost command detects title begins with - "#" in first 10 lines of markdown source, copy the line - striped "#" to the new post view. - - 2011 Mar. 7 [by Preston] - Add: MarkdownPreview command to preiview markdown in browser. - Add: MarkdownNewPost command to convert a markdown - written post into html and set to the new post view. - - - 2011 Mar. 4 [by Preston] - Add: Move blog config info to personal .vimrc - Add: Multiple blog config is now supported with :BlogSwitch - command. - Add: Show which blog your editing at :BlogList view. - Fix: bug running :BlogList in the List view got error. - - 2011 Feb. 15 [by Preston] - Add: BlogPreview Command. - Add: BlogCode command args to specify code type - Change: blog_url uses pure address. - Code: Some code pretty work. - - 2010 Aug. 20 [by Justin] - Fixed a bug with BlogSave command, and added feature to take - an existing document and use the BlogNew command to convert - it to a blog post (which can be saved with the header intact). diff --git a/markdown_posts_upgrade.py b/markdown_posts_upgrade.py new file mode 100755 index 0000000..1dd1653 --- /dev/null +++ b/markdown_posts_upgrade.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python2 +import urllib2, xmlrpclib, re, os, sys +import getpass + +class VimPressException(Exception): + pass + +class VimPressFailedGetMkd(VimPressException): + pass + + +class DataObject(object): + + #CONST + DEFAULT_LIST_COUNT = "15" + IMAGE_TEMPLATE = '%(file)s' + MARKER = dict(bg = "=========== Meta ============", + mid = "=============================", + ed = "========== Content ==========", + more = '"====== Press Here for More ======', + list_title = '"====== %(edit_type)s List in %(blog_url)s =========') + LIST_VIEW_KEY_MAP = dict(enter = "", delete = "") + DEFAULT_META = dict(strid = "", title = "", slug = "", + cats = "", tags = "", editformat = "Markdown", + edittype = "post", textattach = '') + TAG_STRING = "" + TAG_RE = re.compile(TAG_STRING % dict(url = '(?P\S+)', file = '(?P\S+)')) + CUSTOM_FIELD_KEY = "mkd_text" + + #Temp variables. + __xmlrpc = None + __conf_index = 0 + __config = None + + view = 'edit' + vimpress_temp_dir = '' + + blog_username = property(lambda self: self.xmlrpc.username) + blog_url = property(lambda self: self.xmlrpc.blog_url) + conf_index = property(lambda self:self.__conf_index) + + xmlrpc = None + + +class wp_xmlrpc(object): + + def __init__(self, blog_url, username, password): + self.blog_url = blog_url + self.username = username + self.password = password + p = xmlrpclib.ServerProxy(os.path.join(blog_url, "xmlrpc.php")) + self.mw_api = p.metaWeblog + self.wp_api = p.wp + self.mt_api = p.mt + self.demo_api = p.demo + + assert self.demo_api.sayHello() == "Hello!", "XMLRPC Error with communication with '%s'@'%s'" % \ + (username, blog_url) + + self.cache_reset() + + def cache_reset(self): + self.__cache_post_titles = [] + self.__post_title_max = False + + def cache_remove_post(self, postid): + for p in self.__cache_post_titles: + if p["postid"] == str(postid): + self.__cache_post_titles.remove(p) + break + + is_reached_title_max = property(lambda self: self.__post_title_max) + + new_post = lambda self, post_struct, is_publish: self.mw_api.newPost('', + self.username, self.password, post_struct, is_publish) + + get_post = lambda self, post_id: self.mw_api.getPost(post_id, + self.username, self.password) + + edit_post = lambda self, post_id, post_struct: self.mw_api.editPost(post_id, + self.username, self.password, post_struct ) + + delete_post = lambda self, post_id: self.mw_api.deletePost('', post_id, self.username, + self.password, '') + + def get_recent_post_titles(self, retrive_count = 0): + if retrive_count > len(self.__cache_post_titles) and not self.is_reached_title_max: + self.__cache_post_titles = self.mt_api.getRecentPostTitles('', + self.username, self.password, retrive_count) + if len(self.__cache_post_titles) < retrive_count: + self.__post_title_max = True + + return self.__cache_post_titles + + get_categories = lambda self:self.mw_api.getCategories('', self.username, self.password) + + new_media_object = lambda self, object_struct: self.mw_api.newMediaObject('', self.username, + self.password, object_struct) + + get_page = lambda self, page_id: self.wp_api.getPage('', page_id, self.username, self.password) + + delete_page = lambda self, page_id: self.wp_api.deletePage('', + self.username, self.password, page_id) + + get_page_list = lambda self: self.wp_api.getPageList('', self.username, self.password) + +def blog_get_mkd_attachment(post): + """ + Attempts to find a vimpress tag containing a URL for a markdown attachment and parses it. + @params post - the content of a post + @returns a dictionary with the attachment's content and URL + """ + attach = dict() + try: + lead = post.rindex("' + post_more + elif len(page_more) > 0: + content += '' + page_more + + return content + +def loop_proccess_posts(posts, edit_type): + print "ID Title" + for post in posts: + if edit_type == "page": + print u"%(page_id)s\t%(page_title)s" % post, '... ', + sys.stdout.flush() + page_id = post["page_id"].encode("utf-8") + data = g_data.xmlrpc.get_page(page_id) + elif edit_type == "post": + print u"%(postid)s\t%(title)s ... " % post, + sys.stdout.flush() + post_id = post["postid"].encode("utf-8") + data = g_data.xmlrpc.get_post(post_id) + + content = post_struct_get_content(data) + try: + attach = blog_get_mkd_attachment(content) + except VimPressFailedGetMkd: + print "No Markdown Attached." + else: + blog_update(data, content, attach) + attachements_proccessed.append(attach["mkd_name"]) + print "Updated." + + print "\n\n" + +print """ + +Vimrepress 3.x upgrade script + +WHY: + + The older (2.x) vimrepress stores your originally + written markdown text in an attachment with the + post on wordpress. + + A better way is implemented in the new (3.x) + vimrepress, the markdown texts stores in the custom + field of a post. + + If you used vimrepress 2.x to write your blog before, + and want the 3.x to be able to edit your old posts, + this script is needed to convert the attachments + content into the custom field. + +HOW: + + Fillin the address/username/password as asked, script + will scan through your posts, when a markdown attached + post found, it will download and read it into the + database. + +I tested this script works flawlessly to my own blog, +but I don't guarantee no exceptions in other circumstance, +I don't respons for any data lost. + +Backup your wordpress with this plugin: WP-DB-Backup + ( http://wordpress.org/extend/plugins/wp-db-backup/ ) + +or phpmyadmin/adminer/mysqldump anything you like it most. + + WARNNING +######################################################### +Warnning: Backup your wordpress database before procceed. +Warnning: Backup your wordpress database before procceed. +Warnning: Backup your wordpress database before procceed. +######################################################### +""" + +URL = raw_input("Blog URL: ") +USER = raw_input("USERNAME: ") +PASS = getpass.getpass() + +i = raw_input("Have you backed up your wordpress database? [y/N]") +if i.lower() == 'n' or i == '': + print "----> Go and do that, don't risk your data." + sys.exit(1) + +g_data = DataObject() +g_data.xmlrpc = wp_xmlrpc(URL, USER, PASS) + +attachements_proccessed = [] + +i = raw_input("Upgrade pages ?[Y/n]") +if i.lower() == 'y' or i == '': + print "Upgrade pages ..." + pages = g_data.xmlrpc.get_page_list() + loop_proccess_posts(pages, "page") + +i = raw_input("Upgrade Posts ?[Y/n]") +if i.lower() == 'y' or i == '': + count = raw_input("How many recent posts to process? [100]") + if count == '': + count = '100' + assert isinstance(int(count), int), "input a integer please." + posts = g_data.xmlrpc.get_recent_post_titles(count) + loop_proccess_posts(posts, "post") + +if len(attachements_proccessed) > 0: + print "All Done. Congras." + print "You may now delete this attachments from wordpress panel." + print URL+"/wp-admin/upload.php" + print + print "\n".join(attachements_proccessed) + + diff --git a/plugin/blog.vim b/plugin/blog.vim deleted file mode 100644 index 02c4d48..0000000 --- a/plugin/blog.vim +++ /dev/null @@ -1,830 +0,0 @@ -"####################################################################### -" Copyright (C) 2007 Adrien Friggeri. -" -" 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 2, 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, write to the Free Software Foundation, -" Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -" -" Maintainer: Adrien Friggeri -" Pigeond -" Justin Sattery -" Lenin Lee -" Conner McDaniel -" -" Forked: Preston M.[BOYPT] -" -" URL: http://www.friggeri.net/projets/vimblog/ -" http://pigeond.net/blog/2009/05/07/vimpress-again/ -" http://pigeond.net/git/?p=vimpress.git -" http://apt-blog.net -" http://fzysqr.com/ -" -" VimRepress -" - A mod of a mod of a mod of Vimpress. -" - A vim plugin fot writting your wordpress blog. -" -" Version: 2.1.5 -" -" Configure: Add blog configure into your .vimrc (password optional) -" -" let VIMPRESS=[{'username':'user', -" \'password':'pass', -" \'blog_url':'http://your-first-blog.com/' -" \}, -" \{'username':'user', -" \'blog_url':'http://your-second-blog.com/' -" \}] -" -"####################################################################### - -if !has("python") - finish -endif - -function! CompSave(ArgLead, CmdLine, CursorPos) - return "publish\ndraft\n" -endfunction - -function! CompPrev(ArgLead, CmdLine, CursorPos) - return "local\npublish\ndraft\n" -endfunction - -function! CompEditType(ArgLead, CmdLine, CursorPos) - return "post\npage\n" -endfunction - -fun! Completable(findstart, base) - if a:findstart - " locate the start of the word - let line = getline('.') - let start = col('.') - 1 - while start > 0 && line[start - 1] =~ '\a' - let start -= 1 - endwhile - return start - else - " find matching items - let res = [] - for m in split(s:completable,"|") - if m =~ '^' . a:base - call add(res, m) - endif - endfor - return res - endif -endfun - -command! -nargs=* -complete=custom,CompEditType BlogList exec('py blog_list()') -command! -nargs=? -complete=custom,CompEditType BlogNew exec('py blog_new()') -command! -nargs=? -complete=custom,CompSave BlogSave exec('py blog_save()') -command! -nargs=? -complete=custom,CompPrev BlogPreview exec('py blog_preview()') -command! -nargs=1 -complete=file BlogUpload exec('py blog_upload_media()') -command! -nargs=1 BlogOpen exec('py blog_guess_open()') -command! -nargs=? BlogSwitch exec('py blog_config_switch()') -command! -nargs=? BlogCode exec('py blog_append_code()') - -python << EOF -# -*- coding: utf-8 -*- -import urllib, urllib2, vim, xml.dom.minidom, xmlrpclib, sys, string, re, os, mimetypes, webbrowser, tempfile, time -try: - import markdown -except ImportError: - try: - import markdown2 as markdown - except ImportError: - class markdown_stub(object): - def markdown(self, n): - raise VimPressException("The package python-markdown is required and is either not present or not properly installed.") - markdown = markdown_stub() - -image_template = '%(file)s' -blog_username = None -blog_password = None -blog_url = None -blog_conf_index = 0 -vimpress_view = 'edit' -vimpress_temp_dir = '' - -mw_api = None -wp_api = None -marker = ("=========== Meta ============", "=============================", "========== Content ==========") -list_view_key_map = dict(enter = "", delete = "") - -tag_string = "" -tag_re = re.compile(tag_string % dict(url = '(?P\S+)', file = '(?P\S+)')) - -default_meta = dict(strid = "", title = "", slug = "", - cats = "", tags = "", editformat = "Markdown", edittype = "post", textattach = '') - -class VimPressException(Exception): - pass - -class VimPressFailedGetMkd(VimPressException): - pass - -def blog_meta_parse(): - """ - Parses the meta data region of a blog editing buffer. - @returns a dictionary of the meta data - """ - meta = dict() - start = 0 - while not vim.current.buffer[start][1:].startswith(marker[0]): - start +=1 - - end = start + 1 - while not vim.current.buffer[end][1:].startswith(marker[2]): - if not vim.current.buffer[end].startswith('"===='): - line = vim.current.buffer[end][1:].strip().split(":") - k, v = line[0].strip().lower(), ':'.join(line[1:]) - meta[k.strip().lower()] = v.strip() - end += 1 - - meta["post_begin"] = end + 1 - return meta - -def blog_meta_area_update(**kw): - """ - Updates the meta data region of a blog editing buffer. - @params **kwargs - keyworded arguments - """ - start = 0 - while not vim.current.buffer[start][1:].startswith(marker[0]): - start +=1 - - end = start + 1 - while not vim.current.buffer[end][1:].startswith(marker[2]): - if not vim.current.buffer[end].startswith('"===='): - line = vim.current.buffer[end][1:].strip().split(":") - k, v = line[0].strip().lower(), ':'.join(line[1:]) - if k in kw: - new_line = "\"%s: %s" % (line[0], kw[k]) - vim.current.buffer[end] = new_line - end += 1 - -def blog_fill_meta_area(meta): - """ - Creates the meta data region for a blog editing buffer using a dictionary of meta data. Empty keywords - are replaced by default values from the default_meta variable. - @params meta - a dictionary of meta data - """ - for k in default_meta.keys(): - if k not in meta: - meta[k] = default_meta[k] - - meta.update(dict(bg = marker[0], mid = marker[1], ed = marker[2])) - template = dict( \ - post = \ -""""%(bg)s -"StrID : %(strid)s -"Title : %(title)s -"Slug : %(slug)s -"Cats : %(cats)s -"Tags : %(tags)s -"%(mid)s -"EditType : %(edittype)s -"EditFormat : %(editformat)s -"TextAttach : %(textattach)s -"%(ed)s""", - page = \ -""""%(bg)s -"StrID : %(strid)s -"Title : %(title)s -"Slug : %(slug)s -"%(mid)s -"EditType : %(edittype)s -"EditFormat : %(editformat)s -"TextAttach : %(textattach)s -"%(ed)s""") - - if meta["edittype"] not in ("post", "page"): - raise VimPressException("Invalid option: %(edittype)s " % meta) - meta_text = template[meta["edittype"].lower()] % meta - meta = meta_text.split('\n') - vim.current.buffer[0] = meta[0] - vim.current.buffer.append(meta[1:]) - -def blog_get_mkd_attachment(post): - """ - Attempts to find a vimpress tag containing a URL for a markdown attachment and parses it. - @params post - the content of a post - @returns a dictionary with the attachment's content and URL - """ - - attach = dict() - try: - lead = post.rindex("' + post_more - elif len(page_more) > 0: - content += '' + page_more - - content = content.encode("utf-8") - - if edit_type.lower() == "post": - meta_dict["cats"] = ",".join(data["categories"]).encode("utf-8") - meta_dict["tags"] = data["mt_keywords"].encode("utf-8") - - meta_dict['editformat'] = "HTML" - meta_dict['edittype'] = edit_type - - try: - attach = blog_get_mkd_attachment(content) - if "mkd_rawtext" in attach: - meta_dict['editformat'] = "Markdown" - meta_dict['textattach'] = attach["mkd_name"] - content = attach["mkd_rawtext"] - except VimPressFailedGetMkd: - pass - - blog_fill_meta_area(meta_dict) - meta = blog_meta_parse() - vim.current.buffer.append(content.split('\n')) - vim.current.window.cursor = (meta["post_begin"], 0) - vim.command('setl nomodified') - vim.command('setl textwidth=0') - - for v in list_view_key_map.values(): - if vim.eval("mapcheck('%s')" % v): - vim.command('unmap %s' % v) - -@__xmlrpc_api_check -def blog_delete(edit_type, post_id): - """ - Deletes a page or post of specified id. - @params edit_type - either "page" or "post" - post_id - the id of the post or page - """ - global vimpress_view - if edit_type.lower() not in ("post", "page"): - raise VimPressException("Invalid option: %s " % edit_type) - - if edit_type.lower() == "post": - deleted = mw_api.deletePost('0123456789ABCDEF', post_id, blog_username, blog_password, True) - else: - deleted = wp_api.deletePage('', blog_username, blog_password, post_id) - - if deleted: - sys.stdout.write("Deleted %s id %s. \n" % (edit_type, str(post_id))) - else: - sys.stdout.write("There was a problem deleting the %s.\n" % edit_type) - - if vimpress_view.startswith("list"): - blog_list(edit_type) - -@__exception_check -def blog_list_on_key_press(action): - """ - Calls blog open on the current line of a listing buffer. - """ - global vimpress_view - if action.lower() not in ("open", "delete"): - raise VimPressException("Invalid option: %s" % action) - - global vimpress_view - row = vim.current.window.cursor[0] - line = vim.current.buffer[row - 1] - id = line.split()[0] - title = line[len(id):].strip() - - try: - int(id) - except ValueError: - raise VimPressException("Move cursor to a post/page line and press KEY.") - - if len(title) > 30: - title = title[:30] + ' ...' - - if action.lower() == "delete": - confirm = vim_input("Confirm Delete [%s]: %s? [yes/NO]" % (id,title)) - if confirm != 'yes': - sys.stdout.write("Delete Aborted.\n") - return - - vim.command("setl modifiable") - del vim.current.buffer[:] - vim.command("setl nomodified") - - if vimpress_view == "list_page": - edit_type = "page" - elif vimpress_view == "list_post": - edit_type = "post" - else: - raise VimPressException("Command only available in list view.") - - if action == "open": - blog_edit(edit_type, int(id)) - elif action == "delete": - blog_delete(edit_type, int(id)) - -@__exception_check -@__vim_encoding_check -@__xmlrpc_api_check -def blog_list(edit_type = "post", count = "30"): - """ - Creates a listing buffer of specified type. - @params edit_type - either "post(s)" or "page(s)" - count - number to show (only for posts) - """ - global vimpress_view - vimpress_view = 'list' - - blog_wise_open_view() - vim.current.buffer[0] = "\"====== List of %ss in %s =========" % (edit_type.capitalize(), blog_url) - - if edit_type.lower() not in ("post", "posts", "page", "pages"): - raise VimPressException("Invalid option: %s " % edit_type) - - if edit_type.lower() in ("post", "posts"): - vimpress_view = 'list_post' - allposts = mw_api.getRecentPosts('',blog_username, blog_password, int(count)) - vim.current.buffer.append(\ - [(u"%(postid)s\t%(title)s" % p).encode('utf8') for p in allposts]) - else: - vimpress_view = 'list_page' - pages = wp_api.getPageList('', blog_username, blog_password) - vim.current.buffer.append(\ - [(u"%(page_id)s\t%(page_title)s" % p).encode('utf8') for p in pages]) - - vim.command("setl nomodified") - vim.command("setl nomodifiable") - vim.current.window.cursor = (2, 0) - vim.command("map %(enter)s :py blog_list_on_key_press('open')" % list_view_key_map) - vim.command("map %(delete)s :py blog_list_on_key_press('delete')" % list_view_key_map) - sys.stdout.write("Press to edit. to move to trash.\n") - -@__exception_check -@__vim_encoding_check -@__xmlrpc_api_check -def blog_upload_media(file_path): - """ - Uploads a file to the blog. - @params file_path - the file's path - """ - if vimpress_view != 'edit': - raise VimPressException("Command not available at list view.") - if not os.path.exists(file_path): - raise VimPressException("File does not exist: %s" % file_path) - - name = os.path.basename(file_path) - filetype = mimetypes.guess_type(file_path)[0] - with open(file_path) as f: - bits = xmlrpclib.Binary(f.read()) - - result = mw_api.newMediaObject('', blog_username, blog_password, - dict(name = name, type = filetype, bits = bits)) - - ran = vim.current.range - if filetype.startswith("image"): - img = image_template % result - ran.append(img) - else: - ran.append(result["url"]) - ran.append('') - -@__exception_check -@__vim_encoding_check -def blog_append_code(code_type = ""): - if vimpress_view != 'edit': - raise VimPressException("Command not available at list view.") - html = \ -"""
-
""" - if code_type != "": - args = 'lang="%s" line="1"' % code_type - else: - args = 'lang="text"' - - row, col = vim.current.window.cursor - code_block = (html % args).split('\n') - vim.current.range.append(code_block) - vim.current.window.cursor = (row + len(code_block), 0) - -@__exception_check -@__vim_encoding_check -def blog_preview(pub = "local"): - """ - Opens a browser window displaying the content. - @params pub - If "local", the content is shown in a browser locally. - If "draft", the content is saved as a draft and previewed remotely. - If "publish", the content is published and displayed remotely. - """ - if vimpress_view != 'edit': - raise VimPressException("Command not available at list view.") - meta = blog_meta_parse() - rawtext = '\n'.join(vim.current.buffer[meta["post_begin"]:]) - - if pub == "local": - if meta["editformat"].strip().lower() == "markdown": - html = markdown.markdown(rawtext.decode('utf-8')).encode('utf-8') - html_preview(html, meta) - else: - html_preview(rawtext, meta) - elif pub == "publish" or pub == "draft": - meta = blog_save(pub) - if meta["edittype"] == "page": - prev_url = "%s?pageid=%s&preview=true" % (blog_url, meta["strid"]) - else: - prev_url = "%s?p=%s&preview=true" % (blog_url, meta["strid"]) - webbrowser.open(prev_url) - if pub == "draft": - sys.stdout.write("\nYou have to login in the browser to preview the post when save as draft.") - else: - raise VimPressException("Invalid option: %s " % pub) - - -@__exception_check -def blog_guess_open(what): - """ - Tries several methods to get the post id from different user inputs, such as args, url, postid etc. - """ - post_id = '' - blog_index = -1 - if type(what) is str: - - for i, p in enumerate(vim.eval("VIMPRESS")): - if what.startswith(p["blog_url"]): - blog_index = i - - # User input a url contained in the profiles - if blog_index != -1: - guess_id = re.search(r"\S+?p=(\d+)$", what) - - # permantlinks - if guess_id is None: - - # try again for /archives/%post_id% - guess_id = re.search(r"\S+/archives/(\d+)", what) - - # fail, try get full link from headers - if guess_id is None: - headers = urllib.urlopen(what).headers.headers - for link in headers: - if link.startswith("Link:"): - post_id = re.search(r"<\S+?p=(\d+)>", link).group(1) - - # fail, just give up - if post_id == '': - raise VimPressException("Failed to get post/page id from '%s'." % what) - else: - post_id = guess_id.group(1) - - # full link with ID (http://blog.url/?p=ID) - else: - post_id = guess_id.group(1) - - # Uesr input something not a usabe url, try numberic - else: - try: - post_id = str(int(what)) - except ValueError: - pass - - # detected something - if post_id != '': - if blog_index != -1 and blog_index != blog_conf_index: - blog_config_switch(blog_index) - blog_edit("post", post_id) - else: - raise VimPressException("Failed to get post/page id from '%s'." % what) - - -@__exception_check -def blog_update_config(): - """ - Updates the script's configuration variables. - """ - global blog_username, blog_password, blog_url, mw_api, wp_api - try: - config = vim.eval("VIMPRESS")[blog_conf_index] - blog_username = config['username'] - blog_url = config['blog_url'] - sys.stdout.write("Connecting to %s \n" % blog_url) - blog_password = config.get('password', '') - if blog_password == '': - blog_password = vim_input("Enter password for %s" % blog_url, True) - mw_api = xmlrpclib.ServerProxy("%s/xmlrpc.php" % blog_url).metaWeblog - wp_api = xmlrpclib.ServerProxy("%s/xmlrpc.php" % blog_url).wp - - # Setting tags and categories for completefunc - terms = [] - terms.extend([i["description"].encode("utf-8") - for i in mw_api.getCategories('', blog_username, blog_password)]) - # adding tags may make the menu too much items to choose. - #terms.extend([i["name"].encode("utf-8") for i in wp_api.getTags('', blog_username, blog_password)]) - vim.command('let s:completable = "%s"' % '|'.join(terms)) - - except vim.error: - raise VimPressException("Could not find vimpress configuration. Please read ':help vimpress' for more information.") - except KeyError, e: - raise VimPressException("Configuration error: %s" % e) - -@__vim_encoding_check -def vim_input(message = 'input', secret = False): - vim.command('call inputsave()') - vim.command("let user_input = %s('%s :')" % (("inputsecret" if secret else "input"), message)) - vim.command('call inputrestore()') - return vim.eval('user_input') - -@__exception_check -@__vim_encoding_check -def blog_config_switch(conf_index = -1): - """ - Switches the blog to the next index of the configuration array. - """ - global blog_conf_index - try: - conf_index = int(conf_index) - except ValueError: - raise VimPressException("Invalid Index: %s" % conf_index) - - conf = vim.eval("VIMPRESS") - if conf_index == -1: - blog_conf_index += 1 - if blog_conf_index >= len(conf): - blog_conf_index = 0 - else: - if conf_index >= len(conf): - raise VimPressException("Invalid Index: %d" % conf_index) - blog_conf_index = conf_index - - blog_update_config() - if vimpress_view.startswith('list'): - blog_list() - sys.stdout.write("Vimpress switched to %s" % blog_url) - -def html_preview(text_html, meta): - """ - Opens a browser with a local preview of the content. - @params text_html - the html content - meta - a dictionary of the meta data - """ - global vimpress_temp_dir - if vimpress_temp_dir == '': - vimpress_temp_dir = tempfile.mkdtemp(suffix="vimpress") - - html = \ -""" - - -Vimpress Local Preview: %(title)s - - - - -%(content)s - - -""" % dict(content = text_html, title = meta["title"]) - with open(os.path.join(vimpress_temp_dir, "vimpress_temp.html"), 'w') as f: - f.write(html) - webbrowser.open("file://%s" % f.name) - -def blog_wise_open_view(): - """ - Wisely decides whether to wipe out the content of current buffer or open a new splited window. - """ - if vim.current.buffer.name is None and \ - (vim.eval('&modified') == '0' or \ - len(vim.current.buffer) == 1): - vim.command('setl modifiable') - del vim.current.buffer[:] - vim.command('setl nomodified') - else: - vim.command(":new") - vim.command('setl syntax=blogsyntax') - vim.command('setl completefunc=Completable') diff --git a/plugin/vimrepress.py b/plugin/vimrepress.py new file mode 100644 index 0000000..dab0a86 --- /dev/null +++ b/plugin/vimrepress.py @@ -0,0 +1,962 @@ +# -*- coding: utf-8 -*- +import vim +import urllib +import xmlrpclib +import re +import os +import mimetypes +import webbrowser +import tempfile +from ConfigParser import SafeConfigParser + +try: + import markdown +except ImportError: + try: + import markdown2 as markdown + except ImportError: + class markdown_stub(object): + def markdown(self, n): + raise VimPressException("The package python-markdown is " + "required and is either not present or not properly " + "installed.") + + markdown = markdown_stub() + + +def exception_check(func): + def __check(*args, **kwargs): + try: + return func(*args, **kwargs) + except (VimPressException, AssertionError), e: + echoerr(str(e)) + except (xmlrpclib.Fault, xmlrpclib.ProtocolError), e: + if getattr(e, "faultString", None) is None: + echoerr("xmlrpc error: %s" % e) + else: + echoerr("xmlrpc error: %s" % e.faultString.encode("utf-8")) + except IOError, e: + echoerr("network error: %s" % e) + except Exception, e: + echoerr("something wrong: %s" % e) + raise + + return __check + +echomsg = lambda s: vim.command('echomsg "%s"' % s) +echoerr = lambda s: vim.command('echoerr "%s"' % s) + +################################################ +# Helper Classes +################################################# + + +class VimPressException(Exception): + pass + + +class DataObject(object): + + #CONST + DEFAULT_LIST_COUNT = "15" + IMAGE_TEMPLATE = '' \ + '%(file)s' + MARKER = dict(bg="=========== Meta ============", + mid="=============================", + ed="========== Content ==========", + more='"====== Press Here for More ======', + list_title='"====== ' + '%(edit_type)s List in %(blog_url)s =========') + + LIST_VIEW_KEY_MAP = dict(enter="", delete="") + CUSTOM_FIELD_KEY = "mkd_text" + + #Temp variables. + __xmlrpc = None + __conf_index = 0 + __config = None + + view = 'edit' + vimpress_temp_dir = '' + + blog_username = property(lambda self: self.xmlrpc.username) + blog_url = property(lambda self: self.xmlrpc.blog_url) + store_markdown = property(lambda self: self.xmlrpc.store_markdown) + conf_index = property(lambda self: self.__conf_index) + current_post_id = property(lambda self: self.xmlrpc.current_post_id, + lambda self, d: setattr(self.xmlrpc, "current_post_id", d)) + post_cache = property(lambda self: self.xmlrpc.post_cache) + + @property + def current_post(self): + post_id = self.current_post_id + post = self.post_cache.get(post_id) + if post is None and post_id == '': + post = ContentStruct() + if post.post_id != '': + self.current_post_id = post.post_id + self.current_post = post + else: + self.post_cache[''] = post + + assert post is not None, "current_post, no way to return None" + return post + + @current_post.setter + def current_post(self, data): + post_id = str(data.post_id) + + # New post, just post first time + if self.current_post_id == '' and \ + post_id != '' and '' in self.post_cache: + self.post_cache.pop('') + self.current_post_id = post_id + if post_id not in self.post_cache: + self.post_cache[post_id] = data + + @conf_index.setter + def conf_index(self, index): + try: + index = int(index) + except ValueError: + raise VimPressException("Invalid Index: %s" % index) + + #auto increase + if index < 0: + self.__conf_index += 1 + if self.__conf_index >= len(self.config): + self.__conf_index = 0 + # user enter index + else: + assert index < len(self.config), "Invalid Index: %d" % index + self.__conf_index = index + + self.__xmlrpc = None + + @property + def xmlrpc(self): + if self.__xmlrpc is None: + conf_index = self.conf_index + config = self.config[conf_index] + + if "xmlrpc_obj" not in config: + try: + blog_username = config['username'] + blog_password = config.get('password', '') + blog_url = config['blog_url'] + store_markdown = config.get('store_markdown', 'y') + except KeyError, e: + raise VimPressException("Configuration error: %s" % e) + echomsg("Connecting to '%s' ... " % blog_url) + if blog_password == '': + blog_password = vim_input( + "Enter password for %s" % blog_url, True) + config["xmlrpc_obj"] = wp_xmlrpc(blog_url, + blog_username, blog_password, store_markdown) + + self.__xmlrpc = config["xmlrpc_obj"] + + # Setting tags and categories for completefunc + + categories = [i["categoryName"].encode("utf-8") + for i in self.xmlrpc.get_categories()] + vim.command('let s:completable = "%s"' % '|'.join(categories)) + echomsg("done.") + return self.__xmlrpc + + @property + def config(self): + if self.__config is None or len(self.__config) == 0: + + confpsr = SafeConfigParser({'store_markdown': 'y'}) + confile = os.path.expanduser("~/.vimpressrc") + conf_options = ("blog_url", "username", "password", "store_markdown") + + if os.path.exists(confile): + conf_list = [] + confpsr.read(confile) + for sec in confpsr.sections(): + values = [confpsr.get(sec, i) for i in conf_options] + conf_list.append(dict(zip(conf_options, values))) + + if len(conf_list) > 0: + self.__config = conf_list + else: + raise VimPressException( + "You have an empty `~/.vimpressrc', " + "thus no configuration will be read. " + "If you still have your account info in " + "`.vimrc', remove `~/.vimpressrc' and " + "try again.") + + else: + try: + self.__config = vim.eval("VIMPRESS") + except vim.error: + pass + else: + # write config to `~/.vimpressrc`, + # coding account in .vimrc is obsolesced. + if not os.path.exists(confile) and \ + self.__config is not None: + with open(confile, 'w') as f: + for i, c in enumerate(self.__config): + sec = "Blog%d" % i + confpsr.add_section(sec) + for i in conf_options: + confpsr.set(sec, i, c.get(i, '')) + confpsr.write(f) + + echomsg("Your Blog accounts are now copied to " + "`~/.vimpressrc', defining account info " + "in `.vimrc` is now obsolesced, and may " + "lead to secret leak if you share your " + "vim configuration with public. Please " + "REMOVE the `let VIMPRESS =' code in your " + "`.vimrc'. ") + + if self.__config is None or len(self.__config) == 0: + raise VimPressException("Could not find vimpress " + "configuration. Please read ':help vimpress' " + "for more information.") + + return self.__config + + +class wp_xmlrpc(object): + + def __init__(self, blog_url, username, password, store_markdown): + self.blog_url = blog_url + self.username = username + self.password = password + self.store_markdown = store_markdown + p = xmlrpclib.ServerProxy(os.path.join(blog_url, "xmlrpc.php")) + self.mw_api = p.metaWeblog + self.wp_api = p.wp + self.mt_api = p.mt + self.demo_api = p.demo + + assert self.demo_api.sayHello() == "Hello!", \ + "XMLRPC Error with communication with '%s'@'%s'" % \ + (username, blog_url) + + self.cache_reset() + self.post_cache = dict() + + self.current_post_id = '' + + def cache_reset(self): + self.__cache_post_titles = [] + self.__post_title_max = False + + def cache_remove_post(self, postid): + for p in self.__cache_post_titles: + if p["postid"] == str(postid): + self.__cache_post_titles.remove(p) + break + + is_reached_title_max = property(lambda self: self.__post_title_max) + + new_post = lambda self, post_struct: self.mw_api.newPost('', + self.username, self.password, post_struct) + + get_post = lambda self, post_id: self.mw_api.getPost(post_id, + self.username, self.password) + + edit_post = lambda self, post_id, post_struct: \ + self.mw_api.editPost(post_id, self.username, + self.password, post_struct) + + delete_post = lambda self, post_id: self.mw_api.deletePost('', + post_id, self.username, self.password, '') + + def get_recent_post_titles(self, retrive_count=0): + if retrive_count > len(self.__cache_post_titles) and \ + not self.is_reached_title_max: + self.__cache_post_titles = self.mt_api.getRecentPostTitles('', + self.username, self.password, retrive_count) + if len(self.__cache_post_titles) < retrive_count: + self.__post_title_max = True + + return self.__cache_post_titles + + get_categories = lambda self: self.mw_api.getCategories('', + self.username, self.password) + + new_category = lambda self, category: self.wp_api.newCategory('', + self.username, self.password, category) + + new_media_object = lambda self, object_struct: \ + self.mw_api.newMediaObject('', self.username, + self.password, object_struct) + + get_page = lambda self, page_id: self.wp_api.getPage('', + page_id, self.username, self.password) + + delete_page = lambda self, page_id: self.wp_api.deletePage('', + self.username, self.password, page_id) + + get_page_list = lambda self: self.wp_api.getPageList('', + self.username, self.password) + + +class ContentStruct(object): + + buffer_meta = None + post_struct_meta = None + EDIT_TYPE = '' + + @property + def META_TEMPLATE(self): + KEYS_BASIC = ("StrID", "Title", "Slug") + KEYS_EXT = ("Cats", "Tags") + KEYS_BLOG = ("EditType", "EditFormat", "BlogAddr") + + pt = ['"{k:<6}: {{{t}}}'.format(k=p, t=p.lower()) for p in KEYS_BASIC] + if self.EDIT_TYPE == "post": + pt.extend(['"{k:<6}: {{{t}}}'.format(k=p, t=p.lower()) + for p in KEYS_EXT]) + pm = "\n".join(pt) + bm = "\n".join(['"{k:<11}: {{{t}}}'.format(k=p, t=p.lower()) + for p in KEYS_BLOG]) + return u'"{bg}\n{0}\n"{mid}\n{1}\n"{ed}\n'.format(pm, bm, **G.MARKER) + + POST_BEGIN = property(lambda self: len(self.META_TEMPLATE.splitlines())) + raw_text = '' + html_text = '' + + def __init__(self, edit_type=None, post_id=None): + + self.EDIT_TYPE = edit_type + self.buffer_meta = dict(strid='', edittype=edit_type, + blogaddr=g_data.blog_url) + + self.post_struct_meta = dict(title='', + wp_slug='', + post_type=edit_type, + description='', + custom_fields=[], + post_status='draft') + + if post_id is not None: + self.refresh_from_wp(post_id) + + if self.EDIT_TYPE is None: + self.parse_buffer() + + def parse_buffer(self): + start = 0 + while not vim.current.buffer[start][1:].startswith(G.MARKER['bg']): + start += 1 + + end = start + 1 + while not vim.current.buffer[end][1:].startswith(G.MARKER['ed']): + if not vim.current.buffer[end].startswith('"===='): + line = vim.current.buffer[end][1:].strip().split(":") + k, v = line[0].strip().lower(), ':'.join(line[1:]) + self.buffer_meta[k.strip().lower()] = v.strip().decode('utf-8') + end += 1 + + if self.EDIT_TYPE != self.buffer_meta["edittype"]: + self.EDIT_TYPE = self.buffer_meta["edittype"] + + self.buffer_meta["content"] = '\n'.join( + vim.current.buffer[end + 1:]).decode('utf-8') + + def fill_buffer(self): + meta = dict(strid="", title="", slug="", + cats="", tags="", editformat="Markdown", edittype="") + meta.update(self.buffer_meta) + meta_text = self.META_TEMPLATE.format(**meta)\ + .encode('utf-8').splitlines() + vim.current.buffer[0] = meta_text[0] + vim.current.buffer.append(meta_text[1:]) + content = self.buffer_meta.get("content", ' ')\ + .encode('utf-8').splitlines() + vim.current.buffer.append(content) + + def update_buffer_meta(self): + """ + Updates the meta data region of a blog editing buffer. + @params **kwargs - keyworded arguments + """ + kw = self.buffer_meta + start = 0 + while not vim.current.buffer[start][1:].startswith(G.MARKER['bg']): + start += 1 + + end = start + 1 + while not vim.current.buffer[end][1:].startswith(G.MARKER['ed']): + if not vim.current.buffer[end].startswith('"===='): + line = vim.current.buffer[end][1:].strip().split(":") + k, v = line[0].strip().lower(), ':'.join(line[1:]) + if k in kw: + new_line = u"\"%s: %s" % (line[0], kw[k]) + vim.current.buffer[end] = new_line.encode('utf-8') + end += 1 + + def refresh_from_buffer(self): + self.parse_buffer() + + meta = self.buffer_meta + struct = self.post_struct_meta + + struct.update(title=meta["title"], + wp_slug=meta["slug"], post_type=self.EDIT_TYPE) + + if self.EDIT_TYPE == "post": + struct.update(categories=meta["cats"].split(','), + mt_keywords=meta["tags"].split(',')) + + self.rawtext = rawtext = meta["content"] + + #Translate markdown and save in custom fields. + if meta["editformat"].lower() == "markdown": + if g_data.store_markdown == 'y': + # Store markdown in custom field only when enabled + for f in struct["custom_fields"]: + if f["key"] == G.CUSTOM_FIELD_KEY: + f["value"] = rawtext + break + ## Not found, add new custom field. + else: + field = dict(key=G.CUSTOM_FIELD_KEY, value=rawtext) + struct["custom_fields"].append(field) + + struct["description"] = self.html_text = markdown.markdown(rawtext) + else: + struct["description"] = self.html_text = rawtext + + def refresh_from_wp(self, post_id): + # get from wp + self.post_struct_meta = struct = getattr(g_data.xmlrpc, + "get_" + self.EDIT_TYPE)(post_id) + + # struct buffer meta + meta = dict(editformat="HTML", + title=struct["title"], + slug=struct["wp_slug"]) + + if self.EDIT_TYPE == "post": + meta.update(strid=str(struct["postid"]), + cats=", ".join(struct["categories"]), + tags=struct["mt_keywords"]) + MORE_KEY = "mt_text_more" + else: + meta.update(strid=str(struct["page_id"])) + MORE_KEY = "text_more" + + self.html_text = content = struct["description"] + + #detect more text + post_more = struct.get(MORE_KEY, '') + if len(post_more) > 0: + content += u'' + post_more + struct[MORE_KEY] = '' + self.html_text = struct["description"] = content + + #Use Markdown text if exists in custom fields + for field in struct["custom_fields"]: + if field["key"] == G.CUSTOM_FIELD_KEY: + meta['editformat'] = "Markdown" + self.raw_text = content = field["value"] + vim.command('setl syntax=md_blogsyntax') + break + else: + self.raw_text = content + vim.command('setl syntax=html_blogsyntax') + + meta["content"] = content + + self.buffer_meta.update(meta) + + def save_post(self): + ps = self.post_struct_meta + if self.EDIT_TYPE == "post": + if ps.get("postid", '') == '' and self.post_id == '': + post_id = g_data.xmlrpc.new_post(ps) + else: + post_id = ps["postid"] if "postid" in ps \ + else int(self.post_id) + g_data.xmlrpc.edit_post(post_id, ps) + else: + if ps.get("page_id", '') == '' and self.post_id == '': + post_id = g_data.xmlrpc.new_post(ps) + else: + post_id = ps["page_id"] if "page_id" in ps \ + else int(self.post_id) + g_data.xmlrpc.edit_post(post_id, ps) + + self.refresh_from_wp(post_id) + + post_status = property(lambda self: + self.post_struct_meta[self.EDIT_TYPE + "_status"]) + + @post_status.setter + def post_status(self, data): + if data is not None: + self.post_struct_meta[self.EDIT_TYPE + "_status"] = data + + post_id = property(lambda self: self.buffer_meta["strid"]) + + def html_preview(self): + """ + Opens a browser with a local preview of the content. + @params text_html - the html content + meta - a dictionary of the meta data + """ + if g_data.vimpress_temp_dir == '': + g_data.vimpress_temp_dir = tempfile.mkdtemp(suffix="vimpress") + + html = \ + u""" Vimpress Local Preview: %(title)s %(content)s +""" % dict(content=self.html_text, title=self.buffer_meta["title"]) + with open(os.path.join( + g_data.vimpress_temp_dir, "vimpress_temp.html"), 'w') as f: + f.write(html.encode('utf-8')) + webbrowser.open("file://%s" % f.name) + + def remote_preview(self, pub="draft"): + self.post_status = pub + self.save_post() + webbrowser.open("%s?p=%s&preview=true" % + (g_data.blog_url, self.post_id)) + + +################################################# +# Golbal Variables +################################################# +G = DataObject +g_data = DataObject() + +################################################# +# Helper Functions +################################################# + + +def vim_encoding_check(func): + """ + Decorator. + Check vim environment. wordpress via xmlrpc only support unicode data, + setting vim to utf-8 for all data compatible. + """ + def __check(*args, **kw): + orig_enc = vim.eval("&encoding") + if orig_enc is None: + echomsg("Failed to detech current vim encoding. Make sure your" + " content encoded in UTF-8 to have vimpress work " + "correctly.") + elif orig_enc != "utf-8": + modified = vim.eval("&modified") + buf_list = '\n'.join(vim.current.buffer).decode(orig_enc).encode('utf-8').splitlines() + del vim.current.buffer[:] + vim.command("setl encoding=utf-8") + vim.current.buffer[0] = buf_list[0] + if len(buf_list) > 1: + vim.current.buffer.append(buf_list[1:]) + if modified == '0': + vim.command('setl nomodified') + return func(*args, **kw) + return __check + + +def view_switch(view = "", assert_view = "", reset = False): + """ + Decorator. + For commands to switch between edit/list view, data/status need to be configured. + """ + def switch(func): + def __run(*args, **kw): + if assert_view != '': + if g_data.view != assert_view: + raise VimPressException("Command only available at '%s' view." % assert_view) + + if func.func_name == "blog_new": + if g_data.view == "list": + kw["currentContent"] = [''] + else: + kw["currentContent"] = vim.current.buffer[:] + elif func.func_name == "blog_config_switch": + if g_data.view == "list": + kw["refresh_list"] = True + + if reset: + g_data.xmlrpc.cache_reset() + + if view != '': + #Switching view + if g_data.view != view: + + #from list view + if g_data.view == "list": + for v in g_data.LIST_VIEW_KEY_MAP.values(): + if vim.eval("mapcheck('%s')" % v): + vim.command('unmap %s' % v) + + g_data.view = view + + return func(*args, **kw) + return __run + return switch + + +def blog_wise_open_view(): + """ + Wisely decides whether to wipe out the content of current buffer or open a new splited window. + """ + if vim.current.buffer.name is None and \ + (vim.eval('&modified') == '0' or + len(vim.current.buffer) == 1): + vim.command('setl modifiable') + del vim.current.buffer[:] + vim.command('setl nomodified') + else: + vim.command(":new") + vim.command('setl syntax=md_blogsyntax') + vim.command('setl completefunc=vimrepress#CateComplete') + + +@vim_encoding_check +def vim_input(message = 'input', secret = False): + vim.command('call inputsave()') + vim.command("let s:user_input = %s('%s :')" % (("inputsecret" if secret else "input"), message)) + vim.command('call inputrestore()') + return vim.eval('s:user_input') + + +################################################# +# Command Functions +################################################# + +@exception_check +@vim_encoding_check +@view_switch(assert_view = "edit", reset = True) +def blog_save(pub = None): + """ + Saves the current editing buffer. + @params pub - either "draft" or "publish" + """ + if pub not in ("publish", "draft", None): + raise VimPressException(":BlogSave draft|publish") + cp = g_data.current_post + assert cp is not None, "Can't get current post obj." + cp.refresh_from_buffer() + + if cp.buffer_meta["blogaddr"] != g_data.blog_url and cp.post_id != '': + confirm = vim_input("Are u sure saving it to \"%s\" ? BlogAddr in current buffer does NOT matched. \nStill saving it ? (may cause data lost) [yes/NO]" % g_data.blog_url) + assert confirm.lower() == 'yes', "Aborted." + + cp.post_status = pub + cp.save_post() + cp.update_buffer_meta() + g_data.current_post = cp + notify = "%s ID=%s saved with status '%s'" % (cp.post_status, cp.post_id, cp.post_status) + echomsg(notify) + vim.command('setl nomodified') + + +@exception_check +@vim_encoding_check +@view_switch(view = "edit") +def blog_new(edit_type = "post", currentContent = None): + """ + Creates a new editing buffer of specified type. + @params edit_type - either "post" or "page" + """ + if edit_type.lower() not in ("post", "page", "category"): + raise VimPressException("Invalid option: %s " % edit_type) + + if edit_type.lower() == "category": + category_name = vim_input("New Category name") + category_slug = vim_input("Category slug (optional)") + ret = g_data.xmlrpc.new_category(dict(name = category_name, slug = category_slug)) + + if type(ret) is int: + echomsg("Category '%s' created with ID %d. Updating local cache ... " + % (category_name, ret)) + categories = [i["categoryName"].encode("utf-8") + for i in g_data.xmlrpc.get_categories()] + vim.command('let s:completable = "%s"' % '|'.join(categories)) + echomsg("Done.") + else: + echoerr("Category create ERROR: " + str(ret)) + + else: + blog_wise_open_view() + g_data.current_post = ContentStruct(edit_type = edit_type) + cp = g_data.current_post + cp.fill_buffer() + + +@view_switch(view = "edit") +def blog_edit(edit_type, post_id): + """ + Opens a new editing buffer with blog content of specified type and id. + @params edit_type - either "post" or "page" + post_id - the id of the post or page + """ + blog_wise_open_view() + if edit_type.lower() not in ("post", "page"): + raise VimPressException("Invalid option: %s " % edit_type) + post_id = str(post_id) + + if post_id in g_data.post_cache: + cp = g_data.current_post = g_data.post_cache[post_id] + else: + cp = g_data.current_post = ContentStruct(edit_type = edit_type, post_id = post_id) + + cp.fill_buffer() + vim.current.window.cursor = (cp.POST_BEGIN, 0) + vim.command('setl nomodified') + vim.command('setl textwidth=0') + for v in G.LIST_VIEW_KEY_MAP.values(): + if vim.eval("mapcheck('%s')" % v): + vim.command('unmap %s' % v) + + +@view_switch(assert_view = "list") +def blog_delete(edit_type, post_id): + """ + Deletes a page or post of specified id. + @params edit_type - either "page" or "post" + post_id - the id of the post or page + """ + if edit_type.lower() not in ("post", "page"): + raise VimPressException("Invalid option: %s " % edit_type) + deleted = getattr(g_data.xmlrpc, "delete_" + edit_type)(post_id) + assert deleted is True, "There was a problem deleting the %s." % edit_type + echomsg("Deleted %s id %s. " % (edit_type, str(post_id))) + g_data.xmlrpc.cache_remove_post(post_id) + blog_list(edit_type) + + +@exception_check +@view_switch(assert_view = "list") +def blog_list_on_key_press(action, edit_type): + """ + Calls blog open on the current line of a listing buffer. + """ + if action.lower() not in ("open", "delete"): + raise VimPressException("Invalid option: %s" % action) + + row = vim.current.window.cursor[0] + line = vim.current.buffer[row - 1] + id = line.split()[0] + title = line[len(id):].strip() + + try: + int(id) + except ValueError: + if line.find("More") != -1: + assert g_data.xmlrpc.is_reached_title_max is False, "No more posts." + vim.command("setl modifiable") + del vim.current.buffer[len(vim.current.buffer) - 1:] + append_blog_list(edit_type) + vim.current.buffer.append(G.MARKER['more']) + vim.command("setl nomodified") + vim.command("setl nomodifiable") + return + else: + raise VimPressException("Move cursor to a post/page line and press Enter.") + + if len(title) > 30: + title = title[:30] + ' ...' + + if action.lower() == "delete": + confirm = vim_input("Confirm Delete [%s]: %s? [yes/NO]" % (id, title)) + assert confirm.lower() == 'yes', "Delete Aborted." + + vim.command("setl modifiable") + del vim.current.buffer[:] + vim.command("setl nomodified") + + if action == "open": + blog_edit(edit_type, int(id)) + elif action == "delete": + blog_delete(edit_type, int(id)) + + +def append_blog_list(edit_type, count = G.DEFAULT_LIST_COUNT): + if edit_type.lower() == "post": + current_posts = len(vim.current.buffer) - 1 + retrive_count = int(count) + current_posts + posts_titles = g_data.xmlrpc.get_recent_post_titles(retrive_count) + + vim.current.buffer.append( + [(u"%(postid)s\t%(title)s" % p).encode('utf-8') + for p in posts_titles[current_posts:]]) + else: + pages = g_data.xmlrpc.get_page_list() + vim.current.buffer.append( + [(u"%(page_id)s\t%(page_title)s" % p).encode('utf-8') for p in pages]) + + +@exception_check +@vim_encoding_check +@view_switch(view = "list") +def blog_list(edit_type = "post", keep_type = False): + """ + Creates a listing buffer of specified type. + @params edit_type - either "post(s)" or "page(s)" + """ + if keep_type: + first_line = vim.current.buffer[0] + assert first_line.find("List") != -1, "Failed to detect current list type." + edit_type = first_line.split()[1].lower() + + blog_wise_open_view() + vim.current.buffer[0] = G.MARKER["list_title"] % \ + dict(edit_type = edit_type.capitalize(), blog_url = G.blog_url) + + if edit_type.lower() not in ("post", "page"): + raise VimPressException("Invalid option: %s " % edit_type) + + append_blog_list(edit_type, G.DEFAULT_LIST_COUNT) + + if edit_type == "post": + vim.current.buffer.append(G.MARKER['more']) + + vim.command("setl nomodified") + vim.command("setl nomodifiable") + vim.current.window.cursor = (2, 0) + vim.command("map %(enter)s :py blog_list_on_key_press('open', '%%s')" + % G.LIST_VIEW_KEY_MAP % edit_type) + vim.command("map %(delete)s :py blog_list_on_key_press('delete', '%%s')" + % G.LIST_VIEW_KEY_MAP % edit_type) + echomsg("Press to edit. to move to trash.") + + +@exception_check +@vim_encoding_check +@view_switch(assert_view = "edit") +def blog_upload_media(file_path): + """ + Uploads a file to the blog. + @params file_path - the file's path + """ + if not os.path.exists(file_path): + raise VimPressException("File does not exist: %s" % file_path) + + name = os.path.basename(file_path) + filetype = mimetypes.guess_type(file_path)[0] + with open(file_path) as f: + bits = xmlrpclib.Binary(f.read()) + + result = g_data.xmlrpc.new_media_object(dict(name = name, type = filetype, bits = bits)) + + ran = vim.current.range + if filetype.startswith("image"): + img = G.IMAGE_TEMPLATE % result + ran.append(img) + else: + ran.append(result["url"]) + ran.append('') + + +@exception_check +@vim_encoding_check +@view_switch(assert_view = "edit") +def blog_append_code(code_type = ""): + html = \ +"""
+
""" + if code_type == "": + code_type = ("text", "") + else: + code_type = (code_type, ' line="1"') + html = html % code_type + row, col = vim.current.window.cursor + code_block = html.splitlines() + vim.current.range.append(code_block) + vim.current.window.cursor = (row + len(code_block), 0) + + +@exception_check +@vim_encoding_check +@view_switch(assert_view = "edit") +def blog_preview(pub = "local"): + """ + Opens a browser window displaying the content. + @params pub - If "local", the content is shown in a browser locally. + If "draft", the content is saved as a draft and previewed remotely. + If "publish", the content is published and displayed remotely. + """ + cp = g_data.current_post + cp.refresh_from_buffer() + if pub == "local": + cp.html_preview() + elif pub in ("publish", "draft"): + cp.remote_preview(pub) + if pub == "draft": + echomsg("You have to login in the browser to preview the post when save as draft.") + else: + raise VimPressException("Invalid option: %s " % pub) + + +@exception_check +def blog_guess_open(what): + """ + Tries several methods to get the post id from different user inputs, such as args, url, postid etc. + """ + post_id = '' + blog_index = -1 + if type(what) is str: + + if what.startswith(g_data.blog_url): + blog_index = 1 + + # User input a url contained in the profiles + if blog_index != -1: + guess_id = re.search(r"\S+?p=(\d+)$", what) + + # permalinks + if guess_id is None: + + # try again for /archives/%post_id% + guess_id = re.search(r"\S+/archives/(\d+)", what) + + # fail, try get full link from headers + if guess_id is None: + headers = urllib.urlopen(what).headers.headers + for link in headers: + if link.startswith("Link:"): + post_id = re.search(r"<\S+?p=(\d+)>", link).group(1) + + else: + post_id = guess_id.group(1) + + # full link with ID (http://blog.url/?p=ID) + else: + post_id = guess_id.group(1) + + # detected something ? + assert post_id != '', "Failed to get post/page id from '%s'." % what + + #switch view if needed. + if blog_index != -1 and blog_index != g_data.conf_index: + blog_config_switch(blog_index) + + # User input not a url, try numeric + else: + try: + post_id = str(int(what)) + except ValueError: + raise VimPressException("Failed to get post/page id from '%s'." % what) + + blog_edit("post", post_id) + + +@exception_check +@vim_encoding_check +@view_switch() +def blog_config_switch(index = -1, refresh_list = False): + """ + Switches the blog to the 'index' of the configuration array. + """ + g_data.conf_index = index + if refresh_list: + blog_list(keep_type = True) + echomsg("Vimpress switched to '%s'@'%s'" % (g_data.blog_username, g_data.blog_url)) + + diff --git a/plugin/vimrepress.vim b/plugin/vimrepress.vim new file mode 100644 index 0000000..c5a2a18 --- /dev/null +++ b/plugin/vimrepress.vim @@ -0,0 +1,118 @@ +"####################################################################### +" Copyright (C) 2007 Adrien Friggeri. +" +" 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 2, 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, write to the Free Software Foundation, +" Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +" +" Contributors: Adrien Friggeri +" Pigeond +" Justin Sattery +" Lenin Lee +" Conner McDaniel +" +" Forked By: Preston M.[BOYPT] +" Repository: https://bitbucket.org/pentie/vimrepress +" +" URL: http://www.friggeri.net/projets/vimblog/ +" http://pigeond.net/blog/2009/05/07/vimpress-again/ +" http://pigeond.net/git/?p=vimpress.git +" http://fzysqr.com/ +" http://apt-blog.net +" +" VimRepress +" - A mod of a mod of a mod of Vimpress. +" - A vim plugin fot writting your wordpress blog. +" - Write with Markdown, control posts format precisely. +" - Stores Markdown rawtext in wordpress custom fields. +" +" Version: 3.2.1 +" +" Config: Create account configure as `~/.vimpressrc' in the following +" format: +" +"[Blog0] +"blog_url = http://a-blog.com/ +"username = admin +"password = 123456 +" +"[Blog1] +"blog_url = https://someone.wordpress.com/ +"username = someone +"password = +" +"####################################################################### + +if !has("python") + finish +endif + +let s:py_loaded = 0 +let s:vimpress_dir = fnamemodify(expand(""), ":p:h") + +function! s:CompSave(ArgLead, CmdLine, CursorPos) + return "publish\ndraft\n" +endfunction + +function! s:CompPrev(ArgLead, CmdLine, CursorPos) + return "local\npublish\ndraft\n" +endfunction + +function! s:CompEditType(ArgLead, CmdLine, CursorPos) + return "post\npage\n" +endfunction + +function! s:CompNewType(ArgLead, CmdLine, CursorPos) + return "post\npage\ncategory\n" +endfunction + +function! vimrepress#CateComplete(findstart, base) + if a:findstart + " locate the start of the word + let line = getline('.') + let start = col('.') - 1 + while start > 0 && line[start - 1] =~ '\a' + let start -= 1 + endwhile + return start + else + " find matching items + let res = [] + for m in split(s:completable,"|") + if m =~ '^' . a:base + call add(res, m) + endif + endfor + return res + endif +endfun + +function! s:PyCMD(pyfunc) + if (s:py_loaded == 0) + exec("cd " . s:vimpress_dir) + let s:pyfile = fnamemodify("vimrepress.py", ":p") + exec("cd -") + exec("pyfile " . s:pyfile) + let s:py_loaded = 1 + endif + exec('python ' . a:pyfunc) +endfunction + +command! -nargs=? -complete=custom,s:CompEditType BlogList call s:PyCMD('blog_list()') +command! -nargs=? -complete=custom,s:CompNewType BlogNew call s:PyCMD('blog_new()') +command! -nargs=? -complete=custom,s:CompSave BlogSave call s:PyCMD('blog_save()') +command! -nargs=? -complete=custom,s:CompPrev BlogPreview call s:PyCMD('blog_preview()') +command! -nargs=1 -complete=file BlogUpload call s:PyCMD('blog_upload_media()') +command! -nargs=1 BlogOpen call s:PyCMD('blog_guess_open()') +command! -nargs=? BlogSwitch call s:PyCMD('blog_config_switch()') +command! -nargs=? BlogCode call s:PyCMD('blog_append_code()') diff --git a/release_vimpress.sh b/release_vimpress.sh new file mode 100755 index 0000000..3aaf837 --- /dev/null +++ b/release_vimpress.sh @@ -0,0 +1,16 @@ +#!/bin/bash + + +TEMP_DIR=/tmp/vimpress_relase +REV=`hg summary|grep -e "^parent"|awk '{print $2}'|tr ':' '_'` +BRANCH=`hg branch` +hg archive $TEMP_DIR +VER=`grep Version plugin/vimrepress.vim | awk '{print $3}'` +RELEASE_FILE="/tmp/"$BRANCH"_"$VER"_r"$REV".zip" +cd $TEMP_DIR +if [[ -f $RELEASE_FILE ]]; then rm $RELEASE_FILE; fi +zip -x 'README.md' -x '.hgtags' -x '.hg_archival.txt' -x markdown_posts_upgrade.py -x release_vimpress.sh -r $RELEASE_FILE . +rm -rf $TEMP_DIR +echo $RELEASE_FILE + + diff --git a/syntax/blogsyntax.vim b/syntax/html_blogsyntax.vim similarity index 93% rename from syntax/blogsyntax.vim rename to syntax/html_blogsyntax.vim index 130bef5..1c44e28 100644 --- a/syntax/blogsyntax.vim +++ b/syntax/html_blogsyntax.vim @@ -3,6 +3,7 @@ if version < 600 elseif exists("b:current_syntax") finish endif +runtime! syntax/html.vim sy match blogeditorEntry "^ *[0-9]*\t.*$" sy match blogeditorComment '^".*$' sy match blogeditorIdent '^"[^:]*:' diff --git a/syntax/md_blogsyntax.vim b/syntax/md_blogsyntax.vim new file mode 100644 index 0000000..e9b6c5c --- /dev/null +++ b/syntax/md_blogsyntax.vim @@ -0,0 +1,13 @@ +if version < 600 + syntax clear +elseif exists("b:current_syntax") + finish +endif +runtime! syntax/markdown.vim +sy match blogeditorEntry "^ *[0-9]*\t.*$" +sy match blogeditorComment '^".*$' +sy match blogeditorIdent '^"[^:]*:' +hi link blogeditorComment Comment +hi link blogeditorEntry Directory +hi link blogeditorIdent Function +let b:current_syntax = "blogsyntax"