erudit 4.3.0 → 4.3.1-dev.1
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/app/composables/formatText.ts +9 -100
- package/app/composables/og.ts +58 -16
- package/app/pages/contributor/[contributorId].vue +1 -0
- package/app/pages/contributors.vue +1 -0
- package/app/pages/sponsors.vue +1 -0
- package/app/plugins/prerender.server.ts +1 -0
- package/nuxt.config.ts +1 -1
- package/package.json +9 -7
- package/server/api/prerender/ogImages.ts +46 -0
- package/server/erudit/ogImage/fonts/NotoSans-Bold.ttf +0 -0
- package/server/erudit/ogImage/fonts/NotoSans-Regular.ttf +0 -0
- package/server/erudit/ogImage/formatText.ts +12 -0
- package/server/erudit/ogImage/icons.ts +22 -0
- package/server/erudit/ogImage/logotype.ts +51 -0
- package/server/erudit/ogImage/render.ts +90 -0
- package/server/erudit/ogImage/shared.ts +320 -0
- package/server/erudit/ogImage/templates/content.ts +200 -0
- package/server/erudit/ogImage/templates/index.ts +138 -0
- package/server/erudit/ogImage/templates/sitePage.ts +110 -0
- package/server/erudit/staticFile.ts +3 -0
- package/server/routes/og/content/[...contentTypePath].ts +126 -0
- package/server/routes/og/site/contributor/[contributorId].ts +60 -0
- package/server/routes/og/site/contributors.png.ts +38 -0
- package/server/routes/og/site/index.png.ts +51 -0
- package/server/routes/og/site/sponsors.png.ts +38 -0
- package/shared/utils/formatText.ts +73 -0
- package/app/formatters/ru.ts +0 -14
- package/public/og.png +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type SatoriNode,
|
|
3
|
+
DIM_COLOR,
|
|
4
|
+
svgToDataUri,
|
|
5
|
+
truncate,
|
|
6
|
+
ogTitleColor,
|
|
7
|
+
ogRootContainer,
|
|
8
|
+
ogTopRow,
|
|
9
|
+
ogDescription,
|
|
10
|
+
ogBottomSpacer,
|
|
11
|
+
ogActionButton,
|
|
12
|
+
} from '../shared';
|
|
13
|
+
|
|
14
|
+
export interface SitePageOgParams {
|
|
15
|
+
title: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
pageIconSvg: string;
|
|
18
|
+
logotypeDataUri?: string;
|
|
19
|
+
siteName?: string;
|
|
20
|
+
brandColor: string;
|
|
21
|
+
formatText?: (text: string) => string;
|
|
22
|
+
learnButtonText?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildSitePageOgTemplate(params: SitePageOgParams): SatoriNode {
|
|
26
|
+
const titleColor = ogTitleColor(params.brandColor);
|
|
27
|
+
const fmt = params.formatText ?? ((t: string) => t);
|
|
28
|
+
|
|
29
|
+
const children: SatoriNode[] = [];
|
|
30
|
+
|
|
31
|
+
// Top row: site logo + name
|
|
32
|
+
const top = ogTopRow({
|
|
33
|
+
logotypeDataUri: params.logotypeDataUri,
|
|
34
|
+
siteName: params.siteName,
|
|
35
|
+
formatText: params.formatText,
|
|
36
|
+
});
|
|
37
|
+
if (top) children.push(top);
|
|
38
|
+
|
|
39
|
+
// Center: icon + title inline
|
|
40
|
+
const titleTruncated = truncate(params.title, 60);
|
|
41
|
+
children.push({
|
|
42
|
+
type: 'div',
|
|
43
|
+
props: {
|
|
44
|
+
style: {
|
|
45
|
+
display: 'flex',
|
|
46
|
+
flexDirection: 'column' as const,
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
flex: 1,
|
|
49
|
+
paddingLeft: 60,
|
|
50
|
+
paddingRight: 60,
|
|
51
|
+
},
|
|
52
|
+
children: [
|
|
53
|
+
{
|
|
54
|
+
type: 'div',
|
|
55
|
+
props: {
|
|
56
|
+
style: {
|
|
57
|
+
display: 'flex',
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
gap: 24,
|
|
60
|
+
},
|
|
61
|
+
children: [
|
|
62
|
+
{
|
|
63
|
+
type: 'img',
|
|
64
|
+
props: {
|
|
65
|
+
src: svgToDataUri(params.pageIconSvg, DIM_COLOR),
|
|
66
|
+
width: 44,
|
|
67
|
+
height: 44,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: 'div',
|
|
72
|
+
props: {
|
|
73
|
+
style: {
|
|
74
|
+
fontSize: 52,
|
|
75
|
+
fontWeight: 700,
|
|
76
|
+
color: titleColor,
|
|
77
|
+
textAlign: 'left' as const,
|
|
78
|
+
lineHeight: 1.25,
|
|
79
|
+
maxHeight: 200,
|
|
80
|
+
overflow: 'hidden',
|
|
81
|
+
wordBreak: 'break-word' as const,
|
|
82
|
+
},
|
|
83
|
+
children: fmt(titleTruncated),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
// Action button
|
|
90
|
+
...(params.learnButtonText
|
|
91
|
+
? [ogActionButton(params.brandColor, params.learnButtonText)]
|
|
92
|
+
: []),
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Bottom: description or spacer
|
|
98
|
+
if (params.description) {
|
|
99
|
+
children.push(
|
|
100
|
+
ogDescription({
|
|
101
|
+
description: params.description,
|
|
102
|
+
formatText: params.formatText,
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
} else {
|
|
106
|
+
children.push(ogBottomSpacer());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return ogRootContainer(params.brandColor, children);
|
|
110
|
+
}
|
|
@@ -7,6 +7,9 @@ const mimeByExt: Record<string, string> = {
|
|
|
7
7
|
'.jpeg': 'image/jpeg',
|
|
8
8
|
'.gif': 'image/gif',
|
|
9
9
|
'.webp': 'image/webp',
|
|
10
|
+
'.mp4': 'video/mp4',
|
|
11
|
+
'.webm': 'video/webm',
|
|
12
|
+
'.ogg': 'video/ogg',
|
|
10
13
|
'.txt': 'text/plain; charset=utf-8',
|
|
11
14
|
'.json': 'application/json; charset=utf-8',
|
|
12
15
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { getIconSvg } from '#layers/erudit/server/erudit/ogImage/icons';
|
|
3
|
+
import { renderOgImage } from '#layers/erudit/server/erudit/ogImage/render';
|
|
4
|
+
import {
|
|
5
|
+
type ContentOgParams,
|
|
6
|
+
buildContentOgTemplate,
|
|
7
|
+
} from '#layers/erudit/server/erudit/ogImage/templates/content';
|
|
8
|
+
import { loadLogotypeDataUri } from '#layers/erudit/server/erudit/ogImage/logotype';
|
|
9
|
+
import { ogFormatText } from '#layers/erudit/server/erudit/ogImage/formatText';
|
|
10
|
+
import {
|
|
11
|
+
checkOgEnabled,
|
|
12
|
+
getOgBrandColor,
|
|
13
|
+
getOgSiteName,
|
|
14
|
+
getOgLearnPhrase,
|
|
15
|
+
getOgLogotypePath,
|
|
16
|
+
sendOgPng,
|
|
17
|
+
} from '#layers/erudit/server/erudit/ogImage/shared';
|
|
18
|
+
import { parseContentTypePath } from '#layers/erudit/shared/utils/contentTypePath';
|
|
19
|
+
|
|
20
|
+
export default defineEventHandler(async (event) => {
|
|
21
|
+
checkOgEnabled();
|
|
22
|
+
|
|
23
|
+
const rawPath = event.context.params?.contentTypePath;
|
|
24
|
+
if (!rawPath) {
|
|
25
|
+
throw createError({
|
|
26
|
+
statusCode: 400,
|
|
27
|
+
statusMessage: 'Missing content type path',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!rawPath.endsWith('.png')) {
|
|
32
|
+
throw createError({
|
|
33
|
+
statusCode: 400,
|
|
34
|
+
statusMessage: 'OG image path must end with .png',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parsed = parseContentTypePath(rawPath.slice(0, -4));
|
|
39
|
+
const typeOrPart = parsed.type === 'topic' ? parsed.topicPart : parsed.type;
|
|
40
|
+
const contentId = parsed.contentId;
|
|
41
|
+
|
|
42
|
+
// Resolve content from nav tree
|
|
43
|
+
const navNode = ERUDIT.contentNav.getNodeOrThrow(contentId);
|
|
44
|
+
const fullId = navNode.fullId;
|
|
45
|
+
|
|
46
|
+
// Gather data
|
|
47
|
+
const title = await ERUDIT.repository.content.title(fullId);
|
|
48
|
+
const description = await ERUDIT.repository.content.description(fullId);
|
|
49
|
+
|
|
50
|
+
const bookNode = ERUDIT.contentNav.getBookFor(fullId);
|
|
51
|
+
const isBook = navNode.type === 'book';
|
|
52
|
+
const bookTitle = bookNode
|
|
53
|
+
? await ERUDIT.repository.content.title(bookNode.fullId)
|
|
54
|
+
: undefined;
|
|
55
|
+
|
|
56
|
+
const brandColor = getOgBrandColor();
|
|
57
|
+
const siteName = getOgSiteName();
|
|
58
|
+
|
|
59
|
+
// Content type icon and label
|
|
60
|
+
const contentIconSvg = getIconSvg(typeOrPart);
|
|
61
|
+
|
|
62
|
+
const phrases = ERUDIT.language.phrases;
|
|
63
|
+
const contentLabel =
|
|
64
|
+
typeOrPart === 'book'
|
|
65
|
+
? phrases.book
|
|
66
|
+
: typeOrPart === 'group'
|
|
67
|
+
? phrases.group
|
|
68
|
+
: typeOrPart === 'page'
|
|
69
|
+
? phrases.page
|
|
70
|
+
: typeOrPart === 'article'
|
|
71
|
+
? phrases.article
|
|
72
|
+
: typeOrPart === 'summary'
|
|
73
|
+
? phrases.summary
|
|
74
|
+
: typeOrPart === 'practice'
|
|
75
|
+
? phrases.practice
|
|
76
|
+
: phrases.content;
|
|
77
|
+
|
|
78
|
+
// Book icon (for book row above title)
|
|
79
|
+
const bookIconSvg = bookTitle ? getIconSvg('book') : undefined;
|
|
80
|
+
|
|
81
|
+
// Decoration image
|
|
82
|
+
let decorationDataUri: string | undefined;
|
|
83
|
+
const decorationPath = await ERUDIT.repository.content.decoration(fullId);
|
|
84
|
+
if (decorationPath) {
|
|
85
|
+
const fullDecorationPath = ERUDIT.paths.project(decorationPath);
|
|
86
|
+
if (existsSync(fullDecorationPath)) {
|
|
87
|
+
const decorationBuffer = readFileSync(fullDecorationPath);
|
|
88
|
+
const ext = decorationPath.split('.').pop()?.toLowerCase();
|
|
89
|
+
const mime =
|
|
90
|
+
ext === 'svg'
|
|
91
|
+
? 'image/svg+xml'
|
|
92
|
+
: ext === 'png'
|
|
93
|
+
? 'image/png'
|
|
94
|
+
: ext === 'jpg' || ext === 'jpeg'
|
|
95
|
+
? 'image/jpeg'
|
|
96
|
+
: ext === 'webp'
|
|
97
|
+
? 'image/webp'
|
|
98
|
+
: 'image/png';
|
|
99
|
+
decorationDataUri = `data:${mime};base64,${decorationBuffer.toString('base64')}`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const logotypeDataUri = await loadLogotypeDataUri(getOgLogotypePath());
|
|
104
|
+
|
|
105
|
+
const params: ContentOgParams = {
|
|
106
|
+
title,
|
|
107
|
+
description,
|
|
108
|
+
contentLabel,
|
|
109
|
+
contentIconSvg,
|
|
110
|
+
bookTitle,
|
|
111
|
+
bookIconSvg,
|
|
112
|
+
isBook,
|
|
113
|
+
decorationDataUri,
|
|
114
|
+
logotypeDataUri,
|
|
115
|
+
siteName,
|
|
116
|
+
brandColor,
|
|
117
|
+
formatText: ogFormatText,
|
|
118
|
+
learnButtonText: getOgLearnPhrase(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const template = buildContentOgTemplate(params);
|
|
122
|
+
const cacheKey = `content_${typeOrPart}/${fullId}`;
|
|
123
|
+
const png = await renderOgImage(cacheKey, template);
|
|
124
|
+
|
|
125
|
+
return sendOgPng(event, png);
|
|
126
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { renderOgImage } from '#layers/erudit/server/erudit/ogImage/render';
|
|
2
|
+
import {
|
|
3
|
+
type SitePageOgParams,
|
|
4
|
+
buildSitePageOgTemplate,
|
|
5
|
+
} from '#layers/erudit/server/erudit/ogImage/templates/sitePage';
|
|
6
|
+
import { loadLogotypeDataUri } from '#layers/erudit/server/erudit/ogImage/logotype';
|
|
7
|
+
import { getIconSvg } from '#layers/erudit/server/erudit/ogImage/icons';
|
|
8
|
+
import { ogFormatText } from '#layers/erudit/server/erudit/ogImage/formatText';
|
|
9
|
+
import {
|
|
10
|
+
checkOgEnabled,
|
|
11
|
+
getOgBrandColor,
|
|
12
|
+
getOgSiteName,
|
|
13
|
+
getOgOpenPhrase,
|
|
14
|
+
getOgLogotypePath,
|
|
15
|
+
sendOgPng,
|
|
16
|
+
} from '#layers/erudit/server/erudit/ogImage/shared';
|
|
17
|
+
|
|
18
|
+
export default defineEventHandler(async (event) => {
|
|
19
|
+
checkOgEnabled();
|
|
20
|
+
|
|
21
|
+
const rawId = event.context.params?.contributorId;
|
|
22
|
+
if (!rawId) {
|
|
23
|
+
throw createError({
|
|
24
|
+
statusCode: 400,
|
|
25
|
+
statusMessage: 'Missing contributor ID',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Strip .png suffix if Nitro passes it as part of the param
|
|
30
|
+
const contributorId = rawId.replace(/\.png$/, '');
|
|
31
|
+
|
|
32
|
+
const phrases = ERUDIT.language.phrases;
|
|
33
|
+
|
|
34
|
+
const contributor = await $fetch<{
|
|
35
|
+
id: string;
|
|
36
|
+
displayName?: string;
|
|
37
|
+
short?: string;
|
|
38
|
+
}>(`/api/contributor/page/${contributorId}`);
|
|
39
|
+
|
|
40
|
+
const displayName = contributor.displayName || contributor.id;
|
|
41
|
+
|
|
42
|
+
const params: SitePageOgParams = {
|
|
43
|
+
title: displayName,
|
|
44
|
+
description: phrases.contributor_page_description(displayName),
|
|
45
|
+
pageIconSvg: getIconSvg('contributor'),
|
|
46
|
+
logotypeDataUri: await loadLogotypeDataUri(getOgLogotypePath()),
|
|
47
|
+
siteName: getOgSiteName(),
|
|
48
|
+
brandColor: getOgBrandColor(),
|
|
49
|
+
formatText: ogFormatText,
|
|
50
|
+
learnButtonText: getOgOpenPhrase(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const template = buildSitePageOgTemplate(params);
|
|
54
|
+
const png = await renderOgImage(
|
|
55
|
+
`__site_contributor_${contributorId}`,
|
|
56
|
+
template,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return sendOgPng(event, png);
|
|
60
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { renderOgImage } from '#layers/erudit/server/erudit/ogImage/render';
|
|
2
|
+
import {
|
|
3
|
+
type SitePageOgParams,
|
|
4
|
+
buildSitePageOgTemplate,
|
|
5
|
+
} from '#layers/erudit/server/erudit/ogImage/templates/sitePage';
|
|
6
|
+
import { loadLogotypeDataUri } from '#layers/erudit/server/erudit/ogImage/logotype';
|
|
7
|
+
import { getIconSvg } from '#layers/erudit/server/erudit/ogImage/icons';
|
|
8
|
+
import { ogFormatText } from '#layers/erudit/server/erudit/ogImage/formatText';
|
|
9
|
+
import {
|
|
10
|
+
checkOgEnabled,
|
|
11
|
+
getOgBrandColor,
|
|
12
|
+
getOgSiteName,
|
|
13
|
+
getOgOpenPhrase,
|
|
14
|
+
getOgLogotypePath,
|
|
15
|
+
sendOgPng,
|
|
16
|
+
} from '#layers/erudit/server/erudit/ogImage/shared';
|
|
17
|
+
|
|
18
|
+
export default defineEventHandler(async (event) => {
|
|
19
|
+
checkOgEnabled();
|
|
20
|
+
|
|
21
|
+
const phrases = ERUDIT.language.phrases;
|
|
22
|
+
|
|
23
|
+
const params: SitePageOgParams = {
|
|
24
|
+
title: phrases.contributors,
|
|
25
|
+
description: phrases.contributors_description,
|
|
26
|
+
pageIconSvg: getIconSvg('contributors'),
|
|
27
|
+
logotypeDataUri: await loadLogotypeDataUri(getOgLogotypePath()),
|
|
28
|
+
siteName: getOgSiteName(),
|
|
29
|
+
brandColor: getOgBrandColor(),
|
|
30
|
+
formatText: ogFormatText,
|
|
31
|
+
learnButtonText: getOgOpenPhrase(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const template = buildSitePageOgTemplate(params);
|
|
35
|
+
const png = await renderOgImage('__site_contributors', template);
|
|
36
|
+
|
|
37
|
+
return sendOgPng(event, png);
|
|
38
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { renderOgImage } from '#layers/erudit/server/erudit/ogImage/render';
|
|
2
|
+
import {
|
|
3
|
+
type IndexOgParams,
|
|
4
|
+
buildIndexOgTemplate,
|
|
5
|
+
} from '#layers/erudit/server/erudit/ogImage/templates/index';
|
|
6
|
+
import { loadLogotypeDataUri } from '#layers/erudit/server/erudit/ogImage/logotype';
|
|
7
|
+
import { ogFormatText } from '#layers/erudit/server/erudit/ogImage/formatText';
|
|
8
|
+
import {
|
|
9
|
+
checkOgEnabled,
|
|
10
|
+
getOgBrandColor,
|
|
11
|
+
getOgSiteName,
|
|
12
|
+
getOgSiteShort,
|
|
13
|
+
getOgLogotypePath,
|
|
14
|
+
sendOgPng,
|
|
15
|
+
} from '#layers/erudit/server/erudit/ogImage/shared';
|
|
16
|
+
|
|
17
|
+
export default defineEventHandler(async (event) => {
|
|
18
|
+
checkOgEnabled();
|
|
19
|
+
|
|
20
|
+
const brandColor = getOgBrandColor();
|
|
21
|
+
const siteName = getOgSiteName();
|
|
22
|
+
const logotypeDataUri = await loadLogotypeDataUri(getOgLogotypePath());
|
|
23
|
+
|
|
24
|
+
const siteShort =
|
|
25
|
+
getOgSiteShort() || ERUDIT.config.public.asideMajor?.siteInfo?.short;
|
|
26
|
+
|
|
27
|
+
let description: string | undefined;
|
|
28
|
+
try {
|
|
29
|
+
const indexPage = await $fetch<{
|
|
30
|
+
description?: string;
|
|
31
|
+
seo?: { description?: string };
|
|
32
|
+
}>('/api/indexPage');
|
|
33
|
+
description = indexPage?.seo?.description || indexPage?.description;
|
|
34
|
+
} catch {
|
|
35
|
+
// Ignore
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const params: IndexOgParams = {
|
|
39
|
+
logotypeDataUri,
|
|
40
|
+
siteName,
|
|
41
|
+
short: siteShort || undefined,
|
|
42
|
+
description,
|
|
43
|
+
brandColor,
|
|
44
|
+
formatText: ogFormatText,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const template = buildIndexOgTemplate(params);
|
|
48
|
+
const png = await renderOgImage('__index', template);
|
|
49
|
+
|
|
50
|
+
return sendOgPng(event, png);
|
|
51
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { renderOgImage } from '#layers/erudit/server/erudit/ogImage/render';
|
|
2
|
+
import {
|
|
3
|
+
type SitePageOgParams,
|
|
4
|
+
buildSitePageOgTemplate,
|
|
5
|
+
} from '#layers/erudit/server/erudit/ogImage/templates/sitePage';
|
|
6
|
+
import { loadLogotypeDataUri } from '#layers/erudit/server/erudit/ogImage/logotype';
|
|
7
|
+
import { getIconSvg } from '#layers/erudit/server/erudit/ogImage/icons';
|
|
8
|
+
import { ogFormatText } from '#layers/erudit/server/erudit/ogImage/formatText';
|
|
9
|
+
import {
|
|
10
|
+
checkOgEnabled,
|
|
11
|
+
getOgBrandColor,
|
|
12
|
+
getOgSiteName,
|
|
13
|
+
getOgOpenPhrase,
|
|
14
|
+
getOgLogotypePath,
|
|
15
|
+
sendOgPng,
|
|
16
|
+
} from '#layers/erudit/server/erudit/ogImage/shared';
|
|
17
|
+
|
|
18
|
+
export default defineEventHandler(async (event) => {
|
|
19
|
+
checkOgEnabled();
|
|
20
|
+
|
|
21
|
+
const phrases = ERUDIT.language.phrases;
|
|
22
|
+
|
|
23
|
+
const params: SitePageOgParams = {
|
|
24
|
+
title: phrases.sponsors,
|
|
25
|
+
description: phrases.sponsors_description,
|
|
26
|
+
pageIconSvg: getIconSvg('sponsors'),
|
|
27
|
+
logotypeDataUri: await loadLogotypeDataUri(getOgLogotypePath()),
|
|
28
|
+
siteName: getOgSiteName(),
|
|
29
|
+
brandColor: getOgBrandColor(),
|
|
30
|
+
formatText: ogFormatText,
|
|
31
|
+
learnButtonText: getOgOpenPhrase(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const template = buildSitePageOgTemplate(params);
|
|
35
|
+
const png = await renderOgImage('__site_sponsors', template);
|
|
36
|
+
|
|
37
|
+
return sendOgPng(event, png);
|
|
38
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { FormatText, FormatTextState } from '@erudit-js/core/formatText';
|
|
2
|
+
|
|
3
|
+
type LanguageFormatText = (text: string) => string;
|
|
4
|
+
|
|
5
|
+
function enFormatter(text: string): string {
|
|
6
|
+
return text.replaceAll("'", '\u2019');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function ruFormatter(text: string): string {
|
|
10
|
+
return text.replace(
|
|
11
|
+
/ (в|не|без|для|до|за|из|к|на|над|о|об|от|по|под|при|про|с|у|через|вокруг|около|после|перед|между|внутри|вне|из-за|из-под|ради|сквозь|среди|насчёт|вследствие|благодаря|несмотря|наперекор|вопреки|подле|возле|рядом|навстречу) /gimu,
|
|
12
|
+
' $1\xa0',
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const builtInFormatters: Partial<Record<string, LanguageFormatText>> = {
|
|
17
|
+
en: enFormatter,
|
|
18
|
+
ru: ruFormatter,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function createFormatTextFn(
|
|
22
|
+
languageCode: string,
|
|
23
|
+
extraFormatter?: LanguageFormatText,
|
|
24
|
+
): FormatText {
|
|
25
|
+
const langFormatter =
|
|
26
|
+
extraFormatter ?? builtInFormatters[languageCode] ?? ((t: string) => t);
|
|
27
|
+
|
|
28
|
+
const quoteSymbols: [string, string] =
|
|
29
|
+
languageCode === 'ru' ? ['\u00AB', '\u00BB'] : ['\u201C', '\u201D'];
|
|
30
|
+
|
|
31
|
+
function formatText(text: string, state?: FormatTextState): string;
|
|
32
|
+
function formatText(text: undefined, state?: FormatTextState): undefined;
|
|
33
|
+
function formatText(
|
|
34
|
+
text?: string,
|
|
35
|
+
state?: FormatTextState,
|
|
36
|
+
): string | undefined;
|
|
37
|
+
function formatText(
|
|
38
|
+
text?: string,
|
|
39
|
+
state?: FormatTextState,
|
|
40
|
+
): string | undefined {
|
|
41
|
+
if (text === undefined) {
|
|
42
|
+
return text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Normalize spacing
|
|
46
|
+
text = text
|
|
47
|
+
.trim()
|
|
48
|
+
.replace(/\r\n/gm, '\n')
|
|
49
|
+
.replace(/\n{3,}/gm, '\n\n')
|
|
50
|
+
.replace(/[ \t]+/gm, ' ');
|
|
51
|
+
|
|
52
|
+
// Normalize dashes
|
|
53
|
+
text = text.replace(/(^| )--($| )/gm, '$1\u2014$2');
|
|
54
|
+
|
|
55
|
+
// Normalize quotes
|
|
56
|
+
let quoteOpen = state?.quote === 'opened';
|
|
57
|
+
text = text.replaceAll(/"/gm, () => {
|
|
58
|
+
quoteOpen = !quoteOpen;
|
|
59
|
+
if (state) {
|
|
60
|
+
state.quote = quoteOpen ? 'opened' : 'closed';
|
|
61
|
+
}
|
|
62
|
+
return quoteOpen ? quoteSymbols[0] : quoteSymbols[1];
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Normalize ellipsis
|
|
66
|
+
text = text.replace(/\.{3}/gm, '\u2026');
|
|
67
|
+
|
|
68
|
+
// Language-specific formatting
|
|
69
|
+
return langFormatter(text);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return formatText;
|
|
73
|
+
}
|
package/app/formatters/ru.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export default (text: string) => {
|
|
2
|
-
text = ruStickyPrepositions(text);
|
|
3
|
-
return text;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Formats prepositions in Russian text so that they are always adjacent to the next word and are not left hanging “in the air” when the line breaks.
|
|
8
|
-
*/
|
|
9
|
-
function ruStickyPrepositions(text: string): string {
|
|
10
|
-
return text.replace(
|
|
11
|
-
/ (в|не|без|для|до|за|из|к|на|над|о|об|от|по|под|при|про|с|у|через|вокруг|около|после|перед|между|внутри|вне|из-за|из-под|ради|сквозь|среди|насчёт|вследствие|благодаря|несмотря|наперекор|вопреки|подле|возле|рядом|навстречу) /gimu,
|
|
12
|
-
' $1\xa0',
|
|
13
|
-
);
|
|
14
|
-
}
|
package/public/og.png
DELETED
|
Binary file
|