在電梯裡遇見雙胞胎 Dev::Coder
首頁 | 關於我 | 筆記 // 當你開始尋找,你就已經在前往的路上...

安裝

blogpost 的程式碼用 Mercurial (hg) 代管在 Gooble Project Hosting,但文件都還是放在 AsciiDoc/blogpost 的網站 - Stuart’s Notes。

首先取得 blogpost 的程式碼,並完成初步的組態:

$ hg clone https://code.google.com/p/blogpost/ 1
destination directory: blogpost
requesting all changes
adding changesets
adding manifests
adding file changes
added 93 changesets with 157 changes to 13 files
updating to branch default
9 files updated, 0 files merged, 0 files removed, 0 files unresolved

$ cd blogpost; tree -F
.
|-- asciidocapi.py
|-- blogpost.py*
|-- conf/
|   |-- blogpost_example.conf
|   `-- wordpress-8.2.7.conf
|-- doc/
|   `-- blogpost.1.txt
|-- README
`-- wordpresslib.py

2 directories, 7 files

$ ln -s blogpost.py ~/bin/blogpost             2

$ ln -s conf/blogpost_example.conf ~/.blogpost 3

$ vi ~/.blogpost
  #
  # Example blogpost.py configuration file.
  #

  # Wordpress XML_RPC URL (don't forget to append /xmlrpc.php)
  URL = 'http://joebloggs.wordpress.com/xmlrpc.php' 4

  # Wordpress login name.
  USERNAME = 'joebloggs'

  # Wordpress password.
  PASSWORD = 'secret'

  # Leading command-line arguments to start asciidoc.
  # Default
  #ASCIIDOC = ['asciidoc']
  # UNIX example
  #ASCIIDOC = ['python', '/home/srackham/projects/asciidoc/trunk/asciidoc.py']
  # Windows example
  #ASCIIDOC = ['python', 'c:\\bin\\asciidoc\\asciidoc.py']
1 將程式碼下載到適當的地方。
2blogpost.py 放到 ~/bin 下,方便在任何地方執行 blogpost
3conf/blogpost_example.conf 為範本,將組態檔放到 ~/.blogpost
4URL 裡的 joebloggs 代換成自己的名字,例如 http://imsardine.wordpress.com/xmlrpc.php,然後將自己的帳號密碼填入 USERNAMEPASSWORD 即可。
Tip 登入資訊除了可以組態在 ~/.blogpost 之外,也可以直接寫在 blogpost.py 裡。但維護多支 blogpost.py 的成本相對比較高(例如 blogpost.py 有些小 bug 需要被 hack、日後要昇級多支 .py 檔等),如果有發佈到多個 WordPress 網誌的需求,可以透過 blogpost --conf-file=CONF_FILE 指定不同網誌專用的組態檔。

[NOTE] .~/.blogpost 裡的 ASCIIDOC 是個晃子?

blogpost 也是透過 AsciiDoc 將 .txt 轉成 .html,因此本機也要安裝有 AsciiDoc 才行。

從組態檔的設計來看,裡頭的 ASCIIDOC 似乎可以用來指定外部程式 asciidoc 的位置,但 trace 過原始碼之後,才發現 blogpost 根本不理這個設定,內部會試著找出 asciidoc module 直接做 import,而非以外部程式的方式整合 AsciiDoc。它會依序從下面這幾個地方找出 asciidoc module:

  • 環境變數 ASCIIDOC_PY

  • 從環境變數 PATH 底下去找 asciidoc.pyasciidoc.pycasciidoc

  • 從目前的工作目錄(current working directory)底下去找 asciidoc.pyasciidoc.pycasciidoc

細節請參考下面 “拆解 blogpost.pyasciidocapi.py“ 一節的說明。

測試安裝:

$ blogpost list

只要沒有出現任何錯誤訊息,就代表已經安裝成功。

blogpost 的基本用法

先瞭解一下 blogpost 的用法:

blogpost [OPTIONS] COMMAND [BLOG_FILE]

其中 BLOG_FILE 是 AsciiDoc 原始的純文字檔(或產生出來的 HTML),至於 COMMAND 可以是:[TBD]

  • categories - 列出所有 category,方便下達 -c CATEGORIES, --categories=CATEGORIES

  • delete -

  • dump -

  • info -

  • list -

  • post - post 同時支援 create 跟 update 兩種動作,但 update 不支援 title 的更新?

例如: 加上 --force 與 -a icons 的例子 [TBD]

$ vi helloworld.txt
= Hello World =

Say hello from AsciiDoc + blogpost

$ blogpost post helloworld.txt
blogpost: creating published post 'Hello World ='... 1
blogpost: id: 37
blogpost: url: http://imsardine.wordpress.com/2011/11/26/hello-world/

$ blogpost list 2
title:      Hello World =
id:         37
url:        http://imsardine.wordpress.com/2011/11/26/hello-world/
type:       post
categories: Uncategorized
created:    Sat Nov 26 15:20:28 2011

$ vi helloworld.txt
$ blogpost post helloworld.txt
blogpost: updating published post 'Hello World ='... 3
blogpost: id: 37
blogpost: url: http://imsardine.wordpress.com/2011/11/26/hello-world/

$ blogpost post helloworld.txt
blogpost: skipping unmodified: /tmp/helloworld.txt
$ cat helloworld.blogpost
...
p11
sS'checksum'
p12
S'0428c0bb228bee3769da7b6cad4f9163' 4
...
S'url'
p27
S'http://imsardine.files.wordpress.com/2011/11/6.png' 5
p28
sS'checksum'
p29
S'f64cfcfac0c4219edebc96d6d043c84c'
...

$ blogpost delete helloworld.txt    6
blogpost: deleting post 37...
blogpost: deleting cache file: /tmp/helloworld.blogpost
1blogpost posthelloworld.txt 發表到 WordPress。
2blogpost list 列出已發表的文章。
3 blogpost post 也可以用來更新文章。
4 為什麼 blogpost 知道文件沒有變更過?那是因為 blogpost post 會產生一支對應的 .blogpost(用 pickle 寫出的),記錄最後一次發佈到 WordPress 的狀態。
5 看起來不同文件並不能共用圖檔?
6blogpost delete 將已發表的文章移到回收桶。

[TBD]:

  • -c CATEGORIES, --categories=CATEGORIES

  • -f CONF_FILE, --conf-file=CONF_FILE

  • 簡單說明 xxx.blogpost 裡面記錄著什麼東西

  • 不知道為什麼 update 幾次之後,文章會變成排程中,好像固定會加 8 小時?

可以直接寫在檔案裡的 options 有:

  • blogpost-status = published | unpublished

  • blogpost-doctype = article | book | manpage

  • blogpost-posttype = page | post

  • blogpost-title

  • blogpost-categories

用 AsciiDoc 寫一篇 WordPress 網誌 [TBD]

沒什麼不同,要知道如何從檔案裡直接給 categories 而已。

挑選一個適合 AsciiDoc 產出的佈景主題

挑選的條件有:

  • 顯示主要內容的部份要夠寬,而且字體不能太小。

  • 由於沒有 TOC,因此 section title 必須要能夠被突顯出來,否則文件很長時會分不清楚章節。

推薦幾個效果還不錯的:

拆解 blogpost.pyasciidocapi.py

.
|-- asciidocapi.py
|-- blogpost.py*
|-- conf/
|   |-- blogpost_example.conf
|   `-- wordpress-8.2.7.conf 1
|-- doc/
|   `-- blogpost.1.txt
|-- README
`-- wordpresslib.py
1 AsciiDoc 自 8.3.0 之後就開始內建 wordpress.conf,這裡提供的 wordpress-8.2.7.conf 是給 AsciiDoc 8.2.7 之前的版本使用的(8.2.7 是 8.3.0 的前一個版本)。
blogpost.py
import wordpresslib # http://www.blackbirdblog.it/programmazione/progetti/28 1
import asciidocapi 2

