use std::fmt::Write as _;
use crate::api::wiki::{
CursorPage, WikiAttachment, WikiComment, WikiGrid, WikiGridRef, WikiHits, WikiPage,
WikiPageRef, WikiResource,
};
use crate::render::Context;
use crate::render::style::Palette;
use crate::render::table::{Column, cursor_tally, open_page_tally, render};
#[must_use]
pub fn hits(found: &WikiHits, ctx: &Context) -> String {
let columns = [
Column::whole("SLUG", 44, Palette::key()),
Column::new("TYPE", 5, anstyle::Style::new()),
Column::new("MODIFIED", 10, Palette::label()),
Column::new("TITLE", 50, Palette::untrusted()),
];
let rows: Vec<Vec<String>> = found
.results
.iter()
.map(|hit| {
vec![
hit.slug.clone(),
hit.kind.clone(),
hit.modified_at
.as_deref()
.map_or_else(|| "-".to_owned(), |at| at.chars().take(10).collect()),
hit.title.clone(),
]
})
.collect();
let mut out = render(&columns, &rows, ctx);
out.push_str(&open_page_tally(found.results.len(), found.next_page, ctx));
out
}
#[must_use]
pub fn comments(slug: &str, list: &CursorPage<WikiComment>, ctx: &Context) -> String {
let mut out = String::with_capacity(list.results.len() * 200 + 64);
let paint = ctx.painter();
for comment in &list.results {
let author = comment.author.as_deref().unwrap_or("-");
let mut header = format!(
"--- {} by {author} at {}",
comment.id,
comment.created_at.as_deref().unwrap_or("-")
);
if comment.resolved {
header.push_str(" (resolved)");
}
if comment.deleted {
header.push_str(" (deleted)");
}
if let Some(posts) = comment.thread_posts.filter(|posts| *posts > 1) {
let _ = write!(header, " — {posts} in thread: --thread {}", comment.id);
}
let _ = writeln!(out, "{}", paint.paint(&header, Palette::label()));
if comment.deleted || comment.body.is_empty() {
continue;
}
crate::render::text::quoted_block(
&mut out,
&format!("wiki:{slug}/comment/{} by {author}", comment.id),
crate::render::untrusted::Author::Wiki,
&comment.body,
0,
ctx,
);
}
out.push_str(&cursor_tally(
list.results.len(),
list.next_cursor.as_deref(),
ctx,
));
out
}
fn megabytes(size: &str) -> String {
match size.parse::<f64>() {
Ok(mb) if mb > 0.0 => format!("{size} MB"),
Ok(_) => "<0.01 MB".to_owned(),
Err(_) => size.to_owned(),
}
}
#[must_use]
pub fn attachments(list: &CursorPage<WikiAttachment>, ctx: &Context) -> String {
let columns = [
Column::whole("ID", 10, Palette::key()),
Column::whole("SIZE", 10, anstyle::Style::new()),
Column::new("TYPE", 18, anstyle::Style::new()),
Column::new("CREATED", 10, Palette::label()),
Column::whole("NAME", 40, Palette::untrusted()),
];
let rows: Vec<Vec<String>> = list
.results
.iter()
.map(|file| {
vec![
file.id.to_string(),
megabytes(&file.size),
file.mimetype.as_deref().unwrap_or("-").to_owned(),
file.created_at
.as_deref()
.map_or_else(|| "-".to_owned(), |at| at.chars().take(10).collect()),
file.name.clone(),
]
})
.collect();
let mut out = render(&columns, &rows, ctx);
out.push_str(&cursor_tally(
list.results.len(),
list.next_cursor.as_deref(),
ctx,
));
out
}
#[must_use]
pub fn grid(grid: &WikiGrid, ctx: &Context) -> String {
let paint = ctx.painter();
let label = |text: &str| paint.paint(text, Palette::label());
let mut out = String::with_capacity(256 + grid.rows.len() * 64);
let _ = writeln!(
out,
"{} {}",
paint.paint(&grid.id, Palette::key()),
grid.title
);
let _ = writeln!(
out,
"{} {} {} {}",
label("page:"),
grid.page.as_ref().map_or("-", |page| page.slug.as_str()),
label("revision:"),
if grid.revision.is_empty() {
"-"
} else {
&grid.revision
},
);
let columns: Vec<String> = grid
.columns
.iter()
.map(|column| format!("{}:{}", column.slug, column.kind))
.collect();
let _ = writeln!(out, "{} {}", label("columns:"), columns.join(" "));
let mut table = String::with_capacity(grid.rows.len() * 64);
let titles: Vec<String> = grid
.columns
.iter()
.map(|column| tsv_cell(&column.title))
.collect();
let _ = writeln!(table, "{}", titles.join("\t"));
for row in &grid.rows {
let cells: Vec<String> = row
.cells
.iter()
.map(|value| tsv_cell(&cell_text(value)))
.collect();
let _ = writeln!(table, "{}", cells.join("\t"));
}
let (body, withheld) = crate::render::untrusted::head(&table, ctx.description_lines);
let body = if ctx.is_human() {
format!("```\n{}\n```", body.trim_end())
} else {
body
};
crate::render::text::quoted_block(
&mut out,
&format!("wiki:grid/{}", grid.id),
crate::render::untrusted::Author::Wiki,
&body,
withheld,
ctx,
);
let shown = grid.rows.len().saturating_sub(withheld);
let _ = writeln!(
out,
"{}",
label(&format!("shown {shown} of {}", grid.rows.len()))
);
out
}
fn cell_text(value: &serde_json::Value) -> String {
use serde_json::Value;
match value {
Value::Null => String::new(),
Value::String(text) => text.clone(),
Value::Array(items) => items.iter().map(cell_text).collect::<Vec<_>>().join(", "),
Value::Object(fields) => ["username", "display", "key"]
.iter()
.find_map(|name| fields.get(*name).and_then(Value::as_str))
.map_or_else(|| value.to_string(), str::to_owned),
other => other.to_string(),
}
}
fn tsv_cell(text: &str) -> String {
let mut cell = String::with_capacity(text.len());
for character in text.chars() {
match character {
'\\' => cell.push_str("\\\\"),
'\t' => cell.push_str("\\t"),
'\n' => cell.push_str("\\n"),
'\r' => cell.push_str("\\r"),
other => cell.push(other),
}
}
cell
}
#[must_use]
pub fn grids(list: &CursorPage<WikiGridRef>, ctx: &Context) -> String {
let columns = [
Column::whole("ID", 36, Palette::key()),
Column::new("CREATED", 10, Palette::label()),
Column::new("TITLE", 50, Palette::untrusted()),
];
let rows: Vec<Vec<String>> = list
.results
.iter()
.map(|grid| {
vec![
grid.id.clone(),
day(grid.created_at.as_deref()),
grid.title.clone(),
]
})
.collect();
let mut out = render(&columns, &rows, ctx);
out.push_str(&cursor_tally(
list.results.len(),
list.next_cursor.as_deref(),
ctx,
));
out
}
#[must_use]
pub fn resources(list: &CursorPage<WikiResource>, ctx: &Context) -> String {
let columns = [
Column::new("TYPE", 10, anstyle::Style::new()),
Column::whole("ID", 36, Palette::key()),
Column::new("CREATED", 10, Palette::label()),
Column::whole("NAME", 40, Palette::untrusted()),
];
let rows: Vec<Vec<String>> = list
.results
.iter()
.map(|resource| {
vec![
resource.kind.clone(),
resource.id.clone(),
day(resource.created_at.as_deref()),
resource.name.clone(),
]
})
.collect();
let mut out = render(&columns, &rows, ctx);
out.push_str(&cursor_tally(
list.results.len(),
list.next_cursor.as_deref(),
ctx,
));
out
}
fn day(at: Option<&str>) -> String {
at.map_or_else(|| "-".to_owned(), |at| at.chars().take(10).collect())
}
#[must_use]
pub fn access(access: &crate::api::wiki::WikiAccess, ctx: &Context) -> String {
let paint = ctx.painter();
let label = |text: &str| paint.paint(text, Palette::label());
let mut out = String::with_capacity(128 + access.entries.len() * 80);
let policy = access.policy.as_deref().unwrap_or("-");
let inherited = access
.inherited_policy
.as_deref()
.filter(|_| policy == "inherited")
.map_or_else(String::new, |inherited| format!(" ({inherited})"));
let _ = writeln!(
out,
"{} {} {policy}{inherited} {} {}",
paint.paint(&access.slug, Palette::key()),
label("access:"),
label("all staff:"),
access.all_staff_role.as_deref().unwrap_or("-"),
);
let columns = [
Column::whole("ID", 24, Palette::key()),
Column::new("ROLE", 12, anstyle::Style::new()),
Column::new("KIND", 5, anstyle::Style::new()),
Column::new("VIA", 9, Palette::label()),
Column::whole("WHO", 30, Palette::untrusted()),
];
let rows: Vec<Vec<String>> = access
.entries
.iter()
.map(|entry| {
vec![
entry.id.clone(),
entry.role.clone(),
entry.kind.clone(),
entry.via.clone(),
entry.who.clone(),
]
})
.collect();
out.push_str(&render(&columns, &rows, ctx));
out.push_str(&cursor_tally(access.entries.len(), None, ctx));
out
}
#[must_use]
pub fn operation(
operation: &crate::api::wiki::WikiOperation,
status: &crate::api::wiki::OperationStatus,
) -> String {
let mut line = format!(
"operation {} {}: {}",
operation.kind, operation.id, status.status
);
if let Some(percentage) = status.percentage.filter(|_| !status.is_done()) {
let _ = write!(line, " {percentage:.0}%");
}
if let Some(slug) = status.page_slug() {
let _ = write!(line, " — page {slug}");
}
if let Some(grid) = status.grid_id() {
let _ = write!(line, ", grid {grid}");
}
if let Some(details) = &status.details {
let _ = write!(line, " ({details})");
}
line.push('\n');
line
}
#[must_use]
pub fn pages(list: &CursorPage<WikiPageRef>, ctx: &Context) -> String {
let columns = [
Column::whole("SLUG", 60, Palette::key()),
Column::whole("ID", 10, Palette::label()),
];
let rows: Vec<Vec<String>> = list
.results
.iter()
.map(|page| vec![page.slug.clone(), page.id.to_string()])
.collect();
let mut out = render(&columns, &rows, ctx);
out.push_str(&cursor_tally(
list.results.len(),
list.next_cursor.as_deref(),
ctx,
));
out
}
#[must_use]
pub fn page(page: &WikiPage, ctx: &Context) -> String {
let mut out = String::with_capacity(256 + page.content.as_ref().map_or(0, String::len));
let paint = ctx.painter();
let label = |text: &str| paint.paint(text, Palette::label());
let _ = writeln!(
out,
"{} {}",
paint.paint(&page.slug, Palette::key()),
page.title
);
let _ = writeln!(
out,
"{} {} {} {} {} {}",
label("id:"),
page.id,
label("type:"),
page.page_type.as_deref().unwrap_or("-"),
label("modified:"),
page.modified_at.as_deref().unwrap_or("-"),
);
if let Some(content) = page.content.as_deref().filter(|text| !text.is_empty()) {
let folded;
let content = if ctx.description_lines.is_some() {
folded = fold_data_uris(content);
folded.as_str()
} else {
content
};
let (body, withheld) = crate::render::untrusted::head(content, ctx.description_lines);
crate::render::text::quoted_block(
&mut out,
&format!("wiki:{}", page.slug),
crate::render::untrusted::Author::Wiki,
&body,
withheld,
ctx,
);
}
out
}
const FOLD_OVER: usize = 256;
fn fold_data_uris(text: &str) -> String {
const MARKER: &str = ";base64,";
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(at) = rest.find("data:") {
let after = &rest[at..];
let Some(marker) = after.find(MARKER).filter(|&end| {
after[5..end]
.chars()
.all(|c| c.is_ascii_alphanumeric() || "+-./".contains(c))
}) else {
out.push_str(&rest[..at + 5]);
rest = &rest[at + 5..];
continue;
};
let start = at + marker + MARKER.len();
let length = rest[start..]
.find(|c: char| !(c.is_ascii_alphanumeric() || "+/=".contains(c)))
.unwrap_or(rest.len() - start);
out.push_str(&rest[..start]);
if length > FOLD_OVER {
let tenths = (length * 3 / 4 * 10 + 512) / 1024;
let _ = write!(
out,
"…({}.{} KB, --full shows it)",
tenths / 10,
tenths % 10
);
} else {
out.push_str(&rest[start..start + length]);
}
rest = &rest[start + length..];
}
out.push_str(rest);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::{Audience, Format};
#[test]
fn a_long_inline_picture_is_folded_to_its_size() {
let payload = "A".repeat(4096);
let text = format!(
"before {{% drawio data=\"data:image/svg+xml;base64,{payload}\" width=\"600\" %}} after"
);
assert_eq!(
fold_data_uris(&text),
"before {% drawio data=\"data:image/svg+xml;base64,…(3.0 KB, --full shows it)\" width=\"600\" %} after"
);
}
#[test]
fn a_short_payload_and_plain_text_are_left_alone() {
let text = "icon  and the word data: here";
assert_eq!(fold_data_uris(text), text);
}
#[test]
fn full_shows_an_inline_picture_byte_for_byte() {
let content = format!("x data:image/png;base64,{}", "B".repeat(1000));
let mut shown = sample();
shown.content = Some(content.clone());
let mut full = ctx();
full.description_lines = None;
assert!(page(&shown, &full).contains(&content));
assert!(page(&shown, &ctx()).contains("…(0.7 KB, --full shows it)"));
}
fn ctx() -> Context {
Context {
format: Format::Text,
audience: Audience::Machine,
description_lines: Some(2),
extra_fields: Vec::new(),
width: 80,
images: false,
inline: crate::render::image::Inline::default(),
}
}
fn sample() -> WikiPage {
WikiPage {
id: 4521,
slug: "users/ilubenets/runbook".to_owned(),
title: "Deploy runbook".to_owned(),
page_type: Some("wysiwyg".to_owned()),
modified_at: Some("2026-09-01T10:15:00Z".to_owned()),
content: Some("# Deploy\n\n1. Tag the release.\n2. Watch the pipeline.".to_owned()),
}
}
#[test]
fn pages_view_is_stable() {
let list = CursorPage {
results: vec![
WikiPageRef {
id: 4521,
slug: "users/ilubenets/runbook".to_owned(),
},
WikiPageRef {
id: 4522,
slug: "users/ilubenets/runbook/rollback".to_owned(),
},
],
next_cursor: Some("eyJpZCI6NDUyMn0=".to_owned()),
};
insta::assert_snapshot!(pages(&list, &ctx()));
}
#[test]
fn hits_view_is_stable() {
let found = WikiHits {
results: vec![
crate::api::wiki::WikiHit {
slug: "users/ilubenets/runbook".to_owned(),
title: "Deploy runbook".to_owned(),
kind: "page".to_owned(),
modified_at: Some("2026-09-01T10:15:00Z".to_owned()),
url: None,
snippet: Some("tag the release".to_owned()),
},
crate::api::wiki::WikiHit {
slug: "users/ilubenets/runbook/.files/rollback.pdf".to_owned(),
title: "rollback.pdf".to_owned(),
kind: "file".to_owned(),
modified_at: None,
url: None,
snippet: None,
},
],
next_page: Some(2),
};
insta::assert_snapshot!(hits(&found, &ctx()));
}
#[test]
fn comments_view_is_stable() {
let comment = |id: u64, body: &str| WikiComment {
id,
author: Some("ilubenets".to_owned()),
created_at: Some("2026-09-02T08:00:00Z".to_owned()),
body: body.to_owned(),
resolved: false,
deleted: false,
quote: None,
thread_posts: Some(1),
};
let list = CursorPage {
results: vec![
WikiComment {
thread_posts: Some(3),
..comment(7001, "Step 2 needs the canary first.")
},
WikiComment {
resolved: true,
..comment(7002, "Typo in the title.")
},
WikiComment {
deleted: true,
..comment(7003, "")
},
],
next_cursor: None,
};
insta::assert_snapshot!(comments("users/ilubenets/runbook", &list, &ctx()));
}
#[test]
fn attachments_view_is_stable() {
let list = CursorPage {
results: vec![
WikiAttachment {
id: 901,
name: "rollback.pdf".to_owned(),
size: "0.25".to_owned(),
mimetype: Some("application/pdf".to_owned()),
created_at: Some("2026-09-01T11:00:00Z".to_owned()),
author: Some("ilubenets".to_owned()),
download_url: None,
},
WikiAttachment {
id: 902,
name: "notes.txt".to_owned(),
size: "-".to_owned(),
mimetype: None,
created_at: None,
author: None,
download_url: None,
},
],
next_cursor: Some("eyJpZCI6OTAyfQ==".to_owned()),
};
insta::assert_snapshot!(attachments(&list, &ctx()));
}
fn sample_grid() -> WikiGrid {
use crate::api::wiki::{GridColumn, GridRow};
let column = |slug: &str, title: &str, kind: &str| GridColumn {
slug: slug.to_owned(),
title: title.to_owned(),
kind: kind.to_owned(),
};
WikiGrid {
id: "8f1e2d3c-4b5a-4c6d-8e7f-9a0b1c2d3e4f".to_owned(),
title: "Releases".to_owned(),
page: Some(WikiPageRef {
id: 4521,
slug: "users/ilubenets/runbook".to_owned(),
}),
revision: "12".to_owned(),
columns: vec![
column("version", "Version", "string"),
column("owner", "Owner", "staff"),
column("ticket", "Ticket", "ticket"),
column("status", "Status", "ticket_field"),
column("done", "Done", "checkbox"),
],
rows: vec![
GridRow {
id: "1".to_owned(),
cells: vec![
serde_json::json!("1.2.0"),
serde_json::json!([{"username": "ilubenets", "display_name": "Ilya"}]),
serde_json::json!({"key": "PROJ-1", "resolved": false}),
serde_json::json!({"key": "inProgress", "display": "В работе"}),
serde_json::json!(false),
],
},
GridRow {
id: "2".to_owned(),
cells: vec![
serde_json::json!("1.3.0\tbeta\nsecond line"),
serde_json::json!([]),
serde_json::Value::Null,
serde_json::Value::Null,
serde_json::json!(true),
],
},
],
}
}
#[test]
fn grid_view_is_stable() {
let full = Context {
description_lines: None,
..ctx()
};
insta::assert_snapshot!(grid(&sample_grid(), &full));
}
#[test]
fn a_long_grid_is_cut_and_says_so() {
let text = grid(&sample_grid(), &ctx());
assert!(text.contains("(+1 more lines: --full)"), "{text}");
assert!(text.trim_end().ends_with("shown 1 of 2"), "{text}");
}
#[test]
fn grids_view_is_stable() {
let list = CursorPage {
results: vec![WikiGridRef {
id: "8f1e2d3c-4b5a-4c6d-8e7f-9a0b1c2d3e4f".to_owned(),
title: "Releases".to_owned(),
created_at: Some("2026-08-01T09:00:00Z".to_owned()),
}],
next_cursor: None,
};
insta::assert_snapshot!(grids(&list, &ctx()));
}
#[test]
fn resources_view_is_stable() {
let list = CursorPage {
results: vec![
WikiResource {
kind: "attachment".to_owned(),
id: "901".to_owned(),
name: "rollback.pdf".to_owned(),
created_at: Some("2026-09-01T11:00:00Z".to_owned()),
},
WikiResource {
kind: "grid".to_owned(),
id: "8f1e2d3c-4b5a-4c6d-8e7f-9a0b1c2d3e4f".to_owned(),
name: "Releases".to_owned(),
created_at: None,
},
],
next_cursor: Some("eyJpZCI6OTAyfQ==".to_owned()),
};
insta::assert_snapshot!(resources(&list, &ctx()));
}
#[test]
fn access_view_is_stable() {
use crate::api::wiki::{AccessEntry, WikiAccess};
let entry = |id: &str, role: &str, kind: &str, who: &str, via: &str| AccessEntry {
id: id.to_owned(),
role: role.to_owned(),
kind: kind.to_owned(),
who: who.to_owned(),
via: via.to_owned(),
inheritance: None,
};
let page_access = WikiAccess {
slug: "users/ilubenets/runbook".to_owned(),
policy: Some("custom".to_owned()),
inherited_policy: None,
all_staff_role: None,
entries: vec![
entry("a1", "author", "user", "ilubenets", "direct"),
entry("g7", "reader", "group", "Backend team", "inherited"),
],
};
insta::assert_snapshot!(access(&page_access, &ctx()));
}
#[test]
fn operation_view_is_stable() {
use crate::api::wiki::{OperationStatus, WikiOperation};
let grid = WikiOperation {
id: "op2".to_owned(),
kind: "clone_inline_grid".to_owned(),
};
let running = OperationStatus {
status: "in_progress".to_owned(),
percentage: Some(40.0),
details: None,
result: None,
};
let done = OperationStatus {
status: "success".to_owned(),
percentage: Some(100.0),
details: None,
result: Some(serde_json::json!({
"grid_id": "4c1d2e3f-0000-4000-8000-000000000002",
"page": {"id": 4700, "slug": "users/ilubenets/other"}
})),
};
insta::assert_snapshot!(operation(&grid, &running) + &operation(&grid, &done));
}
#[test]
fn page_compact_view_is_stable() {
insta::assert_snapshot!(page(&sample(), &ctx()));
}
#[test]
fn page_terminal_view_is_stable() {
let human = Context {
audience: Audience::Human,
..ctx()
};
insta::assert_snapshot!(page(&sample(), &human));
}
}