-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathproject.js
260 lines (223 loc) · 6.65 KB
/
project.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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import util from 'node:util';
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import yaml from 'js-yaml';
import {
parseDirname,
detectLangs,
parseReadmes,
getTitle,
parser,
parseTracks,
} from './common.js';
const getSummary = (rootNode) => {
const headingIdx = rootNode.children.findIndex(node => (
node.type === 'heading'
&& node.depth === 2
&& /(resumen|resumo)/i.test(node.children[0]?.value)
));
if (headingIdx === -1 || rootNode.children[headingIdx + 1].type !== 'paragraph') {
throw new Error('No project summary found');
}
return parser.stringify(parser.runSync(rootNode.children[headingIdx + 1]));
};
export const loadYaml = async fname => (
yaml.load(await readFile(fname, 'utf8'))
);
export const flattenLearningObjectives = (node, prefix = '') => {
if (typeof node === 'string') {
return [`${prefix}${node}`];
}
return Object.keys(node).reduce(
(memo, key) => [
...memo,
`${prefix}${key}`,
...(
Array.isArray(node[key])
? node[key].reduce(
(prev, val) => [
...prev,
...flattenLearningObjectives(val, `${prefix}${key}/`),
],
[],
)
: []
),
],
[],
);
};
const findChildren = (tree, nodePath) => {
// if (!nodePath) {
// return tree;
// }
const parts = nodePath.split('/');
if (parts.length === 1) {
return tree[parts[0]];
}
// each element can be either a string (leaf) or an object (node with children)
const found = tree[parts[0]].find(node => (
typeof node === 'string'
? node === parts[1]
: !!node[parts[1]]
));
if (typeof found === 'string') {
return null; // no children
}
return findChildren(found, parts.slice(1).join('/'));
};
const findFlattenedChildren = (known, learningObjective) => {
const children = findChildren(known, learningObjective);
if (!children) {
return null;
}
const flattened = flattenLearningObjectives({ [learningObjective]: children });
const leavesOnly = flattened.reduce(
(memo, item) => {
const parts = item.split('/');
const parent = parts.length > 1 ? parts.slice(0, -1).join('/') : null;
if (parent) {
return memo.filter(i => i !== parent).concat(item);
}
return memo.concat(item);
},
[],
);
return leavesOnly;
};
// @description este función valida y "aplana" los learning objetives
export const transformLearningObjectives = async (dir, opts, meta = {}) => {
const { learningObjectives, variants } = meta;
if (!learningObjectives) {
// FIXME: Deberíamos ignorar variantes si no hay learning objectives??
return {};
}
const shouldValidate = opts.lo && existsSync(`${opts.lo}/data.yml`);
const known = !shouldValidate ? null : await loadYaml(`${opts.lo}/data.yml`);
const knownFlattened = !shouldValidate ? null : flattenLearningObjectives(known);
const allowedProps = ['id', 'optional', 'exclude'];
const parseLearningObjectives = (arr, isVariant = false) => {
const parsed = arr.map((strOrObj) => {
const obj = (
typeof strOrObj === 'string'
? { id: strOrObj }
: strOrObj
);
if (typeof obj?.id !== 'string') {
throw new Error(
`Invalid learning objective: ${util.inspect(strOrObj)}`,
);
}
if (Object.keys(obj).some(key => !allowedProps.includes(key))) {
throw new Error(
`Invalid learning objective prop: ${util.inspect(strOrObj)}`,
);
}
if (shouldValidate && !knownFlattened.includes(obj.id)) {
throw Object.assign(
new Error(`Unknown learning objectives: ${obj.id}.`),
{ path: meta.__source || dir },
);
}
if (!isVariant && obj.exclude) {
throw new Error('Only variants can have excluded learning objectives');
}
return obj;
});
if (!shouldValidate) {
return parsed;
}
// Expand children when only parent is mentioned?
return parsed.reduce(
({ expanded, ids }, learningObjective) => {
const { id, ...rest } = learningObjective;
const flattenedChildren = findFlattenedChildren(known, id);
const x = (
!flattenedChildren
? [learningObjective]
: parseLearningObjectives(flattenedChildren).map(obj => ({ ...obj, ...rest }))
);
const unique = x.filter(obj => !ids.includes(obj.id));
return {
expanded: expanded.concat(unique),
ids: ids.concat(unique.map(obj => obj.id)),
};
},
{ expanded: [], ids: [] },
).expanded;
};
const parsedLearningObjectives = parseLearningObjectives(learningObjectives);
return {
learningObjectives: parsedLearningObjectives,
variants: variants?.map(variant => ({
...variant,
learningObjectives: parseLearningObjectives(variant.learningObjectives, true)
.filter(({ id, optional, exclude }) => !parsedLearningObjectives.some(
lo => (
lo.id === id
&& !!optional === !!lo.optional
&& !!exclude === !!lo.exclude
),
)),
})),
};
};
const allowedTags = ['featured', 'beta', 'deprecated', 'hidden'];
const parseTags = (tags) => {
if (!tags) {
return null;
}
if (!Array.isArray(tags)) {
throw new Error('Invalid tags');
}
tags.forEach((tag) => {
if (typeof tag !== 'string') {
throw new Error('Invalid tag');
}
if (!allowedTags.includes(tag)) {
throw new Error(`Invalid tag: ${tag}`);
}
});
return tags;
};
export const parseProject = async (dir, opts, pkg) => {
const { prefix, slug } = parseDirname(dir);
const langs = await detectLangs(dir);
const { parsedLocales, meta } = await parseReadmes(
dir,
langs,
'project',
async rootNode => ({
title: getTitle(rootNode),
summary: getSummary(rootNode),
}),
);
const { cover, thumb, tags } = meta;
const { track, tracks } = parseTracks(meta);
const {
learningObjectives,
variants,
} = await transformLearningObjectives(dir, opts, meta) || {};
const parsedTags = parseTags(tags);
return {
slug,
repo: opts.repo,
path: path.relative(process.cwd(), dir),
version: opts.version,
parserVersion: pkg.version,
createdAt: new Date(),
prefix: parseInt(prefix, 10),
track,
tracks,
...(!!parsedTags?.length && { tags: parsedTags }),
...(!!learningObjectives && { learningObjectives }),
...(!!variants && { variants }),
intl: langs.reduce(
(memo, lang, idx) => ({ ...memo, [lang]: parsedLocales[idx] }),
{},
),
cover,
thumb,
};
};