client: refactor linking and routing

Print all links through new uri.js component
Refactor the router to use more predictable parsing
Fix linking to entities with weird names (that contain slashes, + etc.)
This commit is contained in:
rr- 2017-01-20 21:51:04 +01:00
parent 6714f05b49
commit 1acceb941d
65 changed files with 380 additions and 295 deletions

View file

@ -1,7 +1,7 @@
<div class='comment-container'>
<div class='avatar'>
<% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %>
<a href='/user/<%- encodeURIComponent(ctx.user.name) %>'>
<a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'>
<% } %>
<%= ctx.makeThumbnail(ctx.user ? ctx.user.avatarUrl : null) %>
@ -23,7 +23,7 @@
<nav class='readonly'><%
%><strong><span class='nickname'><%
%><% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %><%
%><a href='/user/<%- encodeURIComponent(ctx.user.name) %>'><%
%><a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'><%
%><% } %><%
%><%- ctx.user ? ctx.user.name : 'Deleted user' %><%

View file

@ -4,7 +4,7 @@
--><li><!--
--><div class='post-thumbnail'><!--
--><% if (ctx.canViewPosts) { %><!--
--><a href='/post/<%- encodeURIComponent(post.id) %>'><!--
--><a href='<%- ctx.formatClientLink('post', post.id) %>'><!--
--><% } %><!--
--><%= ctx.makeThumbnail(post.thumbnailUrl) %><!--
--><% if (ctx.canViewPosts) { %><!--

View file

@ -1,11 +1,11 @@
<div class='content-wrapper' id='help'>
<nav class='buttons primary'><!--
--><ul><!--
--><li data-name='about'><a href='/help/about'>About</a></li><!--
--><li data-name='keyboard'><a href='/help/keyboard'>Keyboard</a></li><!--
--><li data-name='search'><a href='/help/search'>Search syntax</a></li><!--
--><li data-name='comments'><a href='/help/comments'>Comments</a></li><!--
--><li data-name='tos'><a href='/help/tos'>Terms of service</a></li><!--
--><li data-name='about'><a href='<%- ctx.formatClientLink('help', 'about') %>'>About</a></li><!--
--><li data-name='keyboard'><a href='<%- ctx.formatClientLink('help', 'keyboard') %>'>Keyboard</a></li><!--
--><li data-name='search'><a href='<%- ctx.formatClientLink('help', 'search') %>'>Search syntax</a></li><!--
--><li data-name='comments'><a href='<%- ctx.formatClientLink('help', 'comments') %>'>Comments</a></li><!--
--><li data-name='tos'><a href='<%- ctx.formatClientLink('help', 'tos') %>'>Terms of service</a></li><!--
--></ul><!--
--></nav>

View file

@ -1,9 +1,9 @@
<nav class='buttons secondary'><!--
--><ul><!--
--><li data-name='default'><a href='/help/search'>General</a></li><!--
--><li data-name='posts'><a href='/help/search/posts'>Posts</a></li><!--
--><li data-name='users'><a href='/help/search/users'>Users</a></li><!--
--><li data-name='tags'><a href='/help/search/tags'>Tags</a></li><!--
--><li data-name='default'><a href='<%- ctx.formatClientLink('help', 'search') %>'>General</a></li><!--
--><li data-name='posts'><a href='<%- ctx.formatClientLink('help', 'search', 'posts') %>'>Posts</a></li><!--
--><li data-name='users'><a href='<%- ctx.formatClientLink('help', 'search', 'users') %>'>Users</a></li><!--
--><li data-name='tags'><a href='<%- ctx.formatClientLink('help', 'search', 'tags') %>'>Tags</a></li><!--
--></ul><!--
--></nav>

View file

@ -8,7 +8,7 @@
<%= ctx.makeTextInput({name: 'search-text', placeholder: 'enter some tags'}) %>
<input type='submit' value='Search'/>
<span class=sep>or</span>
<a href='/posts'>browse all posts</a>
<a href='<%- ctx.formatClientLink('posts') %>'>browse all posts</a>
</form>
<% } %>
<div class='post-info-container'></div>

View file

@ -2,6 +2,6 @@
<li><%- ctx.postCount %> posts</li><span class='sep'>
</span><li><%= ctx.makeFileSize(ctx.diskUsage) %></li><span class='sep'>
</span><li>Build <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%- ctx.version %></a> from <%= ctx.makeRelativeTime(ctx.buildDate) %></li><span class='sep'>
</span><% if (ctx.canListSnapshots) { %><li><a href='/history'>History</a></li><span class='sep'>
</span><% if (ctx.canListSnapshots) { %><li><a href='<%- ctx.formatClientLink('history') %>'>History</a></li><span class='sep'>
</span><% } %>
</ul>

View file

@ -31,7 +31,7 @@
<div class='buttons'>
<input type='submit' value='Log in'/>
<% if (ctx.canSendMails) { %>
<a class='append' href='/password-reset'>Forgot the password?</a>
<a class='append' href='<%- ctx.formatClientLink('password-reset') %>'>Forgot the password?</a>
<% } %>
</div>
</form>

View file

@ -1,5 +1,5 @@
<div class='not-found'>
<h1>Not found</h1>
<p><%- ctx.path %> is not a valid URL.</p>
<p><a href='/'>Back to main page</a></p>
<p><a href='<%- ctx.formatClientLink() %>'>Back to main page</a></p>
</div>

View file

@ -2,9 +2,9 @@
<h1>Post #<%- ctx.post.id %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li><a href='/post/<%- ctx.post.id %>'><i class='fa fa-reply'></i> Main view</a></li><!--
--><li><a href='<%- ctx.formatClientLink('post', ctx.post.id) %>'><i class='fa fa-reply'></i> Main view</a></li><!--
--><% if (ctx.canMerge) { %><!--
--><li data-name='merge'><a href='/post/<%- ctx.post.id %>/merge'>Merge with&hellip;</a></li><!--
--><li data-name='merge'><a href='<%- ctx.formatClientLink('post', ctx.post.id, 'merge') %>'>Merge with&hellip;</a></li><!--
--><% } %><!--
--></ul><!--
--></nav>

View file

@ -67,14 +67,14 @@
--><% for (let tag of ctx.post.tags) { %><!--
--><li><!--
--><% if (ctx.canViewTags) { %><!--
--><a href='/tag/<%- encodeURIComponent(tag) %>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><!--
--><a href='<%- ctx.formatClientLink('tag', tag) %>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><!--
--><i class='fa fa-tag'></i><!--
--><% } %><!--
--><% if (ctx.canViewTags) { %><!--
--></a><!--
--><% } %><!--
--><% if (ctx.canListPosts) { %><!--
--><a href='/posts/query=<%- encodeURIComponent(tag) %>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><!--
--><a href='<%- ctx.formatClientLink('posts', {query: tag}) %>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><!--
--><% } %><!--
--><%- tag %>&#32;<!--
--><% if (ctx.canListPosts) { %><!--

View file

@ -8,7 +8,7 @@
%><input data-safety=sketchy type='button' class='mousetrap safety safety-sketchy <%- ctx.settings.listPosts.sketchy ? '' : 'disabled' %>'/><%
%><input data-safety=unsafe type='button' class='mousetrap safety safety-unsafe <%- ctx.settings.listPosts.unsafe ? '' : 'disabled' %>'/><%
%><wbr/><%
%><a class='mousetrap button append' href='/help/search/posts'>Syntax help</a><%
%><a class='mousetrap button append' href='<%- ctx.formatClientLink('help', 'search', 'posts') %>'>Syntax help</a><%
%><% if (ctx.canMassTag) { %><%
%><wbr/><%
%><span class='masstag'><%

View file

@ -5,7 +5,7 @@
<li>
<a class='thumbnail-wrapper <%= post.tags.length > 0 ? "tags" : "no-tags" %>'
title='@<%- post.id %> (<%- post.type %>)&#10;&#10;Tags: <%- post.tags.map(tag => '#' + tag).join(' ') || 'none' %>'
href='<%= ctx.canViewPosts ? ctx.getPostUrl(post.id, ctx.parameters) : "" %>'>
href='<%= ctx.canViewPosts ? ctx.getPostUrl(post.id, ctx.parameters) : '' %>'>
<%= ctx.makeThumbnail(post.thumbnailUrl) %>
<span class='type' data-type='<%- post.type %>'>
<%- post.type %>

View file

@ -5,7 +5,7 @@
<ul class='input'>
<li>
<%= ctx.makeCheckbox({
text: "Enable keyboard shortcuts <a class='append icon' href='/help/keyboard'><i class='fa fa-question-circle-o'></i></a>",
text: "Enable keyboard shortcuts <a class='append icon' href='" + ctx.formatClientLink('help', 'keyboard') + "'><i class='fa fa-question-circle-o'></i></a>",
name: 'keyboard-shortcuts',
checked: ctx.browsingSettings.keyboardShortcuts,
}) %>

View file

@ -2,15 +2,15 @@
<h1><%- ctx.tag.names[0] %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li data-name='summary'><a href='/tag/<%- encodeURIComponent(ctx.tag.names[0]) %>'>Summary</a></li><!--
--><li data-name='summary'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0]) %>'>Summary</a></li><!--
--><% if (ctx.canEditAnything) { %><!--
--><li data-name='edit'><a href='/tag/<%- encodeURIComponent(ctx.tag.names[0]) %>/edit'>Edit</a></li><!--
--><li data-name='edit'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0], 'edit') %>'>Edit</a></li><!--
--><% } %><!--
--><% if (ctx.canMerge) { %><!--
--><li data-name='merge'><a href='/tag/<%- encodeURIComponent(ctx.tag.names[0]) %>/merge'>Merge with&hellip;</a></li><!--
--><li data-name='merge'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0], 'merge') %>'>Merge with&hellip;</a></li><!--
--><% } %><!--
--><% if (ctx.canDelete) { %><!--
--><li data-name='delete'><a href='/tag/<%- encodeURIComponent(ctx.tag.names[0]) %>/delete'>Delete</a></li><!--
--><li data-name='delete'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0], 'delete') %>'>Delete</a></li><!--
--><% } %><!--
--></ul><!--
--></nav>

