fastbrowser_cli 1.0.37 → 1.0.40
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/dist/contribs/_shared/fastbrowser_helper.d.ts +13 -0
- package/dist/contribs/_shared/fastbrowser_helper.d.ts.map +1 -0
- package/dist/contribs/_shared/fastbrowser_helper.js +44 -0
- package/dist/contribs/_shared/fastbrowser_helper.js.map +1 -0
- package/dist/contribs/linkedin_cli/src/cli.d.ts +3 -0
- package/dist/contribs/linkedin_cli/src/cli.d.ts.map +1 -0
- package/dist/contribs/linkedin_cli/src/cli.js +299 -0
- package/dist/contribs/linkedin_cli/src/cli.js.map +1 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_profile_helper.d.ts +73 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_profile_helper.d.ts.map +1 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_profile_helper.js +866 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_profile_helper.js.map +1 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.d.ts +61 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.d.ts.map +1 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.js +885 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.js.map +1 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_thread_helper.d.ts +11 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_thread_helper.d.ts.map +1 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_thread_helper.js +145 -0
- package/dist/contribs/linkedin_cli/src/libs/linkedin_thread_helper.js.map +1 -0
- package/dist/contribs/twitter_cli/src/cli.d.ts +3 -0
- package/dist/contribs/twitter_cli/src/cli.d.ts.map +1 -0
- package/dist/contribs/twitter_cli/src/cli.js +273 -0
- package/dist/contribs/twitter_cli/src/cli.js.map +1 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_profile_helper.d.ts +28 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_profile_helper.d.ts.map +1 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_profile_helper.js +274 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_profile_helper.js.map +1 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.d.ts +43 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.d.ts.map +1 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.js +519 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.js.map +1 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_thread_helper.d.ts +11 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_thread_helper.d.ts.map +1 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_thread_helper.js +213 -0
- package/dist/contribs/twitter_cli/src/libs/twitter_thread_helper.js.map +1 -0
- package/dist/fastbrowser_cli/fastbrowser_cli.js +43 -0
- package/dist/fastbrowser_cli/fastbrowser_cli.js.map +1 -1
- package/dist/fastbrowser_httpd/libs/tool-schemas.d.ts +4 -0
- package/dist/fastbrowser_httpd/libs/tool-schemas.d.ts.map +1 -1
- package/dist/fastbrowser_httpd/libs/tool-schemas.js +4 -0
- package/dist/fastbrowser_httpd/libs/tool-schemas.js.map +1 -1
- package/dist/fastbrowser_mcp/fastbrowser_mcp.js +36 -2
- package/dist/fastbrowser_mcp/fastbrowser_mcp.js.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts +2 -0
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.js +12 -0
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.js.map +1 -1
- package/dist/fastbrowser_mcp/libs/response_formatter.d.ts +1 -0
- package/dist/fastbrowser_mcp/libs/response_formatter.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/libs/response_formatter.js +27 -0
- package/dist/fastbrowser_mcp/libs/response_formatter.js.map +1 -1
- package/dist/shared/fastbrowser_helper.d.ts +13 -0
- package/dist/shared/fastbrowser_helper.d.ts.map +1 -0
- package/dist/shared/fastbrowser_helper.js +39 -0
- package/dist/shared/fastbrowser_helper.js.map +1 -0
- package/examples/linkedin_cli_TOREMOVE/README.md +7 -0
- package/examples/{linkedin_cli → linkedin_cli_TOREMOVE}/linkedin_dm.sh +8 -4
- package/examples/linkedin_cli_TOREMOVE/linkedin_dm.ts +326 -0
- package/examples/linkedin_cli_TOREMOVE/linkedin_dm_messages.ts +279 -0
- package/examples/linkedin_cli_TOREMOVE/linkedin_full_cycle.sh +5 -0
- package/examples/{linkedin_cli → linkedin_cli_TOREMOVE}/linkedin_post.sh +3 -0
- package/examples/linkedin_cli_TOREMOVE/message_thread.a11y.txt +252 -0
- package/listitem +4 -0
- package/package.json +7 -3
- package/skills/fastbrowser/SKILL.md +33 -25
- package/src/contribs/_shared/fastbrowser_helper.ts +54 -0
- package/src/contribs/linkedin_cli/README.md +80 -0
- package/src/contribs/linkedin_cli/data/linkedin_posts_jeromeetienne.a11y.txt +2364 -0
- package/src/contribs/linkedin_cli/data/linkedin_posts_jontwigge.a11y.txt +2740 -0
- package/src/contribs/linkedin_cli/data/linkedin_posts_julien_guezennec.a11y.txt +2073 -0
- package/src/contribs/linkedin_cli/data/linkedin_profile_jeromeetienne.a11y.txt +1863 -0
- package/src/contribs/linkedin_cli/data/linkedin_profile_jontwigge.a11y.txt +1738 -0
- package/src/contribs/linkedin_cli/data/linkedin_profile_julien_guezennec.a11y.txt +2182 -0
- package/src/contribs/linkedin_cli/src/cli.ts +345 -0
- package/src/contribs/linkedin_cli/src/libs/linkedin_profile_helper.ts +964 -0
- package/src/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.ts +982 -0
- package/src/contribs/linkedin_cli/src/libs/linkedin_thread_helper.ts +171 -0
- package/src/contribs/twitter_cli/README.md +79 -0
- package/src/contribs/twitter_cli/data/twitter_chat.a11y.txt +215 -0
- package/src/contribs/twitter_cli/data/twitter_home.a11y.txt +467 -0
- package/src/contribs/twitter_cli/data/twitter_profile.a11y.txt +418 -0
- package/src/contribs/twitter_cli/data/twitter_profile_jontwigge.a11y.txt +484 -0
- package/src/contribs/twitter_cli/data/twitter_profile_molokoloco.a11y.txt +483 -0
- package/src/contribs/twitter_cli/src/cli.ts +315 -0
- package/src/contribs/twitter_cli/src/libs/twitter_profile_helper.ts +328 -0
- package/src/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.ts +607 -0
- package/src/contribs/twitter_cli/src/libs/twitter_thread_helper.ts +240 -0
- package/src/fastbrowser_cli/fastbrowser_cli.ts +51 -0
- package/src/fastbrowser_httpd/libs/tool-schemas.ts +6 -0
- package/src/fastbrowser_mcp/fastbrowser_mcp.ts +46 -3
- package/src/fastbrowser_mcp/libs/mcp_target_helper.ts +11 -0
- package/src/fastbrowser_mcp/libs/response_formatter.ts +29 -0
- package/src/shared/fastbrowser_helper.ts +49 -0
- package/tsconfig.json +1 -1
- package/examples/mcp_client_playwright.ts +0 -34
- /package/examples/{linkedin_cli → linkedin_cli_TOREMOVE}/linkedin.snapshot.txt +0 -0
- /package/examples/{twitter_cli → twitter_cli_TOREMOVE}/twitter_post.sh +0 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
// npm imports
|
|
2
|
+
import { A11yQuery, A11yTree } from 'a11y_parse';
|
|
3
|
+
const HERO_BUTTON_BLACKLIST = new Set([
|
|
4
|
+
'Resources',
|
|
5
|
+
'Open to',
|
|
6
|
+
'Enhance profile',
|
|
7
|
+
'More',
|
|
8
|
+
'Message',
|
|
9
|
+
'Profile photo',
|
|
10
|
+
'Featured overflow menu',
|
|
11
|
+
'Next',
|
|
12
|
+
'Previous',
|
|
13
|
+
'Add experience',
|
|
14
|
+
'Add education',
|
|
15
|
+
]);
|
|
16
|
+
const DURATION_REGEXP = /\d{4}|Present|\byrs?\b|\bmos?\b/;
|
|
17
|
+
const EMPLOYMENT_TYPES = new Set([
|
|
18
|
+
'Full-time',
|
|
19
|
+
'Part-time',
|
|
20
|
+
'Self-employed',
|
|
21
|
+
'Contract',
|
|
22
|
+
'Freelance',
|
|
23
|
+
'Internship',
|
|
24
|
+
'Apprenticeship',
|
|
25
|
+
'Seasonal',
|
|
26
|
+
]);
|
|
27
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
28
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
29
|
+
//
|
|
30
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
31
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
32
|
+
export class LinkedinProfileHelper {
|
|
33
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
34
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
35
|
+
//
|
|
36
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
37
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
38
|
+
static parseProfile(rawSnapshot, slug) {
|
|
39
|
+
const treeText = LinkedinProfileHelper.extractAxTreeText(rawSnapshot);
|
|
40
|
+
const profile = {
|
|
41
|
+
slug,
|
|
42
|
+
displayName: null,
|
|
43
|
+
headline: null,
|
|
44
|
+
location: null,
|
|
45
|
+
connectionsCount: null,
|
|
46
|
+
followersCount: null,
|
|
47
|
+
about: null,
|
|
48
|
+
website: null,
|
|
49
|
+
currentCompany: null,
|
|
50
|
+
currentEducation: null,
|
|
51
|
+
openToWork: false,
|
|
52
|
+
experience: [],
|
|
53
|
+
education: [],
|
|
54
|
+
};
|
|
55
|
+
if (treeText.length === 0) {
|
|
56
|
+
return profile;
|
|
57
|
+
}
|
|
58
|
+
const root = A11yTree.parse(treeText);
|
|
59
|
+
const heroLink = LinkedinProfileHelper.findHeroProfileLink(root, slug);
|
|
60
|
+
const heroCard = heroLink !== undefined ? LinkedinProfileHelper.findHeroCard(heroLink) : undefined;
|
|
61
|
+
const contactInfoLink = A11yQuery.querySelector(root, 'link[name="Contact info"]');
|
|
62
|
+
profile.displayName = LinkedinProfileHelper.extractDisplayName(heroLink);
|
|
63
|
+
profile.location = LinkedinProfileHelper.extractLocation(contactInfoLink);
|
|
64
|
+
profile.headline = LinkedinProfileHelper.extractHeadline(heroCard, profile.location);
|
|
65
|
+
profile.connectionsCount = LinkedinProfileHelper.extractConnectionsCount(root);
|
|
66
|
+
profile.followersCount = LinkedinProfileHelper.extractFollowersCount(root);
|
|
67
|
+
profile.about = LinkedinProfileHelper.extractAbout(root);
|
|
68
|
+
profile.openToWork = LinkedinProfileHelper.extractOpenToWork(root);
|
|
69
|
+
const heroButtons = heroCard !== undefined ? LinkedinProfileHelper.collectHeroButtons(heroCard) : [];
|
|
70
|
+
profile.website = LinkedinProfileHelper.extractWebsite(root);
|
|
71
|
+
const companyAndSchool = LinkedinProfileHelper.extractCompanyAndSchool(heroButtons);
|
|
72
|
+
profile.currentCompany = companyAndSchool.company;
|
|
73
|
+
profile.currentEducation = companyAndSchool.school;
|
|
74
|
+
profile.experience = LinkedinProfileHelper.extractExperience(root);
|
|
75
|
+
profile.education = LinkedinProfileHelper.extractEducation(root);
|
|
76
|
+
return profile;
|
|
77
|
+
}
|
|
78
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
79
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
80
|
+
//
|
|
81
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
82
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
83
|
+
static formatMarkdown(profile) {
|
|
84
|
+
const headerName = profile.displayName !== null ? profile.displayName : profile.slug;
|
|
85
|
+
const lines = [];
|
|
86
|
+
lines.push(`# ${headerName} (${profile.slug})`);
|
|
87
|
+
const fields = [
|
|
88
|
+
{ label: 'Headline', value: profile.headline },
|
|
89
|
+
{ label: 'Location', value: profile.location },
|
|
90
|
+
{ label: 'Connections', value: profile.connectionsCount },
|
|
91
|
+
{ label: 'Followers', value: profile.followersCount },
|
|
92
|
+
{ label: 'Current', value: profile.currentCompany },
|
|
93
|
+
{ label: 'Education', value: profile.currentEducation },
|
|
94
|
+
{ label: 'Website', value: profile.website },
|
|
95
|
+
];
|
|
96
|
+
for (const field of fields) {
|
|
97
|
+
if (field.value === null) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
lines.push(`- ${field.label}: ${field.value}`);
|
|
101
|
+
}
|
|
102
|
+
if (profile.openToWork === true) {
|
|
103
|
+
lines.push('- Open to work: yes');
|
|
104
|
+
}
|
|
105
|
+
if (profile.about !== null) {
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push('## About');
|
|
108
|
+
lines.push(profile.about);
|
|
109
|
+
}
|
|
110
|
+
if (profile.experience.length > 0) {
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push('## Experience');
|
|
113
|
+
for (const exp of profile.experience) {
|
|
114
|
+
const heading = exp.totalDuration !== null
|
|
115
|
+
? `### ${exp.company ?? ''} — ${exp.totalDuration}`
|
|
116
|
+
: `### ${exp.company ?? ''}`;
|
|
117
|
+
lines.push(heading);
|
|
118
|
+
for (const role of exp.roles) {
|
|
119
|
+
lines.push(LinkedinProfileHelper.formatRoleLine(role));
|
|
120
|
+
if (role.description !== null) {
|
|
121
|
+
lines.push(` ${role.description}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (profile.education.length > 0) {
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push('## Education');
|
|
129
|
+
for (const edu of profile.education) {
|
|
130
|
+
lines.push(LinkedinProfileHelper.formatEducationLine(edu));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
135
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
136
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
137
|
+
//
|
|
138
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
139
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
140
|
+
static resolveSafetyUrl(url) {
|
|
141
|
+
const prefix = 'https://www.linkedin.com/safety/go/?';
|
|
142
|
+
if (url.startsWith(prefix) === false) {
|
|
143
|
+
return url;
|
|
144
|
+
}
|
|
145
|
+
const queryString = url.slice(prefix.length);
|
|
146
|
+
const params = new URLSearchParams(queryString);
|
|
147
|
+
const target = params.get('url');
|
|
148
|
+
if (target === null) {
|
|
149
|
+
return url;
|
|
150
|
+
}
|
|
151
|
+
return target;
|
|
152
|
+
}
|
|
153
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
154
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
155
|
+
// Private — common helpers
|
|
156
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
157
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
158
|
+
static extractAxTreeText(rawOutput) {
|
|
159
|
+
const lines = [];
|
|
160
|
+
for (const line of rawOutput.split('\n')) {
|
|
161
|
+
if (/^\s*uid=/.test(line) === true) {
|
|
162
|
+
lines.push(line);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return lines.join('\n');
|
|
166
|
+
}
|
|
167
|
+
static cleanString(value) {
|
|
168
|
+
if (value === undefined) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const trimmed = value.trim();
|
|
172
|
+
if (trimmed.length === 0) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return trimmed;
|
|
176
|
+
}
|
|
177
|
+
static getValue(node) {
|
|
178
|
+
return LinkedinProfileHelper.cleanString(node.attributes['value']);
|
|
179
|
+
}
|
|
180
|
+
static collectParagraphValues(node, options) {
|
|
181
|
+
const values = [];
|
|
182
|
+
LinkedinProfileHelper.collectParagraphValuesInner(node, options, values);
|
|
183
|
+
return values;
|
|
184
|
+
}
|
|
185
|
+
static collectParagraphValuesInner(node, options, out) {
|
|
186
|
+
for (const child of node.children) {
|
|
187
|
+
if (options.stopAtList === true && child.role === 'list') {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (child.role === 'paragraph') {
|
|
191
|
+
const value = LinkedinProfileHelper.getValue(child);
|
|
192
|
+
if (value !== null) {
|
|
193
|
+
out.push(value);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
LinkedinProfileHelper.collectParagraphValuesInner(child, options, out);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
static collectSubtreeText(node) {
|
|
200
|
+
const parts = [];
|
|
201
|
+
for (const descendant of A11yTree.walk(node)) {
|
|
202
|
+
if (descendant.role === 'StaticText' && descendant.name !== undefined) {
|
|
203
|
+
const trimmed = descendant.name.trim();
|
|
204
|
+
if (trimmed.length > 0) {
|
|
205
|
+
parts.push(trimmed);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (parts.length === 0) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return parts.join(' ').trim();
|
|
213
|
+
}
|
|
214
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
215
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
216
|
+
// Private — hero card identification
|
|
217
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
218
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
219
|
+
static findHeroProfileLink(root, slug) {
|
|
220
|
+
const candidates = A11yQuery.querySelectorAll(root, `link[url="https://www.linkedin.com/in/${slug}/"]`);
|
|
221
|
+
for (const candidate of candidates) {
|
|
222
|
+
const heading = A11yQuery.querySelector(candidate, 'heading[level="2"]');
|
|
223
|
+
if (heading !== undefined) {
|
|
224
|
+
return candidate;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
static findHeroCard(heroLink) {
|
|
230
|
+
// Walk up until we find an ancestor that also contains the contact info link,
|
|
231
|
+
// then go up one more level so the hero card includes the sibling block where
|
|
232
|
+
// LinkedIn renders the current company / school / website buttons.
|
|
233
|
+
let cursor = heroLink.parent;
|
|
234
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
235
|
+
if (cursor === undefined) {
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
const contact = A11yQuery.querySelector(cursor, 'link[name="Contact info"]');
|
|
239
|
+
if (contact !== undefined) {
|
|
240
|
+
return cursor.parent ?? cursor;
|
|
241
|
+
}
|
|
242
|
+
cursor = cursor.parent;
|
|
243
|
+
}
|
|
244
|
+
return heroLink;
|
|
245
|
+
}
|
|
246
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
247
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
248
|
+
// Private — header fields
|
|
249
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
250
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
251
|
+
static extractDisplayName(heroLink) {
|
|
252
|
+
if (heroLink === undefined) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const heading = A11yQuery.querySelector(heroLink, 'heading[level="2"]');
|
|
256
|
+
if (heading === undefined) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
return LinkedinProfileHelper.cleanString(heading.name);
|
|
260
|
+
}
|
|
261
|
+
static extractLocation(contactInfoLink) {
|
|
262
|
+
if (contactInfoLink === undefined) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const grandparent = contactInfoLink.parent?.parent;
|
|
266
|
+
if (grandparent === undefined) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
for (const child of grandparent.children) {
|
|
270
|
+
if (child.role !== 'paragraph') {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const value = LinkedinProfileHelper.getValue(child);
|
|
274
|
+
if (value === null) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (value === '·') {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
static extractHeadline(heroCard, location) {
|
|
285
|
+
if (heroCard === undefined) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
for (const node of A11yTree.walk(heroCard)) {
|
|
289
|
+
if (node.role !== 'paragraph') {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const value = LinkedinProfileHelper.getValue(node);
|
|
293
|
+
if (value === null) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (value.startsWith('·') === true) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (location !== null && value === location) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (/^[\d,]+\+?\s+(followers|connections)$/.test(value) === true) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
static extractConnectionsCount(root) {
|
|
310
|
+
for (const node of A11yTree.walk(root)) {
|
|
311
|
+
if (node.role !== 'paragraph') {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const value = LinkedinProfileHelper.getValue(node);
|
|
315
|
+
if (value === null) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const match = /^([\d,]+\+?) connections$/.exec(value);
|
|
319
|
+
if (match === null) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
return match[1];
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
static extractFollowersCount(root) {
|
|
327
|
+
for (const node of A11yTree.walk(root)) {
|
|
328
|
+
const candidates = [
|
|
329
|
+
node.attributes['value'],
|
|
330
|
+
node.name,
|
|
331
|
+
];
|
|
332
|
+
for (const candidate of candidates) {
|
|
333
|
+
if (candidate === undefined) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const match = /^([\d,]+\+?) followers$/.exec(candidate.trim());
|
|
337
|
+
if (match === null) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
return match[1];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
static extractAbout(root) {
|
|
346
|
+
const heading = LinkedinProfileHelper.findSectionHeading(root, 'About');
|
|
347
|
+
if (heading === undefined) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const section = LinkedinProfileHelper.findSectionBody(heading);
|
|
351
|
+
if (section === undefined) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
return LinkedinProfileHelper.collectSubtreeText(section);
|
|
355
|
+
}
|
|
356
|
+
static extractOpenToWork(root) {
|
|
357
|
+
for (const node of A11yTree.walk(root)) {
|
|
358
|
+
if (node.attributes['value'] === 'Open to work') {
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
365
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
366
|
+
// Private — hero buttons (current company / education / website)
|
|
367
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
368
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
369
|
+
static collectHeroButtons(heroCard) {
|
|
370
|
+
const buttons = [];
|
|
371
|
+
for (const node of A11yTree.walk(heroCard)) {
|
|
372
|
+
if (node.role !== 'button') {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const name = LinkedinProfileHelper.cleanString(node.name);
|
|
376
|
+
if (name === null) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (HERO_BUTTON_BLACKLIST.has(name) === true) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (/notification|Premium|verifications/i.test(name) === true) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
buttons.push(node);
|
|
386
|
+
}
|
|
387
|
+
return buttons;
|
|
388
|
+
}
|
|
389
|
+
static extractWebsite(root) {
|
|
390
|
+
// Walk in document order. The website link sits between the displayName heading
|
|
391
|
+
// and the first "content section" heading (Highlights / About / Analytics / etc.).
|
|
392
|
+
// LinkedIn places it inside a `button` whose child link points to its safety/go redirector.
|
|
393
|
+
const sectionBoundaryNames = new Set([
|
|
394
|
+
'Highlights',
|
|
395
|
+
'About',
|
|
396
|
+
'Analytics',
|
|
397
|
+
'Featured',
|
|
398
|
+
'Activity',
|
|
399
|
+
'Services',
|
|
400
|
+
'Experience',
|
|
401
|
+
'Education',
|
|
402
|
+
]);
|
|
403
|
+
let inHeroRegion = false;
|
|
404
|
+
for (const node of A11yTree.walk(root)) {
|
|
405
|
+
if (node.role === 'heading' && node.attributes['level'] === '2') {
|
|
406
|
+
const name = node.name;
|
|
407
|
+
if (inHeroRegion === false) {
|
|
408
|
+
if (name !== undefined && name !== '0 notifications' && name !== '0 notifications total') {
|
|
409
|
+
inHeroRegion = true;
|
|
410
|
+
}
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (name !== undefined && sectionBoundaryNames.has(name) === true) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (inHeroRegion === false) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (node.role !== 'link') {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const url = node.attributes['url'];
|
|
425
|
+
if (url === undefined) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (url.startsWith('https://www.linkedin.com/safety/go/') === false) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
return LinkedinProfileHelper.resolveSafetyUrl(url);
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
static extractCompanyAndSchool(heroButtons) {
|
|
436
|
+
const result = { company: null, school: null };
|
|
437
|
+
const remaining = [];
|
|
438
|
+
for (const button of heroButtons) {
|
|
439
|
+
const link = A11yQuery.querySelector(button, 'link[url^="https://www.linkedin.com/safety/go/"]');
|
|
440
|
+
if (link !== undefined) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
remaining.push(button);
|
|
444
|
+
}
|
|
445
|
+
if (remaining.length >= 1) {
|
|
446
|
+
result.company = LinkedinProfileHelper.cleanString(remaining[0].name);
|
|
447
|
+
}
|
|
448
|
+
if (remaining.length >= 2) {
|
|
449
|
+
result.school = LinkedinProfileHelper.cleanString(remaining[1].name);
|
|
450
|
+
}
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
454
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
455
|
+
// Private — section helpers
|
|
456
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
457
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
458
|
+
static findSectionHeading(root, name) {
|
|
459
|
+
for (const node of A11yQuery.querySelectorAll(root, 'heading[level="2"]')) {
|
|
460
|
+
if (node.name === name) {
|
|
461
|
+
return node;
|
|
462
|
+
}
|
|
463
|
+
if (node.name !== undefined && node.name.startsWith(`${name} (`) === true) {
|
|
464
|
+
return node;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
static findSectionBody(heading) {
|
|
470
|
+
// Find the first sibling (or sibling-of-ancestor) after the heading that holds
|
|
471
|
+
// real content. Skip over wrappers that only contain action buttons / links
|
|
472
|
+
// (LinkedIn renders Add / Edit controls between the heading and the body on
|
|
473
|
+
// the user's own profile).
|
|
474
|
+
let cursor = heading;
|
|
475
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
476
|
+
if (cursor === undefined) {
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
const parent = cursor.parent;
|
|
480
|
+
if (parent === undefined) {
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
const idx = parent.children.indexOf(cursor);
|
|
484
|
+
for (let i = idx + 1; i < parent.children.length; i++) {
|
|
485
|
+
const sibling = parent.children[i];
|
|
486
|
+
if (sibling.role !== 'generic' && sibling.role !== 'paragraph' && sibling.role !== 'list') {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (LinkedinProfileHelper.isControlContainer(sibling) === true) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
return sibling;
|
|
493
|
+
}
|
|
494
|
+
cursor = parent;
|
|
495
|
+
}
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
static isControlContainer(node) {
|
|
499
|
+
if (node.role !== 'generic') {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
if (node.children.length === 0) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
for (const child of node.children) {
|
|
506
|
+
if (child.role !== 'button' && child.role !== 'link') {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
513
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
514
|
+
// Private — experience
|
|
515
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
516
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
517
|
+
static extractExperience(root) {
|
|
518
|
+
const heading = LinkedinProfileHelper.findSectionHeading(root, 'Experience');
|
|
519
|
+
if (heading === undefined) {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
const body = LinkedinProfileHelper.findSectionBody(heading);
|
|
523
|
+
if (body === undefined) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
const entries = [];
|
|
527
|
+
for (const child of body.children) {
|
|
528
|
+
if (child.role !== 'generic') {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const entry = LinkedinProfileHelper.parseExperienceEntry(child);
|
|
532
|
+
if (entry === null) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
entries.push(entry);
|
|
536
|
+
}
|
|
537
|
+
return entries;
|
|
538
|
+
}
|
|
539
|
+
static parseExperienceEntry(entry) {
|
|
540
|
+
const logoName = LinkedinProfileHelper.findLogoName(entry);
|
|
541
|
+
const directList = LinkedinProfileHelper.findDirectChildList(entry);
|
|
542
|
+
if (directList !== undefined) {
|
|
543
|
+
return LinkedinProfileHelper.parseMultiRoleExperience(entry, directList, logoName);
|
|
544
|
+
}
|
|
545
|
+
return LinkedinProfileHelper.parseSingleRoleExperience(entry, logoName);
|
|
546
|
+
}
|
|
547
|
+
static buildSkipValues(values) {
|
|
548
|
+
const skip = new Set();
|
|
549
|
+
for (const value of values) {
|
|
550
|
+
if (value === null) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
skip.add(value);
|
|
554
|
+
}
|
|
555
|
+
return skip;
|
|
556
|
+
}
|
|
557
|
+
static findLogoName(entry) {
|
|
558
|
+
for (const node of A11yTree.walk(entry)) {
|
|
559
|
+
if (node.role !== 'img') {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const name = LinkedinProfileHelper.cleanString(node.name);
|
|
563
|
+
if (name === null) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (name.endsWith(' logo') === true) {
|
|
567
|
+
return name.slice(0, name.length - ' logo'.length).trim();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
static findDirectChildList(entry) {
|
|
573
|
+
const stack = [...entry.children];
|
|
574
|
+
while (stack.length > 0) {
|
|
575
|
+
const node = stack.shift();
|
|
576
|
+
if (node === undefined) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (node.role === 'list') {
|
|
580
|
+
return node;
|
|
581
|
+
}
|
|
582
|
+
if (node.role === 'listitem' || node.role === 'list') {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
// Only descend through wrappers that are likely to be siblings of a list,
|
|
586
|
+
// not into actual list contents.
|
|
587
|
+
if (node.role === 'generic') {
|
|
588
|
+
stack.push(...node.children);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
593
|
+
static parseMultiRoleExperience(entry, list, logoName) {
|
|
594
|
+
const headerValues = LinkedinProfileHelper.collectParagraphValues(entry, { stopAtList: true });
|
|
595
|
+
const company = headerValues.length > 0 ? headerValues[0] : logoName;
|
|
596
|
+
const totalDuration = headerValues.length > 1 ? headerValues[1] : null;
|
|
597
|
+
const skip = LinkedinProfileHelper.buildSkipValues([company, totalDuration]);
|
|
598
|
+
const roles = [];
|
|
599
|
+
for (const item of list.children) {
|
|
600
|
+
if (item.role !== 'listitem') {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
roles.push(LinkedinProfileHelper.parseRoleEntry(item, skip));
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
company,
|
|
607
|
+
totalDuration,
|
|
608
|
+
roles,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
static parseSingleRoleExperience(entry, logoName) {
|
|
612
|
+
const skip = LinkedinProfileHelper.buildSkipValues([logoName]);
|
|
613
|
+
const role = LinkedinProfileHelper.parseRoleEntry(entry, skip);
|
|
614
|
+
let company = logoName;
|
|
615
|
+
const values = LinkedinProfileHelper.collectParagraphValues(entry, { stopAtList: false });
|
|
616
|
+
// The company name is generally one of the paragraphs near the top.
|
|
617
|
+
// We trust the logo name first, but fall back to a paragraph that looks like a company.
|
|
618
|
+
if (company === null) {
|
|
619
|
+
for (const value of values) {
|
|
620
|
+
if (DURATION_REGEXP.test(value) === true) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (value === role.role) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
company = value;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
company,
|
|
632
|
+
totalDuration: null,
|
|
633
|
+
roles: [role],
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
static parseRoleEntry(entry, skipValues) {
|
|
637
|
+
const values = LinkedinProfileHelper.collectParagraphValues(entry, { stopAtList: true });
|
|
638
|
+
const role = {
|
|
639
|
+
role: null,
|
|
640
|
+
employmentType: null,
|
|
641
|
+
duration: null,
|
|
642
|
+
location: null,
|
|
643
|
+
description: null,
|
|
644
|
+
};
|
|
645
|
+
const consumed = new Set();
|
|
646
|
+
// First non-empty value is the role title.
|
|
647
|
+
for (let i = 0; i < values.length; i++) {
|
|
648
|
+
const value = values[i];
|
|
649
|
+
role.role = value;
|
|
650
|
+
consumed.add(i);
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
// Find duration (matches a date-like pattern).
|
|
654
|
+
for (let i = 0; i < values.length; i++) {
|
|
655
|
+
if (consumed.has(i) === true) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (DURATION_REGEXP.test(values[i]) === true) {
|
|
659
|
+
role.duration = values[i];
|
|
660
|
+
consumed.add(i);
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// Detect employment type, possibly embedded in "Company · Type".
|
|
665
|
+
for (let i = 0; i < values.length; i++) {
|
|
666
|
+
if (consumed.has(i) === true) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const value = values[i];
|
|
670
|
+
const type = LinkedinProfileHelper.extractEmploymentType(value);
|
|
671
|
+
if (type !== null) {
|
|
672
|
+
role.employmentType = type;
|
|
673
|
+
consumed.add(i);
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// Remaining unconsumed values: pick the first one that's not a URL and
|
|
678
|
+
// not a known company / total-duration value already consumed by the caller.
|
|
679
|
+
for (let i = 0; i < values.length; i++) {
|
|
680
|
+
if (consumed.has(i) === true) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
const value = values[i];
|
|
684
|
+
if (value.startsWith('http://') === true || value.startsWith('https://') === true) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (skipValues.has(value) === true) {
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
role.location = value;
|
|
691
|
+
consumed.add(i);
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
role.description = LinkedinProfileHelper.findRoleDescription(entry);
|
|
695
|
+
return role;
|
|
696
|
+
}
|
|
697
|
+
static extractEmploymentType(value) {
|
|
698
|
+
if (EMPLOYMENT_TYPES.has(value) === true) {
|
|
699
|
+
return value;
|
|
700
|
+
}
|
|
701
|
+
const parts = value.split('·').map((s) => s.trim());
|
|
702
|
+
for (const part of parts) {
|
|
703
|
+
if (EMPLOYMENT_TYPES.has(part) === true) {
|
|
704
|
+
return part;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
static findRoleDescription(entry) {
|
|
710
|
+
// Description lives in a StaticText with substantial length, before any "skill chips" link.
|
|
711
|
+
let bestText = null;
|
|
712
|
+
let bestLength = 0;
|
|
713
|
+
for (const node of A11yTree.walk(entry)) {
|
|
714
|
+
if (node.role !== 'StaticText') {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const text = LinkedinProfileHelper.cleanString(node.name);
|
|
718
|
+
if (text === null) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (text.length < 20) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (text.length > bestLength) {
|
|
725
|
+
bestText = text;
|
|
726
|
+
bestLength = text.length;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// Some descriptions live in `paragraph > generic[value]` instead of StaticText.
|
|
730
|
+
if (bestText === null) {
|
|
731
|
+
for (const node of A11yTree.walk(entry)) {
|
|
732
|
+
if (node.role !== 'generic') {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const value = LinkedinProfileHelper.getValue(node);
|
|
736
|
+
if (value === null) {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
if (value.length < 20) {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (value.length > bestLength) {
|
|
743
|
+
bestText = value;
|
|
744
|
+
bestLength = value.length;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return bestText;
|
|
749
|
+
}
|
|
750
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
751
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
752
|
+
// Private — education
|
|
753
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
754
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
755
|
+
static extractEducation(root) {
|
|
756
|
+
const heading = LinkedinProfileHelper.findSectionHeading(root, 'Education');
|
|
757
|
+
if (heading === undefined) {
|
|
758
|
+
return [];
|
|
759
|
+
}
|
|
760
|
+
const body = LinkedinProfileHelper.findSectionBody(heading);
|
|
761
|
+
if (body === undefined) {
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
764
|
+
const entries = [];
|
|
765
|
+
for (const child of body.children) {
|
|
766
|
+
if (child.role !== 'generic') {
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const entry = LinkedinProfileHelper.parseEducationEntry(child);
|
|
770
|
+
if (entry === null) {
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
entries.push(entry);
|
|
774
|
+
}
|
|
775
|
+
return entries;
|
|
776
|
+
}
|
|
777
|
+
static parseEducationEntry(entry) {
|
|
778
|
+
const logoName = LinkedinProfileHelper.findLogoName(entry);
|
|
779
|
+
const values = LinkedinProfileHelper.collectParagraphValues(entry, { stopAtList: false });
|
|
780
|
+
if (values.length === 0 && logoName === null) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
const result = {
|
|
784
|
+
school: null,
|
|
785
|
+
degree: null,
|
|
786
|
+
dates: null,
|
|
787
|
+
};
|
|
788
|
+
const consumed = new Set();
|
|
789
|
+
// Prefer the logo name as school when available, otherwise take the first paragraph.
|
|
790
|
+
if (logoName !== null) {
|
|
791
|
+
result.school = logoName;
|
|
792
|
+
for (let i = 0; i < values.length; i++) {
|
|
793
|
+
if (values[i] === logoName) {
|
|
794
|
+
consumed.add(i);
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
else if (values.length > 0) {
|
|
800
|
+
result.school = values[0];
|
|
801
|
+
consumed.add(0);
|
|
802
|
+
}
|
|
803
|
+
// Find dates (e.g. "1984 – 1988").
|
|
804
|
+
for (let i = 0; i < values.length; i++) {
|
|
805
|
+
if (consumed.has(i) === true) {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (DURATION_REGEXP.test(values[i]) === true) {
|
|
809
|
+
result.dates = values[i];
|
|
810
|
+
consumed.add(i);
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// Degree is the next remaining paragraph.
|
|
815
|
+
for (let i = 0; i < values.length; i++) {
|
|
816
|
+
if (consumed.has(i) === true) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
result.degree = values[i];
|
|
820
|
+
consumed.add(i);
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
return result;
|
|
824
|
+
}
|
|
825
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
826
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
827
|
+
// Private — markdown formatting
|
|
828
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
829
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
830
|
+
static formatRoleLine(role) {
|
|
831
|
+
const parts = [];
|
|
832
|
+
const titleParts = [];
|
|
833
|
+
if (role.role !== null) {
|
|
834
|
+
titleParts.push(role.role);
|
|
835
|
+
}
|
|
836
|
+
if (role.employmentType !== null) {
|
|
837
|
+
titleParts.push(role.employmentType);
|
|
838
|
+
}
|
|
839
|
+
if (titleParts.length > 0) {
|
|
840
|
+
parts.push(titleParts.join(' · '));
|
|
841
|
+
}
|
|
842
|
+
if (role.duration !== null) {
|
|
843
|
+
parts.push(role.duration);
|
|
844
|
+
}
|
|
845
|
+
if (role.location !== null) {
|
|
846
|
+
parts.push(role.location);
|
|
847
|
+
}
|
|
848
|
+
const body = parts.length > 0 ? parts.join(' — ') : '(role)';
|
|
849
|
+
return `- ${body}`;
|
|
850
|
+
}
|
|
851
|
+
static formatEducationLine(edu) {
|
|
852
|
+
const parts = [];
|
|
853
|
+
if (edu.school !== null) {
|
|
854
|
+
parts.push(edu.school);
|
|
855
|
+
}
|
|
856
|
+
if (edu.degree !== null) {
|
|
857
|
+
parts.push(edu.degree);
|
|
858
|
+
}
|
|
859
|
+
if (edu.dates !== null) {
|
|
860
|
+
parts.push(edu.dates);
|
|
861
|
+
}
|
|
862
|
+
const body = parts.length > 0 ? parts.join(' — ') : '(education)';
|
|
863
|
+
return `- ${body}`;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
//# sourceMappingURL=linkedin_profile_helper.js.map
|