使用 Antlr 将 org 文件翻译为 html

Table of Contents


本文会涉及到以下内容:

  1. 背景
  2. Antlr 在 Python 下的使用
  3. Antlr 处理文本的过程
  4. g4 文件说明

背景

这个项目目的是练习 Antlr 在 Python 下的使用。 其中 org 文件的格式如下:

#+TITLE: hello world
#+CSS: a.css

* header1[id=myHeader]
** hello world[id=myHeader]
   this is a test
   and this is another test
    another test
*** hello kitty[id=myHeader]
    what can i do for you
    and hahhaaa

其中 TITLE 标记表示标题,其中 CSS 是自定义的,表示要引用的 css 文件地址,并非 Org mode 支持的功能。同时在一级、二级可以自定义 css 的 id,来灵活的定义格式。

下面来看如何将其转换为 html 文件。

Antlr 在 Python 下使用

Antlr 可以将 g4 文件转化为很多语言,生成 lexer, parser 文件,其中支持的目标语言包括 Java、Python、C++、Go 等。

在 Python 下使用,非常简单:

  1. 安装 Python 运行时环境

    pip install antlr4-python3-runtime
    
  2. 指定语言目标

    antlr4 -Dlanguage=Python3 MyGrammar.g4
    

Antlr 处理文本的过程

语言的处理过程分为两个独立的阶段:

  1. Lexing: 将文本转化为 tokens 符号。
  2. Parsing: 从 tokens 中构建语法树。

首先来看 Lexing 过程:

  1. 大写符号表示的表示 lexer 规则。
  2. lexer 首先找到一个匹配最好的规则来匹配当前的输入。
  3. 最好的规则是能够匹配长度最长的一个。
  4. tokens 产生过程有如下的可能:
    • 如果只有一个规则匹配输入,会将匹配的输入 push 到 token stream 中。
    • 如果有多个规则匹配输入,最好的匹配是第一个遇到的 lexer 规则。

例如:

FILEPATH: ('A'..'Z'|'a'..'z'|'0'..'9'|':'|'\\'|'/'|' '|'-'|'_'|'.')+ ;
TITLE: ('A'..'Z'|'a'..'z'|' ')+ ;

如果匹配一个 TITLE,它会被认定为 FILEPATH, 而不是 TITLE,所以当我们使用 TITLE 的时候它肯定是找不到。也就是在定义 Lexer 规则的时候,我们要尽量不要让它们有交叉,否则可能就会出现类似 mismatched input xxx' expecting xxx 这样的错误。

如何写 g4 文件

首先看下为实现该应用定义的 demo.g4 文件。

grammar demo;
org: line+;
line: '#+TITLE:' one params? NEWLINE  # title
      |'#+CSS: ' path # css
      | HEADER1 WS? one params? NEWLINE  # header1
      | HEADER2 WS? one params? NEWLINE  # header2
      | HEADER3 WS? one params? NEWLINE  # header3
      | content+     # con
      | NEWLINE                 # newline
      ;
path: (ID | '/' | '.')+ # p ;
content: (WS* ID)+ NEWLINE       # con2;

params : '[' exprlist ']' # Para;

exprlist: ID '=' ID # expr ;

one: (WS* ID)+;

NEWLINE : '\r'? '\n' ;
HEADER1: '*' ;
HEADER2: '**' ;
HEADER3: '***' ;
WS : [ \t]+ ;
ID : [0-9a-zA-Z]+;

语法定义以 grammer 开头,后面的名称和文件名对应。大写的表示 Lexer 规则, 小写的为 Parser 规则。需要注意很多的规则都是可复用的,也就是在大部分情况下,我们都可以在别的语言中找到一些通用的模式。例如这个Grammars written for ANTLR v4

20190519_093709_60100zaN.png

在该应用中,我们用到了 visitor 模式,在这种模式下,需要对定义的 g4 文件的 parser 规则加上标签。方式也比较简单,只需要在行的后面加上 # <label>就可以了。需要注意:

  1. 行结尾的;号要在标签的后面。
  2. 标签的大小写不敏感。

如何执行

首先,使用 vistor 模式来生成词法和语法文件:

antlr4 -visitor -no-listener c.g

同时定义处理文件,文件内容如下:

from demoVisitor import demoVisitor
HEAD_CSS = '''
<link rel=\"stylesheet\" type=\"text/css\" href=\"{}\">
'''
class Org2Html(demoVisitor):
    def __init__(self):
        self.html = '<html>'

    def visitCss(self, ctx):
        path = ctx.path().getText()
        self.html = self.html + "<head>" + HEAD_CSS.format(path)

    def visitTitle(self, ctx):
        title = ctx.one().getText()
        if ctx.params():
            a, b = self.visit(ctx.params())
        else:
            a = 'id'
            b = 'xxx'
        title = "<title {}=\"{}\">{}</title></head>".format(a, b, title)
        self.html = self.html + title + '\n'
        print(self.html)
    def visitPara(self, ctx):
            # need return by all level
            return self.visit(ctx.exprlist())
    def visitExpr(self, ctx):
            a = ctx.ID(0).getText()
            b = ctx.ID(1).getText()
            print(a, b)
            return a, b

在该文件中,如果想要访问某个 parser 规则,只需要 visit 后跟上对应的标签即可。对于嵌套的例如 Para Expr,如果想在在最上层获得结果,每个都返回,如 VisitPara 函数。

同时获取某个节点的 value 使用 getText 即可。对于其他的使用可以阅读 The Definitive ANTLR 4 Reference 2

最后可以执行的项目地址在:org2html

参考

  1. Python target
  2. The Definitive ANTLR 4 Reference, book