• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Blightmud / Blightmud / 22756411130

06 Mar 2026 08:59AM UTC coverage: 73.55% (-5.2%) from 78.779%
22756411130

push

github

web-flow
Vendors pulldown-cmark-mdcat as pulldown-cmark-blightmud (#1364)

Due to the crates.io yank of pulldown-cmark-mdcat it has now been
vendored in blightmud source while respecting the original licensing.
The name was changed to pulldown-cmark-blightmud to make clear that we
don't intend to maintain a fork of the original pulldown-cmark-mdcat.

762 of 1831 new or added lines in 20 files covered. (41.62%)

4 existing lines in 2 files now uncovered.

9610 of 13066 relevant lines covered (73.55%)

409.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.05
/crates/pulldown-cmark-blightmud/src/render/write.rs
1
// Copyright 2020 Sebastian Wiesner <sebastian@swsnr.de>
2

3
// This Source Code Form is subject to the terms of the Mozilla Public
4
// License, v. 2.0. If a copy of the MPL was not distributed with this
5
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6

7
use std::cmp::{max, min};
8
use std::io::{Result, Write};
9
use std::iter::zip;
10

11
use anstyle::Style;
12
use pulldown_cmark::{Alignment, CodeBlockKind, HeadingLevel};
13
use syntect::highlighting::HighlightState;
14
use syntect::parsing::{ParseState, ScopeStack};
15
use textwrap::core::{display_width, Word};
16
use textwrap::WordSeparator;
17

18
use crate::references::*;
19
use crate::render::data::{CurrentLine, CurrentTable, LinkReferenceDefinition, TableCell};
20
use crate::render::highlighting::highlighter;
21
use crate::render::state::*;
22
use crate::terminal::capabilities::{MarkCapability, StyleCapability, TerminalCapabilities};
23
use crate::terminal::osc::{clear_link, set_link_url};
24
use crate::terminal::TerminalSize;
25
use crate::theme::CombineStyle;
26
use crate::Theme;
27
use crate::{Environment, Settings};
28

29
pub fn write_indent<W: Write>(writer: &mut W, level: u16) -> Result<()> {
1,442✔
30
    write!(writer, "{}", " ".repeat(level as usize))
1,442✔
31
}
1,442✔
32

33
pub fn write_styled<W: Write, S: AsRef<str>>(
4,104✔
34
    writer: &mut W,
4,104✔
35
    capabilities: &TerminalCapabilities,
4,104✔
36
    style: &Style,
4,104✔
37
    text: S,
4,104✔
38
) -> Result<()> {
4,104✔
39
    match capabilities.style {
4,104✔
NEW
40
        None => write!(writer, "{}", text.as_ref()),
×
41
        Some(StyleCapability::Ansi) => write!(
4,104✔
42
            writer,
4,104✔
43
            "{}{}{}",
44
            style.render(),
4,104✔
45
            text.as_ref(),
4,104✔
46
            style.render_reset()
4,104✔
47
        ),
48
    }
49
}
4,104✔
50

51
fn write_remaining_lines<W: Write>(
302✔
52
    writer: &mut W,
302✔
53
    capabilities: &TerminalCapabilities,
302✔
54
    style: &Style,
302✔
55
    indent: u16,
302✔
56
    mut buffer: String,
302✔
57
    next_lines: &[&[Word]],
302✔
58
    last_line: &[Word],
302✔
59
) -> Result<CurrentLine> {
302✔
60
    // Finish the previous line
61
    writeln!(writer)?;
302✔
62
    write_indent(writer, indent)?;
302✔
63
    // Now write all lines up to the last
64
    for line in next_lines {
302✔
65
        match line.split_last() {
6✔
NEW
66
            None => {
×
NEW
67
                // The line was empty, so there's nothing to do anymore.
×
NEW
68
            }
×
69
            Some((last, heads)) => {
6✔
70
                for word in heads {
81✔
71
                    buffer.push_str(word.word);
81✔
72
                    buffer.push_str(word.whitespace);
81✔
73
                }
81✔
74
                buffer.push_str(last.word);
6✔
75
                write_styled(writer, capabilities, style, &buffer)?;
6✔
76
                writeln!(writer)?;
6✔
77
                write_indent(writer, indent)?;
6✔
78
                buffer.clear();
6✔
79
            }
80
        };
81
    }
82

83
    // Now write the last line and keep track of its width
84
    match last_line.split_last() {
302✔
85
        None => {
86
            // The line was empty, so there's nothing to do anymore.
NEW
87
            Ok(CurrentLine::empty())
×
88
        }
89
        Some((last, heads)) => {
302✔
90
            for word in heads {
923✔
91
                buffer.push_str(word.word);
923✔
92
                buffer.push_str(word.whitespace);
923✔
93
            }
923✔
94
            buffer.push_str(last.word);
302✔
95
            write_styled(writer, capabilities, style, &buffer)?;
302✔
96
            Ok(CurrentLine {
302✔
97
                length: textwrap::core::display_width(&buffer) as u16,
302✔
98
                trailing_space: Some(last.whitespace.to_owned()),
302✔
99
            })
302✔
100
        }
101
    }
102
}
302✔
103

104
pub fn write_styled_and_wrapped<W: Write, S: AsRef<str>>(
3,145✔
105
    writer: &mut W,
3,145✔
106
    capabilities: &TerminalCapabilities,
3,145✔
107
    style: &Style,
3,145✔
108
    max_width: u16,
3,145✔
109
    indent: u16,
3,145✔
110
    current_line: CurrentLine,
3,145✔
111
    text: S,
3,145✔
112
) -> Result<CurrentLine> {
3,145✔
113
    let words = WordSeparator::UnicodeBreakProperties
3,145✔
114
        .find_words(text.as_ref())
3,145✔
115
        .collect::<Vec<_>>();
3,145✔
116
    match words.first() {
3,145✔
117
        // There were no words in the text so we just do nothing.
NEW
118
        None => Ok(current_line),
×
119
        Some(first_word) => {
3,145✔
120
            let current_width = current_line.length
3,145✔
121
                + indent
3,145✔
122
                + current_line
3,145✔
123
                    .trailing_space
3,145✔
124
                    .as_ref()
3,145✔
125
                    .map_or(0, |s| display_width(s.as_ref()) as u16);
3,145✔
126

127
            // If the current line is not empty and we can't even add the first first word of the text to it
128
            // then lets finish the line and start over.  If the current line is empty the word simply doesn't
129
            // fit into the terminal size so we must print it anyway.
130
            if 0 < current_line.length
3,145✔
131
                && max_width < current_width + display_width(first_word) as u16
2,458✔
132
            {
133
                writeln!(writer)?;
107✔
134
                write_indent(writer, indent)?;
107✔
135
                return write_styled_and_wrapped(
107✔
136
                    writer,
107✔
137
                    capabilities,
107✔
138
                    style,
107✔
139
                    max_width,
107✔
140
                    indent,
107✔
141
                    CurrentLine::empty(),
107✔
142
                    text,
107✔
143
                );
144
            }
3,038✔
145

146
            let widths = [
3,038✔
147
                // For the first line we need to subtract the length of the current line, and
3,038✔
148
                // the trailing space we need to add if we add more words to this line
3,038✔
149
                (max_width - current_width.min(max_width)) as f64,
3,038✔
150
                // For remaining lines we only need to account for the indent
3,038✔
151
                (max_width - indent) as f64,
3,038✔
152
            ];
3,038✔
153
            let lines = textwrap::wrap_algorithms::wrap_first_fit(&words, &widths);
3,038✔
154
            match lines.split_first() {
3,038✔
155
                None => {
156
                    // there was nothing to wrap so we continue as before
NEW
157
                    Ok(current_line)
×
158
                }
159
                Some((first_line, tails)) => {
3,038✔
160
                    let mut buffer = String::with_capacity(max_width as usize);
3,038✔
161

162
                    // Finish the current line
163
                    let new_current_line = match first_line.split_last() {
3,038✔
164
                        None => {
165
                            // The first line was empty, so there's nothing to do anymore.
NEW
166
                            current_line
×
167
                        }
168
                        Some((last, heads)) => {
3,038✔
169
                            if let Some(s) = current_line.trailing_space {
3,038✔
170
                                buffer.push_str(&s);
1,725✔
171
                            }
1,725✔
172
                            for word in heads {
8,678✔
173
                                buffer.push_str(word.word);
8,678✔
174
                                buffer.push_str(word.whitespace);
8,678✔
175
                            }
8,678✔
176
                            buffer.push_str(last.word);
3,038✔
177
                            let length =
3,038✔
178
                                current_line.length + textwrap::core::display_width(&buffer) as u16;
3,038✔
179
                            write_styled(writer, capabilities, style, &buffer)?;
3,038✔
180
                            buffer.clear();
3,038✔
181
                            CurrentLine {
3,038✔
182
                                length,
3,038✔
183
                                trailing_space: Some(last.whitespace.to_owned()),
3,038✔
184
                            }
3,038✔
185
                        }
186
                    };
187

188
                    // Now write the rest of the lines
189
                    match tails.split_last() {
3,038✔
190
                        None => {
191
                            // There are no more lines and we're done here.
192
                            //
193
                            // We arrive here when the text fragment we wrapped above was
194
                            // shorter than the max length of the current line, i.e. we're
195
                            // still continuing with the current line.
196
                            Ok(new_current_line)
2,736✔
197
                        }
198
                        Some((last_line, next_lines)) => write_remaining_lines(
302✔
199
                            writer,
302✔
200
                            capabilities,
302✔
201
                            style,
302✔
202
                            indent,
302✔
203
                            buffer,
302✔
204
                            next_lines,
302✔
205
                            last_line,
302✔
206
                        ),
207
                    }
208
                }
209
            }
210
        }
211
    }
212
}
3,145✔
213

214
pub fn write_mark<W: Write>(writer: &mut W, capabilities: &TerminalCapabilities) -> Result<()> {
431✔
215
    if let Some(mark) = capabilities.marks {
431✔
NEW
216
        match mark {
×
NEW
217
            MarkCapability::ITerm2(marks) => marks.set_mark(writer),
×
218
        }
219
    } else {
220
        Ok(())
431✔
221
    }
222
}
431✔
223

224
pub fn write_rule<W: Write>(
3✔
225
    writer: &mut W,
3✔
226
    capabilities: &TerminalCapabilities,
3✔
227
    theme: &Theme,
3✔
228
    length: u16,
3✔
229
) -> std::io::Result<()> {
3✔
230
    let rule = "\u{2550}".repeat(length as usize);
3✔
231
    write_styled(
3✔
232
        writer,
3✔
233
        capabilities,
3✔
234
        &Style::new().fg_color(Some(theme.rule_color)),
3✔
235
        rule,
3✔
236
    )
237
}
3✔
238

239
pub fn write_code_block_border<W: Write>(
100✔
240
    writer: &mut W,
100✔
241
    theme: &Theme,
100✔
242
    capabilities: &TerminalCapabilities,
100✔
243
    terminal_size: &TerminalSize,
100✔
244
) -> std::io::Result<()> {
100✔
245
    let separator = "\u{2500}".repeat(terminal_size.columns.min(20) as usize);
100✔
246
    write_styled(
100✔
247
        writer,
100✔
248
        capabilities,
100✔
249
        &Style::new().fg_color(Some(theme.code_block_border_color)),
100✔
250
        separator,
100✔
NEW
251
    )?;
×
252
    writeln!(writer)
100✔
253
}
100✔
254

255
pub fn write_link_refs<W: Write>(
513✔
256
    writer: &mut W,
513✔
257
    environment: &Environment,
513✔
258
    capabilities: &TerminalCapabilities,
513✔
259
    links: Vec<LinkReferenceDefinition>,
513✔
260
) -> Result<()> {
513✔
261
    if !links.is_empty() {
513✔
NEW
262
        writeln!(writer)?;
×
NEW
263
        for link in links {
×
NEW
264
            write_styled(
×
NEW
265
                writer,
×
NEW
266
                capabilities,
×
NEW
267
                &link.style,
×
NEW
268
                format!("[{}]: ", link.index),
×
NEW
269
            )?;
×
270

271
            // If we can resolve the link try to write it as inline link to make the URL
272
            // clickable.  This mostly helps images inside inline links which we had to write as
273
            // reference links because we can't nest inline links.
NEW
274
            if let Some(url) = environment.resolve_reference(&link.target) {
×
NEW
275
                match &capabilities.style {
×
276
                    Some(StyleCapability::Ansi) => {
NEW
277
                        set_link_url(writer, url, &environment.hostname)?;
×
NEW
278
                        write_styled(writer, capabilities, &link.style, link.target)?;
×
NEW
279
                        clear_link(writer)?;
×
280
                    }
NEW
281
                    None => write_styled(writer, capabilities, &link.style, link.target)?,
×
282
                };
283
            } else {
NEW
284
                write_styled(writer, capabilities, &link.style, link.target)?;
×
285
            }
286

NEW
287
            if !link.title.is_empty() {
×
NEW
288
                write_styled(
×
NEW
289
                    writer,
×
NEW
290
                    capabilities,
×
NEW
291
                    &link.style,
×
NEW
292
                    format!(" {}", link.title),
×
NEW
293
                )?;
×
NEW
294
            }
×
NEW
295
            writeln!(writer)?;
×
296
        }
297
    };
513✔
298
    Ok(())
513✔
299
}
513✔
300

301
pub fn write_start_code_block<W: Write>(
50✔
302
    writer: &mut W,
50✔
303
    settings: &Settings,
50✔
304
    indent: u16,
50✔
305
    style: Style,
50✔
306
    block_kind: CodeBlockKind<'_>,
50✔
307
) -> Result<StackedState> {
50✔
308
    write_indent(writer, indent)?;
50✔
309
    write_code_block_border(
50✔
310
        writer,
50✔
311
        &settings.theme,
50✔
312
        &settings.terminal_capabilities,
50✔
313
        &settings.terminal_size,
50✔
NEW
314
    )?;
×
315
    // And start the indent for the contents of the block
316
    write_indent(writer, indent)?;
50✔
317

318
    match (&settings.terminal_capabilities.style, block_kind) {
50✔
319
        (Some(StyleCapability::Ansi), CodeBlockKind::Fenced(name)) if !name.is_empty() => {
50✔
320
            match settings.syntax_set.find_syntax_by_token(&name) {
49✔
NEW
321
                None => Ok(LiteralBlockAttrs {
×
NEW
322
                    indent,
×
NEW
323
                    style: settings.theme.code_style.on_top_of(&style),
×
NEW
324
                }
×
NEW
325
                .into()),
×
326
                Some(syntax) => {
49✔
327
                    let parse_state = ParseState::new(syntax);
49✔
328
                    let highlight_state = HighlightState::new(highlighter(), ScopeStack::new());
49✔
329
                    Ok(HighlightBlockAttrs {
49✔
330
                        indent,
49✔
331
                        highlight_state,
49✔
332
                        parse_state,
49✔
333
                    }
49✔
334
                    .into())
49✔
335
                }
336
            }
337
        }
338
        (_, _) => Ok(LiteralBlockAttrs {
1✔
339
            indent,
1✔
340
            style: settings.theme.code_style.on_top_of(&style),
1✔
341
        }
1✔
342
        .into()),
1✔
343
    }
344
}
50✔
345

346
pub fn write_start_heading<W: Write>(
431✔
347
    writer: &mut W,
431✔
348
    capabilities: &TerminalCapabilities,
431✔
349
    style: Style,
431✔
350
    level: HeadingLevel,
431✔
351
) -> Result<StackedState> {
431✔
352
    write_styled(
431✔
353
        writer,
431✔
354
        capabilities,
431✔
355
        &style,
431✔
356
        "\u{2504}".repeat(level as usize),
431✔
NEW
357
    )?;
×
358

359
    // Headlines never wrap, so indent doesn't matter
360
    Ok(StackedState::Inline(
431✔
361
        InlineState::InlineBlock,
431✔
362
        InlineAttrs { style, indent: 0 },
431✔
363
    ))
431✔
364
}
431✔
365

NEW
366
fn calculate_column_widths(table: &CurrentTable) -> Option<Vec<usize>> {
×
NEW
367
    let first_row = table.head.as_ref().or(table.rows.first())?;
×
NEW
368
    let mut widths = vec![0; first_row.cells.len()];
×
NEW
369
    let rows = table.head.iter().chain(table.rows.as_slice());
×
NEW
370
    for row in rows {
×
NEW
371
        let current = row.cells.as_slice().iter().map(|cell| {
×
NEW
372
            cell.fragments
×
NEW
373
                .as_slice()
×
NEW
374
                .iter()
×
NEW
375
                .fold(0, |acc, x| acc + x.len())
×
NEW
376
        });
×
NEW
377
        widths = zip(widths, current).map(|(a, b)| max(a, b)).collect();
×
378
    }
NEW
379
    Some(widths)
×
NEW
380
}
×
381

382
// TODO: Support themes for table rule.
NEW
383
fn write_table_rule<W: Write>(
×
NEW
384
    writer: &mut W,
×
NEW
385
    capabilities: &TerminalCapabilities,
×
NEW
386
    length: u16,
×
NEW
387
) -> Result<()> {
×
NEW
388
    let rule = "\u{2500}".repeat(length.into());
×
NEW
389
    write_styled(writer, capabilities, &Style::new(), rule)?;
×
NEW
390
    writeln!(writer)
×
NEW
391
}
×
392

NEW
393
fn format_table_cell(cell: TableCell, width: usize, alignment: Alignment) -> String {
×
394
    use Alignment::*;
NEW
395
    let content = cell.fragments.join("");
×
NEW
396
    match alignment {
×
NEW
397
        Left | None => format!(" {:<width$} ", content),
×
NEW
398
        Center => format!(" {:^width$} ", content),
×
NEW
399
        Right => format!(" {:>width$} ", content),
×
400
    }
NEW
401
}
×
402

NEW
403
pub fn write_table<W: Write>(
×
NEW
404
    writer: &mut W,
×
NEW
405
    capabilities: &TerminalCapabilities,
×
NEW
406
    terminal_size: &TerminalSize,
×
NEW
407
    table: CurrentTable,
×
NEW
408
) -> Result<()> {
×
NEW
409
    if let Some(widths) = calculate_column_widths(&table) {
×
410
        // Calculate length of the table rule.
NEW
411
        let total_width: usize = widths.iter().sum();
×
NEW
412
        let rule_length = min(
×
413
            // We use two spaces for padding for each cell in format_table_cell.
NEW
414
            (total_width + 2 * widths.len())
×
NEW
415
                .try_into()
×
NEW
416
                .unwrap_or(u16::MAX),
×
NEW
417
            terminal_size.columns,
×
418
        );
NEW
419
        write_table_rule(writer, capabilities, rule_length)?;
×
420

421
        // Write the table head in bold if any.
NEW
422
        if let Some(head) = table.head {
×
NEW
423
            for ((cell, &width), &alignment) in zip(zip(head.cells, &widths), &table.alignments) {
×
NEW
424
                write_styled(
×
NEW
425
                    writer,
×
NEW
426
                    capabilities,
×
NEW
427
                    &Style::new().bold(),
×
NEW
428
                    format_table_cell(cell, width, alignment),
×
NEW
429
                )?;
×
430
            }
NEW
431
            writeln!(writer)?;
×
NEW
432
            write_table_rule(writer, capabilities, rule_length)?;
×
NEW
433
        }
×
434

435
        // Write table body.
NEW
436
        for row in table.rows {
×
NEW
437
            for ((cell, &width), &alignment) in zip(zip(row.cells, &widths), &table.alignments) {
×
NEW
438
                write_styled(
×
NEW
439
                    writer,
×
NEW
440
                    capabilities,
×
NEW
441
                    &Style::new(),
×
NEW
442
                    format_table_cell(cell, width, alignment),
×
NEW
443
                )?;
×
444
            }
NEW
445
            writeln!(writer)?;
×
446
        }
NEW
447
        write_table_rule(writer, capabilities, rule_length)?;
×
NEW
448
    }
×
449
    // Do nothing when there are no rows in the table, which should be impossible.
NEW
450
    Ok(())
×
NEW
451
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc