Nikita Lipsky
2 years ago
committed by
GitHub
20 changed files with 723 additions and 46 deletions
@ -1,6 +1,6 @@ |
|||||||
iosApp/Podfile.lock |
iosApp/Podfile.lock |
||||||
iosApp/Pods/* |
iosApp/Pods/* |
||||||
iosApp/ResourcesDemo.xcworkspace/* |
iosApp/iosApp.xcworkspace/* |
||||||
iosApp/ResourcesDemo.xcodeproj/* |
iosApp/iosApp.xcodeproj/* |
||||||
!iosApp/ResourcesDemo.xcodeproj/project.pbxproj |
!iosApp/iosApp.xcodeproj/project.pbxproj |
||||||
shared/shared.podspec |
shared/shared.podspec |
||||||
|
@ -1,4 +1,4 @@ |
|||||||
target 'ResourcesDemo' do |
target 'iosApp' do |
||||||
use_frameworks! |
use_frameworks! |
||||||
platform :ios, '14.1' |
platform :ios, '14.1' |
||||||
pod 'shared', :path => '../shared' |
pod 'shared', :path => '../shared' |
||||||
|
@ -0,0 +1,34 @@ |
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
xmlns:aapt="http://schemas.android.com/aapt" |
||||||
|
android:width="108dp" |
||||||
|
android:height="108dp" |
||||||
|
android:viewportWidth="108" |
||||||
|
android:viewportHeight="108"> |
||||||
|
<path |
||||||
|
android:fillType="evenOdd" |
||||||
|
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z" |
||||||
|
android:strokeWidth="1" |
||||||
|
android:strokeColor="#00000000"> |
||||||
|
<aapt:attr name="android:fillColor"> |
||||||
|
<gradient |
||||||
|
android:endX="78.5885" |
||||||
|
android:endY="90.9159" |
||||||
|
android:startX="48.7653" |
||||||
|
android:startY="61.0927" |
||||||
|
android:type="linear"> |
||||||
|
<item |
||||||
|
android:color="#44000000" |
||||||
|
android:offset="0.0" /> |
||||||
|
<item |
||||||
|
android:color="#00000000" |
||||||
|
android:offset="1.0" /> |
||||||
|
</gradient> |
||||||
|
</aapt:attr> |
||||||
|
</path> |
||||||
|
<path |
||||||
|
android:fillColor="#FFFFFF" |
||||||
|
android:fillType="nonZero" |
||||||
|
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z" |
||||||
|
android:strokeWidth="1" |
||||||
|
android:strokeColor="#00000000" /> |
||||||
|
</vector> |
@ -0,0 +1,104 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2020 The Android Open Source Project |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.jetbrains.compose.resources.vector |
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.PathFillType |
||||||
|
import androidx.compose.ui.graphics.StrokeCap |
||||||
|
import androidx.compose.ui.graphics.StrokeJoin |
||||||
|
import androidx.compose.ui.graphics.TileMode |
||||||
|
import androidx.compose.ui.unit.Density |
||||||
|
import androidx.compose.ui.unit.Dp |
||||||
|
import androidx.compose.ui.unit.dp |
||||||
|
|
||||||
|
private const val ALPHA_MASK = 0xFF000000.toInt() |
||||||
|
|
||||||
|
// parseColorValue is copied from Android: |
||||||
|
// https://cs.android.com/android-studio/platform/tools/base/+/05fadd8cb2aaafb77da02048c7a240b2147ff293:sdk-common/src/main/java/com/android/ide/common/vectordrawable/VdUtil.kt;l=58 |
||||||
|
/** |
||||||
|
* Parses a color value in #AARRGGBB format. |
||||||
|
* |
||||||
|
* @param color the color value string |
||||||
|
* @return the integer color value |
||||||
|
*/ |
||||||
|
internal fun parseColorValue(color: String): Int { |
||||||
|
require(color.startsWith("#")) { "Invalid color value $color" } |
||||||
|
|
||||||
|
return when (color.length) { |
||||||
|
7 -> { |
||||||
|
// #RRGGBB |
||||||
|
color.substring(1).toUInt(16).toInt() or ALPHA_MASK |
||||||
|
} |
||||||
|
9 -> { |
||||||
|
// #AARRGGBB |
||||||
|
color.substring(1).toUInt(16).toInt() |
||||||
|
} |
||||||
|
4 -> { |
||||||
|
// #RGB |
||||||
|
val v = color.substring(1).toUInt(16).toInt() |
||||||
|
var k = (v shr 8 and 0xF) * 0x110000 |
||||||
|
k = k or (v shr 4 and 0xF) * 0x1100 |
||||||
|
k = k or (v and 0xF) * 0x11 |
||||||
|
k or ALPHA_MASK |
||||||
|
} |
||||||
|
5 -> { |
||||||
|
// #ARGB |
||||||
|
val v = color.substring(1).toUInt(16).toInt() |
||||||
|
var k = (v shr 12 and 0xF) * 0x11000000 |
||||||
|
k = k or (v shr 8 and 0xF) * 0x110000 |
||||||
|
k = k or (v shr 4 and 0xF) * 0x1100 |
||||||
|
k = k or (v and 0xF) * 0x11 |
||||||
|
k or ALPHA_MASK |
||||||
|
} |
||||||
|
else -> ALPHA_MASK |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
internal fun parseFillType(fillType: String): PathFillType = when (fillType) { |
||||||
|
"nonZero" -> PathFillType.NonZero |
||||||
|
"evenOdd" -> PathFillType.EvenOdd |
||||||
|
else -> throw UnsupportedOperationException("unknown fillType: $fillType") |
||||||
|
} |
||||||
|
|
||||||
|
internal fun parseStrokeCap(strokeCap: String): StrokeCap = when (strokeCap) { |
||||||
|
"butt" -> StrokeCap.Butt |
||||||
|
"round" -> StrokeCap.Round |
||||||
|
"square" -> StrokeCap.Square |
||||||
|
else -> throw UnsupportedOperationException("unknown strokeCap: $strokeCap") |
||||||
|
} |
||||||
|
|
||||||
|
internal fun parseStrokeJoin(strokeJoin: String): StrokeJoin = when (strokeJoin) { |
||||||
|
"miter" -> StrokeJoin.Miter |
||||||
|
"round" -> StrokeJoin.Round |
||||||
|
"bevel" -> StrokeJoin.Bevel |
||||||
|
else -> throw UnsupportedOperationException("unknown strokeJoin: $strokeJoin") |
||||||
|
} |
||||||
|
|
||||||
|
internal fun parseTileMode(tileMode: String): TileMode = when (tileMode) { |
||||||
|
"clamp" -> TileMode.Clamp |
||||||
|
"repeated" -> TileMode.Repeated |
||||||
|
"mirror" -> TileMode.Mirror |
||||||
|
else -> throw throw UnsupportedOperationException("unknown tileMode: $tileMode") |
||||||
|
} |
||||||
|
|
||||||
|
internal fun String?.parseDp(density: Density): Dp = with(density) { |
||||||
|
return when { |
||||||
|
this@parseDp == null -> 0f.dp |
||||||
|
endsWith("dp") -> removeSuffix("dp").toFloat().dp |
||||||
|
endsWith("px") -> removeSuffix("px").toFloat().toDp() |
||||||
|
else -> throw UnsupportedOperationException("value should ends with dp or px") |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,276 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2020 The Android Open Source Project |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.jetbrains.compose.resources.vector |
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.Offset |
||||||
|
import androidx.compose.ui.graphics.Color |
||||||
|
import androidx.compose.ui.graphics.Brush |
||||||
|
import androidx.compose.ui.graphics.PathFillType |
||||||
|
import androidx.compose.ui.graphics.SolidColor |
||||||
|
import androidx.compose.ui.graphics.StrokeCap |
||||||
|
import androidx.compose.ui.graphics.StrokeJoin |
||||||
|
import androidx.compose.ui.graphics.TileMode |
||||||
|
import androidx.compose.ui.graphics.vector.DefaultPivotX |
||||||
|
import androidx.compose.ui.graphics.vector.DefaultPivotY |
||||||
|
import androidx.compose.ui.graphics.vector.DefaultRotation |
||||||
|
import androidx.compose.ui.graphics.vector.DefaultScaleX |
||||||
|
import androidx.compose.ui.graphics.vector.DefaultScaleY |
||||||
|
import androidx.compose.ui.graphics.vector.DefaultTranslationX |
||||||
|
import androidx.compose.ui.graphics.vector.DefaultTranslationY |
||||||
|
import androidx.compose.ui.graphics.vector.EmptyPath |
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector |
||||||
|
import androidx.compose.ui.graphics.vector.addPathNodes |
||||||
|
import org.jetbrains.compose.resources.vector.BuildContext.Group |
||||||
|
import androidx.compose.ui.unit.Density |
||||||
|
import org.jetbrains.compose.resources.vector.xmldom.* |
||||||
|
|
||||||
|
|
||||||
|
// Parsing logic is the same as in Android implementation |
||||||
|
// (compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParser.kt) |
||||||
|
// |
||||||
|
// Except there is no support for linking with external resources |
||||||
|
// (for example, we can't reference to color defined in another file) |
||||||
|
// |
||||||
|
// Specification: |
||||||
|
// https://developer.android.com/reference/android/graphics/drawable/VectorDrawable |
||||||
|
|
||||||
|
private const val ANDROID_NS = "http://schemas.android.com/apk/res/android" |
||||||
|
private const val AAPT_NS = "http://schemas.android.com/aapt" |
||||||
|
|
||||||
|
private class BuildContext { |
||||||
|
val currentGroups = mutableListOf<Group>() |
||||||
|
|
||||||
|
enum class Group { |
||||||
|
/** |
||||||
|
* Group that exists in xml file |
||||||
|
*/ |
||||||
|
Real, |
||||||
|
|
||||||
|
/** |
||||||
|
* Group that doesn't exist in xml file. We add it manually when we see <clip-path> node. |
||||||
|
* It will be automatically popped when the real group will be popped. |
||||||
|
*/ |
||||||
|
Virtual |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
internal fun Element.parseVectorRoot(density: Density): ImageVector { |
||||||
|
val context = BuildContext() |
||||||
|
val builder = ImageVector.Builder( |
||||||
|
defaultWidth = attributeOrNull(ANDROID_NS, "width").parseDp(density), |
||||||
|
defaultHeight = attributeOrNull(ANDROID_NS, "height").parseDp(density), |
||||||
|
viewportWidth = attributeOrNull(ANDROID_NS, "viewportWidth")?.toFloat() ?: 0f, |
||||||
|
viewportHeight = attributeOrNull(ANDROID_NS, "viewportHeight")?.toFloat() ?: 0f |
||||||
|
) |
||||||
|
parseVectorNodes(builder, context) |
||||||
|
return builder.build() |
||||||
|
} |
||||||
|
|
||||||
|
private fun Element.parseVectorNodes(builder: ImageVector.Builder, context: BuildContext) { |
||||||
|
childrenSequence |
||||||
|
.filterIsInstance<Element>() |
||||||
|
.forEach { |
||||||
|
it.parseVectorNode(builder, context) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun Element.parseVectorNode(builder: ImageVector.Builder, context: BuildContext) { |
||||||
|
when (nodeName) { |
||||||
|
"path" -> parsePath(builder) |
||||||
|
"clip-path" -> parseClipPath(builder, context) |
||||||
|
"group" -> parseGroup(builder, context) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun Element.parsePath(builder: ImageVector.Builder) { |
||||||
|
builder.addPath( |
||||||
|
pathData = addPathNodes(attributeOrNull(ANDROID_NS, "pathData")), |
||||||
|
pathFillType = attributeOrNull(ANDROID_NS, "fillType") |
||||||
|
?.let(::parseFillType) ?: PathFillType.NonZero, |
||||||
|
name = attributeOrNull(ANDROID_NS, "name") ?: "", |
||||||
|
fill = attributeOrNull(ANDROID_NS, "fillColor")?.let(::parseStringBrush) |
||||||
|
?: apptAttr(ANDROID_NS, "fillColor")?.let(Element::parseElementBrush), |
||||||
|
fillAlpha = attributeOrNull(ANDROID_NS, "fillAlpha")?.toFloat() ?: 1.0f, |
||||||
|
stroke = attributeOrNull(ANDROID_NS, "strokeColor")?.let(::parseStringBrush) |
||||||
|
?: apptAttr(ANDROID_NS, "strokeColor")?.let(Element::parseElementBrush), |
||||||
|
strokeAlpha = attributeOrNull(ANDROID_NS, "strokeAlpha")?.toFloat() ?: 1.0f, |
||||||
|
strokeLineWidth = attributeOrNull(ANDROID_NS, "strokeWidth")?.toFloat() ?: 1.0f, |
||||||
|
strokeLineCap = attributeOrNull(ANDROID_NS, "strokeLineCap") |
||||||
|
?.let(::parseStrokeCap) ?: StrokeCap.Butt, |
||||||
|
strokeLineJoin = attributeOrNull(ANDROID_NS, "strokeLineJoin") |
||||||
|
?.let(::parseStrokeJoin) ?: StrokeJoin.Miter, |
||||||
|
strokeLineMiter = attributeOrNull(ANDROID_NS, "strokeMiterLimit")?.toFloat() ?: 1.0f, |
||||||
|
trimPathStart = attributeOrNull(ANDROID_NS, "trimPathStart")?.toFloat() ?: 0.0f, |
||||||
|
trimPathEnd = attributeOrNull(ANDROID_NS, "trimPathEnd")?.toFloat() ?: 1.0f, |
||||||
|
trimPathOffset = attributeOrNull(ANDROID_NS, "trimPathOffset")?.toFloat() ?: 0.0f |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
private fun Element.parseClipPath(builder: ImageVector.Builder, context: BuildContext) { |
||||||
|
builder.addGroup( |
||||||
|
name = attributeOrNull(ANDROID_NS, "name") ?: "", |
||||||
|
clipPathData = addPathNodes(attributeOrNull(ANDROID_NS, "pathData")) |
||||||
|
) |
||||||
|
context.currentGroups.add(Group.Virtual) |
||||||
|
} |
||||||
|
|
||||||
|
private fun Element.parseGroup(builder: ImageVector.Builder, context: BuildContext) { |
||||||
|
builder.addGroup( |
||||||
|
attributeOrNull(ANDROID_NS, "name") ?: "", |
||||||
|
attributeOrNull(ANDROID_NS, "rotation")?.toFloat() ?: DefaultRotation, |
||||||
|
attributeOrNull(ANDROID_NS, "pivotX")?.toFloat() ?: DefaultPivotX, |
||||||
|
attributeOrNull(ANDROID_NS, "pivotY")?.toFloat() ?: DefaultPivotY, |
||||||
|
attributeOrNull(ANDROID_NS, "scaleX")?.toFloat() ?: DefaultScaleX, |
||||||
|
attributeOrNull(ANDROID_NS, "scaleY")?.toFloat() ?: DefaultScaleY, |
||||||
|
attributeOrNull(ANDROID_NS, "translateX")?.toFloat() ?: DefaultTranslationX, |
||||||
|
attributeOrNull(ANDROID_NS, "translateY")?.toFloat() ?: DefaultTranslationY, |
||||||
|
EmptyPath |
||||||
|
) |
||||||
|
context.currentGroups.add(Group.Real) |
||||||
|
|
||||||
|
parseVectorNodes(builder, context) |
||||||
|
|
||||||
|
do { |
||||||
|
val removedGroup = context.currentGroups.removeLastOrNull() |
||||||
|
builder.clearGroup() |
||||||
|
} while (removedGroup == Group.Virtual) |
||||||
|
} |
||||||
|
|
||||||
|
private fun parseStringBrush(str: String) = SolidColor(Color(parseColorValue(str))) |
||||||
|
|
||||||
|
private fun Element.parseElementBrush(): Brush? = |
||||||
|
childrenSequence |
||||||
|
.filterIsInstance<Element>() |
||||||
|
.find { it.nodeName == "gradient" } |
||||||
|
?.parseGradient() |
||||||
|
|
||||||
|
private fun Element.parseGradient(): Brush? { |
||||||
|
return when (attributeOrNull(ANDROID_NS, "type")) { |
||||||
|
"linear" -> parseLinearGradient() |
||||||
|
"radial" -> parseRadialGradient() |
||||||
|
"sweep" -> parseSweepGradient() |
||||||
|
else -> null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS") |
||||||
|
private fun Element.parseLinearGradient() = Brush.linearGradient( |
||||||
|
colorStops = parseColorStops(), |
||||||
|
start = Offset( |
||||||
|
attributeOrNull(ANDROID_NS, "startX")?.toFloat() ?: 0f, |
||||||
|
attributeOrNull(ANDROID_NS, "startY")?.toFloat() ?: 0f |
||||||
|
), |
||||||
|
end = Offset( |
||||||
|
attributeOrNull(ANDROID_NS, "endX")?.toFloat() ?: 0f, |
||||||
|
attributeOrNull(ANDROID_NS, "endY")?.toFloat() ?: 0f |
||||||
|
), |
||||||
|
tileMode = attributeOrNull(ANDROID_NS, "tileMode")?.let(::parseTileMode) ?: TileMode.Clamp |
||||||
|
) |
||||||
|
|
||||||
|
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS") |
||||||
|
private fun Element.parseRadialGradient() = Brush.radialGradient( |
||||||
|
colorStops = parseColorStops(), |
||||||
|
center = Offset( |
||||||
|
attributeOrNull(ANDROID_NS, "centerX")?.toFloat() ?: 0f, |
||||||
|
attributeOrNull(ANDROID_NS, "centerY")?.toFloat() ?: 0f |
||||||
|
), |
||||||
|
radius = attributeOrNull(ANDROID_NS, "gradientRadius")?.toFloat() ?: 0f, |
||||||
|
tileMode = attributeOrNull(ANDROID_NS, "tileMode")?.let(::parseTileMode) ?: TileMode.Clamp |
||||||
|
) |
||||||
|
|
||||||
|
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS") |
||||||
|
private fun Element.parseSweepGradient() = Brush.sweepGradient( |
||||||
|
colorStops = parseColorStops(), |
||||||
|
center = Offset( |
||||||
|
attributeOrNull(ANDROID_NS, "centerX")?.toFloat() ?: 0f, |
||||||
|
attributeOrNull(ANDROID_NS, "centerY")?.toFloat() ?: 0f, |
||||||
|
) |
||||||
|
) |
||||||
|
|
||||||
|
private fun Element.parseColorStops(): Array<Pair<Float, Color>> { |
||||||
|
val items = childrenSequence |
||||||
|
.filterIsInstance<Element>() |
||||||
|
.filter { it.nodeName == "item" } |
||||||
|
.toList() |
||||||
|
|
||||||
|
val colorStops = items.mapIndexedNotNullTo(mutableListOf()) { index, item -> |
||||||
|
item.parseColorStop(defaultOffset = index.toFloat() / items.lastIndex.coerceAtLeast(1)) |
||||||
|
} |
||||||
|
|
||||||
|
if (colorStops.isEmpty()) { |
||||||
|
val startColor = attributeOrNull(ANDROID_NS, "startColor")?.let(::parseColorValue) |
||||||
|
val centerColor = attributeOrNull(ANDROID_NS, "centerColor")?.let(::parseColorValue) |
||||||
|
val endColor = attributeOrNull(ANDROID_NS, "endColor")?.let(::parseColorValue) |
||||||
|
|
||||||
|
if (startColor != null) { |
||||||
|
colorStops.add(0f to Color(startColor)) |
||||||
|
} |
||||||
|
if (centerColor != null) { |
||||||
|
colorStops.add(0.5f to Color(centerColor)) |
||||||
|
} |
||||||
|
if (endColor != null) { |
||||||
|
colorStops.add(1f to Color(endColor)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return colorStops.toTypedArray() |
||||||
|
} |
||||||
|
|
||||||
|
private fun Element.parseColorStop(defaultOffset: Float): Pair<Float, Color>? { |
||||||
|
val offset = attributeOrNull(ANDROID_NS, "offset")?.toFloat() ?: defaultOffset |
||||||
|
val color = attributeOrNull(ANDROID_NS, "color")?.let(::parseColorValue) ?: return null |
||||||
|
return offset to Color(color) |
||||||
|
} |
||||||
|
|
||||||
|
private fun Element.attributeOrNull(namespace: String, name: String): String? { |
||||||
|
val value = getAttributeNS(namespace, name) |
||||||
|
return if (value.isNotBlank()) value else null |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Attribute of an element can be represented as a separate child: |
||||||
|
* |
||||||
|
* <path ...> |
||||||
|
* <aapt:attr name="android:fillColor"> |
||||||
|
* <gradient ... |
||||||
|
* ... |
||||||
|
* </gradient> |
||||||
|
* </aapt:attr> |
||||||
|
* </path> |
||||||
|
* |
||||||
|
* instead of: |
||||||
|
* |
||||||
|
* <path android:fillColor="red" ... /> |
||||||
|
*/ |
||||||
|
private fun Element.apptAttr( |
||||||
|
namespace: String, |
||||||
|
name: String |
||||||
|
): Element? { |
||||||
|
val prefix = lookupPrefix(namespace) |
||||||
|
return childrenSequence |
||||||
|
.filterIsInstance<Element>() |
||||||
|
.find { |
||||||
|
it.namespaceURI == AAPT_NS && it.localName == "attr" && |
||||||
|
it.getAttribute("name") == "$prefix:$name" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private val Element.childrenSequence get() = sequence<Node> { |
||||||
|
for (i in 0 until childNodes.length) { |
||||||
|
yield(childNodes.item(i)) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
package org.jetbrains.compose.resources.vector.xmldom |
||||||
|
|
||||||
|
/** |
||||||
|
* XML DOM Element. |
||||||
|
*/ |
||||||
|
internal interface Element: Node { |
||||||
|
|
||||||
|
fun getAttributeNS(nameSpaceURI: String, localName: String): String |
||||||
|
|
||||||
|
fun getAttribute(name: String): String |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
package org.jetbrains.compose.resources.vector.xmldom |
||||||
|
|
||||||
|
/** |
||||||
|
* XML DOM Node. |
||||||
|
*/ |
||||||
|
internal interface Node { |
||||||
|
val nodeName: String |
||||||
|
val localName: String |
||||||
|
|
||||||
|
val childNodes: NodeList |
||||||
|
val namespaceURI: String |
||||||
|
|
||||||
|
fun lookupPrefix(namespaceURI: String): String |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
package org.jetbrains.compose.resources.vector.xmldom |
||||||
|
|
||||||
|
/** |
||||||
|
* XML DOM Node list. |
||||||
|
*/ |
||||||
|
internal interface NodeList { |
||||||
|
fun item(i: Int): Node |
||||||
|
|
||||||
|
val length: Int |
||||||
|
} |
@ -0,0 +1,114 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. |
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||||
|
*/ |
||||||
|
package org.jetbrains.compose.resources.vector.xmldom |
||||||
|
|
||||||
|
import platform.Foundation.* |
||||||
|
import platform.darwin.NSObject |
||||||
|
|
||||||
|
internal fun parse(xml: String): Element { |
||||||
|
val parser = DomXmlParser() |
||||||
|
NSXMLParser((xml as NSString).dataUsingEncoding(NSUTF8StringEncoding)!!).apply { |
||||||
|
shouldReportNamespacePrefixes = true |
||||||
|
shouldProcessNamespaces = true |
||||||
|
delegate = parser |
||||||
|
}.parse() |
||||||
|
return parser.root!! |
||||||
|
} |
||||||
|
|
||||||
|
class MalformedXMLException(message: String?) : Exception(message) |
||||||
|
|
||||||
|
private class ElementImpl(override val localName: String, |
||||||
|
override val nodeName: String, |
||||||
|
override val namespaceURI: String, |
||||||
|
val prefixMap: Map<String, String>, |
||||||
|
val attributes: Map<Any?, *>): Element { |
||||||
|
|
||||||
|
override val childNodes: NodeList |
||||||
|
get() = object : NodeList { |
||||||
|
override fun item(i: Int): Node { |
||||||
|
return childs[i] |
||||||
|
} |
||||||
|
|
||||||
|
override val length: Int |
||||||
|
get() = childs.size |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
var childs = mutableListOf<Node>() |
||||||
|
|
||||||
|
override fun getAttributeNS(nameSpaceURI: String, localName: String): String { |
||||||
|
val prefix = prefixMap[nameSpaceURI] |
||||||
|
val attrKey = if (prefix == null) localName else "$prefix:$localName" |
||||||
|
return getAttribute(attrKey) |
||||||
|
} |
||||||
|
|
||||||
|
override fun getAttribute(name: String): String = attributes[name] as String? ?:"" |
||||||
|
|
||||||
|
override fun lookupPrefix(uri: String): String = prefixMap[uri]?:"" |
||||||
|
} |
||||||
|
|
||||||
|
@Suppress("CONFLICTING_OVERLOADS") |
||||||
|
private class DomXmlParser( |
||||||
|
|
||||||
|
) : NSObject(), NSXMLParserDelegateProtocol { |
||||||
|
|
||||||
|
val curPrefixMapInverted = mutableMapOf<String, String>() |
||||||
|
|
||||||
|
var curPrefixMap: Map<String, String> = emptyMap() |
||||||
|
|
||||||
|
val nodeStack = mutableListOf<ElementImpl>() |
||||||
|
|
||||||
|
var root: Element? = null |
||||||
|
|
||||||
|
override fun parser( |
||||||
|
parser: NSXMLParser, |
||||||
|
didStartElement: String, |
||||||
|
namespaceURI: String?, |
||||||
|
qualifiedName: String?, |
||||||
|
attributes: Map<Any?, *> |
||||||
|
) { |
||||||
|
val node = ElementImpl(didStartElement, qualifiedName!!, namespaceURI?:"", |
||||||
|
curPrefixMap, attributes) |
||||||
|
|
||||||
|
if (root == null) root = node |
||||||
|
|
||||||
|
if (!nodeStack.isEmpty()) |
||||||
|
nodeStack.last().childs.add(node) |
||||||
|
|
||||||
|
nodeStack.add(node) |
||||||
|
} |
||||||
|
|
||||||
|
override fun parser( |
||||||
|
parser: NSXMLParser, |
||||||
|
didEndElement: String, |
||||||
|
namespaceURI: String?, |
||||||
|
qualifiedName: String? |
||||||
|
) { |
||||||
|
val node = nodeStack.removeLast() |
||||||
|
assert(node.localName.equals(didEndElement)) |
||||||
|
} |
||||||
|
|
||||||
|
override fun parser(parser: NSXMLParser, |
||||||
|
didStartMappingPrefix: String, |
||||||
|
toURI: String |
||||||
|
) { |
||||||
|
curPrefixMapInverted.put(didStartMappingPrefix, toURI) |
||||||
|
curPrefixMap = curPrefixMapInverted.entries.associateBy({ it.value }, { it.key }) |
||||||
|
} |
||||||
|
|
||||||
|
override fun parser(parser: NSXMLParser, didEndMappingPrefix: String) { |
||||||
|
curPrefixMapInverted.remove(didEndMappingPrefix) |
||||||
|
curPrefixMap = curPrefixMapInverted.entries.associateBy({ it.value }, { it.key }) |
||||||
|
} |
||||||
|
|
||||||
|
override fun parser(parser: NSXMLParser, validationErrorOccurred: NSError) { |
||||||
|
throw MalformedXMLException("validation error occurred") |
||||||
|
} |
||||||
|
|
||||||
|
override fun parser(parser: NSXMLParser, parseErrorOccurred: NSError) { |
||||||
|
throw MalformedXMLException("parse error occurred") |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -0,0 +1,22 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. |
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.jetbrains.compose.resources |
||||||
|
|
||||||
|
import org.jetbrains.compose.resources.vector.xmldom.Element |
||||||
|
import org.jetbrains.compose.resources.vector.xmldom.ElementImpl |
||||||
|
import org.xml.sax.InputSource |
||||||
|
import java.io.ByteArrayInputStream |
||||||
|
import javax.xml.parsers.DocumentBuilderFactory |
||||||
|
|
||||||
|
internal actual fun parseXML(byteArray: ByteArray): Element = |
||||||
|
ElementImpl( |
||||||
|
DocumentBuilderFactory |
||||||
|
.newInstance().apply { |
||||||
|
isNamespaceAware = true |
||||||
|
} |
||||||
|
.newDocumentBuilder() |
||||||
|
.parse(InputSource(ByteArrayInputStream(byteArray))) |
||||||
|
.documentElement) |
@ -0,0 +1,10 @@ |
|||||||
|
package org.jetbrains.compose.resources.vector.xmldom |
||||||
|
|
||||||
|
import org.w3c.dom.Element as DomElement |
||||||
|
|
||||||
|
internal class ElementImpl(val element: DomElement): NodeImpl(element), Element { |
||||||
|
override fun getAttributeNS(nameSpaceURI: String, localName: String): String = |
||||||
|
element.getAttributeNS(nameSpaceURI, localName) |
||||||
|
|
||||||
|
override fun getAttribute(name: String): String = element.getAttribute(name) |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
package org.jetbrains.compose.resources.vector.xmldom |
||||||
|
|
||||||
|
import org.w3c.dom.Node as DomNode |
||||||
|
import org.w3c.dom.Element as DomElement |
||||||
|
|
||||||
|
internal open class NodeImpl(val n: DomNode): Node { |
||||||
|
override val nodeName: String |
||||||
|
get() = n.nodeName |
||||||
|
override val localName: String |
||||||
|
get() = n.localName |
||||||
|
override val childNodes: NodeList |
||||||
|
get() = |
||||||
|
object: NodeList { |
||||||
|
override fun item(i: Int): Node { |
||||||
|
val child = n.childNodes.item(i) |
||||||
|
return if (child is DomElement) ElementImpl(child) else NodeImpl(child) |
||||||
|
} |
||||||
|
|
||||||
|
override val length: Int = n.childNodes.length |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
override val namespaceURI: String |
||||||
|
get() = n.namespaceURI |
||||||
|
|
||||||
|
override fun lookupPrefix(namespaceURI: String): String = n.lookupPrefix(namespaceURI) |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue