-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex.js
128 lines (110 loc) · 4.03 KB
/
index.js
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
const process = require('node:process');
const isSANB = require('is-string-and-not-blank');
const sanitizeHtml = require('sanitize-html');
class Meta {
constructor(
config = {},
logger = console,
/* istanbul ignore next */
levelForMissing = process.env.NODE_ENV === 'development' ? 'error' : 'debug'
) {
this.config = { '/': ['', ''], ...config };
this.logger = logger;
this.levelForMissing = levelForMissing;
// ensure all config keys are arrays with two keys
for (const path of Object.keys(this.config)) {
if (!Array.isArray(this.config[path]))
throw new Error(`path "${path}" was not an array`);
// slice only the first two keys (0 = title, 1 = description)
this.config[path] = this.config[path].slice(0, 2);
// ensure it has both keys
if (this.config[path].length !== 2)
throw new Error(`path "${path}" must have exactly two keys`);
// ensure both keys are strings
if (typeof this.config[path][0] !== 'string')
throw new Error(`path "${path}" needs String for title`);
if (typeof this.config[path][1] !== 'string')
throw new Error(`path "${path}" needs String for description`);
}
this.getByPath = this.getByPath.bind(this);
this.middleware = this.middleware.bind(this);
}
getByPath(path = '/', t, originalPath, ctx) {
if (typeof path !== 'string')
throw new Error('path is required and must be a String');
if (originalPath === null || originalPath === undefined)
originalPath = path;
if (t !== null && t !== undefined && typeof t !== 'function')
throw new Error('t must be a Function');
if (typeof originalPath !== 'string')
throw new Error('originalPath must be a String');
let key = this.config[path];
// it should traverse up and split by / till it finds a parent route
if (!key)
if (
(!ctx || !ctx.status || ctx.status === 200) &&
(path === '/' || path.slice(0, path.lastIndexOf('/')) === '')
)
throw new Error(`path "${path}" needs a meta config key defined`);
else
return this.getByPath(
path.slice(0, path.lastIndexOf('/')) || '/',
t,
originalPath,
ctx
);
// translate the meta information
key = key.map((string) => {
// this has built in support for @ladjs/i18n via `ctx.request.t`
if (t) {
// replace `|` pipe character because
// translation will interpret as ranged interval
// <https://github.com/mashpie/i18n-node/issues/274>
string = string.replace(/\|/g, '|');
string = t(string);
}
return sanitizeHtml(string, {
allowedTags: [],
allowedAttributes: {}
});
});
return { title: key[0], description: key[1] };
}
middleware(ctx, next) {
// return early if there was no `ctx.render` bound
if (!ctx.render) return next();
// provide a default `ctx.state.meta` object used in routes/middleware
ctx.state.meta = ctx.state.meta || {};
//
// lookup page title and description
//
// this has built in support for @ladjs/i18n
// since it exposes `ctx.pathWithoutLocale`
const { getByPath, logger, levelForMissing } = this;
const { render } = ctx;
// override existing render but if and only if
// title and description weren't both set
ctx.render = function (...args) {
// if we already had a title/description (e.g. 404) then return early
if (isSANB(ctx.state.meta.title) && isSANB(ctx.state.meta.description))
return render.call(ctx, ...args);
// otherwise lookup the meta config
let data = {};
try {
data = getByPath(
ctx.pathWithoutLocale || ctx.path || '/',
ctx.request.t,
null,
ctx
);
} catch (err) {
logger[levelForMissing](err);
data = getByPath('/', ctx.request.t, null, ctx);
}
Object.assign(ctx.state.meta, data);
return render.call(ctx, ...args);
};
return next();
}
}
module.exports = Meta;