Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancements to ScalaDoc: New Features Implemented #21436

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from

Conversation

sai-prasad-1
Copy link

This pull request includes several key updates and improvements to the scaladoc done as part of GSOC 2024:

  • Fixed Front Matter Issue: Resolved the issue with data not being available in the site under page.<key>.
  • Added Tags and Blocks:
    • Implemented Include tag.
    • Implemented alt-details tab tag.
    • Implemented tabs block
  • Implemented Language Picker: Added language picker functionality and integrated checks to verify the existence of language folders.
  • Data Loading Improvements: it loads all data from the _data folder into site.data.<variable_name> and configuration data from _config.yml into site.config.

- Added a check in the `LanguagePickerTag` class to handle scenarios where the `languages` argument is provided but is empty.
- Implemented an early return in the `render` method to prevent rendering of the language dropdown HTML when the `languages` argument is empty.
- Modified the logic to check the processed `languagesArgString`, and if it is empty after trimming and processing, the method returns an empty string, avoiding the generation of an empty `<select>` element.
- Added additional debug statements to trace the flow of data, particularly the raw argument, resolved variable values, and extracted language codes.
- Ensured that the tag now fails silently and returns no content if the `languages` argument is missing or empty, thus avoiding potential issues with rendering an empty template.

This update improves the robustness of the `LanguagePickerTag` by ensuring it doesn't output incomplete or unintended HTML, enhancing the user experience and preventing rendering errors.
@tgodzik tgodzik requested a review from Florian3k August 26, 2024 10:27
@bishabosha
Copy link
Member

bishabosha commented Sep 2, 2024

For context, this project is to test the viability of using scaladoc to build https://docs.scala-lang.org, which does utilise more Jekyll features than currently supported - but at the same time it makes effort to simplify the UX - such as a dedicated language picker component

