diff --git a/.gitignore b/.gitignore index 63e265629..6d8ce4be3 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ local.properties *.jks *.keystore + +/_assets diff --git a/app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt b/app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt index 1973f3828..e00e9be78 100644 --- a/app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt +++ b/app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt @@ -3,6 +3,8 @@ package li.songe.gkd.data import android.accessibilityservice.AccessibilityService import android.view.accessibility.AccessibilityNodeInfo import kotlinx.coroutines.Job +import li.songe.gkd.service.CacheTransform +import li.songe.gkd.service.createCacheTransform import li.songe.gkd.service.lastTriggerRule import li.songe.gkd.service.lastTriggerTime import li.songe.gkd.service.querySelector @@ -119,15 +121,37 @@ sealed class ResolvedRule( else -> true } - fun query(nodeInfo: AccessibilityNodeInfo?): AccessibilityNodeInfo? { + private val canCacheIndex = (matches + excludeMatches).any { s -> s.canCacheIndex } + + fun query( + nodeInfo: AccessibilityNodeInfo?, + cacheTransform: CacheTransform? = null + ): AccessibilityNodeInfo? { if (nodeInfo == null) return null var target: AccessibilityNodeInfo? = null - for (selector in matches) { - target = nodeInfo.querySelector(selector, quickFind) ?: return null - } - for (selector in excludeMatches) { - if (nodeInfo.querySelector(selector, quickFind) != null) return null + if (canCacheIndex) { + val transform = cacheTransform ?: createCacheTransform() + for (selector in matches) { + target = nodeInfo.querySelector(selector, quickFind, transform.transform) + ?: return null + } + for (selector in excludeMatches) { + if (nodeInfo.querySelector( + selector, + quickFind, + transform.transform + ) != null + ) return null + } + } else { + for (selector in matches) { + target = nodeInfo.querySelector(selector, quickFind) ?: return null + } + for (selector in excludeMatches) { + if (nodeInfo.querySelector(selector, quickFind) != null) return null + } } + return target } diff --git a/app/src/main/kotlin/li/songe/gkd/debug/KtorErrorPlugin.kt b/app/src/main/kotlin/li/songe/gkd/debug/KtorErrorPlugin.kt index 145666236..ff541c602 100644 --- a/app/src/main/kotlin/li/songe/gkd/debug/KtorErrorPlugin.kt +++ b/app/src/main/kotlin/li/songe/gkd/debug/KtorErrorPlugin.kt @@ -5,6 +5,7 @@ import com.blankj.utilcode.util.LogUtils import io.ktor.http.HttpStatusCode import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.hooks.CallFailed +import io.ktor.server.plugins.origin import io.ktor.server.request.uri import io.ktor.server.response.respond import li.songe.gkd.data.RpcError @@ -13,7 +14,7 @@ val KtorErrorPlugin = createApplicationPlugin(name = "KtorErrorPlugin") { onCall { call -> // TODO 在局域网会被扫描工具批量请求多个路径 if (call.request.uri == "/" || call.request.uri.startsWith("/api/")) { - Log.d("Ktor", "onCall: ${call.request.uri}") + Log.d("Ktor", "onCall: ${call.request.origin.remoteAddress} -> ${call.request.uri}") } } on(CallFailed) { call, cause -> diff --git a/app/src/main/kotlin/li/songe/gkd/service/AbExt.kt b/app/src/main/kotlin/li/songe/gkd/service/AbExt.kt index 195ad3c71..613ed95e1 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/AbExt.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/AbExt.kt @@ -56,14 +56,27 @@ fun AccessibilityNodeInfo.getDepth(): Int { return depth } +fun AccessibilityNodeInfo.getVid(): CharSequence? { + val id = viewIdResourceName ?: return null + val appId = packageName ?: return null + if (id.startsWith(appId) && id.startsWith(":id/", appId.length)) { + return id.subSequence( + appId.length + ":id/".length, + id.length + ) + } + return null +} + fun AccessibilityNodeInfo.querySelector( selector: Selector, quickFind: Boolean = false, + transform: Transform? = null, ): AccessibilityNodeInfo? { + val t = (if (selector.canCacheIndex) transform else defaultTransform) ?: defaultTransform if (selector.isMatchRoot) { if (parent == null) { - val trackNodes = mutableListOf() - return selector.match(this, abTransform, trackNodes) + return selector.match(this, t) } return null } @@ -81,16 +94,16 @@ fun AccessibilityNodeInfo.querySelector( emptyList() }) if (nodes.isNotEmpty()) { - val trackNodes = mutableListOf() + val trackNodes = ArrayList(selector.tracks.size) nodes.forEach { childNode -> - val targetNode = selector.match(childNode, abTransform, trackNodes) + val targetNode = selector.match(childNode, t, trackNodes) if (targetNode != null) return targetNode } } return null } // 在一些开屏广告的界面会造成1-2s的阻塞 - return abTransform.querySelector(this, selector) + return t.querySelector(this, selector) } // 不可以在 多线程/不同协程作用域 里同时使用 @@ -148,17 +161,7 @@ val allowPropertyNames = setOf( private val getAttr: (AccessibilityNodeInfo, String) -> Any? = { node, name -> when (name) { "id" -> node.viewIdResourceName - "vid" -> node.viewIdResourceName?.let { id -> - val appId = node.packageName - if (appId != null && id.startsWith(appId) && id.startsWith(":id/", appId.length)) { - id.subSequence( - appId.length + ":id/".length, - id.length - ) - } else { - null - } - } + "vid" -> node.getVid() "name" -> node.className "text" -> node.text @@ -189,11 +192,194 @@ private val getAttr: (AccessibilityNodeInfo, String) -> Any? = { node, name -> } } -val abTransform = Transform( +data class CacheTransform( + val transform: Transform, + val indexCache: HashMap, +) + +fun createCacheTransform(): CacheTransform { + val indexCache = HashMap() + fun AccessibilityNodeInfo.getChildX(index: Int): AccessibilityNodeInfo? { + return getChild(index)?.also { child -> + indexCache[child] = index + } + } + + fun AccessibilityNodeInfo.getIndexX(): Int { + indexCache[this]?.let { return it } + parent?.forEachIndexed { index, child -> + if (child != null) { + indexCache[child] = index + } + if (child == this) { + return index + } + } + return 0 + } + + val getChildrenCache: (AccessibilityNodeInfo) -> Sequence = { node -> + sequence { + repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { index -> + val child = node.getChildX(index) ?: return@sequence + yield(child) + } + } + } + val transform = Transform( + getAttr = { node, name -> + if (name == "index") { + node.getIndexX() + } else { + getAttr(node, name) + } + }, + getName = { node -> node.className }, + getChildren = getChildrenCache, + getParent = { node -> node.parent }, + getDescendants = { node -> + sequence { + val stack = getChildrenCache(node).toMutableList() + if (stack.isEmpty()) return@sequence + stack.reverse() + val tempNodes = mutableListOf() + do { + val top = stack.removeLast() + yield(top) + for (childNode in getChildrenCache(top)) { + tempNodes.add(childNode) + } + if (tempNodes.isNotEmpty()) { + for (i in tempNodes.size - 1 downTo 0) { + stack.add(tempNodes[i]) + } + tempNodes.clear() + } + } while (stack.isNotEmpty()) + }.take(MAX_DESCENDANTS_SIZE) + }, + getChildrenX = { node, connectExpression -> + sequence { + repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { offset -> + connectExpression.maxOffset?.let { maxOffset -> + if (offset > maxOffset) return@sequence + } + if (connectExpression.checkOffset(offset)) { + val child = node.getChildX(offset) ?: return@sequence + yield(child) + } + } + } + }, + getBeforeBrothers = { node, connectExpression -> + sequence { + val parentVal = node.parent ?: return@sequence + val index = indexCache[node] // 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 indexCache 是空 + if (index != null) { + var i = index - 1 + var offset = 0 + while (0 <= i && i < parentVal.childCount) { + connectExpression.maxOffset?.let { maxOffset -> + if (offset > maxOffset) return@sequence + } + if (connectExpression.checkOffset(offset)) { + val child = parentVal.getChild(i) ?: return@sequence + yield(child) + } + i-- + offset++ + } + } else { + val list = getChildrenCache(parentVal).takeWhile { it != node }.toMutableList() + list.reverse() + yieldAll(list.filterIndexed { i, _ -> + connectExpression.checkOffset( + i + ) + }) + } + } + }, + getAfterBrothers = { node, connectExpression -> + val parentVal = node.parent + if (parentVal != null) { + val index = indexCache[node] + if (index != null) { + sequence { + var i = index + 1 + var offset = 0 + while (0 <= i && i < parentVal.childCount) { + connectExpression.maxOffset?.let { maxOffset -> + if (offset > maxOffset) return@sequence + } + if (connectExpression.checkOffset(offset)) { + val child = parentVal.getChild(i) ?: return@sequence + yield(child) + } + i-- + offset++ + } + } + } else { + getChildrenCache(parentVal).dropWhile { it != node } + .drop(1) + .let { + if (connectExpression.maxOffset != null) { + it.take(connectExpression.maxOffset!! + 1) + } else { + it + } + } + .filterIndexed { i, _ -> + connectExpression.checkOffset( + i + ) + } + } + } else { + emptySequence() + } + }, + getDescendantsX = { node, connectExpression -> + sequence { + val stack = getChildrenCache(node).toMutableList() + if (stack.isEmpty()) return@sequence + stack.reverse() + val tempNodes = mutableListOf() + var offset = 0 + do { + val top = stack.removeLast() + if (connectExpression.checkOffset(offset)) { + yield(top) + } + offset++ + connectExpression.maxOffset?.let { maxOffset -> + if (offset > maxOffset) return@sequence + } + for (childNode in getChildrenCache(top)) { + tempNodes.add(childNode) + } + if (tempNodes.isNotEmpty()) { + for (i in tempNodes.size - 1 downTo 0) { + stack.add(tempNodes[i]) + } + tempNodes.clear() + } + } while (stack.isNotEmpty()) + } + }, + ) + + return CacheTransform(transform, indexCache) +} + +val defaultCacheTransform = createCacheTransform() + +// no cache +val defaultTransform = Transform( getAttr = getAttr, getName = { node -> node.className }, getChildren = getChildren, - getChild = { node, index -> if (index in 0.. node.parent }, getDescendants = { node -> sequence { @@ -201,9 +387,14 @@ val abTransform = Transform( if (stack.isEmpty()) return@sequence stack.reverse() val tempNodes = mutableListOf() + var offset = 0 do { val top = stack.removeLast() yield(top) + offset++ + if (offset > MAX_DESCENDANTS_SIZE) { + return@sequence + } for (childNode in getChildren(top)) { tempNodes.add(childNode) } @@ -214,6 +405,19 @@ val abTransform = Transform( tempNodes.clear() } } while (stack.isNotEmpty()) - }.take(MAX_DESCENDANTS_SIZE) - } + } + }, + getChildrenX = { node, connectExpression -> + sequence { + repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { offset -> + connectExpression.maxOffset?.let { maxOffset -> + if (offset > maxOffset) return@sequence + } + if (connectExpression.checkOffset(offset)) { + val child = node.getChild(offset) ?: return@sequence + yield(child) + } + } + } + }, ) diff --git a/app/src/main/kotlin/li/songe/gkd/service/GkdAbService.kt b/app/src/main/kotlin/li/songe/gkd/service/GkdAbService.kt index 21b35588d..39823e226 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/GkdAbService.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/GkdAbService.kt @@ -148,6 +148,7 @@ class GkdAbService : CompositionAbService({ } else { queryThread } + val common = ctx === queryThread queryTaskJob = scope.launchTry(ctx) { val activityRule = getAndUpdateCurrentRules() for (rule in (activityRule.currentRules)) { @@ -180,7 +181,14 @@ class GkdAbService : CompositionAbService({ return@launchTry } if (!matchApp) continue - val target = rule.query(nodeVal) ?: continue + val target = + rule.query(nodeVal, if (common) defaultCacheTransform else null) + if (common) { + defaultCacheTransform.indexCache.clear() + } + if (target == null) { + continue + } if (activityRule !== getAndUpdateCurrentRules()) break if (rule.checkDelay() && rule.actionDelayJob == null) { rule.actionDelayJob = scope.launch(queryThread) { @@ -521,8 +529,11 @@ class GkdAbService : CompositionAbService({ } } val targetNode = - serviceVal.safeActiveWindow?.querySelector(selector, gkdAction.quickFind) - ?: throw RpcError("没有查询到节点") + serviceVal.safeActiveWindow?.querySelector( + selector, + gkdAction.quickFind, + if (selector.canCacheIndex) createCacheTransform().transform else null + ) ?: throw RpcError("没有查询到节点") if (gkdAction.action == null) { // 仅查询 diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AppItemPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/AppItemPage.kt index c84d61a31..c3f184f25 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/AppItemPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/AppItemPage.kt @@ -371,6 +371,7 @@ fun AppItemPage( TextButton(onClick = { if (oldSource == source) { toast("规则无变动") + setEditGroupRaw(null) return@TextButton } val newGroupRaw = try { @@ -446,6 +447,7 @@ fun AppItemPage( TextButton(onClick = { if (oldSource == source) { toast("禁用项无变动") + setExcludeGroupRaw(null) return@TextButton } setExcludeGroupRaw(null) diff --git a/app/src/main/kotlin/li/songe/gkd/ui/GlobalRuleExcludePage.kt b/app/src/main/kotlin/li/songe/gkd/ui/GlobalRuleExcludePage.kt index 97494d680..e2a38d174 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/GlobalRuleExcludePage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/GlobalRuleExcludePage.kt @@ -346,6 +346,7 @@ fun GlobalRuleExcludePage(subsItemId: Long, groupKey: Int) { TextButton(onClick = { if (oldSource == source) { toast("禁用项无变动") + showEditDlg = false return@TextButton } showEditDlg = false diff --git a/app/src/main/kotlin/li/songe/gkd/ui/GlobalRulePage.kt b/app/src/main/kotlin/li/songe/gkd/ui/GlobalRulePage.kt index ac775713d..02dcf9775 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/GlobalRulePage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/GlobalRulePage.kt @@ -343,6 +343,7 @@ fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) { confirmButton = { TextButton(onClick = { if (oldSource == source) { + setEditGroupRaw(null) toast("规则无变动") return@TextButton } diff --git a/app/src/main/kotlin/li/songe/gkd/ui/home/HomePage.kt b/app/src/main/kotlin/li/songe/gkd/ui/home/HomePage.kt index 6a136df44..c81797080 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/home/HomePage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/home/HomePage.kt @@ -1,7 +1,6 @@ package li.songe.gkd.ui.home -import android.app.Activity -import android.content.Intent +import android.webkit.URLUtil import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem @@ -18,6 +17,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.blankj.utilcode.util.LogUtils import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph +import li.songe.gkd.MainActivity import li.songe.gkd.util.ProfileTransitions data class BottomNavItem( @@ -29,13 +29,19 @@ data class BottomNavItem( @Destination(style = ProfileTransitions::class) @Composable fun HomePage() { + val context = LocalContext.current as MainActivity val vm = hiltViewModel() val tab by vm.tabFlow.collectAsState() - val intent: Intent? = (LocalContext.current as Activity).intent + val intent = context.intent LaunchedEffect(key1 = intent, block = { if (intent != null) { - LogUtils.d(intent) + context.intent = null + val data = intent.data + val url = data?.getQueryParameter("url") + if (data?.scheme == "gkd" && data.host == "import" && URLUtil.isNetworkUrl(url)) { + LogUtils.d(data, url) + } } }) diff --git a/selector/build.gradle.kts b/selector/build.gradle.kts index 3eb8a4930..563b4dad5 100644 --- a/selector/build.gradle.kts +++ b/selector/build.gradle.kts @@ -16,6 +16,11 @@ kotlin { generateTypeScriptDefinitions() browser {} } + sourceSets { + all { + languageSettings.optIn("kotlin.js.ExperimentalJsExport") + } + } sourceSets["commonMain"].dependencies { implementation(libs.kotlin.stdlib.common) } diff --git a/selector/src/commonMain/kotlin/li/songe/selector/ExtSyntaxError.kt b/selector/src/commonMain/kotlin/li/songe/selector/ExtSyntaxError.kt deleted file mode 100644 index 632c0af3e..000000000 --- a/selector/src/commonMain/kotlin/li/songe/selector/ExtSyntaxError.kt +++ /dev/null @@ -1,30 +0,0 @@ -package li.songe.selector - -import kotlin.js.ExperimentalJsExport -import kotlin.js.JsExport - -@OptIn(ExperimentalJsExport::class) -@JsExport -data class ExtSyntaxError internal constructor( - val expectedValue: String, - val position: Int, - val source: String, -) : Exception( - "expected $expectedValue in selector at position $position, but got ${ - source.getOrNull( - position - ) - }" -) { - internal companion object { - fun assert(source: String, offset: Int, value: String = "", expectedValue: String? = null) { - if (offset >= source.length || (value.isNotEmpty() && !value.contains(source[offset]))) { - throw ExtSyntaxError(expectedValue ?: value, offset, source) - } - } - - fun throwError(source: String, offset: Int, expectedValue: String = ""): Nothing { - throw ExtSyntaxError(expectedValue, offset, source) - } - } -} \ No newline at end of file diff --git a/selector/src/commonMain/kotlin/li/songe/selector/GkdSyntaxError.kt b/selector/src/commonMain/kotlin/li/songe/selector/GkdSyntaxError.kt new file mode 100644 index 000000000..740f159dd --- /dev/null +++ b/selector/src/commonMain/kotlin/li/songe/selector/GkdSyntaxError.kt @@ -0,0 +1,31 @@ +package li.songe.selector + +import kotlin.js.JsExport + +@JsExport +data class GkdSyntaxError internal constructor( + val expectedValue: String, + val position: Int, + val source: String, +) : Exception( + "expected $expectedValue in selector at position $position, but got ${ + source.getOrNull( + position + ) + }" +) + +internal fun gkdAssert( + source: String, + offset: Int, + value: String = "", + expectedValue: String? = null +) { + if (offset >= source.length || (value.isNotEmpty() && !value.contains(source[offset]))) { + throw GkdSyntaxError(expectedValue ?: value, offset, source) + } +} + +internal fun gkdError(source: String, offset: Int, expectedValue: String = ""): Nothing { + throw GkdSyntaxError(expectedValue, offset, source) +} \ No newline at end of file diff --git a/selector/src/commonMain/kotlin/li/songe/selector/CommonSelector.kt b/selector/src/commonMain/kotlin/li/songe/selector/MultiplatformSelector.kt similarity index 63% rename from selector/src/commonMain/kotlin/li/songe/selector/CommonSelector.kt rename to selector/src/commonMain/kotlin/li/songe/selector/MultiplatformSelector.kt index 5742cde11..cf84e33f6 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/CommonSelector.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/MultiplatformSelector.kt @@ -1,12 +1,10 @@ package li.songe.selector -import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport - -@OptIn(ExperimentalJsExport::class) @JsExport -class CommonSelector private constructor( +@Suppress("UNUSED") +class MultiplatformSelector private constructor( internal val selector: Selector, ) { val tracks = selector.tracks @@ -19,21 +17,24 @@ class CommonSelector private constructor( val qfTextValue = selector.qfTextValue val canQf = selector.canQf val isMatchRoot = selector.isMatchRoot + fun checkType(getType: (String) -> String): Boolean { + return selector.checkType(getType) + } - fun match(node: T, transform: CommonTransform): T? { + fun match(node: T, transform: MultiplatformTransform): T? { return selector.match(node, transform.transform) } @Suppress("UNCHECKED_CAST") - fun matchTrack(node: T, transform: CommonTransform): Array? { + fun matchTrack(node: T, transform: MultiplatformTransform): Array? { return selector.matchTracks(node, transform.transform)?.toTypedArray() as Array? } override fun toString() = selector.toString() companion object { - fun parse(source: String) = CommonSelector(Selector.parse(source)) - fun parseOrNull(source: String) = Selector.parseOrNull(source)?.let(::CommonSelector) + fun parse(source: String) = MultiplatformSelector(Selector.parse(source)) + fun parseOrNull(source: String) = Selector.parseOrNull(source)?.let(::MultiplatformSelector) } } diff --git a/selector/src/commonMain/kotlin/li/songe/selector/CommonTransform.kt b/selector/src/commonMain/kotlin/li/songe/selector/MultiplatformTransform.kt similarity index 62% rename from selector/src/commonMain/kotlin/li/songe/selector/CommonTransform.kt rename to selector/src/commonMain/kotlin/li/songe/selector/MultiplatformTransform.kt index 18acdf297..ce4a74ca4 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/CommonTransform.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/MultiplatformTransform.kt @@ -1,11 +1,10 @@ package li.songe.selector -import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport -@OptIn(ExperimentalJsExport::class) @JsExport -class CommonTransform( +@Suppress("UNCHECKED_CAST", "UNUSED") +class MultiplatformTransform( getAttr: (T, String) -> Any?, getName: (T) -> String?, getChildren: (T) -> Array, @@ -18,28 +17,23 @@ class CommonTransform( getParent = getParent, ) - @Suppress("UNCHECKED_CAST", "UNUSED") - val querySelectorAll: (T, CommonSelector) -> Array = { node, selector -> + val querySelectorAll: (T, MultiplatformSelector) -> Array = { node, selector -> val result = transform.querySelectorAll(node, selector.selector).toList().toTypedArray() result as Array } - @Suppress("UNUSED") - val querySelector: (T, CommonSelector) -> T? = { node, selector -> + val querySelector: (T, MultiplatformSelector) -> T? = { node, selector -> transform.querySelectorAll(node, selector.selector).firstOrNull() } - - @Suppress("UNCHECKED_CAST", "UNUSED") - val querySelectorTrackAll: (T, CommonSelector) -> Array> = { node, selector -> + val querySelectorTrackAll: (T, MultiplatformSelector) -> Array> = { node, selector -> val result = transform.querySelectorTrackAll(node, selector.selector) .map { it.toTypedArray() as Array }.toList().toTypedArray() result as Array> } - @Suppress("UNCHECKED_CAST", "UNUSED") - val querySelectorTrack: (T, CommonSelector) -> Array? = { node, selector -> + val querySelectorTrack: (T, MultiplatformSelector) -> Array? = { node, selector -> transform.querySelectorTrackAll(node, selector.selector).firstOrNull() ?.toTypedArray() as Array? } diff --git a/selector/src/commonMain/kotlin/li/songe/selector/NodeFc.kt b/selector/src/commonMain/kotlin/li/songe/selector/NodeFc.kt deleted file mode 100644 index 7b559b6f6..000000000 --- a/selector/src/commonMain/kotlin/li/songe/selector/NodeFc.kt +++ /dev/null @@ -1,17 +0,0 @@ -package li.songe.selector - -internal interface NodeMatchFc { - operator fun invoke(node: T, transform: Transform): Boolean -} - -interface NodeSequenceFc { - operator fun invoke(sq: Sequence): Sequence -} - -internal val emptyNodeSequence = object : NodeSequenceFc { - override fun invoke(sq: Sequence) = emptySequence() -} - -internal interface NodeTraversalFc { - operator fun invoke(node: T, transform: Transform): Sequence -} diff --git a/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt b/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt index dcaecb042..d1f128e60 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/Selector.kt @@ -2,54 +2,32 @@ package li.songe.selector import li.songe.selector.data.BinaryExpression 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) { override fun toString(): String { return propertyWrapper.toString() } - val tracks by lazy { + val tracks = run { val list = mutableListOf(propertyWrapper) while (true) { list.add(list.last().to?.to ?: break) } - list.map { p -> p.propertySegment.tracked }.toTypedArray() + list.map { p -> p.propertySegment.tracked }.toTypedArray() } val trackIndex = tracks.indexOfFirst { it }.let { i -> if (i < 0) 0 else i } - val connectKeys by lazy { - var c = propertyWrapper.to - val keys = mutableListOf() - while (c != null) { - c?.apply { - keys.add(connectSegment.operator.key) - } - c = c?.to?.to - } - keys.toTypedArray() - } - - val propertyNames by lazy { - var p: PropertyWrapper? = propertyWrapper - val names = mutableSetOf() - while (p != null) { - val s = p!!.propertySegment - p = p!!.to?.to - names.addAll(s.propertyNames) - } - names.distinct().toTypedArray() - } - fun match( node: T, transform: Transform, - trackNodes: MutableList = mutableListOf(), + trackNodes: MutableList = ArrayList(tracks.size), ): T? { val trackTempNodes = matchTracks(node, transform, trackNodes) ?: return null return trackTempNodes[trackIndex] @@ -58,30 +36,30 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper fun matchTracks( node: T, transform: Transform, - trackNodes: MutableList = mutableListOf(), + trackNodes: MutableList = ArrayList(tracks.size), ): List? { return propertyWrapper.matchTracks(node, transform, trackNodes) } val qfIdValue = propertyWrapper.propertySegment.expressions.firstOrNull().let { e -> - if (e is BinaryExpression && e.name == "id" && e.operator == CompareOperator.Equal && e.value is String) { - e.value + if (e is BinaryExpression && e.name == "id" && e.operator == CompareOperator.Equal && e.value is PrimitiveValue.StringValue) { + e.value.value } else { null } } val qfVidValue = propertyWrapper.propertySegment.expressions.firstOrNull().let { e -> - if (e is BinaryExpression && e.name == "vid" && e.operator == CompareOperator.Equal && e.value is String) { - e.value + if (e is BinaryExpression && e.name == "vid" && e.operator == CompareOperator.Equal && e.value is PrimitiveValue.StringValue) { + e.value.value } else { null } } val qfTextValue = propertyWrapper.propertySegment.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 String) { - e.value + 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) { + e.value.value } else { null } @@ -91,9 +69,58 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper // 主动查询 val isMatchRoot = propertyWrapper.propertySegment.expressions.firstOrNull().let { e -> - e is BinaryExpression && e.name == "depth" && e.operator == CompareOperator.Equal && e.value == 0 + e is BinaryExpression && e.name == "depth" && e.operator == CompareOperator.Equal && e.value.value == 0 } + val connectKeys = run { + var c = propertyWrapper.to + val keys = mutableListOf() + while (c != null) { + c.apply { + keys.add(connectSegment.operator.key) + } + c = c.to.to + } + keys.toTypedArray() + } + + private val binaryExpressions = run { + var p: PropertyWrapper? = propertyWrapper + val names = mutableListOf() + while (p != null) { + val s = p.propertySegment + names.addAll(s.binaryExpressions) + p = p.to?.to + } + names.distinct().toTypedArray() + } + + val propertyNames = run { + binaryExpressions.map { e -> e.name }.distinct().toTypedArray() + } + + fun checkType(getType: (String) -> String): Boolean { + binaryExpressions.forEach { e -> + if (e.value.value != null) { + val type = getType(e.name) + if (!(when (type) { + "boolean" -> e.value is PrimitiveValue.BooleanValue + "int" -> e.value is PrimitiveValue.IntValue + "string" -> e.value is PrimitiveValue.StringValue + else -> false + }) + ) return false + } + } + return false + } + + + val canCacheIndex = + connectKeys.contains(ConnectOperator.BeforeBrother.key) || connectKeys.contains( + ConnectOperator.AfterBrother.key + ) || propertyNames.contains("index") + companion object { fun parse(source: String) = ParserSet.selectorParser(source) fun parseOrNull(source: String) = try { diff --git a/selector/src/commonMain/kotlin/li/songe/selector/Transform.kt b/selector/src/commonMain/kotlin/li/songe/selector/Transform.kt index 8776539ab..1789fc569 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/Transform.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/Transform.kt @@ -1,66 +1,127 @@ package li.songe.selector +import li.songe.selector.data.ConnectExpression +@Suppress("UNUSED") class Transform( val getAttr: (T, String) -> Any?, val getName: (T) -> CharSequence?, val getChildren: (T) -> Sequence, - val getChild: (T, Int) -> T? = { node, offset -> getChildren(node).elementAtOrNull(offset) }, val getParent: (T) -> T?, - val getAncestors: (T) -> Sequence = { node -> + + val getDescendants: (T) -> Sequence = { node -> + sequence { // 深度优先 先序遍历 + // https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelector + val stack = getChildren(node).toMutableList() + if (stack.isEmpty()) return@sequence + stack.reverse() + val tempNodes = mutableListOf() + do { + val top = stack.removeLast() + yield(top) + for (childNode in getChildren(top)) { + // 可针对 sequence 优化 + tempNodes.add(childNode) + } + if (tempNodes.isNotEmpty()) { + for (i in tempNodes.size - 1 downTo 0) { + stack.add(tempNodes[i]) + } + tempNodes.clear() + } + } while (stack.isNotEmpty()) + } + }, + + val getChildrenX: (T, ConnectExpression) -> Sequence = { node, connectExpression -> + getChildren(node) + .let { + if (connectExpression.maxOffset != null) { + it.take(connectExpression.maxOffset!! + 1) + } else { + it + } + } + .filterIndexed { i, _ -> + connectExpression.checkOffset( + i + ) + } + }, + val getAncestors: (T, ConnectExpression) -> Sequence = { node, connectExpression -> sequence { var parentVar: T? = getParent(node) ?: return@sequence + var offset = 0 while (parentVar != null) { parentVar?.let { - yield(it) + if (connectExpression.checkOffset(offset)) { + yield(it) + } + offset++ + connectExpression.maxOffset?.let { maxOffset -> + if (offset > maxOffset) { + return@sequence + } + } parentVar = getParent(it) } } } }, - val getAncestor: (T, Int) -> T? = { node, offset -> getAncestors(node).elementAtOrNull(offset) }, - - val getBeforeBrothers: (T) -> Sequence = { node -> - sequence { - val parentVal = getParent(node) ?: return@sequence + val getBeforeBrothers: (T, ConnectExpression) -> Sequence = { node, connectExpression -> + val parentVal = getParent(node) + if (parentVal != null) { val list = getChildren(parentVal).takeWhile { it != node }.toMutableList() list.reverse() - yieldAll(list) + list.asSequence().filterIndexed { i, _ -> + connectExpression.checkOffset( + i + ) + } + } else { + emptySequence() } }, - val getBeforeBrother: (T, Int) -> T? = { node, offset -> - getBeforeBrothers(node).elementAtOrNull( - offset - ) - }, - - val getAfterBrothers: (T) -> Sequence = { node -> - sequence { - val parentVal = getParent(node) ?: return@sequence - yieldAll(getChildren(parentVal).dropWhile { it != node }.drop(1)) + val getAfterBrothers: (T, ConnectExpression) -> Sequence = { node, connectExpression -> + val parentVal = getParent(node) + if (parentVal != null) { + getChildren(parentVal).dropWhile { it != node } + .drop(1) + .let { + if (connectExpression.maxOffset != null) { + it.take(connectExpression.maxOffset!! + 1) + } else { + it + } + }.filterIndexed { i, _ -> + connectExpression.checkOffset( + i + ) + } + } else { + emptySequence() } }, - val getAfterBrother: (T, Int) -> T? = { node, offset -> - getAfterBrothers(node).elementAtOrNull( - offset - ) - }, - /** - * 遍历下面所有子孙节点,不包含自己 - */ - val getDescendants: (T) -> Sequence = { node -> - sequence { // 深度优先 先序遍历 - // https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelector + val getDescendantsX: (T, ConnectExpression) -> Sequence = { node, connectExpression -> + sequence { val stack = getChildren(node).toMutableList() if (stack.isEmpty()) return@sequence stack.reverse() val tempNodes = mutableListOf() + var offset = 0 do { val top = stack.removeLast() - yield(top) + if (connectExpression.checkOffset(offset)) { + yield(top) + } + offset++ + connectExpression.maxOffset?.let { maxOffset -> + if (offset > maxOffset) { + return@sequence + } + } for (childNode in getChildren(top)) { - // 可针对 sequence 优化 tempNodes.add(childNode) } if (tempNodes.isNotEmpty()) { @@ -74,11 +135,10 @@ class Transform( }, ) { - val querySelectorAll: (T, Selector) -> Sequence = { node, selector -> sequence { // cache trackNodes - val trackNodes: MutableList = mutableListOf() + val trackNodes = ArrayList(selector.tracks.size) val r0 = selector.match(node, this@Transform, trackNodes) if (r0 != null) yield(r0) getDescendants(node).forEach { childNode -> @@ -104,7 +164,6 @@ class Transform( } } - @Suppress("UNUSED") val querySelectorTrack: (T, Selector) -> List? = { node, selector -> querySelectorTrackAll( node, selector diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/BinaryExpression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/BinaryExpression.kt index 657f4e079..9a969ce6e 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/BinaryExpression.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/BinaryExpression.kt @@ -2,50 +2,16 @@ package li.songe.selector.data import li.songe.selector.Transform -data class BinaryExpression(val name: String, val operator: CompareOperator, val value: Any?) : +data class BinaryExpression( + val name: String, + val operator: CompareOperator, + val value: PrimitiveValue +) : Expression() { override fun match(node: T, transform: Transform) = - operator.compare(transform.getAttr(node, name), value) + operator.compare(transform.getAttr(node, name), value.value) - override val propertyNames = listOf(name) + override val binaryExpressions = listOf(this) - override fun toString() = "${name}${operator}${ - if (value is String) { - val wrapChar = '"' - val sb = StringBuilder() - 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 10..0x1f -> { - sb.append("\\x" + c.code.toString(16)) - } - - else -> { - sb.append(c) - } - } - } - } - sb.append(wrapChar) - sb.toString() - } else { - value - } - }" + override fun toString() = "${name}${operator.key}${value}" } diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/CompareOperator.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/CompareOperator.kt index 528687835..4d917af2a 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/CompareOperator.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/CompareOperator.kt @@ -1,8 +1,8 @@ package li.songe.selector.data sealed class CompareOperator(val key: String) { - override fun toString() = key abstract fun compare(left: Any?, right: Any?): Boolean + abstract fun allowType(type: PrimitiveValue): Boolean companion object { // https://stackoverflow.com/questions/47648689 @@ -35,70 +35,96 @@ sealed class CompareOperator(val key: String) { left == right } } + + override fun allowType(type: PrimitiveValue) = true } data object NotEqual : CompareOperator("!=") { override fun compare(left: Any?, right: Any?) = !Equal.compare(left, right) + override fun allowType(type: PrimitiveValue) = true } data object Start : CompareOperator("^=") { override fun compare(left: Any?, right: Any?): Boolean { return if (left is CharSequence && right is CharSequence) left.startsWith(right) else false } + + override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue } data object NotStart : CompareOperator("!^=") { override fun compare(left: Any?, right: Any?): Boolean { return if (left is CharSequence && right is CharSequence) !left.startsWith(right) else false } + + override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue } data object Include : CompareOperator("*=") { override fun compare(left: Any?, right: Any?): Boolean { return if (left is CharSequence && right is CharSequence) left.contains(right) else false } + + override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue } data object NotInclude : CompareOperator("!*=") { override fun compare(left: Any?, right: Any?): Boolean { return if (left is CharSequence && right is CharSequence) !left.contains(right) else false } + + override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue } data object End : CompareOperator("$=") { override fun compare(left: Any?, right: Any?): Boolean { return if (left is CharSequence && right is CharSequence) left.endsWith(right) else false } + + override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue } data object NotEnd : CompareOperator("!$=") { override fun compare(left: Any?, right: Any?): Boolean { return if (left is CharSequence && right is CharSequence) !left.endsWith(right) else false } + + override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue } data object Less : CompareOperator("<") { override fun compare(left: Any?, right: Any?): Boolean { return if (left is Int && right is Int) left < right else false } + + + override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue } data object LessEqual : CompareOperator("<=") { override fun compare(left: Any?, right: Any?): Boolean { return if (left is Int && right is Int) left <= right else false } + + override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue } data object More : CompareOperator(">") { override fun compare(left: Any?, right: Any?): Boolean { return if (left is Int && right is Int) left > right else false } + + + override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue } data object MoreEqual : CompareOperator(">=") { override fun compare(left: Any?, right: Any?): Boolean { return if (left is Int && right is Int) left >= right else false } + + + override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue } } \ No newline at end of file diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectExpression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectExpression.kt index 46f2669f5..c71321034 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectExpression.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectExpression.kt @@ -1,10 +1,8 @@ package li.songe.selector.data -import li.songe.selector.NodeSequenceFc - sealed class ConnectExpression { - abstract val isConstant: Boolean abstract val minOffset: Int - - internal abstract val traversal: NodeSequenceFc + abstract val maxOffset: Int? + abstract fun checkOffset(offset: Int): Boolean + abstract fun getOffset(i: Int): Int } diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectOperator.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectOperator.kt index f280d1100..be298d833 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectOperator.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectOperator.kt @@ -3,9 +3,9 @@ package li.songe.selector.data import li.songe.selector.Transform sealed class ConnectOperator(val key: String) { - override fun toString() = key - abstract fun traversal(node: T, transform: Transform): Sequence - abstract fun traversal(node: T, transform: Transform, offset: Int): T? + abstract fun traversal( + node: T, transform: Transform, connectExpression: ConnectExpression + ): Sequence companion object { // https://stackoverflow.com/questions/47648689 @@ -20,54 +20,47 @@ sealed class ConnectOperator(val key: String) { * A + B, 1,2,3,A,B,7,8 */ data object BeforeBrother : ConnectOperator("+") { - override fun traversal(node: T, transform: Transform) = - transform.getBeforeBrothers(node) + override fun traversal( + node: T, transform: Transform, connectExpression: ConnectExpression + ) = transform.getBeforeBrothers(node, connectExpression) - override fun traversal(node: T, transform: Transform, offset: Int): T? = - transform.getBeforeBrother(node, offset) } /** * A - B, 1,2,3,B,A,7,8 */ data object AfterBrother : ConnectOperator("-") { - override fun traversal(node: T, transform: Transform) = - transform.getAfterBrothers(node) - - override fun traversal(node: T, transform: Transform, offset: Int): T? = - transform.getAfterBrother(node, offset) + override fun traversal( + node: T, transform: Transform, connectExpression: ConnectExpression + ) = transform.getAfterBrothers(node, connectExpression) } /** * A > B, A is the ancestor of B */ data object Ancestor : ConnectOperator(">") { - override fun traversal(node: T, transform: Transform) = transform.getAncestors(node) + override fun traversal( + node: T, transform: Transform, connectExpression: ConnectExpression + ) = transform.getAncestors(node, connectExpression) - override fun traversal(node: T, transform: Transform, offset: Int): T? = - transform.getAncestor(node, offset) } /** * A < B, A is the child of B */ data object Child : ConnectOperator("<") { - override fun traversal(node: T, transform: Transform) = transform.getChildren(node) - - override fun traversal(node: T, transform: Transform, offset: Int): T? = - transform.getChild(node, offset) + override fun traversal( + node: T, transform: Transform, connectExpression: ConnectExpression + ) = transform.getChildrenX(node, connectExpression) } /** * A << B, A is the descendant of B */ data object Descendant : ConnectOperator("<<") { - override fun traversal(node: T, transform: Transform) = - transform.getDescendants(node) - - override fun traversal(node: T, transform: Transform, offset: Int): T? = - transform.getDescendants(node).elementAtOrNull(offset) + override fun traversal( + node: T, transform: Transform, connectExpression: ConnectExpression + ) = transform.getDescendantsX(node, connectExpression) } - } diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectSegment.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectSegment.kt index cb879ebba..ac2514adb 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectSegment.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectSegment.kt @@ -1,7 +1,6 @@ package li.songe.selector.data import li.songe.selector.Transform -import li.songe.selector.NodeTraversalFc data class ConnectSegment( val operator: ConnectOperator = ConnectOperator.Ancestor, @@ -11,26 +10,10 @@ data class ConnectSegment( if (operator == ConnectOperator.Ancestor && connectExpression is PolynomialExpression && connectExpression.a == 1 && connectExpression.b == 0) { return "" } - return operator.toString() + connectExpression.toString() + return operator.key + connectExpression.toString() } - internal val traversal = if (connectExpression.isConstant) { - object : NodeTraversalFc { - override fun invoke(node: T, transform: Transform): Sequence = sequence { - val node1 = operator.traversal(node, transform, connectExpression.minOffset) - if (node1 != null) { - yield(node1) - } - } - } - } else { - object : NodeTraversalFc { - override fun invoke(node: T, transform: Transform): Sequence { - return connectExpression.traversal( - operator.traversal(node, transform) - ) - } - } + fun traversal(node: T, transform: Transform): Sequence { + return operator.traversal(node, transform, connectExpression) } - } diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectWrapper.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectWrapper.kt index 2bfea5444..16f8d9345 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectWrapper.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/ConnectWrapper.kt @@ -12,7 +12,7 @@ data class ConnectWrapper( fun matchTracks( node: T, transform: Transform, - trackNodes: MutableList = mutableListOf(), + trackNodes: MutableList, ): List? { connectSegment.traversal(node, transform).forEach { if (it == null) return@forEach diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/Expression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/Expression.kt index 981ee4d20..2562eb6de 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/Expression.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/Expression.kt @@ -5,5 +5,5 @@ import li.songe.selector.Transform sealed class Expression { abstract fun match(node: T, transform: Transform): Boolean - abstract val propertyNames: List + abstract val binaryExpressions: List } diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalExpression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalExpression.kt index 769c68803..e2cd3fd6e 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalExpression.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalExpression.kt @@ -11,7 +11,7 @@ data class LogicalExpression( return operator.compare(node, transform, left, right) } - override val propertyNames = left.propertyNames + right.propertyNames + override val binaryExpressions = left.binaryExpressions + right.binaryExpressions override fun toString(): String { val leftStr = if (left is LogicalExpression && left.operator != operator) { @@ -24,6 +24,6 @@ data class LogicalExpression( } else { right.toString() } - return "$leftStr\u0020$operator\u0020$rightStr" + return "$leftStr\u0020${operator.key}\u0020$rightStr" } } \ No newline at end of file diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalOperator.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalOperator.kt index 732f0200c..b671c1bb2 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalOperator.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/LogicalOperator.kt @@ -12,7 +12,6 @@ sealed class LogicalOperator(val key: String) { } } - override fun toString() = key abstract fun compare( node: T, transform: Transform, diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/PolynomialExpression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/PolynomialExpression.kt index 7facfeafb..805ace166 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/PolynomialExpression.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/PolynomialExpression.kt @@ -1,7 +1,5 @@ package li.songe.selector.data -import li.songe.selector.NodeSequenceFc - /** * an+b */ @@ -30,50 +28,112 @@ data class PolynomialExpression(val a: Int = 0, val b: Int = 1) : ConnectExpress return "(${a}n${bOp}${b})" } - val numbers = if (a < 0) { - if (b < 0) { - emptyList() - } else if (b > 0) { + private fun invalidValue(): Nothing { + error("invalid PolynomialExpression: a=$a, b=$b") + } + + override val minOffset = if (a > 0) { + if (b > 0) { + a + b + } else if (b == 0) { + a + } else { + // 2n-10 -> n>=6 + // 3n-10 -> n>=4 + // 3n-3 -> n>=2 + // 3n-1 -> n>=1 + // an+b>0 -> n>-b/a + val minN = -b / a + 1 + a * minN + b + } + } else if (a == 0) { + if (b > 0) { + b + } else { + invalidValue() + } + } else { + if (b > 0) { if (b <= -a) { - emptyList() + invalidValue() } else { - val list = mutableListOf() - var n = 1 - while (a * n + b > 0) { - list.add(a * n + b) - n++ - } - list.sorted() + // -2n+9 -> (1_7,2_5,3_3,4_1) -> (1,3,5,7) -> 1 + // -3n+9 -> (1_6,2_3) -> (3,6) + // -5n+7 -> (1_2) -> (2) + val maxN = -b / a - if (b % a == 0) 1 else 0 + a * maxN + b } } else { - emptyList() + invalidValue() + } + } - 1 + + override val maxOffset = if (a > 0) { + null + } else if (a == 0) { + if (b > 0) { + b + } else { + invalidValue() } - } else if (a > 0) { - // infinite - emptyList() } else { - if (b < 0) { - emptyList() - } else if (b > 0) { - listOf(b) + if (b > 0) { + if (b <= -a) { + invalidValue() + } else { + a + b + } } else { - emptyList() + invalidValue() } - } + } - 1 + + private val isConstant = minOffset == maxOffset - override val isConstant = numbers.size == 1 - override val minOffset = (numbers.firstOrNull() ?: 1) - 1 - private val b1 = b - 1 - private val maxAb = a + b // when a<=0 + // (2n-1) -> (1,3,5) -> [0,2,4] + override fun checkOffset(offset: Int): Boolean { + if (isConstant) { + return offset == minOffset + } + val y = (offset + 1) - b + return y % a == 0 && y / a >= 1 + } - override val traversal = object : NodeSequenceFc { - override fun invoke(sq: Sequence): Sequence { - return (if (a > 0) { - sq + private val innerGetOffset: (Int) -> Int = if (a > 0) { + if (b > 0) { + { i -> a * i + b } + } else if (b == 0) { + { i -> a * i + b } + } else { + val minN = -b / a + 1 + { i -> a * (minN + i) + b } + } + } else if (a == 0) { + if (b > 0) { + { i -> + if (i != 0) { + invalidValue() + } + b + } + } else { + invalidValue() + } + } else { + if (b > 0) { + if (b <= -a) { + invalidValue() } else { - sq.take(maxAb) - }).filterIndexed { x, _ -> (x - b1) % a == 0 && (x - b1) / a > 0 } + // -2n+9 -> (1_7,2_5,3_3,4_1) -> (1,3,5,7) -> 1 + // -3n+9 -> (1_6,2_3) -> (3,6) + // -5n+7 -> (1_2) -> (2) + val maxN = -b / a - if (b % a == 0) 1 else 0 + { i -> a * (maxN - i) + b } + } + } else { + invalidValue() } } + override fun getOffset(i: Int) = innerGetOffset(i) - 1 } diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/PrimitiveValue.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/PrimitiveValue.kt new file mode 100644 index 000000000..f0c3b74fe --- /dev/null +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/PrimitiveValue.kt @@ -0,0 +1,53 @@ +package li.songe.selector.data + +sealed class PrimitiveValue(open val value: Any?) { + data object NullValue : PrimitiveValue(null) { + override fun toString() = "null" + } + + data class BooleanValue(override val value: Boolean) : PrimitiveValue(value) { + override fun toString() = value.toString() + } + + data class IntValue(override val value: Int) : PrimitiveValue(value) { + override fun toString() = value.toString() + } + + data class StringValue(override val value: String) : PrimitiveValue(value) { + 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() + } + } +} diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/PropertySegment.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/PropertySegment.kt index 36ea52e32..87ad1e1bf 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/PropertySegment.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/PropertySegment.kt @@ -1,6 +1,5 @@ package li.songe.selector.data -import li.songe.selector.NodeMatchFc import li.songe.selector.Transform @@ -14,31 +13,22 @@ data class PropertySegment( ) { private val matchAnyName = name.isBlank() || name == "*" - val propertyNames = - (if (matchAnyName) listOf("name") else emptyList()) + expressions.map { e -> e.propertyNames } - .flatten() + val binaryExpressions = expressions.map { e -> e.binaryExpressions }.flatten() override fun toString(): String { val matchTag = if (tracked) "@" else "" return matchTag + name + expressions.joinToString("") { "[$it]" } } - private val matchName = if (matchAnyName) { - object : NodeMatchFc { - override fun invoke(node: T, transform: Transform) = true - } - } else { - object : NodeMatchFc { - override fun invoke(node: T, transform: Transform): Boolean { - val str = transform.getName(node) ?: return false - if (str.length == name.length) { - return str.contentEquals(name) - } else if (str.length > name.length) { - return str[str.length - name.length - 1] == '.' && str.endsWith(name) - } - return false - } + private fun matchName(node: T, transform: Transform): Boolean { + if (matchAnyName) return true + val str = transform.getName(node) ?: return false + if (str.length == name.length) { + return str.contentEquals(name) + } else if (str.length > name.length) { + return str[str.length - name.length - 1] == '.' && str.endsWith(name) } + return false } fun match(node: T, transform: Transform): Boolean { diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/PropertyWrapper.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/PropertyWrapper.kt index 2dc05c1c0..3a94f812b 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/PropertyWrapper.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/PropertyWrapper.kt @@ -17,7 +17,7 @@ data class PropertyWrapper( fun matchTracks( node: T, transform: Transform, - trackNodes: MutableList = mutableListOf(), + trackNodes: MutableList, ): List? { if (!propertySegment.match(node, transform)) { return null diff --git a/selector/src/commonMain/kotlin/li/songe/selector/data/TupleExpression.kt b/selector/src/commonMain/kotlin/li/songe/selector/data/TupleExpression.kt index 342d2b7aa..9f7d69472 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/data/TupleExpression.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/data/TupleExpression.kt @@ -1,18 +1,18 @@ package li.songe.selector.data -import li.songe.selector.NodeSequenceFc -import li.songe.selector.util.filterIndexes - data class TupleExpression( val numbers: List, ) : ConnectExpression() { - override val isConstant = numbers.size == 1 override val minOffset = (numbers.firstOrNull() ?: 1) - 1 + override val maxOffset = numbers.lastOrNull() + private val indexes = numbers.map { x -> x - 1 } - override val traversal: NodeSequenceFc = object : NodeSequenceFc { - override fun invoke(sq: Sequence): Sequence { - return sq.filterIndexes(indexes) - } + override fun checkOffset(offset: Int): Boolean { + return indexes.binarySearch(offset) >= 0 + } + + override fun getOffset(i: Int): Int { + return numbers[i] } override fun toString(): String { diff --git a/selector/src/commonMain/kotlin/li/songe/selector/parser/ParserSet.kt b/selector/src/commonMain/kotlin/li/songe/selector/parser/ParserSet.kt index fa1eefa78..af6a3dc81 100644 --- a/selector/src/commonMain/kotlin/li/songe/selector/parser/ParserSet.kt +++ b/selector/src/commonMain/kotlin/li/songe/selector/parser/ParserSet.kt @@ -1,6 +1,5 @@ package li.songe.selector.parser -import li.songe.selector.ExtSyntaxError import li.songe.selector.Selector import li.songe.selector.data.BinaryExpression import li.songe.selector.data.CompareOperator @@ -12,9 +11,12 @@ 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.gkdError internal object ParserSet { val whiteCharParser = Parser("\u0020\t\r\n") { source, offset, prefix -> @@ -27,7 +29,7 @@ internal object ParserSet { ParserResult(data, i - offset) } val whiteCharStrictParser = Parser("\u0020\t\r\n") { source, offset, prefix -> - ExtSyntaxError.assert(source, offset, prefix, "whitespace") + gkdAssert(source, offset, prefix, "whitespace") whiteCharParser(source, offset) } val nameParser = @@ -37,7 +39,7 @@ internal object ParserSet { if ((s0 != null) && !prefix.contains(s0)) { return@Parser ParserResult("") } - ExtSyntaxError.assert(source, i, prefix, "*0-9a-zA-Z_") + gkdAssert(source, i, prefix, "*0-9a-zA-Z_") var data = source[i].toString() i++ if (data == "*") { // 范匹配 @@ -47,7 +49,7 @@ internal object ParserSet { while (i < source.length) { // . 不能在开头和结尾 if (data[i - offset - 1] == '.') { - ExtSyntaxError.assert(source, i, prefix, "[0-9a-zA-Z_]") + gkdAssert(source, i, prefix, "[0-9a-zA-Z_]") } if (center.contains(source[i])) { data += source[i] @@ -65,13 +67,13 @@ internal object ParserSet { source.startsWith( subOperator.key, offset ) - } ?: ExtSyntaxError.throwError(source, offset, "ConnectOperator") + } ?: gkdError(source, offset, "ConnectOperator") ParserResult(operator, operator.key.length) } val integerParser = Parser("1234567890") { source, offset, prefix -> var i = offset - ExtSyntaxError.assert(source, i, prefix, "number") + gkdAssert(source, i, prefix, "number") var s = "" while (i < source.length && prefix.contains(source[i])) { s += source[i] @@ -81,7 +83,7 @@ internal object ParserSet { try { s.toInt() } catch (e: NumberFormatException) { - ExtSyntaxError.throwError(source, offset, "valid format number") + gkdError(source, offset, "valid format number") }, i - offset ) } @@ -90,7 +92,7 @@ internal object ParserSet { // [+-][a][n] val monomialParser = Parser("+-1234567890n") { source, offset, prefix -> var i = offset - ExtSyntaxError.assert(source, i, prefix) + gkdAssert(source, i, prefix) /** * one of 1, -1 */ @@ -109,7 +111,7 @@ internal object ParserSet { } i += whiteCharParser(source, i).length // [a][n] - ExtSyntaxError.assert(source, i, integerParser.prefix + "n") + gkdAssert(source, i, integerParser.prefix + "n") val coefficient = if (integerParser.prefix.contains(source[i])) { val coefficientResult = integerParser(source, i) i += coefficientResult.length @@ -132,23 +134,23 @@ internal object ParserSet { // (+-an+-b) val polynomialExpressionParser = Parser("(0123456789n") { source, offset, prefix -> var i = offset - ExtSyntaxError.assert(source, i, prefix) + gkdAssert(source, i, prefix) val monomialResultList = mutableListOf>>() when (source[i]) { '(' -> { i++ i += whiteCharParser(source, i).length - ExtSyntaxError.assert(source, i, monomialParser.prefix) + gkdAssert(source, i, monomialParser.prefix) while (source[i] != ')') { if (monomialResultList.size > 0) { - ExtSyntaxError.assert(source, i, "+-") + gkdAssert(source, i, "+-") } val monomialResult = monomialParser(source, i) monomialResultList.add(monomialResult) i += monomialResult.length i += whiteCharParser(source, i).length if (i >= source.length) { - ExtSyntaxError.assert(source, i, ")") + gkdAssert(source, i, ")") } } i++ @@ -167,21 +169,20 @@ internal object ParserSet { } map.mapKeys { power -> if (power.key > 1) { - ExtSyntaxError.throwError(source, offset, "power must be 0 or 1") + gkdError(source, offset, "power must be 0 or 1") } } - val polynomialExpression = PolynomialExpression(map[1] ?: 0, map[0] ?: 0) - polynomialExpression.apply { - if ((a <= 0 && numbers.isEmpty()) || (numbers.isNotEmpty() && numbers.first() <= 0)) { - ExtSyntaxError.throwError(source, offset, "valid polynomialExpression") - } + val polynomialExpression = try { + PolynomialExpression(map[1] ?: 0, map[0] ?: 0) + } catch (e: Exception) { + gkdError(source, offset, "valid polynomialExpression") } ParserResult(polynomialExpression, i - offset) } val tupleExpressionParser = Parser { source, offset, _ -> var i = offset - ExtSyntaxError.assert(source, i, "(") + gkdAssert(source, i, "(") i++ val numbers = mutableListOf() while (i < source.length && source[i] != ')') { @@ -189,11 +190,11 @@ internal object ParserSet { val intResult = integerParser(source, i) if (numbers.isEmpty()) { if (intResult.data <= 0) { - ExtSyntaxError.throwError(source, i, "positive integer") + gkdError(source, i, "positive integer") } } else { if (intResult.data <= numbers.last()) { - ExtSyntaxError.throwError(source, i, ">" + numbers.last()) + gkdError(source, i, ">" + numbers.last()) } } i += intResult.length @@ -203,10 +204,10 @@ internal object ParserSet { i++ i += whiteCharParser(source, i).length // (1,2,3,) or (1, 2, 6) - ExtSyntaxError.assert(source, i, integerParser.prefix + ")") + gkdAssert(source, i, integerParser.prefix + ")") } } - ExtSyntaxError.assert(source, i, ")") + gkdAssert(source, i, ")") i++ ParserResult(TupleExpression(numbers), i - offset) } @@ -246,30 +247,30 @@ internal object ParserSet { Parser(CompareOperator.allSubClasses.joinToString("") { it.key }) { source, offset, _ -> val operator = CompareOperator.allSubClasses.find { compareOperator -> source.startsWith(compareOperator.key, offset) - } ?: ExtSyntaxError.throwError(source, offset, "CompareOperator") + } ?: gkdError(source, offset, "CompareOperator") ParserResult(operator, operator.key.length) } val stringParser = Parser("`'\"") { source, offset, prefix -> var i = offset - ExtSyntaxError.assert(source, i, prefix) + gkdAssert(source, i, prefix) val startChar = source[i] i++ if (i >= source.length) { - ExtSyntaxError.throwError(source, i, "any char") + gkdError(source, i, "any char") } var data = "" while (source[i] != startChar) { if (i >= source.length - 1) { - ExtSyntaxError.assert(source, i, startChar.toString()) + gkdAssert(source, i, startChar.toString()) break } // https://www.rfc-editor.org/rfc/inline-errata/rfc7159.html if (source[i].code in 0x0000..0x001F) { - ExtSyntaxError.throwError(source, i, "0-1f escape char") + gkdError(source, i, "0-1f escape char") } if (source[i] == '\\') { i++ - ExtSyntaxError.assert(source, i) + gkdAssert(source, i) data += when (source[i]) { '\\' -> '\\' '\'' -> '\'' @@ -282,7 +283,7 @@ internal object ParserSet { 'x' -> { repeat(2) { i++ - ExtSyntaxError.assert(source, i, "0123456789abcdefABCDEF") + gkdAssert(source, i, "0123456789abcdefABCDEF") } source.substring(i - 2 + 1, i + 1).toInt(16).toChar() } @@ -290,13 +291,13 @@ internal object ParserSet { 'u' -> { repeat(4) { i++ - ExtSyntaxError.assert(source, i, "0123456789abcdefABCDEF") + gkdAssert(source, i, "0123456789abcdefABCDEF") } source.substring(i - 4 + 1, i + 1).toInt(16).toChar() } else -> { - ExtSyntaxError.throwError(source, i, "escape char") + gkdError(source, i, "escape char") } } } else { @@ -312,12 +313,12 @@ internal object ParserSet { private val varStr = varPrefix + '.' + ('0'..'9').joinToString("") val propertyParser = Parser(varPrefix) { source, offset, prefix -> var i = offset - ExtSyntaxError.assert(source, i, prefix) + gkdAssert(source, i, prefix) var data = source[i].toString() i++ while (i < source.length && varStr.contains(source[i])) { if (source[i] == '.') { - ExtSyntaxError.assert(source, i + 1, prefix) + gkdAssert(source, i + 1, prefix) } data += source[i] i++ @@ -326,51 +327,59 @@ internal object ParserSet { } val valueParser = - Parser("tfn" + stringParser.prefix + integerParser.prefix) { source, offset, prefix -> + Parser("tfn-" + stringParser.prefix + integerParser.prefix) { source, offset, prefix -> var i = offset - ExtSyntaxError.assert(source, i, prefix) - val value: Any? = when (source[i]) { + gkdAssert(source, i, prefix) + val value: PrimitiveValue = when (source[i]) { 't' -> { i++ "rue".forEach { c -> - ExtSyntaxError.assert(source, i, c.toString()) + gkdAssert(source, i, c.toString()) i++ } - true + PrimitiveValue.BooleanValue(true) } 'f' -> { i++ "alse".forEach { c -> - ExtSyntaxError.assert(source, i, c.toString()) + gkdAssert(source, i, c.toString()) i++ } - false + PrimitiveValue.BooleanValue(false) } 'n' -> { i++ "ull".forEach { c -> - ExtSyntaxError.assert(source, i, c.toString()) + gkdAssert(source, i, c.toString()) i++ } - null + PrimitiveValue.NullValue } in stringParser.prefix -> { val s = stringParser(source, i) i += s.length - s.data + PrimitiveValue.StringValue(s.data) + } + + '-' -> { + 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 - n.data + PrimitiveValue.IntValue(n.data) } else -> { - ExtSyntaxError.throwError(source, i, prefix) + gkdError(source, i, prefix) } } ParserResult(value, i - offset) @@ -385,6 +394,9 @@ internal object ParserSet { i += operatorResult.length i += whiteCharParser(source, i).length val valueResult = valueParser(source, i) + if (!operatorResult.data.allowType(valueResult.data)) { + gkdError(source, i, "valid primitive value") + } i += valueResult.length ParserResult( BinaryExpression( @@ -398,7 +410,7 @@ internal object ParserSet { i += whiteCharParser(source, i).length val operator = LogicalOperator.allSubClasses.find { logicalOperator -> source.startsWith(logicalOperator.key, offset) - } ?: ExtSyntaxError.throwError(source, offset, "LogicalOperator") + } ?: gkdError(source, offset, "LogicalOperator") ParserResult(operator, operator.key.length) } @@ -420,21 +432,21 @@ internal object ParserSet { while (i - 1 >= count && source[i - 1 - count] in whiteCharParser.prefix) { count++ } - ExtSyntaxError.throwError( + gkdError( source, i - count - lastToken.length, "LogicalOperator" ) } } i++ parserResults.add(expressionParser(source, i).apply { i += length }) - ExtSyntaxError.assert(source, i, ")") + gkdAssert(source, i, ")") i++ } in "|&" -> { parserResults.add(logicalOperatorParser(source, i).apply { i += length }) i += whiteCharParser(source, i).length - ExtSyntaxError.assert(source, i, "(" + propertyParser.prefix) + gkdAssert(source, i, "(" + propertyParser.prefix) } else -> { @@ -444,7 +456,7 @@ internal object ParserSet { i += whiteCharParser(source, i).length } if (parserResults.isEmpty()) { - ExtSyntaxError.throwError( + gkdError( source, i - offset, "Expression" ) } @@ -486,12 +498,12 @@ internal object ParserSet { val attrParser = Parser("[") { source, offset, prefix -> var i = offset - ExtSyntaxError.assert(source, i, prefix) + gkdAssert(source, i, prefix) i++ i += whiteCharParser(source, i).length val exp = expressionParser(source, i) i += exp.length - ExtSyntaxError.assert(source, i, "]") + gkdAssert(source, i, "]") i++ ParserResult( exp.data, i - offset @@ -515,7 +527,7 @@ internal object ParserSet { expressions.add(attrResult.data) } if (nameResult.length == 0 && expressions.size == 0) { - ExtSyntaxError.throwError(source, i, "[") + gkdError(source, i, "[") } ParserResult(PropertySegment(tracked, nameResult.data, expressions), i - offset) } @@ -537,6 +549,7 @@ internal object ParserSet { i += whiteCharStrictParser(source, i).length combinatorResult.data } else { + // A B ConnectSegment(connectExpression = PolynomialExpression(1, 0)) } val selectorResult = selectorUnitParser(source, i) @@ -548,7 +561,7 @@ internal object ParserSet { val endParser = Parser { source, offset, _ -> if (offset != source.length) { - ExtSyntaxError.throwError(source, offset, "EOF") + gkdError(source, offset, "EOF") } ParserResult(Unit, 0) } diff --git a/selector/src/commonMain/kotlin/li/songe/selector/util/FilterIndexesSequence.kt b/selector/src/commonMain/kotlin/li/songe/selector/util/FilterIndexesSequence.kt deleted file mode 100644 index 69c11e8ad..000000000 --- a/selector/src/commonMain/kotlin/li/songe/selector/util/FilterIndexesSequence.kt +++ /dev/null @@ -1,42 +0,0 @@ -package li.songe.selector.util - -internal class FilterIndexesSequence( - private val sequence: Sequence, - private val indexes: List, -) : Sequence { - override fun iterator() = object : Iterator { - val iterator = sequence.iterator() - var seqIndex = 0 // sequence - var i = 0 // indexes - var nextItem: T? = null - - fun calcNext(): T? { - if (seqIndex > indexes.last()) return null - while (iterator.hasNext()) { - val item = iterator.next() - if (indexes[i] == seqIndex) { - i++ - seqIndex++ - return item - } - seqIndex++ - } - return null - } - - override fun next(): T { - val result = nextItem - nextItem = null - return result ?: calcNext() ?: throw NoSuchElementException() - } - - override fun hasNext(): Boolean { - nextItem = nextItem ?: calcNext() - return nextItem != null - } - } -} - -internal fun Sequence.filterIndexes(indexes: List): Sequence { - return FilterIndexesSequence(this, indexes) -} \ No newline at end of file diff --git a/selector/src/jvmTest/kotlin/li/songe/selector/ParserTest.kt b/selector/src/jvmTest/kotlin/li/songe/selector/ParserTest.kt index 9c577d083..9d4702d69 100644 --- a/selector/src/jvmTest/kotlin/li/songe/selector/ParserTest.kt +++ b/selector/src/jvmTest/kotlin/li/songe/selector/ParserTest.kt @@ -6,12 +6,75 @@ import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.booleanOrNull import kotlinx.serialization.json.intOrNull import li.songe.selector.parser.ParserSet -import li.songe.selector.util.filterIndexes import org.junit.Test +import java.io.BufferedOutputStream import java.io.File +import java.io.FileOutputStream +import java.net.URL +import java.util.zip.ZipInputStream class ParserTest { + private val projectCwd = File("../").absolutePath + private val assetsDir = File("$projectCwd/_assets").apply { + if (!exists()) { + mkdir() + } + } + private val json = Json { + ignoreUnknownKeys = true + } + private val transform = Transform(getAttr = { node, name -> + if (name == "_id") return@Transform node.id + if (name == "_pid") return@Transform node.pid + val value = node.attr[name] ?: return@Transform null + if (value is JsonNull) return@Transform null + value.intOrNull ?: value.booleanOrNull ?: value.content + }, getName = { node -> node.attr["name"]?.content }, getChildren = { node -> + node.children.asSequence() + }, getParent = { node -> node.parent }) + + private val idToSnapshot = HashMap() + + private fun getOrDownloadNode(url: String): TestNode { + val githubAssetId = url.split('/').last() + idToSnapshot[githubAssetId]?.let { return it } + + val file = assetsDir.resolve("$githubAssetId.json") + if (!file.exists()) { + URL("https://github.com/gkd-kit/inspect/files/${githubAssetId}/file.zip").openStream() + .use { inputStream -> + val zipInputStream = ZipInputStream(inputStream) + var entry = zipInputStream.nextEntry + while (entry != null) { + if (entry.name.endsWith(".json")) { + val outputStream = BufferedOutputStream(FileOutputStream(file)) + val buffer = ByteArray(1024) + var bytesRead: Int + while (zipInputStream.read(buffer).also { bytesRead = it } != -1) { + outputStream.write(buffer, 0, bytesRead) + } + outputStream.close() + break + } + entry = zipInputStream.nextEntry + } + zipInputStream.closeEntry() + zipInputStream.close() + } + } + val nodes = json.decodeFromString(file.readText()).nodes + + nodes.forEach { node -> + node.parent = nodes.getOrNull(node.pid) + node.parent?.apply { + children.add(node) + } + } + return nodes.first().apply { + idToSnapshot[githubAssetId] = this + } + } @Test fun test_expression() { @@ -24,42 +87,26 @@ class ParserTest { fun string_selector() { val text = "ImageView < @FrameLayout < LinearLayout < RelativeLayout RelativeLayout > TextView[text\$='广告']" - println("trackIndex: " + Selector.parse(text).trackIndex) + val selector = Selector.parse(text) + println("trackIndex: " + selector.trackIndex) + println("canCacheIndex: " + Selector.parse("A + B").canCacheIndex) + println("canCacheIndex: " + Selector.parse("A > B - C").canCacheIndex) } @Test fun query_selector() { - val projectCwd = File("../").absolutePath val text = - "* > View[isClickable=true][childCount=1][textLen=0] > Image[isClickable=false][textLen=0]" + "@[vid=\"rv_home_tab\"] <<(99-n) [vid=\"header_container\"] -(-2n+9) [vid=\"layout_refresh\"] +2 [vid=\"home_v10_frag_content\"]" val selector = Selector.parse(text) println("selector: $selector") - - val jsonString = File("$projectCwd/_assets/snapshot-1686629593092.json").readText() - val json = Json { - ignoreUnknownKeys = true - } - val nodes = json.decodeFromString(jsonString).nodes - - nodes.forEach { node -> - node.parent = nodes.getOrNull(node.pid) - node.parent?.apply { - children.add(node) - } - } - val transform = Transform(getAttr = { node, name -> - val value = node.attr[name] ?: return@Transform null - if (value is JsonNull) return@Transform null - value.intOrNull ?: value.booleanOrNull ?: value.content - }, getName = { node -> node.attr["name"]?.content }, getChildren = { node -> - node.children.asSequence() - }, getParent = { node -> node.parent }) - val targets = transform.querySelectorAll(nodes.first(), selector).toList() + val node = getOrDownloadNode("https://i.gkd.li/i/14325747") + val targets = transform.querySelectorAll(node, selector).toList() println("target_size: " + targets.size) + println("target_id: " + targets.map { t -> t.id }) assertTrue(targets.size == 1) println("id: " + targets.first().id) - val trackTargets = transform.querySelectorTrackAll(nodes.first(), selector).toList() + val trackTargets = transform.querySelectorTrackAll(node, selector).toList() println("trackTargets_size: " + trackTargets.size) assertTrue(trackTargets.size == 1) println(trackTargets.first().mapIndexed { index, testNode -> @@ -69,45 +116,22 @@ class ParserTest { @Test fun check_parser() { - println(Selector.parse("View > Text")) - } - - private val json = Json { - ignoreUnknownKeys = true - } - - private fun getTreeNode(name: String): TestNode { - val jsonString = File("../_assets/$name").readText() - val nodes = json.decodeFromString(jsonString).nodes - nodes.forEach { node -> - node.parent = nodes.getOrNull(node.pid) - node.parent?.apply { - children.add(node) - } - } - return nodes.first() + val selector = Selector.parse("View > Text[index>-0]") + println("selector: $selector") + println("canCacheIndex: " + selector.canCacheIndex) } - private val transform = Transform(getAttr = { node, name -> - if (name == "_id") return@Transform node.id - if (name == "_pid") return@Transform node.pid - val value = node.attr[name] ?: return@Transform null - if (value is JsonNull) return@Transform null - value.intOrNull ?: value.booleanOrNull ?: value.content - }, getName = { node -> node.attr["name"]?.content }, getChildren = { node -> - node.children.asSequence() - }, getParent = { node -> node.parent }) @Test fun check_query() { - val text = "@TextView[text^='跳过'] + LinearLayout TextView[text*=`跳转`]" + val text = "@TextView - [text=\"签到提醒\"] <3, 3->21 // 1,3->24 - val snapshotNode = getTreeNode("snapshot-1698997584508.json") + val snapshotNode = getOrDownloadNode("https://i.gkd.li/i/13247733") val (x1, x2) = (1..6).toList().shuffled().subList(0, 2).sorted() val x1N = transform.querySelectorAll(snapshotNode, Selector.parse("[_id=15] >$x1 *")).count() @@ -166,8 +181,7 @@ class ParserTest { println("source:$source") val selector = Selector.parse(source) println("selector:$selector") - // https://i.gkd.li/import/13247610 - val snapshotNode = getTreeNode("snapshot-1698990932472.json") + val snapshotNode = getOrDownloadNode("https://i.gkd.li/i/13247610") println("result:" + transform.querySelectorAll(snapshotNode, selector).map { n -> n.id } .toList()) } diff --git a/selector/src/jvmTest/kotlin/li/songe/selector/TestNode.kt b/selector/src/jvmTest/kotlin/li/songe/selector/TestNode.kt index 369326022..4b4c31605 100644 --- a/selector/src/jvmTest/kotlin/li/songe/selector/TestNode.kt +++ b/selector/src/jvmTest/kotlin/li/songe/selector/TestNode.kt @@ -15,5 +15,9 @@ data class TestNode( @Transient var children: MutableList = mutableListOf() + + override fun toString(): String { + return id.toString() + } }