View file

@ -19,7 +19,7 @@
</td>
<td class='usages'>
<% if (ctx.tagCategory.name) { %>
<a href='/tags/query=category:<%- encodeURIComponent(ctx.tagCategory.name) %>'>
<a href='<%- ctx.formatClientLink('tags', {query: 'category:' + ctx.tagCategory.name}) %>'>
<%- ctx.tagCategory.tagCount %>
</a>
<% } else { %>

View file

@ -1,6 +1,6 @@
<div class='tag-delete'>
<form>
<p>This tag has <a href='/posts/query=<%- encodeURIComponent(ctx.tag.names[0]) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.tag.names[0]}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
<ul class='input'>
<li>

View file

@ -36,6 +36,6 @@
<section class='description'>
<hr/>
<%= ctx.makeMarkdown(ctx.tag.description || 'This tag has no description yet.') %>
<p>This tag has <a href='/posts/query=<%- encodeURIComponent(ctx.tag.names[0]) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.tag.names[0]}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
</section>
</div>

View file

@ -8,9 +8,9 @@
<div class='buttons'>
<input type='submit' value='Search'/>
<a class='button append' href='/help/search/tags'>Syntax help</a>
<a class='button append' href='<%- ctx.formatClientLink('help', 'search', 'tags') %>'>Syntax help</a>
<% if (ctx.canEditTagCategories) { %>
<a class='append' href='/tag-categories'>Tag categories</a>
<a class='append' href='<%- ctx.formatClientLink('tag-categories') %>'>Tag categories</a>
<% } %>
</div>
</form>

View file

@ -4,37 +4,37 @@
<thead>
<th class='names'>
<% if (ctx.query == 'sort:name' || !ctx.query) { %>
<a href='/tags/query=-sort:name'>Tag name(s)</a>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:name'}) %>'>Tag name(s)</a>
<% } else { %>
<a href='/tags/query=sort:name'>Tag name(s)</a>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:name'}) %>'>Tag name(s)</a>
<% } %>
</th>
<th class='implications'>
<% if (ctx.query == 'sort:implication-count') { %>
<a href='/tags/query=-sort:implication-count'>Implications</a>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:implication-count'}) %>'>Implications</a>
<% } else { %>
<a href='/tags/query=sort:implication-count'>Implications</a>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:implication-count'}) %>'>Implications</a>
<% } %>
</th>
<th class='suggestions'>
<% if (ctx.query == 'sort:suggestion-count') { %>
<a href='/tags/query=-sort:suggestion-count'>Suggestions</a>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:suggestion-count'}) %>'>Suggestions</a>
<% } else { %>
<a href='/tags/query=sort:suggestion-count'>Suggestions</a>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:suggestion-count'}) %>'>Suggestions</a>
<% } %>
</th>
<th class='usages'>
<% if (ctx.query == 'sort:usages') { %>
<a href='/tags/query=-sort:usages'>Usages</a>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:usages'}) %>'>Usages</a>
<% } else { %>
<a href='/tags/query=sort:usages'>Usages</a>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:usages'}) %>'>Usages</a>
<% } %>
</th>
<th class='creation-time'>
<% if (ctx.query == 'sort:creation-time') { %>
<a href='/tags/query=-sort:creation-time'>Created on</a>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:creation-time'}) %>'>Created on</a>
<% } else { %>
<a href='/tags/query=sort:creation-time'>Created on</a>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:creation-time'}) %>'>Created on</a>
<% } %>
</th>
</thead>

View file

@ -2,12 +2,12 @@
<h1><%- ctx.user.name %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li data-name='summary'><a href='/user/<%- encodeURIComponent(ctx.user.name) %>'>Summary</a></li><!--
--><li data-name='summary'><a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'>Summary</a></li><!--
--><% if (ctx.canEditAnything) { %><!--
--><li data-name='edit'><a href='/user/<%- encodeURIComponent(ctx.user.name) %>/edit'>Account settings</a></li><!--
--><li data-name='edit'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'edit') %>'>Account settings</a></li><!--
--><% } %><!--
--><% if (ctx.canDelete) { %><!--
--><li data-name='delete'><a href='/user/<%- encodeURIComponent(ctx.user.name) %>/delete'>Account deletion</a></li><!--
--><li data-name='delete'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'delete') %>'>Account deletion</a></li><!--
--><% } %><!--
--></ul><!--
--></nav>

