mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
feat(selector): support not exp, method invoke, type check, object type
Some checks are pending
Build-Apk / build (push) Waiting to run
Some checks are pending
Build-Apk / build (push) Waiting to run
This commit is contained in:
parent
695563c7b8
commit
6490018230
|
@ -398,7 +398,14 @@ data class RawSubscription(
|
||||||
listOfNotNull(r.matches, r.excludeMatches, r.anyMatches).flatten()
|
listOfNotNull(r.matches, r.excludeMatches, r.anyMatches).flatten()
|
||||||
}.flatten()
|
}.flatten()
|
||||||
|
|
||||||
val allSelector = allSelectorStrings.map { s -> Selector.parseOrNull(s) }
|
val allSelector = allSelectorStrings.map { s ->
|
||||||
|
try {
|
||||||
|
Selector.parse(s)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtils.d("非法选择器", e.toString())
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
allSelector.forEachIndexed { i, s ->
|
allSelector.forEachIndexed { i, s ->
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
|
|
|
@ -130,7 +130,7 @@ sealed class ResolvedRule(
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
private val canCacheIndex = (matches + excludeMatches).any { s -> s.canCacheIndex }
|
private val canCacheIndex = (matches + excludeMatches).any { s -> s.useCache }
|
||||||
private val transform = if (canCacheIndex) defaultCacheTransform.transform else defaultTransform
|
private val transform = if (canCacheIndex) defaultCacheTransform.transform else defaultTransform
|
||||||
|
|
||||||
fun query(
|
fun query(
|
||||||
|
@ -159,7 +159,7 @@ sealed class ResolvedRule(
|
||||||
}
|
}
|
||||||
return target
|
return target
|
||||||
} finally {
|
} finally {
|
||||||
defaultCacheTransform.indexCache.clear()
|
defaultCacheTransform.cache.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,21 @@ import android.accessibilityservice.AccessibilityService
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
import android.view.accessibility.AccessibilityNodeInfo
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
|
import com.blankj.utilcode.util.LogUtils
|
||||||
|
import li.songe.gkd.BuildConfig
|
||||||
|
import li.songe.selector.MismatchExpressionTypeException
|
||||||
|
import li.songe.selector.MismatchOperatorTypeException
|
||||||
|
import li.songe.selector.MismatchParamTypeException
|
||||||
import li.songe.selector.Selector
|
import li.songe.selector.Selector
|
||||||
import li.songe.selector.Transform
|
import li.songe.selector.Transform
|
||||||
import li.songe.selector.data.PrimitiveValue
|
import li.songe.selector.UnknownIdentifierException
|
||||||
|
import li.songe.selector.UnknownIdentifierMethodException
|
||||||
|
import li.songe.selector.UnknownMemberException
|
||||||
|
import li.songe.selector.UnknownMemberMethodException
|
||||||
|
import li.songe.selector.getCharSequenceAttr
|
||||||
|
import li.songe.selector.getCharSequenceInvoke
|
||||||
|
import li.songe.selector.getIntInvoke
|
||||||
|
import li.songe.selector.initDefaultTypeInfo
|
||||||
|
|
||||||
val AccessibilityService.safeActiveWindow: AccessibilityNodeInfo?
|
val AccessibilityService.safeActiveWindow: AccessibilityNodeInfo?
|
||||||
get() = try {
|
get() = try {
|
||||||
|
@ -51,24 +63,13 @@ inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun AccessibilityNodeInfo.getChildOrNull(i: Int?): AccessibilityNodeInfo? {
|
||||||
* 此方法小概率造成无限节点片段,底层原因未知
|
i ?: return null
|
||||||
*
|
return if (i in 0 until childCount) {
|
||||||
* https://github.com/gkd-kit/gkd/issues/28
|
getChild(i)
|
||||||
*/
|
} else {
|
||||||
fun AccessibilityNodeInfo.getDepth(): Int {
|
null
|
||||||
var p: AccessibilityNodeInfo? = this
|
|
||||||
var depth = 0
|
|
||||||
while (true) {
|
|
||||||
val p2 = p?.parent
|
|
||||||
if (p2 != null) {
|
|
||||||
p = p2
|
|
||||||
depth++
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return depth
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun AccessibilityNodeInfo.getVid(): CharSequence? {
|
fun AccessibilityNodeInfo.getVid(): CharSequence? {
|
||||||
|
@ -135,51 +136,34 @@ val getChildren: (AccessibilityNodeInfo) -> Sequence<AccessibilityNodeInfo> = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowPropertyNames by lazy {
|
private val typeInfo by lazy {
|
||||||
mapOf(
|
initDefaultTypeInfo().apply {
|
||||||
"id" to PrimitiveValue.StringValue.TYPE_NAME,
|
nodeType.props = nodeType.props.filter { !it.name.startsWith('_') }.toTypedArray()
|
||||||
"vid" to PrimitiveValue.StringValue.TYPE_NAME,
|
contextType.props = contextType.props.filter { !it.name.startsWith('_') }.toTypedArray()
|
||||||
|
}.contextType
|
||||||
"name" to PrimitiveValue.StringValue.TYPE_NAME,
|
|
||||||
"text" to PrimitiveValue.StringValue.TYPE_NAME,
|
|
||||||
"text.length" to PrimitiveValue.IntValue.TYPE_NAME,
|
|
||||||
"desc" to PrimitiveValue.StringValue.TYPE_NAME,
|
|
||||||
"desc.length" to PrimitiveValue.IntValue.TYPE_NAME,
|
|
||||||
|
|
||||||
"clickable" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
|
||||||
"focusable" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
|
||||||
"checkable" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
|
||||||
"checked" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
|
||||||
"editable" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
|
||||||
"longClickable" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
|
||||||
"visibleToUser" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
|
||||||
|
|
||||||
"left" to PrimitiveValue.IntValue.TYPE_NAME,
|
|
||||||
"top" to PrimitiveValue.IntValue.TYPE_NAME,
|
|
||||||
"right" to PrimitiveValue.IntValue.TYPE_NAME,
|
|
||||||
"bottom" to PrimitiveValue.IntValue.TYPE_NAME,
|
|
||||||
"width" to PrimitiveValue.IntValue.TYPE_NAME,
|
|
||||||
"height" to PrimitiveValue.IntValue.TYPE_NAME,
|
|
||||||
|
|
||||||
"index" to PrimitiveValue.IntValue.TYPE_NAME,
|
|
||||||
"depth" to PrimitiveValue.IntValue.TYPE_NAME,
|
|
||||||
"childCount" to PrimitiveValue.IntValue.TYPE_NAME,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Selector.checkSelector(): String? {
|
fun Selector.checkSelector(): String? {
|
||||||
binaryExpressions.forEach { e ->
|
val error = checkType(typeInfo) ?: return null
|
||||||
if (!allowPropertyNames.contains(e.name)) {
|
if (BuildConfig.DEBUG) {
|
||||||
return "未知属性:${e.name}"
|
LogUtils.d(
|
||||||
}
|
"Selector check error",
|
||||||
if (e.value.type != "null" && allowPropertyNames[e.name] != e.value.type) {
|
source,
|
||||||
return "非法类型:${e.name}=${e.value.type}"
|
error.message
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
return when (error) {
|
||||||
|
is MismatchExpressionTypeException -> "不匹配表达式类型:${error.exception.stringify()}"
|
||||||
|
is MismatchOperatorTypeException -> "不匹配操作符类型:${error.exception.stringify()}"
|
||||||
|
is MismatchParamTypeException -> "不匹配参数类型:${error.call.stringify()}"
|
||||||
|
is UnknownIdentifierException -> "未知属性:${error.value.value}"
|
||||||
|
is UnknownIdentifierMethodException -> "未知方法:${error.value.value}"
|
||||||
|
is UnknownMemberException -> "未知属性:${error.value.property}"
|
||||||
|
is UnknownMemberMethodException -> "未知方法:${error.value.property}"
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createGetAttr(): ((AccessibilityNodeInfo, String) -> Any?) {
|
private fun createGetNodeAttr(cache: NodeCache): ((AccessibilityNodeInfo, String) -> Any?) {
|
||||||
var tempNode: AccessibilityNodeInfo? = null
|
var tempNode: AccessibilityNodeInfo? = null
|
||||||
val tempRect = Rect()
|
val tempRect = Rect()
|
||||||
var tempVid: CharSequence? = null
|
var tempVid: CharSequence? = null
|
||||||
|
@ -198,6 +182,28 @@ private fun createGetAttr(): ((AccessibilityNodeInfo, String) -> Any?) {
|
||||||
}
|
}
|
||||||
return tempVid
|
return tempVid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在无缓存时, 此方法小概率造成无限节点片段,底层原因未知
|
||||||
|
*
|
||||||
|
* https://github.com/gkd-kit/gkd/issues/28
|
||||||
|
*/
|
||||||
|
fun AccessibilityNodeInfo.getDepthX(): Int {
|
||||||
|
var p: AccessibilityNodeInfo = this
|
||||||
|
var depth = 0
|
||||||
|
while (true) {
|
||||||
|
val p2 = cache.parent[p] ?: p.parent.apply {
|
||||||
|
cache.parent[p] = this
|
||||||
|
}
|
||||||
|
if (p2 != null) {
|
||||||
|
p = p2
|
||||||
|
depth++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return depth
|
||||||
|
}
|
||||||
return { node, name ->
|
return { node, name ->
|
||||||
when (name) {
|
when (name) {
|
||||||
"id" -> node.viewIdResourceName
|
"id" -> node.viewIdResourceName
|
||||||
|
@ -226,8 +232,13 @@ private fun createGetAttr(): ((AccessibilityNodeInfo, String) -> Any?) {
|
||||||
"height" -> node.getTempRect().height()
|
"height" -> node.getTempRect().height()
|
||||||
|
|
||||||
"index" -> node.getIndex()
|
"index" -> node.getIndex()
|
||||||
"depth" -> node.getDepth()
|
"depth" -> node.getDepthX()
|
||||||
"childCount" -> node.childCount
|
"childCount" -> node.childCount
|
||||||
|
|
||||||
|
"parent" -> cache.parent[node] ?: node.parent.apply {
|
||||||
|
cache.parent[node] = this
|
||||||
|
}
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -235,22 +246,43 @@ private fun createGetAttr(): ((AccessibilityNodeInfo, String) -> Any?) {
|
||||||
|
|
||||||
data class CacheTransform(
|
data class CacheTransform(
|
||||||
val transform: Transform<AccessibilityNodeInfo>,
|
val transform: Transform<AccessibilityNodeInfo>,
|
||||||
val indexCache: HashMap<AccessibilityNodeInfo, Int>,
|
val cache: NodeCache,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class NodeCache(
|
||||||
|
val child: MutableMap<Pair<AccessibilityNodeInfo, Int>, AccessibilityNodeInfo> = HashMap(),
|
||||||
|
val index: MutableMap<AccessibilityNodeInfo, Int> = HashMap(),
|
||||||
|
val parent: MutableMap<AccessibilityNodeInfo, AccessibilityNodeInfo?> = HashMap(),
|
||||||
|
) {
|
||||||
|
fun clear() {
|
||||||
|
emptyMap<String, String>()
|
||||||
|
child.clear()
|
||||||
|
parent.clear()
|
||||||
|
index.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createCacheTransform(): CacheTransform {
|
fun createCacheTransform(): CacheTransform {
|
||||||
val indexCache = HashMap<AccessibilityNodeInfo, Int>()
|
val cache = NodeCache()
|
||||||
|
fun AccessibilityNodeInfo.getParentX(): AccessibilityNodeInfo? {
|
||||||
|
return parent?.also { parent ->
|
||||||
|
cache.parent[this] = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun AccessibilityNodeInfo.getChildX(index: Int): AccessibilityNodeInfo? {
|
fun AccessibilityNodeInfo.getChildX(index: Int): AccessibilityNodeInfo? {
|
||||||
return getChild(index)?.also { child ->
|
return cache.child[this to index] ?: getChild(index)?.also { child ->
|
||||||
indexCache[child] = index
|
cache.index[child] = index
|
||||||
|
cache.parent[child] = this
|
||||||
|
cache.child[this to index] = child
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun AccessibilityNodeInfo.getIndexX(): Int {
|
fun AccessibilityNodeInfo.getIndexX(): Int {
|
||||||
indexCache[this]?.let { return it }
|
cache.index[this]?.let { return it }
|
||||||
parent?.forEachIndexed { index, child ->
|
getParentX()?.forEachIndexed { index, child ->
|
||||||
if (child != null) {
|
if (child != null) {
|
||||||
indexCache[child] = index
|
cache.index[child] = index
|
||||||
}
|
}
|
||||||
if (child == this) {
|
if (child == this) {
|
||||||
return index
|
return index
|
||||||
|
@ -267,22 +299,60 @@ fun createCacheTransform(): CacheTransform {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val getAttr = createGetAttr()
|
val getNodeAttr = createGetNodeAttr(cache)
|
||||||
|
val getParent = { node: AccessibilityNodeInfo ->
|
||||||
|
cache.parent[node] ?: node.parent.apply {
|
||||||
|
cache.parent[node] = this
|
||||||
|
}
|
||||||
|
}
|
||||||
val transform = Transform(
|
val transform = Transform(
|
||||||
getAttr = { node, name ->
|
getAttr = { node, name ->
|
||||||
when (name) {
|
when (node) {
|
||||||
"index" -> {
|
is AccessibilityNodeInfo -> {
|
||||||
node.getIndexX()
|
when (name) {
|
||||||
|
"index" -> {
|
||||||
|
node.getIndexX()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
getNodeAttr(node, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is CharSequence -> getCharSequenceAttr(node, name)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
getAttr(node, name)
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getInvoke = { target, name, args ->
|
||||||
|
when (target) {
|
||||||
|
is AccessibilityNodeInfo -> when (name) {
|
||||||
|
"getChild" -> {
|
||||||
|
args.getIntOrNull()?.let { index ->
|
||||||
|
if (index in 0 until target.childCount) {
|
||||||
|
target.getChildX(index)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
is CharSequence -> getCharSequenceInvoke(target, name, args)
|
||||||
|
is Int -> getIntInvoke(target, name, args)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
getName = { node -> node.className },
|
getName = { node -> node.className },
|
||||||
getChildren = getChildrenCache,
|
getChildren = getChildrenCache,
|
||||||
getParent = { node -> node.parent },
|
getParent = getParent,
|
||||||
getDescendants = { node ->
|
getDescendants = { node ->
|
||||||
sequence {
|
sequence {
|
||||||
val stack = getChildrenCache(node).toMutableList()
|
val stack = getChildrenCache(node).toMutableList()
|
||||||
|
@ -319,9 +389,9 @@ fun createCacheTransform(): CacheTransform {
|
||||||
},
|
},
|
||||||
getBeforeBrothers = { node, connectExpression ->
|
getBeforeBrothers = { node, connectExpression ->
|
||||||
sequence {
|
sequence {
|
||||||
val parentVal = node.parent ?: return@sequence
|
val parentVal = getParent(node) ?: return@sequence
|
||||||
val index =
|
// 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 cache.index 是空
|
||||||
indexCache[node] // 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 indexCache 是空
|
val index = cache.index[node]
|
||||||
if (index != null) {
|
if (index != null) {
|
||||||
var i = index - 1
|
var i = index - 1
|
||||||
var offset = 0
|
var offset = 0
|
||||||
|
@ -349,9 +419,9 @@ fun createCacheTransform(): CacheTransform {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getAfterBrothers = { node, connectExpression ->
|
getAfterBrothers = { node, connectExpression ->
|
||||||
val parentVal = node.parent
|
val parentVal = getParent(node)
|
||||||
if (parentVal != null) {
|
if (parentVal != null) {
|
||||||
val index = indexCache[node]
|
val index = cache.index[node]
|
||||||
if (index != null) {
|
if (index != null) {
|
||||||
sequence {
|
sequence {
|
||||||
var i = index + 1
|
var i = index + 1
|
||||||
|
@ -418,12 +488,41 @@ fun createCacheTransform(): CacheTransform {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return CacheTransform(transform, indexCache)
|
return CacheTransform(transform, cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<Any?>.getIntOrNull(i: Int = 0): Int? {
|
||||||
|
return getOrNull(i) as? Int ?: return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createTransform(): Transform<AccessibilityNodeInfo> {
|
fun createTransform(): Transform<AccessibilityNodeInfo> {
|
||||||
|
val cache = NodeCache()
|
||||||
|
val getNodeAttr = createGetNodeAttr(cache)
|
||||||
return Transform(
|
return Transform(
|
||||||
getAttr = createGetAttr(),
|
getAttr = { target, name ->
|
||||||
|
when (target) {
|
||||||
|
is AccessibilityNodeInfo -> getNodeAttr(target, name)
|
||||||
|
is CharSequence -> getCharSequenceAttr(target, name)
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getInvoke = { target, name, args ->
|
||||||
|
when (target) {
|
||||||
|
is AccessibilityNodeInfo -> when (name) {
|
||||||
|
"getChild" -> {
|
||||||
|
target.getChildOrNull(args.getIntOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
is CharSequence -> getCharSequenceInvoke(target, name, args)
|
||||||
|
is Int -> getIntInvoke(target, name, args)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
getName = { node -> node.className },
|
getName = { node -> node.className },
|
||||||
getChildren = getChildren,
|
getChildren = getChildren,
|
||||||
getParent = { node -> node.parent },
|
getParent = { node -> node.parent },
|
||||||
|
|
|
@ -40,8 +40,8 @@ others_floating_bubble_view = { module = "io.github.torrydo:floating-bubble-view
|
||||||
androidx_appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.0" }
|
androidx_appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.0" }
|
||||||
androidx_core_ktx = { module = "androidx.core:core-ktx", version = "1.13.1" }
|
androidx_core_ktx = { module = "androidx.core:core-ktx", version = "1.13.1" }
|
||||||
androidx_lifecycle_runtime_ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version = "2.8.2" }
|
androidx_lifecycle_runtime_ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version = "2.8.2" }
|
||||||
androidx_junit = { module = "androidx.test.ext:junit", version = "1.1.5" }
|
androidx_junit = { module = "androidx.test.ext:junit", version = "1.2.1" }
|
||||||
androidx_espresso = { module = "androidx.test.espresso:espresso-core", version = "3.5.1" }
|
androidx_espresso = { module = "androidx.test.espresso:espresso-core", version = "3.6.1" }
|
||||||
androidx_room_runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
androidx_room_runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||||
androidx_room_compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
androidx_room_compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||||
androidx_room_ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
androidx_room_ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package li.songe.selector
|
||||||
|
|
||||||
|
import kotlin.js.JsExport
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
data class BinaryExpression(
|
||||||
|
override val start: Int,
|
||||||
|
override val end: Int,
|
||||||
|
val left: ValueExpression,
|
||||||
|
val operator: PositionImpl<CompareOperator>,
|
||||||
|
val right: ValueExpression,
|
||||||
|
) : Expression() {
|
||||||
|
override fun <T> match(node: T, transform: Transform<T>): Boolean {
|
||||||
|
return operator.value.compare(node, transform, left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val binaryExpressions
|
||||||
|
get() = arrayOf(this)
|
||||||
|
|
||||||
|
override fun stringify() = "${left.stringify()}${operator.stringify()}${right.stringify()}"
|
||||||
|
|
||||||
|
val properties: Array<String>
|
||||||
|
get() = arrayOf(*left.properties, *right.properties)
|
||||||
|
|
||||||
|
val methods: Array<String>
|
||||||
|
get() = arrayOf(*left.methods, *right.methods)
|
||||||
|
}
|
|
@ -0,0 +1,314 @@
|
||||||
|
package li.songe.selector
|
||||||
|
|
||||||
|
import kotlin.js.JsExport
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
sealed class CompareOperator(val key: String) : Stringify {
|
||||||
|
override fun stringify() = key
|
||||||
|
|
||||||
|
internal abstract fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean
|
||||||
|
|
||||||
|
internal abstract fun allowType(left: ValueExpression, right: ValueExpression): Boolean
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// https://stackoverflow.com/questions/47648689
|
||||||
|
val allSubClasses by lazy {
|
||||||
|
listOf(
|
||||||
|
Equal,
|
||||||
|
NotEqual,
|
||||||
|
Start,
|
||||||
|
NotStart,
|
||||||
|
Include,
|
||||||
|
NotInclude,
|
||||||
|
End,
|
||||||
|
NotEnd,
|
||||||
|
Less,
|
||||||
|
LessEqual,
|
||||||
|
More,
|
||||||
|
MoreEqual,
|
||||||
|
Matches,
|
||||||
|
NotMatches
|
||||||
|
).sortedBy { -it.key.length }.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
// example
|
||||||
|
// id="com.lptiyu.tanke:id/ab1"
|
||||||
|
// id="com.lptiyu.tanke:id/ab2"
|
||||||
|
private fun CharSequence.contentReversedEquals(other: CharSequence): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (this.length != other.length) return false
|
||||||
|
for (i in this.length - 1 downTo 0) {
|
||||||
|
if (this[i] != other[i]) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Equal : CompareOperator("=") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
val right = rightExp.getAttr(node, transform)
|
||||||
|
return if (left is CharSequence && right is CharSequence) {
|
||||||
|
left.contentReversedEquals(right)
|
||||||
|
} else {
|
||||||
|
left == right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression) = true
|
||||||
|
}
|
||||||
|
|
||||||
|
data object NotEqual : CompareOperator("!=") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
return !Equal.compare(node, transform, leftExp, rightExp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression) = true
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Start : CompareOperator("^=") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
val right = rightExp.getAttr(node, transform)
|
||||||
|
return if (left is CharSequence && right is CharSequence) {
|
||||||
|
left.startsWith(right)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression): Boolean {
|
||||||
|
return (left is ValueExpression.StringLiteral || left is ValueExpression.Variable) && (right is ValueExpression.StringLiteral || right is ValueExpression.Variable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data object NotStart : CompareOperator("!^=") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
val right = rightExp.getAttr(node, transform)
|
||||||
|
return if (left is CharSequence && right is CharSequence) {
|
||||||
|
!left.startsWith(right)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression) =
|
||||||
|
Start.allowType(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Include : CompareOperator("*=") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
val right = rightExp.getAttr(node, transform)
|
||||||
|
return if (left is CharSequence && right is CharSequence) {
|
||||||
|
left.contains(right)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression) =
|
||||||
|
Start.allowType(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object NotInclude : CompareOperator("!*=") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
val right = rightExp.getAttr(node, transform)
|
||||||
|
return if (left is CharSequence && right is CharSequence) {
|
||||||
|
!left.contains(right)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression) =
|
||||||
|
Start.allowType(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object End : CompareOperator("$=") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
val right = rightExp.getAttr(node, transform)
|
||||||
|
return if (left is CharSequence && right is CharSequence) {
|
||||||
|
left.endsWith(right)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression) =
|
||||||
|
Start.allowType(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object NotEnd : CompareOperator("!$=") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
val right = rightExp.getAttr(node, transform)
|
||||||
|
return if (left is CharSequence && right is CharSequence) {
|
||||||
|
!left.endsWith(
|
||||||
|
right
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression) =
|
||||||
|
Start.allowType(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Less : CompareOperator("<") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
val right = rightExp.getAttr(node, transform)
|
||||||
|
return if (left is Int && right is Int) left < right else false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression): Boolean {
|
||||||
|
return (left is ValueExpression.Variable || left is ValueExpression.IntLiteral) && (right is ValueExpression.IntLiteral || right is ValueExpression.Variable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data object LessEqual : CompareOperator("<=") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
val right = rightExp.getAttr(node, transform)
|
||||||
|
return if (left is Int && right is Int) left <= right else false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression) =
|
||||||
|
Less.allowType(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object More : CompareOperator(">") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
val right = rightExp.getAttr(node, transform)
|
||||||
|
return if (left is Int && right is Int) left > right else false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression) =
|
||||||
|
Less.allowType(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object MoreEqual : CompareOperator(">=") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
val right = rightExp.getAttr(node, transform)
|
||||||
|
return if (left is Int && right is Int) left >= right else false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression) =
|
||||||
|
Less.allowType(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Matches : CompareOperator("~=") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
return if (left is CharSequence && rightExp is ValueExpression.StringLiteral) {
|
||||||
|
rightExp.outMatches(left)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression): Boolean {
|
||||||
|
return (left is ValueExpression.Variable) && (right is ValueExpression.StringLiteral && right.matches != null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data object NotMatches : CompareOperator("!~=") {
|
||||||
|
override fun <T> compare(
|
||||||
|
node: T,
|
||||||
|
transform: Transform<T>,
|
||||||
|
leftExp: ValueExpression,
|
||||||
|
rightExp: ValueExpression
|
||||||
|
): Boolean {
|
||||||
|
val left = leftExp.getAttr(node, transform)
|
||||||
|
return if (left is CharSequence && rightExp is ValueExpression.StringLiteral) {
|
||||||
|
!rightExp.outMatches(left)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allowType(left: ValueExpression, right: ValueExpression): Boolean {
|
||||||
|
return Matches.allowType(left, right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package li.songe.selector.data
|
package li.songe.selector
|
||||||
|
|
||||||
sealed class ConnectExpression {
|
sealed class ConnectExpression {
|
||||||
abstract val minOffset: Int
|
abstract val minOffset: Int
|
|
@ -1,9 +1,9 @@
|
||||||
package li.songe.selector.data
|
package li.songe.selector
|
||||||
|
|
||||||
import li.songe.selector.Transform
|
sealed class ConnectOperator(val key: String) : Stringify {
|
||||||
|
override fun stringify() = key
|
||||||
|
|
||||||
sealed class ConnectOperator(val key: String) {
|
internal abstract fun <T> traversal(
|
||||||
abstract fun <T> traversal(
|
|
||||||
node: T, transform: Transform<T>, connectExpression: ConnectExpression
|
node: T, transform: Transform<T>, connectExpression: ConnectExpression
|
||||||
): Sequence<T>
|
): Sequence<T>
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
package li.songe.selector.data
|
package li.songe.selector
|
||||||
|
|
||||||
import li.songe.selector.Transform
|
|
||||||
|
|
||||||
data class ConnectSegment(
|
data class ConnectSegment(
|
||||||
val operator: ConnectOperator = ConnectOperator.Ancestor,
|
val operator: ConnectOperator = ConnectOperator.Ancestor,
|
||||||
|
@ -10,10 +8,10 @@ data class ConnectSegment(
|
||||||
if (operator == ConnectOperator.Ancestor && connectExpression is PolynomialExpression && connectExpression.a == 1 && connectExpression.b == 0) {
|
if (operator == ConnectOperator.Ancestor && connectExpression is PolynomialExpression && connectExpression.a == 1 && connectExpression.b == 0) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return operator.key + connectExpression.toString()
|
return operator.stringify() + connectExpression.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> traversal(node: T, transform: Transform<T>): Sequence<T?> {
|
internal fun <T> traversal(node: T, transform: Transform<T>): Sequence<T?> {
|
||||||
return operator.traversal(node, transform, connectExpression)
|
return operator.traversal(node, transform, connectExpression)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,20 +1,18 @@
|
||||||
package li.songe.selector.data
|
package li.songe.selector
|
||||||
|
|
||||||
import li.songe.selector.Transform
|
|
||||||
|
|
||||||
data class ConnectWrapper(
|
data class ConnectWrapper(
|
||||||
val connectSegment: ConnectSegment,
|
val segment: ConnectSegment,
|
||||||
val to: PropertyWrapper,
|
val to: PropertyWrapper,
|
||||||
) {
|
) {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return (to.toString() + "\u0020" + connectSegment.toString()).trim()
|
return (to.toString() + "\u0020" + segment.toString()).trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> matchTracks(
|
internal fun <T> matchTracks(
|
||||||
node: T, transform: Transform<T>,
|
node: T, transform: Transform<T>,
|
||||||
trackNodes: MutableList<T>,
|
trackNodes: MutableList<T>,
|
||||||
): List<T>? {
|
): List<T>? {
|
||||||
connectSegment.traversal(node, transform).forEach {
|
segment.traversal(node, transform).forEach {
|
||||||
if (it == null) return@forEach
|
if (it == null) return@forEach
|
||||||
val r = to.matchTracks(it, transform, trackNodes)
|
val r = to.matchTracks(it, transform, trackNodes)
|
||||||
if (r != null) return r
|
if (r != null) return r
|
|
@ -0,0 +1,45 @@
|
||||||
|
package li.songe.selector
|
||||||
|
|
||||||
|
import kotlin.js.JsExport
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
sealed class SelectorCheckException(override val message: String) : Exception(message)
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
data class UnknownIdentifierException(
|
||||||
|
val value: ValueExpression.Identifier,
|
||||||
|
) : SelectorCheckException("Unknown Identifier: ${value.value}")
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
data class UnknownMemberException(
|
||||||
|
val value: ValueExpression.MemberExpression,
|
||||||
|
) : SelectorCheckException("Unknown Member: ${value.property}")
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
data class UnknownIdentifierMethodException(
|
||||||
|
val value: ValueExpression.Identifier,
|
||||||
|
) : SelectorCheckException("Unknown Identifier Method: ${value.value}")
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
data class UnknownMemberMethodException(
|
||||||
|
val value: ValueExpression.MemberExpression,
|
||||||
|
) : SelectorCheckException("Unknown Member Method: ${value.property}")
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
data class MismatchParamTypeException(
|
||||||
|
val call: ValueExpression.CallExpression,
|
||||||
|
val argument: ValueExpression.LiteralExpression,
|
||||||
|
val type: PrimitiveType
|
||||||
|
) : SelectorCheckException("Mismatch Param Type: ${argument.value} should be ${type.key}")
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
data class MismatchExpressionTypeException(
|
||||||
|
val exception: BinaryExpression,
|
||||||
|
val leftType: PrimitiveType,
|
||||||
|
val rightType: PrimitiveType,
|
||||||
|
) : SelectorCheckException("Mismatch Expression Type: ${exception.stringify()}")
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
data class MismatchOperatorTypeException(
|
||||||
|
val exception: BinaryExpression,
|
||||||
|
) : SelectorCheckException("Mismatch Operator Type: ${exception.stringify()}")
|
|
@ -1,8 +1,6 @@
|
||||||
package li.songe.selector.data
|
package li.songe.selector
|
||||||
|
|
||||||
import li.songe.selector.Transform
|
sealed class Expression : Position {
|
||||||
|
|
||||||
sealed class Expression {
|
|
||||||
internal abstract fun <T> match(node: T, transform: Transform<T>): Boolean
|
internal abstract fun <T> match(node: T, transform: Transform<T>): Boolean
|
||||||
|
|
||||||
abstract val binaryExpressions: Array<BinaryExpression>
|
abstract val binaryExpressions: Array<BinaryExpression>
|
|
@ -0,0 +1,31 @@
|
||||||
|
package li.songe.selector
|
||||||
|
|
||||||
|
|
||||||
|
data class LogicalExpression(
|
||||||
|
override val start: Int,
|
||||||
|
override val end: Int,
|
||||||
|
val left: Expression,
|
||||||
|
val operator: PositionImpl<LogicalOperator>,
|
||||||
|
val right: Expression,
|
||||||
|
) : Expression() {
|
||||||
|
override fun <T> match(node: T, transform: Transform<T>): Boolean {
|
||||||
|
return operator.value.compare(node, transform, left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val binaryExpressions
|
||||||
|
get() = left.binaryExpressions + right.binaryExpressions
|
||||||
|
|
||||||
|
override fun stringify(): String {
|
||||||
|
val leftStr = if (left is LogicalExpression && left.operator.value != operator.value) {
|
||||||
|
"(${left.stringify()})"
|
||||||
|
} else {
|
||||||
|
left.stringify()
|
||||||
|
}
|
||||||
|
val rightStr = if (right is LogicalExpression && right.operator.value != operator.value) {
|
||||||
|
"(${right.stringify()})"
|
||||||
|
} else {
|
||||||
|
right.stringify()
|
||||||
|
}
|
||||||
|
return "$leftStr\u0020${operator.stringify()}\u0020$rightStr"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
package li.songe.selector.data
|
package li.songe.selector
|
||||||
|
|
||||||
import li.songe.selector.Transform
|
sealed class LogicalOperator(val key: String) : Stringify {
|
||||||
|
override fun stringify() = key
|
||||||
|
|
||||||
sealed class LogicalOperator(val key: String) {
|
|
||||||
companion object {
|
companion object {
|
||||||
// https://stackoverflow.com/questions/47648689
|
// https://stackoverflow.com/questions/47648689
|
||||||
val allSubClasses by lazy {
|
val allSubClasses by lazy {
|
||||||
|
@ -12,7 +12,7 @@ sealed class LogicalOperator(val key: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun <T> compare(
|
internal abstract fun <T> compare(
|
||||||
node: T,
|
node: T,
|
||||||
transform: Transform<T>,
|
transform: Transform<T>,
|
||||||
left: Expression,
|
left: Expression,
|
|
@ -10,7 +10,6 @@ class MultiplatformSelector private constructor(
|
||||||
val tracks = selector.tracks
|
val tracks = selector.tracks
|
||||||
val trackIndex = selector.trackIndex
|
val trackIndex = selector.trackIndex
|
||||||
val connectKeys = selector.connectKeys
|
val connectKeys = selector.connectKeys
|
||||||
val propertyNames = selector.propertyNames
|
|
||||||
|
|
||||||
val qfIdValue = selector.qfIdValue
|
val qfIdValue = selector.qfIdValue
|
||||||
val qfVidValue = selector.qfVidValue
|
val qfVidValue = selector.qfVidValue
|
||||||
|
@ -18,15 +17,8 @@ class MultiplatformSelector private constructor(
|
||||||
val canQf = selector.canQf
|
val canQf = selector.canQf
|
||||||
val isMatchRoot = selector.isMatchRoot
|
val isMatchRoot = selector.isMatchRoot
|
||||||
|
|
||||||
// [name,operator,value][]
|
val binaryExpressions = selector.binaryExpressions
|
||||||
val binaryExpressions = selector.binaryExpressions.map { e ->
|
fun checkType(typeInfo: TypeInfo) = selector.checkType(typeInfo)
|
||||||
arrayOf(
|
|
||||||
e.name,
|
|
||||||
e.operator.key,
|
|
||||||
e.value.type,
|
|
||||||
e.value.toString()
|
|
||||||
)
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
fun <T : Any> match(node: T, transform: MultiplatformTransform<T>): T? {
|
fun <T : Any> match(node: T, transform: MultiplatformTransform<T>): T? {
|
||||||
return selector.match(node, transform.transform)
|
return selector.match(node, transform.transform)
|
||||||
|
|
|
@ -5,13 +5,15 @@ import kotlin.js.JsExport
|
||||||
@JsExport
|
@JsExport
|
||||||
@Suppress("UNCHECKED_CAST", "UNUSED")
|
@Suppress("UNCHECKED_CAST", "UNUSED")
|
||||||
class MultiplatformTransform<T : Any>(
|
class MultiplatformTransform<T : Any>(
|
||||||
getAttr: (T, String) -> Any?,
|
getAttr: (Any?, String) -> Any?,
|
||||||
|
getInvoke: (Any?, String, List<Any?>) -> Any?,
|
||||||
getName: (T) -> String?,
|
getName: (T) -> String?,
|
||||||
getChildren: (T) -> Array<T>,
|
getChildren: (T) -> Array<T>,
|
||||||
getParent: (T) -> T?,
|
getParent: (T) -> T?,
|
||||||
) {
|
) {
|
||||||
internal val transform = Transform(
|
internal val transform = Transform(
|
||||||
getAttr = getAttr,
|
getAttr = getAttr,
|
||||||
|
getInvoke = getInvoke,
|
||||||
getName = getName,
|
getName = getName,
|
||||||
getChildren = { node -> getChildren(node).asSequence() },
|
getChildren = { node -> getChildren(node).asSequence() },
|
||||||
getParent = getParent,
|
getParent = getParent,
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package li.songe.selector
|
||||||
|
|
||||||
|
data class NotExpression(
|
||||||
|
override val start: Int,
|
||||||
|
val expression: Expression
|
||||||
|
) : Expression() {
|
||||||
|
override val end: Int
|
||||||
|
get() = expression.end
|
||||||
|
|
||||||
|
override fun <T> match(node: T, transform: Transform<T>): Boolean {
|
||||||
|
return !expression.match(node, transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val binaryExpressions: Array<BinaryExpression>
|
||||||
|
get() = expression.binaryExpressions
|
||||||
|
|
||||||
|
override fun stringify(): String {
|
||||||
|
return "!(${expression.stringify()})"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package li.songe.selector.data
|
package li.songe.selector
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* an+b
|
* an+b
|
26
selector/src/commonMain/kotlin/li/songe/selector/Position.kt
Normal file
26
selector/src/commonMain/kotlin/li/songe/selector/Position.kt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package li.songe.selector
|
||||||
|
|
||||||
|
import kotlin.js.JsExport
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
sealed interface Stringify {
|
||||||
|
fun stringify(): String
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface Position : Stringify {
|
||||||
|
val start: Int
|
||||||
|
val end: Int
|
||||||
|
val length: Int
|
||||||
|
get() = end - start
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
data class PositionImpl<T : Stringify>(
|
||||||
|
override val start: Int,
|
||||||
|
override val end: Int,
|
||||||
|
val value: T
|
||||||
|
) : Position {
|
||||||
|
override fun stringify(): String {
|
||||||
|
return value.stringify()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,4 @@
|
||||||
package li.songe.selector.data
|
package li.songe.selector
|
||||||
|
|
||||||
import li.songe.selector.Transform
|
|
||||||
|
|
||||||
|
|
||||||
data class PropertySegment(
|
data class PropertySegment(
|
||||||
|
@ -18,7 +16,7 @@ data class PropertySegment(
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
val matchTag = if (tracked) "@" else ""
|
val matchTag = if (tracked) "@" else ""
|
||||||
return matchTag + name + expressions.joinToString("") { "[$it]" }
|
return matchTag + name + expressions.joinToString("") { "[${it.stringify()}]" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> matchName(node: T, transform: Transform<T>): Boolean {
|
private fun <T> matchName(node: T, transform: Transform<T>): Boolean {
|
|
@ -1,9 +1,7 @@
|
||||||
package li.songe.selector.data
|
package li.songe.selector
|
||||||
|
|
||||||
import li.songe.selector.Transform
|
|
||||||
|
|
||||||
data class PropertyWrapper(
|
data class PropertyWrapper(
|
||||||
val propertySegment: PropertySegment,
|
val segment: PropertySegment,
|
||||||
val to: ConnectWrapper? = null,
|
val to: ConnectWrapper? = null,
|
||||||
) {
|
) {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
|
@ -11,7 +9,7 @@ data class PropertyWrapper(
|
||||||
to.toString() + "\u0020"
|
to.toString() + "\u0020"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}) + propertySegment.toString()
|
}) + segment.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> matchTracks(
|
fun <T> matchTracks(
|
||||||
|
@ -19,7 +17,7 @@ data class PropertyWrapper(
|
||||||
transform: Transform<T>,
|
transform: Transform<T>,
|
||||||
trackNodes: MutableList<T>,
|
trackNodes: MutableList<T>,
|
||||||
): List<T>? {
|
): List<T>? {
|
||||||
if (!propertySegment.match(node, transform)) {
|
if (!segment.match(node, transform)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
trackNodes.add(node)
|
trackNodes.add(node)
|
|
@ -1,13 +1,10 @@
|
||||||
package li.songe.selector
|
package li.songe.selector
|
||||||
|
|
||||||
import li.songe.selector.data.BinaryExpression
|
import li.songe.selector.parser.selectorParser
|
||||||
import li.songe.selector.data.CompareOperator
|
|
||||||
import li.songe.selector.data.ConnectOperator
|
|
||||||
import li.songe.selector.data.PrimitiveValue
|
|
||||||
import li.songe.selector.data.PropertyWrapper
|
|
||||||
import li.songe.selector.parser.ParserSet
|
|
||||||
|
|
||||||
class Selector internal constructor(private val propertyWrapper: PropertyWrapper) {
|
class Selector internal constructor(
|
||||||
|
val source: String, private val propertyWrapper: PropertyWrapper
|
||||||
|
) {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return propertyWrapper.toString()
|
return propertyWrapper.toString()
|
||||||
}
|
}
|
||||||
|
@ -17,7 +14,7 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
|
||||||
while (true) {
|
while (true) {
|
||||||
list.add(list.last().to?.to ?: break)
|
list.add(list.last().to?.to ?: break)
|
||||||
}
|
}
|
||||||
list.map { p -> p.propertySegment.tracked }.toTypedArray<Boolean>()
|
list.map { p -> p.segment.tracked }.toTypedArray<Boolean>()
|
||||||
}
|
}
|
||||||
|
|
||||||
val trackIndex = tracks.indexOfFirst { it }.let { i ->
|
val trackIndex = tracks.indexOfFirst { it }.let { i ->
|
||||||
|
@ -41,25 +38,25 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
|
||||||
return propertyWrapper.matchTracks(node, transform, trackNodes)
|
return propertyWrapper.matchTracks(node, transform, trackNodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
val qfIdValue = propertyWrapper.propertySegment.expressions.firstOrNull().let { e ->
|
val qfIdValue = propertyWrapper.segment.expressions.firstOrNull().let { e ->
|
||||||
if (e is BinaryExpression && e.name == "id" && e.operator == CompareOperator.Equal && e.value is PrimitiveValue.StringValue) {
|
if (e is BinaryExpression && e.left.value == "id" && e.operator.value == CompareOperator.Equal && e.right is ValueExpression.StringLiteral) {
|
||||||
e.value.value
|
e.right.value
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val qfVidValue = propertyWrapper.propertySegment.expressions.firstOrNull().let { e ->
|
val qfVidValue = propertyWrapper.segment.expressions.firstOrNull().let { e ->
|
||||||
if (e is BinaryExpression && e.name == "vid" && e.operator == CompareOperator.Equal && e.value is PrimitiveValue.StringValue) {
|
if (e is BinaryExpression && e.left.value == "vid" && e.operator.value == CompareOperator.Equal && e.right is ValueExpression.StringLiteral) {
|
||||||
e.value.value
|
e.right.value
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val qfTextValue = propertyWrapper.propertySegment.expressions.firstOrNull().let { e ->
|
val qfTextValue = propertyWrapper.segment.expressions.firstOrNull().let { e ->
|
||||||
if (e is BinaryExpression && e.name == "text" && (e.operator == CompareOperator.Equal || e.operator == CompareOperator.Start || e.operator == CompareOperator.Include || e.operator == CompareOperator.End) && e.value is PrimitiveValue.StringValue) {
|
if (e is BinaryExpression && e.left.value == "text" && (e.operator.value == CompareOperator.Equal || e.operator.value == CompareOperator.Start || e.operator.value == CompareOperator.Include || e.operator.value == CompareOperator.End) && e.right is ValueExpression.StringLiteral) {
|
||||||
e.value.value
|
e.right.value
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -68,8 +65,8 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
|
||||||
val canQf = qfIdValue != null || qfVidValue != null || qfTextValue != null
|
val canQf = qfIdValue != null || qfVidValue != null || qfTextValue != null
|
||||||
|
|
||||||
// 主动查询
|
// 主动查询
|
||||||
val isMatchRoot = propertyWrapper.propertySegment.expressions.firstOrNull().let { e ->
|
val isMatchRoot = propertyWrapper.segment.expressions.firstOrNull().let { e ->
|
||||||
e is BinaryExpression && e.name == "depth" && e.operator == CompareOperator.Equal && e.value.value == 0
|
e is BinaryExpression && e.left.value == "depth" && e.operator.value == CompareOperator.Equal && e.right.value == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
val connectKeys = run {
|
val connectKeys = run {
|
||||||
|
@ -77,7 +74,7 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
|
||||||
val keys = mutableListOf<String>()
|
val keys = mutableListOf<String>()
|
||||||
while (c != null) {
|
while (c != null) {
|
||||||
c.apply {
|
c.apply {
|
||||||
keys.add(connectSegment.operator.key)
|
keys.add(segment.operator.key)
|
||||||
}
|
}
|
||||||
c = c.to.to
|
c = c.to.to
|
||||||
}
|
}
|
||||||
|
@ -88,28 +85,144 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
|
||||||
var p: PropertyWrapper? = propertyWrapper
|
var p: PropertyWrapper? = propertyWrapper
|
||||||
val expressions = mutableListOf<BinaryExpression>()
|
val expressions = mutableListOf<BinaryExpression>()
|
||||||
while (p != null) {
|
while (p != null) {
|
||||||
val s = p.propertySegment
|
val s = p.segment
|
||||||
expressions.addAll(s.binaryExpressions)
|
expressions.addAll(s.binaryExpressions)
|
||||||
p = p.to?.to
|
p = p.to?.to
|
||||||
}
|
}
|
||||||
expressions.distinct().toTypedArray()
|
expressions.distinct().toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
val propertyNames = run {
|
val useCache = run {
|
||||||
binaryExpressions.map { e -> e.name }.distinct().toTypedArray()
|
if (connectKeys.contains(ConnectOperator.BeforeBrother.key)) {
|
||||||
|
return@run true
|
||||||
|
}
|
||||||
|
if (connectKeys.contains(ConnectOperator.AfterBrother.key)) {
|
||||||
|
return@run true
|
||||||
|
}
|
||||||
|
binaryExpressions.forEach { b ->
|
||||||
|
if (b.properties.any { useCacheProperties.contains(it) }) {
|
||||||
|
return@run true
|
||||||
|
}
|
||||||
|
if (b.methods.any { useCacheMethods.contains(it) }) {
|
||||||
|
return@run true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@run false
|
||||||
}
|
}
|
||||||
|
|
||||||
val canCacheIndex =
|
fun checkType(typeInfo: TypeInfo): SelectorCheckException? {
|
||||||
connectKeys.contains(ConnectOperator.BeforeBrother.key) || connectKeys.contains(
|
try {
|
||||||
ConnectOperator.AfterBrother.key
|
propertyWrapper.segment.binaryExpressions.forEach { exp ->
|
||||||
) || propertyNames.contains("index")
|
if (!exp.operator.value.allowType(exp.left, exp.right)) {
|
||||||
|
throw MismatchOperatorTypeException(exp)
|
||||||
|
}
|
||||||
|
val leftType = getExpType(exp.left, typeInfo)
|
||||||
|
val rightType = getExpType(exp.right, typeInfo)
|
||||||
|
if (leftType != null && rightType != null && leftType != rightType) {
|
||||||
|
throw MismatchExpressionTypeException(exp, leftType, rightType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: SelectorCheckException) {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(source: String) = ParserSet.selectorParser(source)
|
fun parse(source: String) = selectorParser(source)
|
||||||
fun parseOrNull(source: String) = try {
|
fun parseOrNull(source: String) = try {
|
||||||
ParserSet.selectorParser(source)
|
selectorParser(source)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val useCacheProperties by lazy {
|
||||||
|
arrayOf("index", "parent")
|
||||||
|
}
|
||||||
|
private val useCacheMethods by lazy {
|
||||||
|
arrayOf("getChild")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getExpType(exp: ValueExpression, typeInfo: TypeInfo): PrimitiveType? {
|
||||||
|
return when (exp) {
|
||||||
|
is ValueExpression.NullLiteral -> null
|
||||||
|
is ValueExpression.BooleanLiteral -> PrimitiveType.BooleanType
|
||||||
|
is ValueExpression.IntLiteral -> PrimitiveType.IntType
|
||||||
|
is ValueExpression.StringLiteral -> PrimitiveType.StringType
|
||||||
|
is ValueExpression.Variable -> checkVariable(exp, typeInfo).type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkVariable(value: ValueExpression.Variable, typeInfo: TypeInfo): TypeInfo {
|
||||||
|
return when (value) {
|
||||||
|
is ValueExpression.CallExpression -> {
|
||||||
|
val method = when (value.callee) {
|
||||||
|
is ValueExpression.CallExpression -> {
|
||||||
|
throw IllegalArgumentException("Unsupported nested call")
|
||||||
|
}
|
||||||
|
|
||||||
|
is ValueExpression.Identifier -> {
|
||||||
|
// getChild(0)
|
||||||
|
typeInfo.methods.find { it.name == value.callee.value && it.params.size == value.arguments.size }
|
||||||
|
?: throw UnknownIdentifierMethodException(value.callee)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ValueExpression.MemberExpression -> {
|
||||||
|
// parent.getChild(0)
|
||||||
|
checkVariable(
|
||||||
|
value.callee.object0, typeInfo
|
||||||
|
).methods.find { it.name == value.callee.property && it.params.size == value.arguments.size }
|
||||||
|
?: throw UnknownMemberMethodException(value.callee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
method.params.forEachIndexed { index, argTypeInfo ->
|
||||||
|
when (val argExp = value.arguments[index]) {
|
||||||
|
is ValueExpression.NullLiteral -> {}
|
||||||
|
is ValueExpression.BooleanLiteral -> {
|
||||||
|
if (argTypeInfo.type != PrimitiveType.BooleanType) {
|
||||||
|
throw MismatchParamTypeException(
|
||||||
|
value,
|
||||||
|
argExp,
|
||||||
|
PrimitiveType.BooleanType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ValueExpression.IntLiteral -> {
|
||||||
|
if (argTypeInfo.type != PrimitiveType.IntType) {
|
||||||
|
throw MismatchParamTypeException(value, argExp, PrimitiveType.IntType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ValueExpression.StringLiteral -> {
|
||||||
|
if (argTypeInfo.type != PrimitiveType.StringType) {
|
||||||
|
throw MismatchParamTypeException(
|
||||||
|
value,
|
||||||
|
argExp,
|
||||||
|
PrimitiveType.StringType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ValueExpression.Variable -> {
|
||||||
|
checkVariable(argExp, argTypeInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return method.returnType
|
||||||
|
}
|
||||||
|
|
||||||
|
is ValueExpression.Identifier -> {
|
||||||
|
typeInfo.props.find { it.name == value.value }?.type
|
||||||
|
?: throw UnknownIdentifierException(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ValueExpression.MemberExpression -> {
|
||||||
|
checkVariable(value.object0, typeInfo).props.find { it.name == value.property }?.type
|
||||||
|
?: throw UnknownMemberException(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ package li.songe.selector
|
||||||
import kotlin.js.JsExport
|
import kotlin.js.JsExport
|
||||||
|
|
||||||
@JsExport
|
@JsExport
|
||||||
data class GkdSyntaxError internal constructor(
|
data class SyntaxError internal constructor(
|
||||||
val expectedValue: String,
|
val expectedValue: String,
|
||||||
val position: Int,
|
val position: Int,
|
||||||
val source: String,
|
val source: String,
|
||||||
|
@ -17,21 +17,21 @@ data class GkdSyntaxError internal constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun gkdAssert(
|
internal fun gkdAssert(
|
||||||
source: String,
|
source: CharSequence,
|
||||||
offset: Int,
|
offset: Int,
|
||||||
value: String = "",
|
value: String = "",
|
||||||
expectedValue: String? = null
|
expectedValue: String? = null
|
||||||
) {
|
) {
|
||||||
if (offset >= source.length || (value.isNotEmpty() && !value.contains(source[offset]))) {
|
if (offset >= source.length || (value.isNotEmpty() && !value.contains(source[offset]))) {
|
||||||
throw GkdSyntaxError(expectedValue ?: value, offset, source)
|
throw SyntaxError(expectedValue ?: value, offset, source.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun gkdError(
|
internal fun gkdError(
|
||||||
source: String,
|
source: CharSequence,
|
||||||
offset: Int,
|
offset: Int,
|
||||||
expectedValue: String = "",
|
expectedValue: String = "",
|
||||||
cause: Exception? = null
|
cause: Exception? = null
|
||||||
): Nothing {
|
): Nothing {
|
||||||
throw GkdSyntaxError(expectedValue, offset, source, cause)
|
throw SyntaxError(expectedValue, offset, source.toString(), cause)
|
||||||
}
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
package li.songe.selector
|
package li.songe.selector
|
||||||
|
|
||||||
import li.songe.selector.data.ConnectExpression
|
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
class Transform<T>(
|
class Transform<T>(
|
||||||
val getAttr: (T, String) -> Any?,
|
val getAttr: (Any?, String) -> Any?,
|
||||||
|
val getInvoke: (Any?, String, List<Any?>) -> Any? = { _, _, _ -> null },
|
||||||
val getName: (T) -> CharSequence?,
|
val getName: (T) -> CharSequence?,
|
||||||
val getChildren: (T) -> Sequence<T>,
|
val getChildren: (T) -> Sequence<T>,
|
||||||
val getParent: (T) -> T?,
|
val getParent: (T) -> T?,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package li.songe.selector.data
|
package li.songe.selector
|
||||||
|
|
||||||
data class TupleExpression(
|
data class TupleExpression(
|
||||||
val numbers: List<Int>,
|
val numbers: List<Int>,
|
71
selector/src/commonMain/kotlin/li/songe/selector/TypeInfo.kt
Normal file
71
selector/src/commonMain/kotlin/li/songe/selector/TypeInfo.kt
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package li.songe.selector
|
||||||
|
|
||||||
|
import kotlin.js.JsExport
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
sealed class PrimitiveType(val key: String) {
|
||||||
|
data object BooleanType : PrimitiveType("boolean")
|
||||||
|
data object IntType : PrimitiveType("int")
|
||||||
|
data object StringType : PrimitiveType("string")
|
||||||
|
data class ObjectType(val name: String) : PrimitiveType("object")
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
data class MethodInfo(
|
||||||
|
val name: String,
|
||||||
|
val returnType: TypeInfo,
|
||||||
|
val params: Array<TypeInfo> = emptyArray(),
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || this::class != other::class) return false
|
||||||
|
|
||||||
|
other as MethodInfo
|
||||||
|
|
||||||
|
if (name != other.name) return false
|
||||||
|
if (returnType != other.returnType) return false
|
||||||
|
if (!params.contentEquals(other.params)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = name.hashCode()
|
||||||
|
result = 31 * result + returnType.hashCode()
|
||||||
|
result = 31 * result + params.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
data class PropInfo(
|
||||||
|
val name: String,
|
||||||
|
val type: TypeInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
data class TypeInfo(
|
||||||
|
val type: PrimitiveType,
|
||||||
|
var props: Array<PropInfo> = arrayOf(),
|
||||||
|
var methods: Array<MethodInfo> = arrayOf(),
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || this::class != other::class) return false
|
||||||
|
|
||||||
|
other as TypeInfo
|
||||||
|
|
||||||
|
if (type != other.type) return false
|
||||||
|
if (!props.contentEquals(other.props)) return false
|
||||||
|
if (!methods.contentEquals(other.methods)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = type.hashCode()
|
||||||
|
result = 31 * result + props.contentHashCode()
|
||||||
|
result = 31 * result + methods.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
package li.songe.selector
|
||||||
|
|
||||||
|
import kotlin.js.JsExport
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
sealed class ValueExpression(open val value: Any?, open val type: String) : Position {
|
||||||
|
override fun stringify() = value.toString()
|
||||||
|
internal abstract fun <T> getAttr(node: T, transform: Transform<T>): Any?
|
||||||
|
abstract val properties: Array<String>
|
||||||
|
abstract val methods: Array<String>
|
||||||
|
|
||||||
|
sealed class Variable(
|
||||||
|
override val value: String,
|
||||||
|
) : ValueExpression(value, "var")
|
||||||
|
|
||||||
|
data class Identifier internal constructor(
|
||||||
|
override val start: Int,
|
||||||
|
override val value: String,
|
||||||
|
) : Variable(value) {
|
||||||
|
override val end = start + value.length
|
||||||
|
override fun <T> getAttr(node: T, transform: Transform<T>): Any? {
|
||||||
|
return transform.getAttr(node, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val properties: Array<String>
|
||||||
|
get() = arrayOf(value)
|
||||||
|
override val methods: Array<String>
|
||||||
|
get() = emptyArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MemberExpression internal constructor(
|
||||||
|
override val start: Int,
|
||||||
|
override val end: Int,
|
||||||
|
val object0: Variable,
|
||||||
|
val property: String,
|
||||||
|
) : Variable(value = "${object0.stringify()}.$property") {
|
||||||
|
override fun <T> getAttr(node: T, transform: Transform<T>): Any? {
|
||||||
|
return transform.getAttr(object0.getAttr(node, transform), property)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val properties: Array<String>
|
||||||
|
get() = arrayOf(*object0.properties, property)
|
||||||
|
override val methods: Array<String>
|
||||||
|
get() = object0.methods
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CallExpression internal constructor(
|
||||||
|
override val start: Int,
|
||||||
|
override val end: Int,
|
||||||
|
val callee: Variable,
|
||||||
|
val arguments: List<ValueExpression>,
|
||||||
|
) : Variable(
|
||||||
|
value = "${callee.stringify()}(${arguments.joinToString(",") { it.stringify() }})",
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun <T> getAttr(node: T, transform: Transform<T>): Any? {
|
||||||
|
return when (callee) {
|
||||||
|
is CallExpression -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
is Identifier -> {
|
||||||
|
transform.getInvoke(
|
||||||
|
node,
|
||||||
|
callee.value,
|
||||||
|
arguments.map { it.getAttr(node, transform) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MemberExpression -> {
|
||||||
|
transform.getInvoke(
|
||||||
|
callee.object0.getAttr(node, transform),
|
||||||
|
callee.property,
|
||||||
|
arguments.map { it.getAttr(node, transform) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val properties: Array<String>
|
||||||
|
get() = callee.properties.toMutableList()
|
||||||
|
.plus(arguments.flatMap { it.properties.toList() })
|
||||||
|
.toTypedArray()
|
||||||
|
override val methods: Array<String>
|
||||||
|
get() = when (callee) {
|
||||||
|
is CallExpression -> callee.methods
|
||||||
|
is Identifier -> arrayOf(callee.value)
|
||||||
|
is MemberExpression -> arrayOf(*callee.object0.methods, callee.property)
|
||||||
|
}.toMutableList().plus(arguments.flatMap { it.methods.toList() })
|
||||||
|
.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class LiteralExpression(
|
||||||
|
override val value: Any?,
|
||||||
|
override val type: String,
|
||||||
|
) : ValueExpression(value, type) {
|
||||||
|
override fun <T> getAttr(node: T, transform: Transform<T>) = value
|
||||||
|
|
||||||
|
override val properties: Array<String>
|
||||||
|
get() = emptyArray()
|
||||||
|
override val methods: Array<String>
|
||||||
|
get() = emptyArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NullLiteral internal constructor(
|
||||||
|
override val start: Int,
|
||||||
|
) : LiteralExpression(null, "null") {
|
||||||
|
override val end = start + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BooleanLiteral internal constructor(
|
||||||
|
override val start: Int,
|
||||||
|
override val value: Boolean
|
||||||
|
) : LiteralExpression(value, "boolean") {
|
||||||
|
override val end = start + if (value) 4 else 5
|
||||||
|
}
|
||||||
|
|
||||||
|
data class IntLiteral internal constructor(
|
||||||
|
override val start: Int,
|
||||||
|
override val end: Int,
|
||||||
|
override val value: Int
|
||||||
|
) : LiteralExpression(value, "int")
|
||||||
|
|
||||||
|
data class StringLiteral internal constructor(
|
||||||
|
override val start: Int,
|
||||||
|
override val end: Int,
|
||||||
|
override val value: String,
|
||||||
|
internal val matches: ((CharSequence) -> Boolean)? = null
|
||||||
|
) : LiteralExpression(value, "string") {
|
||||||
|
|
||||||
|
override fun stringify() = escapeString(value)
|
||||||
|
|
||||||
|
internal val outMatches = matches?.let { optimizeMatchString(value) ?: it } ?: { false }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
package li.songe.selector.data
|
|
||||||
|
|
||||||
import li.songe.selector.Transform
|
|
||||||
|
|
||||||
data class BinaryExpression(
|
|
||||||
val name: String,
|
|
||||||
val operator: CompareOperator,
|
|
||||||
val value: PrimitiveValue
|
|
||||||
) : Expression() {
|
|
||||||
override fun <T> match(node: T, transform: Transform<T>) =
|
|
||||||
operator.compare(transform.getAttr(node, name), value)
|
|
||||||
|
|
||||||
override val binaryExpressions
|
|
||||||
get() = arrayOf(this)
|
|
||||||
|
|
||||||
override fun toString() = "${name}${operator.key}${value}"
|
|
||||||
}
|
|
|
@ -1,194 +0,0 @@
|
||||||
package li.songe.selector.data
|
|
||||||
|
|
||||||
sealed class CompareOperator(val key: String) {
|
|
||||||
abstract fun compare(left: Any?, right: PrimitiveValue): Boolean
|
|
||||||
abstract fun allowType(type: PrimitiveValue): Boolean
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// https://stackoverflow.com/questions/47648689
|
|
||||||
val allSubClasses by lazy {
|
|
||||||
listOf(
|
|
||||||
Equal,
|
|
||||||
NotEqual,
|
|
||||||
Start,
|
|
||||||
NotStart,
|
|
||||||
Include,
|
|
||||||
NotInclude,
|
|
||||||
End,
|
|
||||||
NotEnd,
|
|
||||||
Less,
|
|
||||||
LessEqual,
|
|
||||||
More,
|
|
||||||
MoreEqual,
|
|
||||||
Matches,
|
|
||||||
NotMatches
|
|
||||||
).sortedBy { -it.key.length }.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
// example
|
|
||||||
// id="com.lptiyu.tanke:id/ab1"
|
|
||||||
// id="com.lptiyu.tanke:id/ab2"
|
|
||||||
private fun CharSequence.contentReversedEquals(other: CharSequence): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (this.length != other.length) return false
|
|
||||||
for (i in this.length - 1 downTo 0) {
|
|
||||||
if (this[i] != other[i]) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data object Equal : CompareOperator("=") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
|
||||||
left.contentReversedEquals(right.value)
|
|
||||||
} else {
|
|
||||||
left == right.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue) = true
|
|
||||||
}
|
|
||||||
|
|
||||||
data object NotEqual : CompareOperator("!=") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue) = !Equal.compare(left, right)
|
|
||||||
override fun allowType(type: PrimitiveValue) = true
|
|
||||||
}
|
|
||||||
|
|
||||||
data object Start : CompareOperator("^=") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
|
||||||
left.startsWith(right.value)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
|
|
||||||
}
|
|
||||||
|
|
||||||
data object NotStart : CompareOperator("!^=") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
|
||||||
!left.startsWith(right.value)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
|
|
||||||
}
|
|
||||||
|
|
||||||
data object Include : CompareOperator("*=") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
|
||||||
left.contains(right.value)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
|
|
||||||
}
|
|
||||||
|
|
||||||
data object NotInclude : CompareOperator("!*=") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
|
||||||
!left.contains(right.value)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
|
|
||||||
}
|
|
||||||
|
|
||||||
data object End : CompareOperator("$=") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
|
||||||
left.endsWith(right.value)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
|
|
||||||
}
|
|
||||||
|
|
||||||
data object NotEnd : CompareOperator("!$=") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
|
||||||
!left.endsWith(
|
|
||||||
right.value
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
|
|
||||||
}
|
|
||||||
|
|
||||||
data object Less : CompareOperator("<") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is Int && right is PrimitiveValue.IntValue) left < right.value else false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue
|
|
||||||
}
|
|
||||||
|
|
||||||
data object LessEqual : CompareOperator("<=") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is Int && right is PrimitiveValue.IntValue) left <= right.value else false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue
|
|
||||||
}
|
|
||||||
|
|
||||||
data object More : CompareOperator(">") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is Int && right is PrimitiveValue.IntValue) left > right.value else false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue
|
|
||||||
}
|
|
||||||
|
|
||||||
data object MoreEqual : CompareOperator(">=") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is Int && right is PrimitiveValue.IntValue) left >= right.value else false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue
|
|
||||||
}
|
|
||||||
|
|
||||||
data object Matches : CompareOperator("~=") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
|
||||||
right.outMatches(left)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue): Boolean {
|
|
||||||
return type is PrimitiveValue.StringValue && type.matches != null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data object NotMatches : CompareOperator("!~=") {
|
|
||||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
|
||||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
|
||||||
!right.outMatches(left)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allowType(type: PrimitiveValue): Boolean {
|
|
||||||
return Matches.allowType(type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
package li.songe.selector.data
|
|
||||||
|
|
||||||
import li.songe.selector.Transform
|
|
||||||
|
|
||||||
data class LogicalExpression(
|
|
||||||
val left: Expression,
|
|
||||||
val operator: LogicalOperator,
|
|
||||||
val right: Expression,
|
|
||||||
) : Expression() {
|
|
||||||
override fun <T> match(node: T, transform: Transform<T>): Boolean {
|
|
||||||
return operator.compare(node, transform, left, right)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val binaryExpressions
|
|
||||||
get() = left.binaryExpressions + right.binaryExpressions
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
val leftStr = if (left is LogicalExpression && left.operator != operator) {
|
|
||||||
"($left)"
|
|
||||||
} else {
|
|
||||||
left.toString()
|
|
||||||
}
|
|
||||||
val rightStr = if (right is LogicalExpression && right.operator != operator) {
|
|
||||||
"($right)"
|
|
||||||
} else {
|
|
||||||
right.toString()
|
|
||||||
}
|
|
||||||
return "$leftStr\u0020${operator.key}\u0020$rightStr"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
package li.songe.selector.data
|
|
||||||
|
|
||||||
sealed class PrimitiveValue(open val value: Any?, open val type: String) {
|
|
||||||
data object NullValue : PrimitiveValue(null, "null") {
|
|
||||||
override fun toString() = "null"
|
|
||||||
}
|
|
||||||
|
|
||||||
data class BooleanValue(override val value: Boolean) : PrimitiveValue(value, TYPE_NAME) {
|
|
||||||
override fun toString() = value.toString()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TYPE_NAME = "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class IntValue(override val value: Int) : PrimitiveValue(value, TYPE_NAME) {
|
|
||||||
override fun toString() = value.toString()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TYPE_NAME = "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class StringValue(
|
|
||||||
override val value: String,
|
|
||||||
val matches: ((CharSequence) -> Boolean)? = null
|
|
||||||
) : PrimitiveValue(value, TYPE_NAME) {
|
|
||||||
|
|
||||||
val outMatches: (value: CharSequence) -> Boolean = run {
|
|
||||||
matches ?: return@run { false }
|
|
||||||
getMatchValue(value, "(?is)", ".*")?.let { startsWithValue ->
|
|
||||||
return@run { value -> value.startsWith(startsWithValue, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
getMatchValue(value, "(?is).*", ".*")?.let { containsValue ->
|
|
||||||
return@run { value -> value.contains(containsValue, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
getMatchValue(value, "(?is).*", "")?.let { endsWithValue ->
|
|
||||||
return@run { value -> value.endsWith(endsWithValue, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
return@run matches
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TYPE_NAME = "string"
|
|
||||||
private const val REG_SPECIAL_STRING = "\\^$.?*|+()[]{}"
|
|
||||||
private fun getMatchValue(value: String, prefix: String, suffix: String): String? {
|
|
||||||
if (value.startsWith(prefix) && value.endsWith(suffix) && value.length >= (prefix.length + suffix.length)) {
|
|
||||||
for (i in prefix.length until value.length - suffix.length) {
|
|
||||||
if (value[i] in REG_SPECIAL_STRING) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value.subSequence(prefix.length, value.length - suffix.length).toString()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
val wrapChar = '"'
|
|
||||||
val sb = StringBuilder(value.length + 2)
|
|
||||||
sb.append(wrapChar)
|
|
||||||
value.forEach { c ->
|
|
||||||
val escapeChar = when (c) {
|
|
||||||
wrapChar -> wrapChar
|
|
||||||
'\n' -> 'n'
|
|
||||||
'\r' -> 'r'
|
|
||||||
'\t' -> 't'
|
|
||||||
'\b' -> 'b'
|
|
||||||
'\\' -> '\\'
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
if (escapeChar != null) {
|
|
||||||
sb.append("\\" + escapeChar)
|
|
||||||
} else {
|
|
||||||
when (c.code) {
|
|
||||||
in 0..0xf -> {
|
|
||||||
sb.append("\\x0" + c.code.toString(16))
|
|
||||||
}
|
|
||||||
|
|
||||||
in 0x10..0x1f -> {
|
|
||||||
sb.append("\\x" + c.code.toString(16))
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
sb.append(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.append(wrapChar)
|
|
||||||
return sb.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ package li.songe.selector.parser
|
||||||
|
|
||||||
internal data class Parser<T>(
|
internal data class Parser<T>(
|
||||||
val prefix: String = "",
|
val prefix: String = "",
|
||||||
private val temp: (source: String, offset: Int, prefix: String) -> ParserResult<T>
|
private val temp: (source: CharSequence, offset: Int, prefix: String) -> ParserResult<T>
|
||||||
) {
|
) {
|
||||||
operator fun invoke(source: String, offset: Int) = temp(source, offset, prefix)
|
operator fun invoke(source: CharSequence, offset: Int) = temp(source, offset, prefix)
|
||||||
}
|
}
|
|
@ -1,22 +1,28 @@
|
||||||
package li.songe.selector.parser
|
package li.songe.selector.parser
|
||||||
|
|
||||||
|
import li.songe.selector.BinaryExpression
|
||||||
|
import li.songe.selector.CompareOperator
|
||||||
|
import li.songe.selector.ConnectExpression
|
||||||
|
import li.songe.selector.ConnectOperator
|
||||||
|
import li.songe.selector.ConnectSegment
|
||||||
|
import li.songe.selector.ConnectWrapper
|
||||||
|
import li.songe.selector.Expression
|
||||||
|
import li.songe.selector.LogicalExpression
|
||||||
|
import li.songe.selector.LogicalOperator
|
||||||
|
import li.songe.selector.NotExpression
|
||||||
|
import li.songe.selector.PolynomialExpression
|
||||||
|
import li.songe.selector.Position
|
||||||
|
import li.songe.selector.PositionImpl
|
||||||
|
import li.songe.selector.PropertySegment
|
||||||
|
import li.songe.selector.PropertyWrapper
|
||||||
import li.songe.selector.Selector
|
import li.songe.selector.Selector
|
||||||
import li.songe.selector.data.BinaryExpression
|
import li.songe.selector.TupleExpression
|
||||||
import li.songe.selector.data.CompareOperator
|
import li.songe.selector.ValueExpression
|
||||||
import li.songe.selector.data.ConnectExpression
|
|
||||||
import li.songe.selector.data.ConnectOperator
|
|
||||||
import li.songe.selector.data.ConnectSegment
|
|
||||||
import li.songe.selector.data.ConnectWrapper
|
|
||||||
import li.songe.selector.data.Expression
|
|
||||||
import li.songe.selector.data.LogicalExpression
|
|
||||||
import li.songe.selector.data.LogicalOperator
|
|
||||||
import li.songe.selector.data.PolynomialExpression
|
|
||||||
import li.songe.selector.data.PrimitiveValue
|
|
||||||
import li.songe.selector.data.PropertySegment
|
|
||||||
import li.songe.selector.data.PropertyWrapper
|
|
||||||
import li.songe.selector.data.TupleExpression
|
|
||||||
import li.songe.selector.gkdAssert
|
import li.songe.selector.gkdAssert
|
||||||
import li.songe.selector.gkdError
|
import li.songe.selector.gkdError
|
||||||
|
import li.songe.selector.parser.ParserSet.connectSelectorParser
|
||||||
|
import li.songe.selector.parser.ParserSet.endParser
|
||||||
|
import li.songe.selector.parser.ParserSet.whiteCharParser
|
||||||
import li.songe.selector.toMatches
|
import li.songe.selector.toMatches
|
||||||
|
|
||||||
internal object ParserSet {
|
internal object ParserSet {
|
||||||
|
@ -212,10 +218,26 @@ internal object ParserSet {
|
||||||
i++
|
i++
|
||||||
ParserResult(TupleExpression(numbers), i - offset)
|
ParserResult(TupleExpression(numbers), i - offset)
|
||||||
}
|
}
|
||||||
private val tupleExpressionReg = Regex("^\\(\\s*\\d+,.*$")
|
|
||||||
|
private fun isTupleExpression(source: CharSequence): Boolean {
|
||||||
|
// ^\(\s*\d+\s*,
|
||||||
|
var i = 0
|
||||||
|
if (source.getOrNull(i) != '(') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
if (source.getOrNull(i) !in '0'..'9') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i += integerParser(source, i).length
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
return source.getOrNull(i) == ','
|
||||||
|
}
|
||||||
|
|
||||||
val connectExpressionParser = Parser(polynomialExpressionParser.prefix) { source, offset, _ ->
|
val connectExpressionParser = Parser(polynomialExpressionParser.prefix) { source, offset, _ ->
|
||||||
var i = offset
|
var i = offset
|
||||||
if (tupleExpressionReg.matches(source.subSequence(offset, source.length))) {
|
if (isTupleExpression(source.subSequence(offset, source.length))) {
|
||||||
val tupleExpressionResult = tupleExpressionParser(source, i)
|
val tupleExpressionResult = tupleExpressionParser(source, i)
|
||||||
i += tupleExpressionResult.length
|
i += tupleExpressionResult.length
|
||||||
ParserResult(tupleExpressionResult.data, i - offset)
|
ParserResult(tupleExpressionResult.data, i - offset)
|
||||||
|
@ -244,13 +266,20 @@ internal object ParserSet {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val attrOperatorParser =
|
private fun attrOperatorParser(
|
||||||
Parser(CompareOperator.allSubClasses.joinToString("") { it.key }) { source, offset, _ ->
|
source: CharSequence,
|
||||||
val operator = CompareOperator.allSubClasses.find { compareOperator ->
|
offset: Int
|
||||||
source.startsWith(compareOperator.key, offset)
|
): PositionImpl<CompareOperator> {
|
||||||
} ?: gkdError(source, offset, "CompareOperator")
|
val operator = CompareOperator.allSubClasses.find { compareOperator ->
|
||||||
ParserResult(operator, operator.key.length)
|
source.startsWith(compareOperator.key, offset)
|
||||||
}
|
} ?: gkdError(source, offset, "CompareOperator")
|
||||||
|
return PositionImpl(
|
||||||
|
start = offset,
|
||||||
|
end = offset + operator.key.length,
|
||||||
|
value = operator
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val stringParser = Parser("`'\"") { source, offset, prefix ->
|
val stringParser = Parser("`'\"") { source, offset, prefix ->
|
||||||
var i = offset
|
var i = offset
|
||||||
gkdAssert(source, i, prefix)
|
gkdAssert(source, i, prefix)
|
||||||
|
@ -312,7 +341,7 @@ internal object ParserSet {
|
||||||
|
|
||||||
private val varPrefix = "_" + ('a'..'z').joinToString("") + ('A'..'Z').joinToString("")
|
private val varPrefix = "_" + ('a'..'z').joinToString("") + ('A'..'Z').joinToString("")
|
||||||
private val varStr = varPrefix + '.' + ('0'..'9').joinToString("")
|
private val varStr = varPrefix + '.' + ('0'..'9').joinToString("")
|
||||||
val propertyParser = Parser(varPrefix) { source, offset, prefix ->
|
private val propertyParser = Parser(varPrefix) { source, offset, prefix ->
|
||||||
var i = offset
|
var i = offset
|
||||||
gkdAssert(source, i, prefix)
|
gkdAssert(source, i, prefix)
|
||||||
var data = source[i].toString()
|
var data = source[i].toString()
|
||||||
|
@ -327,120 +356,253 @@ internal object ParserSet {
|
||||||
ParserResult(data, i - offset)
|
ParserResult(data, i - offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
val valueParser =
|
private fun isVarChar(c: Char?, start: Boolean = false): Boolean {
|
||||||
Parser("tfn-" + stringParser.prefix + integerParser.prefix) { source, offset, prefix ->
|
c ?: return false
|
||||||
var i = offset
|
return (c == '_' || c in 'a'..'z' || c in 'A'..'Z' || (!start && c in '0'..'9'))
|
||||||
gkdAssert(source, i, prefix)
|
}
|
||||||
val value: PrimitiveValue = when (source[i]) {
|
|
||||||
't' -> {
|
private fun matchLiteral(source: CharSequence, offset: Int, raw: String): Boolean {
|
||||||
|
if (source.startsWith(raw, offset)) {
|
||||||
|
val c = source.getOrNull(offset + raw.length) ?: return true
|
||||||
|
return !(c == '_' || c in 'a'..'z' || c in 'A'..'Z' || c in '0'..'9')
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseVariable(source: CharSequence, offset: Int): ValueExpression {
|
||||||
|
var i = offset
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
if (i >= source.length) {
|
||||||
|
gkdError(source, i, "Variable")
|
||||||
|
}
|
||||||
|
if (matchLiteral(source, i, "true")) {
|
||||||
|
return ValueExpression.BooleanLiteral(start = i, value = true)
|
||||||
|
} else if (matchLiteral(source, i, "false")) {
|
||||||
|
return ValueExpression.BooleanLiteral(start = i, value = false)
|
||||||
|
} else if (matchLiteral(source, i, "null")) {
|
||||||
|
return ValueExpression.NullLiteral(start = i)
|
||||||
|
}
|
||||||
|
if (source[i] in stringParser.prefix) {
|
||||||
|
val result = stringParser(source, i)
|
||||||
|
i += result.length
|
||||||
|
return ValueExpression.StringLiteral(
|
||||||
|
start = i - result.length,
|
||||||
|
end = i,
|
||||||
|
value = result.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (source[i] == '-') {
|
||||||
|
i++
|
||||||
|
val result = integerParser(source, i)
|
||||||
|
i += result.length
|
||||||
|
return ValueExpression.IntLiteral(
|
||||||
|
start = i - result.length - 1,
|
||||||
|
end = i,
|
||||||
|
value = -result.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (source[i] in integerParser.prefix) {
|
||||||
|
val result = integerParser(source, i)
|
||||||
|
i += result.length
|
||||||
|
return ValueExpression.IntLiteral(
|
||||||
|
start = i - result.length,
|
||||||
|
end = i, value = result.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastToken: ValueExpression.Variable? = null
|
||||||
|
while (i < source.length) {
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
val char = source.getOrNull(i)
|
||||||
|
when {
|
||||||
|
char == '(' -> {
|
||||||
|
val start = i
|
||||||
i++
|
i++
|
||||||
"rue".forEach { c ->
|
i += whiteCharParser(source, i).length
|
||||||
gkdAssert(source, i, c.toString())
|
if (lastToken != null) {
|
||||||
|
// 暂不支持 object()()
|
||||||
|
if (lastToken is ValueExpression.CallExpression) {
|
||||||
|
gkdError(source, i, "Variable")
|
||||||
|
}
|
||||||
|
val arguments = mutableListOf<ValueExpression>()
|
||||||
|
while (i < source.length && source[i] != ')') {
|
||||||
|
val result = parseVariable(source, i)
|
||||||
|
arguments.add(result)
|
||||||
|
i += result.length
|
||||||
|
if (source.getOrNull(i) == ',') {
|
||||||
|
i++
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
gkdAssert(source, i, ")")
|
||||||
i++
|
i++
|
||||||
}
|
lastToken = ValueExpression.CallExpression(
|
||||||
PrimitiveValue.BooleanValue(true)
|
start = lastToken.start,
|
||||||
}
|
end = i,
|
||||||
|
lastToken,
|
||||||
'f' -> {
|
arguments
|
||||||
i++
|
)
|
||||||
"alse".forEach { c ->
|
} else {
|
||||||
gkdAssert(source, i, c.toString())
|
val result = parseVariable(source, i)
|
||||||
|
i += result.length
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
gkdAssert(source, i, ")")
|
||||||
i++
|
i++
|
||||||
|
val end = i
|
||||||
|
return when (result) {
|
||||||
|
is ValueExpression.BooleanLiteral -> result.copy(
|
||||||
|
start = start
|
||||||
|
)
|
||||||
|
|
||||||
|
is ValueExpression.IntLiteral -> result.copy(start = start, end = end)
|
||||||
|
is ValueExpression.NullLiteral -> result.copy(start = start)
|
||||||
|
is ValueExpression.StringLiteral -> result.copy(
|
||||||
|
start = start,
|
||||||
|
end = end
|
||||||
|
)
|
||||||
|
|
||||||
|
is ValueExpression.CallExpression -> result.copy(
|
||||||
|
start = start,
|
||||||
|
end = end
|
||||||
|
)
|
||||||
|
|
||||||
|
is ValueExpression.Identifier -> result.copy(start = start)
|
||||||
|
is ValueExpression.MemberExpression -> result.copy(
|
||||||
|
start = start,
|
||||||
|
end = end
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PrimitiveValue.BooleanValue(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
'n' -> {
|
char == '.' -> {
|
||||||
i++
|
i++
|
||||||
"ull".forEach { c ->
|
if (lastToken !is ValueExpression.Variable) {
|
||||||
gkdAssert(source, i, c.toString())
|
gkdError(source, i, "Variable")
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
PrimitiveValue.NullValue
|
if (!isVarChar(source.getOrNull(i), true)) {
|
||||||
|
gkdError(source, i, "Variable")
|
||||||
|
}
|
||||||
|
val property = source.drop(i).takeWhile { c -> isVarChar(c, false) }.toString()
|
||||||
|
lastToken = ValueExpression.MemberExpression(
|
||||||
|
start = lastToken.start,
|
||||||
|
end = i + property.length,
|
||||||
|
lastToken,
|
||||||
|
property
|
||||||
|
)
|
||||||
|
i += property.length
|
||||||
}
|
}
|
||||||
|
|
||||||
in stringParser.prefix -> {
|
isVarChar(char) -> {
|
||||||
val s = stringParser(source, i)
|
val variable = source.drop(i).takeWhile { c -> isVarChar(c) }.toString()
|
||||||
i += s.length
|
lastToken = ValueExpression.Identifier(start = i, variable)
|
||||||
PrimitiveValue.StringValue(s.data)
|
i += variable.length
|
||||||
}
|
|
||||||
|
|
||||||
'-' -> {
|
|
||||||
i++
|
|
||||||
gkdAssert(source, i, integerParser.prefix)
|
|
||||||
val n = integerParser(source, i)
|
|
||||||
i += n.length
|
|
||||||
PrimitiveValue.IntValue(-n.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
in integerParser.prefix -> {
|
|
||||||
val n = integerParser(source, i)
|
|
||||||
i += n.length
|
|
||||||
PrimitiveValue.IntValue(n.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
gkdError(source, i, prefix)
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParserResult(value, i - offset)
|
|
||||||
}
|
}
|
||||||
|
if (lastToken == null) {
|
||||||
|
gkdError(source, i, "Variable")
|
||||||
|
}
|
||||||
|
return lastToken
|
||||||
|
}
|
||||||
|
|
||||||
val binaryExpressionParser = Parser { source, offset, _ ->
|
private fun valueParser(source: CharSequence, offset: Int): ValueExpression {
|
||||||
|
val prefix = "tfn-" + stringParser.prefix + integerParser.prefix + varPrefix
|
||||||
|
gkdAssert(source, offset, prefix)
|
||||||
|
val result = parseVariable(source, offset)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun binaryExpressionParser(source: CharSequence, offset: Int): BinaryExpression {
|
||||||
var i = offset
|
var i = offset
|
||||||
val parserResult = propertyParser(source, i)
|
val leftValueResult = valueParser(source, i)
|
||||||
i += parserResult.length
|
i += leftValueResult.length
|
||||||
i += whiteCharParser(source, i).length
|
i += whiteCharParser(source, i).length
|
||||||
val operatorResult = attrOperatorParser(source, i)
|
val operatorResult = attrOperatorParser(source, i)
|
||||||
i += operatorResult.length
|
i += operatorResult.length
|
||||||
i += whiteCharParser(source, i).length
|
i += whiteCharParser(source, i).length
|
||||||
val valueResult = valueParser(source, i).let { result ->
|
val rightValueResult = valueParser(source, i).let { result ->
|
||||||
// check regex
|
// check regex
|
||||||
if ((operatorResult.data == CompareOperator.Matches || operatorResult.data == CompareOperator.NotMatches) && result.data is PrimitiveValue.StringValue) {
|
if ((operatorResult.value == CompareOperator.Matches || operatorResult.value == CompareOperator.NotMatches) && result is ValueExpression.StringLiteral) {
|
||||||
val matches = try {
|
val matches = try {
|
||||||
result.data.value.toMatches()
|
result.value.toMatches()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
gkdError(source, i, "valid primitive string regex", e)
|
gkdError(source, i, "valid primitive string regex", e)
|
||||||
}
|
}
|
||||||
result.copy(data = result.data.copy(matches = matches))
|
result.copy(
|
||||||
|
matches = matches
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!operatorResult.data.allowType(valueResult.data)) {
|
i += rightValueResult.length
|
||||||
gkdError(source, i, "valid primitive value")
|
return BinaryExpression(
|
||||||
}
|
start = offset,
|
||||||
i += valueResult.length
|
end = i,
|
||||||
ParserResult(
|
leftValueResult,
|
||||||
BinaryExpression(
|
operatorResult,
|
||||||
parserResult.data, operatorResult.data, valueResult.data
|
rightValueResult
|
||||||
), i - offset
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val logicalOperatorParser = Parser { source, offset, _ ->
|
private fun logicalOperatorParser(
|
||||||
|
source: CharSequence,
|
||||||
|
offset: Int
|
||||||
|
): PositionImpl<LogicalOperator> {
|
||||||
var i = offset
|
var i = offset
|
||||||
i += whiteCharParser(source, i).length
|
i += whiteCharParser(source, i).length
|
||||||
val operator = LogicalOperator.allSubClasses.find { logicalOperator ->
|
val operator = LogicalOperator.allSubClasses.find { logicalOperator ->
|
||||||
source.startsWith(logicalOperator.key, offset)
|
source.startsWith(logicalOperator.key, offset)
|
||||||
} ?: gkdError(source, offset, "LogicalOperator")
|
} ?: gkdError(source, offset, "LogicalOperator")
|
||||||
ParserResult(operator, operator.key.length)
|
return PositionImpl(
|
||||||
|
start = i,
|
||||||
|
end = i + operator.key.length,
|
||||||
|
value = operator
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun unaryExpressionParser(
|
||||||
|
source: CharSequence,
|
||||||
|
offset: Int
|
||||||
|
): NotExpression {
|
||||||
|
var i = offset
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
|
gkdAssert(source, i, "!")
|
||||||
|
val start = i
|
||||||
|
i += 1
|
||||||
|
gkdAssert(source, i, "(")
|
||||||
|
val expression = expressionParser(source, i, true)
|
||||||
|
i += expression.length
|
||||||
|
return NotExpression(
|
||||||
|
start = start,
|
||||||
|
expression
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// a>1 && a>1 || a>1
|
// a>1 && a>1 || a>1
|
||||||
// (a>1 || a>1) && a>1
|
// (a>1 || a>1) && a>1
|
||||||
fun expressionParser(source: String, offset: Int): ParserResult<Expression> {
|
fun expressionParser(
|
||||||
|
source: CharSequence,
|
||||||
|
offset: Int,
|
||||||
|
one: Boolean = false, // 是否只解析一个表达式
|
||||||
|
): Expression {
|
||||||
var i = offset
|
var i = offset
|
||||||
i += whiteCharParser(source, i).length
|
i += whiteCharParser(source, i).length
|
||||||
// [exp, ||, exp, &&, &&]
|
// [exp, ||, exp, &&, &&]
|
||||||
val parserResults = mutableListOf<ParserResult<*>>()
|
val parserResults = mutableListOf<Position>()
|
||||||
while (i < source.length && source[i] != ']' && source[i] != ')') {
|
while (i < source.length && source[i] != ']' && source[i] != ')') {
|
||||||
when (source[i]) {
|
when (source[i]) {
|
||||||
'(' -> {
|
'(' -> {
|
||||||
|
val start = i
|
||||||
if (parserResults.isNotEmpty()) {
|
if (parserResults.isNotEmpty()) {
|
||||||
val lastToken = parserResults.last()
|
val lastToken = parserResults.last()
|
||||||
if (lastToken.data !is LogicalOperator) {
|
if (!(lastToken is PositionImpl<*> && lastToken.value is LogicalOperator)) {
|
||||||
var count = 0
|
var count = 0
|
||||||
while (i - 1 >= count && source[i - 1 - count] in whiteCharParser.prefix) {
|
while (i - 1 >= count && source[i - 1 - count] in whiteCharParser.prefix) {
|
||||||
count++
|
count++
|
||||||
|
@ -450,16 +612,44 @@ internal object ParserSet {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [(a)=1]
|
||||||
|
// [(a=1)]
|
||||||
i++
|
i++
|
||||||
parserResults.add(expressionParser(source, i).apply { i += length })
|
val exp = expressionParser(source, i).apply { i += length }
|
||||||
gkdAssert(source, i, ")")
|
gkdAssert(source, i, ")")
|
||||||
i++
|
i++
|
||||||
|
val end = i
|
||||||
|
parserResults.add(
|
||||||
|
when (exp) {
|
||||||
|
is BinaryExpression -> exp.copy(
|
||||||
|
start = start,
|
||||||
|
end = end
|
||||||
|
)
|
||||||
|
|
||||||
|
is LogicalExpression -> exp.copy(
|
||||||
|
start = start,
|
||||||
|
end = end
|
||||||
|
)
|
||||||
|
|
||||||
|
is NotExpression -> exp.copy(
|
||||||
|
start = start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (one) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
in "|&" -> {
|
in "|&" -> {
|
||||||
parserResults.add(logicalOperatorParser(source, i).apply { i += length })
|
parserResults.add(logicalOperatorParser(source, i).apply { i += length })
|
||||||
i += whiteCharParser(source, i).length
|
i += whiteCharParser(source, i).length
|
||||||
gkdAssert(source, i, "(" + propertyParser.prefix)
|
gkdAssert(source, i, "(!" + propertyParser.prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
'!' -> {
|
||||||
|
parserResults.add(unaryExpressionParser(source, i).apply { i += length })
|
||||||
|
i += whiteCharParser(source, i).length
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -474,21 +664,29 @@ internal object ParserSet {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (parserResults.size == 1) {
|
if (parserResults.size == 1) {
|
||||||
return ParserResult(parserResults.first().data as Expression, i - offset)
|
return parserResults.first() as Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
// 运算符优先级 && > ||
|
// 运算符优先级 && > ||
|
||||||
// a && b || c -> ab || c
|
// a && b || c -> ab || c
|
||||||
// 0 1 2 3 4 -> 0 1 2
|
// 0 1 2 3 4 -> 0 1 2
|
||||||
val tokens = parserResults.map { it.data }.toMutableList()
|
val tokens = parserResults.toMutableList()
|
||||||
var index = 0
|
var index = 0
|
||||||
while (index < tokens.size) {
|
while (index < tokens.size) {
|
||||||
val token = tokens[index]
|
val token = tokens[index]
|
||||||
if (token == LogicalOperator.AndOperator) {
|
if (token is PositionImpl<*> && token.value == LogicalOperator.AndOperator) {
|
||||||
|
val left = tokens[index - 1] as Expression
|
||||||
|
val right = tokens[index + 1] as Expression
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val operator = token as PositionImpl<LogicalOperator>
|
||||||
tokens[index] = LogicalExpression(
|
tokens[index] = LogicalExpression(
|
||||||
left = tokens[index - 1] as Expression,
|
start = left.start,
|
||||||
operator = LogicalOperator.AndOperator,
|
end = right.end,
|
||||||
right = tokens[index + 1] as Expression
|
left = left,
|
||||||
|
|
||||||
|
operator = operator,
|
||||||
|
right = right
|
||||||
)
|
)
|
||||||
tokens.removeAt(index - 1)
|
tokens.removeAt(index - 1)
|
||||||
tokens.removeAt(index + 1 - 1)
|
tokens.removeAt(index + 1 - 1)
|
||||||
|
@ -497,15 +695,22 @@ internal object ParserSet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (tokens.size > 1) {
|
while (tokens.size > 1) {
|
||||||
|
val left = tokens[0] as Expression
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val operator = tokens[1] as PositionImpl<LogicalOperator>
|
||||||
|
val right = tokens[2] as Expression
|
||||||
tokens[1] = LogicalExpression(
|
tokens[1] = LogicalExpression(
|
||||||
left = tokens[0] as Expression,
|
start = left.start,
|
||||||
operator = tokens[1] as LogicalOperator.OrOperator,
|
end = right.end,
|
||||||
right = tokens[2] as Expression
|
left = left,
|
||||||
|
operator = operator,
|
||||||
|
right = right
|
||||||
)
|
)
|
||||||
tokens.removeAt(0)
|
tokens.removeAt(0)
|
||||||
tokens.removeAt(2 - 1)
|
tokens.removeAt(2 - 1)
|
||||||
}
|
}
|
||||||
return ParserResult(tokens.first() as Expression, i - offset)
|
return tokens.first() as Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -519,7 +724,7 @@ internal object ParserSet {
|
||||||
gkdAssert(source, i, "]")
|
gkdAssert(source, i, "]")
|
||||||
i++
|
i++
|
||||||
ParserResult(
|
ParserResult(
|
||||||
exp.data, i - offset
|
exp, i - offset
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -578,30 +783,30 @@ internal object ParserSet {
|
||||||
}
|
}
|
||||||
ParserResult(Unit, 0)
|
ParserResult(Unit, 0)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
val selectorParser: (String) -> Selector = { source ->
|
|
||||||
var i = 0
|
internal fun selectorParser(source: String): Selector {
|
||||||
i += whiteCharParser(source, i).length
|
var i = 0
|
||||||
val combinatorSelectorResult = connectSelectorParser(source, i)
|
i += whiteCharParser(source, i).length
|
||||||
i += combinatorSelectorResult.length
|
val combinatorSelectorResult = connectSelectorParser(source, i)
|
||||||
|
i += combinatorSelectorResult.length
|
||||||
i += whiteCharParser(source, i).length
|
|
||||||
i += endParser(source, i).length
|
i += whiteCharParser(source, i).length
|
||||||
val data = combinatorSelectorResult.data
|
i += endParser(source, i).length
|
||||||
val propertySelectorList = mutableListOf<PropertySegment>()
|
val data = combinatorSelectorResult.data
|
||||||
val combinatorSelectorList = mutableListOf<ConnectSegment>()
|
val propertySelectorList = mutableListOf<PropertySegment>()
|
||||||
propertySelectorList.add(data.first)
|
val combinatorSelectorList = mutableListOf<ConnectSegment>()
|
||||||
data.second.forEach {
|
propertySelectorList.add(data.first)
|
||||||
propertySelectorList.add(it.second)
|
data.second.forEach {
|
||||||
combinatorSelectorList.add(it.first)
|
propertySelectorList.add(it.second)
|
||||||
}
|
combinatorSelectorList.add(it.first)
|
||||||
val wrapperList = mutableListOf(PropertyWrapper(propertySelectorList.first()))
|
}
|
||||||
combinatorSelectorList.forEachIndexed { index, combinatorSelector ->
|
val wrapperList = mutableListOf(PropertyWrapper(propertySelectorList.first()))
|
||||||
val combinatorSelectorWrapper = ConnectWrapper(combinatorSelector, wrapperList.last())
|
combinatorSelectorList.forEachIndexed { index, combinatorSelector ->
|
||||||
val propertySelectorWrapper =
|
val combinatorSelectorWrapper = ConnectWrapper(combinatorSelector, wrapperList.last())
|
||||||
PropertyWrapper(propertySelectorList[index + 1], combinatorSelectorWrapper)
|
val propertySelectorWrapper =
|
||||||
wrapperList.add(propertySelectorWrapper)
|
PropertyWrapper(propertySelectorList[index + 1], combinatorSelectorWrapper)
|
||||||
}
|
wrapperList.add(propertySelectorWrapper)
|
||||||
Selector(wrapperList.last())
|
}
|
||||||
}
|
return Selector(source, wrapperList.last())
|
||||||
}
|
}
|
280
selector/src/commonMain/kotlin/li/songe/selector/util.kt
Normal file
280
selector/src/commonMain/kotlin/li/songe/selector/util.kt
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
package li.songe.selector
|
||||||
|
|
||||||
|
import kotlin.js.JsExport
|
||||||
|
|
||||||
|
fun escapeString(value: String, wrapChar: Char = '"'): String {
|
||||||
|
val sb = StringBuilder(value.length + 2)
|
||||||
|
sb.append(wrapChar)
|
||||||
|
value.forEach { c ->
|
||||||
|
val escapeChar = when (c) {
|
||||||
|
wrapChar -> wrapChar
|
||||||
|
'\n' -> 'n'
|
||||||
|
'\r' -> 'r'
|
||||||
|
'\t' -> 't'
|
||||||
|
'\b' -> 'b'
|
||||||
|
'\\' -> '\\'
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (escapeChar != null) {
|
||||||
|
sb.append("\\" + escapeChar)
|
||||||
|
} else {
|
||||||
|
when (c.code) {
|
||||||
|
in 0..0xf -> {
|
||||||
|
sb.append("\\x0" + c.code.toString(16))
|
||||||
|
}
|
||||||
|
|
||||||
|
in 0x10..0x1f -> {
|
||||||
|
sb.append("\\x" + c.code.toString(16))
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
sb.append(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append(wrapChar)
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val REG_SPECIAL_STRING = "\\^$.?*|+()[]{}"
|
||||||
|
private fun getMatchValue(value: String, prefix: String, suffix: String): String? {
|
||||||
|
if (value.startsWith(prefix) && value.endsWith(suffix) && value.length >= (prefix.length + suffix.length)) {
|
||||||
|
for (i in prefix.length until value.length - suffix.length) {
|
||||||
|
if (value[i] in REG_SPECIAL_STRING) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value.subSequence(prefix.length, value.length - suffix.length).toString()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun optimizeMatchString(value: String): ((CharSequence) -> Boolean)? {
|
||||||
|
getMatchValue(value, "(?is)", ".*")?.let { startsWithValue ->
|
||||||
|
return { value -> value.startsWith(startsWithValue, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
getMatchValue(value, "(?is).*", ".*")?.let { containsValue ->
|
||||||
|
return { value -> value.contains(containsValue, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
getMatchValue(value, "(?is).*", "")?.let { endsWithValue ->
|
||||||
|
return { value -> value.endsWith(endsWithValue, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
class DefaultTypeInfo(
|
||||||
|
val booleanType: TypeInfo,
|
||||||
|
val intType: TypeInfo,
|
||||||
|
val stringType: TypeInfo,
|
||||||
|
val contextType: TypeInfo,
|
||||||
|
val nodeType: TypeInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
fun initDefaultTypeInfo(): DefaultTypeInfo {
|
||||||
|
val booleanType = TypeInfo(PrimitiveType.BooleanType)
|
||||||
|
val intType = TypeInfo(PrimitiveType.IntType)
|
||||||
|
val stringType = TypeInfo(PrimitiveType.StringType)
|
||||||
|
|
||||||
|
intType.methods = arrayOf(
|
||||||
|
MethodInfo("toString", stringType),
|
||||||
|
MethodInfo("toString", stringType, arrayOf(intType)),
|
||||||
|
MethodInfo("plus", intType, arrayOf(intType)),
|
||||||
|
MethodInfo("minus", intType, arrayOf(intType)),
|
||||||
|
MethodInfo("times", intType, arrayOf(intType)),
|
||||||
|
MethodInfo("div", intType, arrayOf(intType)),
|
||||||
|
MethodInfo("rem", intType, arrayOf(intType)),
|
||||||
|
)
|
||||||
|
stringType.props = arrayOf(
|
||||||
|
PropInfo("length", intType),
|
||||||
|
)
|
||||||
|
stringType.methods = arrayOf(
|
||||||
|
MethodInfo("get", stringType, arrayOf(intType)),
|
||||||
|
MethodInfo("at", stringType, arrayOf(intType)),
|
||||||
|
MethodInfo("substring", stringType, arrayOf(intType, intType)),
|
||||||
|
MethodInfo("toInt", intType),
|
||||||
|
MethodInfo("toInt", intType, arrayOf(intType)),
|
||||||
|
MethodInfo("indexOf", intType, arrayOf(stringType)),
|
||||||
|
MethodInfo("indexOf", intType, arrayOf(stringType, intType)),
|
||||||
|
)
|
||||||
|
|
||||||
|
val contextType = TypeInfo(PrimitiveType.ObjectType("context"))
|
||||||
|
val nodeType = TypeInfo(PrimitiveType.ObjectType("node"))
|
||||||
|
|
||||||
|
nodeType.props = arrayOf(
|
||||||
|
PropInfo("_id", intType),
|
||||||
|
PropInfo("_pid", intType),
|
||||||
|
|
||||||
|
PropInfo("id", stringType),
|
||||||
|
PropInfo("vid", stringType),
|
||||||
|
PropInfo("name", stringType),
|
||||||
|
PropInfo("text", stringType),
|
||||||
|
PropInfo("desc", stringType),
|
||||||
|
|
||||||
|
PropInfo("clickable", booleanType),
|
||||||
|
PropInfo("focusable", booleanType),
|
||||||
|
PropInfo("checkable", booleanType),
|
||||||
|
PropInfo("checked", booleanType),
|
||||||
|
PropInfo("editable", booleanType),
|
||||||
|
PropInfo("longClickable", booleanType),
|
||||||
|
PropInfo("visibleToUser", booleanType),
|
||||||
|
|
||||||
|
PropInfo("left", intType),
|
||||||
|
PropInfo("top", intType),
|
||||||
|
PropInfo("right", intType),
|
||||||
|
PropInfo("bottom", intType),
|
||||||
|
PropInfo("width", intType),
|
||||||
|
PropInfo("height", intType),
|
||||||
|
|
||||||
|
PropInfo("childCount", intType),
|
||||||
|
PropInfo("index", intType),
|
||||||
|
PropInfo("depth", intType),
|
||||||
|
|
||||||
|
PropInfo("parent", nodeType),
|
||||||
|
)
|
||||||
|
nodeType.methods = arrayOf(
|
||||||
|
MethodInfo("getChild", nodeType, arrayOf(intType)),
|
||||||
|
)
|
||||||
|
contextType.methods = arrayOf(*nodeType.methods)
|
||||||
|
contextType.props = arrayOf(*nodeType.props)
|
||||||
|
return DefaultTypeInfo(
|
||||||
|
booleanType = booleanType,
|
||||||
|
intType = intType,
|
||||||
|
stringType = stringType,
|
||||||
|
contextType = contextType,
|
||||||
|
nodeType = nodeType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
fun getIntInvoke(target: Int, name: String, args: List<Any?>): Any? {
|
||||||
|
return when (name) {
|
||||||
|
"plus" -> {
|
||||||
|
target + (args.getIntOrNull() ?: return null)
|
||||||
|
}
|
||||||
|
|
||||||
|
"minus" -> {
|
||||||
|
target - (args.getIntOrNull() ?: return null)
|
||||||
|
}
|
||||||
|
|
||||||
|
"times" -> {
|
||||||
|
target * (args.getIntOrNull() ?: return null)
|
||||||
|
}
|
||||||
|
|
||||||
|
"div" -> {
|
||||||
|
target / (args.getIntOrNull()?.also { if (it == 0) return null } ?: return null)
|
||||||
|
}
|
||||||
|
|
||||||
|
"rem" -> {
|
||||||
|
target % (args.getIntOrNull()?.also { if (it == 0) return null } ?: return null)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal fun List<Any?>.getIntOrNull(i: Int = 0): Int? {
|
||||||
|
val v = getOrNull(i)
|
||||||
|
if (v is Int) return v
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
fun getStringInvoke(target: String, name: String, args: List<Any?>): Any? {
|
||||||
|
return getCharSequenceInvoke(target, name, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCharSequenceInvoke(target: CharSequence, name: String, args: List<Any?>): Any? {
|
||||||
|
|
||||||
|
return when (name) {
|
||||||
|
"get" -> {
|
||||||
|
target.getOrNull(args.getIntOrNull() ?: return null).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
"at" -> {
|
||||||
|
val i = args.getIntOrNull() ?: return null
|
||||||
|
if (i < 0) {
|
||||||
|
target.getOrNull(target.length + i).toString()
|
||||||
|
} else {
|
||||||
|
target.getOrNull(i).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"substring" -> {
|
||||||
|
when (args.size) {
|
||||||
|
1 -> {
|
||||||
|
val start = args.getIntOrNull() ?: return null
|
||||||
|
if (start < 0) return null
|
||||||
|
if (start >= target.length) return ""
|
||||||
|
target.substring(
|
||||||
|
start,
|
||||||
|
target.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
2 -> {
|
||||||
|
val start = args.getIntOrNull() ?: return null
|
||||||
|
if (start < 0) return null
|
||||||
|
if (start >= target.length) return ""
|
||||||
|
val end = args.getIntOrNull(1) ?: return null
|
||||||
|
if (end < start) return null
|
||||||
|
target.substring(
|
||||||
|
start,
|
||||||
|
end.coerceAtMost(target.length)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"toInt" -> when (args.size) {
|
||||||
|
0 -> target.toString().toIntOrNull()
|
||||||
|
1 -> {
|
||||||
|
val radix = args.getIntOrNull() ?: return null
|
||||||
|
if (radix !in 2..36) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
target.toString().toIntOrNull(radix)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
"indexOf" -> {
|
||||||
|
when (args.size) {
|
||||||
|
1 -> {
|
||||||
|
val str = args[0] as? CharSequence ?: return null
|
||||||
|
target.indexOf(str.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
2 -> {
|
||||||
|
val str = args[0] as? CharSequence ?: return null
|
||||||
|
val startIndex = args.getIntOrNull(1) ?: return null
|
||||||
|
target.indexOf(str.toString(), startIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsExport
|
||||||
|
fun getStringAttr(target: String, name: String): Any? {
|
||||||
|
return getCharSequenceAttr(target, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCharSequenceAttr(target: CharSequence, name: String): Any? {
|
||||||
|
return when (name) {
|
||||||
|
"length" -> target.length
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,15 +24,47 @@ class ParserTest {
|
||||||
private val json = Json {
|
private val json = Json {
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
private val transform = Transform<TestNode>(getAttr = { node, name ->
|
|
||||||
if (name == "_id") return@Transform node.id
|
private fun getNodeAttr(node: TestNode, name: String): Any? {
|
||||||
if (name == "_pid") return@Transform node.pid
|
if (name == "_id") return node.id
|
||||||
val value = node.attr[name] ?: return@Transform null
|
if (name == "_pid") return node.pid
|
||||||
if (value is JsonNull) return@Transform null
|
if (name == "parent") return node.parent
|
||||||
value.intOrNull ?: value.booleanOrNull ?: value.content
|
val value = node.attr[name] ?: return null
|
||||||
}, getName = { node -> node.attr["name"]?.content }, getChildren = { node ->
|
if (value is JsonNull) return null
|
||||||
node.children.asSequence()
|
return value.intOrNull ?: value.booleanOrNull ?: value.content
|
||||||
}, getParent = { node -> node.parent })
|
}
|
||||||
|
|
||||||
|
private fun getNodeInvoke(target: TestNode, name: String, args: List<Any?>): Any? {
|
||||||
|
when (name) {
|
||||||
|
"getChild" -> {
|
||||||
|
val arg = (args.getIntOrNull() ?: return null)
|
||||||
|
return target.children.getOrNull(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val transform = Transform<TestNode>(
|
||||||
|
getAttr = { target, name ->
|
||||||
|
when (target) {
|
||||||
|
is TestNode -> getNodeAttr(target, name)
|
||||||
|
is String -> getCharSequenceAttr(target, name)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getInvoke = { target, name, args ->
|
||||||
|
when (target) {
|
||||||
|
is Int -> getIntInvoke(target, name, args)
|
||||||
|
is CharSequence -> getCharSequenceInvoke(target, name, args)
|
||||||
|
is TestNode -> getNodeInvoke(target, name, args)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getName = { node -> node.attr["name"]?.content },
|
||||||
|
getChildren = { node -> node.children.asSequence() },
|
||||||
|
getParent = { node -> node.parent }
|
||||||
|
)
|
||||||
|
|
||||||
private val idToSnapshot = HashMap<String, TestNode>()
|
private val idToSnapshot = HashMap<String, TestNode>()
|
||||||
|
|
||||||
|
@ -42,7 +74,7 @@ class ParserTest {
|
||||||
|
|
||||||
val file = assetsDir.resolve("$githubAssetId.json")
|
val file = assetsDir.resolve("$githubAssetId.json")
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
URL("https://github.com/gkd-kit/inspect/files/${githubAssetId}/file.zip").openStream()
|
URL("https://f.gkd.li/${githubAssetId}").openStream()
|
||||||
.use { inputStream ->
|
.use { inputStream ->
|
||||||
val zipInputStream = ZipInputStream(inputStream)
|
val zipInputStream = ZipInputStream(inputStream)
|
||||||
var entry = zipInputStream.nextEntry
|
var entry = zipInputStream.nextEntry
|
||||||
|
@ -78,7 +110,7 @@ class ParserTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test_expression() {
|
fun test_expression() {
|
||||||
println(ParserSet.expressionParser("a>1&&b>1&&c>1||d>1", 0).data)
|
println(ParserSet.expressionParser("a>1&&b>1&&c>1||d>1", 0))
|
||||||
println(Selector.parse("View[a>1&&b>1&&c>1||d>1&&x^=1] > Button[a>1||b*='zz'||c^=1]"))
|
println(Selector.parse("View[a>1&&b>1&&c>1||d>1&&x^=1] > Button[a>1||b*='zz'||c^=1]"))
|
||||||
println(Selector.parse("[id=`com.byted.pangle:id/tt_splash_skip_btn`||(id=`com.hupu.games:id/tv_time`&&text*=`跳过`)]"))
|
println(Selector.parse("[id=`com.byted.pangle:id/tt_splash_skip_btn`||(id=`com.hupu.games:id/tv_time`&&text*=`跳过`)]"))
|
||||||
}
|
}
|
||||||
|
@ -89,8 +121,8 @@ class ParserTest {
|
||||||
"ImageView < @FrameLayout < LinearLayout < RelativeLayout <n LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text\$='广告']"
|
"ImageView < @FrameLayout < LinearLayout < RelativeLayout <n LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text\$='广告']"
|
||||||
val selector = Selector.parse(text)
|
val selector = Selector.parse(text)
|
||||||
println("trackIndex: " + selector.trackIndex)
|
println("trackIndex: " + selector.trackIndex)
|
||||||
println("canCacheIndex: " + Selector.parse("A + B").canCacheIndex)
|
println("canCacheIndex: " + Selector.parse("A + B").useCache)
|
||||||
println("canCacheIndex: " + Selector.parse("A > B - C").canCacheIndex)
|
println("canCacheIndex: " + Selector.parse("A > B - C").useCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -118,7 +150,7 @@ class ParserTest {
|
||||||
fun check_parser() {
|
fun check_parser() {
|
||||||
val selector = Selector.parse("View > Text[index>-0]")
|
val selector = Selector.parse("View > Text[index>-0]")
|
||||||
println("selector: $selector")
|
println("selector: $selector")
|
||||||
println("canCacheIndex: " + selector.canCacheIndex)
|
println("canCacheIndex: " + selector.useCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -188,7 +220,7 @@ class ParserTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun check_regex() {
|
fun check_regex() {
|
||||||
val source = "[vid~=`(?is)TV.*`]"
|
val source = "[1<parent.getChild][vid=`im_cover`]"
|
||||||
println("source:$source")
|
println("source:$source")
|
||||||
val selector = Selector.parse(source)
|
val selector = Selector.parse(source)
|
||||||
val snapshotNode = getOrDownloadNode("https://i.gkd.li/i/14445410")
|
val snapshotNode = getOrDownloadNode("https://i.gkd.li/i/14445410")
|
||||||
|
@ -196,4 +228,32 @@ class ParserTest {
|
||||||
println("result:" + transform.querySelectorAll(snapshotNode, selector).map { n -> n.id }
|
println("result:" + transform.querySelectorAll(snapshotNode, selector).map { n -> n.id }
|
||||||
.toList())
|
.toList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun check_var() {
|
||||||
|
val result = ParserSet.parseVariable("rem(3)", 0)
|
||||||
|
println("result: $result")
|
||||||
|
println("check_var: " + result.stringify())
|
||||||
|
|
||||||
|
val selector = Selector.parse("[vid.get(-2)=`l`]")
|
||||||
|
println("selector: $selector")
|
||||||
|
|
||||||
|
val snapshotNode = getOrDownloadNode("https://i.gkd.li/i/14445410")
|
||||||
|
println(
|
||||||
|
"result: [" + transform.querySelectorAll(snapshotNode, selector)
|
||||||
|
.joinToString("||") { "_id=${it.id}" } + "]"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun check_type() {
|
||||||
|
val source =
|
||||||
|
"[visibleToUser=true][((parent.getChild(0,).getChild( (0), )=null) && (((`` >= 1)))) || (name=null && desc=null)]"
|
||||||
|
val selector = Selector.parse(source)
|
||||||
|
val typeInfo = initDefaultTypeInfo().contextType
|
||||||
|
val error = selector.checkType(typeInfo)
|
||||||
|
println("useCache: ${selector.useCache}")
|
||||||
|
println("error: $error")
|
||||||
|
println("check_type: $selector")
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user