-
Notifications
You must be signed in to change notification settings - Fork 0
/
rm.hbs
404 lines (333 loc) · 14.7 KB
/
rm.hbs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
[![view on npm](https://img.shields.io/npm/v/@uttori/wiki.svg)](https://www.npmjs.com/package/@uttori/wiki)
[![npm module downloads](https://img.shields.io/npm/dt/@uttori/wiki.svg)](https://www.npmjs.com/package/@uttori/wiki)
[![Build Status](https://travis-ci.com/uttori/uttori-wiki.svg?branch=master)](https://travis-ci.com/uttori/uttori-wiki)
[![Coverage Status](https://coveralls.io/repos/github/uttori/uttori-wiki/badge.svg?branch=master)](https://coveralls.io/github/uttori/uttori-wiki?branch=master)
# Uttori Wiki
UttoriWiki is a fast, simple, wiki / knowledge base built around Express.js using the [Uttori](https://github.com/uttori) set of components allowing single chunks of functionality be changed or swapped out to fit specific needs.
Why yet another knowledge management / note taking app? I wanted to have something that functioned as a Wiki or Blog or similar small app that I could reuse components for and keep extensible.
Because of that, UttoriWiki is plugin based. Search and Storage engines are fully configurable. The format of the data is also up to you: Markdown, Wikitext, Creole, AsciiDoc, Textile, reStructuredText, BBCode, Pendown, etc.
Nothing is prescribed. Don't want to write in Markdown? You don't need to! Don't want to store files on disk? Choose a database storage engine! Already running a bunch of external dependencies and want to plug into those? You can _most likely_ do it!
Rendering happens in a pipeline making it easy to render to Markdown, then filter words out and replace text with emojis.
If you want to test it out, check out [the demo repo](https://github.com/uttori/uttori-wiki-demo-site) to get up and going in a minutes.
## Configuration
Please see `src/config.js` or [the config doc](https://github.com/uttori/uttori-wiki/blob/master/docs/config.md) for all options. Below is an example configuration using some plugins:
- [@uttori/storage-provider-json-file](https://github.com/uttori/uttori-storage-provider-json-file)
- [@uttori/search-provider-lunr](https://github.com/uttori/uttori-search-provider-lunr)
- [@uttori/plugin-renderer-replacer](https://github.com/uttori/uttori-plugin-renderer-replacer)
- [@uttori/plugin-renderer-markdown-it](https://github.com/uttori/uttori-plugin-renderer-markdown-it)
- [@uttori/plugin-upload-multer](https://github.com/uttori/uttori-plugin-upload-multer)
- [@uttori/plugin-generator-sitemap](https://github.com/uttori/uttori-plugin-generator-sitemap)
- [@uttori/plugin-analytics-json-file](https://github.com/uttori/uttori-plugin-analytics-json-file)
```javascript
import { Plugin: StorageProvider } from '@uttori/storage-provider-json-file';
import { Plugin: SearchProvider } from '@uttori/search-provider-lunr';
import AnalyticsPlugin from '@uttori/plugin-analytics-json-file';
import MarkdownItRenderer from '@uttori/plugin-renderer-markdown-it';
import ReplacerRenderer from '@uttori/plugin-renderer-replacer';
import MulterUpload from '@uttori/plugin-upload-multer';
import SitemapGenerator from '@uttori/plugin-generator-sitemap';
import { AddQueryOutputToViewModel } from '@uttori/wiki';
const config = {
homePage: 'home-page',
ignoreSlugs: ['home-page'],
excerptLength: 400,
publicUrl: 'http://127.0.0.1:8000/wiki',
themePath: path.join(__dirname, 'theme'),
publicPath: path.join(__dirname, 'public'),
useDeleteKey: false,
deleteKey: process.env.DELETE_KEY || '',
useEditKey: false,
editKey: process.env.EDIT_KEY || '',
publicHistory: true,
allowedDocumentKeys: [],
// Plugins
plugins: [
StorageProvider,
SearchProvider,
AnalyticsPlugin,
MarkdownItRenderer,
ReplacerRenderer,
MulterUpload,
SitemapGenerator,
],
// Use the JSON to Disk Storage Provider
[StorageProvider.configKey]: {
// Path in which to store content (markdown files, etc.)
contentDirectory: `${__dirname}/content`,
// Path in which to store content history (markdown files, etc.)
historyDirectory: `${__dirname}/content/history`,
// File Extension
extension: 'json',
},
// Use the Lunr Search Provider
[SearchProvider.configKey]: {
// Optional Lunr locale
lunr_locales: [],
// Ignore Slugs
ignoreSlugs: ['home-page'],
},
// Plugin: Analytics with JSON Files
[AnalyticsPlugin.configKey]: {
events: {
getPopularDocuments: ['popular-documents'],
updateDocument: ['document-save', 'document-delete'],
validateConfig: ['validate-config'],
},
// Directory files will be uploaded to.
directory: `${__dirname}/data`,
// Name of the JSON file.
name: 'visits',
// File extension to use for the JSON file.
extension: 'json',
},
// Plugin: Markdown rendering with MarkdownIt
[MarkdownItRenderer.configKey]: {
events: {
renderContent: ['render-content'],
renderCollection: ['render-search-results'],
validateConfig: ['validate-config'],
},
// Uttori Specific Configuration
uttori: {
// Prefix for relative URLs, useful when the Express app is not at root.
baseUrl: '',
// Safe List, if a domain is not in this list, it is set to 'external nofollow noreferrer'.
allowedExternalDomains: [
'my-site.org',
],
// Open external domains in a new window.
openNewWindow: true,
// Table of Contents
toc: {
// The opening DOM tag for the TOC container.
openingTag: '<nav class="table-of-contents">',
// The closing DOM tag for the TOC container.
closingTag: '</nav>',
// Slugify options for convering content to anchor links.
slugify: {
lower: true,
},
},
},
},
// Plugin: Replace text
[ReplacerRenderer.configKey]: {
events: {
renderContent: ['render-content'],
renderCollection: ['render-search-results'],
validateConfig: ['validate-config'],
},
// Rules for text replace
rules: [
{
test: /bunny|rabbit/gm,
output: '🐰',
},
],
},
// Plugin: Multer Upload
[MulterUpload.configKey]: {
events: {
bindRoutes: ['bind-routes'],
validateConfig: ['validate-config'],
},
// Directory files will be uploaded to
directory: `${__dirname}/uploads`,
// URL to POST files to
route: '/upload',
// URL to GET uploads from
publicRoute: '/uploads',
},
// Plugin: Sitemap Generator
[SitemapGenerator.configKey]: {
events: {
callback: ['document-save', 'document-delete'],
validateConfig: ['validate-config'],
},
// Sitemap URL (ie https://wiki.domain.tld)
base_url: 'https://wiki.domain.tld',
// Location where the XML sitemap will be written to.
directory: `${__dirname}/themes/default/public`,
urls: [
{
url: '/',
lastmod: new Date().toISOString(),
priority: '1.00',
},
{
url: '/tags',
lastmod: new Date().toISOString(),
priority: '0.90',
},
{
url: '/new',
lastmod: new Date().toISOString(),
priority: '0.70',
},
],
},
// Plugin: View Model Related Documents
[AddQueryOutputToViewModel.configKey]: {
events: {
callback: [
'view-model-home',
'view-model-edit',
'view-model-new',
'view-model-search',
'view-model-tag',
'view-model-tag-index',
'view-model-detail',
],
},
queries: {
'view-model-home' : [
{
key: 'tags',
query: `SELECT tags FROM documents WHERE slug NOT_IN ("${ignoreSlugs.join('", "')}") ORDER BY id ASC LIMIT -1`,
format: (tags) => [...new Set(tags.flatMap((t) => t.tags))].filter(Boolean).sort((a, b) => a.localeCompare(b)),
fallback: [],
},
{
key: 'documents',
query: `SELECT * FROM documents WHERE slug NOT_IN ("${ignoreSlugs.join('", "')}") ORDER BY id ASC LIMIT -1`,
fallback: [],
},
{
key: 'popularDocuments',
fallback: [],
format: (results) => results.map((result) => result.slug),
queryFunction: async (target, context) => {
const ignoreSlugs = ['home-page'];
const [popular] = await context.hooks.fetch('popular-documents', { limit: 5 }, context);
const slugs = `"${popular.map(({ slug }) => slug).join('", "')}"`;
const query = `SELECT 'slug', 'title' FROM documents WHERE slug NOT_IN (${ignoreSlugs}) AND slug IN (${slugs}) ORDER BY updateDate DESC LIMIT 5`;
const [results] = await context.hooks.fetch('storage-query', query);
return [results];
},
}
],
},
},
// Middleware Configuration in the form of ['function', 'param1', 'param2', ...]
middleware: [
['disable', 'x-powered-by'],
['enable', 'view cache'],
['set', 'views', path.join(`${__dirname}/themes/`, 'default', 'templates')],
// EJS Specific Setup
['use', layouts],
['set', 'layout extractScripts', true],
['set', 'layout extractStyles', true],
// If you use the `.ejs` extension use the below:
// ['set', 'view engine', 'ejs'],
// I prefer using `.html` templates:
['set', 'view engine', 'html'],
['engine', 'html', ejs.renderFile],
],
redirects: [
{
route: '/:year/:slug',
target: '/:slug',
status: 301,
appendQueryString: true,
},
],
// Override route handlers
homeRoute: (request, response, next) => { ... },
tagIndexRoute: (request, response, next) => { ... },
tagRoute: (request, response, next) => { ... },
searchRoute: (request, response, next) => { ... },
editRoute: (request, response, next) => { ... },
deleteRoute: (request, response, next) => { ... },
saveRoute: (request, response, next) => { ... },
saveNewRoute: (request, response, next) => { ... },
newRoute: (request, response, next) => { ... },
detailRoute: (request, response, next) => { ... },
previewRoute: (request, response, next) => { ... },
historyIndexRoute: (request, response, next) => { ... },
historyDetailRoute: (request, response, next) => { ... },
historyRestoreRoute: (request, response, next) => { ... },
notFoundRoute: (request, response, next) => { ... },
saveValidRoute: (request, response, next) => { ... },
// Custom per route middleware, in the order they should be used
routeMiddleware: {
home: [],
tagIndex: [],
tag: [],
search: [],
notFound: [],
create: [],
saveNew: [],
preview: [],
edit: [],
delete: [],
historyIndex: [],
historyDetail: [],
historyRestore: [],
save: [],
detail: [],
},
};
export default config;
```
Use in an example Express.js app:
```javascript
// Server
import express from 'express';
// Reference the Uttori Wiki middleware
import { wiki as middleware } from '@uttori/wiki';
// Pull in our custom config, example above
import config from './config.js';
// Initilize Your app
const app = express();
// Setup the app
app.set('port', process.env.PORT || 8000);
app.set('ip', process.env.IP || '127.0.0.1');
// Setup Express
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
// Setup the wiki, could also mount under a sub directory path with other applications
app.use('/', middleware(config));
// Listen for connections
app.listen(app.get('port'), app.get('ip'), () => {
console.log('✔ listening at %s:%d', app.get('ip'), app.get('port'));
});
```
## Events
The following events are avaliable to hook into through plugins and are used in the methods below:
| Name | Type | Returns | Description |
|------------------------------|------------|---------------------------|-------------|
| `bind-routes` | `dispatch` | | Called after the default routes are bound to the server. |
| `document-delete` | `dispatch` | | Called when a document is about to be deleted. |
| `document-save` | `filter` | Uttori Document | Called when a document is about to be saved. |
| `render-content` | `filter` | HTML Content | Called when content is being prepared to be shown. |
| `render-search-results` | `filter` | Array of Uttori Documents | Called when search results have been collected and is being prepared to be shown. |
| `validate-config` | `dispatch` | | Called after initial configuration validation. |
| `validate-invalid` | `dispatch` | | Called when a document is found invalid (spam?). |
| `validate-valid` | `dispatch` | | Called when a document is found to be valid. |
| `validate-save` | `validate` | Boolean | Called before saving a document to validate the document. |
| `view-model-detail` | `filter` | View Model | Called when rendering the detail page just before being shown. |
| `view-model-edit` | `filter` | View Model | Called when rendering the edit page just before being shown. |
| `view-model-error-404` | `filter` | View Model | Called when rendering a 404 Not Found error page just before being shown. |
| `view-model-history-detail` | `filter` | View Model | Called when rendering a history detail page just before being shown. |
| `view-model-history-index` | `filter` | View Model | Called when rendering a history index page just before being shown. |
| `view-model-history-restore` | `filter` | View Model | Called when rendering a history restore page just before being shown. |
| `view-model-home` | `filter` | View Model | Called when rendering the home page just before being shown. |
| `view-model-metadata` | `filter` | View Model | Called after the initial view model metadata is setup. |
| `view-model-new` | `filter` | View Model | Called when rendering the new document page just before being shown. |
| `view-model-search` | `filter` | View Model | Called when rendering a search result page just before being shown. |
| `view-model-tag-index` | `filter` | View Model | Called when rendering the tag index page just before being shown. |
| `view-model-tag` | `filter` | View Model | Called when rendering a tag detail page just before being shown. |
* * *
## API Reference
{{>main}}
* * *
## Tests
To run the test suite, first install the dependencies, then run `npm test`:
```bash
npm install
DEBUG=Uttori* npm test
```
## Contributors
- [Matthew Callis](https://github.com/MatthewCallis) - author of UttoriWiki
## License
[MIT](LICENSE)