This commit is contained in:
Eva
2025-05-31 06:20:40 +02:00
committed by GitHub
32 changed files with 686 additions and 331 deletions

View File

@ -36,6 +36,7 @@ $button-disabled-background-color = #CCC
$post-thumbnail-border-color = $main-color $post-thumbnail-border-color = $main-color
$post-thumbnail-no-tags-border-color = #F44 $post-thumbnail-no-tags-border-color = #F44
$default-tag-category-background-color = $active-tab-background-color $default-tag-category-background-color = $active-tab-background-color
$default-pool-category-background-color = $active-tab-background-color
$new-tag-background-color = #DFC $new-tag-background-color = #DFC
$new-tag-text-color = black $new-tag-text-color = black
$implied-tag-background-color = #FFC $implied-tag-background-color = #FFC

View File

@ -1,47 +1,100 @@
@import colors @import colors
.pool-list .pool-list
table ul
width: 100% list-style-type: none
border-spacing: 0 padding: 0
text-align: left display: flex
line-height: 1.3em align-content: flex-end
tr:hover td flex-wrap: wrap
background: $top-navigation-color margin: 0 -0.25em
th, td
padding: 0.1em 0.5em
th
white-space: nowrap
background: $top-navigation-color
.names
width: 84%
.post-count
text-align: center
width: 8%
.creation-time
text-align: center
width: 8%
white-space: pre
ul
list-style-type: none
margin: 0
padding: 0
display: inline
li
padding: 0
display: inline
&:not(:last-child):after
content: ', '
@media (max-width: 800px)
.posts
display: none
.darktheme .pool-list li
table position: relative
tr:hover td flex-grow: 1
background: $top-navigation-color-darktheme margin: 2em 1.5em 2em 1.2em
th display: inline-block
background: $top-navigation-color-darktheme text-align: left
min-width: 10em
width: 12vw
&:not(.flexbox-dummy)
min-height: 7.5em
height: 9vw
.thumbnail-wrapper
display: inline-block
width: 100%
height: 100%
line-height: 80%
font-size: 80%
color: white
outline: none
border-right: 20px solid transparent
&:before
content: ' '
display: block
position: relative
width: 100%
height: 20px
bottom: 20px
.thumbnail
width: 100%
height: 100%
outline-offset: -2px
background-size: cover
transition: top .1s ease-in-out, right .1s ease-in-out
background-position: 50% 30%
position: absolute
display: inline-block
box-shadow: 0 0 0 1px rgba(0,0,0,0.2)
.thumbnail-1, .thumbnail.empty
right: -4px
top: -4px
z-index: 30
.thumbnail-2
right: -10px
top: -10px
z-index: 20
.thumbnail-3
right: -16px
top: -16px
z-index: 10
.pool-name
color: black
font-size: 1em
text-align: center
a
width: 100%
display: inline-block
a:active, a:focus
.thumbnail
outline: 2px solid $main-color !important
.pool-list ul li:hover
.thumbnail-wrapper
.thumbnail-1
right: -0px
top: -0px
.thumbnail-3
right: -20px
top: -20px
.pool-list ul li:has(a:focus), .pool-list ul li:has(a:active)
.thumbnail-wrapper
.thumbnail-1
right: -0px
top: -0px
.thumbnail-3
right: -20px
top: -20px
.pool-list-header .pool-list-header
label label
@ -61,3 +114,21 @@
.darktheme .pool-list-header .darktheme .pool-list-header
.append .append
color: $inactive-link-color-darktheme color: $inactive-link-color-darktheme
.post-flow
ul
li
min-width: inherit
width: inherit
margin: 0 0.25em 0.5em 0.25em
&:not(.flexbox-dummy)
height: 14vw
.thumbnail
position: static
outline-offset: -1px
.thumbnail-wrapper.no-tags
.thumbnail
outline: 2px solid $post-thumbnail-no-tags-border-color
&:hover a, a:active, a:focus
.thumbnail
outline: 2px solid $main-color !important

View File

@ -0,0 +1,39 @@
@import colors
.pool-navigator-container
padding: 0
margin: 0 auto
.pool-info-wrapper
box-sizing: border-box
width: 100%
margin: 0 0 1em 0
display: flex
padding: 0.5em 1em
border: 1px solid $line-color
background: $top-navigation-color
.pool-name
flex: 1 1
text-align: center
overflow: hidden
white-space: nowrap
-o-text-overflow: ellipsis
text-overflow: ellipsis
.first, .last
flex-basis: 1em
.first, .prev, .next, .last
flex: 0 1
white-space: nowrap
>span
padding-top: 2px
padding-bottom: 2px
margin: 0 .25em
.darktheme .pool-navigator-container .pool-info-wrapper
border: 1px solid $top-navigation-color-darktheme
background: $window-color-darktheme

View File

@ -0,0 +1,9 @@
.pool-navigators>ul
list-style-type: none
margin: 0
padding: 0
>li
margin-bottom: 1em
&:last-child
margin-bottom: 0

View File

@ -329,6 +329,10 @@
<td><code>feature-time</code></td> <td><code>feature-time</code></td>
<td>alias of <code>feature-time</code></td> <td>alias of <code>feature-time</code></td>
</tr> </tr>
<tr>
<td><code>pool</code></td>
<td>pool order, requires pool named token</code></td>
</tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,6 +1,6 @@
<div class='pool-delete'> <div class='pool-delete'>
<form> <form>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p> <p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id + ' -sort:pool'}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
<ul class='input'> <ul class='input'>
<li> <li>

View File

