@diplodoc/cli 4.0.0

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +785 -0
  2. package/LICENSE +21 -0
  3. package/README.md +62 -0
  4. package/README.ru.md +63 -0
  5. package/build/app.client.css +47 -0
  6. package/build/app.client.js +3 -0
  7. package/build/index.js +3993 -0
  8. package/build/index.js.map +7 -0
  9. package/build/lib.js +3374 -0
  10. package/build/lib.js.map +7 -0
  11. package/build/linter.js +1265 -0
  12. package/build/linter.js.map +7 -0
  13. package/package.json +126 -0
  14. package/src/cmd/build/index.ts +304 -0
  15. package/src/cmd/index.ts +4 -0
  16. package/src/cmd/publish/index.ts +92 -0
  17. package/src/cmd/publish/upload.ts +61 -0
  18. package/src/cmd/translate/index.ts +261 -0
  19. package/src/cmd/xliff/compose.ts +222 -0
  20. package/src/cmd/xliff/extract.ts +237 -0
  21. package/src/cmd/xliff/index.ts +27 -0
  22. package/src/constants.ts +122 -0
  23. package/src/globals.d.ts +1 -0
  24. package/src/index.ts +54 -0
  25. package/src/models.ts +249 -0
  26. package/src/packages/credentials/index.ts +1 -0
  27. package/src/packages/credentials/yandex-oauth.ts +42 -0
  28. package/src/resolvers/index.ts +3 -0
  29. package/src/resolvers/lintPage.ts +119 -0
  30. package/src/resolvers/md2html.ts +142 -0
  31. package/src/resolvers/md2md.ts +147 -0
  32. package/src/services/argv.ts +38 -0
  33. package/src/services/authors.ts +64 -0
  34. package/src/services/contributors.ts +104 -0
  35. package/src/services/includers/batteries/common.ts +34 -0
  36. package/src/services/includers/batteries/generic.ts +130 -0
  37. package/src/services/includers/batteries/index.ts +3 -0
  38. package/src/services/includers/batteries/sourcedocs.ts +33 -0
  39. package/src/services/includers/batteries/unarchive.ts +97 -0
  40. package/src/services/includers/index.ts +157 -0
  41. package/src/services/index.ts +6 -0
  42. package/src/services/leading.ts +88 -0
  43. package/src/services/metadata.ts +249 -0
  44. package/src/services/plugins.ts +76 -0
  45. package/src/services/preset.ts +55 -0
  46. package/src/services/tocs.ts +401 -0
  47. package/src/services/utils.ts +151 -0
  48. package/src/steps/index.ts +6 -0
  49. package/src/steps/processAssets.ts +36 -0
  50. package/src/steps/processExcludedFiles.ts +47 -0
  51. package/src/steps/processLinter.ts +100 -0
  52. package/src/steps/processLogs.ts +18 -0
  53. package/src/steps/processMapFile.ts +35 -0
  54. package/src/steps/processPages.ts +312 -0
  55. package/src/steps/processServiceFiles.ts +95 -0
  56. package/src/steps/publishFilesToS3.ts +47 -0
  57. package/src/utils/file.ts +17 -0
  58. package/src/utils/glob.ts +14 -0
  59. package/src/utils/index.ts +8 -0
  60. package/src/utils/logger.ts +42 -0
  61. package/src/utils/markup.ts +125 -0
  62. package/src/utils/path.ts +24 -0
  63. package/src/utils/presets.ts +20 -0
  64. package/src/utils/singlePage.ts +228 -0
  65. package/src/utils/toc.ts +87 -0
  66. package/src/utils/url.ts +3 -0
  67. package/src/utils/worker.ts +10 -0
  68. package/src/validator.ts +150 -0
  69. package/src/vcs-connector/client/github.ts +52 -0
  70. package/src/vcs-connector/connector-models.ts +76 -0
  71. package/src/vcs-connector/connector-validator.ts +114 -0
  72. package/src/vcs-connector/github.ts +333 -0
  73. package/src/vcs-connector/index.ts +15 -0
  74. package/src/workers/linter/index.ts +62 -0
