Skip to main content

mdsee_layout/
lib.rs

1//! mdsee-layout(design.md §5)。
2//!
3//! Document ASTを表示可能なレイアウトへ変換する。
4//! Terminal escape sequenceは生成しない。
5
6mod block;
7mod model;
8mod table;
9mod wrap;
10
11pub use model::{
12    CodeLayout, LayoutBlock, LayoutDocument, LayoutLine, LayoutSpan, LinkTarget, RuleLayout,
13    SemanticStyle, TableLayout, TextBlock,
14};
15pub use table::TableLayoutEngine;
16
17use thiserror::Error;
18
19use mdsee_core::Document;
20
21/// layout options(§21, §22)。
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct LayoutOptions {
24    pub terminal_width: u16,
25    /// 本文の最大幅。デフォルト100(§21)。
26    pub max_width: u16,
27    /// 左右margin。デフォルト2(§22)。
28    pub margin: u16,
29}
30
31impl Default for LayoutOptions {
32    fn default() -> Self {
33        Self {
34            terminal_width: 80,
35            max_width: 100,
36            margin: 2,
37        }
38    }
39}
40
41/// Layout context(§21)。
42///
43/// Sprint 1では幅情報のみを保持する。§21の `theme` / `capabilities` は
44/// §101の依存方向(layout → terminal 禁止)と両立しないため、
45/// 必要になった時点で設計を改訂したうえで導入する。
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct LayoutContext {
48    pub terminal_width: u16,
49    pub content_width: u16,
50}
51
52impl LayoutContext {
53    /// `content_width = min(terminal_width - margin * 2, max_width)`(§21)。
54    pub fn from_options(options: &LayoutOptions) -> Self {
55        let inner = options
56            .terminal_width
57            .saturating_sub(options.margin.saturating_mul(2));
58        let content_width = inner.min(options.max_width).max(1);
59        Self {
60            terminal_width: options.terminal_width,
61            content_width,
62        }
63    }
64}
65
66/// layout error(§66)。
67///
68/// Sprint 1では発生しないが、pipeline署名(§100)のために定義する。
69#[derive(Debug, Error)]
70pub enum LayoutError {
71    #[error("layout failed")]
72    LayoutFailed,
73}
74
75/// Document ASTをLayoutDocumentへ変換する(§100 基本pipeline)。
76pub fn layout(document: &Document, options: &LayoutOptions) -> Result<LayoutDocument, LayoutError> {
77    let ctx = LayoutContext::from_options(options);
78    let blocks = document
79        .blocks
80        .iter()
81        .map(|block| block::layout_block(block, &ctx))
82        .collect();
83    Ok(LayoutDocument { blocks })
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn content_width_is_min_of_inner_width_and_max_width() {
92        // terminal 80 / margin 2 / max 100 → 76
93        let ctx = LayoutContext::from_options(&LayoutOptions {
94            terminal_width: 80,
95            max_width: 100,
96            margin: 2,
97        });
98        assert_eq!(ctx.content_width, 76);
99
100        // terminal 120 → max_width 100 で頭打ち(§21)
101        let ctx = LayoutContext::from_options(&LayoutOptions {
102            terminal_width: 120,
103            max_width: 100,
104            margin: 2,
105        });
106        assert_eq!(ctx.content_width, 100);
107    }
108
109    #[test]
110    fn content_width_never_drops_below_one() {
111        let ctx = LayoutContext::from_options(&LayoutOptions {
112            terminal_width: 0,
113            max_width: 100,
114            margin: 2,
115        });
116        assert_eq!(ctx.content_width, 1);
117    }
118}