1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-07-31 02:59:43 +02:00

Typescript rewrite (#147)

* Updated highlight.js

* Update .codexdocsrc.sample

remove undefined page for a fresh new install

* backend rewritten in TS

* test -> TS, .added dockerignore, bug fixed

* Removed compiled js files, eslint codex/ts added

* fixed jsdocs warning, leaving editor confirmation

* use path.resolve for DB paths

* db drives updated + fixed User model

* redundant cleared + style fixed

* explicit type fixing

* fixing testing code

* added body block type

* compiled JS files -> dist, fixed compiling errors

* fixed compiling error, re-organized ts source code

* updated Dockerfile

* fixed link to parent page

* up nodejs version

* fix package name

* fix deps

Co-authored-by: nvc8996 <nvc.8996@gmail.com>
Co-authored-by: Taly <vitalik7tv@yandex.ru>
This commit is contained in:
Nikita Melnikov 2022-03-05 22:57:23 +04:00 committed by GitHub
parent 059cfb96f9
commit 34514761f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
99 changed files with 3817 additions and 2249 deletions

View file

@ -0,0 +1,18 @@
import { NextFunction, Request, Response } from 'express';
interface InputFunction {
(req: Request, res: Response, next: NextFunction): void;
}
/**
* Helper for making async middlewares for express router
*
* @param {Function} fn - input function
* @returns {function(*=, *=, *=)}
*/
export default function asyncMiddleware(fn: InputFunction): (req: Request, res: Response, next: NextFunction) => void {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next))
.catch(next);
};
}

View file

@ -0,0 +1,45 @@
import crypto from 'crypto';
/**
*
* @param {string} hexStr - input hex string
* @returns {string} - output binary string
*/
function hexToBinary(hexStr: string): string {
return (parseInt(hexStr, 16).toString(2))
.padStart(8, '0');
}
/**
* Create binary md5
*
* @param stringToHash - string to hash
* @returns {string} - binary hash of argument
*/
export function binaryMD5(stringToHash: string): string {
return hexToBinary(crypto.createHash('md5')
.update(stringToHash)
.digest('hex'));
}
/**
* Returns 16 random bytes in hex format
*
* @returns {Promise<string>}
*/
export function random16(): Promise<string> {
return new Promise((resolve, reject) => {
crypto.randomBytes(16, (err, raw) => {
if (err) {
reject(err);
}
resolve(raw.toString('hex'));
});
});
}
export default {
binaryMD5,
random16,
};

View file

