#table #text-formatting #markdown-tables #terminal #format #output-format #markdown-html

no-std stanza

一个具有可自定义文本格式化和渲染器的抽象表格模型

6 个版本 (破坏性更新)

0.5.1 2023年12月20日
0.5.0 2023年12月20日
0.4.0 2023年12月19日
0.3.0 2022年10月31日
0.1.0 2022年10月2日

#100命令行界面

Download history 2/week @ 2024-04-14 6/week @ 2024-04-21 1/week @ 2024-04-28 7/week @ 2024-05-12 18/week @ 2024-05-19 10/week @ 2024-05-26 16/week @ 2024-06-02 12/week @ 2024-06-09 9/week @ 2024-06-16 3/week @ 2024-06-23 33/week @ 2024-06-30 31/week @ 2024-07-07 5/week @ 2024-07-14 44/week @ 2024-07-21 45/week @ 2024-07-28

每月下载量 125
6 crate 使用

MIT 许可证

105KB
1.5K SLoC

Stanza

一个用 Rust 编写的抽象表格模型,具有可自定义的文本格式化和渲染器。

Crates.io docs.rs Build Status codecov no_std

Screenshot

为什么选择 Stanza

  • 功能全面:Stanza 支持广泛的样式功能——各种文本格式控制、前景/背景/填充颜色、边框样式、多个水平和垂直表头和分隔符,甚至嵌套表格等。
  • 可插拔的渲染器:将表格模型与渲染实现分离,让您可以在输出格式之间切换。例如,您可能输出到终端的表格可以切换为生成 Markdown 文档。您还可以添加自己的渲染器;例如,输出 HTML 或绘制 TUI/Curses 屏幕。
  • 易于使用:简单的事情容易做,困难的事情也能做到。Stanza 提供了构建“静态”表格的流畅 API 和用于程序化构建表格的 API。
  • 无需标准库:Stanza 是 no_std,这意味着它可以在嵌入式设备中使用。
  • 性能:构建上图所示的表格模型需要 ~10 µs,渲染它需要 ~200 µs。(Markdown 需要 roughly half that time。)效率可能在桌面和服务器用例中不是问题,但在低功耗设备上却有很大影响。

入门指南

添加依赖项

cargo add stanza

基本表格

Stanza 故意将 Table 模型与一组可选的 Style 修饰符以及最终的 Renderer(用于将表格转换为可打印的 Display 对象)分开。

模型中有四种主要类型的元素: TableColRowCell

Table 同时包含 ColRow 对象。Stanza 中的表格是按行排列的;即,Col 仅用于分配样式;Row 是数据所在的位置。

一个 Row 包含一组 Cell 向量。反过来,一个 Cell 包含一个 Content 枚举。您将最频繁使用最简单的内容类型,即一个 Content::Label

让我们从一个最基础的2x3表格开始。

use stanza::renderer::console::Console;
use stanza::renderer::Renderer;
use stanza::table::Table;

// build a table model
let table = Table::default()
    .with_row(["Department", "Budget"])
    .with_row(["Sales", "90000"])
    .with_row(["Engineering", "270000"]);

// configure a renderer that will later turn the model into a string
let renderer = Console::default();

// render the table, outputting to stdout
println!("{}", renderer.render(&table));

在这里,表格的打印明显分为两个不同的步骤。

  1. 构建表格模型。
  2. 在模型上调用渲染器以生成输出。

生成的输出

╔═══════════╤══════╗
║Department │Budget║
╟───────────┼──────╢
║Sales      │90000 ║
╟───────────┼──────╢
║Engineering│270000║
╚═══════════╧══════╝

对于几十行来说,这已经相当不错了。我们没有明确指定上述所有概念,例如,我们没有提到 ColCellContent 类型。Stanza 提供了一种高度简化的语法来构建表格,其中可能不需要额外的灵活性。然而,了解底层发生的事情仍然很有价值。可以使用下面的 完全显式语法 生成相同的表格模型。

