1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-08-07 22:45:23 +02:00

Fixed comments,added extra solutions for the first task

This commit is contained in:
timakasucces 2019-03-05 21:36:18 +03:00
commit df10d46198
62 changed files with 2797 additions and 2709 deletions

View file

@ -1,9 +1,10 @@
{
"title": "CodeX Editor   🤩🧦🤨",
"title": "CodeX Docs",
"menu": [
"Guides",
"API",
"Plugins",
{"title": "Support Project", "uri": "/support"}
]
],
"landingFrameSrc": "https://codex.so/editor?frame=1"
}

2
.gitignore vendored
View file

@ -70,3 +70,5 @@ typings/
# Cache of babel and others
.cache/
.eslintcache
.DS_Store
.codexdocsrc

11
nodemon.json Normal file
View file

@ -0,0 +1,11 @@
{
"ignore": [
"node_modules",
"src/frontend",
"public/dist"
],
"events": {
"restart": "echo \"App restarted due to: '$FILENAME'\""
},
"ext": "js,twig"
}

View file

@ -16,17 +16,23 @@
"@babel/polyfill": "^7.0.0",
"bcrypt": "^3.0.3",
"body-parser": "latest",
"codex.editor": "^2.1.3",
"codex.editor": "^2.8.1",
"codex.editor.delimiter": "^1.0.2",
"codex.editor.embed": "^2.1.2",
"codex.editor.header": "^2.0.5",
"commander": "^2.19.0",
"codex.editor.image": "^2.0.3",
"codex.editor.quote": "^2.1.5",
"codex.editor.raw": "^2.0.2",
"cookie-parser": "~1.4.3",
"csurf": "^1.9.0",
"debug": "~4.1.0",
"dotenv": "^6.2.0",
"jsonwebtoken": "^8.4.0",
"eslint-plugin-standard": "^4.0.0",
"express": "~4.16.0",
"http-errors": "~1.7.1",
"jsonwebtoken": "^8.4.0",
"module-dispatcher": "^1.0.2",
"module-dispatcher": "^2.0.0",
"morgan": "~1.9.0",
"multer": "^1.3.1",
"nedb": "^1.8.0",
@ -45,9 +51,9 @@
"babel-loader": "^8.0.2",
"chai": "^4.1.2",
"chai-http": "^4.0.0",
"codex.editor.code": "^2.0.0",
"codex.editor.inline-code": "^1.0.1",
"codex.editor.list": "^1.0.2",
"codex.editor.code": "^2.3.1",
"codex.editor.inline-code": "^1.2.0",
"codex.editor.list": "^1.2.3",
"codex.editor.marker": "^1.0.1",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
@ -55,6 +61,8 @@
"eslint": "^5.3.0",
"eslint-config-codex": "github:codex-team/eslint-config",
"eslint-plugin-chai-friendly": "^0.4.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^8.0.1",
"highlight.js": "^9.13.1",
"husky": "^1.1.2",
"mini-css-extract-plugin": "^0.4.3",

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
.hljs{display:block;background:#fff;padding:.5em;color:#333;overflow-x:auto}.hljs-comment,.hljs-meta{color:#969896}.hljs-emphasis,.hljs-quote,.hljs-string,.hljs-strong,.hljs-template-variable,.hljs-variable{color:#df5000}.hljs-keyword,.hljs-selector-tag,.hljs-type{color:#a71d5d}.hljs-attribute,.hljs-bullet,.hljs-literal,.hljs-symbol{color:#0086b3}.hljs-name,.hljs-section{color:#63a35c}.hljs-tag{color:#333}.hljs-attr,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-title{color:#795da3}.hljs-addition{color:#55a532;background-color:#eaffea}.hljs-deletion{color:#bd2c00;background-color:#ffecec}.hljs-link{text-decoration:underline}
.hljs{display:block;overflow-x:auto;padding:.5em;color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-built_in,.hljs-class .hljs-title{color:#e6c07b}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -17,9 +17,9 @@ app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'twig');
require('./utils/twig');
app.use(logger('dev'));
app.use(logger('dev '));
app.use(express.json());
app.use(express.urlencoded({extended: true}));
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, '../public')));

View file

@ -0,0 +1,26 @@
const Alias = require('../models/alias');
/**
* @class Aliases
* @classdesc Aliases controller
*/
class Aliases {
/**
* @static
* Find and return entity with given alias
*
* @param {string} aliasName - alias name of entity
* @returns {Promise<Alias>}
*/
static async get(aliasName) {
const alias = await Alias.get(aliasName);
if (!alias.id) {
throw new Error('Entity with given alias does not exist');
}
return alias;
}
}
module.exports = Aliases;

View file

