fastbrowser_cli 1.0.37 → 1.0.39
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 +39 -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 +49 -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,885 @@
|
|
|
1
|
+
// npm imports
|
|
2
|
+
import { A11yQuery, A11yTree } from 'a11y_parse';
|
|
3
|
+
const TIMESTAMP_SHORT_REGEXP = /^(\d+[smhdw]|\d+\s+(?:second|minute|hour|day|week|month|year)s?)\s*•/i;
|
|
4
|
+
const TIMESTAMP_LONG_REGEXP = /^\d+\s+(?:second|minute|hour|day|week|month|year)s?\s+ago\b/i;
|
|
5
|
+
const ACTIVITY_ID_REGEXP = /urn:li:activity:(\d+)/;
|
|
6
|
+
const COUNT_LEADING_REGEXP = /^(\d+(?:[.,]\d+)?)/;
|
|
7
|
+
const ANALYTICS_LINK_NAME_REGEXP = /^(\d+(?:[.,]\d+)?)\s+impressions/i;
|
|
8
|
+
const NOISE_LITERALS = new Set([
|
|
9
|
+
'·',
|
|
10
|
+
'•',
|
|
11
|
+
'Like',
|
|
12
|
+
'Comment',
|
|
13
|
+
'Repost',
|
|
14
|
+
'Send',
|
|
15
|
+
'Boost',
|
|
16
|
+
'Follow',
|
|
17
|
+
'hashtag',
|
|
18
|
+
'Activate to view larger image,',
|
|
19
|
+
'See content credentials',
|
|
20
|
+
'…more',
|
|
21
|
+
'…',
|
|
22
|
+
'Show translation',
|
|
23
|
+
'reposted this',
|
|
24
|
+
'• You',
|
|
25
|
+
'• 1st',
|
|
26
|
+
'• 2nd',
|
|
27
|
+
'• 3rd',
|
|
28
|
+
'• 3rd+',
|
|
29
|
+
'Verified',
|
|
30
|
+
'Premium',
|
|
31
|
+
'graphical user interface',
|
|
32
|
+
'graphical user interface, application',
|
|
33
|
+
'No alternative text description for this image',
|
|
34
|
+
]);
|
|
35
|
+
const NOISE_PREFIXES = [
|
|
36
|
+
'Promote this post to reach people',
|
|
37
|
+
'Activate to view larger image',
|
|
38
|
+
'Verified •',
|
|
39
|
+
'Premium •',
|
|
40
|
+
'• ',
|
|
41
|
+
'Visible to anyone',
|
|
42
|
+
'Translated from ',
|
|
43
|
+
'This post is not eligible',
|
|
44
|
+
'Open control menu',
|
|
45
|
+
'View ',
|
|
46
|
+
'Loaded ',
|
|
47
|
+
];
|
|
48
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
49
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
50
|
+
//
|
|
51
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
52
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
53
|
+
export class LinkedinRecentPostsHelper {
|
|
54
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
55
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
56
|
+
//
|
|
57
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
58
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
59
|
+
static parsePosts(rawSnapshot, viewingSlug) {
|
|
60
|
+
const treeText = LinkedinRecentPostsHelper.extractAxTreeText(rawSnapshot);
|
|
61
|
+
if (treeText.length === 0) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const root = A11yTree.parse(treeText);
|
|
65
|
+
const headings = A11yQuery.querySelectorAll(root, 'heading[level="2"]');
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
const posts = [];
|
|
68
|
+
for (const heading of headings) {
|
|
69
|
+
if (heading.name === undefined) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (heading.name.startsWith('Feed post number ') === false) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const container = LinkedinRecentPostsHelper.walkUpToPostContainer(heading);
|
|
76
|
+
if (container === undefined) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (seen.has(container.uid) === true) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
seen.add(container.uid);
|
|
83
|
+
posts.push(LinkedinRecentPostsHelper.extractPost(container, heading, viewingSlug));
|
|
84
|
+
}
|
|
85
|
+
return posts;
|
|
86
|
+
}
|
|
87
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
88
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
89
|
+
//
|
|
90
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
91
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
92
|
+
static formatMarkdown(posts) {
|
|
93
|
+
if (posts.length === 0) {
|
|
94
|
+
return '_no posts found_';
|
|
95
|
+
}
|
|
96
|
+
const sections = [];
|
|
97
|
+
for (const post of posts) {
|
|
98
|
+
sections.push(LinkedinRecentPostsHelper.formatPostMarkdown(post));
|
|
99
|
+
}
|
|
100
|
+
return sections.join('\n\n---\n\n');
|
|
101
|
+
}
|
|
102
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
103
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
104
|
+
// Tree filtering / post container walk
|
|
105
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
106
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
107
|
+
static extractAxTreeText(rawOutput) {
|
|
108
|
+
const lines = [];
|
|
109
|
+
for (const line of rawOutput.split('\n')) {
|
|
110
|
+
if (/^\s*uid=/.test(line) === true) {
|
|
111
|
+
lines.push(line);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
static walkUpToPostContainer(heading) {
|
|
117
|
+
let cursor = heading.parent;
|
|
118
|
+
for (let depth = 0; depth < 16; depth++) {
|
|
119
|
+
if (cursor === undefined) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
if (cursor.role === 'listitem') {
|
|
123
|
+
return cursor;
|
|
124
|
+
}
|
|
125
|
+
cursor = cursor.parent;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
130
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
131
|
+
// Per-post extraction
|
|
132
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
133
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
134
|
+
static extractPost(container, heading, viewingSlug) {
|
|
135
|
+
const repost = LinkedinRecentPostsHelper.detectRepost(container);
|
|
136
|
+
const author = LinkedinRecentPostsHelper.extractAuthor(container);
|
|
137
|
+
const timestamp = LinkedinRecentPostsHelper.extractTimestamp(container);
|
|
138
|
+
const activity = LinkedinRecentPostsHelper.extractActivity(container);
|
|
139
|
+
const counts = LinkedinRecentPostsHelper.extractCounts(container);
|
|
140
|
+
const mediaTypes = LinkedinRecentPostsHelper.extractMedia(container);
|
|
141
|
+
const text = LinkedinRecentPostsHelper.extractBodyText(container, heading, {
|
|
142
|
+
authorDisplayName: author.displayName,
|
|
143
|
+
authorHeadline: author.headline,
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
url: activity !== null ? activity.url : null,
|
|
147
|
+
activityId: activity !== null ? activity.activityId : null,
|
|
148
|
+
authorSlug: author.slug,
|
|
149
|
+
authorDisplayName: author.displayName,
|
|
150
|
+
authorHeadline: author.headline,
|
|
151
|
+
timestamp: timestamp.timestamp,
|
|
152
|
+
isEdited: timestamp.isEdited,
|
|
153
|
+
text,
|
|
154
|
+
isRepost: repost.isRepost,
|
|
155
|
+
repostedBy: repost.isRepost === true ? (repost.repostedBy ?? viewingSlug) : null,
|
|
156
|
+
reactionCount: counts.reactionCount,
|
|
157
|
+
commentCount: counts.commentCount,
|
|
158
|
+
repostCount: counts.repostCount,
|
|
159
|
+
impressionCount: counts.impressionCount,
|
|
160
|
+
hasMedia: mediaTypes.length > 0,
|
|
161
|
+
mediaTypes,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
165
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
166
|
+
// Repost detection
|
|
167
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
168
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
169
|
+
static detectRepost(container) {
|
|
170
|
+
const repostedNode = LinkedinRecentPostsHelper.findRepostedThis(container);
|
|
171
|
+
if (repostedNode === undefined) {
|
|
172
|
+
return { isRepost: false, repostedBy: null };
|
|
173
|
+
}
|
|
174
|
+
let repostedBy = null;
|
|
175
|
+
let cursor = repostedNode.parent;
|
|
176
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
177
|
+
if (cursor === undefined) {
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
const link = A11yQuery.querySelector(cursor, 'link[url^="/in/"]');
|
|
181
|
+
if (link !== undefined) {
|
|
182
|
+
const url = link.attributes['url'];
|
|
183
|
+
if (url !== undefined) {
|
|
184
|
+
repostedBy = LinkedinRecentPostsHelper.slugFromInUrl(url);
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
cursor = cursor.parent;
|
|
189
|
+
}
|
|
190
|
+
return { isRepost: true, repostedBy };
|
|
191
|
+
}
|
|
192
|
+
static findRepostedThis(container) {
|
|
193
|
+
for (const node of A11yTree.walk(container)) {
|
|
194
|
+
if (node.role !== 'StaticText') {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (node.name === undefined) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (node.name.trim() === 'reposted this') {
|
|
201
|
+
return node;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
207
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
208
|
+
// Author extraction
|
|
209
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
210
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
211
|
+
static extractAuthor(container) {
|
|
212
|
+
const authorLink = LinkedinRecentPostsHelper.findAuthorLink(container);
|
|
213
|
+
if (authorLink === undefined) {
|
|
214
|
+
return { slug: null, displayName: null, headline: null };
|
|
215
|
+
}
|
|
216
|
+
const url = authorLink.attributes['url'];
|
|
217
|
+
const slug = url !== undefined ? LinkedinRecentPostsHelper.slugFromMiniProfileUrl(url) : null;
|
|
218
|
+
const headerScope = LinkedinRecentPostsHelper.findAuthorHeaderScope(authorLink);
|
|
219
|
+
const values = LinkedinRecentPostsHelper.collectGenericValues(headerScope);
|
|
220
|
+
const dedup = LinkedinRecentPostsHelper.dedupeAdjacent(values);
|
|
221
|
+
const filtered = LinkedinRecentPostsHelper.filterAuthorValues(dedup);
|
|
222
|
+
const displayName = filtered.length >= 1 ? filtered[0] : null;
|
|
223
|
+
const headline = filtered.length >= 2 ? filtered[1] : null;
|
|
224
|
+
return { slug, displayName, headline };
|
|
225
|
+
}
|
|
226
|
+
static findAuthorLink(container) {
|
|
227
|
+
const links = A11yQuery.querySelectorAll(container, 'link[url*="miniProfileUrn"]');
|
|
228
|
+
for (const link of links) {
|
|
229
|
+
const name = link.name;
|
|
230
|
+
if (name === undefined) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (name.startsWith('View ') === true && name.endsWith('graphic link') === true) {
|
|
234
|
+
return link;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (links.length > 0) {
|
|
238
|
+
return links[0];
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
static findAuthorHeaderScope(authorLink) {
|
|
243
|
+
// The author's display name, connection indicator, headline, and timestamp all
|
|
244
|
+
// live in a sibling generic of the profile link. Walk up two levels — the
|
|
245
|
+
// profile link is wrapped twice — to land on the generic that contains all of them.
|
|
246
|
+
let cursor = authorLink.parent;
|
|
247
|
+
for (let depth = 0; depth < 3; depth++) {
|
|
248
|
+
if (cursor === undefined) {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
cursor = cursor.parent;
|
|
252
|
+
}
|
|
253
|
+
return cursor ?? authorLink;
|
|
254
|
+
}
|
|
255
|
+
static collectGenericValues(node) {
|
|
256
|
+
const out = [];
|
|
257
|
+
for (const descendant of A11yTree.walk(node)) {
|
|
258
|
+
if (descendant.role !== 'generic') {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const value = descendant.attributes['value'];
|
|
262
|
+
if (value === undefined) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const trimmed = value.trim();
|
|
266
|
+
if (trimmed.length === 0) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
out.push(trimmed);
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
static dedupeAdjacent(values) {
|
|
274
|
+
const out = [];
|
|
275
|
+
for (const value of values) {
|
|
276
|
+
if (out.length > 0 && out[out.length - 1] === value) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
out.push(value);
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
static filterAuthorValues(values) {
|
|
284
|
+
const out = [];
|
|
285
|
+
for (const value of values) {
|
|
286
|
+
if (LinkedinRecentPostsHelper.isAuthorNoiseValue(value) === true) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
out.push(value);
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
static isAuthorNoiseValue(value) {
|
|
294
|
+
if (value.startsWith('•') === true) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (value.startsWith('Verified •') === true) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
if (value.startsWith('Premium •') === true) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
if (TIMESTAMP_SHORT_REGEXP.test(value) === true) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
if (TIMESTAMP_LONG_REGEXP.test(value) === true) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
if (value === 'Visit my website') {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if (value === 'Follow') {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
if (value === 'You') {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
if (/^\d+\s+followers$/.test(value) === true) {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
// Connection-degree indicators: "1st", "2nd", "3rd", "3rd+".
|
|
322
|
+
if (/^\d+(st|nd|rd|th)\+?$/.test(value) === true) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
static slugFromMiniProfileUrl(url) {
|
|
328
|
+
const match = url.match(/\/in\/([^/?#]+)/);
|
|
329
|
+
if (match === null) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
return match[1];
|
|
333
|
+
}
|
|
334
|
+
static slugFromInUrl(url) {
|
|
335
|
+
const match = url.match(/^\/in\/([^/?#]+)/);
|
|
336
|
+
if (match === null) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
return match[1];
|
|
340
|
+
}
|
|
341
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
342
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
343
|
+
// Timestamp extraction
|
|
344
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
345
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
346
|
+
static extractTimestamp(container) {
|
|
347
|
+
// Prefer the short StaticText (e.g. "1d •" / "12h • Edited •"); fall back to
|
|
348
|
+
// the long generic value (e.g. "1 day ago • Visible to anyone…").
|
|
349
|
+
for (const node of A11yTree.walk(container)) {
|
|
350
|
+
if (node.role !== 'StaticText') {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (node.name === undefined) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const trimmed = node.name.trim();
|
|
357
|
+
if (TIMESTAMP_SHORT_REGEXP.test(trimmed) === false) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
return LinkedinRecentPostsHelper.normalizeTimestamp(trimmed);
|
|
361
|
+
}
|
|
362
|
+
for (const node of A11yTree.walk(container)) {
|
|
363
|
+
if (node.role !== 'generic') {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const value = node.attributes['value'];
|
|
367
|
+
if (value === undefined) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const trimmed = value.trim();
|
|
371
|
+
if (TIMESTAMP_LONG_REGEXP.test(trimmed) === false) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
return LinkedinRecentPostsHelper.normalizeTimestamp(trimmed);
|
|
375
|
+
}
|
|
376
|
+
return { timestamp: null, isEdited: false };
|
|
377
|
+
}
|
|
378
|
+
static normalizeTimestamp(raw) {
|
|
379
|
+
const isEdited = /\bEdited\b/.test(raw);
|
|
380
|
+
// Drop the trailing " • Edited • Visible…" segment — keep just the time portion.
|
|
381
|
+
const segments = raw.split('•').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
382
|
+
let timestamp = segments.length > 0 ? segments[0] : raw.trim();
|
|
383
|
+
// If long form ("1 day ago"), keep as-is. If short form ("1d"), keep as-is.
|
|
384
|
+
timestamp = timestamp.replace(/\s+/g, ' ').trim();
|
|
385
|
+
return { timestamp, isEdited };
|
|
386
|
+
}
|
|
387
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
388
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
389
|
+
// Activity URL / id extraction
|
|
390
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
391
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
392
|
+
static extractActivity(container) {
|
|
393
|
+
const analyticsLink = A11yQuery.querySelector(container, 'link[url^="/analytics/post-summary/urn:li:activity:"]');
|
|
394
|
+
if (analyticsLink !== undefined) {
|
|
395
|
+
const result = LinkedinRecentPostsHelper.activityFromUrl(analyticsLink.attributes['url']);
|
|
396
|
+
if (result !== null) {
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const updateLinks = A11yQuery.querySelectorAll(container, 'link[url^="/feed/update/urn:li:activity:"]');
|
|
401
|
+
for (const link of updateLinks) {
|
|
402
|
+
const result = LinkedinRecentPostsHelper.activityFromUrl(link.attributes['url']);
|
|
403
|
+
if (result !== null) {
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
static activityFromUrl(url) {
|
|
410
|
+
if (url === undefined) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const match = url.match(ACTIVITY_ID_REGEXP);
|
|
414
|
+
if (match === null) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
const activityId = match[1];
|
|
418
|
+
return {
|
|
419
|
+
activityId,
|
|
420
|
+
url: `https://www.linkedin.com/feed/update/urn:li:activity:${activityId}/`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
424
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
425
|
+
// Counts
|
|
426
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
427
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
428
|
+
static extractCounts(container) {
|
|
429
|
+
return {
|
|
430
|
+
reactionCount: LinkedinRecentPostsHelper.extractReactionCount(container),
|
|
431
|
+
commentCount: LinkedinRecentPostsHelper.extractCommentCount(container),
|
|
432
|
+
repostCount: LinkedinRecentPostsHelper.extractRepostCount(container),
|
|
433
|
+
impressionCount: LinkedinRecentPostsHelper.extractImpressionCount(container),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
static extractReactionCount(container) {
|
|
437
|
+
// The reactions list is `list > listitem > button` where the count lives in a
|
|
438
|
+
// nested `generic[value]` matching /^\d+$/ (e.g. button > generic > generic[value="4"]).
|
|
439
|
+
// Pick the first one. The button's name (e.g. "1 reaction" or "Julien and 3 others")
|
|
440
|
+
// is also a fallback when the value isn't present.
|
|
441
|
+
const buttons = A11yQuery.querySelectorAll(container, 'list > listitem > button');
|
|
442
|
+
for (const button of buttons) {
|
|
443
|
+
for (const descendant of A11yTree.walk(button)) {
|
|
444
|
+
if (descendant.uid === button.uid) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (descendant.role !== 'generic') {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const value = descendant.attributes['value'];
|
|
451
|
+
if (value === undefined) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const trimmed = value.trim();
|
|
455
|
+
if (/^\d+$/.test(trimmed) === false) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const parsed = parseInt(trimmed, 10);
|
|
459
|
+
if (Number.isNaN(parsed) === true) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
return parsed;
|
|
463
|
+
}
|
|
464
|
+
if (button.name !== undefined) {
|
|
465
|
+
const match = button.name.match(/^(\d+)\s+reactions?$/i);
|
|
466
|
+
if (match !== null) {
|
|
467
|
+
const parsed = parseInt(match[1], 10);
|
|
468
|
+
if (Number.isNaN(parsed) === false) {
|
|
469
|
+
return parsed;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
static extractCommentCount(container) {
|
|
477
|
+
return LinkedinRecentPostsHelper.findLeadingCount(container, /\bcomments?\s+on\b/i);
|
|
478
|
+
}
|
|
479
|
+
static extractRepostCount(container) {
|
|
480
|
+
return LinkedinRecentPostsHelper.findLeadingCount(container, /\breposts?\s+of\b/i);
|
|
481
|
+
}
|
|
482
|
+
static findLeadingCount(container, namePattern) {
|
|
483
|
+
for (const node of A11yTree.walk(container)) {
|
|
484
|
+
if (node.role !== 'button') {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (node.name === undefined) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (namePattern.test(node.name) === false) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const match = node.name.match(COUNT_LEADING_REGEXP);
|
|
494
|
+
if (match === null) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
const cleaned = match[1].replace(/[.,]/g, '');
|
|
498
|
+
const parsed = parseInt(cleaned, 10);
|
|
499
|
+
if (Number.isNaN(parsed) === true) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
return parsed;
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
static extractImpressionCount(container) {
|
|
507
|
+
const link = A11yQuery.querySelector(container, 'link[url^="/analytics/post-summary/urn:li:activity:"]');
|
|
508
|
+
if (link === undefined) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
if (link.name === undefined) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
const match = link.name.match(ANALYTICS_LINK_NAME_REGEXP);
|
|
515
|
+
if (match === null) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
const cleaned = match[1].replace(/[.,]/g, '');
|
|
519
|
+
const parsed = parseInt(cleaned, 10);
|
|
520
|
+
if (Number.isNaN(parsed) === true) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
return parsed;
|
|
524
|
+
}
|
|
525
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
526
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
527
|
+
// Media detection
|
|
528
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
529
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
530
|
+
static extractMedia(container) {
|
|
531
|
+
const media = [];
|
|
532
|
+
const hasImage = A11yQuery.querySelector(container, 'button[name^="Activate to view larger image"]') !== undefined;
|
|
533
|
+
if (hasImage === true) {
|
|
534
|
+
media.push('image');
|
|
535
|
+
}
|
|
536
|
+
const hasDocument = LinkedinRecentPostsHelper.detectDocumentCarousel(container);
|
|
537
|
+
if (hasDocument === true) {
|
|
538
|
+
media.push('document');
|
|
539
|
+
}
|
|
540
|
+
const hasVideo = LinkedinRecentPostsHelper.detectVideo(container);
|
|
541
|
+
if (hasVideo === true) {
|
|
542
|
+
media.push('video');
|
|
543
|
+
}
|
|
544
|
+
const hasArticle = LinkedinRecentPostsHelper.detectArticleShare(container);
|
|
545
|
+
if (hasArticle === true) {
|
|
546
|
+
media.push('article');
|
|
547
|
+
}
|
|
548
|
+
return media;
|
|
549
|
+
}
|
|
550
|
+
static detectDocumentCarousel(container) {
|
|
551
|
+
for (const node of A11yTree.walk(container)) {
|
|
552
|
+
if (node.role !== 'iframe') {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const progress = A11yQuery.querySelector(node, 'progressbar');
|
|
556
|
+
if (progress !== undefined) {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
static detectVideo(container) {
|
|
563
|
+
for (const node of A11yTree.walk(container)) {
|
|
564
|
+
if (node.role !== 'button') {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (node.name === undefined) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (/play\s+video/i.test(node.name) === true) {
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
static detectArticleShare(container) {
|
|
577
|
+
// LinkedIn renders article previews two ways:
|
|
578
|
+
// (a) a nested `article` element (long-form post / pulse article)
|
|
579
|
+
// (b) an outbound `link[url^="http"]` paired with a sibling `generic[value]`
|
|
580
|
+
// containing the publisher domain (newsletter / blog share)
|
|
581
|
+
const outerArticle = A11yQuery.querySelector(container, 'article');
|
|
582
|
+
const articles = A11yQuery.querySelectorAll(container, 'article');
|
|
583
|
+
for (const article of articles) {
|
|
584
|
+
if (outerArticle !== undefined && article.uid === outerArticle.uid) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
const links = A11yQuery.querySelectorAll(container, 'link');
|
|
590
|
+
for (const link of links) {
|
|
591
|
+
const url = link.attributes['url'];
|
|
592
|
+
if (url === undefined) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (url.startsWith('http://') === false && url.startsWith('https://') === false) {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (LinkedinRecentPostsHelper.isInternalUrl(url) === true) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (LinkedinRecentPostsHelper.linkLooksLikeShareCard(link) === true) {
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
static isInternalUrl(url) {
|
|
608
|
+
if (url.startsWith('https://www.linkedin.com/') === true) {
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
if (url.startsWith('https://lnkd.in/') === true) {
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
static linkLooksLikeShareCard(link) {
|
|
617
|
+
for (const child of link.children) {
|
|
618
|
+
if (child.role !== 'generic') {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
for (const grand of child.children) {
|
|
622
|
+
if (grand.role !== 'generic') {
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const value = grand.attributes['value'];
|
|
626
|
+
if (value === undefined) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (/^[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i.test(value.trim()) === true) {
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
637
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
638
|
+
// Body text extraction
|
|
639
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
640
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
641
|
+
static extractBodyText(container, heading, header) {
|
|
642
|
+
const skipUids = LinkedinRecentPostsHelper.collectBodySkipUids(container, heading);
|
|
643
|
+
const noise = new Set(NOISE_LITERALS);
|
|
644
|
+
if (header.authorDisplayName !== null) {
|
|
645
|
+
noise.add(header.authorDisplayName);
|
|
646
|
+
}
|
|
647
|
+
if (header.authorHeadline !== null) {
|
|
648
|
+
noise.add(header.authorHeadline);
|
|
649
|
+
}
|
|
650
|
+
const fragments = [];
|
|
651
|
+
for (const node of A11yTree.walk(container)) {
|
|
652
|
+
if (skipUids.has(node.uid) === true) {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const fragment = LinkedinRecentPostsHelper.fragmentForNode(node);
|
|
656
|
+
if (fragment === null) {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (noise.has(fragment) === true) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (LinkedinRecentPostsHelper.matchesNoisePrefix(fragment) === true) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
fragments.push(fragment);
|
|
666
|
+
}
|
|
667
|
+
const deduped = LinkedinRecentPostsHelper.dedupeAdjacent(fragments);
|
|
668
|
+
return deduped.join(' ').replace(/\s+/g, ' ').trim();
|
|
669
|
+
}
|
|
670
|
+
static collectBodySkipUids(container, heading) {
|
|
671
|
+
const skipRoots = [];
|
|
672
|
+
// Skip the heading subtree itself.
|
|
673
|
+
skipRoots.push(heading);
|
|
674
|
+
// Skip the author header block — it's the second child of the heading's parent.
|
|
675
|
+
const headingParent = heading.parent;
|
|
676
|
+
if (headingParent !== undefined) {
|
|
677
|
+
const headingIdx = headingParent.children.indexOf(heading);
|
|
678
|
+
if (headingIdx !== -1) {
|
|
679
|
+
const next = headingParent.children[headingIdx + 1];
|
|
680
|
+
if (next !== undefined && next.children.length > 0) {
|
|
681
|
+
const headerBlock = next.children[0];
|
|
682
|
+
skipRoots.push(headerBlock);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Repost banner — the StaticText "reposted this" sits in a generic that
|
|
687
|
+
// includes the page-owner's profile link. Skip the surrounding generic so the
|
|
688
|
+
// link name doesn't leak into the body text.
|
|
689
|
+
const repostedNode = LinkedinRecentPostsHelper.findRepostedThis(container);
|
|
690
|
+
if (repostedNode !== undefined) {
|
|
691
|
+
let cursor = repostedNode.parent;
|
|
692
|
+
for (let depth = 0; depth < 3; depth++) {
|
|
693
|
+
if (cursor === undefined) {
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
const ownerLink = A11yQuery.querySelector(cursor, 'link[url^="/in/"]');
|
|
697
|
+
const photoLink = A11yQuery.querySelector(cursor, 'link[url*="miniProfileUrn"]');
|
|
698
|
+
if (ownerLink !== undefined && photoLink !== undefined) {
|
|
699
|
+
skipRoots.push(cursor);
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
cursor = cursor.parent;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
// Skip subtrees rooted at any button, list, iframe, or nested article.
|
|
706
|
+
const outerArticle = A11yQuery.querySelector(container, 'article');
|
|
707
|
+
for (const node of A11yTree.walk(container)) {
|
|
708
|
+
if (node.role === 'button') {
|
|
709
|
+
skipRoots.push(node);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (node.role === 'list') {
|
|
713
|
+
skipRoots.push(node);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
if (node.role === 'iframe') {
|
|
717
|
+
skipRoots.push(node);
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (node.role === 'alert') {
|
|
721
|
+
skipRoots.push(node);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (node.role === 'article' && outerArticle !== undefined && node.uid !== outerArticle.uid) {
|
|
725
|
+
skipRoots.push(node);
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
// Profile graphic-link inside the post body (author header was already
|
|
729
|
+
// skipped, but inline mentions like `link "Benoit Raphael" url="/in/.../"`
|
|
730
|
+
// should be kept — so don't skip those).
|
|
731
|
+
if (node.role === 'link' && node.attributes['url'] !== undefined) {
|
|
732
|
+
const url = node.attributes['url'];
|
|
733
|
+
if (url.startsWith('/analytics/post-summary/') === true) {
|
|
734
|
+
skipRoots.push(node);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const skipUids = new Set();
|
|
740
|
+
for (const root of skipRoots) {
|
|
741
|
+
for (const descendant of A11yTree.walk(root)) {
|
|
742
|
+
skipUids.add(descendant.uid);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return skipUids;
|
|
746
|
+
}
|
|
747
|
+
static fragmentForNode(node) {
|
|
748
|
+
if (node.role === 'StaticText' && node.name !== undefined) {
|
|
749
|
+
const trimmed = node.name.trim();
|
|
750
|
+
if (trimmed.length === 0) {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
return trimmed;
|
|
754
|
+
}
|
|
755
|
+
const value = node.attributes['value'];
|
|
756
|
+
if (value !== undefined) {
|
|
757
|
+
const trimmed = value.trim();
|
|
758
|
+
if (trimmed.length === 0) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
// Hashtag links wrap a generic value="hashtag" + generic value="#X" — keep
|
|
762
|
+
// only the # variant.
|
|
763
|
+
if (trimmed === 'hashtag') {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
return trimmed;
|
|
767
|
+
}
|
|
768
|
+
if (node.role === 'link' && node.name !== undefined) {
|
|
769
|
+
const trimmed = node.name.trim();
|
|
770
|
+
if (trimmed.length === 0) {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
// If the link wraps a hashtag (its first descendant is generic value="hashtag"),
|
|
774
|
+
// rely on the inner generic value="#X" instead — return null here.
|
|
775
|
+
if (LinkedinRecentPostsHelper.linkIsHashtag(node) === true) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
// If the link has any text descendants (StaticText / generic[value]), they will
|
|
779
|
+
// emit their own fragments — skip the link's own name to avoid duplicates.
|
|
780
|
+
if (LinkedinRecentPostsHelper.linkHasTextDescendant(node) === true) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
return trimmed;
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
static linkIsHashtag(link) {
|
|
788
|
+
for (const child of link.children) {
|
|
789
|
+
if (child.role !== 'generic') {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const value = child.attributes['value'];
|
|
793
|
+
if (value === undefined) {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (value.trim() === 'hashtag') {
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
static linkHasTextDescendant(link) {
|
|
803
|
+
for (const descendant of A11yTree.walk(link)) {
|
|
804
|
+
if (descendant.uid === link.uid) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (descendant.role === 'StaticText' && descendant.name !== undefined && descendant.name.trim().length > 0) {
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
const value = descendant.attributes['value'];
|
|
811
|
+
if (value !== undefined && value.trim().length > 0) {
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
static matchesNoisePrefix(fragment) {
|
|
818
|
+
for (const prefix of NOISE_PREFIXES) {
|
|
819
|
+
if (fragment.startsWith(prefix) === true) {
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
826
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
827
|
+
// Markdown rendering
|
|
828
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
829
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
830
|
+
static formatPostMarkdown(post) {
|
|
831
|
+
const headerName = post.authorDisplayName !== null
|
|
832
|
+
? post.authorDisplayName
|
|
833
|
+
: (post.authorSlug !== null ? post.authorSlug : 'unknown');
|
|
834
|
+
const slugSuffix = post.authorSlug !== null ? ` (${post.authorSlug})` : '';
|
|
835
|
+
const headerParts = [`${headerName}${slugSuffix}`];
|
|
836
|
+
if (post.timestamp !== null) {
|
|
837
|
+
headerParts.push(post.timestamp);
|
|
838
|
+
}
|
|
839
|
+
if (post.isEdited === true) {
|
|
840
|
+
headerParts.push('edited');
|
|
841
|
+
}
|
|
842
|
+
if (post.isRepost === true && post.repostedBy !== null) {
|
|
843
|
+
headerParts.push(`reposted by ${post.repostedBy}`);
|
|
844
|
+
}
|
|
845
|
+
const lines = [];
|
|
846
|
+
lines.push(`## ${headerParts.join(' · ')}`);
|
|
847
|
+
if (post.authorHeadline !== null) {
|
|
848
|
+
lines.push(`_${post.authorHeadline}_`);
|
|
849
|
+
}
|
|
850
|
+
lines.push('');
|
|
851
|
+
if (post.text.length === 0) {
|
|
852
|
+
lines.push('_(no text)_');
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
lines.push(post.text);
|
|
856
|
+
}
|
|
857
|
+
if (post.mediaTypes.length > 0) {
|
|
858
|
+
lines.push('');
|
|
859
|
+
lines.push(`media: ${post.mediaTypes.join(', ')}`);
|
|
860
|
+
}
|
|
861
|
+
const countParts = [];
|
|
862
|
+
if (post.reactionCount !== null) {
|
|
863
|
+
countParts.push(`reactions: ${post.reactionCount}`);
|
|
864
|
+
}
|
|
865
|
+
if (post.commentCount !== null) {
|
|
866
|
+
countParts.push(`comments: ${post.commentCount}`);
|
|
867
|
+
}
|
|
868
|
+
if (post.repostCount !== null) {
|
|
869
|
+
countParts.push(`reposts: ${post.repostCount}`);
|
|
870
|
+
}
|
|
871
|
+
if (post.impressionCount !== null) {
|
|
872
|
+
countParts.push(`impressions: ${post.impressionCount}`);
|
|
873
|
+
}
|
|
874
|
+
if (countParts.length > 0) {
|
|
875
|
+
lines.push('');
|
|
876
|
+
lines.push(countParts.join(' · '));
|
|
877
|
+
}
|
|
878
|
+
if (post.url !== null) {
|
|
879
|
+
lines.push('');
|
|
880
|
+
lines.push(post.url);
|
|
881
|
+
}
|
|
882
|
+
return lines.join('\n');
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
//# sourceMappingURL=linkedin_recent_posts_helper.js.map
|