use stanza::style::Styles;
use stanza::table::{Cell, Col, Content, Row, Table};

Table::with_styles(Styles::default())
    .with_cols(vec![
        Col::new(Styles::default()),
        Col::new(Styles::default()),
    ])
    .with_row(Row::new(
        Styles::default(),
        vec![
            Cell::new(
                Styles::default(),
                Content::Label(String::from("Department")),
            ),
            Cell::new(Styles::default(), Content::Label(String::from("Budget"))),
        ],
    ))
    .with_row(Row::new(
        Styles::default(),
        vec![
            Cell::new(Styles::default(), Content::Label(String::from("Sales"))),
            Cell::new(Styles::default(), Content::Label(String::from("90000"))),
        ],
    ))
    .with_row(Row::new(
        Styles::default(),
        vec![
            Cell::new(
                Styles::default(),
                Content::Label(String::from("Engineering")),
            ),
            Cell::new(Styles::default(), Content::Label(String::from("270000"))),
        ],
    ));

我们已从4行增加到30多行,但这是使用完全显式语法指定此模型所必需的。您会很高兴地知道,Stanza 的语法不是二元的 非此即彼:它允许您根据需要逐步使用更明确的构造来细化样式或布局。注意,我们第一次迭代时不需要指定列——表格模型将根据单元格数量自动生成列定义。

然而,我们的表格缺少了一些优雅之处。理想情况下,我们希望顶部行充当标题。上下文也有些拥挤。最后,预算数字应该理想地右对齐。让我们构建一个更漂亮的表格。

use stanza::renderer::console::Console;
use stanza::renderer::Renderer;
use stanza::style::{HAlign, Header, MinWidth, Styles};
use stanza::table::{Cell, Col, Row, Table};

let table = Table::default()
    .with_cols(vec![
        Col::new(Styles::default().with(MinWidth(20))),
        Col::new(Styles::default().with(MinWidth(15)).with(HAlign::Right))
    ])
    .with_row(Row::new(
        Styles::default().with(Header(true)),
        vec!["Department".into(), "Budget".into()],
    ))
    .with_row(["Sales", "90000"])
    .with_row(["Engineering", "270000"]);

let renderer = Console::default();
println!("{}", renderer.render(&table));
╔════════════════════╤═══════════════╗
║Department          │         Budget║
╠════════════════════╪═══════════════╣
║Sales               │          90000║
╟────────────────────┼───────────────╢
║Engineering         │         270000║
╚════════════════════╧═══════════════╝

之前曾承诺您可以将相同的表格模型输出到各种格式。要切换到 Markdown,只需替换渲染器

use stanza::renderer::markdown::Markdown;
use stanza::renderer::Renderer;

// build the model ...

let renderer = Markdown::default();

// render ...
|Department          |         Budget|
|:-------------------|--------------:|
|Sales               |          90000|
|Engineering         |         270000|

看,我们有 Markdown 了!

样式

这是很好地过渡到样式的部分。Stanza 建立在将模型与渲染器分离的哲学之上。虽然这提供了非凡的灵活性,但也创造了一个问题。每个渲染器都是不同的,这限制了样式的可移植性。例如,Console 渲染器支持比 Markdown 更丰富的输出。

在构建表格模型时,我们几乎总是有一个特定的输出格式在心中。称之为“首选”格式。为首选格式装饰表格是正常的。我们还想在某种替代格式中渲染表格,而不会破坏输出,或者更糟糕的是,因为替代渲染可能不支持某些样式修饰符而导致 panic

样式始终是可选的;如果渲染器可以有意义地使用样式,则取决于渲染器。渲染器会忽略它们不理解的风格,并且可能在保留内容可读性的同时降低不受完全支持的风格。例如,MarkdownBlink 样式视而不见,但它仍然会渲染文本——尽管没有闪烁效果。您甚至可能在将来创建自己的渲染器以及一些只有该渲染器才能理解的风格。这丝毫不会影响现有的渲染器。