View file

@ -51,6 +51,6 @@
<li><i class='fa fa-star-half-o'></i> vote up/down on posts and comments</li>
</ul>
<hr/>
<p>By creating an account, you are agreeing to the <a href='/help/tos'>Terms of Service</a>.</p>
<p>By creating an account, you are agreeing to the <a href='<%- ctx.formatClientLink('help', 'tos') %>'>Terms of Service</a>.</p>
</div>
</div>

View file

@ -10,9 +10,9 @@
<nav>
<p><strong>Quick links</strong></p>
<ul>
<li><a href='/posts/query=submit:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.uploadedPostCount %> uploads</a></li>
<li><a href='/posts/query=fav:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.favoritePostCount %> favorites</a></li>
<li><a href='/posts/query=comment:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.commentCount %> comments</a></li>
<li><a href='<%- ctx.formatClientLink('posts', {query: 'submit:' + ctx.user.name}) %>'><%- ctx.user.uploadedPostCount %> uploads</a></li>
<li><a href='<%- ctx.formatClientLink('posts', {query: 'fav:' + ctx.user.name}) %>'><%- ctx.user.favoritePostCount %> favorites</a></li>
<li><a href='<%- ctx.formatClientLink('posts', {query: 'comment:' + ctx.user.name}) %>'><%- ctx.user.commentCount %> comments</a></li>
</ul>
</nav>
@ -20,8 +20,8 @@
<nav>
<p><strong>Only visible to you</strong></p>
<ul>
<li><a href='/posts/query=special:liked'><%- ctx.user.likedPostCount %> liked posts</a></li>
<li><a href='/posts/query=special:disliked'><%- ctx.user.dislikedPostCount %> disliked posts</a></li>
<li><a href='<%- ctx.formatClientLink('posts', {query: 'special:liked'}) %>'><%- ctx.user.likedPostCount %> liked posts</a></li>
<li><a href='<%- ctx.formatClientLink('posts', {query: 'special:disliked'}) %>'><%- ctx.user.dislikedPostCount %> disliked posts</a></li>
</ul>
</nav>
<% } %>

View file

@ -8,7 +8,7 @@
<div class='buttons'>
<input type='submit' value='Search'/>
<a class='append' href='/help/search/users'>Syntax help</a>
<a class='append' href='<%- ctx.formatClientLink('help', 'search', 'users') %>'>Syntax help</a>
</div>
</form>
</div>

View file

