| 作者: | limodou |
|---|---|
| 联系: | limodou AT gmail.com |
| 版本: | 0.1 |
| 主页: | http://wiki.woodpecker.org.cn/moin/NewEdit |
| BLOG: | http://www.donews.net/limodou |
| 版权: | FDL |
我没有看错吧!你可能要问。的确没有。这里是我在了解了“ 文学编程 ”的一些基本知识后,萌生的一个想法。学过 Python 的人可能知道, docutils 是一个 Python 下的文档处理模块,它使用一种叫 reStructuredText(以下简写为reST)的文本书写格式来编写文档,这种格式是从 StructuredText 发展起来的,通过一些约定的文本格式,就可以通过转换工具将文档由文本转换为 Html, Latex, DocBook等许多种格式。而且 docutils 虽然是使用 Python 编写的,但 reST 并不是只限于 Python 的应用,你可以用它来书写任意的内容。而且许多的 Python 项目都采用它来编写文档。
什么叫文学编程?简单地讲就是采用文学创作的方式来写程序,因此更看重文档的作用,它是一种文档与代码的混合。它是由Donald Knuth 在1984年提出的。具体的可以参见 literateprogramming 网站。
因此我想如果可以对 reST 进行扩展,增加文学编程的功能,岂不是件好事。
在分析了 docutils 之后,我认为可以通过扩展 directive(指示器) 来实现。一个标准的 reST directive 的格式为:
.. name:: parameters :option: value content
而且 docutils 提供了扩展新的 directive 的机制。因此你可以创建自已的新的 directive。于是我创建了一个新的 code directive,格式为:
.. code:: code_name :file: output_filename :display: on|off content
这里 code_name 表示这段代码的名字。 file 可选项表示这段代码的输出结果将保存到哪个文件中。文件可以带路径。如果不同的代码块,但有着相同的文件名的,表示将输出到一个文件中,不过这里另有要求,后面会讲到。 display 可选项表示代码是否显示在文档中,或不显示在文档中。content为代码块的内容。其中,目前支持在一个文件内的代码块的引用。在 content 中,一行只能引用一个代码块,写法为:
[缩近]<<code name>>
代码块使用<<和>>来包括,名字可以有空格。但最终都视为无空格的串,如下面几种写法表示同一个名字:
<< code name >> <<code name>> <<codename>>
代码块前可以有空白(注意是空格)。在输出时,有缩近的代码块,所有行都会进行缩近,因此特别适合对缩近有要求的语言。
首先对于要输出的代码需要使用 code directive 的格式来编写。一个代码块可以指定一个 file 的参数,它表示输出结果保存到哪个文件中。对于每个代码块需要指定一个名字,这个名字在对应一个输出文件的代码块中不能重复。一般来说,在一个输出文件所对应的代码块中,应该有一个命名为 main 的代码块,它表示根代码,通过它可以在内容中关联到其它的代码块。当在一个输出文件对应的代码块中找不到名为 main 的代码块时,第一个代码块就被默认为是这个文件的根代码块。因此如果一个文档中的不同的代码块的 file 参数不是同一个文件的话,那就表示这些代码块将输出到不同的文件中。
比如下面的代码块是可以的:
.. code:: main
:file: a.c
#include <stdio.h>
main()
{
printf("hello, world.\n");
}
.. code:: main
:file: a.py
print "hello, world."
它们的名字相同,但 file 参数不同。
由于对于一个文件的处理是顺序执行的,因此当第一个代码块设定了 file 参数后,当前处理文件即指定为这个文件名。这样当后面的代码块不设定 file 参数时,表示与前面使用相同的文件。比如:
.. code:: main
:file: a.c
#include <stdio.h>
main()
{
<<main_code>>
}
.. code:: main_code
printf("hello, world.\n");
printf("hello, reST.\n");
输出结果将为:
#include <stdio.h>
main()
{
printf("hello, world.\n");
printf("hello, reST.\n");
}
可以看到上面定义了两个代码块,同时 main 的内容包含了 <<main_code>> 。 main 代码块指定了 file 参数为 a.c ,但 main_code 没有指定。因此 main_code 将自动使用前一个代码块的文件名,因此与 main 代码块相同。所以 main 与 main_code 可以认为是属于同一组的代码块(即对应一个输出文件)。因此 <<main_code>> 将直接引用下面的 main_code 代码块。同时你还可以看到,当 main_code 代码块的内容插入到 main 中时,每一行都缩近了。因为在 main 中定义代码块的引用时,前面有空格,因此上整个插入的内容,每行都进行了缩近。
在使用转换工具前你需要安装最新版的 docutils 模块。
所有以上的操作需要一个转换工具来完成。它由两个文件组成:
doc.py 采用了rst2html.py的操作界面。没有增加新的命令行参数。因此如何使用你可以执行:
python doc.py --help
来查看详细的命令行参数。
那么在执行时,所有定义在 reST 文档中的代码将按指定的文件进行输出。在创建文件时,你将会在命令行看到执行步骤。同时,这个工具可以自动创建目录。
命令示例:
python doc.py t.txt t.html
通过 doc.py 工具的扩展可以实现部分文学编程。不过此代码功能有限,并且没有经过详细的测试。我想在以后的使用中会不停地完善。如果你有兴趣欢迎与我交流。
作为示例,把两个代码放在文档中。你可以通过:
python doc.py doc.txt doc.html
来看出输出结果。不过,目标文件名为 doc_.py 和 docnotes_.py 。
from docutils import nodes, utils
from docutils.parsers.rst import directives, states
import docnodes
display_values = ('on', 'off')
def display(argument):
return directives.choice(argument, display_values)
def code(name, arguments, options, content, lineno,
content_offset, block_text, state, state_machine):
opt = {'display':'on'}
opt.update(options)
docnodes.Node(content, ''.join(arguments), **opt)
if opt['display'].lower() == 'on':
return [nodes.literal_block('', '\n'.join(content))]
else:
return []
code.content = 1
code.arguments = (1, 0, 1)
code.options = {
'file':directives.unchanged, #used to save code to the file
'display':display, #if show code in document
}
directives.register_directive('code', code)
try:
import locale
locale.setlocale(locale.LC_ALL, '')
except:
pass
from docutils.core import publish_cmdline, default_description
description = ('Generates (X)HTML documents from standalone reStructuredText '
'sources. ' + default_description)
publish_cmdline(writer_name='html', description=description)
docnodes.render()
import re
import sys
import os.path
import traceback
DEBUG = 0
class Node(object):
node_pattern = re.compile(r'^(?P<blank>\s*)<<(?P<nodename>[^<]+)>>\s*')
def __init__(self, text, name, **opts):
"""text will be passed by doctuils, and it'll be a list of string"""
self.text = text
self.pieces = []
self.output = None
self.name = Node.compressname(name)
self.outputfile = opts.get('file', None)
self.init()
self.parent_nodelist = add_node(self)
def render(self, nodelist):
if self.output is None:
buf = []
for p in self.pieces:
if isinstance(p, (str, unicode)):
buf.append(p)
else:
buf.extend(p.render(nodelist))
self.output = buf
return self.output
def init(self):
for i in self.text:
b = Node.node_pattern.search(i)
if b:
nodename = b.groupdict()['nodename']
indent = len(b.groupdict()['blank'])
self.pieces.append(LinkNode(Node.compressname(nodename), indent))
else:
self.pieces.append(i)
def compressname(name):
return name.replace(' ', '')
compressname = staticmethod(compressname)
class LinkNode(object):
def __init__(self, name, indent=0):
self.name = name
self.indent = indent
def render(self, nodelist):
node = nodelist.get(self.name, None)
if node:
return [' '*self.indent + x for x in node.render(nodelist)]
else:
return [' '*self.indent + '<<' + self.name + '>>']
class OrderedDict(dict):
def __init__(self, d=None):
super(dict, self).__init__(d)
self._sequence = []
def __setitem__(self, key, val):
if not self.has_key(key):
self._sequence.append(key)
dict.__setitem__(self, key, val)
def __delitem__(self, key):
dict.__delitem__(self, key)
self._sequence.remove(key)
def getlist(self):
return self._sequence
class NodeList(object):
def __init__(self):
self.list = {}
self.currentfile = None
def add_node(self, node):
if node.outputfile:
self.currentfile = node.outputfile
nodelist = self.list.setdefault(self.currentfile, OrderedDict({}))
nodelist[node.name] = node
return nodelist
def render(self):
for filename, nodelist in self.list.items():
if filename:
basedir, filen = os.path.split(filename)
if basedir and not os.path.exists(basedir):
try:
print 'create dir', basedir
os.makedirs(basedir)
except:
error_output('Error: there is something wrong with create directory ' + basedir)
try:
print 'create file', filename
f = file(filename, 'w')
except:
error_output('Error: there is something wrong with create file ' + filename)
else:
f = sys.stdout
f.write(self._render(nodelist))
f.write('\n')
f.close()
def _render(self, nodelist):
main = self.find_mainnode(nodelist)
return '\n'.join(flatlist(main.render(nodelist)))
def find_mainnode(self, nodelist):
"""if there is a node named main, then it's the main node, if there is none,
so the first node is the main node"""
main = nodelist.get("main", None)
if not main:
main = nodelist.get(nodelist.getlist()[0])
return main
_nodelist = NodeList()
def add_node(node):
return _nodelist.add_node(node)
def get_root_nodelist():
return _nodelist
def render():
get_root_nodelist().render()
def error_output(msg):
print msg
if DEBUG:
traceback.print_exc()
else:
print 'You can set --debug to see the traceback'
sys.exit(1)
def flatlist(alist):
buf = []
for i in alist:
if isinstance(i, list):
buf.extend(flatlist(i))
else:
buf.append(i)
return buf
if __name__ == '__main__':
text = """Test program
<< node 1 >>
<< node 2 >>
"""
node = Node(text.splitlines(), "main")
node1text = """if __name__ == '__main__':
"""
node1 = Node(node1text.splitlines(), "node1")
node2text = """print "hello, world"
"""
node2 = Node(node2text.splitlines(), "node2")
render()