@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,261 @@
1
+ import {
2
+ eachLimit,
3
+ retry,
4
+ asyncify,
5
+ } from 'async';
6
+
7
+ import {dirname, resolve, join} from 'path';
8
+ import {readFile, writeFile, mkdir} from 'fs/promises';
9
+ import {XMLParser} from 'fast-xml-parser';
10
+
11
+ import {Session} from '@yandex-cloud/nodejs-sdk/dist/session';
12
+ import {TranslationServiceClient} from '@yandex-cloud/nodejs-sdk/dist/generated/yandex/cloud/service_clients';
13
+ import {
14
+ TranslateRequest,
15
+ TranslateRequest_Format as Format,
16
+ } from '@yandex-cloud/nodejs-sdk/dist/generated/yandex/cloud/ai/translate/v2/translation_service';
17
+
18
+ const yfm2xliff = require('@doc-tools/yfm2xliff/lib/cjs');
19
+
20
+ import {ArgvService} from '../../services';
21
+ import {getYandexOAuthToken} from '../../packages/credentials';
22
+ import {glob, logger} from '../../utils';
23
+
24
+ import {Argv, Arguments} from 'yargs';
25
+
26
+ import {YandexCloudTranslateGlossaryPair} from '../../models';
27
+
28
+ const composer = async (xliff: string, skeleton: string): Promise<string> => new Promise((res, rej) =>
29
+ yfm2xliff.compose(xliff, skeleton, (err: Error, composed: string) => {
30
+ if (err) {
31
+ rej(err);
32
+ }
33
+
34
+ return res(composed);
35
+ }));
36
+
37
+ const command = 'translate';
38
+
39
+ const description = 'translate documentation with Yandex.Cloud Translator API';
40
+
41
+ const translate = {
42
+ command,
43
+ description,
44
+ handler,
45
+ builder,
46
+ };
47
+
48
+ const MD_GLOB = '**/*.md';
49
+ const REQUESTS_LIMIT = 20;
50
+ const RETRY_LIMIT = 8;
51
+ const MTRANS_LOCALE = 'MTRANS';
52
+
53
+ function builder<T>(argv: Argv<T>) {
54
+ return argv
55
+ .option('source-language', {
56
+ alias: 'sl',
57
+ describe: 'source language code',
58
+ type: 'string',
59
+ })
60
+ .option('target-language', {
61
+ alias: 'tl',
62
+ describe: 'target language code',
63
+ type: 'string',
64
+ })
65
+ .demandOption(
66
+ ['source-language', 'target-language'],
67
+ 'command requires to specify source and target languages');
68
+ }
69
+
70
+ class TranslatorError extends Error {
71
+ path: string;
72
+
73
+ constructor(message: string, path: string) {
74
+ super(message);
75
+
76
+ this.path = path;
77
+ }
78
+ }
79
+
80
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
81
+ async function handler(args: Arguments<any>) {
82
+ ArgvService.init({
83
+ ...args,
84
+ });
85
+
86
+ const {input,
87
+ output,
88
+ yandexCloudTranslateFolderId,
89
+ yandexCloudTranslateGlossaryPairs,
90
+ sl: sourceLanguage,
91
+ tl: targetLanguage,
92
+ } = args;
93
+
94
+ logger.info(input, `translating documentation from ${sourceLanguage} to ${targetLanguage} language`);
95
+
96
+ try {
97
+ let found = [];
98
+
99
+ ({state: {found}} = await glob(join(input, MD_GLOB), {
100
+ nosort: true,
101
+ }));
102
+
103
+ const oauthToken = await getYandexOAuthToken();
104
+
105
+ const translatorParams = {
106
+ input,
107
+ output,
108
+ sourceLanguage,
109
+ targetLanguage,
110
+ yandexCloudTranslateGlossaryPairs,
111
+ folderId: yandexCloudTranslateFolderId,
112
+ oauthToken,
113
+ };
114
+
115
+ const translateFn = translator(translatorParams);
116
+
117
+ await eachLimit(found, REQUESTS_LIMIT, asyncify(translateFn));
118
+ } catch (err) {
119
+ if (err instanceof Error || err instanceof TranslatorError) {
120
+ const message = err.message;
121
+
122
+ const file = err instanceof TranslatorError ? err.path : '';
123
+
124
+ logger.error(file, message);
125
+ }
126
+ }
127
+
128
+ logger.info(output, `translated documentation from ${sourceLanguage} to ${targetLanguage} language`);
129
+ }
130
+
131
+ export type TranslatorParams = {
132
+ oauthToken: string;
133
+ folderId: string;
134
+ input: string;
135
+ output: string;
136
+ sourceLanguage: string;
137
+ targetLanguage: string;
138
+ yandexCloudTranslateGlossaryPairs: YandexCloudTranslateGlossaryPair[];
139
+ };
140
+
141
+ function translator(params: TranslatorParams) {
142
+ const {
143
+ oauthToken,
144
+ folderId,
145
+ input,
146
+ output,
147
+ sourceLanguage,
148
+ targetLanguage,
149
+ yandexCloudTranslateGlossaryPairs,
150
+ } = params;
151
+
152
+ const session = new Session({oauthToken});
153
+ const client = session.client(TranslationServiceClient);
154
+
155
+ return async (mdPath: string) => {
156
+ try {
157
+ logger.info(mdPath, 'translating');
158
+
159
+ const md = await readFile(resolve(mdPath), {encoding: 'utf-8'});
160
+
161
+ const extracted = await yfm2xliff.extract({
162
+ md,
163
+ mdPath,
164
+ source: sourceLanguage,
165
+ target: targetLanguage,
166
+ sklPath: '',
167
+ xlfPath: '',
168
+ });
169
+
170
+ const texts = parseSourcesFromXLIFF(extracted.xliff);
171
+
172
+ const machineTranslateParams = TranslateRequest.fromPartial({
173
+ texts,
174
+ folderId,
175
+ sourceLanguageCode: sourceLanguage,
176
+ targetLanguageCode: targetLanguage,
177
+ glossaryConfig: {
178
+ glossaryData: {
179
+ glossaryPairs: yandexCloudTranslateGlossaryPairs,
180
+ },
181
+ },
182
+ format: Format.PLAIN_TEXT,
183
+ });
184
+
185
+ const translations = await retry({times: RETRY_LIMIT, interval: (count: number) => {
186
+ // eslint-disable-next-line no-bitwise
187
+ return (1 << count) * 1000;
188
+ }}, asyncify(async () =>
189
+ await client.translate(machineTranslateParams)
190
+ .then((results: {translations: {text: string}[]}) =>
191
+ results.translations.map(({text}: {text: string}) => text)),
192
+ ));
193
+
194
+ const createXLIFFDocumentParams = {
195
+ sourceLanguage: sourceLanguage + '-' + MTRANS_LOCALE,
196
+ targetLanguage: targetLanguage + '-' + MTRANS_LOCALE,
197
+ sources: texts,
198
+ targets: translations as string[],
199
+ };
200
+
201
+ const translatedXLIFF = createXLIFFDocument(createXLIFFDocumentParams);
202
+
203
+ const composed = await composer(translatedXLIFF, extracted.skeleton);
204
+
205
+ const outputPath = mdPath.replace(input, output);
206
+
207
+ await mkdir(dirname(outputPath), {recursive: true});
208
+ await writeFile(outputPath, composed);
209
+
210
+ logger.info(outputPath, 'finished translating');
211
+ } catch (err) {
212
+ if (err instanceof Error) {
213
+ throw new TranslatorError(err.toString(), mdPath);
214
+ }
215
+ }
216
+ };
217
+ }
218
+
219
+ function parseSourcesFromXLIFF(xliff: string) {
220
+ const parser = new XMLParser();
221
+
222
+ const inputs = parser.parse(xliff)?.xliff?.file?.body['trans-unit'] ?? [];
223
+
224
+ return Array.isArray(inputs)
225
+ ? inputs.map(({source}: {source: string}) => source)
226
+ : [inputs.source];
227
+ }
228
+
229
+ export type CreateXLIFFDocumentParams = {
230
+ sourceLanguage: string;
231
+ targetLanguage: string;
232
+ sources: string[];
233
+ targets: string[];
234
+ };
235
+
236
+ function createXLIFFDocument(params: CreateXLIFFDocumentParams) {
237
+ const {sourceLanguage, targetLanguage, sources, targets} = params;
238
+
239
+ const unit = (text: string, i: number): string => `
240
+ <trans-unit id="${i + 1}">
241
+ <source xml:lang="${sourceLanguage}">${sources[i]}</source>
242
+ <target xml:lang="${targetLanguage}">${text}</target>
243
+ </trans-unit>`;
244
+
245
+ const doc = `
246
+ <?xml version="1.0" encoding="UTF-8"?>
247
+ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
248
+ <file original="" source-language="${sourceLanguage}" target-language="${targetLanguage}">
249
+ <header>
250
+ <skl><external-file href="" /></skl>
251
+ </header>
252
+ <body>${targets.map(unit)}</body>
253
+ </file>
254
+ </xliff>`;
255
+
256
+ return doc;
257
+ }
258
+
259
+ export {translate};
260
+
261
+ export default {translate};
@@ -0,0 +1,222 @@
1
+ const {promises: {readFile, writeFile, mkdir}} = require('fs');
2
+ import {join, extname, dirname} from 'path';
3
+
4
+ import markdownTranslation, {ComposeParameters} from '@diplodoc/markdown-translation';
5
+ import {Arguments, Argv} from 'yargs';
6
+ import {eachLimit} from 'async';
7
+
8
+ import {ArgvService} from '../../services';
9
+ import {glob, logger} from '../../utils';
10
+
11
+ const command = 'compose';
12
+
13
+ const description = 'compose xliff and skeleton into documentation';
14
+
15
+ const compose = {command, description, handler, builder};
16
+
17
+ const SKL_MD_GLOB = '**/*.skl.md';
18
+ const XLF_GLOB = '**/*.xliff';
19
+ const MAX_CONCURRENCY = 50;
20
+
21
+ class ComposeError extends Error {
22
+ path: string;
23
+
24
+ constructor(message: string, path: string) {
25
+ super(message);
26
+
27
+ this.path = path;
28
+ }
29
+ }
30
+
31
+ const USAGE = 'yfm xliff compose \
32
+ --input <folder-with-xliff-and-skeleton> \
33
+ --ouput <folder-to-store-translated-markdown>';
34
+
35
+ function builder<T>(argv: Argv<T>) {
36
+ return argv
37
+ .option('input', {
38
+ alias: 'i',
39
+ describe: 'input folder with xliff and skeleton files',
40
+ type: 'string',
41
+ }).option('output', {
42
+ alias: 'o',
43
+ describe: 'output folder where translated markdown will be stored',
44
+ type: 'string',
45
+ }).demandOption(['input', 'output'], USAGE);
46
+ }
47
+
48
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
49
+ async function handler(args: Arguments<any>) {
50
+ ArgvService.init({
51
+ ...args,
52
+ });
53
+
54
+ const {input, output} = args;
55
+
56
+ let cache = {};
57
+ let skeletonPaths: string[] = [];
58
+ let xliffPaths: string[] = [];
59
+
60
+ try {
61
+ ({state: {found: skeletonPaths, cache}} = await glob(join(input, SKL_MD_GLOB), {
62
+ nosort: false,
63
+ cache,
64
+ }));
65
+
66
+ ({state: {found: xliffPaths, cache}} = await glob(join(input, XLF_GLOB), {
67
+ nosort: false,
68
+ cache,
69
+ }));
70
+
71
+ if (xliffPaths.length !== skeletonPaths.length) {
72
+ throw new ComposeError('number of xliff and skeleton files does\'not match', input);
73
+ }
74
+ } catch (err) {
75
+ if (err instanceof Error || err instanceof ComposeError) {
76
+ const file = err instanceof ComposeError ? err.path : input;
77
+
78
+ logger.error(file, err.message);
79
+ }
80
+ }
81
+
82
+ const pipelineParameters = {input, output};
83
+ const configuredPipeline = pipeline(pipelineParameters);
84
+
85
+ try {
86
+ logger.info(input, 'staring translated markdown composition pipeline');
87
+
88
+ await eachLimit(xliffPaths, MAX_CONCURRENCY, configuredPipeline);
89
+
90
+ logger.info(input, 'finished translated markdown composition pipeline');
91
+ } catch (err) {
92
+ if (err instanceof Error || err instanceof ComposeError) {
93
+ const file = err instanceof ComposeError ? err.path : input;
94
+
95
+ logger.error(file, err.message);
96
+ }
97
+ }
98
+ }
99
+
100
+ export type PipelineParameters = {
101
+ input: string;
102
+ output: string;
103
+ };
104
+
105
+ function pipeline(params: PipelineParameters) {
106
+ const {input, output} = params;
107
+
108
+ return async (xliffPath: string) => {
109
+ const extension = extname(xliffPath);
110
+ const extensionLessPath = xliffPath.replace(extension, '');
111
+ const skeletonPath = extensionLessPath + '.skl.md';
112
+
113
+ const readerParameters = {xliffPath, skeletonPath};
114
+ const read = await reader(readerParameters);
115
+
116
+ const composerParameters = {
117
+ ...read,
118
+ skeletonPath,
119
+ xliffPath,
120
+ };
121
+ const {markdown} = await composer(composerParameters);
122
+
123
+ const inputRelativePath = extensionLessPath.slice(input.length);
124
+ const markdownPath = join(output, inputRelativePath) + '.md';
125
+
126
+ const writerParameters = {
127
+ markdown,
128
+ markdownPath,
129
+ };
130
+ await writer(writerParameters);
131
+ };
132
+ }
133
+
134
+ export type ReaderParameters = {
135
+ skeletonPath: string;
136
+ xliffPath: string;
137
+ };
138
+
139
+ async function reader(params: ReaderParameters) {
140
+ const {skeletonPath, xliffPath} = params;
141
+
142
+ let skeleton;
143
+ let xlf;
144
+
145
+ try {
146
+ logger.info(skeletonPath, 'reading skeleton file');
147
+
148
+ skeleton = await readFile(skeletonPath, {encoding: 'utf-8'});
149
+
150
+ logger.info(skeletonPath, 'finished reading skeleton file');
151
+ } catch (err) {
152
+ if (err instanceof Error) {
153
+ throw new ComposeError(err.message, skeletonPath);
154
+ }
155
+ }
156
+
157
+ try {
158
+ logger.info(xliffPath, 'reading xliff file');
159
+
160
+ xlf = await readFile(xliffPath, {encoding: 'utf-8'});
161
+
162
+ logger.info(xliffPath, 'finished reading xliff file');
163
+ } catch (err) {
164
+ if (err instanceof Error) {
165
+ throw new ComposeError(err.message, xliffPath);
166
+ }
167
+ }
168
+
169
+ return {skeleton, xlf};
170
+ }
171
+
172
+ export type ComposerParameters = {
173
+ skeletonPath: string;
174
+ xliffPath: string;
175
+ } & ComposeParameters;
176
+
177
+ async function composer(params: ComposerParameters) {
178
+ const {skeletonPath, xliffPath} = params;
179
+ let markdown;
180
+
181
+ try {
182
+ logger.info(skeletonPath, 'composing markdown from xliff and skeleton');
183
+ logger.info(xliffPath, 'composing markdown from xliff and skeleton');
184
+
185
+ markdown = markdownTranslation.compose(params);
186
+
187
+ logger.info(skeletonPath, 'finished composing markdown from xliff and skeleton');
188
+ logger.info(xliffPath, 'finished composing markdown from xliff and skeleton');
189
+ } catch (err) {
190
+ if (err instanceof Error) {
191
+ throw new ComposeError(err.message, `${xliffPath} ${skeletonPath}`);
192
+ }
193
+ }
194
+
195
+ return {markdown};
196
+ }
197
+
198
+ export type WriterParameters = {
199
+ markdown: string;
200
+ markdownPath: string;
201
+ };
202
+
203
+ async function writer(params: WriterParameters) {
204
+ const {markdown, markdownPath} = params;
205
+
206
+ try {
207
+ logger.info(markdownPath, 'writing markdown file');
208
+
209
+ await mkdir(dirname(markdownPath), {recursive: true});
210
+ await writeFile(markdownPath, markdown);
211
+
212
+ logger.info(markdownPath, 'finished writing markdown file');
213
+ } catch (err) {
214
+ if (err instanceof Error) {
215
+ throw new ComposeError(err.message, markdownPath);
216
+ }
217
+ }
218
+ }
219
+
220
+ export {compose};
221
+
222
+ export default {compose};
@@ -0,0 +1,237 @@
1
+ const {promises: {readFile, writeFile, mkdir}} = require('fs');
2
+ import {join, dirname, extname} from 'path';
3
+
4
+ import markdownTranslation, {ExtractParameters} from '@diplodoc/markdown-translation';
5
+ import {Arguments, Argv} from 'yargs';
6
+ import {eachLimit, asyncify} from 'async';
7
+
8
+ import {ArgvService} from '../../services';
9
+ import {glob, logger} from '../../utils';
10
+
11
+ const command = 'extract';
12
+
13
+ const description = 'extract xliff and skeleton from yfm documentation';
14
+
15
+ const extract = {command, description, handler, builder};
16
+
17
+ const MD_GLOB = '**/*.md';
18
+
19
+ const MAX_CONCURRENCY = 50;
20
+
21
+ class ExtractError extends Error {
22
+ path: string;
23
+
24
+ constructor(message: string, path: string) {
25
+ super(message);
26
+
27
+ this.path = path;
28
+ }
29
+ }
30
+
31
+ const USAGE = `yfm xliff extract \
32
+ --input <folder-with-markdown> \
33
+ --output <folder-to-store-xlff-and-skeleton> \
34
+ --sll <source-language>-<source-locale> \
35
+ --tll <target-language>-<target-locale>
36
+
37
+ where <source/target-language> is the language code, as described in ISO 639-1.
38
+
39
+ where <source/target-locale> is the locale code in alpha-2 format, as described in ISO 3166-1`;
40
+
41
+ function builder<T>(argv: Argv<T>) {
42
+ return argv
43
+ .option('source-language-locale', {
44
+ alias: 'sll',
45
+ describe: 'source language and locale',
46
+ type: 'string',
47
+ }).option('target-language-locale', {
48
+ alias: 'tll',
49
+ describe: 'target language and locale',
50
+ type: 'string',
51
+ }).option('input', {
52
+ alias: 'i',
53
+ describe: 'input folder with markdown files',
54
+ type: 'string',
55
+ }).option('output', {
56
+ alias: 'o',
57
+ describe: 'output folder to store xliff and skeleton files',
58
+ type: 'string',
59
+ }).demandOption(['source-language-locale', 'target-language-locale', 'input', 'output'], USAGE);
60
+ }
61
+
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ async function handler(args: Arguments<any>) {
64
+ ArgvService.init({
65
+ ...args,
66
+ });
67
+
68
+ const {input, output, sourceLanguageLocale, targetLanguageLocale} = args;
69
+
70
+ let source;
71
+ let target;
72
+
73
+ try {
74
+ source = parseLanguageLocale(sourceLanguageLocale);
75
+ target = parseLanguageLocale(targetLanguageLocale);
76
+ } catch (err) {
77
+ if (err instanceof Error) {
78
+ logger.error(input, err.message);
79
+ }
80
+ }
81
+
82
+ let cache = {};
83
+ let found: string[] = [];
84
+
85
+ try {
86
+ ({state: {found, cache}} = await glob(join(input, MD_GLOB), {
87
+ nosort: true,
88
+ cache,
89
+ }));
90
+ } catch (err) {
91
+ if (err instanceof Error) {
92
+ logger.error(input, err.message);
93
+ }
94
+ }
95
+
96
+ const pipelineParameters = {source, target, input, output};
97
+ const configuredPipeline = pipeline(pipelineParameters);
98
+
99
+ try {
100
+ logger.info(input, 'starting xliff and skeleton generation pipeline');
101
+
102
+ await eachLimit(found, MAX_CONCURRENCY, asyncify(configuredPipeline));
103
+
104
+ logger.info(input, 'finished xliff and skeleton generation pipeline');
105
+ } catch (err) {
106
+ if (err instanceof Error || err instanceof ExtractError) {
107
+ const file = err instanceof ExtractError ? err.path : input;
108
+
109
+ logger.error(file, err.message);
110
+ }
111
+ }
112
+ }
113
+
114
+ function parseLanguageLocale(languageLocale: string) {
115
+ const [language, locale] = languageLocale.split('-');
116
+ if (language?.length && locale?.length) {
117
+ return {language, locale};
118
+ }
119
+
120
+ throw new Error('invalid language-locale string');
121
+ }
122
+
123
+ export type PipelineParameters = {
124
+ input: string;
125
+ output: string;
126
+ source: ExtractParameters['source'];
127
+ target: ExtractParameters['target'];
128
+ };
129
+
130
+ function pipeline(params: PipelineParameters) {
131
+ const {input, output, source, target} = params;
132
+
133
+ return async (markdownPath: string) => {
134
+ const markdown = await reader({path: markdownPath});
135
+ const extension = extname(markdownPath);
136
+
137
+ const outputRelativePath = markdownPath
138
+ .replace(extension, '')
139
+ .slice(input.length);
140
+
141
+ const outputPath = join(output, outputRelativePath);
142
+ const xlfPath = outputPath + '.xliff';
143
+ const skeletonPath = outputPath + '.skl.md';
144
+
145
+ const extractParameters = {
146
+ markdownPath,
147
+ skeletonPath,
148
+ markdown,
149
+ source,
150
+ target,
151
+ };
152
+
153
+ const extracted = await extractor(extractParameters);
154
+
155
+ const writerParameters = {
156
+ ...extracted,
157
+ xlfPath,
158
+ skeletonPath,
159
+ };
160
+
161
+ await writer(writerParameters);
162
+ };
163
+ }
164
+
165
+ export type ReaderParameters = {
166
+ path: string;
167
+ };
168
+
169
+ async function reader(params: ReaderParameters) {
170
+ const {path} = params;
171
+
172
+ let markdown;
173
+ try {
174
+ logger.info(path, 'reading markdown file');
175
+
176
+ markdown = await readFile(path, {encoding: 'utf-8'});
177
+
178
+ logger.info(path, 'finished reading markdown file');
179
+ } catch (err) {
180
+ if (err instanceof Error) {
181
+ throw new ExtractError(err.message, path);
182
+ }
183
+ }
184
+
185
+ return markdown;
186
+ }
187
+
188
+ export type ExtractorParameters = {
189
+ source: ExtractParameters['source'];
190
+ target: ExtractParameters['target'];
191
+ skeletonPath: string;
192
+ markdownPath: string;
193
+ markdown: string;
194
+ };
195
+
196
+ async function extractor(params: ExtractorParameters) {
197
+ let extracted;
198
+
199
+ logger.info(params.markdownPath, 'generating skeleton and xliff from markdown');
200
+
201
+ try {
202
+ extracted = markdownTranslation.extract(params);
203
+ } catch (err) {
204
+ if (err instanceof Error) {
205
+ throw new ExtractError(err.message, params.markdownPath);
206
+ }
207
+ }
208
+
209
+ logger.info(params.markdownPath, 'finished generating skeleton and xliff from markdown');
210
+
211
+ return extracted;
212
+ }
213
+
214
+ export type WriterParameters = {
215
+ skeletonPath: string;
216
+ skeleton: string;
217
+ xlfPath: string;
218
+ xlf: string;
219
+ };
220
+
221
+ async function writer(params: WriterParameters) {
222
+ const {xlfPath, skeletonPath, xlf, skeleton} = params;
223
+
224
+ logger.info(params.xlfPath, 'writing xliff file');
225
+ logger.info(params.skeletonPath, 'writing skeleton file');
226
+
227
+ await mkdir(dirname(xlfPath), {recursive: true});
228
+
229
+ await Promise.all([writeFile(skeletonPath, skeleton), writeFile(xlfPath, xlf)]);
230
+
231
+ logger.info(params.xlfPath, 'finished writing xliff file');
232
+ logger.info(params.skeletonPath, 'finished writing skeleton file');
233
+ }
234
+
235
+ export {extract};
236
+
237
+ export default {extract};