diff --git a/pkl-doc/pkl-doc.gradle.kts b/pkl-doc/pkl-doc.gradle.kts index 449d0c5e..75e5028f 100644 --- a/pkl-doc/pkl-doc.gradle.kts +++ b/pkl-doc/pkl-doc.gradle.kts @@ -69,6 +69,15 @@ publishing { } } +tasks.test { + inputs.dir("src/test/files/DocGeneratorTest/input") + inputs.dir("src/test/files/DocGeneratorTest/output") + inputs.dir("src/test/files/DocMigratorTest/input") + inputs.dir("src/test/files/DocMigratorTest/output") + inputs.dir("src/test/files/SinglePackageTest/input") + inputs.dir("src/test/files/SinglePackageTest/output") +} + val testNativeExecutable by tasks.registering(Test::class) { dependsOn(tasks.assembleNative) @@ -76,7 +85,7 @@ val testNativeExecutable by classpath = sourceSets.test.get().runtimeClasspath inputs.dir("src/test/files/DocGeneratorTest/input") - outputs.dir("src/test/files/DocGeneratorTest/output") + inputs.dir("src/test/files/DocGeneratorTest/output") systemProperty("org.pkl.doc.NativeExecutableTest", "true") filter { includeTestsMatching("org.pkl.doc.NativeExecutableTest") } @@ -89,7 +98,7 @@ val testJavaExecutable by dependsOn(tasks.javaExecutable) inputs.dir("src/test/files/DocGeneratorTest/input") - outputs.dir("src/test/files/DocGeneratorTest/output") + inputs.dir("src/test/files/DocGeneratorTest/output") systemProperty("org.pkl.doc.JavaExecutableTest", "true") filter { includeTestsMatching("org.pkl.doc.JavaExecutableTest") } @@ -105,4 +114,6 @@ tasks.jar { manifest { attributes += mapOf("Main-Class" to "org.pkl.doc.Main") } htmlValidator { sources = files("src/test/files/DocGeneratorTest/output") } -tasks.validateHtml { mustRunAfter(testJavaExecutable) } +// Tests usually read expected output files, but may write missing ones before failing. +// If that happens, delay validation until after any test tasks in the graph. +tasks.validateHtml { mustRunAfter(tasks.test, testJavaExecutable, testNativeExecutable) } diff --git a/pkl-doc/src/main/kotlin/org/pkl/doc/ClassPageGenerator.kt b/pkl-doc/src/main/kotlin/org/pkl/doc/ClassPageGenerator.kt index 42d4cbac..d14e1ef4 100644 --- a/pkl-doc/src/main/kotlin/org/pkl/doc/ClassPageGenerator.kt +++ b/pkl-doc/src/main/kotlin/org/pkl/doc/ClassPageGenerator.kt @@ -26,8 +26,17 @@ internal class ClassPageGenerator( clazz: PClass, pageScope: ClassScope, isTestMode: Boolean, + isSinglePackageSite: Boolean, consoleOut: OutputStream, -) : ModuleOrClassPageGenerator(docsiteInfo, clazz, pageScope, isTestMode, consoleOut) { +) : + ModuleOrClassPageGenerator( + docsiteInfo, + clazz, + pageScope, + isTestMode, + isSinglePackageSite, + consoleOut, + ) { override val html: HTML.() -> Unit = { renderHtmlHead() diff --git a/pkl-doc/src/main/kotlin/org/pkl/doc/DocGenerator.kt b/pkl-doc/src/main/kotlin/org/pkl/doc/DocGenerator.kt index f6e677f4..7bdc5af7 100644 --- a/pkl-doc/src/main/kotlin/org/pkl/doc/DocGenerator.kt +++ b/pkl-doc/src/main/kotlin/org/pkl/doc/DocGenerator.kt @@ -190,28 +190,11 @@ class DocGenerator( "Docsite is not up to date. Expected: ${DocMigrator.CURRENT_VERSION}. Found: ${docMigrator.docsiteVersion}. Use DocMigrator to migrate the site." ) } - val htmlGenerator = - HtmlGenerator(docsiteInfo, docPackages, importResolver, outputDir, isTestMode, consoleOut) val searchIndexGenerator = SearchIndexGenerator(outputDir, consoleOut) val packageDataGenerator = PackageDataGenerator(outputDir, consoleOut) val runtimeDataGenerator = RuntimeDataGenerator(descendingVersionComparator, outputDir, consoleOut) - coroutineScope { - for (docPackage in docPackages) { - launch { - docPackage.deletePackageDir() - coroutineScope { - launch { htmlGenerator.generate(docPackage) } - launch { searchIndexGenerator.generate(docPackage) } - launch { packageDataGenerator.generate(docPackage) } - } - } - } - } - - writeOutputLine("Generated HTML for packages") - val newlyGeneratedPackages = docPackages.map(::PackageData).sortedBy { it.ref.pkg } val currentSearchIndex = searchIndexGenerator.getCurrentSearchIndex() @@ -226,6 +209,32 @@ class DocGenerator( newlyGeneratedPackages + existingCurrentPackages, descendingVersionComparator, ) + val isSinglePackageSite = docsiteInfo.overview == null && currentPackages.size == 1 + val htmlGenerator = + HtmlGenerator( + docsiteInfo, + docPackages, + importResolver, + outputDir, + isTestMode, + isSinglePackageSite, + consoleOut, + ) + + coroutineScope { + for (docPackage in docPackages) { + launch { + docPackage.deletePackageDir() + coroutineScope { + launch { htmlGenerator.generate(docPackage) } + launch { searchIndexGenerator.generate(docPackage) } + launch { packageDataGenerator.generate(docPackage) } + } + } + } + } + + writeOutputLine("Generated HTML for packages") createCurrentDirectories(currentPackages, existingCurrentPackages) searchIndexGenerator.generateSiteIndex(currentPackages) diff --git a/pkl-doc/src/main/kotlin/org/pkl/doc/DocsiteInfo.kt b/pkl-doc/src/main/kotlin/org/pkl/doc/DocsiteInfo.kt index 0faeec0f..bdfba777 100644 --- a/pkl-doc/src/main/kotlin/org/pkl/doc/DocsiteInfo.kt +++ b/pkl-doc/src/main/kotlin/org/pkl/doc/DocsiteInfo.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,10 @@ data class DocsiteInfo( * * Uses the same Markdown format as Pkldoc comments. Unless expanded, only the first paragraph is * shown. + * + * If [overview] is `null` and the generated site has only one distinct package name, the main + * page redirects to that package page and generated breadcrumbs omit the site title segment. The + * structure of the generated site is unchanged. */ val overview: String?, diff --git a/pkl-doc/src/main/kotlin/org/pkl/doc/HtmlGenerator.kt b/pkl-doc/src/main/kotlin/org/pkl/doc/HtmlGenerator.kt index e8f4836b..3f8cf2bb 100644 --- a/pkl-doc/src/main/kotlin/org/pkl/doc/HtmlGenerator.kt +++ b/pkl-doc/src/main/kotlin/org/pkl/doc/HtmlGenerator.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ internal class HtmlGenerator( importResolver: (URI) -> ModuleSchema, private val outputDir: Path, private val isTestMode: Boolean, + private val isSinglePackageSite: Boolean, consoleOut: OutputStream, ) : AbstractGenerator(consoleOut) { private val siteScope = @@ -35,14 +36,25 @@ internal class HtmlGenerator( suspend fun generate(docPackage: DocPackage) = coroutineScope { val packageScope = siteScope.getPackage(docPackage.docPackageInfo) - launch { PackagePageGenerator(docsiteInfo, docPackage, packageScope, consoleOut).run() } + launch { + PackagePageGenerator(docsiteInfo, docPackage, packageScope, isSinglePackageSite, consoleOut) + .run() + } for (docModule in docPackage.docModules) { if (docModule.isUnlisted) continue val moduleScope = packageScope.getModule(docModule.name) launch { - ModulePageGenerator(docsiteInfo, docPackage, docModule, moduleScope, isTestMode, consoleOut) + ModulePageGenerator( + docsiteInfo, + docPackage, + docModule, + moduleScope, + isTestMode, + isSinglePackageSite, + consoleOut, + ) .run() } @@ -56,6 +68,7 @@ internal class HtmlGenerator( clazz, ClassScope(clazz, moduleScope.url, moduleScope), isTestMode, + isSinglePackageSite, consoleOut, ) .run() @@ -65,7 +78,9 @@ internal class HtmlGenerator( } suspend fun generateSite(packages: List) = coroutineScope { - launch { MainPageGenerator(docsiteInfo, packages, siteScope, consoleOut).run() } + launch { + MainPageGenerator(docsiteInfo, packages, siteScope, isSinglePackageSite, consoleOut).run() + } launch { generateStaticResources() } } diff --git a/pkl-doc/src/main/kotlin/org/pkl/doc/MainOrPackagePageGenerator.kt b/pkl-doc/src/main/kotlin/org/pkl/doc/MainOrPackagePageGenerator.kt index b332f2d4..1f5bcf56 100644 --- a/pkl-doc/src/main/kotlin/org/pkl/doc/MainOrPackagePageGenerator.kt +++ b/pkl-doc/src/main/kotlin/org/pkl/doc/MainOrPackagePageGenerator.kt @@ -21,8 +21,9 @@ import kotlinx.html.* internal abstract class MainOrPackagePageGenerator( docsiteInfo: DocsiteInfo, pageScope: S, + isSinglePackageSite: Boolean, consoleOut: OutputStream, -) : PageGenerator(docsiteInfo, pageScope, consoleOut) where S : PageScope { +) : PageGenerator(docsiteInfo, pageScope, isSinglePackageSite, consoleOut) where S : PageScope { protected fun UL.renderModuleOrPackage( name: String, moduleOrPackageScope: DocScope, diff --git a/pkl-doc/src/main/kotlin/org/pkl/doc/MainPageGenerator.kt b/pkl-doc/src/main/kotlin/org/pkl/doc/MainPageGenerator.kt index 3065a209..cd7f02ec 100644 --- a/pkl-doc/src/main/kotlin/org/pkl/doc/MainPageGenerator.kt +++ b/pkl-doc/src/main/kotlin/org/pkl/doc/MainPageGenerator.kt @@ -17,47 +17,53 @@ package org.pkl.doc import java.io.OutputStream import kotlinx.html.* +import kotlinx.serialization.json.Json internal class MainPageGenerator( docsiteInfo: DocsiteInfo, private val packagesData: List, pageScope: SiteScope, + private val isSinglePackageSite: Boolean, consoleOut: OutputStream, -) : MainOrPackagePageGenerator(docsiteInfo, pageScope, consoleOut) { +) : MainOrPackagePageGenerator(docsiteInfo, pageScope, false, consoleOut) { override val html: HTML.() -> Unit = { - renderHtmlHead() + if (isSinglePackageSite) { + renderRedirectPage() + } else { + renderHtmlHead() + body { + onLoad = "onLoad()" - body { - onLoad = "onLoad()" + renderPageHeader(null, null, null, null) - renderPageHeader(null, null, null, null) + main { + h1 { + id = "declaration-title" - main { - h1 { - id = "declaration-title" - - +(docsiteInfo.title ?: "") - } - - val memberDocs = MemberDocs(docsiteInfo.overview, pageScope, listOf(), isDeclaration = true) - - renderMemberGroupLinks( - Triple("Overview", "#_overview", memberDocs.isExpandable), - Triple("Packages", "#_packages", packagesData.isNotEmpty()), - ) - - if (docsiteInfo.overview != null) { - renderAnchor("_overview") - div { - id = "_declaration" - classes = setOf("member") - - memberDocs.renderExpandIcon(this) - memberDocs.renderDocComment(this) + +(docsiteInfo.title ?: "") } - } - renderPackages() + val memberDocs = + MemberDocs(docsiteInfo.overview, pageScope, listOf(), isDeclaration = true) + + renderMemberGroupLinks( + Triple("Overview", "#_overview", memberDocs.isExpandable), + Triple("Packages", "#_packages", packagesData.isNotEmpty()), + ) + + if (docsiteInfo.overview != null) { + renderAnchor("_overview") + div { + id = "_declaration" + classes = setOf("member") + + memberDocs.renderExpandIcon(this) + memberDocs.renderDocComment(this) + } + } + + renderPackages() + } } } } @@ -66,6 +72,28 @@ internal class MainPageGenerator( +(docsiteInfo.title ?: "Pkldoc") } + private fun HTML.renderRedirectPage() { + val packagePageUrl = "${packagesData.single().ref.basePath}/current/index.html" + + lang = "en-US" + + head { + meta { charset = "UTF-8" } + title { renderPageTitle() } + script { unsafe { raw("window.location.replace(${Json.encodeToString(packagePageUrl)});") } } + } + body { + main { + p { + a { + href = packagePageUrl + +packagePageUrl + } + } + } + } + } + private fun HtmlBlockTag.renderPackages() { if (packagesData.isEmpty()) return diff --git a/pkl-doc/src/main/kotlin/org/pkl/doc/ModuleOrClassPageGenerator.kt b/pkl-doc/src/main/kotlin/org/pkl/doc/ModuleOrClassPageGenerator.kt index 9f9110b4..25cb4348 100644 --- a/pkl-doc/src/main/kotlin/org/pkl/doc/ModuleOrClassPageGenerator.kt +++ b/pkl-doc/src/main/kotlin/org/pkl/doc/ModuleOrClassPageGenerator.kt @@ -29,8 +29,9 @@ internal abstract class ModuleOrClassPageGenerator( protected val clazz: PClass, scope: S, private val isTestMode: Boolean, + isSinglePackageSite: Boolean, consoleOut: OutputStream, -) : PageGenerator(docsiteInfo, scope, consoleOut) where S : PageScope { +) : PageGenerator(docsiteInfo, scope, isSinglePackageSite, consoleOut) where S : PageScope { protected fun HtmlBlockTag.renderProperties() { if (!clazz.hasListedProperty) return diff --git a/pkl-doc/src/main/kotlin/org/pkl/doc/ModulePageGenerator.kt b/pkl-doc/src/main/kotlin/org/pkl/doc/ModulePageGenerator.kt index 435ed32e..f1b32447 100644 --- a/pkl-doc/src/main/kotlin/org/pkl/doc/ModulePageGenerator.kt +++ b/pkl-doc/src/main/kotlin/org/pkl/doc/ModulePageGenerator.kt @@ -24,6 +24,7 @@ internal class ModulePageGenerator( docModule: DocModule, pageScope: ModuleScope, isTestMode: Boolean, + isSinglePackageSite: Boolean, consoleOut: OutputStream, ) : ModuleOrClassPageGenerator( @@ -31,6 +32,7 @@ internal class ModulePageGenerator( docModule.schema.moduleClass, pageScope, isTestMode, + isSinglePackageSite, consoleOut, ) { private val module = docModule.schema diff --git a/pkl-doc/src/main/kotlin/org/pkl/doc/PackagePageGenerator.kt b/pkl-doc/src/main/kotlin/org/pkl/doc/PackagePageGenerator.kt index 8acbcaef..5b66c5f9 100644 --- a/pkl-doc/src/main/kotlin/org/pkl/doc/PackagePageGenerator.kt +++ b/pkl-doc/src/main/kotlin/org/pkl/doc/PackagePageGenerator.kt @@ -22,8 +22,15 @@ internal class PackagePageGenerator( docsiteInfo: DocsiteInfo, private val docPackage: DocPackage, pageScope: PackageScope, + isSinglePackageSite: Boolean, consoleOut: OutputStream, -) : MainOrPackagePageGenerator(docsiteInfo, pageScope, consoleOut) { +) : + MainOrPackagePageGenerator( + docsiteInfo, + pageScope, + isSinglePackageSite, + consoleOut, + ) { override val html: HTML.() -> Unit = { renderHtmlHead() diff --git a/pkl-doc/src/main/kotlin/org/pkl/doc/PageGenerator.kt b/pkl-doc/src/main/kotlin/org/pkl/doc/PageGenerator.kt index fee8d4c1..1e632eec 100644 --- a/pkl-doc/src/main/kotlin/org/pkl/doc/PageGenerator.kt +++ b/pkl-doc/src/main/kotlin/org/pkl/doc/PageGenerator.kt @@ -32,6 +32,7 @@ import org.pkl.core.util.IoUtils internal abstract class PageGenerator( protected val docsiteInfo: DocsiteInfo, protected val pageScope: S, + private val isSinglePackageSite: Boolean, consoleOut: OutputStream, ) : AbstractGenerator(consoleOut) where S : PageScope { companion object { @@ -203,13 +204,6 @@ internal abstract class PageGenerator( } protected fun HtmlBlockTag.renderParentLinks() { - a { - classes = setOf("declaration-parent-link") - href = pageScope.relativeSiteUrl.toString() - - +(docsiteInfo.title ?: "Pkldoc") - } - val packageScope = when (pageScope) { is ClassScope -> pageScope.parent!!.parent @@ -217,33 +211,33 @@ internal abstract class PageGenerator( else -> null } - if (packageScope != null) { - +" > " - - a { - classes = setOf("declaration-parent-link") - href = packageScope.urlRelativeTo(pageScope).toString() - - +packageScope.name - } - } - val moduleScope = when (pageScope) { is ClassScope -> pageScope.parent else -> null } - if (moduleScope != null) { - +" > " + var isFirst = true + fun renderLink(text: String, url: String) { + if (isFirst) isFirst = false else +" > " a { classes = setOf("declaration-parent-link") - href = moduleScope.urlRelativeTo(pageScope).toString() + href = url - +moduleScope.name + +text } } + + if (!isSinglePackageSite) { + renderLink(docsiteInfo.title ?: "Pkldoc", pageScope.relativeSiteUrl.toString()) + } + if (packageScope != null) { + renderLink(packageScope.name, packageScope.urlRelativeTo(pageScope).toString()) + } + if (moduleScope != null) { + renderLink(moduleScope.name, moduleScope.urlRelativeTo(pageScope).toString()) + } } protected fun HtmlBlockTag.renderClassExtendsClause(clazz: PClass, currScope: DocScope) { diff --git a/pkl-doc/src/test/files/SinglePackageTest/input/com.package1-4.5.6/doc-package-info.pkl b/pkl-doc/src/test/files/SinglePackageTest/input/com.package1-4.5.6/doc-package-info.pkl new file mode 100644 index 00000000..2dd7f852 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/input/com.package1-4.5.6/doc-package-info.pkl @@ -0,0 +1,22 @@ +/// Additional version fixture for single-package site tests. +amends "pkl:DocPackageInfo" + +name = "com.package1" +version = "4.5.6" +importUri = "https://example.com/" +authors { + "package1-publisher@example.com" +} +sourceCode = "https://example.com/package1/" +sourceCodeUrlScheme = "https://example.com/package1%{path}#L%{line}-L%{endLine}" +issueTracker = "https://issues.example.com/package1/" +dependencies { + new { + name = "pkl" + // use fixed version to avoid churn in expected test outputs + version = "0.24.0" + sourceCode = "https://github.com/apple/pkl/blob/dev/stdlib/" + sourceCodeUrlScheme = "https://github.com/apple/pkl/blob/0.24.0/stdlib%{path}#L%{line}-L%{endLine}" + documentation = "https://pages.github.com/apple/pkl/stdlib/pkl/0.24.0/" + } +} diff --git a/pkl-doc/src/test/files/SinglePackageTest/input/com.package1-4.5.6/minimal.pkl b/pkl-doc/src/test/files/SinglePackageTest/input/com.package1-4.5.6/minimal.pkl new file mode 100644 index 00000000..ca56484b --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/input/com.package1-4.5.6/minimal.pkl @@ -0,0 +1,11 @@ +/// Additional version module fixture. +module com.package1.minimal + +/// Additional version class fixture. +class Person { + /// The person's name. + name: String +} + +/// A module-level property. +greeting: String diff --git a/pkl-doc/src/test/files/SinglePackageTest/input/com.package1/doc-package-info.pkl b/pkl-doc/src/test/files/SinglePackageTest/input/com.package1/doc-package-info.pkl new file mode 100644 index 00000000..1536ed51 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/input/com.package1/doc-package-info.pkl @@ -0,0 +1,22 @@ +/// Minimal package fixture for single-package site tests. +amends "pkl:DocPackageInfo" + +name = "com.package1" +version = "1.2.3" +importUri = "https://example.com/" +authors { + "package1-publisher@example.com" +} +sourceCode = "https://example.com/package1/" +sourceCodeUrlScheme = "https://example.com/package1%{path}#L%{line}-L%{endLine}" +issueTracker = "https://issues.example.com/package1/" +dependencies { + new { + name = "pkl" + // use fixed version to avoid churn in expected test outputs + version = "0.24.0" + sourceCode = "https://github.com/apple/pkl/blob/dev/stdlib/" + sourceCodeUrlScheme = "https://github.com/apple/pkl/blob/0.24.0/stdlib%{path}#L%{line}-L%{endLine}" + documentation = "https://pages.github.com/apple/pkl/stdlib/pkl/0.24.0/" + } +} diff --git a/pkl-doc/src/test/files/SinglePackageTest/input/com.package1/minimal.pkl b/pkl-doc/src/test/files/SinglePackageTest/input/com.package1/minimal.pkl new file mode 100644 index 00000000..321e8cb8 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/input/com.package1/minimal.pkl @@ -0,0 +1,11 @@ +/// Minimal module fixture. +module com.package1.minimal + +/// Minimal class fixture. +class Person { + /// The person's name. + name: String +} + +/// A module-level property. +greeting: String diff --git a/pkl-doc/src/test/files/SinglePackageTest/input/docsite-info.pkl b/pkl-doc/src/test/files/SinglePackageTest/input/docsite-info.pkl new file mode 100644 index 00000000..8fe8ab44 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/input/docsite-info.pkl @@ -0,0 +1,3 @@ +amends "pkl:DocsiteInfo" + +title = "Single Package Docs" diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/.pkldoc/VERSION b/pkl-doc/src/test/files/SinglePackageTest/output/.pkldoc/VERSION new file mode 100644 index 00000000..d8263ee9 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/.pkldoc/VERSION @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/index.html b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/index.html new file mode 100644 index 00000000..5457466d --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/index.html @@ -0,0 +1,71 @@ + + + + + com.package1 (1.2.3) • Single Package Docs + + + + + + + + + + +
+ + +
+
+

com.package11.2.3

+ +
+
+
package com.package1
+

Minimal package fixture for single-package site tests.

+
+
Authors:
+
package1-publisher@example.com
+
Version:
+
1.2.3
+
Source code:
+
https://example.com/package1/
+
Issue tracker:
+
https://issues.example.com/package1/
+ + + + +
+
+
+
+

Modules

+ +
+
+ + diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/minimal/Person.html b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/minimal/Person.html new file mode 100644 index 00000000..ee584b3c --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/minimal/Person.html @@ -0,0 +1,178 @@ + + + + + Person (com.package1/minimal:1.2.3) • Single Package Docs + + + + + + + + + + +
+ + +
+
com.package1 > com.package1.minimal +

Person1.2.3

+ +
+
+
class Person
+

Minimal class fixture.

+
+ + + + + + +
+
+
+
+

Properties

+ +
+
+
+

Methods(show inherited)

+
    +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
+
+
+ + diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/minimal/index.html b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/minimal/index.html new file mode 100644 index 00000000..ac7c7b51 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/minimal/index.html @@ -0,0 +1,238 @@ + + + + + minimal (com.package1:1.2.3) • Single Package Docs + + + + + + + + + + +
+ + +
+
com.package1 +

com.package1.minimal1.2.3

+ +
+
+
module com.package1.minimal
+

Minimal module fixture.

+
+
Module URI:
+
https://example.com/minimal.pklcontent_copy
+
Source code:
+
minimal.pkl
+ + + + + + +
+
+
+
+

Properties(show inherited)

+
    +
  • +
    + +
  • +
  • +
    +
    link +
    +
    +
    +
    +
    greeting: StringSource
    +

    A module-level property.

    +
    +
    +
  • +
+
+
+
+

Methods(show inherited)

+
    +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    +
    + +
  • +
+
+
+
+

Classes

+ +
+
+ + diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/package-data.json b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/package-data.json new file mode 100644 index 00000000..d9854e31 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/package-data.json @@ -0,0 +1 @@ +{"ref":{"pkg":"com.package1","pkgUri":null,"version":"1.2.3"},"summary":"Minimal package fixture for single-package site tests.","sourceCode":"https://example.com/package1/","sourceCodeUrlScheme":"https://example.com/package1%{path}#L%{line}-L%{endLine}","dependencies":[{"ref":{"pkg":"pkl","pkgUri":null,"version":"0.24.0"}}],"modules":[{"ref":{"pkg":"com.package1","pkgUri":null,"version":"1.2.3","module":"minimal"},"summary":"Minimal module fixture.","moduleClass":{"ref":{"pkg":"com.package1","pkgUri":null,"version":"1.2.3","module":"minimal","type":"ModuleClass"},"superclasses":[{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Module"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Typed"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Object"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Any"}]},"classes":[{"ref":{"pkg":"com.package1","pkgUri":null,"version":"1.2.3","module":"minimal","type":"Person"},"superclasses":[{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Typed"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Object"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Any"}]}]}]} \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/search-index.js b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/search-index.js new file mode 100644 index 00000000..4856635a --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/1.2.3/search-index.js @@ -0,0 +1 @@ +searchData='[{"name":"com.package1.minimal","kind":1,"url":"minimal/index.html"},{"name":"greeting","kind":5,"url":"minimal/index.html#greeting","sig":": String","parId":0},{"name":"Person","kind":3,"url":"minimal/Person.html","parId":0},{"name":"name","kind":5,"url":"minimal/Person.html#name","sig":": String","parId":2}]'; diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/index.html b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/index.html new file mode 100644 index 00000000..fc42609d --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/index.html @@ -0,0 +1,71 @@ + + + + + com.package1 (4.5.6) • Single Package Docs + + + + + + + + + + +
+ + +
+
+

com.package14.5.6

+ +
+
+
package com.package1
+

Additional version fixture for single-package site tests.

+
+
Authors:
+
package1-publisher@example.com
+
Version:
+
4.5.6
+
Source code:
+
https://example.com/package1/
+
Issue tracker:
+
https://issues.example.com/package1/
+ + + + +
+
+
+
+

Modules

+ +
+
+ + diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/minimal/Person.html b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/minimal/Person.html new file mode 100644 index 00000000..5b6fca05 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/minimal/Person.html @@ -0,0 +1,178 @@ + + + + + Person (com.package1/minimal:4.5.6) • Single Package Docs + + + + + + + + + + +
+ + +
+
com.package1 > com.package1.minimal +

Person4.5.6

+ +
+
+
class Person
+

Additional version class fixture.

+
+ + + + + + +
+
+
+
+

Properties

+ +
+
+
+

Methods(show inherited)

+
    +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
+
+
+ + diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/minimal/index.html b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/minimal/index.html new file mode 100644 index 00000000..c15feb97 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/minimal/index.html @@ -0,0 +1,238 @@ + + + + + minimal (com.package1:4.5.6) • Single Package Docs + + + + + + + + + + +
+ + +
+
com.package1 +

com.package1.minimal4.5.6

+ +
+
+
module com.package1.minimal
+

Additional version module fixture.

+
+
Module URI:
+
https://example.com/minimal.pklcontent_copy
+
Source code:
+
minimal.pkl
+ + + + + + +
+
+
+
+

Properties(show inherited)

+
    +
  • +
    + +
  • +
  • +
    +
    link +
    +
    +
    +
    +
    greeting: StringSource
    +

    A module-level property.

    +
    +
    +
  • +
+
+
+
+

Methods(show inherited)

+
    +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    +
    + +
  • +
+
+
+
+

Classes

+
    +
  • +
    + +
  • +
+
+
+ + diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/package-data.json b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/package-data.json new file mode 100644 index 00000000..a20bb208 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/package-data.json @@ -0,0 +1 @@ +{"ref":{"pkg":"com.package1","pkgUri":null,"version":"4.5.6"},"summary":"Additional version fixture for single-package site tests.","sourceCode":"https://example.com/package1/","sourceCodeUrlScheme":"https://example.com/package1%{path}#L%{line}-L%{endLine}","dependencies":[{"ref":{"pkg":"pkl","pkgUri":null,"version":"0.24.0"}}],"modules":[{"ref":{"pkg":"com.package1","pkgUri":null,"version":"4.5.6","module":"minimal"},"summary":"Additional version module fixture.","moduleClass":{"ref":{"pkg":"com.package1","pkgUri":null,"version":"4.5.6","module":"minimal","type":"ModuleClass"},"superclasses":[{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Module"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Typed"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Object"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Any"}]},"classes":[{"ref":{"pkg":"com.package1","pkgUri":null,"version":"4.5.6","module":"minimal","type":"Person"},"superclasses":[{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Typed"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Object"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Any"}]}]}]} \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/search-index.js b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/search-index.js new file mode 100644 index 00000000..4856635a --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/4.5.6/search-index.js @@ -0,0 +1 @@ +searchData='[{"name":"com.package1.minimal","kind":1,"url":"minimal/index.html"},{"name":"greeting","kind":5,"url":"minimal/index.html#greeting","sig":": String","parId":0},{"name":"Person","kind":3,"url":"minimal/Person.html","parId":0},{"name":"name","kind":5,"url":"minimal/Person.html#name","sig":": String","parId":2}]'; diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/index.html b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/index.html new file mode 100644 index 00000000..fc42609d --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/index.html @@ -0,0 +1,71 @@ + + + + + com.package1 (4.5.6) • Single Package Docs + + + + + + + + + + +
+ + +
+
+

com.package14.5.6

+ +
+
+
package com.package1
+

Additional version fixture for single-package site tests.

+
+
Authors:
+
package1-publisher@example.com
+
Version:
+
4.5.6
+
Source code:
+
https://example.com/package1/
+
Issue tracker:
+
https://issues.example.com/package1/
+ + + + +
+
+
+
+

Modules

+ +
+
+ + diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/minimal/Person.html b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/minimal/Person.html new file mode 100644 index 00000000..5b6fca05 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/minimal/Person.html @@ -0,0 +1,178 @@ + + + + + Person (com.package1/minimal:4.5.6) • Single Package Docs + + + + + + + + + + +
+ + +
+
com.package1 > com.package1.minimal +

Person4.5.6

+ +
+
+
class Person
+

Additional version class fixture.

+
+ + + + + + +
+
+
+
+

Properties

+ +
+
+
+

Methods(show inherited)

+
    +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
+
+
+ + diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/minimal/index.html b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/minimal/index.html new file mode 100644 index 00000000..c15feb97 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/minimal/index.html @@ -0,0 +1,238 @@ + + + + + minimal (com.package1:4.5.6) • Single Package Docs + + + + + + + + + + +
+ + +
+
com.package1 +

com.package1.minimal4.5.6

+ +
+
+
module com.package1.minimal
+

Additional version module fixture.

+
+
Module URI:
+
https://example.com/minimal.pklcontent_copy
+
Source code:
+
minimal.pkl
+ + + + + + +
+
+
+
+

Properties(show inherited)

+
    +
  • +
    + +
  • +
  • +
    +
    link +
    +
    +
    +
    +
    greeting: StringSource
    +

    A module-level property.

    +
    +
    +
  • +
+
+
+
+

Methods(show inherited)

+
    +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    +
    + +
  • +
+
+
+
+

Classes

+
    +
  • +
    + +
  • +
+
+
+ + diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/package-data.json b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/package-data.json new file mode 100644 index 00000000..a20bb208 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/package-data.json @@ -0,0 +1 @@ +{"ref":{"pkg":"com.package1","pkgUri":null,"version":"4.5.6"},"summary":"Additional version fixture for single-package site tests.","sourceCode":"https://example.com/package1/","sourceCodeUrlScheme":"https://example.com/package1%{path}#L%{line}-L%{endLine}","dependencies":[{"ref":{"pkg":"pkl","pkgUri":null,"version":"0.24.0"}}],"modules":[{"ref":{"pkg":"com.package1","pkgUri":null,"version":"4.5.6","module":"minimal"},"summary":"Additional version module fixture.","moduleClass":{"ref":{"pkg":"com.package1","pkgUri":null,"version":"4.5.6","module":"minimal","type":"ModuleClass"},"superclasses":[{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Module"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Typed"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Object"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Any"}]},"classes":[{"ref":{"pkg":"com.package1","pkgUri":null,"version":"4.5.6","module":"minimal","type":"Person"},"superclasses":[{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Typed"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Object"},{"pkg":"pkl","pkgUri":null,"version":"0.24.0","module":"base","type":"Any"}]}]}]} \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/search-index.js b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/search-index.js new file mode 100644 index 00000000..4856635a --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/com.package1/current/search-index.js @@ -0,0 +1 @@ +searchData='[{"name":"com.package1.minimal","kind":1,"url":"minimal/index.html"},{"name":"greeting","kind":5,"url":"minimal/index.html#greeting","sig":": String","parId":0},{"name":"Person","kind":3,"url":"minimal/Person.html","parId":0},{"name":"name","kind":5,"url":"minimal/Person.html#name","sig":": String","parId":2}]'; diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/1.2.3/index.json b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/1.2.3/index.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/1.2.3/index.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/1.2.3/minimal/Person.json b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/1.2.3/minimal/Person.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/1.2.3/minimal/Person.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/1.2.3/minimal/index.json b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/1.2.3/minimal/index.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/1.2.3/minimal/index.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/4.5.6/index.json b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/4.5.6/index.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/4.5.6/index.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/4.5.6/minimal/Person.json b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/4.5.6/minimal/Person.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/4.5.6/minimal/Person.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/4.5.6/minimal/index.json b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/4.5.6/minimal/index.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/4.5.6/minimal/index.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/_/index.json b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/_/index.json new file mode 100644 index 00000000..e86d59cc --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/_/index.json @@ -0,0 +1,12 @@ +{ + "knownVersions": [ + { + "text": "4.5.6", + "href": "../4.5.6/index.html" + }, + { + "text": "1.2.3", + "href": "../1.2.3/index.html" + } + ] +} \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/_/minimal/Person.json b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/_/minimal/Person.json new file mode 100644 index 00000000..bdd6b8a2 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/_/minimal/Person.json @@ -0,0 +1,12 @@ +{ + "knownVersions": [ + { + "text": "4.5.6", + "href": "../../4.5.6/minimal/Person.html" + }, + { + "text": "1.2.3", + "href": "../../1.2.3/minimal/Person.html" + } + ] +} \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/_/minimal/index.json b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/_/minimal/index.json new file mode 100644 index 00000000..66f104cc --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/data/com.package1/_/minimal/index.json @@ -0,0 +1,12 @@ +{ + "knownVersions": [ + { + "text": "4.5.6", + "href": "../../4.5.6/minimal/index.html" + }, + { + "text": "1.2.3", + "href": "../../1.2.3/minimal/index.html" + } + ] +} \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/fonts/MaterialIcons-Regular.woff2 b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/MaterialIcons-Regular.woff2 new file mode 100644 index 00000000..9fa21125 Binary files /dev/null and b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/MaterialIcons-Regular.woff2 differ diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/fonts/lato-v14-latin_latin-ext-700.woff2 b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/lato-v14-latin_latin-ext-700.woff2 new file mode 100644 index 00000000..e344f0e8 Binary files /dev/null and b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/lato-v14-latin_latin-ext-700.woff2 differ diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/fonts/lato-v14-latin_latin-ext-regular.woff2 b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/lato-v14-latin_latin-ext-regular.woff2 new file mode 100644 index 00000000..b41315e5 Binary files /dev/null and b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/lato-v14-latin_latin-ext-regular.woff2 differ diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/fonts/open-sans-v15-latin_latin-ext-700.woff2 b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/open-sans-v15-latin_latin-ext-700.woff2 new file mode 100644 index 00000000..749a9714 Binary files /dev/null and b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/open-sans-v15-latin_latin-ext-700.woff2 differ diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/fonts/open-sans-v15-latin_latin-ext-700italic.woff2 b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/open-sans-v15-latin_latin-ext-700italic.woff2 new file mode 100644 index 00000000..6133af50 Binary files /dev/null and b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/open-sans-v15-latin_latin-ext-700italic.woff2 differ diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/fonts/open-sans-v15-latin_latin-ext-italic.woff2 b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/open-sans-v15-latin_latin-ext-italic.woff2 new file mode 100644 index 00000000..3ca0fa8c Binary files /dev/null and b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/open-sans-v15-latin_latin-ext-italic.woff2 differ diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/fonts/open-sans-v15-latin_latin-ext-regular.woff2 b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/open-sans-v15-latin_latin-ext-regular.woff2 new file mode 100644 index 00000000..a337154f Binary files /dev/null and b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/open-sans-v15-latin_latin-ext-regular.woff2 differ diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/fonts/source-code-pro-v7-latin_latin-ext-700.woff2 b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/source-code-pro-v7-latin_latin-ext-700.woff2 new file mode 100644 index 00000000..76b6b79f Binary files /dev/null and b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/source-code-pro-v7-latin_latin-ext-700.woff2 differ diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/fonts/source-code-pro-v7-latin_latin-ext-regular.woff2 b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/source-code-pro-v7-latin_latin-ext-regular.woff2 new file mode 100644 index 00000000..10abca8e Binary files /dev/null and b/pkl-doc/src/test/files/SinglePackageTest/output/fonts/source-code-pro-v7-latin_latin-ext-regular.woff2 differ diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/images/apple-touch-icon.png b/pkl-doc/src/test/files/SinglePackageTest/output/images/apple-touch-icon.png new file mode 100644 index 00000000..fb060005 Binary files /dev/null and b/pkl-doc/src/test/files/SinglePackageTest/output/images/apple-touch-icon.png differ diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/images/favicon-16x16.png b/pkl-doc/src/test/files/SinglePackageTest/output/images/favicon-16x16.png new file mode 100644 index 00000000..7ecded2f Binary files /dev/null and b/pkl-doc/src/test/files/SinglePackageTest/output/images/favicon-16x16.png differ diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/images/favicon-32x32.png b/pkl-doc/src/test/files/SinglePackageTest/output/images/favicon-32x32.png new file mode 100644 index 00000000..a5d588e4 Binary files /dev/null and b/pkl-doc/src/test/files/SinglePackageTest/output/images/favicon-32x32.png differ diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/images/favicon.svg b/pkl-doc/src/test/files/SinglePackageTest/output/images/favicon.svg new file mode 100644 index 00000000..441aec22 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/images/favicon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/index.html b/pkl-doc/src/test/files/SinglePackageTest/output/index.html new file mode 100644 index 00000000..d543c5bd --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/index.html @@ -0,0 +1,13 @@ + + + + + Single Package Docs + + + +
+

com.package1/current/index.html

+
+ + diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/scripts/pkldoc.js b/pkl-doc/src/test/files/SinglePackageTest/output/scripts/pkldoc.js new file mode 100644 index 00000000..5fb89175 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/scripts/pkldoc.js @@ -0,0 +1,737 @@ +// noinspection DuplicatedCode + +'use strict'; + +// Whether the current browser is WebKit. +let isWebKitBrowser; + +// The lazily initialized worker for running searches, if any. +let searchWorker = null; + +// Tells whether non-worker search is ready for use. +// Only relevant if we determined that we can't use a worker. +let nonWorkerSearchInitialized = false; + +// The search div containing search input and search results. +let searchElement; + +// The search input element. +let searchInput; + +// The package name associated with the current page, if any. +let packageName; + +let packageVersion; + +// The module name associated with the current page, if any. +let moduleName; + +// The class name associated with the current page, if any. +let className; + +// Prefix to turn a site-relative URL into a page-relative URL. +// One of "", "../", "../../", etc. +let rootUrlPrefix; + +// Prefix to turn a package-relative URL into a page-relative URL. +// One of "", "../", "../../", etc. +let packageUrlPrefix; + +// The search result currently selected in the search results list. +let selectedSearchResult = null; + +// Initializes the UI. +// Wrapped in a function to avoid execution in tests. +// noinspection JSUnusedGlobalSymbols +function onLoad() { + isWebKitBrowser = navigator.userAgent.indexOf('AppleWebKit') !== -1; + searchElement = document.getElementById('search'); + searchInput = document.getElementById('search-input'); + packageName = searchInput.dataset.packageName || null; + packageVersion = searchInput.dataset.packageVersion || null; + moduleName = searchInput.dataset.moduleName || null; + className = searchInput.dataset.className || null; + rootUrlPrefix = searchInput.dataset.rootUrlPrefix; + packageUrlPrefix = searchInput.dataset.packageUrlPrefix; + + initExpandTargetMemberDocs(); + initNavigateToMemberPage(); + initToggleMemberDocs(); + initToggleInheritedMembers(); + initCopyModuleUriToClipboard(); + initSearchUi(); +} + +// If page URL contains a fragment, expand the target member's docs. +// Handled in JS rather than CSS so that target member can still be manually collapsed. +function initExpandTargetMemberDocs() { + const expandTargetDocs = () => { + const hash = window.location.hash; + if (hash.length === 0) return; + + const target = document.getElementById(hash.substring(1)); + if (!target) return; + + const member = target.nextElementSibling; + if (!member || !member.classList.contains('with-expandable-docs')) return; + + expandMemberDocs(member); + } + + window.addEventListener('hashchange', expandTargetDocs); + expandTargetDocs(); +} + +// For members that have their own page, navigate to that page when the member's box is clicked. +function initNavigateToMemberPage() { + const elements = document.getElementsByClassName('with-page-link'); + for (const element of elements) { + const memberLink = element.getElementsByClassName('name-decl')[0]; + // check if this is actually a link + // (it isn't if the generator couldn't resolve the link target) + if (memberLink.tagName === 'A') { + element.addEventListener('click', (e) => { + // don't act if user clicked a link + if (e.target !== null && e.target.closest('a') !== null) return; + + // don't act if user clicked to select some text + if (window.getSelection().toString()) return; + + memberLink.click(); + }); + } + } +} + +// Expands and collapses member docs. +function initToggleMemberDocs() { + const elements = document.getElementsByClassName('with-expandable-docs'); + for (const element of elements) { + element.addEventListener('click', (e) => { + // don't act if user clicked a link + if (e.target !== null && e.target.closest('a') !== null) return; + + // don't act if user clicked to select some text + if (window.getSelection().toString()) return; + + toggleMemberDocs(element); + }); + } +} + +// Shows and hides inherited members. +function initToggleInheritedMembers() { + const memberGroups = document.getElementsByClassName('member-group'); + for (const group of memberGroups) { + const button = group.getElementsByClassName('toggle-inherited-members-link')[0]; + if (button !== undefined) { + const members = group.getElementsByClassName('inherited'); + button.addEventListener('click', () => toggleInheritedMembers(button, members)); + } + } +} + +// Copies the module URI optionally displayed on a module page to the clipboard. +function initCopyModuleUriToClipboard() { + const copyUriButtons = document.getElementsByClassName('copy-uri-button'); + + for (const button of copyUriButtons) { + const moduleUri = button.previousElementSibling; + + button.addEventListener('click', e => { + e.stopPropagation(); + const range = document.createRange(); + range.selectNodeContents(moduleUri); + const selection = getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + try { + document.execCommand('copy'); + } catch (e) { + } finally { + selection.removeAllRanges(); + } + }); + } +} + +// Expands or collapses member docs. +function toggleMemberDocs(memberElem) { + const comments = memberElem.getElementsByClassName('expandable'); + const icon = memberElem.getElementsByClassName('expandable-docs-icon')[0]; + const isCollapsed = icon.textContent === 'expand_more'; + + if (isCollapsed) { + for (const comment of comments) expandElement(comment); + icon.textContent = 'expand_less'; + } else { + for (const comment of comments) collapseElement(comment); + icon.textContent = 'expand_more'; + } +} + +// Expands member docs unless they are already expanded. +function expandMemberDocs(memberElem) { + const icon = memberElem.getElementsByClassName('expandable-docs-icon')[0]; + const isCollapsed = icon.textContent === 'expand_more'; + + if (!isCollapsed) return; + + const comments = memberElem.getElementsByClassName('expandable'); + for (const comment of comments) expandElement(comment); + icon.textContent = 'expand_less'; +} + +// Shows and hides inherited members. +function toggleInheritedMembers(button, members) { + const isCollapsed = button.textContent === 'show inherited'; + + if (isCollapsed) { + for (const member of members) expandElement(member); + button.textContent = 'hide inherited'; + } else { + for (const member of members) collapseElement(member); + button.textContent = 'show inherited' + } +} + +// Expands an element. +// Done in two steps to make transition work (can't transition from 'hidden'). +// For some reason (likely related to removing 'hidden') the transition isn't animated in FF. +// When using timeout() instead of requestAnimationFrame() +// there is *some* animation in FF but still doesn't look right. +function expandElement(element) { + element.classList.remove('hidden'); + + requestAnimationFrame(() => { + element.classList.remove('collapsed'); + }); +} + +// Collapses an element. +// Done in two steps to make transition work (can't transition to 'hidden'). +function collapseElement(element) { + element.classList.add('collapsed'); + + const listener = () => { + element.removeEventListener('transitionend', listener); + element.classList.add('hidden'); + }; + element.addEventListener('transitionend', listener); +} + +// Initializes the search UI and sets up delayed initialization of the search engine. +function initSearchUi() { + // initialize search engine the first time that search input receives focus + const onFocus = () => { + searchInput.removeEventListener('focus', onFocus); + initSearchWorker(); + }; + searchInput.addEventListener('focus', onFocus); + + // clear search when search input loses focus, + // except if this happens due to a search result being clicked, + // in which case clearSearch() will be called by the link's click handler, + // and calling it here would prevent the click handler from firing + searchInput.addEventListener('focusout', () => { + if (document.querySelector('#search-results:hover') === null) clearSearch(); + }); + + // trigger search when user hasn't typed in a while + let timeoutId = null; + // Using anything other than `overflow: visible` for `#search-results` + // slows down painting significantly in WebKit browsers (at least Safari/Mac). + // Compensate by using a higher search delay, which is less annoying than a blocking UI. + const delay = isWebKitBrowser ? 200 : 100; + searchInput.addEventListener('input', () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => triggerSearch(searchInput.value), delay); + }); + + // keyboard shortcut for entering search + document.addEventListener('keyup', e => { + // could additionally support '/' like GitHub and Gmail do, + // but this would require overriding the default behavior of '/' on Firefox + if (e.key === 's') searchInput.focus(); + }); + + // keyboard navigation for search results + searchInput.addEventListener('keydown', e => { + const results = document.getElementById('search-results'); + if (results !== null) { + if (e.key === 'ArrowDown') { + selectNextResult(results.firstElementChild); + e.preventDefault(); + } else if (e.key === 'ArrowUp') { + selectPrevResult(results.firstElementChild); + e.preventDefault(); + } + } + }); + searchInput.addEventListener('keyup', e => { + if (e.key === 'Enter' && selectedSearchResult !== null) { + selectedSearchResult.firstElementChild.click(); + clearSearch(); + } + }); +} + +// Initializes the search worker. +function initSearchWorker() { + const workerScriptUrl = rootUrlPrefix + 'scripts/search-worker.js'; + + try { + searchWorker = new Worker(workerScriptUrl, {name: packageName === null ? "main" : packageName + '/' + packageVersion}); + searchWorker.addEventListener('message', e => handleSearchResults(e.data.query, e.data.results)); + } catch (e) { + // could not initialize worker, presumably because we are a file:/// page and content security policy got in the way + // fall back to running searches synchronously without a worker + // this requires loading search related scripts that would otherwise be loaded by the worker + + searchWorker = null; + let pendingScripts = 3; + + const onScriptLoaded = () => { + if (--pendingScripts === 0) { + initSearchIndex(); + nonWorkerSearchInitialized = true; + if (searchInput.focused) { + triggerSearch(searchInput.value); + } + } + }; + + const script1 = document.createElement('script'); + script1.src = (packageName === null ? rootUrlPrefix : packageUrlPrefix) + 'search-index.js'; + script1.async = true; + script1.onload = onScriptLoaded; + document.head.append(script1); + + const script2 = document.createElement('script'); + script2.src = rootUrlPrefix; + script2.async = true; + script2.onload = onScriptLoaded; + document.head.append(script2); + + const script3 = document.createElement('script'); + script3.src = workerScriptUrl; + script3.async = true; + script3.onload = onScriptLoaded; + document.head.append(script3); + } +} + +// Updates search results unless they are stale. +function handleSearchResults(query, results) { + if (query.inputValue !== searchInput.value) return; + + updateSearchResults(renderSearchResults(query, results)); +} + +// TODO: Should this (or its callers) use requestAnimationFrame() ? +// Removes any currently displayed search results, then displays the given results if non-null. +function updateSearchResults(resultsDiv) { + selectedSearchResult = null; + + const oldResultsDiv = document.getElementById('search-results'); + if (oldResultsDiv !== null) { + searchElement.removeChild(oldResultsDiv); + } + + if (resultsDiv != null) { + searchElement.append(resultsDiv); + selectNextResult(resultsDiv.firstElementChild); + } +} + +// Returns the module of the given member, or `null` if the given member is a module. +function getModule(member) { + switch (member.level) { + case 0: + return null; + case 1: + return member.parent; + case 2: + return member.parent.parent; + } +} + +// Triggers a search unless search input is invalid or incomplete. +function triggerSearch(inputValue) { + const query = parseSearchInput(inputValue); + if (!isActionableQuery(query)) { + handleSearchResults(query, null); + return; + } + + if (searchWorker !== null) { + searchWorker.postMessage({query, packageName, moduleName, className}); + } else if (nonWorkerSearchInitialized) { + const results = runSearch(query, packageName, moduleName, className); + handleSearchResults(query, results); + } +} + +// Tells if the given Unicode character is a whitespace character. +function isWhitespace(ch) { + const cp = ch.codePointAt(0); + if (cp >= 9 && cp <= 13 || cp === 32 || cp === 133 || cp === 160) return true; + if (cp < 5760) return false; + return cp === 5760 || cp >= 8192 && cp <= 8202 + || cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287 || cp === 12288; +} + +// Trims the given Unicode characters. +function trim(chars) { + const length = chars.length; + let startIdx, endIdx; + + for (startIdx = 0; startIdx < length; startIdx += 1) { + if (!isWhitespace(chars[startIdx])) break; + } + for (endIdx = chars.length - 1; endIdx > startIdx; endIdx -= 1) { + if (!isWhitespace(chars[endIdx])) break; + } + return chars.slice(startIdx, endIdx + 1); +} + +// Parses the user provided search input. +// Preconditions: +// inputValue !== '' +function parseSearchInput(inputValue) { + const chars = trim(Array.from(inputValue)); + const char0 = chars[0]; // may be undefined + const char1 = chars[1]; // may be undefined + const prefix = char1 === ':' ? char0 + char1 : null; + const kind = + prefix === null ? null : + char0 === 'm' ? 1 : + char0 === 't' ? 2 : + char0 === 'c' ? 3 : + char0 === 'f' ? 4 : + char0 === 'p' ? 5 : + undefined; + const unprefixedChars = kind !== null && kind !== undefined ? + trim(chars.slice(2, chars.length)) : + chars; + const normalizedCps = toNormalizedCodePoints(unprefixedChars); + return {inputValue, prefix, kind, normalizedCps}; +} + +// Converts a Unicode character array to an array of normalized Unicode code points. +// Normalization turns characters into their base forms, e.g., é into e. +// Since JS doesn't support case folding, `toLocaleLowerCase()` is used instead. +// Note: Keep in sync with same function in search-worker.js. +function toNormalizedCodePoints(characters) { + return Uint32Array.from(characters, ch => ch.normalize('NFD')[0].toLocaleLowerCase().codePointAt(0)); +} + +// Tells if the given query is valid and long enough to be worth running. +// Prefixed queries require fewer minimum characters than unprefixed queries. +// This avoids triggering a search while typing a prefix yet still enables searching for single-character names. +// For example, `p:e` finds `pkl.math#E`. +function isActionableQuery(query) { + const kind = query.kind; + const queryCps = query.normalizedCps; + return kind !== undefined && (kind !== null && queryCps.length > 0 || queryCps.length > 1); +} + +// Renders the given search results for the given query. +// Preconditions: +// isActionableQuery(query) ? results !== null : results === null +function renderSearchResults(query, results) { + const resultsDiv = document.createElement('div'); + resultsDiv.id = 'search-results'; + const ul = document.createElement('ul'); + resultsDiv.append(ul); + + if (results === null) { + if (query.kind !== undefined) return null; + + const li = document.createElement('li'); + li.className = 'heading'; + li.textContent = 'Unknown search prefix. Use one of m: (module), c: (class), f: (function), or p: (property).'; + ul.append(li); + return resultsDiv; + } + + const {exactMatches, classMatches, moduleMatches, otherMatches} = results; + + if (exactMatches.length + classMatches.length + moduleMatches.length + otherMatches.length === 0) { + renderHeading('No results found', ul); + return resultsDiv; + } + + if (exactMatches.length > 0) { + renderHeading('Top hits', ul); + renderMembers(query.normalizedCps, exactMatches, ul); + } + if (classMatches.length > 0) { + renderHeading('Class', ul, className); + renderMembers(query.normalizedCps, classMatches, ul); + } + if (moduleMatches.length > 0) { + renderHeading('Module', ul, moduleName); + renderMembers(query.normalizedCps, moduleMatches, ul); + } + if (otherMatches.length > 0) { + renderHeading('Other results', ul); + renderMembers(query.normalizedCps, otherMatches, ul); + } + + return resultsDiv; +} + +// Adds a heading such as `Top matches` to the search results list. +function renderHeading(title, ul, name = null) { + const li = document.createElement('li'); + li.className = 'heading'; + li.append(title); + if (name != null) { + li.append(' '); + li.append(span('heading-name', name)) + } + ul.append(li); +} + +// Adds matching members to the search results list. +function renderMembers(queryCps, members, ul) { + for (const member of members) { + ul.append(renderMember(queryCps, member)); + } +} + +// Renders a member to be added to the search result list. +function renderMember(queryCps, member) { + const result = document.createElement('li'); + result.className = 'result'; + if (member.deprecated) result.className = 'deprecated'; + + const link = document.createElement('a'); + result.append(link); + + link.href = (packageName === null ? rootUrlPrefix : packageUrlPrefix) + member.url; + link.addEventListener('mousedown', () => selectResult(result)); + link.addEventListener('click', clearSearch); + + const keyword = getKindKeyword(member.kind); + // noinspection JSValidateTypes (IntelliJ bug?) + if (keyword !== null) { + link.append(span('keyword', keyword), ' '); + } + + // prefix with class name if a class member + if (member.level === 2) { + link.append(span("context", member.parent.name + '.')); + } + + const name = span('result-name'); + if (member.matchNameIdx === 0) { // main name matched + highlightMatch(queryCps, member.names[0], member.matchStartIdx, name); + } else { // aka name matched + name.append(member.name); + } + link.append(name); + + if (member.signature !== null) { + link.append(member.signature); + } + + if (member.matchNameIdx > 0) { // aka name matched + link.append(' '); + const aka = span('aka'); + aka.append('(known as: '); + const name = span('aka-name'); + highlightMatch(queryCps, member.names[member.matchNameIdx], member.matchStartIdx, name); + aka.append(name, ')'); + link.append(aka); + } + + // add module name if not a module + const module = getModule(member); + if (module !== null) { + link.append(' ', span('context', '(' + module.name + ')')); + } + + return result; +} + +// Returns the keyword for the given member kind. +function getKindKeyword(kind) { + switch (kind) { + case 0: + return "package"; + case 1: + return "module"; + case 2: + return "typealias"; + case 3: + return "class"; + case 4: + return "function"; + case 5: + // properties have no keyword + return null; + } +} + +// Highlights the matching characters in a member name. +// Preconditions: +// queryCps.length > 0 +// computeMatchFrom(queryCps, name.normalizedCps, name.wordStarts, matchStartIdx) +function highlightMatch(queryCps, name, matchStartIdx, parentElem) { + const queryLength = queryCps.length; + const codePoints = name.codePoints; + const nameCps = name.normalizedCps; + const nameLength = nameCps.length; + const wordStarts = name.wordStarts; + + let queryIdx = 0; + let queryCp = queryCps[0]; + let startIdx = matchStartIdx; + + if (startIdx > 0) { + parentElem.append(String.fromCodePoint(...codePoints.subarray(0, startIdx))); + } + + for (let nameIdx = startIdx; nameIdx < nameLength; nameIdx += 1) { + const nameCp = nameCps[nameIdx]; + + if (queryCp !== nameCp) { + const newNameIdx = wordStarts[nameIdx]; + parentElem.append( + span('highlight', String.fromCodePoint(...codePoints.subarray(startIdx, nameIdx)))); + startIdx = newNameIdx; + parentElem.append(String.fromCodePoint(...codePoints.subarray(nameIdx, newNameIdx))); + nameIdx = newNameIdx; + } + + queryIdx += 1; + if (queryIdx === queryLength) { + parentElem.append( + span('highlight', String.fromCodePoint(...codePoints.subarray(startIdx, nameIdx + 1)))); + if (nameIdx + 1 < nameLength) { + parentElem.append(String.fromCodePoint(...codePoints.subarray(nameIdx + 1, nameLength))); + } + return; + } + + queryCp = queryCps[queryIdx]; + } + + throw 'Precondition violated: `computeMatchFrom()`'; +} + +// Creates a span element. +function span(className, text = null) { + const result = document.createElement('span'); + result.className = className; + result.textContent = text; + return result; +} + +// Creates a text node. +function text(content) { + return document.createTextNode(content); +} + +// Navigates to the next member entry in the search results list, skipping headings. +function selectNextResult(ul) { + let next = selectedSearchResult === null ? ul.firstElementChild : selectedSearchResult.nextElementSibling; + while (next !== null) { + if (!next.classList.contains('heading')) { + selectResult(next); + scrollIntoView(next, { + behavior: 'instant', // better for keyboard navigation + scrollMode: 'if-needed', + block: 'nearest', + inline: 'nearest', + }); + return; + } + next = next.nextElementSibling; + } +} + +// Navigates to the previous member entry in the search results list, skipping headings. +function selectPrevResult(ul) { + let prev = selectedSearchResult === null ? ul.lastElementChild : selectedSearchResult.previousElementSibling; + while (prev !== null) { + if (!prev.classList.contains('heading')) { + selectResult(prev); + const prev2 = prev.previousElementSibling; + // make any immediately preceding heading visible as well (esp. important for first heading) + const scrollTo = prev2 !== null && prev2.classList.contains('heading') ? prev2 : prev; + scrollIntoView(scrollTo, { + behavior: 'instant', // better for keyboard navigation + scrollMode: 'if-needed', + block: 'nearest', + inline: 'nearest', + }); + return; + } + prev = prev.previousElementSibling; + } +} + +// Selects the given entry in the search results list. +function selectResult(li) { + if (selectedSearchResult !== null) { + selectedSearchResult.classList.remove('selected'); + } + li.classList.add('selected'); + selectedSearchResult = li; +} + +// Clears the search input and hides/removes the search results list. +function clearSearch() { + searchInput.value = ''; + updateSearchResults(null); +} + +const updateRuntimeDataWith = (buildAnchor) => (fragmentId, entries) => { + if (!entries) return; + const fragment = document.createDocumentFragment(); + let first = true; + for (const entry of entries) { + const a = document.createElement("a"); + buildAnchor(entry, a); + if (first) { + first = false; + } else { + fragment.append(", "); + } + fragment.append(a); + } + + const element = document.getElementById(fragmentId); + element.append(fragment); + element.classList.remove("hidden"); // dd + element.previousElementSibling.classList.remove("hidden"); // dt +} + +// Functions called by JS data scripts. +// noinspection JSUnusedGlobalSymbols +const runtimeData = { + knownVersions: (versions, myVersion) => { + updateRuntimeDataWith((entry, anchor) => { + const { text, href } = entry; + anchor.textContent = text; + // noinspection JSUnresolvedReference + if (text === myVersion) { + anchor.className = "current-version"; + } else if (href) { + anchor.href = href; + } + })("known-versions", versions); + }, + knownUsagesOrSubtypes: updateRuntimeDataWith((entry, anchor) => { + const { text, href } = entry; + anchor.textContent = text; + // noinspection JSUnresolvedReference + anchor.textContent = text; + if (href) { + anchor.href = href; + } + }), +} diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/scripts/scroll-into-view.min.js b/pkl-doc/src/test/files/SinglePackageTest/output/scripts/scroll-into-view.min.js new file mode 100644 index 00000000..de62d093 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/scripts/scroll-into-view.min.js @@ -0,0 +1,30 @@ +/** + * MIT License + * + * Copyright (c) 2023 Cody Olsen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i=n)return setElementScroll(e,l.x,l.y),e._scrollSettings=null,t.end(COMPLETE);var r=1-t.ease(o);if(setElementScroll(e,l.x-l.differenceX*r,l.y-l.differenceY*r),i>=t.time)return t.endIterations++,animate(e);raf(animate.bind(null,e))}}function defaultIsWindow(e){return e.self===e}function transitionScrollTo(e,t,n,l){var i,o=!t._scrollSettings,r=t._scrollSettings,a=Date.now(),s={passive:!0};function f(e){t._scrollSettings=null,t.parentElement&&t.parentElement._scrollSettings&&t.parentElement._scrollSettings.end(e),n.debug&&console.log("Scrolling ended with type",e,"for",t),l(e),i&&(t.removeEventListener("touchstart",i,s),t.removeEventListener("wheel",i,s))}r&&r.end(CANCELED);var c=n.maxSynchronousAlignments;return null==c&&(c=3),t._scrollSettings={startTime:a,endIterations:0,target:e,time:n.time,ease:n.ease,align:n.align,isWindow:n.isWindow||defaultIsWindow,maxSynchronousAlignments:c,end:f},"cancellable"in n&&!n.cancellable||(i=f.bind(null,CANCELED),t.addEventListener("touchstart",i,s),t.addEventListener("wheel",i,s)),o&&animate(t),i}function defaultIsScrollable(e){return"pageXOffset"in e||(e.scrollHeight!==e.clientHeight||e.scrollWidth!==e.clientWidth)&&"hidden"!==getComputedStyle(e).overflow}function defaultValidTarget(){return!0}function findParentElement(e){if(e.assignedSlot)return findParentElement(e.assignedSlot);if(e.parentElement)return"BODY"===e.parentElement.tagName?e.parentElement.ownerDocument.defaultView||e.parentElement.ownerDocument.ownerWindow:e.parentElement;if(e.getRootNode){var t=e.getRootNode();if(11===t.nodeType)return t.host}}module.exports=function(e,t,n){if(e){"function"==typeof t&&(n=t,t=null),t||(t={}),t.time=isNaN(t.time)?1e3:t.time,t.ease=t.ease||function(e){return 1-Math.pow(1-e,e/2)};var l,i=findParentElement(e),o=1,r=t.validTarget||defaultValidTarget,a=t.isScrollable;for(t.debug&&(console.log("About to scroll to",e),i||console.error("Target did not have a parent, is it mounted in the DOM?"));i;)if(t.debug&&console.log("Scrolling parent node",i),r(i,o)&&(a?a(i,defaultIsScrollable):defaultIsScrollable(i))&&(o++,l=transitionScrollTo(e,i,t,s)),!(i=findParentElement(i))){s(COMPLETE);break}return l}function s(e){--o||n&&n(e)}}; + + },{}],2:[function(require,module,exports){ + window.scrollIntoView=require("./scrollIntoView"); + + },{"./scrollIntoView":1}]},{},[2]); diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/scripts/search-worker.js b/pkl-doc/src/test/files/SinglePackageTest/output/scripts/search-worker.js new file mode 100644 index 00000000..224b731e --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/scripts/search-worker.js @@ -0,0 +1,282 @@ +// noinspection DuplicatedCode + +'use strict'; + +// populated by `initSearchIndex()` +let searchIndex; + +// noinspection ThisExpressionReferencesGlobalObjectJS +const isWorker = 'DedicatedWorkerGlobalScope' in this; + +if (isWorker) { + const workerName = self.name; + // relative to this file + const searchIndexUrl = workerName === "main" ? + '../search-index.js' : + '../' + workerName + '/search-index.js'; + importScripts(searchIndexUrl); + initSearchIndex(); + addEventListener('message', e => { + const {query, packageName, moduleName, className} = e.data; + const results = runSearch(query, packageName, moduleName, className); + postMessage({query, results}); + }); +} else { + // non-worker environment + // `pkldoc.js` loads scripts and calls `initSearchIndex()` +} + +// Initializes the search index. +function initSearchIndex() { + // noinspection JSUnresolvedVariable + const data = JSON.parse(searchData); + const index = Array(data.length); + let idx = 0; + + for (const entry of data) { + const name = entry.name; + const names = toIndexedNames(entry); + // 0 -> package, 1 -> module, 2 -> type alias, 3 -> class, 4 -> function, 5 -> property + const kind = entry.kind; + const url = entry.url; + // noinspection JSUnresolvedVariable + const signature = entry.sig === undefined ? null : entry.sig; + // noinspection JSUnresolvedVariable + const parent = entry.parId === undefined ? null : index[entry.parId]; + const level = parent === null ? 0 : parent.parent === null ? 1 : 2; + const deprecated = entry.deprecated !== undefined; + + index[idx++] = { + name, + names, + kind, + url, + signature, + parent, + level, + deprecated, + // remaining attributes are set by `computeMatchFrom` and hence aren't strictly part of the search index + matchNameIdx: -1, // names[matchNameIdx] is the name that matched + matchStartIdx: -1, // names[matchNameIdx].codePoints[matchStartIdx] is the first code point that matched + similarity: 0 // number of code points matched relative to total number of code points (between 0.0 and 1.0) + }; + } + + searchIndex = index; +} + +// Runs a search and returns its results. +function runSearch(query, packageName, moduleName, className) { + const queryCps = query.normalizedCps; + const queryKind = query.kind; + + let exactMatches = []; + let classMatches = []; + let moduleMatches = []; + let otherMatches = []; + + for (const member of searchIndex) { + if (queryKind !== null && queryKind !== member.kind) continue; + + if (!isMatch(queryCps, member)) continue; + + if (member.similarity === 1) { + exactMatches.push(member); + } else if (moduleName !== null && member.level === 1 && moduleName === member.parent.name) { + moduleMatches.push(member); + } else if (moduleName !== null && member.level === 2 && moduleName === member.parent.parent.name) { + if (className !== null && className === member.parent.name) { + classMatches.push(member); + } else { + moduleMatches.push(member); + } + } else { + otherMatches.push(member); + } + } + + // Sorts members best-first. + function compareMembers(member1, member2) { + const normDiff = member2.similarity - member1.similarity; // higher is better + if (normDiff !== 0) return normDiff; + + const lengthDiff = member1.matchNameLength - member2.matchNameLength; // lower is better + if (lengthDiff !== 0) return lengthDiff; + + const kindDiff = member2.kind - member1.kind; // higher is better + if (kindDiff !== 0) return kindDiff; + + return member1.matchNameIdx - member2.matchNameIdx; // lower is better + } + + exactMatches.sort(compareMembers); + classMatches.sort(compareMembers); + moduleMatches.sort(compareMembers); + otherMatches.sort(compareMembers); + + return {exactMatches, classMatches, moduleMatches, otherMatches}; +} + +// Indexes a member's names. +function toIndexedNames(entry) { + const result = []; + result.push(toIndexedName(entry.name)); + // noinspection JSUnresolvedVariable + const alsoKnownAs = entry.aka; + if (alsoKnownAs !== undefined) { + for (const name of alsoKnownAs) { + result.push(toIndexedName(name)); + } + } + return result; +} + +// Indexes the given name. +function toIndexedName(name) { + const characters = Array.from(name); + const codePoints = Uint32Array.from(characters, ch => ch.codePointAt(0)); + const normalizedCps = toNormalizedCodePoints(characters); + const wordStarts = toWordStarts(characters); + + return {codePoints, normalizedCps, wordStarts}; +} + +// Converts a Unicode character array to an array of normalized Unicode code points. +// Normalization turns characters into their base forms, e.g., é into e. +// Since JS doesn't support case folding, `toLocaleLowerCase()` is used instead. +function toNormalizedCodePoints(characters) { + return Uint32Array.from(characters, ch => ch.normalize('NFD')[0].toLocaleLowerCase().codePointAt(0)); +} + +// Returns an array of same length as `characters` that for every index, holds the index of the next word start. +// Preconditions: +// characters.length > 0 +function toWordStarts(characters) { + const length = characters.length; + // -1 is used as 'no next word start exists' -> use signed int array + const result = length <= 128 ? new Int8Array(length) : new Int16Array(length); + + if (length > 1) { + let class1 = toCharClass(characters[length - 1]); + let class2; + let wordStart = -1; + for (let idx = length - 1; idx >= 1; idx -= 1) { + class2 = class1; + class1 = toCharClass(characters[idx - 1]); + const diff = class1 - class2; + // transitions other than uppercase -> other + if (diff !== 0 && diff !== 3) wordStart = idx; + result[idx] = wordStart; + // uppercase -> other + if (diff === 3) wordStart = idx - 1; + } + } + + // first character is always a word start + result[0] = 0; + + return result; +} + + +// Partitions characters into uppercase, digit, dot, and other. +function toCharClass(ch) { + const regexIsUppercase = /\p{Lu}/u + const regexIsNumericCharacter = /\p{N}/u + return regexIsUppercase.test(ch) ? 3 : regexIsNumericCharacter.test(ch) ? 2 : ch === '.' ? 1 : 0; +} + +// Tests if `queryCps` matches any of `member`'s names. +// If so, records information about the match in `member`. +// Preconditions: +// queryCps.length > 0 +function isMatch(queryCps, member) { + const queryLength = queryCps.length; + let nameIdx = 0; + + for (const name of member.names) { + const nameCps = name.normalizedCps; + const nameLength = nameCps.length; + const wordStarts = name.wordStarts; + const maxStartIdx = nameLength - queryLength; + + for (let startIdx = 0; startIdx <= maxStartIdx; startIdx += 1) { + const matchLength = computeMatchFrom(queryCps, nameCps, wordStarts, startIdx); + if (matchLength > 0) { + member.matchNameIdx = nameIdx; + member.matchStartIdx = startIdx; + // Treat exact match of last module name component as exact match (similarity == 1). + // For example, treat "PodSpec" as exact match for "io.k8s.api.core.v1.PodSpec". + // Because "ps" is considered an exact match for "PodSpec", + // it is also considered an exact match for "io.k8s.api.core.v1.PodSpec". + const isExactMatchOfLastModuleNameComponent = + startIdx > 0 && nameCps[startIdx - 1] === 46 /* '.' */ && matchLength === nameLength - startIdx; + member.similarity = isExactMatchOfLastModuleNameComponent ? 1 : matchLength / nameLength; + member.matchNameLength = nameLength; + return true; + } + } + + nameIdx += 1; + } + + return false; +} + +// Tests if the given query matches the given name from `startIdx` on. +// Returns the number of code points matched. +// Word start matches get special treatment. +// For example, `sb` is considered to match all code points of `StringBuilder`. +// Preconditions: +// queryCps.length > 0 +// nameCps.length > 0 +// wordStarts.length === nameCps.length +// startIdx < nameCps.length +function computeMatchFrom(queryCps, nameCps, wordStarts, startIdx) { + const queryLength = queryCps.length; + const nameLength = nameCps.length; + const beginsWithWordStart = wordStarts[startIdx] === startIdx; + + let queryIdx = 0; + let matchLength = 0; + let queryCp = queryCps[0]; + + for (let nameIdx = startIdx; nameIdx < nameLength; nameIdx += 1) { + const nameCp = nameCps[nameIdx]; + + if (queryCp === nameCp) { + matchLength += 1; + } else { // check for word start match + if (nameIdx === startIdx || !beginsWithWordStart) return 0; + + const newNameIdx = wordStarts[nameIdx]; + if (newNameIdx === -1) return 0; + + const newNameCp = nameCps[newNameIdx]; + if (queryCp !== newNameCp) return 0; + + matchLength += newNameIdx - nameIdx + 1; + nameIdx = newNameIdx; + } + + queryIdx += 1; + if (queryIdx === queryLength) { + // in case of a word start match, increase matchLength by number of remaining chars of the last matched word + const nextIdx = nameIdx + 1; + if (beginsWithWordStart && nextIdx < nameLength) { + const nextStart = wordStarts[nextIdx]; + if (nextStart === -1) { + matchLength += nameLength - nextIdx; + } else { + matchLength += nextStart - nextIdx; + } + } + + return matchLength; + } + + queryCp = queryCps[queryIdx]; + } + + return 0; +} diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/search-index.js b/pkl-doc/src/test/files/SinglePackageTest/output/search-index.js new file mode 100644 index 00000000..89a3f9ca --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/search-index.js @@ -0,0 +1 @@ +searchData='[{"name":"com.package1","kind":0,"url":"com.package1/current/index.html"},{"name":"com.package1.minimal","kind":1,"url":"com.package1/current/minimal/index.html"}]'; diff --git a/pkl-doc/src/test/files/SinglePackageTest/output/styles/pkldoc.css b/pkl-doc/src/test/files/SinglePackageTest/output/styles/pkldoc.css new file mode 100644 index 00000000..2c3632a3 --- /dev/null +++ b/pkl-doc/src/test/files/SinglePackageTest/output/styles/pkldoc.css @@ -0,0 +1,680 @@ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + src: local('Lato Regular'), local('Lato-Regular'), + url('../fonts/lato-v14-latin_latin-ext-regular.woff2') format('woff2') +} + +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 700; + src: local('Lato Bold'), local('Lato-Bold'), + url('../fonts/lato-v14-latin_latin-ext-700.woff2') format('woff2') +} + +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans Regular'), local('OpenSans-Regular'), + url('../fonts/open-sans-v15-latin_latin-ext-regular.woff2') format('woff2') +} + +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: local('Open Sans Italic'), local('OpenSans-Italic'), + url('../fonts/open-sans-v15-latin_latin-ext-italic.woff2') format('woff2') +} + +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), + url('../fonts/open-sans-v15-latin_latin-ext-700.woff2') format('woff2') +} + +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 700; + src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), + url('../fonts/open-sans-v15-latin_latin-ext-700italic.woff2') format('woff2') +} + +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Code Pro'), local('SourceCodePro-Regular'), + url('../fonts/source-code-pro-v7-latin_latin-ext-regular.woff2') format('woff2') +} + +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 700; + src: local('Source Code Pro Bold'), local('SourceCodePro-Bold'), + url('../fonts/source-code-pro-v7-latin_latin-ext-700.woff2') format('woff2') +} + +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: local('Material Icons'), + local('MaterialIcons-Regular'), + url(../fonts/MaterialIcons-Regular.woff2) format('woff2'); +} + +.material-icons { + /*noinspection CssNoGenericFontName*/ + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + display: inline-block; + width: 1em; + height: 1em; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: 'liga'; +} + +input[type=search] { + -webkit-appearance: textfield; +} + +input[type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} + +input::-moz-placeholder { + opacity: 1; +} + +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, font, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td { + border: 0; + font-family: inherit; + font-size: 100%; + font-style: inherit; + font-weight: inherit; + margin: 0; + outline: 0; + padding: 0; + vertical-align: baseline; +} + +body { + margin: 0; + font-family: Lato, Arial, sans-serif; + background-color: #f0f3f6; + scroll-behavior: smooth; +} + +a, a:visited, a:hover, a:active { + color: inherit; +} + +a:hover { + text-decoration: none; + transition: 0s; +} + +code, .member-modifiers, .member-signature, .doc-comment pre, #search-results li.result, .result-name, .heading-name, .aka-name { + font-family: "Source Code Pro", monospace; + letter-spacing: -0.03em; +} + +header { + position: fixed; + top: 0; + left: 0; + width: 100vw; /* vw to make sure that positioning is the same whether or not vertical scrollbar is displayed */ + height: 32px; + z-index: 1; + background-color: #364550; + padding: 7px 0 7px; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.18), 0 4px 8px rgba(0, 0, 0, 0.28); +} + +#doc-title { + position: absolute; + margin-top: 8px; + margin-left: 15px; +} + +#doc-title a { + color: #fff; + text-decoration: none; +} + +#search { + position: relative; + width: 50vw; + margin: 0 auto; +} + +#search-icon { + position: absolute; + left: 0; + top: 2px; + padding: 4px; + font-size: 21px; + color: #a5a9a9; +} + +#search-input { + margin-top: 2px; + width: 100%; + height: 28px; + text-indent: 28px; + font-size: 0.85em; + background-color: rgba(255, 255, 255, 0.2); + border: none; + border-radius: 3px; + color: #fff; +} + +#search-input:focus { + background-color: #6D7880; + outline: none; +} + +#search-input::placeholder { + text-align: center; + color: #A5A9A9; +} + +#search-input:focus::placeholder { + color: transparent; +} + +#search-results { + position: fixed; + box-sizing: border-box; + top: 38px; + left: 25vw; + right: 25vw; + width: 50vw; + max-height: 80%; + color: #103a51; + background: white; + border: solid 1px #6D7880; + border-radius: 3px; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.18), 0 4px 8px rgba(0, 0, 0, 0.28); + white-space: nowrap; + + overflow: auto; /* in safari, this slows down painting, blocking the ui */ + /*noinspection CssUnknownProperty*/ + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; +} + +#search-results a { + text-decoration: none; +} + +#search-results a:hover { + text-decoration: underline; +} + +#search-results ul { + list-style: none; + font-size: 0.9em; +} + +#search-results li { + padding: 0.2ch 3ch; + height: 17px; /* used same height regardless of which fonts are used in content */ +} + +#search-results li.heading { + background-color: #f0f3f6; + padding: 0.4ch 1ch; +} + +#search-results li.result { + font-size: 0.9em; +} + +#search-results .keyword { + color: #000082; +} + +#search-results .highlight { + font-weight: bold; +} + +#search-results .context { + color: gray; +} + +#search-results .selected, #search-results .selected .keyword, #search-results .selected .aka, #search-results .selected .context { + background: darkblue; + color: white; +} + +#search-results .deprecated { + text-decoration: line-through; +} + +/* make sure that line-through of highlighted region of selected search result has the right color */ +#search-results .deprecated.selected .highlight { + text-decoration: line-through; +} + +main { + width: 70%; + margin: 60px auto 20px; +} + +.declaration-parent-link { + margin: 0 0 1rem; +} + +#declaration-title { + font-size: 2em; + font-weight: bold; + color: #103a51; + margin: 0.5rem 0; +} + +#declaration-version { + color: #A5A9A9; + font-size: 0.9em; + vertical-align: bottom; + padding-left: 0.25em; +} + +.member-group-links { + margin: 0.75em 0 1em 0; +} + +.member-group-links li { + display: inline-block; + margin-right: 1em; +} + +.member-info { + display: grid; + grid-template-columns: auto 1fr; + line-height: 1.5; + margin-top: 0.5em; + font-size: 0.9em; +} + +.member-info dt { + grid-column: 1; + text-align: right; +} + +.member-info dd { + grid-column: 2; + margin-left: 0.5em; +} + +.copy-uri-button { + cursor: pointer; + font-size: inherit; + margin-left: 0.5em; +} + +.member-group { + /* for absolutely positioned anchors */ + position: relative; +} + +.member-group-title { + margin: 1rem; + font-weight: bold; + color: #103a51; +} + +.toggle-inherited-members { + font-size: 0.9em; + font-weight: normal; + margin-left: 0.5em; +} + +.button-link { + text-decoration: underline; +} + +.button-link:hover, .button-link:active { + text-decoration: none; + cursor: pointer; +} + +.member-group ul { + list-style: none; +} + +.member-group li { + /* for absolutely positioned anchors */ + position: relative; +} + +.anchor, +.anchor-param1, +.anchor-param2, +.anchor-param3, +.anchor-param4, +.anchor-param5, +.anchor-param6, +.anchor-param7, +.anchor-param8, +.anchor-param9 { + position: absolute; + top: -60px; + left: 0; +} + +.anchor:target ~ .member, +.anchor-param1:target ~ .member, +.anchor-param2:target ~ .member, +.anchor-param3:target ~ .member, +.anchor-param4:target ~ .member, +.anchor-param5:target ~ .member, +.anchor-param6:target ~ .member, +.anchor-param7:target ~ .member, +.anchor-param8:target ~ .member, +.anchor-param9:target ~ .member { + border-left: 3px solid #222832; +} + +.anchor:target ~ .member .name-decl, +.anchor-param1:target ~ .member .param1, +.anchor-param2:target ~ .member .param2, +.anchor-param3:target ~ .member .param3, +.anchor-param4:target ~ .member .param4, +.anchor-param5:target ~ .member .param5, +.anchor-param6:target ~ .member .param6, +.anchor-param7:target ~ .member .param7, +.anchor-param8:target ~ .member .param8, +.anchor-param9:target ~ .member .param9 { + font-weight: bold; +} + +.member { + border-left: 3px solid transparent; + margin: 0 auto 0.5rem; + background-color: #fff; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + font-size: 0.9em; + padding: 10px; + color: #222832; +} + +.member:hover { + background-color: #f2f2f2; +} + +.member-left { + width: 25%; + display: inline; + float: left; + padding-right: 6px; + min-height: 1px; + text-align: right; +} + +.member-modifiers { + color: #000082; +} + +.member-main { + display: block; + overflow: hidden; +} + +.member-deprecated { + text-decoration: line-through; +} + +.member-selflink { + visibility: hidden; + display: inline; + float: left; + padding-right: 20px; + color: #222832; + text-decoration: none; +} + +.member-source-link { + visibility: hidden; + color: #fff; + background-color: #868e96; + display: inline-block; + margin-left: 1em; + padding: .25em .4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + vertical-align: bottom; + border-radius: .25rem +} + +.member-source-link:visited, .member-source-link:hover, .member-source-link:active { + color: #fff; +} + +.member:hover .member-source-link, .member:hover .member-selflink { + visibility: visible; +} + +.member.inherited, .member.hidden-member { + opacity: 0.75; +} + +.member.inherited .context { + color: gray; +} + +.member.with-page-link, .member.with-expandable-docs { + cursor: pointer; +} + +.member .expandable-docs-icon { + float: right; +} + +/* +Don't style a.name-decl as link +because the entire .member.with-page-link is effectively a link (via JS). +*/ +.member.with-page-link a.name-decl { + text-decoration: none; +} + +.expandable { + transform: scaleY(1); + transition: transform 0.25s; +} + +.expandable.collapsed { + transform: scaleY(0); +} + +.expandable.hidden { + display: none; +} + +#_declaration .expandable { + transform: none; + transition: none; +} + +#_declaration .expandable.collapsed { + mask: linear-gradient(rgb(0 0 0), transparent) content-box; + height: 100px; +} + +#_declaration .expandable.hidden { + display: block; +} + +/* show an otherwise hidden inherited member if it's a link target */ +.anchor:target + .expandable.collapsed.hidden { + display: inherit; + transform: scaleY(1); +} + +.doc-comment { + color: #103a51; + margin-top: 0.5rem; + font-family: "Open Sans", sans-serif; + font-size: 0.9em; +} + +.doc-comment p { + margin: 0.7em 0; +} + +.doc-comment p:first-child { + margin-top: 0; +} + +.doc-comment p:last-child { + margin-bottom: 0; +} + +.doc-comment h1, +.doc-comment h2, +.doc-comment h3, +.doc-comment h4, +.doc-comment h5, +.doc-comment h6 { + margin-bottom: 0.7em; + margin-top: 1.4em; + display: block; + text-align: left; + font-weight: bold; +} + +.doc-comment pre { + padding: 0.5em; + border: 0 solid #ddd; + background-color: #364550; + color: #ddd; + margin: 5px 0; + display: block; + border-radius: 0.2em; + overflow-x: auto; +} + +.doc-comment ul { + display: block; + list-style: circle; + padding-left: 20px; +} + +.doc-comment ol { + display: block; + padding-left:20px; +} + +.doc-comment ol.decimal { + list-style: decimal; +} + +.doc-comment ol.lowerAlpha { + list-style: lower-alpha; +} + +.doc-comment ol.upperAlpha { + list-style: upper-alpha; +} + +.doc-comment ol.lowerRoman { + list-style: lower-roman; +} + +.doc-comment ol.upperRoman { + list-style: upper-roman; +} + +.doc-comment li { + display: list-item; +} + +.doc-comment code { + font-weight: normal; +} + +.doc-comment em, .doc-comment i { + font-style: italic; +} + +.doc-comment strong, .doc-comment b { + font-weight: bold; +} + +.runtime-data.hidden { + display: none; +} + +.runtime-data .current-version { + font-weight: bold; +} + +/* +Styling for Markdown tables in doc comments. +From: https://gist.github.com/andyferra/2554919 +*/ + +table { + padding: 0; +} + +table tr { + border-top: 1px solid #cccccc; + background-color: white; + margin: 0; + padding: 0; +} + +table tr:nth-child(2n) { + background-color: #f8f8f8; +} + +table tr th { + font-weight: bold; + border: 1px solid #cccccc; + text-align: left; + margin: 0; + padding: 6px 13px; +} + +table tr td { + border: 1px solid #cccccc; + text-align: left; + margin: 0; + padding: 6px 13px; +} + +table tr th :first-child, table tr td :first-child { + margin-top: 0; +} + +table tr th :last-child, table tr td :last-child { + margin-bottom: 0; +} diff --git a/pkl-doc/src/test/kotlin/org/pkl/doc/CliDocGeneratorTest.kt b/pkl-doc/src/test/kotlin/org/pkl/doc/CliDocGeneratorTest.kt index e40604ba..78bc6a93 100644 --- a/pkl-doc/src/test/kotlin/org/pkl/doc/CliDocGeneratorTest.kt +++ b/pkl-doc/src/test/kotlin/org/pkl/doc/CliDocGeneratorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,6 +75,8 @@ class CliDocGeneratorTest { // Run the doc generator three times; second time adds new versions for the `birds` package @JvmStatic private fun generateDocs(): List = helper.generateDocs() + + @JvmStatic private fun generateSpDocs(): List = helper.generateSpDocs() } @Test @@ -159,6 +161,16 @@ class CliDocGeneratorTest { ) } + @ParameterizedTest + @MethodSource("generateSpDocs") + fun testSp(relativeFilePath: String) { + DocTestUtils.testExpectedFile( + helper.spExpectedOutputDir, + helper.spBaseActualOutputDir, + relativeFilePath, + ) + } + @Test fun `creates a symlink called current by default`(@TempDir tempDir: Path) { PackageServer.populateCacheDir(tempDir) diff --git a/pkl-doc/src/test/kotlin/org/pkl/doc/DocGeneratorTest.kt b/pkl-doc/src/test/kotlin/org/pkl/doc/DocGeneratorTest.kt index b2aa8d8f..d498a0db 100644 --- a/pkl-doc/src/test/kotlin/org/pkl/doc/DocGeneratorTest.kt +++ b/pkl-doc/src/test/kotlin/org/pkl/doc/DocGeneratorTest.kt @@ -24,8 +24,13 @@ class DocGeneratorTest { @Test @EnabledForJreRange(min = JRE.JAVA_21) fun `uses virtual thread executor on JDK 21+`() { - // On older JDKs, we get a ThreadPoolExecutor. - assertThat(DocGenerator.createDefaultExecutor().javaClass.canonicalName) - .isEqualTo("java.util.concurrent.ThreadPerTaskExecutor") + val executor = DocGenerator.createDefaultExecutor() + try { + // On older JDKs, we get a ThreadPoolExecutor. + assertThat(executor.javaClass.canonicalName) + .isEqualTo("java.util.concurrent.ThreadPerTaskExecutor") + } finally { + executor.shutdown() + } } } diff --git a/pkl-doc/src/test/kotlin/org/pkl/doc/DocGeneratorTestHelper.kt b/pkl-doc/src/test/kotlin/org/pkl/doc/DocGeneratorTestHelper.kt index 1e1cfd8a..222ed22b 100644 --- a/pkl-doc/src/test/kotlin/org/pkl/doc/DocGeneratorTestHelper.kt +++ b/pkl-doc/src/test/kotlin/org/pkl/doc/DocGeneratorTestHelper.kt @@ -40,6 +40,10 @@ class DocGeneratorTestHelper { projectDir.resolve("src/test/files/DocGeneratorTest/input").apply { assert(exists()) } } + private val spInputDir: Path by lazy { + projectDir.resolve("src/test/files/SinglePackageTest/input").apply { assert(exists()) } + } + internal val docsiteModule: URI by lazy { inputDir.resolve("docsite-info.pkl").apply { assert(exists()) }.toUri() } @@ -68,35 +72,85 @@ class DocGeneratorTestHelper { .map { it.toUri() } } + internal val spInputModules: List by lazy { + // intentionally not filtered + spInputDir.listFilesRecursively().map { it.toUri() } + } + internal val expectedOutputDir: Path by lazy { projectDir.resolve("src/test/files/DocGeneratorTest/output").createDirectories() } + internal val spExpectedOutputDir: Path by lazy { + projectDir.resolve("src/test/files/SinglePackageTest/output").createDirectories() + } + internal val expectedOutputFiles: List by lazy { expectedOutputDir.listFilesRecursively() } + internal val spExpectedOutputFiles: List by lazy { + spExpectedOutputDir.listFilesRecursively() + } + val baseActualOutputDir: Path by lazy { tempDir.resolve("work/DocGeneratorTest").createDirectories() } + val spBaseActualOutputDir: Path by lazy { + tempDir.resolve("work/SinglePackageTest").createDirectories() + } + val actualOutputDir: Path by lazy { baseActualOutputDir.resolve("run-1") } val actualOutputDir2: Path by lazy { baseActualOutputDir.resolve("run-2") } internal val actualOutputFiles: List by lazy { baseActualOutputDir.listFilesRecursively() } + internal val spActualOutputFiles: List by lazy { + spBaseActualOutputDir.listFilesRecursively() + } + internal val cacheDir: Path by lazy { tempDir.resolve("cache") } internal val expectedRelativeOutputFiles: List by lazy { - expectedOutputFiles.map { path -> - IoUtils.toNormalizedPathString(expectedOutputDir.relativize(path)).let { str -> - // Git will by default clone symlinks as shortcuts on Windows, and shortcuts have a - // `.lnk` extension. - if (IoUtils.isWindows() && str.endsWith(".lnk")) str.dropLast(4) else str - } - } + expectedOutputFiles.map { path -> relativeOutputPath(expectedOutputDir, path) } + } + + internal val spExpectedRelativeOutputFiles: List by lazy { + spExpectedOutputFiles.map { path -> relativeOutputPath(spExpectedOutputDir, path) } } internal val actualRelativeOutputFiles: List by lazy { - actualOutputFiles.map { IoUtils.toNormalizedPathString(baseActualOutputDir.relativize(it)) } + actualOutputFiles.map { relativeOutputPath(baseActualOutputDir, it) } + } + + internal val spActualRelativeOutputFiles: List by lazy { + spActualOutputFiles.map { relativeOutputPath(spBaseActualOutputDir, it) } + } + + internal val actualRelativeOutputFileSet: Set by lazy { + actualRelativeOutputFiles.toSet() + } + + internal val spActualRelativeOutputFileSet: Set by lazy { + spActualRelativeOutputFiles.toSet() + } + + private fun relativeOutputPath(baseDir: Path, path: Path): String { + return IoUtils.toNormalizedPathString(baseDir.relativize(path)).let { str -> + // Git will by default clone symlinks as shortcuts on Windows, and shortcuts have a + // `.lnk` extension. + if (IoUtils.isWindows() && str.endsWith(".lnk")) str.dropLast(4) else str + } + } + + private fun assertExpectedFilesGenerated(expectedFiles: List, actualFiles: Set) { + val missingFiles = expectedFiles - actualFiles + if (missingFiles.isNotEmpty()) { + val error = buildString { + appendLine("The following expected files were not actually generated:") + missingFiles.forEach { appendLine(it) } + } + Assertions.fail(error) + } } fun runPklDocCli(executable: Path, options: CliDocGeneratorOptions) { @@ -183,14 +237,7 @@ class DocGeneratorTestHelper { ) doGenerate(options2) - val missingFiles = expectedRelativeOutputFiles - actualRelativeOutputFiles - if (missingFiles.isNotEmpty()) { - val error = buildString { - appendLine("The following expected files were not actually generated:") - missingFiles.forEach { appendLine(it) } - } - Assertions.fail(error) - } + assertExpectedFilesGenerated(expectedRelativeOutputFiles, actualRelativeOutputFileSet) return actualRelativeOutputFiles } @@ -202,4 +249,21 @@ class DocGeneratorTestHelper { fun generateDocs(): List { return generateDocsWith { CliDocGenerator(it).run() } } + + fun generateSpDocs(): List { + PackageServer.populateCacheDir(cacheDir) + + val options = + CliDocGeneratorOptions( + CliBaseOptions(sourceModules = spInputModules, moduleCacheDir = cacheDir), + outputDir = spBaseActualOutputDir, + isTestMode = true, + noSymlinks = true, + ) + CliDocGenerator(options).run() + + assertExpectedFilesGenerated(spExpectedRelativeOutputFiles, spActualRelativeOutputFileSet) + + return spActualRelativeOutputFiles + } } diff --git a/pkl-doc/src/test/kotlin/org/pkl/doc/JavaExecutableTest.kt b/pkl-doc/src/test/kotlin/org/pkl/doc/JavaExecutableTest.kt index bfca1d95..59f35afa 100644 --- a/pkl-doc/src/test/kotlin/org/pkl/doc/JavaExecutableTest.kt +++ b/pkl-doc/src/test/kotlin/org/pkl/doc/JavaExecutableTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ class JavaExecutableTest { helper.generateDocsWithCli(Executables.pkldoc.javaExecutable) } - @ParameterizedTest() + @ParameterizedTest @MethodSource("generateDocs") fun test(relativePath: String) { DocTestUtils.testExpectedFile( diff --git a/pkl-doc/src/test/kotlin/org/pkl/doc/NativeExecutableTest.kt b/pkl-doc/src/test/kotlin/org/pkl/doc/NativeExecutableTest.kt index 2fb013a5..d15371a6 100644 --- a/pkl-doc/src/test/kotlin/org/pkl/doc/NativeExecutableTest.kt +++ b/pkl-doc/src/test/kotlin/org/pkl/doc/NativeExecutableTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ class NativeExecutableTest { } } - @ParameterizedTest() + @ParameterizedTest @MethodSource("generateDocs") fun test(relativePath: String) { DocTestUtils.testExpectedFile(