use super::text_utils::{WrappedLine, wrap_spans_with_ranges};
use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use std::ops::Range;
use unicode_width::UnicodeWidthStr;
use crate::config::RColorConfig;
use crate::highlighter::{TokenType, tokenize_r};
use crate::pager::style_convert::nu_ansi_color_to_ratatui;
pub fn render_markdown(
input: &str,
default_code_lang: Option<&str>,
wrap_width: Option<usize>,
) -> Vec<Line<'static>> {
render_markdown_with_text(input, default_code_lang, wrap_width).lines
}
pub(super) fn render_markdown_with_text(
input: &str,
default_code_lang: Option<&str>,
wrap_width: Option<usize>,
) -> RenderedMarkdown {
render_markdown_document(input, default_code_lang, wrap_width, |_| {
LinkDisplay::LabelAndDestination
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum LinkDisplay {
LabelOnly,
LabelAndDestination,
}
pub(super) fn render_markdown_document(
input: &str,
default_code_lang: Option<&str>,
wrap_width: Option<usize>,
link_display: impl Fn(&str) -> LinkDisplay,
) -> RenderedMarkdown {
let mut options = Options::empty();
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TABLES);
let parser = Parser::new_ext(input, options);
let mut writer = Writer::new(
parser,
default_code_lang.map(|s| s.to_string()),
wrap_width,
&link_display,
);
writer.run();
let mut document = RenderedMarkdown {
lines: writer.lines,
text: writer.text,
links: writer.links,
};
document.project_links();
document
}
pub(super) struct RenderedMarkdown {
pub lines: Vec<Line<'static>>,
pub text: Vec<RenderedText>,
pub links: Vec<RenderedLink>,
}
pub(super) struct RenderedLink {
pub destination: String,
pub label: String,
pub text_ranges: Vec<LinkTextRange>,
pub fragments: Vec<LinkFragment>,
}
pub(super) struct LinkTextRange {
pub text_index: usize,
pub range: Range<usize>,
}
#[derive(Debug, PartialEq, Eq)]
pub(super) struct LinkFragment {
pub line: usize,
pub byte_range: Range<usize>,
pub columns: Range<usize>,
}
impl RenderedMarkdown {
fn project_links(&mut self) {
for link in &mut self.links {
for label in &link.text_ranges {
for fragment in &self.text[label.text_index].fragments {
let start = label.range.start.max(fragment.source_range.start);
let end = label.range.end.min(fragment.source_range.end);
if start >= end {
continue;
}
let start = fragment.visual_start + start - fragment.source_range.start;
let end = fragment.visual_start + end - fragment.source_range.start;
let line = &self.lines[fragment.line];
link.fragments.push(LinkFragment {
line: fragment.line,
byte_range: start..end,
columns: display_column(line, start)..display_column(line, end),
});
}
}
}
}
}
fn display_column(line: &Line<'_>, mut byte_offset: usize) -> usize {
let mut columns = 0;
for span in &line.spans {
let bytes = byte_offset.min(span.content.len());
columns += UnicodeWidthStr::width(&span.content[..bytes]);
byte_offset -= bytes;
if byte_offset == 0 {
break;
}
}
columns
}
pub(super) struct RenderedText {
pub text: String,
pub fragments: Vec<TextFragment>,
}
pub(super) struct TextFragment {
pub line: usize,
pub source_range: Range<usize>,
pub visual_start: usize,
}
struct TableLine {
text_index: usize,
wrapped: WrappedLine,
}
#[derive(Clone)]
struct InlineSpan {
span: Span<'static>,
link: Option<usize>,
}
struct Styles {
h1: Style,
h2: Style,
h3: Style,
code: Style,
emphasis: Style,
strong: Style,
link: Style,
blockquote_prefix: Style,
code_block: Style,
code_block_bg: Color,
}
impl Default for Styles {
fn default() -> Self {
Self {
h1: Style::new().bold().underlined(),
h2: Style::new().bold(),
h3: Style::new().italic(),
code: Style::new().cyan(),
emphasis: Style::new().italic(),
strong: Style::new().bold(),
link: Style::new().cyan().underlined(),
blockquote_prefix: Style::new().green(),
code_block: Style::new().dim(),
code_block_bg: Color::Indexed(236),
}
}
}
fn token_type_to_style(tt: TokenType) -> Style {
static CONFIG: std::sync::LazyLock<RColorConfig> =
std::sync::LazyLock::new(RColorConfig::default);
let color = match tt {
TokenType::Comment => CONFIG.comment,
TokenType::String => CONFIG.string,
TokenType::Number => CONFIG.number,
TokenType::Keyword => CONFIG.keyword,
TokenType::Constant => CONFIG.constant,
TokenType::Operator => CONFIG.operator,
TokenType::Punctuation => CONFIG.punctuation,
TokenType::Identifier => CONFIG.identifier,
TokenType::Whitespace | TokenType::Other => return Style::default(),
};
match color {
nu_ansi_term::Color::Default => Style::default(),
c => Style::new().fg(nu_ansi_color_to_ratatui(c)),
}
}
fn is_r_language(lang: &str) -> bool {
matches!(lang, "r" | "R")
}
struct Writer<'a, I: Iterator<Item = Event<'a>>> {
iter: I,
lines: Vec<Line<'static>>,
text: Vec<RenderedText>,
links: Vec<RenderedLink>,
link_display: &'a dyn Fn(&str) -> LinkDisplay,
styles: Styles,
default_code_lang: Option<String>,
wrap_width: Option<usize>,
inline_styles: Vec<Style>,
current_spans: Vec<InlineSpan>,
in_heading: Option<HeadingLevel>,
in_code_block: bool,
code_block_lang: Option<String>,
code_block_buffer: String,
list_indices: Vec<Option<u64>>,
list_depth: usize,
blockquote_depth: usize,
link: Option<usize>,
needs_newline: bool,
pending_marker: bool,
has_output: bool,
in_table: bool,
table_rows: Vec<Vec<Vec<InlineSpan>>>,
table_current_row: Vec<Vec<InlineSpan>>,
table_cell_spans: Vec<InlineSpan>,
}
impl<'a, I: Iterator<Item = Event<'a>>> Writer<'a, I> {
fn new(
iter: I,
default_code_lang: Option<String>,
wrap_width: Option<usize>,
link_display: &'a dyn Fn(&str) -> LinkDisplay,
) -> Self {
Self {
iter,
lines: Vec::new(),
text: Vec::new(),
links: Vec::new(),
link_display,
styles: Styles::default(),
default_code_lang,
wrap_width,
inline_styles: Vec::new(),
current_spans: Vec::new(),
in_heading: None,
in_code_block: false,
code_block_lang: None,
code_block_buffer: String::new(),
list_indices: Vec::new(),
list_depth: 0,
blockquote_depth: 0,
link: None,
needs_newline: false,
pending_marker: false,
has_output: false,
in_table: false,
table_rows: Vec::new(),
table_current_row: Vec::new(),
table_cell_spans: Vec::new(),
}
}
fn run(&mut self) {
while let Some(ev) = self.iter.next() {
self.handle_event(ev);
}
self.flush_line();
}
fn handle_event(&mut self, event: Event<'a>) {
match event {
Event::Start(tag) => self.start_tag(tag),
Event::End(tag) => self.end_tag(tag),
Event::Text(text) => self.on_text(text),
Event::Code(code) => self.on_inline_code(code),
Event::SoftBreak => self.on_soft_break(),
Event::HardBreak => self.on_hard_break(),
Event::Rule => self.on_rule(),
Event::Html(html) => self.on_html(html),
Event::InlineHtml(html) => self.on_inline_html(html),
Event::FootnoteReference(_)
| Event::TaskListMarker(_)
| Event::InlineMath(_)
| Event::DisplayMath(_) => {}
}
}
fn start_tag(&mut self, tag: Tag<'a>) {
match tag {
Tag::Paragraph => {
if self.in_table {
return;
}
self.ensure_blank_line_before_block();
}
Tag::Heading { level, .. } => {
self.ensure_blank_line_before_block();
self.in_heading = Some(level);
let prefix = match level {
HeadingLevel::H1 => "# ",
HeadingLevel::H2 => "## ",
HeadingLevel::H3 => "### ",
HeadingLevel::H4 => "#### ",
HeadingLevel::H5 => "##### ",
HeadingLevel::H6 => "###### ",
};
let style = match level {
HeadingLevel::H1 => self.styles.h1,
HeadingLevel::H2 => self.styles.h2,
_ => self.styles.h3,
};
self.push_span(Span::styled(prefix.to_string(), style));
self.inline_styles.push(style);
}
Tag::BlockQuote(_) => {
self.ensure_blank_line_before_block();
self.blockquote_depth += 1;
}
Tag::CodeBlock(kind) => {
self.ensure_blank_line_before_block();
self.in_code_block = true;
let explicit_lang = match kind {
CodeBlockKind::Fenced(lang) => {
let lang = lang.split_whitespace().next().unwrap_or("");
if lang.is_empty() {
None
} else {
Some(lang.to_string())
}
}
CodeBlockKind::Indented => None,
};
self.code_block_lang = explicit_lang.or_else(|| self.default_code_lang.clone());
self.code_block_buffer.clear();
}
Tag::List(start) => {
if self.list_depth == 0 {
self.ensure_blank_line_before_block();
} else {
self.flush_line();
}
self.list_indices.push(start);
self.list_depth += 1;
}
Tag::Item => {
self.flush_line();
self.pending_marker = true;
}
Tag::Emphasis => {
self.inline_styles.push(self.styles.emphasis);
}
Tag::Strong => {
self.inline_styles.push(self.styles.strong);
}
Tag::Strikethrough => {
self.inline_styles.push(Style::new().crossed_out());
}
Tag::Link { dest_url, .. } => {
self.link = Some(self.links.len());
self.links.push(RenderedLink {
destination: dest_url.to_string(),
label: String::new(),
text_ranges: Vec::new(),
fragments: Vec::new(),
});
}
Tag::Table(_) => {
self.ensure_blank_line_before_block();
self.in_table = true;
self.table_rows.clear();
}
Tag::TableHead => {
self.table_current_row.clear();
}
Tag::TableRow => {
self.table_current_row.clear();
}
Tag::TableCell => {
self.table_cell_spans.clear();
}
Tag::HtmlBlock
| Tag::Image { .. }
| Tag::FootnoteDefinition(_)
| Tag::MetadataBlock(_)
| Tag::DefinitionList
| Tag::DefinitionListTitle
| Tag::DefinitionListDefinition
| Tag::Superscript
| Tag::Subscript => {}
}
}
fn end_tag(&mut self, tag: TagEnd) {
match tag {
TagEnd::Paragraph => {
if self.in_table {
return;
}
self.flush_line();
self.needs_newline = true;
}
TagEnd::Heading(_) => {
self.inline_styles.pop();
self.flush_line();
self.in_heading = None;
self.needs_newline = true;
}
TagEnd::BlockQuote(_) => {
self.flush_line();
self.blockquote_depth = self.blockquote_depth.saturating_sub(1);
self.needs_newline = true;
}
TagEnd::CodeBlock => {
self.flush_code_block();
self.in_code_block = false;
self.code_block_lang = None;
self.needs_newline = true;
}
TagEnd::List(_) => {
self.flush_line();
self.list_indices.pop();
self.list_depth = self.list_depth.saturating_sub(1);
if self.list_depth == 0 {
self.needs_newline = true;
}
}
TagEnd::Item => {
self.flush_line();
if let Some(Some(idx)) = self.list_indices.last_mut() {
*idx += 1;
}
}
TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough => {
self.inline_styles.pop();
}
TagEnd::Link => {
if let Some(index) = self.link.take() {
let link = &self.links[index];
if (self.link_display)(&link.destination) == LinkDisplay::LabelAndDestination
&& link.label != link.destination
{
self.push_span(Span::styled(
format!(" ({})", link.destination),
self.styles.link,
));
}
}
}
TagEnd::Table => {
self.render_table();
self.in_table = false;
self.needs_newline = true;
}
TagEnd::TableHead => {
let row = std::mem::take(&mut self.table_current_row);
self.table_rows.insert(0, row); }
TagEnd::TableRow => {
let row = std::mem::take(&mut self.table_current_row);
self.table_rows.push(row);
}
TagEnd::TableCell => {
let spans = std::mem::take(&mut self.table_cell_spans);
self.table_current_row.push(spans);
}
TagEnd::HtmlBlock
| TagEnd::Image
| TagEnd::FootnoteDefinition
| TagEnd::MetadataBlock(_)
| TagEnd::DefinitionList
| TagEnd::DefinitionListTitle
| TagEnd::DefinitionListDefinition
| TagEnd::Superscript
| TagEnd::Subscript => {}
}
}
fn on_text(&mut self, text: CowStr<'a>) {
if self.in_table {
let style = self.current_style();
self.push_span(Span::styled(text.to_string(), style));
return;
}
if self.in_code_block {
self.code_block_buffer.push_str(&text);
return;
}
self.emit_prefix_if_needed();
let style = self.current_style();
self.push_span(Span::styled(text.to_string(), style));
}
fn on_inline_code(&mut self, code: CowStr<'a>) {
if !self.in_table {
self.emit_prefix_if_needed();
}
self.push_span(Span::styled(
code.to_string(),
self.current_style().patch(self.styles.code),
));
}
fn on_soft_break(&mut self) {
if self.in_table {
self.push_span(Span::styled(" ", self.current_style()));
return;
}
self.push_span(Span::styled(" ", self.current_style()));
}
fn on_hard_break(&mut self) {
if let Some(index) = self.link {
self.links[index].label.push('\n');
}
self.flush_line();
}
fn on_html(&mut self, html: CowStr<'a>) {
for line in html.lines() {
if !line.trim().is_empty() {
self.emit_prefix_if_needed();
self.push_span(Span::raw(line.to_string()));
self.flush_line();
}
}
}
fn on_inline_html(&mut self, html: CowStr<'a>) {
let trimmed = html.trim();
if trimmed.eq_ignore_ascii_case("<br>")
|| trimmed.eq_ignore_ascii_case("<br/>")
|| trimmed.eq_ignore_ascii_case("<br />")
{
if self.in_table {
self.push_span(Span::raw("\n"));
} else {
self.on_hard_break();
}
return;
}
self.on_text(html);
}
fn on_rule(&mut self) {
self.flush_line();
if self.has_output {
self.push_blank_line();
}
self.push_unwrapped_line(Line::from("———"));
self.has_output = true;
self.needs_newline = true;
}
fn current_style(&self) -> Style {
let mut s = if self.link.is_some() {
self.styles.link
} else {
Style::default()
};
for sty in &self.inline_styles {
s = s.patch(*sty);
}
s
}
fn push_span(&mut self, span: Span<'static>) {
if let Some(index) = self.link {
self.links[index].label.push_str(&span.content);
}
let span = InlineSpan {
span,
link: self.link,
};
if self.in_table {
self.table_cell_spans.push(span);
} else {
self.current_spans.push(span);
}
}
fn push_prefix(&mut self, span: Span<'static>) {
self.current_spans.push(InlineSpan { span, link: None });
}
fn emit_prefix_if_needed(&mut self) {
if !self.current_spans.is_empty() {
return;
}
for _ in 0..self.blockquote_depth {
self.push_prefix(Span::styled("> ", self.styles.blockquote_prefix));
}
if self.list_depth > 0 {
let indent_levels = self.list_depth.saturating_sub(1);
if indent_levels > 0 {
self.push_prefix(Span::raw(" ".repeat(indent_levels)));
}
if self.pending_marker {
if let Some(maybe_idx) = self.list_indices.last() {
match maybe_idx {
Some(idx) => {
self.push_prefix(Span::raw(format!("{}. ", idx)));
}
None => {
self.push_prefix(Span::raw("- "));
}
}
}
self.pending_marker = false;
} else {
self.push_prefix(Span::raw(" "));
}
}
}
fn flush_line(&mut self) {
if self.current_spans.is_empty() {
return;
}
let spans = std::mem::take(&mut self.current_spans);
let text_index = self.push_search_text(&spans);
let spans = spans.into_iter().map(|span| span.span).collect::<Vec<_>>();
if let Some(width) = self.wrap_width
&& !self.in_code_block
&& !self.in_table
&& self.in_heading.is_none()
{
let indent = self.continuation_indent();
for wrapped in wrap_spans_with_ranges(&spans, width, indent) {
self.text[text_index].fragments.push(TextFragment {
line: self.lines.len(),
source_range: wrapped.source_range,
visual_start: wrapped.indent,
});
self.lines.push(Line::from(wrapped.spans));
}
self.has_output = true;
return;
}
self.push_line_with_text(Line::from(spans), text_index);
self.has_output = true;
}
fn continuation_indent(&self) -> usize {
let mut indent = self.blockquote_depth * 2; if self.list_depth > 0 {
indent += self.list_depth.saturating_sub(1) * 2;
if let Some(Some(idx)) = self.list_indices.last() {
let digits = if *idx == 0 {
1
} else {
(*idx).ilog10() as usize + 1
};
indent += digits + 2; } else {
indent += 2; }
}
indent
}
fn flush_code_line(&mut self) {
let spans = std::mem::take(&mut self.current_spans);
let text_index = self.push_search_text(&spans);
let spans = spans.into_iter().map(|span| span.span).collect::<Vec<_>>();
let line = Line::from(spans).style(Style::new().bg(self.styles.code_block_bg));
self.push_line_with_text(line, text_index);
self.has_output = true;
}
fn push_blank_line(&mut self) {
self.lines.push(Line::from(""));
}
fn push_search_text(&mut self, spans: &[InlineSpan]) -> usize {
let index = self.text.len();
let mut text = String::new();
for span in spans {
let start = text.len();
text.push_str(&span.span.content);
if let Some(link) = span.link {
let ranges = &mut self.links[link].text_ranges;
if let Some(last) = ranges.last_mut()
&& last.text_index == index
&& last.range.end == start
{
last.range.end = text.len();
} else {
ranges.push(LinkTextRange {
text_index: index,
range: start..text.len(),
});
}
}
}
self.text.push(RenderedText {
text,
fragments: Vec::new(),
});
index
}
fn push_unwrapped_line(&mut self, line: Line<'static>) {
let spans = line
.spans
.iter()
.cloned()
.map(|span| InlineSpan { span, link: None })
.collect::<Vec<_>>();
let index = self.push_search_text(&spans);
self.push_line_with_text(line, index);
}
fn push_line_with_text(&mut self, line: Line<'static>, index: usize) {
let len = self.text[index].text.len();
self.text[index].fragments.push(TextFragment {
line: self.lines.len(),
source_range: 0..len,
visual_start: 0,
});
self.lines.push(line);
}
fn ensure_blank_line_before_block(&mut self) {
if self.needs_newline && self.has_output {
self.flush_line();
self.push_blank_line();
self.needs_newline = false;
}
}
fn flush_code_block(&mut self) {
let buffer = std::mem::take(&mut self.code_block_buffer);
let source = buffer.strip_suffix('\n').unwrap_or(&buffer);
let use_r_highlight = self.code_block_lang.as_deref().is_some_and(is_r_language);
if use_r_highlight {
let tokens = tokenize_r(source);
self.emit_prefix_if_needed();
for token in &tokens {
debug_assert!(
token.start <= source.len() && token.end <= source.len(),
"token [{}, {}) out of bounds for source len {}",
token.start,
token.end,
source.len()
);
let text = &source[token.start..token.end];
let style = token_type_to_style(token.token_type);
let parts: Vec<&str> = text.split('\n').collect();
for (i, part) in parts.iter().enumerate() {
if i > 0 {
self.flush_code_line();
self.emit_prefix_if_needed();
}
if !part.is_empty() {
self.push_span(Span::styled(part.to_string(), style));
}
}
}
self.flush_code_line();
} else {
for line_text in source.split('\n') {
self.emit_prefix_if_needed();
self.push_span(Span::styled(line_text.to_string(), self.styles.code_block));
self.flush_code_line();
}
}
}
fn split_cell_lines(cell: &[InlineSpan]) -> Vec<Vec<InlineSpan>> {
let mut lines: Vec<Vec<InlineSpan>> = vec![Vec::new()];
for span in cell {
if span.span.content.as_ref() == "\n" {
lines.push(Vec::new());
} else {
lines.last_mut().unwrap().push(span.clone());
}
}
lines
}
fn spans_width(spans: &[Span<'static>]) -> usize {
spans
.iter()
.map(|s| UnicodeWidthStr::width(s.content.as_ref()))
.sum()
}
fn render_table(&mut self) {
if self.table_rows.is_empty() {
return;
}
let n_cols = self.table_rows.iter().map(|r| r.len()).max().unwrap_or(0);
if n_cols == 0 {
return;
}
let mut split_rows: Vec<Vec<Vec<TableLine>>> = Vec::new();
for row in std::mem::take(&mut self.table_rows) {
let mut cells = Vec::new();
for cell in row {
let mut cell_lines = Vec::new();
for spans in Self::split_cell_lines(&cell) {
let text_index = self.push_search_text(&spans);
let spans = spans.into_iter().map(|span| span.span).collect();
cell_lines.push(TableLine {
text_index,
wrapped: WrappedLine::unwrapped(spans),
});
}
cells.push(cell_lines);
}
split_rows.push(cells);
}
let mut col_widths = vec![0usize; n_cols];
for row in &split_rows {
for (col_idx, cell_lines) in row.iter().enumerate() {
for sub_line in cell_lines {
let w = Self::spans_width(&sub_line.wrapped.spans);
col_widths[col_idx] = col_widths[col_idx].max(w);
}
}
}
if let Some(wrap_width) = self.wrap_width {
let decoration = self.blockquote_depth * 2 + 2 + n_cols * 3;
let available = wrap_width.saturating_sub(decoration);
let total_natural: usize = col_widths.iter().sum();
if total_natural > available && available > 0 {
let target_widths = distribute_column_widths(&col_widths, available);
for row in &mut split_rows {
for (col_idx, cell_lines) in row.iter_mut().enumerate() {
let target = target_widths[col_idx];
if target == 0 {
continue;
}
let mut new_lines = Vec::new();
for sub_line in cell_lines.drain(..) {
for wrapped in
wrap_spans_with_ranges(&sub_line.wrapped.spans, target, 0)
{
new_lines.push(TableLine {
text_index: sub_line.text_index,
wrapped,
});
}
}
*cell_lines = new_lines;
}
}
col_widths = target_widths;
}
}
for (row_idx, row) in split_rows.iter().enumerate() {
let max_sub_lines = row.iter().map(|cell| cell.len()).max().unwrap_or(1);
for sub_line_idx in 0..max_sub_lines {
let mut spans: Vec<Span<'static>> = Vec::new();
for _ in 0..self.blockquote_depth {
spans.push(Span::styled("> ", self.styles.blockquote_prefix));
}
spans.push(Span::raw("| "));
for (col_idx, cell_lines) in row.iter().enumerate() {
let target = col_widths.get(col_idx).copied().unwrap_or(0);
if let Some(sub_line) = cell_lines.get(sub_line_idx) {
let w = Self::spans_width(&sub_line.wrapped.spans);
let visual_start =
spans.iter().map(|span| span.content.len()).sum::<usize>();
self.text[sub_line.text_index].fragments.push(TextFragment {
line: self.lines.len(),
source_range: sub_line.wrapped.source_range.clone(),
visual_start: visual_start + sub_line.wrapped.indent,
});
for s in &sub_line.wrapped.spans {
spans.push(s.clone());
}
let pad = target.saturating_sub(w);
if pad > 0 {
spans.push(Span::raw(" ".repeat(pad)));
}
} else {
spans.push(Span::raw(" ".repeat(target)));
}
spans.push(Span::raw(" | "));
}
self.lines.push(Line::from(spans));
self.has_output = true;
}
if row_idx == 0 {
let mut sep_spans: Vec<Span<'static>> = Vec::new();
for _ in 0..self.blockquote_depth {
sep_spans.push(Span::styled("> ", self.styles.blockquote_prefix));
}
sep_spans.push(Span::raw("| "));
for (col_idx, w) in col_widths.iter().enumerate() {
sep_spans.push(Span::raw("-".repeat(*w)));
if col_idx + 1 < n_cols {
sep_spans.push(Span::raw(" | "));
}
}
sep_spans.push(Span::raw(" |"));
self.lines.push(Line::from(sep_spans));
}
}
}
}
fn distribute_column_widths(natural: &[usize], available: usize) -> Vec<usize> {
let n = natural.len();
if n == 0 {
return vec![];
}
if available <= n {
let mut widths = vec![0usize; n];
for w in widths.iter_mut().take(available) {
*w = 1;
}
return widths;
}
let mut result = natural.to_vec();
let mut locked = vec![false; n];
let mut locked_total = 0usize;
let mut unlocked_count = n;
loop {
let remaining = available.saturating_sub(locked_total);
let share = remaining.checked_div(unlocked_count).unwrap_or(0);
let mut changed = false;
for i in 0..n {
if !locked[i] && result[i] <= share {
locked[i] = true;
locked_total += result[i];
unlocked_count -= 1;
changed = true;
}
}
if !changed {
break;
}
}
if unlocked_count > 0 {
let remaining = available.saturating_sub(locked_total);
let share = remaining.checked_div(unlocked_count).unwrap_or(0);
let extra = remaining % unlocked_count;
let mut idx = 0;
for i in 0..n {
if !locked[i] {
result[i] = share + if idx < extra { 1 } else { 0 };
idx += 1;
}
}
}
result
}
#[cfg(test)]
mod tests;