@ -0,0 +1,49 @@
<div class='pool-navigator-container'>
<div class='pool-info-wrapper'>
<span class='first'>
<% if (ctx.canViewPosts && ctx.previousPost && ctx.firstPost) { %>
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.firstPost.id, ctx.parameters) %>'>
<% } %>
«
<% if (ctx.canViewPosts && ctx.previousPost && ctx.firstPost) { %>
</a>
<% } %>
</span>
<span class='prev'>
<% if (ctx.canViewPosts && ctx.previousPost) { %>
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.previousPost.id, ctx.parameters) %>'>
<% } %>
prev
<% if (ctx.canViewPosts && ctx.previousPost) { %>
</a>
<% } %>
</span>
<span class='pool-name'>
<% if (ctx.canViewPools) { %>
<a class='<%- ctx.linkClass %>' href='<%= ctx.formatClientLink("pool", ctx.pool.id) %>'>
<% } %>
Pool: <%- ctx.getPrettyName(ctx.pool.names[0]) %>
<% if (ctx.canViewPools) { %>
</a>
<% } %>
</span>
<span class='next'>
<% if (ctx.canViewPosts && ctx.nextPost) { %>
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.nextPost.id, ctx.parameters) %>'>
<% } %>
next
<% if (ctx.canViewPosts && ctx.nextPost) { %>
</a>
<% } %>
</span>
<span class='last'>
<% if (ctx.canViewPosts && ctx.nextPost && ctx.lastPost) { %>
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.lastPost.id, ctx.parameters) %>'>
<% } %>
»
<% if (ctx.canViewPosts && ctx.nextPost && ctx.lastPost) { %>
</a>
<% } %>
</span>
</div>
</div>

View File

@ -0,0 +1,4 @@
<div class='pool-navigators'>
<ul>
</ul>
</div>

View File

@ -18,6 +18,6 @@
<section class='description'> <section class='description'>
<hr/> <hr/>
<%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %> <%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p> <p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id + ' -sort:pool'}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
</section> </section>
</div> </div>

View File

@ -1,48 +1,19 @@
<div class='pool-list table-wrap'> <% if (ctx.postFlow) { %><div class='pool-list post-flow'><% } else { %><div class='pool-list'><% } %>
<% if (ctx.response.results.length) { %> <% if (ctx.response.results.length) { %>
<table> <ul>
<thead> <% for (let pool of ctx.response.results) { %>
<th class='names'> <li data-pool-id='<%= pool.id %>'>
<% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %> <a class='thumbnail-wrapper' href='<%= ctx.canViewPools ? ctx.formatClientLink("pool", pool.id) : "" %>'>
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:name'}) %>'>Pool name(s)</a> <% if (ctx.canViewPosts) { %>
<% } else { %> <%= ctx.makePoolThumbnails(pool.posts, ctx.postFlow) %>
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:name'}) %>'>Pool name(s)</a>
<% } %> <% } %>
</th> </a>
<th class='post-count'> <div class='pool-name'>
<% if (ctx.parameters.query == 'sort:post-count') { %> <%= ctx.makePoolLink(pool.id, false, false, pool, name) %>
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:post-count'}) %>'>Post count</a> </div>
<% } else { %> </li>
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:post-count'}) %>'>Post count</a> <% } %>
<% } %> <%= ctx.makeFlexboxAlign() %>
</th> </ul>
<th class='creation-time'>
<% if (ctx.parameters.query == 'sort:creation-time') { %>
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:creation-time'}) %>'>Created on</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:creation-time'}) %>'>Created on</a>
<% } %>
</th>
</thead>
<tbody>
<% for (let pool of ctx.response.results) { %>
<tr>
<td class='names'>
<ul>
<% for (let name of pool.names) { %>
<li><%= ctx.makePoolLink(pool.id, false, false, pool, name) %></li>
<% } %>
</ul>
</td>
<td class='post-count'>
<a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + pool.id}) %>'><%- pool.postCount %></a>
</td>
<td class='creation-time'>
<%= ctx.makeRelativeTime(pool.creationTime) %>
</td>
</tr>
<% } %>
</tbody>
</table>
<% } %> <% } %>
</div> </div>

View File

@ -52,6 +52,10 @@
<div class='content'> <div class='content'>
<div class='post-container'></div> <div class='post-container'></div>
<% if (ctx.canListPools && ctx.canViewPools) { %>
<div class='pool-navigators-container'></div>
<% } %>
<div class='after-mobile-controls'> <div class='after-mobile-controls'>
<% if (ctx.canCreateComments) { %> <% if (ctx.canCreateComments) { %>
<h2>Add comment</h2> <h2>Add comment</h2>

View File

@ -2,6 +2,7 @@
const router = require("../router.js"); const router = require("../router.js");
const api = require("../api.js"); const api = require("../api.js");
const settings = require("../models/settings.js");
const uri = require("../util/uri.js"); const uri = require("../util/uri.js");
const PoolList = require("../models/pool_list.js"); const PoolList = require("../models/pool_list.js");
const topNavigation = require("../models/top_navigation.js"); const topNavigation = require("../models/top_navigation.js");
@ -13,7 +14,6 @@ const EmptyView = require("../views/empty_view.js");
const fields = [ const fields = [
"id", "id",
"names", "names",
"posts",
"creationTime", "creationTime",
"postCount", "postCount",
"category", "category",
@ -100,14 +100,21 @@ class PoolListController {
return uri.formatClientLink("pools", parameters); return uri.formatClientLink("pools", parameters);
}, },
requestPage: (offset, limit) => { requestPage: (offset, limit) => {
const canEditPosts = api.hasPrivilege("pools:edit") || api.hasPrivilege("pools:edit:posts");
const effectiveFields = fields.concat([canEditPosts ? "posts": "postsMicro"]);
return PoolList.search( return PoolList.search(
this._ctx.parameters.query, this._ctx.parameters.query,
offset, offset,
limit, limit,
fields effectiveFields
); );
}, },
pageRenderer: (pageCtx) => { pageRenderer: (pageCtx) => {
Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege("posts:view"),
canViewPools: api.hasPrivilege("pools:view"),
postFlow: settings.get().postFlow,
});
return new PoolsPageView(pageCtx); return new PoolsPageView(pageCtx);
}, },
}); });

