diff --git a/src/RouteJs.AspNet/AttributeRouteFetcher.cs b/src/RouteJs.AspNet/AttributeRouteFetcher.cs index ca87c83..d23b31f 100644 --- a/src/RouteJs.AspNet/AttributeRouteFetcher.cs +++ b/src/RouteJs.AspNet/AttributeRouteFetcher.cs @@ -3,7 +3,7 @@ using System.Linq; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Internal; +//using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Template; diff --git a/src/RouteJs.AspNet/GlobalSuppressions.cs b/src/RouteJs.AspNet/GlobalSuppressions.cs new file mode 100644 index 0000000..9b548b4 --- /dev/null +++ b/src/RouteJs.AspNet/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0063:Utiliser une instruction 'using' simple")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0037:Utiliser un nom de membre déduit", Scope = "member", Target = "~M:RouteJs.RouteJs.GetJsonData~System.String")] diff --git a/src/RouteJs.AspNet/Properties/launchSettings.json b/src/RouteJs.AspNet/Properties/launchSettings.json new file mode 100644 index 0000000..bd4b3a5 --- /dev/null +++ b/src/RouteJs.AspNet/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:62667/", + "sslPort": 44316 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "RouteJs.AspNet": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/src/RouteJs.AspNet/RouteJs.AspNet.csproj b/src/RouteJs.AspNet/RouteJs.AspNet.csproj new file mode 100644 index 0000000..f67efe0 --- /dev/null +++ b/src/RouteJs.AspNet/RouteJs.AspNet.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp3.1 + + + + + + + + + + Always + + + Always + + + + + + + + + diff --git a/src/RouteJs.AspNet/RouteJsController.cs b/src/RouteJs.AspNet/RouteJsController.cs index abb2803..db54f26 100644 --- a/src/RouteJs.AspNet/RouteJsController.cs +++ b/src/RouteJs.AspNet/RouteJsController.cs @@ -3,11 +3,12 @@ using Microsoft.AspNetCore.Mvc; namespace RouteJs -{ - /// - /// ASP.NET MVC controller for RouteJs. Renders JavaScript to handle routing of ASP.NET URLs. - /// - public class RouteJsController : Controller +{ + /// + /// ASP.NET MVC controller for RouteJs. Renders JavaScript to handle routing of ASP.NET URLs. + /// + [ApiExplorerSettings(IgnoreApi = true)] + public class RouteJsController : Controller { /// /// How long to cache the JavaScript output for. Only used when a unique hash is present in the URL. diff --git a/src/RouteJs.AspNet/RouteJsHelper.cs b/src/RouteJs.AspNet/RouteJsHelper.cs index f1993ae..0546c8a 100644 --- a/src/RouteJs.AspNet/RouteJsHelper.cs +++ b/src/RouteJs.AspNet/RouteJsHelper.cs @@ -7,7 +7,8 @@ using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Html; - +using Microsoft.Extensions.Hosting; + namespace RouteJs { /// @@ -37,7 +38,7 @@ public class RouteJsHelper : IRouteJsHelper /// /// Hosting environment /// - private readonly IHostingEnvironment _env; + private readonly IWebHostEnvironment _env; private readonly IActionContextAccessor _actionContextAccessor; @@ -51,7 +52,7 @@ public class RouteJsHelper : IRouteJsHelper public RouteJsHelper( IUrlHelperFactory urlHelperFactory, IServiceProvider serviceProvider, - IHostingEnvironment env, + IWebHostEnvironment env, IActionContextAccessor actionContextAccessor) { _urlHelperFactory = urlHelperFactory; diff --git a/src/RouteJs.AspNet/TemplateRouteFetcher.cs b/src/RouteJs.AspNet/TemplateRouteFetcher.cs index 0165882..1f71bed 100644 --- a/src/RouteJs.AspNet/TemplateRouteFetcher.cs +++ b/src/RouteJs.AspNet/TemplateRouteFetcher.cs @@ -37,15 +37,18 @@ public TemplateRouteFetcher(IConstraintsProcessor constraintsProcessor, IRouteTe /// Processed route information public IEnumerable GetRoutes(RouteData routeData) { - var routeCollection = routeData.Routers.OfType().First(); - for (var i = 0; i < routeCollection.Count; i++) - { - var route = routeCollection[i]; - if (route is Route) - { - yield return ProcessTemplateRoute((Route)route); - } - } + var routeCollection = routeData.Routers.OfType().FirstOrDefault(); + if (routeCollection != null) + { + for (var i = 0; i < routeCollection.Count; i++) + { + var route = routeCollection[i]; + if (route is Route) + { + yield return ProcessTemplateRoute((Route)route); + } + } + } } /// diff --git a/src/RouteJs.AspNet/bundleconfig.json b/src/RouteJs.AspNet/bundleconfig.json new file mode 100644 index 0000000..5436586 --- /dev/null +++ b/src/RouteJs.AspNet/bundleconfig.json @@ -0,0 +1,8 @@ +[ + { + "outputFileName": "compiler/resources/router.min.js", + "inputFiles": [ + "compiler/resources/router.js" + ] + } +] diff --git a/src/RouteJs.AspNet/compiler/resources/router.js b/src/RouteJs.AspNet/compiler/resources/router.js new file mode 100644 index 0000000..a6602fb --- /dev/null +++ b/src/RouteJs.AspNet/compiler/resources/router.js @@ -0,0 +1,324 @@ +/*! + * RouteJs by Daniel Lo Nigro (Daniel15) - http://dl.vc/routejs + * Version {VERSION} + * Released under the BSD license. + */ +(function (window) { + 'use strict'; + + // Helper methods + function merge (first, second) { + /// + /// Return a new object that contains the properties from both of these objects. If a property + /// exists in both objects, the property in `second` will override the property in `first`. + /// + var result = {}, + key; + + for (key in first) { + if (first.hasOwnProperty(key)) { + result[key] = first[key]; + } + } + + for (key in second) { + if (second.hasOwnProperty(key)) { + result[key] = second[key]; + } + } + + return result; + } + + var arrayIndexOf; + + // Check for native Array.indexOf support + if (Array.prototype.indexOf) { + arrayIndexOf = function (array, searchElement) { + return array.indexOf(searchElement); + }; + } else { + arrayIndexOf = function (array, searchElement) { + for (var i = 0, count = array.length; i < count; i++) { + if (array[i] === searchElement) { + return i; + } + } + return -1; + }; + } + + function escapeRegExp(string) { + /// + /// Escapes a string for usage in a regular expression + /// + /// Input string + /// String suitable for inserting in a regex + + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + + var Route = function (route, settings) { + ///Handles route processing + ///Route information + + var paramRegex = /\{(\w+)\}/g, + matches; + + if (!route.optional) { + route.optional = []; + } + + this.settings = merge({ lowerCaseUrls: false }, settings); + this.route = route; + this._params = []; + + // Grab all the parameters from the URL + while ((matches = paramRegex.exec(this.route.url)) !== null) { + this._params.push(matches[1].toLowerCase()); + } + }; + + Route.prototype = { + build: function (routeValues) { + /// + /// Build a URL using this route, based on the passed route values. Returns null if the + /// route values provided are not sufficent to build a URL using this route. + /// + ///Route values + ///URL, or null when building a URL is not possible + + // Keys of values are case insensitive and are converted to lowercase server-side. + // Convert keys of input to lowercase too. + var routeValuesLowercase = {}; + for (var key in routeValues) { + if (routeValues.hasOwnProperty(key)) { + routeValuesLowercase[key.toLowerCase()] = routeValues[key]; + } + } + + var finalValues = merge(this.route.defaults, routeValuesLowercase), + processedParams = { controller: true, action: true }, + finalUrl; + + // Ensure area matches, if provided + if ( + this.route.defaults.area && + this.route.defaults.area.toLowerCase() !== (routeValuesLowercase.area || '').toLowerCase() + ) { + return null; + } + + if (!this._checkConstraints(finalValues) || !this._checkNonDefaultValues(finalValues, processedParams)) { + return null; + } + + // Try to merge all URL parameters + // If null, this means a required parameters was not specified. + finalUrl = this._merge(finalValues, processedParams); + if (finalUrl === null) { + return null; + } + + finalUrl = this._trimOptional(finalUrl) + this._extraParams(routeValues, processedParams, finalUrl.indexOf('?') > -1); + return finalUrl; + }, + + _checkNonDefaultValues: function (finalValues, processedParams) { + ///Checks that any values using a non-default value have a matching merge field in the URL. + ///Route values merged with defaults + ///Array of parameters that have already been processed + ///true if all non-default parameters have a matching merge field, otherwise false. + + for (var key in this.route.defaults) { + if (!this.route.defaults.hasOwnProperty(key)) { + continue; + } + // We don't care about case when comparing defaults. + if ( + (this.route.defaults[key] + '').toLowerCase() !== (finalValues[key] + '').toLowerCase() && + arrayIndexOf(this._params, key) === -1 + ) { + return false; + } else { + // Any defaults don't need to be explicitly specified in the querystring + processedParams[key] = true; + } + } + + return true; + }, + + _merge: function (finalValues, processedParams) { + /// + /// Merges parameters into the URL, keeping track of which parameters have been added and + /// ensuring that all required parameters are specified. + /// + ///Route values merged with defaults + ///Array of parameters that have already been processed + ///URL with parameters merged in, or null if not all parameters were specified + + var finalUrl = this.settings.lowerCaseUrls ? this.route.url.toLowerCase() : this.route.url; + + for (var i = 0, count = this._params.length; i < count; i++) { + var paramName = this._params[i], + isProvided = finalValues[paramName] !== undefined, + isOptional = arrayIndexOf(this.route.optional, paramName) > -1; + + if (!isProvided && !isOptional) { + return null; + } + + if (isProvided) { + var paramRegex = new RegExp('\{' + escapeRegExp(paramName) + '}', 'i'); + var paramValue = this.settings.lowerCaseUrls && this._shouldConvertParam(paramName) + ? finalValues[paramName].toLowerCase() + : finalValues[paramName]; + finalUrl = finalUrl.replace(paramRegex, encodeURIComponent(paramValue)); + } + + processedParams[paramName] = true; + } + + return finalUrl; + }, + + _trimOptional: function (finalUrl) { + ///Trims any unused optional parameter segments from the end of the URL + ///URL with used parameters merged in + ///URL with unused optional parameters removed + var urlPieces = finalUrl.split('/'); + for (var i = urlPieces.length - 1; i >= 0; i--) { + // If it has a parameter, assume it's an ignored one (otherwise it would have been merged above) + if (urlPieces[i].indexOf('{') > -1) { + urlPieces.splice(i, 1); + } + } + return urlPieces.join('/'); + }, + + _extraParams: function (routeValues, processedParams, alreadyHasParams) { + ///Add any additional parameters not specified in the URL as querystring parameters + ///Route values + ///Array of parameters that have already been processed + ///Whether this URL already has querystring parameters in it + ///URL encoded querystring parameters + + var params = ''; + + // Add all other parameters to the querystring + for (var key in routeValues) { + if (!processedParams[key.toLowerCase()]) { + params += (alreadyHasParams ? '&' : '?') + encodeURIComponent(key) + '=' + encodeURIComponent(routeValues[key]); + alreadyHasParams = true; + } + } + + return params; + }, + + _checkConstraints: function (routeValues) { + ///Validate that the route constraints match the specified route values + ///Route values + ///true if the route validation succeeds, otherwise false. + + // Bail out early if there's no constraints on this route + if (!this.route.constraints) { + return true; + } + + if (!this._parsedConstraints) { + this._parsedConstraints = this._parseConstraints(); + } + + // Check every constraint matches + for (var key in this._parsedConstraints) { + if (this._parsedConstraints.hasOwnProperty(key) && !this._parsedConstraints[key].test(routeValues[key])) { + return false; + } + } + + return true; + }, + + _parseConstraints: function () { + ///Parse the string constraints into regular expressions + + var parsedConstraints = {}; + + for (var key in this.route.constraints) { + if (this.route.constraints.hasOwnProperty(key)) { + parsedConstraints[key.toLowerCase()] = + new RegExp('^(' + this.route.constraints[key].replace(/\\/g, '\\') + ')$', 'i'); + } + } + + return parsedConstraints; + }, + + _shouldConvertParam: function (param) { + ///Gets if we should convert this param. + ///The param to check + + return ( + param === 'controller' || + param === 'action' || + param === 'area' + ); + }, + }; + + var RouteManager = function (settings) { + ///Manages routes and selecting the correct route to use when routing URLs + ///Raw route information + + this.baseUrl = settings.baseUrl; + this.lowerCaseUrls = settings.lowerCaseUrls; + this.routes = []; + var routeSettings = { + lowerCaseUrls: this.lowerCaseUrls + }; + for (var i = 0, count = settings.routes.length; i < count; i++) { + this.routes.push(new Route(settings.routes[i], routeSettings)); + } + }; + + RouteManager.prototype = { + action: function (controller, action, routeValues) { + ///Generate a URL to an action + ///Name of the controller + ///Name of the action + ///Route values + ///URL for the specified action + + routeValues = routeValues || { }; + routeValues.controller = controller || window.RouteJs.activeController; + routeValues.action = action; + return this.route(routeValues); + }, + + route: function (routeValues) { + ///Generate a URL to an action + ///Route values + ///URL for the specified action + + for (var i = 0, count = this.routes.length; i < count; i++) { + var url = this.routes[i].build(routeValues); + if (url) { + return this.baseUrl + url; + } + } + + throw new Error('No route could be matched to route values: ' + routeValues); + } + }; + + // Public API + window.RouteJs = { + version: '{VERSION}', + Route: Route, + RouteManager: RouteManager + }; +}(window)); diff --git a/src/RouteJs.AspNet/compiler/resources/router.min.js b/src/RouteJs.AspNet/compiler/resources/router.min.js new file mode 100644 index 0000000..71b8509 --- /dev/null +++ b/src/RouteJs.AspNet/compiler/resources/router.min.js @@ -0,0 +1,6 @@ +/*! + * RouteJs by Daniel Lo Nigro (Daniel15) - http://dl.vc/routejs + * Version {VERSION} + * Released under the BSD license. + */ +(function(n){"use strict";function u(n,t){var r={};for(var i in n)n.hasOwnProperty(i)&&(r[i]=n[i]);for(i in t)t.hasOwnProperty(i)&&(r[i]=t[i]);return r}function f(n){return n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}var i,t,r;i=Array.prototype.indexOf?function(n,t){return n.indexOf(t)}:function(n,t){for(var i=0,r=n.length;i-1)},_checkNonDefaultValues:function(n,t){for(var r in this.route.defaults)if(this.route.defaults.hasOwnProperty(r)){if((this.route.defaults[r]+"").toLowerCase()!==(n[r]+"").toLowerCase()&&i(this._params,r)===-1)return!1;t[r]=!0}return!0},_merge:function(n,t){for(var s,h,u=this.settings.lowerCaseUrls?this.route.url.toLowerCase():this.route.url,e=0,c=this._params.length;e-1;if(!o&&!l)return null;o&&(s=new RegExp("{"+f(r)+"}","i"),h=this.settings.lowerCaseUrls&&this._shouldConvertParam(r)?n[r].toLowerCase():n[r],u=u.replace(s,encodeURIComponent(h)));t[r]=!0}return u},_trimOptional:function(n){for(var t=n.split("/"),i=t.length-1;i>=0;i--)t[i].indexOf("{")>-1&&t.splice(i,1);return t.join("/")},_extraParams:function(n,t,i){var u="";for(var r in n)t[r.toLowerCase()]||(u+=(i?"&":"?")+encodeURIComponent(r)+"="+encodeURIComponent(n[r]),i=!0);return u},_checkConstraints:function(n){if(!this.route.constraints)return!0;this._parsedConstraints||(this._parsedConstraints=this._parseConstraints());for(var t in this._parsedConstraints)if(this._parsedConstraints.hasOwnProperty(t)&&!this._parsedConstraints[t].test(n[t]))return!1;return!0},_parseConstraints:function(){var t={};for(var n in this.route.constraints)this.route.constraints.hasOwnProperty(n)&&(t[n.toLowerCase()]=new RegExp("^("+this.route.constraints[n].replace(/\\/g,"\\")+")$","i"));return t},_shouldConvertParam:function(n){return n==="controller"||n==="action"||n==="area"}};r=function(n){var r,i,u;for(this.baseUrl=n.baseUrl,this.lowerCaseUrls=n.lowerCaseUrls,this.routes=[],r={lowerCaseUrls:this.lowerCaseUrls},i=0,u=n.routes.length;i