< 返回版块

2019-04-30 23:24    责任编辑:tk

标签:rust, wordpress, gutenberg,

从Rust到远方:前奏

来源:https://mnt.io/2018/08/21/from-rust-to-beyond-prelude/

我的工作中有个机会来开始了一个试验:用Rust为新的Gutenberg文章格式写一个独立的解析器,并且要绑定到不同的平台和环境。

gutenberg_logo Gutenberg博客解析器项目的图标。

这个系列就是关于这些绑定,并且要解释如何让Rust飞跃地球去到其它的星系。Rust将要登陆到

目前这艘飞船正在飞向Java星系,除非飞船坠毁了或者没有足够的资源存活下去了,否则这个系列会一直持续下去。

Gutenberg文档格式

我们快速的介绍一下什么是Gutenberg,以及为什么要用这种新的格式。如果你希望看到更深入的介绍,我强烈推荐读一下Gutenberg语言。注意其实对于理解Gutenberg文档格式这个阅读不是必须的。

Gutenberg是下一代Wordpress编辑器。它是基于上一代的一个小革命。其解锁的功能非常的强大。

这个编辑器将会带来创建页面和文章的全新体验,他使得编写丰富格式的文章非常简单,它支持“块”,这将使以前只能通过快捷码,定制HTML或者叫做“mystery meat”的embed码才能实现的功能变得简单。-Matt Mullenweg

博客文章的格式其实就是HTML,而且以后也会是。但是通过附加的标记增加了一个语意层。标记是用注释来实现的,而且借鉴了XML的语法,像下面一样:


<!-- wp:ns/block-name {"attributes": "as JSON"} -->
    <p>phrase</p>
<!-- /wp:ns/block-name -->

Gutenberg格式提供了两种基本结构:块和短语。上面这个例子里面两种都有使用:有一个块封装了短语。短语其实就是除了块之外的任何东西。我们来解读一下这个例子:

以这个标记开头( <!-- … -->), wp: 用来表示Gutenberg块的必需标记, 紧接着的是合规的块名字, 一对可选的名字空间 (这里用 ns, 默认是 core) 以及块名字(这里用 block-name), 用斜线分开, 一个块可以有可选属性用JSON 对象来表示 (参见 RFC 7159, Section 4, Objects), 最后,一个块可以有子块, 也就是一个由块和短语异构的集合. 在上面的例子中, 有一个子块 <p>phrase</p>. 后面我们将要展示的块都不包含子块:

<!-- wp:ns/block-name {"attributes": "as JSON"} /-->

完整的语法可以参考这个解析器文档

最后,这个解析器是用在编辑器端而不是渲染端。一但渲染完成博客文章就变成了普通的HTML文件。当然有些块是动态的,不过那是另外一个话题。

block-logic-flow1 编辑器的流程图 (块是如何工作的).

这个语法其实规模比较小。然而最具挑战的是要做到在多平台尽可能的做到高性能,高效使用内存。有些文章可以大到很多兆。我们可不想让这个解析器成为瓶颈。甚至如果它只用来创建文章状态(比较上面的结构),我们做过的测量中有些文章的加载花了好几秒钟。这段时间用户只能被阻塞等待。还有一些场景中,我们碰到语言虚拟机的内存极限。

因此这是一个试验项目。目前的解析器是用JavaScript(用PEG.js)和PHP(用phpegjs)实现的。这个项目的目的就是用Rust来写这个解析器,能够运行在JavaScript和PHP虚拟机中,当然还有其它的平台。我们尽力做到高性能和高效使用内存。

为什么选Rust?

这是一个很好的问题! 谢谢你这样问。我可以用下面这个列表来总结一下:

  • 它很快, 我们恰好也需要速度,

  • 它内存安全并且高效,

  • 没有垃圾回收器, 这让跨环境的内存管理变得简单,

  • 可以暴露 C API (通过外部函数接口, FFI), 简化了到多个环境的集成,

  • 可以编译到很多平台,

  • 因为我喜欢它.

这个试验的一个目标是要让多个绑定只维护一份实现(或许是未来的一个参考实现)。

解析器

这个解析器是用Rust写的。依赖非常棒的nom库。

nom nom 会非常开心的从你的文件里面读出一个字节 🙂.

源代码可以在仓库的src/目录找到。代码量很小而且读起来比较有意思。

这个解析器生成一个语法的抽象语法树(AST),这个树的节点有如下定义:


pub enum Node<'a> {
    Block {
        name: (Input<'a>, Input<'a>),
        attributes: Option<Input<'a>>,
        children: Vec<Node<'a>>
    },
    Phrase(Input<'a>)
}

就是这样。我们再一次看到了块名字,属性,子节点以及短语。块的子节点是由一些节点组成的集合,是递归的。Input<'a>的类型是&'a[u8],即一个字节切片。

解析器的主要入口是root函数,它是代表的语法的根源,定义如下:


pub fn root(
    input: Input
) -> Result<(Input, Vec<ast::Node>), nom::Err<Input>>;

可以看到,解析器成功的时候会返回的是一个节点的集合,下面是一个简单的例子:


use gutenberg_post_parser::{root, ast::Node};

let input = &b"<!-- wp:foo {\"bar\": true} /-->"[..];
let output = Ok(
    (
        // The remaining data.
        &b""[..],

        // The Abstract Syntax Tree.
        vec![
            Node::Block {
                name: (&b"core"[..], &b"foo"[..]),
                attributes: Some(&b"{\"bar\": true}"[..]),
                children: vec![]
            }
        ]
    )
);

assert_eq!(root(input), output);

root函数和AST将会是我们在绑定中要去使用的主要部件。解析器的其它内部实现将会保持私有。

绑定

Rust to

从现在开始,我们的目标是在不同的平台或者环境暴露root函数和Node枚举,准备好了么?

3。。。2 。。。1 。。。发射!