1use 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
19const EXPR_MAX_LENGTH: usize = 80;
21const EXPR_CLIP_LENGTH: usize = 50;
23
24#[derive(Debug, Eq, PartialEq)]
26pub(crate) struct Group(pub String);
27
28impl Group {
29 pub fn display_name(&self) -> &str {
31 &self.0
32 }
33
34 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#[derive(Debug, Clone, Copy)]
69pub(crate) enum InputOutput {
70 Input,
72 Output,
74}
75
76#[derive(Debug)]
78pub(crate) struct Parameter {
79 decl: Decl,
81 meta: MetaMap,
83 io: InputOutput,
85}
86
87impl Parameter {
88 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 MetaMap::default()
103 }
104 }
105 }
106 None => MetaMap::default(),
107 };
108 Self { decl, meta, io }
109 }
110
111 pub fn name(&self) -> String {
113 self.decl.name().text().to_owned()
114 }
115
116 pub fn meta(&self) -> &MetaMap {
118 &self.meta
119 }
120
121 pub fn ty(&self) -> String {
123 self.decl.ty().to_string()
124 }
125
126 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 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 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 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 pub fn description(&self, summarize: bool) -> Markup {
216 self.meta().render_description(summarize)
217 }
218
219 pub fn render_remaining_meta(&self, assets: &Path) -> Option<Markup> {
224 self.meta()
225 .render_remaining(&[DESCRIPTION_KEY, "group"], assets)
226 }
227
228 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
272pub(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 "Expression"
297 } else {
298 "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}