• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<lambda>null1 package org.jetbrains.dokka
2 
3 import com.intellij.psi.*
4 import com.intellij.psi.impl.source.javadoc.CorePsiDocTagValueImpl
5 import com.intellij.psi.impl.source.tree.JavaDocElementType
6 import com.intellij.psi.javadoc.*
7 import com.intellij.psi.util.PsiTreeUtil
8 import com.intellij.util.IncorrectOperationException
9 import com.intellij.util.containers.isNullOrEmpty
10 import org.jetbrains.dokka.Model.CodeNode
11 import org.jetbrains.kotlin.utils.join
12 import org.jetbrains.kotlin.utils.keysToMap
13 import org.jsoup.Jsoup
14 import org.jsoup.nodes.Element
15 import org.jsoup.nodes.Node
16 import org.jsoup.nodes.TextNode
17 import java.io.File
18 import java.net.URI
19 import java.util.regex.Pattern
20 
21 private val NAME_TEXT = Pattern.compile("(\\S+)(.*)", Pattern.DOTALL)
22 private val TEXT = Pattern.compile("(\\S+)\\s*(.*)", Pattern.DOTALL)
23 
24 data class JavadocParseResult(
25     val content: Content,
26     val deprecatedContent: Content?,
27     val attributeRefs: List<String>,
28     val apiLevel: DocumentationNode? = null,
29     val deprecatedLevel: DocumentationNode? = null,
30     val artifactId: DocumentationNode? = null,
31     val attribute: DocumentationNode? = null
32 ) {
33     companion object {
34         val Empty = JavadocParseResult(Content.Empty,
35             null,
36             emptyList(),
37             null,
38             null,
39             null
40         )
41     }
42 }
43 
44 interface JavaDocumentationParser {
parseDocumentationnull45     fun parseDocumentation(element: PsiNamedElement): JavadocParseResult
46 }
47 
48 class JavadocParser(
49     private val refGraph: NodeReferenceGraph,
50     private val logger: DokkaLogger,
51     private val signatureProvider: ElementSignatureProvider,
52     private val externalDocumentationLinkResolver: ExternalDocumentationLinkResolver
53 ) : JavaDocumentationParser {
54 
55     private fun ContentSection.appendTypeElement(
56         signature: String,
57         selector: (DocumentationNode) -> DocumentationNode?
58     ) {
59         append(LazyContentBlock {
60             val node = refGraph.lookupOrWarn(signature, logger)?.let(selector)
61             if (node != null) {
62                 it.append(NodeRenderContent(node, LanguageService.RenderMode.SUMMARY))
63                 it.symbol(":")
64                 it.text(" ")
65             }
66         })
67     }
68 
69     override fun parseDocumentation(element: PsiNamedElement): JavadocParseResult {
70         val docComment = (element as? PsiDocCommentOwner)?.docComment
71         if (docComment == null) return JavadocParseResult.Empty
72         val result = MutableContent()
73         var deprecatedContent: Content? = null
74         val firstParagraph = ContentParagraph()
75         firstParagraph.convertJavadocElements(
76             docComment.descriptionElements.dropWhile { it.text.trim().isEmpty() },
77             element
78         )
79         val paragraphs = firstParagraph.children.dropWhile { it !is ContentParagraph }
80         firstParagraph.children.removeAll(paragraphs)
81         if (!firstParagraph.isEmpty()) {
82             result.append(firstParagraph)
83         }
84         paragraphs.forEach {
85             result.append(it)
86         }
87 
88         if (element is PsiMethod) {
89             val tagsByName = element.searchInheritedTags()
90             for ((tagName, tags) in tagsByName) {
91                 for ((tag, context) in tags) {
92                     val section = result.addSection(javadocSectionDisplayName(tagName), tag.getSubjectName())
93                     val signature = signatureProvider.signature(element)
94                     when (tagName) {
95                         "param" -> {
96                             section.appendTypeElement(signature) {
97                                 it.details
98                                     .find { node -> node.kind == NodeKind.Parameter && node.name == tag.getSubjectName() }
99                                     ?.detailOrNull(NodeKind.Type)
100                             }
101                         }
102                         "return" -> {
103                             section.appendTypeElement(signature) { it.detailOrNull(NodeKind.Type) }
104                         }
105                     }
106                     section.convertJavadocElements(tag.contentElements(), context)
107                 }
108             }
109         }
110 
111         val attrRefSignatures = mutableListOf<String>()
112         var since: DocumentationNode? = null
113         var deprecated: DocumentationNode? = null
114         var artifactId: DocumentationNode? = null
115         var attrName: String? = null
116         var attrDesc: Content? = null
117         var attr: DocumentationNode? = null
118         docComment.tags.forEach { tag ->
119             when (tag.name.toLowerCase()) {
120                 "see" -> result.convertSeeTag(tag)
121                 "deprecated" -> {
122                     deprecatedContent = Content().apply {
123                         convertJavadocElements(tag.contentElements(), element)
124                     }
125                 }
126                 "attr" -> {
127                     when (tag.valueElement?.text) {
128                         "ref" ->
129                             tag.getAttrRef(element)?.let {
130                                 attrRefSignatures.add(it)
131                             }
132                         "name" -> attrName = tag.getAttrName()
133                         "description" -> attrDesc = tag.getAttrDesc(element)
134                     }
135                 }
136                 "since", "apisince" -> {
137                     since = DocumentationNode(tag.getApiLevel() ?: "", Content.Empty, NodeKind.ApiLevel)
138                 }
139                 "deprecatedsince" -> {
140                     deprecated = DocumentationNode(tag.getApiLevel() ?: "", Content.Empty, NodeKind.DeprecatedLevel)
141                 }
142                 "artifactid" -> {
143                     artifactId = DocumentationNode(tag.artifactId() ?: "", Content.Empty, NodeKind.ArtifactId)
144                 }
145                 in tagsToInherit -> {
146                 }
147                 else -> {
148                     val subjectName = tag.getSubjectName()
149                     val section = result.addSection(javadocSectionDisplayName(tag.name), subjectName)
150                     section.convertJavadocElements(tag.contentElements(), element)
151                 }
152             }
153         }
154         attrName?.let { name ->
155             attr = DocumentationNode(name, attrDesc ?: Content.Empty, NodeKind.AttributeRef)
156         }
157         return JavadocParseResult(result, deprecatedContent, attrRefSignatures, since, deprecated, artifactId, attr)
158     }
159 
160     private val tagsToInherit = setOf("param", "return", "throws")
161 
162     private data class TagWithContext(val tag: PsiDocTag, val context: PsiNamedElement)
163 
164     fun PsiDocTag.artifactId(): String? {
165         var artifactName: String? = null
166         if (dataElements.isNotEmpty()) {
167             artifactName = join(dataElements.map { it.text }, "")
168         }
169         return artifactName
170     }
171 
172     fun PsiDocTag.getApiLevel(): String? {
173         if (dataElements.isNotEmpty()) {
174             val data = dataElements
175             if (data[0] is CorePsiDocTagValueImpl) {
176                 val docTagValue = data[0]
177                 if (docTagValue.firstChild != null) {
178                     val apiLevel = docTagValue.firstChild
179                     return apiLevel.text
180                 }
181             }
182         }
183         return null
184     }
185 
186     private fun PsiDocTag.getAttrRef(element: PsiNamedElement): String? {
187         if (dataElements.size > 1) {
188             val elementText = dataElements[1].text
189             try {
190                 val linkComment = JavaPsiFacade.getInstance(project).elementFactory
191                     .createDocCommentFromText("/** {@link $elementText} */", element)
192                 val linkElement = PsiTreeUtil.getChildOfType(linkComment, PsiInlineDocTag::class.java)?.linkElement()
193                 val signature = resolveInternalLink(linkElement)
194                 val attrSignature = "AttrMain:$signature"
195                 return attrSignature
196             } catch (e: IncorrectOperationException) {
197                 return null
198             }
199         } else return null
200     }
201 
202     private fun PsiDocTag.getAttrName(): String? {
203         if (dataElements.size > 1) {
204             val nameMatcher = NAME_TEXT.matcher(dataElements[1].text)
205             if (nameMatcher.matches()) {
206                 return nameMatcher.group(1)
207             } else {
208                 return null
209             }
210         } else return null
211     }
212 
213     private fun PsiDocTag.getAttrDesc(element: PsiNamedElement): Content? {
214         return Content().apply {
215             convertJavadocElementsToAttrDesc(contentElements(), element)
216         }
217     }
218 
219     private fun PsiMethod.searchInheritedTags(): Map<String, Collection<TagWithContext>> {
220 
221         val output = tagsToInherit.keysToMap { mutableMapOf<String?, TagWithContext>() }
222 
223         fun recursiveSearch(methods: Array<PsiMethod>) {
224             for (method in methods) {
225                 recursiveSearch(method.findSuperMethods())
226             }
227             for (method in methods) {
228                 for (tag in method.docComment?.tags.orEmpty()) {
229                     if (tag.name in tagsToInherit) {
230                         output[tag.name]!![tag.getSubjectName()] = TagWithContext(tag, method)
231                     }
232                 }
233             }
234         }
235 
236         recursiveSearch(arrayOf(this))
237         return output.mapValues { it.value.values }
238     }
239 
240 
241     private fun PsiDocTag.contentElements(): Iterable<PsiElement> {
242         val tagValueElements = children
243             .dropWhile { it.node?.elementType == JavaDocTokenType.DOC_TAG_NAME }
244             .dropWhile { it is PsiWhiteSpace }
245             .filterNot { it.node?.elementType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS }
246         return if (getSubjectName() != null) tagValueElements.dropWhile { it is PsiDocTagValue } else tagValueElements
247     }
248 
249     private fun ContentBlock.convertJavadocElements(elements: Iterable<PsiElement>, element: PsiNamedElement) {
250         val doc = Jsoup.parse(expandAllForElements(elements, element))
251         doc.body().childNodes().forEach {
252             convertHtmlNode(it)?.let { append(it) }
253         }
254         doc.head().childNodes().forEach {
255             convertHtmlNode(it)?.let { append(it) }
256         }
257     }
258 
259     private fun ContentBlock.convertJavadocElementsToAttrDesc(elements: Iterable<PsiElement>, element: PsiNamedElement) {
260         val doc = Jsoup.parse(expandAllForElements(elements, element))
261         doc.body().childNodes().forEach {
262             convertHtmlNode(it)?.let {
263                 var content = it
264                 if (content is ContentText) {
265                     var description = content.text
266                     val matcher = TEXT.matcher(content.text)
267                     if (matcher.matches()) {
268                         val command = matcher.group(1)
269                         if (command == "description") {
270                             description = matcher.group(2)
271                             content = ContentText(description)
272                         }
273                     }
274                 }
275                 append(content)
276             }
277         }
278     }
279 
280     private fun expandAllForElements(elements: Iterable<PsiElement>, element: PsiNamedElement): String {
281         val htmlBuilder = StringBuilder()
282         elements.forEach {
283             if (it is PsiInlineDocTag) {
284                 htmlBuilder.append(convertInlineDocTag(it, element))
285             } else {
286                 htmlBuilder.append(it.text)
287             }
288         }
289         return htmlBuilder.toString().trim()
290     }
291 
292     private fun convertHtmlNode(node: Node, isBlockCode: Boolean = false): ContentNode? {
293         if (isBlockCode) {
294             return if (node is TextNode) { // Fixes b/129762453
295                 val codeNode = CodeNode(node.wholeText, "")
296                 ContentText(codeNode.text().removePrefix("#"))
297             } else { // Fixes b/129857975
298                 ContentText(node.toString())
299             }
300         }
301         if (node is TextNode) {
302             return ContentText(node.text().removePrefix("#"))
303         } else if (node is Element) {
304             val childBlock = createBlock(node)
305             node.childNodes().forEach {
306                 val child = convertHtmlNode(it, isBlockCode = childBlock is ContentBlockCode)
307                 if (child != null) {
308                     childBlock.append(child)
309                 }
310             }
311             return (childBlock)
312         }
313         return null
314     }
315 
316     private fun createBlock(element: Element): ContentBlock = when (element.tagName()) {
317         "p" -> ContentParagraph()
318         "b", "strong" -> ContentStrong()
319         "i", "em" -> ContentEmphasis()
320         "s", "del" -> ContentStrikethrough()
321         "code" -> ContentCode()
322         "pre" -> ContentBlockCode()
323         "ul" -> ContentUnorderedList()
324         "ol" -> ContentOrderedList()
325         "li" -> ContentListItem()
326         "a" -> createLink(element)
327         "br" -> ContentBlock().apply { hardLineBreak() }
328 
329         "dl" -> ContentDescriptionList()
330         "dt" -> ContentDescriptionTerm()
331         "dd" -> ContentDescriptionDefinition()
332 
333         "table" -> ContentTable()
334         "tbody" -> ContentTableBody()
335         "tr" -> ContentTableRow()
336         "th" -> {
337             val colspan = element.attr("colspan")
338             val rowspan = element.attr("rowspan")
339             ContentTableHeader(colspan, rowspan)
340         }
341         "td" -> {
342             val colspan = element.attr("colspan")
343             val rowspan = element.attr("rowspan")
344             ContentTableCell(colspan, rowspan)
345         }
346 
347         "h1" -> ContentHeading(1)
348         "h2" -> ContentHeading(2)
349         "h3" -> ContentHeading(3)
350         "h4" -> ContentHeading(4)
351         "h5" -> ContentHeading(5)
352         "h6" -> ContentHeading(6)
353 
354         "div" -> {
355             val divClass = element.attr("class")
356             if (divClass == "special reference" || divClass == "note") ContentSpecialReference()
357             else ContentParagraph()
358         }
359 
360         "script" -> {
361 
362             // If the `type` attr is an empty string, we want to use null instead so that the resulting generated
363             // Javascript does not contain a `type` attr.
364             //
365             // Example:
366             // type == ""   => <script type="" src="...">
367             // type == null => <script src="...">
368             val type = if (element.attr("type").isNotEmpty()) {
369                 element.attr("type")
370             } else {
371                 null
372             }
373             ScriptBlock(type, element.attr("src"))
374         }
375 
376         else -> ContentBlock()
377     }
378 
379     private fun createLink(element: Element): ContentBlock {
380         return when {
381             element.hasAttr("docref") -> {
382                 val docref = element.attr("docref")
383                 ContentNodeLazyLink(docref, { -> refGraph.lookupOrWarn(docref, logger) })
384             }
385             element.hasAttr("href") -> {
386                 val href = element.attr("href")
387 
388                 val uri = try {
389                     URI(href)
390                 } catch (_: Exception) {
391                     null
392                 }
393 
394                 if (uri?.isAbsolute == false) {
395                     ContentLocalLink(href)
396                 } else {
397                     ContentExternalLink(href)
398                 }
399             }
400             element.hasAttr("name") -> {
401                 ContentBookmark(element.attr("name"))
402             }
403             else -> ContentBlock()
404         }
405     }
406 
407     private fun MutableContent.convertSeeTag(tag: PsiDocTag) {
408         val linkElement = tag.linkElement() ?: return
409         val seeSection = findSectionByTag(ContentTags.SeeAlso) ?: addSection(ContentTags.SeeAlso, null)
410 
411         val valueElement = tag.referenceElement()
412         val externalLink = resolveExternalLink(valueElement)
413         val text = ContentText(linkElement.text)
414 
415         val linkSignature by lazy { resolveInternalLink(valueElement) }
416         val node = when {
417             externalLink != null -> {
418                 val linkNode = ContentExternalLink(externalLink)
419                 linkNode.append(text)
420                 linkNode
421             }
422             linkSignature != null -> {
423                 val linkNode =
424                     ContentNodeLazyLink(
425                         (tag.valueElement ?: linkElement).text,
426                         { -> refGraph.lookupOrWarn(linkSignature, logger) }
427                     )
428                 linkNode.append(text)
429                 linkNode
430             }
431             else -> text
432         }
433         seeSection.append(node)
434     }
435 
436     private fun convertInlineDocTag(tag: PsiInlineDocTag, element: PsiNamedElement) = when (tag.name) {
437         "link", "linkplain" -> {
438             val valueElement = tag.referenceElement()
439             val externalLink = resolveExternalLink(valueElement)
440             val linkSignature by lazy { resolveInternalLink(valueElement) }
441             if (externalLink != null || linkSignature != null) {
442                 val labelText = tag.dataElements.firstOrNull { it is PsiDocToken }?.text ?: valueElement!!.text
443                 val linkTarget = if (externalLink != null) "href=\"$externalLink\"" else "docref=\"$linkSignature\""
444                 val link = "<a $linkTarget>$labelText</a>"
445                 if (tag.name == "link") "<code>$link</code>" else link
446             } else if (valueElement != null) {
447                 valueElement.text
448             } else {
449                 ""
450             }
451         }
452         "code", "literal" -> {
453             val text = StringBuilder()
454             tag.dataElements.forEach { text.append(it.text) }
455             val escaped = text.toString().trimStart().htmlEscape()
456             if (tag.name == "code") "<code>$escaped</code>" else escaped
457         }
458         "inheritDoc" -> {
459             val result = (element as? PsiMethod)?.let {
460                 // @{inheritDoc} is only allowed on functions
461                 val parent = tag.parent
462                 when (parent) {
463                     is PsiDocComment -> element.findSuperDocCommentOrWarn()
464                     is PsiDocTag -> element.findSuperDocTagOrWarn(parent)
465                     else -> null
466                 }
467             }
468             result ?: tag.text
469         }
470         "docRoot" -> {
471             // TODO: fix that
472             "https://developer.android.com/"
473         }
474         "sample" -> {
475             tag.text?.let { tagText ->
476                 val (absolutePath, delimiter) = getSampleAnnotationInformation(tagText)
477                 val code = retrieveCodeInFile(absolutePath, delimiter)
478                 return if (code != null && code.isNotEmpty()) {
479                     "<pre is-upgraded>$code</pre>"
480                 } else {
481                     ""
482                 }
483             }
484         }
485 
486         // Loads MathJax script from local source, which then updates MathJax HTML code
487         "usesMathJax" -> {
488             "<script src=\"/_static/js/managed/mathjax/MathJax.js?config=TeX-AMS_SVG\"></script>"
489         }
490 
491         else -> tag.text
492     }
493 
494     private fun PsiDocTag.referenceElement(): PsiElement? =
495         linkElement()?.let {
496             if (it.node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) {
497                 PsiTreeUtil.findChildOfType(it, PsiJavaCodeReferenceElement::class.java)
498             } else {
499                 it
500             }
501         }
502 
503     private fun PsiDocTag.linkElement(): PsiElement? =
504         valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace }
505 
506     private fun resolveExternalLink(valueElement: PsiElement?): String? {
507         val target = valueElement?.reference?.resolve()
508         if (target != null) {
509             return externalDocumentationLinkResolver.buildExternalDocumentationLink(target)
510         }
511         return null
512     }
513 
514     private fun resolveInternalLink(valueElement: PsiElement?): String? {
515         val target = valueElement?.reference?.resolve()
516         if (target != null) {
517             return signatureProvider.signature(target)
518         }
519         return null
520     }
521 
522     fun PsiDocTag.getSubjectName(): String? {
523         if (name == "param" || name == "throws" || name == "exception") {
524             return valueElement?.text
525         }
526         return null
527     }
528 
529     private fun PsiMethod.findSuperDocCommentOrWarn(): String {
530         val method = findFirstSuperMethodWithDocumentation(this)
531         if (method != null) {
532             val descriptionElements = method.docComment?.descriptionElements?.dropWhile {
533                 it.text.trim().isEmpty()
534             } ?: return ""
535 
536             return expandAllForElements(descriptionElements, method)
537         }
538         logger.warn("No docs found on supertype with {@inheritDoc} method ${this.name} in ${this.containingFile.name}:${this.lineNumber()}")
539         return ""
540     }
541 
542 
543     private fun PsiMethod.findSuperDocTagOrWarn(elementToExpand: PsiDocTag): String {
544         val result = findFirstSuperMethodWithDocumentationforTag(elementToExpand, this)
545 
546         if (result != null) {
547             val (method, tag) = result
548 
549             val contentElements = tag.contentElements().dropWhile { it.text.trim().isEmpty() }
550 
551             val expandedString = expandAllForElements(contentElements, method)
552 
553             return expandedString
554         }
555         logger.warn("No docs found on supertype for @${elementToExpand.name} ${elementToExpand.getSubjectName()} with {@inheritDoc} method ${this.name} in ${this.containingFile.name}:${this.lineNumber()}")
556         return ""
557     }
558 
559     private fun findFirstSuperMethodWithDocumentation(current: PsiMethod): PsiMethod? {
560         val superMethods = current.findSuperMethods()
561         for (method in superMethods) {
562             val docs = method.docComment?.descriptionElements?.dropWhile { it.text.trim().isEmpty() }
563             if (!docs.isNullOrEmpty()) {
564                 return method
565             }
566         }
567         for (method in superMethods) {
568             val result = findFirstSuperMethodWithDocumentation(method)
569             if (result != null) {
570                 return result
571             }
572         }
573 
574         return null
575     }
576 
577     private fun findFirstSuperMethodWithDocumentationforTag(
578         elementToExpand: PsiDocTag,
579         current: PsiMethod
580     ): Pair<PsiMethod, PsiDocTag>? {
581         val superMethods = current.findSuperMethods()
582         val mappedFilteredTags = superMethods.map {
583             it to it.docComment?.tags?.filter { it.name == elementToExpand.name }
584         }
585 
586         for ((method, tags) in mappedFilteredTags) {
587             tags ?: continue
588             for (tag in tags) {
589                 val (tagSubject, elementSubject) = when (tag.name) {
590                     "throws" -> {
591                         // match class names only for throws, ignore possibly fully qualified path
592                         // TODO: Always match exactly here
593                         tag.getSubjectName()?.split(".")?.last() to elementToExpand.getSubjectName()?.split(".")?.last()
594                     }
595                     else -> {
596                         tag.getSubjectName() to elementToExpand.getSubjectName()
597                     }
598                 }
599 
600                 if (tagSubject == elementSubject) {
601                     return method to tag
602                 }
603             }
604         }
605 
606         for (method in superMethods) {
607             val result = findFirstSuperMethodWithDocumentationforTag(elementToExpand, method)
608             if (result != null) {
609                 return result
610             }
611         }
612         return null
613     }
614 
615     /**
616      * Returns information inside @sample
617      *
618      * Component1 is the absolute path to the file
619      * Component2 is the delimiter if exists in the file
620      */
621     private fun getSampleAnnotationInformation(tagText: String): Pair<String, String> {
622         val pathContent = tagText
623             .trim { it == '{' || it == '}' }
624             .removePrefix("@sample ")
625 
626         val formattedPath = pathContent.substringBefore(" ").trim()
627         val potentialDelimiter = pathContent.substringAfterLast(" ").trim()
628 
629         val delimiter = if (potentialDelimiter == formattedPath) "" else potentialDelimiter
630         val path = "samples/$formattedPath"
631 
632         return Pair(path, delimiter)
633     }
634 
635     /**
636      * Retrieves the code inside a file.
637      *
638      * If betweenTag is not empty, it retrieves the code between
639      * BEGIN_INCLUDE($betweenTag) and END_INCLUDE($betweenTag) comments.
640      *
641      * Also, the method will trim every line with the number of spaces in the first line
642      */
643     private fun retrieveCodeInFile(path: String, betweenTag: String = "") = StringBuilder().apply {
644             try {
645                 if (betweenTag.isEmpty()) {
646                     appendContent(path)
647                 } else {
648                     appendContentBetweenIncludes(path, betweenTag)
649                 }
650             } catch (e: java.lang.Exception) {
651                 logger.error("No file found when processing Java @sample. Path to sample: $path")
652             }
653         }
654 
655     private fun StringBuilder.appendContent(path: String) {
656         val spaces = InitialSpaceIndent()
657         File(path).forEachLine {
658             appendWithoutInitialIndent(it, spaces)
659         }
660     }
661 
662     private fun StringBuilder.appendContentBetweenIncludes(path: String, includeTag: String) {
663         var shouldAppend = false
664         val beginning = "BEGIN_INCLUDE($includeTag)"
665         val end = "END_INCLUDE($includeTag)"
666         val spaces = InitialSpaceIndent()
667         File(path).forEachLine {
668             if (shouldAppend) {
669                 if (it.contains(end)) {
670                     shouldAppend = false
671                 } else {
672                     appendWithoutInitialIndent(it, spaces)
673                 }
674             } else {
675                 if (it.contains(beginning)) shouldAppend = true
676             }
677         }
678     }
679 
680     private fun StringBuilder.appendWithoutInitialIndent(it: String, spaces: InitialSpaceIndent) {
681         if (spaces.value == -1) {
682             spaces.value = (it.length - it.trimStart().length).coerceAtLeast(0)
683             appendln(it)
684         } else {
685             appendln(if (it.isBlank()) it else it.substring(spaces.value, it.length))
686         }
687     }
688 
689     private data class InitialSpaceIndent(var value: Int = -1)
690 }
691