diff --git a/docs/docs/usage/dottydoc.md b/docs/docs/usage/dottydoc.md index dcf056a7d57b..f7e15b692d99 100644 --- a/docs/docs/usage/dottydoc.md +++ b/docs/docs/usage/dottydoc.md @@ -1,6 +1,6 @@ --- layout: doc-page -title: Dottydoc +title: Dottydoc [Legacy] --- Dottydoc is a tool to generate a combined documentation and API reference for diff --git a/docs/docs/usage/scala3doc/blog.md b/docs/docs/usage/scala3doc/blog.md new file mode 100644 index 000000000000..af257074dce9 --- /dev/null +++ b/docs/docs/usage/scala3doc/blog.md @@ -0,0 +1,9 @@ +--- +title: Built-in blog +--- + +# {{page.title}} + +Scala3doc allows you to include a simple blog in your documentation. For now, it +provides only basic features. In the future, we plan to include more advanced +features like tagging or author pages. diff --git a/docs/docs/usage/scala3doc/docComments.md b/docs/docs/usage/scala3doc/docComments.md new file mode 100644 index 000000000000..cd33cf3000e1 --- /dev/null +++ b/docs/docs/usage/scala3doc/docComments.md @@ -0,0 +1,96 @@ +--- +title: API Documentation +--- + +# {{ page.title }} + +Scala3doc's main feature is creating API documentation from code comments. + +By default, the code comments are understood as Markdown, though we also support +Scaladoc's old [Wiki syntax](https://docs.scala-lang.org/style/scaladoc.html). + +## Syntax + +### Definition links + +Our definition link syntax is quite close to Scaladoc's syntax, though we have made some +quality-of-life improvements. + +#### Basic syntax + +A definition link looks as follows: `[[scala.collection.immutable.List]]`. + +Which is to say, a definition link is a sequence of identifiers separated by +`.`. The identifiers can be separated with `#` as well for Scaladoc compatibility. + +By default, an identifier `id` references the first (in source order) entity +named `id`. An identifier can end with `$`, which forces it to refer to a value +(an object, a value, a given); an identifier can also end with `!`, which forces +it to refer to a type (a class, a type alias, a type member). + +The links are resolved relative to the current location in source. That is, when +documenting a class, the links are relative to the entity enclosing the class (a +package, a class, an object); the same applies to documenting definitions. + +Special characters in links can be backslash-escaped, which makes them part of +identifiers instead. For example, `` [[scala.collection.immutable\.List]] `` +references the class named `` `immutable.List` `` in package `scala.collection`. + +#### New syntax + +We have extended Scaladoc definition links to make them a bit more pleasant to +write and read in source. The aim was also to bring the link and Scala syntax +closer together. The new features are: + +1. `package` can be used as a prefix to reference the enclosing package + Example: + ``` + package utils + class C { + def foo = "foo". + } + /** See also [[package.C]]. */ + class D { + def bar = "bar". + } + ``` + The `package` keyword helps make links to the enclosing package shorter + and a bit more resistant to name refactorings. +1. `this` can be used as a prefix to reference the enclosing classlike + Example: + ``` + class C { + def foo = "foo" + /** This is not [[this.foo]], this is bar. */ + def bar = "bar" + } + ``` + Using a Scala keyword here helps make the links more familiar, as well as + helps the links survive class name changes. +1. Backticks can be used to escape identifiers + Example: + ``` + def `([.abusive.])` = ??? + /** TODO: Figure out what [[`([.abusive.])`]] is. */ + def foo = `([.abusive.])` + ``` + Scaladoc required backslash-escaping to reference such identifiers. Instead, + Scala3doc allows using the familiar Scala backtick quotation. + +#### Why keep the Wiki syntax for links? + +There are a few reasons why we've kept the Wiki syntax for documentation links +instead of reusing the Markdown syntax. Those are: + +1. Nameless links in Markdown are ugly: `[](definition)` vs `[[definition]]` + By far, most links in documentation are nameless. It should be obvious how to + write them. +2. Local member lookup collides with URL fragments: `[](#field)` vs `[[#field]]` +3. Overload resolution collides with MD syntax: `[](meth(Int))` vs `[[meth(Int)]]` +4. Now that we have a parser for the link syntax, we can allow spaces inside (in + Scaladoc one needed to slash-escape those), but that doesn't get recognized + as a link in Markdown: `[](meth(Int, Float))` vs `[[meth(Int, Float)]]` + +None of these make it completely impossible to use the standard Markdown link +syntax, but they make it much more awkward and ugly than it needs to be. On top +of that, Markdown link syntax doesn't even save any characters. diff --git a/docs/docs/usage/scala3doc/index.md b/docs/docs/usage/scala3doc/index.md new file mode 100644 index 000000000000..2603ebf76256 --- /dev/null +++ b/docs/docs/usage/scala3doc/index.md @@ -0,0 +1,26 @@ +--- +title: Scala3doc +--- + +![Scala3doc logo](/images/scala3doc-logo.png) + +Scala3doc is tool to generate documentation for your Scala 3 projects. It provies similar features to `javadoc` or `scaladoc` as well as `jekyll` or `docusaurus`. + +As you probably have guessed, this whole site was created using Scala3doc. + + +{% for post in site.posts %} +## [{{ post.title }}](/{{ post.url }}) + +{{ post.excerpt }} + +[ (read more) ](/{{ post.url }}) + +{% endfor %} + +## Other extensions + +We would love to have your feedback on what you think would be good in order to +render the documentation you want! Perhaps you would like to render method +definitions or members? Do you want to have runnable code snippets? Let us know +by filing [issues](https://github.com/lampepfl/dotty/issues/new)! diff --git a/docs/docs/usage/scala3doc/specificTags.md b/docs/docs/usage/scala3doc/specificTags.md new file mode 100644 index 000000000000..9429a0c333e7 --- /dev/null +++ b/docs/docs/usage/scala3doc/specificTags.md @@ -0,0 +1,15 @@ +--- +title: Scala3doc-specific Tags and Features +--- + +# {{page.title}} + +Scala3doc extends Markdown with additional features, such as linking +to API definitions. This can be used from within static documentation and blog +posts to provide blend-in content. + +## Linking to API + +Scala3doc allows linking to API documentation with Wiki-style links. Linking to +`scala.collection.immutable.List` is as simple as +`[[scala.collection.immutable.List]]`. For more information on the exact syntax, see [doc comment documentation](./docComments.html#definition-links). diff --git a/docs/docs/usage/scala3doc/staticSite.md b/docs/docs/usage/scala3doc/staticSite.md new file mode 100644 index 000000000000..66d9521cd831 --- /dev/null +++ b/docs/docs/usage/scala3doc/staticSite.md @@ -0,0 +1,125 @@ +--- +title: Static docucmentation +--- + +# {{ page.title}} + +Scala3doc is able to generate static sites, known from [Jekyll](http://jekyllrb.com/) or [Docusaurus](https://docusaurus.io/). +Having a combined tool allows to provide interaction between static documentation and API, thus allowing the two to blend naturally. + +Creating a site is just as simple as in Jekyll. The site root contains the +layout of the site and all files placed there will be either considered static, +or processed for template expansion. + +The files that are considered for template expansion must end in `*.{html,md}` +and will from here on be referred to as "template files" or "templates". + +A simple "hello world" site could look something like this: + +``` +├── docs +│ └── getting-started.md +└── index.html +``` + +This will give you a site with the following files in generated documentation: + +``` +index.html +docs/getting-started.html +``` + +Scala3doc can transform both files and directories (to organize your documentation into tree-like structure). By default directories has title based on file name and has empty content. There is an option to include `index.html` or `index.md` (not both) to provide both content and properties like title (see [Properties](#properties)). + +## Properties + +Scala3doc uses the [Liquid](https://shopify.github.io/liquid/) templating engine +and provides a number of custom filters and tags specific to Scala +documentation. + +In Scala3doc, all templates can contain YAML front-matter. The front-matter +is parsed and put into the `page` variable available in templates via Liquid. + +Scala3doc uses some predefined properties to controls some aspect of page. + +Predefined properties: + + - **title** provide page title that will be used in navigation and html metadata. + - **extraCss** additional `.css` files that will be included in this page. Paths should be relative to documentation root. **This setting is not exported to template engine.** + - **extraJs** additional `.js` files that will be included in this page. Paths should be relative to documentation root. **This setting is not exported to template engine.** + - **hasFrame** when set to `false` page will not include default layout (navigation, breadcrumbs etc.) but only token html wrapper to provide metadata and resources (js and css files). **This setting is not exported to template engine.** +- **layout** - predefined layout to use, see below. **This setting is not exported to template engine.** + + +## Using existing Templates and Layouts + +To perform template expansion, Dottydoc looks at the `layout` field in the front-matter. +Here's a simple example of the templating system in action, `index.html`: + +```html +--- +layout: main +--- + +

