[Markdown][IJPL-90975] Replace file system cache for plantUML extension

GitOrigin-RevId: f1c843d6060e9158f7245592f0b8730732cd68b5
This commit is contained in:
Ekaterina Berezhko
2024-05-13 21:32:50 +00:00
committed by intellij-monorepo-bot
parent 129419aa70
commit 5a5c335cf6
7 changed files with 38 additions and 127 deletions
@@ -34,5 +34,6 @@
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
<orderEntry type="module" module-name="intellij.platform.diagnostic" />
<orderEntry type="module" module-name="intellij.platform.util.coroutines" />
<orderEntry type="library" name="caffeine" level="project" />
</component>
</module>
@@ -1,25 +1,25 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.intellij.plugins.markdown.extensions.common.plantuml
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.util.io.FileUtil
import com.github.benmanes.caffeine.cache.Caffeine
import com.intellij.openapi.util.registry.Registry
import org.intellij.markdown.ast.ASTNode
import org.intellij.plugins.markdown.MarkdownBundle
import org.intellij.plugins.markdown.extensions.CodeFenceGeneratingProvider
import org.intellij.plugins.markdown.extensions.MarkdownBrowserPreviewExtension
import org.intellij.plugins.markdown.extensions.MarkdownCodeFenceCacheableProvider
import org.intellij.plugins.markdown.extensions.MarkdownExtensionWithDownloadableFiles
import org.intellij.plugins.markdown.extensions.MarkdownExtensionWithDownloadableFiles.FileEntry
import org.intellij.plugins.markdown.ui.preview.MarkdownHtmlPanel
import org.intellij.plugins.markdown.ui.preview.html.MarkdownCodeFencePluginCacheCollector
import org.intellij.plugins.markdown.ui.preview.html.MarkdownUtil
import org.jetbrains.annotations.ApiStatus
import java.io.File
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.*
@ApiStatus.Internal
class PlantUMLCodeGeneratingProvider(
collector: MarkdownCodeFencePluginCacheCollector? = null
): MarkdownCodeFenceCacheableProvider(collector), MarkdownExtensionWithDownloadableFiles, MarkdownBrowserPreviewExtension.Provider {
class PlantUMLCodeGeneratingProvider: CodeFenceGeneratingProvider, MarkdownExtensionWithDownloadableFiles, MarkdownBrowserPreviewExtension.Provider {
private val cache = Caffeine.newBuilder().softValues().build<String, String>()
override val externalFiles: Iterable<String>
get() = ownFiles
@@ -31,10 +31,21 @@ class PlantUMLCodeGeneratingProvider(
}
override fun generateHtml(language: String, raw: String, node: ASTNode): String {
val key = getUniqueFile(language.lowercase(), raw, "png").toFile()
cacheDiagram(key, raw)
collector?.addAliveCachedFile(this, key)
return "<img src=\"${key.toURI()}\" from-extension=true/>"
val content = obtainGeneratedContent(raw)
val header = "data:image/png;base64,"
return """<img src="$header$content" from-extension=true/>"""
}
// Not thread safe
private fun obtainGeneratedContent(raw: String): String {
val key = MarkdownUtil.md5(raw, "salt")
val cached = cache.getIfPresent(key)
if (cached != null) {
return cached
}
val generated = generateDiagram(raw)
cache.put(key, generated)
return generated
}
override val displayName: String
@@ -60,36 +71,26 @@ class PlantUMLCodeGeneratingProvider(
return null
}
private fun cacheDiagram(path: File, text: String) {
if (!path.exists()) {
generateDiagram(text, path)
}
}
@Throws(IOException::class)
private fun generateDiagram(text: CharSequence, diagramPath: File) {
var innerText: String = text.toString().trim()
if (!innerText.startsWith("@startuml")) {
innerText = "@startuml\n$innerText"
private fun generateDiagram(text: CharSequence): String {
val content = buildString {
if (!text.startsWith("@startuml")) {
append("@startuml\n")
}
append(text)
if (!text.endsWith("@enduml")) {
append("\n@enduml")
}
}
if (!innerText.endsWith("@enduml")) {
innerText += "\n@enduml"
}
FileUtil.createParentDirs(diagramPath)
storeDiagram(innerText, diagramPath)
val stream = ByteArrayOutputStream()
PlantUMLJarManager.getInstance().generateImage(content, stream)
val encodedContent = Base64.getEncoder().encode(stream.toByteArray())
return encodedContent.toString(Charsets.UTF_8)
}
companion object {
const val jarFilename = "plantuml.jar"
private val ownFiles = listOf(jarFilename)
private val dowloadableFiles = listOf(FileEntry(jarFilename) { Registry.stringValue("markdown.plantuml.download.link") })
private fun storeDiagram(source: String, file: File) {
try {
file.outputStream().buffered().use { PlantUMLJarManager.getInstance().generateImage(source, it) }
} catch (exception: Exception) {
thisLogger().warn("Cannot save diagram PlantUML diagram. ", exception)
}
}
}
}
@@ -9,7 +9,6 @@ import io.netty.buffer.Unpooled
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.http.*
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.ide.BuiltInServerManager.Companion.getInstance
import org.jetbrains.ide.HttpRequestHandler
import org.jetbrains.io.FileResponses.checkCache
@@ -96,7 +95,7 @@ class PreviewStaticServer : HttpRequestHandler() {
default-src 'none';
script-src ${StringUtil.join(scripts.map(::stripQueryParameters), " ")};
style-src https: ${StringUtil.join(styles.map(::stripQueryParameters), " ")} 'unsafe-inline';
img-src file: *; connect-src 'none'; font-src * data: *;
img-src file: * data:; connect-src 'none'; font-src * data: *;
object-src 'none'; media-src 'none'; child-src 'none';
"""
}
@@ -1,5 +0,0 @@
```plantuml
@startuml
Alice -> Bob: test
@enduml
```
@@ -1,3 +0,0 @@
```plantuml
Alice -> Bob: test
```
@@ -1,5 +0,0 @@
```puml
@startuml
Alice -> Bob: test
@enduml
```
@@ -1,77 +0,0 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.plugins.markdown.extensions
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.testFramework.LightPlatformCodeInsightTestCase
import com.intellij.testFramework.rules.TempDirectory
import org.intellij.plugins.markdown.MarkdownTestingUtil
import org.intellij.plugins.markdown.extensions.common.plantuml.PlantUMLCodeGeneratingProvider
import org.intellij.plugins.markdown.extensions.common.plantuml.PlantUMLJarManager
import org.intellij.plugins.markdown.ui.preview.html.MarkdownCodeFenceHtmlCache
import org.intellij.plugins.markdown.ui.preview.html.MarkdownUtil
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExternalResource
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import java.util.*
@RunWith(JUnit4::class)
class PlantUmlGenerationTest: LightPlatformCodeInsightTestCase() {
private val tempDirectory = TempDirectory()
@Rule
@JvmField
val rules: TestRule = RuleChain.outerRule(tempDirectory).around(SetupRule(tempDirectory))
class SetupRule(private val tempDirectory: TempDirectory): ExternalResource() {
private var disposable: Disposable? = null
override fun before() {
disposable = Disposer.newDisposable()
ExtensionTestingUtil.mockPathManager(tempDirectory.newDirectoryPath(), disposable!!)
val extension = requireNotNull(MarkdownExtensionsUtil.findCodeFenceGeneratingProvider<PlantUMLCodeGeneratingProvider>())
PlantUMLJarManager.getInstance().dropCache()
ExtensionTestingUtil.downloadExtension(extension, project = null)
val state = Collections.singletonMap(extension.id, true)
ExtensionTestingUtil.replaceExtensionsState(state, disposable!!)
}
override fun after() {
disposable?.let(Disposer::dispose)
disposable = null
PlantUMLJarManager.getInstance().dropCache()
}
}
@Test
fun `test plantUML1`() = doTest()
@Test
fun `test plantUML2`() = doTest()
@Test
fun `test puml`() = doTest()
private fun doTest() {
val virtualFile = getVirtualFile("${getTestName(true)}.md")
val result = MarkdownUtil.generateMarkdownHtml(virtualFile, VfsUtilCore.loadText(virtualFile), project)
val hash = MarkdownUtil.md5(virtualFile.path, MarkdownCodeFenceHtmlCache.MARKDOWN_FILE_PATH_KEY)
println(result)
assertTrue(result.contains(hash))
}
override fun getTestName(lowercaseFirstLetter: Boolean): String {
val name = super.getTestName(lowercaseFirstLetter)
return name.trimStart().replace(' ', '_')
}
override fun getTestDataPath(): String {
return "${MarkdownTestingUtil.TEST_DATA_PATH}/extensions/plantuml/"
}
}