#pretty-print #printing #dsl #pretty

nightly typeset

用于定义源代码美化的领域特定语言

6个稳定版本

2.0.4 2024年4月14日
2.0.3 2023年10月23日
2.0.2 2023年7月5日
2.0.1 2023年6月22日
0.1.0 2023年3月17日

#71编程语言

Download history 45/week @ 2024-05-05 1/week @ 2024-05-12 1/week @ 2024-05-26 3/week @ 2024-06-02 15/week @ 2024-06-23 1/week @ 2024-06-30 9/week @ 2024-07-07 17/week @ 2024-07-14 1/week @ 2024-07-21 47/week @ 2024-07-28

66 每月下载量
typeset-parser 中使用

自定义许可

150KB
4.5K SLoC

typeset-rs

定义源代码美化的嵌入式领域特定语言。

概念

布局语言设计得非常好,它可以很好地应用于某些归纳数据结构的结构性递归遍历;这是你要美化的东西的抽象表示。

布局是一棵由文本字面量组成的树,这些字面量通过填充、非填充组合或换行符组合在一起。布局求解器将在布局中选择组合并将它们转换为换行符,以便使布局适合给定的布局缓冲区宽度。它将贪婪地进行,尽可能多地在一行中放置字面量。在这样做的时候,它将尊重组合构建时注解的属性。

求解器是一个抽象概念,通过两个伴随函数具体实现,一个是作为 compile 实现的 编译器,另一个是作为 render 实现的 渲染器

空构造函数

有时在数据结构中可能有可选数据(例如,类型为“字符串选项”),当省略时不应有布局。为了使这种情况易于处理,添加了布局组合的 null 元素。另一种选择是你需要在布局函数中跟踪迄今为止构建的布局的累积变量。

fn layout_option(maybe_string: Option<String>) -> Layout {
  match maybe_string {
    None => null(),
    Some(data) => text(data)
  }
}

与,例如,使用累积器

fn layout_option(maybe_string: Option<String>, result: Layout) -> Layout {
  match maybe_string {
    None => result,
    Some(data) => comp(result, text(data), true, false)
  }
}

编译器将消除布局中的 null,并且不会渲染,例如

let foobar = comp(text("foo"), comp(null(), text("bar"), false, false), false, false);

当渲染 foobar 时,当布局适合布局缓冲区时,结果将是

       7
       |
foobar |
       |

单词字面量构造函数

这些都是我们要排版的外部终端。

let foo = text("foo");

当渲染 foo 时,当布局适合布局缓冲区时,结果将是

    4
    |
foo |
    |

当布局不适合布局缓冲区时,它将简单地溢出缓冲区

  2
  |
fo|o
  |

修复构造函数

有时你需要将某些布局的一部分作为内联渲染,即其组合不应被分割;这就是 fix 构造函数的用途。换句话说,固定布局被当作字面量处理。

let foobar = fix(comp(text("foo"), text("bar"), false, false));

在渲染固定布局 foobar 时,当布局适合布局缓冲区时,结果将是

       7
       |
foobar |
       |

如果不适合,将会溢出缓冲区

  2
  |
fo|obar
  |

Grp 构造函数

grp 构造函数防止求解器破坏其组合,只要组左边还有可以破坏的组合。这在需要将布局的一部分视为一个项目时非常有用。

let foobarbaz = comp(text("foo"), grp(comp(text("bar"), text("baz"), false, false)), false, false);

在渲染 foobarbaz 时,当布局适合布局缓冲区时,结果将是

          10
          |
foobarbaz |
          |

如果其中一个文本不符合布局缓冲区,结果将是

       7
       |
foo    |
barbaz |
       |

相反,如果没有对组进行注释,结果将是

       7
       |
foobar |
baz    |
       |

由于 barbaz 之间的组合没有被保护,并且布局求解器是贪婪的,它想要尽可能多地在一行上放置文本,而不溢出缓冲区。如果组仍然不适合布局缓冲区,则组将被破坏,结果将是

    4
    |
foo |
bar |
baz |
    |

Seq 构造函数

seq 构造函数强制求解器在其中一个组合被破坏时立即破坏所有组合。这在您有数据是序列或类似列表性质时非常有用;当一个项目被放置在新的一行时,序列中的其余项目也应如此。

let foobarbaz = seq(comp(text("foo"), comp(text("bar"), text("baz"), false, false), false, false));

在渲染 foobarbaz 时,当布局适合布局缓冲区时,结果将是

          10
          |
foobarbaz |
          |

如果其中一个文本不符合布局缓冲区,结果将是

       7
       |
foo    |
bar    |
baz    |
       |

由于组合是序列的一部分;即当一个组合被破坏时,它们都会被破坏。

Nest 构造函数

nest 构造函数仅仅提供了一个额外的缩进级别,用于其范围内的所有文本。每个缩进级别的宽度是作为 render 函数的参数给出的。

let foobarbaz = comp(text("foo"), nest(comp(text("bar"), text("baz"), false, false)), false, false);

当以缩进宽度为 2 的方式渲染 foobarbaz 时,当布局适合布局缓冲区时,结果将是

          10
          |
