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