######################################################################
# Configuration file parameters.
# Create a separate configuration file named .blogpost in your $HOME
# directory or use the --conf-file option (see the
# blogpost_example.conf example).
# Alternatively you could just edit the values below. 3
######################################################################

URL = None      # Wordpress XML-RPC URL (don't forget to append /xmlrpc.php)
USERNAME = None # Wordpress login name.
PASSWORD = None # Wordpress password.

def load_conf(conf_file):
    """
    Import optional configuration file which is used to override global
    configuration settings.
    """
    execfile(conf_file, globals()) 4

OPTIONS = None  # Parsed command-line options OptionParser object. 5

class Media(object):

    def __init__(self, filename):
        self.filename = filename # Client file name.
        self.checksum = None     # Client file MD5 checksum.
        self.url = None          # WordPress media file URL.

    def upload(self, blog):
        """
        Upload media file to WordPress server if it is new or has changed.
        """
        checksum = md5.new(open(self.filename,'rb').read()).hexdigest()
        if not (blog.options.force_media
                or self.checksum is None
                or self.checksum != checksum):
            infomsg('skipping unmodified: %s' % self.filename)
        else:
            infomsg('uploading: %s...' % self.filename)
            if not blog.options.dry_run:
                self.url =  blog.server.newMediaObject(self.filename)
                infomsg('url: %s' % self.url)
            else:
                self.url = self.filename  # Dummy value for debugging.
            self.checksum = checksum

