Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

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