mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
perf: indexCache
This commit is contained in:
parent
36719e2b5f
commit
ad7d39a042
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -19,3 +19,5 @@ local.properties
|
|||
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
/_assets
|
||||
|
|
|
@ -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
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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<AccessibilityNodeInfo>? = null,
|
||||
): AccessibilityNodeInfo? {
|
||||
val t = (if (selector.canCacheIndex) transform else defaultTransform) ?: defaultTransform
|
||||
if (selector.isMatchRoot) {
|
||||
if (parent == null) {
|
||||
val trackNodes = mutableListOf<AccessibilityNodeInfo>()
|
||||
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<AccessibilityNodeInfo>()
|
||||
val trackNodes = ArrayList<AccessibilityNodeInfo>(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,22 +192,61 @@ private val getAttr: (AccessibilityNodeInfo, String) -> Any? = { node, name ->
|
|||
}
|
||||
}
|
||||
|
||||
val abTransform = Transform(
|
||||
getAttr = getAttr,
|
||||
data class CacheTransform(
|
||||
val transform: Transform<AccessibilityNodeInfo>,
|
||||
val indexCache: HashMap<AccessibilityNodeInfo, Int>,
|
||||
)
|
||||
|
||||
fun createCacheTransform(): CacheTransform {
|
||||
val indexCache = HashMap<AccessibilityNodeInfo, Int>()
|
||||
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<AccessibilityNodeInfo> = { 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 = getChildren,
|
||||
getChild = { node, index -> if (index in 0..<node.childCount) node.getChild(index) else null },
|
||||
getChildren = getChildrenCache,
|
||||
getParent = { node -> node.parent },
|
||||
getDescendants = { node ->
|
||||
sequence {
|
||||
val stack = getChildren(node).toMutableList()
|
||||
val stack = getChildrenCache(node).toMutableList()
|
||||
if (stack.isEmpty()) return@sequence
|
||||
stack.reverse()
|
||||
val tempNodes = mutableListOf<AccessibilityNodeInfo>()
|
||||
do {
|
||||
val top = stack.removeLast()
|
||||
yield(top)
|
||||
for (childNode in getChildren(top)) {
|
||||
for (childNode in getChildrenCache(top)) {
|
||||
tempNodes.add(childNode)
|
||||
}
|
||||
if (tempNodes.isNotEmpty()) {
|
||||
|
@ -215,5 +257,167 @@ val abTransform = Transform(
|
|||
}
|
||||
} 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<AccessibilityNodeInfo>()
|
||||
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,
|
||||
getParent = { node -> node.parent },
|
||||
getDescendants = { node ->
|
||||
sequence {
|
||||
val stack = getChildren(node).toMutableList()
|
||||
if (stack.isEmpty()) return@sequence
|
||||
stack.reverse()
|
||||
val tempNodes = mutableListOf<AccessibilityNodeInfo>()
|
||||
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)
|
||||
}
|
||||
if (tempNodes.isNotEmpty()) {
|
||||
for (i in tempNodes.size - 1 downTo 0) {
|
||||
stack.add(tempNodes[i])
|
||||
}
|
||||
tempNodes.clear()
|
||||
}
|
||||
} while (stack.isNotEmpty())
|
||||
}
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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) {
|
||||
// 仅查询
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -346,6 +346,7 @@ fun GlobalRuleExcludePage(subsItemId: Long, groupKey: Int) {
|
|||
TextButton(onClick = {
|
||||
if (oldSource == source) {
|
||||
toast("禁用项无变动")
|
||||
showEditDlg = false
|
||||
return@TextButton
|
||||
}
|
||||
showEditDlg = false
|
||||
|
|
|
@ -343,6 +343,7 @@ fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) {
|
|||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
if (oldSource == source) {
|
||||
setEditGroupRaw(null)
|
||||
toast("规则无变动")
|
||||
return@TextButton
|
||||
}
|
||||
|
|
|
@ -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<HomeVm>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -16,6 +16,11 @@ kotlin {
|
|||
generateTypeScriptDefinitions()
|
||||
browser {}
|
||||
}
|
||||
sourceSets {
|
||||
all {
|
||||
languageSettings.optIn("kotlin.js.ExperimentalJsExport")
|
||||
}
|
||||
}
|
||||
sourceSets["commonMain"].dependencies {
|
||||
implementation(libs.kotlin.stdlib.common)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 <T : Any> match(node: T, transform: CommonTransform<T>): T? {
|
||||
fun <T : Any> match(node: T, transform: MultiplatformTransform<T>): T? {
|
||||
return selector.match(node, transform.transform)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> matchTrack(node: T, transform: CommonTransform<T>): Array<T>? {
|
||||
fun <T : Any> matchTrack(node: T, transform: MultiplatformTransform<T>): Array<T>? {
|
||||
return selector.matchTracks(node, transform.transform)?.toTypedArray<Any?>() as Array<T>?
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
package li.songe.selector
|
||||
|
||||
import kotlin.js.ExperimentalJsExport
|
||||
import kotlin.js.JsExport
|
||||
|
||||
@OptIn(ExperimentalJsExport::class)
|
||||
@JsExport
|
||||
class CommonTransform<T : Any>(
|
||||
@Suppress("UNCHECKED_CAST", "UNUSED")
|
||||
class MultiplatformTransform<T : Any>(
|
||||
getAttr: (T, String) -> Any?,
|
||||
getName: (T) -> String?,
|
||||
getChildren: (T) -> Array<T>,
|
||||
|
@ -18,28 +17,23 @@ class CommonTransform<T : Any>(
|
|||
getParent = getParent,
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "UNUSED")
|
||||
val querySelectorAll: (T, CommonSelector) -> Array<T> = { node, selector ->
|
||||
val querySelectorAll: (T, MultiplatformSelector) -> Array<T> = { node, selector ->
|
||||
val result =
|
||||
transform.querySelectorAll(node, selector.selector).toList().toTypedArray<Any?>()
|
||||
result as Array<T>
|
||||
}
|
||||
|
||||
@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<Array<T>> = { node, selector ->
|
||||
val querySelectorTrackAll: (T, MultiplatformSelector) -> Array<Array<T>> = { node, selector ->
|
||||
val result = transform.querySelectorTrackAll(node, selector.selector)
|
||||
.map { it.toTypedArray<Any?>() as Array<T> }.toList().toTypedArray<Any?>()
|
||||
result as Array<Array<T>>
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "UNUSED")
|
||||
val querySelectorTrack: (T, CommonSelector) -> Array<T>? = { node, selector ->
|
||||
val querySelectorTrack: (T, MultiplatformSelector) -> Array<T>? = { node, selector ->
|
||||
transform.querySelectorTrackAll(node, selector.selector).firstOrNull()
|
||||
?.toTypedArray<Any?>() as Array<T>?
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package li.songe.selector
|
||||
|
||||
internal interface NodeMatchFc {
|
||||
operator fun <T> invoke(node: T, transform: Transform<T>): Boolean
|
||||
}
|
||||
|
||||
interface NodeSequenceFc {
|
||||
operator fun <T> invoke(sq: Sequence<T?>): Sequence<T?>
|
||||
}
|
||||
|
||||
internal val emptyNodeSequence = object : NodeSequenceFc {
|
||||
override fun <T> invoke(sq: Sequence<T?>) = emptySequence<T?>()
|
||||
}
|
||||
|
||||
internal interface NodeTraversalFc {
|
||||
operator fun <T> invoke(node: T, transform: Transform<T>): Sequence<T?>
|
||||
}
|
|
@ -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<Boolean>()
|
||||
}
|
||||
|
||||
val trackIndex = tracks.indexOfFirst { it }.let { i ->
|
||||
if (i < 0) 0 else i
|
||||
}
|
||||
|
||||
val connectKeys by lazy {
|
||||
var c = propertyWrapper.to
|
||||
val keys = mutableListOf<String>()
|
||||
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<String>()
|
||||
while (p != null) {
|
||||
val s = p!!.propertySegment
|
||||
p = p!!.to?.to
|
||||
names.addAll(s.propertyNames)
|
||||
}
|
||||
names.distinct().toTypedArray()
|
||||
}
|
||||
|
||||
fun <T> match(
|
||||
node: T,
|
||||
transform: Transform<T>,
|
||||
trackNodes: MutableList<T> = mutableListOf(),
|
||||
trackNodes: MutableList<T> = 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 <T> matchTracks(
|
||||
node: T,
|
||||
transform: Transform<T>,
|
||||
trackNodes: MutableList<T> = mutableListOf(),
|
||||
trackNodes: MutableList<T> = ArrayList(tracks.size),
|
||||
): List<T>? {
|
||||
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<String>()
|
||||
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<BinaryExpression>()
|
||||
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 {
|
||||
|
|
|
@ -1,54 +1,14 @@
|
|||
package li.songe.selector
|
||||
|
||||
import li.songe.selector.data.ConnectExpression
|
||||
|
||||
@Suppress("UNUSED")
|
||||
class Transform<T>(
|
||||
val getAttr: (T, String) -> Any?,
|
||||
val getName: (T) -> CharSequence?,
|
||||
val getChildren: (T) -> Sequence<T>,
|
||||
val getChild: (T, Int) -> T? = { node, offset -> getChildren(node).elementAtOrNull(offset) },
|
||||
val getParent: (T) -> T?,
|
||||
val getAncestors: (T) -> Sequence<T> = { node ->
|
||||
sequence {
|
||||
var parentVar: T? = getParent(node) ?: return@sequence
|
||||
while (parentVar != null) {
|
||||
parentVar?.let {
|
||||
yield(it)
|
||||
parentVar = getParent(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
val getAncestor: (T, Int) -> T? = { node, offset -> getAncestors(node).elementAtOrNull(offset) },
|
||||
|
||||
val getBeforeBrothers: (T) -> Sequence<T?> = { node ->
|
||||
sequence {
|
||||
val parentVal = getParent(node) ?: return@sequence
|
||||
val list = getChildren(parentVal).takeWhile { it != node }.toMutableList()
|
||||
list.reverse()
|
||||
yieldAll(list)
|
||||
}
|
||||
},
|
||||
val getBeforeBrother: (T, Int) -> T? = { node, offset ->
|
||||
getBeforeBrothers(node).elementAtOrNull(
|
||||
offset
|
||||
)
|
||||
},
|
||||
|
||||
val getAfterBrothers: (T) -> Sequence<T?> = { node ->
|
||||
sequence {
|
||||
val parentVal = getParent(node) ?: return@sequence
|
||||
yieldAll(getChildren(parentVal).dropWhile { it != node }.drop(1))
|
||||
}
|
||||
},
|
||||
val getAfterBrother: (T, Int) -> T? = { node, offset ->
|
||||
getAfterBrothers(node).elementAtOrNull(
|
||||
offset
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* 遍历下面所有子孙节点,不包含自己
|
||||
*/
|
||||
val getDescendants: (T) -> Sequence<T> = { node ->
|
||||
sequence { // 深度优先 先序遍历
|
||||
// https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelector
|
||||
|
@ -73,12 +33,112 @@ class Transform<T>(
|
|||
}
|
||||
},
|
||||
|
||||
) {
|
||||
val getChildrenX: (T, ConnectExpression) -> Sequence<T> = { 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<T> = { node, connectExpression ->
|
||||
sequence {
|
||||
var parentVar: T? = getParent(node) ?: return@sequence
|
||||
var offset = 0
|
||||
while (parentVar != null) {
|
||||
parentVar?.let {
|
||||
if (connectExpression.checkOffset(offset)) {
|
||||
yield(it)
|
||||
}
|
||||
offset++
|
||||
connectExpression.maxOffset?.let { maxOffset ->
|
||||
if (offset > maxOffset) {
|
||||
return@sequence
|
||||
}
|
||||
}
|
||||
parentVar = getParent(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
val getBeforeBrothers: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
|
||||
val parentVal = getParent(node)
|
||||
if (parentVal != null) {
|
||||
val list = getChildren(parentVal).takeWhile { it != node }.toMutableList()
|
||||
list.reverse()
|
||||
list.asSequence().filterIndexed { i, _ ->
|
||||
connectExpression.checkOffset(
|
||||
i
|
||||
)
|
||||
}
|
||||
} else {
|
||||
emptySequence()
|
||||
}
|
||||
},
|
||||
val getAfterBrothers: (T, ConnectExpression) -> Sequence<T> = { 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 getDescendantsX: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
|
||||
sequence {
|
||||
val stack = getChildren(node).toMutableList()
|
||||
if (stack.isEmpty()) return@sequence
|
||||
stack.reverse()
|
||||
val tempNodes = mutableListOf<T>()
|
||||
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 getChildren(top)) {
|
||||
tempNodes.add(childNode)
|
||||
}
|
||||
if (tempNodes.isNotEmpty()) {
|
||||
for (i in tempNodes.size - 1 downTo 0) {
|
||||
stack.add(tempNodes[i])
|
||||
}
|
||||
tempNodes.clear()
|
||||
}
|
||||
} while (stack.isNotEmpty())
|
||||
}
|
||||
},
|
||||
|
||||
) {
|
||||
val querySelectorAll: (T, Selector) -> Sequence<T> = { node, selector ->
|
||||
sequence {
|
||||
// cache trackNodes
|
||||
val trackNodes: MutableList<T> = mutableListOf()
|
||||
val trackNodes = ArrayList<T>(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<T>(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
val querySelectorTrack: (T, Selector) -> List<T>? = { node, selector ->
|
||||
querySelectorTrackAll(
|
||||
node, selector
|
||||
|
|
|
@ -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 <T> match(node: T, transform: Transform<T>) =
|
||||
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}"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 <T> traversal(node: T, transform: Transform<T>): Sequence<T?>
|
||||
abstract fun <T> traversal(node: T, transform: Transform<T>, offset: Int): T?
|
||||
abstract fun <T> traversal(
|
||||
node: T, transform: Transform<T>, connectExpression: ConnectExpression
|
||||
): Sequence<T>
|
||||
|
||||
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 <T> traversal(node: T, transform: Transform<T>) =
|
||||
transform.getBeforeBrothers(node)
|
||||
override fun <T> traversal(
|
||||
node: T, transform: Transform<T>, connectExpression: ConnectExpression
|
||||
) = transform.getBeforeBrothers(node, connectExpression)
|
||||
|
||||
override fun <T> traversal(node: T, transform: Transform<T>, offset: Int): T? =
|
||||
transform.getBeforeBrother(node, offset)
|
||||
}
|
||||
|
||||
/**
|
||||
* A - B, 1,2,3,B,A,7,8
|
||||
*/
|
||||
data object AfterBrother : ConnectOperator("-") {
|
||||
override fun <T> traversal(node: T, transform: Transform<T>) =
|
||||
transform.getAfterBrothers(node)
|
||||
|
||||
override fun <T> traversal(node: T, transform: Transform<T>, offset: Int): T? =
|
||||
transform.getAfterBrother(node, offset)
|
||||
override fun <T> traversal(
|
||||
node: T, transform: Transform<T>, connectExpression: ConnectExpression
|
||||
) = transform.getAfterBrothers(node, connectExpression)
|
||||
}
|
||||
|
||||
/**
|
||||
* A > B, A is the ancestor of B
|
||||
*/
|
||||
data object Ancestor : ConnectOperator(">") {
|
||||
override fun <T> traversal(node: T, transform: Transform<T>) = transform.getAncestors(node)
|
||||
override fun <T> traversal(
|
||||
node: T, transform: Transform<T>, connectExpression: ConnectExpression
|
||||
) = transform.getAncestors(node, connectExpression)
|
||||
|
||||
override fun <T> traversal(node: T, transform: Transform<T>, offset: Int): T? =
|
||||
transform.getAncestor(node, offset)
|
||||
}
|
||||
|
||||
/**
|
||||
* A < B, A is the child of B
|
||||
*/
|
||||
data object Child : ConnectOperator("<") {
|
||||
override fun <T> traversal(node: T, transform: Transform<T>) = transform.getChildren(node)
|
||||
|
||||
override fun <T> traversal(node: T, transform: Transform<T>, offset: Int): T? =
|
||||
transform.getChild(node, offset)
|
||||
override fun <T> traversal(
|
||||
node: T, transform: Transform<T>, connectExpression: ConnectExpression
|
||||
) = transform.getChildrenX(node, connectExpression)
|
||||
}
|
||||
|
||||
/**
|
||||
* A << B, A is the descendant of B
|
||||
*/
|
||||
data object Descendant : ConnectOperator("<<") {
|
||||
override fun <T> traversal(node: T, transform: Transform<T>) =
|
||||
transform.getDescendants(node)
|
||||
|
||||
override fun <T> traversal(node: T, transform: Transform<T>, offset: Int): T? =
|
||||
transform.getDescendants(node).elementAtOrNull(offset)
|
||||
override fun <T> traversal(
|
||||
node: T, transform: Transform<T>, connectExpression: ConnectExpression
|
||||
) = transform.getDescendantsX(node, connectExpression)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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 <T> invoke(node: T, transform: Transform<T>): Sequence<T?> = sequence {
|
||||
val node1 = operator.traversal(node, transform, connectExpression.minOffset)
|
||||
if (node1 != null) {
|
||||
yield(node1)
|
||||
fun <T> traversal(node: T, transform: Transform<T>): Sequence<T?> {
|
||||
return operator.traversal(node, transform, connectExpression)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
object : NodeTraversalFc {
|
||||
override fun <T> invoke(node: T, transform: Transform<T>): Sequence<T?> {
|
||||
return connectExpression.traversal(
|
||||
operator.traversal(node, transform)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ data class ConnectWrapper(
|
|||
|
||||
fun <T> matchTracks(
|
||||
node: T, transform: Transform<T>,
|
||||
trackNodes: MutableList<T> = mutableListOf(),
|
||||
trackNodes: MutableList<T>,
|
||||
): List<T>? {
|
||||
connectSegment.traversal(node, transform).forEach {
|
||||
if (it == null) return@forEach
|
||||
|
|
|
@ -5,5 +5,5 @@ import li.songe.selector.Transform
|
|||
sealed class Expression {
|
||||
abstract fun <T> match(node: T, transform: Transform<T>): Boolean
|
||||
|
||||
abstract val propertyNames: List<String>
|
||||
abstract val binaryExpressions: List<BinaryExpression>
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -12,7 +12,6 @@ sealed class LogicalOperator(val key: String) {
|
|||
}
|
||||
}
|
||||
|
||||
override fun toString() = key
|
||||
abstract fun <T> compare(
|
||||
node: T,
|
||||
transform: Transform<T>,
|
||||
|
|
|
@ -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<Int>()
|
||||
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()
|
||||
}
|
||||
} else if (a > 0) {
|
||||
// infinite
|
||||
emptyList()
|
||||
} - 1
|
||||
|
||||
override val maxOffset = if (a > 0) {
|
||||
null
|
||||
} else if (a == 0) {
|
||||
if (b > 0) {
|
||||
b
|
||||
} else {
|
||||
if (b < 0) {
|
||||
emptyList()
|
||||
} else if (b > 0) {
|
||||
listOf(b)
|
||||
invalidValue()
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
override val traversal = object : NodeSequenceFc {
|
||||
override fun <T> invoke(sq: Sequence<T?>): Sequence<T?> {
|
||||
return (if (a > 0) {
|
||||
sq
|
||||
} else {
|
||||
sq.take(maxAb)
|
||||
}).filterIndexed { x, _ -> (x - b1) % a == 0 && (x - b1) / a > 0 }
|
||||
if (b > 0) {
|
||||
if (b <= -a) {
|
||||
invalidValue()
|
||||
} else {
|
||||
a + b
|
||||
}
|
||||
} else {
|
||||
invalidValue()
|
||||
}
|
||||
} - 1
|
||||
|
||||
private val isConstant = minOffset == maxOffset
|
||||
|
||||
// (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
|
||||
}
|
||||
|
||||
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 {
|
||||
// -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package li.songe.selector.data
|
||||
|
||||
import li.songe.selector.NodeMatchFc
|
||||
import li.songe.selector.Transform
|
||||
|
||||
|
||||
|
@ -14,22 +13,15 @@ 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 <T> invoke(node: T, transform: Transform<T>) = true
|
||||
}
|
||||
} else {
|
||||
object : NodeMatchFc {
|
||||
override fun <T> invoke(node: T, transform: Transform<T>): Boolean {
|
||||
private fun <T> matchName(node: T, transform: Transform<T>): Boolean {
|
||||
if (matchAnyName) return true
|
||||
val str = transform.getName(node) ?: return false
|
||||
if (str.length == name.length) {
|
||||
return str.contentEquals(name)
|
||||
|
@ -38,8 +30,6 @@ data class PropertySegment(
|
|||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> match(node: T, transform: Transform<T>): Boolean {
|
||||
return matchName(node, transform) && expressions.all { ex -> ex.match(node, transform) }
|
||||
|
|
|
@ -17,7 +17,7 @@ data class PropertyWrapper(
|
|||
fun <T> matchTracks(
|
||||
node: T,
|
||||
transform: Transform<T>,
|
||||
trackNodes: MutableList<T> = mutableListOf(),
|
||||
trackNodes: MutableList<T>,
|
||||
): List<T>? {
|
||||
if (!propertySegment.match(node, transform)) {
|
||||
return null
|
||||
|
|
|
@ -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<Int>,
|
||||
) : 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 <T> invoke(sq: Sequence<T?>): Sequence<T?> {
|
||||
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 {
|
||||
|
|
|
@ -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<ParserResult<Pair<Int, Int>>>()
|
||||
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<Int>()
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
package li.songe.selector.util
|
||||
|
||||
internal class FilterIndexesSequence<T>(
|
||||
private val sequence: Sequence<T>,
|
||||
private val indexes: List<Int>,
|
||||
) : Sequence<T> {
|
||||
override fun iterator() = object : Iterator<T> {
|
||||
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 <T> Sequence<T>.filterIndexes(indexes: List<Int>): Sequence<T> {
|
||||
return FilterIndexesSequence(this, indexes)
|
||||
}
|
|
@ -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<TestNode>(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<String, TestNode>()
|
||||
|
||||
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<TestSnapshot>(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 <n LinearLayout < RelativeLayout + LinearLayout > 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<TestSnapshot>(jsonString).nodes
|
||||
|
||||
nodes.forEach { node ->
|
||||
node.parent = nodes.getOrNull(node.pid)
|
||||
node.parent?.apply {
|
||||
children.add(node)
|
||||
}
|
||||
}
|
||||
val transform = Transform<TestNode>(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"))
|
||||
val selector = Selector.parse("View > Text[index>-0]")
|
||||
println("selector: $selector")
|
||||
println("canCacheIndex: " + selector.canCacheIndex)
|
||||
}
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
private fun getTreeNode(name: String): TestNode {
|
||||
val jsonString = File("../_assets/$name").readText()
|
||||
val nodes = json.decodeFromString<TestSnapshot>(jsonString).nodes
|
||||
nodes.forEach { node ->
|
||||
node.parent = nodes.getOrNull(node.pid)
|
||||
node.parent?.apply {
|
||||
children.add(node)
|
||||
}
|
||||
}
|
||||
return nodes.first()
|
||||
}
|
||||
|
||||
private val transform = Transform<TestNode>(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=\"签到提醒\"] <<n [vid=\"webViewContainer\"]"
|
||||
val selector = Selector.parse(text)
|
||||
println("selector: $selector")
|
||||
println(selector.trackIndex)
|
||||
println(selector.tracks.toList())
|
||||
|
||||
val snapshotNode = getTreeNode("snapshot-1693227637861.json")
|
||||
val targets = transform.querySelectorAll(snapshotNode, selector).toList()
|
||||
val node = getOrDownloadNode("https://i.gkd.li/i/14384152")
|
||||
val targets = transform.querySelectorAll(node, selector).toList()
|
||||
println("target_size: " + targets.size)
|
||||
println(targets.firstOrNull())
|
||||
}
|
||||
|
@ -128,14 +152,6 @@ class ParserTest {
|
|||
println("check_quote:$selector")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun check_seq() {
|
||||
println(
|
||||
listOf(1, 2, 3, 4, 5, 6, 7, 8).asSequence().filterIndexes(listOf(0, 1, 7, 10)).toList()
|
||||
)
|
||||
println(listOf(0).asSequence().filterIndexes(listOf(0, 1, 7, 10)).toList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun check_tuple() {
|
||||
val source = "[_id=15] >(1,2,9) X + Z >(7+9n) *"
|
||||
|
@ -143,10 +159,9 @@ class ParserTest {
|
|||
val selector = Selector.parse(source)
|
||||
println("check_quote:$selector")
|
||||
|
||||
// https://i.gkd.li/import/13247733
|
||||
// 1->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())
|
||||
}
|
||||
|
|
|
@ -15,5 +15,9 @@ data class TestNode(
|
|||
|
||||
@Transient
|
||||
var children: MutableList<TestNode> = mutableListOf()
|
||||
|
||||
override fun toString(): String {
|
||||
return id.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user