176 lines
7.2 KiB
Haxe
176 lines
7.2 KiB
Haxe
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 Leon van Kammen/NLNET
|
|
package xrfragment;
|
|
|
|
//return untyped __js__("window.location.search");
|
|
|
|
#if js
|
|
var ok:Bool = js.Syntax.code('
|
|
// haxe workarounds
|
|
Array.prototype.contains = Array.prototype.includes
|
|
|
|
if (typeof Array.prototype.remove !== "function") {
|
|
Array.prototype.remove = function (item) {
|
|
const oldLength = this.length
|
|
let newLength = 0
|
|
|
|
for (let i = 0; i < oldLength; i++) {
|
|
const entry = this[i]
|
|
if (entry === item) {
|
|
let newLength = i++
|
|
|
|
while (i !== this.length) {
|
|
const entry = this[i]
|
|
if (entry !== item) this[newLength++] = entry
|
|
i++
|
|
}
|
|
|
|
this.length = newLength
|
|
for (let i = newLength; i < oldLength; i++) delete this[i]
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
');
|
|
#end
|
|
|
|
@:expose // <- makes the class reachable from plain JavaScript
|
|
@:keep // <- avoids accidental removal by dead code elimination
|
|
class Filter {
|
|
|
|
/**
|
|
* # Spec
|
|
*
|
|
* > version 1.0.0 [](https://github.com/coderofsalvation/xrfragment/actions) generated by `make doc` @ $(date +"%Y-%m-%dT%H:%M:%S%z")
|
|
*
|
|
* In case your programming language has no parser ([check here](https://github.com/coderofsalvation/xrfragment/tree/main/dist)) you can [crosscompile it](https://github.com/coderofsalvation/xrfragment/blob/main/build.hxml), or roll your own `Filter.parse(str)` using the spec:
|
|
*/
|
|
|
|
// 1. requirement: receive arguments: filter (string)
|
|
|
|
private var str:String = "";
|
|
private var q:haxe.DynamicAccess<Dynamic> = {}; // 1. create an associative array/object to store filter-arguments as objects
|
|
private var isProp:EReg = ~/^.*=[><=!]?/; // 1. detect object id's & properties `foo=1` and `foo` (reference regex= `/^.*=[><=!]?/` )
|
|
private var isExclude:EReg = ~/^-/; // 1. detect excluders like `-foo`,`-foo=1`,`-.foo`,`-/foo` (reference regex= `/^-/` )
|
|
private var isRoot:EReg = ~/^[-]?\//; // 1. detect root selectors like `/foo` (reference regex= `/^[-]?\//` )
|
|
private var isNumber:EReg = ~/^[0-9\.]+$/; // 1. detect number values like `foo=1` (reference regex= `/^[0-9\.]+$/` )
|
|
private var isDeepSelect:EReg = ~/(^-|\*$)/; // 1. detect nested keys like 'foo*' (reference regex= `/\*$/` )
|
|
private var isSelectorExclude:EReg = ~/^-/; // 1. detect exclude keys like `-foo` (reference regex= `/^-/` )
|
|
|
|
public function new(str:String){
|
|
if( str != null ) this.parse(str);
|
|
}
|
|
|
|
public function toObject() : Dynamic {
|
|
return Reflect.copy(this.q);
|
|
}
|
|
|
|
public function get() : Dynamic {
|
|
return Reflect.copy(this.q);
|
|
}
|
|
|
|
public function parse(str:String) : Dynamic {
|
|
|
|
var token = str.split(" ");
|
|
var q:haxe.DynamicAccess<Dynamic> = {};
|
|
|
|
function process(str,prefix = ""){
|
|
str = StringTools.trim(str);
|
|
var k:String = str.split("=")[0]; // 1. for every filter token split string on `=`
|
|
var v:String = str.split("=")[1];
|
|
// retrieve existing filter if any
|
|
var filter:haxe.DynamicAccess<Dynamic> = {};
|
|
if( q.get(prefix+k) ) filter = q.get(prefix+k);
|
|
|
|
if( isProp.match(str) ){ // 1. <b>WHEN</b></b> when a `:` key/value is detected:
|
|
var oper:String = "";
|
|
if( str.indexOf("*") != -1 ) oper = "*"; // 1. then scan for `*` operator (means include all objects for [src](#src) embedded fragment)
|
|
if( str.indexOf(">") != -1 ) oper = ">"; // 1. then scan for `>` operator
|
|
if( str.indexOf("<") != -1 ) oper = "<"; // 1. then scan for `<` operator
|
|
if( isExclude.match(k) ){
|
|
k = k.substr(1); // 1. then strip key-operator: convert "-foo" into "foo"
|
|
}
|
|
v = v.substr(oper.length); // 1. then strip value operator: change value ">=foo" into "foo"
|
|
if( oper.length == 0 ) oper = "=";
|
|
var rule:haxe.DynamicAccess<Dynamic> = {};
|
|
if( isNumber.match(v) ) rule[ oper ] = Std.parseFloat(v);
|
|
else rule[oper] = v;
|
|
q.set('expr',rule);
|
|
}else{ // 1. <b>ELSE </b> we are dealing with an object
|
|
q.set("root", isRoot.match(str) ? true : false ); // 1. and we set `root` to `true` or `false` (true=`/` root selector is present)
|
|
}
|
|
q.set("show", isExclude.match(str) ? false : true ); // 1. therefore we we set `show` to `true` or `false` (false=excluder `-`)
|
|
q.set("deep", isDeepSelect.match(k) ? true : false ); // 1. set `deep` (for objectnames with * suffix or negative selectors)
|
|
q.set("key", isDeepSelect.replace(k,'') );
|
|
q.set("value",v);
|
|
}
|
|
for( i in 0...token.length ) process( token[i] );
|
|
return this.q = q;
|
|
}
|
|
|
|
@:keep
|
|
public function test( ?obj:Dynamic ):Bool{
|
|
var qualify:Bool = false;
|
|
// first apply includes, then excludes
|
|
for ( k in Reflect.fields(obj) ){
|
|
var v:String = Std.string( Reflect.field(obj,k) );
|
|
if( testProperty( k, v) ) qualify = true;
|
|
}
|
|
for ( k in Reflect.fields(obj) ){
|
|
var v:String = Std.string( Reflect.field(obj,k) );
|
|
if( testProperty( k, v, true) ) qualify = false;
|
|
}
|
|
return qualify;
|
|
}
|
|
|
|
/*
|
|
* this is a utility function of a filter which helps
|
|
* in telling if a property qualifies according to this filter object
|
|
*/
|
|
@:keep
|
|
public function testProperty( property:String, value:String, ?exclude:Bool ):Bool{
|
|
var conds:Int = 0;
|
|
var fails:Int = 0;
|
|
var qualify:Int = 0;
|
|
|
|
var testprop = function(expr:Bool) : Bool {
|
|
conds+=1;
|
|
fails+= expr ? 0 : 1;
|
|
return expr;
|
|
}
|
|
|
|
// class or id
|
|
if( q[value] != null ){
|
|
var v:haxe.DynamicAccess<Dynamic> = q[value];
|
|
if( v.get(property) != null ) return v.get(property);
|
|
}
|
|
|
|
// conditional rules
|
|
if( Reflect.field(q,'expr') ){
|
|
var f:Dynamic = Reflect.field(q,'expr');
|
|
|
|
if( !Reflect.field(q,'show') ){
|
|
if( Reflect.field(f,'!=') != null && testprop( Std.string(value) == Std.string(Reflect.field(f,'!='))) && exclude ) qualify += 1;
|
|
}else{
|
|
if( Reflect.field(f,'*') != null && testprop( Std.parseFloat(value) != null ) ) qualify += 1;
|
|
if( Reflect.field(f,'>') != null && testprop( Std.parseFloat(value) > Std.parseFloat(Reflect.field(f,'>' )) ) ) qualify += 1;
|
|
if( Reflect.field(f,'<') != null && testprop( Std.parseFloat(value) < Std.parseFloat(Reflect.field(f,'<' )) ) ) qualify += 1;
|
|
if( Reflect.field(f,'=') != null && (
|
|
testprop( value == Reflect.field(f,'=')) ||
|
|
testprop( Std.parseFloat(value) == Std.parseFloat(Reflect.field(f,'=')))
|
|
)) qualify += 1;
|
|
}
|
|
}
|
|
return qualify > 0;
|
|
}
|
|
}
|
|
/**
|
|
* > icanhazcode? yes, see [Parser.hx](https://github.com/coderofsalvation/xrfragment/blob/main/src/xrfragment/Filter.hx)
|
|
*
|
|
* # Tests
|
|
*
|
|
* the spec is tested with [JSON unittests](./../src/spec) consumed by [Test.hx](./../src/Test.hx) to cross-test all languages.
|
|
*/
|