import{ Channel }from"./channel.js"; import { Dialog } from "./dialog.js"; import{ Emoji }from"./emoji.js"; import { Guild } from "./guild.js"; import{ Localuser }from"./localuser.js"; import { Member } from "./member.js"; class MarkDown{ txt : string[]; keep:boolean; stdsize:boolean; owner:Localuser|Channel; info:Localuser["info"]; constructor(text : string|string[],owner:MarkDown["owner"],{keep=false,stdsize=false} = {}){ if((typeof text)===(typeof "")){ this.txt=(text as string).split(""); }else{ this.txt=(text as string[]); } if(this.txt===undefined){ this.txt=[]; } this.info=owner.info; this.keep=keep; this.owner=owner; this.stdsize=stdsize; } get localuser(){ if(this.owner instanceof Localuser){ return this.owner; }else{ return this.owner.localuser; } } get rawString(){ return this.txt.join(""); } get textContent(){ return this.makeHTML().textContent; } makeHTML({keep=this.keep,stdsize=this.stdsize}={}){ return this.markdown(this.txt,{keep,stdsize}); } markdown(text : string|string[],{keep=false,stdsize=false} = {}){ let txt : string[]; if((typeof text)===(typeof "")){ txt=(text as string).split(""); }else{ txt=(text as string[]); } if(txt===undefined){ txt=[]; } const span=document.createElement("span"); let current=document.createElement("span"); function appendcurrent(){ if(current.innerHTML!==""){ span.append(current); current=document.createElement("span"); } } for(let i=0;i"&&txt[i+2]===" "){ element=document.createElement("div"); const line=document.createElement("div"); line.classList.add("quoteline"); element.append(line); element.classList.add("quote"); keepys="> "; i+=3; } if(keepys){ appendcurrent(); if(!first&&!stdsize){ span.appendChild(document.createElement("br")); } const build:string[]=[]; for(;txt[i]!=="\n"&&txt[i]!==undefined;i++){ build.push(txt[i]); } try{ if(stdsize){ element=document.createElement("span"); } if(keep){ element.append(keepys); //span.appendChild(document.createElement("br")); } element.appendChild(this.markdown(build,{keep,stdsize})); span.append(element); }finally{ i-=1; continue; } } if(first){ i++; } } if(txt[i]==="\n"){ if(!stdsize){ appendcurrent(); span.append(document.createElement("br")); } continue; } if(txt[i]==="`"){ let count=1; if(txt[i+1]==="`"){ count++; if(txt[i+2]==="`"){ count++; } } let build=""; if(keep){ build+="`".repeat(count); } let find=0; let j=i+count; let init=true; for(;txt[j]!==undefined&&(txt[j]!=="\n"||count===3)&&find!==count;j++){ if(txt[j]==="`"){ find++; }else{ if(find!==0){ build+="`".repeat(find); find=0; } if(init&&count===3){ if(txt[j]===" "||txt[j]==="\n"){ init=false; } if(keep){ build+=txt[j]; } continue; } build+=txt[j]; } } if(stdsize){ build=build.replaceAll("\n",""); } if(find===count){ appendcurrent(); i=j; if(keep){ build+="`".repeat(find); } if(count!==3&&!stdsize){ const samp=document.createElement("samp"); samp.textContent=build; span.appendChild(samp); }else{ const pre=document.createElement("pre"); if(build.at(-1)==="\n"){ build=build.substring(0,build.length-1); } if(txt[i]==="\n"){ i++; } pre.textContent=build; span.appendChild(pre); } i--; continue; } } if(txt[i]==="*"){ let count=1; if(txt[i+1]==="*"){ count++; if(txt[i+2]==="*"){ count++; } } let build:string[]=[]; let find=0; let j=i+count; for(;txt[j]!==undefined&&find!==count;j++){ if(txt[j]==="*"){ find++; }else{ build.push(txt[j]); if(find!==0){ build=build.concat(new Array(find).fill("*")); find=0; } } } if(find===count&&(count!=1||txt[i+1]!==" ")){ appendcurrent(); i=j; const stars="*".repeat(count); if(count===1){ const i=document.createElement("i"); if(keep){ i.append(stars); } i.appendChild(this.markdown(build,{keep,stdsize})); if(keep){ i.append(stars); } span.appendChild(i); }else if(count===2){ const b=document.createElement("b"); if(keep){ b.append(stars); } b.appendChild(this.markdown(build,{keep,stdsize})); if(keep){ b.append(stars); } span.appendChild(b); }else{ const b=document.createElement("b"); const i=document.createElement("i"); if(keep){ b.append(stars); } b.appendChild(this.markdown(build,{keep,stdsize})); if(keep){ b.append(stars); } i.appendChild(b); span.appendChild(i); } i--; continue; } } if(txt[i]==="_"){ let count=1; if(txt[i+1]==="_"){ count++; if(txt[i+2]==="_"){ count++; } } let build:string[]=[]; let find=0; let j=i+count; for(;txt[j]!==undefined&&find!==count;j++){ if(txt[j]==="_"){ find++; }else{ build.push(txt[j]); if(find!==0){ build=build.concat(new Array(find).fill("_")); find=0; } } } if(find===count&&(count!=1||(txt[j+1]===" "||txt[j+1]==="\n"||txt[j+1]===undefined))){ appendcurrent(); i=j; const underscores="_".repeat(count); if(count===1){ const i=document.createElement("i"); if(keep){ i.append(underscores); } i.appendChild(this.markdown(build,{keep,stdsize})); if(keep){ i.append(underscores); } span.appendChild(i); }else if(count===2){ const u=document.createElement("u"); if(keep){ u.append(underscores); } u.appendChild(this.markdown(build,{keep,stdsize})); if(keep){ u.append(underscores); } span.appendChild(u); }else{ const u=document.createElement("u"); const i=document.createElement("i"); if(keep){ i.append(underscores); } i.appendChild(this.markdown(build,{keep,stdsize})); if(keep){ i.append(underscores); } u.appendChild(i); span.appendChild(u); } i--; continue; } } if(txt[i]==="~"&&txt[i+1]==="~"){ const count=2; let build:string[]=[]; let find=0; let j=i+2; for(;txt[j]!==undefined&&find!==count;j++){ if(txt[j]==="~"){ find++; }else{ build.push(txt[j]); if(find!==0){ build=build.concat(new Array(find).fill("~")); find=0; } } } if(find===count){ appendcurrent(); i=j-1; const tildes="~~"; if(count===2){ const s=document.createElement("s"); if(keep){ s.append(tildes); } s.appendChild(this.markdown(build,{keep,stdsize})); if(keep){ s.append(tildes); } span.appendChild(s); } continue; } } if(txt[i]==="|"&&txt[i+1]==="|"){ const count=2; let build:string[]=[]; let find=0; let j=i+2; for(;txt[j]!==undefined&&find!==count;j++){ if(txt[j]==="|"){ find++; }else{ build.push(txt[j]); if(find!==0){ build=build.concat(new Array(find).fill("~")); find=0; } } } if(find===count){ appendcurrent(); i=j-1; const pipes="||"; if(count===2){ const j=document.createElement("j"); if(keep){ j.append(pipes); } j.appendChild(this.markdown(build,{keep,stdsize})); j.classList.add("spoiler"); j.onclick=MarkDown.unspoil; if(keep){ j.append(pipes); } span.appendChild(j); } continue; } } if((!keep)&&txt[i]==="h" && txt[i + 1]==="t" && txt[i + 2]==="t" && txt[i + 3]==="p"){ let build="http"; let j = i+4; const endchars=new Set(["\\", "<", ">", "|", "]"," "]); for(; txt[j] !== undefined; j++){ const char=txt[j]; if(endchars.has(char)){ break; } build+=char; } if(URL.canParse(build)){ appendcurrent(); const a=document.createElement("a"); //a.href=build; MarkDown.safeLink(a,build); a.textContent=build; a.target="_blank"; i=j-1; span.appendChild(a); continue; } } if(txt[i]==="<" && (txt[i + 1]==="@"||txt[i + 1]==="#")){ let id=""; let j = i+2; const numbers=new Set(["0", "1", "2", "3", "4","5","6","7","8","9"]); for(; txt[j] !== undefined; j++){ const char=txt[j]; if(!numbers.has(char)){ break; } id+=char; } if(txt[j]===">"){ appendcurrent(); const mention=document.createElement("span"); mention.classList.add("mentionMD"); mention.contentEditable="false"; const char=txt[i + 1]; i=j; switch(char){ case "@": const user=this.localuser.userMap.get(id); if(user){ mention.textContent=`@${user.name}`; let guild:null|Guild=null; if(this.owner instanceof Channel){ guild=this.owner.guild; } if(!keep){ user.bind(mention,guild); } if(guild){ Member.resolveMember(user,guild).then(member=>{ if(member){ mention.textContent=`@${member.name}`; } }) } }else{ mention.textContent=`@unknown`; } break; case "#": const channel=this.localuser.channelids.get(id); if(channel){ mention.textContent=`#${channel.name}`; if(!keep){ mention.onclick=_=>{ this.localuser.goToChannel(id); } } }else{ mention.textContent=`#unknown`; } break; } span.appendChild(mention); mention.setAttribute("real",`<${char}${id}>`); continue; } } if(txt[i]==="<" && txt[i + 1]==="t" && txt[i + 2]===":"){ let found=false; const build=["<","t",":"]; let j = i+3; for(; txt[j] !== void 0; j++){ build.push(txt[j]); if(txt[j]===">"){ found=true; break; } } if(found){ appendcurrent(); i=j; const parts=build.join("").match(/^$/) as RegExpMatchArray; const dateInput=new Date(Number.parseInt(parts[1]) * 1000); let time=""; if(Number.isNaN(dateInput.getTime())) time=build.join(""); else{ if(parts[3]==="d") time=dateInput.toLocaleString(void 0, {day: "2-digit", month: "2-digit", year: "numeric"}); else if(parts[3]==="D") time=dateInput.toLocaleString(void 0, {day: "numeric", month: "long", year: "numeric"}); else if(!parts[3] || parts[3]==="f") time=dateInput.toLocaleString(void 0, {day: "numeric", month: "long", year: "numeric"}) + " " + dateInput.toLocaleString(void 0, {hour: "2-digit", minute: "2-digit"}); else if(parts[3]==="F") time=dateInput.toLocaleString(void 0, {day: "numeric", month: "long", year: "numeric", weekday: "long"}) + " " + dateInput.toLocaleString(void 0, {hour: "2-digit", minute: "2-digit"}); else if(parts[3]==="t") time=dateInput.toLocaleString(void 0, {hour: "2-digit", minute: "2-digit"}); else if(parts[3]==="T") time=dateInput.toLocaleString(void 0, {hour: "2-digit", minute: "2-digit", second: "2-digit"}); else if(parts[3]==="R") time=Math.round((Date.now() - (Number.parseInt(parts[1]) * 1000))/1000/60) + " minutes ago"; } const timeElem=document.createElement("span"); timeElem.classList.add("markdown-timestamp"); timeElem.textContent=time; span.appendChild(timeElem); continue; } } if(txt[i] === "<" && (txt[i + 1] === ":" || (txt[i + 1] === "a" && txt[i + 2] === ":"))){ let found=false; const build = txt[i + 1] === "a" ? ["<","a",":"] : ["<",":"]; let j = i+build.length; for(; txt[j] !== void 0; j++){ build.push(txt[j]); if(txt[j]===">"){ found=true; break; } } if(found){ const buildjoin=build.join(""); const parts=buildjoin.match(/^<(a)?:\w+:(\d{10,30})>$/); if(parts && parts[2]){ appendcurrent(); i=j; const isEmojiOnly = txt.join("").trim()===buildjoin.trim(); const owner=(this.owner instanceof Channel)?this.owner.guild:this.owner; const emoji=new Emoji({name: buildjoin,id: parts[2],animated: Boolean(parts[1])},owner); span.appendChild(emoji.getHTML(isEmojiOnly)); continue; } } } if(txt[i] == "[" && !keep){ let partsFound=0; let j=i+1; const build=["["]; for(; txt[j] !== void 0; j++){ build.push(txt[j]); if(partsFound === 0 && txt[j] === "]"){ if(txt[j + 1] === "(" && txt[j + 2] === "h" && txt[j + 3] === "t" && txt[j + 4] === "t" && txt[j + 5] === "p" && (txt[j + 6] === "s" || txt[j + 6] === ":") ){ partsFound++; }else{ break; } }else if(partsFound === 1 && txt[j] === ")"){ partsFound++; break; } } if(partsFound === 2){ appendcurrent(); const parts=build.join("").match(/^\[(.+)\]\((https?:.+?)( ('|").+('|"))?\)$/); if(parts){ const linkElem=document.createElement("a"); if(URL.canParse(parts[2])){ i=j; MarkDown.safeLink(linkElem,parts[2]) linkElem.textContent=parts[1]; linkElem.target="_blank"; linkElem.rel="noopener noreferrer"; linkElem.title=(parts[3] ? parts[3].substring(2, parts[3].length - 1)+"\n\n" : "") + parts[2]; span.appendChild(linkElem); continue; } } } } current.textContent+=txt[i]; } appendcurrent(); return span; } static unspoil(e:any) : void{ e.target.classList.remove("spoiler"); e.target.classList.add("unspoiled"); } giveBox(box:HTMLDivElement){ box.onkeydown=_=>{ //console.log(_); }; let prevcontent=""; box.onkeyup=_=>{ const content=MarkDown.gatherBoxText(box); if(content!==prevcontent){ prevcontent=content; this.txt=content.split(""); this.boxupdate(box); } }; box.onpaste=_=>{ if(!_.clipboardData)return; console.log(_.clipboardData.types); const data=_.clipboardData.getData("text"); document.execCommand("insertHTML", false, data); _.preventDefault(); if(!box.onkeyup)return; box.onkeyup(new KeyboardEvent("_")); }; } boxupdate(box:HTMLElement){ const restore = saveCaretPosition(box); box.innerHTML=""; box.append(this.makeHTML({keep: true})); if(restore){ restore(); } } static gatherBoxText(element:HTMLElement):string{ if(element.tagName.toLowerCase()==="img"){ return(element as HTMLImageElement).alt; } if(element.tagName.toLowerCase()==="br"){ return"\n"; } if(element.hasAttribute("real")){ return element.getAttribute("real") as string; } let build=""; for(const thing of element.childNodes){ if(thing instanceof Text){ const text=thing.textContent; build+=text; continue; } const text=this.gatherBoxText(thing as HTMLElement); if(text){ build+=text; } } return build; } static readonly trustedDomains=new Set([location.host]) static safeLink(elm:HTMLElement,url:string){ if(URL.canParse(url)){ const Url=new URL(url); if(elm instanceof HTMLAnchorElement&&this.trustedDomains.has(Url.host)){ elm.href=url; elm.target="_blank"; return; } elm.onmouseup=_=>{ if(_.button===2) return; console.log(":3") function open(){ const proxy=window.open(url, '_blank') if(proxy&&_.button===1){ proxy.focus(); }else if(proxy){ window.focus(); } } if(this.trustedDomains.has(Url.host)){ open(); }else{ const full=new Dialog([ "vdiv", ["title","You're leaving spacebar"], ["text","You're going to "+Url.host+" are you sure you want to go there?"], ["hdiv", ["button","","Nevermind",_=>full.hide()], ["button","","Go there",_=>{open();full.hide()}], ["button","","Go there and trust in the future",_=>{ open(); full.hide(); this.trustedDomains.add(Url.host); }] ] ]); full.show(); } } }else{ throw Error(url+" is not a valid URL") } } static replace(base:HTMLElement,newelm:HTMLElement){ const basechildren=base.children; const newchildren=newelm.children; for(const thing of newchildren){ } } } //solution from https://stackoverflow.com/questions/4576694/saving-and-restoring-caret-position-for-contenteditable-div let text=""; function saveCaretPosition (context){ const selection = window.getSelection(); if(!selection)return; const range = selection.getRangeAt(0); range.setStart(context, 0); text=selection.toString(); let len = text.length+1; for(const str in text.split("\n")){ if(str.length!==0){ len--; } } len+=+(text[text.length-1]==="\n"); return function restore(){ if(!selection)return; const pos = getTextNodeAtPosition(context, len); selection.removeAllRanges(); const range = new Range(); range.setStart(pos.node, pos.position); selection.addRange(range); }; } function getTextNodeAtPosition(root, index){ const NODE_TYPE = NodeFilter.SHOW_TEXT; const treeWalker = document.createTreeWalker(root, NODE_TYPE, elem=>{ if(!elem.textContent)return 0; if(index > elem.textContent.length){ index -= elem.textContent.length; return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; }); const c = treeWalker.nextNode(); return{ node: c? c: root, position: index }; } export{MarkDown};