diff --git a/README.md b/README.md index bfec0b4..2610f35 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,30 @@ -# The Snowdrop Messenger -Roadmap: - -- [x] WRAC(S) support -- [x] User Agents -- [x] Server list Management -- [ ] Notifications -- [ ] Settings -- [x] Authentication -- [ ] GitHub repository -- [ ] Web version -- [ ] Mobile UI -- [ ] Mobile version -- [ ] Proxy for RAC(S) -- [ ] RAC(S) support -- [ ] Desktop Version +[English](README_EN.md) + + + + + +# Что такое Snowdrop? + +***Snowdrop Messenger*** - это красивый, современный, многофункциональный и кроссплатформенный мессенджер на базе протокола обмена сообщениями RAC, а также его новейших дополнений + + # Какие преемущества у Snowdrop среди других RAC клиентов? + +В ***TSM*** есть уникальные функции, использовать которые можно через удобный графический интерфейс, также ***Snowdrop*** имеет очень малый вес, потому-что написан на чистом JavaScript с использованием своих библиотек и технологий - ВЕСЬ код, что есть в мессенджере СОВЕРШЕННО ТОЧНО используется, никакие строчки не лежат без дела занимая место +Также присутствует ПОЛНЫЙ ПЕРЕВОД на русский и английский + +# Какие различия у разных версий Snowdrop? + +| TSM | Web | Desktop | Mobile | +|---|---|---|---| +| Нужен выход в интернет | ✅ | ❌ | ❌ | +| Нужно скачивать | ❌ | ✅ | ✅ | +| Поддержка WRAC протокола | ✅ | ✅ | ✅ | +| Поддержка RAC протокола | ❌ | ✅ | ❌ | +| Поддержка SSL/TLS | ✅ | ✅ | ✅ | +| Авто-обновление | ❌ | ✅ | ❌ | +| Бесплатно и открытый код | ✅ | ✅ | ✅ | + +# А где скачать? + +Пока-что доступна только Web версия, вскоре выйдет Mobile (Android и iOS) и Desktop (ПК) diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..1102322 --- /dev/null +++ b/README_EN.md @@ -0,0 +1,28 @@ + + + + +# What is Snowdrop? + +***Snowdrop Messenger*** is a beautiful, modern, multifunctional and cross-platform messenger based on the RAC messaging protocol, as well as its latest additions + +# What are the advantages of Snowdrop among other RAC clients? + +***TSM*** has unique functions that can be used through a convenient graphical interface, also ***Snowdrop*** has a very small weight, because it is written in pure JavaScript using its own libraries and technologies - ALL the code that is in the messenger is ABSOLUTELY EXACTLY used, no lines lie idle taking up space +Also there is a FULL TRANSLATION into Russian and English + +# What are the differences between the different versions of Snowdrop? + +| TSM | Web | Desktop | Mobile | +|---|---|---|---| +| Internet access required | ✅ | ❌ | ❌ | +| Need to download | ❌ | ✅ | ✅ | +| WRAC protocol support | ✅ | ✅ | ✅ | +| RAC protocol support | ❌ | ✅ | ❌ | +| SSL/TLS support | ✅ | ✅ | ✅ | +| Auto-update | ❌ | ✅ | ❌ | +| Free and open source | ✅ | ✅ | ✅ | + +# Where to download? + +So far, only the Web version is available, Mobile (Android and iOS) and Desktop (PC) will be released soon diff --git a/assets/lft.png b/assets/lft.png new file mode 100644 index 0000000..9b5be7f Binary files /dev/null and b/assets/lft.png differ diff --git a/assets/lftl.png b/assets/lftl.png new file mode 100644 index 0000000..6f539ae Binary files /dev/null and b/assets/lftl.png differ diff --git a/assets/lfwb.png b/assets/lfwb.png new file mode 100644 index 0000000..1a01fcf Binary files /dev/null and b/assets/lfwb.png differ diff --git a/assets/lfwbl.png b/assets/lfwbl.png new file mode 100644 index 0000000..8785a3d Binary files /dev/null and b/assets/lfwbl.png differ diff --git a/assets/lmt.png b/assets/lmt.png new file mode 100644 index 0000000..4c529c6 Binary files /dev/null and b/assets/lmt.png differ diff --git a/assets/lmtl.png b/assets/lmtl.png new file mode 100644 index 0000000..8d9792e Binary files /dev/null and b/assets/lmtl.png differ diff --git a/assets/lmwb.png b/assets/lmwb.png new file mode 100644 index 0000000..03ac29a Binary files /dev/null and b/assets/lmwb.png differ diff --git a/assets/lmwbl.png b/assets/lmwbl.png new file mode 100644 index 0000000..2ccecfe Binary files /dev/null and b/assets/lmwbl.png differ diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..70e2733 Binary files /dev/null and b/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..0286b35 --- /dev/null +++ b/index.html @@ -0,0 +1,119 @@ + + + + + Snowdrop Messenger + + + +
+
+
+ Logo + Snowdrop + +
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ + + +
+
+
+
+
+ +
+ +
+ +
+ +
+ + +
+ + + + + + +
+
+
+ + +
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/lib/marked.min.js b/lib/marked.min.js new file mode 100644 index 0000000..b4e0d73 --- /dev/null +++ b/lib/marked.min.js @@ -0,0 +1,69 @@ +/** + * marked v15.0.12 - a markdown parser + * Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + +/** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ +(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("marked",f)}else {g["marked"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports}; +"use strict";var H=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var Te=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var ye=(l,e)=>{for(var t in e)H(l,t,{get:e[t],enumerable:!0})},Re=(l,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Te(e))!we.call(l,s)&&s!==t&&H(l,s,{get:()=>e[s],enumerable:!(n=be(e,s))||n.enumerable});return l};var Se=l=>Re(H({},"__esModule",{value:!0}),l);var kt={};ye(kt,{Hooks:()=>L,Lexer:()=>x,Marked:()=>E,Parser:()=>b,Renderer:()=>$,TextRenderer:()=>_,Tokenizer:()=>S,defaults:()=>w,getDefaults:()=>z,lexer:()=>ht,marked:()=>k,options:()=>it,parse:()=>pt,parseInline:()=>ct,parser:()=>ut,setOptions:()=>ot,use:()=>lt,walkTokens:()=>at});module.exports=Se(kt);function z(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var w=z();function N(l){w=l}var I={exec:()=>null};function h(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(s,i)=>{let r=typeof i=="string"?i:i.source;return r=r.replace(m.caret,"$1"),t=t.replace(s,r),n},getRegex:()=>new RegExp(t,e)};return n}var m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}#`),htmlBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}<(?:[a-z].*>|!--)`,"i")},$e=/^(?:[ \t]*(?:\n|$))+/,_e=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Le=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,O=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,ze=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,F=/(?:[*+-]|\d{1,9}[.)])/,ie=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,oe=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Me=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),Q=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,Pe=/^[^\n]+/,U=/(?!\s*\])(?:\\.|[^\[\]\\])+/,Ae=h(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",U).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Ee=h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,F).getRegex(),v="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",K=/|$))/,Ce=h("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",K).replace("tag",v).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),le=h(Q).replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Ie=h(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",le).getRegex(),X={blockquote:Ie,code:_e,def:Ae,fences:Le,heading:ze,hr:O,html:Ce,lheading:oe,list:Ee,newline:$e,paragraph:le,table:I,text:Pe},re=h("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Oe={...X,lheading:Me,table:re,paragraph:h(Q).replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",re).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex()},Be={...X,html:h(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",K).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:I,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:h(Q).replace("hr",O).replace("heading",` *#{1,6} *[^ +]`).replace("lheading",oe).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},qe=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ve=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,ae=/^( {2,}|\\)\n(?!\s*$)/,De=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,ue=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,je=h(ue,"u").replace(/punct/g,D).getRegex(),Fe=h(ue,"u").replace(/punct/g,pe).getRegex(),he="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",Qe=h(he,"gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Ue=h(he,"gu").replace(/notPunctSpace/g,He).replace(/punctSpace/g,Ge).replace(/punct/g,pe).getRegex(),Ke=h("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Xe=h(/\\(punct)/,"gu").replace(/punct/g,D).getRegex(),We=h(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),Je=h(K).replace("(?:-->|$)","-->").getRegex(),Ve=h("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",Je).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),q=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Ye=h(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",q).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ke=h(/^!?\[(label)\]\[(ref)\]/).replace("label",q).replace("ref",U).getRegex(),ge=h(/^!?\[(ref)\](?:\[\])?/).replace("ref",U).getRegex(),et=h("reflink|nolink(?!\\()","g").replace("reflink",ke).replace("nolink",ge).getRegex(),J={_backpedal:I,anyPunctuation:Xe,autolink:We,blockSkip:Ne,br:ae,code:ve,del:I,emStrongLDelim:je,emStrongRDelimAst:Qe,emStrongRDelimUnd:Ke,escape:qe,link:Ye,nolink:ge,punctuation:Ze,reflink:ke,reflinkSearch:et,tag:Ve,text:De,url:I},tt={...J,link:h(/^!?\[(label)\]\((.*?)\)/).replace("label",q).getRegex(),reflink:h(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",q).getRegex()},j={...J,emStrongRDelimAst:Ue,emStrongLDelim:Fe,url:h(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},fe=l=>st[l];function R(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,fe)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,fe);return l}function V(l){try{l=encodeURI(l).replace(m.percentDecode,"%")}catch{return null}return l}function Y(l,e){let t=l.replace(m.findPipe,(i,r,o)=>{let a=!1,c=r;for(;--c>=0&&o[c]==="\\";)a=!a;return a?"|":" |"}),n=t.split(m.splitPipe),s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length0?-2:-1}function me(l,e,t,n,s){let i=e.href,r=e.title||null,o=l[1].replace(s.other.outputLinkReplace,"$1");n.state.inLink=!0;let a={type:l[0].charAt(0)==="!"?"image":"link",raw:t,href:i,title:r,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,a}function rt(l,e,t){let n=l.match(t.other.indentCodeCompensation);if(n===null)return e;let s=n[1];return e.split(` +`).map(i=>{let r=i.match(t.other.beginningSpace);if(r===null)return i;let[o]=r;return o.length>=s.length?i.slice(s.length):i}).join(` +`)}var S=class{options;rules;lexer;constructor(e){this.options=e||w}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:A(n,` +`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=rt(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:s}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let s=A(n,"#");(this.options.pedantic||!s||this.rules.other.endingSpaceChar.test(s))&&(n=s.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:A(t[0],` +`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=A(t[0],` +`).split(` +`),s="",i="",r=[];for(;n.length>0;){let o=!1,a=[],c;for(c=0;c1,i={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");let r=this.rules.other.listItemRegex(n),o=!1;for(;e;){let c=!1,p="",u="";if(!(t=r.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let d=t[2].split(` +`,1)[0].replace(this.rules.other.listReplaceTabs,Z=>" ".repeat(3*Z.length)),g=e.split(` +`,1)[0],T=!d.trim(),f=0;if(this.options.pedantic?(f=2,u=d.trimStart()):T?f=t[1].length+1:(f=t[2].search(this.rules.other.nonSpaceChar),f=f>4?1:f,u=d.slice(f),f+=t[1].length),T&&this.rules.other.blankLine.test(g)&&(p+=g+` +`,e=e.substring(g.length+1),c=!0),!c){let Z=this.rules.other.nextBulletRegex(f),te=this.rules.other.hrRegex(f),ne=this.rules.other.fencesBeginRegex(f),se=this.rules.other.headingBeginRegex(f),xe=this.rules.other.htmlBeginRegex(f);for(;e;){let G=e.split(` +`,1)[0],C;if(g=G,this.options.pedantic?(g=g.replace(this.rules.other.listReplaceNesting," "),C=g):C=g.replace(this.rules.other.tabCharGlobal," "),ne.test(g)||se.test(g)||xe.test(g)||Z.test(g)||te.test(g))break;if(C.search(this.rules.other.nonSpaceChar)>=f||!g.trim())u+=` +`+C.slice(f);else{if(T||d.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||ne.test(d)||se.test(d)||te.test(d))break;u+=` +`+g}!T&&!g.trim()&&(T=!0),p+=G+` +`,e=e.substring(G.length+1),d=C.slice(f)}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0));let y=null,ee;this.options.gfm&&(y=this.rules.other.listIsTask.exec(u),y&&(ee=y[0]!=="[ ] ",u=u.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:p,task:!!y,checked:ee,loose:!1,text:u,tokens:[]}),i.raw+=p}let a=i.items.at(-1);if(a)a.raw=a.raw.trimEnd(),a.text=a.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let c=0;cd.type==="space"),u=p.length>0&&p.some(d=>this.rules.other.anyLine.test(d.raw));i.loose=u}if(i.loose)for(let c=0;c({text:a,tokens:this.lexer.inline(a),header:!1,align:r.align[c]})));return r}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===` +`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let r=A(n.slice(0,-1),"\\");if((n.length-r.length)%2===0)return}else{let r=de(t[2],"()");if(r===-2)return;if(r>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+r;t[2]=t[2].substring(0,r),t[0]=t[0].substring(0,a).trim(),t[3]=""}}let s=t[2],i="";if(this.options.pedantic){let r=this.rules.other.pedanticHrefTitle.exec(s);r&&(s=r[1],i=r[3])}else i=t[3]?t[3].slice(1,-1):"";return s=s.trim(),this.rules.other.startAngleBracket.test(s)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?s=s.slice(1):s=s.slice(1,-1)),me(t,{href:s&&s.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let s=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[s.toLowerCase()];if(!i){let r=n[0].charAt(0);return{type:"text",raw:r,text:r}}return me(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s||s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let r=[...s[0]].length-1,o,a,c=r,p=0,u=s[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(u.lastIndex=0,t=t.slice(-1*e.length+r);(s=u.exec(t))!=null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o)continue;if(a=[...o].length,s[3]||s[4]){c+=a;continue}else if((s[5]||s[6])&&r%3&&!((r+a)%3)){p+=a;continue}if(c-=a,c>0)continue;a=Math.min(a,a+c+p);let d=[...s[0]][0].length,g=e.slice(0,r+s.index+d+a);if(Math.min(r,a)%2){let f=g.slice(1,-1);return{type:"em",raw:g,text:f,tokens:this.lexer.inlineTokens(f)}}let T=g.slice(2,-2);return{type:"strong",raw:g,text:T,tokens:this.lexer.inlineTokens(T)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),s=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return s&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,s;return t[2]==="@"?(n=t[1],s="mailto:"+n):(n=t[1],s=n),{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,s;if(t[2]==="@")n=t[0],s="mailto:"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?s="http://"+t[0]:s=t[0]}return{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var x=class l{tokens;options;state;tokenizer;inlineQueue;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||w,this.options.tokenizer=this.options.tokenizer||new S,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:B.normal,inline:P.normal};this.options.pedantic?(t.block=B.pedantic,t.inline=P.pedantic):this.options.gfm&&(t.block=B.gfm,this.options.breaks?t.inline=P.breaks:t.inline=P.gfm),this.tokenizer.rules=t}static get rules(){return{block:B,inline:P}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,` +`),this.blockTokens(e,this.tokens);for(let t=0;t(s=r.call({lexer:this},e,t))?(e=e.substring(s.raw.length),t.push(s),!0):!1))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);let r=t.at(-1);s.raw.length===1&&r!==void 0?r.raw+=` +`:t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=` +`+s.raw,r.text+=` +`+s.text,this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=` +`+s.raw,r.text+=` +`+s.raw,this.inlineQueue.at(-1).src=r.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let i=e;if(this.options.extensions?.startBlock){let r=1/0,o=e.slice(1),a;this.options.extensions.startBlock.forEach(c=>{a=c.call({lexer:this},o),typeof a=="number"&&a>=0&&(r=Math.min(r,a))}),r<1/0&&r>=0&&(i=e.substring(0,r+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i))){let r=t.at(-1);n&&r?.type==="paragraph"?(r.raw+=` +`+s.raw,r.text+=` +`+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);continue}if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="text"?(r.raw+=` +`+s.raw,r.text+=` +`+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(e){let r="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(r);break}else throw new Error(r)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,s=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(s=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(s=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(s=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,r="";for(;e;){i||(r=""),i=!1;let o;if(this.options.extensions?.inline?.some(c=>(o=c.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let c=t.at(-1);o.type==="text"&&c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,r)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let a=e;if(this.options.extensions?.startInline){let c=1/0,p=e.slice(1),u;this.options.extensions.startInline.forEach(d=>{u=d.call({lexer:this},p),typeof u=="number"&&u>=0&&(c=Math.min(c,u))}),c<1/0&&c>=0&&(a=e.substring(0,c+1))}if(o=this.tokenizer.inlineText(a)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(r=o.raw.slice(-1)),i=!0;let c=t.at(-1);c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(e){let c="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(c);break}else throw new Error(c)}}return t}};var $=class{options;parser;constructor(e){this.options=e||w}space(e){return""}code({text:e,lang:t,escaped:n}){let s=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+` +`;return s?'
'+(n?i:R(i,!0))+`
+`:"
"+(n?i:R(i,!0))+`
+`}blockquote({tokens:e}){return`
+${this.parser.parse(e)}
+`}html({text:e}){return e}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)} +`}hr(e){return`
+`}list(e){let t=e.ordered,n=e.start,s="";for(let o=0;o +`+s+" +`}listitem(e){let t="";if(e.task){let n=this.checkbox({checked:!!e.checked});e.loose?e.tokens[0]?.type==="paragraph"?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=n+" "+R(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • +`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    +`}table(e){let t="",n="";for(let i=0;i${s}`),` + +`+t+` +`+s+`
    +`}tablerow({text:e}){return` +${e} +`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+` +`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${R(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){let s=this.parser.parseInline(n),i=V(e);if(i===null)return s;e=i;let r='
    ",r}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let i=V(e);if(i===null)return R(n);e=i;let r=`${n}{let o=i[r].flat(1/0);n=n.concat(this.walkTokens(o,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let s={...n};if(s.async=this.defaults.async||s.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let r=t.renderers[i.name];r?t.renderers[i.name]=function(...o){let a=i.renderer.apply(this,o);return a===!1&&(a=r.apply(this,o)),a}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let r=t[i.level];r?r.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),s.extensions=t),n.renderer){let i=this.defaults.renderer||new $(this.defaults);for(let r in n.renderer){if(!(r in i))throw new Error(`renderer '${r}' does not exist`);if(["options","parser"].includes(r))continue;let o=r,a=n.renderer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u||""}}s.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new S(this.defaults);for(let r in n.tokenizer){if(!(r in i))throw new Error(`tokenizer '${r}' does not exist`);if(["options","rules","lexer"].includes(r))continue;let o=r,a=n.tokenizer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new L;for(let r in n.hooks){if(!(r in i))throw new Error(`hook '${r}' does not exist`);if(["options","block"].includes(r))continue;let o=r,a=n.hooks[o],c=i[o];L.passThroughHooks.has(r)?i[o]=p=>{if(this.defaults.async)return Promise.resolve(a.call(i,p)).then(d=>c.call(i,d));let u=a.call(i,p);return c.call(i,u)}:i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,r=n.walkTokens;s.walkTokens=function(o){let a=[];return a.push(r.call(this,o)),i&&(a=a.concat(i.call(this,o))),a}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,s)=>{let i={...s},r={...this.defaults,...i},o=this.onError(!!r.silent,!!r.async);if(this.defaults.async===!0&&i.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);let a=r.hooks?r.hooks.provideLexer():e?x.lex:x.lexInline,c=r.hooks?r.hooks.provideParser():e?b.parse:b.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(n):n).then(p=>a(p,r)).then(p=>r.hooks?r.hooks.processAllTokens(p):p).then(p=>r.walkTokens?Promise.all(this.walkTokens(p,r.walkTokens)).then(()=>p):p).then(p=>c(p,r)).then(p=>r.hooks?r.hooks.postprocess(p):p).catch(o);try{r.hooks&&(n=r.hooks.preprocess(n));let p=a(n,r);r.hooks&&(p=r.hooks.processAllTokens(p)),r.walkTokens&&this.walkTokens(p,r.walkTokens);let u=c(p,r);return r.hooks&&(u=r.hooks.postprocess(u)),u}catch(p){return o(p)}}}onError(e,t){return n=>{if(n.message+=` +Please report this to https://github.com/markedjs/marked.`,e){let s="

    An error occurred:

    "+R(n.message+"",!0)+"
    ";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}};var M=new E;function k(l,e){return M.parse(l,e)}k.options=k.setOptions=function(l){return M.setOptions(l),k.defaults=M.defaults,N(k.defaults),k};k.getDefaults=z;k.defaults=w;k.use=function(...l){return M.use(...l),k.defaults=M.defaults,N(k.defaults),k};k.walkTokens=function(l,e){return M.walkTokens(l,e)};k.parseInline=M.parseInline;k.Parser=b;k.parser=b.parse;k.Renderer=$;k.TextRenderer=_;k.Lexer=x;k.lexer=x.lex;k.Tokenizer=S;k.Hooks=L;k.parse=k;var it=k.options,ot=k.setOptions,lt=k.use,at=k.walkTokens,ct=k.parseInline,pt=k,ut=b.parse,ht=x.lex; + +if(__exports != exports)module.exports = exports;return module.exports})); diff --git a/proto/rac.js b/proto/rac.js new file mode 100644 index 0000000..d342d07 --- /dev/null +++ b/proto/rac.js @@ -0,0 +1 @@ +// ъуъ \ No newline at end of file diff --git a/proto/wrac.js b/proto/wrac.js new file mode 100644 index 0000000..8ed932d --- /dev/null +++ b/proto/wrac.js @@ -0,0 +1,147 @@ + async function wRAC(onOpenCb) { + if (!connectedServer) return; + if (ws && ws.readyState === 1) return onOpenCb(); + + if (ws) { + ws.onopen = null; + ws.onerror = null; + ws.onclose = null; + ws.onmessage = null; + ws.close(); + } + + ws = new WebSocket(connectedServer); + ws.binaryType = "arraybuffer"; + + ws.onopen = function() { + if (onOpenCb) onOpenCb(); + }; + ws.onerror = function() {}; + ws.onclose = function() {}; + ws.onmessage = function(event) { + if (typeof event.data === "string") return; + let buf = new Uint8Array(event.data); + if (buf.length === 1 && (buf[0] === 0x01 || buf[0] === 0x02)) return; + let str = new TextDecoder().decode(buf).trim(); + if (/^\d+$/.test(str)) { + ws.send(new Uint8Array([0x00, 0x01])); + } else { + let lines = str.split('\n').filter(Boolean); + messages = lines; + showMessages(); + } + }; + } + + async function checkServerInfo({ proto, address, port }) { + protocolCheck.state = "loading"; + protocolCheck.protoVersion = null; + protocolCheck.serverSoftware = null; + updateServerInfoLabels(); + let url; + try { + url = buildServerUrl({ proto, address, port }); + let wsTest; + if (proto === "wRACs" || proto === "wRAC") { + wsTest = new WebSocket(url); + } else { + protocolCheck.state = "error"; + updateServerInfoLabels(); + return; + } + wsTest.binaryType = "arraybuffer"; + let resolved = false; + wsTest.onopen = () => { + wsTest.send(new Uint8Array([0x69])); + }; + wsTest.onmessage = (e) => { + if (resolved) return; + resolved = true; + let arr = new Uint8Array(e.data); + if (arr.length < 2) { + protocolCheck.state = "error"; + updateServerInfoLabels(); + wsTest.close(); + return; + } + let versionByte = arr[0]; + let ver = ""; + if (versionByte === 0x01) ver = "v1"; + else if (versionByte === 0x02) ver = "x1.99"; + else if (versionByte === 0x03) ver = "v2"; + else ver = "Unknown"; + protocolCheck.protoVersion = ver; + protocolCheck.serverSoftware = new TextDecoder().decode(arr.slice(1)); + protocolCheck.state = "done"; + updateServerInfoLabels(); + wsTest.close(); + }; + wsTest.onerror = () => { + protocolCheck.state = "error"; + updateServerInfoLabels(); + }; + } catch (e) { + protocolCheck.state = "error"; + updateServerInfoLabels(); + } +} + +async function registerUser({ proto, address, port, username, password }) { + return new Promise((resolve, reject) => { + const url = buildServerUrl({ proto, address, port }); + const wsReg = new WebSocket(url); + wsReg.binaryType = "arraybuffer"; + + let resolved = false; + const timeout = setTimeout(() => { + if (!resolved) { + resolved = true; + wsReg.close(); + reject(t('connectionTimeout')); + } + }, 5000); + + wsReg.onopen = () => { + const encoder = new TextEncoder(); + const data = new Uint8Array([ + 0x03, + ...encoder.encode(username + '\n' + password) + ]); + wsReg.send(data); + }; + + wsReg.onmessage = (e) => { + clearTimeout(timeout); + resolved = true; + wsReg.close(); + + const data = new Uint8Array(e.data); + + if (data.length > 0 && data[0] === 0x01) { + reject(t('usernameTaken')); + } else if (data.length === 0) { + resolve(); + } else { + reject(t('unexpectedResponse')); + } + }; + + wsReg.onerror = (error) => { + clearTimeout(timeout); + if (!resolved) { + resolved = true; + reject(t('connectionFailed') + ': ' + error.message); + } + }; + + wsReg.onclose = (event) => { + if (!resolved) { + clearTimeout(timeout); + resolved = true; + if (event.code !== 1000) { + reject(t('connectionClosed') + ` (code ${event.code})`); + } + } + }; + }); +} \ No newline at end of file diff --git a/src/chat.js b/src/chat.js new file mode 100644 index 0000000..9e815b5 --- /dev/null +++ b/src/chat.js @@ -0,0 +1,156 @@ +const nickMarkers = [ + { marker: "\uB9AC\u3E70", color: "nick-green" }, + { marker: "\u2550\u2550\u2550", color: "nick-lightred" }, + { marker: "\u00B0\u0298", color: "nick-lightmagenta" }, + { marker: "\u2042", color: "nick-gold" }, + { marker: "\u0D9E", color: "nick-amogus" }, +]; + +function extractNickColor(str) { + let m = str.match(/<([^>]+)>/); + if (!m) return { nick: "unauth", colorClass: "nick-unauth" }; + let beforeNick = str.substring(0, m.index); + let nick = m[1]; + let colorClass = "nick-cyan"; + for (const { marker, color } of nickMarkers) { + if (beforeNick.includes(marker)) { + colorClass = color; + break; + } + } + return { nick, colorClass }; +} + +function parseMsg(msg) { + let date = ""; + let text = msg.trim(); + let colorClass = ""; + let nick = ""; + + let m = text.match(/^\[(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})\]\s*(.*)$/); + if (m) { + date = m[1]; + text = m[2]; + } + + let start = text.indexOf('<'); + let end = text.indexOf('>'); + if (start !== -1 && end !== -1 && end > start) { + text = text.slice(0, start) + text.slice(end + 1); + } + + let nickMatch = text.match(/^(.*?<[^>]+>)(\s?)(.*)$/); + if (nickMatch) { + let prefix = nickMatch[1]; + let afterNick = nickMatch[3]; + let ext = extractNickColor(prefix); + nick = ext.nick; + colorClass = ext.colorClass; + text = afterNick; + return { nick, text, date, colorClass }; + } + + if (date && !nick) { + return { nick: "unauth", text, date, colorClass: "nick-unauth" }; + } + return { nick: "unauth", text, date: "", colorClass: "nick-unauth" }; +} + +function getVisibleMessages(messages) { + if (connectedServer && connectedServer.startsWith("wss://meex.lol:52667") && messages.length > 3) { + return messages.slice(3); + } + return messages; +} + +if (window.marked) { + marked.setOptions({ + breaks: true, + gfm: true, + smartypants: true + }); + + const renderer = new marked.Renderer(); + renderer.link = function(href, title, text) { + return `
    ${text}`; + }; + marked.setOptions({ renderer }); +} + +function showMessages() { + let chat = document.getElementById('chat-area'); + chat.innerHTML = ''; + let displayMessages = getVisibleMessages([...messages].reverse()); + + for (let msg of displayMessages) { + let { nick, text, date, colorClass } = parseMsg(msg); + + let msgHtml = ""; + if (text) { + if (window.marked) { + try { + msgHtml = marked.parse(text); + } catch (e) { + console.error("Markdown processing error:", e); + msgHtml = text.replace(/"); + } + } else { + msgHtml = text.replace(/"); + } + } + + chat.innerHTML += ` +
    + ${nick ? nick : ""} + ${msgHtml} + ${date ? "[" + date + "]" : ""} +
    `; + } +} + +function sendMsg() { + const msg = document.getElementById('chat-input').value.trim(); + if (!msg || !connectedServer) return; + const { username, password } = getActiveServerCreds(); + wRAC(() => { + let arr; + let format = (settings && settings.messageFormat) ? settings.messageFormat : DEFAULT_SETTINGS.messageFormat; + let formatted = format; + if (formatted.includes("{name}")) formatted = formatted.replace("{name}", username ?? ""); + if (formatted.includes("{text}")) formatted = formatted.replace("{text}", msg ?? ""); + if (username && password) { + let enc = new TextEncoder(); + let uname = enc.encode(username); + let pass = enc.encode(password); + let text = enc.encode(formatted); + let total = new Uint8Array(1 + uname.length + 1 + pass.length + 1 + text.length); + total[0] = 0x02; + total.set(uname, 1); + total[1 + uname.length] = 10; + total.set(pass, 1 + uname.length + 1); + total[1 + uname.length + 1 + pass.length] = 10; + total.set(text, 1 + uname.length + 1 + pass.length + 1); + arr = total; + } else { + arr = [0x01, ...new TextEncoder().encode(formatted)]; + arr = new Uint8Array(arr); + } + ws.send(arr); + document.getElementById('chat-input').value = ""; + setTimeout(fetchMessages, 200); + }); +} + +function fetchMessages() { + wRAC(() => { + ws.send(new Uint8Array([0x00])); + }); +} + +document.getElementById('send-btn').onclick = sendMsg; +document.getElementById('chat-input').addEventListener('keydown', function (e) { + if (e.key === "Enter") sendMsg(); +}); + +setInterval(fetchMessages, 6000); +window.onload = () => { fetchMessages(); }; \ No newline at end of file diff --git a/src/panel.js b/src/panel.js new file mode 100644 index 0000000..4d34c4c --- /dev/null +++ b/src/panel.js @@ -0,0 +1,417 @@ +function getProtoLabel(proto) { + let label = { proto, className: proto }; + if (proto === "wRACs") label.className = "wRACs"; + else if (proto === "wRAC") label.className = "wRAC"; + else if (proto === "RACs") label.className = "RACs"; + else if (proto === "RAC") label.className = "RAC"; + else label.className = ""; + return label; +} + +function buildServerUrl({ proto, address, port }) { + if (proto === "wRACs") return `wss://${address}:${port}`; + if (proto === "wRAC") return `ws://${address}:${port}`; + if (proto === "RACs") return `rac+tls://${address}:${port}`; + if (proto === "RAC") return `rac://${address}:${port}`; + return `${address}:${port}`; +} + +function parseServerUrl(url) { + let m; + if (m = url.match(/^wss:\/\/([^:\/]+):(\d+)$/)) return { proto: "wRACs", address: m[1], port: m[2] }; + if (m = url.match(/^ws:\/\/([^:\/]+):(\d+)$/)) return { proto: "wRAC", address: m[1], port: m[2] }; + if (m = url.match(/^rac\+tls:\/\/([^:\/]+):(\d+)$/)) return { proto: "RACs", address: m[1], port: m[2] }; + if (m = url.match(/^rac:\/\/([^:\/]+):(\d+)$/)) return { proto: "RAC", address: m[1], port: m[2] }; + return { proto: "wRACs", address: url, port: "" }; +} + +function getSavedServers() { + let arr = []; + try { arr = JSON.parse(localStorage.getItem('snowdrop_servers') || "[]") } catch (e) { } + if (arr.length && typeof arr[0] === "string") { + arr = arr.map(url => { + const { proto, address, port } = parseServerUrl(url); + return { title: address, proto, address, port }; + }); + } + return arr; +} + +function saveServers(arr) { + localStorage.setItem('snowdrop_servers', JSON.stringify(arr)); +} + +let servers = getSavedServers(); +let ws = null; +let messages = []; +let connectedServer = servers[0] ? buildServerUrl(servers[0]) : null; + +let protocolCheck = { + state: "idle", + protoVersion: null, + serverSoftware: null, + lastFields: {} +}; + +function updateServerInfoLabels() { + const protoInfo = document.getElementById('proto-version-info'); + const swInfo = document.getElementById('server-software-info'); + if (!protoInfo || !swInfo) return; + if (protocolCheck.state === "idle" || protocolCheck.state === "error") { + protoInfo.style.display = "none"; + swInfo.style.display = "none"; + } else if (protocolCheck.state === "loading") { + protoInfo.style.display = ""; + protoInfo.textContent = t('protocolChecking'); + swInfo.style.display = ""; + swInfo.textContent = t('serverChecking'); + } else if (protocolCheck.state === "done") { + protoInfo.style.display = ""; + protoInfo.textContent = t('protocolVersion', { version: protocolCheck.protoVersion }); + swInfo.style.display = ""; + swInfo.textContent = t('serverSoftware', { software: protocolCheck.serverSoftware }); + } +} + +let dragSrcIdx = null; + +function handleDragStart(e) { + dragSrcIdx = +this.getAttribute('data-server-idx'); + this.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', dragSrcIdx); +} + +function handleDragOver(e) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + this.classList.add('drag-over'); +} + +function handleDragLeave(e) { + this.classList.remove('drag-over'); +} + +function handleDrop(e) { + e.preventDefault(); + this.classList.remove('drag-over'); + const fromIdx = dragSrcIdx; + const toIdx = +this.getAttribute('data-server-idx'); + if (fromIdx === toIdx || fromIdx == null || toIdx == null) return; + + const moved = servers.splice(fromIdx, 1)[0]; + servers.splice(toIdx, 0, moved); + saveServers(servers); + + if (connectedServer === buildServerUrl(moved)) { + connectedServer = buildServerUrl(servers[toIdx]); + } + + renderChannels(); + fetchMessages(); +} + +function handleDragEnd(e) { + document.querySelectorAll('#channels .channel').forEach(c => c.classList.remove('dragging', 'drag-over')); + dragSrcIdx = null; +} + +function renderChannels() { + const channels = document.getElementById('channels'); + if (!channels) return; + channels.innerHTML = ''; + + if (!servers || !servers.length) { + return; + } + + servers.forEach((srv, idx) => { + const label = getProtoLabel(srv.proto); + const url = buildServerUrl(srv); + const isSelected = connectedServer === url || (!connectedServer && idx === 0); + + const div = document.createElement('div'); + div.className = 'channel' + (isSelected ? ' selected' : ''); + div.setAttribute('data-server-idx', idx); + + div.setAttribute('draggable', 'true'); + div.ondragstart = handleDragStart; + div.ondragover = handleDragOver; + div.ondragleave = handleDragLeave; + div.ondrop = handleDrop; + div.ondragend = handleDragEnd; + + div.innerHTML = ` +
    +
    + ${t(label.proto)} + ${srv.title} +
    + + +
    + ${srv.address}:${srv.port} + `; + + div.querySelector('.channel-header-main').onclick = function (e) { + document.querySelectorAll('#channels .channel').forEach(c => c.classList.remove('selected')); + div.classList.add('selected'); + connectedServer = url; + fetchMessages(); + }; + div.querySelector('.delete-server-btn').onclick = function (e) { + e.stopPropagation(); + if (confirm(t('confirmDelete', { title: srv.title }))) { + servers.splice(idx, 1); + saveServers(servers); + if (servers.length) { + connectedServer = buildServerUrl(servers[Math.min(idx, servers.length - 1)]); + } else { + connectedServer = null; + messages = []; + showMessages(); + } + renderChannels(); + fetchMessages(); + } + }; + div.querySelector('.edit-server-btn').onclick = function (e) { + e.stopPropagation(); + openEditModal(idx); + }; + channels.appendChild(div); + }); +} + +function openEditModal(idx) { + const srv = servers[idx]; + modalTitle.value = srv.title || ""; + modalAddress.value = srv.address || ""; + modalPort.value = srv.port || ""; + modalProto.value = srv.proto || "wRACs"; + modalUsername.value = srv.username || ""; + modalPassword.value = srv.password || ""; + modalError.textContent = ""; + modalBg.style.display = "flex"; + setTimeout(() => modalTitle.focus(), 50); + + protocolCheck.state = "idle"; + updateServerInfoLabels(); + + function triggerCheck() { + if (modalAddress.value && modalPort.value && modalProto.value) { + checkServerInfo({ + proto: modalProto.value, + address: modalAddress.value, + port: modalPort.value + }); + } else { + protocolCheck.state = "idle"; + updateServerInfoLabels(); + } + } + modalAddress.oninput = triggerCheck; + modalPort.oninput = triggerCheck; + modalProto.onchange = triggerCheck; + triggerCheck(); + + saveBtn.onclick = function () { + const title = modalTitle.value.trim(); + const address = modalAddress.value.trim(); + const port = modalPort.value.trim(); + const proto = modalProto.value; + const username = modalUsername.value.trim(); + const password = modalPassword.value; + if (!title || !address || !port || !proto) { + modalError.textContent = t('fillAllFields'); + return; + } + if (!/^[a-zA-Z0-9а-яА-Я\-\.\s_]+$/.test(title)) { + modalError.textContent = t('invalidTitle'); + return; + } + if (!/^[a-zA-Z0-9\-\.]+$/.test(address)) { + modalError.textContent = t('invalidAddress'); + return; + } + if (!/^\d+$/.test(port) || +port < 1 || +port > 65535) { + modalError.textContent = t('invalidPort'); + return; + } + if (servers.some((srv2, i) => i !== idx && srv2.proto === proto && srv2.address === address && srv2.port === port)) { + modalError.textContent = t('duplicateServer'); + return; + } + servers[idx] = { title, proto, address, port, username, password }; + saveServers(servers); + connectedServer = buildServerUrl(servers[idx]); + closeModal(); + renderChannels(); + fetchMessages(); + }; +} + +function getActiveServerCreds() { + if (!connectedServer) return {}; + let idx = servers.findIndex( + s => buildServerUrl(s) === connectedServer + ); + if (idx === -1) return {}; + let { username, password } = servers[idx]; + return { username: username || "", password: password || "" }; +} + +const modalBg = document.getElementById('server-modal-bg'); +const modalForm = document.getElementById('server-modal'); +const modalTitle = document.getElementById('modal-server-title'); +const modalAddress = document.getElementById('modal-server-address'); +const modalPort = document.getElementById('modal-server-port'); +const modalProto = document.getElementById('modal-server-proto'); +const modalUsername = document.getElementById('modal-server-username'); +const modalPassword = document.getElementById('modal-server-password'); +const modalError = document.getElementById('server-modal-error'); +const saveBtn = document.getElementById('save-server-btn'); +const cancelBtn = document.getElementById('cancel-server-btn'); +const registerBtn = document.getElementById('register-server-btn'); + +registerBtn.onclick = async function() { + const username = modalUsername.value.trim(); + const password = modalPassword.value; + const address = modalAddress.value.trim(); + const port = modalPort.value.trim(); + const proto = modalProto.value; + + if (!username || !password) { + modalError.textContent = t('fillUsernamePassword'); + modalError.className = 'error'; + return; + } + + if (!address || !port) { + modalError.textContent = t('fillServerDetails'); + modalError.className = 'error'; + return; + } + + try { + modalError.textContent = t('registering'); + modalError.className = 'info'; + registerBtn.disabled = true; + + await registerUser({ proto, address, port, username, password }); + + modalError.textContent = t('registrationSuccess'); + modalError.className = 'success'; + } catch (error) { + modalError.textContent = error.message || t('unknownError'); + modalError.className = 'error'; + } finally { + registerBtn.disabled = false; + } +}; + +function openModal() { + modalTitle.value = ""; + modalAddress.value = ""; + modalPort.value = ""; + modalProto.value = "wRACs"; + modalUsername.value = ""; + modalPassword.value = ""; + modalError.textContent = ""; + modalBg.style.display = "flex"; + setTimeout(() => modalTitle.focus(), 50); + + protocolCheck.state = "idle"; + updateServerInfoLabels(); + + function triggerCheck() { + if (modalAddress.value && modalPort.value && modalProto.value) { + checkServerInfo({ + proto: modalProto.value, + address: modalAddress.value, + port: modalPort.value + }); + } else { + protocolCheck.state = "idle"; + updateServerInfoLabels(); + } + } + modalAddress.oninput = triggerCheck; + modalPort.oninput = triggerCheck; + modalProto.onchange = triggerCheck; + triggerCheck(); + + saveBtn.onclick = function () { + const title = modalTitle.value.trim(); + const address = modalAddress.value.trim(); + const port = modalPort.value.trim(); + const proto = modalProto.value; + const username = modalUsername.value.trim(); + const password = modalPassword.value; + if (!title || !address || !port || !proto) { + modalError.textContent = t('fillAllFields'); + return; + } + if (!/^[a-zA-Z0-9а-яА-Я\-\.\s_]+$/.test(title)) { + modalError.textContent = t('invalidTitle'); + return; + } + if (!/^[a-zA-Z0-9\-\.]+$/.test(address)) { + modalError.textContent = t('invalidAddress'); + return; + } + if (!/^\d+$/.test(port) || +port < 1 || +port > 65535) { + modalError.textContent = t('invalidPort'); + return; + } + if (servers.some(srv => srv.proto === proto && srv.address === address && srv.port === port)) { + modalError.textContent = t('duplicateServer'); + return; + } + servers.push({ title, proto, address, port, username, password }); + saveServers(servers); + connectedServer = buildServerUrl(servers[servers.length - 1]); + closeModal(); + renderChannels(); + fetchMessages(); + }; +} + +function closeModal() { + modalBg.style.display = "none"; + protocolCheck.state = "idle"; + updateServerInfoLabels(); +} + +document.getElementById('add-server-btn').onclick = openModal; +cancelBtn.onclick = closeModal; + +function updateUIStrings() { + document.getElementById('app-title').textContent = t('appName'); + document.getElementById('add-server-btn').textContent = t('addServer'); + document.getElementById('register-server-btn').textContent = t('register'); + document.getElementById('header-settings-btn').title = t('settings'); + document.getElementById('label-server-title').textContent = t('serverTitle'); + document.getElementById('label-server-address').textContent = t('address'); + document.getElementById('label-server-port').textContent = t('port'); + document.getElementById('label-server-proto').textContent = t('protocol'); + document.getElementById('label-server-username').textContent = t('username'); + document.getElementById('label-server-password').textContent = t('password'); + document.getElementById('save-server-btn').textContent = t('save'); + document.getElementById('cancel-server-btn').textContent = t('cancel'); + document.getElementById('settings-label-lang').textContent = t('settingsLabelLang'); + document.getElementById('settings-label-format').textContent = t('settingsLabelFormat'); + document.getElementById('settings-label-theme').textContent = t('settingsLabelTheme') || "Theme"; + document.getElementById('chat-input').placeholder = t('writeMessage'); + document.getElementById('send-btn').title = t('send'); + renderChannels(); +} + +window.updateUIStrings = updateUIStrings; + +updateUIStrings(); +renderChannels(); \ No newline at end of file diff --git a/src/settings.js b/src/settings.js new file mode 100644 index 0000000..ed8cc3f --- /dev/null +++ b/src/settings.js @@ -0,0 +1,130 @@ +const MESSAGE_FORMAT_PRESETS = [ + { id: "snowdrop", label: "Snowdrop", format: "ඞ<{name}> {text}", class: "preset-snowdrop" }, + { id: "brac", label: "bRAC", format: "리㹰<{name}> {text}", class: "preset-brac" }, + { id: "crab", label: "CRAB", format: "═══<{name}> {text}", class: "preset-crab" }, + { id: "mefidroniy", label: "Mefidroniy", format: "°ʘ<{name}> {text}", class: "preset-mefidroniy" }, + { id: "crack", label: "cRACk", format: "⁂<{name}> {text}", class: "preset-crack" }, + { id: "default", label: "Default (clRAC)", format: "<{name}> {text}", class: "preset-default" } +]; +const DEFAULT_SETTINGS = { + lang: "en", + messageFormat: "ඞ<{name}> {text}", + messageFormatPreset: "snowdrop", + theme: "dark" +}; + +function getSettings() { + try { + let s = JSON.parse(localStorage.getItem("snowdrop_settings") || "{}"); + return { ...DEFAULT_SETTINGS, ...s }; + } catch (e) { + return { ...DEFAULT_SETTINGS }; + } +} + +function saveSettings(s) { + localStorage.setItem("snowdrop_settings", JSON.stringify(s)); +} + +let settings = getSettings(); + +function applyTheme(theme) { + let dark = document.getElementById('theme-dark-css'); + let light = document.getElementById('theme-light-css'); + if (dark) dark.remove(); + if (light) light.remove(); + + if (theme === "light") { + let link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'themes/light.css'; + link.id = 'theme-light-css'; + document.head.appendChild(link); + } else { + let link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'themes/dark.css'; + link.id = 'theme-dark-css'; + document.head.appendChild(link); + } +} + +function getCurrentFormatPresetId(fmt) { + for (const preset of MESSAGE_FORMAT_PRESETS) { + if (preset.format === fmt) return preset.id; + } + return null; +} + +const settingsBtn = document.getElementById('header-settings-btn'); +const settingsModalBg = document.getElementById('settings-modal-bg'); +const settingsModal = document.getElementById('settings-modal'); +const settingsLangSelect = document.getElementById('settings-lang-select'); +const settingsFormatInput = document.getElementById('settings-format-input'); +const settingsModalError = document.getElementById('settings-modal-error'); +const settingsModalPresets = document.querySelectorAll('.settings-format-preset-btn'); +const saveSettingsBtn = document.getElementById('save-settings-btn'); +const cancelSettingsBtn = document.getElementById('cancel-settings-btn'); +const settingsThemeSelect = document.getElementById('settings-theme-select'); + +function updateSettingsModalFields() { + settingsLangSelect.value = settings.lang || "ru"; + settingsFormatInput.value = settings.messageFormat || DEFAULT_SETTINGS.messageFormat; + settingsThemeSelect.value = settings.theme || "dark"; + let id = getCurrentFormatPresetId(settingsFormatInput.value); + settingsModalPresets.forEach(btn => { + btn.classList.toggle("selected", btn.dataset.id === id); + }); +} + +function openSettingsModal() { + updateSettingsModalFields(); + settingsModalError.textContent = ""; + settingsModalBg.style.display = "flex"; + setTimeout(() => settingsLangSelect.focus(), 50); +} + +function closeSettingsModal() { + settingsModalBg.style.display = "none"; +} + +settingsBtn.onclick = openSettingsModal; +cancelSettingsBtn.onclick = closeSettingsModal; + +settingsModalPresets.forEach(btn => { + btn.onclick = function () { + settingsFormatInput.value = btn.getAttribute("data-format") + .replace(/</g, "<").replace(/>/g, ">"); + settingsModalPresets.forEach(b => b.classList.remove("selected")); + btn.classList.add("selected"); + }; +}); + +saveSettingsBtn.onclick = function () { + const lang = settingsLangSelect.value; + const format = settingsFormatInput.value.trim(); + const theme = settingsThemeSelect.value; + settings = { + ...settings, + lang, + messageFormat: format, + messageFormatPreset: getCurrentFormatPresetId(format) || null, + theme + }; + saveSettings(settings); + applyTheme(theme); + closeSettingsModal(); + if (window.updateUIStrings) window.updateUIStrings(); +}; + +settingsThemeSelect.onchange = function () { + settings.theme = settingsThemeSelect.value; + saveSettings(settings); + applyTheme(settings.theme); +}; + +settingsModalBg.addEventListener("click", function (e) { + if (e.target === settingsModalBg) closeSettingsModal(); +}); + +applyTheme(settings.theme); \ No newline at end of file diff --git a/src/translations.js b/src/translations.js new file mode 100644 index 0000000..464fc1d --- /dev/null +++ b/src/translations.js @@ -0,0 +1,97 @@ +const translations = { + ru: { + appName: "Snowdrop", + addServer: "+ Добавить сервер", + serverTitle: "Название сервера", + address: "IP/домен", + port: "Порт", + protocol: "Протокол", + username: "Никнейм", + password: "Пароль", + save: "Сохранить", + cancel: "Отмена", + editServer: "Изменить сервер", + deleteServer: "Удалить сервер", + confirmDelete: 'Удалить сервер "{title}"?', + settings: "Настройки", + language: "Язык", + messageFormat: "Формат сообщения", + settingsSave: "Сохранить", + settingsCancel: "Отмена", + invalidTitle: "Некорректное название", + invalidAddress: "Некорректный адрес", + invalidPort: "Некорректный порт", + duplicateServer: "Такой сервер уже добавлен", + fillAllFields: "Заполните все поля", + writeMessage: "Написать сообщение...", + send: "Отправить", + plain: "Простой", + wRACs: "wRACs", + wRAC: "wRAC (open snowdrop using http://)", + RACs: "RACs (WIP)", + RAC: "RAC (WIP)", + settingsLabelLang: "Язык", + settingsLabelFormat: "Формат сообщения", + settingsLabelTheme: "Тема", + protocolChecking: "Проверка версии протокола...", + serverChecking: "Проверка ПО сервера...", + protocolVersion: "Версия протокола: {version}", + serverSoftware: "ПО сервера: {software}", + register: "Регистрация", + fillUsernamePassword: "Введите ник и пароль", + registering: "Регистрация...", + unknownError: "Никнейм занят, или соединение не удалось" + }, + en: { + appName: "Snowdrop", + addServer: "+ Add server", + serverTitle: "Server title", + address: "IP/domain", + port: "Port", + protocol: "Protocol", + username: "Username", + password: "Password", + save: "Save", + cancel: "Cancel", + editServer: "Edit server", + deleteServer: "Delete server", + confirmDelete: 'Delete server "{title}"?', + settings: "Settings", + language: "Language", + messageFormat: "Message format", + settingsSave: "Save", + settingsCancel: "Cancel", + invalidTitle: "Invalid title", + invalidAddress: "Invalid address", + invalidPort: "Invalid port", + duplicateServer: "This server is already added", + fillAllFields: "Fill all fields", + writeMessage: "Write a message...", + send: "Send", + plain: "Plain", + wRACs: "wRACs", + wRAC: "wRAC (open snowdrop using http://)", + RACs: "RACs (WIP)", + RAC: "RAC (WIP)", + settingsLabelLang: "Language", + settingsLabelFormat: "Message format", + settingsLabelTheme: "Theme", + protocolChecking: "Checking protocol version...", + serverChecking: "Checking server software...", + protocolVersion: "Protocol version: {version}", + serverSoftware: "Server software: {software}", + register: "Register", + fillUsernamePassword: "Enter username and password", + registering: "Registering...", + unknownError: "Username is taken or connection failed" + } +}; + +function t(key, vars = {}) { + const lang = settings && settings.lang || "ru"; + let str = translations[lang][key] || translations['ru'][key] || key; + Object.keys(vars).forEach(k => { + str = str.replace("{" + k + "}", vars[k]); + }); + return str; +} \ No newline at end of file diff --git a/themes/dark.css b/themes/dark.css new file mode 100644 index 0000000..587f562 --- /dev/null +++ b/themes/dark.css @@ -0,0 +1,167 @@ +body { + background: #18191c; + color: #dee2e6; +} +#app-container { + background: #18191c; +} +#channel-list { + background: #141517; + border-right: 1px solid #101114; +} +#channel-list-header { + color: #fff; + border-bottom: 1px solid #101114; +} +#channels { + background: inherit; +} +.channel { + color: #8a99a8; + background: inherit; +} +.channel.selected, .channel:hover { + background: #23272a; + color: #fff; +} +.proto-label { + color: #fff; + background: #5b8fbd; +} +.proto-label.wRAC { + background: #555; +} +.proto-label.wRACs { + background: #5b8fbd; +} +.proto-label.RAC { + background: #aa4000; +} +.proto-label.RACs { + background: #007c40; +} +.channel-title { + color: #fff; +} +.channel-address { + color: #888; +} +#add-server-btn { + background: #23272a; + color: #5b8fbd; +} +#add-server-btn:hover { + background: #222e39; + color: #fff; +} +#main { + background: #18191c; +} +#chat-area { + background: #18191c; + color: #fff; +} +.message .msg { + color: #fff; +} +.nick-green { color: #18b800; } +.nick-lightred { color: #ff9191; } +.nick-lightmagenta { color: #e782ff; } +.nick-gold { color: #ffd700; } +.nick-cyan { color: #5b8fbd; } +.nick-unauth { color: #666; font-style: italic;} +.nick-amogus { color: #aeff00; } +.msg { color: #fff; } +.time { color: #8a8a8a; } +#chat-input-area { + background: #18191c; + border-top: 1px solid #101114; +} +#chat-input { + background: #101114; + color: #fff; +} +#chat-input:focus { + background: #222327; +} +#send-btn { + background: #5b8fbd; + color: #fff; +} +#send-btn:active { + background: #3976a1; +} +#server-modal-bg, #settings-modal-bg { + background: rgba(0,0,0,0.65); +} +#server-modal, #settings-modal { + background: #15171b; + color: #ddd; + box-shadow: 0 10px 36px #000a; +} +#server-modal label, #settings-modal label { + color: #ddd; +} +#server-modal input, #server-modal select, +#settings-modal input, #settings-modal select { + background: #23272a; + color: #fff; + border: none; +} +#save-server-btn, #save-settings-btn { + background: #5b8fbd; + color: #fff; +} +#save-server-btn:hover, #save-settings-btn:hover { + background: #3976a1; +} +#cancel-server-btn, #cancel-settings-btn { + background: #23272a; + color: #aaa; +} +#cancel-server-btn:hover, #cancel-settings-btn:hover { + background: #393c42; + color: #fff; +} +#server-modal-error, #settings-modal-error { + color: #ff9191; +} +.settings-format-preset-btn { + background: #23272a; + color: #fff; +} +.settings-format-preset-btn.selected, +.settings-format-preset-btn:hover { + background: #1c2330; +} +.preset-snowdrop { color: #aeff00; } +.preset-brac { color: #18b800; } +.preset-crab { color: #ff9191; } +.preset-mefidroniy { color: #e782ff; } +.preset-crack { color: #ffd700; } +.preset-default { color: #5b8fbd; } +.preset-plain { color: #fff; } + +#register-server-btn { + background: #007c40; + color: #fff; + margin-right: auto; + font-size: 1em; + border-radius: 7px; + border: none; + padding: 8px 18px; + cursor: pointer; + font-weight: bold; +} + +#register-server-btn:hover { + background: #006030; +} + +#server-modal-error.success { + color: #18b800; +} + +#server-modal-error.info { + color: #5b8fbd; +} \ No newline at end of file diff --git a/themes/light.css b/themes/light.css new file mode 100644 index 0000000..3e4afac --- /dev/null +++ b/themes/light.css @@ -0,0 +1,170 @@ +body { + background: #f6f6f7; + color: #222; +} +#app-container { + background: #f6f6f7; +} +#channel-list { + background: #fafbfc; + border-right: 1px solid #e3e6ea; +} +#channel-list-header { + color: #323638; + border-bottom: 1px solid #e3e6ea; +} +#channels { + background: inherit; +} +.channel { + color: #4c5862; + background: inherit; +} +.channel.selected, .channel:hover { + background: #e6ecf4; + color: #222; +} +.proto-label { + background: #a8d0f0; + color: #222; +} +.proto-label.wRAC { + background: #ccc; +} +.proto-label.wRACs { + background: #a8d0f0; +} +.proto-label.RAC { + background: #ffb080; +} +.proto-label.RACs { + background: #6ee7b7; +} +.channel-title { + color: #222; +} +.channel-address { + color: #a5a5a5; +} +#add-server-btn { + background: #e6ecf4; + color: #5b8fbd; +} +#add-server-btn:hover { + background: #d2e2f5; + color: #222; +} +#main { + background: #fff; +} +#chat-area { + background: #fff; + color: #222; +} +.message .msg { + color: #222; +} +.nick-green { color: #1bb800; } +.nick-lightred { color: #e76b6b; } +.nick-lightmagenta { color: #c359d7; } +.nick-gold { color: #bda700; } +.nick-cyan { color: #358dd1; } +.nick-unauth { color: #aaa; } +.nick-amogus { color: #7fa800; } +.msg { color: #222; } +.time { color: #888; } +#chat-input-area { + background: #fafbfc; + border-top: 1px solid #e3e6ea; +} +#chat-input { + background: #f0f2f4; + color: #222; +} +#chat-input:focus { + background: #e6ecf4; +} +#send-btn { + background: #a8d0f0; + color: #222; +} +#send-btn:active { + background: #5b8fbd; + color: #fff; +} +#server-modal-bg, #settings-modal-bg { + background: rgba(255,255,255,0.7); +} +#server-modal, #settings-modal { + background: #fff; + color: #222; + box-shadow: 0 10px 36px #0002; +} +#server-modal label, #settings-modal label { + color: #333; +} +#server-modal input, #server-modal select, +#settings-modal input, #settings-modal select { + background: #f0f2f4; + color: #222; + border: 1px solid #e3e6ea; +} +#save-server-btn, #save-settings-btn { + background: #a8d0f0; + color: #222; +} +#save-server-btn:hover, #save-settings-btn:hover { + background: #5b8fbd; + color: #fff; +} +#cancel-server-btn, #cancel-settings-btn { + background: #e6ecf4; + color: #888; +} +#cancel-server-btn:hover, #cancel-settings-btn:hover { + background: #c3d7ec; + color: #222; +} +#server-modal-error, #settings-modal-error { + color: #e76b6b; +} +.settings-format-preset-btn { + background: #e6ecf4; + color: #222; +} +.settings-format-preset-btn.selected, +.settings-format-preset-btn:hover { + background: #c3d7ec; + color: #222; +} +.preset-snowdrop { color: #7fa800; } +.preset-brac { color: #1bb800; } +.preset-crab { color: #e76b6b; } +.preset-mefidroniy { color: #c359d7; } +.preset-crack { color: #bda700; } +.preset-default { color: #358dd1; } +.preset-plain { color: #222; } + +#register-server-btn { + background: #1bb800; + color: #fff; + margin-right: auto; + font-size: 1em; + border-radius: 7px; + border: none; + padding: 8px 18px; + cursor: pointer; + font-weight: bold; +} + +#register-server-btn:hover { + background: #0a9e00; +} + +#server-modal-error.success { + color: #1bb800; +} + +#server-modal-error.info { + color: #5b8fbd; +} \ No newline at end of file diff --git a/themes/styles.css b/themes/styles.css new file mode 100644 index 0000000..a63fdae --- /dev/null +++ b/themes/styles.css @@ -0,0 +1,432 @@ +body { + margin: 0; + padding: 0; + font-family: 'gg sans', 'Segoe UI', Arial, sans-serif; + height: 100vh; + overflow: hidden; +} +#app-container { + display: flex; + height: 100vh; +} +#channel-list { + width: 260px; + display: flex; + flex-direction: column; + padding: 0; + box-sizing: border-box; +} +#channel-list-header { + padding: 16px 18px 8px 18px; + font-weight: bold; + font-size: 1rem; + letter-spacing: 1px; + display: flex; + align-items: center; + justify-content: space-between; +} +.header-settings-btn { + background: none; + border: none; + margin-left: 8px; + padding: 1px 0 0 7px; + display: flex; + align-items: center; + cursor: pointer; + opacity: 0.8; + transition: opacity 0.18s; +} +.header-settings-btn:hover { + opacity: 1; +} +.header-settings-btn svg { + width: 23px; + height: 23px; + display: block; +} +#channels { + flex: 1; + padding: 10px 0; + overflow-y: auto; +} +.channel { + padding: 10px 20px; + cursor: pointer; + font-size: 1rem; + display: flex; + flex-direction: column; + border-radius: 6px; + transition: background 0.2s, color 0.2s; + margin-bottom: 2px; + position: relative; +} +.channel-header-row { + display: flex; + align-items: center; + justify-content: space-between; +} +.channel-header-main { + display: flex; + align-items: center; + min-width: 0; +} +.proto-label { + display: inline-block; + min-width: 48px; + font-family: monospace; + font-size: 1em; + font-weight: bold; + border-radius: 4px; + padding: 2px 8px; + margin-right: 10px; + text-align: center; +} +.channel-title { + font-size: 1.05em; + font-weight: normal; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 120px; +} +.channel-address { + font-size: 0.92em; + margin-left: 5px; + margin-top: 1px; + word-break: break-all; +} +.edit-server-btn { + background: none; + border: none; + cursor: pointer; + padding: 4px; + margin-left: 0; + align-self: flex-start; + opacity: 0.7; + transition: opacity 0.15s; + margin-right: 4px; + display: flex; + align-items: center; + justify-content: center; +} +.edit-server-btn:hover { + opacity: 1; +} +.edit-server-btn svg { + width: 19px; + height: 19px; + display: block; + pointer-events: none; +} +.delete-server-btn { + background: none; + border: none; + cursor: pointer; + padding: 4px; + margin-left: 0; + align-self: flex-start; + opacity: 0.7; + transition: opacity 0.15s; + display: flex; + align-items: center; + justify-content: center; +} +.delete-server-btn:hover { + opacity: 1; +} +.delete-server-btn svg { + width: 19px; + height: 19px; + display: block; + pointer-events: none; +} +#add-server-btn { + width: 95%; + margin: 10px auto 15px auto; + padding: 10px 0; + border: none; + border-radius: 7px; + font-size: 1em; + font-weight: bold; + cursor: pointer; + transition: background 0.2s, color 0.2s; + display: block; +} + #sidebar-footer { + text-align: left; + margin: 9px 0 0 18px; + font-size: 0.81em; + color: #999; + line-height: 1.33; + padding-bottom: 8px; + } + #sidebar-footer a { + color: #5b8fbd; + text-decoration: none; + font-size: 0.81em; + } + #add-server-btn { + margin-bottom: 7px !important; + } +#main { + flex: 1; + display: flex; + flex-direction: column; +} +#chat-area { + flex: 1; + overflow-y: auto; + padding: 24px 24px 12px 24px; + display: flex; + flex-direction: column-reverse; + font-family: "Fira Mono", "Consolas", "Menlo", "monospace"; + font-size: 1.09rem; + line-height: 1.7; +} +.message { + display: flex; + flex-direction: row; + align-items: flex-start; + padding: 2px 0 2px 0; + white-space: pre-wrap; + word-break: break-word; + min-height: 32px; +} +.nick { + font-weight: bold; + margin-right: 10px; + min-width: 130px; + text-align: left; + flex-shrink: 0; + font-family: inherit; +} +.nick-green {} +.nick-lightred {} +.nick-lightmagenta {} +.nick-gold {} +.nick-cyan {} +.nick-unauth {} +.nick-amogus {} +.msg { + flex: 1; + text-align: left; + font-family: inherit; + white-space: pre-wrap; + word-break: break-word; + padding-right: 16px; +} +.time { + font-size: 0.94em; + margin-left: 0; + min-width: 145px; + text-align: right; + flex-shrink: 0; + font-family: inherit; +} +#chat-input-area { + display: flex; + flex-direction: column; + padding: 10px 18px 16px 18px; + border-top: 1px solid; +} +#chat-input-row { + display: flex; + align-items: center; + gap: 10px; +} +#chat-input { + flex: 1; + padding: 10px 16px; + font-size: 1rem; + border-radius: 8px; + border: none; + outline: none; + margin-right: 6px; + transition: background 0.2s; +} +#send-btn { + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + transition: background 0.2s; + padding: 0 8px 0 8px; + display: flex; + align-items: center; + justify-content: center; + height: 40px; + width: 48px; + min-width: 48px; +} +#send-btn svg { + width: 28px; + height: 28px; + display: block; +} +#server-modal-bg, +#settings-modal-bg { + position: fixed; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + display: none; + z-index: 1000; + align-items: center; + justify-content: center; +} +#server-modal, #settings-modal { + border-radius: 12px; + padding: 26px 28px 18px 28px; + width: 340px; + box-shadow: 0 10px 36px #000a; + display: flex; + flex-direction: column; + gap: 14px; +} +#settings-modal { + width: 400px; + min-height: 240px; +} +#server-modal label, #settings-modal label { + font-size: 1em; + margin-bottom: 2px; +} +#server-modal input, #server-modal select, +#settings-modal input, #settings-modal select { + border-radius: 6px; + padding: 7px 10px; + font-size: 1em; + margin-bottom: 8px; + width: 100%; + box-sizing: border-box; +} +#server-modal .small-row { + display: flex; + gap: 8px; +} +#server-modal .small-row > div { + flex: 1; +} +#server-modal-btns, #settings-modal-btns { + display: flex; + gap: 10px; + justify-content: flex-end; +} +#save-server-btn, #cancel-server-btn, +#save-settings-btn, #cancel-settings-btn { + font-size: 1em; + border-radius: 7px; + border: none; + padding: 8px 18px; + cursor: pointer; + font-weight: bold; +} +.settings-format-presets { + display: flex; + flex-wrap: wrap; + gap: 9px; + margin-bottom: 5px; +} +.settings-format-preset-btn { + border: none; + border-radius: 6px; + padding: 6px 10px; + font-size: 1em; + cursor: pointer; + outline: none; + font-family: inherit; + font-weight: 500; + display: flex; + align-items: center; + transition: background 0.16s, color 0.16s; +} +#settings-format-input { + margin-top: 0px; + margin-bottom: 0px; +} +#settings-modal-lang-row { + display: flex; + align-items: center; + gap: 17px; + margin-bottom: 7px; +} + +/* --- DRAG & DROP --- */ +.channel.dragging { + opacity: 0.5; + border: 2px dashed #99bbee; +} +.channel.drag-over { + background: #e4eaf1 !important; + border: 2px dashed #8db3e6; +} + +@media (max-width: 800px) { + #app-container { + flex-direction: column; + } + #channel-list { + width: 100vw; + border-right: none; + } + #main { + width: 100vw; + } + .nick { + min-width: 90px; + font-size: 0.99em; + } + .time { + min-width: 90px; + font-size: 0.89em; + } + #send-btn { + width: 40px; + min-width: 40px; + height: 36px; + } + #send-btn svg { + width: 22px; + height: 22px; + } + #server-modal, #settings-modal { + width: 95vw; + min-width: 0; + } +} + +#register-server-btn { + background: #3976a1; + color: #fff; + font-size: 1em; + border-radius: 7px; + border: none; + padding: 8px 18px; + cursor: pointer; + font-weight: bold; +} + +#register-server-btn:hover { + background: #2a5c80; +} + +.light-theme #register-server-btn { + background: #5b8fbd; + color: #fff; +} + +.light-theme #register-server-btn:hover { + background: #4a7ba6; +} +#server-modal-btns { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +#server-modal-btns > div { + display: flex; + gap: 10px; +} \ No newline at end of file