1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//! Shared building blocks for WDL declaration pages.
use std::path::Path;
use maud::Markup;
use maud::Render;
use maud::html;
/// How a [`DeclarationHero`] title should be rendered.
///
/// WDL identifiers are shown as code literals, while friendly human-facing
/// display names (such as a workflow's `meta.name`) are shown as plain text.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum TitleKind {
/// A raw WDL identifier, wrapped in `<code class="heading-code-literal">`.
#[default]
Identifier,
/// A friendly human display name, rendered as plain text.
Plain,
}
/// A rich header for a WDL declaration page.
///
/// Renders the declaration kind, a sans-serif title, an authored summary,
/// badges, and an optional source card showing the workspace-relative path.
pub(crate) struct DeclarationHero<'a> {
/// The declaration kind label (e.g. `Struct`, `Task`).
kind: &'a str,
/// An optional class applied to the kind label for accent coloring.
kind_class: Option<&'a str>,
/// The Pagefind type filter value (e.g. `struct`), rendered as
/// `type:{value}`.
pagefind_type: Option<&'a str>,
/// The declaration name shown as the title.
name: &'a str,
/// How the title is rendered (code literal vs. plain text).
title_kind: TitleKind,
/// The rendered summary shown beneath the title.
summary: Markup,
/// The workspace-relative path to the source WDL document, if any.
source_path: Option<&'a Path>,
/// Badges rendered alongside the title.
badges: Vec<Markup>,
}
impl<'a> DeclarationHero<'a> {
/// Create a new hero for a declaration of the given `kind` and `name`.
///
/// The `summary` is rendered immediately and may be any [`Render`] value,
/// such as a plain string or previously rendered [`Markup`].
///
/// The title defaults to [`TitleKind::Identifier`], rendering `name` as a
/// code literal; call [`DeclarationHero::title_kind`] to override this for
/// friendly display names.
pub(crate) fn new(kind: &'a str, name: &'a str, summary: impl Render) -> Self {
Self {
kind,
kind_class: None,
pagefind_type: None,
name,
title_kind: TitleKind::default(),
summary: summary.render(),
source_path: None,
badges: Vec::new(),
}
}
/// Set how the title is rendered.
///
/// Use [`TitleKind::Plain`] when `name` is a friendly display name rather
/// than a WDL identifier.
pub(crate) fn title_kind(mut self, kind: TitleKind) -> Self {
self.title_kind = kind;
self
}
/// Set the accent class applied to the kind label.
pub(crate) fn kind_class(mut self, class: &'a str) -> Self {
self.kind_class = Some(class);
self
}
/// Set the Pagefind type filter value for the kind label.
pub(crate) fn pagefind_type(mut self, ty: &'a str) -> Self {
self.pagefind_type = Some(ty);
self
}
/// Set the workspace-relative source path shown in the source card.
pub(crate) fn source_path(mut self, path: &'a Path) -> Self {
self.source_path = Some(path);
self
}
/// Append a badge to be rendered alongside the title.
pub(crate) fn badge(mut self, badge: Markup) -> Self {
self.badges.push(badge);
self
}
/// Render the hero as HTML.
pub(crate) fn render(&self, assets: &Path) -> Markup {
let kind_class = match self.kind_class {
Some(class) => format!("declaration-hero__kind {class}"),
None => "declaration-hero__kind".to_string(),
};
let pagefind_filter = self.pagefind_type.map(|ty| format!("type:{ty}"));
html! {
header class="declaration-hero" {
p class=(kind_class) data-pagefind-filter=[pagefind_filter] {
(self.kind)
}
h1 id="title" data-pagefind-meta="title" class="declaration-hero__title" {
@match self.title_kind {
TitleKind::Identifier => code class="heading-code-literal" { (self.name) },
TitleKind::Plain => (self.name),
}
}
div class="declaration-hero__summary markdown-body" {
(self.summary)
}
@if !self.badges.is_empty() {
div class="main__badge-container" {
@for badge in &self.badges {
(badge)
}
}
}
@if let Some(path) = self.source_path {
div class="source-card" {
span class="source-card__icon" aria-hidden="true" {
img
src=(assets.join("folder.dark.svg").to_string_lossy().replace('\\', "/"))
class="block light:hidden"
alt="";
img
src=(assets.join("folder.light.svg").to_string_lossy().replace('\\', "/"))
class="hidden light:block"
alt="";
}
code class="source-card__path" title=(path.to_string_lossy()) { (path.to_string_lossy()) }
button
type="button"
class="source-card__copy"
aria-label="Copy source path"
x-data="{ copied: false }"
x-on:click=(format!("navigator.clipboard.writeText({path:?}); copied = true; clearTimeout($data._copyTimer); $data._copyTimer = setTimeout(() => copied = false, 3000)"))
x-text="copied ? 'Copied!' : 'Copy'"
{ "Copy" }
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::DeclarationHero;
#[test]
fn declaration_hero_renders_title_and_source_card() {
let html = DeclarationHero::new("Struct", "Scroll", "An enchanted scroll.")
.source_path(Path::new("main.wdl"))
.render(Path::new("assets"))
.into_string();
assert!(html.contains("class=\"declaration-hero__title\""));
// The identifier used as the page title is styled as a code literal
// while remaining inside the h1 (preserving its heading size via CSS).
assert!(html.contains("<code class=\"heading-code-literal\">Scroll</code>"));
assert!(html.contains("source-card__path\" title=\"main.wdl\">main.wdl"));
assert!(html.contains("src=\"assets/folder.dark.svg\""));
assert!(html.contains("src=\"assets/folder.light.svg\""));
assert!(!html.contains(">▱<"));
assert!(html.contains("navigator.clipboard.writeText"));
assert!(!html.contains("<code>Scroll</code>"));
let assets = crate::get_assets();
let icon = std::str::from_utf8(assets.get("folder.dark.svg").expect("bundled folder icon"))
.expect("folder icon is valid UTF-8");
assert!(icon.contains("M3.12508 8.14668"));
}
}