diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml
new file mode 100644
index 0000000..eaf28fd
--- /dev/null
+++ b/.github/workflows/website.yml
@@ -0,0 +1,21 @@
+name: Deploy to GitHub Pages
+on:
+ push:
+ branches:
+ - main
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ ref: main
+ - name: Build
+ run: cp test/test.html doc/.
+ working-directory: ./
+ - name: Deploy to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./doc
diff --git a/src/xrfragment/Parser.hx b/src/xrfragment/Parser.hx
index 91566e5..1fcd70c 100644
--- a/src/xrfragment/Parser.hx
+++ b/src/xrfragment/Parser.hx
@@ -1,11 +1,6 @@
package xrfragment;
-/**
- * # XR Fragments (key/value params)
- *
- * > ⛁ = define in 3D asset-file (as custom property or default projection)
- * > ☇ = mutable, using navigator URI (`document.location.href` e.g.)
- */
+import xrfragment.XRF;
@:expose // <- makes the class reachable from plain JavaScript
@:keep // <- avoids accidental removal by dead code elimination
@@ -14,14 +9,51 @@ class Parser {
@:keep
public static function parse(key:String,value:String,resultMap:haxe.DynamicAccess):Bool {
- var Frag:Map = new Map(); // | param | type | scope(s) | category | notes |
- // |---------|---------------|-------|--------------------|---------------------------------|
- Frag.set("prio", Type.isInt); // | prio | int (-10..1) | ⛁ | Asset loading / linking | $(cat doc/notes/prio.md) |
- Frag.set("pos", Type.isVector); // | pos | 3D vector | ⛁ ☇ |HREF navigation/portals | |
- Frag.set("q", Type.isString); // | q | string | ⛁ |Query Selector | |
- var vec:String = "1,2,3"; //
- //if( Type.isVector(vec) ) trace("ja");
+ // here we define allowed characteristics & datatypes for each fragment (stored as bitmasked int for performance purposes)
+ var Frag:Map = new Map();
+
+ // category: asset loading linking
+ Frag.set("prio", XRF.ASSET_OBJ | XRF.T_INT );
+ Frag.set("#", XRF.ASSET | XRF.T_PREDEFINED_VIEW );
+ Frag.set("class", XRF.ASSET_OBJ | XRF.T_STRING );
+ Frag.set("src", XRF.ASSET_OBJ | XRF.T_URL );
+ Frag.set("src_audio", XRF.ASSET_OBJ | XRF.T_URL );
+ Frag.set("src_shader", XRF.ASSET_OBJ | XRF.T_URL );
+ Frag.set("src_env", XRF.ASSET | XRF.T_URL );
+ Frag.set("src_env_audio", XRF.ASSET | XRF.T_URL );
+
+ // category: href navigation / portals / teleporting
+ Frag.set("pos", XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_VECTOR3 | XRF.T_STRING_OBJ );
+ Frag.set("href", XRF.ASSET_OBJ | XRF.T_URL | XRF.T_PREDEFINED_VIEW );
+
+ // category: query selector | object manipulation
+ Frag.set("q", XRF.PV_OVERRIDE | XRF.T_STRING );
+ Frag.set("scale", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_INT );
+ Frag.set("rot", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_VECTOR3 );
+ Frag.set("translate", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_VECTOR3 );
+ Frag.set("visible", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_INT );
+
+ // category: animation
+ Frag.set("t", XRF.ASSET | XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_VECTOR2 | XRF.BROWSER_OVERRIDE );
+ Frag.set("gravity", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_VECTOR3 );
+ Frag.set("physics", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_VECTOR3 );
+ Frag.set("scroll", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_STRING );
+
+ // category: device / viewport settings
+ Frag.set("fov", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_INT | XRF.BROWSER_OVERRIDE );
+ Frag.set("clip", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_VECTOR2 | XRF.BROWSER_OVERRIDE );
+ Frag.set("fog", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_STRING | XRF.BROWSER_OVERRIDE );
+
+ // category: author / metadata
+ Frag.set("namespace", XRF.ASSET | XRF.T_STRING );
+ Frag.set("SPFX", XRF.ASSET | XRF.T_STRING );
+ Frag.set("unit", XRF.ASSET | XRF.T_STRING );
+ Frag.set("description", XRF.ASSET | XRF.T_STRING );
+
+ // category: multiparty
+ Frag.set("src_session", XRF.ASSET | XRF.T_URL | XRF.PV_OVERRIDE | XRF.BROWSER_OVERRIDE | XRF.PROMPT );
+
/**
* # XR Fragments parser
*
@@ -29,65 +61,19 @@ class Parser {
* the gist of it:
*/
if( Frag.exists(key) ){ // 1. check if param exist
- if( Frag.get(key).match(value) ){ // 1. each key has a regex to validate its value-type (see regexes)
- var v:Value = new Value();
- guessType(v, value); // 1. extract the type
- // process multiple values
- if( value.split("|").length > 1 ){ // 1. use `|` on stringvalues, to split multiple values
- v.args = new Array();
- var args:Array = value.split("|");
- for( i in 0...args.length){
- var x:Value = new Value();
- guessType(x, args[i]); // 1. for each multiple value, guess the type
- v.args.push( x );
- }
- }
- resultMap.set(key, v );
- }else { trace("[ i ] fragment '"+key+"' has incompatible value ("+value+")"); return false; }
- }else { trace("[ i ] fragment '"+key+"' does not exist or has no type defined (yet)"); return false; }
+ var v:XRF = new XRF(key, Frag.get(key));
+ if( !v.validate(value) ){
+ trace("[ i ] fragment '"+key+"' has incompatible value ("+value+")");
+ return false;
+ }
+ resultMap.set(key, v );
+ }else{ trace("[ i ] fragment '"+key+"' does not exist or has no type typed (yet)"); return false; }
return true;
}
- @:keep
- public static function guessType(v:Value, str:String):Void {
- v.string = str;
- if( str.split(",").length > 1){ // 1. `,` assumes 1D/2D/3D vector-values like x[,y[,z]]
- var xyz:Array = str.split(","); // 1. parseFloat(..) and parseInt(..) is applied to vector/float and int values
- if( xyz.length > 0 ) v.x = Std.parseFloat(xyz[0]); // 1. anything else will be treated as string-value
- if( xyz.length > 1 ) v.y = Std.parseFloat(xyz[1]); // 1. incompatible value-types will be dropped / not used
- if( xyz.length > 2 ) v.y = Std.parseFloat(xyz[2]); //
- } // > the xrfragment specification should stay simple enough
- // > for anyone to write a parser using either regexes or grammar/lexers
- if( Type.isColor.match(str) ) v.color = str; // > therefore expressions/comprehensions are not supported (max wildcard/comparison operators for queries e.g.)
- if( Type.isFloat.match(str) ) v.float = Std.parseFloat(str);
- if( Type.isInt.match(str) ) v.int = Std.parseInt(str);
- }
+}
-}
- // # Parser Value types
- //
- // | type | info | format | example |
-class Value { // |------|------|--------|----------------------------------|
- public var x:Float; // |vector| x,y,z| comma-separated | #pos=1,2,3 |
- public var y:Float;
- public var z:Float;
- public var color:String; // |string| color| FFFFFF (hex) | #fog=5m,FFAACC |
- public var string:String; // |string| | | #q=-sun |
- public var int:Int; // |int | | [-]x[xxxxx] | #price:>=100 |
- public var float:Float; // |float | | [-]x[.xxxx] (ieee)| #prio=-20 |
- public var args:Array; // |array | mixed| \|-separated | #pos=0,0,0\|90,0,0 |
- public function new(){} //
- // > rule for thumb: type-limitations will piggyback JSON limitations (IEEE floatsize e.g.)
-}
- // Regexes:
-class Type { //
- static public var isColor:EReg = ~/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; // 1. hex colors are detected using regex `/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/`
- static public var isInt:EReg = ~/^[0-9]+$/; // 1. integers are detected using regex `/^[0-9]+$/`
- static public var isFloat:EReg = ~/^[0-9]+\.[0-9]+$/; // 1. floats are detected using regex `/^[0-9]+\.[0-9]+$/`
- static public var isVector:EReg = ~/([,]+|\w)/; // 1. vectors are detected using regex `/[,]/` (but can also be an string referring to an entity-ID in the asset)
- static public var isString:EReg = ~/.*/; // 1. anything else is string `/.*/`
-}
/// # Tests
///
diff --git a/src/xrfragment/Parser.lua b/src/xrfragment/Parser.lua
new file mode 100644
index 0000000..8732f86
--- /dev/null
+++ b/src/xrfragment/Parser.lua
@@ -0,0 +1,95 @@
+XF = {}
+
+function split (inputstr, sep)
+ if sep == nil then
+ sep = "%s"
+ end
+ local t={}
+ for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
+ table.insert(t, str)
+ end
+ return t
+end
+
+function XF.parse(key, value, resultMap)
+ local frag = {}
+ frag["prio"] = "^%d+$"
+ frag["pos"] = "(%d+),(%d+),(%d+)"
+ frag["q"] = ".+"
+
+ if frag[key] ~= nil then
+ local regex = frag[key]
+ if string.match(value, regex) then
+ local v = Value:new()
+ XF.guessType(v, value)
+ if string.find(value, "|") then
+ v.args = {}
+ local args = split(value, "|")
+ for k, arg in ipairs(args) do
+ local x = Value:new()
+ XF.guessType(x, arg)
+ table.insert(v.args, x)
+ end
+ end
+ resultMap[key] = v;
+ else
+ error("[ i ] fragment '"..key.."' has incompatible value ("..value..")")
+ return false
+ end
+ else
+ error("[ i ] fragment '"..key.."' does not exist or has no type defined (yet)")
+ return false
+ end
+ return true
+end
+
+function XF.guessType(v, str)
+ v.string = str
+ local parts = split(str, ",")
+ if #parts > 1 then
+ v.x = tonumber(parts[1])
+ v.y = tonumber(parts[2])
+ if #parts > 2 then
+ v.z = tonumber(parts[3])
+ end
+ end
+
+ if string.match(str, Type.isColor) then
+ v.color = str
+ end
+ if string.match(str, Type.isFloat) then
+ v.float = tonumber(str)
+ end
+ if string.match(str, Type.isInt) then
+ v.int_val = tonumber(str)
+ end
+end
+
+Value = {}
+
+function Value:new()
+ local obj = {}
+ obj.x = nil
+ obj.y = nil
+ obj.z = nil
+ obj.color = nil
+ obj.string = nil
+ obj.int_val = nil
+ obj.float = nil
+ obj.args = nil
+ setmetatable(obj, self)
+ self.__index = self
+ return obj
+end
+
+Type = {}
+Type.isColor = "^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$"
+Type.isInt = "^[0-9]+$"
+Type.isFloat = "^[0-9]+%.[0-9]+$"
+Type.isVector = "([,]+|%w)"
+
+local map = {}
+XF.parse("pos","1,2,3",map)
+print(map.pos.z)
+XF.parse("pos","1,2,3|3,4,5",map)
+print(map.pos.args[2].z)
diff --git a/src/xrfragment/Query.hx b/src/xrfragment/Query.hx
index c3fa92f..65482b0 100644
--- a/src/xrfragment/Query.hx
+++ b/src/xrfragment/Query.hx
@@ -59,6 +59,10 @@ class Query {
return classAlias.match(token) ? StringTools.replace(token,".","class:") : token;
}
+ public function get() : Dynamic {
+ return this.q;
+ }
+
public function parse(str:String,recurse:Bool = false) : Dynamic {
var token = str.split(" ");
@@ -103,8 +107,7 @@ class Query {
}
}
for( i in 0...token.length ) process( expandAliases(token[i]) );
- this.q = q;
- return this.q;
+ return this.q = q;
}
@:keep
diff --git a/src/xrfragment/URI.hx b/src/xrfragment/URI.hx
index 0f20597..04c5c94 100644
--- a/src/xrfragment/URI.hx
+++ b/src/xrfragment/URI.hx
@@ -1,6 +1,7 @@
package xrfragment;
import xrfragment.Parser;
+import xrfragment.XRF;
/**
*
@@ -41,7 +42,7 @@ import xrfragment.Parser;
@:keep // <- avoids accidental removal by dead code elimination
class URI {
@:keep
- public static function parse(qs:String):haxe.DynamicAccess {
+ public static function parse(qs:String,browser_override:Bool):haxe.DynamicAccess {
var fragment:Array = qs.split("#"); // 1. fragment URI starts with `#`
var splitArray:Array = fragment[1].split('&'); // 1. fragments are split by `&`
var resultMap:haxe.DynamicAccess = {}; // 1. store key/values into a associative array or dynamic object
@@ -56,6 +57,12 @@ class URI {
var ok:Bool = Parser.parse(key,value,resultMap); // 1. every recognized fragment key/value-pair is added to a central map/associative array/object
}
}
+ if( browser_override ){
+ for (key in resultMap.keys()) {
+ var xrf:XRF = resultMap.get(key);
+ if( !xrf.is( XRF.BROWSER_OVERRIDE ) ) resultMap.remove(key);
+ }
+ }
return resultMap;
}
}
diff --git a/src/xrfragment/XRF.hx b/src/xrfragment/XRF.hx
new file mode 100644
index 0000000..7dfe7d6
--- /dev/null
+++ b/src/xrfragment/XRF.hx
@@ -0,0 +1,107 @@
+package xrfragment;
+
+class XRF {
+
+ /*
+ * this class represents a fragment (value)
+ */
+
+ // public static inline readonly ASSET
+ public static var ASSET:Int = 1; // fragment is immutable (typed in asset) globally
+ public static var ASSET_OBJ:Int = 2; // fragment is immutable (typed in object in asset)
+ public static var PV_OVERRIDE:Int = 4; // fragment can be overriden when specified in predefined view value
+ public static var QUERY_OPERATOR = 8; // fragment will be applied to result of queryselecto
+ public static var PROMPT:Int = 16; // ask user whether this fragment value can be changed
+ public static var ROUNDROBIN:Int = 32; // evaluation of this (multi) value can be roundrobined
+ public static var BROWSER_OVERRIDE:Int = 64; // fragment can be overriden by (manual) browser URI change
+
+ // highlevel types
+ public static var T_COLOR:Int = 128;
+ public static var T_INT:Int = 256;
+ public static var T_FLOAT:Int = 512;
+ public static var T_VECTOR2:Int = 1024;
+ public static var T_VECTOR3:Int = 2048;
+ public static var T_URL:Int = 4096;
+ public static var T_PREDEFINED_VIEW:Int = 8192;
+ public static var T_STRING:Int = 16384;
+ public static var T_STRING_OBJ:Int = 32768;
+
+ // regexes
+ public static var isColor:EReg = ~/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; // 1. hex colors are detected using regex `/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/`
+ public static var isInt:EReg = ~/^[0-9]+$/; // 1. integers are detected using regex `/^[0-9]+$/`
+ public static var isFloat:EReg = ~/^[0-9]+\.[0-9]+$/; // 1. floats are detected using regex `/^[0-9]+\.[0-9]+$/`
+ public static var isVector:EReg = ~/([,]+|\w)/; // 1. vectors are detected using regex `/[,]/` (but can also be an string referring to an entity-ID in the asset)
+ public static var isUrl:EReg = ~/(:\/\/)?\..*/; // 1. url/file */`
+ public static var isUrlOrPretypedView:EReg = ~/(^#|:\/\/)?\..*/; // 1. url/file */`
+ public static var isString:EReg = ~/.*/; // 1. anything else is string `/.*/`
+
+ // value holder(s) // |------|------|--------|----------------------------------|
+ public var fragment:String;
+ public var flags:Int;
+ public var x:Float; // |vector| x,y,z| comma-separated | #pos=1,2,3 |
+ public var y:Float;
+ public var z:Float;
+ public var color:String; // |string| color| FFFFFF (hex) | #fog=5m,FFAACC |
+ public var string:String; // |string| | | #q=-sun |
+ public var int:Int; // |int | | [-]x[xxxxx] | #price:>=100 |
+ public var float:Float; // |float | | [-]x[.xxxx] (ieee)| #prio=-20 |
+ public var args:Array; // |array | mixed| \|-separated | #pos=0,0,0\|90,0,0 |
+ public var query:Query;
+ //
+ public function new(_fragment:String,_flags:Int){
+ fragment = _fragment;
+ flags = _flags;
+ }
+
+ public function is(flag:Int):Bool {
+ return (flags & flag) != 0;
+ }
+
+ public static function set(flag:Int, flags:Int):Int {
+ return flags | flag;
+ }
+
+ public static function unset(flag:Int, flags:Int):Int {
+ return flags & ~flag;
+ }
+
+ public function validate(value:String) : Bool{
+ guessType(this, value); // 1. extract the type
+ // process multiple values
+ if( value.split("|").length > 1 ){ // 1. use `|` on stringvalues, to split multiple values
+ this.args = new Array();
+ var args:Array = value.split("|");
+ for( i in 0...args.length){
+ var x:XRF = new XRF(fragment,flags);
+ guessType(x, args[i]); // 1. for each multiple value, guess the type
+ this.args.push( x );
+ }
+ }
+ // special case: query has its own DSL (*TODO* allow fragments to have custom validators)
+ if( fragment == "q" ) query = (new Query(value)).get();
+ // validate
+ var ok:Bool = true;
+ if( !Std.isOfType(args,Array) ){
+ if( is(T_VECTOR3) && !(Std.isOfType(x,Float) && Std.isOfType(y,Float) && Std.isOfType(z,Float)) ) ok = false;
+ if( is(T_VECTOR2) && !(Std.isOfType(x,Float) && Std.isOfType(y,Float)) ) ok = false;
+ if( is(T_INT) && !Std.isOfType(int,Int) ) ok = false;
+ }
+ return ok;
+ }
+
+ @:keep
+ public function guessType(v:XRF, str:String):Void {
+ v.string = str;
+ if( str.split(",").length > 1){ // 1. `,` assumes 1D/2D/3D vector-values like x[,y[,z]]
+ var xyz:Array = str.split(","); // 1. parseFloat(..) and parseInt(..) is applied to vector/float and int values
+ if( xyz.length > 0 ) v.x = Std.parseFloat(xyz[0]); // 1. anything else will be treated as string-value
+ if( xyz.length > 1 ) v.y = Std.parseFloat(xyz[1]); // 1. incompatible value-types will be dropped / not used
+ if( xyz.length > 2 ) v.z = Std.parseFloat(xyz[2]); //
+ } // > the xrfragment specification should stay simple enough
+ // > for anyone to write a parser using either regexes or grammar/lexers
+ if( isColor.match(str) ) v.color = str; // > therefore expressions/comprehensions are not supported (max wildcard/comparison operators for queries e.g.)
+ if( isFloat.match(str) ) v.float = Std.parseFloat(str);
+ if( isInt.match(str) ) v.int = Std.parseInt(str);
+ }
+
+}
diff --git a/src/xrfragment/XRF.lua b/src/xrfragment/XRF.lua
new file mode 100644
index 0000000..2f9701f
--- /dev/null
+++ b/src/xrfragment/XRF.lua
@@ -0,0 +1,112 @@
+local XRF = {}
+
+XRF.ASSET = 1
+XRF.ASSET_OBJ = 2
+XRF.PV_OVERRIDE = 4
+XRF.QUERY_OPERATOR = 8
+XRF.PROMPT = 16
+XRF.ROUNDROBIN = 32
+XRF.BROWSER_OVERRIDE = 64
+XRF.T_COLOR = 128
+XRF.T_INT = 256
+XRF.T_FLOAT = 512
+XRF.T_VECTOR2 = 1024
+XRF.T_VECTOR3 = 2048
+XRF.T_URL = 4096
+XRF.T_PREDEFINED_VIEW = 8192
+XRF.T_STRING = 16384
+XRF.T_STRING_OBJ = 32768
+
+XRF.isColor = string.match("#([A-fa-f0-9]{6}|[A-fa-f0-9]{3})", "")
+XRF.isInt = string.match("^[0-9]+$", "")
+XRF.isFloat = string.match("^[0-9]+%.[0-9]+$", "")
+XRF.isVector = string.match("([,]+|%w)", "")
+XRF.isUrl = string.match("(://)?..*", "")
+XRF.isUrlOrPretypedView = string.match("(^#|://)?..*", "")
+XRF.isString = string.match(".+", "")
+
+function XRF.new(_fragment, _flags)
+ local self = {}
+ self.fragment = _fragment
+ self.flags = _flags
+ self.x = 0
+ self.y = 0
+ self.z = 0
+ self.color = ""
+ self.string = ""
+ self.int = 0
+ self.float = 0
+ self.args = {}
+ self.query = {}
+ function self.is(flag)
+ print(flag)
+ print(self.flags)
+ return self.flags & flag ~= 0
+ end
+ return self
+end
+
+function XRF.set(flag, flags)
+ return flags | flag
+end
+
+function XRF.unset(flag, flags)
+ return flags & ~flag
+end
+
+function XRF.validate(self, value)
+ XRF.guessType(self, value)
+ if string.len(value:split("|")) then
+ self.args = {}
+ for i, val in ipairs(value:split("|")) do
+ local xrf = XRF.new(self.fragment, self.flags)
+ XRF.guessType(xrf, val)
+ table.insert(self.args, xrf)
+ end
+ end
+ if self.fragment == "q" then
+ --self.query = (new Query(value)):get()
+ end
+ if not isinstance(self.args, 'array') then
+ if self:is(XRF.T_VECTOR3) and (not isinstance(self.x, 'number') or not isinstance(self.y, 'number') or not isinstance(self.z, 'number')) then
+ ok = false
+ end
+ if self:is(XRF.T_VECTOR2) and (not isinstance(self.x, 'number') or not isinstance(self.y, 'number')) then
+ ok = false
+ end
+ if self:is(XRF.T_INT) and not isinstance(self.int, 'number') then
+ ok = false
+ end
+ end
+ return ok
+end
+
+function XRF.guessType(v, str)
+ v.string = str
+ if string.len(str:split(",")) > 1 then
+ local xyz = string.split(str, ",")
+ if #xyz > 0 then
+ v.x = tonumber(xyz[1])
+ end
+ if #xyz > 1 then
+ v.y = tonumber(xyz[2])
+ end
+ if #xyz > 2 then
+ v.z = tonumber(xyz[3])
+ end
+ end
+ if XRF.isColor:match(str) then
+ v.color = str
+ end
+ if XRF.isFloat:match(str) then
+ v.float = tonumber(str)
+ end
+ if XRF.isInt:match(str) then
+ v.int = tonumber(str)
+ end
+end
+
+x = XRF.new("pos", XRF.ASSET | XRF.ROUNDROBIN )
+print( "roundrobin: " .. ( x.is( XRF.ROUNDROBIN ) and "ja" or "nee" ) )
+
+return XRF
diff --git a/test/test.html b/test/test.html
new file mode 100644
index 0000000..abe04fe
--- /dev/null
+++ b/test/test.html
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+