View File

@ -11,6 +11,7 @@ const PostList = require("../models/post_list.js");
const PostMainView = require("../views/post_main_view.js"); const PostMainView = require("../views/post_main_view.js");
const BasePostController = require("./base_post_controller.js"); const BasePostController = require("./base_post_controller.js");
const EmptyView = require("../views/empty_view.js"); const EmptyView = require("../views/empty_view.js");
const PoolNavigatorListControl = require("../controls/pool_navigator_list_control.js");
class PostMainController extends BasePostController { class PostMainController extends BasePostController {
constructor(ctx, editMode) { constructor(ctx, editMode) {
@ -26,6 +27,7 @@ class PostMainController extends BasePostController {
]).then( ]).then(
(responses) => { (responses) => {
const [post, aroundResponse] = responses; const [post, aroundResponse] = responses;
let aroundPool = null;
// remove junk from query, but save it into history so that it can // remove junk from query, but save it into history so that it can
// be still accessed after history navigation / page refresh // be still accessed after history navigation / page refresh
@ -39,23 +41,36 @@ class PostMainController extends BasePostController {
) )
: uri.formatClientLink("post", ctx.parameters.id); : uri.formatClientLink("post", ctx.parameters.id);
router.replace(url, ctx.state, false); router.replace(url, ctx.state, false);
misc.splitByWhitespace(parameters.query).forEach((item) => {
const found = item.match(/^pool:([0-9]+)/i);
if (found) {
const activePool = parseInt(found[1]);
post.pools.map((pool) => {
if (pool.id == activePool) {
aroundPool = pool;
}
});
}
});
} }
this._post = post; this._post = post;
this._view = new PostMainView({ this._view = new PostMainView({
post: post, post: post,
editMode: editMode, editMode: editMode,
prevPostId: aroundResponse.prev prevPostId: aroundPool
? aroundResponse.prev.id ? (aroundPool.previousPost ? aroundPool.previousPost.id : null)
: null, : (aroundResponse.prev ? aroundResponse.prev.id : null),
nextPostId: aroundResponse.next nextPostId: aroundPool
? aroundResponse.next.id ? (aroundPool.nextPost ? aroundPool.nextPost.id : null)
: null, : (aroundResponse.next ? aroundResponse.next.id : null),
canEditPosts: api.hasPrivilege("posts:edit"), canEditPosts: api.hasPrivilege("posts:edit"),
canDeletePosts: api.hasPrivilege("posts:delete"), canDeletePosts: api.hasPrivilege("posts:delete"),
canFeaturePosts: api.hasPrivilege("posts:feature"), canFeaturePosts: api.hasPrivilege("posts:feature"),
canListComments: api.hasPrivilege("comments:list"), canListComments: api.hasPrivilege("comments:list"),
canCreateComments: api.hasPrivilege("comments:create"), canCreateComments: api.hasPrivilege("comments:create"),
canListPools: api.hasPrivilege("pools:list"),
canViewPools: api.hasPrivilege("pools:view"),
parameters: parameters, parameters: parameters,
}); });

View File

@ -0,0 +1,34 @@
"use strict";
const api = require("../api.js");
const misc = require("../util/misc.js");
const events = require("../events.js");
const views = require("../util/views.js");
const template = views.getTemplate("pool-navigator");
class PoolNavigatorControl extends events.EventTarget {
constructor(hostNode, poolPostNearby) {
super();
this._hostNode = hostNode;
this._poolPostNearby = poolPostNearby;
views.replaceContent(
this._hostNode,
template({
pool: poolPostNearby,
parameters: { query: `pool:${poolPostNearby.id}` },
linkClass: misc.makeCssName(poolPostNearby.category, "pool"),
canViewPosts: api.hasPrivilege("posts:view"),
canViewPools: api.hasPrivilege("pools:view"),
firstPost: poolPostNearby.firstPost,
previousPost: poolPostNearby.previousPost,
nextPost: poolPostNearby.nextPost,
lastPost: poolPostNearby.lastPost,
getPrettyName: misc.getPrettyName,
})
);
}
}
module.exports = PoolNavigatorControl;

View File

@ -0,0 +1,49 @@
"use strict";
const events = require("../events.js");
const views = require("../util/views.js");
const PoolNavigatorControl = require("../controls/pool_navigator_control.js");
const template = views.getTemplate("pool-navigator-list");
class PoolNavigatorListControl extends events.EventTarget {
constructor(hostNode, poolPostNearby) {
super();
this._hostNode = hostNode;
this._poolPostNearby = poolPostNearby;
this._indexToNode = {};
for (const entry of this._poolPostNearby) {
this._installPoolNavigatorNode(entry);
}
}
get _poolNavigatorListNode() {
return this._hostNode;
}
_installPoolNavigatorNode(poolPostNearby) {
const poolListItemNode = document.createElement("div");
const poolControl = new PoolNavigatorControl(
poolListItemNode,
poolPostNearby,
);
this._indexToNode[poolPostNearby.id] = poolListItemNode;
this._poolNavigatorListNode.appendChild(poolListItemNode);
}
_uninstallPoolNavigatorNode(index) {
const poolListItemNode = this._indexToNode[index];
poolListItemNode.parentNode.removeChild(poolListItemNode);
}
_evtAdd(e) {
this._installPoolNavigatorNode(e.detail.index);
}
_evtRemove(e) {
this._uninstallPoolNavigatorNode(e.detail.index);
}
}
module.exports = PoolNavigatorListControl;

View File

@ -36,7 +36,7 @@ class Pool extends events.EventTarget {
} }
get posts() { get posts() {
return this._posts; return this._postsMicro || this._posts;
} }
get postCount() { get postCount() {
@ -51,6 +51,22 @@ class Pool extends events.EventTarget {
return this._lastEditTime; return this._lastEditTime;
} }
get firstPost() {
return this._firstPost;
}
get lastPost() {
return this._lastPost;
}
get previousPost() {
return this._previousPost;
}
get nextPost() {
return this._nextPost;
}
set names(value) { set names(value) {
this._names = value; this._names = value;
} }
@ -169,10 +185,15 @@ class Pool extends events.EventTarget {
_creationTime: response.creationTime, _creationTime: response.creationTime,
_lastEditTime: response.lastEditTime, _lastEditTime: response.lastEditTime,
_postCount: response.postCount || 0, _postCount: response.postCount || 0,
_postsMicro: response.postsMicro,
_firstPost: response.firstPost || null,
_lastPost: response.lastPost || null,
_previousPost: response.previousPost || null,
_nextPost: response.nextPost || null,
}; };
for (let obj of [this, this._orig]) { for (let obj of [this, this._orig]) {
obj._posts.sync(response.posts); obj._posts.sync(response.posts || []);
} }
Object.assign(this, map); Object.assign(this, map);

View File

@ -40,19 +40,36 @@ function makeRelativeTime(time) {
); );
} }
function makeThumbnail(url) { function makeThumbnail(url, klass, extraProperties) {
return makeElement( return makeElement(
"span", "span",
url url
? { ? {
class: "thumbnail", class: klass || "thumbnail",
style: `background-image: url(\'${url}\')`, style: `background-image: url(\'${url}\')`,
} }
: { class: "thumbnail empty" }, : { class: "thumbnail empty" },
makeElement("img", { alt: "thumbnail", src: url }) makeElement("img", Object.assign({ alt: "thumbnail", src: url }, extraProperties || {}))
); );
} }
function makePoolThumbnails(posts, postFlow) {
if (posts.length == 0) {
return makeThumbnail(null);
}
if (postFlow) {
return makeThumbnail(posts.at(0).thumbnailUrl);
}
let s = "";
for (let i = 0; i < Math.min(3, posts.length); i++) {
s += makeThumbnail(posts.at(i).thumbnailUrl, "thumbnail thumbnail-" + (i+1), i === 0 ? {fetchPriority: "high"} : {});
}
return s;
}
function makeRadio(options) { function makeRadio(options) {
_imbueId(options); _imbueId(options);
return makeElement( return makeElement(
@ -254,7 +271,7 @@ function makePoolLink(id, includeHash, includeCount, pool, name) {
misc.escapeHtml(text) misc.escapeHtml(text)
) )
: makeElement( : makeElement(
"span", "div",
{ class: misc.makeCssName(category, "pool") }, { class: misc.makeCssName(category, "pool") },
misc.escapeHtml(text) misc.escapeHtml(text)
); );
@ -436,6 +453,7 @@ function getTemplate(templatePath) {
makeFileSize: makeFileSize, makeFileSize: makeFileSize,
makeMarkdown: makeMarkdown, makeMarkdown: makeMarkdown,
makeThumbnail: makeThumbnail, makeThumbnail: makeThumbnail,
makePoolThumbnails: makePoolThumbnails,
makeRadio: makeRadio, makeRadio: makeRadio,
makeCheckbox: makeCheckbox, makeCheckbox: makeCheckbox,
makeSelect: makeSelect, makeSelect: makeSelect,

View File

@ -30,7 +30,7 @@ class PoolCreateView extends events.EventTarget {
} }
for (let node of this._formNode.querySelectorAll( for (let node of this._formNode.querySelectorAll(
"input, select, textarea, posts" "input, select, textarea"
)) { )) {
node.addEventListener("change", (e) => { node.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change")); this.dispatchEvent(new CustomEvent("change"));

View File

@ -10,6 +10,7 @@ const PostContentControl = require("../controls/post_content_control.js");
const PostNotesOverlayControl = require("../controls/post_notes_overlay_control.js"); const PostNotesOverlayControl = require("../controls/post_notes_overlay_control.js");
const PostReadonlySidebarControl = require("../controls/post_readonly_sidebar_control.js"); const PostReadonlySidebarControl = require("../controls/post_readonly_sidebar_control.js");
const PostEditSidebarControl = require("../controls/post_edit_sidebar_control.js"); const PostEditSidebarControl = require("../controls/post_edit_sidebar_control.js");
const PoolNavigatorListControl = require("../controls/pool_navigator_list_control.js");
const CommentControl = require("../controls/comment_control.js"); const CommentControl = require("../controls/comment_control.js");
const CommentListControl = require("../controls/comment_list_control.js"); const CommentListControl = require("../controls/comment_list_control.js");
@ -57,6 +58,7 @@ class PostMainView {
this._installSidebar(ctx); this._installSidebar(ctx);
this._installCommentForm(); this._installCommentForm();
this._installComments(ctx.post.comments); this._installComments(ctx.post.comments);
this._installPoolNavigators(ctx);
const showPreviousImage = () => { const showPreviousImage = () => {
if (ctx.prevPostId) { if (ctx.prevPostId) {
@ -137,6 +139,20 @@ class PostMainView {
} }
} }
_installPoolNavigators(ctx) {
const poolNavigatorsContainerNode = document.querySelector(
"#content-holder .pool-navigators-container"
);
if (!poolNavigatorsContainerNode) {
return;
}
this.poolNavigatorsControl = new PoolNavigatorListControl(
poolNavigatorsContainerNode,
ctx.post.pools,
);
}
_installCommentForm() { _installCommentForm() {
const commentFormContainer = document.querySelector( const commentFormContainer = document.querySelector(
"#content-holder .comment-form-container" "#content-holder .comment-form-container"

264
client/package-lock.json generated
View File

@ -27,13 +27,19 @@
"html-minifier": "^3.5.18", "html-minifier": "^3.5.18",
"jimp": "^0.13.0", "jimp": "^0.13.0",
"pretty-error": "^3.0.3", "pretty-error": "^3.0.3",
"stylus": "^0.54.8", "stylus": "^0.59.0",
"terser": "^4.8.1", "terser": "^4.8.1",
"underscore": "^1.12.1", "underscore": "^1.12.1",
"watchify": "^4.0.0", "watchify": "^4.0.0",
"ws": "^7.4.6" "ws": "^7.4.6"
} }
}, },
"node_modules/@adobe/css-tools": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz",
"integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==",
"dev": true
},
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.10.3", "version": "7.10.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz",
@ -516,18 +522,6 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
}, },
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true,
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz",
@ -1737,27 +1731,6 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/css": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
"integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"source-map": "^0.6.1",
"source-map-resolve": "^0.5.2",
"urix": "^0.1.0"
}
},
"node_modules/css-parse": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz",
"integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
"dev": true,
"dependencies": {
"css": "^2.0.0"
}
},
"node_modules/css-select": { "node_modules/css-select": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
@ -1795,15 +1768,6 @@
"url": "https://github.com/sponsors/fb55" "url": "https://github.com/sponsors/fb55"
} }
}, },
"node_modules/css/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/csso": { "node_modules/csso": {
"version": "3.5.1", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz",
@ -1830,15 +1794,6 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/decode-uri-component": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"dev": true,
"engines": {
"node": ">=0.10"
}
},
"node_modules/define-properties": { "node_modules/define-properties": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -3676,13 +3631,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"deprecated": "https://github.com/lydell/resolve-url#deprecated",
"dev": true
},
"node_modules/ripemd160": { "node_modules/ripemd160": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
@ -3698,12 +3646,6 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}, },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"node_modules/sax": { "node_modules/sax": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@ -3794,19 +3736,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/source-map-resolve": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
"integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
"dev": true,
"dependencies": {
"atob": "^2.1.2",
"decode-uri-component": "^0.2.0",
"resolve-url": "^0.2.1",
"source-map-url": "^0.4.0",
"urix": "^0.1.0"
}
},
"node_modules/source-map-support": { "node_modules/source-map-support": {
"version": "0.4.18", "version": "0.4.18",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
@ -3816,12 +3745,6 @@
"source-map": "^0.5.6" "source-map": "^0.5.6"
} }
}, },
"node_modules/source-map-url": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
"dev": true
},
"node_modules/stream-browserify": { "node_modules/stream-browserify": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
@ -3912,18 +3835,15 @@
} }
}, },
"node_modules/stylus": { "node_modules/stylus": {
"version": "0.54.8", "version": "0.59.0",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz",
"integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==", "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"css-parse": "~2.0.0", "@adobe/css-tools": "^4.0.1",
"debug": "~3.1.0", "debug": "^4.3.2",
"glob": "^7.1.6", "glob": "^7.1.6",
"mkdirp": "~1.0.4",
"safer-buffer": "^2.1.2",
"sax": "~1.2.4", "sax": "~1.2.4",
"semver": "^6.3.0",
"source-map": "^0.7.3" "source-map": "^0.7.3"
}, },
"bin": { "bin": {
@ -3931,28 +3851,33 @@
}, },
"engines": { "engines": {
"node": "*" "node": "*"
},
"funding": {
"url": "https://opencollective.com/stylus"
} }
}, },
"node_modules/stylus/node_modules/mkdirp": { "node_modules/stylus/node_modules/debug": {
"version": "1.0.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true, "dev": true,
"bin": { "dependencies": {
"mkdirp": "bin/cmd.js" "ms": "2.1.2"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
} }
}, },
"node_modules/stylus/node_modules/semver": { "node_modules/stylus/node_modules/ms": {
"version": "6.3.0", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true, "dev": true
"bin": {
"semver": "bin/semver.js"
}
}, },
"node_modules/stylus/node_modules/source-map": { "node_modules/stylus/node_modules/source-map": {
"version": "0.7.3", "version": "0.7.3",
@ -4219,13 +4144,6 @@
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
"dev": true "dev": true
}, },
"node_modules/urix": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
"deprecated": "Please see https://github.com/lydell/urix#deprecated",
"dev": true
},
"node_modules/url": { "node_modules/url": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
@ -4602,6 +4520,12 @@
} }
}, },
"dependencies": { "dependencies": {
"@adobe/css-tools": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz",
"integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==",
"dev": true
},
"@babel/runtime": { "@babel/runtime": {
"version": "7.10.3", "version": "7.10.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz",
@ -5072,12 +4996,6 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
}, },
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true
},
"available-typed-arrays": { "available-typed-arrays": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz",
@ -6246,35 +6164,6 @@
"randomfill": "^1.0.3" "randomfill": "^1.0.3"
} }
}, },
"css": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
"integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"source-map": "^0.6.1",
"source-map-resolve": "^0.5.2",
"urix": "^0.1.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"css-parse": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz",
"integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
"dev": true,
"requires": {
"css": "^2.0.0"
}
},
"css-select": { "css-select": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
@ -6326,12 +6215,6 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"decode-uri-component": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"dev": true
},
"define-properties": { "define-properties": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -7829,12 +7712,6 @@
"path-parse": "^1.0.6" "path-parse": "^1.0.6"
} }
}, },
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"dev": true
},
"ripemd160": { "ripemd160": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
@ -7850,12 +7727,6 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}, },
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"sax": { "sax": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@ -7931,19 +7802,6 @@
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true "dev": true
}, },
"source-map-resolve": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
"integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
"dev": true,
"requires": {
"atob": "^2.1.2",
"decode-uri-component": "^0.2.0",
"resolve-url": "^0.2.1",
"source-map-url": "^0.4.0",
"urix": "^0.1.0"
}
},
"source-map-support": { "source-map-support": {
"version": "0.4.18", "version": "0.4.18",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
@ -7953,12 +7811,6 @@
"source-map": "^0.5.6" "source-map": "^0.5.6"
} }
}, },
"source-map-url": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
"dev": true
},
"stream-browserify": { "stream-browserify": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
@ -8040,31 +7892,31 @@
} }
}, },
"stylus": { "stylus": {
"version": "0.54.8", "version": "0.59.0",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz",
"integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==", "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==",
"dev": true, "dev": true,
"requires": { "requires": {
"css-parse": "~2.0.0", "@adobe/css-tools": "^4.0.1",
"debug": "~3.1.0", "debug": "^4.3.2",
"glob": "^7.1.6", "glob": "^7.1.6",
"mkdirp": "~1.0.4",
"safer-buffer": "^2.1.2",
"sax": "~1.2.4", "sax": "~1.2.4",
"semver": "^6.3.0",
"source-map": "^0.7.3" "source-map": "^0.7.3"
}, },
"dependencies": { "dependencies": {
"mkdirp": { "debug": {
"version": "1.0.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true "dev": true,
"requires": {
"ms": "2.1.2"
}
}, },
"semver": { "ms": {
"version": "6.3.0", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true "dev": true
}, },
"source-map": { "source-map": {
@ -8287,12 +8139,6 @@
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
"dev": true "dev": true
}, },
"urix": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
"dev": true
},
"url": { "url": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",

View File

@ -28,7 +28,7 @@
"html-minifier": "^3.5.18", "html-minifier": "^3.5.18",
"jimp": "^0.13.0", "jimp": "^0.13.0",
"pretty-error": "^3.0.3", "pretty-error": "^3.0.3",
"stylus": "^0.54.8", "stylus": "^0.59.0",
"terser": "^4.8.1", "terser": "^4.8.1",
"underscore": "^1.12.1", "underscore": "^1.12.1",
"watchify": "^4.0.0", "watchify": "^4.0.0",

View File

@ -975,6 +975,43 @@ data.
Retrieves information about posts that are before or after an existing post. Retrieves information about posts that are before or after an existing post.
## Getting pools around post
- **Request**
`GET /post/<id>/pools-nearby`
- **Output**
```json5
[
{
"pool": <pool>,
"firstPost": <first-post>,
"lastPost": <last-post>,
"nextPost": <next-post>,
"previousPost": <previous-post>
},
...
]
```
- **Field meaning**
- `<pool>`: The associated [micro pool resource](#micro-pool).
- `<first-post>`: A [micro post resource](#micro-post) that displays the first post in the pool.
- `<last-post>`: A [micro post resource](#micro-post) that displays the last post in the pool.
- `<next-post>`: A [micro post resource](#micro-post) that displays the next post in the pool.
- `<previous-post>`: A [micro post resource](#micro-post) that displays the previous post in the pool.
- **Errors**
- the post does not exist
- privileges are too low
- **Description**
Retrieves extra information about any pools that the post is in.
## Deleting post ## Deleting post
- **Request** - **Request**

View File

@ -5,12 +5,10 @@ from szurubooru import db, errors, model, rest, search
from szurubooru.func import ( from szurubooru.func import (
auth, auth,
favorites, favorites,
mime,
posts, posts,
scores, scores,
serialization, serialization,
snapshots, snapshots,
tags,
versions, versions,
) )

View File

@ -108,6 +108,7 @@ class PoolSerializer(serialization.BaseSerializer):
"lastEditTime": self.serialize_last_edit_time, "lastEditTime": self.serialize_last_edit_time,
"postCount": self.serialize_post_count, "postCount": self.serialize_post_count,
"posts": self.serialize_posts, "posts": self.serialize_posts,
"postsMicro": self.serialize_posts_micro,
} }
def serialize_id(self) -> Any: def serialize_id(self) -> Any:
@ -143,6 +144,14 @@ class PoolSerializer(serialization.BaseSerializer):
] ]
] ]
def serialize_posts_micro(self) -> Any:
posts_micro = []
for i, rel in enumerate(self.pool.posts):
posts_micro.append(posts.serialize_micro_post(rel, None))
if i == 2:
break
return posts_micro
def serialize_pool( def serialize_pool(
pool: model.Pool, options: List[str] = [] pool: model.Pool, options: List[str] = []

View File

@ -1,12 +1,15 @@
import hmac import hmac
import logging import logging
from collections import namedtuple
from datetime import datetime from datetime import datetime
from itertools import tee, chain, islice
from typing import Any, Callable, Dict, List, Optional, Tuple from typing import Any, Callable, Dict, List, Optional, Tuple
import sqlalchemy as sa import sqlalchemy as sa
from szurubooru import config, db, errors, model, rest from szurubooru import config, db, errors, model, rest
from szurubooru.func import ( from szurubooru.func import (
auth,
comments, comments,
files, files,
image_hash, image_hash,
@ -15,7 +18,6 @@ from szurubooru.func import (
pools, pools,
scores, scores,
serialization, serialization,
snapshots,
tags, tags,
users, users,
util, util,
@ -96,6 +98,13 @@ FLAG_MAP = {
model.Post.FLAG_SOUND: "sound", model.Post.FLAG_SOUND: "sound",
} }
# https://stackoverflow.com/a/1012089
def _get_nearby_iter(post_list):
previous_item, current_item, next_item = tee(post_list, 3)
previous_item = chain([None], previous_item)
next_item = chain(islice(next_item, 1, None), [None])
return zip(previous_item, current_item, next_item)
def get_post_security_hash(id: int) -> str: def get_post_security_hash(id: int) -> str:
return hmac.new( return hmac.new(
@ -337,8 +346,10 @@ class PostSerializer(serialization.BaseSerializer):
] ]
def serialize_pools(self) -> List[Any]: def serialize_pools(self) -> List[Any]:
if not auth.has_privilege(self.auth_user, "pools:list"):
return []
return [ return [
pools.serialize_micro_pool(pool) {**pools.serialize_micro_pool(pool), **get_pool_posts_nearby(self.post, pool)} if auth.has_privilege(self.auth_user, "pools:view") else pools.serialize_micro_pool(pool)
for pool in sorted( for pool in sorted(
self.post.pools, key=lambda pool: pool.creation_time self.post.pools, key=lambda pool: pool.creation_time
) )
@ -968,3 +979,38 @@ def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]:
] ]
else: else:
return [] return []
def serialize_safe_post(
post: Optional[model.Post]
) -> rest.Response:
return {"id": getattr(post, "post_id", None)} if post else None
def serialize_id_post(
post_id: Optional[int]
) -> rest.Response:
return serialize_safe_post(try_get_post_by_id(post_id)) if post_id else None
def get_pool_posts_nearby(
post: model.Post, pool: model.Pool
) -> rest.Response:
prev_post_id = None
next_post_id = None
first_post_id = pool.posts[0].post_id,
last_post_id = pool.posts[-1].post_id,
for previous_item, current_item, next_item in _get_nearby_iter(pool.posts):
if post.post_id == current_item.post_id:
if previous_item != None:
prev_post_id = previous_item.post_id
if next_item != None:
next_post_id = next_item.post_id
break
return {
"firstPost": serialize_id_post(first_post_id),
"lastPost": serialize_id_post(last_post_id),
"previousPost": serialize_id_post(prev_post_id),
"nextPost": serialize_id_post(next_post_id),
}

View File

@ -1,4 +1,4 @@
from typing import Any, Dict, Optional, Tuple from typing import Any, Dict, Optional, Tuple, Callable, Union
import sqlalchemy as sa import sqlalchemy as sa
@ -114,12 +114,26 @@ def _pool_filter(
query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool
) -> SaQuery: ) -> SaQuery:
assert criterion assert criterion
return search_util.create_subquery_filter( from szurubooru.search.configs import util as search_util
model.Post.post_id, subquery = db.session.query(model.PoolPost.post_id.label("foreign_id"))
model.PoolPost.post_id, subquery = subquery.options(sa.orm.lazyload("*"))
model.PoolPost.pool_id, subquery = search_util.create_num_filter(model.PoolPost.pool_id)(subquery, criterion, False)
search_util.create_num_filter, subquery = subquery.subquery("t")
)(query, criterion, negated) expression = model.Post.post_id.in_(subquery)
if negated:
expression = ~expression
return query.filter(expression)
def _pool_sort(
query: SaQuery, pool_id: Optional[int], order: str
) -> SaQuery:
if pool_id is None:
return query
db_query = query.join(model.PoolPost, sa.and_(model.PoolPost.post_id == model.Post.post_id, model.PoolPost.pool_id == pool_id))
if order == tokens.SortToken.SORT_DESC:
return db_query.order_by(model.PoolPost.order.desc())
return db_query.order_by(model.PoolPost.order.asc())
def _category_filter( def _category_filter(
@ -153,6 +167,7 @@ def _category_filter(
class PostSearchConfig(BaseSearchConfig): class PostSearchConfig(BaseSearchConfig):
def __init__(self) -> None: def __init__(self) -> None:
self.user = None # type: Optional[model.User] self.user = None # type: Optional[model.User]
self.pool_id = None # type: Optional[int]
def on_search_query_parsed(self, search_query: SearchQuery) -> SaQuery: def on_search_query_parsed(self, search_query: SearchQuery) -> SaQuery:
new_special_tokens = [] new_special_tokens = []
@ -177,6 +192,10 @@ class PostSearchConfig(BaseSearchConfig):
else: else:
new_special_tokens.append(token) new_special_tokens.append(token)
search_query.special_tokens = new_special_tokens search_query.special_tokens = new_special_tokens
self.pool_id = None
for token in search_query.named_tokens:
if token.name == "pool" and isinstance(token.criterion, criteria.PlainCriterion):
self.pool_id = token.criterion.value
def create_around_query(self) -> SaQuery: def create_around_query(self) -> SaQuery:
return db.session.query(model.Post).options(sa.orm.lazyload("*")) return db.session.query(model.Post).options(sa.orm.lazyload("*"))
@ -382,7 +401,7 @@ class PostSearchConfig(BaseSearchConfig):
) )
@property @property
def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]: def sort_columns(self) -> Dict[str, Union[Tuple[SaColumn, str], Callable[[SaQuery], None]]]:
return util.unalias_dict( return util.unalias_dict(
[ [
( (
@ -444,6 +463,10 @@ class PostSearchConfig(BaseSearchConfig):
["feature-date", "feature-time"], ["feature-date", "feature-time"],
(model.Post.last_feature_time, self.SORT_DESC), (model.Post.last_feature_time, self.SORT_DESC),
), ),
(
["pool"],
lambda subquery, order: _pool_sort(subquery, self.pool_id, order)
)
] ]
) )

View File

@ -205,6 +205,7 @@ def create_subquery_filter(
filter_column: SaColumn, filter_column: SaColumn,
filter_factory: SaColumn, filter_factory: SaColumn,
subquery_decorator: Callable[[SaQuery], None] = None, subquery_decorator: Callable[[SaQuery], None] = None,
order: SaQuery = None,
) -> Filter: ) -> Filter:
filter_func = filter_factory(filter_column) filter_func = filter_factory(filter_column)

View File

@ -181,14 +181,19 @@ class Executor:
_format_dict_keys(self.config.sort_columns), _format_dict_keys(self.config.sort_columns),
) )
) )
column, default_order = self.config.sort_columns[ entry = self.config.sort_columns[
sort_token.name sort_token.name
] ]
order = _get_order(sort_token.order, default_order) if callable(entry):
if order == sort_token.SORT_ASC: order = _get_order(sort_token.order, sort_token.SORT_DESC)
db_query = db_query.order_by(column.asc()) db_query = entry(db_query, order)
elif order == sort_token.SORT_DESC: else:
db_query = db_query.order_by(column.desc()) column, default_order = entry
order = _get_order(sort_token.order, default_order)
if order == sort_token.SORT_ASC:
db_query = db_query.order_by(column.asc())
elif order == sort_token.SORT_DESC:
db_query = db_query.order_by(column.desc())
db_query = self.config.finalize_query(db_query) db_query = self.config.finalize_query(db_query)
return db_query return db_query