为了组织样式,Stanza 提出了两个基本规则:特定性可分配性

特定性

样式像它们的 CSS 对应物一样级联。在某个较高级别元素上分配的样式将自动应用于其下所有层级的元素。以下显示了 特定性 层次结构。

Table
  └─> Col
       └─> Row
            └─> Cell

一般来说,如果一个元素与另一个元素相交的元素更多,则认为前者的优先级高于后者。例如,表格与所有其他元素相交,因此它在特定性排序中处于顶部。相反,单元格只与其自身、一行、一列和一个表格相交,因此它坚定地处于底部。

行和列并不总是那么简单明了,因为表格可能非常高或非常宽。然而,高表格更为常见。实际上,人们通常是通过添加行而不是列来扩展表格的。列会与更多元素相交;因此,它在特定性层次结构中更高。

特定性层次结构用于解决冲突的样式。比如说,我们给表格分配了 TextFg::Magenta 样式,给单元格分配了 TextFg::Cyan 样式。单元格应该渲染哪种样式?当然是 Cyan。这是我们期望的正常电子表格的行为。尽管单元格继承了表格和列的样式,但它的样式会覆盖其父元素中的任何样式。

在表格级别定义的样式将应用于表格及其包含的所有内容,并且可能被任何较低级别的样式覆盖。在列级别定义的样式将应用于列及其交叉的所有单元格,并且可能被单元格样式覆盖。同样,在行级别定义的样式将应用于行及其所有单元格,它也可能被单元格覆盖。但当单元格从列和行继承冲突的样式而没有自己的覆盖样式时,会发生什么?行样式具有优先级,因为它的特定性更高。

可分配性

尽管样式以自上而下的方式级联,但这并不意味着样式可以分配给任何元素。以 Header 样式为例。它可以分配给行或列。(诗句支持垂直标题。)Header 是否可以分配给整个表格?是的,在这种情况下,所有行和列都会被视为标题。但是标题单元格没有意义。是否可以将样式分配给特定元素,可以通过调用某些 S: StyleS::assignability() 静态特质方法来确定,返回一个 Assignability 枚举的变体。可分配性层次结构如下所示。

Cell
 ├───> Col
 │       └─┐
 └─> Row   │
      └────┴─> Table

可以分配给单元格的样式也可以分配给行、列和表格。值得注意的例子包括文本格式化样式——BoldItalicUnderlineBlinkStrikethroughHAlign,以及一些着色样式——TextFgTextBgTextInvertFillBgFillInvert

在单元格上方,层次结构在列和行元素处是分离的。任何可以分配给行的样式也可以分配给表格。同样,任何可以分配给列的样式也可以分配给表格。这些包括 HeaderSeparator

可以分配给行的样式不一定可以分配给列,反之亦然。例如,MinWidthMaxWidth 只能分配给列——在行级别上它们没有意义。(当然,在单元格级别上也没有意义。)

最后,一些样式只能分配给表格。这些包括 BorderFgBorderBg——用于更改表格中所有边框的颜色。

可分配性规则在运行时强制执行。尝试分配不可分配的样式会失败,并引发 panic。因此,将样式的返回 Assignability 值更改为更限制性的变体将构成重大更改。

考虑上述所有内容,让我们再创建一个表格来演示覆盖样式。

文本处理

我们不需要做太多工作就能使文本布局良好。通常,只需要一个MinWidth列样式,可能还需要一个HAlign。有时我们可能有大量的文本,这会推大我们的列大小。

use stanza::renderer::console::Console;
use stanza::renderer::Renderer;
use stanza::style::{Header, MinWidth, Styles};
use stanza::table::{Col, Row, Table};