old_code.txt Outdated Show resolved Hide resolved
.scalafmt.conf Outdated Show resolved Hide resolved
@@ -1776,7 +1776,7 @@ object Build {
bundleCSS.taskValue
),
libraryDependencies ++= Dependencies.flexmarkDeps ++ Seq(
"nl.big-o" % "liqp" % "0.8.2",
"nl.big-o" % "liqp" % "0.9",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for context, this was necessary to support the custom syntax

Comment on lines +1 to +27
document.addEventListener("DOMContentLoaded", function () {
const tabContainers = document.querySelectorAll('.tabs');
tabContainers.forEach(container => {
const radios = container.querySelectorAll('.tab-radio');
const labels = container.querySelectorAll('.tab-label');
const contents = container.querySelectorAll('.tab-content');

// Hide all tab contents except the first
contents.forEach((content, index) => {
if (index !== 0) content.style.display = 'none';
});

// Check the first radio button
if (radios.length > 0) radios[0].checked = true;
contents[0].style.display = 'block'; // Ensure the first tab content is displayed

labels.forEach((label, index) => {
label.addEventListener('click', () => {
// Hide all tab contents
contents.forEach(content => content.style.display = 'none');

// Show the clicked tab's content
contents[index].style.display = 'block';
});
});
});
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a bug here in that by default all tabs are hidden, and only the last tab button works, but then it shows the contents of the first tab - https://new-docs-scala-lang.vercel.app/docs/getting-started/index.html

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in fact there is an exception on line 15 (undefined is not an object, evaluating contents[0].style)

Copy link
Member

@bishabosha bishabosha Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

however checking now with Chrome it works, so this bug seems to appear in Safari, but not Chrome

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty file?

@KacperFKorban
Copy link
Member

Is this PR ready for review? (I might have some free time to review it)

}

.alt-details-control:checked + .alt-details-toggle + .alt-details-detail {
position: static;
Copy link
Member

@bishabosha bishabosha Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hard to tell if this is necessary, given the alt-details use javascript to operate now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty file?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicated?

}

class TabBlock extends Block("tab") {
override def render(context: TemplateContext, nodes: LNode*): AnyRef = {
Copy link
Member

@bishabosha bishabosha Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there was some additional unported validation logic for acceptable combinations of tab labels, but that was pretty specialised to the needs of docs.scala-lang.org - so it would be hard to see how to make that configurable (perhaps by yaml?) - so I think its acceptable to leave out for now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something like a matcher (if one of these labels exists, then ensure that tabs are arranged in this order..)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be moved to a page within the "internals" section https://dotty.epfl.ch/docs/internals/index.html

override def render(context: TemplateContext, nodes: LNode*): AnyRef = {
val inputString = nodes.head.render(context).toString

val pattern = """(.*)for=(.*)""".r
Copy link
Member

@bishabosha bishabosha Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this for= is supposed to be optional (i.e. we should still be able to extract the tabName if there is no for=)

val settings = if (parsedYaml == null) {
Map("page" -> inner) ++ global
} else {
Map("page" -> parsedYaml.asScala.toMap) ++ global
Copy link
Member

@bishabosha bishabosha Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the global keys should be filtered out of parsedYaml - which is the old logic

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e. remove from parsedYaml any values where the key is in globalKeys

}

// Early return if the languages argument is empty
if (languagesWithEnFirst.isEmpty) {
Copy link
Member

@bishabosha bishabosha Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is dead code

// Create the dropdown HTML
val dropdown = new StringBuilder
dropdown.append(
"<select id='language-selector' name='language' onChange='handleLanguageChange(this)' style='font-size: 14px; padding: 5px; border: 1px solid #ccc; border-radius: 3px;'>"
Copy link
Member

@bishabosha bishabosha Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its better to set up the behaviour from a JS file, rather than call a global function (see onChange=)

newHeaders += header
context.put(s"tab-headers-$uniqueId", newHeaders)
}
val contents = context.get(s"tab-contents-$uniqueId")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the same fix me with pattern matching (I didn't test the previous one btw, I have no idea if it would type check)

Comment on lines +38 to +40
def get[T](key: String): Option[T] = {
data.get(key).map(_.asInstanceOf[T])
}
Copy link
Member

@bishabosha bishabosha Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an improvement here would be to use a scala.reflect.TypeTest implicit evidence: https://www.scala-lang.org/api/3.5.0/scala/reflect/TypeTest.html - so it actually tests the value, rather than casting


configFile match {
case Some(path) =>
val inputStream: InputStream = Files.newInputStream(path)
Copy link
Member

@bishabosha bishabosha Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs to be closed at some point - I dont see anything in javadoc to suggest the load method closes it

Comment on lines +83 to +95
// languagesOpt match {
// case Some(languages) =>
// for (language <- languages) {
// // Cast the key to String to avoid type mismatch errors
// val languageCode = language("code".asInstanceOf[language.K]).asInstanceOf[String]
// val languageFolderPath = Paths.get(baseDir, languageCode)
// if (!Files.exists(languageFolderPath) || !Files.isDirectory(languageFolderPath)) {
// throw new IllegalArgumentException(s"Language folder for '$languageCode' does not exist at path: $languageFolderPath")
// }
// }
// case None =>
// println(s"Warning: No languages found in configuration.")
// }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happened here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i commented this out as it was only checking for the existence of folder, but i figured out we need a way to check the files as well

Comment on lines +49 to +62
private def convertToJava(value: Any): Object = {
value match {
case map: java.util.Map[_, _] =>
val javaMap = new LinkedHashMap[String, Object]()
map.asScala.foreach { case (k, v) => javaMap.put(k.toString, convertToJava(v)) }
javaMap.asInstanceOf[Object]
case list: java.util.List[_] =>
val javaList = new java.util.ArrayList[Object]()
list.asScala.foreach(v => javaList.add(convertToJava(v)))
javaList.asInstanceOf[Object]
case other =>
other.asInstanceOf[Object]
}
}
Copy link
Member

@bishabosha bishabosha Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't seem to do much other than a deep copy, I guess snakeyaml could be parsing data as an immutable map?

mutableProperties.put("site",siteData)

// val parseSettings = ParseSettings.Builder().withFlavor(Flavor.JEKYLL).build().withBlock(AltDetails())
val liqpParser = TemplateParser.Builder()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So at some point the connection should be made between enabling the experimental settings flag and enabling these new features

Copy link
Member

@KacperFKorban KacperFKorban left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added my two cents, hope you don't mind.

Also, it would be extremely useful to add tests examples to the scaladoc-testcases static site.

@@ -143,6 +143,8 @@ class ScaladocSettings extends SettingGroup with AllScalaSettings:

val dynamicSideMenu: Setting[Boolean] =
BooleanSetting(RootSetting, "dynamic-side-menu", "Generate side menu via JS instead of embedding it in every html file", false)
val experimentalFeatures: Setting[Boolean] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: Can this be renamed to something more meaningful?

Comment on lines +50 to +60
val matchResult = pattern.findFirstMatchIn(inputString)
if (matchResult.isEmpty) {
return ("", "", "")
}

val id = matchResult.get.group(1)
val title = matchResult.get.group(2)
val cssClass = matchResult.get.group(3)

(id, title, cssClass)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: try using a match here.

Comment on lines +142 to +143
// "styles/staticsitestyles.css",

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: Shouldn't the new css/js files also be for staticsite only?

}
}

private def readFileContent(filePath: String): Try[String] = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: it seems that every use case of this function always rethrows this error. Can we just not catch it in the first place?

val filePath = s"${IncludeTag.docsFolder}/$filename"
println(s"Attempting to include file: $filePath")

val inputData = args(1).render(context)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: should this also be able to handle more arguments?

}

object IncludeTag {
@volatile private var docsFolder: String = "_docs"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major: this should just be a class parameter, instead of a shared global state

}

object LanguagePickerTag {
@volatile private var config: Config = null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: can we get rid of this shared global state too?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should be able to add a constructor argument to LanguagePickerTag

@sai-prasad-1 sai-prasad-1 marked this pull request as draft September 18, 2024 05:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants