1#![allow(missing_docs)]
2
3pub mod clipboard;
15pub mod interact;
16pub mod messages;
17pub mod section_chrome;
18pub mod style;
19pub mod theme;
20pub mod theme_catalog;
21
22mod chrome;
23mod doc;
24mod emit;
25mod lower;
26mod plan;
27pub(crate) mod settings;
28mod text;
29
30use crate::config::ResolvedConfig;
31use crate::core::output::OutputFormat;
32use crate::core::output_model::OutputResult;
33use crate::guide::{GuideSection, GuideSectionKind, GuideView};
34
35pub(crate) use messages::{
36 render_messages_from_settings as render_messages, render_messages_without_config,
37};
38pub(crate) use plan::plan_output;
39pub use settings::RenderBackend;
40#[allow(unused_imports)]
41pub use settings::{
42 GuideDefaultFormat, HelpChromeSettings, HelpLayout, HelpTableChrome, PresentationEffect,
43 RenderProfile, RenderRuntime, RenderRuntimeBuilder, RenderSettings, RenderSettingsBuilder,
44 ResolvedHelpChromeSettings, ResolvedRenderSettings, TableBorderStyle, TableOverflow,
45 UiPresentation, help_layout_from_config, resolve_settings,
46};
47pub(crate) use settings::{build_presentation_defaults_layer, explain_presentation_effect};
48pub use style::{StyleOverrides, StyleToken, ThemeStyler};
49pub(crate) use text::visible_inline_text;
50pub use theme::DEFAULT_THEME_NAME;
51pub use theme_catalog as theme_loader;
52
53#[derive(Debug, Clone, Copy)]
54pub(crate) struct StructuredGuideRenderOptions<'a> {
55 pub(crate) source_guide: Option<&'a GuideView>,
56 pub(crate) layout: HelpLayout,
57 pub(crate) title_prefix: Option<&'a str>,
58 pub(crate) show_footer_rule: Option<bool>,
59}
60
61fn render_output_with_profile(
62 output: &OutputResult,
63 settings: &RenderSettings,
64 profile: RenderProfile,
65) -> String {
66 let plan = plan_output(output, settings, profile);
67 emit::emit_doc(
68 &lower::lower_output(output, &plan),
69 plan.format,
70 &plan.settings,
71 )
72}
73
74pub fn render_rows(rows: &[crate::core::row::Row], settings: &RenderSettings) -> String {
75 render_output(
76 &OutputResult {
77 items: crate::core::output_model::OutputItems::Rows(rows.to_vec()),
78 document: None,
79 meta: Default::default(),
80 },
81 settings,
82 )
83}
84
85pub fn render_output(output: &OutputResult, settings: &RenderSettings) -> String {
105 render_output_with_profile(output, settings, RenderProfile::Normal)
106}
107
108pub fn render_output_for_copy(output: &OutputResult, settings: &RenderSettings) -> String {
109 render_output_with_profile(output, settings, RenderProfile::CopySafe)
110}
111
112pub(crate) fn render_json_value(value: &serde_json::Value, settings: &RenderSettings) -> String {
113 let render_settings = settings.plain_copy_settings();
114 let resolved = resolve_settings(&render_settings, RenderProfile::CopySafe);
115 emit::emit_doc(
116 &doc::Doc {
117 blocks: vec![doc::Block::Json(doc::JsonBlock {
118 text: serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_string()),
119 })],
120 },
121 OutputFormat::Json,
122 &resolved,
123 )
124}
125
126pub(crate) fn render_structured_output(
127 config: &ResolvedConfig,
128 settings: &RenderSettings,
129 output: &OutputResult,
130) -> String {
131 render_structured_output_with_config(output, None, settings, config)
132}
133
134pub(crate) fn copy_output_to_clipboard(
135 output: &OutputResult,
136 settings: &RenderSettings,
137 clipboard: &clipboard::ClipboardService,
138) -> Result<(), clipboard::ClipboardError> {
139 clipboard.copy_text(&render_output_for_copy(output, settings))
140}
141
142#[cfg(test)]
143pub(crate) fn render_structured_output_with_layout(
144 output: &OutputResult,
145 settings: &RenderSettings,
146 layout: HelpLayout,
147) -> String {
148 render_structured_output_with_source_guide(output, None, settings, layout)
149}
150
151pub(crate) fn render_structured_output_with_source_guide(
152 output: &OutputResult,
153 source_guide: Option<&GuideView>,
154 settings: &RenderSettings,
155 layout: HelpLayout,
156) -> String {
157 render_structured_output_with_guide_options(
158 output,
159 settings,
160 StructuredGuideRenderOptions {
161 source_guide,
162 layout,
163 title_prefix: None,
164 show_footer_rule: None,
165 },
166 )
167}
168
169pub(crate) fn render_structured_output_with_config(
170 output: &OutputResult,
171 source_guide: Option<&GuideView>,
172 settings: &RenderSettings,
173 config: &ResolvedConfig,
174) -> String {
175 render_structured_output_with_guide_options(
176 output,
177 settings,
178 StructuredGuideRenderOptions {
179 source_guide,
180 layout: help_layout_from_config(config),
181 title_prefix: None,
182 show_footer_rule: None,
183 },
184 )
185}
186
187pub(crate) fn render_structured_output_with_guide_options(
188 output: &OutputResult,
189 settings: &RenderSettings,
190 options: StructuredGuideRenderOptions<'_>,
191) -> String {
192 let plan = plan_output(output, settings, RenderProfile::Normal);
193 if !matches!(plan.format, OutputFormat::Guide | OutputFormat::Markdown) {
194 return emit::emit_doc(
195 &lower::lower_output(output, &plan),
196 plan.format,
197 &plan.settings,
198 );
199 }
200
201 let Some(guide) = options
202 .source_guide
203 .cloned()
204 .or_else(|| GuideView::try_from_output_result(output))
205 .or_else(|| GuideView::try_from_row_projection(output))
206 else {
207 return render_output(output, settings);
208 };
209
210 let guide = apply_title_prefix(&guide, options.title_prefix);
211 let guide_output = guide.to_output_result();
212 let guide_plan = plan_output(&guide_output, settings, RenderProfile::Normal);
213
214 if matches!(guide_plan.format, OutputFormat::Markdown) {
215 emit::emit_doc(
216 &lower::lower_output(&guide_output, &guide_plan),
217 OutputFormat::Markdown,
218 &guide_plan.settings,
219 )
220 } else {
221 emit::emit_doc(
222 &lower::lower_guide_help_layout(
223 &guide,
224 &guide_plan,
225 options.layout,
226 options
227 .show_footer_rule
228 .unwrap_or_else(|| default_show_footer_rule(options.layout)),
229 ),
230 OutputFormat::Guide,
231 &guide_plan.settings,
232 )
233 }
234}
235
236#[cfg(test)]
237pub(crate) fn render_guide_with_layout(
238 guide: &GuideView,
239 settings: &RenderSettings,
240 layout: HelpLayout,
241) -> String {
242 render_guide_with_layout_with_chrome(
243 guide,
244 settings,
245 layout,
246 default_show_footer_rule(layout),
247 None,
248 )
249}
250
251#[cfg(test)]
252pub(crate) fn render_guide_with_layout_with_chrome(
253 guide: &GuideView,
254 settings: &RenderSettings,
255 layout: HelpLayout,
256 show_footer_rule: bool,
257 title_prefix: Option<&str>,
258) -> String {
259 let output = guide.to_output_result();
260 render_structured_output_with_guide_options(
261 &output,
262 settings,
263 StructuredGuideRenderOptions {
264 source_guide: Some(guide),
265 layout,
266 title_prefix,
267 show_footer_rule: Some(show_footer_rule),
268 },
269 )
270}
271
272fn default_show_footer_rule(layout: HelpLayout) -> bool {
273 matches!(layout, HelpLayout::Full)
274}
275
276fn apply_title_prefix(view: &GuideView, title_prefix: Option<&str>) -> GuideView {
277 let Some(prefix) = title_prefix else {
278 return view.clone();
279 };
280 let mut updated = view.clone();
281 if let Some(first) = updated.sections.first_mut() {
282 first.title = format!("{prefix} · {}", first.title);
283 } else if !updated.usage.is_empty() {
284 updated.sections.insert(
285 0,
286 GuideSection {
287 title: format!("{prefix} · Usage"),
288 kind: GuideSectionKind::Usage,
289 paragraphs: updated.usage.clone(),
290 entries: Vec::new(),
291 data: None,
292 },
293 );
294 updated.usage.clear();
295 }
296 updated
297}
298
299#[cfg(test)]
300mod tests;