@ziteh/yangchun-comment-client 0.1.0 → 0.2.0-alpha.1

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.
Files changed (50) hide show
  1. package/README.md +6 -0
  2. package/dist/src/api/apiService.d.ts +22 -0
  3. package/dist/src/api/globalApiService.d.ts +10 -0
  4. package/dist/src/components/comment-admin.d.ts +25 -0
  5. package/dist/src/components/comment-dialog.d.ts +18 -0
  6. package/dist/src/components/comment-info.d.ts +14 -0
  7. package/dist/src/components/comment-input.d.ts +31 -0
  8. package/dist/src/components/list/comment-list-item.d.ts +19 -0
  9. package/dist/src/components/list/comment-list.d.ts +9 -0
  10. package/dist/src/components/yangchun-comment.d.ts +58 -0
  11. package/dist/src/components/yangchun-comment.styles.d.ts +2 -0
  12. package/dist/src/index.d.ts +1 -0
  13. package/dist/src/utils/comment.d.ts +6 -0
  14. package/dist/src/utils/format.d.ts +2 -0
  15. package/dist/src/utils/i18n.d.ts +46 -0
  16. package/dist/src/utils/pow.d.ts +9 -0
  17. package/dist/{utils → src/utils}/pseudonym.d.ts +0 -1
  18. package/dist/src/utils/sanitize.d.ts +1 -0
  19. package/dist/test/sanitize.test.d.ts +1 -0
  20. package/dist/yangchun-comment.js +962 -0
  21. package/dist/yangchun-comment.js.map +1 -0
  22. package/dist/yangchun-comment.umd.cjs +962 -0
  23. package/dist/yangchun-comment.umd.cjs.map +1 -0
  24. package/package.json +17 -13
  25. package/dist/element.d.ts +0 -97
  26. package/dist/element.js +0 -680
  27. package/dist/index.d.ts +0 -9
  28. package/dist/index.js +0 -23
  29. package/dist/types.d.ts +0 -3
  30. package/dist/utils/apiService.d.ts +0 -15
  31. package/dist/utils/apiService.js +0 -118
  32. package/dist/utils/format.d.ts +0 -1
  33. package/dist/utils/format.js +0 -14
  34. package/dist/utils/i18n.d.ts +0 -51
  35. package/dist/utils/i18n.js +0 -98
  36. package/dist/utils/pseudonym.js +0 -34
  37. package/dist/utils/sanitize.d.ts +0 -4
  38. package/dist/utils/sanitize.js +0 -59
  39. package/dist/utils/wordBank.js +0 -692
  40. package/dist/views/comments.d.ts +0 -3
  41. package/dist/views/comments.js +0 -119
  42. package/dist/views/preview.d.ts +0 -3
  43. package/dist/views/preview.js +0 -53
  44. package/dist/yangchun-comment.css +0 -1
  45. package/dist/yangchun-comment.es.js +0 -317
  46. package/dist/yangchun-comment.es.js.map +0 -1
  47. package/dist/yangchun-comment.umd.js +0 -317
  48. package/dist/yangchun-comment.umd.js.map +0 -1
  49. /package/dist/{types.js → src/utils/pow.worker.d.ts} +0 -0
  50. /package/dist/{utils → src/utils}/wordBank.d.ts +0 -0
