mirror of
https://github.com/JetBrains/intellij-platform-plugin-template.git
synced 2025-12-05 14:21:55 +00:00
Support embedded CSS styles in SVG by inlining the SVG styles in EmbeddedToInlineCssSvgTransformerHint
This commit is contained in:
parent
74db9cd93b
commit
593ce91bc1
@ -0,0 +1,179 @@
|
|||||||
|
package org.jetbrains.plugins.template.weatherApp.ui.components
|
||||||
|
|
||||||
|
import org.jetbrains.jewel.ui.painter.PainterProviderScope
|
||||||
|
import org.jetbrains.jewel.ui.painter.PainterSvgPatchHint
|
||||||
|
import org.w3c.dom.Document
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
import org.w3c.dom.Node
|
||||||
|
import org.w3c.dom.NodeList
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.StringWriter
|
||||||
|
import javax.xml.transform.OutputKeys
|
||||||
|
import javax.xml.transform.Transformer
|
||||||
|
import javax.xml.transform.TransformerException
|
||||||
|
import javax.xml.transform.TransformerFactory
|
||||||
|
import javax.xml.transform.dom.DOMSource
|
||||||
|
import javax.xml.transform.stream.StreamResult
|
||||||
|
import javax.xml.xpath.XPathConstants
|
||||||
|
import javax.xml.xpath.XPathFactory
|
||||||
|
|
||||||
|
object EmbeddedToInlineCssSvgTransformerHint : PainterSvgPatchHint {
|
||||||
|
private val CSS_STYLEABLE_TAGS = listOf(
|
||||||
|
"linearGradient", "radialGradient", "pattern",
|
||||||
|
"filter", "clipPath", "mask", "symbol",
|
||||||
|
"marker", "font", "image"
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun PainterProviderScope.patch(element: Element) {
|
||||||
|
val processedElement = element.inlineEmbeddedStylesCSS()
|
||||||
|
|
||||||
|
println(PrintableElement(processedElement).writeToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.inlineEmbeddedStylesCSS(): Element {
|
||||||
|
val svgElement = this
|
||||||
|
|
||||||
|
svgElement.moveStyleableElementsToDefsNode(CSS_STYLEABLE_TAGS)
|
||||||
|
|
||||||
|
val cache = svgElement.parseCssDefinitionsInStylesElement()
|
||||||
|
|
||||||
|
svgElement.inlineStyleDeclarations(cache)
|
||||||
|
|
||||||
|
return svgElement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.getElementsWithAttributeXPath(attributeName: String): List<Element> {
|
||||||
|
val xPath = XPathFactory.newInstance().newXPath()
|
||||||
|
|
||||||
|
val eligibleNodes = xPath.evaluate(
|
||||||
|
"//*[@$attributeName]",
|
||||||
|
this,
|
||||||
|
XPathConstants.NODESET
|
||||||
|
) as NodeList
|
||||||
|
|
||||||
|
return buildList {
|
||||||
|
for (i in 0 until eligibleNodes.length) {
|
||||||
|
eligibleNodes.item(i)
|
||||||
|
.let { node -> if (node is Element) add(node) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun Element.inlineStyleDeclarations(cache: Map<String, Map<String, String>>) {
|
||||||
|
val classAttributeName = "class"
|
||||||
|
val styleElementName = "style"
|
||||||
|
|
||||||
|
for (element in getElementsWithAttributeXPath(classAttributeName)) {
|
||||||
|
if (element.hasAttribute(classAttributeName)) {
|
||||||
|
val cssClassId = element.getAttribute(classAttributeName)
|
||||||
|
if (cssClassId.isBlank()) continue
|
||||||
|
|
||||||
|
element.removeAttribute(classAttributeName)
|
||||||
|
|
||||||
|
// Set a new "style" attribute (example value)
|
||||||
|
val styleAttributesCache = cache[cssClassId] ?: continue
|
||||||
|
val styleAttributes = styleAttributesCache.entries.joinToString(";") { "${it.key}:${it.value}" }
|
||||||
|
element.setAttribute(styleElementName, styleAttributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getSingleChildElement(styleElementName)
|
||||||
|
?.let { styleNode -> this.removeChild(styleNode) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.moveStyleableElementsToDefsNode(stylableElementTags: List<String>) {
|
||||||
|
// Find or create <defs> element
|
||||||
|
val defs = ensureDefsNodeExists()
|
||||||
|
|
||||||
|
// For each tag, find all elements and move those not already inside defs
|
||||||
|
stylableElementTags.forEach { tag ->
|
||||||
|
val nodes = getElementsByTagName(tag)
|
||||||
|
(0..<nodes.length)
|
||||||
|
.map { nodes.item(it) to defs }
|
||||||
|
.forEach { (nodeToMove, newParentNode) ->
|
||||||
|
if (nodeToMove.parentNode != newParentNode) {
|
||||||
|
newParentNode.appendChild(nodeToMove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See: https://www.w3.org/TR/2018/CR-SVG2-20181004/struct.html#DefsElement
|
||||||
|
*/
|
||||||
|
private fun Element.ensureDefsNodeExists(): Node {
|
||||||
|
var defsNode = getElementsByTagName("defs").item(0)
|
||||||
|
|
||||||
|
if (defsNode == null) {
|
||||||
|
defsNode = this.ownerDocument.createElement("defs")
|
||||||
|
insertBefore(defsNode, this.firstChild)
|
||||||
|
}
|
||||||
|
return defsNode
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.parseCssDefinitionsInStylesElement(): Map<String, Map<String, String>> {
|
||||||
|
val styleNode = this.getChildElements("style")
|
||||||
|
.firstOrNull() ?: return emptyMap()
|
||||||
|
|
||||||
|
val cssClassIdRegex = Regex("""\.([^\s{]+)\s*\{\s*([^}]+)\s*}""")
|
||||||
|
|
||||||
|
return buildMap {
|
||||||
|
cssClassIdRegex.findAll(styleNode.textContent).forEach { match ->
|
||||||
|
val styleId = match.groups[1]?.value ?: return@forEach
|
||||||
|
val styleAttributes = match.groups[2]?.value ?: return@forEach
|
||||||
|
|
||||||
|
val styleAttributesMap = styleAttributes
|
||||||
|
.split(";")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.associate { attributeKeyValue ->
|
||||||
|
val (key, value) = attributeKeyValue.trim().split(":")
|
||||||
|
key.trim() to value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
this[styleId] = styleAttributesMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.getChildElements(tagName: String): List<Element> {
|
||||||
|
val childNodes = childNodes
|
||||||
|
val result = ArrayList<Element>()
|
||||||
|
for (i in 0 until childNodes.length) {
|
||||||
|
val node = childNodes.item(i)
|
||||||
|
if (node is Element && tagName == node.tagName) {
|
||||||
|
result.add(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.getSingleChildElement(tagName: String): Element? {
|
||||||
|
return getChildElements(tagName).getOrNull(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PrintableElement(private val element: Element) {
|
||||||
|
|
||||||
|
fun writeToString(): String {
|
||||||
|
return element.ownerDocument.writeToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Document.writeToString(): String {
|
||||||
|
val tf = TransformerFactory.newInstance()
|
||||||
|
val transformer: Transformer
|
||||||
|
|
||||||
|
try {
|
||||||
|
transformer = tf.newTransformer()
|
||||||
|
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
|
||||||
|
|
||||||
|
val writer = StringWriter()
|
||||||
|
transformer.transform(DOMSource(this), StreamResult(writer))
|
||||||
|
return writer.buffer.toString()
|
||||||
|
} catch (e: TransformerException) {
|
||||||
|
error("Unable to render XML document to string: ${e.message}")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
error("Unable to render XML document to string: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -97,7 +97,7 @@ internal fun WeatherDetailsCard(
|
|||||||
key = WeatherIcons.cloudy,
|
key = WeatherIcons.cloudy,
|
||||||
// key = if (isNightTime) weatherForecastData.weatherType.nightIconKey else weatherForecastData.weatherType.dayIconKey,
|
// key = if (isNightTime) weatherForecastData.weatherType.nightIconKey else weatherForecastData.weatherType.dayIconKey,
|
||||||
contentDescription = weatherForecastData.weatherType.label,
|
contentDescription = weatherForecastData.weatherType.label,
|
||||||
hint = CssStyleInlinerSvgPatchHint
|
hint = EmbeddedToInlineCssSvgTransformerHint
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user