foobarbaz |
          |

如果其中一个文本不符合布局缓冲区,结果将是;

       7
       |
foobar |
  baz  |
       |

当布局缓冲区只能容纳一个文本时,结果将是

    4
    |
foo |
  ba|r
  ba|z
    |

在这种情况下,由于给定的缩进,barbaz 将会溢出布局缓冲区。

Pack 构造函数

pack 构造函数定义了一个缩进级别,但隐式地将缩进宽度设置为它注释的布局中第一个文本的索引。例如,如果您在类似 Lisp 的语言中格式化术语,所有其他函数参数通常都会“缩进”到第一个参数相同的缓冲区索引,这很有用。

let foobarbaz = comp(text("foo"), pack(comp(text("bar"), text("baz"), false, false)), false, false);

在渲染 foobarbaz 时,当布局适合布局缓冲区时,结果将是

          10
          |
foobarbaz |
          |

如果其中一个文本不符合,结果将是

       7
       |
foobar |
   baz |
       |

当布局缓冲区只能容纳一个文本时,结果将是

    4
    |
foo |
bar |
baz |
    |

计算要缩进的缓冲区索引的公式是

max((indent_level * indent_width), mark)

即标记索引只会在它大于当前缩进时被选择。

强制换行组合

强制换行组合正是这样,它是一个预先破坏的组合。

let foobar = line(text("foo"), text("bar"));

在渲染 foobar 时,无论布局是否适合布局缓冲区,结果都将是这样

        8
        |
foo     |
bar     |
        |

无填充组合

无填充组合将两个布局组合在一起,没有任何空白。

let foobar = comp(text("foo"), text("bar"), false, false);

当渲染 foobar 时,当布局适合布局缓冲区时,结果将是

        8
        |
foobar  |
        |

填充组合

填充组合将两个布局组合在一起,并带有空白。

let foobar = comp(text("foo"), text("bar"), true, false);

当渲染 foobar 时,当布局适合布局缓冲区时,结果将是

        8
        |
foo bar |
        |

中缀固定组合

中缀固定组合是针对左操作数的左最左文本和右操作数的右最右文本固定在一起的组合的语法糖。即以下两个布局是等价的

let foobarbaz1 = comp(text("foo"), comp(text("bar"), text("baz"), false, true), false, false);
let foobarbaz2 = comp(text("foo"), fix(comp(text("bar"), text("baz"), false, false)), false, false);

上面的例子可能让人觉得很简单,认为中缀固定组合并没有提供太多价值;但请记住,您正在组合布局,而不仅仅是文本来。因此,对中缀固定组合进行标准化实际上是非常具有挑战性的,因为在布局树中,当“固定”在布局树中“下沉”时,需要考虑许多不同的情况;这是编译器负责的一部分。

中缀固定组合在您需要将文本固定到某个布局的开始或结束位置时很有用,例如在序列或类似列表的数据结构中的项目之间添加分隔符。如果没有这个功能,如果您想要将文本固定到下一个文本,您将需要再次使用累加变量;如果您想要将文本固定到最后一个文本,可能还需要使用续集。

布局编译

您的自定义布局函数(格式化程序)将构建一个布局,然后您需要对其进行编译和渲染

...
let document = compile(layout);
let result = render(document, 2, 80);
println!(result);
...

也就是说,应该将布局提供给编译器,编译器会返回一个可用于渲染的文档,然后您将这个文档以及缩进宽度和布局缓冲区宽度作为参数传递给渲染器;在上面的例子中,缩进宽度是2,布局缓冲区宽度是80。

将求解器拆分为compilerender的原因是,如果结果要在宽度可变的缓冲区中显示;即您不需要在渲染过程中使用可变缓冲区宽度重新编译布局。

领域特定语言和解析

此外,还定义了一个小型领域特定语言,并实现了一个过程宏解析器,这使得您可以更简洁地编写布局(与使用给定的构造函数完整拼写布局树相比,我们在本介绍中一直这样做)

...
use typeset_parser::layout;

let my_layout = layout! {
  nest ("foo" !& "bar") @
  pack (seq ("baz" + fragment1)) @@
  fix (fragment2 + fragment3)
};
...

完整的语法如下

x         (Identifier variables for layout fragments)
null      (Constructor for the empty layout)
"x"       (Constructor for a word/text layout literal over a string x)
fix u     (Constructor for a fixed layout over a layout u)
grp u     (Constructor for a group layout over a layout u)
seq u     (Constructor for a sequence layout over a layout u)
nest u    (Constructor for a indented/nested layout over a layout u)
pack u    (Constructor for a indexed margin layout over a layout u)
u @ v     (Forced linebreak composition of layouts u and v)
u @@ v    (Forced double linebreak composition of layouts u and v)
u & v     (Unpadded composition of layouts u and v)
u !& v    (Infix fixed unpadded composition of layouts u and v)
u + v     (Padded composition of layouts u and v)
u !+ v    (Infix fixed padded composition of layouts u and v)

示例

有关如何将所有这些布局构造函数组合起来以创建更复杂、更有用的内容的示例,请参阅示例目录。

依赖关系

~240KB