class Blogpost(object):

    # Valid blog parameter names.
    PARAMETER_NAMES = ('categories','status','title','doctype','posttype')

    def set_blog_file(self, blog_file):
        if blog_file is not None:
            self.blog_file = blog_file
            if self.media_dir is None:
                self.media_dir = os.path.abspath(os.path.dirname(blog_file)) 6
            self.cache_file = os.path.splitext(blog_file)[0] + '.blogpost'

    def set_title_from_blog_file(self): 7
        """
        Set title attribute from title in blog file.
        """
        if not self.is_html():
            # AsciiDoc blog file.
            #TODO: Skip leading comment blocks.
            for line in open(self.blog_file):
                # Skip blank lines and comment lines.
                if not re.match(r'(^//)|(^\s*$)', line):
                    break
            else:
                die('unable to find document title in %s' % self.blog_file)
            self.title = line.strip()
            if self.title.startswith('= '):
                self.title = line[2:].strip() 8

    def asciidoc2html(self):
        """
        Convert AsciiDoc blog_file to Wordpress compatible HTML content.
        """
        asciidoc = asciidocapi.AsciiDocAPI()         9
        asciidoc.options('--no-header-footer')
        asciidoc.options('--doctype', self.doctype)
        asciidoc.options('--attribute', 'blogpost')  10
        for attr in OPTIONS.attributes:              11
            asciidoc.options('--attribute', attr)
        for opt in OPTIONS.asciidoc_opts:
            print '%r' % opt
            opt = opt.partition(' ')
            if opt[2]:
                s = opt[2]
                s = s.strip()
                if (s.startswith('"') and s.endswith('"')
                        or s.startswith("'") and s.endswith("'")):
                    # Strip quotes.
                    s = s[1:-1]
                asciidoc.options(opt[0], s)
            else:
                asciidoc.options(opt[0])
        if OPTIONS.verbose > 1:
            asciidoc.options('--verbose')
        verbose('asciidoc: options: %r' % asciidoc.options.values)
        outfile = StringIO.StringIO()
        asciidoc.execute(self.blog_file, outfile, backend='wordpress') 12
        result = outfile.getvalue()
        result = unicode(result,'utf8')
        self.content = StringIO.StringIO(result.encode('utf8'))
        for s in asciidoc.messages:
            infomsg('asciidoc: %s' % s)

    def get_parameters(self):
        '''
        Load blogpost parameters from AsciiDoc blogpost file. 13
        Check attribute value validity.
        '''
        def check_value(*valid_values):
            if value not in valid_values:
                die('%s: line %d: invalid attribute value: blogpost-%s: %s' %
                    (os.path.basename(self.blog_file), lineno, name, value))

        if self.blog_file is None:
            return
        if os.path.splitext(self.blog_file)[1].lower() in ('.htm','.html'):
            return
        reo = re.compile(r':blogpost-(?P<name>[-\w]+):\s+(?P<value>.*)')
        lineno = 1
        for line in open(self.blog_file):
            mo = re.match(reo, line)
            if mo:
                name = mo.group('name')
                value = mo.group('value').strip()
                if name in self.PARAMETER_NAMES:
                    self.parameters[name] = value
                else:
                    warning('%s: line %d: invalid attribute name: blogpost-%s' %
                            (os.path.basename(self.blog_file), lineno, name))
                if name == 'status':
                    check_value('published','unpublished')
                elif name == 'doctype':
                    check_value('article','book','manpage')
                elif name == 'posttype':
                    check_value('page','post')
            lineno += 1

    def process_media(self):
        """
        Upload images referenced in the HTML content and replace content urls
        with WordPress urls.

        Source urls are considered relative to self.media_dir.
        Processes <a> and <img> tags provided they reference files with
        valid media file name extensions.

        Caches the names and checksum of uploaded files in self.cache_file.  If
        self.cache_file is None then caching is not used and no cache file
        written.
        """
        # All these extensions may not be supported by your WordPress server,
        # Check with your hoster if you get an 'Invalid file type' error.
        media_exts = (
            'gif','jpg','jpeg','png',
            'pdf','doc','odt',
            'mp3','ogg','wav','m4a','mov','wmv','avi','mpg',
        )
        result = StringIO.StringIO()
        rexp = re.compile(r'(?i)<(?P<tag>(a\b[^>]* href)|(img\b[^>]* src))="(?P<src>.+?)"')
        for line in self.content: 14
            lineout = ''
            mo = rexp.search(line)
            while mo:
                tag = mo.group('tag')
                src = mo.group('src')
                url = src
                if os.path.splitext(src)[1][1:].lower() in media_exts:
                    media_obj = self.media.get(src)
                    media_file = os.path.join(self.media_dir, src)
                    if not os.path.isfile(media_file):
                        if media_obj:
                            url =  media_obj.url
                            infomsg('missing media file: %s' % media_file)
                        else:
                            url = src
                    else:
                        if not media_obj:
                            media_obj = Media(media_file)
                            self.media[src] = media_obj
                        media_obj.upload(self)
                        url =  media_obj.url
                        self.updated_at = int(time.time())
                lineout += line[:mo.start()] + ('<%s="%s"' % (tag, url))
                line = line[mo.end():]
                mo = rexp.search(line)
            lineout += line
            result.write(lineout)
        result.seek(0)
        self.content = result
