xrfragment-haxe/src/xrfragment/URI.hx

506 lines
15 KiB
Haxe
Raw Normal View History

2023-05-23 11:30:39 +02:00
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 Leon van Kammen/NLNET
2024-04-10 16:38:50 +00:00
/*
* various snippets originate from:
*
* http://haxe.org/doc/snip/uri_parser,
* https://github.com/haxecocktail/cocktail-url/blob/master/cocktail/url/URI.hx
*/
2023-03-09 22:32:28 +01:00
package xrfragment;
2023-03-31 12:58:53 +02:00
import xrfragment.Parser;
2023-04-14 14:45:50 +02:00
import xrfragment.XRF;
2023-03-31 12:58:53 +02:00
2023-04-02 21:19:03 +02:00
/**
2023-06-27 12:15:04 +02:00
* # Spec
2023-04-02 21:22:22 +02:00
*
2023-06-27 12:15:04 +02:00
* > 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
2023-04-06 18:27:49 +02:00
*
* ```
2024-02-14 15:47:34 +00:00
* reserved = gen-delims / sub-delims / xrf-scheme
2023-04-06 18:27:49 +02:00
* gen-delims = "#" / "&"
2023-06-27 12:15:04 +02:00
* sub-delims = "," / "="
2024-02-14 15:47:34 +00:00
* xrf-scheme = "xrf://"
2023-04-06 18:27:49 +02:00
* ```
2023-04-06 18:29:53 +02:00
*
2023-06-27 12:15:04 +02:00
* 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:
2023-04-06 18:30:36 +02:00
*
2023-04-02 21:19:03 +02:00
*/
@:expose // <- makes the class reachable from plain JavaScript
@:keep // <- avoids accidental removal by dead code elimination
2023-03-31 14:47:54 +02:00
class URI {
2024-03-29 17:36:48 +00:00
2024-04-10 16:38:50 +00:00
/**
* URI parts names
*/
private static var _parts : Array<String> = ["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<Dynamic> = {};
public var XRF : haxe.DynamicAccess<Dynamic> = {};
public var URN : String;
/**
* class constructor
*/
public function new( )
{
}
2024-03-29 17:36:48 +00:00
2023-04-02 21:19:03 +02:00
@:keep
2024-04-10 16:38:50 +00:00
public static function parseFragment(url:String,filter:Int):haxe.DynamicAccess<Dynamic> {
2023-06-27 12:15:04 +02:00
var store:haxe.DynamicAccess<Dynamic> = {}; // 1. store key/values into a associative array or dynamic object
2023-06-10 14:43:07 +02:00
if( url == null || url.indexOf("#") == -1 ) return store;
var fragment:Array<String> = url.split("#"); // 1. fragment URI starts with `#`
2023-04-02 21:19:03 +02:00
var splitArray:Array<String> = fragment[1].split('&'); // 1. fragments are split by `&`
for (i in 0...splitArray.length) { // 1. loop thru each fragment
2023-03-31 14:40:24 +02:00
2023-04-02 21:19:03 +02:00
var splitByEqual = splitArray[i].split('='); // 1. for each fragment split on `=` to separate key/values
2023-03-31 12:58:53 +02:00
var regexPlus = ~/\+/g; // 1. fragment-values are urlencoded (space becomes `+` using `encodeUriComponent` e.g.)
var key:String = splitByEqual[0];
2023-04-14 17:37:33 +02:00
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(" "));
2023-03-09 22:32:28 +01:00
}
2023-11-16 14:50:57 +01:00
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);
2023-04-14 17:37:33 +02:00
}
2023-04-14 14:45:50 +02:00
}
}
return store;
2023-03-09 22:32:28 +01:00
}
2024-02-08 19:40:43 +01:00
@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("#");
}
2024-02-14 15:47:34 +00:00
2024-04-10 16:38:50 +00:00
/**
* 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 = "/" + 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;
}
2024-04-16 18:40:49 +02:00
url.host = "";
2024-04-10 16:38:50 +00:00
}
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<Dynamic> = 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<String> = 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;
2024-04-16 13:19:08 +00:00
resultURI.source = newUrl;
2024-04-10 16:38:50 +00:00
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)
{
if( newUrl.charAt(0) != '/' && newUrl.indexOf("://") == -1 ){
var stripRelative : EReg = ~/\.\/.*/;
directory = stripRelative.replace( directory, '');
2024-04-10 16:38:50 +00:00
directory += newURI.directory;
}else{
directory = newURI.directory;
}
}
resultURI.directory = directory;
if (newURI.file != null)
{
resultURI.file = newURI.file;
2024-04-16 18:40:49 +02:00
}else{
resultURI.file = url.file;
2024-04-10 16:38:50 +00:00
}
2024-04-10 16:38:50 +00:00
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;
}
2023-03-09 22:32:28 +01:00
}
2023-06-27 12:15:04 +02:00
/**
* > 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.
*/