|
72 | 72 | previewUnavailable: '预览暂不可用', |
73 | 73 | previewUnavailableBody: '使用下方按钮在 PythonIDE 中打开这个作品。', |
74 | 74 | community: '社区作品', |
| 75 | + communityPost: '社区帖子', |
| 76 | + communityPostSummary: '在 PythonIDE 中查看完整内容、评论和互动。', |
| 77 | + targetComment: '目标评论', |
75 | 78 | remoteImport: '远程导入', |
76 | 79 | importRemoteProject: '导入远程项目', |
77 | 80 | missingProjectURL: '缺少项目地址', |
|
156 | 159 | previewUnavailable: 'Preview unavailable', |
157 | 160 | previewUnavailableBody: 'Use the buttons below to open this work in PythonIDE.', |
158 | 161 | community: 'Community work', |
| 162 | + communityPost: 'Community post', |
| 163 | + communityPostSummary: 'View the full post, comments, and interactions in PythonIDE.', |
| 164 | + targetComment: 'Target comment', |
159 | 165 | remoteImport: 'Remote import', |
160 | 166 | importRemoteProject: 'Import remote project', |
161 | 167 | missingProjectURL: 'Project URL missing', |
|
232 | 238 | return String(navigatorLanguage || '').toLowerCase().startsWith('zh') ? 'zh' : 'en'; |
233 | 239 | } |
234 | 240 |
|
| 241 | + function normalizedCommunityIdentifier(value) { |
| 242 | + const normalized = String(value || ''); |
| 243 | + return normalized.length <= 160 && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(normalized) |
| 244 | + ? normalized |
| 245 | + : ''; |
| 246 | + } |
| 247 | + |
| 248 | + function parseCommunityRoute(url) { |
| 249 | + if (url.hash) return null; |
| 250 | + const pathParts = url.pathname.split('/'); |
| 251 | + if (pathParts.length !== 3 || pathParts[0] !== '' || pathParts[1] !== 'community') return null; |
| 252 | + |
| 253 | + const postID = normalizedCommunityIdentifier(decodeSegment(pathParts[2])); |
| 254 | + if (!postID) return null; |
| 255 | + |
| 256 | + const allowedQueryNames = new Set(['commentID', 'lang', 'v']); |
| 257 | + for (const name of url.searchParams.keys()) { |
| 258 | + if (!allowedQueryNames.has(name) || url.searchParams.getAll(name).length !== 1) return null; |
| 259 | + } |
| 260 | + if (url.searchParams.has('lang') && !['zh', 'en'].includes(url.searchParams.get('lang'))) return null; |
| 261 | + |
| 262 | + const rawCommentID = url.searchParams.get('commentID'); |
| 263 | + const commentID = rawCommentID === null ? '' : normalizedCommunityIdentifier(rawCommentID); |
| 264 | + if (rawCommentID !== null && !commentID) return null; |
| 265 | + |
| 266 | + const canonicalPath = `/community/${encodeURIComponent(postID)}`; |
| 267 | + return { |
| 268 | + type: 'community', |
| 269 | + postID, |
| 270 | + commentID, |
| 271 | + path: commentID |
| 272 | + ? `${canonicalPath}?commentID=${encodeURIComponent(commentID)}` |
| 273 | + : canonicalPath, |
| 274 | + }; |
| 275 | + } |
| 276 | + |
235 | 277 | function parseRoutePath(value) { |
236 | 278 | const path = safeForwardedPath(value) || '/'; |
237 | 279 | const url = new URL(path, SITE_ORIGIN); |
| 280 | + if (url.pathname === '/community' || url.pathname.startsWith('/community/')) { |
| 281 | + return parseCommunityRoute(url) || { type: 'home', path: '/' }; |
| 282 | + } |
238 | 283 | const parts = url.pathname.split('/').filter(Boolean); |
239 | 284 | if (parts[0] === 's' && parts[1]) { |
240 | 285 | return { type: 'script', id: decodeSegment(parts[1]), path: url.pathname }; |
|
261 | 306 | } |
262 | 307 |
|
263 | 308 | function customURLFor(route) { |
| 309 | + if (route.type === 'community') { |
| 310 | + const path = `pythonide://community/post/${encodeURIComponent(route.postID)}`; |
| 311 | + return route.commentID |
| 312 | + ? `${path}?commentID=${encodeURIComponent(route.commentID)}` |
| 313 | + : path; |
| 314 | + } |
264 | 315 | if (route.type === 'script') { |
265 | 316 | return `pythonide://community/script?id=${encodeURIComponent(route.id)}`; |
266 | 317 | } |
|
424 | 475 | formatCount, |
425 | 476 | isProjectScript, |
426 | 477 | normalizedTags, |
| 478 | + normalizedCommunityIdentifier, |
427 | 479 | parseRoutePath, |
428 | 480 | previewLines, |
429 | 481 | preferredLanguage, |
|
932 | 984 | setLoading(false); |
933 | 985 | } |
934 | 986 |
|
| 987 | + function loadCommunity() { |
| 988 | + currentView = 'community'; |
| 989 | + currentScript = null; |
| 990 | + el.authorRow.hidden = true; |
| 991 | + el.openApp.disabled = false; |
| 992 | + setText(el.openAppLabel, tr('openApp')); |
| 993 | + hideStatus(); |
| 994 | + setText(el.eyebrow, `PythonIDE · ${tr('community')}`); |
| 995 | + setText(el.title, tr('communityPost')); |
| 996 | + setText(el.summary, tr('communityPostSummary')); |
| 997 | + renderGeneric(`PythonIDE · ${tr('communityPost')}`, '#'); |
| 998 | + renderStats([ |
| 999 | + { value: tr('readOnlyValue'), label: tr('preview') }, |
| 1000 | + { value: 'App', label: tr('openMethod') }, |
| 1001 | + { value: route.commentID ? '1' : '—', label: tr('targetComment') }, |
| 1002 | + { value: tr('safeValue'), label: tr('linkStructure') }, |
| 1003 | + ]); |
| 1004 | + setText(el.actionTitle, tr('openApp')); |
| 1005 | + setText(el.actionDescription, tr('communityPostSummary')); |
| 1006 | + updatePageMetadata(tr('communityPost'), tr('communityPostSummary'), DEFAULT_SHARE_IMAGE); |
| 1007 | + setLoading(false); |
| 1008 | + } |
| 1009 | + |
935 | 1010 | function loadHome() { |
936 | 1011 | currentView = 'home'; |
937 | 1012 | currentScript = null; |
|
1022 | 1097 | if (currentView === 'script-error') renderScriptError(); |
1023 | 1098 | else if (currentView === 'import') loadImport(); |
1024 | 1099 | else if (currentView === 'short') loadShort(); |
| 1100 | + else if (currentView === 'community') loadCommunity(); |
1025 | 1101 | else if (currentView === 'home') loadHome(); |
1026 | 1102 | } |
1027 | 1103 |
|
|
1059 | 1135 | if (route.type === 'script') loadScript(initialScriptData(route.id)); |
1060 | 1136 | else if (route.type === 'import') loadImport(); |
1061 | 1137 | else if (route.type === 'short') loadShort(); |
| 1138 | + else if (route.type === 'community') loadCommunity(); |
1062 | 1139 | else loadHome(); |
1063 | 1140 | }(typeof globalThis !== 'undefined' ? globalThis : this)); |
0 commit comments