Skip to main content

wowlab_common/markdown/
inline.rs

1use std::fmt::Display;
2
3const SHORT_SHA_LEN: usize = 7;
4
5fn wrap(text: impl Display, prefix: &str, suffix: &str) -> String {
6    format!("{prefix}{text}{suffix}")
7}
8
9/// Wrap text in bold markers (`**text**`).
10pub fn bold(text: impl Display) -> String {
11    wrap(text, "**", "**")
12}
13
14/// Wrap text in italic markers (`*text*`).
15pub fn italic(text: impl Display) -> String {
16    wrap(text, "*", "*")
17}
18
19/// Wrap text in bold and italic markers (`***text***`).
20pub fn bold_italic(text: impl Display) -> String {
21    wrap(text, "***", "***")
22}
23
24/// Wrap text in underline markers (`__text__`).
25pub fn underline(text: impl Display) -> String {
26    wrap(text, "__", "__")
27}
28
29/// Wrap text in strikethrough markers (`~~text~~`).
30pub fn strike(text: impl Display) -> String {
31    wrap(text, "~~", "~~")
32}
33
34/// Wrap text in underline + italic markers (`__*text*__`).
35pub fn underline_italic(text: impl Display) -> String {
36    wrap(text, "__*", "*__")
37}
38
39/// Wrap text in underline + bold markers (`__**text**__`).
40pub fn underline_bold(text: impl Display) -> String {
41    wrap(text, "__**", "**__")
42}
43
44/// Wrap text in underline + bold + italic markers (`__***text***__`).
45pub fn underline_bold_italic(text: impl Display) -> String {
46    wrap(text, "__***", "***__")
47}
48
49/// Wrap text in inline-code backticks (`` `text` ``).
50pub fn code(text: impl Display) -> String {
51    wrap(text, "`", "`")
52}
53
54/// Prefix text with `>>> ` to start a Discord block quote.
55pub fn quote_block(text: impl Display) -> String {
56    wrap(text, ">>> ", "")
57}
58
59/// Render an H1 heading (`# text`).
60pub fn h1(text: impl Display) -> String {
61    wrap(text, "# ", "")
62}
63
64/// Render an H2 heading (`## text`).
65pub fn h2(text: impl Display) -> String {
66    wrap(text, "## ", "")
67}
68
69/// Render an H3 heading (`### text`).
70pub fn h3(text: impl Display) -> String {
71    wrap(text, "### ", "")
72}
73
74/// Render Discord-style subtext (`-# text`).
75pub fn subtext(text: impl Display) -> String {
76    wrap(text, "-# ", "")
77}
78
79/// Wrap text in Discord spoiler markers (`||text||`).
80pub fn spoiler(text: impl Display) -> String {
81    wrap(text, "||", "||")
82}
83
84/// Render a Discord channel mention (`<#id>`).
85pub fn channel(text: impl Display) -> String {
86    wrap(text, "<#", ">")
87}
88
89/// Render a Discord user mention (`<@id>`).
90pub fn user(text: impl Display) -> String {
91    wrap(text, "<@", ">")
92}
93
94/// Render a Discord role mention (`<@&id>`).
95pub fn role(text: impl Display) -> String {
96    wrap(text, "<@&", ">")
97}
98
99/// Render a fenced code block with optional language tag.
100pub fn code_block(language: &str, content: impl Display) -> String {
101    if language.is_empty() {
102        format!("```\n{content}\n```")
103    } else {
104        format!("```{language}\n{content}\n```")
105    }
106}
107
108/// Prefix every line of `text` with `> ` to build a multi-line blockquote.
109pub fn quote(text: impl Display) -> String {
110    let text = text.to_string();
111
112    text.lines()
113        .map(|line| format!("> {line}"))
114        .collect::<Vec<_>>()
115        .join("\n")
116}
117
118/// Render a markdown link (`[text](url)`).
119#[inline]
120pub fn link(text: impl Display, url: impl Display) -> String {
121    format!("[{text}]({url})")
122}
123
124/// Render a markdown link with a hover title (`[text](url "title")`).
125#[inline]
126pub fn link_titled(text: impl Display, url: impl Display, title: impl Display) -> String {
127    format!("[{text}]({url} \"{title}\")")
128}
129
130/// Render an in-page anchor link (`[text](#anchor)`).
131#[inline]
132pub fn anchor_link(text: impl Display, anchor: impl Display) -> String {
133    format!("[{text}](#{anchor})")
134}
135
136/// Render a Discord slash-command mention (`</name:id>`).
137#[inline]
138pub fn slash_command(name: &str, id: impl Display) -> String {
139    format!("</{name}:{id}>")
140}
141
142/// Render a Discord timestamp (`<t:unix>`).
143#[inline]
144#[must_use]
145pub fn timestamp(unix: i64) -> String {
146    format!("<t:{unix}>")
147}
148
149/// Render a styled Discord timestamp (style: t/T time, d/D date, f/F date+time, R relative).
150#[inline]
151#[must_use]
152pub fn timestamp_styled(unix: i64, style: char) -> String {
153    format!("<t:{unix}:{style}>")
154}
155
156/// Render a relative Discord timestamp (`<t:unix:R>`).
157#[inline]
158#[must_use]
159pub fn relative_time(unix: i64) -> String {
160    timestamp_styled(unix, 'R')
161}
162
163/// Render a bold key with a plain value: `**key:** value`.
164#[inline]
165pub fn kv(key: impl Display, value: impl Display) -> String {
166    format!("**{key}:** {value}")
167}
168
169/// Render a bold key with a code-wrapped value: `` **key:** `value` ``.
170#[inline]
171pub fn kv_code(key: impl Display, value: impl Display) -> String {
172    format!("**{key}:** `{value}`")
173}
174
175/// Render addition/deletion counts as code-wrapped labels: `` `+n` `-m` ``.
176#[inline]
177pub fn diff_stats(additions: impl Display, deletions: impl Display) -> String {
178    format!("`+{additions}` `-{deletions}`")
179}
180
181/// Format a git commit hash as a short linked reference.
182#[must_use]
183pub fn commit(sha: &str, message: &str) -> String {
184    // #t(rust_unchecked_indexing) upper bound is clamped to sha.len()
185    let short_sha = &sha[..SHORT_SHA_LEN.min(sha.len())];
186    let first_line = message.lines().next().unwrap_or("");
187
188    format!("`{short_sha}` {first_line}")
189}
190
191/// Render a plain arrow between two values (`from → to`).
192#[inline]
193pub fn arrow(from: impl Display, to: impl Display) -> String {
194    format!("{from} → {to}")
195}
196
197/// Render an arrow between two code-wrapped values (`` `from` → `to` ``).
198#[inline]
199pub fn arrow_code(from: impl Display, to: impl Display) -> String {
200    format!("`{from}` → `{to}`")
201}