diff options
Diffstat (limited to 'plugins/base/src/main/kotlin/renderers/html')
18 files changed, 0 insertions, 2253 deletions
diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlContent.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlContent.kt deleted file mode 100644 index 1ef6e04c..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlContent.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html - -import org.jetbrains.dokka.pages.ContentBreakLine -import org.jetbrains.dokka.pages.Style - - -/** - * Html-specific style that represents <hr> tag if used in conjunction with [ContentBreakLine] - */ -internal object HorizontalBreakLineStyle : Style { - // this exists as a simple internal solution to avoid introducing unnecessary public API on content level. - // If you have the need to implement proper horizontal divider (i.e to support `---` markdown element), - // consider removing this and providing proper API for all formats and levels -} diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt deleted file mode 100644 index 083876d5..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt +++ /dev/null @@ -1,1013 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html - -import kotlinx.html.* -import kotlinx.html.stream.createHTML -import org.jetbrains.dokka.DokkaSourceSetID -import org.jetbrains.dokka.Platform -import org.jetbrains.dokka.base.DokkaBase -import org.jetbrains.dokka.base.renderers.* -import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer -import org.jetbrains.dokka.base.renderers.html.innerTemplating.DefaultTemplateModelFactory -import org.jetbrains.dokka.base.renderers.html.innerTemplating.DefaultTemplateModelMerger -import org.jetbrains.dokka.base.renderers.html.innerTemplating.DokkaTemplateTypes -import org.jetbrains.dokka.base.renderers.html.innerTemplating.HtmlTemplater -import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint -import org.jetbrains.dokka.base.resolvers.local.DokkaBaseLocationProvider -import org.jetbrains.dokka.base.templating.* -import org.jetbrains.dokka.base.transformers.documentables.CallableExtensions -import org.jetbrains.dokka.base.translators.documentables.shouldDocumentConstructors -import org.jetbrains.dokka.links.DRI -import org.jetbrains.dokka.model.* -import org.jetbrains.dokka.model.properties.PropertyContainer -import org.jetbrains.dokka.model.properties.WithExtraProperties -import org.jetbrains.dokka.pages.* -import org.jetbrains.dokka.pages.HtmlContent -import org.jetbrains.dokka.plugability.* -import org.jetbrains.dokka.transformers.pages.PageTransformer -import org.jetbrains.dokka.utilities.htmlEscape - -internal const val TEMPLATE_REPLACEMENT: String = "###" -internal const val TOGGLEABLE_CONTENT_TYPE_ATTR = "data-togglable" - -public open class HtmlRenderer( - context: DokkaContext -) : DefaultRenderer<FlowContent>(context) { - private val sourceSetDependencyMap: Map<DokkaSourceSetID, List<DokkaSourceSetID>> = - context.configuration.sourceSets.associate { sourceSet -> - sourceSet.sourceSetID to context.configuration.sourceSets - .map { it.sourceSetID } - .filter { it in sourceSet.dependentSourceSets } - } - - private val templateModelFactories = listOf(DefaultTemplateModelFactory(context)) // TODO: Make extension point - private val templateModelMerger = DefaultTemplateModelMerger() - private val templater = HtmlTemplater(context).apply { - setupSharedModel(templateModelMerger.invoke(templateModelFactories) { buildSharedModel() }) - } - - private var shouldRenderSourceSetTabs: Boolean = false - - override val preprocessors: List<PageTransformer> = context.plugin<DokkaBase>().query { htmlPreprocessors } - - /** - * Tabs themselves are created in HTML plugin since, currently, only HTML format supports them. - * [TabbedContentType] is used to mark content that should be inside tab content. - * A tab can display multiple [TabbedContentType]. - * The content style [ContentStyle.TabbedContent] is used to determine where tabs will be generated. - * - * @see TabbedContentType - * @see ContentStyle.TabbedContent - */ - private fun createTabs(pageContext: ContentPage): List<ContentTab> { - return when(pageContext) { - is ClasslikePage -> createTabsForClasslikes(pageContext) - is PackagePage -> createTabsForPackage(pageContext) - else -> throw IllegalArgumentException("Page ${pageContext.name} cannot have tabs") - } - } - - private fun createTabsForClasslikes(page: ClasslikePage): List<ContentTab> { - val documentables = page.documentables - val csEnum = documentables.filterIsInstance<DEnum>() - val csWithConstructor = documentables.filterIsInstance<WithConstructors>() - val scopes = documentables.filterIsInstance<WithScope>() - val constructorsToDocumented = csWithConstructor.flatMap { it.constructors } - - val containsRenderableConstructors = constructorsToDocumented.isNotEmpty() && documentables.shouldDocumentConstructors() - val containsRenderableMembers = - containsRenderableConstructors || scopes.any { it.classlikes.isNotEmpty() || it.functions.isNotEmpty() || it.properties.isNotEmpty() } - - @Suppress("UNCHECKED_CAST") - val extensions = (documentables as List<WithExtraProperties<DClasslike>>).flatMap { - it.extra[CallableExtensions]?.extensions - ?.filterIsInstance<Documentable>().orEmpty() - } - .distinctBy { it.sourceSets to it.dri } // [Documentable] has expensive equals/hashCode at the moment, see #2620 - return listOfNotNull( - if(!containsRenderableMembers) null else - ContentTab( - "Members", - listOf( - BasicTabbedContentType.CONSTRUCTOR, - BasicTabbedContentType.TYPE, - BasicTabbedContentType.PROPERTY, - BasicTabbedContentType.FUNCTION - ) - ), - if (extensions.isEmpty()) null else ContentTab( - "Members & Extensions", - listOf( - BasicTabbedContentType.CONSTRUCTOR, - BasicTabbedContentType.TYPE, - BasicTabbedContentType.PROPERTY, - BasicTabbedContentType.FUNCTION, - BasicTabbedContentType.EXTENSION_PROPERTY, - BasicTabbedContentType.EXTENSION_FUNCTION - ) - ), - if(csEnum.isEmpty()) null else ContentTab( - "Entries", - listOf( - BasicTabbedContentType.ENTRY - ) - ) - ) - } - - private fun createTabsForPackage(page: PackagePage): List<ContentTab> { - val p = page.documentables.single() as DPackage - return listOfNotNull( - if (p.typealiases.isEmpty() && p.classlikes.isEmpty()) null else ContentTab( - "Types", - listOf( - BasicTabbedContentType.TYPE, - ) - ), - if (p.functions.isEmpty()) null else ContentTab( - "Functions", - listOf( - BasicTabbedContentType.FUNCTION, - BasicTabbedContentType.EXTENSION_FUNCTION, - ) - ), - if (p.properties.isEmpty()) null else ContentTab( - "Properties", - listOf( - BasicTabbedContentType.PROPERTY, - BasicTabbedContentType.EXTENSION_PROPERTY, - ) - ) - ) - } - - private fun <R> TagConsumer<R>.prepareForTemplates() = - if (context.configuration.delayTemplateSubstitution || this is ImmediateResolutionTagConsumer) this - else ImmediateResolutionTagConsumer(this, context) - - override fun FlowContent.wrapGroup( - node: ContentGroup, - pageContext: ContentPage, - childrenCallback: FlowContent.() -> Unit - ) { - val additionalClasses = node.style.joinToString(" ") { it.toString().toLowerCase() } - return when { - node.hasStyle(ContentStyle.TabbedContent) -> div(additionalClasses) { - val contentTabs = createTabs(pageContext) - - div(classes = "tabs-section") { - attributes["tabs-section"] = "tabs-section" - contentTabs.forEachIndexed { index, contentTab -> - button(classes = "section-tab") { - if (index == 0) attributes["data-active"] = "" - attributes[TOGGLEABLE_CONTENT_TYPE_ATTR] = - contentTab.tabbedContentTypes.joinToString(",") { it.toHtmlAttribute() } - text(contentTab.text) - } - } - } - div(classes = "tabs-section-body") { - childrenCallback() - } - } - node.hasStyle(ContentStyle.WithExtraAttributes) -> div { - node.extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } - childrenCallback() - } - node.dci.kind in setOf(ContentKind.Symbol) -> div("symbol $additionalClasses") { - childrenCallback() - } - node.hasStyle(ContentStyle.KDocTag) -> span("kdoc-tag") { childrenCallback() } - node.hasStyle(ContentStyle.Footnote) -> div("footnote") { childrenCallback() } - node.hasStyle(TextStyle.BreakableAfter) -> { - span { childrenCallback() } - wbr { } - } - node.hasStyle(TextStyle.Breakable) -> { - span("breakable-word") { childrenCallback() } - } - node.hasStyle(TextStyle.Span) -> span { childrenCallback() } - node.dci.kind == ContentKind.Symbol -> div("symbol $additionalClasses") { - childrenCallback() - } - node.dci.kind == SymbolContentKind.Parameters -> { - span("parameters $additionalClasses") { - childrenCallback() - } - } - node.dci.kind == SymbolContentKind.Parameter -> { - span("parameter $additionalClasses") { - childrenCallback() - } - } - node.hasStyle(TextStyle.InlineComment) -> div("inline-comment") { childrenCallback() } - node.dci.kind == ContentKind.BriefComment -> div("brief $additionalClasses") { childrenCallback() } - node.dci.kind == ContentKind.Cover -> div("cover $additionalClasses") { //TODO this can be removed - childrenCallback() - } - node.dci.kind == ContentKind.Deprecation -> div("deprecation-content") { childrenCallback() } - node.hasStyle(TextStyle.Paragraph) -> p(additionalClasses) { childrenCallback() } - node.hasStyle(TextStyle.Block) -> div(additionalClasses) { - childrenCallback() - } - node.hasStyle(TextStyle.Quotation) -> blockQuote(additionalClasses) { childrenCallback() } - node.hasStyle(TextStyle.FloatingRight) -> span("clearfix") { span("floating-right") { childrenCallback() } } - node.hasStyle(TextStyle.Strikethrough) -> strike { childrenCallback() } - node.isAnchorable -> buildAnchor( - node.anchor!!, - node.anchorLabel!!, - node.buildSourceSetFilterValues() - ) { childrenCallback() } - node.extra[InsertTemplateExtra] != null -> node.extra[InsertTemplateExtra]?.let { templateCommand(it.command) } - ?: Unit - node.hasStyle(ListStyle.DescriptionTerm) -> DT(emptyMap(), consumer).visit { - this@wrapGroup.childrenCallback() - } - node.hasStyle(ListStyle.DescriptionDetails) -> DD(emptyMap(), consumer).visit { - this@wrapGroup.childrenCallback() - } - node.extra.extraTabbedContentType() != null -> div() { - node.extra.extraTabbedContentType()?.let { attributes[TOGGLEABLE_CONTENT_TYPE_ATTR] = it.value.toHtmlAttribute() } - this@wrapGroup.childrenCallback() - } - else -> childrenCallback() - } - } - - private fun FlowContent.copyButton() = span(classes = "top-right-position") { - span("copy-icon") - copiedPopup("Content copied to clipboard", "popup-to-left") - } - - private fun FlowContent.copiedPopup(notificationContent: String, additionalClasses: String = "") = - div("copy-popup-wrapper $additionalClasses") { - span("copy-popup-icon") - span { - text(notificationContent) - } - } - - override fun FlowContent.buildPlatformDependent( - content: PlatformHintedContent, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>? - ) { - buildPlatformDependent( - content.sourceSets.filter { - sourceSetRestriction == null || it in sourceSetRestriction - }.associateWith { setOf(content.inner) }, - pageContext, - content.extra, - content.style - ) - } - - private fun FlowContent.buildPlatformDependent( - nodes: Map<DisplaySourceSet, Collection<ContentNode>>, - pageContext: ContentPage, - extra: PropertyContainer<ContentNode> = PropertyContainer.empty(), - styles: Set<Style> = emptySet(), - shouldHaveTabs: Boolean = shouldRenderSourceSetTabs - ) { - val contents = contentsForSourceSetDependent(nodes, pageContext) - val isOnlyCommonContent = contents.singleOrNull()?.let { (sourceSet, _) -> - sourceSet.platform == Platform.common - && sourceSet.name.equals("common", ignoreCase = true) - && sourceSet.sourceSetIDs.all.all { sourceSetDependencyMap[it]?.isEmpty() == true } - } ?: false - - // little point in rendering a single "common" tab - it can be - // assumed that code without any tabs is common by default - val renderTabs = shouldHaveTabs && !isOnlyCommonContent - - val divStyles = "platform-hinted ${styles.joinToString()}" + if (renderTabs) " with-platform-tabs" else "" - div(divStyles) { - attributes["data-platform-hinted"] = "data-platform-hinted" - extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } - if (renderTabs) { - div("platform-bookmarks-row") { - attributes["data-toggle-list"] = "data-toggle-list" - contents.forEachIndexed { index, pair -> - button(classes = "platform-bookmark") { - attributes["data-filterable-current"] = pair.first.sourceSetIDs.merged.toString() - attributes["data-filterable-set"] = pair.first.sourceSetIDs.merged.toString() - if (index == 0) attributes["data-active"] = "" - attributes["data-toggle"] = pair.first.sourceSetIDs.merged.toString() - text(pair.first.name) - } - } - } - } - contents.forEach { - consumer.onTagContentUnsafe { +it.second } - } - } - } - - private fun contentsForSourceSetDependent( - nodes: Map<DisplaySourceSet, Collection<ContentNode>>, - pageContext: ContentPage, - ): List<Pair<DisplaySourceSet, String>> { - var counter = 0 - return nodes.toList().map { (sourceSet, elements) -> - val htmlContent = createHTML(prettyPrint = false).prepareForTemplates().div { - elements.forEach { - buildContentNode(it, pageContext, sourceSet) - } - }.stripDiv() - sourceSet to createHTML(prettyPrint = false).prepareForTemplates() - .div(classes = "content sourceset-dependent-content") { - if (counter++ == 0) attributes["data-active"] = "" - attributes["data-togglable"] = sourceSet.sourceSetIDs.merged.toString() - unsafe { - +htmlContent - } - } - }.sortedBy { it.first.comparableKey } - } - - override fun FlowContent.buildDivergent(node: ContentDivergentGroup, pageContext: ContentPage) { - if (node.implicitlySourceSetHinted) { - val groupedInstancesBySourceSet = node.children.flatMap { instance -> - instance.sourceSets.map { sourceSet -> instance to sourceSet } - }.groupBy( - Pair<ContentDivergentInstance, DisplaySourceSet>::second, - Pair<ContentDivergentInstance, DisplaySourceSet>::first - ) - - val nodes = groupedInstancesBySourceSet.mapValues { - val distinct = - groupDivergentInstancesWithSourceSet(it.value, it.key, pageContext, - beforeTransformer = { instance, _, sourceSet -> - createHTML(prettyPrint = false).prepareForTemplates().div { - instance.before?.let { before -> - buildContentNode(before, pageContext, sourceSet) - } - }.stripDiv() - }, - afterTransformer = { instance, _, sourceSet -> - createHTML(prettyPrint = false).prepareForTemplates().div { - instance.after?.let { after -> - buildContentNode(after, pageContext, sourceSet) - } - }.stripDiv() - }) - - val isPageWithOverloadedMembers = pageContext is MemberPage && pageContext.documentables().size > 1 - - val contentOfSourceSet = mutableListOf<ContentNode>() - distinct.onEachIndexed{ index, (_, distinctInstances) -> - distinctInstances.firstOrNull()?.before?.let { contentOfSourceSet.add(it) } - contentOfSourceSet.addAll(distinctInstances.map { it.divergent }) - (distinctInstances.firstOrNull()?.after ?: if (index != distinct.size - 1) ContentBreakLine(setOf(it.key)) else null) - ?.let { contentOfSourceSet.add(it) } - - // content kind main is important for declarations list to avoid double line breaks - if (node.dci.kind == ContentKind.Main && index != distinct.size - 1) { - if (isPageWithOverloadedMembers) { - // add some spacing and distinction between function/property overloads. - // not ideal, but there's no other place to modify overloads page atm - contentOfSourceSet.add(ContentBreakLine(setOf(it.key), style = setOf(HorizontalBreakLineStyle))) - } else { - contentOfSourceSet.add(ContentBreakLine(setOf(it.key))) - } - } - } - contentOfSourceSet - } - buildPlatformDependent(nodes, pageContext) - } else { - node.children.forEach { - buildContentNode(it.divergent, pageContext, it.sourceSets) - } - } - } - - private fun groupDivergentInstancesWithSourceSet( - instances: List<ContentDivergentInstance>, - sourceSet: DisplaySourceSet, - pageContext: ContentPage, - beforeTransformer: (ContentDivergentInstance, ContentPage, DisplaySourceSet) -> String, - afterTransformer: (ContentDivergentInstance, ContentPage, DisplaySourceSet) -> String - ): Map<SerializedBeforeAndAfter, List<ContentDivergentInstance>> = - instances.map { instance -> - instance to Pair( - beforeTransformer(instance, pageContext, sourceSet), - afterTransformer(instance, pageContext, sourceSet) - ) - }.groupBy( - Pair<ContentDivergentInstance, SerializedBeforeAndAfter>::second, - Pair<ContentDivergentInstance, SerializedBeforeAndAfter>::first - ) - - private fun ContentPage.documentables(): List<Documentable> { - return (this as? WithDocumentables)?.documentables ?: emptyList() - } - - override fun FlowContent.buildList( - node: ContentList, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>? - ) { - return when { - node.ordered -> { - ol { buildListItems(node.children, pageContext, sourceSetRestriction) } - } - node.hasStyle(ListStyle.DescriptionList) -> { - dl { node.children.forEach { it.build(this, pageContext, sourceSetRestriction) } } - } - else -> { - ul { buildListItems(node.children, pageContext, sourceSetRestriction) } - } - } - } - - public open fun OL.buildListItems( - items: List<ContentNode>, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>? = null - ) { - items.forEach { - if (it is ContentList) - buildList(it, pageContext) - else - li { it.build(this, pageContext, sourceSetRestriction) } - } - } - - public open fun UL.buildListItems( - items: List<ContentNode>, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>? = null - ) { - items.forEach { - if (it is ContentList) - buildList(it, pageContext) - else - li { it.build(this, pageContext) } - } - } - - override fun FlowContent.buildResource( - node: ContentEmbeddedResource, - pageContext: ContentPage - ) { // TODO: extension point there - if (node.isImage()) { - img(src = node.address, alt = node.altText) - } else { - println("Unrecognized resource type: $node") - } - } - - private fun FlowContent.buildRow( - node: ContentGroup, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>? - ) { - node.children - .filter { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } } - .takeIf { it.isNotEmpty() } - ?.let { - when (pageContext) { - is MultimoduleRootPage -> buildRowForMultiModule(node, it, pageContext, sourceSetRestriction) - is ModulePage -> buildRowForModule(node, it, pageContext, sourceSetRestriction) - else -> buildRowForContent(node, it, pageContext, sourceSetRestriction) - } - } - } - - private fun FlowContent.buildRowForMultiModule( - contextNode: ContentGroup, - toRender: List<ContentNode>, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>? - ) { - buildAnchor(contextNode) - div(classes = "table-row") { - div("main-subrow " + contextNode.style.joinToString(separator = " ")) { - buildRowHeaderLink(toRender, pageContext, sourceSetRestriction, contextNode.anchor, "w-100") - div { - buildRowBriefSectionForDocs(toRender, pageContext, sourceSetRestriction) - } - } - } - } - - private fun FlowContent.buildRowForModule( - contextNode: ContentGroup, - toRender: List<ContentNode>, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>? - ) { - buildAnchor(contextNode) - div(classes = "table-row") { - addSourceSetFilteringAttributes(contextNode) - div { - div("main-subrow " + contextNode.style.joinToString(separator = " ")) { - buildRowHeaderLink(toRender, pageContext, sourceSetRestriction, contextNode.anchor) - div("pull-right") { - if (ContentKind.shouldBePlatformTagged(contextNode.dci.kind)) { - createPlatformTags(contextNode, cssClasses = "no-gutters") - } - } - } - div { - buildRowBriefSectionForDocs(toRender, pageContext, sourceSetRestriction) - } - } - } - } - - private fun FlowContent.buildRowForContent( - contextNode: ContentGroup, - toRender: List<ContentNode>, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>? - ) { - buildAnchor(contextNode) - div(classes = "table-row") { - contextNode.extra.extraTabbedContentType()?.let { attributes[TOGGLEABLE_CONTENT_TYPE_ATTR] = it.value.toHtmlAttribute() } - addSourceSetFilteringAttributes(contextNode) - div("main-subrow keyValue " + contextNode.style.joinToString(separator = " ")) { - buildRowHeaderLink(toRender, pageContext, sourceSetRestriction, contextNode.anchor) - div { - toRender.filter { it !is ContentLink && !it.hasStyle(ContentStyle.RowTitle) } - .takeIf { it.isNotEmpty() }?.let { - div("title") { - it.forEach { - it.build(this, pageContext, sourceSetRestriction) - } - } - } - } - } - } - } - - private fun FlowContent.buildRowHeaderLink( - toRender: List<ContentNode>, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>?, - anchorDestination: String?, - classes: String = "" - ) { - toRender.filter { it is ContentLink || it.hasStyle(ContentStyle.RowTitle) }.takeIf { it.isNotEmpty() }?.let { - div(classes) { - it.filter { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } } - .forEach { - span("inline-flex") { - div { - it.build(this, pageContext, sourceSetRestriction) - } - if (it is ContentLink && !anchorDestination.isNullOrBlank()) { - buildAnchorCopyButton(anchorDestination) - } - } - } - } - } - } - - private fun FlowContent.addSourceSetFilteringAttributes( - contextNode: ContentGroup, - ) { - attributes["data-filterable-current"] = contextNode.buildSourceSetFilterValues() - attributes["data-filterable-set"] = contextNode.buildSourceSetFilterValues() - } - - private fun ContentNode.buildSourceSetFilterValues(): String { - // This value is used in HTML and JS for filtering out source set declarations, - // it is expected that the separator is the same here and there. - // See https://github.com/Kotlin/dokka/issues/3011#issuecomment-1568620493 - return this.sourceSets.joinToString(",") { - it.sourceSetIDs.merged.toString() - } - } - - private fun FlowContent.buildRowBriefSectionForDocs( - toRender: List<ContentNode>, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>?, - ) { - toRender.filter { it !is ContentLink }.takeIf { it.isNotEmpty() }?.let { - it.forEach { - span(classes = if (it.dci.kind == ContentKind.Comment) "brief-comment" else "") { - it.build(this, pageContext, sourceSetRestriction) - } - } - } - } - - private fun FlowContent.createPlatformTagBubbles(sourceSets: List<DisplaySourceSet>, cssClasses: String = "") { - div("platform-tags $cssClasses") { - sourceSets.sortedBy { it.name }.forEach { - div("platform-tag") { - when (it.platform.key) { - "common" -> classes = classes + "common-like" - "native" -> classes = classes + "native-like" - "jvm" -> classes = classes + "jvm-like" - "js" -> classes = classes + "js-like" - "wasm" -> classes = classes + "wasm-like" - } - text(it.name) - } - } - } - } - - private fun FlowContent.createPlatformTags( - node: ContentNode, - sourceSetRestriction: Set<DisplaySourceSet>? = null, - cssClasses: String = "" - ) { - node.takeIf { sourceSetRestriction == null || it.sourceSets.any { s -> s in sourceSetRestriction } }?.let { - createPlatformTagBubbles(node.sourceSets.filter { - sourceSetRestriction == null || it in sourceSetRestriction - }.sortedBy { it.name }, cssClasses) - } - } - - override fun FlowContent.buildTable( - node: ContentTable, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>? - ) { - when { - node.style.contains(CommentTable) -> buildDefaultTable(node, pageContext, sourceSetRestriction) - else -> div(classes = "table") { - node.extra.extraHtmlAttributes().forEach { attributes[it.extraKey] = it.extraValue } - node.children.forEach { - buildRow(it, pageContext, sourceSetRestriction) - } - } - } - - } - - public fun FlowContent.buildDefaultTable( - node: ContentTable, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>? - ) { - table { - thead { - node.header.forEach { - tr { - it.children.forEach { - th { - it.build(this@table, pageContext, sourceSetRestriction) - } - } - } - } - } - tbody { - node.children.forEach { - tr { - it.children.forEach { - td { - it.build(this, pageContext, sourceSetRestriction) - } - } - } - } - } - } - } - - - override fun FlowContent.buildHeader(level: Int, node: ContentHeader, content: FlowContent.() -> Unit) { - val classes = node.style.joinToString { it.toString() }.toLowerCase() - when (level) { - 1 -> h1(classes = classes, content) - 2 -> h2(classes = classes, content) - 3 -> h3(classes = classes, content) - 4 -> h4(classes = classes, content) - 5 -> h5(classes = classes, content) - else -> h6(classes = classes, content) - } - } - - private fun FlowContent.buildAnchor( - anchor: String, - anchorLabel: String, - sourceSets: String, - content: FlowContent.() -> Unit - ) { - a { - attributes["data-name"] = anchor - attributes["anchor-label"] = anchorLabel - attributes["id"] = anchor - attributes["data-filterable-set"] = sourceSets - } - content() - } - - private fun FlowContent.buildAnchor(anchor: String, anchorLabel: String, sourceSets: String) = - buildAnchor(anchor, anchorLabel, sourceSets) {} - - private fun FlowContent.buildAnchor(node: ContentNode) { - node.anchorLabel?.let { label -> buildAnchor(node.anchor!!, label, node.buildSourceSetFilterValues()) } - } - - - override fun FlowContent.buildNavigation(page: PageNode) { - div(classes = "breadcrumbs") { - val path = locationProvider.ancestors(page).filterNot { it is RendererSpecificPage }.asReversed() - if (path.size > 1) { - buildNavigationElement(path.first(), page) - path.drop(1).forEach { node -> - span(classes = "delimiter") { - text("/") - } - buildNavigationElement(node, page) - } - } - } - } - - private fun FlowContent.buildNavigationElement(node: PageNode, page: PageNode) = - if (node.isNavigable) { - val isCurrentPage = (node == page) - if (isCurrentPage) { - span(classes = "current") { - text(node.name) - } - } else { - buildLink(node, page) - } - } else { - text(node.name) - } - - private fun FlowContent.buildLink(to: PageNode, from: PageNode) = - locationProvider.resolve(to, from)?.let { path -> - buildLink(path) { - text(to.name) - } - } ?: span { - attributes["data-unresolved-link"] = to.name.htmlEscape() - text(to.name) - } - - public fun FlowContent.buildAnchorCopyButton(pointingTo: String) { - span(classes = "anchor-wrapper") { - span(classes = "anchor-icon") { - attributes["pointing-to"] = pointingTo - } - copiedPopup("Link copied to clipboard") - } - } - - public fun FlowContent.buildLink( - to: DRI, - platforms: List<DisplaySourceSet>, - from: PageNode? = null, - block: FlowContent.() -> Unit - ) { - locationProvider.resolve(to, platforms.toSet(), from)?.let { buildLink(it, block) } - ?: run { context.logger.error("Cannot resolve path for `$to` from `$from`"); block() } - } - - override fun buildError(node: ContentNode) { - context.logger.error("Unknown ContentNode type: $node") - } - - override fun FlowContent.buildLineBreak() { - br() - } - override fun FlowContent.buildLineBreak(node: ContentBreakLine, pageContext: ContentPage) { - if (node.style.contains(HorizontalBreakLineStyle)) { - hr() - } else { - buildLineBreak() - } - } - - override fun FlowContent.buildLink(address: String, content: FlowContent.() -> Unit) { - a(href = address, block = content) - } - - override fun FlowContent.buildDRILink( - node: ContentDRILink, - pageContext: ContentPage, - sourceSetRestriction: Set<DisplaySourceSet>? - ) { - locationProvider.resolve(node.address, node.sourceSets, pageContext)?.let { address -> - buildLink(address) { - buildText(node.children, pageContext, sourceSetRestriction) - } - } ?: if (isPartial) { - templateCommand(ResolveLinkCommand(node.address)) { - buildText(node.children, pageContext, sourceSetRestriction) - } - } else { - span { - attributes["data-unresolved-link"] = node.address.toString().htmlEscape() - buildText(node.children, pageContext, sourceSetRestriction) - } - } - } - - override fun FlowContent.buildCodeBlock( - code: ContentCodeBlock, - pageContext: ContentPage - ) { - div("sample-container") { - val codeLang = "lang-" + code.language.ifEmpty { "kotlin" } - val stylesWithBlock = code.style + TextStyle.Block + codeLang - pre { - code(stylesWithBlock.joinToString(" ") { it.toString().toLowerCase() }) { - attributes["theme"] = "idea" - code.children.forEach { buildContentNode(it, pageContext) } - } - } - /* - Disable copy button on samples as: - - it is useless - - it overflows with playground's run button - */ - if (!code.style.contains(ContentStyle.RunnableSample)) copyButton() - } - } - - override fun FlowContent.buildCodeInline( - code: ContentCodeInline, - pageContext: ContentPage - ) { - val codeLang = "lang-" + code.language.ifEmpty { "kotlin" } - val stylesWithBlock = code.style + codeLang - code(stylesWithBlock.joinToString(" ") { it.toString().toLowerCase() }) { - code.children.forEach { buildContentNode(it, pageContext) } - } - } - - override fun FlowContent.buildText(textNode: ContentText) { - buildText(textNode, textNode.style) - } - - private fun FlowContent.buildText(textNode: ContentText, unappliedStyles: Set<Style>) { - when { - textNode.extra[HtmlContent] != null -> { - consumer.onTagContentUnsafe { raw(textNode.text) } - } - unappliedStyles.contains(TextStyle.Indented) -> { - consumer.onTagContentEntity(Entities.nbsp) - buildText(textNode, unappliedStyles - TextStyle.Indented) - } - unappliedStyles.isNotEmpty() -> { - val styleToApply = unappliedStyles.first() - applyStyle(styleToApply) { - buildText(textNode, unappliedStyles - styleToApply) - } - } - textNode.hasStyle(ContentStyle.RowTitle) || textNode.hasStyle(TextStyle.Cover) -> - buildBreakableText(textNode.text) - else -> text(textNode.text) - } - } - - private inline fun FlowContent.applyStyle(styleToApply: Style, crossinline body: FlowContent.() -> Unit) { - when (styleToApply) { - TextStyle.Bold -> b { body() } - TextStyle.Italic -> i { body() } - TextStyle.Strikethrough -> strike { body() } - TextStyle.Strong -> strong { body() } - TextStyle.Var -> htmlVar { body() } - TextStyle.Underlined -> underline { body() } - is TokenStyle -> span("token ${styleToApply.prismJsClass()}") { body() } - else -> body() - } - } - - private fun TokenStyle.prismJsClass(): String = when(this) { - // Prism.js parser adds Builtin token instead of Annotation - // for some reason, so we also add it for consistency and correct coloring - TokenStyle.Annotation -> "annotation builtin" - else -> this.toString().toLowerCase() - } - - override fun render(root: RootPageNode) { - shouldRenderSourceSetTabs = shouldRenderSourceSetTabs(root) - super.render(root) - } - - override fun buildPage(page: ContentPage, content: (FlowContent, ContentPage) -> Unit): String = - buildHtml(page, page.embeddedResources) { - content(this, page) - } - - private fun PageNode.getDocumentableType(): String? = - when(this) { - is PackagePage -> "package" - is ClasslikePage -> "classlike" - is MemberPage -> "member" - else -> null - } - - public open fun buildHtml( - page: PageNode, - resources: List<String>, content: FlowContent.() -> Unit - ): String { - return templater.renderFromTemplate(DokkaTemplateTypes.BASE) { - val generatedContent = - createHTML().div("main-content") { - page.getDocumentableType()?.let { attributes["data-page-type"] = it } - id = "content" - (page as? ContentPage)?.let { - attributes["pageIds"] = "${context.configuration.moduleName}::${page.pageId}" - } - content() - } - - templateModelMerger.invoke(templateModelFactories) { - buildModel( - page, - resources, - locationProvider, - generatedContent - ) - } - } - } - - /** - * This is deliberately left open for plugins that have some other pages above ours and would like to link to them - * instead of ours when clicking the logo - */ - public open fun FlowContent.clickableLogo(page: PageNode, pathToRoot: String) { - if (context.configuration.delayTemplateSubstitution && page is ContentPage) { - templateCommand(PathToRootSubstitutionCommand(pattern = "###", default = pathToRoot)) { - a { - href = "###index.html" - templateCommand( - ProjectNameSubstitutionCommand( - pattern = "@@@", - default = context.configuration.moduleName - ) - ) { - span { - text("@@@") - } - } - } - } - } else { - a { - href = pathToRoot + "index.html" - text(context.configuration.moduleName) - } - } - } - - private val ContentNode.isAnchorable: Boolean - get() = anchorLabel != null - - private val ContentNode.anchorLabel: String? - get() = extra[SymbolAnchorHint]?.anchorName - - private val ContentNode.anchor: String? - get() = extra[SymbolAnchorHint]?.contentKind?.let { contentKind -> - (locationProvider as DokkaBaseLocationProvider).anchorForDCI(DCI(dci.dri, contentKind), sourceSets) - } - - private val isPartial = context.configuration.delayTemplateSubstitution -} - -private fun TabbedContentType.toHtmlAttribute(): String = - when(this) { - is BasicTabbedContentType -> - when(this) { - BasicTabbedContentType.ENTRY -> "ENTRY" - BasicTabbedContentType.TYPE -> "TYPE" - BasicTabbedContentType.CONSTRUCTOR -> "CONSTRUCTOR" - BasicTabbedContentType.FUNCTION -> "FUNCTION" - BasicTabbedContentType.PROPERTY -> "PROPERTY" - BasicTabbedContentType.EXTENSION_PROPERTY -> "EXTENSION_PROPERTY" - BasicTabbedContentType.EXTENSION_FUNCTION -> "EXTENSION_FUNCTION" - } - else -> throw IllegalStateException("Unknown TabbedContentType $this") - } - -/** - * Tabs for a content with [ContentStyle.TabbedContent]. - * - * @see ContentStyle.TabbedContent] - */ -private data class ContentTab(val text: String, val tabbedContentTypes: List<TabbedContentType>) - -public fun List<SimpleAttr>.joinAttr(): String = joinToString(" ") { it.extraKey + "=" + it.extraValue } - -private fun String.stripDiv() = drop(5).dropLast(6) // TODO: Find a way to do it without arbitrary trims - -private val PageNode.isNavigable: Boolean - get() = this !is RendererSpecificPage || strategy != RenderingStrategy.DoNothing - -private fun PropertyContainer<ContentNode>.extraHtmlAttributes() = allOfType<SimpleAttr>() -private fun PropertyContainer<ContentNode>.extraTabbedContentType() = this[TabbedContentTypeExtra] - -private val DisplaySourceSet.comparableKey - get() = sourceSetIDs.merged.let { it.scopeId + it.sourceSetName } diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt deleted file mode 100644 index fccfd145..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/NavigationDataProvider.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html - -import org.jetbrains.dokka.base.renderers.sourceSets -import org.jetbrains.dokka.base.signatures.KotlinSignatureUtils.annotations -import org.jetbrains.dokka.base.transformers.documentables.isDeprecated -import org.jetbrains.dokka.base.transformers.documentables.isException -import org.jetbrains.dokka.base.utils.canonicalAlphabeticalOrder -import org.jetbrains.dokka.model.* -import org.jetbrains.dokka.pages.* -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.plugability.plugin -import org.jetbrains.dokka.plugability.querySingle -import org.jetbrains.dokka.analysis.kotlin.internal.DocumentableLanguage -import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin - -public abstract class NavigationDataProvider( - dokkaContext: DokkaContext -) { - private val documentableSourceLanguageParser = dokkaContext.plugin<InternalKotlinAnalysisPlugin>().querySingle { documentableSourceLanguageParser } - - public open fun navigableChildren(input: RootPageNode): NavigationNode = input.withDescendants() - .first { it is ModulePage || it is MultimoduleRootPage }.let { visit(it as ContentPage) } - - public open fun visit(page: ContentPage): NavigationNode = - NavigationNode( - name = page.displayableName(), - dri = page.dri.first(), - sourceSets = page.sourceSets(), - icon = chooseNavigationIcon(page), - styles = chooseStyles(page), - children = page.navigableChildren() - ) - - /** - * Parenthesis is applied in 1 case: - * - page only contains functions (therefore documentable from this page is [DFunction]) - */ - private fun ContentPage.displayableName(): String = - if (this is WithDocumentables && documentables.all { it is DFunction }) { - "$name()" - } else { - name - } - - private fun chooseNavigationIcon(contentPage: ContentPage): NavigationNodeIcon? = - if (contentPage is WithDocumentables) { - val documentable = contentPage.documentables.firstOrNull() - val isJava = documentable?.hasAnyJavaSources() ?: false - - when (documentable) { - is DTypeAlias -> NavigationNodeIcon.TYPEALIAS_KT - is DClass -> when { - documentable.isException -> NavigationNodeIcon.EXCEPTION - documentable.isAbstract() -> { - if (isJava) NavigationNodeIcon.ABSTRACT_CLASS else NavigationNodeIcon.ABSTRACT_CLASS_KT - } - else -> if (isJava) NavigationNodeIcon.CLASS else NavigationNodeIcon.CLASS_KT - } - is DFunction -> NavigationNodeIcon.FUNCTION - is DProperty -> { - val isVar = documentable.extra[IsVar] != null - if (isVar) NavigationNodeIcon.VAR else NavigationNodeIcon.VAL - } - is DInterface -> if (isJava) NavigationNodeIcon.INTERFACE else NavigationNodeIcon.INTERFACE_KT - is DEnum, - is DEnumEntry -> if (isJava) NavigationNodeIcon.ENUM_CLASS else NavigationNodeIcon.ENUM_CLASS_KT - is DAnnotation -> { - if (isJava) NavigationNodeIcon.ANNOTATION_CLASS else NavigationNodeIcon.ANNOTATION_CLASS_KT - } - is DObject -> NavigationNodeIcon.OBJECT - else -> null - } - } else { - null - } - - private fun Documentable.hasAnyJavaSources(): Boolean { - return this.sourceSets.any { sourceSet -> - documentableSourceLanguageParser.getLanguage(this, sourceSet) == DocumentableLanguage.JAVA - } - } - - private fun DClass.isAbstract() = - modifier.values.all { it is KotlinModifier.Abstract || it is JavaModifier.Abstract } - - private fun chooseStyles(page: ContentPage): Set<Style> = - if (page.containsOnlyDeprecatedDocumentables()) setOf(TextStyle.Strikethrough) else emptySet() - - private fun ContentPage.containsOnlyDeprecatedDocumentables(): Boolean { - if (this !is WithDocumentables) { - return false - } - return this.documentables.isNotEmpty() && this.documentables.all { it.isDeprecatedForAllSourceSets() } - } - - private fun Documentable.isDeprecatedForAllSourceSets(): Boolean { - val sourceSetAnnotations = this.annotations() - return sourceSetAnnotations.isNotEmpty() && sourceSetAnnotations.all { (_, annotations) -> - annotations.any { it.isDeprecated() } - } - } - - private val navigationNodeOrder: Comparator<NavigationNode> = - compareBy(canonicalAlphabeticalOrder) { it.name } - - private fun ContentPage.navigableChildren() = - if (this is ClasslikePage) { - this.navigableChildren() - } else { - children - .filterIsInstance<ContentPage>() - .map { visit(it) } - .sortedWith(navigationNodeOrder) - } - - private fun ClasslikePage.navigableChildren(): List<NavigationNode> { - // Classlikes should only have other classlikes as navigable children - val navigableChildren = children - .filterIsInstance<ClasslikePage>() - .map { visit(it) } - - val isEnumPage = documentables.any { it is DEnum } - return if (isEnumPage) { - // no sorting for enum entries, should be the same order as in source code - navigableChildren - } else { - navigableChildren.sortedWith(navigationNodeOrder) - } - } -} diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt deleted file mode 100644 index eae43daf..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html - -import kotlinx.html.* -import kotlinx.html.stream.createHTML -import org.jetbrains.dokka.base.renderers.html.NavigationNodeIcon.CLASS -import org.jetbrains.dokka.base.renderers.html.NavigationNodeIcon.CLASS_KT -import org.jetbrains.dokka.base.renderers.pageId -import org.jetbrains.dokka.base.templating.AddToNavigationCommand -import org.jetbrains.dokka.links.DRI -import org.jetbrains.dokka.model.DisplaySourceSet -import org.jetbrains.dokka.model.WithChildren -import org.jetbrains.dokka.pages.* -import org.jetbrains.dokka.plugability.DokkaContext - -public class NavigationPage( - public val root: NavigationNode, - public val moduleName: String, - public val context: DokkaContext -) : RendererSpecificPage { - - override val name: String = "navigation" - - override val children: List<PageNode> = emptyList() - - override fun modified(name: String, children: List<PageNode>): NavigationPage = this - - override val strategy: RenderingStrategy = RenderingStrategy<HtmlRenderer> { - createHTML().visit(root, this) - } - - private fun <R> TagConsumer<R>.visit(node: NavigationNode, renderer: HtmlRenderer): R = with(renderer) { - if (context.configuration.delayTemplateSubstitution) { - templateCommand(AddToNavigationCommand(moduleName)) { - visit(node, "${moduleName}-nav-submenu", renderer) - } - } else { - visit(node, "${moduleName}-nav-submenu", renderer) - } - } - - private fun <R> TagConsumer<R>.visit(node: NavigationNode, navId: String, renderer: HtmlRenderer): R = - with(renderer) { - div("sideMenuPart") { - id = navId - attributes["pageId"] = "${moduleName}::${node.pageId}" - div("overview") { - if (node.children.isNotEmpty()) { - span("navButton") { - onClick = """document.getElementById("$navId").classList.toggle("hidden");""" - span("navButtonContent") - } - } - buildLink(node.dri, node.sourceSets.toList()) { - val withIcon = node.icon != null - if (withIcon) { - // in case link text is so long that it needs to have word breaks, - // and it stretches to two or more lines, make sure the icon - // is always on the left in the grid and is not wrapped with text - span("nav-link-grid") { - span("nav-link-child ${node.icon?.style()}") - span("nav-link-child") { - nodeText(node) - } - } - } else { - nodeText(node) - } - } - } - node.children.withIndex().forEach { (n, p) -> visit(p, "$navId-$n", renderer) } - } - } - - private fun FlowContent.nodeText(node: NavigationNode) { - if (node.styles.contains(TextStyle.Strikethrough)) { - strike { - buildBreakableText(node.name) - } - } else { - buildBreakableText(node.name) - } - } -} - -public data class NavigationNode( - val name: String, - val dri: DRI, - val sourceSets: Set<DisplaySourceSet>, - val icon: NavigationNodeIcon?, - val styles: Set<Style> = emptySet(), - override val children: List<NavigationNode> -) : WithChildren<NavigationNode> - -/** - * [CLASS] represents a neutral (a.k.a Java-style) icon, - * whereas [CLASS_KT] should be Kotlin-styled - */ -public enum class NavigationNodeIcon( - private val cssClass: String -) { - CLASS("class"), - CLASS_KT("class-kt"), - ABSTRACT_CLASS("abstract-class"), - ABSTRACT_CLASS_KT("abstract-class-kt"), - ENUM_CLASS("enum-class"), - ENUM_CLASS_KT("enum-class-kt"), - ANNOTATION_CLASS("annotation-class"), - ANNOTATION_CLASS_KT("annotation-class-kt"), - INTERFACE("interface"), - INTERFACE_KT("interface-kt"), - FUNCTION("function"), - EXCEPTION("exception-class"), - OBJECT("object"), - TYPEALIAS_KT("typealias-kt"), - VAL("val"), - VAR("var"); - - internal fun style(): String = "nav-icon $cssClass" -} - -public fun NavigationPage.transform(block: (NavigationNode) -> NavigationNode): NavigationPage = - NavigationPage(root.transform(block), moduleName, context) - -public fun NavigationNode.transform(block: (NavigationNode) -> NavigationNode): NavigationNode = - run(block).let { NavigationNode(it.name, it.dri, it.sourceSets, it.icon, it.styles, it.children.map(block)) } diff --git a/plugins/base/src/main/kotlin/renderers/html/SearchbarDataInstaller.kt b/plugins/base/src/main/kotlin/renderers/html/SearchbarDataInstaller.kt deleted file mode 100644 index 83d4b24f..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/SearchbarDataInstaller.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html - -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import org.jetbrains.dokka.Platform -import org.jetbrains.dokka.base.renderers.sourceSets -import org.jetbrains.dokka.base.templating.AddToSearch -import org.jetbrains.dokka.links.DRI -import org.jetbrains.dokka.model.DisplaySourceSet -import org.jetbrains.dokka.model.dfs -import org.jetbrains.dokka.model.withDescendants -import org.jetbrains.dokka.pages.* -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.transformers.pages.PageTransformer - -public data class SearchRecord( - val name: String, - val description: String? = null, - val location: String, - val searchKeys: List<String> = listOf(name) -) { - public companion object -} - -public open class SearchbarDataInstaller( - public val context: DokkaContext -) : PageTransformer { - - public data class DRIWithSourceSets(val dri: DRI, val sourceSet: Set<DisplaySourceSet>) - - public data class SignatureWithId(val driWithSourceSets: DRIWithSourceSets, val displayableSignature: String) { - public constructor(dri: DRI, page: ContentPage) : this( DRIWithSourceSets(dri, page.sourceSets()), - getSymbolSignature(page, dri)?.let { flattenToText(it) } ?: page.name) - - val id: String - get() = with(driWithSourceSets.dri) { - listOfNotNull( - packageName?.takeIf { it.isNotBlank() }, - classNames, - callable?.name - ).joinToString(".") - } - } - - private val mapper = jacksonObjectMapper() - - public open fun generatePagesList( - pages: List<SignatureWithId>, - locationResolver: DriResolver - ): List<SearchRecord> = - pages.map { pageWithId -> - createSearchRecord( - name = pageWithId.displayableSignature, - description = pageWithId.id, - location = resolveLocation(locationResolver, pageWithId.driWithSourceSets).orEmpty(), - searchKeys = listOf( - pageWithId.id.substringAfterLast("."), - pageWithId.displayableSignature, - pageWithId.id, - ) - ) - }.sortedWith(compareBy({ it.name }, { it.description })) - - public open fun createSearchRecord( - name: String, - description: String?, - location: String, - searchKeys: List<String> - ): SearchRecord = - SearchRecord(name, description, location, searchKeys) - - public open fun processPage(page: PageNode): List<SignatureWithId> = - when (page) { - is ContentPage -> page.takeIf { page !is ModulePageNode && page !is PackagePageNode }?.dri - ?.map { dri -> SignatureWithId(dri, page) }.orEmpty() - else -> emptyList() - } - - private fun resolveLocation(locationResolver: DriResolver, driWithSourceSets: DRIWithSourceSets): String? = - locationResolver(driWithSourceSets.dri, driWithSourceSets.sourceSet).also { location -> - if (location.isNullOrBlank()) context.logger.warn("Cannot resolve path for ${driWithSourceSets.dri}") - } - - override fun invoke(input: RootPageNode): RootPageNode { - val signatureWithIds = input.withDescendants().fold(emptyList<SignatureWithId>()) { pageList, page -> - pageList + processPage(page) - } - val page = RendererSpecificResourcePage( - name = "scripts/pages.json", - children = emptyList(), - strategy = RenderingStrategy.DriLocationResolvableWrite { resolver -> - val content = signatureWithIds.run { - generatePagesList(this, resolver) - } - - if (context.configuration.delayTemplateSubstitution) { - mapper.writeValueAsString(AddToSearch(context.configuration.moduleName, content)) - } else { - mapper.writeValueAsString(content) - } - }) - - return input.modified(children = input.children + page) - } -} - -private fun getSymbolSignature(page: ContentPage, dri: DRI) = - page.content.dfs { it.dci.kind == ContentKind.Symbol && it.dci.dri.contains(dri) } - -private fun flattenToText(node: ContentNode): String { - fun getContentTextNodes(node: ContentNode, sourceSetRestriction: DisplaySourceSet): List<ContentText> = - when (node) { - is ContentText -> listOf(node) - is ContentComposite -> node.children - .filter { sourceSetRestriction in it.sourceSets } - .flatMap { getContentTextNodes(it, sourceSetRestriction) } - .takeIf { node.dci.kind != ContentKind.Annotations && node.dci.kind != ContentKind.Source } - .orEmpty() - else -> emptyList() - } - - val sourceSetRestriction = - node.sourceSets.find { it.platform == Platform.common } ?: node.sourceSets.first() - return getContentTextNodes(node, sourceSetRestriction).joinToString("") { it.text } -} diff --git a/plugins/base/src/main/kotlin/renderers/html/Tags.kt b/plugins/base/src/main/kotlin/renderers/html/Tags.kt deleted file mode 100644 index 7d6fc390..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/Tags.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html - -import kotlinx.html.* -import kotlinx.html.stream.createHTML -import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer -import org.jetbrains.dokka.base.templating.Command -import org.jetbrains.dokka.base.templating.toJsonString - -public typealias TemplateBlock = TemplateCommand.() -> Unit - -@HtmlTagMarker -public fun FlowOrPhrasingContent.wbr(classes: String? = null, block: WBR.() -> Unit = {}): Unit = - WBR(attributesMapOf("class", classes), consumer).visit(block) - -@Suppress("unused") -public open class WBR(initialAttributes: Map<String, String>, consumer: TagConsumer<*>) : - HTMLTag("wbr", consumer, initialAttributes, namespace = null, inlineTag = true, emptyTag = false), - HtmlBlockInlineTag - -/** - * Work-around until next version of kotlinx.html doesn't come out - */ -@HtmlTagMarker -public inline fun FlowOrPhrasingContent.strike(classes : String? = null, crossinline block : STRIKE.() -> Unit = {}) : Unit = STRIKE(attributesMapOf("class", classes), consumer).visit(block) - -public open class STRIKE(initialAttributes: Map<String, String>, override val consumer: TagConsumer<*>) : - HTMLTag("strike", consumer, initialAttributes, null, false, false), HtmlBlockInlineTag - -@HtmlTagMarker -public inline fun FlowOrPhrasingContent.underline(classes : String? = null, crossinline block : UNDERLINE.() -> Unit = {}) : Unit = UNDERLINE(attributesMapOf("class", classes), consumer).visit(block) - -public open class UNDERLINE(initialAttributes: Map<String, String>, override val consumer: TagConsumer<*>) : - HTMLTag("u", consumer, initialAttributes, null, false, false), HtmlBlockInlineTag - -public const val TEMPLATE_COMMAND_SEPARATOR: String = ":" -public const val TEMPLATE_COMMAND_BEGIN_BORDER: String = "[+]cmd" -public const val TEMPLATE_COMMAND_END_BORDER: String = "[-]cmd" - -public fun FlowOrMetaDataContent.templateCommandAsHtmlComment(data: Command, block: FlowOrMetaDataContent.() -> Unit = {}): Unit = - (consumer as? ImmediateResolutionTagConsumer)?.processCommand(data, block) - ?: let{ - comment( "$TEMPLATE_COMMAND_BEGIN_BORDER$TEMPLATE_COMMAND_SEPARATOR${toJsonString(data)}") - block() - comment(TEMPLATE_COMMAND_END_BORDER) - } - -public fun <T: Appendable> T.templateCommandAsHtmlComment(command: Command, action: T.() -> Unit ) { - append("<!--$TEMPLATE_COMMAND_BEGIN_BORDER$TEMPLATE_COMMAND_SEPARATOR${toJsonString(command)}-->") - action() - append("<!--$TEMPLATE_COMMAND_END_BORDER-->") -} - -public fun FlowOrMetaDataContent.templateCommand(data: Command, block: TemplateBlock = {}): Unit = - (consumer as? ImmediateResolutionTagConsumer)?.processCommand(data, block) - ?: TemplateCommand(attributesMapOf("data", toJsonString(data)), consumer).visit(block) - -public fun <T> TagConsumer<T>.templateCommand(data: Command, block: TemplateBlock = {}): T = - (this as? ImmediateResolutionTagConsumer)?.processCommandAndFinalize(data, block) - ?: TemplateCommand(attributesMapOf("data", toJsonString(data)), this).visitAndFinalize(this, block) - -public fun templateCommandFor(data: Command, consumer: TagConsumer<*>): TemplateCommand = - TemplateCommand(attributesMapOf("data", toJsonString(data)), consumer) - -public class TemplateCommand(initialAttributes: Map<String, String>, consumer: TagConsumer<*>) : - HTMLTag( - "dokka-template-command", - consumer, - initialAttributes, - namespace = null, - inlineTag = true, - emptyTag = false - ), - CommonAttributeGroupFacadeFlowInteractivePhrasingContent - -// This hack is outrageous. I hate it but I cannot find any other way around `kotlinx.html` type system. -public fun TemplateBlock.buildAsInnerHtml(): String = createHTML(prettyPrint = false).run { - TemplateCommand(emptyMap, this).visitAndFinalize(this, this@buildAsInnerHtml).substringAfter(">").substringBeforeLast("<") -} diff --git a/plugins/base/src/main/kotlin/renderers/html/command/consumers/ImmediateResolutionTagConsumer.kt b/plugins/base/src/main/kotlin/renderers/html/command/consumers/ImmediateResolutionTagConsumer.kt deleted file mode 100644 index 9cde1fca..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/command/consumers/ImmediateResolutionTagConsumer.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html.command.consumers - -import kotlinx.html.TagConsumer -import kotlinx.html.visit -import org.jetbrains.dokka.base.DokkaBase -import org.jetbrains.dokka.base.renderers.html.TemplateBlock -import org.jetbrains.dokka.base.renderers.html.templateCommand -import org.jetbrains.dokka.base.renderers.html.templateCommandFor -import org.jetbrains.dokka.base.templating.Command -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.plugability.plugin -import org.jetbrains.dokka.plugability.query - -public class ImmediateResolutionTagConsumer<out R>( - private val downstream: TagConsumer<R>, - private val context: DokkaContext -): TagConsumer<R> by downstream { - - public fun processCommand(command: Command, block: TemplateBlock) { - context.plugin<DokkaBase>().query { immediateHtmlCommandConsumer } - .find { it.canProcess(command) } - ?.processCommand(command, block, this) - ?: run { templateCommandFor(command, downstream).visit(block) } - } - - public fun processCommandAndFinalize(command: Command, block: TemplateBlock): R { - return context.plugin<DokkaBase>().query { immediateHtmlCommandConsumer } - .find { it.canProcess(command) } - ?.processCommandAndFinalize(command, block, this) - ?: downstream.templateCommand(command, block) - } -} - diff --git a/plugins/base/src/main/kotlin/renderers/html/command/consumers/PathToRootConsumer.kt b/plugins/base/src/main/kotlin/renderers/html/command/consumers/PathToRootConsumer.kt deleted file mode 100644 index 9ac6eb91..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/command/consumers/PathToRootConsumer.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html.command.consumers - -import org.jetbrains.dokka.base.renderers.html.TemplateBlock -import org.jetbrains.dokka.base.renderers.html.buildAsInnerHtml -import org.jetbrains.dokka.base.templating.Command -import org.jetbrains.dokka.base.templating.ImmediateHtmlCommandConsumer -import org.jetbrains.dokka.base.templating.PathToRootSubstitutionCommand - -public object PathToRootConsumer: ImmediateHtmlCommandConsumer { - override fun canProcess(command: Command): Boolean = command is PathToRootSubstitutionCommand - - override fun <R> processCommand(command: Command, block: TemplateBlock, tagConsumer: ImmediateResolutionTagConsumer<R>) { - command as PathToRootSubstitutionCommand - tagConsumer.onTagContentUnsafe { +block.buildAsInnerHtml().replace(command.pattern, command.default) } - } - - override fun <R> processCommandAndFinalize(command: Command, block: TemplateBlock, tagConsumer: ImmediateResolutionTagConsumer<R>): R { - processCommand(command, block, tagConsumer) - return tagConsumer.finalize() - } - -} diff --git a/plugins/base/src/main/kotlin/renderers/html/command/consumers/ReplaceVersionsConsumer.kt b/plugins/base/src/main/kotlin/renderers/html/command/consumers/ReplaceVersionsConsumer.kt deleted file mode 100644 index dd95c202..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/command/consumers/ReplaceVersionsConsumer.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html.command.consumers - -import org.jetbrains.dokka.base.renderers.html.TemplateBlock -import org.jetbrains.dokka.base.templating.Command -import org.jetbrains.dokka.base.templating.ImmediateHtmlCommandConsumer -import org.jetbrains.dokka.base.templating.ReplaceVersionsCommand -import org.jetbrains.dokka.plugability.DokkaContext - -public class ReplaceVersionsConsumer(private val context: DokkaContext) : ImmediateHtmlCommandConsumer { - override fun canProcess(command: Command): Boolean = command is ReplaceVersionsCommand - - override fun <R> processCommand( - command: Command, - block: TemplateBlock, - tagConsumer: ImmediateResolutionTagConsumer<R> - ) { - command as ReplaceVersionsCommand - tagConsumer.onTagContentUnsafe { +context.configuration.moduleVersion.orEmpty() } - } - - override fun <R> processCommandAndFinalize(command: Command, block: TemplateBlock, tagConsumer: ImmediateResolutionTagConsumer<R>): R { - processCommand(command, block, tagConsumer) - return tagConsumer.finalize() - } -} diff --git a/plugins/base/src/main/kotlin/renderers/html/command/consumers/ResolveLinkConsumer.kt b/plugins/base/src/main/kotlin/renderers/html/command/consumers/ResolveLinkConsumer.kt deleted file mode 100644 index 292e88b0..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/command/consumers/ResolveLinkConsumer.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html.command.consumers - -import kotlinx.html.SPAN -import kotlinx.html.span -import kotlinx.html.unsafe -import kotlinx.html.visit -import org.jetbrains.dokka.base.renderers.html.TemplateBlock -import org.jetbrains.dokka.base.renderers.html.buildAsInnerHtml -import org.jetbrains.dokka.base.templating.Command -import org.jetbrains.dokka.base.templating.ImmediateHtmlCommandConsumer -import org.jetbrains.dokka.base.templating.ResolveLinkCommand -import org.jetbrains.dokka.utilities.htmlEscape - -public object ResolveLinkConsumer: ImmediateHtmlCommandConsumer { - override fun canProcess(command: Command): Boolean = command is ResolveLinkCommand - - override fun <R> processCommand(command: Command, block: TemplateBlock, tagConsumer: ImmediateResolutionTagConsumer<R>) { - command as ResolveLinkCommand - SPAN(mapOf("data-unresolved-link" to command.dri.toString().htmlEscape()), tagConsumer).visit { - unsafe { block.buildAsInnerHtml() } - } - } - - override fun <R> processCommandAndFinalize(command: Command, block: TemplateBlock, tagConsumer: ImmediateResolutionTagConsumer<R>): R { - command as ResolveLinkCommand - return tagConsumer.span { - attributes["data-unresolved-link"] = command.dri.toString().htmlEscape() - } - } -} diff --git a/plugins/base/src/main/kotlin/renderers/html/htmlFormatingUtils.kt b/plugins/base/src/main/kotlin/renderers/html/htmlFormatingUtils.kt deleted file mode 100644 index b6ce4147..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/htmlFormatingUtils.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html - -import kotlinx.html.FlowContent -import kotlinx.html.span - -public fun FlowContent.buildTextBreakableAfterCapitalLetters(name: String, hasLastElement: Boolean = false) { - if (name.contains(" ")) { - val withOutSpaces = name.split(" ") - withOutSpaces.dropLast(1).forEach { - buildBreakableText(it) - +" " - } - buildBreakableText(withOutSpaces.last()) - } else { - val content = name.replace(Regex("(?<=[a-z])([A-Z])"), " $1").split(" ") - joinToHtml(content, hasLastElement) { - it - } - } -} - -public fun FlowContent.buildBreakableDotSeparatedHtml(name: String) { - val phrases = name.split(".") - phrases.forEachIndexed { i, e -> - val elementWithOptionalDot = e.takeIf { i == phrases.lastIndex } ?: "$e." - if (e.length > 10) { - buildTextBreakableAfterCapitalLetters(elementWithOptionalDot, hasLastElement = i == phrases.lastIndex) - } else { - buildBreakableHtmlElement(elementWithOptionalDot, i == phrases.lastIndex) - } - } -} - -private fun FlowContent.joinToHtml(elements: List<String>, hasLastElement: Boolean = true, onEach: (String) -> String) { - elements.dropLast(1).forEach { - buildBreakableHtmlElement(onEach(it)) - } - elements.takeIf { it.isNotEmpty() && it.last().isNotEmpty() }?.let { - if (hasLastElement) { - span { - buildBreakableHtmlElement(it.last(), last = true) - } - } else { - buildBreakableHtmlElement(it.last(), last = false) - } - } -} - -private fun FlowContent.buildBreakableHtmlElement(element: String, last: Boolean = false) { - element.takeIf { it.isNotBlank() }?.let { - span { - +it - } - } - if (!last) { - wbr { } - } -} - -public fun FlowContent.buildBreakableText(name: String) { - if (name.contains(".")) buildBreakableDotSeparatedHtml(name) - else buildTextBreakableAfterCapitalLetters(name, hasLastElement = true) -} diff --git a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt deleted file mode 100644 index dad013e2..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html - -import org.jetbrains.dokka.base.DokkaBase -import org.jetbrains.dokka.base.DokkaBaseConfiguration -import org.jetbrains.dokka.base.templating.AddToSourcesetDependencies -import org.jetbrains.dokka.base.templating.toJsonString -import org.jetbrains.dokka.pages.RendererSpecificResourcePage -import org.jetbrains.dokka.pages.RenderingStrategy -import org.jetbrains.dokka.pages.RootPageNode -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.plugability.configuration -import org.jetbrains.dokka.transformers.pages.PageTransformer - -public open class NavigationPageInstaller( - public val context: DokkaContext -) : NavigationDataProvider(context), PageTransformer { - override fun invoke(input: RootPageNode): RootPageNode = - input.modified( - children = input.children + NavigationPage( - root = navigableChildren(input), - moduleName = context.configuration.moduleName, - context = context - ) - ) -} - -public class CustomResourceInstaller( - public val dokkaContext: DokkaContext -) : PageTransformer { - private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(dokkaContext) - - private val customAssets = configuration?.customAssets?.map { - RendererSpecificResourcePage("images/${it.name}", emptyList(), RenderingStrategy.Copy(it.absolutePath)) - }.orEmpty() - - private val customStylesheets = configuration?.customStyleSheets?.map { - RendererSpecificResourcePage("styles/${it.name}", emptyList(), RenderingStrategy.Copy(it.absolutePath)) - }.orEmpty() - - override fun invoke(input: RootPageNode): RootPageNode { - val customResourcesPaths = (customAssets + customStylesheets).map { it.name }.toSet() - val withEmbeddedResources = - input.transformContentPagesTree { it.modified(embeddedResources = it.embeddedResources + customResourcesPaths) } - if(dokkaContext.configuration.delayTemplateSubstitution) - return withEmbeddedResources - val (currentResources, otherPages) = withEmbeddedResources.children.partition { it is RendererSpecificResourcePage } - return input.modified(children = otherPages + currentResources.filterNot { it.name in customResourcesPaths } + customAssets + customStylesheets) - } -} - -public class ScriptsInstaller(private val dokkaContext: DokkaContext) : PageTransformer { - - // scripts ending with `_deferred.js` are loaded with `defer`, otherwise `async` - private val scriptsPages = listOf( - "scripts/clipboard.js", - "scripts/navigation-loader.js", - "scripts/platform-content-handler.js", - "scripts/main.js", - "scripts/prism.js", - - // It's important for this script to be deferred because it has logic that makes decisions based on - // rendered elements (for instance taking their clientWidth), and if not all styles are loaded/applied - // at the time of inspecting them, it will give incorrect results and might lead to visual bugs. - // should be easy to test if you open any page in incognito or by reloading it (Ctrl+Shift+R) - "scripts/symbol-parameters-wrapper_deferred.js", - ) - - override fun invoke(input: RootPageNode): RootPageNode = - input.let { root -> - if (dokkaContext.configuration.delayTemplateSubstitution) root - else root.modified(children = input.children + scriptsPages.toRenderSpecificResourcePage()) - }.transformContentPagesTree { - it.modified( - embeddedResources = it.embeddedResources + scriptsPages - ) - } -} - -public class StylesInstaller(private val dokkaContext: DokkaContext) : PageTransformer { - private val stylesPages = listOf( - "styles/style.css", - "styles/main.css", - "styles/prism.css", - "styles/logo-styles.css", - "styles/font-jb-sans-auto.css" - ) - - override fun invoke(input: RootPageNode): RootPageNode = - input.let { root -> - if (dokkaContext.configuration.delayTemplateSubstitution) root - else root.modified(children = input.children + stylesPages.toRenderSpecificResourcePage()) - }.transformContentPagesTree { - it.modified( - embeddedResources = it.embeddedResources + stylesPages - ) - } -} - -public object AssetsInstaller : PageTransformer { - private val imagesPages = listOf( - "images/arrow_down.svg", - "images/logo-icon.svg", - "images/go-to-top-icon.svg", - "images/footer-go-to-link.svg", - "images/anchor-copy-button.svg", - "images/copy-icon.svg", - "images/copy-successful-icon.svg", - "images/theme-toggle.svg", - "images/burger.svg", - "images/homepage.svg", - - // navigation icons - "images/nav-icons/abstract-class.svg", - "images/nav-icons/abstract-class-kotlin.svg", - "images/nav-icons/annotation.svg", - "images/nav-icons/annotation-kotlin.svg", - "images/nav-icons/class.svg", - "images/nav-icons/class-kotlin.svg", - "images/nav-icons/enum.svg", - "images/nav-icons/enum-kotlin.svg", - "images/nav-icons/exception-class.svg", - "images/nav-icons/field-value.svg", - "images/nav-icons/field-variable.svg", - "images/nav-icons/function.svg", - "images/nav-icons/interface.svg", - "images/nav-icons/interface-kotlin.svg", - "images/nav-icons/object.svg", - "images/nav-icons/typealias-kotlin.svg", - ) - - override fun invoke(input: RootPageNode): RootPageNode = input.modified( - children = input.children + imagesPages.toRenderSpecificResourcePage() - ) -} - -private fun List<String>.toRenderSpecificResourcePage(): List<RendererSpecificResourcePage> = - map { RendererSpecificResourcePage(it, emptyList(), RenderingStrategy.Copy("/dokka/$it")) } - -public class SourcesetDependencyAppender( - public val context: DokkaContext -) : PageTransformer { - private val name = "scripts/sourceset_dependencies.js" - override fun invoke(input: RootPageNode): RootPageNode { - val dependenciesMap = context.configuration.sourceSets.associate { - it.sourceSetID to it.dependentSourceSets - } - - fun createDependenciesJson(): String = - dependenciesMap.map { (key, values) -> key.toString() to values.map { it.toString() } }.toMap() - .let { content -> - if (context.configuration.delayTemplateSubstitution) { - toJsonString(AddToSourcesetDependencies(context.configuration.moduleName, content)) - } else { - "sourceset_dependencies='${toJsonString(content)}'" - } - } - - val deps = RendererSpecificResourcePage( - name = name, - children = emptyList(), - strategy = RenderingStrategy.Write(createDependenciesJson()) - ) - - return input.modified( - children = input.children + deps - ).transformContentPagesTree { it.modified(embeddedResources = it.embeddedResources + name) } - } -} diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt deleted file mode 100644 index fe6f0089..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelFactory.kt +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html.innerTemplating - -import freemarker.core.Environment -import freemarker.template.* -import kotlinx.html.* -import kotlinx.html.stream.createHTML -import org.jetbrains.dokka.DokkaConfiguration -import org.jetbrains.dokka.base.DokkaBase -import org.jetbrains.dokka.base.DokkaBaseConfiguration -import org.jetbrains.dokka.base.renderers.URIExtension -import org.jetbrains.dokka.base.renderers.html.TEMPLATE_REPLACEMENT -import org.jetbrains.dokka.base.renderers.html.command.consumers.ImmediateResolutionTagConsumer -import org.jetbrains.dokka.base.renderers.html.templateCommand -import org.jetbrains.dokka.base.renderers.html.templateCommandAsHtmlComment -import org.jetbrains.dokka.base.renderers.isImage -import org.jetbrains.dokka.base.resolvers.local.LocationProvider -import org.jetbrains.dokka.base.templating.PathToRootSubstitutionCommand -import org.jetbrains.dokka.base.templating.ProjectNameSubstitutionCommand -import org.jetbrains.dokka.base.templating.ReplaceVersionsCommand -import org.jetbrains.dokka.base.templating.SubstitutionCommand -import org.jetbrains.dokka.model.DisplaySourceSet -import org.jetbrains.dokka.model.withDescendants -import org.jetbrains.dokka.pages.ContentPage -import org.jetbrains.dokka.pages.PageNode -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.plugability.configuration -import java.net.URI - -public class DefaultTemplateModelFactory( - public val context: DokkaContext -) : TemplateModelFactory { - private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(context) - private val isPartial = context.configuration.delayTemplateSubstitution - - private fun <R> TagConsumer<R>.prepareForTemplates() = - if (context.configuration.delayTemplateSubstitution || this is ImmediateResolutionTagConsumer) this - else ImmediateResolutionTagConsumer(this, context) - - public data class SourceSetModel(val name: String, val platform: String, val filter: String) - - override fun buildModel( - page: PageNode, - resources: List<String>, - locationProvider: LocationProvider, - content: String - ): TemplateMap { - val path = locationProvider.resolve(page) - val pathToRoot = locationProvider.pathToRoot(page) - val mapper = mutableMapOf<String, Any>() - mapper["pageName"] = page.name - mapper["resources"] = PrintDirective { - val sb = StringBuilder() - if (isPartial) - sb.templateCommandAsHtmlComment( - PathToRootSubstitutionCommand( - TEMPLATE_REPLACEMENT, - default = pathToRoot - ) - ) { resourcesForPage(TEMPLATE_REPLACEMENT, resources) } - else - sb.resourcesForPage(pathToRoot, resources) - sb.toString() - } - mapper["content"] = PrintDirective { content } - mapper["version"] = PrintDirective { - createHTML().prepareForTemplates().templateCommand(ReplaceVersionsCommand(path.orEmpty())) - } - mapper["template_cmd"] = TemplateDirective(context.configuration, pathToRoot) - - if (page is ContentPage) { - val sourceSets = page.content.withDescendants() - .flatMap { it.sourceSets } - .distinct() - .sortedBy { it.comparableKey } - .map { SourceSetModel(it.name, it.platform.key, it.sourceSetIDs.merged.toString()) } - .toList() - - if (sourceSets.isNotEmpty()) { - mapper["sourceSets"] = sourceSets - } - } - return mapper - } - - override fun buildSharedModel(): TemplateMap { - val mapper = mutableMapOf<String, Any>() - - mapper["footerMessage"] = - (configuration?.footerMessage?.takeIf(String::isNotBlank) ?: DokkaBaseConfiguration.defaultFooterMessage) - - configuration?.homepageLink?.takeIf(String::isNotBlank)?.let { mapper["homepageLink"] = it } - - return mapper - } - - private val DisplaySourceSet.comparableKey - get() = sourceSetIDs.merged.let { it.scopeId + it.sourceSetName } - private val String.isAbsolute: Boolean - get() = URI(this).isAbsolute - - private fun Appendable.resourcesForPage(pathToRoot: String, resources: List<String>): Unit = - resources.forEach { resource -> - - val resourceHtml = with(createHTML()) { - when { - - resource.URIExtension == "css" -> - link( - rel = LinkRel.stylesheet, - href = if (resource.isAbsolute) resource else "$pathToRoot$resource" - ) - - resource.URIExtension == "js" -> - script( - type = ScriptType.textJavaScript, - src = if (resource.isAbsolute) resource else "$pathToRoot$resource" - ) { - if (resource == "scripts/main.js" || resource.endsWith("_deferred.js")) - defer = true - else - async = true - } - - resource.isImage() -> link(href = if (resource.isAbsolute) resource else "$pathToRoot$resource") - else -> null - } - } - if (resourceHtml != null) { - append(resourceHtml) - } - } - -} - -private class PrintDirective(val generateData: () -> String) : TemplateDirectiveModel { - override fun execute( - env: Environment, - params: MutableMap<Any?, Any?>?, - loopVars: Array<TemplateModel>?, - body: TemplateDirectiveBody? - ) { - if (params?.isNotEmpty() == true) throw TemplateModelException( - "Parameters are not allowed" - ) - if (loopVars?.isNotEmpty() == true) throw TemplateModelException( - "Loop variables are not allowed" - ) - env.out.write(generateData()) - } -} - -private class TemplateDirective( - val configuration: DokkaConfiguration, - val pathToRoot: String -) : TemplateDirectiveModel { - override fun execute( - env: Environment, - params: MutableMap<Any?, Any?>?, - loopVars: Array<TemplateModel>?, - body: TemplateDirectiveBody? - ) { - val commandName = params?.get(PARAM_NAME) ?: throw TemplateModelException( - "The required $PARAM_NAME parameter is missing." - ) - val replacement = (params[PARAM_REPLACEMENT] as? SimpleScalar)?.asString ?: TEMPLATE_REPLACEMENT - - when ((commandName as? SimpleScalar)?.asString) { - "pathToRoot" -> { - body ?: throw TemplateModelException( - "No directive body for $commandName command." - ) - executeSubstituteCommand( - PathToRootSubstitutionCommand( - replacement, pathToRoot - ), - "pathToRoot", - pathToRoot, - Context(env, body) - ) - } - - "projectName" -> { - body ?: throw TemplateModelException( - "No directive body $commandName command." - ) - executeSubstituteCommand( - ProjectNameSubstitutionCommand( - replacement, configuration.moduleName - ), - "projectName", - configuration.moduleName, - Context(env, body) - ) - } - - else -> throw TemplateModelException( - "The parameter $PARAM_NAME $commandName is unknown" - ) - } - } - - private data class Context(val env: Environment, val body: TemplateDirectiveBody) - - private fun executeSubstituteCommand( - command: SubstitutionCommand, - name: String, - value: String, - ctx: Context - ) { - if (configuration.delayTemplateSubstitution) - ctx.env.out.templateCommandAsHtmlComment(command) { - renderWithLocalVar(name, command.pattern, ctx) - } - else { - renderWithLocalVar(name, value, ctx) - } - } - - private fun renderWithLocalVar(name: String, value: String, ctx: Context) = - with(ctx) { - env.setVariable(name, SimpleScalar(value)) - body.render(env.out) - env.setVariable(name, null) - } - - companion object { - const val PARAM_NAME = "name" - const val PARAM_REPLACEMENT = "replacement" - } -} diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt deleted file mode 100644 index 2f17183d..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/DefaultTemplateModelMerger.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html.innerTemplating - -public class DefaultTemplateModelMerger : TemplateModelMerger { - override fun invoke( - factories: List<TemplateModelFactory>, - buildModel: TemplateModelFactory.() -> TemplateMap - ): TemplateMap { - val mapper = mutableMapOf<String, Any?>() - factories.map(buildModel).forEach { partialModel -> - partialModel.forEach { (k, v) -> - mapper[k] = v - } - } - return mapper - } -} diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt deleted file mode 100644 index 1638c9c0..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/HtmlTemplater.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html.innerTemplating - -import freemarker.cache.ClassTemplateLoader -import freemarker.cache.FileTemplateLoader -import freemarker.cache.MultiTemplateLoader -import freemarker.log.Logger -import freemarker.template.Configuration -import freemarker.template.TemplateExceptionHandler -import org.jetbrains.dokka.base.DokkaBase -import org.jetbrains.dokka.base.DokkaBaseConfiguration -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.plugability.configuration -import java.io.StringWriter - - -public enum class DokkaTemplateTypes( - public val path: String -) { - BASE("base.ftl") -} - -public typealias TemplateMap = Map<String, Any?> - -public class HtmlTemplater( - context: DokkaContext -) { - - init { - // to disable logging, but it isn't reliable see [Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY] - // (use SLF4j further) - System.setProperty( - Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY, - System.getProperty(Logger.SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY) ?: Logger.LIBRARY_NAME_NONE - ) - } - - private val configuration = configuration<DokkaBase, DokkaBaseConfiguration>(context) - private val templaterConfiguration = - Configuration(Configuration.VERSION_2_3_31).apply { configureTemplateEngine() } - - private fun Configuration.configureTemplateEngine() { - val loaderFromResources = ClassTemplateLoader(javaClass, "/dokka/templates") - templateLoader = configuration?.templatesDir?.let { - MultiTemplateLoader( - arrayOf( - FileTemplateLoader(it), - loaderFromResources - ) - ) - } ?: loaderFromResources - - unsetLocale() - defaultEncoding = "UTF-8" - templateExceptionHandler = TemplateExceptionHandler.RETHROW_HANDLER - logTemplateExceptions = false - wrapUncheckedExceptions = true - fallbackOnNullLoopVariable = false - templateUpdateDelayMilliseconds = Long.MAX_VALUE - } - - public fun setupSharedModel(model: TemplateMap) { - templaterConfiguration.setSharedVariables(model) - } - - public fun renderFromTemplate( - templateType: DokkaTemplateTypes, - generateModel: () -> TemplateMap - ): String { - val out = StringWriter() - // Freemarker has own thread-safe cache to keep templates - val template = templaterConfiguration.getTemplate(templateType.path) - val model = generateModel() - template.process(model, out) - - return out.toString() - } -} - diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt deleted file mode 100644 index 3af11bf9..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelFactory.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html.innerTemplating - -import org.jetbrains.dokka.base.resolvers.local.LocationProvider -import org.jetbrains.dokka.pages.PageNode - -public interface TemplateModelFactory { - public fun buildModel( - page: PageNode, - resources: List<String>, - locationProvider: LocationProvider, - content: String - ): TemplateMap - - public fun buildSharedModel(): TemplateMap -} diff --git a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelMerger.kt b/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelMerger.kt deleted file mode 100644 index ada0c6cd..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/innerTemplating/TemplateModelMerger.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html.innerTemplating - -public fun interface TemplateModelMerger { - public fun invoke(factories: List<TemplateModelFactory>, buildModel: TemplateModelFactory.() -> TemplateMap): TemplateMap -} diff --git a/plugins/base/src/main/kotlin/renderers/html/shouldRenderSourceSetBubbles.kt b/plugins/base/src/main/kotlin/renderers/html/shouldRenderSourceSetBubbles.kt deleted file mode 100644 index a7bafadb..00000000 --- a/plugins/base/src/main/kotlin/renderers/html/shouldRenderSourceSetBubbles.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.renderers.html - -import org.jetbrains.dokka.model.withDescendants -import org.jetbrains.dokka.pages.ContentPage -import org.jetbrains.dokka.pages.RootPageNode - -internal fun shouldRenderSourceSetTabs(page: RootPageNode): Boolean { - return page.withDescendants() - .flatMap { pageNode -> - if (pageNode is ContentPage) pageNode.content.withDescendants() - else emptySequence() - } - .flatMap { contentNode -> contentNode.sourceSets } - .distinct() - .count() > 1 -} |