let table = Table::default()
    .with_cols(vec![
        Col::new(Styles::default().with(MinWidth(15))),
        Col::new(Styles::default().with(MinWidth(30))),
    ])
    .with_row(Row::new(
        Styles::default().with(Header(true)),
        vec!["Poem".into(), "Extract".into()]
    ))
    .with_row(Row::from([
        "Antigonish",
        "Yesterday, upon the stair, I met a man who wasn't there! He wasn't there again today, Oh how I wish he'd go away!"
    ]))
    .with_row(Row::from([
        "The Raven",
        "Ah, distinctly I remember it was in the bleak December; And each separate dying ember wrought its ghost upon the floor."
    ]));

println!("{}", Console::default().render(&table));
╔═══════════════╤═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗
║Poem           │Extract                                                                                                                ║
╠═══════════════╪═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣
║Antigonish     │Yesterday, upon the stair, I met a man who wasn't there! He wasn't there again today, Oh how I wish he'd go away!      ║
╟───────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢
║The Raven      │Ah, distinctly I remember it was in the bleak December; And each separate dying ember wrought its ghost upon the floor.║
╚═══════════════╧═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝

正如我们所预期的,“提取”列太宽了。处理这个问题的一个简单方法是为列设置MaxWidth样式。只需将.with(MaxWidth(40))添加到现有样式即可。

╔═══════════════╤════════════════════════════════════════╗
║Poem           │Extract                                 ║
╠═══════════════╪════════════════════════════════════════╣
║Antigonish     │Yesterday, upon the stair, I met a man  ║
║               │who wasn't there! He wasn't there again ║
║               │today, Oh how I wish he'd go away!      ║
╟───────────────┼────────────────────────────────────────╢
║The Raven      │Ah, distinctly I remember it was in the ║
║               │bleak December; And each separate dying ║
║               │ember wrought its ghost upon the floor. ║
╚═══════════════╧════════════════════════════════════════╝

这样就好多了!但因为我们正在处理一首诗,所以我们可能应该尊重作者对行断的选择。Stanza支持换行符,导致行断正好处在需要的地方。最好的是,你可以将换行符与MaxWidth结合使用,得到如下结果:

╔═══════════════╤════════════════════════════════════════╗
║Poem           │Extract                                 ║
╠═══════════════╪════════════════════════════════════════╣
║Antigonish     │Yesterday, upon the stair,              ║
║               │I met a man who wasn't there!           ║
║               │He wasn't there again today,            ║
║               │Oh how I wish he'd go away!             ║
╟───────────────┼────────────────────────────────────────╢
║The Raven      │Ah, distinctly I remember it was in the ║
║               │bleak December;                         ║
║               │And each separate dying ember wrought   ║
║               │its ghost upon the floor.               ║
╚═══════════════╧════════════════════════════════════════╝

标题

支持多行和多列标题是Stanza的独特功能。让我们画一个乘法表来展示。这也可以作为程序化构建表的例子。

use stanza::renderer::console::Console;
use stanza::renderer::Renderer;
use stanza::style::{HAlign, Header, MinWidth, Styles};
use stanza::table::{Col, Row, Table};

const NUMS: i8 = 6; // the table will multiply from 1 to NUMS

// builds just the header row -- there should be two (top and bottom)
fn build_header_row() -> Row {
    let mut cells = vec!["".into()];
    for col in 1..=NUMS {
        cells.push(col.into());
    }
    cells.push("".into());
    Row::new(Styles::default().with(Header(true)), cells)
}

// builds all body rows (each row also contains a pair column header cells)
fn build_body_rows() -> Vec<Row> {
    (1..=NUMS)
        .map(|row| {
            let mut cells = vec![row.into()];
            for col in 1..=NUMS {
                cells.push((row * col).into());
            }
            cells.push(row.into());
            Row::new(Styles::default(), cells)
        })
        .collect()
}

let mut table = Table::with_styles(
    Styles::default()
        .with(HAlign::Right) // numbers should be right-aligned
        .with(MinWidth(5)), // give each column some space
)
.with_cols(
    (0..NUMS + 2)
        .map(|col| {
            Col::new(
                // only the first and last columns are headers
                Styles::default().with(Header(col == 0 || col == NUMS + 1)),
            )
        })
        .collect(),
);

