「Rust + Nom → Parser」分析 roamresearch graph

494 阅读2分钟

「这是我参与11月更文挑战的第 17 天,活动详情查看:2021最后一次更文挑战


如果你以前没有做过 parser,那么编写分析器可能会让人感到害怕。它使人联想到晦涩难懂的语法被输入到神秘的工具中,从而产生数千行不可读的代码。

但它其实不一定是这样的。像 nom 这样的解析器库提供了一个小函数库,每个函数聚焦一件事,然后允许你把它们放在一起,就可以创建一个完整的解析器。

我最近建立了一个有意思的实用程序,将 roamresearch graph 导出为 HTML,这项任务的一部分涉及到解析Roam markdown类似格式。编写解析器的经验还不错,所以我在这里对解析器文件做了注释,以便你能看到它是如何组合在一起的。

首先我们只是导入所有的解析器和组合器。

use nom::{
    branch::alt,
    bytes::complete::{is_not, tag, take_until, take_while1},
    character::complete::{char, multispace0},
    character::{is_newline, is_space},
    combinator::{all_consuming, map, map_parser, opt, recognize},
    error::context,
    sequence::{delimited, pair, preceded, separated_pair, terminated, tuple},
    IResult,
};
use urlocator::{UrlLocation, UrlLocator};

分析器的输入是单个块的标记,输出是 Vec<Expression>。有些表达式,如Bold,包含其他表达式。至于其他表达式,例如 Image,就直接渲染出 HTML。

大多数这些表达式实际渲染成HTML的过程并不是那么有趣,但如果有人关心的话,下次再说吧。

#[derive(Debug, PartialEq, Eq)]
pub enum Expression<'a> {
    Text(&'a str),
    RawHyperlink(&'a str),
    Image {
        alt: &'a str,
        url: &'a str,
    },
    BraceDirective(&'a str),
    Table,
    PageEmbed(&'a str),
    BlockEmbed(&'a str),
    TripleBacktick(&'a str),
    SingleBacktick(&'a str),
    Hashtag(&'a str, bool),
    Link(&'a str),
    MarkdownLink {
        title: &'a str,
        url: &'a str,
    },
    BlockRef(&'a str),
    Attribute {
        name: &'a str,
        value: Vec<Expression<'a>>,
    },
    Bold(Vec<Expression<'a>>),
    Italic(Vec<Expression<'a>>),
    Strike(Vec<Expression<'a>>),
    Highlight(Vec<Expression<'a>>),
    BlockQuote(Vec<Expression<'a>>),
    HRule,
}

首先,先实现一个实用函数来匹配任何非空白字符:

fn nonws_char(c: char) -> bool {
    !is_space(c as u8) && !is_newline(c as u8)
}

我们的第一个解析器:word,只是得到一个单词。

take_while1(nonws_char) 创建一个分析器,读取至少一个与 nonws_char 匹配的字符。

每个解析器都遵循这个格式。它把输入作为一个参数,并返回一个 IResult<REM, DATA>,其中REM是字符串的剩余未解析部分,DATA是解析器返回的类型。内置的解析器也可以使用这种类型的返回值,所以我们在这里不需要做任何特别的事情。

对于这个解析器来说,输入都是字符串,但nom也是可以处理二进制格式,所以 &[u8] 在其他情况下也常见到。

fn word(input: &str) -> IResult<&str, &str> {
    take_while1(nonws_char)(input)
}