Fork of daniellemaywood.uk/gleam — Wasm codegen work
24 kB
779 lines
1mod printer;
2mod source_links;
3#[cfg(test)]
4mod tests;
5
6use std::{collections::HashMap, time::SystemTime};
7
8use camino::Utf8PathBuf;
9use hexpm::version::Version;
10use printer::Printer;
11
12use crate::{
13 build::{Module, Package},
14 config::{DocsPage, PackageConfig},
15 docs::source_links::SourceLinker,
16 io::{Content, FileSystemReader, OutputFile},
17 package_interface::PackageInterface,
18 paths::ProjectPaths,
19 type_::{self},
20 version::COMPILER_VERSION,
21};
22use askama::Template;
23use ecow::EcoString;
24use itertools::Itertools;
25use serde::{Deserialize, Serialize};
26use serde_json::to_string as serde_to_string;
27
28#[derive(PartialEq, Eq, Copy, Clone, Debug)]
29pub enum DocContext {
30 HexPublish,
31 Build,
32}
33
34#[derive(PartialEq, Debug, Serialize, Deserialize)]
35pub struct PackageInformation {
36 #[serde(rename = "gleam.toml")]
37 package_config: PackageConfig,
38}
39
40/// Like `ManifestPackage`, but lighter and cheaper to clone as it is all that
41/// we need for printing documentation.
42#[derive(Debug, Clone)]
43pub struct Dependency {
44 pub version: Version,
45 pub kind: DependencyKind,
46}
47
48#[derive(Debug, Clone, Copy)]
49pub enum DependencyKind {
50 Hex,
51 Path,
52 Git,
53}
54
55#[derive(Debug)]
56pub struct DocumentationConfig<'a> {
57 pub package_config: &'a PackageConfig,
58 pub dependencies: HashMap<EcoString, Dependency>,
59 pub analysed: &'a [Module],
60 pub docs_pages: &'a [DocsPage],
61 pub rendering_timestamp: SystemTime,
62 pub context: DocContext,
63}
64
65pub fn generate_html<IO: FileSystemReader>(
66 paths: &ProjectPaths,
67 config: DocumentationConfig<'_>,
68 fs: IO,
69) -> Vec<OutputFile> {
70 let DocumentationConfig {
71 package_config: config,
72 dependencies,
73 analysed,
74 docs_pages,
75 rendering_timestamp,
76 context: is_hex_publish,
77 } = config;
78
79 let modules = analysed
80 .iter()
81 .filter(|module| module.origin.is_src())
82 .filter(|module| !config.is_internal_module(&module.name));
83
84 let rendering_timestamp = rendering_timestamp
85 .duration_since(SystemTime::UNIX_EPOCH)
86 .expect("get current timestamp")
87 .as_secs()
88 .to_string();
89
90 // Define user-supplied (or README) pages
91 let pages: Vec<_> = docs_pages
92 .iter()
93 .map(|page| Link {
94 name: page.title.to_string(),
95 path: page.path.to_string(),
96 })
97 .collect();
98
99 let doc_links = config.links.iter().map(|doc_link| Link {
100 name: doc_link.title.to_string(),
101 path: doc_link.href.to_string(),
102 });
103
104 let repo_link = config
105 .repository
106 .as_ref()
107 .map(|r| r.url())
108 .map(|path| Link {
109 name: "Repository".into(),
110 path,
111 });
112
113 let host = if is_hex_publish == DocContext::HexPublish {
114 "https://hexdocs.pm"
115 } else {
116 ""
117 };
118
119 // https://github.com/gleam-lang/gleam/issues/3020
120 let links: Vec<_> = match is_hex_publish {
121 DocContext::HexPublish => doc_links
122 .chain(repo_link)
123 .chain([Link {
124 name: "Hex".into(),
125 path: format!("https://hex.pm/packages/{0}", config.name).to_string(),
126 }])
127 .collect(),
128 DocContext::Build => doc_links.chain(repo_link).collect(),
129 };
130
131 let mut files = vec![];
132
133 let mut search_items = vec![];
134
135 let modules_links: Vec<_> = modules
136 .clone()
137 .map(|m| {
138 let path = [&m.name, ".html"].concat();
139 Link {
140 path,
141 name: m.name.split('/').join("<wbr />/"),
142 }
143 })
144 .sorted()
145 .collect();
146
147 // Generate user-supplied (or README) pages
148 for page in docs_pages {
149 let content = fs.read(&page.source).unwrap_or_default();
150 let rendered_content = render_markdown(&content, MarkdownSource::Standalone);
151 let unnest = page_unnest(&page.path);
152
153 let page_path_without_ext = page.path.split('.').next().unwrap_or("");
154 let page_title = match page_path_without_ext {
155 // The index page, such as README, should not push it's page title
156 "index" => format!("{} · v{}", config.name, config.version),
157 // Other page title's should say so
158 _other => format!("{} · {} · v{}", page.title, config.name, config.version),
159 };
160 let page_meta_description = match page_path_without_ext {
161 "index" => config.description.to_string().clone(),
162 _other => "".to_owned(),
163 };
164 let path = Utf8PathBuf::from(&page.path);
165
166 let temp = PageTemplate {
167 gleam_version: COMPILER_VERSION,
168 links: &links,
169 pages: &pages,
170 modules: &modules_links,
171 project_name: &config.name,
172 page_title: &page_title,
173 page_meta_description: &page_meta_description,
174 file_path: &path.clone(),
175 project_version: &config.version.to_string(),
176 content: rendered_content,
177 rendering_timestamp: &rendering_timestamp,
178 host,
179 unnest: &unnest,
180 };
181
182 files.push(OutputFile {
183 path,
184 content: Content::Text(temp.render().expect("Page template rendering")),
185 });
186
187 search_items.push(SearchItem {
188 type_: SearchItemType::Page,
189 parent_title: config.name.to_string(),
190 title: config.name.to_string(),
191 content: escape_html_content(content),
192 reference: page.path.to_string(),
193 })
194 }
195
196 // Generate module documentation pages
197 for module in modules {
198 let name = module.name.clone();
199 let unnest = page_unnest(&module.name);
200
201 // Read module src & create line number lookup structure
202 let source_links = SourceLinker::new(paths, config, module);
203
204 let documentation_content = module.ast.documentation.iter().join("\n");
205 let rendered_documentation =
206 render_markdown(&documentation_content.clone(), MarkdownSource::Comment);
207
208 let mut printer = Printer::new(
209 module.ast.type_info.package.clone(),
210 module.name.clone(),
211 &module.ast.names,
212 &dependencies,
213 );
214
215 let types = printer.type_definitions(&source_links, &module.ast.definitions);
216 let values = printer.value_definitions(&source_links, &module.ast.definitions);
217
218 types.iter().for_each(|type_| {
219 let constructors = type_
220 .constructors
221 .iter()
222 .map(|constructor| {
223 let arguments = constructor
224 .arguments
225 .iter()
226 .map(|argument| {
227 format!("{}\n{}", argument.name, argument.text_documentation)
228 })
229 .join("\n");
230
231 format!(
232 "{}\n{}\n{}",
233 constructor.raw_definition, constructor.text_documentation, arguments
234 )
235 })
236 .join("\n");
237
238 search_items.push(SearchItem {
239 type_: SearchItemType::Type,
240 parent_title: module.name.to_string(),
241 title: type_.name.to_string(),
242 content: format!(
243 "{}\n{}\n{}\n{}",
244 type_.raw_definition,
245 type_.text_documentation,
246 constructors,
247 import_synonyms(&module.name, type_.name)
248 ),
249 reference: format!("{}.html#{}", module.name, type_.name),
250 })
251 });
252 values.iter().for_each(|value| {
253 search_items.push(SearchItem {
254 type_: SearchItemType::Value,
255 parent_title: module.name.to_string(),
256 title: value.name.to_string(),
257 content: format!(
258 "{}\n{}\n{}",
259 value.raw_definition,
260 value.text_documentation,
261 import_synonyms(&module.name, value.name)
262 ),
263 reference: format!("{}.html#{}", module.name, value.name),
264 })
265 });
266
267 search_items.push(SearchItem {
268 type_: SearchItemType::Module,
269 parent_title: module.name.to_string(),
270 title: module.name.to_string(),
271 content: documentation_content,
272 reference: format!("{}.html", module.name),
273 });
274
275 let page_title = format!("{} · {} · v{}", name, config.name, config.version);
276 let page_meta_description = "";
277 let path = Utf8PathBuf::from(format!("{}.html", module.name));
278
279 let template = ModuleTemplate {
280 gleam_version: COMPILER_VERSION,
281 host,
282 unnest,
283 links: &links,
284 pages: &pages,
285 documentation: rendered_documentation,
286 modules: &modules_links,
287 project_name: &config.name,
288 page_title: &page_title,
289 page_meta_description,
290 module_name: EcoString::from(&name),
291 file_path: &path.clone(),
292 project_version: &config.version.to_string(),
293 types,
294 values,
295 rendering_timestamp: &rendering_timestamp,
296 };
297
298 files.push(OutputFile {
299 path,
300 content: Content::Text(
301 template
302 .render()
303 .expect("Module documentation template rendering"),
304 ),
305 });
306 }
307
308 // Render static assets
309
310 files.push(OutputFile {
311 path: Utf8PathBuf::from("css/atom-one-light.min.css"),
312 content: Content::Text(
313 std::include_str!("../templates/docs-css/atom-one-light.min.css").to_string(),
314 ),
315 });
316
317 files.push(OutputFile {
318 path: Utf8PathBuf::from("css/atom-one-dark.min.css"),
319 content: Content::Text(
320 std::include_str!("../templates/docs-css/atom-one-dark.min.css").to_string(),
321 ),
322 });
323
324 files.push(OutputFile {
325 path: Utf8PathBuf::from("css/index.css"),
326 content: Content::Text(std::include_str!("../templates/docs-css/index.css").to_string()),
327 });
328
329 // highlightjs:
330
331 files.push(OutputFile {
332 path: Utf8PathBuf::from("js/highlight.min.js"),
333 content: Content::Text(
334 std::include_str!("../templates/docs-js/highlight.min.js").to_string(),
335 ),
336 });
337
338 files.push(OutputFile {
339 path: Utf8PathBuf::from("js/highlightjs-gleam.js"),
340 content: Content::Text(
341 std::include_str!("../templates/docs-js/highlightjs-gleam.js").to_string(),
342 ),
343 });
344
345 files.push(OutputFile {
346 path: Utf8PathBuf::from("js/highlightjs-erlang.min.js"),
347 content: Content::Text(
348 std::include_str!("../templates/docs-js/highlightjs-erlang.min.js").to_string(),
349 ),
350 });
351
352 files.push(OutputFile {
353 path: Utf8PathBuf::from("js/highlightjs-elixir.min.js"),
354 content: Content::Text(
355 std::include_str!("../templates/docs-js/highlightjs-elixir.min.js").to_string(),
356 ),
357 });
358
359 files.push(OutputFile {
360 path: Utf8PathBuf::from("js/highlightjs-javascript.min.js"),
361 content: Content::Text(
362 std::include_str!("../templates/docs-js/highlightjs-javascript.min.js").to_string(),
363 ),
364 });
365
366 files.push(OutputFile {
367 path: Utf8PathBuf::from("js/highlightjs-typescript.min.js"),
368 content: Content::Text(
369 std::include_str!("../templates/docs-js/highlightjs-typescript.min.js").to_string(),
370 ),
371 });
372
373 // lunr.min.js, search-data.json and index.js
374
375 files.push(OutputFile {
376 path: Utf8PathBuf::from("js/lunr.min.js"),
377 content: Content::Text(std::include_str!("../templates/docs-js/lunr.min.js").to_string()),
378 });
379
380 let search_data_json = serde_to_string(&SearchData {
381 items: search_items,
382 programming_language: SearchProgrammingLanguage::Gleam,
383 })
384 .expect("search index serialization");
385
386 files.push(OutputFile {
387 path: Utf8PathBuf::from("search-data.json"),
388 content: Content::Text(search_data_json.to_string()),
389 });
390
391 files.push(OutputFile {
392 path: Utf8PathBuf::from("js/index.js"),
393 content: Content::Text(std::include_str!("../templates/docs-js/index.js").to_string()),
394 });
395
396 // web fonts:
397
398 files.push(OutputFile {
399 path: Utf8PathBuf::from("fonts/karla-v23-regular-latin-ext.woff2"),
400 content: Content::Binary(
401 include_bytes!("../templates/docs-fonts/karla-v23-regular-latin-ext.woff2").to_vec(),
402 ),
403 });
404
405 files.push(OutputFile {
406 path: Utf8PathBuf::from("fonts/karla-v23-regular-latin.woff2"),
407 content: Content::Binary(
408 include_bytes!("../templates/docs-fonts/karla-v23-regular-latin.woff2").to_vec(),
409 ),
410 });
411
412 files.push(OutputFile {
413 path: Utf8PathBuf::from("fonts/karla-v23-bold-latin-ext.woff2"),
414 content: Content::Binary(
415 include_bytes!("../templates/docs-fonts/karla-v23-bold-latin-ext.woff2").to_vec(),
416 ),
417 });
418
419 files.push(OutputFile {
420 path: Utf8PathBuf::from("fonts/karla-v23-bold-latin.woff2"),
421 content: Content::Binary(
422 include_bytes!("../templates/docs-fonts/karla-v23-bold-latin.woff2").to_vec(),
423 ),
424 });
425
426 files.push(OutputFile {
427 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-cyrillic-ext.woff2"),
428 content: Content::Binary(
429 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-cyrillic-ext.woff2")
430 .to_vec(),
431 ),
432 });
433
434 files.push(OutputFile {
435 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-cyrillic.woff2"),
436 content: Content::Binary(
437 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-cyrillic.woff2")
438 .to_vec(),
439 ),
440 });
441
442 files.push(OutputFile {
443 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-greek-ext.woff2"),
444 content: Content::Binary(
445 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-greek-ext.woff2")
446 .to_vec(),
447 ),
448 });
449
450 files.push(OutputFile {
451 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-greek.woff2"),
452 content: Content::Binary(
453 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-greek.woff2").to_vec(),
454 ),
455 });
456
457 files.push(OutputFile {
458 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-latin-ext.woff2"),
459 content: Content::Binary(
460 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-latin-ext.woff2")
461 .to_vec(),
462 ),
463 });
464
465 files.push(OutputFile {
466 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-latin.woff2"),
467 content: Content::Binary(
468 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-latin.woff2").to_vec(),
469 ),
470 });
471
472 files
473}
474
475pub fn generate_json_package_interface(
476 path: Utf8PathBuf,
477 package: &Package,
478 cached_modules: &im::HashMap<EcoString, type_::ModuleInterface>,
479) -> OutputFile {
480 OutputFile {
481 path,
482 content: Content::Text(
483 serde_json::to_string(&PackageInterface::from_package(package, cached_modules))
484 .expect("JSON module interface serialisation"),
485 ),
486 }
487}
488
489pub fn generate_json_package_information(path: Utf8PathBuf, config: PackageConfig) -> OutputFile {
490 OutputFile {
491 path,
492 content: Content::Text(package_information_as_json(config)),
493 }
494}
495
496fn package_information_as_json(config: PackageConfig) -> String {
497 let info = PackageInformation {
498 package_config: config,
499 };
500 serde_json::to_string_pretty(&info).expect("JSON module information serialisation")
501}
502
503fn page_unnest(path: &str) -> String {
504 let unnest = path
505 .strip_prefix('/')
506 .unwrap_or(path)
507 .split('/')
508 .skip(1)
509 .map(|_| "..")
510 .join("/");
511 if unnest.is_empty() {
512 ".".into()
513 } else {
514 unnest
515 }
516}
517
518#[test]
519fn page_unnest_test() {
520 // Pages
521 assert_eq!(page_unnest("wibble.html"), ".");
522 assert_eq!(page_unnest("/wibble.html"), ".");
523 assert_eq!(page_unnest("/wibble/woo.html"), "..");
524 assert_eq!(page_unnest("/wibble/wobble/woo.html"), "../..");
525
526 // Modules
527 assert_eq!(page_unnest("string"), ".");
528 assert_eq!(page_unnest("gleam/string"), "..");
529 assert_eq!(page_unnest("gleam/string/inspect"), "../..");
530}
531
532fn escape_html_content(it: String) -> String {
533 it.replace('&', "&")
534 .replace('<', "<")
535 .replace('>', ">")
536 .replace('\"', """)
537 .replace('\'', "'")
538}
539
540#[test]
541fn escape_html_content_test() {
542 assert_eq!(
543 escape_html_content("&<>\"'".to_string()),
544 "&<>"'"
545 );
546}
547
548fn import_synonyms(parent: &str, child: &str) -> String {
549 format!("Synonyms:\n{parent}.{child}\n{parent} {child}")
550}
551
552fn text_documentation(doc: &Option<(u32, EcoString)>) -> String {
553 let raw_text = doc
554 .as_ref()
555 .map(|(_, it)| it.to_string())
556 .unwrap_or_else(|| "".into());
557
558 // TODO: parse markdown properly and extract the text nodes
559 raw_text.replace("```gleam", "").replace("```", "")
560}
561
562fn markdown_documentation(doc: &Option<(u32, EcoString)>) -> String {
563 doc.as_ref()
564 .map(|(_, doc)| render_markdown(doc, MarkdownSource::Comment))
565 .unwrap_or_default()
566}
567
568/// An enum to represent the source of a Markdown string to render.
569enum MarkdownSource {
570 /// A Markdown string that comes from the documentation of a
571 /// definition/module. This means that each line is going to be preceded by
572 /// a whitespace.
573 Comment,
574 /// A Markdown string coming from a standalone file like a README.md.
575 Standalone,
576}
577
578fn render_markdown(text: &str, source: MarkdownSource) -> String {
579 let text = match source {
580 MarkdownSource::Standalone => text.into(),
581 // Doc comments start with "///\s", which can confuse the markdown parser
582 // and prevent tables from rendering correctly, so remove that first space.
583 MarkdownSource::Comment => text
584 .split('\n')
585 .map(|s| s.strip_prefix(' ').unwrap_or(s))
586 .join("\n"),
587 };
588
589 let mut s = String::with_capacity(text.len() * 3 / 2);
590 let p = pulldown_cmark::Parser::new_ext(&text, pulldown_cmark::Options::all());
591 pulldown_cmark::html::push_html(&mut s, p);
592 s
593}
594
595#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
596struct Link {
597 name: String,
598 path: String,
599}
600
601#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
602struct TypeConstructor {
603 definition: String,
604 raw_definition: String,
605 documentation: String,
606 text_documentation: String,
607 arguments: Vec<TypeConstructorArg>,
608}
609
610#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
611struct TypeConstructorArg {
612 name: String,
613 doc: String,
614 text_documentation: String,
615}
616
617#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
618struct TypeDefinition<'a> {
619 name: &'a str,
620 definition: String,
621 raw_definition: String,
622 documentation: String,
623 constructors: Vec<TypeConstructor>,
624 text_documentation: String,
625 source_url: String,
626 deprecation_message: String,
627 opaque: bool,
628}
629
630#[derive(PartialEq, Eq, PartialOrd, Ord)]
631struct DocsValues<'a> {
632 name: &'a str,
633 definition: String,
634 raw_definition: String,
635 documentation: String,
636 text_documentation: String,
637 source_url: String,
638 deprecation_message: String,
639}
640
641#[derive(Template)]
642#[template(path = "documentation_page.html")]
643struct PageTemplate<'a> {
644 gleam_version: &'a str,
645 unnest: &'a str,
646 host: &'a str,
647 page_title: &'a str,
648 page_meta_description: &'a str,
649 file_path: &'a Utf8PathBuf,
650 project_name: &'a str,
651 project_version: &'a str,
652 pages: &'a [Link],
653 links: &'a [Link],
654 modules: &'a [Link],
655 content: String,
656 rendering_timestamp: &'a str,
657}
658
659#[derive(Template)]
660#[template(path = "documentation_module.html")]
661struct ModuleTemplate<'a> {
662 gleam_version: &'a str,
663 unnest: String,
664 host: &'a str,
665 page_title: &'a str,
666 page_meta_description: &'a str,
667 file_path: &'a Utf8PathBuf,
668 module_name: EcoString,
669 project_name: &'a str,
670 project_version: &'a str,
671 pages: &'a [Link],
672 links: &'a [Link],
673 modules: &'a [Link],
674 types: Vec<TypeDefinition<'a>>,
675 values: Vec<DocsValues<'a>>,
676 documentation: String,
677 rendering_timestamp: &'a str,
678}
679
680#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
681struct SearchData {
682 items: Vec<SearchItem>,
683 #[serde(rename = "proglang")]
684 programming_language: SearchProgrammingLanguage,
685}
686
687#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
688struct SearchItem {
689 #[serde(rename = "type")]
690 type_: SearchItemType,
691 #[serde(rename = "parentTitle")]
692 parent_title: String,
693 title: String,
694 #[serde(rename = "doc")]
695 content: String,
696 #[serde(rename = "ref")]
697 reference: String,
698}
699
700#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
701#[serde(rename_all = "lowercase")]
702enum SearchItemType {
703 Value,
704 Module,
705 Page,
706 Type,
707}
708
709#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
710#[serde(rename_all = "lowercase")]
711enum SearchProgrammingLanguage {
712 // Elixir,
713 // Erlang,
714 Gleam,
715}
716
717#[test]
718fn package_config_to_json() {
719 let input = r#"
720name = "my_project"
721version = "1.0.0"
722licences = ["Apache-2.0", "MIT"]
723description = "Pretty complex config"
724target = "erlang"
725repository = { type = "github", user = "example", repo = "my_dep" }
726links = [{ title = "Home page", href = "https://example.com" }]
727internal_modules = ["my_app/internal"]
728gleam = ">= 0.30.0"
729
730[dependencies]
731gleam_stdlib = ">= 0.18.0 and < 2.0.0"
732my_other_project = { path = "../my_other_project" }
733
734[dev-dependencies]
735gleeunit = ">= 1.0.0 and < 2.0.0"
736
737[documentation]
738pages = [{ title = "My Page", path = "my-page.html", source = "./path/to/my-page.md" }]
739
740[erlang]
741application_start_module = "my_app/application"
742extra_applications = ["inets", "ssl"]
743
744[javascript]
745typescript_declarations = true
746runtime = "node"
747
748[javascript.deno]
749allow_all = false
750allow_ffi = true
751allow_env = ["DATABASE_URL"]
752allow_net = ["example.com:443"]
753allow_read = ["./database.sqlite"]
754"#;
755
756 let config = toml::from_str::<PackageConfig>(&input).unwrap();
757 let info = PackageInformation {
758 package_config: config.clone(),
759 };
760 let json = package_information_as_json(config);
761 let output = format!("--- GLEAM.TOML\n{input}\n\n--- EXPORTED JSON\n\n{json}");
762 insta::assert_snapshot!(output);
763
764 let roundtrip: PackageInformation = serde_json::from_str(&json).unwrap();
765 assert_eq!(info, roundtrip);
766}
767
768#[test]
769fn barebones_package_config_to_json() {
770 let input = r#"
771name = "my_project"
772version = "1.0.0"
773"#;
774
775 let config = toml::from_str::<PackageConfig>(&input).unwrap();
776 let json = package_information_as_json(config);
777 let output = format!("--- GLEAM.TOML\n{input}\n\n--- EXPORTED JSON\n\n{json}");
778 insta::assert_snapshot!(output);
779}