// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 Leon van Kammen/NLNET /* * various snippets originate from: * * http://haxe.org/doc/snip/uri_parser, * https://github.com/haxecocktail/cocktail-url/blob/master/cocktail/url/URI.hx */ package xrfragment; import xrfragment.Parser; import xrfragment.XRF; /** * # Spec * * > version 1.0.0 [![Actions Status](https://github.com/coderofsalvation/xrfragment/workflows/test/badge.svg)](https://github.com/coderofsalvation/xrfragment/actions) generated by `make doc` @ $(date +"%Y-%m-%dT%H:%M:%S%z") * * ### XR Fragment URI Grammar * * ``` * reserved = gen-delims / sub-delims / xrf-scheme * gen-delims = "#" / "&" * sub-delims = "," / "=" * xrf-scheme = "xrf://" * ``` * * 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 `Parser.parse(k,v,store)` using the spec: * */ @:expose // <- makes the class reachable from plain JavaScript @:keep // <- avoids accidental removal by dead code elimination class URI { /** * URI parts names */ private static var _parts : Array = ["source", "scheme", "authority", "userInfo", "user", "password","host","port","relative","path","directory","file","query","fragment"]; /** * URI parts */ public var url : String; public var source : String; public var scheme : String; public var authority : String; public var userInfo : String; public var user : String; public var password : String; public var host : String; public var port : String; public var relative : String; public var path : String; public var directory : String; public var file : String; public var fileExt : String; public var query : String; public var fragment : String = ""; public var hash : haxe.DynamicAccess = {}; public var XRF : haxe.DynamicAccess = {}; public var URN : String; /** * class constructor */ public function new( ) { } @:keep public static function parseFragment(url:String,filter:Int):haxe.DynamicAccess { var store:haxe.DynamicAccess = {}; // 1. store key/values into a associative array or dynamic object if( url == null || url.indexOf("#") == -1 ) return store; var fragment:Array = url.split("#"); // 1. fragment URI starts with `#` var splitArray:Array = fragment[1].split('&'); // 1. fragments are split by `&` for (i in 0...splitArray.length) { // 1. loop thru each fragment var splitByEqual = splitArray[i].split('='); // 1. for each fragment split on `=` to separate key/values var regexPlus = ~/\+/g; // 1. fragment-values are urlencoded (space becomes `+` using `encodeUriComponent` e.g.) var key:String = splitByEqual[0]; var value:String = ""; if (splitByEqual.length > 1) { if( XRF.isVector.match(splitByEqual[1]) ) value = splitByEqual[1]; else value = StringTools.urlDecode(regexPlus.split(splitByEqual[1]).join(" ")); } var ok:Bool = Parser.parse(key,value,store,i); // 1. for every recognized fragment key/value-pair call [Parser.parse](#%E2%86%AA%20Parser.parse%28k%2Cv%2Cstore%29) } if( filter != null && filter != 0 ){ for (key in store.keys()) { var xrf:XRF = store.get(key); if( !xrf.is( filter ) ){ store.remove(key); } } } return store; } @keep public static function template(uri:String, vars:Dynamic):String { var parts = uri.split("#"); if( parts.length == 1 ) return uri; // we only do level1 fragment expansion var frag = parts[1]; frag = StringTools.replace(frag,"{","::"); frag = StringTools.replace(frag,"}","::"); frag = new haxe.Template(frag).execute(vars); frag = StringTools.replace(frag,"null",""); // *TODO* needs regex to check for [#&]null[&] parts[1] = frag; return parts.join("#"); } /** * Parse a string url and return a typed * object from it. * * note : implementation originate from here : * http://haxe.org/doc/snip/uri_parser */ public static function parse(stringUrl:String, flags:Int ):URI { // The almighty regexp (courtesy of http://blog.stevenlevithan.com/archives/parseuri) var r : EReg = ~/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/; if( stringUrl.indexOf("://") == -1 && stringUrl.charAt(0) != '/' && stringUrl.charAt(0) != '#' ){ stringUrl = "/" + stringUrl; // workaround for relative urls } // Match the regexp to the url r.match(stringUrl); var url:URI = new URI(); // Use reflection to set each part for (i in 0..._parts.length) { Reflect.setField(url, _parts[i], r.matched(i)); } //hack for relative url with only a file if (isRelative(url) == true) { if (url.directory == null && url.host != null) { url.file = url.host; } url.host = ""; } url.hash = {}; if( url.fragment != null && url.fragment.length > 0 ){ url.XRF = xrfragment.URI.parseFragment( "#"+url.fragment, flags ); var key:String; for( key in url.XRF.keys() ){ var v:haxe.DynamicAccess = url.XRF.get(key); url.hash[key] = v.get("string"); } } computeVars(url); return url; } private static function computeVars( url:URI ) { // clean up url var r = ~/\/\//g; if( url.directory != null && url.directory.indexOf("//") != -1 ){ url.directory = r.replace(url.directory,"/"); } if( url.path != null && url.path.indexOf("//") != -1 ){ url.path = r.replace(url.path,"/"); } if( url.file != null && url.file.indexOf("//") != -1 ){ url.file = r.replace(url.file,"/"); } // generate URN url.URN = url.scheme + "://" + url.host; if( url.port != null ) url.URN += ":"+url.port; url.URN += url.directory; // extract file extension if any if( url.file != null){ var parts:Array = url.file.split("."); if( parts.length > 1 ){ url.fileExt = parts.pop(); } } } /** * Serialize an URl OBJect into an * URI string */ public static function toString(url:URI):String { var result:String = ""; if (url.scheme != null) { result += url.scheme + "://"; } if (url.user != null) { result += url.user + ":"; } if (url.password != null) { result += url.password + "@"; } if (url.host != null) { result += url.host; } if (url.port != null) { result += ":" + url.port; } if (url.directory != null) { result += url.directory; } if (url.file != null) { result += url.file; } if (url.query != null) { result += "?" + url.query; } if (url.fragment != null) { result += "#" + url.fragment; } return result; } /** * takes 2 urls and return a new url which is the result * of appending the second url to the first. * * if the first url points to a file, the file is removed * and the appended url is added after the last directory * * only the query string and fragment of the appended url are used */ public static function appendURI(url:URI, appendedURI:URI):URI { if (isRelative(url) == true) { return appendToRelativeURI(url, appendedURI); } else { return appendToAbsoluteURI(url, appendedURI); } } /** * return wether the url is relative (true) * or absolute (false) */ public static function isRelative(url:URI):Bool { return url.scheme == null; } /** * append the appended url to a relative url */ public static function appendToRelativeURI(url:URI, appendedURI:URI):URI { //when relative url parsed, if it contains only a file (ex : "style.css") //then it will store it in the host attribute. So if the url has no directory //then only the appended url content is returned, as this method replace the file //part of the base url anyway if (url.directory == null || url.host == null) { return cloneURI(appendedURI); } var resultURI:URI = new URI(); resultURI.host = url.host; resultURI.directory = url.directory; if (appendedURI.host != null) { resultURI.directory += appendedURI.host; } if (appendedURI.directory != null) { var directory = appendedURI.directory; if (appendedURI.host == null) { //remove the initial '/' char if no host, as already present //in base url resultURI.directory += directory.substr(1); } else { resultURI.directory += directory; } } if (appendedURI.file != null) { resultURI.file = appendedURI.file; } resultURI.path = resultURI.directory + resultURI.file; if (appendedURI.query != null) { resultURI.query = appendedURI.query; } if (appendedURI.fragment != null) { resultURI.fragment = appendedURI.fragment; } return resultURI; } /** * append the appended url to an absolute url */ public static function appendToAbsoluteURI(url:URI, appendedURI:URI):URI { var resultURI:URI = new URI(); if (url.scheme != null) { resultURI.scheme = url.scheme; } if (url.host != null) { resultURI.host = url.host; } var directory:String = ""; if (url.directory != null) { directory = url.directory; } if (appendedURI.host != null) { appendedURI.directory += appendedURI.host; } if (appendedURI.directory != null) { directory += appendedURI.directory; } resultURI.directory = directory; if (appendedURI.file != null) { resultURI.file = appendedURI.file; } resultURI.path = resultURI.directory + resultURI.file; if (appendedURI.query != null) { resultURI.query = appendedURI.query; } if (appendedURI.fragment != null) { resultURI.fragment = appendedURI.fragment; } return resultURI; } /** * append the appended url to an absolute url */ public static function toAbsolute(url:URI, newUrl:String ):URI { var newURI:URI = parse(newUrl,0); var resultURI:URI = new URI(); resultURI.port = url.port; resultURI.source = newUrl; if (newURI.scheme != null) { resultURI.scheme = newURI.scheme; }else{ resultURI.scheme = url.scheme; } if (newURI.host != null && newURI.host.length > 0 ) { resultURI.host = newURI.host; resultURI.port = null; resultURI.fragment = null; resultURI.hash = {}; resultURI.XRF = {}; if( newURI.port != null ){ resultURI.port = newURI.port; } }else{ resultURI.host = url.host; } var directory:String = ""; if (url.directory != null) { directory = url.directory; } if (newURI.directory != null && newURI.source.charAt(0) != "#") { if( newUrl.charAt(0) != '/' && newUrl.indexOf("://") == -1 ){ var stripRelative : EReg = ~/\.\/.*/; directory = stripRelative.replace( directory, ''); directory += newURI.directory; }else{ directory = newURI.directory; } } resultURI.directory = directory; trace("1:"+newURI.file); trace("2:"+url.file); if (newURI.file != null) { resultURI.file = newURI.file; }else{ resultURI.file = url.file; } trace("3:"+resultURI.file); resultURI.path = resultURI.directory + resultURI.file; if (newURI.query != null) { resultURI.query = newURI.query; } if (newURI.fragment != null) { resultURI.fragment = newURI.fragment; } resultURI.hash = newURI.hash; resultURI.XRF = newURI.XRF; computeVars(resultURI); return resultURI; } /** * clone the provided url */ private static function cloneURI(url:URI):URI { var clonedURI:URI = new URI(); clonedURI.url = url.url; clonedURI.source = url.source; clonedURI.scheme = url.scheme; clonedURI.authority = url.authority; clonedURI.userInfo = url.userInfo; clonedURI.password = url.password; clonedURI.host = url.host; clonedURI.port = url.port; clonedURI.relative = url.relative; clonedURI.path = url.path; clonedURI.directory = url.directory; clonedURI.file = url.file; clonedURI.query = url.query; clonedURI.fragment = url.fragment; return clonedURI; } } /** * > icanhazcode? yes, see [URI.hx](https://github.com/coderofsalvation/xrfragment/blob/main/src/xrfragment/URI.hx) * * # Tests * * the spec is tested with [JSON unittests](./../src/spec) consumed by [Test.hx](./../src/Test.hx) to cross-test all languages. */