@miso.ai/doggoganger 0.9.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +33 -0
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/bin/version.js +46 -0
- package/cli/index.js +52 -0
- package/cli/server.js +4 -0
- package/package.json +39 -0
- package/src/api/ask.js +147 -0
- package/src/api/handlers.js +11 -0
- package/src/api/index.js +20 -0
- package/src/api/interactions.js +9 -0
- package/src/api/recommendation.js +9 -0
- package/src/api/search.js +8 -0
- package/src/api/utils.js +6 -0
- package/src/data/articles.js +24 -0
- package/src/data/fields.js +49 -0
- package/src/data/index.js +5 -0
- package/src/data/lorem.js +156 -0
- package/src/data/markdown/index.js +234 -0
- package/src/data/products.js +30 -0
- package/src/data/utils.js +31 -0
- package/src/data/words.yaml +178 -0
- package/src/index.js +28 -0
- package/src/utils.js +26 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: NPM publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish-npm:
|
|
9
|
+
environment: production
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Get version
|
|
13
|
+
id: version
|
|
14
|
+
run: |
|
|
15
|
+
VERSION=${{ github.event.release.tag_name }}
|
|
16
|
+
echo "::set-output name=version::$(echo $VERSION | sed -e 's/v//gI')"
|
|
17
|
+
- uses: actions/checkout@v2
|
|
18
|
+
- uses: actions/setup-node@v2
|
|
19
|
+
with:
|
|
20
|
+
node-version: 16
|
|
21
|
+
registry-url: https://registry.npmjs.org/
|
|
22
|
+
- run: npm install
|
|
23
|
+
- run: git config --global user.name "${{ github.actor }}"
|
|
24
|
+
- run: git config --global user.email "github-action-${{ github.actor }}@users.noreply.github.com"
|
|
25
|
+
- run: npm run version ${{ steps.version.outputs.version }}
|
|
26
|
+
- run: npm publish
|
|
27
|
+
if: "!github.event.release.prerelease"
|
|
28
|
+
env:
|
|
29
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
30
|
+
- run: npm publish --tag beta
|
|
31
|
+
if: "github.event.release.prerelease"
|
|
32
|
+
env:
|
|
33
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Miso Technologies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/bin/version.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
const VERSION_REGEXP = /^\d+\.\d+\.\d+(?:-beta\.\d+)?$/;
|
|
8
|
+
const version = process.argv[2];
|
|
9
|
+
|
|
10
|
+
if (!version) {
|
|
11
|
+
console.log(`Usage: npm run version [version]`);
|
|
12
|
+
process.exit();
|
|
13
|
+
}
|
|
14
|
+
if (!VERSION_REGEXP.test(version)) {
|
|
15
|
+
console.error(`Illegal version: ${version}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const rootDir = join(__dirname, '..');
|
|
20
|
+
//const VERSION_FILE_NAME = 'src/version.js';
|
|
21
|
+
const PACKAGE_FILE_NAME = 'package.json';
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
function writeVersionFile(path, version) {
|
|
25
|
+
const filePath = join(rootDir, path, VERSION_FILE_NAME);
|
|
26
|
+
if (existsSync(filePath)) {
|
|
27
|
+
writeFileSync(filePath, `export default '${version}';`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
function readPackageFileSync(path) {
|
|
33
|
+
const file = join(path, PACKAGE_FILE_NAME);
|
|
34
|
+
return existsSync(file) ? JSON.parse(readFileSync(file)) : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writePackageFileSync(path, data) {
|
|
38
|
+
const file = join(path, PACKAGE_FILE_NAME);
|
|
39
|
+
if (!existsSync(file)) {
|
|
40
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
writeFileSync(join(path, PACKAGE_FILE_NAME), JSON.stringify(data, null, 2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { name, version: _version, ...project } = readPackageFileSync(rootDir);
|
|
46
|
+
writePackageFileSync(rootDir, { name, version, ...project });
|
package/cli/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, resolve, join } from 'path';
|
|
4
|
+
import nodemon from 'nodemon';
|
|
5
|
+
import yargs from 'yargs/yargs';
|
|
6
|
+
import { hideBin } from 'yargs/helpers';
|
|
7
|
+
import doggoganger from '../src/index.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const SRC_DIR = join(__dirname, '../src');
|
|
11
|
+
|
|
12
|
+
let { watch, ...argv } = yargs(hideBin(process.argv))
|
|
13
|
+
.option('port', {
|
|
14
|
+
alias: 'p',
|
|
15
|
+
describe: 'Port of mock API endpoint',
|
|
16
|
+
default: 9901,
|
|
17
|
+
type: 'number',
|
|
18
|
+
})
|
|
19
|
+
.option('watch', {
|
|
20
|
+
alias: 'w',
|
|
21
|
+
describe: 'Watch files for changes',
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
})
|
|
24
|
+
.option('answer-format', {
|
|
25
|
+
describe: 'Answer field format in answers API response',
|
|
26
|
+
type: 'string',
|
|
27
|
+
})
|
|
28
|
+
.option('serve', {
|
|
29
|
+
alias: 's',
|
|
30
|
+
describe: 'Serve static files as well',
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
})
|
|
33
|
+
.argv;
|
|
34
|
+
|
|
35
|
+
const { port, serve, ['answer-format']: answerFormat } = argv;
|
|
36
|
+
argv = { port, serve, answerFormat };
|
|
37
|
+
|
|
38
|
+
if (watch) {
|
|
39
|
+
const exec = `node ${resolve(__dirname, 'server.js')} ${JSON.stringify(JSON.stringify(argv))}`;
|
|
40
|
+
nodemon({
|
|
41
|
+
exec,
|
|
42
|
+
watch: argv.static ? [SRC_DIR, '.'] : SRC_DIR,
|
|
43
|
+
ignore: ['*/node_modules/*'],
|
|
44
|
+
//ext: 'js json yaml',
|
|
45
|
+
verbose: true,
|
|
46
|
+
}).on('log', ({ colour }) => {
|
|
47
|
+
console.log(colour);
|
|
48
|
+
}).on('quit', () => process.exit());
|
|
49
|
+
|
|
50
|
+
} else {
|
|
51
|
+
doggoganger(argv);
|
|
52
|
+
}
|
package/cli/server.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@miso.ai/doggoganger",
|
|
3
|
+
"version": "0.9.0-beta.10",
|
|
4
|
+
"description": "A dummy miso endpoint for demo and testing",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"doggoganger": "cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "nodemon src/index.js",
|
|
14
|
+
"version": "node ./bin/version.js"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/MisoAI/doggoganger.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/MisoAI/doggoganger/",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"contributors": [
|
|
23
|
+
"simonpai <simon.pai@askmiso.com>"
|
|
24
|
+
],
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/MisoAI/doggoganger/issues"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@koa/cors": "^4.0.0",
|
|
30
|
+
"@koa/router": "^12.0.0",
|
|
31
|
+
"js-yaml": "^4.1.0",
|
|
32
|
+
"koa": "^2.14.1",
|
|
33
|
+
"koa-body": "^6.0.1",
|
|
34
|
+
"koa-static": "^5.0.0",
|
|
35
|
+
"nodemon": "^2.0.15",
|
|
36
|
+
"uuid": "^8.3.2",
|
|
37
|
+
"yargs": "^17.5.1"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/api/ask.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import Router from '@koa/router';
|
|
2
|
+
import { v4 as uuid } from 'uuid';
|
|
3
|
+
import { lorem, md, articles, utils } from '../data/index.js';
|
|
4
|
+
import { parseBodyIfNecessary } from './utils.js';
|
|
5
|
+
|
|
6
|
+
const { randomInt } = utils;
|
|
7
|
+
|
|
8
|
+
const CPS = 100;
|
|
9
|
+
const ITEMS_LOADING_TIME = 3;
|
|
10
|
+
const STAGES = [
|
|
11
|
+
{
|
|
12
|
+
name: 'fetch',
|
|
13
|
+
duration: 1.5,
|
|
14
|
+
text: `Checking the question and fetching results... `,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'verify',
|
|
18
|
+
duration: 1.5,
|
|
19
|
+
text: `Verifying results... `,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'generate',
|
|
23
|
+
duration: 1.5,
|
|
24
|
+
text: `Generating answer... `,
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function formatDatetime(timestamp) {
|
|
29
|
+
const str = new Date(timestamp).toISOString();
|
|
30
|
+
return str.endsWith('Z') ? str.slice(0, -1) : str;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function answer(format) {
|
|
34
|
+
switch (format) {
|
|
35
|
+
case 'markdown':
|
|
36
|
+
return md.markdown({});
|
|
37
|
+
case 'plaintext':
|
|
38
|
+
default:
|
|
39
|
+
return lorem.lorem({
|
|
40
|
+
min: 50,
|
|
41
|
+
max: 100,
|
|
42
|
+
decorates: ['description'],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const answers = new Map();
|
|
48
|
+
|
|
49
|
+
class Answer {
|
|
50
|
+
|
|
51
|
+
constructor(question, previous_question_id, { answerFormat = 'markdown' } = {}) {
|
|
52
|
+
this.question_id = uuid();
|
|
53
|
+
this.question = question;
|
|
54
|
+
this.previous_answer_id = previous_question_id;
|
|
55
|
+
this.timestamp = Date.now();
|
|
56
|
+
this.datetime = formatDatetime(this.timestamp);
|
|
57
|
+
|
|
58
|
+
this.answer = answer(answerFormat);
|
|
59
|
+
this.relatedResources = [...articles({ rows: randomInt(6, 8) })];
|
|
60
|
+
this.sources = [...articles({ rows: randomInt(4, 6) })];
|
|
61
|
+
|
|
62
|
+
this.previous_question_id = previous_question_id && answers.get(previous_question_id) || undefined;
|
|
63
|
+
answers.set(this.question_id, this);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get id() {
|
|
67
|
+
return this.question_id;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get() {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const elapsed = (now - this.timestamp) / 1000;
|
|
73
|
+
const [answer_stage, answer, finished] = this._answer(elapsed);
|
|
74
|
+
const sources = this._sources(elapsed);
|
|
75
|
+
const related_resources = this._relatedResources(elapsed);
|
|
76
|
+
const { question_id, question, datetime, previous_question_id } = this;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
affiliation: undefined,
|
|
80
|
+
answer,
|
|
81
|
+
answer_stage,
|
|
82
|
+
datetime,
|
|
83
|
+
finished,
|
|
84
|
+
previous_question_id,
|
|
85
|
+
question,
|
|
86
|
+
question_id,
|
|
87
|
+
related_resources,
|
|
88
|
+
sources,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_answer(elapsed) {
|
|
93
|
+
for (const stage of STAGES) {
|
|
94
|
+
elapsed -= stage.duration;
|
|
95
|
+
if (elapsed < 0) {
|
|
96
|
+
return [stage.name, stage.text, false];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const length = Math.floor(elapsed * CPS);
|
|
100
|
+
const finished = length >= this.answer.length;
|
|
101
|
+
const text = finished ? this.answer : this.answer.slice(0, length);
|
|
102
|
+
return ['result', text, finished];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_sources(elapsed) {
|
|
106
|
+
const { sources } = this;
|
|
107
|
+
const { length } = sources;
|
|
108
|
+
const loaded = Math.floor(length * elapsed / ITEMS_LOADING_TIME);
|
|
109
|
+
return sources.slice(0, loaded);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_relatedResources(elapsed) {
|
|
113
|
+
const { relatedResources } = this;
|
|
114
|
+
const { length } = relatedResources;
|
|
115
|
+
const loaded = Math.floor(length * elapsed / ITEMS_LOADING_TIME);
|
|
116
|
+
return relatedResources.slice(0, loaded);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default function({ answerFormat }) {
|
|
122
|
+
const options = Object.freeze({ answerFormat });
|
|
123
|
+
const router = new Router();
|
|
124
|
+
|
|
125
|
+
router.post('/questions', (ctx) => {
|
|
126
|
+
const { question, previous_answer_id } = parseBodyIfNecessary(ctx.request.body);
|
|
127
|
+
const answerFormat = ctx.get('x-answer-format') || options.answerFormat;
|
|
128
|
+
const answer = new Answer(question, previous_answer_id, { answerFormat });
|
|
129
|
+
const data = {
|
|
130
|
+
question_id: answer.id,
|
|
131
|
+
};
|
|
132
|
+
ctx.body = JSON.stringify({ data });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
router.get('/questions/:id/answer', (ctx) => {
|
|
136
|
+
const { id } = ctx.params;
|
|
137
|
+
const answer = answers.get(id);
|
|
138
|
+
if (!answer) {
|
|
139
|
+
ctx.status = 404;
|
|
140
|
+
} else {
|
|
141
|
+
const data = answer.get();
|
|
142
|
+
ctx.body = JSON.stringify({ data });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return router;
|
|
147
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { products as _products } from '../data/index.js';
|
|
2
|
+
import { parseBodyIfNecessary } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export function products(ctx) {
|
|
5
|
+
const { rows = 5 } = parseBodyIfNecessary(ctx.request.body);
|
|
6
|
+
ctx.body = {
|
|
7
|
+
data: {
|
|
8
|
+
products: [..._products({ rows })],
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
package/src/api/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Router from '@koa/router';
|
|
2
|
+
import ask from './ask.js';
|
|
3
|
+
import recommendation from './recommendation.js';
|
|
4
|
+
import search from './search.js';
|
|
5
|
+
import interactions from './interactions.js';
|
|
6
|
+
|
|
7
|
+
function use(router, path, middleware) {
|
|
8
|
+
router.use(path, middleware.routes(), middleware.allowedMethods());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function(options) {
|
|
12
|
+
const router = new Router();
|
|
13
|
+
|
|
14
|
+
use(router, '/ask', ask(options));
|
|
15
|
+
use(router, '/recommendation', recommendation);
|
|
16
|
+
use(router, '/search', search);
|
|
17
|
+
use(router, '/interactions', interactions);
|
|
18
|
+
|
|
19
|
+
return router;
|
|
20
|
+
};
|
package/src/api/utils.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as u from './utils.js';
|
|
2
|
+
import * as fields from './fields.js';
|
|
3
|
+
|
|
4
|
+
export function *articles({ rows, ...options } = {}) {
|
|
5
|
+
for (let i = 0; i < rows; i++) {
|
|
6
|
+
yield article({ ...options, index: i });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function article({} = {}) {
|
|
11
|
+
const id = u.id();
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
product_id: id,
|
|
15
|
+
authors: fields.authors(),
|
|
16
|
+
categories: [],
|
|
17
|
+
tags: fields.tags(),
|
|
18
|
+
title: fields.title({ size: [4, 10] }),
|
|
19
|
+
snippet: fields.description({ size: [20, 40] }),
|
|
20
|
+
//html,
|
|
21
|
+
cover_image: fields.image(),
|
|
22
|
+
url: `/products/${id}`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as lorem from './lorem.js';
|
|
2
|
+
import { imageUrl } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export function image({ size = 300 } = {}) {
|
|
5
|
+
return imageUrl(size);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function authors({ size = [1, 3] } = {}) {
|
|
9
|
+
return lorem.lorem({
|
|
10
|
+
size,
|
|
11
|
+
decorates: ['title'],
|
|
12
|
+
output: 'array',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function tags({ size = [1, 4] } = {}) {
|
|
17
|
+
return lorem.lorem({
|
|
18
|
+
size,
|
|
19
|
+
output: 'array',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// TODO: categories
|
|
24
|
+
|
|
25
|
+
export function title({ size = [2, 6] } = {}) {
|
|
26
|
+
return lorem.lorem({
|
|
27
|
+
size,
|
|
28
|
+
decorates: ['title'],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function description({ size = [10, 20] } = {}) {
|
|
33
|
+
return lorem.lorem({
|
|
34
|
+
size,
|
|
35
|
+
decorates: ['description'],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function availability() {
|
|
40
|
+
return Math.random() > 0.3 ? 'IN_STOCK' : 'OUT_OF_STOCK';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function price() {
|
|
44
|
+
return Math.floor(Math.random() * 10000) / 100;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function rating() {
|
|
48
|
+
return Math.floor(Math.random() * 500) / 100 + 1;
|
|
49
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, resolve } from 'path';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import { randomInt } from './utils.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const DEFAULT_WORDS = yaml.load(readFileSync(resolve(__dirname, './words.yaml'), 'utf8'));
|
|
9
|
+
|
|
10
|
+
export function lorem({ decorates = [], output = 'string', size, min, max, ...options } = {}) {
|
|
11
|
+
let iterator = limit(size || [min, max])(base(options));
|
|
12
|
+
for (const decorate of decorates) {
|
|
13
|
+
iterator = lookup(decorate)(iterator);
|
|
14
|
+
}
|
|
15
|
+
return lookup(output)(iterator);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const FNS = {
|
|
19
|
+
string,
|
|
20
|
+
array,
|
|
21
|
+
title,
|
|
22
|
+
description,
|
|
23
|
+
multiline,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function lookup(fn) {
|
|
27
|
+
return typeof fn === 'string' ? FNS[fn]() : fn;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// base //
|
|
31
|
+
export function *base({ words = DEFAULT_WORDS, fixedStarts = 0 } = {}) {
|
|
32
|
+
const wordsLength = words.length;
|
|
33
|
+
for (let i = 0; ; i++) {
|
|
34
|
+
yield words[i < fixedStarts ? i : Math.floor(Math.random() * wordsLength)];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// output //
|
|
39
|
+
export function string({ separator = ' ' } = {}) {
|
|
40
|
+
return iterator => [...iterator].join(separator);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function array() {
|
|
44
|
+
return iterator => [...iterator];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function multiline({
|
|
48
|
+
wordsPerLine = {
|
|
49
|
+
avg: 10,
|
|
50
|
+
std: 3,
|
|
51
|
+
min: 1,
|
|
52
|
+
},
|
|
53
|
+
} = {}) {
|
|
54
|
+
return iterator => {
|
|
55
|
+
let slen = gaussMS(wordsPerLine);
|
|
56
|
+
let result = '';
|
|
57
|
+
for (let word of iterator) {
|
|
58
|
+
if (result) {
|
|
59
|
+
if (slen-- === 0) {
|
|
60
|
+
result += '\n';
|
|
61
|
+
slen = gaussMS(wordsPerLine);
|
|
62
|
+
} else {
|
|
63
|
+
result += ' ';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
result += word;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// decorators //
|
|
73
|
+
export function limit(size = [5, 10]) {
|
|
74
|
+
const n = typeof size === 'number' ? size : randomInt(...size);
|
|
75
|
+
return function *(iterator) {
|
|
76
|
+
let i = 0;
|
|
77
|
+
for (let word of iterator) {
|
|
78
|
+
if (i++ >= n) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
yield word;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function title({} = {}) {
|
|
87
|
+
return function *(iterator) {
|
|
88
|
+
for (let word of iterator) {
|
|
89
|
+
yield capitalize(word);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function description({
|
|
95
|
+
wordsPerSentence = {
|
|
96
|
+
avg: 24,
|
|
97
|
+
std: 5,
|
|
98
|
+
min: 1,
|
|
99
|
+
},
|
|
100
|
+
} = {}) {
|
|
101
|
+
return function *(iterator) {
|
|
102
|
+
let word;
|
|
103
|
+
let slen = 0;
|
|
104
|
+
for (let _word of iterator) {
|
|
105
|
+
if (word) {
|
|
106
|
+
yield word;
|
|
107
|
+
}
|
|
108
|
+
word = _word;
|
|
109
|
+
if (slen === 0) {
|
|
110
|
+
word = capitalize(word);
|
|
111
|
+
slen = gaussMS(wordsPerSentence);
|
|
112
|
+
}
|
|
113
|
+
if (--slen === 0) {
|
|
114
|
+
word += '.';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (word) {
|
|
118
|
+
if (!word.endsWith('.')) {
|
|
119
|
+
word += '.';
|
|
120
|
+
}
|
|
121
|
+
yield word;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// helpers //
|
|
127
|
+
function capitalize(word) {
|
|
128
|
+
return word[0].toUpperCase() + word.substring(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// TODO: have a random variable expression
|
|
132
|
+
function gaussMS(args) {
|
|
133
|
+
if (typeof args === 'number') {
|
|
134
|
+
return Math.round(avg);
|
|
135
|
+
}
|
|
136
|
+
let { avg, std, min, max } = args;
|
|
137
|
+
if (std === undefined) {
|
|
138
|
+
std = avg / 4;
|
|
139
|
+
}
|
|
140
|
+
let n = gaussRandom() * std + avg;
|
|
141
|
+
if (min !== undefined) {
|
|
142
|
+
n = Math.max(min, n);
|
|
143
|
+
}
|
|
144
|
+
if (max !== undefined) {
|
|
145
|
+
n = Math.min(max, n);
|
|
146
|
+
}
|
|
147
|
+
return Math.round(n);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function gaussRandom() {
|
|
151
|
+
return uniformRandom() + uniformRandom() + uniformRandom();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function uniformRandom() {
|
|
155
|
+
return Math.random() * 2 - 1;
|
|
156
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { randomInt, imageUrl, shuffle } from '../utils.js';
|
|
2
|
+
import * as lorem from '../lorem.js';
|
|
3
|
+
|
|
4
|
+
// TODO: wild mode that generates edge cases
|
|
5
|
+
|
|
6
|
+
export function markdown({ features, blocks = [8, 12] } = {}) {
|
|
7
|
+
// TODO: block features
|
|
8
|
+
return [
|
|
9
|
+
atxHeading({ features }),
|
|
10
|
+
paragraph({ features }),
|
|
11
|
+
fencedCodeBlock({ features }),
|
|
12
|
+
paragraph({ features }),
|
|
13
|
+
table({ features }),
|
|
14
|
+
image(),
|
|
15
|
+
paragraph({ features }),
|
|
16
|
+
hr(),
|
|
17
|
+
atxHeading({ features }),
|
|
18
|
+
paragraph({ features }),
|
|
19
|
+
list({ features }),
|
|
20
|
+
paragraph({ features }),
|
|
21
|
+
].join('\n\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// leaf blocks //
|
|
25
|
+
export function hr() {
|
|
26
|
+
// wild mode: while spaces at the beginning (< 4), in between, in the end
|
|
27
|
+
// wild mode: no line break for '*'
|
|
28
|
+
return '*-_'.charAt(randomInt(0, 2)).repeat(randomInt(3, 6));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function atxHeading({ features, level = [1, 6], size = [1, 8], content }) {
|
|
32
|
+
const words = content || lorem.lorem({ size });
|
|
33
|
+
return `${'#'.repeat(randomInt(...level))} ${words}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function setextHeading({ features, level = [1, 2], size = [1, 8], content }) {
|
|
37
|
+
const words = content || lorem.lorem({ size });
|
|
38
|
+
return `${words}\n${'=-'.charAt(randomInt(...level) - 1).repeat(3)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function linkReferenceDefinition({ label, destination, title }) {
|
|
42
|
+
return `[${label}]: ${destination}${title !== undefined ? ` ${title}` : ''}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function indentedCodeBlock({ lang, content, size }) {
|
|
46
|
+
content = content || codeContent({ lang, size });
|
|
47
|
+
return indent(4, content);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function fencedCodeBlock({ lang, content, size, fenceChar = '`' }) {
|
|
51
|
+
if (fenceChar === 'random') {
|
|
52
|
+
fenceChar = '`~'.charAt(randomInt(0, 1));
|
|
53
|
+
}
|
|
54
|
+
content = content || codeContent({ lang, size });
|
|
55
|
+
// TODO: escape fenceChar in content
|
|
56
|
+
return `${fenceChar.repeat(3)}${lang || ''}\n${content}\n${fenceChar.repeat(3)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function paragraph({ features, size = [20, 50] }) {
|
|
60
|
+
return lorem.lorem({ size, decorates: ['description', decorate({ features })] });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function table({ features, columns = [2, 4], rows = [2, 8] }) {
|
|
64
|
+
columns = randomInt(...columns);
|
|
65
|
+
rows = randomInt(...rows);
|
|
66
|
+
const defs = [...multiply({ size: 1 }, columns - 1), { size: [3, 8] }];
|
|
67
|
+
const header = lorem.lorem({ size: columns, output: 'array' });
|
|
68
|
+
const delimiter = defs.map(() => '---');
|
|
69
|
+
const body = [ header, delimiter ];
|
|
70
|
+
for (let i = 0; i < rows - 1; i++) {
|
|
71
|
+
body.push(defs.map(({ size }) => lorem.lorem({ size })));
|
|
72
|
+
}
|
|
73
|
+
return body.map(tableRow).join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function image({ url, imageSize = [400, 250], ...options } = {}) {
|
|
77
|
+
url = url || imageUrl(imageSize);
|
|
78
|
+
return ``;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// container blocks //
|
|
82
|
+
export function blockquote({ features, size = [3, 5] }) {
|
|
83
|
+
// TODO
|
|
84
|
+
return _blockquote(lorem.lorem({ size }));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const LIST_ITEM_TYPES = ['ordered', 'bullet', 'task'];
|
|
88
|
+
|
|
89
|
+
export function list({ features, type = 'random', count = [1, 8], size = [5, 15] }) {
|
|
90
|
+
count = typeof count === 'number' ? count : randomInt(...count);
|
|
91
|
+
const t = type === 'random' ? LIST_ITEM_TYPES[Math.floor(3 * Math.random())] : type;
|
|
92
|
+
const items = [];
|
|
93
|
+
while (count > 0) {
|
|
94
|
+
const c = randomInt(1, count);
|
|
95
|
+
let content = paragraph({ features, size });
|
|
96
|
+
if (c > 1) {
|
|
97
|
+
content += `\n${list({ features, type, count: c - 1, size })}`;
|
|
98
|
+
}
|
|
99
|
+
items.push(listItem(t, content));
|
|
100
|
+
count -= c;
|
|
101
|
+
}
|
|
102
|
+
return items.join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// inline //
|
|
106
|
+
export function codeSpan(options) {
|
|
107
|
+
return `\`${_content(options)}\``;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function emphasis({ level = [1, 3], options }) {
|
|
111
|
+
level = typeof level === 'number' ? level : randomInt(...level);
|
|
112
|
+
const str = '_*'.charAt(randomInt(0, 1)).repeat(level);
|
|
113
|
+
return `${str}${_content(options)}${str}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function link({ url = 'https://miso.ai', ...options } = {}) {
|
|
117
|
+
return `[${_content(options)}](${url})`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// TODO: ref link
|
|
121
|
+
// TODO: autolink
|
|
122
|
+
// TODO: hard line break
|
|
123
|
+
|
|
124
|
+
const INLINE_FEATURES = {
|
|
125
|
+
'code-span': () => ['`', '`'],
|
|
126
|
+
'emphasis-1': () => multiply(_emphasisAdfix(1), 2),
|
|
127
|
+
'emphasis-2': () => multiply(_emphasisAdfix(2), 2),
|
|
128
|
+
'emphasis-3': () => multiply(_emphasisAdfix(3), 2),
|
|
129
|
+
'strikethrough': () => ['~', '~'],
|
|
130
|
+
'link': ({ url = 'https://miso.ai' } = {}) => [`[`, `](${url})`],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
function _emphasisAdfix(level = [1, 3]) {
|
|
134
|
+
level = typeof level === 'number' ? level : randomInt(...level);
|
|
135
|
+
return '_*'.charAt(randomInt(0, 1)).repeat(level);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const INLINE_FEATURE_LIST = Object.keys(INLINE_FEATURES);
|
|
139
|
+
const INLINE_FEATURE_SET = new Set(INLINE_FEATURE_LIST);
|
|
140
|
+
|
|
141
|
+
// decorator //
|
|
142
|
+
export function decorate({ features = INLINE_FEATURE_LIST, size = [1, 3], rest = [0, 8] } = {}) {
|
|
143
|
+
features = features.filter(f => INLINE_FEATURE_SET.has(f));
|
|
144
|
+
|
|
145
|
+
const unused = shuffle([...features]);
|
|
146
|
+
let unusedCursor = unused.length - 1;
|
|
147
|
+
|
|
148
|
+
const rollRest = () => typeof rest === 'number' ? rest : randomInt(...rest);
|
|
149
|
+
const rollFeatureSize = () => typeof size === 'number' ? size : randomInt(...size);
|
|
150
|
+
const rollFeatureType = () => unusedCursor >= 0 ? unused[unusedCursor--] : features[randomInt(0, features.length - 1)];
|
|
151
|
+
|
|
152
|
+
return function *(iterator) {
|
|
153
|
+
let count = rollRest();
|
|
154
|
+
let suffix;
|
|
155
|
+
let lastWord;
|
|
156
|
+
for (const word of iterator) {
|
|
157
|
+
if (lastWord) {
|
|
158
|
+
yield lastWord;
|
|
159
|
+
}
|
|
160
|
+
if (count === 0) {
|
|
161
|
+
if (suffix) {
|
|
162
|
+
lastWord = `${word}${suffix}`;
|
|
163
|
+
suffix = undefined;
|
|
164
|
+
count = rollRest();
|
|
165
|
+
} else {
|
|
166
|
+
const [prefix, s] = INLINE_FEATURES[rollFeatureType()]();
|
|
167
|
+
lastWord = `${prefix}${word}`;
|
|
168
|
+
suffix = s;
|
|
169
|
+
count = rollFeatureSize();
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
lastWord = word;
|
|
173
|
+
count--;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (lastWord) {
|
|
177
|
+
yield suffix !== undefined ? `${lastWord}${suffix}` : lastWord;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// helper //
|
|
183
|
+
function _content({ size = [1, 3], content } = {}) {
|
|
184
|
+
return content || lorem.lorem({ size });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function _blockquote(content) {
|
|
188
|
+
return content.split('\n').map(line => `> ${line}`).join('\n');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function indent(size, content) {
|
|
192
|
+
return content.split('\n').map(line => ' '.repeat(size) + line).join('\n');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function listItem(type, content) {
|
|
196
|
+
const [firstLine, restLines] = content.split('\n', 2);
|
|
197
|
+
const result = `${listItemPrefix(type)} ${firstLine}`;
|
|
198
|
+
const indentSize = type === 'ordered' ? 3 : 2;
|
|
199
|
+
return !restLines ? result : result + `\n${indent(indentSize, restLines)}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function listItemPrefix(type, checked = 'random') {
|
|
203
|
+
switch (type) {
|
|
204
|
+
case 'ordered':
|
|
205
|
+
return '1.';
|
|
206
|
+
case 'bullet':
|
|
207
|
+
return '-';
|
|
208
|
+
case 'task':
|
|
209
|
+
const mark = (checked === 'random' ? Math.random() < 0.5 : !!checked) ? 'x' : ' ';
|
|
210
|
+
return `- [${mark}]`;
|
|
211
|
+
default:
|
|
212
|
+
throw new Error(`unknown list item type: ${type}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function codeContent({ lang, size = [10, 30] }) {
|
|
217
|
+
// TODO
|
|
218
|
+
switch (lang) {
|
|
219
|
+
default:
|
|
220
|
+
return lorem.lorem({ output: 'multiline', size });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function tableRow(cells) {
|
|
225
|
+
return `| ${cells.join(' | ')} |`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function multiply(obj, i) {
|
|
229
|
+
const arr = [];
|
|
230
|
+
for (let j = 0; j < i; j++) {
|
|
231
|
+
arr.push(typeof obj === 'function' ? obj() : obj);
|
|
232
|
+
}
|
|
233
|
+
return arr;
|
|
234
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as u from './utils.js';
|
|
2
|
+
import * as fields from './fields.js';
|
|
3
|
+
|
|
4
|
+
export function *products({ rows, ...options } = {}) {
|
|
5
|
+
for (let i = 0; i < rows; i++) {
|
|
6
|
+
yield product({ ...options, index: i });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function product({} = {}) {
|
|
11
|
+
const id = u.id();
|
|
12
|
+
const prices = u.repeat(fields.price, [1, 2]);
|
|
13
|
+
prices.sort();
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
product_id: id,
|
|
17
|
+
authors: fields.authors(),
|
|
18
|
+
categories: [],
|
|
19
|
+
tags: fields.tags(),
|
|
20
|
+
title: fields.title(),
|
|
21
|
+
description: fields.description(),
|
|
22
|
+
//html,
|
|
23
|
+
cover_image: fields.image(),
|
|
24
|
+
url: `/products/${id}`,
|
|
25
|
+
sale_price: prices[0],
|
|
26
|
+
original_price: prices[prices.length - 1],
|
|
27
|
+
rating: fields.rating(),
|
|
28
|
+
availability: fields.availability(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function randomInt(min, max) {
|
|
2
|
+
return max == null || (max <= min) ? min : (min + Math.floor(Math.random() * (max - min)));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
// TODO: pass in size
|
|
6
|
+
export function repeat(fn, range) {
|
|
7
|
+
const n = randomInt(...range);
|
|
8
|
+
const result = [];
|
|
9
|
+
for (let i = 0; i < n; i++) {
|
|
10
|
+
result.push(fn());
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function id() {
|
|
16
|
+
return Math.random().toString(36).substring(2, 10);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function shuffle(array) {
|
|
20
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
21
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
22
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
23
|
+
}
|
|
24
|
+
return array;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function imageUrl(size) {
|
|
28
|
+
const seed = Math.floor(Math.random() * 1000);
|
|
29
|
+
const sizePath = Array.isArray(size) ? size.length > 1 ? `${size[0]}/${size[1]}` : `${size[0]}` : `${size}`;
|
|
30
|
+
return `https://picsum.photos/seed/${seed}/${sizePath}`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
- lorem
|
|
2
|
+
- ipsum
|
|
3
|
+
- dolor
|
|
4
|
+
- sit
|
|
5
|
+
- amet
|
|
6
|
+
- consectetur
|
|
7
|
+
- adipiscing
|
|
8
|
+
- elit
|
|
9
|
+
- curabitur
|
|
10
|
+
- vel
|
|
11
|
+
- hendrerit
|
|
12
|
+
- libero
|
|
13
|
+
- eleifend
|
|
14
|
+
- blandit
|
|
15
|
+
- nunc
|
|
16
|
+
- ornare
|
|
17
|
+
- odio
|
|
18
|
+
- ut
|
|
19
|
+
- orci
|
|
20
|
+
- gravida
|
|
21
|
+
- imperdiet
|
|
22
|
+
- nullam
|
|
23
|
+
- purus
|
|
24
|
+
- lacinia
|
|
25
|
+
- a
|
|
26
|
+
- pretium
|
|
27
|
+
- quis
|
|
28
|
+
- congue
|
|
29
|
+
- praesent
|
|
30
|
+
- sagittis
|
|
31
|
+
- laoreet
|
|
32
|
+
- auctor
|
|
33
|
+
- mauris
|
|
34
|
+
- non
|
|
35
|
+
- velit
|
|
36
|
+
- eros
|
|
37
|
+
- dictum
|
|
38
|
+
- proin
|
|
39
|
+
- accumsan
|
|
40
|
+
- sapien
|
|
41
|
+
- nec
|
|
42
|
+
- massa
|
|
43
|
+
- volutpat
|
|
44
|
+
- venenatis
|
|
45
|
+
- sed
|
|
46
|
+
- eu
|
|
47
|
+
- molestie
|
|
48
|
+
- lacus
|
|
49
|
+
- quisque
|
|
50
|
+
- porttitor
|
|
51
|
+
- ligula
|
|
52
|
+
- dui
|
|
53
|
+
- mollis
|
|
54
|
+
- tempus
|
|
55
|
+
- at
|
|
56
|
+
- magna
|
|
57
|
+
- vestibulum
|
|
58
|
+
- turpis
|
|
59
|
+
- ac
|
|
60
|
+
- diam
|
|
61
|
+
- tincidunt
|
|
62
|
+
- id
|
|
63
|
+
- condimentum
|
|
64
|
+
- enim
|
|
65
|
+
- sodales
|
|
66
|
+
- in
|
|
67
|
+
- hac
|
|
68
|
+
- habitasse
|
|
69
|
+
- platea
|
|
70
|
+
- dictumst
|
|
71
|
+
- aenean
|
|
72
|
+
- neque
|
|
73
|
+
- fusce
|
|
74
|
+
- augue
|
|
75
|
+
- leo
|
|
76
|
+
- eget
|
|
77
|
+
- semper
|
|
78
|
+
- mattis
|
|
79
|
+
- tortor
|
|
80
|
+
- scelerisque
|
|
81
|
+
- nulla
|
|
82
|
+
- interdum
|
|
83
|
+
- tellus
|
|
84
|
+
- malesuada
|
|
85
|
+
- rhoncus
|
|
86
|
+
- porta
|
|
87
|
+
- sem
|
|
88
|
+
- aliquet
|
|
89
|
+
- et
|
|
90
|
+
- nam
|
|
91
|
+
- suspendisse
|
|
92
|
+
- potenti
|
|
93
|
+
- vivamus
|
|
94
|
+
- luctus
|
|
95
|
+
- fringilla
|
|
96
|
+
- erat
|
|
97
|
+
- donec
|
|
98
|
+
- justo
|
|
99
|
+
- vehicula
|
|
100
|
+
- ultricies
|
|
101
|
+
- varius
|
|
102
|
+
- ante
|
|
103
|
+
- primis
|
|
104
|
+
- faucibus
|
|
105
|
+
- ultrices
|
|
106
|
+
- posuere
|
|
107
|
+
- cubilia
|
|
108
|
+
- curae
|
|
109
|
+
- etiam
|
|
110
|
+
- cursus
|
|
111
|
+
- aliquam
|
|
112
|
+
- quam
|
|
113
|
+
- dapibus
|
|
114
|
+
- nisl
|
|
115
|
+
- feugiat
|
|
116
|
+
- egestas
|
|
117
|
+
- class
|
|
118
|
+
- aptent
|
|
119
|
+
- taciti
|
|
120
|
+
- sociosqu
|
|
121
|
+
- ad
|
|
122
|
+
- litora
|
|
123
|
+
- torquent
|
|
124
|
+
- per
|
|
125
|
+
- conubia
|
|
126
|
+
- nostra
|
|
127
|
+
- inceptos
|
|
128
|
+
- himenaeos
|
|
129
|
+
- phasellus
|
|
130
|
+
- nibh
|
|
131
|
+
- pulvinar
|
|
132
|
+
- vitae
|
|
133
|
+
- urna
|
|
134
|
+
- iaculis
|
|
135
|
+
- lobortis
|
|
136
|
+
- nisi
|
|
137
|
+
- viverra
|
|
138
|
+
- arcu
|
|
139
|
+
- morbi
|
|
140
|
+
- pellentesque
|
|
141
|
+
- metus
|
|
142
|
+
- commodo
|
|
143
|
+
- ut
|
|
144
|
+
- facilisis
|
|
145
|
+
- felis
|
|
146
|
+
- tristique
|
|
147
|
+
- ullamcorper
|
|
148
|
+
- placerat
|
|
149
|
+
- aenean
|
|
150
|
+
- convallis
|
|
151
|
+
- sollicitudin
|
|
152
|
+
- integer
|
|
153
|
+
- rutrum
|
|
154
|
+
- duis
|
|
155
|
+
- est
|
|
156
|
+
- etiam
|
|
157
|
+
- bibendum
|
|
158
|
+
- donec
|
|
159
|
+
- pharetra
|
|
160
|
+
- vulputate
|
|
161
|
+
- maecenas
|
|
162
|
+
- mi
|
|
163
|
+
- fermentum
|
|
164
|
+
- consequat
|
|
165
|
+
- suscipit
|
|
166
|
+
- aliquam
|
|
167
|
+
- habitant
|
|
168
|
+
- senectus
|
|
169
|
+
- netus
|
|
170
|
+
- fames
|
|
171
|
+
- quisque
|
|
172
|
+
- euismod
|
|
173
|
+
- curabitur
|
|
174
|
+
- lectus
|
|
175
|
+
- elementum
|
|
176
|
+
- tempor
|
|
177
|
+
- risus
|
|
178
|
+
- cras
|
package/src/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import Koa from 'koa';
|
|
2
|
+
import Router from '@koa/router';
|
|
3
|
+
import cors from '@koa/cors';
|
|
4
|
+
import serveStatic from 'koa-static';
|
|
5
|
+
import { koaBody } from 'koa-body';
|
|
6
|
+
import _api from './api/index.js';
|
|
7
|
+
import { exclusion } from './utils.js';
|
|
8
|
+
|
|
9
|
+
export default function doggoganger({ port = 9901, serve = false, ...options } = {}) {
|
|
10
|
+
const app = new Koa();
|
|
11
|
+
const router = new Router();
|
|
12
|
+
const api = _api(options);
|
|
13
|
+
|
|
14
|
+
router.use('/api', api.routes(), api.allowedMethods());
|
|
15
|
+
|
|
16
|
+
if (serve) {
|
|
17
|
+
app
|
|
18
|
+
.use(exclusion)
|
|
19
|
+
.use(serveStatic('.'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
app
|
|
23
|
+
.use(cors())
|
|
24
|
+
.use(koaBody())
|
|
25
|
+
.use(router.routes())
|
|
26
|
+
.use(router.allowedMethods())
|
|
27
|
+
.listen(port);
|
|
28
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function shallServe(path) {
|
|
2
|
+
if (path.startsWith('/node_modules/') || path.startsWith('.')) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
const i = path.lastIndexOf('.');
|
|
6
|
+
const ext = i < 0 ? path.substring(i + 1) : '';
|
|
7
|
+
switch (ext) {
|
|
8
|
+
case 'md':
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
switch (path) {
|
|
12
|
+
case 'package.json':
|
|
13
|
+
case 'package-lock.json':
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function exclusion(ctx, next) {
|
|
20
|
+
const { path } = ctx;
|
|
21
|
+
if (shallServe(path)) {
|
|
22
|
+
await next();
|
|
23
|
+
} else {
|
|
24
|
+
ctx.status = 404;
|
|
25
|
+
}
|
|
26
|
+
}
|