@symbiosis-lab/moss-plugin-matters 1.4.2
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/CHANGELOG.md +88 -0
- package/README.md +18 -0
- package/assets/icon.svg +1 -0
- package/assets/manifest.json +36 -0
- package/codegen.ts +26 -0
- package/e2e/moss-cli.test.ts +338 -0
- package/features/api/fetch-articles.feature +39 -0
- package/features/auth/wallet-auth.feature +27 -0
- package/features/download/retry-logic.feature +36 -0
- package/features/download/self-correcting.feature +83 -0
- package/features/download/worker-pool.feature +29 -0
- package/features/social/fetch-social-data.feature +40 -0
- package/features/steps/api.steps.ts +180 -0
- package/features/steps/download.steps.ts +423 -0
- package/features/steps/incremental-sync.steps.ts +105 -0
- package/features/steps/self-correcting.steps.ts +575 -0
- package/features/steps/social.steps.ts +257 -0
- package/features/steps/syndication.steps.ts +264 -0
- package/features/steps/wallet-auth.steps.ts +185 -0
- package/features/sync/article-sync.feature +49 -0
- package/features/sync/homepage-grid.feature +43 -0
- package/features/sync/incremental-sync.feature +28 -0
- package/features/syndication/create-draft.feature +35 -0
- package/package.json +58 -0
- package/src/__generated__/schema.graphql +4289 -0
- package/src/__generated__/types.ts +5355 -0
- package/src/__tests__/api.test.ts +678 -0
- package/src/__tests__/auth-route.test.ts +38 -0
- package/src/__tests__/auth-routing.test.ts +462 -0
- package/src/__tests__/auto-detect.test.ts +412 -0
- package/src/__tests__/binding-guard.test.ts +256 -0
- package/src/__tests__/config.test.ts +212 -0
- package/src/__tests__/converter.test.ts +289 -0
- package/src/__tests__/credential.test.ts +332 -0
- package/src/__tests__/domain.test.ts +341 -0
- package/src/__tests__/downloader.test.ts +679 -0
- package/src/__tests__/folder-detection.test.ts +289 -0
- package/src/__tests__/force-fresh-login.test.ts +236 -0
- package/src/__tests__/main.test.ts +2437 -0
- package/src/__tests__/progress.test.ts +93 -0
- package/src/__tests__/session.test.ts +375 -0
- package/src/__tests__/social-integration.test.ts +386 -0
- package/src/__tests__/social-sync-logic.test.ts +107 -0
- package/src/__tests__/social.test.ts +788 -0
- package/src/__tests__/sync.test.ts +1273 -0
- package/src/__tests__/syndication-toast-law.test.ts +649 -0
- package/src/__tests__/syndication.test.ts +125 -0
- package/src/__tests__/test-profile-escape.test.ts +209 -0
- package/src/__tests__/url-detect.test.ts +79 -0
- package/src/__tests__/utils.test.ts +226 -0
- package/src/api.ts +1366 -0
- package/src/auth-route.ts +38 -0
- package/src/config.ts +80 -0
- package/src/converter.ts +305 -0
- package/src/credential.ts +329 -0
- package/src/domain.ts +183 -0
- package/src/downloader.ts +761 -0
- package/src/main.ts +2092 -0
- package/src/progress.ts +89 -0
- package/src/queries/user.graphql +85 -0
- package/src/queries/viewer.graphql +104 -0
- package/src/social.ts +413 -0
- package/src/sync.ts +818 -0
- package/src/types.ts +477 -0
- package/src/url-detect.ts +49 -0
- package/src/utils.ts +305 -0
- package/test-fixtures/syndication-test-site/input/index.md +8 -0
- package/test-fixtures/syndication-test-site/input/posts/rich-test-article.md +90 -0
- package/test-helpers/TEST_ACCOUNT.md +151 -0
- package/test-helpers/api-client.ts +252 -0
- package/test-helpers/fixtures/articles.ts +147 -0
- package/test-helpers/wallet-auth.ts +305 -0
- package/test-setup/e2e.ts +93 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +39 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-specific type definitions for the Matters Syndicator Plugin
|
|
3
|
+
*
|
|
4
|
+
* Common types (ProcessContext, SyndicateContext, HookResult, etc.) are
|
|
5
|
+
* imported from moss-plugin-sdk.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export SDK types for convenience
|
|
9
|
+
export type {
|
|
10
|
+
ProcessContext,
|
|
11
|
+
SyndicateContext,
|
|
12
|
+
HookResult,
|
|
13
|
+
DeploymentInfo,
|
|
14
|
+
ProjectInfo,
|
|
15
|
+
ArticleInfo,
|
|
16
|
+
PluginMessage,
|
|
17
|
+
} from "@symbiosis-lab/moss-api";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Matters API Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
export interface PageInfo {
|
|
24
|
+
endCursor: string;
|
|
25
|
+
hasNextPage: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MattersTag {
|
|
29
|
+
id: string;
|
|
30
|
+
content: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MattersArticle {
|
|
34
|
+
id: string;
|
|
35
|
+
title: string;
|
|
36
|
+
slug: string;
|
|
37
|
+
shortHash: string;
|
|
38
|
+
content: string; // HTML content
|
|
39
|
+
summary: string;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
revisedAt?: string;
|
|
42
|
+
tags: MattersTag[];
|
|
43
|
+
cover?: string;
|
|
44
|
+
language?: string; // e.g. "zh_hans" / "zh_hant" / "en" — public per-article language (G)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface MattersDraft {
|
|
48
|
+
id: string;
|
|
49
|
+
title: string;
|
|
50
|
+
content: string; // HTML content
|
|
51
|
+
summary?: string;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
updatedAt?: string;
|
|
54
|
+
tags?: string[];
|
|
55
|
+
cover?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface MattersCollectionArticle {
|
|
59
|
+
id: string;
|
|
60
|
+
shortHash: string;
|
|
61
|
+
title: string;
|
|
62
|
+
slug: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface MattersCollection {
|
|
66
|
+
id: string;
|
|
67
|
+
title: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
cover?: string;
|
|
70
|
+
articles: MattersCollectionArticle[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface MattersPinnedWork {
|
|
74
|
+
id: string;
|
|
75
|
+
type: "article" | "collection";
|
|
76
|
+
title: string;
|
|
77
|
+
slug?: string; // articles only
|
|
78
|
+
shortHash?: string; // articles only
|
|
79
|
+
cover?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface MattersUserProfile {
|
|
83
|
+
userName: string;
|
|
84
|
+
displayName: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
avatar?: string;
|
|
87
|
+
profileCover?: string;
|
|
88
|
+
language?: string; // e.g., "zh_hans", "zh_hant", "en"
|
|
89
|
+
pinnedWorks?: MattersPinnedWork[]; // optional for backwards compat
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Internal Types
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
export interface SyncResult {
|
|
97
|
+
created: number;
|
|
98
|
+
updated: number;
|
|
99
|
+
skipped: number;
|
|
100
|
+
errors: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extended sync result that includes the article path map for link rewriting.
|
|
105
|
+
* The articlePathMap maps Matters URLs and shortHashes to local file paths.
|
|
106
|
+
*/
|
|
107
|
+
export interface SyncResultWithMap {
|
|
108
|
+
result: SyncResult;
|
|
109
|
+
articlePathMap: Map<string, string>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface MediaDownloadResult {
|
|
113
|
+
filesProcessed: number;
|
|
114
|
+
imagesDownloaded: number;
|
|
115
|
+
imagesSkipped: number;
|
|
116
|
+
errors: string[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface DownloadAndRewriteResult {
|
|
120
|
+
content: string;
|
|
121
|
+
downloadedCount: number;
|
|
122
|
+
errors: string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ExtractedMedia {
|
|
126
|
+
url: string;
|
|
127
|
+
localFilename: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface ParsedFrontmatter {
|
|
131
|
+
frontmatter: Record<string, unknown>;
|
|
132
|
+
body: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// GraphQL Response Types
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
export interface ViewerArticlesResponse {
|
|
140
|
+
viewer: {
|
|
141
|
+
id: string;
|
|
142
|
+
userName: string;
|
|
143
|
+
articles: {
|
|
144
|
+
totalCount: number;
|
|
145
|
+
pageInfo: PageInfo;
|
|
146
|
+
edges: Array<{
|
|
147
|
+
node: MattersArticle;
|
|
148
|
+
}>;
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface ViewerDraftsResponse {
|
|
154
|
+
viewer: {
|
|
155
|
+
id: string;
|
|
156
|
+
drafts: {
|
|
157
|
+
pageInfo: PageInfo;
|
|
158
|
+
edges: Array<{
|
|
159
|
+
node: MattersDraft;
|
|
160
|
+
}>;
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ViewerCollectionsResponse {
|
|
166
|
+
viewer: {
|
|
167
|
+
id: string;
|
|
168
|
+
collections: {
|
|
169
|
+
totalCount: number;
|
|
170
|
+
pageInfo: PageInfo;
|
|
171
|
+
edges: Array<{
|
|
172
|
+
node: {
|
|
173
|
+
id: string;
|
|
174
|
+
title: string;
|
|
175
|
+
description?: string;
|
|
176
|
+
cover?: string;
|
|
177
|
+
articles: {
|
|
178
|
+
edges: Array<{
|
|
179
|
+
node: MattersCollectionArticle;
|
|
180
|
+
}>;
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
}>;
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface ViewerProfileResponse {
|
|
189
|
+
viewer: {
|
|
190
|
+
id: string;
|
|
191
|
+
userName: string;
|
|
192
|
+
displayName: string;
|
|
193
|
+
info: {
|
|
194
|
+
description?: string;
|
|
195
|
+
profileCover?: string;
|
|
196
|
+
};
|
|
197
|
+
avatar?: string;
|
|
198
|
+
settings: {
|
|
199
|
+
language?: string;
|
|
200
|
+
};
|
|
201
|
+
pinnedWorks?: Array<{
|
|
202
|
+
id: string;
|
|
203
|
+
pinned: boolean;
|
|
204
|
+
title: string;
|
|
205
|
+
cover?: string;
|
|
206
|
+
__typename?: string;
|
|
207
|
+
slug?: string;
|
|
208
|
+
shortHash?: string;
|
|
209
|
+
}>;
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// Frontmatter Data Types
|
|
215
|
+
// ============================================================================
|
|
216
|
+
|
|
217
|
+
export interface FrontmatterData {
|
|
218
|
+
title: string;
|
|
219
|
+
/** Marks this file as its folder's home page (moss `home: true` marker). */
|
|
220
|
+
home?: boolean;
|
|
221
|
+
date?: string;
|
|
222
|
+
updated?: string;
|
|
223
|
+
tags?: string[];
|
|
224
|
+
cover?: string;
|
|
225
|
+
syndicated?: string[];
|
|
226
|
+
description?: string;
|
|
227
|
+
collections?: Record<string, number> | string[];
|
|
228
|
+
order?: string[]; // For ordered folders: list of article filenames
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// Social Data Types (for .moss/data/social/matters.json)
|
|
233
|
+
// ============================================================================
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* User information for social interactions
|
|
237
|
+
*/
|
|
238
|
+
export interface SocialUser {
|
|
239
|
+
id: string;
|
|
240
|
+
userName: string;
|
|
241
|
+
displayName: string;
|
|
242
|
+
avatar?: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Comment on an article
|
|
247
|
+
*/
|
|
248
|
+
export interface MattersComment {
|
|
249
|
+
id: string;
|
|
250
|
+
content: string;
|
|
251
|
+
createdAt: string;
|
|
252
|
+
state: "active" | "archived" | "banned" | "collapsed";
|
|
253
|
+
upvotes: number;
|
|
254
|
+
author: SocialUser;
|
|
255
|
+
replyToId?: string;
|
|
256
|
+
replyToAuthor?: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Donation to an article
|
|
261
|
+
*/
|
|
262
|
+
export interface MattersDonation {
|
|
263
|
+
id: string;
|
|
264
|
+
sender: SocialUser;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Appreciation (claps) for an article
|
|
269
|
+
*/
|
|
270
|
+
export interface MattersAppreciation {
|
|
271
|
+
amount: number;
|
|
272
|
+
createdAt: string;
|
|
273
|
+
sender: SocialUser;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Social data for a single article
|
|
278
|
+
*/
|
|
279
|
+
export interface ArticleSocialData {
|
|
280
|
+
comments: MattersComment[];
|
|
281
|
+
donations: MattersDonation[];
|
|
282
|
+
appreciations: MattersAppreciation[];
|
|
283
|
+
/**
|
|
284
|
+
* commentCount as reported by Matters at the last successful sync.
|
|
285
|
+
* Used to skip per-article fetches when the remote count hasn't changed.
|
|
286
|
+
* Undefined for entries written before this field existed (treated as
|
|
287
|
+
* "unknown" — sync will fetch as before, then populate this).
|
|
288
|
+
*/
|
|
289
|
+
lastKnownCommentCount?: number;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Complete social data stored in .moss/data/social/matters.json
|
|
294
|
+
*
|
|
295
|
+
* Schema:
|
|
296
|
+
* - schemaVersion: Version string for future migrations (currently "1.0.0")
|
|
297
|
+
* - updatedAt: ISO timestamp of last update
|
|
298
|
+
* - articles: Map of source .md path (project-relative) to social data
|
|
299
|
+
*
|
|
300
|
+
* Merge strategy: Upsert by ID (add new, update existing, never delete)
|
|
301
|
+
*/
|
|
302
|
+
export interface MattersSocialData {
|
|
303
|
+
/** Schema version for forward compatibility */
|
|
304
|
+
schemaVersion: string;
|
|
305
|
+
/** ISO timestamp of last update */
|
|
306
|
+
updatedAt: string;
|
|
307
|
+
/** Social data keyed by source .md path (project-relative) */
|
|
308
|
+
articles: Record<string, ArticleSocialData>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// Lightweight Comment-Count Discovery Types
|
|
313
|
+
// ============================================================================
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Minimal article node for the "which articles have new comments?" query.
|
|
317
|
+
* Just enough to join against local social data and compare counts.
|
|
318
|
+
*/
|
|
319
|
+
export interface ArticleCommentCount {
|
|
320
|
+
shortHash: string;
|
|
321
|
+
commentCount: number;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export interface ViewerArticleCommentCountsResponse {
|
|
325
|
+
viewer: {
|
|
326
|
+
id: string;
|
|
327
|
+
articles: {
|
|
328
|
+
pageInfo: PageInfo;
|
|
329
|
+
edges: Array<{ node: ArticleCommentCount }>;
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface UserArticleCommentCountsResponse {
|
|
335
|
+
user: {
|
|
336
|
+
id: string;
|
|
337
|
+
articles: {
|
|
338
|
+
pageInfo: PageInfo;
|
|
339
|
+
edges: Array<{ node: ArticleCommentCount }>;
|
|
340
|
+
} | null;
|
|
341
|
+
} | null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// Social Data GraphQL Response Types
|
|
346
|
+
// ============================================================================
|
|
347
|
+
|
|
348
|
+
export interface ArticleCommentsResponse {
|
|
349
|
+
article: {
|
|
350
|
+
id: string;
|
|
351
|
+
shortHash: string;
|
|
352
|
+
comments: {
|
|
353
|
+
totalCount: number;
|
|
354
|
+
pageInfo: PageInfo;
|
|
355
|
+
edges: Array<{
|
|
356
|
+
node: {
|
|
357
|
+
id: string;
|
|
358
|
+
content: string;
|
|
359
|
+
createdAt: string;
|
|
360
|
+
state: string;
|
|
361
|
+
upvotes: number;
|
|
362
|
+
author: {
|
|
363
|
+
id: string;
|
|
364
|
+
userName: string;
|
|
365
|
+
displayName: string;
|
|
366
|
+
avatar?: string;
|
|
367
|
+
};
|
|
368
|
+
replyTo?: {
|
|
369
|
+
id: string;
|
|
370
|
+
author: {
|
|
371
|
+
userName: string;
|
|
372
|
+
};
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
}>;
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export interface ArticleDonationsResponse {
|
|
381
|
+
article: {
|
|
382
|
+
id: string;
|
|
383
|
+
shortHash: string;
|
|
384
|
+
donations: {
|
|
385
|
+
totalCount: number;
|
|
386
|
+
pageInfo: PageInfo;
|
|
387
|
+
edges: Array<{
|
|
388
|
+
node: {
|
|
389
|
+
id: string;
|
|
390
|
+
sender: {
|
|
391
|
+
id: string;
|
|
392
|
+
userName: string;
|
|
393
|
+
displayName: string;
|
|
394
|
+
avatar?: string;
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
}>;
|
|
398
|
+
};
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export interface ArticleAppreciationsResponse {
|
|
403
|
+
article: {
|
|
404
|
+
id: string;
|
|
405
|
+
shortHash: string;
|
|
406
|
+
appreciationsReceived: {
|
|
407
|
+
totalCount: number;
|
|
408
|
+
pageInfo: PageInfo;
|
|
409
|
+
edges: Array<{
|
|
410
|
+
node: {
|
|
411
|
+
amount: number;
|
|
412
|
+
createdAt: string;
|
|
413
|
+
sender: {
|
|
414
|
+
id: string;
|
|
415
|
+
userName: string;
|
|
416
|
+
displayName: string;
|
|
417
|
+
avatar?: string;
|
|
418
|
+
};
|
|
419
|
+
};
|
|
420
|
+
}>;
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ============================================================================
|
|
426
|
+
// Draft/Syndication Types
|
|
427
|
+
// ============================================================================
|
|
428
|
+
|
|
429
|
+
export interface MattersDraftWithArticle extends MattersDraft {
|
|
430
|
+
/** Present when draft has been published */
|
|
431
|
+
article?: {
|
|
432
|
+
id: string;
|
|
433
|
+
shortHash: string;
|
|
434
|
+
slug: string;
|
|
435
|
+
};
|
|
436
|
+
publishState: "unpublished" | "pending" | "published";
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export interface PutDraftInput {
|
|
440
|
+
id?: string;
|
|
441
|
+
title?: string;
|
|
442
|
+
content?: string;
|
|
443
|
+
summary?: string;
|
|
444
|
+
tags?: string[];
|
|
445
|
+
cover?: string;
|
|
446
|
+
collections?: string[];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export interface PutDraftResponse {
|
|
450
|
+
putDraft: MattersDraftWithArticle;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export interface PublishArticleResponse {
|
|
454
|
+
publishArticle: {
|
|
455
|
+
id: string;
|
|
456
|
+
article: {
|
|
457
|
+
id: string;
|
|
458
|
+
shortHash: string;
|
|
459
|
+
slug: string;
|
|
460
|
+
};
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export interface PutCollectionInput {
|
|
465
|
+
id?: string;
|
|
466
|
+
title?: string;
|
|
467
|
+
cover?: string;
|
|
468
|
+
description?: string;
|
|
469
|
+
pinned?: boolean;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export interface PutCollectionResponse {
|
|
473
|
+
putCollection: {
|
|
474
|
+
id: string;
|
|
475
|
+
title: string;
|
|
476
|
+
};
|
|
477
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure URL classifiers for the Matters browser-panel.
|
|
3
|
+
*
|
|
4
|
+
* The published-article URL is only a TRIGGER; the Matters API (draft.article)
|
|
5
|
+
* remains the source of truth for confirming a publish. These functions never
|
|
6
|
+
* confirm a publish on their own — they are hints to fire an immediate API
|
|
7
|
+
* verify instead of waiting for the next 5s poll cycle.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract the pathname from a URL string. Returns "" on parse failure.
|
|
12
|
+
*/
|
|
13
|
+
function pathOf(url: string): string {
|
|
14
|
+
try {
|
|
15
|
+
return new URL(url).pathname;
|
|
16
|
+
} catch {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns true when the URL points to the Matters draft editor:
|
|
23
|
+
* `…/me/drafts/<id>`.
|
|
24
|
+
*/
|
|
25
|
+
export function isDraftUrl(url: string): boolean {
|
|
26
|
+
return /^\/me\/drafts\/[^/]+/.test(pathOf(url));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns true when the URL looks like a published Matters article:
|
|
31
|
+
* `/@<user>/<slug>-<shortHash>`.
|
|
32
|
+
*
|
|
33
|
+
* The hash suffix requirement (`-[a-z0-9]{6,}` at the end of the slug) rejects
|
|
34
|
+
* profile sub-pages like `/@user/followers`, `/@user/settings`,
|
|
35
|
+
* `/@user/bookmarks`, and bare `/@user`.
|
|
36
|
+
*
|
|
37
|
+
* Accepted false-trigger: Matters sub-pages whose final path segment ends in
|
|
38
|
+
* `-<6+ alnum>` (e.g. `/@u/tags-abcdef`) will pass this check. This is
|
|
39
|
+
* harmless — the API `draft.article` verify rejects them as unrelated content.
|
|
40
|
+
*
|
|
41
|
+
* This is a HINT only — always verify publication via the API before acting on it.
|
|
42
|
+
*/
|
|
43
|
+
export function looksLikePublishedArticleUrl(url: string): boolean {
|
|
44
|
+
const path = pathOf(url);
|
|
45
|
+
// Must be /@user/slug-<shortHash>
|
|
46
|
+
// - segment under @ handle is required (rejects bare /@user)
|
|
47
|
+
// - trailing segment must end with -[a-z0-9]{6,} (the Matters hash suffix)
|
|
48
|
+
return /^\/@[^/]+\/[^/]+-[a-z0-9]{6,}$/.test(path);
|
|
49
|
+
}
|