package/dist/index.js DELETED
@@ -1,23 +0,0 @@
1
- import { YangchunCommentElement } from './element';
2
- export function initYangchunComment(elementId = 'ycc-app', options = {}) {
3
- const host = document.getElementById(elementId);
4
- if (!host)
5
- throw new Error(`Container #${elementId} not found`);
6
- const el = document.createElement('yangchun-comment');
7
- if (options.post)
8
- el.post = options.post;
9
- if (options.apiUrl)
10
- el.apiUrl = options.apiUrl;
11
- if (options.authorName)
12
- el.authorName = options.authorName;
13
- if (options.language)
14
- el.language = options.language;
15
- host.innerHTML = '';
16
- host.appendChild(el);
17
- return el;
18
- }
19
- // Define custom element if not already defined (avoid double-define in HMR/dev)
20
- if (!customElements.get('yangchun-comment')) {
21
- customElements.define('yangchun-comment', YangchunCommentElement);
22
- }
23
- export default initYangchunComment;
package/dist/types.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import type { Comment } from '@ziteh/yangchun-comment-shared';
2
- export type CommentMap = Record<string, Comment>;
3
- export type TabType = 'write' | 'preview';
@@ -1,15 +0,0 @@
1
- import type { Comment } from '@ziteh/yangchun-comment-shared';
2
- export declare const createApiService: (apiUrl: string) => {
3
- getComments: (post: string) => Promise<Comment[]>;
4
- addComment: (post: string, pseudonym: string, nameHash: string, msg: string, replyTo: string | null) => Promise<string | null>;
5
- updateComment: (post: string, commentId: string, pseudonym: string, nameHash: string, msg: string) => Promise<boolean>;
6
- deleteComment: (post: string, commentId: string) => Promise<boolean>;
7
- saveAuthInfo: (id: string, timestamp: number, token: string) => void;
8
- getAuthInfo: (commentId: string) => {
9
- id: string;
10
- timestamp: number;
11
- token: string;
12
- } | null;
13
- removeAuthInfo: (commentId: string) => void;
14
- canEditComment: (commentId: string) => boolean;
15
- };
@@ -1,118 +0,0 @@
1
- // TODO HttpOnly Cookie?
2
- export const createApiService = (apiUrl) => {
3
- const commentAuthMap = new Map();
4
- const getComments = async (post) => {
5
- const url = new URL('/api/comments', apiUrl);
6
- url.searchParams.append('post', post);
7
- const res = await fetch(url);
8
- return await res.json();
9
- };
10
- const addComment = async (post, pseudonym, nameHash, msg, replyTo) => {
11
- try {
12
- const url = new URL('/api/comments', apiUrl);
13
- url.searchParams.append('post', post); // Get honeypot field if present
14
- const websiteField = document.querySelector('input[name="website"]');
15
- const website = websiteField ? websiteField.value : '';
16
- const res = await fetch(url, {
17
- method: 'POST',
18
- headers: { 'Content-Type': 'application/json' },
19
- body: JSON.stringify({
20
- pseudonym,
21
- nameHash,
22
- msg,
23
- replyTo,
24
- website, // Include honeypot field
25
- }),
26
- });
27
- if (res.ok) {
28
- const data = await res.json();
29
- saveAuthInfo(data.id, data.timestamp, data.token);
30
- return data.id;
31
- }
32
- return null;
33
- }
34
- catch {
35
- return null;
36
- }
37
- };
38
- const updateComment = async (post, commentId, pseudonym, nameHash, msg) => {
39
- const authInfo = getAuthInfo(commentId);
40
- if (!authInfo)
41
- return false;
42
- try {
43
- const url = new URL('/api/comments', apiUrl);
44
- url.searchParams.append('post', post);
45
- const response = await fetch(url, {
46
- method: 'PUT',
47
- headers: { 'Content-Type': 'application/json' },
48
- body: JSON.stringify({
49
- id: authInfo.id,
50
- timestamp: authInfo.timestamp,
51
- token: authInfo.token,
52
- pseudonym,
53
- nameHash,
54
- msg,
55
- }),
56
- });
57
- return response.ok;
58
- }
59
- catch {
60
- return false;
61
- }
62
- };
63
- const deleteComment = async (post, commentId) => {
64
- const authInfo = getAuthInfo(commentId);
65
- if (!authInfo)
66
- return false;
67
- try {
68
- const url = new URL('/api/comments', apiUrl);
69
- url.searchParams.append('post', post);
70
- const response = await fetch(url, {
71
- method: 'DELETE',
72
- headers: {
73
- 'Content-Type': 'application/json',
74
- 'X-Comment-ID': authInfo.id,
75
- 'X-Comment-Token': authInfo.token,
76
- 'X-Comment-Timestamp': authInfo.timestamp.toString(),
77
- },
78
- });
79
- if (response.ok) {
80
- removeAuthInfo(commentId);
81
- return true;
82
- }
83
- return false;
84
- }
85
- catch {
86
- return false;
87
- }
88
- };
89
- const saveAuthInfo = (id, timestamp, token) => {
90
- commentAuthMap.set(id, { timestamp, token });
91
- // optionally store in sessionStorage for persistence across page reloads
92
- const encryptedInfo = btoa(JSON.stringify({ timestamp }));
93
- sessionStorage.setItem(`comment_auth_${id}`, encryptedInfo);
94
- };
95
- const getAuthInfo = (commentId) => {
96
- const authInfo = commentAuthMap.get(commentId);
97
- if (authInfo)
98
- return { id: commentId, ...authInfo };
99
- return null;
100
- };
101
- const removeAuthInfo = (commentId) => {
102
- commentAuthMap.delete(commentId);
103
- sessionStorage.removeItem(`comment_auth_${commentId}`);
104
- };
105
- const canEditComment = (commentId) => {
106
- return !!getAuthInfo(commentId);
107
- };
108
- return {
109
- getComments,
110
- addComment,
111
- updateComment,
112
- deleteComment,
113
- saveAuthInfo,
114
- getAuthInfo,
115
- removeAuthInfo,
116
- canEditComment,
117
- };
118
- };
@@ -1 +0,0 @@
1
- export declare function formatDate(timestamp: number): string;
@@ -1,14 +0,0 @@
1
- export function formatDate(timestamp) {
2
- const date = new Date(timestamp);
3
- const y = date.getFullYear();
4
- const m = String(date.getMonth() + 1).padStart(2, '0');
5
- const d = String(date.getDate()).padStart(2, '0');
6
- let hour = date.getHours();
7
- const minute = String(date.getMinutes()).padStart(2, '0');
8
- const period = hour >= 12 ? 'PM' : 'AM';
9
- hour = hour % 12;
10
- if (hour === 0)
11
- hour = 12;
12
- const h = String(hour).padStart(2, '0');
13
- return `${y}/${m}/${d} ${h}:${minute} ${period}`;
14
- }
@@ -1,51 +0,0 @@
1
- export interface I18nStrings {
2
- anonymous: string;
3
- replyTo: string;
4
- edit: string;
5
- delete: string;
6
- reply: string;
7
- replyingTo: string;
8
- cancelReply: string;
9
- editing: string;
10
- cancelEdit: string;
11
- updateComment: string;
12
- submitComment: string;
13
- namePlaceholder: string;
14
- messagePlaceholder: string;
15
- loading: string;
16
- confirmDelete: string;
17
- editFailed: string;
18
- submitFailed: string;
19
- deleteFailed: string;
20
- nameTooLong: string;
21
- messageTooLong: string;
22
- write: string;
23
- preview: string;
24
- emptyPreview: string;
25
- markdownHelp: string;
26
- modified: string;
27
- commentSystemTitle: string;
28
- commentSystemDesc: string;
29
- commentTimeLimit: string;
30
- markdownSyntax: string;
31
- markdownBasicSupport: string;
32
- markdownLinkExample: string;
33
- markdownImageExample: string;
34
- markdownItalicExample: string;
35
- markdownBoldExample: string;
36
- markdownListExample: string;
37
- markdownOrderedListExample: string;
38
- markdownInlineCodeExample: string;
39
- markdownCodeBlockExample: string;
40
- pseudonymNotice: string;
41
- editingPseudonymNotice: string;
42
- author: string;
43
- noComments: string;
44
- }
45
- export declare const en: I18nStrings;
46
- export declare const zhHant: I18nStrings;
47
- export declare const createI18n: (initLang?: I18nStrings) => {
48
- t: (key: keyof I18nStrings) => string;
49
- setLanguage: (lang: I18nStrings) => void;
50
- getLanguage: () => I18nStrings;
51
- };
@@ -1,98 +0,0 @@
1
- export const en = {
2
- anonymous: 'Anonymous',
3
- replyTo: 'Reply to',
4
- edit: 'Edit',
5
- delete: 'Delete',
6
- reply: 'Reply',
7
- replyingTo: 'Replying to: ',
8
- modified: 'Modified on',
9
- cancelReply: 'Cancel',
10
- editing: 'Editing: ',
11
- cancelEdit: 'Cancel',
12
- updateComment: 'Update',
13
- submitComment: 'Submit',
14
- namePlaceholder: 'Name (optional)',
15
- messagePlaceholder: 'Your comment...\nSupports Markdown syntax',
16
- loading: 'Loading...',
17
- confirmDelete: 'Are you sure you want to delete this comment?',
18
- editFailed: 'Failed to edit comment. Permission may have expired.',
19
- submitFailed: 'Failed to submit comment.',
20
- deleteFailed: 'Failed to delete comment. Permission may have expired.',
21
- nameTooLong: 'Name is too long',
22
- messageTooLong: 'Message is too long',
23
- write: 'Write',
24
- preview: 'Preview',
25
- emptyPreview: 'Nothing to preview',
26
- markdownHelp: 'Help',
27
- commentSystemTitle: 'Comments',
28
- commentSystemDesc: 'This is a simple comment system. You can post your opinions or respond to other comments. Click "Preview" to see how your comment looks before posting.',
29
- commentTimeLimit: "After posting a comment, you can edit or delete it within two minutes, as long as you don't leave or refresh the page.",
30
- markdownSyntax: 'Syntax',
31
- markdownBasicSupport: 'Basic Markdown syntax is supported. HTML is not supported.',
32
- markdownLinkExample: '[Link](https://www.example.com)',
33
- markdownImageExample: '![Image](https://www.example.com/sample.jpg)',
34
- markdownItalicExample: '*Italic* or _Italic_',
35
- markdownBoldExample: '**Bold** or __Bold__',
36
- markdownListExample: '- List item',
37
- markdownOrderedListExample: '1. Ordered list item',
38
- markdownInlineCodeExample: '`Inline code`',
39
- markdownCodeBlockExample: '```\nCode block\n```',
40
- pseudonymNotice: 'Will be converted to a unique pseudonym, longer names help avoid impersonation',
41
- editingPseudonymNotice: 'Cannot be changed when editing',
42
- author: 'Author',
43
- noComments: 'No comments yet',
44
- };
45
- export const zhHant = {
46
- anonymous: '匿名',
47
- replyTo: '回覆給',
48
- edit: '編輯',
49
- delete: '刪除',
50
- reply: '回覆',
51
- modified: '修改於',
52
- replyingTo: '回覆給:',
53
- cancelReply: '取消',
54
- editing: '編輯中:',
55
- cancelEdit: '取消',
56
- updateComment: '更新',
57
- submitComment: '發送',
58
- namePlaceholder: '名稱 (選填)',
59
- messagePlaceholder: '留言內容...\n支援 Markdown 語法',
60
- loading: '載入中...',
61
- confirmDelete: '確定要刪除此留言嗎?',
62
- editFailed: '編輯留言失敗,可能權限已過期',
63
- submitFailed: '發送留言失敗',
64
- deleteFailed: '刪除留言失敗,可能權限已過期',
65
- nameTooLong: '暱稱過長',
66
- messageTooLong: '留言內容過長',
67
- write: '編輯',
68
- preview: '預覽',
69
- emptyPreview: '沒有內容可供預覽',
70
- markdownHelp: '說明',
71
- commentSystemTitle: '留言',
72
- commentSystemDesc: '這是一個簡單的留言系統,你可以發表意見或回應其他留言。發佈前可點擊「預覽」查看留言樣式。',
73
- commentTimeLimit: '發佈留言後,在不離開或重新整理頁面的情況下,你可以編輯或刪除兩分鐘內的留言。',
74
- markdownSyntax: '語法',
75
- markdownBasicSupport: '支援基本 Markdown 語法,不支援 HTML。',
76
- markdownLinkExample: '[連結](https://www.example.com)',
77
- markdownImageExample: '![圖片](https://www.example.com/sample.jpg)',
78
- markdownItalicExample: '*斜體* 或 _斜體_',
79
- markdownBoldExample: '**粗體** 或 __粗體__',
80
- markdownListExample: '- 清單',
81
- markdownOrderedListExample: '1. 編號清單',
82
- markdownInlineCodeExample: '`行內程式碼`',
83
- markdownCodeBlockExample: '```\n程式碼區塊\n```',
84
- pseudonymNotice: '名稱將被轉換為化名,使用較長的名稱有助於避免被冒充',
85
- editingPseudonymNotice: '編輯時無法更改',
86
- author: '作者',
87
- noComments: '尚未有留言',
88
- };
89
- export const createI18n = (initLang = en) => {
90
- let currentStrings = initLang;
91
- return {
92
- t: (key) => currentStrings[key],
93
- setLanguage: (lang) => {
94
- currentStrings = lang;
95
- },
96
- getLanguage: () => currentStrings,
97
- };
98
- };
@@ -1,34 +0,0 @@
1
- import { adjectives, nouns } from './wordBank';
2
- export async function generateSHA256Hash(text) {
3
- const encoder = new TextEncoder();
4
- const data = encoder.encode(text);
5
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
6
- const hashArray = Array.from(new Uint8Array(hashBuffer));
7
- return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
8
- }
9
- function selectWordsFromHash(hash) {
10
- // Using the first 8 characters of the hash to select an adjective
11
- const adjIndex = parseInt(hash.substring(0, 8), 16) % adjectives.length;
12
- // Using the next 8 characters of the hash to select a noun
13
- const nounIndex = parseInt(hash.substring(8, 16), 16) % nouns.length;
14
- return {
15
- adjective: adjectives[adjIndex],
16
- noun: nouns[nounIndex],
17
- };
18
- }
19
- export async function generatePseudonymAndHash(originalName) {
20
- const name = (originalName || '').trim();
21
- if (name.length === 0) {
22
- return {
23
- pseudonym: '',
24
- hash: '',
25
- };
26
- }
27
- const hash = await generateSHA256Hash(name);
28
- const { adjective, noun } = selectWordsFromHash(hash);
29
- const pseudonym = `${adjective} ${noun}`;
30
- return {
31
- pseudonym,
32
- hash,
33
- };
34
- }
@@ -1,4 +0,0 @@
1
- import { type Config as DomPurifyConfig } from 'dompurify';
2
- export declare const DOMPURIFY_CONFIG: DomPurifyConfig;
3
- export declare function setupDOMPurifyHooks(): void;
4
- export declare function sanitizeHtml(html: string): string;
@@ -1,59 +0,0 @@
1
- import DOMPurify, {} from 'dompurify';
2
- export const DOMPURIFY_CONFIG = {
3
- ALLOWED_TAGS: [
4
- 'a',
5
- 'b',
6
- 'i',
7
- 'em',
8
- 'strong',
9
- 's',
10
- 'p',
11
- 'ul',
12
- 'ol',
13
- 'li',
14
- 'code',
15
- 'pre',
16
- 'blockquote',
17
- 'h6', // only H6
18
- 'hr',
19
- 'br',
20
- 'img',
21
- ],
22
- ALLOWED_ATTR: ['href', 'src', 'alt'],
23
- ALLOW_DATA_ATTR: false, // disable data-* attributes
24
- ALLOW_ARIA_ATTR: false, // disable aria-* attributes
25
- // Explicitly blocklist
26
- // FORBID_TAGS: ['style', 'script', 'iframe', 'object', 'form', 'embed'],
27
- // FORBID_ATTR: ['style', 'onclick', 'onmouseover', 'onload', 'onunload', 'onerror'],
28
- };
29
- export function setupDOMPurifyHooks() {
30
- DOMPurify.addHook('afterSanitizeAttributes', (node) => {
31
- // Make all links open in a new tab, and prevent window.opener vulnerability
32
- if (node.tagName === 'A') {
33
- node.setAttribute('rel', 'noopener noreferrer');
34
- node.setAttribute('target', '_blank');
35
- }
36
- // Optimize image loading
37
- if (node.tagName === 'IMG') {
38
- node.setAttribute('loading', 'lazy');
39
- }
40
- });
41
- DOMPurify.addHook('uponSanitizeAttribute', (_node, data) => {
42
- // Only allow http: and https: for href/src attributes
43
- // Remove javascript: and other potentially dangerous protocols
44
- if (data.attrName === 'href' || data.attrName === 'src') {
45
- try {
46
- const url = new URL(data.attrValue || '');
47
- if (url.protocol !== 'http:' && url.protocol !== 'https:') {
48
- data.keepAttr = false;
49
- }
50
- }
51
- catch {
52
- data.keepAttr = false;
53
- }
54
- }
55
- });
56
- }
57
- export function sanitizeHtml(html) {
58
- return DOMPurify.sanitize(html, DOMPURIFY_CONFIG);
59
- }