table.push_row(build_header_row());
table.push_rows(build_body_rows());
table.push_row(build_header_row());

println!("{}", Console::default().render(&table));
╔═════╦═════╤═════╤═════╤═════╤═════╤═════╦═════╗
║     ║    1│    2│    3│    4│    5│    6║     ║
╠═════╬═════╪═════╪═════╪═════╪═════╪═════╬═════╣
║    1║    1│    2│    3│    4│    5│    6║    1║
╟─────╫─────┼─────┼─────┼─────┼─────┼─────╫─────╢
║    2║    2│    4│    6│    8│   10│   12║    2║
╟─────╫─────┼─────┼─────┼─────┼─────┼─────╫─────╢
║    3║    3│    6│    9│   12│   15│   18║    3║
╟─────╫─────┼─────┼─────┼─────┼─────┼─────╫─────╢
║    4║    4│    8│   12│   16│   20│   24║    4║
╟─────╫─────┼─────┼─────┼─────┼─────┼─────╫─────╢
║    5║    5│   10│   15│   20│   25│   30║    5║
╟─────╫─────┼─────┼─────┼─────┼─────┼─────╫─────╢
║    6║    6│   12│   18│   24│   30│   36║    6║
╠═════╬═════╪═════╪═════╪═════╪═════╪═════╬═════╣
║     ║    1│    2│    3│    4│    5│    6║     ║
╚═════╩═════╧═════╧═════╧═════╧═════╧═════╩═════╝

行和列标题的处理完全是渲染器特定的。例如,Markdown最多支持一个标题行,即表的第一行。它仍然能够正确渲染内容,尽管没有Console那样美观。

|     |    1|    2|    3|    4|    5|    6|     |
|----:|----:|----:|----:|----:|----:|----:|----:|
|    1|    1|    2|    3|    4|    5|    6|    1|
|    2|    2|    4|    6|    8|   10|   12|    2|
|    3|    3|    6|    9|   12|   15|   18|    3|
|    4|    4|    8|   12|   16|   20|   24|    4|
|    5|    5|   10|   15|   20|   25|   30|    5|
|    6|    6|   12|   18|   24|   30|   36|    6|
|     |    1|    2|    3|    4|    5|    6|     |

分隔符

分隔符是将表细分为逻辑部分的一种方式。可能有多个这样的部分,分割可以是水平、垂直或两者结合。通过将Separator样式分配给RowCol,或者使用separator()便捷方法实例化Row/Col来创建分隔符。后者通常更受欢迎,因为它更简洁;前者允许你指定额外的样式以提供更大的灵活性。让我们在基本的数独谜题上试一下这个方法。

use stanza::renderer::console::Console;
use stanza::renderer::Renderer;
use stanza::style::{HAlign, MinWidth, Styles};
use stanza::table::{Col, Row, Table};

let table = Table::with_styles(Styles::default().with(MinWidth(3)).with(HAlign::Centred))
    .with_cols(vec![
        Col::default(),
        Col::default(),
        Col::default(),
        Col::separator(),
        Col::default(),
        Col::default(),
        Col::default(),
        Col::separator(),
        Col::default(),
        Col::default(),
        Col::default(),
    ])
    .with_row(["5", "3", " ", "", " ", "7", " ", "", " ", " ", " "])
    .with_row(["6", " ", " ", "", "1", "9", "5", "", " ", " ", " "])
    .with_row([" ", "9", "8", "", " ", " ", " ", "", " ", "6", " "])
    .with_row(Row::separator())
    .with_row(["8", " ", " ", "", " ", "6", " ", "", " ", " ", "3"])
    .with_row(["4", " ", " ", "", "8", " ", "3", "", " ", " ", "1"])
    .with_row(["7", " ", " ", "", " ", "2", " ", "", " ", " ", "6"])
    .with_row(Row::separator())
    .with_row([" ", "6", " ", "", " ", " ", " ", "", " ", "2", "8"])
    .with_row([" ", " ", " ", "", "4", "1", "9", "", " ", " ", "5"])
    .with_row([" ", " ", " ", "", " ", "8", " ", "", " ", "7", "9"]);