1wordpresslib 跟 WordProcess 溝通。
2asciidocapi.txt 轉成 .html
3 跟 WordPress 的連線資訊也可以直接組態在 blogpost.py 裡。
4 直接執行組態檔,裡面定義有 URLUSERNAMEPASSWORDASCIIDOC,不過 ASCIIDOC 好像沒有用到?在 WordPress 上同時有多個 blog 要維護時,把登入資訊寫在 blogpost.py 會比較方便。
5 blogpost 所有的 command-line options 都會被搜集到這裡來。
6 如果沒有給定 --media-dir 的話,預設以 blog file (*.txt) 的位置做為起點。
7 原來 title 是從 .txt 檔由下往下找到第一行不是註解的,然後再從中取出文字。
8line[2:].strip() 改成 a[2:][:-2].strip() 就可以解決抬頭後面總是跟著 ` =` 的問題。
9 沒有給定 asciidoc_py 的參數,所以 AsciiDocAPI 內部會自動尋找 asciidoc 指令的位置。
10 這表示文件裡有機會判斷 blogpost 這個 attribute 是否有被定義來做出不同的反應。
11 從 command-line 設定 attribute 是透過 -a ATTRIBUTE--attribute=ATTRIBUTE,而非 --asciidoc-opt
12 blogpost 並沒有真正寫出 .html,而是從記憶體接下轉檔的結果。
13 blogpost 的 command-line options 可以直接寫在 .txt 裡,例如 :blogpost-categories: asciidoc,wordpress。(前面固定冠上 blogpost-)。
14 從 HTML 的產出去分析 <a><img>,以相對於 media_dir 的路徑來解讀。
asciidocapi.py
def find_in_path(fname, path=None):
    """
    Find file fname in paths. Return None if not found.
    """
    if path is None:
        path = os.environ.get('PATH', '')
    for dir in path.split(os.pathsep):
        fpath = os.path.join(dir, fname)
        if os.path.isfile(fpath):
            return fpath
    else:
        return None

class Options(object):
    """
    Stores asciidoc(1) command options.
    """
    def __init__(self, values=[]):
        self.values = values[:]
    def __call__(self, name, value=None): 1
        """Shortcut for append method."""
        self.append(name, value)
    def append(self, name, value=None):
        if type(value) in (int,float):
            value = str(value)
        self.values.append((name,value))  2

