wdl_doc/
parameter.rs

1//! Create HTML documentation for WDL parameters.
2
3use std::path::Path;
4
5use maud::Markup;
6use maud::PreEscaped;
7use maud::html;
8use wdl_ast::AstNode;
9use wdl_ast::AstToken;
10use wdl_ast::v1::Decl;
11use wdl_ast::v1::MetadataValue;
12
13use crate::meta::DESCRIPTION_KEY;
14use crate::meta::MaybeSummarized;
15use crate::meta::MetaMap;
16use crate::meta::MetaMapExt;
17use crate::meta::summarize_if_needed;
18
19/// The maximum length of an expression before it is summarized.
20const EXPR_MAX_LENGTH: usize = 80;
21/// The length of an expression when summarized.
22const EXPR_CLIP_LENGTH: usize = 50;
23
24/// A group of inputs.
25#[derive(Debug, Eq, PartialEq)]
26pub(crate) struct Group(pub String);
27
28impl Group {
29    /// Get the display name of the group.
30    pub fn display_name(&self) -> &str {
31        &self.0
32    }
33
34    /// Get the id of the group.
35    pub fn id(&self) -> String {
36        self.0.replace(" ", "-").to_lowercase()
37    }
38}
39
40impl PartialOrd for Group {
41    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
42        Some(self.cmp(other))
43    }
44}
45
46impl Ord for Group {
47    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
48        if self.0 == other.0 {
49            return std::cmp::Ordering::Equal;
50        }
51        if self.0 == "Common" {
52            return std::cmp::Ordering::Less;
53        }
54        if other.0 == "Common" {
55            return std::cmp::Ordering::Greater;
56        }
57        if self.0 == "Resources" {
58            return std::cmp::Ordering::Greater;
59        }
60        if other.0 == "Resources" {
61            return std::cmp::Ordering::Less;
62        }
63        self.0.cmp(&other.0)
64    }
65}
66
67/// Whether a parameter is an input or output.
68#[derive(Debug, Clone, Copy)]
69pub(crate) enum InputOutput {
70    /// An input parameter.
71    Input,
72    /// An output parameter.
73    Output,
74}
75
76/// A parameter (input or output) in a workflow or task.
77#[derive(Debug)]
78pub(crate) struct Parameter {
79    /// The declaration of the parameter.
80    decl: Decl,
81    /// Any meta entries associated with the parameter.
82    meta: MetaMap,
83    /// Whether the parameter is an input or output.
84    io: InputOutput,
85}
86
87impl Parameter {
88    /// Create a new parameter.
89    pub fn new(decl: Decl, meta: Option<MetadataValue>, io: InputOutput) -> Self {
90        let meta = match meta {
91            Some(ref m) => {
92                match m {
93                    MetadataValue::Object(o) => o
94                        .items()
95                        .map(|item| (item.name().text().to_string(), item.value().clone()))
96                        .collect(),
97                    MetadataValue::String(_s) => {
98                        MetaMap::from([(DESCRIPTION_KEY.to_string(), m.clone())])
99                    }
100                    _ => {
101                        // If it's not an object or string, we don't know how to handle it.
102                        MetaMap::default()
103                    }
104                }
105            }
106            None => MetaMap::default(),
107        };
108        Self { decl, meta, io }
109    }
110
111    /// Get the name of the parameter.
112    pub fn name(&self) -> String {
113        self.decl.name().text().to_owned()
114    }
115
116    /// Get the meta of the parameter.
117    pub fn meta(&self) -> &MetaMap {
118        &self.meta
119    }
120
121    /// Get the type of the parameter as a string.
122    pub fn ty(&self) -> String {
123        self.decl.ty().to_string()
124    }
125
126    /// Get the expr of the parameter as HTML.
127    ///
128    /// If `summarize` is `false`, the full expression is rendered in a code
129    /// block with WDL syntax highlighting.
130    pub fn render_expr(&self, summarize: bool) -> Markup {
131        let expr = self
132            .decl
133            .expr()
134            .map(|expr| expr.text().to_string())
135            .unwrap_or("None".to_string());
136        if !summarize {
137            // If we are not summarizing, we need to remove the first
138            // line from the leading whitespace calculation as the first line never
139            // leads with whitespace.
140            let mut lines = expr.lines();
141            let first_line = lines.next().expect("expr should have at least one line");
142
143            let common_indent = lines
144                .clone()
145                .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
146                .min()
147                .unwrap_or(0);
148
149            let remaining_expr = lines
150                .map(|line| line.chars().skip(common_indent).collect::<String>())
151                .collect::<Vec<_>>()
152                .join("\n");
153
154            let full_expr = if remaining_expr.is_empty() {
155                first_line
156            } else {
157                &format!("{first_line}\n{remaining_expr}")
158            };
159
160            return html! {
161                sprocket-code language="wdl" {
162                    (full_expr)
163                }
164            };
165        }
166
167        match summarize_if_needed(expr, EXPR_MAX_LENGTH, EXPR_CLIP_LENGTH) {
168            MaybeSummarized::No(expr) => {
169                html! { code { (expr) } }
170            }
171            MaybeSummarized::Yes(summary) => {
172                html! {
173                    div class="main__summary-container" {
174                        code { (summary) }
175                        "..."
176                        button type="button" class="main__button" x-on:click="expr_expanded = !expr_expanded" {
177                            b x-text="expr_expanded ? 'Hide full expression' : 'Show full expression'" {}
178                        }
179                    }
180                }
181            }
182        }
183    }
184
185    /// Get whether the input parameter is required.
186    ///
187    /// Returns `None` for outputs.
188    pub fn required(&self) -> Option<bool> {
189        match self.io {
190            InputOutput::Input => match self.decl.as_unbound_decl() {
191                Some(d) => Some(!d.ty().is_optional()),
192                _ => Some(false),
193            },
194            InputOutput::Output => None,
195        }
196    }
197
198    /// Get the `group` meta entry of the parameter as a [`Group`], if the meta
199    /// entry exists and is a String.
200    pub fn group(&self) -> Option<Group> {
201        self.meta().get("group").and_then(|value| {
202            if let MetadataValue::String(s) = value {
203                Some(Group(
204                    s.text()
205                        .map(|t| t.text().to_string())
206                        .expect("meta string should not be interpolated"),
207                ))
208            } else {
209                None
210            }
211        })
212    }
213
214    /// Render the description of the parameter.
215    pub fn description(&self, summarize: bool) -> Markup {
216        self.meta().render_description(summarize)
217    }
218
219    /// Render any remaining metadata as HTML.
220    ///
221    /// This will render all metadata key-value pairs except for `description`
222    /// and `group`.
223    pub fn render_remaining_meta(&self, assets: &Path) -> Option<Markup> {
224        self.meta()
225            .render_remaining(&[DESCRIPTION_KEY, "group"], assets)
226    }
227
228    /// Render the parameter as HTML.
229    pub fn render(&self, assets: &Path) -> Markup {
230        let show_expr = self.required() != Some(true);
231        html! {
232            div class="main__grid-row" x-data=(
233                if show_expr { "{ description_expanded: false, expr_expanded: false }" } else { "{ description_expanded: false }" }
234            ) {
235                div class="main__grid-cell" {
236                    code { (self.name()) }
237                }
238                div class="main__grid-cell" {
239                    code { (self.ty()) }
240                }
241                @if show_expr {
242                    div class="main__grid-cell" { (self.render_expr(true)) }
243                }
244                div class="main__grid-cell" {
245                    (self.description(true))
246                }
247                div x-show="description_expanded" class="main__grid-full-width-cell" {
248                    (self.description(false))
249                }
250                @if show_expr {
251                    div x-show="expr_expanded" class="main__grid-full-width-cell" {
252                        (self.render_expr(false))
253                    }
254                }
255            }
256            @if let Some(addl_meta) = self.render_remaining_meta(assets) {
257                div class="main__grid-full-width-cell" x-data="{ addl_meta_expanded: false }" {
258                    div class="main__addl-meta-outer-container" {
259                        button type="button" class="main__button" x-on:click="addl_meta_expanded = !addl_meta_expanded" {
260                            b x-text="addl_meta_expanded ? 'Hide Additional Meta' : 'Show Additional Metadata'" {}
261                        }
262                        div x-show="addl_meta_expanded" class="main__addl-meta-inner-container" {
263                            (addl_meta)
264                        }
265                    }
266                }
267            }
268        }
269    }
270}
271
272/// Render a table for non-required parameters (both inputs and outputs
273/// accepted).
274///
275/// A separate implementation is used for non-required parameters
276/// because they require an extra column for the default value (when inputs)
277/// or expression (when outputs). This may seem like a duplication on its
278/// surface, but because of the way CSS/HTML grids work, this is the most
279/// straightforward way to handle the different shape grids.
280///
281/// The distinction between inputs and outputs is made by checking if the
282/// `required` method returns `None` for any of the provided parameters. If it
283/// does, the parameter is an output (and all other parameters will also be
284/// treated as outputs), and the third column will be labeled "Expression". If
285/// it returns `Some(true)` or `Some(false)` for every parameter, they are all
286/// inputs and the third column will be labeled "Default".
287pub(crate) fn render_non_required_parameters_table<'a, I>(params: I, assets: &Path) -> Markup
288where
289    I: Iterator<Item = &'a Parameter>,
290{
291    let params = params.collect::<Vec<_>>();
292
293    let third_col = if params.iter().any(|p| p.required().is_none()) {
294        // If any parameter is an output, we use "Expression" as the third column
295        // header.
296        "Expression"
297    } else {
298        // If all parameters are inputs, we use "Default" as the third column header.
299        "Default"
300    };
301
302    let rows = params
303        .iter()
304        .map(|param| param.render(assets).into_string())
305        .collect::<Vec<_>>()
306        .join(&html! { div class="main__grid-row-separator" {} }.into_string());
307
308    html! {
309        div class="main__grid-container" {
310            div class="main__grid-non-req-param-container" {
311                div class="main__grid-header-cell" { "Name" }
312                div class="main__grid-header-cell" { "Type" }
313                div class="main__grid-header-cell" { (third_col) }
314                div class="main__grid-header-cell" { "Description" }
315                div class="main__grid-header-separator" {}
316                (PreEscaped(rows))
317            }
318        }
319    }
320}