Detailed changes
@@ -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,
);
});
@@ -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 {
@@ -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})"),
@@ -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(),
@@ -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(),
}
@@ -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(),
@@ -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()
}
@@ -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(),
}
}
@@ -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 {
@@ -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(),
@@ -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}")
}
_ => {
@@ -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(),
}
}
@@ -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})"),
@@ -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 => {
@@ -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(),
@@ -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(
@@ -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(" | ")
@@ -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;
@@ -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("<");
+ write!(formatter, "<")?;
start_of_unescaped = None;
}
// Escaped since `>` is used for blockquotes. `>` 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, ">")?;
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: \
"#;
- 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``"
);
}