@ -0,0 +1,177 @@
import Datastore from 'nedb';
import { AliasData } from '../../models/alias';
import { FileData } from '../../models/file';
import { PageData } from '../../models/page';
import { PageOrderData } from '../../models/pageOrder';
import { UserData } from '../../models/user';
import initDb from './initDb';
/**
* @typedef Options - optional params
* @param {boolean} multi - (false) allows to take action to several documents
* @param {boolean} upsert - (false) if true, upsert document with update fields.
* Method will return inserted doc or number of affected docs if doc hasn't been inserted
* @param {boolean} returnUpdatedDocs - (false) if true, returns affected docs
*/
interface Options {
multi?: boolean;
upsert?: boolean;
returnUpdatedDocs?: boolean;
}
interface ResolveFunction {
(value: any): void;
}
interface RejectFunction {
(reason?: unknown): void;
}
/**
* @class Database
* @classdesc Simple decorator class to work with nedb datastore
*
* @property {Datastore} db - nedb Datastore object
*/
export class Database<DocType> {
private db: Datastore;
/**
* @class
*
* @param {Object} nedbInstance - nedb Datastore object
*/
constructor(nedbInstance: Datastore) {
this.db = nedbInstance;
}
/**
* Insert new document into the database
*
* @see https://github.com/louischatriot/nedb#inserting-documents
*
* @param {Object} doc - object to insert
* @returns {Promise<Object|Error>} - inserted doc or Error object
*/
public async insert(doc: DocType): Promise<DocType> {
return new Promise((resolve, reject) => this.db.insert(doc, (err, newDoc) => {
if (err) {
reject(err);
}
resolve(newDoc);
}));
}
/**
* Find documents that match passed query
*
* @see https://github.com/louischatriot/nedb#finding-documents
*
* @param {Object} query - query object
* @param {Object} projection - projection object
* @returns {Promise<Array<Object>|Error>} - found docs or Error object
*/
public async find(query: Record<string, unknown>, projection?: DocType): Promise<Array<DocType>> {
const cbk = (resolve: ResolveFunction, reject: RejectFunction) => (err: Error | null, docs: DocType[]) => {
if (err) {
reject(err);
}
resolve(docs);
};
return new Promise((resolve, reject) => {
if (projection) {
this.db.find(query, projection, cbk(resolve, reject));
} else {
this.db.find(query, cbk(resolve, reject));
}
});
}
/**
* Find one document matches passed query
*
* @see https://github.com/louischatriot/nedb#finding-documents
*
* @param {Object} query - query object
* @param {Object} projection - projection object
* @returns {Promise<Object|Error>} - found doc or Error object
*/
public async findOne(query: Record<string, unknown>, projection?: DocType): Promise<DocType> {
const cbk = (resolve: ResolveFunction, reject: RejectFunction) => (err: Error | null, doc: DocType) => {
if (err) {
reject(err);
}
resolve(doc);
};
return new Promise((resolve, reject) => {
if (projection) {
this.db.findOne(query, projection, cbk(resolve, reject));
} else {
this.db.findOne(query, cbk(resolve, reject));
}
});
}
/**
* Update document matches query
*
* @see https://github.com/louischatriot/nedb#updating-documents
*
* @param {Object} query - query object
* @param {Object} update - fields to update
* @param {Options} options - optional params
* @returns {Promise<number|Object|Object[]|Error>} - number of updated rows or affected docs or Error object
*/
public async update(query: Record<string, unknown>, update: DocType, options: Options = {}): Promise<number|boolean|Array<DocType>> {
return new Promise((resolve, reject) => this.db.update(query, update, options, (err, result, affectedDocs) => {
if (err) {
reject(err);
}
switch (true) {
case options.returnUpdatedDocs:
resolve(affectedDocs);
break;
case options.upsert:
if (affectedDocs) {
resolve(affectedDocs);
}
resolve(result);
break;
default:
resolve(result);
}
}));
}
/**
* Remove document matches passed query
*
* @see https://github.com/louischatriot/nedb#removing-documents
*
* @param {Object} query - query object
* @param {Options} options - optional params
* @returns {Promise<number|Error>} - number of removed rows or Error object
*/
public async remove(query: Record<string, unknown>, options: Options = {}): Promise<number> {
return new Promise((resolve, reject) => this.db.remove(query, options, (err, result) => {
if (err) {
reject(err);
}
resolve(result);
}));
}
}
export default {
pages: new Database<PageData>(initDb('pages')),
password: new Database<UserData>(initDb('password')),
aliases: new Database<AliasData>(initDb('aliases')),
pagesOrder: new Database<PageOrderData>(initDb('pagesOrder')),
files: new Database<FileData>(initDb('files')),
};

View file

@ -0,0 +1,16 @@
import Datastore from 'nedb';
import config from 'config';
import path from 'path';
/**
* Init function for nedb instance
*
* @param {string} name - name of the data file
* @returns {Datastore} db - nedb instance
*/
export default function initDb(name: string): Datastore {
return new Datastore({
filename: path.resolve(`./${config.get('database')}/${name}.db`),
autoload: true,
});
}

View file

@ -0,0 +1,38 @@
/**
* Merge to objects recursively
*
* @param {object} target
* @param {object[]} sources
* @returns {object}
*/
/**
* @param {Record<string, any>} target - target to merge into
* @param {...any[]} sources - sources to merge from
*/
function deepMerge(target: Record<string, any>, ...sources: any[]): Record<string, unknown> {
const isObject = (item: unknown): boolean => !!item && typeof item === 'object' && !Array.isArray(item);
if (!sources.length) {
return target;
}
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) {
Object.assign(target, { [key]: {} });
}
deepMerge(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
return deepMerge(target, ...sources);
}
export default deepMerge;

View file

