wdl_doc/
document.rs

1//! Create HTML documentation for WDL documents.
2//!
3//! This module defines the [`Document`] struct, which represents an entire WDL
4//! document's HTML representation (i.e., an index page that links to other
5//! pages).
6//!
7//! See [`crate::task::Task`], [`crate::workflow::Workflow`], and
8//! [`crate::struct::Struct`] for how to render individual tasks, workflows, and
9//! structs.
10
11use std::path::PathBuf;
12use std::rc::Rc;
13
14use maud::Markup;
15use maud::PreEscaped;
16use maud::Render;
17use maud::html;
18use wdl_ast::AstToken;
19use wdl_ast::SupportedVersion;
20use wdl_ast::SyntaxTokenExt;
21use wdl_ast::VersionStatement;
22
23use crate::HTMLPage;
24use crate::Markdown;
25use crate::VersionBadge;
26use crate::docs_tree::Header;
27use crate::docs_tree::PageSections;
28use crate::docs_tree::PageType;
29use crate::runnable::Runnable;
30
31/// Parse the preamble comments of a document using the version statement.
32pub fn parse_preamble_comments(version: &VersionStatement) -> String {
33    let comments = version
34        .keyword()
35        .inner()
36        .preceding_trivia()
37        .map(|t| match t.kind() {
38            wdl_ast::SyntaxKind::Comment => match t.to_string().strip_prefix("## ") {
39                Some(comment) => comment.to_string(),
40                None => "".to_string(),
41            },
42            wdl_ast::SyntaxKind::Whitespace => "".to_string(),
43            _ => {
44                panic!("Unexpected token kind: {:?}", t.kind())
45            }
46        })
47        .collect::<Vec<_>>();
48    comments.join("\n")
49}
50
51/// A WDL document. This is an index page that links to other HTML pages.
52#[derive(Debug)]
53pub(crate) struct Document {
54    /// The name of the document.
55    name: String,
56    /// The [`VersionBadge`] which displays the WDL version of the document.
57    version: VersionBadge,
58    /// The AST node for the version statement.
59    ///
60    /// This is used to fetch to any preamble comments.
61    version_statement: VersionStatement,
62    /// The pages that this document should link to.
63    local_pages: Vec<(PathBuf, Rc<HTMLPage>)>,
64}
65
66impl Document {
67    /// Create a new document.
68    pub(crate) fn new(
69        name: String,
70        version: SupportedVersion,
71        version_statement: VersionStatement,
72        local_pages: Vec<(PathBuf, Rc<HTMLPage>)>,
73    ) -> Self {
74        Self {
75            name,
76            version: VersionBadge::new(version),
77            version_statement,
78            local_pages,
79        }
80    }
81
82    /// Get the name of the document.
83    pub fn name(&self) -> &str {
84        &self.name
85    }
86
87    /// Render the version of the document as a badge.
88    pub fn render_version(&self) -> Markup {
89        self.version.render()
90    }
91
92    /// Get the preamble comments of the document as HTML if there are any.
93    pub fn render_preamble(&self) -> Option<Markup> {
94        let preamble = parse_preamble_comments(&self.version_statement);
95        if preamble.is_empty() {
96            return None;
97        }
98        Some(html! {
99            div class="markdown-body" {
100                (Markdown(&preamble).render())
101            }
102        })
103    }
104
105    /// Render the document as HTML.
106    pub fn render(&self) -> (Markup, PageSections) {
107        let rows = self.local_pages.iter().map(|page| {
108            html! {
109                div class="main__grid-row" x-data="{ description_expanded: false }" {
110                    @match page.1.page_type() {
111                        PageType::Struct(_) => {
112                            div class="main__grid-cell" {
113                                a class="text-brand-pink-400 hover:text-pink-200" href=(page.0.to_string_lossy()) {
114                                    (page.1.name())
115                                }
116                            }
117                            div class="main__grid-cell" { code { "struct" } }
118                            div class="main__grid-cell" { "N/A" }
119                        }
120                        PageType::Task(t) => {
121                            div class="main__grid-cell" {
122                                a class="text-brand-violet-400 hover:text-violet-200" href=(page.0.to_string_lossy()) {
123                                    (page.1.name())
124                                }
125                            }
126                            div class="main__grid-cell" { code { "task" } }
127                            div class="main__grid-cell" {
128                                (t.render_description(true))
129                            }
130                        }
131                        PageType::Workflow(w) => {
132                            div class="main__grid-cell" {
133                                a class="text-brand-emerald-400 hover:text-brand-emerald-200" href=(page.0.to_string_lossy()) {
134                                    (page.1.name())
135                                }
136                            }
137                            div class="main__grid-cell" { code { "workflow" } }
138                            div class="main__grid-cell" {
139                                (w.render_description(true))
140                            }
141                        }
142                        // Index pages should not link to other index pages.
143                        PageType::Index(_) => {
144                            // This should be unreachable
145                            div class="main__grid-cell" { "ERROR" }
146                            div class="main__grid-cell" { "ERROR" }
147                            div class="main__grid-cell" { "ERROR" }
148                        }
149                    }
150                    div x-show="description_expanded" class="main__grid-full-width-cell" {
151                        @match page.1.page_type() {
152                            PageType::Struct(_) => "ERROR"
153                            PageType::Task(t) => {
154                                (t.render_description(false))
155                            }
156                            PageType::Workflow(w) => {
157                                (w.render_description(false))
158                            }
159                            PageType::Index(_) => "ERROR"
160                        }
161                    }
162                }
163            }
164        }.into_string()).collect::<Vec<_>>().join(&html! { div class="main__grid-row-separator" {} }.into_string());
165
166        let markup = html! {
167            div class="main__container" {
168                h1 id="title" class="main__title" { (self.name()) }
169                div class="main__badge-container" {
170                    (self.render_version())
171                }
172                @if let Some(preamble) = self.render_preamble() {
173                    div id="preamble" class="main__section" {
174                        (preamble)
175                    }
176                }
177                div class="main__section" {
178                    h2 id="toc" class="main__section-header" { "Table of Contents" }
179                    div class="main__grid-container" {
180                        div class="main__grid-toc-container" {
181                            div class="main__grid-header-cell" { "Page" }
182                            div class="main__grid-header-cell" { "Type" }
183                            div class="main__grid-header-cell" { "Description" }
184                            div class="main__grid-header-separator" {}
185                            (PreEscaped(rows))
186                        }
187                    }
188                }
189            }
190        };
191
192        let mut headers = PageSections::default();
193        headers.push(Header::Header(
194            "Preamble".to_string(),
195            "preamble".to_string(),
196        ));
197        headers.push(Header::Header(
198            "Table of Contents".to_string(),
199            "toc".to_string(),
200        ));
201
202        (markup, headers)
203    }
204}