Skip to main content

documents/
markdown.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! One window of a Markdown version, laid out by the city's one Markdown
7//! grammar (`crates/documents/spec/Markdown.lean`, D20-D26).
8//!
9//! The window is cut the way a range is cut, then, when the version runs
10//! on past it, ended where its second-to-last block ends: the last block
11//! may continue outside the window, and a block laid out from half its
12//! lines is a different block. Only the UTF-8 encodings are laid out,
13//! because a block names its bytes and only there do the bytes of the
14//! text read from a window line up with the version's.
15
16mod export;
17mod lowering;
18mod position;
19mod target;
20mod tree;
21
22pub use export::{EXPORT_BYTES_MAX, export};
23pub use tree::{Align, Block, Check, Construct, Inline, Laid, ListItem, Order, Preview};
24pub use tree::{ReplyState, Row, Spacing};
25
26use kernel::AxError;
27
28use crate::encoding::Encoding;
29use crate::format::Format;
30use crate::layout;
31use crate::span::{Span, offset};
32use crate::window::{Lifted, Window, cut};
33
34/// The preview of the window `viewport` asks for, cut from bytes
35/// [`lift`](crate::lift) named.
36///
37/// # Errors
38/// The [`cut`] refusals: lifted bytes that do not hold the window, or
39/// that are not text in `encoding`.
40pub fn preview(encoding: Encoding, lifted: Lifted<'_>, viewport: Span) -> Result<Preview, AxError> {
41    match encoding {
42        Encoding::Utf16Le | Encoding::Utf16Be => Ok(Preview::Unsupported { encoding }),
43        Encoding::Utf8 | Encoding::Utf8Bom => laid(encoding, lifted, viewport).map(Preview::Laid),
44    }
45}
46
47/// The blocks of a model's reply that can no longer change, read by the
48/// grammar a preview is (D30): a settled reply is read as a version
49/// holding exactly its text, one still streaming only up to its closure
50/// point (D31). At most one window, as a preview; `span.end` is where
51/// the rest of the text is sent from.
52///
53/// # Errors
54/// The [`cut`] refusals: a text holding a NUL is not unmarked UTF-8
55/// text (D4).
56pub fn reply(text: &str, state: ReplyState) -> Result<Laid, AxError> {
57    let bytes = text.as_bytes();
58    let size = match state {
59        ReplyState::Settled => offset(bytes.len()),
60        ReplyState::Streaming => layout::closed(bytes),
61    };
62    laid(
63        Encoding::Utf8,
64        Lifted { at: 0, bytes, size },
65        Span::ordered(0, size),
66    )
67}
68
69/// One window of UTF-8 text, cut, ended on a block, and laid out.
70fn laid(encoding: Encoding, lifted: Lifted<'_>, viewport: Span) -> Result<Laid, AxError> {
71    let window = settled(cut(encoding, lifted, viewport)?, lifted.size);
72    let blocks = lowering::blocks(&window.text, window.span.start());
73    Ok(Laid {
74        span: window.span,
75        blocks,
76    })
77}
78
79/// The window, ended at its second-to-last block when the version runs
80/// on past it (`Documents.Markdown.settle`). A window whose only block
81/// runs on, or that reaches the version's end, stays as it was cut, so
82/// a reader always moves forward.
83fn settled(window: Window, size: u64) -> Window {
84    if window.span.end() >= size {
85        return window;
86    }
87    let ends = layout::blocks(Format::Markdown, window.text.as_bytes());
88    let Some(end) = ends
89        .iter()
90        .rev()
91        .nth(1)
92        .map(|block| block.end())
93        .filter(|end| *end > 0)
94    else {
95        return window;
96    };
97    let kept = usize::try_from(end)
98        .ok()
99        .and_then(|end| window.text.get(..end));
100    match kept {
101        Some(text) => Window {
102            span: Span::ordered(
103                window.span.start(),
104                window.span.start().saturating_add(offset(text.len())),
105            ),
106            text: text.to_owned(),
107        },
108        None => window,
109    }
110}
111
112#[cfg(test)]
113#[allow(
114    clippy::unwrap_used,
115    clippy::expect_used,
116    clippy::panic,
117    clippy::indexing_slicing,
118    clippy::arithmetic_side_effects,
119    reason = "test code"
120)]
121mod tests;