@ovv/artalk
Version:
A self-hosted comment system
1 lines • 480 kB
Source Map (JSON)
{"version":3,"file":"ArtalkLite.mjs","sources":["../src/lib/utils.ts","../src/i18n/en.ts","../src/i18n/zh-CN.ts","../src/i18n/external.ts","../src/i18n/index.ts","../../../node_modules/.pnpm/insane@2.6.2/node_modules/insane/she.js","../../../node_modules/.pnpm/assignment@2.0.0/node_modules/assignment/assignment.js","../../../node_modules/.pnpm/insane@2.6.2/node_modules/insane/lowercase.js","../../../node_modules/.pnpm/insane@2.6.2/node_modules/insane/toMap.js","../../../node_modules/.pnpm/insane@2.6.2/node_modules/insane/attributes.js","../../../node_modules/.pnpm/insane@2.6.2/node_modules/insane/elements.js","../../../node_modules/.pnpm/insane@2.6.2/node_modules/insane/parser.js","../../../node_modules/.pnpm/insane@2.6.2/node_modules/insane/sanitizer.js","../../../node_modules/.pnpm/insane@2.6.2/node_modules/insane/defaults.js","../../../node_modules/.pnpm/insane@2.6.2/node_modules/insane/insane.js","../src/lib/sanitizer.ts","../../../node_modules/.pnpm/hanabi@0.4.0/node_modules/hanabi/dist/hanabi.js","../src/lib/highlight.ts","../src/lib/marked-renderer.ts","../src/lib/marked.ts","../src/lib/injection/container.ts","../src/context.ts","../src/lib/merge-deep.ts","../src/defaults.ts","../src/config.ts","../src/plugins/stat.ts","../src/api/v2.ts","../src/api/fetch.ts","../src/api/handler.ts","../src/api/index.ts","../src/plugins/markdown.ts","../src/plugins/editor/_plug.ts","../src/plugins/editor/local-storage.ts","../src/plugins/editor/textarea.ts","../src/plugins/editor/submit-btn.ts","../src/plugins/editor/submit-add.ts","../src/plugins/editor/submit.ts","../src/plugins/editor/state-reply.ts","../src/plugins/editor/state-edit.ts","../src/plugins/editor/closable.ts","../src/plugins/editor/header-event.ts","../src/plugins/editor/header-user.ts","../src/plugins/editor/header-link.ts","../src/plugins/editor/mover.ts","../src/lib/ui.ts","../src/plugins/editor/emoticons.ts","../src/plugins/editor/upload.ts","../src/plugins/editor/preview.ts","../src/plugins/editor/index.ts","../src/plugins/editor/_kit.ts","../src/lib/event-manager.ts","../src/plugins/editor-kit.ts","../src/plugins/list/with-editor.ts","../src/plugins/list/unread.ts","../src/plugins/list/count.ts","../src/plugins/list/sidebar-btn.ts","../src/plugins/list/unread-badge.ts","../src/plugins/list/goto-dispatcher.ts","../src/plugins/list/goto-focus.ts","../src/plugins/list/copyright.ts","../src/plugins/list/no-comment.ts","../src/plugins/list/dropdown.ts","../src/plugins/list/time-ticking.ts","../src/components/error-dialog.ts","../src/plugins/list/error-dialog.ts","../src/plugins/list/loading.ts","../src/plugins/list/fetch.ts","../src/plugins/list/reach-bottom.ts","../src/plugins/list/goto-first.ts","../src/plugins/list/index.ts","../src/plugins/notifies.ts","../src/plugins/version-check.ts","../src/plugins/admin-only-elem.ts","../src/plugins/dark-mode.ts","../src/plugins/page-vote.ts","../src/services/api.ts","../src/lib/user.ts","../src/services/user.ts","../src/components/checker/captcha-renders.ts","../src/components/checker/captcha.ts","../src/components/checker/admin.ts","../src/components/dialog.ts","../src/components/checker/index.ts","../src/services/checkers.ts","../src/editor/editor.html?raw","../src/editor/ui.ts","../src/editor/state.ts","../src/editor/editor.ts","../src/services/editor.ts","../src/services/i18n.ts","../src/data.ts","../src/services/data.ts","../src/layer/layer.ts","../src/layer/scrollbar-helper.ts","../src/layer/wrap.ts","../src/layer/layer-manager.ts","../src/services/layer.ts","../src/list/list.html?raw","../src/list/nest.ts","../src/list/layout/nest.ts","../src/list/layout/flat.ts","../src/list/layout/index.ts","../src/lib/detect.ts","../src/comment/height-limit.ts","../src/comment/comment.html?raw","../src/comment/renders/avatar.ts","../src/comment/renders/header.ts","../src/comment/renders/content.ts","../src/comment/renders/reply-at.ts","../src/comment/renders/reply-to.ts","../src/comment/renders/pending.ts","../src/components/action-btn.ts","../src/comment/renders/actions.ts","../src/comment/renders/index.ts","../src/comment/render.ts","../src/comment/actions.ts","../src/comment/comment-node.ts","../src/list/comment.ts","../src/components/read-more-btn.ts","../src/list/paginator/read-more.ts","../src/components/pagination.ts","../src/list/paginator/up-down.ts","../src/list/page.ts","../src/list/list.ts","../src/services/list.ts","../src/layer/sidebar-layer.html?raw","../src/layer/sidebar-layer.ts","../src/services/sidebar.ts","../src/services/index.ts","../src/plugins/index.ts","../src/plugins/mount-error.ts","../src/mount.ts","../src/lib/watch-conf.ts","../src/services/config.ts","../src/services/events.ts","../src/artalk.ts","../src/main.ts"],"sourcesContent":["export function createElement<E extends HTMLElement = HTMLElement>(htmlStr: string = ''): E {\n const div = document.createElement('div')\n div.innerHTML = htmlStr.trim()\n return (div.firstElementChild || div) as E\n}\n\nexport function getHeight(el: HTMLElement): number {\n const num = parseFloat(getComputedStyle(el, null).height.replace('px', ''))\n return num || 0 // NaN -> 0\n}\n\nexport function htmlEncode(str: string) {\n const temp = document.createElement('div')\n temp.innerText = str\n const output = temp.innerHTML\n return output\n}\n\nexport function htmlDecode(str: string) {\n const temp = document.createElement('div')\n temp.innerHTML = str\n const output = temp.innerText\n return output\n}\n\nexport function getQueryParam(name: string) {\n const match = RegExp(`[?&]${name}=([^&]*)`).exec(window.location.search)\n return match && decodeURIComponent(match[1].replace(/\\+/g, ' '))\n}\n\nexport function getOffset(el: HTMLElement, relativeTo?: HTMLElement) {\n const getOffsetRecursive = (element: HTMLElement): { top: number; left: number } => {\n const rect = element.getBoundingClientRect()\n const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft\n const scrollTop = window.pageYOffset || document.documentElement.scrollTop\n return {\n top: rect.top + scrollTop,\n left: rect.left + scrollLeft,\n }\n }\n\n const elOffset = getOffsetRecursive(el)\n if (!relativeTo) return elOffset\n\n const relativeToOffset = getOffsetRecursive(relativeTo)\n\n return {\n top: elOffset.top - relativeToOffset.top,\n left: elOffset.left - relativeToOffset.left,\n }\n}\n\nexport function padWithZeros(vNumber: number, width: number) {\n let numAsString = vNumber.toString()\n while (numAsString.length < width) {\n numAsString = `0${numAsString}`\n }\n return numAsString\n}\n\nexport function dateFormat(date: Date) {\n const vDay = padWithZeros(date.getDate(), 2)\n const vMonth = padWithZeros(date.getMonth() + 1, 2)\n const vYear = padWithZeros(date.getFullYear(), 2)\n // var vHour = padWithZeros(date.getHours(), 2);\n // var vMinute = padWithZeros(date.getMinutes(), 2);\n // var vSecond = padWithZeros(date.getSeconds(), 2);\n return `${vYear}-${vMonth}-${vDay}`\n}\n\nexport function timeAgo(date: Date, $t = (n: any) => n) {\n try {\n const oldTime = date.getTime()\n const currTime = new Date().getTime()\n const diffValue = currTime - oldTime\n\n const days = Math.floor(diffValue / (24 * 3600 * 1000))\n if (days === 0) {\n // 计算相差小时数\n const leave1 = diffValue % (24 * 3600 * 1000) // 计算天数后剩余的毫秒数\n const hours = Math.floor(leave1 / (3600 * 1000))\n if (hours === 0) {\n // 计算相差分钟数\n const leave2 = leave1 % (3600 * 1000) // 计算小时数后剩余的毫秒数\n const minutes = Math.floor(leave2 / (60 * 1000))\n if (minutes === 0) {\n // 计算相差秒数\n const leave3 = leave2 % (60 * 1000) // 计算分钟数后剩余的毫秒数\n const seconds = Math.round(leave3 / 1000)\n if (seconds < 10) return $t('now')\n return `${seconds} ${$t('seconds')}`\n }\n return `${minutes} ${$t('minutes')}`\n }\n return `${hours} ${$t('hours')}`\n }\n if (days < 0) return $t('now')\n\n if (days < 8) {\n return `${days} ${$t('days')}`\n }\n\n return dateFormat(date)\n } catch (error) {\n console.error(error)\n return ' - '\n }\n}\n\nexport function getGravatarURL(opts: { params: string; mirror: string; emailHash: string }) {\n return `${opts.mirror.replace(/\\/$/, '')}/${opts.emailHash}?${opts.params.replace(/^\\?/, '')}`\n}\n\nexport function sleep(ms: number) {\n return new Promise((resolve) => {\n setTimeout(resolve, ms)\n })\n}\n\n/** 版本号比较(a < b :-1 | 0 | b < a :1) */\nexport function versionCompare(a: string, b: string) {\n const pa = a.split('.')\n const pb = b.split('.')\n for (let i = 0; i < 3; i++) {\n const na = Number(pa[i])\n const nb = Number(pb[i])\n if (na > nb) return 1\n if (nb > na) return -1\n if (!Number.isNaN(na) && Number.isNaN(nb)) return 1\n if (Number.isNaN(na) && !Number.isNaN(nb)) return -1\n }\n return 0\n}\n\n/** 获取修正后的 UserAgent */\nexport async function getCorrectUserAgent() {\n const uaRaw = navigator.userAgent\n if (!(navigator as any).userAgentData || !(navigator as any).userAgentData.getHighEntropyValues) {\n return uaRaw\n }\n\n // @link https://web.dev/migrate-to-ua-ch/\n // @link https://web.dev/user-agent-client-hints/\n const uaData = (navigator as any).userAgentData\n let uaGot: any = null\n try {\n uaGot = await uaData.getHighEntropyValues(['platformVersion'])\n } catch (err) {\n console.error(err)\n return uaRaw\n }\n const majorPlatformVersion = Number(uaGot.platformVersion.split('.')[0])\n\n if (uaData.platform === 'Windows') {\n if (majorPlatformVersion >= 13) {\n // @link https://docs.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11\n // @date 2022-4-29\n // Win 11 样本:\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36\"\n return uaRaw.replace(/Windows NT 10.0/, 'Windows NT 11.0')\n }\n }\n if (uaData.platform === 'macOS') {\n if (majorPlatformVersion >= 11) {\n // 11 => BigSur\n // @date 2022-4-29\n // (Intel Chip) macOS 12.3 样本:\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36\"\n // (Arm Chip) macOS 样本:\"Mozilla/5.0 (Macintosh; ARM Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.2 Safari/605.1.15\"\n return uaRaw.replace(\n /(Mac OS X \\d+_\\d+_\\d+|Mac OS X)/,\n `Mac OS X ${uaGot.platformVersion.replace(/\\./g, '_')}`,\n )\n }\n }\n\n return uaRaw\n}\n\n/** 是否为完整的 URL */\nexport function isValidURL(urlRaw: string) {\n // @link https://www.rfc-editor.org/rfc/rfc3986\n let url: URL\n try {\n url = new URL(urlRaw)\n } catch (_) {\n return false\n }\n return url.protocol === 'http:' || url.protocol === 'https:'\n}\n\n/** 获取基于 conf.server 的 URL */\nexport function getURLBasedOnApi(opts: { base: string; path: string }) {\n return getURLBasedOn(opts.base, opts.path)\n}\n\n/** 获取基于某个 baseURL 的 URL */\nexport function getURLBasedOn(baseURL: string, path: string) {\n return `${baseURL.replace(/\\/$/, '')}/${path.replace(/^\\//, '')}`\n}\n","const en = {\n /* Editor */\n placeholder: 'Leave a comment',\n noComment: 'No Comment',\n send: 'Send',\n signIn: 'Sign in',\n signUp: 'Sign up',\n save: 'Save',\n nick: 'Nickname',\n email: 'Email',\n link: 'Website',\n emoticon: 'Emoji',\n preview: 'Preview',\n uploadImage: 'Upload Image',\n uploadFail: 'Upload Failed',\n commentFail: 'Failed to comment',\n restoredMsg: 'Content has been restored',\n onlyAdminCanReply: 'Only admin can reply',\n uploadLoginMsg: 'Please fill in your name and email to upload',\n\n /* List */\n counter: '{count} Comments',\n sortLatest: 'Latest',\n sortOldest: 'Oldest',\n sortBest: 'Best',\n sortAuthor: 'Author',\n openComment: 'Open Comment',\n closeComment: 'Close Comment',\n listLoadFailMsg: 'Failed to load comments',\n listRetry: 'Retry',\n loadMore: 'Load More',\n\n /* Comment */\n admin: 'Admin',\n reply: 'Reply',\n voteUp: 'Up',\n voteDown: 'Down',\n voteFail: 'Vote Failed',\n readMore: 'Read More',\n actionConfirm: 'Confirm',\n collapse: 'Collapse',\n collapsed: 'Collapsed',\n collapsedMsg: 'This comment has been collapsed',\n expand: 'Expand',\n approved: 'Approved',\n pending: 'Pending',\n pendingMsg: 'Pending, visible only to commenter.',\n edit: 'Edit',\n editCancel: 'Cancel Edit',\n delete: 'Delete',\n deleteConfirm: 'Confirm',\n pin: 'Pin',\n unpin: 'Unpin',\n\n /* Time */\n seconds: 'seconds ago',\n minutes: 'minutes ago',\n hours: 'hours ago',\n days: 'days ago',\n now: 'just now',\n\n /* Checker */\n adminCheck: 'Enter admin password:',\n captchaCheck: 'Enter the CAPTCHA to continue:',\n confirm: 'Confirm',\n cancel: 'Cancel',\n\n /* Sidebar */\n msgCenter: 'Messages',\n ctrlCenter: 'Dashboard',\n\n /* Auth */\n userProfile: 'Profile',\n noAccountPrompt: \"Don't have an account?\",\n haveAccountPrompt: 'Already have an account?',\n forgetPassword: 'Forget Password',\n resetPassword: 'Reset Password',\n changePassword: 'Change Password',\n confirmPassword: 'Confirm Password',\n passwordMismatch: 'Passwords do not match',\n verificationCode: 'Verification Code',\n verifySend: 'Send Code',\n verifyResend: 'Resend',\n waitSeconds: 'Wait {seconds}s',\n emailVerified: 'Email has been verified',\n password: 'Password',\n username: 'Username',\n nextStep: 'Next Step',\n skipVerify: 'Skip verification',\n logoutConfirm: 'Are you sure to logout?',\n accountMergeNotice: 'Your email has multiple accounts with different id.',\n accountMergeSelectOne: 'Please select one you want to merge all the data into it.',\n accountMergeConfirm: 'All data will be merged into one account, the id is {id}.',\n dismiss: 'Dismiss',\n merge: 'Merge',\n\n /* General */\n client: 'Client',\n server: 'Server',\n loading: 'Loading',\n loadFail: 'Load Failed',\n editing: 'Editing',\n editFail: 'Edit Failed',\n deleting: 'Deleting',\n deleteFail: 'Delete Failed',\n reqGot: 'Request got',\n reqAborted: 'Request timed out or terminated unexpectedly',\n updateMsg: 'Please update Artalk {name} to get the best experience!',\n currentVersion: 'Current Version',\n ignore: 'Ignore',\n open: 'Open',\n openName: 'Open {name}',\n}\n\nexport type I18n = typeof en\nexport type I18nKeys = keyof I18n\n\nexport default en\n","import type { I18n } from '.'\n\nconst zhCN: I18n = {\n /* Editor */\n placeholder: '键入内容...',\n noComment: '「此时无声胜有声」',\n send: '发送',\n signIn: '登录',\n signUp: '注册',\n save: '保存',\n nick: '昵称',\n email: '邮箱',\n link: '网址',\n emoticon: '表情',\n preview: '预览',\n uploadImage: '上传图片',\n uploadFail: '上传失败',\n commentFail: '评论失败',\n restoredMsg: '内容已自动恢复',\n onlyAdminCanReply: '仅管理员可评论',\n uploadLoginMsg: '填入你的名字邮箱才能上传哦',\n\n /* List */\n counter: '{count} 条评论',\n sortLatest: '最新',\n sortOldest: '最早',\n sortBest: '最热',\n sortAuthor: '作者',\n openComment: '打开评论',\n closeComment: '关闭评论',\n listLoadFailMsg: '无法获取评论列表数据',\n listRetry: '点击重新获取',\n loadMore: '加载更多',\n\n /* Comment */\n admin: '管理员',\n reply: '回复',\n voteUp: '赞同',\n voteDown: '反对',\n voteFail: '投票失败',\n readMore: '阅读更多',\n actionConfirm: '确认操作',\n collapse: '折叠',\n collapsed: '已折叠',\n collapsedMsg: '该评论已被系统或管理员折叠',\n expand: '展开',\n approved: '已审',\n pending: '待审',\n pendingMsg: '审核中,仅本人可见。',\n edit: '编辑',\n editCancel: '取消编辑',\n delete: '删除',\n deleteConfirm: '确认删除',\n pin: '置顶',\n unpin: '取消置顶',\n\n /* Time */\n seconds: '秒前',\n minutes: '分钟前',\n hours: '小时前',\n days: '天前',\n now: '刚刚',\n\n /* Checker */\n adminCheck: '键入密码来验证管理员身份:',\n captchaCheck: '键入验证码继续:',\n confirm: '确认',\n cancel: '取消',\n\n /* Sidebar */\n msgCenter: '通知中心',\n ctrlCenter: '控制中心',\n\n /* Auth */\n userProfile: '个人资料',\n noAccountPrompt: '没有账号?',\n haveAccountPrompt: '已有账号?',\n forgetPassword: '忘记密码',\n resetPassword: '重置密码',\n changePassword: '修改密码',\n confirmPassword: '确认密码',\n passwordMismatch: '两次输入的密码不一致',\n verificationCode: '验证码',\n verifySend: '发送验证码',\n verifyResend: '重新发送',\n waitSeconds: '等待 {seconds}秒',\n emailVerified: '邮箱已验证',\n password: '密码',\n username: '用户名',\n nextStep: '下一步',\n skipVerify: '跳过验证',\n logoutConfirm: '确定要退出登录吗?',\n accountMergeNotice: '您的电子邮件下有多个不同 ID 的账户。',\n accountMergeSelectOne: '请选择将所有数据合并到其中的一个。',\n accountMergeConfirm: '所有数据将合并到 ID 为 {id} 的账户中。',\n dismiss: '忽略',\n merge: '合并',\n\n /* General */\n client: '客户端',\n server: '服务器',\n loading: '加载中',\n loadFail: '加载失败',\n editing: '修改中',\n editFail: '修改失败',\n deleting: '删除中',\n deleteFail: '删除失败',\n reqGot: '请求响应',\n reqAborted: '请求超时或意外终止',\n updateMsg: '请更新 Artalk {name} 以获得更好的体验!',\n currentVersion: '当前版本',\n ignore: '忽略',\n open: '打开',\n openName: '打开{name}',\n}\n\nexport default zhCN\n","import type { I18n } from '.'\n\nexport const GLOBAL_LOCALES_KEY = 'ArtalkI18n'\n\nexport function defineLocaleExternal(lang: string, locale: I18n, aliases?: string[]) {\n if (!window[GLOBAL_LOCALES_KEY]) window[GLOBAL_LOCALES_KEY] = {}\n window[GLOBAL_LOCALES_KEY][lang] = locale\n if (aliases)\n aliases.forEach((l) => {\n window[GLOBAL_LOCALES_KEY][l] = locale\n })\n return locale\n}\n","import * as Utils from '../lib/utils'\nimport en, { I18n, I18nKeys } from './en'\nimport zhCN from './zh-CN'\nimport { GLOBAL_LOCALES_KEY } from './external'\n\nexport type * from './en'\n\n// @note the key of language is followed by `ISO 639`\n// https://en.wikipedia.org/wiki/ISO_639\n// https://datatracker.ietf.org/doc/html/rfc5646#section-2.1\nexport const internal = {\n en,\n 'en-US': en,\n 'zh-CN': zhCN,\n}\n\n/**\n * find a locale object by language name\n */\nexport function findLocaleSet(lang: string): I18n {\n // normalize a key of language to `ISO 639`\n lang = lang.replace(\n /^([a-zA-Z]+)(-[a-zA-Z]+)?$/,\n (_, p1: string, p2: string) => p1.toLowerCase() + (p2 || '').toUpperCase(),\n )\n\n // internal finding\n if (internal[lang]) {\n return internal[lang]\n }\n\n // external finding\n if (window[GLOBAL_LOCALES_KEY] && window[GLOBAL_LOCALES_KEY][lang]) {\n return window[GLOBAL_LOCALES_KEY][lang]\n }\n\n // case when not found:\n // use `en` by default\n return internal.en\n}\n\n/**\n * System locale setting\n */\nlet LocaleConf: I18n | string = 'en'\nlet LocaleDict: I18n = findLocaleSet(LocaleConf) // en by default\n\n/**\n * Set system locale\n */\nexport function setLocale(locale: I18n | string) {\n if (locale === LocaleConf) return\n LocaleConf = locale\n LocaleDict = typeof locale === 'string' ? findLocaleSet(locale) : locale\n}\n\n/**\n * Get an i18n message by key\n */\nexport function t(key: I18nKeys, args: { [key: string]: string } = {}) {\n let str = LocaleDict?.[key] || key\n str = str.replace(/\\{\\s*(\\w+?)\\s*\\}/g, (_, token) => args[token] || '')\n\n return Utils.htmlEncode(str)\n}\n\nexport default t\n","'use strict';\n\nvar escapes = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n};\nvar unescapes = {\n '&': '&',\n '<': '<',\n '>': '>',\n '"': '\"',\n ''': \"'\"\n};\nvar rescaped = /(&|<|>|"|')/g;\nvar runescaped = /[&<>\"']/g;\n\nfunction escapeHtmlChar (match) {\n return escapes[match];\n}\nfunction unescapeHtmlChar (match) {\n return unescapes[match];\n}\n\nfunction escapeHtml (text) {\n return text == null ? '' : String(text).replace(runescaped, escapeHtmlChar);\n}\n\nfunction unescapeHtml (html) {\n return html == null ? '' : String(html).replace(rescaped, unescapeHtmlChar);\n}\n\nescapeHtml.options = unescapeHtml.options = {};\n\nmodule.exports = {\n encode: escapeHtml,\n escape: escapeHtml,\n decode: unescapeHtml,\n unescape: unescapeHtml,\n version: '1.0.0-browser'\n};\n","'use strict';\n\nfunction assignment (result) {\n var stack = Array.prototype.slice.call(arguments, 1);\n var item;\n var key;\n while (stack.length) {\n item = stack.shift();\n for (key in item) {\n if (item.hasOwnProperty(key)) {\n if (Object.prototype.toString.call(result[key]) === '[object Object]') {\n result[key] = assignment(result[key], item[key]);\n } else {\n result[key] = item[key];\n }\n }\n }\n }\n return result;\n}\n\nmodule.exports = assignment;\n","'use strict';\n\nmodule.exports = function lowercase (string) {\n return typeof string === 'string' ? string.toLowerCase() : string;\n};\n","'use strict';\n\nfunction toMap (list) {\n return list.reduce(asKey, {});\n}\n\nfunction asKey (accumulator, item) {\n accumulator[item] = true;\n return accumulator;\n}\n\nmodule.exports = toMap;\n","'use strict';\n\nvar toMap = require('./toMap');\nvar uris = ['background', 'base', 'cite', 'href', 'longdesc', 'src', 'usemap'];\n\nmodule.exports = {\n uris: toMap(uris) // attributes that have an href and hence need to be sanitized\n};\n","'use strict';\n\nvar toMap = require('./toMap');\nvar voids = ['area', 'br', 'col', 'hr', 'img', 'wbr', 'input', 'base', 'basefont', 'link', 'meta'];\n\nmodule.exports = {\n voids: toMap(voids)\n};\n","'use strict';\n\nvar he = require('he');\nvar lowercase = require('./lowercase');\nvar attributes = require('./attributes');\nvar elements = require('./elements');\nvar rstart = /^<\\s*([\\w:-]+)((?:\\s+[\\w:-]+(?:\\s*=\\s*(?:(?:\"[^\"]*\")|(?:'[^']*')|[^>\\s]+))?)*)\\s*(\\/?)\\s*>/;\nvar rend = /^<\\s*\\/\\s*([\\w:-]+)[^>]*>/;\nvar rattrs = /([\\w:-]+)(?:\\s*=\\s*(?:(?:\"((?:[^\"])*)\")|(?:'((?:[^'])*)')|([^>\\s]+)))?/g;\nvar rtag = /^</;\nvar rtagend = /^<\\s*\\//;\n\nfunction createStack () {\n var stack = [];\n stack.lastItem = function lastItem () {\n return stack[stack.length - 1];\n };\n return stack;\n}\n\nfunction parser (html, handler) {\n var stack = createStack();\n var last = html;\n var chars;\n\n while (html) {\n parsePart();\n }\n parseEndTag(); // clean up any remaining tags\n\n function parsePart () {\n chars = true;\n parseTag();\n\n var same = html === last;\n last = html;\n\n if (same) { // discard, because it's invalid\n html = '';\n }\n }\n\n function parseTag () {\n if (html.substr(0, 4) === '<!--') { // comments\n parseComment();\n } else if (rtagend.test(html)) {\n parseEdge(rend, parseEndTag);\n } else if (rtag.test(html)) {\n parseEdge(rstart, parseStartTag);\n }\n parseTagDecode();\n }\n\n function parseEdge (regex, parser) {\n var match = html.match(regex);\n if (match) {\n html = html.substring(match[0].length);\n match[0].replace(regex, parser);\n chars = false;\n }\n }\n\n function parseComment () {\n var index = html.indexOf('-->');\n if (index >= 0) {\n if (handler.comment) {\n handler.comment(html.substring(4, index));\n }\n html = html.substring(index + 3);\n chars = false;\n }\n }\n\n function parseTagDecode () {\n if (!chars) {\n return;\n }\n var text;\n var index = html.indexOf('<');\n if (index >= 0) {\n text = html.substring(0, index);\n html = html.substring(index);\n } else {\n text = html;\n html = '';\n }\n if (handler.chars) {\n handler.chars(text);\n }\n }\n\n function parseStartTag (tag, tagName, rest, unary) {\n var attrs = {};\n var low = lowercase(tagName);\n var u = elements.voids[low] || !!unary;\n\n rest.replace(rattrs, attrReplacer);\n\n if (!u) {\n stack.push(low);\n }\n if (handler.start) {\n handler.start(low, attrs, u);\n }\n\n function attrReplacer (match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {\n if (doubleQuotedValue === void 0 && singleQuotedValue === void 0 && unquotedValue === void 0) {\n attrs[name] = void 0; // attribute is like <button disabled></button>\n } else {\n attrs[name] = he.decode(doubleQuotedValue || singleQuotedValue || unquotedValue || '');\n }\n }\n }\n\n function parseEndTag (tag, tagName) {\n var i;\n var pos = 0;\n var low = lowercase(tagName);\n if (low) {\n for (pos = stack.length - 1; pos >= 0; pos--) {\n if (stack[pos] === low) {\n break; // find the closest opened tag of the same type\n }\n }\n }\n if (pos >= 0) {\n for (i = stack.length - 1; i >= pos; i--) {\n if (handler.end) { // close all the open elements, up the stack\n handler.end(stack[i]);\n }\n }\n stack.length = pos;\n }\n }\n}\n\nmodule.exports = parser;\n","'use strict';\n\nvar he = require('he');\nvar lowercase = require('./lowercase');\nvar attributes = require('./attributes');\nvar elements = require('./elements');\n\nfunction sanitizer (buffer, options) {\n var last;\n var context;\n var o = options || {};\n\n reset();\n\n return {\n start: start,\n end: end,\n chars: chars\n };\n\n function out (value) {\n buffer.push(value);\n }\n\n function start (tag, attrs, unary) {\n var low = lowercase(tag);\n\n if (context.ignoring) {\n ignore(low); return;\n }\n if ((o.allowedTags || []).indexOf(low) === -1) {\n ignore(low); return;\n }\n if (o.filter && !o.filter({ tag: low, attrs: attrs })) {\n ignore(low); return;\n }\n\n out('<');\n out(low);\n Object.keys(attrs).forEach(parse);\n out(unary ? '/>' : '>');\n\n function parse (key) {\n var value = attrs[key];\n var classesOk = (o.allowedClasses || {})[low] || [];\n var attrsOk = (o.allowedAttributes || {})[low] || [];\n var valid;\n var lkey = lowercase(key);\n if (lkey === 'class' && attrsOk.indexOf(lkey) === -1) {\n value = value.split(' ').filter(isValidClass).join(' ').trim();\n valid = value.length;\n } else {\n valid = attrsOk.indexOf(lkey) !== -1 && (attributes.uris[lkey] !== true || testUrl(value));\n }\n if (valid) {\n out(' ');\n out(key);\n if (typeof value === 'string') {\n out('=\"');\n out(he.encode(value));\n out('\"');\n }\n }\n function isValidClass (className) {\n return classesOk && classesOk.indexOf(className) !== -1;\n }\n }\n }\n\n function end (tag) {\n var low = lowercase(tag);\n var allowed = (o.allowedTags || []).indexOf(low) !== -1;\n if (allowed) {\n if (context.ignoring === false) {\n out('</');\n out(low);\n out('>');\n } else {\n unignore(low);\n }\n } else {\n unignore(low);\n }\n }\n\n function testUrl (text) {\n var start = text[0];\n if (start === '#' || start === '/') {\n return true;\n }\n var colon = text.indexOf(':');\n if (colon === -1) {\n return true;\n }\n var questionmark = text.indexOf('?');\n if (questionmark !== -1 && colon > questionmark) {\n return true;\n }\n var hash = text.indexOf('#');\n if (hash !== -1 && colon > hash) {\n return true;\n }\n return o.allowedSchemes.some(matches);\n\n function matches (scheme) {\n return text.indexOf(scheme + ':') === 0;\n }\n }\n\n function chars (text) {\n if (context.ignoring === false) {\n out(o.transformText ? o.transformText(text) : text);\n }\n }\n\n function ignore (tag) {\n if (elements.voids[tag]) {\n return;\n }\n if (context.ignoring === false) {\n context = { ignoring: tag, depth: 1 };\n } else if (context.ignoring === tag) {\n context.depth++;\n }\n }\n\n function unignore (tag) {\n if (context.ignoring === tag) {\n if (--context.depth <= 0) {\n reset();\n }\n }\n }\n\n function reset () {\n context = { ignoring: false, depth: 0 };\n }\n}\n\nmodule.exports = sanitizer;\n","'use strict';\n\nvar defaults = {\n allowedAttributes: {\n a: ['href', 'name', 'target', 'title', 'aria-label'],\n iframe: ['allowfullscreen', 'frameborder', 'src'],\n img: ['src', 'alt', 'title', 'aria-label']\n },\n allowedClasses: {},\n allowedSchemes: ['http', 'https', 'mailto'],\n allowedTags: [\n 'a', 'abbr', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em',\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'main', 'mark',\n 'ol', 'p', 'pre', 'section', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table',\n 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'\n ],\n filter: null\n};\n\nmodule.exports = defaults;\n","'use strict';\n\nvar he = require('he');\nvar assign = require('assignment');\nvar parser = require('./parser');\nvar sanitizer = require('./sanitizer');\nvar defaults = require('./defaults');\n\nfunction insane (html, options, strict) {\n var buffer = [];\n var configuration = strict === true ? options : assign({}, defaults, options);\n var handler = sanitizer(buffer, configuration);\n\n parser(html, handler);\n\n return buffer.join('');\n}\n\ninsane.defaults = defaults;\nmodule.exports = insane;\n","import insane from 'insane'\n\nconst insaneOptions = {\n allowedClasses: {},\n // @refer CVE-2018-8495\n // @link https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-8495\n // @link https://leucosite.com/Microsoft-Edge-RCE/\n // @link https://medium.com/@knownsec404team/analysis-of-the-security-issues-of-url-scheme-in-pc-from-cve-2018-8495-934478a36756\n allowedSchemes: [\n 'http',\n 'https',\n 'mailto',\n 'data', // for support base64 encoded image (安全性有待考虑)\n ],\n allowedTags: [\n 'a',\n 'abbr',\n 'article',\n 'b',\n 'blockquote',\n 'br',\n 'caption',\n 'code',\n 'del',\n 'details',\n 'div',\n 'em',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'hr',\n 'i',\n 'img',\n 'ins',\n 'kbd',\n 'li',\n 'main',\n 'mark',\n 'ol',\n 'p',\n 'pre',\n 'section',\n 'span',\n 'strike',\n 'strong',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'th',\n 'thead',\n 'tr',\n 'u',\n 'ul',\n ],\n allowedAttributes: {\n '*': ['title', 'accesskey'],\n a: ['href', 'name', 'target', 'aria-label', 'rel'],\n img: ['src', 'alt', 'title', 'atk-emoticon', 'aria-label', 'data-src', 'class', 'loading'],\n // for code highlight\n code: ['class'],\n span: ['class', 'style'],\n },\n filter: (node) => {\n // class whitelist\n const allowed = [\n ['code', /^hljs\\W+language-(.*)$/],\n ['span', /^(hljs-.*)$/],\n ['img', /^lazyload$/],\n ]\n allowed.forEach(([tag, reg]) => {\n if (node.tag === tag && !!node.attrs.class && !(reg as RegExp).test(node.attrs.class)) {\n delete node.attrs.class\n }\n })\n\n // allow <span> set color sty\n if (\n node.tag === 'span' &&\n !!node.attrs.style &&\n !/^color:(\\W+)?#[0-9a-f]{3,6};?$/i.test(node.attrs.style)\n ) {\n delete node.attrs.style\n }\n\n return true\n },\n}\n\nexport function sanitize(content: string): string {\n // @link https://github.com/markedjs/marked/discussions/1232\n // @link https://gist.github.com/lionel-rowe/bb384465ba4e4c81a9c8dada84167225\n return insane(content, insaneOptions)\n}\n","(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (global.hanabi = factory());\n}(this, (function () { 'use strict';\n\nfunction createCommonjsModule(fn, module) {\n\treturn module = { exports: {} }, fn(module, module.exports), module.exports;\n}\n\nvar index$1 = createCommonjsModule(function (module) {\n'use strict';\n\nvar comment = module.exports = function () {\n\treturn new RegExp('(?:' + comment.line().source + ')|(?:' + comment.block().source + ')', 'gm');\n};\n\ncomment.line = function () {\n\treturn /(?:^|\\s)\\/\\/(.+?)$/gm;\n};\n\ncomment.block = function () {\n\treturn /\\/\\*([\\S\\s]*?)\\*\\//gm;\n};\n});\n\nvar defaultColors = ['23AC69', '91C132', 'F19726', 'E8552D', '1AAB8E', 'E1147F', '2980C1', '1BA1E6', '9FA0A0', 'F19726', 'E30B20', 'E30B20', 'A3338B'];\n\nvar index = function (input, ref) {\n if ( ref === void 0 ) ref = {};\n var colors = ref.colors; if ( colors === void 0 ) colors = defaultColors;\n\n var index = 0;\n var cache = {};\n var wordRe = /[\\u4E00-\\u9FFF\\u3400-\\u4dbf\\uf900-\\ufaff\\u3040-\\u309f\\uac00-\\ud7af\\u0400-\\u04FF]+|\\w+/;\n var leftAngleRe = /</;\n\n var re = new RegExp((\"(\" + (wordRe.source) + \"|\" + (leftAngleRe.source) + \")|(\" + (index$1().source) + \")\"), 'gmi');\n return input\n .replace(re, function (m, word, cm) {\n if (cm) {\n return toComment(cm)\n }\n\n if (word === '<') {\n return '<'\n }\n var color;\n if (cache[word]) {\n color = cache[word];\n } else {\n color = colors[index];\n cache[word] = color;\n }\n\n var out = \"<span style=\\\"color: #\" + color + \"\\\">\" + word + \"</span>\";\n index = ++index % colors.length;\n return out\n })\n};\n\nfunction toComment(cm) {\n return (\"<span style=\\\"color: slategray\\\">\" + cm + \"</span>\")\n}\n\nreturn index;\n\n})));\n","import hanabi from 'hanabi'\n\nexport function renderCode(code: string) {\n return hanabi(code)\n}\n","import { marked, Tokens } from 'marked'\nimport { renderCode } from './highlight'\nimport type { Config } from '@/types'\n\nexport interface RendererOptions {\n imgLazyLoad: Config['imgLazyLoad']\n}\n\nexport function getRenderer(options: RendererOptions) {\n const renderer = new marked.Renderer()\n renderer.link = markedLinkRenderer(renderer, renderer.link)\n renderer.code = markedCodeRenderer()\n renderer.image = markedImageRenderer(renderer, renderer.image, options)\n return renderer\n}\n\nconst markedLinkRenderer =\n (renderer: any, orgLinkRenderer: (args: Tokens.Link) => string) => (args: Tokens.Link) => {\n const getLinkOrigin = (link: string) => {\n try {\n return new URL(link).origin\n } catch {\n return ''\n }\n }\n const isSameOriginLink = getLinkOrigin(args.href) === window.location.origin\n const html = orgLinkRenderer.call(renderer, args)\n return html.replace(\n /^<a /,\n `<a target=\"_blank\" ${!isSameOriginLink ? `rel=\"noreferrer noopener nofollow\"` : ''} `,\n )\n }\n\nconst markedCodeRenderer =\n () =>\n ({ lang, text }: Tokens.Code): string => {\n // Colorize the block only if the language is known to highlight.js\n const realLang = !lang ? 'plaintext' : lang\n let colorized = text\n if ((window as any).hljs) {\n if (realLang && (window as any).hljs.getLanguage(realLang)) {\n colorized = (window as any).hljs.highlight(realLang, text).value\n }\n } else {\n colorized = renderCode(text)\n }\n\n return (\n `<pre rel=\"${realLang}\">\\n` +\n `<code class=\"hljs language-${realLang}\">${colorized.replace(/&/g, '&')}</code>\\n` +\n `</pre>`\n )\n }\n\nconst markedImageRenderer =\n (\n renderer: any,\n orgImageRenderer: (args: Tokens.Image) => string,\n { imgLazyLoad }: RendererOptions,\n ) =>\n (args: Tokens.Image): string => {\n const html = orgImageRenderer.call(renderer, args)\n if (!imgLazyLoad) return html\n if (imgLazyLoad === 'native' || (imgLazyLoad as any) === true)\n return html.replace(/^<img /, '<img class=\"lazyload\" loading=\"lazy\" ')\n if (imgLazyLoad === 'data-src')\n return html.replace(/^<img /, '<img class=\"lazyload\" ').replace('src=', 'data-src=')\n return html\n }\n","import { Marked } from 'marked'\nimport type { MarkedOptions } from 'marked'\n\nimport { sanitize } from './sanitizer'\nimport { renderCode } from './highlight'\nimport { getRenderer } from './marked-renderer'\nimport type { Config } from '@/types'\n\ntype Replacer = (raw: string) => string\n\nlet instance: Marked | undefined\nlet replacers: Replacer[] = []\n\nconst markedOptions: MarkedOptions = {\n gfm: true,\n breaks: true,\n async: false,\n}\n\n/** Get Marked instance */\nexport function getInstance() {\n return instance\n}\n\nexport function setReplacers(arr: Replacer[]) {\n replacers = arr\n}\n\nexport interface MarkedInitOptions {\n markedOptions: Config['markedOptions']\n imgLazyLoad: Config['imgLazyLoad']\n}\n\n/** 初始化 marked */\nexport function initMarked(options: MarkedInitOptions) {\n try {\n if (!Marked.name) return\n } catch {\n return\n }\n\n // @see https://github.com/markedjs/marked/blob/4afb228d956a415624c4e5554bb8f25d047676fe/src/Tokenizer.js#L329\n const marked = new Marked()\n marked.setOptions({\n renderer: getRenderer({\n imgLazyLoad: options.imgLazyLoad,\n }),\n ...markedOptions,\n ...options.markedOptions,\n })\n\n instance = marked\n}\n\n/** 解析 markdown */\nexport default function marked(src: string): string {\n let markedContent = getInstance()?.parse(src) as string\n if (!markedContent) {\n // 无 Markdown 模式简单处理\n markedContent = simpleMarked(src)\n }\n\n let dest = sanitize(markedContent)\n\n // 内容替换器\n replacers.forEach((replacer) => {\n if (typeof replacer === 'function') dest = replacer(dest)\n })\n\n return dest\n}\n\nfunction simpleMarked(src: string) {\n return (\n src\n // .replace(/\\*\\*(.*?)\\*\\*/g, '<strong>$1</strong>')\n .replace(\n /```\\s*([^]+?.*?[^]+?[^]+?)```/g,\n (_, code) => `<pre><code>${renderCode(code)}</code></pre>`,\n )\n // .replace(/`([^`]+?)`/g, '<code>$1</code>')\n .replace(/!\\[(.*?)\\]\\((.*?)\\)/g, (_, alt, imgSrc) => `<img src=\"${imgSrc}\" alt=\"${alt}\" />`)\n .replace(\n /\\[(.*?)\\]\\((.*?)\\)/g,\n (_, text, link) => `<a href=\"${link}\" target=\"_blank\">${text}</a>`,\n )\n .replace(/\\n/g, '<br>')\n )\n}\n","type PrimitiveKey = string | number | symbol\ntype Services = { [key: PrimitiveKey]: any }\n\nexport type Constructor<T, S = Services, D extends readonly (keyof S)[] = any> = (\n ...args: { [K in keyof D]: D[K] extends keyof S ? S[D[K]] : never }\n) => T\n\nexport type Lifecycle = 'transient' | 'singleton'\n\nexport interface Provider<T, S = Services> {\n /** Implementation constructor */\n impl: Constructor<T>\n\n /** Dependency constructors */\n deps: readonly (keyof S)[]\n\n /** Lifecycle */\n lifecycle: Lifecycle\n}\n\nexport interface ProvideFuncOptions {\n /** Lifecycle (default: 'singleton') */\n lifecycle?: Lifecycle\n}\n\nexport interface DependencyContainer<S = Services> {\n provide<K extends keyof S, T extends S[K] = any, D extends readonly (keyof S)[] = any>(\n key: K,\n impl: Constructor<T, S, D>,\n deps?: D,\n opts?: ProvideFuncOptions,\n ): void\n\n inject<T = undefined, K extends keyof S = any>(key: K): T extends undefined ? S[K] : T\n}\n\nexport function createInjectionContainer<S = Services>(): DependencyContainer<S> {\n const providers = new Map<PrimitiveKey, Provider<any, S>>()\n const initializedDeps = new Map<PrimitiveKey, any>()\n\n const provide: DependencyContainer<S>['provide'] = (key, impl, deps, opts = {}) => {\n providers.set(key, { impl, deps: deps || [], lifecycle: opts.lifecycle || 'singleton' })\n }\n\n const inject: DependencyContainer<S>['inject'] = (key) => {\n const provider = providers.get(key)\n if (!provider) {\n throw new Error(`No provide for ${String(key)}`)\n }\n\n if (provider.lifecycle === 'singleton' && initializedDeps.has(key)) {\n return initializedDeps.get(key)\n }\n\n const { impl, deps } = provider\n const params = deps.map((d) => inject(d))\n const resolved = impl(...params)\n\n initializedDeps.set(key, resolved)\n\n return resolved\n }\n\n return { provide, inject }\n}\n","import type {\n Config,\n ConfigPartial,\n CommentData,\n ListFetchParams,\n Context as IContext,\n SidebarShowPayload,\n Services,\n} from './types'\nimport * as I18n from './i18n'\nimport * as marked from './lib/marked'\nimport { createInjectionContainer } from './lib/injection'\nimport type { CheckerCaptchaPayload, CheckerPayload } from './components/checker'\n\n/**\n * Artalk Context\n */\nclass Context implements IContext {\n private _deps = createInjectionContainer<Services>()\n\n constructor(private _$root: HTMLElement) {}\n\n getEl(): HTMLElement {\n return this._$root\n }\n\n destroy(): void {\n this.trigger('unmounted')\n while (this._$root.firstChild) {\n this._$root.removeChild(this._$root.firstChild)\n }\n }\n\n // -------------------------------------------------------------------\n // Dependency Injection\n // -------------------------------------------------------------------\n provide: IContext['provide'] = (key, impl, deps, opts) => {\n this._deps.provide(key, impl, deps, opts)\n }\n\n inject: IContext['inject'] = (key) => {\n return this._deps.inject(key)\n }\n get = this.inject\n\n // -------------------------------------------------------------------\n // Event Manager\n // -------------------------------------------------------------------\n on: IContext['on'] = (name, handler) => {\n this.inject('events').on(name, handler)\n }\n\n off: IContext['off'] = (name, handler) => {\n this.inject('events').off(name, handler)\n }\n\n trigger: IContext['trigger'] = (name, payload) => {\n this.inject('events').trigger(name, payload)\n }\n\n // -------------------------------------------------------------------\n // Configurations\n // -------------------------------------------------------------------\n getConf(): Config {\n return this.inject('config').get()\n }\n\n updateConf(conf: ConfigPartial): void {\n this.inject('config').update(conf)\n }\n\n watchConf<T extends (keyof Config)[]>(\n keys: T,\n effect: (conf: Pick<Config, T[number]>) => void,\n ): void {\n this.inject('config').watchConf(keys, effect)\n }\n\n getMarked() {\n return marked.getInstance()\n }\n\n setDarkMode(darkMode: boolean | 'auto'): void {\n this.updateConf({ darkMode })\n }\n\n get conf() {\n return this.getConf()\n }\n set conf(val) {\n console.error('Cannot set config directly, please call updateConf()')\n }\n get $root() {\n return this.getEl()\n }\n set $root(val) {\n console.error('set $root is prohibited')\n }\n\n // -------------------------------------------------------------------\n // I18n: Internationalization\n // -------------------------------------------------------------------\n $t(key: I18n.I18nKeys, args: { [key: string]: string } = {}): string {\n return I18n.t(key, args)\n }\n\n // -------------------------------------------------------------------\n // HTTP API Client\n // -------------------------------------------------------------------\n getApi() {\n return this.inject('api')\n }\n\n getApiHandlers() {\n return this.inject('apiHandlers')\n }\n\n // -------------------------------------------------------------------\n // User Manager\n // -------------------------------------------------------------------\n getUser() {\n return this.inject('user')\n }\n\n // -------------------------------------------------------------------\n // Data Manager\n // -------------------------------------------------------------------\n getData() {\n return this.inject('data')\n }\n\n fetch(params: Partial<ListFetchParams>): void {\n this.getData().fetchComments(params)\n }\n\n reload(): void {\n this.getData().fetchComments({ offset: 0 })\n }\n\n // -------------------------------------------------------------------\n // List\n // -------------------------------------------------------------------\n listGotoFirst(): void {\n this.trigger('list-goto-first')\n }\n\n getCommentList = this.getCommentNodes\n getCommentNodes() {\n return this.inject('list').getCommentNodes()\n }\n\n getCommentDataList = this.getComments\n getComments() {\n return this.getData().getComments()\n }\n\n // -------------------------------------------------------------------\n // Editor\n // -------------------------------------------------------------------\n replyComment(commentData: CommentData, $comment: HTMLElement): void {\n this.inject('editor').setReplyComment(commentData, $comment)\n }\n\n editComment(commentData: CommentData, $comment: HTMLElement): void {\n this.inject('editor').setEditComment(commentData, $comment)\n }\n\n editorShowLoading(): void {\n this.inject('editor').showLoading()\n }\n\n editorHideLoading(): void {\n this.inject('editor').hideLoading()\n }\n\n editorShowNotify(msg, type): void {\n this.inject('editor').showNotify(msg, type)\n }\n\n editorResetState(): void {\n this.inject('editor').resetState()\n }\n\n // -------------------------------------------------------------------\n // Sidebar\n // -------------------------------------------------------------------\n showSidebar(payload?: SidebarShowPayload): void {\n this.inject('sidebar').show(payload)\n }\n\n hideSidebar(): void {\n this.inject('sidebar').hide()\n }\n\n // -------------------------------------------------------------------\n // Checker\n // -------------------------------------------------------------------\n checkAdmin(payload: CheckerPayload): Promise<void> {\n return this.inject('checkers').checkAdmin(payload)\n }\n\n checkCaptcha(payload: CheckerCaptchaPayload): Promise<void> {\n return this.inject('checkers').checkCaptcha(payload)\n }\n}\n\nexport default Context\n","/**\n * Performs a deep merge of objects and returns new object.\n * Does not modify objects (immutable) and merges arrays via concatenation.\n *\n * @param objects - Objects to merge\n * @returns New object with merged key/values\n */\nexport function mergeDeep<T>(...objects: any[]): T {\n const isObject = (obj: any) => obj && typeof obj === 'object' && obj.constructor === Object\n\n return objects.reduce((prev, obj) => {\n Object.keys(obj ?? {}).forEach((key) => {\n // Avoid prototype pollution\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n return\n }\n\n const pVal = prev[key]\n const oVal = obj[key]\n\n if (Array.isArray(pVal) && Array.isArray(oVal)) {\n prev[key] = pVal.concat(...oVal)\n } else if (isObject(pVal) && isObject(oVal)) {\n prev[key] = mergeDeep(pVal, oVal)\n } else {\n prev[key] = oVal\n }\n })\n\n return prev\n }, {})\n}\n","import type { Config } from '@/types'\n\nexport const Defaults: Readonly<RequiredExcept<Config, ExcludedKeys>> = {\n el: '',\n pageKey: '',\n pageTitle: '',\n server: '',\n site: '',\n\n placeholder: '',\n noComment: '',\n sendBtn: '',\n darkMode: false,\n editorTravel: true,\n\n flatMode: 'auto',\n nestMax: 2,\n nestSort: 'DATE_ASC',\n\n emoticons: ARTALK_LITE\n ? false\n : 'https://cdn.jsdelivr.net/gh/ArtalkJS/Emoticons/grps/default.json',\n\n pageVote: true,\n\n vote: ARTALK_LITE ? false : true,\n voteDown: false,\n uaBadge: ARTALK_LITE ? false : true,\n listSort: true,\n preview: ARTALK_LITE ? false : true,\n countEl: '.artalk-comment-count',\n pvEl: '.artalk-pv-count',\n statPageKeyAttr: 'data-page-key',\n\n gravatar: {\n mirror: 'https://www.gravatar.com/avatar/',\n params: 'sha256=1&d=mp&s=240',\n },\n\n pagination: {\n pageSize: 20,\n readMore: true,\n autoLoad: true,\n },\n\n heightLimit: {\n content: 300,\n children: 400,\n scrollable: false,\n },\n\n imgUpload: true,\n imgLazyLoad: false,\n reqTimeout: 15000,\n versionCheck: true,\n useBackendConf: true,\n preferRemoteConf: false,\n listUnreadHighlight: false,\n pvAdd: true,\n fetchCommentsOnInit: true,\n\n locale: 'en',\n apiVersion: '',\n pluginURLs: [],\n markedReplacers: [],\n markedOptions: {},\n}\n\ntype RequiredExcept<T, K extends keyof T> = Required<Omit<T, K>> & Pick<T, K>\ntype FunctionKeys<T> = Exclude<\n { [K in keyof T]: NonNullable<T[K]> extends (...args: any[]) => any ? K : never }[keyof T],\n undefined\n>\ntype ExcludedKeys = FunctionKeys<Config>\n","import type { ApiOptions } from './api/options'\nimport { mergeDeep } from './lib/merge-deep'\nimport type { ApiHandlers } from './api'\nimport { Defaults } from './defaults'\nimport type { Config, ConfigPartial, UserManager } from '@/types'\n\n/**\n * Handle the custom config which is provided by the user\n *\n * @param customConf - The custom config object which is provided by the user\n * @param full - If `full` is `true`, the return value will be the complete config for Artalk instance creation\n * @returns The config for Artalk instance creation\n */\nexport function handelCustomConf(customConf: ConfigPartial, full: true): Config\nexport function handelCustomConf(customConf: ConfigPartial, full?: false): ConfigPartial\nexport function handelCustomConf(customConf: ConfigPartial, full = false) {\n // Merge default config\n const conf: ConfigPartial = full ? mergeDeep(Defaults, customConf) : customConf\n\n // Default pageKey\n if (conf.pageKey === '') conf.pageKey = `${window.location.pathname}` // @see http://bl.ocks.org/abernier/3070589\n\n // Default pageTitle\n if (conf.pageTitle === '') conf.pageTitle = `${document.title}`\n\n // Server\n if (conf.server) conf.server = conf.server.replace(/\\/$/, '').replace(/\\/api\\/?$/, '')\n\n // Language auto-detection\n if (conf.locale === 'auto') conf.locale = navigator.language\n\n // Flat mode auto-detection\n if (conf.flatMode === 'auto') conf.flatMode = window.matchMedia('(max-width: 768px)').matches\n\n // Change flatMode by nestMax\n if (typeof conf.nestMax === 'number' && Number(conf.nestMax) <= 1) conf.flatMode