svelte-pathfinder
Version:
Tiny, state-based, advanced router for SvelteJS.
1,008 lines (887 loc) • 26.1 kB
JavaScript
function noop() { }
function run(fn) {
return fn();
}
function run_all(fns) {
fns.forEach(run);
}
function is_function(thing) {
return typeof thing === 'function';
}
function safe_not_equal(a, b) {
return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
function subscribe(store, ...callbacks) {
if (store == null) {
return noop;
}
const unsub = store.subscribe(...callbacks);
return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub;
}
function get_store_value(store) {
let value;
subscribe(store, _ => value = _)();
return value;
}
function set_current_component(component) {
}
const dirty_components = [];
const binding_callbacks = [];
const render_callbacks = [];
const flush_callbacks = [];
const resolved_promise = Promise.resolve();
let update_scheduled = false;
function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
function tick() {
schedule_update();
return resolved_promise;
}
function add_render_callback(fn) {
render_callbacks.push(fn);
}
// flush() calls callbacks in this order:
// 1. All beforeUpdate callbacks, in order: parents before children
// 2. All bind:this callbacks, in reverse order: children before parents.
// 3. All afterUpdate callbacks, in order: parents before children. EXCEPT
// for afterUpdates called during the initial onMount, which are called in
// reverse order: children before parents.
// Since callbacks might update component values, which could trigger another
// call to flush(), the following steps guard against this:
// 1. During beforeUpdate, any updated components will be added to the
// dirty_components array and will cause a reentrant call to flush(). Because
// the flush index is kept outside the function, the reentrant call will pick
// up where the earlier call left off and go through all dirty components. The
// current_component value is saved and restored so that the reentrant call will
// not interfere with the "parent" flush() call.
// 2. bind:this callbacks cannot trigger new flush() calls.
// 3. During afterUpdate, any updated components will NOT have their afterUpdate
// callback called a second time; the seen_callbacks set, outside the flush()
// function, guarantees this behavior.
const seen_callbacks = new Set();
let flushidx = 0; // Do *not* move this inside the flush() function
function flush() {
// Do not reenter flush while dirty components are updated, as this can
// result in an infinite loop. Instead, let the inner flush handle it.
// Reentrancy is ok afterwards for bindings etc.
if (flushidx !== 0) {
return;
}
do {
// first, call beforeUpdate functions
// and update components
try {
while (flushidx < dirty_components.length) {
const component = dirty_components[flushidx];
flushidx++;
set_current_component(component);
update(component.$$);
}
}
catch (e) {
// reset dirty state to not end up in a deadlocked state and then rethrow
dirty_components.length = 0;
flushidx = 0;
throw e;
}
dirty_components.length = 0;
flushidx = 0;
while (binding_callbacks.length)
binding_callbacks.pop()();
// then, once components are updated, call
// afterUpdate functions. This may cause
// subsequent updates...
for (let i = 0; i < render_callbacks.length; i += 1) {
const callback = render_callbacks[i];
if (!seen_callbacks.has(callback)) {
// ...so guard against infinite loops
seen_callbacks.add(callback);
callback();
}
}
render_callbacks.length = 0;
} while (dirty_components.length);
while (flush_callbacks.length) {
flush_callbacks.pop()();
}
update_scheduled = false;
seen_callbacks.clear();
}
function update($$) {
if ($$.fragment !== null) {
$$.update();
run_all($$.before_update);
const dirty = $$.dirty;
$$.dirty = [-1];
$$.fragment && $$.fragment.p($$.ctx, dirty);
$$.after_update.forEach(add_render_callback);
}
}
const subscriber_queue = [];
/**
* Creates a `Readable` store that allows reading by subscription.
* @param value initial value
* @param {StartStopNotifier}start start and stop notifications for subscriptions
*/
function readable(value, start) {
return {
subscribe: writable(value, start).subscribe
};
}
/**
* Create a `Writable` store that allows both updating and reading by subscription.
* @param {*=}value initial value
* @param {StartStopNotifier=}start start and stop notifications for subscriptions
*/
function writable(value, start = noop) {
let stop;
const subscribers = new Set();
function set(new_value) {
if (safe_not_equal(value, new_value)) {
value = new_value;
if (stop) { // store is ready
const run_queue = !subscriber_queue.length;
for (const subscriber of subscribers) {
subscriber[1]();
subscriber_queue.push(subscriber, value);
}
if (run_queue) {
for (let i = 0; i < subscriber_queue.length; i += 2) {
subscriber_queue[i][0](subscriber_queue[i + 1]);
}
subscriber_queue.length = 0;
}
}
}
}
function update(fn) {
set(fn(value));
}
function subscribe(run, invalidate = noop) {
const subscriber = [run, invalidate];
subscribers.add(subscriber);
if (subscribers.size === 1) {
stop = start(set) || noop;
}
run(value);
return () => {
subscribers.delete(subscriber);
if (subscribers.size === 0) {
stop();
stop = null;
}
};
}
return { set, update, subscribe };
}
function derived(stores, fn, initial_value) {
const single = !Array.isArray(stores);
const stores_array = single
? [stores]
: stores;
const auto = fn.length < 2;
return readable(initial_value, (set) => {
let inited = false;
const values = [];
let pending = 0;
let cleanup = noop;
const sync = () => {
if (pending) {
return;
}
cleanup();
const result = fn(single ? values[0] : values, set);
if (auto) {
set(result);
}
else {
cleanup = is_function(result) ? result : noop;
}
};
const unsubscribers = stores_array.map((store, i) => subscribe(store, (value) => {
values[i] = value;
pending &= ~(1 << i);
if (inited) {
sync();
}
}, () => {
pending |= (1 << i);
}));
inited = true;
sync();
return function stop() {
run_all(unsubscribers);
cleanup();
};
});
}
const specialLinks =
/^((mailto:)|(tel:)|(sms:)|(data:)|(blob:)|(javascript:)|(ftp(s?):\/\/)|(file:\/\/))/;
const hasLocation = typeof location !== 'undefined';
const hasProcess = typeof process !== 'undefined';
const hasHistory = typeof history !== 'undefined';
const hasPushState = hasHistory && isFn(history.pushState);
const hasWindow = typeof window !== 'undefined';
const isSubWindow = hasWindow && window !== window.parent;
const isFileScheme =
hasLocation && (location.protocol === 'file:' || /[-_\w]+[.][\w]+$/i.test(location.pathname));
const sideEffect = hasWindow && hasHistory && hasLocation && !isSubWindow;
const useHashbang = !hasPushState || isFileScheme;
const hashbang = '#!';
const prefs = {
array: {
separator: ',',
format: 'bracket',
},
convertTypes: true,
breakHooks: true,
hashbang: false,
anchor: false,
scroll: false,
focus: false,
nesting: 3,
sideEffect,
base: '',
};
function getPath() {
const pathname = getLocation().pathname;
if (!pathname) return;
const base = getBase();
const path = trimPrefix(pathname, base);
return prependPrefix(path);
}
function getLocation() {
if (!hasLocation) return {};
if (prefs.hashbang || useHashbang) {
const hash = location.hash;
return new URL(
hash.indexOf(hashbang) === 0 ? hash.substring(2) : hash.substring(1),
'file:'
);
}
return location;
}
function getBase() {
if (prefs.base) return prefs.base;
if (hasLocation && (prefs.hashbang || useHashbang)) return location.pathname;
return '/';
}
function getFullURL(url) {
(prefs.hashbang || useHashbang) && (url = hashbang + url);
const base = getBase();
return (base[base.length - 1] === '/' ? base.substring(0, base.length - 1) : base) + url;
}
function getShortURL(url) {
url = trimPrefix(url, getLocation().origin);
const base = getBase();
url = trimPrefix(url, base);
(prefs.hashbang || useHashbang) && (url = trimPrefix(url, hashbang));
return prependPrefix(url);
}
function isBtn(el) {
const tagName = el.tagName.toLowerCase();
const type = el.type && el.type.toLowerCase();
return (
tagName === 'button' ||
(tagName === 'input' && ['button', 'submit', 'image'].includes(type))
);
}
function closest(el, tagName) {
while (el && el.nodeName.toLowerCase() !== tagName) el = el.parentNode;
return !el || el.nodeName.toLowerCase() !== tagName ? null : el;
}
function setScroll(scroll, hash = '') {
const anchor = trimPrefix(normalizeHash(hash), '#');
if (scroll && prefs.scroll) {
const opts = isObj(prefs.scroll) ? { ...prefs.scroll, ...scroll } : scroll;
const { top = 0, left = 0 } = scroll;
const { scrollHeight, scrollWidth } = document.documentElement;
if (top <= scrollHeight && left <= scrollWidth) return scrollTo(opts);
const cancel = observeResize((entries) => {
if (!entries[0]) return cancel();
if (
(!top || entries[0].contentRect.height >= top) &&
(!left || entries[0].contentRect.width >= left)
) {
cancel();
scrollTo(opts);
}
}, document.documentElement);
} else if (anchor && prefs.anchor) {
const opts = isObj(prefs.anchor) ? prefs.anchor : {};
const el = document.getElementById(anchor);
if (el) return scrollTo(opts, el);
const cancel = observeDom(() => {
const el = document.getElementById(anchor);
if (el) {
cancel();
scrollTo(opts, el);
}
});
} else if (prefs.scroll) {
scrollTo();
}
}
function setFocus(keepFocusId, activeElement) {
if (!prefs.focus) return;
setTimeout(() => {
const autofocus = focus();
if (autofocus) return autofocus.focus();
const cancel = observeDom(() => {
const autofocus = focus();
if (autofocus) {
cancel();
autofocus.focus();
}
});
const body = document.body;
const tabindex = body.getAttribute('tabindex');
body.tabIndex = -1;
body.focus({ preventScroll: true });
if (tabindex !== null) {
body.setAttribute('tabindex', tabindex);
} else {
body.removeAttribute('tabindex');
}
getSelection().removeAllRanges();
});
function focus() {
if (keepFocusId) {
return document.getElementById(keepFocusId);
} else if (
document.activeElement !== activeElement &&
document.activeElement !== document.body
) {
return document.activeElement;
} else {
return document.querySelector('[autofocus]');
}
}
}
function parseQuery(str = '', { decode = decodeURIComponent } = {}) {
return str
? str
.replace('?', '')
.replace(/\+/g, ' ')
.split('&')
.filter(Boolean)
.reduce((obj, p) => {
let [key, val] = p.split(/=(.*)/, 2);
key = decode(key || '');
val = decode(val || '');
let o = parseKeys(key, val);
obj = Object.keys(o).reduce((obj, key) => {
const val = prefs.convertTypes ? convertType(o[key]) : o[key];
if (obj[key]) {
Array.isArray(obj[key])
? (obj[key] = obj[key].concat(val))
: Object.assign(obj[key], val);
} else {
obj[key] = val;
}
return obj;
}, obj);
return obj;
}, {})
: {};
}
function stringifyQuery(obj = {}, { encode = encodeURIComponent } = {}) {
return Object.keys(obj)
.reduce((a, k) => {
if (Object.prototype.hasOwnProperty.call(obj, k) && isNaN(parseInt(k, 10))) {
if (Array.isArray(obj[k])) {
if (prefs.array.format === 'separator') {
a.push(`${k}=${obj[k].join(prefs.array.separator)}`);
} else {
obj[k].forEach((v) => a.push(`${k}[]=${encode(v)}`));
}
} else if (isObj(obj[k])) {
let o = parseKeys(k, obj[k]);
a.push(stringifyObj(o));
} else {
a.push(`${k}=${encode(obj[k])}`);
}
}
return a;
}, [])
.join('&');
}
function injectParams(pattern, params, { encode = encodeURIComponent } = {}) {
return pattern.replace(/(\/|^)([:*][^/]*?)(\?)?(?=[/.]|$)/g, (param, _, key) => {
param = params[key === '*' ? 'wild' : key.substring(1)];
return param ? `/${encode(param)}` : '';
});
}
function parseParams(
path = '',
pattern = '*',
{ loose = false, sensitive = false, blank = false, decode = decodeURIComponent } = {}
) {
const blanks = {};
const rgx =
pattern instanceof RegExp
? pattern
: pattern.split('/').reduce((rgx, seg, i, { length }) => {
if (seg) {
const pfx = seg[0];
if (pfx === '*') {
blanks['wild'] = undefined;
rgx += '/(?<wild>.*)';
} else if (pfx === ':') {
const opt = seg.indexOf('?', 1);
const ext = seg.indexOf('.', 1);
const isOpt = !!~opt;
const isExt = !!~ext;
const key = seg.substring(1, isOpt ? opt : isExt ? ext : seg.length);
blanks[key] = undefined;
rgx +=
isOpt && !isExt ? `(?:/(?<${key}>[^/]+?))?` : `/(?<${key}>[^/]+?)`;
if (isExt) rgx += `${isOpt ? '?' : ''}\\${seg.substring(ext)}`;
} else {
rgx += `/${seg}`;
}
}
if (i === length - 1) {
rgx += loose ? '(?:$|/)' : '/?$';
}
return rgx;
}, '^');
const flags = sensitive ? '' : 'i';
const matches = new RegExp(rgx, flags).exec(path);
return matches
? Object.entries(matches.groups || {}).reduce((params, [key, val]) => {
const value = decode(val);
params[key] = prefs.convertTypes ? convertType(value) : value;
return params;
}, {})
: blank
? blanks
: null;
}
function normalizeHash(fragment, { decode = decodeURIComponent } = {}) {
return decode(fragment);
}
function prependPrefix(str, pfx = '/', strict = false) {
str += '';
return !str && strict ? str : str.indexOf(pfx) !== 0 ? pfx + str : str;
}
function trimPrefix(str, pfx) {
return (str + '').indexOf(pfx) === 0 ? str.substring(pfx.length) : str;
}
function isObj(obj) {
return !Array.isArray(obj) && typeof obj === 'object' && obj !== null;
}
function isFn(fn) {
return typeof fn === 'function';
}
function shallowCopy(value) {
if (typeof value !== 'object' || value === null) return value;
return Object.create(Object.getPrototypeOf(value), Object.getOwnPropertyDescriptors(value));
}
function hookLauncher(hooks) {
return (...args) => {
const arr = [...hooks];
return !(prefs.breakHooks
? arr.some((cb) => cb(...args) === false)
: arr.reduce((stop, cb) => cb(...args) === false || stop, false));
};
}
function listenEvent(...args) {
window.addEventListener(...args);
return () => window.removeEventListener(...args);
}
function scrollTo({ top = 0, left = 0, ...opts } = {}, el) {
if (el) {
document.documentElement.scrollIntoView
? el.scrollIntoView({ behavior: 'smooth', ...opts })
: window.scrollTo({ top: el.offsetTop - top, behavior: 'smooth', ...opts });
} else {
window.scrollTo({ top, left, behavior: 'smooth', ...opts });
}
}
function observeResize(cb, el, t = 5000) {
const observer = new ResizeObserver(cb);
observer.observe(el);
const off = () => observer.unobserve(el);
setTimeout(off, t);
return off;
}
function observeDom(cb, t = 5000) {
const observer = new MutationObserver(cb);
observer.observe(document.body, {
childList: true,
subtree: true,
});
const off = () => observer.disconnect();
setTimeout(off, t);
return off;
}
function convertType(val) {
if (Array.isArray(val)) {
val[val.length - 1] = convertType(val[val.length - 1]);
return val;
} else if (typeof val === 'object') {
return Object.entries(val).reduce((obj, [k, v]) => {
obj[k] = convertType(v);
return obj;
}, {});
}
if (val === 'true' || val === 'false') {
return val === 'true';
} else if (val === 'null') {
return null;
} else if (val === 'undefined') {
return undefined;
} else if (val !== '' && !isNaN(Number(val)) && Number(val).toString() === val) {
return Number(val);
} else if (prefs.array.format === 'separator' && typeof val === 'string') {
const arr = val.split(prefs.array.separator);
return arr.length > 1 ? arr : val;
}
return val;
}
function parseKeys(key, val) {
const brackets = /(\[[^[\]]*])/,
child = /(\[[^[\]]*])/g;
let seg = brackets.exec(key),
parent = seg ? key.slice(0, seg.index) : key,
keys = [];
parent && keys.push(parent);
let i = 0;
while ((seg = child.exec(key)) && i < prefs.nesting) {
i++;
keys.push(seg[1]);
}
seg && keys.push(`[${key.slice(seg.index)}]`);
return parseObj(keys, val);
}
function parseObj(chain, val) {
let leaf = val;
for (let i = chain.length - 1; i >= 0; --i) {
let root = chain[i],
obj;
if (root === '[]') {
obj = [].concat(leaf);
} else {
obj = {};
const key =
root.charAt(0) === '[' && root.charAt(root.length - 1) === ']'
? root.slice(1, -1)
: root,
j = parseInt(key, 10);
if (!isNaN(j) && root !== key && String(j) === key && j >= 0) {
obj = [];
obj[j] = prefs.convertTypes ? convertType(leaf) : leaf;
} else {
obj[key] = leaf;
}
}
leaf = obj;
}
return leaf;
}
function stringifyObj(obj = {}, nesting = '') {
return Object.entries(obj)
.map(([key, val]) => {
if (typeof val === 'object') {
return stringifyObj(val, nesting ? `${nesting}[${key}]` : key);
} else {
return `${nesting}[${key}]=${val}`;
}
})
.join('&');
}
const pathable = createParsableStore(function path($path = '') {
if (typeof $path === 'string') $path = trimPrefix($path, '/').split('/');
return !Object.prototype.hasOwnProperty.call($path, 'toString')
? Object.defineProperty($path, 'toString', {
value() {
return prependPrefix(this.join('/'));
},
configurable: false,
writable: false,
})
: $path;
});
const queryable = createParsableStore(function query($query = '') {
if (typeof $query === 'string') $query = parseQuery($query);
return !Object.prototype.hasOwnProperty.call($query, 'toString')
? Object.defineProperty($query, 'toString', {
value() {
return prependPrefix(stringifyQuery(this), '?', true);
},
configurable: false,
writable: false,
})
: $query;
});
const fragmentable = createParsableStore(function fragment($fragment = '') {
return prependPrefix(normalizeHash($fragment), '#', true);
});
function createParamStore(path) {
return (pattern, options = {}) => {
if (pattern instanceof RegExp)
throw new Error('Paramable does not support RegExp patterns.');
let params;
pattern = pattern.replace(/\/$/, '');
const { subscribe } = writable({}, (set) => {
return path.subscribe(($path) => {
params = parseParams($path.toString(), pattern, { blank: true, ...options });
set(shallowCopy(params));
});
});
function set(value = {}) {
if (Object.entries(params).some(([key, val]) => val !== value[key])) {
path.update(($path) => {
const tail = options.loose
? prependPrefix($path.slice(pattern.split('/').length - 1).join('/'))
: '';
return injectParams(pattern + tail, value);
});
}
}
return {
get() {
return get_store_value(this);
},
update(fn) {
set(fn(this.get()));
},
subscribe,
set,
};
};
}
function createParsableStore(parse) {
return (value, cbx) => {
let serialized = value && value.toString();
!Array.isArray(cbx) && (cbx = [cbx]);
const hooks = new Set(cbx);
const runHooks = hookLauncher(hooks);
const { subscribe, set } = writable((value = parse(value)), () => () => hooks.clear());
function update(val) {
val = parse(val);
if (val.toString() !== serialized && runHooks(val, value, parse.name) !== false) {
serialized = val.toString();
value = val;
set(value);
}
}
runHooks(null, value, parse.name);
return {
subscribe,
update(fn) {
update(fn(get_store_value(this)));
},
set(value) {
update(value);
},
hook(cb) {
if (isFn(cb)) {
hooks.add(cb);
cb(null, value, parse.name);
}
return () => hooks.delete(cb);
},
};
};
}
const pathname = getPath();
const { search, hash } = getLocation();
let init = true;
let popstate = false;
let replace = false;
let len = 0;
const path = pathable(pathname, before);
const query = queryable(search, before);
const fragment = fragmentable(hash, before);
const state = writable({});
const url = derived(
[path, query, fragment],
([$path, $query, $fragment], set) => {
let skip = false;
tick().then(() => {
if (skip) return;
set($path + $query + $fragment);
});
return () => (skip = true);
},
pathname + search + hash
);
const pattern = derived(path, ($path) => parseParams.bind(null, $path.toString()));
function before() {
if (!prefs.scroll && !prefs.focus) return;
state.update(($state = {}) => {
prefs.scroll &&
($state._scroll = {
top: window.pageYOffset,
left: window.pageXOffset,
});
prefs.focus && ($state._focus = document.activeElement.id);
return $state;
});
}
function after(url, state) {
const anchor = url.indexOf('#') >= 0 ? url.slice(url.indexOf('#')) : '';
const activeElement = document.activeElement;
!isObj(state) && (state = {});
tick()
.then(() => setFocus(state._focus, activeElement))
.then(() => setScroll(state._scroll, anchor));
}
if (sideEffect || isSubWindow) {
const cleanup = new Set();
cleanup.add(
url.subscribe(($url) => {
if (!init && !popstate && prefs.sideEffect) {
if (hasPushState) {
history[replace ? 'replaceState' : 'pushState']({}, null, getFullURL($url));
} else {
location.hash = getFullURL($url);
}
}
!popstate && after($url);
!replace && len++;
init = replace = popstate = false;
})
);
if (hasPushState) {
cleanup.add(
state.subscribe(($state) => {
if (init || !prefs.sideEffect) return;
history.replaceState(
$state,
null,
location.pathname + location.search + location.hash
);
})
);
cleanup.add(
listenEvent('popstate', (e) => {
popstate = true;
goto(location.href, e.state);
after(getShortURL(location.href), e.state);
})
);
} else {
cleanup.add(
listenEvent('hashchange', () => {
popstate = true;
if (!prefs.hashbang && !useHashbang) return fragment.set(location.hash);
goto(location.hash);
after(getShortURL(location.hash));
})
);
}
cleanup.add(
listenEvent(
'beforeunload',
() => {
cleanup.forEach((off) => off());
cleanup.clear();
},
true
)
);
}
function goto(url = '', data = {}) {
const { pathname, search, hash } =
url instanceof URL ? url : new URL(getShortURL(url), 'file:');
path.set(pathname);
query.set(search);
fragment.set(hash);
tick().then(() => state.set(data || {}));
}
function back(url) {
if (len > 0 && sideEffect && prefs.sideEffect) {
history.back();
len--;
} else {
tick().then(() => goto(url));
}
}
function redirect(url, data) {
tick().then(() => {
replace = true;
goto(url, data);
});
}
function click(e) {
if (
!e.target ||
e.ctrlKey ||
e.metaKey ||
e.altKey ||
e.shiftKey ||
e.button ||
e.which !== 1 ||
e.defaultPrevented
)
return;
const a = closest(e.target, 'a');
if (
!a ||
a.target ||
a.hasAttribute('download') ||
(a.hasAttribute('rel') && a.getAttribute('rel').includes('external'))
)
return;
const href = a.getAttribute('href');
const url = a.href;
if (
!href ||
url.indexOf(location.origin) !== 0 ||
specialLinks.test(href) ||
(!prefs.hashbang && !useHashbang && href.startsWith('#'))
)
return;
e.preventDefault();
goto(url, Object.assign({}, a.dataset));
}
function submit(e) {
if (!e.target || e.defaultPrevented) return;
const form = e.target;
const btn = e.submitter || (isBtn(document.activeElement) && document.activeElement);
let action = form.action;
let method = form.method;
let target = form.target;
if (btn) {
btn.hasAttribute('formaction') && (action = btn.formAction);
btn.hasAttribute('formmethod') && (method = btn.formMethod);
btn.hasAttribute('formtarget') && (target = btn.formTarget);
}
if (method && method.toLowerCase() !== 'get') return;
if (target && target.toLowerCase() !== '_self') return;
const { pathname, hash } = new URL(action);
const search = [];
const state = {};
const elements = form.elements;
const len = elements.length;
for (let i = 0; i < len; i++) {
const element = elements[i];
if (!element.name || element.disabled) continue;
if (['checkbox', 'radio'].includes(element.type) && !element.checked) {
continue;
}
if (isBtn(element) && element !== btn) {
continue;
}
if (element.type === 'hidden') {
state[element.name] = element.value;
continue;
}
search.push(`${element.name}=${element.value}`);
}
let url = prependPrefix(`${pathname}?${search.join('&')}${hash}`);
if (hasProcess && url.match(/^\/[a-zA-Z]:\//)) {
url = url.replace(/^\/[a-zA-Z]:\//, '/');
}
e.preventDefault();
goto(url, state);
}
const paramable = createParamStore(path);
export { back, click, fragment, goto, paramable, path, pattern, prefs, query, redirect, state, submit, url };