class AsciiDocAPI(object):
    """
    AsciiDoc API class.
    """
    def __init__(self, asciidoc_py=None):
        """
        Locate and import asciidoc.py.
        Initialize instance attributes.
        """
        self.options = Options()
        self.attributes = {}
        self.messages = []
        # Search for the asciidoc command file.
        # Try ASCIIDOC_PY environment variable first.
        cmd = os.environ.get('ASCIIDOC_PY')
        if cmd:
            if not os.path.isfile(cmd):
                raise AsciiDocError('missing ASCIIDOC_PY file: %s' % cmd)
        elif asciidoc_py:
            # Next try path specified by caller.
            cmd = asciidoc_py
            if not os.path.isfile(cmd):
                raise AsciiDocError('missing file: %s' % cmd)
        else:
            # Try shell search paths.
            for fname in ['asciidoc.py','asciidoc.pyc','asciidoc']:
                cmd = find_in_path(fname)
                if cmd: break
            else:
                # Finally try current working directory.
                for cmd in ['asciidoc.py','asciidoc.pyc','asciidoc']:
                    if os.path.isfile(cmd): break
                else:
                    raise AsciiDocError('failed to locate asciidoc')
        self.cmd = os.path.realpath(cmd)
        self.__import_asciidoc()

    def __import_asciidoc(self, reload=False):
        '''
        Import asciidoc module (script or compiled .pyc).
        See
        http://groups.google.com/group/asciidoc/browse_frm/thread/66e7b59d12cd2f91
        for an explanation of why a seemingly straight-forward job turned out
        quite complicated.
        '''
        if os.path.splitext(self.cmd)[1] in ['.py','.pyc']:
            sys.path.insert(0, os.path.dirname(self.cmd))
            try:
                try:
                    if reload:
                        import __builtin__  # Because reload() is shadowed.
                        __builtin__.reload(self.asciidoc)
                    else:
                        import asciidoc
                        self.asciidoc = asciidoc
                except ImportError:
                    raise AsciiDocError('failed to import ' + self.cmd)
            finally:
                del sys.path[0]
        else:
            # The import statement can only handle .py or .pyc files, have to
            # use imp.load_source() for scripts with other names.
            try:
                imp.load_source('asciidoc', self.cmd)
                import asciidoc
                self.asciidoc = asciidoc
            except ImportError:
                raise AsciiDocError('failed to import ' + self.cmd)
        if Version(self.asciidoc.VERSION) < Version(MIN_ASCIIDOC_VERSION):
            raise AsciiDocError(
                'asciidocapi %s requires asciidoc %s or better'
                % (API_VERSION, MIN_ASCIIDOC_VERSION))

    def execute(self, infile, outfile=None, backend=None):
        """
        Compile infile to outfile using backend format.
        infile can outfile can be file path strings or file like objects.
        """
        self.messages = []
        opts = Options(self.options.values)
        if outfile is not None:
            opts('--out-file', outfile)
        if backend is not None:
            opts('--backend', backend)
        for k,v in self.attributes.items():
            if v == '' or k[-1] in '!@':
                s = k
            elif v is None: # A None value undefines the attribute.
                s = k + '!'
            else:
                s = '%s=%s' % (k,v)
            opts('--attribute', s)
        args = [infile]
        # The AsciiDoc command was designed to process source text then
        # exit, there are globals and statics in asciidoc.py that have
        # to be reinitialized before each run -- hence the reload.
        self.__import_asciidoc(reload=True)
        try:
            try:
                self.asciidoc.execute(self.cmd, opts.values, args)
            finally:
                self.messages = self.asciidoc.messages[:]
        except SystemExit, e:
            if e.code:
                raise AsciiDocError(self.messages[-1])
1 blogpost.py 大量用到這個 shortcut。
2 內部用一個 list 來維護 key/value pair,不斷地串接下去。

TBD: 找 asciidoc module 的過程。

如果覺得這份文件對您有幫助,別忘了按個讚並分享給更多的人;有任何需要改進的地方,也請不吝指教(請在下面留言),謝謝!
comments powered by Disqus