println!("{}", Console::default().render(&table));
╔═══╤═══╤═══╤═══╤═══╤═══╤═══╤═══╤═══╤═══╤═══╗
║ 5 │ 3 │   │   │   │ 7 │   │   │   │   │   ║
╟───┼───┼───┤   ├───┼───┼───┤   ├───┼───┼───╢
║ 6 │   │   │   │ 1 │ 9 │ 5 │   │   │   │   ║
╟───┼───┼───┤   ├───┼───┼───┤   ├───┼───┼───╢
║   │ 9 │ 8 │   │   │   │   │   │   │ 6 │   ║
╟───┴───┴───┘   └───┴───┴───┘   └───┴───┴───╢
║                                           ║
╟───┬───┬───┐   ┌───┬───┬───┐   ┌───┬───┬───╢
║ 8 │   │   │   │   │ 6 │   │   │   │   │ 3 ║
╟───┼───┼───┤   ├───┼───┼───┤   ├───┼───┼───╢
║ 4 │   │   │   │ 8 │   │ 3 │   │   │   │ 1 ║
╟───┼───┼───┤   ├───┼───┼───┤   ├───┼───┼───╢
║ 7 │   │   │   │   │ 2 │   │   │   │   │ 6 ║
╟───┴───┴───┘   └───┴───┴───┘   └───┴───┴───╢
║                                           ║
╟───┬───┬───┐   ┌───┬───┬───┐   ┌───┬───┬───╢
║   │ 6 │   │   │   │   │   │   │   │ 2 │ 8 ║
╟───┼───┼───┤   ├───┼───┼───┤   ├───┼───┼───╢
║   │   │   │   │ 4 │ 1 │ 9 │   │   │   │ 5 ║
╟───┼───┼───┤   ├───┼───┼───┤   ├───┼───┼───╢
║   │   │   │   │   │ 8 │   │   │   │ 7 │ 9 ║
╚═══╧═══╧═══╧═══╧═══╧═══╧═══╧═══╧═══╧═══╧═══╝

动态内容

到目前为止,我们一直在表创建时确定数据,使用底层Content::Label。更常见的情况是,在所有必要的数据都可用后,将表模型作为某个过程的最后一步构建,然后立即渲染。

还有另一种方法,其中表模型纯粹用作占位布局,可能有几个静态标签(例如,标题)。其余的数据可以在渲染时计算。这种“延迟绑定”方法是由Content::Computed实现的。

以下示例显示了早期绑定单元格值与晚期绑定值之间的区别。左下角的值在模型构建时调用current_time()获取。右下角的单元格使用Content::Computed嵌入一个闭包,在调用render()时评估。示例故意注入了2秒的等待时间,以便两个值不同。

use std::thread;
use std::time::Duration;
use stanza::renderer::console::Console;
use stanza::renderer::Renderer;
use stanza::style::{Header, Styles};
use stanza::table::{Content, Row, Table};

fn current_time() -> String {
    chrono::Utc::now().format("%H:%M:%S").to_string()
}

// build the table model
let table = Table::default()
    .with_row(Row::new(
        Styles::default().with(Header(true)),
        vec!["Early-bound".into(), "Late-bound".into()],
    ))
    .with_row(Row::new(
        Styles::default(),
        vec![
            current_time().into(),
            Content::Computed(Box::new(current_time)).into(),
        ],
    ));

// wait a little
thread::sleep(Duration::from_secs(2));

// render the table
println!("{}", Console::default().render(&table));
╔═══════════╤══════════╗
║Early-bound│Late-bound║
╠═══════════╪══════════╣
║07:40:41   │07:40:43  ║
╚═══════════╧══════════╝

