Attachment 'liteprog.html'
Download使用reStructuredText来进行文学编程
作者: | limodou |
---|---|
联系: | limodou AT gmail.com |
版本: | 0.1 |
主页: | http://wiki.woodpecker.org.cn/moin/NewEdit |
BLOG: | http://www.donews.net/limodou |
版权: | FDL |
1 前言
我没有看错吧!你可能要问。的确没有。这里是我在了解了“ 文学编程 ”的一些基本知识后,萌生的一个想法。学过 Python 的人可能知道, docutils 是一个 Python 下的文档处理模块,它使用一种叫 reStructuredText(以下简写为reST)的文本书写格式来编写文档,这种格式是从 StructuredText 发展起来的,通过一些约定的文本格式,就可以通过转换工具将文档由文本转换为 Html, Latex, DocBook等许多种格式。而且 docutils 虽然是使用 Python 编写的,但 reST 并不是只限于 Python 的应用,你可以用它来书写任意的内容。而且许多的 Python 项目都采用它来编写文档。
什么叫文学编程?简单地讲就是采用文学创作的方式来写程序,因此更看重文档的作用,它是一种文档与代码的混合。它是由Donald Knuth 在1984年提出的。具体的可以参见 literateprogramming 网站。
因此我想如果可以对 reST 进行扩展,增加文学编程的功能,岂不是件好事。
2 格式要求
在分析了 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>>
代码块前可以有空白(注意是空格)。在输出时,有缩近的代码块,所有行都会进行缩近,因此特别适合对缩近有要求的语言。
3 输出代码的要求
首先对于要输出的代码需要使用 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 中定义代码块的引用时,前面有空格,因此上整个插入的内容,每行都进行了缩近。
4 转换工具
在使用转换工具前你需要安装最新版的 docutils 模块。
所有以上的操作需要一个转换工具来完成。它由两个文件组成:
- doc.py 主程序
- docnotes.py 结点处理程序
doc.py 采用了rst2html.py的操作界面。没有增加新的命令行参数。因此如何使用你可以执行:
python doc.py --help
来查看详细的命令行参数。
那么在执行时,所有定义在 reST 文档中的代码将按指定的文件进行输出。在创建文件时,你将会在命令行看到执行步骤。同时,这个工具可以自动创建目录。
命令示例:
python doc.py t.txt t.html
5 小结
通过 doc.py 工具的扩展可以实现部分文学编程。不过此代码功能有限,并且没有经过详细的测试。我想在以后的使用中会不停地完善。如果你有兴趣欢迎与我交流。
作为示例,把两个代码放在文档中。你可以通过:
python doc.py doc.txt doc.html
来看出输出结果。不过,目标文件名为 doc_.py 和 docnotes_.py 。
6 附录A: doc.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()
7 附录B: docnotes.py代码
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()
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.