View File

@ -1,4 +1,4 @@
from typing import Any, Callable from typing import Any, Callable, Union
SaColumn = Any SaColumn = Any
SaQuery = Any SaQuery = Any

View File

@ -14,6 +14,7 @@ def inject_config(config_injector):
"privileges": { "privileges": {
"posts:list": model.User.RANK_REGULAR, "posts:list": model.User.RANK_REGULAR,
"posts:view": model.User.RANK_REGULAR, "posts:view": model.User.RANK_REGULAR,
"pools:list": model.User.RANK_REGULAR,
}, },
} }
) )
@ -125,3 +126,25 @@ def test_trying_to_retrieve_single_without_privileges(
context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)), context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)),
{"post_id": 999}, {"post_id": 999},
) )
def test_get_pool_post_around(user_factory, post_factory, pool_factory, pool_post_factory):
p1 = post_factory(id=1)
p2 = post_factory(id=2)
p3 = post_factory(id=3)
db.session.add_all([p1, p2, p3])
pool = pool_factory(id=1)
db.session.add(pool)
pool_posts = [pool_post_factory(pool=pool, post=p1), pool_post_factory(pool=pool, post=p2), pool_post_factory(pool=pool, post=p3)]
db.session.add_all(pool_posts)
result = posts.get_pool_posts_nearby(p1, pool)
assert result["previousPost"] == None and result["nextPost"]["id"] == 2
result = posts.get_pool_posts_nearby(p2, pool)
assert result["previousPost"]["id"] == 1 and result["nextPost"]["id"] == 3
result = posts.get_pool_posts_nearby(p3, pool)
assert result["previousPost"]["id"] == 2 and result["nextPost"] == None