@ -1,4 +1,5 @@
const Model = require('../models/page');
const Alias = require('../models/alias');
/**
* @class Pages
@ -85,7 +86,18 @@ class Pages {
const page = new Model(data);
return page.save();
const insertedPage = await page.save();
if (insertedPage.uri) {
const alias = new Alias({
id: insertedPage._id,
type: Alias.types.PAGE
}, insertedPage.uri);
alias.save();
}
return insertedPage;
} catch (validationError) {
throw new Error(validationError);
}
@ -132,14 +144,33 @@ class Pages {
*/
static async update(id, data) {
const page = await Model.get(id);
const previousUri = page.uri;
if (!page._id) {
throw new Error('Page with given id does not exist');
}
page.data = data;
if (data.uri && !data.uri.match(/^[a-z0-9'-]+$/i)) {
throw new Error('Uri has unexpected characters');
}
return page.save();
page.data = data;
const updatedPage = await page.save();
if (updatedPage.uri !== previousUri) {
if (updatedPage.uri) {
const alias = new Alias({
id: updatedPage._id,
type: Alias.types.PAGE
}, updatedPage.uri);
alias.save();
}
Alias.markAsDeprecated(previousUri);
}
return updatedPage;
}
/**
@ -155,6 +186,10 @@ class Pages {
throw new Error('Page with given id does not exist');
}
const alias = await Alias.get(page.uri);
await alias.destroy();
return page.destroy();
}
}

View file

@ -0,0 +1,126 @@
const Model = require('../models/pageOrder');
/**
* @class PagesOrder
* @classdesc PagesOrder controller
*
* Manipulates with Pages: changes the order, deletes, updates and so on...
*/
class PagesOrder {
/**
* Returns Page's order
*
* @param {string} parentId - of which page we want to get children order
* @returns {Promise<PageOrder>}
*/
static async get(parentId) {
const order = await Model.get(parentId);
if (!order._id) {
throw new Error('Page with given id does not contain order');
}
return order;
}
/**
* Returns all records about page's order
*
* @returns {Promise<PagesOrder[]>}
*/
static async getAll() {
return Model.getAll();
}
/**
* Pushes the child page to the parent's order list
*
* @param {string} parentId - parent page's id
* @param {string} childId - new page pushed to the order
*/
static async push(parentId, childId) {
const order = await Model.get(parentId);
order.push(childId);
await order.save();
}
/**
* Move one page to another Page's order
*
* @param {string} oldParentId - old parent page's id
* @param {string} newParentId - new parent page's id
* @param {string} targetPageId - page's id which is changing the parent page
*/
static async move(oldParentId, newParentId, targetPageId) {
const oldParentOrder = await Model.get(oldParentId);
oldParentOrder.remove(targetPageId);
await oldParentOrder.save();
const newParentOrder = await Model.get(newParentId);
newParentOrder.push(targetPageId);
await newParentOrder.save();
}
/**
* Returns new array with ordered pages
*
* @param {Page[]} pages - list of all available pages
* @param {string} currentPageId - page's id around which we are ordering
* @param {string} parentPageId - parent page's id that contains page above
* @param {Boolean} ignoreSelf - should we ignore current page in list or not
* @return {Page[]}
*/
static async getOrderedChildren(pages, currentPageId, parentPageId, ignoreSelf = false) {
const children = await Model.get(parentPageId);
const unordered = pages.filter(page => page._parent === parentPageId).map(page => page._id);
// Create unique array with ordered and unordered pages id
const ordered = [ ...new Set([...children.order, ...unordered]) ];
const result = [];
ordered.forEach(pageId => {
pages.forEach(page => {
if (page._id === pageId && (pageId !== currentPageId || !ignoreSelf)) {
result.push(page);
}
});
});
return result;
}
/**
* @param {string[]} unordered
* @param {string} currentPageId - page's id that changes the order
* @param {string} parentPageId - parent page's id that contains both two pages
* @param {string} putAbovePageId - page's id above which we put the target page
*/
static async update(unordered, currentPageId, parentPageId, putAbovePageId) {
const pageOrder = await Model.get(parentPageId);
// Create unique array with ordered and unordered pages id
pageOrder.order = [ ...new Set([...pageOrder.order, ...unordered]) ];
pageOrder.putAbove(currentPageId, putAbovePageId);
await pageOrder.save();
}
/**
* @param parentId
* @returns {Promise<void>}
*/
static async remove(parentId) {
const order = await Model.get(parentId);
if (!order._id) {
throw new Error('Page with given id does not contain order');
}
return order.destroy();
}
}
module.exports = PagesOrder;

View file

@ -23,14 +23,14 @@ class Docs {
* @constructor
*/
constructor() {
console.log('CodeX Docs initialized');
this.writing = new Writing();
this.page = new Page();
document.addEventListener('DOMContentLoaded', (event) => {
this.docReady();
});
console.log('CodeX Docs initialized');
}
/**

View file

@ -1,6 +1,9 @@
import hljs from 'highlight.js/lib/highlight';
import javascript from 'highlight.js/lib/languages/javascript';
import style from 'highlight.js/styles/github-gist.css';
import xml from 'highlight.js/lib/languages/xml';
import json from 'highlight.js/lib/languages/json';
// eslint-disable-next-line
import style from 'highlight.js/styles/atom-one-dark.css';
/**
* @class CodeStyles
@ -11,11 +14,13 @@ export default class CodeStyler {
* @param {string} selector - CSS selector for code blocks
* @param {string[]} languages - list of languages to highlight, see hljs.listLanguages()
*/
constructor({selector, languages = [ 'javascript' ]}) {
constructor({ selector, languages = ['javascript', 'xml', 'json'] }) {
this.codeBlocksSelector = selector;
this.languages = languages;
this.langsAvailable = {
javascript
javascript,
xml,
json
};
this.init();

View file

@ -1,9 +1,17 @@
import CodeXEditor from 'codex.editor';
/**
* Tools for the Editor
*/
import Header from 'codex.editor.header';
import CodeTool from 'codex.editor.code';
import InlineCode from 'codex.editor.inline-code';
import Quote from 'codex.editor.quote';
import Marker from 'codex.editor.marker';
import ListTool from 'codex.editor.list';
import CodeTool from 'codex.editor.code';
import Delimiter from 'codex.editor.delimiter';
import InlineCode from 'codex.editor.inline-code';
import List from 'codex.editor.list';
import RawTool from 'codex.editor.raw';
import Embed from 'codex.editor.embed';
/**
* Class for working with Editor.js
@ -11,32 +19,59 @@ import ListTool from 'codex.editor.list';
export default class Editor {
/**
* Creates Editor instance
* @property {object} initialData - data to start with
* @param {object} editorConfig - configuration object for Editor.js
* @param {object} data.blocks - data to start with
* @param {object} options
* @param {string} options.headerPlaceholder - placeholder for Header tool
*/
constructor({initialData}) {
this.editor = new CodeXEditor({
constructor(editorConfig = {}, options = {}) {
const defaultConfig = {
tools: {
header: {
class: Header,
inlineToolbar: ['link', 'marker'],
config: {
placeholder: 'Enter a title'
placeholder: options.headerPlaceholder || ''
}
},
code: CodeTool,
// image: {
// class: ImageTool,
// inlineToolbar: true,
// config: {
// endpoints: {
// byFile: '/editor/transport',
// byUrl: '/editor/transport'
// }
// }
// },
list: {
class: List,
inlineToolbar: true
},
quote: {
class: Quote,
inlineToolbar: true
},
code: {
class: CodeTool,
shortcut: 'CMD+SHIFT+D'
},
rawTool: {
class: RawTool,
shortcut: 'CMD+SHIFT+R'
},
delimiter: Delimiter,
embed: Embed,
inlineCode: {
class: InlineCode,
shortcut: 'CMD+SHIFT+I'
shortcut: 'CMD+SHIFT+C'
},
Marker: {
marker: {
class: Marker,
shortcut: 'CMD+SHIFT+M'
},
list: {
class: ListTool,
inlineToolbar: true
}
},
data: initialData || {
data: {
blocks: [
{
type: 'header',
@ -47,7 +82,9 @@ export default class Editor {
}
]
}
});
};
this.editor = new CodeXEditor(Object.assign(defaultConfig, editorConfig));
}
/**

View file

@ -27,7 +27,7 @@ export default class Writing {
* Init code highlighting
*/
async createCodeStyling() {
const {default: CodeStyler} = await import(/* webpackChunkName: "code-styling" */ './../classes/codeStyler');
const { default: CodeStyler } = await import(/* webpackChunkName: "code-styling" */ './../classes/codeStyler');
return new CodeStyler({
selector: '.block-code'

View file

@ -27,7 +27,10 @@ export default class Writing {
this.nodes = {
editorWrapper: null,
saveButton: null,
parentIdSelector: null
removeButton: null,
parentIdSelector: null,
putAboveIdSelector: null,
uriInput: null
};
}
@ -40,11 +43,7 @@ export default class Writing {
/**
* Create Editor
*/
this.nodes.editorWrapper = document.createElement('div');
this.nodes.editorWrapper.id = 'codex-editor';
moduleEl.appendChild(this.nodes.editorWrapper);
this.nodes.editorWrapper = document.getElementById('codex-editor');
if (settings.page) {
this.page = settings.page;
}
@ -56,11 +55,28 @@ export default class Writing {
/**
* Activate form elements
*/
this.nodes.saveButton = moduleEl.querySelector('[name="js-submit"]');
this.nodes.saveButton = moduleEl.querySelector('[name="js-submit-save"]');
this.nodes.saveButton.addEventListener('click', () => {
this.saveButtonClicked();
});
this.nodes.removeButton = moduleEl.querySelector('[name="js-submit-remove"]');
if (this.nodes.removeButton) {
this.nodes.removeButton.addEventListener('click', () => {
const isUserAgree = window.confirm('Are you sure?');
if (!isUserAgree) {
return;
}
this.removeButtonClicked();
});
}
this.nodes.parentIdSelector = moduleEl.querySelector('[name="parent"]');
this.nodes.putAboveIdSelector = moduleEl.querySelector('[name="above"]');
this.nodes.uriInput = moduleEl.querySelector('[name="uri-input"]');
};
/**
@ -68,10 +84,14 @@ export default class Writing {
* @return {Promise<Editor>}
*/
async loadEditor() {
const {default: Editor} = await import(/* webpackChunkName: "editor" */ './../classes/editor');
const { default: Editor } = await import(/* webpackChunkName: "editor" */ './../classes/editor');
return new Editor({
initialData: this.page ? this.page.body : null
const editorConfig = this.page ? {
data: this.page.body
} : {};
return new Editor(editorConfig, {
headerPlaceholder: 'Enter a title'
});
}
@ -84,13 +104,31 @@ export default class Writing {
const editorData = await this.editor.save();
const firstBlock = editorData.blocks.length ? editorData.blocks[0] : null;
const title = firstBlock && firstBlock.type === 'header' ? firstBlock.data.text : null;
let uri = '';
if (this.nodes.uriInput && this.nodes.uriInput.value) {
if (this.nodes.uriInput.value.match(/^[a-z0-9'-]+$/i)) {
uri = this.nodes.uriInput.value;
} else {
throw new Error('Uri has unexpected characters');
}
}
if (!title) {
throw new Error('Entry should start with Header');
}
/** get ordering selector value */
let putAbovePageId = null;
if (this.nodes.putAboveIdSelector) {
putAbovePageId = this.nodes.putAboveIdSelector.value;
}
return {
parent: this.nodes.parentIdSelector.value,
putAbovePageId: putAbovePageId,
uri: uri,
body: editorData
};
}
@ -115,7 +153,7 @@ export default class Writing {
response = await response.json();
if (response.success) {
document.location = '/page/' + response.result._id;
window.location.pathname = response.result.uri ? response.result.uri : '/page/' + response.result._id;
} else {
alert(response.error);
console.log('Validation failed:', response.error);
@ -128,4 +166,31 @@ export default class Writing {
console.log('Saving error: ', savingError);
}
}
/**
* @returns {Promise<void>}
*/
async removeButtonClicked() {
try {
const endpoint = this.page ? '/api/page/' + this.page._id : '';
let response = await fetch(endpoint, {
method: 'DELETE'
});
response = await response.json();
if (response.success) {
if (response.result && response.result._id) {
document.location = '/page/' + response.result._id;
} else {
document.location = '/';
}
} else {
alert(response.error);
console.log('Server fetch failed:', response.error);
}
} catch (e) {
console.log('Server fetch failed due to the:', e);
}
}
}

View file

@ -2,6 +2,16 @@
font-size: 15px;
color: var(--color-text-second);
@media (--mobile) {
font-size: 13px;
display: none;
margin-top: 20px;
}
&--toggled {
display: block !important;
}
a {
text-decoration: none;
}
@ -9,9 +19,21 @@
&__section {
margin-bottom: 30px;
@media (--mobile) {
margin-bottom: 20px;
}
&:last-of-type {
margin-bottom: 0;
}
&-title {
margin-bottom: 15px;
color: var(--color-link-active);
@media (--mobile) {
margin-bottom: 10px;
}
}
&-list {
@ -26,3 +48,18 @@
}
}
}
.docs-aside-toggler {
display: none;
font-size: 13px;
cursor: pointer;
color: var(--color-text-second);
@media (--mobile) {
display: block;
}
svg {
margin-right: 10px;
}
}

View file

@ -1,13 +1,20 @@
.docs-header {
font-size: 15.8px;
border-bottom: 1px solid var(--color-line-gray);
line-height: 50px;
display: flex;
flex-shrink: 0;
padding: 0 var(--layout-padding-horisontal);
border-bottom: 1px solid var(--color-line-gray);
font-size: 15.8px;
line-height: 50px;
flex-wrap: wrap;
position: relative;
@media (--mobile){
line-height: 40px;
}
a {
text-decoration: none;
display: inline-block;
text-decoration: none;
}
&__logo {
@ -18,10 +25,40 @@
&__menu {
display: flex;
margin: 0 0 0 auto;
padding-left: 0;
@media (--mobile) {
flex-basis: 100%;
font-size: 12px;
}
li {
display: inline-flex;
align-items: center;
list-style: none;
margin-left: 20px;
@media (--mobile) {
margin-left: 0;
margin-right: 15px;
}
}
&-add {
@media (--mobile) {
position: absolute;
right: 15px;
top: 15px;
line-height: 1em;
margin: 0 !important;
}
a {
@media (--mobile) {
font-size: 0;
padding: 8px;
margin-right: 0;
}
}
}
a:not(.docs-header__button) {
@ -35,6 +72,7 @@
&__button {
@apply --button;
@apply --button-primary;
margin: auto 30px auto auto;
}
}

View file

@ -0,0 +1,30 @@
/**
* Index page landing iframe
*/
.landing-body {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.landing-loader {
position: absolute;
left: 50%;
top: 50%;
z-index: -1;
transform: translate(-50%, -50%);
& > svg {
width: 80px;
height: 80px;
}
}
.landing-frame {
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 500ms ease;
will-change: opacity;
}

View file

@ -6,10 +6,18 @@
display: flex;
color: var(--color-text-second);
@media (--mobile) {
font-size: 13px;
}
&-nav {
color: inherit;
text-decoration: none;
@media (--mobile) {
display: none;
}
&:hover {
color: var(--color-link-active);
}
@ -24,10 +32,15 @@
&-time {
margin-left: auto;
@media (--mobile) {
margin-left: 0;
}
}
&-button {
@apply --button;
@apply --button-primary;
padding: 5px 10px;
font-size: 13px;
margin-left: 10px;
@ -40,6 +53,26 @@
letter-spacing: -0.04px;
margin-bottom: -0.2em;
}
.cdx-marker {
background: rgba(245,235,111,0.33);
padding: 3px 0;
}
&__content {
a {
text-decoration: none;
border-bottom: 1px solid #000;
padding-bottom: 1px;
color: inherit;
&:hover {
color: var(--color-link-active);
border-bottom-color: var(--color-link-active);
}
}
}
}
/**
@ -66,9 +99,9 @@
*/
.block-code {
padding: 20px !important;
border-radius: 5px;
font-size: 13px;
border-radius: 3px;
border: 1px solid var(--color-line-gray);
//border: 1px solid var(--color-line-gray);
font-family: Menlo,Monaco,Consolas,Courier New,monospace;
line-height: 1.7em;
}

View file

@ -1,20 +1,57 @@
.writing-header {
display: flex;
padding: 15px 0;
margin-top: calc(-1 * var(--layout-padding-vertical));
position: sticky;
top: 0;
padding: 15px 0;
margin-top: calc(-1 * var(--layout-padding-vertical));
background: #fff;
z-index: 2;
box-shadow: 0 3px 10px #fff;
z-index: 2;
font-size: 14px;
&__save {
@apply --button;
margin-left: auto;
@apply --button-primary;
margin: auto;
}
&__left {
margin: auto 0;
color: var(--color-text-second);
& span {
margin-right: 10px;
}
}
select {
max-width: 100px
}
}
.writing-buttons {
&__remove {
@apply --button;
@apply --button-danger;
}
}
.uri-input {
box-sizing: border-box;
width: 100%;
padding: 10px 12px;
border-radius: 3px;
border: 1px solid rgba(201, 201, 204, 0.48);
box-shadow: inset 0 1px 2px 0 rgba(35, 44, 72, 0.06);
outline: none;
}
.writing-editor {
@media (--desktop) {
margin: 0 -100px;
}
}
.codex-editor__redactor .ce-block:first-of-type .ce-header {
font-size: 32px;
}

View file

@ -2,14 +2,31 @@
display: flex;
padding: 0 var(--layout-padding-horisontal);
@media (--mobile) {
flex-wrap: wrap;
}
&__aside {
width: var(--layout-width-aside);
@media (--mobile) {
width: 100%;
flex-basis: 100%;
padding: 20px var(--layout-padding-horisontal) !important;
margin: 0 calc(-1 * var(--layout-padding-horisontal));
border-bottom: 1px solid var(--color-line-gray);
}
}
&__content {
flex-grow: 2;
word-wrap: break-word;
@media (--mobile) {
width: 100%;
flex-basis: 100%;
}
&-inner {
max-width: var(--layout-width-main-col);
margin: 0 auto;
@ -19,5 +36,9 @@
&__aside,
&__content {
padding: var(--layout-padding-vertical) 0;
@media (--mobile) {
padding: 20px 0;
}
}
}

View file

@ -5,6 +5,7 @@
@import url('components/aside.pcss');
@import url('components/writing.pcss');
@import url('components/page.pcss');
@import url('components/landing.pcss');
body {
font-family: system-ui, Helvetica, Arial, Verdana;

View file

@ -3,6 +3,8 @@
--color-text-second: #7B7E89;
--color-line-gray: #E8E8EB;
--color-link-active: #388AE5;
--color-button-danger: #ff1629;
--color-gray-border: rgba(var(--color-line-gray), 0.48);
/**
* Site layout sizes
@ -12,22 +14,54 @@
--layout-width-aside: 200px;
--layout-width-main-col: 650px;
@media (--mobile) {
--layout-padding-horisontal: 15px;
--layout-padding-vertical: 15px;
}
--button {
display: inline-block;
background: var(--color-link-active);
color: #fff;
border-radius: 3px;
padding: 9px 15px;
border-radius: 3px;
color: #6c6375;
background: #fcfcff;
box-shadow: inset 0 0 0 1px rgba(184, 189, 206, 0.2);
font-size: 14px;
line-height: 1em;
text-decoration: none;
cursor: pointer;
white-space: nowrap;
svg {
margin: 0 0.3em 0 -0.05em;
}
}
--button-danger {
background: var(--color-button-danger);
color: #fff;
box-shadow: none;
&:hover {
background: color-mod(var(--color-button-danger) blackness(+10%));
}
}
--button-primary {
background: var(--color-link-active);
color: #fff;
box-shadow: none;
&:hover {
background: color-mod(var(--color-link-active) blackness(+10%));
}
}
}
/**
* Custom media queries
*/
@custom-media --desktop all and (min-width: 1050px);
@custom-media --tablet all and (max-width: 1050px);
@custom-media --mobile all and (max-width: 980px);
@custom-media --retina all and (-webkit-min-device-pixel-ratio: 1.5);

View file

@ -0,0 +1,60 @@
<svg class="ldi-zbvw97" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 80 80" preserveAspectRatio="xMidYMid">
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" style="transform-origin:50px 50px 0">
<g style="transform-origin:50px 50px 0">
<g style="transform-origin:50px 50px 0;transform:scale(.6)">
<g style="transform-origin:50px 50px 0">
<style>
.st3{fill:#dff0fc}
.st5{fill:#69cdff}
</style>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.346153846153846s;animation-direction:normal">
<path class="st3" d="M69.4 28.7c-3-2.8-6.5-4.8-10.3-6-4.6-1.5-7.7-5.8-7.7-10.6V7.5c4.9.2 9.7 1.1 14.2 2.9 7.4 2.9 9.5 12.6 3.8 18.3z" fill="#dff0fc"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.2923076923076924s;animation-direction:normal">
<path class="st5" d="M33 11c-4.4 1.9-8.5 4.6-12.1 8l3.2 3.2c3.4 3.4 8.6 4.2 12.9 2.1 3.6-1.8 7.5-2.8 11.6-3 0-8.1-8.2-13.5-15.6-10.3z" fill="#69cdff"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.2384615384615383s;animation-direction:normal">
<path class="st3" d="M10.4 34.4c-1.8 4.5-2.7 9.3-2.9 14.2H12c4.8 0 9.1-3.1 10.6-7.7 1.3-3.8 3.3-7.3 6.1-10.3-5.7-5.7-15.4-3.6-18.3 3.8z" fill="#dff0fc"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.1846153846153846s;animation-direction:normal">
<path class="st5" d="M11 67c1.9 4.4 4.6 8.5 8 12.1l3.2-3.2c3.4-3.4 4.2-8.6 2.1-12.9-1.8-3.6-2.8-7.5-3-11.6-8.1 0-13.5 8.2-10.3 15.6z" fill="#69cdff"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.1307692307692307s;animation-direction:normal">
<path class="st5" d="M89 33c-1.9-4.4-4.6-8.5-8-12.1l-3.2 3.2c-3.4 3.4-4.2 8.6-2.1 12.9 1.8 3.6 2.8 7.5 3 11.6 8.1 0 13.5-8.2 10.3-15.6z" fill="#69cdff"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.0769230769230769s;animation-direction:normal">
<path class="st3" d="M89.6 65.6c1.8-4.5 2.7-9.3 2.9-14.2H88c-4.8 0-9.1 3.1-10.6 7.7-1.3 3.8-3.3 7.3-6.1 10.3 5.7 5.7 15.4 3.6 18.3-3.8z" fill="#dff0fc"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.023076923076923s;animation-direction:normal">
<path class="st5" d="M67 89c4.4-1.9 8.5-4.6 12.1-8l-3.2-3.2c-3.4-3.4-8.6-4.2-12.9-2.1-3.6 1.8-7.5 2.8-11.6 3 0 8.1 8.2 13.5 15.6 10.3z" fill="#69cdff"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-.9692307692307691s;animation-direction:normal">
<path class="st3" d="M34.4 89.6c4.5 1.8 9.3 2.7 14.2 2.9V88c0-4.8-3.1-9.1-7.7-10.6-3.8-1.3-7.3-3.3-10.3-6.1-5.7 5.7-3.6 15.4 3.8 18.3z" fill="#dff0fc"/>
</g>
</g>
</g>
</g>
</g>
<style>
.ld-spin {
will-change: transform;
}
@keyframes ld-spin{0%{-webkit-transform:rotate(0);transform:rotate(0);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}50%{-webkit-transform:rotate(180deg);transform:rotate(180deg);animation-timing-function:cubic-bezier(.215,.61,.355,1)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes ld-spin{0%{-webkit-transform:rotate(0);transform:rotate(0);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}50%{-webkit-transform:rotate(180deg);transform:rotate(180deg);animation-timing-function:cubic-bezier(.215,.61,.355,1)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}path{stroke-width:0}.ld.ld-spin{-webkit-animation:ld-spin 1s infinite;animation:ld-spin 1s infinite}
</style>
</svg>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -0,0 +1 @@
<svg width="13" height="10" viewBox="0 0 13 10" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h13v2H0V0zm0 4h13v2H0V4zm0 4h13v2H0V8z" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 164 B

142
src/models/alias.js Normal file
View file

@ -0,0 +1,142 @@
const { aliases: aliasesDb } = require('../utils/database/index');
const binaryMD5 = require('../utils/crypto');
/**
* @typedef {Object} AliasData
* @property {string} _id - alias id
* @property {string} hash - alias binary hash
* @property {string} type - entity type
* @property {boolean} deprecated - indicate if alias deprecated
* @property {string} id - entity id
*
*/
/**
* @class Alias
* @classdesc Alias model
*
* @property {string} _id - alias id
* @property {string} hash - alias binary hash
* @property {string} type - entity type
* @property {boolean} deprecated - indicate if alias deprecated
* @property {string} id - entity title
*/
class Alias {
/**
* Return Alias types
*
* @returns {Object}
*/
static get types() {
return {
PAGE: 'page'
};
};
/**
* Find and return alias with given alias
* @param {string} aliasName - alias of entity
* @returns {Promise<Alias>}
*/
static async get(aliasName) {
const hash = binaryMD5(aliasName);
let data = await aliasesDb.findOne({ hash: hash, deprecated: false });
if (!data) {
data = await aliasesDb.findOne({ hash: hash });
}
return new Alias(data);
}
/**
* @constructor
*
* @param {AliasData} data
* @param {string} aliasName - alias of entity
*/
constructor(data = {}, aliasName = '') {
if (data === null) {
data = {};
}
if (data._id) {
this._id = data._id;
}
if (aliasName) {
this.hash = binaryMD5(aliasName);
}
this.data = data;
}
/**
* Save or update alias data in the database
*
* @returns {Promise<Alias>}
*/
async save() {
if (!this._id) {
const insertedRow = await aliasesDb.insert(this.data);
this._id = insertedRow._id;
} else {
await aliasesDb.update({ _id: this._id }, this.data);
}
return this;
}
/**
* Set AliasData object fields to internal model fields
*
* @param {AliasData} aliasData
*/
set data(aliasData) {
const { id, type, hash, deprecated } = aliasData;
this.id = id || this.id;
this.type = type || this.type;
this.hash = hash || this.hash;
this.deprecated = deprecated || false;
}
/**
* Return AliasData object
*
* @returns {AliasData}
*/
get data() {
return {
_id: this._id,
id: this.id,
type: this.type,
hash: this.hash,
deprecated: this.deprecated
};
}
/**
* Mark alias as deprecated
* @param {string} aliasName - alias of entity
* @returns {Promise<Alias>}
*/
static async markAsDeprecated(aliasName) {
const alias = await Alias.get(aliasName);
alias.deprecated = true;
return alias.save();
}
/**
* @returns {Promise<Alias>}
*/
async destroy() {
await aliasesDb.remove({ _id: this._id });
delete this._id;
return this;
}
}
module.exports = Alias;

View file

@ -1,12 +1,13 @@
const {pages: db} = require('../utils/database/index');
const { pages: pagesDb } = require('../utils/database/index');
const translateString = require('../utils/translation');
/**
* @typedef {Object} PageData
* @property {string} _id - page id
* @property {string} title - page title
* @property {string} uri - page uri
* @property {*} body - page body
* @property {string} parent - id of parent page
*
*/
/**
@ -15,6 +16,7 @@ const {pages: db} = require('../utils/database/index');
*
* @property {string} _id - page id
* @property {string} title - page title
* @property {string} uri - page uri
* @property {*} body - page body
* @property {string} _parent - id of parent page
*/
@ -25,7 +27,18 @@ class Page {
* @returns {Promise<Page>}
*/
static async get(_id) {
const data = await db.findOne({_id});
const data = await pagesDb.findOne({ _id });
return new Page(data);
}
/**
* Find and return model of page with given uri
* @param {string} uri - page uri
* @returns {Promise<Page>}
*/
static async getByUri(uri) {
const data = await pagesDb.findOne({ uri });
return new Page(data);
}
@ -37,7 +50,7 @@ class Page {
* @returns {Promise<Page[]>}
*/
static async getAll(query = {}) {
const docs = await db.find(query);
const docs = await pagesDb.find(query);
return Promise.all(docs.map(doc => new Page(doc)));
}
@ -65,11 +78,12 @@ class Page {
* @param {PageData} pageData
*/
set data(pageData) {
const {body, parent} = pageData;
const { body, parent, uri } = pageData;
this.body = body || this.body;
this.title = this.extractTitleFromBody();
this._parent = parent || this._parent;
this.uri = uri || '';
this._parent = parent || this._parent || '0';
}
/**
@ -81,6 +95,7 @@ class Page {
return {
_id: this._id,
title: this.title,
uri: this.uri,
body: this.body,
parent: this._parent
};
@ -96,6 +111,21 @@ class Page {
return headerBlock ? headerBlock.data.text : '';
}
/**
* Transform title for uri
* @return {string}
*/
transformTitleToUri() {
return translateString(this.title
.replace(/&nbsp;/g, ' ')
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
.replace(/ +/g, ' ')
.trim()
.toLowerCase()
.split(' ')
.join('-'));
}
/**
* Link given page as parent
*
@ -111,7 +141,7 @@ class Page {
* @returns {Promise<Page>}
*/
get parent() {
return db.findOne({_id: this._parent})
return pagesDb.findOne({ _id: this._parent })
.then(data => new Page(data));
}
@ -121,7 +151,7 @@ class Page {
* @returns {Promise<Page[]>}
*/
get children() {
return db.find({parent: this._id})
return pagesDb.find({ parent: this._id })
.then(data => data.map(page => new Page(page)));
}
@ -131,12 +161,14 @@ class Page {
* @returns {Promise<Page>}
*/
async save() {
this.uri = await this.composeUri(this.uri);
if (!this._id) {
const insertedRow = await db.insert(this.data);
const insertedRow = await pagesDb.insert(this.data);
this._id = insertedRow._id;
} else {
await db.update({_id: this._id}, this.data);
await pagesDb.update({ _id: this._id }, this.data);
}
return this;
@ -148,13 +180,37 @@ class Page {
* @returns {Promise<Page>}
*/
async destroy() {
await db.remove({_id: this._id});
await pagesDb.remove({ _id: this._id });
delete this._id;
return this;
}
/**
* Find and return available uri
*
* @returns {Promise<string>}
*/
async composeUri(uri) {
let pageWithSameUriCount = 0;
if (!this._id) {
uri = this.transformTitleToUri();
}
if (uri) {
let pageWithSameUri = await Page.getByUri(uri);
while (pageWithSameUri._id && pageWithSameUri._id !== this._id) {
pageWithSameUriCount++;
pageWithSameUri = await Page.getByUri(uri + `-${pageWithSameUriCount}`);
}
}
return pageWithSameUriCount ? uri + `-${pageWithSameUriCount}` : uri;
}
/**
* Return readable page data
*

212
src/models/pageOrder.js Normal file
View file

@ -0,0 +1,212 @@
const { pagesOrder: db } = require('../utils/database/index');
/**
* @typedef {Object} PageOrderData
* @property {string} _id - row unique id
* @property {string} page - page id
* @property {Array<string>} order - list of ordered pages
*/
/**
* @class PageOrder
* @classdesc PageOrder
*
* Creates order for Pages with children
*/
class PageOrder {
/**
* Returns current Page's children order
*
* @param {string} pageId - page's id
* @returns {PageOrder}
*/
static async get(pageId) {
const order = await db.findOne({ page: pageId });
let data = {};
if (!order) {
data.page = pageId;
} else {
data = order;
}
return new PageOrder(data);
}
/**
* Find all pages which match passed query object
*
* @param {Object} query
* @returns {Promise<Page[]>}
*/
static async getAll(query = {}) {
const docs = await db.find(query);
return Promise.all(docs.map(doc => new PageOrder(doc)));
}
/**
* @constructor
*
* @param {PageOrderData} data
*/
constructor(data = {}) {
if (data === null) {
data = {};
}
if (data._id) {
this._id = data._id;
}
this.data = data;
}
/**
* constructor data setter
* @param {PageOrderData} pageOrderData
*/
set data(pageOrderData) {
this._page = pageOrderData.page || 0;
this._order = pageOrderData.order || [];
}
/**
* Return Page Children order
* @returns {PageOrderData}
*/
get data() {
return {
_id: this._id,
page: '' + this._page,
order: this._order
};
}
/**
* Pushes page id to the orders array
*
* @param {string} pageId - page's id
*/
push(pageId) {
if (typeof pageId === 'string') {
this._order.push(pageId);
} else {
throw new Error('given id is not string');
}
}
/**
* Removes page id from orders array
*
* @param {string} pageId - page's id
*/
remove(pageId) {
const found = this._order.indexOf(pageId);
if (found >= 0) {
this._order.splice(found, 1);
}
}
/**
* @param {string} currentPageId - page's id that changes the order
* @param {string} putAbovePageId - page's id above which we put the target page
*
* @returns void
*/
putAbove(currentPageId, putAbovePageId) {
const found1 = this.order.indexOf(putAbovePageId);
const found2 = this.order.indexOf(currentPageId);
if (found1 === -1 || found2 === -1) {
return;
}
const margin = found1 < found2 ? 1 : 0;
this.order.splice(found1, 0, currentPageId);
this.order.splice(found2 + margin, 1);
}
/**
* Returns page before passed page with id
*
* @param {string} pageId
*/
getPageBefore(pageId) {
const currentPageInOrder = this.order.indexOf(pageId);
/**
* If page not found or first return nothing
*/
if (currentPageInOrder <= 0) {
return;
}
return this.order[currentPageInOrder - 1];
}
/**
* Returns page before passed page with id
*
* @param pageId
*/
getPageAfter(pageId) {
const currentPageInOrder = this.order.indexOf(pageId);
/**
* If page not found or is last
*/
if (currentPageInOrder === -1 || currentPageInOrder === this.order.length - 1) {
return;
}
return this.order[currentPageInOrder + 1];
}
/**
* @param {string[]} order - define new order
*/
set order(order) {
this._order = order;
}
/**
* Returns ordered list
*
* @return {string[]}
*/
get order() {
return this._order;
}
/**
* Save or update page data in the database
*/
async save() {
if (!this._id) {
const insertedRow = await db.insert(this.data);
this._id = insertedRow._id;
} else {
await db.update({ _id: this._id }, this.data);
}
return this;
}
/**
* Remove page data from the database
*/
async destroy() {
await db.remove({ _id: this._id });
delete this._id;
return this;
}
}
module.exports = PageOrder;

View file

@ -1,4 +1,4 @@
const {password: db} = require('../utils/database/index');
const { password: db } = require('../utils/database/index');
/**
* @class User

35
src/routes/aliases.js Normal file
View file

@ -0,0 +1,35 @@
const express = require('express');
const router = express.Router();
const Aliases = require('../controllers/aliases');
const Pages = require('../controllers/pages');
const Alias = require('../models/alias');
/**
* GET /*
*
* Return document with given alias
*/
router.get('*', async (req, res) => {
try {
const alias = await Aliases.get(req.originalUrl.slice(1)); // Cuts first '/' character
switch (alias.type) {
case Alias.types.PAGE: {
let page = await Pages.get(alias.id);
let pageParent = await page.parent;
res.render('pages/page', {
page, pageParent
});
}
}
} catch (err) {
res.status(400).json({
success: false,
error: err.message
});
}
});
module.exports = router;

View file

@ -2,12 +2,13 @@ const express = require('express');
const router = express.Router();
const multer = require('multer')();
const Pages = require('../../controllers/pages');
const PagesOrder = require('../../controllers/pagesOrder');
/**
* GET /page/:id
*
* Return PageData of page with given id
*/
router.get('/page/:id', async (req, res) => {
try {
const page = await Pages.get(req.params.id);
@ -52,8 +53,11 @@ router.get('/pages', async (req, res) => {
*/
router.put('/page', multer.any(), async (req, res) => {
try {
const {title, body, parent} = req.body;
const page = await Pages.insert({title, body, parent});
const { title, body, parent } = req.body;
const page = await Pages.insert({ title, body, parent });
/** push to the orders array */
await PagesOrder.push(parent, page._id);
res.json({
success: true,
@ -73,12 +77,24 @@ router.put('/page', multer.any(), async (req, res) => {
* Update page data in the database
*/
router.post('/page/:id', multer.any(), async (req, res) => {
const {id} = req.params;
const { id } = req.params;
try {
const {title, body, parent} = req.body;
const page = await Pages.update(id, {title, body, parent});
const { title, body, parent, putAbovePageId, uri } = req.body;
const pages = await Pages.getAll();
let page = await Pages.get(id);
if (page._parent !== parent) {
await PagesOrder.move(page._parent, parent, id);
} else {
if (putAbovePageId && putAbovePageId !== '0') {
const unordered = pages.filter(_page => _page._parent === page._parent).map(_page => _page._id);
await PagesOrder.update(unordered, page._id, page._parent, putAbovePageId);
}
}
page = await Pages.update(id, { title, body, parent, uri });
res.json({
success: true,
result: page
@ -98,11 +114,56 @@ router.post('/page/:id', multer.any(), async (req, res) => {
*/
router.delete('/page/:id', async (req, res) => {
try {
const page = await Pages.remove(req.params.id);
const pageId = req.params.id;
const page = await Pages.get(pageId);
const parentPageOrder = await PagesOrder.get(page._parent);
const pageBeforeId = parentPageOrder.getPageBefore(page._id);
const pageAfterId = parentPageOrder.getPageAfter(page._id);
let pageToRedirect;
if (pageBeforeId) {
pageToRedirect = await Pages.get(pageBeforeId);
} else if (pageAfterId) {
pageToRedirect = await Pages.get(pageAfterId);
} else {
pageToRedirect = page._parent !== '0' ? await Pages.get(page._parent) : null;
}
/**
* remove current page and go deeper to remove children with orders
*
* @param startFrom
* @returns {Promise<void>}
*/
const deleteRecursively = async function (startFrom) {
let order = [];
try {
const children = await PagesOrder.get(startFrom);
order = children.order;
} catch (e) {}
order.forEach(async id => {
await deleteRecursively(id);
});
await Pages.remove(startFrom);
try {
await PagesOrder.remove(startFrom);
} catch (e) {}
};
await deleteRecursively(req.params.id);
// remove also from parent's order
parentPageOrder.remove(req.params.id);
await parentPageOrder.save();
res.json({
success: true,
result: page
result: pageToRedirect
});
} catch (err) {
res.status(400).json({

View file

@ -4,7 +4,7 @@ const router = express.Router();
/* GET home page. */
router.get('/', verifyToken, async (req, res) => {
res.render('index', { title: 'Express', isAuthorized: res.locals.isAuthorized });
res.render('pages/index', { isAuthorized: res.locals.isAuthorized });
});
module.exports = router;

View file

@ -4,6 +4,7 @@ const router = express.Router();
const home = require('./home');
const pages = require('./pages');
const auth = require('./auth');
const aliases = require('./aliases');
const api = require('./api');
const pagesMiddleware = require('./middlewares/pages');
@ -12,5 +13,6 @@ router.use('/', pagesMiddleware, home);
router.use('/', pagesMiddleware, pages);
router.use('/', pagesMiddleware, auth);
router.use('/api', api);
router.use('/', aliases);
module.exports = router;

View file

@ -1,15 +1,50 @@
const Pages = require('../../controllers/pages');
const PagesOrder = require('../../controllers/pagesOrder');
const asyncMiddleware = require('../../utils/asyncMiddleware');
/**
* Process one-level pages list to parent-childrens list
* Process one-level pages list to parent-children list
*
* @param {string} parentPageId - parent page id
* @param {Page[]} pages - list of all available pages
* @param {PagesOrder[]} pagesOrder - list of pages order
* @param {number} level
* @param {number} currentLevel
*
* @return {Page[]}
*/
function createMenuTree(pages) {
return pages.filter(page => page._parent === '0').map(page => {
function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel = 1) {
const childrenOrder = pagesOrder.find(order => order.data.page === parentPageId);
/**
* branch is a page children in tree
* if we got some children order on parents tree, then we push found pages in order sequence
* otherwise just find all pages includes parent tree
*/
let ordered = [];
if (childrenOrder) {
ordered = childrenOrder.order.map(pageId => {
return pages.find(page => page._id === pageId);
});
}
const unordered = pages.filter(page => page._parent === parentPageId);
const branch = [ ...new Set([...ordered, ...unordered]) ];
/**
* stop recursion when we got the passed max level
*/
if (currentLevel === level + 1) {
return [];
}
/**
* Each parents children can have subbranches
*/
return branch.filter(page => page && page._id).map(page => {
return Object.assign({
children: pages.filter(child => child._parent === page._id).reverse()
children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1)
}, page.data);
});
}
@ -21,13 +56,19 @@ function createMenuTree(pages) {
* @param next
*/
module.exports = asyncMiddleware(async function (req, res, next) {
try {
const menu = await Pages.getAll();
/**
* Pages without parent
* @type {string}
*/
const parentIdOfRootPages = '0';
res.locals.menu = createMenuTree(menu);
try {
const pages = await Pages.getAll();
const pagesOrder = await PagesOrder.getAll();
res.locals.menu = createMenuTree(parentIdOfRootPages, pages, pagesOrder, 2);
} catch (error) {
console.log('Can not load menu:', error);
}
next();
});

View file

@ -1,6 +1,7 @@
const express = require('express');
const router = express.Router();
const Pages = require('../controllers/pages');
const PagesOrder = require('../controllers/pagesOrder');
const verifyToken = require('./middlewares/token');
const allowEdit = require('./middlewares/locals');
@ -24,12 +25,14 @@ router.get('/page/edit/:id', verifyToken, allowEdit, async (req, res, next) => {
const pageId = req.params.id;
try {
let page = await Pages.get(pageId);
let pagesAvailable = await Pages.getAllExceptChildren(pageId);
const page = await Pages.get(pageId);
const pagesAvailable = await Pages.getAllExceptChildrens(pageId);
const parentsChildrenOrdered = await PagesOrder.getOrderedChildren(pagesAvailable, pageId, page._parent, true);
res.render('pages/form', {
pagesAvailable,
page
page,
parentsChildrenOrdered,
pagesAvailable
});
} catch (error) {
res.status(404);

12
src/utils/crypto.js Normal file
View file

@ -0,0 +1,12 @@
const crypto = require('crypto');
/**
* Create binary md5
* @param stringToHash - string to hash
* @returns {string} - binary hash of argument
*/
module.exports = function binaryMD5(stringToHash) {
return crypto.createHash('md5')
.update(stringToHash)
.digest('binary');
};

View file

@ -0,0 +1,6 @@
const Datastore = require('nedb');
const config = require('../../../config');
const db = new Datastore({ filename: `./${config.database}/aliases.db`, autoload: true });
module.exports = db;

View file

@ -1,5 +1,7 @@
const pages = require('./pages');
const password = require('./password');
const aliases = require('./aliases');
const pagesOrder = require('./pagesOrder');
/**
* @class Database
@ -144,5 +146,7 @@ class Database {
module.exports = {
class: Database,
pages: new Database(pages),
password: new Database(password)
password: new Database(password),
aliases: new Database(aliases),
pagesOrder: new Database(pagesOrder)
};

View file

@ -1,6 +1,6 @@
const Datastore = require('nedb');
const config = require('../../../config');
const db = new Datastore({filename: `./${config.database}/pages.db`, autoload: true});
const db = new Datastore({ filename: `./${config.database}/pages.db`, autoload: true });
module.exports = db;

View file

@ -0,0 +1,5 @@
const Datastore = require('nedb');
const config = require('../../../config');
const db = new Datastore({ filename: `./${config.database}/pagesOrder.db`, autoload: true });
module.exports = db;

View file

@ -1,6 +1,6 @@
const Datastore = require('nedb');
const config = require('../../../config');
const db = new Datastore({filename: `./${config.database}/password.db`, autoload: true});
const db = new Datastore({ filename: `./${config.database}/password.db`, autoload: true });
module.exports = db;

View file

@ -40,8 +40,8 @@ module.exports = class RCParser {
return RCParser.DEFAULTS;
}
const file = fs.readFileSync(rcPath, {encoding: 'UTF-8'});
const rConfig = {};
const file = fs.readFileSync(rcPath, { encoding: 'UTF-8' });
const rConfig = RCParser.DEFAULTS;
let userConfig;
try {
@ -51,8 +51,11 @@ module.exports = class RCParser {
return RCParser.DEFAULTS;
}
rConfig.title = userConfig.title || RCParser.DEFAULTS.title;
rConfig.menu = userConfig.menu || RCParser.DEFAULTS.menu;
for (let option in userConfig) {
if (userConfig.hasOwnProperty(option)) {
rConfig[option] = userConfig[option] || RCParser.DEFAULTS[option] || undefined;
}
}
if (!(rConfig.menu instanceof Array)) {
console.log('Menu section in the rc file must be an array.');
@ -70,7 +73,7 @@ module.exports = class RCParser {
return false;
}
const {title, uri} = option;
const { title, uri } = option;
if (!title || typeof title !== 'string') {
console.log(`Menu option #${i} title must be a string.`);

78
src/utils/translation.js Normal file
View file

@ -0,0 +1,78 @@
const translationTable = {
'а': 'a',
'б': 'b',
'в': 'v',
'г': 'g',
'д': 'd',
'е': 'e',
'ж': 'g',
'з': 'z',
'и': 'i',
'й': 'y',
'к': 'k',
'л': 'l',
'м': 'm',
'н': 'n',
'о': 'o',
'п': 'p',
'р': 'r',
'с': 's',
'т': 't',
'у': 'u',
'ф': 'f',
'ы': 'i',
'э': 'e',
'А': 'A',
'Б': 'B',
'В': 'V',
'Г': 'G',
'Д': 'D',
'Е': 'E',
'Ж': 'G',
'З': 'Z',
'И': 'I',
'Й': 'Y',
'К': 'K',
'Л': 'L',
'М': 'M',
'Н': 'N',
'О': 'O',
'П': 'P',
'Р': 'R',
'С': 'S',
'Т': 'T',
'У': 'U',
'Ф': 'F',
'Ы': 'I',
'Э': 'E',
'ё': 'yo',
'х': 'h',
'ц': 'ts',
'ч': 'ch',
'ш': 'sh',
'щ': 'shch',
'ъ': "''",
'ь': "'",
'ю': 'yu',
'я': 'ya',
'Ё': 'YO',
'Х': 'H',
'Ц': 'TS',
'Ч': 'CH',
'Ш': 'SH',
'Щ': 'SHCH',
'Ъ': "''",
'Ь': "'",
'Ю': 'YU',
'Я': 'YA'
};
/**
* Function to translate string
*
* @param string - string to translate
* @returns {string} - translated string
*/
module.exports = function translateString(string) {
return string.replace(/[А-яёЁ]/g, (char) => translationTable[char] || char);
};

View file

@ -1,14 +1,27 @@
<div class="docs-aside-toggler" onclick="document.querySelector('.docs-aside').classList.toggle('docs-aside--toggled')">
{{ svg('menu') }} Table of contents
</div>
<div class="docs-aside">
{% for firstLevelPage in menu %}
<section class="docs-aside__section">
<a class="docs-aside__section-title" href="/page/{{ firstLevelPage._id }}">
<a class="docs-aside__section-title"
{% if firstLevelPage.uri %}
href="/{{ firstLevelPage.uri }}"
{% else %}
href="/page/{{ firstLevelPage._id }}"
{% endif %}>
{{ firstLevelPage.title }}
</a>
{% if firstLevelPage.children is not empty %}
<ul class="docs-aside__section-list">
{% for child in firstLevelPage.children %}
<li>
<a href="/page/{{ child._id }}">
<a
{% if child.uri %}
href="/{{ child.uri }}"
{% else %}
href="/page/{{ child._id }}"
{% endif %}>
{{ child.title }}
</a>
</li>

View file

@ -2,7 +2,7 @@
<a href="/" class="docs-header__logo">
{{ config.title }}
</a>
<ul class="docs-header__menu">
<ul class="docs-header__menu-add">
<li>
{% if isAuthorized == true %}
<a class="docs-header__button" href="/page/new">
@ -13,7 +13,12 @@
</li>
{% for option in config.menu %}
<li>
<a href="{{option.uri}}">
<a
{% if child.uri %}
href="{{ option.uri }}"
{% else %}
href="/page/{{ option._id }}"
{% endif %}>
{{ option.title }}
</a>
</li>

View file

@ -1,6 +0,0 @@
{% extends 'layout.twig' %}
{% block body %}
{{title}}
<p>Welcome to {{title}}</p>
{% endblock %}

View file

@ -1,11 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<title>{{ config.title }}</title>
<link rel="stylesheet" href="/dist/main.css" />
<meta property="og:type" content="article" />
<meta property="og:title" content="{{ page.title }}" />
<meta property="article:modified_time" content="{{ (page.body.time / 1000) | date("c") }}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<body>
{% include "components/header.twig" with isAuthorized %}
{% include "components/header.twig" with res.locals.isAuthorized %}
<div class="docs">
<aside class="docs__aside">
{% include "components/aside.twig" %}

View file

@ -1 +1,2 @@
<pre class="block-code">{{ code }}</pre>
<pre class="block-code">{{ code|escape }}</pre>

View file

@ -1,9 +1,10 @@
{% set tag = 'ul' %}
{% if type == 'orderd' %}
{% if style == 'ordered' %}
{% set tag = 'ol' %}
{% endif %}
<{{ tag }} class="block-list block-list--{{ type }}">
<{{ tag }} class="block-list block-list--{{ style }}">
{% for item in items %}
<li>
{{ item }}

View file

@ -7,32 +7,55 @@
}
</style>
<section data-module="writing">
<module-settings hidden>
<textarea name="module-settings" hidden>
{
"page": {{ page | json_encode }}
}
</module-settings>
</textarea>
<header class="writing-header">
<span class="writing-header__left">
<span>
New Page at the
{% set currentPageId = 0 %}
{% if page is not empty %}
{% set currentPageId = page._id %}
{% endif %}
<select name="parent">
<option value="0">Root</option>
{% for _page in pagesAvailable %}
{% if _page._id != currentPageId %}
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
<select name="parent">
<option value="0">Root</option>
{% for _page in pagesAvailable %}
{% if _page._id != currentPageId %}
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
{{ _page.title }}
</option>
{% endif %}
{% endfor %}
</select>
</span>
<span class="writing-header__save" name="js-submit">
Save
{% endif %}
{% endfor %}
</select>
</span>
{% if parentsChildrenOrdered is not empty %}
<span>
Put Above
<select name="above">
<option value="0">—</option>
{% for _page in parentsChildrenOrdered %}
<option value="{{ _page._id }}">{{ _page.title }}</option>
{% endfor %}
</select>
</span>
{% endif %}
</span>
<span class="writing-header__save" name="js-submit-save">Save</span>
</header>
<div class="writing-editor">
<div id="codex-editor"></div>
</div>
<div class="writing-buttons">
{% if page._id is not empty %}
<span class="writing-buttons__remove" name="js-submit-remove">Remove</span>
{% endif %}
</div>
{% if page is not empty %}
<p><input type="text" class="uri-input" name="uri-input" placeholder="URI(Optional)" value="{{ page.uri }}"></p>
{% endif %}
</section>
{% endblock %}

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html style="height: 100%">
<head>
<title>{{ config.title }}</title>
<link rel="stylesheet" href="/dist/main.css" />
<link rel="preload" href="{{ config.landingFrameSrc }}" as="document">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<body class="landing-body">
{% include "components/header.twig" %}
<div class="landing-loader" id="frame-loader">
{{ svg('loader') }}
</div>
<iframe class="landing-frame" src="{{ config.landingFrameSrc }}" seamless frameborder="0" onload="this.style.opacity = 1; setTimeout(document.getElementById('frame-loader').remove(), 500)"></iframe>
</body>
</html>

View file

@ -7,8 +7,13 @@
Documentation
</a>
{% if page._parent %}
<a href="/page/{{ page._parent }}" class="page__header-nav">
Parent {{ pageParent.title }}
<a class="page__header-nav"
{% if pageParent.uri %}
href="/{{ pageParent.uri }}"
{% else %}
href="/page/{{ pageParent._id }}"
{% endif %}>
{{ pageParent.title }}
</a>
{% endif %}
<time class="page__header-time">

View file

@ -1,17 +1,18 @@
const {app} = require('../bin/www');
const { app } = require('../bin/www');
const chai = require('chai');
const chaiHTTP = require('chai-http');
const {expect} = chai;
const { expect } = chai;
chai.use(chaiHTTP);
describe('Express app', () => {
it('App is available', async () => {
it('App is available', async (done) => {
let agent = chai.request.agent(app);
const result = await agent
.get('/');
expect(result).to.have.status(200);
done();
});
});

139
test/models/alias.js Normal file
View file

@ -0,0 +1,139 @@
const {expect} = require('chai');
const fs = require('fs');
const path = require('path');
const config = require('../../config');
const Alias = require('../../src/models/alias');
const binaryMD5 = require('../../src/utils/crypto');
const {aliases} = require('../../src/utils/database');
describe('Alias model', () => {
after(() => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './aliases.db');
if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB);
}
});
it('Working with empty model', async () => {
let alias = new Alias();
expect(alias.data).to.be.a('object');
let {data} = alias;
expect(data._id).to.be.undefined;
expect(data.hash).to.be.undefined;
expect(data.type).to.be.undefined;
expect(data.deprecated).to.be.false;
expect(data.id).to.be.undefined;
alias = new Alias();
data = alias.data;
expect(data._id).to.be.undefined;
expect(data.hash).to.be.undefined;
expect(data.type).to.be.undefined;
expect(data.deprecated).to.be.false;
expect(data.id).to.be.undefined;
const initialData = {
_id: 'alias_id',
type: Alias.types.PAGE,
id: 'page_id'
};
const aliasName = 'alias name';
alias = new Alias(initialData, aliasName);
data = alias.data;
expect(data._id).to.equal(initialData._id);
expect(data.hash).to.equal(binaryMD5(aliasName));
expect(data.type).to.equal(initialData.type);
expect(data.deprecated).to.equal(false);
const update = {
type: Alias.types.PAGE,
id: 'page_id',
hash: binaryMD5('another test hash'),
deprecated: true
};
alias.data = update;
data = alias.data;
expect(data._id).to.equal(initialData._id);
expect(data.type).to.equal(update.type);
expect(data.hash).to.equal(update.hash);
expect(data.deprecated).to.equal(update.deprecated);
});
it('Static get method', async () => {
const initialData = {
type: Alias.types.PAGE,
id: 'page_id'
};
const aliasName = 'alias name';
const alias = new Alias(initialData, aliasName);
const savedAlias = await alias.save();
const foundAlias = await Alias.get(aliasName);
const {data} = foundAlias;
expect(data._id).to.equal(savedAlias._id);
expect(data.hash).to.equal(binaryMD5(aliasName));
expect(data.type).to.equal(initialData.type);
expect(data.deprecated).to.equal(false);
});
it('Saving, updating and deleting model in the database', async () => {
const initialData = {
type: Alias.types.PAGE,
id: 'page_id'
};
const aliasName = 'alias name';
const alias = new Alias(initialData, aliasName);
const savedAlias = await alias.save();
expect(savedAlias._id).not.be.undefined;
expect(savedAlias.hash).to.equal(binaryMD5(aliasName));
expect(savedAlias.type).to.equal(initialData.type);
expect(savedAlias.id).to.equal(initialData.id);
expect(savedAlias.deprecated).to.equal(false);
const insertedAlias = await aliases.findOne({_id: savedAlias._id});
expect(insertedAlias._id).to.equal(savedAlias._id);
expect(insertedAlias.hash).to.equal(savedAlias.hash);
expect(insertedAlias.type).to.equal(savedAlias.type);
expect(insertedAlias.id).to.equal(savedAlias.id);
expect(insertedAlias.deprecated).to.equal(savedAlias.deprecated);
const updateData = {
type: Alias.types.PAGE,
id: 'page_id',
hash: binaryMD5('another test hash'),
deprecated: true
};
alias.data = updateData;
await alias.save();
expect(alias._id).to.equal(insertedAlias._id);
const updatedAlias = await aliases.findOne({_id: alias._id});
expect(updatedAlias._id).to.equal(savedAlias._id);
expect(updatedAlias.hash).to.equal(updateData.hash);
expect(updatedAlias.type).to.equal(updateData.type);
expect(updatedAlias.id).to.equal(updateData.id);
expect(updatedAlias.deprecated).to.equal(updateData.deprecated);
});
});

View file

@ -4,8 +4,21 @@ const path = require('path');
const config = require('../../config');
const Page = require('../../src/models/page');
const {pages} = require('../../src/utils/database');
const translateString = require('../../src/utils/translation');
describe('Page model', () => {
const transformToUri = (string) => {
return translateString(string
.replace(/&nbsp;/g, ' ')
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
.replace(/ +/g, ' ')
.trim()
.toLowerCase()
.split(' ')
.join('-'));
};
after(() => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db');
@ -23,8 +36,9 @@ describe('Page model', () => {
expect(data._id).to.be.undefined;
expect(data.title).to.be.empty;
expect(data.uri).to.be.empty;
expect(data.body).to.be.undefined;
expect(data.parent).to.be.undefined;
expect(data.parent).to.be.equal('0');
page = new Page(null);
@ -32,8 +46,9 @@ describe('Page model', () => {
expect(data._id).to.be.undefined;
expect(data.title).to.be.empty;
expect(data.uri).to.be.empty;
expect(data.body).to.be.undefined;
expect(data.parent).to.be.undefined;
expect(data.parent).to.be.equal('0');
const initialData = {
_id: 'page_id',
@ -57,13 +72,15 @@ describe('Page model', () => {
expect(data._id).to.equal(initialData._id);
expect(data.title).to.equal(initialData.body.blocks[0].data.text);
expect(data.uri).to.be.empty;
expect(data.body).to.deep.equal(initialData.body);
expect(data.parent).to.be.undefined;
expect(data.parent).to.be.equal('0');
expect(json._id).to.equal(initialData._id);
expect(json.title).to.equal(initialData.body.blocks[0].data.text);
expect(json.title).to.equal(initialData.body.blocks[0].data.text);
expect(json.body).to.deep.equal(initialData.body);
expect(json.parent).to.be.undefined;
expect(json.parent).to.be.equal('0');
const update = {
_id: 12345,
@ -85,8 +102,9 @@ describe('Page model', () => {
expect(data._id).to.equal(initialData._id);
expect(data.title).to.equal(update.body.blocks[0].data.text);
expect(data.uri).to.be.empty;
expect(data.body).to.equal(update.body);
expect(data.parent).to.be.undefined;
expect(data.parent).to.be.equal('0');
});
it('Saving, updating and deleting model in the database', async () => {
@ -96,7 +114,7 @@ describe('Page model', () => {
{
type: 'header',
data: {
text: 'Page header'
text: 'New page header'
}
}
]
@ -108,6 +126,7 @@ describe('Page model', () => {
expect(savedPage._id).not.be.undefined;
expect(savedPage.title).to.equal(initialData.body.blocks[0].data.text);
expect(savedPage.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text));
expect(savedPage.body).to.equal(initialData.body);
expect(page._id).not.be.undefined;
@ -115,6 +134,7 @@ describe('Page model', () => {
expect(insertedPage._id).to.equal(page._id);
expect(insertedPage.title).to.equal(page.title);
expect(insertedPage.uri).to.equal(page.uri);
expect(insertedPage.body).to.deep.equal(page.body);
const updateData = {
@ -127,7 +147,8 @@ describe('Page model', () => {
}
}
]
}
},
uri: 'updated-uri'
};
page.data = updateData;
@ -139,6 +160,7 @@ describe('Page model', () => {
expect(updatedPage._id).to.equal(savedPage._id);
expect(updatedPage.title).to.equal(updateData.body.blocks[0].data.text);
expect(updatedPage.uri).to.equal(updateData.uri);
expect(updatedPage.body).to.deep.equal(updateData.body);
await page.destroy();
@ -150,6 +172,39 @@ describe('Page model', () => {
expect(removedPage).to.be.null;
});
it('Handle multiple page creation with the same uri', async () => {
const initialData = {
body: {
blocks: [
{
type: 'header',
data: {
text: 'New page header'
}
}
]
}
};
const firstPage = new Page(initialData);
let firstSavedPage = await firstPage.save();
const secondPage = new Page(initialData);
let secondSavedPage = await secondPage.save();
expect(secondSavedPage.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text) + '-1');
const newUri = 'new-uri';
firstPage.data = {...firstPage.data, uri: newUri};
firstSavedPage = await firstPage.save();
expect(firstSavedPage.uri).to.equal(newUri);
const thirdPage = new Page(initialData);
let thirdSavedPage = await thirdPage.save();
expect(thirdSavedPage.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text));
});
it('Static get method', async () => {
const initialData = {
body: {
@ -173,6 +228,7 @@ describe('Page model', () => {
expect(data._id).to.equal(savedPage._id);
expect(data.title).to.equal(initialData.body.blocks[0].data.text);
expect(data.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text));
expect(data.body).to.deep.equal(initialData.body);
await page.destroy();
@ -213,6 +269,7 @@ describe('Page model', () => {
expect(foundPages.length).to.equal(2);
foundPages.forEach((page, i) => {
expect(page.title).to.equal(pagesToSave[i].body.blocks[0].data.text);
expect(page.uri).to.equal(transformToUri(pagesToSave[i].body.blocks[0].data.text));
expect(page.body).to.deep.equal(pagesToSave[i].body);
});
});
@ -257,6 +314,7 @@ describe('Page model', () => {
expect(testedParent._id).to.equal(parentId);
expect(testedParent.title).to.equal(parent.body.blocks[0].data.text);
expect(testedParent.uri).to.equal(transformToUri(parent.body.blocks[0].data.text));
expect(testedParent.body).to.deep.equal(parent.body);
const children = await parent.children;
@ -267,6 +325,7 @@ describe('Page model', () => {
expect(testedChild._id).to.equal(childId);
expect(testedChild.title).to.equal(child.body.blocks[0].data.text);
expect(testedChild.uri).to.equal(transformToUri(child.body.blocks[0].data.text));
expect(testedChild.body).to.deep.equal(child.body);
expect(testedChild._parent).to.equal(child._parent);
expect(testedChild._parent).to.equal(parent._id);
@ -293,4 +352,31 @@ describe('Page model', () => {
expect(page.title).to.equal(pageData.body.blocks[0].data.text);
});
it('test deletion', async () => {
const pages = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const orders = {
'0' : ['1', '2', '3'],
'1' : ['4', '5'],
'5' : ['6', '7', '8'],
'3' : ['9']
};
function deleteRecursively(startFrom) {
const order = orders[startFrom];
if (!order) {
const found = pages.indexOf(startFrom);
pages.splice(found, 1);
return;
}
order.forEach(id => {
deleteRecursively(id);
});
const found = pages.indexOf(startFrom);
pages.splice(found, 1);
}
});
});

143
test/models/pageOrder.js Normal file
View file

@ -0,0 +1,143 @@
const {expect} = require('chai');
const fs = require('fs');
const path = require('path');
const config = require('../../config');
const PageOrder = require('../../src/models/pageOrder');
const {pagesOrder} = require('../../src/utils/database');
describe('PageOrder model', () => {
after(() => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pagesOrder.db');
if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB);
}
});
it('Empty Model', async () => {
let pageOrder = new PageOrder();
expect(pageOrder.data).to.be.a('object');
let {data} = pageOrder;
expect(data._id).to.be.undefined;
expect(data.page).to.be.to.equal('0');
expect(data.order).to.be.an('array').that.is.empty;
page = new PageOrder(null);
data = page.data;
expect(data._id).to.be.undefined;
expect(data.page).to.be.to.equal('0');
expect(data.order).to.be.an('array').that.is.empty;
const testData = {
_id: 'order_id',
page: 'page_id',
order: []
};
page = new PageOrder(testData);
data = page.data;
expect(data._id).to.equal(testData._id);
expect(data.page).to.equal(testData.page);
expect(data.order).to.be.an('array').that.is.empty;
});
it('Testing Model methods', async () => {
const testData = {
page: 'page_id',
order: ['1', '2']
};
const pageOrder = new PageOrder(testData);
let {data} = await pageOrder.save();
expect(data._id).not.be.undefined;
expect(data.page).to.equal(testData.page);
expect(data.order).to.deep.equals(testData.order);
const insertedPageOrder = await pagesOrder.findOne({_id: data._id});
expect(insertedPageOrder._id).to.equal(data._id);
expect(insertedPageOrder.page).to.equal(data.page);
expect(insertedPageOrder.order).to.deep.equal(data.order);
const updateData = {
page: 'page_id_2',
order: ['3']
};
pageOrder.data = updateData;
await pageOrder.save();
expect(pageOrder.data._id).to.equal(insertedPageOrder._id);
const updatedData = await pagesOrder.findOne({_id: insertedPageOrder._id});
expect(updatedData.page).to.equal(updateData.page);
expect(updatedData.order).to.deep.equal(updateData.order);
await pageOrder.destroy();
expect(pageOrder.data._id).to.be.undefined;
const removedPage = await pagesOrder.findOne({_id: updatedData._id});
expect(removedPage).to.be.null;
});
it('Testing push and remove order methods', async () => {
const testData = {
page: 'page_id',
order: ['1', '2']
};
const pageOrder = new PageOrder(testData);
await pageOrder.save();
pageOrder.push('3');
expect(pageOrder.data.order).to.be.an('array').that.is.not.empty;
pageOrder.data.order.forEach((el) => {
expect(el).to.be.an('string')
});
expect(pageOrder.data.order).to.deep.equals(['1', '2', '3']);
pageOrder.remove('2');
expect(pageOrder.data.order).to.deep.equals(['1', '3']);
expect(() => {
pageOrder.push(3);
}).to.throw('given id is not string');
pageOrder.push('4');
pageOrder.push('5');
pageOrder.push('2');
pageOrder.putAbove('2', '3');
expect(pageOrder.data.order).to.deep.equals(['1', '2', '3', '4', '5']);
pageOrder.putAbove('2', '10');
expect(pageOrder.data.order).to.deep.equals(['1', '2', '3', '4', '5']);
await pageOrder.destroy();
});
it('Testing static methods', async () => {
const testData = {
page: 'page_id',
order: ['1', '2']
};
const pageOrder = new PageOrder(testData);
const insertedData = await pageOrder.save();
const insertedPageOrder = await PageOrder.get(insertedData.data.page);
expect(insertedPageOrder).to.instanceOf(PageOrder);
expect(insertedPageOrder.data._id).to.be.equal(insertedData.data._id);
const emptyInstance = await PageOrder.get(null);
expect(emptyInstance.data.page).to.be.equal('0');
expect(emptyInstance.data.order).to.be.an('array').that.is.empty;
await pageOrder.destroy();
});
});

59
test/rest/aliases.js Normal file
View file

@ -0,0 +1,59 @@
const {app} = require('../../bin/www');
const fs = require('fs');
const path = require('path');
const config = require('../../config');
const chai = require('chai');
const chaiHTTP = require('chai-http');
const {expect} = chai;
chai.use(chaiHTTP);
describe('Aliases REST: ', () => {
let agent;
before(async () => {
agent = chai.request.agent(app);
});
after(async () => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db');
if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB);
}
const pathToAliasDB = path.resolve(__dirname, '../../', config.database, './aliases.db');
if (fs.existsSync(pathToAliasDB)) {
fs.unlinkSync(pathToAliasDB);
}
});
it('Finding page with alias', async () => {
const body = {
time: 1548375408533,
blocks: [
{
type: 'header',
data: {
text: 'Test header'
}
}
]
};
const put = await agent
.put('/api/page')
.send({body});
expect(put).to.have.status(200);
expect(put).to.be.json;
const {result: {uri}} = put.body;
const get = await agent.get('/' + uri);
expect(get).to.have.status(200);
});
});

View file

@ -1,5 +1,8 @@
const {app} = require('../../bin/www');
const model = require('../../src/models/page');
const Page = require('../../src/models/page');
const PageOrder = require('../../src/models/pageOrder');
const translateString = require('../../src/utils/translation');
const fs = require('fs');
const path = require('path');
@ -12,16 +15,36 @@ chai.use(chaiHTTP);
describe('Pages REST: ', () => {
let agent;
const transformToUri = (string) => {
return translateString(string
.replace(/&nbsp;/g, ' ')
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
.replace(/ +/g, ' ')
.trim()
.toLowerCase()
.split(' ')
.join('-'));
};
before(async () => {
agent = chai.request.agent(app);
});
after(async () => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db');
const pathToPagesDB = path.resolve(__dirname, '../../', config.database, './pages.db');
const pathToPagesOrderDB = path.resolve(__dirname, '../../', config.database, './pagesOrder.db');
const pathToAliasesDB = path.resolve(__dirname, '../../', config.database, './aliases.db');
if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB);
if (fs.existsSync(pathToPagesDB)) {
fs.unlinkSync(pathToPagesDB);
}
if (fs.existsSync(pathToPagesOrderDB)) {
fs.unlinkSync(pathToPagesOrderDB);
}
if (fs.existsSync(pathToAliasesDB)) {
fs.unlinkSync(pathToAliasesDB);
}
});
@ -36,10 +59,10 @@ describe('Pages REST: ', () => {
}
]
};
const parent = 0;
const res = await agent
.put('/api/page')
.send({body});
.send({body, parent});
expect(res).to.have.status(200);
expect(res).to.be.json;
@ -49,6 +72,7 @@ describe('Pages REST: ', () => {
expect(success).to.be.true;
expect(result._id).to.be.a('string');
expect(result.title).to.equal(body.blocks[0].data.text);
expect(result.uri).to.equal(transformToUri(body.blocks[0].data.text));
expect(result.body).to.deep.equal(body);
const createdPage = await model.get(result._id);
@ -56,9 +80,14 @@ describe('Pages REST: ', () => {
expect(createdPage).not.be.null;
expect(createdPage._id).to.equal(result._id);
expect(createdPage.title).to.equal(body.blocks[0].data.text);
expect(createdPage.uri).to.equal(transformToUri(body.blocks[0].data.text));
expect(createdPage.body).to.deep.equal(body);
createdPage.destroy();
const pageOrder = await PageOrder.get('' + (createdPage.data.parent || 0));
expect(pageOrder.order).to.be.an('array');
await createdPage.destroy();
await pageOrder.destroy();
});
it('Page data validation on create', async () => {
@ -106,12 +135,15 @@ describe('Pages REST: ', () => {
expect(success).to.be.true;
const foundPage = await model.get(_id);
const pageOrder = await PageOrder.get('' + foundPage._parent);
expect(foundPage._id).to.equal(_id);
expect(foundPage.title).to.equal(body.blocks[0].data.text);
expect(foundPage.uri).to.equal(transformToUri(body.blocks[0].data.text));
expect(foundPage.body).to.deep.equal(body);
foundPage.destroy();
await pageOrder.destroy();
await foundPage.destroy();
});
it('Finding page with not existing id', async () => {
@ -157,10 +189,11 @@ describe('Pages REST: ', () => {
}
]
};
const updatedUri = 'updated-uri';
res = await agent
.post(`/api/page/${_id}`)
.send({body: updatedBody});
.send({body: updatedBody, uri: updatedUri});
expect(res).to.have.status(200);
expect(res).to.be.json;
@ -171,18 +204,83 @@ describe('Pages REST: ', () => {
expect(result._id).to.equal(_id);
expect(result.title).not.equal(body.blocks[0].data.text);
expect(result.title).to.equal(updatedBody.blocks[0].data.text);
expect(result.uri).not.equal(transformToUri(body.blocks[0].data.text));
expect(result.uri).to.equal(updatedUri);
expect(result.body).not.equal(body);
expect(result.body).to.deep.equal(updatedBody);
const updatedPage = await model.get(_id);
const pageOrder = await PageOrder.get('' + updatedPage._parent);
expect(updatedPage._id).to.equal(_id);
expect(updatedPage.title).not.equal(body.blocks[0].data.text);
expect(updatedPage.title).to.equal(updatedBody.blocks[0].data.text);
expect(updatedPage.uri).not.equal(transformToUri(body.blocks[0].data.text));
expect(updatedPage.uri).to.equal(updatedUri);
expect(updatedPage.body).not.equal(body);
expect(updatedPage.body).to.deep.equal(updatedBody);
updatedPage.destroy();
await pageOrder.destroy();
await updatedPage.destroy();
});
it('Handle multiple page creation with the same uri', async () => {
const body = {
blocks: [
{
type: 'header',
data: {
text: 'Page header'
}
}
]
};
let res = await agent
.put('/api/page')
.send({body});
expect(res).to.have.status(200);
expect(res).to.be.json;
const {result: {_id}} = res.body;
res = await agent
.put('/api/page')
.send({body: body});
expect(res).to.have.status(200);
expect(res).to.be.json;
const {success: secondPageSuccess, result: secondPageResult} = res.body;
expect(secondPageSuccess).to.be.true;
expect(secondPageResult.title).to.equal(body.blocks[0].data.text);
expect(secondPageResult.uri).to.equal(transformToUri(body.blocks[0].data.text) + '-1');
expect(secondPageResult.body).to.deep.equal(body);
const newFirstPageUri = 'New-uri';
res = await agent
.post(`/api/page/${_id}`)
.send({body: body, uri: newFirstPageUri});
expect(res).to.have.status(200);
expect(res).to.be.json;
res = await agent
.put('/api/page')
.send({body: body});
expect(res).to.have.status(200);
expect(res).to.be.json;
const {success: thirdPageSuccess, result: thirdPageResult} = res.body;
expect(thirdPageSuccess).to.be.true;
expect(thirdPageResult.title).to.equal(body.blocks[0].data.text);
expect(thirdPageResult.uri).to.equal(transformToUri(body.blocks[0].data.text));
expect(thirdPageResult.body).to.deep.equal(body);
});
it('Updating page with not existing id', async () => {
@ -214,7 +312,7 @@ describe('Pages REST: ', () => {
{
type: 'header',
data: {
text: 'Page header'
text: 'Page header to be deleted'
}
}
]
@ -238,13 +336,18 @@ describe('Pages REST: ', () => {
const {success, result} = res.body;
expect(success).to.be.true;
expect(result._id).to.be.undefined;
expect(result.title).to.equal(body.blocks[0].data.text);
expect(result.body).to.deep.equal(body);
const deletedPage = await model.get(_id);
if (result) {
expect(result._id).to.be.undefined;
expect(result.title).to.equal(body.blocks[0].data.text);
expect(result.uri).to.equal(transformToUri(body.blocks[0].data.text));
expect(result.body).to.deep.equal(body);
const deletedPage = await model.get(_id);
expect(deletedPage._id).to.be.undefined;
expect(deletedPage._id).to.be.undefined;
} else {
expect(result).to.be.null;
}
});
it('Removing page with not existing id', async () => {
@ -259,4 +362,164 @@ describe('Pages REST: ', () => {
expect(success).to.be.false;
expect(error).to.equal('Page with given id does not exist');
});
async function createPageTree() {
/**
* Creating page tree
*
* 0
* / \
* 1 2
* / \ \
* 3 5 6
* / / \
* 4 7 8
*/
const body = {
blocks: [
{
type: 'header',
data: {
text: 'Page header'
}
}
]
};
let parent, res, result;
/** Page 1 */
parent = 0;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page1 = result;
/** Page 2 */
parent = 0;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page2 = result;
/** Page 3 */
parent = page1._id;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page3 = result;
/** Page 4 */
parent = page3._id;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page4 = result;
/** Page 5 */
parent = page1._id;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page5 = result;
/** Page 6 */
parent = page2._id;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page6 = result;
/** Page 7 */
parent = page6._id;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page7 = result;
/** Page 8 */
parent = page6._id;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page8 = result;
return [
0,
page1._id,
page2._id,
page3._id,
page4._id,
page5._id,
page6._id,
page7._id,
page8._id
];
}
it('Removing a page and its children', async () => {
let pages = await createPageTree();
/**
* Deleting from tree page1
* Also pages 3, 5 and 4 must be deleted
*/
await agent
.delete(`/api/page/${pages[1]}`);
const page3 = await Page.get(pages[3]);
expect(page3.data._id).to.be.undefined;
const page4 = await Page.get(pages[4]);
expect(page4.data._id).to.be.undefined;
const page5 = await Page.get(pages[5]);
expect(page5.data._id).to.be.undefined;
/** Okay, pages above is deleted */
const page2 = await Page.get(pages[2]);
expect(page2.data._id).not.to.be.undefined;
/** First check pages 6, 7 and 8 before deleting */
let page6 = await Page.get(pages[6]);
expect(page6.data._id).not.to.be.undefined;
let page7 = await Page.get(pages[7]);
expect(page7.data._id).not.to.be.undefined;
let page8 = await Page.get(pages[8]);
expect(page8.data._id).not.to.be.undefined;
/**
* Delete page6
* also pages 7 and 8 must be deleted
*/
await agent
.delete(`/api/page/${pages[6]}`);
page6 = await Page.get(pages[6]);
expect(page6.data._id).to.be.undefined;
page7 = await Page.get(pages[7]);
expect(page7.data._id).to.be.undefined;
page8 = await Page.get(pages[8]);
expect(page8.data._id).to.be.undefined;
});
});

952
yarn.lock

File diff suppressed because it is too large Load diff