Created
April 14, 2016 15:28
-
-
Save tayiorbeii/e9c48e79d87984517ee83fe8bb696bf2 to your computer and use it in GitHub Desktop.
Link.jsx is a wrapper for react-router's Link that adds some helper functionality for things like relative links.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
import { Link } from 'react-router' | |
import * as urlTools from './url' | |
const { node, object, string, oneOfType } = React.PropTypes | |
/** | |
* A wrapper around react-router Link to add support for relative URLs | |
* and easily adding a slug. All URL's will also be normalized, this allows | |
* the use dot operators. So a URL like: `/my/bad/../path` will be normalized | |
* to `/my/path`. | |
* | |
* URIs: | |
* 1. If the `to` prop starts with `/` it will be normalized and then used as | |
* canonical URL. | |
* 2. If the `to` prop starts with `./` it will be converted to a relative URL | |
* from the current location and then normalized. | |
* 3. If the `to` prop _does not_ start with `/` or `./` it will be converted | |
* to a relative URL from the current location and then normalized. | |
* | |
* Slugs: | |
* 1. If the `slug` prop has a value, it will be slugified and joined as to | |
* the URI as the last part. | |
* 2. If the `slug` prop is set but the `to` prop is not then the slugified | |
* value of `slug` will be used as the URI relative to the current path. | |
*/ | |
export default React.createClass({ | |
propTypes: { | |
to: oneOfType([ string, object ]), | |
slug: string, | |
location: object, | |
children: node | |
}, | |
getDefaultProps: () => ({ to: './' }), | |
/** | |
* A high level version of the function createLocationDescriptor, | |
* from react-router:Link, that processes URIs that can be relative | |
* and adds slugs as requested. | |
* This always returns a full location object. | |
*/ | |
createLocationDescriptor: (to, slug) => { | |
let { query, hash, state } = typeof to === 'string' ? {} : to | |
let uri = typeof to === 'string' ? to : to.pathname | |
if (urlTools.isRelative(uri)) { | |
uri = urlTools.join(window.location.pathname, uri) | |
} | |
if (slug) { | |
uri = urlTools.join(uri, urlTools.slugify(slug)) | |
} | |
return { | |
pathname: urlTools.normalize(uri), | |
query, hash, state | |
} | |
}, | |
render: function () { | |
const { to, slug, ...otherProps } = this.props | |
return ( | |
<Link | |
to={this.createLocationDescriptor(to, slug)} | |
{...otherProps} | |
/> | |
) | |
} | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export let clean = (url) => { | |
// Remove consecutive slashes | |
// If it starts with a slash, treat as a standalone path. Otherwise | |
// we expect a protocol to be present | |
let slashRe = /([^:\s])\/+/g | |
if (url.charAt(0) === '/') { | |
slashRe = /([\w\s]|.?)\/\/+/g | |
} | |
let cleaned = url.replace(slashRe, '$1/') | |
// Remove any trailing slash, even if there are parameters. | |
return cleaned.replace(/\/(\?|&|#[^!]|$)/g, '$1') | |
} | |
export let join = (...parts) => { | |
// Do a simple join, then clean. | |
return clean(parts.join('/')) | |
} | |
export let parts = (url) => { | |
// Return the RHS of the url split into parts. So no protocol parts. | |
let protoParts = url.split('://') | |
let uri = '' | |
if (protoParts.length === 1) { | |
uri = protoParts[0] | |
} else { | |
uri = protoParts[1] | |
} | |
uri = clean(uri) | |
let uriParts = uri.split('/') | |
// If the URI/Path started with a '/', the first part will be empty. | |
// We don't want that. | |
if (uriParts[0] === '') { | |
return uriParts.slice(1) | |
} | |
return uriParts | |
} | |
export let proto = (url) => { | |
// Return LHS of the url, the protocol part. | |
// Returns null if none is found | |
let protoParts = url.split(':') | |
if (protoParts.length <= 1) { | |
return null | |
} | |
return protoParts[0] | |
} | |
export let pop = (url, num = 1) => { | |
let lhs = proto(url) | |
let rhs = parts(url) | |
let uri = '' | |
if (num < 1) { | |
return clean(url) | |
} | |
if (rhs.length > num) { | |
rhs = rhs.slice(0, (-1 * num)) | |
uri = join(...rhs) | |
} | |
if (lhs) { | |
return `${lhs}://${uri}` | |
} | |
return `/${uri}` | |
} | |
export let isRelative = (url) => { | |
if (url.length === 0) { | |
return true | |
} | |
return url.charAt(0) !== '/' | |
} | |
/** | |
* Removes any redundant parts and will process | |
* path manipulators `.`, `./` and `..` appropriately | |
* to create a clean and accurate URI | |
*/ | |
export let normalize = (url) => { | |
const uriParts = parts(url) | |
if (uriParts.length === 0) { | |
return '' | |
} | |
const res = uriParts.reduce((res, val) => { | |
if (val === '..') { | |
return pop(res) | |
} | |
if (val === '.' || val === '') { | |
return res | |
} | |
return join(res, val) | |
}, '') | |
// Due to the join and that we start with an empty | |
// string, the result will always have a / at it's root. | |
// But we respect relative input URLs, so remove it if the | |
// input was relative. | |
if (isRelative(url)) { | |
return res.slice(1) | |
} | |
return res | |
} | |
export let slugify = (unslug) => { | |
// Convert spaces to hyphens. | |
// Remove non-alphanumeric. | |
// Convert to lowercase | |
let slug = unslug.trim().toLowerCase() | |
// If it's not space, alpha-num or hyphen | |
slug = slug.replace(/[^\w\s-]+|[_]/g, '') | |
// add hyphens as needed. | |
slug = slug.replace(/[\s-]+/g, '-') | |
return slug | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment