Fork of daniellemaywood.uk/gleam — Wasm codegen work
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}