View File

@ -105,7 +105,14 @@ def test_serialize_post(
pool_category_factory, pool_category_factory,
config_injector, config_injector,
): ):
config_injector({"data_url": "http://example.com/", "secret": "test"}) config_injector({
"privileges": {
"pools:list": model.User.RANK_REGULAR,
"pools:view": model.User.RANK_REGULAR,
},
"data_url": "http://example.com/",
"secret": "test"
})
with patch("szurubooru.func.comments.serialize_comment"), patch( with patch("szurubooru.func.comments.serialize_comment"), patch(
"szurubooru.func.users.serialize_micro_user" "szurubooru.func.users.serialize_micro_user"
), patch("szurubooru.func.posts.files.has"): ), patch("szurubooru.func.posts.files.has"):
@ -249,6 +256,10 @@ def test_serialize_post(
"description": "desc", "description": "desc",
"category": "test-cat1", "category": "test-cat1",
"postCount": 1, "postCount": 1,
"firstPost": {"id": 1},
"lastPost": {"id": 1},
"previousPost": None,
"nextPost": None,
}, },
{ {
"id": 2, "id": 2,
@ -256,6 +267,10 @@ def test_serialize_post(
"description": "desc2", "description": "desc2",
"category": "test-cat2", "category": "test-cat2",
"postCount": 1, "postCount": 1,
"firstPost": {"id": 1},
"lastPost": {"id": 1},
"previousPost": None,
"nextPost": None,
}, },
], ],
"user": "post author", "user": "post author",

