UNPKG

@ovv/artalk

Version:

A self-hosted comment system

1 lines 622 kB
{"version":3,"file":"Artalk.cjs","sources":["../src/lib/utils.ts","../src/i18n/en.ts","../src/i18n/external.ts","../src/i18n/index.ts","../src/i18n/zh-CN.ts","../../../node_modules/.pnpm/marked@14.1.3/node_modules/marked/lib/marked.esm.js","../../../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/insane.js","../../../node_modules/.pnpm/insane@2.6.2/node_modules/insane/defaults.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/context.ts","../src/lib/injection/container.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/index.ts","../src/plugins/editor/_plug.ts","../src/plugins/editor/local-storage.ts","../src/plugins/editor/submit-add.ts","../src/plugins/editor/submit.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/header-event.ts","../src/plugins/editor/header-user.ts","../src/plugins/editor/header-link.ts","../src/plugins/editor/textarea.ts","../src/plugins/editor/submit-btn.ts","../src/plugins/editor/state-reply.ts","../src/plugins/editor/state-edit.ts","../src/plugins/editor/closable.ts","../src/plugins/editor/_kit.ts","../src/lib/event-manager.ts","../src/plugins/editor-kit.ts","../src/plugins/list/goto-focus.ts","../src/components/error-dialog.ts","../src/plugins/list/error-dialog.ts","../src/plugins/list/index.ts","../src/plugins/list/fetch.ts","../src/plugins/list/loading.ts","../src/plugins/list/unread.ts","../src/plugins/list/with-editor.ts","../src/plugins/list/count.ts","../src/plugins/list/sidebar-btn.ts","../src/plugins/list/unread-badge.ts","../src/plugins/list/dropdown.ts","../src/plugins/list/goto-dispatcher.ts","../src/plugins/list/no-comment.ts","../src/plugins/list/copyright.ts","../src/plugins/list/time-ticking.ts","../src/plugins/list/reach-bottom.ts","../src/plugins/list/goto-first.ts","../src/plugins/version-check.ts","../src/plugins/dark-mode.ts","../src/plugins/page-vote.ts","../src/services/api.ts","../src/lib/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/ui.ts","../src/editor/state.ts","../src/editor/editor.ts","../src/editor/editor.html?raw","../src/data.ts","../src/layer/layer.ts","../src/layer/scrollbar-helper.ts","../src/layer/wrap.ts","../src/layer/layer-manager.ts","../src/list/layout/nest.ts","../src/list/nest.ts","../src/list/layout/flat.ts","../src/list/layout/index.ts","../src/comment/height-limit.ts","../src/comment/renders/header.ts","../src/components/action-btn.ts","../src/comment/renders/actions.ts","../src/comment/renders/index.ts","../src/comment/renders/avatar.ts","../src/comment/renders/content.ts","../src/comment/renders/reply-at.ts","../src/comment/renders/reply-to.ts","../src/comment/renders/pending.ts","../src/comment/render.ts","../src/comment/comment.html?raw","../src/comment/actions.ts","../src/comment/comment-node.ts","../src/lib/detect.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/list/list.html?raw","../src/list/comment.ts","../src/layer/sidebar-layer.ts","../src/layer/sidebar-layer.html?raw","../src/services/sidebar.ts","../src/plugins/index.ts","../src/services/index.ts","../src/services/data.ts","../src/api/handler.ts","../src/services/i18n.ts","../src/services/user.ts","../src/services/editor.ts","../src/services/list.ts","../src/services/layer.ts","../src/plugins/markdown.ts","../src/plugins/admin-only-elem.ts","../src/plugins/notifies.ts","../src/mount.ts","../src/plugins/mount-error.ts","../src/lib/watch-conf.ts","../src/artalk.ts","../src/services/events.ts","../src/services/config.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\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","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","/**\n * marked v14.1.3 - a markdown parser\n * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed)\n * https://github.com/markedjs/marked\n */\n\n/**\n * DO NOT EDIT THIS FILE\n * The code in this file is generated from files in ./src/\n */\n\n/**\n * Gets the original marked default options.\n */\nfunction _getDefaults() {\n return {\n async: false,\n breaks: false,\n extensions: null,\n gfm: true,\n hooks: null,\n pedantic: false,\n renderer: null,\n silent: false,\n tokenizer: null,\n walkTokens: null,\n };\n}\nlet _defaults = _getDefaults();\nfunction changeDefaults(newDefaults) {\n _defaults = newDefaults;\n}\n\n/**\n * Helpers\n */\nconst escapeTest = /[&<>\"']/;\nconst escapeReplace = new RegExp(escapeTest.source, 'g');\nconst escapeTestNoEncode = /[<>\"']|&(?!(#\\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\\w+);)/;\nconst escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');\nconst escapeReplacements = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n};\nconst getEscapeReplacement = (ch) => escapeReplacements[ch];\nfunction escape$1(html, encode) {\n if (encode) {\n if (escapeTest.test(html)) {\n return html.replace(escapeReplace, getEscapeReplacement);\n }\n }\n else {\n if (escapeTestNoEncode.test(html)) {\n return html.replace(escapeReplaceNoEncode, getEscapeReplacement);\n }\n }\n return html;\n}\nconst caret = /(^|[^\\[])\\^/g;\nfunction edit(regex, opt) {\n let source = typeof regex === 'string' ? regex : regex.source;\n opt = opt || '';\n const obj = {\n replace: (name, val) => {\n let valSource = typeof val === 'string' ? val : val.source;\n valSource = valSource.replace(caret, '$1');\n source = source.replace(name, valSource);\n return obj;\n },\n getRegex: () => {\n return new RegExp(source, opt);\n },\n };\n return obj;\n}\nfunction cleanUrl(href) {\n try {\n href = encodeURI(href).replace(/%25/g, '%');\n }\n catch {\n return null;\n }\n return href;\n}\nconst noopTest = { exec: () => null };\nfunction splitCells(tableRow, count) {\n // ensure that every cell-delimiting pipe has a space\n // before it to distinguish it from an escaped pipe\n const row = tableRow.replace(/\\|/g, (match, offset, str) => {\n let escaped = false;\n let curr = offset;\n while (--curr >= 0 && str[curr] === '\\\\')\n escaped = !escaped;\n if (escaped) {\n // odd number of slashes means | is escaped\n // so we leave it alone\n return '|';\n }\n else {\n // add space before unescaped |\n return ' |';\n }\n }), cells = row.split(/ \\|/);\n let i = 0;\n // First/last cell in a row cannot be empty if it has no leading/trailing pipe\n if (!cells[0].trim()) {\n cells.shift();\n }\n if (cells.length > 0 && !cells[cells.length - 1].trim()) {\n cells.pop();\n }\n if (count) {\n if (cells.length > count) {\n cells.splice(count);\n }\n else {\n while (cells.length < count)\n cells.push('');\n }\n }\n for (; i < cells.length; i++) {\n // leading or trailing whitespace is ignored per the gfm spec\n cells[i] = cells[i].trim().replace(/\\\\\\|/g, '|');\n }\n return cells;\n}\n/**\n * Remove trailing 'c's. Equivalent to str.replace(/c*$/, '').\n * /c*$/ is vulnerable to REDOS.\n *\n * @param str\n * @param c\n * @param invert Remove suffix of non-c chars instead. Default falsey.\n */\nfunction rtrim(str, c, invert) {\n const l = str.length;\n if (l === 0) {\n return '';\n }\n // Length of suffix matching the invert condition.\n let suffLen = 0;\n // Step left until we fail to match the invert condition.\n while (suffLen < l) {\n const currChar = str.charAt(l - suffLen - 1);\n if (currChar === c && !invert) {\n suffLen++;\n }\n else if (currChar !== c && invert) {\n suffLen++;\n }\n else {\n break;\n }\n }\n return str.slice(0, l - suffLen);\n}\nfunction findClosingBracket(str, b) {\n if (str.indexOf(b[1]) === -1) {\n return -1;\n }\n let level = 0;\n for (let i = 0; i < str.length; i++) {\n if (str[i] === '\\\\') {\n i++;\n }\n else if (str[i] === b[0]) {\n level++;\n }\n else if (str[i] === b[1]) {\n level--;\n if (level < 0) {\n return i;\n }\n }\n }\n return -1;\n}\n\nfunction outputLink(cap, link, raw, lexer) {\n const href = link.href;\n const title = link.title ? escape$1(link.title) : null;\n const text = cap[1].replace(/\\\\([\\[\\]])/g, '$1');\n if (cap[0].charAt(0) !== '!') {\n lexer.state.inLink = true;\n const token = {\n type: 'link',\n raw,\n href,\n title,\n text,\n tokens: lexer.inlineTokens(text),\n };\n lexer.state.inLink = false;\n return token;\n }\n return {\n type: 'image',\n raw,\n href,\n title,\n text: escape$1(text),\n };\n}\nfunction indentCodeCompensation(raw, text) {\n const matchIndentToCode = raw.match(/^(\\s+)(?:```)/);\n if (matchIndentToCode === null) {\n return text;\n }\n const indentToCode = matchIndentToCode[1];\n return text\n .split('\\n')\n .map(node => {\n const matchIndentInNode = node.match(/^\\s+/);\n if (matchIndentInNode === null) {\n return node;\n }\n const [indentInNode] = matchIndentInNode;\n if (indentInNode.length >= indentToCode.length) {\n return node.slice(indentToCode.length);\n }\n return node;\n })\n .join('\\n');\n}\n/**\n * Tokenizer\n */\nclass _Tokenizer {\n options;\n rules; // set by the lexer\n lexer; // set by the lexer\n constructor(options) {\n this.options = options || _defaults;\n }\n space(src) {\n const cap = this.rules.block.newline.exec(src);\n if (cap && cap[0].length > 0) {\n return {\n type: 'space',\n raw: cap[0],\n };\n }\n }\n code(src) {\n const cap = this.rules.block.code.exec(src);\n if (cap) {\n const text = cap[0].replace(/^(?: {1,4}| {0,3}\\t)/gm, '');\n return {\n type: 'code',\n raw: cap[0],\n codeBlockStyle: 'indented',\n text: !this.options.pedantic\n ? rtrim(text, '\\n')\n : text,\n };\n }\n }\n fences(src) {\n const cap = this.rules.block.fences.exec(src);\n if (cap) {\n const raw = cap[0];\n const text = indentCodeCompensation(raw, cap[3] || '');\n return {\n type: 'code',\n raw,\n lang: cap[2] ? cap[2].trim().replace(this.rules.inline.anyPunctuation, '$1') : cap[2],\n text,\n };\n }\n }\n heading(src) {\n const cap = this.rules.block.heading.exec(src);\n if (cap) {\n let text = cap[2].trim();\n // remove trailing #s\n if (/#$/.test(text)) {\n const trimmed = rtrim(text, '#');\n if (this.options.pedantic) {\n text = trimmed.trim();\n }\n else if (!trimmed || / $/.test(trimmed)) {\n // CommonMark requires space before trailing #s\n text = trimmed.trim();\n }\n }\n return {\n type: 'heading',\n raw: cap[0],\n depth: cap[1].length,\n text,\n tokens: this.lexer.inline(text),\n };\n }\n }\n hr(src) {\n const cap = this.rules.block.hr.exec(src);\n if (cap) {\n return {\n type: 'hr',\n raw: rtrim(cap[0], '\\n'),\n };\n }\n }\n blockquote(src) {\n const cap = this.rules.block.blockquote.exec(src);\n if (cap) {\n let lines = rtrim(cap[0], '\\n').split('\\n');\n let raw = '';\n let text = '';\n const tokens = [];\n while (lines.length > 0) {\n let inBlockquote = false;\n const currentLines = [];\n let i;\n for (i = 0; i < lines.length; i++) {\n // get lines up to a continuation\n if (/^ {0,3}>/.test(lines[i])) {\n currentLines.push(lines[i]);\n inBlockquote = true;\n }\n else if (!inBlockquote) {\n currentLines.push(lines[i]);\n }\n else {\n break;\n }\n }\n lines = lines.slice(i);\n const currentRaw = currentLines.join('\\n');\n const currentText = currentRaw\n // precede setext continuation with 4 spaces so it isn't a setext\n .replace(/\\n {0,3}((?:=+|-+) *)(?=\\n|$)/g, '\\n $1')\n .replace(/^ {0,3}>[ \\t]?/gm, '');\n raw = raw ? `${raw}\\n${currentRaw}` : currentRaw;\n text = text ? `${text}\\n${currentText}` : currentText;\n // parse blockquote lines as top level tokens\n // merge paragraphs if this is a continuation\n const top = this.lexer.state.top;\n this.lexer.state.top = true;\n this.lexer.blockTokens(currentText, tokens, true);\n this.lexer.state.top = top;\n // if there is no continuation then we are done\n if (lines.length === 0) {\n break;\n }\n const lastToken = tokens[tokens.length - 1];\n if (lastToken?.type === 'code') {\n // blockquote continuation cannot be preceded by a code block\n break;\n }\n else if (lastToken?.type === 'blockquote') {\n // include continuation in nested blockquote\n const oldToken = lastToken;\n const newText = oldToken.raw + '\\n' + lines.join('\\n');\n const newToken = this.blockquote(newText);\n tokens[tokens.length - 1] = newToken;\n raw = raw.substring(0, raw.length - oldToken.raw.length) + newToken.raw;\n text = text.substring(0, text.length - oldToken.text.length) + newToken.text;\n break;\n }\n else if (lastToken?.type === 'list') {\n // include continuation in nested list\n const oldToken = lastToken;\n const newText = oldToken.raw + '\\n' + lines.join('\\n');\n const newToken = this.list(newText);\n tokens[tokens.length - 1] = newToken;\n raw = raw.substring(0, raw.length - lastToken.raw.length) + newToken.raw;\n text = text.substring(0, text.length - oldToken.raw.length) + newToken.raw;\n lines = newText.substring(tokens[tokens.length - 1].raw.length).split('\\n');\n continue;\n }\n }\n return {\n type: 'blockquote',\n raw,\n tokens,\n text,\n };\n }\n }\n list(src) {\n let cap = this.rules.block.list.exec(src);\n if (cap) {\n let bull = cap[1].trim();\n const isordered = bull.length > 1;\n const list = {\n type: 'list',\n raw: '',\n ordered: isordered,\n start: isordered ? +bull.slice(0, -1) : '',\n loose: false,\n items: [],\n };\n bull = isordered ? `\\\\d{1,9}\\\\${bull.slice(-1)}` : `\\\\${bull}`;\n if (this.options.pedantic) {\n bull = isordered ? bull : '[*+-]';\n }\n // Get next list item\n const itemRegex = new RegExp(`^( {0,3}${bull})((?:[\\t ][^\\\\n]*)?(?:\\\\n|$))`);\n let endsWithBlankLine = false;\n // Check if current bullet point can start a new List Item\n while (src) {\n let endEarly = false;\n let raw = '';\n let itemContents = '';\n if (!(cap = itemRegex.exec(src))) {\n break;\n }\n if (this.rules.block.hr.test(src)) { // End list if bullet was actually HR (possibly move into itemRegex?)\n break;\n }\n raw = cap[0];\n src = src.substring(raw.length);\n let line = cap[2].split('\\n', 1)[0].replace(/^\\t+/, (t) => ' '.repeat(3 * t.length));\n let nextLine = src.split('\\n', 1)[0];\n let blankLine = !line.trim();\n let indent = 0;\n if (this.options.pedantic) {\n indent = 2;\n itemContents = line.trimStart();\n }\n else if (blankLine) {\n indent = cap[1].length + 1;\n }\n else {\n indent = cap[2].search(/[^ ]/); // Find first non-space char\n indent = indent > 4 ? 1 : indent; // Treat indented code blocks (> 4 spaces) as having only 1 indent\n itemContents = line.slice(indent);\n indent += cap[1].length;\n }\n if (blankLine && /^[ \\t]*$/.test(nextLine)) { // Items begin with at most one blank line\n raw += nextLine + '\\n';\n src = src.substring(nextLine.length + 1);\n endEarly = true;\n }\n if (!endEarly) {\n const nextBulletRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\\\d{1,9}[.)])((?:[ \\t][^\\\\n]*)?(?:\\\\n|$))`);\n const hrRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\\\* *){3,})(?:\\\\n+|$)`);\n const fencesBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\\`\\`\\`|~~~)`);\n const headingBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`);\n const htmlBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}<[a-z].*>`, 'i');\n // Check if following lines should be included in List Item\n while (src) {\n const rawLine = src.split('\\n', 1)[0];\n let nextLineWithoutTabs;\n nextLine = rawLine;\n // Re-align to follow commonmark nesting rules\n if (this.options.pedantic) {\n nextLine = nextLine.replace(/^ {1,4}(?=( {4})*[^ ])/g, ' ');\n nextLineWithoutTabs = nextLine;\n }\n else {\n nextLineWithoutTabs = nextLine.replace(/\\t/g, ' ');\n }\n // End list item if found code fences\n if (fencesBeginRegex.test(nextLine)) {\n break;\n }\n // End list item if found start of new heading\n if (headingBeginRegex.test(nextLine)) {\n break;\n }\n // End list item if found start of html block\n if (htmlBeginRegex.test(nextLine)) {\n break;\n }\n // End list item if found start of new bullet\n if (nextBulletRegex.test(nextLine)) {\n break;\n }\n // Horizontal rule found\n if (hrRegex.test(nextLine)) {\n break;\n }\n if (nextLineWithoutTabs.search(/[^ ]/) >= indent || !nextLine.trim()) { // Dedent if possible\n itemContents += '\\n' + nextLineWithoutTabs.slice(indent);\n }\n else {\n // not enough indentation\n if (blankLine) {\n break;\n }\n // paragraph continuation unless last line was a different block level element\n if (line.replace(/\\t/g, ' ').search(/[^ ]/) >= 4) { // indented code block\n break;\n }\n if (fencesBeginRegex.test(line)) {\n break;\n }\n if (headingBeginRegex.test(line)) {\n break;\n }\n if (hrRegex.test(line)) {\n break;\n }\n itemContents += '\\n' + nextLine;\n }\n if (!blankLine && !nextLine.trim()) { // Check if current line is blank\n blankLine = true;\n }\n raw += rawLine + '\\n';\n src = src.substring(rawLine.length + 1);\n line = nextLineWithoutTabs.slice(indent);\n }\n }\n if (!list.loose) {\n // If the previous item ended with a blank line, the list is loose\n if (endsWithBlankLine) {\n list.loose = true;\n }\n else if (/\\n[ \\t]*\\n[ \\t]*$/.test(raw)) {\n endsWithBlankLine = true;\n }\n }\n let istask = null;\n let ischecked;\n // Check for task list items\n if (this.options.gfm) {\n istask = /^\\[[ xX]\\] /.exec(itemContents);\n if (istask) {\n ischecked = istask[0] !== '[ ] ';\n itemContents = itemContents.replace(/^\\[[ xX]\\] +/, '');\n }\n }\n list.items.push({\n type: 'list_item',\n raw,\n task: !!istask,\n checked: ischecked,\n loose: false,\n text: itemContents,\n tokens: [],\n });\n list.raw += raw;\n }\n // Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic\n list.items[list.items.length - 1].raw = list.items[list.items.length - 1].raw.trimEnd();\n list.items[list.items.length - 1].text = list.items[list.items.length - 1].text.trimEnd();\n list.raw = list.raw.trimEnd();\n // Item child tokens handled here at end because we needed to have the final item to trim it first\n for (let i = 0; i < list.items.length; i++) {\n this.lexer.state.top = false;\n list.items[i].tokens = this.lexer.blockTokens(list.items[i].text, []);\n if (!list.loose) {\n // Check if list should be loose\n const spacers = list.items[i].tokens.filter(t => t.type === 'space');\n const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => /\\n.*\\n/.test(t.raw));\n list.loose = hasMultipleLineBreaks;\n }\n }\n // Set all items to loose if list is loose\n if (list.loose) {\n for (let i = 0; i < list.items.length; i++) {\n list.items[i].loose = true;\n }\n }\n return list;\n }\n }\n html(src) {\n const cap = this.rules.block.html.exec(src);\n if (cap) {\n const token = {\n type: 'html',\n block: true,\n raw: cap[0],\n pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style',\n text: cap[0],\n };\n return token;\n }\n }\n def(src) {\n const cap = this.rules.block.def.exec(src);\n if (cap) {\n const tag = cap[1].toLowerCase().replace(/\\s+/g, ' ');\n const href = cap[2] ? cap[2].replace(/^<(.*)>$/, '$1').replace(this.rules.inline.anyPunctuation, '$1') : '';\n const title = cap[3] ? cap[3].substring(1, cap[3].length - 1).replace(this.rules.inline.anyPunctuation, '$1') : cap[3];\n return {\n type: 'def',\n tag,\n raw: cap[0],\n href,\n title,\n };\n }\n }\n table(src) {\n const cap = this.rules.block.table.exec(src);\n if (!cap) {\n return;\n }\n if (!/[:|]/.test(cap[2])) {\n // delimiter row must have a pipe (|) or colon (:) otherwise it is a setext heading\n return;\n }\n const headers = splitCells(cap[1]);\n const aligns = cap[2].replace(/^\\||\\| *$/g, '').split('|');\n const rows = cap[3] && cap[3].trim() ? cap[3].replace(/\\n[ \\t]*$/, '').split('\\n') : [];\n const item = {\n type: 'table',\n raw: cap[0],\n header: [],\n align: [],\n rows: [],\n };\n if (headers.length !== aligns.length) {\n // header and align columns must be equal, rows can be different.\n return;\n }\n for (const align of aligns) {\n if (/^ *-+: *$/.test(align)) {\n item.align.push('right');\n }\n else if (/^ *:-+: *$/.test(align)) {\n item.align.push('center');\n }\n else if (/^ *:-+ *$/.test(align)) {\n item.align.push('left');\n }\n else {\n item.align.push(null);\n }\n }\n for (let i = 0; i < headers.length; i++) {\n item.header.push({\n text: headers[i],\n tokens: this.lexer.inline(headers[i]),\n header: true,\n align: item.align[i],\n });\n }\n for (const row of rows) {\n item.rows.push(splitCells(row, item.header.length).map((cell, i) => {\n return {\n text: cell,\n tokens: this.lexer.inline(cell),\n header: false,\n align: item.align[i],\n };\n }));\n }\n return item;\n }\n lheading(src) {\n const cap = this.rules.block.lheading.exec(src);\n if (cap) {\n return {\n type: 'heading',\n raw: cap[0],\n depth: cap[2].charAt(0) === '=' ? 1 : 2,\n text: cap[1],\n tokens: this.lexer.inline(cap[1]),\n };\n }\n }\n paragraph(src) {\n const cap = this.rules.block.paragraph.exec(src);\n if (cap) {\n const text = cap[1].charAt(cap[1].length - 1) === '\\n'\n ? cap[1].slice(0, -1)\n : cap[1];\n return {\n type: 'paragraph',\n raw: cap[0],\n text,\n tokens: this.lexer.inline(text),\n };\n }\n }\n text(src) {\n const cap = this.rules.block.text.exec(src);\n if (cap) {\n return {\n type: 'text',\n raw: cap[0],\n text: cap[0],\n tokens: this.lexer.inline(cap[0]),\n };\n }\n }\n escape(src) {\n const cap = this.rules.inline.escape.exec(src);\n if (cap) {\n return {\n type: 'escape',\n raw: cap[0],\n text: escape$1(cap[1]),\n };\n }\n }\n tag(src) {\n const cap = this.rules.inline.tag.exec(src);\n if (cap) {\n if (!this.lexer.state.inLink && /^<a /i.test(cap[0])) {\n this.lexer.state.inLink = true;\n }\n else if (this.lexer.state.inLink && /^<\\/a>/i.test(cap[0])) {\n this.lexer.state.inLink = false;\n }\n if (!this.lexer.state.inRawBlock && /^<(pre|code|kbd|script)(\\s|>)/i.test(cap[0])) {\n this.lexer.state.inRawBlock = true;\n }\n else if (this.lexer.state.inRawBlock && /^<\\/(pre|code|kbd|script)(\\s|>)/i.test(cap[0])) {\n this.lexer.state.inRawBlock = false;\n }\n return {\n type: 'html',\n raw: cap[0],\n inLink: this.lexer.state.inLink,\n inRawBlock: this.lexer.state.inRawBlock,\n block: false,\n text: cap[0],\n };\n }\n }\n link(src) {\n const cap = this.rules.inline.link.exec(src);\n if (cap) {\n const trimmedUrl = cap[2].trim();\n if (!this.options.pedantic && /^</.test(trimmedUrl)) {\n // commonmark requires matching angle brackets\n if (!(/>$/.test(trimmedUrl))) {\n return;\n }\n // ending angle bracket cannot be escaped\n const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\\\');\n if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) {\n return;\n }\n }\n else {\n // find closing parenthesis\n const lastParenIndex = findClosingBracket(cap[2], '()');\n if (lastParenIndex > -1) {\n const start = cap[0].indexOf('!') === 0 ? 5 : 4;\n const linkLen = start + cap[1].length + lastParenIndex;\n cap[2] = cap[2].substring(0, lastParenIndex);\n cap[0] = cap[0].substring(0, linkLen).trim();\n cap[3] = '';\n }\n }\n let href = cap[2];\n let title = '';\n if (this.options.pedantic) {\n // split pedantic href and title\n const link = /^([^'\"]*[^\\s])\\s+(['\"])(.*)\\2/.exec(href);\n if (link) {\n href = link[1];\n title = link[3];\n }\n }\n else {\n title = cap[3] ? cap[3].slice(1, -1) : '';\n }\n href = href.trim();\n if (/^</.test(href)) {\n if (this.options.pedantic && !(/>$/.test(trimmedUrl))) {\n // pedantic allows starting angle bracket without ending angle bracket\n href = href.slice(1);\n }\n else {\n href = href.slice(1, -1);\n }\n }\n return outputLink(cap, {\n href: href ? href.replace(this.rules.inline.anyPunctuation, '$1') : href,\n title: title ? title.replace(this.rules.inline.anyPunctuation, '$1') : title,\n }, cap[0], this.lexer);\n }\n }\n reflink(src, links) {\n let cap;\n if ((cap = this.rules.inline.reflink.exec(src))\n || (cap = this.rules.inline.nolink.exec(src))) {\n const linkString = (cap[2] || cap[1]).replace(/\\s+/g, ' ');\n const link = links[linkString.toLowerCase()];\n if (!link) {\n const text = cap[0].charAt(0);\n return {\n type: 'text',\n raw: text,\n text,\n };\n }\n return outputLink(cap, link, cap[0], this.lexer);\n }\n }\n emStrong(src, maskedSrc, prevChar = '') {\n let match = this.rules.inline.emStrongLDelim.exec(src);\n if (!match)\n return;\n // _ can't be between two alphanumerics. \\p{L}\\p{N} includes non-english alphabet/numbers as well\n if (match[3] && prevChar.match(/[\\p{L}\\p{N}]/u))\n return;\n const nextChar = match[1] || match[2] || '';\n if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) {\n // unicode Regex counts emoji as 1 char; spread into array for proper count (used multiple times below)\n const lLength = [...match[0]].length - 1;\n let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0;\n const endReg = match[0][0] === '*' ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd;\n endReg.lastIndex = 0;\n // Clip maskedSrc to same section of string as src (move to lexer?)\n maskedSrc = maskedSrc.slice(-1 * src.length + lLength);\n while ((match = endReg.exec(maskedSrc)) != null) {\n rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6];\n if (!rDelim)\n continue; // skip single * in __abc*abc__\n rLength = [...rDelim].length;\n if (match[3] || match[4]) { // found another Left Delim\n delimTotal += rLength;\n continue;\n }\n else if (match[5] || match