@@ -0,0 +1,125 @@
1
+ import {platform} from 'process';
2
+
3
+ import {CUSTOM_STYLE, Platforms, ResourceType} from '../constants';
4
+ import {SinglePageResult, Resources} from '../models';
5
+ import {ArgvService, PluginService} from '../services';
6
+ import {preprocessPageHtmlForSinglePage} from './singlePage';
7
+ import {render, DocInnerProps, DocPageData} from '@diplodoc/client';
8
+ import client from '../../scripts/client';
9
+
10
+ export interface TitleMeta {
11
+ title?: string;
12
+ }
13
+ export type Meta = TitleMeta & Resources;
14
+
15
+ export function generateStaticMarkup(props: DocInnerProps<DocPageData>, pathToBundle: string): string {
16
+ const {title: metaTitle, style, script} = props.data.meta as Meta || {};
17
+ const {title: tocTitle} = props.data.toc;
18
+ const {title: pageTitle} = props.data;
19
+
20
+ const title = getTitle({
21
+ metaTitle,
22
+ tocTitle: tocTitle as string,
23
+ pageTitle,
24
+ });
25
+ const resources = getResources({style, script});
26
+
27
+ const {staticContent} = ArgvService.getConfig();
28
+
29
+ const html = staticContent ? render(props) : '';
30
+
31
+ return `
32
+ <!DOCTYPE html>
33
+ <html lang="${props.lang}">
34
+ <head>
35
+ <meta charset="utf-8">
36
+ ${getMetadata(props.data.meta as Record<string, string>)}
37
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
38
+ <title>${title}</title>
39
+ <style type="text/css">
40
+ body {
41
+ height: 100vh;
42
+ }
43
+ </style>
44
+ <link type="text/css" rel="stylesheet" href="${client.bundle.css(pathToBundle)}" />
45
+ ${PluginService.getHeadContent()}
46
+ ${resources}
47
+ </head>
48
+ <body class="yc-root yc-root_theme_light">
49
+ <div id="root">${html}</div>
50
+ <script type="application/javascript">
51
+ window.STATIC_CONTENT = ${staticContent}
52
+ window.__DATA__ = ${JSON.stringify(props)};
53
+ </script>
54
+ <script type="application/javascript" src="${client.bundle.js(pathToBundle)}"></script>
55
+ </body>
56
+ </html>
57
+ `;
58
+ }
59
+
60
+ interface GetTitleOptions {
61
+ tocTitle?: string;
62
+ metaTitle?: string;
63
+ pageTitle?: string;
64
+ }
65
+
66
+ function getTitle({tocTitle, metaTitle, pageTitle}: GetTitleOptions) {
67
+ const resultPageTitle = metaTitle || pageTitle;
68
+
69
+ if (!resultPageTitle && tocTitle) {
70
+ return tocTitle;
71
+ }
72
+
73
+ if (resultPageTitle && !tocTitle) {
74
+ return resultPageTitle;
75
+ }
76
+
77
+ return resultPageTitle && tocTitle ? `${resultPageTitle} | ${tocTitle}` : '';
78
+ }
79
+
80
+ function getMetadata(metadata: Record<string, string>): string {
81
+ if (!metadata) {
82
+ return '';
83
+ }
84
+
85
+ // Exclude resources from meta, proceed them separately
86
+ const metaEntries = Object.entries(metadata).filter(([key]) => !Object.keys(ResourceType).includes(key));
87
+
88
+ return metaEntries
89
+ .map(([name, content]) => {
90
+ return `<meta name="${name}" content="${content}">`;
91
+ })
92
+ .join('\n');
93
+ }
94
+
95
+ function getResources({style, script}: Resources) {
96
+ const resourcesTags: string[] = [];
97
+
98
+ if (style) {
99
+ style.forEach((el, id) => resourcesTags.push(
100
+ `<link rel="stylesheet" type="text/css" href="${el}" ${id === 0 && `id="${CUSTOM_STYLE}"`}>`,
101
+ ));
102
+ }
103
+
104
+ if (script) {
105
+ script.forEach((el) => resourcesTags.push(
106
+ `<script src="${el}"></script>`,
107
+ ));
108
+ }
109
+
110
+ return resourcesTags.join('\n');
111
+ }
112
+
113
+ export const сarriage = platform === Platforms.WINDOWS ? '\r\n' : '\n';
114
+
115
+ export function joinSinglePageResults(singlePageResults: SinglePageResult[], root: string, tocDir: string): string {
116
+ const delimeter = `${сarriage}${сarriage}<hr class="yfm-page__delimeter">${сarriage}${сarriage}`;
117
+ return singlePageResults
118
+ .filter(({content}) => content)
119
+ .map(({content, path, title}) => preprocessPageHtmlForSinglePage(content, {root, path, tocDir, title}))
120
+ .join(delimeter);
121
+ }
122
+
123
+ export function replaceDoubleToSingleQuotes(str: string): string {
124
+ return str.replace(/"/g, '\'');
125
+ }
@@ -0,0 +1,24 @@
1
+ import {sep} from 'path';
2
+ import {Platforms} from '../constants';
3
+
4
+ export function addSlashPrefix(path: string): string {
5
+ const slashPrefix = path.startsWith(sep) ? '' : sep;
6
+
7
+ return `${slashPrefix}${path}`;
8
+ }
9
+
10
+ export function convertBackSlashToSlash(path: string): string {
11
+ if (process.platform === Platforms.WINDOWS) {
12
+ return path.replace(/\\/g, '/');
13
+ }
14
+
15
+ return path;
16
+ }
17
+
18
+ export function convertSlashToWindowsBackSlashes(path: string): string {
19
+ if (process.platform === Platforms.WINDOWS) {
20
+ return path.replace(/\//g, '\\\\');
21
+ }
22
+
23
+ return path;
24
+ }
@@ -0,0 +1,20 @@
1
+ import {dirname, relative, resolve} from 'path';
2
+
3
+ import {ArgvService, PresetService} from '../services';
4
+
5
+ export function getVarsPerFile(filePath: string): Record<string, string> {
6
+ const {vars: argVars} = ArgvService.getConfig();
7
+
8
+ return {
9
+ ...PresetService.get(dirname(filePath)),
10
+ ...argVars,
11
+ };
12
+ }
13
+
14
+ export function getVarsPerRelativeFile(filePath: string): Record<string, string> {
15
+ const {input} = ArgvService.getConfig();
16
+ const root = resolve(input);
17
+ const relativeFilePath = relative(root, filePath);
18
+
19
+ return getVarsPerFile(relativeFilePath);
20
+ }
@@ -0,0 +1,228 @@
1
+ import HTMLElement from 'node-html-parser/dist/nodes/html';
2
+ import {parse} from 'node-html-parser';
3
+ import {resolve, sep, relative} from 'path';
4
+ import {resolveRelativePath} from '@doc-tools/transform/lib/utilsFS';
5
+ import url from 'url';
6
+ import _ from 'lodash';
7
+
8
+ import {isExternalHref} from './url';
9
+
10
+ interface ModifyNode {
11
+ innerHTML: string;
12
+ rawTagName: string;
13
+ attrEntries?: string[][];
14
+ }
15
+
16
+ interface PreprocessSinglePageOptions {
17
+ root: string;
18
+ path: string;
19
+ tocDir: string;
20
+ title?: string;
21
+ }
22
+
23
+ const HEADERS_SELECTOR = 'h1, h2, h3, h4, h5, h6';
24
+
25
+ function getNewNode(options: ModifyNode): HTMLElement | null {
26
+ const {rawTagName, innerHTML, attrEntries} = options;
27
+
28
+ const nodeNew = parse(`<html><${rawTagName}></${rawTagName}></html>`)
29
+ .querySelector(`${rawTagName}`);
30
+
31
+ if (!nodeNew) {
32
+ return null;
33
+ }
34
+
35
+ if (attrEntries) {
36
+ for (const [name, value] of attrEntries) {
37
+ nodeNew.setAttribute(name, value);
38
+ }
39
+ }
40
+
41
+ nodeNew.innerHTML = innerHTML;
42
+
43
+ return nodeNew;
44
+ }
45
+
46
+ export function decreaseHeadingLevels(root: HTMLElement) {
47
+ const headersSelector = 'h1, h2, h3, h4, h5';
48
+
49
+ root.querySelectorAll(headersSelector).forEach((node) => {
50
+ const {rawTagName} = node;
51
+ const newHeadingLevel = Number(rawTagName.charAt(1)) + 1;
52
+
53
+ node.rawTagName = `h${newHeadingLevel}`;
54
+ });
55
+ }
56
+
57
+ export function tryFixFirstPageHeader(root: HTMLElement) {
58
+ const firstPageHeader = root.querySelector(HEADERS_SELECTOR);
59
+ if (!firstPageHeader || firstPageHeader.rawTagName === 'h1') {
60
+ return;
61
+ }
62
+
63
+ firstPageHeader.rawTagName = 'h1';
64
+ }
65
+
66
+
67
+ export function replaceLinks(rootEl: HTMLElement, options: PreprocessSinglePageOptions) {
68
+ const {root, path, tocDir} = options;
69
+
70
+ rootEl.querySelectorAll('a:not(.yfm-anchor):not([target="_blank"])').forEach((node) => {
71
+ const href = node.getAttribute('href') || '';
72
+
73
+ const resolvedPath = resolve(root, path);
74
+ const linkFullPath = resolveRelativePath(resolvedPath, href);
75
+ const isLinkOutOfToc = !linkFullPath.startsWith(tocDir);
76
+
77
+
78
+ let preparedHref = href;
79
+
80
+ if (isLinkOutOfToc) {
81
+ preparedHref = relative(tocDir, linkFullPath);
82
+ } else {
83
+ const {pathname, hash} = url.parse(href);
84
+ if (pathname) {
85
+ preparedHref = getSinglePageAnchorId({
86
+ root,
87
+ currentPath: resolvedPath,
88
+ pathname,
89
+ hash,
90
+ });
91
+ } else if (hash) {
92
+ preparedHref = getSinglePageAnchorId({root, currentPath: resolvedPath, hash});
93
+ }
94
+ }
95
+
96
+
97
+ node.setAttribute('href', preparedHref);
98
+ });
99
+ }
100
+
101
+ export function replaceImages(rootEl: HTMLElement, options: PreprocessSinglePageOptions) {
102
+ const {root, path, tocDir} = options;
103
+
104
+ rootEl.querySelectorAll('img').forEach((node) => {
105
+ const href = node.getAttribute('src') || '';
106
+
107
+ if (isExternalHref(href)) {
108
+ return;
109
+ }
110
+
111
+ const resolvedPath = resolve(root, path);
112
+ const linkFullPath = resolveRelativePath(resolvedPath, href);
113
+ const preparedHref = relative(tocDir, linkFullPath);
114
+
115
+
116
+ node.setAttribute('src', preparedHref);
117
+ });
118
+ }
119
+
120
+
121
+ function prepareAnchorAttr(name: string, value: string, pageId: string) {
122
+ switch (name) {
123
+ case 'href':
124
+ return `#${pageId}_${value.slice(1)}`;
125
+ case 'id':
126
+ return `${pageId}_${value}`;
127
+ default:
128
+ return value;
129
+ }
130
+ }
131
+
132
+ function prepareAnchorAttrs(node: HTMLElement, pageId: string) {
133
+ for (const [name, value] of Object.entries(node.attributes)) {
134
+ const preparedValue = prepareAnchorAttr(name, value, pageId);
135
+
136
+ node.setAttribute(name, preparedValue);
137
+ }
138
+ }
139
+
140
+ export function addPagePrefixToAnchors(rootEl: HTMLElement, options: PreprocessSinglePageOptions) {
141
+ const {root, path} = options;
142
+
143
+ const resolvedPath = resolve(root, path);
144
+ const pageIdAnchor = getSinglePageAnchorId({root, currentPath: resolvedPath});
145
+ const originalArticleHref = transformLinkToOriginalArticle({root, currentPath: resolvedPath});
146
+ const pageId = pageIdAnchor.slice(1);
147
+ const anchorSelector = '.yfm-anchor';
148
+
149
+ // Add the page prefix id to all existing anchors
150
+ rootEl.querySelectorAll(anchorSelector).forEach((node) => {
151
+ prepareAnchorAttrs(node, pageId);
152
+ });
153
+
154
+ const mainHeader = rootEl.querySelector('h1');
155
+ if (mainHeader) {
156
+ const anchor = parse(`<a class="yfm-anchor" aria-hidden="true" href="${pageIdAnchor}" id="${pageId}"></a>`);
157
+ if (!anchor) {
158
+ return;
159
+ }
160
+
161
+ mainHeader.setAttribute('data-original-article', `${originalArticleHref}.html`);
162
+ mainHeader.appendChild(anchor);
163
+ }
164
+
165
+ rootEl.querySelectorAll(HEADERS_SELECTOR).forEach((node) => {
166
+ prepareAnchorAttrs(node, pageId);
167
+ });
168
+ }
169
+
170
+ export function addMainTitle(rootEl: HTMLElement, options: PreprocessSinglePageOptions) {
171
+ const {title} = options;
172
+
173
+ if (!title) {
174
+ return;
175
+ }
176
+
177
+ const mainTitle = getNewNode({innerHTML: title, rawTagName: 'h1'});
178
+
179
+ if (!mainTitle) {
180
+ return;
181
+ }
182
+
183
+ rootEl.insertAdjacentHTML('afterbegin', mainTitle.toString());
184
+ }
185
+
186
+ export function getSinglePageAnchorId(args: {
187
+ root: string;
188
+ currentPath: string;
189
+ pathname?: string;
190
+ hash?: string | null;
191
+ }) {
192
+ const {root, currentPath, pathname, hash} = args;
193
+ let resultAnchor = currentPath;
194
+
195
+ if (pathname) {
196
+ resultAnchor = resolveRelativePath(currentPath, pathname);
197
+ }
198
+
199
+ resultAnchor = resultAnchor
200
+ .replace(root, '')
201
+ .replace(/\.(md|ya?ml|html)$/i, '')
202
+ .replace(new RegExp(_.escapeRegExp(sep), 'gi'), '_');
203
+
204
+ if (hash) {
205
+ resultAnchor = resultAnchor + '_' + hash.slice(1);
206
+ }
207
+
208
+ return `#${resultAnchor}`;
209
+ }
210
+
211
+ export function transformLinkToOriginalArticle(opts: {root: string; currentPath: string}) {
212
+ const {root, currentPath} = opts;
213
+
214
+ return currentPath.replace(root, '').replace(/\.(md|ya?ml|html)$/i, '');
215
+ }
216
+
217
+ export function preprocessPageHtmlForSinglePage(content: string, options: PreprocessSinglePageOptions) {
218
+ const root = parse(content);
219
+
220
+ addMainTitle(root, options);
221
+ tryFixFirstPageHeader(root);
222
+ addPagePrefixToAnchors(root, options);
223
+ decreaseHeadingLevels(root);
224
+ replaceLinks(root, options);
225
+ replaceImages(root, options);
226
+
227
+ return root.toString();
228
+ }
@@ -0,0 +1,87 @@
1
+ import {relative, dirname, basename, extname, format, join} from 'path';
2
+
3
+ import {YfmToc} from '../models';
4
+ import {filterFiles} from '../services/utils';
5
+ import {isExternalHref} from './url';
6
+ import {getSinglePageAnchorId} from './singlePage';
7
+
8
+ export function transformToc(toc: YfmToc | null, pathToFileDirectory: string): YfmToc | null {
9
+ if (!toc) {
10
+ return null;
11
+ }
12
+
13
+ const localToc: YfmToc = JSON.parse(JSON.stringify(toc));
14
+
15
+ if (localToc.items) {
16
+ localToc.items = filterFiles(localToc.items, 'items', {}, {
17
+ removeHiddenTocItems: true,
18
+ });
19
+ }
20
+
21
+ const baseTocPath: string = localToc.base || '';
22
+ const navigationItemQueue = [localToc];
23
+
24
+ while (navigationItemQueue.length) {
25
+ const navigationItem = navigationItemQueue.shift();
26
+
27
+ if (!navigationItem) {
28
+ continue;
29
+ }
30
+
31
+ const {items, href} = navigationItem;
32
+
33
+ if (items) {
34
+ navigationItemQueue.push(...navigationItem.items);
35
+ }
36
+
37
+ if (href && !isExternalHref(href)) {
38
+ /* Path to directory with toc.yaml */
39
+ const pathToIndexDirectory: string = relative(pathToFileDirectory, baseTocPath);
40
+
41
+ const fileExtension: string = extname(href);
42
+ const filename: string = basename(href, fileExtension);
43
+ const transformedFilename: string = format({
44
+ name: filename,
45
+ ext: '.html',
46
+ });
47
+
48
+ navigationItem.href = join(pathToIndexDirectory, dirname(href), transformedFilename);
49
+ }
50
+ }
51
+
52
+ return localToc;
53
+ }
54
+
55
+ export function transformTocForSinglePage(toc: YfmToc | null, options: {root: string; currentPath: string}) {
56
+ const {root, currentPath} = options;
57
+
58
+ if (!toc) {
59
+ return null;
60
+ }
61
+
62
+ const localToc: YfmToc = JSON.parse(JSON.stringify(toc));
63
+
64
+ if (localToc.items) {
65
+ localToc.items = filterFiles(localToc.items, 'items', {}, {
66
+ removeHiddenTocItems: true,
67
+ });
68
+ }
69
+
70
+ function processItems(items: YfmToc[]) {
71
+ items.forEach((item) => {
72
+ if (item.items) {
73
+ processItems(item.items);
74
+ }
75
+
76
+ if (item.href && !isExternalHref(item.href)) {
77
+ item.href = getSinglePageAnchorId({root, currentPath, pathname: item.href});
78
+ }
79
+ });
80
+ }
81
+
82
+ processItems(localToc.items);
83
+
84
+ localToc.singlePage = true;
85
+
86
+ return localToc;
87
+ }
@@ -0,0 +1,3 @@
1
+ export function isExternalHref(href: string) {
2
+ return href.startsWith('http') || href.startsWith('//');
3
+ }
@@ -0,0 +1,10 @@
1
+ export function splitOnChunks<T>(array: T[], chunkSize = 1000) {
2
+ const chunks: (T[])[] = [];
3
+
4
+ for (let i = 0, j = array.length; i < j; i += chunkSize) {
5
+ const chunk: T[] = array.slice(i, i + chunkSize);
6
+ chunks.push(chunk);
7
+ }
8
+
9
+ return chunks;
10
+ }
@@ -0,0 +1,150 @@
1
+ import {Arguments} from 'yargs';
2
+ import {join, resolve} from 'path';
3
+ import {readFileSync} from 'fs';
4
+ import {load} from 'js-yaml';
5
+ import merge from 'lodash/merge';
6
+ import log from '@doc-tools/transform/lib/log';
7
+ import {REDIRECTS_FILENAME, LINT_CONFIG_FILENAME, YFM_CONFIG_FILENAME} from './constants';
8
+ import {ConnectorValidatorProps} from './vcs-connector/connector-models';
9
+
10
+ function notEmptyStringValidator(value: unknown): Boolean {
11
+ if (typeof value === 'string') {
12
+ return Boolean(value);
13
+ }
14
+
15
+ return false;
16
+ }
17
+
18
+ function requiredValueValidator(value: unknown): Boolean {
19
+ return Boolean(value);
20
+ }
21
+
22
+ const validators: Record<string, ConnectorValidatorProps> = {
23
+ 'storageEndpoint': {
24
+ errorMessage: 'Endpoint of S3 storage must be provided when publishes.',
25
+ validateFn: notEmptyStringValidator,
26
+ },
27
+ 'storageBucket': {
28
+ errorMessage: 'Bucket name of S3 storage must be provided when publishes.',
29
+ validateFn: notEmptyStringValidator,
30
+ },
31
+ 'storageKeyId': {
32
+ errorMessage: 'Key Id of S3 storage must be provided when publishes.',
33
+ validateFn: notEmptyStringValidator,
34
+ defaultValue: process.env.YFM_STORAGE_KEY_ID,
35
+ },
36
+ 'storageSecretKey': {
37
+ errorMessage: 'Secret key of S3 storage must be provided when publishes.',
38
+ validateFn: notEmptyStringValidator,
39
+ defaultValue: process.env.YFM_STORAGE_SECRET_KEY,
40
+ },
41
+ 'storageRegion': {
42
+ errorMessage: 'Region of S3 storage must be provided when publishes.',
43
+ validateFn: notEmptyStringValidator,
44
+ defaultValue: 'eu-central-1',
45
+ },
46
+ };
47
+
48
+ interface Redirect {
49
+ from: string;
50
+ to: string;
51
+ }
52
+
53
+ interface RedirectsConfig {
54
+ common: Redirect[];
55
+ [lang: string]: Redirect[];
56
+ }
57
+
58
+ function validateRedirects(redirectsConfig: RedirectsConfig, pathToRedirects: string) {
59
+ const redirects: Redirect[] = Object.keys(redirectsConfig).reduce((res, redirectSectionName) => {
60
+ const sectionRedirects = redirectsConfig[redirectSectionName];
61
+ res.push(...sectionRedirects);
62
+ return res;
63
+ }, [] as Redirect[]);
64
+
65
+ const getContext = (from: string, to: string) => ` [Context: \n- from: ${from}\n- to: ${to} ]`;
66
+ const formatMessage = (message: string, pathname: string, from: string, to: string) => (
67
+ `${pathname}: ${message} ${getContext(from, to)}`
68
+ );
69
+
70
+ redirects.forEach((redirect) => {
71
+ const {from, to} = redirect;
72
+
73
+ if (!from || !to) {
74
+ throw new Error(formatMessage('One of the two parameters is missing', pathToRedirects, from, to));
75
+ }
76
+
77
+ if (from === to) {
78
+ throw new Error(formatMessage('Parameters must be different', pathToRedirects, from, to));
79
+ }
80
+ });
81
+ }
82
+
83
+ export function argvValidator(argv: Arguments<Object>): Boolean {
84
+ try {
85
+ // Combine passed argv and properties from configuration file.
86
+ const pathToConfig = argv.config ? String(argv.config) : join(String(argv.input), YFM_CONFIG_FILENAME);
87
+ const content = readFileSync(resolve(pathToConfig), 'utf8');
88
+ Object.assign(argv, load(content) || {});
89
+ } catch (error) {
90
+ if (error.name === 'YAMLException') {
91
+ log.error(`Error to parse ${YFM_CONFIG_FILENAME}: ${error.message}`);
92
+ }
93
+ }
94
+
95
+ let lintConfig: unknown = {};
96
+ try {
97
+ const pathToConfig = join(String(argv.input), LINT_CONFIG_FILENAME);
98
+ const content = readFileSync(resolve(pathToConfig), 'utf8');
99
+
100
+ lintConfig = load(content) || {};
101
+ } catch (error) {
102
+ if (error.name === 'YAMLException') {
103
+ log.error(`Error to parse ${LINT_CONFIG_FILENAME}: ${error.message}`);
104
+ }
105
+ } finally {
106
+ const preparedLintConfig = merge(lintConfig, {
107
+ 'log-levels': {
108
+ MD033: argv.allowHTML ? 'disabled' : 'error',
109
+ },
110
+ });
111
+
112
+ Object.assign(argv, {lintConfig: preparedLintConfig});
113
+ }
114
+
115
+ try {
116
+ const pathToRedirects = join(String(argv.input), REDIRECTS_FILENAME);
117
+ const redirectsContent = readFileSync(resolve(pathToRedirects), 'utf8');
118
+ const redirects = load(redirectsContent);
119
+
120
+ validateRedirects(redirects as RedirectsConfig, pathToRedirects);
121
+ } catch (error) {
122
+ if (error.name === 'YAMLException') {
123
+ log.error(`Error to parse ${REDIRECTS_FILENAME}: ${error.message}`);
124
+ }
125
+
126
+ if (error.code !== 'ENOENT') {
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ if (argv.publish) {
132
+ for (const [field, validator] of Object.entries(validators)) {
133
+ const value = argv[field] ?? validator.defaultValue;
134
+
135
+ if (!validator) {
136
+ continue;
137
+ }
138
+
139
+ const validateFn = validator.validateFn ?? requiredValueValidator;
140
+
141
+ if (!validateFn(value)) {
142
+ throw new Error(validator.errorMessage);
143
+ }
144
+
145
+ argv[field] = value;
146
+ }
147
+ }
148
+
149
+ return true;
150
+ }