Refactor markdown formatting utilities to avoid building intermediate strings (#29511)

These were nearly always used when using `format!` / `write!` etc, so it
makes sense to not have an intermediate `String`.

Release Notes:

- N/A
This commit is contained in:
Michael Sloan 2025-04-27 13:04:51 -06:00 committed by GitHub
parent 6db974dd32
commit 609c528ceb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 154 additions and 137 deletions

View file

@ -43,7 +43,7 @@ use ui::{
Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, TextSize, Tooltip, prelude::*,
};
use util::ResultExt as _;
use util::markdown::MarkdownString;
use util::markdown::MarkdownCodeBlock;
use workspace::Workspace;
use zed_actions::assistant::OpenRulesLibrary;
@ -882,7 +882,11 @@ impl ActiveThread {
});
rendered.input.update(cx, |this, cx| {
this.replace(
MarkdownString::code_block("json", tool_input).to_string(),
MarkdownCodeBlock {
tag: "json",
text: tool_input,
}
.to_string(),
cx,
);
});

View file

@ -14,7 +14,7 @@ use regex::{Regex, RegexBuilder};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownInlineCode;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CodeSymbolsInput {
@ -102,7 +102,7 @@ impl Tool for CodeSymbolsTool {
match &input.path {
Some(path) => {
let path = MarkdownString::inline_code(path);
let path = MarkdownInlineCode(path);
if page > 1 {
format!("List page {page} of code symbols for {path}")
} else {

View file

@ -11,7 +11,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, path::Path};
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownInlineCode;
/// If the model requests to read a file whose size exceeds this, then
/// the tool will return the file's symbol outline instead of its contents,
@ -82,7 +82,7 @@ impl Tool for ContentsTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ContentsToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownString::inline_code(&input.path);
let path = MarkdownInlineCode(&input.path);
match (input.start, input.end) {
(Some(start), None) => format!("Read {path} (from line {start})"),

View file

@ -10,7 +10,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownInlineCode;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CopyPathToolInput {
@ -63,8 +63,8 @@ impl Tool for CopyPathTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CopyPathToolInput>(input.clone()) {
Ok(input) => {
let src = MarkdownString::inline_code(&input.source_path);
let dest = MarkdownString::inline_code(&input.destination_path);
let src = MarkdownInlineCode(&input.source_path);
let dest = MarkdownInlineCode(&input.destination_path);
format!("Copy {src} to {dest}")
}
Err(_) => "Copy path".to_string(),

View file

@ -10,7 +10,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownInlineCode;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateDirectoryToolInput {
@ -53,10 +53,7 @@ impl Tool for CreateDirectoryTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CreateDirectoryToolInput>(input.clone()) {
Ok(input) => {
format!(
"Create directory {}",
MarkdownString::inline_code(&input.path)
)
format!("Create directory {}", MarkdownInlineCode(&input.path))
}
Err(_) => "Create directory".to_string(),
}

View file

@ -10,7 +10,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownInlineCode;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateFileToolInput {
@ -73,7 +73,7 @@ impl Tool for CreateFileTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownString::inline_code(&input.path);
let path = MarkdownInlineCode(&input.path);
format!("Create file {path}")
}
Err(_) => DEFAULT_UI_TEXT.to_string(),

View file

@ -9,7 +9,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, path::Path, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownInlineCode;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsToolInput {
@ -66,11 +66,11 @@ impl Tool for DiagnosticsTool {
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
.ok()
.and_then(|input| match input.path {
Some(path) if !path.is_empty() => Some(MarkdownString::inline_code(&path)),
Some(path) if !path.is_empty() => Some(path),
_ => None,
})
{
format!("Check diagnostics for {path}")
format!("Check diagnostics for {}", MarkdownInlineCode(&path))
} else {
"Check project diagnostics".to_string()
}

View file

@ -14,7 +14,7 @@ use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownEscaped;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
@ -134,7 +134,7 @@ impl Tool for FetchTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<FetchToolInput>(input.clone()) {
Ok(input) => format!("Fetch {}", MarkdownString::escape(&input.url)),
Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)),
Err(_) => "Fetch URL".to_string(),
}
}

View file

@ -13,7 +13,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, fmt::Write, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownInlineCode;
use util::paths::PathMatcher;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@ -75,7 +75,7 @@ impl Tool for GrepTool {
match serde_json::from_value::<GrepToolInput>(input.clone()) {
Ok(input) => {
let page = input.page();
let regex_str = MarkdownString::inline_code(&input.regex);
let regex_str = MarkdownInlineCode(&input.regex);
let case_info = if input.case_sensitive {
" (case-sensitive)"
} else {

View file

@ -8,7 +8,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, path::Path, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownInlineCode;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ListDirectoryToolInput {
@ -63,7 +63,7 @@ impl Tool for ListDirectoryTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ListDirectoryToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownString::inline_code(&input.path);
let path = MarkdownInlineCode(&input.path);
format!("List the {path} directory's contents")
}
Err(_) => "List directory".to_string(),

View file

@ -8,7 +8,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::Path, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownInlineCode;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct MovePathToolInput {
@ -61,8 +61,8 @@ impl Tool for MovePathTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<MovePathToolInput>(input.clone()) {
Ok(input) => {
let src = MarkdownString::inline_code(&input.source_path);
let dest = MarkdownString::inline_code(&input.destination_path);
let src = MarkdownInlineCode(&input.source_path);
let dest = MarkdownInlineCode(&input.destination_path);
let src_path = Path::new(&input.source_path);
let dest_path = Path::new(&input.destination_path);
@ -71,7 +71,7 @@ impl Tool for MovePathTool {
.and_then(|os_str| os_str.to_os_string().into_string().ok())
{
Some(filename) if src_path.parent() == dest_path.parent() => {
let filename = MarkdownString::inline_code(&filename);
let filename = MarkdownInlineCode(&filename);
format!("Rename {src} to {filename}")
}
_ => {

View file

@ -8,7 +8,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownEscaped;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct OpenToolInput {
@ -41,7 +41,7 @@ impl Tool for OpenTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<OpenToolInput>(input.clone()) {
Ok(input) => format!("Open `{}`", MarkdownString::escape(&input.path_or_url)),
Ok(input) => format!("Open `{}`", MarkdownEscaped(&input.path_or_url)),
Err(_) => "Open file or URL".to_string(),
}
}

View file

@ -11,7 +11,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownInlineCode;
/// If the model requests to read a file whose size exceeds this, then
/// the tool will return an error along with the model's symbol outline,
@ -71,7 +71,7 @@ impl Tool for ReadFileTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownString::inline_code(&input.path);
let path = MarkdownInlineCode(&input.path);
match (input.start_line, input.end_line) {
(Some(start), None) => format!("Read file {path} (from line {start})"),
(Some(start), Some(end)) => format!("Read file {path} (lines {start}-{end})"),

View file

@ -8,7 +8,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, ops::Range, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
use util::markdown::MarkdownInlineCode;
use crate::schema::json_schema_for;
@ -91,7 +91,7 @@ impl Tool for SymbolInfoTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<SymbolInfoToolInput>(input.clone()) {
Ok(input) => {
let symbol = MarkdownString::inline_code(&input.symbol);
let symbol = MarkdownInlineCode(&input.symbol);
match input.command {
Info::Definition => {

View file

@ -15,7 +15,7 @@ use std::path::Path;
use std::sync::Arc;
use ui::IconName;
use util::command::new_smol_command;
use util::markdown::MarkdownString;
use util::markdown::MarkdownInlineCode;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct TerminalToolInput {
@ -55,17 +55,14 @@ impl Tool for TerminalTool {
let first_line = lines.next().unwrap_or_default();
let remaining_line_count = lines.count();
match remaining_line_count {
0 => MarkdownString::inline_code(&first_line).0,
1 => {
MarkdownString::inline_code(&format!(
"{} - {} more line",
first_line, remaining_line_count
))
.0
}
n => {
MarkdownString::inline_code(&format!("{} - {} more lines", first_line, n)).0
}
0 => MarkdownInlineCode(&first_line).to_string(),
1 => MarkdownInlineCode(&format!(
"{} - {} more line",
first_line, remaining_line_count
))
.to_string(),
n => MarkdownInlineCode(&format!("{} - {} more lines", first_line, n))
.to_string(),
}
}
Err(_) => "Run terminal command".to_string(),

View file

@ -27,7 +27,7 @@ use std::time::Duration;
use unindent::Unindent as _;
use util::ResultExt as _;
use util::command::new_smol_command;
use util::markdown::MarkdownString;
use util::markdown::MarkdownCodeBlock;
use crate::assertions::{AssertionsReport, RanAssertion, RanAssertionResult};
use crate::example::{Example, ExampleContext, FailedAssertion, JudgeAssertion};
@ -863,7 +863,10 @@ impl RequestMarkdown {
write!(
&mut tools,
"{}\n",
MarkdownString::code_block("json", &format!("{:#}", tool.input_schema))
MarkdownCodeBlock {
tag: "json",
text: &format!("{:#}", tool.input_schema)
}
)
.unwrap();
}
@ -910,7 +913,10 @@ impl RequestMarkdown {
));
messages.push_str(&format!(
"{}\n",
MarkdownString::code_block("json", &format!("{:#}", tool_use.input))
MarkdownCodeBlock {
tag: "json",
text: &format!("{:#}", tool_use.input)
}
));
}
MessageContent::ToolResult(tool_result) => {
@ -972,7 +978,10 @@ pub fn response_events_to_markdown(
));
response.push_str(&format!(
"{}\n",
MarkdownString::code_block("json", &format!("{:#}", tool_use.input))
MarkdownCodeBlock {
tag: "json",
text: &format!("{:#}", tool_use.input)
}
));
}
Ok(

View file

@ -61,7 +61,7 @@ use serde_json::Value;
use settings::Settings;
use theme::ThemeSettings;
use ui::{IntoElement, Styled, div, prelude::*, v_flex};
use util::markdown::MarkdownString;
use util::markdown::MarkdownEscaped;
use crate::outputs::OutputContent;
@ -170,7 +170,7 @@ impl TableView {
let row_content = schema
.fields
.iter()
.map(|field| MarkdownString::escape(&cell_content(record, &field.name)).0)
.map(|field| MarkdownEscaped(&cell_content(record, &field.name)).to_string())
.collect::<Vec<_>>();
row_content.join(" | ")

View file

@ -13,7 +13,10 @@ use schemars::{
use serde::Deserialize;
use serde_json::Value;
use std::{any::TypeId, fmt::Write, rc::Rc, sync::Arc, sync::LazyLock};
use util::{asset_str, markdown::MarkdownString};
use util::{
asset_str,
markdown::{MarkdownEscaped, MarkdownInlineCode, MarkdownString},
};
use crate::{SettingsAssets, settings_store::parse_json_with_comments};
@ -152,7 +155,7 @@ impl KeymapFile {
match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
KeymapFileLoadResult::Success { key_bindings } => Ok(key_bindings),
KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => Err(anyhow!(
"Error loading built-in keymap \"{asset_path}\": {error_message}"
"Error loading built-in keymap \"{asset_path}\": {error_message}",
)),
KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
"JSON parse error in built-in keymap \"{asset_path}\": {error}"
@ -171,7 +174,7 @@ impl KeymapFile {
error_message,
..
} if key_bindings.is_empty() => Err(anyhow!(
"Error loading built-in keymap \"{asset_path}\": {error_message}"
"Error loading built-in keymap \"{asset_path}\": {error_message}",
)),
KeymapFileLoadResult::Success { key_bindings, .. }
| KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings),
@ -251,7 +254,7 @@ impl KeymapFile {
write!(
section_errors,
"\n\n - Unrecognized fields: {}",
MarkdownString::inline_code(&format!("{:?}", unrecognized_fields.keys()))
MarkdownInlineCode(&format!("{:?}", unrecognized_fields.keys()))
)
.unwrap();
}
@ -280,7 +283,7 @@ impl KeymapFile {
write!(
section_errors,
"\n\n- In binding {}, {indented_err}",
inline_code_string(keystrokes),
MarkdownInlineCode(&format!("\"{}\"", keystrokes))
)
.unwrap();
}
@ -299,16 +302,15 @@ impl KeymapFile {
let mut error_message = "Errors in user keymap file.\n".to_owned();
for (context, section_errors) in errors {
if context.is_empty() {
write!(error_message, "\n\nIn section without context predicate:").unwrap()
let _ = write!(error_message, "\n\nIn section without context predicate:");
} else {
write!(
let _ = write!(
error_message,
"\n\nIn section with {}:",
MarkdownString::inline_code(&format!("context = \"{}\"", context))
)
.unwrap()
MarkdownInlineCode(&format!("context = \"{}\"", context))
);
}
write!(error_message, "{section_errors}").unwrap();
let _ = write!(error_message, "{section_errors}");
}
KeymapFileLoadResult::SomeFailedToLoad {
key_bindings,
@ -330,14 +332,14 @@ impl KeymapFile {
return Err(format!(
"expected two-element array of `[name, input]`. \
Instead found {}.",
MarkdownString::inline_code(&action.0.to_string())
MarkdownInlineCode(&action.0.to_string())
));
}
let serde_json::Value::String(ref name) = items[0] else {
return Err(format!(
"expected two-element array of `[name, input]`, \
but the first element is not a string in {}.",
MarkdownString::inline_code(&action.0.to_string())
MarkdownInlineCode(&action.0.to_string())
));
};
let action_input = items[1].clone();
@ -353,7 +355,7 @@ impl KeymapFile {
return Err(format!(
"expected two-element array of `[name, input]`. \
Instead found {}.",
MarkdownString::inline_code(&action.0.to_string())
MarkdownInlineCode(&action.0.to_string())
));
}
};
@ -363,23 +365,23 @@ impl KeymapFile {
Err(ActionBuildError::NotFound { name }) => {
return Err(format!(
"didn't find an action named {}.",
inline_code_string(&name)
MarkdownInlineCode(&format!("\"{}\"", &name))
));
}
Err(ActionBuildError::BuildError { name, error }) => match action_input_string {
Some(action_input_string) => {
return Err(format!(
"can't build {} action from input value {}: {}",
inline_code_string(&name),
MarkdownString::inline_code(&action_input_string),
MarkdownString::escape(&error.to_string())
MarkdownInlineCode(&format!("\"{}\"", &name)),
MarkdownInlineCode(&action_input_string),
MarkdownEscaped(&error.to_string())
));
}
None => {
return Err(format!(
"can't build {} action - it requires input data via [name, input]: {}",
inline_code_string(&name),
MarkdownString::escape(&error.to_string())
MarkdownInlineCode(&format!("\"{}\"", &name)),
MarkdownEscaped(&error.to_string())
));
}
},
@ -390,7 +392,7 @@ impl KeymapFile {
Err(InvalidKeystrokeError { keystroke }) => {
return Err(format!(
"invalid keystroke {}. {}",
inline_code_string(&keystroke),
MarkdownInlineCode(&format!("\"{}\"", &keystroke)),
KEYSTROKE_PARSE_EXPECTED_MESSAGE
));
}
@ -606,11 +608,6 @@ impl KeymapFile {
}
}
// Double quotes a string and wraps it in backticks for markdown inline code..
fn inline_code_string(text: &str) -> MarkdownString {
MarkdownString::inline_code(&format!("\"{}\"", text))
}
#[cfg(test)]
mod tests {
use crate::KeymapFile;

View file

@ -1,6 +1,6 @@
use std::fmt::{Display, Formatter};
/// Markdown text.
/// Indicates that the wrapped `String` is markdown text.
#[derive(Debug, Clone)]
pub struct MarkdownString(pub String);
@ -10,31 +10,45 @@ impl Display for MarkdownString {
}
}
impl MarkdownString {
/// Escapes markdown special characters in markdown text blocks. Markdown code blocks follow
/// different rules and `MarkdownString::inline_code` or `MarkdownString::code_block` should be
/// used in that case.
///
/// Also escapes the following markdown extensions:
///
/// * `^` for superscripts
/// * `$` for inline math
/// * `~` for strikethrough
///
/// Escape of some characters is unnecessary, because while they are involved in markdown syntax,
/// the other characters involved are escaped:
///
/// * `!`, `]`, `(`, and `)` are used in link syntax, but `[` is escaped so these are parsed as
/// plaintext.
///
/// * `;` is used in HTML entity syntax, but `&` is escaped, so they are parsed as plaintext.
///
/// TODO: There is one escape this doesn't do currently. Period after numbers at the start of the
/// line (`[0-9]*\.`) should also be escaped to avoid it being interpreted as a list item.
pub fn escape(text: &str) -> Self {
let mut chunks = Vec::new();
/// Escapes markdown special characters in markdown text blocks. Markdown code blocks follow
/// different rules and `MarkdownInlineCode` or `MarkdownCodeBlock` should be used in that case.
///
/// Also escapes the following markdown extensions:
///
/// * `^` for superscripts
/// * `$` for inline math
/// * `~` for strikethrough
///
/// Escape of some characters is unnecessary, because while they are involved in markdown syntax,
/// the other characters involved are escaped:
///
/// * `!`, `]`, `(`, and `)` are used in link syntax, but `[` is escaped so these are parsed as
/// plaintext.
///
/// * `;` is used in HTML entity syntax, but `&` is escaped, so they are parsed as plaintext.
///
/// TODO: There is one escape this doesn't do currently. Period after numbers at the start of the
/// line (`[0-9]*\.`) should also be escaped to avoid it being interpreted as a list item.
pub struct MarkdownEscaped<'a>(pub &'a str);
/// Implements `Display` to format markdown inline code (wrapped in backticks), handling code that
/// contains backticks and spaces. All whitespace is treated as a single space character. For text
/// that does not contain whitespace other than ' ', this escaping roundtrips through
/// pulldown-cmark.
///
/// When used in tables, `|` should be escaped like `\|` in the text provided to this function.
pub struct MarkdownInlineCode<'a>(pub &'a str);
/// Implements `Display` to format markdown code blocks, wrapped in 3 or more backticks as needed.
pub struct MarkdownCodeBlock<'a> {
pub tag: &'a str,
pub text: &'a str,
}
impl Display for MarkdownEscaped<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let mut start_of_unescaped = None;
for (ix, c) in text.char_indices() {
for (ix, c) in self.0.char_indices() {
match c {
// Always escaped.
'\\' | '`' | '*' | '_' | '[' | '^' | '$' | '~' | '&' |
@ -45,10 +59,10 @@ impl MarkdownString {
match start_of_unescaped {
None => {}
Some(start_of_unescaped) => {
chunks.push(&text[start_of_unescaped..ix]);
write!(formatter, "{}", &self.0[start_of_unescaped..ix])?;
}
}
chunks.push("\\");
write!(formatter, "\\")?;
// Can include this char in the "unescaped" text since a
// backslash was just emitted.
start_of_unescaped = Some(ix);
@ -59,10 +73,10 @@ impl MarkdownString {
match start_of_unescaped {
None => {}
Some(start_of_unescaped) => {
chunks.push(&text[start_of_unescaped..ix]);
write!(formatter, "{}", &self.0[start_of_unescaped..ix])?;
}
}
chunks.push("&lt;");
write!(formatter, "&lt;")?;
start_of_unescaped = None;
}
// Escaped since `>` is used for blockquotes. `&gt;` is used since Markdown supports
@ -71,10 +85,10 @@ impl MarkdownString {
match start_of_unescaped {
None => {}
Some(start_of_unescaped) => {
chunks.push(&text[start_of_unescaped..ix]);
write!(formatter, "{}", &self.0[start_of_unescaped..ix])?;
}
}
chunks.push("gt;");
write!(formatter, "&gt;")?;
start_of_unescaped = None;
}
_ => {
@ -85,17 +99,14 @@ impl MarkdownString {
}
}
if let Some(start_of_unescaped) = start_of_unescaped {
chunks.push(&text[start_of_unescaped..])
write!(formatter, "{}", &self.0[start_of_unescaped..])?;
}
Self(chunks.concat())
Ok(())
}
}
/// Returns markdown for inline code (wrapped in backticks), handling code that contains backticks
/// and spaces. All whitespace is treated as a single space character. For text that does not
/// contain whitespace other than ' ', this escaping roundtrips through pulldown-cmark.
///
/// When used in tables, `|` should be escaped like `\|` in the text provided to this function.
pub fn inline_code(text: &str) -> Self {
impl Display for MarkdownInlineCode<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
// Apache License 2.0, same as this crate.
//
// Copied from `pulldown-cmark-to-cmark-20.0.0` with modifications:
@ -103,12 +114,11 @@ impl MarkdownString {
// * Handling of all whitespace. pulldown-cmark-to-cmark is anticipating
// `Code` events parsed by pulldown-cmark.
//
// * Direct return of string.
//
// https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L290
let mut all_whitespace = true;
let text = text
let text = self
.0
.chars()
.map(|c| {
if c.is_whitespace() {
@ -123,7 +133,7 @@ impl MarkdownString {
// When inline code has leading and trailing ' ' characters, additional space is needed
// to escape it, unless all characters are space.
if all_whitespace {
Self(format!("`{text}`"))
write!(formatter, "`{text}`")
} else {
// More backticks are needed to delimit the inline code than the maximum number of
// backticks in a consecutive run.
@ -133,14 +143,17 @@ impl MarkdownString {
&[b' ', .., b' '] => " ", // Space needed to escape inner space.
_ => "", // No space needed.
};
Self(format!("{backticks}{space}{text}{space}{backticks}"))
write!(formatter, "{backticks}{space}{text}{space}{backticks}")
}
}
}
/// Returns markdown for code blocks, wrapped in 3 or more backticks as needed.
pub fn code_block(tag: &str, text: &str) -> Self {
impl Display for MarkdownCodeBlock<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let tag = self.tag;
let text = self.text;
let backticks = "`".repeat(3.max(count_max_consecutive_chars(text, '`') + 1));
Self(format!("{backticks}{tag}\n{text}\n{backticks}\n"))
write!(formatter, "{backticks}{tag}\n{text}\n{backticks}\n")
}
}
@ -170,7 +183,7 @@ mod tests {
use super::*;
#[test]
fn test_markdown_string_escape() {
fn test_markdown_escaped() {
let input = r#"
# Heading
@ -221,20 +234,20 @@ mod tests {
HTML entity: \&nbsp;
"#;
assert_eq!(MarkdownString::escape(input).0, expected);
assert_eq!(MarkdownEscaped(input).to_string(), expected);
}
#[test]
fn test_markdown_string_inline_code() {
assert_eq!(MarkdownString::inline_code(" ").0, "` `");
assert_eq!(MarkdownString::inline_code("text").0, "`text`");
assert_eq!(MarkdownString::inline_code("text ").0, "`text `");
assert_eq!(MarkdownString::inline_code(" text ").0, "` text `");
assert_eq!(MarkdownString::inline_code("`").0, "`` ` ``");
assert_eq!(MarkdownString::inline_code("``").0, "``` `` ```");
assert_eq!(MarkdownString::inline_code("`text`").0, "`` `text` ``");
fn test_markdown_inline_code() {
assert_eq!(MarkdownInlineCode(" ").to_string(), "` `");
assert_eq!(MarkdownInlineCode("text").to_string(), "`text`");
assert_eq!(MarkdownInlineCode("text ").to_string(), "`text `");
assert_eq!(MarkdownInlineCode(" text ").to_string(), "` text `");
assert_eq!(MarkdownInlineCode("`").to_string(), "`` ` ``");
assert_eq!(MarkdownInlineCode("``").to_string(), "``` `` ```");
assert_eq!(MarkdownInlineCode("`text`").to_string(), "`` `text` ``");
assert_eq!(
MarkdownString::inline_code("some `text` no leading or trailing backticks").0,
MarkdownInlineCode("some `text` no leading or trailing backticks").to_string(),
"``some `text` no leading or trailing backticks``"
);
}