嵌套表格

最大的布局灵活性来自于嵌套表格。嵌套基本上允许你将不同结构的在不同顶层表中组合在一起。

下一个示例结合了两个不同的数据集,并添加了垂直分隔符以保持整洁。

use stanza::renderer::console::Console;
use stanza::renderer::Renderer;
use stanza::style::{HAlign, MinWidth, Separator, Styles};
use stanza::table::{Col, Row, Table};

let table = Table::with_styles(Styles::default().with(HAlign::Centred))
    .with_cols(vec![
        Col::default(),
        Col::new(Styles::default().with(Separator(true)).with(MinWidth(5))),
        Col::default()
    ])
    .with_row(Row::from(["Sensors", "", "Stocks"]))
    .with_row(Row::new(Styles::default(), vec![
        Table::default()
            .with_row(Row::from(["Water", "19.3"]))
            .with_row(Row::from(["Oil", "65.1"]))
            .into(),
        "".into(),
        Table::default()
            .with_row(Row::from(["AAPL", "138.20"]))
            .with_row(Row::from(["AMZN", "113.20"]))
            .with_row(Row::from(["IBM", "118.81"]))
            .into(),
    ]));


println!("{}", Console::default().render(&table));
╔════════════╤═════╤═════════════╗
║  Sensors   │     │   Stocks    ║
╟────────────┤     ├─────────────╢
║╔═════╤════╗│     │╔════╤══════╗║
║║Water│19.3║│     │║AAPL│138.20║║
║╟─────┼────╢│     │╟────┼──────╢║
║║Oil  │65.1║│     │║AMZN│113.20║║
║╚═════╧════╝│     │╟────┼──────╢║
║            │     │║IBM │118.81║║
║            │     │╚════╧══════╝║
╚════════════╧═════╧═════════════╝

请注意,在幕后,嵌套使用的是 Content::Nested,尽管在我们给出的示例中我们没有显式地使用这个枚举变体。这是因为简化的语法使用 into() 将一个 Table 转换为 Content::Nested(Table)。这和在实现了 ToString 的任何值上调用 into() 类似——值将被转换为 Content::Label(String)

嵌套表的显著限制是,内部表的全部字符格式将被外部表单元格的格式所替换。您仍然可以使用任何布局样式(HAlignMinWidthHeader等),只是字符格式样式(BoldItalicTextFg等)将被忽略。

组合内容

到目前为止,我们已经使用了各种 Content 枚举变体来分配不同类型的内容——纯文本、计算值和嵌套表——到任何给定的单元格。如果我们需要将几种不同类型的内容组合到单个单元格中怎么办?这是通过使用 Content::Composite 变体来实现的。

让我们以上面的“嵌套表”示例为例。目前,它将“传感器温度”和“股票价格”的标签分别显示在嵌套表上方的单独行中。让我们将它们合并到一个单元格中。

use stanza::renderer::console::Console;
use stanza::renderer::Renderer;
use stanza::style::{HAlign, MinWidth, Separator, Styles};
use stanza::table::{Col, Content, Row, Table};

let table = Table::with_styles(Styles::default().with(HAlign::Centred))
    .with_cols(vec![
        Col::default(),
        Col::new(Styles::default().with(Separator(true)).with(MinWidth(5))),
        Col::default(),
    ])
    .with_row(Row::new(
        Styles::default(),
        vec![
            Content::Composite(vec![
                "Sensors\n".into(),
                Table::default()
                    .with_row(Row::from(["Water", "19.3"]))
                    .with_row(Row::from(["Oil", "65.1"]))
                    .into(),
            ])
            .into(),
            "".into(),
            Content::Composite(vec![
                "Stocks\n".into(),
                Table::default()
                    .with_row(Row::from(["AAPL", "138.20"]))
                    .with_row(Row::from(["AMZN", "113.20"]))
                    .with_row(Row::from(["IBM", "118.81"]))
                    .into(),
            ])
            .into(),
        ],
    ));