View File

@ -725,6 +725,7 @@ def test_filter_by_feature_date(
"sort:fav-time", "sort:fav-time",
"sort:feature-date", "sort:feature-date",
"sort:feature-time", "sort:feature-time",
"sort:pool",
], ],
) )
def test_sort_tokens(verify_unpaged, post_factory, input): def test_sort_tokens(verify_unpaged, post_factory, input):
@ -865,6 +866,45 @@ def test_tumbleweed(
verify_unpaged("-special:tumbleweed", [1, 2, 3]) verify_unpaged("-special:tumbleweed", [1, 2, 3])
def test_sort_pool(
post_factory, pool_factory, pool_category_factory, verify_unpaged
):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post4 = post_factory(id=4)
pool1 = pool_factory(
id=1,
names=["pool1"],
description="desc",
category=pool_category_factory("test-cat1"),
)
pool1.posts = [post1, post4, post3]
pool2 = pool_factory(
id=2,
names=["pool2"],
description="desc",
category=pool_category_factory("test-cat2"),
)
pool2.posts = [post3, post4, post2]
db.session.add_all(
[
post1,
post2,
post3,
post4,
pool1,
pool2
]
)
db.session.flush()
verify_unpaged("pool:1 sort:pool", [1, 4, 3])
verify_unpaged("pool:2 sort:pool", [3, 4, 2])
verify_unpaged("pool:1 pool:2 sort:pool", [4, 3])
verify_unpaged("pool:2 pool:1 sort:pool", [3, 4])
verify_unpaged("sort:pool", [1, 2, 3, 4])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"input,expected_post_ids", "input,expected_post_ids",
[ [