@ -0,0 +1,124 @@
import fs from 'fs';
import path from 'path';
import config from 'config';
const rcPath = path.resolve(__dirname, '../../../', config.get('rcFile') || './.codexdocsrc');
/**
* @typedef {object} menu
* @property {string} title - menu option title
* @property {string} uri - menu option href
*/
interface Menu {
title: string;
uri: string;
[key: string]: string;
}
/**
* @typedef {object} RCData
* @property {string} title - website title
* @property {Menu[]} menu - options for website menu
*/
interface RCData {
title: string;
menu: Menu[];
[key: string]: string | Menu[];
}
/**
* @class RCParser
* @classdesc Class to parse runtime configuration file for CodeX Docs engine
*/
export default class RCParser {
/**
* Default CodeX Docs configuration
*
* @static
* @returns {{title: string, menu: Array}}
*/
public static get DEFAULTS():RCData {
return {
title: 'CodeX Docs',
menu: [],
};
}
/**
* Find and parse runtime configuration file
*
* @static
* @returns {{title: string, menu: []}}
*/
public static getConfiguration(): RCData {
if (!fs.existsSync(rcPath)) {
return RCParser.DEFAULTS;
}
const file = fs.readFileSync(rcPath, 'utf-8');
const rConfig = RCParser.DEFAULTS;
let userConfig;
try {
userConfig = JSON.parse(file);
} catch (e) {
console.log('CodeX Docs rc file should be in JSON format.');
return RCParser.DEFAULTS;
}
for (const option in userConfig) {
if (Object.prototype.hasOwnProperty.call(userConfig, 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.');
rConfig.menu = RCParser.DEFAULTS.menu;
}
rConfig.menu = rConfig.menu.filter((option: string | Menu, i:number) => {
i = i + 1;
if (typeof option === 'string') {
return true;
}
if (!option || option instanceof Array || typeof option !== 'object') {
console.log(`Menu option #${i} in rc file must be a string or an object`);
return false;
}
const { title, uri } = option;
if (!title || typeof title !== 'string') {
console.log(`Menu option #${i} title must be a string.`);
return false;
}
if (!uri || typeof uri !== 'string') {
console.log(`Menu option #${i} uri must be a string.`);
return false;
}
return true;
});
rConfig.menu = rConfig.menu.map((option: string | Menu) => {
if (typeof option === 'string') {
return {
title: option,
/* Replace all non alpha- and numeric-symbols with '-' */
uri: '/' + option.toLowerCase().replace(/[ -/:-@[-`{-~]+/, '-'),
};
}
return option;
});
return rConfig;
}
}

View file

@ -0,0 +1,85 @@
interface TransTable {
[key: string]: string;
}
const translationTable: TransTable = {
а: '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
*/
/**
* @param {string} string - input text to be translated
* @returns {string} text - translated text
*/
export default function translateString(string: string): string {
return string.replace(/[А-яёЁ]/g, (char) => translationTable[char] || char);
}

48
src/backend/utils/twig.ts Normal file
View file

@ -0,0 +1,48 @@
/**
* Twig extensions
*/
import twig from 'twig';
import fs from 'fs';
import urlify from './urlify';
export default (function () {
'use strict';
/**
* Function for include svg on page
*
* @example svg('path/from/root/dir')
* @param {string} filename - name of icon
* @returns {string} - svg code
*/
twig.extendFunction('svg', function (filename: string) {
return fs.readFileSync(`./src/frontend/svg/${filename}.svg`, 'utf-8');
});
/**
* Convert text to URL-like string
* Example: "What is <mark>clean data</mark>" -> "what-is-clean-data"
*
* @param {string} string - source string with HTML
* @returns {string} alias-like string
*/
twig.extendFilter('urlify', function (string: string) {
return urlify(string);
});
/**
* Parse link as URL object
*
* @param {string} linkUrl - link to be processed
* @returns {string} url url data
*/
twig.extendFunction('parseLink', function (linkUrl: string): string {
try {
return new URL(linkUrl).toString();
} catch (e) {
console.log(e);
return '';
}
});
}());

View file

@ -0,0 +1,33 @@
import translateString from './translation';
/**
* Convert text to URL-like string
* Example: "What is <mark>clean data</mark>" -> "what-is-clean-data"
*
* @param {string} string - source string with HTML
* @returns {string} alias-like string
*/
export default function urlify(string: string): string {
// strip tags
string = string.replace(/(<([^>]+)>)/ig, '');
// remove nbsp
string = string.replace(/&nbsp;/g, ' ');
// remove all symbols except chars
string = string.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ');
// remove whitespaces
string = string.replace(/ +/g, ' ').trim();
// lowercase
string = string.toLowerCase();
// join words with hyphens
string = string.split(' ').join('-');
// translate
string = translateString(string);
return string;
}