Hello world!

+``` + +With a simple main template like this: + +{% raw %} +```html + + + Hello, world! + + + {{ content }} + + +``` + +Would result in `{{ content }}` being replaced by `

Hello world!

` from +the `index.html` file. +{% endraw %} + +Layouts must be placed in a `_layouts` directory in the site root: + +``` +├── _layouts +│ └── main.html +├── docs +│ └── getting-started.md +└── index.html +``` + +Sidebar +======= +Scala3doc by default uses layout of files in `docs` directory to create table of content. There is also ability to override it by providing a `sidebar.yml` file in the site root: + +```yaml +sidebar: + - title: Blog + url: blog/index.html + - title: Docs + url: docs/index.html + - title: Usage + subsection: + - title: Dottydoc + url: docs/usage/dottydoc.html + - title: sbt-projects + url: docs/usage/sbt-projects.html +``` + +The `sidebar` key is mandatory, as well as `title` for each element. The +default table of contents allows you to have subsections - albeit the current +depth limit is 2 however it accepts both files and directories and latter can be used to provide deeper structures. + +The items which have on the `subsection` level does not accepts `url`. + +``` +├── blog +│ └── _posts +│ └── 2016-12-05-implicit-function-types.md +├── index.html +└── sidebar.yml +``` diff --git a/docs/images/scala3doc-logo.png b/docs/images/scala3doc-logo.png new file mode 100644 index 000000000000..6958f3235344 Binary files /dev/null and b/docs/images/scala3doc-logo.png differ diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 1b901d208ab7..c9831cad720f 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -15,7 +15,9 @@ sidebar: url: docs/usage/language-versions.html - title: cbt-projects url: docs/usage/cbt-projects.html - - title: Dottydoc + - title: Scala3doc + url: docs/usage/scala3doc + - title: Dottydoc [Legacy] url: docs/usage/dottydoc.html - title: Reference subsection: diff --git a/scala3doc/scala3-docs/index.html b/scala3doc/scala3-docs/index.html index d824cdadff87..46022b3068d6 100644 --- a/scala3doc/scala3-docs/index.html +++ b/scala3doc/scala3-docs/index.html @@ -20,7 +20,7 @@ diff --git a/scala3doc/src/dotty/dokka/DottyDokkaPlugin.scala b/scala3doc/src/dotty/dokka/DottyDokkaPlugin.scala index 08fa3456611f..d4c1c2823072 100644 --- a/scala3doc/src/dotty/dokka/DottyDokkaPlugin.scala +++ b/scala3doc/src/dotty/dokka/DottyDokkaPlugin.scala @@ -168,9 +168,9 @@ class DottyDokkaPlugin extends DokkaJavaPlugin: .overrideExtension(dokkaBase.getLocationProvider) ) - extension (ctx: DokkaContext): - def siteContext: Option[StaticSiteContext] = ctx.getConfiguration.asInstanceOf[DottyDokkaConfig].staticSiteContext - def args: Args = ctx.getConfiguration.asInstanceOf[DottyDokkaConfig].docConfiguration.args +extension (ctx: DokkaContext): + def siteContext: Option[StaticSiteContext] = ctx.getConfiguration.asInstanceOf[DottyDokkaConfig].staticSiteContext + def args: Args = ctx.getConfiguration.asInstanceOf[DottyDokkaConfig].docConfiguration.args // TODO (https://github.com/lampepfl/scala3doc/issues/232): remove once problem is fixed in Dokka extension [T] (builder: ExtensionBuilder[T]): diff --git a/scala3doc/src/dotty/dokka/site/LoadedTemplate.scala b/scala3doc/src/dotty/dokka/site/LoadedTemplate.scala index dad0136b375d..c6c104a52373 100644 --- a/scala3doc/src/dotty/dokka/site/LoadedTemplate.scala +++ b/scala3doc/src/dotty/dokka/site/LoadedTemplate.scala @@ -20,11 +20,18 @@ case class LazyEntry(getKey: String, value: () => String) extends JMapEntry[Stri case class LoadedTemplate(templateFile: TemplateFile, children: List[LoadedTemplate], file: File): private def brief(ctx: StaticSiteContext): String = - val code = Jsoup.parse(resolveToHtml(ctx).code) - code.select("p").first().outerHtml() + try + val code = Jsoup.parse(resolveToHtml(ctx).code) + Option(code.select("p").first()).fold("...")(_.outerHtml()) + catch + case e: Throwable => + // TODO (https://github.com/lampepfl/scala3doc/issues/238): provide proper error handling + println(s"[ERROR] Unable to process brief for ${templateFile.file}") + e.printStackTrace() + "..." def lazyTemplateProperties(ctx: StaticSiteContext): JMap[String, Object] = new java.util.AbstractMap[String, Object](): - def entrySet(): JSet[JMapEntry[String, Object]] = + lazy val entrySet: JSet[JMapEntry[String, Object]] = val site = templateFile.settings.getOrElse("page", Map.empty).asInstanceOf[Map[String, Object]] site.asJava.entrySet() ++ JSet( LazyEntry("url", () => ctx.relativePath(LoadedTemplate.this).toString), diff --git a/scala3doc/src/dotty/dokka/site/StaticSiteContext.scala b/scala3doc/src/dotty/dokka/site/StaticSiteContext.scala index d4acc738e197..66f6c4945bc8 100644 --- a/scala3doc/src/dotty/dokka/site/StaticSiteContext.scala +++ b/scala3doc/src/dotty/dokka/site/StaticSiteContext.scala @@ -44,23 +44,28 @@ class StaticSiteContext(val root: File, sourceSets: Set[SourceSetWrapper], args: lazy val mainPages: Seq[StaticPageNode] = templates.map(templateToPage) + val docsPath = root.toPath.resolve("docs") + lazy val allPages: Seq[StaticPageNode] = sideBarConfig.fold(mainPages){ sidebar => def flattenPages(p: StaticPageNode): Set[Path] = Set(p.template.file.toPath) ++ p.getChildren.asScala.collect { case p: StaticPageNode => flattenPages(p) }.flatten val mainFiles = mainPages.toSet.flatMap(flattenPages) - val docsPath = root.toPath.resolve("docs") + val allPaths = if !Files.exists(docsPath) then Nil else Files.walk(docsPath, FileVisitOption.FOLLOW_LINKS).iterator().asScala.toList - val orphanedFiles = allPaths.filterNot(mainFiles.contains).filter { p => + val orphanedFiles = allPaths.filterNot { p => + def name = p.getFileName.toString + def isMain = name == "index.html" || name == "index.md" + mainFiles.contains(p) || (isMain && mainFiles.contains(p.getParent)) + }.filter { p => val name = p.getFileName.toString name.endsWith(".md") || name.endsWith(".html") } val orphanedTemplates = orphanedFiles.flatMap(p => loadTemplate(p.toFile, isBlog = false)) - mainPages ++ orphanedTemplates.map(templateToPage) } @@ -114,13 +119,17 @@ class StaticSiteContext(val root: File, sourceSets: Set[SourceSetWrapper], args: private def loadSidebarContent(entry: Sidebar): LoadedTemplate = entry match case Sidebar.Page(title, url) => val isBlog = title == "Blog" - val path = if isBlog then "blog" else url.stripSuffix(".html") + ".md" - val file = root.toPath.resolve(path) // Add support for .html files! - val LoadedTemplate(template, children, tFile) = loadTemplate(file.toFile, isBlog).get // Add proper logging if file does not exisits - LoadedTemplate(template.copy(settings = template.settings + ("title" -> title)), children, tFile) + val path = if isBlog then "blog" else + if Files.exists(root.toPath.resolve(url)) then url + else url.stripSuffix(".html") + ".md" + + val file = root.toPath.resolve(path).toFile + val LoadedTemplate(template, children, _) = loadTemplate(file, isBlog).get // Add proper logging if file does not exisits + LoadedTemplate(template.copy(settings = template.settings + ("title" -> title), file = file), children, file) + case Sidebar.Category(title, nested) => // Add support for index.html/index.md files! - val fakeFile = new File(root, title) + val fakeFile = new File(new File(root, "docs"), title) LoadedTemplate(emptyTemplate(fakeFile, title), nested.map(loadSidebarContent), fakeFile) private def loadAllFiles() = diff --git a/scala3doc/src/dotty/dokka/site/StaticSiteLocationProvider.scala b/scala3doc/src/dotty/dokka/site/StaticSiteLocationProvider.scala index 3092446d6b5d..4619c8c290e0 100644 --- a/scala3doc/src/dotty/dokka/site/StaticSiteLocationProvider.scala +++ b/scala3doc/src/dotty/dokka/site/StaticSiteLocationProvider.scala @@ -20,23 +20,40 @@ class StaticSiteLocationProvider(ctx: DokkaContext, pageNode: RootPageNode) private def updatePageEntry(page: PageNode, jpath: JList[String]): JList[String] = page match case page: StaticPageNode => - if (page.getDri.contains(docsRootDRI)) JList("index") - else { - val path = jpath.asScala.toList - val start = if (path.head == "--root--") List("docs") else path.take(1) + ctx.siteContext.fold(jpath) { context => + val rawFilePath = context.root.toPath.relativize(page.template.file.toPath) val pageName = page.template.file.getName val dotIndex = pageName.lastIndexOf('.') - val newName = if (dotIndex < 0) pageName else pageName.substring(0, dotIndex) - (start ++ path.drop(1).dropRight(1) ++ List(newName)).asJava + val newPath = + if (dotIndex < 0) rawFilePath.resolve("index") + else rawFilePath.resolveSibling(pageName.substring(0, dotIndex)) + + newPath.iterator.asScala.map(_.toString).toList.asJava } + case page: ContentPage if page.getDri.contains(docsDRI) => - JList("docs") + JList("docs", "index") case page: ContentPage if page.getDri.contains(apiPageDRI) => JList("api", "index") case _ if jpath.size() > 1 && jpath.get(0) == "--root--" && jpath.get(1) == "-a-p-i" => (List("api") ++ jpath.asScala.drop(2)).asJava + + case _: org.jetbrains.dokka.pages.ModulePage if ctx.siteContext.isEmpty => + JList("index") case _ => jpath override val getPathsIndex: JMap[PageNode, JList[String]] = super.getPathsIndex.asScala.mapValuesInPlace(updatePageEntry).asJava + + + override def pathTo(node: PageNode, context: PageNode): String = + val nodePaths = getPathsIndex.get(node).asScala + val contextPaths = Option(context).fold(Nil)(getPathsIndex.get(_).asScala.dropRight(1)) + val commonPaths = nodePaths.zip(contextPaths).takeWhile{ case (a, b) => a == b }.size + + val contextPath = contextPaths.drop(commonPaths).map(_ => "..") + val nodePath = nodePaths.drop(commonPaths) match + case l if l.isEmpty => Seq("index") + case l => l + (contextPath ++ nodePath).mkString("/") \ No newline at end of file diff --git a/scala3doc/src/dotty/dokka/site/common.scala b/scala3doc/src/dotty/dokka/site/common.scala index 9f93d75c51a6..6f2936d8959e 100644 --- a/scala3doc/src/dotty/dokka/site/common.scala +++ b/scala3doc/src/dotty/dokka/site/common.scala @@ -28,6 +28,7 @@ val defaultMarkdownOptions: DataHolder = .set(AnchorLinkExtension.ANCHORLINKS_WRAP_TEXT, false) .set(AnchorLinkExtension.ANCHORLINKS_ANCHOR_CLASS, "anchor") .set(EmojiExtension.ROOT_IMAGE_PATH, "https://github.global.ssl.fastly.net/images/icons/emoji/") + .set(WikiLinkExtension.LINK_ESCAPE_CHARS, "") .set(Parser.EXTENSIONS, java.util.Arrays.asList( TablesExtension.create(), TaskListExtension.create(), diff --git a/scala3doc/src/dotty/dokka/site/processors.scala b/scala3doc/src/dotty/dokka/site/processors.scala index fcccd9f02f4f..75163f9f5cef 100644 --- a/scala3doc/src/dotty/dokka/site/processors.scala +++ b/scala3doc/src/dotty/dokka/site/processors.scala @@ -92,9 +92,10 @@ class SitePagesCreator(ctx: Option[StaticSiteContext]) extends BaseStaticSitePro override def transform(input: RootPageNode, ctx: StaticSiteContext): RootPageNode = val (contentPage, others) = input.getChildren.asScala.toList.partition { _.isInstanceOf[ContentPage] } val modifiedModuleRoot = processRootPage(input, contentPage) - val (indexes, children) = ctx.allPages.partition(_.template.isIndexPage()) + val (indexes, children) = ctx.allPages.partition(f => + f.template.isIndexPage() && f.template.file.toPath.getParent() == ctx.docsPath ) // TODO (https://github.com/lampepfl/scala3doc/issues/238): provide proper error handling - if (indexes.size > 1) println("ERROR: Multiple index pages found $indexes}") + if (indexes.size > 1) println(s"ERROR: Multiple index pages found ${indexes.map(_.template.file)}") val rootContent = indexes.headOption.fold(ctx.asContent(Text(), mkDRI(extra = "root_content")).get(0))(_.getContent)