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 / printer.rs
31 kB 899 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2025 The Gleam contributors 3 4use std::{ 5 collections::{HashMap, HashSet}, 6 ops::Deref, 7}; 8 9use ecow::{EcoString, eco_format}; 10 11use crate::{ 12 ast::{ 13 ArgNames, CustomType, Function, Publicity, RecordConstructorArg, SrcSpan, TypeAlias, 14 TypedArg, TypedDefinitions, TypedModuleConstant, TypedRecordConstructor, 15 }, 16 type_::{ 17 Deprecation, PRELUDE_MODULE_NAME, PRELUDE_PACKAGE_NAME, Type, TypeVar, 18 printer::{Names, PrintMode}, 19 }, 20}; 21use pretty_arena::*; 22 23use super::{ 24 Dependency, DependencyKind, DocsValues, TypeConstructor, TypeConstructorArg, TypeDefinition, 25 markdown_documentation, source_links::SourceLinker, text_documentation, 26}; 27 28#[derive(Clone, Copy)] 29pub struct PrintOptions { 30 pub print_highlighting: bool, 31 pub print_html: bool, 32} 33 34impl PrintOptions { 35 pub fn all() -> Self { 36 Self { 37 print_highlighting: true, 38 print_html: true, 39 } 40 } 41} 42 43pub struct Printer<'a> { 44 options: PrintOptions, 45 names: &'a Names, 46 47 package: EcoString, 48 module: EcoString, 49 50 /// Type variables which don't have annotated names and we have generated 51 /// names for. 52 printed_type_variables: HashMap<u64, EcoString>, 53 /// Names of type variables that we have generated or printed in a given 54 /// definition. This ensures that we have no duplicate generated names. 55 printed_type_variable_names: HashSet<EcoString>, 56 /// An incrementing number used to generate the next type variable name. 57 /// `0` becomes `a`, `1` becomes `b`, etc. 58 next_type_variable_id: u64, 59 60 dependencies: &'a HashMap<EcoString, Dependency>, 61} 62 63impl<'a, 'doc> Printer<'a> { 64 pub fn new( 65 package: EcoString, 66 module: EcoString, 67 names: &'a Names, 68 dependencies: &'a HashMap<EcoString, Dependency>, 69 ) -> Printer<'a> { 70 Printer { 71 options: PrintOptions::all(), 72 names, 73 package, 74 module, 75 printed_type_variables: HashMap::new(), 76 printed_type_variable_names: HashSet::new(), 77 next_type_variable_id: 0, 78 dependencies, 79 } 80 } 81 82 // This is currently only used in the tests, though it might be useful in 83 // application code in future. If it is needed, simply remove this attribute. 84 #[cfg(test)] 85 pub fn set_options(&mut self, options: PrintOptions) { 86 self.options = options; 87 } 88 89 pub fn type_definitions( 90 &mut self, 91 source_links: &SourceLinker, 92 definitions: &'a TypedDefinitions, 93 ) -> Vec<TypeDefinition<'a>> { 94 let mut type_definitions = vec![]; 95 let arena = DocumentArena::new(); 96 97 for CustomType { 98 location, 99 name, 100 publicity, 101 constructors, 102 documentation, 103 deprecation, 104 opaque, 105 parameters, 106 .. 107 } in &definitions.custom_types 108 { 109 if !publicity.is_public() { 110 continue; 111 } 112 113 type_definitions.push(TypeDefinition { 114 name, 115 definition: print(self.custom_type( 116 &arena, 117 name, 118 parameters, 119 constructors, 120 *opaque, 121 )), 122 raw_definition: self 123 .raw(|this| this.custom_type(&arena, name, parameters, constructors, *opaque)), 124 documentation: markdown_documentation(documentation), 125 text_documentation: text_documentation(documentation), 126 deprecation_message: match deprecation { 127 Deprecation::NotDeprecated => "".to_string(), 128 Deprecation::Deprecated { message } => message.to_string(), 129 }, 130 constructors: if *opaque { 131 Vec::new() 132 } else { 133 constructors 134 .iter() 135 .map(|constructor| TypeConstructor { 136 definition: print(self.record_constructor(&arena, constructor)), 137 raw_definition: self 138 .raw(|this| this.record_constructor(&arena, constructor)), 139 documentation: markdown_documentation(&constructor.documentation), 140 text_documentation: text_documentation(&constructor.documentation), 141 arguments: constructor 142 .arguments 143 .iter() 144 .filter_map(|arg| arg.label.as_ref().map(|(_, label)| (arg, label))) 145 .map(|(argument, label)| TypeConstructorArg { 146 name: label.trim_end().to_string(), 147 doc: markdown_documentation(&argument.doc), 148 text_documentation: text_documentation(&argument.doc), 149 }) 150 .filter(|arg| !arg.doc.is_empty()) 151 .collect(), 152 }) 153 .collect() 154 }, 155 source_url: source_links.url(*location), 156 opaque: *opaque, 157 }); 158 } 159 160 for TypeAlias { 161 location, 162 alias: name, 163 parameters, 164 type_, 165 publicity, 166 documentation, 167 deprecation, 168 .. 169 } in &definitions.type_aliases 170 { 171 if !publicity.is_public() { 172 continue; 173 } 174 type_definitions.push(TypeDefinition { 175 name, 176 definition: print( 177 self.type_alias(&arena, name, type_, parameters) 178 .group(&arena), 179 ), 180 raw_definition: self.raw(|this| { 181 this.type_alias(&arena, name, type_, parameters) 182 .group(&arena) 183 }), 184 documentation: markdown_documentation(documentation), 185 text_documentation: text_documentation(documentation), 186 constructors: vec![], 187 source_url: source_links.url(*location), 188 deprecation_message: match deprecation { 189 Deprecation::NotDeprecated => "".to_string(), 190 Deprecation::Deprecated { message } => message.to_string(), 191 }, 192 opaque: false, 193 }); 194 } 195 196 type_definitions.sort(); 197 type_definitions 198 } 199 200 /// Print a definition without HTML highlighting, such as for search data 201 fn raw<'doc1, 'a1: 'doc1>( 202 &mut self, 203 definition: impl FnOnce(&mut Self) -> Document<'a1, 'doc1>, 204 ) -> String { 205 let options = self.options; 206 // Turn off highlighting for this definition 207 self.options = PrintOptions { 208 print_highlighting: false, 209 print_html: false, 210 }; 211 let result = print(definition(self)); 212 // Restore previous options 213 self.options = options; 214 format!("```\n{result}\n```") 215 } 216 217 pub fn value_definitions( 218 &mut self, 219 source_links: &SourceLinker, 220 definitions: &'a TypedDefinitions, 221 ) -> Vec<DocsValues<'a>> { 222 let mut value_definitions = vec![]; 223 let arena = DocumentArena::new(); 224 225 for Function { 226 location, 227 name, 228 arguments, 229 publicity, 230 deprecation, 231 return_type, 232 documentation, 233 .. 234 } in &definitions.functions 235 { 236 let Some((_, name)) = name else { continue }; 237 if !publicity.is_public() { 238 continue; 239 } 240 241 // Ensure that any type variables we printed in previous definitions don't 242 // affect our printing of this definition. Two type variables in different 243 // definitions can have the same name without clashing. 244 self.printed_type_variable_names.clear(); 245 self.next_type_variable_id = 0; 246 247 value_definitions.push(DocsValues { 248 name, 249 definition: print(self.function_signature(&arena, name, arguments, return_type)), 250 raw_definition: self 251 .raw(|this| this.function_signature(&arena, name, arguments, return_type)), 252 documentation: markdown_documentation(documentation), 253 text_documentation: text_documentation(documentation), 254 source_url: source_links.url(*location), 255 deprecation_message: match deprecation { 256 Deprecation::NotDeprecated => "".to_string(), 257 Deprecation::Deprecated { message } => message.to_string(), 258 }, 259 }); 260 } 261 262 for TypedModuleConstant { 263 documentation, 264 location, 265 publicity, 266 name, 267 type_, 268 deprecation, 269 .. 270 } in &definitions.constants 271 { 272 if !publicity.is_public() { 273 continue; 274 } 275 276 value_definitions.push(DocsValues { 277 name, 278 definition: print(self.constant(&arena, name, type_)), 279 raw_definition: self.raw(|this| this.constant(&arena, name, type_)), 280 documentation: markdown_documentation(documentation), 281 text_documentation: text_documentation(documentation), 282 source_url: source_links.url(*location), 283 deprecation_message: match deprecation { 284 Deprecation::NotDeprecated => "".to_string(), 285 Deprecation::Deprecated { message } => message.to_string(), 286 }, 287 }); 288 } 289 290 value_definitions.sort(); 291 value_definitions 292 } 293 294 fn custom_type( 295 &mut self, 296 arena: &'doc DocumentArena<'a, 'doc>, 297 name: &'a str, 298 parameters: &'a [(SrcSpan, EcoString)], 299 constructors: &'a [TypedRecordConstructor], 300 opaque: bool, 301 ) -> Document<'a, 'doc> { 302 let arguments = if parameters.is_empty() { 303 EMPTY_DOCUMENT 304 } else { 305 Self::wrap_arguments( 306 arena, 307 parameters 308 .iter() 309 .map(|(_, parameter)| self.variable(arena, parameter)), 310 ) 311 }; 312 313 let type_head = docvec![ 314 arena, 315 self.keyword( 316 arena, 317 if opaque { 318 PUB_OPAQUE_TYPE_SPACE_DOCUMENT 319 } else { 320 PUB_TYPE_SPACE_DOCUMENT 321 } 322 ), 323 self.title(arena, name), 324 arguments 325 ]; 326 327 if constructors.is_empty() || opaque { 328 return type_head; 329 } 330 331 let constructors = arena.concat(constructors.iter().map(|constructor| { 332 LINE_DOCUMENT 333 .append(arena, self.record_constructor(arena, constructor)) 334 .nest(arena, INDENT) 335 })); 336 337 docvec![ 338 arena, 339 type_head, 340 SPACE_OPEN_CURLY_DOCUMENT, 341 constructors, 342 LINE_DOCUMENT, 343 CLOSE_CURLY_DOCUMENT 344 ] 345 } 346 347 pub fn record_constructor( 348 &mut self, 349 arena: &'doc DocumentArena<'a, 'doc>, 350 constructor: &'a TypedRecordConstructor, 351 ) -> Document<'a, 'doc> { 352 if constructor.arguments.is_empty() { 353 return self.title(arena, &constructor.name); 354 } 355 356 let arguments = constructor.arguments.iter().map( 357 |RecordConstructorArg { label, type_, .. }| match label { 358 Some((_, label)) => self 359 .variable(arena, label) 360 .append(arena, COLON_SPACE_DOCUMENT) 361 .append(arena, self.type_(arena, type_, PrintMode::Normal)), 362 None => self.type_(arena, type_, PrintMode::Normal), 363 }, 364 ); 365 366 let arguments = Self::wrap_arguments(arena, arguments); 367 368 docvec![arena, self.title(arena, &constructor.name), arguments].group(arena) 369 } 370 371 fn type_alias( 372 &mut self, 373 arena: &'doc DocumentArena<'a, 'doc>, 374 name: &'a str, 375 type_: &Type, 376 parameters: &[(SrcSpan, EcoString)], 377 ) -> Document<'a, 'doc> { 378 let parameters = if parameters.is_empty() { 379 EMPTY_DOCUMENT 380 } else { 381 let arguments = parameters 382 .iter() 383 .map(|(_, parameter)| self.variable(arena, parameter)); 384 Self::wrap_arguments(arena, arguments) 385 }; 386 387 docvec![ 388 arena, 389 self.keyword(arena, PUB_TYPE_SPACE_DOCUMENT), 390 self.title(arena, name), 391 parameters, 392 SPACE_EQUAL_DOCUMENT, 393 LINE_DOCUMENT 394 .append(arena, self.type_(arena, type_, PrintMode::ExpandAliases)) 395 .nest(arena, INDENT) 396 ] 397 } 398 399 fn constant( 400 &mut self, 401 arena: &'doc DocumentArena<'a, 'doc>, 402 name: &'a str, 403 type_: &Type, 404 ) -> Document<'a, 'doc> { 405 self.register_local_type_variable_names(type_); 406 407 docvec![ 408 arena, 409 self.keyword(arena, PUB_CONST_SPACE_DOCUMENT), 410 self.title(arena, name), 411 COLON_SPACE_DOCUMENT, 412 self.type_(arena, type_, PrintMode::Normal) 413 ] 414 } 415 416 fn function_signature( 417 &mut self, 418 arena: &'doc DocumentArena<'a, 'doc>, 419 name: &'a str, 420 arguments: &'a [TypedArg], 421 return_type: &Type, 422 ) -> Document<'a, 'doc> { 423 for argument in arguments { 424 self.register_local_type_variable_names(&argument.type_); 425 } 426 self.register_local_type_variable_names(return_type); 427 428 let arguments = if arguments.is_empty() { 429 OPEN_CLOSE_PAREN_DOCUMENT 430 } else { 431 Self::wrap_arguments( 432 arena, 433 arguments.iter().map(|argument| { 434 let name = self.variable(arena, self.argument_name(arena, argument)); 435 docvec![ 436 arena, 437 name, 438 COLON_SPACE_DOCUMENT, 439 self.type_(arena, &argument.type_, PrintMode::Normal) 440 ] 441 .group(arena) 442 }), 443 ) 444 }; 445 446 docvec![ 447 arena, 448 self.keyword(arena, PUB_FN_SPACE_DOCUMENT), 449 self.title(arena, name), 450 arguments, 451 SPACE_RIGHT_ARROW_SPACE_DOCUMENT, 452 self.type_(arena, return_type, PrintMode::Normal) 453 ] 454 .group(arena) 455 } 456 457 fn argument_name( 458 &self, 459 arena: &'doc DocumentArena<'a, 'doc>, 460 arg: &'a TypedArg, 461 ) -> Document<'a, 'doc> { 462 match &arg.names { 463 ArgNames::Named { name, .. } => name.to_doc(arena), 464 ArgNames::NamedLabelled { label, name, .. } => { 465 docvec![arena, label, SPACE_DOCUMENT, name] 466 } 467 // We remove the underscore from discarded function arguments since we don't want to 468 // expose this kind of detail: https://github.com/gleam-lang/gleam/issues/2561 469 ArgNames::Discard { name, .. } => match name.strip_prefix('_').unwrap_or(name) { 470 "" => LOWERCASE_ARG_DOCUMENT, 471 name => name.to_doc(arena), 472 }, 473 ArgNames::LabelledDiscard { label, name, .. } => { 474 docvec![ 475 arena, 476 label, 477 SPACE_DOCUMENT, 478 name.strip_prefix('_').unwrap_or(name).to_doc(arena) 479 ] 480 } 481 } 482 } 483 484 fn wrap_arguments( 485 arena: &'doc DocumentArena<'a, 'doc>, 486 arguments: impl IntoIterator<Item = Document<'a, 'doc>>, 487 ) -> Document<'a, 'doc> { 488 OPEN_PAREN_BREAK_DOCUMENT 489 .append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT)) 490 .nest_if_broken(arena, INDENT) 491 .append(arena, TRAILING_COMMA_BREAK_DOCUMENT) 492 .append(arena, CLOSE_PAREN_DOCUMENT) 493 } 494 495 fn type_arguments( 496 arena: &'doc DocumentArena<'a, 'doc>, 497 arguments: impl IntoIterator<Item = Document<'a, 'doc>>, 498 ) -> Document<'a, 'doc> { 499 EMPTY_BREAK_DOCUMENT 500 .append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT)) 501 .nest_if_broken(arena, INDENT) 502 .append(arena, TRAILING_COMMA_BREAK_DOCUMENT) 503 .group(arena) 504 .surround(arena, OPEN_PAREN_DOCUMENT, CLOSE_PAREN_DOCUMENT) 505 } 506 507 fn type_( 508 &mut self, 509 arena: &'doc DocumentArena<'a, 'doc>, 510 type_: &Type, 511 print_mode: PrintMode, 512 ) -> Document<'a, 'doc> { 513 match type_ { 514 Type::Named { 515 package, 516 module, 517 name, 518 arguments, 519 publicity, 520 .. 521 } => { 522 let name = match print_mode { 523 // If we are printing a type for a type alias, and the alias 524 // is reexporting an internal type, we want to show that it 525 // is aliasing that internal type, rather than showing it as 526 // aliasing itself. 527 PrintMode::ExpandAliases if *package == self.package => { 528 self.named_type_name(arena, publicity, package, module, name) 529 } 530 // If we are printing a type alias which aliases an internal 531 // type from a different package, we still want to print the 532 // public name for that type. If we are not printing a type 533 // alias at all, we also want to use the public name. 534 PrintMode::ExpandAliases | PrintMode::Normal => { 535 // If we are using a reexported internal type, we want to 536 // print it public name, whether it is from this package 537 // or otherwise. 538 if let Some((module, alias)) = 539 self.names.reexport_alias(module.clone(), name.clone()) 540 { 541 self.named_type_name(arena, &Publicity::Public, package, module, alias) 542 } else { 543 self.named_type_name(arena, publicity, package, module, name) 544 } 545 } 546 }; 547 548 if arguments.is_empty() { 549 name 550 } else { 551 name.append( 552 arena, 553 Self::type_arguments( 554 arena, 555 arguments 556 .iter() 557 .map(|argument| self.type_(arena, argument, PrintMode::Normal)), 558 ), 559 ) 560 } 561 } 562 Type::Fn { arguments, return_ } => docvec![ 563 arena, 564 self.keyword(arena, FN_DOCUMENT), 565 Self::type_arguments( 566 arena, 567 arguments 568 .iter() 569 .map(|argument| self.type_(arena, argument, PrintMode::Normal)) 570 ), 571 SPACE_RIGHT_ARROW_SPACE_DOCUMENT, 572 self.type_(arena, return_, PrintMode::Normal) 573 ], 574 Type::Tuple { elements } => docvec![ 575 arena, 576 HASHTAG_DOCUMENT, 577 Self::type_arguments( 578 arena, 579 elements 580 .iter() 581 .map(|element| self.type_(arena, element, PrintMode::Normal)) 582 ), 583 ], 584 Type::Var { type_ } => match type_.as_ref().borrow().deref() { 585 TypeVar::Link { type_ } => self.type_(arena, type_, PrintMode::Normal), 586 587 TypeVar::Unbound { id } | TypeVar::Generic { id } => { 588 let name = self.type_variable(*id); 589 self.variable(arena, name) 590 } 591 }, 592 } 593 } 594 595 fn type_variable(&mut self, id: u64) -> EcoString { 596 if let Some(name) = self.names.get_type_variable(id) { 597 return name.clone(); 598 } 599 600 if let Some(name) = self.printed_type_variables.get(&id) { 601 return name.clone(); 602 } 603 604 loop { 605 let name = self.next_letter(); 606 if !self.printed_type_variable_names.contains(&name) { 607 _ = self.printed_type_variable_names.insert(name.clone()); 608 _ = self.printed_type_variables.insert(id, name.clone()); 609 return name; 610 } 611 } 612 } 613 614 // Copied from the `next_letter` method of the `type_::printer`. 615 fn next_letter(&mut self) -> EcoString { 616 let alphabet_length = 26; 617 let char_offset = b'a'; 618 let mut chars = vec![]; 619 let mut n; 620 let mut rest = self.next_type_variable_id; 621 622 loop { 623 n = rest % alphabet_length; 624 rest = rest / alphabet_length; 625 chars.push((n as u8 + char_offset) as char); 626 627 if rest == 0 { 628 break; 629 } 630 rest -= 1; 631 } 632 633 self.next_type_variable_id += 1; 634 chars.into_iter().rev().collect() 635 } 636 637 fn named_type_name( 638 &self, 639 arena: &'doc DocumentArena<'a, 'doc>, 640 publicity: &Publicity, 641 package: &str, 642 module: &str, 643 name: &EcoString, 644 ) -> Document<'a, 'doc> { 645 // There's no documentation page for the prelude 646 if package == PRELUDE_PACKAGE_NAME && module == PRELUDE_MODULE_NAME { 647 return self.title(arena, name); 648 } 649 650 // Internal types don't get linked 651 if !publicity.is_public() { 652 return docvec![ 653 arena, 654 self.comment(arena, INTERNAL_ATTRIBUTE_SPACE_DOCUMENT), 655 self.title(arena, name) 656 ]; 657 } 658 659 // Linking to a type within the same page 660 if package == self.package && module == self.module { 661 return self.link(arena, eco_format!("#{name}"), self.title(arena, name), None); 662 } 663 664 // Linking to a module within the package 665 if package == self.package { 666 // If we are linking to the current package, we might be viewing the 667 // documentation locally and so we need to generate a relative link. 668 669 let mut module_path = module.split('/').peekable(); 670 let mut current_module = self.module.split('/'); 671 672 // The documentation page for the final segment of the module is just 673 // an html file by itself, so it doesn't form part of the path and doesn't 674 // need to be backtracked using `..`. 675 let module_name = module_path.next_back().unwrap_or(module); 676 _ = current_module.next_back(); 677 678 // The two modules might have some sharer part of the path, which we 679 // don't need to traverse back through. However, if the two modules are 680 // something like `gleam/a/wibble/wobble` and `gleam/b/wibble/wobble`, 681 // the `wibble` folders are two different folders despite being at the 682 // same position with the same name. 683 let mut encountered_different_path = false; 684 let mut path = Vec::new(); 685 686 // Calculate how far backwards in the directory tree we need to walk 687 for segment in current_module { 688 // If this is still part of the shared path, we can just skip it: 689 // no need to go back and forth through the same directory in the 690 // path! 691 if !encountered_different_path && module_path.peek() == Some(&segment) { 692 _ = module_path.next(); 693 } else { 694 encountered_different_path = true; 695 path.push(".."); 696 } 697 } 698 699 // Once we have walked backwards, we walk forwards again to the correct 700 // page. 701 path.extend(module_path); 702 path.push(module_name); 703 704 let qualified_name = docvec![ 705 arena, 706 self.variable(arena, EcoString::from(module_name)), 707 DOT_DOCUMENT, 708 self.title(arena, name) 709 ]; 710 711 let title = eco_format!("{module}.{{type {name}}}"); 712 713 return self.link( 714 arena, 715 eco_format!("{path}.html#{name}", path = path.join("/")), 716 qualified_name, 717 Some(title), 718 ); 719 } 720 721 let module_name = module.split('/').next_back().unwrap_or(module); 722 let qualified_name = docvec![ 723 arena, 724 self.variable(arena, EcoString::from(module_name)), 725 DOT_DOCUMENT, 726 self.title(arena, name) 727 ]; 728 let title = eco_format!("{module}.{{type {name}}}"); 729 730 // We can't reliably link to documentation if the type is from a path 731 // or git dependency 732 match self.dependencies.get(package) { 733 Some(Dependency { 734 kind: DependencyKind::Hex, 735 version, 736 }) => self.link( 737 arena, 738 eco_format!( 739 "https://{package}.hexdocs.pm/{version}/{module}.html#{name}", 740 package = package.replace('_', "-") 741 ), 742 qualified_name, 743 Some(title), 744 ), 745 Some(_) | None => self.span_with_title(arena, qualified_name, title), 746 } 747 } 748 749 /// Walk a type and register all the type variable names which occur within 750 /// it. This is to ensure that when generating type variable names for 751 /// unannotated arguments, we don't print any that clash with existing names. 752 /// 753 /// We preregister all names before actually printing anything, because we 754 /// could run into code like this: 755 /// 756 /// ```gleam 757 /// pub fn wibble(_, _: a) -> b {} 758 /// ``` 759 /// 760 /// If we did not preregister the type variables in this case, we would end 761 /// up printing `fn wibble(_: a, _: a) -> b` which is not correct. 762 /// 763 fn register_local_type_variable_names(&mut self, type_: &Type) { 764 match type_ { 765 Type::Named { arguments, .. } => { 766 for argument in arguments { 767 self.register_local_type_variable_names(argument); 768 } 769 } 770 Type::Fn { arguments, return_ } => { 771 for argument in arguments { 772 self.register_local_type_variable_names(argument); 773 } 774 self.register_local_type_variable_names(return_); 775 } 776 Type::Var { type_ } => match type_.borrow().deref() { 777 TypeVar::Link { type_ } => self.register_local_type_variable_names(type_), 778 TypeVar::Unbound { id } | TypeVar::Generic { id } => { 779 if let Some(name) = self.names.get_type_variable(*id) { 780 _ = self.printed_type_variable_names.insert(name.clone()); 781 } 782 } 783 }, 784 Type::Tuple { elements } => { 785 for element in elements { 786 self.register_local_type_variable_names(element); 787 } 788 } 789 } 790 } 791 792 fn keyword( 793 &self, 794 arena: &'doc DocumentArena<'a, 'doc>, 795 keyword: Document<'static, 'static>, 796 ) -> Document<'a, 'doc> { 797 self.colour_span(arena, keyword, ColourClass::Keyword) 798 } 799 800 fn comment( 801 &self, 802 arena: &'doc DocumentArena<'a, 'doc>, 803 name: impl Documentable<'a, 'doc>, 804 ) -> Document<'a, 'doc> { 805 self.colour_span(arena, name, ColourClass::Comment) 806 } 807 808 fn title( 809 &self, 810 arena: &'doc DocumentArena<'a, 'doc>, 811 name: impl Documentable<'a, 'doc>, 812 ) -> Document<'a, 'doc> { 813 self.colour_span(arena, name, ColourClass::Title) 814 } 815 816 fn variable( 817 &self, 818 arena: &'doc DocumentArena<'a, 'doc>, 819 name: impl Documentable<'a, 'doc>, 820 ) -> Document<'a, 'doc> { 821 self.colour_span(arena, name, ColourClass::Variable) 822 } 823 824 fn colour_span( 825 &self, 826 arena: &'doc DocumentArena<'a, 'doc>, 827 name: impl Documentable<'a, 'doc>, 828 colour_class: ColourClass, 829 ) -> Document<'a, 'doc> { 830 if !self.options.print_highlighting { 831 return name.to_doc(arena); 832 } 833 834 let open_tag = match colour_class { 835 ColourClass::Variable => ZERO_WIDTH_OPEN_VARIABLE_COLOUR_SPAN, 836 ColourClass::Title => ZERO_WIDTH_OPEN_TITLE_COLOUR_SPAN, 837 ColourClass::Keyword => ZERO_WIDTH_OPEN_KEYWORD_COLOUR_SPAN, 838 ColourClass::Comment => ZERO_WIDTH_OPEN_COMMENT_COLOUR_SPAN, 839 }; 840 841 name.to_doc(arena) 842 .surround(arena, open_tag, ZERO_WIDTH_CLOSED_SPAN_TAG_DOCUMENT) 843 } 844 845 fn link( 846 &self, 847 arena: &'doc DocumentArena<'a, 'doc>, 848 href: EcoString, 849 name: impl Documentable<'a, 'doc>, 850 title: Option<EcoString>, 851 ) -> Document<'a, 'doc> { 852 if !self.options.print_html { 853 return name.to_doc(arena); 854 } 855 856 let opening_tag = if let Some(title) = title { 857 eco_format!(r#"<a href="{href}" title="{title}">"#) 858 } else { 859 eco_format!(r#"<a href="{href}">"#) 860 }; 861 862 name.to_doc(arena).surround( 863 arena, 864 arena.zero_width_string(opening_tag), 865 ZERO_WIDTH_CLOSED_A_TAG_DOCUMENT, 866 ) 867 } 868 869 fn span_with_title( 870 &self, 871 arena: &'doc DocumentArena<'a, 'doc>, 872 name: impl Documentable<'a, 'doc>, 873 title: EcoString, 874 ) -> Document<'a, 'doc> { 875 if !self.options.print_html { 876 return name.to_doc(arena); 877 } 878 879 name.to_doc(arena).surround( 880 arena, 881 arena.zero_width_string(eco_format!(r#"<span title="{title}">"#)), 882 ZERO_WIDTH_CLOSED_SPAN_TAG_DOCUMENT, 883 ) 884 } 885} 886 887enum ColourClass { 888 Variable, 889 Title, 890 Keyword, 891 Comment, 892} 893 894const MAX_COLUMNS: isize = 65; 895const INDENT: isize = 2; 896 897fn print<'a, 'doc>(doc: Document<'a, 'doc>) -> String { 898 doc.to_pretty_string(MAX_COLUMNS) 899}