@ -4,7 +4,7 @@
--><li>
<div class='wrapper'>
<% if (ctx.canViewUsers) { %>
<a class='image' href='/user/<%- encodeURIComponent(user.name) %>'>
<a class='image' href='<%- ctx.formatClientLink('user', user.name) %>'>
<% } %>
<%= ctx.makeThumbnail(user.avatarUrl) %>
<% if (ctx.canViewUsers) { %>
@ -12,7 +12,7 @@
<% } %>
<div class='details'>
<% if (ctx.canViewUsers) { %>
<a href='/user/<%- encodeURIComponent(user.name) %>'>
<a href='<%- ctx.formatClientLink('user', user.name) %>'>
<% } %>
<%- user.name %>
<% if (ctx.canViewUsers) { %>

View file

@ -5,6 +5,7 @@ const request = require('superagent');
const config = require('./config.js');
const events = require('./events.js');
const progress = require('./util/progress.js');
const uri = require('./util/uri.js');
let fileTokens = {};

View file

@ -2,6 +2,7 @@
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const topNavigation = require('../models/top_navigation.js');
const LoginView = require('../views/login_view.js');
@ -21,7 +22,7 @@ class LoginController {
api.forget();
api.login(e.detail.name, e.detail.password, e.detail.remember)
.then(() => {
const ctx = router.show('/');
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('Logged in');
}, error => {
this._loginView.showError(error.message);
@ -34,16 +35,16 @@ class LogoutController {
constructor() {
api.forget();
api.logout();
const ctx = router.show('/');
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('Logged out');
}
}
module.exports = router => {
router.enter('/login', (ctx, next) => {
router.enter(['login'], (ctx, next) => {
ctx.controller = new LoginController();
});
router.enter('/logout', (ctx, next) => {
router.enter(['logout'], (ctx, next) => {
ctx.controller = new LogoutController();
});
};

View file

@ -1,7 +1,7 @@
'use strict';
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const PostList = require('../models/post_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
@ -28,7 +28,7 @@ class CommentsController {
getClientUrlForPage: page => {
const parameters = Object.assign(
{}, ctx.parameters, {page: page});
return '/comments/' + misc.formatUrlParameters(parameters);
return uri.formatClientLink('comments', parameters);
},
requestPage: page => {
return PostList.search(
@ -69,7 +69,6 @@ class CommentsController {
};
module.exports = router => {
router.enter('/comments/:parameters?',
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
router.enter(['comments'],
(ctx, next) => { new CommentsController(ctx); });
};

View file

@ -12,13 +12,13 @@ class HelpController {
}
module.exports = router => {
router.enter('/help', (ctx, next) => {
router.enter(['help'], (ctx, next) => {
new HelpController();
});
router.enter('/help/:section', (ctx, next) => {
router.enter(['help', ':section'], (ctx, next) => {
new HelpController(ctx.parameters.section);
});
router.enter('/help/:section/:subsection', (ctx, next) => {
router.enter(['help', ':section', ':subsection'], (ctx, next) => {
new HelpController(ctx.parameters.section, ctx.parameters.subsection);
});
};

View file

@ -44,7 +44,7 @@ class HomeController {
};
module.exports = router => {
router.enter('/', (ctx, next) => {
router.enter([], (ctx, next) => {
ctx.controller = new HomeController();
});
};

View file

@ -12,7 +12,7 @@ class NotFoundController {
};
module.exports = router => {
router.enter('*', (ctx, next) => {
router.enter(null, (ctx, next) => {
ctx.controller = new NotFoundController(ctx.canonicalPath);
});
};

View file

@ -2,6 +2,7 @@
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const topNavigation = require('../models/top_navigation.js');
const PasswordResetView = require('../views/password_reset_view.js');
@ -20,7 +21,7 @@ class PasswordResetController {
this._passwordResetView.disableForm();
api.forget();
api.logout();
api.get('/password-reset/' + e.detail.userNameOrEmail)
api.get(uri.formatApiLink('password-reset', e.detail.userNameOrEmail))
.then(() => {
this._passwordResetView.showSuccess(
'E-mail has been sent. To finish the procedure, ' +
@ -37,26 +38,26 @@ class PasswordResetFinishController {
api.forget();
api.logout();
let password = null;
api.post('/password-reset/' + name, {token: token})
api.post(uri.formatApiLink('password-reset', name), {token: token})
.then(response => {
password = response.password;
return api.login(name, password, false);
}).then(() => {
const ctx = router.show('/');
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('New password: ' + password);
}, error => {
const ctx = router.show('/');
const ctx = router.show(uri.formatClientLink());
ctx.controller.showError(error.message);
});
}
}
module.exports = router => {
router.enter('/password-reset', (ctx, next) => {
router.enter(['password-reset'], (ctx, next) => {
ctx.controller = new PasswordResetController();
});
router.enter(/\/password-reset\/([^:]+):([^:]+)$/, (ctx, next) => {
ctx.controller = new PasswordResetFinishController(
ctx.parameters[0], ctx.parameters[1]);
router.enter(['password-reset', ':descriptor'], (ctx, next) => {
const [name, token] = ctx.parameters.descriptor.split(':', 2);
ctx.controller = new PasswordResetFinishController(name, token);
});
};

View file

@ -3,6 +3,7 @@
const router = require('../router.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const settings = require('../models/settings.js');
const Post = require('../models/post.js');
const PostList = require('../models/post_list.js');
@ -55,7 +56,8 @@ class PostDetailController extends BasePostController {
misc.disableExitConfirmation();
if (this._id !== e.detail.post.id) {
router.replace(
'/post/' + e.detail.post.id + '/' + section, null, false);
uri.formatClientLink('post', e.detail.post.id, section),
null, false);
}
}
@ -67,7 +69,9 @@ class PostDetailController extends BasePostController {
this._installView(e.detail.post, 'merge');
this._view.showSuccess('Post merged.');
router.replace(
'/post/' + e.detail.targetPost.id + '/merge', null, false);
uri.formatClientLink(
'post', e.detail.targetPost.id, 'merge'),
null, false);
}, error => {
this._view.showError(error.message);
this._view.enableForm();
@ -77,7 +81,7 @@ class PostDetailController extends BasePostController {
module.exports = router => {
router.enter(
'/post/:id/merge',
['post', ':id', 'merge'],
(ctx, next) => {
ctx.controller = new PostDetailController(ctx, 'merge');
});

View file

@ -2,7 +2,7 @@
const api = require('../api.js');
const settings = require('../models/settings.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const PostList = require('../models/post_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
@ -52,7 +52,7 @@ class PostListController {
history.pushState(
null,
window.title,
'/posts/' + misc.formatUrlParameters(e.detail.parameters));
uri.formatClientLink('posts', e.detail.parameters));
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
@ -89,7 +89,7 @@ class PostListController {
this._pageController.run({
parameters: this._ctx.parameters,
getClientUrlForPage: page => {
return '/posts/' + misc.formatUrlParameters(
return uri.formatClientLink('posts',
Object.assign({}, this._ctx.parameters, {page: page}));
},
requestPage: page => {
@ -114,7 +114,6 @@ class PostListController {
module.exports = router => {
router.enter(
'/posts/:parameters(.*)?',
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
['posts'],
(ctx, next) => { ctx.controller = new PostListController(ctx); });
};

View file

@ -2,6 +2,7 @@
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const settings = require('../models/settings.js');
const Comment = require('../models/comment.js');
@ -29,8 +30,8 @@ class PostMainController extends BasePostController {
if (parameters.query) {
ctx.state.parameters = parameters;
const url = editMode ?
'/post/' + ctx.parameters.id + '/edit' :
'/post/' + ctx.parameters.id;
uri.formatClientLink('post', ctx.parameters.id, 'edit') :
uri.formatClientLink('post', ctx.parameters.id);
router.replace(url, ctx.state, false);
}
@ -124,7 +125,7 @@ class PostMainController extends BasePostController {
}
_evtMergePost(e) {
router.show('/post/' + e.detail.post.id + '/merge');
router.show(uri.formatClientLink('post', e.detail.post.id, 'merge'));
}
_evtDeletePost(e) {
@ -133,7 +134,7 @@ class PostMainController extends BasePostController {
e.detail.post.delete()
.then(() => {
misc.disableExitConfirmation();
const ctx = router.show('/posts');
const ctx = router.show(uri.formatClientLink('posts'));
ctx.controller.showSuccess('Post deleted.');
}, error => {
this._view.sidebarControl.showError(error.message);
@ -244,8 +245,7 @@ class PostMainController extends BasePostController {
}
module.exports = router => {
router.enter('/post/:id/edit/:parameters(.*)?',
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
router.enter(['post', ':id', 'edit'],
(ctx, next) => {
// restore parameters from history state
if (ctx.state.parameters) {
@ -254,8 +254,7 @@ module.exports = router => {
ctx.controller = new PostMainController(ctx, true);
});
router.enter(
'/post/:id/:parameters(.*)?',
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
['post', ':id'],
(ctx, next) => {
// restore parameters from history state
if (ctx.state.parameters) {

View file

@ -2,6 +2,7 @@
const api = require('../api.js');
const router = require('../router.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const progress = require('../util/progress.js');
const topNavigation = require('../models/top_navigation.js');
@ -61,7 +62,7 @@ class PostUploadController {
.then(() => {
this._view.clearMessages();
misc.disableExitConfirmation();
const ctx = router.show('/posts');
const ctx = router.show(uri.formatClientLink('posts'));
ctx.controller.showSuccess('Posts uploaded.');
}, error => {
if (error.uploadable) {
@ -149,7 +150,7 @@ class PostUploadController {
}
module.exports = router => {
router.enter('/upload', (ctx, next) => {
router.enter(['upload'], (ctx, next) => {
ctx.controller = new PostUploadController();
});
};

View file

@ -22,7 +22,7 @@ class SettingsController {
};
module.exports = router => {
router.enter('/settings', (ctx, next) => {
router.enter(['settings'], (ctx, next) => {
ctx.controller = new SettingsController();
});
};

View file

@ -1,7 +1,7 @@
'use strict';
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const SnapshotList = require('../models/snapshot_list.js');
const PageController = require('../controllers/page_controller.js');
const topNavigation = require('../models/top_navigation.js');
@ -25,7 +25,7 @@ class SnapshotsController {
getClientUrlForPage: page => {
const parameters = Object.assign(
{}, ctx.parameters, {page: page});
return '/history/' + misc.formatUrlParameters(parameters);
return uri.formatClientLink('history', parameters);
},
requestPage: page => {
return SnapshotList.search('', page, 25);
@ -43,7 +43,6 @@ class SnapshotsController {
}
module.exports = router => {
router.enter('/history/:parameters?',
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
router.enter(['history'],
(ctx, next) => { ctx.controller = new SnapshotsController(ctx); });
};

View file

@ -51,7 +51,7 @@ class TagCategoriesController {
}
module.exports = router => {
router.enter('/tag-categories', (ctx, next) => {
router.enter(['tag-categories'], (ctx, next) => {
ctx.controller = new TagCategoriesController(ctx, next);
});
};

View file

@ -3,6 +3,7 @@
const router = require('../router.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const tags = require('../tags.js');
const Tag = require('../models/tag.js');
const topNavigation = require('../models/top_navigation.js');
@ -61,7 +62,8 @@ class TagController {
misc.disableExitConfirmation();
if (this._name !== e.detail.tag.names[0]) {
router.replace(
'/tag/' + e.detail.tag.names[0] + '/' + section, null, false);
uri.formatClientLink('tag', e.detail.tag.names[0], section),
null, false);
}
}
@ -99,7 +101,8 @@ class TagController {
this._view.showSuccess('Tag merged.');
this._view.enableForm();
router.replace(
'/tag/' + e.detail.targetTagName + '/merge', null, false);
uri.formatClientLink('tag', e.detail.targetTagName, 'merge'),
null, false);
}, error => {
this._view.showError(error.message);
this._view.enableForm();
@ -111,7 +114,7 @@ class TagController {
this._view.disableForm();
e.detail.tag.delete()
.then(() => {
const ctx = router.show('/tags/');
const ctx = router.show(uri.formatClientLink('tags'));
ctx.controller.showSuccess('Tag deleted.');
}, error => {
this._view.showError(error.message);
@ -121,16 +124,16 @@ class TagController {
}
module.exports = router => {
router.enter('/tag/:name(.+?)/edit', (ctx, next) => {
router.enter(['tag', ':name', 'edit'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'edit');
});
router.enter('/tag/:name(.+?)/merge', (ctx, next) => {
router.enter(['tag', ':name', 'merge'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'merge');
});
router.enter('/tag/:name(.+?)/delete', (ctx, next) => {
router.enter(['tag', ':name', 'delete'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'delete');
});
router.enter('/tag/:name(.+)', (ctx, next) => {
router.enter(['tag', ':name'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'summary');
});
};

View file

@ -1,7 +1,7 @@
'use strict';
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const TagList = require('../models/tag_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
@ -49,7 +49,7 @@ class TagListController {
history.pushState(
null,
window.title,
'/tags/' + misc.formatUrlParameters(e.detail.parameters));
uri.formatClientLink('tags', e.detail.parameters));
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
@ -60,7 +60,7 @@ class TagListController {
getClientUrlForPage: page => {
const parameters = Object.assign(
{}, this._ctx.parameters, {page: page});
return '/tags/' + misc.formatUrlParameters(parameters);
return uri.formatClientLink('tags', parameters);
},
requestPage: page => {
return TagList.search(
@ -75,7 +75,6 @@ class TagListController {
module.exports = router => {
router.enter(
'/tags/:parameters(.*)?',
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
['tags'],
(ctx, next) => { ctx.controller = new TagListController(ctx); });
};

View file

@ -2,6 +2,7 @@
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const config = require('../config.js');
const views = require('../util/views.js');
@ -77,7 +78,8 @@ class UserController {
misc.disableExitConfirmation();
if (this._name !== e.detail.user.name) {
router.replace(
'/user/' + e.detail.user.name + '/' + section, null, false);
uri.formatClientLink('user', e.detail.user.name, section),
null, false);
}
}
@ -135,10 +137,10 @@ class UserController {
api.logout();
}
if (api.hasPrivilege('users:list')) {
const ctx = router.show('/users');
const ctx = router.show(uri.formatClientLink('users'));
ctx.controller.showSuccess('Account deleted.');
} else {
const ctx = router.show('/');
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('Account deleted.');
}
}, error => {
@ -149,13 +151,13 @@ class UserController {
}
module.exports = router => {
router.enter('/user/:name', (ctx, next) => {
router.enter(['user', ':name'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'summary');
});
router.enter('/user/:name/edit', (ctx, next) => {
router.enter(['user', ':name', 'edit'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'edit');
});
router.enter('/user/:name/delete', (ctx, next) => {
router.enter(['user', ':name', 'delete'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'delete');
});
};

View file

@ -1,7 +1,7 @@
'use strict';
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const UserList = require('../models/user_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
@ -41,7 +41,7 @@ class UserListController {
history.pushState(
null,
window.title,
'/users/' + misc.formatUrlParameters(e.detail.parameters));
uri.formatClientLink('users', e.detail.parameters));
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
@ -52,7 +52,7 @@ class UserListController {
getClientUrlForPage: page => {
const parameters = Object.assign(
{}, this._ctx.parameters, {page: page});
return '/users/' + misc.formatUrlParameters(parameters);
return uri.formatClientLink('users', parameters);
},
requestPage: page => {
return UserList.search(this._ctx.parameters.query, page);
@ -69,7 +69,6 @@ class UserListController {
module.exports = router => {
router.enter(
'/users/:parameters(.*)?',
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
['users'],
(ctx, next) => { ctx.controller = new UserListController(ctx); });
};

View file

@ -2,6 +2,7 @@
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const User = require('../models/user.js');
const topNavigation = require('../models/top_navigation.js');
const RegistrationView = require('../views/registration_view.js');
@ -32,7 +33,7 @@ class UserRegistrationController {
api.forget();
return api.login(e.detail.name, e.detail.password, false);
}).then(() => {
const ctx = router.show('/');
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('Welcome aboard!');
}, error => {
this._view.showError(error.message);
@ -42,7 +43,7 @@ class UserRegistrationController {
}
module.exports = router => {
router.enter('/register', (ctx, next) => {
router.enter(['register'], (ctx, next) => {
new UserRegistrationController();
});
};

View file

@ -3,6 +3,7 @@
const api = require('../api.js');
const tags = require('../tags.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const settings = require('../models/settings.js');
const events = require('../events.js');
const views = require('../util/views.js');
@ -308,7 +309,7 @@ class TagInputControl extends events.EventTarget {
tagLinkNode.classList.add(className);
}
tagLinkNode.setAttribute(
'href', '/tag/' + encodeURIComponent(tagName));
'href', uri.formatClientLink('tag', tagName));
const tagIconNode = document.createElement('i');
tagIconNode.classList.add('fa');
tagIconNode.classList.add('fa-tag');
@ -319,7 +320,7 @@ class TagInputControl extends events.EventTarget {
searchLinkNode.classList.add(className);
}
searchLinkNode.setAttribute(
'href', '/posts/query=' + encodeURIComponent(tagName));
'href', uri.formatClientLink('posts', {query: tagName}));
searchLinkNode.textContent = tagName + ' ';
searchLinkNode.addEventListener('click', e => {
e.preventDefault();
@ -360,7 +361,9 @@ class TagInputControl extends events.EventTarget {
if (!browsingSettings.tagSuggestions) {
return;
}
api.get('/tag-siblings/' + tag.names[0], {noProgress: true})
api.get(
uri.formatApiLink('tag-siblings', tag.names[0]),
{noProgress: true})
.then(response => {
return Promise.resolve(response.results);
}, response => {

View file

@ -8,7 +8,7 @@ const router = require('./router.js');
history.scrollRestoration = 'manual';
router.exit(
/.*/,
null,
(ctx, next) => {
ctx.state.scrollX = window.scrollX;
ctx.state.scrollY = window.scrollY;
@ -20,7 +20,7 @@ router.exit(
const mousetrap = require('mousetrap');
router.enter(
/.*/,
null,
(ctx, next) => {
mousetrap.reset();
next();

View file

@ -1,6 +1,7 @@
'use strict';
const api = require('../api.js');
const uri = require('../util/uri.js');
const events = require('../events.js');
class Comment extends events.EventTarget {
@ -38,9 +39,9 @@ class Comment extends events.EventTarget {
text: this._text,
};
let promise = this._id ?
api.put('/comment/' + this._id, detail) :
api.post(
'/comments', Object.assign({postId: this._postId}, detail));
api.put(uri.formatApiLink('comment', this.id), detail) :
api.post(uri.formatApiLink('comments'),
Object.assign({postId: this._postId}, detail));
return promise.then(response => {
this._updateFromResponse(response);
@ -55,7 +56,7 @@ class Comment extends events.EventTarget {
delete() {
return api.delete(
'/comment/' + this._id,
uri.formatApiLink('comment', this.id),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
@ -68,7 +69,9 @@ class Comment extends events.EventTarget {
}
setScore(score) {
return api.put('/comment/' + this._id + '/score', {score: score})
return api.put(
uri.formatApiLink('comment', this.id, 'score'),
{score: score})
.then(response => {
this._updateFromResponse(response);
this.dispatchEvent(new CustomEvent('changeScore', {

View file

@ -1,11 +1,12 @@
'use strict';
const api = require('../api.js');
const uri = require('../util/uri.js');
const Post = require('./post.js');
class Info {
static get() {
return api.get('/info')
return api.get(uri.formatApiLink('info'))
.then(response => {
return Promise.resolve(Object.assign(
{},

View file

@ -1,6 +1,7 @@
'use strict';
const api = require('../api.js');
const uri = require('../util/uri.js');
const tags = require('../tags.js');
const events = require('../events.js');
const NoteList = require('./note_list.js');
@ -69,7 +70,9 @@ class Post extends events.EventTarget {
static reverseSearch(content) {
let apiPromise = api.post(
'/posts/reverse-search', {}, {content: content});
uri.formatApiLink('posts', 'reverse-search'),
{},
{content: content});
let returnedPromise = apiPromise
.then(response => {
if (response.exactPost) {
@ -85,7 +88,7 @@ class Post extends events.EventTarget {
}
static get(id) {
return api.get('/post/' + id)
return api.get(uri.formatApiLink('post', id))
.then(response => {
return Promise.resolve(Post.fromResponse(response));
});
@ -149,8 +152,8 @@ class Post extends events.EventTarget {
}
let apiPromise = this._id ?
api.put('/post/' + this._id, detail, files) :
api.post('/posts', detail, files);
api.put(uri.formatApiLink('post', this.id), detail, files) :
api.post(uri.formatApiLink('posts'), detail, files);
return apiPromise.then(response => {
this._updateFromResponse(response);
@ -176,14 +179,18 @@ class Post extends events.EventTarget {
}
feature() {
return api.post('/featured-post', {id: this._id})
return api.post(
uri.formatApiLink('featured-post'),
{id: this._id})
.then(response => {
return Promise.resolve();
});
}
delete() {
return api.delete('/post/' + this._id, {version: this._version})
return api.delete(
uri.formatApiLink('post', this.id),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
@ -195,9 +202,9 @@ class Post extends events.EventTarget {
}
merge(targetId, useOldContent) {
return api.get('/post/' + encodeURIComponent(targetId))
return api.get(uri.formatApiLink('post', targetId))
.then(response => {
return api.post('/post-merge/', {
return api.post(uri.formatApiLink('post-merge'), {
removeVersion: this._version,
remove: this._id,
mergeToVersion: response.version,
@ -216,7 +223,9 @@ class Post extends events.EventTarget {
}
setScore(score) {
return api.put('/post/' + this._id + '/score', {score: score})
return api.put(
uri.formatApiLink('post', this.id, 'score'),
{score: score})
.then(response => {
const prevFavorite = this._ownFavorite;
this._updateFromResponse(response);
@ -237,7 +246,7 @@ class Post extends events.EventTarget {
}
addToFavorites() {
return api.post('/post/' + this.id + '/favorite')
return api.post(uri.formatApiLink('post', this.id, 'favorite'))
.then(response => {
const prevScore = this._ownScore;
this._updateFromResponse(response);
@ -258,7 +267,7 @@ class Post extends events.EventTarget {
}
removeFromFavorites() {
return api.delete('/post/' + this.id + '/favorite')
return api.delete(uri.formatApiLink('post', this.id, 'favorite'))
.then(response => {
const prevScore = this._ownScore;
this._updateFromResponse(response);

View file

@ -1,29 +1,32 @@
'use strict';
const api = require('../api.js');
const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js');
const Post = require('./post.js');
class PostList extends AbstractList {
static getAround(id, searchQuery) {
const url =
`/post/${id}/around?fields=id` +
`&query=${encodeURIComponent(searchQuery)}`;
return api.get(url);
return api.get(
uri.formatApiLink(
'post', id, 'around', {query: searchQuery, fields: 'id'}));
}
static search(text, page, pageSize, fields) {
const url =
`/posts/?query=${encodeURIComponent(text)}` +
`&page=${page}` +
`&pageSize=${pageSize}` +
`&fields=${fields.join(',')}`;
return api.get(url).then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: PostList.fromResponse(response.results)}));
});
return api.get(
uri.formatApiLink(
'posts', {
query: text,
page: page,
pageSize: pageSize,
fields: fields.join(','),
}))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: PostList.fromResponse(response.results)}));
});
}
}

View file

@ -1,21 +1,20 @@
'use strict';
const api = require('../api.js');
const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js');
const Snapshot = require('./snapshot.js');
class SnapshotList extends AbstractList {
static search(text, page, pageSize) {
const url =
`/snapshots/?query=${encodeURIComponent(text)}` +
`&page=${page}` +
`&pageSize=${pageSize}`;
return api.get(url).then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: SnapshotList.fromResponse(response.results)}));
});
return api.get(uri.formatApiLink(
'snapshots', {query: text, page: page, pageSize: pageSize}))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: SnapshotList.fromResponse(response.results)}));
});
}
}

View file

@ -1,6 +1,7 @@
'use strict';
const api = require('../api.js');
const uri = require('../util/uri.js');
const events = require('../events.js');
const misc = require('../util/misc.js');
@ -33,7 +34,7 @@ class Tag extends events.EventTarget {
}
static get(name) {
return api.get('/tag/' + encodeURIComponent(name))
return api.get(uri.formatApiLink('tag', name))
.then(response => {
return Promise.resolve(Tag.fromResponse(response));
});
@ -60,8 +61,8 @@ class Tag extends events.EventTarget {
}
let promise = this._origName ?
api.put('/tag/' + encodeURIComponent(this._origName), detail) :
api.post('/tags', detail);
api.put(uri.formatApiLink('tag', this._origName), detail) :
api.post(uri.formatApiLink('tags'), detail);
return promise
.then(response => {
this._updateFromResponse(response);
@ -75,9 +76,9 @@ class Tag extends events.EventTarget {
}
merge(targetName) {
return api.get('/tag/' + encodeURIComponent(targetName))
return api.get(uri.formatApiLink('tag', targetName))
.then(response => {
return api.post('/tag-merge/', {
return api.post(uri.formatApiLink('tag-merge'), {
removeVersion: this._version,
remove: this._origName,
mergeToVersion: response.version,
@ -96,7 +97,7 @@ class Tag extends events.EventTarget {
delete() {
return api.delete(
'/tag/' + encodeURIComponent(this._origName),
uri.formatApiLink('tag', this._origName),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {

View file

@ -1,6 +1,7 @@
'use strict';
const api = require('../api.js');
const uri = require('../util/uri.js');
const events = require('../events.js');
class TagCategory extends events.EventTarget {
@ -45,9 +46,9 @@ class TagCategory extends events.EventTarget {
let promise = this._origName ?
api.put(
'/tag-category/' + encodeURIComponent(this._origName),
uri.formatApiLink('tag-category', this._origName),
detail) :
api.post('/tag-categories', detail);
api.post(uri.formatApiLink('tag-categories'), detail);
return promise
.then(response => {
@ -63,7 +64,7 @@ class TagCategory extends events.EventTarget {
delete() {
return api.delete(
'/tag-category/' + encodeURIComponent(this._origName),
uri.formatApiLink('tag-category', this._origName),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {

View file

@ -1,6 +1,7 @@
'use strict';
const api = require('../api.js');
const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js');
const TagCategory = require('./tag_category.js');
@ -26,12 +27,13 @@ class TagCategoryList extends AbstractList {
}
static get() {
return api.get('/tag-categories/').then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: TagCategoryList.fromResponse(response.results)}));
});
return api.get(uri.formatApiLink('tag-categories'))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: TagCategoryList.fromResponse(response.results)}));
});
}
get defaultCategory() {
@ -54,7 +56,10 @@ class TagCategoryList extends AbstractList {
if (this._defaultCategory !== this._origDefaultCategory) {
promises.push(
api.put(
`/tag-category/${this._defaultCategory.name}/default`));
uri.formatApiLink(
'tag-category',
this._defaultCategory.name,
'default')));
}
return Promise.all(promises)

View file

@ -1,22 +1,26 @@
'use strict';
const api = require('../api.js');
const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js');
const Tag = require('./tag.js');
class TagList extends AbstractList {
static search(text, page, pageSize, fields) {
const url =
`/tags/?query=${encodeURIComponent(text)}` +
`&page=${page}` +
`&pageSize=${pageSize}` +
`&fields=${fields.join(',')}`;
return api.get(url).then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: TagList.fromResponse(response.results)}));
});
return api.get(
uri.formatApiLink(
'tags', {
query: text,
page: page,
pageSize: pageSize,
fields: fields.join(','),
}))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: TagList.fromResponse(response.results)}));
});
}
}

View file

@ -1,6 +1,7 @@
'use strict';
const api = require('../api.js');
const uri = require('../util/uri.js');
const events = require('../events.js');
class User extends events.EventTarget {
@ -40,7 +41,7 @@ class User extends events.EventTarget {
}
static get(name) {
return api.get('/user/' + encodeURIComponent(name))
return api.get(uri.formatApiLink('user', name))
.then(response => {
return Promise.resolve(User.fromResponse(response));
});
@ -73,10 +74,8 @@ class User extends events.EventTarget {
let promise = this._orig._name ?
api.put(
'/user/' + encodeURIComponent(this._orig._name),
detail,
files) :
api.post('/users', detail, files);
uri.formatApiLink('user', this._orig._name), detail, files) :
api.post(uri.formatApiLink('users'), detail, files);
return promise
.then(response => {
@ -92,7 +91,7 @@ class User extends events.EventTarget {
delete() {
return api.delete(
'/user/' + encodeURIComponent(this._orig._name),
uri.formatApiLink('user', this._orig._name),
{version: this._version})
.then(response => {
this.dispatchEvent(new CustomEvent('delete', {

View file

@ -1,20 +1,21 @@
'use strict';
const api = require('../api.js');
const uri = require('../util/uri.js');
const AbstractList = require('./abstract_list.js');
const User = require('./user.js');
class UserList extends AbstractList {
static search(text, page) {
const url =
`/users/?query=${encodeURIComponent(text)}` +
`&page=${page}&pageSize=30`;
return api.get(url).then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: UserList.fromResponse(response.results)}));
});
return api.get(
uri.formatApiLink(
'users', {query: text, page: page, pageSize: 30}))
.then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: UserList.fromResponse(response.results)}));
});
}
}

View file

@ -1,6 +1,7 @@
'use strict';
// modified page.js by visionmedia
// - changed regexes to components
// - removed unused crap
// - refactored to classes
// - simplified method chains
@ -9,19 +10,12 @@
// - rename .save() to .replaceState()
// - offer .url
const pathToRegexp = require('path-to-regexp');
const clickEvent = document.ontouchstart ? 'touchstart' : 'click';
const uri = require('./util/uri.js');
let location = window.history.location || window.location;
const base = '';
function _decodeURLEncodedURIComponent(val) {
if (typeof val !== 'string') {
return val;
}
return decodeURIComponent(val.replace(/\+/g, ' '));
}
function _isSameOrigin(href) {
let origin = location.protocol + '//' + location.hostname;
if (location.port) {
@ -55,11 +49,28 @@ class Context {
};
class Route {
constructor(path, options) {
options = options || {};
this.path = (path === '*') ? '(.*)' : path;
constructor(path) {
this.method = 'GET';
this.regexp = pathToRegexp(this.path, this.keys = [], options);
this.path = path;
this.parameterNames = [];
if (this.path === null) {
this.regex = /.*/;
} else {
let parts = [];
for (let component of this.path) {
if (component[0] === ':') {
parts.push('([^/]+)');
this.parameterNames.push(component.substr(1));
} else { // assert [a-z]+
parts.push(component);
}
}
let regexString = '^/' + parts.join('/');
regexString += '(?:/*|/((?:(?:[a-z]+=[^/]+);)*(?:[a-z]+=[^/]+)))$';
this.parameterNames.push('variable');
this.regex = new RegExp(regexString);
}
}
middleware(fn) {
@ -72,24 +83,39 @@ class Route {
}
match(path, parameters) {
const keys = this.keys;
const qsIndex = path.indexOf('?');
const pathname = ~qsIndex ? path.slice(0, qsIndex) : path;
const m = this.regexp.exec(pathname);
const match = this.regex.exec(pathname);
if (!m) {
if (!match) {
return false;
}
for (let i = 1, len = m.length; i < len; ++i) {
const key = keys[i - 1];
const val = _decodeURLEncodedURIComponent(m[i]);
if (val !== undefined ||
!(hasOwnProperty.call(parameters, key.name))) {
parameters[key.name] = val;
try {
for (let i = 1; i < match.length; i++) {
const name = this.parameterNames[i - 1];
const value = match[i];
if (value === undefined) {
continue;
}
if (name === 'variable') {
for (let word of (value || '').split(/;/)) {
const [key, subvalue] = word.split(/=/, 2);
parameters[key] = uri.unescapeParam(subvalue);
}
} else {
parameters[name] = uri.unescapeParam(value);
}
}
} catch (e) {
return false;
}
// XXX: it is very unfitting place for this
parameters.query = parameters.query || '';
parameters.page = parseInt(parameters.page || '1');
return true;
}
};

View file

@ -1,6 +1,7 @@
'use strict';
const markdown = require('./markdown.js');
const uri = require('./uri.js');
function decamelize(str, sep) {
sep = sep === undefined ? '-' : sep;
@ -99,44 +100,10 @@ function formatInlineMarkdown(text) {
return markdown.formatInlineMarkdown(text);
}
function formatUrlParameters(dict) {
let result = [];
for (let key of Object.keys(dict)) {
const value = dict[key];
if (key === 'parameters') {
continue;
}
if (value) {
result.push(`${key}=${encodeURIComponent(value)}`);
}
}
return result.join(';');
}
function splitByWhitespace(str) {
return str.split(/\s+/).filter(s => s);
}
function parseUrlParameters(query) {
let result = {};
for (let word of (query || '').split(/;/)) {
const [key, value] = word.split(/=/, 2);
result[key] = value;
}
result.query = result.query || '';
result.page = parseInt(result.page || '1');
return result;
}
function parseUrlParametersRoute(ctx, next) {
// ctx.parameters = {"user":...,"action":...} from /users/:user/:action
// ctx.parameters.parameters = value of :parameters as per /url/:parameters
Object.assign(
ctx.parameters,
parseUrlParameters(ctx.parameters.parameters));
next();
}
function unindent(callSite, ...args) {
function format(str) {
let size = -1;
@ -232,9 +199,6 @@ function dataURItoBlob(dataURI) {
module.exports = {
range: range,
formatUrlParameters: formatUrlParameters,
parseUrlParameters: parseUrlParameters,
parseUrlParametersRoute: parseUrlParametersRoute,
formatRelativeTime: formatRelativeTime,
formatFileSize: formatFileSize,
formatMarkdown: formatMarkdown,

62
client/js/util/uri.js Normal file
View file

@ -0,0 +1,62 @@
'use strict';
function formatApiLink(...values) {
let parts = [];
for (let value of values) {
if (value.constructor === Object) {
// assert this is the last piece
let variableParts = [];
for (let key of Object.keys(value)) {
if (value[key]) {
variableParts.push(
key + '=' + encodeURIComponent(value[key].toString()));
}
}
if (variableParts.length) {
parts.push('?' + variableParts.join('&'));
}
break;
} else {
parts.push(encodeURIComponent(value.toString()));
}
}
return '/' + parts.join('/');
}
function escapeParam(text) {
return encodeURIComponent(text).replace(/%/g, '$');
}
function unescapeParam(text) {
return decodeURIComponent(text.replace(/\$/g, '%'));
}
function formatClientLink(...values) {
let parts = [];
for (let value of values) {
if (value.constructor === Object) {
// assert this is the last piece
let variableParts = [];
for (let key of Object.keys(value)) {
if (value[key]) {
variableParts.push(
key + '=' + escapeParam(value[key].toString()));
}
}
if (variableParts.length) {
parts.push(variableParts.join(';'));
}
break;
} else {
parts.push(escapeParam(value.toString()));
}
}
return '/' + parts.join('/');
}
module.exports = {
formatClientLink: formatClientLink,
formatApiLink: formatApiLink,
escapeParam: escapeParam,
unescapeParam: unescapeParam,
};

View file

@ -6,6 +6,7 @@ const templates = require('../templates.js');
const tags = require('../tags.js');
const domParser = new DOMParser();
const misc = require('./misc.js');
const uri = require('./uri.js');
function _imbueId(options) {
if (!options.id) {
@ -152,19 +153,15 @@ function makeNumericInput(options) {
}
function getPostUrl(id, parameters) {
let url = '/post/' + encodeURIComponent(id);
if (parameters && parameters.query) {
url += '/query=' + encodeURIComponent(parameters.query);
}
return url;
return uri.formatClientLink(
'post', id,
parameters ? {query: parameters.query} : {});
}
function getPostEditUrl(id, parameters) {
let url = '/post/' + encodeURIComponent(id) + '/edit';
if (parameters && parameters.query) {
url += '/query=' + encodeURIComponent(parameters.query);
}
return url;
return uri.formatClientLink(
'post', id, 'edit',
parameters ? {query: parameters.query} : {});
}
function makePostLink(id, includeHash) {
@ -175,7 +172,7 @@ function makePostLink(id, includeHash) {
return api.hasPrivilege('posts:view') ?
makeElement(
'a',
{'href': '/post/' + encodeURIComponent(id)},
{href: uri.formatClientLink('post', id)},
misc.escapeHtml(text)) :
misc.escapeHtml(text);
}
@ -191,13 +188,13 @@ function makeTagLink(name, includeHash) {
makeElement(
'a',
{
'href': '/tag/' + encodeURIComponent(name),
'class': misc.makeCssName(category, 'tag'),
href: uri.formatClientLink('tag', name),
class: misc.makeCssName(category, 'tag'),
},
misc.escapeHtml(text)) :
makeElement(
'span',
{'class': misc.makeCssName(category, 'tag')},
{class: misc.makeCssName(category, 'tag')},
misc.escapeHtml(text));
}
@ -206,7 +203,7 @@ function makeUserLink(user) {
text += user && user.name ? misc.escapeHtml(user.name) : 'Anonymous';
const link = user && api.hasPrivilege('users:view') ?
makeElement(
'a', {'href': '/user/' + encodeURIComponent(user.name)}, text) :
'a', {href: uri.formatClientLink('user', user.name)}, text) :
text;
return makeElement('span', {class: 'user'}, link);
}
@ -385,6 +382,7 @@ function getTemplate(templatePath) {
makeElement: makeElement,
makeCssName: misc.makeCssName,
makeNumericInput: makeNumericInput,
formatClientLink: uri.formatClientLink
});
return htmlToDom(templateFactory(ctx));
};

View file

@ -1,7 +1,7 @@
'use strict';
const router = require('../router.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const views = require('../util/views.js');
const PostContentControl = require('../controls/post_content_control.js');
const PostNotesOverlayControl
@ -88,7 +88,7 @@ class HomeView {
_evtFormSubmit(e) {
e.preventDefault();
this._searchInputNode.blur();
router.show('/posts/' + misc.formatUrlParameters({
router.show(uri.formatClientLink('posts', {
query: this._searchInputNode.value}));
}
}

View file

@ -2,7 +2,6 @@
const router = require('../router.js');
const keyboard = require('../util/keyboard.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
const holderTemplate = views.getTemplate('manual-pager');

View file

@ -2,6 +2,7 @@
const router = require('../router.js');
const views = require('../util/views.js');
const uri = require('../util/uri.js');
const keyboard = require('../util/keyboard.js');
const PostContentControl = require('../controls/post_content_control.js');
const PostNotesOverlayControl =
@ -61,19 +62,19 @@ class PostMainView {
keyboard.bind('e', () => {
if (ctx.editMode) {
router.show('/post/' + ctx.post.id);
router.show(uri.formatClientLink('post', ctx.post.id));
} else {
router.show('/post/' + ctx.post.id + '/edit');
router.show(uri.formatClientLink('post', ctx.post.id, 'edit'));
}
});
keyboard.bind(['a', 'left'], () => {
if (ctx.prevPostId) {
router.show('/post/' + ctx.prevPostId);
router.show(uri.formatClientLink('post', ctx.prevPostId));
}
});
keyboard.bind(['d', 'right'], () => {
if (ctx.nextPostId) {
router.show('/post/' + ctx.nextPostId);
router.show(uri.formatClientLink('post', ctx.nextPostId));
}
});
}

View file

@ -1,7 +1,6 @@
'use strict';
const events = require('../events.js');
const misc = require('../util/misc.js');
const search = require('../util/search.js');
const views = require('../util/views.js');

View file

@ -22,7 +22,6 @@
"merge": "^1.2.0",
"mousetrap": "^1.5.3",
"nprogress": "^0.2.0",
"path-to-regexp": "^1.5.1",
"stylus": "^0.54.2",
"superagent": "^1.8.3",
"uglify-js": "git://github.com/mishoo/UglifyJS2.git#harmony",