println!("{}", Console::default().render(&table));
╔════════════╤═════╤═════════════╗
║  Sensors   │     │   Stocks    ║
║╔═════╤════╗│     │╔════╤══════╗║
║║Water│19.3║│     │║AAPL│138.20║║
║╟─────┼────╢│     │╟────┼──────╢║
║║Oil  │65.1║│     │║AMZN│113.20║║
║╚═════╧════╝│     │╟────┼──────╢║
║            │     │║IBM │118.81║║
║            │     │╚════╧══════╝║
╚════════════╧═════╧═════════════╝

高级渲染

除了 render() 方法外,还有一种更复杂的替代方法——render_with_hints(),它接受一个不可变引用的 Table 和一个 RenderHint 切片。提示提供了对渲染器行为的更高级控制。通常,在大多数用例中不需要它们——在先前的示例中,我们飞快地通过了渲染器正确执行的操作。

提示在 Stanza 内部用于向渲染器提供可能不知道的额外上下文。例如,当您使用 Content::Nested 时,表将被递归渲染。这对于可能使用字符格式样式(当使用 Console 渲染器时输出特殊的 ANSI 转义序列)的内部表来说是一个问题。这些转义序列会干扰外部表单元格在样式化过程中生成的转义序列。为了解决这个问题,外部渲染例程在递归过程中传递了 RenderHint::Nested,告诉内部渲染例程抑制这些转义序列。

为什么要在渲染器自动处理的情况下使用提示?以嵌套表为例。虽然 Content::Nested 枚举变体很方便,但它非常不灵活。使用它意味着嵌套表将使用与外部表相同的渲染器来绘制。这就是为什么在先前的示例中,内部表和外部表共享整体外观和感觉。

要完全控制表格样式,我们需要在将输出嵌入外部表格之前预渲染内部表格。这样我们就可以控制内部渲染的所有方面。实际上,有人可能会使用完全不同类型的渲染器。在下面的示例中,我们使用了两种不同的Console渲染器:外部渲染器保持默认配置,而内部渲染器去掉了外部边框。

use stanza::renderer::console::{Console, Decor};
use stanza::renderer::{Renderer, RenderHint};
use stanza::style::{HAlign, MinWidth, Separator, Styles};
use stanza::table::{Col, Row, Table};

let inner_renderer = Console({
    let mut decor = Decor::default();
    decor.draw_outer_border = false;
    decor
});

let sensors = Table::default()
    .with_row(Row::from(["Water", "19.3"]))
    .with_row(Row::from(["Oil", "65.1"]));

let stocks = Table::default()
    .with_row(Row::from(["AAPL", "138.20"]))
    .with_row(Row::from(["AMZN", "113.20"]))
    .with_row(Row::from(["IBM", "118.81"]));

let outer = Table::with_styles(Styles::default().with(HAlign::Centred))
    .with_cols(vec![
        Col::default(),
        Col::new(Styles::default().with(Separator(true)).with(MinWidth(5))),
        Col::default(),
    ])
    .with_row(Row::from(["Sensors", "", "Stocks"]))
    .with_row(Row::new(
        Styles::default(),
        vec![
            inner_renderer.render_with_hints(&sensors, &[RenderHint::Nested]).into(),
            "".into(),
            inner_renderer.render_with_hints(&stocks, &[RenderHint::Nested]).into()
        ],
    ));

println!("{}", Console::default().render(&outer));
╔══════════╤═════╤═══════════╗
║ Sensors  │     │  Stocks   ║
╟──────────┤     ├───────────╢
║Water│19.3│     │AAPL│138.20║
║─────┼────│     │────┼──────║
║Oil  │65.1│     │AMZN│113.20║
║          │     │────┼──────║
║          │     │IBM │118.81║
╚══════════╧═════╧═══════════╝

无运行时依赖